线性表
分类
顺序表
- 实现
- 一个分配固定长度的数组,两个int变量,分别是maxSize(数组总长度),listSize(已存在元素,数组实际长度)
- 效率
- 对于insert和remove操作(头尾除外),平均来说,需要移动一半元素,故时间代价Φ(n);其他操作Φ(1),如访问元素
- 适合访问操作比修改操作多的场景、以及元素数量固定或大概估计出线性表长度的场景
- 比链表有更高的空间效率,因为没有指针这些结构性开销
- 应用
- Java中把它实现成List接口,规范其具有的行为。最常用的就是实现了该接口的ArrayList类。
- ArrayList类有以下特点:
-
- 私有属性主要是Object[]和size。Object[]的长度length(默认值为10)比size,因为size表示数组实际的大小。
-
- 基于上面一点,与一般的顺序表相比,ArrayList具有扩容ensureCapacity()和缩容trimToSize()的行为
-
- 扩容和缩容是开销很大的行为,在容量不足和容量过多的时候,都会执行数组拷贝,拷贝过程中使用函数arraycopy(Object src, int srcPos, Object dest, int destPos, int length),这是一个native方法,调用C/C++代码,效率很高,所以Java推荐在进行大量数组拷贝的时候使用该方法。
-
- 所以在使用ArrayList时,建议在确定其大小的场景下使用,尽量避免频繁的扩容。
-
- 增删和访问元素就不多说了,跟上面讲的顺序表的特性一样。顺便提一下ArrayList的边界检查和modCount属性。在add()和remove()等涉及越界访问数组元素的操作,会先调用rangeCheck()进行检查,超出则抛出异常IndexOutOfBoundsException;而modCount,是非线程安全的ArrayList为了避免线程的不确定性而引入Fail-Fast机制。Fail-Fast机制
链表
- 实现
- 建立结点类和利用指针,结点类有个好处是能被栈和队列的连接实现方式重用
- 通过增加一个表头结点,避免当链表为空时处理的复杂性,初始状态下,head/curr/tail指针都指向表头结点。
- 效率
- 对于访问操作,平均来说,只能从头遍历链表,时间代价Φ(n)
- 增删指针所指的结点,如栈和队列中经常发生,时间代价Φ(1)
- 但如果增删指定的元素,需要遍历找到该元素,时间代价为Φ(n)
- 应用
-
- 在Java中,最常用的是双向链表LinkedList,继承于AbstractSequentialList,实现 List 接口、Deque 接口。故也可以被当作堆栈、队列或双端队列进行操作
-
- 数据结构是基于双向循环链表的,且头结点中不存放数据,链表空时,previous和next都指向header表头。(一般的单向链表previous和next为null)
-
- 在LinkedList增删元素甚至扩容效率都很高,主要注意修改指针的先后。还有,由于是循环链表,插入位置如果是size,则在头结点前面插入。
-
- 访问元素需要遍历,所以效率不高,Java使用”根据index的大小来决定开始遍历的一端(if (index < (size >> 1)) )“来提高效率。
栈
—— 仅在一端进行插入或删除操作的线性表;元素写入和读出顺序相反,”LIFO”后进先出;单向延伸特性
分类
顺序栈
- 数组最后一个元素的位置作为栈顶(top),进行插入或删除操作,时间代价Φ(1)
- 固定长度,可能浪费空间
链式栈
只能在表头进行插入和删除,不需要空的表头节点
队列
—— 仅在从队尾插入,从队首删除元素的线性表;”FIFO”先进先出
分类
顺序队列(循环队列)
- 实现循环数组,首尾指针变量front/rear(int类型,记录数组索引),初始状态front=1,rear=0
- 通过对size求余%,达到循环遍历数组;front==rear时,认为队列只有一个元素
- 为区分队列是空还是满,选择大小为n+1的数组,只存储n个元素;front-rear=1时,表示队列空,front-rear=2时,表示队列满
链式队列
使用一个空的表头结点,初始化时,front和rear同时指向头结点