题目描述

给定一个整数数组a[0,...,n-1],求数组中第k小数

输入描述

首先输入数组长度n和k,其中1<=n<=5000, 1<=k<=n

然后输出n个整形元素,每个数的范围[1, 5000]

输出描述

该数组中第k小数

样例输入

4 2
1 2 3 4

样例输出

2

其实可以用 堆 来做,保证根节点为最小值,然后逐步剔除。不过当然也可以直接排序。
权当熟悉一下STL:
 #include <vector>
#include <algorithm>
#include <iostream>
using namespace std; int main()
{
int n, k;
cin >> n >> k; vector<int> a(n, );
for (int i = ; i < n; i++)
{
cin >> a[i];
}
sort(a.begin(), a.end()); cout << a[k-]; return ;
}

在和 Space_Double7 讨论后(详见讨论区),于是有了想要比较 快排 和 堆排 对于这道题各自的效率的想法。
于是写了一个程序来比较它们各自的运行时间,程序主要包括 随机生成输入数据、分别用快排和堆排求解并计时、比较 这几个部分。
代码如下:
 #include <vector>
#include <algorithm>
#include <iostream>
#include <stdlib.h>
#include <time.h>
#include <windows.h> using namespace std; // for timing
clock_t start, stop;
double durationOfQsort, durationOfHeapsort; vector<int> num;
int k, ans, n;
bool found; // 快排: 选定轴点
int parti(int lo, int hi)
{
swap(num[lo], num[lo + rand() % (hi - lo + )]);
int pivot = num[lo];
while (lo < hi)
{
while ((lo < hi) && (pivot <= num[hi])) hi--;
num[lo] = num[hi];
while ((lo < hi) && (num[lo] <= pivot)) lo++;
num[hi] = num[lo];
}
num[lo] = pivot;
if (lo == k)
{
found = true; // 表征已确定找到第 k 小数
ans = num[k];
}
return lo;
} // 快排主体
void quicksort(int lo, int hi)
{
if ((hi - lo < ) || (found))
{
if ((!found) && (lo == k))
{
found = true;
ans = num[k];
}
return;
}
int mi = parti(lo, hi - );
quicksort(lo, mi);
quicksort(mi + , hi);
} #define InHeap(n, i) (( (-1) < (i) ) && ( (i) < (n) ))
#define Parent(i) ((i-1)>>1)
#define LastInternal(n) Parent(n-1)
#define LChild(i) (1+((i) << 1))
#define RChild(i) ((1+(i)) << 1)
#define LChildValid(n, i) InHeap(n, LChild(i))
#define RChildValid(n, i) InHeap(n, RChild(i))
#define Bigger(PQ, i, j) ((PQ[i])<(PQ[j])? j : i)
#define ProperParent(PQ, n, i) \
(RChildValid(n, i) ? Bigger(PQ, Bigger(PQ, i, LChild(i)), RChild(i)):\
(LChildValid(n, i) ? Bigger(PQ, i, LChild(i)):i\
)\
) // 对向量前 n 个元素中的第 i 实施下滤操作
int percolateDown(int n, int i)
{
int j;
while (i != (j = ProperParent(num, n, i)))
{
swap(num[i], num[j]);
i = j;
}
return i;
} // Floyd 建堆算法
void heapify()
{
for (int i = LastInternal(n); InHeap(n, i); i--)
percolateDown(n, i);
} // 删除堆中最大的元素
int delMax(int hi)
{
int maxElem = num[];
num[] = num[hi];
percolateDown(hi, );
return maxElem;
} // 堆排主体
void heapsort()
{
heapify();
int hi = n;
while (hi > )
{
--hi;
num[hi] = delMax(hi);
if (hi == k)
{
ans = num[k];
return;
}
}
} int main()
{
int scoreOfQsort = , scoreOfHeapsort = ; for (int iter = ; iter < ; iter++)
{
// 确定 n 的大致最大范围,注意随机 n 会有向右 MaxN 的偏差
const int MaxN = ; // 产生一个 0..n-1 的随机序列输入数组,n 最大为3000
cout << "**********************第" << iter + << "次************************" << endl;
//srand(unsigned(clock()));
n = rand() % MaxN + MaxN;
vector<int> a(n, );
for (int i = ; i < n; i++)
a[i] = i;
random_shuffle(a.begin(), a.end()); cout << "产生一个 0.." << n - << " 的随机序列输入数组:" << endl;
/*for (int i = 0; i < n; i++)
cout << a[i] << " ";*/
cout << endl; // 随机生成 k
//srand(unsigned(clock()));
k = rand() % n;
cout << "k = " << k << endl << endl; // 第 k 小的数一定是 k,因为 random_shuffle,同时避免退化情形对快排一般性的影响
cout << "在该数组中第 " << k << " 小的数是: " << k << endl << endl << endl; // qsort
cout << "快排:" << endl;
num = a;
start = clock();
found = false;
quicksort(, n);
stop = clock();
durationOfQsort = ((double)(stop - start)*) / CLK_TCK;
cout << "Ok.. 我已经找到了第 " << k << " 小的数,它是: " << ans << endl;
cout << "快排用时: " << durationOfQsort << "ms" << endl;
if (ans != k)
{
cout << "注意!!!答案错误!!!" << endl;
system("pause");
}
/*else
{
cout << "此时的序列情况是: ";
for (int i = 0; i < n; i++)
cout << num[i] << " ";
}*/
cout << endl << endl; // heapsort
cout << "堆排:" << endl;
num = a;
start = clock();
heapsort();
stop = clock();
durationOfHeapsort = ((double)(stop - start) * ) / CLK_TCK;
cout << "Ok.. 我已经找到了第 " << k << " 小的数,它是: " << num[k] << endl;
cout << "堆排用时: " << durationOfHeapsort << "ms" << endl;
if (num[k] != k)
{
cout << "注意!!!答案错误!!!" << endl;
system("pause");
}
/*else
{
cout << "此时的序列情况是: ";
for (int i = 0; i < n; i++)
cout << num[i] << " ";
}*/
cout << endl << endl; if (durationOfHeapsort > durationOfQsort) scoreOfQsort++;
else scoreOfHeapsort++;
}
cout << "*******************END***********************";
cout << endl << endl << "总比分(快排:堆排): " << scoreOfQsort << " : " << scoreOfHeapsort;
cout << endl; return ;
}

运行了几下,大致可以看出 快排:堆排 的效率比 趋近于 2:1(对于每个case的耗时少则计分,计分多的效率高),这是其中 3 次的结果:

感兴趣的可以自己在本地运行一下~


然而我发现依然存在一个问题,我们主要想讨论的是 对于 “一找到 第k小 的元素便立即退出”这件事 对于整个完全排序本身优化程度的大小。

横向来看,仅仅比较 优化后的排序 耗时并不能确定 到底是快速排序 本身对于随机数据而言比 堆排序 性能好(从上面看,似乎是显然的),还是 “一找到 第k小 的元素便立即退出”这个优化在这里 帮了快速排序的大忙,使其在这里表现出了更好的性能。

纵向来看,我们想看看该优化对于完全排序而言,能优化到什么程度。

所以我决定比较两种排序 优化退出的时间占完全排序的时间 的比,来看看这个优化对于完全排序的影响程度。

这是修改以后的代码,算的是百分比,百分比越小,优化越明显


 #include <vector>
#include <algorithm>
#include <iostream>
#include <stdlib.h>
#include <time.h>
#include <windows.h> using namespace std; // for timing
clock_t start, stop, stop1;
double durationOfQsort, durationOfHeapsort; vector<int> num;
int k, ans, n;
bool found;
double time1, time2;
double average1 = , average2 = ; // 快排: 选定轴点
int parti(int lo, int hi)
{
swap(num[lo], num[lo + rand() % (hi - lo + )]);
int pivot = num[lo];
while (lo < hi)
{
while ((lo < hi) && (pivot <= num[hi])) hi--;
num[lo] = num[hi];
while ((lo < hi) && (num[lo] <= pivot)) lo++;
num[hi] = num[lo];
}
num[lo] = pivot;
if ((!found)&&(lo == k))
{
stop1 = clock();
found = true;
}
return lo;
} // 快排主体
void quicksort(int lo, int hi)
{
if (hi - lo < )
{
if ((!found) && (lo == k))
{
stop1 = clock();
found = true;
}
return;
}
int mi = parti(lo, hi - );
quicksort(lo, mi);
quicksort(mi + , hi);
} #define InHeap(n, i) (( (-1) < (i) ) && ( (i) < (n) ))
#define Parent(i) ((i-1)>>1)
#define LastInternal(n) Parent(n-1)
#define LChild(i) (1+((i) << 1))
#define RChild(i) ((1+(i)) << 1)
#define LChildValid(n, i) InHeap(n, LChild(i))
#define RChildValid(n, i) InHeap(n, RChild(i))
#define Bigger(PQ, i, j) ((PQ[i])<(PQ[j])? j : i)
#define ProperParent(PQ, n, i) \
(RChildValid(n, i) ? Bigger(PQ, Bigger(PQ, i, LChild(i)), RChild(i)):\
(LChildValid(n, i) ? Bigger(PQ, i, LChild(i)):i\
)\
) // 对向量前 n 个元素中的第 i 实施下滤操作
int percolateDown(int n, int i)
{
int j;
while (i != (j = ProperParent(num, n, i)))
{
swap(num[i], num[j]);
i = j;
}
return i;
} // Floyd 建堆算法
void heapify()
{
for (int i = LastInternal(n); InHeap(n, i); i--)
percolateDown(n, i);
} // 删除堆中最大的元素
int delMax(int hi)
{
int maxElem = num[];
num[] = num[hi];
percolateDown(hi, );
return maxElem;
} // 堆排主体
void heapsort()
{
heapify();
int hi = n;
while (hi > )
{
--hi;
num[hi] = delMax(hi);
if (hi == k) stop1 = clock();
}
} int main()
{
// 确定 n 的大致最大范围,注意随机 n 会有向右 MaxN 的偏差
const int MaxN = ; // 计算次数
const int times = ; int scoreOfQsort = , scoreOfHeapsort = ; for (int iter = ; iter < times; iter++)
{ // 产生一个 0..n-1 的随机序列输入数组,n 最大为3000
cout << "**********************第" << iter + << "次************************" << endl;
//srand(unsigned(clock()));
n = rand() % MaxN + MaxN;
vector<int> a(n, );
for (int i = ; i < n; i++)
a[i] = i;
random_shuffle(a.begin(), a.end()); cout << "产生一个 0.." << n - << " 的随机序列输入数组:" << endl;
/*for (int i = 0; i < n; i++)
cout << a[i] << " ";*/
cout << endl; // 随机生成 k
//srand(unsigned(clock()));
k = rand() % n;
cout << "k = " << k << endl << endl; // 第 k 小的数一定是 k,因为 random_shuffle,同时避免退化情形对快排一般性的影响
cout << "在该数组中第 " << k << " 小的数是: " << k << endl << endl << endl; // qsort
cout << "快排:" << endl;
num = a;
start = clock();
found = false;
quicksort(, n);
stop = clock();
time1 = (double)(stop1 - start) * / CLK_TCK;
time2 = (double)(stop - start) * / CLK_TCK;
cout << "找到 k 的时间: " << time1 << " ms" << endl;
cout << "完全排序 的时间: " << time2 << " ms" << endl;
durationOfQsort = time1 / time2 * ;
average1 += durationOfQsort;
/*cout << "Ok.. 我已经找到了第 " << k << " 小的数,它是: " << ans << endl;*/
cout << "快排占比: " << durationOfQsort << " %" << endl;
/*if (ans != k)
{
cout << "注意!!!答案错误!!!" << endl;
system("pause");
}*/
/*else
{
cout << "此时的序列情况是: ";
for (int i = 0; i < n; i++)
cout << num[i] << " ";
}*/
cout << endl << endl; // heapsort
cout << "堆排:" << endl;
num = a;
start = clock();
heapsort();
stop = clock();
time1 = (double)(stop1 - start) * / CLK_TCK;
time2 = (double)(stop - start) * / CLK_TCK;
cout << "找到 k 的时间: " << time1 << " ms" << endl;
cout << "完全排序 的时间: " << time2 << " ms" << endl;
durationOfHeapsort = time1 / time2 * ;
average2 += durationOfHeapsort;
//cout << "Ok.. 我已经找到了第 " << k << " 小的数,它是: " << num[k] << endl;
cout << "堆排占比: " << durationOfHeapsort << " %" << endl;
/*if (num[k] != k)
{
cout << "注意!!!答案错误!!!" << endl;
system("pause");
}*/
/*else
{
cout << "此时的序列情况是: ";
for (int i = 0; i < n; i++)
cout << num[i] << " ";
}*/
cout << endl << endl; if (durationOfHeapsort > durationOfQsort) scoreOfQsort++;
else scoreOfHeapsort++;
}
cout << "*******************END***********************";
cout << endl << endl << "总比分(快排:堆排): " << scoreOfQsort << " : " << scoreOfHeapsort;
cout << endl;
cout << endl << "快排平均占比: " << average1 / times << " %" << endl;
cout << endl << "堆排平均占比: " << average2 / times << " %" << endl; return ;
}


运行 3 次的结果:

可见,

1、这个优化对于两种排序的影响程度是差不多的(百分比越小,优化越明显);

2、对于完全排序而言,它大概相当于在前面加了一个 0.6 的系数,也就是 只 干了完全排序 0.6 倍的工作量,正如分析来看,依然是常系数级的优化。

由于数据是完全随机的(并且没有重复元素),快排也适应得很好,在实际用途中(对于近似随机数据),它的效率是可观的。


不!还没完!

来来来,现在我们回到原问题本身,回到 查找 数组 第 k 小数 这样经典而基础的问题本身上来……

尽管 原问题 数据规模小,水水就能过,但是既然已经鼓捣过了,干脆鼓捣完。

所以我还是决定写一个对于大规模数据具有普适意义的尽可能优化的算法来解决问题(优化到线性复杂度)。

再考虑这个问题,在写了一遍快排之后,会发现这是一个与 快排中选取轴点 很类似的问题。

轴点是左右集合的分界点,左集合所有元素比轴点小,右集合所有元素比轴点大,你可以发现,找第 K 小数就是找在位置 K 上的轴点(也正如上述优化所想)!

然而我们依然要向上面所写的程序一样,找到 k 就退出吗?

1、快速选取算法

考虑这样的情形,

当前选取的轴点正好是 第k个,自然就退出;

当选取的轴点比 k 小时,我们实际上可以不用再对左集合排序了!因为我们只需要知道它们都比轴点要小,而且知道它们的个数,而此时轴点比 k 还要小,所以我们可以继续只对 右集合 分治下去!

同样的,当选取的轴点比 k 大时,我们实际上可以不用再对右集合排序了!因为我们只需要知道它们都比轴点要大,而且知道它们的个数,而此时轴点比 k 还要大,所以我们可以继续只对 左集合 分治下去!

这样,我们可以把每一次的向下两次递归变成一次。

用 非递归 版本代码大致表示如下:

 void quickSelect()
{
for (int lo = , hi = n-; lo < hi; )
{
int i = lo, j = hi, pivot = num[lo];
while (i < j)
{
while ( (i<j) && (pivot <= num[j]) ) j--;
num[i] = num[j];
while ( (i<j) && (num[i] <= pivot) ) i++;
num[j] = num[i];
}
num[i] = pivot;
if ( k<=i ) hi = i - ;
if ( i<=k ) lo = i + ;
}
} // 结束后 num[k] 即是解

对于一般情况(数据接近随机),跟快排一样,此算法效率很高,不过快排的缺点它也一样具有。也就是当轴点把左右集合划分得极不均匀甚至某一个集合为空时,此时效率跟快排一样退化到 O(n^2)。

接下来考虑, 堆排是不是也有优化空间呢?

2、堆选取算法

题目只需要我们选取 第 K 小数,跟快速选取一样,无关目的的部分我们完全没必要做无用功。

基于这样的考虑,我们完全可以只维护一个规模为 K 的大根堆嘛!~

算法思考大致是:

首先将序列前 K 的元素用 Floyd 建堆(O(k)效率)维护成一个大根堆。

然后将剩下的元素依次插入堆,每插入一个,随机删除堆顶,使堆的规模保持在 k。

这样当所有的元素插入完毕,那么堆的根就是问题的解。

一般情况下,它的效率是比完全排序效率要高的,不过当 k 接近于 n/2 的时候,它的复杂度又会退化到 O( nlogn )。

难道真的不能从实质上将这个问题优化到线性效率上来吗?

3、k-选取算法

算法里面对于排序有一个丧病的思路:选定一个 k,当序列长度小于 k 时,sort 函数直接不作处理返回原序列。整个序列经过这样一次 sort 之后当然不是有序的,此时对这个序列做一次插入排序(因为插入排序在处理 “几乎” 有序的序列时,运行非常快)。根据算导的结论,这样的复杂度是 O(nk + n log(n/k)) 的。(其实就是相当于做n/k次k长的插入)

这种思想在这里我们也可以借鉴,大致的算法思想如下:

0) 选定一个数Q,Q为一个不大的常数;

select(A, k):

1) 如果序列A规模不大于 Q 时直接蛮力算法;      // 递归基

2) 将A均匀划分为 n/Q 个子序列,各含 Q 个元素;

3) 各子序列分别排序(可采用任何排序算法),计算中位数,并将所有中位数组成一个序列;

4) 递归调用select(),计算出中位数序列的中位数,记作M;

5) 根据元素相对于 M 的大小,将 A 中元素分为三个子集:L(小于),E(相等)和G(大于);

6) if ( |L| >= k ) return select(L, k);

  else if ( |L| + |E| >= k ) return M;

else return select(G, k - |L| - |E|);

复杂度分析:(计最坏情况下运行时间为 T(n))

2): O(n);

3): 由于Q为常数,累计也为 O(n);

4): 递归调用,T(n/Q)

5): O(n);

6): 中位数序列的中位数一定是全局中位数 M,而 L 和 G 的规模一定不会超过 3n/4。

所以可得如下递推关系:

T(n) = cn + T(n/Q) + T(3n/4),c 为常数

如果取 Q = 5, 则有:

T(n) = cn + T(n/5) + T(3n/4) = O(20cn) = O(n)

可见,复杂度是线性。虽如此,其常系数过大,且算法过程较复杂,在一般规模的应用中难以真正体现出效率的优势。

Reference : 《数据结构习题解析》,邓俊辉

												

数组第K小数问题 及其对于 快排和堆排 的相关优化比较的更多相关文章

  1. 排序算法C语言实现——冒泡、快排、堆排对比

    对冒泡.快排.堆排这3个算法做了验证,结果分析如下: 一.结果分析 时间消耗:快排 < 堆排 < 冒泡. 空间消耗:冒泡O(1) = 堆排O(1) < 快排O(logn)~O(n) ...

  2. Java常见的几种排序算法-插入、选择、冒泡、快排、堆排等

    本文就是介绍一些常见的排序算法.排序是一个非常常见的应用场景,很多时候,我们需要根据自己需要排序的数据类型,来自定义排序算法,但是,在这里,我们只介绍这些基础排序算法,包括:插入排序.选择排序.冒泡排 ...

  3. 【原创】海量数据处理问题(一) ---- 外排,堆排,K查找的应用

    这篇博客源自对一个内存无法处理的词频统计问题的思考,最后给出的解决办法是自己想的,可以肯定这不是最好的解法.但是通过和同学的讨论,仍然感觉这是一个有意义及有意思的问题,所以和大家分享与探讨. 如果有误 ...

  4. 排序算法的实现(归并,快排,堆排,希尔排序 O(N*log(N)))

    今天跟着左老师的视频,理解了四种复杂度为 O(N*log(N))的排序算法,以前也理解过过程,今天根据实际的代码,感觉基本的算法还是很简单的,只是自己写的时候可能一些边界条件,循环控制条件把握不好. ...

  5. 2020-03-25:快排、堆排和归并都是O(nlog n)的算法,为何JDK选择快速排序?

    福哥答案2020-03-26: 口诀如下:冒选插希快 堆归计桶基(冒泡,选择,插入,希尔,快速,堆,归并,计数,桶,基数)冒线 平平 稳常小选平 平平 不常小插线 平平 稳常序希线 四组 不常组快四 ...

  6. python 内置速度最快算法(堆排)

    import random import time from heapq import heappush, heappop def heapsort(iterable): h = [] for val ...

  7. python 快排,堆排,归并

    #归并排序 def mergeSort(a,L,R) :     if(L>=R) :         return     mid=((L+R)>>1)     mergeSort ...

  8. 【递归打卡2】求两个有序数组的第K小数

    [题目] 给定两个有序数组arr1和arr2,已知两个数组的长度分别为 m1 和 m2,求两个数组中的第 K 小数.要求时间复杂度O(log(m1 + m2)). [举例] 例如 arr1 = [1, ...

  9. 两个有序数组的上中位数和第K小数问题

    哈,再介绍个操蛋的问题.当然,网上有很多解答,但是能让你完全看懂的不多,即便它的结果是正确的,可是解释上也是有问题的. 所以,为了以示正听,我也做了分析和demo,只要你愿意学习,你就一定能学会,并且 ...

随机推荐

  1. Internship-ZOJ2532(网络流求割边)

    Internship Time Limit: 5 Seconds      Memory Limit: 32768 KB CIA headquarter collects data from acro ...

  2. Android属性动画完全解析(下)

    转载:http://blog.csdn.net/guolin_blog/article/details/44171115 大家好,欢迎继续回到Android属性动画完全解析.在上一篇文章当中我们学习了 ...

  3. easyui 动态列

    $.post('${createLink(action:"build Columns url ")}', params, function(data){ var columns = ...

  4. mysql 查询日志

    1. 登录mysql mysql -u root -p; 2. 查看日志启用情况以及日志所在位置 show variables like 'log_%'; 结果示例如下 3. 找到对应的日志文件,保存 ...

  5. SQL2008游标

    最近让写一个自动生成数据的存储过程,其中会遍历表中数据并做出相应处理,因为数据量不算太大所以使用到了游标,初识游标遇到几个小问题,所以来和大家一起分享一下: 使用游标的五个步骤: 1.声明游标 语法: ...

  6. C语言面试题(二)

    上篇对嵌入式中C语言基本数据类型,关键字和常用操作进行了汇总,这篇我们将侧重字符串操作.请看下面的字符串处理函数:    a.库函数    1)将字符串src拷贝到字符数组dest内        c ...

  7. iOS - OC NSStream 文件流

    前言 @interface NSStream : NSObject @interface NSOutputStream : NSStream 1.文件流的使用 NSString *filePath = ...

  8. git github 异常

    git :版本控制工具 github:项目托管 git clone failed:git是否安装正确 github commit failed:github 是否账号 / 密码是否正确(密码错误也可以 ...

  9. Web1.0、Web2.0、Web3.0的主要区别

    Web1.0:以静态.单向阅读为主,网站内信息可以直接和其他网站信息进行交互,能通过第三方信息平台同时对多家网站信息进行整合使用. Web2.0:以分享为特征的实时网络,用户在互联网上拥有自己的数据, ...

  10. android mvvm

    android studio 需要gradle 1.5.0以上才支持 dependencies { classpath 'com.android.tools.build:gradle:1.5.0'} ...