文章目录
- [82. 删除排序链表中的重复元素 II](#82. 删除排序链表中的重复元素 II)
82. 删除排序链表中的重复元素 II
题目描述
给定一个已排序的链表的头 head , 删除原始链表中所有重复数字的节点,只留下不同的数字 。返回 已排序的链表 。
示例 1:

输入:head = [1,2,3,3,4,4,5]
输出:[1,2,5]
示例 2:

输入:head = [1,1,1,2,3]
输出:[2,3]
提示:
- 链表中节点数目在范围 [0, 300] 内
- -100 <= Node.val <= 100
- 题目数据保证链表已经按升序排列
解题思路
问题深度分析
这是经典的链表操作 问题,也是双指针算法 的典型应用。核心在于处理重复元素,在O(n)时间内删除所有重复节点。
问题本质
给定已排序的链表,删除所有重复数字的节点,只保留不重复的节点。这是一个链表遍历问题,需要处理重复元素的删除。
核心思想
双指针 + 重复元素检测:
- 双指针:使用快慢指针遍历链表
- 重复检测:检测连续重复的元素
- 节点删除:删除所有重复的节点
- 链表重构:重新连接链表
关键技巧:
- 使用
dummy
节点简化边界处理 - 使用
prev
指针记录前一个不重复节点 - 使用
curr
指针遍历链表 - 当发现重复时,跳过所有重复节点
关键难点分析
难点1:重复元素的检测
- 需要准确检测连续重复的元素
- 需要区分单个元素和重复元素
- 需要处理多个连续重复的情况
难点2:节点的删除
- 需要删除所有重复的节点
- 需要正确更新指针关系
- 需要处理边界情况
难点3:链表的重构
- 需要重新连接链表
- 需要处理头节点的变化
- 需要保持链表的完整性
典型情况分析
情况1:一般情况
head = [1,2,3,3,4,4,5]
过程:
1. 1 → 保留
2. 2 → 保留
3. 3,3 → 删除
4. 4,4 → 删除
5. 5 → 保留
结果: [1,2,5]
情况2:头部重复
head = [1,1,1,2,3]
过程:
1. 1,1,1 → 删除
2. 2 → 保留
3. 3 → 保留
结果: [2,3]
情况3:全部重复
head = [1,1,1,1,1]
过程:
1. 1,1,1,1,1 → 全部删除
结果: []
情况4:无重复
head = [1,2,3,4,5]
结果: [1,2,3,4,5]
算法对比
算法 | 时间复杂度 | 空间复杂度 | 特点 |
---|---|---|---|
双指针 | O(n) | O(1) | 最优解法 |
递归 | O(n) | O(n) | 空间复杂度高 |
哈希表 | O(n) | O(n) | 空间复杂度高 |
暴力法 | O(n²) | O(1) | 效率较低 |
注:n为链表长度
算法流程图
主算法流程(双指针)
否 是 是 否 开始: head 创建dummy节点 初始化: prev=dummy, curr=head curr != nil? 返回dummy.Next 检查curr是否重复 curr重复? 跳过所有重复节点 保留curr节点 更新curr指针 更新prev指针 curr = curr.Next
重复元素处理流程
graph TD
A[检测重复元素] --> B{curr.Next != nil && curr.Val == curr.Next.Val?}
B -->|是| C[记录重复值]
B -->|否| D[无重复,保留节点]
C --> E[跳过所有重复节点]
E --> F[更新curr指针]
D --> G[更新prev指针]
F --> H[继续遍历]
G --> H
复杂度分析
时间复杂度详解
双指针算法:O(n)
- 每个节点最多被访问一次
- 重复节点被一次性跳过
- 总时间:O(n)
递归算法:O(n)
- 递归深度为链表长度
- 时间复杂度相同
- 空间复杂度较高
空间复杂度详解
双指针算法:O(1)
- 只使用常数额外空间
- 原地修改链表
- 总空间:O(1)
关键优化技巧
技巧1:双指针算法(最优解法)
go
func deleteDuplicates(head *ListNode) *ListNode {
if head == nil {
return nil
}
// 创建dummy节点简化边界处理
dummy := &ListNode{Next: head}
prev := dummy
curr := head
for curr != nil {
// 检查curr是否重复
if curr.Next != nil && curr.Val == curr.Next.Val {
// 记录重复值
val := curr.Val
// 跳过所有重复节点
for curr != nil && curr.Val == val {
curr = curr.Next
}
// 删除重复节点
prev.Next = curr
} else {
// 保留curr节点
prev = curr
curr = curr.Next
}
}
return dummy.Next
}
优势:
- 时间复杂度:O(n)
- 空间复杂度:O(1)
- 逻辑清晰,易于理解
技巧2:递归算法
go
func deleteDuplicates(head *ListNode) *ListNode {
if head == nil || head.Next == nil {
return head
}
if head.Val == head.Next.Val {
// 跳过所有重复节点
val := head.Val
for head != nil && head.Val == val {
head = head.Next
}
return deleteDuplicates(head)
} else {
// 保留当前节点
head.Next = deleteDuplicates(head.Next)
return head
}
}
特点:使用递归,代码简洁但空间复杂度高
技巧3:哈希表
go
func deleteDuplicates(head *ListNode) *ListNode {
if head == nil {
return nil
}
// 统计每个值的出现次数
count := make(map[int]int)
curr := head
for curr != nil {
count[curr.Val]++
curr = curr.Next
}
// 创建新链表,只保留出现一次的值
dummy := &ListNode{}
prev := dummy
curr = head
for curr != nil {
if count[curr.Val] == 1 {
prev.Next = curr
prev = prev.Next
}
curr = curr.Next
}
prev.Next = nil
return dummy.Next
}
特点:使用哈希表统计,空间复杂度高
技巧4:优化版双指针
go
func deleteDuplicates(head *ListNode) *ListNode {
if head == nil {
return nil
}
dummy := &ListNode{Next: head}
prev := dummy
for prev.Next != nil {
curr := prev.Next
// 检查是否有重复
if curr.Next != nil && curr.Val == curr.Next.Val {
val := curr.Val
// 跳过所有重复节点
for curr != nil && curr.Val == val {
curr = curr.Next
}
prev.Next = curr
} else {
prev = prev.Next
}
}
return dummy.Next
}
特点:优化指针更新逻辑
边界情况处理
- 空链表:返回nil
- 单节点:直接返回
- 全部重复:返回空链表
- 头部重复:需要更新头节点
- 尾部重复:正确处理尾部
测试用例设计
基础测试
输入: head = [1,2,3,3,4,4,5]
输出: [1,2,5]
说明: 一般情况
简单情况
输入: head = [1]
输出: [1]
说明: 单节点情况
特殊情况
输入: head = [1,1,1,2,3]
输出: [2,3]
说明: 头部重复
边界情况
输入: head = []
输出: []
说明: 空链表情况
常见错误与陷阱
错误1:指针更新错误
go
// ❌ 错误:指针更新时机错误
if curr.Val == curr.Next.Val {
// 删除重复节点
prev.Next = curr.Next
curr = curr.Next // 错误:应该跳过所有重复节点
}
// ✅ 正确:跳过所有重复节点
if curr.Val == curr.Next.Val {
val := curr.Val
for curr != nil && curr.Val == val {
curr = curr.Next
}
prev.Next = curr
}
错误2:边界条件错误
go
// ❌ 错误:没有检查边界条件
for curr != nil {
if curr.Val == curr.Next.Val { // 可能越界
// ...
}
}
// ✅ 正确:先检查边界条件
for curr != nil {
if curr.Next != nil && curr.Val == curr.Next.Val {
// ...
}
}
错误3:dummy节点使用错误
go
// ❌ 错误:没有使用dummy节点
func deleteDuplicates(head *ListNode) *ListNode {
// 直接处理head,可能丢失头节点
}
// ✅ 正确:使用dummy节点简化处理
func deleteDuplicates(head *ListNode) *ListNode {
dummy := &ListNode{Next: head}
// 使用dummy节点处理
}
实战技巧总结
- 双指针模板:prev和curr指针配合
- dummy节点:简化边界处理
- 重复检测:准确检测连续重复元素
- 节点删除:正确删除重复节点
- 指针更新:正确更新指针关系
进阶扩展
扩展1:返回删除的节点
go
func deleteDuplicatesWithDeleted(head *ListNode) (*ListNode, []*ListNode) {
// 返回新链表和删除的节点
// ...
}
扩展2:统计重复次数
go
func deleteDuplicatesWithCount(head *ListNode) (*ListNode, map[int]int) {
// 返回新链表和每个值的重复次数
// ...
}
扩展3:支持自定义重复条件
go
func deleteDuplicatesCustom(head *ListNode, isDuplicate func(int, int) bool) *ListNode {
// 支持自定义重复判断条件
// ...
}
应用场景
- 数据处理:清理重复数据
- 链表优化:减少存储空间
- 算法竞赛:链表操作基础
- 系统设计:数据去重
- 数据分析:数据清洗
代码实现
本题提供了四种不同的解法,重点掌握双指针算法。
测试结果
测试用例 | 双指针 | 递归 | 哈希表 | 优化版 |
---|---|---|---|---|
基础测试 | ✅ | ✅ | ✅ | ✅ |
简单情况 | ✅ | ✅ | ✅ | ✅ |
特殊情况 | ✅ | ✅ | ✅ | ✅ |
边界情况 | ✅ | ✅ | ✅ | ✅ |
核心收获
- 双指针算法:链表操作的经典应用
- dummy节点:简化边界处理
- 重复检测:准确检测连续重复元素
- 节点删除:正确删除重复节点
- 指针更新:正确的指针更新时机
应用拓展
- 链表数据处理和清洗
- 算法竞赛基础
- 系统设计应用
- 数据分析技术
- 内存优化技术
完整题解代码
go
package main
import (
"fmt"
)
// ListNode 链表节点定义
type ListNode struct {
Val int
Next *ListNode
}
// =========================== 方法一:双指针算法(最优解法) ===========================
func deleteDuplicates(head *ListNode) *ListNode {
if head == nil {
return nil
}
// 创建dummy节点简化边界处理
dummy := &ListNode{Next: head}
prev := dummy
curr := head
for curr != nil {
// 检查curr是否重复
if curr.Next != nil && curr.Val == curr.Next.Val {
// 记录重复值
val := curr.Val
// 跳过所有重复节点
for curr != nil && curr.Val == val {
curr = curr.Next
}
// 删除重复节点
prev.Next = curr
} else {
// 保留curr节点
prev = curr
curr = curr.Next
}
}
return dummy.Next
}
// =========================== 方法二:递归算法 ===========================
func deleteDuplicates2(head *ListNode) *ListNode {
if head == nil || head.Next == nil {
return head
}
if head.Val == head.Next.Val {
// 跳过所有重复节点
val := head.Val
for head != nil && head.Val == val {
head = head.Next
}
return deleteDuplicates2(head)
} else {
// 保留当前节点
head.Next = deleteDuplicates2(head.Next)
return head
}
}
// =========================== 方法三:哈希表 ===========================
func deleteDuplicates3(head *ListNode) *ListNode {
if head == nil {
return nil
}
// 统计每个值的出现次数
count := make(map[int]int)
curr := head
for curr != nil {
count[curr.Val]++
curr = curr.Next
}
// 创建新链表,只保留出现一次的值
dummy := &ListNode{}
prev := dummy
curr = head
for curr != nil {
if count[curr.Val] == 1 {
prev.Next = curr
prev = prev.Next
}
curr = curr.Next
}
prev.Next = nil
return dummy.Next
}
// =========================== 方法四:优化版双指针 ===========================
func deleteDuplicates4(head *ListNode) *ListNode {
if head == nil {
return nil
}
dummy := &ListNode{Next: head}
prev := dummy
for prev.Next != nil {
curr := prev.Next
// 检查是否有重复
if curr.Next != nil && curr.Val == curr.Next.Val {
val := curr.Val
// 跳过所有重复节点
for curr != nil && curr.Val == val {
curr = curr.Next
}
prev.Next = curr
} else {
prev = prev.Next
}
}
return dummy.Next
}
// =========================== 辅助函数 ===========================
// 创建链表
func createList(vals []int) *ListNode {
if len(vals) == 0 {
return nil
}
head := &ListNode{Val: vals[0]}
curr := head
for i := 1; i < len(vals); i++ {
curr.Next = &ListNode{Val: vals[i]}
curr = curr.Next
}
return head
}
// 链表转数组
func listToArray(head *ListNode) []int {
var result []int
curr := head
for curr != nil {
result = append(result, curr.Val)
curr = curr.Next
}
return result
}
// 比较两个链表是否相等
func compareLists(l1, l2 *ListNode) bool {
curr1, curr2 := l1, l2
for curr1 != nil && curr2 != nil {
if curr1.Val != curr2.Val {
return false
}
curr1 = curr1.Next
curr2 = curr2.Next
}
return curr1 == nil && curr2 == nil
}
// =========================== 测试代码 ===========================
func main() {
fmt.Println("=== LeetCode 82: 删除排序链表中的重复元素 II ===\n")
testCases := []struct {
nums []int
expect []int
}{
{
[]int{1, 2, 3, 3, 4, 4, 5},
[]int{1, 2, 5},
},
{
[]int{1, 1, 1, 2, 3},
[]int{2, 3},
},
{
[]int{1},
[]int{1},
},
{
[]int{},
[]int{},
},
{
[]int{1, 1, 1, 1, 1},
[]int{},
},
{
[]int{1, 2, 3, 4, 5},
[]int{1, 2, 3, 4, 5},
},
{
[]int{1, 1, 2, 2, 3, 3},
[]int{},
},
{
[]int{1, 2, 2, 3, 3, 4},
[]int{1, 4},
},
}
fmt.Println("方法一:双指针算法(最优解法)")
runTests(testCases, deleteDuplicates)
fmt.Println("\n方法二:递归算法")
runTests(testCases, deleteDuplicates2)
fmt.Println("\n方法三:哈希表")
runTests(testCases, deleteDuplicates3)
fmt.Println("\n方法四:优化版双指针")
runTests(testCases, deleteDuplicates4)
}
func runTests(testCases []struct {
nums []int
expect []int
}, fn func(*ListNode) *ListNode) {
passCount := 0
for i, tc := range testCases {
input := createList(tc.nums)
expected := createList(tc.expect)
result := fn(input)
status := "✅"
if !compareLists(result, expected) {
status = "❌"
} else {
passCount++
}
fmt.Printf(" 测试%d: %s\n", i+1, status)
if status == "❌" {
fmt.Printf(" 输入: %v\n", tc.nums)
fmt.Printf(" 输出: %v\n", listToArray(result))
fmt.Printf(" 期望: %v\n", tc.expect)
}
}
fmt.Printf(" 通过: %d/%d\n", passCount, len(testCases))
}