当前位置: 首页 > news >正文

JavaScript常见算法题分类

1. 哈希表

1.1. 两数之和

题目描述:给定一个整数数组 nums 和一个目标值 target,请你在该数组中找出和为目标值的那两个整数,并返回他们的数组下标。

解题思路:使用哈希表存储已经遍历过的数值和它们的下标。每遍历一个数,检查它的补数是否已经存在于哈希表中,若存在则返回。

代码实现:

function twoSum(nums: number[], target: number): number[] {const map = new Map<number, number>(); // 存储数值和下标的哈希表for (let i = 0; i < nums.length; i++) {const complement = target - nums[i]; // 计算补数if (map.has(complement)) { // 如果补数已经存在于哈希表中return [map.get(complement), i]; // 返回补数的下标和当前数的下标}map.set(nums[i], i); // 存入当前数和下标}return [];
}

2. 双指针

2.1. 最接近的三数之和

题目描述:给你一个长度为 n 的整数数组 nums 和一个目标值 target。请你从数组中选出三个整数,使它们的和与 target 最接近。返回这三个数的和。假定每组输入只存在唯一答案。

题目示例:

输入:nums = [-1, 2, 1, -4], target = 1
输出:2
解释:与目标值 1 最接近的和是 2 ,即 -1 + 2 + 1 = 2

思路分析:

1. 排序 + 双指针:这是解决此类问题的经典思路。首先我们对数组进行排序,然后利用双指针来缩小范围,找到最接近目标值的三数之和。

2. 步骤解析:

  • 先对数组进行排序,这样方便后续使用双指针。

  • 外层循环固定一个数,内层使用双指针扫描剩余的数,计算三数之和并比较与目标值的差值,记录最小差值。

  • 根据当前和与目标值的大小关系,移动双指针来缩小差距。

3. 终止条件:当找到一个和恰好等于目标值时,可以直接返回。

代码实现:

function threeSumClosest(nums: number[], target: number): number {// 首先对数组进行排序nums.sort((a, b) => a - b);// 初始化最接近的和为一个较大的数let closestSum = Number.MAX_SAFE_INTEGER;// 遍历数组,固定第一个数for (let i = 0; i < nums.length - 2; i++) {// 使用双指针遍历后面的两个数let left = i + 1;let right = nums.length - 1;while (left < right) {// 计算当前三数的和const currentSum = nums[i] + nums[left] + nums[right];// 如果找到与目标值完全相等的和,直接返回if (currentSum === target) {return currentSum;}// 比较当前的和与之前记录的最接近的和if (Math.abs(currentSum - target) < Math.abs(closestSum - target)) {closestSum = currentSum;  // 更新最接近的和}// 根据当前和与目标值的关系移动指针if (currentSum < target) {left++;  // 当前和小于目标值,移动左指针使和增大} else {right--;  // 当前和大于目标值,移动右指针使和减小}}}// 返回最终的最接近的三数之和return closestSum;
}

代码详解:

1. 排序:首先对数组进行升序排序,方便我们使用双指针。

nums.sort((a, b) => a - b);

2. 遍历数组:外层循环遍历数组,并且固定第一个数 nums[i],接着使用双指针 left 和 right。 

for (let i = 0; i < nums.length - 2; i++) {let left = i + 1;let right = nums.length - 1;

3. 计算和:在每次双指针遍历时,计算三个数的和 currentSum,并且检查它是否比之前记录的 closestSum 更接近目标值。

const currentSum = nums[i] + nums[left] + nums[right];

4. 更新最接近的和:如果 currentSum 与目标值更接近,就更新 closestSum。

if (Math.abs(currentSum - target) < Math.abs(closestSum - target)) {closestSum = currentSum;
}

5. 移动双指针:根据 currentSum 与 target 的关系,移动指针。如果当前和小于目标值,说明需要增加和,则移动左指针;反之,移动右指针。

if (currentSum < target) {left++;
} else {right--;
}

扩展思路:

  • 时间复杂度优化:当前算法的时间复杂度为 O(n^2),在面试中对于小规模的输入是可以接受的,但若输入数组很大,可能会超时。可以考虑使用其他更复杂的优化手段。

  • K Sum 问题:此题是经典的“三数之和”问题的变体,可以进一步扩展到 K Sum,如四数之和、五数之和可以通过增加递归层来实现。

2.2. 通过删除字母匹配到字典里最长单词

题目描述: 给你一个字符串 s 和一个字符串数组 dictionary,请你找出并返回 dictionary 中由 s 删除某些字符后可以形成的 最长单词。如果答案不止一个,返回字典序最小的那个单词。如果 dictionary 中没有单词可以由 s 删除一些字符后形成,返回空字符串。

题目示例:

输入:s = "abpcplea", dictionary = ["ale","apple","monkey","plea"]
输出:"apple"
解释:从字符串 s 中删除 "b" 和 "c",可以形成 "apple"。
输入:s = "abpcplea", dictionary = ["a","b","c"]
输出:"a"

思路分析:

1. 双指针匹配:遍历 dictionary 中的每个单词,使用双指针法来判断是否可以通过删除 s 中的某些字符形成该单词。对于每个字典单词,设一个指针 j 遍历它,另一个指针 i 遍历字符串 s,如果 s[i] == dictionary[j],那么两个指针都向前移动,直到匹配完整个单词或者 s 被遍历完。

2. 长度优先 + 字典序优先:对于每个可以匹配的单词,我们按照以下规则判断:

  • 优先选择长度最长的单词。

  • 如果有多个长度相同的匹配单词,选择字典序最小的。

代码实现:

function findLongestWord(s: string, dictionary: string[]): string {let result = "";// 遍历字典中的每一个单词for (let word of dictionary) {// 使用双指针来判断是否可以通过删除 s 中的某些字符形成该单词let i = 0, j = 0;while (i < s.length && j < word.length) {// 如果字符相同,两个指针都移动if (s[i] === word[j]) {j++;}i++; // s 的指针总是往前走}// 如果 j 能遍历完 word,说明 s 可以通过删除一些字符变成 wordif (j === word.length) {// 判断是否需要更新结果// 条件是:当前 word 的长度大于 result 或者相同长度时字典序更小if (word.length > result.length || (word.length === result.length && word < result)){result = word;}}}return result;
}

代码详解:

1. 初始化结果:初始时,result 为空字符串,用来存放当前找到的符合条件的最长单词。

for (let word of dictionary) {

2. 遍历字典:我们需要遍历 dictionary 中的每个单词,来检查它是否可以通过删除 s 中的某些字符形成。

while (i < s.length && j < word.length) {if (s[i] === word[j]) {j++;}i++;
}

3. 双指针判断匹配:

  • i 用于遍历字符串 s,j 用于遍历当前的字典单词 word。

  • 如果 s[i] === word[j],说明当前字符匹配,j 指针前进;无论是否匹配,i 都会前进,直到 s 遍历完。

4. 判断是否更新结果:如果 j === word.length,说明字典单词完全匹配,可以通过删除 s 中的一些字符得到。接下来,我们需要根据长度和字典序更新 result:

  • 如果当前的 word 比 result 长,直接更新。

  • 如果长度相同,则比较字典序,字典序小的更新。 

if (word.length > result.length || (word.length === result.length && word < result)) {result = word;
}

5. 返回结果:遍历完成后,返回最终找到的单词。

扩展思路:

  • 排序优化:可以在处理 dictionary 之前,先按长度降序、字典序升序进行排序,这样在遍历的时候,只要找到一个匹配单词就可以提前结束循环,提高效率。
  • 进一步优化:对于非常大的 dictionary,可以通过 Trie 树等数据结构来优化查找过程,但这需要较为复杂的实现。

2.3. 判断子序列

题目描述:给定字符串 s 和 t,判断 s 是否为 t 的子序列。一个字符串的 子序列 是指可以通过删除原字符串的一部分字符(可以不删除任何字符)而不改变剩余字符的相对顺序形成的新字符串。

题目示例:

// 示例1
输入: s = "abc", t = "ahbgdc"
输出: true
解释: s 是 t 的子序列// 示例2
输入: s = "axc", t = "ahbgdc"
输出: false
解释: s 不是 t 的子序列

思路分析:

1. 双指针法:

  • 使用两个指针,一个指向 s,一个指向 t,从头开始比较每个字符。

  • 如果 s[i] == t[j],则 i 和 j 同时后移,否则仅 j 后移。

  • 最终判断 i 是否能遍历完整个 s 字符串,若能则说明 s 是 t 的子序列。

2. 复杂度分析:

  • 时间复杂度:O(n),n 是 t 的长度,最坏情况下需要遍历整个 t。

  • 空间复杂度:O(1),只需要常数空间来存储指针。

代码实现:

function isSubsequence(s: string, t: string): boolean {let i = 0; // 指向 s 的指针let j = 0; // 指向 t 的指针// 当 t 还没有遍历完时进行判断while (i < s.length && j < t.length) {if (s[i] === t[j]) {// 若字符相同,移动 s 的指针i++;}// 每次都移动 t 的指针j++;}// 如果 i 最终能够遍历完整个 s,则说明 s 是 t 的子序列return i === s.length;
}// 测试用例
console.log(isSubsequence("abc", "ahbgdc")); // true
console.log(isSubsequence("axc", "ahbgdc")); // false

详细注释与讲解:

1. 变量定义:

  • i 是指向字符串 s 的指针,初始值为 0。

  • j 是指向字符串 t 的指针,初始值也为 0。

2. 核心逻辑:

  • 使用 while 循环遍历 t,在 i 尚未遍历完整个 s 时持续判断。

  • 在每一次迭代中,判断 s[i] 和 t[j] 是否相同:

    • 若相同,则 i++,即子序列成功匹配了一个字符,继续匹配下一个字符。

    • 若不同,只移动 t 的指针 j++,继续在 t 中寻找与 s[i] 相匹配的字符。

3. 返回结果:

  • 循环结束后,若 i === s.length,说明 s 中的所有字符都在 t 中找到并按照顺序匹配,返回 true;否则返回 false。

思考与扩展:

  • 进阶问题:可以考虑字符串 t 非常长而 s 较短的情况,这时可以通过提前处理 t(如构建一个索引表)来加速匹配过程,从而优化算法的时间复杂度。

2.4. 合并两个有序数组

题目描述:给你两个按非递减顺序排列的整数数组 nums1 和 nums2,另有两个整数 m 和 n,分别表示 nums1 和 nums2 中的元素数目。请你合并 nums2 到 nums1 中,使合并后的数组同样按非递减顺序排列。

注意:最终,合并后数组不应由函数返回,而是存储在数组 nums1 中。为了应对这种情况,nums1 的初始长度为 m + n,其中前 m 个元素表示应合并的元素,后 n 个元素为 0,应忽略。nums2 的长度为 n 。

题目示例:

// 示例1
输入: nums1 = [1,2,3,0,0,0], m = 3, nums2 = [2,5,6], n = 3
输出: [1,2,2,3,5,6]// 示例 2
输入: nums1 = [1], m = 1, nums2 = [], n = 0
输出: [1]// 提示
nums1.length == m + n
nums2.length == n
0 <= m, n <= 200
1 <= nums1[i], nums2[j] <= 10^9

思路分析:

1. 从后往前合并:

  • 由于 nums1 的后面部分有足够的空间可以存放 nums2 中的元素,因此我们可以从两个数组的末尾开始比较,较大的元素放入 nums1 的末尾。

  • 这样可以避免每次插入元素后再去移动数组元素,提高效率。

2. 具体步骤:

  • 设置三个指针,分别指向 nums1 和 nums2 的最后一个有效元素,以及合并后数组的最后一个位置。

  • 每次比较 nums1 和 nums2 的最后一个有效元素,将较大的元素放入合并后数组的末尾。

  • 当 nums2 还有剩余元素时,继续将剩余元素放入 nums1 。

3. 复杂度分析:

  • 时间复杂度:O(m + n),需要遍历 nums1 和 nums2 中的所有元素。

  • 空间复杂度:O(1),只使用了常数空间。

代码实现:

function merge(nums1: number[], m: number, nums2: number[], n: number): void {let i = m - 1; // nums1 的有效元素指针let j = n - 1; // nums2 的有效元素指针let k = m + n - 1; // 合并后数组的末尾指针// 从后向前遍历,比较 nums1 和 nums2 的元素while (i >= 0 && j >= 0) {if (nums1[i] > nums2[j]) {nums1[k] = nums1[i]; // 将较大值放入 nums1 的末尾i--; // nums1 指针向前移动} else {nums1[k] = nums2[j]; // 将较大值放入 nums1 的末尾j--; // nums2 指针向前移动}k--; // 合并后的数组指针向前移动}// 如果 nums2 还有剩余元素,将它们放入 nums1while (j >= 0) {nums1[k] = nums2[j];j--;k--;}// 无需处理 nums1 剩余元素,因为它们已经在适当位置上
}// 测试用例
const nums1 = [1, 2, 3, 0, 0, 0];
merge(nums1, 3, [2, 5, 6], 3);
console.log(nums1); // 输出: [1, 2, 2, 3, 5, 6]

详细注释与讲解:

1. 变量定义:

  • i 是指向 nums1 有效部分最后一个元素的指针,初始值为 m - 1。

  • j 是指向 nums2 最后一个元素的指针,初始值为 n - 1。

  • k 是合并后的数组的最后一个位置的指针,初始值为 m + n - 1。

2. 核心逻辑:

  • 使用 while 循环比较 nums1[i] 和 nums2[j] 的大小,较大的元素放到 nums1[k] 位置。

  • 每次比较后,将相应的指针向前移动。

  • 当 nums2 中还有未处理的元素时,使用 while 循环将它们复制到 nums1 中。

3. 边界情况:

  • 如果 nums2 为空,直接返回 nums1 原来的内容。

  • 如果 nums1 的所有有效元素都比 nums2 小,不需要额外操作。

思考与扩展:

  • 扩展问题:如何处理任意多个有序数组的合并?这种问题可以通过类似的方式解决,使用归并排序的思想逐步合并多个有序数组,时间复杂度为 O(kn log k),其中 k 是数组的数量。

3. 滑动窗口

3.1. 滑动窗口最大值

题目描述:给定一个数组 nums 和滑动窗口的大小 k,请找出所有滑动窗口里的最大值。

题目标例:

代码块
输入:nums = [1,3,-1,-3,5,3,6,7], k = 3
输出:[3,3,5,5,6,7]
解释:
滑动窗口的位置    最大值
---------------
[1  3  -1]  -3   5   3   6   7    31 [3  -1   -3]  5   3   6   7    31  3 [-1   -3   5]  3   6   7    51  3  -1  [-3   5   3]  6   7    51  3  -1   -3  [5   3   6]  7    61  3  -1   -3   5  [3   6   7]   7// 提示:
1 <= nums.length <= 10^5
-10^4 <= nums[i] <= 10^4
1 <= k <= nums.length

思路分析:

1. 暴力解法:对每一个滑动窗口,找到其中的最大值,直接使用双重循环计算最大值,时间复杂度为 O(n * k),但当数组长度较大时,性能较差。

2. 双端队列(Deque)优化:为了提升性能,可以使用双端队列(Deque)来维护一个窗口中的最大值。队列中存储数组的索引值,并且保持队列中的元素按照从大到小排列:

  • 对于每一个新元素,将队列尾部所有比当前元素小的元素移除。

  • 保证队列头部始终是当前窗口的最大值。

  • 当队列中的最大值元素超出窗口范围时,将其移除。

3. 复杂度分析:

  • 时间复杂度:O(n),每个元素最多进出队列一次。

  • 空间复杂度:O(k),队列中最多维护 k 个元素。

代码实现:

function maxSlidingWindow(nums: number[], k: number): number[] {const deque: number[] = []; // 双端队列,存储的是数组的索引const result: number[] = []; // 存放最大值的结果数组for (let i = 0; i < nums.length; i++) {// 当队列的第一个元素(最大值)超出窗口范围时,移除它if (deque.length && deque[0] < i - k + 1) {deque.shift();}// 保证队列中的元素值按照从大到小排列,移除小于当前元素的索引while (deque.length && nums[deque[deque.length - 1]] < nums[i]) {deque.pop();}// 将当前元素索引添加到队列尾部deque.push(i);// 当窗口长度达到 k 时,记录当前窗口的最大值(即队列的第一个元素)if (i >= k - 1) {result.push(nums[deque[0]]);}}return result;
}// 测试用例
const nums = [1, 3, -1, -3, 5, 3, 6, 7];
const k = 3;
console.log(maxSlidingWindow(nums, k)); // 输出: [3, 3, 5, 5, 6, 7]

详细注释与讲解:

1. 双端队列的用法:

  • deque 用来维护当前滑动窗口的最大值,它存储的不是值而是索引。这样可以通过 nums[deque[0]] 来获取窗口中的最大值。

  • 由于队列中元素按照值从大到小的顺序排列,因此队列的头部总是滑动窗口中的最大值。

2. 核心逻辑:

  • 每次处理一个新元素时,首先检查队列中头部的元素是否超出了滑动窗口的左边界(i - k + 1),如果超出则移除。

  • 通过一个 while 循环,将队列尾部所有比当前元素小的元素移除,保证新元素插入后队列仍然是按降序排列的。

  • 每次滑动窗口满 k 个元素时,队列头部元素就是当前窗口的最大值,将其记录到 result 数组中。


文章转载自:

http://lEMnJ1h8.pgrsf.cn
http://DL6rtVO6.pgrsf.cn
http://IGi1MI0g.pgrsf.cn
http://elRmRkSQ.pgrsf.cn
http://YiG5Mbm4.pgrsf.cn
http://Yz9XsNFi.pgrsf.cn
http://ITPwfzwC.pgrsf.cn
http://o1mODlLK.pgrsf.cn
http://6ReyVyEj.pgrsf.cn
http://KxS0NOmh.pgrsf.cn
http://Kv5UKT2z.pgrsf.cn
http://l8smYWSr.pgrsf.cn
http://UFKLRl71.pgrsf.cn
http://0EVsJA3x.pgrsf.cn
http://LAvGqC8A.pgrsf.cn
http://mZZbvIAU.pgrsf.cn
http://IY8BtNky.pgrsf.cn
http://jflNM34C.pgrsf.cn
http://nZx4JnZY.pgrsf.cn
http://2YdbIAGO.pgrsf.cn
http://Rbp3JkL5.pgrsf.cn
http://Yqa4bmIm.pgrsf.cn
http://GLH9DIES.pgrsf.cn
http://Tu3m0ktp.pgrsf.cn
http://HzfWWnOQ.pgrsf.cn
http://2Z31Mcrf.pgrsf.cn
http://NWxx9Unq.pgrsf.cn
http://DvsuDdvo.pgrsf.cn
http://9bp4ZMzh.pgrsf.cn
http://Ek6wuYLu.pgrsf.cn
http://www.dtcms.com/a/372268.html

相关文章:

  • python---多态
  • 中兴B860AV3.2-M/B860AV3.1-M2-内存大小区分参考指南
  • 【开题答辩全过程】以 校园二手货物交易平台为例,包含答辩的问题和答案
  • 【PyTorch】图像多分类部署
  • 阿里云上启动enclave 并与宿主机通信
  • Python 多任务编程:进程、线程与协程全面解析
  • Wan系列模型解析--VACE
  • 关于学习的一些感悟
  • 如何在Python中使用正则表达式替换特定格式的文本?
  • 【正则表达式】 正则表达式断言(Assertion)是什么?
  • GD32入门到实战39--SRAM
  • [RootersCTF2019]I_<3_Flask
  • 多功能台灯设计与实现(论文+源码)
  • SpringBoot+RustFS实现高效文件存储解决方案
  • Docker04-镜像源切换
  • Python 2025:量化金融与智能交易的新纪元
  • 基于 WeKnora 构建企业级 RAG 知识库:Windows 部署与实践全解析
  • 【Android】View 的基础知识
  • FastDFS V6双IP特性及配置
  • Spring Boot常用注解-详细解析+示例
  • 使用 Doxygen 生成 C++ 与 Python 项目文档
  • 【面试题】Transformer基础原理与数学模型
  • 插入排序与希尔排序
  • LLM面试基础(一)
  • More Effective C++ 条款33:将非尾端类设计为抽象类
  • 《详解链式队列:原理、操作与销毁方法》
  • Linux 系统资源监控与告警脚本
  • 记录jilu~
  • 现代云原生数据平台
  • 【Python脚本系列】PyCryptodome库解决网盘内.m3u8视频文件无法播放的问题(三)