1、线性表的定义
所谓线性表就像是一条线一样连在一起的数据结构,它的严格的定义是:在有限非空的集合中,存在唯一的被称为 ”第一个“ 的数据元素;存在唯一的被称为 ”最后一个“ 的数据元素;除了第一个数据元素外,集合中所有元素有且仅有唯一的前驱元素;除了最后一个元素外,集合中有且仅有唯一的后继元素。
2、线性表的顺序表示
线性表的顺序表示我们简称为顺序表,它表示用一组地址连续的的内存单元一次存储元素,从这个定义我们可以知道这种存储方式实际上是用屋里地址上的连续性来表示数据元素之间的逻辑相邻性。由此可知只要确定了线性表的起始位置,顺序表中的任何一个元素都可以随机存取,所以顺序表是一种支持随机存取的数据结构。这是不是就是我们的数组,没错,很多编程语言中的数组就是顺序表实现的一种实现形式。我们用一个图示来说明
从上面的图示中,可以知道实际上顺序表就是在内存中分配一块连续的内存区域,然后将这块内存区域的首地址作为基地址。假如我们有一个指针,那么我们不断调整指针的位置就能遍历这个表中的数据,因此我们可以得出一个寻址的公式 L(i) = L(base_address) + (i-1)data_size,i表示表中第i个元素,base_address表示基地址,data_size表示存储数据元素所占的字节大小。
数组我们都不陌生,数组有一个特性就是它的下标都是从0开始的,为什么要这样设计呢?从人的理解角度来看从1开始不是更符合我们的认知吗?其实下标应该更准确地称之为 ”偏移量“ ,也就是说假如第一个元素我们说它是基于首地址偏移为0的位置,第二个元素就是基于首地址偏移为1的位置,因此当我们在随即存取某一个位置的元素时,通过下标也就是偏移量我们能很快地计算得到L(offset) = L(base_address) + offset * data_size,可以看到数组元素在存取时其时间复杂度为O(1)。
借助数组我们可以很快地实现一个顺序表的结构;
#define LIST_INIT_SIZE 100 // 表的初始分配量
#define LISTINCREMENT 10 // 扩容增量
typedef struct {
ElemType *elem;// 存储空间的基地址
int length; // 当前的长度
int list_size; // 当前分配的存储容量(以sizeof(ElemType为单位))
}SqList;
// 初始化顺序表
status InitList_Sq(SqList &L) {
//构造一个空顺序表
L.elem = (ElemType *)malloc(LIST_INIT_SIZE * sizeof(ElemType));
if (!L.elem) exit(OVERFLOW);
L.length = 0;
L.list_size = LIST_INIT_SIZE;
return OK;
}
3、顺序表的插入与删除
数组在内存中的连续性为元素的随机存取带来很大的方便可以实现在O(1)的时间复杂度情况下存取元素,但是凡事都有两面性,这种特性为数据的存取带来便利,而对于数据的插入和删除两种操作基很麻烦了。
我们假设数组的长度为n,现在我们想要在第 i 个位置插入一个元素。因此为了保证元素的插入,我们必须为这个元素挪出空间来,这就要求我们必须将后面 i ~n 个元素都顺序往后移动一位,如果这样那么时间复杂度就为O(n). 但是如果是在数组的末尾插入元素复杂度为O(1).
如果数组中的元素是有序的,在某个位置插入元素时,就必须按照上面的方法搬动元素,但是如果数组中的元素无序,数组只是被用来当作一个集合使用,这种情况下为了避免大规模搬动数据,我们可以这样做:将原来位置 i 上的元素移动到数组最后一个位置上,而将新元素插入到位置 i 上,这样就可以避免大量搬运操作。举个例子,假如现在有一个长度为10的数组,里面存储了5个元素,现在我们需要将一个新的元素x插入第3个位置,为了避免大规模移动我们可以这样做:
class Solution {
public void moveElement(int[] arry, int x) {
arry[5] = arry[2];
arry[2] = x;
}
}
与插入操作类似,如果我们要删除第 i 个位置上的元素,为了内存的连续性,我们就必须将后面的元素依次往前挪动位置,同样,如果是在数组末尾删除一个元素,时间复杂度为O(1),但如果在数组的首部删除元素我们就需要将后面所有元素往前移动移动一个位置,时间复杂度为O(n).
可以想象一下这个场景,假如我们需要在一个数组中依次删除某几个元素,根据数组的特性我们知道,每删除一个元素,都会搬移元素位置。那我们可以采用这样的方式,我们可以先记录下已经删除的元素,每次进行删除操作时并不是真正的删除,只是进行了标记,当数组没有更多的空间时,我们再触发执行一次真正的删除操作。这不就可以大大降低移动元素的操作吗。