👉快速排序递归法
快速排序递归的基本思想:任取待排序元素序列中的某元素作为基准值,按照该排序码将待排序集合分割成两子序列,左子序列中所有元素均小于基准值,右子序列中所有元素均大于基准值,然后最左右子序列重复该程,直到所有元素都排列在相应位置上为止
也就是说,分为若趟排序,每一趟排序都把一个元素排到它的指定位置。再以这个元素为分界,分成左右两个数列,再分别对左右两个数列循环重复使用该方法,每一次都确定一个元素的位置,直至数列全部有序
🍗hoare法
这个方法的基本思想:利用左右两个指针,假设每一趟需要确定位置的元素为key,key为最左边的元素,左指针指向最左边,右指针指向最右边。利用右指针找比key小的元素,找到后右指针先不动。再利用左指针找比key大的元素,找到后左右指针指向的元素交换。直至左右指针相遇,再把相遇点的元素和key交换,这样排完一趟后,key的左边都是比它小的元素,右边都是比它大的元素
这样单趟排完后,可以看到5(key)这个元素的左边都是比它小的,右边都是比它大的。
单趟排完后,以5作为分割,先排序5左边的数列,每次生成的key都作为分割,先把左边的数列排列有序,之后再排序key右边的数列直至整个数列全部有序即可。
利用递归法,当右指针等于或小于左指针的时候,该次递归结束
下图为全程演示
//hoare
int PartQuickSort1(int* a, int left, int right) {
//单趟排序能够将一个数据排好,不需要再动了
int key = left;
while (left < right) {
//右指针找小
while (left < right && a[right] >= a[key])
right--;
//左指针找大
while (left < right && a[left] <= a[key])
left++;
//左右都找到后交换两个数据
//要注意避免左指针比右指针大了也交换
if (left < right)
Swap(&a[left], &a[right]);
}
//交换key和左右指针相遇点的数据
int meet = left;
Swap(&a[meet], &a[key]);
//返回相遇点
return meet;
}
void QuickSort(int* a, int begin, int end) {
//递归结束的条件:当begin大于end时,或者等于end时
if (begin >= end)
return;
int key = PartQuickSort1(a, begin, end);
//单趟排序能够将一个数据排好,不需要再动了
//所以递归实现每一次key位置的左边区间和右边区间
QuickSort(a, begin, key - 1);
QuickSort(a, key + 1, end);
}
🍗挖坑法
挖坑法和hoare法差不多,个人感觉挖坑法比hoare法的出错率更低
基本思想:先用key记录下最左边的值,然后最左边的位置形成一个坑位,右指针往前找比key要小的元素,找到之后填入坑位,右指针的位置变成一个新的坑位;接着左指针往后找比key大的元素,找到后填入右指针指向的坑位,左指针的位置变成一个新的坑位,依次往复循环,直到左指针和右指针相遇,相遇点就是一个坑位,再把key填入该坑位即可。
一趟排序过后,key的左边都是比它小的元素,右边都是比它大的元素
递归思想和hoare法一样,每一次都以新的key为分割,先把左子区间排列有序,再把右子区间排列有序
下图为全程演示
//挖坑法
int PartQuickSort2(int* a, int left, int right) {
int key = a[left];
//记录坑位
int hole = left;
while (left < right) {
//右边找小
while (left < right && a[right]>key)
right--;
//找到之后放入坑位
//并且更新坑位
a[hole] = a[right];
hole = right;
//左边找大
while (left < right && a[left] < key)
left++;
//找到之后放入坑位
//并且更新坑位
a[hole] = a[left];
hole = left;
}
//将key填入最后一个坑位
a[hole] = key;
//返回相遇点
return hole;
}
void QuickSort(int* a, int begin, int end) {
//递归结束的条件:当begin大于end时,或者等于end时
if (begin >= end)
return;
int key = PartQuickSort2(a, begin, end);
//单趟排序能够将一个数据排好,不需要再动了
//所以递归实现每一次key位置的左边区间和右边区间
QuickSort(a, begin, key - 1);
QuickSort(a, key + 1, end);
}
🍗前后指针法
基本思想:key的值为最左边的元素,利用前后指针,后指针先走找比key小的元素,如果找到了前指针往后走一步,当前指针比key大,后指针同时比key小时,交换两个指针的元素。当后指针比数列最右边的位置还后时,前指针指向的位置和key的位置交换。一趟排序后,key的左边都是比它小的元素,右边都是比它大的元素
这个方法的递归思想和前面的都一样,每一次都以新的key为分割,先把左子区间排列有序,再把右子区间排列有序
//前后指针法
int PartQuickSort3(int* a, int left, int right) {
int key = left;
//创建前后指针
int prev = left;
int cur = left + 1;
while (cur <= right) {
//如果后指针小于key位置的值
//前指针往前走一步
if (a[cur] < a[key])
prev++;
//如果后指针的值小于key位置的值
//并且前指针的值大于key位置的值
//则两个位置的数据交换
//小的到前面,大的到后面
if (a[prev] > a[key] && a[cur] < a[key])
Swap(&a[prev], &a[cur]);
cur++;
}
//走到最后,交换前指针和key位置的值
Swap(&a[key], &a[prev]);
return prev;
}
void QuickSort(int* a, int begin, int end) {
//递归结束的条件:当begin大于end时,或者等于end时
if (begin >= end)
return;
int key = PartQuickSort3(a, begin, end);
//单趟排序能够将一个数据排好,不需要再动了
//所以递归实现每一次key位置的左边区间和右边区间
QuickSort(a, begin, key - 1);
QuickSort(a, key + 1, end);
}
👉对于快速排序递归法的优化
🍗三数取中作key
对于以上递归的几种方法都有一个公共点那就是都得取一个key值,但是会有特殊的情况;例如如果是排序一个逆序时,如果还是按照取最左边的话效率就会低下一点,这时候我们可以对key这个值进行优化一下。
对最左值,最右值,中间位置的值,这三个元素进行比较,取三数中第二大的元素,然后和最左边的元素进行交换
这样一来就能够提升效率了,下面以挖坑法为例
int GetMIdIndex(int* a, int left, int right) {
//取最左边,中间,最右边,三数中的中间值
int mid = left + (right - left) / 2;
if (a[left] < a[mid]) {
if (a[mid] < a[right])
return mid;
else if (a[left] > a[right])
return left;
else
return right;
}
else {
if (a[mid] > a[right])
return mid;
else if (a[left] < a[right])
return left;
else
return right;
}
}
//挖坑法
int PartQuickSort2(int* a, int left, int right) {
//三数取中优化,将取出的key值和最左边交换
int mid = GetMIdIndex(a, left, right);
Swap(&a[left], &a[mid]);
int key = a[left];
//记录坑位
int hole = left;
while (left < right) {
//右边找小
while (left < right && a[right]>key)
right--;
//找到之后放入坑位
//并且更新坑位
a[hole] = a[right];
hole = right;
//左边找大
while (left < right && a[left] < key)
left++;
//找到之后放入坑位
//并且更新坑位
a[hole] = a[left];
hole = left;
}
//将key填入最后一个坑位
a[hole] = key;
//返回相遇点
return hole;
}
其他的两个方法也是一样的,取出中间值后,再进行和最左值交换
🍗小区间优化法
对于递归来说,越往下深度越深,也就是次数会越多。如果递归到需要排序的数列只有几个元素的时候,再使用递归法就会极大的消耗时间,所以我们到不如可以判断一下,如果需要排序的数列只有几个元素了,我们直接使用插入排序将它排好,这样可以极大的减少递归的深度
void QuickSort(int* a, int begin, int end) {
//递归结束的条件:当begin大于end时,或者等于end时
if (begin >= end)
return;
//进行小区间优化
if (end - begin <= 8)
InsertSort(a + begin, end - begin + 1);
else {
int key = PartQuicksort3(a, begin, end);
//单趟排序能够将一个数据排好,不需要再动了
//所以递归实现每一次key位置的左边区间和右边区间
QuickSort(a, begin, key - 1);
QuickSort(a, key + 1, end);
}
}
👉快速排序非递归法
既然有了递归法,那我们也需要学会使用非递归法
快速排序的非递归法需要用到栈的先入后出的性质,这里就不写栈的创建了,具体可以看之前的文章 栈和队列
模拟递归的流程,我们可以先把数列的最左位置和最右位置放入栈里,然后依次取出,取出来后再进行任意方法的单趟排序,将第一次的key排好,然后更新key,再分成两个部分,对最左和key-1的位置入栈,对key+1和最右的位置入栈,这样取出来的时候就呼应上了。循环反复这样执行,但是要注意如果最左的位置不小于最右的位置时,那么就不再进行入栈操作,也就是说这个时候栈就为空了,那我们的循环就结束。
//快速排序非递归法
void QuickSortNonR(int* a, int begin, int end) {
ST st;
StackInit(&st);
//先入左再入右
StackPush(&st, begin);
StackPush(&st, end);
while (!StackEmpty(&st)) {
//取出最右
int right = Stacktop(&st);
StackPop(&st);
//取出最左
int left = Stacktop(&st);
StackPop(&st);
if (left >= right)
continue;
int key = PartQuickSort2(a, left, right);
//对key位置进行分割,分别入栈
StackPush(&st, key + 1);
StackPush(&st, right);
StackPush(&st, left);
StackPush(&st, key - 1);
}
StackDestroy(&st);
}
👉总结
快速排序相较于上几个排序思路也没有复杂很多,但是方法很大,最好是全部都能理解吃透,整体而言还是比较有难度的,需要勤加练习。