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

链表操作与反转

一、链表的基本概念

链表是一种重要的线性数据结构,由一系列节点(Node)组成。每个节点通常包含两个主要部分:

  1. 数据域:存储实际的数据元素
  2. 指针域:包含指向其他节点的引用(指针)

链表根据指针的方向可以分为:

  • 单向链表:每个节点只包含指向下一个节点的指针
  • 双向链表:每个节点包含指向前一个和后一个节点的指针
  • 循环链表:尾节点指向头节点形成环状结构

与数组在内存中的连续存储不同,链表的节点可以在内存中分散存储,通过指针将这些节点"链接"起来形成逻辑上的线性序列。这种存储方式带来了以下特点:

优势:

  1. 动态大小:可以方便地插入和删除节点,不需要预先分配固定空间
  2. 插入/删除效率高:在已知位置操作只需O(1)时间复杂度
  3. 内存利用率高:不需要连续的存储空间

劣势:

  1. 随机访问效率低:需要从头遍历,时间复杂度为O(n)
  2. 额外空间开销:需要存储指针信息
  3. 缓存不友好:分散存储可能导致缓存命中率低

应用场景:

  1. 实现栈、队列等抽象数据类型
  2. 浏览器历史记录(前进/后退)
  3. 音乐播放列表(上一曲/下一曲)
  4. 内存管理中的空闲内存块链表
  5. 哈希表中的冲突解决方法之一

示例操作:

  • 插入:在节点A和B之间插入新节点C

    1. 创建新节点C
    2. 将C的next指向B
    3. 将A的next指向C
  • 删除:删除节点B

    1. 找到B的前驱节点A
    2. 将A的next指向B的next
    3. 释放B的内存

二、单链表与双链表的结构

1. 单链表

在单链表(Singly Linked List)这种线性数据结构中,每个节点(Node)包含两个基本部分:

  1. 数据域(Data Field):存储实际的数据元素
  2. 指针域(Next Pointer):存储指向下一个节点的地址

具体特性如下:

  • 单向性:每个节点仅保留对直接后继节点的引用
  • 终止条件:尾节点的指针被显式设置为nullptr(C++)或NULL(C),表示链表结束
  • 遍历方向:只能从头节点开始顺序向后访问,无法逆向遍历
  • 实现示例:
struct ListNode {int val;        // 数据域ListNode *next; // 指针域ListNode(int x) : val(x), next(nullptr) {}
};

典型应用场景包括:

  1. 实现动态大小的栈(Stack)结构
  2. 操作系统中的进程调度队列
  3. 多项式代数运算的存储表示
  4. 浏览器访问历史记录的前进功能

与双链表的对比:

  • 内存效率:单链表比双链表节省一个指针的存储空间(通常4/8字节)
  • 操作限制:单链表不支持直接删除当前节点(需要知道前驱节点),而双链表可以
  • 实现复杂度:单链表的插入/删除操作需要更谨慎的指针维护

注意事项:

  • 遍历时需要检查current->next != nullptr防止越界
  • 所有节点必须保证指针正确终止,否则会导致内存泄漏或无限循环
  • 在C++11及以上标准中,推荐使用nullptr而非NULL表示空指针

结构示意图:

head → [val1]→[val2]→[val3]→nullptr
2. 双链表

每个节点包含两个指针域:一个prev指针指向其前驱节点(前一个节点),一个next指针指向其后继节点(后一个节点)。这种双向链接的结构使得链表可以从任意节点向前或向后遍历。

具体特性如下:

  1. 头节点(第一个节点)的prev指针始终指向nullptr,表示链表起始位置没有前驱节点
  2. 尾节点(最后一个节点)的next指针始终指向nullptr,表示链表末尾没有后继节点
  3. 中间节点的两个指针都有效:prev指向前一个节点,next指向后一个节点

例如在C++中,双向链表节点的典型定义如下:

struct ListNode {int val;        // 节点存储的数据ListNode* prev; // 指向前驱节点的指针ListNode* next; // 指向后继节点的指针
};

这种双向链接结构使得插入和删除操作更加高效:

  • 在指定节点前插入新节点时,只需修改相邻节点的4个指针
  • 删除节点时,也只需调整相邻节点的2个指针
  • 查找时可以双向遍历,在某些场景下能减少遍历时间

应用场景包括:

  • 浏览器历史记录(前进/后退功能)
  • 音乐播放器的播放列表(上一曲/下一曲)
  • 需要频繁插入删除的缓存系统

结构示意图:

nullptr ←[val1]⇄[val2]⇄[val3]→ nullptr

三、基本操作(单链表为例)

1. 对比数组与链表
操作数组(Array)链表(Linked List)
访问元素O(1)(下标直接访问)O(n)(需从头遍历)
插入/删除O(n)(需移动元素)O(1)(只需修改指针)
内存分配连续内存,大小固定分散内存,动态扩容
空间效率可能有内存浪费无内存浪费
2. 单链表基本操作实现
// 单链表类
class LinkedList {
private:ListNode* head;  // 头节点public:LinkedList() : head(nullptr) {}// 1. 尾插法(在链表末尾插入)void push_back(int val) {ListNode* newNode = new ListNode(val);if (!head) {  // 链表为空时head = newNode;return;}ListNode* cur = head;while (cur->next) cur = cur->next;  // 找到尾节点cur->next = newNode;}// 2. 按值查找ListNode* find(int val) {ListNode* cur = head;while (cur && cur->val != val) {cur = cur->next;}return cur;  // 找到返回节点,否则返回nullptr}// 3. 删除指定值的节点void remove(int val) {if (!head) return;  // 空链表// 处理头节点是目标的情况if (head->val == val) {ListNode* temp = head;head = head->next;delete temp;return;}// 查找目标节点的前驱ListNode* cur = head;while (cur->next && cur->next->val != val) {cur = cur->next;}if (cur->next) {  // 找到目标节点ListNode* temp = cur->next;cur->next = cur->next->next;delete temp;}}// 4. 修改节点值(将旧值改为新值)bool update(int oldVal, int newVal) {ListNode* node = find(oldVal);if (node) {node->val = newVal;return true;}return false;}
};

四、经典应用

1. 链表反转

链表反转的实现步骤与示例

基本概念

链表反转是指将链表的节点顺序完全倒置,例如原链表 1→2→3→null 经过反转后变为 3→2→1→null。这一操作在数据结构和算法中非常常见,常用于优化某些操作(如从尾部遍历)或解决特定问题(如回文链表判断)。

方法1:迭代法
  1. 初始化指针

    • 定义三个指针:prev(指向已反转部分的头节点,初始为 null)、current(指向当前待反转节点,初始为链表头节点)、next(临时保存当前节点的下一个节点)。
    • 例如,初始时 prev = nullcurrent 指向节点 1
  2. 遍历链表并反转指针

    • 步骤 1:保存 current.nextnext(防止断链)。
      • 若当前链表为 1→2→3→null,则 next = 2
    • 步骤 2:将 current.next 指向 prev,完成当前节点的反转。
      • 此时 1→nullprev 更新为 1current 移动到 2
    • 步骤 3:重复上述过程,直到 currentnull
      • 第二次迭代后,链表变为 2→1→nullprev 更新为 2
      • 第三次迭代后,链表变为 3→2→1→nullprev 更新为 3
  3. 终止条件

    • currentnull 时,prev 即为新链表的头节点,返回 prev
应用场景
  1. 双向遍历优化:单链表反转后,可模拟从尾部向前遍历。
  2. 回文判断:通过反转后半部分链表与前半部分比较,判断是否为回文结构。
  3. LRU缓存淘汰:某些实现中需频繁调整链表顺序。
复杂度分析
  • 时间复杂度:O(n),需遍历所有节点。
  • 空间复杂度:迭代法为 O(1),递归法为 O(n)(栈空间)。
ListNode* reverseListIterative(ListNode* head) {ListNode* prev = nullptr;  // 前驱节点ListNode* cur = head;      // 当前节点while (cur) {ListNode* next = cur->next;  // 保存后继节点cur->next = prev;            // 反转指针prev = cur;                  // 移动前驱指针cur = next;                  // 移动当前指针}return prev;  // 新的头节点
}
方法2:递归法(时间O(n),空间O(n),递归栈开销)

递归法是一种自上而下的解法,通过不断分解问题为子问题来实现链表的反转。具体步骤如下:

  1. 递归终止条件:当链表为空或只有一个节点时,直接返回当前节点。因为单节点链表本身就是反转后的结果。

  2. 递归调用:对于当前节点head,先递归处理其后续节点head.next,这个递归调用会一直深入直到到达链表末尾。

  3. 节点反转:在递归返回的过程中,将当前节点的下一节点的next指针指向当前节点(即head.next.next = head),实现局部反转。

  4. 断环处理:将当前节点的next指针置为null,避免产生循环引用。这一步非常重要,否则链表会形成环状结构。

  5. 返回新头节点:递归的最深层会返回新的头节点(即原链表的尾节点),这个节点会在递归过程中一直传递回最外层。

示例代码

public ListNode reverseList(ListNode head) {// 终止条件:空链表或单节点链表if (head == null || head.next == null) {return head;}// 递归处理后续节点ListNode newHead = reverseList(head.next);// 反转操作head.next.next = head;head.next = null;// 返回新的头节点return newHead;
}

应用场景

  • 适用于链表长度适中的情况(递归深度可控)
  • 在函数式编程或递归思想优先的场景中使用
  • 教学演示中常用于展示递归的运作原理

注意事项

  • 对于超长链表可能导致栈溢出(StackOverflowError)
  • 递归调用会产生额外的内存开销(每次递归都需要保存现场)
  • 在实际工程中,迭代法通常是更优的选择
2. 环检测(判断链表是否有环)

使用快慢指针法(Floyd判圈算法)检测链表中的环:

算法原理

  1. 初始化两个指针:
    • 慢指针(slow)每次移动1个节点
    • 快指针(fast)每次移动2个节点
  2. 执行遍历:
    • 在每次迭代中,快指针会比慢指针多移动一个节点
    • 如果链表无环,快指针会先到达链表尾部(NULL节点)
  3. 环检测:
    • 如果链表存在环,快指针最终会从后方追上慢指针
    • 此时两指针指向同一个节点,可确认存在环结构

数学证明

  • 设环外长度为L,环长度为C
  • 当慢指针进入环时,快指针已在环内移动了L步
  • 快指针每次比慢指针多走1步,最多需要C次移动即可追上

应用场景

  1. 链表环检测(如Leetcode 141题)
  2. 寻找环起点(相遇后重置慢指针到head,两指针同速移动)
  3. 计算环长度(第二次相遇时的移动次数)

算法特性

  • 时间复杂度:O(n)
  • 空间复杂度:O(1)
  • 优势:无需额外存储空间,仅用两个指针即可完成检测

边界条件处理

  • 空链表直接返回无环
  • 单节点链表检查next是否为NULL
  • 确保快指针每次移动时检查next和next.next是否存在
bool hasCycle(ListNode *head) {if (!head || !head->next) return false;ListNode* slow = head;ListNode* fast = head->next;while (slow != fast) {if (!fast || !fast->next) return false;  // 快指针到达终点,无环slow = slow->next;fast = fast->next->next;}return true;  // 相遇,有环
}

五、练习

// 测试代码
#include <iostream>
using namespace std;// 打印链表
void printList(ListNode* head) {ListNode* cur = head;while (cur) {cout << cur->val << "→";cur = cur->next;}cout << "null" << endl;
}int main() {// 构建链表:1→2→3→4→5→nullListNode* head = new ListNode(1);head->next = new ListNode(2);head->next->next = new ListNode(3);head->next->next->next = new ListNode(4);head->next->next->next->next = new ListNode(5);cout << "原链表:";printList(head);// 迭代法反转ListNode* reversed1 = reverseListIterative(head);cout << "迭代反转后:";printList(reversed1);// 递归法反转(再次反转回原顺序)ListNode* reversed2 = reverseListRecursive(reversed1);cout << "递归反转后(恢复原顺序):";printList(reversed2);return 0;
}

输出结果:

原链表:1→2→3→4→5→null
迭代反转后:5→4→3→2→1→null
递归反转后(恢复原顺序):1→2→3→4→5→null
http://www.dtcms.com/a/394593.html

相关文章:

  • AI编程 -- 学习笔记
  • 动态规划问题 -- 子数组模型(乘积最大数组)
  • 【AIGC】大模型面试高频考点18-大模型压力测试指标
  • Cannot find a valid baseurl for repo: base/7/x86_64
  • Lowpoly建模练习集
  • 六、kubernetes 1.29 之 Pod 控制器02
  • OpenCV:人脸检测,Haar 级联分类器原理
  • 类和对象 (上)
  • FreeRTOS 队列集(Queue Set)机制详解
  • 【论文速递】2025年第20周(May-11-17)(Robotics/Embodied AI/LLM)
  • 【秋招笔试】2025.09.21网易秋招笔试真题
  • C++ 之 【特殊类设计 与 类型转换】
  • 第14章 MySQL索引
  • Entities - 遍历与查询
  • TargetGroup 全面优化:从六个维度打造卓越用户体验
  • Proxy与Reflect
  • 浅解Letterbox算法
  • 【Triton 教程】triton_language.permute
  • JavaScript洗牌算法实践
  • 掌握timedatectl命令:Ubuntu 系统时间管理指南
  • 【RT Thread】RTT内核对象机制详解
  • Seata分布式事务
  • 用例图讲解
  • makefile原理
  • AUTOSAR CP开发流程总结
  • 通过VNC实现树莓派远程桌面访问
  • linux信号done
  • BeanUtils.copyProperties 映射规则详解
  • 物联网 frid卡控制
  • LeetCode刷题记录----322.零钱兑换(Medium)