Skip to content

Commit

Permalink
nSum
Browse files Browse the repository at this point in the history
  • Loading branch information
weilaiqishi committed Aug 16, 2022
1 parent c236c34 commit fd87c6b
Show file tree
Hide file tree
Showing 8 changed files with 443 additions and 6 deletions.
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,10 @@

## leetcode

- [leetcode 动态规划](./前端/1html&js/算法/0核心框架/0_动态规划.md)
- [leetcode 双指针链表](./前端/1html&js/算法/0核心框架/0_双指针链表.md)
- [leetcode 双指针数组](./前端/1html&js/算法/0核心框架/0_双指针数组.md)
- [leetcode nSum](./前端/1html&js/算法/0核心框架/0_nSum.md)
- [leetcode JS实现LRU](./前端/1html&js/算法/1数据结构系列/LRU.md)

## React
Expand Down Expand Up @@ -85,6 +87,8 @@

- [lowcode designable-formily-taro-react](./前端/业务场景/lowcode/基于%20Designable%20开发%20Taro%20小程序低代码玩具系统(0).md)
- [lowcode designable-formily-taro-react](./前端/业务场景/lowcode/基于%20Designable%20开发%20Taro%20小程序低代码玩具系统(1).md)
- [lowcode designable-formily-taro-react](./前端/业务场景/lowcode/基于%20Designable%20开发%20Taro%20小程序低代码玩具系统(2).md)
- [ssr ssr入门](./前端/业务场景/ssr/ssr入门.md)

## 导图文件要用xmind打开

Expand Down
27 changes: 22 additions & 5 deletions 前端/1html&js/js基础-数据类型.md
Original file line number Diff line number Diff line change
Expand Up @@ -444,15 +444,17 @@ for循环出key
自身
可枚举属性
返回key数组
3.`Object.values()`
`Object.values()`
返回value数组
4.`Object.entries()`
`Object.entries()`
返回下标0是key、下标1是value的数组的数组
5.`Object.getOwnPropertyNames`
3.`Object.getOwnPropertyNames`
自身
所有属性
所有属性 不包括Symbol
返回key数组
6.反射
4.反射 `Reflect.ownKeys`
获取自身所有属性名称,包括不可枚举属性,Symbol
其实就是 `Object.getOwnPropertyNames``Object.getOwnPropertySymbols` 之和
**对象属性的顺序**
在ES5和早期标准中,没有指定属性的顺序
Expand Down Expand Up @@ -488,6 +490,21 @@ Symbol:当属性的类型是Symbol时,
NaN和任何比都是false
`Object.is(valueA,valueB)` 以与严格相等运算符相同的方式检查相等性的参数,但有两个区别。
首先,`NaN` 等于另一个 `NaN` 值:
```js
Object.is(NaN, NaN); // => true
Object.is(NaN, 1); // => false
```
其次,`Object.is()` 区分 `-0``+0`
```js
Object.is(-0, +0); // => false
```
### typeof instanceof Object.prototype.toString.call()
typeof 对于原始类型来说,除了 null 都可以显示正确的类型
Expand Down
2 changes: 1 addition & 1 deletion 前端/1html&js/ts基础.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@

缺点

- 有一定的学习成本,需要理解接口(Interfaces)、泛型(Generics)、类(Classes)、枚举类型(Enums)等前端工程师可能不是很熟悉的概念
- 有一定的学习成本,需要理解接口(`Interfaces`)、泛型(`Generics`)、类(`Classes`)、枚举类型(`Enums`)等前端工程师可能不是很熟悉的概念
- 前期会增加一些开发成本
- ts编译是需要时间的,这就意味着项目大了以后,开发环境启动和生产环境打包的速度就成了考验

Expand Down
133 changes: 133 additions & 0 deletions 前端/1html&js/算法/0核心框架/0_nSum.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
# nSum

<https://mp.weixin.qq.com/s/fSyJVvggxHq28a0SdmZm6Q>

## twoSum 问题

先排序 再左右指针
**根据 sum 和 target 的比较,移动左右指针**

如果要求返回所有和为 target 的元素对儿,且不能出现重复

**根据 sum 和 target 的比较,移动左右指针** 这一步中 `sum == target` 条件分支 要跳过所有重复的元素

<https://leetcode.cn/problems/kLl5u1/>

通用函数 非答案

```ts
function twoSum(numbers: number[], target?: number, start?: number): number[][] {
target = target || 0
start = start || 0
numbers.sort((a, b) => a - b)
let p1 = start
let p2 = numbers.length - 1
const res: number[][] = []
while (p1 < p2) {
const sum = numbers[p1] + numbers[p2]
const left = numbers[p1]
const right = numbers[p2]
if (sum < target) {
while (p1 < p2 && numbers[p1] === left) p1++
} else if (sum > target) {
while (p1 < p2 && numbers[p2] === right) p2--
} else {
res.push([left, right])
while (p1 < p2 && numbers[p1] === left) p1++
while (p1 < p2 && numbers[p2] === right) p2--
}
}

return res
}
```

## 3Sum

力扣第 15 题「三数之和」

<https://leetcode.cn/problems/3sum/>

确定了第一个数字之后,剩下的两个数字可以是什么呢?其实就是和为 `target - nums[i]` 的两个数字

而且不能让第一个数重复,至于后面的两个数,我们复用的 twoSum 函数会保证它们不重复。所以代码中必须用一个 while 循环来保证 3Sum 中第一个元素不重复。

```ts
function threeSum(nums: number[], target?: number): number[][] {
target = target || 0
nums.sort((a, b) => a - b)
const n = nums.length
const res: number[][] = []
// 穷举 threeSum 的第一个数
for (let i = 0; i < n; i++) {
// 对 target - nums[i] 计算 twoSum
const tuples = twoSum(nums, target - nums[i], i + 1)
tuples.forEach(item => {
res.push([nums[i], ...item])
})
// 跳过第一个数字重复的情况,否则会出现重复结果
while (i < n - 1 && nums[i] == nums[i + 1]) i++
}
return res
}
```

排序的复杂度为 `O(NlogN)``twoSumTarget` 函数中的双指针操作为 `O(N)``threeSumTarget` 函数在 for 循环中调用 `twoSumTarget` 所以总的时间复杂度就是 `O(NlogN + N^2) = O(N^2)`

## 4Sum 问题

力扣第 18 题「四数之和」

<https://leetcode.cn/problems/4sum/>

统一出一个 `nSum` 函数

```ts
function nSum(numbers: number[], target?: number, start?: number, n?: number): number[][] {
/* 注意:调用这个函数之前一定要先给 nums 排序 */

target = target || 0
start = start || 0
n = n || 0

// 至少是 2Sum,且数组大小不应该小于 n
const sz = numbers.length
const res: number[][] = []

if (n < 2 || sz < n) return res

// 2Sum 是 base case
if (n === 2) {
// 双指针那一套操作
let p1 = start
let p2 = numbers.length - 1
while (p1 < p2) {
const sum = numbers[p1] + numbers[p2]
const left = numbers[p1]
const right = numbers[p2]
if (sum < target) {
while (p1 < p2 && numbers[p1] === left) p1++
} else if (sum > target) {
while (p1 < p2 && numbers[p2] === right) p2--
} else {
res.push([left, right])
while (p1 < p2 && numbers[p1] === left) p1++
while (p1 < p2 && numbers[p2] === right) p2--
}
}
} else {
// n > 2 时,递归计算 (n-1)Sum 的结果
for (let i = start; i < sz; i++) {
const sub: number[][] = nSum(numbers, target - numbers[i], i + 1, n - 1)
sub.forEach(item => {
res.push([numbers[i], ...item])
})
while (i < sz - 1 && numbers[i] === numbers[i + 1]) i++
}
}

return res
}
```

`n == 2` 时是 `twoSum` 的双指针解法,`n > 2` 时就是穷举第一个数字,然后递归调用计算 `(n-1)Sum`,组装答案。
144 changes: 144 additions & 0 deletions 前端/1html&js/算法/0核心框架/0_动态规划.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
# 动态规划解题套路框架

<https://labuladong.gitee.io/algo/1/7/>

动态规划问题的一般形式就是求最值,求解动态规划的核心问题是穷举

首先,虽然动态规划的核心思想就是穷举求最值,但是问题可以千变万化,穷举所有可行解其实并不是一件容易的事,需要你熟练掌握递归思维,只有列出`正确的「状态转移方程」`,才能正确地穷举。而且,你需要判断算法问题是否具备`「最优子结构」`,是否能够通过子问题的最值得到原问题的最值。另外,动态规划问题存在`「重叠子问题」`,如果暴力穷举的话效率会很低,所以需要你使用「备忘录」或者「DP table」来优化穷举过程,避免不必要的计算。

以上提到的重叠子问题、最优子结构、状态转移方程就是动态规划三要素。

思维框架,辅助你思考状态转移方程:

**明确 base case -> 明确「状态」-> 明确「选择」 -> 定义 dp 数组/函数的含义。**

## 斐波那契数列

力扣第 509 题「 斐波那契数」

<https://leetcode.cn/problems/fibonacci-number/>

### 暴力递归

> PS:但凡遇到需要递归的问题,最好都画出递归树,这对你分析算法的复杂度,寻找算法低效的原因都有巨大帮助。
![斐波那契数列 暴力递归](https://labuladong.gitee.io/algo/images/%e5%8a%a8%e6%80%81%e8%a7%84%e5%88%92%e8%af%a6%e8%a7%a3%e8%bf%9b%e9%98%b6/1.jpg)

这个递归树怎么理解?就是说想要计算原问题 f(20),我就得先计算出子问题 f(19) 和 f(18),然后要计算 f(19),我就要先算出子问题 f(18) 和 f(17),以此类推。最后遇到 f(1) 或者 f(2) 的时候,结果已知,就能直接返回结果,递归树不再向下生长了。

递归算法的时间复杂度怎么计算?就是用子问题个数乘以解决一个子问题需要的时间。

首先计算子问题个数,即递归树中节点的总数。显然二叉树节点总数为指数级别,所以子问题个数为 O(2^n)。

后计算解决一个子问题的时间,在本算法中,没有循环,只有 f(n - 1) + f(n - 2) 一个加法操作,时间为 O(1)。

所以,这个算法的时间复杂度为二者相乘,即 O(2^n),指数级别,爆炸。

观察递归树,很明显发现了算法低效的原因:存在大量重复计算,比如 f(18) 被计算了两次,而且你可以看到,以 f(18) 为根的这个递归树体量巨大,多算一遍,会耗费巨大的时间。更何况,还不止 f(18) 这一个节点被重复计算,所以这个算法及其低效。

这就是动态规划问题的第一个性质:`重叠子问题`

### 带备忘录的递归解法

即然耗时的原因是重复计算,那么我们可以造一个「备忘录」,每次算出某个子问题的答案后别急着返回,先记到「备忘录」里再返回;每次遇到一个子问题先去「备忘录」里查一查,如果发现之前已经解决过这个问题了,直接把答案拿出来用,不要再耗时去计算了。

一般使用一个数组充当这个「备忘录」,当然你也可以使用哈希表(字典),思想都是一样的。

现在,画出递归树,你就知道「备忘录」到底做了什么。

![斐波那契数列 带备忘录的递归解法](https://labuladong.gitee.io/algo/images/%e5%8a%a8%e6%80%81%e8%a7%84%e5%88%92%e8%af%a6%e8%a7%a3%e8%bf%9b%e9%98%b6/2.jpg)

实际上,带「备忘录」的递归算法,把一棵存在巨量冗余的递归树通过「剪枝」,改造成了一幅不存在冗余的递归图,极大减少了子问题(即递归图中节点)的个数。

![斐波那契数列 递归图](https://labuladong.gitee.io/algo/images/%e5%8a%a8%e6%80%81%e8%a7%84%e5%88%92%e8%af%a6%e8%a7%a3%e8%bf%9b%e9%98%b6/3.jpg)

递归算法的时间复杂度怎么计算?就是用子问题个数乘以解决一个子问题需要的时间。

子问题个数,即图中节点的总数,由于本算法不存在冗余计算,子问题就是 f(1), f(2), f(3) … f(20),数量和输入规模 n = 20 成正比,所以子问题个数为 O(n)。

解决一个子问题的时间,同上,没有什么循环,时间为 O(1)。

所以,本算法的时间复杂度是 O(n),比起暴力算法,是降维打击。

至此,带备忘录的递归解法的效率已经和迭代的动态规划解法一样了。实际上,这种解法和常见的动态规划解法已经差不多了,只不过这种解法是「自顶向下」进行「递归」求解,我们更常见的动态规划代码是「自底向上」进行「递推」求解。

```ts
function fib(n: number): number {
if (n === 0) return 0
const arr = [0, 1]
for (let i = 2; i <= n; i++) {
arr[i] = arr[i-1] + arr[i-2]
}
return arr[n]
};
```

### dp 数组的迭代(递推)解法

有了上一步「备忘录」的启发,我们可以把这个「备忘录」独立出来成为一张表,通常叫做 DP table,在这张表上完成「自底向上」的推算岂不美哉!

![斐波那契数列 递推](https://labuladong.gitee.io/algo/images/%e5%8a%a8%e6%80%81%e8%a7%84%e5%88%92%e8%af%a6%e8%a7%a3%e8%bf%9b%e9%98%b6/4.jpg)

画个图就很好理解了,而且你发现这个 DP table 特别像之前那个「剪枝」后的结果,只是反过来算而已。实际上,带备忘录的递归解法中的「备忘录」,最终完成后就是这个 DP table,所以说这两种解法其实是差不多的,大部分情况下,效率也基本相同。

这里,引出「状态转移方程」这个名词,实际上就是描述问题结构的数学形式:

![状态转移方程](https://labuladong.gitee.io/algo/images/%e5%8a%a8%e6%80%81%e8%a7%84%e5%88%92%e8%af%a6%e8%a7%a3%e8%bf%9b%e9%98%b6/fib.png)

为啥叫「状态转移方程」?其实就是为了听起来高端。

f(n) 的函数参数会不断变化,所以你把参数 n 想做一个状态,这个状态 n 是由状态 n - 1 和状态 n - 2 转移(相加)而来,这就叫状态转移,仅此而已。

你会发现,上面的几种解法中的所有操作,例如 `return f(n - 1) + f(n - 2)``dp[i] = dp[i - 1] + dp[i - 2]`,以及对备忘录或 DP table 的初始化操作,都是围绕这个方程式的不同表现形式。

## 凑零钱问题

力扣第 322 题「 零钱兑换」

<https://leetcode.cn/problems/coin-change/>

给你 k 种面值的硬币,面值分别为 c1, c2 ... ck,每种硬币的数量无限,再给一个总金额 amount,问你最少需要几枚硬币凑出这个金额,如果不可能凑出,算法返回 -1 。

那么,既然知道了这是个动态规划问题,就要思考如何列出正确的状态转移方程?

1、**确定 base case**,这个很简单,显然目标金额 amount 为 0 时算法返回 0,因为不需要任何硬币就已经凑出目标金额了。

2、**确定「状态」**,也就是原问题和子问题中会变化的变量。由于硬币数量无限,硬币的面额也是题目给定的,只有目标金额会不断地向 base case 靠近,所以唯一的「状态」就是目标金额 amount。

3、**确定「选择」**,也就是导致「状态」产生变化的行为。目标金额为什么变化呢,因为你在选择硬币,你每选择一枚硬币,就相当于减少了目标金额。所以说所有硬币的面值,就是你的「选择」。

4、**明确 dp 函数/数组的定义**。我们这里讲的是自顶向下的解法,所以会有一个递归的 dp 函数,一般来说函数的参数就是状态转移中会变化的量,也就是上面说到的「状态」;函数的返回值就是题目要求我们计算的量。就本题来说,状态只有一个,即「目标金额」,题目要求我们计算凑出目标金额所需的最少硬币数量。

**所以我们可以这样定义 dp 函数:dp(n) 表示,输入一个目标金额 n,返回凑出目标金额 n 所需的最少硬币数量。**

目标金额为 0 时,所需硬币数量为 0;当目标金额小于 0 时,无解,返回 -1

状态转移方程:

![状态转移方程:](https://labuladong.gitee.io/algo/images/%e5%8a%a8%e6%80%81%e8%a7%84%e5%88%92%e8%af%a6%e8%a7%a3%e8%bf%9b%e9%98%b6/coin.png)

消除一下重叠子问题

![消除一下重叠子问题](https://labuladong.gitee.io/algo/images/%e5%8a%a8%e6%80%81%e8%a7%84%e5%88%92%e8%af%a6%e8%a7%a3%e8%bf%9b%e9%98%b6/5.jpg)

### dp 数组的迭代解法

dp 数组的定义:当目标金额为 i 时,至少需要 `dp[i]` 枚硬币凑出。dp 数组中的值都初始化为 amount + 1,因为凑成 amount 金额的硬币数最多只可能等于 amount(全用 1 元面值的硬币),所以初始化为 amount + 1 就相当于初始化为正无穷,便于后续取最小值。为啥不直接初始化为 int 型的最大值 Integer.MAX_VALUE 呢?因为后面有 `dp[i - coin] + 1`,这就会导致整型溢出。

```ts
function coinChange(coins: number[], amount: number): number {
const arr = new Array(amount + 1).fill(amount + 1)

// base case
arr[0] = 0
for (let i = 0; i < arr.length; i++) {
for (const coin of coins) {
if (i - coin < 0) {
continue
}
arr[i] = Math.min(arr[i], 1 + arr[i - coin])
}
}
return (arr[amount] === amount + 1) ? -1 : arr[amount]
};
```
Empty file.
Loading

0 comments on commit fd87c6b

Please sign in to comment.