leetcode 75. 颜色分类 —— 荷兰国旗问题(Dutch national flag problem)

1,063 阅读2分钟

荷兰国旗是由红白蓝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):
    i0
    j0
    ksize of A - 1

    while j <= k:
        if A[j] < mid:
            swap A[i] and A[j]
            ii + 1
            jj + 1
        else if A[j] > mid:
            swap A[j] and A[k]
            kk - 1
        else:
            jj + 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++
            }
        }
    }
}