数据结构
常见的数据结构有:
- 栈,队列,链表
- 集合,字典
- 树,图,堆
图
图的常用操作:
- 深度优先遍历:尽可能深的搜索图的分支
- 访问根节点
- 对根节点的没访问过的相邻节点挨个进行深度优先遍历
- 广度优先遍历:先访问离根节点近的节点
- 新建一个队列,把根节点入队
- 把队头出队并访问
- 把队头的没访问过的相邻节点入队
LeetCode题目示例:417.太平洋大西洋水流问题,题目链接:https://leetcode-cn.com/problems/pacific-atlantic-water-flow/
解题思路:
- 把矩阵想象成图
- 从海岸线逆流而上遍历图,所到之处就是可以流到某个大洋的坐标
解题步骤:
- 新建两个矩阵,分别记录能流到两个大洋的坐标
- 从海岸线,多管齐下,同时深度优先遍历图,过程中填充上述矩阵
- 遍历两个矩阵,找出能流到两个大洋的坐标
堆
- 堆是一种特殊的完全二叉树
- 所有的节点都大于等于(最大堆)或小于等于(最小堆)它的子节点
JS中的堆
- JS中通常用数组表示堆
- 任意节点的左子节点的位置是:2 * index + 1
- 任意节点的右子节点的位置是:2 * index + 1
- 父节点位置是(index - 1) / 2
算法
算法主要是以下几种:
- 基础技巧:分治、二分、贪心
- 排序算法:快速排序、归并排序、计数排序
- 搜索算法:回溯、递归、深度优先遍历、广度优先遍历、二叉搜索树等
- 图论:最短路径、最小生成树
- 动态规划:背包问题、最长子序列
学习算法推荐一个动画的网址:https://visualgo.net/zh/sorting
排序和搜索
排序:把某个乱序的数组变成升序或者降序的数组。JS中的排序:数组的sort方法
搜索:找出数组中某个元素的下标。JS中的搜索:数组的indexOf方法
排序算法
- 冒泡排序、选择排序、插入排序、归并排序、快速排序
- 选择排序的思路:
- 找到数组中的最小值,选中它并将其放置在第一位
- 接着找到第二小的值,选中它并将其放置在第二位
- 以此类推,执行n - 1轮
- 插入排序的思路:
- 从第二个数开始往前比
- 比它大就往后排
- 以此类推进行到最后一个数
- 归并排序的思路:
- 分:把数组劈成两半,再递归地对子数组进行"分"操作,直到分成一个个单独的数
- 合:把两个数合并为有序数组,再对有序数组进行合并,直到全部子数组合并为一个完整数组
- 新建一个空数组res,用于存放最终排序后的数组
- 比较两个有序数组的头部,较小者出队并推入res中
- 如果两个数组中还有值,就重复第二步
- 快速排序的思路:
- 分区:从数组中任意选择一个元素作为
"基准"
,所有比"基准"
小的元素放在基准前面,比基准大的元素放在基准的后面 - 递归:递归地对基准前后的子数组进行分区
递归的时间复杂度为O(logN),分区操作的时间复杂度为O(n)
搜索算法
- 顺序搜索、二分搜索
- 顺序搜索的思路:
- 遍历数组
- 找到跟目标值相等的元素,就返回它的下标
- 遍历结束后,如果没有搜索到目标值,就返回-1
- 二分搜索的思路:
- 从数组的中间元素开始,如果中间元素正好是目标值,则搜索结束
- 如果目标值大于或者小于中间元素,则在大于或小于中间元素的那一半数据中搜索
分治算法
**场景一:**归并排序
- 分:把数组从中间一分为二
- 解:递归地对两个子数组进行归并排序
- 合:合并有序子数组
**场景二:**快速排序
- 分:选基准,按基准把数组分为两个子数组
- 解:递归地对两个子数组进行快速排序
- 合:对两个子数组进行合并
LeetCode题目示例:
226.翻转二叉树https://leetcode-cn.com/problems/invert-binary-tree/
解题思路:
- 先翻转左右子树,再将子树换个位置
- 符合“分、解、合”特性
解题步骤:
- 分:获取左右子树
- 解:递归地翻转左右子树
- 把翻转后的左右子树换个位置放到根节点上
**总结:**分而治之是算法设计中的一种方法。它将一个问题分成多个和原问题相似的小问题,递归解决小问题,再将结果合并以解决原来的问题
动态规划
- 动态规划是
算法设计
中的一种方法 - 它将一个问题分解为
相互重叠
的子问题,通过反复求解子问题,来解决原来的问题
**LeetCode题目示例:**70.爬楼梯https://leetcode-cn.com/problems/climbing-stairs/
解题思路:
首先是分析一下,我们最后一步要干啥,要迈最后一步阶梯上楼,而这一步可能迈过一个台阶,也可能迈过两个台阶,那还愣着干啥,分情况讨论啊!**第一种情况,最后一步迈一阶,那之前呢,之前就是n-1个台阶啊,你管他几步,先打包带走!第二种情况,最后一步迈两阶,那之前呢,之前就是n-2个台阶啊,也一起打包带走!**这是不是就是总共的可能了?也就是说:
爬第n阶楼梯的方法数量,等于 2 部分之和,即:
爬上 n−1 阶楼梯的方法数量。因为再爬1阶就能到第n阶
爬上 n−2 阶楼梯的方法数量,因为再爬2阶就能到第n阶
f(n) = f(n - 1) + f(n - 2)
解题步骤:
- 定义子问题:F(n) = F(n-1) + F(n-2)。
- 反复执行:从2循环到n,执行上述公式
var climbStairs = function(n) {
const dp = [1,1];
for(let i = 2; i <= n; i+=1) {
dp[i] = dp[i - 1] + dp[i - 2];
}
return dp[n];
};
**LeetCode题目示例:**198.打家劫舍https://leetcode-cn.com/problems/house-robber/
解题思路:
打家劫舍这道题目是动态规划中的简单题,但是个人觉得还是蛮有意思的,所以今天的主题就是将一些有意思的题目做以分享,也希望大家能喜欢。打家劫舍这道题在近两年作为面试题出现的公司有百度,腾讯,谷歌,微软,网易等等,这样看来互联网公司面试的时候还是比较open的哈。这道题还是先往简单的情况分析(题目降维),即如果只有一个房屋可以打劫的时候,显然最大值就是第一个房屋内的金额。当用f(n)表示从前面n个房屋中获取的最大金额,则f(1) = nums[0]。同理f(2) = max(nums[0], nums[1])。当房屋变成3个的时候情况发生了变化,就是小偷可能要思考一下我偷不偷第3个房屋的钱,如果偷了,那么第二个房屋的钱我就不能偷。也就是说他为了争取利益最大化,就要在只偷第2个房屋的钱和偷1和3房屋的钱之间进行比较大小,这样得出的答案就是在有3个房屋的情况下能获得的最大金额。现在房屋变成了4个,小偷现在又要合计了,如果我偷第4个房屋的钱,那第三个房屋的钱我就不能碰,这样我就需要将只有2个房屋情况下的最大收益与第4个房屋的收益进行求和,然后与我不偷第4个房屋情况下的收益(就是只有3个房屋可偷的情况下的收益)进行比较大小。经过这样的迭代,就会将从1到n个房屋的情况下的最大收益记录下来,然后返回出记录表里的最后一个元素就是最大值了。而这个记录表就是我们维护的dp矩阵,状态转移方程就是f(n) = max(f(n-2)+nums[n], f(n-1))。
解题步骤:
- 定义子问题:f(n) = max(f(n-2) + nums[n] , f(n-1))
- 反复执行:从2循环到n,执行上述公式
var rob = function(nums) {
const n = nums.length
if(n === 0) return 0;
const dp = [0, nums[0]]
for(let i = 2; i <= n; i++){
dp[i] = Math.max(dp[i - 2] + nums[i-1], dp[i-1])
}
return dp[dp.length-1]
};
贪心算法
- 期盼通过每个阶段的局部最优选择,从而达到全局的最优
- 结果并不一定是最优
**LeetCode题目示例:**455.分发饼干https://leetcode-cn.com/problems/assign-cookies/
解题思路:
- 局部最优:既能满足孩子,又消耗最少
- 先将“最小的饼干”分给“胃口最小”的孩子
解题步骤:
- 对饼干数组和胃口数组升序降序
- 遍历饼干数组,找到能满足第一个孩子的饼干
- 然后继续遍历饼干数组,找到满足第二、三、…、n个孩子的饼干
var findContentChildren = function(g, s) {
const sortFunc = function(a, b){
return a-b;
};
g.sort(sortFunc); // 将数组作升序排序
s.sort(sortFunc);
let i = 0;
s.forEach(n => {
if(n >= g[i]){
i+=1;
}
});
return i
};
**LeetCode题目示例:**122.买卖股票的最佳时机Ⅱhttps://leetcode-cn.com/problems/best-time-to-buy-and-sell-stock-ii/
解题思路:
- 前提:上帝视角,知道未来的价格
- 局部最优:见好就收,见差不动,不做任何长远打算
解题步骤:
- 新建一个变量,用来统计总利润
- 遍历价格数组,如果当前价格比昨天高,就在昨天买,今天卖,否则就不交易
- 遍历结束后,返回所有利润之和
var maxProfit = function(prices) {
let profit = 0;
for(let i=0; i<=prices.length; i++) {
if(prices[i+1] > prices[i]){
profit += prices[i+1] - prices[i]
}
}
return profit
};
回溯算法
回溯算法是一种渐进式
寻找并构建问题解决方式的策略
回溯算法会先从一个可能的动作开始解决问题,如果不行,就回溯并选择另一个动作,直到将问题解决
**LeetCode题目示例:**46.全排列https://leetcode-cn.com/problems/permutations/
解题思路:
- 要求:1.所有排列情况 2.没有重复元素
- 有出路,有死路
解题步骤:
- 用递归模拟出所有情况
- 遇到包含重复元素的情况,就回溯
- 收集所有到达递归终点的情况,并返回
**LeetCode题目示例:**78.子集https://leetcode-cn.com/problems/subsets/
解题思路:
- 要求:1.所有子集 2.没有重复元素
- 有出路,有死路
解题步骤:
- 用递归模拟出所有情况
- 保证接的数字都是后面的数字
深度优先遍历与广度优先遍历
深度优先遍历:尽可能深的搜索树的分支
口诀:
- 访问根节点
- 对根节点的children挨个进行深度优先遍历
LeetCode题目示例:104.二叉树的最大深度,题目链接:https://leetcode-cn.com/problems/maximum-depth-of-binary-tree/
解题思路:
- 求最大深度,考虑使用深度优先遍历
- 在深度优先遍历的过程中,记录每个节点所在的层级level,找出最大的层级即可
//Depth-first Searches(DFS)
var maxDepth = function(root){
let res = 0; //initialize result
const dfs = (node, level) => {
if(!root) return;
res = Math.max(res, level); //res should be maximum between res and level
if(node.left) dfs(node.left, level + 1); //if node has children, dfs it and increase level by one
if(node.right) dfs(node.right, level + 1); //if node has children, dfs it and increase level by one
};
dfs(root, 1); //input root and initialize level to 1
return res;
}
广度优先遍历:先访问离根节点最近的节点
口诀:
- 新建一个队列,把根节点入队
- 把队头出队并访问
- 把队头的children挨个入队
- 重复第二、三步,直到队列为空
LeetCode题目示例:111.二叉树的最小深度,题目链接:https://leetcode-cn.com/problems/minimum-depth-of-binary-tree/
解题思路:
- 求最小深度,考虑使用广度优先遍历
- 在广度优先遍历过程中,遇到叶子节点,停止遍历,返回节点层级
//Breadth-first Searches
二叉树
如图所示为一棵现实中的二叉树
-
先序遍历
顺序:
- 访问根节点
- 对根节点的左子树进行先序遍历
- 对根节点的右子树进行先序遍历
-
中序遍历
顺序:
- 对根节点的左子树进行中序遍历
- 访问根节点
- 对根节点的右子树进行中序遍历
-
后序遍历
顺序:
- 对根节点的左子树进行后序遍历
- 对根节点的右子树进行后序遍历
- 访问根节点