[灵感源于算法] 链表类问题技巧总结

[灵感源于算法] 链表类问题技巧总结
@水墨不写bug



文章目录


一、链表类题目实用技巧总结

1. 动手画图的重要性

  • 实用性:链表操作需要精确的指针操作,画图可避免逻辑错误,特别在处理复杂指针重定向时
  • 示例:反转链表(可视化指针变化)
cpp 复制代码
ListNode* reverseList(ListNode* head) {
    ListNode *prev = nullptr;  // 前驱指针
    ListNode *curr = head;     // 当前指针
    
    while (curr) {
        // 画图示意:A->B->C 变为 A<-B  C
        ListNode *next = curr->next;  // 临时保存下一个节点
        curr->next = prev;           // 反转指针方向
        prev = curr;                 // 前移prev
        curr = next;                 // 前移curr
    }
    return prev;  // 新链表头
}

图解过程

复制代码
初始: 1 → 2 → 3 → ∅
步骤1: ∅ ← 1  2 → 3 → ∅  (prev=1, curr=2)
步骤2: ∅ ← 1 ← 2  3 → ∅  (prev=2, curr=3)
最终: ∅ ← 1 ← 2 ← 3      (prev=3, curr=null)

优势:避免指针丢失错误,清晰展示每一步状态变化

2. 虚拟头节点技巧

  • 实用性:消除头节点特殊处理,统一操作逻辑,避免空指针异常
  • 示例 :删除所有值为val的节点
cpp 复制代码
ListNode* removeElements(ListNode* head, int val) {
    ListNode dummy(0);         // 创建虚拟头节点
    dummy.next = head;
    ListNode* curr = &dummy;   // 当前指针指向虚拟头
    
    while (curr->next) {
        if (curr->next->val == val) {
            ListNode* toDelete = curr->next;
            curr->next = curr->next->next;  // 跳过待删除节点
            delete toDelete;    // 释放内存
        } else {
            curr = curr->next;  // 正常后移
        }
    }
    return dummy.next;        // 返回真实头节点
}

优势对比

cpp 复制代码
// 无虚拟头节点的实现(需要额外处理头节点)
ListNode* removeElements(ListNode* head, int val) {
    // 特殊处理头节点连续匹配的情况
    while (head && head->val == val) {
        ListNode* temp = head;
        head = head->next;
        delete temp;
    }
    if (!head) return nullptr;
    
    // 处理后续节点
    ListNode* curr = head;
    while (curr->next) {
        if (curr->next->val == val) {
            ListNode* temp = curr->next;
            curr->next = temp->next;
            delete temp;
        } else {
            curr = curr->next;
        }
    }
    return head;
}

优势:虚拟头节点统一了删除逻辑,代码更简洁,避免重复处理头节点

3. 多指针辅助技巧

  • 实用性:使用额外指针简化复杂操作,避免指针操作顺序错误
  • 示例:在排序链表中插入节点
cpp 复制代码
ListNode* insertNode(ListNode* head, int val) {
    ListNode* newNode = new ListNode(val);
    
    // 情况1:插入到头节点前面
    if (!head || val < head->val) {
        newNode->next = head;
        return newNode;
    }
    
    // 使用双指针:prev跟踪前驱,curr遍历
    ListNode *prev = head;
    ListNode *curr = head->next;
    
    while (curr && curr->val < val) {
        prev = curr;
        curr = curr->next;
    }
    
    // 插入到prev和curr之间
    prev->next = newNode;
    newNode->next = curr;
    
    return head;
}

优势 :使用prevcurr两个指针,避免在单指针方案中需要记住前驱节点的麻烦

4. 快慢双指针技巧

  • 实用性:解决链表中的位置关系问题,时间复杂度O(N),空间复杂度O(1)
  • 应用1:检测环形链表及环入口
cpp 复制代码
ListNode *detectCycle(ListNode *head) {
    ListNode *slow = head, *fast = head;
    
    // 第一阶段:检测环是否存在
    while (fast && fast->next) {
        slow = slow->next;        // 慢指针走1步
        fast = fast->next->next;  // 快指针走2步
        if (slow == fast) break;  // 相遇说明有环
    }
    
    // 无环情况
    if (!fast || !fast->next) return nullptr;
    
    // 第二阶段:查找环入口
    slow = head;  // 慢指针重置到头
    while (slow != fast) {
        slow = slow->next;  // 两者同步前进
        fast = fast->next;
    }
    return slow;  // 相遇点即为环入口
}
  • 应用2:查找倒数第K个节点
cpp 复制代码
ListNode* getKthFromEnd(ListNode* head, int k) {
    ListNode *fast = head, *slow = head;
    
    // fast先走k步
    for (int i = 0; i < k; ++i) {
        if (!fast) return nullptr; // 链表长度不足k
        fast = fast->next;
    }
    
    // 同步移动直到fast到达末尾
    while (fast) {
        slow = slow->next;
        fast = fast->next;
    }
    return slow;  // slow指向倒数第k个节点
}
  • 应用3:找到链表中点(奇数为中点,偶数为上中点)
cpp 复制代码
ListNode* findMiddle(ListNode* head) {
    ListNode *slow = head, *fast = head;
    
    while (fast && fast->next && fast->next->next) {
        slow = slow->next;        // 慢指针走1步
        fast = fast->next->next;  // 快指针走2步
    }
    return slow;
}

二、能力提升题目推荐

1、leetcode 2. 两数相加(另一种意义上的大数相加)

逆序链表数字相加,其实逆序更加好做。

cpp 复制代码
class Solution {
public:
    ListNode* addTwoNumbers(ListNode* l1, ListNode* l2) {
        ListNode* cur1 = l1,*cur2 = l2,*phead = new ListNode(0),*prev = phead;
        int t = 0;
        while(cur1 || cur2 || t)
        {
            if(cur1)
            {
                t += cur1->val;
                cur1 = cur1->next;
            }
            if(cur2)
            {
                t += cur2->val;
                cur2 = cur2->next;
            }
            prev->next = new ListNode(t % 10);
            t /= 10;
            prev = prev->next;
        }
        ListNode* ret = phead->next;
        delete phead;
        return ret;
    }
};

2、leetcode 25. K 个一组翻转链表

有难度的一道题目,主要思想是:
先统计需要反转多少次,接下来进行
1、同时维护两个链表;
2、头插pcur,pcur再移动到尾部,重复循环头插

cpp 复制代码
class Solution {
public:
    ListNode* reverseKGroup(ListNode* head, int k) {
        /*整体思路:K个一组进行头插,k个插完之后,pcur移动到末尾,再次循环进行k个一组进行头插
        最后不满k个一组的,不用改变链表的结构,放在后面直接链入最终结果链表*/
        ListNode* phead = new ListNode(-1),*cur = head,*pcur = phead;//pcur自己维护一个链表

        int count = 0;
        while(cur)//统计整体节点个数
        {
            ++count;
            cur = cur->next;
        }
        int sp = count / k;//一共需要反转的轮数
        cur = head;//重置cur到head节点
        while(sp--)
        {
            for(int i = 0;i < k;++i)
            {
                ListNode* next = cur->next,*pnext = pcur->next;//p:表示pcur维护的答案链表
                pcur->next = cur;
                cur->next = pnext;
                cur = next;
            }
            while(pcur->next)
                pcur = pcur->next;
        }
        pcur->next = cur;//把剩余的部分接入答案链表中
        ListNode* ret = phead->next;
        delete phead;
        return ret;
    }
};

3、leetcode 24. 两两交换链表中的节点

其实就是上一题的k==2的特殊情况,但是针对这一题实现的逻辑会更加简单

cpp 复制代码
class Solution {
public:
    ListNode* swapPairs(ListNode* head) {
        if(head == nullptr || head->next == nullptr)
            return head;
        ListNode* phead = new ListNode(0);//头节点,简化逻辑
        phead->next = head;
        ListNode* cur = head,*next = head->next,*prev = phead;
        while(cur && cur->next)
        {
            cur->next = next->next;
            next->next = cur;
            prev->next = next;
            
            prev = cur;
            cur = cur->next;
            if(cur)
                next = cur->next;
        }
        cur = phead->next;
        delete phead;
        return cur;
    }
};

三、总结与建议

  1. 画图先行:在纸上画出初始状态、目标状态和中间步骤,特别是涉及多个指针的操作
  2. 虚拟头节点 :当操作可能改变头节点时,优先考虑使用dummy node
  3. 多指针策略
    • 使用prev/curr处理插入/删除
    • 使用curr/next避免指针丢失
    • 复杂操作时创建临时指针保存关键节点
  4. 快慢指针应用场景
    • 环检测:快慢指针是否相遇
    • 环入口:相遇后重置慢指针
    • 中点查找:快指针到末尾时慢指针位置
    • 倒数第K个:快指针先走K步
    • 链表相交:双指针走不同路径

完~
未经作者同意禁止转载

相关推荐
程序员Xu几秒前
大厂机试题解法笔记大纲+按知识点分类+算法编码训练
笔记·算法
DarkChunk1 小时前
[BUG]Cursor C++扩展不支持
算法
怀旧,1 小时前
【数据结构】7. 栈和队列
数据结构
冠离sir1 小时前
代码随想录训练营第三十天 | 452. 用最少数量的箭引爆气球 435. 无重叠区间 763.划分字母区间
算法
数据与人工智能律师1 小时前
数据淘金时代:公开爬取如何避开法律雷区?
网络·人工智能·算法·云计算·区块链
闻缺陷则喜何志丹2 小时前
【强连通分量 拓扑序】P9431 [NAPC-#1] Stage3 - Jump Refreshers|普及+
c++·算法·图论·拓扑序·洛谷·强连通分量
liulilittle3 小时前
通过高级处理器硬件指令集AES-NI实现AES-256-CFB算法。
linux·服务器·c++·算法·安全·加密·openssl
W说编程3 小时前
算法导论第一章:算法基础与排序艺术
c语言·数据结构·算法
titan TV man3 小时前
上海市计算机学会竞赛平台2022年5月月赛丙组最远城市距离
数据结构·算法
慢半拍iii11 小时前
数据结构——D/串
c语言·开发语言·数据结构·c++