贪心和递归分治
Question:
找零问题的核心是在几种不同面值如 1、5、10 分的硬币中,用最少的枚数凑出针一个需要找零的钱数。
贪心(greedy)算法:
它的核心逻辑是我们先选择面值较大的来找,再逐渐选小面额的。为什么这里是从大到小,而不是从小到大呢?因为通常面值越大,用到的数量就越少。
function minCoinChange(coins, amount) {
var change = [];
var total = 0;
for (let i = coins.length; i >= 0; i--) { // 从大到小循环
var coin = coins[i];
while (total + coin <= amount) { // 将硬币逐个加入,面值要小于商品价格
change.push(coin); // 将硬币加入到结果
total += coin; // 将硬币累加到总数
}
}
return change;
}
贪心是最简单的一种解决方案,
可是这种方案有两个核心问题:1、找不到答案。2、找不到最优解。
递归(recursion) 和 分治(divide and conquer)
递归的遍历类似树(tree) 形的数据结构,深度优先(DFS)的遍历顺序。
递归带来的问题:重叠子问题。去重
回溯和记忆函数
回溯(backtracking) 的思想
记忆函数创建一个备忘录(memoization),来起到在执行中去重的作用。
递推和动态规划
第一个是无后效性,第二个是最优子结构。后无效性是说,如我们上图中所示,一个顶点下的子问题分支上的问题之间,依赖是单向的,后续的决策不会影响之前某个阶段的状态。而最优子结构指的是子问题之间是相互独立的,我们完全可以只基于子问题路径前面的状态推导出来,而不受其它路径状态的影响。
在动态规划中,解决问题用的是状态转移方程
- 初始化状态
- 状态参数
状态转移方程:
其实就是递推公式的思考模式
function minCoinChange(coins, amount) {
var dp = Array(amount + 1).fill(Infinity); // 每种硬币需要多少
dp[0] = 0; // 找0元,需要0个硬币
for (let coin of coins) { // 循环每种硬币
for (let i = coin; i <= amount; i++) { // 遍历所有数量
dp[i] = Math.min(dp[i], dp[i - coin] + 1); // 更新最少需要用到的面值
}
}
return dp[amount] === Infinity ? -1 : dp[amount]; // 如果最后一个是无限的,没法找
}
延伸:位运算符
按位与(& AND)表示的是,对于每一个比特位,如果两个操作数相应的比特位都是 1 时,结果则为 1,否则为 0。 比如 5&9 的运算结果就是 0001,也就是 1。
0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 0 1 // 5
0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 0 0 1 // 9
0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 // 5&9
按位或(I OR)来说,针对每一个比特位,当两个操作数相应的比特位至少有一个 1 时,结果为 1,否则为 0。比如 5|9 的运算结果就是 1 1 0 1,也就是 13。
0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 0 1 // 5
0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 0 0 1 // 9
0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 0 1 // 5|9
对于按位非(~ NOT)来说,指的则是反转操作数的比特位,即 0 变成 1,1 变成 0。那么~5 的运算结果就是 -6,~9 的运算结果就是 -10。
0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 0 1 // 5
1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 1 0 // -5
0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 0 0 1 // 9
1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 1 1 0 // -9
按位异或(^ XOR),它的操作是对于每一个比特位,当两个操作数相应的比特位有且只有一个 1 时,结果为 1,否则为 0。那么 5^9 的运算结果就是 1 1 0 0,也就是 12。
0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 0 1 // 5
0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 0 0 1 // 9
0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 0 0 // 5^9
再来看看位移。这里又分为左移(<< Left shift),有符号右移(>> Right shift)和无符号右移(>>> Zero-fill right shift)。左移会将 a 的二进制形式向左移 b (< 32) 比特位,右边用 0 填充,所以 9<<1 的结果就是 18。而有符号右移则会将 a 的二进制表示向右移 b (< 32) 位,丢弃被移出的位,所以 9>>>1 的结果就是 4。最后无符号右移会将 a 的二进制表示向右移 b (< 32) 位,丢弃被移出的位,并使用 0 在左侧填充,所以 9>>>1 的结果就是 2147483643。
0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 0 0 1 // 9
0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 0 0 1 0 // 9 << 1
0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 0 0 // 9 >> 1
0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 1 1 // 9 >>> 1
极客时间《Jvascript进阶实战课》学习笔记 Day21