注:纯手打,如有错误欢迎评论区交流!
转载请注明出处:https://blog.youkuaiyun.com/testleaf/article/details/148953015
编写此文是为了更好地学习前端知识,如果损害了有关人的利益,请联系删除!
本文章将不定时更新,敬请期待!!!
欢迎点赞、收藏、转发、关注,多谢!!!
前端JavaScript力扣HOT100刷题【1-50】:
https://blog.youkuaiyun.com/testleaf/article/details/147258015
九、图论
51、【中等】岛屿数量
题目描述:
给你一个由'1'
(陆地)和'0'
(水)组成的的二维网格,请你计算网格中岛屿的数量。
岛屿总是被水包围,并且每座岛屿只能由水平方向和/或竖直方向上相邻的陆地连接形成。
此外,你可以假设该网格的四条边均被水包围。
示例 1:
输入:
grid = [ ["1","1","1","1","0"], ["1","1","0","1","0"], ["1","1","0","0","0"], ["0","0","0","0","0"] ]
输出:1
示例 2:
输入:
grid = [ ["1","1","0","0","0"], ["1","1","0","0","0"], ["0","0","1","0","0"], ["0","0","0","1","1"] ]
输出:3
/**
* @param {character[][]} grid
* @return {number}
*/
var numIslands = function(grid) {
const m = grid.length, n = grid[0].length;
function dfs(i, j) {
// 出界,或者不是 '1',就不再往下递归
if (i < 0 || i >= m || j < 0 || j >= n || grid[i][j] !== '1') {
return;
}
grid[i][j] = '2'; // 插旗!避免来回横跳无限递归
dfs(i, j - 1); // 往左走
dfs(i, j + 1); // 往右走
dfs(i - 1, j); // 往上走
dfs(i + 1, j); // 往下走
}
let ans = 0;
for (let i = 0; i < m; i++) {
for (let j = 0; j < n; j++) {
if (grid[i][j] === '1') { // 找到了一个新的岛
dfs(i, j); // 把这个岛插满旗子,这样后面遍历到的 '1' 一定是新的岛
ans++;
}
}
}
return ans;
};
52、【中等】腐烂的橘子
题目描述:
在给定的m x n
网格grid
中,每个单元格可以有以下三个值之一:
值0
代表空单元格;
值1
代表新鲜橘子;
值2
代表腐烂的橘子。
每分钟,腐烂的橘子 周围 4 个方向上相邻 的新鲜橘子都会腐烂。
返回 直到单元格中没有新鲜橘子为止所必须经过的最小分钟数。如果不可能,返回-1
。
示例 1:
输入:grid = [[2,1,1],[1,1,0],[0,1,1]]
输出:4
示例 2:
输入:grid = [[2,1,1],[0,1,1],[1,0,1]]
输出:-1
解释:左下角的橘子(第 2 行, 第 0 列)永远不会腐烂,因为腐烂只会发生在 4 个方向上。
示例 3:
输入:grid = [[0,2]]
输出:0
解释:因为 0 分钟时已经没有新鲜橘子了,所以答案就是 0 。
/**
* @param {number[][]} grid
* @return {number}
*/
var orangesRotting = function(grid) {
const m = grid.length, n = grid[0].length;
let fresh = 0;
let q = [];
for (let i = 0; i < m; i++) {
for (let j = 0; j < n; j++) {
if (grid[i][j] === 1) {
fresh++; // 统计新鲜橘子个数
} else if (grid[i][j] === 2) {
q.push([i, j]); // 一开始就腐烂的橘子
}
}
}
let ans = 0;
while (fresh && q.length) {
ans++; // 经过一分钟
const tmp = q;
q = [];
for (const [x, y] of tmp) { // 已经腐烂的橘子
for (const [i, j] of [[x - 1, y], [x + 1, y], [x, y - 1], [x, y + 1]]) { // 四方向
if (0 <= i && i < m && 0 <= j && j < n && grid[i][j] === 1) { // 新鲜橘子
fresh--;
grid[i][j] = 2; // 变成腐烂橘子
q.push([i, j]);
}
}
}
}
return fresh ? -1 : ans;
};
53、【中等】课程表
题目描述:
你这个学期必须选修numCourses
门课程,记为0
到numCourses - 1
。
在选修某些课程之前需要一些先修课程。 先修课程按数组prerequisites
给出,其中prerequisites[i] = [ai, bi]
,表示如果要学习课程ai
则 必须 先学习课程bi
。
- 例如,先修课程对
[0, 1]
表示:想要学习课程0
,你需要先完成课程1
。请你判断是否可能完成所有课程的学习?如果可以,返回
true
;否则,返回false
。
示例 1:
输入:numCourses = 2, prerequisites = [[1,0]]
输出:true
解释:总共有 2 门课程。学习课程 1 之前,你需要完成课程 0 。这是可能的。
示例 2:
输入:numCourses = 2, prerequisites = [[1,0],[0,1]]
输出:false
解释:总共有 2 门课程。学习课程 1 之前,你需要先完成课程 0 ;并且学习课程 0 之前,你还应先完成课程 1 。这是不可能的。
/**
* @param {number} numCourses
* @param {number[][]} prerequisites
* @return {boolean}
*/
var canFinish = function(numCourses, prerequisites) {
const g = Array.from({ length: numCourses }, () => []);
for (const [a, b] of prerequisites) {
g[b].push(a);
}
const colors = Array(numCourses).fill(0);
// 返回 true 表示找到了环
function dfs(x) {
colors[x] = 1; // x 正在访问中
for (const y of g[x]) {
if (colors[y] === 1 || colors[y] === 0 && dfs(y)) {
return true; // 找到了环
}
}
colors[x] = 2; // x 完全访问完毕
return false; // 没有找到环
}
for (let i = 0; i < numCourses; i++) {
if (colors[i] === 0 && dfs(i)) {
return false; // 有环
}
}
return true; // 没有环
};
54、【中等】实现 Trie (前缀树)
题目描述:
Trie(发音类似 “try”)或者说 前缀树 是一种树形数据结构,用于高效地存储和检索字符串数据集中的键。这一数据结构有相当多的应用情景,例如自动补全和拼写检查。
请你实现 Trie 类:
Trie()
初始化前缀树对象。void insert(String word)
向前缀树中插入字符串word
。boolean search(String word)
如果字符串word
在前缀树中,返回true
(即,在检索之前已经插入);否则,返回false
。boolean startsWith(String prefix)
如果之前已经插入的字符串word
的前缀之一为prefix
,返回true
;否则,返回false
。
示例:
输入
["Trie", "insert", "search", "search", "startsWith", "insert", "search"] [[], ["apple"], ["apple"], ["app"], ["app"], ["app"], ["app"]]
输出
[null, null, true, false, true, null, true]
解释
Trie trie = new Trie();
trie.insert("apple");
trie.search("apple"); // 返回 True
trie.search("app"); // 返回 False
trie.startsWith("app"); // 返回 True
trie.insert("app");
trie.search("app"); // 返回 True
var Trie = function() {
this.root = new Node();
};
function Node() {
this.son = Array(26).fill(null);
this.end = false;
}
/**
* @param {string} word
* @return {void}
*/
Trie.prototype.insert = function(word) {
let cur = this.root;
for (let c of word) {
c = c.charCodeAt(0) - 'a'.charCodeAt(0);
if (cur.son[c] === null) {
cur.son[c] = new Node();
}
cur = cur.son[c];
}
cur.end = true;
};
Trie.prototype._find = function(word) {
let cur = this.root;
for (let c of word) {
c = c.charCodeAt(0) - 'a'.charCodeAt(0);
if (cur.son[c] === null) {
return 0;
}
cur = cur.son[c];
}
return cur.end ? 2 : 1;
};
/**
* @param {string} word
* @return {boolean}
*/
Trie.prototype.search = function(word) {
return this._find(word) === 2;
};
/**
* @param {string} prefix
* @return {boolean}
*/
Trie.prototype.startsWith = function(prefix) {
return this._find(prefix) !== 0;
};
/**
* Your Trie object will be instantiated and called as such:
* var obj = new Trie()
* obj.insert(word)
* var param_2 = obj.search(word)
* var param_3 = obj.startsWith(prefix)
*/
十、回溯
55、【中等】全排列
题目描述:
给定一个不含重复数字的数组nums
,返回其 所有可能的全排列 。你可以 按任意顺序 返回答案。
示例 1:
输入:nums = [1,2,3]
输出:[[1,2,3],[1,3,2],[2,1,3],[2,3,1],[3,1,2],[3,2,1]]
示例 2:
输入:nums = [0,1]
输出:[[0,1],[1,0]]
示例 3:
输入:nums = [1]
输出:[[1]]
/**
* @param {number[]} nums
* @return {number[][]}
*/
var permute = function(nums) {
const res = [], path = [];
backtracking(nums, []);
return res;
function backtracking(nums, used) {
const k = nums.length
if(path.length === k) {
res.push(Array.from(path));
return;
}
for (let i = 0; i < k; i++ ) {
if(used[i]) continue;
path.push(nums[i]);
used[i] = true;
backtracking(nums, used);
path.pop();
used[i] = false;
}
}
};
56、【中等】子集
题目描述:
给你一个整数数组nums
,数组中的元素 互不相同 。返回该数组所有可能的子集(幂集)。
解集 不能 包含重复的子集。你可以按 任意顺序 返回解集。
示例 1:
输入:nums = [1,2,3]
输出:[[],[1],[2],[1,2],[3],[1,3],[2,3],[1,2,3]]
示例 2:
输入:nums = [0]
输出:[[],[0]]
/**
* @param {number[]} nums
* @return {number[][]}
*/
var subsets = function(nums) {
let result = []
let path = []
backtracking(0)
return result
function backtracking(startIndex) {
result.push([...path])
for(let i = startIndex; i < nums.length; i++) {
path.push(nums[i])
backtracking(i + 1)
path.pop()
}
}
};
57、【中等】电话号码的字母组合
题目描述:
给定一个仅包含数字2-9
的字符串,返回所有它能表示的字母组合。答案可以按 任意顺序 返回。
给出数字到字母的映射如下(与电话按键相同)。注意 1 不对应任何字母。
示例 1:
输入:digits = "23"
输出:["ad","ae","af","bd","be","bf","cd","ce","cf"]
示例 2:
输入:digits = ""
输出:[]
示例 3:
输入:digits = "2"
输出:["a","b","c"]
/**
* @param {string} digits
* @return {string[]}
*/
var letterCombinations = function(digits) {
const k = digits.length;
const map = ["","","abc","def","ghi","jkl","mno","pqrs","tuv","wxyz"];
if(!k) return [];
if(k === 1) return map[digits].split("");
const res = [], path = [];
backtracking(digits, k, 0);
return res;
function backtracking(n, k, a) {
if(path.length === k) {
res.push(path.join(""));
return;
}
for(const v of map[n[a]]) {
path.push(v);
backtracking(n, k, a + 1);
path.pop();
}
}
};
58、【中等】组合总和
题目描述:
给你一个 无重复元素 的整数数组candidates
和一个目标整数target
,找出candidates
中可以使数字和为目标数target
的 所有 不同组合 ,并以列表形式返回。你可以按 任意顺序 返回这些组合。
candidates
中的 同一个 数字可以 无限制重复被选取 。如果至少一个数字的被选数量不同,则两种组合是不同的。
对于给定的输入,保证和为target
的不同组合数少于150
个。
示例 1:
输入:candidates = [2,3,6,7], target = 7
输出:[[2,2,3],[7]]
解释:
2
和3
可以形成一组候选,2 + 2 + 3 = 7
。注意2
可以使用多次。
7
也是一个候选,7 = 7
。
仅有这两种组合。
示例 2:
输入:candidates = [2,3,5], target = 8
输出:[[2,2,2,2],[2,3,3],[3,5]]
示例 3:
输入:candidates = [2], target = 1
输出:[]
/**
* @param {number[]} candidates
* @param {number} target
* @return {number[][]}
*/
var combinationSum = function(candidates, target) {
const res = [], path = [];
candidates.sort((a,b)=>a-b);
backtracking(0, 0);
return res;
function backtracking(j, sum) {
if (sum === target) {
res.push(Array.from(path));
return;
}
for(let i = j; i < candidates.length; i++ ) {
const n = candidates[i];
if(n > target - sum) break;
path.push(n);
sum += n;
backtracking(i, sum);
path.pop();
sum -= n;
}
}
};
59、【中等】括号生成
题目描述:
数字n
代表生成括号的对数,请你设计一个函数,用于能够生成所有可能的并且 有效的 括号组合。
示例 1:
输入:n = 3
输出:["((()))","(()())","(())()","()(())","()()()"]
示例 2:
输入:n = 1
输出:["()"]
/**
* @param {number} n
* @return {string[]}
*/
var generateParenthesis = function(n) {
const ans = [];
const path = Array(n * 2); // 所有括号长度都是一样的 2n
dfs(0, 0); // 一开始没有填括号
return ans;
// 目前填了 left 个左括号,right 个右括号
function dfs(left, right) {
if (right === n) { // 填完 2n 个括号
ans.push(path.join('')); // 加入答案
return;
}
if (left < n) { // 可以填左括号
path[left + right] = '('; // 直接覆盖
dfs(left + 1, right);
}
if (right < left) { // 可以填右括号
path[left + right] = ')'; // 直接覆盖
dfs(left, right + 1);
}
}
};
60、【中等】单词搜索
题目描述:
给定一个m x n
二维字符网格board
和一个字符串单词word
。如果word
存在于网格中,返回true
;否则,返回false
。
单词必须按照字母顺序,通过相邻的单元格内的字母构成,其中“相邻”单元格是那些水平相邻或垂直相邻的单元格。同一个单元格内的字母不允许被重复使用。
示例1:
输入:board = [["A","B","C","E"],["S","F","C","S"],["A","D","E","E"]], word = "ABCCED"
输出:true
示例 2:
输入:board = [["A","B","C","E"],["S","F","C","S"],["A","D","E","E"]], word = "SEE"
输出:true
示例 3:
输入:board = [["A","B","C","E"],["S","F","C","S"],["A","D","E","E"]], word = "ABCB"
输出:false
/**
* @param {character[][]} board
* @param {string} word
* @return {boolean}
*/
var exist = function(board, word) {
const cnt = new Map();
for (const row of board) {
for (const c of row) {
cnt.set(c, (cnt.get(c) ?? 0) + 1);
}
}
// 优化一
const wordCnt = new Map();
for (const c of word) {
wordCnt.set(c, (wordCnt.get(c) ?? 0) + 1);
if (wordCnt.get(c) > (cnt.get(c) ?? 0)) {
return false;
}
}
// 优化二
if ((cnt.get(word[word.length - 1]) ?? 0) < (cnt.get(word[0]) ?? 0)) {
word = word.split('').reverse();
}
const m = board.length, n = board[0].length;
for (let i = 0; i < m; i++) {
for (let j = 0; j < n; j++) {
if (dfs(i, j, 0)) {
return true; // 搜到了!
}
}
}
return false; // 没搜到
function dfs(i, j, k) {
if (board[i][j] !== word[k]) {
return false; // 匹配失败
}
if (k + 1 === word.length) {
return true; // 匹配成功!
}
board[i][j] = 0; // 标记访问过
for (const [x, y] of [[i, j - 1], [i, j + 1], [i - 1, j], [i + 1, j]]) { // 相邻格子
if (0 <= x && x < m && 0 <= y && y < n && dfs(x, y, k + 1)) {
return true; // 搜到了!
}
}
board[i][j] = word[k]; // 恢复现场
return false; // 没搜到
}
};
61、【中等】分割回文串
题目描述:
给你一个字符串s
,请你将s
分割成一些 子串,使每个子串都是 回文串 。返回s
所有可能的分割方案。
示例 1:
输入:s = "aab"
输出:[["a","a","b"],["aa","b"]]
示例 2:
输入:s = "a"
输出:[["a"]]
/**
* @param {string} s
* @return {string[][]}
*/
var partition = function(s) {
const n = s.length;
const ans = [];
const path = [];
dfs(0);
return ans;
// 考虑 s[i] ~ s[n-1] 怎么分割
function dfs(i) {
if (i === n) { // s 分割完毕
ans.push(path.slice()); // 复制 path
return;
}
for (let j = i; j < n; j++) { // 枚举子串的结束位置
if (isPalindrome(s, i, j)) {
path.push(s.substring(i, j + 1)); // 分割!
// 考虑剩余的 s[j+1] ~ s[n-1] 怎么分割
dfs(j + 1);
path.pop(); // 恢复现场
}
}
}
function isPalindrome(s, left, right) {
while (left < right) {
if (s.charAt(left++) !== s.charAt(right--)) {
return false;
}
}
return true;
}
};
十一、二分查找
63、【简单】搜索插入位置
题目描述:
给定一个排序数组和一个目标值,在数组中找到目标值,并返回其索引。如果目标值不存在于数组中,返回它将会被按顺序插入的位置。
请必须使用时间复杂度为O(log n)
的算法。
示例 1:
输入:nums = [1,3,5,6], target = 5
输出:2
示例 2:
输入:nums = [1,3,5,6], target = 2
输出:1
示例 3:
输入:nums = [1,3,5,6], target = 7
输出:4
/**
* @param {number[]} nums
* @param {number} target
* @return {number}
*/
var searchInsert = function(nums, target) {
const n = nums.length;
let left = 0, right = n - 1, ans = n;
while (left <= right) {
let mid = ((right - left) >> 1) + left;
if (target <= nums[mid]) {
ans = mid;
right = mid - 1;
} else {
left = mid + 1;
}
}
return ans;
};
63、【中等】搜索二维矩阵
题目描述:
给你一个满足下述两条属性的m x n
整数矩阵:
- 每行中的整数从左到右按非严格递增顺序排列。
- 每行的第一个整数大于前一行的最后一个整数。
给你一个整数
target
,如果target
在矩阵中,返回true
;否则,返回false
。
示例 1:
输入:matrix = [[1,3,5,7],[10,11,16,20],[23,30,34,60]], target = 3
输出:true
示例 2:
输入:matrix = [[1,3,5,7],[10,11,16,20],[23,30,34,60]], target = 13
输出:false
/**
* @param {number[][]} matrix
* @param {number} target
* @return {boolean}
*/
var searchMatrix = function(matrix, target) {
const m = matrix.length, n = matrix[0].length;
let left = -1, right = m * n;
while (left + 1 < right) {
const mid = Math.floor((left + right) / 2);
const x = matrix[Math.floor(mid / n)][mid % n];
if (x === target) {
return true;
}
if (x < target) {
left = mid;
} else {
right = mid;
}
}
return false;
};
64、【中等】在排序数组中查找元素的第一个和最后一个位置
题目描述:
给你一个按照非递减顺序排列的整数数组nums
,和一个目标值target
。请你找出给定目标值在数组中的开始位置和结束位置。
如果数组中不存在目标值target
,返回[-1, -1]
。
你必须设计并实现时间复杂度为O(log n)
的算法解决此问题。
示例 1:
输入:nums = [5,7,7,8,8,10], target = 8
输出:[3,4]
示例 2:
输入:nums = [5,7,7,8,8,10], target = 6
输出:[-1,-1]
示例 3:
输入:nums = [], target = 0
输出:[-1,-1]
/**
* @param {number[]} nums
* @param {number} target
* @return {number[]}
*/
var searchRange = function(nums, target) {
let ans = [-1, -1];
const leftIdx = binarySearch(nums, target, true);
const rightIdx = binarySearch(nums, target, false) - 1;
if (leftIdx <= rightIdx && rightIdx < nums.length && nums[leftIdx] === target && nums[rightIdx] === target) {
ans = [leftIdx, rightIdx];
}
return ans;
function binarySearch(nums, target, lower){
let left = 0, right = nums.length - 1, ans = nums.length;
while (left <= right) {
const mid = Math.floor((left + right) / 2);
if (nums[mid] > target || (lower && nums[mid] >= target)) {
right = mid - 1;
ans = mid;
} else {
left = mid + 1;
}
}
return ans;
}
};
65、【中等】搜索旋转排序数组
题目描述:
整数数组nums
按升序排列,数组中的值 互不相同 。
在传递给函数之前,nums
在预先未知的某个下标k
(0 <= k < nums.length
)上进行了 旋转,使数组变为[nums[k], nums[k+1], ..., nums[n-1], nums[0], nums[1], ..., nums[k-1]]
(下标 从 0 开始 计数)。例如,[0,1,2,4,5,6,7]
在下标3
处经旋转后可能变为[4,5,6,7,0,1,2]
。
给你 旋转后 的数组nums
和一个整数target
,如果nums
中存在这个目标值target
,则返回它的下标,否则返回-1
。
你必须设计一个时间复杂度为O(log n)
的算法解决此问题。
示例 1:
输入:nums = [4,5,6,7,0,1,2], target = 0
输出:4
示例 2:
输入:nums = [4,5,6,7,0,1,2], target = 3
输出:-1
示例 3:
输入:nums = [1], target = 0
输出:-1
/**
* @param {number[]} nums
* @param {number} target
* @return {number}
*/
var search = function(nums, target) {
if (!nums.length) return -1;
let l = 0, r = nums.length - 1;
while (l <= r) {
const mid = Math.floor((l + r) / 2);
if (nums[mid] === target) return mid;
if (nums[0] <= nums[mid]) {
if (nums[0] <= target && target < nums[mid]) {
r = mid - 1;
} else {
l = mid + 1;
}
} else {
if (nums[mid] < target && target <= nums[nums.length - 1]) {
l = mid + 1;
} else {
r = mid - 1;
}
}
}
return -1;
};
66、【中等】寻找旋转排序数组中的最小值
题目描述:
已知一个长度为n
的数组,预先按照升序排列,经由1
到n
次 旋转 后,得到输入数组。例如,原数组nums = [0,1,2,4,5,6,7]
在变化后可能得到:
若旋转4
次,则可以得到[4,5,6,7,0,1,2]
若旋转7
次,则可以得到[0,1,2,4,5,6,7]
注意,数组[a[0], a[1], a[2], ..., a[n-1]]
旋转一次 的结果为数组[a[n-1], a[0], a[1], a[2], ..., a[n-2]]
。
给你一个元素值 互不相同 的数组nums
,它原来是一个升序排列的数组,并按上述情形进行了多次旋转。请你找出并返回数组中的 最小元素 。
你必须设计一个时间复杂度为O(log n)
的算法解决此问题。
示例 1:
输入:nums = [3,4,5,1,2]
输出:1
解释:原数组为[1,2,3,4,5]
,旋转3
次得到输入数组。
示例 2:
输入:nums = [4,5,6,7,0,1,2]
输出:0
解释:原数组为[0,1,2,4,5,6,7]
,旋转4
次得到输入数组。
示例 3:
输入:nums = [11,13,15,17]
输出:11
解释:原数组为[11,13,15,17]
,旋转4
次得到输入数组。
/**
* @param {number[]} nums
* @return {number}
*/
var findMin = function(nums) {
let low = 0;
let high = nums.length - 1;
while (low < high) {
const pivot = low + Math.floor((high - low) / 2);
if (nums[pivot] < nums[high]) {
high = pivot;
} else {
low = pivot + 1;
}
}
return nums[low];
};
十二、栈
69、【简单】有效的括号
题目描述:
给定一个只包括'('
,')'
,'{'
,'}'
,'['
,']'
的字符串s
,判断字符串是否有效。
有效字符串需满足:
1、左括号必须用相同类型的右括号闭合。
2、左括号必须以正确的顺序闭合。
3、每个右括号都有一个对应的相同类型的左括号。
示例 1:
输入:s = "()"
输出:true
示例 2:
输入:s = "()[]{}"
输出:true
示例 3:
输入:s = "(]"
输出:false
示例 4:
输入:s = "([])"
输出:true
/**
* @param {string} s
* @return {boolean}
*/
var isValid = function(s) {
const n = s.length;
if (n % 2 === 1) {
return false;
}
const pairs = new Map([
[')', '('],
[']', '['],
['}', '{']
]);
const stk = [];
for (let ch of s){
if (pairs.has(ch)) {
if (!stk.length || stk[stk.length - 1] !== pairs.get(ch)) {
return false;
}
stk.pop();
}
else {
stk.push(ch);
}
};
return !stk.length;
};
70、【中等】最小栈
题目描述:
设计一个支持push
,pop
,top
操作,并能在常数时间内检索到最小元素的栈。
实现MinStack
类:
MinStack()
初始化堆栈对象。
void push(int val)
将元素val推入堆栈。
void pop()
删除堆栈顶部的元素。
int top()
获取堆栈顶部的元素。
int getMin()
获取堆栈中的最小元素。
示例 1:
输入:
["MinStack","push","push","push","getMin","pop","top","getMin"]
[[],[-2],[0],[-3],[],[],[],[]]
输出:
[null,null,null,null,-3,null,0,-2]
解释:
MinStack minStack = new MinStack();
minStack.push(-2);
minStack.push(0);
minStack.push(-3);
minStack.getMin(); --> 返回 -3.
minStack.pop();
minStack.top(); --> 返回 0.
minStack.getMin(); --> 返回 -2.
var MinStack = function() {
this.x_stack = [];
this.min_stack = [Infinity];
};
/**
* @param {number} val
* @return {void}
*/
MinStack.prototype.push = function(val) {
this.x_stack.push(val);
this.min_stack.push(Math.min(this.min_stack[this.min_stack.length - 1], val));
};
/**
* @return {void}
*/
MinStack.prototype.pop = function() {
this.x_stack.pop();
this.min_stack.pop();
};
/**
* @return {number}
*/
MinStack.prototype.top = function() {
return this.x_stack[this.x_stack.length - 1];
};
/**
* @return {number}
*/
MinStack.prototype.getMin = function() {
return this.min_stack[this.min_stack.length - 1];
};
/**
* Your MinStack object will be instantiated and called as such:
* var obj = new MinStack()
* obj.push(val)
* obj.pop()
* var param_3 = obj.top()
* var param_4 = obj.getMin()
*/
71、【中等】字符串解码
题目描述:
给定一个经过编码的字符串,返回它解码后的字符串。
编码规则为:k[encoded_string]
,表示其中方括号内部的encoded_string
正好重复k
次。注意k
保证为正整数。
你可以认为输入字符串总是有效的;输入字符串中没有额外的空格,且输入的方括号总是符合格式要求的。
此外,你可以认为原始数据不包含数字,所有的数字只表示重复的次数k
,例如不会出现像3a
或2[4]
的输入。
示例 1:
输入:s = "3[a]2[bc]"
输出:"aaabcbc"
示例 2:
输入:s = "3[a2[c]]"
输出:"accaccacc"
示例 3:
输入:s = "2[abc]3[cd]ef"
输出:"abcabccdcdcdef"
示例 4:
输入:s = "abc3[cd]xyz"
输出:"abccdcdcdxyz"
/**
* @param {string} s
* @return {string}
*/
var decodeString = function(s) {
let stack = [];
let currentString = '';
let currentNum = 0;
for (let char of s) {
if (char === '[') {
stack.push(currentString);
stack.push(currentNum);
currentString = '';
currentNum = 0;
} else if (char === ']') {
let num = stack.pop();
let prevString = stack.pop();
currentString = prevString + currentString.repeat(num);
} else if (char >= '0' && char <= '9') {
currentNum = currentNum * 10 + parseInt(char);
} else {
currentString += char;
}
}
return currentString;
};
72、【中等】每日温度
题目描述:
给定一个整数数组temperatures
,表示每天的温度,返回一个数组answer
,其中answer[i]
是指对于第i
天,下一个更高温度出现在几天后。如果气温在这之后都不会升高,请在该位置用0
来代替。
示例 1:
输入:temperatures = [73,74,75,71,69,72,76,73]
输出:[1,1,4,2,1,1,0,0]
示例 2:
输入:temperatures = [30,40,50,60]
输出:[1,1,1,0]
示例 3:
输入:temperatures = [30,60,90]
输出:[1,1,0]
/**
* @param {number[]} temperatures
* @return {number[]}
*/
var dailyTemperatures = function(temperatures) {
const n = temperatures.length;
const answer = new Array(n).fill(0);
const stack = [];
for (let i = 0; i < n; i++) {
while (stack.length > 0 && temperatures[i] > temperatures[stack[stack.length - 1]]) {
const index = stack.pop();
answer[index] = i - index;
}
stack.push(i);
}
return answer;
};
十三、堆
74、【中等】数组中的第K个最大元素
题目描述:
给定整数数组nums
和整数k
,请返回数组中第k
个最大的元素。
请注意,你需要找的是数组排序后的第k
个最大的元素,而不是第k
个不同的元素。
你必须设计并实现时间复杂度为O(n)
的算法解决此问题。
示例 1:
输入:[3,2,1,5,6,4], k = 2
输出:5
示例 2:
输入:[3,2,3,1,2,4,5,5,6], k = 4
输出:4
/**
* @param {number[]} nums
* @param {number} k
* @return {number}
*/
var findKthLargest = function(nums, k) {
// 构建最大堆
buildMaxHeap(nums);
// 执行 k-1 次 extractMax,然后堆顶就是第 k 大的元素
for (let i = 0; i < k - 1; i++) {
extractMax(nums);
}
return nums[0]; // 堆顶就是第 k 大的元素
// 构建最大堆
function buildMaxHeap(nums) {
const n = nums.length;
for (let i = Math.floor(n / 2) - 1; i >= 0; i--) {
heapify(nums, i, n);
}
}
// 调整堆(下沉操作)
function heapify(nums, i, heapSize) {
const left = 2 * i + 1; // 左子节点的索引
const right = 2 * i + 2; // 右子节点的索引
let largest = i; // 假设当前节点是最大的
// 如果左子节点存在且比当前节点大,更新 largest
if (left < heapSize && nums[left] > nums[largest]) {
largest = left;
}
// 如果右子节点存在且比当前节点大,更新 largest
if (right < heapSize && nums[right] > nums[largest]) {
largest = right;
}
// 如果最大值不是当前节点,交换它们,并递归调整
if (largest !== i) {
[nums[i], nums[largest]] = [nums[largest], nums[i]]; // 交换
heapify(nums, largest, heapSize); // 递归调整受影响的子树
}
}
// 提取最大值(移除堆顶)
function extractMax(nums) {
const max = nums[0];
nums[0] = nums.pop(); // 用最后一个元素替换堆顶
heapify(nums, 0, nums.length); // 调整堆
return max;
}
};
75、【中等】前 K 个高频元素
题目描述:
给你一个整数数组nums
和一个整数k
,请你返回其中出现频率前k
高的元素。你可以按 任意顺序 返回答案。
示例 1:
输入:nums = [1,1,1,2,2,3], k = 2
输出:[1,2]
示例 2:
输入:nums = [1], k = 1
输出:[1]
/**
* @param {number[]} nums
* @param {number} k
* @return {number[]}
*/
var topKFrequent = function(nums, k) {
const cnt = new Map();
for (const x of nums) {
cnt.set(x, (cnt.get(x) ?? 0) + 1);
}
const arr = Array.from(cnt).sort((a,b)=>b[1]-a[1]);
const res = [];
for(let i=0;i<k;i++){
res.push(arr[i][0]);
}
return res;
};
十四、贪心算法
77、【简单】买卖股票的最佳时机
题目描述:
给定一个数组prices
,它的第i
个元素prices[i]
表示一支给定股票第i
天的价格。
你只能选择 某一天 买入这只股票,并选择在 未来的某一个不同的日子 卖出该股票。设计一个算法来计算你所能获取的最大利润。
返回你可以从这笔交易中获取的最大利润。如果你不能获取任何利润,返回0
。
示例 1:
输入:[7,1,5,3,6,4]
输出:5
解释:在第 2 天(股票价格 = 1)的时候买入,在第 5 天(股票价格 = 6)的时候卖出,最大利润 = 6-1 = 5 。
注意利润不能是 7-1 = 6, 因为卖出价格需要大于买入价格;同时,你不能在买入前卖出股票。
示例 2:
输入:prices = [7,6,4,3,1]
输出:0
解释:在这种情况下, 没有交易完成, 所以最大利润为 0。
/**
* @param {number[]} prices
* @return {number}
*/
var maxProfit = function(prices) {
let ans = 0;
let minPrice = prices[0];
for (const p of prices) {
ans = Math.max(ans, p - minPrice);
minPrice = Math.min(minPrice, p);
}
return ans;
};
78、【中等】跳跃游戏
题目描述:
给你一个非负整数数组nums
,你最初位于数组的 第一个下标 。数组中的每个元素代表你在该位置可以跳跃的最大长度。
判断你是否能够到达最后一个下标,如果可以,返回true
;否则,返回false
。
示例 1:
输入:nums = [2,3,1,1,4]
输出:true
解释:可以先跳 1 步,从下标 0 到达下标 1, 然后再从下标 1 跳 3 步到达最后一个下标。
示例 2:
输入:nums = [3,2,1,0,4]
输出:false
解释:无论怎样,总会到达下标为 3 的位置。但该下标的最大跳跃长度是 0 , 所以永远不可能到达最后一个下标。
/**
* @param {number[]} nums
* @return {boolean}
*/
var canJump = function(nums) {
let maxReach = 0;
for (let i = 0; i < nums.length; i++) {
if (i > maxReach) {
return false;
}
maxReach = Math.max(maxReach, i + nums[i]);
if (maxReach >= nums.length - 1) {
return true;
}
}
return false;
};
79、【中等】跳跃游戏 II
题目描述:
给定一个长度为n
的 0 索引整数数组nums
。初始位置为nums[0]
。
每个元素nums[i]
表示从索引i
向后跳转的最大长度。换句话说,如果你在nums[i]
处,你可以跳转到任意nums[i + j]
处:
0 <= j <= nums[i]
i + j < n
返回到达
nums[n - 1]
的最小跳跃次数。生成的测试用例可以到达nums[n - 1]
。
示例 1:
输入:nums = [2,3,1,1,4]
输出:2
解释: 跳到最后一个位置的最小跳跃数是 2。
从下标为 0 跳到下标为 1 的位置,跳 1 步,然后跳 3 步到达数组的最后一个位置。
示例 2:
输入:nums = [2,3,0,1,4]
输出:2
/**
* @param {number[]} nums
* @return {number}
*/
var jump = function(nums) {
let maxPos = 0; // 当前能到达的最远位置
let end = 0; // 当前跳跃的边界
let steps = 0; // 跳跃次数
for (let i = 0; i < nums.length - 1; ++i) {
maxPos = Math.max(maxPos, i + nums[i]); // 更新能到达的最远位置
if (i == end) { // 到达当前跳跃的边界
end = maxPos; // 更新下一次跳跃的边界
++steps; // 增加跳跃次数
}
}
return steps;
};
80、【中等】划分字母区间
题目描述:
给你一个字符串s
。我们要把这个字符串划分为尽可能多的片段,同一字母最多出现在一个片段中。例如,字符串"ababcc"
能够被分为["abab", "cc"]
,但类似["aba", "bcc"]
或["ab", "ab", "cc"]
的划分是非法的。
注意,划分结果需要满足:将所有划分结果按顺序连接,得到的字符串仍然是s
。
返回一个表示每个字符串片段的长度的列表。
示例 1:
输入:s = "ababcbacadefegdehijhklij"
输出:[9,7,8]
解释:
划分结果为"ababcbaca"
、"defegde"
、"hijhklij"
。
每个字母最多出现在一个片段中。
像"ababcbacadefegde"
,"hijhklij"
这样的划分是错误的,因为划分的片段数较少。
示例 2:
输入:s = "eccbbbbdec"
输出:[10]
/**
* @param {string} s
* @return {number[]}
*/
var partitionLabels = function(s) {
const last = new Array(26); // 用于存储每个字符的最后出现位置
const length = s.length;
const codePointA = 'a'.codePointAt(0); // 获取 'a' 的 Unicode 码点(97)
// 第一次遍历,记录每个字符的最后出现位置
for (let i = 0; i < length; i++) {
last[s.codePointAt(i) - codePointA] = i;
}
const partition = []; // 存储最终的分区长度结果
let start = 0, end = 0; // 当前分区的起始和结束位置
// 第二次遍历,动态维护分区边界
for (let i = 0; i < length; i++) {
end = Math.max(end, last[s.codePointAt(i) - codePointA]); // 更新当前分区的结束位置
if (i === end) { // 如果当前索引等于分区结束位置,说明分区完成
partition.push(end - start + 1); // 记录分区长度
start = end + 1; // 更新起始位置为下一个分区的开始
}
}
return partition;
};
十五、动态规划
81、【简单】爬楼梯
题目描述:
假设你正在爬楼梯。需要n
阶你才能到达楼顶。
每次你可以爬1
或2
个台阶。你有多少种不同的方法可以爬到楼顶呢?
示例 1:
输入:n = 2
输出:2
解释:有两种方法可以爬到楼顶。
1 阶 + 1 阶
2 阶
示例 2:
输入:n = 3
输出:3
解释:有三种方法可以爬到楼顶。
1 阶 + 1 阶 + 1 阶
1 阶 + 2 阶
2 阶 + 1 阶
/**
* @param {number} n
* @return {number}
*/
var climbStairs = function(n) {
// 爬一楼
let p = 1
// 爬二楼
let q = 2
if(n == 1){
return p
}else if(n == 2){
return q
}else{
// 从第三楼开始,只有两种上楼方式,从前一层再爬一楼和从前二层再爬两楼。
// 可以推出 f(n) = f(n -1) + f(n -2)
// 直接递归会超时,所以用的for循环求结果
let r = 0;
for(let i = 3; i <= n; i++){
r = q + p;
p = q;
q = r;
}
return r;
}
};
82、【简单】杨辉三角
题目描述:
给定一个非负整数numRows
,生成「杨辉三角」的前numRows
行。
在「杨辉三角」中,每个数是它左上方和右上方的数的和。
示例 1:
输入:numRows = 5
输出:[[1],[1,1],[1,2,1],[1,3,3,1],[1,4,6,4,1]]
示例 2:
输入:numRows = 1
输出:[[1]]
/**
* @param {number} numRows
* @return {number[][]}
*/
var generate = function(numRows) {
const ret = [];
for (let i = 0; i < numRows; i++) {
const row = new Array(i + 1).fill(1);
for (let j = 1; j < row.length - 1; j++) {
row[j] = ret[i - 1][j - 1] + ret[i - 1][j];
}
ret.push(row);
}
return ret;
};
83、【中等】打家劫舍
题目描述:
你是一个专业的小偷,计划偷窃沿街的房屋。每间房内都藏有一定的现金,影响你偷窃的唯一制约因素就是相邻的房屋装有相互连通的防盗系统,如果两间相邻的房屋在同一晚上被小偷闯入,系统会自动报警。
给定一个代表每个房屋存放金额的非负整数数组,计算你 不触动警报装置的情况下 ,一夜之内能够偷窃到的最高金额。
示例 1:
输入:[1,2,3,1]
输出:4
解释:偷窃 1 号房屋 (金额 = 1) ,然后偷窃 3 号房屋 (金额 = 3)。
偷窃到的最高金额 = 1 + 3 = 4 。
示例 2:
输入:[2,7,9,3,1]
输出:12
解释:偷窃 1 号房屋 (金额 = 2), 偷窃 3 号房屋 (金额 = 9),接着偷窃 5 号房屋 (金额 = 1)。
偷窃到的最高金额 = 2 + 9 + 1 = 12 。
/**
* @param {number[]} nums
* @return {number}
*/
var rob = function(nums) {
if (nums == null || nums.length === 0) {
return 0;
}
const length = nums.length;
if (length === 1) {
return nums[0];
}
let first = nums[0];
let second = Math.max(nums[0], nums[1]);
for (let i = 2; i < length; i++) {
const temp = second;
second = Math.max(first + nums[i], second);
first = temp;
}
return second;
};
84、【中等】完全平方数
题目描述:
给你一个整数n
,返回 和为n
的完全平方数的最少数量 。
完全平方数 是一个整数,其值等于另一个整数的平方;换句话说,其值等于一个整数自乘的积。例如,1
、4
、9
和16
都是完全平方数,而3
和11
不是。
示例 1:
输入:n = 12
输出:3
解释:12 = 4 + 4 + 4
示例 2:
输入:n = 13
输出:2
解释:13 = 4 + 9
/**
* @param {number} n
* @return {number}
*/
var numSquares = function(n) {
const f = new Array(n + 1).fill(0); // 初始化动态规划数组
for (let i = 1; i <= n; i++) {
let minn = Number.MAX_VALUE; // 初始化最小值
for (let j = 1; j * j <= i; j++) {
minn = Math.min(minn, f[i - j * j]); // 更新最小值
}
f[i] = minn + 1; // 当前值的最小数量为最小值加1
}
return f[n]; // 返回结果
};
85、【中等】零钱兑换
题目描述:
给你一个整数数组coins
,表示不同面额的硬币;以及一个整数amount
,表示总金额。
计算并返回可以凑成总金额所需的 最少的硬币个数 。如果没有任何一种硬币组合能组成总金额,返回-1
。
你可以认为每种硬币的数量是无限的。
示例 1:
输入:coins = [1, 2, 5], amount = 11
输出:3
解释:11 = 5 + 5 + 1
示例 2:
输入:coins = [2], amount = 3
输出:-1
示例 3:
输入:coins = [1], amount = 0
输出:0
/**
* @param {number[]} coins
* @param {number} amount
* @return {number}
*/
var coinChange = function(coins, amount) {
const dp = new Array(amount + 1).fill(Infinity);
dp[0] = 0;
for (const coin of coins) {
for (let x = coin; x <= amount; x++) {
dp[x] = Math.min(dp[x], dp[x - coin] + 1);
}
}
return dp[amount] !== Infinity ? dp[amount] : -1;
};
86、【中等】单词拆分
题目描述:
给你一个字符串s
和一个字符串列表wordDict
作为字典。如果可以利用字典中出现的一个或多个单词拼接出s
则返回true
。
注意:不要求字典中出现的单词全部都使用,并且字典中的单词可以重复使用。
示例 1:
输入:s = "leetcode", wordDict = ["leet", "code"]
输出:true
解释: 返回 true 因为 “leetcode” 可以由 “leet” 和 “code” 拼接成。
示例 2:
输入:s = "applepenapple", wordDict = ["apple", "pen"]
输出:true
解释: 返回 true 因为 “applepenapple” 可以由 “apple” “pen” “apple” 拼接成。
注意,你可以重复使用字典中的单词。
示例 3:
输入:s = "catsandog", wordDict = ["cats", "dog", "sand", "and", "cat"]
输出:false
/**
* @param {string} s
* @param {string[]} wordDict
* @return {boolean}
*/
var wordBreak = function(s, wordDict) {
const wordDictSet = new Set(wordDict);
const dp = new Array(s.length + 1).fill(false);
dp[0] = true;
for (let i = 1; i <= s.length; i++) {
for (let j = 0; j < i; j++) {
if (dp[j] && wordDictSet.has(s.substring(j, i))) {
dp[i] = true;
break;
}
}
}
return dp[s.length];
};
87、【中等】最长递增子序列
题目描述:
给你一个整数数组nums
,找到其中最长严格递增子序列的长度。
子序列 是由数组派生而来的序列,删除(或不删除)数组中的元素而不改变其余元素的顺序。例如,[3,6,2,7]
是数组[0,3,1,6,2,2,7]
的子序列。
示例 1:
输入:nums = [10,9,2,5,3,7,101,18]
输出:4
解释:最长递增子序列是 [2,3,7,101],因此长度为 4 。
示例 2:
输入:nums = [0,1,0,3,2,3]
输出:4
示例 3:
输入:nums = [7,7,7,7,7,7,7]
输出:1
/**
* @param {number[]} nums
* @return {number}
*/
var lengthOfLIS = function(nums) {
if (nums.length === 0) {
return 0;
}
const dp = new Array(nums.length).fill(1);
for (let i = 0; i < nums.length; i++) {
for (let j = 0; j < i; j++) {
if (nums[i] > nums[j]) {
dp[i] = Math.max(dp[i], dp[j] + 1);
}
}
}
return Math.max(...dp);
};
88、【中等】乘积最大子数组
题目描述:
给你一个整数数组nums
,请你找出数组中乘积最大的非空连续 子数组(该子数组中至少包含一个数字),并返回该子数组所对应的乘积。
测试用例的答案是一个 32-位 整数。
示例 1:
输入:nums = [2,3,-2,4]
输出:6
解释: 子数组 [2,3] 有最大乘积 6。
示例 2:
输入:nums = [-2,0,-1]
输出:0
解释: 结果不能为 2, 因为 [-2,-1] 不是子数组。
/**
* @param {number[]} nums
* @return {number}
*/
var maxProduct = function(nums) {
if (nums.length === 0) return 0;
let maxF = nums[0], minF = nums[0];
let ans = nums[0];
for (let i = 1; i < nums.length; i++) {
const current = nums[i];
const tempMax = maxF;
// 计算新的最大值和最小值
maxF = Math.max(tempMax * current, current, minF * current);
minF = Math.min(minF * current, current, tempMax * current);
// 更新最终结果
ans = Math.max(maxF, ans);
}
return ans;
};
89、【中等】分割等和子集
题目描述:
给你一个 只包含正整数 的 非空 数组nums
。请你判断是否可以将这个数组分割成两个子集,使得两个子集的元素和相等。
示例 1:
输入:nums = [1,5,11,5]
输出:true
解释:数组可以分割成 [1, 5, 5] 和 [11] 。
示例 2:
输入:nums = [1,2,3,5]
输出:false
解释:数组不能分割成两个元素和相等的子集。
/**
* @param {number[]} nums
* @return {boolean}
*/
var canPartition = function(nums) {
const sum = nums.reduce((a, b) => a + b, 0);
if (sum % 2 !== 0) return false;
const target = sum / 2;
const dp = new Array(target + 1).fill(false);
dp[0] = true;
for (const num of nums) {
for (let i = target; i >= num; i--) {
dp[i] = dp[i] || dp[i - num];
}
}
return dp[target];
};
十六、多维动态规划
91、【中等】不同路径
题目描述:
一个机器人位于一个m x n
网格的左上角 (起始点在下图中标记为 “Start” )。
机器人每次只能向下或者向右移动一步。机器人试图达到网格的右下角(在下图中标记为 “Finish” )。
问总共有多少条不同的路径?
示例 1:
输入:m = 3, n = 7
输出:28
示例 2:
输入:m = 3, n = 2
输出:3
解释:
从左上角开始,总共有 3 条路径可以到达右下角。
向右 -> 向下 -> 向下
向下 -> 向下 -> 向右
向下 -> 向右 -> 向下
示例 3:
输入:m = 7, n = 3
输出:28
示例 4:
输入:m = 3, n = 3
输出:6
/**
* @param {number} m
* @param {number} n
* @return {number}
*/
var uniquePaths = function(m, n) {
const f = new Array(n).fill(1); // 初始化动态规划数组
for (let i = 1; i < m; i++) { // 遍历每一行
for (let j = 1; j < n; j++) { // 遍历每一列
f[j] += f[j - 1]; // 更新当前网格的路径数
}
}
return f[n - 1]; // 返回结果
};
92、【中等】最小路径和
题目描述:
给定一个包含非负整数的m x n
网格grid
,请找出一条从左上角到右下角的路径,使得路径上的数字总和为最小。
**说明:**每次只能向下或者向右移动一步。
示例 1:
输入:grid = [[1,3,1],[1,5,1],[4,2,1]]
输出:7
解释:因为路径 1→3→1→1→1 的总和最小。
示例 2:
输入:grid = [[1,2,3],[4,5,6]]
输出:12
/**
* @param {number[][]} grid
* @return {number}
*/
var minPathSum = function(grid) {
if (!grid || grid.length === 0 || grid[0].length === 0) {
return 0;
}
const m = grid.length;
const n = grid[0].length;
const dp = new Array(m).fill().map(() => new Array(n).fill(0));
dp[0][0] = grid[0][0];
// 初始化第一行
for (let j = 1; j < n; j++) {
dp[0][j] = dp[0][j - 1] + grid[0][j];
}
// 初始化第一列
for (let i = 1; i < m; i++) {
dp[i][0] = dp[i - 1][0] + grid[i][0];
}
// 填充其余部分
for (let i = 1; i < m; i++) {
for (let j = 1; j < n; j++) {
dp[i][j] = Math.min(dp[i - 1][j], dp[i][j - 1]) + grid[i][j];
}
}
return dp[m - 1][n - 1];
};
93、【中等】最长回文子串
题目描述:
给你一个字符串s
,找到s
中最长的 回文 子串。
示例 1:
输入:s = "babad"
输出:"bab"
解释:“aba” 同样是符合题意的答案。
示例 2:
输入:s = "cbbd"
输出:"bb"
/**
* @param {string} s
* @return {string}
*/
var longestPalindrome = function(s) {
if (s.length < 1) return "";
let start = 0, end = 0;
for (let i = 0; i < s.length; i++) {
let len1 = expandAroundCenter(s, i, i); // 奇数长度
let len2 = expandAroundCenter(s, i, i + 1); // 偶数长度
let len = Math.max(len1, len2);
if (len > end - start) {
start = i - Math.floor((len - 1) / 2);
end = i + Math.floor(len / 2);
}
}
return s.substring(start, end + 1);
function expandAroundCenter(s, left, right) {
while (left >= 0 && right < s.length && s[left] === s[right]) {
left--;
right++;
}
return right - left - 1;
}
};
94、【中等】最长公共子序列
题目描述:
给定两个字符串text1
和text2
,返回这两个字符串的最长 公共子序列 的长度。如果不存在 公共子序列 ,返回0
。
一个字符串的 子序列 是指这样一个新的字符串:它是由原字符串在不改变字符的相对顺序的情况下删除某些字符(也可以不删除任何字符)后组成的新字符串。
- 例如,
"ace"
是"abcde"
的子序列,但"aec"
不是"abcde"
的子序列。两个字符串的 公共子序列 是这两个字符串所共同拥有的子序列。
示例 1:
输入:text1 = "abcde", text2 = "ace"
输出:3
解释:最长公共子序列是 “ace” ,它的长度为 3 。
示例 2:
输入:text1 = "abc", text2 = "abc"
输出:3
解释:最长公共子序列是 “abc” ,它的长度为 3 。
示例 3:
输入:text1 = "abc", text2 = "def"
输出:0
解释:两个字符串没有公共子序列,返回 0 。
/**
* @param {string} text1
* @param {string} text2
* @return {number}
*/
var longestCommonSubsequence = function(text1, text2) {
const m = text1.length;
const n = text2.length;
const dp = Array.from({ length: m + 1 }, () => new Array(n + 1).fill(0));
for (let i = 1; i <= m; i++) {
for (let j = 1; j <= n; j++) {
if (text1[i - 1] === text2[j - 1]) {
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[m][n];
};
95、【中等】编辑距离
题目描述:
给你两个单词word1
和word2
, 请返回将word1
转换成word2
所使用的最少操作数 。
你可以对一个单词进行如下三种操作:
- 插入一个字符
- 删除一个字符
- 替换一个字符
示例 1:
输入:word1 = "horse", word2 = "ros"
输出:3
解释:
horse -> rorse (将 ‘h’ 替换为 ‘r’)
rorse -> rose (删除 ‘r’)
rose -> ros (删除 ‘e’)
示例 2:
输入:word1 = "intention", word2 = "execution"
输出:5
解释:
intention -> inention (删除 ‘t’)
inention -> enention (将 ‘i’ 替换为 ‘e’)
enention -> exention (将 ‘n’ 替换为 ‘x’)
exention -> exection (将 ‘n’ 替换为 ‘c’)
exection -> execution (插入 ‘u’)
/**
* @param {string} word1
* @param {string} word2
* @return {number}
*/
var minDistance = function(word1, word2) {
const m = word1.length;
const n = word2.length;
const dp = Array.from({ length: m + 1 }, () => new Array(n + 1).fill(0));
for (let i = 0; i <= m; i++) {
dp[i][0] = i;
}
for (let j = 0; j <= n; j++) {
dp[0][j] = j;
}
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];
};
十七、技巧
96、【简单】只出现一次的数字
题目描述:
给你一个 非空 整数数组nums
,除了某个元素只出现一次以外,其余每个元素均出现两次。找出那个只出现了一次的元素。
你必须设计并实现线性时间复杂度的算法来解决此问题,且该算法只使用常量额外空间。
示例 1 :
输入:nums = [2,2,1]
输出:1
示例 2 :
输入:nums = [4,1,2,1,2]
输出:4
示例 3 :
输入:nums = [1]
输出:1
/**
* @param {number[]} nums
* @return {number}
*/
var singleNumber = function(nums) {
let result = 0;
for (const num of nums) {
result ^= num;
}
return result;
};
97、【简单】多数元素
题目描述:
给定一个大小为n
的数组nums
,返回其中的多数元素。多数元素是指在数组中出现次数 大于⌊ n/2 ⌋
的元素。
你可以假设数组是非空的,并且给定的数组总是存在多数元素。
示例 1:
输入:nums = [3,2,3]
输出:3
示例 2:
输入:nums = [2,2,1,1,1,2,2]
输出:2
/**
* @param {number[]} nums
* @return {number}
*/
var majorityElement = function(nums) {
let candidate = nums[0];
let count = 1;
for (let i = 1; i < nums.length; i++) {
if (count === 0) {
candidate = nums[i];
count = 1;
} else if (nums[i] === candidate) {
count++;
} else {
count--;
}
}
return candidate;
};
98、【中等】颜色分类
题目描述:
给定一个包含红色、白色和蓝色、共n
个元素的数组nums
,原地 对它们进行排序,使得相同颜色的元素相邻,并按照红色、白色、蓝色顺序排列。
我们使用整数0
、1
和2
分别表示红色、白色和蓝色。
必须在不使用库内置的 sort 函数的情况下解决这个问题。
示例 1:
输入:nums = [2,0,2,1,1,0]
输出:[0,0,1,1,2,2]
示例 2:
输入:nums = [2,0,1]
输出:[0,1,2]
/**
* @param {number[]} nums
* @return {void} Do not return anything, modify nums in-place instead.
*/
var sortColors = function(nums) {
let low = 0;
let mid = 0;
let high = nums.length - 1;
while (mid <= high) {
if (nums[mid] === 0) {
[nums[low], nums[mid]] = [nums[mid], nums[low]];
low++;
mid++;
} else if (nums[mid] === 1) {
mid++;
} else if (nums[mid] === 2) {
[nums[mid], nums[high]] = [nums[high], nums[mid]];
high--;
}
}
};
99、【中等】下一个排列
题目描述:
整数数组的一个 排列 就是将其所有成员以序列或线性顺序排列。
例如,arr = [1,2,3]
,以下这些都可以视作arr
的排列:[1,2,3]
、[1,3,2]
、[3,1,2]
、[2,3,1]
。
整数数组的 下一个排列 是指其整数的下一个字典序更大的排列。更正式地,如果数组的所有排列根据其字典顺序从小到大排列在一个容器中,那么数组的 下一个排列 就是在这个有序容器中排在它后面的那个排列。如果不存在下一个更大的排列,那么这个数组必须重排为字典序最小的排列(即,其元素按升序排列)。
例如,arr = [1,2,3]
的下一个排列是[1,3,2]
。
类似地,arr = [2,3,1]
的下一个排列是[3,1,2]
。
而arr = [3,2,1]
的下一个排列是[1,2,3]
,因为[3,2,1]
不存在一个字典序更大的排列。
给你一个整数数组nums
,找出nums
的下一个排列。
必须 原地 修改,只允许使用额外常数空间。
示例 1:
输入:nums = [1,2,3]
输出:[1,3,2]
示例 2:
输入:nums = [3,2,1]
输出:[1,2,3]
示例 3:
输入:nums = [1,1,5]
输出:[1,5,1]
/**
* @param {number[]} nums
* @return {void} Do not return anything, modify nums in-place instead.
*/
var nextPermutation = function(nums) {
let i = nums.length - 2;
while (i >= 0 && nums[i] >= nums[i + 1]) {
i--;
}
if (i >= 0) {
let j = nums.length - 1;
while (j >= 0 && nums[j] <= nums[i]) {
j--;
}
[nums[i], nums[j]] = [nums[j], nums[i]];
}
let left = i + 1;
let right = nums.length - 1;
while (left < right) {
[nums[left], nums[right]] = [nums[right], nums[left]];
left++;
right--;
}
};
100、【中等】寻找重复数
题目描述:
给定一个包含n + 1
个整数的数组nums
,其数字都在[1, n]
范围内(包括1
和n
),可知至少存在一个重复的整数。
假设nums
只有 一个重复的整数 ,返回 这个重复的数 。
你设计的解决方案必须 不修改 数组nums
且只用常量级O(1)
的额外空间。
示例 1:
输入:nums = [1,3,4,2,2]
输出:2
示例 2:
输入:nums = [3,1,3,4,2]
输出:3
示例 3:
输入:nums = [3,3,3,3,3]
输出:3
/**
* @param {number[]} nums
* @return {number}
*/
var findDuplicate = function(nums) {
let slow = nums[0];
let fast = nums[0];
// 检测环
do {
slow = nums[slow];
fast = nums[nums[fast]];
} while (slow !== fast);
// 找到环的入口
slow = nums[0];
while (slow !== fast) {
slow = nums[slow];
fast = nums[fast];
}
return slow;
};