快速排序&半快速排序思想求n个无序数中第k或者前k个数

来源:互联网 发布:维普数据库免费账号 编辑:程序博客网 时间:2024/05/29 10:21

一、快速排序总结:  

给快速排序做个总结是看到之前上数据结构的时候给老师写的一个关于考试程序纠错的邮件。错误的程序如下:

void QuickSort(RecType R[],int s,int t){int i=s,j=t;RecType tmp;if (s<t) {tmp=R[s];     while (i!=j)  {while (j>i && R[j].key>tmp.key) //在该处若初始序列首尾值相等,将陷入死循环 j--;                      //一种修改方法是 R[j].key>=tmp.keyR[i]=R[j];                  //另一种是在R[i]=R[j]后加i++语句(后面是j--)while (i<j && R[i].key<tmp.key)       i++;           R[j]=R[i];}R[i]=tmp;QuickSort(R,s,i-1);    QuickSort(R,i+1,t);    }}
  

   之前写快速排序都是喜欢上面那样写一个函数,快排的经典思想就是分而治之。对一个数组,先选一个pivotkey(枢轴中心),一般情况下就直接选数组的第一个元素。然后对数组从后向前遍历,将小于pivotkey的元素放在数组左边;又对数组从前向后遍历,将大于pivotkey的元素放在数组的右边;如此反复之后前后遍历的伪指针相等,这时候也就是pivotkey值应在的位置:左半部分全小于pivotkey,右半部分全大于pivotkey。 接下来分而治之,采用递归在将左半部分和右半部分分别再做上述操作。时间复杂度最好的情况下(每次枢轴放在数组正中间)是O(n*logn)。 最坏的情况(数组本身已经有序顺序或者逆序)是O(n^2)。 快速排序复杂度分析

       上面的写法将数组partition和算法递归的过程写在了一起。两个过程分开写的写法如下(易于理解):

//partition 过程,返回数组枢轴值正确位置的 index
int quickpass(int low, int high, int *a){int pivotkey = a[low];while(low < high){while(low<high && a[high]>=pivotkey)high--;a[low] = a[high];while(low<high && a[low]<=pivotkey)low++;a[high] = a[low];} a[low] =  pivotkey;return low;}
//算法递归过程,分而治之void quicksort(int low, int high, int *a){int pivot; if(low < high){pivot = quickpass(low, high, a); quicksort(low, pivot-1, a); quicksort(pivot+1, high, a);}}

      上面两个写法的特点都是选定数组的第一个元素作为枢轴值。然后从后向前扫描,将大小于枢轴值得元素放到数组左边,伪指针j初始值为end,操作为j--;然后又从前向后扫描,将大于枢轴值的元素放在数组右边,伪指针i初始值为start,操作为 i++;交替两种操作,直到 i=j,就是枢轴值应该放的位置,也就是partition过程要返回的index,另外数组在c语言中实现是采用的不是值传递,而是引用传递,那么同样也是返回了枢轴值放在了正确位置的排序后数组。

    算法教材《算法设计技巧与分析》采用的写法就比较直接些:

#include <iostream>using namespace std;void swap(int &a, int &b){int temp = a;a = b;b = temp;/* 几种比较好的swap写法 a = a^b;b = a^b;a = a^b;*//*a = a+b;b = a-b;a = a-b;*/}int partition(int low, int high, int *a){int pivotkey = a[low];  int i = low;    for(int j=low+1; j<=high; j++)  {if(a[j]<=pivotkey){    i = i+1;    if(i!=j)swap(a[i], a[j]);}   }  swap(a[low], a[i]);   return i;}void quicksort(int low, int high, int *a){int pivot = 0;if(low<high)  //注意此处不能写成low!=high,有时partition部分返回的index就是pivot=low,如果if判断条件是low!=high,那么pivot-1就越界了{pivot = partition(low, high, a);quicksort(low, pivot-1, a);quicksort(pivot+1, high, a);}}int main(){int a[3];for(int i =0; i<3; i++)cin>>a[i];quicksort(0, 2, a);for(int j=0; j<3; j++)cout<<a[j];system("pause");return 0;}

      算法教材写的思路也是在partition部分设置第一个元素是枢轴值,但是在扫描整个数组的时候,不是和之前的写法那样前后交替扫描,而是同向设置两个伪指针i和j,j从第二个元素开始依次扫描整个数组,当找到一个小于等于枢轴值得元素时(也就是该元素应该放在数轴值的左边),由于i 其实总是指向数组中最后一个小于枢轴值的元素,那么i=i+1的后,如果i!=j,意思就是ij之间有元素,这些元素(包括i)必须是大于pivotkey的,交换ij所指的值,就是让大于枢轴值得在右边,小于枢轴值得在左边;但如果i=j,说明ij指的元素都是小于等于pivokey的,没有必要交换。 i+1的操作其实保证i每次都是指向数组中最后一个小于等于枢轴值的元素(这也是说快速排序是不稳定的排序算法的原因所在,对 8 4 5 6 4 7这样的序列会让两个4的相对位置发生改变),j就继续向前扫描。

     注意j扫描完全数组之后i就是指向了数组中最后一个元素值小于数轴值的位置,此时交换枢轴值和a[i],然后返回枢轴值应处于的正确位置index。


二、快速排序思想衍生的半快速排序思想

         前面的快速排序中partition过程每次利用枢轴值将数组分成左半部分全小于枢轴值,右半部分全大于枢轴值(或者相反),算法递归过程再将左半部分和右半部分分别再进行快排递归,得到左半部分和右半部分分别有序,这样整个数组就有序。

       有时候需要处理的问题不需对整个数组排序,只需要对数组做一个partition,在某index处,数组前半部分全小于index_key, 数组后半部分全大于index_key(或者相反)。比如经典面试题: 求数组中出现次数超过一半的数字(剑指offer面试题T29) Problem 1203 - 找相同   Problem 1204 - 继续找相同以及  对N个数,找出前最小的K个数(剑指offer面试题30)

      对前者,如果要将数组全部排好序之后,再计算每个数字出现的次数(该书作者的假想这么做,那么时间复杂度是O(n*logn))。 但是对要统计每个数字出现的个数的话,一般想到的应该是 计数排序,a[i]存储数组元素,b[a[i]]存储每个元素出现的次数,到时遍历b数组,看是否有大于一般的值。若b[i]值大于数组元素个数的一般,那么答案就是 i,b[a[i]] = b[i]。但是计数排序有个限制就是要求数组元素都是非负数。

       对前者,其实可以考虑到这种情况下数组特点: 数组中超过一半的那个数必然出现在数组中间的位置,那么问题就转化为求n个元素的数组中第 n/2 大的元素,也就是求n个元素中第K大的元素(经典题)

      这一类问题都可以采用半快速排序的思想,快排中每一次partition都将返回一个index,如果index>k,那么再对start到index-1这一段快排吗;如果index<k,那么再对inde+1到end这一段快排。直到partition返回的index = k (上面的题目中k=1/2)。

     对后者,其实也是同样的道理,找出前最小的k个数,那么快排 partition 部分返回的index要是等于k,那么位于index左半部分的数都是前最小的k个数,只是这k个数没有排序而已(如果题目不要顺序输出前K个数的话)。


三、半快速排序思想时间复杂度的分析

         前面两个题要是采用直观的方法:先对整个数组排序,然后再处理数组的话,显然时间复杂度是O(n*logn);

         采用半快速排序思想,时间复杂度可以降到O(n),但是也正如同快速排序的实际算法复杂度依靠初始数组一样: 如果初始数组已经是顺序/逆序数组,那么每次partition过程后递归的子树就是一个单向树,复杂度是O(n^2),所以woj1203的题目用半排序思想其实过不了。对于空间复杂度,半快速排序由于存在递归设计,无法做到O(1)的情况。 这也是对于求数组中出现次数超过一半的数字类型问题,为什么前面WOJ1203题目用半快速排序可以过(忽略时间复杂度的话),但是WOJ1204不可以。前者给的内存有65536KB,而后者之后5240KB。解决这个问题就可以采用算法教材candidate算法,后一篇博文分析。  

     另外WOJ1203寻找多数元素的这种题目也可以用C++中的std_map 或者java中的HashMap做,用空间换时间.