目录
本文将对快速排序进行详细的解读,耐心看完,你一定会有所收获
二、分区处理,将比这个数大的数放到这个数右边,将比这个数小的数放到左边;
优化:为了防止数据比较极端的情况,我们要采取两种方式对快速排序优化
基本步骤:
一、先从待排序的数列中选出一个数;
二、分区处理,将比这个数大的数放到这个数右边,将比这个数小的数放到左边;
三、继续分区,直到区间缩小到只有一个数;
一、先从待排序的数列中选出一个数;
这里先默认以最左边的数当作key,在分析复杂度的时候,我们会再次分析如何选数
二、分区处理,将比这个数大的数放到这个数右边,将比这个数小的数放到左边;
如何才能做到让选出key左边全是比key小的数,而右边全是比key大的数呢?即如何达到下图效果呢?
1.霍尔法
这里我们假设已经通过三数取中或随机选key,将最左边的数字换成了key,即key=7
那我们如何做到七的左边全是小于七的数字,七的右边全是大于七的数字呢
在这里我们定义头尾两个变量:
right向左边寻找,它的目的是找到比key要小的数字,如果找到就停止寻找;
left向右边寻找,它的目的是找到比key大的数字,如果找到就停止寻找;
在上图的例子中,right一开始的值就是1,1显然比key要小,因此right停止寻找
left向右寻找,直到找到9,9显然比key要大,因此left也停止寻找
到现在为止,left和right的位置如下图:
我们交换left和right指向的两个数字,这样比key大的9就去了right的后面,比key小的1,就来到了left左边,如图:
重复上述过程,直到left和right相遇,如图:
此时我们让相遇位置和key交换,得到以下结果:
这样我们就做到了上面的要求.
问题一:为什么能保证相遇位置一定小于等于key?
首先我们要知道是如何相遇的,无非就两种情况:
第一种情况:左边没动,右边一直向左寻找,直到相遇
右边找比key小的值,一直向左走,说明没有找到比key小的值就遇到了左边,停了下来
左边要么是原来的key,要么曾经已经找到过一次比key大的值,并且完成了交换,所以左边一定小于等于key,此时相遇一定小于等于key
第二种情况:右边没动,左边一直向右寻找,直到相遇
左边找比key大的,一直没有找到,而左边能开始找的前提是右边已经停下来了,也就是说右边已经找到了比key要小的值,此时相遇,相遇位置一定小于等于key
问题二:为什么左边是key,右边就要先走?
如图,如果左边先走,到这一步和右边先走是一样的,左边区域除了第一个key,其他数字已经全都小于等于了key,右边数字全都大于等于了key,按照要求,我们想让key左边都是小于等于key的数,所以,我们要把让left和key交换,这样我们就能明白为什么左边是key就要让右边先走了
代码:
int PartSort1(int* a, int begin, int end)
{
//霍尔大佬的方法
int mid = getmidindex(a, begin, end);
Swap(&a[begin], &a[mid]);
int left = begin;
int right = end;
int keyi = left;
while (left < right)
{
while (left < right && a[right] >= a[keyi])
{
right--;
}
while (left < right && a[left] <= a[keyi])
{
left++;
}
Swap(&a[left], &a[right]);
}
Swap(&a[left], &a[keyi]);
keyi = left;
return keyi;
}
需要注意:为了方便后续递归,我们返回的是key最终应在的位置
2.挖坑法
挖坑法比起霍尔的方法,更加直观
挖坑法的本质就是将left的值存起来,形成一个坑位,再像霍尔法一样将小的值填到左坑位,这样右边就是新的坑位了,再从左边找大的值填到右边
最终left和right会在坑位相遇,再将一开始的key填入这个坑位即可
代码:
int partSort2(int* a, int begin, int end)
{
//挖坑法
int mid = getmidindex(a,begin,end);
Swap(&a[begin],&a[mid]);
int left = begin;
int right = end;
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;
}
三、继续分区,直到区间缩小到只有一个数;
完成一次排序后,我们要考虑如何多次排序以完成排序的功能
递归法:
在分好一个数字后,我们的数组被分成三部分:
我们需要将key左边进行相同的操作,将key右边也进行相同的操作
直到区间只剩下一个数字,因此递归条件是:区间存在
前面我们用霍尔法或挖坑法获得了keyi,这样我们将(begin,keyi-1)传入函数再进行一次排序
将(keyi+1,end)也传入函数,继续一次排序,这样递归进行下去,就实现了排序
代码:
void QuickSort(int* a, int begin, int end)
{
//递归
if (begin >= end)
{
return;
}
int keyi = PartSort1(a,begin,end);
QuickSort(a, begin, keyi - 1);
QuickSort(a, keyi + 1, end);
}
非递归法:
递归算法虽然书写简单,但由于每次都要调用函数栈帧,若递归层数太多,性能会大幅度降低,因此我们要来书写非递归算法、
要将递归改造为非递归,我们首先要搞清楚,原来的递归算法是依靠左右两个边界来界定要排序的范围的,现在我们依然需要这两个边界,因此我们选择借用栈来保存边界数据,若对栈的知识仍有疑惑,请参考我的这篇文章,里面对栈有较详细的讲解:http://t.csdn.cn/Qdm74
void QucikSortNONR(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);
int keyi = PartSort1(a, left, right);
//成功划分出区间 begin~keyi-1,keyi,keyi+1~end
if (left < keyi-1)
{
StackPush(&st, left);
StackPush(&st, keyi - 1);
}
if (keyi+1 < right)
{
StackPush(&st, keyi + 1);
StackPush(&st, right);
}
}
StackDestroy(&st);
}
时间复杂度:
关于快速排序的时间复杂度,在比较好的情况下,快速排序的递归像是一棵二叉树,时间复杂度约为O(n*logn),但在较差的情况下,树向一段严重倾斜,这样第一次需要遍历n此次,第二次遍历n-1次......依此类推时间复杂度约为O(n^2)
优化:为了防止数据比较极端的情况,我们要采取两种方式对快速排序优化
1.三数取中:
int getmidindex(int* a,int begin,int end)
{
int mid = (begin + end) / 2;
if (a[begin] < a[mid])
{
if (a[mid] < a[end])
{
return mid;
}
else if (a[end] < a[begin])
{
return begin;
}
else
{
return end;
}
}
else // a[mid] < a[begin]
{
if (a[begin] < a[end])
{
return begin;
}
else if (a[end] < a[mid])
{
return mid;
}
else
{
return end;
}
}
}
2.随机取key:
在三数取中的逻辑中,我们也没有毕业一定选择中间的值,可以进行随机优化
int getmidindex(int* a,int begin,int end)
{
//int mid = (begin + end) / 2;
int mid = begin + rand() % (end - begin);
if (a[begin] < a[mid])
{
if (a[mid] < a[end])
{
return mid;
}
else if (a[end] < a[begin])
{
return begin;
}
else
{
return end;
}
}
else // a[mid] < a[begin]
{
if (a[begin] < a[end])
{
return begin;
}
else if (a[end] < a[mid])
{
return mid;
}
else
{
return end;
}
}
}
有关快速排序的实现和细节以及时间复杂度就跟大家分享完啦,认真看完的你一定有所收获,如果对你有所帮助,请给博主点赞收藏关注,这是我分享知识的最大动力,我们下次再见!