快速排序及其扩展应用
2023-06-10
快速排序算法的思想
快速排序是利用分治思想实现的排序算法,其思路描述为:
- 在待排序的数组中选定一个枢轴元素,然后按照这个元素划分整个数组,使得比该元素小的元素都在一侧,比该元素大的元素都在另一侧,升序排序的情况下,就是较小的在左侧,较大的在右侧;
- 因为划分后枢轴元素已经位于最终排序数组中准确位置上(即一直到排序完成,枢轴元素的位置都不用再次改变),所以只需要把递归处理枢轴前的区间和枢轴后的区间,也就是通过划分和递归,把排序问题分而治之;
- 如果一个区间只有一个元素,天然有序,直接返回,否则继续划分-递归的步骤,直到数组排序完成。
快速排序算法的实现
实现快速排序算法的难点是实现一个枢轴划分函数 partition,实现 partition 函数之后,只需要按照划分-递归的思路实现 quickSort 即可,Go 语言的一种实现方式如下:
func partition(arr []int, l, r int) int {
base := arr[l]
i, j := l, r
for i < j {
for i < j && arr[j] >= base {
j--
}
arr[i] = arr[j]
for i < j && arr[i] <= base {
i++
}
arr[j] = arr[i]
}
arr[i] = base
return i
}
func quickSort(arr []int, l, r int) {
if l >= r {
return
}
mid := partition(arr, l, r)
quickSort(arr, l, mid-1)
quickSort(arr, mid+1, r)
}
快速排序的延申应用
寻找数组中第K小(大)元素,最小(最大)的 K 个元素
因为选定枢轴元素并划分完成后,该枢轴元素的右边元素都比它大,左边元素都比它小,所以如果一次划分返回枢轴元素的下标为 m, 那么该位置的元素就是数组中的第 m 小元素,也是第 len(array) - m 大的元素,数组中的前 m 个元素就是数组中最小的K个元素,那么这种划分可以用来求第 K 小(大)元素或较小(大)的 K 个元素,具体思路如下:
- 目标: 求数组 nums 中第 K 小元素
- 算法:
- 对数组进行一次划分,返回的小标为 m
- 如果 m == K-1, 返回nums[m], 否则:
- 如果 K-1 < m, 继续划分数组以m为界限的左边部分,否则,划分右半部分
- 直到 m == k-1
需要注意的是,这种思路借助了快排思想,而快排是一种内部排序算法,即需要把所有元素加载到内存中,因此,这种算法只适合求解内存能容纳的大小的数组的求解,不适合在海量数据(或者数据流)中寻找第 K 大的元素。
此类问题还有一种求解思路是借助小顶堆(大顶堆求解),如果寻找第K大元素或者最大的K个元素,就用小顶堆,相反的,求第K小或者最小的K个元素就用大顶堆。
面试中遇到类似问题,一定要分清场景,如果是输入是个数组,一般两种方法都可行,如果输入是存在数据库或者磁盘中的海量数据,或者一直持续传入的数据流,一般只适用堆的方法。
双指针划分思路的其他应用
这篇博客中的划分,是用一种类似双指针的思路的原地划分思路,不需要额外的内存空间,可以文字描述为:
- 两个指针 i, j 分别指向数组首尾元素,先用一个外部变量 base 存储 nums[i] 的元素,那么相当于取走了 nums[i] 中的元素,nums[i]是空的;
- 当 i < j 时:
- 因为 nums[i] 是 "空的", 那么便从数组右侧找到一个小于 base 的元素 nums[j], 将 nums[j] 存入 nums[i], 此时nums[j] 是"空的";
- 因为 nums[j] 是 "空的", 那么便从数组右侧找到一个小于 base 的元素 nums[i], 将nums[i] 存入 nums[j], 此时nums[i] 是"空的";
- i == j 之前不断循环
- 循环退出后,i == j 并且 nums[i] 是"空的", 将 base 存入 nums[i]
以上思路可以适用于其他的数组划分问题,比如有一个问题是,将给定数组按照奇数在左侧,偶数在右侧划分,这个问题完全可以借用上述算法,从右侧找奇数放到左侧,从左侧找偶数放到右侧,便可以 O(n) 时间复杂度完成原地划分。