Question:
小朋友 A 在和 ta 的小伙伴们玩传信息游戏,游戏规则如下:

1. 有 n 名玩家,所有玩家编号分别为 0 ~ n-1,其中小朋友 A 的编号为 0
2. 每个玩家都有固定的若干个可传信息的其他玩家(也可能没有)。传信息的关系是单向的(比如 A 可以向 B 传信息,但 B 不能向 A 传信息)。
3. 每轮信息必须需要传递给另一个人,且信息可重复经过同一个人

给定总玩家数 n,以及按 [玩家编号,对应可传递玩家编号] 关系组成的二维数组 relation。返回信息从小 A (编号 0 ) 经过 k 轮传递到编号为 n-1 的小伙伴处的方案数;若不能到达,返回 0。

示例 1:
```
输入:n = 5, relation = [[0,2],[2,1],[3,4],[2,3],[1,4],[2,0],[0,4]], k = 3

输出:3

解释:信息从小 A 编号 0 处开始,经 3 轮传递,到达编号 4。

共有 3 种方案,分别是 0->2->0->4, 0->2->1->4, 0->2->3->4。
```
示例 2:
```
输入:n = 3, relation = [[0,2],[2,1]], k = 2

输出:0

解释:信息不能从小 A 处经过 2 轮传递到编号 2
```

限制:
* 2 <= n <= 10
* 1 <= k <= 5
* 1 <= relation.length <= 90, 且 relation[i].length == 2
* 0 <= relation[i][0],relation[i][1] < n 且 relation[i][0] != relation[i][1]

Solution:
### 基本分析

`n` 和 `k` 的数据范围都很小,并且根据题目对 `relation` 的定义可以知道这是一个边权相等的图。

对于边权相等的图,统计有限步数的到达某个节点的方案数,最常见的方式是使用 `BFS` 或 `DFS`。

---

### BFS

一个朴素的做法是使用 `BFS` 进行求解。

起始时,将起点入队,按照常规的 `BFS` 方式进行拓展,直到拓展完第 $k$ 层。

然后统计队列中编号为 `n-1` 的节点的出现次数。

一些细节:为了方便找到某个点 $i$ 所能到达的节点,我们需要先预处理出所有的边。数据量较少,直接使用 `Map` 套 `Set` 即可。

代码:
```Java
class Solution {
    public int numWays(int n, int[][] rs, int k) {
        Map<Integer, Set<Integer>> map = new HashMap<>();
        for (int[] r : rs) {
            int a = r[0], b = r[1];
            Set<Integer> s = map.getOrDefault(a, new HashSet<>());
            s.add(b);
            map.put(a, s);
        }
        Deque<Integer> d = new ArrayDeque<>();
        d.addLast(0);
        while (!d.isEmpty() && k-- > 0) {
            int size = d.size();
            while (size-- > 0) {
                int poll = d.pollFirst();
                Set<Integer> es = map.get(poll);
                if (es == null) continue;
                for (int next : es) {
                    d.addLast(next);
                }
            }
        }
        int ans = 0;
        while (!d.isEmpty()) {
            if (d.pollFirst() == n - 1) ans++;
        }
        return ans;
    }
}
```
* 时间复杂度:最多搜索 $k$ 层,最坏情况下每层的每个节点都能拓展出 $n - 1$ 个节点,复杂度为 $O(n^k)$
* 空间复杂度:最坏情况下,所有节点都相互连通。复杂度为 $O(n^k + n^2)$

---

### DFS

同理,我们可以使用 `DFS` 进行求解。

在 `DFS` 过程中限制深度最多为 $k$,然后检查所达节点为 `n-1` 的次数即可。

代码:
```Java
class Solution {
    Map<Integer, Set<Integer>> map = new HashMap<>();
    int n, k, ans;
    public int numWays(int _n, int[][] rs, int _k) {
        n = _n; k = _k;
        for (int[] r : rs) {
            int a = r[0], b = r[1];
            Set<Integer> s = map.getOrDefault(a, new HashSet<>());
            s.add(b);
            map.put(a, s);
        }
        dfs(0, 0);
        return ans;
    }
    void dfs(int u, int sum) {
        if (sum == k) {
            if (u == n - 1) ans++;
            return;
        }
        Set<Integer> es = map.get(u);
        if (es == null) return;
        for (int next : es) {
            dfs(next, sum + 1);
        }
    }
}
```
* 时间复杂度:最多搜索 $k$ 层,最坏情况下每层的每个节点都能拓展出 $n - 1$ 个节点,复杂度为 $O(n^k)$
* 空间复杂度:最坏情况下,所有节点都相互连通。忽略递归带来的额外空间消耗,复杂度为 $O(n^2)$

---

### 动态规划

假设当前我们已经走了 $i$ 步,所在位置为 $j$,那么剩余的 $k - i$ 步,能否到达位置 $n - 1$,仅取决于「剩余步数 $k - i$」和「边权关系 `relation`」,而与我是如何到达位置 $i$ 无关。

而对于方案数而言,如果已经走了 $i$ 步,所在位置为 $j$,到达位置 $n - 1$ 的方案数仅取决于「剩余步数 $i - k$」、「边权关系 `relation`」和「花费 $i$ 步到达位置 $j$ 的方案数」。

**以上分析,归纳到边界「已经走了 $0$ 步,所在位置为 $0$」同样成立。**

**这是一个「无后效性」的问题,可以使用动态规划进行求解。**

定义 $f[i][j]$ 为当前已经走了 $i$ 步,所在位置为 $j$ 的方案数。

那么 $f[k][n - 1]$ 为最终答案,$f[0][0] = 1$ 为显而易见的初始化条件。

不失一般性的考虑,$f[i][j]$ 该如何转移,$f[i][j]$ 应该为所有能够到达位置 $j$ 的点 $p$ 的 $f[i - 1][p]$ 的总和:

$$
f[i][j] = \sum_{i = 0}^{relation.length - 1}f[i - 1][p], relation[i][0]=p,relation[i][1]=j
$$

代码:
```Java
class Solution {
    public int numWays(int n, int[][] rs, int k) {
        int[][] f = new int[k + 1][n];
        f[0][0] = 1;
        for (int i = 1; i <= k; i++) {
            for (int[] r : rs) {
                int a = r[0], b = r[1];
                f[i][b] += f[i - 1][a];
            }
        }
        return f[k][n - 1];
    }
}
```
* 时间复杂度:$O(relation.length * k)$
* 空间复杂度:$O(n * k)$

### 最后

这是我们「刷穿 LeetCode」系列文章的第 `LCP 07` 篇,系列开始于 2021/01/01,截止于起始日 LeetCode 上共有 1916 道题目,部分是有锁题,我们将先把所有不带锁的题目刷完。

在这个系列文章里面,除了讲解解题思路以外,还会尽可能给出最为简洁的代码。如果涉及通解还会相应的代码模板。

为了方便各位同学能够电脑上进行调试和提交代码,我建立了相关的仓库:https://github.com/SharingSource/LogicStack-LeetCode 。

在仓库地址里,你可以看到系列文章的题解链接、系列文章的相应代码、LeetCode 原题链接和其他优选题解。


LCR 161. 连续天数的最高销售额

Question:
某公司每日销售额记于整数数组 `sales`,请返回所有连续一或多天销售额总和的最大值。

要求实现时间复杂度为 $O(n)$ 的算法。

示例 1:
```
输入:sales = [-2,1,-3,4,-1,2,1,-5,4]

输出:6

解释:[4,-1,2,1] 此连续四天的销售总额最高,为 6。
```
示例 2:
```
输入:sales = [5,4,-1,7,8]

输出:23

解释:[5,4,-1,7,8] 此连续五天的销售总额最高,为 23。
```

提示:
* $1 <= arr.length <= 10^5$
* $-100 <= arr[i] <= 100$

Solution:
### 前缀和 or 线性 DP

当要我们求「连续段」区域和的时候,要很自然的想到「前缀和」。

所谓前缀和,是指对原数组“累计和”的描述,通常是指一个与原数组等长的数组。

设前缀和数组为 `sum`,**`sum` 的每一位记录的是从「起始位置」到「当前位置」的元素和**。例如 $sum[x]$ 是指原数组中“起始位置”到“位置 `x`”这一连续段的元素和。

有了前缀和数组 `sum`,当我们求连续段 $[i, j]$ 的区域和时,利用「容斥原理」,便可进行快速求解。

通用公式:`ans = sum[j] - sum[i - 1]`。

由于涉及 `-1` 操作,为减少边界处理,我们可让前缀和数组下标从 $1$ 开始。在进行快速求和时,再根据原数组下标是否从 $1$ 开始,决定是否进行相应的下标偏移。

学习完一维前缀和后,回到本题。

先用 `nums` 预处理出前缀和数组 `sum`,然后在遍历子数组右端点 `j` 的过程中,通过变量 `m` 动态记录已访问的左端点 `i` 的前缀和最小值。最终,在所有 `sum[j] - m` 的取值中选取最大值作为答案。

代码实现上,我们无需明确计算前缀和数组 `sum`,而是使用变量 `s` 表示当前累计的前缀和(充当右端点),并利用变量 `m` 记录已访问的前缀和的最小值(充当左端点)即可。

**本题除了将其看作为「前缀和裸题用有限变量进行空间优化」以外,还能以「线性 DP」角度进行理解。**

定义 $f[i]$ 为考虑前 $i$ 个元素,且第 $nums[i]$ 必选的情况下,形成子数组的最大和。

不难发现,仅考虑前 $i$ 个元素,且 $nums[i]$ 必然参与的子数组中。要么是 $nums[i]$ 自己一个成为子数组,要么与前面的元素共同组成子数组。

因此,状态转移方程:
$$
f[i] = \max(f[i - 1] + nums[i], nums[i])
$$

由于 $f[i]$ 仅依赖于 $f[i - 1]$ 进行转移,可使用有限变量进行优化,因此写出来的代码也是和上述前缀和角度分析的类似。

Java 代码:
```Java
class Solution {
    public int maxSales(int[] nums) {
        int s = 0, m = 0, ans = -10010;
        for (int x : nums) {
            s += x;
            ans = Math.max(ans, s - m);
            m = Math.min(m, s);
        }
        return ans;
    }
}
```
C++ 代码:
```C++
class Solution {
public:
    int maxSales(vector<int>& nums) {
        int s = 0, m = 0, ans = -10010;
        for (int x : nums) {
            s += x;
            ans = max(ans, s - m);
            m = min(m, s);
        }
        return ans;
    }
};
```
Python 代码:
```Python
class Solution:
    def maxSales(self, nums: List[int]) -> int:
        s, m, ans = 0, 0, -10010
        for x in nums:
            s += x
            ans = max(ans, s - m)
            m = min(m, s)
        return ans
```
TypeScript 代码:
```TypeScript
function maxSales(nums: number[]): number {
    let s = 0, m = 0, ans = -10010;
    for (let x of nums) {
        s += x;
        ans = Math.max(ans, s - m);
        m = Math.min(m, s);
    }
    return ans;
};
```
* 时间复杂度:$O(n)$
* 空间复杂度:$O(1)$

---

### 分治

“分治法”的核心思路是将大问题拆分成更小且相似的子问题,通过递归解决这些子问题,最终合并子问题的解来得到原问题的解。

实现分治,关键在于对“递归函数”的设计(入参 & 返回值)。

在涉及数组的分治题中,左右下标 `l` 和 `r` 必然会作为函数入参,因为它能用于表示当前所处理的区间,即小问题的范围。

对于本题,仅将最大子数组和(答案)作为返回值并不足够,因为单纯从小区间的解无法直接推导出大区间的解,我们需要一些额外信息来辅助求解。

具体的,我们可以将返回值设计成四元组,分别代表 `区间和`,`前缀最大值`,`后缀最大值` 和 `最大子数组和`,用 `[sum, lm, rm, max]` 表示。

有了完整的函数签名 `int[] dfs(int[] nums, int l, int r)`,考虑如何实现分治:

1. 根据当前区间 $[l, r]$ 的长度进行分情况讨论:
   1. 若 $l = r$,只有一个元素,区间和为 $nums[l]$,而 最大子数组和、前缀最大值 和 后缀最大值 由于允许“空数组”,因此均为 $\max(nums[l], 0)$
   2. 否则,将当前问题划分为两个子问题,通常会划分为两个相同大小的子问题,划分为 $[l, mid]$ 和 $[mid + 1, r]$ 两份,递归求解,其中 $mid = \left \lfloor \frac{l + r}2{} \right \rfloor$

随后考虑如何用“子问题”的解合并成“原问题”的解:

1. **合并区间和 (`sum`):** 当前问题的区间和等于左右两个子问题的区间和之和,即 `sum = left[0] + right[0]`。
2. **合并前缀最大值 (`lm`):** 当前问题的前缀最大值可以是左子问题的前缀最大值,或者左子问题的区间和加上右子问题的前缀最大值。即 `lm = max(left[1], left[0] + right[1])`。
3. **合并后缀最大值 (`rm`):** 当前问题的后缀最大值可以是右子问题的后缀最大值,或者右子问题的区间和加上左子问题的后缀最大值。即 `rm = max(right[2], right[0] + left[2])`。
4. **合并最大子数组和 (`max`):** 当前问题的最大子数组和可能出现在左子问题、右子问题,或者跨越左右两个子问题的边界。因此,`max` 可以通过 `max(left[3], right[3], left[2] + right[1])` 来得到。

一些细节:由于我们在计算 `lm`、`rm` 和 `max` 的时候允许数组为空,而答案对子数组的要求是至少包含一个元素。因此对于 `nums` 全为负数的情况,我们会错误得出最大子数组和为 `0` 的答案。针对该情况,需特殊处理,遍历一遍 `nums`,若最大值为负数,直接返回最大值。

Java 代码:

```Java
class Solution {
    // 返回值: [sum, lm, rm, max] = [区间和, 前缀最大值, 后缀最大值, 最大子数组和]
    int[] dfs(int[] nums, int l, int r) {
        if (l == r) {
            int t = Math.max(nums[l], 0);
            return new int[]{nums[l], t, t, t};
        }
        // 划分成两个子区间,分别求解
        int mid = l + r >> 1;
        int[] left = dfs(nums, l, mid), right = dfs(nums, mid + 1, r);
        // 组合左右子区间的信息,得到当前区间的信息
        int[] ans = new int[4];
        ans[0] = left[0] + right[0]; // 当前区间和
        ans[1] = Math.max(left[1], left[0] + right[1]); // 当前区间前缀最大值
        ans[2] = Math.max(right[2], right[0] + left[2]); // 当前区间后缀最大值
        ans[3] = Math.max(Math.max(left[3], right[3]), left[2] + right[1]); // 最大子数组和
        return ans;
    }
    public int maxSales(int[] nums) {
        int m = nums[0];
        for (int x : nums) m = Math.max(m, x);
        if (m <= 0) return m;
        return dfs(nums, 0, nums.length - 1)[3];
    }
}
```
C++ 代码:
```C++
class Solution {
public:
    // 返回值: [sum, lm, rm, max] = [区间和, 前缀最大值, 后缀最大值, 最大子数组和]
    vector<int> dfs(vector<int>& nums, int l, int r) {
        if (l == r) {
            int t = max(nums[l], 0);
            return {nums[l], t, t, t};
        }
        // 划分成两个子区间,分别求解
        int mid = l + r >> 1;
        auto left = dfs(nums, l, mid), right = dfs(nums, mid + 1, r);
        // 组合左右子区间的信息,得到当前区间的信息
        vector<int> ans(4);
        ans[0] = left[0] + right[0]; // 当前区间和
        ans[1] = max(left[1], left[0] + right[1]); // 当前区间前缀最大值
        ans[2] = max(right[2], right[0] + left[2]); // 当前区间后缀最大值
        ans[3] = max({left[3], right[3], left[2] + right[1]}); // 最大子数组和
        return ans;
    }
    int maxSales(vector<int>& nums) {
        int m = nums[0];
        for (int x : nums) m = max(m, x);
        if (m <= 0) return m;
        return dfs(nums, 0, nums.size() - 1)[3];
    }
};
```
Python 代码:
```Python
class Solution:
    def maxSales(self, nums: List[int]) -> int:
        def dfs(l, r):
            if l == r:
                t = max(nums[l], 0)
                return [nums[l], t, t, t]
            # 划分成两个子区间,分别求解
            mid = (l + r) // 2
            left, right = dfs(l, mid), dfs(mid + 1, r)
            # 组合左右子区间的信息,得到当前区间的信息
            ans = [0] * 4
            ans[0] = left[0] + right[0] # 当前区间和
            ans[1] = max(left[1], left[0] + right[1]) # 当前区间前缀最大值
            ans[2] = max(right[2], right[0] + left[2]) # 当前区间后缀最大值
            ans[3] = max(left[3], right[3], left[2] + right[1]) # 最大子数组和
            return ans

        m = max(nums)
        if m <= 0:
            return m
        return dfs(0, len(nums) - 1)[3]
```
TypeScript 代码:
```TypeScript
function maxSales(nums: number[]): number {
    const dfs = function (l: number, r: number): number[] {
        if (l == r) {
            const t = Math.max(nums[l], 0);
            return [nums[l], t, t, t];
        }
        // 划分成两个子区间,分别求解
        const mid = (l + r) >> 1;
        const left = dfs(l, mid), right = dfs(mid + 1, r);
        // 组合左右子区间的信息,得到当前区间的信息
        const ans = Array(4).fill(0);
        ans[0] = left[0] + right[0]; // 当前区间和
        ans[1] = Math.max(left[1], left[0] + right[1]); // 当前区间前缀最大值
        ans[2] = Math.max(right[2], right[0] + left[2]); // 当前区间后缀最大值
        ans[3] = Math.max(left[3], right[3], left[2] + right[1]); // 最大子数组和
        return ans;
    }

    const m = Math.max(...nums);
    if (m <= 0) return m;
    return dfs(0, nums.length - 1)[3];
};
```
* 时间复杂度:$O(n)$
* 空间复杂度:$O(\log{n})$

### 最后

这是我们「刷穿 LeetCode」系列文章的第 `No.LCR 161` 篇,系列开始于 2021/01/01,截止于起始日 LeetCode 上共有 1916 道题目,部分是有锁题,我们将先把所有不带锁的题目刷完。

在这个系列文章里面,除了讲解解题思路以外,还会尽可能给出最为简洁的代码。如果涉及通解还会相应的代码模板。

为了方便各位同学能够电脑上进行调试和提交代码,我建立了相关的仓库:https://github.com/SharingSource/LogicStack-LeetCode 。

在仓库地址里,你可以看到系列文章的题解链接、系列文章的相应代码、LeetCode 原题链接和其他优选题解。


LeetCode/剑指 Offer II/剑指 Offer II 003. 前 n 个数字二进制中 1 的个数（简单）.md

剑指 Offer II 003. 前 n 个数字二进制中 1 的个数

Question:
给定一个非负整数 `n` ,请计算 `0` 到 `n` 之间的每个数字的二进制表示中 $1$ 的个数,并输出一个数组。

示例 1:
```
输入: n = 2

输出: [0,1,1]

解释:
0 --> 0
1 --> 1
2 --> 10
```
示例 2:
```
输入: n = 5

输出: [0,1,1,2,1,2]

解释:
0 --> 0
1 --> 1
2 --> 10
3 --> 11
4 --> 100
5 --> 101
```

说明 :
* $0 <= n <= 10^5$

进阶:
* 给出时间复杂度为 $O(n \times sizeof(integer))$ 的解答非常容易。但你可以在线性时间 $O(n)$ 内用一趟扫描做到吗?
* 要求算法的空间复杂度为 $O(n)$。
* 你能进一步完善解法吗?要求在 `C++` 或任何其他语言中不使用任何内置函数(如 `C++` 中的 `__builtin_popcount` )来执行此操作。

Solution:
### 模拟

这道题要对每个数进行统计,因此不会有比 $O(n)$ 更低的做法。

而很容易想到的朴素做法是对每个数进行「位运算」计数,每个数都是 $32$ 位的,因此是一个 $O(32n)$ 的做法。

代码:
```Java
class Solution {
    public int[] countBits(int n) {
        int[] ans = new int[n + 1];
        for (int i = 0; i <= n; i++) ans[i] = getCnt(i);
        return ans;
    }
    int getCnt(int u) {
        int ans = 0;
        for (int i = 0; i < 32; i++) ans += (u >> i) & 1;
        return ans;
    }
}
```
* 时间复杂度:$O(n)$
* 空间复杂度:使用与输入同等规模的空间存储答案。复杂度为 $O(n)$

---

### 动态规划

事实上,这道题是有严格 $O(n)$ 的解法的,要求 $O(n)$ 复杂度又是输出方案的题,通常就是递推的 DP 题。

用已算出的值去凑出要算的值。

那么对于这类问题我们该如何考虑呢?一般是靠经验,如果实在没见过这类题型的话,我们就需要在纸上画一下,分析一下我们**朴素做法的最后一步**是怎么进行的。

不失一般性的,假设当前我要统计的数的 `i`,`i` 对应的二进制表示是 `00000...0010100101`(共 32 位)

如果我们是使用「朴素解法」求解的话,无论是从高位进行统计,还是从低位进行统计,最后一位扫描的都是边缘的数(如果是 1 就计数,不是 1 就不计数)。

* **从低位到高位**,最后一步在扫描**最高位**之前,**统计出 1 的个数应该等同于将 `i` 左移一位,并在最低位补 0,也就是等于 `ans[i << 1]`,这时候就要求我们在计算 `i` 的时候 `i << 1` 已经被算出来(从大到小遍历)**
* **从高位到低位**,最后一步在扫描**最低位**之前,**统计出 1 的个数应该等同于将 `i` 右移一位,并在最高位补 0,也就是等于 `ans[i >> 1]`,这时候就要求我们在计算 `i` 的时候 `i >> 1` 已经被算出来(从小到大遍历)**

通过**对「朴素做法」的最后一步分析**,转移方程就出来了:

* 当**从大到小遍历** :$f(i) = f(i << 1) + ((i >>31 ) \& 1)$
* 当**从小到大遍历** :$f(i) = f(i >> 1) + ( i \& 1 )$

代码:
```Java
class Solution {
    // 从小到大遍历
    public int[] countBits(int n) {
        int[] ans = new int[n + 1];
        // ans[i] = 「i >> 1 所包含的 1 的个数」+「i 的最低位是否为 1」
        for (int i = 1; i <= n; i++) ans[i] = ans[i >> 1] + (i & 1);
        return ans;
    }
}
```
-
```Java
class Solution {
    // 从大到小遍历
    public int[] countBits(int n) {
        int[] ans = new int[n + 1];
        for (int i = n; i >= 0; i--) {
            // 如果计算 i 所需要的 i << 1 超过 n,则不存储在 ans 内,需要额外计算
            int u = i << 1 <= n ? ans[i << 1] : getCnt(i << 1);
            // ans[i] =「i << 1 所包含的 1 的个数」 + 「i 的最高位是否为 1」
            ans[i] = u + ((i >> 31) & 1);
        }
        return ans;
    }
    int getCnt(int u) {
        int ans = 0;
        for (int i = 0; i < 32; i++) ans += (u >> i) & 1;
        return ans;
    }
}
```
* 时间复杂度:$O(n)$
* 空间复杂度:使用与输入同等规模的空间存储答案。复杂度为 $O(n)$

---

### 位运算说明

* `a >> b & 1` 代表检查 `a` 的第 `b` 位是否为 $1$,有两种可能性 $0$ 或者 $1$

* `a += 1 << b` 代表将 `a` 的第 `b` 位设置为 $1$ (当第 `b` 位为 $0$ 的时候适用)

如不想写对第 `b` 位为 $0$ 的前置判断,`a += 1 << b` 也可以改成 `a |= 1 << b`

在之前的题解我就强调过,以上两个操作在位运算中**使用频率超高,建议每位同学都加深理解**。

### 最后

这是我们「刷穿 LeetCode」系列文章的第 `剑指 Offer II 003` 篇,系列开始于 2021/01/01,截止于起始日 LeetCode 上共有 1916 道题目,部分是有锁题,我们将先把所有不带锁的题目刷完。

在这个系列文章里面,除了讲解解题思路以外,还会尽可能给出最为简洁的代码。如果涉及通解还会相应的代码模板。

为了方便各位同学能够电脑上进行调试和提交代码,我建立了相关的仓库:https://github.com/SharingSource/LogicStack-LeetCode 。

在仓库地址里,你可以看到系列文章的题解链接、系列文章的相应代码、LeetCode 原题链接和其他优选题解。


LeetCode/剑指 Offer II/剑指 Offer II 005. 单词长度的最大乘积（中等）.md

剑指 Offer II 005. 单词长度的最大乘积

Question:
给定一个字符串数组 `words`,请计算当两个字符串 `words[i]` 和 `words[j]` 不包含相同字符时,它们长度的乘积的最大值。假设字符串中只包含英语的小写字母。如果没有不包含相同字符的一对字符串,返回 $0$。

示例 1:
```
输入: words = ["abcw","baz","foo","bar","fxyz","abcdef"]

输出: 16

解释: 这两个单词为 "abcw", "fxyz"。它们不包含相同字符,且长度的乘积最大。
```
示例 2:
```
输入: words = ["a","ab","abc","d","cd","bcd","abcd"]

输出: 4

解释: 这两个单词为 "ab", "cd"。
```
示例 3:
```
输入: words = ["a","aa","aaa","aaaa"]

输出: 0

解释: 不存在这样的两个单词。
```

提示:
* $2 <= words.length <= 1000$
* $1 <= words[i].length <= 1000$
* `words[i]` 仅包含小写字母

Solution:
### 模拟

根据题意进行模拟即可,利用每个 $words[i]$ 只有小写字母,且只需要区分两字符是否有字母重复。

我们可以使用一个 `int` 来代指某个 $word[i]$:低 $26$ 来代指字母 `a-z` 是否出现过。

然后对每个「字符对」所对应的两个 `int` 值执行 `&` 操作(若两字符无重复字符,则结果为 $0$),并得出最终答案。

代码:
```Java []
class Solution {
    public int maxProduct(String[] words) {
        int n = words.length, idx = 0;
        int[] masks = new int[n];
        for (String w : words) {
            int t = 0;
            for (int i = 0; i < w.length(); i++) {
                int u = w.charAt(i) - 'a';
                t |= (1 << u);
            }
            masks[idx++] = t;
        }
        int ans = 0;
        for (int i = 0; i < n; i++) {
            for (int j = 0; j < i; j++) {
                if ((masks[i] & masks[j]) == 0) ans = Math.max(ans, words[i].length() * words[j].length());
            }
        }
        return ans;
    }
}
```
* 时间复杂度:令 $n$ 为 $words$ 数组的长度,转换出 $masks$ 的复杂度为 $O(\sum_{i = 0}^{i = n - 1}words[i].length)$;得到答案的复杂度为 $O(n^2)$。整体复杂度为 $O(\max(\sum_{i = 0}^{i = n - 1}words[i].length, n^2))$
* 空间复杂度:$O(n)$

---

### 优化

不难发现,对于词频相同($mask$ 值相等)的两字符,只需要保留字符长度大的即可,因此我们可以使用「哈希表」代替 $masks$ 数组。

代码:
```Java
class Solution {
    public int maxProduct(String[] words) {
        Map<Integer, Integer> map = new HashMap<>();
        for (String w : words) {
            int t = 0, m = w.length();
            for (int i = 0; i < m; i++) {
                int u = w.charAt(i) - 'a';
                t |= (1 << u);
            }
            if (!map.containsKey(t) || map.get(t) < m) map.put(t, m);
        }
        int ans = 0;
        for (int a : map.keySet()) {
            for (int b : map.keySet()) {
                if ((a & b) == 0) ans = Math.max(ans, map.get(a) * map.get(b));
            }
        }
        return ans;
    }
}
```
* 时间复杂度:令 $n$ 为 $words$ 数组的长度,得到 $map$ 的复杂度为 $O(\sum_{i = 0}^{i = n - 1}words[i].length)$;得到答案的复杂度为 $O(n^2)$。整体复杂度为 $O(\max(\sum_{i = 0}^{i = n - 1}words[i].length, n^2))$
* 空间复杂度:$O(n)$

### 最后

这是我们「刷穿 LeetCode」系列文章的第 `剑指 Offer II 005` 篇,系列开始于 2021/01/01,截止于起始日 LeetCode 上共有 1916 道题目,部分是有锁题,我们将先把所有不带锁的题目刷完。

在这个系列文章里面,除了讲解解题思路以外,还会尽可能给出最为简洁的代码。如果涉及通解还会相应的代码模板。

为了方便各位同学能够电脑上进行调试和提交代码,我建立了相关的仓库:https://github.com/SharingSource/LogicStack-LeetCode 。

在仓库地址里,你可以看到系列文章的题解链接、系列文章的相应代码、LeetCode 原题链接和其他优选题解。


LeetCode/剑指 Offer II/剑指 Offer II 007. 数组中和为 0 的三个数（中等）.md

Question:
给你一个包含 $n$ 个整数的数组 `nums`,判断 `nums` 中是否存在三个元素 `a`,`b`,`c` ,使得 `a + b + c = 0` ?

请你找出所有和为 $0$ 且不重复的三元组。

注意:答案中不可以包含重复的三元组。

示例 1:
```
输入:nums = [-1,0,1,2,-1,-4]

输出:[[-1,-1,2],[-1,0,1]]
```
示例 2:
```
输入:nums = []

输出:[]
```
示例 3:
```
输入:nums = [0]

输出:[]
```

提示:
* $0 <= nums.length <= 3000$
* $-10^5 <= nums[i] <= 10^5$

Solution:
### 排序 + 双指针

对数组进行排序,使用三个指针 `i`、`j` 和 `k` 分别代表要找的三个数。

1. 通过枚举 `i` 确定第一个数,另外两个指针 `j`,`k` 分别从左边 `i + 1` 和右边 `n - 1` 往中间移动,找到满足 `nums[i] + nums[j] + nums[k] == 0` 的所有组合。

2. `j` 和 `k` 指针的移动逻辑,分情况讨论 `sum = nums[i] + nums[j] + nums[k]` :
    * `sum` > 0:`k` 左移,使 `sum` 变小
    * `sum` < 0:`j` 右移,使 `sum` 变大
    * `sum` = 0:找到符合要求的答案,存起来

由于题目要求答案不能包含重复的三元组,所以在确定第一个数和第二个数的时候,要跳过数值一样的下标(在三数之和确定的情况下,确保第一个数和第二个数不会重复,即可保证三元组不重复)。

代码:
```Java
class Solution {
    public List<List<Integer>> threeSum(int[] nums) {
        Arrays.sort(nums);
        int n = nums.length;
        List<List<Integer>> ans = new ArrayList<>();
        for (int i = 0; i < n; i++) {
            if (i > 0 && nums[i] == nums[i - 1]) continue;
            int j = i + 1, k = n - 1;
            while (j < k) {
                while (j > i + 1 && j < n && nums[j] == nums[j - 1]) j++;
                if (j >= k) break;
                int sum = nums[i] + nums[j] + nums[k];
                if (sum == 0) {
                    ans.add(Arrays.asList(nums[i], nums[j], nums[k]));
                    j++;
                } else if (sum > 0) {
                    k--;
                } else if (sum < 0) {
                    j++;
                }
            }
        }
        return ans;
    }
}
```
* 时间复杂度:排序的复杂度为 $O(n\log{n})$,对于每个 `i` 而言,最坏的情况 `j` 和 `k` 都要扫描一遍数组的剩余部分,复杂度为 $O(n^2)$。整体复杂度为 $O(n^2)$
* 空间复杂度:$O(\log{n})$

### 最后

这是我们「刷穿 LeetCode」系列文章的第 `剑指 Offer II 007` 篇,系列开始于 2021/01/01,截止于起始日 LeetCode 上共有 1916 道题目,部分是有锁题,我们将先把所有不带锁的题目刷完。

在这个系列文章里面,除了讲解解题思路以外,还会尽可能给出最为简洁的代码。如果涉及通解还会相应的代码模板。

为了方便各位同学能够电脑上进行调试和提交代码,我建立了相关的仓库:https://github.com/SharingSource/LogicStack-LeetCode 。

在仓库地址里,你可以看到系列文章的题解链接、系列文章的相应代码、LeetCode 原题链接和其他优选题解。


LeetCode/剑指 Offer II/剑指 Offer II 008. 和大于等于 target 的最短子数组（中等）.md

Question:
给定一个含有 `n` 个正整数的数组和一个正整数 `target`。

找出该数组中满足其和 `≥ target` 的长度最小的 连续子数组 $[nums_{l}, nums_{l+1}, ..., nums_{r-1}, nums_{r}]$ ,并返回其长度。如果不存在符合条件的子数组,返回 $0$ 。

示例 1:
```
输入:target = 7, nums = [2,3,1,2,4,3]

输出:2

解释:子数组 [4,3] 是该条件下的长度最小的子数组。
```
示例 2:
```
输入:target = 4, nums = [1,4,4]

输出:1
```
示例 3:
```
输入:target = 11, nums = [1,1,1,1,1,1,1,1]

输出:0
```

提示:
* $1 <= target <= 10^9$
* $1 <= nums.length <= 10^5$
* $1 <= nums[i] <= 10^5$

Solution:
### 前缀和 + 二分

利用 $nums[i]$ 的数据范围为 $[1, 10^5]$,可知前缀和数组满足「单调递增」。

我们先预处理出前缀和数组 `sum`(前缀和数组下标默认从 $1$ 开始),对于每个 $nums[i]$ 而言,假设其对应的前缀和值为 $s = sum[i + 1]$,我们将 $nums[i]$ 视为子数组的右端点,问题转换为:在前缀和数组下标 $[0, i]$ 范围内找到满足「**值小于等于 $s - t$**」的最大下标,充当子数组左端点的前一个值。

利用前缀和数组的「单调递增」(即具有二段性),该操作可使用「二分」来做。

代码:
```Java
class Solution {
    public int minSubArrayLen(int t, int[] nums) {
        int n = nums.length, ans = n + 10;
        int[] sum = new int[n + 10];
        for (int i = 1; i <= n; i++) sum[i] = sum[i - 1] + nums[i - 1];
        for (int i = 1; i <= n; i++) {
            int s = sum[i], d = s - t;
            int l = 0, r = i;
            while (l < r) {
                int mid = l + r + 1 >> 1;
                if (sum[mid] <= d) l = mid;
                else r = mid - 1;
            }
            if (sum[r] <= d) ans = Math.min(ans, i - r);
        }
        return ans == n + 10 ? 0 : ans;
    }
}
```
* 时间复杂度:预处理前缀和数组的复杂度为 $O(n)$,遍历前缀和数组统计答案复杂度为 $O(n\log{n})$。整体复杂度为 $O(n\log{n})$
* 空间复杂度:$O(n)$

### 最后

这是我们「刷穿 LeetCode」系列文章的第 `剑指 Offer II 008` 篇,系列开始于 2021/01/01,截止于起始日 LeetCode 上共有 1916 道题目,部分是有锁题,我们将先把所有不带锁的题目刷完。

在这个系列文章里面,除了讲解解题思路以外,还会尽可能给出最为简洁的代码。如果涉及通解还会相应的代码模板。

为了方便各位同学能够电脑上进行调试和提交代码,我建立了相关的仓库:https://github.com/SharingSource/LogicStack-LeetCode 。

在仓库地址里,你可以看到系列文章的题解链接、系列文章的相应代码、LeetCode 原题链接和其他优选题解。


LeetCode/剑指 Offer II/剑指 Offer II 009. 乘积小于 K 的子数组（中等）.md

Question:
给你一个整数数组 `nums` 和一个整数 $k$ ,请你返回子数组内所有元素的乘积严格小于 $k$ 的连续子数组的数目。

示例 1:
```
输入:nums = [10,5,2,6], k = 100

输出:8

解释:8 个乘积小于 100 的子数组分别为:[10]、[5]、[2],、[6]、[10,5]、[5,2]、[2,6]、[5,2,6]。
需要注意的是 [10,5,2] 并不是乘积小于 100 的子数组。
```
示例 2:
```
输入:nums = [1,2,3], k = 0

输出:0
```

提示:
* $1 <= nums.length <= 3 \times 10^4$
* $1 <= nums[i] <= 1000$
* $0 <= k <= 10^6$

Solution:
### 滑动窗口

利用 $1 <= nums[i] <= 1000$,我们可以从前往后处理所有的 $nums[i]$,使用一个变量 $cur$ 记录当前窗口的乘积,使用两个变量 $j$ 和 $i$ 分别代表当前窗口的左右端点。

当 $cur >= k$ 时,我们考虑将左端点 $j$ 右移,同时消除原来左端点元素 $nums[j]$ 对 $cur$ 的贡献,直到 $cur >= k$ 不再满足,这样我们就可以得到每个右端点 $nums[i]$ 的最远左端点 $nums[j]$,从而得知以 $nums[i]$ 为结尾的合法子数组个数为 $i - j + 1$。

Java 代码:
```Java
class Solution {
    public int numSubarrayProductLessThanK(int[] nums, int k) {
        int n = nums.length, ans = 0;
        if (k <= 1) return 0;
        for (int i = 0, j = 0, cur = 1; i < n; i++) {
            cur *= nums[i];
            while (cur >= k) cur /= nums[j++];
            ans += i - j + 1;
        }
        return ans;
    }
}
```
TypeScript 代码:
```TypeScript
function numSubarrayProductLessThanK(nums: number[], k: number): number {
    let n = nums.length, ans = 0
    if (k <= 1) return 0
    for (let i = 0, j = 0, cur = 1; i < n; i++) {
        cur *= nums[i]
        while (cur >= k) cur /= nums[j++]
        ans += i - j + 1
    }
    return ans
};
```
* 时间复杂度:$O(n)$
* 空间复杂度:$O(1)$

### 最后

这是我们「刷穿 LeetCode」系列文章的第 `剑指 Offer II 009` 篇,系列开始于 2021/01/01,截止于起始日 LeetCode 上共有 1916 道题目,部分是有锁题,我们将先把所有不带锁的题目刷完。

在这个系列文章里面,除了讲解解题思路以外,还会尽可能给出最为简洁的代码。如果涉及通解还会相应的代码模板。

为了方便各位同学能够电脑上进行调试和提交代码,我建立了相关的仓库:https://github.com/SharingSource/LogicStack-LeetCode 。

在仓库地址里,你可以看到系列文章的题解链接、系列文章的相应代码、LeetCode 原题链接和其他优选题解。


LeetCode/剑指 Offer II/剑指 Offer II 010. 和为 k 的子数组（中等）.md

Question:
给你一个整数数组 `nums` 和一个整数 `k` ,请你统计并返回该数组中和为 `k` 的子数组的个数 。

示例 1:
```
输入:nums = [1,1,1], k = 2

输出:2
```
示例 2:
```
输入:nums = [1,2,3], k = 3

输出:2
```

提示:
* $1 <= nums.length <= 2 \times 10^4$
* $-1000 <= nums[i] <= 1000$
* $-10^7 <= k <= 10^7$

Solution:
### 前缀和 + 哈希表

这是一道经典的前缀和运用题。

统计以每一个 $nums[i]$ 为结尾,和为 $k$ 的子数组数量即是答案。

我们可以预处理前缀和数组 `sum`(前缀和数组下标默认从 $1$ 开始),对于求解以某一个 $nums[i]$ 为结尾的,和为 $k$ 的子数组数量,本质上是求解在 $[0, i]$ 中,`sum` 数组中有多少个值为 $sum[i + 1] - k$ 的数,这可以在遍历过程中使用「哈希表」进行同步记录。

Java 代码:
```Java
class Solution {
    public int subarraySum(int[] nums, int k) {
        int n = nums.length, ans = 0;
        int[] sum = new int[n + 10];
        for (int i = 1; i <= n; i++) sum[i] = sum[i - 1] + nums[i - 1];
        Map<Integer, Integer> map = new HashMap<>();
        map.put(0, 1);
        for (int i = 1; i <= n; i++) {
            int t = sum[i], d = t - k;
            ans += map.getOrDefault(d, 0);
            map.put(t, map.getOrDefault(t, 0) + 1);
        }
        return ans;
    }
}
```
TypeScript 代码:
```TypeScript
function subarraySum(nums: number[], k: number): number {
    let n = nums.length, ans = 0
    const sum = new Array<number>(n + 10).fill(0)
    for (let i = 1; i <= n; i++) sum[i] = sum[i - 1] + nums[i - 1]
    const map = new Map<number, number>()
    map.set(0, 1)
    for (let i = 1; i <= n; i++) {
        const t = sum[i], d = t - k
        if (map.has(d)) ans += map.get(d)
        map.set(t, map.has(t) ? map.get(t) + 1 : 1)
    }
    return ans
};
```
* 时间复杂度:预处理前缀和的复杂度为 $O(n)$,统计答案的复杂度为 $O(n)$。整体复杂度为 $O(n)$
* 空间复杂度:$O(n)$

### 最后

这是我们「刷穿 LeetCode」系列文章的第 `剑指 Offer II 010` 篇,系列开始于 2021/01/01,截止于起始日 LeetCode 上共有 1916 道题目,部分是有锁题,我们将先把所有不带锁的题目刷完。

在这个系列文章里面,除了讲解解题思路以外,还会尽可能给出最为简洁的代码。如果涉及通解还会相应的代码模板。

为了方便各位同学能够电脑上进行调试和提交代码,我建立了相关的仓库:https://github.com/SharingSource/LogicStack-LeetCode 。

在仓库地址里,你可以看到系列文章的题解链接、系列文章的相应代码、LeetCode 原题链接和其他优选题解。


LeetCode/剑指 Offer II/剑指 Offer II 011. 0 和 1 个数相同的子数组（中等）.md

Question:
给定一个二进制数组 `nums` , 找到含有相同数量的 $0$ 和 $1$ 的最长连续子数组,并返回该子数组的长度。

示例 1:
```
输入: nums = [0,1]

输出: 2

说明: [0, 1] 是具有相同数量0和1的最长连续子数组。
```
示例 2:
```
输入: nums = [0,1,0]

输出: 2

说明: [0, 1] (或 [1, 0]) 是具有相同数量0和1的最长连续子数组。
```

提示:
* $1 <= nums.length <= 10^5$
* `nums[i]` 不是 $0$ 就是 $1$

Solution:
### 前缀和 + 哈希表

根据题意,我们可以轻易发现如下性质:如果答案非 $0$,那么子数组长度必然为偶数,且长度至少为 $2$。

具体的,我们在预处理前缀和时,将 $nums[i]$ 为 $0$ 的值当做 $-1$ 处理。

从而将问题转化为:**如何求得最长一段区间和为 $0$ 的子数组。** 同时使用「哈希表」来记录「某个前缀和出现的最小下标」是多少。

Java 代码:
```Java
class Solution {
    public int findMaxLength(int[] nums) {
        int n = nums.length, ans = 0;
        int[] sum = new int[n + 10];
        for (int i = 1; i <= n; i++) sum[i] = sum[i - 1] + (nums[i - 1] == 0 ? -1 : 1);
        Map<Integer, Integer> map = new HashMap<>();
        map.put(0, 0);
        for (int i = 1; i <= n; i++) {
            int t = sum[i];
            if (map.containsKey(t)) ans = Math.max(ans, i - map.get(t));
            if (!map.containsKey(t)) map.put(t, i);
        }
        return ans;
    }
}
```
TypeScript 代码:
```TypeScript
function findMaxLength(nums: number[]): number {
    let n = nums.length, ans = 0
    const sum = new Array<number>(n + 10).fill(0)
    for (let i = 1; i <= n; i++) sum[i] = sum[i - 1] + (nums[i - 1] == 0 ? -1 : 1)
    const map = new Map<number, number>()
    map.set(0, 0)
    for (let i = 1; i <= n; i++) {
        const t = sum[i]
        if (map.has(t)) ans = Math.max(ans, i - map.get(t))
        if (!map.has(t)) map.set(t, i)
    }
    return ans
};
```
* 时间复杂度:$O(n)$
* 空间复杂度:$O(n)$

### 最后

这是我们「刷穿 LeetCode」系列文章的第 `剑指 Offer II 011` 篇,系列开始于 2021/01/01,截止于起始日 LeetCode 上共有 1916 道题目,部分是有锁题,我们将先把所有不带锁的题目刷完。

在这个系列文章里面,除了讲解解题思路以外,还会尽可能给出最为简洁的代码。如果涉及通解还会相应的代码模板。

为了方便各位同学能够电脑上进行调试和提交代码,我建立了相关的仓库:https://github.com/SharingSource/LogicStack-LeetCode 。

在仓库地址里,你可以看到系列文章的题解链接、系列文章的相应代码、LeetCode 原题链接和其他优选题解。


LeetCode/剑指 Offer II/剑指 Offer II 029. 排序的循环链表（中等）.md

Question:
给定循环单调非递减列表中的一个点,写一个函数向这个列表中插入一个新元素 `insertVal`,使这个列表仍然是循环升序的。

给定的可以是这个列表中任意一个顶点的指针,并不一定是这个列表中最小元素的指针。

如果有多个满足条件的插入位置,可以选择任意一个位置插入新的值,插入后整个列表仍然保持有序。

如果列表为空(给定的节点是 `null`),需要创建一个循环有序列表并返回这个节点。否则。请返回原先给定的节点。

示例 1:

```
输入:head = [3,4,1], insertVal = 2

输出:[3,4,1,2]
```

```
解释:在上图中,有一个包含三个元素的循环有序列表,你获得值为 3 的节点的指针,我们需要向表中插入元素 2 。新插入的节点应该在 1 和 3 之间,插入之后,整个列表如上图所示,最后返回节点 3 。
```

示例 2:
```
输入:head = [], insertVal = 1

输出:[1]

解释:列表为空(给定的节点是 null),创建一个循环有序列表并返回这个节点。
```
示例 3:
```
输入:head = [1], insertVal = 0

输出:[1,0]
```

提示:
* $0 <= Number of Nodes <= 5 \times 10^4$
* $-10^6 <= Node.val <= 10^6$
* $-10^6 <= insertVal <= 10^6$

Solution:
### 链表

这是一道常规的链表模拟题。

为了方便,我们记 `insertVal` 为 `x`,记 `head` 为 `he`。

起始我们先将待插入的节点创建出来,记为 `t`,当 `he` 为空时,直接返回 `t` 即可。

由于我们需要返回原本的头结点,因此我们先使用变量 `ans` 对原始的 `he` 进行转存,随后复用 `he` 来充当游标进行遍历。

我们先对链表进行一次完成遍历,遍历过程中维护节点最值 `max` 和 `min`,由于链表是循环的,我们需要使用 `he.next != ans` 作为我们循环的结束条件,含义为回到链表开头。

此时根据最大值和最小值是否相等(即整段链表值是否一致)来进行分情况讨论:

* 若满足 `max = min`,此时目标节点 `t` 插入在哪个位置都满足条件,我们直接将其与 `ans` 关联即可;
* 若不满足 `max = min`,此时我们先对链表进行一次遍历,找到有序列表的结束点(结束点的定义为:当前节点值为最大值,下一节点值为最小值。即为有序链表分割位置的左端点),在根据「插入值 `x` 是否为新链表的最值」进行分情况讨论:
    * 若满足 $x >= max$ 或 $x <= min$ ,说明目标节点 `t` 插入分割位置即可;
    * 若不满足上述两条件,需要从分割位置出发,找到目标插入位置,即满足 `he.val <= x && x <= he.next.val` 的位置。

代码:
```Java
class Solution {
    public Node insert(Node he, int x) {
        Node t = new Node(x);
        t.next = t;
        if (he == null) return t;
        Node ans = he;
        int min = he.val, max = he.val;
        while (he.next != ans) {
            he = he.next;
            min = Math.min(min, he.val);
            max = Math.max(max, he.val);
        }
        if (min == max) {
            t.next = ans.next;
            ans.next = t;
        } else {
            while (!(he.val == max && he.next.val == min)) he = he.next;
            while (!(x <= min || x >= max) && !(he.val <= x && x <= he.next.val)) he = he.next;
            t.next = he.next;
            he.next = t;
        }
        return ans;
    }
}
```
* 时间复杂度:$O(n)$
* 空间复杂度:$O(1)$

### 最后

这是我们「刷穿 LeetCode」系列文章的第 `剑指 Offer II 029` 篇,系列开始于 2021/01/01,截止于起始日 LeetCode 上共有 1916 道题目,部分是有锁题,我们将先把所有不带锁的题目刷完。

在这个系列文章里面,除了讲解解题思路以外,还会尽可能给出最为简洁的代码。如果涉及通解还会相应的代码模板。

为了方便各位同学能够电脑上进行调试和提交代码,我建立了相关的仓库:https://github.com/SharingSource/LogicStack-LeetCode 。

在仓库地址里,你可以看到系列文章的题解链接、系列文章的相应代码、LeetCode 原题链接和其他优选题解。


LeetCode/剑指 Offer II/剑指 Offer II 030. 插入、删除和随机访问都是 O(1) 的容器（中等）.md

剑指 Offer II 030. 插入、删除和随机访问都是 O(1) 的容器

Question:
实现 `RandomizedSet` 类:

* `RandomizedSet()` 初始化 `RandomizedSet` 对象
* `bool insert(int val)` 当元素 `val` 不存在时,向集合中插入该项,并返回 `true`;否则,返回 `false`。
* `bool remove(int val)` 当元素 `val` 存在时,从集合中移除该项,并返回 `true`;否则,返回 `false`。
* `int getRandom()` 随机返回现有集合中的一项(测试用例保证调用此方法时集合中至少存在一个元素)。每个元素应该有 相同的概率 被返回。

你必须实现类的所有函数,并满足每个函数的 平均 时间复杂度为 $O(1)$ 。

示例:
```
输入
["RandomizedSet", "insert", "remove", "insert", "getRandom", "remove", "insert", "getRandom"]
[[], [1], [2], [2], [], [1], [2], []]

输出
[null, true, false, true, 2, true, false, 2]

解释
RandomizedSet randomizedSet = new RandomizedSet();
randomizedSet.insert(1); // 向集合中插入 1 。返回 true 表示 1 被成功地插入。
randomizedSet.remove(2); // 返回 false ,表示集合中不存在 2 。
randomizedSet.insert(2); // 向集合中插入 2 。返回 true 。集合现在包含 [1,2] 。
randomizedSet.getRandom(); // getRandom 应随机返回 1 或 2 。
randomizedSet.remove(1); // 从集合中移除 1 ,返回 true 。集合现在包含 [2] 。
randomizedSet.insert(2); // 2 已在集合中,所以返回 false 。
randomizedSet.getRandom(); // 由于 2 是集合中唯一的数字,getRandom 总是返回 2 。
```

提示:
* $-2^{31} <= val <= 2^{31} - 1$
* 最多调用 `insert`、`remove` 和 `getRandom` 函数 $2 * 10^5$ 次
* 在调用 `getRandom` 方法时,数据结构中 至少存在一个 元素。

Solution:
### 哈希表 + 删除交换

对于 `insert` 和 `remove` 操作容易想到使用「哈希表」来实现 $O(1)$ 复杂度,但对于 `getRandom` 操作,比较理想的情况是能够在一个数组内随机下标进行返回。

将两者结合,我们可以将哈希表设计为:以入参 `val` 为键,数组下标 `loc` 为值。

**为了确保严格 $O(1)$,我们不能「使用拒绝采样」和「在数组非结尾位置添加/删除元素」。**

因此我们需要申请一个足够大的数组 `nums`(利用数据范围为 $2* 10^5$),并使用变量 `idx` 记录当前使用到哪一位(即下标在 $[0, idx]$ 范围内均是存活值)。

对于几类操作逻辑:

* `insert` 操作:使用哈希表判断 `val` 是否存在,存在的话返回 `false`,否则将其添加到 `nums`,更新 `idx`,同时更新哈希表;
* `remove` 操作:使用哈希表判断 `val` 是否存在,不存在的话返回 `false`,否则从哈希表中将 `val` 删除,同时取出其所在 `nums` 的下标 `loc`,然后将 `nums[idx]` 赋值到 `loc` 位置,并更新 `idx`(含义为将原本处于 `loc` 位置的元素删除),同时更新原本位于 `idx` 位置的数在哈希表中的值为 `loc`(若 `loc` 与 `idx` 相等,说明删除的是最后一个元素,这一步可跳过);
* `getRandom` 操作:由于我们人为确保了 $[0, idx]$ 均为存活值,因此直接在 $[0, idx + 1)$ 范围内进行随机即可。

代码:
```Java
class RandomizedSet {
    static int[] nums = new int[200010];
    Random random = new Random();
    Map<Integer, Integer> map = new HashMap<>();
    int idx = -1;
    public boolean insert(int val) {
        if (map.containsKey(val)) return false;
        nums[++idx] = val;
        map.put(val, idx);
        return true;
    }
    public boolean remove(int val) {
        if (!map.containsKey(val)) return false;
        int loc = map.remove(val);
        if (loc != idx) map.put(nums[idx], loc);
        nums[loc] = nums[idx--];
        return true;
    }
    public int getRandom() {
        return nums[random.nextInt(idx + 1)];
    }
}
```
* 时间复杂度:所有操作均为 $O(1)$
* 空间复杂度:$O(n)$

### 最后

这是我们「刷穿 LeetCode」系列文章的第 `剑指 Offer II 030` 篇,系列开始于 2021/01/01,截止于起始日 LeetCode 上共有 1916 道题目,部分是有锁题,我们将先把所有不带锁的题目刷完。

在这个系列文章里面,除了讲解解题思路以外,还会尽可能给出最为简洁的代码。如果涉及通解还会相应的代码模板。

为了方便各位同学能够电脑上进行调试和提交代码,我建立了相关的仓库:https://github.com/SharingSource/LogicStack-LeetCode 。

在仓库地址里,你可以看到系列文章的题解链接、系列文章的相应代码、LeetCode 原题链接和其他优选题解。


LeetCode/剑指 Offer II/剑指 Offer II 041. 滑动窗口的平均值（简单）.md

Question:
给定一个整数数据流和一个窗口大小,根据该滑动窗口的大小,计算滑动窗口里所有数字的平均值。

实现 `MovingAverage` 类:

* `MovingAverage(int size)` 用窗口大小 `size` 初始化对象。
* `double next(int val)` 成员函数 `next` 每次调用的时候都会往滑动窗口增加一个整数,请计算并返回数据流中最后 `size` 个值的移动平均值,即滑动窗口里所有数字的平均值。

示例:
```
输入:
inputs = ["MovingAverage", "next", "next", "next", "next"]
inputs = [[3], [1], [10], [3], [5]]

输出:
[null, 1.0, 5.5, 4.66667, 6.0]

解释:
MovingAverage movingAverage = new MovingAverage(3);
movingAverage.next(1); // 返回 1.0 = 1 / 1
movingAverage.next(10); // 返回 5.5 = (1 + 10) / 2
movingAverage.next(3); // 返回 4.66667 = (1 + 10 + 3) / 3
movingAverage.next(5); // 返回 6.0 = (10 + 3 + 5) / 3
```

提示:
* $1 <= size <= 1000$
* $-10^5 <= val <= 10^5$
* 最多调用 `next` 方法 $10^4$ 次

Solution:
### 双端队列

根据题意,我们可以使用变量 `n` 将初始化传入的 `size` 进行转存,同时使用「双端队列」来存储 `next` 所追加的值(添加到队列尾部),当双端队列所包含元素超过规定数量 `n` 时,我们从队列头部进行 `pop` 操作,整个维护过程使用变量 `sum` 记录当前包含的元素和。

利用 `next` 操作最多被调用 $10^4$ 次,我们可以使用直接开个 $10^4$ 数组来充当双端队列,使用两指针 `j` 和 `i` 分别指向队列的头部和尾部。

Java 代码:
```Java
class MovingAverage {
    int[] arr = new int[10010];
    int n, sum, j, i;
    public MovingAverage(int size) {
        n = size;
    }
    public double next(int val) {
        sum += arr[i++] = val;
        if (i - j > n) sum -= arr[j++];
        return sum * 1.0 / (i - j);
    }
}
```
Typescript 代码:
```Typescript
class MovingAverage {
    arr: number[] = new Array<number>(10010).fill(0)
    n: number = 0; sum: number = 0; i: number = 0; j: number = 0;
    constructor(size: number) {
        this.n = size
    }
    next(val: number): number {
        this.sum += this.arr[this.i++] = val
        if (this.i - this.j > this.n) this.sum -= this.arr[this.j++]
        return this.sum / (this.i - this.j)
    }
}
```
* 时间复杂度:$O(m)$,其中 $m$ 为 `next` 操作的调用次数
* 空间复杂度:$O(n)$

### 最后

这是我们「刷穿 LeetCode」系列文章的第 `剑指 Offer II 041` 篇,系列开始于 2021/01/01,截止于起始日 LeetCode 上共有 1916 道题目,部分是有锁题,我们将先把所有不带锁的题目刷完。

在这个系列文章里面,除了讲解解题思路以外,还会尽可能给出最为简洁的代码。如果涉及通解还会相应的代码模板。

为了方便各位同学能够电脑上进行调试和提交代码,我建立了相关的仓库:https://github.com/SharingSource/LogicStack-LeetCode 。

在仓库地址里,你可以看到系列文章的题解链接、系列文章的相应代码、LeetCode 原题链接和其他优选题解。


LeetCode/剑指 Offer II/剑指 Offer II 067. 最大的异或（中等）.md

Question:
给定一个整数数组 `nums`,返回 `nums[i] XOR nums[j]` 的最大运算结果,其中 $0 ≤ i ≤ j < n$ 。

示例 1:
```
输入:nums = [3,10,5,25,2,8]

输出:28

解释:最大运算结果是 5 XOR 25 = 28.
```
示例 2:
```
输入:nums = [0]

输出:0
```
示例 3:
```
输入:nums = [2,4]

输出:6
```
示例 4:
```
输入:nums = [8,10,2]

输出:10
```
示例 5:
```
输入:nums = [14,70,53,83,49,91,36,80,92,51,66,70]

输出:127
```

提示:
* $1 <= nums.length <= 2 \times 10^4$
* $0 <= nums[i] <= 2^{31} - 1$

进阶:你可以在 $O(n)$ 的时间解决这个问题吗?

Solution:
### 基本分析

要求得数组 `nums` 中的「最大异或结果」,假定 $nums[i]$ 与 $nums[j]$ 异或可以取得最终结果。

**由于异或计算「每位相互独立」(又称为不进位加法),同时具有「相同值异或结果为 $0$,不同值异或结果为 $1$」的特性。**

因此对于 $nums[j]$ 而言,可以从其二进制表示中的最高位开始往低位找,尽量让每一位的异或结果为 $1$,这样找到的 $nums[i]$ 与 $nums[j]$ 的异或结果才是最大的。

具体的,我们需要先将 `nums` 中下标范围为 $[0, j]$ 的数(二进制表示)加入 $Trie$ 中,然后每次贪心的匹配每一位(优先匹配与之不同的二进制位)。

---

### 证明

由于我们会从前往后扫描 `nums` 数组,因此 $nums[j]$ 必然会被处理到,所以我们只需要证明,在选定 $nums[j]$ 的情况下,我们的算法能够在 $[0, j]$ 范围内找到 $nums[i]$ 即可。

假定我们算法找出来的数值与 $nums[j]$ 的异或结果为 $x$,而真实的最优异或结果为 $y$。

接下来需要证得 $x$ 和 $y$ 相等。

由于找的是「最大异或结果」, 而 $x$ 是一个合法值,因此我们天然有 $x \leq y$。

然后利用反证法证明 $x \geq y$,假设 $x \geq y$ 不成立,即有 $x < y$,那么从两者的二进制表示的高位开始找,必然能找到第一位不同:$y$ 的「不同位」的值为 $1$,而 $x$ 的「不同位」的值为 $0$。

那么对应到选择这一个「不同位」的逻辑:能够选择与 $nums[j]$ 该位不同的值,使得该位的异或结果为 $1$,但是我们的算法选择了与 $nums[j]$ 该位相同的值,使得该位的异或结果为 $0$。

这与我们的算法逻辑冲突,因此必然不存在这样的「不同位」。即 $x < y$ 不成立,反证 $x \geq y$ 成立。

得证 $x$ 与 $y$ 相等。

---

### Trie 数组实现

可以使用数组来实现 $Trie$,但由于 OJ 每跑一个样例都会创建一个新的对象,因此使用数组实现,相当于每跑一个数据都需要 `new` 一个百万级别的数组,会 TLE 。

因此这里使用数组实现必须要做的一个优化是:**使用 `static` 来修饰 $Trie$ 数组,然后在初始化时做相应的清理工作。**

担心有不熟 Java 的同学,在代码里添加了相应注释说明。

代码:
```Java
class Solution {
    // static 成员整个类独一份,只有在类首次加载时才会创建,因此只会被 new 一次
    static int N = (int)1e6;
    static int[][] trie = new int[N][2];
    static int idx = 0;
    // 每跑一个数据,会被实例化一次,每次实例化的时候被调用,做清理工作
    public Solution() {
        for (int i = 0; i <= idx; i++) {
            Arrays.fill(trie[i], 0);
        }
        idx = 0;
    }
    void add(int x) {
        int p = 0;
        for (int i = 31; i >= 0; i--) {
            int u = (x >> i) & 1;
            if (trie[p][u] == 0) trie[p][u] = ++idx;
            p = trie[p][u];
        }
    }
    int getVal(int x) {
        int ans = 0;
        int p = 0;
        for (int i = 31; i >= 0; i--) {
            int a = (x >> i) & 1, b = 1 - a;
            if (trie[p][b] != 0) {
                ans |= (b << i);
                p = trie[p][b];
            } else {
                ans |= (a << i);
                p = trie[p][a];
            }
        }
        return ans;
    }
    public int findMaximumXOR(int[] nums) {
        int ans = 0;
        for (int i : nums) {
            add(i);
            int j = getVal(i);
            ans = Math.max(ans, i ^ j);
        }
        return ans;
    }
}
```
* 时间复杂度:$O(n)$
* 空间复杂度:$O(1e6)$

---

### Trie 类实现

相比于使用 `static` 来优化,一个更好的做法是使用类来实现 $Trie$,这样可以真正做到「按需分配」内存,缺点是会发生不确定次数的 `new`。

代码:
```Java
class Solution {
    class Node {
        Node[] ns = new Node[2];
    }
    Node root = new Node();
    void add(int x) {
        Node p = root;
        for (int i = 31; i >= 0; i--) {
            int u = (x >> i) & 1;
            if (p.ns[u] == null) p.ns[u] = new Node();
            p = p.ns[u];
        }
    }
    int getVal(int x) {
        int ans = 0;
        Node p = root;
        for (int i = 31; i >= 0; i--) {
            int a = (x >> i) & 1, b = 1 - a;
            if (p.ns[b] != null) {
                ans |= (b << i);
                p = p.ns[b];
            } else {
                ans |= (a << i);
                p = p.ns[a];
            }
        }
        return ans;
    }
    public int findMaximumXOR(int[] nums) {
        int ans = 0;
        for (int i : nums) {
            add(i);
            int j = getVal(i);
            ans = Math.max(ans, i ^ j);
        }
        return ans;
    }
}
```
* 时间复杂度:$O(n)$
* 空间复杂度:$O(n)$

### 最后

这是我们「刷穿 LeetCode」系列文章的第 `No.剑指 Offer II 067` 篇,系列开始于 2021/01/01,截止于起始日 LeetCode 上共有 1916 道题目,部分是有锁题,我们将先把所有不带锁的题目刷完。

在这个系列文章里面,除了讲解解题思路以外,还会尽可能给出最为简洁的代码。如果涉及通解还会相应的代码模板。

为了方便各位同学能够电脑上进行调试和提交代码,我建立了相关的仓库:https://github.com/SharingSource/LogicStack-LeetCode 。

在仓库地址里,你可以看到系列文章的题解链接、系列文章的相应代码、LeetCode 原题链接和其他优选题解。


LeetCode/剑指 Offer II/剑指 Offer II 069. 山峰数组的顶部（简单）.md

Question:
符合下列属性的数组 `arr` 称为 山峰数组(山脉数组) :
* `arr.length >= 3`
* 存在 `i`(`0 < i < arr.length - 1`)使得:
	* `arr[0] < arr[1] < ... arr[i-1] < arr[i]`
	* `arr[i] > arr[i+1] > ... > arr[arr.length - 1]`

给定由整数组成的山峰数组 arr ,返回任何满足 `arr[0] < arr[1] < ... arr[i - 1] < arr[i] > arr[i + 1] > ... > arr[arr.length - 1]` 的下标 `i` ,即山峰顶部。

示例 1:
```
输入:arr = [0,1,0]

输出:1
```
示例 2:
```
输入:arr = [1,3,5,4,2]

输出:2
```
示例 3:
```
输入:arr = [0,10,5,2]

输出:1
```
示例 4:
```
输入:arr = [3,4,5,1]

输出:2
```
示例 5:
```
输入:arr = [24,69,100,99,79,78,67,36,26,19]

输出:2
```

提示:
* 3 <= arr.length <= $10^4$
* 0 <= arr[i] <= $10^6$
* 题目数据保证 arr 是一个山脉数组

**进阶:很容易想到时间复杂度 O(n) 的解决方案,你可以设计一个 O(log(n)) 的解决方案吗?**

Solution:
### 二分

往常我们使用「二分」进行查值,需要确保序列本身满足「二段性」:当选定一个端点(基准值)后,结合「一段满足 & 另一段不满足」的特性来实现“折半”的查找效果。

但本题求的是峰顶索引值,如果我们选定数组头部或者尾部元素,其实无法根据大小关系“直接”将数组分成两段。

但可以利用题目发现如下性质:**由于 `arr` 数值各不相同,因此峰顶元素左侧必然满足严格单调递增,峰顶元素右侧必然不满足。**

因此 **以峰顶元素为分割点的 `arr` 数组,根据与 前一元素/后一元素 的大小关系,具有二段性:**

* 峰顶元素左侧满足 $arr[i-1] < arr[i]$ 性质,右侧不满足
* 峰顶元素右侧满足 $arr[i] > arr[i+1]$ 性质,左侧不满足

因此我们可以选择任意条件,写出若干「二分」版本。

代码:
```Java
class Solution {
    // 根据 arr[i-1] < arr[i] 在 [1,n-1] 范围内找值
    // 峰顶元素为符合条件的最靠近中心的元素
    public int peakIndexInMountainArray(int[] arr) {
        int n = arr.length;
        int l = 1, r = n - 1;
        while (l < r) {
            int mid = l + r + 1 >> 1;
            if (arr[mid - 1] < arr[mid]) l = mid;
            else r = mid - 1;
        }
        return r;
    }
}
```
```Java
class Solution {
    // 根据 arr[i] > arr[i+1] 在 [0,n-2] 范围内找值
    // 峰顶元素为符合条件的最靠近中心的元素值
    public int peakIndexInMountainArray(int[] arr) {
        int n = arr.length;
        int l = 0, r = n - 2;
        while (l < r) {
            int mid = l + r >> 1;
            if (arr[mid] > arr[mid + 1]) r = mid;
            else l = mid + 1;
        }
        return r;
    }
}
```
```Java
class Solution {
    // 根据 arr[i-1] > arr[i] 在 [1,n-1] 范围内找值
    // 峰顶元素为符合条件的最靠近中心的元素的前一个值
    public int peakIndexInMountainArray(int[] arr) {
        int n = arr.length;
        int l = 1, r = n - 1;
        while (l < r) {
            int mid = l + r >> 1;
            if (arr[mid - 1] > arr[mid]) r = mid;
            else l = mid + 1;
        }
        return r - 1;
    }
}
```
```Java
class Solution {
    // 根据 arr[i] < arr[i+1] 在 [0,n-2] 范围内找值
    // 峰顶元素为符合条件的最靠近中心的元素的下一个值
    public int peakIndexInMountainArray(int[] arr) {
        int n = arr.length;
        int l = 0, r = n - 2;
        while (l < r) {
            int mid = l + r + 1 >> 1;
            if (arr[mid] < arr[mid + 1]) l = mid;
            else r = mid - 1;
        }
        return r + 1;
    }
}
```
* 时间复杂度:$O(\log{n})$
* 空间复杂度:$O(1)$

---

### 三分

事实上,我们还可以利用「三分」来解决这个问题。

顾名思义,**「三分」就是使用两个端点将区间分成三份,然后通过每次否决三分之一的区间来逼近目标值。**

具体的,由于峰顶元素为全局最大值,因此我们可以每次将当前区间分为 $[l, m1]$、$[m1, m2]$ 和 $[m2, r]$ 三段,如果满足 $arr[m1] > arr[m2]$,说明峰顶元素不可能存在与 $[m2, r]$ 中,让 $r = m2 - 1$ 即可。另外一个区间分析同理。

代码:
```Java
class Solution {
    public int peakIndexInMountainArray(int[] arr) {
        int n = arr.length;
        int l = 0, r = n - 1;
        while (l < r) {
            int m1 = l + (r - l) / 3;
            int m2 = r - (r - l) / 3;
            if (arr[m1] > arr[m2]) r = m2 - 1;
            else l = m1 + 1;
        }
        return r;
    }
}
```
* 时间复杂度:$O(\log{n})$
* 空间复杂度:$O(1)$

---

### 二分 & 三分 & k 分 ?

必须说明一点,「二分」和「三分」在渐进复杂度上都是一样的,都可以通过换底公式转化为可忽略的常数,因此两者的复杂度都是 $O(\log{n})$。

因此选择「二分」还是「三分」取决于要解决的是什么问题:

* 二分通常用来解决单调函数的找 $target$ 问题,但进一步深入我们发现只需要满足「二段性」就能使用「二分」来找分割点;
* 三分则是解决单峰函数极值问题。

**因此一般我们将「通过比较两个端点,每次否决 1/3 区间 来解决单峰最值问题」的做法称为「三分」;而不是简单根据单次循环内将区间分为多少份来判定是否为「三分」。**

随手写了一段反例代码:
```Java
class Solution {
    public int peakIndexInMountainArray(int[] arr) {
        int left = 0, right = arr.length - 1;
        while(left < right) {
            int m1 = left + (right - left) / 3;
            int m2 = right - (right - left + 2) / 3;
            if (arr[m1] > arr[m1 + 1]) {
                right = m1;
            } else if (arr[m2] < arr[m2 + 1]) {
                left = m2 + 1;
            } else {
                left = m1;
                right = m2;
            }
        }
        return left;
    }
}
```

这并不是「三分」做法,最多称为「变形二分」。本质还是利用「二段性」来做分割的,只不过同时 check 了两个端点而已。

如果这算「三分」的话,那么我能在一次循环里面划分 $k - 1$ 个端点来实现 $k$ 分?

**显然这是没有意义的,因为按照这种思路写出来的所谓的「四分」、「五分」、「k 分」是需要增加同等数量的分支判断的。这时候单次 `while` 决策就不能算作 $O(1)$ 了,而是需要在 $O(k)$ 的复杂度内决定在哪个分支,就跟上述代码有三个分支进行判断一样。** 因此,这种写法只能算作是「变形二分」。

**综上,只有「二分」和「三分」的概念,不存在所谓的 $k$ 分。** 同时题解中的「三分」部分提供的做法就是标准的「三分」做法。

### 最后

这是我们「刷穿 LeetCode」系列文章的第 `No.剑指 Offer II 069` 篇,系列开始于 2021/01/01,截止于起始日 LeetCode 上共有 1916 道题目,部分是有锁题,我们将先把所有不带锁的题目刷完。

在这个系列文章里面,除了讲解解题思路以外,还会尽可能给出最为简洁的代码。如果涉及通解还会相应的代码模板。

为了方便各位同学能够电脑上进行调试和提交代码,我建立了相关的仓库:https://github.com/SharingSource/LogicStack-LeetCode 。

在仓库地址里,你可以看到系列文章的题解链接、系列文章的相应代码、LeetCode 原题链接和其他优选题解。


LeetCode/剑指 Offer II/剑指 Offer II 091. 粉刷房子（中等）.md

Question:
假如有一排房子,共 `n` 个,每个房子可以被粉刷成红色、蓝色或者绿色这三种颜色中的一种,你需要粉刷所有的房子并且使其相邻的两个房子颜色不能相同。

当然,因为市场上不同颜色油漆的价格不同,所以房子粉刷成不同颜色的花费成本也是不同的。每个房子粉刷成不同颜色的花费是以一个 `n x 3` 的正整数矩阵 `costs` 来表示的。

例如,`costs[0][0]` 表示第 $0$ 号房子粉刷成红色的成本花费;`costs[1][2]` 表示第 $1$ 号房子粉刷成绿色的花费,以此类推。

请计算出粉刷完所有房子最少的花费成本。

示例 1:
```
输入: costs = [[17,2,17],[16,16,5],[14,3,19]]

输出: 10

解释: 将 0 号房子粉刷成蓝色,1 号房子粉刷成绿色,2 号房子粉刷成蓝色。
最少花费: 2 + 5 + 3 = 10。
```
示例 2:
```
输入: costs = [[7,6,2]]

输出: 2
```

提示:
* $costs.length == n$
* $costs[i].length == 3$
* $1 <= n <= 100$
* $1 <= costs[i][j] <= 20$

Solution:
### 状态机 DP

为了方便,我们记 `costs` 为 `cs`。

根据题意,当我们从前往后决策每间房子的颜色时,当前房子所能刷的颜色,取决于上一间房子的颜色。

我们可以定义 $f[i][j]$ 为考虑下标不超过 $i$ 的房子,且最后一间房子颜色为 $j$ 时的最小成本。

起始我们有 $f[0][i] = cs[0][i]$,代表只有第一间房子时,对应成本为第一间房子的上色成本。

然后不失一般性考虑,$f[i][j]$ 该如何计算:$f[i][j]$ 为所有 $f[i - 1][prev]$(其中 $prev \neq j$)中的最小值加上 $cs[i][j]$。

本质上这是一道「状态机 DP」问题:某些状态只能由规则限定的状态所转移,通常我们可以从 $f[i][j]$ 能够更新哪些目标状态(后继状态)进行转移,也能够从 $f[i][j]$ 依赖哪些前置状态(前驱状态)来转移。

一些细节:考虑到我们 $f[i][X]$ 的计算只依赖于 $f[i - 1][X]$,因此我们可以使用三个变量来代替我们的动规数组。

Java 代码:
```Java
class Solution {
    public int minCost(int[][] cs) {
        int n = cs.length;
        int a = cs[0][0], b = cs[0][1], c = cs[0][2];
        for (int i = 1; i < n; i++) {
            int d = Math.min(b, c) + cs[i][0];
            int e = Math.min(a, c) + cs[i][1];
            int f = Math.min(a, b) + cs[i][2];
            a = d; b = e; c = f;
        }
        return Math.min(a, Math.min(b, c));
    }
}
```

Java 代码:

```Java
class Solution {
    public int minCost(int[][] cs) {
        int n = cs.length;
        int a = cs[0][0], b = cs[0][1], c = cs[0][2];
        for (int i = 0; i < n - 1; i++) {
            int d = Math.min(b, c) + cs[i + 1][0];
            int e = Math.min(a, c) + cs[i + 1][1];
            int f = Math.min(a, b) + cs[i + 1][2];
            a = d; b = e; c = f;
        }
        return Math.min(a, Math.min(b, c));
    }
}
```
* 时间复杂度:$O(n \times C)$,其中 $C = 3$ 为颜色数量
* 空间复杂度:$O(1)$

### 最后

这是我们「刷穿 LeetCode」系列文章的第 `剑指 Offer II 091` 篇,系列开始于 2021/01/01,截止于起始日 LeetCode 上共有 1916 道题目,部分是有锁题,我们将先把所有不带锁的题目刷完。

在这个系列文章里面,除了讲解解题思路以外,还会尽可能给出最为简洁的代码。如果涉及通解还会相应的代码模板。

为了方便各位同学能够电脑上进行调试和提交代码,我建立了相关的仓库:https://github.com/SharingSource/LogicStack-LeetCode 。

在仓库地址里,你可以看到系列文章的题解链接、系列文章的相应代码、LeetCode 原题链接和其他优选题解。


LeetCode/剑指 Offer II/剑指 Offer II 114. 外星文字典（困难）.md

Question:
现有一种使用英语字母的外星文语言,这门语言的字母顺序与英语顺序不同。

给定一个字符串列表 `words` ,作为这门语言的词典,`words` 中的字符串已经 按这门新语言的字母顺序进行了排序 。

请你根据该词典还原出此语言中已知的字母顺序,并 按字母递增顺序 排列。若不存在合法字母顺序,返回 `""` 。若存在多种可能的合法字母顺序,返回其中 任意一种 顺序即可。

字符串 `s` 字典顺序小于 字符串 `t` 有两种情况:

在第一个不同字母处,如果 `s` 中的字母在这门外星语言的字母顺序中位于 `t` 中字母之前,那么 `s` 的字典顺序小于 `t` 。
如果前面 `min(s.length, t.length)` 字母都相同,那么 `s.length < t.length` 时,`s` 的字典顺序也小于 `t` 。

示例 1:
```
输入:words = ["wrt","wrf","er","ett","rftt"]

输出:"wertf"
```
示例 2:
```
输入:words = ["z","x"]

输出:"zx"
```
示例 3:
```
输入:words = ["z","x","z"]

输出:""

解释:不存在合法字母顺序,因此返回 "" 。
```

提示:
* $1 <= words.length <= 100$
* $1 <= words[i].length <= 100$
* `words[i]` 仅由小写英文字母组成

Solution:
### 建图 + 拓扑排序

为了方便,我们称 `words` 为 `ws`,同时将两个字符串 `a` 和 `b` 之间的字典序关系简称为「关系」。

由于数组长度和每个 $ws[i]$ 的最大长度均为 $100$,我们可以实现复杂度为 $O(n^3)$ 的算法。

首先容易想到,我们从前往后处理每个 $ws[i]$,利用 `ws` 数组本身已按字典序排序,然后通过 $ws[i]$ 与 $ws[j]$ 的关系(其中 $j$ 的范围为 $[0, i - 1]$),来构建字符之间的关系。

具体的,当我们明确字符 $c1$ 比 $c2$ 字典序要小,可以建立从 $c1$ 到 $c2$ 的有向边,同时统计增加 $c1$ 的出度,增加 $c2$ 的入度。

当建图完成后,我们从所有入度为 $0$ 的点开始(含义为没有比这些字符字典序更小的字符),跑一遍拓扑排序:在 `BFS` 过程中,不断的删点(出队的点可以看做从图中移除)和更新删除点的出边点的入度,若有新的入度为 $0$ 的点,则将其进行入队操作。

> **不了解拓扑排序的同学可以看前置 🧀 : [图论拓扑排序入门](https://mp.weixin.qq.com/s?__biz=MzU4NDE3MTEyMA==&mid=2247489706&idx=1&sn=771cd807f39d1ca545640c0ef7e5baec)**

若最终出队节点数量与总数量 $cnt$ 相等,说明这是一张拓扑图(无环,字符之间不存在字典序冲突),出队的顺序即是字典序,否则存在冲突,返回空串。

代码:
```Java
class Solution {
    int N = 26, M = N * N, idx = 0, cnt = 0;
    int[] he = new int[N], e = new int[M], ne = new int[M];
    int[] in = new int[N], out = new int[N];
    boolean[] vis = new boolean[N];
    void add(int a, int b) {
        e[idx] = b;
        ne[idx] = he[a];
        he[a] = idx++;
        out[a]++; in[b]++;
    }
    public String alienOrder(String[] ws) {
        int n = ws.length;
        Arrays.fill(he, -1);
        for (int i = 0; i < n; i++) {
            for (char c : ws[i].toCharArray()) {
                if (!vis[c - 'a'] && ++cnt >= 0) vis[c - 'a'] = true;
            }
            for (int j = 0; j < i; j++) {
                if (!build(ws[j], ws[i])) return "";
            }
        }
        Deque<Integer> d = new ArrayDeque<>();
        for (int i = 0; i < 26; i++) {
            if (vis[i] && in[i] == 0) d.addLast(i);
        }
        StringBuilder sb = new StringBuilder();
        while (!d.isEmpty()) {
            int u = d.pollFirst();
            sb.append((char)(u + 'a'));
            for (int i = he[u]; i != -1; i = ne[i]) {
                int j = e[i];
                if (--in[j] == 0) d.addLast(j);
            }
        }
        return sb.length() == cnt ? sb.toString() : "";
    }
    boolean build(String a, String b) {
        int n = a.length(), m = b.length(), len = Math.min(n, m);
        for (int i = 0; i < len; i++) {
            int c1 = a.charAt(i) - 'a', c2 = b.charAt(i) - 'a';
            if (c1 != c2) {
                add(c1, c2);
                return true;
            }
        }
        return n <= m;
    }
}
```
* 时间复杂度:令 $n$ 为数组 `ws` 的长度,统计字符数的复杂度为 $O(\sum_{i}^{n - 1} len(ws[i]))$,建图复杂度为 $O(n^3)$;跑拓扑序构建答案复杂度 $O(n^2)$。整体复杂度为 $O(n^3)$
* 空间复杂度:$O(C^2)$,其中 $C = 26$ 为字符集大小

### 最后

这是我们「刷穿 LeetCode」系列文章的第 `剑指 Offer II 114` 篇,系列开始于 2021/01/01,截止于起始日 LeetCode 上共有 1916 道题目,部分是有锁题,我们将先把所有不带锁的题目刷完。

在这个系列文章里面,除了讲解解题思路以外,还会尽可能给出最为简洁的代码。如果涉及通解还会相应的代码模板。

为了方便各位同学能够电脑上进行调试和提交代码,我建立了相关的仓库:https://github.com/SharingSource/LogicStack-LeetCode 。

在仓库地址里,你可以看到系列文章的题解链接、系列文章的相应代码、LeetCode 原题链接和其他优选题解。


LeetCode/剑指 Offer II/剑指 Offer II 115. 重建序列（中等）.md

Question:
给定一个长度为 `n` 的整数数组 `nums` ,其中 `nums` 是范围为 $[1,n]$ 的整数的排列。还提供了一个 `2D` 整数数组 `sequences`,其中 `sequences[i]` 是 `nums` 的子序列。

检查 `nums` 是否是唯一的最短 超序列 。最短 超序列 是 长度最短 的序列,并且所有序列 `sequences[i]` 都是它的子序列。对于给定的数组 `sequences`,可能存在多个有效的 超序列 。

* 例如,对于 `sequences = [[1,2],[1,3]]` ,有两个最短的 超序列,`[1,2,3]` 和 `[1,3,2]` 。
* 而对于 `sequences = [[1,2],[1,3],[1,2,3]]`,唯一可能的最短 超序列 是 `[1,2,3]` 。`[1,2,3,4]` 是可能的超序列,但不是最短的。

如果 `nums` 是序列的唯一最短 超序列 ,则返回 `true` ,否则返回 `false` 。

子序列 是一个可以通过从另一个序列中删除一些元素或不删除任何元素,而不改变其余元素的顺序的序列。

示例 1:
```
输入:nums = [1,2,3], sequences = [[1,2],[1,3]]

输出:false

解释:有两种可能的超序列:[1,2,3]和[1,3,2]。
序列 [1,2] 是[1,2,3]和[1,3,2]的子序列。
序列 [1,3] 是[1,2,3]和[1,3,2]的子序列。
因为 nums 不是唯一最短的超序列,所以返回false。
```
示例 2:
```
输入:nums = [1,2,3], sequences = [[1,2]]

输出:false

解释:最短可能的超序列为 [1,2]。
序列 [1,2] 是它的子序列:[1,2]。
因为 nums 不是最短的超序列,所以返回false。
```
示例 3:
```
输入:nums = [1,2,3], sequences = [[1,2],[1,3],[2,3]]

输出:true

解释:最短可能的超序列为[1,2,3]。
序列 [1,2] 是它的一个子序列:[1,2,3]。
序列 [1,3] 是它的一个子序列:[1,2,3]。
序列 [2,3] 是它的一个子序列:[1,2,3]。
因为 nums 是唯一最短的超序列,所以返回true。
```

提示:
* $n == nums.length$
* $1 <= n <= 104$
* `nums` 是 $[1, n]$ 范围内所有整数的排列
* $1 <= sequences.length <= 10^4$
* $1 <= sequences[i].length <= 104$
* $1 <= sum(sequences[i].length) <= 10^5$
* $1 <= sequences[i][j] <= n$
* `sequences` 的所有数组都是 唯一 的
* `sequences[i]` 是 `nums` 的一个子序列

Solution:
### 拓扑排序 + 构造

为了方便,我们令 `sequences` 为 `ss`。

根据题意,如果我们能够利用所有的 $ss[i]$ 构造出一个唯一的序列,且该序列与 `nums` 相同,则返回 `True`,否则返回 `False`。

将每个 $ss[i]$ 看做对 $ss[i]$ 所包含点的前后关系约束,我们可以将问题转换为拓扑排序问题。

利用所有 $ss[i]$ 构造新图:对于 $ss[i] = [A_1, A_2, ..., A_k]$,我们将其转换为点 $A_1$ -> $A_2$ -> ... -> $A_k$ 的有向图,同时统计每个点的入度情况。

然后在新图上跑一遍拓扑排序,构造对应的拓扑序列,与 `nums` 进行对比。

实现上,由于拓扑排序过程中,出点的顺序即为拓扑序,因此我们并不需要完整保存整个拓扑序,只需使用一个变量 `loc` 来记录当前拓扑序的下标,将出点 $t$ 与 $nums[loc]$ 做比较即可。

在拓扑序过程中若有 $t$ 不等于 $nums[loc]$(构造出来的方案与 `nums` 不同) 或某次拓展过程中发现队列元素不止 $1$ 个(此时可能的原因有 :「起始入度为 $0$ 的点不止一个或存在某些点根本不在 $ss$ 中」或「单次拓展新产生的入度为 $0$ 的点不止一个,即拓扑序不唯一」),则直接返回 `False`,

Java 代码:
```Java
class Solution {
    int N = 10010, M = N, idx;
    int[] he = new int[N], e = new int[M], ne = new int[M], in = new int[N];
    void add(int a, int b) {
        e[idx] = b;
        ne[idx] = he[a];
        he[a] = idx++;
        in[b]++;
    }
    public boolean sequenceReconstruction(int[] nums, int[][] ss) {
        int n = nums.length;
        Arrays.fill(he, -1);
        for (int[] s : ss) {
            for (int i = 1; i < s.length; i++) add(s[i - 1], s[i]);
        }
        Deque<Integer> d = new ArrayDeque<>();
        for (int i = 1; i <= n; i++) {
            if (in[i] == 0) d.addLast(i);
        }
        int loc = 0;
        while (!d.isEmpty()) {
            if (d.size() != 1) return false;
            int t = d.pollFirst();
            if (nums[loc++] != t) return false;
            for (int i = he[t]; i != -1; i = ne[i]) {
                int j = e[i];
                if (--in[j] == 0) d.addLast(j);
            }
        }
        return true;
    }
}
```
TypeScript 代码:
```TypeScript
const N = 10010, M = N
const he: number[] = new Array<number>(N).fill(-1), e = new Array<number>(N).fill(0), ne = new Array<number>(N).fill(0), ind = new Array<number>(N).fill(0);
let idx = 0
function add(a: number, b: number): void {
    e[idx] = b
    ne[idx] = he[a]
    he[a] = idx++
    ind[b]++
}
function sequenceReconstruction(nums: number[], ss: number[][]): boolean {
    he.fill(-1); ind.fill(0)
    idx = 0
    const n = nums.length
    for (const s of ss) {
        for (let i = 1; i < s.length; i++) add(s[i - 1], s[i])
    }
    const stk: number[] = new Array<number>()
    let head = 0, tail = 0
    for (let i = 1; i <= n; i++) {
        if (ind[i] == 0) stk[tail++] = i
    }
    let loc = 0
    while (head < tail) {
        if (tail - head > 1) return false
        const t = stk[head++]
        if (nums[loc++] != t) return false
        for (let i = he[t]; i != -1; i = ne[i]) {
            const j = e[i]
            if (--ind[j] == 0) stk[tail++] = j
        }
    }
    return true
};
```
* 时间复杂度:建图复杂度为 $O(\sum_{i = 0}^{n - 1}ss[i].length)$;跑拓扑排序的复杂度为 $O(n + \sum_{i = 0}^{n - 1}ss[i].length)$。整体复杂度为 $O(n + \sum_{i = 0}^{n - 1}ss[i].length)$
* 空间复杂度: $O(n + \sum_{i = 0}^{n - 1}ss[i].length)$

### 最后

这是我们「刷穿 LeetCode」系列文章的第 `剑指 Offer II 115` 篇,系列开始于 2021/01/01,截止于起始日 LeetCode 上共有 1916 道题目,部分是有锁题,我们将先把所有不带锁的题目刷完。

在这个系列文章里面,除了讲解解题思路以外,还会尽可能给出最为简洁的代码。如果涉及通解还会相应的代码模板。

为了方便各位同学能够电脑上进行调试和提交代码,我建立了相关的仓库:https://github.com/SharingSource/LogicStack-LeetCode 。

在仓库地址里,你可以看到系列文章的题解链接、系列文章的相应代码、LeetCode 原题链接和其他优选题解。


LeetCode/剑指 Offer/剑指 Offer 04. 二维数组中的查找（中等）.md

剑指 Offer 04. 二维数组中的查找

Question:
在一个 $n \times m$ 的二维数组中,每一行都按照从左到右递增的顺序排序,每一列都按照从上到下递增的顺序排序。请完成一个高效的函数,输入这样的一个二维数组和一个整数,判断数组中是否含有该整数。

示例:
```
现有矩阵 matrix 如下:

[
  [1,   4,  7, 11, 15],
  [2,   5,  8, 12, 19],
  [3,   6,  9, 16, 22],
  [10, 13, 14, 17, 24],
  [18, 21, 23, 26, 30]
]

给定 target = 5,返回 true。
给定 target = 20,返回 false。
```

限制:
* $0 <= n <= 1000$
* $0 <= m <= 1000$

Solution:
### 二分

与 [(题解)74. 搜索二维矩阵](https://leetcode-cn.com/problems/search-a-2d-matrix/solution/gong-shui-san-xie-yi-ti-shuang-jie-er-fe-l0pq/) 不同,本题没有确保「每行的第一个整数大于前一行的最后一个整数」,因此我们无法采取「两次二分」的做法。

只能退而求之,遍历行/列,然后再对列/行进行二分。

代码:
```Java
class Solution {
    public boolean searchMatrix(int[][] matrix, int target) {
        if (matrix.length == 0 || matrix[0].length == 0) return false;
        int m = matrix.length, n = matrix[0].length;
        for (int i = 0; i < m; i++) {
            int l = 0, r = n - 1;
            while (l < r) {
                int mid = l + r + 1 >> 1;
                if (matrix[i][mid] <= target) l = mid;
                else r = mid - 1;
            }
            if (matrix[i][r] == target) return true;
        }
        return false;
    }
}
```
-
```Java
class Solution {
    public boolean searchMatrix(int[][] matrix, int target) {
        if (matrix.length == 0 || matrix[0].length == 0) return false;
        int m = matrix.length, n = matrix[0].length;
        for (int i = 0; i < n; i++) {
            int l = 0, r = m - 1;
            while (l < r) {
                int mid = l + r + 1 >> 1;
                if (matrix[mid][i] <= target) l = mid;
                else r = mid - 1;
            }
            if (matrix[r][i] == target) return true;
        }
        return false;
    }
}
```
* 时间复杂度:$O(m\log{n})$ 或 $O(n\log{m})$
* 空间复杂度:$O(1)$

---

### 抽象 BST

该做法则与 [(题解)74. 搜索二维矩阵](https://leetcode-cn.com/problems/search-a-2d-matrix/solution/gong-shui-san-xie-yi-ti-shuang-jie-er-fe-l0pq/) 的「解法二」完全一致。

我们可以将二维矩阵抽象成「以右上角为根的 BST」:

那么我们可以从根(右上角)开始搜索,如果当前的节点不等于目标值,可以按照树的搜索顺序进行:

1. 当前节点「大于」目标值,搜索当前节点的「左子树」,也就是当前矩阵位置的「左方格子」,即 $c$--
2. 当前节点「小于」目标值,搜索当前节点的「右子树」,也就是当前矩阵位置的「下方格子」,即 $r$++

代码:
```Java
class Solution {
    public boolean searchMatrix(int[][] matrix, int target) {
        if (matrix.length == 0 || matrix[0].length == 0) return false;
        int m = matrix.length, n = matrix[0].length;
        int r = 0, c = n - 1;
        while (r < m && c >= 0) {
            if (matrix[r][c] < target) r++;
            else if (matrix[r][c] > target) c--;
            else return true;
        }
        return false;
    }
}
```
* 时间复杂度:$O(m + n)$
* 空间复杂度:$O(1)$

### 最后

这是我们「刷穿 LeetCode」系列文章的第 `剑指 Offer 04` 篇,系列开始于 2021/01/01,截止于起始日 LeetCode 上共有 1916 道题目,部分是有锁题,我们将先把所有不带锁的题目刷完。

在这个系列文章里面,除了讲解解题思路以外,还会尽可能给出最为简洁的代码。如果涉及通解还会相应的代码模板。

为了方便各位同学能够电脑上进行调试和提交代码,我建立了相关的仓库:https://github.com/SharingSource/LogicStack-LeetCode 。

在仓库地址里,你可以看到系列文章的题解链接、系列文章的相应代码、LeetCode 原题链接和其他优选题解。


LeetCode/剑指 Offer/剑指 Offer 06. 从尾到头打印链表（简单）.md

剑指 Offer 06. 从尾到头打印链表

Question:
输入一个链表的头节点,从尾到头反过来返回每个节点的值(用数组返回)。

示例 1:
```
输入:head = [1,3,2]

输出:[2,3,1]
```

限制:
* $0 <= 链表长度 <= 10000$

Solution:
### 迭代

使用「迭代」方式求解是极其容易的。

在遍历链表 `head` 时,使用变长数组将节点值进行转存,随后再利用变长数组来「从后往前」构造定长数组并进行返回即可(其中 `TS`  代码无须额外构造定长数组,直接在原有数组基础上使用「双指针」翻转即可)。

Java 代码:
```Java
class Solution {
    public int[] reversePrint(ListNode head) {
        List<Integer> list = new ArrayList<>();
        while (head != null) {
            list.add(head.val);
            head = head.next;
        }
        int n = list.size();
        int[] ans = new int[n];
        for (int i = 0; i < n; i++) ans[i] = list.get(n - 1 - i);
        return ans;
    }
}
```
TypeScript 代码:
```TypeScript
function reversePrint(head: ListNode | null): number[] {
    const ans = new Array<number>()
    while (head != null) {
        ans.push(head.val)
        head = head.next
    }
    const n = ans.length
    for (let i = 0, j = n - 1; i < j; i++, j--) {
        const c = ans[i]
        ans[i] = ans[j]
        ans[j] = c
    }
    return ans
};
```
* 时间复杂度:$O(n)$
* 空间复杂度:$O(n)$

---

### 递归

使用「递归」求解只需要进行常规的 `DFS`  即可,每次先将当前节点的 `next` 指针进行递归处理,然后再将当前节点值加入数组,即可实现「从后往前」的顺序添加。

Java 代码:
```Java
class Solution {
    public int[] reversePrint(ListNode head) {
        List<Integer> list = new ArrayList<>();
        dfs(head, list);
        int n = list.size();
        int[] ans = new int[n];
        for (int i = 0; i < n; i++) ans[i] = list.get(i);
        return ans;
    }
    void dfs(ListNode head, List<Integer> list) {
        if (head == null) return ;
        dfs(head.next, list);
        list.add(head.val);
    }
}
```
TypeScript 代码:
```TypeScript
function reversePrint(head: ListNode | null): number[] {
    const ans: number[] = new Array<number>()
    dfs(head, ans)
    return ans
};
function dfs(head: ListNode | null, list: number[]) {
    if (head == null) return
    dfs(head.next, list)
    list.push(head.val)
}
```
* 时间复杂度:$O(n)$
* 空间复杂度:$O(n)$

### 最后

这是我们「刷穿 LeetCode」系列文章的第 `剑指 Offer 06` 篇,系列开始于 2021/01/01,截止于起始日 LeetCode 上共有 1916 道题目,部分是有锁题,我们将先把所有不带锁的题目刷完。

在这个系列文章里面,除了讲解解题思路以外,还会尽可能给出最为简洁的代码。如果涉及通解还会相应的代码模板。

为了方便各位同学能够电脑上进行调试和提交代码,我建立了相关的仓库:https://github.com/SharingSource/LogicStack-LeetCode 。

在仓库地址里,你可以看到系列文章的题解链接、系列文章的相应代码、LeetCode 原题链接和其他优选题解。


LeetCode/剑指 Offer/剑指 Offer 10- I. 斐波那契数列（简单）.md

Question:
写一个函数,输入 n ,求斐波那契(Fibonacci)数列的第 n 项(即 F(N))。斐波那契数列的定义如下:

* F(0) = 0,   F(1) = 1
* F(N) = F(N - 1) + F(N - 2), 其中 N > 1.

斐波那契数列由 0 和 1 开始,之后的斐波那契数就是由之前的两数相加而得出。

答案需要取模 1e9+7(1000000007),如计算初始结果为:1000000008,请返回 1。

示例 1:
```
输入:n = 2

输出:1
```
示例 2:
```
输入:n = 5

输出:5
```

提示:
* 0 <= n <= 100

Solution:
### 递推实现动态规划

既然转移方程都给出了,直接根据转移方程从头到尾递递推一遍即可。

代码:
```Java
class Solution {
    int mod = (int)1e9+7;
    public int fib(int n) {
        if (n <= 1) return n;
        int a = 0, b = 1;
        for (int i = 2; i <= n; i++) {
            int c = a + b;
            c %= mod;
            a = b;
            b = c;
        }
        return b;
    }
}
```
* 时间复杂度:$O(n)$
* 空间复杂度:$O(1)$

---

### 递归实现动态规划

能以「递推」形式实现动态规划,自然也能以「递归」的形式实现。

为防止重复计算,我们需要加入「记忆化搜索」功能,同时利用某个值 $x$ 在不同的样例之间可能会作为“中间结果”被重复计算,并且计算结果 $fib(x)$ 固定,我们可以使用 `static` 修饰缓存器,以实现计算过的结果在所有测试样例中共享。

代码:
```Java
class Solution {
    static int mod = (int)1e9+7;
    static int N = 110;
    static int[] cache = new int[N];
    public int fib(int n) {
        if (n <= 1) return n;
        if (cache[n] != 0) return cache[n];
        cache[n] = fib(n - 1) + fib(n - 2);
        cache[n] %= mod;
        return cache[n];
    }
}
```
* 时间复杂度:$O(n)$
* 空间复杂度:$O(1)$

---

### 打表

经过「解法二」,我们进一步发现,可以利用数据范围只有 $100$ 进行打表预处理,然后直接返回。

代码:
```Java
class Solution {
    static int mod = (int)1e9+7;
    static int N = 110;
    static int[] cache = new int[N];
    static {
        cache[1] = 1;
        for (int i = 2; i < N; i++) {
            cache[i] = cache[i - 1] + cache[i - 2];
            cache[i] %= mod;
        }
    }
    public int fib(int n) {
        return cache[n];
    }
}
```
* 时间复杂度:将打表逻辑放到本地执行,复杂度为 $O(1)$;否则为 $O(C)$,$C$ 为常量,固定为 $110$
* 空间复杂度:$O(C)$

---

### 矩阵快速幂

**对于数列递推问题,可以使用矩阵快速幂进行加速,最完整的介绍在 [这里](https://mp.weixin.qq.com/s?__biz=MzU4NDE3MTEyMA==&mid=2247488198&idx=1&sn=8272ca6b0ef6530413da4a270abb68bc&chksm=fd9cb9d9caeb30cf6c2defab0f5204adc158969d64418916e306f6bf50ae0c38518d4e4ba146&token=1067450240&lang=zh_CN#rd) 讲过。**

对于本题,某个 $f(n)$ 依赖于 $f(n - 1)$ 和 $f(n - 2)$,将其依赖的状态存成列向量:

$$
\begin{bmatrix}
f(n - 1)\\
f(n - 2)
\end{bmatrix}
$$

目标值 $f(n)$ 所在矩阵为:

$$
\begin{bmatrix}
f(n)\\
f(n - 1)
\end{bmatrix}
$$

根据矩阵乘法,不难发现:

$$
\begin{bmatrix}
f(n)\\
f(n - 1)
\end{bmatrix}

=

\begin{bmatrix}
1& 1\\
1& 0
\end{bmatrix}

*

\begin{bmatrix}
f(n - 1)\\
f(n - 2)
\end{bmatrix}
$$

我们令:

$$
mat =
\begin{bmatrix}
1& 1\\
1& 0
\end{bmatrix}
$$

起始时,我们只有 $
\begin{bmatrix}
f(1)\\
f(0)
\end{bmatrix}
$,根据递推式得:

$$
\begin{bmatrix}
f(n)\\
f(n - 1)
\end{bmatrix}

=

mat * mat * ... * mat *

\begin{bmatrix}
f(1)\\
f(0)
\end{bmatrix}
$$

再根据矩阵乘法具有「结合律」,最终可得:

$$
\begin{bmatrix}
f(n)\\
f(n - 1)
\end{bmatrix}

=

mat^{n - 1}

*

\begin{bmatrix}
f(1)\\
f(0)
\end{bmatrix}
$$

计算 $mat^{n - 1}$ 可以套用「快速幂」进行求解。

代码:
```Java
class Solution {
    int mod = (int)1e9+7;
    long[][] mul(long[][] a, long[][] b) {
        int r = a.length, c = b[0].length, z = b.length;
        long[][] ans = new long[r][c];
        for (int i = 0; i < r; i++) {
            for (int j = 0; j < c; j++) {
                for (int k = 0; k < z; k++) {
                    ans[i][j] += a[i][k] * b[k][j];
                    ans[i][j] %= mod;
                }
            }
        }
        return ans;
    }
    public int fib(int n) {
        if (n <= 1) return n;
        long[][] mat = new long[][]{
            {1, 1},
            {1, 0}
        };
        long[][] ans = new long[][]{
            {1},
            {0}
        };
        int x = n - 1;
        while (x != 0) {
            if ((x & 1) != 0) ans = mul(mat, ans);
            mat = mul(mat, mat);
            x >>= 1;
        }
        return (int)(ans[0][0] % mod);
    }
}
```
* 时间复杂度:$O(\log{n})$
* 空间复杂度:$O(1)$

### 最后

这是我们「刷穿 LeetCode」系列文章的第 `剑指 Offer 10- I` 篇,系列开始于 2021/01/01,截止于起始日 LeetCode 上共有 1916 道题目,部分是有锁题,我们将先把所有不带锁的题目刷完。

在这个系列文章里面,除了讲解解题思路以外,还会尽可能给出最为简洁的代码。如果涉及通解还会相应的代码模板。

为了方便各位同学能够电脑上进行调试和提交代码,我建立了相关的仓库:https://github.com/SharingSource/LogicStack-LeetCode 。

在仓库地址里,你可以看到系列文章的题解链接、系列文章的相应代码、LeetCode 原题链接和其他优选题解。


LeetCode/剑指 Offer/剑指 Offer 11. 旋转数组的最小数字（简单）.md

剑指 Offer 11. 旋转数组的最小数字

Question:
把一个数组最开始的若干个元素搬到数组的末尾,我们称之为数组的旋转。

给你一个可能存在 重复 元素值的数组 `numbers`,它原来是一个升序排列的数组,并按上述情形进行了一次旋转。请返回旋转数组的最小元素。例如,数组 `[3,4,5,1,2]` 为 `[1,2,3,4,5]` 的一次旋转,该数组的最小值为 $1$。

注意,数组 `[a[0], a[1], a[2], ..., a[n-1]]` 旋转一次 的结果为数组 `[a[n-1], a[0], a[1], a[2], ..., a[n-2]]`。

示例 1:
```
输入:numbers = [3,4,5,1,2]

输出:1
```
示例 2:
```
输入:numbers = [2,2,2,0,1]

输出:0
```

提示:
* $n = numbers.length$
* $1 <= n <= 5000$
* $-5000 <= numbers[i] <= 5000$
* `numbers` 原来是一个升序排序的数组,并进行了 $1$ 至 $n$ 次旋转

Solution:
### 二分

根据题意,我们知道,所谓的旋转其实就是「将某个下标前面的所有数整体移到后面,使得数组从整体有序变为分段有序」。

但和 [153. 寻找旋转排序数组中的最小值](https://leetcode-cn.com/problems/find-minimum-in-rotated-sorted-array/solution/gong-shui-san-xie-yan-ge-olognyi-qi-kan-6d969/) 不同的是,本题元素并不唯一。

**这意味着我们无法直接根据与** $nums[0]$ **的大小关系,将数组划分为两段,即无法通过「二分」来找到旋转点。**

**因为「二分」的本质是二段性,并非单调性。只要一段满足某个性质,另外一段不满足某个性质,就可以用「二分」。**

如果你有看过我 [严格 O(logN),一起看清二分的本质](https://leetcode-cn.com/problems/search-in-rotated-sorted-array/solution/shua-chuan-lc-yan-ge-ologn100yi-qi-kan-q-xifo/) 这篇题解,你应该很容易就理解上句话的意思。如果没有也没关系,我们可以先解决本题,在理解后你再去做 [153. 寻找旋转排序数组中的最小值](https://leetcode-cn.com/problems/find-minimum-in-rotated-sorted-array/),我认为这两题都是一样的,不存在先后关系。

举个🌰,我们使用数据 `[0,1,2,2,2,3,4,5]` 来理解为什么不同的旋转点会导致「二段性丢失」:

java 代码:
```Java
class Solution {
    public int minArray(int[] nums) {
        int n = nums.length;
        int l = 0, r = n - 1;
        while (l < r && nums[0] == nums[r]) r--;
        while (l < r) {
            int mid = l + r + 1 >> 1;
            if (nums[mid] >= nums[0]) l = mid;
            else r = mid - 1;
        }
        return r + 1 < n ? nums[r + 1] : nums[0];
    }
}
```
TypeScript 代码:
```TypeScript
function minArray(nums: number[]): number {
    const n = nums.length
    let l = 0, r = n - 1
    while (l < r && nums[0] == nums[r]) r--
    while (l < r) {
        const mid = l + r + 1 >> 1
        if (nums[mid] >= nums[0]) l = mid
        else r = mid - 1
    }
    return r + 1 < n ? nums[r + 1] : nums[0]
};
```
* 时间复杂度:恢复二段性处理中,最坏的情况下(考虑整个数组都是同一个数)复杂度是 $O(n)$,而之后的找旋转点是「二分」,复杂度为 $O(\log{n})$。整体复杂度为 $O(n)$
* 空间复杂度:$O(1)$

### 最后

这是我们「刷穿 LeetCode」系列文章的第 `剑指 Offer 11` 篇,系列开始于 2021/01/01,截止于起始日 LeetCode 上共有 1916 道题目,部分是有锁题,我们将先把所有不带锁的题目刷完。

在这个系列文章里面,除了讲解解题思路以外,还会尽可能给出最为简洁的代码。如果涉及通解还会相应的代码模板。

为了方便各位同学能够电脑上进行调试和提交代码,我建立了相关的仓库:https://github.com/SharingSource/LogicStack-LeetCode 。

在仓库地址里,你可以看到系列文章的题解链接、系列文章的相应代码、LeetCode 原题链接和其他优选题解。


LeetCode/剑指 Offer/剑指 Offer 15. 二进制中1的个数（简单）.md

剑指 Offer 15. 二进制中1的个数

Question:
请实现一个函数,输入一个整数(以二进制串形式),输出该数二进制表示中 1 的个数。例如,把 9 表示成二进制是 1001,有 2 位是 1。因此,如果输入 9,则该函数输出 2。

示例 1:
```
输入:00000000000000000000000000001011
输出:3
解释:输入的二进制串 00000000000000000000000000001011 中,共有三位为 '1'。
```
示例 2:
```
输入:00000000000000000000000010000000
输出:1
解释:输入的二进制串 00000000000000000000000010000000 中,共有一位为 '1'。
```
示例 3:
```
输入:11111111111111111111111111111101
输出:31
解释:输入的二进制串 11111111111111111111111111111101 中,共有 31 位为 '1'。
```

提示:
* 输入必须是长度为 32 的 二进制串 。

Solution:
### 「位数检查」解法

一个朴素的做法是,对 `int` 的每一位进行检查,并统计 $1$ 的个数。

代码:
```Java
public class Solution {
    public int hammingWeight(int n) {
        int ans = 0;
        for (int i = 0; i < 32; i++) {
            ans += ((n >> i) & 1);
        }
        return ans;
    }
}
```
* 时间复杂度:$O(k)$,$k$ 为 `int` 的位数,固定为 $32$ 位
* 空间复杂度:$O(1)$

***

### 「右移统计」解法

对于方法一,即使 $n$ 的高位均为是 $0$,我们也会对此进行循环检查。

因此另外一个做法是:通过 `n & 1` 来统计当前 $n$ 的最低位是否为 $1$,同时每次直接对 $n$ 进行右移并高位补 0。

当 $n = 0$ 代表,我们已经将所有的 $1$ 统计完成。

这样的做法,可以确保只会循环到最高位的 $1$。

代码:
```Java
public class Solution {
    public int hammingWeight(int n) {
        int ans = 0;
        while (n != 0) {
            ans += (n & 1);
            n >>>= 1;
        }
        return ans;
    }
}
```
* 时间复杂度:$O(k)$,$k$ 为 `int` 的位数,固定为 $32$ 位,最坏情况 $n$ 的二进制表示全是 $1$
* 空间复杂度:$O(1)$

---

### 「lowbit」解法

对于方法二,如果最高位 $1$ 和 最低位 $1$ 之间全是 $0$,我们仍然会诸次右移,直到处理到最高位的 $1$ 为止。

那么是否有办法,只对位数为 $1$ 的二进制位进行处理呢?

使用 `lowbit` 即可做到,`lowbit` 会在 $O(1)$ 复杂度内返回二进制表示中最低位 $1$ 所表示的数值。

例如 $(0000...111100)_2$ 传入 `lowbit` 返回 $(0000...000100)_2$;$(0000...00011)_2$ 传入 `lowbit` 返回 $(0000...00001)_2$ ...

代码:
```Java
public class Solution {
    public int hammingWeight(int n) {
        int ans = 0;
        for (int i = n; i != 0; i -= lowbit(i)) ans++;
        return ans;
    }
    int lowbit(int x) {
        return x & -x;
    }
}
```
* 时间复杂度:$O(k)$,$k$ 为 `int` 的位数,固定为 $32$ 位,最坏情况 $n$ 的二进制表示全是 $1$
* 空间复杂度:$O(1)$

---

### 「分组统计」解法

以上三种解法都是 $O(k)$ 的,事实上我们可以通过分组统计的方式,做到比 $O(k)$ 更低的复杂度。

代码:
```Java
public class Solution {
    public int hammingWeight(int n) {
        n = (n & 0x55555555) + ((n >>> 1)  & 0x55555555);
        n = (n & 0x33333333) + ((n >>> 2)  & 0x33333333);
        n = (n & 0x0f0f0f0f) + ((n >>> 4)  & 0x0f0f0f0f);
        n = (n & 0x00ff00ff) + ((n >>> 8)  & 0x00ff00ff);
        n = (n & 0x0000ffff) + ((n >>> 16) & 0x0000ffff);
        return n;
    }
}
```
* 时间复杂度:$O(\log{k})$,$k$ 为 `int` 的位数,固定为 $32$ 位
* 空间复杂度:$O(1)$

PS. 对于该解法,如果大家学有余力的话,还是建议大家在纸上模拟一下这个过程。如果不想深入,也可以当成模板背过(写法非常固定),但通常如果不是写底层框架,你几乎不会遇到需要一个 $O(\log{k})$ 解法的情况。

而且这个做法的最大作用,不是处理 `int`,而是处理更大位数的情况,在长度只有 $32$ 位的 `int` 的情况下,该做法不一定就比循环要快(该做法会产生多个的中间结果,导致赋值发生多次,而且由于指令之间存在对 $n$ 数值依赖,可能不会被优化为并行指令),这个道理和对于排序元素少的情况下,我们会选择「选择排序」而不是「归并排序」是一样的。

### 最后

这是我们「刷穿 LeetCode」系列文章的第 `剑指 Offer 15` 篇,系列开始于 2021/01/01,截止于起始日 LeetCode 上共有 1916 道题目,部分是有锁题,我们将先把所有不带锁的题目刷完。

在这个系列文章里面,除了讲解解题思路以外,还会尽可能给出最为简洁的代码。如果涉及通解还会相应的代码模板。

为了方便各位同学能够电脑上进行调试和提交代码,我建立了相关的仓库:https://github.com/SharingSource/LogicStack-LeetCode 。

在仓库地址里,你可以看到系列文章的题解链接、系列文章的相应代码、LeetCode 原题链接和其他优选题解。


LeetCode/剑指 Offer/剑指 Offer 20. 表示数值的字符串（中等）.md

剑指 Offer 20. 表示数值的字符串

Question:
请实现一个函数用来判断字符串是否表示数值(包括整数和小数)。

**数值**(按顺序)可以分成以下几个部分:

1. 若干空格
2. 一个 小数 或者 整数
3. (可选)一个 `'e'` 或 `'E'` ,后面跟着一个 整数
4. 若干空格

**小数**(按顺序)可以分成以下几个部分:

1. (可选)一个符号字符(`'+'` 或 `'-'`)
2. 下述格式之一:
	1. 至少一位数字,后面跟着一个点 `'.'`
	2. 至少一位数字,后面跟着一个点 `'.'` ,后面再跟着至少一位数字
	3. 一个点 `'.'` ,后面跟着至少一位数字

**整数**(按顺序)可以分成以下几个部分:

1. (可选)一个符号字符(`'+'` 或 `'-'`)
2. 至少一位数字

部分数值列举如下:
* `["+100", "5e2", "-123", "3.1416", "-1E-16", "0123"]`

部分非数值列举如下:
* `["12e", "1a3.14", "1.2.3", "+-5", "12e+5.4"]`

示例 1:
```
输入:s = "0"

输出:true
```
示例 2:
```
输入:s = "e"

输出:false
```
示例 3:
```
输入:s = "."

输出:false
```
示例 4:
```
输入:s = "    .1  "

输出:true
```

提示:
* $1 <= s.length <= 20$
* `s` 仅含英文字母(大写和小写),数字(`0-9`),加号 `'+'`,减号 `'-'` ,空格 `' '` 或者点 `'.'` 。

Solution:
### 模拟

我们对题面中的「数值」、「小数」和「整数」进行重新定义。

* 整数:可以有 `'+'` 或 `'-'`,但不能同时存在;且至少有一个数字
* 小数:可以有 `'+'` 或 `'-'`,但不能同时存在;有 `.`,且 `.` 的两边至少有一个数字
* 数值:前后可以有连续段的空格,其余位置则不能有;可以有 `E/e`,但最多只能有一个,同时 `E/e` 分割的左边可以是「整数」或「小数」,右边则只能是「整数」

根据上面的重新定义,再来设计我们的基本处理流程:

* 将 `s` 两端的连续段空格进行去除,得到不该含有空格的核心串,若核心串为空,则返回 `False`;
* 将所有的小写 `e` 切换为 `E`
* 从前往后,找到第一个 `E` 的所在位置 `idx`,根据是否有 `E` 进行分情况讨论:
  * 若没有 `E`(即 `idx = n`),判断整个核心串是否为「整数」或「小数」
  * 若有 `E`(即 `idx < n`),判断以 `E` 为分割的左半部分是否为「整数」或「小数」,判断以 `E` 为分割的右半部分是否「整数」

Java 代码:

```Java
class Solution {
    public boolean isNumber(String s) {
        int n = s.length(), l = 0, r = n - 1;
        while (l < n && s.charAt(l) == ' ') l++;
        while (r >= 0 && s.charAt(r) == ' ') r--;
        if (l > r) return false;
        s = s.substring(l, r + 1).replace('e', 'E');
        n = s.length();
        int idx = 0;
        while (idx < n && s.charAt(idx) != 'E') idx++;
        if (idx == n) {
            return check(s, true);
        } else {
            return check(s.substring(0, idx), true) && check(s.substring(idx + 1), false);
        }
    }
    boolean check(String s, boolean isDecimal) {
        if (s.equals(".") || s.equals("")) return false;
        int n = s.length();
        for (int i = 0, cnt = 0; i < n; i++) {
            char c = s.charAt(i);
            if (c == '+' || c == '-') {
                if (i != 0 || i == n - 1) return false;
            } else if (c == '.') {
                if (!isDecimal) return false;
                if (cnt != 0) return false;
                boolean a = i - 1 >= 0 && Character.isDigit(s.charAt(i - 1));
                boolean b = i + 1 < n && Character.isDigit(s.charAt(i + 1));
                if (!(a || b)) return false;
                cnt++;
            } else if (!Character.isDigit(c)) return false;
        }
        return true;
    }
}
```
C++ 代码:
```C++
class Solution {
public:
    bool isNumber(string s) {
        int n = s.length(), l = 0, r = n - 1;
        while (l < n && s[l] == ' ') l++;
        while (r >= 0 && s[r] == ' ') r--;
        if (l > r) return false;
        s = s.substr(l, r - l + 1);
        for (char& c : s) {
            if (c == 'e') c = 'E';
        }
        n = s.length();
        int idx = 0;
        while (idx < n && s[idx] != 'E') idx++;
        if (idx == n) {
            return check(s, true);
        } else {
            return check(s.substr(0, idx), true) && check(s.substr(idx + 1), false);
        }
    }
    bool check(string s, bool isDecimal) {
        if (s == "." || s == "") return false;
        int n = s.length();
        for (int i = 0, cnt = 0; i < n; i++) {
            char c = s[i];
            if (c == '+' || c == '-') {
                if (i != 0 || i == n - 1) return false;
            } else if (c == '.') {
                if (!isDecimal) return false;
                if (cnt != 0) return false;
                bool a = i - 1 >= 0 && isdigit(s[i - 1]);
                bool b = i + 1 < n && isdigit(s[i + 1]);
                if (!(a || b)) return false;
                cnt++;
            } else if (!isdigit(c)) return false;
        }
        return true;
    }
};
```
Python 代码:
```Python
class Solution:
    def isNumber(self, s: str) -> bool:
        n, l, r = len(s), 0, len(s) - 1
        while l < n and s[l] == ' ':
            l += 1
        while r >= 0 and s[r] == ' ':
            r -= 1
        if l > r:
            return False
        s = s[l:r + 1].replace('e', 'E')
        n, idx = len(s), 0
        while idx < n and s[idx] != 'E':
            idx += 1
        if idx == n:
            return self.check(s, True)
        else:
            return self.check(s[:idx], True) and self.check(s[idx + 1:], False)

    def check(self, s: str, is_decimal: bool) -> bool:
        if s == "." or s == "":
            return False
        n, cnt = len(s), 0
        for i in range(n):
            c = s[i]
            if c == '+' or c == '-':
                if i != 0 or i == n - 1:
                    return False
            elif c == '.':
                if not is_decimal:
                    return False
                if cnt != 0:
                    return False
                a = i - 1 >= 0 and s[i - 1].isdigit()
                b = i + 1 < n and s[i + 1].isdigit()
                if not (a or b):
                    return False
                cnt += 1
            elif not c.isdigit():
                return False
        return True
```
TypeScript 代码:
```TypeScript
function isNumber(s: string): boolean {
    let n = s.length, l = 0, r = n - 1;
    while (l < n && s.charAt(l) === ' ') l++;
    while (r >= 0 && s.charAt(r) === ' ') r--;
    if (l > r) return false;
    s = s.substring(l, r + 1).replace(/e/g, 'E');
    n = s.length;
    let idx = 0;
    while (idx < n && s.charAt(idx) !== 'E') idx++;
    if (idx === n) {
        return check(s, true);
    } else {
        return check(s.substring(0, idx), true) && check(s.substring(idx + 1), false);
    }
}

function check(s: string, isDecimal: boolean): boolean {
    if (s === '.' || s === '') return false;
    const n = s.length;
    for (let i = 0, cnt = 0; i < n; i++) {
        const c = s.charAt(i);
        if (c === '+' || c === '-') {
            if (i !== 0 || i === n - 1) return false;
        } else if (c === '.') {
            if (!isDecimal) return false;
            if (cnt !== 0) return false;
            const a = i - 1 >= 0 && /\d/.test(s.charAt(i - 1));
            const b = i + 1 < n && /\d/.test(s.charAt(i + 1));
            if (!(a || b)) return false;
            cnt++;
        } else if (!/\d/.test(c)) return false;
    }
    return true;
}
```
* 时间复杂度:$O(n)$
* 空间复杂度:$O(1)$

### 最后

这是我们「刷穿 LeetCode」系列文章的第 `剑指 Offer 20` 篇,系列开始于 2021/01/01,截止于起始日 LeetCode 上共有 1916 道题目,部分是有锁题,我们将先把所有不带锁的题目刷完。

在这个系列文章里面,除了讲解解题思路以外,还会尽可能给出最为简洁的代码。如果涉及通解还会相应的代码模板。

为了方便各位同学能够电脑上进行调试和提交代码,我建立了相关的仓库:https://github.com/SharingSource/LogicStack-LeetCode 。

在仓库地址里,你可以看到系列文章的题解链接、系列文章的相应代码、LeetCode 原题链接和其他优选题解。


LeetCode/剑指 Offer/剑指 Offer 22. 链表中倒数第k个节点（简单）.md

剑指 Offer 22. 链表中倒数第k个节点

Question:
输入一个链表,输出该链表中倒数第k个节点。为了符合大多数人的习惯,本题从1开始计数,即链表的尾节点是倒数第1个节点。

例如,一个链表有 6 个节点,从头节点开始,它们的值依次是 1、2、3、4、5、6。这个链表的倒数第 3 个节点是值为 4 的节点。

示例:
```
给定一个链表: 1->2->3->4->5, 和 k = 2.

返回链表 4->5.
```

Solution:
### 栈/队列 解法

一个使用额外空间的解法是利用栈(队列),将所有的节点压入占中栈(队列)中,令当前栈(队列)容量为 $cnt$。

然后从栈顶(队列头)弹出 $k$ 个($cnt - k + 1$ 个)元素,最后一个出栈(出队列)的元素即是答案。

代码(栈):
```Java
class Solution {
    public ListNode getKthFromEnd(ListNode head, int k) {
        Deque<ListNode> d = new ArrayDeque<>();
        while (head != null) {
            d.addLast(head);
            head = head.next;
        }
        ListNode ans = null;
        while (k-- > 0) ans = d.pollLast();
        return ans;
    }
}
```
代码(队列):
```Java
class Solution {
    public ListNode getKthFromEnd(ListNode head, int k) {
        Deque<ListNode> d = new ArrayDeque<>();
        while (head != null) {
            d.addLast(head);
            head = head.next;
        }
        k = d.size() - k + 1;
        ListNode ans = null;
        while (k-- > 0) ans = d.pollFirst();
        return ans;
    }
}
```
* 时间复杂度:$O(n)$
* 空间复杂度:$O(n)$

---

### 差值法

我们可以先对链表进行一次完整遍历,拿到总长度 $cnt$,最后由 $cnt - k$ 即是倒数第 $k$ 个节点距离 $head$ 节点的距离。

代码:
```Java
class Solution {
    public ListNode getKthFromEnd(ListNode head, int k) {
        int cnt = 0;
        ListNode tmp = head;
        while (tmp != null && ++cnt > 0) tmp = tmp.next;
        cnt -= k;
        while (cnt-- > 0) head = head.next;
        return head;
    }
}
```
* 时间复杂度:$O(n)$
* 空间复杂度:$O(1)$

---

### 快慢指针

事实上,我们还可以使用「快慢指针」进行求解。

起始先让快指针 `fast` 先走 $k$ 步,此时 `fast` 和 `slow` 之间距离为 $k$,之后让 `fast` 和 `slow` 指针一起走(始终维持相对距离为 $k$),当 `fast` 到达链表尾部,`slow` 即停在倒数第 $k$ 个节点处。

代码:
```Java
class Solution {
    public ListNode getKthFromEnd(ListNode head, int k) {
        ListNode slow = head, fast = head;
        while (k-- > 0) fast = fast.next;
        while (fast != null) {
            slow = slow.next;
            fast = fast.next;
        }
        return slow;
    }
}
```
* 时间复杂度:$O(n)$
* 空间复杂度:$O(1)$

### 最后

这是我们「刷穿 LeetCode」系列文章的第 `No.剑指 Offer 22. 链表中倒数第k个节点` 篇,系列开始于 2021/01/01,截止于起始日 LeetCode 上共有 1916 道题目,部分是有锁题,我们将先把所有不带锁的题目刷完。

在这个系列文章里面,除了讲解解题思路以外,还会尽可能给出最为简洁的代码。如果涉及通解还会相应的代码模板。

为了方便各位同学能够电脑上进行调试和提交代码,我建立了相关的仓库:https://github.com/SharingSource/LogicStack-LeetCode 。

在仓库地址里,你可以看到系列文章的题解链接、系列文章的相应代码、LeetCode 原题链接和其他优选题解。


LeetCode/剑指 Offer/剑指 Offer 24. 反转链表（简单）.md

Question:
定义一个函数,输入一个链表的头节点,反转该链表并输出反转后链表的头节点。

示例:
```
输入: 1->2->3->4->5->NULL

输出: 5->4->3->2->1->NULL
```

限制:
* $0 <= 节点个数 <= 5000$

Solution:
### 迭代

迭代做法只需要使用两个指针即可,复用 `head` 变量来代指当前处理到的节点,使用 `prev` 变量代指原链表中 `head` 的上一节点,只需使用循环构造出所有的 `head.next = prev` 关系即可。

Java 代码:
```Java
class Solution {
    public ListNode reverseList(ListNode head) {
        ListNode prev = null;
        while (head != null) {
            ListNode temp = head.next;
            head.next = prev;
            prev = head;
            head = temp;
        }
        return prev;
    }
}
```
TypeScript 代码:
```TypeScript
function reverseList(head: ListNode | null): ListNode | null {
    let prev = null
    while (head != null) {
        const temp = head.next
        head.next = prev
        prev = head
        head = temp
    }
    return prev
};
```
* 时间复杂度:$O(n)$
* 空间复杂度:$O(1)$

---

### 递归

相对而言,递归方法需要一定的抽象能力:将 `reverseList` 作为我们的递归函数,函数的含义为将以 `head` 为头节点进行翻转,并返回新的头节点。

不失一般性考虑某个递归回合:当传入节点 `head` 为空,或者其 `next` 节点为空(即只有单个节点)时,我们 直接返回 `head` 即可。而对于剩余情况,对于某个节点 `head` 而言,我们可以先递归处理它的剩余部分 `reverseList(head.next)` ,此时我们拿到的返回值既是以 `head.next` 为头结点进行翻转后的新链表头结点,也是以 `head` 为头结点进行翻转后的新链表头结点,将其记为 `ans`。我们只需要关注原有 `head` 部分如何处理,即将原来的 `head -> head.next` 关系进行翻转,则有 `head.next.next = head`,并 `head.next` 指向空即可,最后返回 `ans`。

Java 代码:
```Java
class Solution {
    public ListNode reverseList(ListNode head) {
        if (head == null || head.next == null) return head;
        ListNode ans = reverseList(head.next);
        head.next.next = head;
        head.next = null;
        return ans;
    }
}
```
TypeScript 代码:
```TypeScript
function reverseList(head: ListNode | null): ListNode | null {
    if (head == null || head.next == null) return head
    const ans = reverseList(head.next)
    head.next.next = head
    head.next = null
    return ans
};
```
* 时间复杂度:$O(n)$
* 空间复杂度:忽略递归带来的额外空间开销,复杂度为 $O(1)$

### 最后

这是我们「刷穿 LeetCode」系列文章的第 `No.剑指 Offer 24` 篇,系列开始于 2021/01/01,截止于起始日 LeetCode 上共有 1916 道题目,部分是有锁题,我们将先把所有不带锁的题目刷完。

在这个系列文章里面,除了讲解解题思路以外,还会尽可能给出最为简洁的代码。如果涉及通解还会相应的代码模板。

为了方便各位同学能够电脑上进行调试和提交代码,我建立了相关的仓库:https://github.com/SharingSource/LogicStack-LeetCode 。

在仓库地址里,你可以看到系列文章的题解链接、系列文章的相应代码、LeetCode 原题链接和其他优选题解。


LeetCode/剑指 Offer/剑指 Offer 25. 合并两个排序的链表（简单）.md

剑指 Offer 25. 合并两个排序的链表

Question:
输入两个递增排序的链表,合并这两个链表并使新链表中的节点仍然是递增排序的。

示例1:
```
输入:1->2->4, 1->3->4

输出:1->1->2->3->4->4
```

限制:
* $0 <= 链表长度 <= 1000$

Solution:
### 迭代 - 二路归并

使用「迭代」方式求解的话,本质与合并两个有序数组并无区别。

创建一个虚拟头结点 `dummy` 来拼接合并后的链表,`cur` 代表当前合并链表的末尾位置,同时复用入参的 `l1` 和 `l2` 来进行构造。

当 `l1` 和 `l2` 均不为空的时候,找到两者中的较小值拼接到 `cur ` 的后面,若 `l1` 和 `l2` 任一为空,则将另外的链表拼接完。

Java 代码:
```Java
class Solution {
    public ListNode mergeTwoLists(ListNode l1, ListNode l2) {
        ListNode dummy = new ListNode(-1), cur = dummy;
        while (l1 != null || l2 != null) {
            if (l1 != null && l2 != null) {
                if (l1.val < l2.val) {
                    cur.next = l1;
                    cur = cur.next; l1 = l1.next;
                } else {
                    cur.next = l2;
                    cur = cur.next; l2 = l2.next;
                }
            } else if (l1 != null) {
                cur.next = l1;
                cur = cur.next; l1 = l1.next;
            } else {
                cur.next = l2;
                cur = cur.next; l2 = l2.next;
            }
        }
        return dummy.next;
    }
}
```
TypeScript 代码:
```TypeScript
function mergeTwoLists(l1: ListNode | null, l2: ListNode | null): ListNode | null {
    let dummy = new ListNode(-1), cur = dummy
    while (l1 != null || l2 != null) {
        if (l1 != null && l2 != null) {
            if (l1.val < l2.val) {
                cur.next = l1
                cur = cur.next; l1 = l1.next
            } else {
                cur.next = l2
                cur = cur.next; l2 = l2.next
            }
        } else if (l1 != null) {
            cur.next = l1
            cur = cur.next; l1 = l1.next
        } else {
            cur.next = l2
            cur = cur.next; l2 = l2.next
        }
    }
    return dummy.next
};
```
* 时间复杂度:$O(n + m)$
* 空间复杂度:$O(1)$

---

### 递归

另一种实现方式是使用「递归」进行构造。

直接将 `mergeTwoLists` 作为递归函数,该函数的定义为传入两个「有序」链表,并返回合并链表的头结点。

显然当 `l1` 或者 `l2` 任一为空时,返回另一链表即可。

而对于其余一般情况而言,根据 `l1` 和 `l2` 的节点值分情况讨论:

* 若 `l1.val < l2.val`,说明此时 `l1` 必然是当前构造回合中值最小的节点,也就是需要被返回的头结点,而 `l1` 后接的内容应该是 `mergeTwoLists(l1.next, l2)`,含义 `l1` 后接的内容应该是由有序链表 `l1.next` 和 `l2` 合并而来;
* 其余情况 `l1.val >= l2.val`,分析同理,此时有 `l2.next = mergeTwoLists(l2.next, l1)`,同时返回 `l2`。

Java 代码:
```Java
class Solution {
    public ListNode mergeTwoLists(ListNode l1, ListNode l2) {
        if (l1 == null) return l2;
        else if (l2 == null) return l1;
        else if (l1.val < l2.val) {
            l1.next = mergeTwoLists(l1.next, l2);
            return l1;
        } else {
            l2.next = mergeTwoLists(l2.next, l1);
            return l2;
        }
    }
}
```
TypeScript 代码:
```TypeScript
function mergeTwoLists(l1: ListNode | null, l2: ListNode | null): ListNode | null {
    if (l1 == null) return l2
    else if (l2 == null) return l1
    else if (l1.val < l2.val) {
        l1.next = mergeTwoLists(l1.next, l2)
        return l1
    } else {
        l2.next = mergeTwoLists(l2.next, l1)
        return l2
    }
};
```
* 时间复杂度:$O(n + m)$
* 空间复杂度:忽略递归带来的额外空间开销,复杂度为 $O(1)$

### 最后

这是我们「刷穿 LeetCode」系列文章的第 `No.剑指 Offer 25` 篇,系列开始于 2021/01/01,截止于起始日 LeetCode 上共有 1916 道题目,部分是有锁题,我们将先把所有不带锁的题目刷完。

在这个系列文章里面,除了讲解解题思路以外,还会尽可能给出最为简洁的代码。如果涉及通解还会相应的代码模板。

为了方便各位同学能够电脑上进行调试和提交代码,我建立了相关的仓库:https://github.com/SharingSource/LogicStack-LeetCode 。

在仓库地址里,你可以看到系列文章的题解链接、系列文章的相应代码、LeetCode 原题链接和其他优选题解。


LeetCode/剑指 Offer/剑指 Offer 32 - I. 从上到下打印二叉树（中等）.md

剑指 Offer 32 - I. 从上到下打印二叉树

Question:
从上到下打印出二叉树的每个节点,同一层的节点按照从左到右的顺序打印。

例如:

给定二叉树: `[3,9,20,null,null,15,7]`,
```
    3
   / \
  9  20
    /  \
   15   7
```
返回:
```
[3,9,20,15,7]
```

提示:
* `节点总数 <= 1000`

Solution:
### 迭代 - BFS

使用「迭代」进行求解是容易的,只需使用常规的 `BFS` 方法进行层序遍历即可。

Java 代码:
```Java
class Solution {
    public int[] levelOrder(TreeNode root) {
        List<Integer> list = new ArrayList<>();
        Deque<TreeNode> d = new ArrayDeque<>();
        if (root != null) d.addLast(root);
        while (!d.isEmpty()) {
            TreeNode t = d.pollFirst();
            list.add(t.val);
            if (t.left != null) d.addLast(t.left);
            if (t.right != null) d.addLast(t.right);
        }
        int n = list.size();
        int[] ans = new int[n];
        for (int i = 0; i < n; i++) ans[i] = list.get(i);
        return ans;
    }
}
```
TypeScript 代码:
```TypeScript
function levelOrder(root: TreeNode | null): number[] {
    let he = 0, ta = 0
    const ans: number[] = new Array<number>()
    const d: TreeNode[] = new Array<TreeNode>()
    if (root != null) d[ta++] = root
    while (he < ta) {
        const t = d[he++]
        ans.push(t.val)
        if (t.left != null) d[ta++] = t.left
        if (t.right != null) d[ta++] = t.right
    }
    return ans
};
```
* 时间复杂度:$O(n)$
* 空间复杂度:$O(n)$

---

### 递归 - DFS

使用「递归」来进行「层序遍历」虽然不太符合直观印象,但也是可以的。

此时我们需要借助「哈希表」来存储起来每一层的节点情况。

首先我们按照「中序遍历」的方式进行 `DFS`,同时在 `DFS` 过程中传递节点所在的深度(`root` 节点默认在深度最小的第 $0$ 层),每次处理当前节点时,通过哈希表获取所在层的数组,并将当前节点值追加到数组尾部,同时维护一个最大深度 `max`,在 `DFS` 完成后,再使用深度范围 $[0, max]$ 从哈希表中进行构造答案。

Java 代码:
```Java
class Solution {
    Map<Integer, List<Integer>> map = new HashMap<>();
    int max = -1, cnt = 0;
    public int[] levelOrder(TreeNode root) {
        dfs(root, 0);
        int[] ans = new int[cnt];
        for (int i = 0, idx = 0; i <= max; i++) {
            for (int x : map.get(i)) ans[idx++] = x;
        }
        return ans;
    }
    void dfs(TreeNode root, int depth) {
        if (root == null) return ;
        max = Math.max(max, depth);
        cnt++;
        dfs(root.left, depth + 1);
        List<Integer> list = map.getOrDefault(depth, new ArrayList<Integer>());
        list.add(root.val);
        map.put(depth, list);
        dfs(root.right, depth + 1);
    }
}
```
TypeScript 代码:
```TypeScript
const map:Map<number, Array<number>> = new Map<number, Array<number>>()
let max = -1, cnt = 0
function levelOrder(root: TreeNode | null): number[] {
    map.clear()
    max = -1; cnt = 0;
    dfs(root, 0)
    const ans: Array<number> = new Array<number>()
    for (let i = 0; i <= max; i++) {
        for (const x of map.get(i)) ans.push(x)
    }
    return ans
};
function dfs(root: TreeNode | null, depth: number): void {
    if (root == null) return
    max = Math.max(max, depth)
    cnt++
    dfs(root.left, depth + 1)
    if (!map.has(depth)) map.set(depth, new Array<number>())
    map.get(depth).push(root.val)
    dfs(root.right, depth + 1)
}
```
* 时间复杂度:$O(n)$
* 空间复杂度:$O(n)$

### 最后

这是我们「刷穿 LeetCode」系列文章的第 `No.剑指 Offer 32 - I` 篇,系列开始于 2021/01/01,截止于起始日 LeetCode 上共有 1916 道题目,部分是有锁题,我们将先把所有不带锁的题目刷完。

在这个系列文章里面,除了讲解解题思路以外,还会尽可能给出最为简洁的代码。如果涉及通解还会相应的代码模板。

为了方便各位同学能够电脑上进行调试和提交代码,我建立了相关的仓库:https://github.com/SharingSource/LogicStack-LeetCode 。

在仓库地址里,你可以看到系列文章的题解链接、系列文章的相应代码、LeetCode 原题链接和其他优选题解。


LeetCode/剑指 Offer/剑指 Offer 32 - II. 从上到下打印二叉树 II（简单）.md

Question:
从上到下按层打印二叉树,同一层的节点按从左到右的顺序打印,每一层打印到一行。

例如:
给定二叉树: `[3,9,20,null,null,15,7]`,
```
    3
   / \
  9  20
    /  \
   15   7
```
返回其层次遍历结果:
```
[
  [3],
  [9,20],
  [15,7]
]
```

提示:
* `节点总数 <= 1000`

Solution:
### 迭代 - BFS

这道题是 [(题解)剑指 Offer 32 - I. 从上到下打印二叉树](https://leetcode.cn/problems/cong-shang-dao-xia-da-yin-er-cha-shu-lcof/solution/by-ac_oier-a9n5/) 的练习版本。

只需要在每次 `BFS` 拓展时,将完整的层进行取出,存如独立数组后再加入答案。

Java 代码:
```Java
class Solution {
    public List<List<Integer>> levelOrder(TreeNode root) {
        Deque<TreeNode> d = new ArrayDeque<>();
        if (root != null) d.addLast(root);
        List<List<Integer>> ans = new ArrayList<>();
        while (!d.isEmpty()) {
            int sz = d.size();
            List<Integer> list = new ArrayList<>();
            while (sz-- > 0) {
                TreeNode t = d.pollFirst();
                list.add(t.val);
                if (t.left != null) d.addLast(t.left);
                if (t.right != null) d.addLast(t.right);
            }
            ans.add(list);
        }
        return ans;
    }
}
```
Typescript 代码:
```Typescript
function levelOrder(root: TreeNode | null): number[][] {
    const ans: number[][] = new Array<Array<number>>()
    const stk: TreeNode[] = new Array<TreeNode>()
    let he = 0, ta = 0
    if (root != null) stk[ta++] = root
    while (he < ta) {
        const temp = new Array<number>()
        let sz = ta - he
        while (sz-- > 0) {
            const t = stk[he++]
            temp.push(t.val)
            if (t.left != null) stk[ta++] = t.left
            if (t.right != null) stk[ta++] = t.right
        }
        ans.push(temp)
    }
    return ans
};
```
* 时间复杂度:$O(n)$
* 空间复杂度:$O(n)$

---

### 递归 - DFS

同理,可以使用「递归」来进行「层序遍历」。

此时我们需要借助「哈希表」来存储起来每一层的节点情况。

首先我们按照「中序遍历」的方式进行 `DFS`,同时在 `DFS` 过程中传递节点所在的深度(`root` 节点默认在深度最小的第 $0$ 层),每次处理当前节点时,通过哈希表获取所在层的数组,并将当前节点值追加到数组尾部,同时维护一个最大深度 `max`,在 `DFS` 完成后,再使用深度范围 $[0, max]$ 从哈希表中进行构造答案。

Java 代码:
```Java
class Solution {
    Map<Integer, List<Integer>> map = new HashMap<>();
    int max = -1;
    public List<List<Integer>> levelOrder(TreeNode root) {
        dfs(root, 0);
        List<List<Integer>> ans = new ArrayList<>();
        for (int i = 0; i <= max; i++) ans.add(map.get(i));
        return ans;
    }
    void dfs(TreeNode root, int depth) {
        if (root == null) return ;
        max = Math.max(max, depth);
        dfs(root.left, depth + 1);
        List<Integer> list = map.getOrDefault(depth, new ArrayList<>());
        list.add(root.val);
        map.put(depth, list);
        dfs(root.right, depth + 1);
    }
}
```
TypeScript 代码:
```TypeScript
const map: Map<number, Array<number>> = new Map<number, Array<number>>()
let max = -1
function levelOrder(root: TreeNode | null): number[][] {
    map.clear()
    max = -1
    dfs(root, 0)
    const ans = new Array<Array<number>>()
    for (let i = 0; i <= max; i++) ans.push(map.get(i))
    return ans
};
function dfs(root: TreeNode | null, depth: number): void {
    if (root == null) return
    max = Math.max(max, depth)
    dfs(root.left, depth + 1)
    if (!map.has(depth)) map.set(depth, new Array<number>())
    map.get(depth).push(root.val)
    dfs(root.right, depth + 1)
}
```
* 时间复杂度:$O(n)$
* 空间复杂度:$O(n)$

### 最后

这是我们「刷穿 LeetCode」系列文章的第 `No.剑指 Offer 32 - II` 篇,系列开始于 2021/01/01,截止于起始日 LeetCode 上共有 1916 道题目,部分是有锁题,我们将先把所有不带锁的题目刷完。

在这个系列文章里面,除了讲解解题思路以外,还会尽可能给出最为简洁的代码。如果涉及通解还会相应的代码模板。

为了方便各位同学能够电脑上进行调试和提交代码,我建立了相关的仓库:https://github.com/SharingSource/LogicStack-LeetCode 。

在仓库地址里,你可以看到系列文章的题解链接、系列文章的相应代码、LeetCode 原题链接和其他优选题解。


LeetCode/剑指 Offer/剑指 Offer 32 - III. 从上到下打印二叉树 III（中等）.md

Question:
请实现一个函数按照之字形顺序打印二叉树,即第一行按照从左到右的顺序打印,第二层按照从右到左的顺序打印,第三行再按照从左到右的顺序打印,其他行以此类推。

例如:

给定二叉树: `[3,9,20,null,null,15,7]`,
```
    3
   / \
  9  20
    /  \
   15   7
```
返回其层次遍历结果:
```
[
  [3],
  [20,9],
  [15,7]
]
```

提示:
* `节点总数 <= 1000`

Solution:
### 迭代 - BFS

这题相比于前两道二叉树打印题目,增加了打印方向的要求。

在 `BFS` 过程中,入队我们可以仍然采用「左子节点优先入队」进行,而在出队构造答案时,我们则要根据当前所在层数来做判别:对于所在层数为偶数(`root` 节点在第 $0$ 层),我们按照「出队添加到尾部」的方式进行;对于所在层数为奇数,我们按照「出队添加到头部」的方式进行。

> 为支持「从尾部追加元素」和「从头部追加元素」操作,`Java` 可使用基于链表的 `LinkedList`,而 `TS` 可创建定长数组后通过下标赋值。

其中判断当前所在层数,无须引用额外变量,直接根据当前 `ans` 的元素大小即可。

Java 代码:
```Java
class Solution {
    public List<List<Integer>> levelOrder(TreeNode root) {
        List<List<Integer>> ans = new ArrayList<>();
        Deque<TreeNode> d = new ArrayDeque<>();
        if (root != null) d.addLast(root);
        while (!d.isEmpty()) {
            LinkedList<Integer> list = new LinkedList<>();
            int sz = d.size(), dirs = ans.size() % 2;
            while (sz-- > 0) {
                TreeNode t = d.pollFirst();
                if (dirs == 0) list.addLast(t.val);
                else list.addFirst(t.val);
                if (t.left != null) d.addLast(t.left);
                if (t.right != null) d.addLast(t.right);
            }
            ans.add(list);
        }
        return ans;
    }
}
```
Typescript 代码:
```Typescript
function levelOrder(root: TreeNode | null): number[][] {
    const ans = new Array<Array<number>>()
    const stk = new Array<TreeNode>()
    let he = 0, ta = 0
    if (root != null) stk[ta++] = root
    while (he < ta) {
        const dirs = ans.length % 2 == 0
        let sz = ta - he, idx = dirs ? 0 : sz - 1
        const temp = new Array<number>(sz)
        while (sz-- > 0) {
            const t = stk[he++]
            temp[idx] = t.val
            idx += dirs ? 1 : -1
            if (t.left != null) stk[ta++] = t.left
            if (t.right != null) stk[ta++] = t.right
        }
        ans.push(temp)
    }
    return ans
};
```
* 时间复杂度:$O(n)$
* 空间复杂度:$O(n)$

---

### 递归 - DFS

递归的实现方式与前两题同理。

不过对于 `TS`  语言来说,由于 `DFS` 过程中无法知道当前层有多少节点,因此只能在使用「哈希表」记录每层「从左往右」的方向,然后在构造答案时,运用「双指针」来将奇数层的节点进行翻转。

Java 代码:
```Java
class Solution {
    Map<Integer, LinkedList<Integer>> map = new HashMap<>();
    int max = -1;
    public List<List<Integer>> levelOrder(TreeNode root) {
        dfs(root, 0);
        List<List<Integer>> ans = new ArrayList<>();
        for (int i = 0; i <= max; i++) ans.add(map.get(i));
        return ans;
    }
    void dfs(TreeNode root, int depth) {
        if (root == null) return ;
        max = Math.max(max, depth);
        dfs(root.left, depth + 1);
        LinkedList<Integer> list = map.getOrDefault(depth, new LinkedList<>());
        if (depth % 2 == 0) list.addLast(root.val);
        else list.addFirst(root.val);
        map.put(depth, list);
        dfs(root.right, depth + 1);
    }
}
```
TypeScript 代码:
```TypeScript
const map: Map<number, Array<number>> = new Map<number, Array<number>> ()
let max = -1
function levelOrder(root: TreeNode | null): number[][] {
    map.clear()
    max = -1
    dfs(root, 0)
    const ans = new Array<Array<number>>()
    for (let i = 0; i <= max; i++) {
        const temp = map.get(i)
        if (i % 2 == 1) {
            for (let p = 0, q = temp.length - 1; p < q; p++, q--) {
                const c = temp[p]
                temp[p] = temp[q]
                temp[q] = c
            }
        }
        ans.push(temp)
    }
    return ans
};
function dfs(root: TreeNode | null, depth: number): void {
    if (root == null) return
    max = Math.max(max, depth)
    dfs(root.left, depth + 1)
    if (!map.has(depth)) map.set(depth, new Array<number>())
    map.get(depth).push(root.val)
    dfs(root.right, depth + 1)
}
```
* 时间复杂度:$O(n)$
* 空间复杂度:$O(n)$

### 最后

这是我们「刷穿 LeetCode」系列文章的第 `No.剑指 Offer 32 - III` 篇,系列开始于 2021/01/01,截止于起始日 LeetCode 上共有 1916 道题目,部分是有锁题,我们将先把所有不带锁的题目刷完。

在这个系列文章里面,除了讲解解题思路以外,还会尽可能给出最为简洁的代码。如果涉及通解还会相应的代码模板。

为了方便各位同学能够电脑上进行调试和提交代码,我建立了相关的仓库:https://github.com/SharingSource/LogicStack-LeetCode 。

在仓库地址里,你可以看到系列文章的题解链接、系列文章的相应代码、LeetCode 原题链接和其他优选题解。


LeetCode/剑指 Offer/剑指 Offer 34. 二叉树中和为某一值的路径（中等）.md

剑指 Offer 34. 二叉树中和为某一值的路径

Question:
给你二叉树的根节点 `root` 和一个整数目标和 `targetSum`,找出所有 从根节点到叶子节点 路径总和等于给定目标和的路径。

叶子节点 是指没有子节点的节点。

示例 1:

```
输入:root = [5,4,8,11,null,13,4,7,2,null,null,5,1], targetSum = 22

输出:[[5,4,11,2],[5,8,4,5]]
```
示例 2:

```
输入:root = [1,2,3], targetSum = 5

输出:[]
```
示例 3:
```
输入:root = [1,2], targetSum = 0

输出:[]
```

提示:
* 树中节点总数在范围 $[0, 5000]$ 内
* $-1000 <= Node.val <= 1000$
* $-1000 <= targetSum <= 1000$

Solution:
### DFS

较为直观的做法是使用 `DFS`,在 `DFS`  过程中记录路径以及路径对应的元素和,当出现元素和为 `target`,且到达了叶子节点,说明找到了一条满足要求的路径,将其加入答案。

使用 `DFS` 的好处是在记录路径的过程中可以使用「回溯」的方式进行记录及回退,而无须时刻进行路径数组的拷贝。

Java 代码:
```Java
class Solution {
    List<List<Integer>> ans = new ArrayList<>();
    int t;
    public List<List<Integer>> pathSum(TreeNode root, int target) {
        t = target;
        dfs(root, 0, new ArrayList<>());
        return ans;
    }
    void dfs(TreeNode root, int cur, List<Integer> list) {
        if (root == null) return ;
        list.add(root.val);
        if (cur + root.val == t && root.left == null && root.right == null) ans.add(new ArrayList<>(list));
        dfs(root.left, cur + root.val, list);
        dfs(root.right, cur + root.val, list);
        list.remove(list.size() - 1);
    }
}
```
TypeScript 代码:
```TypeScript
let ans: number[][]
let t
function pathSum(root: TreeNode | null, target: number): number[][] {
    ans = new Array<Array<number>>()
    t = target
    dfs(root, 0, new Array<number>())
    return ans
};
function dfs(root: TreeNode | null, cur: number, list: Array<number>): void {
    if (root == null) return
    list.push(root.val)
    if (cur + root.val == t && root.left == null && root.right == null) ans.push(list.slice())
    dfs(root.left, cur + root.val, list)
    dfs(root.right, cur + root.val, list)
    list.pop()
}
```
* 时间复杂度:最坏情况所有路径均为合法路径,复杂度为 $O(n \times h)$
* 空间复杂度:最坏情况所有路径均为合法路径,复杂度为 $O(n \times h)$

---

### BFS

使用 `BFS` 的话,我们需要封装一个类/结构体 `TNode`,该结构体存储所对应的原始节点 `node`,到达 `node` 所经过的路径 `list`,以及对应的路径和 `tot`。

由于 `BFS` 过程并非按照路径进行(即相邻出队的节点并非在同一路径),因此我们每次创建新的 `TNode` 对象时,需要对路径进行拷贝操作。

Java 代码:
```Java
class Solution {
    class Node {
        TreeNode node;
        List<Integer> list;
        int tot;
        Node (TreeNode _node, List<Integer> _list, int _tot) {
            node = _node; list = new ArrayList<>(_list); tot = _tot;
            list.add(node.val); tot += node.val;
        }
    }
    public List<List<Integer>> pathSum(TreeNode root, int target) {
        List<List<Integer>> ans = new ArrayList<>();
        Deque<Node> d = new ArrayDeque<>();
        if (root != null) d.addLast(new Node(root, new ArrayList<>(), 0));
        while (!d.isEmpty()) {
            Node t = d.pollFirst();
            if (t.tot == target && t.node.left == null && t.node.right == null) ans.add(t.list);
            if (t.node.left != null) d.addLast(new Node(t.node.left, t.list, t.tot));
            if (t.node.right != null) d.addLast(new Node(t.node.right, t.list, t.tot));
        }
        return ans;
    }
}
```
Typescript 代码:
```Typescript
class TNode {
    node: TreeNode
    tot: number
    list: Array<number>
    constructor(node: TreeNode, tot: number, list: Array<number>) {
        this.node = node; this.tot = tot; this.list = list.slice();
        this.list.push(node.val)
        this.tot += node.val
    }
}
function pathSum(root: TreeNode | null, target: number): number[][] {
    const ans = new Array<Array<number>>()
    const stk = new Array<TNode>()
    let he = 0, ta = 0
    if (root != null) stk[ta++] = new TNode(root, 0, new Array<number>())
    while (he < ta) {
        const t = stk[he++]
        if (t.tot == target && t.node.left == null && t.node.right == null) ans.push(t.list)
        if (t.node.left != null) stk[ta++] = new TNode(t.node.left, t.tot, t.list)
        if (t.node.right != null) stk[ta++] = new TNode(t.node.right, t.tot, t.list)
    }
    return ans
};
```
* 时间复杂度:最坏情况所有路径均为合法路径,复杂度为 $O(n \times h)$
* 空间复杂度:最坏情况所有路径均为合法路径,复杂度为 $O(n \times h)$

### 最后

这是我们「刷穿 LeetCode」系列文章的第 `No.剑指 Offer 34` 篇,系列开始于 2021/01/01,截止于起始日 LeetCode 上共有 1916 道题目,部分是有锁题,我们将先把所有不带锁的题目刷完。

在这个系列文章里面,除了讲解解题思路以外,还会尽可能给出最为简洁的代码。如果涉及通解还会相应的代码模板。

为了方便各位同学能够电脑上进行调试和提交代码,我建立了相关的仓库:https://github.com/SharingSource/LogicStack-LeetCode 。

在仓库地址里,你可以看到系列文章的题解链接、系列文章的相应代码、LeetCode 原题链接和其他优选题解。