mirror of
https://github.com/NohamR/knowledge-kit.git
synced 2026-05-25 04:17:17 +00:00
feature: Weex APM
This commit is contained in:
190
Chapter8 - Algorithm/8.1.md
Normal file
190
Chapter8 - Algorithm/8.1.md
Normal file
@@ -0,0 +1,190 @@
|
||||
## leetcode 968. 监控二叉树
|
||||
|
||||
## 题目描述
|
||||
给定一个二叉树,我们在树的节点上安装监控。
|
||||
|
||||
节点上的每个摄影头都可以监视其父对象、自身及其直接子对象
|
||||
|
||||
计算监控树的所有节点所需的最小监控数量。
|
||||
|
||||
2个例子:
|
||||
|
||||
case1:
|
||||
|
||||
``` shell
|
||||
1
|
||||
|
|
||||
2 监控
|
||||
/ \
|
||||
3 4
|
||||
|
||||
```
|
||||
|
||||
Case2:
|
||||
|
||||
```shell
|
||||
1
|
||||
/
|
||||
2 监控
|
||||
/
|
||||
3
|
||||
/
|
||||
4 监控
|
||||
\
|
||||
5
|
||||
```
|
||||
|
||||
## 分析
|
||||
|
||||
- 一颗监控可以覆盖:当前节点、当前节点的父节点、当前节点的所有子节点3层
|
||||
|
||||
- 本题目要求使用最小数量的监控解决问题。那么也就是贪心思维的体现,那么问题来了,什么策略才可以使用最小数量的监控?
|
||||
|
||||
- 一棵二叉树中,叶子节点数量肯定是最多的。所以要想最小数量安装监控,优先选择非叶子节点上安装监控。这也是本题中贪心思维的体现。
|
||||
- 如果最后一层叶子节点不安装监控,那么肯定是叶子节点的父节点安装监控,同理叶子节点的父节点的父节点,也会被监控覆盖到,所以叶子节点的父节点的父节点不用安装监控。于是
|
||||
叶子节点的父节点的父节点的父节点就必须安装监控。
|
||||
|
||||
|
||||
|
||||
- 对于二叉树一定是采用递归法或者迭代法解决,本题选择递归法。
|
||||
|
||||
- 因为要根据叶子节点的状态来反推父节点的状态,所以采用后续遍历。
|
||||
|
||||
- 另外需要根据左右子树的返回值来判断,所以递归函数需要返回值
|
||||
|
||||
- 为了方便定义3个状态。
|
||||
|
||||
- 未设置监控 NotCovered = 0
|
||||
- 被监控覆盖 IsCoverd = 1
|
||||
- 设置监控 SetCamera = 2
|
||||
|
||||
- 如何定义递归函数?
|
||||
|
||||
- 返回值就是数字,存在3种情况,也就是上面定义的3种状态
|
||||
- 递归函数的终止条件是什么?遇到叶子节点的左右空子树的情况下,该选用什么状态?思考下:共3种情况
|
||||
- 空节点选用 “未覆盖 NotCovered”?
|
||||
❌ 问题 :叶子节点必须安装监控来覆盖空节点
|
||||
如果空节点是 NotCovered,那么空节点的父节点,也就是叶子节点,必须设置监控,才可以保证叶子节点的左右子节点才可以被监控覆盖到。
|
||||
❌ 结果:摄像头数量过多,不符合最小化原则
|
||||
这和题目要求的最小监控数量不契合
|
||||
- 空节点选用 “安装监控 SetCamera”?
|
||||
❌ 问题:叶子节点自动变为被覆盖状态 (IsCovered = 1)
|
||||
如果空节点是 SetCamera,那么空节点的父节点,也就是叶子节点,状态一定是被覆盖 IsCoverd,如果叶子节点是 IsCoverd,
|
||||
❌ 结果:叶子节点的父节点不需要安装监控,破坏了"叶子节点的父节点安装监控"的最优策略
|
||||
反推叶子节点的父节点就不需要安装监控了(因为监控必须间隔设置,覆盖 - 监控 - 覆盖 - 监控 这样的形式),那这个情况也和题目的预设条件不满足。
|
||||
- 所以空节点应该选用 “被监控覆盖 IsCoverd” 这个状态
|
||||
✅ 正确:叶子节点为未覆盖状态 (NotCovered = 0)
|
||||
空节点被监控覆盖 IsCoverd,那么空节点的父节点,也就是叶子节点就不需要设置监控,
|
||||
✅ 结果:叶子节点的父节点必须安装监控,符合贪心策略
|
||||
也就是未设置监控 NotCovered = 0,那么叶子节点的父节点才需要设置监控 SetCamera
|
||||
|
||||
## 状态转换情况
|
||||
说明:
|
||||
- 未设置监控 NotCovered = 0
|
||||
- 被监控覆盖 IsCoverd = 1
|
||||
- 设置监控 SetCamera = 2
|
||||
|
||||
| leftChild | rightChild | root | count=0 | 说明 |
|
||||
| --------- | ---------- | ---- | ------- | ------------------------------------------------------------ |
|
||||
| 1 | 1 | 0 | +0 | 空节点(叶子节点的子节点)必须同时处于被覆盖状态 |
|
||||
| | | | | |
|
||||
| 0 | 0 | 2 | +1 | 普通节点不管 left、right 只要有1个处于未覆盖状态,那么父节点一定要设置监控才可以“罩着”下面的子节点 |
|
||||
| 0 | 1 | 2 | +1 | |
|
||||
| 1 | 0 | 2 | +1 | |
|
||||
| 2 | 0 | 2 | +1 | |
|
||||
| 0 | 2 | 2 | +1 | |
|
||||
| | | | | |
|
||||
| 2 | 2 | 1 | +0 | 其他情况,父节点都是处于被监控覆盖的状态,不需要增加监控 |
|
||||
| 2 | 1 | 0 | +0 | |
|
||||
| 1 | 2 | 1 | +0 | |
|
||||
| 2 | 1 | 1 | +0 | |
|
||||
|
||||
|
||||
|
||||
## 贪心思想
|
||||
|
||||
本题目贪心体现在(监控数量 count = 0):
|
||||
- 空节点(叶子节点的子节点):处于被监控状态(IsCovered 状态),没有安装监控。count 不变
|
||||
- 叶子节点:不设置监控,处于 NotCovered 状态。需要父节点罩着。count 不变
|
||||
- 叶子节点的父节点:安装监控,处于 SetCamera 状态,count++
|
||||
- 叶子节点的爷爷节点:处于被监控状态(IsCovered 状态),没有安装监控。count 不变
|
||||
- ♻️ 循环往复
|
||||
|
||||
|
||||
|
||||
## 代码实现(JS 为例)
|
||||
|
||||
```js
|
||||
/**
|
||||
* @param {TreeNode} root
|
||||
* @return {number}
|
||||
*/
|
||||
var minCameraCover = function(root) {
|
||||
let count = 0
|
||||
const Mode_NotCovered = 0 // 未设置监控
|
||||
const Mode_IsCovered = 1 // 被监控覆盖
|
||||
const Mode_SetCamera = 2 // 设置监控
|
||||
|
||||
const traverse = (node) => {
|
||||
// 空节点视为已覆盖(推到过程见上面注释部分)
|
||||
if (node === null) return Mode_IsCovered
|
||||
|
||||
// 后序遍历
|
||||
let left = traverse(node.left)
|
||||
let right = traverse(node.right)
|
||||
|
||||
// 如果左右孩子有一个未被覆盖,当前节点需要安装摄像头
|
||||
if (left === Mode_NotCovered || right === Mode_NotCovered) {
|
||||
count++
|
||||
return Mode_SetCamera
|
||||
}
|
||||
// 如果左孩子和右孩子都是覆盖状态,那么父节点处于非覆盖状态
|
||||
if (left === Mode_IsCovered && right === Mode_IsCovered) {
|
||||
return Mode_NotCovered
|
||||
}
|
||||
// 如果左孩子或者右孩子是设置监控状态,那么父节点处于监控覆盖状态
|
||||
if (left === Mode_SetCamera || right === Mode_SetCamera) {
|
||||
return Mode_IsCovered
|
||||
}
|
||||
return Mode_NotCovered
|
||||
}
|
||||
|
||||
let rootResult = traverse(root)
|
||||
// 检查根节点状态,如果未被覆盖则需要增加一个摄像头
|
||||
if (rootResult === Mode_NotCovered) count++
|
||||
return count
|
||||
};
|
||||
```
|
||||
提交后发现空间复杂度一般,去掉定义的状态和更加清楚的 if 分支,代码如下
|
||||
|
||||
```js
|
||||
var minCameraCover = function(root) {
|
||||
let count = 0
|
||||
|
||||
const traverse = (node) => {
|
||||
// 空节点视为已覆盖(推到过程见上面注释部分)
|
||||
if (node === null) return 1
|
||||
|
||||
// 后序遍历
|
||||
let left = traverse(node.left)
|
||||
let right = traverse(node.right)
|
||||
|
||||
// 如果左右孩子有一个未被覆盖,当前节点需要安装摄像头
|
||||
if (left === 0 || right === 0) {
|
||||
count++
|
||||
return 2
|
||||
}
|
||||
// 如果左孩子或者右孩子是设置监控状态,那么父节点处于监控覆盖状态
|
||||
if (left === 2 || right === 2) {
|
||||
return 1
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
let rootResult = traverse(root)
|
||||
// 检查根节点状态,如果未被覆盖则需要增加一个摄像头
|
||||
if (rootResult === 0) count++
|
||||
return count
|
||||
};
|
||||
```
|
||||
|
||||
161
Chapter8 - Algorithm/8.2.md
Normal file
161
Chapter8 - Algorithm/8.2.md
Normal file
@@ -0,0 +1,161 @@
|
||||
# 《剑指 Offer》字符串“左旋”、“右旋”里的数学秘密
|
||||
|
||||
> 为什么要写本篇文章?看上去这是 easy 级别的题目。但“点是面的缩影,面是点的抽象”,单独一道题似乎很简单,我们可以比较轻松做出来。但是这一类题目的本质是什么?不要处于混沌的状态解决了题目,但下次遇到类似的,还是要迟疑思考一会儿。本篇文章带你吃透问题的本质和背后的数学推导。
|
||||
|
||||
|
||||
|
||||
## 题目描述
|
||||
字符串的左旋转操作是把字符串前面的若干个字符转移到字符串的尾部。
|
||||
|
||||
请定义一个函数实现字符串左旋转操作的功能。比如:
|
||||
- 输入字符串 "abcdefg" 和数字 2
|
||||
- 该函数将返回左旋转 2 位后的结果 "cdefgab"
|
||||
|
||||
请实现该函数
|
||||
|
||||
## 结论
|
||||
只要是字符串的左旋、右旋,都用整体逆序 + 部分逆序的方法,也可以是部分逆序 + 整体逆序。
|
||||
|
||||
## 分析
|
||||
关于反转(也就是逆序),有2个 feature:
|
||||
- 反转的可逆性:反转(反转(x)) = x
|
||||
- 类似负负得正。比如:'123' 经过一次反转后为 '321', '321' 再经过一次反转为 '123'
|
||||
- 反转的可组合性:反转 (A + B) = 反转(B) + 反转(A)
|
||||
- '123456' 按照长度为3进行拆分为2部分,s1 + s2。s1 = '123', s2 = '456
|
||||
- 先对后面的 s2,也就是'456' 反转得到 '654',即 s2' = '654'
|
||||
- 再对前面的 s1,也就是'123' 反转得到 '321',即 s1' = '321'
|
||||
- 再对 s1' 和 s2' 进行拼接, s1' + s2' = '654321'
|
||||
- 观察发现 s1' + s2' 就等于对整体 s1 + s2 逆序后的结果。
|
||||
|
||||
再来观察看看:左旋、右旋题目要求的是什么?
|
||||
|
||||
前提:假设一个逆序函数,可以将 x 作为输入,输出是 x'。这个共识、前提成立,我们再进行后续的推导:
|
||||
1. 原始字符串通过 k 为分割,可以拆分为: A + B。题目求的是什么? B + A
|
||||
2. 思考:根据上面的2个特性,通过什么变化可以从 A + B,得到 B + A 呢?
|
||||
不难得出结论,有2个方案:
|
||||
- 先整体逆序,`逆序 (A + B) = 逆序(B) + 逆序(A) = B' + A'`
|
||||
- 再局部逆序,`逆序(B') + 逆序(A') = B + A`
|
||||
|
||||
结论:我们发现这时候的结果刚好满足题目要求。所以这些方法都是有迹可循的,符合数学群论中的 **“逆运算”** 和 **“运算律”** 的思想
|
||||
|
||||
|
||||
## 方法1:先整体逆序,再局部逆序
|
||||
1. 原始字符串通过 k 拆分为: A + B 的结构,左旋后变为 B + A
|
||||
2. 先整体逆序。`逆序(A) = A' 逆序(B) = B'。大的结构还是逆序(A + B) = B' + A'`
|
||||
3. 再局部逆序:`逆序(B') + 逆序(A') = B + A`
|
||||
|
||||
结论:我们发现这时候的结果刚好满足题目要求,不管是先局部再整体,还是先整体再局部,效果是等价的。
|
||||
|
||||
```javascript
|
||||
// 方法1: 先整体,再部分
|
||||
const rotateLeft = (message, k) => {
|
||||
const length = message.length
|
||||
let datasource = Array.from(message)
|
||||
|
||||
const reverse = (datasource, fromIndex, toIndex) => {
|
||||
for (; fromIndex < toIndex; fromIndex++, toIndex--) {
|
||||
let temp = datasource[fromIndex]
|
||||
datasource[fromIndex] = datasource[toIndex]
|
||||
datasource[toIndex] = temp
|
||||
}
|
||||
}
|
||||
|
||||
// 1. 先整体逆序
|
||||
reverse(datasource, 0, length - 1)
|
||||
|
||||
// 2. 再局部逆序
|
||||
// 先对左半部分逆序
|
||||
/*
|
||||
已知:leftTo = k,leftLength = fullLength - k,求 leftTo?
|
||||
注意:此时的 length 不等于 k,因为左旋的前半段为 k,剩余的后半段 length 为完整的 length - k
|
||||
leftTo - leftFrom + 1 = leftLength
|
||||
leftTo = leftLength + leftFrom - 1
|
||||
代入得到:
|
||||
leftTo = (fullLength - k) + 0 - 1 = length - k - 1
|
||||
*/
|
||||
reverse(datasource, 0, length - k - 1)
|
||||
// 再对右半部分逆序
|
||||
/*
|
||||
已知:rightTo = fullLength - 1,rightLength = k,求 rightFrom?
|
||||
rightTo - rightFrom + 1 = rightLength
|
||||
rightFrom = rightTo - rightLength + 1
|
||||
代入得到:
|
||||
rightFrom = (length - 1) - k + 1 = length - k
|
||||
*/
|
||||
reverse(datasource, length - k, length - 1)
|
||||
|
||||
// 3. 字符串数组拼接为结果
|
||||
return datasource.join('')
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
## 方法2:先部分逆序,再整体逆序
|
||||
思考:能不能先局部逆序,再整体逆序?
|
||||
分析:继续用上面的思路推导下
|
||||
1. 原始字符串通过 k 拆分为: A + B 的结构,左旋后变为 B + A
|
||||
2. 先局部逆序:`逆序(A) + 逆序(B) = A' + B'`
|
||||
3. 再整体逆序。`逆序(A + B) = 逆序(B) + 逆序(A)`。但是此刻我们的输入为: A' + B',
|
||||
|
||||
所以等价于:`逆序(A' + B') = 逆序(B') + 逆序(A') = B + A`
|
||||
|
||||
结论:我们发现这时候的结果刚好满足题目要求,不管是先局部再整体,还是先整体再局部,效果是等价的。
|
||||
|
||||
```javascript
|
||||
// 方法2: 先部分,再整体
|
||||
const rotateLeft1 = (message, k) => {
|
||||
const length = message.length
|
||||
let datasource = Array.from(message)
|
||||
|
||||
|
||||
const reverse = (datasource, fromIndex, toIndex) => {
|
||||
for (; fromIndex < toIndex; fromIndex++, toIndex--) {
|
||||
let temp = datasource[fromIndex]
|
||||
datasource[fromIndex] = datasource[toIndex]
|
||||
datasource[toIndex] = temp
|
||||
}
|
||||
}
|
||||
|
||||
// 1. 再局部逆序
|
||||
|
||||
// abcdefg -> gfedc ba ->
|
||||
// 先对左半部分逆序
|
||||
/*
|
||||
已知:leftTo = k,leftLength = fullLength - k,求 leftTo?
|
||||
注意:此时的 length 不等于 k,因为左旋的前半段为 k,剩余的后半段 length 为完整的 length - k
|
||||
leftTo - leftFrom + 1 = leftLength
|
||||
leftTo = leftLength + leftFrom - 1
|
||||
代入得到:
|
||||
leftTo = (fullLength - k) + 0 - 1 = length - k - 1
|
||||
*/
|
||||
reverse(datasource, 0, k - 1)
|
||||
// 再对右半部分逆序
|
||||
/*
|
||||
已知:rightTo = fullLength - 1,rightLength = k,求 rightFrom?
|
||||
rightTo - rightFrom + 1 = rightLength
|
||||
rightFrom = rightTo - rightLength + 1
|
||||
代入得到:
|
||||
rightFrom = (length - 1) - k + 1 = length - k
|
||||
*/
|
||||
reverse(datasource, k, length - 1)
|
||||
|
||||
// 2. 先整体逆序
|
||||
reverse(datasource, 0, length - 1)
|
||||
|
||||
// 3. 字符串数组拼接为结果
|
||||
return datasource.join('')
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
0
Chapter8 - Algorithm/chapter8.md
Normal file
0
Chapter8 - Algorithm/chapter8.md
Normal file
Reference in New Issue
Block a user