首页/数据结构/线性表/单链表的实现 🔗 在 Obsidian 中打开
数据结构 · 线性表

单链表的实现

重要度 ⭐⭐⭐⭐⭐ 线性表单链表链式存储
速查
单链表每个结点含数据域 + 指针域,通过指针把分散的存储单元串起来。带头结点可统一空表与非空表操作;头插法建表逆序、尾插法同序;插入必须先接新结点、再改前驱指针

核心概念

单链表是用一组任意的存储单元存放线性表的元素,通过指针链接各结点。每个结点包含数据域和指针域。

┌────────┬────────┐
│  data  │  next  │
│ 数据域  │ 指针域  │
└────────┴────────┘

typedef struct LNode {
    ElemType data;        // 数据域
    struct LNode *next;   // 指针域
} LNode, *LinkList;

带头结点 vs 不带头结点

带头结点(推荐):

[头结点] → [a₁] → [a₂] → ... → [aₙ] → NULL
  ↑
 L (指向头结点)
  • 头结点不存储有效数据
  • 空表:$L\text{->next == NULL}$
  • 优点:统一了空表和非空表的操作

不带头结点

[a₁] → [a₂] → ... → [aₙ] → NULL
↑
L
  • 空表:$L == NULL$
  • 缺点:插入删除第一个元素需要特殊处理

基本操作

1. 初始化(带头结点)

bool InitList(LinkList *L) {
    *L = (LNode *)malloc(sizeof(LNode));
    if (*L == NULL) return false;
    (*L)->next = NULL;
    return true;
}

2. 头插法建立链表

LinkList List_HeadInsert(LinkList *L) {
    LNode *s;  ElemType x;
    *L = (LNode *)malloc(sizeof(LNode));
    (*L)->next = NULL;
    scanf("%d", &x);
    while (x != 9999) {
        s = (LNode *)malloc(sizeof(LNode));
        s->data = x;
        s->next = (*L)->next;   // 新结点先指向原首元结点
        (*L)->next = s;          // 头结点指向新结点
        scanf("%d", &x);
    }
    return *L;
}
特点元素顺序与输入顺序相反(逆序建表)。

3. 尾插法建立链表

LinkList List_TailInsert(LinkList *L) {
    *L = (LNode *)malloc(sizeof(LNode));
    LNode *s, *r = *L;  // r 为尾指针
    ElemType x;
    scanf("%d", &x);
    while (x != 9999) {
        s = (LNode *)malloc(sizeof(LNode));
        s->data = x;
        r->next = s;
        r = s;             // 尾指针后移
        scanf("%d", &x);
    }
    r->next = NULL;
    return *L;
}
特点元素顺序与输入顺序相同;末尾别忘了 r->next = NULL

4. 按位序插入 ListInsert(&L, i, e)

bool ListInsert(LinkList L, int i, ElemType e) {
    if (i < 1) return false;
    LNode *p = L;       // p 指向头结点
    int j = 0;          // 当前 p 指向第 j 个结点
    while (p != NULL && j < i - 1) { p = p->next; j++; }
    if (p == NULL) return false;   // i 超出范围
    LNode *s = (LNode *)malloc(sizeof(LNode));
    s->data = e;
    s->next = p->next;   // ① 先接
    p->next = s;         // ② 再断
    return true;
}
关键步骤(顺序不能错)$1.\ s\text{->next} = p\text{->next}\ ①$ $2.\ p\text{->next} = s\ ②$。若①②颠倒,会丢失后续链表

5. 删除操作 ListDelete(&L, i, &e)

bool ListDelete(LinkList L, int i, ElemType *e) {
    if (i < 1) return false;
    LNode *p = L;  int j = 0;
    while (p->next != NULL && j < i - 1) { p = p->next; j++; }
    if (p->next == NULL) return false;
    LNode *q = p->next;     // q 指向待删结点
    *e = q->data;
    p->next = q->next;      // 断链
    free(q);
    return true;
}

6. 查找与求表长

LNode *GetElem(LinkList L, int i) {      // 按位查找,O(n)
    if (i < 0) return NULL;
    LNode *p = L;  int j = 0;
    while (p != NULL && j < i) { p = p->next; j++; }
    return p;
}
LNode *LocateElem(LinkList L, ElemType e) {  // 按值查找,O(n)
    LNode *p = L->next;
    while (p != NULL && p->data != e) p = p->next;
    return p;
}
int Length(LinkList L) {                 // 求表长,O(n)
    int len = 0;  LNode *p = L->next;
    while (p != NULL) { len++; p = p->next; }
    return len;
}

手算示例

例 1:头插法建表(输入 1,2,3,4,5)

插入1:L → [1] → NULL
插入2:L → [2] → [1] → NULL
插入3:L → [3] → [2] → [1] → NULL
插入4:L → [4] → [3] → [2] → [1] → NULL
插入5:L → [5] → [4] → [3] → [2] → [1] → NULL

结果:5 → 4 → 3 → 2 → 1(逆序!)

例 2:删除操作模拟

链表:L → [头] → [1] → [2] → [3] → [4] → NULL,删除第 2 个元素:

  1. $p = L$(头结点),$j = 0$
  2. j < 1,$p = p\text{->next} = [1]$,$j = 1$
  3. $p\text{->next} = [2] \neq NULL$
  4. $q = p\text{->next} = [2]$
  5. $e = q\text{->data} = 2$
  6. $p\text{->next} = q\text{->next} = [3]$
  7. free(q)

结果:L → [头] → [1] → [3] → [4] → NULL

常见考法

考法 1输入 1,2,3,4,5,头插法结果 → 5→4→3→2→1;尾插法结果 → 1→2→3→4→5。
考法 2 · 代码填空插入两条关键语句的顺序 → 先 $s\text{->next} = p\text{->next}$,再 $p\text{->next} = s$。
考法 3画出删除某个结点后的链表状态(注意断链与 free)。
考法 4单链表中插入一个元素的时间复杂度 → O(n)(需先找到位置);已知位置则 O(1)。

易错点

易错清单
  1. 插入两条语句顺序:必须先 $s\text{->next} = p\text{->next}$ 再 $p\text{->next} = s$。
  2. 头结点 vs 首结点:头结点是第 0 个,首结点是第 1 个。
  3. 循环条件:插入找第 $i-1$ 个结点,删除找第 $i-1$ 个结点。
  4. 头插法逆序:输入和输出顺序相反,常考。
  5. 尾插法别忘 r->next = NULL

核心结论

操作时间复杂度
按位查找O(n)
按值查找O(n)
插入(已知位置)O(1)
删除(已知位置)O(1)
头插法建表O(n)
尾插法建表O(n)
比较顺序表链表
存储方式连续离散
随机访问O(1) ✅O(n) ❌
插入删除O(n)O(1) ✅(已知位置)
存储密度低(指针开销)

记忆卡片

带头结点单链表的空表判断?
$L\text{->next == NULL}$。
头插法建表的特点?
元素顺序与输入顺序相反。
在 p 后插入 s 的两条关键语句?
$s\text{->next}=p\text{->next}$;$p\text{->next}=s$(顺序不能颠倒)。
删除 p 的后继 q 的核心语句?
$p\text{->next}=q\text{->next};\ \text{free}(q)$。

交互动画 · 头插 / 尾插建表与插入语句顺序

1首元结点 2data|next 3data|next head头结点 s新结点 ① s->next = p->next ② p->next = s
带头结点的单链表:输入序列 1, 2, 3
选择建表方式,观察结点出现的顺序
头插法每次把新结点插到头结点之后 → 链序与输入相反;尾插法用尾指针 r 在表尾追加 → 链序与输入一致。

相关知识点

sequential-list-implementation doubly-and-circular-linked-list linear-list-applications singly-linked-list-operations

↑ 以上为站内 HTML 相对链接(纯网页可浏览);本页右上「在 Obsidian 中打开」跳回源笔记。