当前位置: 首页 > news >正文

深入浅出:C++ 链表完全指南

从零开始掌握链表 | 图文并茂 | 代码实战

一、什么是链表?

链表(Linked List)是一种线性数据结构,它由一系列节点(Node)组成。与数组不同的是,链表中的元素在内存中不是连续存储的,而是通过指针连接在一起。

🚂 火车车厢的比喻

想象一列火车🚂,每节车厢就是一个节点:

  • 车厢本身:装载货物(数据)
  • 车厢挂钩:连接下一节车厢(指针)
  • 车头:第一节车厢(头指针 HEAD)

你可以随时加挂或卸下车厢(插入/删除),但要找到第5节车厢,必须从车头开始一节一节数过去。这就是链表的工作原理!

链表节点的结构

每个链表节点包含两个关键部分:

┌─────────────┬─────────────┐
│   数据域     │   指针域    │
│    Data     │    Next →   │
└─────────────┴─────────────┘

完整链表示例:

HEAD↓
┌────┬──┐    ┌────┬──┐    ┌────┬──┐
│ 10 │→ │ →  │ 20 │→ │ →  │ 30 │∅ │
└────┴──┘    └────┴──┘    └────┴──┘节点1        节点2        节点3(尾)

C++ 代码实现:

// C++ 链表节点定义
struct Node {int data;        // 数据域:存储实际数据Node* next;      // 指针域:指向下一个节点// 构造函数Node(int val) : data(val), next(nullptr) {}
};

二、链表 vs 数组:该如何选择?

理解链表的最佳方式就是将它与数组进行对比:

特性链表 🔗数组 📦
内存分配分散的,动态分配连续的,静态或动态
大小可动态增长固定大小(静态数组)
访问元素O(n) - 需要遍历O(1) - 直接索引
插入/删除O(1) - 只需改变指针O(n) - 需要移动元素
内存开销额外的指针空间无额外开销
缓存友好性较差(内存不连续)很好(连续内存)

💡 何时使用链表?

  • ✅ 需要频繁插入和删除操作
  • ✅ 数据量不确定,需要动态增长
  • ✅ 不需要随机访问元素
  • ✅ 实现栈、队列等其他数据结构

💡 何时使用数组?

  • ✅ 需要快速随机访问
  • ✅ 数据量固定或变化不大
  • ✅ 内存连续性很重要(缓存优化)
  • ✅ 很少进行插入删除操作

三、链表的基本操作

1. 在头部插入节点

这是最简单的操作,时间复杂度为 O(1)

void insertAtHead(int val) {Node* newNode = new Node(val);  // 创建新节点newNode->next = head;            // 新节点指向原头节点head = newNode;                  // 更新头指针
}

图解:

插入前: HEAD → [10] → [20] → NULL插入5:  [5] → [10] → [20] → NULL↑HEAD

2. 在尾部插入节点

需要遍历到最后一个节点,时间复杂度为 O(n)

void insertAtTail(int val) {Node* newNode = new Node(val);if (head == nullptr) {      // 如果链表为空head = newNode;return;}Node* temp = head;while (temp->next != nullptr) {  // 遍历到最后一个节点temp = temp->next;}temp->next = newNode;            // 连接新节点
}

图解:

插入前: HEAD → [10] → [20] → NULL遍历:   HEAD → [10] → [20] → NULL↑temp插入30: HEAD → [10] → [20] → [30] → NULL

3. 删除节点

删除节点需要找到前一个节点,修改其指针:

void deleteByValue(int val) {if (head == nullptr) return;// 如果要删除的是头节点if (head->data == val) {Node* temp = head;head = head->next;delete temp;return;}// 查找要删除的节点Node* temp = head;while (temp->next != nullptr && temp->next->data != val) {temp = temp->next;}if (temp->next != nullptr) {Node* nodeToDelete = temp->next;temp->next = temp->next->next;  // 跳过要删除的节点delete nodeToDelete;             // 释放内存}
}

图解:

删除前: HEAD → [10] → [20] → [30] → NULL删除20: HEAD → [10] ───────┐↓[20]×      [30] → NULL(删除)删除后: HEAD → [10] → [30] → NULL

4. 搜索节点

遍历链表查找特定值:

bool search(int val) {Node* temp = head;while (temp != nullptr) {if (temp->data == val) {return true;}temp = temp->next;}return false;
}

5. 反转链表 ⭐

这是一个经典的面试题,需要改变所有指针的方向:

void reverse() {Node* prev = nullptr;Node* current = head;Node* next = nullptr;while (current != nullptr) {next = current->next;      // 保存下一个节点current->next = prev;      // 反转指针prev = current;            // 移动prevcurrent = next;            // 移动current}head = prev;                   // 更新头指针
}

图解:

原链表: HEAD → [10] → [20] → [30] → NULL步骤1:  NULL ← [10]   [20] → [30] → NULL↑      ↑prev  current步骤2:  NULL ← [10] ← [20]   [30] → NULL↑      ↑prev  current步骤3:  NULL ← [10] ← [20] ← [30]↑HEAD反转后: HEAD → [30] → [20] → [10] → NULL

6. 获取链表长度

int length() {int count = 0;Node* temp = head;while (temp != nullptr) {count++;temp = temp->next;}return count;
}

7. 打印链表

void display() {if (head == nullptr) {cout << "链表为空" << endl;return;}Node* temp = head;while (temp != nullptr) {cout << temp->data;if (temp->next != nullptr) {cout << " -> ";}temp = temp->next;}cout << endl;
}

四、完整的链表类实现

#include <iostream>
using namespace std;// 定义链表节点结构
struct Node {int data;Node* next;Node(int val) : data(val), next(nullptr) {}
};// 链表类
class LinkedList {
private:Node* head;public:// 构造函数LinkedList() : head(nullptr) {}// 析构函数 - 释放所有节点~LinkedList() {Node* current = head;while (current != nullptr) {Node* temp = current;current = current->next;delete temp;}}// 在头部插入void insertAtHead(int val) {Node* newNode = new Node(val);newNode->next = head;head = newNode;}// 在尾部插入void insertAtTail(int val) {Node* newNode = new Node(val);if (head == nullptr) {head = newNode;return;}Node* temp = head;while (temp->next != nullptr) {temp = temp->next;}temp->next = newNode;}// 删除指定值的节点void deleteByValue(int val) {if (head == nullptr) return;if (head->data == val) {Node* temp = head;head = head->next;delete temp;return;}Node* temp = head;while (temp->next != nullptr && temp->next->data != val) {temp = temp->next;}if (temp->next != nullptr) {Node* nodeToDelete = temp->next;temp->next = temp->next->next;delete nodeToDelete;}}// 搜索节点bool search(int val) {Node* temp = head;while (temp != nullptr) {if (temp->data == val) return true;temp = temp->next;}return false;}// 反转链表void reverse() {Node* prev = nullptr;Node* current = head;Node* next = nullptr;while (current != nullptr) {next = current->next;current->next = prev;prev = current;current = next;}head = prev;}// 获取长度int length() {int count = 0;Node* temp = head;while (temp != nullptr) {count++;temp = temp->next;}return count;}// 打印链表void display() {if (head == nullptr) {cout << "链表为空" << endl;return;}Node* temp = head;while (temp != nullptr) {cout << temp->data;if (temp->next != nullptr) cout << " -> ";temp = temp->next;}cout << endl;}
};// 测试代码
int main() {LinkedList list;cout << "=== 链表操作演示 ===" << endl;// 插入操作list.insertAtTail(10);list.insertAtTail(20);list.insertAtTail(30);cout << "插入 10, 20, 30: ";list.display();list.insertAtHead(5);cout << "头部插入 5: ";list.display();// 搜索操作cout << "搜索 20: " << (list.search(20) ? "找到" : "未找到") << endl;// 删除操作list.deleteByValue(20);cout << "删除 20: ";list.display();// 反转链表list.reverse();cout << "反转链表: ";list.display();// 获取长度cout << "链表长度: " << list.length() << endl;return 0;
}

输出:

=== 链表操作演示 ===
插入 10, 20, 30: 10 -> 20 -> 30
头部插入 5: 5 -> 10 -> 20 -> 30
搜索 20: 找到
删除 20: 5 -> 10 -> 30
反转链表: 30 -> 10 -> 5
链表长度: 3

五、链表的类型

1. 单链表(Single Linked List)

每个节点只有一个指向下一个节点的指针,这是我们上面讨论的类型。

HEAD → [A] → [B] → [C] → NULL

2. 双链表(Double Linked List)

每个节点有两个指针,分别指向前一个和后一个节点,可以双向遍历。

struct DoubleNode {int data;DoubleNode* prev;  // 指向前一个节点DoubleNode* next;  // 指向后一个节点
};
NULL ← [A] ⇄ [B] ⇄ [C] → NULL↑HEAD

3. 循环链表(Circular Linked List)

最后一个节点的指针指向第一个节点,形成一个环。

      ┌─────────────────┐↓                 │
HEAD → [A] → [B] → [C] ─┘

📊 应用场景对比

链表类型主要应用场景
单链表简单的顺序访问、栈实现
双链表浏览器前进后退、LRU缓存
循环链表音乐播放器循环播放、操作系统进程调度

六、链表的常见面试题

1. 检测链表中的环(Floyd判圈算法)

使用快慢指针,如果链表有环,快慢指针最终会相遇。

bool hasCycle(Node* head) {if (!head) return false;Node* slow = head;Node* fast = head;while (fast && fast->next) {slow = slow->next;           // 慢指针走一步fast = fast->next->next;     // 快指针走两步if (slow == fast) {return true;             // 相遇,说明有环}}return false;
}

原理图解:

有环的链表:[1] → [2] → [3] → [4]↑           ↓←-- [6] ← [5]slow: 1 → 2 → 3 → 4 → 5 → 6 → 3 → 4...
fast: 1 → 3 → 5 → 3...在节点3相遇!

2. 找到链表的中间节点

同样使用快慢指针:

Node* findMiddle(Node* head) {Node* slow = head;Node* fast = head;while (fast && fast->next) {slow = slow->next;fast = fast->next->next;}return slow;  // 快指针到末尾时,慢指针在中间
}

示例:

链表: [1] → [2] → [3] → [4] → [5] → NULLslow: 1     2     3     4     5
fast: 1     3     5     NULL↑中间节点

3. 合并两个有序链表

Node* mergeLists(Node* l1, Node* l2) {Node dummy(0);  // 哨兵节点Node* tail = &dummy;while (l1 && l2) {if (l1->data < l2->data) {tail->next = l1;l1 = l1->next;} else {tail->next = l2;l2 = l2->next;}tail = tail->next;}tail->next = l1 ? l1 : l2;  // 连接剩余部分return dummy.next;
}

4. 删除链表的倒数第N个节点

使用双指针,先让快指针走N步,然后快慢指针同时移动:

Node* removeNthFromEnd(Node* head, int n) {Node dummy(0);dummy.next = head;Node* fast = &dummy;Node* slow = &dummy;// 快指针先走n+1步for (int i = 0; i <= n; i++) {fast = fast->next;}// 快慢指针同时移动while (fast != nullptr) {fast = fast->next;slow = slow->next;}// 删除节点Node* temp = slow->next;slow->next = slow->next->next;delete temp;return dummy.next;
}

5. 判断链表是否为回文

bool isPalindrome(Node* head) {// 找到中间节点Node* slow = head;Node* fast = head;while (fast && fast->next) {slow = slow->next;fast = fast->next->next;}// 反转后半部分Node* prev = nullptr;while (slow) {Node* next = slow->next;slow->next = prev;prev = slow;slow = next;}// 比较前半部分和反转后的后半部分Node* left = head;Node* right = prev;while (right) {if (left->data != right->data) {return false;}left = left->next;right = right->next;}return true;
}

七、时间复杂度总结

操作时间复杂度说明
访问O(n)需要从头遍历
搜索O(n)需要遍历查找
头部插入O(1)直接修改头指针
尾部插入O(n)需要遍历到末尾
头部删除O(1)直接修改头指针
尾部删除O(n)需要找到倒数第二个节点
中间插入/删除O(n)需要先找到位置

优化方案

如果需要频繁在尾部插入,可以维护一个 tail 指针:

class LinkedList {
private:Node* head;Node* tail;  // 维护尾指针public:void insertAtTail(int val) {Node* newNode = new Node(val);if (!head) {head = tail = newNode;} else {tail->next = newNode;tail = newNode;}}  // 现在是 O(1)!
};

八、实际应用场景

1. 浏览器历史记录

使用双链表实现前进和后退功能:

struct HistoryNode {string url;HistoryNode* prev;HistoryNode* next;
};class BrowserHistory {
private:HistoryNode* current;public:void visit(string url) {HistoryNode* newPage = new HistoryNode{url, current, nullptr};if (current) current->next = newPage;current = newPage;}void back() {if (current && current->prev) {current = current->prev;}}void forward() {if (current && current->next) {current = current->next;}}
};

2. LRU 缓存

使用双链表 + 哈希表实现:

class LRUCache {
private:int capacity;list<pair<int, int>> cache;  // 双链表unordered_map<int, list<pair<int, int>>::iterator> map;public:LRUCache(int cap) : capacity(cap) {}int get(int key) {if (map.find(key) == map.end()) return -1;// 移到链表头部auto it = map[key];int value = it->second;cache.erase(it);cache.push_front({key, value});map[key] = cache.begin();return value;}void put(int key, int value) {if (map.find(key) != map.end()) {cache.erase(map[key]);} else if (cache.size() >= capacity) {// 删除最久未使用的int oldKey = cache.back().first;cache.pop_back();map.erase(oldKey);}cache.push_front({key, value});map[key] = cache.begin();}
};

3. 音乐播放器(循环链表)

struct Song {string title;Song* next;
};class MusicPlayer {
private:Song* current;public:void nextSong() {if (current) current = current->next;// 如果是循环链表,永远不会为空}void play() {if (current) {cout << "正在播放: " << current->title << endl;}}
};

九、学习建议与总结

💡 学习链表的最佳实践

  1. 画图!画图!画图!

    • 在纸上画出节点和指针的变化过程
    • 这是理解指针操作的最佳方法
  2. 理解指针

    • 链表的核心是指针操作
    • 确保你理解 *-> 的含义
  3. 注意边界情况

    • 空链表(head == nullptr)
    • 单节点链表
    • 删除头节点或尾节点
  4. 内存管理

    • 不要忘记 delete 删除的节点
    • 使用析构函数释放所有节点
  5. 多练习

    • 在 LeetCode 上刷链表题
    • 推荐题目:206, 141, 21, 160, 234

🎯 常见陷阱

// ❌ 错误:直接修改会丢失链表
Node* temp = head;
head = head->next;  // 如果没有保存temp,原head会泄漏// ✅ 正确:先保存,再删除
Node* temp = head;
head = head->next;
delete temp;// ❌ 错误:空指针访问
if (head->next->data == 5)  // 如果head或head->next为空会崩溃// ✅ 正确:检查空指针
if (head && head->next && head->next->data == 5)

📚 进阶学习路径

  1. 基础链表

    • 单链表的实现和操作
  2. 进阶链表

    • 双链表
    • 循环链表
    • 跳表(Skip List)
  3. 应用实践

    • 实现栈和队列
    • LRU 缓存实现
    • 图的邻接表表示
  4. 算法竞赛

    • 链表相关的经典算法题
    • 复杂的指针操作技巧

🔍 与标准库的比较

C++ 标准库提供了 std::list(双链表)和 std::forward_list(单链表):

#include <list>
#include <forward_list>// 双链表
std::list<int> myList = {1, 2, 3};
myList.push_back(4);
myList.push_front(0);// 单链表
std::forward_list<int> fList = {1, 2, 3};
fList.push_front(0);

何时使用自己实现的链表?

  • 学习和面试准备
  • 需要定制化的功能
  • 嵌入式系统(避免STL开销)
  • 理解底层原理

总结

链表是计算机科学中最基础也是最重要的数据结构之一。虽然在现代编程中,我们更多地使用标准库提供的容器,但理解链表的原理对于:

✅ 理解其他复杂数据结构(如图、树)
✅ 掌握指针操作和内存管理
✅ 通过技术面试
✅ 优化程序性能

都有着重要的意义。

记住这个学习口诀:

画图理解,代码实践,多做题目,注意边界!


参考资源

  • 📖 《数据结构与算法分析》- Mark Allen Weiss
  • 📖 《算法导论》- CLRS
  • 💻 LeetCode 链表题目集
  • 💻 cppreference.com - C++ 标准库文档
http://www.dtcms.com/a/423777.html

相关文章:

  • NumPy 与 Pandas 的详细应用(含实例)
  • 2345浏览器网页版入口中文版合肥seo优化公司
  • 网站建设报价包括哪些学校网站建设电话
  • 音视频编解码全流程之用Extractor后Muxer生成MP4
  • 高德地图实现经纬度及获取编码、所属行政区、GIS
  • wordpress 扁平化新站seo快速排名 排名
  • 2025年在招投标及竞品信息采集机器人领域,主流RPA全面解析
  • 电子商务网站建设与管理期末考试网站开发方案案例
  • Node.js命令行工具开发
  • 《面向物理交互任务的触觉传感阵列仿真》2020AIM论文解读
  • 未来最紧缺的十大专业seo优化师
  • OCP证书考试难度怎么样?
  • Vue3 defineModel === 实现原理
  • 唐山营销型网站建设2023新闻头条最新消息今天
  • 计算机网络---传输层
  • 如何在阿里云上做网站制作软件的手机软件
  • 深入理解 Java 虚拟机:从原理到实践的全方位剖析
  • 网站谷歌seo做哪些凌点视频素材网
  • 手机app应用网站C语言做网站需要创建窗口吗
  • uniapp 安卓FTP上传下载操作原生插件
  • 国外知名平面设计网站黄骅打牌吧
  • C++ I/O流与文件操作速查
  • 网站制作哪家好又便宜做电商网站的流程
  • 网络边界突围:运营商QoS限速策略
  • 【笔记】在WPF中Decorator是什么以及何时优先考虑 Decorator 派生类
  • [算法练习]Day 4:定长滑动窗口
  • 外汇交易网站开发做网站前端后台
  • 小红书网站建设目的优化师简历
  • 集群的概述和分类和负载均衡集群
  • 专业的商城网站开发搜索引擎优化不包括