合并、去重、排序三合一算法详解
一、问题理解
1. 任务要求
- 合并两个数组
- 去除重复元素
- 排序(不使用内置的
sort()函数) - 处理边界情况(空数组等)
2. 示例
python
输入: nums1 = [3, 1, 2], nums2 = [2, 4, 3]
合并: [3, 1, 2, 2, 4, 3]
去重: [3, 1, 2, 4] # 去掉了重复的2和3
排序: [1, 2, 3, 4]
二、完整代码实现
python
def merge_and_unique(nums1, nums2):
"""
合并两个数组,去重,然后排序(不使用sort函数)
时间复杂度:O(n²)(冒泡排序部分)
空间复杂度:O(n)
"""
# 1. 合并两个数组
merged = nums1 + nums2
# 2. 去重(使用集合)
unique = [] # 存储去重后的元素
seen = set() # 记录已经见过的元素
for num in merged:
if num not in seen:
seen.add(num)
unique.append(num)
# 3. 排序(使用冒泡排序,不使用sort())
n = len(unique)
# 冒泡排序:外层循环控制排序轮数
for i in range(n):
# 内层循环:相邻元素比较
# 每轮把最大的元素"冒泡"到最后
for j in range(n - i - 1):
if unique[j] > unique[j + 1]:
# 交换两个元素
unique[j], unique[j + 1] = unique[j + 1], unique[j]
return unique
三、代码逐行详解
第一部分:合并数组
python
# 方法1:直接相加(最简单)
merged = nums1 + nums2
# 方法2:使用extend(原地扩展)
merged = nums1.copy() # 先复制,避免修改原数组
merged.extend(nums2)
# 方法3:使用列表推导式
merged = [x for x in nums1] + [x for x in nums2]
# 方法4:使用星号解包(Python 3.5+)
merged = [*nums1, *nums2]
第二部分:去重
python
# 方法1:使用集合(推荐,O(1)查找)
unique = []
seen = set() # 创建空集合
for num in merged:
if num not in seen: # 如果没见过这个数字
seen.add(num) # 记录到集合中
unique.append(num) # 添加到结果列表
# 方法2:使用字典(类似集合)
unique = []
seen = {} # 空字典
for num in merged:
if num not in seen:
seen[num] = True # 值可以是任意值,我们只关心键
unique.append(num)
# 方法3:直接使用set去重(但会失去顺序)
unique = list(set(merged))
# 注意:集合是无序的,所以顺序可能改变
为什么用集合而不用列表检查重复?
python
# ❌ 用列表(慢!O(n)查找)
seen_list = []
for num in merged:
if num not in seen_list: # 每次都要遍历整个列表
seen_list.append(num)
# ✅ 用集合(快!O(1)查找)
seen_set = set()
for num in merged:
if num not in seen_set: # 哈希查找,速度极快
seen_set.add(num)
第三部分:冒泡排序详解
python
# 冒泡排序思想:像水泡一样,大的元素慢慢"浮"到顶部
n = len(unique)
# 外层循环:控制排序轮数
# 每轮把一个最大的元素放到正确位置
for i in range(n):
# 内层循环:比较相邻元素
# n-i-1: 因为最后i个元素已经排好序了
for j in range(n - i - 1):
# 如果前一个元素比后一个大,交换它们
if unique[j] > unique[j + 1]:
# Python交换两个变量的简洁写法
unique[j], unique[j + 1] = unique[j + 1], unique[j]
冒泡排序可视化
初始数组: [5, 3, 8, 1]
第一轮 (i=0):
比较 5和3: 5>3 → 交换 → [3, 5, 8, 1]
比较 5和8: 5<8 → 不交换 → [3, 5, 8, 1]
比较 8和1: 8>1 → 交换 → [3, 5, 1, 8]
第一轮结束,8在正确位置
第二轮 (i=1):
比较 3和5: 3<5 → 不交换 → [3, 5, 1, 8]
比较 5和1: 5>1 → 交换 → [3, 1, 5, 8]
第二轮结束,5在正确位置
第三轮 (i=2):
比较 3和1: 3>1 → 交换 → [1, 3, 5, 8]
第三轮结束,3在正确位置
最终: [1, 3, 5, 8]
为什么是 range(n - i - 1)?
python
# i=0 时: range(n-1) # 比较0到n-2,共n-1次比较
# i=1 时: range(n-2) # 比较0到n-3,共n-2次比较
# i=2 时: range(n-3) # 比较0到n-4,共n-3次比较
# ...
# 因为最后i个元素已经排好序了,不需要再比较
元素交换的三种方法
python
# 方法1:Python特有(推荐)
a, b = b, a
# 方法2:使用临时变量
temp = a
a = b
b = temp
# 方法3:使用加减法(仅限数字)
a = a + b
b = a - b
a = a - b
四、优化版本
1. 优化冒泡排序(提前结束)
python
def bubble_sort_optimized(arr):
"""优化版冒泡排序:如果某轮没有交换,说明已经有序,提前结束"""
n = len(arr)
for i in range(n):
swapped = False # 标记本轮是否发生交换
for j in range(n - i - 1):
if arr[j] > arr[j + 1]:
arr[j], arr[j + 1] = arr[j + 1], arr[j]
swapped = True
# 如果没有发生交换,说明已经有序,提前结束
if not swapped:
break
return arr
2. 完整优化版函数
python
def merge_and_unique_optimized(nums1, nums2):
"""
合并两个数组,去重,优化版冒泡排序
"""
# 1. 合并
merged = nums1 + nums2
# 2. 去重
unique = []
seen = set()
for num in merged:
if num not in seen:
seen.add(num)
unique.append(num)
# 3. 优化版冒泡排序
n = len(unique)
for i in range(n):
swapped = False
for j in range(n - i - 1):
if unique[j] > unique[j + 1]:
unique[j], unique[j + 1] = unique[j + 1], unique[j]
swapped = True
# 如果没有交换,提前结束
if not swapped:
break
return unique
五、使用快速排序(更高效)
1. 快速排序实现
python
def quick_sort(arr):
"""快速排序实现,时间复杂度 O(n log n)"""
if len(arr) <= 1:
return arr
# 选择基准值(这里选中间元素)
pivot = arr[len(arr) // 2]
# 分割数组
left = [x for x in arr if x < pivot]
middle = [x for x in arr if x == pivot]
right = [x for x in arr if x > pivot]
# 递归排序并合并
return quick_sort(left) + middle + quick_sort(right)
def merge_and_unique_quick(nums1, nums2):
"""
使用快速排序的版本(更高效)
"""
# 1. 合并
merged = nums1 + nums2
# 2. 去重
unique = []
seen = set()
for num in merged:
if num not in seen:
seen.add(num)
unique.append(num)
# 3. 快速排序
return quick_sort(unique)
2. 快速排序原理
快速排序步骤:
1. 选择一个"基准"元素(pivot)
2. 将数组分成三部分:小于基准、等于基准、大于基准
3. 递归地对小于和大于的部分进行排序
4. 合并结果
示例:对 [3, 6, 8, 1, 2] 排序
选择基准 6:
小于6: [3, 1, 2]
等于6: [6]
大于6: [8]
递归排序 [3, 1, 2] 和 [8]
最终合并: [1, 2, 3] + [6] + [8] = [1, 2, 3, 6, 8]
六、边界情况处理
1. 空数组情况
python
def merge_and_unique_safe(nums1, nums2):
# 处理空数组
if not nums1 and not nums2:
return []
# 合并(如果为空数组,+操作也能正常工作)
merged = nums1 + nums2
# 去重
if not merged:
return []
unique = []
seen = set()
for num in merged:
if num not in seen:
seen.add(num)
unique.append(num)
# 排序
if len(unique) <= 1:
return unique # 0或1个元素已经有序
# 冒泡排序
n = len(unique)
for i in range(n):
for j in range(n - i - 1):
if unique[j] > unique[j + 1]:
unique[j], unique[j + 1] = unique[j + 1], unique[j]
return unique
2. 测试各种边界情况
python
def test_edge_cases():
test_cases = [
([], []), # 两个空数组
([1, 2, 3], []), # 一个空数组
([], [4, 5, 6]), # 另一个空数组
([1, 1, 1], [1, 1, 1]), # 所有元素相同
([3, 2, 1], [1, 2, 3]), # 反向重复
([9, 8, 7], [1, 2, 3]), # 无重复
([], [1, None, 2]), # 包含None(可能报错)
]
for nums1, nums2 in test_cases:
try:
result = merge_and_unique_safe(nums1, nums2)
print(f"{nums1} + {nums2} → {result}")
except Exception as e:
print(f"{nums1} + {nums2} → 错误: {e}")
七、常见错误与修正
错误1:忘记初始化集合
python
# ❌ 错误:seen = () 创建的是元组,不是集合
seen = () # 这是空元组,不能add,不能检查成员
# ✅ 正确:
seen = set() # 空集合
错误2:冒泡排序范围错误
python
# ❌ 错误:范围太大,会导致索引越界
for j in range(n): # j最大为n-1,但j+1会到n,越界
if unique[j] > unique[j+1]:
...
# ✅ 正确:
for j in range(n - i - 1): # j最大为n-i-2,j+1为n-i-1
if unique[j] > unique[j+1]:
...
错误3:去重逻辑错误
python
# ❌ 错误:先添加再检查
for num in merged:
unique.append(num) # 先添加
if num not in seen: # 再检查(但已经添加了)
seen.add(num)
# 结果:重复元素没有被去除!
# ✅ 正确:先检查再添加
for num in merged:
if num not in seen:
seen.add(num)
unique.append(num)
八、复杂度分析
时间复杂度
- 合并:O(m+n),m和n是两个数组的长度
- 去重:O(m+n),每个元素检查一次
- 冒泡排序 :O(k²),k是去重后的元素数量
- 最好情况:O(k)(优化版,数组已有序)
- 最坏情况:O(k²)(数组完全逆序)
- 总时间复杂度:O(m+n + k²)
空间复杂度
- 合并:O(m+n),需要存储合并后的数组
- 去重:O(k),集合和结果列表
- 排序:O(1)(原地排序)或 O(k)(快速排序递归栈)
- 总空间复杂度:O(m+n)
九、替代方案比较
| 方法 | 优点 | 缺点 | 时间复杂度 |
|---|---|---|---|
| 冒泡排序 | 简单易懂,代码短 | 效率低(O(n²)) | O(n²) |
| 快速排序 | 效率高(O(n log n)) | 递归可能栈溢出 | O(n log n) |
| 使用内置sort | 最简单,效率高 | 不符合题目要求 | O(n log n) |
| 插入排序 | 简单,对小数组高效 | 效率低(O(n²)) | O(n²) |
| 选择排序 | 简单,交换次数少 | 效率低(O(n²)) | O(n²) |
十、完整测试用例
python
def test_merge_and_unique():
print("测试开始...")
print("-" * 50)
# 测试用例
test_cases = [
# (nums1, nums2, 期望结果)
([1, 2, 3], [2, 3, 4], [1, 2, 3, 4]),
([], [1, 2, 3], [1, 2, 3]),
([4, 5, 6], [], [4, 5, 6]),
([], [], []),
([1, 1, 1], [1, 1, 1], [1]),
([5, 3, 1], [2, 4, 6], [1, 2, 3, 4, 5, 6]),
([9, 8, 7], [6, 5, 4], [4, 5, 6, 7, 8, 9]),
([1, 3, 5], [2, 4, 6, 5, 3], [1, 2, 3, 4, 5, 6]),
]
all_passed = True
for nums1, nums2, expected in test_cases:
result = merge_and_unique(nums1, nums2)
if result == expected:
print(f"✓ {nums1} + {nums2} → {result}")
else:
print(f"✗ {nums1} + {nums2} → {result} (期望: {expected})")
all_passed = False
print("-" * 50)
if all_passed:
print("所有测试通过!")
else:
print("有测试失败!")
# 运行测试
test_merge_and_unique()
十一、扩展:支持多种数据类型
python
def merge_and_unique_general(nums1, nums2):
"""
支持多种数据类型的通用版本
"""
# 合并
merged = nums1 + nums2
# 去重(使用字典存储类型和值的组合)
unique = []
seen = set()
for item in merged:
# 为每个元素创建唯一的标识(处理不可哈希类型)
try:
# 如果元素可哈希,直接使用
if item not in seen:
seen.add(item)
unique.append(item)
except TypeError:
# 如果元素不可哈希(如列表),转换为元组
item_tuple = tuple(item) if isinstance(item, list) else item
if item_tuple not in seen:
seen.add(item_tuple)
unique.append(item)
# 排序:需要检查元素是否可比较
try:
# 尝试排序
return sorted(unique) # 使用sorted简化,实际面试中可能需要自己实现
except TypeError:
# 如果元素类型不一致,无法排序
print("警告:元素类型不一致,无法排序")
return unique
十二、面试常见问题
Q1:为什么冒泡排序效率低?
A:冒泡排序需要多次遍历数组,每次只移动一个元素到正确位置。对于n个元素,最多需要n(n-1)/2次比较,时间复杂度为O(n²)。
Q2:如何优化去重过程?
A:
- 使用集合而不是列表检查重复
- 如果数组已排序,可以使用双指针法去重(O(n)时间)
Q3:如果数组很大(比如100万个元素),应该用什么排序?
A:应该用O(n log n)的排序算法,如快速排序、归并排序、堆排序。冒泡排序在大量数据下太慢。
Q4:是否可以在合并时就排序?
A:可以,如果两个数组已经有序,可以使用归并排序中的合并步骤,在合并时保持有序,然后再去重。
十三、练习题
1. 力扣 88. 合并两个有序数组 ⭐⭐
- 链接:https://leetcode.cn/problems/merge-sorted-array/
- 难度:简单
- 题目:将 nums2 合并到 nums1 中,使 nums1 成为一个有序数组
- 与我们的区别 :
- 我们的题目:两个无序数组,需要去重+排序
- 这道题:两个有序数组,不需要去重 ,不需要额外空间(原地合并)
- 核心技巧:从后向前合并,避免覆盖
python
class Solution:
def merge(self, nums1: List[int], m: int, nums2: List[int], n: int) -> None:
"""
原地合并两个有序数组
nums1有足够的空间(长度为m+n)
"""
p1, p2, p = m-1, n-1, m+n-1
while p1 >= 0 and p2 >= 0:
if nums1[p1] > nums2[p2]:
nums1[p] = nums1[p1]
p1 -= 1
else:
nums1[p] = nums2[p2]
p2 -= 1
p -= 1
# 如果nums2还有剩余
while p2 >= 0:
nums1[p] = nums2[p2]
p2 -= 1
p -= 1
2. 力扣 349. 两个数组的交集 ⭐
- 链接:https://leetcode.cn/problems/intersection-of-two-arrays/
- 难度:简单
- 题目:给定两个数组,返回它们的交集
- 与我们的区别 :
- 我们的题目:合并两个数组(取并集)
- 这道题:求两个数组的交集(共同元素)
- 核心技巧:使用集合求交集
python
class Solution:
def intersection(self, nums1: List[int], nums2: List[int]) -> List[int]:
# 方法1:使用集合交集
return list(set(nums1) & set(nums2))
# 方法2:使用哈希表
seen = set(nums1)
result = []
for num in nums2:
if num in seen:
result.append(num)
seen.remove(num) # 避免重复
return result
3. 力扣 350. 两个数组的交集 II ⭐⭐
- 链接:https://leetcode.cn/problems/intersection-of-two-arrays-ii/
- 难度:简单
- 题目:返回两个数组的交集,但元素可以重复出现
- 与我们的区别 :
- 我们的题目:去重
- 这道题:不去重,保留重复次数的最小值
- 核心技巧:使用哈希表计数
python
from collections import Counter
class Solution:
def intersect(self, nums1: List[int], nums2: List[int]) -> List[int]:
# 方法1:使用Counter
c1 = Counter(nums1)
c2 = Counter(nums2)
result = []
for num in c1:
if num in c2:
result.extend([num] * min(c1[num], c2[num]))
return result
# 方法2:排序+双指针
nums1.sort()
nums2.sort()
i = j = 0
result = []
while i < len(nums1) and j < len(nums2):
if nums1[i] < nums2[j]:
i += 1
elif nums1[i] > nums2[j]:
j += 1
else:
result.append(nums1[i])
i += 1
j += 1
return result
4. 力扣 912. 排序数组 ⭐⭐⭐
- 链接:https://leetcode.cn/problems/sort-an-array/
- 难度:中等
- 题目:给你一个整数数组 nums,请你将该数组升序排列
- 与我们的区别 :
- 我们的题目:需要先合并、去重,再排序
- 这道题:只需要排序(但要求高效)
- 核心技巧:需要实现高效的排序算法(不能直接用sort)
python
class Solution:
def sortArray(self, nums: List[int]) -> List[int]:
"""快速排序实现"""
def quick_sort(arr):
if len(arr) <= 1:
return arr
pivot = arr[len(arr)//2]
left = [x for x in arr if x < pivot]
middle = [x for x in arr if x == pivot]
right = [x for x in arr if x > pivot]
return quick_sort(left) + middle + quick_sort(right)
return quick_sort(nums)
- 数组合并(力扣88、21)
- 去重技巧(力扣26、27、349)
- 排序算法(力扣75、148、912)
- 综合应用(力扣56、977)
十四、总结
这个题目考察了三个核心技能:
- 数组操作:合并两个数组
- 去重算法:使用集合高效去重
- 排序算法:手动实现排序(如冒泡排序)
关键点:
- 使用
set()进行高效去重 - 理解冒泡排序的双层循环结构
- 掌握 Python 的元素交换语法
- 注意边界情况(空数组、单个元素等)
面试建议:
- 先完成基本功能,再考虑优化
- 解释算法的时间复杂度和空间复杂度
- 讨论可能的优化方案
- 写出完整的测试用例
记住:即使题目要求不使用 sort(),也要了解不同排序算法的优缺点,以便在面试中深入讨论。