C/C++ 数据结构(六)链表迭代器与底层

本篇 核心知识 :自定义双向链表迭代器、链表删除 / 插入 / 拼接 / 反转操作、链表冒泡排序、STL 容器嵌套、容器底层与面试要点


一、双向链表迭代器详解

概念

迭代是对原生指针的封装类 ,自定义双向链表的迭代器本质就是封装节点指针,重载++--*->等运算符,模拟指针行为,实现统一遍历接口。迭代器隶属于对应容器,不同容器迭代器底层实现不同。

特性

  1. 双向链表迭代器支持前置 ++/ 后置 ++、前置 --/ 后置 --,可双向遍历;

  2. 前置自增 / 自减:先移动指针,再返回自身,可返回引用;

  3. 后置自增 / 自减:先保留当前状态,再移动指针,需生成临时对象,不能返回引用

  4. begin():指向链表第一个有效数据节点;

  5. end():指向尾结点(尾后位置),遵循 ST 标准前闭后开遍历区间;

  6. 迭代器仅操作节点指针,不修改节点数据本身。

代码示例(双向链表迭代器完整实现)

复制代码
#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迭代器底层就是普通指针,无需重载过多运算符;链表、树等容器必须自定义迭代器,这是不同容器迭代器的核心区别。


二、链表删除操作(按迭代器删除)

概念

接收迭代器参数,删除该迭代器指向的节点,修改前后节点的指针指向,释放内存并返回下一个有效迭代器(解决迭代器失效问题)。

特性

  1. 操作核心:先保存待删节点的后继节点,再修改前驱与后继的指针连接;

  2. 步骤:保存后继 → 重定向指针 → 释放节点内存 → 返回后继迭代器;

  3. 带头尾结点链表无需判断边界,代码统一;

  4. 删除后原迭代器失效,必须使用返回值继续遍历。

代码示例

复制代码
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)区间内所有节点(遵循前闭后开规则),仅修改首尾指针,批量摘除中间节点并释放内存。

特性

  1. 区间为左闭右开,end指向的节点不删除;

  2. 先记录区间首尾的前驱、后继节点,再批量释放中间节点;

  3. 操作完成后将两端节点重新连接。

代码示例

复制代码
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;
}

四、链表拼接操作

概念

将一个完整链表整体插入到当前链表指定迭代器位置,合并两条链表,原链表置空(不释放内存)

特性

  1. 仅修改指针指向,不拷贝节点数据,效率极高;

  2. 拼接完成后,原链表必须重置为空链表(头结点 next 指向尾结点),防止野指针;

  3. 区分:插入到迭代器之前 / 之后,指针修改逻辑不同。

代码示例

复制代码
// 将链表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;
}

拓展

链表拼接相比数组拼接优势极大,数组需要批量拷贝数据,链表仅修改指针。


五、链表冒泡排序

概念

基于冒泡思想对双向链表排序,优先交换节点指针,不交换数据(大数据 / 自定义对象场景性能更优)。

特性

  1. 排序前提:元素个数 ≥ 2,空链表 / 单个节点无需排序;

  2. 冒泡规则:多层循环,外层控制排序趟数,内层相邻节点比较;

  3. 双向链表优势:可通过prev直接获取前驱节点,单链表无法做到;

  4. 每一趟排序都会将当前最大值 "冒泡" 到末尾,下一趟比较次数递减。

代码示例

复制代码
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;
        }
    }
}

对比

数组冒泡:直接交换元素值,内存连续操作简单;

链表冒泡:交换节点指针,逻辑复杂,但海量数据下开销更小。


六、链表反转(头插法实现)

概念

利用头插法遍历原链表,逐个将节点插入新链表头部,实现整体反转。

特性

  1. 不新建节点,仅修改指针指向;

  2. 时间复杂度 (O(n)),一次遍历即可完成;

  3. 双向链表也可直接交换每个节点prevnext指针实现反转。

代码示例(头插法反转)

复制代码
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 / 自定义容器的模板参数可以是另一个容器,实现二维、多层数据结构。

特性

  1. 基础用法:vector<vector<int>> 模拟二维数组;

  2. 进阶用法:vector<list<int>> 模拟哈希表拉链法(解决哈希冲突经典方案);

  3. 模板类型约束:嵌套容器内的类型,必须支持拷贝、对应运算符重载。

代码示例

复制代码
#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)),无内存挪动;

缺点:不支持随机访问,遍历效率低;

适用:频繁随机增删、数据动态变化的场景。

通用编码规范

  1. 等值判断:nullptr == p(常量放左侧),避免漏写等号引发 bug;

  2. 指针操作:优先画图分析指针走向,杜绝断链、野指针;

  3. 内存管理:堆节点使用后必须delete,防止泄漏。


九、补充知识点:容器类型要求

概念

STL 容器 / 模板类对存储类型存在隐性约束。

特性

  1. 自定义类型必须支持拷贝构造(容器添加元素会拷贝对象);

  2. 排序、比较场景,需要重载==<>等运算符;

  3. 类中包含指针成员时,必须实现深拷贝,防止内存错误。

相关推荐
牛油果子哥q1 小时前
AVL平衡树与红黑树深度精讲对比,平衡因子、四大旋转原理、着色规则、平衡策略、性能差异与面试手撕全解
数据结构·c++·面试
汉克老师2 小时前
GESP7级C++考试语法知识(二、指数函数(3、综合练习)
c++·算法·数学建模·指数函数·gesp7级·复利
C++ 老炮儿的技术栈2 小时前
Ubuntu root账号自动登陆
linux·运维·服务器·c语言·c++·ubuntu·visual studio
Irissgwe2 小时前
map/set/multimap/multiset 的底层逻辑与实现
数据结构·c++·算法·二叉树·stl·c·红黑树
IronMurphy2 小时前
【算法五十八】23. 合并 K 个升序链表
数据结构·算法·链表
凡人叶枫3 小时前
Effective C++ 条款39:明智而审慎地使用 private 继承
java·数据库·c++·嵌入式开发
不想写代码的星星3 小时前
伪共享:逻辑无共享,物理打成狗
c++
noipp3 小时前
【无标题】
c语言·数据结构·c++·算法
森G4 小时前
64、完善聊天室程序(TLV拓展)---------网络编程
网络·c++·tcp/ip