数据结构java
但凡IT江湖侠士,算法与数据结构为必修之课。早有前辈已经明确指出:程序=算法+数据结构 。要想在以后的江湖历练中通关,数据结构必不可少。数据结构与算法相辅相成,亦是阴阳互补之法。golang
开篇web
说道数组,几乎每一个IT江湖人士都不陌生,甚至过半人还会很自信觉的它很简单。 的确,在菜菜所知道的编程语言中几乎都会有数组的影子。不过它不只仅是一种基础的数据类型,更是一种基础的数据结构。若是你觉的对数组足够了解,那能不能回答一下:算法
数组的本质定义?编程
数组的内存结构?c#
数组有什么优点?数组
数组有什么劣势?缓存
数组的应用场景?服务器
数组为何大部分都从0开始编号?数据结构
数组可否用其余容器来代替,例如c#中的List<T>?
定义
所谓数组,是相同的元素序列。数组是在程序设计中,为了处理方便,把具备相同类型的若干元素按无序的形式组织起来的一种形式。
——百科
正如以上所述,数组在应用上属于数据的容器。不过我仍是要补充两点:
1. 数组在数据结构范畴属于一种线性结构,也就是只有前置节点和后续节点的数据结构,除数组以外,像咱们平时所用的队列,栈,链表等也都属于线性结构。
有线性结构固然就有非线性结构,好比以后咱们要介绍的二叉树,图 等等,这里再也不展开~~~
2. 数组元素在内存分配上是连续的。这一点对于数组这种数据结构来讲很是重要,甚至能够说是它最大的“杀手锏”。下边会有更详细的介绍。
优点和劣势
优点
我相信全部人在使用数组的时候都知道数组能够按照下标来访问,例如 array[1] 。做为一种最基础的数据结构是什么使数组具备这样的随机访问方式呢?天性聪慧的你可能已经想到了:内存连续+相同数据类型。
如今咱们抽象一下数据在内存上分配的情景。
1. 说到数组按下标访问,不得不说一下大多数人的一个“误解”:数组适合查找元素。为何说是误解呢,是由于这种说法不够准确,准确的说数组适合按下标来查找元素,并且按照下标查找元素的时间复杂度是O(1)。为何呢?咱们知道要访问数组的元素须要知道元素在内存中对应的内存地址,而数组指向的内存的地址为首元素的地址,即:array[0]。因为数组的每一个元素都是相同的类型,每一个类型占用的字节数系统是知道的,因此要想访问一个数组的元素,按照下标查找能够抽象为:
array[n]=array[0]+size*n
以上是元素地址的运算,其中size为每一个元素的大小,若是为int类型数据,那size就为4个字节。其实确切的说,n的本质是一个离首元素的偏移量,因此array[n]就是距离首元素n个偏移量的元素,所以计算array[n]的内存地址只需以上公式。
论证一下,若是下标从1开始计算,那array[n]的内存地址计算公式就会变为:
array[n]=array[0]+size*(n-1)
对比很容易发现,从1开始编号比从0开始编号每次获取内存地址都多了一次 减法运算,也就多了一次cpu指令的运行。这也是数组从0下标开始访问一个缘由。
其实还有一种可能性,那就是全部现代编程语言的鼻祖:C语言,它是从0开始计数下标的,因此如今全部衍生出来的后代语言也就延续了这个传统。虽然不符合人类的思想,可是符合计算机的原理。固然也有一些语言能够设置为不从下标0开始计算,这里再也不展开,有兴趣的能够去搜索一下。
2. 因为数组的连续性,因此在遍历数组的时候很是快,不只得益于数组的连续性,另外也得益于cpu的缓存,由于cpu读取缓存只能读取连续内存的内容,因此数组的连续性正好符合cpu缓存的指令原理,要知道cpu缓存的速度要比内存的速度快上不少。
劣势
1. 因为数组在内存排列上是连续的,并且要保持这种连续性,因此当增长一个元素或删除一个元素的时候,为了保证连续性,须要作大量元素的移动工做。
举个栗子:要在数组头部插入一个新元素,为了在头部腾出位置,全部的元素都要后移一位,假设元素个数为n,这就致使了时间复杂度为O(n)的一次操做,固然若是是在数组末尾插入新元素,其余全部元素都没必要移动,操做的时间复杂度为O(1)。
固然这里有一个技巧:若是你的业务要求并非数组连续有序的,当在位置k插入元素的时候,只须要把k元素转移到数组末尾,新元素插入到k位置便可。固然仔细沉思一下这种业务场景可能性过小了,数组均可以无序,我直接插入末尾便可,没有必要非得在k位置插入把。~~
固然还有一个特殊场景:若是是屡次连续的k位置插入操做,咱们彻底能够合并为一次“批量插入”操做:把k以后的元素总体移动sum(插入次数)个位置,无需一个个位置移动,把三次操做的时间复杂度合并为一次。
与插入对应的就有删除操做,同理,删除操做数组为了保持连续性,也须要元素的移动。
综上所述,数组在添加和删除元素的场景下劣势比较明显,因此在具体业务场景下应该避免频繁添加和删除的操做。
2. 数组的连续性就要求建立数组的时候,内存必须有相应大小的连续区块,若是不存在,数组就有可能出现建立失败的现象。在某些高级语言中(好比c#,golang,java)就有可能引起一次GC(垃圾回收)操做,GC操做在系统运行中是很是昂贵的,有的语言甚至会挂起全部线程的操做,对外的表现就是“暂停服务”。
3. 数组要求全部元素为同一个类型。在存储数据维度,它可能算是一种劣势,可是为了按照下标快速查找元素,业务中这也是一种优点。仁者见仁智者见智而已。
4. 数组是长度固定的数据结构,因此在原始数组的基础上扩容是不可能的,有的语言可能实现数组的“伪扩容”,为何说是“伪”呢,由于原理实际上是建立了一个容量更大的数组来存放原数组元素,发生了数据复制的过程,只不过对于调用者而已透明而已。
5. 数组有访问越界的可能。咱们按照下标访问数组的时候若是下标超出了数组长度,在现代多数高级语言中,直接就会引起异常了,可是一些低级语言好比C 有可能会访问到数组元素之外的数据,由于要访问的内存地址确实存在。
其余
不少编程语言中你会发现“纯数组”并无提供直接删除元素的方法(例如:c#,golang),而是须要将数组转化为另外一种数据结构来实现数组元素的删除。好比在golang种能够转化为slice。这也验证了数组的不变性。
咱们学习的每一个数据结构其实都有对应的适合场景,只不过是场景多少的问题,具体何时用,须要咱们对该数据结构的特性作深刻分析。
关于数组的特性,经过以上介绍能够知道最大的一个亮点就是按照下标访问,那有没有具体业务映射这种特性呢?
1. 相信不少IT人士都遇到过会员机制,每一个会员到达必定的经验值就会升级,怎么判断当前的经验是否到达升级条件呢?咱们是否是能够这样作:好比当前会员等级为3,判断是否到达等级4的经验值,只须要array[4]的值判断便可,大多数人把配置放到DB,资源耗费太严重。也有的人放到其余容器缓存。可是大部分场景下查询的时间复杂度要比数组大不少。
2. 在分布式底层应用中,咱们会有利用一致性哈希方案来解决每一个请求交给哪一个服务器去处理的场景。有兴趣的同窗能够本身去研究一下。其中有一个环节:根据哈希值查找对应的服务器,这是典型的读多写少的应用,并且比较偏底层。若是用其余数据结构来解决大量的查找问题,可能会触碰到性能的瓶颈。而数据按下标访问时间复杂度为O(1)的特性,使得数组在相似这些应用中很是普遍。