From c27d12f2d23d43fd283cba11d6150b51b05ad6dc Mon Sep 17 00:00:00 2001 From: krahets Date: Sat, 11 May 2024 15:33:42 +0800 Subject: [PATCH] Fix 6. and 239. in selected_coding_interview --- ...43\346\234\200\345\244\247\345\200\274.md" | 302 ++++++++---------- ...27\345\275\242\345\217\230\346\215\242.md" | 57 +++- 2 files changed, 180 insertions(+), 179 deletions(-) diff --git "a/selected_coding_interview/docs/239. \346\273\221\345\212\250\347\252\227\345\217\243\346\234\200\345\244\247\345\200\274.md" "b/selected_coding_interview/docs/239. \346\273\221\345\212\250\347\252\227\345\217\243\346\234\200\345\244\247\345\200\274.md" index 1eaf142..4c24875 100644 --- "a/selected_coding_interview/docs/239. \346\273\221\345\212\250\347\252\227\345\217\243\346\234\200\345\244\247\345\200\274.md" +++ "b/selected_coding_interview/docs/239. \346\273\221\345\212\250\347\252\227\345\217\243\346\234\200\345\244\247\345\200\274.md" @@ -1,140 +1,89 @@ -# 方法一:辅助矩阵 +## 解题思路: -如下图所示,矩阵顺时针旋转 90º 后,可找到以下规律: +设窗口区间为 $[i, j]$ ,最大值为 $x_j$ 。当窗口向前移动一格,则区间变为 $[i+1,j+1]$ ,即添加了 $nums[j + 1]$ ,删除了 $nums[i]$ 。 -- 「第 $i$ 行」元素旋转到「第 $n - 1 - i$ 列」元素; -- 「第 $j$ 列」元素旋转到「第 $j$ 行」元素; - -因此,对于矩阵任意第 $i$ 行、第 $j$ 列元素 $matrix[i][j]$ ,矩阵旋转 90º 后「元素位置旋转公式」为: +若只向窗口 $[i, j]$ 右边添加数字 $nums[j + 1]$ ,则新窗口最大值可以 **通过一次对比** 使用 $O(1)$ 时间得到,即: $$ -\begin{aligned} -matrix[i][j] & \rightarrow matrix[j][n - 1 - i] \\ -原索引位置 & \rightarrow 旋转后索引位置 -\end{aligned} +x_{j+1} = \max(x_{j}, nums[j + 1]) $$ -![ccw-01-07.001.png](https://pic.leetcode-cn.com/1638557961-AVzCQb-ccw-01-07.001.png) - -根据以上「元素旋转公式」,考虑遍历矩阵,将各元素依次写入到旋转后的索引位置。但仍**存在问题**:在写入一个元素 $matrix[i][j] \rightarrow matrix[j][n - 1 - i]$ 后,原矩阵元素 $matrix[j][n - 1 - i]$ 就会**被覆盖(即丢失)**,而此丢失的元素就无法被写入到旋转后的索引位置了。 - -为解决此问题,考虑借助一个「辅助矩阵」暂存原矩阵,通过遍历辅助矩阵所有元素,将各元素填入「原矩阵」旋转后的新索引位置即可。 - -```Python [] -class Solution: - def rotate(self, matrix: List[List[int]]) -> None: - n = len(matrix) - # 深拷贝 matrix -> tmp - tmp = copy.deepcopy(matrix) - # 根据元素旋转公式,遍历修改原矩阵 matrix 的各元素 - for i in range(n): - for j in range(n): - matrix[j][n - 1 - i] = tmp[i][j] -``` - -```Java [] -class Solution { - public void rotate(int[][] matrix) { - int n = matrix.length; - // 深拷贝 matrix -> tmp - int[][] tmp = new int[n][]; - for (int i = 0; i < n; i++) - tmp[i] = matrix[i].clone(); - // 根据元素旋转公式,遍历修改原矩阵 matrix 的各元素 - for (int i = 0; i < n; i++) { - for (int j = 0; j < n; j++) { - matrix[j][n - 1 - i] = tmp[i][j]; - } - } - } -} -``` - -```C++ [] -class Solution { -public: - void rotate(vector>& matrix) { - int n = matrix.size(); - // 深拷贝 matrix -> tmp - vector> tmp = matrix; - // 根据元素旋转公式,遍历修改原矩阵 matrix 的各元素 - for (int i = 0; i < n; i++) { - for (int j = 0; j < n; j++) { - matrix[j][n - 1 - i] = tmp[i][j]; - } - } - } -}; -``` - -如以上代码所示,遍历矩阵所有元素的时间复杂度为 $O(N^2)$ ;由于借助了一个辅助矩阵,**空间复杂度**为 $O(N^2)$ 。 - -# 方法二:原地修改 - -考虑不借助辅助矩阵,通过在原矩阵中直接「原地修改」,实现空间复杂度 $O(1)$ 的解法。 - -以位于矩阵四个角点的元素为例,设矩阵左上角元素 $A$ 、右上角元素 $B$ 、右下角元素 $C$ 、左下角元素 $D$ 。矩阵旋转 90º 后,相当于依次先后执行 $D \rightarrow A$ , $C \rightarrow D$ , $B \rightarrow C$ , $A \rightarrow B$ 修改元素,即如下「首尾相接」的元素旋转操作: +而由于删除的 $nums[i]$ 可能恰好是窗口内唯一的最大值 $x_j$ ,因此不能通过以上方法计算 $x_{j+1}$ ,而必须使用 $O(j-i)$ 时间, **遍历整个窗口区间** 获取最大值,即: $$ -A \leftarrow D \leftarrow C \leftarrow B \leftarrow A +x_{j+1} = \max(nums(i+1), \cdots , num(j+1)) $$ -![ccw-01-07.002.png](https://pic.leetcode-cn.com/1638557961-BSxFQQ-ccw-01-07.002.png) +根据以上分析,可得 **暴力法** 的时间复杂度为 $O((n-k+1)k) \approx O(nk)$ 。 -如上图所示,由于第 $1$ 步 $D \rightarrow A$ 已经将 $A$ 覆盖(导致 $A$ 丢失),此丢失导致最后第 $4$ 步 $A \rightarrow B$ 无法赋值。为解决此问题,考虑借助一个「辅助变量 $tmp$ 」预先存储 $A$ ,此时的旋转操作变为: +- 设数组 $nums$ 的长度为 $n$ ,则共有 $(n-k+1)$ 个窗口; +- 获取每个窗口最大值需线性遍历,时间复杂度为 $O(k)$ 。 -$$ -暂存 \ tmp = A \\ -A \leftarrow D \leftarrow C \leftarrow B \leftarrow tmp -$$ +![Picture1.png](https://pic.leetcode-cn.com/1600878237-pBiBdf-Picture1.png){:width=650} -![ccw-01-07.003.png](https://pic.leetcode-cn.com/1638557961-hYpOoH-ccw-01-07.003.png) +> **本题难点:** 如何在每次窗口滑动后,将 “获取窗口内最大值” 的时间复杂度从 $O(k)$ 降低至 $O(1)$ 。 -如上图所示,一轮可以完成矩阵 4 个元素的旋转。因而,只要分别以矩阵左上角 $1/4$ 的各元素为起始点执行以上旋转操作,即可完整实现矩阵旋转。 +回忆 [最小栈](https://leetcode.cn/problems/min-stack/) ,其使用 **单调栈** 实现了随意入栈、出栈情况下的 $O(1)$ 时间获取 “栈内最小值” 。本题同理,不同点在于 “出栈操作” 删除的是 “列表尾部元素” ,而 “窗口滑动” 删除的是 “列表首部元素” 。 -具体来看,当矩阵大小 $n$ 为偶数时,取前 $\frac{n}{2}$ 行、前 $\frac{n}{2}$ 列的元素为起始点;当矩阵大小 $n$ 为奇数时,取前 $\frac{n}{2}$ 行、前 $\frac{n + 1}{2}$ 列的元素为起始点。 +窗口对应的数据结构为 **双端队列** ,本题使用 **单调队列** 即可解决以上问题。遍历数组时,每轮保证单调队列 $deque$ : -令 $matrix[i][j] = A$ ,根据文章开头的元素旋转公式,可推导得适用于任意起始点的元素旋转操作: +1. $deque$ 内 **仅包含窗口内的元素** $\Rightarrow$ 每轮窗口滑动移除了元素 $nums[i - 1]$ ,需将 $deque$ 内的对应元素一起删除。 +2. $deque$ 内的元素 **非严格递减** $\Rightarrow$ 每轮窗口滑动添加了元素 $nums[j + 1]$ ,需将 $deque$ 内所有 $< nums[j + 1]$ 的元素删除。 -$$ -暂存 tmp = matrix[i][j] \\ -matrix[i][j] \leftarrow matrix[n - 1 - j][i] \leftarrow matrix[n - 1 - i][n - 1 - j] \leftarrow matrix[j][n - 1 - i] \leftarrow tmp -$$ +### 算法流程: -> 如下图所示,为示例矩阵的算法执行流程。 +1. **初始化:** 双端队列 $deque$ ,结果列表 $res$ ,数组长度 $n$ ; +2. **滑动窗口:** 左边界范围 $i \in [1 - k, n - k]$ ,右边界范围 $j \in [0, n - 1]$ ; + 1. 若 $i > 0$ 且 队首元素 $deque[0]$ $=$ 被删除元素 $nums[i - 1]$ :则队首元素出队; + 2. 删除 $deque$ 内所有 $< nums[j]$ 的元素,以保持 $deque$ 递减; + 3. 将 $nums[j]$ 添加至 $deque$ 尾部; + 4. 若已形成窗口(即 $i \geq 0$ ):将窗口最大值(即队首元素 $deque[0]$ )添加至列表 $res$ ; +3. **返回值:** 返回结果列表 $res$ ; - + -## 代码 +## 代码: -> 后三个 Tab 为「代码注释解析」。 +Python 通过 `zip(range(), range())` 可实现滑动窗口的左右边界 `i, j` 同时遍历。 ```Python [] class Solution: - def rotate(self, matrix: List[List[int]]) -> None: - n = len(matrix) - for i in range(n // 2): - for j in range((n + 1) // 2): - tmp = matrix[i][j] - matrix[i][j] = matrix[n - 1 - j][i] - matrix[n - 1 - j][i] = matrix[n - 1 - i][n - 1 - j] - matrix[n - 1 - i][n - 1 - j] = matrix[j][n - 1 - i] - matrix[j][n - 1 - i] = tmp + def maxSlidingWindow(self, nums: List[int], k: int) -> List[int]: + deque = collections.deque() + res, n = [], len(nums) + for i, j in zip(range(1 - k, n + 1 - k), range(n)): + # 删除 deque 中对应的 nums[i-1] + if i > 0 and deque[0] == nums[i - 1]: + deque.popleft() + # 保持 deque 递减 + while deque and deque[-1] < nums[j]: + deque.pop() + deque.append(nums[j]) + # 记录窗口最大值 + if i >= 0: + res.append(deque[0]) + return res ``` ```Java [] class Solution { - public void rotate(int[][] matrix) { - int n = matrix.length; - for (int i = 0; i < n / 2; i++) { - for (int j = 0; j < (n + 1) / 2; j++) { - int tmp = matrix[i][j]; - matrix[i][j] = matrix[n - 1 - j][i]; - matrix[n - 1 - j][i] = matrix[n - 1 - i][n - 1 - j]; - matrix[n - 1 - i][n - 1 - j] = matrix[j][n - 1 - i]; - matrix[j][n - 1 - i] = tmp; - } + public int[] maxSlidingWindow(int[] nums, int k) { + if(nums.length == 0 || k == 0) return new int[0]; + Deque deque = new LinkedList<>(); + int[] res = new int[nums.length - k + 1]; + for(int j = 0, i = 1 - k; j < nums.length; i++, j++) { + // 删除 deque 中对应的 nums[i-1] + if(i > 0 && deque.peekFirst() == nums[i - 1]) + deque.removeFirst(); + // 保持 deque 递减 + while(!deque.isEmpty() && deque.peekLast() < nums[j]) + deque.removeLast(); + deque.addLast(nums[j]); + // 记录窗口最大值 + if(i >= 0) + res[i] = deque.peekFirst(); } + return res; } } ``` @@ -142,57 +91,75 @@ class Solution { ```C++ [] class Solution { public: - void rotate(vector>& matrix) { - int n = matrix.size(); - for (int i = 0; i < n / 2; i++) { - for (int j = 0; j < (n + 1) / 2; j++) { - int tmp = matrix[i][j]; - matrix[i][j] = matrix[n - 1 - j][i]; - matrix[n - 1 - j][i] = matrix[n - 1 - i][n - 1 - j]; - matrix[n - 1 - i][n - 1 - j] = matrix[j][n - 1 - i]; - matrix[j][n - 1 - i] = tmp; - } + vector maxSlidingWindow(vector& nums, int k) { + if(nums.size() == 0 || k == 0) return {}; + deque deque; + vector res(nums.size() - k + 1); + for(int j = 0, i = 1 - k; j < nums.size(); i++, j++) { + // 删除 deque 中对应的 nums[i-1] + if(i > 0 && deque.front() == nums[i - 1]) + deque.pop_front(); + // 保持 deque 递减 + while(!deque.empty() && deque.back() < nums[j]) + deque.pop_back(); + deque.push_back(nums[j]); + // 记录窗口最大值 + if(i >= 0) + res[i] = deque.front(); } + return res; + } } }; ``` +可以将 “未形成窗口” 和 “形成窗口后” 两个阶段拆分到两个循环里实现。代码虽变长,但减少了冗余的判断操作。 + ```Python [] class Solution: - def rotate(self, matrix: List[List[int]]) -> None: - # 设矩阵行列数为 n - n = len(matrix) - # 起始点范围为 0 <= i < n // 2 , 0 <= j < (n + 1) // 2 - # 其中 '//' 为整数除法 - for i in range(n // 2): - for j in range((n + 1) // 2): - # 暂存 A 至 tmp - tmp = matrix[i][j] - # 元素旋转操作 A <- D <- C <- B <- tmp - matrix[i][j] = matrix[n - 1 - j][i] - matrix[n - 1 - j][i] = matrix[n - 1 - i][n - 1 - j] - matrix[n - 1 - i][n - 1 - j] = matrix[j][n - 1 - i] - matrix[j][n - 1 - i] = tmp + def maxSlidingWindow(self, nums: List[int], k: int) -> List[int]: + if not nums or k == 0: return [] + deque = collections.deque() + # 未形成窗口 + for i in range(k): + while deque and deque[-1] < nums[i]: + deque.pop() + deque.append(nums[i]) + res = [deque[0]] + # 形成窗口后 + for i in range(k, len(nums)): + if deque[0] == nums[i - k]: + deque.popleft() + while deque and deque[-1] < nums[i]: + deque.pop() + deque.append(nums[i]) + res.append(deque[0]) + return res ``` ```Java [] class Solution { - public void rotate(int[][] matrix) { - // 设矩阵行列数为 n - int n = matrix.length; - // 起始点范围为 0 <= i < n / 2 , 0 <= j < (n + 1) / 2 - // 其中 '/' 为整数除法 - for (int i = 0; i < n / 2; i++) { - for (int j = 0; j < (n + 1) / 2; j++) { - // 暂存 A 至 tmp - int tmp = matrix[i][j]; - // 元素旋转操作 A <- D <- C <- B <- tmp - matrix[i][j] = matrix[n - 1 - j][i]; - matrix[n - 1 - j][i] = matrix[n - 1 - i][n - 1 - j]; - matrix[n - 1 - i][n - 1 - j] = matrix[j][n - 1 - i]; - matrix[j][n - 1 - i] = tmp; - } + public int[] maxSlidingWindow(int[] nums, int k) { + if(nums.length == 0 || k == 0) return new int[0]; + Deque deque = new LinkedList<>(); + int[] res = new int[nums.length - k + 1]; + // 未形成窗口 + for(int i = 0; i < k; i++) { + while(!deque.isEmpty() && deque.peekLast() < nums[i]) + deque.removeLast(); + deque.addLast(nums[i]); } + res[0] = deque.peekFirst(); + // 形成窗口后 + for(int i = k; i < nums.length; i++) { + if(deque.peekFirst() == nums[i - k]) + deque.removeFirst(); + while(!deque.isEmpty() && deque.peekLast() < nums[i]) + deque.removeLast(); + deque.addLast(nums[i]); + res[i - k + 1] = deque.peekFirst(); + } + return res; } } ``` @@ -200,27 +167,32 @@ class Solution { ```C++ [] class Solution { public: - void rotate(vector>& matrix) { - // 设矩阵行列数为 n - int n = matrix.size(); - // 起始点范围为 0 <= i < n / 2 , 0 <= j < (n + 1) / 2 - // 其中 '/' 为整数除法 - for (int i = 0; i < n / 2; i++) { - for (int j = 0; j < (n + 1) / 2; j++) { - // 暂存 A 至 tmp - int tmp = matrix[i][j]; - // 元素旋转操作 A <- D <- C <- B <- tmp - matrix[i][j] = matrix[n - 1 - j][i]; - matrix[n - 1 - j][i] = matrix[n - 1 - i][n - 1 - j]; - matrix[n - 1 - i][n - 1 - j] = matrix[j][n - 1 - i]; - matrix[j][n - 1 - i] = tmp; - } + vector maxSlidingWindow(vector& nums, int k) { + if(nums.size() == 0 || k == 0) return {}; + deque deque; + vector res(nums.size() - k + 1); + // 未形成窗口 + for(int i = 0; i < k; i++) { + while(!deque.empty() && deque.back() < nums[i]) + deque.pop_back(); + deque.push_back(nums[i]); + } + res[0] = deque.front(); + // 形成窗口后 + for(int i = k; i < nums.size(); i++) { + if(deque.front() == nums[i - k]) + deque.pop_front(); + while(!deque.empty() && deque.back() < nums[i]) + deque.pop_back(); + deque.push_back(nums[i]); + res[i - k + 1] = deque.front(); } + return res; } }; ``` -## 复杂度分析 +### 复杂度分析: -- **时间复杂度 $O(N^2)$ :** 其中 $N$ 为输入矩阵的行(列)数。需要将矩阵中每个元素旋转到新的位置,即对矩阵所有元素操作一次,使用 $O(N^2)$ 时间。 -- **空间复杂度 $O(1)$ :** 临时变量 $tmp$ 使用常数大小的额外空间。值得注意,当循环中进入下轮迭代,上轮迭代初始化的 $tmp$ 占用的内存就会被自动释放,因此无累计使用空间。 +- **时间复杂度 $O(n)$ :** 其中 $n$ 为数组 $nums$ 长度;线性遍历 $nums$ 占用 $O(n)$ ;每个元素最多仅入队和出队一次,因此单调队列 $deque$ 占用 $O(2n)$ 。 +- **空间复杂度 $O(k)$ :** 双端队列 $deque$ 中最多同时存储 $k$ 个元素(即窗口大小)。 diff --git "a/selected_coding_interview/docs/6. N \345\255\227\345\275\242\345\217\230\346\215\242.md" "b/selected_coding_interview/docs/6. N \345\255\227\345\275\242\345\217\230\346\215\242.md" index b586706..5c88b7e 100644 --- "a/selected_coding_interview/docs/6. N \345\255\227\345\275\242\345\217\230\346\215\242.md" +++ "b/selected_coding_interview/docs/6. N \345\255\227\345\275\242\345\217\230\346\215\242.md" @@ -1,20 +1,22 @@ -#### 解题思路: - -- **题目理解:** - - 字符串 `s` 是以 $Z$ 字形为顺序存储的字符串,目标是按行打印。 - - 设 `numRows` 行字符串分别为 $s_1$ , $s_2$ ,..., $s_n$,则容易发现:按顺序遍历字符串 `s` 时,每个字符 `c` 在 $Z$ 字形中对应的 **行索引** 先从 $s_1$ 增大至 $s_n$,再从 $s_n$ 减小至 $s_1$ …… 如此反复。 - - 因此,解决方案为:模拟这个行索引的变化,在遍历 `s` 中把每个字符填到正确的行 `res[i]` 。 -- **算法流程:** 按顺序遍历字符串 `s`; - 1. **`res[i] += c`:** 把每个字符 `c` 填入对应行 $s_i$; - 2. **`i += flag`:** 更新当前字符 `c` 对应的行索引; - 3. **`flag = - flag`:** 在达到 $Z$ 字形转折点时,执行反向。 -- **复杂度分析:** - - 时间复杂度 $O(N)$ :遍历一遍字符串 `s`; - - 空间复杂度 $O(N)$ :各行字符串共占用 $O(N)$ 额外空间。 +## 解题思路: + +字符串 `s` 是以 $Z$ 字形为顺序存储的字符串,目标是按行打印。 + +设 `numRows` 行字符串分别为 $s_1$ , $s_2$ , $\dots$ , $s_n$,则容易发现:按顺序遍历字符串 `s` 时,每个字符 `c` 在 N 字形中对应的 **行索引** 先从 $s_1$ 增大至 $s_n$,再从 $s_n$ 减小至 $s_1$ …… 如此反复。 + +因此解决方案为:模拟这个行索引的变化,在遍历 `s` 中把每个字符填到正确的行 `res[i]` 。 + +### 算法流程: + +按顺序遍历字符串 `s` : + +1. **`res[i] += c`:** 把每个字符 `c` 填入对应行 $s_i$; +2. **`i += flag`:** 更新当前字符 `c` 对应的行索引; +3. **`flag = - flag`:** 在达到 $Z$ 字形转折点时,执行反向。 -#### 代码: +## 代码: ```Python [] class Solution: @@ -47,3 +49,30 @@ class Solution { } } ``` + +```C++ [] +class Solution { +public: + string convert(string s, int numRows) { + if (numRows < 2) + return s; + vector rows(numRows); + int i = 0, flag = -1; + for (char c : s) { + rows[i].push_back(c); + if (i == 0 || i == numRows -1) + flag = - flag; + i += flag; + } + string res; + for (const string &row : rows) + res += row; + return res; + } +}; +``` + +### 复杂度分析: + +- **时间复杂度 $O(N)$** :遍历一遍字符串 `s`; +- **空间复杂度 $O(N)$** :各行字符串共占用 $O(N)$ 额外空间。