原理
我们对以下序列进行快速排序:
-1 | 2 | 3 | -2 | 5 | 2 | -3 | 1 |
下面我将第一个数作为基准数(pivot = arr[left],这边left指的是最左端数 )来进行比较(也可以是序列中其他数):
-1 | 2 | 3 | -2 | 5 | 2 | -3 | 1 |
我们需要以基准数为分界点,让基准数左边的数比它小(即左边序列成员都 <= -1),基准数右边的数比它大(即又边序列成员都 >= -1)。
排序过程
总序列
我们通过设置两个哨兵left,right (left < right),从序列两端开始与基准数(为了方便后面基准数我将标红)比较。
-1 | 2 | 3 | -2 | 5 | 2 | -3 | 1 |
left | right |
我们先移动右边的哨兵(必须是右边先移动,后面我会解释为什么这样做):
由于 1 > -1,符合条件(条件:arr[right] >= pivot),所以我们将哨兵right左移到下一成员与基准数比较。
我们发现 -3 < -1 ,不符合条件,所以哨兵right停下。
-1 | 2 | 3 | -2 | 5 | 2 | -3 | 1 |
left | right |
接下来我们将哨兵left右移,发现 2 > -1 , 不符合条件 (条件:arr[left] <= pivot),所以哨兵left停下。
-1 | 2 | 3 | -2 | 5 | 2 | -3 | 1 |
left | right |
当两个哨兵停下时(即 arr[left] > pivot , arr[right] < pivot ),进行成员交换。
将left和right处成员交换。
-1 | -3 | 3 | -2 | 5 | 2 | 2 | 1 |
left | right |
交换后就使得哨兵left处的值小于基准数,哨兵right处的值大于基准数。
然后我们移动右哨兵right继续比较,直到遇到不符合条件的成员,发现 -2 < -1 ,right哨兵停下。
-1 | -3 | 3 | -2 | 5 | 2 | 2 | 1 |
left | right |
下面继续移动左哨兵left,发现 3 > -1 , left哨兵停下。
-1 | -3 | 3 | -2 | 5 | 2 | 2 | 1 |
left | right |
将left和right处成员交换。
-1 | -3 | -2 | 3 | 5 | 2 | 2 | 1 |
left | right |
继续移动哨兵right,发现left哨兵与right哨兵相遇了,说明我们第一遍的排序结束了。
-1 | -3 | -2 | 3 | 5 | 2 | 2 | 1 |
left,right |
我们只需要将基准数的位置与哨兵相遇处交换:
int temp = pivot;
pivot = arr[left]; // 或者 pivot = arr[right];
arr[left] = temp;// 或者 arr[right] = temp;
-2 | -3 | -1 | 3 | 5 | 2 | 2 | 1 |
左侧成员 <= -1 | left,right | 右侧成员 >= -1 |
经历了第一遍比较后,我们发现:
- 基准数左侧的成员都小于基准数,右侧成员都大于基准数。
- 但左边和右边的序列目前都还未排序完。
现在有了之前排序的思路,我们只要将其再分别处理左边和右边的序列即可。
左序列
-2 | -3 | -1 | |||||
left | right |
由于 -3 < -2 ,哨兵right停下。我们移动哨兵left到下个位置,发现与哨兵right相遇。
同之前在总序列一样,只需要将基准数的位置与哨兵相遇处交换,可得:
-3 | -2 | -1 |
这样我们第一遍排序后左测序列已经排序完了。
右序列
-1 | 3 | 5 | 2 | 2 | 1 | ||
left | right |
方法仍然是一样的,下面我不做详细解释了,直接演示下去。
arr[right] < pivot(基准数) , 哨兵right停下。
-1 | 3 | 5 | 2 | 2 | 1 | ||
left | right |
发现arr[left] > pivot,哨兵left停下。
-1 | 3 | 5 | 2 | 2 | 1 | ||
left | right |
将left和right处成员交换。
-1 | 3 | 1 | 2 | 2 | 5 | ||
left | right |
继续移动哨兵right,到其停下。
-1 | 3 | 1 | 2 | 2 | 5 | ||
left | right |
继续移动哨兵left,到其停下。
-1 | 3 | 1 | 2 | 2 | 5 | ||
left,right |
将基准数的位置与哨兵相遇处交换:
-1 | 2 | 1 | 2 | 3 | 5 | ||
left,right |
我们发现与总序列的排序后状况类似,我们继续迭代排序下去。
右序列中的左序列
2 | 1 | 2 | 3 |
排序后:
1 | 2 | 2 | 3 |
这样我们整个序列就排序完了。
原序列为:
-1 | 2 | 3 | -2 | 5 | 2 | -3 | 1 |
排序后为:
-3 | -2 | -1 | 1 | 2 | 2 | 4 | 5 |
可以发现快速排序的每一遍处理就是将基准数归位,迭代下去,排序就完成了。
可以说快速排序是冒泡排序的改良版,它的最优以及平均时间复杂度为O(nlogn),但它遇到最坏的情况下时间复杂度仍和冒泡排序一样为O(n^2)。
这个是我用python写的可视化(可能看起来不太好)
# 橙色为排序基准数 黄色为右哨兵 青色为左哨兵 绿色为交换后的基准数位置
为什么要右哨兵先行?
我们可以修改代码看看左哨兵先行会发生什么。
修改后的排序值:-3 0 -1 5 7 (这里数组越界了所以7这个值出错了,pivot = a[5] 很显然越界)
很明显这出错了,下面我将排序过程写出来。
-1 | -3 | 0 | 7 | 5 |
left | right |
左哨兵先行排序过程
总序列
QuickSort(array,0,num); // 0为遍历的序列左端位置,num为序列右端位置
<1>左哨兵移动至停下
-1 | -3 | 0 | 7 | 5 |
left | right |
<2>右边哨兵移动至停下
-1 | -3 | 0 | 7 | 5 |
left,right |
<3>与基准数交换
0 | -3 | -1 | 7 | 5 |
left,right |
左序列
进入左序列递归 QuickSort(array,0,left-1); // left -1 =1
0 | -3 | -1 | 7 | 5 |
left | right |
<1>左哨兵移动至停下
0 | -3 | -1 | 7 | 5 |
left,right |
<2>与基准数交换
-3 | 0 | -1 | 7 | 5 |
left,right |
再次进入左序列递归 QuickSort(array,0,left-1); // left -1 =0
由于 0 == left - 1 ,退出本次的左序列递归。
进入右序列递归函数QuickSort(array,0+1,right);
由于 0+1 == right ,退出右序列递归。
右序列
进入右序列递归 QuickSort(array,left+1,5); // left -1 =1
-3 | 0 | -1 | 7 | 5 |
left | right |
<1>左哨兵移动至停下
-3 | 0 | -1 | 7 | 5 |
left,right |
<2>与基准数交换
-3 | 0 | -1 | 5 | 7 |
结果为:
原序列 | -1 | -3 | 0 | 7 | 5 |
左哨兵先行结果 | -3 | 0 | -1 | 5 | 7 |
右哨兵先行结果 | -3 | -1 | 0 | 5 | 7 |
原因
我觉得原因是:
哨兵left移动后到达比基准数大的数位置,可能改位置后的还有一个数大于这个基准数,但left已经无法移动。
如果此时哨兵left所在位置右边都大于基准数的话,只能被迫等待right移动会面,把大于基准数的数交换到了前面。
例:
这是待排序序列:
1 0 3 7 2
哨兵left移动到3
1 0 3 7 2
^
下面移动哨兵right到3,然后与基准数交换
3 0 1 7 2
很显然这使得比基准数大的数交换到了左边。
如果是右哨兵先行的话就能保证交换的数是绝对比基准数小的。
当然这也是我们使用的基准数是第一位数才造成的问题,我们可以自己改动代码来解决问题。
代码
最后给大家奉上代码:
C
可以在dev上编译
#include <stdio.h>
int QuickSort(int a[],int left,int right){
if(left >= right)
return 0; //到最末结束递归用 。
int pivot = a[left]; //基准数,用作被对比 。
int l =left,r =right;//l,r为哨兵即数组中位置标记 。
while(l < r){
while(a[r] >= pivot && l < r )//右哨兵先行 。
r--;
while(a[l] <= pivot && l < r )
l++;
//这两个while判定排序正确的话(即左侧数比基准数小,右侧数比基准数大),则将哨兵移动至下一位
if(l < r){ //两哨兵停下且未相遇,即左侧数比基准数大,右侧数比基准数小,交换即可 。
int temp = a[l];//这里我推荐写个swap(&a,&b);来实现
a[l] = a[r];
a[r] = temp;
}
}
a[left]=a[l];
a[l]=pivot; //将基准数移至中间 。
QuickSort(a,left,l-1);
QuickSort(a,l+1,right);//递归遍历左右 。
}
void main(){
int num=0,i=0;
scanf("%d",&num);//输入数组成员个数 。
int array[num];
for(i=0;i < num;i++)
scanf("%d",&array[i]);//输入数组成员 。
QuickSort(array,0,num);//快速排序 。
for(i=0;i < num;i++)
printf("%d ",array[i]);//打印排序后数组成员 。
}
C++
可以在VS上编译
#include <iostream>
using namespace std;
int QuickSort(int a[], int left, int right) {
if (left >= right)
return 0; //到最末结束递归用 。
int pivot = a[left]; //基准数,用作被对比 。
int l = left, r = right;//l,r为哨兵即数组中位置标记 。
while (l < r) {
while (a[r] >= pivot && l < r)//右哨兵先行 。
r--;
while (a[l] <= pivot && l < r)
l++;
//这两个while判定排序正确的话(即左侧数比基准数小,右侧数比基准数大),则将哨兵移动至下一位
if (l < r) { //两哨兵停下且未相遇,即左侧数比基准数大,右侧数比基准数小,交换即可 。
int temp = a[l]; //这里我推荐写个swap(&a,&b);来实现
a[l] = a[r];
a[r] = temp;
}
}
a[left] = a[l];
a[l] = pivot; //将基准数移至中间 。
QuickSort(a, left, l - 1);
QuickSort(a, l + 1, right);//递归遍历左右 。
}
void main() {
int num = 0, i = 0;
cin >> num;//输入数组成员个数 。
int* array = new int[num + 1]; //动态申请空间,gcc可以直接申请含变量的,VS的c语言可以用malloc来申请空间大小,calloc决定块。
for (i = 0; i < num; i++)
cin >> array[i + 1];//输入数组成员 。
QuickSort(array, 0, num);//快速排序 。
for (i = 0; i < num; i++)
cout << array[i + 1] <<" ";
delete[] array;
}
Python
def QuickSort(arr, left, right):
if left >= right:
return None # 到最末结束递归用 。
pivot = arr[left] # 基准数,用作被对比 。
l = left
r = right # l,r为哨兵即数组中位置标记 。
while l < r:
while arr[r] >= pivot and l < r: # 右哨兵先行 。
r = r - 1
while arr[l] <= pivot and l < r:
l = l + 1 # 这两个while判定排序正确的话(即左侧数比基准数小,右侧数比基准数大),则将哨兵移动至下一位
if l < r: # 两哨兵停下且未相遇,即左侧数比基准数大,右侧数比基准数小,交换即可 。
temp = arr[l]
arr[l] = arr[r]
arr[r] = temp # swap操作
arr[left] = arr[l]
arr[l] = pivot # 将基准数移至中间 。
QuickSort(arr, left, l - 1)
QuickSort(arr, l + 1, right) # 递归遍历左右 。
array = input("输入序列,以空格分开成员。\n")
array = array.split(' ') # 注意输入格式正确
array = [int(x) for x in array]
length = len(array)
print(f"length= {length}")
QuickSort(array, 0, length - 1)
print(array)