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

数据结构---链表操作技巧

一、双指针法(Two Pointers)

1. 快慢指针(Floyd判圈算法)
  • 原理:用两个指针(快指针每次走2步,慢指针每次走1步),若链表有环则会相遇。
  • 应用场景
    • 检测链表是否有环(相遇则有环)。
    • 找环的入口节点(相遇后慢指针从起点、快指针从相遇点同速走,相遇处为入口)。
    • 找链表中点(快指针到末尾时,慢指针在中点)。
  • 代码示例(找中点)
    ListNode* middleNode(ListNode* head) {ListNode *slow = head, *fast = head;while (fast && fast->next) {slow = slow->next;fast = fast->next->next;}return slow;  // 慢指针指向中点
    }
    
2. 前后指针(窗口,固定间距)
  • 原理:两个指针保持固定距离,用于找倒数第k个节点。
  • 应用场景:删除倒数第k个节点(前指针先移动k步,再与后指针同速移动)。
  • 代码示例(删除倒数第k个节点)
    ListNode* removeNthFromEnd(ListNode* head, int k) {ListNode* dummy = new ListNode(0);dummy->next = head;ListNode *first = dummy, *second = dummy;// 先让first移动k+1步(包含dummy)for (int i = 1; i <= k+1; i++) {first = first->next;}// 同时移动first和second,直到first到末尾while (first) {first = first->next;second = second->next;}// second.next即为倒数第k个节点,删除它second->next = second->next->next;return dummy->next;
    }
    

二、链表翻转(Reverse Linked List)

1. 迭代法
  • 原理:通过修改指针方向,逐个反转节点指向。
  • 应用场景:反转链表(如求解回文链表、K个节点一组反转)。
  • 代码示例
    ListNode* reverseList(ListNode* head) {ListNode* prev = nullptr;ListNode* curr = head;while (curr) {ListNode* nextTemp = curr->next;  // 保存下一个节点curr->next = prev;                // 反转指针prev = curr;                      // 前指针后移curr = nextTemp;                  // 当前指针后移}return prev;  // 新的头节点
    }
    
2. 递归法
  • 原理:通过递归到链表尾部,再逐层反转指针。
  • 代码示例
    ListNode* reverseListRecursive(ListNode* head) {// 递归终止条件:空节点或尾节点if (!head || !head->next) return head;ListNode* newHead = reverseListRecursive(head->next);head->next->next = head;  // 反转当前节点与后继节点的指针head->next = nullptr;     // 原头节点变为尾节点,next置空return newHead;           // 新头节点为原尾节点
    }
    

三、递归处理(Recursion)

1. 适用场景
  • 结构对称的操作:如合并有序链表、判断回文链表。
  • 子问题与原问题形式相同:如翻转链表、树状结构转换。
  • 代码示例(合并两个有序链表)
    ListNode* mergeTwoLists(ListNode* l1, ListNode* l2) {// 递归终止:任一链表为空时返回另一个if (!l1) return l2;if (!l2) return l1;// 比较当前节点值,递归合并剩余部分if (l1->val < l2->val) {l1->next = mergeTwoLists(l1->next, l2);return l1;} else {l2->next = mergeTwoLists(l1, l2->next);return l2;}
    }
    

四、哈希表映射(Hash Map)

1. 应用场景:复制带随机指针的链表(Random List Node)
  • 问题:链表节点包含valnextrandom(指向任意节点或空),需复制整个链表。
  • 解决方案
    1. 用哈希表记录原节点到新节点的映射。
    2. 先复制valnext,再通过映射处理random指针。
  • 代码示例
    Node* copyRandomList(Node* head) {if (!head) return nullptr;unordered_map<Node*, Node*> map;// 第一步:复制节点并建立映射Node* curr = head;while (curr) {map[curr] = new Node(curr->val);curr = curr->next;}// 第二步:处理next和random指针curr = head;while (curr) {map[curr]->next = map[curr->next];map[curr]->random = map[curr->random];curr = curr->next;}return map[head];
    }
    

五、链表分割(Partition List)

1. 原理:根据值将链表分为两部分(小于x的节点在前,其余在后)。
  • 应用场景:LeetCode 86. 分隔链表。
  • 代码示例
    ListNode* partition(ListNode* head, int x) {ListNode* small = new ListNode(0);  // 小于x的链表头ListNode* large = new ListNode(0);  // 大于等于x的链表头ListNode* s = small;ListNode* l = large;// 遍历原链表,按值分类while (head) {if (head->val < x) {s->next = head;s = s->next;} else {l->next = head;l = l->next;}head = head->next;}// 合并两部分链表,尾节点置空l->next = nullptr;s->next = large->next;return small->next;
    }
    

六、快慢指针找环(Floyd判圈算法扩展)

1. 检测环并找入口
  • 步骤
    1. 快慢指针相遇时,慢指针走了a步,快指针走了2a步,环长为b,则相遇点距入口为a - k*b(k为整数)。
    2. 让慢指针从起点、快指针从相遇点同速走,相遇处即为环入口。
  • 代码示例
    ListNode* detectCycle(ListNode* head) {if (!head || !head->next) return nullptr;ListNode *slow = head, *fast = head;// 第一步:找相遇点while (fast && fast->next) {slow = slow->next;fast = fast->next->next;if (slow == fast) break;}if (!fast || !fast->next) return nullptr;  // 无环// 第二步:找环入口slow = head;while (slow != fast) {slow = slow->next;fast = fast->next;}return slow;
    }
    

七、哨兵节点(Sentinel Node)

1. 与虚拟头节点的区别
  • 虚拟头节点:固定在链表头部,用于简化头节点操作。
  • 哨兵节点:泛指用于标记边界的特殊节点(如尾哨兵nullptr),用于判断链表结束。
  • 应用场景:遍历链表时判断是否到达末尾(curr != nullptr)。

八、归并排序(链表版)

1. 原理
  • 用快慢指针找中点,将链表分为两半。
  • 递归排序两半链表,再合并有序链表。
  • 应用场景:对链表进行排序(时间复杂度O(n log n),优于冒泡/插入排序)。
  • 代码示例
    ListNode* sortList(ListNode* head) {// 终止条件:空链表或单节点链表if (!head || !head->next) return head;// 找中点ListNode *slow = head, *fast = head->next;while (fast && fast->next) {slow = slow->next;fast = fast->next->next;}// 分割链表ListNode* secondHalf = slow->next;slow->next = nullptr;  // 切断第一半与第二半的连接// 递归排序两半链表ListNode* l1 = sortList(head);ListNode* l2 = sortList(secondHalf);// 合并有序链表return mergeTwoLists(l1, l2);
    }
    

技巧总结与应用场景对比

技巧核心思想典型场景时间复杂度
双指针(快慢)不同速度移动指针找中点、检测环、找环入口O(n)
链表翻转反转指针方向回文链表、K组反转、链表逆序O(n)
递归处理将问题分解为子问题合并有序链表、树状转换、对称操作O(n)
哈希表映射记录节点映射关系复制带随机指针的链表O(n)
链表分割按值分类构建新链表分隔链表(如小于x的节点在前)O(n)
归并排序分治思想+双指针+合并有序链表链表排序O(n log n)

注意事项

  1. 内存管理:动态创建的节点需手动释放(C++中用delete,Java中由GC处理)。
  2. 边界条件:处理空链表、单节点链表时避免指针越界。
  3. 空间复杂度:哈希表等技巧可能增加O(n)空间,需根据场景选择。

九、虚拟头结点(Dummy Node)

1. 原理

虚拟头结点(Dummy Node)是一个人为创建的、不存储实际数据的节点,它的next指针指向链表的真正头结点。通过引入这个额外节点,可以统一处理链表的各种操作(尤其是涉及头结点修改的情况),避免因头结点为空或被删除而导致的边界条件判断问题。

2. 应用场景
  • 链表的插入操作(特别是在头结点前插入新节点)。
  • 链表的删除操作(特别是删除头结点)。
  • 合并两个有序链表。
  • 反转链表的部分节点等。
3. 代码示例(删除特定值的节点)

不使用虚拟头结点时,需要单独处理头结点被删除的情况:

ListNode* removeElements(ListNode* head, int val) {// 单独处理头结点需要删除的情况while (head != nullptr && head->val == val) {ListNode* temp = head;head = head->next;delete temp;}ListNode* curr = head;// 处理非头结点的删除while (curr != nullptr && curr->next != nullptr) {if (curr->next->val == val) {ListNode* temp = curr->next;curr->next = curr->next->next;delete temp;} else {curr = curr->next;}}return head;
}

使用虚拟头结点后,所有节点的删除操作可以统一处理:

ListNode* removeElements(ListNode* head, int val) {// 创建虚拟头结点,其next指向真正的头结点ListNode* dummy = new ListNode(0);dummy->next = head;ListNode* curr = dummy;  // 从虚拟头结点开始遍历while (curr->next != nullptr) {if (curr->next->val == val) {ListNode* temp = curr->next;curr->next = curr->next->next;delete temp;} else {curr = curr->next;}}ListNode* result = dummy->next;  // 真正的头结点可能已被修改delete dummy;  // 释放虚拟头结点的内存return result;
}
4. 优势总结
  • 简化逻辑:无需单独判断头结点是否需要修改或删除,所有节点的操作方式保持一致。
  • 避免空指针异常:当链表为空(head == nullptr)时,虚拟头结点仍能保证代码正常执行。
  • 提高可读性:减少了边界条件的判断,使代码结构更清晰。

虚拟头结点是链表操作中的常用技巧,尤其在处理复杂链表问题时能显著简化代码逻辑,降低出错概率。

http://www.dtcms.com/a/355885.html

相关文章:

  • 关于PCB面试问题
  • 01.<<基础入门:了解网络的基本概念>>
  • 大模型微调示例三之Llama-Factory_Lora
  • 机器学习和高性能计算中常用的几种浮点数精度
  • 拼团商城源码分享拼团余额提现网站定制开发源码二开
  • 二叉树高度-递归方式
  • 大模型应用开发与大模型开发有什么区别?
  • c语言动态数组扩容
  • [数据结构] 复杂度和包装类和泛型
  • 虚函数指针和虚函数表的创建时机和存放位置
  • AI记忆革命:从七秒遗忘到终身学习
  • 线程池的执行原理
  • set_property CLOCK_DEDICATED_ROUTE BACKBONE/FALSE对时钟进行约束
  • 强化学习之GRPO
  • 硬件IIC使用问题汇总
  • 错误模块路径: C:\Windows\Microsoft.NET\Framework64\v4.0.30319\clr.dll
  • IMIX数据全链路解析
  • 探索淀粉深加工的无限可能:2026 济南展览会前瞻
  • KeyHydra 2.008 安装教程 3ds Max 2020-2024 详细步骤图解(附安装包下载)
  • 【JavaScript】递归的问题以及优化方法
  • week5-[一维数组]去重
  • (笔记)Android窗口管理系统分析
  • 向量方法证明正余弦定理的数学理论体系
  • 如何保证数据的安全性和隐私性?
  • Spring Boot + KingbaseES 连接池实战
  • TypeScript:枚举类型
  • Milvus向量数据库是什么?
  • Active Directory Basics
  • UPAM(Unified Prompt Attack Model
  • 应急响应/windows权限维持/Linux权限维持