⭐🌟
前言
首先我们来了解一下什么是快速排序,快速排序是交换排序中的其中一个,是一种比较高效的排序方法,时间复杂度为:N(logN)。通常采用分治算法,在1959年时由Tony Hoare发明,在1961年发布。顾名思义快排快排就是在八大排序中最快的一种,要不然快速排序也对不起这个名字。下面就来了解一下快排到底怎么实现的。*
⭐快排递归
🌟快速排序(挖坑法)
什么是挖坑?假设有这样一个数组
我们可以在数组最左边的数为坑位,也可以在数组的最右边的数作为坑位,看个人喜欢,一般将最左边的数作为坑位。
创建以下变量来作为下标:
int begin = 0, end = n-1; //n为数组的元素个数
int pivot = begin; //坑位
int key = arr[begin];
在这里把坑位的元素先保存到key,防止数据丢失,因而数组第一个数就可以被覆盖了从而形成坑;
开始时end下标先走去找比key小的值,找到后end停下来,我们把end下标对应的值放到坑位,然后end这个下标对应的值变成新的坑位。
此后begin对应下标值开始去找比key大的数与上方end一样,找到比key的值大后停下,我们把begin下标对应的值放到坑位,然后begin形成新的坑位。
begin结束后到end,end结束后到begin。那么什么时候才能结束呢?没错就是begin与end相遇时整个循环停下来
在结束循环后我们把end或者begin的其中一个下标赋予pivot;在把key的值放入pivot对应数组的下标即完成第一次排序
那么现在只是完成第一次预排序剩下的该怎么完成呢?
在数组里面进行挖坑排序第一趟后我们可以发现,当第一次找到坑位key的值就奠定了这个数在整个数组的位置。
我们将end与begin相遇时的下标记录下来用pivot保存,此时我们形成这样的区间 [ 0, pivot-1] pivot [ pivot+1 , n-1]; 此时pivot左边的数都是小于pivot的,右边的数都是大于pivot的。那么我们能不能就是将pivot下标左边和右边的都重新看成一个缩小版的新数组,然后再去挑一个新的坑再进行挖坑的思想进行下去,将大的问题逐步分成小问题。
如此下来我们就用递归的方法来解决剩下的子数组。那么问题来了到什么时候递归才会终止?
递归结束的判断就是数组区间数只剩一个的时候:
数组会一直往下分,当数组都分成了一个数时,我们就可以认为这个数就是有序的了就不再继续往下递归
void QuickSort1(int* arr, int left, int right)//传参时left为0,right为数组最后一个元素下标
{
if (left >= right) //递归结束标志
return;
int begin = left, end = right;
int pivot = begin;
int key = arr[begin];
while (begin < end) //判断是否相遇
{
while (arr[end] >= key && begin < end) //end开始找小,begin<end是为了防止数组里有多个相同数导致死循环
--end;
arr[pivot] = arr[end];
pivot = end;
while (arr[begin] < key && begin < end)//begin开始找大,begin<end是为了防止数组里有多个相同数导致死循环
++begin;
arr[pivot] = arr[begin];
pivot = begin;
}
pivot = begin;
arr[pivot] = key; //将key值归还
//[ left(0), pivot-1 ] pivot [ pivot+1, right]
QuickSort1(arr, left, pivot - 1);
QuickSort1(arr, pivot + 1, right);
}
🌟快速排序(Hoare法)
在前面我们讲了挖坑法,接下来的左右指针法对于挖坑法大同小异,排出来的第一趟的数与挖坑法排出来的顺序是有差别的。
还是原来的数组:
创建以下变量来表示下标
int begin = left, end = right;
int key = left;
把key看成一个准基点就是整个数组里面数的标杆,开始时下标end从数组最后一个数往前找小,找到的值对比标杆key,找到比标杆小的值后停下来;begin开始往后找大对比标杆key,找到比标杆大的值后停下来;交换begin与end对应的值,这样就可以将大于key的数往后排,小的key的数往前排。
那么什么时候停下来呢?还是begin与end相遇时结束整个循环。
最后交换key与begin或者end其中一个的数值完成第一次排序。
第一趟排序完剩下的可以利用递归思想将其他数据排序
下面代码展示
void Swap(int* x, int* y) //两数交换
{
int tmp = *x;
*x = *y;
*y = tmp;
}
void QuickSort2(int* arr, int left, int right)
{
if (left >= right)
return;
int key = left;
int begin = left ,end = right;
while(end>begin)
{
while(arr[end] >= arr[key] && end>begin)
end--;
while(arr[begin] <= arr[key] && end>begin)
begin++;
Swap(&arr[begin],&arr[end]);
}
Swap(&arr[begin],&arr[key]);
key = begin;
//区间[left,key-1] key [key+1,right]
QuickSort2(arr, left, key - 1);
QuickSort2(arr, key + 1, right);
}
🌟快速排序(前后指针法)
还是原来的数组,我们创立前后两个指针
int prev = left; //后指针
int cur = left+1; //前指针
int key = left; //标杆
前后指针法:
先将第一个数作为key值,创建前指针cur和后指针prev,开始时cur去找小,当cur找到比标杆key对应的值小的时候,++prev然后与cur发生交换,若是cur遇到比标杆值小的时候什么都不用管继续往后找即可。
那么什么时候是结束标志呢?当前指针cur超出数组范围后我们结束整个循环。最后标杆key与prev交换值即完成第一趟排序,再后面就是我们递归的思想了。
void Swap(int* x, int* y) //两数交换
{
int tmp = *x;
*x = *y;
*y = tmp;
}
void QuickSort3(int* arr, int left, int right)
{
if (left >= right)
return;
int key = left;
int prev = left, cur = left+1;
while(cur <= right)
{
if(arr[cur] < arr[key])
{
++prev;
Swap(&arr[cur],&arr[prev]);
}
++cur;
}
Swap(&arr[prev],&arr[key]);
key = prev;
//区间[left,key-1] key [key+1,right]
QuickSort3(arr, left, key - 1);
QuickSort3(arr, key + 1, right);
}
🔥 快速排序的优化
🍀三数取中
快排好是好,但是坏起来(这里的坏起来是指有序情况下)所达到的时间复杂度可以达到N^2。
为什么这样说呢?
不妨我们想想同为交换排序的冒泡排序,在有序的情况下我们可以在遍历第一遍时,我们就可以跳出来,但是快排不一样。
假设在一个升序的数组中,我们挖坑法,选到都是最左边或者最右边,那么在递归的时后都取不到中间值,都是在边缘取值那么我们所要排的数据是不是都要遍历一遍。
这个工程可不敢想象,我们有没有解决的方法呢?
我们可以采取三数取中的方法。
三数取中:就是在数组中分别取最开始,中间和最后一个数,进行判断挑选出中间值。
这样就能避免在一个数组中挑到最小或者最大数从而造成效率的低下。
int GetMidIndex(int* arr, int left, int right)
{
int mid = left + ((right - left) >> 1);//获取中间值下标
if (arr[left] > arr[mid])
{
if (arr[mid] > arr[right])
return mid;
else if (arr[right] > arr[left])
return left;
else
return right;
}
else //arr[mid] > arr[left]
{
if (arr[mid] < arr[right])
return mid;
else if (arr[right] > arr[left])
return right;
else
return left;
}
}
🍀小区间优化
快速排序第一趟排完后,就将一个数组中的一个数位置定位下来了。后面利用递归方式将数组往下分成小区间数组的过程中,我们是先将数组的左半边进行不断缩小成一个个数后进行排序,将左半边排完有序后才到右半边数组排序。
这样的递归方式就像是二叉树中前序遍历。
将整个大数组看成是树的根,当快排遍历完第一遍后,可以确定一个数的位置;将这个数看数组分界点,分成左右子树,也就是左半边和右半边的数组。当整个数组都分成一个个元素的时候不就是相当于遍历的整棵二叉树了吗?
一棵二叉树的节点分布就像金字塔一样:
越往下分所要递归的次数就越多,每往下一层所要递归的次数就是上一层的两倍。
假设一棵树的高度为10,那么我们可以得到这棵树的总节点为2^10-1 = 1023。
最后一层占有节点数为:512
倒数第二层节点数为:256
倒数第三层节点数为:128
这三层节点数占总节点数的87.5%.
那么快排递归次数也是如此;我们将最后三层的数据可以拿出来单独处理,这样就可以减少大量递归次数,减少栈帧创建的消耗和时间的消耗。
后面三层我们可以利用其他排序方法来帮我们处理,用什么排序来帮我们来做这件事呢?
我们可以用插入排序来帮我们处理:
快排每一次预排序都可以帮我们确定一个数在数组中应该待的位置,当我们快排排到后三层的时候是不是可以认为整个数组的总体都近乎有序。对于近乎有序的数组排序最快的莫过于插入排序了,插入排序在近乎有序的数组进行排序时时间复杂可以达到O(N),这也是为什么要用插入排序来帮忙了。
//两数交换
void Swap(int* x, int* y)
{
int tmp = *x;
*x = *y;
*y = tmp;
}
//三数取中
int GetMidIndex(int* arr, int left, int right)
{
int mid = left + ((right - left) >> 1);//获取中间值下标
if (arr[left] > arr[mid])
{
if (arr[mid] > arr[right])
return mid;
else if (arr[right] > arr[left])
return left;
else
return right;
}
else //arr[mid] > arr[left]
{
if (arr[mid] < arr[right])
return mid;
else if (arr[right] > arr[left])
return right;
else
return left;
}
}
//插入排序
void InsertSort(int* a, int n)
{
for (int i = 0; i < n-1; ++i)
{
int end = i;
int tmp = a[end + 1];
while (end >= 0)
{
if (tmp < a[end])
{
a[end + 1] = a[end];
--end;
}
else
{
break;
}
}
a[end + 1] = tmp;
}
}
void QuickSort1(int* arr, int left, int right)
{
if (begin >= end)
{
return;
}
if ((left - right + 1) < 15)
{
// 小区间用直接插入排序替代,减少递归调用次数
InsertSort(a + left, right - left + 1);
}
else //这里采用了快速排序的挖坑法
{
int index = GetMidIndex(arr, left, right);//进行三数取中
Swap(&arr[index], &arr[left]); //选出的值与数组第一个数进行交换
int begin = left, end = right;
int pivot = begin;
int key = arr[begin];
while (begin < end) //判断是否相遇
{
while (arr[end] >= key && begin < end) //end开始找小,begin<end是为了防止数组里有多个相同数导致死循环
--end;
arr[pivot] = arr[end];
pivot = end;
while (arr[begin] <= key && begin < end)//begin开始找大,begin<end是为了防止数组里有多个相同数导致死循环
++begin;
arr[pivot] = arr[begin];
pivot = begin;
}
pivot = begin;
arr[pivot] = key; //将key值归还
//[left,pivot-1] pivot [pivot+1,right]
QuickSort1(arr, left, pivot - 1);
QuickSort1(arr, pivot + 1, right);
}
}
⭐快排非递归
快排非递归思想,借鉴了栈帧建立与销毁的原理。
学过数据结构都了解过,栈的结构特征就是后进先出;这里了我们得借用数据结构栈来帮我们实现非递归的过程。
怎么利用栈的特性?在栈中应该传递什么进去?
在小区间优化过程中我们提到了,递归过程就如同二叉树的前序遍历,先递归完左半部分的整体数组,待有左半部分数组有序后,再递归右半部分的数组进行排序。那么我们是不是可以利用栈的先进后出的特点,先将数组右半部的区间下标先入栈,再将数组左半部分的区间入栈。
这样出栈时第一时间就可以先排序数组左半部分的区间了;当数组左半部分进行预排序后,再将数组左半部分的右半部分数组区间再入栈,后将左半部分数组的左半部分数组再入栈。一直迭代下去,这样就可以先将左半部分的数组都优先排序了,当左边都排序完,出栈都只剩右半部分的数组区间了,再继续预排入栈即可完成排序。
当栈为空时,栈里面没有可待排序的区间了这样就完成了快排非递归的过程。
由于C语言库函数中并没有给我们提供数据结构栈,我们得自己写一个栈出来:
typedef int STDatatype;
typedef struct Stack
{
STDatatype* a;
int capacity;
int top;
}ST;
void StackInit(ST* ps);
void StackDestroy(ST* ps);
void StackPush(ST* ps, STDatatype x);
void StackPop(ST* ps);
STDatatype StackTop(ST* ps);
bool StackEmpty(ST* ps);
int StackSize(ST* ps);
void StackInit(ST* ps)
{
assert(ps);
ps->a = (STDatatype*)malloc(sizeof(STDatatype) * 4);
if (ps->a == NULL)
{
perror("malloc fail");
exit(-1);
}
ps->top = 0;
ps->capacity = 4;
}
void StackDestroy(ST* ps)
{
assert(ps);
free(ps->a);
ps->a = NULL;
ps->top = ps->capacity = 0;
}
void StackPush(ST* ps, STDatatype x)
{
assert(ps);
if (ps->top == ps->capacity)
{
STDatatype* tmp = (STDatatype*)realloc(ps->a, ps->capacity * 2 * sizeof(STDatatype));
if (tmp == NULL)
{
perror("realloc fail");
exit(-1);
}
ps->a = tmp;
ps->capacity *= 2;
}
ps->a[ps->top] = x;
ps->top++;
}
void StackPop(ST* ps)
{
assert(ps);
assert(!StackEmpty(ps));
ps->top--;
}
STDatatype StackTop(ST* ps)
{
assert(ps);
assert(!StackEmpty(ps));
return ps->a[ps->top - 1];
}
bool StackEmpty(ST* ps)
{
assert(ps);
return ps->top == 0;
}
int StackSize(ST* ps)
{
assert(ps);
return ps->top;
}
void Swap(int* x, int* y)
{
int tmp = *x;
*x = *y;
*y = tmp;
}
int GetMidIndex(int* arr, int left, int right)
{
int mid = left + ((right - left) >> 1);//获取中间值下标
if (arr[left] > arr[mid])
{
if (arr[mid] > arr[right])
return mid;
else if (arr[right] > arr[left])
return left;
else
return right;
}
else //arr[mid] > arr[left]
{
if (arr[mid] < arr[right])
return mid;
else if (arr[right] > arr[left])
return right;
else
return left;
}
}
//快排Hoare法
int QuickSort2(int* arr, int left, int right)
{
int mid = GetMidIndex(a, begin, end);//三数取中
Swap(&a[begin], &a[mid]);
int key = left;
int begin = left ,end = right;
while(end>begin)
{
while(arr[end] >= arr[key] && end>begin)
end--;
while(arr[begin] <= arr[key] && end>begin)
begin++;
Swap(&arr[begin],&arr[end]);
}
Swap(&arr[begin],&arr[key]);
key = begin;
return key;//将排好的数的下标返回
}
//快排非递归
void QuickSortNonR(int* arr, int left, int right)
{
ST s;
StackInit(&s);//初始化栈
StackPush(&s, left);//先将整体数组区间入栈
StackPush(&s, right);
while (!StackEmpty(&s))//判读栈是否为空
{
right = StackTop(&s);
StackPop(&s);
left = StackTop(&s);
StackPop(&s);
int keyi = QuickSort2(arr, left, right);//这里采用了快排Hoare法
if (right > keyi + 1)//只剩一个元素时不在入栈
{
StackPush(&s, keyi + 1);
StackPush(&s, right);
}
if (left < keyi - 1)
{
StackPush(&s, left);
StackPush(&s, keyi - 1);
}
}
StackDestroy(&s);
}
至此快速排序就介绍到这里了,感谢大家支持!!!!