本篇 核心知识 :STL 容器基础(list 双向链表)、范围 for 循环、迭代器、手写双向循环链表、双向链表常用接口、链表经典算法(反转 / 排序 / 去重 / 合并)、友元在链表中的使用
一、STL list 容器基础
概念
list 是 C++ 标准模板库 ST 提供的双向链表容器,底层由双向节点构成,封装了链表增删改查全部接口,无需手动管理节点与堆内存。
特性
-
物理内存不连续,依靠前后指针关联节点;
-
任意位置插入 / 删除效率 O (1),随机访问不支持(只能迭代遍历);
-
自动管理内存,容器销毁自动释放全部节点;
-
支持泛型,可存放任意合法数据类型(内置 / 自定义类)。
代码示例
#include <list>
#include <iostream>
using namespace std;
int main()
{
list<int> lst;
// 尾插
lst.push_back(10);
lst.push_back(20);
// 头插
lst.push_front(5);
return 0;
}
拓展:list 与 vector 区别
vector:顺序连续内存,随机访问快、中间增删慢;
list:链式离散,随机访问慢、任意位置增删快。
二、list 遍历两种写法(迭代器、范围 for)
1. 迭代器遍历
概念
迭代器是容器通用指针封装 ,用来指向容器元素,begin()首元素迭代器、end()末尾无效迭代器(尾后)。
特性
语法:容器类型::iterator 迭代器名;
++it向后移动,*it取元素值;
end 不作为有效元素,循环终止条件it != lst.end()。
list<int>::iterator it = lst.begin();
while(it != lst.end())
{
cout << *it << " ";
++it;
}
2. 范围 for (C++11 新特性)
概念
自动推导容器元素类型,自动遍历全部元素,底层由迭代器实现。
特性
auto自动推导变量类型;
auto &可修改元素,const auto&只读不修改。
//只读遍历
for(auto x : lst)
cout << x;
//可修改
for(auto &x : lst)
x *=2;
相似对比
迭代器:灵活支持中途增删、定点遍历;
范围 for:代码简洁,适合全容器遍历,中途删除易失效。
三、手写双向链表设计思路
概念
自行用 C++ 类封装双向链表,节点包含数据域 + 前驱 prev + 后继 next,整体链表类统一管理头尾。
特性
-
节点结构:
prev(上节点地址)、data、next(下节点地址); -
链表增设哨兵头结点(头傀儡):不存有效数据,统一空链表 / 首节点操作逻辑;
-
链表类将节点私有化,通过成员函数访问,需要时声明友元(list 类 / 迭代器访问私有节点)。
代码示例:节点与链表基础框架
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 MyList {
private:
Node<T>* head; //哨兵头
public:
MyList(){ head = new Node<T>(T()); }
~MyList(){/*析构释放全部节点*/}
};
拓展:友元使用场景
节点成员私有化,迭代器 / 链表需要访问节点私有成员,将对应类声明为节点友元。
四、双向链表核心接口(增、删)
1. 指定位置插入
概念:在 pos 迭代对应节点前插入新节点
特性
插入固定三步:
-
新节点前驱 = 原前驱节点;
-
新节点后继 = 原目标节点;
-
原前驱 next 指向新节点、原目标 prev 指向新节点。
顺序不能乱,防止断链。
//伪代码示意
Node<T>* newNode = new Node(val);
newNode->prev = pos->prev;
newNode->next = pos;
pos->prev->next = newNode;
pos->prev = newNode;
2. 指定位置删除
概念:摘除目标节点,前后节点互相链接,delete 释放内存
特性
-
先让被删节点的前驱 ->next = 被删后继;
-
被删后继 ->prev = 被删前驱;
-
delete 释放当前节点,杜绝内存泄漏。
Node
* del = pos;
del->prev->next = del->next;
del->next->prev = del->prev;
delete del;
五、双向链表经典算法
5.1 链表反转
概念
改变双向链表所有节点的prev(前驱)、next(后继)指针指向,颠倒节点顺序,原首节点变为尾节点,原尾节点变为首节点;仅修改指针,不改动节点内部数据。
特性
-
核心逻辑:遍历链表,逐个交换当前节点的前驱、后继指针;
-
哨兵头结点保持不变,仅反转有效数据节点;
-
时间复杂度 (O(n)),仅需一次完整遍历;
-
相比单链表,双向链表反转逻辑更直观,可借助前驱指针回溯。
代码示例(基于模板双向链表)
template<typename T>
void MyList<T>::reverse()
{
if (head->next == nullptr) // 空链表,无需反转
return;
Node<T>* cur = head->next;
Node<T>* temp = nullptr;
// 遍历所有有效节点,交换prev和next
while (cur != nullptr)
{
// 暂存原后继节点
temp = cur->next;
// 交换前驱、后继指针
cur->next = cur->prev;
cur->prev = temp;
cur = temp;
}
// 交换哨兵头与原首尾节点的指向
temp = head->next;
head->next = head->prev;
head->prev = temp;
}
拓展
单链表反转需要额外临时节点记录后继,双向链表利用prev天然支持双向遍历,实现复杂度更低。
5.2 链表排序
概念
对双向链表的有效节点进行排序,优先交换节点指针指向,而非交换节点内部数据;区别于数组直接交换元素值的实现方式。
特性
-
核心原因:若节点存储大型自定义对象,拷贝 / 交换数据开销极大,修改指针仅操作地址,效率更高;
-
常用算法:冒泡排序(适配链表结构,实现简单);
-
排序过程中需同步维护每个节点的
prev和next指针,防止断链; -
哨兵头结点位置始终固定,不参与排序。
代码示例(双向链表冒泡排序)
template<typename T>
void MyList<T>::sort()
{
if (head->next == nullptr || head->next->next == nullptr)
return; // 空链表/单个节点,无需排序
Node<T>* p;
// 外层控制排序轮数
for (p = head->next; p->next != nullptr; p = p->next)
{
Node<T>* q = head->next;
// 内层相邻节点比较,交换指针
while (q->next != nullptr)
{
if (q->data > q->next->data)
{
// 交换两个相邻节点(仅修改指针,不交换数据)
Node<T>* left = q;
Node<T>* right = q->next;
left->next = right->next;
if (right->next != nullptr)
right->next->prev = left;
right->prev = left->prev;
left->prev->next = right;
right->next = left;
left->prev = right;
q = right;
}
q = q->next;
}
}
}
相似概念对比
数组排序:内存连续,直接交换元素值,实现简单;
链表排序:内存离散,优先交换指针,大数据场景性能优势明显。
5.3 删除重复节点
概念
遍历有序双向链表,查找数值重复的节点,摘除重复节点并释放堆内存,最终保留唯一值节点,保证链表无重复元素。
特性
-
前提:一般针对有序链表(无序链表去重效率极低);
-
操作步骤:定位重复节点 → 修改前后节点指针完成摘除 →
delete释放节点,防止内存泄漏; -
遍历过程中指针需谨慎移动,避免节点丢失。
代码示例
template<typename T>
void MyList<T>::eraseRepeat()
{
if (head->next == nullptr)
return;
Node<T>* cur = head->next;
while (cur->next != nullptr)
{
// 当前节点与下一个节点值重复
if (cur->data == cur->next->data)
{
Node<T>* delNode = cur->next;
// 摘除重复节点
cur->next = delNode->next;
if (delNode->next != nullptr)
delNode->next->prev = cur;
// 释放内存
delete delNode;
}
else
{
cur = cur->next;
}
}
}
拓展
无序链表去重通常需要额外容器辅助记录已存在的值,时间、空间开销会增加。
5.4 两个有序链表合并
概念
将两条同排序规则的有序双向链表,合并为一条新的有序双向链表;全程仅修改节点指针指向,不新建节点、不拷贝数据。
特性
-
要求:两个原链表排序规则一致(同为升序 / 同为降序);
-
核心逻辑:依次比较两条链表当前节点值,选取较小节点接入新链表;
-
合并完成后,原链表失效,所有节点归属新链表;
-
时间复杂度 (O(m+n))(m、n 为两条链表节点总数)。
代码示例(合并两个升序双向链表)
template<typename T>
// 传入两个有序链表,返回合并后的新链表头
Node<T>* mergeList(Node<T>* h1, Node<T>* h2)
{
Node<T>* newHead = new Node<T>(); // 新哨兵头结点
Node<T>* tail = newHead;
Node<T>* p1 = h1->next;
Node<T>* p2 = h2->next;
// 同时遍历两个链表,择优接入
while (p1 != nullptr && p2 != nullptr)
{
if (p1->data < p2->data)
{
tail->next = p1;
p1->prev = tail;
p1 = p1->next;
}
else
{
tail->next = p2;
p2->prev = tail;
p2 = p2->next;
}
tail = tail->next;
}
// 接入剩余节点
if (p1 != nullptr)
{
tail->next = p1;
p1->prev = tail;
}
if (p2 != nullptr)
{
tail->next = p2;
p2->prev = tail;
}
return newHead;
}
拓展
合并后建议手动释放原链表的哨兵头结点,避免内存冗余。
六、构造函数与析构函数(双向链表专属)
概念
构造函数:创建链表对象时自动执行,完成哨兵头结点初始化;
析构函数:链表对象生命周期结束时自动执行,逐个释放所有节点内存,彻底杜绝内存泄漏。
特性
-
构造函数
链表初始化时,仅创建哨兵头结点,其
prev、next默认置为nullptr;无需创建有效数据节点,保证空链表结构合法。
-
析构函数
遍历所有有效数据节点,逐个
delete释放;最后释放哨兵头结点,遵循「先释数据节点,再释头结点」顺序;
析构函数无参数、无返回值,不能重载。
代码示例
template<typename T>
// 构造函数:初始化哨兵头结点
MyList<T>::MyList()
{
head = new Node<T>();
head->prev = nullptr;
head->next = nullptr;
}
template<typename T>
// 析构函数:释放全部节点
MyList<T>::~MyList()
{
Node<T>* p = head->next;
// 释放所有有效节点
while (p != nullptr)
{
Node<T>* temp = p;
p = p->next;
delete temp;
}
// 最后释放哨兵头结点
delete head;
head = nullptr;
}
拓展
-
若不自定义析构,编译器默认析构不会释放堆上节点,必然造成内存泄漏;
-
可结合拷贝构造、赋值重载,实现链表深拷贝,防止浅拷贝导致的重复释放问题。
七、模板在链表中的应用
概念
使用 C++类模板封装双向链表节点与链表整体,脱离固定数据类型限制,一套代码可适配整型、浮点型、字符串、自定义结构体 / 类等任意数据类型。
特性
-
语法规则:以
template<class T>或template<typename T>作为模板声明,T为通用类型占位符; -
作用域:模板声明作用于后续整个类 / 成员函数,类外实现成员函数时,必须重复书写模板头;
-
实例化:使用时显式指定具体类型(如
MyList<int>、MyList<string>),编译器自动生成对应类型代码; -
兼容性:模板代码建议写在同一头文件(分离编译会导致链接报错)。
代码示例(完整模板框架)
#include <iostream>
using namespace std;
// 模板声明 T:通用数据类型
template<class T>
// 模板节点类
struct Node
{
T data;
Node<T>* prev;
Node<T>* next;
Node(const T& val) : data(val), prev(nullptr), next(nullptr) {}
};
// 模板链表类
template<class T>
class MyList
{
private:
Node<T>* head; // 哨兵头结点
public:
MyList(); // 构造
~MyList(); // 析构
void pushBack(const T& val); // 尾插
void reverse(); // 反转
void sort(); // 排序
void eraseRepeat();// 去重
};
相似概念对比
普通链表:固定数据类型,多类型需求需重复编写多套代码;
模板链表:泛型设计,代码复用性极强,是 STL 容器的核心实现思想。
拓展
-
模板支持多类型参数
template<class T1, class T2>; -
可对特定类型做模板特化,定制特殊逻辑;
-
模板不支持运行期类型推导,必须在编译期确定具体类型。