快速排序
主要思想
1. 在数组中选一个元素作为基准(通常选用数组第一个元素作为基准);
2. 将数组中小于基准数的数据移到基准数左边,大于基准数的移到右边;
3. 对于基准数左、右两边的数组,不断重复以上两个过程,直到每个子集只有一个元素,即为全部有序。
代码实现
func quickSort(arr []int, begin, end int) {
if begin > end {
return
}
base := arr[begin] // 选择基准,一般选择数组首元素作为排序基准
i, j := begin, end // 前后双指针分别指向首尾元素
for i < j { // i,j双指针相遇时,结束当前循环
for (i < j) && (arr[j] > base) { // 数组右部元素大于基准元素,j指针就一直向前移动
j--
}
for (i < j) && (arr[i] <= base) { // 数组左边元素小于等于基准元素,i指针就一直向后移动
i++
}
arr[i], arr[j] = arr[j], arr[i] // 直到i指针指向的元素大于基准,j指针指向的元素小于基准,此时i和j指向的元素交换位置,完成一对小数在后大数在前的排序
}
arr[i], arr[begin] = arr[begin], arr[i] // 结束外层循环时i == j,此时基准所在元素与当前i指向元素交换位置,完成一次基准与i位置的排序
quickSort(arr, begin, i - 1) // 递归调用自身,完成对[begin, i - 1]下标范围内元素的排序
quickSort(arr, i + 1, end) // 递归调用,完成对[i + 1, end]下标范围内元素的排序
}
复杂度分析
快速排序之所以比较快,是因为与冒泡排序相比,每次的交换是跳跃式的,每次排序的时候设置一个基准点,将小于等于基准点的数全部放到基准点的左边,将大于等于基准点的数全部放到基准点的右边。这样在每次交换的时候就不会像冒泡排序一样每次只能在相邻的数之间进行交换,交换的距离就大的多了。因此总的比较和交换次数就少了,速度自然就提高了。
当然在最坏的情况下,仍可能是相邻的两个数进行了交换。因此快速排序的最差时间复杂度和冒泡排序是一样的都是O(n^2),它的平均时间复杂度为O (nlog2n) ,空间复杂度为O(1)。
快排解决常见面试题
经常遇到的一个问题,是求TOP K的问题,不管是面试后端、大数据开发都经常会被问到这个问题。除了大顶堆(解决Top K小)、小顶堆(解决Top K大)的思路之外,还可以使用快排解决。
经典TOP K的4中通用解决方案
对于经典TopK问题,有如下 4 种通用解决方案:
一、用快排最最最高效解决 TopK 问题:
二、大根堆(前 K 小) / 小根堆(前 K 大),Java中有现成的 PriorityQueue,实现起来最简单:
三、二叉搜索树也可以 解决 TopK 问题哦
四、数据范围有限时直接计数排序就行了:
用快排最高效解决 TopK 问题:O(N)
注意找前 K 大/前 K 小问题不需要对整个数组进行 O(NlogN) 的排序!
求前K小,直接通过快排切分排好第 K 小的数(下标为 K-1),那么它左边的数就是比它小的另外 K-1 个数。
参考链接:https://leetcode-cn.com/problems/zui-xiao-de-kge-shu-lcof/solution/3chong-jie-fa-miao-sha-topkkuai-pai-dui-er-cha-sou/
大根堆(前 K 小) / 小根堆(前 K 大):O(NlogK)
求前 K 小,可以用一个容量为 K 的大根堆,每次 poll 出最大的数,那堆中保留的就是前 K 小啦(注意不是小根堆!小根堆的话需要把全部的元素都入堆,那是 O(NlogN),而不是 O(NlogK)。
二叉搜索树也可以 O(NlogK)解决 TopK 问题
BST 相对于前两种方法没那么常见,但是也很简单,和大根堆的思路差不多~
要提的是,与前两种方法相比,BST 有一个好处是求得的前K大的数字是有序的。
因为有重复的数字,所以可以使用TreeMap 而不是 TreeSet(有的语言的标准库自带 TreeMultiset,也是可以的)。
TreeMap的key 是数字,value 是该数字的个数。
遍历数组中的数字,维护一个数字总个数为 K 的 TreeMap:
1. 若目前 map 中数字个数小于 K,则将 map 中当前数字对应的个数 +1;
2. 否则,判断当前数字与 map 中最大的数字的大小关系:
若当前数字大于等于 map 中的最大数字,就直接跳过该数字;
若当前数字小于 map 中的最大数字,则将 map 中当前数字对应的个数 +1,并将 map 中最大数字对应的个数减 1。
数据范围有限时使用计数排序就行了:O(N)
func getLeastNumbers(arr []int, k int) []int {
if k == 0 || len(arr) == 0 {
return []int{}
}
// 统计每个数字出现的次数
counter := make([]int, 10001) // 初始化10001个长度的数组,初始值都为0
for _, n := range arr {
counter[n]++
}
// 根据counter数组从头找出k个数作为返回结果
res := make([]int, 0)
idx := 0
for i := 0; i < len(counter); i++ {
for counter[i] > 0 && idx < k {
res = append(res, i)
idx++
counter[i]--
}
if idx == k {
break
}
}
return res
}
剑指 Offer 40. 最小的k个数
题目描述
输入整数数组 arr ,找出其中最小的 k 个数。例如,输入4、5、1、6、2、7、3、8这8个数字,则最小的4个数字是1、2、3、4。
示例 1:
输入:arr = [3,2,1], k = 2
输出:[1,2] 或者 [2,1]
示例 2:
输入:arr = [0,1,2,1], k = 1
输出:[0]
来源:力扣(LeetCode)
链接:https://leetcode-cn.com/problems/zui-xiao-de-kge-shu-lcof
算法实现
func getLeastNumbers(arr []int, k int) []int {
if len(arr) <= k {
return arr
}
quickSort(arr, 0, len(arr) - 1)
return arr[0:k]
}
func quickSort(arr []int, begin, end int) {
if begin > end {
return
}
base := arr[begin]
i, j := begin, end
for i < j {
for (i < j) && (arr[j] > base) {
j--
}
for (i < j) && (arr[i] <= base) {
i++
}
arr[i], arr[j] = arr[j], arr[i]
}
arr[i], arr[begin] = arr[begin], arr[i]
quickSort(arr, begin, i - 1)
quickSort(arr, i + 1, end)
}
剑指 Offer 21. 调整数组顺序使奇数位于偶数前面
题目描述
输入一个整数数组,实现一个函数来调整该数组中数字的顺序,使得所有奇数位于数组的前半部分,所有偶数位于数组的后半部分。
示例
输入:nums = [1,2,3,4]
输出:[1,3,2,4]
注:[3,1,2,4] 也是正确的答案之一。
算法实现
主要思路如下:
- 使用双指针分别指向数组首位元素下标;
- 当左下标小于右下标,且当前左下标对应元素为奇数,左指针就一直往后移动,直达指向偶数;
- 当左下标小于右下标,且当前右下标对应元素为偶数,右指针就一直往前移动,直到指向奇数;
- 当左指针指向偶数,右指针指向奇数时,两个元素交换位置;
- left = right时,遍历结束,返回数组即为所求结果。
是不是跟快排的思路相似,可以使用快排的代码作为模板,只需要进行一次“快排”即可,代码如下:
func exchange(nums []int) []int {
left, right := 0, len(nums) - 1
for left < right {
for (left < right) && (nums[left] % 2 == 1) { // 左边指针遇到奇数一直后移,直到遇到偶数
left++
}
for (left < right) && (nums[right] % 2 == 0) { // 右边指针遇到偶数一直前移,直到遇到奇数
right--
}
nums[left], nums[right] = nums[right], nums[left] // 直到指针指向偶数,右指针指向奇数,此时交换
}
return nums
}
原文链接:https://blog.csdn.net/qq_40941722/article/details/94396010