快速排序和交换排序详解(含三路划分)
前言:快速排序和交换排序都属于交换排序,底层思想是根据序列中两个记录键值的比较结果来对换这两个记录在序列中的位置,将键值较⼤的记录向序列的尾部移动,键值较⼩的记录向序列的头部移动,从而达到排序的结果
1 冒泡排序
基本思想:
- 相邻比较:从第一个元素开始,依次和右边邻居比大小
- 大的后移:如果左边元素比右边大,就交换位置,让大元素往右挪
- 重复筛选:每遍历一轮,当前最大的元素就会 “沉” 到未排序部分的最后
- 逐步缩小范围:排除已 “沉底” 的最大元素,对剩下的未排序部分重复上述步骤,直到所有元素有序
冒牌排序动态图:

代码实现:
void my_swap(int* a, int* b)
{int temp = *a;*a = *b;*b = temp;
}
void BubbleSort(int* a, int n) 
{int exchange = 0;for (int i = 0; i < n; i++){for (int j = 0; j <n-i-1 ; j++){if (a[j] > a[j + 1]) {exchange = 1;swap(&a[j], &a[j + 1]);}}if (exchange == 0) {break;}}}
冒泡排序详解
第一步部分:
for (int j = 0; j <n-i-1 ; j++){if (a[j] > a[j + 1]) {exchange = 1;swap(&a[j], &a[j + 1]);}}
内层循环控制执行在未排序的范围内相邻元素的比较和交换,从而使未排序范围中的最后一个位置是当前范围的最大值。
对内存循环条件j <n-i-1的说明:
内层循环是逐渐减少的随着外层循环(因为每完成一次内层循环就确保了一个元素的位置),已经确定位置的后面元素自然不用再重复比较进行排序。所以循环次数与外层的i有关。
第二部分
if (exchange == 0) {break;}
在内层循环中,假如遍历这个序列的元素进行比较的话,如果存在
a[j] > a[j + 1],则将exchange赋值为1。那么如果内层循环一边后exchange还是为0则表明这个序列就是有序的,我们直接跳出外层循环从而节省时间。
第三部分
for (int i = 0; i < n; i++){......}
外层循环用来控制内层循环的次数,也是对内层循环里面的比较范围进行更改和限制。一次外层循环的结束表明就会有一个最大的元素被放至未排序部分的末尾,并且内层循环的范围也会相应的
-1。
2 快速排序
快速排序是Hoare于1962年提出的⼀种⼆叉树结构的交换排序⽅法,其基本思想为:任取待排序元素序列中的某元素作为基准值,按照该排序码将待排序集合分割成两⼦序列,左⼦序列中所有元素均⼩于基准值,右⼦序列中所有元素均⼤于基准值,然后最左右⼦序列重复该过程,直到所有元素都排列在相应位置上为⽌。
2 主体框架
快速排序的本质就是找基准值将这个待排序的序列通过基准值的位置确定,将序列一分为二,类似于二叉树一样的结构,将排序的问题分成多个小的排序的序列,从而加快排序速度。
快速排序很明显可以通过递归的方式实现
快速排序实现主体框架
void QuickSort(int arr[],int left,int right)
{if (left >= right){return;}//得到一个基准值int k = k_QuickSort(arr, left, right);QuickSort(arr, left, k - 1);QuickSort(arr, k + 1, right);
}
在该递归中,我们一步步的确定基准值,从而使序列有序化。
k_QuickSort函数是我们获取基准值的函数,有多种方式可以实现。
3 k_QuickSort函数的实现
3.1 hoare版本
3.1.1 算法思路
- 创建左右指针,确定基准值
- 从右向左找出⽐基准值⼩的数据,从左向右找出⽐基准值⼤的数据,左右指针数据交换,进⼊下次循环
3.1.2 代码实现
int  k_QuickSort(int arr[], int left, int right)
{//定义第一个为基准值int k = left;left++;while (right >= left){while (right >= left && arr[right] > arr[k]){right--;}while (right >= left && arr[left] < arr[k]){left++;}if (left <= right){my_swap(&(arr[left++]), &(arr[right--]));}}my_swap(&(arr[k]), &(arr[right]));return right;
}
代码详解
第一部分:
定义未排序的第一个值为基准值,并将left++改变left位置
int k = left;
left++;
第二部分:
right从右往左找比基准值小或等于的,left从左往右找比基准值大或等于的。找到后,left和right对应下标元素互换。
while (right >= left && arr[right] > arr[k]){right--;}while (right >= left && arr[left] < arr[k]){left++;}if (left <= right){my_swap(&(arr[left++]), &(arr[right--]));
关于left和right指定的数据和key值相等时也要交换的说明:
值相同参与交换确实会有额外的损耗(如没必要的交换),但这样是为了处理数组大量数据重复时,无法进行有效的分割。

正是由于相等交换的设计,才能让我们在大量数据重复时还能将基准值定位到数组中间而不是数组的两端,从而能有效的分割数组,从而提升时间复杂度,避免了在大量数据重复时,时间复杂度降为O(n2)
第三部分:
最后将基准值与right指向的元素互换位置,这时候基准值左边的数都不大于它,右边的数都不小于它
my_swap(&(arr[k]), &(arr[right]));
return right;
关于跳出循环后选择right的位置作为基准值放置位置说明:
循环结束的条件是left>right,此时right走到left的左边,而left经过的数据元素均不大于基准值,所以选择这个位置。
3.2 挖坑法
3.2.1 算法思想
- 用变量存储基准值,并在此处形成坑,创建左右指针(一个指向数组头一个指向数值位)
- 右指针从右往左找比基准值小的数据,找到后立即放入左边的坑中,此处变成新坑
- 左边指针从左往右找比基准值大的数据,找到放入右坑,当前位置变成新坑。
- 如此循环,直到left=right,此时将基准值填入这个坑中,并返回坑的下标
注意点:
- 左右指针的解释:左右指针并不是我们之前学的那个存储地址的指针,而是定位数据元素的下标的变量(可以通过下标找元素,所以形象的称为指针)
- 对坑的解释:我们可以将数据想象成种在这个数组土壤里的萝卜,我们一开始将基准值这个萝卜从地里拿走了形成一个坑,并按照规则去填坑并产生新的坑,直到最后一个坑将基准值这个萝卜放进去就结束。
3.1.2 代码实现
int k_QuickSort(int* arr, int left, int right)
{int hole = left;int temp = arr[left];while (left < right){//找到右边比较小的while (left < right && arr[right] > temp){right--;}//找到之后进行填坑挖坑arr[hole] = arr[right];hole = right;//找到左边比较大的while (left < right && arr[left] < temp){left++;}arr[hole] = arr[left];hole = left;}arr[hole] = temp;return hole;
}
代码详解
第一部分
循环条件为left<right,因为我们退出循环时需要left=right,此时这个位置为基准值的坑位。循环里面两个while循环,分别从右边找值填坑再产生新的坑(使指向坑的小下标hole更新)。
while (left < right){//找到右边比较小的while (left < right && arr[right] > temp){right--;}//找到之后进行填坑挖坑arr[hole] = arr[right];hole = right;//找到左边比较大的while (left < right && arr[left] < temp){left++;}arr[hole] = arr[left];hole = left;}
第二部分
最后将基准值填入最后一个坑中,并返回hole表示基准值的下标
arr[hole] = temp;
return hole;
填坑法的问题:
在我们循环里面有两个while循环:
while (left < right && arr[right] > temp)
while (left < right && arr[left] < temp)
我们可以看到只有找到大于或小于temp的数据元素的时候,left和right指针才移动,可是假如全部都是重复元素数据呢,那left和right指针就不会移动,会陷入死循环。可是将arr[right] > temp和arr[left] < temp的=都补上的话,在一定条件下时间复杂度又会降为O(n2)
综上所述:我们在找基准值的方法中很少用挖坑法
3.3 lomuto前后指针法
3.3.1 算法思想
- 创建前后指针prev和pcur,初始时prev指向数组第一个元素,pcur指向后面prev后面一个元素
- 当pcur指向元素小于基准值时,prev++再与pcur指向元素交换
- pcur指向数据元素- >=基准值时,只有- pcur移动
- 当pcur>right时循环结束,此时prev的位置便是基准值应该与之交换的位置
  
3.3.2 代码实现
int k_QuickSort(int arr[], int left, int right)
{int k = left;//int prev = left, pcur = left;int prev = left, pcur = left+1;while (pcur <= right){if (arr[pcur]<arr[k]&&++prev!=pcur)//先++再判断{my_swap(&(arr[pcur]), &(arr[prev]));}pcur++;}my_swap(&(arr[k]), &(arr[prev]));return prev;
}
prev扫描过的元素都是<基准值的(除了第一个基准值元素自己),所以最后prev的位置便是基准值的位置。
++prev!=pcur的说明:
当prev++与基准值pcur相等时,便不用交换元素,因为此时它们指向的就是一个元素。
快排时间复杂度:O(nlogn)
快排空间复杂度:O(logn)(递归导致)
4 快速排序的非递归版本
我们在快速排序中运用递归的过程就是将数组分割,并再将分割后的序列的首地址和尾地址传递给找基准值的函数。我们可以写出确定一个序列中的基准值函数
k_QuickSort,我们只要知道了每个序列的首地址和尾地址便可以一直调用这个k_QuickSort函数使数组有序。
我么可以借用栈来实现对每个序列前后指针的存储并释放填入k_QuickSort的参数,实现非递归快速排序
代码如下:
void my_QuickSort(int* arr, int left, int right)
{ST st;STInit(&st);//首先插入最开始的left和right进入栈中STPush(&st, left);STPush(&st, right);while (!STEmpty(&st)){int end = STTop(&st);STPop(&st);int begin = STTop(&st);STPop(&st);//将这最开始的拿出来进行lumoto方法int k = begin;int prev = begin;int pcur = prev + 1;while (pcur <= right){if (arr[pcur]<arr[k] && ++prev != pcur){my_swap(&(arr[prev]), &(arr[pcur]));}pcur++;}my_swap(&(arr[prev]), &(arr[k]));int ki = prev;if (ki - 1 > begin){STPush(&st, begin);STPush(&st, ki-1);}if (end > ki + 1){STPush(&st, ki+1);STPush(&st,end);}}STDestroy(&st);
}
代码详解:
第一部分
创建一个栈对象
st,并将数组的最开始的头尾指针left和right插入栈中,使栈不为空。
ST st;
STInit(&st);
//首先插入最开始的left和right进入栈中
STPush(&st, left);
STPush(&st, right);
第二部分
- 首先从栈中取出待排序得
begin和end,取出后将其从栈中除去- 有待排序的
begin和end,运用lumoto将基准值确定- 根据基准值将待排序列分为左部分
[begin,ki-1],右部分[ki+1,end],并在符合条件下将左部分和右部分的首尾指针插入栈中- 当栈为空循环结束
while (!STEmpty(&st)){int end = STTop(&st);STPop(&st);int begin = STTop(&st);STPop(&st);//将这最开始的拿出来进行lumoto方法//lumoto找基准值方法开始int k = begin;int prev = begin;int pcur = prev + 1;while (pcur <= right){if (arr[pcur]<arr[k] && ++prev != pcur){my_swap(&(arr[prev]), &(arr[pcur]));}pcur++;}my_swap(&(arr[prev]), &(arr[k]));int ki = prev;//lumoto找基准值方法结束//此时ki就是基准值得下标 if (ki - 1 > begin){STPush(&st, begin);STPush(&st, ki-1);}if (end > ki + 1){STPush(&st, ki+1);STPush(&st,end);}}
5 三路划分(找基准值)
三路划分是用来处理待排序序列有大量重复值的情况的。
5.1 算法思想
- key默认取- left位置的值
 -- left指向区间最左边,- right指向区间最后边,- cur指向- left+1位置
- cur遇到⽐- key⼩的值后跟left位置交换,换到左边,- left++,- cur++
- cur遇到⽐- key⼤的值后跟- right位置交换,换到右边,- right--
- cur遇到跟- key相等的值后,- cur++
- 直到cur>right结束
  
经过三路划分后,
left和right之间的是与基准值相同的值,left左边的是<基准值的数据,right右边的是>基准值的数据
代码如下:
typedef struct KeyWayIndex
{int lefti;int righti;
}KeyWayIndex;
KeyWayIndex PartSort3Way(int* arr, int left, int right)
{int key = arr[left];int cur = left + 1;while (cur <= right){if (arr[cur] < key){my_swap(&arr[cur], &arr[left]);left++;cur++;}else if (arr[cur] > key){my_swap(&arr[cur], &arr[right]);right--;}else{cur++;}}KeyWayIndex keyindex;keyindex.lefti = left;keyindex.righti = right;return keyindex;
}
三路划分后我们只要将
left之前的进行排序和right之后的进行排序就行了。


