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 1 → 2 → 3 → 4 → 5 → null
current
第一步: null ← 1 2 → 3 → 4 → 5 → null
prev current
第二步: null ← 1 ← 2 3 → 4 → 5 → null
prev current
...
最终: null ← 1 ← 2 ← 3 ← 4 ← 5
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)。
解题思路
- 快慢指针找到中间节点
- 反转后半部分链表
- 比较前后两部分是否相同
- (可选)恢复链表原状
代码实现
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)
问题分析
找到环形链表的入口节点。
解题思路:数学推导 + 双指针
- 快慢指针找到相遇点
- 数学推导:从head到入口 = 从相遇点到入口
- 一个指针从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 + 整数圈
算法技巧总结
链表常用技巧
- 双指针法:快慢指针解决中间节点、环检测等问题
- 虚拟头节点:简化边界条件处理
- 多指针迭代:用于反转、分割等操作
- 数学推导:结合数学分析优化算法
复杂度分析要点
- 时间复杂度优先考虑O(n)解法
- 空间复杂度优先考虑O(1)原地操作
- 注意边界条件和特殊输入
调试建议
- 画图分析指针移动过程
- 测试边界情况:空链表、单节点、双节点
- 验证循环链表是否正确处理
这些题目涵盖了链表操作的核心技巧,掌握后能够解决大多数链表相关问题。建议反复练习,理解每个算法的核心思想。