大一学生第一篇博客,记录下自己看网课的心得,文中有错误或是不足的地方还请指出。
快速排序(英语:Quicksort),又称划分交换排序,简称快排,一种排序算法,最早由东尼·霍尔提出。在平均状况下,排序 n个项目要O(nlog n)(大O符号)次比较。在最坏状况下则需要 O(n2)次比较,但这种状况并不常见。事实上,快速排序 O(nlog n)通常明显比其他算法更快,因为它的内部循环(inner loop)可以在大部分的架构上很有效率地达成。
快速排序采用了一种分而治之的思想, 将数组分为三部分:pivot,比pivot小的数和比pivot大的数,其中pivot是从数组A中选的一个主元。算法的伪代码如下:
void quick_sort(int *a, int n)
{
if ( N < 2 )
return;
pivot = 从a中选一个主元;
将 S = { a \ pivot } 分成2个独立子集:
a1 = { b | b <= pivot } 和
a2 = { b | b >= pivot };
a = quick_sort(a1,n1)
{pivot}
quick_sort(a2,n2);
}
可以看到,整个算法的排序步骤就是划分子集后进行递归排序。
· 如果令 pivot = a[0]
在传统的快速排序中,往往选择头元素作为pivot,但是这样在数组基本有序的情况会出现很多次没有用处的比较。下面是一个已经有序的数组:我们可以看到的是
算法首先将1和后面的元素分成两部分:
1 2 3 4 5 6 ··· N-1 N
&再将2和后面的元素分成两部分:
2 3 4 5 6 ··· N-1 N
再将3和后面的元素分成两部分:
3 4 5 6 ··· N-1 N
······
上述的过程用时间复杂度表示如下:
T ( N ) = O( N ) + T ( N–1 )
= O( N ) + O ( N–1 ) + T( N–2 )
= O( N ) + O ( N–1 ) + ··· + O( 1 )
= O( N2 )
· 若使用rand()函数随机取pivot
采用此种方法会直接调库,相比于在数组中选择一个数字,调库更容易返回一个大于或小于数组中所有元素的值。这样的话,时间开销太大,十分浪费内存,显然不是最优解。
· 分别取头尾和中间的数,取其中位数作为pivot的值
例如8、12、3的中位数就是8,就使用8作为pivot,下面我们来看看代码
int median3(int* a, int left, int right)
{
int center = left + (right - left) / 2; // Use this form to prevent overflow
if (a[left] > a[center])
swap(a[left], a[center]);
if (a[left] > a[right])
swap(a[left], a[right]);
if (a[center] > a[right])
swap(a[center], a[right]);
// In this time, a[left] <= a[center] <= a[right]
// Hide the pivot on the position of right-1
swap(a[center], a[right - 1]);
return a[right - 1];
}
在第3行代码中,采用了防止溢出的形式;
第4~7行代码,将最小值交换到了 a[left];
第8~9行代码,确定了剩下两个元素的次序。
这时我们将 a[center] 与 a[right - 1] 交换了位置,之所以这样交换,是因为我们已经确定了a[center] <= a[right],所以将其交换到 a[right-1] 时其相对次序不变,这样暂时将pivot交换出去有利于后面比较部分的编程。
首先来看代码
// Core recursion function
void q_sort(int* a, int left, int right)
{
int pivot = 0;
int cutoff = 3;
int low = 0;
int high = 0;
// If the sequence elements are suffciently large, use quick sort
if (cutoff <= right - left)
{
pivot = median3(a, left, right);
low = left;
high = right - 1;
while (1)
{
// Move the sequence smaller than the reference to the left
// and the big to the right
while (a[++low] < pivot);
while (a[--high] > pivot);
if (low < high)
swap(a[low], a[high]);
else
break;
}
// Change the pivot to the correct position
swap(a[low], a[right - 1]);
q_sort(a, left, low - 1);
q_sort(a, low + 1, right);
}
// If there's too few elements, use simple sort
else
bubble_sort(a + left, right - left + 1);
}
void swap(int& a, int& b)
{
int tmp = a;
a = b;
b = tmp;
}
下面用一个实例来讲解下子集划分的原理和代码。
[80 10 40 90 0 30 50 20 70 60] (60为median3交换到right-1位置的pivot)
我们分别令 low = 0,high = 9 作为这个序列的“指针”,当然它并不是一个真正的指针,只是起到了指针的作用。接下来我们要做的事就是将剩下的元素分为小于pivot的部分和大于pivot的部分。
当 a[low] 小于pivot的时候,low持续向右移动,直到 a[low] <= pivot。
当 a[high] 大于pivot的时候,high持续向左移动,直到 a[high] >= pivot。
由于刚才midian3函数进行了次序的交换,有效地防止了low和high“指针”的越界。
以下为一趟排序的过程:
当 low == 0 时,a[low] <= pivot,low停止移动。
当 high == 7 时,a[high] >= pivot,high停止移动。
于是,a[0]和a[7]交换位置:
[20 10 40 90 0 30 50 80 70 60]
接下来low继续移动到3,high继续移动到6,a[3]和a[6]交换位置:
[20 10 40 50 0 30 90 80 70 60]
low继续向右移动到6,high继续向左移动到5,此时,a[6]之前的数是小于pivot的,a[6]之后的数是大于等于pivot的,所以我们就能够确定pivot的正确位置就是a[6],所以我们将a[6]与a[9]交换:
[20 10 40 50 0 30 60 80 70 90]
这时第一趟排序就已经完成。但同时有一个新的问题:如果有元素正好等于pivot怎么办?
· 停下来交换
假设有一个元素全都相等的数组,就会进行很多次没有用处的交换,虽然看起来有点浪费,但是这样处理的话pivot会被换到接近中间的位置,使整个数组恰好被分成两部分。这种情况下按照时间复杂度来计算的话,最终的时间复杂度为O(NlogN),并不是最坏情况。
· 不理它,继续移动指针
还是那个元素全都相等的数组,如果我们不理它,继续移动指针,那么pivot就会被放在数组的末尾。这种情况下数组就被分成了最后一个元素和一个大小为n-1的数组,这样的时间复杂度就为O(N2)。于是我们经过一个时间复杂度为(N2)的排序算法却什么都没有做,这是很不划算的。
综上所述,我们采用停下来交换的方法。
第一趟排序结束后,我们递归调用算法,分别对 [20 10 40 50 0 30] 和 [80 70 90] 这两个子数组进行快速排序。
快速排序时,如果用递归的话,对于时间和空间的占用都是非常严重的,同时对小规模的数据 (例如 n <= 100的) 时候可能效率还不如简单的排序。我们的解决方案就是当递归的数据规模充分小,则停止递归,直接调用简单排序 (如冒泡排序) 。在程序中定义一个cutoff的阈值,如果数据规模 n < cutoff,就采用冒泡排序,我自己代码中为了观察整个算法的执行过程所以将cutoff设置成了很小的值。
划重点!!!划重点!!!划重点!!!重要的事说11遍
cutoff初始化必须大于1
cutoff初始化必须大于1
cutoff初始化必须大于1
(以下的简单推导可选择性查看)
cutoff == 0 的时候由于下标访问会导致数组越界。
cutoff == 1 的时候median3函数会对其进行排序,假设数组为 [0 1] 或者 [1 0],经过median3后数组会变成 [0 1],low和high最终会移动到0上,while循环后的swap会将排好序的两个数字再次交换导致最后结果错误。
调用函数时参数太多显然不合适,所以必须要对算法的接口进行统一。封装函数代码仅一行:
void quick_sort(int* a, const int& n){
q_sort(a, 0, n - 1);
}
#include
using namespace std;
void bubble_sort(int* a, const int& n);
// Uniform interface
void quick_sort(int* a, const int& n);
// Core recursion function
void q_sort(int* a, int left, int right);
int median3(int* a, int left, int right);
void swap(int& a, int& b);
int main(){
int a[10] = { 9, 8, 7, 6, 5, 4, 3, 2, 1, 0 };
for (int i = 0; i < 10; i++){
cout << a[i] << " ";
}
quick_sort(a, 10);
for (int i = 0; i < 10; i++){
cout << a[i] << " ";
}
return 0;
}
// Time complexity best case: n, worst case: n^2
void bubble_sort(int* a, const int& n){
int flag = 0;
int tmp = 0;
for (int p = n - 1; p; p--){
flag = 0;
for (int i = 0; i < p; i++){
if (a[i] > a[i + 1]){
tmp = a[i];
a[i] = a[i + 1];
a[i + 1] = tmp;
flag = 1; // Identification has exchanged
}
if (flag == 0){
break;
}
} // of for i
} // of for p
} // of function
// Uniform interface
void quick_sort(int* a, const int& n){
q_sort(a, 0, n - 1);
}
// Core recursion function
void q_sort(int* a, int left, int right){
int pivot = 0;
int cutoff = 2;
int low = 0;
int high = 0;
// If the sequence elements are suffciently large, use quick sort
if (cutoff <= right - left) {
pivot = median3(a, left, right);
low = left;
high = right - 1;
while (1) {
// Move the sequence smaller than the reference to the left
// and the big to the right
while (a[++low] < pivot);
while (a[--high] > pivot);
if (low < high){
swap(a[low], a[high]);
}else{
break;
}
}
// Change the pivot to the correct position
swap(a[low], a[right - 1]);
q_sort(a, left, low - 1);
q_sort(a, low + 1, right);
}
// If there's too few elements, use simple sort
else{
bubble_sort(a + left, right - left + 1);
}
}
int median3(int* a, int left, int right){
int center = left + (right - left) / 2; // Use this form to prevent overflow
if (a[left] > a[center]){
swap(a[left], a[center]);
}
if (a[left] > a[right]){
swap(a[left], a[right]);
}
if (a[center] > a[right]){
swap(a[center], a[right]);
}
// In this time, a[left] <= a[center] <= a[right]
// Hide the pivot on the position of right-1
swap(a[center], a[right - 1]);
return a[right - 1];
}
void swap(int& a, int& b){
int tmp = a;
a = b;
b = tmp;
}
[1].快速排序.中国大学MOOC 浙江大学何钦铭、陈越《数据结构》https://www.icourse163.org/learn/ZJU-93001?tid=1003997005#/learn/content?type=detail&id=1007588514
[2].快速排序.维基百科 https://zh.wikipedia.org/wiki/%E5%BF%AB%E9%80%9F%E6%8E%92%E5%BA%8F