JavaGuide项目中的常见链表算法问题解析
链表是数据结构中最基础也是最重要的结构之一,掌握链表相关算法对程序员来说至关重要。本文将深入分析JavaGuide项目中提到的几道经典链表算法题,帮助读者系统性地理解链表操作的核心思想。
两数相加问题
问题描述
给定两个非空链表表示的非负整数,数字按逆序存储在每个节点中。要求将这两个数相加并以相同形式返回一个新的链表。
关键思路
- 哑节点技巧:使用dummy节点简化边界条件处理
- 进位处理:维护一个carry变量记录进位值
- 同步遍历:同时遍历两个链表,处理对应位置的数字相加
算法实现
public ListNode addTwoNumbers(ListNode l1, ListNode l2) {
ListNode dummy = new ListNode(0);
ListNode curr = dummy;
int carry = 0;
while(l1 != null || l2 != null) {
int x = (l1 != null) ? l1.val : 0;
int y = (l2 != null) ? l2.val : 0;
int sum = carry + x + y;
carry = sum / 10;
curr.next = new ListNode(sum % 10);
curr = curr.next;
if(l1 != null) l1 = l1.next;
if(l2 != null) l2 = l2.next;
}
if(carry > 0) {
curr.next = new ListNode(carry);
}
return dummy.next;
}
复杂度分析
- 时间复杂度:O(max(m,n)),m和n分别是两个链表的长度
- 空间复杂度:O(max(m,n)),结果链表的长度最多为max(m,n)+1
链表反转问题
问题描述
将给定的单链表进行反转,返回反转后的链表头节点。
关键思路
- 三指针法:使用pre、curr、next三个指针完成反转
- 逐步反转:每次将当前节点的next指向前一个节点
- 指针移动:处理完当前节点后,三个指针同步前移
算法实现
public ListNode reverseList(ListNode head) {
ListNode pre = null;
ListNode curr = head;
while(curr != null) {
ListNode next = curr.next; // 保存下一个节点
curr.next = pre; // 反转当前节点
pre = curr; // pre指针前移
curr = next; // curr指针前移
}
return pre;
}
复杂度分析
- 时间复杂度:O(n),需要遍历整个链表
- 空间复杂度:O(1),仅使用固定数量的额外空间
链表中倒数第k个节点
问题描述
找出单链表中倒数第k个节点并返回。
关键思路
- 双指针法:使用快慢指针,快指针先走k步
- 同步移动:然后快慢指针一起移动,直到快指针到达末尾
- 边界处理:处理k大于链表长度等特殊情况
算法实现
public ListNode findKthToTail(ListNode head, int k) {
if(head == null || k <= 0) return null;
ListNode fast = head;
ListNode slow = head;
// 快指针先走k步
for(int i = 0; i < k; i++) {
if(fast == null) return null; // k大于链表长度
fast = fast.next;
}
// 同步移动直到快指针到达末尾
while(fast != null) {
fast = fast.next;
slow = slow.next;
}
return slow;
}
复杂度分析
- 时间复杂度:O(n),最多需要遍历链表两次
- 空间复杂度:O(1),仅使用固定数量的指针
删除倒数第N个节点
问题描述
删除链表中倒数第n个节点,并返回链表头节点。
关键思路
- 哑节点技巧:避免处理头节点删除的特殊情况
- 双指针法:快指针先走n+1步,然后同步移动
- 节点删除:当快指针到达末尾时,慢指针指向待删除节点的前驱
算法实现
public ListNode removeNthFromEnd(ListNode head, int n) {
ListNode dummy = new ListNode(0);
dummy.next = head;
ListNode fast = dummy;
ListNode slow = dummy;
// 快指针先走n+1步
for(int i = 0; i <= n; i++) {
fast = fast.next;
}
// 同步移动直到快指针到达末尾
while(fast != null) {
fast = fast.next;
slow = slow.next;
}
// 删除目标节点
slow.next = slow.next.next;
return dummy.next;
}
复杂度分析
- 时间复杂度:O(n),只需遍历链表一次
- 空间复杂度:O(1),使用固定数量的额外空间
合并两个有序链表
问题描述
将两个升序链表合并为一个新的升序链表并返回。
关键思路
- 递归法:比较两个链表头节点,较小的作为合并后链表的头
- 迭代法:使用新链表逐个比较添加节点
- 剩余处理:当某一链表遍历完后,直接连接另一链表的剩余部分
递归实现
public ListNode mergeTwoLists(ListNode l1, ListNode l2) {
if(l1 == null) return l2;
if(l2 == null) return l1;
if(l1.val < l2.val) {
l1.next = mergeTwoLists(l1.next, l2);
return l1;
} else {
l2.next = mergeTwoLists(l1, l2.next);
return l2;
}
}
迭代实现
public ListNode mergeTwoLists(ListNode l1, ListNode l2) {
ListNode dummy = new ListNode(0);
ListNode curr = dummy;
while(l1 != null && l2 != null) {
if(l1.val < l2.val) {
curr.next = l1;
l1 = l1.next;
} else {
curr.next = l2;
l2 = l2.next;
}
curr = curr.next;
}
curr.next = (l1 != null) ? l1 : l2;
return dummy.next;
}
复杂度分析
- 时间复杂度:O(m+n),需要遍历两个链表的所有节点
- 空间复杂度:递归为O(m+n),迭代为O(1)
总结
通过这五道经典链表算法题的解析,我们可以总结出链表问题的一些通用解法:
- 双指针技巧:快慢指针、前后指针等可以解决大部分链表遍历问题
- 哑节点技巧:简化头节点处理的边界条件
- 递归思想:适合处理链表反转、合并等可分治的问题
- 画图辅助:在解决复杂链表问题时,画出示意图有助于理清思路
掌握这些基础算法后,可以进一步挑战更复杂的链表问题,如环形链表检测、链表排序、复杂链表复制等。链表操作是算法面试中的高频考点,需要反复练习才能熟练掌握。
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考