转自https://blog.csdn.net/payshent/article/details/60879120
一、快速排序的思想
快速排序是生活中比较常用的一种排序算法,它的特点就像名字一样速度快、效率高。
快速排序采用的思想是分治思想,先简单的介绍一下分治的思想。分治算法的基本思想是将一个规模为N的问题分解为K个规模较小的子问题,这些子问题相互独立且与原问题性质相同。求出子问题的解,就可以得到原问题的解。下面这张图会说明分治算法是如何进行的:将cn分成了两个cn/2,转而分成了cn/4,cn/8......我们通过这样一层一层的求解规模小的子问题,将其合并之后就能求出原问题的解。
![](https://i-blog.csdnimg.cn/blog_migrate/3804546b5f7a70f5b3d65ff1ad47aff7.jpeg)
既然快速排序用到了分治思想,那么也就是说快速排序也就和分治算法一样为了进行排序需要先对其划分的子区间进行排序。其基本思想就是:在待排序的序列中选取一个值作为一个基准值,按照这个基准值得大小将这个序列划分成两个子序列,基准值会在这两个子序列的中间,一边是比基准小的,另一边就是比基准大的。这样快速排序第一次排完,我们选取的这个基准值就会出现在它该出现的位置上。这就是快速排序的单趟算法,也就是完成了一次快速排序。然后再对这两个子序列按照同样的方法进行排序,直到只剩下一个元素或者没有元素的时候就停止,这时候所有的元素都出现在了该出现的位置上。
二、快速排序的单趟算法:
目前关于快速排序的单趟算法,我所熟知的只有这三种方法:左右指针法、挖坑法、前后指针法。
1、左右指针法:
左右指针法实现思路:在一段区间内我们有一个值key,从左边区间进行遍历,直到找到一个大于key的值就停下,然后再从右边找小于key的值,找到一个也停下来。我们将左右的值进行交换,这样左边那个大于key的值就被换到了右边,而右边那个比key小的值就被换到了左边。当左右两个指针相遇的时候就说明所有元素都与key做过了比较。然后再将左指针所在的元素赋值给key。此时按照上述方法进行递归实现[left, key]和[key+1, right]。
![](https://i-blog.csdnimg.cn/blog_migrate/6814311b7954059322c25d085cb96cb1.png)
这个图对于单趟排序做了一个简单的说明
左右指针法代码如下:
- int PartSort1(int* a,int left,int right)//左右指针法
- {
- int mid = GetMidIndex(a,left,right); //此处是对快排的优化,再后面会提到
- swap(a[mid],a[right]);
- int key = right;//利用key作为基准值的下标
- while (left < right)
- {
- //左指针向右找第一个比key大的数
- while (left < right && a[left] <= a[key])
- {
- ++left;
- }
- //右指针向左扎找第一个比key的数
- while (left < right && a[right] >= a[key])
- {
- --right;
- }
- //交换左右指针所指的值
- if (a[left] != a[right])
- {
- std::swap(a[left],a[right]);
- }
- }
- //将key值放到正确位置上
- swap(a[left],a[key]);
- return left;
- }
2、挖坑法:
挖坑法的思想是类似于左右指针法的,思路是先将最右边的值保存下来,作为key值。这时候最右边的值被取出去,最右边就相当于有了一个坑,我们从左向右进行遍历,找到一个比key大的数就把它填到这个坑里,这时候就相当于坑在左边,我们有从右向左进行遍历找比key小的数,找到后再次填到坑里。依次类推,大致的思想和上面的解法其实是很相似的。
挖坑法的代码如下:
- int PartSort2(int*a,int left,int right) //挖坑法
- {
- int mid = GetMidIndex(a,left,right);
- swap(a[mid],a[right]);
- int key = a[right];//将区间最右侧数据基准值
- int blank = right;//首次将坑设置到key处
- while (left < right)
- {
- //左指针向右找比key大的数据
- while (left < right && a[left] <= key)
- {
- ++left;
- }
- a[blank] = a[left];//用找到的数据填坑
- blank = left;//更新坑所在位置
- //右指针想左找比key小的数据
- while (left < right && a[right] >= key)
- {
- --right;
- }
- a[blank] = a[right];//用找到的数据填坑
- blank = right;//更新坑
- }
- a[blank] = key;//最后用key值填坑
- return blank;
- }
前后指针法的思路就是有两个指针,一个为cur,另一个为prev。开始的时候让cur指向left,让prev指向left的前一个位置。让cur向后找比key小的值,找到之后就让prev++,如果此时prev与cur不相等就让prev与cur进行交换。如果找不到比key小的值就一直让cur向后走,直到走到区间的最右边就停止,当cur走到边界的时候就让cur与prev进行交换。不断缩小边界,相同的方法进行遍历子区间。
前后指针法的代码:
- int PartSort3(int* a,int left,int right) //前后指针法
- {
- int mid = GetMidIndex(a,left,right);
- swap(a[mid],a[right]);
- int key = right;//key保存基准值的下标
- int cur = left;
- int prev = cur - 1;
- while (cur != right)
- {
- if (a[cur] < a[key] && a[++prev] != a[cur])
- {
- swap(a[cur],a[prev]);
- }
- ++cur;
- }
- swap(a[++prev],a[cur]);
- return prev;
- }
通过上面讲解快速排序的单趟算法我们可以知道,快速排序是将一个问题转化为求解小区间来进行解决。如果每次我们选的那个key值刚好是整个区间序列的中间的那个位置,那么它分成的来那个过子区间就会相差不大,这时候我们我们可以把快速排序看成一颗二叉树。图如下:
![](https://i-blog.csdnimg.cn/blog_migrate/72dc1f010c19f6b6111652725e35aa21.png)
我们可以看到如果选的key值的正确位置刚好在这个序列的中间,那么此时可以看成一个二叉树。这个时候快速排序的时间复杂度是O(n*lg n)。但是,如果这个key值得正确位置是在这个区间的最边上,就是说我们选择的这个key是最大值或者最小值,那么就会产生的一个子区间就是空的,这时候快速排序的时间复杂度就会达到O(n*n)。所以,我们需要的是时间复杂度小的快速排序,为此我们就要让快速排序选择的那个key值都能恰好处在这个序列的中间。于是,我们以此思想来进行优化快速排序。
优化1:三数取中法
三数取中法就是我们取三个数中间的那个数,这样我们就能在给定的一段区间中找到那个每次出现在中间的那个数。代码如下:
- int GetMidIndex(int* a,int left,int right) //三数取中法
- {
- int mid = left + ((right - left) >> 1);
- if (a[left] < a[mid])
- {
- if (a[mid] < a[right])
- {
- return mid;
- }
- else if (a[left] < a[right])
- {
- return right;
- }
- else
- {
- return left;
- }
- }
- else//a[left]>=a[mid]
- {
- if (a[mid] > a[right])
- {
- return mid;
- }
- else if (a[left] > a[right])
- {
- return right;
- }
- else
- {
- return left;
- }
- }
- }
当我们划分的子区间很小的时候(一般情况下13为判断的标准),我们使用快速排序对于这些小区间进行排序的时候,如果我们还使用快速排序的话就会得不偿失。因为快速排序对子区间的划分就像二叉树一样,越到下面递归越深,那么还不如我们把这剩下的数取出来用其他的排序,这样的话也就提高快速排序的效率。具体代码如下:
- void QuickSort(int *a,int left,int right) //小区间优化
- {
- assert(a);
- if (left < right)
- {
- if (right - left > 13)
- {
- int div = PartSort1(a,left,right);
- QuickSort(a,left,div-1);
- QuickSort(a,div+1,right);
- }
- else
- {
- InsertSort(a+left,right-left+1); //这里的InsertSort用的是直接插入排序
- }
- }
- }
在这里我给出直接插入排序的代码实现,大家可以看一下。关于直接插入排序我会在下一篇博客中会讲的
- //用仿函数同时实现升序降序功能
- template <class T>
- struct Less//升序
- {
- bool operator()(const T& l,const T& r)
- {
- return l < r;
- }
- };
- template <class T>
- struct Greater//降序
- {
- bool operator()(const T& l,const T& r)
- {
- return l > r;
- }
- };
- template <class T ,class Compare>
- void InsertSort(T* a,size_t n)
- {
- assert(a);
- for (size_t i = 1; i < n; ++i)
- {
- int end = i-1;
- T tmp = a[i];
- while(end >= 0)
- {
- if (Compare()(tmp,a[end]))//利用仿函数实现比较
- {
- a[end+1] = a[end];
- --end;
- }
- else
- {
- break;
- }
- }
- a[end+1] = tmp;
- }
- }
四、关于快速排序的非递归实现
上面的快速排序使用递归来实现的,我们知道如果递归特别深的情况下就会不断的去创建函数的栈帧,增加了函数调用的开销就会影响函数的执行效率,那么这时采用非递归的快速排序就是非常有必要的。其实说到非递归,就非常简单了,直接使用前面学过的栈来进行实现。
非递归代码如下:
- void QuickSortNoneR(int* a,int left,int right) //非递归实现快速排序
- {
- assert(a);
- stack<int> s;//创建一个栈
- s.push(right);
- s.push(left);
- while(!s.empty())
- {
- int start = s.top();//先取左边界
- s.pop();
- int end = s.top();//再取右边界
- s.pop();
- //int div = PartSort1(a,start,end);
- //int div = PartSort2(a,start,end);
- int div = PartSort3(a,start,end);
- if (start < div-1)
- {
- s.push(div - 1);
- s.push(start);
- }
- if (end > div+1)
- {
- s.push(end);
- s.push(div + 1);
- }
- }
- }