12.讲排序(下):如何用快排思想在O(n)内查找第K大元素

归并排序和快速排序都用到了分治思想。问题:如何在O(n)的时间复杂度内查找一个无序数组中的第K大元素?

1. 归并排序的原理 Merge sort

如图所示:
在这里插入图片描述
分治算法一般都是用递归来实现的。分治是一种解决问题的处理思想,递归是一种编程技巧,这两者并不冲突。


归并排序的性能分析

  • 归并排序是稳定算法
  • 归并排序的时间复杂度,O(nlogn)。(master公式分析)
  • 归并排序的空间复杂度,O(n)。

2.快速排序的原理

快排的思想是这样的:如果要排序数组中下标从p到r之间的一组数据,我们选择p到r之间的任意一个数据作为pivot(分区点)。

我们遍历p到r之间的数据,将小于pivot的放到左边,将大于pivot的放到右边,将pivot放到中间。经过这一步骤之后,数组p到r之间的数据就被分成了三个部分,前面p到q-1之间都是小于pivot的,中间是pivot,后面的q+1到r之间是大于pivot的。
在这里插入图片描述

递推公式:
quick_sort(p…r) = quick_sort(p…q-1) + quick_sort(q+1, r)

终止条件:
p >= r
// 快速排序,A是数组,n表示数组的大小
quick_sort(A, n) {
  quick_sort_c(A, 0, n-1)
}
// 快速排序递归函数,p,r为下标
quick_sort_c(A, p, r) {
  if p >= r then return
  
  q = partition(A, p, r) // 获取分区点
  quick_sort_c(A, p, q-1)
  quick_sort_c(A, q+1, r)
}

分区函数:

partition(A, p, r) {
  pivot := A[r]
  i := p
  for j := p to r-1 do {
    if A[j] < pivot {
      swap A[i] with A[j]
      i := i+1
    }
  }
  swap A[i] with A[r]
  return i

在这里插入图片描述
快速排序的性能分析

  • 快速排序是不稳定的
  • 快速排序的时间复杂度,O(nlogn),最差O(n^2). 为避免出现最差情况,有一些手段合理选择pivot。
  • 快速排序的空间复杂度,O(1),原地排序。

3.解答开篇

解答开篇的问题:O(n)时间复杂度内求无序数组中的第K大元素。比如,4, 2, 5, 12, 3这样一组数据,第3大元素就是4。

我们选择数组区间A[0…n-1]的最后一个元素A[n-1]作为pivot,对数组A[0…n-1]原地分区,这样数组就分成了三部分,A[0…p-1]、A[p]、A[p+1…n-1]。

如果p+1=K,那A[p]就是要求解的元素;如果K>p+1, 说明第K大元素出现在A[p+1…n-1]区间,我们再按照上面的思路递归地在A[p+1…n-1]这个区间内查找。同理,如果K<p+1,那我们就在A[0…p-1]区间查找。
在这里插入图片描述
第一次分区查找,我们需要对大小为n的数组执行分区操作,需要遍历n个元素。第二次分区查找,我们只需要对大小为n/2的数组执行分区操作,需要遍历n/2个元素。依次类推,分区遍历元素的个数分别为、n/2、n/4、n/8、n/16.……直到区间缩小为1。

如果我们把每次分区遍历的元素个数加起来,就是:n+n/2+n/4+n/8+…+1。这是一个等比数列求和,最后的和等于2n-1。所以,上述解决思路的时间复杂度就为O(n)。

你可能会说,我有个很笨的办法,每次取数组中的最小值,将其移动到数组的最前面,然后在剩下的数组中继续找最小值,以此类推,执行K次,找到的数据不就是第K大元素了吗?

不过,时间复杂度就并不是O(n)了,而是O(K * n)。你可能会说,时间复杂度前面的系数不是可以忽略吗?O(K * n)不就等于O(n)吗?

这个可不能这么简单地划等号。当K是比较小的常量时,比如1、2,那最好时间复杂度确实是O(n);但当K等于n/2或者n时,这种最坏情况下的时间复杂度就是O(n2)了。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
可以使用速选择算法(QuickSelect Algorithm)来实现在 O(n) 时间复杂度内查找数组中第 k 大的元素速选择算法的基本思路与排序类似,都是通过分治的思想将问题规模不断缩小。但是,速选择算法只需要对单边递归进行处理,而不需要对两边都递归处理。具体步骤如下: 1. 选择数组中的一个元素作为 pivot 元素。 2. 将数组中小于 pivot 的元素放在 pivot 左边,大于 pivot 的元素放在 pivot 右边。这个过程可以使用 partition 函数实现,可以参考排序的实现。 3. 如果 pivot 的位置恰好为 k-1,则 pivot 就是第 k 大的元素,直接返回。 4. 如果 pivot 的位置小于 k-1,则第 k 大的元素在 pivot 右边,对右边的元素再进行速选择。 5. 如果 pivot 的位置大于 k-1,则第 k 大的元素在 pivot 左边,对左边的元素再进行速选择。 下面是一个基于速选择算法的实现: ```c int partition(int arr[], int left, int right) { int pivot = arr[right]; int i = left - 1; for (int j = left; j < right; j++) { if (arr[j] < pivot) { i++; int temp = arr[i]; arr[i] = arr[j]; arr[j] = temp; } } int temp = arr[i + 1]; arr[i + 1] = arr[right]; arr[right] = temp; return i + 1; } int quickSelect(int arr[], int left, int right, int k) { if (left == right) { return arr[left]; } int pivotIndex = partition(arr, left, right); if (pivotIndex == k - 1) { return arr[pivotIndex]; } else if (pivotIndex < k - 1) { return quickSelect(arr, pivotIndex + 1, right, k); } else { return quickSelect(arr, left, pivotIndex - 1, k); } } int findKthLargest(int arr[], int size, int k) { return quickSelect(arr, 0, size - 1, size - k + 1); } ``` 其中,`partition` 函数用于将数组分成左右两个部分,`quickSelect` 函数用于递归地进行速选择,`findKthLargest` 函数是对外的接口,用于调用 `quickSelect` 函数并返回结果。 需要注意的是,在 `quickSelect` 函数中,`k` 的值是倒数第 k 大的元素在数组中的位置,因此需要将 `size - k + 1` 作为 `quickSelect` 的参数传入,最终返回的是第 k 大的元素的值。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值