深入理解代替单纯记忆
UPDATE: 2020年05月28日,再次学习快排,发现本文中的分区写法容易出错,边界情况尤为如此,同时不太方便理解。更容易理解的我认为左边两个指针,同时向后移动的写法,具体参考快排Partition笔记
无误的写出快排还是有点难度,主要是一些细节点容易搞不清楚,需要反复思考和练习
本文不会完整讲解快排实现,网上有太多了。本文会针对在实现算法时几个易错点分析。使用语言为Swift
两种Partition实现思路
partition是快排的核心,即分区。我目前知道两种实现思路
-
头尾各一个指针i、j,i = start; j = end
- 选array[0]作为pivot
- i从左到右扫描,j从右到左扫描
- i定位到比pivot大的位置,j定位到比pivot小的位置,i和j的值交换。重复该过程,直到i和j重合
- 最后将array[0](pivot的位置)和i位置的值交换,让pivot到合适的位置
-
也是两个指针,但只需要从左到有扫描。具体做法参考> 快排Partition笔记
本文后面章节,仅针对第1种算法实现进行讨论
头尾指针Partition分析
测试数据是
[13, 63, 1, 21, 20, 2, 37, 46, 9, 13]
先给出正确的算法实现
func partition(_ array: inout [Int], start: Int, end: Int) -> Int {
if start < 0 || end < 0 { return -1 }
if start > end { return -1 }
if start == end { return start }
let pivot = array[start]
var i = start
var j = end
while i != j {
// i < j是防止j越界
while i < j && array[j] > pivot {
j -= 1
}
// i < j是防止i越界
while i < j && array[i] <= pivot {
i += 1
}
if i < j {
let tmp = array[i]
array[i] = array[j]
array[j] = tmp
}
}
array[start] = array[i]
array[i] = pivot
return i
}
输出结果:[2, 13, 1, 9, 13, 20, 37, 46, 21, 63], 分割点的下标是4
扫描是先左到右还是先右到左?
因为最终i和j会重合,若先左到右扫描
- 只要pivot不是数组中的最小值(如果是最小值是不会出错的),则i最终停下的位置的值是大于pivot的(大家仔细想想)
- 那此时如果最终把i和pivot交换,会出现错误。显然i的值大于pivot,交换后i的值就跑到start位置了
- 怎么办?选择最后一个元素end作为pivot就可以了,因为最后一个元素肯定要大于pivot,所以i和end交换没毛病
- 反过来,若先右到左也是类似问题,i最终到达的是小于pivot的位置
总结一下,
- 若想选择第一个元素作为pivot,那一定要先从右到左扫描
- 若想先从左到右扫描,那不能选择第一个元素作为pivot,可以选择最后一个元素
array[i] <= pivot和array[j] > pivot的组合该怎么写?
当array[i] < pivot而非<=时,与pivot相等的元素便会被交换到右半部分。所以此时要求必须array[j] >= pivot,否则本来在右半部分和pivot相等的值会被错误地交换到左半部分。
本例中会导致无限循环,第一个13和最后一个13会无限交换
那上面的例子可以写成array[i] < pivot和arrauy[j] >= pivot的组合吗?-----不行!
因为i是从start开始,即i一开始是指向pivot的,如果是上面的组合,当从左到右扫描时,pivot可能会被错误地交换到右半部分。对,在循环过程中,pivot不能被移动,否则最后一步就无法将pivot移动到合适的位置了。看下错误的结果
[1, 2, 13, 21, 20, 63, 37, 46, 13, 13]
如何解决?
- 我们可以让i从start+1开始,避开pivot被交换的情况
- 也可以使用
array[i] <= pivot和array[j] > pivot的组合
那么array[i] <= pivot和array[j] >= pivot行吗?----不行!
这意味着两边扫描时,遇到与pivot相等的都略过不交换,来看下错误结果
2, 9, 1, 13, 20, 21, 37, 46, 63, 13]
总结一下,
- 有且仅有一个
<=或>= - pivot在循环过程中不能被移动,所以从pivot的这边开始扫描的代码应该使用
<=或>=
--未完待续