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

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

双向链表实现涉及查找、插入与删除的指针调整逻辑,需特别注意边界条件与内存释放。对比顺序表可知,链表更适合动态数据量与频繁增删场景,而顺序表胜在随机访问效率。通过移除元素与反转链表算法实战,展示迭代法中多指针协作的细节,强化对线性表底层机制的理解与代码落地能力。

邪神洛基发布于 2026/3/29更新于 2026/9/968 浏览
数据结构:双向链表实现与算法实战

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

上一节我们梳理了双向链表的概念与基础结构,本节将深入其核心操作的具体实现,包括查找、插入、删除的指针调整细节,并对比顺序表与链表的差异,最后通过两道经典算法题巩固理解。

一、双向链表核心操作实现

1. 查找节点

查找逻辑与单链表类似,利用临时指针遍历即可。由于是循环双向链表,终止条件为回到头结点。

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;
}

2. 指定位置插入

双向链表在指定位置插入分为'之后'和'之前'两种情况,关键在于维护 prev 和 next 的双向连接。

在指定节点后插入

核心是四步指针调整:先保存后继,再断连,最后建立新节点与前驱、后继的关系。

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;
    
     (p->next != h && p->next != pos) {
        p = p->next;
    }
     (p->next == h) ; 
    
    ListNode* newnode = (x);
    ListNode* pr = p->next;
    
    newnode->next = pr;
    newnode->prev = p;
    p->next = newnode;
    pr->prev = newnode;
}
// 寻找 pos 的前驱
while
if
return
// 未找到
LTcreat

3. 指定位置删除

删除时需断开前驱和后继的连接,释放内存,并处理边界空指针风险。

void LTErase(ListNode* pos) {
    assert(pos);
    ListNode* p = pos->prev;
    p->next = pos->next;           // 前驱跳过当前节点
    pos->next->prev = p;           // 后继指向前驱
    free(pos);
    pos = NULL;                    // 防止野指针
}

4. 完整工程示例

以下代码整合了初始化、增删改查及测试逻辑,可直接编译运行验证。

list.h

#include<stdio.h>
#include<stdlib.h>
#include<assert.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 LTDestory(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);

list.c

#include"list.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 LTDestory(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 && p->next != pos) {
        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"list.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);
    
    LTDestory(h);
}

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

二、顺序表与链表分析

两者均为线性表,但存储方式决定了适用场景。

特性顺序表链表
逻辑结构一对一顺序关系一对一顺序关系
存储方式连续内存空间离散内存空间(指针链接)
随机访问O(1),效率高O(n),需遍历
插入/删除涉及大量数据移动,效率低仅需修改指针,效率高
空间管理需预分配,可能浪费或不足动态申请,灵活但开销稍大

结论:若场景侧重频繁随机访问且数据量固定(如学生信息表),首选顺序表;若侧重频繁增删且数据动态变化(如队列、栈实现),链表更优。

三、链表算法实战

1. 移除链表元素

题目要求移除所有值为 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) { h = p; pr = p; } // 首节点
            else { pr->next = p; pr = p; } // 拼接后续
        }
        p = p->next;
    }
    if (pr) pr->next = NULL; // 尾节点置空
    return h;
}

关键点:遍历时需保留新链表的尾指针 pr,并在循环结束后将其 next 置为 NULL,避免残留野指针。

2. 反转链表

反转链表的核心在于改变节点的指向。使用三个指针迭代是最稳妥的方法,时间复杂度 O(n),空间复杂度 O(1)。

struct ListNode* reverseList(struct ListNode* head) {
    node *s1 = NULL;      // 已反转部分的头
    node *s2 = head;      // 当前待反转节点
    node *s3 = NULL;      // 暂存下一个节点
    
    if (s2) s3 = s2->next;
    
    while (s2) {
        s2->next = s1;    // 反转指向
        s1 = s2;          // 前移已反转部分
        s2 = s3;          // 前移当前节点
        if (s3) s3 = s3->next; // 更新暂存节点
    }
    return s1;
}

执行流程:每轮循环中,先将当前节点 s2 的 next 指向 s1,然后整体后移。注意 s3 必须在修改 s2->next 之前保存,否则链表会断裂。

总结

双向链表的实现重点在于指针维护的准确性,尤其是插入和删除时的前后关联。相比顺序表,它在动态操作上更具优势,但牺牲了随机访问性能。掌握这两类结构的特性,配合指针操作的算法训练,能更好地应对底层数据结构相关的面试与工程问题。

目录

  1. 数据结构:双向链表实现与算法实战
  2. 一、双向链表核心操作实现
  3. 1. 查找节点
  4. 2. 指定位置插入
  5. 在指定节点后插入
  6. 在指定节点前插入
  7. 3. 指定位置删除
  8. 4. 完整工程示例
  9. 二、顺序表与链表分析
  10. 三、链表算法实战
  11. 1. 移除链表元素
  12. 2. 反转链表
  13. 总结

更多推荐文章

查看全部
  • 图像与人脸识别技术实现:C++ OpenCV 与 Matlab
  • 基于 Spring Boot 的在线考试系统设计与实现——学生课程实践记录
  • 算法:双指针解法移动零
  • OpenClaw 本地部署与 AI 助理自动化任务配置
  • Spark DataFusion Comet 向量化:Rust Native ScanExec 与 Selection Vectors
  • 攻防世界 Web 漏洞题解:Lottery 与 ics-05
  • 数据结构核心:链表详解与实现
  • 数据结构核心考点与算法实现指南
  • 数据结构:二叉树与堆
  • 程序员职场进阶:除代码外需掌握的关键技能
  • 数据结构:八种常见排序算法详解
  • 利用 AI 快速解析 COM.MFASHIONGALLERY.EMAG 接口
  • Telegram Bot 与 Mini-App 开发实践:获取 window.Telegram.WebApp 对象及解析
  • JavaSE 入门:注释、方法、基础数据类型与输入输出
  • Rust 异步编程实战:构建高性能 WebSocket 服务
  • AIGC 音乐制作全流程:从旋律生成到人声合成
  • Windows 上如何用 Conda 管理多个 Python 版本?
  • Linux Shell 脚本基础语法与自动化运维实战
  • 宇树 G1 机器人强化学习训练实战教程
  • Django 请求对象 request 详解:前端传参与后端接收

相关免费在线工具

  • 加密/解密文本

    使用加密算法(如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