3.链表:用指针串联的动态数据结构

一、什么是链表?

链表(Linked List)是一种非连续、非顺序 的线性数据结构,它通过指针 将一组零散的内存块串联起来,每个内存块被称为节点(Node)

简单来说,链表就像一串 "糖葫芦":

  • 每个 "山楂" 是一个节点,包含数据和下一个节点的地址;
  • 通过 "竹签"(指针)将所有节点串联起来,形成一个完整的链条。

二、链表的核心结构:节点

每个链表节点由两部分组成:

  1. 数据域:存储节点的具体数据;
  2. 指针域:存储下一个节点的地址。

对应的 C 语言代码定义如下:

复制代码
// 链表节点结构体
typedef struct Node {
    int data;          // 数据域:存储节点数据
    struct Node* next; // 指针域:指向next节点
} Node;

三、链表的基本操作

1. 头插法:在链表头部插入节点

头插法是将新节点插入到链表的最前端,步骤如下:

  1. 让新节点的next指向当前的头节点;
  2. 更新头节点为新节点。

代码实现:

复制代码
// 头插法:在链表头部插入新节点
void headInsert(Node** head, int value) {
    // 1. 创建新节点
    Node* newNode = (Node*)malloc(sizeof(Node));
    newNode->data = value;
    // 2. 新节点的next指向当前头节点
    newNode->next = *head;
    // 3. 更新头节点为新节点
    *head = newNode;
}

2. 尾插法:在链表尾部插入节点

尾插法是将新节点插入到链表的最后端,步骤如下:

  1. 遍历链表找到最后一个节点;
  2. 让最后一个节点的next指向新节点;
  3. 新节点的next指向NULL

代码实现:

复制代码
// 尾插法:在链表尾部插入新节点
void tailInsert(Node** head, int value) {
    // 1. 创建新节点
    Node* newNode = (Node*)malloc(sizeof(Node));
    newNode->data = value;
    newNode->next = NULL;

    // 2. 如果链表为空,直接让头节点指向新节点
    if (*head == NULL) {
        *head = newNode;
        return;
    }

    // 3. 遍历找到最后一个节点
    Node* p = *head;
    while (p->next != NULL) {
        p = p->next;
    }
    // 4. 最后一个节点的next指向新节点
    p->next = newNode;
}

3. 遍历链表

遍历链表是从头部开始,依次访问每个节点,直到遇到NULL结束,时间复杂度为O(n)

代码实现:

复制代码
// 遍历链表并打印所有节点
void traverseList(Node* head) {
    Node* p = head;
    while (p != NULL) {
        printf("%d ", p->data);
        p = p->next;
    }
    printf("\n");
}

4. 指定位置插入节点

在指定位置插入节点,需要先找到该位置的前驱节点,步骤如下:

  1. 遍历找到前驱节点p
  2. 让新节点的next指向pnext
  3. pnext指向新节点。

代码实现:

复制代码
// 指定位置插入节点:在第pos个位置插入新节点(pos从0开始)
void insertAt(Node** head, int pos, int value) {
    // 1. 处理头插情况(pos=0)
    if (pos == 0) {
        headInsert(head, value);
        return;
    }

    // 2. 找到前驱节点
    Node* p = *head;
    for (int i = 0; i < pos - 1 && p != NULL; i++) {
        p = p->next;
    }

    // 3. 如果前驱节点不存在,直接返回
    if (p == NULL) {
        printf("插入位置无效!\n");
        return;
    }

    // 4. 创建新节点并插入
    Node* newNode = (Node*)malloc(sizeof(Node));
    newNode->data = value;
    newNode->next = p->next;
    p->next = newNode;
}

5. 指定位置删除节点

删除指定位置的节点,需要先找到该位置的前驱节点,步骤如下:

  1. 遍历找到前驱节点p
  2. 保存要删除的节点tmp = p->next
  3. pnext指向tmpnext
  4. 释放tmp的内存。

代码实现:

复制代码
// 指定位置删除节点:删除第pos个位置的节点(pos从0开始)
void deleteAt(Node** head, int pos) {
    // 1. 处理头删情况(pos=0)
    if (pos == 0 && *head != NULL) {
        Node* tmp = *head;
        *head = (*head)->next;
        free(tmp);
        return;
    }

    // 2. 找到前驱节点
    Node* p = *head;
    for (int i = 0; i < pos - 1 && p != NULL && p->next != NULL; i++) {
        p = p->next;
    }

    // 3. 如果前驱节点或要删除的节点不存在,直接返回
    if (p == NULL || p->next == NULL) {
        printf("删除位置无效!\n");
        return;
    }

    // 4. 删除节点并释放内存
    Node* tmp = p->next;
    p->next = tmp->next;
    free(tmp);
}

6. 头删与尾删
头删:直接删除头节点,更新头节点为下一个节点;
尾删:遍历找到倒数第二个节点,让其next指向NULL,释放最后一个节点。
代码实现:

// 头删:删除链表的第一个节点
void headDelete(Node** head) {
    if (*head == NULL) {
        printf("链表为空,无法头删!\n");
        return;
    }
    Node* tmp = *head;
    *head = (*head)->next;
    free(tmp);
}

// 尾删:删除链表的最后一个节点
void tailDelete(Node** head) {
    if (*head == NULL) {
        printf("链表为空,无法尾删!\n");
        return;
    }

    // 1. 如果只有一个节点,直接删除
    if ((*head)->next == NULL) {
        free(*head);
        *head = NULL;
        return;
    }

    // 2. 找到倒数第二个节点
    Node* p = *head;
    while (p->next->next != NULL) {
        p = p->next;
    }

    // 3. 删除最后一个节点
    Node* tmp = p->next;
    p->next = NULL;
    free(tmp);
}

四、链表 vs 数组

链表和数组是两种最常用的线性数据结构,它们的区别如下:

五、链表的适用场景

根据链表和数组的特性,它们的适用场景如下:

  1. 链表适用场景

    • 数据量不确定,需要动态扩容;
    • 频繁进行插入、删除操作;
    • 不需要频繁随机访问。
  2. 数组适用场景

    • 数据量固定,需要频繁随机访问;
    • 内存连续,需要高访问效率。

六、完整代码示例

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

// 链表节点结构体
typedef struct Node {
    int data;
    struct Node* next;
} Node;

// 头插法
void headInsert(Node** head, int value) {
    Node* newNode = (Node*)malloc(sizeof(Node));
    newNode->data = value;
    newNode->next = *head;
    *head = newNode;
}

// 尾插法
void tailInsert(Node** head, int value) {
    Node* newNode = (Node*)malloc(sizeof(Node));
    newNode->data = value;
    newNode->next = NULL;

    if (*head == NULL) {
        *head = newNode;
        return;
    }

    Node* p = *head;
    while (p->next != NULL) {
        p = p->next;
    }
    p->next = newNode;
}

// 遍历链表
void traverseList(Node* head) {
    Node* p = head;
    while (p != NULL) {
        printf("%d ", p->data);
        p = p->next;
    }
    printf("\n");
}

// 指定位置插入
void insertAt(Node** head, int pos, int value) {
    if (pos == 0) {
        headInsert(head, value);
        return;
    }

    Node* p = *head;
    for (int i = 0; i < pos - 1 && p != NULL; i++) {
        p = p->next;
    }

    if (p == NULL) {
        printf("插入位置无效!\n");
        return;
    }

    Node* newNode = (Node*)malloc(sizeof(Node));
    newNode->data = value;
    newNode->next = p->next;
    p->next = newNode;
}

// 指定位置删除
void deleteAt(Node** head, int pos) {
    if (pos == 0 && *head != NULL) {
        Node* tmp = *head;
        *head = (*head)->next;
        free(tmp);
        return;
    }

    Node* p = *head;
    for (int i = 0; i < pos - 1 && p != NULL && p->next != NULL; i++) {
        p = p->next;
    }

    if (p == NULL || p->next == NULL) {
        printf("删除位置无效!\n");
        return;
    }

    Node* tmp = p->next;
    p->next = tmp->next;
    free(tmp);
}

// 头删
void headDelete(Node** head) {
    if (*head == NULL) {
        printf("链表为空,无法头删!\n");
        return;
    }
    Node* tmp = *head;
    *head = (*head)->next;
    free(tmp);
}

// 尾删
void tailDelete(Node** head) {
    if (*head == NULL) {
        printf("链表为空,无法尾删!\n");
        return;
    }

    if ((*head)->next == NULL) {
        free(*head);
        *head = NULL;
        return;
    }

    Node* p = *head;
    while (p->next->next != NULL) {
        p = p->next;
    }

    Node* tmp = p->next;
    p->next = NULL;
    free(tmp);
}

// 释放链表内存
void freeList(Node** head) {
    Node* p = *head;
    while (p != NULL) {
        Node* tmp = p;
        p = p->next;
        free(tmp);
    }
    *head = NULL;
}

int main() {
    Node* head = NULL;

    // 测试头插法
    headInsert(&head, 3);
    headInsert(&head, 5);
    headInsert(&head, 7);
    printf("头插后链表:");
    traverseList(head); // 输出:7 5 3

    // 测试尾插法
    tailInsert(&head, 9);
    printf("尾插后链表:");
    traverseList(head); // 输出:7 5 3 9

    // 测试指定位置插入
    insertAt(&head, 2, 6);
    printf("在位置2插入6后:");
    traverseList(head); // 输出:7 5 6 3 9

    // 测试指定位置删除
    deleteAt(&head, 2);
    printf("删除位置2的节点后:");
    traverseList(head); // 输出:7 5 3 9

    // 测试头删
    headDelete(&head);
    printf("头删后链表:");
    traverseList(head); // 输出:5 3 9

    // 测试尾删
    tailDelete(&head);
    printf("尾删后链表:");
    traverseList(head); // 输出:5 3

    // 释放链表内存
    freeList(&head);
    printf("释放内存后,链表为空:%s\n", head == NULL ? "是" : "否"); // 输出:是

    return 0;
}

七、总结

链表是一种动态、灵活的线性数据结构,它通过指针将节点串联起来,支持高效的插入和删除操作,但随机访问效率较低。

在实际开发中,链表常用于:

  • 动态数组(如ArrayList的底层实现);
  • 队列、栈等数据结构的实现;
  • 操作系统的内存管理;
  • 图和树的遍历等。

希望这篇文章能帮助你深入理解链表的原理和实现!

相关推荐
郝学胜-神的一滴3 小时前
干货版《算法导论》17:二叉树核心原理、遍历逻辑与高阶实操全解
数据结构·c++·python·算法·计算机·编程
weixin_383196473 小时前
java基础面试题@Autowired和@Resource区别
java·开发语言
Lzh编程小栈3 小时前
【嵌入式底层】SPI超透彻详解:通信原理、四线结构、四种模式、寄存器底层 + 面试全集
c语言·stm32·单片机·面试·职场和发展
疯狂打码的少年3 小时前
【数据结构】二叉树的性质(五大性质+计算)
数据结构·笔记·算法
言乐64 小时前
Python游戏水平测试辅助系统2
开发语言·windows·python·游戏·django
我不是疯子是傻子4 小时前
Qt 实时曲线卡顿优化:从QPainter到OpenGL的3级加速实战
开发语言·qt
zlinear数据采集卡4 小时前
D223 ADC数据处理流水线:从原始采样值到工程单位的完整转换链
开发语言·arm开发·嵌入式硬件·fpga开发·开源·c#
wabs6664 小时前
关于图论【A*算法 | 卡码网127.骑士的攻击的思考】
数据结构·算法·图论·卡码网·广搜的改进版
凉茶钱4 小时前
【数据结构】堆的应用
c语言·数据结构