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

一、双指针法(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)时,虚拟头结点仍能保证代码正常执行。
  • 提高可读性:减少了边界条件的判断,使代码结构更清晰。

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

posted @ 2025-09-07 18:36  yjbjingcha  阅读(64)  评论(0)    收藏  举报