链表、栈、队列经典算法题详解

25 阅读7分钟

1. 杨辉三角(LeetCode 118)

问题分析

杨辉三角的每个数字等于它上方两个数字之和,首尾元素始终为1。

解题思路

  • 使用List嵌套List模拟二维结构
  • 外层List存储每一行,内层List存储当前行的数字
  • 利用动态规划思想:triangle[i][j] = triangle[i-1][j-1] + triangle[i-1][j]

代码实现

java

class Solution {
    public List<List<Integer>> generate(int numRows) {
        List<List<Integer>> triangle = new ArrayList<>();
        
        // 处理第一行
        if (numRows >= 1) {
            List<Integer> firstRow = new ArrayList<>();
            firstRow.add(1);
            triangle.add(firstRow);
        }
        
        // 生成后续行
        for (int i = 1; i < numRows; i++) {
            List<Integer> currentRow = new ArrayList<>();
            
            // 第一个元素总是1
            currentRow.add(1);
            
            // 中间元素通过上一行计算得出
            List<Integer> previousRow = triangle.get(i - 1);
            for (int j = 1; j < i; j++) {
                int sum = previousRow.get(j - 1) + previousRow.get(j);
                currentRow.add(sum);
            }
            
            // 最后一个元素总是1
            currentRow.add(1);
            
            triangle.add(currentRow);
        }
        
        return triangle;
    }
}

复杂度分析

  • 时间复杂度:O(n²)
  • 空间复杂度:O(n²)

2. 链表的中间结点(LeetCode 876)

问题分析

找到链表的中间节点,如果节点数为偶数,返回第二个中间节点。

解题思路:快慢指针法

  • 快指针每次走两步,慢指针每次走一步
  • 当快指针到达末尾时,慢指针正好在中间

代码实现

java

class Solution {
    public ListNode middleNode(ListNode head) {
        if (head == null) return null;
        
        ListNode slow = head;
        ListNode fast = head;
        
        // 注意判断条件的顺序,避免空指针异常
        while (fast != null && fast.next != null) {
            slow = slow.next;
            fast = fast.next.next;
        }
        
        return slow;
    }
}

示例分析

text

链表: 1→2→3→4→5
slow: 1→2→3
fast: 1→3→5→null
结果: 3

链表: 1→2→3→4→5→6
slow: 1→2→3→4
fast: 1→3→5→null
结果: 4

3. 移除链表元素(LeetCode 203)

问题分析

删除链表中所有值等于给定值的节点。

解题思路:双指针法

  • 使用两个指针:当前指针和前驱指针
  • 遍历链表,遇到目标值就跳过该节点
  • 特别注意头节点可能需要被删除的情况

代码实现

java

class Solution {
    public ListNode removeElements(ListNode head, int val) {
        // 处理头节点可能被删除的情况
        while (head != null && head.val == val) {
            head = head.next;
        }
        
        if (head == null) return null;
        
        ListNode prev = head;
        ListNode current = head.next;
        
        while (current != null) {
            if (current.val == val) {
                // 跳过当前节点
                prev.next = current.next;
            } else {
                // 移动前驱指针
                prev = current;
            }
            // 移动当前指针
            current = current.next;
        }
        
        return head;
    }
}

优化版本(使用虚拟头节点)

java

class Solution {
    public ListNode removeElements(ListNode head, int val) {
        // 创建虚拟头节点,简化边界处理
        ListNode dummy = new ListNode(0);
        dummy.next = head;
        
        ListNode prev = dummy;
        ListNode current = head;
        
        while (current != null) {
            if (current.val == val) {
                prev.next = current.next;
            } else {
                prev = current;
            }
            current = current.next;
        }
        
        return dummy.next;
    }
}

4. 反转链表(LeetCode 206)

问题分析

将链表原地反转,要求空间复杂度O(1)。

解题思路:三指针迭代法

  • 使用三个指针:prev、current、next
  • 逐个反转节点指向
  • 最终prev成为新的头节点

代码实现

java

class Solution {
    public ListNode reverseList(ListNode head) {
        if (head == null) return null;
        
        ListNode prev = null;
        ListNode current = head;
        
        while (current != null) {
            ListNode next = current.next;  // 保存下一个节点
            current.next = prev;           // 反转指向
            prev = current;                // 移动prev
            current = next;                // 移动current
        }
        
        return prev;  // prev成为新的头节点
    }
}

反转过程图示

text

初始: null ← prev  12345null
                current

第一步: null1  2345null
          prev  current

第二步: null12  345null
               prev  current

...
最终: null12345
                          prev  current(null)

5. 链表分割(牛客网)

问题分析

将链表按给定值x分割,小于x的节点在前,大于等于x的节点在后。

解题思路:双链表法

  • 创建两个虚拟链表:small和large
  • 遍历原链表,按值分配到两个链表中
  • 连接两个链表并处理边界情况

代码实现

java

public class Partition {
    public ListNode partition(ListNode head, int x) {
        if (head == null) return null;
        
        // 创建两个虚拟头节点
        ListNode smallDummy = new ListNode(0);
        ListNode largeDummy = new ListNode(0);
        
        ListNode smallTail = smallDummy;
        ListNode largeTail = largeDummy;
        ListNode current = head;
        
        // 分配节点到两个链表
        while (current != null) {
            if (current.val < x) {
                smallTail.next = current;
                smallTail = smallTail.next;
            } else {
                largeTail.next = current;
                largeTail = largeTail.next;
            }
            current = current.next;
        }
        
        // 连接两个链表
        smallTail.next = largeDummy.next;
        largeTail.next = null;  // 重要:避免循环链表
        
        return smallDummy.next;
    }
}

6. 链表的回文结构(牛客网)

问题分析

判断链表是否为回文结构,要求时间复杂度O(n),空间复杂度O(1)。

解题思路

  1. 快慢指针找到中间节点
  2. 反转后半部分链表
  3. 比较前后两部分是否相同
  4. (可选)恢复链表原状

代码实现

java

public class PalindromeList {
    public boolean chkPalindrome(ListNode head) {
        if (head == null || head.next == null) return true;
        
        // 1. 找到中间节点
        ListNode middle = findMiddle(head);
        
        // 2. 反转后半部分
        ListNode reversedHalf = reverseList(middle.next);
        
        // 3. 比较前后两部分
        ListNode p1 = head;
        ListNode p2 = reversedHalf;
        boolean isPalindrome = true;
        
        while (p2 != null) {
            if (p1.val != p2.val) {
                isPalindrome = false;
                break;
            }
            p1 = p1.next;
            p2 = p2.next;
        }
        
        // 4. 恢复链表(可选)
        middle.next = reverseList(reversedHalf);
        
        return isPalindrome;
    }
    
    private ListNode findMiddle(ListNode head) {
        ListNode slow = head;
        ListNode fast = head;
        
        while (fast.next != null && fast.next.next != null) {
            slow = slow.next;
            fast = fast.next.next;
        }
        
        return slow;
    }
    
    private ListNode reverseList(ListNode head) {
        ListNode prev = null;
        ListNode current = head;
        
        while (current != null) {
            ListNode next = current.next;
            current.next = prev;
            prev = current;
            current = next;
        }
        
        return prev;
    }
}

7. 相交链表(LeetCode 160)

问题分析

找到两个链表的相交节点,要求时间复杂度O(n),空间复杂度O(1)。

解题思路:双指针法

  • 计算两个链表的长度差
  • 让长链表指针先走差值步
  • 两个指针同步前进,第一个相同节点即为交点

代码实现

java

public class Solution {
    public ListNode getIntersectionNode(ListNode headA, ListNode headB) {
        if (headA == null || headB == null) return null;
        
        // 计算链表长度
        int lenA = getLength(headA);
        int lenB = getLength(headB);
        
        // 对齐起点
        ListNode pA = headA;
        ListNode pB = headB;
        
        if (lenA > lenB) {
            pA = moveForward(pA, lenA - lenB);
        } else {
            pB = moveForward(pB, lenB - lenA);
        }
        
        // 同步前进寻找交点
        while (pA != null && pB != null) {
            if (pA == pB) return pA;
            pA = pA.next;
            pB = pB.next;
        }
        
        return null;
    }
    
    private int getLength(ListNode head) {
        int length = 0;
        ListNode current = head;
        while (current != null) {
            length++;
            current = current.next;
        }
        return length;
    }
    
    private ListNode moveForward(ListNode head, int steps) {
        ListNode current = head;
        for (int i = 0; i < steps && current != null; i++) {
            current = current.next;
        }
        return current;
    }
}

优化版本(更简洁的双指针)

java

public class Solution {
    public ListNode getIntersectionNode(ListNode headA, ListNode headB) {
        if (headA == null || headB == null) return null;
        
        ListNode pA = headA;
        ListNode pB = headB;
        
        // 两个指针走相同的总路程,最终会在交点相遇
        while (pA != pB) {
            pA = (pA == null) ? headB : pA.next;
            pB = (pB == null) ? headA : pB.next;
        }
        
        return pA;
    }
}

8. 环形链表(LeetCode 141)

问题分析

判断链表中是否有环。

解题思路:快慢指针(Floyd判圈算法)

  • 快指针每次走两步,慢指针每次走一步
  • 如果有环,快慢指针最终会相遇
  • 如果无环,快指针会先到达null

代码实现

java

public class Solution {
    public boolean hasCycle(ListNode head) {
        if (head == null || head.next == null) return false;
        
        ListNode slow = head;
        ListNode fast = head;
        
        while (fast != null && fast.next != null) {
            slow = slow.next;
            fast = fast.next.next;
            
            if (slow == fast) return true;
        }
        
        return false;
    }
}

9. 环形链表 II(LeetCode 142)

问题分析

找到环形链表的入口节点。

解题思路:数学推导 + 双指针

  1. 快慢指针找到相遇点
  2. 数学推导:从head到入口 = 从相遇点到入口
  3. 一个指针从head开始,一个从相遇点开始,同步前进,相遇点即为入口

代码实现

java

public class Solution {
    public ListNode detectCycle(ListNode head) {
        if (head == null || head.next == null) return null;
        
        // 1. 找到相遇点
        ListNode meetingPoint = findMeetingPoint(head);
        if (meetingPoint == null) return null;
        
        // 2. 找到环的入口
        ListNode p1 = head;
        ListNode p2 = meetingPoint;
        
        while (p1 != p2) {
            p1 = p1.next;
            p2 = p2.next;
        }
        
        return p1;
    }
    
    private ListNode findMeetingPoint(ListNode head) {
        ListNode slow = head;
        ListNode fast = head;
        
        while (fast != null && fast.next != null) {
            slow = slow.next;
            fast = fast.next.next;
            
            if (slow == fast) return slow;
        }
        
        return null;
    }
}

数学原理

text

设:
- 头节点到环入口距离:a
- 环入口到相遇点距离:b  
- 相遇点到环入口距离:c
- 环周长:b + c

快指针路程:a + n(b + c) + b
慢指针路程:a + b

快指针速度是慢指针2倍:
a + n(b + c) + b = 2(a + b)
=> a = (n-1)(b+c) + c

结论:从head到入口的距离 = 从相遇点走c + 整数圈

算法技巧总结

链表常用技巧

  1. 双指针法:快慢指针解决中间节点、环检测等问题
  2. 虚拟头节点:简化边界条件处理
  3. 多指针迭代:用于反转、分割等操作
  4. 数学推导:结合数学分析优化算法

复杂度分析要点

  • 时间复杂度优先考虑O(n)解法
  • 空间复杂度优先考虑O(1)原地操作
  • 注意边界条件和特殊输入

调试建议

  1. 画图分析指针移动过程
  2. 测试边界情况:空链表、单节点、双节点
  3. 验证循环链表是否正确处理

这些题目涵盖了链表操作的核心技巧,掌握后能够解决大多数链表相关问题。建议反复练习,理解每个算法的核心思想。