接下来我们将用图像模拟来一步步演示快速排序的过程,这样我们将会通过视觉和大脑一起来梳理快速排序的思路。
后文示例的C语言代码将实现图像模拟的过程。
一、图像模拟 快速排序 过程
我们选取十个数字0~9当做我们的排序数字,并将其打乱。然后我们将按照升序进行排列。如下图:
1、选取基准数
首先要在这个序列中随便找一个基准数,在此我们选取第一个数字5作为基准数字。(选取基准数有多种方式,此方式不是唯一)如下图:
接下来我们要将这个基准数字5挪动到其应该呆的位置。
那么它应该呆在哪个位置呢,其实这个位置要满足两个条件:位置的左边都是小于5的数字,位置的右边都是大于5的数字(升序)。我们将这个位置叫作数字在排序中的正确位置,简称“ 正确位置”。
现在我们知道了要将5挪动到其正确位置,那么就产生了一个问题,怎么寻找正确位置呢?
这里就要引入快速排序的核心思想了,即“ 两端查找比较交换”。这个思想我们就不从文字予以解释了,下文直接上图。需要说明的是,在通过“两端查找比较交换”查找“正确位置”的时候会顺便将小于基准数5和大于基准数5的数字归位。
2、用指针指向无序序列的两端
我们在两端引入两个指针,一个指向左端,一个指向右端,分别用left和right进行标识。(两端指针)
3、查找比较交换(此过程是循环过程,圈号为重复的步骤)
①、右指针查找比较 我们先让右指针right进行查找(因为是升序,right先查找,后文还将提到)比基准数字5小的数字。如果指针下的数字不比基准数小,就一直向前查找。(查找比较) 此时right指针走到数字4时,和基准数字5比较后,显然小于。此时right指针应该停下。
②左指针查找比较 接下来让left指针开始查找大于基准数字5的数字,走到数字8时,和基准数字5比较显然大于,left指针停下。(查找比较)
③交换 现在left指针和right指针都指向了一个数字,接下来拿出这两个数字进行位置交换。(交换)
交换后
①右指针查找比较 接下来重新让right指针向前寻找比基准数5小的数。走到3时,与基准数5比较后小于成立,停下。(查找)
②左指针查找比较 再让left指针继续向后寻找比5大的数字。走到6时与5比较后大于成立,停下。(查找)
③交换 此时再次拿出两指针下数值进行位置交换。(交换)
①右指针查找比较 重新让right指针向前查找小于基准数5的数字,走到2时与基准数比较后小于成立,停下。(查找)
②左指针查找比较(查找) 让left指针继续向后查找大于基准数5的数字,此时我们可以看到两指针重合,这个时候就要停前进了,因为两指针重合表示基准数5的“正确位置”已经找到了。
③交换 找到基准数5的正确位置后,就要将基准数字挪动到正确位置,这时就要交换两位置的数字。(交换)
交换后。
到此“两端查找比较交换”的第一轮正式结束,你发现了吗?现在基准数5的左边都是小于5的数字,而右边都是大于5的数字。
需要说明:上文提到right指针先走,在这里将进行解释。
▪ 我们观察上面交换的两幅图像可以看到,与基准数字5交换位置的数字2是小于基准数字5的,交换位置后也正好让2挪动到了比5小的那边了。大家想想为什么两指针正好停在了比5小的2上面了呢。这当然是我们故意而为之的。
▪因为right指针是寻找比基准数5小的数字,当其停止后一定是停在了小于5的数字上面。然后再让left指针移动,left指针最后停止的位置必将是和right相遇的时候,这就保证了两指针停止的位置必然是小于基准数字的。
▪最后交换位置后,小的数字也就到了基准数字的左边。
▪ 当然,如果你选取基准数的方法不一样,升序降序不一样。先让right走还是left走也就不一样了。
说完指针先后的问题后,我们继续总结这一轮结束后的一些发现
思想总结:
我们可以发现“两端查找比较交换”的思想就是为了寻找“正确位置”, 可以说通过“两端查找比较交换”的思想实现了寻找基准数的“正确位置”。
你也会发现以基准数5为界限,分为了两个(2、0、1、4、3)和(6、8、9、7)两个无序序列。
剩下的工作就是对两个无序序列重复上述过程:
1、选取基准数
2、用指针指向无序序列两端
3、两端查找比较交换
话不多说,我们继续用图像模拟演示。(红线表示本轮的“两端查找比较交换”结束)
这一轮的“两端查找比较交换”我们要单独拿出5左边的序列进行排序,我们先来看一下经过上一轮的“两端查找比较交换”排序后的前后比较:
现在我们单独拿出5左边的序列(2、0、1、4、3)进行排序
选取基准数
我们还是选取第序列中的第一个数字2作为基准数。
用指针指向无序序列两端
注意,这一轮指针指向的是数字5左端(2、0、1、4、3)序列的两端。
查找比较交换
在此就不在进行文字详细描述,全程图像模拟演示。 需要记住这一轮right寻找比基准数2小的,left寻找比基准数2大的(因为是升序),两者都找到后就进行交换,两者相遇后就表示“正确位置”以找到。“正确位置”找到后就应该将基准数归位。
完成这轮排序后,你又会发现,以基准数2为界限分为了(1、0)和(4、3)两个无序序列。
到此,我们还有(1、0)序列,(4、3)序列,(6、8、9、7)序列,以及6、8、9、7序列排序完成后的(8、9、7)序列未进行排序。
我就不在一一用图像模拟,因为每一轮思路都一样,即每一轮处理就是将这一轮的基准数放在“正确位置”上。直到所有的数都放在“正确位置”上,排序就结束了。所以大家要重在理解上述思路。 理解了上述思路后,我们进行下面的代码设计。
二、程序代码设计(C语言)
理解了快速排序的思路后,就要去想如何用代码实现了。
执行每一轮的衔接问题:
这里要说一个重要的问题:问题如下▼
▲不知道大家在看上面图像模拟的时候,脑海里产生一个疑问没有,这个疑问就是,每一轮的“查找比较排序”执行完成后,程序应该怎么去执行下一轮的呢?每一轮应该怎样用程序很好的衔接呢? 关于这个问题,当然有很多种思路,下面的代码则用了递归的方法去实现每一轮的衔接。
下文,有代码叙述和C代码,大家可以结合在一起看,其中有递归的过程,需要懂得一些递归的相关知识。
代码叙述:
1、 首先,调用QkSort函数的时候需要给其传递三个值,
▷ “数组”,“左指针位置”,“右指针位置”。
2、用变量tmp作为存放本轮循环的基准数。
3、用变量i作为左指针,获取传递过来的值(变量left的值)
4、用变量j作为右指针,获取传递过来的值(变量right的值)
5、第一层while外循环的内部结构是:
▷有一个一直从后向前寻找比基准数小的右指针
▷有一个一直从前向后寻找比基准数大的左指针
▷有一个用于交换数字的if语句
6、while外循环执行完后就找到了“正确位置”
▷“基准数”归位的两个交换位置表达式:
▷arr[left] = arr[i];
▷arr[i] = tmp;
7、第一个递归
▷QkSort(arr, left, i - 1);
▷这个递归一直解决每次循环完后产生的两个序列的左序列,直到所有左序列都排序完成。
▷因此,你可以看到,第二个实际参数一直是将左指针传递给形参,
▷第三个实际参数一直是将上轮基准数的下标-1传递给形参。
▷这样就无形的将这个完整的数组虚拟的切割成了两个。
▷将参数传递过去后,函数的操作范围正好是无序的左序列。
▷如下图:
8、第二个递归 ▷QkSort(arr, i + 1, right);
▷这个递归需要等到每轮的左序列排完序后,即第一个递归都执行完后才执行。
▷其解决每次循环完后产生的两个序列的右序列,直到所有序列排序完成。
▷程序在执行完所有的第二个递归后,也表示着全部序列完成排序,序列整体已经有序,排序完成。
●说明:这个程序的难点就在于对递归的理解。
C语言代码:
#include<stdio.h>
//快速排序函数,形参列表为数组,左指针位置,右指针位置,int *arr等价于int arr[]
void QkSort(int *arr, int left, int right){
if (left > right) //左指针位置必须大于右指针位置
{
return;
}
//变量tmp为基准数,在此规定基准数为序列的第一个数,即左指针指向的数
int tmp = arr[left];
int i = left; //左指针
int j = right; //右指针
//外循环,直到左指针和右指针相等时退出,表示根据当前基准数以完成当前序列排序
while (i != j)
{ //内循环1,寻找到比基准数小的数时退出循环,此循环控制右指针
while (arr[j] >= tmp && j > i)
{
j--;
}
//内循环2,寻找到比基准数大的数时退出循环,此循环控制左指针
while (arr[i] <= tmp && j > i)
{
i++;
}
//经过以上两个内循环后,此时的左指针和右指针分别指向了
//比基准数小和比基准数大的数
//接下来要将这两个指针的数据进行交换
if (j > i)//交换前判断右指针是否大于左指针
{
int t = arr[i];
arr[i] = arr[j];
arr[j] = t;
}
}//外循环尾
//执行完循环后,就找到了基准数的排序位置,将基准数tmp与i位置进行交换
arr[left] = arr[i];
arr[i] = tmp;
//*********************************************
//下面的程序为递归,可能存在多层递归调用
//*********************************************
//此时的数组分为了两部分,基准数左边都是小于基准数的,右边都是大于基准数的,
//现在进行递归,对基准数左边的数进行排序,此时递归可能会有多层
QkSort(arr, left, i - 1);
//进行到这步时,基准数左边已经全部有序,而右边还未进行排序,
//现在进行递归,对基准数右边的数据进行排序,此时递归可能有多层
QkSort(arr, i + 1, right);
}
int main()
{
int arr[] = { 0, 4, 3, 5, 65, 2, 64, 68, 34, 94, 53, 74, 13 };
int len = sizeof(arr)/sizeof(int);
printf("待排序数值:");
for (int i = 0; i <=len-1; i++)
{
printf("%d ",arr[i]);
}
printf("\n");
printf("排序后的数值:");
QkSort(arr,0,len-1);//调用快速排序函数
for (int i = 0; i <=len-1; i++)
{
printf("%d ", arr[i]);
}
}
代码运行结果: