【数据结构与算法】第6篇:线性表(二):单链表的实现(头插法、尾插法)

一、什么是单链表

单链表是一系列节点通过指针连接而成的数据结构。每个节点包含两部分:

  • 数据域:存储数据

  • 指针域:指向下一个节点

最后一个节点的指针指向NULL,表示链表结束。

画个图:

text

复制代码
head → [data|next] → [data|next] → [data|next] → NULL

与顺序表的对比

  • 顺序表:连续内存,随机访问快,插入删除慢

  • 单链表:非连续内存,随机访问慢,插入删除快


二、节点的结构定义

2.1 结构体自引用

单链表的节点定义中,指针域指向的是同类型的节点,这叫结构体自引用。

c

复制代码
typedef struct Node {
    int data;           // 数据域
    struct Node *next;  // 指针域,指向下一个节点
} Node, *PNode;

注意 :这里必须写 struct Node *next,不能写成 Node *next。因为在 typedef 还没生效的时候,Node 这个类型名还不存在。

2.2 链表结构

为了方便管理,我们还可以定义一个链表结构体:

c

复制代码
typedef struct {
    PNode head;    // 头指针
    int size;      // 节点个数
} LinkedList;

当然,也可以只用一个 head 指针来管理链表。但加上 size 可以方便地获取长度,不用每次都遍历。


三、头结点(哑结点)的作用

链表的头指针有两种处理方式:

方式一:不带头结点

text

复制代码
head → [节点1] → [节点2] → NULL
  • head 直接指向第一个有效节点

  • 空链表时 head = NULL

方式二:带头结点(哑结点)

text

复制代码
head → [哑结点|next] → [节点1] → [节点2] → NULL
  • head 指向一个不存数据的头结点

  • 头结点的 next 指向第一个有效节点

  • 空链表时,头结点的 next = NULL

带头结点的好处

  1. 统一操作:插入和删除第一个节点时,不用单独处理。代码更简洁。

  2. 空链表有固定状态 :不用判断 head == NULL,只需要判断 head->next == NULL

本专栏采用带头结点的方式,代码会更统一。


四、基本操作实现

4.1 初始化

c

复制代码
void initList(LinkedList *list) {
    // 创建头结点(哑结点)
    list->head = (PNode)malloc(sizeof(Node));
    if (list->head == NULL) {
        printf("初始化失败\n");
        exit(1);
    }
    list->head->next = NULL;  // 空链表
    list->size = 0;
}

4.2 创建新节点

插入操作中经常要创建新节点,写一个辅助函数:

c

复制代码
PNode createNode(int value) {
    PNode newNode = (PNode)malloc(sizeof(Node));
    if (newNode == NULL) {
        printf("创建节点失败\n");
        return NULL;
    }
    newNode->data = value;
    newNode->next = NULL;
    return newNode;
}

4.3 头插法

在链表头部插入节点(第一个有效节点的位置)。

c

复制代码
void insertAtHead(LinkedList *list, int value) {
    PNode newNode = createNode(value);
    if (newNode == NULL) return;
    
    newNode->next = list->head->next;  // 新节点指向原来的第一个节点
    list->head->next = newNode;         // 头结点指向新节点
    list->size++;
}

画图理解:

text

复制代码
插入前:
head → [哑结点] → [A] → [B] → NULL

插入新节点X:
1. X->next = A
2. 哑结点->next = X

结果:
head → [哑结点] → [X] → [A] → [B] → NULL

4.4 尾插法

在链表尾部插入节点。

c

复制代码
void insertAtTail(LinkedList *list, int value) {
    PNode newNode = createNode(value);
    if (newNode == NULL) return;
    
    // 找到最后一个节点
    PNode cur = list->head;
    while (cur->next != NULL) {
        cur = cur->next;
    }
    
    cur->next = newNode;
    list->size++;
}

尾插法需要遍历到末尾,时间复杂度O(n)。如果经常尾插,可以维护一个尾指针优化。

4.5 指定位置插入

在索引 pos 处插入(pos从0开始,0表示第一个有效节点)。

c

复制代码
int insertAt(LinkedList *list, int pos, int value) {
    if (pos < 0 || pos > list->size) {
        printf("插入位置不合法\n");
        return -1;
    }
    
    // 找到要插入位置的前一个节点
    PNode prev = list->head;
    for (int i = 0; i < pos; i++) {
        prev = prev->next;
    }
    
    PNode newNode = createNode(value);
    if (newNode == NULL) return -1;
    
    newNode->next = prev->next;
    prev->next = newNode;
    list->size++;
    
    return 0;
}

关键点 :插入操作的关键是找到前驱节点。在单链表中,只有知道了前驱节点,才能把新节点链进去。

4.6 删除指定位置

c

复制代码
int deleteAt(LinkedList *list, int pos) {
    if (pos < 0 || pos >= list->size) {
        printf("删除位置不合法\n");
        return -1;
    }
    
    // 找到要删除节点的前一个节点
    PNode prev = list->head;
    for (int i = 0; i < pos; i++) {
        prev = prev->next;
    }
    
    PNode toDelete = prev->next;
    int value = toDelete->data;
    prev->next = toDelete->next;
    free(toDelete);
    list->size--;
    
    return value;
}

画图理解删除:

text

复制代码
删除前:
... → [prev] → [toDelete] → [next] → ...

操作:
prev->next = toDelete->next
free(toDelete)

结果:
... → [prev] → [next] → ...

4.7 按值查找

c

复制代码
int find(LinkedList *list, int value) {
    PNode cur = list->head->next;  // 跳过哑结点
    int pos = 0;
    while (cur != NULL) {
        if (cur->data == value) {
            return pos;
        }
        cur = cur->next;
        pos++;
    }
    return -1;
}

4.8 遍历打印

c

复制代码
void print(LinkedList *list) {
    PNode cur = list->head->next;  // 跳过哑结点
    printf("size=%d, [", list->size);
    while (cur != NULL) {
        printf("%d", cur->data);
        if (cur->next != NULL) printf(" -> ");
        cur = cur->next;
    }
    printf("]\n");
}

4.9 销毁链表

c

复制代码
void destroyList(LinkedList *list) {
    PNode cur = list->head;
    while (cur != NULL) {
        PNode temp = cur;
        cur = cur->next;
        free(temp);
    }
    list->head = NULL;
    list->size = 0;
}

五、完整代码演示

c

复制代码
#include <stdio.h>
#include <stdlib.h>

typedef struct Node {
    int data;
    struct Node *next;
} Node, *PNode;

typedef struct {
    PNode head;
    int size;
} LinkedList;

void initList(LinkedList *list) {
    list->head = (PNode)malloc(sizeof(Node));
    if (list->head == NULL) {
        printf("初始化失败\n");
        exit(1);
    }
    list->head->next = NULL;
    list->size = 0;
}

PNode createNode(int value) {
    PNode newNode = (PNode)malloc(sizeof(Node));
    if (newNode == NULL) return NULL;
    newNode->data = value;
    newNode->next = NULL;
    return newNode;
}

void insertAtHead(LinkedList *list, int value) {
    PNode newNode = createNode(value);
    if (newNode == NULL) return;
    newNode->next = list->head->next;
    list->head->next = newNode;
    list->size++;
}

void insertAtTail(LinkedList *list, int value) {
    PNode newNode = createNode(value);
    if (newNode == NULL) return;
    PNode cur = list->head;
    while (cur->next != NULL) {
        cur = cur->next;
    }
    cur->next = newNode;
    list->size++;
}

int insertAt(LinkedList *list, int pos, int value) {
    if (pos < 0 || pos > list->size) {
        printf("插入位置不合法\n");
        return -1;
    }
    PNode prev = list->head;
    for (int i = 0; i < pos; i++) {
        prev = prev->next;
    }
    PNode newNode = createNode(value);
    if (newNode == NULL) return -1;
    newNode->next = prev->next;
    prev->next = newNode;
    list->size++;
    return 0;
}

int deleteAt(LinkedList *list, int pos) {
    if (pos < 0 || pos >= list->size) {
        printf("删除位置不合法\n");
        return -1;
    }
    PNode prev = list->head;
    for (int i = 0; i < pos; i++) {
        prev = prev->next;
    }
复制代码
PNode toDelete = prev->next;
    int value = toDelete->data;
    prev->next = toDelete->next;
    free(toDelete);
    list->size--;
    return value;
}

int find(LinkedList *list, int value) {
    PNode cur = list->head->next;
    int pos = 0;
    while (cur != NULL) {
        if (cur->data == value) return pos;
        cur = cur->next;
        pos++;
    }
    return -1;
}

void print(LinkedList *list) {
    PNode cur = list->head->next;
    printf("size=%d, [", list->size);
    while (cur != NULL) {
        printf("%d", cur->data);
        if (cur->next != NULL) printf(" -> ");
        cur = cur->next;
    }
    printf("]\n");
}

void destroyList(LinkedList *list) {
    PNode cur = list->head;
    while (cur != NULL) {
        PNode temp = cur;
        cur = cur->next;
        free(temp);
    }
    list->head = NULL;
    list->size = 0;
}

int main() {
    LinkedList list;
    initList(&list);
    
    // 头插法
    insertAtHead(&list, 10);
    insertAtHead(&list, 20);
    insertAtHead(&list, 30);
    print(&list);  // [30 -> 20 -> 10]
    
    // 尾插法
    insertAtTail(&list, 40);
    insertAtTail(&list, 50);
    print(&list);  // [30 -> 20 -> 10 -> 40 -> 50]
    
    // 指定位置插入
    insertAt(&list, 2, 25);
    print(&list);  // [30 -> 20 -> 25 -> 10 -> 40 -> 50]
    
    // 删除
    int val = deleteAt(&list, 2);
    printf("删除的值: %d\n", val);
    print(&list);  // [30 -> 20 -> 10 -> 40 -> 50]
    
    // 查找
    int pos = find(&list, 40);
    printf("40的位置: %d\n", pos);
    
    destroyList(&list);
    return 0;
}

运行结果:

text

复制代码
size=3, [30 -> 20 -> 10]
size=5, [30 -> 20 -> 10 -> 40 -> 50]
size=6, [30 -> 20 -> 25 -> 10 -> 40 -> 50]
删除的值: 25
size=5, [30 -> 20 -> 10 -> 40 -> 50]
40的位置: 3

六、头插法 vs 尾插法

方法 操作位置 时间复杂度 结果顺序
头插法 头部插入 O(1) 逆序(先插入的在后面)
尾插法 尾部插入 O(n) 正序(先插入的在前面)

如果输入序列是1,2,3:

  • 头插法结果:3 → 2 → 1

  • 尾插法结果:1 → 2 → 3


七、复杂度分析

操作 时间复杂度 说明
头插 O(1) 直接修改指针
尾插 O(n) 需要遍历到末尾
中间插入 O(n) 需要找到前驱节点
删除 O(n) 需要找到前驱节点
按值查找 O(n) 需要遍历
按索引访问 O(n) 没有随机访问能力

八、小结

这一篇我们实现了单链表,要点总结:

要点 说明
节点结构 数据域 + 指针域,结构体自引用
头结点 哑结点,统一操作,简化代码
头插法 O(1)时间复杂度,结果逆序
尾插法 O(n)时间复杂度,结果正序
插入/删除 关键是找到前驱节点
优缺点 插入删除快,但无随机访问

下一篇我们会讲单链表的经典面试题:反转链表、找中间节点、判断环等。


九、思考题

  1. 带头结点和不带头结点的链表,在插入第一个节点时有什么区别?

  2. 如果要在链表的指定位置插入节点,为什么要先找到前驱节点,而不是直接找到目标位置?

  3. 头插法构建的链表和尾插法构建的链表,在遍历顺序上有什么不同?

  4. 写一个函数,计算链表的长度(要求时间复杂度O(n))。

欢迎在评论区讨论你的答案。

相关推荐
2401_873204652 小时前
C++与Node.js集成
开发语言·c++·算法
☆5662 小时前
基于C++的区块链实现
开发语言·c++·算法
剑飞的编程思维2 小时前
电商系统三类迭代方案评审重点
学习·系统架构·自动化·运维开发·学习方法
ysa0510302 小时前
迷宫传送[最短路径]
c++·笔记·算法·深度优先
左左右右左右摇晃2 小时前
数据结构——链表
数据结构·链表
计算机安禾2 小时前
【数据结构与算法】第5篇:线性表(一):顺序表(ArrayList)的实现与应用
c语言·开发语言·数据结构·c++·算法·visual studio code·visual studio
仰泳的熊猫2 小时前
题目2584:蓝桥杯2020年第十一届省赛真题-数字三角形
数据结构·c++·算法·蓝桥杯
2401_864959282 小时前
C++与Python混合编程实战
开发语言·c++·算法
鄭郑2 小时前
Figma学习笔记--02
笔记·学习·figma