跳转至
跳转到正文

2. 抽象数据类型与线性表

章节导引

本页从《FDS 数据结构基础讲义》拆分而来,保留原章节锚点,方便从旧总览页和旧链接跳转。

学习目标

区分接口和实现,理解数组与链表在访问、插入、删除上的取舍。

前置知识

结构体/指针或引用、数组和基础动态内存直觉。

建议用时

建议 5–6 小时:ADT 1 小时,数组/链表 3 小时,多项式/游标 1–2 小时。

练习建议

写出线性表 ADT;比较数组表和链表复杂度;手画一次链表插入和删除。

参考资料与引用边界

  • 整理者:Lumner。
  • 课程来源:根据 FDS/ 目录下的课件整理;本章对应课件:FDS/DS02_Ch03_List.ppt
  • 原始讲义文件:note/FDS_数据结构基础讲义.md
  • 引用边界:这是公开学习笔记,不替代课程正式教材、教师课件或考试要求;外部引用时请注明来自本网站整理版。

2.1 ADT:把“是什么”和“怎么做”分开

数据类型可以看成 {对象集合} + {操作集合}。抽象数据类型 ADT 更强调接口和语义,而不是具体实现。

以线性表为例:

  • 对象:\((item_0,item_1,\ldots,item_{N-1})\)
  • 操作:求长度、打印、查找第 k 个元素、查找某元素位置、插入、删除等。
  • 实现:数组、链表、游标链表都可以实现同一个 List ADT。

这就是抽象的价值:用户只关心 InsertDeleteFind 的行为;实现者负责选择结构并控制复杂度。

2.2 数组实现线性表

数组实现使用连续内存,天然支持随机访问:

操作 复杂度 原因
Find_Kth(k) \(O(1)\) 地址可直接计算
Find(x) \(O(N)\) 需要顺序扫描
在末尾插入 平均可为 \(O(1)\) 若容量足够
在中间插入 \(O(N)\) 后续元素需要右移
删除中间元素 \(O(N)\) 后续元素需要左移

数组的主要问题是容量需要预估,扩容需要搬迁元素;中间插入删除代价高。

2.3 链表实现线性表

链表由节点组成,每个节点保存数据和指向下一节点的指针。

typedef struct Node *PtrToNode;
struct Node {
    ElementType Element;
    PtrToNode Next;
};
typedef PtrToNode List;
typedef PtrToNode Position;

使用头节点可以统一空表、首元节点插入和普通插入的处理。

插入

在位置 P 后插入 X

void Insert(ElementType X, List L, Position P) {
    Position TmpCell = malloc(sizeof(struct Node));
    if (TmpCell == NULL)
        FatalError("Out of space");
    TmpCell->Element = X;
    TmpCell->Next = P->Next;
    P->Next = TmpCell;
}

链表插入本身是 \(O(1)\),但前提是已经拿到位置 P。如果还要先查找 P,总复杂度会包含查找的 \(O(N)\)

删除

删除值为 X 的第一个节点:

void Delete(ElementType X, List L) {
    Position P = FindPrevious(X, L);
    if (!IsLast(P, L)) {
        Position TmpCell = P->Next;
        P->Next = TmpCell->Next;
        free(TmpCell);
    }
}

删除需要找到前驱节点,因为单链表无法从当前节点直接回到前一个节点。

2.4 双向循环链表

双向链表节点有 PrevNext 两个方向。循环链表让尾节点指向头节点,使“从末尾回到开头”变成自然操作。

适合场景:

  • 需要频繁向前、向后移动。
  • 需要在已知节点前后插入删除。
  • 需要轮转处理任务,例如循环调度。

代价:

  • 每个节点多一个指针,空间增加。
  • 插入删除时要维护两条链接,代码更容易写错。

2.5 多项式 ADT

多项式可表示为若干项 <指数, 系数>

\(P(x)=a_1x^{e_1}+a_2x^{e_2}+\cdots+a_nx^{e_n}\)

两种常见表示:

表示 适合场景 缺点
系数数组 最高次数不大且较密集 稀疏多项式浪费空间
按指数有序链表 稀疏多项式 查找某次项较慢

多项式相加的核心是“归并”:

while (P != NULL && Q != NULL) {
    if (P->Exponent > Q->Exponent) {
        Attach(P->Coefficient, P->Exponent, &Rear);
        P = P->Next;
    } else if (P->Exponent < Q->Exponent) {
        Attach(Q->Coefficient, Q->Exponent, &Rear);
        Q = Q->Next;
    } else {
        int sum = P->Coefficient + Q->Coefficient;
        if (sum != 0)
            Attach(sum, P->Exponent, &Rear);
        P = P->Next;
        Q = Q->Next;
    }
}

这和合并两个有序表类似,复杂度为两个多项式项数之和。

2.6 多重链表与稀疏矩阵

多重链表用于一个节点同时属于多个逻辑链表。例如课程注册系统:

  • 从课程角度:一门课连接所有选课学生。
  • 从学生角度:一个学生连接所有已选课程。

同一个“选课关系”节点可以同时挂在课程链和学生链上,避免重复存储关系。

稀疏矩阵也常用类似思想:只存非零元素,并为行、列分别建立链接。若非零元素个数为 \(K\),空间就能从 \(O(RC)\) 降到 \(O(K)\)

2.7 游标实现链表

某些语言或场景没有指针,可以用数组下标模拟指针。课件称为 cursor implementation。

typedef int PtrToNode;
typedef PtrToNode List;
typedef PtrToNode Position;

struct Node {
    ElementType Element;
    Position Next;
};

struct Node CursorSpace[SpaceSize];

CursorSpace[i].Next 存下一个节点的下标。数组 0 号位置通常作为空闲链表头,模拟 mallocfree

核心思想:指针本质上也是“地址”。当真实地址不可用时,下标也可以承担连接关系。