文章目录
快速排序
-
思路
快速排序与归并排序类似,也是使用了分而治之的思路,即在数组中随机寻找一个元素,称为“主元”,然后将数组中其余元素分为两个子集,子集1中元素比主元小,放在主元左边;子集2中元素比主元大,放在主元右边,该过程称为“子集划分”。将两个子集递归地“找主元”和“子集划分”,当最后子集只有一个元素的时候,数组就排好序了。 -
伪码描述
void QuickSort(ListElementType *A,int N) { pivot = 从A[]中选取一个主元; 将剩余元素分成两个子集: A1 = {a∈A[] | a ≤ pivot}和 A2 = {a∈A[] | a ≤ pivot}; A[] = QuickSort(A1,Size1) ∪ pivot ∪ QuickSort(A2,Size2); }
-
选取主元
主元的选取其实关乎算法的复杂度,如果主元每次都选中最小的或最大的一个元素的话,快速算法的时间复杂度其实就会变成 O ( N 2 ) O(N^2) O(N2),而当主元是整个数组的第(Size/2)大的时候(刚好将两个子集长度差最小),快速排序的时间复杂度就会变成 O ( N log N ) O(N \log N) O(NlogN)。但是我们并不知道无序数组中的每个元素的大小,所以当我们选取主元的时候,一般会取无序数组第一个元素、中间元素和最后元素三个元素的中位数。/* 快速排序中寻找主元的操作,采用取第一个元素、中间元素和最后一个元素的中位数的方法 */ ListElementType Median3(ListElementType *A, int Left, int Right) { int Center = (Left + Right) / 2; /* 数组中间位置的元素 */ /* 交换位置,使得A[Left] < A[Center] < A[Right],取A[Center]作为主元 */ 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]); } /* 交换倒数第二个元素和A[Center],方便后面将数组子集的划分 */ Swap(&A[Center], &A[Right - 1]); /* 返回主元 */ return A[Right-1]; }
-
子集划分
- 在选取主元的函数中,我们已经将主元放在了A[Right-1]的位置上,且,已经直到A[Left] ≤ \leq ≤A[Right-1] ≤ \leq ≤A[Right],所以我们只需要处理从A[Left+1]到A[Right-2]的元素。
- 例:下面的序列是从A[Left+1]到A[Right-1],其中A[Right-1]=6就是数组的主元,对下面的序列进行子集划分。
- 定义左边的指针i和右边的指针j;
- i指向的元素与主元6进行比较,如果A[i]
<
<
< 6,i继续向右移动,直到A[i]
≥
\geq
≥ 6,此时i暂停;
- j指向的元素与主元6进行比较,如果A[j]
>
>
> 6,j就继续向左移动,直到A[j]
≤
\leq
≤ 6,此时j暂停;
- 此时A[i]指向元素大于等于6,A[j]指向元素小于等于6,将A[i]与A[j]交换位置;
- 重复第2、第3和第4步,直到i
>
>
> j;
- 此时A[i]是数组中最左边的大于主元6的元素,将A[i]和6互换位置。至此划分子集的操作完成。
- 定义左边的指针i和右边的指针j;
-
代码实现
/* 接口不规范的快速排序 */ void Quick_Sort(ListElementType *A, int Left,int Right) { int i = 0, j = 0; ListElementType Pivot; /* 寻找主元 */ /* 如果数组元素比阈值大,则调用快速排序,否则直接插入排序就好了 */ if (Left < Right) { Pivot = Median3(A, Left, Right); /* 找到主元 */ i = Left ; j = Right - 1; /* 划分子集 */ while (i < j) { i++; j--; while (A[i] < Pivot) { i++; } while (A[j] > Pivot) { j--; } if (i < j) { Swap(&A[i], &A[j]); } else { break; } } Swap(&A[Right - 1], &A[i]); /* 递归调用快速排序 */ Quick_Sort(A, Left, i - 1); Quick_Sort(A, i + 1, Right); } } /* 快速排序 */ void QuickSort(ListElementType *A, int Size) { Quick_Sort(A, 0, Size - 1); }
-
代码优化
我们都知道使用递归函数是很占用系统资源的,为了加快排序速度,可以设置一个阈值CutOff,当递归某一阶段中数组的元素个数小于了这个阈值CutOff,就不必再继续调用递归函数,而是直接插入排序就好了。/* 接口不规范的快速排序 */ void Quick_Sort(ListElementType *A, int Left,int Right) { int i = 0, j = 0; ListElementType Pivot; /* 寻找主元 */ /* 如果数组元素比阈值大,则调用快速排序,否则直接插入排序就好了 */ if (CutOff <= Right - Left) { Pivot = Median3(A, Left, Right); /* 找到主元 */ i = Left; j = Right - 1; /* 划分子集 */ while (i < j) { i++; j--; while (A[i] < Pivot) { i++; } while (A[j] > Pivot) { j--; } if (i < j) { Swap(&A[i], &A[j]); } else { break; } } Swap(&A[Right - 1], &A[i]); /* 递归调用快速排序 */ Quick_Sort(A, Left, i - 1); Quick_Sort(A, i + 1, Right); } else { InsertionSort(A + Left, Right - Left + 1); } } /* 快速排序 */ void QuickSort(ListElementType *A, int Size) { Quick_Sort(A, 0, Size - 1); }
-
时间复杂度
最坏情况时间复杂度: T w o r s t ( N ) = O ( N 2 ) T_{worst}(N)=O(N^2) Tworst(N)=O(N2)
最好情况时间复杂的: T b e s t ( N ) = O ( N log N ) T_{best}(N)=O(N \log N) Tbest(N)=O(NlogN)
平均时间复杂度: T a v g ( N ) = O ( N log N ) T_{avg}(N)=O(N \log N) Tavg(N)=O(NlogN)
空间复杂度: S ( N ) = O ( log 2 N ) S(N)=O(\log_2 N) S(N)=O(log2N) -
稳定性
快速排序是不稳定排序。