一、基本原理(以升序为例)
快速排序的核心思想,是选定一个值为key(一般为左边或者右边的第一个值),把比key大的值放在key的左边,把比key小的值放在key的右边,经过一轮排序,形成:比key小——key——比key大(key排在有序时应该在的位置),这样的序列,继续对[0,key-1]和[key+1,n-1]这两个区间进行重复排序,依次进行这个过程,直至区间为1或不存在时,整个待排序数组即为有序。
二、画图理解+代码展示
主函数和教换函数部分:
void Swap(int* a, int* b)
{
int tmp = *a;
*a = *b;
*b = tmp;
}
int main()
{
int arr[] = { 6,1,2,7,9,3,4,5,10,8 };
int sz = sizeof(arr) / sizeof(arr[0]);
QuickSort(arr, 0, sz - 1);
for (int i = 0; i < sz; i++)
{
printf("%d ", arr[i]);
}
return 0;
}
1.霍尔法
左边选key右边先走(右边选key,左边先走)(可以保证相遇位置一定比key小),右边找大于key的值后停留,左边找小于key的值停留,二者交换,重复右边走找大于key,左边走找小于key,交换,直到二者相遇,将key值与相遇位置的值进行交换,然后对[0,key-1],[key+1,n-1]这两个区间继续排序,直至区间为1或不存在时,整个待排序数组即为有序。
void QuickSort1(int* arr, int left, int right)
{
if (left >= right)//当区间为1或不存在时进行返回
{
return;
}
int keyi = left;
int begin = left;
int end = right;//保存区间端点,便于后续使用
while (left < right)//左边做key,右边先走
{
while (right > left && arr[keyi] <= arr[right])//右边找小
{
right--;
}
while (right > left && arr[keyi] >= arr[left])//左边找大
{
left++;
}
Swap(&arr[left], &arr[right]);//此时左边已经找到了大,右边也找到了小,二者进行交换
}
Swap(&arr[keyi], &arr[left]);//此时左右已经相遇,交换keyi与相遇位置的值(相遇位置既是left也是right)
QuickSort1(arr, begin, left - 1);//对[0,left-1]
QuickSort1(arr, left + 1, end);//对[left+1,n-1]
}
2.挖坑法
将左边第一个元素挖出来放在key中,右边走找比key小的值,找到后将该值转移到之前的坑位,并形成一个新坑。左边走找比key大的值,找到后将该值转移到坑中,并形成一个新坑,然后右边继续走,依次循环往复,最后左边和右边会在坑位相遇,此时把key保存的值放入坑中(此时key就放在了最终排序完成时key所应该在的位置。)形成:比key小——key——比key大的序列,再反复对左右区间进行递归即可使全体有序。
void QuickSort2(int* arr, int left, int right)
{
if (left >= right)//当区间只剩一个元素或者不存在时返回
{
return;
}
int begin = left;
int end = right;
int hole = left;
int key = arr[left];//找好hole,key,左边,右边的值
while (begin < end)//当左边和右边没有相遇时继续进行
{
while (end > begin && arr[end] > key)//找右小
{
end--;
}
arr[hole] = arr[end];
hole = end;//赋值与挖坑
while (end > begin && arr[begin] < key)//找左大
{
begin++;
}
arr[hole] = arr[begin];
hole = begin;//赋值与挖坑
}
arr[end] = key;//将key值放在相遇位置(相遇位置就是key的最终位置,既是end也是begin)
QuickSort2(arr, left, end - 1);
QuickSort2(arr, end + 1, right);
}
3.指针法
令prev指向起始位置,cur指向起始位置的下一个位置,key取起始位置的值。循环进行如下操作:若arr[cur]>key,++cur;若arr[cur]<=key,++prev,arr[prev]=arr[cur],++cur。简单来说就是:当cur指向的值小于key时,prev往后移1,cur和prev指向的值互换,无论cur指向的值与key光系如何,最后cur都要往后移1。当cur走完全部元素时循环结束,把key填入到prev的位置(此时,key已经排在了最终位置),再对[0,prev-1],[prev+1,n-1]重复进行上述操作即可最终使整个序列有序.
void QuickSort3(int* arr, int left, int right)
{
if (left >= right)//当区间只剩一个元素或者不存在时返回
{
return;
}
int prev = left;
int key = left;
int cur = left + 1;//以left为基准找好prev,cur,key的值
while (cur <= right)//当cur未走完时,继续执行
{
if (arr[cur] < arr[key] && ++prev != cur)//要点!!!
{
Swap(&arr[prev], &arr[cur]);
}
cur++;
}
Swap(&arr[key], &arr[prev]);//当cur走完时,交换key和prev所指向的值
QuickSort3(arr, left, prev - 1);
QuickSort3(arr, prev + 1, right);
}
三、快速排序的优化
可见,快速排序实际上很类似于堆排序(其中的二分思想),可以想象所选key位置的值的最终位置越接近于sz/2处,原序列越接近二分,会使快速排序的效率越高,此外快速排序排顺序或者逆序或者类似2,2,3,3,4,5,6,6这种有重复数字连接在一起的数据时效率会很低。所以选key的过程很重要,以下是两种常用的选key方法,
1.随机选key法
在用随机数法在区间中生成一个随机数作为key值
int getrandomkey(int left,int right)
{
srand((unsigned int)time(NULL));
int key = rand() % (right-left+1);
return key+left;
}
2.三数取中法
分别检测left(左端点),right(右端点),mid(中间值)的大小,选出其中第二大的值所对应的位置作为key。
int getmidkey(int* arr, int left,int right)
{
int mid = (left + right) / 2;
if (arr[left] > arr[mid])
{
if (arr[right] > arr[left])
{
return left;
}
else
{
if (arr[mid] > arr[right])
{
return mid;
}
else
{
return right;
}
}
}
else
{
if (arr[right] > arr[left])
{
if (arr[right] > arr[mid])
{
return mid;
}
else
{
return right;
}
}
else
{
return left;
}
}
}
优化时,只需让以上两个函数找到的更好的key值与原来的key值交换即可
int key=left;//一般都是左边选key
int taget=getrandomkey(left,right);//用以上两种方法选出更好的key(target)
Swap(&arr[key],&arr[target]);//交换key与target位置的值
3.小区间优化
当区间小于一定值时采用别的排序,可提高效率。
void QuickSort(int* arr,int left,int right)
{
if(left>=right)
{
return;
}
if(right-left>10)//当排序区间小于10时,采用快速排序。
{
InsertSort(arr,left,right);
}
........
........
}
四、快速排序非递归
要明确,递归每次改变的是区间,所以快速排序的非递归主要是把每次递归区间的改变通过循环来进行。这里我们使用栈进行辅助(队列也是可行的)
整个区间入栈--->整个区间出栈--->第一次排序--->返回key值的位置--->[0,key-1]入栈,[key+1,n]入栈--->[0,key-1]出栈,排序--->[0,key-1]的子区间入栈出栈排序(直到[0,key-1]的子区间排完)--->[key+1,n]出栈,排序--->[key+1,n]的子区间入栈出栈排序(直到[0,key-1]的子区间排完)-->整体有序。(当子区间只有一个值或者不存在时停止入栈)(当栈为空时,说明排序已经结束)
栈的代码不在这里展示了,想看的话可以翻翻博主的博客。
int _QuickSort4(int* arr, int left, int right)//这里也可以用霍尔法或者挖坑法来写单趟排序
{
if (left >= right)
{
return;
}
int prev = left;
int key = left;
int cur = left + 1;
while (cur <= right)
{
if (arr[cur] < arr[key] && ++prev != cur)
{
Swap(&arr[prev], &arr[cur]);
}
cur++;
}
Swap(&arr[key], &arr[prev]);
return prev;//返回key值最终所在的位置
}
void QuickSort4(int* arr, int left, int right)
{
ST st;
StackInit(&st);//创建栈并初始化
StackPush(&st,left);
StackPush(&st,right);//将整个区间放入栈中
while (!StackEmpty(&st))//当栈不为空是,继续循环
{
//注意end和begin的取值顺序,因为栈是后进先出,所以这里先取尾end,再取头begin
int end = StackTop(&st);
StackPop(&st);
int begin = StackTop(&st);
StackPop(&st);
int key = _QuickSort(arr, begin, end);//单趟排序得到key
if (key + 1 < end)//当[key+1,end]有两个及以上元素时,再让区间入栈
{
StackPush(&st, key + 1);
StackPush(&st, end);
}
if (key - 1 > begin)//当[begin,key-1]有两个及以上元素时,再让区间入栈
{
StackPush(&st, begin);
StackPush(&st, key - 1);
}
}
}
五、特性总结
1.时间复杂度:O(N*logN)
2.空间复杂度;O(logN)
3.稳定性:不稳定
六、小结
本人是C语言萌新一枚,希望能和大家多多交流,共同进步。如果对文章内容有什么指正、建议、疑问。欢迎在下方评论区留言一起探讨,谢谢大家!