0 文章目录
- 寻找第k大元素的实现思路;
- 快速排序算法分析;
- partition部分的两种C++实现代码;
- 可运行的完整C++代码(不是在leetcode里的,那里也已经有各种各样的参考答案了),可自定义输入输出进行测试。
1 寻找数组中第k个最大元素
在未排序的数组中找到第 k 个最大的元素。请注意,你需要找的是数组排序后的第 k 个最大的元素,而不是第 k 个不同的元素。
输入: (分别是数组长度、k值。这里
与力扣题目要求略有不同
,请自己根据需要酌情修改代码)
6 2
[3,2,1,5,6,4]
输出:
5
实现思路分析:
很容易想到的办法是,给未排序的数组排序,如果是升序排列,假设数组长度为n,那么所求第k个最大元素的值即下标为(n-k)对应的值
。
选择一个高效的排序算法,写好对应的输入输出函数即可。一般可采用堆排序、快速排序等时间复杂度、空间复杂度都比较低的算法,本文采用快速排序。
优化:
容易想到:冒泡排序中,第k大的元素在第k趟就已经排好了,可以直接提取,后面的(n-k)趟其实是不需要的操作,那么快速排序是否也存在这种“省力”的机会呢?
快速排序的思想是:选取一个pivot枢纽元素,比它大的放在右边,比它小的放在左边, 针对左边的一部分,重复上述操作递归。如果这个pivot经过第一轮递归后的位置是正好是(n-k),那么可以直接返回pivot的值即答案。如果(n-k)在pivot下标值的右边,那么只需要递归执行右边部分寻找即可,这样算法的时间复杂度可从 n log n 进一步降低到 n
。
2 快速排序算法分析
上面思路说起来简单,然而实现起来确实另一回事了。(哭泣)如果很久不看然后突然考到真的写不出来。难就难在,每一个变量的作用、变量在循环外还是循环内,如何实现递归、正确终止递归
,以及最难的,在不开辟新数组空间的情况下,如何实现(与pivot比较后)大小数的交换。即 partition 部分算法的实现。
首先说递归部分
,有两种常见的实现方式。一种是单独写partition部分,将其返回值作为递归数组的边界;另一种是只用一个函数搞定,直接在内部传递pivot下标值递归。
大致实现思路分别如下:
第一种:
int partition(int arr[], int left, int right) //找基准数 划分
{
......
//为了方便看清递归架构,此处具体实现省略,后面3再具体讲。
......
}
void quick_sort(int arr[], int left, int right)
{
if (left > right)
return;
int j = partition(arr, left, right);
quick_sort(arr, left, j - 1);
quick_sort(arr, j + 1, right);
}
第二种:
void QuickSort(int array[], int start, int last)
{
int i = start;
int j = last;
int pivot = array[i];
if (i < j)
{
......
//此处依然省略partition部分的代码
......
//把基准数放到i位置
array[i] = pivot;
//递归方法
QuickSort(array, start, i - 1);
QuickSort(array, i + 1, last);
}
}
3 partition部分的两种C++实现代码
利用双指针
的思想,从左右两头开始向中间走,边走边和pivot的值比较,左边应该比pivot小,如果遇到比pivot大的就停下来准备交换,右边同理,遇到比pivot小的就停止移动指针。对于具体如何实现交换
,主要有两种实现思路:
第一种是:
- 先走right,右边找到小于pivot的值时,right指针停下;
(一定要先走right,因为它遇到比pivot小的值停下,最后 left 和 right 遇到的时候,可以拿二者指向的值与pivot交换,先走right可以保证它一定小于pivot。)
- 再走left,左边找到大于pivot的值时,left指针停下;
- 此时如果 left < right,交换二者;
- 循环上述三步,直到 left 和right 相遇;
- 交换当前 left(right)指针指向的位置与初始first位置的数值。
代码如下:
int partition(int *nums, int left, int right)
{
int pivot = nums[left];
int first = left;
while (left < right) {
while (left < right & nums[right] >= pivot) {
right--;
}
while (left < right & nums[left] <= pivot) {
left++;
}
if(left < right){
int tem = nums[right];
nums[right] = nums[left];
nums[left] = tem;
};
}
nums[first] = nums[left];
nums[left] = pivot;
return left;
}
第二种是:
- 选取 left 指针指向的值为pivot(首个元素);
- 先走 right,遇到比pivot小的值,停下指针,将这个值赋给 left 指针指向的元素;(此时,left 的值已经赋值给了pivot,所以直接覆盖没问题)
- 再走 left,遇到比 pivot 大的值,就停下指针。将 left 中的值赋给第2步中 right 指针指向的元素;(第2步中,刚给 left 赋的值是一定小于pivot的)
- 循环执行2、3步,直至 left 和 right 指针相遇;
- 至此,2、3两步就完成了交换一大一小两个值,然后最开始 0 位置的值存在 pivot中,将 pivot 放到排序之后的枢纽位置,即 left 和 right 指针指向的位置。
代码如下:
int partition(vector<int> &nums, int left, int right) {
int pivot = nums[left];
while (left < right) {
while (left < right & nums[right] >= pivot) {
right--;
}
nums[left] = nums[right];
while (left < right & nums[left] < pivot) {
left++;
}
nums[right] = nums[left];
}
nums[left] = pivot;
return left;
}
4 可运行的完整C++代码
最后附上一份可以直接在编译器 / IDE中执行的C++代码:
#include <iostream>
#include <stdio.h>
#include <stdlib.h>
int partition(int *nums, int left, int right)
{
int pivot = nums[left];
int first = left;
while (left < right) {
while (left < right & nums[right] >= pivot) {
right--;
}
while (left < right & nums[left] <= pivot) {
left++;
}
if(left < right){
int tem = nums[right];
nums[right] = nums[left];
nums[left] = tem;
};
}
nums[first] = nums[left];
nums[left] = pivot;
return left;
}
int theKthNumber(int *nums, int n, int k){
int left = 0;
int right = n - 1;
int target = n - k;
while (true) {
int p = partition(nums, left, right);
if (p == target) {
return nums[p];
} else if (target < p) {
right = p - 1;
} else {
left = p + 1;
}
}
}
int main(){
int len,k;
scanf("%d%d", &len, &k);
int *N = (int *)malloc(len*(sizeof(int)));
for (int i = 0; i <len ; ++i) {
scanf("%d", &N[i]);
}
printf("%d\n",theKthNumber(N, len, k));
return 0;
}