Compare commits

...

10 Commits

38 changed files with 2505 additions and 0 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

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,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,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

@ -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;
}

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,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,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,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,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,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,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;
}

View File

@ -0,0 +1,61 @@
/**
* https://leetcode.cn/problems/minimum-size-subarray-sum/?envType=study-plan-v2&envId=top-interview-150
* @param {number} target
* @param {number[]} nums
* @return {number}
*/
const minSubArrayLen = function (target, nums) {
};
/*
题目要求我们找到最短的子数组子数组就是原数组中连续的某部分我们先遍历原数组之后在遍历到的位置开始往下遍历并且求和
如果结果大于等于target就返回这个长度
*/
function f1(target, nums) {
let minLen = Infinity; // 初始化最小长度
for (let left = 0; left < nums.length; left++) {
let sum = 0; // 保存当前子数组的和
for (let right = left; right < nums.length; right++) {
sum += nums[right];
if (sum >= target) {
minLen = Math.min(minLen, right - left + 1);
// 后面匹配的结果都会符合sum>=target但是一定比当前子数组长所以直接break
break;
}
}
}
return minLen === Infinity ? 0 : minLen; // 注意清理初始化的值如果没有符合要求的返回0而不是Infinity
}
function f2(target, nums) {
let minLen = Infinity; // 初始化最小长度
let left = 0; // 滑动窗口的左边界
let sum = 0; // 滑动窗口中所有值的和
for (let right = 0; right < nums.length; right++) {
sum += nums[right];
while (sum >= target) {
minLen = Math.min(minLen, right - left + 1);
// 如果靠近right的数值很大靠近left的数值很小会造成数组长度很长所以要从左缩小窗口
sum -= nums[left];
left++;
}
}
return minLen === Infinity ? 0 : minLen; // 注意清理初始化的值如果没有符合要求的返回0而不是Infinity
}
function f3(target, nums) {
let minLen = Infinity; // 初始化最小长度
let left = 0; // 滑动窗口的左边界
let sum = 0; // 滑动窗口中所有值的和
for (let right = 0; right < nums.length; right++) {
sum += nums[right];
while (sum >= target) {
minLen = Math.min(minLen, right - left + 1);
// 如果靠近right的数值很大靠近left的数值很小会造成数组长度很长所以要从左缩小窗口
sum -= nums[left];
left++;
}
}
return minLen === Infinity ? 0 : minLen; // 注意清理初始化的值如果没有符合要求的返回0而不是Infinity
}

View File

@ -0,0 +1,112 @@
/**
* https://leetcode.cn/problems/find-all-anagrams-in-a-string/
* @param {string} s
* @param {string} p
* @return {number[]}
*/
const findAnagrams = function (s, p) {
};
/*
按照暴力解法的话我们可能会把p的所有异位词都枚举出来之后遍历遍历所有的异位词利用indexof查看每一个异位词在s中的位置如果
这个异位词在s中存在那么就把起始下标放入到结果集中如果p非常长的话那么所有异位词就是一个长度的全排列!m,加上对s的查找那就是一个
(n+m)*!m, 这显然是不能接受的所以我们需要换一种思路.
思路异位词和原词在单词数量上是一致的所以我们可以维护一个在s上长度位p.length的窗口只要这个窗口里面所有单词出现的数量和
p中所有单词出现的数量一致的话那么就认为这个窗口表示的单词是p的一个异位词最后只需把窗口的起始下标放入结果集合即可
*/
function f1(s, p) {
const result = []; // 返回的结果集
if (s.length < p.length) return result; // p比s还长在s中不可能有p的异位词直接返回
const need = new Map(); // 记录p中每一个词出现的次数用于验证异位词
const window = new Map(); // 记录当前窗口中的异位词
// 统计p的所有字符出现的次数
for (const char of p) {
need.set(char, (need.get(char) || 0) + 1);
}
let left = 0; // 滑动窗口的左边界
let right = 0; // 滑动窗口的右边界
let valid = 0; // 滑动窗口中符合异位字的字符数量;
while (right < s.length) {
const char = s[right];
if (need.has(char)) {
window.set(char, (window.get(char) || 0) + 1);
if (window.get(char) === need.get(char)) {
valid++;
}
}
// 如果窗口长度等于异位字符p的长度那么就检测当前窗口是否是一个异位词
if (right - left + 1 === p.length) {
if (valid === need.size) {
result.push(left); // 把left添加到结果集中
}
const leftChar = s[left];
// 收缩窗口
left++;
// 判断收缩掉的这个字符是否是need中的某一个字符
if (need.has(leftChar)) {
if (window.get(leftChar) === need.get(leftChar)) {
valid--; // 验证符合异位词字符个数的词减1
}
window.set(leftChar, window.get(leftChar) - 1); // 使这个字符的数量减1
}
}
// 扩展窗口
right++;
}
return result;
}
function f3(s, p) {
const result = []; // 返回的结果集
if (s.length < p.length) return result; // p比s还长在s中不可能有p的异位词直接返回
const need = new Map(); // 记录p中每一个字符出现的次数用于验证异位词
const window = new Map(); // 记录当前窗口中的异位词
// 统计p的所有字符出现的次数
for (const char of p) {
need.set(char, (need.get(char) || 0) + 1);
}
let left = 0; // 滑动窗口的左边界
let right = 0; // 滑动窗口的右边界
while (right < s.length) {
const char = s[right];
// 扩展窗口
window.set(char, (window.get(char) || 0) + 1);
// 如果当前窗口大小达到p的长度判断是否为异位词
if (right - left + 1 === p.length) {
// 比较窗口内字符的频次是否和p中一致
let isAnagram = true;
for (const [key, value] of need) {
if (window.get(key) !== value) {
isAnagram = false;
break;
}
}
if (isAnagram) {
result.push(left); // 将符合条件的起始索引加入结果
}
// 收缩窗口
const leftChar = s[left];
window.set(leftChar, window.get(leftChar) - 1); // 减少左边字符的频次
if (window.get(leftChar) === 0) {
window.delete(leftChar); // 如果频次为0移除该字符
}
left++; // 移动左指针
}
// 扩展右指针
right++;
}
return result;
}