早已听过快排的大名,今天去学了快排的两种实现方式,简单记一下笔记
主算法
快排和归并排序均为分而治之,快排也是将原序列划分为两个规模更小的子序列,然后递归地进行排序,但与归并排序不同的是,快排在划分子序列的时候要求:前一序列中的任何元素都不得超过后一序列中的各个元素。因此,在对前一子序列和后一子序列进行排序之后,只需简单将二者串接起来,原序列自然有序。
平凡解(递归基):只剩单个元素时,本身就是有序的解。
因此mergesort的计算量和难点在于合,而quicksort在于分
轴点
引入轴点(pivor) 的概念:左/右侧的元素,均不比它更大/小
图摘自清华大学数据结构慕课电子讲义
以轴点为界,原序列的划分自然实现:
[
l
o
,
h
i
)
=
[
l
o
,
m
i
)
+
[
m
i
]
+
(
m
i
,
h
i
)
[lo, hi)=[lo, mi)+[mi]+(mi, hi)
[lo,hi)=[lo,mi)+[mi]+(mi,hi)
注意,每次将mi找到后,均不再将其划分到前缀或后缀中去,而是将其位置固定住。
void quicksort(int lo, int hi)
{
if (hi - lo < 2) //单元素区间自然有序
return;
int mi = partition(lo, hi); //先构造轴点,再
quicksort(lo, mi); //[lo, mi)前缀排序
quicksort(mi+1, hi); //[mi+1, hi)后缀排序
//注意轴点找出便不再去动它,因此这里两个递归都不包含已找出的轴点mi
}
在有序序列中,所有元素皆为轴点,反之亦然
因此,快速排序就是将所有元素逐个转换为轴点的过程
快速划分:LUG版
算法流程
- 任取一个元素作为候选轴点,将其与首元素互换位置,并记录下该轴点的值。记录之后,lo可在逻辑上看做空闲状态
- 从hi-1开始,逆序依次与轴点比较,若不小于轴点,则令hi–,即将该元素归入G中。直到末元素hi不满足此条件,将该元素移至空闲单元lo中(因为此时lo在逻辑上被看做空闲状态,因此可直接赋值,而不用调用swap),赋值完毕后单元hi变为空闲。转入步骤3
- 若首元素lo不大于轴点,则令lo++,即将该元素归入子序列L中。直到首元素lo不满足此条件,同样将该元素的值存入单元hi中,此时单元lo变为空闲。转入步骤2
- 通过在外层叠加一层循环while(lo<hi),反复执行2、3两步骤。直到lo=hi时,退出循环,此时只剩下空闲单元lo,将记录好的轴点数值放入单元lo中,并返回轴点的秩lo
在以上过程当中,始终保持如下不变性:
L
≤
p
i
v
o
t
≤
G
L\leq pivot\leq G
L≤pivot≤G;
U
=
[
l
o
,
h
i
)
U=[lo, hi)
U=[lo,hi)中,单元
[
l
o
]
[lo]
[lo]和
[
h
i
]
[hi]
[hi]交替空闲
int partition1(int lo, int hi) //[lo, hi)
{
swap(nums[lo], nums[lo + rand() % (hi - lo)]);
int pivot = nums[lo];
hi--;
while (lo < hi)
{
while ((lo < hi) && (pivot <= nums[hi]))
hi--;
nums[lo] = nums[hi]; //由于这种版本当前将lo位置视为逻辑上的空闲位置,所以才能直接复制;其他算法需用swap交换二者
while ((lo < hi) && (nums[lo] <= pivot))
lo++;
nums[hi] = nums[lo]; //由于这种版本当前将hi位置视为逻辑上的空闲位置,所以才能直接复制;其他算法需用swap交换二者
}
nums[lo] = pivot;
return lo;
}
性能分析
算法不稳定(unstable),就地算法(in-place algorithm)
- 空间:O(1) 附加空间
- 时间:单次partition算法为O(n),而总体上:
-最好:每次划分都(接近)平均,轴点总是(接近)中央: T ( n ) = 2 T ( ( n − 1 ) / 2 ) + O ( n ) = O ( n l o g n ) T(n)=2T((n-1)/2)+O(n)=O(nlogn) T(n)=2T((n−1)/2)+O(n)=O(nlogn)
最坏:每次划分都极不均衡(比如轴点总是最小/最大元素): T ( n ) = T ( n − 1 ) + T ( 0 ) + O ( n ) = O ( n 2 ) T(n)=T(n-1)+T(0)+O(n)=O(n^2) T(n)=T(n−1)+T(0)+O(n)=O(n2)
平均: O ( n l o g n ) O(nlogn) O(nlogn)
为降低最坏情况出现的概率,每次采用随机选取轴点的方法;或每次选取三个元素,选择数值居中的元素(三者取中)。但这些策略也只能降低最坏情况的概率,而无法杜绝
快速划分:LGU版
算法流程
将整个区间划分为四个区间:
S
=
[
l
o
,
h
i
)
=
[
l
o
]
+
(
l
o
,
m
i
]
+
(
m
i
,
k
)
+
[
k
,
h
i
)
=
p
i
v
o
t
+
L
+
G
+
U
S=[lo, hi)=[lo]+(lo, mi]+(mi, k)+[k, hi)=pivot+L+G+U
S=[lo,hi)=[lo]+(lo,mi]+(mi,k)+[k,hi)=pivot+L+G+U
L
<
p
i
v
o
t
<
G
L<pivot<G
L<pivot<G
图摘自清华大学数据结构慕课电子讲义
- 任取一个元素作为候选轴点,将其与首元素互换位置,并记录下该轴点的值。初始令mi=lo
- k从lo+1开始, 从左向右考察每一个[k]:若[k]严格小于轴点,则将其内元素与[mi]内元素交换(注意此处是交换swap,不能像上一个版本那样直接赋值!!!),即L向右拓展,然后k++;否则直接将k++,即G向右拓展
- 线性扫描完成后,区间划分为L和G两个区间,此时将位于区间起始位置lo处的轴点与L区间末尾元素[mi]交换(swap[]),使轴点归位。返回轴点的秩mi
性能分析
就地算法,依旧不稳定
- 空间:O(1) 附加空间
- 时间:单次partition算法为O(n),而总体上
-最好:每次划分都(接近)平均,轴点总是(接近)中央: T ( n ) = 2 T ( ( n − 1 ) / 2 ) + O ( n ) = O ( n l o g n ) T(n)=2T((n-1)/2)+O(n)=O(nlogn) T(n)=2T((n−1)/2)+O(n)=O(nlogn)
-最坏:每次划分都极不均衡(比如轴点总是最小/最大元素): T ( n ) = T ( n − 1 ) + T ( 0 ) + O ( n ) = O ( n 2 ) T(n)=T(n-1)+T(0)+O(n)=O(n^2) T(n)=T(n−1)+T(0)+O(n)=O(n2)
-平均:O(nlogn)