Skip to content

Commit

Permalink
更新「09. 基础算法」相关内容
Browse files Browse the repository at this point in the history
  • Loading branch information
itcharge committed Dec 8, 2023
1 parent 9f3fb25 commit 41b4368
Show file tree
Hide file tree
Showing 6 changed files with 125 additions and 124 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -87,9 +87,9 @@

#### 3.1.2 题目大意

**描述**:给定一个整数数组 `nums` 和一个整数目标值 `target`
**描述**:给定一个整数数组 $nums$ 和一个整数目标值 $target$

**要求**:在该数组中找出和为 `target` 的两个整数,并输出这两个整数的下标。可以按任意顺序返回答案。
**要求**:在该数组中找出和为 $target$ 的两个整数,并输出这两个整数的下标。可以按任意顺序返回答案。

**说明**

Expand Down Expand Up @@ -121,8 +121,8 @@

##### 思路 1:枚举算法

1. 使用两重循环枚举数组中每一个数 `nums[i]``nums[j]`,判断所有的 `nums[i] + nums[j]` 是否等于 `target`
2. 如果出现 `nums[i] + nums[j] == target`,则说明数组中存在和为 `target` 的两个整数,将两个整数的下标 `i``j` 输出即可。
1. 使用两重循环枚举数组中每一个数 $nums[i]$、$nums[j]$,判断所有的 $nums[i] + nums[j]$ 是否等于 $target$
2. 如果出现 $nums[i] + nums[j] == target$,则说明数组中存在和为 $target$ 的两个整数,将两个整数的下标 $i$、$j$ 输出即可。

##### 思路 1:代码

Expand All @@ -138,7 +138,7 @@ class Solution:

##### 思路 1:复杂度分析

- **时间复杂度**:$O(n^2)$
- **时间复杂度**:$O(n^2)$,其中 $n$ 为数组 $nums$ 的元素数量。
- **空间复杂度**:$O(1)$。

### 3.2 计数质数
Expand All @@ -155,7 +155,7 @@ class Solution:

**说明**

- $0 \le n \le 5 * 10^6$。
- $0 \le n \le 5 \times 10^6$。

**示例**

Expand Down Expand Up @@ -184,7 +184,7 @@ class Solution:

这样我们就可以通过枚举 $[2, n - 1]$ 上的所有数 $x$,并判断 $x$ 是否为质数。

在遍历枚举的同时,我们维护一个用于统计小于 $n$ 的质数数量的变量 `cnt`。如果符合要求,则将计数 `cnt` 加 $1$。最终返回该数目作为答案。
在遍历枚举的同时,我们维护一个用于统计小于 $n$ 的质数数量的变量 $cnt$。如果符合要求,则将计数 $cnt$ 加 $1$。最终返回该数目作为答案。

考虑到如果 $i$ 是 $x$ 的因数,则 $\frac{x}{i}$ 也必然是 $x$ 的因数,则我们只需要检验这两个因数中的较小数即可。而较小数一定会落在 $[2, \sqrt x]$ 上。因此我们在检验 $x$ 是否为质数时,只需要枚举 $[2, \sqrt x]$ 中的所有数即可。

Expand Down Expand Up @@ -254,7 +254,7 @@ class Solution:

我们可以在 $[1, n]$ 区间中枚举整数三元组 $(a, b, c)$ 中的 $a$ 和 $b$。然后判断 $a^2 + b^2$ 是否小于等于 $n$,并且是完全平方数。

在遍历枚举的同时,我们维护一个用于统计平方和三元组数目的变量 `cnt`。如果符合要求,则将计数 `cnt` 加 $1$。最终,我们返回该数目作为答案。
在遍历枚举的同时,我们维护一个用于统计平方和三元组数目的变量 $cnt$。如果符合要求,则将计数 $cnt$ 加 $1$。最终,我们返回该数目作为答案。

利用枚举算法统计平方和三元组数目的时间复杂度为 $O(n^2)$。

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@

$fact(n) = \begin{cases} 1 & \text{n = 0} \cr n * fact(n - 1) & \text{n > 0} \end{cases}$

根据阶乘计算方法的数学定义,我们可以使用调用函数自身的方式来实现阶乘函数 `fact(n)` ,其实现代码可以写作:
根据阶乘计算方法的数学定义,我们可以使用调用函数自身的方式来实现阶乘函数 $fact(n)$ ,其实现代码可以写作:

```python
def fact(n):
Expand All @@ -15,7 +15,7 @@ def fact(n):
return n * fact(n - 1)
```

`n = 6` 为例,上述代码中阶乘函数 `fact(6):` 的计算过程如下:
$n = 6$ 为例,上述代码中阶乘函数 $fact(6)$ 的计算过程如下:

```python
fact(6)
Expand All @@ -36,21 +36,21 @@ fact(6)

上面的例子也可以用语言描述为:

1. 函数从 `fact(6)` 开始,一层层地调用 `fact(5)``fact(4)`、…… 一直调用到最底层的 `fact(0)`
2.`n == 0` 时,`fact(0)` 不再继续调用自身,而是直接向上一层返回结果 `1`
3. `fact(1)` 通过下一层 `fact(0)` 的计算结果得出 `fact(1) = 1 * 1 = 1`,从而向上一层返回结果 `1`
4. `fact(2)` 通过下一层 `fact(1)` 的计算结果得出 `fact(2) = 2 * 1 = 2 `,从而向上一层返回结果 `2`
5. `fact(3)` 通过下一层 `fact(2)` 的计算结果得出 `fact(3) = 3 * 2 = 6 `,从而向上一层返回结果 `6`
6. `fact(4)` 通过下一层 `fact(3)` 的计算结果得出 `fact(4) = 4 * 6 = 24`,从而向上一层返回结果 `24`
7. `fact(5)` 通过下一层 `fact(4)` 的计算结果得出 `fact(5) = 5 * 24 = 120`,从而向上一层返回结果 `120`
8. `fact(6)` 通过下一层 `fact(5)` 的计算结果得出 `fact(6) = 6 * 120 = 720`,从而返回函数的最终结果 `720`
1. 函数从 $fact(6)$ 开始,一层层地调用 $fact(5)$、$fact(4)$、…… 一直调用到最底层的 $fact(0)$
2.$n == 0$ 时,$fact(0)$ 不再继续调用自身,而是直接向上一层返回结果 $1$
3. $fact(1)$ 通过下一层 $fact(0)$ 的计算结果得出 $fact(1) = 1 \times 1 = 1$,从而向上一层返回结果 $1$
4. $fact(2)$ 通过下一层 $fact(1)$ 的计算结果得出 $fact(2) = 2 \times 1 = 2 $,从而向上一层返回结果 $2$
5. $fact(3)$ 通过下一层 $fact(2)$ 的计算结果得出 $fact(3) = 3 \times 2 = 6 $,从而向上一层返回结果 $6$
6. $fact(4)$ 通过下一层 $fact(3)$ 的计算结果得出 $fact(4) = 4 \times 6 = 24$,从而向上一层返回结果 $24$
7. $fact(5)$ 通过下一层 $fact(4)$ 的计算结果得出 $fact(5) = 5 \times 24 = 120$,从而向上一层返回结果 $120$
8. $fact(6)$ 通过下一层 $fact(5)$ 的计算结果得出 $fact(6) = 6 \times 120 = 720$,从而返回函数的最终结果 $720$

这就是阶乘函数的递归计算过程。

根据上面的描述,我们可以把阶乘函数的递归计算过程分为两个部分:

1. 先逐层向下调用自身,直到达到结束条件(即 `n == 0`)。
2. 然后再向上逐层返回结果,直到返回原问题的解(即返回 `fact(6) == 720`)。
1. 先逐层向下调用自身,直到达到结束条件(即 $n == 0$)。
2. 然后再向上逐层返回结果,直到返回原问题的解(即返回 $fact(6) == 720$)。

这两个部分也可以叫做「递推过程」和「回归过程」,如下面两幅图所示:

Expand Down Expand Up @@ -117,11 +117,11 @@ fact(6)

递归的终止条件也叫做递归出口。在写出了递推公式之后,就要考虑递归的终止条件是什么。如果没有递归的终止条件,函数就会无限地递归下去,程序就会失控崩溃了。通常情况下,递归的终止条件是问题的边界值。

在找到递归的终止条件时,我们应该直接给出该条件下的处理方法。一般地,在这种情境下,问题的解决方案是直观的、容易的。例如阶乘中 `fact(0) = 1`。斐波那契数列中 `f(1) = 1, f(2) = 2`
在找到递归的终止条件时,我们应该直接给出该条件下的处理方法。一般地,在这种情境下,问题的解决方案是直观的、容易的。例如阶乘中 $fact(0) = 1$。斐波那契数列中 $f(1) = 1$,$f(2) = 2$

### 3.3 将递推公式和终止条件翻译成代码

在写出递推公式和明确终止条件之后,我们就可以将其翻译成代码了。这一步也可以分为 3 步来做:
在写出递推公式和明确终止条件之后,我们就可以将其翻译成代码了。这一步也可以分为 $3$ 步来做:

1. **定义递归函数**:明确函数意义、传入参数、返回结果等。
2. **书写递归主体**:提取重复的逻辑,缩小问题规模。
Expand All @@ -131,7 +131,7 @@ fact(6)

在定义递归函数时,一定要明确递归函数的意义,也就是要明白这个问题传入的参数是什么,最终返回的结果是要解决的什么问题。

比如说阶乘函数 `fact(n)`,这个函数的传入参数是问题的规模 `n`,最终返回的结果是 `n` 的阶乘值。
比如说阶乘函数 $fact(n)$,这个函数的传入参数是问题的规模 $n$,最终返回的结果是 $n$ 的阶乘值。

#### 3.3.2 书写递归主体

Expand Down Expand Up @@ -191,15 +191,15 @@ $f(n) = \begin{cases} 0 & n = 0 \cr 1 & n = 1 \cr f(n - 2) + f(n - 1) & n > 1 \e

#### 5.1.2 题目大意

**描述**:给定一个整数 `n`
**描述**:给定一个整数 $n$

**要求**:计算第 `n` 个斐波那契数。
**要求**:计算第 $n$ 个斐波那契数。

**说明**

- 斐波那契数列的定义如下:
- `f(0) = 0, f(1) = 1`
- `f(n) = f(n - 1) + f(n - 2)`,其中 `n > 1`
- $f(0) = 0, f(1) = 1$
- $f(n) = f(n - 1) + f(n - 2)$,其中 $n > 1$


**示例**
Expand All @@ -226,10 +226,10 @@ $f(n) = \begin{cases} 0 & n = 0 \cr 1 & n = 1 \cr f(n - 2) + f(n - 1) & n > 1 \e

根据我们的递推三步走策略,写出对应的递归代码。

1. 写出递推公式:`f(n) = f(n - 1) + f(n - 2)`
2. 明确终止条件:`f(0) = 0, f(1) = 1`
1. 写出递推公式:$f(n) = f(n - 1) + f(n - 2)$
2. 明确终止条件:$f(0) = 0, f(1) = 1$
3. 翻译为递归代码:
1. 定义递归函数:`fib(self, n)` 表示输入参数为问题的规模 `n`,返回结果为第 `n` 个斐波那契数。
1. 定义递归函数:`fib(self, n)` 表示输入参数为问题的规模 $n$,返回结果为第 $n$ 个斐波那契数。
2. 书写递归主体:`return self.fib(n - 1) + self.fib(n - 2)`
3. 明确递归终止条件:
1. `if n == 0: return 0`
Expand Down Expand Up @@ -260,7 +260,7 @@ class Solution:

#### 5.2.2 题目大意

**描述**:给定一个二叉树的根节点 `root`
**描述**:给定一个二叉树的根节点 $root$

**要求**:找出该二叉树的最大深度。

Expand Down Expand Up @@ -291,11 +291,11 @@ class Solution:

根据递归三步走策略,写出对应的递归代码。

1. 写出递推公式:`当前二叉树的最大深度 = max(当前二叉树左子树的最大深度, 当前二叉树右子树的最大深度) + 1`
1. 写出递推公式:**当前二叉树的最大深度 = max(当前二叉树左子树的最大深度, 当前二叉树右子树的最大深度) + 1**
- 即:先得到左右子树的高度,在计算当前节点的高度。
2. 明确终止条件:当前二叉树为空。
3. 翻译为递归代码:
1. 定义递归函数:`maxDepth(self, root)` 表示输入参数为二叉树的根节点 `root`,返回结果为该二叉树的最大深度。
1. 定义递归函数:`maxDepth(self, root)` 表示输入参数为二叉树的根节点 $root$,返回结果为该二叉树的最大深度。
2. 书写递归主体:`return max(self.maxDepth(root.left) + self.maxDepth(root.right))`
3. 明确递归终止条件:`if not root: return 0`

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,16 +18,18 @@

![](https://qcdn.itcharge.cn/images/20220414093828.png)

分治算法一般都比较适合使用递归算法来实现。但除了递归算法之外,分治算法还可以通过迭代算法来实现。比较常见的例子有:快速傅里叶变换算法、二分查找算法、非递归实现的归并排序算法等等。
一般情况下,分治算法比较适合使用递归算法来实现。但除了递归算法之外,分治算法还可以通过迭代算法来实现。比较常见的例子有:快速傅里叶变换算法、二分查找算法、非递归实现的归并排序算法等等。

我们先来讲解一下分支算法的适用条件,再来讲解一下基本步骤。

### 1.3 分治算法的适用条件

分治算法能够解决的问题,一般需要满足以下 $4$ 个条件:

1. 原问题可以分解为若干个规模较小的相同子问题。
2. 分解出来的子问题可以独立求解,即子问题之间不包含公共的子子问题。
3. 具有分解的终止条件,也就是说当问题的规模足够小时,能够用较简单的方法解决。
4. 子问题的解可以合并为原问题的解,并且合并操作的复杂度不能太高,否则就无法起到减少算法总体复杂度的效果了。
1. **可分解**原问题可以分解为若干个规模较小的相同子问题。
2. **子问题可独立求解**分解出来的子问题可以独立求解,即子问题之间不包含公共的子子问题。
3. **具有分解的终止条件**:当问题的规模足够小时,能够用较简单的方法解决。
4. **可合并**子问题的解可以合并为原问题的解,并且合并操作的复杂度不能太高,否则就无法起到减少算法总体复杂度的效果了。

## 2. 分治算法的基本步骤

Expand All @@ -46,15 +48,15 @@
按照分而治之的策略,在编写分治算法的代码时,也是按照上面的 $3$ 个步骤来编写的,其对应的伪代码如下:

```python
def divide_and_conquer(problem): # problem 为问题规模
if problem < d: # 当问题规模足够小时,直接解决该问题
return solove(); # 直接求解
def divide_and_conquer(problems_n): # problems_n 为问题规模
if problems_n < d: # 当问题规模足够小时,直接解决该问题
return solove() # 直接求解

k_problems = divide(problem) # 将问题分解为 k 个相同形式的子问题
problems_k = divide(problems_n) # 将问题分解为 k 个相同形式的子问题

res = [0 for _ in range(k)] # res 用来保存 k 个子问题的解
for k_problem in k_problems:
res[i] = divide_and_conquer(k_problem) # 递归的求解 k 个子问题
for problem_k in problems_k:
res[i] = divide_and_conquer(problem_k) # 递归的求解 k 个子问题

ans = merge(res) # 合并 k 个子问题的解
return ans # 返回原问题的解
Expand All @@ -66,7 +68,7 @@ def divide_and_conquer(problem): # problem 为问题规模

一般来讲,分治算法将一个问题划分为 $a$ 个形式相同的子问题,每个子问题的规模为 $n/b$,则总的时间复杂度的递归表达式可以表示为:

$T(n) = \begin{cases} \begin{array} \ \Theta{(1)} & n = 1 \cr a * T(n/b) + f(n) & n > 1 \end{array} \end{cases}$
$T(n) = \begin{cases} \begin{array} \ \Theta{(1)} & n = 1 \cr a \times T(n/b) + f(n) & n > 1 \end{array} \end{cases}$

其中,每次分解时产生的子问题个数是 $a$ ,每个子问题的规模是原问题规模的 $1 / b$,分解和合并 $a$ 个子问题的时间复杂度是 $f(n)$。

Expand All @@ -80,11 +82,11 @@ $T(n) = \begin{cases} \begin{array} \ \Theta{(1)} & n = 1 \cr a * T(n/b) + f(n)

我们得出归并排序算法的递归表达式如下:

$T(n) = \begin{cases} \begin{array} \ O{(1)} & n = 1 \cr 2T(n/2) + O(n) & n > 1 \end{array} \end{cases}$
$T(n) = \begin{cases} \begin{array} \ O{(1)} & n = 1 \cr 2 \times T(n/2) + O(n) & n > 1 \end{array} \end{cases}$

根据归并排序的递归表达式,当 $n > 1$ 时,可以递推求解:

$\begin{align} T(n) & = 2T(n/2) + O(n) \cr & = 2(2T(n / 4) + O(n/2)) + O(n) \cr & = 4T(n/4) + 2O(n) \cr & = 8T(n/8) + 3O(n) \cr & = …… \cr & = 2^x \times T(n/2^x) + x \times O(n) \end{align}$
$\begin{align} T(n) & = 2 \times T(n/2) + O(n) \cr & = 2 \times (2 \times T(n / 4) + O(n/2)) + O(n) \cr & = 4 \times T(n/4) + 2 \times O(n) \cr & = 8 \times T(n/8) + 3 \times O(n) \cr & = …… \cr & = 2^x \times T(n/2^x) + x \times O(n) \end{align}$

递推最终规模为 $1$,令 $n = 2^x$,则 $x = \log_2n$,则:

Expand Down Expand Up @@ -122,7 +124,7 @@ $T(n) = \begin{cases} \begin{array} \ O{(1)} & n = 1 \cr 2T(n/2) + O(n) & n > 1

#### 4.1.2 题目大意

**描述**:给定一个整数数组 `nums`
**描述**:给定一个整数数组 $nums$

**要求**:对该数组升序排列。

Expand Down Expand Up @@ -189,13 +191,13 @@ class Solution:

#### 4.2.2 题目大意

**描述**:给定一个含有 $n$ 个元素有序的(升序)整型数组 `nums` 和一个目标值 `target`
**描述**:给定一个含有 $n$ 个元素有序的(升序)整型数组 $nums$ 和一个目标值 $target$

**要求**:返回 `target` 在数组 `nums` 中的位置,如果找不到,则返回 $-1$。
**要求**:返回 $target$ 在数组 $nums$ 中的位置,如果找不到,则返回 $-1$。

**说明**

- 假设 `nums` 中的所有元素是不重复的。
- 假设 $nums$ 中的所有元素是不重复的。
- $n$ 将在 $[1, 10000]$ 之间。
- $-9999 \le nums[i] \le 9999$。

Expand All @@ -212,10 +214,10 @@ class Solution:
我们使用分治算法来解决这道题。与其他分治题目不一样的地方是二分查找不用进行合并过程,最小子问题的解就是原问题的解。

1. **分解**:将数组的 $n$ 个元素分解为左右两个各包含 $\frac{n}{2}$ 个元素的子序列。
2. **求解**:取中间元素 `nums[mid]``target` 相比。
2. **求解**:取中间元素 $nums[mid]$$target$ 相比。
1. 如果相等,则找到该元素;
2. 如果 `nums[mid] < target`,则递归在左子序列中进行二分查找。
3. 如果 `nums[mid] > target`,则递归在右子序列中进行二分查找。
2. 如果 $nums[mid] < target$,则递归在左子序列中进行二分查找。
3. 如果 $nums[mid] > target$,则递归在右子序列中进行二分查找。

二分查找的的分治算法过程如下图所示。

Expand Down
Loading

0 comments on commit 41b4368

Please sign in to comment.