LeetCode/剑指 Offer/剑指 Offer 35. 复杂链表的复制（中等）.md

剑指 Offer 35. 复杂链表的复制

Question:
给你一个长度为 `n` 的链表,每个节点包含一个额外增加的随机指针 `random`,该指针可以指向链表中的任何节点或空节点。

构造这个链表的 深拷贝。 深拷贝应该正好由 `n` 个 全新 节点组成,其中每个新节点的值都设为其对应的原节点的值。新节点的 `next` 指针和 `random` 指针也都应指向复制链表中的新节点,并使原链表和复制链表中的这些指针能够表示相同的链表状态。复制链表中的指针都不应指向原链表中的节点 。

例如,如果原链表中有 `X` 和 `Y` 两个节点,其中 `X.random --> Y` 。那么在复制链表中对应的两个节点 `x` 和 `y` ,同样有 `x.random --> y` 。

返回复制链表的头节点。

用一个由 `n` 个节点组成的链表来表示输入/输出中的链表。每个节点用一个 `[val, random_index]` 表示:

* `val`:一个表示 `Node.val` 的整数。
* `random_index`:随机指针指向的节点索引(范围从 $0$ 到 $n-1$);如果不指向任何节点,则为  `null` 。

你的代码 只 接受原链表的头节点 `head` 作为传入参数。

示例 1:

```
输入:head = [[7,null],[13,0],[11,4],[10,2],[1,0]]

输出:[[7,null],[13,0],[11,4],[10,2],[1,0]]
```
示例 2:

```
输入:head = [[1,1],[2,1]]

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

```
输入:head = [[3,null],[3,0],[3,null]]

输出:[[3,null],[3,0],[3,null]]
```
示例 4:
```
输入:head = []

输出:[]

解释:给定的链表为空(空指针),因此返回 null。
```

提示:
* $0 <= n <= 1000$
* $-10000 <= Node.val <= 10000$

Solution:
### 模拟 + 哈希表

如果不考虑 `random` 指针的话,对一条链表进行拷贝,我们只需要使用两个指针:一个用于遍历原链表,一个用于构造新链表(始终指向新链表的尾部)即可。这一步操作可看做是「创建节点 + 构建 `next` 指针关系」。

现在在此基础上增加一个 `random` 指针,我们可以将 `next` 指针和 `random` 指针关系的构建拆开进行:

1. 先不考虑 `random` 指针,和原本的链表复制一样,创建新新节点,并构造 `next` 指针关系,同时使用「哈希表」记录原节点和新节点的映射关系;
2. 对原链表和新链表进行同时遍历,对于原链表的每个节点上的 `random` 都通过「哈希表」找到对应的新 `random` 节点,并在新链表上构造 `random` 关系。

代码:
```Java
class Solution {
    public Node copyRandomList(Node head) {
        Node t = head;
        Node dummy = new Node(-10010), cur = dummy;
        Map<Node, Node> map = new HashMap<>();
        while (head != null) {
            Node node = new Node(head.val);
            map.put(head, node);
            cur.next = node;
            cur = cur.next; head = head.next;
        }
        cur = dummy.next; head = t;
        while (head != null) {
            cur.random = map.get(head.random);
            cur = cur.next; head = head.next;
        }
        return dummy.next;
    }
}
```
* 时间复杂度:$O(n)$
* 空间复杂度:$O(n)$

---

### 模拟(原地算法)

显然时间复杂度上无法优化,考虑如何降低空间(不使用「哈希表」)。

我们使用「哈希表」的目的为了实现原节点和新节点的映射关系,更进一步的是为了快速找到某个节点 `random` 在新链表的位置。

那么我们可以利用原链表的 `next` 做一个临时中转,从而实现映射。

具体的,我们可以按照如下流程进行:

1. 对原链表的每个节点节点进行复制,并追加到原节点的后面;
2. 完成 $1$ 操作之后,链表的奇数位置代表了原链表节点,链表的偶数位置代表了新链表节点,且每个原节点的 `next` 指针执行了对应的新节点。这时候,我们需要构造新链表的 `random` 指针关系,可以利用 `link[i + 1].random = link[i].random.next`,$i$ 为奇数下标,含义为 **新链表节点的 `random` 指针指向旧链表对应节点的 `random` 指针的下一个值**;
3. 对链表进行拆分操作。

代码:
```Java
class Solution {
    public Node copyRandomList(Node head) {
        if (head == null) return head;
        Node t = head;
        while (head != null) {
            Node node = new Node(head.val);
            node.next = head.next;
            head.next = node;
            head = node.next;
        }
        head = t;
        while (head != null) {
            if (head.random != null) head.next.random = head.random.next;
            head = head.next.next;
        }
        head = t;
        Node ans = head.next;
        while (head != null) {
            Node ne = head.next;
            if (ne != null) head.next = ne.next;
            head = ne;
        }
        return ans;
    }
}
```
* 时间复杂度:$O(n)$
* 空间复杂度:$O(1)$

### 最后

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

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

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

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


LeetCode/剑指 Offer/剑指 Offer 37. 序列化二叉树（困难）.md

剑指 Offer 37. 序列化二叉树

Question:
序列化是将一个数据结构或者对象转换为连续的比特位的操作,进而可以将转换后的数据存储在一个文件或者内存中,同时也可以通过网络传输到另一个计算机环境,采取相反方式重构得到原数据。

请设计一个算法来实现二叉树的序列化与反序列化。这里不限定你的序列 / 反序列化算法执行逻辑,你只需要保证一个二叉树可以被序列化为一个字符串并且将这个字符串反序列化为原始的树结构。

提示: 输入输出格式与 LeetCode 目前使用的方式一致,详情请参阅 LeetCode 序列化二叉树的格式。你并非必须采取这种方式,你也可以采用其他的方法解决这个问题。

示例 1:

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

提示:
* 树中结点数在范围 $[0, 10^4]$ 内
* -1000 <= Node.val <= 1000

Solution:
### 基本思路

无论使用何种「遍历方式」进行二叉树存储,为了方便,我们都需要对空节点有所表示。

其实题目本身的样例就给我们提供了很好的思路:**使用层序遍历的方式进行存储,对于某个叶子节点的空节点进行存储,同时确保不递归存储空节点对应的子节点。**

---

### 层序遍历

根据节点值的数据范围 `-1000 <= Node.val <= 1000`(我是在 [297. 二叉树的序列化与反序列化](https://leetcode-cn.com/problems/serialize-and-deserialize-binary-tree/) 看的,你也可以不使用数字,使用某个特殊字符进行表示,只要能在反序列时有所区分即可),我们可以建立占位节点 `emptyNode` 用来代指空节点(`emptyNode.val = INF`)。

序列化:先特判掉空树的情况,之后就是常规的层序遍历逻辑:

1. 起始时,将 `root` 节点入队;
2. 从队列中取出节点,检查节点是否有左/右节点:
    * 如果有的话,将值追加序列化字符中(注意使用分隔符),并将节点入队;
    * 如果没有,检查当前节点是否为 `emptyNode` 节点,如果不是 `emptyNode` 说明是常规的叶子节点,需要将其对应的空节点进行存储,即将 `emptyNode` 入队;
3. 循环流程 $2$,直到整个队列为空。

反序列:同理,怎么「序列化」就怎么进行「反序列」即可:

1. 起始时,构造出 `root` 并入队;
2. 每次从队列取出元素时,同时从序列化字符中截取两个值(对应左右节点),检查是否为 `INF`,若不为 `INF` 则构建对应节点;
3. 循环流程 $2$,直到整个序列化字符串被处理完(注意跳过最后一位分隔符)。

代码:
```Java
public class Codec {
    int INF = -2000;
    TreeNode emptyNode = new TreeNode(INF);
    public String serialize(TreeNode root) {
        if (root == null) return "";

        StringBuilder sb = new StringBuilder();
        Deque<TreeNode> d = new ArrayDeque<>();
        d.addLast(root);
        while (!d.isEmpty()) {
            TreeNode poll = d.pollFirst();
            sb.append(poll.val + "_");
            if (!poll.equals(emptyNode)) {
                d.addLast(poll.left != null ? poll.left : emptyNode);
                d.addLast(poll.right != null ? poll.right : emptyNode);
            }
        }
        return sb.toString();
    }

    public TreeNode deserialize(String data) {
        if (data.equals("")) return null;

        String[] ss = data.split("_");
        int n = ss.length;
        TreeNode root = new TreeNode(Integer.parseInt(ss[0]));
        Deque<TreeNode> d = new ArrayDeque<>();
        d.addLast(root);
        for (int i = 1; i < n - 1; i += 2) {
            TreeNode poll = d.pollFirst();
            int a = Integer.parseInt(ss[i]), b = Integer.parseInt(ss[i + 1]);
            if (a != INF) {
                poll.left = new TreeNode(a);
                d.addLast(poll.left);
            }
            if (b != INF) {
                poll.right = new TreeNode(b);
                d.addLast(poll.right);
            }
        }
        return root;
    }
}
```
* 时间复杂度:$O(n)$
* 空间复杂度:$O(n)$

### 最后

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

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

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

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


LeetCode/剑指 Offer/剑指 Offer 38. 字符串的排列（中等）.md

剑指 Offer 38. 字符串的排列

Question:
输入一个字符串,打印出该字符串中字符的所有排列。

你可以以任意顺序返回这个字符串数组,但里面不能有重复元素。

示例:
```
输入:s = "abc"
输出:["abc","acb","bac","bca","cab","cba"]
```

限制:
* 1 <= s 的长度 <= 8

Solution:
### 基本分析

根据题意,我们需要返回所有不重复的排列字符串。

此类问题通常都是使用 `DFS` 来做:**决策当前字符串的某一位填入什么字符。**

设计如下 `DFS` 函数:

```Java
/**
 *   cs : 原字符串
 *    u : 当前决策到目标字符串的哪一位
 *  cur : 当前目标字符串
 */
void dfs(char[] cs, int u, String cur);
```

而对于去重通常有两种方式:

1. 使用 `Set` 实现去重;
2. 先对原字符串进行排序,然后确保相同字符传入同一目标位置的动作只发生一次。

---

### 回溯(Set 去重)

使用 `HashSet` 实现去重十分简单,就是不用考虑去重问题。

直接决策目标字符串哪一位取哪个字符即可,同时使用布尔数组记录哪些字符已使用。

代码:
```Java
class Solution {
    int N = 10;
    Set<String> set = new HashSet<>();
    boolean[] vis = new boolean[N];
    public String[] permutation(String s) {
        char[] cs = s.toCharArray();
        dfs(cs, 0, "");
        String[] ans = new String[set.size()];
        int idx = 0;
        for (String str : set) ans[idx++] = str;
        return ans;
    }
    void dfs(char[] cs, int u, String cur) {
        int n = cs.length;
        if (u == n) {
            set.add(cur);
            return;
        }
        for (int i = 0; i < n; i++) {
            if (!vis[i]) {
                vis[i] = true;
                dfs(cs, u + 1, cur + String.valueOf(cs[i]));
                vis[i] = false;
            }
        }
    }
}
```
* 时间复杂度:$O(n * n!)$
* 空间复杂度:$O(n)$

---

### 回溯(排序去重)

想要通过预处理实现去重,需要「**先对原字符串进行排序,然后确保相同字符传入同一目标位置的动作只发生一次**」。

具体的,我们可以在「决策目标字符串的第 $u$ 个位置」时,对于 `cs[i-1] = cs[i]` 的情况进行跳过,同时为了确保这个动作不会影响到后面的位置决策,需要利用同一目标位置会进行“回溯”的特性,增加一个 `!vis[i - 1]` 的判断。

这样对于形如 `...xxx...` 的原字符串,使用字符 `x` 决策目标字符串的第 $u$ 个位置的动作只会发生一次,从而确保了一模一样的分支不会在「递归树」中展开多次。

此类去重方式在很多方式出现过,事实上这种去重方式不仅仅能应用于排列问题。

其本质是对递归树中「状态完全相同」的节点进行跳过,从而实现去重。

这里的「状态完全相同」是指:**当前形成部分结果为 $cur$ 相同,同时剩余字符集合也相同**。

代码:
```Java
class Solution {
    int N = 10;
    List<String> list = new ArrayList<>();
    boolean[] vis = new boolean[N];
    public String[] permutation(String s) {
        char[] cs = s.toCharArray();
        Arrays.sort(cs);
        dfs(cs, 0, "");
        String[] ans = new String[list.size()];
        int idx = 0;
        for (String str : list) ans[idx++] = str;
        return ans;
    }
    void dfs(char[] cs, int u, String cur) {
        int n = cs.length;
        if (u == n) {
            list.add(cur);
            return;
        }
        for (int i = 0; i < n; i++) {
            if (i > 0 && !vis[i - 1] && cs[i] == cs[i - 1]) continue;
            if (!vis[i]) {
                vis[i] = true;
                dfs(cs, u + 1, cur + String.valueOf(cs[i]));
                vis[i] = false;
            }
        }
    }
}
```
* 时间复杂度:$O(n * n!)$
* 空间复杂度:$O(n)$

### 最后

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

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

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

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


LeetCode/剑指 Offer/剑指 Offer 41. 数据流中的中位数（困难）.md

剑指 Offer 41. 数据流中的中位数

Question:
中位数是有序列表中间的数。如果列表长度是偶数,中位数则是中间两个数的平均值。

例如,

* `[2,3,4]` 的中位数是 $3$
* `[2,3]` 的中位数是 $(2 + 3) / 2 = 2.5$

设计一个支持以下两种操作的数据结构:
* `void addNum(int num)` - 从数据流中添加一个整数到数据结构中。
* `double findMedian()` - 返回目前所有元素的中位数。

示例:
```
addNum(1)
addNum(2)
findMedian() -> 1.5
addNum(3)
findMedian() -> 2
```

进阶:
* 如果数据流中所有整数都在 $0$ 到 $100$ 范围内,你将如何优化你的算法?
* 如果数据流中 `99%` 的整数都在 $0$ 到 $100$ 范围内,你将如何优化你的算法?

Solution:
### 优先队列(堆)

这是一道经典的数据结构运用题。

具体的,**我们可以使用两个优先队列(堆)来维护整个数据流数据,令维护数据流左半边数据的优先队列(堆)为 `l`,维护数据流右半边数据的优先队列(堆)为 `r`**。

显然,**为了可以在 $O(1)$ 的复杂度内取得当前中位数,我们应当令 `l` 为大根堆,`r` 为小根堆,并人为固定 `l` 和 `r` 之前存在如下的大小关系**:

* **当数据流元素数量为偶数:`l` 和 `r` 大小相同,此时动态中位数为两者堆顶元素的平均值;**
* **当数据流元素数量为奇数:`l` 比 `r` 多一,此时动态中位数为 `l` 的堆顶原数。**

为了满足上述说的奇偶性堆大小关系,在进行 `addNum` 时,我们应当分情况处理:

* 插入前两者大小相同,说明插入前数据流元素个数为偶数,插入后变为奇数。我们期望操作完达到「`l` 的数量为 `r` 多一,同时双堆维持有序」,进一步分情况讨论:
    * 如果 `r` 为空,说明当前插入的是首个元素,直接添加到 `l` 即可;
    * 如果 `r` 不为空,且 `num <= r.peek()`,说明 `num` 的插入位置不会在后半部分(不会在 `r` 中),直接加到 `l` 即可;
    * 如果 `r` 不为空,且 `num > r.peek()`,说明 `num` 的插入位置在后半部分,此时将 `r` 的堆顶元素放到 `l` 中,再把 `num` 放到 `r`(相当于从 `r` 中置换一位出来放到 `l` 中)。

* 插入前两者大小不同,说明前数据流元素个数为奇数,插入后变为偶数。我们期望操作完达到「`l` 和 `r` 数量相等,同时双堆维持有序」,进一步分情况讨论(此时 `l` 必然比 `r` 元素多一):
    * 如果 `num >= l.peek()`,说明 `num` 的插入位置不会在前半部分(不会在 `l` 中),直接添加到 `r` 即可。
    * 如果 `num < l.peek()`,说明 `num` 的插入位置在前半部分,此时将 `l` 的堆顶元素放到 `r` 中,再把 `num` 放入 `l` 中(相等于从 `l` 中替换一位出来当到 `r` 中)。

代码:
```Java
class MedianFinder {
    PriorityQueue<Integer> l = new PriorityQueue<>((a,b)->b-a);
    PriorityQueue<Integer> r = new PriorityQueue<>((a,b)->a-b);
    public void addNum(int num) {
        int s1 = l.size(), s2 = r.size();
        if (s1 == s2) {
            if (r.isEmpty() || num <= r.peek()) {
                l.add(num);
            } else {
                l.add(r.poll());
                r.add(num);
            }
        } else {
            if (l.peek() <= num) {
                r.add(num);
            } else {
                r.add(l.poll());
                l.add(num);
            }
        }
    }
    public double findMedian() {
        int s1 = l.size(), s2 = r.size();
        if (s1 == s2) {
            return (l.peek() + r.peek()) / 2.0;
        } else {
            return l.peek();
        }
    }
}
```
* 时间复杂度:`addNum` 函数的复杂度为 $O(\log{n})$;`findMedian` 函数的复杂度为 $O(1)$
* 空间复杂度:$O(n)$

---

### 进阶

* 如果数据流中所有整数都在 $0$ 到 $100$ 范围内,你将如何优化你的算法?

可以使用建立长度为 $101$ 的桶,每个桶分别统计每个数的出现次数,同时记录数据流中总的元素数量,每次查找中位数时,先计算出中位数是第几位,从前往后扫描所有的桶得到答案。

**这种做法相比于对顶堆做法,计算量上没有优势,更多的是空间上的优化。**

**对顶堆解法两个操作中耗时操作复杂度为 $O(\log{n})$,$\log$ 操作常数不会超过 $3$,在极限数据 $10^7$ 情况下计算量仍然低于耗时操作复杂度为 $O(C)$($C$ 固定为 $101$)桶计数解法。**

* 如果数据流中 `99%` 的整数都在 $0$ 到 $100$ 范围内,你将如何优化你的算法?

和上一问解法类似,对于 $1$% 采用哨兵机制进行解决即可,在常规的最小桶和最大桶两侧分别维护一个有序序列,即建立一个代表负无穷和正无穷的桶。

上述两个进阶问题的代码如下,但注意由于真实样例的数据分布不是进阶所描述的那样(不是绝大多数都在 $[0,100]$ 范围内),所以会 `TLE`。

代码:
```Java
class MedianFinder {
    TreeMap<Integer, Integer> head = new TreeMap<>(), tail = new TreeMap<>();
    int[] arr = new int[101];
    int a, b, c;
    public void addNum(int num) {
        if (num >= 0 && num <= 100) {
            arr[num]++;
            b++;
        } else if (num < 0) {
            head.put(num, head.getOrDefault(num, 0) + 1);
            a++;
        } else if (num > 100) {
            tail.put(num, tail.getOrDefault(num, 0) + 1);
            c++;
        }
    }
    public double findMedian() {
        int size = a + b + c;
        if (size % 2 == 0) return (find(size / 2) + find(size / 2 + 1)) / 2.0;
        return find(size / 2 + 1);
    }
    int find(int n) {
        if (n <= a) {
            for (int num : head.keySet()) {
                n -= head.get(num);
                if (n <= 0) return num;
            }
        } else if (n <= a + b) {
            n -= a;
            for (int i = 0; i <= 100; i++) {
                n -= arr[i];
                if (n <= 0) return i;
            }
        } else {
            n -= a + b;
            for (int num : tail.keySet()) {
                n -= tail.get(num);
                if (n <= 0) return num;
            }
        }
        return -1; // never
    }
}
```

### 最后

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

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

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

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


LeetCode/剑指 Offer/剑指 Offer 42. 连续子数组的最大和（简单）.md

剑指 Offer 42. 连续子数组的最大和

Question:
输入一个整型数组,数组中的一个或连续多个整数组成一个子数组。求所有子数组的和的最大值。

要求时间复杂度为$O(n)$。

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

输出: 6

解释: 连续子数组 [4,-1,2,1] 的和最大,为 6。
```

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

Solution:
### 动态规划

这是一道简单线性 DP 题。

定义 $f[i]$ 为考虑以 $nums[i]$ 为结尾的子数组的最大值。

不失一般性的考虑 $f[i]$ 如何转移。

显然对于 $nums[i]$ 而言,以它为结尾的子数组分两种情况:

* $num[i]$ 自身作为独立子数组:$f[i] = nums[i]$ ;
* $num[i]$ 与之前的数值组成子数组,由于是子数组,其只能接在 $nums[i - 1]$,即有:$f[i] = f[i - 1] + nums[i]$。

最终 $f[i]$ 为上述两种情况取 $\max$ 即可:

$$
f[i] = \max(nums[i], f[i - 1] + nums[i])
$$

代码:
```Java
class Solution {
    public int maxSubArray(int[] nums) {
        int n = nums.length;
        int[] f = new int[n];
        f[0] = nums[0];
        int ans = f[0];
        for (int i = 1; i < n; i++) {
            f[i] = Math.max(nums[i], f[i - 1] + nums[i]);
            ans = Math.max(ans, f[i]);
        }
        return ans;
    }
}
```
* 时间复杂度:$O(n)$
* 空间复杂度:$O(n)$

---

### 空间优化

观察状态转移方程,我们发现 $f[i]$ 明确值依赖于 $f[i - 1]$。

因此我们可以使用「有限变量」或者「滚动数组」的方式,将空间优化至 $O(1)$。

代码:
```Java
class Solution {
    public int maxSubArray(int[] nums) {
        int n = nums.length;
        int max = nums[0], ans = max;
        for (int i = 1; i < n; i++) {
            max = Math.max(nums[i], max + nums[i]);
            ans = Math.max(ans, max);
        }
        return ans;
    }
}
```
```Java
class Solution {
    public int maxSubArray(int[] nums) {
        int n = nums.length;
        int[] f = new int[2];
        f[0] = nums[0];
        int ans = f[0];
        for (int i = 1; i < n; i++) {
            int a = i & 1, b = (i - 1) & 1;
            f[a] = Math.max(nums[i], f[b] + nums[i]);
            ans = Math.max(ans, f[a]);
        }
        return ans;
    }
}
```
* 时间复杂度:$O(n)$
* 空间复杂度:$O(1)$

---

### 拓展

一个有意思的拓展是,将 **加法** 替换成 **乘法**。

题目变成 [152. 乘积最大子数组(中等)](https://leetcode-cn.com/problems/maximum-product-subarray/)。

又该如何考虑呢?

一个朴素的想法,仍然是考虑定义 $f[i]$ 代表以 $nums[i]$ 为结尾的最大值,但存在「负负得正」取得最大值的情况,光维护一个前缀最大值显然是不够的,我们可以多引入一维 $g[i]$ 作为前缀最小值。

其余分析与本题同理。

代码:
```Java
class Solution {
    public int maxProduct(int[] nums) {
        int n = nums.length;
        int[] g = new int[n + 1]; // 考虑前 i 个,结果最小值
        int[] f = new int[n + 1]; // 考虑前 i 个,结果最大值
        g[0] = 1;
        f[0] = 1;
        int ans = nums[0];
        for (int i = 1; i <= n; i++) {
            int x = nums[i - 1];
            g[i] = Math.min(x, Math.min(g[i - 1] * x, f[i - 1] * x));
            f[i] = Math.max(x, Math.max(g[i - 1] * x, f[i - 1] * x));
            ans = Math.max(ans, f[i]);
        }
        return ans;
    }
}
```
```Java
class Solution {
    public int maxProduct(int[] nums) {
        int n = nums.length;
        int min = 1, max = 1;
        int ans = nums[0];
        for (int i = 1; i <= n; i++) {
            int x = nums[i - 1];
            int nmin = Math.min(x, Math.min(min * x, max * x));
            int nmax = Math.max(x, Math.max(min * x, max * x));
            min = nmin;
            max = nmax;
            ans = Math.max(ans, max);
        }
        return ans;
    }
}
```

### 最后

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

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

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

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


LeetCode/剑指 Offer/剑指 Offer 44. 数字序列中某一位的数字（中等）.md

剑指 Offer 44. 数字序列中某一位的数字

Question:
数字以 `0123456789101112131415...` 的格式序列化到一个字符序列中。在这个序列中,第 $5$ 位(从下标 $0$ 开始计数)是 $5$,第 $13$ 位是 $1$,第 $19$ 位是 $4$,等等。

请写一个函数,求任意第 $n$ 位对应的数字。

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

输出:3
```
示例 2:
```
输入:n = 11

输出:0
```

限制:
* $0 <= n < 2^31$

Solution:
### 模拟

我们知道,对于长度为 $len$ 的数字的范围为 $[10^{len - 1}, 10^{len} - 1]$(共 $9 \times 10^{len - 1}$ 个),总长度为:

$$
L = len \times 9 \times 10^{len - 1}
$$

因此我们可以先对 $n$ 进行不断试减(更新 $n$),确定下来目标数字 $x$ 的长度为多少,假设为 $len$。

然后直接计算出长度 $len$ 的最小值为 $s = 10^{len - 1}$,由于范围内的数长度都是 $len$,因此我们可以直接定位到目标数字 $x$ 为何值。

根据目标值 $x$ 必然满足关系式:

$$
(x - s + 1) \times len \geq n
$$

变形可得:

$$
x \geq \left \lfloor \frac{n}{len} \right \rfloor - 1 + s
$$

对 $n$ 进行最后一次的试减(更新 $n$),若恰好有 $n = 0$,说明答案为 $x$ 的最后一位,可由 `x % 10` 取得;若大于 $0$,说明答案是 $x + 1$ 的第 $n$ 位(十进制表示,从左往右数),可由 `(x + 1) / (int) (Math.pow(10, len - n)) % 10` 取得。

代码:
```Java
class Solution {
    public int findNthDigit(int n) {
        int len = 1;
        while (len * 9 * Math.pow(10, len - 1) < n) {
            n -= len * 9 * Math.pow(10, len - 1);
            len++;
        }
        long s = (long) Math.pow(10, len - 1);
        long x = n / len - 1 + s;
        n -= (x - s + 1) * len;
        return n == 0 ? (int) (x % 10) : (int) ((x + 1) / Math.pow(10, len - n) % 10);
    }
}
```
* 时间复杂度:$O(\log{n})$
* 空间复杂度:$O(1)$

---

### 补充

上述讲解可能对于新手并不友好,尤其是对一些上来只做剑指 Offer 的面试向同学,下面使用更为通俗的方式进行讲解。

对于长度 `len` 的所有数,其最小值为 $10^{len - 1}$,最大值为 $10^{len} - 1$,每个数所能贡献的数的个数为 $len$ 个,因此对于长度为 `len` 的所有数,其所能贡献的数的个数为 $(10^{len} - 10^{len - 1}) \times len$。我们专门开一个函数 `getCnt` 来计算长度为 `len` 的数的个数有多少。

然后我们从 `len = 1` 开始,不断对 `n` 进行试减(`len` 递增),直到 `getCnt(len) <= n` 不再满足。

假设此时的递增到的长度为 `len`,可知长度为 `len` 的最小值 $start = 10^{len - 1}$,我们可以通过计算偏移量 $\left \lfloor \frac{n}{len} \right \rfloor$ 并将其累加到 $start$ 中去,同时对 `n` 进行 $\left \lfloor \frac{n}{len} \right \rfloor \times len$ 的消减。

然后根据 `n` 是否为 $0$ 进行分情况讨论:

* 当 `n` 为 $0$,说明答案落在 $start - 1$ 的最后一位,即 `(start - 1) % 10` ;
* 当 `n` 不为 $0$,说明答案为 `start` 数值中从左往右数的第 $n$ 位,可通过解法一的方式取得,也可以将其转字符串再通过下标获取。

代码:
```Java
class Solution {
    long getCnt(int len) {
        return (long)(Math.pow(10, len) - Math.pow(10, len - 1)) * len;
    }
    public int findNthDigit(int n) {
        int len = 1;
        while (getCnt(len) <= n) {
            n -= getCnt(len);
            len++;
        }
        long start = (long)Math.pow(10, len - 1);
        start += n / len;
        n -= n / len * len;
        if (n == 0) return (int)(start - 1) % 10;
        else return String.valueOf(start).toCharArray()[n - 1] - '0';
    }
}
```
* 时间复杂度:$O(\log{n})$
* 空间复杂度:$O(1)$

### 最后

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

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

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

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


LeetCode/剑指 Offer/剑指 Offer 52. 两个链表的第一个公共节点（简单）.md

剑指 Offer 52. 两个链表的第一个公共节点

Question:
输入两个链表,找出它们的第一个公共节点。

如下面的两个链表:

在节点 c1 开始相交。

示例 1:

```
输入:intersectVal = 8, listA = [4,1,8,4,5], listB = [5,0,1,8,4,5], skipA = 2, skipB = 3

输出:Reference of the node with value = 8

输入解释:相交节点的值为 8 (注意,如果两个列表相交则不能为 0)。从各自的表头开始算起,链表 A 为 [4,1,8,4,5],链表 B 为 [5,0,1,8,4,5]。在 A 中,相交节点前有 2 个节点;在 B 中,相交节点前有 3 个节点。
```

示例 2:

```
输入:intersectVal = 2, listA = [0,9,1,2,4], listB = [3,2,4], skipA = 3, skipB = 1

输出:Reference of the node with value = 2

输入解释:相交节点的值为 2 (注意,如果两个列表相交则不能为 0)。从各自的表头开始算起,链表 A 为 [0,9,1,2,4],链表 B 为 [3,2,4]。在 A 中,相交节点前有 3 个节点;在 B 中,相交节点前有 1 个节点。
```

示例 3:

```
输入:intersectVal = 0, listA = [2,6,4], listB = [1,5], skipA = 3, skipB = 2

输出:null

输入解释:从各自的表头开始算起,链表 A 为 [2,6,4],链表 B 为 [1,5]。由于这两个链表不相交,所以 intersectVal 必须为 0,而 skipA 和 skipB 可以是任意值。

解释:这两个链表不相交,因此返回 null。
```

注意:
* 如果两个链表没有交点,返回 null.
* 在返回结果后,两个链表仍须保持原有的结构。
* 可假定整个链表结构中没有循环。
* 程序尽量满足 $O(n)$ 时间复杂度,且仅用 $O(1)$ 内存。

Solution:
### 朴素解法

一个朴素的解法自然是两层枚举,逐个检查哪个节点相同。

代码:
```Java
public class Solution {
    public ListNode getIntersectionNode(ListNode a, ListNode b) {
        for (ListNode h1 = a; h1 != null ; h1 = h1.next) {
            for (ListNode h2 = b; h2 != null ; h2 = h2.next) {
                if (h1 == h2) return h1;
            }
        }
        return null;
    }
}
```
* 时间复杂度:$O(n * m)$
* 空间复杂度:$O(1)$

---

### 栈解法

这是一种「从后往前」找的方式。

将两条链表分别压入两个栈中,然后循环比较两个栈的栈顶元素,同时记录上一位栈顶元素。

当遇到第一个不同的节点时,结束循环,上一位栈顶元素即是答案。

代码:
```Java
public class Solution {
    public ListNode getIntersectionNode(ListNode a, ListNode b) {
        Deque<ListNode> d1 = new ArrayDeque<>(), d2 = new ArrayDeque<>();
        while (a != null) {
            d1.add(a);
            a = a.next;
        }
        while (b != null) {
            d2.add(b);
            b = b.next;
        }
        ListNode ans = null;
        while (!d1.isEmpty() && !d2.isEmpty() && d1.peekLast() == d2.peekLast()) {
            ans = d1.pollLast();
            d2.pollLast();
        }
        return ans;
    }
}
```
* 时间复杂度:$O(n + m)$
* 空间复杂度:$O(n + m)$

---

### Set 解法

这是一种「从前往后」找的方式。

使用 `Set` 数据结构,先对某一条链表进行遍历,同时记录下来所有的节点。

然后在对第二链条进行遍历时,检查当前节点是否在 `Set` 中出现过,第一个在 `Set` 出现过的节点即是交点。

代码:
```Java
public class Solution {
    public ListNode getIntersectionNode(ListNode a, ListNode b) {
        Set<ListNode> set = new HashSet<>();
        while (a != null) {
            set.add(a);
            a = a.next;
        }
        while (b != null && !set.contains(b)) b = b.next;
        return b;
    }
}
```
* 时间复杂度:$O(n + m)$
* 空间复杂度:$O(n)$

---

### 差值法

由于两条链表在相交节点后面的部分完全相同,因此我们可以先对两条链表进行遍历,分别得到两条链表的长度,并计算差值 `d`。

让长度较长的链表先走 `d` 步,然后两条链表同时走,第一个相同的节点即是节点。

代码:
```Java
public class Solution {
    public ListNode getIntersectionNode(ListNode a, ListNode b) {
        int c1 = 0, c2 = 0;
        ListNode ta = a, tb = b;
        while (ta != null && c1++ >= 0) ta = ta.next;
        while (tb != null && c2++ >= 0) tb = tb.next;
        int d = c1 - c2;
        if (d > 0) {
            while (d-- > 0) a = a.next;
        } else if (d < 0) {
            d = -d;
            while (d-- > 0) b = b.next;
        }
        while (a != b) {
            a = a.next;
            b = b.next;
        }
        return a;
    }
}
```
* 时间复杂度:$O(n + m)$
* 空间复杂度:$O(1)$

---

### 等值法

这是「差值法」的另外一种实现形式,原理同样利用「两条链表在相交节点后面的部分完全相同」。

我们令第一条链表相交节点之前的长度为 `a`,第二条链表相交节点之前的长度为 `b`,相交节点后的公共长度为 `c`(注意 `c` 可能为 $0$,即不存在相交节点)。

分别对两条链表进行遍历:
* 当第一条链表遍历完,移动到第二条链表的头部进行遍历;
* 当第二条链表遍历完,移动到第一条链表的头部进行遍历。

如果存在交点:**第一条链表首次到达「第一个相交节点」的充要条件是第一条链表走了 $a + c + b$ 步,由于两条链表同时出发,并且步长相等,因此当第一条链表走了 $a + c + b$ 步时,第二条链表同样也是走了 $a + c + b$ 步,即 第二条同样停在「第一个相交节点」的位置。**

如果不存在交点:**两者会在走完 $a + c + b + c$ 之后同时变为 $null$,退出循环。**

代码:
```Java
public class Solution {
    public ListNode getIntersectionNode(ListNode a, ListNode b) {
        ListNode ta = a, tb = b;
        while (ta != tb) {
            ta = ta == null ? b : ta.next;
            tb = tb == null ? a : tb.next;
        }
        return ta;
    }
}
```
* 时间复杂度:$O(n + m)$
* 空间复杂度:$O(1)$

### 最后

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

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

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

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


LeetCode/剑指 Offer/剑指 Offer 53 - I. 在排序数组中查找数字 I（简单）.md

剑指 Offer 53 - I. 在排序数组中查找数字 I

Question:
统计一个数字在排序数组中出现的次数。

示例 1:
```
输入: nums = [5,7,7,8,8,10], target = 8

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

输出: 0
```

限制:
* 0 <= 数组长度 <= 50000

Solution:
### 二分单边 + 线性扫描

一个朴素的想法是,找到目标值 $target$ 「首次」出现或者「最后」出现的下标,然后「往后」或者「往前」进行数量统计。

代码:
```Java
// 找到目标值「最后」出现的分割点,并「往前」进行统计
class Solution {
    public int search(int[] nums, int t) {
        int n = nums.length;
        int l = 0, r = n - 1;
        while (l < r) {
            int mid = l + r + 1 >> 1;
            if (nums[mid] <= t) l = mid;
            else r = mid - 1;
        }
        int ans = 0;
        while (r >= 0 && nums[r] == t && r-- >= 0) ans++;
        return ans;
    }
}
```
```Java
// 找到目标值「首次」出现的分割点,并「往后」进行统计
class Solution {
    public int search(int[] nums, int t) {
        int n = nums.length;
        int l = 0, r = n - 1;
        while (l < r) {
            int mid = l + r >> 1;
            if (nums[mid] >= t) r = mid;
            else l = mid + 1;
        }
        int ans = 0;
        while (l < n && nums[l] == t && l++ >= 0) ans++;
        return ans;
    }
}
```
* 时间复杂度:二分找到分割点之后,需要往前或者往后进行扫描。复杂度为 $O(n)$
* 空间复杂度:$O(1)$

---

### 二分两边

进一步,我们可以直接经过两次「二分」找到左右边界,计算总长度即是 $target$ 的数量。

代码:
```Java
class Solution {
    public int search(int[] nums, int t) {
        int n = nums.length;
        if (n == 0) return 0;
        int a = -1, b = -1;

        // 二分出左边界
        int l = 0, r = n - 1;
        while (l < r) {
            int mid = l + r >> 1;
            if (nums[mid] >= t) r = mid;
            else l = mid + 1;
        }
        if (nums[r] != t) return 0;
        a = r;

        // 二分出右边界
        l = 0; r = n - 1;
        while (l < r) {
            int mid = l + r + 1 >> 1;
            if (nums[mid] <= t) l = mid;
            else r = mid - 1;
        }
        if (nums[r] != t) return 0;
        b = r;

        return b - a + 1;
    }
}
```
* 时间复杂度:$O(\log{n})$
* 空间复杂度:$O(1)$

### 最后

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

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

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

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


LeetCode/剑指 Offer/剑指 Offer 53 - II. 0～n-1中缺失的数字（简单）.md

Question:
一个长度为 $n-1$ 的递增排序数组中的所有数字都是唯一的,并且每个数字都在范围 $[0, n-1]$ 之内。

在范围 $[0, n-1]$ 内的 $n$ 个数字中有且只有一个数字不在该数组中,请找出这个数字。

示例 1:
```
输入: [0,1,3]

输出: 2
```
示例 2:
```
输入: [0,1,2,3,4,5,6,7,9]

输出: 8
```

限制:
* $1 <= 数组长度 <= 10000$

Solution:
### 二分

强调过无数次,二分的本质是「二段性」。

我们可以考虑对于一个给定数组 `nums` 中,对于缺失元素左右数值而言,有何性质。

假设我们缺失的元素数值为 `x`,那么对于 `x` 左边的元素(若有)必然是完整且连续的,也就是其必然满足 `nums[i] = i`,而其右边的元素(若有)必然由于 `x` 的缺失,导致必然不满足 `nums[i] = i`,因此在以缺失元素为分割点的数轴上,具有二段性,我们可以使用「二分」来找该分割点。

同时由于缺失元素可能是 $[0, n - 1]$ 范围内的最大值,因此我们需要二分结束后再 `check` 一次,若不满足,说明缺失的元素值为 $n - 1$。

Java 代码:
```Java
class Solution {
    public int missingNumber(int[] nums) {
        int l = 0, r = nums.length - 1;
        while (l < r) {
            int mid = l + r + 1 >> 1;
            if (nums[mid] == mid) l = mid;
            else r = mid - 1;
        }
        return nums[r] != r ? r : r + 1;
    }
}
```
Typescript 代码:
```Typescript
function missingNumber(nums: number[]): number {
    let l = 0, r = nums.length - 1
    while (l < r) {
        const mid = l + r + 1 >> 1
        if (nums[mid] == mid) l = mid
        else r = mid - 1
    }
    return nums[r] != r ? r : r + 1
};
```
* 时间复杂度:$O(\log{n})$
* 空间复杂度:$O(1)$

### 最后

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

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

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

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


LeetCode/剑指 Offer/剑指 Offer 59 - I. 滑动窗口的最大值（困难）.md

剑指 Offer 59 - I. 滑动窗口的最大值

Question:
给你一个整数数组 `nums`,有一个大小为 `k` 的滑动窗口从数组的最左侧移动到数组的最右侧。你只可以看到在滑动窗口内的 `k` 个数字。滑动窗口每次只向右移动一位。

返回滑动窗口中的最大值 。

示例 1:
```
输入:nums = [1,3,-1,-3,5,3,6,7], k = 3

输出:[3,3,5,5,6,7]

解释:
滑动窗口的位置                最大值
---------------               -----
[1  3  -1] -3  5  3  6  7       3
 1 [3  -1  -3] 5  3  6  7       3
 1  3 [-1  -3  5] 3  6  7       5
 1  3  -1 [-3  5  3] 6  7       5
 1  3  -1  -3 [5  3  6] 7       6
 1  3  -1  -3  5 [3  6  7]      7
```
示例 2:
```
输入:nums = [1], k = 1

输出:[1]
```

提示:
* $1 <= nums.length <= 10^5$
* $-10^4 <= nums[i] <= 10^4$
* $1 <= k <= nums.length$

Solution:
### 优先队列(堆)

根据题意,容易想到优先队列(大根堆),同时为了确保滑动窗口的大小合法性,我们以二元组 $(idx, nums[idx])$ 的形式进行入队。

当下标达到首个滑动窗口的右端点后,每次尝试从优先队列(大根堆)中取出最大值(若堆顶元素的下标小于当前滑动窗口左端点时,则丢弃该元素)。

代码:
```Java
class Solution {
    public int[] maxSlidingWindow(int[] nums, int k) {
        PriorityQueue<int[]> q = new PriorityQueue<>((a,b)->b[1]-a[1]);
        int n = nums.length, m = n - k + 1, idx = 0;
        int[] ans = new int[m];
        for (int i = 0; i < n; i++) {
            q.add(new int[]{i, nums[i]});
            if (i >= k - 1) {
                while (q.peek()[0] <= i - k) q.poll();
                ans[idx++] = q.peek()[1];
            }
        }
        return ans;
    }
}
```
* 时间复杂度:$O(n\log{n})$
* 空间复杂度:$O(n)$

---

### 线段树

容易将问题转换为「区间求和」问题:使用原始的 `nums` 构建线段树等价于在位置 $i$ 插入 $nums[i]$,即单点操作,而查询每个滑动窗口最大值,则对应的区间查询。

由于只涉及单点修改,无须实现懒标记 `pushdown` 操作,再结合 $n$ 的数据范围为 $10^5$,无须进行动态开点。

直接写 `build` 四倍空间的线段树数组实现即可。

代码:
```Java
class Solution {
    class Node {
        int l, r, val;
        Node (int _l, int _r) {
            l = _l; r = _r; val = Integer.MIN_VALUE;
        }
    }
    Node[] tr = new Node[100010 * 4];
    void build(int u, int l, int r) {
        tr[u] = new Node(l, r);
        if (l == r) return ;
        int mid = l + r >> 1;
        build(u << 1, l, mid);
        build(u << 1 | 1, mid + 1, r);
    }
    void update(int u, int x, int v) {
        if (tr[u].l == x && tr[u].r == x) {
            tr[u].val = Math.max(tr[u].val, v);
            return ;
        }
        int mid = tr[u].l + tr[u].r >> 1;
        if (x <= mid) update(u << 1, x, v);
        else update(u << 1 | 1, x, v);
        pushup(u);
    }
    int query(int u, int l, int r) {
        if (l <= tr[u].l && tr[u].r <= r) return tr[u].val;
        int mid = tr[u].l + tr[u].r >> 1, ans = Integer.MIN_VALUE;
        if (l <= mid) ans = query(u << 1, l, r);
        if (r > mid) ans = Math.max(ans, query(u << 1 | 1, l, r));
        return ans;
    }
    void pushup(int u) {
        tr[u].val = Math.max(tr[u << 1].val, tr[u << 1 | 1].val);
    }
    public int[] maxSlidingWindow(int[] nums, int k) {
        int n = nums.length, m = n - k + 1;
        int[] ans = new int[m];
        build(1, 1, n);
        for (int i = 0; i < n; i++) update(1, i + 1, nums[i]);
        for (int i = k; i <= n; i++) ans[i - k] = query(1, i - k + 1, i);
        return ans;
    }
}
```
* 时间复杂度:建立线段树复杂度为 $O(n\log{n})$;构建答案复杂度为 $O(n\log{n})$。整体复杂度为 $O(n\log{n})$
* 空间复杂度:$O(n)$

---

### 分块

另外一个做法是分块,又名「优雅的暴力」,也是莫队算法的基础。

具体的,除了给定的 `nums` 以外,我们构建一个分块数组 `region`,其中 `region[idx] = x` 含义为块编号为 `idx` 的最大值为 `x`,其中一个块对应一个原始区间 $[l, r]$。

如何定义块大小是实现分块算法的关键。

对于本题,本质是求若干个大小为 $k$ 的区间最大值。

我们可以设定块大小为 $\sqrt{k}$,这样所需创建的分块数组大小为 $\frac{n}{\sqrt{k}}$。分块数组的更新操作为 $O(1)$,而查询则为 $\sqrt{k}$。

容易证明查询操作的复杂度:对于每个长度为 $k$ 的 $[l, r]$ 查询操作而言,最多遍历两个(左右端点对应的块)的块内元素,复杂度为 $O(\sqrt{k})$,同时最多遍历 $\sqrt{k}$ 个块,复杂度同为 $O(\sqrt{k})$。

因此最多两步复杂度为 $O(\sqrt{k})$ 的块内操作,最多 $\sqrt{k}$ 步复杂度为 $O(1)$ 的块间操作,整体复杂度为 $O(\sqrt{k})$。

因此使用分块算法总的计算量为 $n\times\sqrt{k} = 10^6$,可以过。

分块算法的几个操作函数:

* `int getIdx(int x)` :计算原始下标对应的块编号;
* `void add(int x, int v)` :计算原始下标 `x` 对应的下标 `idx`,并将 `region[idx]` 和 `v` 取 `max` 来更新 `region[idx]`;
* `int query(int l, int r)` :查询 $[l, r]$ 中的最大值,如果 $l$ 和 $r$ 所在块相同,直接遍历 $[l, r]$ 进行取值;若 $l$ 和 $r$ 不同块,则处理 $l$ 和 $r$ 对应的块内元素后,对块编号在 $(getIdx(l), getIdx(r))$ 之间的块进行遍历。

代码:
```Java
class Solution {
    int n, m, len;
    int[] nums, region;
    int getIdx(int x) {
        return x / len;
    }
    void add(int x, int v) {
        region[getIdx(x)] = Math.max(region[getIdx(x)], v);
    }
    int query(int l, int r) {
        int ans = Integer.MIN_VALUE;
        if (getIdx(l) == getIdx(r)) {
            for (int i = l; i <= r; i++) ans = Math.max(ans, nums[i]);
        } else {
            int i = l, j = r;
            while (getIdx(i) == getIdx(l)) ans = Math.max(ans, nums[i++]);
            while (getIdx(j) == getIdx(r)) ans = Math.max(ans, nums[j--]);
            for (int k = getIdx(i); k <= getIdx(j); k++) ans = Math.max(ans, region[k]);
        }
        return ans;
    }
    public int[] maxSlidingWindow(int[] _nums, int k) {
        nums = _nums;
        n = nums.length; len = (int) Math.sqrt(k); m = n / len + 10;
        region = new int[m];
        Arrays.fill(region, Integer.MIN_VALUE);
        for (int i = 0; i < n; i++) add(i, nums[i]);
        int[] ans = new int[n - k + 1];
        for (int i = 0; i < n - k + 1; i++) ans[i] = query(i, i + k - 1);
        return ans;
    }
}
```
* 时间复杂度:数组大小为 $n$,块大小为 $\sqrt{k}$,分块数组大小为 $\frac{n}{\sqrt{k}}$。预处理分块数组复杂度为 $O(n)$(即 `add` 操作复杂度为 $O(1)$ );构造答案复杂度为 $O(n\times\sqrt{k})$(即 `query` 操作复杂度为 $O(\sqrt{k})$,最多有 $n$ 次查询)
* 空间复杂度:$\frac{n}{\sqrt{k}}$

---

### 单调队列

关于 `RMQ` 的另外一个优秀做法通常是使用「单调队列/单调栈」。

当然,我们也能不依靠经验,而从问题的本身出发,逐步分析出该做法。

假设我们当前处理到某个长度为 $k$ 的窗口,此时窗口往后滑动一格,会导致后一个数(新窗口的右端点)添加进来,同时会导致前一个数(旧窗口的左端点)移出窗口。

随着窗口的不断平移,该过程会一直发生。**若同一时刻存在两个数 $nums[j]$ 和 $nums[i]$($j < i$)所在一个窗口内,下标更大的数会被更晚移出窗口,此时如果有 $nums[j] <= nums[i]$ 的话,可以完全确定 $nums[j]$ 将不会成为后续任何一个窗口的最大值,此时可以将必然不会是答案的 $nums[j]$ 从候选中进行移除**。

不难发现,当我们将所有必然不可能作为答案的元素(即所有满足的小于等于 $nums[i]$ )移除后,候选集合满足「单调递减」特性,即集合首位元素为当前窗口中的最大值(为了满足窗口长度为 $k$ 的要求,在从集合头部取答案时需要先将下标小于的等于的 $i - k$ 的元素移除)。

为方便从尾部添加元素,从头部获取答案,我们可使用「双端队列」存储所有候选元素。

代码:
```Java
class Solution {
    public int[] maxSlidingWindow(int[] nums, int k) {
        Deque<Integer> d = new ArrayDeque<>();
        int n = nums.length, m = n - k + 1;
        int[] ans = new int[m];
        for (int i = 0; i < n; i++) {
            while (!d.isEmpty() && nums[d.peekLast()] <= nums[i]) d.pollLast();
            d.addLast(i);
            if (i >= k - 1) {
                while (!d.isEmpty() && d.peekFirst() <= i - k) d.pollFirst();
                ans[i - k + 1] = nums[d.peekFirst()];
            }
        }
        return ans;
    }
}
```
* 时间复杂度:$O(n)$
* 空间复杂度:$O(n)$

### 最后

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

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

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

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


LeetCode/面试题/面试题 01.02. 判定是否互为字符重排（简单）.md

面试题 01.02. 判定是否互为字符重排

Question:
给定两个字符串 `s1` 和 `s2`,请编写一个程序,确定其中一个字符串的字符重新排列后,能否变成另一个字符串。

示例 1:
```
输入: s1 = "abc", s2 = "bca"

输出: true
```
示例 2:
```
输入: s1 = "abc", s2 = "bad"

输出: false
```

说明:
* $0 <= len(s1) <= 100$
* $0 <= len(s2) <= 100$

Solution:
### 模拟

根据题意,对两字符串进行词频统计,统计过程中使用变量 `tot` 记录词频不同的字符个数。

Java 代码:
```Java
class Solution {
    public boolean CheckPermutation(String s1, String s2) {
        int n = s1.length(), m = s2.length(), tot = 0;
        if (n != m) return false;
        int[] cnts = new int[128];
        for (int i = 0; i < n; i++) {
            if (++cnts[s1.charAt(i)] == 1) tot++;
            if (--cnts[s2.charAt(i)] == 0) tot--;
        }
        return tot == 0;
    }
}
```
TypeScript 代码:
```TypeScript
function CheckPermutation(s1: string, s2: string): boolean {
    let n = s1.length, m = s2.length, tot = 0
    if (n != m) return false
    const cnts = new Array<number>(128).fill(0)
    for (let i = 0; i < n; i++) {
        if (++cnts[s1.charCodeAt(i)] == 1) tot++
        if (--cnts[s2.charCodeAt(i)] == 0) tot--
    }
    return tot == 0
};
```
Python3 代码:
```Python3
class Solution:
    def CheckPermutation(self, s1: str, s2: str) -> bool:
        return Counter(s1) == Counter(s2)
```
* 时间复杂度:$O(n)$
* 空间复杂度:$O(C)$,其中 $C = 128$ 为字符集大小

### 最后

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

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

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

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


面试题 01.05. 一次编辑

Question:
字符串有三种编辑操作:插入一个字符、删除一个字符或者替换一个字符。 给定两个字符串,编写一个函数判定它们是否只需要一次(或者零次)编辑。

示例 1:
```
输入:
first = "pale"
second = "ple"

输出: True
```
示例 2:
```
输入:
first = "pales"
second = "pal"

输出: False
```

Solution:
### 双指针模拟

为了方便,我们令 $a = first$、$b = second$,两者长度为 $n$ 和 $m$,并让 $a$ 为两种中的长度较短的那个(若 $b$ 较短,则将两者交换)。

接下来是简单的双指针处理(使用 $cnt$ 记录操作次数):

1. 我们最多使用不超过一次的操作,因此如果 $\left | n - m \right | > 1$,直接返回 `False`;
2. 若两者长度差不超过 $1$,使用 $i$ 和 $j$ 分别指向两字符的最左侧进行诸位检查:
    * 若 $a[i] = b[j]$,让 $i$ 和 $j$ 继续后移进行检查;
    * 若 $a[i] \neq b[j]$,根据两字符串长度进行分情况讨论:
        * 若 $n = m$,说明此时只能通过「替换」操作消除不同,分别让 $i$ 和 $j$ 后移,并对 $cnt$ 进行加一操作;
        * 若 $n \neq m$,由于我们人为确保了 $a$ 更短,即此时是 $n < m$,此时只能通过对 $a$ 字符串进行「添加」操作来消除不同,此时让 $j$ 后移,$i$ 不动(含义为在 $a$ 字符串中的 $i$ 位置增加一个 $b[j]$ 字符),并对 $cnt$ 进行加一操作。

最终我们根据 $cnt$ 是否不超过 $1$ 来返回结果。

代码:
```Java
class Solution {
    public boolean oneEditAway(String a, String b) {
        int n = a.length(), m = b.length();
        if (Math.abs(n - m) > 1) return false;
        if (n > m) return oneEditAway(b, a);
        int i = 0, j = 0, cnt = 0;
        while (i < n && j < m && cnt <= 1) {
            char c1 = a.charAt(i), c2 = b.charAt(j);
            if (c1 == c2) {
                i++; j++;
            } else {
                if (n == m) {
                    i++; j++; cnt++;
                } else {
                    j++; cnt++;
                }
            }
        }
        return cnt <= 1;
    }
}
```
* 时间复杂度:令 $n$ 和 $m$ 分别为两字符串长度,复杂度为 $O(\max(n, m))$
* 空间复杂度:$O(1)$

### 最后

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

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

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

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


面试题 01.08. 零矩阵

Question:
编写一种算法,若 $M \times N$ 矩阵中某个元素为 $0$,则将其所在的行与列清零。

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

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

输出:
[
  [0,0,0,0],
  [0,4,5,0],
  [0,3,1,0]
]
```

Solution:
### 模拟

根据题意进行模拟。

Java 代码:
```Java
class Solution {
    public void setZeroes(int[][] mat) {
        int n = mat.length, m = mat[0].length;
        boolean[] rows = new boolean[n], cols = new boolean[m];
        for (int i = 0; i < n; i++) {
            for (int j = 0; j < m; j++) {
                if (mat[i][j] == 0) rows[i] = cols[j] = true;
            }
        }
        for (int i = 0; i < n; i++) {
            for (int j = 0; j < m; j++) {
                if (rows[i] || cols[j]) mat[i][j] = 0;
            }
        }
    }
}
```
TypeScript 代码:
```TypeScript
function setZeroes(mat: number[][]): void {
    const n = mat.length, m = mat[0].length
    const rows = new Array<boolean>(n).fill(false), cols = new Array<boolean>(m).fill(false)
    for (let i = 0; i < n; i++) {
        for (let j = 0; j < m; j++) {
            if (mat[i][j] == 0) rows[i] = cols[j] = true
        }
    }
    for (let i = 0; i < n; i++) {
        for (let j = 0; j < m; j++) {
            if (rows[i] || cols[j]) mat[i][j] = 0
        }
    }
};
```
Python 代码:
```Python
class Solution:
    def setZeroes(self, mat: List[List[int]]) -> None:
        n, m = len(mat), len(mat[0])
        rows, cols = [False] * n, [False] * m
        for i in range(n):
            for j in range(m):
                if mat[i][j] == 0:
                    rows[i] = cols[j] = True
        for i in range(n):
            for j in range(m):
                mat[i][j] = 0 if rows[i] or cols[j] else mat[i][j]
```
* 时间复杂度:$O(n \times m)$
* 空间复杂度:$O(n + m)$

### 最后

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

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

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

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


面试题 01.09. 字符串轮转

Question:
字符串轮转。给定两个字符串 `s1` 和 `s2`,请编写代码检查 `s2` 是否为 `s1` 旋转而成(比如,`waterbottle` 是 `erbottlewat` 旋转后的字符串)。

示例1:
```
输入:s1 = "waterbottle", s2 = "erbottlewat"

输出:True
```
示例2:
```
输入:s1 = "aa", s2 = "aba"

输出:False
```
提示:
* 字符串长度在 $[0, 100000]$ 范围内。

说明:
你能只调用一次检查子串的方法吗?

Solution:
### 字符串哈希

若两字符串互为旋转,则「其一字符串」必然为「另一字符串拓展两倍长度后(循环子串)」的子串。

基于此,我们可以使用「字符串哈希」进行求解:先计算 `s2` 的字符串哈希值 `t`,然后构造出 `s1 + s1` 的哈希数组和次方数组,在两数组中检查是否存在长度为 $n$ 的连续子段的哈希值 `cur` 与 `t` 相等。

**不了解字符串哈希的同学可以看前置🧀 : [字符串哈希入门](https://mp.weixin.qq.com/s?__biz=MzU4NDE3MTEyMA==&mid=2247489813&idx=1&sn=7f3bc18ca390d85b17655f7164d8e660)**

> 一些细节:其他语言可能不像 `Java` 那样存在自然取模,可手动取模,对于自带高精度的语言若不取模会导致单次计算复杂度上升,会 `TLE`。

Java 代码:
```Java
class Solution {
    static int N = 200010, P = 13131;
    static int[] h = new int[N], p = new int[N];
    public boolean isFlipedString(String s1, String s2) {
        if (s1.length() != s2.length()) return false;
        int n = s1.length();
        for (int i = 1; i <= n; i++) h[i] = h[i - 1] * P + s2.charAt(i - 1);
        int t = h[n]; // s2 hash
        s1 = s1 + s1;
        p[0] = 1;
        for (int i = 1; i <= 2 * n; i++) {
            h[i] = h[i - 1] * P + s1.charAt(i - 1);
            p[i] = p[i - 1] * P;
        }
        for (int i = 1; i + n - 1 <= 2 * n; i++) {
            int j = i + n - 1, cur = h[j] - h[i - 1] * p[j - i + 1];
            if (cur == t) return true;
        }
        return false;
    }
}
```
Python 代码:
```Python
N, P, MOD = 200010, 13131, 987654321
h, p = [0] * N, [1] * N
class Solution:
    def isFlipedString(self, s1: str, s2: str) -> bool:
        if len(s1) != len(s2):
            return False
        if s1 == s2:
            return True
        n = len(s1)
        for i in range(1, n + 1):
            h[i] = (h[i - 1] * P + ord(s2[i - 1])) % MOD
        t = h[n]
        s1 = s1 + s1
        for i in range(1, 2 * n + 1):
            h[i] = (h[i - 1] * P + ord(s1[i - 1])) % MOD
            p[i] = (p[i - 1] * P) % MOD
        for i in range(1, n + 1):
            j = i + n - 1
            cur = (h[j] - h[i - 1] * p[j - i + 1]) % MOD
            if cur == t:
                return True
        return False
```
* 时间复杂度:$O(n)$
* 空间复杂度:$O(n)$

### 最后

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

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

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

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


面试题 02.05. 链表求和

Question:
给定两个用链表表示的整数,每个节点包含一个数位。

这些数位是反向存放的,也就是个位排在链表首部。

编写函数对这两个整数求和,并用链表形式返回结果。

示例:
```
输入:(7 -> 1 -> 6) + (5 -> 9 -> 2),即617 + 295

输出:2 -> 1 -> 9,即912
```

示例:
```
输入:(6 -> 1 -> 7) + (2 -> 9 -> 5),即617 + 295

输出:9 -> 1 -> 2,即912
```

进阶:思考一下,假设这些数位是正向存放的,又该如何解决呢?

Solution:
### 链表(反向)

访问链表节点的顺序为「个位、百位、千位 ...」,即与执行「竖式加法」时访问的位数次序一致。

因此我们可以边遍历,边模拟「竖式加法」的过程,使用变量 $t$ 存储进位情况,当 $l1$ 和 $l2$ 没遍历完,或者进位 $t$ 不为 $0$,该计算过程就可以继续,每次使用当前计算结果 `t % 10` 来创建新节点,使用 `t / 10` 来更新进位。

代码:
```Java
class Solution {
    public ListNode addTwoNumbers(ListNode l1, ListNode l2) {
        ListNode ans = new ListNode(-1), temp = ans;
        int t = 0;
        while (l1 != null || l2 != null || t != 0) {
            if (l1 != null) {
                t += l1.val;
                l1 = l1.next;
            }
            if (l2 != null) {
                t += l2.val;
                l2 = l2.next;
            }
            temp.next = new ListNode(t % 10);
            temp = temp.next;
            t /= 10;
        }
        return ans.next;
    }
}
```
* 时间复杂度:令 $n$ 为链表 `l1` 的长度,$m$ 为链表 `l2` 的长度,复杂度为 $O(\max(n, m))$
* 空间复杂度:$O(1)$

---

### 链表(正向)

如果将链表翻转(先访问到的是数值高位),该如何处理?

由于我们的「竖式加法」是从低位开始,因此我们需要先使用数据结构(栈 / 数组)对链表元素进行存储,再实现「从低位访问」并进行「竖式加法」的模拟过程。

为了验证代码正确性,我们可以先对 `l1` 和 `l2` 进行翻转,再走链表(正向)的处理逻辑。

代码:
```Java
class Solution {
    ListNode reverse(ListNode node) {
        ListNode prev = null, cur = node;
        while (cur != null) {
            ListNode temp = cur.next;
            cur.next = prev;
            prev = cur; cur = temp;
        }
        return prev;
    }
    public ListNode addTwoNumbers(ListNode l1, ListNode l2) {
        l1 = reverse(l1);
        l2 = reverse(l2);

        Deque<Integer> d1 = new ArrayDeque<>(), d2 = new ArrayDeque<>();
        while (l1 != null) {
            d1.addLast(l1.val);
            l1 = l1.next;
        }
        while (l2 != null) {
            d2.addLast(l2.val);
            l2 = l2.next;
        }
        ListNode ans = new ListNode(-1), temp = ans;
        int t = 0;
        while (!d1.isEmpty() || !d2.isEmpty() || t != 0) {
            if (!d1.isEmpty()) t += d1.pollLast();
            if (!d2.isEmpty()) t += d2.pollLast();
            temp.next = new ListNode(t % 10);
            temp = temp.next;
            t /= 10;
        }
        return ans.next;
    }
}
```
* 时间复杂度:令 $n$ 为链表 `l1` 的长度,$m$ 为链表 `l2` 的长度,复杂度为 $O(\max(n, m))$
* 空间复杂度:$O(n + m)$

### 最后

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

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

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

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


面试题 03.01. 三合一

Question:
三合一。描述如何只用一个数组来实现三个栈。

你应该实现push(stackNum, value)、pop(stackNum)、isEmpty(stackNum)、peek(stackNum)方法。stackNum表示栈下标,value表示压入的值。

构造函数会传入一个stackSize参数,代表每个栈的大小。

示例1:
```
 输入:
["TripleInOne", "push", "push", "pop", "pop", "pop", "isEmpty"]
[[1], [0, 1], [0, 2], [0], [0], [0], [0]]
 输出:
[null, null, null, 1, -1, -1, true]
说明:当栈为空时`pop, peek`返回-1,当栈满时`push`不压入元素。
```
示例2:
```
 输入:
["TripleInOne", "push", "push", "push", "pop", "pop", "pop", "peek"]
[[2], [0, 1], [0, 2], [0, 3], [0], [0], [0], [0]]
 输出:
[null, null, null, null, 2, 1, -1, -1]
```

Solution:
### 二维数组

题目只要求我们使用「一个数组」来实现栈,并没有限制我们使用数组的维度。

因此一个简单的做法是,建立一个二维数组 $data$ 来做。

二维数组的每一行代表一个栈,同时使用一个 $locations$ 记录每个栈「待插入」的下标。

代码:
```java
class TripleInOne {
    int N = 3;
    // 3 * n 的数组,每一行代表一个栈
    int[][] data;
    // 记录每个栈「待插入」位置
    int[] locations;

    public TripleInOne(int stackSize) {
        data = new int[N][stackSize];
        locations = new int[N];
    }

    public void push(int stackNum, int value) {
        int[] stk = data[stackNum];
        int loc = locations[stackNum];
        if (loc < stk.length) {
            stk[loc] = value;
            locations[stackNum]++;
        }
    }

    public int pop(int stackNum) {
        int[] stk = data[stackNum];
        int loc = locations[stackNum];
        if (loc > 0) {
            int val = stk[loc - 1];
            locations[stackNum]--;
            return val;
        } else {
            return -1;
        }
    }

    public int peek(int stackNum) {
        int[] stk = data[stackNum];
        int loc = locations[stackNum];
        if (loc > 0) {
            return stk[loc - 1];
        } else {
            return -1;
        }
    }

    public boolean isEmpty(int stackNum) {
        return locations[stackNum] == 0;
    }
}
```
* 时间复杂度:所有的操作均为 $O(1)$。
* 空间复杂度:$O(k * n)$。k 为我们需要实现的栈的个数,n 为栈的容量。

***

### 一维数组

当然了,我们也能使用一个一维数组来做。

建立一个长度为 $3 * stackSize$ 的数组,并将 3 个栈的「待插入」存储在 $locations$ 数组。

代码:
```java
class TripleInOne {
    int N = 3;
    int[] data;
    int[] locations;
    int size;
    public TripleInOne(int stackSize) {
        size = stackSize;
        data = new int[size * N];
        locations = new int[N];
        for (int i = 0; i < N; i++) {
            locations[i] = i * size;
        }
    }

    public void push(int stackNum, int value) {
        int idx = locations[stackNum];
        if (idx < (stackNum + 1) * size) {
            data[idx] = value;
            locations[stackNum]++;
        }
    }

    public int pop(int stackNum) {
        int idx = locations[stackNum];
        if (idx > stackNum * size) {
            locations[stackNum]--;
            return data[idx - 1];
        } else {
            return -1;
        }
    }

    public int peek(int stackNum) {
        int idx = locations[stackNum];
        if (idx > stackNum * size) {
            return data[idx - 1];
        } else {
            return -1;
        }
    }

    public boolean isEmpty(int stackNum) {
        return locations[stackNum] == stackNum * size;
    }
}
```
* 时间复杂度:所有的操作均为 $O(1)$。
* 空间复杂度:$O(k * n)$。k 为我们需要实现的栈的个数,n 为栈的容量。

### 最后

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

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

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

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


面试题 04.06. 后继者

Question:
设计一个算法,找出二叉搜索树中指定节点的 “下一个” 节点(也即中序后继)。

如果指定节点没有对应的 “下一个” 节点,则返回 `null`。

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

  2
 / \
1   3

输出: 2
```
示例 2:
```
输入: root = [5,3,6,2,4,null,null,1], p = 6

      5
     / \
    3   6
   / \
  2   4
 /
1

输出: null
```

Solution:
### BST 特性 + 递归

利用 `BST` 的特性,我们可以根据当前节点 `root` 与 `p` 的值大小关系来确定搜索方向:

1. 若有 `root.val <= p.val` : 根据 `BST` 特性可知当前节点 `root` 及其左子树子节点均满足「值小于等于 `p.val`」,因此不可能是 `p` 点的后继,我们直接到 `root` 的右子树搜索 `p` 的后继(递归处理);
2. 若有 `root.val > p.val` : 当第一次搜索到满足此条件的节点时,在以 `root` 为根节点的子树中「位于最左下方」的值为 `p` 的后继,但也有可能 `root` 没有左子树,因此 `p` 的后继要么在 `root` 的左子树中(若有),要么是 `root` 本身,此时我们可以直接到 `root` 的左子树搜索,若搜索结果为空返回 `root`,否则返回搜索结果。

代码:
```Java
class Solution {
    public TreeNode inorderSuccessor(TreeNode root, TreeNode p) {
        if (root == null) return null;
        if (root.val <= p.val) return inorderSuccessor(root.right, p);
        TreeNode ans = inorderSuccessor(root.left, p);
        return ans == null ? root : ans;
    }
}
```
* 时间复杂度:复杂度与深度相关,最坏情况下仍会搜索所有节点,复杂度为 $O(n)$
* 空间复杂度:忽略递归带来的额外空间消耗,复杂度为 $O(1)$

### 最后

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

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

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

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


面试题 10.02. 变位词组

Question:
编写一种方法,对字符串数组进行排序,将所有变位词组合在一起。变位词是指字母相同,但排列不同的字符串。

注意:本题相对原题稍作修改

示例:
```
输入: ["eat", "tea", "tan", "ate", "nat", "bat"],

输出:
[
  ["ate","eat","tea"],
  ["nat","tan"],
  ["bat"]
]
```
说明:
* 所有输入均为小写字母。
* 不考虑答案输出的顺序。

Solution:
### 模拟 + 排序

一个朴素的想法是根据题意进行模拟,对每个字符串进行排序作为 `key`,从而实现相同的「变位词」对应同一个 `key`,使用哈希表进行统计即可。

代码:
```Java
class Solution {
    public List<List<String>> groupAnagrams(String[] ss) {
        List<List<String>> ans = new ArrayList<>();
        Map<String, List<String>> map = new HashMap<>();
        for (String s : ss) {
            char[] cs = s.toCharArray();
            Arrays.sort(cs);
            String key = String.valueOf(cs);
            List<String> list = map.getOrDefault(key, new ArrayList<>());
            list.add(s);
            map.put(key, list);
        }
        for (String key : map.keySet()) ans.add(map.get(key));
        return ans;
    }
}
```
* 时间复杂度:$O(\sum_{i = 0}^{n - 1}ss[i].length * \log{ss[i].length})$
* 空间复杂度:$O(\sum_{i = 0}^{n - 1}ss[i])$

---

### 模拟 + 计数

方法一无法做到线性,主要是存在对字符串进行排序的环节。

事实上,我们可以利用字符集大小有限作为切入点(只包含小写字母),使用一个大小为 $26$ 的数组进行计数,然后对计数后的数组统计值进行拼接,作为哈希表的 `key`,从而实现线性复杂度。

代码:
```Java
class Solution {
    public List<List<String>> groupAnagrams(String[] ss) {
        List<List<String>> ans = new ArrayList<>();
        Map<String, List<String>> map = new HashMap<>();
        for (String s : ss) {
            int[] cnts = new int[26];
            for (char c : s.toCharArray()) cnts[c - 'a']++;
            StringBuilder sb = new StringBuilder();
            for (int i : cnts) sb.append(i + "_");
            String key = sb.toString();
            List<String> list = map.getOrDefault(key, new ArrayList<>());
            list.add(s);
            map.put(key, list);
        }
        for (String key : map.keySet()) ans.add(map.get(key));
        return ans;
    }
}
```
* 时间复杂度:令 $n$ 为数组大小,$C$ 为字符集大小,固定为 $26$。整体复杂度为 $O(\sum_{i = 0}^{n - 1}ss[i].length + n * C)$,
* 空间复杂度:$O(\sum_{i = 0}^{n - 1}ss[i])$

---

### 质数分解唯一性

事实上,我们还能使用「质数分解唯一性」性质,使用质数乘积代指某个「变位词」。

具体的,我们可以先使用 `static` 代码块(确保只会发生一次)打表最小的 $26$ 个质数(任意 $26$ 个都可以,使用小的,乘积溢出风险低一点),这 $26$ 个质数分别对应了 $26$ 个字母。

对于一个「变位词」而言,其对应的质数乘积必然相同。

代码:
```Java
class Solution {
    static int[] nums = new int[26];
    static {
        for (int i = 2, idx = 0; idx != 26; i++) {
            boolean ok = true;
            for (int j = 2; j <= i / j; j++) {
                if (i % j == 0) {
                    ok = false;
                    break;
                }
            }
            if (ok) nums[idx++] = i;
        }
    }
    public List<List<String>> groupAnagrams(String[] ss) {
        List<List<String>> ans = new ArrayList<>();
        Map<Long, List<String>> map = new HashMap<>();
        for (String s : ss) {
            long cur = 1;
            for (char c : s.toCharArray()) {
                cur *= nums[c - 'a'];
            }
            List<String> list = map.getOrDefault(cur, new ArrayList<>());
            list.add(s);
            map.put(cur, list);
        }
        for (long key : map.keySet()) ans.add(map.get(key));
        return ans;
    }
}
```
* 时间复杂度:$O(\sum_{i = 0}^{n - 1}ss[i].length)$
* 空间复杂度:$O(\sum_{i = 0}^{n - 1}ss[i])$

---

### 溢出说明

使用 `long` 仍然存在溢出风险,但使用“长度不受限制”的高精度哈希值实现是不现实的。

哈希值必须是有限值域内,才有意义。

换句话说,如果使用高精度的哈希值的话,我们是无法直接将两个哈希值进行异或判断结果是否为 $0$ 来得出哈希值是否相同的结论,而是需要使用 $O(Len)$ 的复杂度来判定哈希值是否相同。

因此,针对存在的哈希冲突问题,要么是解决冲突;要是使用与「字符串哈希」类似的做法,不处理溢出(相当于模 $2^{64}$),但这样会存在溢出次数不一样的值对应的哈希值相同的问题,只能说是一种期望冲突不发生的做法。

### 最后

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

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

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

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


Question:
运用你所掌握的数据结构,设计和实现一个  LRU (最近最少使用) 缓存机制 。
实现 LRUCache 类:

* LRUCache(int capacity) 以正整数作为容量 capacity 初始化 LRU 缓存
* int get(int key) 如果关键字 key 存在于缓存中,则返回关键字的值,否则返回 -1 。
* void put(int key, int value) 如果关键字已经存在,则变更其数据值;如果关键字不存在,则插入该组「关键字-值」。当缓存容量达到上限时,它应该在写入新数据之前删除最久未使用的数据值,从而为新的数据值留出空间。

进阶:你是否可以在 $O(1)$ 时间复杂度内完成这两种操作?

示例:
```
输入
["LRUCache", "put", "put", "get", "put", "get", "put", "get", "get", "get"]
[[2], [1, 1], [2, 2], [1], [3, 3], [2], [4, 4], [1], [3], [4]]
输出
[null, null, null, 1, null, -1, null, -1, 3, 4]

解释
LRUCache lRUCache = new LRUCache(2);
lRUCache.put(1, 1); // 缓存是 {1=1}
lRUCache.put(2, 2); // 缓存是 {1=1, 2=2}
lRUCache.get(1);    // 返回 1
lRUCache.put(3, 3); // 该操作会使得关键字 2 作废,缓存是 {1=1, 3=3}
lRUCache.get(2);    // 返回 -1 (未找到)
lRUCache.put(4, 4); // 该操作会使得关键字 1 作废,缓存是 {4=4, 3=3}
lRUCache.get(1);    // 返回 -1 (未找到)
lRUCache.get(3);    // 返回 3
lRUCache.get(4);    // 返回 4
```

提示:
* 1 <= capacity <= 3000
* 0 <= key <= 3000
* 0 <= value <= $10^4$
* 最多调用 3 * $10^4$ 次 get 和 put

Solution:
### 基本分析

LRU 是一种十分常见的页面置换算法。

将 LRU 翻译成大白话就是:**当不得不淘汰某些数据时(通常是容量已满),选择最久未被使用的数据进行淘汰。**

**题目让我们实现一个容量固定的 `LRUCache` 。如果插入数据时,发现容器已满时,则先按照 LRU 规则淘汰一个数据,再将新数据插入,其中「插入」和「查询」都算作一次“使用”。**

可以通过 🌰 来理解,假设我们有容量为 $2$ 的 `LRUCache` 和 测试键值对 `[`1-1`,`2-2`,`3-3`]` ,将其按照顺序进行插入 & 查询:
* 插入 `1-1`,此时最新的使用数据为 `1-1`
* 插入 `2-2`,此时最新使用数据变为 `2-2`
* 查询 `1-1`,此时最新使用数据为 `1-1`
* 插入 `3-3`,由于容器已经达到容量,需要先淘汰已有数据才能插入,这时候会淘汰 `2-2`,`3-3` 成为最新使用数据

键值对存储方面,我们可以使用「哈希表」来确保插入和查询的复杂度为 $O(1)$。

另外我们还需要额外维护一个「使用顺序」序列。

我们期望当「新数据被插入」或「发生键值对查询」时,能够将当前键值对放到序列头部,这样当触发 LRU 淘汰时,只需要从序列尾部进行数据删除即可。

**期望在 $O(1)$ 复杂度内调整某个节点在序列中的位置,很自然想到双向链表。**

----

### 双向链表

具体的,我们使用哈希表来存储「键值对」,键值对的键作为哈希表的 Key,而哈希表的 Value 则使用我们自己封装的 `Node` 类,`Node` 同时作为双向链表的节点。

* 插入:检查当前键值对是否已经存在于哈希表:
  * 如果存在,则更新键值对,并将当前键值对所对应的 `Node` 节点调整到链表头部(`refresh` 操作)
  * 如果不存在,则检查哈希表容量是否已经达到容量:
    * 没达到容量:插入哈希表,并将当前键值对所对应的 `Node` 节点调整到链表头部(`refresh` 操作)
    * 已达到容量:先从链表尾部找到待删除元素进行删除(`delete` 操作),然后再插入哈希表,并将当前键值对所对应的 `Node` 节点调整到链表头部(`refresh` 操作)
* 查询:如果没在哈希表中找到该 Key,直接返回 $-1$;如果存在该 Key,则将对应的值返回,并将当前键值对所对应的 `Node` 节点调整到链表头部(`refresh` 操作)

一些细节:

* 为了减少双向链表左右节点的「判空」操作,我们预先建立两个「哨兵」节点 `head` 和 `tail`。

代码:
```Java
class LRUCache {
    class Node {
        int k, v;
        Node l, r;
        Node(int _k, int _v) {
            k = _k;
            v = _v;
        }
    }
    int n;
    Node head, tail;
    Map<Integer, Node> map;
    public LRUCache(int capacity) {
        n = capacity;
        map = new HashMap<>();
        head = new Node(-1, -1);
        tail = new Node(-1, -1);
        head.r = tail;
        tail.l = head;
    }

    public int get(int key) {
        if (map.containsKey(key)) {
            Node node = map.get(key);
            refresh(node);
            return node.v;
        }
        return -1;
    }

    public void put(int key, int value) {
        Node node = null;
        if (map.containsKey(key)) {
            node = map.get(key);
            node.v = value;
        } else {
            if (map.size() == n) {
                Node del = tail.l;
                map.remove(del.k);
                delete(del);
            }
            node = new Node(key, value);
            map.put(key, node);
        }
        refresh(node);
    }

    // refresh 操作分两步:
    // 1. 先将当前节点从双向链表中删除(如果该节点本身存在于双向链表中的话)
    // 2. 将当前节点添加到双向链表头部
    void refresh(Node node) {
        delete(node);
        node.r = head.r;
        node.l = head;
        head.r.l = node;
        head.r = node;
    }

    // delete 操作:将当前节点从双向链表中移除
    // 由于我们预先建立 head 和 tail 两位哨兵,因此如果 node.l 不为空,则代表了 node 本身存在于双向链表(不是新节点)
    void delete(Node node) {
        if (node.l != null) {
            Node left = node.l;
            left.r = node.r;
            node.r.l = left;
        }
    }
}
```
* 时间复杂度:各操作均为 $O(1)$
* 空间复杂度:$O(n)$

### 最后

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

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

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

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


Question:
有些数的素因子只有 `3`,`5`,`7`,请设计一个算法找出第 `k` 个数。注意,不是必须有这些素因子,而是必须不包含其他的素因子。例如,前几个数按顺序应该是 `1`,`3`,`5`,`7`,`9`,`15`,`21`。

示例 1:
```
输入: k = 5

输出: 9
```

Solution:
### 基本分析

本题的基本思路与 [264. 丑数 II : 从朴素优先队列到多路归并](https://mp.weixin.qq.com/s?__biz=MzU4NDE3MTEyMA==&mid=2247490029&idx=1&sn=bba9ddff88d247db310406ee418d5a15) 完全一致。

---

### 优先队列(小根堆)

有了基本的分析思路,一个简单的解法是使用优先队列:

1. 起始先将最小数值 $1$ 放入队列
2. 每次从队列取出最小值 $x$,然后将 $x$ 所对应的数值 $3x$、$5x$ 和 $7x$ 进行入队
3. 对步骤 2 循环多次,第 $k$ 次出队的值即是答案

为了防止同一数值多次进队,我们需要使用数据结构 $Set$ 来记录入过队列的数值。

Java 代码:
```Java
class Solution {
    public int getKthMagicNumber(int k) {
        int[] nums = new int[]{3, 5, 7};
        PriorityQueue<Long> q = new PriorityQueue<>();
        Set<Long> set = new HashSet<>();
        q.add(1L); set.add(1L);
        while (!q.isEmpty()) {
            long t = q.poll();
            if (--k == 0) return (int) t;
            for (int x : nums) {
                if (!set.contains(x * t)) {
                    q.add(x * t); set.add(x * t);
                }
            }
        }
        return -1;
    }
}
```
Python3 代码:
```Python3
class Solution:
    def getKthMagicNumber(self, k: int) -> int:
        nums = [3, 5, 7]
        q, vis = [], set()
        q.append(1)
        vis.add(1)
        while q:
            t = heapq.heappop(q)
            k -= 1
            if k == 0:
                return t
            for x in nums:
                if t * x not in vis:
                    heapq.heappush(q, t * x)
                    vis.add(t * x)
        return -1
```
* 时间复杂度:$O(k\log{k})$
* 空间复杂度:$O(k)$

---

### 多路归并(多指针)

从解法一中不难发现,我们「往后产生的数值」都是基于「已有数值」而来(使用「已有数值」乘上 $3$、$5$、$7$)。

因此,如果我们最终的数值序列为 $a1,a2,a3,...,an$ 的话,序列中的每一个数都必然能够被以下三个序列(中的至少一个)覆盖:

* 由数值 $\times 3$ 所得的有序序列:$1 \times 3$、$2 \times 3$、$3 \times 3$、$4 \times 3$、$5 \times 3$、$6 \times 3$、$8 \times 3$ ...
* 由数值 $\times 5$ 所得的有序序列:$1 \times 5$、$2 \times 5$、$3 \times 5$、$4 \times 5$、$5 \times 5$、$6 \times 5$、$8 \times 5$ ...
* 由数值 $\times 6$ 所得的有序序列:$1 \times 7$、$2 \times 7$、$3 \times 7$、$4 \times 7$、$5 \times 7$、$6 \times 7$、$8 \times 6$ ...

举个🌰,假设我们需要求得 $[1, 3, 5, 7, 9, 15, 21]$ 数值序列 $arr$ 的最后一位,那么该序列可以看作以下三个有序序列归并而来:

* $1 \times 3, 3 \times 3, 5 \times 3, ... , 15 \times 3, 21 \times 3$ ,将 $3$ 提出,即 $arr \times 3$
* $1 \times 5, 3 \times 5, 5 \times 5, ... , 15 \times 5, 21 \times 5$ ,将 $5$ 提出,即 $arr \times 5$
* $1 \times 7, 3 \times 7, 5 \times 7, ... , 15 \times 7, 21 \times 7$ ,将 $7$ 提出,即 $arr \times 7$

因此我们可以使用三个指针来指向目标序列 $arr$ 的某个下标(下标 $0$ 作为哨兵不使用,起始都为 $1$),使用 $arr[下标] \times 系数$(`3`、`5` 和 `7`) 代表当前使用到三个有序序列中的哪一位,同时使用 $idx$ 表示当前生成到 $arr$ 哪一位数值。

Java 代码:
```Java
class Solution {
    public int getKthMagicNumber(int k) {
        int[] ans = new int[k + 1];
        ans[1] = 1;
        for (int i3 = 1, i5 = 1, i7 = 1, idx = 2; idx <= k; idx++) {
            int a = ans[i3] * 3, b = ans[i5] * 5, c = ans[i7] * 7;
            int min = Math.min(a, Math.min(b, c));
            if (min == a) i3++;
            if (min == b) i5++;
            if (min == c) i7++;
            ans[idx] = min;
        }
        return ans[k];
    }
}
```
TypeScript 代码:
```TypeScript
function getKthMagicNumber(k: number): number {
    const ans = new Array<number>(k + 1).fill(1)
    for (let i3 = 1, i5 = 1, i7 = 1, idx = 2; idx <= k; idx++) {
        const a = ans[i3] * 3, b = ans[i5] * 5, c = ans[i7] * 7
        const min = Math.min(a, Math.min(b, c))
        if (a == min) i3++
        if (b == min) i5++
        if (c == min) i7++
        ans[idx] = min
    }
    return ans[k]
};
```
Python 代码:
```Python
class Solution:
    def getKthMagicNumber(self, k: int) -> int:
        ans = [1] * (k + 1)
        i3, i5, i7 = 1, 1, 1
        for idx in range(2, k + 1):
            a, b, c = ans[i3] * 3, ans[i5] * 5, ans[i7] * 7
            cur = min([a, b, c])
            i3 = i3 + 1 if cur == a else i3
            i5 = i5 + 1 if cur == b else i5
            i7 = i7 + 1 if cur == c else i7
            ans[idx] = cur
        return ans[k]
```
* 时间复杂度:$O(k)$
* 空间复杂度:$O(k)$

### 最后

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

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

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

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


面试题 17.10. 主要元素

Question:
数组中占比超过一半的元素称之为主要元素。给你一个 整数 数组,找出其中的主要元素。

若没有,返回 -1 。请设计时间复杂度为 $O(N)$ 、空间复杂度为 $O(1)$ 的解决方案。

示例 1:
```
输入:[1,2,5,9,5,9,5,5,5]

输出:5
```
示例 2:
```
输入:[3,2]

输出:-1
```
示例 3:
```
输入:[2,2,1,1,1,2,2]

输出:2
```

Solution:
### 哈希表

一个朴素的做法是使用哈希表进行计数,如果发现某个元素数量超过总数一半,说明找到了答案。

代码:
```Java
class Solution {
    public int majorityElement(int[] nums) {
        int n = nums.length;
        Map<Integer, Integer> map = new HashMap<>();
        for (int x : nums) {
            map.put(x, map.getOrDefault(x, 0) + 1);
            if (map.get(x) > n / 2) return x;
        }
        return -1;
    }
}
```
* 时间复杂度:$O(n)$
* 空间复杂度:$O(n)$

---

### 摩尔投票

这还是道「摩尔投票」模板题。

**[摩尔投票](https://zh.wikipedia.org/wiki/%E5%A4%9A%E6%95%B0%E6%8A%95%E7%A5%A8%E7%AE%97%E6%B3%95) :在集合中寻找可能存在的多数元素,这一元素在输入的序列重复出现并占到了序列元素的一半以上;在第一遍遍历之后应该再进行一个遍历以统计第一次算法遍历的结果出现次数,确定其是否为众数;如果一个序列中没有占到多数的元素,那么第一次的结果就可能是无效的随机元素。**

**换句话说,每次将两个不同的元素进行「抵消」,如果最后有元素剩余,则「可能」为元素个数大于总数一半的那个。**

具体的,我们定义一个变量 $x$ 来保存那个可能为主要元素的值,$cnt$ 用来记录该值的出现次数。然后在遍历数组 $nums$ 过程中执行如下逻辑:

* 如果 $cnt$ 为 $0$:说明之前出现过的 $x$ 已经被抵消完了,更新一下 $x$ 为当前值,出现次数为 $1$:`x = nums[i], cnt = 1`;
* 如果 $cnt$ 不为 $0$:说明之前统计的 $x$ 还没被抵消完,这是根据 $nums[i]$ 与 $x$ 是否相等进行计算即可:`cnt += nums[i] == x ? 1 : -1`。

当处理完 $nums$ 之后,我们得到了一个「可能」的主要元素。注意只是可能,因为我们在处理过程中只使用了 $x$ 和 $cnt$ 来记录,我们是无法确定最后剩下的 $x$ 是经过多次抵消后剩余的主要元素,还是只是不存在主要元素的数组中的无效随机元素。

因此我们需要再进行一次遍历,检查这个「可能」的主要元素 $x$ 的出现次数是否超过总数一半。

代码:
```Java
class Solution {
    public int majorityElement(int[] nums) {
        int n = nums.length;
        int x = -1, cnt = 0;
        for (int i : nums) {
            if (cnt == 0) {
                x = i;
                cnt = 1;
            } else {
                cnt += x == i ? 1 : -1;
            }
        }
        cnt = 0;
        for (int i : nums) if (x == i) cnt++;
        return cnt > n / 2 ? x : -1;
    }
}
```
* 时间复杂度:$O(n)$
* 空间复杂度:$O(1)$

### 最后

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

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

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

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


面试题 17.11. 单词距离

Question:
有个内含单词的超大文本文件,给定任意两个不同的单词,找出在这个文件中这两个单词的最短距离(相隔单词数)。如果寻找过程在这个文件中会重复多次,而每次寻找的单词不同,你能对此优化吗?

示例:
```
输入:words = ["I","am","a","student","from","a","university","in","a","city"], word1 = "a", word2 = "student"

输出:1
```
提示:
* $words.length <= 100000$

Solution:
### 模拟

对 `words` 进行遍历,使用两个指针 `p` 和 `q` 分别记录最近的两个关键字的位置,并维护更新最小距离。

代码:
```Java
class Solution {
    public int findClosest(String[] ws, String a, String b) {
        int n = ws.length, ans = n;
        for (int i = 0, p = -1, q = -1; i < n; i++) {
            String t = ws[i];
            if (t.equals(a)) p = i;
            if (t.equals(b)) q = i;
            if (p != -1 && q != -1) ans = Math.min(ans, Math.abs(p - q));
        }
        return ans;
    }
}
```
* 时间复杂度:$O(n)$
* 空间复杂度:$O(1)$

### 最后

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

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

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

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


面试题 17.14. 最小K个数

Question:
设计一个算法,找出数组中最小的 $k$ 个数。以任意顺序返回这k个数均可。

示例:
```
输入: arr = [1,3,5,7,2,4,6,8], k = 4

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

提示:
* $0 <= len(arr) <= 100000$
* $0 <= k <= \min(100000, len(arr))$

Solution:
### 优先队列(小根堆)

一个直观的想法是使用「优先队列(小根堆)」,起始将所有元素放入堆中,然后再从堆中取出 $k$ 个元素并「顺序」构造答案。

代码:
```Java
class Solution {
    public int[] smallestK(int[] arr, int k) {
        PriorityQueue<Integer> q = new PriorityQueue<>((a,b)->a-b);
        for (int i : arr) q.add(i);
        int[] ans = new int[k];
        for (int i = 0; i < k; i++) ans[i] = q.poll();
        return ans;
    }
}
```
* 时间复杂度:建堆复杂度为 $O(n\log{n})$,构造答案复杂度为 $O(k\log{n})$。整体复杂度为 $O(n\log{n})$
* 空间复杂度:$O(n + k)$

---

### 优先队列(大根堆)

在解法一中,我们将所有的原始都放入堆中,堆中元素最多有 $n$ 个,这导致了我们复杂度的上界为 $O(n\log{n})$。

而另外一个比较优秀的做法是,使用「优先队列(大根堆)」。

当处理到原始 $arr[i]$ 时,根据堆内元素个数,以及其与堆顶元素的关系分情况讨论:

* 堆内元素不足 $k$ 个:直接将 $arr[i]$ 放入堆内;
* 堆内元素为 $k$ 个:根据 $arr[i]$ 与堆顶元素的大小关系分情况讨论:
    * $arr[i] >= heapTop$:$arr[i]$ 不可能属于第 $k$ 小数(已有 $k$ 个元素在堆中),直接丢弃 $arr[i]$;
    * $arr[i] < heapTop$:$arr[i]$ 可能属于第 $k$ 小数,弹出堆顶元素,并放入 $arr[i]$。

当 $arr$ 被处理完,我们再使用堆中元素「逆序」构造答案。

代码:
```Java
class Solution {
    public int[] smallestK(int[] arr, int k) {
        PriorityQueue<Integer> q = new PriorityQueue<>((a,b)->b-a);
        int[] ans = new int[k];
        if (k == 0) return ans;
        for (int i : arr) {
            if (q.size() == k && q.peek() <= i) continue;
            if (q.size() == k) q.poll();
            q.add(i);
        }
        for (int i = k - 1; i >= 0; i--) ans[i] = q.poll();
        return ans;
    }
}
```
* 时间复杂度:建堆复杂度为 $O(n\log{k})$,构造答案复杂度为 $O(k\log{k})$。整体复杂度为 $O(n\log{k})$
* 空间复杂度:$O(k)$

---

### 全排序

Java 中的 `Arrays.sort` 为综合排序实现。会根据数据规模、元素本身是否大致有序选择不同的排序实现。

因此一个比较省事的实现是先使用 `Arrays.sort` 进行排序,再构造答案。

代码:
```Java
class Solution {
    public int[] smallestK(int[] arr, int k) {
        Arrays.sort(arr);
        int[] ans = new int[k];
        for (int i = 0; i < k; i++) ans[i] = arr[i];
        return ans;
    }
}
```
* 时间复杂度:排序(假定 `Arrays.sort` 使用的是双轴快排实现)的复杂度为 $O(n\log{n})$;构造答案复杂度为 $O(k)$。整体复杂度为 $O(n\log{n})$
* 空间复杂度:$O(\log{n} + k)$

---

### 快排数组划分

注意到题目要求「任意顺序返回这 $k$ 个数即可」,因此我们只需要确保前 $k$ 小的数都出现在下标为 $[0, k)$ 的位置即可。

利用「快速排序」的数组划分即可做到。

我们知道快排每次都会将小于等于基准值的值放到左边,将大于基准值的值放到右边。

因此我们可以通过判断基准点的下标 $idx$ 与 $k$ 的关系来确定过程是否结束:

* $idx < k$:基准点左侧不足 $k$ 个,递归处理右边,让基准点下标右移;
* $idx > k$:基准点左侧超过 $k$ 个,递归处理左边,让基准点下标左移;
* $idx = k$:基准点左侧恰好 $k$ 个,输出基准点左侧元素。

代码:
```Java
class Solution {
    int k;
    public int[] smallestK(int[] arr, int _k) {
        k = _k;
        int n = arr.length;
        int[] ans = new int[k];
        if (k == 0) return ans;
        qsort(arr, 0, n - 1);
        for (int i = 0; i < k; i++) ans[i] = arr[i];
        return ans;
    }
    void qsort(int[] arr, int l, int r) {
        if (l >= r) return ;
        int i = l, j = r;
        int ridx = new Random().nextInt(r - l + 1) + l;
        swap(arr, ridx, l);
        int x = arr[l];
        while (i < j) {
            while (i < j && arr[j] >= x) j--;
            while (i < j && arr[i] <= x) i++;
            swap(arr, i, j);
        }
        swap(arr, i, l);
        // 集中答疑:因为题解是使用「基准点左侧」来进行描述(不包含基准点的意思),所以这里用的 k(写成 k - 1 也可以滴
        if (i > k) qsort(arr, l, i - 1);
        if (i < k) qsort(arr, i + 1, r);
    }
    void swap(int[] arr, int l, int r) {
        int tmp = arr[l];
        arr[l] = arr[r];
        arr[r] = tmp;
    }
}
```
* 时间复杂度:由于每次都会随机选择基准值,因此每次递归的数组平均长度为 $n / 2$,那么划分数组操作的次数不会超过 $2 \times n$。整体复杂度为 $O(n)$
* 空间复杂度:$O(\log{n})$

### 最后

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

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

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

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


面试题 17.19. 消失的两个数字

Question:
给定一个数组,包含从 `1` 到 `N` 所有的整数,但其中缺了两个数字。你能在 $O(N)$ 时间内只用 $O(1)$ 的空间找到它们吗?

以任意顺序返回这两个数字均可。

示例 1:
```
输入: [1]

输出: [2,3]
```
示例 2:
```
输入: [2,3]

输出: [1,4]
```
提示:
* $nums.length <= 30000$

Solution:
### 数学

根据题意,给定 `nums` 的长度为 $m$ 且缺失了两个数,所有的 $nums[i]$ 加上缺失数字组成连续排列长度为 $n = m + 2$。

根据等差数量求和公式可知,补齐后的排列总和为 $\frac{n \times (1 + n)}{2}$,补全后的理论总和与实际总和之间的差值 $cur = \frac{n \times (1 + n)}{2} - \sum_{i = 0}^{m - 1}nums[i]$ 为缺失数值之和。

根据补全后数值各不相同可知,两者必不可能同时位于 $t = \left \lfloor \frac{cur}{2} \right \rfloor$ 的同一侧(偏大、偏小或数值重复),因此我们可以计算 $[1, t]$ 范围内的理论总和与实际总和之间的差值来确定其一(将问题转换为求解缺失一值),再结合缺失两值之和 $sum$ 算得答案。

Java 代码:
```Java
class Solution {
    public int[] missingTwo(int[] nums) {
        int n = nums.length + 2, cur = n * (1 + n) / 2;
        for (int x : nums) cur -= x;
        int sum = cur, t = cur / 2;
        cur = t * (1 + t) / 2;
        for (int x : nums) {
            if (x <= t) cur -= x;
        }
        return new int[]{cur, sum - cur};
    }
}
```
TypeScript 代码:
```TypeScript
function missingTwo(nums: number[]): number[] {
    let n = nums.length + 2, cur = Math.floor(n * (1 + n) / 2)
    for (let x of nums) cur -= x
    let sum = cur, t = Math.floor(cur / 2)
    cur = Math.floor(t * (1 + t) / 2)
    for (let x of nums) {
        if (x <= t) cur -= x
    }
    return [cur, sum - cur]
};
```
Python 代码:
```Python
class Solution:
    def missingTwo(self, nums: List[int]) -> List[int]:
        n = len(nums) + 2
        cur = n * (1 + n) // 2 - sum(nums)
        tot, t = cur, cur // 2
        cur = t * (1 + t) // 2 - sum([x for x in nums if x <= t])
        return [cur, tot - cur]
```
* 时间复杂度:$O(n)$
* 空间复杂度:$O(1)$

---

### 异或

另外一类求解方法是利用「异或」+「lowbit」。

由于我们明确了是在 $[1, n + 2]$ 中缺失了两个数,我们可以先通过异或 $[1, n + 2]$ 以及所有的 $nums[i]$ 来得到缺失两个数值异或和 `t`。

我们知道异或结果二进制表示为 $1$ 代表了两缺失值该位置数值不同(一个该位置 $0$,另一个该位置为 $1$),我们可以根据异或和 `t` 中任意一位为 $1$ 的位置来将两个缺失值划分到两组中。

更加优雅的方式是使用 `lowbit` 操作:`d = t & -t` 可快速得到只保留 `t` 中最低位 `1` 的对应数值。

随后将 $[1, n + 2]$ 中满足 `i & d != 0` 的所有 `i`(含义为对应位置为 $1$ 的数值)与 $nums[i]$ 中满足 `nums[i] & d != 0` 的所有 $nums[i]$(含义为对应位置为 $1$ 的数值) 进行异或,最终能够确定其中一个缺失值,再结合最开始的异或和 `t` 即可确定另外一个缺失值。

Java 代码:
```Java
class Solution {
    public int[] missingTwo(int[] nums) {
        int n = nums.length + 2, cur = 0;
        for (int i = 1; i <= n; i++) cur ^= i;
        for (int x : nums) cur ^= x;
        int t = cur, d = cur & -cur;
        cur = 0;
        for (int i = 1; i <= n; i++) {
            if ((d & i) != 0) cur ^= i;
        }
        for (int x : nums) {
            if ((d & x) != 0) cur ^= x;
        }
        return new int[]{cur, t ^ cur};
    }
}
```
TypeScript 代码:
```TypeScript
function missingTwo(nums: number[]): number[] {
    let n = nums.length + 2, cur = 0
    for (let i = 1; i <= n; i++) cur ^= i
    for (let x of nums) cur ^= x
    const t = cur, d = cur & -cur
    cur = 0
    for (let i = 1; i <= n; i++) {
        if ((d & i) != 0) cur ^= i
    }
    for (let x of nums) {
        if ((d & x) != 0) cur ^= x
    }
    return [cur, t ^ cur]
};
```
Python 代码:
```Python
class Solution:
    def missingTwo(self, nums: List[int]) -> List[int]:
        n, cur = len(nums) + 2, 0
        for i in range(1, n + 1):
            cur ^= i
        for x in nums:
            cur ^= x
        t, d = cur, cur & -cur
        cur = 0
        for i in range(1, n + 1):
            if d & i:
                cur ^= i
        for x in nums:
            if d & x:
                cur ^= x
        return [cur, t ^ cur]
```
* 时间复杂度:$O(n)$
* 空间复杂度:$O(1)$

### 最后

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

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

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

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


面试题 17.21. 直方图的水量

Question:
给定一个直方图(也称柱状图),假设有人从上面源源不断地倒水,最后直方图能存多少水量?直方图的宽度为 1。

上面是由数组 [0,1,0,2,1,0,1,3,2,1,2,1] 表示的直方图,在这种情况下,可以接 6 个单位的水(蓝色部分表示水)。 感谢 Marcos 贡献此图。

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

输出: 6
```

Solution:
### 朴素解法

对于每根柱子而言,我们只需要找出「其左边最高的柱子」和「其右边最高的柱子」。

对左右的最高柱子取一个最小值,再和当前柱子的高度做一个比较,即可得出当前位置可以接下的雨水。

同时,边缘的柱子不可能接到雨水(某一侧没有柱子)。

这样的做法属于「暴力做法」,但题目没有给数据范围,我们无法分析到底能否 AC。

唯唯诺诺交一个,过了 ~ (好题,建议加入蓝桥杯

代码:
```java []
class Solution {
    public int trap(int[] height) {
        int n = height.length;
        int ans = 0;
        for (int i = 1; i < n - 1; i++) {
            int cur = height[i];

            // 获取当前位置的左边最大值
            int l = Integer.MIN_VALUE;
            for (int j = i - 1; j >= 0; j--) l = Math.max(l, height[j]);
            if (l <= cur) continue;

            // 获取当前位置的右边边最大值
            int r = Integer.MIN_VALUE;
            for (int j = i + 1; j < n; j++) r = Math.max(r, height[j]);
            if (r <= cur) continue;

            // 计算当前位置可接的雨水
            ans += Math.min(l, r) - cur;
        }
        return ans;
    }
}
```
* 时间复杂度:需要处理所有非边缘的柱子,复杂度为 $O(n)$;对于每根柱子而言,需要往两边扫描分别找到最大值,复杂度为 $O(n)$。整体复杂度为 $O(n^2)$。
* 空间复杂度:$O(1)$。

***

### 预处理最值

朴素解法的思路有了,我们想想怎么优化。

事实上,任何的优化无非都是「减少重复」。

想想在朴素思路中有哪些环节比较耗时,耗时环节中又有哪些地方是重复的,可以优化的。

首先对每根柱子进行遍历,求解每根柱子可以接下多少雨水,这个 $O(n)$ 操作肯定省不了。

**但在求解某根柱子可以接下多少雨水时,需要对两边进行扫描,求两侧的最大值。每一根柱子都进行这样的扫描操作,导致每个位置都被扫描了 $n$ 次。这个过程显然是可优化的。**

换句话说:**我们希望通过不重复遍历的方式找到任意位置的两侧最大值。**

问题转化为:**给定一个数组,如何求得任意位置的左半边的最大值和右半边的最大值。**

一个很直观的方案是:**直接将某个位置的两侧最大值存起来。**

我们可以先从两端分别出发,预处理每个位置的「左右最值」,这样可以将我们「查找左右最值」的复杂度降到 $O(1)$。

整体算法的复杂度也从 $O(n^2)$ 下降到 $O(n)$。

代码:
```java []
class Solution {
    public int trap(int[] height) {
        int n = height.length;
        int ans = 0;
        // 由于预处理最值的时候,我们会直接访问到 height[0] 或者 height[n - 1],因此要特判一下
        if (n == 0) return ans;

        // 预处理每个位置左边的最值
        int[] lm = new int[n];
        lm[0] = height[0];
        for (int i = 1; i < n; i++) lm[i] = Math.max(height[i], lm[i - 1]);

        // 预处理每个位置右边的最值
        int[] rm = new int[n];
        rm[n - 1] = height[n - 1];
        for (int i = n - 2; i >= 0; i--) rm[i] = Math.max(height[i], rm[i + 1]);

        for (int i = 1; i < n - 1; i++) {
            ans += Math.min(lm[i], rm[i]) - height[i];
        }
        return ans;
    }
}
```
* 时间复杂度:预处理出两个最大值数组,复杂度为 $O(n)$;计算每根柱子可接的雨水量,复杂度为 $O(n)$。整体复杂度为 $O(n)$。
* 空间复杂度:使用了数组存储两侧最大值。复杂度为 $O(n)$。

***

### 单调栈

前面我们讲到,优化思路将问题转化为:**给定一个数组,如何求得任意位置的左半边的最大值和右半边的最大值。**

但仔细一想,其实我们并不需要找两侧最大值,只需要找到两侧最近的比当前位置高的柱子就行了。

针对这一类找最近值的问题,有一个通用解法:**单调栈**。

**单调栈其实就是在栈的基础上,维持一个栈内元素单调。**

在这道题,由于需要找某个位置两侧比其高的柱子(只有两侧有比当前位置高的柱子,当前位置才能接下雨水),我们可以维持栈内元素的单调递减。

**PS. 找某侧最近一个比其大的值,使用单调栈维持栈内元素递减;找某侧最近一个比其小的值,使用单调栈维持栈内元素递增 ...**

当某个位置的元素弹出栈时,例如位置 `a` ,我们自然可以得到 `a` 位置两侧比 `a` 高的柱子:

* 一个是导致 `a` 位置元素弹出的柱子( `a` 右侧比 `a` 高的柱子)
* 一个是 `a` 弹栈后的栈顶元素(`a` 左侧比 `a` 高的柱子)

当有了 `a` 左右两侧比 `a` 高的柱子后,便可计算 `a` 位置可接下的雨水量。

代码:
```java []
class Solution {
    public int trap(int[] height) {
        int n = height.length;
        int ans = 0;
        Deque<Integer> d = new ArrayDeque<>();
        for (int i = 0; i < n; i++) {
            while (!d.isEmpty() && height[i] > height[d.peekLast()]) {
                int cur = d.pollLast();

                // 如果栈内没有元素,说明当前位置左边没有比其高的柱子,跳过
                if (d.isEmpty()) continue;

                // 左右位置,并有左右位置得出「宽度」和「高度」
                int l = d.peekLast(), r = i;
                int w = r - l + 1 - 2;
                int h = Math.min(height[l], height[r]) - height[cur];
                ans += w * h;
            }
            d.addLast(i);
        }
        return ans;
    }
}
```
* 时间复杂度:每个元素最多进栈和出栈一次。复杂度为 $O(n)$。
* 空间复杂度:栈最多存储 $n$ 个元素。复杂度为 $O(n)$。

***

### 面积差值

事实上,我们还能利用「面积差值」来进行求解。

我们先统计出「柱子面积」$sum$ 和「以柱子个数为宽、最高柱子高度为高的矩形面积」$full$。

然后分别「从左往右」和「从右往左」计算一次最大高度覆盖面积 $lSum$ 和 $rSum$。

**显然会出现重复面积,并且重复面积只会独立地出现在「山峰」的左边和右边。**

利用此特性,我们可以通过简单的等式关系求解出「雨水面积」:

代码:
```java []
class Solution {
    public int trap(int[] height) {
        int n = height.length;

        int sum = 0, max = 0;
        for (int i = 0; i < n; i++) {
            int cur = height[i];
            sum += cur;
            max = Math.max(max, cur);
        }
        int full = max * n;

        int lSum = 0, lMax = 0;
        for (int i = 0; i < n; i++) {
            lMax = Math.max(lMax, height[i]);
            lSum += lMax;
        }

        int rSum = 0, rMax = 0;
        for (int i = n - 1; i >= 0; i--) {
            rMax = Math.max(rMax, height[i]);
            rSum += rMax;
        }

        return lSum + rSum - full - sum;
    }
}
```
* 时间复杂度:$O(n)$。
* 空间复杂度:$O(1)$。

### 最后

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

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

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

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