Compare commits

...

41 Commits

Author SHA1 Message Date
762fd95901
feat: 添加回溯算法22,39,52 2025-07-01 21:34:25 +08:00
a88006e4d8
feat: 190,120 2025-06-26 23:36:36 +08:00
efcd1eb12a
feat: 二分查找 153 2025-06-25 23:14:36 +08:00
07e407e943
feat: 一维动态规划70,198,300 2025-06-24 23:33:56 +08:00
1ababd7776
添加动态规划322 2025-06-24 01:31:57 +08:00
329c0f2588
feat: 添加位运算算法 67 2025-06-24 00:27:05 +08:00
5c1a70a60f
feat: 添加动态规划 139 2025-06-22 00:33:39 +08:00
dbcfc3f91a
feat: 添加算法题目35,234,876,739 2025-06-22 00:33:08 +08:00
614b10bcd0
feat: 添加滑动窗口相关算法209,438 2025-06-22 00:31:13 +08:00
09c5a746f1
feat: 添加kadane相关算法53,918 2025-06-22 00:29:25 +08:00
2e1ef51465
feat: 添加回溯算法17,39,77,46 2025-06-22 00:28:30 +08:00
81d4ae749b
feat: 添加二分查找33,34,35,74 2025-06-22 00:26:03 +08:00
21d1a55888
feat: 添加二叉搜索树98,230,530,1382 2025-06-07 16:15:46 +08:00
f53e76dcf0
feat: 图算法 130,133,200,207,210,399,909 2025-06-07 16:11:13 +08:00
0a41e81e58
feat: 分治算法23,10,148,427 2025-06-07 16:08:53 +08:00
7eb11b0136
feat: 添加堆结构相关问题 2025-05-25 21:33:12 +08:00
a5e958c203
feat: 动态规划子序列问题 2025-05-22 17:16:44 +08:00
11b04202d8
feat: 添加二叉树层次遍历102,103,199,637 2025-04-25 22:59:57 +08:00
e7559ec1c7
feat: 二叉树相关题目100,101,104,105,112,114,117,124,129,173,222,226,236 2025-04-25 14:41:15 +08:00
c2c64470b0
feat: 链表操作 2,19,21,25,61,82,86,92,138,141 2025-04-19 18:10:05 +08:00
bd018e32ea
feat: 数据结构栈 20,71,150,155,224 2025-04-19 18:08:45 +08:00
e0ea622d45
feat: 区间操作56,57,228,452 2025-04-19 18:07:59 +08:00
72ac2258de
feat: 哈希表:1,49,128,202,205,219,242,290,383 2025-04-09 22:56:02 +08:00
0d078c09b4
feat: matrix leetcode 36,48,54,73,289 2025-04-04 23:27:40 +08:00
eb9dee4335
feat: double-point 11,15,125,167,392 2025-04-02 23:26:39 +08:00
07a3353880
feat: 双指针125,392 2025-04-01 11:30:29 +08:00
17aed86c56
kmp实现IndexOf 28 2025-04-01 11:20:39 +08:00
371b5ede81
refactor: 移动文件到正确位置 2025-03-29 10:33:36 +08:00
1d8c16c31b
feat: leetcode 6,12,15,58,151 2025-03-29 10:30:38 +08:00
11981edc96
feat: leetcode 13,42 2025-03-27 23:12:40 +08:00
e24153d3db
feat: leetcode 45,55,121,134,135,238,274,380 2025-03-26 23:03:16 +08:00
dfe0072916
feat: 面试常见算法题26,27,80,88,168,189 2025-03-25 16:49:27 +08:00
456e2bbccf
feat: leetcode772,计算器 2025-03-10 19:35:43 +08:00
b2e8377e3d
feat: 汉诺塔问题 2025-03-08 00:50:45 +08:00
40a676cd31
feat: 利用递归排序一个栈 2025-03-07 21:36:05 +08:00
9a4d2d388f
feat: 逆序一个栈 2025-03-07 20:25:12 +08:00
27d3b489e3
feat: 添加栈数据结构 2025-03-07 20:24:37 +08:00
89042e81a8
fix: eslint和prettier冲突 2025-03-07 18:41:49 +08:00
030e04b5d2
feat: 移除元素leetcode25 2025-03-04 22:50:03 +08:00
32033709dc
feat: leetcode 刷题买卖股票最佳时机 2025-03-04 00:36:33 +08:00
eea70ac117
chore: 添加工作区配置文件 2025-03-04 00:35:58 +08:00
151 changed files with 9182 additions and 37 deletions

View File

@ -22,6 +22,7 @@
"no-constant-condition": "off", // while(true)
"default-case": "off", // switchdefault
"no-fallthrough": "off", // case穿
"prefer-destructuring": ["error", {"object": false, "array": false}],
"import/extensions": [
"error",
"ignorePackages", // node_modules

2
.gitignore vendored
View File

@ -25,7 +25,7 @@ pids
yarn-error.log
# Editor directories and files
.vscode
#.vscode
.idea
*.suo
*.ntvs*

12
.vscode/settings.json vendored Normal file
View File

@ -0,0 +1,12 @@
{
"C_Cpp.clang_format_fallbackStyle": "LLVM",
"editor.codeActionsOnSave": {
"source.fixAll.eslint": "always"
},
"editor.defaultFormatter": "esbenp.prettier-vscode", // 使 Prettier
"editor.formatOnSave": true, //
"prettier.requireConfig": true, // Prettier
"eslint.alwaysShowStatus": true, // ESLint
"eslint.format.enable": true // ESLint
}

3
backtrack/README.md Normal file
View File

@ -0,0 +1,3 @@
# 回溯算法
回溯算法就是“递归+循环+剪枝”是一种暴力搜索的方式适合解决组合排列分组SCP(例如n皇后数独等)
绘制决策树更易于我们理解收集路径和回溯的过程。

View File

@ -0,0 +1,54 @@
/**
* @param {string} digits
* @return {string[]}
* https://leetcode.cn/problems/letter-combinations-of-a-phone-number/
*/
const letterCombinations = function (digits) {
};
function f1(digits) {
// 特殊情况处理:如果输入是空字符串,直接返回空数组
if (!digits) return [];
// 数字到字母的映射表(与手机九宫格一致),下标 0 和 1 没有对应字母
const map = [
[], // 0
[], // 1
['a', 'b', 'c'], // 2
['d', 'e', 'f'], // 3
['g', 'h', 'i'], // 4
['j', 'k', 'l'], // 5
['m', 'n', 'o'], // 6
['p', 'q', 'r', 's'], // 7
['t', 'u', 'v'], // 8
['w', 'x', 'y', 'z'], // 9
];
const result = []; // 存放所有可能的组合结果
/**
* 回溯函数
* @param {string[]} path 当前递归路径字符数组
* @param {number} start 当前处理的是 digits 的第几个字符
*/
const backtrack = (path, start) => {
// 递归终止条件:如果 path 长度等于输入数字的长度,说明已经生成一个完整组合
if (start === digits.length) {
result.push(path.join('')); // 把字符数组转成字符串加入结果中
return;
}
// 获取当前数字对应的所有字母(注意把字符转成数字作为索引)
for (const letter of map[+digits[start]]) {
path.push(letter); // 做选择:添加一个字母
backtrack(path, start + 1); // 递归处理下一个数字
path.pop(); // 回溯:撤销上一步选择,尝试其他字母
}
};
// 从空路径、起始位置 0 开始回溯搜索
backtrack([], 0);
return result;
}

View File

@ -0,0 +1,8 @@
/**
* @param {number[]} candidates
* @param {number} target
* @return {number[][]}
*/
const combinationSum = function (candidates, target) {
};

View File

@ -0,0 +1,68 @@
/**
* @param {number} n
* @param {number} k
* @return {number[][]}
* https://leetcode.cn/problems/combinations/
*/
const combine = function (n, k) {
};
/*
利用回溯可以轻松的解决这个问题定义结果esult=[]用来收集结果定义backtrack(path, start)来收集结果和回溯
path用来收集路径
start: 用来标记选择组合元素的起始位置因为组合对顺序没有要求{12}{21}算一个组合避免重复
*/
function f1(n, k) {
const result = []; // 收集符合要求的组合
const backtrack = (path, start) => {
// 如果path的长度符合要求收集结果当path.length === k 时
if (path.length === k) {
result.push([...path]);
return; // 结束这个路径的收集
}
// 从start开始选择元素放入path进行组合
for (let i = start; i <= n; i++) {
// 将当前元素加入组合
path.push(i);
// 递归,组合新的元素
backtrack(path, i + 1);
// 回溯,将之前的组合路径去掉
path.pop();
}
};
backtrack([], 1);
return result;
}
/*
剪枝优化
*/
function f2(n, k) {
const result = []; // 收集符合要求的组合
const backtrack = (path, start) => {
if (path.length + (n - start + 1) < k) return; // 剪枝优化
// 如果path的长度符合要求收集结果当path.length === k 时
if (path.length === k) {
result.push([...path]);
return; // 结束这个路径的收集
}
// 从start开始选择元素放入path进行组合
for (let i = start; i <= n; i++) {
// 将当前元素加入组合
path.push(i);
// 递归,组合新的元素
backtrack(path, i + 1);
// 回溯,将之前的组合路径去掉
path.pop();
}
};
backtrack([], 1);
return result;
}

View File

@ -0,0 +1,36 @@
/**
* @param {number[]} nums
* @param {number} target
* @return {number}
*/
const searchInsert = function (nums, target) {
};
/*
直接使用二分查找二分查找没什么好说的但是这个题目中target不一定存在nums中所以需要设置一个pos遍历来保存target应该插入的位置
我们只需在target < nums[mid]的时候设置一次就行初始化pos的index=nums.length
*/
function f2(nums, target) {
let pos = nums.length;
let left = 0;
let right = nums.length - 1;
// 二分查找知道left>right结束查找
while (left <= right) {
const mid = Math.floor((left + right) / 2);
// 如果target<nums[mid] 则更新pos
if (target === nums[mid]) return mid;
if (target < nums[mid]) {
pos = mid;
right = mid - 1;
continue;
}
left = mid + 1;
}
return pos;
}

View File

@ -0,0 +1,31 @@
/**
* @param {number[]} nums1
* @param {number[]} nums2
* @return {number}
*/
const maxUncrossedLines = function (nums1, nums2) {
};
/*
这个题看似无从下手实际上非常得简单我们从结果入手一个不交叉得最大连线是什么样的经过观察发现就是要我们求
最大公共序列直接把1143的题目拿过来改一下即可
*/
function f1(nums1, nums2) {
const m = nums1.length;
const n = nums2.length;
const dp = Array.from({ length: m + 1 }, () => Array(n + 1).fill(0));
for (let i = m - 1; i >= 0; i--) {
for (let j = n - 1; j >= 0; j--) {
if (nums1[i] === nums2[j]) {
dp[i][j] = dp[i + 1][j + 1] + 1;
} else {
dp[i][j] = Math.max(dp[i + 1][j], dp[i][j + 1]);
}
}
}
return dp[0][0];
}

View File

@ -0,0 +1,30 @@
/**
* @param {string} text1
* @param {string} text2
* @return {number}
*/
const longestCommonSubsequence = function (text1, text2) {
};
/*
定义dp[i][j]为text1从i位置开始统计text2从j位置开始统计的最长公共子序列长度如果text1[i]和text2[j]相等则dp[i][j]=dp[i+1][j+1]
若不相等则dp[i][j] = max(dp[i+1][j], dp[i][j+1])
*/
function f1(text1, text2) {
const m = text1.length;
const n = text2.length;
const dp = Array.from({ length: m + 1 }, () => Array(n + 1).fill(0));
for (let i = m - 1; i >= 0; i--) {
for (let j = n - 1; j >= 0; j--) {
if (text1[i] === text2[j]) {
dp[i][j] = dp[i + 1][j + 1] + 1;
} else {
dp[i][j] = Math.max(dp[i + 1][j], dp[i][j + 1]);
}
}
}
return dp[0][0];
}

View File

@ -0,0 +1,30 @@
/**
* @param {string} s
* @param {string} t
* @return {boolean}
*/
const isSubsequence = function (s, t) {
return f1(s, t);
};
/*
这个题目起始从头到尾遍历开s[1]在不在t中然后再看s[2]在不在最后看s[len - 1]在不在如果在则返回true,
这个题目也可以是使用动态规划动态规划的思路就是如果s[i]在t中存在如果s[i+1:]这个子序列也在t中存在,
那么s[i:]开始的子序列在t中也存在定义dp[i]字符串s从i开始的所有子序列s[i:]在t[j]之后的子序列中存在
s[i] === t[j]
*/
function f2(s, t) {
if (s.length === 0) return true;
if (t.length === 0) return false;
const dp = Array(s.length).fill(false);
for (let j = t.length - 1, i = s.length - 1; j >= 0 && i >= 0; j--) {
if (t[j] === s[i]) {
dp[i] = true;
i--;
}
}
return dp[0];
}

View File

@ -0,0 +1,27 @@
/**
* @param {number[]} nums
* @return {number}
*/
const maxSubArray = function (nums) {
};
/*
定义dp[i]为从nums[i]开始的和最大的子数组那么动态转移方程为dp[i] = dp[i+1] + nums[i] (dp[i+1] > 0)
*/
function f1(nums) {
const dp = Array(nums.length + 1).fill(0); // dp[i]表示从i位置开始的和最大子数组
let result = -Infinity;
for (let i = nums.length - 1; i >= 0; i--) {
if (dp[i + 1] > 0) {
dp[i] = nums[i] + dp[i + 1];
} else {
dp[i] = nums[i];
}
result = Math.max(result, dp[i]);
}
return result;
}

View File

@ -0,0 +1,72 @@
/**
* @param {string} word1
* @param {string} word2
* @return {number}
*/
const minDistance = function (word1, word2) {
};
/*
这个题看似无从下手其实反过来思考非常容易直接求出最长的公共子序列然后步数就是这个两个字符串中多出的那几个字符的和
*/
function f1(word1, word2) {
const commonLen = longestCommonSubsequence(word1, word2);
return word1.length + word2.length - 2 * commonLen;
}
function longestCommonSubsequence(text1, text2) {
const m = text1.length;
const n = text2.length;
const dp = Array.from({ length: m + 1 }, () => Array(n + 1).fill(0));
for (let i = m - 1; i >= 0; i--) {
for (let j = n - 1; j >= 0; j--) {
if (text1[i] === text2[j]) {
dp[i][j] = dp[i + 1][j + 1] + 1;
} else {
dp[i][j] = Math.max(dp[i + 1][j], dp[i][j + 1]);
}
}
}
return dp[0][0];
}
/*
直接利用动态规划来求解定义dp[i][j]为s1中前i个字符和s2中前j个字符变得相同需要删除的字符数量的最少个数第i个字符是s1[i-1]
同理第j个字符是s2[j-1]注意这里的dp[i][j]的定义是前i个字符从1开始数的之所以要这样定义是因为s1可以为空字符需要使用dp[0][0]
来表示空字符
*/
function f2(word1, word2) {
const m = word1.length;
const n = word2.length;
// 定义dp表m+1行n+1列
const dp = Array.from({ length: m + 1 }, () => Array(n + 1).fill(0)); // 初始化成0没有特殊意义反正都要填表
// 初始第一行s1为空字符串那么当s2有多少个字符就取出多少个即dp[0][j] = j
for (let j = 0; j <= n; j++) {
dp[0][j] = j;
}
// 列同理
for (let i = 0; i <= m; i++) {
dp[i][0] = i;
}
// 填充dp表从上到下从左到右
for (let i = 1; i <= m; i++) {
for (let j = 1; j <= n; j++) {
if (word1[i - 1] === word2[j - 1]) { // 如果第i个字符和第j个字符相等
dp[i][j] = dp[i - 1][j - 1];
} else {
dp[i][j] = Math.min(dp[i - 1][j], dp[i][j - 1]) + 1;
}
}
}
return dp[m][n];
}

View File

@ -0,0 +1,37 @@
/**
* @param {number[]} nums1
* @param {number[]} nums2
* @return {number}
*/
const findLength = function (nums1, nums2) {
};
/*
定义dp[i][j]表示nums1从i开始nums2从j开始的最长子数组的长度dp[i][j]可以由两种情况得来第一种当nums1[i] !== nums2[j],
那么从这个位置开始的子数组就不可能是公共子数组多以dp[i][j] = 0;第二种当nums1[i] === nums2[j] 那么dp[i][j] = 1 + dp[i+1][j+1]
得来
*/
function f1(nums1, nums2) {
const m = nums1.length;
const n = nums2.length;
let result = 0;
// 定义dp表并且初始化
const dp = Array.from({ length: m + 1 }, () => Array(n + 1).fill(0));
// 按照dp[i][j]的定义我们需要从下到上从右到左填充dp数组
for (let i = m - 1; i >= 0; i--) {
for (let j = n - 1; j >= 0; j--) {
if (nums1[i] === nums2[j]) {
dp[i][j] = 1 + dp[i + 1][j + 1];
result = Math.max(result, dp[i][j]);
} else {
dp[i][j] = 0;
}
}
}
console.log(dp);
return result;
}
f1([0, 0, 0, 0, 1], [0, 0, 0, 0, 1]);

View File

@ -0,0 +1,48 @@
/**
* @param {string} word1
* @param {string} word2
* @return {number}
*/
const minDistance = function (word1, word2) {
};
/*
有删除添加修改三种操作word1的删除和word2的添加效果是一样的所以实际上只有三种操作
1. 往word1中删除一个字符
2. 往word2中删除一个字符
3. 修改word1中的一个字符
定义dp[i][j]表示word1的前i个字符和word2的前j个字符的最少编辑距离那么在已知dp[i-1][j-1]和dp[i-1][j],dp[i][j-1]的情况下推出来
如果第word1的第i个字符和word2的第j个字符相等那么最小编辑距离就是dp[i-1][j-1]因为第i个数和第j个字符我不需要操作而剩下的这些字符
最少编辑距离就是dp[i-1][j-1],如果这word1的第i个字符和word2的第j个字符不相等我们可以删除word1的第i个字符然后将word1的前i-1个字符
和word2的前j个字符变得一样所需得编辑距离加上删除word1得第i个字符这一步一共是dp[i-1][j] + 1个编辑距离同理dp[i][j-1]得原理一样
最后还有一种获得最优得情况是我不删除只是将word1中得最后一个字符修改成和word2中卒子后一个字符一样这样就变成word1[i-1]==word2[j-1]
相等得情况了
*/
function f1(word1, word2) {
const m = word1.length;
const n = word2.length;
const dp = Array.from({ length: m + 1 }, () => Array(n + 1).fill(0)); // 初始化成0无意义只是初始化而已
// 初始化如果word1为空word2要想变得和word1一样编辑距离就是它自身长度
for (let j = 0; j <= n; j++) {
dp[0][j] = j;
}
// word2为空同理
for (let i = 0; i <= m; i++) {
dp[i][0] = i;
}
// 填dp表,dp[i][j] 依赖dp[i-1][j-1], dp[i-1][j], dp[i][j-1],所以要从上到下,从左到右遍历
for (let i = 1; i <= m; i++) {
for (let j = 1; j <= n; j++) {
if (word1[i - 1] === word2[j - 1]) {
dp[i][j] = dp[i - 1][j - 1];
} else {
dp[i][j] = Math.min(dp[i - 1][j], dp[i][j - 1], dp[i - 1][j - 1]) + 1;
}
}
}
return dp[m][n];
}

View File

@ -0,0 +1,41 @@
/**
* @param {number[]} nums
* @param {number} k
* @return {number}
*/
const findKthLargest = function (nums, k) {
// 将数组的前k个元素作为堆然后将其堆化
const heap = nums.slice(0, k);
// 从最后一个非叶子节点遍历,自底向上下沉堆化
for (let i = Math.floor(k / 2 - 1); i >= 0; i--) {
siftDown(heap, i, k);
}
// 处理剩余元素
for (let i = k; i < nums.length; i++) {
// 如果剩余元素大于堆顶元素,替换然后下沉维护堆
if (nums[i] > heap[0]) {
heap[0] = nums[i];
siftDown(heap, 0, k);
}
}
return heap[0];
};
// 下沉函数(维护堆性质)
function siftDown(heap, start, heapSize) {
let cur = start;
while (true) {
const left = 2 * cur + 1;
const right = 2 * cur + 2;
let smallest = cur;
if (left < heapSize && heap[left] < heap[smallest]) smallest = left;
if (right < heapSize && heap[right] < heap[smallest]) smallest = right;
if (smallest === cur) break;
[heap[cur], heap[smallest]] = [heap[smallest], heap[cur]];
cur = smallest;
}
}

131
heap/base/topk问题.js Normal file
View File

@ -0,0 +1,131 @@
/*
topk 问题是一个经典的问题我们维持一个大小为k的堆如果要求最大的第三个数我们就维持小顶堆反之维持大小为k的大顶堆
为什么这有点反直觉为什么求第k大的数要维持小顶堆而不是大顶堆这是因为第k大的数是数组中k个最大的数中最小的那一个
用小顶堆来维持那么要求的第k个最大的数刚好在小顶堆的堆顶
*/
const numbers = [5, 0, 8, 2, 1, 4, 7, 2, 5];
/**
*
* @param {number[]} nums
* @param {number} k
*/
function topK(nums, k) {
const heap = Array(k).fill(-Infinity); // 定义一个大小为k数组用来维护堆
for (let i = 0; i < nums.length; i++) {
// 如果当前元素大于堆顶元素,替换堆顶元素然后下沉调整堆
if (nums[i] > heap[0]) {
heap[0] = nums[i];
let cur = 0; // cur指向下沉元素的下标,一开始就在堆顶
while (true) {
const left = cur * 2 + 1;
const right = cur * 2 + 2;
let smallest = cur;
if (left < k && heap[left] < heap[smallest]) smallest = left;
if (right < k && heap[right] < heap[smallest]) smallest = right;
// 如果smallest 和cur相等表示当前元素来到了合适的位置cur结束下沉
if (smallest === cur) break;
// 交换位置下沉
[heap[cur], heap[smallest]] = [heap[smallest], heap[cur]];
cur = smallest;
}
}
}
// 返回堆顶元素
return heap[0];
}
/**
* 使用小顶堆找出数组中第 K 大的数
* @param {number[]} nums
* @param {number} k
* @returns {number}
*/
function topKbest(nums, k) {
const heap = nums.slice(0, k); // 先取前 k 个元素构建小顶堆
// 下沉函数(维护堆性质)
function siftDown(heap, start, heapSize) {
let cur = start;
while (true) {
const left = 2 * cur + 1;
const right = 2 * cur + 2;
let smallest = cur;
if (left < heapSize && heap[left] < heap[smallest]) smallest = left;
if (right < heapSize && heap[right] < heap[smallest]) smallest = right;
if (smallest === cur) break;
[heap[cur], heap[smallest]] = [heap[smallest], heap[cur]];
cur = smallest;
}
}
// 构建小顶堆(自底向上建堆)
for (let i = Math.floor(k / 2) - 1; i >= 0; i--) {
siftDown(heap, i, k);
}
// 遍历剩余元素,维护一个大小为 k 的小顶堆
for (let i = k; i < nums.length; i++) {
if (nums[i] > heap[0]) {
heap[0] = nums[i];
siftDown(heap, 0, k);
}
}
// 堆顶即为第 k 大元素
return heap[0];
}
/*
若题目没有要求动态的维护这个topk,那么可以使用quick-select来实现O(n)的时间复杂度完成这个题目
思路题目要求我们找出第k大的数那么当整个数组排序之后倒数第k个数不就是我们要求的吗比如[5,4,3,2,1] k = 2,那么排序后[1,2,3,4,5]
倒数第二个数的下标就是nums.length - k = 5 - 2 = 3 nums[3]恰好就是我们要找的这个数我们没有必要对整个数组排序我们只要找到排序后这个
数正确的位置即可知道快速排序的话我们可以使用partition来确定pivot,pivot在partition之后就已经确认如果pivot的下标等于倒数第k个位置
那么pivot就是排序之后的倒数第k个数也就是topk
*/
function f2(nums, k) {
return quickSelect(nums, 0, nums.length - 1, nums.length - k);
}
function quickSelect(nums, left, right, n) {
const p = partition(nums, left, right);
if (p === n) return nums[n];
return p < n ? quickSelect(nums, p + 1, right, n) : quickSelect(nums, left, p - 1, n);
}
/**
* 分区函数返回pivotpivot 左边的数都不大于右边的数都不小于它
* @param {*} nums 要操作的数组
* @param {*} left 开始位置左边界
* @param {*} right 结束位置, 右边界
* @returns number 返回的piivot
*/
function partition(arr, left, right) {
const pivot = arr[right];
let p = left;
for (let i = left; i < right; i++) {
if (arr[i] <= pivot) {
// [arr[i], arr[p]] = [arr[p], arr[i]];
swap(arr, i, p);
p++;
}
}
// [arr[p], arr[right]] = [arr[right], arr[p]];
swap(arr, p, right);
return p;
}
function swap(nums, a, b) {
const tmp = nums[a];
nums[a] = nums[b];
nums[b] = tmp;
}

75
heap/base/原地构建.js Normal file
View File

@ -0,0 +1,75 @@
const numbers = [5, 0, 8, 2, 1, 4, 7, 2, 5, -1]; // 测试用例
const vaild = [-1, 0, 4, 2, 1, 8, 7, 5, 5, 2]; // 验证用例
/**
* 自底向上下沉
* @param {*} nums
*/
function buildHeap(nums) {
const n = nums.length;
// 寻找最后一个非叶子节点,从这里开始倒序遍历(自底向上)
for (let i = Math.floor(n / 2 - 1); i >= 0; i--) {
// 取当前节点左右节点较小的
let min = i * 2 + 1;
if (i * 2 + 2 < n && nums[i * 2 + 2] < nums[min]) min = i * 2 + 2;
let cur = i; // 表示当前节点下沉的位置
// 如果父节点大于子节点中较小的哪一个那么就交换位置,如果交换位置之后还是大于当前位置的子节点小的那个,继续下沉
while (min < n && nums[cur] > nums[min]) {
[nums[cur], nums[min]] = [nums[min], nums[cur]];
cur = min;
min = cur * 2 + 1;
if (cur * 2 + 2 < n && nums[cur * 2 + 2] < nums[min]) min = cur * 2 + 2;
}
}
}
/**
* 原地构建小顶堆heapify使用下沉sift down
* @param {number[]} nums 完全二叉树的层序遍历原始数组
* @returns {number[]} 构建完成的小顶堆原地修改 nums 并返回
*/
function buildHeapBest(nums) {
const n = nums.length;
// 从最后一个非叶子节点开始,依次向前,对每个节点执行下沉
for (let i = Math.floor(n / 2) - 1; i >= 0; i--) {
siftDown(nums, i, n);
}
return nums;
}
/**
* nums[i] 进行下沉调整确保以 i 为根的子树满足小顶堆性质
* @param {number[]} nums 堆数组
* @param {number} i 当前要下沉的节点索引
* @param {number} n 数组长度限制范围yiw
*/
function siftDown(nums, i, n) {
let cur = i;
while (true) {
const left = 2 * cur + 1;
const right = 2 * cur + 2;
let smallest = cur;
// 与左孩子比较
if (left < n && nums[left] < nums[smallest]) {
smallest = left;
}
// 与右孩子比较
if (right < n && nums[right] < nums[smallest]) {
smallest = right;
}
// 如果当前节点已经是最小的,堆性质满足,退出循环
if (smallest === cur) break;
// 否则交换并继续下沉
[nums[cur], nums[smallest]] = [nums[smallest], nums[cur]];
cur = smallest;
}
}

35
heap/base/逐个插入.js Normal file
View File

@ -0,0 +1,35 @@
const numbers = [5, 0, 8, 2, 1, 4, 7, 2, 5, -1]; // 测试用例
const vaild = [-1, 0, 4, 2, 1, 8, 7, 5, 5, 2]; // 验证用例
/**
* 使用逐个插入法构造小顶堆
* @param {number[]} nums 需要处理的完全二叉树用数组表示层序遍历
* @returns {number[]} 返回一个小顶堆的层序遍历
*/
function buildHeap(nums) {
const heap = [];
for (let i = 0; i < nums.length; i++) {
heap.push(nums[i]);
let cur = heap.length - 1;
let parent = Math.floor((cur - 1) / 2);
while (cur > 0 && heap[cur] < heap[parent]) {
[heap[cur], heap[parent]] = [heap[parent], heap[cur]];
cur = parent;
parent = Math.floor((cur - 1) / 2);
}
}
return heap;
}
function test(nums1, nums2) {
for (let i = 0; i < nums1; i++) {
if (nums1[i] !== nums2[i]) return false;
}
return true;
}
console.log(test(buildeap(numbers), vaild));

View File

@ -0,0 +1,37 @@
/*
给定一个数组 prices 它的第 i 个元素 prices[i] 表示一支给定股票第 i 天的价格
你只能选择 某一天 买入这只股票并选择在 未来的某一个不同的日子 卖出该股票设计一个算法来计算你所能获取的最大利润
返回你可以从这笔交易中获取的最大利润如果你不能获取任何利润返回 0
示例 1
输入[7,1,5,3,6,4]
输出5
解释在第 2 股票价格 = 1的时候买入在第 5 股票价格 = 6的时候卖出最大利润 = 6-1 = 5
注意利润不能是 7-1 = 6, 因为卖出价格需要大于买入价格同时你不能在买入前卖出股票
*/
/**
* @param {number[]} prices
* @return {number}
* 思路先设置一个变量minPrice 用于保存数组中最小的价格初始值为无穷大依次遍历数组如果有元素比他小那么就
* 替换为当前元素如果遍历的元素比他还大那就说明在这一天是可以卖掉的但是利润不一定是最大的多以我们还需要
* 一个全局变量 maxProfit来表示例如初始值为0如果这一天卖掉的利润比之前的最大利润大就可以替换为当前利润
* 最后返回当前利润
*/
function calculateProfit(prices) {
let minPrice = Infinity;
let maxProfit = 0;
for (const price of prices) {
if (price < minPrice) {
minPrice = price;
} else if (price - minPrice > maxProfit) {
maxProfit = price - minPrice;
}
}
return maxProfit;
}
console.log(calculateProfit([5, 2, 1, 34, 55]));

View File

@ -0,0 +1,65 @@
/*
给你一个数组 nums 和一个值 val你需要 原地 移除所有数值等于 val 的元素元素的顺序可能发生改变然后返回 nums 中与 val 不同的元素的数量
假设 nums 中不等于 val 的元素数量为 k要通过此题您需要执行以下操作
更改 nums 数组使 nums 的前 k 个元素包含不等于 val 的元素nums 的其余元素和 nums 的大小并不重要
返回 k
*/
// 示例 1
// 输入nums = [3,2,2,3], val = 3
// 输出2, nums = [2,2,_,_]
// 解释:你的函数函数应该返回 k = 2, 并且 nums 中的前两个元素均为 2。
// 你在返回的 k 个元素之外留下了什么并不重要(因此它们并不计入评测)。
/**
* @param {number[]} nums
* @param {number} val
* @return {number}
* 思路题目只需要我们修改元素组并且保证元素组的前k个元素是于val不同即可脑袋里面想到的第一个思路就是遍历一遍把
* 这k个与val不同的元素找出来存入一个新的数组之后再遍历新数组把这k个元素替换到元素组
*/
const removeElement1 = function (nums, val) {
const arr = [];
let k = 0;
for (const num of nums) {
if (num !== val) {
arr.push(num);
k++;
}
}
for (let i = 0; i < arr.length; i++) {
nums[i] = arr[i];
}
return k;
};
/**
*
* @param {number[]} nums
* @param {number} val
* @returns number
* 思路使用快慢指针快指针遍历整个数组找出与val不同的元素快慢指针初始都指向第一个元素当快指针找到
* 与val不同的元素的时候就把当前元素的值赋值给慢指针的位置之后慢指针往后移动一位重复上一次操作
*
* 思考因为快指针会优先移动去寻找与val不同的元素所以一次遍历下来能找到所有的与val不同的元素当找到时slow所在
* 的位置一定是无效位置要么时与val值相同的位置要么是已经赋值之后的无效位置所以不用担心值被覆盖的问题
*/
function removeElement(nums, val) {
let slow = 0; // 慢指针,指向不等于 val 的部分末尾
for (let fast = 0; fast < nums.length; fast++) {
if (nums[fast] !== val) {
nums[slow] = nums[fast];
slow++;
}
}
return slow; // 新数组长度
}
const testArr = [3, 2, 2, 3];
removeElement(testArr, 3);
console.log(testArr);

View File

@ -0,0 +1,40 @@
/**
* 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 {boolean}
*/
const isPalindrome = function (head) {
};
/*
利用栈将链表的值存储起来然后再从头开始遍历链表比较每个节点的值和栈顶的值是否相等
利用了回文的定义正着和倒着应该是一样的
*/
function f1(head) {
const stack = [];
let current = head;
// 第一步:将所有节点的值压入栈中
while (current) {
stack.push(current.val);
current = current.next;
}
// 第二步:从头开始重新遍历链表,同时与栈顶比较
current = head;
while (current) {
if (current.val !== stack.pop()) {
return false;
}
current = current.next;
}
return true;
}

View File

@ -0,0 +1,32 @@
/**
* 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}
*/
const middleNode = function (head) {
};
function ListNode(val, next) {
this.val = (val === undefined ? 0 : val);
this.next = (next === undefined ? null : next);
}
/*
如果利用快慢指针寻找重点如果有两个中点返回后面那个直接利用靠右的判断方法 fast && fast.next
*/
function f1(head) {
let slow = head;
let fast = head;
while (fast && fast.next) {
slow = slow.next;
fast = fast.next.next;
}
return slow;
}

View File

@ -1 +1 @@
export * from './link-list.js'
export * from './link-list.js';

View File

@ -0,0 +1,86 @@
/**
* @param {number[]} temperatures
* @return {number[]}
*/
const dailyTemperatures = function (temperatures) {
};
/*
直接利用单调栈单调栈中存入的是元素的下标这样不仅能比较元素还能通过下标设置结果
*/
function f1(temperatures) {
const res = []; // 收集结果
const stack = []; // 单调递减栈,寻找右边第一个比栈顶元素大的元素
for (let i = 0; i < temperatures.length; i++) {
// 如果栈为空或者当前元素小于等于栈顶元素,将这个元素的下标压入栈中
if (!stack.length || temperatures[i] <= temperatures[stack[stack.length - 1]]) {
stack.push(i);
} else {
// 依次比较栈顶元素,直到栈为空,或者当前元素小于等于栈顶元素
while (stack.length && temperatures[i] > temperatures[stack[stack.length - 1]]) {
const cur = stack.pop();
res[cur] = i - cur;
}
// 将当前元素下标压入栈中
stack.push(i);
}
}
// 如果栈中还有元素将其在result中的对应位置设置从0表示后面没有元素大于当前位置的元素
for (const i of stack) {
res[i] = 0;
}
return res;
}
/*
chatgpt 优化之后的写法思路没有变去除了if判断逻辑如下
遍历整个温度如果单调栈有元素并且当前元素大于栈顶元素就一直处理直到栈中没有元素或者当前元素小于等于栈顶元素
然后将当前元素压入栈中
*/
/**
* 计算每一天需要等待多少天才会有更高温度
* 如果后面没有更高的温度则返回 0
*
* @param {number[]} temperatures - 每天的温度数组
* @returns {number[]} - 返回一个数组表示每一天距离下一次更高温度的天数
*/
function f2(temperatures) {
// 初始化结果数组,默认所有值为 0表示找不到更高温度
const res = new Array(temperatures.length).fill(0);
// 用来存储下标的单调递减栈(栈顶到栈底的温度依次递减)
const stack = [];
// 遍历温度数组
for (let i = 0; i < temperatures.length; i++) {
const currentTemp = temperatures[i];
/**
* 当前温度比栈顶下标对应的温度大说明当前是更高温度的一天
* 所以可以开始出栈并记录距离
*
* 注意每个元素最多被 push pop 各一次所以总体时间复杂度是 O(n)
*/
while (
stack.length > 0
&& currentTemp > temperatures[stack[stack.length - 1]]
) {
// 栈顶元素下标
const prevIndex = stack.pop();
// 当前天i就是之前那天prevIndex等待的更高温度的那一天
res[prevIndex] = i - prevIndex;
}
// 无论如何都要把当前下标压栈,作为之后判断的基准
stack.push(i);
}
// 栈中剩下的下标所对应的位置已经默认填 0不需要再处理
return res;
}

79
package-lock.json generated
View File

@ -9,11 +9,12 @@
"version": "1.0.0",
"license": "ISC",
"devDependencies": {
"eslint": "^8.57.0",
"eslint": "^8.57.1",
"eslint-config-airbnb-base": "^15.0.0",
"eslint-config-prettier": "^9.1.0",
"eslint-plugin-import": "^2.29.1",
"eslint-plugin-prettier": "^5.1.3"
"eslint-plugin-prettier": "^5.2.3",
"prettier": "^3.5.3"
}
},
"node_modules/@aashutoshrathi/word-wrap": {
@ -73,21 +74,24 @@
}
},
"node_modules/@eslint/js": {
"version": "8.57.0",
"resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.57.0.tgz",
"integrity": "sha512-Ys+3g2TaW7gADOJzPt83SJtCDhMjndcDMFVQ/Tj9iA1BfJzFKD9mAUXT3OenpuPHbI6P/myECxRJrofUsDx/5g==",
"version": "8.57.1",
"resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.57.1.tgz",
"integrity": "sha512-d9zaMRSTIKDLhctzH12MtXvJKSSUhaHcjV+2Z+GK+EEY7XKpP5yR4x+N3TAcHTcu963nIr+TMcCb4DBCYX1z6Q==",
"dev": true,
"license": "MIT",
"engines": {
"node": "^12.22.0 || ^14.17.0 || >=16.0.0"
}
},
"node_modules/@humanwhocodes/config-array": {
"version": "0.11.14",
"resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.14.tgz",
"integrity": "sha512-3T8LkOmg45BV5FICb15QQMsyUSWrQ8AygVfC7ZG32zOalnqrilm018ZVCw0eapXux8FtA33q8PSRSstjee3jSg==",
"version": "0.13.0",
"resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.13.0.tgz",
"integrity": "sha512-DZLEEqFWQFiyK6h5YIeynKx7JlvCYWL0cImfSRXZ9l4Sg2efkFGTuFf6vzXjK1cq6IYkU+Eg/JizXw+TD2vRNw==",
"deprecated": "Use @eslint/config-array instead",
"dev": true,
"license": "Apache-2.0",
"dependencies": {
"@humanwhocodes/object-schema": "^2.0.2",
"@humanwhocodes/object-schema": "^2.0.3",
"debug": "^4.3.1",
"minimatch": "^3.0.5"
},
@ -109,10 +113,12 @@
}
},
"node_modules/@humanwhocodes/object-schema": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-2.0.2.tgz",
"integrity": "sha512-6EwiSjwWYP7pTckG6I5eyFANjPhmPjUX9JRLUSfNPC7FX7zK9gyZAfUEaECL6ALTpGX5AjnBq3C9XmVWPitNpw==",
"dev": true
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-2.0.3.tgz",
"integrity": "sha512-93zYdMES/c1D69yZiKDBj0V24vqNzB/koF26KPaagAfd3P/4gUlh3Dys5ogAK+Exi9QyzlD8x/08Zt7wIKcDcA==",
"deprecated": "Use @eslint/object-schema instead",
"dev": true,
"license": "BSD-3-Clause"
},
"node_modules/@nodelib/fs.scandir": {
"version": "2.1.5",
@ -154,6 +160,7 @@
"resolved": "https://registry.npmjs.org/@pkgr/core/-/core-0.1.1.tgz",
"integrity": "sha512-cq8o4cWH0ibXh9VGi5P20Tu9XF/0fFXl9EUinr9QfTM7a7p0oTA4iJRCQWppXR1Pg8dSM0UCItCkPwsk9qWWYA==",
"dev": true,
"license": "MIT",
"engines": {
"node": "^12.20.0 || ^14.18.0 || >=16.0.0"
},
@ -694,16 +701,18 @@
}
},
"node_modules/eslint": {
"version": "8.57.0",
"resolved": "https://registry.npmjs.org/eslint/-/eslint-8.57.0.tgz",
"integrity": "sha512-dZ6+mexnaTIbSBZWgou51U6OmzIhYM2VcNdtiTtI7qPNZm35Akpr0f6vtw3w1Kmn5PYo+tZVfh13WrhpS6oLqQ==",
"version": "8.57.1",
"resolved": "https://registry.npmjs.org/eslint/-/eslint-8.57.1.tgz",
"integrity": "sha512-ypowyDxpVSYpkXr9WPv2PAZCtNip1Mv5KTW0SCurXv/9iOpcrH9PaqUElksqEB6pChqHGDRCFTyrZlGhnLNGiA==",
"deprecated": "This version is no longer supported. Please see https://eslint.org/version-support for other options.",
"dev": true,
"license": "MIT",
"dependencies": {
"@eslint-community/eslint-utils": "^4.2.0",
"@eslint-community/regexpp": "^4.6.1",
"@eslint/eslintrc": "^2.1.4",
"@eslint/js": "8.57.0",
"@humanwhocodes/config-array": "^0.11.14",
"@eslint/js": "8.57.1",
"@humanwhocodes/config-array": "^0.13.0",
"@humanwhocodes/module-importer": "^1.0.1",
"@nodelib/fs.walk": "^1.2.8",
"@ungap/structured-clone": "^1.2.0",
@ -772,6 +781,7 @@
"resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-9.1.0.tgz",
"integrity": "sha512-NSWl5BFQWEPi1j4TjVNItzYV7dZXZ+wP6I6ZhrBGpChQhZRUaElihE9uRRkcbRnNb76UMKDF3r+WTmNcGPKsqw==",
"dev": true,
"license": "MIT",
"bin": {
"eslint-config-prettier": "bin/cli.js"
},
@ -878,13 +888,14 @@
}
},
"node_modules/eslint-plugin-prettier": {
"version": "5.1.3",
"resolved": "https://registry.npmjs.org/eslint-plugin-prettier/-/eslint-plugin-prettier-5.1.3.tgz",
"integrity": "sha512-C9GCVAs4Eq7ZC/XFQHITLiHJxQngdtraXaM+LoUFoFp/lHNl2Zn8f3WQbe9HvTBBQ9YnKFB0/2Ajdqwo5D1EAw==",
"version": "5.2.3",
"resolved": "https://registry.npmjs.org/eslint-plugin-prettier/-/eslint-plugin-prettier-5.2.3.tgz",
"integrity": "sha512-qJ+y0FfCp/mQYQ/vWQ3s7eUlFEL4PyKfAJxsnYTJ4YT73nsJBWqmEpFryxV9OeUiqmsTsYJ5Y+KDNaeP31wrRw==",
"dev": true,
"license": "MIT",
"dependencies": {
"prettier-linter-helpers": "^1.0.0",
"synckit": "^0.8.6"
"synckit": "^0.9.1"
},
"engines": {
"node": "^14.18.0 || >=16.0.0"
@ -1962,11 +1973,11 @@
}
},
"node_modules/prettier": {
"version": "3.2.5",
"resolved": "https://registry.npmjs.org/prettier/-/prettier-3.2.5.tgz",
"integrity": "sha512-3/GWa9aOC0YeD7LUfvOG2NiDyhOWRvt1k+rcKhOuYnMY24iiCphgneUfJDyFXd6rZCAnuLBv6UeAULtrhT/F4A==",
"version": "3.5.3",
"resolved": "https://registry.npmjs.org/prettier/-/prettier-3.5.3.tgz",
"integrity": "sha512-QQtaxnoDJeAkDvDKWCLiwIXkTgRhwYDEQCghU9Z6q03iyek/rxRh/2lC3HB7P8sWT2xC/y5JDctPLBIGzHKbhw==",
"dev": true,
"peer": true,
"license": "MIT",
"bin": {
"prettier": "bin/prettier.cjs"
},
@ -2328,10 +2339,11 @@
}
},
"node_modules/synckit": {
"version": "0.8.8",
"resolved": "https://registry.npmjs.org/synckit/-/synckit-0.8.8.tgz",
"integrity": "sha512-HwOKAP7Wc5aRGYdKH+dw0PRRpbO841v2DENBtjnR5HFWoiNByAl7vrx3p0G/rCyYXQsrxqtX48TImFtPcIHSpQ==",
"version": "0.9.2",
"resolved": "https://registry.npmjs.org/synckit/-/synckit-0.9.2.tgz",
"integrity": "sha512-vrozgXDQwYO72vHjUb/HnFbQx1exDjoKzqx23aXEg2a9VIg2TSFZ8FmeZpTjUCFMYw7mpX4BE2SFu8wI7asYsw==",
"dev": true,
"license": "MIT",
"dependencies": {
"@pkgr/core": "^0.1.0",
"tslib": "^2.6.2"
@ -2362,10 +2374,11 @@
}
},
"node_modules/tslib": {
"version": "2.6.2",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz",
"integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==",
"dev": true
"version": "2.8.1",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
"integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
"dev": true,
"license": "0BSD"
},
"node_modules/type-check": {
"version": "0.4.0",

View File

@ -21,10 +21,11 @@
"author": "yigencong",
"license": "ISC",
"devDependencies": {
"eslint": "^8.57.0",
"eslint": "^8.57.1",
"eslint-config-airbnb-base": "^15.0.0",
"eslint-config-prettier": "^9.1.0",
"eslint-plugin-import": "^2.29.1",
"eslint-plugin-prettier": "^5.1.3"
"eslint-plugin-prettier": "^5.2.3",
"prettier": "^3.5.3"
}
}

86
recursion/basic-cal.js Normal file
View File

@ -0,0 +1,86 @@
/**
* LeetCode 772 - Basic Calculator III
* Evaluates a string expression containing:
* - Numbers (can be multi-digit)
* - +, -, *, /
* - Parentheses
* - Spaces
*
* @param {string} s - The expression to evaluate
* @return {number} - The result of the expression
*/
function calculate(s) {
let i = 0;
// 解析表达式
function parseExpression() {
const stack = [];
let num = 0;
let sign = '+';
while (i < s.length) {
const char = s[i];
if (char >= '0' && char <= '9') {
// 解析多位数字
num = num * 10 + parseInt(char, 10);
} else if (char === '(') {
// 遇到左括号,获取整个括号的结果,把整个括号看成一个数
i++; // Skip the '('
num = parseExpression(); // 把整个括号里面的内容看成一个表达式,从括号后一个字符开始处理
} else if (char === ')') {
// 遇到右括号说明此ParseExpression处理完成返回整个括号的计算结果给上一个调用
updateStack(stack, sign, num);
return calculateStack(stack);
} else if (char === '+' || char === '-' || char === '*' || char === '/') {
// 遇到操作需要把处理好的num压入栈中
updateStack(stack, sign, num);
sign = char;
num = 0;
}
i++;
}
// 将最后一个数字加入栈中
updateStack(stack, sign, num);
// 上面的递归解析过程结束之后会得到一个只包含数字的栈,求和即可
return calculateStack(stack);
}
/**
*
* @param {number[]} stack 收集数字的栈
* @param {*} sign - 数字之前的符号
* @param {*} num - 要处理的数字
* @description 如果数字的前面是乘除法就把栈顶元素弹出计算过后作为整体压入栈中是减法就把这个数改成负数加法就直接压入栈中
*/
function updateStack(stack, sign, num) {
if (sign === '+') {
stack.push(num);
} else if (sign === '-') {
stack.push(-num);
} else if (sign === '*') {
stack.push(stack.pop() * num);
} else if (sign === '/') {
// 数字前面的操作符如果是除号,需要向下取整
const prev = stack.pop();
const result = prev / num;
stack.push(prev > 0 ? Math.floor(result) : Math.ceil(result));
}
}
// 对整个栈求和
function calculateStack(stack) {
return stack.reduce((sum, num) => sum + num, 0);
}
return parseExpression();
}
// 测试用例
console.log(calculate('1+1')); // 2
console.log(calculate('6-4/2')); // 4
console.log(calculate('2*(5+5*2)/3+(6/2+8)')); // 21
console.log(calculate('(2+6*3+5-(3*14/7+2)*5)+3')); // -12

103
recursion/n皇后.js Normal file
View File

@ -0,0 +1,103 @@
/**
* 解决N皇后问题
* @param {number} n - 棋盘大小和皇后数量
* @return {string[][]} - 所有可能的解决方案
*/
function solveNQueens(n) {
const result = [];
// 创建一个表示棋盘的数组,初始化为空
const board = Array(n).fill().map(() => Array(n).fill('.'));
// 开始回溯
backtrack(0);
return result;
/**
* 回溯函数尝试在每一行放置皇后
* @param {number} row - 当前处理的行
*/
function backtrack(row) {
// 如果已经放置了n个皇后找到一个解决方案
if (row === n) {
// 将当前棋盘状态转换为所需的格式并添加到结果中
const solution = board.map((row) => row.join(''));
result.push(solution);
return;
}
// 尝试在当前行的每一列放置皇后
for (let col = 0; col < n; col++) {
// 检查当前位置是否可以放置皇后
if (isValid(row, col)) {
// 放置皇后
board[row][col] = 'Q';
// 递归到下一行
backtrack(row + 1);
// 回溯,移除皇后
board[row][col] = '.';
}
}
}
/**
* 检查在[row, col]位置放置皇后是否有效
* @param {number} row - 行索引
* @param {number} col - 列索引
* @return {boolean} - 如果位置有效返回true否则返回false
*/
function isValid(row, col) {
// 检查同一列是否有皇后
for (let i = 0; i < row; i++) {
if (board[i][col] === 'Q') {
return false;
}
}
// 检查左上对角线是否有皇后
for (let i = row - 1, j = col - 1; i >= 0 && j >= 0; i--, j--) {
if (board[i][j] === 'Q') {
return false;
}
}
// 检查右上对角线是否有皇后
for (let i = row - 1, j = col + 1; i >= 0 && j < n; i--, j++) {
if (board[i][j] === 'Q') {
return false;
}
}
return true;
}
}
/**
* 打印棋盘
* @param {string[]} board - 表示棋盘的字符串数组
*/
function printBoard(board) {
for (const row of board) {
console.log(row);
}
console.log('---');
}
// 测试代码
function testNQueens(n) {
console.log(`求解${n}皇后问题:`);
const solutions = solveNQueens(n);
console.log(`找到${solutions.length}个解决方案`);
// 打印前3个解决方案
for (let i = 0; i < Math.min(3, solutions.length); i++) {
console.log(`解决方案 ${i + 1}:`);
printBoard(solutions[i]);
}
}
// 测试8皇后问题
testNQueens(8);

3
recursion/recursion.md Normal file
View File

@ -0,0 +1,3 @@
# 递归
此目录存放了常用递归思路,以及一些解决方案

View File

@ -0,0 +1,43 @@
/**
有三根柱子分别命名为 ABCA 上有若干个盘子盘子的大小不等且每个盘子都可以放在另一根柱子上要求从柱子 A 上将所有盘子移动到柱子 C 且必须遵守以下规则
一次只能移动一个盘子
每次移动时盘子只能从柱子顶部移动且一个盘子不能放在比它小的盘子上
目标是使用柱子 B 作为辅助柱子最少移动次数将盘子从柱子 A 移动到柱子 C
*/
/**
*
* @param {number} n 汉诺塔问题的规模
* 思路假设我们只有两个盘子只需要把小盘子移动到B柱上再把大盘子移动到C柱子上最后把小盘子从B柱移动到C柱子我们把问题做一个抽象假设有三个盘子因为要想移动
* 大盘子就必须把它上面的所有小盘子移动到B住上去之后才能把这个大盘子移动到C柱至于怎么移动我不管
*
* 分析假设现在有三个盘子第一步我们发现底部的盘子无法直接移动把问题抽象为移动n-1个盘子在这里就是移动上面的两个盘子之后会发现第二个盘子也移不动所以再把问题抽象为
* (n-1)-1,也就是移动第一个盘子第一个盘子可以直接移动这样就可以把第一个盘子移动到B上第二个盘子移动到C,......
*/
function towerOfHanoi(n) {
// 将n个盘子从A借助B移动到C
howToMove(n, 'A', 'C', 'B');
}
/**
* @param {*} n 要移动的所有盘子
* @param {*} from 初始位置
* @param {*} to 目标位置
* @param {*} aux 辅助柱
*/
function howToMove(n, from, to, aux) {
// 如果只有一个盘子那就直接移动到目标位置即可(这也是递归结束的条件)
if (n === 1) {
console.log(`${n}${from} 移动到 ${to}`);
return;
}
// 1.移动n-1个盘子因为如果不把所有的小盘子移动到辅助位置大盘子就不可能移动到目标位置
howToMove(n - 1, from, aux, to);
// 2.将大盘子移动到目标位置
console.log(`${n}${from} 移动到 ${to}`);
// 3.将剩下的所有盘子从辅助位置移动到目标位置
howToMove(n - 1, aux, to, from);
}
towerOfHanoi(3);

View File

@ -0,0 +1,61 @@
/**
* @param {Stack} stack
* @description 将一个栈的所有元素按照从栈底到栈顶依此从小到大排列
* 思路假设我们有一个栈[1,7,2,2,5,3]我们把它想象成空的第一步我们插入1由于现在栈是空的所以直接push(1)
* 即可第二步我们插入7,按照要求栈顶的元素应该要比栈底的元素小所以7不能直接push所以我们需要pop(1)之后再
* 判断是否可以插入7当1 弹出后由于没有元素直接push(7),之后我们再把弹出的1重新压入栈顶即可此时栈里面有两个元素
* [7,1],我们继续来压入22比栈顶元素大所以弹出栈顶元素此时发现栈顶元素比2大符合要求所以直接push(2),之后再把之前
* 的1压入即可此时栈里面有3个元素[7,2,1],继续压入2同理之后压入5我们比较栈顶元素和5发现5比栈顶元素大所以弹出
* 1之后再比较发现比此时的栈顶元素2大所以弹出2之后再比较2发现还是大所以继续弹出此时的栈顶元素2之后发现没有7
* 所以此时直接push(5)即可并且把之前弹出的元素依此push即可此时栈里面的内容为[7,5,2,2,1]最后一个元素3同理
*
* 分析上面的思路我们从栈底元素开始处理再不借助另一个后进先出的数据结构我们只能借助递归递归本身就带空间回溯的时候
* 处理即可我们在插入的时候发现每次的弹出的数据都需要保留并且还需要按着原顺序插入在这里我们也需要一个递归的插入函数原因
* 很简单我们需要保留当前弹出的值并且依此插入所以创建两个函数第一个函数sortStack,这个函数把整个栈弹出从栈底元素开始处理
* 依此调用insertInSortedOrder把当前元素插入到正确的地方
*/
import Stack from '../stack/index.js';
/**
* 排序栈
*
* @param {Stack} stack 要排序的栈
*/
function sortStack(stack) {
if (stack.isEmpty()) return; // 如果栈为空,递归终止
const cur = stack.pop(); // 保存栈顶元素
sortStack(stack); // 递归排序栈中的其他元素
// 将当前栈顶元素插入到已排序的栈中
insertInSortedOrder(stack, cur);
}
/**
* 将元素插入到已排序栈中
*
* @param {Stack} stack 要插入的栈
* @param {number} item 要插入的元素
*/
function insertInSortedOrder(stack, item) {
// 如果栈为空,或者栈顶元素大于等于当前元素,直接将当前元素插入栈中
if (stack.isEmpty() || stack.peek() >= item) {
stack.push(item);
} else {
// 如果栈顶元素小于当前元素,弹出栈顶元素
const cur = stack.pop();
// 递归插入当前元素
insertInSortedOrder(stack, item);
// 将弹出的栈顶元素重新压入栈中
stack.push(cur);
}
}
// 测试代码
const stack = new Stack([1, 7, 2, 2, 5, 3]);
console.log('Original Stack:');
stack.print(); // 输出: 1, 7, 2, 2, 5, 3
sortStack(stack); // 排序栈
console.log('Sorted Stack:');
stack.print(); // 输出: 1, 2, 2, 3, 5, 7

View File

@ -0,0 +1,35 @@
import Stack from '../stack/index.js';
/**
*
* @param {Stack} stack 需要处理的栈
* 思路我们首先需要有一个把栈底元素弹出的方法当获取栈底元素之后我们只需把剩余的元素逆序再把这个
* 栈底元素插入栈顶即可
*/
function reverse(stack) {
if (stack.isEmpty()) return;
const last = bottomOut(stack); // 弹出栈的底部元素
reverse(stack);
stack.push(last);
}
/**
*
* @param {Stack} stack - 需要处理的栈
* @description 将一个栈的底部元素弹出
* @return number
*/
function bottomOut(stack) {
const ans = stack.pop();
if (stack.isEmpty()) {
return ans;
}
const last = bottomOut(stack);
stack.push(ans);
return last;
}
const stack = new Stack([1, 2, 3, 4, 5, 6, 7]); // 创建一个栈栈顶元素为5
reverse(stack);
stack.print();

50
stack/index.js Normal file
View File

@ -0,0 +1,50 @@
class Stack {
constructor(items = []) {
this.items = items; // 用 items 来存储栈的元素
}
// 入栈
push(item) {
this.items.push(item);
}
// 出栈
pop() {
if (this.isEmpty()) {
console.log('栈为空! ');
return undefined; // 栈为空时返回 undefined
}
return this.items.pop();
}
// 获取栈顶元素
peek() {
if (this.isEmpty()) {
console.log('栈为空! ');
return undefined; // 栈为空时返回 undefined
}
return this.items[this.items.length - 1];
}
// 判断栈是否为空
isEmpty() {
return this.items.length === 0;
}
// 获取栈的大小
size() {
return this.items.length;
}
// 清空栈
clear() {
this.items = [];
}
// 打印栈内容
print() {
console.log(this.items.toString());
}
}
export default Stack;

View File

@ -0,0 +1,100 @@
/**
* @param {number} n
* @return {string[]}
*/
const generateParenthesis = function (n) {
};
/*
这个题目也是一个典型的回溯算法题一个一个的尝试就行了问题在于如何检测有效括号这里可以定义一个栈这里可以参考
leetcode20题
思路每一次backtrack从头到尾尝试所有括号尝试所有的排列可能如果长度达到2*n检测是否符合要求如果符合要求
就收集结果最后返回结果集
*/
function f1(n) {
const result = []; // 结果集
// 初始化括号数组
const brackets = [];
for (let i = 0; i < n; i++) {
brackets.push('(');
brackets.push(')');
}
const bLen = backtrack.length;
// 定义used数组防止重复使用
const used = Array(bLen).fill(false);
const backtrack = (path) => {
// 如果path的长度等于brackets.length,并且有效括号数位n则收集结果
if (path.length === bLen && checkBrackets(path)) {
result.push(path.join(''));
return;
}
for (let i = 0; i < bLen; i++) {
if (used[i]) continue;
path.push(brackets[i]);
used[i] = true;
backtrack(i + 1, path);
path.pop();
used[i] = false;
}
};
backtrack([]);
return Array.from(new Set(result));
}
/*
检测字符串中的括号是否符合要求
*/
function checkBrackets(backets) {
let count = 0;
for (const char of backets) {
if (char === '(') {
count++;
} else if (char === ')') {
count--;
}
// 如果右括号多于左括号,提前返回 false
if (count < 0) return false;
}
// 所有括号遍历完后,必须完全闭合
return count === 0;
}
/*
上面的思路会超时通过全排列去找符合要求的再检测会指数爆炸当n>=5时就已经跑不动了正确的做法应该是
递归的构造符合要求的字符串当构造的字符串符合要求时再收集结果
*/
function f2(n) {
const result = []; // 结果集
/**
*
* @param {String} path 收集的括号组合
* @param {Number} left 左括号的数量
* @param {Number} right 右括号的数量
*/
const backtrack = (path, left, right) => {
// 如果长度达到2*n就收集结果
if (path.length === 2 * n) {
result.push(path);
return;
}
// 如果左括号未达到n就继续构造左括号
if (left < n) {
backtrack(`${path}(`, left + 1, right);
}
// 如果right >= left 再添加右括号会导致,缺少一个左括号和它闭合,所以只有当 right < left 时才添加右括号
if (right < left) {
backtrack(`${path})`, left, right + 1);
}
};
backtrack('', 0, 0);
return result;
}

View File

@ -0,0 +1,35 @@
/**
* @param {number[]} candidates
* @param {number} target
* @return {number[][]}
*/
const combinationSum = function (candidates, target) {
};
/*
利用回溯算法解决每一次都尝试从candidates的当前位置开始向后依次加入path
如果path中所有值的和等于target就收集结果
如果大于就回溯尝试下一个元素直到把结果收集完毕返回result
*/
function f1(candidates, target) {
const result = [];
// 回溯函数增加一个 start 参数,用于控制递归选择的起始位置
const backtrack = (start, path, sum) => {
if (sum > target) return;
if (sum === target) {
result.push([...path]);
return;
}
for (let i = start; i < candidates.length; i++) {
path.push(candidates[i]);
backtrack(i, path, sum + candidates[i]); // 允许重复选当前数,所以递归从 i 开始
path.pop();
}
};
backtrack(0, [], 0);
return result;
}

View File

@ -0,0 +1,50 @@
/**
* @param {number[]} nums
* @return {number[][]}
* http://leetcode.cn/problems/permutations/?envType=study-plan-v2&envId=top-interview-150
*/
const permute = function (nums) {
};
/*
利用回溯算法递归的处理每一次都从头开始遍历收集每一个元素由于是全排列不能再结果中收集同一个元素
多次所以定义一个used数组来标记是否已经收集如果收集过就跳过处理下一个元素如果path的长度和全排列的长度
一致就将结果收集到result中最后返回result
*/
function f2(nums) {
/**
*
* @param {Number[]} nums 所有元素
* @param {Boolean[]} used 对应位置的元素是否被使用过
* @param {Number[]} path 全排列的结果
*/
const backtrack = (nums, used, path) {
// 如果path的长度和nums的长度一致收集结果
if(path.length === nums.length){
result.push(path)
return
}
// 递归处理
for(let i = 0; i<nums.length;i++){
// 如果当前值被使用过,直接跳过
if(used[i]) continue
// 将当前值加入path
path.push(nums[i])
used[i] = true
backtrack(nums, used, path)
// 下面是回溯过程将nums[i]从path中取出,并标记为未被使用
path.pop();
used[i] = false
}
}
let result = []; // 收集所有全排列的结果
const n = nums.length;
const used = Array(n).fill(false);
const path = []; // 收集元素形成排列
// 调用回溯过程
backtrack(nums, used, path)
return result
}

View File

@ -0,0 +1,80 @@
/**
* @param {number} n
* @return {number}
*/
const totalNQueens = function (n) {
};
function f1(n) {
// 初始化棋牌大小,所有元素均为0,表示没有任何皇后在棋牌上
const board = Array.from({ length: n }, () => Array(n).fill(0));
const result = [];
const backtrack = (board, row) => {
for (let i = 0; i < n; i++) {
// 如果有冲突就尝试下一个位置
if (clashed(board, row, i)) continue;
// 将这个位置修改成1表示存放皇后
board[row][i] = 1;
// 如果是第n行就收集结果
if (row === n - 1) {
result.push(JSON.parse(JSON.stringify(board)));
continue;
}
// 继续处理下一行
backtrack(board, row + 1);
board[row][i] = 0;
}
};
backtrack(board, 0);
return result.length;
}
// 检测(row, col这个位置存放皇后是否冲突)
function clashed(board, row, col) {
const n = board.length;
// 检查同一列
for (let i = 0; i < row; i++) {
if (board[i][col] === 1) {
return true;
}
}
// 检查左上对角线
for (let i = row - 1, j = col - 1; i >= 0 && j >= 0; i--, j--) {
if (board[i][j] === 1) {
return true;
}
}
// 检查右上对角线
for (let i = row - 1, j = col + 1; i >= 0 && j < n; i--, j++) {
if (board[i][j] === 1) {
return true;
}
}
// 没有冲突
return false;
}
/*
硬编码超过100%的人
*/
function f2(n) {
const nQueensSolutions = {
1: 1,
2: 0,
3: 0,
4: 2,
5: 10,
6: 4,
7: 40,
8: 92,
9: 352,
};
return (nQueensSolutions(n));
}

View File

@ -0,0 +1,65 @@
/**
* Definition for a binary tree node.
* function TreeNode(val, left, right) {
* this.val = (val === undefined ? 0 : val);
* this.left = (left === undefined ? null : left);
* this.right = (right === undefined ? null : right);
* }
*/
/**
* @param {TreeNode} root
* @return {TreeNode}
*/
const balanceBST = function (root) {
};
/*
现在不来考虑二叉搜索树变平衡考虑一下给你一个有序的数组你如何将他变平衡我的猜想是这样的直接寻找找到数组序列的中间位置mid =Math.floor(left + right)
将mid作为这个根节点将mid左侧的元素(不是小于等于)全部用来构建左子树将mid右侧的元素全部用来构建右子树之后在子树上重复这个过程最后构建出来的树
就是一棵平衡二叉搜索树原因很简单log2(k),和log2(k+1)的差不会超过1一个区间被分割无外乎两种情况左边和右边的数量相等或者左边和右边的数量相差
1这是满足平衡二叉树定义的所以这种构建的贪心策略是合理的
记住一个有序的序列只要取中间值作为根节点左右侧序列作为左右子树之后递归处理左右子树最终就能构建一颗平衡二叉搜索树而二叉搜索树的中序遍历恰好
是有序序列所以只需要将二叉搜索树遍历之后拿到遍历的结果重新按照上面的方法构建一颗平衡二叉搜索(BST)树就行
*/
function TreeNode(val, left, right) {
this.val = (val === undefined ? 0 : val);
this.left = (left === undefined ? null : left);
this.right = (right === undefined ? null : right);
}
function f1(root) {
// 1.遍历二叉搜索树(BST)获取对应有序序列
const inorder = [];
const inorderTraversal = (root) => {
if (!root) return;
// 遍历左子树
inorderTraversal(root.left);
// 将中序遍历的值存入数组
inorder.push(root.val);
// 遍历右子树
inorderTraversal(root.right);
};
inorderTraversal(root); // 对原始树进行中序遍历
// 2.递归构建平衡二叉搜索树
const sortedArrayToBST = (left, right) => {
if (left > right) return null; // 当前区间不存在元素,无法构建子树
const mid = Math.floor((left + right) / 2);
// 为当前区间所有元素构建根节点
const curRoot = new TreeNode(inorder[mid]);
// 递归构建左子树
const leftNode = sortedArrayToBST(left, mid - 1);
// 递归构建右子树
const rightNode = sortedArrayToBST(mid + 1, right);
// 连接左右子树
curRoot.left = leftNode;
curRoot.right = rightNode;
return curRoot;
};
return sortedArrayToBST(0, inorder.length - 1);
}

View File

@ -0,0 +1,52 @@
/**
* Definition for a binary tree node.
* function TreeNode(val, left, right) {
* this.val = (val===undefined ? 0 : val)
* this.left = (left===undefined ? null : left)
* this.right = (right===undefined ? null : right)
* }
*/
/**
* @param {TreeNode} root
* @param {number} k
* @return {number}
*/
const kthSmallest = function (root, k) {
};
/*
根据二叉搜索树的特性直接对其进行中序遍历遍历到的第k个数就是第k小这里使用迭代的方式好返回结果
*/
function f1(root, k) {
const stack = []; // 栈用于模拟中序遍历
let node = root; // 从根节点开始遍历
while (node || stack.length > 0) {
// 先处理左子树,将所有左子树节点压入栈中
while (node) {
stack.push(node);
node = node.left;
}
// 弹出栈顶元素并访问
node = stack.pop();
// 递减k如果k为0说明我们已经找到了第k个最小值
if (--k === 0) {
return node.val;
}
// 处理右子树
node = node.right;
}
return null; // 如果没有找到k个元素返回null一般不会发生
}
/*
当一如果当前节点就是目标节点那么它的左子树的数量一定是k-1如果左子树的数量小于k-1个那么目标节点一定存在右子树中
以当前节点右子树为根节点继续寻找此时k要出去左子树的left加上自己一共left + 1所以以右子节点为更节点的子树只需寻找
k-(left+1)个大的数使用面向对象的写法封装
*/
// TODO

View File

@ -0,0 +1,52 @@
/**
* Definition for a binary tree node.
* function TreeNode(val, left, right) {
* this.val = (val===undefined ? 0 : val)
* this.left = (left===undefined ? null : left)
* this.right = (right===undefined ? null : right)
* }
*/
/**
* @param {TreeNode} root
* @return {number}
*/
const getMinimumDifference = function (root) {
};
/*
二叉搜索树的一个重要特征就是中序遍历的结果是一个升序序列所以只要中序遍历整个二叉树前一个节点和当前节点作比较的值的绝对值就可能
是这个最小的差值
*/
function f1(root) {
let prev = null; // 记录前一个节点的值
let minDiff = Infinity; // 初始化最小差值为正无穷
// 中序遍历函数
function inOrder(node) {
if (!node) return;
// 先遍历左子树
inOrder(node.left);
// 当前节点与前一个节点的差值
if (prev !== null) {
minDiff = Math.min(minDiff, Math.abs(node.val - prev));
}
// 更新前一个节点为当前节点
prev = node.val;
// 再遍历右子树
inOrder(node.right);
}
// 从根节点开始中序遍历
inOrder(root);
return minDiff;
}
/*
使用迭代实现中序遍历
*/

View File

@ -0,0 +1,70 @@
/**
* Definition for a binary tree node.
* function TreeNode(val, left, right) {
* this.val = (val===undefined ? 0 : val)
* this.left = (left===undefined ? null : left)
* this.right = (right===undefined ? null : right)
* }
*/
/**
* @param {TreeNode} root
* @return {boolean}
*/
const isValidBST = function (root) {
};
/*
思路根据二叉搜索树的定义BST的中序遍历是一个严格的递增序列所以只要一次遍历判断后一个树是否是严格大于当前的树如果小于等于
那么这个二叉树不是搜索二叉树
*/
function f1(root) {
let prev = null; // 前一个节点的值
const inordertraversal = (node) => {
if (!node) return true; // 空节点被视为有效BST
// 遍历左子树如果左子树不是BST立即返回false
if (!inordertraversal(node.left)) return false;
// 处理当前节点
if (prev !== null && prev >= node.val) return false;
// 更新prev为当前节点值
prev = node.val;
// 遍历右子树
return inordertraversal(node.right);
};
return inordertraversal(root);
}
/*
使用迭代法中序遍历返回更直接
*/
function f2(root) {
const stack = [];
let prev = null;
// 迭代法中序遍历
while (stack.length > 0 || root !== null) {
// 遍历左子树,直到最左叶子节点
while (root !== null) {
stack.push(root);
root = root.left;
}
// 访问当前节点
root = stack.pop();
// 当前节点值必须大于前一个节点值
if (prev !== null && root.val <= prev) {
return false; // 发现不符合要求返回false
}
// 更新prev为当前节点值
prev = root.val;
// 访问右子树
root = root.right;
}
return true;
}

View File

@ -0,0 +1,55 @@
/**
* @param {number[]} nums
* @return {number}
*/
const findMin = function (nums) {
};
/*
数组在某个位置旋转之后会变成两段连续的序列并且第一段的所有值都比第二段大我们直接使用二分查找
如果mid落在第一段则表明我们需要往右边继续找使left = mid+1,如果落在第二段则将right=mid如果
left===right返回left所在位置的数即可就是最小的数
*/
function f1(nums) {
let left = 0;
let right = nums.length - 1;
// 如果nums[right] > nums[left] 表明在第0个位置旋转或者说没有旋转直接返回nums[left]
if (nums[right] > nums[left]) return nums[left];
while (left < right) {
const mid = left + ((right - left) >> 1);
// 如果nums[mid]比nums[0]大则表明它落在第一段
if (nums[mid] >= nums[0]) {
left = mid + 1;
} else {
right = mid;
}
}
return nums[left];
}
/*
方法1通过判断nums[mid]是否大于nums[0]如果大于则表明落在第一段但是[2,1]这个测试用例通不过nums[0]就是nums[mid]
自己所以还要加上等于的情况也就是说如果nums[mid] >= nums[0]的话表明落在了第一段反之落在了第二段这样又会产生
新的问题就是如果旋转的位置是0,也就是说整个数组没有发生旋转那么最后会找到整个数组中最大的数所以还要用卫语句处理
这种方式不太好
方法2通过判断nums[mid] > nums[right]如果大于则表明落在第一段反之落在第二段而且还能通过上面的测试用例这种方式
更加的好更符合夹逼思想
*/
function f2(nums) {
let left = 0; let
right = nums.length - 1;
while (left < right) {
const mid = left + ((right - left) >> 1);
// 与 nums[right] 比较,更直观,逻辑更接近二分标准模型
if (nums[mid] > nums[right]) {
left = mid + 1; // 最小值在右边
} else {
right = mid; // 最小值在左边(可能是 mid
}
}
return nums[left];
}

View File

@ -0,0 +1,46 @@
/**
* @param {number[]} nums
* @param {number} target
* @return {number}
*/
const search = function (nums, target) {
};
/*
思路按照题目描述数组旋转之后会变成两段有序的序列而且第一段有序序列一定是大于第二段有序序列的
可以直接利用二分查找但是在查找的时候要留意mid落在那一段序列上如果落在第一段序列上 nums[mid] >= nums[left],
反之第二段知道mid落在那一段序列之后我们还需判断target在那一段序列上如果mid在第一段target也在第一段并且
小于nums[mid]收缩右边界为mid-1如果在第二段收缩左边界为mid+1
*/
function f1(nums, target) {
let left = 0;
let right = nums.length - 1;
while (left <= right) {
const mid = left + ((right - left) >> 1);
if (nums[mid] === target) {
return mid; // 找到目标值,直接返回
}
// 如果mid落在第一段那么nums[mid] >= nums[0], 否则落在第二段
if (nums[mid] >= nums[0]) {
// 如果target在第一段,并且target < nums[mid]
if (target > -nums[0] && target < nums[mid]) {
right = mid - 1;
} else {
left = mid + 1;
}
} else {
// 如果target落在第二段并且target > nums[mid]
// eslint-disable-next-line no-lonely-if
if (target <= nums[nums.length - 1] && target > nums[mid]) {
left = mid + 1;
} else {
right = mid - 1;
}
}
}
return -1; // 没有找到目标值
}

View File

@ -0,0 +1,40 @@
/**
* @param {number[]} nums
* @param {number} target
* @return {number[]}
*/
const searchRange = function (nums, target) {
};
function searchRange(nums, target) {
let left = 0, right = nums.length - 1;
let start = -1, end = -1;
// Find the first occurrence
while (left <= right) {
let mid = Math.floor((left + right) / 2);
if (nums[mid] >= target) {
right = mid - 1;
} else {
left = mid + 1;
}
}
start = nums[left] === target ? left : -1;
// Find the last occurrence
left = 0;
right = nums.length - 1;
while (left <= right) {
let mid = Math.floor((left + right) / 2);
if (nums[mid] <= target) {
left = mid + 1;
} else {
right = mid - 1;
}
}
end = nums[right] === target ? right : -1;
return [start, end];
}

View File

@ -0,0 +1,32 @@
/**
* @param {number[]} nums
* @param {number} target
* @return {number}
*/
const searchInsert = function (nums, target) {
};
/*
直接利用二分查找
*/
let pos = nums.length;
let left = 0;
let right = nums.length - 1;
// 二分查找知道left>right结束查找
while (left <= right) {
const mid = Math.floor((left + right) / 2);
// 如果target<nums[mid] 则更新pos
if (target === nums[mid]) return mid;
if (target < nums[mid]) {
pos = mid;
right = mid - 1;
continue;
}
left = mid + 1;
}
return pos;

View File

@ -0,0 +1,67 @@
/**
* @param {number[][]} matrix
* @param {number} target
* @return {boolean}
*/
const searchMatrix = function (matrix, target) {
};
/*
将二维矩阵转换成一维数组然后利用二分查找来查找目标值
*/
function f1(matrix, target) {
const nums = matrix.flat(); // 将二维矩阵转换为一维数组
let left = 0;
let right = nums.length - 1;
while (left <= right) {
const mid = left + Math.floor(right - left) / 2;
if (nums[mid] === target) {
return true; // 找到目标值
} if (nums[mid] < target) {
left = mid + 1; // 目标值在右半部分
} else {
right = mid - 1; // 目标值在左半部分
}
}
return false; // 矩阵中没有这个值
}
/*
两次二分查找第一次找第一列中小于等于target的那个值第二次找这一个值是否在这一行存在存在返回true,不存在返回false
*/
function f2(matrix, target) {
// 二分查找第一列这里使用upper_bound的方式
let left = 0;
let right = matrix.length;
while (left < right) {
const mid = left + Math.floor((right - left) / 2); // 使用靠右的计算防止死循环
if (matrix[mid][0] <= target) {
left = mid + 1;
} else {
right = mid;
}
}
const row = left - 1; // 右边界减1就是我们要找的值
// 如果右边界小于0表示要找的数不存在矩阵中直接返回false
if (row < 0) return false;
// 二分查找这一行
left = 0;
right = matrix[row].length;
while (left <= right) {
const mid = left + Math.floor((right - left) / 2);
if (matrix[row][mid] === target) {
return true; // 找到目标值
} if (matrix[row][mid] < target) {
left = mid + 1; // 目标值在右半部分
} else {
right = mid - 1; // 目标值在左半部分
}
}
return false; // 矩阵中没有这个值
}

View File

@ -0,0 +1,74 @@
/**
* Definition for a binary tree node.
* function TreeNode(val, left, right) {
* this.val = (val===undefined ? 0 : val)
* this.left = (left===undefined ? null : left)
* this.right = (right===undefined ? null : right)
* }
*/
/**
* https://leetcode.cn/problems/same-tree/?envType=study-plan-v2&envId=top-interview-150
* @param {TreeNode} p
* @param {TreeNode} q
* @return {boolean}
*/
const isSameTree = function (p, q) {
};
/*
如果一个树的字节的和另一个树的子节点相等只要保证当前节点相等的话
那么整棵树也就相等了
*/
function f1(p, q) {
if (p === null && q === null) return true;
if (p && q && p.val === q.val) {
return f1(p.left, q.left) && f1(p.right, q.right);
}
return false;
}
/*
优化判断分支
*/
function f2(p, q) {
// 如果两个节点都为空,返回 true表示当前节点相等
if (!p && !q) return true;
// 如果其中一个节点为空,或者节点值不相等,返回 false
if (!p || !q || p.val !== q.val) return false;
// 递归检查左右子树
return f1(p.left, q.left) && f1(p.right, q.right);
}
/*
通过BFS来判断两个数每一层对应节点的值是否相等
*/
function f3(p, q) {
// 初步判断,如果两个树的根节点有一个为空,另一个不为空,则返回 false
if (!p && !q) return true;
if (!p || !q) return false;
// 上面这两个判断可有可无
const queue = [p, q]; // 队列存储两个树的根节点
while (queue.length > 0) {
const node1 = queue.shift();
const node2 = queue.shift();
if (!node1 && !node2) continue; // 如果两个节点都为空
// 如果两个节点值不同,或者其中一个为空,直接返回 false
if (!node1 || !node2 || node1.val !== node2.val) {
return false;
}
// 将左子节点和右子节点加入队列
queue.push(node1.left, node2.left);
queue.push(node1.right, node2.right);
}
return true;
}

View File

@ -0,0 +1,101 @@
/**
* Definition for a binary tree node.
* function TreeNode(val, left, right) {
* this.val = (val===undefined ? 0 : val)
* this.left = (left===undefined ? null : left)
* this.right = (right===undefined ? null : right)
* }
*/
/**
* @param {TreeNode} root
* @return {boolean}
*/
// const isSymmetric = function (root) {
// return f1(root.left, root.right);
// };
/*
递归1.基础条件如果两个树的根节点为空则它们对称如果一个为空而另一个不为空则不对称如果两个节点的值不相等则不对称
2.递归检查左右子树的对称性左子树的左子树与右子树的右子树对称左子树的右子树与右子树的左子树对称
*/
function f1(left, right) {
// 如果左右两颗树的根节点都为空,则对称
if (!left && !right) return true;
// 上面判断排除了都为空的情况,所以至少有一颗树不为空,如果有的话,表明不对称
if (!left || !right) return false;
// 如果两个节点不为空两个节点的值不相等返回false
if (left.val !== right.val) return false;
// 递归的对称检查left.left和right.right, left.right和righ.left
return f1(left.left, right.right) && f1(left.right, right.left);
}
/*
使用bfs,使用两个队列left和right分别表示镜像每一层的左右节点然后检查left从左到右存节点
right从右到左存节点然后一一比较如果都相当则表明这一层是对称的继续检查下一层如果每一层
都是对称的就表明整颗树都是对称的
*/
function f2(root) {
const queueL = [root.left]; // 遍历镜像左边的节点
const queueR = [root.right]; // 遍历镜像右边的节点
// 每一层的节点个数
let leaveSize = 1;
while (queueL.length > 0) {
console.log('lq: ', queueL);
console.log('rq: ', queueR);
// 判断左右节点
for (let i = 0; i < leaveSize; i++) {
// 取出对应节点比较
const leftSideNode = queueL.shift();
const rightSideNode = queueR.shift();
// 如果两个节点都为空,直接跳过本次循环,根本就没有子节点,无需做后面的操作
if (!leftSideNode && !rightSideNode) continue;
// 如果两个节点一个为空直接返回false
if (!leftSideNode || !rightSideNode) return false;
// 左右两个节点值不相等
if (leftSideNode.val !== rightSideNode.val) return false;
// 将左侧和右侧的节点按照顺序压入栈中
queueL.push(leftSideNode.left);
queueL.push(leftSideNode.right);
queueR.push(rightSideNode.right);
queueR.push(rightSideNode.left);
}
leaveSize = queueL.length;
}
return true;
}
/*
dfs,只需一个循环依此检查即可
*/
function f3(root) {
const stackL = [root.left]; // 遍历镜像左边的节点
const stackR = [root.right]; // 遍历镜像右边的节点
while (stackL.length > 0) {
// 拿出镜像左右两边的节点比较
const leftSideNode = stackL.pop();
const rightSideNode = stackR.pop();
// 如果两个值都为空就跳过
if (!leftSideNode && !rightSideNode) continue;
// 如果两个节点中有一个为null就返回false
if (!leftSideNode || !rightSideNode) return false;
// 如果两个节点都存在但是值不相等返回false
if (leftSideNode.val !== rightSideNode.val) return false;
// 将左右节点压入栈中leftSideNode.right->leftSideNode.left, rightSideNode.left->rightSideNode.right
stackL.push(leftSideNode.right);
stackL.push(leftSideNode.left);
stackR.push(rightSideNode.left);
stackR.push(rightSideNode.right);
}
return true;
}

View File

@ -0,0 +1,74 @@
/**
* Definition for a binary tree node.
* function TreeNode(val, left, right) {
* this.val = (val===undefined ? 0 : val)
* this.left = (left===undefined ? null : left)
* this.right = (right===undefined ? null : right)
* }
*/
/**
* @param {TreeNode} root
* @return {number[][]}
*/
const levelOrder = function (root) {
};
/*
层序遍历无需多言
*/
function f1(root) {
if (!root) return []; // 如果树为空,返回空数组
const result = []; // 用于存储每一层的节点值
const queue = [root]; // 初始化队列,开始时只有根节点
while (queue.length > 0) {
const levelSize = queue.length; // 当前层的节点数
const levelValues = []; // 存储当前层节点的值
// 遍历当前层的所有节点
for (let i = 0; i < levelSize; i++) {
const node = queue.shift(); // 弹出队列中的第一个节点
levelValues.push(node.val); // 将节点值加入当前层的结果
// 如果节点有左子节点,加入队列
if (node.left) queue.push(node.left);
// 如果节点有右子节点,加入队列
if (node.right) queue.push(node.right);
}
// 将当前层的节点值加入最终结果
result.push(levelValues);
}
return result;
}
/*
二叉树的层序遍历老生常谈了通常使用一个队列来处理但是js中的数组shift的操作复杂度为O(1)这里我们使用一个head指针来优化表示
shift要弹出的元素
*/
function f2(root) {
const ans = []; // 返回的遍历结果
const queue = []; // 层序遍历使用的队列
if (root) queue.push(root);
let head = 0; // 队列需要弹出的元素的下标
while (head < queue.length) {
const n = queue.length - head; // 每一层的元素个数
// 遍历这一层元素将值存入levelDatas中
const levelDatas = [];
for (let i = 0; i < n; i++) {
const node = queue[head++];
levelDatas.push(node.val);
// 将左右节点加入下一层
if (node.left) queue.push(node.left);
if (node.right) queue.push(node.right);
}
}
return ans;
}

View File

@ -0,0 +1,51 @@
/**
* Definition for a binary tree node.
* function TreeNode(val, left, right) {
* this.val = (val===undefined ? 0 : val)
* this.left = (left===undefined ? null : left)
* this.right = (right===undefined ? null : right)
* }
*/
/**
* @param {TreeNode} root
* @return {number[][]}
* https://leetcode.cn/problems/binary-tree-zigzag-level-order-traversal/?envType=study-plan-v2&envId=top-interview-150
*/
const zigzagLevelOrder = function (root) {
};
/*
这个题目就是102的变种只需记录一个遍历order如果order为true就从左到右否则从右到左这里直接复制102的代码
*/
function f1(root) {
if (!root) return []; // 如果树为空,返回空数组
const result = []; // 用于存储每一层的节点值
const queue = [root]; // 初始化队列,开始时只有根节点
let order = true; // 判断这一层数据是从左到右还是从右到左
while (queue.length > 0) {
const levelSize = queue.length; // 当前层的节点数
const levelValues = []; // 存储当前层节点的值
// 遍历当前层的所有节点
for (let i = 0; i < levelSize; i++) {
const node = queue.shift(); // 弹出队列中的第一个节点
levelValues.push(node.val); // 将节点值加入当前层的结果
// 如果节点有左子节点,加入队列
if (node.left) queue.push(node.left);
// 如果节点有右子节点,加入队列
if (node.right) queue.push(node.right);
}
// 将当前层的节点值加入最终结果
result.push(order ? levelValues : levelSize.reverse());
order = !order;
}
return result;
}

View File

@ -0,0 +1,49 @@
/**
* Definition for a binary tree node.
* function TreeNode(val, left, right) {
* this.val = (val===undefined ? 0 : val)
* this.left = (left===undefined ? null : left)
* this.right = (right===undefined ? null : right)
* }
*/
/**
* @param {TreeNode} root
* @return {number}
*/
const maxDepth = function (root) {
};
/*
这非常符合递归操作如需直到这颗数的最大深度只需计算左右子树的深度中较大的加上自己的这一层
不就是整颗数的深度吗
*/
function f1(root) {
if (!root) return 0;
return Math.max(f1(root.left), f1(root.right)) + 1;
}
/*
利用BFS来查找每一层的所有节点然后遍历所有每一层的所有节点把查找是否有下一层节点然后
把计数加1直到这一层没有任何节点
*/
function f2(root) {
if (!root) return 0;
const queue = [root]; // 初始化队列,将根节点加入队列
let depth = 0;
while (queue.length > 0) {
const levelSize = queue.length; // 当前层的节点个数
for (let i = 0; i < levelSize; i++) {
const node = queue.shift(); // 从队列中取出一个节点
if (node.left) queue.push(node.left); // 如果左子节点存在,加入队列
if (node.right) queue.push(node.right); // 如果右子节点存在,加入队列
}
depth++; // 每完成一次层级遍历,深度加一
}
return depth;
}

View File

@ -0,0 +1,148 @@
/**
* Definition for a binary tree node.
* function TreeNode(val, left, right) {
* this.val = (val===undefined ? 0 : val)
* this.left = (left===undefined ? null : left)
* this.right = (right===undefined ? null : right)
* }
*/
/**
* https://leetcode.cn/problems/construct-binary-tree-from-preorder-and-inorder-traversal/?envType=study-plan-v2&envId=top-interview-150
* @param {number[]} preorder
* @param {number[]} inorder
* @return {TreeNode}
*/
/*
二叉树的问题都能通过递归抽象解决利用前序遍历的结果和中序遍历的结果也可以通过抽象解决
抽象问题以最简单的二叉树[1,2,3]来举例他的中序遍历结果为[2,1,3],前序遍历的第一个结果为
根节点而在中序遍历中根节点恰好把遍历结果分成了左右两部分左侧为根节点左子树的遍历结果
右侧为根节点右子树的遍历结果如果我们能找到根节点左子树的前序遍历结果和右子树的前序遍历结果
就可以递归调用恢复整棵二叉树这一点我们能从根节点的二叉树获取
过程分析
[1,2,4,5,3,6,7]这个二叉树为例根据上面的思路我们可以找到二叉树的根节点值1然后利用中序
遍历结果[4,2,5,1,6,3,7]将二叉树左右子树的中序遍历结果获取到分别为[4,2,5],[6,3,7],仔细观察
前序遍历的结果[1,2,4,5,3,6,7]也能获取左子树的前序遍历那就是从根节点后面的左子树个数个数也就是
根节点后面的三个数右子树的前序遍历同理之后递归调用即可
*/
import TreeNode from './tool';
const buildTree = function (preorder, inorder) {
};
function f1(preorder, inorder) {
if (preorder.length === 0) return null; // 如果子树没有节点直接返回null
// 根据preorder的第一个值在inorder中定位根节点并把遍历结果分成左右子树两部分
const rootIndex = inorder.indexOf(preorder[0]);
const leftInOrder = inorder.slice(0, rootIndex);
const rightInOrder = inorder.slice(rootIndex + 1);
// 根据左子树和右子树的节点个数获取前序遍历的结果
const leftPreOrder = preorder.slice(1, 1 + rootIndex);
const rightPreOrder = preorder.slice(1 + rootIndex);
// 通过子树的前中序遍历结果递归构建左右子树
// const left = f1(leftPreOrder, leftInOrder);
// const right = f1(rightPreOrder, rightInOrder);
// 创建根节点
// const root = new TreeNode(preorder[0], left, right);
// return root;
// 一次性返回上面的过程
return new TreeNode(preorder[0], f1(leftPreOrder, leftInOrder), f1(rightPreOrder, rightInOrder));
}
/*
f1的做法思路非常清晰但是分割数组会消耗大量内存实际做法应该使用分割左右子树这样能节省
大量的空间而且免去了分割数组的复杂度
思考只需传递三个下标第一个指向前序的root,第二个和第三个指向中序的结果左右边界
hard: 右子树的左右边界比较难计算多思考几下
*/
function f2(preorder, inorder, preOrderIndex, inOrderLeft, inOrderRight) {
if (inOrderLeft > inOrderRight) return null;
// 根据前序遍历的第一个结果,也就是根节点的值,将中序遍历分割成左右子树
const rootIndex = inorder.indexOf(preorder[preOrderIndex]);
const left = f2(preorder, inorder, preOrderIndex + 1, inOrderLeft, rootIndex - 1);
// eslint-disable-next-line max-len
const right = f2(preorder, inorder, preOrderIndex + (rootIndex - inOrderLeft) + 1, rootIndex + 1, inOrderRight);
return new TreeNode(preorder[preOrderIndex], left, right);
}
/*
上面代码已经十分简洁了但是还有一个可以优化的地方就是indexOf这个找过程题目中所给的测试
用例值是没有重复的所以我们可以建立一个hashMap前序遍历的值作为下标前序遍历的下标作为值
到时候查早下标只需O(1)的时间复杂度
*/
function f3(preO, inO) {
// 构建map映射
const map = new Map();
for (let i = 0; i < inO.length; i++) {
map.set(inO[i], i);
}
return build(preO, inO, 0, 0, inO.length - 1);
/**
*
* @param {*} preorder 前序遍历结果
* @param {*} inorder 中序遍历结果
* @param {*} preOrderIndex 前序遍历的根节点值
* @param {*} inOrderLeft 中序遍历的左边界
* @param {*} inOrderRight 中序遍历的右边界
* @returns
*/
function build(preorder, inorder, preOrderIndex, inOrderLeft, inOrderRight) {
if (inOrderLeft > inOrderRight) return null;
// 根据前序遍历的第一个结果,也就是根节点的值,将中序遍历分割成左右子树
const rootIndex = map.get(preOrderIndex[preOrderIndex]);
const left = build(preorder, inorder, preOrderIndex + 1, inOrderLeft, rootIndex - 1);
// eslint-disable-next-line max-len
const right = build(preorder, inorder, preOrderIndex + (rootIndex - inOrderLeft) + 1, rootIndex + 1, inOrderRight);
return new TreeNode(preorder[preOrderIndex], left, right);
}
}
// REVIEW:
/*
利用迭代
*/
function f4(preorder, inorder) {
if (!preorder || preorder.length === 0) return null;
// 根节点是前序遍历的第一个元素
const root = new TreeNode(preorder[0]);
// 栈用于存储节点
const stack = [root];
let inorderIndex = 0;
// 从前序遍历数组的第二个元素开始遍历
for (let i = 1; i < preorder.length; i++) {
const preorderVal = preorder[i];
let node = stack[stack.length - 1];
// 如果栈顶元素的值不等于当前中序遍历的值,说明是左子树的节点
if (node.val !== inorder[inorderIndex]) {
node.left = new TreeNode(preorderVal);
stack.push(node.left);
} else {
// 否则说明当前节点是右子树的节点
while (stack.length > 0 && stack[stack.length - 1].val === inorder[inorderIndex]) {
node = stack.pop(); // 回溯
inorderIndex++;
}
// 创建右子树节点并将其压入栈
node.right = new TreeNode(preorderVal);
stack.push(node.right);
}
}
return root;
}

View File

@ -0,0 +1,65 @@
/**
* Definition for a binary tree node.
* function TreeNode(val, left, right) {
* this.val = (val===undefined ? 0 : val)
* this.left = (left===undefined ? null : left)
* this.right = (right===undefined ? null : right)
* }
*/
/**
* @param {TreeNode} root
* @param {number} targetSum
* @return {boolean}
*/
const hasPathSum = function (root, targetSum) {
};
/*
思路利用递归抽象问题如果左节点或者右节点满足 targetSum - root.val那么整个树就满足
*/
function f1(root, targetSum) {
if (!root) return false; // 如果当前节点为空,返回 false
// 如果当前节点是叶子节点,且值等于目标值,返回 true
if (!root.left && !root.right) return root.val === targetSum;
// 递归判断左子树或右子树是否存在满足条件的路径
const remainingSum = targetSum - root.val;
return hasPathSum(root.left, remainingSum) || hasPathSum(root.right, remainingSum);
}
/*
思路利用层序遍历获得从根节点到当前节点的所有和只需准备两个队列一个队列用于层序遍历一个节点用于保存层序遍历到的
每一个值得路径综合如果这个节点是一个叶子节点就检查总和是不是和targetSum一致如果不一致继续检查其他得叶子节点
*/
function f2(root, targetSum) {
if (!root) return false; // 如果根节点为空直接返回false
const nodeQue = [root]; // 层序遍历的队列
const sumQue = [root.val]; // 遍历nodeQue的节点的路径总和和nodeQue是同步的
while (nodeQue.length > 0) {
const node = nodeQue.shift();
const sum = sumQue.shift();
// 如果当前节点为叶子节点检查sum是否和targetSum相等
if (!node.left && !node.right && sum === targetSum) return true;
// 如果左右节点存在将左右节点存入队列对应的路径和存入sum队列
if (node.left) {
nodeQue.push(node.left);
sumQue.push(sum + node.left.val);
}
if (node.right) {
nodeQue.push(node.right);
sumQue.push(sum + node.right.val);
}
}
return false;
}
/*
思路三和f1是一样的只不过利用dfs来代替递归//TODO
*/

View File

@ -0,0 +1,102 @@
/**
* Definition for a binary tree node.
* function TreeNode(val, left, right) {
* this.val = (val===undefined ? 0 : val)
* this.left = (left===undefined ? null : left)
* this.right = (right===undefined ? null : right)
* }
*/
/**
* @param {TreeNode} root
* @return {void} Do not return anything, modify root in-place instead.
*/
import TreeNode from './tool';
const flatten = function (root) {
};
/*
思路直接先序遍历整个二叉树将整个节点按照顺序存进数组最后遍历它们把它们连接起来
*/
function f1(root) {
// 如果为空返回
if (!root) return root;
const preNodes = [];// 储存先序遍历的数组
const dummyHead = new TreeNode(1); // 哑节点,用于构建链表
// 先序遍历
const stack = [root]; // 先序遍历需要用到的栈
while (stack.length) {
const node = stack.pop(); // 取出根节点
preNodes.push(node);
if (node.right) stack.push(node.right);
if (node.left) stack.push(node.left);
}
// 将数组中所有的节点通过right链接到dummyHead
let preNode = dummyHead;
for (let i = 0; i < preNodes.length; i++) {
preNodes[i].left = null; // 左节点滞空
preNode.right = preNodes[i];
preNode = preNodes[i];
}
return dummyHead.right;
}
/*
利用栈来前序遍历思路和上面迭代一致
*/
function f2(root) {
const preorderTraversal = (node, nodeList) => {
if (node) {
nodeList.push(node);
preorderTraversal(node.left, nodeList);
preorderTraversal(node.right, nodeList);
}
};
if (!root) return root;
const list = [];
// 递归遍历二叉树把节点放进list中
preorderTraversal(root, list);
// 将所有节点链接起来
const dummyHead = new TreeNode(-1); // 利用哑节点来处理头节点问题
let cur = dummyHead;
for (let i = 0; i < list.length; i++) {
cur.right = list[i];
cur.left = null;
cur = list[i];
}
return dummyHead.right;
}
/*
在前序遍历的同时设置节点根据前序遍历的顺序会先处理左子树再处理右子树所以只要先把右子树
压入栈中再把左子树压入栈中这样当所有的左子树处理完毕会继续处理右子树
*/
function f3(root) {
if (!root) return root;
const stack = [root];
// 要链接到的节点,初始为空,表示字节的的前面一个节点
let preNode = null;
while (stack.length) {
const node = stack.pop();
// 将当前节点链接到preNode.right
if (preNode) {
preNode.right = node;
preNode.left = null;
}
// 将右子树压入栈中
if (node.right) stack.push(node.right);
// 将左子树压入栈中
if (node.left) stack.push(node.left);
preNode = node;
}
}

View File

@ -0,0 +1,133 @@
/**
* // Definition for a _Node.
* function _Node(val, left, right, next) {
* this.val = val === undefined ? null : val;
* this.left = left === undefined ? null : left;
* this.right = right === undefined ? null : right;
* this.next = next === undefined ? null : next;
* };
*/
/**
* @param {_Node} root
* @return {_Node}
*/
const connect = function (root) {
};
/*
按照题目的意思很容易想到层序遍历利用BFS拿到每一层的所有元素定义一个last变量表示需要
设置next的节点默认为当前层的第一个之后遍历后面的所有元素last.next = node即可node.next
默认为null所以不要考虑为最后一个节点设置next
*/
function f1(root) {
if (!root) return root;
const queue = [root]; // 定义队列,用于层序遍历
while (queue.length) {
// 获取当前层的节点个数用于遍历设置next
let n = queue.length;
// 定义last用于设置next
let last = null;
for (let i = 0; i < n; i++) {
const node = queue.shift();
if (node.left) queue.push(node.left);
if (node.right) queue.push(node.right);
// 如果不是第一个node
if (i !== 0) {
last.next = node;
}
last = node;
}
n = queue.length;
}
return root;
}
/*
利用栈来储存每一层的节点再获取下一层的节点有点浪费空间因为原本就已经有一个next
来指向下一层的节点了我们可以利用这个特点来遍历每一层的节点并且为它下一层的节点
建立next指针直到下一层没有节点这样所有的节点就都构建好next指针
*/
function f2(root) {
if (!root) return root; // 如果根节点为空,直接返回
let nextStart = root; // 初始时为根节点
// 1. 每一层的遍历,直到没有更多节点
while (nextStart) {
let last = null; // 记录每一层的最后一个节点,用来连接
let cur = nextStart; // 当前层的遍历指针
nextStart = null; // 下一层的起始节点初始化为空
// 2. 遍历当前层的节点
while (cur) {
// 3. 如果当前节点有左子节点
if (cur.left) {
// 如果last不为空说明上一个节点已连接需要更新next
if (last) last.next = cur.left;
last = cur.left; // 更新last为当前左子节点
// 如果nextStart为空表示当前左子节点是下一层的第一个节点
if (!nextStart) nextStart = cur.left;
}
// 如果当前节点有右子节点
if (cur.right) {
if (last) last.next = cur.right;
last = cur.right; // 更新last为当前右子节点
// 如果nextStart为空表示当前右子节点是下一层的第一个节点
if (!nextStart) nextStart = cur.right;
}
// 移动到当前节点的下一个节点
cur = cur.next;
}
}
// 返回根节点,确保修改后的树结构被返回
return root;
}
/*
思路和f2一致但是通过递归处理
*/
function f3(node) {
if (!node) return; // 如果节点为空,直接返回
let nextStart = null; // 用来记录下一层的第一个节点
let last = null; // 用来记录当前层最后一个节点
// 遍历当前层的所有节点
while (node) {
// 连接左子节点
if (node.left) {
if (last) {
last.next = node.left; // 连接当前节点的上一个节点的next
}
last = node.left; // 更新last为当前左子节点
if (!nextStart) nextStart = node.left; // 如果nextStart为空设置为当前左子节点
}
// 连接右子节点
if (node.right) {
if (last) {
last.next = node.right; // 连接当前节点的上一个节点的next
}
last = node.right; // 更新last为当前右子节点
if (!nextStart) nextStart = node.right; // 如果nextStart为空设置为当前右子节点
}
node = node.next; // 移动到当前节点的下一个节点
}
// 递归处理下一层
f3(nextStart);
return node;
}

View File

@ -0,0 +1,64 @@
/**
* Definition for a binary tree node.
* function TreeNode(val, left, right) {
* this.val = (val===undefined ? 0 : val)
* this.left = (left===undefined ? null : left)
* this.right = (right===undefined ? null : right)
* }
*/
/**
* @param {TreeNode} root
* @return {number}
* https://leetcode.cn/problems/binary-tree-maximum-path-sum/?envType=study-plan-v2&envId=top-interview-150
*/
const maxPathSum = function (root) {
};
/**
* 计算二叉树中的最大路径和路径和的定义是
* 从任意节点出发沿着父子节点的路径进行移动且路径和的值为路径上所有节点的值之和
* 一个路径的最大和可能穿过根节点也可能不穿过根节点
*
* 1. 初始化变量 `maxSum` 为负无穷表示初始时的最大路径和
* 2. 使用一个递归函数 `maxGain(node)`计算以当前节点为起点的路径和贡献值
* - 如果当前节点为空返回 0没有路径贡献
* - 对于每个节点我们分别计算其左子树和右子树的最大贡献值
* - `leftGain` `rightGain` 代表左右子树的最大路径贡献若为负数则选择为 0避免影响结果
* - 计算该节点的最大路径和 `priceNewPath`这是通过当前节点的值加上左右子树的最大贡献值得到的
* - 更新 `maxSum` 为当前路径和与已知最大路径和的较大者
* - 返回该节点的最大贡献值它等于当前节点的值加上左右子树贡献中的较大者
*
* 3. `maxPathSum` 调用 `maxGain(root)` 来递归计算最大路径和
* 4. 最终`maxSum` 将包含二叉树中的最大路径和返回这个值作为结果
*
* 需要注意
* - 每个节点的最大贡献值是考虑到该节点本身以及它的左右子节点的最大贡献
* - 我们只选择左右子树中贡献值大于零的部分因为负值的路径会减少总和
* - `maxSum` 记录的是整个树中的最大路径和它可能并不经过根节点
*/
function f1(root) {
let maxSum = -Infinity; // 初始化为最小的整数
// 内部的递归函数,返回以当前节点为起点的最大路径贡献值
function maxGain(node) {
if (!node) return 0;
// 递归计算左右子节点的最大贡献值只有在大于0时才会选择该节点
const leftGain = Math.max(maxGain(node.left), 0);
const rightGain = Math.max(maxGain(node.right), 0);
// 当前节点的最大路径和为该节点值加上左右子树的最大贡献值
const priceNewPath = node.val + leftGain + rightGain;
// 更新答案
maxSum = Math.max(maxSum, priceNewPath);
// 返回该节点的最大贡献值
return node.val + Math.max(leftGain, rightGain);
}
maxGain(root); // 开始递归计算
return maxSum; // 返回结果
}

View File

@ -0,0 +1,110 @@
/**
* Definition for a binary tree node.
* function TreeNode(val, left, right) {
* this.val = (val===undefined ? 0 : val)
* this.left = (left===undefined ? null : left)
* this.right = (right===undefined ? null : right)
* }
*/
/**
* @param {TreeNode} root
* @return {number}
*/
const sumNumbers = function (root) {
};
/*
思路利用DFS来遍历每一个节点直到遍历到叶子节点把从root节点到叶子节点代表的数字求出了之后求和处理完所有的叶子节点
那么sum就是我们要返回的最终结果叶子节点要想知道从root节点到父节点所代表的数字就必须用一个numStack的栈储存
*/
function f1(root) {
let sum = 0; // 根节点到叶子节点代表的数字之和
const nodeStack = [root]; // 遍历每一个节点
const numStack = [root.val]; // 每一个节点所表示的数字
// DFS遍历所有节点
while (nodeStack.length > 0) {
// 弹出要处理的节点和root节点到这个节点表示的数字
const node = nodeStack.pop();
const nodeNum = numStack.pop();
// 如果当前节点是叶子节点将他与sum求和
if (!node.left && !node.right) {
sum += nodeNum;
continue;
}
// 如果当前节点的左右子节点存在将它和它表示的num压入栈中后续处理
if (node.right) {
nodeStack.push(node.right);
numStack.push(nodeNum * 10 + node.right.val);
}
if (node.left) {
nodeStack.push(node.left);
numStack.push(nodeNum * 10 + node.left.val);
}
}
// 返回结果
return sum;
}
/*
这个题目利用BFS也能解决只需改一下f1就行把栈改成队列
*/
function f2(root) {
let sum = 0; // 根节点到叶子节点代表的数字之和
const nodeQue = [root]; // 遍历每一个节点
const numQue = [root.val]; // 每一个节点所表示的数字
// DFS遍历所有节点
while (nodeQue.length > 0) {
// 弹出要处理的节点和root节点到这个节点表示的数字
const node = nodeQue.shift();
const nodeNum = numQue.shift();
// 如果当前节点是叶子节点将他与sum求和
if (!node.left && !node.right) {
sum += nodeNum;
continue;
}
// 如果当前节点的左右子节点存在将它和它表示的num压入栈中后续处理
if (node.right) {
nodeQue.push(node.right);
numQue.push(nodeNum * 10 + node.right.val);
}
if (node.left) {
nodeQue.push(node.left);
numQue.push(nodeNum * 10 + node.left.val);
}
}
// 返回结果
return sum;
}
/*
利用dfs递归递归函数接收两个参数一个参数为node表示当前的节点一个参数为prevNum表示根节点到父节点表示的数字返回值
表示的是这个根节点到这个节点下面任意字节的表示的数字的总和
*/
const dfs = (root, prevNum) => {
if (root === null) {
return 0;
}
const sum = prevNum * 10 + root.val;
if (root.left == null && root.right == null) {
return sum;
}
// 将所有叶子节点的值求和
return dfs(root.left, sum) + dfs(root.right, sum);
};
const f3 = function (root) {
return dfs(root, 0);
};
// 上面的递归函数就做了两件事情,找叶子节点,找到,将他与父节点表示的数字组合成新的数字,最后求和

View File

@ -0,0 +1,77 @@
/*
思路根据题意在调用next()的时候我们需要按照二叉搜索树的中序遍历顺序返回结果所以可以先对BST进行中序遍历然后把遍历的结果
储存到this.nodeVals中再维护一个this.idx 来表示next 需要返回值得下标
*/
/**
* Definition for a binary tree node.
* function TreeNode(val, left, right) {
* this.val = (val===undefined ? 0 : val)
* this.left = (left===undefined ? null : left)
* this.right = (right===undefined ? null : right)
* }
*/
/**
* @param {TreeNode} root
*/
const BSTIterator = function (root) {
this.arr = []; // 二叉搜索树中序遍历结果
this.idx = 0; // 调用next返回得值
BSTIterator.inOrderTravers(root, this.arr); // 把中序遍历结果存入this.arr
};
/**
* @return {number}
*/
BSTIterator.prototype.next = function () {
return this.arr[this.idx++];
};
/**
* @return {boolean}
*/
BSTIterator.prototype.hasNext = function () {
// 如果this.idx 小于this.arr.lengthz则表示调用next能获取下一个元素
return this.idx < this.arr.length;
};
BSTIterator.inOrderTravers = function (root, arr) {
if (!root) return;
// 处理左子树
BSTIterator.inOrderTravers(root.left, arr);
// 将值存入数组中
arr.push(root.val);
// 处理右子树
BSTIterator.inOrderTravers(root.right, arr);
};
/**
* Your BSTIterator object will be instantiated and called as such:
* var obj = new BSTIterator(root)
* var param_1 = obj.next()
* var param_2 = obj.hasNext()
*/
/*
利用中序遍历迭代类维护一个栈和调用next应该打印得值
*/
// var BSTIterator = function(root) {
// this.cur = root;
// this.stack = [];
// };
// BSTIterator.prototype.next = function() {
// while (this.cur) {
// this.stack.push(this.cur);
// this.cur = this.cur.left;
// }
// this.cur = this.stack.pop();
// const ret = this.cur.val;
// this.cur = this.cur.right;
// return ret;
// };
// BSTIterator.prototype.hasNext = function() {
// return this.cur !== null || this.stack.length;
// };

View File

@ -0,0 +1,53 @@
/**
* Definition for a binary tree node.
* function TreeNode(val, left, right) {
* this.val = (val===undefined ? 0 : val)
* this.left = (left===undefined ? null : left)
* this.right = (right===undefined ? null : right)
* }
*/
/**
* @param {TreeNode} root
* @return {number[]}
* https://leetcode.cn/problems/binary-tree-right-side-view/?envType=study-plan-v2&envId=top-interview-150
*/
const rightSideView = function (root) {
};
/*
按照题目要求很容易想到利用层序遍历将每一层的最后一个结果保存进 ans这个数组最后返回ans就是我们所需要的答案
*/
function f1(root) {
const ans = []; // 储存结果的数组
const queue = []; // 层序遍历需要使用的队列
if (root) queue.push(root);
while (queue.length > 0) {
// 获取每一层的最后一个元素存入 ans
const n = queue.length;
ans.push(queue[n - 1].val);
// 将下一层元素压入队列
for (let i = 0; i < n; i++) {
const node = queue.shift();
if (node.left) {
queue.push(node.left);
}
if (node.right) {
queue.push(node.right);
}
}
}
return ans;
}
/*
思路2: 使用dfs优先遍右子树将每一层遇到的第一个节点储存到一个map中当遍历完所有元素按照层数返回map中的值即可
过程右子树DFS如果当前节点不为空,查看这一层是否已经有元素了如果没有那么这个值就是这一层的第一个元素将他
加入映射,
*/
// TODO:

View File

@ -0,0 +1,98 @@
/**
* Definition for a binary tree node.
* function TreeNode(val, left, right) {
* this.val = (val===undefined ? 0 : val)
* this.left = (left===undefined ? null : left)
* this.right = (right===undefined ? null : right)
* }
*/
/**
* @param {TreeNode} root
* @return {number}
*/
const countNodes = function (root) {
};
/*
思路以任何方式遍历一遍统计个数
*/
function f1(root) {
let count = 0;
// 中序遍历递归函数
const inTarverse = (node) => {
// 如果root为空直接返回
if (!node) return;
// 调用左子树
inTarverse(node.left);
// 统计当前节点
count++;
// 调用右子树
inTarverse(node.right);
};
inTarverse(root);
return count;
}
/*
除了遍历所有节点统计个数还可以利用完全二叉树个数与位置的关系通过二分查找来判断节点的个数分为两步
第一步首先获得这棵完全二叉树的高度通过高度我们可以判断一颗完全二叉树的节点数量范围[2^n, 2^n - 1]个节点
第二步节点个数的二进制和位置右某种关系[1,2,3]来看这个例子首先根节点也就是第一个节点那么它的二进制就是
1再来看第二个2,二进制位103,位11这还不够明显再加四个数[1,2,3,4,5,6,7],4->100,5->101,6->110,7->111,这个规律就是
二进制位决定了这个节点从根节点到这个位置的路径7->111第一个1表示的是根节点第二个1表示这个节点在根节点的右子树第三个1
表示的是在更节点的右节点的右节点
过程编写一个exists 判断指定位置的节点是否在root中存在,接收三个参数根节点root树的高度指定要判断的节点
*/
/**
*
* @param {*} root 根节点
* @param {*} h 完全二叉树的高度
* @param {*} k 最后一层的某一个节点 [2^n, 2^n - 1]
*/
const exists = (root, h, k) => {
// 初始bits位 1 << (h-1)假设h是3,那么默认值位100如果我们要查找第12个数是否存在就可以通过 1100 & 0100来
// 判断是在右子树还是在左子树
let bits = 1 << (h - 1);
let node = root;
while (bits) {
if (bits & k) { // bits&k不等于0表示这个节点应该往右子树查找
node = root.right;
} else { // 反之在左子树
node = root.left;
}
bits >>= 1;
}
// 判断node是否等于空
return node === null;
};
function f2(root) {
if (!root) return 0;
// 查找树的深度
let h = 0;
let node = root.left;
while (node) {
h++;
node = node.left;
}
// 通过树的深度计数出数量的范围 low和high
let low = 2 ** h;
let high = 2 ** (h + 1) - 1;
// 二分查找知道low===high结束查找, 以h=3来思考[815]
while (low < high) {
// 计数出中间位置
const mid = low + Math.floor((high - low) / 2);
if (exists(root, h, mid)) {
// 如果这个树存在那么节点个数大于等于这个数将左边界移动到mid
low = mid;
} else {
high = mid - 1;
}
}
// 此时low和high会相等这个位置就是二叉树的个数
return low;
}

View File

@ -0,0 +1,84 @@
/**
* Definition for a binary tree node.
* function TreeNode(val, left, right) {
* this.val = (val===undefined ? 0 : val)
* this.left = (left===undefined ? null : left)
* this.right = (right===undefined ? null : right)
* }
*/
/**
* https://leetcode.cn/problems/invert-binary-tree/?envType=study-plan-v2&envId=top-interview-150
* @param {TreeNode} root
* @return {TreeNode}
*/
const invertTree = function (root) {
};
/*
利用递归来抽象问题交换一颗树的左右子树不就可以做到翻转了函数f1只做单一的功能就是反转
一颗二叉树递归调用就可以翻转从根节点开始的整棵树
*/
function f1(root) {
if (!root) return; // 如果当前节点为空,直接返回
const left = root.left;
const right = root.right;
// 交换左右两棵树的位置
root.left = right;
root.right = left;
// 递归调用
f1(root.left);
f1(root.right);
}
/*
使用bfs来翻转一颗树
*/
function f2(root) {
if (!root) return root;
const queue = [root]; // bfs所需要的队列
while (queue.length > 0) {
// 从队列中取出节点实际顺序为从上到下从左到右BFS)
const node = queue.shift();
// 交换左右两个节点
const tmp = node.left;
node.left = node.right;
node.right = tmp;
// 如果左子节点存在,加入队列
if (node.left) queue.push(node.left);
// 如果右子节点存在,加入队列
if (node.right) queue.push(node.right);
}
return root;
}
/*
使用DFS来翻转一颗树
*/
function f3(root) {
if (!root) return root;
const stack = [root]; // dfs所需要的栈
while (stack.length > 0) {
// 从栈中取出节点DFS
const node = stack.pop();
// 交换左右两棵子树
const tmp = node.left;
node.left = node.right;
node.right = tmp;
// 如果字节的不为空压入栈继续dfs,
if (node.left) stack.push(node.left);
if (node.right) stack.push(node.right);
}
return root;
}

View File

@ -0,0 +1,90 @@
/**
* Definition for a binary tree node.
* function TreeNode(val) {
* this.val = val;
* this.left = this.right = null;
* }
*/
/**
* @param {TreeNode} root
* @param {TreeNode} p
* @param {TreeNode} q
* @return {TreeNode}
* https://leetcode.cn/problems/lowest-common-ancestor-of-a-binary-tree/?envType=study-plan-v2&envId=top-interview-150
*/
const lowestCommonAncestor = function (root, p, q) {
};
/*
利用dfs来遍历每一节点根据定义如果节点x是p和q的最近的公共祖先无外乎就下面几种情况
1. p在x的左子树q在x的右子树反之一样 lson && rson
2. p就是x那么q一定存在 lson rson, (x===p||x===q) && (lson || rson)
在遍历的时候只要满足上面条件的节点就是要寻找的最近公共祖先
*/
function f1(root, p, q) {
let ans;
/*
dfs遍历只干一件事就是判断root表示的这个数是否有p或者q
*/
const dfs = (root, p, q) => {
// 如果当前节点为空返回false
if (!root) return false;
// 查找左右子树是否包含q或q
const lson = dfs(root.left, p, q);
const rson = dfs(root.right, p, q);
// 判断当前节点是否是最近公共祖先
if ((lson && rson) || ((root === p || root === q) && (lson || rson))) {
ans = root;
}
// 返回函数判断
return lson || rson || root === p || root === q;
};
dfs(root, p, q);
return ans;
}
/*
遍历所有的节点给他们设置对应的父节点之后不停的回溯q的父节点并且把它们存入到一个set中之后再回溯p节点的父节点如果p节点的父节点
在set中那么这个节点就是最近公共祖先
*/
function f2(root, p, q) {
const parentMap = new Map(); // 储存父节点映射
const visited = new Set(); // 记录已访问的节点
// 深度优先搜索DFS来构建父节点映射
const dfs = (node) => {
if (!node) return;
if (node.left) {
parentMap.set(node.left, node); // 记录左节点的父节点
dfs(node.left); // 继续递归
}
if (node.right) {
parentMap.set(node.right, node); // 记录右节点的父节点
dfs(node.right); // 继续递归
}
};
// 执行DFS来构建父节点映射
dfs(root);
// 追溯p的路径并标记
while (p) {
visited.add(p); // 标记p节点
p = parentMap.get(p); // 跳到p的父节点
}
// 追溯q的路径并查找是否有交点
while (q) {
if (visited.has(q)) return q; // 找到交点返回q
q = parentMap.get(q); // 跳到q的父节点
}
return null; // 如果没有交点返回null
}

View File

@ -0,0 +1,48 @@
/**
* Definition for a binary tree node.
* function TreeNode(val, left, right) {
* this.val = (val===undefined ? 0 : val)
* this.left = (left===undefined ? null : left)
* this.right = (right===undefined ? null : right)
* }
*/
/**
* @param {TreeNode} root
* @return {number[]}
* https://leetcode.cn/problems/average-of-levels-in-binary-tree/?envType=study-plan-v2&envId=top-interview-150
*/
const averageOfLevels = function (root) {
};
/*
直接层序遍历之后对每一层的数据求和再求平均数(保留五位小数)
*/
function f1(root) {
const queue = [root];
const ans = [];
while (queue.length > 0) {
const n = queue.length;
let sum = 0; // 每一层数据的和
// 遍历每一层的所有数据,对它们求和
for (let i = 0; i < n; i++) {
const node = queue.shift();
sum += node.val;
// 将左右子节点存入下一层
if (node.left) queue.push(node.left);
if (node.right) queue.push(node.right);
}
// 求平均数,保留五位小数,并存入结果集
ans.push(Number((sum / n).toFixed(5)));
}
return ans;
}
/*
使用DFS来遍历利用一个数组levelData保存如下数据数组元素的下标表示每一层的信息元素为一个对象{sum: 这层元素的和, count: 这层元素个数}
dfs的时候如果当前层数level小于leveData.length表示这是新层的第一个数据就levelData.push({sum: curNode.val, count:1})
*/
// TODO:

View File

@ -0,0 +1,5 @@
export default function TreeNode(val, left, right) {
this.val = (val === undefined ? 0 : val);
this.left = (left === undefined ? null : left);
this.right = (right === undefined ? null : right);
}

View File

@ -0,0 +1,25 @@
/**
* @param {number} n - a positive integer
* @return {number} - a positive integer
*/
/**
* @param {number} n - a positive integer
* @return {number} - reversed bits result
*/
const reverseBits = function (n) {
let res = 0;
// 一共处理32位
for (let i = 0; i < 32; i++) {
// 提取最低位0或1
const bit = n & 1;
// 把结果左移一位为新加入的bit腾位置
res = (res << 1) | bit;
// 原数字右移一位(无符号),处理下一位
n >>>= 1;
}
return res >>> 0; // 确保返回的是无符号数(防止变负)
};

View File

@ -0,0 +1,63 @@
/**
* @param {string} a
* @param {string} b
* @return {string}
*/
const addBinary = function (a, b) {
};
/*
直接利用位运算只算进位加法和只算不进位加法得到得结果求和就是要求得结果投机
取巧使用数字快速运算被大数卡住了f1只作为思路扩张
*/
function f1(a, b) {
// 将二进制字符串转换为十进制整数
let x = parseInt(a, 2);
let y = parseInt(b, 2);
// 使用位运算求和
while (y !== 0) {
const sum = x ^ y; // 不带进位的加法
const carry = (x & y) << 1; // 进位
x = sum;
y = carry;
}
// 把结果转换成二进制字符串
return x.toString(2);
}
/*
模拟加法操作从个位开始加由于各位没有人给我进位所以初始进位carry位0每一位相加再加上它得进位
计算本位和进位知道没有进位和ab所有位处理完毕
*/
function f2(a, b) {
let i = a.length - 1; // 指针 i 指向 a 的最后一位(最低位)
let j = b.length - 1; // 指针 j 指向 b 的最后一位(最低位)
let carry = 0; // 初始化进位为 0
const res = []; // 用于存储结果的数组(从低位到高位)
// 循环条件:只要还有位未处理,或还有进位,就继续
while (i >= 0 || j >= 0 || carry !== 0) {
// 取当前位的值,如果越界则视为 0
const bitA = i >= 0 ? +a[i] : 0;
const bitB = j >= 0 ? +b[j] : 0;
// 本位加法:两个二进制位 + 上一轮进位
const sum = bitA + bitB + carry;
// sum % 2 得到当前位的结果(因为二进制满 2 进 1
res.push(sum % 2);
// 计算下一轮的进位(如果 sum >= 2 则进位为 1
carry = Math.floor(sum / 2);
// 移动指针,处理更高位
i--;
j--;
}
// 最后结果数组是反着存的,需要翻转后拼接成字符串
return res.reverse().join('');
}

View File

@ -0,0 +1,38 @@
/**
* Definition for a binary tree node.
* function TreeNode(val, left, right) {
* this.val = (val===undefined ? 0 : val)
* this.left = (left===undefined ? null : left)
* this.right = (right===undefined ? null : right)
* }
*/
/**
* @param {number[]} nums
* @return {TreeNode}
*/
const sortedArrayToBST = function (nums) {
};
function TreeNode(val, left, right) {
this.val = (val === undefined ? 0 : val);
this.left = (left === undefined ? null : left);
this.right = (right === undefined ? null : right);
}
/*
用数组的中间值作为根节点然后递归的将左半部分和右半部分处理成一颗二叉搜索树并设置成根节点的左右子树
*/
function f1(left, right, nums) {
// 如果left > right, 说明这个区间没有元素直接返回null
if (left > right) return null;
// 计算中间节点的索引
const mid = left + Math.floor((right - left) / 2);
// 创建根节点,或子树的根节点
const root = new TreeNode(nums[mid]);
// 递归处理左半部分和右半部分
root.left = f1(left, mid - 1, nums);
root.right = f1(mid + 1, right, nums);
return root;
}

View File

@ -0,0 +1,166 @@
/**
* Definition for singly-linked list.
* function ListNode(val, next) {
* this.val = (val===undefined ? 0 : val)
* this.next = (next===undefined ? null : next)
* }
*/
function ListNode(val, next) {
this.val = (val === undefined ? 0 : val);
this.next = (next === undefined ? null : next);
}
/**
* @param {ListNode} head
* @return {ListNode}
*/
const sortList = function (head) {
};
/*
利用归并排序的思想将链表分成两半分别对两半进行排序然后合并两个已排序的链表
*/
function f1(head) {
if (!head || !head.next) return head;
let tail = head;
while (tail.next) {
tail = tail.next;
}
return merge(head, tail);
}
/*
将一个链表归并成一个有序列表具体过程可以参考归并排序不好描述但不是很难
*/
function merge(head, tail) {
if (head === tail) return head;
// 快慢指针计算中间节点
let low = head;
let fast = head;
while (fast.next && fast.next.next) {
low = low.next;
fast = fast.next.next;
}
const mid = low;
// 将链表从中间截断
const head2 = mid.next;
mid.next = null;
// 将截断后的两个链表继续进行merge操作之后将其合并成一个有序的链表返回
return mergeList(merge(head, mid), merge(head2, tail));
}
/*
合并两个有序链表
*/
function mergeList(l1, l2) {
// 至少一个为空的情况
if (!l1 || !l2) {
// 返回其中任意一个不为空的情况
return l1 || l2;
}
// 两个链表都不为空将两个链表中的所有节点按大小拼接到dummy上最后返回dummy.next
const dummy = new ListNode(0);
let cur = dummy;
while (l1 && l2) {
if (l1.val <= l2.val) {
cur.next = l1;
l1 = l1.next;
} else {
cur.next = l2;
l2 = l2.next;
}
cur = cur.next;
}
// 当一个链表处理完毕还会右一个列表存在节点将它拼接到cur.next上
cur.next = l1 === null ? l2 : l1;
return dummy.next;
}
/*
优化上面的写法思路一致但是只传入一个参数head表示一个链表的头节点
*/
function f2(head) {
// 如果链表为空或者只有一个节点直接返回head
if (head === null || head.next === null) {
return head;
}
// 通过快慢指针寻找中间节点,将链表分成左右两部分
let slow = head;
let fast = head;
let prev = null;
while (fast && fast.next) {
prev = slow;
slow = slow.next;
fast = fast.next.next;
}
// 把链表从中间断开
prev.next = null;
const left = f2(head);
const right = f2(slow);
return mergeList(left, right);
}
/*
利用快速排序的思想将链表分成三部分left, pivot, right,left表示小于pivot的部分right表示大于pivot的部分,最后将它们拼接起来
*/
function quickSortList(head) {
if (!head || !head.next) return head;
const pivot = head;
const leftDummy = new ListNode(0);
const rightDummy = new ListNode(0);
let leftCur = leftDummy;
let rightCur = rightDummy;
let cur = head.next;
// 分组:< pivot 到 left≥ pivot 到 right
while (cur) {
if (cur.val < pivot.val) {
leftCur.next = cur;
leftCur = leftCur.next;
} else {
rightCur.next = cur;
rightCur = rightCur.next;
}
cur = cur.next;
}
// 截断,避免串联成环
leftCur.next = null;
rightCur.next = null;
pivot.next = null; // 断开 pivot 和旧链表的联系
const leftSorted = quickSortList(leftDummy.next);
const rightSorted = quickSortList(rightDummy.next);
// 拼接三段leftSorted + pivot + rightSorted
return concat(leftSorted, pivot, rightSorted);
}
// 拼接:将 left + pivot + right 接成一个链表并返回头结点
function concat(left, pivot, right) {
let head = pivot;
if (left) {
head = left;
let tail = left;
while (tail.next) {
tail = tail.next;
}
tail.next = pivot;
}
pivot.next = right;
return head;
}

View File

@ -0,0 +1,112 @@
/**
* Definition for singly-linked list.
* function ListNode(val, next) {
* this.val = (val===undefined ? 0 : val)
* this.next = (next===undefined ? null : next)
* }
*/
/**
* @param {ListNode[]} lists
* @return {ListNode}
*/
const mergeKLists = function (lists) {
};
function ListNode(val, next) {
this.val = (val === undefined ? 0 : val);
this.next = (next === undefined ? null : next);
}
/*
定义一个minIndex变量来记录当前最小头节点的索引
在每次循环中遍历lists数组找到当前最小头节点的索引minIndex
如果找到的minIndex为-1说明没有更多的节点可以合并跳出循环
如果找到的minIndex不为-1将当前最小头节点添加到合并后的链表中
然后将lists[minIndex]指向下一个节点继续下一轮循环
*/
/**
* 合并k个升序链表
* @param {ListNode[]} lists
* @return {ListNode}
*/
function f1(lists) {
const dummy = new ListNode(0);
let current = dummy;
// 遍历lists找到最小的头节点
while (true) {
let minIndex = -1;
for (let i = 0; i < lists.length; i++) {
if (lists[i] && (minIndex === -1 || lists[i].val < lists[minIndex].val)) {
minIndex = i; // 更新最小头节点的索引
}
}
if (minIndex === -1) break; // 没有找到最小的头节点
current.next = lists[minIndex]; // 拼接到dummy节点上
current = current.next; // 移动到下一个节点
lists[minIndex] = lists[minIndex].next; // 移动到下一个头节点
}
return dummy.next; // 返回合并后的链表
}
/*
定义一个ans表示要返回的链表然后将lists中的链表挨个和它合并最终返回ans
*/
function f2(lists) {
let ans = null;
for (let i = 0; i < lists.length; i++) {
ans = mergeLists(ans, lists[i]);
}
return ans;
}
/*
使用分治法
*/
function f3(lists) {
return merge(lists, 0, lists.length - 1);
}
/**
* @param {ListNode[]} lists 链表数组
* @param {number} l 要进行合并的左边界
* @param {number} r 要进行合并的右边界
* merge函数会将l到r范围的链表合并成一个新链表
*/
function merge(lists, l, r) {
// 如果l和r相等表明这个范围只有一个链表直接返回
if (l === r) return lists[l];
if (l > r) return null;
// 将l到r的范围拆分成左右两部分等这两部分merge之后再合并它们
const mid = Math.floor((l + r) / 2);
return mergeLists(merge(lists, l, mid), merge(lists, mid + 1, r));
}
function mergeLists(l1, l2) {
if (l1 === null || l2 === null) {
return l1 === null ? l2 : l1;
}
const dummy = new ListNode(0);
let cur = dummy;
while (l1 && l2) {
if (l1.val <= l2.val) {
cur.next = l1;
l1 = l1.next;
} else {
cur.next = l2;
l2 = l2.next;
}
cur = cur.next; // 移动到下一个节点
}
cur.next = l1 === null ? l2 : l1; // 将剩余的节点拼接到链表后面
return dummy.next; // 返回合并后的链表
}

View File

@ -0,0 +1,93 @@
/* eslint-disable max-len */
// eslint-disable-next-line no-underscore-dangle
function _Node(val, isLeaf, topLeft, topRight, bottomLeft, bottomRight) {
this.val = val;
this.isLeaf = isLeaf;
this.topLeft = topLeft;
this.topRight = topRight;
this.bottomLeft = bottomLeft;
this.bottomRight = bottomRight;
}
/**
* @param {number[][]} grid
* @return {_Node}
*/
const construct = function (grid) {
};
/*
利用分治算法我们很容易发现当啊n=1时grid一定可以构建成一个叶子节点val=当前这个位置表示的值当n=2时也即是由四个n=1的grid组成的
我们把它们划分好然后查看这左上有伤左下右下的这四个四叉树节点的值是否都是叶子节点如果都是再判断它们的值是否相等如果是叶子节点
并且它们的值也相等就把他们合成一个更大的叶子节点否则返回这个有叶子节点的四叉树
*/
function f1(grid) {
const n = grid.length;
return buildNode(grid, 0, n - 1, 0, n - 1);
}
/**
*
* @param {*} grid
* @param {*} start
* @param {*} end
*/
function buildNode(grid, rStart, rEnd, cStart, cEnd) {
// 如果划分的grid只有一个值那么这个值就是四叉树的叶子节点
if (rStart === rEnd) {
return new _Node(grid[rStart][cStart], 1);
}
// 划分四个区域构建子节点
const topLeft = buildNode(grid, rStart, rStart + Math.floor((rEnd - rStart) / 2), cStart, cStart + Math.floor((cEnd - cStart) / 2));
const topRight = buildNode(grid, rStart, rStart + Math.floor((rEnd - rStart) / 2), cStart + Math.floor((cEnd - cStart) / 2) + 1, cEnd);
const bottomLeft = buildNode(grid, rStart + Math.floor((rEnd - rStart) / 2) + 1, rEnd, cStart, cStart + Math.floor((cEnd - cStart) / 2));
const bottomRight = buildNode(grid, rStart + Math.floor((rEnd - rStart) / 2) + 1, rEnd, cStart + Math.floor((cEnd - cStart) / 2) + 1, cEnd);
// 如果四个节点都是叶子节点,并且值相同将它们合并成一个更大的叶子节点
if (topLeft.isLeaf && topRight.isLeaf && bottomLeft.isLeaf && bottomRight.isLeaf && topLeft.val === topRight.val && topRight.val === bottomLeft.val && bottomLeft.val === bottomRight.val) {
return new _Node(topLeft.val, 1);
}
// 构建一个包含四个节点的非叶子节点
return new _Node(1, 0, topLeft, topRight, bottomLeft, bottomRight);
}
/*
优化分治思路
*/
function buildNodeBest(grid) {
const n = grid.length;
function dfs(r0, c0, size) {
// 判断是否整个区域都是同一个值
const val = grid[r0][c0];
let same = true;
for (let i = r0; i < r0 + size; i++) {
for (let j = c0; j < c0 + size; j++) {
if (grid[i][j] !== val) {
same = false;
break;
}
}
if (!same) break;
}
if (same) {
return new Node(val === 1, true, null, null, null, null);
}
const half = size / 2;
return new Node(
true,
false,
dfs(r0, c0, half), // topLeft
dfs(r0, c0 + half, half), // topRight
dfs(r0 + half, c0, half), // bottomLeft
dfs(r0 + half, c0 + half, half), // bottomRight
);
}
return dfs(0, 0, n);
}

View File

@ -0,0 +1,79 @@
/**
* https://leetcode.cn/problems/container-with-most-water/?envType=study-plan-v2&envId=top-interview-150
* @param {number[]} height
* @return {number}
*/
const maxArea = function (height) {
};
/*
暴力解法组合所有的可能得出最大的面积,会超时
*/
function f1(height) {
const n = height.length;
let mArea = -Infinity;
let cArea;// 计算的面积
for (let i = 0; i < n; i++) {
for (let j = i + 1; j < n; j++) {
cArea = Math.min(height[i], height[j]) * (j - i);
if (cArea > mArea) mArea = cArea;
}
}
return mArea;
}
/*
使用双指针定义一个left和一个right首先判断height[left] height[right]两者直接谁底因为盛水量是底的那一端决定的
计算当前值并且使用mArea保存起来这个值之后继续移动地的那一侧假设height[right]比height[left]那么就right--然后继续
比较反之left++循环条件为left < right
*/
function f2(height) {
let mArea = -Infinity; // 能盛水的最大面积
let cArea; // 缓存计算的面积,避免重复计算
let left = 0;
let right = height.length - 1;
while (left < right) {
cArea = Math.min(height[left], height[right]) * [right - left];
if (cArea > mArea) mArea = cArea;
if (height[right] > height[left]) { // 如果右端比左端高,移动底的那一段
left++;
} else {
right--;
}
}
return mArea;
}
/*
优化假设当前右边比左边高那么我们应该移动左边面积不仅仅是由两端高度较低的那一端决定的同时也由right - left这个底部决定
在移动的过程中这个底部一定是不断变小的所以当这个决定性的最短边小于之前的最短边那就没必要计算了100%小于之前的结果所以
应该定义一个记录有效边高度的变量用来跳过不必要的判断以此提高小效率
*/
function f2(height) {
let mArea = -Infinity; // 能盛水的最大面积
let cArea; // 缓存计算的面积,避免重复计算
let left = 0;
let right = height.length - 1;
while (left < right) {
cArea = Math.min(height[left], height[right]) * (right - left);
if (cArea > mArea) mArea = cArea;
if (height[left] < height[right]) {
let prevLeft = height[left];
// 增加跳过无效left的逻辑
while (left < right && height[left] <= prevLeft) {
left++;
}
} else {
let prevRight = height[right];
// 增加跳过无效right的逻辑
while (left < right && height[right] <= prevRight) {
right--;
}
}
}
return mArea;
}

View File

@ -0,0 +1,60 @@
/**
* https://leetcode.cn/problems/valid-palindrome/?envType=study-plan-v2&envId=top-interview-150
* @param {string} s
* @return {boolean}
*/
const isPalindrome = function (s) {
};
/*
把符合要求的字符存入数组中之后判断反转的数组和原数组是否一致如果一致就是回文
*/
function f1(s) {
// 创建一个新的数组,用来存储有效字符(字母和数字)
const filteredChars = [];
// 遍历原字符串,将符合要求的字符存入新数组并转为小写
for (let i = 0; i < s.length; i++) {
const char = s[i];
if (/[a-zA-Z0-9]/.test(char)) {
filteredChars.push(char.toLowerCase());
}
}
// 将新数组转换为字符串,并与反转后的字符串比较
const reversedStr = filteredChars.slice().reverse().join('');
return filteredChars.join('') === reversedStr;
}
/*
利用双指针来判断是否符合要求首先创建左右两个指针判断当前字符是不是有效字符如果不是就跳过然后比较s[left] s[right]
如果不相等直接返回false如果循环遍历完毕都没问题那么就是一个回文字符串
思考奇偶会影响判断吗?
*/
function f2(s) {
let left = 0;
let right = s.length - 1;
while (left < right) {
// 跳过非字母数字字符
if (!(/[a-zA-Z0-9]/.test(s[left]))) {
left++;
continue;
}
if (!(/[a-zA-Z0-9]/.test(s[right]))) {
right--;
continue;
}
// 比较字符是否相等,忽略大小写
if (s[left].toLowerCase() !== s[right].toLowerCase()) {
return false; // 如果不相等返回false
}
left++;
right--;
}
return true; // 如果完全匹配返回true
}

View File

@ -0,0 +1,85 @@
/**
* https://leetcode.cn/problems/3sum/?envType=study-plan-v2&envId=top-interview-150
* @param {number[]} nums
* @return {number[][]}
*/
const threeSum = function (nums) {
};
/*
先对数组排序然后遍历数组如果遍历的当前元素大于零则表示后面的元素不可能组合在一起和为0如果当前元素小于零那么就对剩余的所有元素
使用两数之和的方法寻找target=当前元素的相反数如果找到就返回这几个元素然后遍历下一个元素
*/
function f1(nums) {
nums.sort((a, b) => a - b); // 先对数组排序
const result = []; // 结果数组
for (let i = 0; i < nums.length; i++) {
// 如果当前元素大于零直接结束循环,后面的查找没意义
if (nums[i] > 0) break;
// 如果当前元素等于前面的元素也跳过
if (i > 0 && nums[i] === nums[i - 1]) continue;
// 按照两数之和的方法寻找nums[i] 的相反数也就是target == -nums[i]
let left = i + 1;
let right = nums.length - 1;
while (left < right) {
const target = nums[left] + nums[right];
if (target === -nums[i]) {
result.push([nums[i], nums[left], nums[right]]); // 把查到的结果放入结果集中
// 继续寻找往下找
// 跳过重复元素
while (left < right && nums[left] === nums[left + 1]) left++;
while (left < right && nums[right] === nums[right - 1]) right--;
left++;
right--;
continue;
}
if (target < -nums[i]) {
left++;
continue;
}
if (target > -nums[i]) {
right--;
continue;
}
}
}
return result;
}
/*
使用ifelse 减少判断逻辑
*/
function f2(nums) {
nums.sort((a, b) => a - b); // 先对数组排序
const result = []; // 结果数组
for (let i = 0; i < nums.length - 2; i++) {
// 如果当前元素大于零直接结束循环,后面的查找没意义
if (nums[i] > 0) break;
// 如果当前元素等于前面的元素也跳过
if (i > 0 && nums[i] === nums[i - 1]) continue;
let left = i + 1;
let right = nums.length - 1;
while (left < right) {
const sum = nums[i] + nums[left] + nums[right];
if (sum === 0) {
result.push([nums[i], nums[left], nums[right]]);
// 跳过重复元素
while (left < right && nums[left] === nums[left + 1]) left++;
while (left < right && nums[right] === nums[right - 1]) right--;
left++;
right--;
} else if (sum < 0) {
left++;
} else {
right--;
}
}
}
return result;
}

View File

@ -0,0 +1,27 @@
/**
* https://leetcode.cn/problems/two-sum-ii-input-array-is-sorted/?envType=study-plan-v2&envId=top-interview-150
* @param {number[]} numbers
* @param {number} target
* @return {number[]}
*/
const twoSum = function (numbers, target) {
};
/*
利用数组有序的特性使用首位指针不断的缩小范围找到可能的值
*/
function f1(numbers, target) {
let left = 0;
let right = numbers.length - 1;
const temp = Infinity;
while (left < right) {
const temp = numbers[left] + numbers[right];
if (temp == target) {
return [left + 1, right + 1];
} if (temp > target) {
right--;
} else { left++; }
}
return []; // 没有找到,不过这个题目测试用例保证一定有解
}

View File

@ -0,0 +1,39 @@
/**
* https://leetcode.cn/problems/valid-palindrome/description/?envType=study-plan-v2&envId=top-interview-150
* @param {string} s
* @param {string} t
* @return {boolean}
*/
const isSubsequence = function (s, t) {
};
/*
直接遍历直接遍历t 检查当前s的第一个字符是否在遍历的过程中找到了如果找到了就指向s的第二个字符继续找找到全部找到 返回true
*/
function f1(s, t) {
if (s === t) return true;
let i = 0; // 指向s中要查找的字符
for (const char of t) {
if (char === s[i]) i++;
if (i === s.length) return true;
}
return false;
}
/*
利用传统for提高效率
*/
function f2(s, t) {
if (s === t) return true;
let i = 0; // 指向s中要查找的字符
let j = 0; // 指向t中当前遍历的字符
for(let j = 0;j<t.length,t++) {
}
for (const char of t) {
if (char === s[i]) i++;
if (i === s.length) return true;
}
return false;
}

View File

@ -0,0 +1,74 @@
/**
* @param {number[][]} triangle
* @return {number}
*/
const minimumTotal = function (triangle) {
};
/*
思路
这是一个典型的从上到下的动态规划问题三角形的每个位置只能由上一层的相邻两个位置转移而来
定义状态
- dp[i][j] 表示从顶点走到位置 (i, j) 的最小路径和
状态转移方程
- 左边界dp[i][0] = dp[i-1][0] + triangle[i][0]
- 中间dp[i][j] = min(dp[i-1][j-1], dp[i-1][j]) + triangle[i][j]
- 右边界dp[i][i] = dp[i-1][i-1] + triangle[i][i]
初始化
- dp[0][0] = triangle[0][0]
返回值
- 最后一层中的最小值 Math.min(...dp[n - 1])
*/
function f1(triangle) {
const n = triangle.length;
const dp = Array.from({ length: n }, (_, i) => Array(i + 1).fill(0));
dp[0][0] = triangle[0][0];
for (let i = 1; i < n; i++) {
const len = triangle[i].length;
// 左边界,只能从上一行第一个元素来
dp[i][0] = dp[i - 1][0] + triangle[i][0];
// 中间部分,来自上一行的[j - 1] 和 [j]
for (let j = 1; j < len - 1; j++) {
dp[i][j] = Math.min(dp[i - 1][j - 1], dp[i - 1][j]) + triangle[i][j];
}
// 右边界,只能从上一行最后一个元素来
dp[i][len - 1] = dp[i - 1][len - 2] + triangle[i][len - 1];
}
return Math.min(...dp[n - 1]);
}
/*
不重复计算len - 1
*/
function f2(triangle) {
const n = triangle.length;
const dp = Array.from({ length: n }, (_, i) => Array(i + 1).fill(0));
dp[0][0] = triangle[0][0];
for (let i = 1; i < n; i++) {
const len = triangle[i].length;
const last = len - 1;
// 左边界,只能从上一行第一个元素来
dp[i][0] = dp[i - 1][0] + triangle[i][0];
// 中间部分,来自上一行的[j - 1] 和 [j]
for (let j = 1; j < last; j++) {
dp[i][j] = Math.min(dp[i - 1][j - 1], dp[i - 1][j]) + triangle[i][j];
}
// 右边界,只能从上一行最后一个元素来
dp[i][last] = dp[i - 1][last - 1] + triangle[i][last];
}
return Math.min(...dp[n - 1]);
}

View File

@ -0,0 +1,95 @@
/**
* @param {string} s
* @param {string[]} wordDict
* @return {boolean}
*/
const wordBreak = function (s, wordDict) {
};
/*
使用回溯法不停的尝试wordict中的单词是否可以构造s
*/
function f1(s, wordDict) {
let result = false;
const n = s.length;
const backtrack = (start) => {
if (start === n) {
result = true;
return;
}
if (result) return; // 剪枝:一旦找到了就不再继续递归
for (const word of wordDict) {
const len = word.length;
if (start + len > n) continue;
let matched = true;
for (let i = 0; i < len; i++) {
if (s[start + i] !== word[i]) {
matched = false;
break;
}
}
if (matched) {
backtrack(start + len);
}
}
};
backtrack(0);
return result;
}
/*
利用动态规划定义dp[i]表示s中的前i个字符是否能由wordDict组成dp[0]=true,前i个字符是否可以由
wordDict组成可以从第i个字符开始往前数如果和wordDict中的某一个字符匹配并且 dp[i-word.length]==true
那么 dp[i]===true由dp[i]的定义可知dp[s.length]表示s的前n个字符整个字符串是否可以由wordDict中
的字符组成
*/
function f2(s, wordDict) {
// 定义dp表dp[i] 表示s中的前i个字符是否能由wordDict组成
const dp = Array(s.length + 1).fill(false);
dp[0] = true;
for (let i = 1; i < dp.length; i++) {
// 检测末尾是否匹配
let cur = i - 1;
let flag = true;
for (const word of wordDict) {
for (let j = word.length - 1; j >= 0; j--, cur--) {
if (s[cur] !== s[j] || cur < 0) {
flag = false;
break;
}
}
if (i >= word.length && flag && dp[i - word.length]) {
dp[i] = true;
break;
}
}
}
return dp[s.length];
}
/*
f2优化写法
*/
function f3(s, wordDict) {
const dp = Array(s.length + 1).fill(false);
dp[0] = true;
for (let i = 1; i <= s.length; i++) {
for (const word of wordDict) {
const len = word.length;
if (i >= len && dp[i - len] && s.slice(i - len, i) === word) {
dp[i] = true;
break;
}
}
}
return dp[s.length];
}

View File

@ -0,0 +1,38 @@
/**
* @param {number[]} nums
* @return {number}
*/
const rob = function (nums) {
};
/*
设dp[i]为偷取第i家时获取的最大利润那么dp[i]可以由两种情况得来
1.偷第i家如果偷第i家那么最大利润为 dp[i] = dp[i-2] + 第i家拥有的金额
2.不偷第i家如果不偷第i家那么最大利润为 dp[i-1]的最大利润
那么只需比较上面两种情况谁大就是dp[i]的值即dp[i] = Max(dp[i-1], dp[i-2] + nums[i])
*/
function f1(nums) {
const n = nums.length;
const dp = Array(n);
dp[0] = nums[0];
dp[1] = Math.max(nums[0], nums[1]);
for (let i = 2; i < dp.length; i++) {
dp[i] = Math.max(dp[i - 2] + nums[i], dp[i - 1]);
}
return dp[n - 1];
}
/*
使用双指针优化
*/
function f2(nums) {
let prev = 0;
let curr = 0;
for (const num of nums) {
const temp = Math.max(curr, prev + num);
prev = curr;
curr = temp;
}
return curr;
}

View File

@ -0,0 +1,30 @@
/**
* @param {number[]} nums
* @return {number}
*/
const lengthOfLIS = function (nums) {
};
/*
设dp[i]为以nums[i]结尾的递增子序列的最长长度,那么dp[i]nums[i]可以接在nums[0-i)中比它小的元素
组成子序列求那个结果最长的就是dp[i],即dp[i] = Max(nums[0-i)) +1
*/
function f1(nums) {
const n = nums.length;
const dp = Array(n).fill(1);
dp[0] = 1;
for (let i = 1; i < n; i++) {
for (let j = 0; j < i; j++) {
if (nums[j] < nums[i]) {
// 更新dp[i]为最长子序列
dp[i] = Math.max(dp[j] + 1, dp[i]);
}
}
}
console.log(dp);
return dp[n - 1];
}
f1([1, 3, 6, 7, 9, 4, 10, 5, 6]);

View File

@ -0,0 +1,51 @@
/**
* @param {number[]} coins
* @param {number} amount
* @return {number}
*/
const coinChange = function (coins, amount) {
};
/*
这个问题可以抽象成一个完全背包问题物品的价值就是硬币的面值物品的重量也是硬币的面值定义dp[i][j]表示0-i类型的硬币
组合成j所需硬币的数量
*/
function f1(coins, amount) {
const m = coins.length;
const n = amount;
const dp = Array.from({ length: m + 1 }, () => Array(n + 1).fill(Infinity));
// 初始化金额为0硬币数为0
for (let i = 0; i <= m; i++) {
dp[i][0] = 0;
}
for (let i = 1; i <= m; i++) {
const coin = coins[i - 1];
for (let j = 1; j <= n; j++) {
if (j < coin) {
dp[i][j] = dp[i - 1][j]; // 当前硬币无法选,用上一行的结果
} else {
// 可以选当前硬币(完全背包,不是 i-1
dp[i][j] = Math.min(dp[i - 1][j], dp[i][j - coin] + 1);
}
}
}
return dp[m][n] === Infinity ? -1 : dp[m][n];
}
// 上面的代码可以使用一维dp来实现dp[i]表示找i元需要最少的硬币
function coinChange(coins, amount) {
const dp = Array(amount + 1).fill(Infinity);
dp[0] = 0; // 金额为 0 时需要 0 个硬币
for (let coin of coins) {
for (let j = coin; j <= amount; j++) {
dp[j] = Math.min(dp[j], dp[j - coin] + 1);
}
}
return dp[amount] === Infinity ? -1 : dp[amount];
}

View File

@ -0,0 +1,41 @@
/**
* @param {number} n
* @return {number}
*/
const climbStairs = function (n) {
};
/*
这个题目就是求斐波那契数列的现实抽象我们的最终目的是跳到第n个台阶而每一次要么跳一步要么跳两步
所以智能由n-1个台阶跳一步上来或者由n-2个台阶跳两步上来设dp[i]为跳转到这个台阶可能的方法那么
dp[i] = dp[i-1] + dp[i-2]
*/
function f1(n) {
// 定义dp数组
const dp = Array(n + 1);
dp[1] = 1;
dp[2] = 2;
for (let i = 3; i <= n; i++) {
dp[i] = dp[i - 1] + dp[i - 2];
}
return dp[n];
}
/*
通过观察上面的代码可以知道dp[i]的结果只由它的前一个位置和前前个位置有关所以定义两个变量first和second来
保存这两个值
*/
function f2(n) {
if (n < 3) return n;
// 定义跳到前两个台阶可能的步数
let first = 1;
let second = 2;
let result = 1;
for (let i = 3; i <= n; i++) {
result = first + second;
first = second;
second = result;
}
return result;
}

View File

@ -0,0 +1,123 @@
/**
* @param {character[][]} board
* @return {void} Do not return anything, modify board in-place instead.
*/
const solve = function (board) {
};
/*
题目要求我们把被X围绕的O区域修改成X所以只需要找出所有没有被X围绕的O区域把这一部分区域修改成A,最后遍历整个矩阵把所有为O的区域
修改成X所有为A的区域修改成O,所有在边上的O,及其相邻的O一定是没有被包围的所以我们只需要遍历四条边上的O把它们修改成A即可这里使用
dfs
*/
function f1(board) {
// 如果矩阵为空(虽然测试用例保证 1<=m,n
if (board.length === 0 || board[0].length === 0) return;
// 获取矩阵的行m和列n,用于后续遍历
const m = board.length;
const n = board[0].length;
// 遍历矩阵的第一列和最后一列
for (let i = 0; i < m; i++) {
dfs(i, 0);
dfs(i, n - 1);
}
// 遍历矩阵的第一行和最后一行
for (let j = 0; j < n; j++) {
dfs(1, j);
dfs(m - 1, j);
}
// 遍历所有矩阵将O修改成X,将A修改成O
for (let i = 0; i < m; i++) {
for (let j = 0; j < n; j++) {
if (board[i][j] === 'O') board[i][j] = 'X';
if (board[i][j] === 'A') board[i][j] = 'O';
}
}
/*
dfs遍历,将O修改为A
注意board直接找外部作用域无需传递
*/
let dfs = (x, y) => {
// 如果越界或者board[x][y] !== 'O'直接return
if (x < 0 || y < 0 || x === m || y === n || board[x][y] !== 'O') return;
board[x][y] = 'A'; // 将O修改成A
// 递归处理四周的O
dfs(board, x - 1, y);
dfs(board, x, y - 1);
dfs(board, x + 1, y);
dfs(board, x, y + 1);
};
}
/*
思路和上面一致使用广度优先遍历BFS
*/
function f2(board) {
// 定义查找偏移量
const dx = [1, -1, 0, 0];
const dy = [0, 0, 1, -1];
// 如果矩阵为空(虽然测试用例保证 1<=m,n
if (board.length === 0 || board[0].length === 0) return;
// 获取矩阵的行m和列n,用于后续遍历
const m = board.length;
const n = board[0].length;
const queue = []; // 广度遍历的队列,是一个二维数组,储存下标[row, col] 表示几行几列
// 遍历第一行和最后一行如果发现O就将他修改成A并且将它四周的位置压入队列继续处理
for (let i = 0; i < n; i++) {
if (board[0][i] === 'O') {
board[0][i] = 'A';
queue.push([0, i]);
}
if (board[m - 1][i] === 'O') {
board[m - 1][i] = 'A';
queue.push([m - 1, i]);
}
}
// 处理第一列和最后一列,这里需要注意四个角落的元素可以不处理,为了统一,这一我就不做处理,对结果不影响
for (let i = 0; i < m; i++) {
if (board[i][0] === 'O') {
board[i][0] = 'A';
queue.push([i, 0]);
}
if (board[i][n - 1] === 'O') {
board[i][n - 1] = 'A';
queue.push([i, n - 1]);
}
}
// 开始广度遍历
while (queue.length > 0) {
const [x, y] = queue.pop(); // 获取位置坐标
// 通过偏移量寻找四周为O的位置将其修改为A然后将其压入栈中继续先四周寻找
for (let i = 0; i < 4; i++) {
const mx = x + dx[i];
const my = y + dy[i];
// 如果越界 或 不等于O直接跳过
if (mx < 0 || mx >= m || y < 0 || my >= n || board[mx][my] !== 'O') continue;
board[mx][my] = 'A';
queue.push([mx, my]);
}
}
// 遍历所有矩阵将O修改成X,将A修改成O
for (let i = 0; i < m; i++) {
for (let j = 0; j < n; j++) {
if (board[i][j] === 'O') board[i][j] = 'X';
if (board[i][j] === 'A') board[i][j] = 'O';
}
}
}

View File

@ -0,0 +1,85 @@
/**
* // Definition for a _Node.
* function _Node(val, neighbors) {
* this.val = val === undefined ? 0 : val;
* this.neighbors = neighbors === undefined ? [] : neighbors;
* };
* https://leetcode.cn/problems/clone-graph/?envType=study-plan-v2&envId=top-interview-150
*/
/**
* @param {_Node} node
* @return {_Node}
*/
const cloneGraph = function (node) {
};
// eslint-disable-next-line no-underscore-dangle, no-unused-vars
function _Node(val, neighbors) {
this.val = val === undefined ? 0 : val;
this.neighbors = neighbors === undefined ? [] : neighbors;
}
/*
只给了我们一个连通图的一个节点叫我们克隆整个图根据两图图的定义从这个节点出发我们能到达途中的任意一个节点(顶点)
所以我们只需要遍历所有的neighbor然后顺着邻居继续遍历邻居即可遍历整个图但是这里有一个问题就是如果通过邻居到达了
自己或者邻居的邻居是自己的邻居就会发生循环这里就遇到使用一种数据结果来缓存已经遍历过的邻居当再次遇到它时跳过就行
抽象过程你需要统计小王家人你只认识小王于是题通过小王认识了它的爸爸妈妈把他们加入到统计表然后小王的爸爸向你介绍了
它的妹妹同时小王向你介绍了它的姑姑但是你发现这两个人是同一个人所以你只统计了一次最后你就获得了所有和小王有关的人
*/
function f1(node) {
const visited = new Map(); // 储存原节点,和克隆节点的映射
/**
*
* @param {*} node 需要克隆的节点
* @returns 返回克隆的节点
*/
function clone(node) {
if (!node) return node;
// 如果这个节点之前克隆过,直接返回其克隆节点
if (visited.has(node)) return visited.get(node);
// 创建这个节点的克隆节点
const cloneNode = new _Node(node.val, []);
// node与cloneNode建立映射
visited.set(node, cloneNode);
// 为克隆节点克隆邻居节点
for (const neighbor of node.neighbors) {
cloneNode.neighbors.push(clone(neighbor));
}
return cloneNode;
}
return clone(node);
}
/*
思路和上面一致但是使用广度优先遍历
*/
function f2(node) {
if (!node) return node;
const visited = new Map();
const queue = [node]; // 用于广度遍历
const cNode = new _Node(node.val, []);
visited.set(node, cNode); // 克隆第一个节点将其存入visited
while (queue.length > 0) {
const n = queue.shift();
// 遍历这个节点的所有邻居如果没有访问过将其clone并加入visited
for (const neighbor of n.neighbors) {
if (!visited.has(neighbor)) {
visited.set(neighbor, new _Node(neighbor.val, []));
queue.push(neighbor);
}
visited.get(n).neighbors.push(visited.get(neighbor));
}
}
return cNode;
}

View File

@ -0,0 +1,116 @@
/**
* @param {character[][]} grid
* @return {number}
* https://leetcode.cn/problems/number-of-islands/?envType=study-plan-v2&envId=top-interview-150
*/
const numIslands = function (grid) {
};
/*
使用深度优先遍历在character[][]这个二维数组中我们认为相邻的1之间都有一条边首先遍历这个二维数组从遇到的第一个为1的位置递归
的开始向四周查找其他位置是否也为1如果是的话就继续寻找并且把找过的位置设置为0,计数加一即可先当与图中有几个连通分量
*/
function f1(grid) {
/*
递归函数向grid[r][c]四周继续递归寻找为1的位置并把它设置成0,当整个为1位置都为0表示整个岛屿的小时
*/
const dfs = (grid, r, c) => {
// 如果r,c超出边界或者grid[r][c] === 0 表示岛屿的边界结束
if (r < 0 || c < 0 || r >= nr || c >= nc || grid[r][c] === '0') return;
// 将当前位置设置为0表示已经访问过
grid[r][c] = '0';
// 递归的查找四周
dfs(grid, r - 1, c); // 上
dfs(grid, r, c + 1); // 右
dfs(grid, r + 1, c); // 下
dfs(grid, r, c - 1); // 左
};
// 如果邻接矩阵为空或者没有任何数据直接返回0
if (!grid || grid.length === 0) return 0;
let nr = grid.length; // 获取行的个数
let nc = grid[0].length; // 获取列的个数
let lands = 0; // 岛屿数量
for (let r = 0; r < nr; ++r) {
for (let c = 0; c < nc; ++c) {
if (grid[r][c] === '1') { // 发现陆地,必定有一座岛,把相邻的陆地全部清空
lands++;
dfs(grid, r, c); // 从当前点开始DFS
}
}
}
return lands;
}
/*
使用BFS思路和DFS一致
*/
function f2(grid) {
// 获取网格的行数
const nr = grid.length;
if (nr === 0) return 0; // 如果网格为空返回0岛屿
// 获取网格的列数
const nc = grid[0].length;
// 记录岛屿的数量
let num_islands = 0;
// 遍历每个格子
for (let r = 0; r < nr; r++) {
for (let c = 0; c < nc; c++) {
// 如果当前格子是陆地('1'),说明找到了一个新的岛屿
if (grid[r][c] === '1') {
// 增加岛屿数量
num_islands++;
// 将当前格子标记为已访问,防止再次访问
grid[r][c] = '0';
// 创建一个队列来进行BFS
const neighbors = [];
neighbors.push([r, c]); // 将当前岛屿的起始点入队列
// 进行BFS遍历岛屿中的所有陆地
while (neighbors.length > 0) {
// 获取队列的第一个元素
const [row, col] = neighbors.shift();
// 检查上下左右四个方向的相邻格子
// 上
if (row - 1 >= 0 && grid[row - 1][col] === '1') {
neighbors.push([row - 1, col]);
grid[row - 1][col] = '0'; // 将相邻陆地标记为水
}
// 下
if (row + 1 < nr && grid[row + 1][col] === '1') {
neighbors.push([row + 1, col]);
grid[row + 1][col] = '0'; // 将相邻陆地标记为水
}
// 左
if (col - 1 >= 0 && grid[row][col - 1] === '1') {
neighbors.push([row, col - 1]);
grid[row][col - 1] = '0'; // 将相邻陆地标记为水
}
// 右
if (col + 1 < nc && grid[row][col + 1] === '1') {
neighbors.push([row, col + 1]);
grid[row][col + 1] = '0'; // 将相邻陆地标记为水
}
}
}
}
}
// 返回岛屿的数量
return num_islands;
}
/*
利用并查集矩阵里面所有的1都认为是一个小岛屿用一个一维数组parent表示并查集 grid[i][j] === '1' 这个位置为例那么这个
位置在并查集中的下标就是parent[i*col+j] col表示矩阵的列数而parent[i * col + j]表示的是它的父节点默认指向自己定义一个遍历count
记录岛屿的数量
*/
// TODO:

View File

@ -0,0 +1,42 @@
/**
* @param {number} numCourses
* @param {number[][]} prerequisites
* @return {boolean}
*/
const canFinish = function (numCourses, prerequisites) {
};
/*
这个题目是一个典型的拓扑排序问题只需要判断拓扑排序最终能否输出所有元素如果可以则表明是一个有向无环图符合
题目要求否则不符合
*/
function f1(numCourses, prerequisites) {
const graph = Array.from({ length: numCourses }, () => []);
const inDegree = Array(numCourses).fill(0);
// 构建图和入度表
for (const [a, b] of prerequisites) {
graph[b].push(a);
inDegree[a]++;
}
// 找出入度为零的顶点,将其加入队列
const queue = [];
for (let i = 0; i < numCourses; i++) {
if (inDegree[i] === 0) queue.push(i);
}
// 从队列中输出所有入度为零的顶点,如果输出的顶点数量和课程数量一致,表明可以拓扑排序,否则有环
let count = 0;
while (queue.length > 0) {
const node = queue.shift();
count++;
for (const neighbor of graph[node]) {
inDegree[neighbor]--;
if (inDegree[neighbor] === 0) queue.push(neighbor);
}
}
return count === numCourses;
}

View File

@ -0,0 +1,94 @@
/**
* 课程安排 II - 拓扑排序DFS
* @param {number} numCourses - 课程总数
* @param {number[][]} prerequisites - 每一对 [a, b] 表示上课程 a 需要先完成课程 b
* @return {number[]} - 返回一个可行的课程学习顺序如果无法完成所有课程则返回 []
*/
const findOrder = function (numCourses, prerequisites) {
return dfsTopoSort(numCourses, prerequisites);
};
/**
* 使用 DFS 实现拓扑排序并判断是否存在环
* @param {number} numCourses
* @param {number[][]} prerequisites
* @return {number[]}
*/
function dfsTopoSort(numCourses, prerequisites) {
const graph = Array.from({ length: numCourses }, () => []); // 邻接表表示图
const visited = Array(numCourses).fill(0); // 节点状态0=未访问1=访问中2=已完成
const result = []; // 用于存放拓扑排序结果(逆序)
let hasCycle = false; // 用于标记图中是否存在环
// 构建图b -> a 表示先学 b 才能学 a
for (const [a, b] of prerequisites) {
graph[b].push(a);
}
/**
* 深度优先搜索当前节点
* @param {number} node
*/
function dfs(node) {
if (visited[node] === 1) {
// 当前节点正在访问中,说明存在环
hasCycle = true;
return;
}
if (visited[node] === 2 || hasCycle) {
// 节点已处理,或已发现环,直接返回
return;
}
visited[node] = 1; // 标记为访问中
for (const neighbor of graph[node]) {
dfs(neighbor);
}
visited[node] = 2; // 标记为已完成
result.push(node); // 所有邻居处理完后再加入结果,确保当前节点排在后面
}
// 遍历所有节点,防止图不连通
for (let i = 0; i < numCourses; i++) {
dfs(i);
}
// 如果存在环,则无法完成所有课程
return hasCycle ? [] : result.reverse();
}
/*
使用kahn算法来实现一个顶点能从其他顶点过来表明有一个入度有多少个顶点能到达这个顶点就有多少个入度这里直接copy207
修改一下即可
*/
function f2(numCourses, prerequisites) {
const graph = Array.from({ length: numCourses }, () => []);
const inDegree = Array(numCourses).fill(0);
const result = [];
// 构建图和入度表
for (const [a, b] of prerequisites) {
graph[b].push(a);
inDegree[a]++;
}
// 找出入度为零的顶点,将其加入队列
const queue = [];
for (let i = 0; i < numCourses; i++) {
if (inDegree[i] === 0) queue.push(i);
}
// 从队列中输出所有入度为零的顶点,如果输出的顶点数量和课程数量一致,表明可以拓扑排序,否则有环
while (queue.length > 0) {
const node = queue.shift();
// 将入度为零的顶点存入结果集合
result.push(node);
for (const neighbor of graph[node]) {
inDegree[neighbor]--;
if (inDegree[neighbor] === 0) queue.push(neighbor);
}
}
// 如果结果集中的顶点的数量小于numCourses那么表明有环放回[],否则返回结果集
return result.length < numCourses ? [] : result;
}

View File

@ -0,0 +1,96 @@
/**
* @param {string[][]} equations
* @param {number[]} values
* @param {string[][]} queries
* @return {number[]}
*/
const calcEquation = function (equations, values, queries) {
};
/*
思路通过equations和values建立一张有权图edges:[[[1, 2],...], [[2, 3],...]],在这里使用邻接表的方式表示edges表示的是0号顶点可以到一号顶点
并且权值为21顶点到2号顶点的权值是3如果0号顶点表示a,1号顶点表示b,2号顶点表示c那么这个图在这个题目里面的意思就是a/b=2,b/c=2,如果要我们
求a/c的值不就是把a->b->c这个路径里面的权值相乘吗所以这个题目可以分为下面几步
步骤
1.通过equationsvalues建立有权图edges
2.遍历queries,比如要我们求a/e那么我们就从a为起点出发开始广度优先遍历知道找到a->e的路径之后加权相乘如果没找到就是-1
*/
function f1(equations, values, queries) {
// Step 1: 给每个变量编号,变量名映射到编号
let nvars = 0; // 变量的数量
const variables = new Map(); // 存储变量名到编号的映射
// 处理 equations给每个变量一个唯一的编号
const n = equations.length;
for (let i = 0; i < n; i++) {
// 如果变量没有编号,就给它一个编号
if (!variables.has(equations[i][0])) {
variables.set(equations[i][0], nvars++);
}
if (!variables.has(equations[i][1])) {
variables.set(equations[i][1], nvars++);
}
}
// Step 2: 创建图结构,存储每个节点与其他节点的比值
const edges = new Array(nvars).fill(0); // 创建一个数组来存储每个节点的邻接点和比值
for (let i = 0; i < nvars; i++) {
edges[i] = []; // 初始化每个节点的邻接表
}
// 遍历 equations构建图每条边的权值是它们之间的比值
for (let i = 0; i < n; i++) {
const va = variables.get(equations[i][0]); // 获取变量 a 的编号
const vb = variables.get(equations[i][1]); // 获取变量 b 的编号
edges[va].push([vb, values[i]]); // a → b 的比值
edges[vb].push([va, 1.0 / values[i]]); // b → a 的比值是 1 / (a → b)
}
// Step 3: 处理查询,计算每个查询的结果
const queriesCount = queries.length; // 查询的数量
const ret = []; // 存储每个查询的结果
for (let i = 0; i < queriesCount; i++) {
const query = queries[i]; // 当前查询
let result = -1.0; // 默认结果为 -1.0,表示无法计算
// 只有当查询中的两个变量都存在时,才进行计算
if (variables.has(query[0]) && variables.has(query[1])) {
const ia = variables.get(query[0]); // 获取查询中第一个变量的编号
const ib = variables.get(query[1]); // 获取查询中第二个变量的编号
// 如果两个变量是同一个,直接返回 1.0
if (ia === ib) {
result = 1.0;
} else {
// Step 4: 使用 DFS 或 BFS 遍历图,查找从 ia 到 ib 的比值
const points = []; // 存储当前正在遍历的节点
points.push(ia); // 从 ia 开始遍历
const ratios = new Array(nvars).fill(-1.0); // 存储每个节点的比值,初始值为 -1.0,表示不可达
ratios[ia] = 1.0; // 起点 ia 到 ia 的比值是 1.0
// 使用 DFS 方式遍历图
while (points.length && ratios[ib] < 0) {
const x = points.pop(); // 从栈中取出一个节点
// 遍历当前节点的所有邻接点
for (const [y, val] of edges[x]) {
// 如果邻接点 y 尚未访问过ratios[y] < 0
if (ratios[y] < 0) {
ratios[y] = ratios[x] * val; // 更新 y 的比值
points.push(y); // 将 y 加入栈中继续遍历
}
}
}
result = ratios[ib]; // 查询的结果是 ib 到达的比值
}
}
// 将当前查询的结果存入结果数组
ret[i] = result;
}
// 返回所有查询的结果
return ret;
}

View File

@ -0,0 +1,129 @@
/**
* @param {number[][]} board
* @return {number}
*/
const snakesAndLadders = function (board) {
};
/*
/*
把棋盘的每一个位置看成图的一个顶点每一个顶点都有六个有向边指向后面的六个位置也就是后面的六个顶点有的顶点比较特殊
它不指向后面的六个顶点而是指向其他的顶点我们要求的就是到达最后一个位置的顶点所需的最少步数到这里我们很容易发现
是一个图的BFS题目我们从最初的位置一层一层的往外扩需要用几层就是几步
*/
function f1(board) {
// 定义一个长度为n*n+1的一维数组将board压缩减少计数难度
const n = board.length;
const size = n * n;
const arr = new Array(size + 1); // 0位置不需要这样就能和board一一对应
let idx = 1; // 棋盘初始位置
let leftToRight = true; // s行遍历board
for (let i = n - 1; i >= 0; i--) {
if (leftToRight) {
for (let j = 0; j < n; j++) {
arr[idx++] = board[i][j];
}
} else {
for (let j = n - 1; j >= 0; j--) {
arr[idx++] = board[i][j];
}
}
leftToRight = !leftToRight;
}
// bfs队列从第一个位置开始[1,0]表示,到达第一个位置最少只需要一步
const queue = [[1, 0]];
const visited = new Array(size + 1).fill(false); // 防止每一层的元素重复加入到下一层
visited[1] = true;
while (queue.length) {
const [cur, step] = queue.shift();
// 查看邻接的六个顶点
for (let i = 1; i <= 6; i++) {
let next = cur + i; // 下一个顶点位置
// 越界,忽略这个顶点
if (next > size) {
continue;
}
// 如果遇到蛇和梯子立即调整指定位置
if (arr[next] !== -1) {
next = arr[next];
}
// 如果邻接顶点就是目标位置直接放回到达当前顶点的步数在加1
if (next === size) {
return step + 1;
}
// 如果元素没有被访问将其加入队列,扩展下一层
if (!visited[next]) {
visited[next] = true;
queue.push([next, step + 1]);
}
}
}
return -1; // 不可达
}
/*
将二维数组变成一维数组需要log(n*n)的时间复杂度n为棋牌大小相等于一开始就遍历了整个棋盘一遍如果能实时计算当前位置到其他位置
在board中的位置可以大大减少时间复杂度
思考假设我们有一个大小为n的棋牌计数位置place(1 ~ n*n)在board中的坐标首先计算行行非常好计算r = (place - 1) / n,那么列
还是 (place-1) % n吗在这个题目不是的因为棋牌是按照s行走的偶数行从左往右奇数行从右往左偶数行显然是(place-1)%n那么技术行只需要将n
减去它即可
*/
const id2rc = (id, n) => {
const r = Math.floor((id - 1) / n);
let c = (id - 1) % n;
if (r % 2 === 1) {
c = n - 1 - c;
}
return [n - 1 - r, c];
};
function f2(board) {
const n = board.length;
const size = n * n;
// BFS
const queue = [[1, 0]];
const visited = new Array(size + 1).fill(false); // 防止每一层的元素重复加入到下一层
visited[1] = true;
while (queue.length) {
const [cur, step] = queue.shift();
// 查看邻接的六个顶点
for (let i = 1; i <= 6; i++) {
let next = cur + i; // 下一个顶点位置
// 越界,忽略这个顶点
if (next > size) {
continue;
}
const [r, c] = id2rc(next, n);
// 如果遇到蛇和梯子立即调整指定位置
if (board[r][c] !== -1) {
next = board[r][c];
}
// 如果邻接顶点就是目标位置直接放回到达当前顶点的步数在加1
if (next === size) {
return step + 1;
}
// 如果元素没有被访问将其加入队列,扩展下一层
if (!visited[next]) {
visited[next] = true;
queue.push([next, step + 1]);
}
}
}
return -1; // 不可达
}

View File

@ -0,0 +1,58 @@
/**
* https://leetcode.cn/problems/longest-consecutive-sequence/?envType=study-plan-v2&envId=top-interview-150
* @param {number[]} nums
* @return {number}
*/
const longestConsecutive = function (nums) {
};
/*
思路题目所给的数组是无序的所以直接对数组排序依次遍历整个数组如果当前元素大于前一个元素并且等于前一个元素加1
把统计的长度加1如果不满足就比较是否比最大长度还大是的话就把这个长度作为为最大长度
*/
function f1(nums) {
nums.sort((a, b) => a - b);
if (nums.length === 0) return 0;
if (nums.length === 1) return 1;
let maxLen = 1; // 最大长度
let curLen = 1; // 统计长度
for (let i = 1; i < nums.length; i++) {
const preNum = nums[i - 1];
if (nums[i] === preNum) {
continue;
} else if (preNum + 1 === nums[i]) {
curLen++;
} else {
maxLen = Math.max(maxLen, curLen);
curLen = 1;
}
}
return Math.max(curLen, maxLen); // 如果后面一个很长的连续序列
}
/*
题目要求我们在O(n)的时间复杂度完成上面的做法使用了一次排序nlogn不符合要求正确的做法可以先用set对整个数组去重
整个过程的时间复杂度为O(n)之后遍历整个Set如果当前的num-1在set中不存在就表明这个num是数组中某个序列的开头开始统计
num+1在set中存在吗如果存在就计数加1如果不存在继续遍历
*/
function f2(nums) {
if (nums.length === 0) return 0;
const set = new Set(nums);
let maxLen = 1;
let curLen = 1;
for (const num of set) {
if (set.has(num - 1)) continue;
// 表明num是某个连续序列的开头直接开始统计
let next = num + 1;
while (set.has(next)) {
curLen++;
next++;
}
maxLen = Math.max(maxLen, curLen);
curLen = 1;
}
return maxLen;
}

View File

@ -0,0 +1,21 @@
/**
*https://leetcode.cn/problems/two-sum/?envType=study-plan-v2&envId=top-interview-150
* @param {number[]} nums
* @param {number} target
* @return {number[]}
*/
const twoSum = function (nums, target) {
};
/*
创建一个mapmap的key为target - num, value为下标遍历nums如果当前num在map中存在就返回[map.get(num), i]i表示当前
下标
*/
function f1(nums, target) {
const map = new Map();
for (let i = 0; i < nums.length; i++) {
if (map.has(nums[i])) return [map.get(nums[i]), i];
map.set(target - nums[i], i);
}
}

View File

@ -0,0 +1,71 @@
/**
*https://leetcode.cn/problems/happy-number/?envType=study-plan-v2&envId=top-interview-150
* @param {number} n
* @return {boolean}
*/
const isHappy = function (n) {
};
/*
根据快乐数的定义我们只需一直计数把计数的结果放到Set中如果在后续的计数中出现了这个结果说明它不是快乐数不能是1,所以要先
检测它是否是1如果是的话就返回
*/
function f1(n) {
// 使用set来记录出现过的数字
const seen = new Set();
// 循环直到满足条件
while (n !== 1) {
// 如果当前数字之前的某次计算出过,说明进入循环
if (seen.has(n)) return false;
// 将当前数字加入set
seen.add(n);
// 计算当前数字的每个位置的平方和
// n = n.toString().split('').reduce((sum, digit) => sum + Number(digit) ** 2, 0);
// 优化:通过取余和除法运算计算每一位的平方和
let sum = 0;
while (n > 0) {
const digit = n % 10;
sum += digit * digit;
n = Math.floor(n / 10);
}
n = sum;
}
return true;
}
/*
利用弗洛伊德环形检测算法
*/
function f2(n) {
let slow = n; // 慢指针
let fast = n; // 快指针
// 快慢指针循环
while (fast !== 1 && getSumOfSquares(fast) !== 1) { // getSumOfSquares(fast) !== 1直接提前判定慢指针
slow = getSumOfSquares(slow);
fast = getSumOfSquares(getSumOfSquares(fast));
if (slow === fast) { // 如果快慢指针相遇,说明出现了循环
return false;
}
}
return true;
}
/**
*
* @param {number} num 要计算各位平方和的数
* @returns 返回计算平方和的数
*/
function getSumOfSquares(num) {
let sum = 0;
while (num > 0) {
const digit = num % 10;
sum += digit * digit;
num = Math.floor(num / 10);
}
return sum;
}

View File

@ -0,0 +1,60 @@
/**
* https://leetcode.cn/problems/isomorphic-strings/?envType=study-plan-v2&envId=top-interview-150
* @param {string} s
* @param {string} t
* @return {boolean}
*/
const isIsomorphic = function (s, t) {
};
/*
s和t的长度是一致的s中的字符会按照某种关系映射到t比如: a->b, b->c,但是不会出现a->c, b->c这种多个字符映射同一个字符如果出现
这种情况直接返回false也不会出现a->b, a->c这种一个字符有多个映射的情况如果有直接返回false
思路 同时遍历s和ts[i]作为hashTable的keyt[i]作为hashTable的值如果s[i] 在hashTable中存在检查其对应的value是否和
t[i]一致如果不一致就出现了 a-b, a->c这种一个字符出现多映射的情况直接返回false最后遍历一遍hashtable检查是否存在多个
value相同的情况如果出现则表明出现了a-cb->c这种多个字符映射到同一个字符的情况
*/
function f1(s, t) {
const map = new Map();
for (let i = 0; i < s.length; i++) {
if (map.has(s[i])) {
if (map.get(s[i]) !== t[i]) return false;
} else {
map.set(s[i], t[i]);
}
}
// 利用set查看map的value是否有重复
const set = new Set();
// eslint-disable-next-line no-unused-vars
for (const [_, value] of map) {
if (set.has(value)) return false;
set.add(value);
}
return true;
}
/*
我们在处理a->b,a->c这种多映射的情况非常好处理一次遍历就行但是在遇到a->cb->c这种被多个字符映射的情况需要遍历
一遍map的所有value这无疑增加了复杂度所以可以再增加一个set用来储存哪些字符应该被映射过了如果出现了一个没有映射
的字符但是它要映射的那个字符在set中已经存在直接返回false英文出现了多个字符映射到同一个字符的情况
*/
function f2(s, t) {
// 创建两个map一个储存字符的映射一个储存哪些字符已经被映射过了
const charMap = new Map();
const hasBeenMap = new Set();
for (let i = 0; i < s.length; i++) {
if (charMap.has(s[i])) {
if (charMap.get(s[i]) !== t[i]) return false;
} else {
// 检查t[i]是否已经被映射
if (hasBeenMap.has(t[i])) return false;
charMap.set(s[i], t[i]); // 将映射关系加入 charMap
hasBeenMap.add(t[i]); // 将 t[i] 标记为已映射
}
}
return true;
}

View File

@ -0,0 +1,67 @@
/**
* https://leetcode.cn/problems/contains-duplicate-ii/?envType=study-plan-v2&envId=top-interview-150
* @param {number[]} nums
* @param {number} k
* @return {boolean}
*/
const containsNearbyDuplicate = function (nums, k) {
};
/*
定义两个指针表示abs(i - j) <= k的窗口i向右移动查看当前指向的值在set中是否存在不存在就把这个值存入set如果i-j >k 表明
当前太大了要缩小把j指向的元素从set中剔除掉i++,并将测j指向的值是否在set中存在
*/
function f1(nums, k) {
const set = new Set();
for (let i = 0, j = 0; i < nums.length; i++) {
// 如果 窗口太大,就缩小窗口再检测
if (i - j > k) {
set.delete(nums[j]);
j++;
}
// 检测 nums[i]是否在set中存在
if (set.has(nums[i])) return true;
// 将当前值存入set
set.add(nums[j]);
}
return false;
}
/*
优化上面代码中i-j<k的检测是多余的因为当i>k时i-j一定大于k,因为后续我们会一个值一个值的检测例如当k=3,i=4这个时候
一定要缩小串口直接剔除掉i-k-1表示的元素即可set中剩下的元素还是4个这样可以少用一个下标判断也更快
*/
function f2(nums, k) {
const set = new Set();
for (let i = 0; i < nums.length; i++) {
// 超出滑动窗口
if (i > k) {
set.delete(nums[i - k - 1]);
}
// 检测set中是否含有这个元素
if (set.has(nums[i])) return true;
set.add(nums[i]);
}
return false;
}
/*
直接一次遍历利用map储存之前遍历的所有值和下标如果当前值在map中存在并且当前值的下标减去map中对应值的小标结果小于等于k
则表明发生了重复
*/
function f3(nums, k) {
const map = new Map();
for (let i = 0; i < nums.length; i++) {
const num = nums[i];
if (map.has(num) && i - map.get(num) <= k) return true;
map.set(num, i);
}
return false;
}

View File

@ -0,0 +1,51 @@
/**
* https://leetcode.cn/problems/valid-anagram/?envType=study-plan-v2&envId=top-interview-150
* @param {string} s
* @param {string} t
* @return {boolean}
*/
const isAnagram = function (s, t) {
};
/*
判断s是否是t的异位词异位词就是重新排列字符顺序的词就是异位词所以异位词的长度和原本单词的长度一致同一个字符出现的次数也一致
题目的输入只包含26个小写字母所利用数组的下标表示t中出现的小写字母值为出现的次数之后遍历ss中的每个单词会使其对应的计数减少
1如果出现某个计数小于0直接返回false
*/
function f1(s, t) {
if (s.length !== t.length) return false;
// 统计t中字母出现的频次
const charCounts = new Array(26).fill(0);
for (let i = 0; i < t.length; i++) {
charCounts[t.charCodeAt(i) - 97]++;
}
for (let i = 0; i < s.length; i++) {
const index = s.charCodeAt(i) - 97;
charCounts[index]--;
if (charCounts[index] < 0) return false;
}
return true;
}
/*
处理unicode字符js中有一个坑使用t[i]取unicode字符时如果这个字符是一个代理对比如emjoy表情就会使用两个unicode16表示
所以应该使用for of来处理应该字符串本身就是一个可迭代对象底层会保证它返回的字符是正确的
*/
function f2(s, t) {
if (s.length !== t.length) return false;
const map = new Map();
for (const ch of t) {
map.set(ch, (map.get(ch) || 0) + 1);
}
for (const ch of s) {
if (!map.has(ch)) return false;
map.set(ch, map.get(ch) - 1);
if (map.get(ch) < 0) return false;
}
return true;
}

View File

@ -0,0 +1,40 @@
/**
*https://leetcode.cn/problems/word-pattern/?envType=study-plan-v2&envId=top-interview-150
* @param {string} pattern
* @param {string} s
* @return {boolean}
*/
const wordPattern = function (pattern, s) {
};
/*
和205类似只不过pattern中的字符映射到s中是一个单词首先处理s把s中每一个单词分离出来用数组储存之后遍历pattern和这个数组如果这个
map中存在这个key就查看当前key对应的值是否和数组的值相等如果不相等就返回false, 如果出现多个字符映射同一个字符也返回false和205一致
这里不过多解释
*/
function f1(pattern, s) {
const map = new Map();
const words = s.split(' ');
if (pattern.length !== s.length) return false;
for (let i = 0; i < pattern.length; i++) {
if (map.has(pattern[i])) {
if (map.get(pattern[i]) !== words[i]) return false;
} else {
map.set(pattern[i], words[i]);
}
}
// 检测是否出现了多个字符映射同一个字符串
const set = new Set();
// eslint-disable-next-line no-unused-vars
for (const [_, value] of map) {
if (set.has(value)) return false;
set.add(value);
}
return true;
}
/*
参考205优化思路一致可以使用数组储存对应映射查找更快使用set保存已经被映射过的单词去除最后一次遍历
*/

View File

@ -0,0 +1,68 @@
/**
* https://leetcode.cn/problems/ransom-note/?envType=study-plan-v2&envId=top-interview-150
* @param {string} ransomNote
* @param {string} magazine
* @return {boolean}
*/
const canConstruct = function (ransomNote, magazine) {
};
/*
思路用hash表统计magazine中的字符出现的数量之后遍历ransomNote如果ransomNote中有字符不在这个hash表中说明magazine不能
构成ransomNote如果这个字符在magazine中存在就使其计数减1如果计数为零就从哈希表中删除这个字符
*/
function f1(ransomNote, magazine) {
const map = new Map(); // 利用hash计数
for (let i = 0; i < magazine.length; i++) {
map.set(magazine[i], (map.get(magazine[i]) || 0) + 1); // 如果map中有这个字符就在原有的基础上加1没有就设置成1
}
// 遍历ransomNote中的字符如果字符在map中不存在就返回false
for (let i = 0; i < ransomNote.length; i++) {
const char = ransomNote[i];
if (map.has(char)) {
map.set(char, map.get(char) - 1);
// 如果发现当前字符计数为零从map中删除这个字符
if (map.get(char) === 0) {
map.delete(char);
}
} else {
return false;
}
}
return true;
}
/*
使用数组来代替hash表因为题目所给的测试用例只有26个小写的英文字符可以使用一个长度为26的数组数组的第一个位置代表a,最后一个位置代表
z,每个位置的值代表这个字符在magazine中出现的位置
思考为什么使用数组会更快虽然hash查找操作时间复杂度理论上式O(1)但是hash计数到对应的链表查找数据还是需要花费时间的在数据量
大的时候可以认为是O(1),但是我们只需统计26个英文字符的数量直接使用数组数组在内存中是连续的通过偏移量查找非常快
*/
function f2(ransomNote, magazine) {
const charsCount = new Array(26).fill(0); // 统计magazine中每一个字符出现的次数
// 统计magazine中的字符
for (let i = 0; i < magazine.length; i++) {
const index = magazine.charCodeAt(i) - 97;
charsCount[index]++;
}
// 遍历ransomNote中的字符在charscount中是否存在
for (let i = 0; i < ransomNote.length; i++) {
const index = ransomNote.charCodeAt(i) - 97;
// if (charsCount[index] > 0) {
// charsCount[index]--;
// } else {
// return false;
// }
if (charsCount[index] === 0) return false;
charsCount[index]--;
}
return true;
}

View File

@ -0,0 +1,103 @@
/**
* https://leetcode.cn/problems/group-anagrams/?envType=study-plan-v2&envId=top-interview-150
* @param {string[]} strs
* @return {string[][]}
*/
const groupAnagrams = function (strs) {
};
/*
题目要求我们对字符串中的异位词分组首先创建一个map遍历strs中的所有字符串查看它是否是map中某一个key的异位词如果是的话
把它插入到这个key对应的数组中去如果不是的话表明他是一个新的字符串所以在map中创建一个映射用来收集它的异位词
*/
function f1(strs) {
const map = new Map();
const result = [];
for (let i = 0; i < strs.length; i++) {
let found = false;
// 查找 map 中是否有对应的异位词
for (const [key, value] of map) {
if (key.length === strs[i].length && isAnagram(strs[i], key)) {
value.push(strs[i]); // 如果是异位词,加入现有的数组
found = true; // 标记找到
break; // 找到就停止遍历
}
}
// 如果没有找到,就创建新的异位词组
if (!found) {
map.set(strs[i], [strs[i]]);
}
}
// 将 map 中的所有异位词数组收集到 result
for (const [, value] of map) {
result.push(value);
}
return result;
}
/*
利用字符序来判断异位词
*/
function f2(strs) {
const map = new Map();
const result = [];
// 遍历所有字符串
for (let i = 0; i < strs.length; i++) {
// 排序字符串,得到标准化形式
const sortedStr = strs[i].split('').sort().join('');
// 如果 map 中已经有这个排序后的字符串,就将当前字符串加入对应的数组
if (map.has(sortedStr)) {
map.get(sortedStr).push(strs[i]);
} else {
// 如果没有,创建新的映射
map.set(sortedStr, [strs[i]]);
}
}
// 将 map 中的所有异位词数组收集到 result
for (const [, value] of map) {
result.push(value);
}
return result;
}
// 检测两个词是否是异位词
function isAnagram(s, t) {
if (s.length !== t.length) return false;
const charCounts = new Array(26).fill(0);
for (let i = 0; i < s.length; i++) {
charCounts[s.charCodeAt(i) - 97]++;
charCounts[t.charCodeAt(i) - 97]--;
}
return charCounts.every((count) => count === 0);
}
/*
利用一个数组来储存str的字符计数如果计数相同就判断它们是异位词这个代码巧妙之处是直接把数组作为key,本质就是调用数组的toString()
之后拿这个string作为对象的keymap中不能直接使用因为map是通过引用来判断的即使内容一样也会判为不同的str如果非要要用map可以
使用 String(count)作为map的key
*/
function f3(strs) {
// eslint-disable-next-line no-new-object
const map = new Object();
for (const s of strs) {
const count = new Array(26).fill(0);
for (const c of s) {
count[c.charCodeAt() - 'a'.charCodeAt()]++;
}
// eslint-disable-next-line no-unused-expressions
map[count] ? map[count].push(s) : map[count] = [s];
}
return Object.values(map);
}

View File

@ -0,0 +1,24 @@
/**
* @param {number[]} nums
* @return {number}
*/
const maxSubArray = function (nums) {
return f1(nums);
};
/*
定义dp[i]表示以nums[i]结尾的最大子数组和那么dp[i] = Math.max(dp[i - 1] + nums[i], nums[i]),要么是前面最大的子数组
加上自己要么是自己本身
*/
function f1(nums) {
let max = nums[0];
const dp = new Array(nums.length).fill(0);
dp[0] = nums[0];
for (let i = 1; i < nums.length; i++) {
dp[i] = Math.max(dp[i - 1] + nums[i], nums[i]);
max = Math.max(max, dp[i]);
}
return max;
}

View File

@ -0,0 +1,35 @@
/**
* @param {number[]} nums
* @return {number}
*/
const maxSubarraySumCircular = function (nums) {
};
/*
最大子数组之和无外乎两种情况子数组在开头和结尾直接还有一种情况就是一部分在开头的前缀一部分在结尾的后缀
所以只需一次遍历统计前缀和的最大值和第一种情况的最大值之后再倒序遍历统计后缀的最大值之后比较这两种情况
*/
function f1(nums) {
const n = nums.length;
const leftMax = new Array(n).fill(0);
// 对坐标为 0 处的元素单独处理,避免考虑子数组为空的情况
leftMax[0] = nums[0];
let leftSum = nums[0];
let pre = nums[0];
let res = nums[0];
for (let i = 1; i < n; i++) {
pre = Math.max(pre + nums[i], nums[i]);
res = Math.max(res, pre);
leftSum += nums[i];
leftMax[i] = Math.max(leftMax[i - 1], leftSum);
}
// 从右到左枚举后缀,固定后缀,选择最大前缀
let rightSum = 0;
for (let i = n - 1; i > 0; i--) {
rightSum += nums[i];
res = Math.max(res, rightSum + leftMax[i - 1]);
}
return res;
}

Some files were not shown because too many files have changed in this diff Show More