leetcode hot 100 是必须要过一遍的,其次 codetop 勾选公司字节、按照频率排序,前 50 必刷的
刷题三条主线
- top100 排查困难之外的题
- codetop 中 字节+前端+高频题 前 50
- codetop 标记的题
刷题需要背的东西
- js 数组 字符串、Math 等基础 api
- 二分查找、链表、二叉树、回溯、递归、动态规划等 通用公式
top 38 ,如果你面试没时间准备全部,那至少这38题 必须刷
- 2.两数相加
- 3. 无重复字符的最长子串
- 88. 合并两个有序数组
- 165. 比较版本号
- 53. 最大子数组和
- 112. 路径总和
- 20. 有效的括号
- 46. 全排列
- 415. 字符串相加
- 129. 求根到叶子节点数字
- 54. 螺旋矩阵
- 121. 买卖股票的最佳时机 122. 买卖股票的最佳时机 II
- 15. 三数之和
- 200. 岛屿数量
- 141. 环形链表
- 102. 二叉树的层序遍历
- 215. 数组中的第K个最大
- 5. 最长回文子串
- 206. 反转链表
- 209. 长度最小的子数组
- 695. 岛屿的最大面积
- 70. 爬楼梯
- 42.接雨水
- 4.寻找两个正序数组的中位数
- 14.最长公共前缀
-
- 交替合并字符串
- 27.移除元素
- 128.最长连续序列
- 28.找出字符串中第一个匹配项
- 11.盛最多水的容器
- 49.字母异位词分组
-
- 合并两个有序链表
- 704.二分查找
- 26.删除有序数组中的重复项
- 56.合并区间
- 18.四数之和
- 22.括号生成
- 560. 和为 K 的子数组
哈希
- 对每个值进行 hash 存储
- 记住 .split('') 是字符转数组 .join() 是数组转字符
/**
* @param {string[]} strs
* @return {string[][]}
*/
var groupAnagrams = function(strs) {
if(strs.length===0){
return [[""]]
}
if(strs.length===1){
return [[strs[0]]]
}
let map = {}
for(let i = 0;i<strs.length;i++){
const key = strs[i].split('').sort()
if(map[key]){
map[key] = [...map[key],strs[i]]
}else{
map[key] = [strs[i]]
}
}
return Object.values(map)
};
1.1. 128. 最长连续序列
key存数字,value存什么?
- 新存入的数字,如果它找到相邻的数,它希望从邻居数那里获取什么信息?
- 很显然它希望,左邻居告诉它左边能提供的连续长度,右邻居告诉它右边能提供的连续长度
- 加上它自己的长度,就有了自己处在的连续序列的长度
- 同处一个连续序列的数字的value理应都相同,这是它们共同特征
- 但没有必要每个的value都是序列长度,只需要两端的数存序列的长度就好
- 因为靠的是两端和新数对接,序列是连续的,中间没有空位
- 序列的一端找到邻居后,将另一端对应的value更新为最新的序列长度
var longestConsecutive = (nums) => {
let map = new Map()
let max = 0
for (const num of nums) { // 遍历nums数组
if (!map.has(num)) { // 重复的数字不考察,跳过
let preLen = map.get(num - 1) || 0 // 获取左邻居所在序列的长度
let nextLen = map.get(num + 1) || 0 // 获取右邻居所在序列的长度
let curLen = preLen + 1 + nextLen // 新序列的长度
map.set(num, curLen) // 将自己存入 map
max = Math.max(max, curLen) // 和 max 比较,试图刷新max
map.set(num - preLen, curLen) // 更新新序列的左端数字的value
map.set(num + nextLen, curLen) // 更新新序列的右端数字的value
}
}
return max
}
法二
只有当前节点是无左邻居 即是起点时才开始进行向右循环判断 查找
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
}
1.2. 560. 和为 K 的子数组
const subarraySum = (nums, k) => {
const map = { 0: 1 };
let prefixSum = 0;
let count = 0;
for (let i = 0; i < nums.length; i++) {
prefixSum += nums[i];
if (map[prefixSum - k]) { // 这个前缀和已经有了
count += map[prefixSum - k];
}
map[prefixSum] = map[prefixSum] ? map[prefixSum]+1 : 1;
}
return count;
};
1.3. 49. 字母异位词分组
var groupAnagrams = function(strs) {
if(strs.length===0){
return [[""]]
}
if(strs.length===1){
return [[strs[0]]]
}
let map = {}
for(let i = 0;i<strs.length;i++){
const key = strs[i].split('').sort()
if(map[key]){
map[key] = [...map[key],strs[i]]
}else{
map[key] = [strs[i]]
}
}
return Object.values(map)
};
1. 滑动窗口
1.1. 3. 无重复字符的最长子串 🔥
var lengthOfLongestSubstring = function(s) {
let strMap = {[s[0]]:0};
let max= 1
if(!s){
return 0
}
if(s.length === 1){
return 1
}
for(let i = 1;i<s.length;i++){
if(strMap[s[i]] && strMap[s[i]]!== 0){
i = strMap[s[i]];
const length = Object.keys(strMap).length
max = max<length?length:max
strMap= {}
}else if(!strMap[s[i]]){
strMap[s[i]] = i;
if(i === s.length-1){
const length = Object.keys(strMap).length
max = max<length?length:max
}
}
}
return max
};
2. 普通数组
2.1. 704. 二分查找
var search = function(nums, target) {
if(nums.length === 1){
return nums[0] ===target ? 0 :-1
}
let left = 0;
let right= nums.length-1;
while(left<=right){
const sum = left +right;
const index = sum %2 === 0 ? sum/2 :sum/2+0.5;
if(nums[index] === target){
return index;
}
if(nums[index] > target){
right = index-1
}
if(nums[index] < target){
left = index+1
}
}
return -1
};
2.2. 27. 移除元素
var removeElement = (nums, val) => {
let k = 0;
for(let i = 0;i < nums.length;i++){
if(nums[i] != val){
nums[k] = nums[i];
k++
}
}
return k;
};
2.3. 977. 有序数组的平方
var sortedSquares = function(nums) {
let n = nums.length;
let res = new Array(n).fill(0);
let i = 0, j = n - 1, k = n - 1;
while (i <= j) {
let left = nums[i] * nums[i],
right = nums[j] * nums[j];
if (left < right) {
res[k--] = right;
j--;
} else {
res[k--] = left;
i++;
}
}
return res;
};
2.4. 209. 长度最小的子数组🔥
var minSubArrayLen = function(target, nums) {
let start, end
start = end = 0
let sum = 0
let len = nums.length
let ans = Infinity
// 两层循环外层负责 后指针的移动,第二层负责前指针移动
while(end < len){
sum += nums[end];
while (sum >= target) { // 当滑动窗口符合条件时
ans = Math.min(ans, end - start + 1); // 取最小 answer
sum = sum - nums[start]; // 前指针向前移动
start++;
}
end++; // 后指针向后移动
}
return ans === Infinity ? 0 : ans
};
2.5. 59. 螺旋矩阵 II 🔥
var generateMatrix = function(n) {
let startX = startY = 0; // 起始位置
let loop = Math.floor(n/2); // 旋转圈数
let mid = Math.floor(n/2); // 中间位置
let offset = 1; // 控制每一层填充元素个数
let count = 1; // 更新填充数字
let res = new Array(n).fill(0).map(() => new Array(n).fill(0));
while (loop--) {
let row = startX, col = startY;
// 上行从左到右(左闭右开)
for (; col < n - offset; col++) {
res[row][col] = count++;
}
// 右列从上到下(左闭右开)
for (; row < n - offset; row++) {
res[row][col] = count++;
}
// 下行从右到左(左闭右开)
for (; col > startY; col--) {
res[row][col] = count++;
}
// 左列做下到上(左闭右开)
for (; row > startX; row--) {
res[row][col] = count++;
}
// 更新起始位置
startX++;
startY++;
// 更新offset
offset += 1;
}
// 如果n为奇数的话,需要单独给矩阵最中间的位置赋值
if (n % 2 === 1) {
res[mid][mid] = count;
}
return res;
};
2.6. 41. 缺失的第一个正数
思路
- 题目不让用额外的空间,时间复杂度又要是 O(n)
- 我们可以利用 nums 数组这个空间本身,不影响 nums 数组的原信息的情况下,利用它存储一些额外的信息
缺失的最小正整数 的范围
- 假设它为 n,n >= 1 是肯定的,还意味着 1 、2、 3、 …… 、n-1 肯定在数组里存在
- 如果排好序的话,元素 1 ~ n-1 排在数组的前面,然后 n 缺失,比 n 大的元素在不在数组里都不影响 n 是缺失的最小正整数
- 所以 nums 数组的长度最短可以是 n-1,nums.length >= n-1 ,即 n >= nums.length+1
- 比方说,数组有 5 个元素,n 肯定是在 [1,6] 中。n 为 6 ,就是 1~5 正好占满了数组
交换元素 重排数组
- 我们希望数组中尽量小的正整数放在前面,以便更快地找到目标 n
- 1 出现在 位置 0 ,2 出现在位置 1…… 遍历时看哪个位置没有出现该出现的元素
- 即,nums[i] 从 位置 i 交换到 位置 nums[i]-1 。[1,nums.length+1] 以外的数不用交换
题目把 nums 看作一个集合
- 题意把 nums 数组当做一个存放元素的集合,找出没有出现在集合里的最小正整数
- 对元素进行位置的交换,元素继续存在于集合中,没有改变原有的信息
- 将部分的数安排到合适的位置,让 nums 数组承载一些额外信息,帮助解决问题
/**
* @param {number[]} nums
* @return {number}
*/
const firstMissingPositive = (nums) => {
for (let i = 0; i < nums.length; i++) {
while (
nums[i] >= 1 &&
nums[i] <= nums.length && // 对1~nums.length范围内的元素进行安排
nums[nums[i] - 1] !== nums[i] // 已经出现在理想位置的,就不用交换
) {
const temp = nums[nums[i] - 1]; // 交换
nums[nums[i] - 1] = nums[i];
nums[i] = temp;
}
}
// 现在期待的是 [1,2,3,...],如果遍历到不是放着该放的元素
for (let i = 0; i < nums.length; i++) {
if (nums[i] != i + 1) {
return i + 1;
}
}
return nums.length + 1; // 发现元素 1~nums.length 占满了数组,一个没缺
};
2.7. 238. 除自身以外数组的乘积
思路:从左往右遍历,记录从左到当前位置前一位的乘积,然后从右往左遍历,从左到当前位置前一位的乘积乘上右边元素的积。
复杂度:时间复杂度O(n),空间复杂度O(1)
- a\b\c\d
- 1\a\ab\abc(前面数的乘积list1)
- bcd\cd\d\1(后面数的乘积list2)
- list1 与 list2 相乘
var productExceptSelf = function (nums) {
const res = [];
res[0] = 1;
//从左往右遍历
//记录从左到当前位置前一位的乘积
for (let i = 1; i < nums.length; i++) {
res[i] = res[i - 1] * nums[i - 1];
}
let right = 1;
//从右往左遍历
//从左到当前位置前一位的乘积 乘上 右边元素的积
for (let j = nums.length - 1; j >= 0; j--) {
res[j] *= right;
right *= nums[j];
}
return res;
};
2.8. 合并区间
/**
* @param {number[][]} intervals
* @return {number[][]}
*/
/**
* @param {number[][]} intervals
* @return {number[][]}
*/
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
};
3. 字符串
3.1. 344. 反转字符串
/**
* @param {character[]} s
* @return {void} Do not return anything, modify s in-place instead.
*/
var reverseString = function(s) {
let left = 0 ;
let right = s.length-1;
while(left<=right){
[s[left],s[right]] = [s[right],s[left]]
left++
right--
}
return s
};
3.2. 541. 反转字符串 II
/**
* @param {string} s
* @param {number} k
* @return {string} 每计数至 2k 个字符 这个是循环,题目没读懂
*/
var reverseStr = function(s, k) {
const len = s.length;
let resArr = s.split("");
for(let i = 0; i < len; i += 2 * k) { // 每隔 2k 个字符的前 k 个字符进行反转
let l = i - 1;
r = i + k > len ? len : i + k;
while(++l < --r) {
[resArr[l], resArr[r]] = [resArr[r], resArr[l]];
}
}
return resArr.join("");
};
3.3. 151. 反转字符串中的单词
/**
* @param {string} s
* @return {string}
*/
/**
* @param {string} s
* @return {string}
*/
var reverseWords = function(s) {
// 字符串转数组
const strArr = Array.from(s);
// 移除多余空格
removeExtraSpaces(strArr);
// 翻转
reverse(strArr, 0, strArr.length - 1);
let start = 0;
for(let i = 0; i <= strArr.length; i++) {
if (strArr[i] === ' ' || i === strArr.length) {
// 翻转单词
reverse(strArr, start, i - 1);
start = i + 1;
}
}
return strArr.join('');
};
// 删除多余空格
function removeExtraSpaces(strArr) {
let slowIndex = 0;
let fastIndex = 0;
while(fastIndex < strArr.length) {
// 移除开始位置和重复的空格
if (strArr[fastIndex] === ' ' && (fastIndex === 0 || strArr[fastIndex - 1] === ' ')) {
fastIndex++;
} else {
strArr[slowIndex++] = strArr[fastIndex++];
}
}
// 移除末尾空格
strArr.length = strArr[slowIndex - 1] === ' ' ? slowIndex - 1 : slowIndex;
}
// 翻转从 start 到 end 的字符
function reverse(strArr, start, end) {
let left = start;
let right = end;
while(left < right) {
// 交换
[strArr[left], strArr[right]] = [strArr[right], strArr[left]];
left++;
right--;
}
}
3.4. 右旋字符串
3.5. 28. 找出字符串中第一个匹配项的下标
/**
* @param {string} haystack
* @param {string} needle
* @return {number}
*/
var strStr = function(haystack, needle) {
const next = [];
let j = 0;
next.push(j);
for (let i = 1; i < needle.length; i++) {
while (j > 0 && needle[i] !== needle[j]){
j = next[j - 1];
}
if (needle[i] === needle[j]){
j++;
}
next.push(j);
}
console.log(next)
let k = 0;
for (let i = 0; i < haystack.length; ++i) {
while (k > 0 && haystack[i] !== needle[k]){
k = next[k - 1];
}
if (haystack[i] === needle[k]){
k++;
}
if (k === needle.length){
return (i - needle.length + 1);
}
}
return -1;
};
3.6. 459. 重复的子字符串
var repeatedSubstringPattern = function (s) {
if (s.length === 0)
return false;
const getNext = (s) => {
let next = [];
let j = 0;
next.push(j);
for (let i = 1; i < s.length; ++i) {
while (j > 0 && s[i] !== s[j])
j = next[j - 1];
if (s[i] === s[j])
j++;
next.push(j);
}
return next;
}
let next = getNext(s);
console.log(next)
return next[next.length - 1] >0 && s.length % (s.length - next[next.length - 1]) === 0;
};
4. 哈希表
4.1. 242. 有效的字母异位词
var isAnagram = function(s, t) {
if(s.length !== t.length) return false;
let char_count = new Map();
for(let item of s) {
char_count.set(item, (char_count.get(item) || 0) + 1) ;
}
for(let item of t) {
if(!char_count.get(item)) return false;
char_count.set(item, char_count.get(item)-1);
}
return true;
};
4.2. 349. 两个数组的交集
var intersection = function (nums1, nums2) {
if(nums1.length > nums2.length) {
[nums1,nums2] = [nums2,nums1]
}
const set = new Set([])
const res = [];
nums1.forEach((num) => {
if (!set.has(num)) {
set.add(num)
}
});
nums2.forEach((num) => {
if (set.has(num)) {
res.push(num)
}
});
return [... new Set(res)];
};
4.3. 202. 快乐数
var isHappy = function(n) {
const set = new Set([]);
function fn(str){
let sum = 0
for(let i = 0;i<str.length;i++){
sum = sum + Number(str[i])* Number(str[i])
}
if(set.has(sum)){
return false
}
set.add(sum)
if(sum === 1){
return true
}
return fn(String(sum))
}
return fn(String(n)) || false
};
4.4. 1. 两数之和
/**
* @param {number[]} nums
* @param {number} target
* @return {number[]}
*/
var twoSum = function(nums, target) {
const numsMap = new Map();
for(let i = 0;i<nums.length;i++){
numsMap.set(nums[i],i)
}
for(let i = 0;i<nums.length;i++){
const left = target - nums[i] ;
if(numsMap.has(left) && numsMap.get(left) !== i){
return [i,numsMap.get(left)]
}
}
};
4.5. 454. 四数相加 II
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
};
4.6. 383. 赎金信
if(ransomNote.length>magazine.length){
return false
}
let objMap = {}
for(let i = 0;i<magazine.length;i++){
objMap[magazine[i]] = objMap[magazine[i]] ? objMap[magazine[i]]+1:2;
}
for(let i = 0;i<ransomNote.length;i++){
if(objMap[ransomNote[i]]){
objMap[ransomNote[i]]--
}
if(objMap[ransomNote[i]] <0 || !objMap[ransomNote[i]]){
return false
}
}
return true
};
列表转成树形结构
[
{
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;
}
4.7. 15. 三数之和 🔥
- 先排序
- 三指针法 初始第一个指针pre在 0 ,第二个指针 left 1 第三个指针right 在最后
-
- 大于 0 right--
- 小于 0 left++
- left 和 right 走到一起的时候 pre++ left = pre+1 right 在最后
- 重点是左右指针先判断是否重复才继续(只能在这里去重)
var threeSum = function(nums) {
const res = [], len = nums.length
// 将数组排序
nums.sort((a, b) => a - b)
for (let i = 0; i < len; i++) { // i 是第一个数
let l = i + 1, r = len - 1, iNum = nums[i]
// 数组排过序,如果第一个数大于0直接返回res
if (iNum > 0) return res
// 去重
if (iNum !== nums[i - 1]) { // 只有不相等才进入逻辑
while(l < r) {
const lNum = nums[l], rNum = nums[r]
const threeSum = iNum + lNum + rNum
// 三数之和小于0,则左指针向右移动
if (threeSum < 0) l++
else if (threeSum > 0) r--
else {
res.push([iNum, lNum, rNum])
// 去重
while(l < r && nums[l] == nums[l + 1]){
l++
}
while(l < r && nums[r] == nums[r - 1]) {
r--
}
l++
r--
}
}
}
}
return res
};
- 数组排序
- 4 个指针,固定前 2 个指针 移动后面 2 个指针
-
- 根据大小值判断活跃指针是否右移动
- 核心还是去重
var fourSum = function(nums, target) {
const len = nums.length;
if(len < 4) return [];
nums.sort((a, b) => a - b);
const res = [];
for(let i = 0; i < len - 3; i++) {
// 去重i
if(i > 0 && nums[i] === nums[i - 1]) continue;
for(let j = i + 1; j < len - 2; j++) {
// 去重j
if(j > i + 1 && nums[j] === nums[j - 1]) continue;
let l = j + 1, r = len - 1;
while(l < r) {
const sum = nums[i] + nums[j] + nums[l] + nums[r];
if(sum < target) { l++; continue}
if(sum > target) { r--; continue}
res.push([nums[i], nums[j], nums[l], nums[r]]);
// 对nums[left]和nums[right]去重
while(l < r && nums[l] === nums[++l]);
while(l < r && nums[r] === nums[--r]);
}
}
}
return res;
};
4.8. 454. 四数相加 II
解题思路:Hash Map: 简单的说,将四数之和转化为两数之和。
- 列举出nums1和nums2的所有组合放入mapGroup1中。
- 将nums3和nums4进行组合,统计nums3和nums4的和与mapGroup1相加结果为0的个数
- 实际复杂度 n 方
/**
* @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
};
5. 双指针
双指针大部分情况就是 维护一个读指针(快指针) 一个写指针(慢指针)
- 双指针的核心思想是找哪个是快指针
5.1. 27. 移除元素
- 反方向思考 只有不相同的元素进行重新保存 并使用 k 保留顺序
var removeElement = (nums, val) => {
let k = 0;
for(let i = 0;i < nums.length;i++){
if(nums[i] != val){
nums[k] = nums[i];
k++;
}
}
return k;
};
5.2. 206. 反转链表🔥
- cur 是当前节点,最后处理的时候将其改成下一个节点 方便下次使用
- pre 是当前节点的下一个几点
- temp 是用于转换的临时变量
/**
* Definition for singly-linked list.
* function ListNode(val, next) {
* this.val = (val===undefined ? 0 : val)
* this.next = (next===undefined ? null : next)
* }
*/
/**
* @param {ListNode} head
* @return {ListNode}
*/
// 双指针:
var reverseList = function(head) {
if(!head || !head.next) return head;
let temp = null, pre = null, cur = head;
// 1、 暂存 当前的 next 到最后复制给 cur
// 2、 将当前的 next 赋值pre (正向的上一个)
// 3、 pre 给就是当前的 cur
// 4、 cur 重新赋值为 之前暂存的 cur.next
while(cur) {
temp = cur.next;
cur.next = pre;
pre = cur;
cur = temp;
}
// temp = cur = null;
return pre;
};
// 递归:
var reverse = function(pre, head) {
if(!head) return pre;
const temp = head.next;
head.next = pre;
pre = head
return reverse(pre, temp);
}
var reverseList = function(head) {
return reverse(null, head);
};
// 递归2
var reverse = function(head) {
if(!head || !head.next) return head;
// 从后往前翻
const pre = reverse(head.next);
head.next = pre.next;
pre.next = head;
return head;
}
var reverseList = function(head) {
let cur = head;
while(cur && cur.next) {
cur = cur.next;
}
reverse(head);
return cur;
};
5.3. 11. 盛最多水的容器
- 左右哪个短,哪个先移动
var maxArea = function(height) {
let l = 0,r = height.length-1;
let max = 0;
while(l<r){
const result = (r-l)*Math.min(height[l],height[r])
max = Math.max(max,result);
if(height[l] > height[r]){
r--
}else{
l++
}
}
return max
};
5.4. 283. 移动零
- l和 r 之间都是 0 要做的就是将 l-r 向右移动,
- 遇0 则 r+1 非 0 则 r+1 l 与该值互换 并+1
var moveZeroes = function(nums) {
let l = 0 ,r=0;
while(r<=nums.length-1){
if(nums[r]===0){
if(nums[l] === 0){
r++
}
}else{
[nums[r],nums[l]] = [nums[l],nums[r]];
l++;
r++;
}
}
return nums
};
5.5. 42. 接雨水🔥
// 双指针解法
// 注意到动态规划中
// 对于位置 i 的接水量取决于 leftMax 和 rightMax 中的较小者
// 所以我们不必真的知道较大者是谁
// 只要知道较小者是谁就可以了
// 初始化 left = 0, right = n-1, leftMax = 0, rightMax = 0
// 注意到对于位置 left 来说, leftMax 是真正意义上的左侧最大值, 而 rightMax 不是真的右侧最大值
// 而对于位置 right 来说, rightMax 是真正意义上的右侧最大值, 而 leftMax 不是真的左侧最大值
// 从左往右扫描
// 1. 使用 height[left] 和 height[right] 更新 leftMax, rightMax
// 2. 若 height[left] < height[right], 则说明对于位置 left 来说,
// leftMax 一定小于其右侧真正意义上的最大值
// 因为连当前右侧的局部最大值 rightMax 都比不过, 更比不过右侧真正意义上的最大值
// 而我们又不需要真的知道右侧真正意义上的最大值
// 3. 类似地处理 height[left] >= height[right] 的情况
var trap = function (height) {
let ans = 0,
left = 0,
right = height.length - 1,
leftMax = 0,
rightMax = 0;
while (left < right) {
leftMax = Math.max(leftMax, height[left]);
rightMax = Math.max(rightMax, height[right]);
if (leftMax < rightMax) {
ans = ans + leftMax - height[left];
left++
} else {
ans = ans + rightMax - height[right];
right--
}
}
return ans;
};
单调栈解法
相当于「竖着」计算面积,单调栈的做法相当于「横着」计算面积。
这个方法可以总结成 16个字:找上一个更大元素,在找的过程中填坑。
注意 while 中加了等号,这可以让栈中没有重复元素,从而在有很多重复元素的情况下,使用更少的空间。
var trap = function(height) {
let ans = 0;
const st = [];
for (let i = 0; i < height.length; i++) {
while (st.length && height[i] >= height[st[st.length - 1]]) {
const bottomH = height[st.pop()];
if (st.length === 0) {
break;
}
const left = st[st.length - 1];
const dh = Math.min(height[left], height[i]) - bottomH; // 面积的高
ans += dh * (i - left - 1);
}
st.push(i);
}
return ans;
};
6. 动态规划
- 常见于求某种条件的极限值
通用动态规划公式:
对于一个给定问题 P,其状态可以用一个变量(或一组变量)S 表示。动态规划的核心在于构建一个表格(或数据结构)dp,其中 dp[S] 存储与状态 S 相关的最优解或所需信息。动态规划的计算过程通常遵循以下步骤:
- 定义状态:明确描述问题的状态空间,确定状态变量 S。一般是数组 dp,需要考虑初始值的问题,需要考虑
dp[0]的取值问题 - 状态转移方程: 每次循环时根据 dp 执行状态转移公式
-
- 根据问题的性质,建立状态之间的关系,即状态转移方程 dp[S] = f(dp[S1], dp[S2], ..., S)。
- f 是一个函数,它根据前一阶段或前几个阶段的状态(如 S1, S2, ...) 和当前状态 S 的相关信息,计算出当前状态 S 的最优解或所需信息。
- 初始化:为 dp 表格中的基础状态赋值,这些通常是问题的边界条件或最小粒度的子问题解。
- 填充表格:按照一定的顺序(如自底向上、自顶向下等),依据状态转移方程递归地或迭代地计算并填充 dp 表格。
- 解答:从 dp 表格中提取最终答案,通常是某个特定状态 S_final 对应的 dp[S_final] 值。
以上步骤适用于各种动态规划问题,尽管具体的 S、f、初始状态和填充顺序会因问题而异。在实际编程中,dp 可能是一维数组、二维数组,甚至是更复杂的结构,取决于问题的维度和状态表示方式。上述示例中的斐波那契数列、最大连续子序列和以及最长公共子序列问题均遵循这一通用框架。
6.1. 118. 杨辉三角
var generate = function(numRows) {
let list = [[1]]
if(numRows.length ===1){
return list
}
for(let i= 1 ;i<numRows;i++){
let k =i;
let arr = []
for(let j = 0;j<=k;j++){
const left = list[i-1][j-1] || 0
const right = list[i-1][j] || 0
arr[j] = left + right
}
list.push(arr)
}
return list
};
6.2. 152. 乘积最大子数组
var maxProduct = function(nums) {
let max = nums[0]
let imax = 1
let imin = 1
for(let num of nums) {
if(num < 0) {
[imax, imin] = [imin, imax]
}
imax = Math.max(num, num * imax)
imin = Math.min(num, num * imin)
max = Math.max(imax, max)
}
return max
};
/**
如: nums = [2,3,-2,4]
循环
i
0 num = 2; imax = 2, imin = 1, max = 2
1 num = 3 imax = 6, imin = 1, max = 6
2 num = -2 < 0, 交换 => imax = 1, imin = 6 => imax = -2, imin = -12, max = 6
3 num = 4 imax = 4, imin = -48, max = 6
*/
6.3. 70. 爬楼梯🔥
var climbStairs = function(n) {
const dp = [];
dp[0] = 1;
dp[1] = 1;
for (let i = 2; i <= n; i++) {
dp[i] = dp[i - 1] + dp[i - 2];
}
return dp[n];
};
6.4. 416. 分割等和子集
/**
* @param {number[]} nums
* @return {boolean}
*/
// 背包容量为 sum/2
// 求是否存在一组数满足 和为sum/2
var canPartition = function(nums) {
const sum = nums.reduce((prev, curr)=> prev + curr);
if(sum%2 !== 0){
return false
}
const target = sum/2;
const dp = new Array(target + 1).fill(false);
dp[0] = true
for(let num of nums) {
for(let i = target; i >=num; i--) {
dp[i] = dp[i] || dp[i - num];
}
}
return dp[target]
};
6.5. 300. 最长递增子序列
- dp[i]含义:从0到下标为i的序列的最长子序列长度
- 根据这个定义,我们的最终结果(子序列的最大长度)应该是 dp 数组中的最大值。
- 假设我们已经知道了 dp[0..4] 的所有结果,我们如何通过这些已知结果推出 dp[5] 呢?
- nums[5] = 3,既然是递增子序列,我们只要找到前面那些结尾比 3 小的子序列,然后把 3 接到最后,就可以形成一个新的递增子序列,而且这个新的子序列长度加一。
// 洗牌策略 第一个默认第一堆 后面每一个和前面每堆最后一个 比较 更小则放入 否则新建一个堆
// 每一堆最后一个数相连就是 最长递增子序列 堆的个数就是答案
var lengthOfLIS = function(nums) {
let n = nums.length;
if(n == 0){
return 0;
}
let list =[[nums[0]]]
for(let i = 1;i< nums.length;i++){
let isNewNum = true
for(let j = 0;j<list.length;j++){
if(nums[i]<=list[j][list[j].length-1]){
list[j].push(nums[i]);
isNewNum = false
break;
}
}
if(isNewNum){
list.push([nums[i]])
}
}
return list.length
};
// 4 4 3
// 10 8
// 9
6.6. 32. 最长有效括号
1,括号抵消。有效的括号组合必然能左右相消。(即右括号进来时,栈顶为左括号的话就pop)。
2,记录谁被消除了。(引入一个对象,它一方面需要记录是什么括号。另一方面需要记录它在数组的位置)。
3,得出最终栈后,相邻对象间可能被消除了n对括号。(通过位置差值得出)。
4,特殊情况。开头符号或末尾符号都被pop了。未记录栈中。于是 s = ')' + s + ‘(’;
var longestValidParentheses = function(s) {
let max = 0
if (s.length < 1) return max
let len = s.length
// 栈顶之所有加入一个-1,纯粹是为了方便计算有效括号的长度
// 不然就需要手动调整为i-j+1;同时而确保第一个字符为")"时不需要特殊处理
let stack = [-1]
for(let i = 0; i < len; i++) {
let value = s[i]
if (value === '(') {
stack.push(i)
}
if (value === ')') {
stack.pop()
// 栈顶加入一个pivot字符")",实际上是方便计算有效括号串长度
if (stack.length < 1) {
stack.push(i)
} else {
max = Math.max(max, i - stack[stack.length - 1])
}
}
}
return max
};
6.7. 198. 打家劫舍
- 状态转移公式
-
Math.max(dp[i-1],(dp[i-2] || 0)+nums[i])
var rob = function(nums) {
let dp = [nums[0]];
for(let i = 1;i<nums.length;i++){
dp[i] = Math.max(dp[i-1],(dp[i-2] || 0)+nums[i])
}
return Math.max(...dp)
};
6.8. 279. 完全平方数
- 【默认值思考】dp 默认放最大值
Math.min(dp[i], (dp[i - j * j]) + 1);
对数组进行遍历,下标为 i,每次都将当前数字先更新为最大的结果,即 dp[i]=i,比如 i=4,最坏结果为 4=1+1+1+1 即为 4 个数字
动态转移方程为:dp[i] = MIN(dp[i], dp[i - j * j] + 1),i 表示当前数字,j*j 表示平方数
时间复杂度:O(n∗sqrt(n))O(n*sqrt(n))O(n∗sqrt(n)),sqrt 为平方根
var numSquares = function(n) {
const dp = [0]; // 数组长度为n+1,值均为0
for(let i= 1;i<=n;i++){
let j = 1;
let square = 1
dp[i] = i
while(i>= square){
dp[i] = Math.min(dp[i], (dp[i - j * j]) + 1);
j++;
square = j*j
};
}
return dp[n];
};
6.9. 322. 零钱兑换
- dp 初始值怎么设置
- 第二次循环怎么设计
- dp 公式 dp[i] = Math.min(dp[i],dp[i-coins[j]]+1 )
var coinChange = function(coins, amount) {
let dp = new Array( amount + 1 ).fill( Infinity );
dp[0] = 0
for(let i = 1;i<=amount;i++){
let j = 0;
while(j<coins.length){
if(i-coins[j]>=0){
dp[i] = Math.min(dp[i],dp[i-coins[j]]+1 )
}
j++
}
}
return dp[amount] === Infinity?-1 :dp[amount]
};
6.10. 139. 单词拆分
- 第二个循环从小从 0 到 i 开始计算
const wordBreak = (s, wordDict) => {
const wordSet = new Set(wordDict);
const len = s.length;
const dp = new Array(len + 1).fill(false);
dp[0] = true;
for (let i = 1; i <= len; i++) {
for (let j = i - 1; j >= 0; j--) { // j去划分成两部分
const suffix = s.slice(j, i); // 后缀部分 s[j: i-1]
if (wordSet.has(suffix) && dp[j]) { // 后缀部分是单词,且左侧子串[0,j-1]的dp[j]为真
dp[i] = true;
break; // dp[i] = true了,i长度的子串已经可以拆成单词了,不需要j继续划分子串了
}
}
}
return dp[len];
};
7. 多维动态规划
7.1. 62. 不同路径
回溯算法(超时)
var uniquePaths = function(m, n) {
let count = 0;
function dfs(x,y){
if(y === m && x === n){
count++
return;
}
if(y+1<=m){
dfs(x,y+1)
}
if(x+1<=n){
dfs(x+1,y)
}
}
dfs(1,1)
return count
}
7.2. 5. 最长回文子串
var longestPalindrome = function(s) {
if(s.length === 1){
return s
}
let maxLength = 0;
let maxStr = ''
for(let i = 0;i<s.length;i++){
if(i+1<s.length){
fn(i,i+1);
}
if(i+2<s.length){
fn(i,i+2);
}
}
function fn (l,r){
while(l>=0 && r<s.length && s[l] === s[r]){
if(maxLength<r-l){
console.log(l,r)
maxLength = r-l;
maxStr = s.slice(l,r+1)
}
l--;
r++;
}
}
return maxStr || s[0]
};
8. 贪心算法
说实话贪心算法并没有固定的套路。
所以唯一的难点就是如何通过局部最优,推出整体最优。
那么如何能看出局部最优是否能推出整体最优呢?有没有什么固定策略或者套路呢?
不好意思,也没有! 靠自己手动模拟,如果模拟可行,就可以试一试贪心策略,如果不可行,可能需要动态规划。
有同学问了如何验证可不可以用贪心算法呢?
最好用的策略就是举反例,如果想不到反例,那么就试一试贪心吧。
如何判断要不要用贪心算法
- 手动模拟部分值
- 举反例
8.1. 贪心算法一般分为如下四步:
- 将问题分解为若干个子问题
- 找出适合的贪心策略
- 求解每一个子问题的最优解
- 将局部最优解堆叠成全局最优解
8.2. 121. 买卖股票的最佳时机 🔥
var maxProfit = function(prices) {
let ans = 0;
let minPrice = prices[0];
for (const p of prices) {
ans = Math.max(ans, p - minPrice);
minPrice = Math.min(minPrice, p);
}
return ans;
};
8.3. 455. 分发饼干
- 2 个数组都排序
- 双指针循环匹配
var findContentChildren = function(g, s) {
let count = 0;
g.sort((a,b)=>a-b)
s.sort((a,b)=>a-b)
let i =0,j = 0;
while(i<g.length && j< s.length){
console.log(g[i], s[j])
if(g[i] > s[j]){
j++;
}else{
count++
i++
j++
}
}
return count
};
8.4. 376. 摆动序列
var wiggleMaxLength = function(nums) {
if(nums.length <= 1) return nums.length
let result = 1
let preDiff = 0
let curDiff = 0
for(let i = 0; i < nums.length - 1; i++) {
curDiff = nums[i + 1] - nums[i]
if((curDiff > 0 && preDiff <= 0) || (curDiff < 0 && preDiff >= 0)) {
result++
preDiff = curDiff
}
}
return result
};
8.5. 53. 最大子数组和🔥
- 当前指针之前的和为 小于等于 则丢弃当前元素之前的数
var maxSubArray = function (nums) {
let sum = 0; // 临时求和
let ans = nums[0]; // 真是答案
for (let num of nums) {
if (sum > 0) {
sum = sum + num
} else {
sum = num
}
ans = Math.max(ans, sum)
}
return ans
};
8.6. 122. 买卖股票的最佳时机 II
var maxProfit = function(prices) {
let result = 0
for(let i = 1; i < prices.length; i++) {
result += Math.max(prices[i] - prices[i - 1], 0)
}
return result
};
8.7. 134. 加油站
var canCompleteCircuit = function(gas, cost) {
let curSum = 0
let min = Infinity
for(let i = 0; i < gas.length; i++) {
let rest = gas[i] - cost[i]
curSum += rest
if(curSum < min) {
min = curSum
}
}
if(curSum < 0) return -1 //1.总油量 小于 总消耗量
if(min >= 0) return 0 //2. 说明油箱里油没断过
//3. 从后向前,看哪个节点能这个负数填平,能把这个负数填平的节点就是出发节点
for(let i = gas.length -1; i >= 0; i--) {
let rest = gas[i] - cost[i]
min += rest
if(min >= 0) {
return i
}
}
return -1
}
8.8. 1005. K 次取反后最大化的数组和
var largestSumAfterKNegations = function(nums, k) {
nums.sort((a, b) => Math.abs(b) - Math.abs(a)); // 排序
let sum = 0;
for(let i = 0; i < nums.length; i++) {
if(nums[i] < 0 && k-- > 0) { // 负数取反(k 数量足够时)
nums[i] = -nums[i];
}
sum += nums[i]; // 求和
}
if(k % 2 > 0) { // k 有多余的(k若消耗完则应为 -1)
sum -= 2 * nums[nums.length - 1]; // 减去两倍的最小值(因为之前加过一次)
}
return sum;
};
8.9. 135. 分发糖果
8.10. 860. 柠檬水找零
var lemonadeChange = function(bills) {
let fiveCount = 0
let tenCount = 0
for(let i = 0; i < bills.length; i++) {
let bill = bills[i]
if(bill === 5) {
fiveCount += 1
} else if (bill === 10) {
if(fiveCount > 0) {
fiveCount -=1
tenCount += 1
} else {
return false
}
} else {
if(tenCount > 0 && fiveCount > 0) {
tenCount -= 1
fiveCount -= 1
} else if(fiveCount >= 3) {
fiveCount -= 3
} else {
return false
}
}
}
return true
};
8.11. 406. 根据身高重建队列
- 遇到两个维度同时考虑的时候 一定要先考虑一个维度
- 当第一维度相同时 可以开始再按照第二维度进行排序
- 不知道哪个维度 可以分别尝试
var reconstructQueue = function(people) {
let queue = []
people.sort((a, b ) => {
if(b[0] !== a[0]) {
return b[0] - a[0]
} else {
return a[1] - b[1]
}
})
for(let i = 0; i < people.length; i++) {
queue.splice(people[i][1], 0, people[i])
}
return queue
};
8.12. 452. 用最少数量的箭引爆气球
- 完全没有理解
var findMinArrowShots = function(points) {
points.sort((a, b) => {
return a[0] - b[0]
})
let result = 1
for(let i = 1; i < points.length; i++) {
if(points[i][0] > points[i - 1][1]) {
result++
} else {
points[i][1] = Math.min(points[i - 1][1], points[i][1])
}
}
return result
};
9. 回溯算法
回溯算法是一种通过探索所有可能的候选解来找出所有解的算法。如果候选解被确认不是一个解(或者至少不是最后一个解),回溯算法会通过在上一步进行一些变化来丢弃该解,即“回溯”并尝试另一个可能的候选解。
以下是一个简单的回溯算法框架,使用JavaScript编写,可以作为前端实现回溯算法的通用模板:虽然实际编程中不存在严格的数学公式来表示回溯算法,但在编写前端JavaScript或其他语言实现回溯算法时,我们可以抽象出一个通用的逻辑框架或者说模板。以下是一个简化的前端JavaScript风格的回溯算法模板:
什么时候用回溯
- 计算所有可能的结果时
// 定义一个全局结果容器用于存储所有有效解
let result = [];
// 回溯函数,其中:
// - path 是当前正在构建的解的路径
// - choices 是当前剩余可供选择的元素集合
function backtrack(path = [], choices = [...inputArray]) {
// 终止条件:如果找到一个解或者满足特定条件时停止递归
if (/* 达到了结束条件 */) {
// 将当前路径添加到结果集中
result.push([...path]);
return;
}
// 遍历当前层的所有可能性
for (let i = 0; i < choices.length; i++) {
// 做出一个选择,并将其从剩余的选择中移除(如果是不可重复选择的情况)
let choice = choices[i];
let newChoices = [...choices.slice(0, i), ...choices.slice(i + 1)];
// 将当前选择加入到路径中
path.push(choice);
// 递归进入下一层决策树
backtrack(path, newChoices);
// 回溯:撤销当前选择
path.pop();
}
}
// 初始化并调用回溯函数
const inputArray = [/* 待处理数据 */];
backtrack();
// 输出最终结果
console.log(result);
这个模板涵盖了回溯算法的核心逻辑,包括递归调用、选择、递归前进和回溯。具体应用时,你需要根据实际问题调整终止条件部分,以及如何做出选择和是否允许重复选择等细节。例如,在解决子集生成问题时,“终止条件”可能是所有元素都已经被遍历过,而在解决八皇后问题时,则是当所有皇后都被合法放置时。
- 1、首次输入什么
- 2、什么时候第一次 push
- 3、什么时候开始 return
- 4、循环中怎么赋值新值
9.1. 22. 括号生成🔥
// 从左到右的思维 第一个肯定是左括号 最后一个肯定是右括号
var generateParenthesis = function (n) {
var list = []
function dfs(s = '',left,right){
if(!left && !right){
list.push(s);
return;
}
if(left>0){
dfs(`${s}(`,left-1,right)
}
if(right>0&& left<right){
dfs(`${s})`,left,right-1)
}
}
dfs('',n,n)
return list
};
9.2. 78. 子集
- 根据子集是否去重判断什么时候 push
var subsets = function(nums) {
let list = []
function dfs(velueArr,arr){
list.push(velueArr);
if(velueArr.length === nums.length){
return
}
for(let i = 0;i<arr.length;i++){
dfs([...velueArr,arr[i]],arr.slice(i+1,arr.length))
}
}
dfs([],nums)
return list
};
9.3. 46. 全排列🔥
- 1、不含重复数字的数组
- 2、每次都是一个 for 循环 但是只取 当前 arr 没有的值
/**
* @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
};
9.4. 131. 分割回文串
- 能手写一个函数实现判断指定区间是否符合回文
const isPalindrome = (s, l, r) => {
for (let i = l, j = r; i < j; i++, j--) {
if(s[i] !== s[j]) return false;
}
return true;
}
var partition = function(s) {
const res = [], len = s.length;
function backtracking(startIndex,arr=[]) {
if(arr.join('').length >= len) {// 当前收集的数组已饱和
res.push(arr);
return;
}
for(let i = startIndex; i < len; i++) {
if(isPalindrome(s, startIndex, i)){ // 如果当前区间是数组
// 携带当前的结果 并寻找剩下的数据 进行排查
backtracking(i + 1,[...arr,s.slice(startIndex, i + 1)]);
}
}
}
backtracking(0);
return res;
};
9.5. 39. 组合总和
/**
* @param {number[]} candidates
* @param {number} target
* @return {number[][]}
*/
var combinationSum = function(candidates, target) {
let list = []
candidates.filter(v=>v<=target)
const sum=(array) => array.reduce((a,b)=>a+b,0)
function dfs(start,arr){
const value = sum(arr)
if(value === target){
list.push(arr);
return;
}
if(value>target){
return
}
for(let i =start;i<candidates.length;i++){
dfs(i,[...arr,candidates[i]])
}
}
dfs(0,[]);
return list
};
10. 二分查找
10.1. 4. 寻找两个正序数组的中位数
/**
* @param {number[]} nums1
* @param {number[]} nums2
* @return {number}
*/
/**
* @param {number[]} nums1
* @param {number[]} nums2
* @return {number}
*/
var findMedianSortedArrays = function(nums1, nums2) {
// 保证num1是比较短的数组
if (nums1.length > nums2.length) {
[nums1, nums2] = [nums2, nums1];
}
const length1 = nums1.length;
const length2 = nums2.length;
let min = 0;
let max = length1;
let half = Math.floor((length1 + length2 + 1) / 2);
while (max >= min) {
const i = Math.floor((max + min) / 2);
const j = half - i;
if (i > min && nums1[i - 1] > nums2[j]) {
max = i - 1;
} else if (i < max && nums1[i] < nums2[j - 1]) {
min = i + 1;
} else {
let left,right;
if (i === 0) left = nums2[j - 1];
else if (j === 0) left = nums1[i - 1];
else left = Math.max(nums1[i - 1], nums2[j - 1]);
if (i === length1) right = nums2[j];
else if (j === length2) right = nums1[i];
else right = Math.min(nums1[i], nums2[j]);
return (length1 + length2) % 2 ? left : (left + right) / 2;
}
}
return 0;
};
11. 技巧题
根据题目的特定限制 找技巧
11.1. 136. 只出现一次的数字
/**
* @param {number[]} nums
* @return {number}
出现 2 次的数字在异或中都抵消了,最后得出只出现 1 次的数
*/
var singleNumber = (nums) => {
let res = nums[0]
for (let i = 1; i < nums.length; i++) {
res = res ^ nums[i]
}
return res
}
11.2. 169. 多数元素
/**
* @param {number[]} nums
* @return {number}
设置nums[0] 为 x 当前值等于 x m+1 否则 m-1 当 m==0 则说明当前值
相同的加1, 不相同的减1, 因为是大于一半, 所以最后肯定剩下大于一半的那个
m再次等于 0 所以一定是遇到了
*/
var majorityElement = function(nums) {
let x = 0
let m = 0
for(let n of nums){
x = m === 0 ? n : x
m = m + (x === n ? 1 : -1)
console.log(x,m)
}
return x
};
11.3. 两个数组中完全独立的数据
就是找到仅在两个数组中出现过一次的数据
var a = [1, 2, 4], b = [1, 3, 8, 4]
const newArr = a.concat(b).filter((item, _, arr) => {
return arr.indexOf(item) === arr.lastIndexOf(item)
})
11.4. 31. 下一个排列🔥
1、从后向前 找一个小大的场景
2、将小和其后面所有数 差值最小的数互换
3、除了小那个位置后面的数据按照从小到大重新排序
/**
* @param {number[]} nums
* @return {void} Do not return anything, modify nums in-place instead.
*/
var nextPermutation = function(nums) {
let i = nums.length - 2;
// 从后向前 找一个小大的场景 这个值 index 是 i
while (i >= 0 && nums[i] >= nums[i+1]){
i--
}
const minNum = nums[i]
if (i >= 0){
let j = nums.length - 1;
// 将小和其后面所有数 差值最小的 那个数 互换
while (j >= i && nums[j] <= minNum){
j--
}
// 交换两个数
[nums[j], nums[i]] = [nums[i], nums[j]]
}
// 除了小那个位置后面的数据按照从小到大重新排序
let l = i + 1;
let r = nums.length - 1;
while (l < r){
[nums[l], nums[r]] = [nums[r], nums[l]]
l++
r--
}
}
11.5. 75. 颜色分类
- 一种颜色丢前面 一种颜色丢后面 遍历完就是排序完
var sortColors = function(nums) {
for(let i=0,len=nums.length; i<len; i++) {
nums[i]===0 && (nums.splice(i,1), nums.unshift(0))
nums[i]===2 && (nums.push(2), nums.splice(i,1),len--,i--)
}
return nums
};
12. 栈
12.1. 155. 最小栈🔥
- 使用一个min_stack 存储最小栈,当发现当前入栈数比 最小栈栈顶还小则也入最小栈
- 当弹出时判断是否是最小栈栈顶 是的话也弹出
var MinStack = function() {
this.x_stack = [];
this.min_stack = [Infinity];
};
MinStack.prototype.push = function(x) {
this.x_stack.push(x);
this.min_stack.push(Math.min(this.min_stack[this.min_stack.length - 1], x));
};
MinStack.prototype.pop = function() {
this.x_stack.pop();
this.min_stack.pop();
};
MinStack.prototype.top = function() {
return this.x_stack[this.x_stack.length - 1];
};
MinStack.prototype.getMin = function() {
return this.min_stack[this.min_stack.length - 1];
};
12.2. 84. 柱状图中最大的矩形
var largestRectangleArea = function(heights) {
const n = heights.length;
const left = Array(n).fill(-1);
const st = [];
for (let i = 0; i < n; i++) {
const x = heights[i];
while (st.length && x <= heights[st[st.length - 1]]) {
st.pop();
}
if (st.length) {
left[i] = st[st.length - 1];
}
st.push(i);
}
const right = Array(n).fill(n);
st.length = 0;
for (let i = n - 1; i >= 0; i--) {
const x = heights[i];
while (st.length && x <= heights[st[st.length - 1]]) {
st.pop();
}
if (st.length) {
right[i] = st[st.length - 1];
}
st.push(i);
}
let ans = 0;
for (let i = 0; i < n; i++) {
ans = Math.max(ans, heights[i] * (right[i] - left[i] - 1));
}
return ans;
};
12.3. 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;
};
13. 链表
13.1. 2. 两数相加
- 核心代码
- 每一步都需要
new ListNode()
let result = new ListNode();
let current = result;
return result;
var addTwoNumbers = function (l1, l2) {
let result = new ListNode();
let l3 = result;
let pre = 0;
let val = 0;
while (l1 || l2) {
const l1val = !l1 ? 0 : l1.val;
const l2val = !l2 ? 0 : l2.val;
val = l1val + l2val + (pre || 0);
pre = val > 9 ? 1 : null;
val = val > 9 ? val - 10 : val;
l3.val = val
l1 = l1 ? l1.next : null;
l2 = l2 ? l2.next : null;
if (l1 || l2 || pre) {
l3.next = new ListNode(pre);
l3 = l3.next
}
}
return result
};
14. 堆
14.1. 215. 数组中的第K个最大元素🔥
/*
* @lc app=leetcode.cn id=215 lang=javascript
*
* [215] 数组中的第K个最大元素
*/
/**
* @param {number[]} nums
* @param {number} k
* @return {number}
*/
var findKthLargest = function (nums, k) {
let low = 0
let high = nums.length - 1
while (low <= high) {
const mid = partition(nums, low, high)
if (mid === k - 1) return nums[mid]
mid < k - 1 ? low = mid + 1 : high = mid - 1
}
}
function partition(arr, low, high) {
let mid = Math.floor(low + (high - low) / 2)
const pivot = arr[mid]; // 这里记得添加分号
// 把pivot放在arr的最后面
[arr[mid], arr[high]] = [arr[high], arr[mid]]
let i = low
// 把pivot排除在外,不对pivot进行排序
let j = high - 1
while (i <= j) {
while (arr[i] > pivot) i++
while (arr[j] < pivot) j--
if (i <= j) {
[arr[i], arr[j]] = [arr[j], arr[i]]
i++; j--;
}
}
// 因为arr[i]是属于left的,pivot也是属于left的
// 故我们可以把原本保护起来的pivot和现在数组的中间值交换
[arr[high], arr[i]] = [arr[i], arr[high]]
return i
}
15. 二叉树
15.1. 102. 二叉树的层序遍历
- 第一场循环遍历树 这一层搜集每一层的数据
- 循环遍历节点 手机该层的数据
var levelOrder = function(root) {
if (root === null) return [];
let ans = [];
let cur = [root];
while (cur.length) {
let nxt = []; // 缓存下一次要遍历的下一层
let vals = []; // 当前层的所有值
for (const node of cur) {
vals.push(node.val);
if (node.left) nxt.push(node.left);
if (node.right) nxt.push(node.right);
}
cur = nxt;
ans.push(vals);
}
return ans;
};
16. 图论
16.1. 200. 岛屿数量
var numIslands = function (grid) {
let res = 0;
function dfs(i, j) {
console.log(grid)
grid[i][j] = '0'; // 贪心 先标记为 0 统计一个打沉一个 因为不会影响到下一个计算
if (grid[i + 1] && grid[i + 1][j] === '1') dfs(i + 1, j);
if (grid[i - 1] && grid[i - 1][j] === '1') dfs(i - 1, j);
if (grid[i][j + 1] && grid[i][j + 1] === '1') dfs(i, j + 1);
if (grid[i][j - 1] && grid[i][j - 1] === '1') dfs(i, j - 1);
}
for (let i = 0; i < grid.length; i++) {
for (let j = 0; j < grid[0].length; j++) {
if (grid[i][j] === '1') { // 如果当前是陆地就展开搜索
dfs(i, j);
res++;
}
}
}
return res;
};
17. 矩阵
17.1. 54. 螺旋矩阵
在画图分析后,判断出路线都是有固定方向的 先→再↓再←再↑再→.....一直循环到没数字
因此定义4个方向边界 当触及边界时即按固定方向转向 且其对应的边界值向内收缩1
若没触及边界 即按自身方向继续行走 改变坐标值直到触边界/数字全部遍历过
- 在其方向为右 且未触碰边界值时 列向右走(j++)
- 当触碰时转向
- for 循环用来收集数据的
- 循环中主要来调整 i、j 的指针
/**
* @param {number[][]} matrix
* @return {number[]}
*/
var spiralOrder = function (matrix) {
let count = 0;
let mLength = matrix.length
let nLength = matrix[0].length
const num = mLength * nLength
let m = 0, n = 0;
let arr = []
while (arr.length < num) {
while (n < nLength - count) {
arr.push(matrix[m][n]);
n++
}
n--; m++;
if (arr.length === num) break // 遍历结束
while (m < mLength - count) {
arr.push(matrix[m][n]);
m++
}
if (arr.length === num) break // 遍历结束
m--
n--
while (n >= count) {
arr.push(matrix[m][n]);
n--
}
if (arr.length === num) break // 遍历结束
n = count;
m--
while (m > count) {
arr.push(matrix[m][n]);
m--
}
count++
m = count
n = count
}
return arr
};
17.2. 48. 旋转图像
var rotate = function(matrix) {
let martrixLength = matrix.length
for(let i=0; i < martrixLength; i++) {
for(let j=i; j < martrixLength; j++) {
[matrix[j][i],matrix[i][j]] = [matrix[i][j], matrix[j][i]]
}
}
return matrix.map(item => item.reverse())
};
17.3. 240. 搜索二维矩阵 II
var searchMatrix = function(matrix, target) {
if(matrix.length==0) return false // 判空
let [left, up]=[matrix[0].length-1, 0]; // 初始化位置
while(left>=0 && up<matrix.length){
if(matrix[up][left]>target){
left--;
}else if(matrix[up][left]<target){
up++;
}else{
return true;
}
}
return false;
};