2.1 线性表与顺序表

线性表的逻辑定义与基本操作,顺序存储的实现、插入删除的移动规律与复杂度分析。

考查要求 ★★★ 重点掌握 线性表的定义随机存取插入删除移动次数顺序表算法题

线性表的定义

  • a1a_1 是唯一的”第一个”元素,称为表头元素;ana_n 是唯一的”最后一个”元素,称为表尾元素。
  • 除第一个元素外,每个元素有且仅有一个直接前驱;除最后一个元素外,每个元素有且仅有一个直接后继。
  • ii 称为元素 aia_i 在线性表中的位序——注意位序从 1 开始,而 C 数组下标从 0 开始。
12345a₁a₂a₃a₄a₅表头表尾
线性表的逻辑结构:元素一个接一个,一对一的前驱后继关系

线性表的特点:元素个数有限;元素具有逻辑上的顺序性;每个元素都是单个数据元素;数据类型都相同(每个元素占用相同大小的存储空间);表中元素具有抽象性,即仅讨论元素间的逻辑关系,而不考虑元素究竟表示什么内容。

基本操作

InitList(&L);            // 初始化表,构造一个空的线性表
Length(L);               // 求表长
LocateElem(L, e);        // 按值查找
GetElem(L, i);           // 按位查找:获取第 i 个位置的元素
ListInsert(&L, i, e);    // 插入:在第 i 个位置插入元素 e
ListDelete(&L, i, &e);   // 删除:删除第 i 个位置的元素,并用 e 返回
PrintList(L);  Empty(L);  DestroyList(&L);

顺序表:线性表的顺序存储

用一组地址连续的存储单元依次存储线性表中的数据元素,使逻辑上相邻的两个元素在物理位置上也相邻,就得到了顺序表。

设每个元素占 ℓ\ell 个存储单元,第一个元素的地址为 LOC(A)\mathrm{LOC}(A),则

LOC(ai)=LOC(A)+(i−1)×ℓ\mathrm{LOC}(a_i) = \mathrm{LOC}(A) + (i - 1) \times \ell
a₁0a₂1a₃2⋯aᵢi−1⋯aₙn−1LOC(A)LOC(A)+(i−1)·ℓ空闲数组下标 ↑    只需一次计算即可访问任意元素 ⇒ 随机存取
顺序表的存储:知道首地址与元素大小,就能在 O(1) 时间内算出任意元素的地址

因此顺序表最主要的特点是随机访问:通过首地址和元素序号可在 O(1)O(1) 时间内找到指定元素。

类型定义

// 静态分配:数组大小在编译时确定
#define MaxSize 50
typedef struct {
    ElemType data[MaxSize];  // 顺序表的元素
    int length;              // 当前长度
} SqList;

// 动态分配:存储空间在运行时由 malloc 申请
#define InitSize 100
typedef struct {
    ElemType *data;          // 指示动态分配数组的指针
    int MaxSize, length;
} SeqList;
// 初始化:L.data = (ElemType*)malloc(sizeof(ElemType) * InitSize);

基本操作的实现

下面的动画演示了三种核心操作。试着在不同位置插入、删除,观察需要移动的元素个数如何随位置变化:

动画演示顺序表的插入、删除与查找
L.data[MaxSize](下标从 0 开始;位序 i 从 1 开始)0123456789122537415668length
正在移动新元素被删元素空闲空间
C 代码
1bool ListInsert(SqList &L, int i, ElemType e) {
2 if (i < 1 || i > L.length + 1) // 判断 i 是否合法
3 return false;
4 if (L.length >= MaxSize) // 存储空间已满
5 return false;
6 for (int j = L.length; j >= i; j--)
7 L.data[j] = L.data[j - 1]; // 后移
8 L.data[i - 1] = e; // 放入 e
9 L.length++;
10 return true;
11}
1/1顺序表 L 当前有 6 个元素,MaxSize = 10。选择操作并点击「执行」。
L.length6

插入:从后往前,依次后移

在第 ii 个位置插入元素 e,需要把第 ii 个到第 nn 个元素依次后移一位,空出位置 ii 再放入 e。注意必须从表尾开始移动,否则会覆盖尚未移动的元素。

  • 合法的插入位置为 1≤i≤n+11 \le i \le n + 1(可以插在表尾之后)。
  • 最好情况:在表尾插入(i=n+1i = n + 1),不移动元素,O(1)O(1)。
  • 最坏情况:在表头插入(i=1i = 1),移动 nn 个元素,O(n)O(n)。
  • 平均情况:设在每个位置插入的概率相同,pi=1n+1p_i = \frac{1}{n+1},则平均移动次数为
∑i=1n+1pi(n−i+1)=1n+1∑i=1n+1(n−i+1)=1n+1⋅n(n+1)2=n2\sum_{i=1}^{n+1} p_i (n - i + 1) = \frac{1}{n+1} \sum_{i=1}^{n+1}(n - i + 1) = \frac{1}{n+1} \cdot \frac{n(n+1)}{2} = \frac{n}{2}

删除:从前往后,依次前移

删除第 ii 个元素,需要把第 i+1i+1 到第 nn 个元素依次前移一位。

  • 合法的删除位置为 1≤i≤n1 \le i \le n。
  • 最好 O(1)O(1)(删表尾),最坏 O(n)O(n)(删表头)。
  • 平均移动次数:∑i=1n1n(n−i)=n−12\displaystyle\sum_{i=1}^{n} \frac{1}{n}(n - i) = \frac{n-1}{2}。

查找

  • 按位查找 GetElem(L, i):直接返回 L.data[i-1],时间复杂度 O(1)O(1)。
  • 按值查找 LocateElem(L, e):顺序比较,平均比较次数 n+12\frac{n+1}{2},时间复杂度 O(n)O(n)。

顺序表的优缺点

优点缺点
随机访问,按位查找 O(1)O(1)插入、删除平均要移动一半元素
存储密度高(只存数据,不存指针)需要连续的大块空间,扩容代价大
实现简单,缓存友好静态分配时容量难以预估

算法设计题精讲

408 的算法题通常要求:①给出基本设计思想;②用 C/C++ 写出算法;③说明时间与空间复杂度。下面两道是顺序表的经典题型。

例题 1(408 真题 2010)

设将 nn(n>1n > 1)个整数存放到一维数组 R 中。设计一个在时间和空间两方面都尽可能高效的算法,将 R 中保存的序列循环左移 pp(0<p<n0 < p < n)个位置,即将 (X0,X1,…,Xn−1)(X_0, X_1, \ldots, X_{n-1}) 变换为 (Xp,Xp+1,…,Xn−1,X0,X1,…,Xp−1)(X_p, X_{p+1}, \ldots, X_{n-1}, X_0, X_1, \ldots, X_{p-1})。

查看解答

设计思想:三次逆置。把数组看成 ABAB 两段(AA 为前 pp 个元素,BB 为其余 n−pn-p 个),要得到 BABA。利用 (A−1B−1)−1=BA(A^{-1}B^{-1})^{-1} = BA:先逆置 AA,再逆置 BB,最后逆置整个数组。

例如 abcdefgh 左移 3 位:cba hgfed → cbahgfed → 整体逆置得 defghabc。

void Reverse(int R[], int from, int to) {
    for (int i = 0; i < (to - from + 1) / 2; i++) {
        int temp = R[from + i];
        R[from + i] = R[to - i];
        R[to - i] = temp;
    }
}
void Converse(int R[], int n, int p) {
    Reverse(R, 0, p - 1);
    Reverse(R, p, n - 1);
    Reverse(R, 0, n - 1);
}

三次 Reverse 的时间分别为 O(p/2)O(p/2)、O((n−p)/2)O((n-p)/2)、O(n/2)O(n/2),总时间复杂度 O(n)O(n),空间复杂度 O(1)O(1)。

例题 2:删除所有值为 x 的元素

对长度为 nn 的顺序表 L,编写一个时间复杂度为 O(n)O(n)、空间复杂度为 O(1)O(1) 的算法,删除表中所有值为 x 的数据元素。

查看解答

设计思想:用 k 记录不等于 x 的元素个数,边扫描边把不等于 x 的元素前移到下标 k 的位置,最后令表长为 k。每个元素最多移动一次。

void DeleteX(SqList &L, ElemType x) {
    int k = 0;
    for (int i = 0; i < L.length; i++)
        if (L.data[i] != x)
            L.data[k++] = L.data[i];
    L.length = k;
}

这种”快慢双指针”思想在顺序表题中极为常用:i 负责扫描,k 负责写入。

即时练习

即时练习5 题
点击选项即可作答
  1. 1
    线性表的顺序存储结构是一种( )。
  2. 2
    在长度为 n 的顺序表中,在第 i(1 ≤ i ≤ n+1)个位置插入一个新元素,需要向后移动( )个元素。
  3. 3
    对于顺序表,访问第 i 个位置的元素和在第 i 个位置插入一个元素的时间复杂度分别为( )。
  4. 4
    若长度为 n 的非空线性表采用顺序存储结构,删除第 i 个元素时,i 的合法值是( )。
  5. 5
    设线性表有 n 个元素,严格说来,以下操作中,( )在顺序表上实现要比在链表上实现的效率高。
    Ⅰ. 输出第 i(1 ≤ i ≤ n)个元素值
    Ⅱ. 交换第 3 个元素与第 4 个元素的值
    Ⅲ. 顺序输出这 n 个元素的值
学完了吗?

标记完成后,侧栏会显示进度。进度仅保存在本浏览器中。