链表与list
1. 链表实现

特点: 每一个节点都是在堆内存上独立 new 出来的,节点内存不连续。即逻辑地址连续,而物理地址不连续。
优点:
- 内存利用率高,不需要大块连续内存
- 插入和删除节点不需要移动其它节点,时间复杂度 O(1)
- 不需要专门进行扩容操作
缺点:
- 内存占用量大,每一个节点多出存放地址的空间
- 节点内存不连续,无法进行内存的随机访问
- 链表的搜索效率不高,只能从头节点开始逐结点遍历
常见的链表形式包括:单链表、双向链表、环状链表
1.1 单链表
单链表的定义:每一个节点只能找到下一个节点,而无法找到上一个节点,如下图所示:

特点:
- 每一个节点除了数据域,还有一个 next 指针域指向下一个节点(存储了下一个节点的地址),但是无法回退到前一个节点
- 末尾节点的指针域是 NULL
如下代码为单链表的实现:
// 链表节点定义 struct Node { Node(int val = 0) : data_(val) , next_(nullptr) { } int data_; Node* next_; }; class SList { public: SList(); ~SList(); public: // 尾插法 void push_back(int val); // 头插法 void push_front(int val); // 删除第一个值为 val 节点 void remove(int val); // 删除所有值为 val 的节点 void removeAll(int val); // 搜索是否存在值为 val 的元素 bool find(int val); // 打印 void print() const; private: Node* head_; // 链表头节点 }; SList::SList() : head_(new Node) { } SList::~SList() { while (head_->next_ != nullptr) { Node* node = head_->next_; head_->next_ = node->next_; delete node; } delete head_; head_ = nullptr; } void SList::push_back(int val) { Node* cur = head_; while (cur->next_ != nullptr) { cur = cur->next_; } cur->next_ = new Node(val); } void SList::push_front(int val) { Node* node = new Node(val); node->next_ = head_->next_; head_->next_ = node; } void SList::remove(int val) { Node* cur = head_; while (cur->next_ != nullptr) { if (cur->next_->data_ == val) { Node* node = cur->next_; cur->next_ = node->next_; delete node; break; } cur = cur->next_; } } void SList::removeAll(int val) { Node* cur = head_; while (cur && cur->next_ != nullptr) { if (cur->next_->data_ == val) { Node* node = cur->next_; cur->next_ = node->next_; delete node; } else { cur = cur->next_; } } } bool SList::find(int val) { Node* cur = head_->next_; while (cur) { if (cur->data_ == val) { return true; } cur = cur->next_; } return false; } void SList::print() const { Node* cur = head_->next_; while (cur != nullptr) { cout << cur->data_ << "->"; cur = cur->next_; } cout << endl; }
1.2 单向循环链表

特点:
- 每一个节点除了数据域,还有一个 next 指针域指向下一个节点(存储了下一个节点的地址)
- 末尾节点的指针域指向了头节点
其代码的实现如下:
class CircleLink { public: CircleLink(); ~CircleLink(); public: // 尾插法 void push_back(int val); // 头插法 void push_front(int val); // 删除第一个值为 val 的节点 void remove(int val); // 查询是否存在值为 val 的节点 bool find(int val); // 打印 void print() const; private: struct Node { Node(int data = 0) : data_(data) , next_(nullptr) { } int data_; Node* next_; }; Node* head_; Node* tail_; }; CircleLink::CircleLink() : head_(new Node()) { tail_->next_ = head_; head_->next_ = head_; } CircleLink::~CircleLink() { Node* cur = head_->next_; while (cur != head_) { head_->next_ = cur->next_; delete cur; cur = head_->next_; } delete head_; } void CircleLink::push_back(int val) { Node* node = new Node(val); node->next_ = tail_->next_; tail_->next_ = node; tail_ = node; if (head_->next_ == head_) { head_->next_ = node; } } void CircleLink::push_front(int val) { Node* node = new Node(val); node->next_ = head_->next_; head_->next_ = node; if (node->next_ == head_) { tail_ = node; } } void CircleLink::remove(int val) { if (head_->next_ == head_) return; Node* cur = head_; while (cur->next_ != tail_) { if (cur->next_->data_ == val) { Node* nxt = cur->next_; cur->next_ = nxt->next_; delete nxt; return; } cur = cur->next_; } if (tail_->data_ == val) { cur->next_ = tail_->next_; delete tail_; tail_ = cur; } } bool CircleLink::find(int val) { Node* cur = head_->next_; while (cur != head_) { if (cur->data_ == val) return true; cur = cur->next_; } return false; } void CircleLink::print() const { Node* cur = head_->next_; while (cur != head_) { cout << cur->data_ << "->"; cur = cur->next_; } cout << endl; }
1.3 双向链表

特点:
- 每一个节点除了数据域,还有 next 指针域指向下一个节点,prev 指针域指向前一个节点
- 头节点的 prev 是 NULL,末尾节点的 next 是 NULL
其代码实现如下:
class DoubleLink { public: DoubleLink(); ~DoubleLink(); // 头插法 void push_front(int val); // 尾插法 void push_back(int val); // 删除 void remove(int val); // 查找 bool find(int val); // 打印 void print() const; private: // 节点类型 struct Node { Node(int data = 0) : data_(data) , next_(nullptr) , prev_(nullptr) { } int data_; Node* next_; Node* prev_; }; Node* head_; }; DoubleLink::DoubleLink() : head_(new Node()) { } DoubleLink::~DoubleLink() { Node* cur = head_; while (cur) { head_ = head_->next_; delete cur; cur = head_; } } void DoubleLink::push_back(int val) { Node* cur = head_; while (cur->next_ != nullptr) { cur = cur->next_; } Node* node = new Node(val); cur->next_ = node; node->prev_ = cur; node->next_ = nullptr; } void DoubleLink::push_front(int val) { Node* node = new Node(val); Node* nxt = head_->next_; node->next_ = nxt; node->prev_ = head_; nxt->prev_ = node; head_->next_ = node; } void DoubleLink::remove(int val) { Node* cur = head_->next_; while (cur) { if (cur->data_ == val) { cur->prev_->next_ = cur->next_; if (cur->next_) { cur->next_->prev_ = cur->prev_; } delete cur; break; } cur = cur->next_; } } bool DoubleLink::find(int val) { Node* cur = head_->next_; while (cur) { if (cur->data_ == val) { return true; } cur = cur->next_; } return false; } void DoubleLink::print() const { Node* cur = head_->next_; while (cur) { cout << cur->data_ << "->"; cur = cur->next_; } cout << endl; }
1.4 双向循环链表

特点:
- 每一个节点除了数据域,还有 next 指针域指向下一个节点,prev 指针域指向前一个节点
- 头节点的 prev 指向末尾节点,末尾节点的 next 指向头节点
其代码实现如下:
class DoubleCircleLink { public: DoubleCircleLink(); ~DoubleCircleLink(); // 头插法 void push_front(int val); // 尾插法 void push_back(int val); // 删除 void remove(int val); // 查找 bool find(int val); // 打印 void print() const; private: // 节点类型 struct Node { Node(int data = 0) : data_(data) , next_(nullptr) , prev_(nullptr) { } int data_; Node* next_; Node* prev_; }; Node* head_; }; DoubleCircleLink::DoubleCircleLink() : head_(new Node()) { head_->next_ = head_; head_->prev_ = head_; } DoubleCircleLink::~DoubleCircleLink() { Node* cur = head_->next_; while (cur != head_) { head_->next_ = cur->next_; cur->next_->prev_ = head_; delete cur; cur = head_->next_; } delete head_; head_ = nullptr; } void DoubleCircleLink::push_back(int val) { Node* node = new Node(val); head_->prev_->next_ = node; node->next_ = head_; node->prev_ = head_->prev_; head_->prev_ = node; } void DoubleCircleLink::push_front(int val) { Node* node = new Node(val); Node* nxt = head_->next_; node->next_ = nxt; node->prev_ = head_; nxt->prev_ = node; head_->next_ = node; } void DoubleCircleLink::remove(int val) { Node* cur = head_->next_; while (cur != head_) { if (cur->data_ == val) { cur->prev_->next_ = cur->next_; cur->next_->prev_ = cur->prev_; delete cur; break; } cur = cur->next_; } } bool DoubleCircleLink::find(int val) { Node* cur = head_->next_; while (cur != head_) { if (cur->data_ == val) { return true; } cur = cur->next_; } return false; } void DoubleCircleLink::print() const { Node* cur = head_->next_; while (cur != head_) { cout << cur->data_ << "->"; cur = cur->next_; } cout << endl; }
2. 常见的算法问题
2.1 单链表逆序
如果采用暴力的方法,可以定义一个动态数组,然后遍历单链表,将单链表中的每个元素记录下来,然后使用双指针对数组进行逆序操作,完成之后又将数组变回单链表。如果采用这种方法,那么将会消耗掉很多的额外空间,造成资源的浪费。
更好的方法是进行原地逆序,即不采用额外的存储空间。其过程是对于头节点之后的所有元素,采用头插法依次插入到链表中即可,如下图所示:

练习题目: 206. 反转链表 - 力扣(LeetCode)
2.2 合并两个有序链表
由于两个链表是有序的,因此我们可以采用双指针的思想,对于要合并的链表,定义两个指针,一个指向当前节点,另一个指向当前节点的前驱节点,而对于另一个链表,则只需一个指针遍历即可,如下图所示:

其过程如下:
- 如果 point3 的数值小于 point1 的数值,执行 2,否则执行 8;
- point2 的 next 指向 point3;
- head2 的 next 指向 point3 的 next;
- point3 的 next 指向 point1;
- point2 移动到下一个元素;
- point3 指向 head2 的下一个元素;
- 重复步骤 1;
- point1 和 point2 均移动到下一个元素;
2.3 访问倒数第k个元素
如果采用最暴力的方法,先遍历一次单链表,得到单链表的长度,然后再从头节点开始遍历,直到找到倒数第 k 个元素,那么其需要遍历两次单链表。
更好的一种方法是,使用双指针的思想,定义两个指针,一个快指针,一个慢指针。先让快指针找到正数第 k 个元素,然后慢指针从头开始走,如下图所示:

然后快、慢指针一块走,当快指针指向 NULL 的时候,慢指针所指的元素即为倒数第 k 个元素,如下图所示:

2.4 判断链表是否有环
判断一个单链表是否有环,我们依然可以采用双指针的思想,来分别定义一个快指针、一个慢指针,每一次,快指针走 2 步,而慢指针走 1 步。这就如同我们学过的 "追及相遇问题",即两个人在操场上跑步,一个人跑的快,一个人跑的慢,从同一起点开始跑,两个人一定会在某一点处相遇。

如果要找到环的入口节点,那么该如何做?
我们先定义几个变量,从头节点到环入口的距离我们定义为变量 , 从环入口节点到相遇节点的距离定义为变量 ,从相遇节点到链表末节点的距离定义为变量 ,如图所示:

图中红色节点即为快、慢指针的相遇节点。由于快、慢指针相遇时,快指针所走的距离 为慢指针所走距离 的 2 倍,即存在 ,且由图可知:,而 ,那么有如下推导过程:
由于此时快慢指针均指向相遇节点,即图中的红色节点。那么让慢指针指向链表的头节点,慢指针和快指针同时向后走,一次走一步,两个指针相遇时,即为环入口节点。
2.5 约瑟夫环
约瑟夫环是一个数学的应用问题: 已知 n 个人(以编号 1,2,3...n 分别表示)围坐在一张圆桌周围,从编号为 k 的人开始报数,数到 m 的那个人出列,它的下一个人又从 1 开始报数,数到 m 的那个人又出列,如此规律重复下去,直到圆桌周围的人全部出列,输出人的出列顺序。
约瑟夫环的典型应用就是单向循环链表。
3. STL 实现
3.1 list
STL 的 list 实现了双向循环链表数据结构,其本身和 list 的节点是不同的结构,其节点(node)结构如下:
template<class T> struct __list_node { typedef void* void_pointer; void_pointer prev; void_pointer next; T data; };
显然,这是一个双向链表,且是环状双向链表,如下图所示:

list 不能像 vector 一样使用普通指针作为迭代器,由于其是双向链表,所以迭代器必须具备前移、后移的能力,所以 list 提供的是 Bidirectional Iterators
。而且,插入操作和接合操作都不会造成原有的 list 迭代器失效。甚至 list 的元素删除操作,也只有 “指向被删除元素” 的那个迭代器失效,其他迭代器不受任何影响。
3.2 slist
slist 是 SGI STL 所提供的单向链表,并不在标准规格之内。
其与 list 的主要区别在于:slist的迭代器属于单向的 Forward Iterator
,而后者的迭代器属于双向的 Bidirectional Iterator
。共同的特点是:插入、移除、接合等操作并不会造成原有的迭代器失效。
如图为 slist 的节点和迭代器的设计架构:

注意:比较两个迭代器是否等同时,由于 __slist_iterator
并未对 operator==
实施重载,所以会调用 __slist_iterator_base::operator==
。根据其中定义可以发现两个 slist 迭代器是否相等取决于 __slist_node_base* node
是否相等。
而 operator*()
的定义则是通过将 node 强转为派生类来实现的:
template<class T, class Ref, class Ptr> struct __slist_iterator : public __slist_iterator_base { typedef __slist_node<T> list_node; typedef Ref reference; reference operator*() const { return ((list_node*)node)->data; } };
在 slist 的源码中,end() 的定义有所不同:
template<class T, class Alloc=alloc> class slist { iterator end() { return iterator(0); } };
如下图所示,其定义为 0:

本文作者:Leaos
本文链接:https://www.cnblogs.com/tuilk/p/16980967.html
版权声明:本作品采用知识共享署名-非商业性使用-禁止演绎 2.5 中国大陆许可协议进行许可。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步