做题策略
最近面了 几个一线厂,发现前端算法基本属于 leetcode hot 100 中的题 且有以下特点
- 字符串、数组、对象,堆栈等 相关操作题几乎占据 99%
- 对前端来说几乎都是简单、中等 的题,当然如出困难级别的 自动放弃了
- 链表、二叉树几乎不考 自动放弃 如准备时间充足可准备
- 前端对象相关操作 (将树铺平 将铺平数组改为树)
- 题目描述太多的一般不会出,因为笔试时间限制一般 15min 以内 长题干 需要的时间很长除非专门流出笔试时间
- 如果拿到题后 3-5 分钟一点头绪都没 则主动咨询面试官能否换题
我们拿到 leetcode hot 100 之后
先按照 “出题频率排序” 从最高的频率开始往下刷 遇到“困难”、链表、二叉树等直接跳过,这么算下大概还剩 80 题 如下是详细题解
列表转成树形结构
[
{
id: 1,
text: '节点1',
parentId: 0 //这里用0表示为顶级节点
},
{
id: 2,
text: '节点1_1',
parentId: 1 //通过这个字段来确定子父级
}
...
]
转成
[
{
id: 1,
text: '节点1',
parentId: 0,
children: [
{
id:2,
text: '节点1_1',
parentId:1
}
]
}
]
function filterArray(data, pid) {
let tree = [];
for (let i = 0; i < data.length; i++) {
if (data[i].pid == pid) {
tree.push({...data[i],children : filterArray(data, data[i].id)});
}
}
return tree;
}
树形结构转成列表
[
{
id: 1,
text: '节点1',
parentId: 0,
children: [
{
id:2,
text: '节点1_1',
parentId:1
}
]
}
]
转成
[
{
id: 1,
text: '节点1',
parentId: 0 //这里用0表示为顶级节点
},
{
id: 2,
text: '节点1_1',
parentId: 1 //通过这个字段来确定子父级
}
...
]
复制代码
1、两数之和
- 利用 hash (可用对象模拟)然后求另一个值
var twoSum = function(nums, target) {
const numsMap = {};
for(let i = 0;i<nums.length;i++){
numsMap[nums[i]] = i
}
for(let i = 0;i<nums.length;i++){
const left = target - nums[i] ;
if(numsMap[left] && numsMap[left] !== i){
return [i,numsMap[left]]
}
}
};
5. 最长回文子串
- 该返回回文, 不是奇数就是偶数,那么根据奇偶数 两问题拆分细化,合并结果就是最终答案
var longestPalindrome = function (s) {
let res = ""
for (let i = 0; i < s.length; i++) {
// 处理奇数回文串
const s1 = palindrome(s, i, i)
// 处理偶数回文串
const s2 = palindrome(s, i, i + 1)
res = res.length <= s1.length ? s1 : res
res = res.length <= s2.length ? s2 : res
}
return res
};
// 返回以l,r为中心点扩散的最长回文串
function palindrome(s, l, r) {
while (l >= 0 && r < s.length && s[l] === s[r]) {
l-- // 左扩张
r++ // 右扩张
}
return s.slice(l + 1, r)
}
3 无重复字符的最长子串
- 潜在子串 字符存到 hash 来验证 如果存在重复 则从出事位置重复的那个值 index 举行往下走
- 类似双指针概念,默认左右指针 右指针向右走当发现走的值在左右指针之间有重复,则先存下当前子串长度,然后左指针改为该重复值的 index 右指针重新从左指针的位置向右走
var lengthOfLongestSubstring = function(s) {
let maxLeng = 1;
if(!s) return 0
for(let i = 0;i<s.length;i++){
const strMap = {[s[i]]:true}
for(let j = i+1;j<= s.length;j++){
if(!strMap[s[j]]){
strMap[s[j]] = true
} else {
if(maxLeng<(j-i)){
maxLeng = j-i
}
break;
}
if(j === s.length){
if(maxLeng<(j-i)){
maxLeng = j-i
}
}
}
}
return maxLeng
};
146.LRU 缓存
用map保存key
1.get 有key 缓存value 删掉key 再set一遍
2.put 有key 删掉 重新set 超出内存 删掉第一个key
var LRUCache = function(capacity) {
this.capacity = capacity;
this.map = new Map();
};
LRUCache.prototype.get = function(key) {
if(this.map.has(key)){
let temp=this.map.get(key)
this.map.delete(key);
this.map.set(key, temp);
return temp
}else{
return -1
}
};
LRUCache.prototype.put = function(key, value) {
if(this.map.has(key)){
this.map.delete(key);
}
this.map.set(key,value);
if(this.map.size > this.capacity){
this.map.delete(this.map.keys().next().value);
}
};
70.爬楼梯
- 从答案反向思维,第一步 只存在 一步或者 2 步的情况
- 枚举发现符合公式
-
- n=1 result=1
- n=2 result=2
- n=3 result=3
- n=4 result=5 为 result(n=2)+result(n=3)之和
- n=5 result=8 为 result(n=3)+result(n=4)之和
- 该数列为斐波那契数列,数列的定义是从第三位开始,每一位的数为前两位之和。
var climbStairs = function(n) {
step[1] = 1;//爬一步方法
step[2] = 2;//爬两步方法
const step = []
for(let i = 3;i<=n;i++){
step[i] = step[i-2] + step[i-1];
}
return step[n]
};
49. 字母异位词分组
- 这道题 最难的是题目意思很模糊,题目没读懂肯定写不出来
- 题目说人话:将相同字母组成的单词放在一个数组,如果只有一个那就放一个
如下 由b\a\t 三个字母组成的单词 在输入中只有一个;n\a\t三个字母组成的单词有 2 个
e\a\t三个字母组成的单词有 3 个
- 互为字母异位词的两个字符串包含的字母相同,因此对两个字符串分别进行排序之后得到的字符串一定是相同的,故可以将排序之后的字符串作为哈希表的键。
输入: strs = ["eat", "tea", "tan", "ate", "nat", "bat"]
输出: [["bat"],["nat","tan"],["ate","eat","tea"]]
var groupAnagrams = function(strs) {
const map = new Map();
for (let str of strs) {
let array = Array.from(str);
array.sort();
let key = array.toString();
let list = map.get(key) ? map.get(key) : new Array();
list.push(str);
map.set(key, list);
}
return Array.from(map.values());
};
121. 买卖股票的最佳时机
- 贪心算法 卖出那天肯定是最大的股价,买入的价格肯定是 max 之前的 min
/**
* @param {number[]} prices
* @return {number}
*/
var maxProfit = function(prices) {
const len = prices.length;
let maxProfit = 0,maxPrice = prices[len-1];
for(let i=len-2;i>=0;i--){
maxProfit = Math.max(maxProfit,maxPrice-prices[i]);
maxPrice = Math.max(maxPrice,prices[i]);
}
return maxProfit;
};
15. 三数之和
- 先排序,锁定一个值 然后双指针靠拢,由于不能重复 所以遇到相同值跳指针
var threeSum = function (nums) {
let i, L, R, sum = 0, store = []
let newNums = nums.sort((a, b) => a - b)
for (i = 0; i < newNums.length; i++) {
if (newNums[i] > 0) break
if(newNums[i] === newNums[i - 1]) continue
L = i + 1
R = newNums.length - 1
while (L < R) {
sum = newNums[i] + newNums[L] + newNums[R]
if (sum === 0) {
store.push([newNums[i], newNums[L], newNums[R]])
while (newNums[L] === newNums[L + 1]) L++
L++
} else if (sum < 0) {
L++
} else if (sum > 0) {
R--
}
}
}
return store
}
11. 盛最多水的容器
- 双指针法。从左右两边开始计算面积,应用较高的线来寻找较长的范围,从而获得较大的面积。因此当左值较小时,左指针增加,右值较小时,右指针减小。
var maxArea = function(height) {
let l = 0,r = height.length-1;
let sum = 0;
let maxSum = 0
while (l<r) {
sum = (r-l)* Math.min(height[l], height[r])
if(height[l] >height[r]){
r--;
}else{
l++;
}
maxSum = Math.max(maxSum, sum);
}
return maxSum;
};
20. 有效的括号
虽然看题目就知道是堆栈,但是一时还在很不知道怎么写
- 不使用堆栈也可直接贪心解决:该字符一定至少存在一对[]、{}、()
var isValid = function (s) {
while (s.length) {
var temp = s;
s = s.replace('()', '');
s = s.replace('[]', '');
s = s.replace('{}', '');
if (s == temp) return false
}
return true;
};
堆栈版
var isValid = function(s) {
const stack = [];
const map = {
"(":")",
"{":"}",
"[":"]"
};
for(const x of s) {
if(x in map) {
stack.push(x);
continue;
};
if(map[stack.pop()] !== x) return false;
}
return !stack.length;
};
200. 岛屿数量
注意题意 连续大陆就是一个岛屿
遍历二维数组,每当遇到1开启搜索模式,从当前节点向左/右/上/下,每次分别移动一步,如果是1则替换为0
var numIslands = function(grid) {
function dfs(grid,i,j){
// 递归终止条件
if(i<0||i>=grid.length||j<0||j>=grid[0].length||grid[i][j]==='0'){
return
}
grid[i][j]='0' // 走过的标记为0
dfs(grid, i + 1, j)
dfs(grid, i, j + 1)
dfs(grid, i - 1, j)
dfs(grid, i, j - 1)
}
let count=0
for(let i=0;i<grid.length;i++){
for(let j=0;j<grid[0].length;j++){
if(grid[i][j]==='1'){
dfs(grid,i,j)
count++
}
}
}
return count
};
128. 最长连续序列
- 本身很简单 但是多了一个条件“时间复杂度为 O(n)”
- 查找 Set 中的元素的时间复杂度是 O(1),JS 的 Set 能给数组去掉重复元素
将数组元素存入 set 中,遍历数组 nums
如果 当前项 - 1 存在于 set ,说明当前项不是连续序列的起点,跳过,继续遍历
当前项没有“左邻居”,它就是连续序列的起点
不断在 set 中查看 cur + 1 是否存在,存在,则 count +1
cur 不再有 “右邻居” 了,就算出了一段连续序列的长度
var longestConsecutive = (nums) => {
const set = new Set(nums) // set存放数组的全部数字
let max = 0
for (let i = 0; i < nums.length; i++) {
if (!set.has(nums[i] - 1)) { // nums[i]没有左邻居,是序列的起点
let cur = nums[i]
let count = 1
while (set.has(cur + 1)) { // cur有右邻居cur+1
cur++ // 更新cur
count++
}
max = Math.max(max, count) // cur不再有右邻居,检查count是否最大
}
}
return max
}
215. 数组中的第K个最大元素
最常用以下解法
但是 复杂度不符合
- 时间复杂度:O(nlogn)
- 空间复杂度:O(logn)
/**
* @param {number[]} nums
* @param {number} k
* @return {number}
*/
var findKthLargest = function(nums, k) {
return nums.sort((a,b)=>b-a)[k-1]
};
279. 完全平方数
- 动态规划
- 思路:dp[i] 表示i的完全平方和的最少数量,dp[i - j * j] + 1表示减去一个完全平方数j的完全平方之后的数量加1就等于dp[i],只要在dp[i], dp[i - j * j] + 1中寻找一个较少的就是最后dp[i]的值。
复杂度:时间复杂度O(n* sqrt(n)),n是输入的整数,需要循环n次,每次计算dp方程的复杂度sqrt(n),空间复杂度O(n)
var numSquares = function(n) {
// 创建一个数组,直接在for里面填充好一点,不用fill(),这样节省时间
let dp = []
dp[0] = 0
for(let i = 1; i <= n; i++){
dp[i] = i
for(let j = 1; i - j * j >= 0; j++){
dp[i] = Math.min( dp[i] ,dp[i - j * j] +1 )
}
}
return dp[n]
};
739. 每日温度
栈中记录还没算出「下一个更大元素」的那些数(的下标)。
var dailyTemperatures = function (temperatures) {
const n = temperatures.length;
const ans = new Array(n).fill(0);
const st = [];
for (let i = 0; i < n; i++) {
const t = temperatures[i];
while (st.length && t > temperatures[st[st.length - 1]]) {
const j = st.pop();
ans[j] = i - j;
}
st.push(i);
}
return ans;
};
22. 括号生成
- n 意味着 n个左括号, n个右括号 递归配对,第一个一定是左括号,且剩下的一定是左括号少于右括号
- 每一个树状递归结尾都会走到 终止路线
/**
* @param {number} n
* @return {string[]}
*/
var generateParenthesis = function (n) {
var list = []
function dfs(str, left, right) {
if (left == 0 && right == 0) { // 当前左右都用光了则一次拼接完成
list.push(str)
return;
}
if (left > 0) {
dfs(str + '(', left - 1, right)
}
if (right > 0 && left < right) {
dfs(str + ')', left, right - 1)
}
}
dfs('', n, n)
return list
};
39. 组合总和
/**
* @param {number[]} candidates
* @param {number} target
* @return {number[][]}
*/
const combinationSum = (candidates, target) => {
const res = [];
function dfs(start, temp, sum){ // start是当前选择的起点索引 temp是当前的集合 sum是当前求和
if (sum > target) {
return; // 结束当前递归
}
if (sum === target) {
res.push(temp);
return;
}
for (let i = start; i < candidates.length; i++) {
dfs(i, [...temp,candidates[i]], sum + candidates[i]);
}
};
dfs(0, [], 0); // 最开始可选的数是从第0项开始的,传入一个空集合,sum也为0
return res;
};
46. 全排列
/**
* @param {number[]} nums
* @return {number[][]} 认真读题 《不含重复数字的数组》
*/
var permute = function(nums) {
let result = []
function fn(arr){
if(arr.length === nums.length){
result.push(arr)
return;
}
nums.forEach(v=>{
if(!arr.includes(v)){
fn([...arr,v])
}
})
}
fn([])
return result
};
53. 最大子数组和
var maxSubArray = function(nums) {
let ans = nums[0];
let sum = 0;
for(const num of nums) {
if(sum > 0) {
sum += num;
} else {
sum = num;
}
ans = Math.max(ans, sum);
}
return ans;
};
56. 合并区间
var merge = function (intervals) {
intervals.sort((a, b) => a[0] - b[0])
for (let i = 1; i < intervals.length; i++) {
if (intervals[i][0] <= intervals[i - 1][1]) {
intervals[i - 1][1] = Math.max(intervals[i][1], intervals[i - 1][1])
intervals.splice(i, 1)
i--
}
}
return intervals
};
78. 子集
var subsets = function (nums) {
let target = [[]];
function fn(oldArr,j){
if(j> nums.length){
return;
}
for(let k = j+1;k < nums.length;k++) {
const newArr = [...oldArr,nums[k]]
target.push(newArr);
fn(newArr,k);
}
}
for(let i = 0;i<nums.length;i++) {
target.push([nums[i]]);
fn([nums[i]],i)
}
return target
};
581 最短无序连续子数组
var findUnsortedSubarray = function(nums) {
//浅复制原数组,并排序做为参考
const temp = nums.slice();
temp.sort((a,b) => a - b);
//定义左右指针,找到符合题意的子数组的左右边界
let i = 0, j = temp.length - 1;
while (i <= j) {
if (temp[i] == nums[i]) {//如果相同,左指针一直右移
i++;
} else {//如果不同(此时子数组的左边界已经确定),则从右开始确定子数组的右边界
if (temp[j] == nums[j]) {//如果相同,右指针一直左移
j--;
} else {//如果不同,此时左右边界都确定了,就可以得出最短子数组的长度返回了
return j - i + 1;
}
}
}
//如果前面的循环里没有返回,说明原数组nums是有序的,最短子数组的长度为0
return 0;
};
560. 和为 K 的子数组
var subarraySum = function(nums, k) {
if (nums.length <= 0) return 0;
let map = new Map([[0, 1]]);
let sum = 0,
count = 0;
for (let i = 0; i < nums.length; i++) {
sum += nums[i];
// 注意顺序,要先判断存不存在sum - k
// 然后再设置map
if (map.has(sum - k)) {
count += map.get(sum - k);
}
if (!map.has(sum)) {
map.set(sum, 1);
} else {
map.set(sum, map.get(sum) + 1);
}
}
return count;
};
647. 回文子串
var countSubstrings = function(s) {
var res = [];
for (var i = 0; i < s.length; i ++) {
for (var j = 0; j < 2; j++) {
var left = i;
var right = left + j;
while(s[left] && s[left] === s[right]) {
var subString = s.substring(left, right + 1);
res.push(subString);
left--;
right++;
}
}
}
return res.length;
};
26. 删除有序数组中的重复项
/**
* @param {number[]} nums
* @return {number}
*/
var removeDuplicates = function (nums) {
let slow = 0;
let fast = 0;
while(fast < nums.length) {
if (nums[slow] !== nums[fast]) {
slow++;
nums[slow] = nums[fast];
}
fast += 1;
}
return slow + 1;
};
33. 搜索旋转排序数组
var search = function(nums, target) {
let map = new Map;
for(let i = 0;i < nums.length;i++)
map.set(nums[i],i);
if(map.has(target)) return map.get(target);
else return -1;
};
34. 在排序数组中查找元素的第一个和最后一个位置
var searchRange = function(nums, target) {
//查找左边界的函数,是左边界二分查找模版
const findLeft = (nums, target) => {
let left = 0, right = nums.length - 1
while (left <= right) {
let mid = Math.floor((right - left) / 2) + left // 当前做左和右的 中间值
if (nums[mid] > target)// 在中间值的左边 则更新右角标
right = mid - 1
else if (nums[mid] < target) // 在中间值的右边 则更新左角标
left = mid + 1
else if (nums[mid] == target)
right = mid - 1
}
//注意不要在此处进行条件判断返回-1
return left
}
let result = new Array(2)
//而是在此处进行条件判断
const resultLeft = findLeft(nums, target)
if (resultLeft >= nums.length || nums[resultLeft] != target)// 出界或者不存在则返回 -1 -1
result = [-1, -1]
else
result = [resultLeft, findLeft(nums, target + 1) - 1] // 否则利用 目标值+1 其左边界就是期望的右边界
return result
};
48. 旋转图像
解题思路:找规律:
- 首先,对于矩阵中第 i行的第 j个元素,在旋转后,它出现在倒数第 i列的第 j行个位置。推导出:
Matrix[j][n - 1 - i] = Matrix[i][j]
- 由于题目要求原地旋转,所以需要一次性同时交换四个位置:按照1中的公式,找出这四个位置的旋转关系:
Matrix[j][n - 1 - i] = Matrix[i][j]
Matrix[n - 1 - i][n - 1 - j] = Matrix[j][n - 1 - i]
Matrix[n - 1 - j][i] = Matrix[n - 1 - i][n - 1 - j]
Matrix[i][j] = Matrix[n - 1 - j][i]
- 确定旋转的次数:
-
- 当n为偶数时:需要旋转 n^2 / 4 = n / 2 * n / 2次。即:双层循环,每层次数为 n / 2 次
- 当n为奇数时:需要旋转 (n^2 − 1) / 4 = ((n−1)/2) * ((n+1)/2)次。即:双层循环,第一层次数为n - 1 / 2次,第二层次数为 n + 1 / 2次(其实也可以反过来)。
- 综合以上两种情况:第一层循环的次数为 Math.floor(n / 2) 次,第二层循环的次数为Math.floor((n + 1) / 2)次(其实也可以反过来)
var rotate = function(matrix) {
const n = matrix.length;
for (let row = 0; row < Math.floor(n / 2); row++) {
for (let col = 0; col < Math.floor((n + 1) / 2); col++) {
const temp = matrix[row][col] // 需要引入一个临时变量来中转一下
matrix[row][col] = matrix[n - 1 - col][row];
matrix[n - 1 - col][row] = matrix[n - 1 - row][n - 1 - col];
matrix[n - 1 - row][n - 1 - col] = matrix[col][n - 1 - row];
matrix[col][n - 1 - row] = temp;
}
}
};
56. 合并区间
解题思路: 先排序,再合并。
- 先将区间集合根据左区间的进行升序排列。原因:
-
- 其实用一端就能判断出两个区间有没有重叠
- 只需要关心合并后的右区间
- 将默认的重叠区间设为intervals[0]。然后从intervals[1]开始遍历,如果当前区间的左区间 <= 重叠区间的右区间。说明有重叠。将两个区间进行合并,并且更新重叠区间:overLappingIntervals[1] = max(overLappingIntervals[1], cur[1])。
- 如果当前区间没和当前的重叠区间发生重叠,那么将重叠区间加入res。并且更新最新的重叠区间为cur。
- 记得遍历完区间后,需要将最后一个重叠区间加入到res中。
var merge = function (intervals) {
const res = [];
// 先根据左区间进行排序,好处: 1.判断一端就能保证区间有没有重叠 2.只需要确定合并后的右区间
intervals = intervals.sort((a, b) => a[0] - b[0]);
// overLappingIntervals 为最新的重叠区间,默认的第一个重叠区间为 intervals[0]
let overLappingIntervals = intervals[0];
for (let i = 1; i < intervals.length; i++) {
let cur = intervals[i];
// 有重合: |------|
// |------|
if (overLappingIntervals[1] >= cur[0]) {
overLappingIntervals[1] = Math.max(cur[1], overLappingIntervals[1]);
} else {
// 不重合,overLappingIntervals推入res数组
res.push(overLappingIntervals);
overLappingIntervals = cur; // 更新 overLappingIntervals
}
}
// 重点!! 记得将最后一个重叠区间push
res.push(overLappingIntervals);
return res;
};
75. 颜色分类
- 第一个循环把 0 放在最前面
- 第二个循环把 1 放在 最后一个 0开始的位置
var sortColors = function(nums) {
let ptr = 0;
for (let i = 0; i < nums.length; ++i) {
if (nums[i] === 0) {
[nums[i], nums[ptr]] = [nums[ptr], nums[i]];
++ptr;
}
}
for (let i = ptr; i < nums.length; ++i) {
if (nums[i] === 1) {
[nums[i], nums[ptr]] = [nums[ptr], nums[i]];
++ptr;
}
}
};
88 合并两个有序数组
var merge = function(nums1, m, nums2, n) {
let k = m + n - 1, i = m - 1, j = n - 1;
while(j >= 0){
if(nums1[i] >= nums2[j] ){
nums1[k--] = nums1[i--]
}else{
nums1[k--] = nums2[j--]
}
}
};
189. 轮转数组
215. 数组中的第K个最大元素
/**
* @param {number[]} nums
* @param {number} k
* @return {number}
*/
var findKthLargest = function(nums, k) {
return nums.sort((a,b)=>b-a)[k-1]
};
283. 移动零
/**
* @param {number[]} nums
* @return {void} Do not return anything, modify nums in-place instead.
*/
var moveZeroes = function(nums) {
nums.sort((a,b) => b? 0: -1)
};
14、最长公共前缀
/**
* @param {string[]} strs
* @return {string}
*/
var longestCommonPrefix = function(strs) {
strs.sort()//按编码排序
if (strs.length === 0) return ''//空数组返回''
var first = strs[0],
end = strs[strs.length - 1]
if(first === end || end.match(eval('/^' + first + '/'))){
return first//first包含于end返回first
}
for(var i=0;i<first.length;i++){
if(first[i] !== end[i]){
return first.substring(0,i)//匹配失败时返回相应字符串
}
}
};
136. 只出现一次的数字
var singleNumber = (nums) => {
let res = nums[0]
for (let i = 1; i < nums.length; i++) {
res = res ^ nums[i]
}
return res
}
169. 多数元素
/**
* @param {number[]} nums
* @return {number}
*/
var majorityElement = function(nums) {
nums.sort((a,b) => a - b)
return nums[Math.floor(nums.length / 2)]
};
242. 有效的字母异位词
287. 寻找重复数
快慢指针:题目说数组必存在重复数,所以 nums 数组肯定可以抽象为有环链表。
首先,如果有环的话,那么快慢指针一定会在环内相遇。
- 相遇时,慢指针走的距离:D+S1D+S1
- 假设相遇时快指针已经绕环 n 次,它走的距离:D+n(S1+S2)+S1D+n(S1+S2)+S1
- 因为快指针的速度是 2 倍,所以相同时间走的距离也是 2 倍:D+n(S1+S2)+S1 = 2(D+S1)D+n(S1+S2)+S1=2(D+S1)即 (n-1)S1+ nS2=D(n−1)S1+nS2=D
- 我们不关心绕了几次环,取 n = 1 这种特定情况,消掉 S1: D=S2
所以此时,让快指针回到原点,当下次慢指针和快指针相遇的时候,就是在环的入口处。
1 3 4 2 2
0 1 2 3 4
var findDuplicate = function(nums) {
let fast = 0;
let slow = 0;
while (1) {
fast = nums[nums[fast]];
slow = nums[slow];
if (slow === fast) {
fast = 0;
while(nums[slow] != nums[fast]) {
fast = nums[fast];
slow = nums[slow];
}
return nums[slow];
}
}
};
454. 四数相加 II
解题思路:Hash Map: 简单的说,将四数之和转化为两数之和。
- 列举出nums1和nums2的所有组合放入mapGroup1中。
- 将nums3和nums4进行组合,统计nums3和nums4的和与mapGroup1相加结果为0的个数
/**
* @param {number[]} nums1
* @param {number[]} nums2
* @param {number[]} nums3
* @param {number[]} nums4
* @return {number}
*/
var fourSumCount = function(nums1, nums2, nums3, nums4) {
const numMap = new Map()
let res = 0;
for(let n1 of nums1) {
for(let n2 of nums2) {
let cnt = numMap.get(n1+n2) || 0
numMap.set(n1+n2, cnt+1)
}
}
for(let n3 of nums3) {
for(let n4 of nums4) {
if (numMap.has(0-(n3+n4))) {
res += numMap.get(0-(n3+n4))
}
}
}
return res
};
1021. 删除最外层的括号
核心还是堆栈,思维方式要反
输入:s = "(()())(())"
每次输入)时 前面移除一位,如果移除空了则加入字符中记录
/**
* @param {string} S
* @return {string}
*/
var removeOuterParentheses = function(S) {
if (!S || S === '') {
return '';
}
let res = '';
let stack = [];
let start = 0;
for (let i = 0; i < S.length; i++) {
if (S[i] === '(') {
stack.push('(');
} else {
stack.pop();
if (stack.length === 0) {
res += S.substring(start + 1, i);
start = i + 1;
}
}
}
return res;
};
1047. 删除字符串中的所有相邻重复项
- 消消乐策略
- 堆栈模型
/**
* @param {string} s
* @return {string}
*/
var removeDuplicates = function(s) {
const stack = [s[0]];
let topIndex = 0;
for (let i = 1; i < s.length; i++) {
// 1. 长度为0的情况,直接进入队列
if (topIndex === -1) {
stack.push(s[i]);
topIndex += 1;
// 2. 判断栈顶和当前的s[i]是否相等,相等的话则出栈
} else if (stack[topIndex] === s[i]) {
topIndex -= 1;
stack.pop();
} else {
// 不相等的话,入栈,topIndex + 1
topIndex += 1;
stack.push(s[i]);
}
}
return stack.join('');
};
55. 跳跃游戏
- 实质是判断数组是否为环形链表
/**
* @param {number[]} nums
* @return {boolean}
*/
var canJump = function(nums) {
let max = 0;// 计算每个元素能跳最远的位置,所有元素最远的位置小于数组长度 则肯定跳不到最后
for(var i = 0; i <= max; i++){
max = Math.max(nums[i] + i, max);
}
return i >= nums.length
};
回溯算法
常用模板
- 大函数包裹小函数
- 小函数一般两个传参,
-
- 第一个参 是解合计 首次传递默认是空
- 第二个参数是子问题的项 首次传递大函数的默认值
- 首先是终止 if 内包裹最后一条时 做的操作 如 push 等
- 下面是循环 符合条件则继续递归小函数
- 每次递归小函数时 参数变小 类似回溯
- 回溯算法最终解一般在小函数外定义的变量进行收集
如下是常见的标准模板
function backtrack(path, options) {
if (/* 终止条件 */) {
// 存储/打印结果
result.push(path.slice()); // 注意要拷贝一份,不要直接 push path
return;
}
for (let i = 0; i < options.length; i++) {
// 做选择
path.push(options[i]);
// 继续递归探索
backtrack(path, options);
// 撤销选择
path.pop();
}
}
// 使用示例
let result = [];
backtrack([], [1, 2, 3]); // 这里的例子是求 [1,2,3] 的所有子集
console.log(result);
17. 电话号码的字母组合
var letterCombinations = function(digits) {
if(!digits){
return []
}
const map = {
2: ["a", "b", "c"],
3: ["d", "e", "f"],
4: ["g", "h", "i"],
5: ["j", "k", "l"],
6: ["m", "n", "o"],
7: ["p", "q", "r", "s"],
8: ["t", "u", "v"],
9: ["w", "x", "y", "z"],
};
let arr = []
function dfs(startArr,strs){
if(!strs.length){
arr.push(startArr)
return ;
}else{
const str = map[strs[0]];
for(let i = 0;i< str.length;i++){
dfs(startArr+str[i],strs.slice(1,strs.length))
}
}
}
dfs("",digits);
return arr
};
22. 括号生成
var generateParenthesis = function(n) {
const res = [];
// 1. 第一个关键点: '所有可能' 确定用递归的方式实现。
const dfs = function(lRemain , rRemain, str) {
// 2. 第二个关键点: 递归结束的条件:str的长度为 n 的两倍
if (str.length === n * 2) {
res.push(str);
return;
}
// 3. 第三个关键点: 有效的字符串必须是 左括号优先进入。且左括号的个数永远小于等于右括号
// 选择左括号的前提条件:只要左括号还有的剩,可以直接选择左括号
if (lRemain > 0) {
dfs(lRemain - 1, rRemain, str + '(');
}
// 选择右括号的前提条件: 剩余的右括号要大于左括号时,才可以让右括号进入,才能保证有效性
if (rRemain > lRemain) {
dfs(lRemain, rRemain - 1, str + ')');
}
}
dfs(n , n, '');
return res;
}
39. 组合总和
/**
* @param {number[]} candidates
* @param {number} target
* @return {number[][]}
*/
var combinationSum = function(candidates, target) {
const res = [];
const dfs = (canStartIndex, currentSum, currentArr) => {
if (currentSum > target) {
return;
}
if (currentSum === target) {
res.push([...currentArr])
return;
}
for (let i = canStartIndex; i < candidates.length; i++) {
dfs(i, currentSum + candidates[i], [...currentArr, candidates[i]]);
}
}
dfs(0, 0, []);
return res;
}
46. 全排列
/**
* @param {number[]} nums
* @return {number[][]}
*/
var permute = function(nums) {
const res = [];
const dfs = (usedIndexs, currentArr) => {
if (currentArr.length === nums.length) {
res.push([...currentArr]);
return;
}
for (let i = 0; i < nums.length; i++) {
// 如果已经选择过当前下标,那么就不做处理
if (usedIndexs[i]) {
continue;
}
dfs(
{...usedIndexs, [i]: true},
[...currentArr, nums[i]]
);
}
}
dfs({}, []);
return res;
};
78. 子集
/**
* @param {number[]} nums
* @return {number[][]}
*/
var subsets = function(nums) {
const res = [];
const dfs = (startIndex, list) => {
res.push(list);
// 由于必须从选择过了的元素右侧开始选择
for (let i = startIndex; i < nums.length; i++) {
dfs(i + 1, [...list, nums[i]]);
}
};
dfs(0, []);
return res;
};
// []
// / | \
// [1] [2] [3]
// / \ |
// [1, 2] [1, 3] [2, 3]
// /
// [1, 2, 3]
79. 单词搜索
var exist = function (board, word) {
const row = board.length;
const columns = board[0].length;
let used = new Array(row).fill(false).map(() => new Array(columns).fill(false))
// 该函数计算从 i,j 这个点开始找能否找到
const canFind = (i, j, currentWordIndex) => {
if (currentWordIndex === word.length) {
return true;
}
if (i < 0 || j < 0 || i >= row || j >= columns) {
return false; // 1.下标越界的场景,不能走出框外 (下标越界的情况要最先判断、提前return。防止return)
}
if (used[i][j]) {
return false // 3. 可能上一步往上走、下一步又往下走了,不行。所以记录一下(不能走走过的点)
}
// 在dfs函数中,判断false场景
if (board[i][j] !== word[currentWordIndex]) {
return false // 2. 下一步的字符对不上
}
used[i][j] = true;
// 上、下、左、右都走走一下,试一试
const canFindRes =
canFind(i + 1, j, currentWordIndex + 1)
|| canFind(i, j + 1, currentWordIndex + 1)
|| canFind(i - 1, j, currentWordIndex + 1)
|| canFind(i, j - 1, currentWordIndex + 1)
if (canFindRes) {
return true;
} else {
// 重点!!!: 如果上下左右都走不通,这个点行不通,回撤路径
used[i][j] = false;
return false;
}
}
for (let i = 0; i < row; i++) {
for (let j = 0; j < columns; j++) {
// 先找到 '头'。这个总没错的
if (board[i][j] === word[0]) {
// 每到一个点做的事情是一样的。DFS 往下选点,构建路径。
if (canFind(i, j, 0)) {
// 一旦找到一个就立马return
return true;
}
}
}
}
return false;
};
console.log('res', exist([["A","B","C","E"],["S","F","C","S"],["A","D","E","E"]], 'SEE'));
93. 复原 IP 地址
/**
* @param {string} s
* @return {string[]}
*/
var restoreIpAddresses = (s) => {
const res = [];
const dfs = (canStartIndex, selectArr) => {
const unCheckedRes = selectArr.join('.');
if (selectArr.length === 4 && unCheckedRes.length === s.length + 3) {
res.push(unCheckedRes);
}
for (let singleLen = 1; singleLen <= 3; singleLen++) {
const nextStartIndex = canStartIndex + singleLen;
// 假设下标越界了,直接返回
if (nextStartIndex > s.length) {
return;
}
const str = s.substring(canStartIndex, nextStartIndex);
// 假设当前截断的数超过255 或 02、033 这样的情况直接返回
if (Number(str) > 255 || (str.length > 1 && str[0] === '0')) {
return;
}
dfs(nextStartIndex, [...selectArr, str]);
}
}
dfs(0, []);
return res;
}
动态规划
动态规划(Dynamic Programming, DP)是一种解决优化问题的方法,它将问题分解为更小的子问题,并将这些子问题的解存储起来,以便在解决更大问题时重用它们。这种方法避免了重复计算相同的子问题,从而提高了算法的效率。
在JavaScript中,实现动态规划通常涉及创建一个数组(或二维数组)来存储子问题的解,然后按照问题的要求填充这个数组。
下面是一个使用动态规划解决“斐波那契数列”问题的模板和例子:
// 动态规划模板 - 斐波那契数列
function fibonacci(n) {
// 创建一个数组来存储斐波那契数列的值
const dp = new Array(n + 1).fill(0);
// 初始化基本情况
dp[0] = 0;
if (n > 0) {
dp[1] = 1;
}
// 填充剩余的值
for (let i = 2; i <= n; i++) {
dp[i] = dp[i - 1] + dp[i - 2];
}
// 返回第n个斐波那契数
return dp[n];
}
// 示例:计算第10个斐波那契数
const n = 10;
console.log(fibonacci(n)); // 输出: 55
在这个例子中,fibonacci函数接受一个参数n,并返回斐波那契数列中的第n个数。我们使用一个数组dp来存储斐波那契数列的值,其中dp[i]表示第i个斐波那契数。我们从基本情况开始(即dp[0] = 0和dp[1] = 1),然后使用一个循环来计算剩余的值。
53. 最大子数组和
var maxSubArray = function(nums) {
let ans = nums[0];
let sum = 0;
for(const num of nums) {
if(sum > 0) {
sum += num;
} else {
sum = num;
}
ans = Math.max(ans, sum);
}
return ans;
};
62. 不同路径
/**
* @param {number} m
* @param {number} n
* @return {number}
*/
var uniquePaths = function(m, n) {
// 特殊情况,单独处理
if (m * n === 1 || n * m === 2) {
return 1;
}
// 1. 定义dp[i][j]: 到达dp[i][j]有几种不同的路径
// 2. dp[i][j] = dp[i + 1][j] + dp[i][j + 1]
// 3. Base Case: dp[m - 1][*] = 1; dp[*][n - 1] = 1;
const dp = new Array(m).fill(false).map(() => new Array(n).fill(false));
for (let row = 0; row < m; row++) {
dp[row][0] = 1;
}
for (let col = 0; col < n; col++) {
dp[0][col] = 1;
}
for (let i = 1; i < m; i++) {
for (let j = 1; j < n; j++) {
// 考虑一下下标越界的情况
dp[i][j] = dp[i - 1][j] + dp[i][j - 1];
}
}
console.log(dp);
return dp[m - 1][n - 1]
};
64. 最小路径和
var minPathSum = (grid) => {
// 思路:其实这也是一个动态规划的题 要到[m,n]的最小路径,就算到 Math.min([m-1, n],[m. n -1])的最小路径
const columns = grid[0].length; // 列
const row = grid.length; // 行
// 自己维护一个二维数组来保存到各个点的最小路径
const dp = new Array(row)
.fill(null)
.map((item) => new Array(columns).fill(0));
dp[0][0] = grid[0][0];
// 向下和向右都只有一种走法,先将第一行和第一列填充好。(已知条件)
for (let i = 1; i < columns; i++) {
dp[0][i] = dp[0][i - 1] + grid[0][i];
}
for (let i = 1; i < row; i++) {
dp[i][0] = dp[i - 1][0] + grid[i][0];
}
// 开始计算到每个点的路径
for (let i = 1; i < row; i++) {
for (let j = 1; j < columns; j++) {
dp[i][j] = Math.min(dp[i - 1][j], dp[i][j - 1]) + grid[i][j];
}
}
return dp[row - 1][columns - 1];
};
70. 爬楼梯
/**
* @param {number} n
* @return {number}
*/
var climbStairs = function(n) {
const dp = new Array(n + 1).fill(false);
dp[1] = 1;
dp[2] = 2;
for (i = 3; i <= n; i++) {
dp[i] = dp[i - 1] + dp[i - 2];
}
return dp[n];
};
96. 不同的二叉搜索树
/**
* @param {number} n
* @return {number}
*/
var numTrees = function(n) {
// dp[i]=∑dp[j]∗dp[i−j−1],0<=j<=i−1
const dp = new Array(n + 1).fill(false);
dp[0] = 1;
dp[1] = 1;
// 从2开始,计算 dp[i]。
for (let i = 2; i < n + 1; i++) {
let count = 0;
for (let j = 0; j < i; j++) {
// j 为左边分配的个数,那么右边分配的个数为 i - j - 1(还有一个根元素)
count += dp[j] * dp[i - j - 1];
}
dp[i] = count;
}
return dp[n];
};
121. 买卖股票的最佳时机
var maxProfit = function(prices) {
let isMax = 0;
for(let i = 1;i<prices.length;i++){
if(prices[i-1] <prices[i]){
isMax=1
}
}
if(isMax === 0){
return 0
}
let max = Math.max(...prices.slice(1,prices.length));
const min = Math.min(...prices.slice(0,prices.length-1));
return max-min
};
152. 乘积最大子数组
/**
* @param {number[]} nums
* @return {number}
*/
var maxProduct = function(nums) {
let res = nums[0];
// 存在负负得正的情况,所以dp[i]的最大值、最小值都要记录一下,因为nums[i]可能为正数、也可能为负数
const dp = new Array(nums.length).fill(false).map(() => new Array(2));
// base case
dp[0][0] = nums[0]; // 以下标为0结尾的子数组的最小值;
dp[0][1] = nums[0]; // 以下标为0结尾的子数组的最大值;
for (let i = 1; i < nums.length; i++) {
dp[i][0] = Math.min(nums[i], dp[i - 1][0] * nums[i], dp[i - 1][1] * nums[i]);
dp[i][1] = Math.max(nums[i], dp[i - 1][0] * nums[i], dp[i - 1][1] * nums[i]);
res = Math.max(dp[i][1], res);
}
return res;
}
198. 打家劫舍
/**
* @param {number[]} nums
* @return {number}
*/
var rob = function (nums) {
let dp = new Array(nums).fill(false);
dp[0] = nums[0];
dp[1] = Math.max(nums[0], nums[1]);
for (let i = 2; i < nums.length; i++) {
// 假设,当前的值和前前家的加起来大于偷上一家的最大值。
if (nums[i] + dp[i - 2] > dp[i - 1]) {
dp[i] = dp[i - 2] + nums[i]
} else {
dp[i] = dp[i - 1]
}
}
return dp[nums.length - 1];
};
221. 最大正方形
279. 完全平方数
/**
* @param {number} n
* @return {number}
*/
var numSquares = function(n) {
// 定义dp[i]:和为 i 的完全平方数的最小数量
const dp = new Array(n + 1).fill(false);
// 初始化 Base Case:
dp[0] = 0;
dp[1] = 1;
// 计算dp[i]
for (let i = 2; i < n + 1; i++) {
dp[i] = i; // 默认设一个最大值为i(比如4 = 1 + 1 + 1 + 1 就是最坏的情况)
// 枚举 [1, √i]的所有情况
for (let j = 1; i - j * j >= 0; j++) {
dp[i] = Math.min(dp[i - j * j] + 1, dp[i]);
}
}
console.log(dp);
return dp[n];
};
300. 最长递增子序列
/**
* @param {number[]} nums
* @return {number}
*/
var lengthOfLIS = function(nums) {
// 含第 i 个元素的最长上升子序列的长度。
// 在这里顺便做了一个 初始化 Base Case
const dp = new Array(nums.length).fill(1);
let res = dp[0];
for (let i = 1; i < nums.length; i++) {
for (let j = 0; j < i; j++) {
// dp[i] 有个默认的初始化值 为1
if (nums[i] > nums[j]) {
dp[i] = Math.max(dp[i], dp[j] + 1)
} else {
dp[i] = Math.max(dp[i], dp[j]);
}
}
res = Math.max(res, dp[i]);
}
return res;
}
322. 零钱兑换
/**
* @param {number[]} coins
* @param {number} amount
* @return {number}
*/
var coinChange = function (coins, amount) {
if (amount === 0) {
return 0;
}
// 初始化Base Case 1
const dp = new Array(amount + 1).fill(-1);
// 初始化Base Case 2
for (coin of coins) {
dp[coin] = 1;
}
for (i = 1; i < amount + 1; i++) {
for (coin of coins) {
// 考虑下标越界的情况 && 上一步的硬币数是可以组成的
if (i - coin >= 0 && dp[i - coin] > 0) {
// 先确保 dp[i] 能被组成不为 -1。 再考虑取较小值
if (dp[i] > 0) {
dp[i] = Math.min(dp[i], dp[i - coin] + 1)
} else {
dp[i] = dp[i - coin] + 1;
}
}
}
}
console.log(dp);
return dp[amount]
};
647. 回文子串
深度优先搜索
深度优先搜索(Depth-First Search, DFS)是一种用于遍历或搜索树或图的算法。这个算法会尽可能深地搜索树的分支。在图中,这个算法是用来标记访问过和未访问过的顶点,并保持追踪它当前访问的顶点。
这个例子中,我们有一个简单的无向图,用邻接列表来表示。neighbors对象包含了图中每个节点及其相邻节点的信息。dfs函数是深度优先搜索的实现,它接受当前节点和一个记录访问状态的visited对象。如果当前节点未被访问过,我们就标记它为已访问,并对它的每个邻居递归调用dfs函数。
注意:在实际应用中,你可能需要根据你的具体需求和数据结构来调整这个模板。例如,如果你的图是有向的,或者你需要收集关于遍历路径的额外信息,你可能需要修改这个模板来适应这些需求。
function dfs(node, visited) {
if (node == null) {
return;
}
if (!visited[node]) {
visited[node] = true;
console.log(node); // 或者其他你想对节点进行的操作
// 遍历当前节点的所有邻居
for (let neighbor of neighbors[node]) {
dfs(neighbor, visited);
}
}
}
// 示例图(以邻接列表形式表示)
const neighbors = {
'A': ['B', 'C'],
'B': ['A', 'D', 'E'],
'C': ['A', 'F'],
'D': ['B'],
'E': ['B', 'F'],
'F': ['C', 'E'],
};
// 初始化访问状态
const visited = {};
for (let node in neighbors) {
visited[node] = false;
}
// 从节点'A'开始深度优先搜索
dfs('A', visited);