【LeetCode每日一题】19. 删除链表的倒数第 N 个结点 24. 两两交换链表中的节点
每日一题
- 19. 删除链表的倒数第 N 个结点
- 题目
- 总体思路
- 算法步骤
- 时间复杂度与空间复杂度
- 代码
- 知识点
- 哑节点的核心作用
- 1. 统一处理删除头节点的情况
- 2. 避免空指针异常
- 3. 简化指针操作
- 哑节点的工作原理
- 链表结构变化
- 哑节点的其他应用场景
- 1. 链表反转
- 2. 链表合并
- 3. 链表排序
- 24. 两两交换链表中的节点
- 题目
- 总体思路
- 算法步骤
- 时间复杂度与空间复杂度
- 代码
2025.9.1
19. 删除链表的倒数第 N 个结点
题目
给你一个链表,删除链表的倒数第 n 个结点,并且返回链表的头结点。
示例 1:
输入:head = [1,2,3,4,5], n = 2
输出:[1,2,3,5]
示例 2:
输入:head = [1], n = 1
输出:[]
示例 3:
输入:head = [1,2], n = 1
输出:[1]
提示:
链表中结点的数目为 sz
1 <= sz <= 30
0 <= Node.val <= 100
1 <= n <= sz
进阶:你能尝试使用一趟扫描实现吗?
总体思路
这个算法采用快慢指针+哑节点的技巧来删除链表的倒数第n个节点。核心思想是让快指针先移动n步,然后快慢指针同时移动,当快指针到达链表末尾时,慢指针正好指向要删除节点的前一个节点。
算法步骤
- 创建哑节点:作为头节点的前驱,简化边界处理
- 快指针先移动n步:创建n个节点的间隔
- 同时移动快慢指针:直到快指针到达最后一个节点
- 删除目标节点:修改慢指针的next指向
- 返回结果:返回哑节点的下一个节点
时间复杂度与空间复杂度
- 时间复杂度:O(L)
- L 是链表的长度
- 只需要遍历链表一次
- 快指针最多移动 L 步
- 空间复杂度:O(1)
- 只使用了常数级别的额外空间(哑节点和两个指针)
- 原地操作,不依赖链表长度
代码
golang
func removeNthFromEnd(head *ListNode, n int) *ListNode {dummy := &ListNode{Next: head}fast := dummyslow := dummyfor i:=0; i<n; i++ {fast = fast.Next}for fast.Next != nil {fast = fast.Nextslow = slow.Next}slow.Next = slow.Next.Nextreturn dummy.Next
}
/*** 删除链表的倒数第 N 个结点* 使用快慢指针和哑节点技巧来高效解决* * @param head *ListNode 链表的头节点* @param n int 要删除的倒数第n个节点的位置* @return *ListNode 删除节点后的新链表头节点*/
func removeNthFromEnd(head *ListNode, n int) *ListNode {// 创建哑节点(dummy node)作为头节点的前驱// 作用:1. 简化删除头节点的特殊情况 2. 统一操作逻辑dummy := &ListNode{Next: head}// 初始化快慢指针,都指向哑节点// fast: 快指针,用于创建间隔和定位链表末尾// slow: 慢指针,用于定位要删除节点的前一个位置fast := dummyslow := dummy// 第一阶段:快指针先移动n步// 这样快慢指针之间就保持了n个节点的间隔for i := 0; i < n; i++ {// 快指针向前移动一步// 由于有哑节点的存在,不需要担心空指针问题fast = fast.Next}// 第二阶段:快慢指针同时移动// 循环条件:快指针的下一个节点不为nil(即快指针不是最后一个节点)// 当循环结束时,快指针指向最后一个节点,慢指针指向倒数第n+1个节点for fast.Next != nil {fast = fast.Next // 快指针移动一步slow = slow.Next // 慢指针移动一步}// 删除操作:将慢指针的next指向下下一个节点// 此时slow指向倒数第n+1个节点,slow.Next就是要删除的倒数第n个节点// slow.Next.Next可能是nil(如果要删除的是最后一个节点)或有效的节点slow.Next = slow.Next.Next// 返回哑节点的下一个节点,即新链表的头节点// 如果删除的是原头节点,dummy.Next就是新的头节点return dummy.Next
}
知识点
哑节点的核心作用
1. 统一处理删除头节点的情况
没有哑节点:
- 删除头节点需要特殊处理
- 需要额外的条件判断
有哑节点:
- 头节点变成第二个节点
- 删除操作统一处理
2. 避免空指针异常
// 没有哑节点,可能访问空指针
if head == nil {return nil
}// 有哑节点,始终有有效的节点可以操作
dummy := &ListNode{Next: head} // 即使head为nil,dummy也存在
3. 简化指针操作
删除头节点的对比:
// 没有哑节点:需要特殊处理
if 要删除的是头节点 {return head.Next // 直接返回第二个节点
}// 有哑节点:统一处理
slow.Next = slow.Next.Next // 无论删除哪个节点,操作都一样
return dummy.Next // 总是返回哑节点的下一个
哑节点的工作原理
链表结构变化
原始链表:
head → node1 → node2 → node3 → nil
添加哑节点后:
dummy → head → node1 → node2 → node3 → nil
哑节点的其他应用场景
1. 链表反转
func reverseList(head *ListNode) *ListNode {dummy := &ListNode{}current := headfor current != nil {next := current.Nextcurrent.Next = dummy.Nextdummy.Next = currentcurrent = next}return dummy.Next
}
2. 链表合并
func mergeTwoLists(l1, l2 *ListNode) *ListNode {dummy := &ListNode{}tail := dummy// ...合并逻辑return dummy.Next
}
3. 链表排序
func sortList(head *ListNode) *ListNode {dummy := &ListNode{Next: head}// ...排序逻辑return dummy.Next
}
24. 两两交换链表中的节点
题目
给你一个链表,两两交换其中相邻的节点,并返回交换后链表的头节点。你必须在不修改节点内部的值的情况下完成本题(即,只能进行节点交换)。
示例 1:
输入:head = [1,2,3,4]
输出:[2,1,4,3]
示例 2:
输入:head = []
输出:[]
示例 3:
输入:head = [1]
输出:[1]
提示:
链表中节点的数目在范围 [0, 100] 内
0 <= Node.val <= 100
总体思路
链表两两交换节点的核心难点是 指针重连顺序。
如果直接在原链表上操作,需要同时修改 3 组指针,一不小心就会断链。
算法步骤
- 引入 虚拟头节点 dummy,使“头节点”也有前驱,简化边界判断。
- 每次循环只处理 两个节点(first、second)。
- 四步指针重连:
- pre → second
- first → second.Next
- second → first
- pre 前移到 first(下一轮的 pre)
- 循环条件:
pre.Next != nil && pre.Next.Next != nil
保证剩余节点至少有两个,避免空指针或单节点无法成对的问题。
时间复杂度与空间复杂度
时间复杂度 O(n) 每个节点被访问一次,常数级交换操作。
空间复杂度 O(1) 仅用若干指针变量,没有递归栈或额外切片。
代码
golang
一开始写的,并不好
/*** 两两交换链表中的节点* 将链表中每两个相邻节点交换位置* 示例: 1->2->3->4 变成 2->1->4->3* * @param head *ListNode 链表的头节点* @return *ListNode 交换后的链表头节点*/
func swapPairs(head *ListNode) *ListNode {// 边界条件处理:空链表或单节点链表直接返回if head == nil || head.Next == nil {return head}// 创建哑节点,简化头节点交换的处理// 哑节点的Next指向原链表的头节点dummy := &ListNode{Next: head}// 初始化三个指针:// pre - 指向当前要交换的两个节点的前一个节点// first - 指向要交换的第一个节点// second - 指向要交换的第二个节点pre := dummyfirst := pre.Nextsecond := first.Next// 循环交换节点,直到没有更多节点对可交换for first != nil && first.Next != nil {// 交换first和second节点:// 1. 将pre的Next指向second(将前驱节点连接到第二个节点)pre.Next = second// 2. 将first的Next指向second的Next(第一个节点连接到下一对节点)first.Next = second.Next// 3. 将second的Next指向first(第二个节点指向第一个节点,完成交换)second.Next = first// 移动指针到下一对要交换的节点:// pre移动到当前交换后的第二个节点(实际上是原来的第一个节点)pre = first// first移动到下一对的第一个节点first = pre.Next// 如果first为nil,说明没有更多节点了,退出循环if first == nil {break}// second移动到下一对的第二个节点second = first.Next}// 返回哑节点的下一个节点,即新链表的头节点return dummy.Next
}
/*** Definition for singly-linked list.* type ListNode struct {* Val int* Next *ListNode* }*/
func swapPairs(head *ListNode) *ListNode {if head == nil || head.Next == nil {return head}denny := &ListNode{Next: head}pre := dennyfirst := pre.Nextsecond := first.Nextfor first != nil && first.Next != nil {pre.Next = secondfirst.Next = second.Nextsecond.Next = firstpre = firstfirst = pre.Nextif first == nil {break}second = first.Next}return denny.Next
}
ai优化:
func swapPairs(head *ListNode) *ListNode {dummy := &ListNode{Next: head}pre := dummyfor pre.Next != nil && pre.Next.Next != nil {first := pre.Nextsecond := first.Nextpre.Next = secondfirst.Next = second.Nextsecond.Next = firstpre = first}return dummy.Next
}
/*** Definition for singly-linked list.* type ListNode struct {* Val int* Next *ListNode* }*/func swapPairs(head *ListNode) *ListNode {// 1. 创建虚拟头节点,统一所有节点的操作方式dummy := &ListNode{Next: head}// 2. pre 指向“当前待交换节点对”的前驱pre := dummy// 3. 只要后面还有两个节点,就继续交换for pre.Next != nil && pre.Next.Next != nil {// 3-1 取出两个待交换节点first := pre.Next // 第 1 个节点second := first.Next // 第 2 个节点// 3-2 四步指针重连pre.Next = second // 前驱指向 secondfirst.Next = second.Next // 第 1 个节点指向下一对的开头second.Next = first // 第 2 个节点指向第 1 个节点// 3-3 pre 移动到“下一对”的前驱(即本轮 first)pre = first}// 4. dummy.Next 始终指向新头节点return dummy.Next
}