跳到主要内容
极客日志极客日志面向AI+效率的开发者社区
首页博客GitHub 精选镜像工具UI配色美学隐私政策关于联系
搜索内容 / 工具 / 仓库 / 镜像...⌘K搜索
注册
博客列表
C算法

数据结构实战:双向链表实现与算法解析

综述由AI生成双向链表作为线性表的链式存储结构,支持前后双向遍历。了双向链表的查找、指定位置插入(前后)、删除等核心操作实现,对比了顺序表与链表在空间与时间复杂度上的差异,并深入剖析了移除元素与反转链表两道经典算法题的解题思路与代码实现。重点在于指针操作的边界处理及内存管理,适合希望夯实数据结构基础的开发人员阅读。

SparkGeek发布于 2026/3/26更新于 2026/6/922 浏览
数据结构实战:双向链表实现与算法解析

数据结构实战:双向链表实现与算法解析

在之前的讲解中,我们建立了双向链表的基础结构。今天我们将深入实现双向链表的剩余核心操作,并对比顺序表与链表的区别,最后通过两道经典算法题巩固理解。

一、实现双向链表

1. 查找操作

双向链表的查找逻辑与单链表类似,但利用 prev 指针可以反向遍历。这里我们采用从头结点开始正向遍历的方式。

函数原型:

ListNode* LTFind(ListNode* h, type x);

核心实现:

ListNode* LTFind(ListNode* h, type x) {
    if (LTEmpty(h)) {
        return NULL;
    }
    ListNode* p = h->next;
    while (p != h) {
        if (p->data == x) {
            return p;
        }
        p = p->next;
    }
    return NULL;
}

注意: 循环条件 p != h 确保了不会越过尾结点回到头结点,形成死循环。找到目标直接返回节点指针,未找到则返回 NULL。

2. 指定位置插入

双向链表的插入比单链表更灵活,分为在指定节点后插入和在指定节点前插入。

在指定节点之后插入

这是最基础的操作,关键在于保存后继节点指针,防止断链。

函数原型:

void LTInsert(ListNode* pos, type x);

核心实现:

void LTInsert(ListNode* pos, type x) {
    assert(pos); // 确保 pos 有效
    ListNode* p = pos->next;       // 先保存原后继
    ListNode* newnode = LTcreat(x);
    
    // 调整指针关系(4 步)
    pos->next = newnode;         // 1. 新节点接在 pos 后
    newnode->prev = pos;         // 2. 新节点指回 pos
    newnode->next = p;           // 3. 新节点指向原后继
    p->prev = newnode;           // 4. 原后继指回新节点
}

细节: 这里的顺序不能乱。必须先保存 pos->next 给 p,否则一旦执行 pos->next = newnode,原链表后半部分就找不到了。

在指定节点之前插入

这需要先找到 pos 的前驱节点,然后再调用上述逻辑或手动调整指针。

函数原型:

void LTInsertfront(ListNode* h, ListNode* pos, type x);

核心实现:

void LTInsertfront(ListNode* h, ListNode* pos, type x) {
    if (LTEmpty(h)) {
        return;
    }
    ListNode* p = h;
    // 找到 pos 的前驱
    while (p->next != h) {
        if (p->next == pos) {
            break;
        }
        p = p->next;
    }
    if (p->next == h) {
        return; // 没找到 pos
    }
    
    ListNode* newnode = LTcreat(x);
    ListNode* pr = p->next;      // 保存原后继(即 pos)
    
    newnode->next = pr;
    newnode->prev = p;
    p->next = newnode;
    pr->prev = newnode;
}

3. 指定位置删除

删除节点需要修改前驱的 next 和后继的 prev,并释放内存。

函数原型:

void LTErase(ListNode* pos);

核心实现:

void LTErase(ListNode* pos) {
    assert(pos);
    ListNode* p = pos->prev;     // 获取前驱
    
    p->next = pos->next;         // 前驱跳过 pos
    pos->next->prev = p;         // 后继指回前驱
    
    free(pos);                   // 释放内存
    pos = NULL;                  // 置空局部指针
}

边界处理: 如果 pos 是头结点或尾结点,由于是循环链表,逻辑依然适用,因为 prev 和 next 始终存在且指向有效节点。

4. 完整代码参考

为了便于测试和理解,这里整合了初始化、增删改查及销毁的完整代码。

头文件 (1.h)

#include <stdio.h>
#include <stdlib.h>
#include <assert.h>
#include <stdbool.h>

typedef int type;

typedef struct ListNode {
    type data;
    struct ListNode* prev;
    struct ListNode* next;
} ListNode;

// 声明所有接口
void LTInit(ListNode** h);
void LTPushBack(ListNode* h, type x);
ListNode* LTcreat(type x);
void LTPushFront(ListNode* h, type x);
void LTPopBack(ListNode* h);
void LTPopFront(ListNode* h);
void LTDestroy(ListNode* h); // 修正拼写
void print(ListNode* h);
ListNode* LTFind(ListNode* h, type x);
void LTInsert(ListNode* pos, type x);
void LTInsertfront(ListNode* h, ListNode* pos, type x);
void LTErase(ListNode* pos);
bool LTEmpty(ListNode* phead);

实现文件 (1.c)

#include "1.h"

void LTInit(ListNode** h) {
    ListNode* ph = (ListNode*)malloc(sizeof(ListNode));
    if (ph == NULL) {
        perror("malloc fail!");
        exit(1);
    }
    *h = ph;
    (*h)->data = -1;
    (*h)->next = *h;
    (*h)->prev = *h;
}

ListNode* LTcreat(type x) {
    ListNode* ph = (ListNode*)malloc(sizeof(ListNode));
    if (ph == NULL) {
        perror("malloc fail!");
        exit(1);
    }
    ph->data = x;
    ph->next = ph;
    ph->prev = ph;
    return ph;
}

void LTPushBack(ListNode* h, type x) {
    ListNode* p = LTcreat(x);
    p->next = h;
    p->prev = h->prev;
    h->prev->next = p;
    h->prev = p;
}

void LTPushFront(ListNode* h, type x) {
    ListNode* p = LTcreat(x);
    p->next = h->next;
    p->prev = h;
    h->next->prev = p;
    h->next = p;
}

bool LTEmpty(ListNode* phead) {
    assert(phead);
    return phead->next == phead;
}

void LTPopBack(ListNode* h) {
    if (LTEmpty(h)) {
        return;
    }
    ListNode* p = h->prev;
    h->prev = p->prev;
    p->prev->next = h;
    free(p);
}

void LTPopFront(ListNode* h) {
    if (LTEmpty(h)) {
        printf("链表为空,无法头删\n");
        return;
    }
    ListNode* p = h->next;
    h->next = p->next;
    p->next->prev = h;
    free(p);
}

void LTDestroy(ListNode* h) {
    if (LTEmpty(h)) {
        free(h);
        return;
    }
    ListNode* p = h->next;
    while (p != h) {
        ListNode* pr = p;
        p = p->next;
        free(pr);
    }
    free(h);
    h = NULL;
}

void print(ListNode* h) {
    if (LTEmpty(h)) {
        return;
    }
    ListNode* p = h->next;
    while (p != h) {
        printf("%d ", p->data);
        p = p->next;
    }
    printf("\n");
}

ListNode* LTFind(ListNode* h, type x) {
    if (LTEmpty(h)) {
        return NULL;
    }
    ListNode* p = h->next;
    while (p != h) {
        if (p->data == x) {
            return p;
        }
        p = p->next;
    }
    return NULL;
}

void LTInsert(ListNode* pos, type x) {
    assert(pos);
    ListNode* p = pos->next;
    ListNode* newnode = LTcreat(x);
    pos->next = newnode;
    newnode->prev = pos;
    newnode->next = p;
    p->prev = newnode;
}

void LTInsertfront(ListNode* h, ListNode* pos, type x) {
    if (LTEmpty(h)) {
        return;
    }
    ListNode* p = h;
    while (p->next != h) {
        if (p->next == pos) {
            break;
        }
        p = p->next;
    }
    if (p->next == h) {
        return;
    }
    ListNode* newnode = LTcreat(x);
    ListNode* pr = p->next;
    newnode->next = pr;
    newnode->prev = p;
    p->next = newnode;
    pr->prev = newnode;
}

void LTErase(ListNode* pos) {
    assert(pos);
    ListNode* p = pos->prev;
    p->next = pos->next;
    pos->next->prev = p;
    free(pos);
    pos = NULL;
}

测试主程序 (main.c)

#include "1.h"

void test() {
    ListNode* h;
    LTInit(&h);
    LTPushBack(h, 10);
    LTPushBack(h, 15);
    LTPushBack(h, 111);
    print(h);
    
    LTPushFront(h, 2);
    LTPushFront(h, 12);
    print(h);
    
    LTPopBack(h);
    print(h);
    LTPopFront(h);
    print(h);
    
    ListNode* p = LTFind(h, 10);
    LTInsert(p, 100);
    LTInsert(p, 200);
    LTErase(p);
    print(h);
    
    LTDestroy(h);
}

int main() {
    test();
    return 0;
}

双向链表示意图

二、顺序表与链表的分析

在实际开发中,选择顺序表还是链表取决于具体场景。下图直观展示了两者的异同:

顺序表与链表对比图

相同点

  • 逻辑结构:均为线性表,元素一对一排列。
  • 基本操作:都支持插入、删除、查找、遍历。
  • 数据类型:均可存储任意类型数据。

不同点(核心差异)

  • 存储方式:顺序表连续存储,链表离散存储。
  • 访问效率:顺序表随机访问 O(1),链表遍历 O(n)。
  • 空间开销:顺序表需预留空间,链表每个节点多存指针。

关键结论

  • 顺序表:适合频繁随机访问、数据量相对固定的场景(如配置表)。
  • 链表:适合频繁插入删除、数据量动态变化的场景(如任务队列)。

三、链表算法题

1. 移除链表元素

题目描述:给定链表头结点和值 val,移除所有值为 val 的节点。

思路: 我们可以创建一个新链表,只保留不等于 val 的节点。虽然这种方法没有原地释放内存,但在算法竞赛中通常可接受,且逻辑清晰。

代码实现:

struct ListNode* removeElements(struct ListNode* head, int val) {
    Node *h = NULL, *pr = NULL;
    Node *p = head;
    while (p) {
        if (p->val != val) {
            if (h == NULL) {
                h = p;
                pr = p;
            } else {
                pr->next = p;
                pr = p;
            }
        }
        p = p->next;
    }
    if (pr) {
        pr->next = NULL;
    }
    return h;
}

提示:记得将新链表的尾节点 next 置为 NULL,避免野指针。

2. 反转链表

题目描述:反转一个单链表。

思路: 使用三个指针迭代法,时间复杂度 O(n),空间复杂度 O(1)。这是面试中的高频考点。

  • s1:已反转部分的头(初始 NULL)
  • s2:当前待反转节点(初始 head)
  • s3:临时保存下一个节点

代码实现:

struct ListNode* reverseList(struct ListNode* head) {
    node *s1 = NULL;
    node *s2 = head, *s3 = NULL;
    if (s2) {
        s3 = s2->next;
    }
    while (s2) {
        s2->next = s1;   // 反转指向
        s1 = s2;         // s1 后移
        s2 = s3;         // s2 后移
        if (s3) {
            s3 = s3->next; // s3 后移
        }
    }
    return s1;
}

反转链表过程图

图解说明:每次循环将 s2 的 next 指向 s1,然后三个指针依次向后移动,直到 s2 为空,此时 s1 即为新的头结点。

目录

  1. 数据结构实战:双向链表实现与算法解析
  2. 一、实现双向链表
  3. 1. 查找操作
  4. 2. 指定位置插入
  5. 在指定节点之后插入
  6. 在指定节点之前插入
  7. 3. 指定位置删除
  8. 4. 完整代码参考
  9. 二、顺序表与链表的分析
  10. 相同点
  11. 不同点(核心差异)
  12. 关键结论
  13. 三、链表算法题
  14. 1. 移除链表元素
  15. 2. 反转链表
  • 💰 8折买阿里云服务器限时8折了解详情
  • Magick API 一键接入全球大模型注册送1000万token查看
  • 🤖 一键搭建Deepseek满血版了解详情
  • 一键打造专属AI 智能体了解详情
极客日志微信公众号二维码

微信扫一扫,关注极客日志

微信公众号「极客日志V2」,在微信中扫描左侧二维码关注。展示文案:极客日志V2 zeeklog

更多推荐文章

查看全部
  • Kubernetes 与 AI 集成最佳实践
  • 网络安全行业自学与转行入行建议指南
  • Qwen3-ASR-1.7B 赋能博物馆 AR 导览:语音转写与知识图谱联动
  • 大模型竞争进入下半场:商业化落地与新场景探索
  • VSCode 彻底关闭 GitHub Copilot 配置指南
  • DeepFace 与 OpenCV 实现情绪分析器
  • 解决软件安装时 Microsoft Visual C++ 运行库缺失报错
  • Elasticsearch 核心概念与 Java 客户端实战
  • Spring Boot 单元测试实战:如何测试 RESTful 接口
  • 世界模型发展脉络整理:理解世界还是预测未来?
  • 开源知识库 RAGFlow 从部署到实战操作详解
  • 中国黑客群体的真实收入
  • OpenClaw 系统架构深度解析
  • VSCode 配置 Python 开发环境实战指南
  • MySQL 身份鉴别有效性检查与安全加固指南
  • LLM 大模型部署的最佳实践
  • 2025 年六大主流 AI 大模型产品评测与解析
  • Redis Zset 底层实现详解:跳跃表与字典结构
  • 网络安全基础入门:从零开始的学习路线与核心技能
  • OpenClaw 深度解析:AI 智能体的能力、风险与未来

相关免费在线工具

  • 加密/解密文本

    使用加密算法(如AES、TripleDES、Rabbit或RC4)加密和解密文本明文。 在线工具,加密/解密文本在线工具,online

  • Gemini 图片去水印

    基于开源反向 Alpha 混合算法去除 Gemini/Nano Banana 图片水印,支持批量处理与下载。 在线工具,Gemini 图片去水印在线工具,online

  • Base64 字符串编码/解码

    将字符串编码和解码为其 Base64 格式表示形式即可。 在线工具,Base64 字符串编码/解码在线工具,online

  • Base64 文件转换器

    将字符串、文件或图像转换为其 Base64 表示形式。 在线工具,Base64 文件转换器在线工具,online

  • Markdown转HTML

    将 Markdown(GFM)转为 HTML 片段,浏览器内 marked 解析;与 HTML转Markdown 互为补充。 在线工具,Markdown转HTML在线工具,online

  • HTML转Markdown

    将 HTML 片段转为 GitHub Flavored Markdown,支持标题、列表、链接、代码块与表格等;浏览器内处理,可链接预填。 在线工具,HTML转Markdown在线工具,online