14、 算法思想之“分而治之”
14-1分而治之是什么?
- 分而治之是算法设计中的一-种方法。
- 它将一个问题分成多个和原问题相似的小问题,递归解决小问题,再将结果合并以解决原来的问题。
14-1-1分而治之场景-归并排序
- 分:讲数组从中间一分为二
- 解:递归地对两个子数组进行归并排序。(直到把数组分成长度为1的子数组)
- 合:合并有序数组
14-1-2分而治之场景-快速排序
- 分:选择一个基准,按基准把数组分成两个子数组。
- 解:递归地对两个子数组进行快速排序。(直到把数组分成长度为1的子数组)
- 合:合并有序数组
14-2算法题
374. 猜数字大小
解题步骤:
- 分:计算中间元素,分割数组。
- 解:递归地在较大或者较小子数组进行二分搜索。
- 合:不需要此步,因为在子数组中搜到就返回了。
/**
* @param {number} n
* @return {number}
*/
var guessNumber = function(n) {
const rec = (low, heigh) => {
if(low > heigh) {return}
const mid = Math.floor( (low + heigh) / 2);
const res = guess(mid) // int guess(int num) 来获取猜测结果
if(res == 0) {
return mid;
}else if( res == 1) {
return rec(mid + 1, heigh)
}else {
return rec(1, mid -1 )
}
}
return rec(1, n) // 1 到 n 随机选择一个数字
};
226. 翻转二叉树
解题思路:
- 分:获取左右二叉树
- 解:递归左右子树,再进行兑换
- 合:返回一个树(翻转后的)
/**
* @param {TreeNode} root
* @return {TreeNode}
*/
var invertTree = function(root) {
if(!root) {return null;}
// 获取左右二叉树
// let left = root.left;
// let right = root.right;
return {
val: root.val,
left: invertTree(root.right),
right: invertTree(root.left)
}
};
100. 相同的树
解题思路:
- 分:获取左右二叉树
- 解:递归左右子树,判断两个树的左子树和右子树是否相同
- 合:中上结果合并,如果根节点也相同,那么树相同。
/**
* @param {TreeNode} p
* @param {TreeNode} q
* @return {boolean}
*/
var isSameTree = function(p, q) {
if(!q && !p) { return true;}
if(p && q && q.val == p.val && isSameTree(p.left, q.left) && isSameTree(p.right, q.right)) {
return true;
}else {
return false;
}
};
101. 对称二叉树
解题思路:
- 分:获取左右二叉树
- 解:解:递归地判断树1的左子树和树2的右子树是否镜像,树1的右子树和树2的左子树是否镜像。
- 合:中上结果合并,如果根节点镜像也相同,那么树是对称二叉树。
/**
* @param {TreeNode} root
* @return {boolean}
*/
var isSymmetric = function(root) {
if(!root) {return true;}
const isMirror =(l, r) => {
if(!l&&!r) { return true}
if(l && r && l.val === r.val && isMirror(l.left, r.right)&&isMirror(l.right, r.left)){
return true;
}
return false;
}
return isMirror(root.left, root.right)
};
15、 算法思想之“动态规划”
15-1动态规划是什么?
- 动态规划是算法设计中的一种方法。
- 它将一个问题分解为相互重叠的子问题,通过反复求解子问题,来解决原来的问题。
举个栗子:斐波那契数列
- 定义子问题:
F(n) = F(n-1) + F(n-2)。 - 反复执行:从
2循环到n,执行上述公式。
15-2算法题
70. 爬楼梯
/**
* @param {number} n
* @return {number}
*/
var climbStairs = function(n) {
if(n < 2) {return 1;}
const dp = [1,1];
for(let i = 2; i <= n; i++) {
dp[i] = dp[i-1] + dp[i - 2]
}
return dp[n]
};
198. 打家劫舍
解题思路:
f(k) =从前k个房屋中能偷窃到的最大数额。Ak =第k个房屋的钱数。f(k) = max(f(k-2) + Ak, f(k- 1))
解题步骤:
- 定义子问题:
f(k) = max(f(k-2) + Ak, f(k- 1))。 - 反复执行:从
2循环到n,执行上述公式。
/**
* @param {number[]} nums
* @return {number}
*/
var rob = function(nums) {
if(nums.length === 0) { return 0;}
const dp = [0 ,nums[0]];
for(let i = 2; i <= nums.length; i++) {
dp[i] = Math.max(dp[i - 2] + nums[i -1], dp[i - 1])
}
return dp[nums.length]
};
16、 算法思想之“贪心算法”
16-1贪心算法
- 贪心算法是算法设计中的一种方法。
- 期盼通过每个阶段的局部最优选择,从而达到全局的最优。
- 结果并不一定是最优。
贪心算法不一定是最优解,举个栗子
零钱兑换
1、
输入:coins = [1,2,4] a, amount = 11;
输出:3;
解释:11 = 5 + 5 + 1; 优先选择金额较大的
2、
输入:coins = [1,3,4] a, amount = 6;
输出:3;
解释:6 = 4 + 1 + 1; 优先选择金额较大的。
但是我们可以选择3+3 从而让输出结果是2,这才是最优解。
16-2算法题
455. 分发饼干
解题思路
- 局部最优:既能满足孩子,还消耗最少。
- 先将“较小的饼干”分给“胃口最小"的孩子。
解题步骤
- 对饼干数组和胃口数组升序排序。
- 遍历饼干数组,找到能满足第一个孩子的饼干。
- 然后继续遍历饼干数组,找到满足第二、三、.... 、n个孩子的饼干。
/**
* @param {number[]} g
* @param {number[]} s
* @return {number}
*/
var findContentChildren = function(g, s) {
g = g.sort((a,b) => a - b)
s = s.sort((a,b) => a - b);
let j = 0;
for(let i = 0; i< s.length; i++) {
if(s[i] >= g[j]) {
j++;
}
}
return j;
};
122. 买卖股票的最佳时机 II
解题思路
- 前提: 上帝视角,知道未来的价格。
- 局部最优:见好就收,见差就不动,不做任何长远打算。
解题步骤
- 新建一个变量,用来统计总利润。
- 遍历价格数组,如果当前价格比昨天高,就在昨天买,今天卖,否则就不交易。
- 遍历结束后,返回利润。
/**
* @param {number[]} prices
* @return {number}
*/
var maxProfit = function(prices) {
let profits = 0; // 利润
if(prices.length == 0) {return 0;}
for(let i = 0; i < prices.length; i++) {
if(prices[i+1] > prices[i]) {
profits += prices[i+1] - prices[i]
}
}
return profits
};
17、 算法思想之“回溯算法”
17-1回溯算法是什么?
- 回溯算法是算法设计中的一种方法。
- 回溯算法是一种渐进式寻找并构建问题解决方式的策略。
- 回溯算法会先从一个可能的动作开始解决问题,如果不行,就回溯并选择另一个动作,直到将问题解决。
所谓回溯,就是走了一条路,发现走不通,拐回来原点再走另一条路
什么问题可以用回溯算法?
- 有很多路。(比喻)
- 这些路里,有死路,也有出路。(比喻)
- 通常需要递归来模拟所有的路。(比喻)
17-2算法题
46. 全排列
解题思路
- 要求: 1、所有排列情况; 2、没有重复元素。
- 有出路、有死路。
- 考虑使用回溯算法。
解题步骤
- 用递归模拟出所有情况。
- 遇到包含重复元素的情况,就回溯(递归结束)。
- 收集所有到达递归终点的情况,并返回(数组长度多少,就是多少)。
/**
* @param {number[]} nums
* @return {number[][]}
*/
var permute = function(nums) {
let res = [];
const backTrack =(path) => {
if(path.length === nums.length) {
res.push(path);
return;
}
nums.forEach(n => {
if(path.includes(n)) {return;} // 有重复数字的,之前返回,不走递归
backTrack(path.concat(n))
})
}
backTrack([]);
return res;
};
78. 子集
解题思路
- 要求: 1、所有子集 2、没有重复元素。
- 有出路、有死路。
- 考虑使用回溯算法。
解题步骤
- 用递归模拟出所有情况。
- 保证接的数字都是后面的数字。
- 收集所有到达递归终点的情况,并返回。
/**
* @param {number[]} nums
* @return {number[][]}
*/
var subsets = function(nums) {
let res = [];
const backTrack =(path,l,start) => {
if(path.length === l) {
res.push(path);
return;
}
for(let i = start; i<nums.length; i++) {
backTrack(path.concat(nums[i]), l, i + 1) // 保证子集是有序的。
}
}
for(let i = 0; i<= nums.length; i++) {
// i:路径长度 ,0 起始位置
backTrack([],i,0)
}
return res;
};