简介
快速排序(Quicksort)可简称”快排”,属于交换排序中的典型代表,是对同属于交换排序的冒泡排序的改进,体现了分治的思想。
原理详解
首先应该明确,既然属于交换排序,那排序过程的实质是:对给定的待排序列,不断交换调整其中的元素的位置,让每个元素处在正确的位置上,最终达到有序。在明确这点后,再看其基本思想:
基本思想:首先从待排序列中选定一个关键字作为一个枢轴,通过其他关键字与枢轴的比较,将除枢轴外的其余关键字划分成位于枢轴前后的两个子序列,位于枢轴前的子序列中的所有关键字均不大于枢轴,位于枢轴后的子序列中的关键字均不小于枢轴。此时,枢轴作为这两个子序列的分割点,它已经处在正确的位置上。再按同样的方法对这两个子序列分别进行快速排序,依次递归。最终使得原来的待排序列整体有序。
由基本思想可见,快速排序的过程中要做两件事:
(1).首先,将给定的无序序列划分成两部分;
(2).然后,对划分出的两个子序列分别进行快速排序。
对子序列进行快速排序的操作很简单,调用函数即可。所以,快速排序的核心在于划分子序列。
至此,给出一个示例代码,以方便理解前述和后续的内容,将对照代码继续解析。
代码片段1
该段代码定义了QuickSort()方法,其中调用的partition()定义在下面的代码片段2中,所干的事情就是前边提到的(2)。附有注释,简单易懂,就不多说。
/**
* 将无序序列sequence位于s和t之间的元素排序
*
* @param sequence
* 待排序列
* @param s
* 序列的起点
* @param t
* 序列的终点
*/
public void QuickSort(int[] sequence, int s, int t) {
int pivotloc = s; // 枢轴的位置
// 当t>s时,才有必要进行排序
if (t > s) {
// 将待排序列划分成两个子序列:[s, pivotloc - 1]和[pivotloc + 1, t]
pivotloc = partition(sequence, s, t);
// 分别对两个子序列进行快速排序
QuickSort(sequence, s, pivotloc - 1);
QuickSort(sequence, pivotloc + 1, t);
}
}
代码片段2
该段代码定义了partition()方法,所干的事情就是前边提到的(1),即划分成子序列,代码本身简洁精炼,为便于理解加多了几行注释使得长度增加,不必有过多的心理压力。
/**
* 将给定的序列sequence中位于low和high之间的元素划分成两个子序列,并返回枢轴所在的位置
*
* @param sequence
* 待划分的序列
* @param low
* 参与划分的元素的下界
* @param high
* 参与划分的元素的上界
* @return int 本次划分后枢轴所在的位置
*/
private int partition(int[] sequence, int low, int high) {
int piovt = sequence[low]; // 定义枢轴变量并赋初值,同时也是备份了sequence[low]的值
while (low < high) {
// 从high位置开始向左逐个扫描,直到遇到第一个比枢轴小的元素就停止
// 然后将这个元素放到low对应的位置上
while (low < high && sequence[high] >= piovt) {
high--;
}
sequence[low] = sequence[high];
// 接着
// 从low位置开始向右逐个扫描,直到遇到第一个比枢轴大的元素就停止
// 然后将这个元素放到high对应的位置上
while (low < high && sequence[low] <= piovt) {
low++;
}
sequence[high] = sequence[low];
}
// 直到low==high时,上面的while循环才会结束
// 此时,low==high,即为枢轴的位置
sequence[low] = piovt; // 将枢轴的值存放到它的位置上
return low; // 返回枢轴的位置
}
划分子序列:
现结合上面的代码片段2,说说划分子序列的具体过程:
Step1.从high位置开始向左(即位标减小的方向)逐个扫描,直到遇到一个比枢轴小的关键字停止,然后将这个值放到low位置上。
Step2.然后从low位置上开始向右(即位标增大的方向)逐个扫描,直到遇到一个比枢轴大的关键字停止,然后将这个值放到high位置上。
这两步的操作正体现出交换排序 的含义:先从右边开始找一个比较小的,然后从左边开始找个大的,然后将小的放左边,大的放右边,这就完成了“交换”,不过应当注意,这个交换不是真正的一对一交换。
然后循环执行如上两步,直到一直向左走的high和向右走的low相遇就停止。相遇则说明在相遇位置的右边没有比枢轴更大的,在左边没有比枢轴更小的,那当前的位置就是这个枢轴应该待的地方,即正确的位置,所以,枢轴就位了。
于是,以枢轴为分割点,原来的序列被分为两个子序列。只要知道分割点的位置,就能知道划分出的子序列的起止位标,故只需返回枢轴的位置即可。
算法性能
- 时间复杂度
- 平均时间复杂度——O(nlogn)
- 最坏情况下的时间复杂度——O(n*n)
- 空间复杂度
- 平均空间复杂度——O(logn)
- 最坏情况下的空间复杂度——O(n)
- 稳定性——不稳定
深入思考
至此,原理已经详解完了,但是其中有几个值得深入思考的问题,如有兴趣,可以继续一探究竟。
1.如何实现从大到小排序?
细心点就会发现,上述的示例代码实现的是从小到大排序,那要从大到小排序又该怎么写?
其实很简单,回顾下上面示例代码的原理,是选了一个关键字作为枢轴,相当于确定了一个标准,比枢轴小的丢左边,比枢轴大的丢右边,因此,将其反过来,将小的丢右边,大的丢左边就OK啦!因此,修改的部分很小,如下:
// 在partition()方法中做如下修改即可
// 将
while (low < high && sequence[high] >= piovt)
// 改为
while (low < high && sequence[high] <= piovt)
----------
// 将
while (low < high && sequence[low] <= piovt)
// 改为
while (low < high && sequence[low] >= piovt)
2.枢轴随便选一个都可以?
是的,枢轴随便选一个就可以!根据该算法的原理,枢轴只是关键字调整位置的一个标准,任意一个元素都可以作为枢轴。
但是,选枢轴的策略不同,会影响到partition方法的具体实现。例如,可以每次选序列的第三个元素作为枢轴,但是,当遇到长度小于3的序列时,就要改变下策略,如此就需要条件判断,徒增更多的麻烦。如上所示,将第一个元素或者最后一个元素选为枢轴,在编码实现起来最简单!
3.为什么不是真正的一对一交换?
常见的一对一交换是形如这样的:
int a = 25, b = 36, tmp = 0;
tmp = a;
a = b;
b = tmp;
而仔细研究下上面的代码,是先将low0的值备份到piovt,把low0的位置腾出来,然后移动high0到high1,将high1的值放到low0的位置,然后移动low0到新的位置low1,并将这个值放到high1,接着,high1继续向左移动成为high2,并将high2的值放到low1,如此类推。
代码下载
博主已将示例代码的工程打包上传,名为快速排序示例代码(JAVA版),点击此处下载。
博主水平有限,加之时间仓促,错漏难免,望不吝赐教!