快速排序知多少?

来源:互联网 发布:百度bae连接数据库 编辑:程序博客网 时间:2024/06/07 12:38
**写这篇文章的初衷:**

  刚开始接触快速排序是在初学《数据结构》这本书时,当时就没弄懂,加之后来网上做题的时候几乎没有手写过快排的实现,而是全部用的C++ stl的sort,所以导致对快排的印象逐渐衰减,有时候心血来潮想手动实现却发现自己连快排都写不出来。于是下定决心花点时间,好好研究一番。


**这篇文章涉及的主要内容:**

  快排 =》 分治思想 =》 分治思想的应用(求中位数,求第k大数等等)


Part One
  首先来说快排。基本思想就是在数列中随意选择一个数作为基准,然后对数列进行一次划分,把小于这个数的移到该数左边,把大于这个数的移到该数右边。然后在分别对两部分进行递归处理,直到区间里没有数或者只有一个数。

  当选择第一个数为基准时,代码如下:

void quickSort(int array[],int s,int t){if (s >= t) return; int pivot = array[s];int i = s, j = t;while (i < j){while (j > i && array[j] >= pivot) j--;//先从右边开始找到第一个比基准小的数while (i < j && array[i] <= pivot) i++;//再从左边开始找到第一个比基准大的数if (i < j){swap(array[i], array[j]);//交换这两个数,这个函数就不贴了,C++里用引用传递               }}array[s] = array[i];//交换基准array[i] = pivot;quickSort(array,s,i-1);quickSort(array,i+1,t);}/*说一下我觉得这段代码里容易confused的几个points: *1.快速排序是一个递归调用的过程,那么退出递归的条件是什么? => 就是起始点下标s >= 终止点下标t,意思就是如果这个传进来 *的区间没有元素或者只有一个元素,那么肯定就不需要划分了。 *2.选择左边第一个元素为基准时,为什么要先从右边开始寻找?这个道理也同样适用于 如果选择最后一个元素为基准,那么首先应 *该从左边开始寻找。 =>  其实这个地方具体我也不知道为什么,我是这么看的,考虑最后将要跳出while(i < j)循环的情况: *即 i == j,此时他们指向的元素一定是小于基准元素的(具体情况可以仔细考虑),所以这个时候和基准交换并不会影响原先顺序。 *但是如果是基准选在左边,且从左边开始寻找的话,跳出循环的时候i,j指向的元素是大于基准元素的,那么把这个大于基准元素的 *值和左边的基准元素交换,那肯定就错了。所以这时和右边的基准元素交换,就不会改变原有的顺序。 *3.就是这里面有很多的<,<=,>,>=,我经常就弄不清到底要不要添加:( => 关于i和j的关系,肯定是i < j,如果i和j相遇就直接 *退出循环,然后是array[i] <= pivot,因为是找到一个大于pivot的值,所以是当小于等于的时候就继续循环。 */
选择最后一个数为基准的代码跟上面差不多,注意开始要从左边开始寻找就好。

还有一种情况是以中间的数为基准(因为我在求中位数的时候被这个地方给绊住了),话不多说,上代码:

void quickSort(int array[],int s,int t)  {          if(s >= t) return ;          int i = s, j = t;          int pivot = array[(i+j)/2];          while(i < j)          {              while(i < j && array[i] < pivot) i++;//找到第一个比key大的数              while(j > i && array[j] > pivot) j--;//找到第一个比key小的数              if(i <= j)              {                     int temp = array[i];                array[i] = array[j];                array[j] = temp;                  i++;                  j--;              }          }            quickSort(array,s,j);          quickSort(array,i,t);  }/*当i < j时,把比key小的与比key大的交换,直到i>j时,确定了中值,小于等于j的数组划分为左边较小的组,大于等于i的划分为右 *边较大的组,将数组分成了2部分,再分别递归比较.最后当s >= t时跳出递归。 *以中值为基准的代码有一点不同,就是每一次分区之后并不满足 左分区<基准<右分区,array[i] < pivot(没有=号),而且if里面还 *要加上i++与j--。先这么记着。 */ 
关于快排其实还可以更细究一下,即具体选取什么位置的数为基准效率最高。好像是三值取中(第一个元素、最后一个元素、中间一个元素的值居中者),这里便不细细说来了。

Part Two

  众所周知,快排是分治思想的一种体现,至于分治思想,就是分而治之,有点大事化小,小事化了的意思。分治法常见的应用有二分检索、归并分类、快速排序、中位数、以及k max等等。这里便对一些用分治法可以解决的典型例子进行一下记录。

  首先就是求无序数列的中位数。源于我在做PAT 1057这道题目的时候,要求一个无序数列的中位数。当时我就简单粗暴,排序取中间,后来有几个case超时。后来上网查了一下,才发现有更快速的做法。这里先不提这道题,先说一下求无序数列的中位数。

先上链接:http://www.cnblogs.com/shizhh/p/5746151.html

第一种方法就不说了,这里说一下第二种方法和第三种方法。

第二种方法:

中位数满足的性质就是有一半的数比你小,有一半的数比你大。那么利用快排划分的思想,每一次划分都可以划分出一个左区间,一个基准,和一个右区间。如果左区间里数的个数等于右区间里数的个数,那么显然此时的基准就是所求的中位数。如果前者小于后者,那么说明中位数在后者区间中,反之在前者区间中。那么便可以利用二分的思想来寻找这个中位数。

代码:

int getMedian(int array[], int len){int left = 0, right = len - 1;int midIndex = right >> 1;//这里假设N为奇数,那么划分出的区间的个数就是(N-1)/2int index = -1;while (index != midIndex){index = partition(array, left, right);//类似于快排里的一次划分,返回最后基准的下标(左区间个数)if (index < midIndex) left = index + 1; //中位数在右区间else if (index > midIndex) right = index - 1; //中位数在左区间else  break;}return array[index];}int partition(int array[], int left, int right){    if (left > right) return -1;    int pivot = array[right];//选取最右边的一个元素为基准     int i = left, j = right;//声明左右两个指针     while (i < j)    {        while (i < j && array[i] <= pivot) i++;//从左开始找到第一个大于基准的元素         while (j > i && array[j] >= pivot) j--;//从右开始找到第一个小于基准的元素         if (i < j)        {            int t = array[i];            array[i] = array[j];            array[j] = t;        }    }    array[right] = array[i];    array[i] = pivot;    return i;//返回基准的下标,实际上这个下标也就代表着其左区间的个数为i(0到i-1)。}
这里再说一下PAT 1057。上述求中位数有一个缺点就是 会改变原数组的结构。在PAT 1057中,可以不改变原数组的结构而计算出中位数,利用 树状数组。

贴代码:

int lowbit(int k){        return k & (-k);}void add(int pos, int value){while (pos < N){c[pos] += value;pos += lowbit(pos);}}int getSum(int pos){int sum = 0;while (pos > 0){sum += c[pos];pos -= lowbit(pos);}return sum;}//前三个函数都是树状数组的相关函数,代码很简单,但是原理不好理解:<int findMedian(int median){int low = 0, high = N - 1;int mid = 0, tmp = 0;while (low < high){mid = (low + high) / 2;tmp = getSum(mid);if (tmp < median)low = mid+1;elsehigh = mid;}return low;}说一下这里求中位数的思路:树状数组c存储的是下标i的出现次数,即每接收一个数x便让其次数加一add(x,1),而getSum(x)求的则是c[1]+..+c[x],即统计了小于等于数x出现的次数。如果这个次数小于(n-1)/2+1,那么说明中位数在右边区间,low=mid+1,如果这个次数大于等于(n-1)/2+1,说明中位数有可能是mid,也有可能再其左边,所以high = mid;最后返回low就是想要查询的中位数。这个方法不需要修改原有的数组,但这也是结合具体题目的,我觉得正常情况这种方法并不算多么好 = =
还有一种方法是可以suprised 面试官的一种 堆排序,具体可以见上述网页,因为我并不懂堆排序(暂时也懒得看),所以就不写了。

分治法思想其余的应用待更新中。。

0 0
原创粉丝点击