Go 贪心算法与二分查找 | 青训营笔记

62 阅读1分钟

这是我参与「第五届青训营 」伴学笔记创作活动的第 10 天

最长递增子序列

给你一个整数数组 nums ,找到其中最长严格递增子序列的长度。

子序列是由数组派生而来的序列,删除(或不删除)数组中的元素而不改变其余元素的顺序。例如,[3,6,2,7] 是数组 [0,3,1,6,2,2,7] 的子序列。

示例 1:

输入:nums = [10,9,2,5,3,7,101,18]
输出:4
解释:最长递增子序列是 [2,3,7,101],因此长度为 4 。

1. 解法一:动态规划

设以 nums[x] 为最后一个元素的最长递增子序列的长度为 len(x)
则当 nums[j] < nums[i] 时,有 len(i) = Max(len(j)) + 1,其中 (0 <= j < i)
若找不到一个 j (0 <= j < i),使 nums[j] < nums[i] 成立,则 len(i) = 1

例如:
nums = [10,9,2,5,3,7,101,18]
len(0) = 1,[10]
len(1) = 1,[9]
len(2) = 1,[2]
len(3) = len(2)+1 = 2,[2,5]
len(4) = len(2)+1 = 2,[2,3]
len(5) = len(4)+1 = 3,[2,3,7] 或 len(3)+1 = 3,[2,5,7]
len(6) = len(5)+1 = 4,[2,3,7,101] 或 [2,5,7,101]
len(7) = len(5)+1 = 4,[2,3,7,18] 或 [2,5,7,18]

则整个数组的最长递增子序列的长度是 L = Max(len(i)),其中 (0 <= i < n)

由上述讨论,有以下代码:

public int lengthOfLIS(int[] nums) {
    // 整个数组的最长递增子序列的长度
    int L = 0;
    // 以 nums[x] 为最后一个元素的最长递增子序列的长度
    int[] len = new int[nums.length];

    for (int i = 0 ; i < nums.length; i++) {
        len[i] = 1;
        for (int j = 0; j < i; j++) {
            if (nums[j] < nums[i]) {
                if (len[i] < len[j] + 1) {
                    len[i] = len[j] + 1;
                }
            }
        }
        // 算完了 len[i]

        if (L < len[i]) {
            L = len[i];
        }
    }

    return L;
}

时间复杂度 O(n2)O(n^2)1 + 2 + ... + n,n 为数组 nums 的长度。
空间复杂度 O(n)O(n):需要额外的 len 数组,长度为 n。

2. 解法二:贪心 + 二分查找

2.1. 二分法寻找小于 target 的最右边的数

arr 是一个严格单调递增的有序数组。
binarySearchTheMin 返回小于 target 的最右边的数。
如果 arr 中的所有数都 大于或等于 target , 则返回 -1

func binarySearchTheMin(arr []int, target int) int {
    left := 0
    right := len(arr) - 1

    for left <= right {
        mid := left + (right-left)/2

        if arr[mid] < target {
            left = mid + 1
        } else {
            right = mid - 1
        }
    }

    return right
}

做单元测试:

func TestBinarySearchTheMin(t *testing.T) {
    arr := []int{0, 1, 2, 3, 5, 5, 6}

    testCases := []int{-1, 0, 1, 4, 5, 6, 7}
    expected := []int{-1, -1, 0, 3, 3, 5, 6}
    for i, testCase := range testCases {
        pos := binarySearchTheMin(arr, testCase)

        if expected[i] != pos {
            t.Error("ERROR", pos, expected[i])
            continue
        }

        if pos >= 0 && pos < len(arr) {
            t.Log(pos, arr[pos], testCase)
        } else {
            t.Log(pos, "NaN", testCase)
        }
    }
}

输出如下,其中 target-10 时,数组中所有的值都 不小于 (大于或等于) target 则返回 -1

// === RUN   TestBinarySearchTheMin
//                               pos  arr[pos]  target
//     binary_search_test.go:23: -1     NaN      -1
//     binary_search_test.go:23: -1     NaN       0
//     binary_search_test.go:21:  0     0         1
//     binary_search_test.go:21:  3     3         4
//     binary_search_test.go:21:  3     3         5
//     binary_search_test.go:21:  5     5         6
//     binary_search_test.go:21:  6     6         7
// --- PASS: TestBinarySearchTheMin (0.00s)
// PASS

2.2. 贪心算法

func lengthOfLIS(nums []int) int {
    // 递增子序列
    lis := make([]int, len(nums))
    // 递增子序列 lis 的长度, 即 lis[0 : lis_len-1] 为递增子序列的有效范围
    lis_len := 0

    lis[lis_len] = nums[0]
    lis_len++

    for i := 1; i < len(nums); i++ {
        if nums[i] > lis[lis_len-1] {
            lis[lis_len] = nums[i]
            lis_len++
        } else {
            pos := binarySearchTheMin(lis[0:lis_len], nums[i])
            lis[pos+1] = nums[i]
        }

        //fmt.Println(lis[0:lis_len])
    }

    return lis_len
}

时间复杂度 O(nlogn)O(n log{n})O(n)O(n) 遍历数组,O(logn)O(log{n}) 二分查找。 空间复杂度 O(n)O(n):需要额外的 lis 数组,长度为 n。