从两种框架,三种思想来展开对快速排序的叙述
两种框架:递归与非递归
三种思想:左右交换法、挖坑法、前后指针法
但三种思想整体来说就是要找一个key值,把比key小的放在key左边,把比key大的放在key右边,然后再从[0,key-1][key+1,end]这两个区间排序,一直递归或者迭代,直到要排序的节点只有一个的时候,即为有序。
目录
一.递归
0.递归框架
递归框架可以类比成二叉树中的先序,先整体排一遍之后,递归左,再递归右... ...
//递归
void QuickSort(int* arr, int begin, int end)
{
//只有一个节点
if (begin >= end)
{
return;
}
int left = begin, right = end;
//三种方法
int key = //PartSort1(arr, left, right);//左右交换法
//PartSort2(arr, left, right);//挖坑法
PartSort3(arr, left, right);//前后指针法
//[begin,key-1]
QuickSort(arr, begin, key - 1);
//[key+1,end]
QuickSort(arr, key + 1, end);
}
1.左右交换法
思想:默认选最左侧的一个数为key,先从右向左找比key小的,再从左向右找比key大的,交换两数。循环至左右指向同一个数时停止,这时将最左侧的key值与左右指针指向的数交换。
注意:这个思想有三处容易错的地方
1.为了保证最后一次交换一定是要比key小的值,比key值小才可以交换到key值左边,所以要先从右找比key小的。
2.不管是从右找小还是从左找大一定要在循环中加上left<right条件,否则可能发生越界
3.在从右找小或者从左找大的过程中,等于key的情况不能交换,否则可能发生死循环
int PartSort1(int* arr, int left, int right)
{
int keyi = left;
while (left < right)
{
//从右找小
while (left < right && arr[right] >= arr[keyi])
{
right--;
}
//从左找大
while (left < right && arr[left] <= arr[keyi])
{
left++;
}
//交换
Swap(&arr[right], &arr[left]);
}
//循环结束后,交换最后一次,交换最左侧的key值与左右指针同时指向的值
Swap(&arr[left], &arr[keyi]);
keyi = left;
return keyi;
}
2.挖坑法
思想:把最左边的值看作坑,把值用pit保存起来。从右开始找比pit小的数,找到后把此位置的值放到坑中,把此位置看作是新的坑;从左开始找比pit大的数,找到后把此位置的值放到坑中,把此位置看做是新的坑。循环结束后,左右指针指向同一个位置,此位置必然是坑,把一开始最左边的数pit放到坑中。
int PartSort2(int* arr, int left, int right)
{
int pit = arr[left], piti = left;
while (left < right)
{
//从右找小
while (left < right && arr[right] >= pit)
{
right--;
}
//放到坑中
arr[piti] = arr[right];
//把自己变成新的坑
piti = right;
//从左找大
while (left < right && arr[left] <= pit)
{
left++;
}
//放到坑中
arr[piti] = arr[left];
//把自己变成新的坑
piti = left;
}
arr[piti] = pit;
return piti;
}
3.前后指针法
思想:定义两个指针,prev指向最左边,cur = prev+1;从cur位置出发,开始寻找比最左边的key值小的数,找到了就先++prev,如果prev!=cur再交换prev与cur位置的值,不管找没找到,都++cur,将数组每个数挨个遍历,最后再交换最左边的key值与prev。
int PartSort3(int* arr, int left, int end)
{
int prev = left, cur = prev + 1;
int keyi = left;
while (cur <= end)
{
if (arr[cur] < arr[keyi] && ++prev != cur)
{
Swap(&arr[prev], &arr[cur]);
}
cur++;
}
Swap(&arr[keyi], &arr[prev]);
return prev;
}
二.非递归
0.非递归框架
使用栈数据结构,来模拟二叉树的先序递归形式。
思想:(以栈数据结构举例,如果这里使用队列也可以,但是队列更像二叉树的层序遍历)
将要排序的空间首位成对入栈,需要排序时再成对出栈,每排完一次,如果符合条件,就将区间首位再次压入栈中,直到迭代至栈为空。
void QuickSort(int* arr, int begin, int end)
{
Stack sk;
StackInit(&sk);
StackPush(&sk, end);
StackPush(&sk, begin);
while (!StackEmpty(&sk))
{
int left = StackTop(&sk);
StackPop(&sk);
int right = StackTop(&sk);
StackPop(&sk);
//三种方法
int key = //PartSort1(arr, left, right);//左右交换法
//PartSort2(arr, left, right);//挖坑法
PartSort3(arr, left, right);//前后指针法
if (key < right)
{
StackPush(&sk, right);
StackPush(&sk, key + 1);
}
if (left < key)
{
StackPush(&sk, key - 1);
StackPush(&sk, left);
}
}
}
三.时间复杂度/优化(避免最坏情况)/小区间优化/稳定性
1.时间复杂度
这是一张快速排序的逻辑图
时间复杂度:O(N*logN)
空间复杂度:O(logN)
讨论最坏情况:从此图可以看出每次排序好之后,接近二分,但如果是完全有序或者完全逆序
左右的情况就不再是n/2,拿完全有序来说,就是一个左1且右n-1的情况。这样排序下去就会有n层。
最坏情况:一组数据完全有序或者完全逆序,快排的时间复杂度是O(N^2)
2.优化(避免最坏情况)
为了尽可能的避免这种最坏情况的发生,在选key的时候,就不能默认从最左开始选了。
方法:可以采用三数取中,就是从最左,最右,中间,三个数中选出一个不大也不小的数当做key
把这个不大也不小的数的下标记录下来,与最左侧的数交换一下值。这样上面三种方法的代码不需
要很大的改动,仍然选取最左侧为key值,只不过我们把最左侧的这个数换为了一个最合理的数。
int midi = GetMidIndex(arr, left, right);
Swap(&arr[midi], &arr[left]);
int GetMidIndex(int* arr, int begin, int end)
{
int mid = begin + (end - begin) / 2;
if (arr[begin] > arr[end])
{
if (arr[mid] > arr[begin])
return begin;
else if (arr[end] > arr[mid])
return end;
else
return mid;
}
else //arr[begin] < arr[end]
{
if (arr[mid] > arr[end])
return end;
else if (arr[begin] > arr[mid])
return begin;
else
return mid;
}
}
3.小区间优化
仍然使用这张图来解释一下,小区间优化问题。
我们解决了快排的最坏情况之后,快排时间复杂度为O(N*logN)
但是仔细思考一下,可不可以再进一步优化呢?让效率更优一点
递归的最大缺点就是有一个很强的限制性因素,当数据量太大时,递归很容易栈溢出,从而导致程序崩溃!
既然快排所使用的递归是一个类似于二叉树的前序遍历,那么我们从二叉树的角度出发来看一下!
例如:一个n层的二叉树,总结点个数是2^n-1,最后一层的节点个数是2^(n-1)
经过数学计算,2^n-1看作2^n,2^n = 2^(n-1) * 2,最后一层的节点个数就占了总结点个数的一半
当递归划分小区间,区间比较小的时候,就不再递归划分去排序这个小区间。可以考虑直接用其他排序对小区间的处理,比如使用插入排序,假设小区间小于10时,不在递归排序小区间。将会减少至少50%以上的递归次数
总体来说,小区间优化主要是来解决大量数据时递归的栈溢出问题,以及多次开辟栈帧的资源消耗问题。
4.稳定性
先说下结论:快排不稳定。
如果有一组数据中相同的元素在排序之后,更改他们的相对顺序,就认为这个排序不稳定。
在日常生活中,某些特定场合,使用的排序的稳定性及其重要。比如说两位同学分数相同,但是一个先交卷,一个后交卷,按照常理来说,先交卷的同学应该排在后交卷的同学之前,但如果经过排序后,先交卷的同学排到了后交卷同学之后,这就非常不合理了。
原因:
1.比如说有这样一组数据:4,5(1),5(2),5(3),8。
在三数取中时中间的5就会和4交换位置:5(2),5(1),4,5(3),8
2.或者这样一组数据:4,7(1),7(2),2,3,5
在第一次排序结束之后(左右交换法):2,3,4,7(2),7(1)