本篇核心知识:自定义双向链表迭代器、链表删除 / 插入 / 拼接 / 反转操作、链表冒泡排序、STL 容器嵌套、容器底层与面试要点
一、双向链表迭代器详解
概念
迭代是对原生指针的封装类,自定义双向链表的迭代器本质就是封装节点指针,重载++、--、*、->等运算符,模拟指针行为,实现统一遍历接口。迭代器隶属于对应容器,不同容器迭代器底层实现不同。
特性
双向链表迭代器支持前置 ++/ 后置 ++、前置 --/ 后置 --,可双向遍历;
前置自增 / 自减:先移动指针,再返回自身,可返回引用;
后置自增 / 自减:先保留当前状态,再移动指针,需生成临时对象,不能返回引用;
begin():指向链表第一个有效数据节点;end():指向尾结点(尾后位置),遵循 ST 标准前闭后开遍历区间;迭代器仅操作节点指针,不修改节点数据本身。
代码示例(双向链表迭代器完整实现)
#include <iostream> using namespace std; // 链表节点模板 template<typename T> struct Node { T data; Node<T>* prev; Node<T>* next; Node(const T& val) : data(val), prev(nullptr), next(nullptr) {} }; // 双向链表类(内嵌迭代器) template<typename T> class DoubleList { public: // 自定义迭代器类(内嵌类) class Iterator{ private: Node<T>* cur; // 封装节点指针 public: Iterator(Node<T>* p = nullptr) : cur(p) {} // 解引用 T& operator*() { return cur->data; } // 成员访问 Node<T>* operator->() { return cur; } // 前置++ Iterator& operator++(){ cur = cur->next; return *this; } // 后置++(int占位符标识后置) Iterator operator++(int){ Iterator temp = *this; cur = cur->next; return temp; // 临时对象,不能返回引用 } // 前置-- Iterator& operator--(){ cur = cur->prev; return *this; } // 后置-- Iterator operator--(int){ Iterator temp = *this; cur = cur->prev; return temp; } // 判等、判不等 bool operator==(const Iterator& it) const{ return cur == it.cur; } bool operator!=(const Iterator& it) const{ return cur != it.cur; } }; // 链表对外接口 Iterator begin() { return Iterator(head->next); } Iterator end() { return Iterator(tail); } private: Node<T>* head; // 头结点(无数据) Node<T>* tail; // 尾结点(无数据) public: DoubleList(){ head = new Node<T>(T()); tail = new Node<T>(T()); head->next = tail; tail->prev = head; } ~DoubleList(){ Node<T>* p = head->next; while (p != tail){ Node<T>* del = p; p = p->next; delete del; } delete head; delete tail; } // 尾插 void push_back(const T& val); };拓展
vector迭代器底层就是普通指针,无需重载过多运算符;链表、树等容器必须自定义迭代器,这是不同容器迭代器的核心区别。
二、链表删除操作(按迭代器删除)
概念
接收迭代器参数,删除该迭代器指向的节点,修改前后节点的指针指向,释放内存并返回下一个有效迭代器(解决迭代器失效问题)。
特性
操作核心:先保存待删节点的后继节点,再修改前驱与后继的指针连接;
步骤:保存后继 → 重定向指针 → 释放节点内存 → 返回后继迭代器;
带头尾结点链表无需判断边界,代码统一;
删除后原迭代器失效,必须使用返回值继续遍历。
代码示例
template<typename T> typename DoubleList<T>::Iterator DoubleList<T>::erase(Iterator pos) { Node<T>* delNode = pos.operator->(); // 获取待删节点 Node<T>* nextNode = delNode->next; // 保存后继节点 // 重定向指针,完成断链 delNode->prev->next = nextNode; nextNode->prev = delNode->prev; delete delNode; // 释放内存 return Iterator(nextNode); // 返回下一个迭代器 }易错点
不可先释放节点再修改指针,会导致前驱节点失去后继地址,造成断链、内存泄漏。
三、区间删除
概念
删除[start, end)区间内所有节点(遵循前闭后开规则),仅修改首尾指针,批量摘除中间节点并释放内存。
特性
区间为左闭右开,
end指向的节点不删除;先记录区间首尾的前驱、后继节点,再批量释放中间节点;
操作完成后将两端节点重新连接。
代码示例
template<typename T> void DoubleList<T>::erase(Iterator start, Iterator end) { Node<T>* s = start.operator->(); Node<T>* e = end.operator->(); Node<T>* pre = s->prev; Node<T>* next = e; // 批量释放区间内节点 Node<T>* cur = s; while (cur != e) { Node<T>* temp = cur; cur = cur->next; delete temp; } // 重连两端 pre->next = next; next->prev = pre; }四、链表拼接操作
概念
将一个完整链表整体插入到当前链表指定迭代器位置,合并两条链表,原链表置空(不释放内存)。
特性
仅修改指针指向,不拷贝节点数据,效率极高;
拼接完成后,原链表必须重置为空链表(头结点 next 指向尾结点),防止野指针;
区分:插入到迭代器之前/之后,指针修改逻辑不同。
代码示例
// 将链表lst插入到pos迭代器之前 template<typename T> void DoubleList<T>::splice(Iterator pos, DoubleList<T>& lst) { if (lst.begin() == lst.end()) return; // 原链表为空,直接返回 Node<T>* cur = pos.operator->(); Node<T>* lstHead = lst.head->next; Node<T>* lstTail = lst.tail->prev; // 完成拼接 cur->prev->next = lstHead; lstHead->prev = cur->prev; lstTail->next = cur; cur->prev = lstTail; // 原链表置空,不释放头尾结点 lst.head->next = lst.tail; lst.tail->prev = lst.head; }拓展
链表拼接相比数组拼接优势极大,数组需要批量拷贝数据,链表仅修改指针。
五、链表冒泡排序
概念
基于冒泡思想对双向链表排序,优先交换节点指针,不交换数据(大数据 / 自定义对象场景性能更优)。
特性
排序前提:元素个数 ≥ 2,空链表 / 单个节点无需排序;
冒泡规则:多层循环,外层控制排序趟数,内层相邻节点比较;
双向链表优势:可通过
prev直接获取前驱节点,单链表无法做到;每一趟排序都会将当前最大值 “冒泡” 到末尾,下一趟比较次数递减。
代码示例
template<typename T> void DoubleList<T>::sort() { int len = 0; // 统计链表有效节点个数 for (auto it = begin(); it != end(); ++it) len++; if (len < 2) return; // 外层:排序趟数 for (int i = 0; i < len - 1; ++i) { Node<T>* p = head->next; // 内层:相邻节点比较 for (int j = 0; j < len - 1 - i; ++j) { if (p->data > p->next->data) { // 交换两个相邻节点(修改指针,不交换数据) Node<T>* p1 = p; Node<T>* p2 = p->next; Node<T>* p3 = p2->next; p1->next = p3; p2->prev = p1->prev; p1->prev->next = p2; p2->next = p1; p1->prev = p3->prev; p3->prev = p1; } p = p->next; } } }对比
数组冒泡:直接交换元素值,内存连续操作简单;
链表冒泡:交换节点指针,逻辑复杂,但海量数据下开销更小。
六、链表反转(头插法实现)
概念
利用头插法遍历原链表,逐个将节点插入新链表头部,实现整体反转。
特性
不新建节点,仅修改指针指向;
时间复杂度 (O(n)),一次遍历即可完成;
双向链表也可直接交换每个节点
prev与next指针实现反转。
代码示例(头插法反转)
template<typename T> void DoubleList<T>::reverse() { Node<T>* newHead = head; Node<T>* cur = head->next; head->next = tail; tail->prev = head; while (cur != tail) { Node<T>* temp = cur->next; // 头插 cur->next = newHead->next; newHead->next->prev = cur; newHead->next = cur; cur->prev = newHead; cur = temp; } }七、容器嵌套与拓展应用
概念
STL / 自定义容器的模板参数可以是另一个容器,实现二维、多层数据结构。
特性
基础用法:
vector<vector<int>>模拟二维数组;进阶用法:
vector<list<int>>模拟哈希表拉链法(解决哈希冲突经典方案);模板类型约束:嵌套容器内的类型,必须支持拷贝、对应运算符重载。
代码示例
#include <vector> #include <list> using namespace std; // 1. vector嵌套 = 二维动态数组 vector<vector<int>> vec2d; // 2. vector + list 模拟哈希拉链法 vector<list<int>> hashTable;拓展
C++ 标准库未直接提供哈希表容器(早期),开发中常使用vector+list组合自行实现,是底层开发常用技巧。
八、容器核心对比 & 选型总结
1. vector(顺序表)
底层:连续内存数组;
优点:下标随机访问 (O(1)),尾部操作效率高;
缺点:中间增删会挪动元素,频繁扩容影响性能;
适用:查询多、尾部操作多,尽量提前预留容量减少扩容。
2. list(双向链表)
底层:离散节点,前驱 + 后继双指针;
优点:任意位置增删仅改指针 (O(1)),无内存挪动;
缺点:不支持随机访问,遍历效率低;
适用:频繁随机增删、数据动态变化的场景。
通用编码规范
等值判断:
nullptr == p(常量放左侧),避免漏写等号引发 bug;指针操作:优先画图分析指针走向,杜绝断链、野指针;
内存管理:堆节点使用后必须
delete,防止泄漏。
九、补充知识点:容器类型要求
概念
STL 容器 / 模板类对存储类型存在隐性约束。
特性
自定义类型必须支持拷贝构造(容器添加元素会拷贝对象);
排序、比较场景,需要重载
==、<、>等运算符;类中包含指针成员时,必须实现深拷贝,防止内存错误。