今天看到一个网友爆料,说隔壁组来了一个清华姚班的OD,听到这个消息,我都愣住了。这种超硬背景的人才,怎么就去了OD呢?
这不禁让我想起了自己当年刚进公司的时候,面对那些学历逆天的同事,心里有种说不出的压力。
你想,清华姚班的学霸,身后那段简历放在大厂HR眼前,基本就是金光闪闪的证书,谁不想要?而且,像这种级别的学员,别说OD了,进大厂也是分分钟的事情呀~想不通!
不过不管是大厂还是od,人家就凭“清华姚班”这几个字,放在简历上,那真的是镶了金边,走啥路都比你我更宽敞
算法题:翻转对
聊一个比较经典的算法题:翻转对。可能有人一听这个名字就皱了皱眉,觉得有点复杂,但其实只要我们从简单的角度入手,就能慢慢理清楚。别急,我来带你捋一捋。
翻转对(Reverse Pairs)是什么呢?说白了,就是给你一个数组,你需要找出其中的所有“翻转对”。什么是翻转对?假设给你一个数组 arr
,两个索引 i
和 j
满足条件:i < j
且 arr[i] > 2 * arr[j]
,那么这就是一个翻转对。听上去有点抽象,咱们来个例子吧。
假设有个数组 [1, 3, 2, 3, 1]
,我们需要找出其中的所有翻转对。
arr[1] > 2 * arr[4]
,即3 > 2 * 1
,这是一个翻转对。arr[2] > 2 * arr[4]
,即2 > 2 * 1
,也是一个翻转对。
这样,最终的翻转对总共有两个。简单吧?别看问题看起来简单,实际的实现起来就有点挑战了,特别是当数组变得特别大时,直接暴力求解就不太适用了。
暴力解法
首先,我们可以想到暴力解法,那就是一遍一遍地用双层循环去遍历数组,找出所有满足条件的对。这种方法的时间复杂度是 O(n²),也就是对于每一对 i
和 j
,我们都要判断是否满足条件。代码大概长这样:
def reverse_pairs(nums):
count = 0
n = len(nums)
for i in range(n):
for j in range(i + 1, n):
if nums[i] > 2 * nums[j]:
count += 1
return count
简单来说,就是两层循环,比较每一对元素,时间复杂度 O(n²),这对于小数据集还行,但一旦数据量大了,性能就会崩。比如数组有一万条数据,那这两层循环要比较接近 10⁸ 次,能不慢吗?😂
优化思路:使用归并排序
既然暴力方法行不通,那我们得想办法优化。这里我们可以借助归并排序的思想来解决。归并排序的时间复杂度是 O(n log n),所以如果我们利用归并排序的同时,统计在归并过程中的翻转对,我们就能大大提升效率。
具体来说,咱们在归并排序的合并阶段,就可以同时计算翻转对。具体做法是:当我们把两个子数组合并时,如果 left[i] > 2 * right[j]
,那么 left[i]
与 right[j]
到 right[end]
之间的所有元素,都会构成一个翻转对。
这个技巧利用了归并排序的“有序”特性,下面是优化版的代码实现:
def merge_count_split_inv(arr, temp_arr, left, right):
if left == right:
return 0
mid = (left + right) // 2
inv_count = merge_count_split_inv(arr, temp_arr, left, mid)
inv_count += merge_count_split_inv(arr, temp_arr, mid + 1, right)
inv_count += merge_and_count(arr, temp_arr, left, mid, right)
return inv_count
def merge_and_count(arr, temp_arr, left, mid, right):
i = left # Starting index for left subarray
j = mid + 1 # Starting index for right subarray
k = left # Starting index to be sorted
inv_count = 0
while i <= mid and j <= right:
if arr[i] <= arr[j]:
temp_arr[k] = arr[i]
i += 1
else:
temp_arr[k] = arr[j]
j += 1
inv_count += (mid-i + 1) # All the remaining elements in left subarray
# Are greater than arr[j] and contribute to inversions
k += 1
while i <= mid:
temp_arr[k] = arr[i]
i += 1
k += 1
while j <= right:
temp_arr[k] = arr[j]
j += 1
k += 1
for i in range(left, right + 1):
arr[i] = temp_arr[i]
return inv_count
def reverse_pairs(nums):
temp_arr = [0] * len(nums)
return merge_count_split_inv(nums, temp_arr, 0, len(nums) - 1)
在这个优化版的代码中,我们在归并的过程中统计了翻转对。对于每次合并,如果左边的元素大于右边的某个元素的两倍,我们就知道,左边的当前元素和右边的所有剩余元素都会构成翻转对。时间复杂度变成了 O(n log n),这样就可以高效地处理大规模数据。
最终,我们在 reverse_pairs
函数中返回的就是数组中的翻转对数目。
总结
翻转对的计算看似简单,但如果用暴力方法,性能很容易成为瓶颈。我们通过结合归并排序来优化算法,使其时间复杂度降到了 O(n log n),这样即使面对较大的数据集,程序依然能高效运行。大家在处理这类问题时,不妨也尝试运用归并排序的思想,它的优化效果真的很惊人。
对编程、职场感兴趣的同学,大家可以联系我微信:golang404,拉你进入“程序员交流群”。
虎哥作为一名老码农,整理了全网最全《python高级架构师资料合集》。