荷兰国旗是由红白蓝3种颜色的条纹拼接而成,如下图所示:
假设这样的条纹有多条,且各种颜色的数量不一,并且随机组成了一个新的图形,新的图形可能如下图所示:
要求:把这些条纹按照颜色排好,红色的在上半部分,白色的在中间部分,蓝色的在下半部分。
我们把这类问题称作荷兰国旗问题。
本题就是荷兰国旗问题。
下面是解题思路:
设划分依据的值为pivot(具体到本题就是1),
设数组长度为length,
用几个变量划分数组,i、j和k,
- [0, i) 左闭右开。该索引区间的元素小于pivot。
- [i, j) 左闭右开。该索引区间的元素等于pivot。
- [j, k] 左闭右闭。该索引区间的元素是未处理的。
- [k+1, length) 左闭右开。该索引区间的元素大于pivot。
先给出伪代码以描述解法的原理。解法叫做 “三向切分” (a three-way partitioning)。
procedure three-way-partition(A : array of values, mid : value):
i ← 0
j ← 0
k ← size of A - 1
while j <= k:
if A[j] < mid:
swap A[i] and A[j]
i ← i + 1
j ← j + 1
else if A[j] > mid:
swap A[j] and A[k]
k ← k - 1
else:
j ← j + 1
翻译成golang代码(已提交通过),如下
func sortColors(nums []int) {
l := len(nums)
if l < 2 {
return
}
var (
i int
j int
k = l-1
m = 1
)
// 请结合i、j和k划分出的区间的含义来理解循环体中各个操作的目的
for j <= k {
n := nums[j]
if n < m {
// 小于m的往数组左端放
//nums[i], nums[j] = n, nums[i]
nums[j] = nums[i]
nums[i] = n
i++
j++
} else if n > m {
// 大于m的往数组右端放
if nums[k] <= m {
//nums[k], nums[j] = n, nums[k]
nums[j] = nums[k]
nums[k] = n
}
k--
} else {
// 等于m的暂时不动
j++
}
}
}
下面的代码在上述基础上做了少许改进,在访问k索引附近的元素时,尽量多“逗留”一会(多做一些操作)。j 可以看作是靠近数组左端,k 可以看作是靠近数组右端,所以循环体中在两个索引之间切换,就相当于在数组两端的数据间切换。切换到某一端后,尽量多做一些操作,所以该解法考虑到了局部性原理。
什么是局部性原理 ?
越接近cpu的存储器速度越快
- 最高层的L0寄存器,cpu可以在1个时钟周期内访问它们
- 访问L1高速缓存,cpu大约需需要4个时钟周期
- 访问L2高速缓存,cpu大约需需要10个时钟周期
- 访问L3高速缓存,cpu大约需需要50个时钟周期
- 访问主存,cpu大约需要200个时钟周期
可见,cpu访问高速缓存比访问内存快很多。所以代码中要善于利用高速缓存提升程序的性能。
局部性原理包括两个部分
- 时间局部性:被访问过一次的地址很可能在不远的将来再被访问。
- 空间局部性:某个地址被访问,它附近的地址很可能也要被访问。
从上述内容可知,使用数组更容易利用到局部性原理以提升程序性能。
请看代码(已提交通过)
func sortColors(nums []int) {
l := len(nums)
if l < 2 {
return
}
var (
i int
k = l-1
m = 1
)
for j := 0; j <= k; j++ {
n := nums[j]
if n < m {
//nums[i], nums[j] = n, nums[i]
nums[j] = nums[i]
nums[i] = n
i++
} else if n > m {
for j < k && nums[k] > m { // 避免不必要的交换。(这处循环同时利用了局部性原理)
k--
}
//nums[k], nums[j] = n, nums[k]
nums[j] = nums[k]
nums[k] = n
k--
// 处理边界情况,
// 此处可能 j == k,下一步就会j++,使得 j > k,循环就退出了。
// 所以此时必须先处理刚刚赋值到j索引上的数,即原k索引上的数。
// 它小于或等于m,要处理小于m的情况。
if nums[j] < m {
nums[i], nums[j] = nums[j], nums[i]
i++
}
}
}
}