一、顺序表的设计思路
顺序表是采取顺序存储方式的一种线性表实现。所谓的“顺序存储方式”,就是指采用一块连续的内存来保存数据。这样一来,顺序表中元素的邻接关系就可以直接由内存的连续性来保证;简而言之,逻辑上相邻的元素,在其物理地址上也是相邻的。
为了保证表中元素的逻辑关系,顺序表的实现必须保证表中的元素在这块连续的内存中依次排布,并且中间不能存在空缺的内存单元,因为空缺的内存会导致表中元素在逻辑上不连续。
这样一来,只要知道了顺序表中某个元素在表中的下标位置以及其内存地址,就可以获取顺序表中的每个元素。更具体的说,如果已知顺序表中元素的大小为elem
,且下标为i
元素的内存地址为addr
,则顺序表中下标为j
元素的内存地址为:
a
d
d
r
−
(
i
−
j
)
×
s
i
z
e
o
f
(
e
l
e
m
)
addr - (i - j) × sizeof(elem)
addr−(i−j)×sizeof(elem)
如何编程实现顺序表呢?显然,高级语言中使用数组就可以简单表示顺序表的结构;但是,在定义数组时必须使用常量表达式指定其容量。这就意味着,使用常规数组实现的顺序表只能存储固定数量的元素;当线性表中元素的数量达到数组的长度时,后续的添加操作将失败,除非在添加操作之前先删除掉现有的某个元素。
为了支持动态长度的线性表,当线性表中元素的数量达到数组的长度时,必须重新分配一个更大容量的数组,并将现有数组中的元素依次复制到新的数组中,最后再释放掉现有的数组,这就是顺序表的扩容操作。
一种经典的顺序表实现采取“数组+长度+容量”的方式:
- 使用一个数组来表示连续的内存,并在其中容纳实际元素。
- 使用一个值
size
表示当前数组中有效元素的个数。 - 使用一个值
capacity
表示当前数组的最大容量。
当size
与capacity
满足某种关系时,就执行扩容操作。
在Java集合框架中,当
size == capacity
时进行1.5倍扩容;在某些C++ STL实现中,当size == 0.75 * capacity
时进行2倍扩容,其中0.75
被称为装载因子。
二、顺序表的插入操作
为了满足线性表的要求,在插入新元素之前,需要把指定位置的现有元素及其所有后继元素全部向后搬移一个位置,然后将新元素放在指定位置即可。以下是在线性表的下标1
位置添加元素999
的操作过程。
不难看出,如果在顺序表的末尾插入元素,则无需搬移任何元素,直接插入新元素即可,时间复杂度为O(1)
;而在其他位置插入元素时,必须要进行元素搬移,时间复杂度为O(n)
。
三、顺序表的删除操作
为了满足线性表的要求,在删除某元素之前,需要把指定位置的所有后继元素全部向前搬移一个位置,然后删除表尾的冗余元素即可。以下是在线性表的下标1
位置删除元素26
的操作过程。
不难看出,如果在顺序表的末尾删除元素,则无需搬移任何元素,直接删除元素即可,时间复杂度为O(1)
;而在其他位置删除元素时,必须要进行元素搬移,时间复杂度为O(n)
。
四、顺序表的简单查找操作
对于一个无序的顺序表,往往采用简单地顺序查找操作,即从第一个元素开始依次遍历顺序表中的所有元素,直到遇到第一个与给定值匹配的元素位置,并返回此元素的下标。例如以下查找元素93
的操作将返回3
:
五、总结
顺序表是使用顺序存储结构的一种线性表实现,它具有以下特性:
- 支持元素的随机访问
- 表中元素的存储密度高,每个节点只存储元素本身
- 扩展容量不方便
- 在表尾添加或删除元素极快,但在其它位置插入或删除元素很慢