快速排序
快速排序类似合并排序,都会对目标集合进行拆分,然后分别处理,执行时间都为, 快速排序的空间复杂度较低为
.
关键的子问题根据pivot点进行拆分
找到一个中心点,把小于中心点的值放到左边,大于中心点的值放到右边
{ ...小于m m ...大于m}
- 分析过程
1.假设有一个数组[11,88,33,99,44,55,77], 我们选定的pivot点为77
2.抽象出一个低位lo,高位hi,假设中心点位置区间[i, j).
3.划分区域为 [lo, i) --- 小于中心集合 [i, j) --- 未确认的集合 [j,hi-1) --- 大于中心点集合
[11, 88, 33, 99, 44, 55, 77] | lo=0, i=0, j=6, hi=7 ; 11<77 ; 指针移动 i++ |
[11, 88, 33, 99, 44, 55, 77] | lo=0, i=1, j=6, hi=7 ; 88>77 ; 指针移动 --j 并交换位置 |
[11, 55, 33, 99, 44, 88, 77] | lo=0, i=1, j=5, hi=7 ; 55<77 ; 指针移动 i++ |
[11, 55, 33, 99, 44, 88, 77] | lo=0, i=2, j=5, hi=7 ; 33<77 ; 指针移动 i++ |
[11, 55, 33, 99, 44, 88, 77] | lo=0, i=3, j=5, hi=7 ; 99<77 ; 指针移动 --j 并交换位置 |
[11, 55, 33, 44, 99, 88, 77] | lo=0, i=3, j=4, hi=7 ; 44<77 ; 指针移动 i++ |
[11, 55, 33, 44, 99, 88, 77] | lo=0, i=4, j=4, hi=7 ; 此时i=j,表示没有需要处理的元素; 此时需要把中心点移动到中间位置 |
[11, 55, 33, 44, 77, 88, 99] | lo=0, i=4, j=4, hi=7 ; 与j交换位置后 ,完成目标规则集合{ ...小于m m ...大于m} |
总结规律,寻找循环不变式,指针在未处理位置[i, j)移动, 当指针值小于pivot,指针后移, 当指针值大于pivot, 当前值与--后的j值交换位置(左闭右开区间), 依次运行直至i=j即没有需要处理的值,这时候把pivot值与当前j值交换.
- 代码实现
//定义交换方法
function swap(A, l, r){
[A[l],A[r]] = [A[r],A[l]] //图省事可这样写,算法中不建议,没有三步交换快
}
function partition(A, lo, hi){
let pivot = A[hi-1], i=lo, j=hi-1;
// 小于中心点 [lo, i)
// 未处理元素 [i, j)
// 大于中心点 [j, hi-1)
while(i != j){
if(A[i]<pivot){
i++
}else{
swap(A, i, --j)
}
}
// 交换中心点值 中心点位置 [hi-1]
swap(A, j, hi-1)
return j
}
完整快速排序
上述子问题是最关键的步骤, 但是我们发现我们只是根据某种规则找到一个pivot值,并把目标集合处理成{ ...小于m m ...大于m}格式,问题显而易见, 小于和大于中心值的集合并未进行排序
- 猜想如何使其排序
假设数组只有三个值[1, 3, 2] 我们确定pivot值为2, 经过partition函数后为[1, 2, 3], 此情况下已经完成排序
假设数组只有两个值[ 3, 2] 我们确定pivot值为2, 经过partition函数后为[ 2, 3], 此情况下已经完成排序
我们根据最小粒度可以验证数组只有两个元素的时候必定可以完成排序,想到分治.
- 通过图表分析
[11, 88, 33, 99, 44, 55, 77] |
[11, 55, 33, 44, 77, 88, 99] |
[11, 55, 33, 44] 77 [88, 99] |
[11, 33, 44, 55] [88] 99 |
[11, 33] 44 55 88 |
[11] 33 |
[11, 33, 44, 55, 77, 88, 99] |
根据某种规则找到的中心点partition后, 把中心点左右两侧集合分别进行 partition, 当不能在拆分是已经排好顺序.
类似合并排序,快速排序是在拆分过程中就已经进行了排序,归并排序是先拆分,在merge过程中进行排序.
- 程序设计
function swap(A, l, r){
[A[l], A[r]] = [A[r], A[l]]
}
function partition(A, lo, hi){
const pivot = A[hi-1];
let i = lo, j = hi-1;
while(i != j){
A[i] < pivot? i++ : swap(A, i, --j)
}
swap(A, j, hi-1)
return j
}
function qsort(A, lo=0, hi=A.length){
if (hi-lo < 2) return //如果小于2表示有0或者1个元素不用进行排序
const p = partition(A, lo, hi); //寻找中心点位置
qsort(A, lo, p) //左边排序
qsort(A, p+1, hi) //右边排序 区间都是左闭右开
}
- 快速排序优化
由上图表可以简单看出,如果拆分更加平均,拆分树高度会变短,用时更少
所以找到pivot的某种规则就略显重要,上例我们的某种规则是集合最后一位,并不能做到相对的平均
1, 随机打乱原数组 O(n)
2. 使用中位数作为pivot O(n)
3. 找三个数,取中间值作为pivot O(1)