快速排序
快速排序是Hoare于1962年提出的一种二叉树结构的交换排序方法。
基本思想
其基本思想为:任取待排序元素序列中的某元素作为基准值,按照该排序码将待排序集合分割成两子序列,左子序列中所有元素均小于基准值,右子序列中所有元素均大于基准值,然后最左右子序列重复该过程,直到所有元素都排列在相应位置上为止。
单趟基本思想(选取分界线位置keyi):
- 任取待排序元素序列中的某元素(最左边/最右边的数)作为基准值a[keyi]
- 按照该排序码将待排序集合分割成两子序列
- 左子序列中所有元素均小于基准值
- 右子序列中所有元素均大于基准值
整体基本思想:
- 然后最左右子序列重复该过程,直到所有元素都排列在相应位置上为止。
- 核心是区间
单趟基本思路有3中方式去实现:
- hoare版本
- 挖坑版本
- 前后指针版
整体思路 hoare版本
单趟
留下疑问:为什么相遇位置一定比key小?
多趟
左边有序+右边有序=整体有序
把它看作二叉树,递归。
代码实现
单趟
void QuickSort(int* a, int n)
{
int left = 1, right = n - 1;
int keyi = 0;
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]);
}
易错点
我们可能会写成下面这样的代码。
void QuickSort(int* a, int n)
{
int left = 0, right = n - 1;
int key=a[left];
while (left < right)
{
// 右边找小
while (a[right] > key)
{
--right;
}
// 左边找大
while (a[left] < key)
{
++left;
}
Swap(&a[left], &a[right]);
}
Swap(&a[left], &key);
}
【1】临界状态:left的值不变。
只有当a[left] < key的时候,left才会++,而left最开始是0,它的值等于key,所以left会一直不变。
- 解决方法:将left的值改为1,并且key=a[0]。
【2】相遇判断有问题。
while (a[right] > key) while (a[left] < key)
这里只能保证找到符合条件的值然后++left或者--right,并不能保证什么时候停下。而我们停下的条件是两者相遇,所以要在两个while里都加上一个条件。
- 解决办法:while (left < right && a[right] >= a[keyi]) while (left < right && a[left] <= a[keyi])
【3】与key交换出错。
Swap(&a[left], &key);
key是局部变量,最后交换的是与局部变量交换的。而我们希望的是与key存储的位置进行交换,也就是数组里的那个位置。
- 解决办法: int keyi = 0;Swap(&a[left], &a[keyi]);
多趟
void QuickSort(int* a, int begin, int end)
{
if (begin >= end)
return;
int left = begin, right = end;
int keyi = begin;
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;
// [begin, keyi-1] keyi [keyi+1, end]
QuickSort(a, begin, keyi - 1);
QuickSort(a, keyi + 1, end);
}
分为 [begin, keyi-1] keyi [keyi+1, end] ,两边分别递归下去,最后会排成有序。
提问:if (begin >= end)代表的是只有一个值或者不存在该区间的情况。一个值很好理解,那么为什么会有不存在的情况呢?
例如我们这里的4 5,4的下标是3,5的下标是4,那么接下来keyi就是4,[keyi+1, end]这个区间就是不存在的。
易错点
【1】写成 int left = begin+1;这样就会导致有序的情况下出错。
比如 4 5 6 7 ,left指向5,right指向7,begin和keyi指向4,right一直找不到小,直到找到5,left==right,跳出循环,此时4和5交换Swap(&a[left], &a[keyi]); 就会导致出错。
- 解决办法:直接改为int left = begin;
【2】写成while (left < right && a[right] > a[keyi]) while (left < right && a[left] <a[keyi])死循环
比如一组数据是: 2 1 2 6 8 2 这样keyi指向2,而a[right] ==2永远也不可能>2,那么right就会一直不动,当left来到2的时候,a[left] <a[keyi]不再满足,所以left也停下了,然后就是交换2和2,接下来程序继续,一直都会卡在这里,死循环。
- 解决办法: 加上等号判断
- while (left < right && a[right] >= a[keyi]) while (left < right && a[left] <= a[keyi])
性能分析
最理想的状态就是,每一次找都可以二分。时间复杂度是O(N*logN)。
- 当数据有序时,快排就会很吃力,这是为什么呢?
因为有序时,left要找小,right要找大,都找不到,就会一直从头找到尾,时间复杂度就会上升到O(N^2)。
解决:
【1】随机值选key
【2】三数取中,选不是最大也不是最小的那个数做key(与begin交换)。改成以下代码:
int GetMidi(int* a, int begin, int end)
{
int midi = (begin + end) / 2;
// begin end midi三个数选中位数
if (a[begin] < a[midi])
{
if (a[midi] < a[end])
return midi;
else if (a[begin] > a[end])
return begin;
else
return end;
}
else
{
if (a[midi] > a[end])
return midi;
else if (a[begin] < a[end])
return begin;
else
return end;
}
}
void QuickSort(int* a, int begin, int end)
{
if (begin >= end)
return;
int midi = GetMidi(a, begin, end);
Swap(&a[midi], &a[begin]);
int left = begin, right = end;
int keyi = begin;
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;
// [begin, keyi-1] keyi [keyi+1, end]
QuickSort(a, begin, keyi - 1);
QuickSort(a, keyi+1, end);
}
问题解决
上面我们留下疑问:为什么相遇位置一定比key小? (不考虑相遇位置是key)
因为右边先走!
相遇有两种情况,要不就是右边遇到左边,要不就是左边遇到右边
- 右遇左:3 8 4 9 2 6 右边找小到2,左边找大找到8,交换后继续找,右边找不到小了,直接找到left处,二者相遇,此地时左边刚才找到的,且已经与2交换,所以此时这个位置一定比3小。
- 左遇右: 5 3 2 4 7 右边找小找到4,左边找不到大,二者相遇。相遇位置一定比key小。
注意:我们这一篇写的是最左边作key,让right先走。相反的,如果让右边作key,是right先走。