Appearance
全局配置
Appearance
给定一个整数数组 nums 和一个整数目标值 target,请你在该数组中找出 和为目标值 target 的那 两个 整数,并返回它们的数组下标。
你可以假设每种输入只会对应一个答案。但是,数组中同一个元素在答案里不能重复出现。
你可以按任意顺序返回答案。
示例 1:
输入:nums = [2,7,11,15], target = 9
输出:[0,1]
解释:因为 nums[0] + nums[1] == 9 ,返回 [0, 1] 。
示例 2:
输入:nums = [3,2,4], target = 6
输出:[1,2]
示例 3:
输入:nums = [3,3], target = 6
输出:[0,1]
提示:
2 <= nums.length <= 10^4
-10^9 <= nums[i] <= 10^9
-10^9 <= target <= 10^9
只会存在一个有效答案
进阶:你可以想出一个时间复杂度小于 O(n2) 的算法吗?
利用map存储已遍历元素的索引,后续遍历的同时比较是否map中含有目标值和遍历元素的差值,有则输出索引
/**
* @param {number[]} nums
* @param {number} target
* @return {number[]}
*/
var twoSum2 = function(nums, target) {
let len = nums.length
for(let i = 0 ; i < len; i ++) {
for(let j = i + 1; j < len; j ++) {
if (nums[i] + nums[j] === target) {
return [i, j]
}
}
}
};
var twoSum = function(nums, target) {
const len = nums.length
const map = new Map()
for(let i = 0; i < len; i++) {
map.set(nums[i], i)
}
for (let i = 0; i < len; i++) {
const x = target - nums[i]
if (map.has(x)) {
const index = map.get(x)
if (i !== index) {
return [i, index]
}
}
}
return []
}
给你两个 非空 的链表,表示两个非负的整数。它们每位数字都是按照 逆序 的方式存储的,并且每个节点只能存储 一位 数字。
请你将两个数相加,并以相同形式返回一个表示和的链表。
你可以假设除了数字 0 之外,这两个数都不会以 0 开头。
示例 1:
输入:l1 = [2,4,3], l2 = [5,6,4]
输出:[7,0,8]
解释:342 + 465 = 807.
示例 2:
输入:l1 = [0], l2 = [0]
输出:[0]
示例 3:
输入:l1 = [9,9,9,9,9,9,9], l2 = [9,9,9,9]
输出:[8,9,9,9,0,0,0,1]
提示:
每个链表中的节点数在范围 [1, 100] 内
0 <= Node.val <= 9
题目数据保证列表表示的数字不含前导零
需要一个变量记录前一轮同单位节点是否相加是需要进位
处理好当前位取余
处理好同单位节点同时前进
/**
* Definition for singly-linked list.
* function ListNode(val, next) {
* this.val = (val===undefined ? 0 : val)
* this.next = (next===undefined ? null : next)
* }
*/
/**
* @param {ListNode} l1
* @param {ListNode} l2
* @return {ListNode}
*/
var addTwoNumbers = function(l1, l2) {
//进位
let addOne = 0
// // 创建一个头链表用于保存结果
let sum = new ListNode('0')
// // 保存头链表的位置用于最后的链表返回
let head = sum
// 在两个链表之中有一个存在的前提下执行下面的逻辑
while (addOne || l1 || l2) {
// 考虑[9999]与[99]相加的情况,当指针指向第三个9的时候l1的9是存在的而l2的9是不存在的,也就是为null,所以无法相加会进行报错,所以我们需要进行一步优化,如果值不存在时,将其设置为0
let val1 = l1 !== null ? l1.val : 0
let val2 = l2 !== null ? l2.val : 0
// 求和
let r1 = val1 + val2 + addOne
// 如果求和结果>=10,那么进位为1,否则为0
addOne = r1 >= 10 ? 1 : 0
// sum的下一个节点
sum.next = new ListNode(r1 % 10)
// sum指向下一个节点
sum = sum.next
// l1指向下一个节点,以便计算第二个节点的值
if (l1) l1 = l1.next
// l2指向下一个节点,以便计算第二个节点的值
if (l2) l2 = l2.next
}
// 返回计算结果,之所以用head.next是因为head中保存的第一个节点是刚开始定义的“0”
return head.next
};
给定一个字符串 s ,请你找出其中不含有重复字符的 最长子串 的长度。
示例 1:
输入: s = "abcabcbb"
输出: 3
解释: 因为无重复字符的最长子串是 "abc",所以其长度为 3。
示例 2:
输入: s = "bbbbb"
输出: 1
解释: 因为无重复字符的最长子串是 "b",所以其长度为 1。
示例 3:
输入: s = "pwwkew"
输出: 3
解释: 因为无重复字符的最长子串是 "wke",所以其长度为 3。
请注意,你的答案必须是 子串 的长度,"pwke" 是一个子序列,不是子串。
提示:
0 <= s.length <= 5 * 10^4
s 由英文字母、数字、符号和空格组成
map记录每个字符索引
滑动窗口更新字符索引及窗口大小
遇到新的重复字符,取原有字符索引的下一位作为新窗口起点
/**
* @param {string} s
* @return {number}
*/
var lengthOfLongestSubstring = function(s) {
const len = s.length
if (len == 0) return 0;
const map = new Map()
let max = 0;//最长子串长度
let left = 0;//滑动窗口左下标,i相当于滑动窗口右下标
for(let i = 0; i < len; i ++){
if(map.has(s.charAt(i))){ //charAt() 方法用于返回指定索引处的字符。索引范围为从 0 到 length() - 1。
left = Math.max(left, map.get(s.charAt(i)) + 1); //map.get():返回字符所对应的索引,当发现重复元素时,窗口左指针右移
} //map.get('a')=0,因为map中只有第一个a的下标,然后更新left指针到原来left的的下一位
map.set(s.charAt(i),i); //再更新map中a映射的下标
max = Math.max(max, i-left+1); //比较两个参数的大小
}
return max;
};
给定两个大小分别为 m 和 n 的正序(从小到大)数组 nums1 和 nums2。请你找出并返回这两个正序数组的 中位数 。
算法的时间复杂度应该为 O(log (m+n)) 。
示例 1:
输入:nums1 = [1,3], nums2 = [2]
输出:2.00000
解释:合并数组 = [1,2,3] ,中位数 2
示例 2:
输入:nums1 = [1,2], nums2 = [3,4]
输出:2.50000
解释:合并数组 = [1,2,3,4] ,中位数 (2 + 3) / 2 = 2.5
求中位数,其实就是求第 k 小数的一种特殊情况,而求第 k 小数有一种算法
二分查找
由于数列是有序的,其实我们一次遍历就相当于去掉不可能是中位数的一半。假设我们要找第 k 小数,我们可以每次循环排除掉 k/2 个数
无论是找第奇数个还是第偶数个数字,对我们的算法并没有影响,而且在算法进行中,k 的值都有可能从奇数变为偶数,最终都会变为 1 或者由于一个数组空了,直接返回结果
采用递归的思路,为了防止数组长度小于 k/2,所以每次比较 min(k/2,len(数组) 对应的数字,把小的那个对应的数组的数字排除,将两个新数组进入递归,并且 k 要减去排除的数字的个数。递归出口就是当 k=1 或者其中一个数字长度是 0 了
/**
* @param {number[]} nums1
* @param {number[]} nums2
* @return {number}
*/
var findMedianSortedArrays = function(nums1, nums2) {
let len1 = nums1.length, len2 = nums2.length
if (len1 > len2) return findMedianSortedArrays(nums2, nums1)//对nums1和nums2中长度较小的二分
let len = len1 + len2//总长
let start = 0, end = len1 //进行二分的开始和结束位置
let partLen1, partLen2
while (start <= end) {
partLen1 = (start + end) >> 1//nums1二分的位置
partLen2 = ((len + 1) >> 1) - partLen1//nums2二分的位置
//L1:nums1二分之后左边的位置,L2,nums1二分之后右边的位置
//R1:nums2二分之后左边的位置,R2,nums2二分之后右边的位置
//如果左边没字符了,就定义成-Infinity,让所有数都大于它,否则就是nums1二分的位置左边一个
let L1 = partLen1 === 0 ? -Infinity : nums1[partLen1 - 1]
//如果左边没字符了,就定义成-Infinity,让所有数都大于它,否则就是nums2二分的位置左边一个
let L2 = partLen2 === 0 ? -Infinity : nums2[partLen2 - 1]
//如果右边没字符了,就定义成Infinity,让所有数都小于它,否则就是nums1二分的位置
let R1 = partLen1 === len1 ? Infinity : nums1[partLen1]
//如果右边没字符了,就定义成Infinity,让所有数都小于它,否则就是nums1二分的位置
let R2 = partLen2 === len2 ? Infinity : nums2[partLen2]
if (L1 > R2) {//不符合交叉小于等于 继续二分
end = partLen1 - 1
} else if (L2 > R1) {//不符合交叉小于等于 继续二分
start = partLen1 + 1
} else { // L1 <= R2 && L2 <= R1 符合交叉小于等于
return len % 2 === 0 ?
(Math.max(L1, L2) + Math.min(R1, R2)) / 2 : //长度为偶数返回作左侧较大者和右边较小者和的一半
Math.max(L1, L2) //长度为奇数返回作左侧较大者
}
}
}
给你一个字符串 s,找到 s 中最长的回文子串。
如果字符串的反序与原始字符串相同,则该字符串称为回文字符串。
示例 1:
输入:s = "babad"
输出:"bab"
解释:"aba" 同样是符合题意的答案。
示例 2:
输入:s = "cbbd"
输出:"bb"
动态规划问题的一般形式就是求最值
求解动态规划的核心问题是穷举。因为要求最值,肯定要把所有可行的答案穷举出来,然后在其中找最值。
虽然动态规划的核心思想就是穷举求最值,但是问题可以千变万化,穷举所有可行解其实并不是一件容易的事,需要你熟练掌握递归思维,只有列出正确的「状态转移方程」,才能正确地穷举。需要判断算法问题是否具备「最优子结构」,是否能够通过子问题的最值得到原问题的最值。
动态规划问题存在「重叠子问题」,如果暴力穷举的话效率会很低,所以需要你使用「备忘录」或者「DP table」来优化穷举过程,避免不必要的计算。
步骤:
明确 base case :如为 0 或 1 时的结果
明确「状态」:原问题和子问题中会变化的变量
明确「选择」 :导致「状态」产生变化的行为
定义 dp 数组/函数的含义,确定状态转移方程。
dp[i][j]
表示s[i..j]
是否是回文串dp[i][j] = dp[i + 1][j - 1];
/**
* @param {string} s
* @return {string}
*/
// 动态规划:
var longestPalindrome1 = function(s) {
// 子序列问题是常见的算法问题,这类问题都是让你求一个最长子序列,因为最短子序列就是一个字符嘛,没啥可问的。一旦涉及到子序列和最值,那几乎可以肯定,考察的是动态规划技巧,时间复杂度一般都是 O(n^2)。动态规划就是为了减少重复计算的问题。动态规划听起来很高大上。其实说白了就是空间换时间,将计算结果暂存起来,避免重复计算。作用和工程中用 redis 做缓存有异曲同工之妙。
// 对于一个子串而言,如果它是回文串,并且长度大于 2,那么将它首尾的两个字母去除之后,它仍然是个回文串。例如对于字符串 “ababa”,如果我们已经知道 “bab” 是回文串,那么 “ababa” 一定是回文串,这是因为它的首尾两个字母都是 “a”。
// 根据这样的思路,我们就可以用动态规划的方法解决本题。我们用 P(i,j) 表示字符串 i 到 j 个字母组成的串(下文表示成 s[i:j])是否为回文串,是为true,不是为false。
// 不是的情况包含两种可能性:1.s[i,j] 本身不是一个回文串;2.i>j,此时 s[i,j] 本身不合法。
// 既然要用动态规划,那就要定义 dp 数组,找状态转移关系。
// dp[i][j] 表示字符串 i 到 j 个字母组成的串(下文表示成 s[i:j])是否是回文子串。
// 那么我们就可以写出动态规划的状态转移方程:dp[i][j] = (s[i] == s[j]) and (j - i < 3 or dp[i + 1][j - 1])
// 翻译过来就是,在s[i]等于s[j]的前提下,去除首尾字符后的子串的回文性质决定整体子串的回文性质。
// 边界条件是,(j - 1) - (i + 1) < 2 时,即 j - i < 3时,不构成区间,此时没有意义。即 j - i + 1 < 4,也就是说 s[i:j]长度为2或者3时,不用检查子串是否为回文。或者说长度为2或者3的只需要s[i] == s[j] 就已经是一个回文串了。
// 初始化:d[i][i] = true,即单个字符一定是回文串。实际上,计算时,对角线上的值也不会被其它计算参考。
// 输出:在得到一个状态为true的值时,记录起始位置和长度,填表完成以后再截取。
let len = s.length;
if (len < 2) {
return s;
}
// 最长回文子串长度
let maxLen = 1;
// 起始下标
let begin = 0;
// dp[i][j] 表示 s[i..j] 是否是回文串
const dp = new Array(len).fill(0).map(() => {
return new Array(len).fill(false)
})
// 初始化:所有长度为 1 的子串都是回文串
for (let i = 0; i < len; i++) {
dp[i][i] = true;
}
const chars = s.split('');
// 3. 状态转移
// 注意:先填左下角
// 填表规则:先一列一列的填写,再一行一行的填,保证左下方的单元格先进行计算
for (let j = 1;j < len;j++){
for (let i = 0; i < j; i++) {
// 头尾字符不相等,不是回文串
if (chars[i] != chars[j]){
dp[i][j] = false;
}else {
// 相等的情况下
// 考虑头尾去掉以后没有字符剩余,或者剩下一个字符的时候,肯定是回文串
if (j - i < 3){
dp[i][j] = true;
}else {
// 状态转移
dp[i][j] = dp[i + 1][j - 1];
}
}
// 只要dp[i][j] == true 成立,表示s[i...j] 是否是回文串
// 此时更新记录回文长度和起始位置
if (dp[i][j] && j - i + 1 > maxLen){
maxLen = j - i + 1;
begin = i;
}
}
}
return s.substring(begin, begin + maxLen);
};
// 中心扩展算法
var longestPalindrome = function(s) {
// 我们枚举所有的「回文子串中心」并尝试「扩展」,直到无法扩展为止,此时的回文串长度即为此「回文子串中心」下的最长回文串长度。我们对所有的长度求出最大值,即可得到最终的答案。
if (s.length<2){
return s
}
let res = ''
for (let i = 0; i < s.length; i++) {
// 回文子串长度是奇数
expandCenter(i, i)
// 回文子串长度是偶数
expandCenter(i, i + 1)
}
function expandCenter(left, right) {
while (left >= 0 && right < s.length && s[left] == s[right]) {
left--
right++
}
// 注意此处left, right的值循环完后 是恰好不满足循环条件的时刻
// 此时left 到 right的距离为right - left + 1,但是left, right 两个边界不能取,所以应该取left + 1 到 right - 1 的区间长度是right - left + 1
if (right - left - 1 > res.length) {
// slice也要取[left + 1, right - 1]这个区间
res = s.slice(left + 1, right)
}
}
return res
}
给你一个整数 x ,如果 x 是一个回文整数,返回 true ;否则,返回 false 。
回文数是指正序(从左向右)和倒序(从右向左)读都是一样的整数。
例如,121 是回文,而 123 不是。
示例 1:
输入:x = 121 输出:true 示例 2:
输入:x = -121 输出:false 解释:从左向右读, 为 -121 。 从右向左读, 为 121- 。因此它不是一个回文数。 示例 3:
输入:x = 10 输出:false 解释:从右向左读, 为 01 。因此它不是一个回文数。
提示:
-2^31 <= x <= 2^31 - 1
进阶:你能不将整数转为字符串来解决这个问题吗?
双指针,首尾同时移动的同时比较字符是否始终相同
/**
* @param {number} x
* @return {boolean}
*/
var isPalindrome = function(x) {
const str = x.toString()
const len = str.length
const middle = Math.floor(len / 2)
for(let i = 0; i < middle; i++) {
if (str[i] !== str[len - 1 - i]) {
return false
}
}
return true
};
给定一个长度为 n 的整数数组 height 。有 n 条垂线,第 i 条线的两个端点是 (i, 0) 和 (i, height[i]) 。
找出其中的两条线,使得它们与 x 轴共同构成的容器可以容纳最多的水。
返回容器可以储存的最大水量。
说明:你不能倾斜容器。
示例 1:
输入:[1,8,6,2,5,4,8,3,7]
输出:49
解释:图中垂直线代表输入数组 [1,8,6,2,5,4,8,3,7]。在此情况下,容器能够容纳水(表示为蓝色部分)的最大值为 49。
示例 2:
输入:height = [1,1]
输出:1
提示:
n == height.length
2 <= n <= 10^5
0 <= height[i] <= 10^4
双指针首尾指针会和的过程中始终面积取(指针矮的高度 * 双指针距离),排除时也排除矮的指针来缩短双指针距离
/**
* @param {number[]} height
* @return {number}
*/
var maxArea = function(height) {
// 左右指针
// 先保持最远,此时高度由左右两边最矮的决定,总是移动左右两边高度较小的那个指针,记录下当前面积。
// 思想是,两个指针中相对较矮的那条边更可能成为限制因素。移动指针后,存在有更高边的可能,能提升更多的面积。
// 感觉这个移动有点博弈论的味了,每次都移动自己最差的一边,虽然可能变得更差,但是总比不动(或者减小)强,动最差的部分可能找到更好的结果,但是动另一边总会更差或者不变。
let l = 0, r = height.length - 1;
// 最大面积
let ans = 0;
while (l < r) {
let area = Math.min(height[l], height[r]) * (r - l);
ans = Math.max(ans, area);
if (height[l] <= height[r]) {
++l;
}
else {
--r;
}
}
return ans;
};
给你一个整数数组 nums ,判断是否存在三元组 [nums[i], nums[j], nums[k]] 满足 i != j、i != k 且 j != k ,同时还满足 nums[i] + nums[j] + nums[k] == 0 。
请你返回所有和为 0 且不重复的三元组。
注意:答案中不可以包含重复的三元组。
示例 1:
输入:nums = [-1,0,1,2,-1,-4]
输出:[[-1,-1,2],[-1,0,1]]
解释:
nums[0] + nums[1] + nums[2] = (-1) + 0 + 1 = 0 。
nums[1] + nums[2] + nums[4] = 0 + 1 + (-1) = 0 。
nums[0] + nums[3] + nums[4] = (-1) + 2 + (-1) = 0 。
不同的三元组是 [-1,0,1] 和 [-1,-1,2] 。
注意,输出的顺序和三元组的顺序并不重要。
示例 2:
输入:nums = [0,1,1]
输出:[]
解释:唯一可能的三元组和不为 0 。
示例 3:
输入:nums = [0,0,0]
输出:[[0,0,0]]
解释:唯一可能的三元组和为 0 。
提示:
3 <= nums.length <= 3000
-10^5 <= nums[i] <= 10^5
先排序,使数组升序
值得注意的是,数组首尾元素的限制(最小不能大于0、最大值不能小于零)。
再遍历,每次循环的变量arr[i]视为三个数中的最小值,则arr[i]必须小于0
再以 i + 1 和 arr.lenght - 1为数组左右边界,确定双指针,作为第二、第三个数,并计算每轮中三数之和的结果,根据结果确定双指针的指针中是该缩小/增大,还是找到结果直接返回
/**
* @param {number[]} nums
* @return {number[][]}
*/
var threeSum = function(nums) {
if (nums.length < 3) {
return [];
}
// 从小到大排序
const arr = nums.sort((a,b) => a-b);
// 最小值大于 0 或者 最大值小于 0,说明没有无效答案
if (arr[0] > 0 || arr[arr.length - 1] < 0) {
return [];
}
const n = arr.length;
const res = [];
for (let i = 0; i < n; i ++) {
// 如果当前值大于 0,和右侧的值再怎么加也不会等于 0,所以直接退出
if (nums[i] > 0) {
return res;
}
// 当前循环的值和上次循环的一样,就跳过,避免重复值
if (i > 0 && arr[i] === arr[i - 1]) {
continue;
}
// 双指针
let l = i + 1;
let r = n - 1;
while(l < r) {
const temp = arr[i] + arr[l] + arr[r];
if (temp > 0) {
r --;
}
if (temp < 0) {
l ++;
}
if (temp === 0) {
res.push([nums[i], nums[l], nums[r]]);
// 跳过重复值
while(l < r && nums[l] === nums[l + 1]) {
l ++;
}
// 同上
while(l < r && nums[r] === nums[r - 1]) {
r --;
}
l ++;
r --;
}
}
}
return res;
};
给定一个仅包含数字 2-9 的字符串,返回所有它能表示的字母组合。答案可以按 任意顺序 返回。
给出数字到字母的映射如下(与电话按键相同)。注意 1 不对应任何字母。
示例 1:
输入:digits = "23"
输出:["ad","ae","af","bd","be","bf","cd","ce","cf"]
示例 2:
输入:digits = ""
输出:[]
示例 3:
输入:digits = "2"
输出:["a","b","c"]
提示:
0 <= digits.length <= 4
digits[i] 是范围 ['2', '9'] 的一个数字。
确定2-9的字母映射表
dfs递归,传入当前已递归结果字符和当前递归深度。
确定终止条件:当递归到传入字符数组的长度 - 1的索引深度时,结束递归。将结果放入结果数组。
不满足终止条件时,根据映射表,找到对应字符,继续累积到当期已递归的结果中,并增加深度,继续递归。
/**
* @param {string} digits
* @return {string[]}
*/
const letterCombinations = (digits) => {
if (digits.length == 0) return [];
const res = [];
const map = { '2': 'abc', '3': 'def', '4': 'ghi', '5': 'jkl', '6': 'mno', '7': 'pqrs', '8': 'tuv', '9': 'wxyz' };
// dfs: 当前构建的字符串为curStr,现在“翻译”到第i个数字,基于此继续“翻译”
const dfs = (curStr, i) => { // curStr是当前字符串,i是扫描的指针
if (i > digits.length - 1) { // 指针越界,递归的出口
res.push(curStr); // 将解推入res
return; // 结束当前递归分支
}
const letters = map[digits[i]]; // 当前数字对应的字母
for (const letter of letters) { // 一个字母是一个选择,对应一个递归分支
dfs(curStr + letter, i + 1); // 选择翻译成letter,生成新字符串,i指针右移继续翻译(递归)
}
};
dfs('', 0); // 递归的入口,初始字符串为'',从下标0开始翻译
return res;
};
给你一个链表,删除链表的倒数第 n 个结点,并且返回链表的头结点。
示例 1:
输入:head = [1,2,3,4,5], n = 2
输出:[1,2,3,5]
示例 2:
输入:head = [1], n = 1
输出:[]
示例 3:
输入:head = [1,2], n = 1
输出:[1]
提示:
链表中结点的数目为 sz
1 <= sz <= 30
0 <= Node.val <= 100
1 <= n <= sz
进阶:你能尝试使用一趟扫描实现吗?
快慢指针:
1.先将 slow、fast 指针赋值为 head
2.然后 fast 后移 n 步
3.然后快慢指针 slow 和 fast 一起往后移动,直到 fast.next ===null
4.此时 slow.next 就是要删除的结点,删除即可
/**
* Definition for singly-linked list.
* function ListNode(val, next) {
* this.val = (val===undefined ? 0 : val)
* this.next = (next===undefined ? null : next)
* }
*/
/**
* @param {ListNode} head
* @param {number} n
* @return {ListNode}
*/
// 1.先将 slow、fast 指针赋值为 head
// 2.然后 fast 后移 n 步
// 3.然后快慢指针 slow 和 fast 一起往后移动,直到 fast.next === null
// 4.此时 slow.next 就是要删除的结点,删除即可
// 注意点:当 fast 后移 n 步时,如果题目给的 n 与链表中总结点个数相同时,即 要删除的是头结点,那么 fast 会后移到链表外面,即 fast === null,即可通过 fast 是否为空来决定要不要直接返回结果
var removeNthFromEnd = function(head, n) {
let slow = head, fast = head;
// 先让 fast 往后移 n 位
while(n--) {
fast = fast.next;
}
// 如果 n 和 链表中总结点个数相同,即要删除的是链表头结点时,fast 经过上一步已经到外面了
if(!fast) {
return head.next;
}
// 然后 快慢指针 一起往后遍历,当 fast 是链表最后一个结点时,此时 slow 下一个就是要删除的结点
while(fast.next) {
slow = slow.next;
fast = fast.next;
}
slow.next = slow.next.next;
return head;
};
给定一个只包括 '(',')','{','}','[',']' 的字符串 s ,判断字符串是否有效。
有效字符串需满足:
左括号必须用相同类型的右括号闭合。
左括号必须以正确的顺序闭合。
每个右括号都有一个对应的相同类型的左括号。
示例 1:
输入:s = "()"
输出:true
示例 2:
输入:s = "()[]{}"
输出:true
示例 3:
输入:s = "(]"
输出:false
提示:
1 <= s.length <= 10^4
s 仅由括号 '()[]{}' 组成
确定右括号的映射表
遍历字符串,左括号入栈
遇到右括号,看栈顶元素是否和此类型右括号匹配
遍历完成看栈元素是否为空
/**
* @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;
};
将两个升序链表合并为一个新的 升序 链表并返回。新链表是通过拼接给定的两个链表的所有节点组成的。
示例 1:
输入:l1 = [1,2,4], l2 = [1,3,4]
输出:[1,1,2,3,4,4]
示例 2:
输入:l1 = [], l2 = []
输出:[]
示例 3:
输入:l1 = [], l2 = [0]
输出:[0]
提示:
两个链表的节点数目范围是 [0, 50]
-100 <= Node.val <= 100
l1 和 l2 均按 非递减顺序 排列
每轮比较都需要比较两个链表节点的大小,只有更小的节点,才能链接在新节点上,并且所属链表指向下一节点
当某个链表为空时,链接另一个链表全部剩下的节点即可
/**
* Definition for singly-linked list.
* function ListNode(val, next) {
* this.val = (val===undefined ? 0 : val)
* this.next = (next===undefined ? null : next)
* }
*/
/**
* @param {ListNode} list1
* @param {ListNode} list2
* @return {ListNode}
*/
// 迭代
var mergeTwoLists = function(l1, l2) {
// 我们设定一个哨兵节点 prehead ,这可以在最后让我们比较容易地返回合并后的链表
const prehead = new ListNode(-1);
// 我们维护一个 prev 指针,我们需要做的是调整它的 next 指针
let prev = prehead;
// 我们重复以下过程,直到 l1 或者 l2 指向了 null
while (l1 != null && l2 != null) {
// 当 l1 和 l2 都不是空链表时,判断 l1 和 l2 哪一个链表的头节点的值更小,将较小值的节点添加到结果里
// 如果 l1 当前节点的值小于等于 l2 ,我们就把 l1 当前的节点接在 prev 节点的后面同时将 l1 指针往后移一位。否则,我们对 l2 做同样的操作。
if (l1.val <= l2.val) {
prev.next = l1;
l1 = l1.next;
} else {
prev.next = l2;
l2 = l2.next;
}
// 不管我们将哪一个元素接在了后面,我们都需要把 prev 向后移一位。
prev = prev.next;
}
// 在循环终止的时候, l1 和 l2 至多有一个是非空的。
// 由于输入的两个链表都是有序的,所以不管哪个链表是非空的,它包含的所有元素都比前面已经合并链表中的所有元素都要大。这意味着我们只需要简单地将非空链表接在合并链表的后面,并返回合并链表即可。
// 合并后 l1 和 l2 最多只有一个还未被合并完,我们直接将链表末尾指向未合并完的链表即可
prev.next = l1 === null ? l2 : l1;
return prehead.next;
};
// 递归
var mergeTwoLists1 = function(l1, l2) {
if (l1 === null) {
return l2;
} else if (l2 === null) {
return l1;
} else if (l1.val < l2.val) {
l1.next = mergeTwoLists(l1.next, l2);
return l1;
} else {
l2.next = mergeTwoLists(l1, l2.next);
return l2;
}
};
数字 n 代表生成括号的对数,请你设计一个函数,用于能够生成所有可能的并且 有效的 括号组合。
示例 1:
输入:n = 3
输出:["((()))","(()())","(())()","()(())","()()()"]
示例 2:
输入:n = 1
输出:["()"]
提示:
1 <= n <= 8
需递归、回溯:边界条件是构建的字符串长度为2n,参数为剩下的左括号数量、剩下的右括号数量、已构建字符
并且遵循以下规则:
1.只要(有剩,就可以选(。
2.当剩下的)比(多时,才可以选),否则,)不能选,选了就非法。因为剩下的)比(少时,意味着使用的)比(多,这显然是不合法的。
/**
* @param {number} n
* @return {string[]}
*/
var generateParenthesis = function (n) {
// 思路:就是不停选括号,要么选左括号,要么选右括号。
// 1.只要(有剩,就可以选(。
// 2.当剩下的)比(多时,才可以选),否则,)不能选,选了就非法。因为剩下的)比(少时,意味着使用的)比(多,这显然是不合法的。
const res = [];
const dfs = (lRemain, rRemain, str) => { // 左右括号所剩的数量,str是当前构建的字符串
if (str.length == 2 * n) { // 字符串构建完成
res.push(str); // 加入解集
return; // 结束当前递归分支
}
if (lRemain > 0) { // 只要左括号有剩,就可以选它,然后继续做选择(递归)
dfs(lRemain - 1, rRemain, str + "(");
}
if (lRemain < rRemain) { // 右括号比左括号剩的多,才能选右括号
dfs(lRemain, rRemain - 1, str + ")"); // 然后继续做选择(递归)
}
};
dfs(n, n, ""); // 递归的入口,剩余数量都是n,初始字符串是空串
return res;
};
给你一个链表,两两交换其中相邻的节点,并返回交换后链表的头节点。你必须在不修改节点内部的值的情况下完成本题(即,只能进行节点交换)。
示例 1:
输入:head = [1,2,3,4]
输出:[2,1,4,3]
示例 2:
输入:head = []
输出:[]
示例 3:
输入:head = [1]
输出:[1]
提示:
链表中节点的数目在范围 [0, 100] 内
0 <= Node.val <= 100
两个节点两个节点向后移动,每轮循环内用一个临时变量存储后一个节点,并进行节点交换
/**
* Definition for singly-linked list.
* function ListNode(val, next) {
* this.val = (val===undefined ? 0 : val)
* this.next = (next===undefined ? null : next)
* }
*/
/**
* @param {ListNode} head
* @return {ListNode}
*/
var swapPairs = function(head) {
// 创建哑结点 dummyHead,令 dummyHead.next = head
const dummyHead = new ListNode(0);
dummyHead.next = head;
// 令 temp 表示当前到达的节点,初始时 temp = dummyHead
let temp = dummyHead;
// 如果 temp 的后面没有节点或者只有一个节点,则没有更多的节点需要交换,因此结束交换
while (temp.next !== null && temp.next.next !== null) {
// 获得 temp 后面的两个节点 node1 和 node2,通过更新节点的指针关系实现两两交换节点。
const node1 = temp.next;
const node2 = temp.next.next;
// 交换之前的节点关系是 temp -> node1 -> node2,交换之后的节点关系要变成 temp -> node2 -> node1
temp.next = node2;
node1.next = node2.next;
node2.next = node1;
// 完成上述操作之后,节点关系即变成 temp -> node2 -> node1。再令 temp = node1,对链表中的其余节点进行两两交换,直到全部节点都被两两交换。
temp = node1;
}
// 两两交换链表中的节点之后,新的链表的头节点是 dummyHead.next,返回新的链表的头节点即可。
return dummyHead.next;
};
给你链表的头节点 head ,每 k 个节点一组进行翻转,请你返回修改后的链表。
k 是一个正整数,它的值小于或等于链表的长度。如果节点总数不是 k 的整数倍,那么请将最后剩余的节点保持原有顺序。
你不能只是单纯的改变节点内部的值,而是需要实际进行节点交换。
示例 1:
输入:head = [1,2,3,4,5], k = 2
输出:[2,1,4,3,5]
示例 2:
输入:head = [1,2,3,4,5], k = 3
输出:[3,2,1,4,5]
提示:
链表中的节点数目为 n
1 <= k <= n <= 5000
0 <= Node.val <= 1000
进阶:你可以设计一个只用 O(1) 额外内存空间的算法解决此问题吗?
链表节点按照 k 个一组分组,所以可以使用一个指针 head 依次指向每组的头节点。
这个指针每次向前移动 k 步,直至链表结尾。
对于每个分组,我们先判断它的长度是否大于等于 k。若是,我们就翻转这部分链表,否则不需要翻转。
对于一个子链表,除了翻转其本身之外,还需要将子链表的头部与上一个子链表连接,以及子链表的尾部与下一个子链表连接。
/**
* Definition for singly-linked list.
* function ListNode(val, next) {
* this.val = (val===undefined ? 0 : val)
* this.next = (next===undefined ? null : next)
* }
*/
/**
* @param {ListNode} head
* @param {number} k
* @return {ListNode}
*/
// 我们需要把链表节点按照 k 个一组分组,所以可以使用一个指针 head 依次指向每组的头节点。这个指针每次向前移动 k 步,直至链表结尾。对于每个分组,我们先判断它的长度是否大于等于 k。若是,我们就翻转这部分链表,否则不需要翻转。
// 翻转一个链表并不难,过程可以参考「206. 反转链表」。但是对于一个子链表,除了翻转其本身之外,还需要将子链表的头部与上一个子链表连接,以及子链表的尾部与下一个子链表连接。
const myReverse = (head, tail) => {
let prev = tail.next;
let p = head;
while (prev !== tail) {
const nex = p.next;
p.next = prev;
prev = p;
p = nex;
}
return [tail, head];
}
var reverseKGroup = function(head, k) {
const hair = new ListNode(0);
hair.next = head;
let pre = hair;
while (head) {
let tail = pre;
// 查看剩余部分长度是否大于等于 k
for (let i = 0; i < k; ++i) {
tail = tail.next;
if (!tail) {
return hair.next;
}
}
const nex = tail.next;
[head, tail] = myReverse(head, tail);
// 把子链表重新接回原链表
// 还记得我们创建了节点 pre 吗?这个节点一开始被连接到了头节点的前面,而无论之后链表有没有翻转,它的 next 指针都会指向正确的头节点。那么我们只要返回它的下一个节点就好了。
pre.next = head;
tail.next = nex;
pre = tail;
head = tail.next;
}
return hair.next;
};
整数数组的一个 排列 就是将其所有成员以序列或线性顺序排列。
例如,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]
提示:
1 <= nums.length <= 100
0 <= nums[i] <= 100
给定若干个数字,将其组合为一个整数。
将这些数字重新排列,以得到下一个更大的整数。如 123 下一个更大的数为 132。
如果没有更大的整数,则输出最小的整数。
思路: 1.在 尽可能靠右的低位 进行交换,需要 从后向前 查找
2.将一个 尽可能小的「大数」 与前面的「小数」交换。比如 123465,下一个排列应该把 5 和 4 交换而不是把 6 和 4 交换
3.将「大数」换到前面后,需要将「大数」后面的所有数 重置为升序,升序排列就是最小的排列。
以 123465 为例:首先按照上一步,交换 5 和 4,得到 123564;然后需要将 5 之后的数重置为升序,得到 123546。
显然 123546 比 123564 更小,123546 就是 123465 的下一个排列
/**
* @param {number[]} nums
* @return {void} Do not return anything, modify nums in-place instead.
*/
var nextPermutation = function(nums) {
// “下一个排列” 的定义是:给定数字序列的字典序中下一个更大的排列。如果不存在下一个更大的排列,则将数字重新排列成最小的排列(即升序排列)。
// 我们可以将该问题形式化地描述为:给定若干个数字,将其组合为一个整数。如何将这些数字重新排列,以得到下一个更大的整数。如 123 下一个更大的数为 132。如果没有更大的整数,则输出最小的整数。
// 以 1,2,3,4,5,6 为例,其排列依次为:
/**
123456
123465
123546
...
654321
*/
// 可以看到有这样的关系:123456 < 123465 < 123546 < ... < 654321。
// 我们希望下一个数 比当前数大,这样才满足 “下一个排列” 的定义。因此只需要 将后面的「大数」与前面的「小数」交换,就能得到一个更大的数。比如 123456,将 5 和 6 交换就能得到一个更大的数 123465。
// 我们还希望下一个数 增加的幅度尽可能的小,这样才满足“下一个排列与当前排列紧邻“的要求。为了满足这个要求,我们需要:
/**
1.在 尽可能靠右的低位 进行交换,需要 从后向前 查找
2.将一个 尽可能小的「大数」 与前面的「小数」交换。比如 123465,下一个排列应该把 5 和 4 交换而不是把 6 和 4 交换
3.将「大数」换到前面后,需要将「大数」后面的所有数 重置为升序,升序排列就是最小的排列。
以 123465 为例:首先按照上一步,交换 5 和 4,得到 123564;然后需要将 5 之后的数重置为升序,得到 123546。
显然 123546 比 123564 更小,123546 就是 123465 的下一个排列
*/
// 以求 12385764 的下一个排列为例:
//从后向前找第一次出现邻近升序的对儿 A[i] < A[j]
// 首先从后向前查找第一个相邻升序的元素对 (i,j)。这里 i=4,j=5,对应的值为 5,7:
let i = nums.length - 2, j = nums.length - 1;
while(i >= 0){
if(nums[i] < nums[j]){
break;
}
i--; j--;
}
//本身就是最后一个排列(全部降序), 把整体整个翻转变升序进行返回
if(i < 0) {
reverse(nums, 0, nums.length-1);
return;
}
//从[j, end]从后向前找第一个令A[i] < A[k]的 k值 (不邻近升序对儿 ,也有可能近邻)
// 在 [j,end) 从后向前查找第一个大于 A[i] 的值 A[k]。这里 A[i] 是 5,故 A[k] 是 6:
let k;
for(k = nums.length-1; k >= j; k--){
if(nums[i] < nums[k]) break;
}
//得到k
//交换i, k
// 将 A[i] 与 A[k] 交换。这里交换 5、6:
swap(nums, i, k);
// nums[j,end]是降序 改为升序
// 这时 [j,end) 必然是降序,逆置 [j,end),使其升序。这里逆置 [7,5,4]:
reverse(nums, j, nums.length-1);
// 因此,12385764 的下一个排列就是 12386457。
};
function reverse(nums, l, r){
//双指针升序
while(l < r){
swap(nums, l, r);
l++; r--;
}
}
function swap(nums, i, k){
let tmp = nums[i];
nums[i] = nums[k];
nums[k] = tmp;
}
给你一个只包含 '(' 和 ')' 的字符串,找出最长有效(格式正确且连续)括号子串的长度。
示例 1:
输入:s = "(()"
输出:2
解释:最长有效括号子串是 "()"
示例 2:
输入:s = ")()())"
输出:4
解释:最长有效括号子串是 "()()"
示例 3:
输入:s = ""
输出:0
提示:
0 <= s.length <= 3 * 10^4
s[i] 为 '(' 或 ')'
动态规划解法:
定义 dp[i] 表示以下标 i 字符结尾的最长有效括号的长度
s[i]=‘)’ 且 s[i−1]=‘(’,也就是字符串形如 “……()”,我们可以推出:
即“……()”或“()”
dp[i] = (i >= 2 ? dp[i - 2] : 0) + 2
s[i]=‘)’ 且 s[i−1]=‘)’,也就是字符串形如 “……))”,我们可以推出:
如果 s[i−dp[i−1]−1]=‘(’,那么dp[i] = dp[i−1] + dp[i−dp[i−1]−2] + 2,即“……((……))”
即【“……((……))”】 或 【 “((……))” | “(((……))” | “)((……))”】
dp[i] = dp[i - 1] + ((i - dp[i - 1]) >= 2 ? dp[i - dp[i - 1] - 2] : 0) + 2;
/**
* @param {string} s
* @return {number}
*/
// 动态规划解法:
var longestValidParentheses1 = function(s) {
let maxans = 0;
// 定义 dp[i] 表示以下标 i 字符结尾的最长有效括号的长度
let dp = new Array(s.length).fill(0);
// 我们从前往后遍历字符串求解 dp 值
for (let i = 1; i < s.length; i++) {
// 有效的子串一定以 ‘)’ 结尾,因此我们可以知道以 ‘(’ 结尾的子串对应的 dp 值必定为 0 ,我们只需要求解 ‘)’ 在 dp 数组中对应位置的值。
if (s.charAt(i) == ')') {
// s[i]=‘)’ 且 s[i−1]=‘(’,也就是字符串形如 “……()”,我们可以推出:dp[i]=dp[i−2]+2
// 我们可以进行这样的转移,是因为结束部分的 "()" 是一个有效子字符串,并且将之前有效子字符串的长度增加了 2 。
if (s.charAt(i - 1) == '(') {
// “……()”或“()”
dp[i] = (i >= 2 ? dp[i - 2] : 0) + 2;
// s[i]=‘)’ 且 s[i−1]=‘)’,也就是字符串形如 “……))”,我们可以推出:
// 如果 s[i−dp[i−1]−1]=‘(’,那么dp[i] = dp[i−1] + dp[i−dp[i−1]−2] + 2,即“……((……))”
} else if (i - dp[i - 1] > 0 && s.charAt(i - dp[i - 1] - 1) == '(') {
// 【“……((……))”】 或 【 “((……))” | “(((……))” | “)((……))”】
dp[i] = dp[i - 1] + ((i - dp[i - 1]) >= 2 ? dp[i - dp[i - 1] - 2] : 0) + 2;
}
maxans = Math.max(maxans, dp[i]);
}
}
return maxans;
};
// 栈解法
var longestValidParentheses = function(s) {
// 有效子串的最大长度
let maxans = 0;
// 初始化一个长度为n的栈
const stack = [];
// 首先把-1放入栈中,它代表第一个没匹配到的“)”的索引,因为每次为一个单独的“)”时,总是需要重置有效字符串的开始索引。
stack.push(-1);
// 遍历整个字符串
for (let i = 0; i < s.length; i++) {
// 当遇到“(”时,入栈
if (s.charAt(i) == '(') {
stack.push(i);
// 当遇到“)”时
} else {
// 需要将栈顶元素出栈,这里会有两种情况:
stack.pop();
// 如果已经清空了栈,则证明需要从当前“)”处重置有效字符串开始索引
if (stack.length == 0) {
// 将该索引放入栈中
stack.push(i);
// 如果尚未清空栈,说明该“)”有一个有效的“(”与之对应
} else {
// 取当前有效子串和历史计算的最大子串的长度的最大值
// 其中,当前有效子串的长度值为有效字符串开始索引到当前字符索引的差,即这种情况“……)(……)”中,两个右括号中间的长度
maxans = Math.max(maxans, i - stack[stack.length - 1]);
}
}
}
return maxans;
};
整数数组 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
提示:
1 <= nums.length <= 5000
-10^4 <= nums[i] <= 10^4
nums 中的每个值都 独一无二
题目数据保证 nums 在预先未知的某个下标上进行了旋转
-10^4 <= target <= 10^4
二分查找
取一个mid,在0-mid和mid-n中,至少有一半是有序的
分界点,用来分界两个升序数组。所以我们可以总结以下规律:
1、分界点的左侧元素 >= 第一个元素
2、分界点的右侧元素 < 第一个元素
最大最小值就在分界点(虚拟点)的旁边
所以二分时,二分处的值如果大于左边界的值,则证明左半边是有序的,我们可以继续根据目标值此时是否处于有序区间进一步判断 如果目标值位于有序区间,则继续取二分处的前一个数为右边界,否则,则缩小左边界。
右半边有序亦然。
/**
* @param {number[]} nums
* @param {number} target
* @return {number}
*/
// 二分查找:
const search = function(nums, target) {
// 取一个mid,在0-mid和mid-n中,至少有一半是有序的,
// 分界点,用来分界两个升序数组。所以我们可以总结以下规律:
// 1、分界点的左侧元素 >= 第一个元素
// 2、分界点的右侧元素 < 第一个元素
// 最大最小值就在分界点(虚拟点)的旁边
if (!nums.length) return -1
let left = 0, right = nums.length - 1, mid
while (left <= right) {
mid = left + ((right - left) >> 1)
// 终止条件是,mid element === target,结束
if (nums[mid] === target) {
return mid
}
// 左半边有序
if (nums[mid] >= nums[left]) {
// 如果目标值在左半边有序区间范围内
if (target >= nums[left] && target < nums[mid]) {
right = mid - 1
} else {
left = mid + 1
}
// 右半边有序
} else {
// 目标值在右半边有序区间范围内
if (target > nums[mid] && target <= nums[right]) {
left = mid + 1
} else {
right = mid - 1
}
}
}
return -1
}
给你一个按照非递减顺序排列的整数数组 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]
提示:
0 <= nums.length <= 10^5
-10^9 <= nums[i] <= 10^9
nums 是一个非递减数组
-10^9 <= target <= 10^9
二分查找:
向下取整二分索引
求目标值的第一个数,边界条件是大于等于目标值
而求目标值的最后一个位置,边界条件是大于目标值的第一个数的前一个数
/**
* @param {number[]} nums
* @param {number} target
* @return {number[]}
*/
// 二分查找:
const binarySearch = (nums, target, lower) => {
// 由于数组已经排序,因此整个数组是单调递增的,我们可以利用二分法来加速查找的过程。
// 考虑 target 开始和结束位置,其实我们要找的就是数组中「第一个等于 target 的位置」(记为 leftIdx)和「第一个大于 target 的位置减一」(记为 rightIdx)。
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;
}
var searchRange = function(nums, target) {
let ans = [-1, -1];
// 二分查找中,寻找 leftIdx 即为在数组中寻找第一个大于等于 target 的下标,寻找 rightIdx 即为在数组中寻找第一个大于 target 的下标,然后将下标减一。
// 两者的判断条件不同,为了代码的复用,我们定义 binarySearch(nums, target, lower) 表示在 nums 数组中二分查找 target 的位置,如果 lower 为 true,则查找第一个大于等于 target 的下标,否则查找第一个大于 target 的下标。
const leftIdx = binarySearch(nums, target, true);
const rightIdx = binarySearch(nums, target, false) - 1;
// 最后,因为 target 可能不存在数组中,因此我们需要重新校验我们得到的两个下标 leftIdx 和 rightIdx,看是否符合条件,
// 如果符合条件就返回 [leftIdx,rightIdx],不符合就返回 [−1,−1]。
if (leftIdx <= rightIdx && rightIdx < nums.length && nums[leftIdx] === target && nums[rightIdx] === target) {
ans = [leftIdx, rightIdx];
}
return ans;
};
给定一个排序数组和一个目标值,在数组中找到目标值,并返回其索引。如果目标值不存在于数组中,返回它将会被按顺序插入的位置。
请必须使用时间复杂度为 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
提示:
1 <= nums.length <= 10^4
-10^4 <= nums[i] <= 10^4
nums 为 无重复元素 的 升序 排列数组
-10^4 <= target <= 10^4
二分查找
二分值大于等于目标值所找到的数是目标值或者其右边界
/**
* @param {number[]} nums
* @param {number} target
* @return {number}
*/
// 二分查找:
var searchInsert = function(nums, target) {
const n = nums.length;
// 在一个有序数组中找第一个大于等于 target 的下标
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;
};
给你一个 无重复元素 的整数数组 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
输出: []
提示:
1 <= candidates.length <= 30
2 <= candidates[i] <= 40
candidates 的所有元素 互不相同
1 <= target <= 40
递归和回溯dfs
递归的终止条件为 target≤0 或者 candidates 数组被全部找完(找了所有可能)。
每次我们可以选择跳过不用第 idx 个数,即执行 dfs(target,combine,idx+1)。
也可以选择使用第 idx 个数,即执行 dfs(target−candidates[idx],combine,idx),注意到每个数字可以被无限制重复选取,因此搜索的下标仍为 idx。
/**
* @param {number[]} candidates
* @param {number} target
* @return {number[][]}
*/
var combinationSum = function(candidates, target) {
const ans = [];
const dfs = (target, combine, idx) => {
// 递归的终止条件为 target≤0 或者 candidates 数组被全部用完。
if (idx === candidates.length) {
return;
}
if (target === 0) {
ans.push(combine);
return;
}
// 直接跳过
// 每次我们可以选择跳过不用第 idx 个数,即执行 dfs(target,combine,idx+1)。
dfs(target, combine, idx + 1);
// 选择当前数
// 也可以选择使用第 idx 个数,即执行 dfs(target−candidates[idx],combine,idx),注意到每个数字可以被无限制重复选取,因此搜索的下标仍为 idx。
if (target - candidates[idx] >= 0) {
dfs(target - candidates[idx], [...combine, candidates[idx]], idx);
}
}
dfs(target, [], 0);
return ans;
};
给你一个未排序的整数数组 nums ,请你找出其中没有出现的最小的正整数。
请你实现时间复杂度为 O(n) 并且只使用常数级别额外空间的解决方案。
示例 1:
输入:nums = [1,2,0]
输出:3
示例 2:
输入:nums = [3,4,-1,1]
输出:2
示例 3:
输入:nums = [7,8,9,11,12]
输出:1
提示:
1 <= nums.length <= 5 * 10^5
-2^31 <= nums[i] <= 2^31 - 1
哈希表法:
只需从最小的正整数 1开始,依次判断 2 3 4 直到数组的长度 N 是否在数组中;
如果当前考虑的数不在这个数组中,我们就找到了这个缺失的最小正整数;
核心思想: 将数组视为哈希表
而要找的数一定在 [1, N + 1] 左闭右闭(这里 N 是数组的长度)这个区间里。因此,我们可以就把原始的数组当做哈希表来使用。事实上,哈希表其实本身也是一个数组;我们要找的数就在 [1, N + 1] 里,最后 N + 1 这个元素我们不用找。因为在前面的 N 个元素都找不到的情况下,我们才返回 N + 1;
循环两次:
第一次:
就把 1 这个数放到下标为 0 的位置, 2 这个数放到下标为 1 的位的位置,按照这种思路整理一遍数组。
第二次:
然后我们再遍历一次数组,第 1个遇到的它的值不等于下标的那个数,就是我们要找的缺失的第一个正数。
这个思想就相当于我们自己编写哈希函数,这个哈希函数的规则特别简单,那就是数值为 i 的数映射到下标为 i - 1 的位置。
/**
* @param {number[]} nums
* @return {number}
*/
var firstMissingPositive = function(nums) {
// 哈希表法:
// 只需从最小的正整数 1开始,依次判断 2 3 4 直到数组的长度 N 是否在数组中;
// 如果当前考虑的数不在这个数组中,我们就找到了这个缺失的最小正整数;
// 由于我们需要依次判断某一个正整数是否在这个数组里,我们可以先把这个数组中所有的元素放进哈希表。接下来再遍历的时候,就可以以 O(1) 的时间复杂度判断某个正整数是否在这个数组;
// 时间复杂度o(n),空间复杂度o(n),但不满足题意o(1)的空间复杂度要求。
// 核心思想: 将数组视为哈希表
// 题目要求我们「只能使用常数级别的空间」,而要找的数一定在 [1, N + 1] 左闭右闭(这里 N 是数组的长度)这个区间里。因此,我们可以就把原始的数组当做哈希表来使用。事实上,哈希表其实本身也是一个数组;我们要找的数就在 [1, N + 1] 里,最后 N + 1 这个元素我们不用找。因为在前面的 N 个元素都找不到的情况下,我们才返回 N + 1;
// 就把 1 这个数放到下标为 0 的位置, 2 这个数放到下标为 1 的位的位置,按照这种思路整理一遍数组。然后我们再遍历一次数组,第 1 个遇到的它的值不等于下标的那个数,就是我们要找的缺失的第一个正数。这个思想就相当于我们自己编写哈希函数,这个哈希函数的规则特别简单,那就是数值为 i 的数映射到下标为 i - 1 的位置。
const len = nums.length;
// 交换数组元素
function swap(nums, index1, index2) {
let temp = nums[index1];
nums[index1] = nums[index2];
nums[index2] = temp;
}
// 遍历数组,将其放到想要构建哈希数组正确的位置上:例如[3,4,-1,1]遍历后结果是[1, -1, 3, 4]
for (let i = 0; i < len; i++) {
// 遍历过程中,找出处在1至len的数,如果该数不在它应该在的位置nums[i] - 1,就交换当前位置和应该在位置的数
// 交换一次后,我们使得n[i] - 1位置上的数是哈希表正确的位置,但是换过去nums[i]里的数并不一定是它该在的位置,并且如果这个数仍处于1至len之间,那么继续循环交换,使得该位置上要么处于正确的数,要么不在1到len的范围
while (nums[i] > 0 && nums[i] <= len && nums[nums[i] - 1] != nums[i]) {
// 满足在指定范围内、并且没有放在正确的位置上,才交换
// 例如:数值 3 应该放在索引 2 的位置上
swap(nums, nums[i] - 1, i);
}
}
// [1, -1, 3, 4]
// 找出第 1 个遇到的它的值不等于下标加一的那个数
for (let i = 0; i < len; i++) {
if (nums[i] != i + 1) {
return i + 1;
}
}
// 都正确则返回数组长度 + 1
return len + 1;
};
给定 n 个非负整数表示每个宽度为 1 的柱子的高度图,计算按此排列的柱子,下雨之后能接多少雨水。
示例 1:
输入:height = [0,1,0,2,1,0,1,3,2,1,2,1]
输出:6
解释:上面是由数组 [0,1,0,2,1,0,1,3,2,1,2,1] 表示的高度图,在这种情况下,可以接 6 个单位的雨水(蓝色部分表示雨水)。
示例 2:
输入:height = [4,2,0,3,2,5]
输出:9
提示:
n == height.length
1 <= n <= 2 * 10^4
0 <= height[i] <= 10^5
好比好多层木桶套在一起,因此使用双指针,由外往内计算盛水量
装水的多少,当然根据木桶效应,我们只需要看左边最高的墙和右边最高的墙中较矮的一个就够了。
分别确定当前列的左边和右边最高的墙高度:max_left 和 max_right
不管max_left和max_right谁大,我们需要用矮的来减去当前列的高度来计算雨水容量
1.较矮的墙的高度大于当前列的墙的高度: 注水量为较矮的一边,减去当前列的高度
2.较矮的墙的高度小于当前列的墙的高度:注水量为0
3.较矮的墙的高度等于当前列的墙的高度:注水量为0
到双指针碰面完成循环时,确定所有列的蓄水量
/**
* @param {number[]} height
* @return {number}
*/
var trap1 = function(height) {
let sum = 0;
//最两端的列不用考虑,因为一定不会有水。所以下标从 1 到 length - 2
for (let i = 1; i < height.length - 1; i++) {
let max_left = 0;
//找出左边最高
for (let j = i - 1; j >= 0; j--) {
if (height[j] > max_left) {
max_left = height[j];
}
}
let max_right = 0;
//找出右边最高
for (let j = i + 1; j < height.length; j++) {
if (height[j] > max_right) {
max_right = height[j];
}
}
//找出两端较小的
let min = Math.min(max_left, max_right);
//只有较小的一段大于当前列的高度才会有水,其他情况不会有水
if (min > height[i]) {
sum = sum + (min - height[i]);
}
}
return sum;
};
var trap = function(height) {
// 求每一列的水,我们只需要关注当前列,以及左边最高的墙,右边最高的墙就够了。
// 装水的多少,当然根据木桶效应,我们只需要看左边最高的墙和右边最高的墙中较矮的一个就够了。
// 根据较矮的那个墙和当前列的墙的高度可以分为三种情况:
// 1.较矮的墙的高度大于当前列的墙的高度: 注水量为较矮的一边,减去当前列的高度
// 2.较矮的墙的高度小于当前列的墙的高度:注水量为0
// 3.较矮的墙的高度等于当前列的墙的高度:注水量为0
// 遍历每一列,然后分别求出这一列两边最高的墙。找出较矮的一端,和当前列的高度比较,结果就是上边的三种情况。
// 现在需要分别确定当前列的左边和右边最高的墙高度:max_left 和 max_right
// 不管max_left和max_right谁大,我们需要用矮的来减去当前列的高度来计算雨水容量
// 我们从两边边缘开始算起,当两个指针没有相遇时,可以明确的是left或right中矮的那个是限制条件,因为至少另一个比他们高。
// 所以不论是left还是right,我们可以根据矮的那个和当前列的高度算出当前列蓄水量。
// 直到双指针碰面完成循环时,确定所有列的蓄水量
let ans = 0;
let left = 0, right = height.length - 1;
let leftMax = 0, rightMax = 0;
while (left < right) {
leftMax = Math.max(leftMax, height[left]);
rightMax = Math.max(rightMax, height[right]);
if (height[left] < height[right]) {
ans += leftMax - height[left];
++left;
} else {
ans += rightMax - height[right];
--right;
}
}
return ans;
}
给定一个长度为 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
提示:
1 <= nums.length <= 10^4
0 <= nums[i] <= 1000
题目保证可以到达 nums[n-1]
思路:每次在上次能跳到的范围(end)内选择一个能跳的最远的位置(也就是能跳到max_far位置的点)作为下次的起跳点 !
比较之前和本次循环中的最远位置,得到已遍历元素范围内能达到的最远位置
如果循环到了已遍历元素范围内能达到的最远位置,即最小跳跃次数起跳点处,它也是上次起跳的起点处,记录这个位置并更新步数
/**
* @param {number[]} nums
* @return {number}
*/
var jump = function(nums) {
// 思路:每次在上次能跳到的范围(end)内选择一个能跳的最远的位置(也就是能跳到max_far位置的点)作为下次的起跳点 !
const length = nums.length;
// 上次跳跃右边界(下次的最右起跳点),初始时起跳点在0处
let end = 0;
// 目前能跳到的最远位置
let maxPosition = 0;
// 跳跃次数
let steps = 0;
for (let i = 0; i < length - 1; i++) {
// 比较之前和本次循环中的最远位置,得到已遍历元素范围内能达到的最远位置
maxPosition = Math.max(maxPosition, i + nums[i]);
// 如果循环到了已遍历元素范围内能达到的最远位置,即最小跳跃次数起跳点处,它也是上次起跳的起点处
if (i == end) {
// 记录下这个位置
end = maxPosition;
// 更新步数
steps++;
}
}
// 返回所需步数
return steps;
};
给定一个不含重复数字的数组 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]]
提示:
1 <= nums.length <= 6
-10 <= nums[i] <= 10
nums 中的所有整数 互不相同
递归、回溯
dfs:使用一个used数组来记录哪些元素已经使用过,即已经被放入path中。
边界条件:当path的长度和nums长度一样大时,代表找到一个符号条件的排列,放入结果集。
表当前元素已经使用过:used[i] = true
回溯的过程中,将当前的节点从 path 中删除:path.pop();used[i] = false;
/**
* @param {number[]} nums
* @return {number[][]}
*/
// 使用一个used数组来记录哪些元素已经使用过,即已经被放入path中。
// 当path的长度和nums长度一样大时,代表找到一个符号条件的排列,放入结果集。
var permute = function(nums) {
const res = [], path = []
const used = new Array(nums.length).fill(false)
const dfs = () => {
if (path.length == nums.length) {
res.push(path.slice())
return
}
for (let i = 0; i < nums.length; i++) {
if (used[i]) continue
path.push(nums[i])
// 代表当前元素已经使用过
used[i] = true
dfs()
// 回溯的过程中,将当前的节点从 path 中删除
path.pop()
used[i] = false
}
}
dfs()
return res
};
给定一个 n × n 的二维矩阵 matrix 表示一个图像。请你将图像顺时针旋转 90 度。
你必须在 原地 旋转图像,这意味着你需要直接修改输入的二维矩阵。请不要 使用另一个矩阵来旋转图像。
示例 1:
输入:matrix = [[1,2,3],[4,5,6],[7,8,9]]
输出:[[7,4,1],[8,5,2],[9,6,3]]
示例 2:
输入:matrix = [[5,1,9,11],[2,4,8,10],[13,3,6,7],[15,14,12,16]]
输出:[[15,13,2,5],[14,3,4,1],[12,6,8,9],[16,7,10,11]]
提示:
n == matrix.length == matrix[i].length
1 <= n <= 20
-1000 <= matrix[i][j]
<= 1000
用翻转代替旋转
水平翻转(保持每列不动,从上到下变为从下到上):
matrix[row][col]
水平轴翻转matrix[n−row−1][col]
主对角线翻转(左上到右下):
[matrix[i][j], matrix[j][i]] = [matrix[j][i], matrix[i][j]];
/**
* @param {number[][]} matrix
* @return {void} Do not return anything, modify matrix in-place instead.
*/
// 用翻转代替旋转
var rotate = function(matrix) {
const n = matrix.length;
// 水平翻转(保持每列不动,从上到下变为从下到上)
// matrix[row][col] 水平轴翻转 matrix[n−row−1][col]
for (let i = 0; i < Math.floor(n / 2); i++) {
for (let j = 0; j < n; j++) {
[matrix[i][j], matrix[n - i - 1][j]] = [matrix[n - i - 1][j], matrix[i][j]];
}
}
// 主对角线翻转(左上到右下)
// matrix[row][col] 主对角线翻转matrix[col][row]
for (let i = 0; i < n; i++) {
for (let j = 0; j < i; j++) {
[matrix[i][j], matrix[j][i]] = [matrix[j][i], matrix[i][j]];
}
}
};
var rotate1 = function(matrix) {
const n = matrix.length;
// 辅助数组
// 我们使用一个与 matrix 大小相同的辅助数组 matrix_new,临时存储旋转后的结果。
const matrix_new = new Array(n).fill(0).map(() => new Array(n).fill(0));
// 对于矩阵中的元素 matrix[row][col],在旋转后,它的新位置为 matrix_new[col][n−row−1]
// 我们遍历 matrix 中的每一个元素,根据上述规则将该元素存放到 matrix_new中对应的位置。
for (let i = 0; i < n; i++) {
for (let j = 0; j < n; j++) {
matrix_new[j][n - i - 1] = matrix[i][j];
}
}
// 在遍历完成之后,再将 matrix_new中的结果复制到原数组中即可
for (let i = 0; i < n; i++) {
for (let j = 0; j < n; j++) {
matrix[i][j] = matrix_new[i][j];
}
}
};
给你一个字符串数组,请你将 字母异位词 组合在一起。可以按任意顺序返回结果列表。
字母异位词 是由重新排列源单词的所有字母得到的一个新单词。
示例 1:
输入: strs = ["eat", "tea", "tan", "ate", "nat", "bat"]
输出: [["bat"],["nat","tan"],["ate","eat","tea"]]
示例 2:
输入: strs = [""]
输出: [[""]]
示例 3:
输入: strs = ["a"]
输出: [["a"]]
提示:
1 <= strs.length <= 10^4
0 <= strs[i].length <= 100
strs[i] 仅包含小写字母
将字符按ASCII编码排序后存入map
首次遇到:ans.set(asc, [strs[i]])
后续再遇到:ans.get(asc).push(strs[i])
/**
* @param {string[]} strs
* @return {string[][]}
*/
var groupAnagrams = function(strs) {
const len = strs.length, ans = new Map()
for (let i = 0; i < len; i ++) {
let asc = strs[i].split('').map(c => c.charCodeAt()).sort().join()
if (ans.has(asc)) {
ans.get(asc).push(strs[i])
} else {
ans.set(asc, [strs[i]])
}
}
return Array.from(ans.values())
}
按照国际象棋的规则,皇后可以攻击与之处在同一行或同一列或同一斜线上的棋子。
n 皇后问题 研究的是如何将 n 个皇后放置在 n×n 的棋盘上,并且使皇后彼此之间不能相互攻击。
给你一个整数 n ,返回所有不同的 n 皇后问题 的解决方案。
每一种解法包含一个不同的 n 皇后问题 的棋子放置方案,该方案中 'Q' 和 '.' 分别代表了皇后和空位。
示例 1:
输入:n = 4
输出:[[".Q..","...Q","Q...","..Q."],["..Q.","Q...","...Q",".Q.."]]
解释:如上图所示,4 皇后问题存在两个不同的解法。
示例 2:
输入:n = 1
输出:[["Q"]]
提示:
1 <= n <= 9
N 皇后问题是一个经典的回溯算法问题,要求在 N×N 的棋盘上放置 N 个皇后,使得任何两个皇后都不能在同一行、同一列或同一对角线上。
首先,我们定义了一个 isValid 函数,用来检查在棋盘的特定位置放置皇后是否合法。这个函数会检查当前位置的上方、左上方和右上方是否已经有皇后。这是因为在回溯过程中,我们是一行一行地放置皇后的,所以只需要检查已经放置了皇后的行。
然后,我们定义了一个 transformChessBoard 函数,将二维的棋盘转换为一维的字符串数组,以便于存储和返回结果。
接着,我们定义了 backtracing 函数,这是回溯算法的核心部分。这个函数会递归地尝试在每一行的每一列放置皇后,如果当前位置合法,就放置皇后并递归地尝试放置下一行的皇后。如果放置下一行的皇后失败,就会撤销当前位置的皇后,尝试下一列。如果在当前行的所有列都不能放置皇后,就会回溯到上一行,撤销上一行的皇后并尝试其它列。如果成功地在所有行放置了皇后,就将当前棋盘的状态添加到结果中。
最后,我们创建了一个 N×N 的空棋盘,并开始回溯过程。当所有可能的棋盘状态都尝试完后,就返回结果。
递归
n 为输入的棋盘大小,row 是当前递归到棋盘的第几行了,chessBoard为当前棋盘
棋盘的宽度就是for循环的长度,递归的深度就是棋盘的高度,这样就可以套进回溯法的模板
row === n时:将二维数组变为一维字符串数组,
递归函数内迭代,每列只有验证合法就可以放‘Q’
两种情况:放置
chessBoard[row][col] = 'Q'
,回溯撤销chessBoard[row][col] = '.'
isValid:某个位置i,j,在n*n棋盘chessBoard上检查行、检查列、检查45度、检查135度上是否还有字符'Q',如果有,则不合法。
/**
* @param {number} n
* @return {string[][]}
*/
var solveNQueens = function(n) {
/**
void backtracking(参数) {
if (终止条件) {
存放结果;
return;
}
for (选择:本层集合中元素(树中节点孩子的数量就是集合的大小)) {
处理节点;
backtracking(路径,选择列表); // 递归
回溯,撤销处理结果
}
}
*/
function isValid(row, col, chessBoard, n) {
// 检查列
// 这是一个剪枝
for(let i = 0; i < row; i++) {
if(chessBoard[i][col] === 'Q') {
return false
}
}
// 检查 45度角是否有皇后
for(let i = row - 1, j = col - 1; i >= 0 && j >= 0; i--, j--) {
if(chessBoard[i][j] === 'Q') {
return false
}
}
// 检查 135度角是否有皇后
for(let i = row - 1, j = col + 1; i >= 0 && j < n; i--, j++) {
if(chessBoard[i][j] === 'Q') {
return false
}
}
return true
}
// 将二维数组变为一维字符串数组
function transformChessBoard(chessBoard) {
let chessBoardBack = []
chessBoard.forEach(row => {
let rowStr = ''
row.forEach(value => {
rowStr += value
})
chessBoardBack.push(rowStr)
})
return chessBoardBack
}
let result = []
// n 为输入的棋盘大小
// row 是当前递归到棋盘的第几行了
// 棋盘的宽度就是for循环的长度,递归的深度就是棋盘的高度,这样就可以套进回溯法的模板
function backtracing(row,chessBoard) {
if(row === n) {
result.push(transformChessBoard(chessBoard))
return
}
for(let col = 0; col < n; col++) {
// 验证合法就可以放
if(isValid(row, col, chessBoard, n)) {
// 放置皇后
chessBoard[row][col] = 'Q'
backtracing(row + 1,chessBoard)
// 回溯,撤销皇后
chessBoard[row][col] = '.'
}
}
}
let chessBoard = new Array(n).fill([]).map(() => new Array(n).fill('.'))
backtracing(0,chessBoard)
return result
};
给你一个整数数组 nums ,请你找出一个具有最大和的连续子数组(子数组最少包含一个元素),返回其最大和。
子数组 是数组中的一个连续部分。
示例 1:
输入:nums = [-2,1,-3,4,-1,2,1,-5,4]
输出:6
解释:连续子数组 [4,-1,2,1] 的和最大,为 6 。
示例 2:
输入:nums = [1]
输出:1
示例 3:
输入:nums = [5,4,-1,7,8]
输出:23
提示:
1 <= nums.length <= 10^5
-10^4 <= nums[i] <= 10^4
进阶:如果你已经实现复杂度为 O(n) 的解法,尝试使用更为精妙的 分治法 求解。
一次迭代
若当前指针所指元素之前的和小于当前元素值,则丢弃当前元素之前的数列:pre = Math.max(pre + x, x);
重新比较记录最大值:maxAns = Math.max(maxAns, pre);
/**
* @param {number[]} nums
* @return {number}
*/
var maxSubArray = function(nums) {
// 核心思想:若当前指针所指元素之前的和小于0,则丢弃当前元素之前的数列
// 举个例子: [-2, 1, -3, 4, -1, 2, 1, -5, 4]
/* 当前值 之前和 当前和 最大和
第1轮 -2 null -2 -2
第2轮 1 -2 1 1
第3轮 -3 1 -2 1
第4轮 4 -2 4 4
第5轮 -1 4 3 4
第6轮 2 3 5 5
第7轮 1 5 6 6
第8轮 -5 6 1 6
第9轮 4 1 5 6
*/
let pre = 0, maxAns = nums[0];
nums.forEach((x) => {
pre = Math.max(pre + x, x);
maxAns = Math.max(maxAns, pre);
});
return maxAns;
};
给你一个 m 行 n 列的矩阵 matrix ,请按照 顺时针螺旋顺序 ,返回矩阵中的所有元素。
示例 1:
输入:matrix = [[1,2,3],[4,5,6],[7,8,9]]
输出:[1,2,3,6,9,8,7,4,5]
示例 2:
输入:matrix = [[1,2,3,4],[5,6,7,8],[9,10,11,12]]
输出:[1,2,3,4,8,12,11,10,9,5,6,7]
提示:
m == matrix.length
n == matrix[i].length
1 <= m, n <= 10 -100 <= matrix[i][j]
<= 100
可以将矩阵看成若干层,首先输出最外层的元素,其次输出次外层的元素,直到输出最内层的元素。
假设当前层的左上角位于 (top,left),右下角位于 (bottom,right)
上:从左到右遍历上侧元素,依次为 (top,left)到 (top,right)。
右:上到下遍历右侧元素,依次为 (top+1,right)到(bottom,right)
下:从右到左遍历下侧元素,依次为 (bottom,right−1) 到 (bottom,left+1)
左:从下到上遍历左侧元素,依次为 (bottom,left) 到 (top+1,left)。
遍历完当前层的元素之后,将 left 和 top 分别增加 1,将 right 和 bottom 分别减少 1,进入下一层继续遍历,直到遍历完所有元素为止。
/**
* @param {number[][]} matrix
* @return {number[]}
*/
// 按层模拟:
// 可以将矩阵看成若干层,首先输出最外层的元素,其次输出次外层的元素,直到输出最内层的元素。
// 定义矩阵的第 k 层是到最近边界距离为 k 的所有顶点。
var spiralOrder = function(matrix) {
if (!matrix.length || !matrix[0].length) {
return [];
}
const rows = matrix.length, columns = matrix[0].length;
// 最终顺时针输出顺序
const order = [];
// 假设当前层的左上角位于 (top,left),右下角位于 (bottom,right),按照如下顺序遍历当前层的元素。
let left = 0, right = columns - 1, top = 0, bottom = rows - 1;
while (left <= right && top <= bottom) {
// 上:
// // 从左到右遍历上侧元素,依次为 (top,left)到 (top,right)。
for (let column = left; column <= right; column++) {
order.push(matrix[top][column]);
}
// 右:
// 从上到下遍历右侧元素,依次为 (top+1,right)到(bottom,right)。
for (let row = top + 1; row <= bottom; row++) {
order.push(matrix[row][right]);
}
// 如果 left<right 且 top<bottom
if (left < right && top < bottom) {
// 下:
// 则从右到左遍历下侧元素,依次为 (bottom,right−1) 到 (bottom,left+1)
for (let column = right - 1; column > left; column--) {
order.push(matrix[bottom][column]);
}
// 左
// 以及从下到上遍历左侧元素,依次为 (bottom,left) 到 (top+1,left)。
for (let row = bottom; row > top; row--) {
order.push(matrix[row][left]);
}
}
// 遍历完当前层的元素之后,将 left 和 top 分别增加 1,将 right 和 bottom 分别减少 1,进入下一层继续遍历,直到遍历完所有元素为止。
[left, right, top, bottom] = [left + 1, right - 1, top + 1, bottom - 1];
}
return order;
};
给你一个非负整数数组 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 , 所以永远不可能到达最后一个下标。
提示:
1 <= nums.length <= 104
0 <= nums[i] <= 105
尽可能到达最远位置(贪心)。
如果能到达某个位置,那一定能到达它前面的所有位置。
初始化最远位置为 0,然后遍历数组,如果当前位置能到达,并且 当前位置 + 跳数 > 最远位置,就更新最远位置。
最后比较最远位置和数组长度。
/**
* @param {number[]} nums
* @return {boolean}
*/
var canJump = function(nums) {
// 思路:
// 尽可能到达最远位置(贪心)。
// 如果能到达某个位置,那一定能到达它前面的所有位置。
// 方法:
// 初始化最远位置为 0,然后遍历数组,如果当前位置能到达,并且 当前位置 + 跳数 > 最远位置,就更新最远位置。
// 最后比较最远位置和数组长度。
// 时间复杂度 O(n),空间复杂度 O(1)。
const len = nums.length
// 初始化当前能到达最远的位置
let max_i = 0
// i为当前位置,jump是当前位置的跳数
for (let i = 0; i < len - 1; i++) {
// 如果当前位置能到达,并且当前位置+跳数>最远位置
if ((max_i >= i) && (i + nums[i] > max_i)) {
// 更新最远能到达位置
max_i = i + nums[i]
}
// 最远位置到达下个位置,则提前结束
if (max_i < i) {
return false
}
}
return max_i >= len - 1
};
以数组 intervals 表示若干个区间的集合,其中单个区间为 intervals[i] = [starti, endi] 。请你合并所有重叠的区间,并返回 一个不重叠的区间数组,该数组需恰好覆盖输入中的所有区间 。
示例 1:
输入:intervals = [[1,3],[2,6],[8,10],[15,18]]
输出:[[1,6],[8,10],[15,18]]
解释:区间 [1,3] 和 [2,6] 重叠, 将它们合并为 [1,6].
示例 2:
输入:intervals = [[1,4],[4,5]]
输出:[[1,5]]
解释:区间 [1,4] 和 [4,5] 可被视为重叠区间。
提示:
1 <= intervals.length <= 10^4
intervals[i].length == 2
0 <= starti <= endi <= 10^4
一次迭代
1.先按照每个子数组左区间从小到大的顺序给数组排序,如[[1,3],[2,6],[8,10],[15,18]]排序完为[[1,3], [2,6], [8, 10], [15, 18]]
2.排序后,循环从往后比较,将每次前一个数组右区间的值记为t,如果后一个数组的左区间小于t,则合并这两个数组为一个数组;如果大于t,则后一个数组加入,更新t值。
/**
* @param {number[][]} intervals
* @return {number[][]}
*/
var merge = function(intervals) {
// 思路:
// 1.先按照每个子数组左区间从小到大的顺序给数组排序,如[[1,3],[2,6],[8,10],[15,18]]排序完为[[1,3], [2,6], [8, 10], [15, 18]]
// 2.排序后,循环从往后比较,将每次前一个数组右区间的值记为t,如果后一个数组的左区间小于t,则合并这两个数组为一个数组;如果大于t,则后一个数组加入,更新t值。
// 这里选择哪一种排序方式是一个优化时间复杂度的点,从代码简洁性来说,这里选择sort函数冒泡排序。
const sortIntervals = intervals.sort((([a], [b]) => a - b))
// 这里t为每次遍历后的当前所有数组区间中的最大值。初始将t设为-Infinity是因为需要保证第一个元素的左区间大于t,从而将第一个元素添加到最终的结果数组ans中
// t为当前循环右边界最大值
let t = -Infinity, ans = []
// 循环遍历排序数组:这里无非是2种情况:
sortIntervals.forEach((item, i) => {
// 1.当前元素的左区间比已遍历数组区间的最大值还大,证明新区间不在前一个数组区间范围内,直接添加即可。
if (item[0] > t) {
ans.push(item)
// 更新已遍历数组区间的最大值。
t = item[1]
} else {
// 2.当前元素的左区间在前一个数组区间的右区间之内,即前一个数组区间和当前数组区间是连续的,此时取出前一个数组区间,并结合当前数组区间来确定新连续数组的左右区间值是多少
// 取出前一个数组
const newArr = ans.pop()
// 新数组的右区间取前一个数组右区间和本次循环数组右区间的最大值
newArr[1] = Math.max(item[1], t)
// 将构造好的新数组添加到答案数组中
ans.push(newArr)
// 更新已遍历数组区间的最大值为新数组的右区间
t = newArr[1]
}
})
return ans
};
一个机器人位于一个 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
提示:
1 <= m, n <= 100
题目数据保证答案小于等于 2 * 109
动态规划:
f[i][0] = 1;
f[0][j] = 1;
f[i][j] = f[i - 1][j] + f[i][j - 1];
/**
* @param {number} m
* @param {number} n
* @return {number}
*/
// 动态规划:
var uniquePaths1 = function(m, n) {
// 如果 i=0,那么 f(i−1,j) 并不是一个满足要求的状态,我们需要忽略这一项;同理,如果 j=0,那么 f(i,j−1) 并不是一个满足要求的状态,我们需要忽略这一项。
// 初始条件为 f(0,0)=1,即从左上角走到左上角有一种方法。
const f = new Array(m).fill(0).map(() => new Array(n).fill(0));
// 为了方便代码编写,我们可以将所有的 f(0,j) 以及 f(i,0) 都设置为边界条件,它们的值均为 1。
for (let i = 0; i < m; i++) {
f[i][0] = 1;
}
for (let j = 0; j < n; j++) {
f[0][j] = 1;
}
// 每一步只能从向下或者向右移动一步,因此要想走到 (i,j),如果向下走一步,那么会从 (i−1,j) 走过来;如果向右走一步,那么会从 (i,j−1) 走过来。
// 即:f(i,j) = f(i−1,j) + f(i,j−1)
for (let i = 1; i < m; i++) {
for (let j = 1; j < n; j++) {
f[i][j] = f[i - 1][j] + f[i][j - 1];
}
}
// 最终的答案为 f(m−1,n−1)
return f[m - 1][n - 1];
// 时间复杂度:O(mn)。
// 空间复杂度:O(mn),即为存储所有状态需要的空间。注意到 f(i,j) 仅与第 i 行和第 i−1 行的状态有关,因此我们可以使用滚动数组代替代码中的二维数组,使空间复杂度降低为 O(n)。此外,由于我们交换行列的值并不会对答案产生影响,因此我们总可以通过交换 m 和 n 使得 m≤n,这样空间复杂度降低至 O(min(m,n))。
};
// 组合数学:
// 从左上角到右下角的过程中,我们需要移动 m+n−2 次,其中有 m−1 次向下移动,n−1 次向右移动。因此路径的总数,就等于从 m+n−2 次移动中选择 m−1 次向下移动的方案的组合
var uniquePaths = function(m, n) {
let ans = 1;
for (let x = n, y = 1; y < m; ++x, ++y) {
ans = Math.floor(ans * x / y);
}
return ans;
};
给定一个包含非负整数的 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
提示:
m == grid.length
n == grid[i].length
1 <= m, n <= 200
0 <= grid[i][j]
<= 200
由于路径的方向只能是向下或向右
dp[0][0] = grid[0][0];
dp[i][0] = dp[i - 1][0] + grid[i][0];
dp[0][j] = dp[0][j - 1] + grid[0][j];
dp[i][j] = Math.min(dp[i - 1][j], dp[i][j - 1]) + grid[i][j];
/**
* @param {number[][]} grid
* @return {number}
*/
var minPathSum = function(grid) {
// 由于路径的方向只能是向下或向右,因此网格的第一行的每个元素只能从左上角元素开始向右移动到达,网格的第一列的每个元素只能从左上角元素开始向下移动到达,此时的路径是唯一的,因此每个元素对应的最小路径和即为对应的路径上的数字总和。
// 对于不在第一行和第一列的元素,可以从其上方相邻元素向下移动一步到达,或者从其左方相邻元素向右移动一步到达,元素对应的最小路径和等于其上方相邻元素与其左方相邻元素两者对应的最小路径和中的最小值加上当前元素的值。
// 由于每个元素对应的最小路径和与其相邻元素对应的最小路径和有关,因此可以使用动态规划求解。
if (grid == null || grid.length == 0 || grid[0].length == 0) {
return 0;
}
let rows = grid.length, columns = grid[0].length;
// 创建二维数组 dp,与原始网格的大小相同,dp[i][j] 表示从左上角出发到 (i,j) 位置的最小路径和。
const dp = new Array(rows).fill(0).map(() => {
return new Array(columns).fill(0)
})
// 显然,dp[0][0]=grid[0][0]。
dp[0][0] = grid[0][0];
// 对于 dp 中的其余元素,通过以下状态转移方程计算元素值:
// 当 i>0 且 j=0 时,dp[i][0] = dp[i−1][0] + grid[i][0]。
for (let i = 1; i < rows; i++) {
dp[i][0] = dp[i - 1][0] + grid[i][0];
}
// 当 i=0 且 j>0 时,dp[0][j] = dp[0][j−1] + grid[0][j]。
for (let j = 1; j < columns; j++) {
dp[0][j] = dp[0][j - 1] + grid[0][j];
}
// 当 i>0 且 j>0 时,dp[i][j] = min(dp[i−1][j], dp[i][j−1]) + grid[i][j]。
for (let i = 1; i < rows; i++) {
for (let j = 1; j < columns; j++) {
dp[i][j] = Math.min(dp[i - 1][j], dp[i][j - 1]) + grid[i][j];
}
}
// 最后得到 dp[m−1][n−1] 的值即为从网格左上角到网格右下角的最小路径和。
return dp[rows - 1][columns - 1];
// 时间复杂度:O(mn),其中 n 分别是网格的行数和列数。需要对整个网格遍历一次,计算 dp 的每个元素的值。
// 空间复杂度:O(mn),其中 n 分别是网格的行数和列数。创建一个二维数组 dp,和网格大小相同。空间复杂度可以优化,例如每次只存储上一行的 dp 值,则可以将空间复杂度优化到 O(n)。
};
假设你正在爬楼梯。需要 n 阶你才能到达楼顶。
每次你可以爬 1 或 2 个台阶。你有多少种不同的方法可以爬到楼顶呢?
示例 1:
输入:n = 2
输出:2
解释:有两种方法可以爬到楼顶。
示例 2:
输入:n = 3
输出:3
解释:有三种方法可以爬到楼顶。
提示:
1 <= n <= 45
dp[0] = 1;
dp[1] = 1;
dp[i] = dp[i - 1] + dp[i - 2];
/**
* @param {number} n
* @return {number}
*/
var climbStairs1 = function(n) {
const dp = [];
dp[0] = 1;
dp[1] = 1;
for(let i = 2; i <= n; i++) {
dp[i] = dp[i - 1] + dp[i - 2];
}
return dp[n];
};
var climbStairs = function(n) {
// 我们用 f(x) 表示爬到第 x 级台阶的方案数,考虑最后一步可能跨了一级台阶,也可能跨了两级台阶,所以我们可以列出如下式子:
// f(x)=f(x−1)+f(x−2)
// 它意味着爬到第 x 级台阶的方案数是爬到第 x−1 级台阶的方案数和爬到第 x-2 级台阶的方案数的和。很好理解,因为每次只能爬 1 级或 2 级,所以 f(x) 只能从 f(x−1) 和 f(x−2) 转移过来
// 以上是动态规划的转移方程,下面我们来讨论边界条件。我们是从第 0 级开始爬的,所以从第 0 级爬到第 0 级我们可以看作只有一种方案,即 f(0)=1;从第 0 级到第 1 级也只有一种方案,即爬一级,f(1)=1。这两个作为边界条件就可以继续向后推导出第 n 级的正确结果。
let p = 0, q = 0, r = 1;
for (let i = 1; i <= n; ++i) {
p = q;
q = r;
r = p + q;
}
return r;
};
给你两个单词 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')
提示:
0 <= word1.length, word2.length <= 500
word1 和 word2 由小写英文字母组成
可以对任意一个单词进行三种操作:插入一个字符;删除一个字符;替换一个字符。
dp[i][j]
代表 word1 中前 i 个字符,变换到 word2 中前 j 个字符,最短需要操作的次数
D[i][0] = i;
D[0][j] = j;
增,
dp[i][j] = dp[i][j - 1] + 1
删,
dp[i][j] = dp[i - 1][j] + 1
改,如果刚好这两个字母相同
word1[i - 1] = word2[j - 1]
,那么可以直接参考dp[i - 1][j - 1]
,操作不用加一,否则为dp[i - 1][j - 1] + 1
最终的结果为
dp[n][m]
/**
* @param {string} word1
* @param {string} word2
* @return {number}
*/
var minDistance = function(word1, word2) {
// 编辑距离这个问题,因为它看起来十分困难,解法却出奇得简单漂亮,而且它是少有的比较实用的算法
// 例如,DNA 序列是由 A,G,C,T 组成的序列,可以类比成字符串。编辑距离可以衡量两个 DNA 序列的相似度,编辑距离越小,说明这两段 DNA 越相似
// 编辑距离算法被数据科学家广泛应用,是用作机器翻译和语音识别评价标准的基本算法
// 最直观的方法是暴力检查所有可能的编辑方法,取最短的一个。所有可能的编辑方法达到指数级,但我们不需要进行这么多计算,因为我们只需要找到距离最短的序列而不是所有可能的序列。
// 编辑距离问题就是给我们两个字符串 s1 和 s2,只能用三种操作,让我们把 s1 变成 s2,求最少的操作数。
// 需要明确的是,不管是把 s1 变成 s2 还是反过来,结果都是一样的,所以后文就以 s1 变成 s2 举例
// 和最长公共子序列类似,解决两个字符串的动态规划问题,一般都是用两个指针 i, j 分别指向两个字符串的最后,然后一步步往前移动,缩小问题的规模。
/**
base case 是 i 走完 s1 或 j 走完 s2,可以直接返回另一个字符串剩下的长度。
对于每对儿字符 s1[i] 和 s2[j],可以有四种操作:
if s1[i] == s2[j]:
啥都别做(skip)
i, j 同时向前移动
else:
三选一:
插入(insert)
删除(delete)
替换(replace)
这个「三选一」到底该怎么选择呢?很简单,全试一遍,哪个操作最后得到的编辑距离最小,就选谁。这里需要递归技巧
*/
// 思路和算法:
// 我们可以对任意一个单词进行三种操作:
// 插入一个字符;
// 删除一个字符;
// 替换一个字符。
// 题目给定了两个单词,设为 A 和 B,这样我们就能够六种操作方法。
// 但我们可以发现,如果我们有单词 A 和单词 B:
// 对单词 A 删除一个字符和对单词 B 插入一个字符是等价的。例如当单词 A 为 doge,单词 B 为 dog 时,我们既可以删除单词 A 的最后一个字符 e,得到相同的 dog,也可以在单词 B 末尾添加一个字符 e,得到相同的 doge;
// 同理,对单词 B 删除一个字符和对单词 A 插入一个字符也是等价的;
// 对单词 A 替换一个字符和对单词 B 替换一个字符是等价的。例如当单词 A 为 bat,单词 B 为 cat 时,我们修改单词 A 的第一个字母 b -> c,和修改单词 B 的第一个字母 c -> b 是等价的。
// 这样以来,本质不同的操作实际上只有三种:
// 在单词 A 中插入一个字符;
// 在单词 B 中插入一个字符;
// 修改单词 A 的一个字符。
// 这样以来,我们就可以把原问题转化为规模较小的子问题。我们用 A = horse,B = ros 作为例子,来看一看是如何把这个问题转化为规模较小的若干子问题的。
// 在单词 A 中插入一个字符:如果我们知道 horse 到 ro 的编辑距离为 a,那么显然 horse 到 ros 的编辑距离不会超过 a + 1。这是因为我们可以在 a 次操作后将 horse 和 ro 变为相同的字符串,只需要额外的 1 次操作,在单词 A 的末尾添加字符 s,就能在 a + 1 次操作后将 horse 和 ro 变为相同的字符串;
// 在单词 B 中插入一个字符:如果我们知道 hors 到 ros 的编辑距离为 b,那么显然 horse 到 ros 的编辑距离不会超过 b + 1,原因同上;
// 修改单词 A 的一个字符:如果我们知道 hors 到 ro 的编辑距离为 c,那么显然 horse 到 ros 的编辑距离不会超过 c + 1,原因同上。
// 那么从 horse 变成 ros 的编辑距离应该为 min(a + 1, b + 1, c + 1)。
// 注意:为什么我们总是在单词 A 和 B 的末尾插入或者修改字符,能不能在其它的地方进行操作呢?答案是可以的,但是我们知道,操作的顺序是不影响最终的结果的。例如对于单词 cat,我们希望在 c 和 a 之间添加字符 d 并且将字符 t 修改为字符 b,那么这两个操作无论为什么顺序,都会得到最终的结果 cdab。
// 你可能觉得 horse 到 ro 这个问题也很难解决。但是没关系,我们可以继续用上面的方法拆分这个问题,对于这个问题拆分出来的所有子问题,我们也可以继续拆分,直到:
// 字符串 A 为空,如从 转换到 ro,显然编辑距离为字符串 B 的长度,这里是 2;
// 字符串 B 为空,如从 horse 转换到 ,显然编辑距离为字符串 A 的长度,这里是 5。
// 因此,我们就可以使用动态规划来解决这个问题了。我们用 D[i][j] 表示 A 的前 i 个字母和 B 的前 j 个字母之间的编辑距离。
// 如上所述,当我们获得 D[i][j-1],D[i-1][j] 和 D[i-1][j-1] 的值之后就可以计算出 D[i][j]。
// D[i][j-1] 为 A 的前 i 个字符和 B 的前 j - 1 个字符编辑距离的子问题。即对于 B 的第 j 个字符,我们在 A 的末尾添加了一个相同的字符,那么 D[i][j] 最小可以为 D[i][j-1] + 1;
// D[i-1][j] 为 A 的前 i - 1 个字符和 B 的前 j 个字符编辑距离的子问题。即对于 A 的第 i 个字符,我们在 B 的末尾添加了一个相同的字符,那么 D[i][j] 最小可以为 D[i-1][j] + 1;
// D[i-1][j-1] 为 A 前 i - 1 个字符和 B 的前 j - 1 个字符编辑距离的子问题。即对于 B 的第 j 个字符,我们修改 A 的第 i 个字符使它们相同,那么 D[i][j] 最小可以为 D[i-1][j-1] + 1。特别地,如果 A 的第 i 个字符和 B 的第 j 个字符原本就相同,那么我们实际上不需要进行修改操作。在这种情况下,D[i][j] 最小可以为 D[i-1][j-1]。
// 那么我们可以写出如下的状态转移方程:
// 若 A 和 B 的最后一个字母相同:
// D[i][j]=min(D[i][j−1]+1,D[i−1][j]+1,D[i−1][j−1])=1+min(D[i][j−1],D[i−1][j],D[i−1][j−1]−1)
// 若 A 和 B 的最后一个字母不同:D[i][j]=1+min(D[i][j−1],D[i−1][j],D[i−1][j−1])
// 对于边界情况,一个空串和一个非空串的编辑距离为 D[i][0] = i 和 D[0][j] = j,D[i][0] 相当于对 word1 执行 i 次删除操作,D[0][j] 相当于对 word1执行 j 次插入操作。
// 综上我们得到了算法的全部流程。
let n = word1.length;
let m = word2.length;
// 有一个字符串为空串
if (n * m == 0) {
return n + m;
}
// DP 数组
// 定义 dp[i][j]
// dp[i][j] 代表 word1 中前 i 个字符,变换到 word2 中前 j 个字符,最短需要操作的次数
let D = Array.from(Array(n + 1), () => new Array(m + 1));
// 边界状态初始化
// 需要考虑 word1 或 word2 一个字母都没有,即全增加/删除的情况,所以预留 dp[0][j] 和 dp[i][0]
for (let i = 0; i < n + 1; i++) {
D[i][0] = i;
}
for (let j = 0; j < m + 1; j++) {
D[0][j] = j;
}
// 计算所有 DP 值
for (let i = 1; i < n + 1; i++) {
for (let j = 1; j < m + 1; j++) {
// 状态转移:按顺序计算,当计算 dp[i][j] 时,dp[i - 1][j] , dp[i][j - 1] , dp[i - 1][j - 1] 均已经确定了
// 增,dp[i][j] = dp[i][j - 1] + 1
let left = D[i - 1][j] + 1;
// 删,dp[i][j] = dp[i - 1][j] + 1
let down = D[i][j - 1] + 1;
let left_down = D[i - 1][j - 1];
// 如果刚好这两个字母相同 word1[i - 1] = word2[j - 1] ,那么可以直接参考 dp[i - 1][j - 1] ,操作不用加一
// 改,dp[i][j] = dp[i - 1][j - 1] + 1
if (word1.charAt(i - 1) != word2.charAt(j - 1)) {
left_down += 1;
}
// 配合增删改这三种操作,需要对应的 dp 把操作次数加一,取三种的最小
D[i][j] = Math.min(left, Math.min(down, left_down));
}
}
return D[n][m];
// 时间复杂度 :O(mn),其中 m 为 word1 的长度,n 为 word2 的长度。
// 空间复杂度 :O(mn),我们需要大小为 O(mn) 的 D 数组来记录状态值。
};
给定一个 m x n 的矩阵,如果一个元素为 0 ,则将其所在行和列的所有元素都设为 0 。请使用 原地 算法。
示例 1:
输入:matrix = [[1,1,1],[1,0,1],[1,1,1]]
输出:[[1,0,1],[0,0,0],[1,0,1]]
示例 2:
输入:matrix = [[0,1,2,0],[3,4,5,2],[1,3,1,5]]
输出:[[0,0,0,0],[0,4,5,0],[0,3,1,0]]
提示:
m == matrix.length
n == matrix[0].length
1 <= m, n <= 200
-2^31 <= matrix[i][j]
<= 2^31 - 1
进阶:
一个直观的解决方案是使用 O(mn) 的额外空间,但这并不是一个好的解决方案。
一个简单的改进方案是使用 O(m + n) 的额外空间,但这仍然不是最好的解决方案。
你能想出一个仅使用常量空间的解决方案吗?
双重循环两次:
构建两个一维数组row和column,初始化值全为false
第一次双重循环,遇到0时,标记对应一维数组row和column的值为true
第二次双重循环,行row或者column中,有一个为0,即把该处
matrix[i][j]
置为零。
/**
* @param {number[][]} matrix
* @return {void} Do not return anything, modify matrix in-place instead.
*/
var setZeroes = function(matrix) {
const m = matrix.length, n = matrix[0].length;
const row = new Array(m).fill(false);
const col = new Array(n).fill(false);
for (let i = 0; i < m; i++) {
for (let j = 0; j < n; j++) {
if (matrix[i][j] === 0) {
row[i] = col[j] = true;
}
}
}
for (let i = 0; i < m; i++) {
for (let j = 0; j < n; j++) {
if (row[i] || col[j]) {
matrix[i][j] = 0;
}
}
}
};
给你一个满足下述两条属性的 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
提示:
m == matrix.length
n == matrix[i].length
1 <= m, n <= 100
-104 <= matrix[i][j]
, target <= 10^4
先纵向二分查找
再横向二分查找
注意,纵向查找取目标值的左侧边界索引
横向查找时,只当找到target是返回true
/**
* @param {number[][]} matrix
* @param {number} target
* @return {boolean}
*/
// 两次二分查找
var searchMatrix = function(matrix, target) {
// 对矩阵的第一列的元素二分查找,找到最后一个不大于目标值的元素,然后在该元素所在行中二分查找目标值是否存在
const rowIndex = binarySearchFirstColumn(matrix, target);
if (rowIndex < 0) {
return false;
}
return binarySearchRow(matrix[rowIndex], target);
};
const binarySearchFirstColumn = (matrix, target) => {
let low = -1, high = matrix.length - 1;
while (low < high) {
const mid = Math.floor((high - low + 1) / 2) + low;
if (matrix[mid][0] <= target) {
low = mid;
} else {
high = mid - 1;
}
}
return low;
}
const binarySearchRow = (row, target) => {
let low = 0, high = row.length - 1;
while (low <= high) {
const mid = Math.floor((high - low) / 2) + low;
if (row[mid] == target) {
return true;
} else if (row[mid] > target) {
high = mid - 1;
} else {
low = mid + 1;
}
}
return false;
}
给定一个包含红色、白色和蓝色、共 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]
提示:
n == nums.length
1 <= n <= 300
nums[i] 为 0、1 或 2
进阶:
你能想出一个仅使用常数空间的一趟扫描算法吗?
双指针
使用指针p0来交换 0, p2来交换 2。此时,p0的初始值仍然为 0,而 p2的初始值为 n−1。
在遍历的过程中,我们需要找出所有的 0 交换至数组的头部,并且找出所有的 2 交换至数组的尾部。
/**
* @param {number[]} nums
* @return {void} Do not return anything, modify nums in-place instead.
*/
// 单指针:循环两次
var sortColors1 = function(nums) {
let n = nums.length;
// 使用一个指针 ptr 表示「头部」的范围,ptr 中存储了一个整数,表示数组 nums 从位置 0 到位置 ptr−1 都属于「头部」。ptr 的初始值为 0,表示还没有数处于「头部」。
let ptr = 0;
// 在第一次遍历中,我们从左向右遍历整个数组,如果找到了 0,那么就需要将 0 与「头部」位置的元素进行交换,并将「头部」向后扩充一个位置。在遍历结束之后,所有的 0 都被交换到「头部」的范围,并且「头部」只包含 0。
for (let i = 0; i < n; ++i) {
if (nums[i] == 0) {
let temp = nums[i];
nums[i] = nums[ptr];
nums[ptr] = temp;
++ptr;
}
}
// 在第二次遍历中,我们从「头部」开始,从左向右遍历整个数组,如果找到了 1,那么就需要将 1 与「头部」位置的元素进行交换,并将「头部」向后扩充一个位置。在遍历结束之后,所有的 1 都被交换到「头部」的范围,并且都在 0 之后,此时 2 只出现在「头部」之外的位置,因此排序完成。
for (let i = ptr; i < n; ++i) {
if (nums[i] == 1) {
let temp = nums[i];
nums[i] = nums[ptr];
nums[ptr] = temp;
++ptr;
}
}
};
// 双指针:循环一次
var sortColors = function(nums) {
let n = nums.length;
// 使用指针p0来交换 0, p2来交换 2。此时,p0的初始值仍然为 0,而 p2的初始值为 n−1。
// 在遍历的过程中,我们需要找出所有的 0 交换至数组的头部,并且找出所有的 2 交换至数组的尾部。
let p0 = 0, p2 = n - 1;
// 由于此时其中一个指针 p2是从右向左移动的,因此当我们在从左向右遍历整个数组时,如果遍历到的位置超过了 p2,那么就可以直接停止遍历了。
for (let i = 0; i <= p2; ++i) {
// 我们从左向右遍历整个数组,设当前遍历到的位置为 i,对应的元素为 nums[i]
// 对于第二种情况,当我们将 nums[i] 与 nums[p2] 进行交换之后,新的 nums[i] 可能仍然是 2,也可能是 0。
// 当我们找到 2 时,我们需要不断地将其与 nums[p2] 进行交换,直到新的 nums[i] 不为 2
while (i <= p2 && nums[i] == 2) {
let temp = nums[i];
nums[i] = nums[p2];
nums[p2] = temp;
--p2;
}
// 如果找到了 0,那么与前面两种方法类似,将其与 nums[p0] 进行交换,并将 p0向后移动一个位置;
if (nums[i] == 0) {
let temp = nums[i];
nums[i] = nums[p0];
nums[p0] = temp;
++p0;
}
}
}
给你一个字符串 s 、一个字符串 t 。返回 s 中涵盖 t 所有字符的最小子串。如果 s 中不存在涵盖 t 所有字符的子串,则返回空字符串 "" 。
注意:
对于 t 中重复字符,我们寻找的子字符串中该字符数量必须不少于 t 中该字符数量。
如果 s 中存在这样的子串,我们保证它是唯一的答案。
示例 1:
输入:s = "ADOBECODEBANC", t = "ABC"
输出:"BANC"
解释:最小覆盖子串 "BANC" 包含来自字符串 t 的 'A'、'B' 和 'C'。
示例 2:
输入:s = "a", t = "a"
输出:"a"
解释:整个字符串 s 是最小覆盖子串。
示例 3:
输入: s = "a", t = "aa"
输出: ""
解释: t 中两个字符 'a' 均应包含在 s 的子串中,因此没有符合条件的子字符串,返回空字符串。
提示:
m == s.length
n == t.length
1 <= m, n <= 105
s 和 t 由英文字母组成
进阶:你能设计一个在 o(m+n) 时间内解决此问题的算法吗?
/**
* @param {string} s
* @param {string} t
* @return {string}
*/
var minWindow = function(s, t) {
// 本题和438找到字符串中所有字母异位词大同小异,都是关于双指针的快慢指针和左右指针的用法中最经典的技巧之一,滑动窗口技巧。
// 解析:https://labuladong.github.io/algo/di-yi-zhan-da78c/shou-ba-sh-48c1d/wo-xie-le--f7a92/
// 基本思想是:
// 1、我们在字符串 S 中使用双指针中的左右指针技巧,初始化 left = right = 0,把索引左闭右开区间 [left, right) 称为一个「窗口」。
// 2、我们先不断地增加 right 指针扩大窗口 [left, right),直到窗口中的字符串符合要求(包含了 T 中的所有字符)。(相当于在寻找一个「可行解)
// 3、此时,我们停止增加 right,转而不断增加 left 指针缩小窗口 [left, right),直到窗口中的字符串不再符合要求(不包含 T 中的所有字符了)。同时,每次增加 left,我们都要更新一轮结果。(相当于优化这个「可行解」,最终找到最优解,也就是最短的覆盖子串)
// 4、重复第 2 和第 3 步,直到 right 到达字符串 S 的尽头。
// 左右指针轮流前进,窗口大小增增减减,窗口不断向右滑动,这就是「滑动窗口」
// 滑动窗口代码模板基本如下:
/*
int left = 0, right = 0;
while (left < right && right < s.length) {
// 增大窗口,存入s[right]
slidingWindow.push(s[right]);
right++;
while (slidingWindow needs shrink) {
// 缩小窗口,删除s[left]
slidingWindow.shift();
left++;
}
}
*/
// 哈希表 slidingWindow 记录滑动窗口窗口中满足 need 条件的字符及其出现次数
let slidingWindow = new Map();
// 哈希表 need 记录需要匹配的字符及对应的出现次数
let need = new Map();
for (let i = 0; i < t.length; i++) {
if (need.has(t[i])) {
need.set(t[i], need.get(t[i]) + 1);
} else {
need.set(t[i], 1);
}
}
// 窗口左右指针
let left = 0, right = 0;
// 窗口中满足需要的字符个数
let valid = 0;
// 记录最小覆盖子串的起始索引及长度
let start = 0, len = Infinity;
// 边界情况s为空时,不进行循环
while (right < s.length) {
// c 是将移入窗口的字符
let c = s[right];
// 扩大窗口
right++;
// 进行窗口内数据的一系列更新
// 当前字符是所需要的字符,窗口记录下每次出现字符的数量
if (need.has(c)) {
if (slidingWindow.has(c)) {
slidingWindow.set(c, slidingWindow.get(c) + 1);
} else {
slidingWindow.set(c, 1);
}
// 表明当前滑动窗口内某个字符数量满足题目要求t中所要求的该字符数量,某个字符满足后,我们将其标记为满足一个条件,直到满足所有字符要求条件时,窗口内字符才变得可用valid
if (slidingWindow.get(c) === need.get(c)) {
valid++;
}
}
// 当满足所有字符条件,这时需要判断左侧窗口是否要收缩
while (valid === need.size) {
// 由于不知道找到第一个满足要求的子串长度是多少,我们将len设为Infinity,使其初始时可以执行一次
// 其它情况下,我们需要记录下每次得到的最小覆盖子串的起始索引和长度,在循环中后续遇到有更短长度的满足条件的子串时,再次更新子串的起始索引和长度,以提供最后输出答案时使用
if (right - left < len) {
start = left;
len = right - left;
}
// d 是将移出窗口的字符
let d = s[left];
// 缩小窗口
left++;
// 缩小窗口后观察:
// 1.如果所需要的字符也含有移除的字符,我们需要比较移除后,该类字符数量是否还满足字符条件要求
// 2.无关字符不需处理,如果所需要的字符也含有移除的字符
if (need.has(d)) {
// 该字符要求的数量已经到了临界值,此时,再移除最左边元素后,必将不再满足该类字符的条件,因此需要将条件数量减1。
if (slidingWindow.get(d) === need.get(d)) {
valid--;
}
// 更新滑动窗口内移除的那个字符数量
slidingWindow.set(d, slidingWindow.get(d) - 1);
}
}
}
// 返回最小覆盖子串
return len === Infinity ? '' : s.substr(start, len);
};
给你一个整数数组 nums ,数组中的元素 互不相同 。返回该数组所有可能的子集(幂集)。
解集 不能 包含重复的子集。你可以按 任意顺序 返回解集。
示例 1:
输入:nums = [1,2,3]
输出:[[],[1],[2],[1,2],[3],[1,3],[2,3],[1,2,3]]
示例 2:
输入:nums = [0]
输出:[[],[0]]
提示:
1 <= nums.length <= 10
-10 <= nums[i] <= 10
nums 中的所有元素 互不相同
递归、回溯
dfs(cur) 参数表示当前位置是 cur,原序列总长度为 n。
对于 cur 位置,我们需要考虑 num[cur] 取或者不取
取:t.push(nums[cur]);dfs(cur + 1);
不取:回溯。t.pop();dfs(cur + 1);
递归结束条件是:cur === nums.length时,ans.push(t.slice());
/**
* @param {number[]} nums
* @return {number[][]}
*/
var subsets = function(nums) {
// dfs(cur,n) 参数表示当前位置是 cur,原序列总长度为 n。
// 在进在进入 dfs(cur,n) 之前 [0,cur−1] 位置的状态是确定的,而 [cur,n−1] 内位置的状态是不确定的,dfs(cur,n) 需要确定 cur 位置的状态,然后求解子问题 dfs(cur+1,n)。
// 对于 cur 位置,我们需要考虑 a[cur] 取或者不取
// 如果取,我们需要把 a[cur] 放入一个临时的答案数组中(即上面代码中的 t),再执行 dfs(cur+1,n),执行结束后需要对 t 进行回溯;
// 如果不取,则直接执行 dfs(cur+1,n)。在整个递归调用的过程中,cur 是从小到大递增的,当 cur 增加到 n 的时候,记录答案并终止递归。
// 原序列的每个位置在答案序列中的状态有被选中和不被选中两种,我们用 t 数组存放已经被选出的数字。
const t = [];
const ans = [];
const dfs = (cur) => {
// 当前位置是 cur,原序列总长度为 n。
if (cur === nums.length) {
ans.push(t.slice());
return;
}
// 对于 cur 位置,我们需要考虑 a[cur] 取或者不取
// 如果取,我们需要把 a[cur] 放入一个临时的答案数组中(即上面代码中的 t),再执行 dfs(cur+1,n),执行结束后需要对 t 进行回溯;
t.push(nums[cur]);
dfs(cur + 1);
// 如果不取,则直接执行 dfs(cur+1,n)。在整个递归调用的过程中,cur 是从小到大递增的,当 cur 增加到 n 的时候,记录答案并终止递归。
t.pop();
dfs(cur + 1);
}
dfs(0);
return ans;
};
给定一个 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
提示:
m == board.length
n = board[i].length
1 <= m, n <= 6
1 <= word.length <= 15
board 和 word 仅由大小写英文字母组成
进阶:你可以使用搜索剪枝的技术来优化解决方案,使其在 board 更大的情况下可以更快解决问题?
思路:
1.check(i,j,k) 表示判断以网格的 (i,j) 位置出发,能否搜索到单词 word[k..]
2.递归,遍历当前位置的所有相邻位置。如果从某个相邻位置出发,能够搜索到子串 word[k+1..],则返回 true,否则返回 false。
3.边界条件是(1)如果
board[i][j] != s[k]
,当前字符不匹配,直接返回 false。(2)如果当前已经访问到字符串的末尾,且对应字符依然匹配,此时直接返回 true4.双重循环对每一个位置 (i,j) 都调用函数 check(i,j,0) 进行检查:只要有一处返回 true,就说明网格中能够找到相应的单词,否则说明不能找到。
5.为了防止重复遍历相同的位置,需要额外维护一个与 board 等大的 visited 数组,用于标识每个位置是否被访问过
/**
* @param {character[][]} board
* @param {string} word
* @return {boolean}
*/
var exist = function(board, word) {
const h = board.length, w = board[0].length;
const directions = [[0, 1], [0, -1], [1, 0], [-1, 0]];
const visited = new Array(h);
for (let i = 0; i < visited.length; ++i) {
visited[i] = new Array(w).fill(false);
}
// 设函数 check(i,j,k) 表示判断以网格的 (i,j) 位置出发,能否搜索到单词 word[k..]
// 其中 word[k..] 表示字符串 word 从第 k 个字符开始的后缀子串。如果能搜索到,则返回 true,反之返回 false。
const check = (i, j, s, k) => {
// 如果 board[i][j] != s[k],当前字符不匹配,直接返回 false。
if (board[i][j] != s.charAt(k)) {
return false;
// 如果当前已经访问到字符串的末尾,且对应字符依然匹配,此时直接返回 true
} else if (k == s.length - 1) {
return true;
}
visited[i][j] = true;
let result = false;
// 否则,遍历当前位置的所有相邻位置。如果从某个相邻位置出发,能够搜索到子串 word[k+1..],则返回 true,否则返回 false。
for (const [dx, dy] of directions) {
let newi = i + dx, newj = j + dy;
if (newi >= 0 && newi < h && newj >= 0 && newj < w) {
// 为了防止重复遍历相同的位置,需要额外维护一个与 board 等大的 visited 数组,用于标识每个位置是否被访问过。每次遍历相邻位置时,需要跳过已经被访问的位置。
if (!visited[newi][newj]) {
const flag = check(newi, newj, s, k + 1);
if (flag) {
result = true;
break;
}
}
}
}
visited[i][j] = false;
return result;
}
for (let i = 0; i < h; i++) {
for (let j = 0; j < w; j++) {
// 我们对每一个位置 (i,j) 都调用函数 check(i,j,0) 进行检查:只要有一处返回 true,就说明网格中能够找到相应的单词,否则说明不能找到。
const flag = check(i, j, word, 0);
if (flag) {
return true;
}
}
}
return false;
};
给定一个仅包含 0 和 1 、大小为 rows x cols 的二维二进制矩阵,找出只包含 1 的最大矩形,并返回其面积。
示例 1:
输入:matrix = [["1","0","1","0","0"],["1","0","1","1","1"],["1","1","1","1","1"],["1","0","0","1","0"]]
输出:6
解释:最大矩形如上图所示。
示例 2:
输入:matrix = []
输出:0
示例 3:
输入:matrix = [["0"]]
输出:0
示例 4:
输入:matrix = [["1"]]
输出:1
示例 5:
输入:matrix = [["0","0"]]
输出:0
提示:
rows == matrix.length
cols == matrix[0].length
1 <= row, cols <= 200
matrix[i][j]
为 '0' 或 '1'
思路:
1.双重循环,计算行增加后,对每列连续1高度增加的影响,相当于每行都计算一次柱状图的最大矩形。
转化为leetcode 84题求柱状图中最大的矩形
单调栈的意义:用 O(n) 复杂度的一重遍历找到每个元素前后最近的更小/大元素位置
1.核心思想:求每条柱子可以向左右延伸的长度->矩形最大宽度;矩形的高->柱子的高度
计算以每一根柱子高度为高的矩形面积,维护面积最大值
2.朴素的想法:遍历每一根柱子的高度然后向两边进行扩散找到最大宽度
3.单调栈优化:因为最终的目的是寻找对应柱子height[i]右边首个严格小于height[i]的柱子height[r]
左边同理找到首个严格小于height[i]的柱子height[l]
维护一个单调递增栈(栈底->栈顶),那么每当遇到新加入的元素<栈顶便可以确定栈顶柱子右边界
而栈顶柱子左边界就是栈顶柱子下面的柱子(<栈顶柱子)
左右边界确定以后就可以进行面积计算与维护最大面积
时间复杂度:O(N),空间复杂度:O(N)
/**
* @param {character[][]} matrix
* @return {number}
*/
const largestRectangleArea = (heights) => {
let maxArea = 0
const stack = [] //单调递增栈 注意栈存的时下标
// 哨兵的作用是 将最后的元素出栈计算面积 以及 将开头的元素顺利入栈
heights = [0, ...heights, 0] //在heights数组前后增加两个哨兵 用来清零单调递增栈里的元素
for (let i = 0; i < heights.length; i++) {
//当前元素对应的高度小于栈顶元素对应的高度时
while (heights[i] < heights[stack[stack.length - 1]]) {
const stackTopIndex = stack.pop() //出栈
maxArea = Math.max( //计算面积 并更新最大面积
maxArea,
heights[stackTopIndex] * (i - stack[stack.length - 1] - 1)//高乘宽
)
}
stack.push(i)//当前下标加入栈
}
return maxArea
}
var maximalRectangle = function (matrix) {
if (matrix.length == 0) return 0;
let res = 0;
let heights = new Array(matrix[0].length).fill(0);//初始化heights数组
for (let row = 0; row < matrix.length; row++) {
for (let col = 0; col < matrix[0].length; col++) {
if(matrix[row][col] == '1' ) heights[col] += 1;
else heights[col] = 0;
}//求出每一层的 heights[] 然后传给84题的函数
res = Math.max(res, largestRectangleArea(heights));//更新一下最大矩形面积
}
return res;
};
给定一个二叉树的根节点 root ,返回 它的 中序 遍历 。
示例 1:
输入:root = [1,null,2,3]
输出:[1,3,2]
示例 2:
输入:root = []
输出:[]
示例 3:
输入:root = [1]
输出:[1]
提示:
树中节点数目在范围 [0, 100] 内
-100 <= Node.val <= 100
进阶: 递归算法很简单,你可以通过迭代算法完成吗?
/**
* 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[]}
*/
var inorderTraversal = function(root) {
const res = [];
const inorder = (root) => {
if (!root) {
return;
}
inorder(root.left);
res.push(root.val);
inorder(root.right);
}
inorder(root);
return res;
};
给你一个整数 n ,求恰由 n 个节点组成且节点值从 1 到 n 互不相同的 二叉搜索树 有多少种?返回满足题意的二叉搜索树的种数。
示例 1:
输入:n = 3
输出:5
示例 2:
输入:n = 1
输出:1
提示:
1 <= n <= 19
假设 n 个节点存在二叉排序树的个数是 G (n),令 f(i) 为以 i 为根的二叉搜索树的个数,G(n)=f(1)+f(2)+f(3)+f(4)+...+f(n)
当 i 为根节点时,其左子树节点个数为 i-1 个,右子树节点为 n-i,则 f(i) = G(i-1)*G(n-i)
综合两个公式可以得到 卡特兰数 公式 G(n)=G(0)∗G(n−1)+G(1)∗(n−2)+...+G(n−1)∗G(0)
/**
* @param {number} n
* @return {number}
*/
var numTrees = function(n) {
// G(n): 长度为 n 的序列能构成的不同二叉搜索树的个数。
const G = new Array(n + 1).fill(0);
// 对于边界情况,当序列长度为 1(只有根)或为 0(空树)时,只有一种情况
G[0] = 1;
G[1] = 1;
// F(i,n): 以 i 为根、序列长度为 n 的不同二叉搜索树个数 (1≤i≤n)。
// 不同的二叉搜索树的总数 G(n),是对遍历所有 (1≤i≤n) 的 F(i,n) 之和。
for (let i = 2; i <= n; ++i) {
for (let j = 1; j <= i; ++j) {
G[i] += G[j - 1] * G[i - j];
}
}
return G[n];
};
给你一个二叉树的根节点 root ,判断其是否是一个有效的二叉搜索树。
有效 二叉搜索树定义如下:
节点的左子树只包含 小于 当前节点的数。
节点的右子树只包含 大于 当前节点的数。
所有左子树和右子树自身必须也是二叉搜索树。
示例 1:
输入:root = [2,1,3]
输出:true
示例 2:
输入:root = [5,1,4,null,null,3,6]
输出:false
解释:根节点的值是 5 ,但是右子节点的值是 4 。
提示:
树中节点数目范围在[1, 104] 内
-2^31 <= Node.val <= 2^31 - 1
递归:递归验证节点是否严格满足left < val < right
/**
* 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 helper = (root, lower, upper) => {
if (root === null) {
return true;
}
if (root.val <= lower || root.val >= upper) {
return false;
}
return helper(root.left, lower, root.val) && helper(root.right, root.val, upper);
}
// 递归
var isValidBST1 = function(root) {
return helper(root, -Infinity, Infinity);
};
// 中序遍历
var isValidBST = function(root) {
let stack = [];
let inorder = -Infinity;
while (stack.length || root !== null) {
while (root !== null) {
stack.push(root);
root = root.left;
}
root = stack.pop();
// 如果中序遍历得到的节点的值小于等于前一个 inorder,说明不是二叉搜索树
if (root.val <= inorder) {
return false;
}
inorder = root.val;
root = root.right;
}
return true;
};
给你一个二叉树的根节点 root , 检查它是否轴对称。
示例 1:
输入:root = [1,2,2,3,4,4,3]
输出:true
示例 2:
输入:root = [1,2,2,null,3,null,3]
输出:false
提示:
树中节点数目在范围 [1, 1000] 内
-100 <= Node.val <= 100
进阶:你可以运用递归和迭代两种方法解决这个问题吗?
递归的判断每个节点是否满足左右子树都对称,注意叶子结点边界情况也即递归终止条件的确定
/**
* 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}
*/
/**
首先想清楚,判断对称二叉树要比较的是哪两个节点,要比较的可不是左右节点!
其实我们要比较的是两个树(这两个树是根节点的左右子树)
一个树的遍历顺序是左右中,一个树的遍历顺序是右左中。
*/
/**
1.确定递归函数的参数和返回值: 判断这个树是不是对称树,参数自然也是左子树节点和右子树节点,返回值自然是bool类型
2.确定终止条件: 要比较两个节点数值相不相同,首先要把两个节点为空的情况弄清楚:左节点为空,右节点不为空,不对称,return false;左不为空,右为空,不对称 return false;左右都为空,对称,返回true;左右都不为空,比较节点数值,不相同就return false;剩下的就是 左右节点都不为空,且数值相同的情况;
3.确定单层递归的逻辑
*/
// 递归判断是否为对称二叉树:
var isSymmetric = function(root) {
//使用递归遍历左右子树 递归三部曲
// 1. 确定递归的参数 root.left root.right和返回值true false
const compareNode=function(left,right){
//2. 确定终止条件 空的情况
if(left===null && right!==null || left !== null && right===null){
return false;
}else if(left===null && right===null){
return true;
}else if(left.val !== right.val){
return false;
}
//3. 确定单层递归逻辑
// 比较二叉树外侧是否对称:传入的是左节点的左孩子,右节点的右孩子
let outSide=compareNode(left.left,right.right);
// 比较内测是否对称,传入左节点的右孩子,右节点的左孩子。
let inSide=compareNode(left.right,right.left);
// 如果左右都对称就返回true ,有一侧不对称就返回false 。
return outSide && inSide;
}
if(root===null){
return true;
}
return compareNode(root.left,root.right);
};
// 队列实现迭代判断是否为对称二叉树:
var isSymmetric1 = function(root) {
//迭代方法判断是否是对称二叉树
//首先判断root是否为空
if(root===null){
return true;
}
let queue=[];
queue.push(root.left);
queue.push(root.right);
while(queue.length){
let leftNode=queue.shift();//左节点
let rightNode=queue.shift();//右节点
if(leftNode===null&&rightNode===null){
continue;
}
if(leftNode===null||rightNode===null||leftNode.val!==rightNode.val){
return false;
}
queue.push(leftNode.left);//左节点左孩子入队
queue.push(rightNode.right);//右节点右孩子入队
queue.push(leftNode.right);//左节点右孩子入队
queue.push(rightNode.left);//右节点左孩子入队
}
return true;
};
// 栈实现迭代判断是否为对称二叉树:
var isSymmetric2 = function(root) {
//迭代方法判断是否是对称二叉树
//首先判断root是否为空
if(root===null){
return true;
}
let stack=[];
stack.push(root.left);
stack.push(root.right);
while(stack.length){
let rightNode=stack.pop();//左节点
let leftNode=stack.pop();//右节点
if(leftNode===null&&rightNode===null){
continue;
}
if(leftNode===null||rightNode===null||leftNode.val!==rightNode.val){
return false;
}
stack.push(leftNode.left);//左节点左孩子入队
stack.push(rightNode.right);//右节点右孩子入队
stack.push(leftNode.right);//左节点右孩子入队
stack.push(rightNode.left);//右节点左孩子入队
}
return true;
};
给你二叉树的根节点 root ,返回其节点值的 层序遍历 。 (即逐层地,从左到右访问所有节点)。
示例 1:
输入:root = [3,9,20,null,null,15,7]
输出:[[3],[9,20],[15,7]]
示例 2:
输入:root = [1]
输出:[[1]]
示例 3:
输入:root = []
输出:[]
提示:
树中节点数目在范围 [0, 2000] 内
-1000 <= Node.val <= 1000
层序遍历|广度优先遍历|BFS是需要维护一个队列(即答案中的q)不停的进行入队/出队操作的
在 while 循环的每一轮中,都是将当前层的所有结点出队列,(存入变量),再将下一层的所有结点入队列
/**
* 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[][]}
*/
// 二叉树的层序遍历/广度优先搜索
var levelOrder = function(root) {
const ret = [];
if (!root) {
return ret;
}
// 首先根元素入队
const q = [];
q.push(root);
// 当队列不为空的时候
while (q.length !== 0) {
// 求当前队列的长度
const currentLevelSize = q.length;
ret.push([]);
// 依次从队列中取 i个元素进行拓展,然后进入下一次迭代
for (let i = 1; i <= currentLevelSize; ++i) {
const node = q.shift();
ret[ret.length - 1].push(node.val);
if (node.left) q.push(node.left);
if (node.right) q.push(node.right);
}
}
return ret;
};
给定一个二叉树 root ,返回其最大深度。
二叉树的 最大深度 是指从根节点到最远叶子节点的最长路径上的节点数。
示例 1:
输入:root = [3,9,20,null,null,15,7]
输出:3
示例 2:
输入:root = [1,null,2]
输出:2
提示:
树中节点的数量在 [0, 10^4] 区间内。
-100 <= Node.val <= 100
递归的取每个节点左右子树的最大深度+1,边界叶子节点下一层的深度是0
/**
* 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}
*/
// 递归
var maxDepth1 = function(root) {
if(!root) {
return 0;
} else {
const left = maxDepth(root.left);
const right = maxDepth(root.right);
return Math.max(left, right) + 1;
}
};
// 广度优先搜索
const maxDepth = (root) => {
if (root == null) return 0;
const queue = [root];
let depth = 1;
while (queue.length) {
// 当前层的节点个数
const levelSize = queue.length;
// 逐个让当前层的节点出列
for (let i = 0; i < levelSize; i++) {
// 当前出列的节点
const cur = queue.shift();
// 左右子节点入列
if (cur.left) queue.push(cur.left);
if (cur.right) queue.push(cur.right);
}
// 当前层所有节点已经出列,如果队列不为空,说明有下一层节点,depth+1
if (queue.length) depth++;
}
return depth;
};
给定两个整数数组 preorder 和 inorder ,其中 preorder 是二叉树的先序遍历, inorder 是同一棵树的中序遍历,请构造二叉树并返回其根节点。
示例 1:
输入: preorder = [3,9,20,15,7], inorder = [9,3,15,20,7]
输出: [3,9,20,null,null,15,7]
示例 2:
输入: preorder = [-1], inorder = [-1]
输出: [-1]
提示:
1 <= preorder.length <= 3000
inorder.length == preorder.length
-3000 <= preorder[i], inorder[i] <= 3000
preorder 和 inorder 均 无重复 元素
inorder 均出现在 preorder
preorder 保证 为二叉树的前序遍历序列
inorder 保证 为二叉树的中序遍历序列
注意,preorder 和 inorder 均 无重复 元素
通过先序遍历结果取出数组首个元素构建根节点
由于元素不重复,通过indexof找到中序遍历数组中根节点下标
下标前后即为中序遍历左右子树的中序遍历
对于前序遍历,从头到尾依次从前序遍历数组中截取第一个元素,即为中左右各节点的顺序
递归的对每个节点传入的前序、中序数组
知道先序数组为空,中序数组为1时,得到叶子节点
/**
* 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[]} preorder
* @param {number[]} inorder
* @return {TreeNode}
*/
var buildTree = function(preorder, inorder) {
return getTreeRoot(preorder, inorder);
};
function getTreeRoot(preorder, inorder) {
if (preorder.length === 0) return null;
const root = new TreeNode(preorder.splice(0, 1)[0]);
if (inorder.length === 1) return root;
const index = inorder.indexOf(root.val);
const leftTree = inorder.slice(0, index);
const rightTree = inorder.slice(index+1);
root.left = leftTree.length > 0 ? getTreeRoot(preorder, leftTree) : null;
root.right = rightTree.length > 0 ? getTreeRoot(preorder, rightTree): null;
return root;
}
给你一个整数数组 nums ,其中元素已经按 升序 排列,请你将其转换为一棵 高度平衡 二叉搜索树。
高度平衡 二叉树是一棵满足「每个节点的左右两个子树的高度差的绝对值不超过 1 」的二叉树。
示例 1:
输入:nums = [-10,-3,0,5,9]
输出:[0,-3,9,-10,null,5]
解释:[0,-10,5,null,-3,null,9] 也将被视为正确答案:
示例 2:
输入:nums = [1,3]
输出:[3,1]
解释:[1,null,3] 和 [3,1] 都是高度平衡二叉搜索树。
提示:
1 <= nums.length <= 10^4
-10^4 <= nums[i] <= 10^4
nums 按 严格递增 顺序排列
为了平衡,直接截取升序数组的中间元素作为树的根节点
递归的截取中间索引前后的数组作为左右子树的升序数组
终止条件是数组长度为0
/**
* 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}
*/
var sortedArrayToBST = function(nums) {
// 因为涉及到递归,所以必然会有数组为空的情况
if(!nums.length) {
return null;
}
// 1.找到序列中点:
const headIndex = Math.floor(nums.length / 2);
// 实例化节点头部
const head = new TreeNode(nums[headIndex]);
let temp = head;
let left = headIndex - 1;
let right = headIndex + 1;
// 因为是有序升序列表,则当前头部索引的左侧应该都在树的左子树,同理右子树
if(left >=0) {
// 左侧有节点,对左侧节点递归,形成左子树
head.left = sortedArrayToBST(nums.slice(0, headIndex));
}
if(right < nums.length) {
// 右侧有节点,对右侧节点递归,形成右子树
head.right = sortedArrayToBST(nums.slice(right));
}
// 返回节点
return head;
};
给你二叉树的根结点 root ,请你将它展开为一个单链表:
展开后的单链表应该同样使用 TreeNode ,其中 right 子指针指向链表中下一个结点,而左子指针始终为 null 。
展开后的单链表应该与二叉树 先序遍历 顺序相同。
示例 1:
输入:root = [1,2,5,3,4,null,6]
输出:[1,null,2,null,3,null,4,null,5,null,6]
示例 2:
输入:root = []
输出:[]
示例 3:
输入:root = [0]
输出:[0]
提示:
树中结点数在范围 [0, 2000] 内
-100 <= Node.val <= 100
进阶:你可以使用原地算法(O(1) 额外空间)展开这棵树吗?
先使用先序遍历递归地将节点放入数组中
遍历数组按照规则将每个节点的左子节点置为空,右子节点指向数组下一节点
/**
* Definition for a binary tree node.
* function TreeNode(val, left, right) {
* this.val = (val===undefined ? 0 : val)
* this.left = (left===undefined ? null : left)
* this.right = (right===undefined ? null : right)
* }
*/
/**
* @param {TreeNode} root
* @return {void} Do not return anything, modify root in-place instead.
*/
// 注意题目注释,原地修改,不需返回
var flatten = function(root) {
const list = [];
preorderTraversal(root, list);
const size = list.length;
for (let i = 1; i < size; i++) {
const prev = list[i - 1], curr = list[i];
prev.left = null;
prev.right = curr;
}
};
const preorderTraversal = (root, list) => {
if (root != null) {
list.push(root);
preorderTraversal(root.left, list);
preorderTraversal(root.right, list);
}
}
给定一个非负整数 numRows,生成「杨辉三角」的前 numRows 行。
在「杨辉三角」中,每个数是它左上方和右上方的数的和。
示例 1:
输入: numRows = 5
输出: [[1],[1,1],[1,2,1],[1,3,3,1],[1,4,6,4,1]]
示例 2:
输入: numRows = 1
输出: [[1]]
提示:
1 <= numRows <= 30
根据行index生成每行元素数量
每行内迭代每列,根据上一行相同列及前一列生成本行本列值
/**
* @param {number} numRows
* @return {number[][]}
*/
var generate = function(numRows) {
// 结果
const ret = [];
// 行
for (let i = 0; i < numRows; i++) {
// 每行元素个数,初始默认填充1
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;
};
给定一个数组 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。
提示:
1 <= prices.length <= 10^5
0 <= prices[i] <= 10^4
迭代过程中,比较记录当前价格和最低价格以及当前差价和最大差价,即可
/**
* @param {number[]} prices
* @return {number}
*/
var maxProfit = function (prices) {
if (prices.length === 0) return 0;
// 最低买点
let min = prices[0];
// 最大收入
let max = 0;
for (let p of prices) {
// 最佳买点,买入点最低
min = Math.min(min, p);
max = Math.max(max, p - min);
}
return max;
};
给定一个未排序的整数数组 nums ,找出数字连续的最长序列(不要求序列元素在原数组中连续)的长度。
请你设计并实现时间复杂度为 O(n) 的算法解决此问题。
迭代,每轮循环里利用Set看是否有比当前元素小1的元素存在
如果存在则在当前索引下再开启循环找是否有比当前元素大1的元素
直到找不到,记录长度,遍历到下一个元素
示例 1:
输入:nums = [100,4,200,1,3,2]
输出:4
解释:最长数字连续序列是 [1, 2, 3, 4]。它的长度为 4。
示例 2:
输入:nums = [0,3,7,2,5,8,4,6,0,1]
输出:9
提示:
0 <= nums.length <= 105
-10^9 <= nums[i] <= 10 ^ 9
/**
* @param {number[]} nums
* @return {number}
*/
var longestConsecutive = function(nums) {
const set = new Set(nums)
let maxLong = 0
nums.forEach(item => {
if(!set.has(item - 1)) {
let curNum = item
while(set.has(curNum)) {
curNum ++
}
maxLong = Math.max(maxLong, curNum - item)
}
})
return maxLong
};
给你一个字符串 s,请你将 s 分割成一些子串,使每个子串都是 回文串 。返回 s 所有可能的分割方案。
回文串 是正着读和反着读都一样的字符串。
示例 1:
输入:s = "aab"
输出:[["a","a","b"],["aa","b"]]
示例 2:
输入:s = "a"
输出:[["a"]]
提示:
1 <= s.length <= 16
s 仅由小写英文字母组成
先利用动态规划,设 f(i,j) 表示
s[i..j]
是否为回文串,计算出各个截取片段是否是回文串再利用递归和回溯,将满足
f[i][j]
为true的输入到答案中
/**
* @param {string} s
* @return {string[][]}
*/
var partition = function(s) {
// 假设我们当前搜索到字符串的第 i 个字符,且 s[0..i−1] 位置的所有字符已经被分割成若干个回文串,并且分割结果被放入了答案数组 ans 中,那么我们就需要枚举下一个回文串的右边界 j,使得 s[i..j] 是一个回文串。
// 因此,我们可以从 i 开始,从小到大依次枚举 j。对于当前枚举的 j 值,我们使用双指针的方法判断 s[i..j] 是否为回文串:如果 s[i..j] 是回文串,那么就将其加入答案数组 ans 中,并以 j+1 作为新的 i 进行下一层搜索,并在未来的回溯时将 s[i..j] 从 ans 中移除。
const dfs = (i) => {
if (i === n) {
ret.push(ans.slice());
return;
}
for (let j = i; j < n; ++j) {
if (f[i][j]) {
ans.push(s.slice(i, j + 1));
dfs(j + 1);
ans.pop();
}
}
}
const n = s.length;
// 设 f(i,j) 表示 s[i..j] 是否为回文串
const f = new Array(n).fill(0).map(() => new Array(n).fill(true));
let ret = [], ans = [];
for (let i = n - 1; i >= 0; --i) {
for (let j = i + 1; j < n; ++j) {
f[i][j] = (s[i] === s[j]) && f[i + 1][j - 1];
}
}
dfs(0);
return ret;
};
给你一个 非空 整数数组 nums ,除了某个元素只出现一次以外,其余每个元素均出现两次。找出那个只出现了一次的元素。
你必须设计并实现线性时间复杂度的算法来解决此问题,且该算法只使用常量额外空间。
示例 1 :
输入:nums = [2,2,1]
输出:1
示例 2 :
输入:nums = [4,1,2,1,2]
输出:4
示例 3 :
输入:nums = [1]
输出:1
提示:
1 <= nums.length <= 3*10^4
-3*10^4 <= nums[i] <= 3*10^4
除了某个元素只出现一次以外,其余每个元素均出现两次。
任何数和其自身做异或运算,结果是 0
任何数和 0 做异或运算,结果仍然是原来的数
因此,迭代一次,将每个元素和之前的结果进行异或操作,最后的结果就是那个元素
其它思路:利用Set去重后,再利用Map,给每个元素设置2的值,再遍历一次,遇到对应元素后将其值减1,最后所有map的结果中,必然有一个是1,而其它的是0
/**
* @param {number[]} nums
* @return {number}
*/
var singleNumber = function(nums) {
// 除了某个元素只出现一次以外,其余每个元素均出现两次。 觉得这是个突破口!!!!——异或运算!
// 异或运算有以下三个性质。
// 任何数和 0 做异或运算,结果仍然是原来的数,即 a⊕0=a。
// 任何数和其自身做异或运算,结果是 0,即 a⊕a=0。
// 异或运算满足交换律和结合律,即 a⊕b⊕a = b⊕a⊕a = b⊕(a⊕a) = b⊕0 = b
let ans = nums[0];
if (nums.length > 1) {
for (let i = 1; i < nums.length; i++) {
ans = ans ^ nums[i];
}
}
return ans;
};
给你一个长度为 n 的链表,每个节点包含一个额外增加的随机指针 random ,该指针可以指向链表中的任何节点或空节点。
构造这个链表的 深拷贝。 深拷贝应该正好由 n 个 全新 节点组成,其中每个新节点的值都设为其对应的原节点的值。
新节点的 next 指针和 random 指针也都应指向复制链表中的新节点,并使原链表和复制链表中的这些指针能够表示相同的链表状态。
复制链表中的指针都不应指向原链表中的节点 。
例如,如果原链表中有 X 和 Y 两个节点,其中 X.random --> Y 。
那么在复制链表中对应的两个节点 x 和 y ,同样有 x.random --> y 。
返回复制链表的头节点。
用一个由 n 个节点组成的链表来表示输入/输出中的链表。
每个节点用一个 [val, random_index] 表示:
val:一个表示 Node.val 的整数。
random_index:随机指针指向的节点索引(范围从 0 到 n-1);如果不指向任何节点,则为 null 。
你的代码 只 接受原链表的头节点 head 作为传入参数。
示例 1:
输入:head = [[7,null],[13,0],[11,4],[10,2],[1,0]]
输出:[[7,null],[13,0],[11,4],[10,2],[1,0]]
示例 2:
输入:head = [[1,1],[2,1]]
输出:[[1,1],[2,1]]
示例 3:
输入:head = [[3,null],[3,0],[3,null]]
输出:[[3,null],[3,0],[3,null]]
提示:
0 <= n <= 1000
-10^4 <= Node.val <= 10^4
Node.random 为 null 或指向链表中的节点。
普通链表,我们可以直接按照遍历的顺序创建链表节点。
因为随机指针的存在,当我们拷贝节点时,「当前节点的随机指针指向的节点」可能还没创建,因此需要用到递归
利用map,让每个节点独立保存节点信息,以及拷贝操作相互独立。
初始时仅拷贝节点值即可,next和random均递归的去生成,直到其到达空节点,返回null
/**
* // Definition for a Node.
* function Node(val, next, random) {
* this.val = val;
* this.next = next;
* this.random = random;
* };
*/
/**
* @param {Node} head
* @return {Node}
*/
// 如果是普通链表,我们可以直接按照遍历的顺序创建链表节点。
// 因为随机指针的存在,当我们拷贝节点时,「当前节点的随机指针指向的节点」可能还没创建,因此我们需要变换思路。
// 一个可行方案是,我们利用回溯的方式,让每个节点的拷贝操作相互独立。
// 对于当前节点,我们首先要进行拷贝,然后我们进行「当前节点的后继节点」和「当前节点的随机指针指向的节点」拷贝
// 然后我们进行「当前节点的后继节点」和「当前节点的随机指针指向的节点」拷贝,拷贝完成后将创建的新节点的指针返回,即可完成当前节点的两指针的赋值。
var copyRandomList = function(head, cachedNode = new Map()) {
if (head === null) {
return null;
}
// 我们用哈希表记录每一个节点对应新节点的创建情况。
if (!cachedNode.has(head)) {
// 遍历该链表的过程中,我们检查「当前节点的后继节点」和「当前节点的随机指针指向的节点」的创建情况。
// 如果这两个节点中的任何一个节点的新节点没有被创建,我们都立刻递归地进行创建。
// 当我们拷贝完成,回溯到当前层时,我们即可完成当前节点的指针赋值。
// 注意一个节点可能被多个其他节点指向,因此我们可能递归地多次尝试拷贝某个节点,为了防止重复拷贝,我们需要首先检查当前节点是否被拷贝过
// 如果已经拷贝过,我们可以直接从哈希表中取出拷贝后的节点的指针并返回即可。
cachedNode.set(head, {val: head.val})
Object.assign(cachedNode.get(head), {next: copyRandomList(head.next, cachedNode), random: copyRandomList(head.random, cachedNode)})
}
return cachedNode.get(head);
}
给你一个字符串 s 和一个字符串列表 wordDict 作为字典。请你判断是否可以利用字典中出现的单词拼接出 s 。
注意:不要求字典中出现的单词全部都使用,并且字典中的单词可以重复使用。
示例 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
提示:
1 <= s.length <= 300
1 <= wordDict.length <= 1000
1 <= wordDict[i].length <= 20
s 和 wordDict[i] 仅由小写英文字母组成
wordDict 中的所有字符串 互不相同
动态规划:
对于输入的字符串 s,如果我能够从单词列表 wordDict 中找到一个单词匹配 s 的前缀 s[0..k],那么只要我能拼出 s[k+1..],就一定能拼出整个 s。
换句话说,我把规模较大的原问题 wordBreak(s[0..]) 分解成了规模较小的子问题 wordBreak(s[k+1..]),然后通过子问题的解反推出原问题的解。
设d[i]为字符串 s[0,i+1] 是否可由字典中的单词构成
dp[0] = true;
每轮遍历i时,都需要遍历一遍字典,并且在数组不越界条件下,当dp[i]为true且s.startsWith(word, i)时:dp[i + word.length] = true;
/**
* @param {string} s
* @param {string[]} wordDict
* @return {boolean}
*/
var wordBreak1 = function(s, wordDict) {
// 先说说「遍历」的思路,也就是用回溯算法解决本题。回溯算法最经典的应用就是排列组合相关的问题了,不难发现这道题换个说法也可以变成一个排列问题:
// 现在给你一个不包含重复单词的单词列表 wordDict 和一个字符串 s,请你判断是否可以从 wordDict 中选出若干单词的排列(可以重复挑选)构成字符串 s。
// 对于输入的字符串 s,如果我能够从单词列表 wordDict 中找到一个单词匹配 s 的前缀 s[0..k],那么只要我能拼出 s[k+1..],就一定能拼出整个 s。换句话说,我把规模较大的原问题 wordBreak(s[0..]) 分解成了规模较小的子问题 wordBreak(s[k+1..]),然后通过子问题的解反推出原问题的解。
// 用哈希集合方便快速判断是否存在
let wordSet = new Set(wordDict);
// 备忘录,-1 代表未计算,0 代表无法凑出,1 代表可以凑出
let memo = Array(s.length).fill(-1);
// 主函数
function dp(i) {
// base case
if (i == s.length)
return true;
// 防止冗余计算
if (memo[i] !== -1)
return memo[i] == 0 ? false : true;
// 遍历 s[i..] 的所有前缀
for (let j = i + 1; j <= s.length; j++) {
// 看看哪些前缀存在 wordDict 中
let prefix = s.substring(i, j);
if (wordSet.has(prefix) && dp(j)) {
// 找到一个单词匹配 s[i..i+len)
// 只要 s[j..] 可以被拼出,s[i..] 就能被拼出
memo[i] = 1;
return true;
}
}
// s[i..] 无法被拼出
memo[i] = 0;
return false;
}
return dp(0);
// 因为有备忘录的辅助,消除了递归树上的重复节点,使得递归函数的调用次数从指数级别降低为状态的个数 O(N),函数本身的复杂度还是 O(N^2),所以总的时间复杂度是 O(N^3),相较回溯算法的效率有大幅提升。
};
var wordBreak = function(s, wordDict) {
let length = s.length;
const dp = new Array(length + 1);
dp.fill(false);
dp[0] = true;
for (let i = 0; i < length; i++) {
if (!dp[i]) {
continue;
}
for (let word of wordDict) {
if (word.length + i <= s.length && s.startsWith(word, i)) {
dp[i + word.length] = true;
}
}
}
return dp[length];
}
给你一个链表的头节点 head ,判断链表中是否有环。
如果链表中有某个节点,可以通过连续跟踪 next 指针再次到达,则链表中存在环。
为了表示给定链表中的环,评测系统内部使用整数 pos 来表示链表尾连接到链表中的位置(索引从 0 开始)。
注意:pos 不作为参数进行传递 。仅仅是为了标识链表的实际情况。
如果链表中存在环 ,则返回 true 。 否则,返回 false 。
示例 1:
输入:head = [3,2,0,-4], pos = 1
输出:true
解释:链表中有一个环,其尾部连接到第二个节点。
示例 2:
输入:head = [1,2], pos = 0
输出:true
解释:链表中有一个环,其尾部连接到第一个节点。
示例 3:
输入:head = [1], pos = -1
输出:false
解释:链表中没有环。
提示:
链表中节点的数目范围是 [0, 10^4]
-10^5 <= Node.val <= 10^5
pos 为 -1 或者链表中的一个 有效索引 。
进阶:你能用 O(1)(即,常量)内存解决此问题吗?
Floyd 判圈算法」(又称龟兔赛跑算法)
若是环形链表快指针总会和慢指针相遇
假想「乌龟」和「兔子」在链表上移动,「兔子」跑得快,「乌龟」跑得慢。
当「乌龟」和「兔子」从链表上的同一个节点开始移动时,如果该链表中没有环,那么「兔子」将一直处于「乌龟」的前方;
如果该链表中有环,那么「兔子」会先于「乌龟」进入环,并且一直在环内移动。
等到「乌龟」进入环时,由于「兔子」的速度快,它一定会在某个时刻与乌龟相遇,即套了「乌龟」若干圈。
定义两个指针,一快一慢。初始时,慢指针在位置 head,而快指针在位置 head.next
慢指针每次只移动一步,而快指针每次移动两步。
如果在移动的过程中,快指针反过来追上慢指针,就说明该链表为环形链表:slow.next === fast.next.next
否则快指针将到达链表尾部,该链表不为环形链表。
JSON.stringify(head) 秒杀法: 除非不报错,报错就是有环!!
标记法: 给遍历过的节点打记号,如果遍历过程中遇到有记号的说明已环
/**
* Definition for singly-linked list.
* function ListNode(val) {
* this.val = val;
* this.next = null;
* }
*/
/**
* @param {ListNode} head
* @return {boolean}
*/
var hasCycle = function(head) {
// Floyd 判圈算法」(又称龟兔赛跑算法)
// 若是环形链表快指针总会和慢指针相遇
// 假想「乌龟」和「兔子」在链表上移动,「兔子」跑得快,「乌龟」跑得慢。当「乌龟」和「兔子」从链表上的同一个节点开始移动时,如果该链表中没有环,那么「兔子」将一直处于「乌龟」的前方;如果该链表中有环,那么「兔子」会先于「乌龟」进入环,并且一直在环内移动。等到「乌龟」进入环时,由于「兔子」的速度快,它一定会在某个时刻与乌龟相遇,即套了「乌龟」若干圈。
if(head === null) return false
// 我们定义两个指针,一快一慢。初始时,慢指针在位置 head,而快指针在位置 head.next
let slow = head, fast = head.next
while(fast && fast.next) {
// 如果在移动的过程中,快指针反过来追上慢指针,就说明该链表为环形链表。
if (slow.next === fast.next.next) return true
// 慢指针每次只移动一步,而快指针每次移动两步。
slow = slow.next
fast = fast.next.next
}
// 否则快指针将到达链表尾部,该链表不为环形链表。
return false
};
/*
// JSON.stringify(head) 秒杀法😃 除非不报错,报错就是有环!!
var hasCycle = function (head) {
try {
JSON.stringify(head)
} catch{
return true
}
return false
};
*/
/**
// 标记法 给遍历过的节点打记号,如果遍历过程中遇到有记号的说明已环🤓
const hasCycle = function(head) {
while (head) {
if (head.tag) {
return true;
}
head.tag = true;
head = head.next;
}
return false;
};
*/
给定一个链表的头节点 head ,返回链表开始入环的第一个节点。 如果链表无环,则返回 null。
如果链表中有某个节点,可以通过连续跟踪 next 指针再次到达,则链表中存在环。
为了表示给定链表中的环,评测系统内部使用整数 pos 来表示链表尾连接到链表中的位置(索引从 0 开始)。如果 pos 是 -1,则在该链表中没有环。
注意:pos 不作为参数进行传递,仅仅是为了标识链表的实际情况。
不允许修改链表。
示例 1:
输入:head = [3,2,0,-4], pos = 1
输出:返回索引为 1 的链表节点
解释:链表中有一个环,其尾部连接到第二个节点。
示例 2:
输入:head = [1,2], pos = 0
输出:返回索引为 0 的链表节点
解释:链表中有一个环,其尾部连接到第一个节点。
示例 3:
输入:head = [1], pos = -1
输出:返回 null
解释:链表中没有环。
提示:
链表中节点的数目范围在范围 [0, 10^4] 内
-10^5 <= Node.val <= 10^5
pos 的值为 -1 或者链表中的一个有效索引
进阶:你是否可以使用 O(1) 空间解决此题?
哈希表法:为每个遍历的节点存一个布尔值
快慢指针:
我们使用两个指针,fast 与 slow。它们起始都位于链表的头部。
随后,slow 指针每次向后移动一个位置,而 fast 指针向后移动两个位置。
如果链表中存在环,则 fast 指针最终将再次与 slow 指针在环中相遇。
当发现 slow 与 fast 相遇时,我们再额外使用一个指针 ptr
起始,ptr指向链表头部;
随后,它和 slow 每次向后移动一个位置。最终,它们会在入环点相遇。
/**
* Definition for singly-linked list.
* function ListNode(val) {
* this.val = val;
* this.next = null;
* }
*/
/**
* @param {ListNode} head
* @return {ListNode}
*/
// 哈希表法
var detectCycle1 = function(head) {
const visited = new Set();
while (head !== null) {
if (visited.has(head)) {
return head;
}
visited.add(head);
head = head.next;
}
return null;
};
// 快慢指针
var detectCycle = function(head) {
if (head === null) {
return null;
}
// 我们使用两个指针,fast 与 slow。它们起始都位于链表的头部。
let slow = head, fast = head;
while (fast !== null) {
// // 随后,slow 指针每次向后移动一个位置,而 fast 指针向后移动两个位置。
slow = slow.next;
if (fast.next !== null) {
fast = fast.next.next;
} else {
return null;
}
// 如果链表中存在环,则 fast 指针最终将再次与 slow 指针在环中相遇。
if (fast === slow) {
// 当发现 slow 与 fast 相遇时,我们再额外使用一个指针 ptr。
let ptr = head;
// 起始,它指向链表头部;随后,它和 slow 每次向后移动一个位置。最终,它们会在入环点相遇。
while (ptr !== slow) {
ptr = ptr.next;
slow = slow.next;
}
return ptr;
}
}
return null;
};
请你设计并实现一个满足 LRU (最近最少使用) 缓存 约束的数据结构。
实现 LRUCache 类:
LRUCache(int capacity)
以 正整数 作为容量 capacity 初始化 LRU 缓存
int get(int key)
如果关键字 key 存在于缓存中,则返回关键字的值,否则返回 -1 。
void put(int key, int value)
如果关键字 key 已经存在,则变更其数据值 value ;如果不存在,则向缓存中插入该组 key-value 。
如果插入操作导致关键字数量超过 capacity ,则应该 逐出 最久未使用的关键字。
函数 get 和 put 必须以 O(1) 的平均时间复杂度运行。
示例:
输入:
["LRUCache", "put", "put", "get", "put", "get", "put", "get", "get", "get"]
[[2], [1, 1], [2, 2], [1], [3, 3], [2], [4, 4], [1], [3], [4]]
输出:
[null, null, null, 1, null, -1, null, -1, 3, 4]
解释:
LRUCache lRUCache = new LRUCache(2);
lRUCache.put(1, 1); // 缓存是 {1=1}
lRUCache.put(2, 2); // 缓存是 {1=1, 2=2}
lRUCache.get(1); // 返回 1
lRUCache.put(3, 3); // 该操作会使得关键字 2 作废,缓存是 {1=1, 3=3}
lRUCache.get(2); // 返回 -1 (未找到)
lRUCache.put(4, 4); // 该操作会使得关键字 1 作废,缓存是 {4=4, 3=3}
lRUCache.get(1); // 返回 -1 (未找到)
lRUCache.get(3); // 返回 3
lRUCache.get(4); // 返回 4
提示:
1 <= capacity <= 3000
0 <= key <= 10000
0 <= value <= 10^5
最多调用 2 * 10^5 次 get 和 put
这里是引用
class ListNode {
constructor(key, value) {
this.key = key
this.value = value
this.next = null
this.prev = null
}
}
class LRUCache {
constructor(capacity) {
this.capacity = capacity
this.hash = {}
this.count = 0
this.dummyHead = new ListNode()
this.dummyTail = new ListNode()
this.dummyHead.next = this.dummyTail
this.dummyTail.prev = this.dummyHead
}
get(key) {
let node = this.hash[key]
if (node == null) return -1
this.moveToHead(node)
return node.value
}
put(key, value) {
let node = this.hash[key]
if (node == null) {
if (this.count == this.capacity) {
this.removeLRUItem()
}
let newNode = new ListNode(key, value)
this.hash[key] = newNode
this.addToHead(newNode)
this.count++
} else {
node.value = value
this.moveToHead(node)
}
}
moveToHead(node) {
this.removeFromList(node)
this.addToHead(node)
}
removeFromList(node) {
let temp1 = node.prev
let temp2 = node.next
temp1.next = temp2
temp2.prev = temp1
}
addToHead(node) {
node.prev = this.dummyHead
node.next = this.dummyHead.next
this.dummyHead.next.prev = node
this.dummyHead.next = node
}
removeLRUItem() {
let tail = this.popTail()
delete this.hash[tail.key]
this.count--
}
popTail() {
let tail = this.dummyTail.prev
this.removeFromList(tail)
return tail
}
}
给你链表的头结点 head ,请将其按 升序 排列并返回 排序后的链表 。
示例 1:
输入:head = [4,2,1,3]
输出:[1,2,3,4]
示例 2:
输入:head = [-1,5,3,4,0]
输出:[-1,0,3,4,5]
示例 3:
输入:head = []
输出:[]
提示:
链表中节点的数目在范围 [0, 5 * 10^4] 内
-10^5 <= Node.val <= 10^5
进阶:你可以在 O(n log n) 时间复杂度和常数级空间复杂度下,对链表进行排序吗?
自底向上归并排序:
考虑合并两个有序链表
首先求得链表的长度 length,然后将链表拆分成子链表进行合并。
初始时 subLength=1,每个长度为 1 的子链表都是有序的。
如果每个长度为 subLength 的子链表已经有序,合并两个长度为 subLength 的有序子链表,得到长度为 subLength×2 的子链表,一定也是有序的。
当最后一个子链表的长度小于 subLength 时,该子链表也是有序的,合并两个有序子链表之后得到的子链表一定也是有序的。
因此可以保证最后得到的链表是有序的。
/**
* 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}
*/
// 自底向上归并排序
// 首先求得链表的长度 length,然后将链表拆分成子链表进行合并。
// 合并两个有序链表
const merge = (head1, head2) => {
const dummyHead = new ListNode(0);
let temp = dummyHead, temp1 = head1, temp2 = head2;
while (temp1 !== null && temp2 !== null) {
if (temp1.val <= temp2.val) {
temp.next = temp1;
temp1 = temp1.next;
} else {
temp.next = temp2;
temp2 = temp2.next;
}
temp = temp.next;
}
if (temp1 !== null) {
temp.next = temp1;
} else if (temp2 !== null) {
temp.next = temp2;
}
return dummyHead.next;
}
var sortList = function(head) {
if (head === null) {
return head;
}
let length = 0;
let node = head;
while (node !== null) {
length++;
node = node.next;
}
const dummyHead = new ListNode(0, head);
// 用 subLength 表示每次需要排序的子链表的长度,初始时 subLength=1。
for (let subLength = 1; subLength < length; subLength <<= 1) {
// 每次将链表拆分成若干个长度为 subLength 的子链表(最后一个子链表的长度可以小于 subLength),按照每两个子链表一组进行合并,合并后即可得到若干个长度为 subLength×2 的有序子链表(最后一个子链表的长度可以小于 subLength×2)。
let prev = dummyHead, curr = dummyHead.next;
while (curr !== null) {
let head1 = curr;
for (let i = 1; i < subLength && curr.next !== null; i++) {
curr = curr.next;
}
let head2 = curr.next;
curr.next = null;
curr = head2;
for (let i = 1; i < subLength && curr != null && curr.next !== null; i++) {
curr = curr.next;
}
let next = null;
if (curr !== null) {
next = curr.next;
curr.next = null;
}
// 合并两个子链表仍然使用「21. 合并两个有序链表」的做法
const merged = merge(head1, head2);
// 将 subLength 的值加倍,重复第 2 步,对更长的有序子链表进行合并操作,直到有序子链表的长度大于或等于 length,整个链表排序完毕。
prev.next = merged;
while (prev.next !== null) {
prev = prev.next;
}
curr = next;
}
}
return dummyHead.next;
};
/**
如何保证每次合并之后得到的子链表都是有序的呢?可以通过数学归纳法证明。
初始时 subLength=1,每个长度为 1 的子链表都是有序的。
如果每个长度为 subLength 的子链表已经有序,合并两个长度为 subLength 的有序子链表,得到长度为 subLength×2 的子链表,一定也是有序的。
当最后一个子链表的长度小于 subLength 时,该子链表也是有序的,合并两个有序子链表之后得到的子链表一定也是有序的。
因此可以保证最后得到的链表是有序的。
*/
给你一个整数数组 nums ,请你找出数组中乘积最大的非空连续子数组(该子数组中至少包含一个数字),并返回该子数组所对应的乘积。
测试用例的答案是一个 32-位 整数。
子数组 是数组的连续子序列。
示例 1:
输入: nums = [2,3,-2,4]
输出: 6
解释: 子数组 [2,3] 有最大乘积 6。
示例 2:
输入: nums = [-2,0,-1]
输出: 0
解释: 结果不能为 2, 因为 [-2,-1] 不是子数组。
提示:
1 <= nums.length <= 2 * 10^4
-10 <= nums[i] <= 10
nums 的任何前缀或后缀的乘积都 保证 是一个 32 位 整数
动态规划
遍历数组时计算当前最大值,不断更新
令imax为当前最大值,则当前最大值为 imax = max(imax * nums[i], nums[i])
由于存在负数,那么会导致最大的变最小的,最小的变最大的。
因此还需要维护当前最小值imin,imin = min(imin * nums[i], nums[i])
当负数出现时则imax与imin进行交换再进行下一步计算
max是当前所有imax结果中的最大值
max表示以当前节点为终结节点的最大连续子序列乘积
imin表示以当前节点为终结节点的最小连续子序列乘积
/**
* @param {number[]} nums
* @return {number}
*/
var maxProduct = function(nums) {
// 标签:动态规划
// 遍历数组时计算当前最大值,不断更新
// 令imax为当前最大值,则当前最大值为 imax = max(imax * nums[i], nums[i])
// 由于存在负数,那么会导致最大的变最小的,最小的变最大的。因此还需要维护当前最小值imin,imin = min(imin * nums[i], nums[i])
// 当负数出现时则imax与imin进行交换再进行下一步计算
// 时间复杂度:O(n)
// max是当前所有imax结果中的最大值
// imax表示以当前节点为终结节点的最大连续子序列乘积 imin表示以当前节点为终结节点的最小连续子序列乘积
let max = -Infinity, imax = 1, imin = 1;
for(let i=0; i<nums.length; i++){
// //当遇到负数的时候进行交换,因为阶段最小*负数就变阶段最大了,反之同理
if(nums[i] < 0){
let tmp = imax;
imax = imin;
imin = tmp;
}
//对于最小值来说,最小值是本身则说明这个元素值比前面连续子数组的最小值还小。相当于重置了阶段最小值的起始位置
imax = Math.max(imax*nums[i], nums[i]);
imin = Math.min(imin*nums[i], nums[i]);
//对比阶段最大值和结果最大值
max = Math.max(max, imax);
}
return max;
};
已知一个长度为 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 次得到输入数组。
提示:
n == nums.length
1 <= n <= 5000
-5000 <= nums[i] <= 5000
nums 中的所有整数 互不相同
nums 原来是一个升序排序的数组,并进行了 1 至 n 次旋转
仍然是二分查找
设某个数组范围最后一个元素本身为x,如果最小值落在这个范围内
在最小值右侧的元素(不包括最后一个元素本身),它们的值一定都严格小于 x;
而在最小值左侧的元素,它们的值一定都严格大于 x。
所以每次选择下次缩小二分范围时,如果二分处值和最右侧区间值x的大小比较:
如果nums[pivot] < nums[high],则high = pivot;
否则nums[pivot] >= nums[high],则low = pivot + 1;
/**
* @param {number[]} nums
* @return {number}
*/
// 二分查找:
var findMin = function(nums) {
// 在最小值右侧的元素(不包括最后一个元素本身),它们的值一定都严格小于 x;而在最小值左侧的元素,它们的值一定都严格大于 x。
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];
};
设计一个支持 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.
提示:
-2^31 <= val <= 2^31 - 1
pop、top 和 getMin 操作总是在 非空栈 上调用
push, pop, top, and getMin最多被调用 3 * 104 次
使用一个辅助栈,与元素栈同步插入与删除,用于存储与每个元素对应的最小值。
当一个元素要入栈时,我们取当前辅助栈的栈顶存储的最小值,与当前元素比较得出最小值,将这个最小值插入辅助栈中;
当一个元素要出栈时,我们把辅助栈的栈顶元素也一并弹出;
在任意一个时刻,栈内元素的最小值就存储在辅助栈的栈顶元素中。
// 使用一个辅助栈,与元素栈同步插入与删除,用于存储与每个元素对应的最小值。
var MinStack = function() {
this.x_stack = [];
this.min_stack = [Infinity];
};
MinStack.prototype.push = function(x) {
// 当一个元素要入栈时,我们取当前辅助栈的栈顶存储的最小值,与当前元素比较得出最小值,将这个最小值插入辅助栈中;
this.x_stack.push(x);
this.min_stack.push(Math.min(this.min_stack[this.min_stack.length - 1], x));
};
MinStack.prototype.pop = function() {
// 当一个元素要出栈时,我们把辅助栈的栈顶元素也一并弹出;
this.x_stack.pop();
this.min_stack.pop();
};
MinStack.prototype.top = function() {
return this.x_stack[this.x_stack.length - 1];
};
MinStack.prototype.getMin = function() {
// 在任意一个时刻,栈内元素的最小值就存储在辅助栈的栈顶元素中。
return this.min_stack[this.min_stack.length - 1];
};
/**
* 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()
*/
给你两个单链表的头节点 headA 和 headB ,请你找出并返回两个单链表相交的起始节点。
如果两个链表不存在相交节点,返回 null 。
图示两个链表在节点 c1 开始相交:
题目数据 保证 整个链式结构中不存在环。
注意,函数返回结果后,链表必须 保持其原始结构 。
自定义评测:
评测系统 的输入如下(你设计的程序 不适用 此输入):
intersectVal - 相交的起始节点的值。如果不存在相交节点,这一值为 0
listA - 第一个链表
listB - 第二个链表
skipA - 在 listA 中(从头节点开始)跳到交叉节点的节点数
skipB - 在 listB 中(从头节点开始)跳到交叉节点的节点数
评测系统将根据这些输入创建链式数据结构,并将两个头节点 headA 和 headB 传递给你的程序。
如果程序能够正确返回相交节点,那么你的解决方案将被 视作正确答案 。
示例 1:
输入:intersectVal = 8, listA = [4,1,8,4,5], listB = [5,6,1,8,4,5], skipA = 2, skipB = 3
输出:Intersected at '8'
解释:相交节点的值为 8 (注意,如果两个链表相交则不能为 0)。
从各自的表头开始算起,链表 A 为 [4,1,8,4,5],链表 B 为 [5,6,1,8,4,5]。
在 A 中,相交节点前有 2 个节点;在 B 中,相交节点前有 3 个节点。
请注意相交节点的值不为 1,因为在链表 A 和链表 B 之中值为 1 的节点 (A 中第二个节点和 B 中第三个节点) 是不同的节点。
换句话说,它们在内存中指向两个不同的位置,而链表 A 和链表 B 中值为 8 的节点 (A 中第三个节点,B 中第四个节点) 在内存中指向相同的位置。
示例 2:
输入:intersectVal = 2, listA = [1,9,1,2,4], listB = [3,2,4], skipA = 3, skipB = 1
输出:Intersected at '2'
解释:相交节点的值为 2 (注意,如果两个链表相交则不能为 0)。
从各自的表头开始算起,链表 A 为 [1,9,1,2,4],链表 B 为 [3,2,4]。
在 A 中,相交节点前有 3 个节点;在 B 中,相交节点前有 1 个节点。
示例 3:
输入:intersectVal = 0, listA = [2,6,4], listB = [1,5], skipA = 3, skipB = 2
输出:null
解释:从各自的表头开始算起,链表 A 为 [2,6,4],链表 B 为 [1,5]。
由于这两个链表不相交,所以 intersectVal 必须为 0,而 skipA 和 skipB 可以是任意值。
这两个链表不相交,因此返回 null 。
提示:
listA 中节点数目为 m
listB 中节点数目为 n
1 <= m, n <= 3 * 10^4
1 <= Node.val <= 10^5
0 <= skipA <= m
0 <= skipB <= n
如果 listA 和 listB 没有交点,intersectVal 为 0
如果 listA 和 listB 有交点,intersectVal == listA[skipA] == listB[skipB]
进阶:你能否设计一个时间复杂度 O(m + n) 、仅用 O(1) 内存的解决方案?
判断两个链表是否相交,可以使用哈希集合Set存储链表节点。
首先遍历链表 headA,并将链表 headA 中的每个节点加入哈希集合中。
然后遍历链表 headB,对于遍历到的每个节点,判断该节点是否在哈希集合中
如果当前节点在哈希集合中,则后面的节点都在哈希集合中,即从当前节点开始的所有节点都在两个链表的相交部分,因此在链表 headB 中遍历到的第一个在哈希集合中的节点就是两个链表相交的节点,返回该节点。
/**
* Definition for singly-linked list.
* function ListNode(val) {
* this.val = val;
* this.next = null;
* }
*/
/**
* @param {ListNode} headA
* @param {ListNode} headB
* @return {ListNode}
*/
var getIntersectionNode = function(headA, headB) {
// 判断两个链表是否相交,可以使用哈希集合存储链表节点。
const visited = new Set();
// 首先遍历链表 headA,并将链表 headA 中的每个节点加入哈希集合中。
let temp = headA;
while (temp !== null) {
visited.add(temp);
temp = temp.next;
}
// 然后遍历链表 headB,对于遍历到的每个节点,判断该节点是否在哈希集合中
temp = headB;
while (temp !== null) {
// 如果当前节点在哈希集合中,则后面的节点都在哈希集合中,即从当前节点开始的所有节点都在两个链表的相交部分,因此在链表 headB 中遍历到的第一个在哈希集合中的节点就是两个链表相交的节点,返回该节点。
if (visited.has(temp)) {
return temp;
}
// 如果当前节点不在哈希集合中,则继续遍历下一个节点;
temp = temp.next;
}
// 如果链表 headB 中的所有节点都不在哈希集合中,则两个链表不相交,返回 null。
return null;
};
给你两个版本号 version1 和 version2 ,请你比较它们。
版本号由一个或多个修订号组成,各修订号由一个 '.' 连接。
每个修订号由 多位数字 组成,可能包含 前导零 。每个版本号至少包含一个字符。
修订号从左到右编号,下标从 0 开始,最左边的修订号下标为 0 ,下一个修订号下标为 1 ,以此类推。
例如,2.5.33 和 0.1 都是有效的版本号。
比较版本号时,请按从左到右的顺序依次比较它们的修订号。
比较修订号时,只需比较 忽略任何前导零后的整数值 。也就是说,修订号 1 和修订号 001 相等 。
如果版本号没有指定某个下标处的修订号,则该修订号视为 0 。
例如,版本 1.0 小于版本 1.1 ,因为它们下标为 0 的修订号相同,而下标为 1 的修订号分别为 0 和 1 ,0 < 1 。
返回规则如下:
如果 version1 > version2 返回 1, 如果 version1 < version2 返回 -1, 除此之外返回 0。
示例 1:
输入:version1 = "1.01", version2 = "1.001"
输出:0
解释:忽略前导零,"01" 和 "001" 都表示相同的整数 "1"
示例 2:
输入:version1 = "1.0", version2 = "1.0.0"
输出:0
解释:version1 没有指定下标为 2 的修订号,即视为 "0"
示例 3:
输入:version1 = "0.1", version2 = "1.1"
输出:-1
解释:version1 中下标为 0 的修订号是 "0",version2 中下标为 0 的修订号是 "1" 。 0 < 1,所以 version1 < version2
提示:
1 <= version1.length, version2.length <= 500
version1 和 version2 仅包含数字和 '.'
version1 和 version2 都是 有效版本号
version1 和 version2 的所有修订号都可以存储在 32 位整数 中
将版本号字符串转换为数组,从左往右(即从高位到低位)相同的位数进行比较,某位不存在则认为是0
比较两个版本同位数数字大小即可
/**
* @param {string} version1
* @param {string} version2
* @return {number}
*/
var compareVersion = function(version1, version2) {
const v1 = version1.split('.');
const v2 = version2.split('.');
for (let i = 0; i < v1.length || i < v2.length; ++i) {
let x = 0, y = 0;
if (i < v1.length) {
x = parseInt(v1[i]);
}
if (i < v2.length) {
y = parseInt(v2[i]);
}
if (x > y) {
return 1;
}
if (x < y) {
return -1;
}
}
return 0;
};
给定一个大小为 n 的数组 nums ,返回其中的多数元素。多数元素是指在数组中出现次数 大于 ⌊ n/2 ⌋ 的元素。
你可以假设数组是非空的,并且给定的数组总是存在多数元素。
示例 1:
输入:nums = [3,2,3]
输出:3
示例 2:
输入:nums = [2,2,1,1,1,2,2]
输出:2
提示:
n == nums.length
1 <= n <= 5 * 10^4
-10^9 <= nums[i] <= 10^9
进阶:尝试设计时间复杂度为 O(n)、空间复杂度为 O(1) 的算法解决此问题。
摩尔投票法:
如果我们把众数记为 +1,把其他数记为 −1,将它们全部加起来,显然和大于 0,从结果本身我们可以看出众数比其他数多。
从第一个数开始count=1,遇到相同的就加1,遇到不同的就减1,减到0就重新换个数开始计数,总能找到最多的那个
核心就是对拼消耗。
我们维护一个候选众数 candidate 和它出现的次数 count。
初始时 candidate 可以为任意值,count 为 0
遍历数组 nums 中的所有元素,对于每个元素 x,在判断 x 之前
如果 count 的值为 0,我们先将 x 的值赋予 candidate,随后我们判断x
如果 x 与 candidate 相等,那么计数器 count 的值增加 1;
如果 x 与 candidate 不等,那么计数器 count 的值减少 1。
/**
* @param {number[]} nums
* @return {number}
*/
var majorityElement = function(nums) {
// 摩尔投票法:
// 如果我们把众数记为 +1,把其他数记为 −1,将它们全部加起来,显然和大于 0,从结果本身我们可以看出众数比其他数多。
// 从第一个数开始count=1,遇到相同的就加1,遇到不同的就减1,减到0就重新换个数开始计数,总能找到最多的那个
// 核心就是对拼消耗。
// 玩一个诸侯争霸的游戏,假设你方人口超过总人口一半以上,并且能保证每个人口出去干仗都能一对一同归于尽。最后还有人活下来的国家就是胜利。
// 那就大混战呗,最差所有人都联合起来对付你(对应你每次选择作为计数器的数都是众数),或者其他国家也会相互攻击(会选择其他数作为计数器的数),但是只要你们不要内斗,最后肯定你赢。最后能剩下的必定是自己人。
// 我们维护一个候选众数 candidate 和它出现的次数 count。初始时 candidate 可以为任意值,count 为 0
// 我们遍历数组 nums 中的所有元素,对于每个元素 x,在判断 x 之前,如果 count 的值为 0,我们先将 x 的值赋予 candidate,随后我们判断 x:
// 如果 x 与 candidate 相等,那么计数器 count 的值增加 1;
// 如果 x 与 candidate 不等,那么计数器 count 的值减少 1。
// 在遍历完成后,candidate 即为整个数组的众数
let count = 0;
let candidate = null;
for (let num of nums) {
if (count == 0) {
candidate = num;
}
count += (num == candidate) ? 1 : -1;
}
return candidate;
};
var majorityElement1 = function(nums) {
// 排序法
nums.sort((a,b) => a - b);
return nums[nums.length / 2];
}
给定一个整数 n ,返回 n! 结果中尾随零的数量。
提示 n! = n * (n - 1) * (n - 2) * ... * 3 * 2 * 1
示例 1:
输入:n = 3
输出:0
解释:3! = 6 ,不含尾随 0
示例 2:
输入:n = 5
输出:1
解释:5! = 120 ,有一个尾随 0
示例 3:
输入:n = 0
输出:0
提示:
0 <= n <= 10^4
进阶:你可以设计并实现对数时间复杂度的算法来解决此问题吗?
首先末尾有多少个 0 ,只需要给当前数乘以一个 10 就可以加一个 0。
含有 2 的因子每两个出现一次,含有 5 的因子每 5 个出现一次,所有 2 出现的个数远远多于 5,换言之找到一个 5,一定能找到一个 2 与之配对。所以我们只需要找有多少个 5。
直接的,我们只需要判断每个累乘的数有多少个 5 的因子即可
/**
* @param {number} n
* @return {number}
*/
var trailingZeroes = function(n) {
// 0的贡献来源于:本身有0,或是偶数与5的倍数相乘获得;
// 1、由于偶数的个数一定比5的倍数的个数多,因而对于这种情况只考虑5的倍数的个数
// 2、本身有0的数恰好是5的倍数,拥有的0的个数恰好又与是5^n的倍数的n相等
// 综上,只需要计算阶乘中各个5的次方数的个数和即可。【因为是多少次方就会在其中被计算多少次,因而不需要考虑是否会有重复计算的问题】
let count = 0;
while(n >= 5){
n = Math.floor(n / 5);
count += n;
}
return count
};
给定一组非负整数 nums,重新排列每个数的顺序(每个数不可拆分)使之组成一个最大的整数。
注意:输出结果可能非常大,所以你需要返回一个字符串而不是整数。
示例 1:
输入:nums = [10,2]
输出:"210"
示例 2:
输入:nums = [3,30,34,5,9]
输出:"9534330"
提示:
1 <= nums.length <= 100
0 <= nums[i] <= 10^9
要想组成最大的整数,一种直观的想法是把数值大的数放在高位。
于是我们可以比较输入数组的每个元素的最高位,最高位相同的时候比较次高位,以此类推,完成排序,然后把它们拼接起来。
1.这种排序方式对于输入数组 没有相同数字开头 的时候是有效的
2.输入数组 有相同数字开头 的情况,例如 [4,42] 和 [4,45]。
对于 442>424,需要把 4 放在前面;
对于 445<454,需要把 45 放在前面。
此我们需要比较两个数不同的拼接顺序的结果,进而决定它们在结果中的排列顺序。
综上,我们自己构造排序函数sort(compareFn)
compareFn(a, b) 返回值:
大于 0,a 在 b 后,如 [b, a]
小于 0,a 在 b 前,如 [a, b]
等于0,保持 a 和 b 原来的顺序
需,构造两数比较后,更大的数字放前面
/**
* @param {number[]} nums
* @return {string}
*/
var largestNumber = function(nums) {
// 要想组成最大的整数,一种直观的想法是把数值大的数放在高位。于是我们可以比较输入数组的每个元素的最高位,最高位相同的时候比较次高位,以此类推,完成排序,然后把它们拼接起来。
// 1.这种排序方式对于输入数组 没有相同数字开头 的时候是有效的
// 2.输入数组 有相同数字开头 的情况,例如 [4,42] 和 [4,45]。
// 对于 442>424,需要把 4 放在前面;
// 对于 445<454,需要把 45 放在前面。
// 因此我们需要比较两个数不同的拼接顺序的结果,进而决定它们在结果中的排列顺序。
nums.sort((x, y) => {
let sx = 10, sy = 10;
while (sx <= x) {
sx *= 10;
}
while (sy <= y) {
sy *= 10;
}
return '' + (sx * y + x) - ('' + (sy * x + y));
})
if (nums[0] === 0) {
return '0';
}
return nums.join('');
};
给定一个整数数组 nums,将数组中的元素向右轮转 k 个位置,其中 k 是非负数。
示例 1:
输入: nums = [1,2,3,4,5,6,7], k = 3
输出: [5,6,7,1,2,3,4]
解释:
向右轮转 1 步: [7,1,2,3,4,5,6]
向右轮转 2 步: [6,7,1,2,3,4,5]
向右轮转 3 步: [5,6,7,1,2,3,4]
示例 2:
输入:nums = [-1,-100,3,99], k = 2
输出:[3,99,-1,-100]
解释:
向右轮转 1 步: [99,-1,-100,3]
向右轮转 2 步: [3,99,-1,-100]
提示:
1 <= nums.length <= 10^5
-2^31 <= nums[i] <= 2^31 - 1
0 <= k <= 10^5
进阶:
尽可能想出更多的解决方案,至少有 三种 不同的方法可以解决这个问题。
你可以使用空间复杂度为 O(1) 的 原地 算法解决这个问题吗?
首先定义一个反转数组的函数
根据题意,反转三次即可:一次整体反转,两次局部反转。
/**
* @param {number[]} nums
* @param {number} k
* @return {void} Do not return anything, modify nums in-place instead.
*/
// 注意,上面要求不返回值,而是直接改变原数组,进行原地交换。
// 首先定义一个反转数组的函数
const reverse = (nums, start, end) => {
while (start < end) {
const temp = nums[start];
nums[start] = nums[end];
nums[end] = temp;
start += 1;
end -= 1;
}
}
// 根据题意,反转三次即可:一次整体反转,两次局部反转。
// 另外,需要注意k大于num.length的情况,因此需要做取余数处理
var rotate = function(nums, k) {
k %= nums.length;
reverse(nums, 0, nums.length - 1);
reverse(nums, 0, k - 1);
reverse(nums, k, nums.length - 1);
};
你是一个专业的小偷,计划偷窃沿街的房屋。每间房内都藏有一定的现金,影响你偷窃的唯一制约因素就是相邻的房屋装有相互连通的防盗系统,如果两间相邻的房屋在同一晚上被小偷闯入,系统会自动报警。
给定一个代表每个房屋存放金额的非负整数数组,计算你 不触动警报装置的情况下 ,一夜之内能够偷窃到的最高金额。
示例 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 。
提示:
1 <= nums.length <= 100
0 <= nums[i] <= 400
动态规划:
假设一共有n间房子,H[i]表示,第i间房屋的金额,i从0开始。S[i]表示前i间房屋能偷到的最大金额。
S[0] = H[0] // 第一间屋子
S[1] = Math.max(S[0], H[1]) // 第二间屋子最大金额为第二间与第一间中较大的金额
算包含索引为i的那间屋子之前的能偷到的最大金额无非为以下两种情况中的最大的那种:
情况一:不包含i那间屋子。那么总金额为S[i - 1]
情况二:需包含i那间屋子。那么总金额为S[i - 2] + H[i]
S[i] = Math.max(S[i - 1], S[i - 2] + H[i])
/**
* @param {number[]} nums
* @return {number}
*/
var rob = function(nums) {
// 动态规划的的四个解题步骤是:
// 1.定义子问题。子问题是和原问题相似,但规模较小的问题。
// 2.写出子问题的递推关系。
// 3.确定 DP 数组的计算顺序
// 4.空间优化(可选)
// 下面我们一步一步地进行讲解。
// 原问题是 “从全部房子中能偷到的最大金额”,将问题的规模缩小。
// 假设一共有n间房子,H[i]表示,第i间房屋的金额,i从0开始。S[i]表示前i间房屋能偷到的最大金额。
// 则:
// S[0] = H[0] // 第一间屋子
// S[1] = Math.max(S[0], H[1]) // 第二间屋子最大金额为第二间与第一间中较大的金额
// S[2] = Math.max(S[1], S[0] + H[2]) // 第三间屋子的最大金额为前两间屋子的最大金额与第一间屋子和第三间屋子的总和中较大的金额
// 所以,计算包含索引为i的那间屋子之前的能偷到的最大金额无非为以下两种情况中的最大的那种:
// 情况一:不包含i那间屋子。那么总金额为S[i - 1]
// 情况二:需包含i那间屋子。那么总金额为S[i - 2] + H[i]
// S[i] = Math.max(S[i - 1], S[i - 2] + H[i])
// 动态规划有两种计算顺序,一种是自顶向下的、使用备忘录的递归方法,一种是自底向上的、使用 dp 数组的循环方法。不过在普通的动态规划题目中,99% 的情况我们都不需要用到备忘录方法,所以我们最好坚持用自底向上的 dp 数组。
// DP 数组也可以叫”子问题数组”,因为 DP 数组中的每一个元素都对应一个子问题。
// dp[i] 对应子问题 S[i],即偷前 0 至 i 间房子的最大金额
// 对于小偷问题,我们分析子问题的依赖关系,发现每个 S[i] 依赖S[i - 1] 和S[i - 2]。也就是说,dp[i] 依赖 dp[i-1] 和 dp[i-2]
// 既然 DP 数组中的依赖关系都是向右指的,DP 数组的计算顺序就是从左向右。这样我们可以保证,计算一个子问题的时候,它所依赖的那些子问题已经计算出来了。
// 空间优化是动态规划问题的进阶内容了空间优化的基本原理是,很多时候我们并不需要始终持有全部的 DP 数组。
// 对于小偷问题,我们发现,最后一步计算 f(n) 的时候,实际上只用到了 S[n−1] 和 S[n−2] 的结果。n−3 之前的子问题,实际上早就已经用不到了。那么,我们可以只用两个变量保存两个子问题的结果,就可以依次计算出所有的子问题。
let prev = 0;
let curr = 0;
// 每次循环,计算“偷到当前房子为止的最大金额”
for (let i of nums) {
// 循环开始前,curr 表示 dp[k-1],prev 表示 dp[k-2]
// dp[k] = max{ dp[k-1], dp[k-2] + i }
let temp = Math.max(curr, prev + i);
prev = curr;
curr = temp;
// 循环结束时,curr 表示 dp[k],prev 表示 dp[k-1]
}
return curr;
};
var rob1 = function(nums) {
if (nums.length == 0) {
return 0;
}
// 子问题:
// f(k) = 偷 [0..k) 房间中的最大金额
// f(0) = 0
// f(1) = nums[0]
// f(k) = Math.max(rob(k-1), nums[k-1] + rob(k-2))
const N = nums.length;
let dp = new Array(N+1);
dp[0] = 0;
dp[1] = nums[0];
for (let k = 2; k <= N; k++) {
dp[k] = Math.max(dp[k-1], nums[k-1] + dp[k-2]);
}
return dp[N];
}
给定一个二叉树的 根节点 root,想象自己站在它的右侧,按照从顶部到底部的顺序,返回从右侧所能看到的节点值。
示例 1:
输入: [1,2,3,null,5,null,4]
输出: [1,3,4]
示例 2:
输入: [1,null,3]
输出: [1,3]
示例 3:
输入: []
输出: []
提示:
二叉树的节点个数的范围是 [0,100]
-100 <= Node.val <= 100
二叉树右视图
广度优先遍历,只需要把每一层最后一个节点存储到res数组
/**
* 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[]}
*/
var rightSideView = function(root) {
//二叉树右视图 只需要把每一层最后一个节点存储到res数组
let res=[],queue=[];
queue.push(root);
while(queue.length && root !== null){
// 记录当前层级节点个数
let length = queue.length;
while(length--){
let node=queue.shift();
//length长度为0的时候表明到了层级最后一个节点
if(!length){
res.push(node.val);
}
node.left && queue.push(node.left);
node.right && queue.push(node.right);
}
}
return res;
};
/**
二叉树层序遍历
*/
var levelOrder = function(root) {
//二叉树的层序遍历
let res=[],queue=[];
queue.push(root);
if(root===null){
return res;
}
while(queue.length!==0){
// 记录当前层级节点数
let length=queue.length;
//存放每一层的节点
let curLevel=[];
for(let i=0;i<length;i++){
let node=queue.shift();
curLevel.push(node.val);
// 存放当前层下一层的节点
node.left&&queue.push(node.left);
node.right&&queue.push(node.right);
}
//把每一层的结果放到结果数组
res.push(curLevel);
}
return res;
};
给你一个由 '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
提示:
m == grid.length
n == grid[i].length
1 <= m, n <= 300
grid[i][j]
的值为 '0' 或 '1'
遍历二维数组
当遇到为‘1’的元素时,记录一次岛屿数量,并将该元素四周的元素递归地变为‘0’
/**
* @param {character[][]} grid
* @return {number}
*/
// dfs:
// 循环网格,深度优先遍历每个坐标的四周,注意坐标不要越界,遇到陆地加1,并沉没四周的陆地,这样就不会重复计算
const numIslands = (grid) => {
let count = 0
for (let i = 0; i < grid.length; i++) {
for (let j = 0; j < grid[0].length; j++) {//循环网格
if (grid[i][j] === '1') {
//如果为陆地,count++,
count++
turnZero(i, j, grid)
}
}
}
return count
}
function turnZero(i, j, grid) {//沉没四周的陆地
// 注意坐标不要越界
if (i < 0 || i >= grid.length || j < 0 || j >= grid[0].length || grid[i][j] === '0') return //检查坐标的合法性
//让四周的陆地变为海水
grid[i][j] = '0'
turnZero(i, j + 1, grid)
turnZero(i, j - 1, grid)
turnZero(i + 1, j, grid)
turnZero(i - 1, j, grid)
}
给你单链表的头节点 head ,请你反转链表,并返回反转后的链表。
示例 1:
输入:head = [1,2,3,4,5]
输出:[5,4,3,2,1]
示例 2:
输入:head = [1,2]
输出:[2,1]
示例 3:
输入:head = []
输出:[]
提示:
链表中节点的数目范围是 [0, 5000]
-5000 <= Node.val <= 5000
进阶:链表可以选用迭代或递归方式完成反转。你能否用两种方法解决这道题?
一次循环
用两个变量记录连续的前和现两个节点
用临时变量记录后一个节点
断开和后一个节点的链接改为链接前一个节点
节点继续向后移动
/**
* Definition for singly-linked list.
* function ListNode(val, next) {
* this.val = (val===undefined ? 0 : val)
* this.next = (next===undefined ? null : next)
* }
*/
/**
* @param {ListNode} head
* @return {ListNode}
*/
var reverseList = function(head) {
let prev = null;
let curr = head;
while (curr) {
const next = curr.next;
curr.next = prev;
prev = curr;
curr = next;
}
return prev;
};
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
提示:
1 <= word.length, prefix.length <= 2000
word 和 prefix 仅由小写英文字母组成
insert、search 和 startsWith 调用次数 总计 不超过 3 * 10^4 次
Trie树是一种专门处理字符串匹配的数据结构,用来在一批字符串中,快速查找某个指定的字符串或字符串前缀。常用于搜索建议输入法预测等场景。
比如说我们有一组字符串,分别是 ha、head、hacker、book、boss,如果要查询某个指定的字符串是否在这一组中,那么要对每个字符串进行匹配,如果这个查询操作有很多次,这种查询势必是比较耗时的。观察一下,这一组字符串都有公共的前缀,那能不能利用这个特性,降低查询时间复杂度呢?Trie树就是为这种问题而生的,我们看一下把这一组字符串构造成trie树是什么样子:
前缀树最多有26个孩子节点。在每个节点中,使用一个hash表来保存字符与孩子节点之间的对应关系
相比二叉树,前缀树节点的孩子节点比较多,而且前缀树节点中没有val值,其实它的val值可以通过父节点的children这个hash表推算出来,所以没有必要再增加一个val字段
由"a"生成的前缀树如下图:
前缀树的根节点是没有类似二叉树val字段信息的,字符串"a"中只有一个字符,生成的前缀树需要两个节点,孩子节点表示字符‘a’,孩子节点中的isEnd = true表示这个节点为字符串“a”在前缀树中的最后一个节点
search必须完全匹配某个前缀树,而startWith仅匹配到前缀树的一部分即可
var Trie = function() {
this.children = {};
};
/**
* @param {string} word
* @return {void}
*/
Trie.prototype.insert = function(word) {
// 指向子节点的指针数组 children。对于本题而言,数组长度为 26,即小写英文字母的数量。
let node = this.children;
// 子节点存在。沿着指针移动到子节点,继续处理下一个字符。
for (const ch of word) {
// 子节点不存在。创建一个新的子节点,记录在 children\textit{children}children 数组的对应位置上,然后沿着指针移动到子节点,继续搜索下一个字符。
if (!node[ch]) {
node[ch] = {};
}
node = node[ch];
}
// 布尔字段 isEnd,表示该节点是否为字符串的结尾。
node.isEnd = true;
};
Trie.prototype.searchPrefix = function(prefix) {
let node = this.children;
// 子节点存在。沿着指针移动到子节点,继续搜索下一个字符。
for (const ch of prefix) {
if (!node[ch]) {
return false;
}
node = node[ch];
}
// 子节点不存在。说明字典树中不包含该前缀,返回空指针。
return node;
}
/**
* @param {string} word
* @return {boolean}
*/
Trie.prototype.search = function(word) {
const node = this.searchPrefix(word);
return node !== undefined && node.isEnd !== undefined;
};
/**
* @param {string} prefix
* @return {boolean}
*/
Trie.prototype.startsWith = function(prefix) {
return this.searchPrefix(prefix);
};
/**
* 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)
*/
给定整数数组 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
提示:
1 <= k <= nums.length <= 10^5
-10^4 <= nums[i] <= 10^4
使用内置排序算法或者快排对数组 nums 进行排序,然后返回第 N−k 个元素即可
建立一个大根堆,做 k−1k - 1k−1 次删除操作后堆顶元素就是我们要找的答案:
将无序序列构建成一个堆,根据升序降序需求选择大顶堆
将堆顶元素与末尾元素交换,将最大元素「沉」到数组末端
重新调整结构,使其满足堆定义,然后继续交换堆顶与当前末尾元素,反复执行调整、交换步骤,直到整个序列有序。
/**
* 堆排序算法
* @param {number[]} arr - 待排序的数组
* @returns {number[]} - 排序后的数组
*/
function heapSort(arr) {
// 构建最大堆
buildMaxHeap(arr);
// 从最后一个元素开始,依次将其与堆顶元素交换,然后重新调整堆
for (let i = arr.length - 1; i > 0; i--) {
swap(arr, 0, i);
heapify(arr, 0, i);
}
return arr;
}
/**
* 构建最大堆
* @param {number[]} arr - 待构建的数组
*/
function buildMaxHeap(arr) {
// 从最后一个非叶子节点开始,依次进行堆调整
for (let i = Math.floor(arr.length / 2) - 1; i >= 0; i--) {
heapify(arr, i, arr.length);
}
}
/**
* 堆调整函数
* @param {number[]} arr - 待调整的数组
* @param {number} i - 当前节点的索引
* @param {number} len - 堆的长度
*/
function heapify(arr, i, len) {
// 左子节点的索引
let left = 2 * i + 1;
// 右子节点的索引
let right = 2 * i + 2;
// 最大值的索引
let largest = i;
// 如果左子节点比当前节点大,则更新最大值的索引
if (left < len && arr[left] > arr[largest]) {
largest = left;
}
// 如果右子节点比当前节点大,则更新最大值的索引
if (right < len && arr[right] > arr[largest]) {
largest = right;
}
// 如果最大值的索引不是当前节点,则交换它们的值,并继续调整堆
if (largest !== i) {
swap(arr, i, largest);
heapify(arr, largest, len);
}
}
/**
* 交换数组中两个元素的值
* @param {number[]} arr - 待交换的数组
* @param {number} i - 第一个元素的索引
* @param {number} j - 第二个元素的索引
*/
function swap(arr, i, j) {
let temp = arr[i];
arr[i] = arr[j];
arr[j] = temp;
}
/**
* @param {number[]} nums
* @param {number} k
* @return {number}
*/
/**
* @param {number[]} nums
* @param {number} k
* @return {number}
*/
// 整个流程就是上浮下沉
var findKthLargest = function(nums, k) {
/**
将无序序列构建成一个堆,根据升序降序需求选择大顶堆
将堆顶元素与末尾元素交换,将最大元素「沉」到数组末端
重新调整结构,使其满足堆定义,然后继续交换堆顶与当前末尾元素,反复执行调整、交换步骤,直到整个序列有序。
*/
let heapSize=nums.length
buildMaxHeap(nums,heapSize) // 构建好了一个大顶堆
// 进行下沉 大顶堆是最大元素下沉到末尾
for(let i=nums.length-1;i>=nums.length-k+1;i--){
swap(nums,0,i)
--heapSize // 下沉后的元素不参与到大顶堆的调整
// 重新调整大顶堆
maxHeapify(nums, 0, heapSize);
}
return nums[0]
// 自下而上构建一颗大顶堆
function buildMaxHeap(nums,heapSize){
for(let i=Math.floor(heapSize/2)-1;i>=0;i--){
maxHeapify(nums,i,heapSize)
}
}
// 从左向右,自上而下的调整节点
function maxHeapify(nums,i,heapSize){
let l=i*2+1
let r=i*2+2
let largest=i
if(l < heapSize && nums[l] > nums[largest]){
largest=l
}
if(r < heapSize && nums[r] > nums[largest]){
largest=r
}
if(largest!==i){
swap(nums,i,largest) // 进行节点调整
// 继续调整下面的非叶子节点
maxHeapify(nums,largest,heapSize)
}
}
function swap(a, i, j){
let temp = a[i];
a[i] = a[j];
a[j] = temp;
}
};
给你一棵二叉树的根节点 root ,翻转这棵二叉树,并返回其根节点。
示例 1:
输入:root = [4,2,7,1,3,6,9]
输出:[4,7,2,9,6,3,1]
示例 2:
输入:root = [2,1,3]
输出:[2,3,1]
示例 3:
输入:root = []
输出:[]
提示:
树中节点数目范围在 [0, 100] 内
-100 <= Node.val <= 100
递归:交换一下左右节点,然后再递归的交换左节点,右节点
迭代:
先将根节点放入到队列中,然后不断的迭代队列中的元素。对当前元素调换其左右子树的位置,
然后:
1.判断其左子树是否为空,不为空就放入队列中
2.判断其右子树是否为空,不为空就放入队列中
递归实现也就是深度优先遍历的方式,那么对应的就是广度优先遍历。 广度优先遍历需要额外的数据结构--队列,来存放临时遍历到的元素
深度优先遍历的特点是一竿子插到底,不行了再退回来继续;而广度优先遍历的特点是层层扫荡。
/**
* 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}
*/
// 递归:交换一下左右节点,然后再递归的交换左节点,右节点
var invertTree1 = function(root) {
//递归函数的终止条件,节点为空时返回
if(root==null) {
return null;
}
//下面三句是将当前节点的左右子树交换
const tmp = root.right;
root.right = root.left;
root.left = tmp;
//递归交换当前节点的 左子树
invertTree1(root.left);
//递归交换当前节点的 右子树
invertTree1(root.right);
//函数返回时就表示当前这个节点,以及它的左右子树
//都已经交换完了
return root;
};
// 迭代:先将根节点放入到队列中,然后不断的迭代队列中的元素。对当前元素调换其左右子树的位置,然后1.判断其左子树是否为空,不为空就放入队列中2.判断其右子树是否为空,不为空就放入队列中
// 递归实现也就是深度优先遍历的方式,那么对应的就是广度优先遍历。 广度优先遍历需要额外的数据结构--队列,来存放临时遍历到的元素。
// 深度优先遍历的特点是一竿子插到底,不行了再退回来继续;而广度优先遍历的特点是层层扫荡。
var invertTree = function(root) {
if(root==null) {
return null;
}
// 维护一个队列,初始推入第一层的root
const queue = [root]
while(queue.length) {
//每次都从队列中拿一个节点,并交换这个节点的左右子树
const tmp = queue.shift();
[tmp.left, tmp.right] = [tmp.right, tmp.left]; // 交换左右子树
//如果当前节点的左子树不为空,则放入队列等待后续处理
if(tmp.left) {
queue.push(tmp.left);
}
//如果当前节点的右子树不为空,则放入队列等待后续处理
if(tmp.right) {
queue.push(tmp.right);
}
}
//返回处理完的根节点
return root;
}
给定一个二叉搜索树的根节点 root ,和一个整数 k ,请你设计一个算法查找其中第 k 个最小元素(从 1 开始计数)。
示例 1:
输入:root = [3,1,4,null,2], k = 1
输出:1
示例 2:
输入:root = [5,3,6,2,4,null,null,1], k = 3
输出:3
提示:
树中的节点数为 n 。
1 <= k <= n <= 10^4
0 <= Node.val <= 10^4
进阶:如果二叉搜索树经常被修改(插入/删除操作)并且你需要频繁地查找第 k 小的值,你将如何优化算法?
二叉搜索树具有如下性质:
二叉树的中序遍历即按照访问左子树——根结点——右子树的方式遍历二叉树;
在访问其左子树和右子树时,我们也按照同样的方式遍历;直到遍历完整棵树。
所以二叉搜索树的中序遍历是按照键增加的顺序进行的。于是,我们可以通过中序遍历找到第 k 个最小元素。
输出升序数组第k个最小的元素 下标为 k - 1
/**
* 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}
*/
/**
二叉搜索树具有如下性质:
1.结点的左子树只包含小于当前结点的数。
2.结点的右子树只包含大于当前结点的数。
3.所有左子树和右子树自身必须也是二叉搜索树。
*/
// 二叉树的中序遍历即按照访问左子树——根结点——右子树的方式遍历二叉树;
// 在访问其左子树和右子树时,我们也按照同样的方式遍历;直到遍历完整棵树。
// 所以二叉搜索树的中序遍历是按照键增加的顺序进行的。于是,我们可以通过中序遍历找到第 k 个最小元素。
// 迭代
var kthSmallest1 = function(root, k) {
const stack = [];
while (root != null || stack.length) {
while (root != null) {
stack.push(root);
root = root.left;
}
root = stack.pop();
--k;
if (k === 0) {
break;
}
root = root.right;
}
return root.val;
};
// 递归
var kthSmallest = function(root, k) {
const res = [];
// 看到是二叉搜索树应该想到使用中序遍历得到树节点排序好的升序数组
function traversal(node){
if (node){
traversal(node.left);
res.push(node.val);
traversal(node.right);
}
}
traversal(root);
// 输出升序数组第k个最小的元素 下标为 k - 1
return res[k - 1];
};
给你一个单链表的头节点 head ,请你判断该链表是否为回文链表。如果是,返回 true ;否则,返回 false 。
示例 1:
输入:head = [1,2,2,1]
输出:true
示例 2:
输入:head = [1,2]
输出:false
提示:
链表中节点数目在范围[1, 10^5] 内
0 <= Node.val <= 9
进阶:你能否用 O(n) 时间复杂度和 O(1) 空间复杂度解决此题?
将链表里的值复制到数组中后用双指针法
双指针判断是否为回文,执行了n/2次
快慢指针+反转链表,比较前后部分
/**
* 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}
*/
// 方法一:将值复制到数组中后用双指针法
var isPalindrome = function(head) {
// 遍历链表将值复制到数组列表中
const vals = [];
// 用 currentNode 指向当前节点。每次迭代向数组添加 currentNode.val,并更新 currentNode = currentNode.next,当 currentNode = null 时停止循环
while (head !== null) {
vals.push(head.val);
head = head.next;
}
// 双指针判断是否为回文,执行了n/2次
for (let i = 0, j = vals.length - 1; i < j; ++i, --j) {
if (vals[i] !== vals[j]) {
return false;
}
}
return true;
};
给定一个二叉树, 找到该树中两个指定节点的最近公共祖先。
百度百科中最近公共祖先的定义为:“对于有根树 T 的两个节点 p、q,最近公共祖先表示为一个节点 x,满足 x 是 p、q 的祖先且 x 的深度尽可能大(一个节点也可以是它自己的祖先)。”
示例 1:
输入:root = [3,5,1,6,2,0,8,null,null,7,4], p = 5, q = 1
输出:3
解释:节点 5 和节点 1 的最近公共祖先是节点 3 。
示例 2:
输入:root = [3,5,1,6,2,0,8,null,null,7,4], p = 5, q = 4
输出:5
解释:节点 5 和节点 4 的最近公共祖先是节点 5 。因为根据定义最近公共祖先节点可以为节点本身。
示例 3:
输入:root = [1,2], p = 1, q = 2
输出:1
提示:
树中节点数目在范围 [2, 10^5] 内。
-10^9 <= Node.val <= 10^9
所有 Node.val 互不相同 。
p != q
p 和 q 均存在于给定的二叉树中。
二叉树自底向上查找,即二叉树回溯的过程就是从底到上
后序遍历(左右中)就是天然的回溯过程,可以根据左右子树的返回值,来处理中节点的逻辑。
情况一:如果找到一个节点,发现左子树出现结点p,右子树出现节点q,或者 左子树出现结点q,右子树出现节点p,那么该节点就是节点p和q的最近公共祖先。
情况二:因为遇到 q 或者 p 就返回,这样也包含了 q 或者 p 本身就是 公共祖先的情况
/**
* Definition for a binary tree node.
* function TreeNode(val) {
* this.val = val;
* this.left = this.right = null;
* }
*/
/**
* @param {TreeNode} root
* @param {TreeNode} p
* @param {TreeNode} q
* @return {TreeNode}
*/
// 那么二叉树如何可以自底向上查找呢?
// 回溯啊,二叉树回溯的过程就是从低到上。
// 后序遍历(左右中)就是天然的回溯过程,可以根据左右子树的返回值,来处理中节点的逻辑。
// 情况一:如果找到一个节点,发现左子树出现结点p,右子树出现节点q,或者 左子树出现结点q,右子树出现节点p,那么该节点就是节点p和q的最近公共祖先。
// 情况二:因为遇到 q 或者 p 就返回,这样也包含了 q 或者 p 本身就是 公共祖先的情况。
// 递归:
// 1.确定递归函数返回值以及参数:
// 需要递归函数返回值,来告诉我们是否找到节点q或者p,那么返回值为bool类型。如果遇到p或者q,就把q或者p返回,返回值不为空,就说明找到了q或者p。
// 2.确定终止条件
// 遇到空的话,因为树都是空了,所以返回空。
// 如果 root == q,或者 root == p,说明找到 q p ,则将其返回
// 3.确定单层递归逻辑
// 本题函数有返回值,是因为回溯的过程需要递归函数的返回值做判断,但本题我们依然要遍历树的所有节点。回溯遍历整棵二叉树,将结果返回给头结点。
var lowestCommonAncestor = function(root, p, q) {
// 使用递归的方法
// 需要从下到上,所以使用后序遍历
// 1. 确定递归的函数
const travelTree = function(root,p,q) {
// 2. 确定递归终止条件
if(root === null || root === p || root === q) {
return root;
}
// 3. 确定递归单层逻辑
let left = travelTree(root.left,p,q);
let right = travelTree(root.right,p,q);
// 情况一:如果找到一个节点,发现左子树出现结点p,右子树出现节点q,或者 左子树出现结点q,右子树出现节点p,那么该节点就是节点p和q的最近公共祖先。
if(left !== null && right !== null) {
return root;
}
// 如果left为空,说明左子树不包含目标节点,结果由右子树决定。
if(left === null) {
return right;
}
// left不为空,结果由左子树决定
return left;
}
return travelTree(root,p,q);
};
给你一个整数数组 nums,返回 数组 answer ,其中 answer[i] 等于 nums 中除 nums[i] 之外其余各元素的乘积 。
题目数据 保证 数组 nums之中任意元素的全部前缀元素和后缀的乘积都在 32 位 整数范围内。
请不要使用除法,且在 O(n) 时间复杂度内完成此题。
示例 1:
输入: nums = [1,2,3,4]
输出: [24,12,8,6]
示例 2:
输入: nums = [-1,1,0,-3,3]
输出: [0,0,9,0,0]
提示:
2 <= nums.length <= 10^5
-30 <= nums[i] <= 30
保证 数组 nums之中任意元素的全部前缀元素和后缀的乘积都在 32 位 整数范围内
进阶:你可以在 O(1) 的额外空间复杂度内完成这个题目吗?( 出于对空间复杂度分析的目的,输出数组不被视为额外空间。)
遍历两次即可
先遍历一次,answer[i] 先存索引 i 左侧所有元素的乘积(不包含i)
因为索引为 '0' 的元素左侧没有元素, 所以 answer[0] = 1
然后再遍历一次,计算i右侧元素乘积
R 为右侧所有元素的乘积,R从右侧开始计算,每次算出后更新到R变量里,从而节省空间复杂度。
刚开始右边没有元素,所以 R = 1
/**
* @param {number[]} nums
* @return {number[]}
*/
var productExceptSelf = function(nums) {
const length = nums.length;
const answer = new Array(length);
// answer[i] 先存索引 i 左侧所有元素的乘积(不包含i)
// 因为索引为 '0' 的元素左侧没有元素, 所以 answer[0] = 1
answer[0] = 1;
for (let i = 1; i < length; i++) {
answer[i] = nums[i - 1] * answer[i - 1];
}
// 计算i右侧元素乘积
// R 为右侧所有元素的乘积,R从右侧开始计算,每次算出后更新到R变量里,从而节省空间复杂度
// 刚开始右边没有元素,所以 R = 1
let R = 1;
for (let i = length - 1; i >= 0; i--) {
// 对于索引 i,左边的乘积为 answer[i],右边的乘积为 R,则需将二者相乘满足题意 answer[i] 等于 nums 中除 nums[i] 之外其余各元素的乘积
answer[i] = answer[i] * R;
// R 需要包含右边所有的乘积,所以计算下一个结果时需要将当前值乘到 R 上,更新R值,从而减少空间复杂度
R *= nums[i];
}
return answer;
};
给你一个整数数组 nums,有一个大小为 k 的滑动窗口从数组的最左侧移动到数组的最右侧。
你只可以看到在滑动窗口内的 k 个数字。滑动窗口每次只向右移动一位。
返回 滑动窗口中的最大值 。
示例 1:
输入: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 3
1 [3 -1 -3] 5 3 6 7 3
1 3 [-1 -3 5] 3 6 7 5
1 3 -1 [-3 5 3] 6 7 5
1 3 -1 -3 [5 3 6] 7 6
1 3 -1 -3 5 [3 6 7] 7
示例 2:
输入:nums = [1], k = 1
输出:[1]
提示:
1 <= nums.length <= 10^5
-10^4 <= nums[i] <= 10^4
1 <= k <= nums.length
滑动窗口问题可以用单调队列来解决,从队首到队尾单调递减或递增的队列称之为单调队列。
定义一个单调队列,使用一个队列存储所有还没有被移除的下标
在队列中,这些下标按照从小到大的顺序被存储,并且它们在数组 nums中对应的值是严格单调递减的。
result 数组:存储每个滑动窗口中的最大值。
deque 双端队列:存储元素在数组中的索引。
在循环中,首先判断队列头部的元素是否已经不在当前滑动窗口中,如果是,则将其从队列中删除。
然后判断队列尾部的元素是否小于当前元素,如果是,则将其从队列中删除,因为它不可能成为当前滑动窗口的最大值。
将当前元素的索引加入队列尾部。
如果当前滑动窗口已经形成,则将队列头部的元素加入结果数组中。
最后返回结果数组
/**
* @param {number[]} nums
* @param {number} k
* @return {number[]}
*/
var maxSlidingWindow1 = function(nums, k) {
// 滑动窗口形成及移动的过程中,我们注意到元素是从窗口的右侧进入的,然后由于窗口大小是固定的,因此多余的元素是从窗口左侧移除的。 一端进入,另一端移除,这不就是队列的性质吗?所以,该题目可以借助队列来求解。
// 滑动窗口问题可以用单调队列来解决,那么什么是单调队列呢?
// 从队首到队尾单调递减或递增的队列称之为单调队列。
// 一端既可以有元素入队,又有元素出队的队列,称之为双向队列。
// 如何解决本题呢?具体思路如下:
// 存储窗口下的最大值的数组
const res=[];
// 定义一个单调队列,使用一个队列存储所有还没有被移除的下标
// 在队列中,这些下标按照从小到大的顺序被存储,并且它们在数组 nums中对应的值是严格单调递减的。
const queue=[];
// 循环移动窗口
for(let right = 0; right < nums.length; right++){
//为维持单调性,每次循环移动窗口后,当队列不为空的时候,循环比较当前元素的值num[right] 是否大于等于>= 队尾的元素的值num[queue[queue.length-1]],如果是则将队尾的元素删除出队。
while(queue.length && nums[right] >= nums[queue[queue.length-1]]){
queue.pop();
}
// 1.初始循环未到达窗口长度时
// 2.或者中间过程中经过上一步把队列中小于当前元素值的元素全部出队后
//以上两种情况把当前元素入队,使每个元素参与单调队列的计算中
queue.push(right);
//计算窗口的左边界
let left = right - k + 1;
//当队首元素的下标 < 滑动窗口的左边界时,说明队首的元素已经不在滑动窗口内,我们需要队首元素从头部删除出队。队列单调性不变。
if(queue[0] < left){
queue.shift()
}
//因为数组下标从0开始,只有当右边界 + 1 >= 窗口大小时,窗口才形成,形成后就可以按照题意找出最大值,这时我们将单调队列队首的元素索引放入结果。
if(right + 1 >= k){
res.push(nums[queue[0]])
}
}
return res;
};
/**
* 求解滑动窗口中的最大值
* @param {number[]} nums - 整数数组
* @param {number} k - 滑动窗口大小
* @returns {number[]} - 每个滑动窗口中的最大值组成的数组
*/
function maxSlidingWindow(nums, k) {
const result = [];
const deque = []; // 双端队列,存储元素在数组中的索引
for (let i = 0; i < nums.length; i++) {
// 如果队列头部的元素已经不在当前滑动窗口中,则将其从队列中删除
if (deque.length > 0 && deque[0] < i - k + 1) {
deque.shift();
}
// 如果队列尾部的元素小于当前元素,则将其从队列中删除,因为它不可能成为当前滑动窗口的最大值
while (deque.length > 0 && nums[deque[deque.length - 1]] < nums[i]) {
deque.pop();
}
// 将当前元素的索引加入队列尾部
deque.push(i);
// 如果当前滑动窗口已经形成,则将队列头部的元素加入结果数组中
if (i >= k - 1) {
result.push(nums[deque[0]]);
}
}
return result;
}
编写一个高效的算法来搜索 m x n 矩阵 matrix 中的一个目标值 target 。该矩阵具有以下特性:
每行的元素从左到右升序排列。
每列的元素从上到下升序排列。
示例 1:
输入:
matrix = [[1,4,7,11,15],[2,5,8,12,19],[3,6,9,16,22],[10,13,14,17,24],[18,21,23,26,30]], target = 5
输出:true
示例 2:
输入:
matrix = [[1,4,7,11,15],[2,5,8,12,19],[3,6,9,16,22],[10,13,14,17,24],[18,21,23,26,30]], target = 20
输出:false
提示:
m == matrix.length
n == matrix[i].length
1 <= n, m <= 300
-10^9 <= matrix[i][j]
<= 10^9
每行的所有元素从左到右升序排列
每列的所有元素从上到下升序排列
-10^9 <= target <= 10^9
Z 字形查找
我们可以从矩阵 matrix 的右上角 (0,n−1) 进行搜索。
在每一步的搜索过程中,如果我们位于位置 (x,y),那么我们希望在以 matrix 的左下角为左下角、以 (x,y)为右上角的矩阵中进行搜索,即行的范围为 [x,m−1],列的范围为 [0,y]
matrix[x][y] > target ? --y : ++x;
/**
* @param {number[][]} matrix
* @param {number} target
* @return {boolean}
*/
// Z 字形查找
// 我们可以从矩阵 matrix 的右上角 (0,n−1) 进行搜索。
// 在每一步的搜索过程中,如果我们位于位置 (x,y),那么我们希望在以 matrix 的左下角为左下角、以 (x,y)为右上角的矩阵中进行搜索,即行的范围为 [x,m−1],列的范围为 [0,y]
var searchMatrix = function(matrix, target) {
const m = matrix.length, n = matrix[0].length;
let x = 0, y = n - 1;
// 在搜索的过程中,如果我们超出了矩阵的边界,那么说明矩阵中不存在 target。
while (x < m && y >= 0) {
// 如果 matrix[x,y]=target,说明搜索完成;
if (matrix[x][y] === target) {
return true;
}
// 如果 matrix[x,y] > target,由于每一列的元素都是升序排列的,那么在当前的搜索矩阵中,所有位于第 y 列的元素都是严格大于 target 的,因此我们可以将它们全部忽略,即将 y 减少 1;
// 如果 matrix[x,y] < target,由于每一行的元素都是升序排列的,那么在当前的搜索矩阵中,所有位于第 x 行的元素都是严格小于 target 的,因此我们可以将它们全部忽略,即将 x 增加 1。
if (matrix[x][y] > target) {
--y;
} else {
++x;
}
}
return false;
};
给你一个整数 n ,返回 和为 n 的完全平方数的最少数量。
完全平方数 是一个整数,其值等于另一个整数的平方;
换句话说,其值等于一个整数自乘的积。例如,1、4、9 和 16 都是完全平方数,而 3 和 11 不是。
示例 1:
输入:n = 12
输出:3
解释:12 = 4 + 4 + 4
示例 2:
输入:n = 13
输出:2
解释:13 = 4 + 9
提示:
1 <= n <= 10^4
可以推出
ƒ(n - k * k) + 1 = ƒ(n);
且k * k <= n;
从而缩小了问题规模,求得
ƒ(n - k * k) + 1
,即可求得f(n)
i <= n
i - j * j >= 0
minn = Math.min(minn, f[i - j * j]);
f[0] = 0
f[i] = minn + 1;
/**
* @param {number} n
* @return {number}
*/
var numSquares = function(n) {
//假设最小数量值m = ƒ(n)
//那么n的值满足下列公式 ∑(A[i] * A[i]) = n ,其中(1<= i <= n)
//令 k 为满足最小值 m 的时候的最大的平方数。根据题意,一定可以表示成, d + k * k = n ; d >= 0;
// 注意:一定要是满足m最小的时候的k值,一味的取最大平方数,就是贪心算法了
//得出 f(d) + f(k*k) = f(n);
//显然 f(k*k) = 1; 则 f(d) + 1 = f(n); 因为 d = n - k*k;
//则可以推出ƒ(n - k * k) + 1 = ƒ(n) ; 且 k * k <= n;
// 从而缩小了问题规模,求得ƒ(n - k * k) + 1,即可求得f(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; i - j * j >= 0; j++) {
minn = Math.min(minn, f[i - j * j]);
}
f[i] = minn + 1;
}
return f[n];
};
给定一个数组 nums,编写一个函数将所有 0 移动到数组的末尾,同时保持非零元素的相对顺序。
请注意 ,必须在不复制数组的情况下原地对数组进行操作。
示例 1:
输入: nums = [0,1,0,3,12]
输出: [1,3,12,0,0]
示例 2:
输入: nums = [0]
输出: [0]
提示:
1 <= nums.length <= 10^4
-2^31 <= nums[i] <= 2^31 - 1
进阶:你能尽量减少完成的操作次数吗?
定义收集不是0的数的指针s
遍历两次
第一次,开始收集不是零的数,放置s前面:nums[s++] = nums[i];
第二次,s后面的数置零
/**
* @param {number[]} nums
* @return {void} Do not return anything, modify nums in-place instead.
*/
var moveZeroes = function(nums) {
let s = 0;//定义收集不是0的数的指针
//开始收集不是零的数
for (let i = 0; i < nums.length ;i++) {
if(nums[i]!=0){
nums[s++] = nums[i];
}
}
//收集完毕后,后面自然就都是0了
for (let i = s; i < nums.length; i++) {
nums[i]=0;
}
return nums
};
给定一个包含 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
提示:
1 <= n <= 10^5
nums.length == n + 1
1 <= nums[i] <= n
nums 中 只有一个整数 出现 两次或多次 ,其余整数均只出现一次
进阶:
如何证明 nums 中至少存在一个重复的数字?
你可以设计一个线性级时间复杂度 O(n) 的解决方案吗?
数组结合其索引作为链表,数字重复看转化为有环链表,故可用快慢指针Floyd 判圈算法解决
二分查找:利用二分查找,找出一个数出现2次及以上次时,查找范围内的开始索引到中位数的距离,必然小于该范围内实际所持有数字的数量
/**
* @param {number[]} nums
* @return {number}
*/
// 二分查找:
var findDuplicate1 = function(nums) {
// 这段代码定义了一个名为 findDuplicate 的函数,它接受一个名为 nums 的数组作为输入。该函数使用二分查找算法来查找数组中的重复数字。
const n = nums.length;
// 首先,它获取数组的长度 n,并初始化变量 l 和 r 分别为 1 和 n-1。变量 ans 被初始化为 -1,用于存储最终的答案。
let l = 1, r = n - 1, ans = -1;
// 接下来,代码进入一个 while 循环,当 l <= r 时循环继续。
while (l <= r) {
/** 一个数右移1位得到的数是这个数的一半,是因为在二进制表示下,每一位代表的值是2的幂次方。
例如,第0位(最右边的一位)代表2的0次方,第1位代表2的1次方,第2位代表2的2次方,以此类推。
当一个数右移1位时,它的每一位都向右移动了一位。这意味着原来第n位上的数现在在第n-1位上。
由于每一位代表的值是2的幂次方,所以每一位向右移动一位后,它所代表的值变成了原来的一半。
例如,假设有一个二进制数 1101,它表示为十进制数13。当它右移1位后,变成了 0110,表示为十进制数6。可以看到,13除以2等于6。
因此,一个数右移1位得到的数是这个数的一半。
*/
// 在循环内部,首先计算中间值 mid,然后初始化计数器 cnt 为 0。
let mid = (l + r) >> 1;
let cnt = 0;
// 接下来,代码进入一个 for 循环,遍历整个数组。
for (let i = 0; i < n; ++i) {
// 如果当前元素小于等于 mid,则计数器 cnt 加 1。
cnt += (nums[i] <= mid);
}
// 在 for 循环结束后,如果 cnt <= mid,则将左边界更新为 mid + 1;
if (cnt <= mid) {
l = mid + 1;
// 否则将右边界更新为 mid - 1 并将答案更新为 mid。
// 利用二分查找,找出一个数出现2次及以上次时,查找范围内的开始索引到中位数的距离,必然小于该范围内实际所持有数字的数量
} else {
r = mid - 1;
ans = mid;
}
}
// 最后,在循环结束后返回答案 ans。
return ans;
};
// 快慢指针:「Floyd 判圈算法」(又称龟兔赛跑算法)有所了解,它是一个检测链表是否有环的算法
// 根据「Floyd 判圈算法」两个指针在有环的情况下一定会相遇,此时我们再将 slow 放置起点 0,两个指针每次同时移动一步,相遇的点就是答案。
// 这种算法之所以有效,是因为如果链表中存在环,那么快指针一定会在某个时刻进入环中,并且在环中不断追赶慢指针。
// 由于快指针每次移动两个节点,而慢指针每次移动一个节点,所以快指针每次都会比慢指针多走一步,最终追上慢指针。
var findDuplicate = function(nums) {
// 我们对 nums 数组建图,每个位置 i 连一条 i→nums[i] 的边。
// 由于存在的重复的数字 target,因此 target 这个位置一定有起码两条指向它的边,因此整张图一定存在环,且我们要找到的 target 就是这个环的入口。
// 这段代码定义了一个名为 findDuplicate 的函数,它接受一个名为 nums 的数组作为输入。该函数使用快慢指针算法来查找数组中的重复数字。
// 首先,它初始化两个指针 slow 和 fast 都为 0。
let slow = 0, fast = 0;
// 接下来,代码进入一个 do-while 循环。
do {
// 在循环内部,slow 指针每次移动一步,而 fast 指针每次移动两步。
slow = nums[slow];
fast = nums[nums[fast]];
// 循环继续直到两个指针相遇。 此时,此处就是环的入口。
} while (slow != fast);
/**
当快指针追上慢指针时,我们可以确定链表中存在环,但是这个位置并不一定是环的入口。为了找到环的入口,我们需要进行以下步骤:
首先,保持快指针不动,将慢指针移回链表的头部。
然后,同时移动快指针和慢指针,每次都只移动一个节点。
当快指针和慢指针再次相遇时,它们相遇的位置就是环的入口。
这种方法的原理是基于数学推导。假设链表的头部到环的入口的距离为 a,环的入口到快慢指针第一次相遇的位置的距离为 b,快慢指针第一次相遇的位置到环的入口的距离为 c,那么根据快指针每次移动两个节点,慢指针每次移动一个节点,可以得到以下等式:
2(a + b) = a + b + n(b + c)
其中 n 表示快指针在第一次相遇时已经绕环走了几圈。将等式化简可得:
a = (n - 1)(b + c) + c
这意味着从链表头部到环的入口的距离等于从快慢指针第一次相遇的位置到环的入口的距离。因此,我们可以通过上述步骤来找到环的入口。
*/
// 接下来,将 slow 指针重置为 0。而fast仍然处于入口处。
// 由上面的原理可以得到再次相遇时slow的值即为环的入口位置。
//
slow = 0;
// 然后进入另一个 while 循环。
while (slow != fast) {
// 在这个循环内部,两个指针都每次移动一步,直到它们再次相遇。
slow = nums[slow];
fast = nums[fast];
}
// 最后,在循环结束后返回 slow 指针的值。
return slow;
};
给你一个整数数组 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
提示:
1 <= nums.length <= 2500
-10^4 <= nums[i] <= 10^4
进阶:
你能将算法的时间复杂度降低到 O(n log(n)) 吗?
动态规划
dp[i] 表示以 nums[i] 这个数结尾的最长递增子序列的长度。
dp[i] 初始值为 1,因为以 nums[i] 结尾的最长递增子序列起码要包含它自己。
j < i,只要找到前面那些结尾比 i 小的子序列,然后把 i 接到这些子序列末尾,就可以形成一个新的递增子序列
nums[i] > nums[j],dp[i] = Math.max(dp[i], dp[j] + 1)
最后,循环整个数组,找到其中以某个元素结尾,递增子序列的长度最大是多少
/**
* @param {number[]} nums
* @return {number}
*/
var lengthOfLIS1 = function(nums) {
// 动态规划的难点本来就在于寻找正确的状态转移方程,本文就借助经典的「最长递增子序列问题」来讲一讲设计动态规划的通用技巧:数学归纳思想。
// 最长递增子序列(Longest Increasing Subsequence,简写 LIS)是非常经典的一个算法问题,比较容易想到的是动态规划解法,时间复杂度 O(N^2)
// 我们借这个问题来由浅入深讲解如何找状态转移方程,如何写出动态规划解法。
// 比较难想到的是利用二分查找,时间复杂度是 O(NlogN),我们通过一种简单的纸牌游戏来辅助理解这种巧妙的解法。
// dp[i] 表示以 nums[i] 这个数结尾的最长递增子序列的长度。
// 定义: dp[i] 表示以 nums[i] 这个数结尾的最长递增子序列的长度
// 我们就可以推出 base case:dp[i] 初始值为 1,因为以 nums[i] 结尾的最长递增子序列起码要包含它自己。
let dp = new Array(nums.length).fill(1);
// base case: dp 数组全都初始化为 1
for (let i = 0; i < nums.length; i++) {
for (let j = 0; j < i; j++) {
// 既然是递增子序列,我们只要找到前面那些结尾比 i 小的子序列,然后把 i 接到这些子序列末尾,就可以形成一个新的递增子序列,而且这个新的子序列长度加一。
// 并且通过循环找出比其他位置比d[i]小的位置+1后的最大值更新到d[i]上,从而比较得出d[i]的值。例如 [4,5,1,2,3,6],当i 为末尾那个元素时,循环遍历[4,5,1,2,3],比较[4,5,6]和[1,2,3,6]哪个大。
if (nums[i] > nums[j]) {
dp[i] = Math.max(dp[i], dp[j] + 1)
}
}
}
// 最后,循环整个数组,找到其中以某个元素结尾,递增子序列的长度最大是多少
let res = 0;
for (let i = 0; i < dp.length; i++) {
res = Math.max(res, dp[i]);
}
return res;
};
var lengthOfLIS = function(nums) {
var top = new Array(nums.length).fill(0);
// 牌堆数初始化为 0
var piles = 0;
for (var i = 0; i < nums.length; i++) {
// 要处理的扑克牌
var poker = nums[i];
/***** 搜索左侧边界的二分查找 *****/
var left = 0, right = piles;
while (left < right) {
var mid = Math.floor((left + right) / 2);
if (top[mid] > poker) {
right = mid;
} else if (top[mid] < poker) {
left = mid + 1;
} else {
right = mid;
}
}
/*********************************/
// 没找到合适的牌堆,新建一堆
if (left == piles) piles++;
// 把这张牌放到牌堆顶
top[left] = poker;
}
// 牌堆数就是 LIS 长度
return piles;
};
// 求最大递增子序列(更准确的说,这里求的是最大非下降子序列)
function LIS (array) {
// dp[i]表示以array[i]为结束的非下降子序列子序列的长度,一开始它们的值都是1
var dp = [1];
// 记录最大的dp数组的最大值max及其下标
var max = 1, k = 0;
for(var i = 1; i < array.length; i++) {
dp[i] = 1;
for(var j = 0; j < i; j++) {
// 不下降,就是大于或者等于
if (array[i] >= array[j]) {
if(dp[i] < dp[j] + 1) {
dp[i] = dp[j] + 1;
}
}
}
if(dp[i] > max) { // 更大的是不下降子序列的长度计算的情况
max = dp[i]; // 记得将max存储在变量中以供以后使用
k = i; // 记得将该值存储到变量中以供以后使用
}
}
// 初始时把最长递增子序列的最大值放到结果数组中
var ret = [array[k]]
var m = max; // 初始时,m为最长递增子序列的最大长度
var i = k - 1; // 从最后一个位置往前找
// 只要还没找完最长递增子序列,就继续找
while(m > 1) {
// 相邻的dp[i]都是相等或者相差1
// 非下降子序列子序列的长度相差1,并且前一个元素确实小于等于后一个元素
if(dp[i] === (m - 1) && array[i] <= array[k]){
ret.unshift(array[i]);
k = i;
m--;
}
i--;
}
return ret;
}
给你一个整数数组 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
提示:
1 <= coins.length <= 12
1 <= coins[i] <= 2^31 - 1
0 <= amount <= 10^4
动态规划问题的一般形式就是求最值
当目标金额为 i 时,至少需要 dp[i] 枚硬币凑出。
数组大小为amount + 1,初始值也是amount+1
dp[0] = 0;
var coin of coins,dp[i] = Math.min(dp[i], 1 + dp[i - coin]);
(dp[amount] == amount + 1) ? -1 : dp[amount];
/**
* @param {number[]} coins
* @param {number} amount
* @return {number}
*/
// 首先,动态规划问题的一般形式就是求最值。
// 既然是要求最值,核心问题是什么呢?求解动态规划的核心问题是穷举。因为要求最值,肯定要把所有可行的答案穷举出来,然后在其中找最值呗。
// 动态规划的穷举有点特别,因为这类问题存在「重叠子问题」,如果暴力穷举的话效率会极其低下,所以需要「备忘录」或者「DP table」来优化穷举过程,避免不必要的计算。
// 动态规划问题一定会具备「最优子结构」,才能通过子问题的最值得到原问题的最值。只有列出正确的「状态转移方程」才能正确地穷举
// 辅助你思考状态转移方程:1.明确 base case -> 2.明确「状态」-> 3.明确「选择」 -> 4.定义 dp 数组/函数的含义
/*
# 初始化 base case
dp[0][0][...] = base
# 进行状态转移
for 状态1 in 状态1的所有取值:
for 状态2 in 状态2的所有取值:
for ...
dp[状态1][状态2][...] = 求最值(选择1,选择2...)
*/
/*
耗时的原因是重复计算,那么我们可以造一个「备忘录」,每次算出某个子问题的答案后别急着返回,先记到「备忘录」里再返回;每次遇到一个子问题先去「备忘录」里查一查,如果发现之前已经解决过这个问题了,直接把答案拿出来用,不要再耗时去计算了。
一般使用一个数组充当这个「备忘录」,当然你也可以使用哈希表(字典),思想都是一样的。
带备忘录的递归解法的效率已经和迭代的动态规划解法一样了。实际上,这种解法和迭代的动态规划已经差不多了,只不过这种方法叫做「自顶向下」,动态规划叫做「自底向上」。
啥叫「自顶向下」?注意我们刚才画的递归树(或者说图),是从上向下延伸,都是从一个规模较大的原问题比如说 f(20),向下逐渐分解规模,直到 f(1) 和 f(2) 这两个 base case,然后逐层返回答案,这就叫「自顶向下」。
啥叫「自底向上」?反过来,我们直接从最底下,最简单,问题规模最小的 f(1) 和 f(2) 开始往上推,直到推到我们想要的答案 f(20),这就是动态规划的思路,这也是为什么动态规划一般都脱离了递归,而是由循环迭代完成计算。
把这个「备忘录」独立出来成为一张表,就叫做 DP table 吧,在这张表上完成「自底向上」的推算.这个 DP table 特别像之前那个「剪枝」后的结果,只是反过来算而已。实际上,带备忘录的递归解法中的「备忘录」,最终完成后就是这个 DP table,所以说这两种解法其实是差不多的,大部分情况下,效率也基本相同。
*/
/*
「状态转移方程」这个名词,实际上就是描述问题结构的数学形式
把 f(n) 想做一个状态 n,这个状态 n 是由状态 n - 1 和状态 n - 2 相加转移而来,这就叫状态转移
状态转移方程直接代表着暴力解法。
千万不要看不起暴力解,动态规划问题最困难的就是写出这个暴力解,即状态转移方程。只要写出暴力解,优化方法无非是用备忘录或者 DP table,再无奥妙可言。
根据斐波那契数列的状态转移方程,当前状态只和之前的两个状态有关,其实并不需要那么长的一个 DP table 来存储所有的状态,只要想办法存储之前的两个状态就行了。所以,可以进一步优化,把空间复杂度降为 O(1)
这个技巧就是所谓的「状态压缩」,如果我们发现每次状态转移只需要 DP table 中的一部分,那么可以尝试用状态压缩来缩小 DP table 的大小,只记录必要的数据就相当于把DP table 的大小从 n 缩小到 2。动态规划中我们还会看到这样的例子,一般来说是把一个二维的 DP table 压缩成一维,即把空间复杂度从 O(n^2) 压缩到 O(n)。
*/
/**
这个问题是动态规划问题,因为它具有「最优子结构」的。要符合「最优子结构」,子问题间必须互相独立。
设你有面值为 1, 2, 5 的硬币,你想求 amount = 11 时的最少硬币数(原问题),如果你知道凑出 amount = 10, 9, 6 的最少硬币数(子问题),你只需要把子问题的答案加一(再选一枚面值为 1, 2, 5 的硬币),求个最小值,就是原问题的答案。因为硬币的数量是没有限制的,所以子问题之间没有相互制,是互相独立的。
既然知道了这是个动态规划问题,就要思考如何列出正确的状态转移方程?
1、确定 base case,这个很简单,显然目标金额 amount 为 0 时算法返回 0,因为不需要任何硬币就已经凑出目标金额了。
2、确定「状态」,也就是原问题和子问题中会变化的变量。由于硬币数量无限,硬币的面额也是题目给定的,只有目标金额会不断地向 base case 靠近,所以唯一的「状态」就是目标金额 amount。
3、确定「选择」,也就是导致「状态」产生变化的行为。目标金额为什么变化呢,因为你在选择硬币,你每选择一枚硬币,就相当于减少了目标金额。所以说所有硬币的面值,就是你的「选择」。
4、明确 dp 函数/数组的定义。我们这里讲的是自顶向下的解法,所以会有一个递归的 dp 函数,一般来说函数的参数就是状态转移中会变化的量,也就是上面说到的「状态」;函数的返回值就是题目要求我们计算的量。就本题来说,状态只有一个,即「目标金额」,题目要求我们计算凑出目标金额所需的最少硬币数量。
所以我们可以这样定义 dp 函数:dp(n) 表示,输入一个目标金额 n,返回凑出目标金额 n 所需的最少硬币数量。
解法的伪码:
var coinChange = function(coins, amount) {
return dp(coins, amount);
};
var dp = function(coins, n) {
let res = Infinity;
for (let coin of coins) {
res = Math.min(res, 1 + subproblem); // 选择硬币数最少的结果
}
}
return res;
};
加上 base case 即可得到最终的答案。显然目标金额为 0 时,所需硬币数量为 0;当目标金额小于 0 时,无解,返回 -1:
*/
// 递归解法
var coinChange1 = function(coins, amount) {
return dp(coins, amount);
// 定义:要凑出金额 n,至少要 dp(coins, n) 个硬币
function dp(coins, amount) {
// base case
if (amount == 0) return 0;
if (amount < 0) return -1;
let res = Infinity;
for (let coin of coins) {
// 计算子问题的结果
let subProblem = dp(coins, amount - coin);
// 子问题无解则跳过
if (subProblem == -1) continue;
// 在子问题中选择最优解,然后加一
res = Math.min(res, subProblem + 1);
}
return res == Infinity ? -1 : res;
}
};
// 备忘录
var coinChange2 = function(coins, amount) {
const memo = new Array(amount + 1).fill(-666);
/**
* Initialize memo to a special value that will not be computed.
* Represents that it has not been calculated yet.
**/
const dp = (coins, amount) => {
if (amount == 0) return 0;
if (amount < 0) return -1;
// Check memo to prevent redundant calculation
if (memo[amount] != -666) return memo[amount];
let res = Infinity;
for (let coin of coins) {
let subProblem = dp(coins, amount - coin);
// Skip if subproblem has no solution
if (subProblem == -1) continue;
res = Math.min(res, subProblem + 1);
}
// Store the calculation result in memo
memo[amount] = (res == Infinity) ? -1 : res;
return memo[amount];
}
return dp(coins, amount);
};
// dp table
// 自底向上使用 dp table 来消除重叠子问题,关于「状态」「选择」和 base case 与之前没有区别,dp 数组的定义和刚才 dp 函数类似,也是把「状态」,也就是目标金额作为变量。不过 dp 函数体现在函数参数,而 dp 数组体现在数组索引
// dp 数组的定义:当目标金额为 i 时,至少需要 dp[i] 枚硬币凑出。
// 注意:javascript 代码由 chatGPT\U0001f916 根据我的 java 代码翻译,旨在帮助不同背景的读者理解算法逻辑。
// 本代码还未经过力扣测试,仅供参考,如有疑惑,可以参照我写的 java 代码对比查看。
/**
* @param {number[]} coins
* @param {number} amount
* @return {number}
*/
var coinChange = function(coins, amount) {
var dp = new Array(amount + 1).fill(amount + 1);
// The size of the array is amount + 1, and the initial value is also amount + 1
// dp[i] represents the minimum number of coins needed for the amount i
dp[0] = 0;
// The outer loop is traversing all the values of all states
for (var i = 0; i < dp.length; i++) {
// The inner loop is to find the minimum value of all choices
for (var coin of coins) {
// Sub-problems are unsolvable, skip
if (i - coin < 0) {
continue;
}
dp[i] = Math.min(dp[i], 1 + dp[i - coin]);
}
}
return (dp[amount] == amount + 1) ? -1 : dp[amount];
};
给你一个整数数组 nums 和一个整数 k ,请你返回其中出现频率前 k 高的元素。你可以按 任意顺序 返回答案。
示例 1:
输入: nums = [1,1,1,2,2,3], k = 2
输出: [1,2]
示例 2:
输入: nums = [1], k = 1
输出: [1]
提示:
1 <= nums.length <= 10^5
k 的取值范围是 [1, 数组中不相同的元素的个数]
题目数据保证答案唯一,换句话说,数组中前 k 个高频元素的集合是唯一的
进阶:你所设计算法的时间复杂度 必须 优于 O(n log n) ,其中 n 是数组大小。
遍历一次,并用Set记录每个数字出现的频率
维护一个元素数目为 k 的最小堆
每次都将新的元素与堆顶元素(堆中频率最小的元素)进行比较
如果新的元素的频率比堆顶端的元素大,则弹出堆顶端的元素,将新的元素添加进堆中
最终,堆中的 k 个元素即为前 kkk 个高频元素
/**
* @param {number[]} nums
* @param {number} k
* @return {number[]}
*/
var topKFrequent = function(nums, k) {
var freq = new Map()
// 记录每个数字出现的频率
nums.forEach((v) => {
if (!freq.has(v)) {
freq.set(v, 1)
} else {
freq.set(v, freq.get(v) + 1)
}
})
//构建一个只有k个元素的小顶堆结构heap
var heap = [],
len = 0
freq.forEach((value, key)=>{
if (len < k) {
heap.push(key)
if (len === k - 1) buildHeap(heap, freq, k) //初试化heap为堆结构
} else{
if (freq.get(heap[0]) < value) {
heap[0] = key //小顶堆的顶部为最小的,如果有比它更大的就更改
heapify(heap, freq, k, 0) //更改堆顶元素破坏了堆结构,执行heapify再次将数组转换为堆
}
}
len++
})
return heap
};
// 构造堆
let buildHeap = (heap, map, k) => {
for (let i = Math.floor(k / 2); i >= 0; i--) {
heapify(heap, map, k, i)
}
}
// 将数组变为堆结构
let heapify = (heap, freq, k, i) => {
var left = 2 * i + 1, //i的左子节点
right = 2 * i + 2, //i的右子节点
min = i
if (freq.get(heap[left]) < freq.get(heap[min]) && left < k) {
min = left
}
if (freq.get(heap[right]) < freq.get(heap[min]) && right < k) {
min = right
}
if (min !== i) {
;[heap[i], heap[min]] = [heap[min], heap[i]]
heapify(heap, freq, k, min)
}
}
给定一个经过编码的字符串,返回它解码后的字符串。
编码规则为: 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"
提示:
1 <= s.length <= 30
s 由小写英文字母、数字和方括号 '[]' 组成
s 保证是一个 有效 的输入。
s 中所有整数的取值范围为 [1, 300]
根据题意处理
定义存倍数的栈、存 待拼接的str 的栈、倍数的临时变量、字符串的临时变量
遍历一次字符串,分为遇到数字、‘[’、‘]’、字符等四种类型处理
遇到数字,对倍数的临时变量进位叠加处理
遇到‘[’,对之前的字符串入栈处理;对数字入栈处理;字符串、数字初始化;
遇到‘]’,对数字出栈处理;对基础字符串结合数字进行拷贝次数操作;
遇到字符,给字符串的临时变量追加字符处理
/**
* @param {string} s
* @return {string}
*/
const decodeString = (s) => {
let numStack = []; // 存倍数的栈
let strStack = []; // 存 待拼接的str 的栈
let num = 0; // 倍数的“搬运工”
let result = ''; // 字符串的“搬运工”
for (const char of s) { // 逐字符扫描
if (!isNaN(char)) { // 遇到数字
num = num * 10 + Number(char); // 算出倍数
} else if (char == '[') { // 遇到 [
strStack.push(result); // result串入栈
result = ''; // 入栈后清零
numStack.push(num); // 倍数num进入栈等待
num = 0; // 入栈后清零
} else if (char == ']') { // 遇到 ],两个栈的栈顶出栈
let repeatTimes = numStack.pop(); // 获取拷贝次数
result = strStack.pop() + result.repeat(repeatTimes); // 构建子串
} else {
result += char; // 遇到字母,追加给result串
}
}
return result;
};
给你一个 只包含正整数 的 非空 数组 nums 。请你判断是否可以将这个数组分割成两个子集,使得两个子集的元素和相等。
示例 1:
输入:nums = [1,5,11,5]
输出:true
解释:数组可以分割成 [1, 5, 5] 和 [11] 。
示例 2:
输入:nums = [1,2,3,5]
输出:false
解释:数组不能分割成两个元素和相等的子集。
提示:
1 <= nums.length <= 200
1 <= nums[i] <= 100
可转换为0-1 背包问题
给你一个可装载重量为 W 的背包和 N 个物品,每个物品有重量和价值两个属性。其中第 i 个物品的重量为 wt[i],价值为 val[i],现在让你用这个背包装物品,最多能装的价值是多少?
我们可以先对集合求和,得出 sum,把问题转化为 0 - 1 背包问题:
给一个可装载重量为 sum / 2 的背包和 N 个物品,每个物品的重量为 nums[i]。现在让你装物品,是否存在一种装法,能够恰好将背包装满。
状态就是「背包的重量」和「可选择的物品」,选择就是「装进背包」或者「不装进背包」。
dp[i][j] = x
表示,对于前 i 个物品(i 从 1 开始计数),当前背包的容量为 j 时,若 x 为 true,则说明可以恰好将背包装满,若 x 为 false,则说明不能恰好将背包装满。
如果不把 nums[i] 算入子集,或者说你不把这第 i 个物品装入背包,那么是否能够恰好装满背包,取决于上一个状态
dp[i-1][j]
,继承之前的结果。如果把 nums[i] 算入子集,或者说你把这第 i 个物品装入了背包,那么是否能够恰好装满背包,取决于状态
dp[i-1][j-nums[i-1]]
。
dp[i - 1][j-nums[i-1]]
也很好理解:你如果装了第 i 个物品,就要看背包的剩余重量 j - nums[i-1] 限制下是否能够被恰好装满。换句话说,如果 j - nums[i-1] 的重量可以被恰好装满,那么只要把第 i 个物品装进去,也可恰好装满 j 的重量;否则的话,重量 j 肯定是装不满的。
dp 数组:动态规划数组,dp[i] 表示是否可以从数组中选取若干个元素,使得它们的和等于 i。 当我们在遍历到第 i 个元素时,dp[j] 表示是否可以从前 i 个元素中选取若干个元素,使得它们的和等于 j。如果当前元素 nums[i] 可以选取,则有两种情况:
1.不选取当前元素,此时 dp[j] 的值不变,即 dp[j] 仍然表示前 i 个元素中是否可以选取若干个元素,使得它们的和等于 j。
2.选取当前元素,此时 dp[j] 的值应该为 dp[j - nums[i]],表示前 i-1 个元素中是否可以选取若干个元素,使得它们的和等于 j - nums[i],然后再加上当前元素 nums[i] 的值,即可得到前 i 个元素中是否可以选取若干个元素,使得它们的和等于 j。
因此,状态转移方程为 dp[j] = dp[j] || dp[j -nums[i]],表示选取当前元素和不选取当前元素两种情况中,只要有一种情况满足条件,即可更新 dp[j] 的值。
最终,dp[target]的值表示前 n 个元素中是否可以选取若干个元素,使得它们的和等于目标和 target。
/**
* @param {number[]} nums
* @return {boolean}
*/
var canPartition1 = function(nums) {
// 0-1 背包问题:给你一个可装载重量为 W 的背包和 N 个物品,每个物品有重量和价值两个属性。其中第 i 个物品的重量为 wt[i],价值为 val[i],现在让你用这个背包装物品,最多能装的价值是多少?
/**
for 状态1 in 状态1的所有取值:
for 状态2 in 状态2的所有取值:
for ...
dp[状态1][状态2][...] = 择优(选择1,选择2...)
*/
// 对于这个问题,看起来和背包没有任何关系,为什么说它是背包问题呢?
// 给你一个可装载重量为 W 的背包和 N 个物品,每个物品有重量和价值两个属性。其中第 i 个物品的重量为 wt[i],价值为 val[i],现在让你用这个背包装物品,最多能装的价值是多少?
// 那么对于这个问题,我们可以先对集合求和,得出 sum,把问题转化为 0 - 1 背包问题:
// 给一个可装载重量为 sum / 2 的背包和 N 个物品,每个物品的重量为 nums[i]。现在让你装物品,是否存在一种装法,能够恰好将背包装满。
// 第一步要明确两点,「状态」和「选择」。
// 这个前文 经典动态规划:背包问题 已经详细解释过了,状态就是「背包的重量」和「可选择的物品」,选择就是「装进背包」或者「不装进背包」。
// 第二步要明确 dp 数组的定义。
// dp[i][j] = x 表示,对于前 i 个物品(i 从 1 开始计数),当前背包的容量为 j 时,若 x 为 true,则说明可以恰好将背包装满,若 x 为 false,则说明不能恰好将背包装满。
// 比如说,如果 dp[4][9] = true,其含义为:对于容量为 9 的背包,若只是在前 4 个物品中进行选择,可以有一种方法把背包恰好装满。或者说是对于给定的集合中,若只在前 4 个数字中进行选择,存在一个子集的和可以恰好凑出 9。
// 根据这个定义,我们想求的最终答案就是 dp[N][sum/2],base case 就是 dp[..][0] = true 和 dp[0][..] = false
// 因为背包没有空间的时候,就相当于装满了,而当没有物品可选择的时候,肯定没办法装满背包。
// 第三步,根据「选择」,思考状态转移的逻辑。可以根据「选择」对 dp[i][j] 得到以下状态转移:
// 1.如果不把 nums[i] 算入子集,或者说你不把这第 i 个物品装入背包,那么是否能够恰好装满背包,取决于上一个状态 dp[i-1][j],继承之前的结果。
// 2.如果把 nums[i] 算入子集,或者说你把这第 i 个物品装入了背包,那么是否能够恰好装满背包,取决于状态 dp[i-1][j-nums[i-1]]。
// dp[i - 1][j-nums[i-1]] 也很好理解:你如果装了第 i 个物品,就要看背包的剩余重量 j - nums[i-1] 限制下是否能够被恰好装满。
// 换句话说,如果 j - nums[i-1] 的重量可以被恰好装满,那么只要把第 i 个物品装进去,也可恰好装满 j 的重量;否则的话,重量 j 肯定是装不满的。
// 计算数组和
var sum = 0;
for (var num of nums) sum += num;
// 和为奇数时,不可能划分成两个和相等的集合,因为题目说元素只包含正整数
if (sum % 2 !== 0) return false;
var n = nums.length;
// 得到和为一半的值
sum = sum / 2;
// 创建二维数组
var dp = new Array(n + 1).fill(false).map(() => new Array(sum + 1).fill(false));
// base case:因为背包没有空间的时候,就相当于装满了
for (var i = 0; i <= n; i++){
dp[i][0] = true;
}
// 遍历背包元素
for (var i = 1; i <= n; i++) {
// 元素的重量(容量)
for (var j = 1; j <= sum; j++) {
if (j - nums[i - 1] < 0) {
// 背包容量不足,不能装入第 i 个物品
// 不把这第 i 个物品装入背包,那么是否能够恰好装满背包,取决于上一个状态 dp[i-1][j],继承之前的结果。
dp[i][j] = dp[i - 1][j];
} else {
// 装入或不装入背包:
// 装入背包:dp[i - 1][j-nums[i-1]] 也很好理解:你如果装了第 i 个物品,就要看背包的剩余重量 j - nums[i-1] 限制下是否能够被恰好装满。
// 如果不装入背包:不把这第 i 个物品装入背包,那么是否能够恰好装满背包,取决于上一个状态 dp[i-1][j],继承之前的结果。
dp[i][j] = dp[i - 1][j] || dp[i - 1][j - nums[i - 1]];
}
}
}
return dp[n][sum];
};
/**
* 判断是否可以将数组分割成两个子集,使得两个子集的元素和相等
* @param {number[]} nums - 非空正整数数组
* @returns {boolean} - 是否可以分割成两个子集
*/
function canPartition(nums) {
const sum = nums.reduce((a, b) => a + b); // 计算数组元素的和
if (sum % 2 !== 0) {
return false; // 如果数组元素的和为奇数,则无法分割成两个子集
}
const target = sum / 2; // 目标和
const dp = new Array(target + 1).fill(false); // 动态规划数组,dp[i] 表示是否可以从数组中选取若干个元素,使得它们的和等于 i
dp[0] = true; // 初始化,当目标和为 0 时,可以选取空集
// 当我们在遍历到第 i 个元素时,dp[j] 表示是否可以从前 i 个元素中选取若干个元素,使得它们的和等于 j。如果当前元素 nums[i] 可以选取,则有两种情况:
// 不选取当前元素,此时 dp[j] 的值不变,即 dp[j] 仍然表示前 i 个元素中是否可以选取若干个元素,使得它们的和等于 j。
// 选取当前元素,此时 dp[j] 的值应该为 dp[j - nums[i]],表示前 i-1 个元素中是否可以选取若干个元素,使得它们的和等于 j - nums[i],然后再加上当前元素 nums[i] 的值,即可得到前 i 个元素中是否可以选取若干个元素,使得它们的和等于 j。
// 因此,状态转移方程为 dp[j] = dp[j] || dp[j - nums[i]],表示选取当前元素和不选取当前元素两种情况中,只要有一种情况满足条件,即可更新 dp[j] 的值。最终,dp[target] 的值表示前 n 个元素中是否可以选取若干个元素,使得它们的和等于目标和 target。
for (let i = 0; i < nums.length; i++) {
for (let j = target; j >= nums[i]; j--) {
dp[j] = dp[j] || dp[j - nums[i]]; // 状态转移方程
}
}
return dp[target];
}
给定一个二叉树的根节点 root ,和一个整数 targetSum ,求该二叉树里节点值之和等于 targetSum 的 路径 的数目。
路径 不需要从根节点开始,也不需要在叶子节点结束,但是路径方向必须是向下的(只能从父节点到子节点)。
示例 1:
输入:root = [10,5,-3,3,2,null,11,3,-2,null,1], targetSum = 8
输出:3
解释:和等于 8 的路径有 3 条,如图所示。
示例 2:
输入:root = [5,4,8,11,null,13,4,7,2,null,null,5,1], targetSum = 22
输出:3
提示:
二叉树的节点个数的范围是 [0,1000]
-10^9 <= Node.val <= 10^9
-1000 <= targetSum <= 1000
pathSum 函数:求二叉树中节点值之和等于目标和的路径数目,接收一个二叉树的根节点和一个目标和作为参数,返回路径数目。
如果根节点为空,则路径数目为 0。
count 变量:路径数目。
dfs 函数:深度优先遍历二叉树,接收一个节点和一个目标和作为参数。
如果节点为空,则返回。
将当前节点的值从目标和中减去。
如果目标和为 0,则说明找到了一条路径,将路径数目加 1。
递归遍历左子树。
递归遍历右子树。
从根节点开始遍历。
递归遍历左子树。
递归遍历右子树。
返回路径数目。
/**
* Definition for a binary tree node.
* function TreeNode(val, left, right) {
* this.val = (val===undefined ? 0 : val)
* this.left = (left===undefined ? null : left)
* this.right = (right===undefined ? null : right)
* }
*/
/**
* @param {TreeNode} root
* @param {number} targetSum
* @return {number}
*/
// 题目要求的是找出路径和等于给定数值的路径总数, 而: 两节点间的路径和 = 两节点的前缀和之差
// 我们只用遍历整颗树一次,记录每个节点的前缀和,并查询该节点的祖先节点中符合条件的个数,将这个数量加到最终结果上。
var pathSum1 = function(root, targetSum) {
// HashMap的key是前缀和, value是该前缀和的节点数量,记录数量是因为有出现复数路径的可能。
let map=new Map()
let ans=0
dfs(root,0)
return ans
/**
*
* @param {*} root
* @param {*} presum 前缀和
* @returns
*/
function dfs(root,presum){
if(!root){
return 0
}
map.set(presum, (map.get(presum)||0) + 1)
let target = presum + root.val
// 结果
ans += (map.get(target-targetSum)||0)
// 继续找
dfs(root.left, target)
dfs(root.right, target)
// 路径方向必须是向下的(只能从父节点到子节点)
// 当我们讨论两个节点的前缀和差值时,有一个前提:一个节点必须是另一个节点的祖先节点,换句话说,当我们把一个节点的前缀和信息更新到map里时,它应当只对其子节点们有效。
// 如果我们不做状态恢复,当遍历右子树时,左子树中的信息仍会保留在map中,从而产生错误。
// 状态恢复代码的作用就是: 在遍历完一个节点的所有子节点后,将其从map中除去。
// 回溯 撤销
map.set(presum, map.get(presum) - 1)
}
};
/**
* 求二叉树中节点值之和等于 targetSum 的路径数目
* @param {TreeNode} root - 二叉树的根节点
* @param {number} targetSum - 目标和
* @returns {number} - 路径数目
*/
function pathSum(root, targetSum) {
if (!root) {
return 0; // 如果根节点为空,则路径数目为 0
}
let count = 0; // 路径数目
const dfs = (node, sum) => {
if (!node) {
return; // 如果节点为空,则返回
}
sum -= node.val; // 将当前节点的值从目标和中减去
if (sum === 0) {
count++; // 如果目标和为 0,则说明找到了一条路径
}
dfs(node.left, sum); // 递归遍历左子树
dfs(node.right, sum); // 递归遍历右子树
};
dfs(root, targetSum); // 从根节点开始遍历
count += pathSum(root.left, targetSum); // 递归遍历左子树
count += pathSum(root.right, targetSum); // 递归遍历右子树
return count;
}
给定两个字符串 s 和 p,找到 s 中所有 p 的 异位词 的子串,返回这些子串的起始索引。不考虑答案输出的顺序。
异位词 指由相同字母重排列形成的字符串(包括相同的字符串)。
示例 1:
输入: s = "cbaebabacd", p = "abc"
输出: [0,6]
解释:
起始索引等于 0 的子串是 "cba", 它是 "abc" 的异位词。
起始索引等于 6 的子串是 "bac", 它是 "abc" 的异位词。
示例 2:
输入: s = "abab", p = "ab"
输出: [0,1,2]
解释:
起始索引等于 0 的子串是 "ab", 它是 "ab" 的异位词。
起始索引等于 1 的子串是 "ba", 它是 "ab" 的异位词。
起始索引等于 2 的子串是 "ab", 它是 "ab" 的异位词。
提示:
1 <= s.length, p.length <= 3 * 10^4
s 和 p 仅包含小写字母
滑动窗口:
findAnagrams 函数:找到字符串 s 中所有 p 的异位词的子串的起始索引,接收两个字符串作为参数,返回所有符合条件的子串的起始索引。
result 数组:存储符合条件的子串的起始索引。
map Map:存储字符串 p 中每个字符出现的次数。
遍历字符串 p,将每个字符出现的次数存储到 map 中。
left 变量:滑动窗口左边界。
right 变量:滑动窗口右边界。
count 变量:计数器,表示还需要匹配的字符数,初始值为 p 的长度。
在循环中,先将右边界向右移动,如果当前字符在 map 中,则将其从 map 中删除,并更新计数器的值。
如果计数器的值为 0,则说明找到了符合条件的子串,将左边界加入结果数组中。
然后将左边界向右移动,如果左边界字符在 map 中,则将其重新加入 map 中,并更新计数器的值。
最后返回结果数组。
/**
* @param {string} s
* @param {string} p
* @return {number[]}
*/
// 解析:https://labuladong.github.io/algo/di-yi-zhan-da78c/shou-ba-sh-48c1d/wo-xie-le--f7a92/
var findAnagrams1 = function(s, t) {
// 定义 need,window 计数 Map
var need = new Map();
var window = new Map();
// 统计 t 中出现的元素以及它们的个数
for (var i = 0; i < t.length; i++) {
var c = t[i];
if (need.has(c)) {
need.set(c, need.get(c) + 1);
} else {
need.set(c, 1);
}
}
var left = 0, right = 0;
var valid = 0;
var res = []; // 记录结果
while (right < s.length) {
var c = s[right];
right++;
// 进行窗口内数据的一系列更新
if (need.has(c)) {
if (window.has(c)) {
window.set(c, window.get(c) + 1);
} else {
window.set(c, 1);
}
if (need.get(c) === window.get(c))
valid++;
}
// 判断左侧窗口是否要收缩
while (right - left >= t.length) {
// 当窗口符合条件时,把起始索引加入 res
if (valid === need.size) {
res.push(left);
}
var d = s[left];
left++;
// 进行窗口内数据的一系列更新
if (need.has(d)) {
if (window.get(d) === need.get(d))
valid--;
window.set(d, window.get(d) - 1);
}
}
}
return res;
};
/**
* 找到字符串 s 中所有 p 的异位词的子串的起始索引
* @param {string} s - 字符串 s
* @param {string} p - 字符串 p
* @returns {number[]} - 所有符合条件的子串的起始索引
*/
function findAnagrams(s, p) {
const result = []; // 存储符合条件的子串的起始索引
const map = new Map(); // 存储字符串 p 中每个字符出现的次数
for (const c of p) {
map.set(c, (map.get(c) || 0) + 1);
}
let left = 0; // 滑动窗口左边界
let right = 0; // 滑动窗口右边界
let count = p.length; // 计数器,表示还需要匹配的字符数
while (right < s.length) {
const c = s[right]; // 当前字符
if (map.has(c)) {
map.set(c, map.get(c) - 1); // 将当前字符从 map 中删除
if (map.get(c) >= 0) {
count--; // 如果当前字符在 map 中的数量大于等于 0,则计数器减 1
}
}
right++; // 右边界向右移动
while (count === 0) {
if (right - left === p.length) {
result.push(left); // 如果滑动窗口的长度等于 p 的长度,则将左边界加入结果数组中
}
const c = s[left]; // 左边界字符
if (map.has(c)) {
map.set(c, map.get(c) + 1); // 将左边界字符重新加入 map 中
if (map.get(c) > 0) {
count++; // 如果左边界字符在 map 中的数量大于 0,则计数器加 1
}
}
left++; // 左边界向右移动
}
}
return result;
}
给定一个字符串 queryIP。如果是有效的 IPv4 地址,返回 "IPv4" ;
如果是有效的 IPv6 地址,返回 "IPv6" ;
如果不是上述类型的 IP 地址,返回 "Neither" 。
有效的IPv4地址 是 “x1.x2.x3.x4” 形式的IP地址。
其中 0 <= xi <= 255 且 xi 不能包含 前导零。
例如: “192.168.1.1” 、 “192.168.1.0” 为有效IPv4地址, “192.168.01.1” 为无效IPv4地址; “192.168.1.00” 、 “192.168@1.1” 为无效IPv4地址。
一个有效的IPv6地址 是一个格式为“x1:x2:x3:x4:x5:x6:x7:x8” 的IP地址,其中:
1 <= xi.length <= 4
xi 是一个 十六进制字符串 ,可以包含数字、小写英文字母( 'a' 到 'f' )和大写英文字母( 'A' 到 'F' )。
在 xi 中允许前导零。
例如 "2001:0db8:85a3:0000:0000:8a2e:0370:7334" 和 "2001:db8:85a3:0:0:8A2E:0370:7334" 是有效的 IPv6 地址,而 "2001:0db8:85a3::8A2E:037j:7334" 和 "02001:0db8:85a3:0000:0000:8a2e:0370:7334" 是无效的 IPv6 地址。
示例 1:
输入:queryIP = "172.16.254.1"
输出:"IPv4"
解释:有效的 IPv4 地址,返回 "IPv4"
示例 2:
输入:queryIP = "2001:0db8:85a3:0:0:8A2E:0370:7334"
输出:"IPv6"
解释:有效的 IPv6 地址,返回 "IPv6"
示例 3:
输入:queryIP = "256.256.256.256"
输出:"Neither"
解释:既不是 IPv4 地址,又不是 IPv6 地址
提示:
queryIP 仅由英文字母,数字,字符 '.' 和 ':' 组成。
validIPAddress 函数:判断给定的字符串是否为有效的 IPv4 或 IPv6 地址,接收一个字符串作为参数,返回一个字符串。
如果字符串中包含 ".",则判断为 IPv4 地址。
将字符串按 "." 分割成数组。
如果 "." 的数量不为 4,则不是有效的 IPv4 地址。
遍历数组中的每个数字,判断其是否满足 IPv4 地址的要求。
如果数字的长度为 0 或大于 3,则不是有效的 IPv4 地址。
如果数字以 0 开头且长度大于 1,则不是有效的 IPv4 地址。
如果数字中包含非数字字符,则不是有效的 IPv4 地址。
将数字转换为数字类型,如果不在 [0, 255] 范围内,则不是有效的 IPv4 地址。
如果满足上述条件,则是有效的 IPv4 地址。
如果字符串中包含 ":",则判断为 IPv6 地址。
将字符串按 ":" 分割成数组。
如果 ":" 的数量不为 8,则不是有效的 IPv6 地址。
遍历数组中的每个数字,判断其是否满足 IPv6 地址的要求。
如果数字的长度为 0 或大于 4,则不是有效的 IPv6 地址。
如果数字中包含非十六进制字符,则不是有效的 IPv6 地址。
如果满足上述条件,则是有效的 IPv6 地址。
如果字符串既不包含 "." 也不包含 ":",则不是有效的 IPv4 或 IPv6 地址。
/**
* @param {string} queryIP
* @return {string}
*/
var validIPAddress = function(queryIP) {
const isIPv4 = queryIP.indexOf(".") >= 0, isIPv6 = queryIP.indexOf(":") >= 0
if (isIPv4 && !isIPv6) {
if (IPv4Check(queryIP)) {
return "IPv4"
}
} else if (isIPv6 && !isIPv4) {
if (IPv6Check(queryIP)) {
return "IPv6"
}
}
return "Neither";
};
function IPv4Check(ip) {
const splits = ip.split(".")
if (splits.length != 4) {
return false
}
for (const s of splits) {
if (s.length > 3 || s.length == 0 || (s.length > 1 && s.charCodeAt(0) == '0'.charCodeAt(0))) {
return false
}
let cur = 0
for (let i = 0; i < s.length; i++) {
if (s.charCodeAt(i) <= '9'.charCodeAt(0) && s.charCodeAt(i) >= '0'.charCodeAt(0)) {
cur = 10 * cur + s.charCodeAt(i) - '0'.charCodeAt(0)
} else {
return false
}
}
if (cur > 255) {
return false
}
}
return true
};
function IPv6Check(ip) {
const splits = ip.split(":")
if (splits.length != 8) {
return false
}
for (const s of splits) {
if (s.length > 4 || s.length == 0) {
return false
}
for (let i = 0; i < s.length; i++) {
const c = s.charCodeAt(i)
if (! (('0'.charCodeAt(0) <= c && c <= '9'.charCodeAt(0)) || ('a'.charCodeAt(0) <= c && 'f'.charCodeAt(0) >= c) || ('A'.charCodeAt(0) <= c && c <= 'F'.charCodeAt(0)))) {
return false
}
}
}
return true
}
/**
* 判断给定的字符串是否为有效的 IPv4 或 IPv6 地址
* @param {string} queryIP - 待判断的字符串
* @returns {string} - 如果是有效的 IPv4 地址,返回 "IPv4";如果是有效的 IPv6 地址,返回 "IPv6";否则返回 "Neither"
*/
function validIPAddress1(queryIP) {
if (queryIP.includes(".")) { // 如果字符串中包含 ".",则判断为 IPv4 地址
const nums = queryIP.split(".");
if (nums.length !== 4) { // 如果 "." 的数量不为 4,则不是有效的 IPv4 地址
return "Neither";
}
for (const num of nums) {
if (num.length === 0 || num.length > 3) { // 如果数字的长度为 0 或大于 3,则不是有效的 IPv4 地址
return "Neither";
}
if (num[0] === "0" && num.length > 1) { // 如果数字以 0 开头且长度大于 1,则不是有效的 IPv4 地址
return "Neither";
}
for (const c of num) {
if (!/\d/.test(c)) { // 如果数字中包含非数字字符,则不是有效的 IPv4 地址
return "Neither";
}
}
const n = Number(num);
if (n < 0 || n > 255) { // 如果数字不在 [0, 255] 范围内,则不是有效的 IPv4 地址
return "Neither";
}
}
return "IPv4"; // 如果满足上述条件,则是有效的 IPv4 地址
} else if (queryIP.includes(":")) { // 如果字符串中包含 ":",则判断为 IPv6 地址
const nums = queryIP.split(":");
if (nums.length !== 8) { // 如果 ":" 的数量不为 8,则不是有效的 IPv6 地址
return "Neither";
}
for (const num of nums) {
if (num.length === 0 || num.length > 4) { // 如果数字的长度为 0 或大于 4,则不是有效的 IPv6 地址
return "Neither";
}
for (const c of num) {
if (!/[0-9a-fA-F]/.test(c)) { // 如果数字中包含非十六进制字符,则不是有效的 IPv6 地址
return "Neither";
}
}
}
return "IPv6"; // 如果满足上述条件,则是有效的 IPv6 地址
} else {
return "Neither"; // 如果字符串中既不包含 "." 也不包含 ":",则不是有效的 IPv4 或 IPv6 地址
}
}
给你一棵二叉树的根节点,返回该树的 直径 。
二叉树的 直径 是指树中任意两个节点之间最长路径的 长度 。这条路径可能经过也可能不经过根节点 root 。
两节点之间路径的 长度 由它们之间边数表示。
示例 1:
输入:root = [1,2,3,4,5]
输出:3
解释:3 ,取路径 [4,2,1,3] 或 [5,2,1,3] 的长度。
示例 2:
输入:root = [1,2]
输出:1
提示:
树中节点数目在范围 [1, 10^4] 内
-100 <= Node.val <= 100
diameterOfBinaryTree 函数:返回二叉树的直径,接收一个二叉树的根节点作为参数,返回一个数字。
diameter 变量:二叉树的直径,初始值为 0。
depth 函数:计算二叉树的深度,接收一个节点作为参数,返回一个数字。
如果节点为空,则深度为 0。
计算左子树的深度。
计算右子树的深度。
更新直径,即取左子树深度和右子树深度之和的最大值。
返回当前节点的深度,即左子树深度和右子树深度的最大值加 1。
调用 depth 函数计算二叉树的深度。
返回二叉树的直径。
/**
* 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}
*/
// 递归
var diameterOfBinaryTree1 = function(root) {
let result = 0
function getDepth(root) {
if(!root) return 0
const leftDepth = getDepth(root.left)
const rightDepth = getDepth(root.right)
result = Math.max(result,(leftDepth + rightDepth ))
return Math.max(leftDepth,rightDepth) + 1
}
getDepth(root)
return result
};
/**
* 返回二叉树的直径
* @param {TreeNode} root - 二叉树的根节点
* @returns {number} - 二叉树的直径
*/
function diameterOfBinaryTree(root) {
let diameter = 0; // 二叉树的直径
/**
* 计算二叉树的深度
* @param {TreeNode} node - 当前节点
* @returns {number} - 当前节点的深度
*/
function depth(node) {
if (!node) { // 如果节点为空,则深度为 0
return 0;
}
const leftDepth = depth(node.left); // 左子树的深度
const rightDepth = depth(node.right); // 右子树的深度
diameter = Math.max(diameter, leftDepth + rightDepth); // 更新直径
return Math.max(leftDepth, rightDepth) + 1; // 返回当前节点的深度
}
depth(root); // 计算二叉树的深度
return diameter; // 返回二叉树的直径
}
给你一个整数数组 nums 和一个整数 k ,请你统计并返回 该数组中和为 k 的连续子数组的个数 。
示例 1:
输入:nums = [1,1,1], k = 2
输出:2
示例 2:
输入:nums = [1,2,3], k = 3
输出:2
提示:
1 <= nums.length <= 2 * 10^4
-1000 <= nums[i] <= 1000
-10^7 <= k <= 10^7
subarraySum 函数:统计数组中和为 k 的连续子数组的个数,接收一个整数数组和一个目标和作为参数,返回一个数字。
map 变量:哈希表,用于存储前缀和出现的次数。
count 变量:和为 k 的连续子数组的个数,初始值为 0。
sum 变量:前缀和,初始值为 0。
将前缀和为 0 的子数组出现了 1 次存入哈希表。
遍历整数数组中的每个数字。
计算前缀和。
如果前缀和中存在 sum - k,则说明存在和为 k 的连续子数组。
更新计数器,即将和为 sum - k 的连续子数组的个数加到计数器中。
将前缀和存入哈希表。
返回和为 k 的连续子数组的个数。
/**
* @param {number[]} nums
* @param {number} k
* @return {number}
*/
var subarraySum1 = function(nums, k) {
// 前缀和:nums 的第 0 项到 当前项 的和。
// 定义 prefixSum 数组,prefixSum[x]:第 0 项到 第 x 项 的和。
// prefixSum[x] = nums[0] +... + nums[x]
// nums 的某项 = 两个相邻前缀和的差:
// nums[x] = prefixSum[x] - prefixSum[x - 1]
// nums 的 第 i 到 j 项 的和,有:
// nums[i] + ... + nums[j] = prefixSum[j] -prefixSum[i - 1]
// 当 i 为 0,此时 i-1 为 -1,我们故意让 prefixSum[-1] 为 0,使得通式在i=0时也成立:
// nums[0] + ... + nums[j] = prefixSum[j]
// 原题为有几种 i、j 的组合,使得从第 i 到 j 项的子数组和等于 k。
// 题目转化为有几种 i、j 的组合,满足nums[i] + ... + nums[j] = k = prefixSum[j] -prefixSum[i - 1]
// 注意,其实我们不关心具体是哪两项的前缀和之差等于k,只关心等于 k 的前缀和之差出现的次数c,就知道了有c个子数组求和等于k。
// 思路:
// 1.遍历 nums 数组,求每一项的前缀和,统计对应的出现次数,以键值对存入 map。
// 2.遍历 nums 之前,我们让 -1 对应的前缀和为 0,这样通式在边界情况也成立。即在遍历之前,map 初始放入 0:1 键值对(前缀和为0出现1次了)。
// 3.边存边查看 map,如果 map 中存在 key 为「当前前缀和 - k」,说明这个之前出现的前缀和,满足「当前前缀和 - 该前缀和 == k」,它出现的次数,累加给 count。
const map = new Map()
// 遍历 nums 之前,我们让 -1 对应的前缀和为 0,这样通式在边界情况也成立。即在遍历之前,map 初始放入 0:1 键值对(前缀和为0出现1次了)。
map.set(0, 1)
let prefixSum = 0;
let count = 0
for (let i = 0; i < nums.length; i++) {
// 记录前缀和
prefixSum += nums[i]
// 对应上述第三点:边存边查看 map,如果 map 中存在 key 为[当前前缀和 - k],说明这个之前出现的前缀和,满足[当前前缀和 - 该前缀和 == k],它出现的次数,累加给 count。
if (map.get(prefixSum - k)) {
count += map.get(prefixSum - k);
}
// 遍历 nums 数组,求每一项的前缀和,如果某个前缀和出现过,就在它之前的数量上加1
if (map.get(prefixSum)) {
map.set(prefixSum, map.get(prefixSum) + 1);
} else {
// 第一次出现则设置该前缀和为1
map.set(prefixSum,1);
}
}
return count
};
/**
* 统计数组中和为 k 的连续子数组的个数
* @param {number[]} nums - 整数数组
* @param {number} k - 目标和
* @returns {number} - 和为 k 的连续子数组的个数
*/
function subarraySum(nums, k) {
const map = new Map(); // 哈希表,用于存储前缀和出现的次数
let count = 0; // 和为 k 的连续子数组的个数
let sum = 0; // 前缀和
map.set(0, 1); // 初始化,前缀和为 0 的子数组出现了 1 次
for (const num of nums) {
sum += num; // 计算前缀和
if (map.has(sum - k)) { // 如果前缀和中存在 sum - k,则说明存在和为 k 的连续子数组
count += map.get(sum - k); // 更新计数器
}
map.set(sum, (map.get(sum) || 0) + 1); // 将前缀和存入哈希表
}
return count; // 返回和为 k 的连续子数组的个数
}
给定一个 n 个元素有序的(升序)整型数组 nums 和一个目标值 target,写一个函数搜索 nums 中的 target,如果目标值存在返回下标,否则返回 -1。
示例 1:
输入: nums = [-1,0,3,5,9,12], target = 9
输出: 4
解释: 9 出现在 nums 中并且下标为 4
示例 2:
输入: nums = [-1,0,3,5,9,12], target = 2
输出: -1
解释: 2 不存在 nums 中因此返回 -1
提示:
你可以假设 nums 中的所有元素是不重复的。
n 将在 [1, 10000]之间。
nums 的每个元素都将在 [-9999, 9999]之间。
search 函数:在有序数组中查找目标值的下标,接收一个有序整型数组和一个目标值作为参数,返回一个数字。
left 变量:左指针,初始值为 0。
right 变量:右指针,初始值为数组长度减 1。
当左指针小于等于右指针时,继续查找。
mid 变量:中间位置,取左指针和右指针的平均值向下取整。
如果中间位置的值等于目标值,则返回中间位置。
如果中间位置的值小于目标值,则在右半部分查找。
如果中间位置的值大于目标值,则在左半部分查找。
如果没有找到目标值,则返回 -1。
/**
* @param {number[]} nums
* @param {number} target
* @return {number}
*/
var search1 = function(nums, target) {
const len = nums.length
let start = 0, end = len - 1;
while(start <= end) {
let middle = Math.floor((end + start) / 2)
if (target > nums[middle]) {
start = middle + 1
} else if (target < nums[middle]) {
end = middle - 1
} else {
return middle
}
}
return -1
};
/**
* 在有序数组中查找目标值的下标
* @param {number[]} nums - 有序整型数组
* @param {number} target - 目标值
* @returns {number} - 目标值的下标,如果不存在则返回 -1
*/
function search(nums, target) {
let left = 0; // 左指针
let right = nums.length - 1; // 右指针
while (left <= right) { // 当左指针小于等于右指针时,继续查找
const mid = Math.floor((left + right) / 2); // 中间位置
if (nums[mid] === target) { // 如果中间位置的值等于目标值,则返回中间位置
return mid;
} else if (nums[mid] < target) { // 如果中间位置的值小于目标值,则在右半部分查找
left = mid + 1;
} else { // 如果中间位置的值大于目标值,则在左半部分查找
right = mid - 1;
}
}
return -1; // 如果没有找到目标值,则返回 -1
}
给定一个整数数组 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]
提示:
1 <= temperatures.length <= 10^5
30 <= temperatures[i] <= 100
单调栈是一种特殊的栈数据结构,它维护栈内元素的单调性。单调栈分为两种类型:
单调递增栈:栈内元素从栈底到栈顶单调递增。 单调递减栈:栈内元素从栈底到栈顶单调递减。 单调栈常用于解决一些需要快速找到某个元素左边或右边第一个比它大或小的元素的问题。
在单调递增栈中,每轮遍历会将每轮元素索引入栈,记录前面有哪些元素比他小,当后面遍历时如果有更大的元素时方便比较。 这里需要使用while循环,一旦存在一个较大元素,就可以将前面递减的较小元素的一次性逐个出栈,得出一批元素的第一个比它大元素的计算结果。
使用单调栈:
单调栈的本质是空间换时间,因为在遍历的过程中需要用一个栈来记录右边第一个比当前元素大的元素,优点是只需要遍历一次。
单调栈存的是递减温度时的下标,当遍历遇到比栈顶最小值更大的温度时,这时就需要更新单调栈,移除较低的温度,将此值放入到合适的位置,维持单调栈递减的状态,并记录该放入位置到遍历索引的距离。
此时这个距离内的其它元素的值是递减的,直到当前遍历索引位置处,所以这个单调递减的长度就是下一个更高温度的长度。
dailyTemperatures 函数:计算每天下一个更高温度出现的天数,接收一个整数数组作为参数,返回一个整数数组。
n 变量:数组长度。
result 数组:初始化结果数组,每个元素的值为 0。
stack 数组:单调栈,存储数组下标。
遍历整数数组中的每个元素。
如果栈不为空且当前元素的值大于栈顶元素的值,则弹出栈顶元素,计算下一个更高温度出现的天数,并将结果存入结果数组中。
将当前元素的下标入栈。
返回结果数组
/**
* @param {number[]} temperatures
* @return {number[]}
*/
var dailyTemperatures1 = function (temperatures) {
// 单调栈的本质是空间换时间,因为在遍历的过程中需要用一个栈来记录右边第一个比当前元素的元素,优点是只需要遍历一次。
let answer = new Array(temperatures.length).fill(0); // 初始化
// 单调栈里只需要存放元素的下标i就可以了,如果需要使用对应的元素,直接T[i]就可以获取。
// 可以维护一个存储下标的单调栈,从栈底到栈顶的下标对应的温度列表中的温度依次递减。如果一个下标在单调栈里,则表示尚未找到下一次温度更高的下标。
let monotone = []; //栈数组
// 正向遍历温度列表。对于温度列表中的每个元素 temperatures[i],如果栈为空,则直接将 i 进栈,如果栈不为空,则比较栈顶元素 prevIndex 对应的温度 temperatures[prevIndex] 和当前温度 temperatures[i],如果 temperatures[i] > temperatures[prevIndex],则将 prevIndex 移除,并将 prevIndex 对应的等待天数赋为 i - prevIndex,重复上述操作直到栈为空或者栈顶元素对应的温度小于等于当前温度,然后将 i 进栈。
// 为什么可以在弹栈的时候更新 ans[prevIndex] 呢?因为在这种情况下,即将进栈的 i 对应的 temperatures[i] 一定是 temperatures[prevIndex] 右边第一个比它大的元素
// 试想如果 prevIndex 和 i 有比它大的元素,假设下标为 j,那么 prevIndex 一定会在下标 j 的那一轮被弹掉。
// 由于单调栈满足从栈底到栈顶元素对应的温度递减,因此每次有元素进栈时,会将温度更低的元素全部移除,并更新出栈元素对应的等待天数,这样可以确保等待天数一定是最小的
for (let i = 0; i < temperatures.length; i++) {
if (i > 0 && temperatures[i - 1] < temperatures[i]) {
// 从后往前循环数组模拟从栈顶到栈低
for (let j = monotone.length - 1; j >= 0; j--) {
if (temperatures[i] > temperatures[monotone[j]]) {
// 给answer数组中的对应元素赋值为两个元素下标差值
answer[monotone[j]] = i - monotone[j];
// 出栈
monotone.pop();
}
}
}
// 入栈当前元素下标
monotone.push(i);
}
return answer;
};
/**
* 计算每天下一个更高温度出现的天数
* @param {number[]} temperatures - 每天的温度
* @returns {number[]} - 每天下一个更高温度出现的天数
*/
function dailyTemperatures(temperatures) {
const n = temperatures.length; // 数组长度
const result = new Array(n).fill(0); // 初始化结果数组,每个元素的值为 0
const stack = []; // 单调栈,存储数组下标
for (let i = 0; i < n; i++) {
while (stack.length > 0 && temperatures[i] > temperatures[stack[stack.length - 1]]) {
const j = stack.pop(); // 弹出栈顶元素
result[j] = i - j; // 计算下一个更高温度出现的天数
}
stack.push(i); // 将当前元素的下标入栈
}
return result; // 返回结果数组
}
function dailyTemperatures(temperatures) {
const n = temperatures.length; // 数组长度
const result = new Array(n).fill(0); // 初始化结果数组,每个元素的值为 0
const stack = []; // 单调栈,存储数组下标
for (let i = 0; i < n; i++) {
console.log('******************************')
console.log('i', i)
while (stack.length > 0 && temperatures[i] > temperatures[stack[stack.length - 1]]) {
const j = stack.pop(); // 弹出栈顶元素
result[j] = i - j; // 计算下一个更高温度出现的天数
console.log('j', j)
console.log('i-j', i-j)
}
stack.push(i); // 将当前元素的下标入栈
console.log('stack', stack)
console.log('******************************')
}
return result; // 返回结果数组
}
const temperatures = [73,74,75,71,69,72,76,73]
dailyTemperatures(temperatures)
给你一个字符串 s 。我们要把这个字符串划分为尽可能多的片段,同一字母最多出现在一个片段中。
注意,划分结果需要满足:将所有划分结果按顺序连接,得到的字符串仍然是 s 。
返回一个表示每个字符串片段的长度的列表。
示例 1:
输入:s = "ababcbacadefegdehijhklij"
输出:[9,7,8]
解释:
划分结果为 "ababcbaca"、"defegde"、"hijhklij" 。
每个字母最多出现在一个片段中。
像 "ababcbacadefegde", "hijhklij" 这样的划分是错误的,因为划分的片段数较少。
示例 2:
输入:s = "eccbbbbdec"
输出:[10]
提示:
1 <= s.length <= 500
s 仅由小写英文字母组成
贪心策略:维护该段必须要包含的最大索引就可以确定最小的分割方式
- 首次求出每一个字母出现的最大索引,如s=aabcacb,f(a)=4,f(b)=6,f(c)=5
- 第二次遍历对比当前出现的字母是不是最后出现的一次, a. 若不是说明要到后面才能分割(因为后面段不能再出现这段出现过的字母) b. 若是说明(仅仅考虑当前字母)已经可以进行分割了,但是也得同时满足前面的字母!
因此维护当前段的字母出现索引的最大值end,若i=end说明该段字母全部满足条件,分割 3. 利用始末索引相减+1即可求得该段的大小,同时求得更新start索引
partitionLabels 函数:划分字符串为尽可能多的片段,同一字母最多出现在一个片段中,接收一个字符串作为参数,返回一个数字数组。
last 数组:存储每个字母最后出现的位置。 n 变量:字符串长度。 遍历字符串中的每个字符。 更新字母最后出现的位置。 result
数组:存储每个字符串片段的长度。 start 变量:当前片段的起始位置,初始值为 0。 end 变量:当前片段的结束位置,初始值为 0。
遍历字符串中的每个字符。 更新当前片段的结束位置,即取当前位置和当前字母最后出现的位置的最大值。
如果当前位置等于当前片段的结束位置,则当前片段已找到。 将当前片段的长度存入结果数组中。 更新下一个片段的起始位置。
返回每个字符串片段的长度的列表。
/**
* @param {string} s
* @return {number[]}
*/
// 贪心算法:
var partitionLabels = function(s) {
// 1, 首先看第一个字母,找到它在串里最后的一个位置,记作last或一段的最后位置。
// 2, 在从0~last这个范围里,挨个查其他字母,看他们的最后位置是不是比刚才的last或一段的最后位置大。
// 如果没有刚才的last或一段的最后位置大,无视它继续往后找。
// 如果比刚才的大,说明这一段的分隔位置必须往后移动,所以我们把last或一段的最后位置更新为当前的字母的最后位置。
// 3,肯定到有一个时间,这个last就更新不了了,那么这个时候这个位置就是我们的分隔位置。
// 注意题目要分隔后的长度,我们就用last - startindex + 1。
// 4,找到一个分割位,更新一下起始位置,同理搜索就行了。
// 数组last维护一个存放26个字母最后一个位置的哈希
const last = new Array(26);
const length = s.length;
// 字符a的Unicode 编码值
const codePointA = 'a'.codePointAt(0);
// 循环遍历整个字符串s
for (let i = 0; i < length; i++) {
// 记录下每种字符最后出现的位置
last[s.codePointAt(i) - codePointA] = i;
}
// 表示每个字符串片段的长度的列表
const partition = [];
// 遍历中字符串分割的开始和结束位置
let start = 0, end = 0;
// 再次循环遍历字符串s
for (let i = 0; i < length; i++) {
// codePointAt 返回值是在字符串中的给定索引的字符的Unicode 编码值。如果在指定的位置没有元素则返回 undefined。例如'ABC'.codePointAt(1); // 66
// 在从0~last这个范围里,挨个查其他字母,看他们的最后位置是不是比刚才的last或一段的最后位置大。
// 找出当前出现的各类字符中的最后一个位置的索引作为分割的边界end
end = Math.max(end, last[s.codePointAt(i) - codePointA]);
// 如果恰好时,证明处于分割位置处
if (i == end) {
// 记录当前分割字符串长度
partition.push(end - start + 1);
// 更新可分割字符串开始的位置
start = end + 1;
}
}
return partition;
};
给你 k 枚相同的鸡蛋,并可以使用一栋从第 1 层到第 n 层共有 n 层楼的建筑。
已知存在楼层 f ,满足 0 <= f <= n ,任何从 高于 f 的楼层落下的鸡蛋都会碎,从 f 楼层或比它低的楼层落下的鸡蛋都不会破。
每次操作,你可以取一枚没有碎的鸡蛋并把它从任一楼层 x 扔下(满足 1 <= x <= n)。
如果鸡蛋碎了,你就不能再次使用它。如果某枚鸡蛋扔下后没有摔碎,则可以在之后的操作中 重复使用 这枚鸡蛋。
请你计算并返回要确定 f 确切的值 的 最小操作次数 是多少?
示例 1:
输入:k = 1, n = 2
输出:2
解释:
鸡蛋从 1 楼掉落。如果它碎了,肯定能得出 f = 0 。
否则,鸡蛋从 2 楼掉落。如果它碎了,肯定能得出 f = 1 。
如果它没碎,那么肯定能得出 f = 2 。
因此,在最坏的情况下我们需要移动 2 次以确定 f 是多少。
示例 2:
输入:k = 2, n = 6
输出:3
示例 3:
输入:k = 3, n = 14
输出:4
提示:
1 <= k <= 100
1 <= n <= 10^4
使用动态规划的方法解决了鸡蛋掉落问题
将问题从: N 个楼层,有 K 个蛋,求最少要扔 T 次,才能保证当 F 无论是 0 <= F <= N 中哪个值,都能测试出来
转变为:有 K 个蛋,扔 T 次,求可以确定 F 的个数,然后得出 N 个楼层
比如: N = 1 层楼 在 1 层扔,碎了,因为楼层高于 F 才会碎,所以 F < 1 。
又因为 0 <= F <= N ,所以能确定 F = 0 在 1 层扔,没碎,因为从 F 楼层或比它低的楼层落下的鸡蛋都不会碎,所以 F >= 1 。
又因为 0 <= F <= N ,所以能确定 F = 1
再比如: N = 2 层楼 在 1 层扔,碎了,F < 1,所以确定 F = 0 在 1 层扔,没碎,但在 2 层扔,碎了, F >= 1 && F < 2,所以确定 F = 1 在 2 层扔,没碎,F >= 2,所以确定 F = 2
如果只有 1 个蛋,或只有 1 次机会时,只可以确定出 T + 1 个 F
其他情况时,递归。【蛋碎了减 1 个,机会减 1 次】 + 【蛋没碎,机会减 1 次】
题目给出了 K ,不断增大 T ,计算出来的 F 的个数已经超过了 N + 1 时,就找到了答案
鸡蛋掉落问题是一个经典的动态规划问题,其思想主要包括以下几个方面:
状态定义:首先需要定义状态,即确定问题的子问题。在鸡蛋掉落问题中,状态可以由两个变量定义:鸡蛋的个数
k
和楼层数n
。我们可以使用一个二维数组dp[k][n]
来表示状态。即k个鸡蛋,n层楼时,最少需要操作的次数为dp[k][n]状态转移:接下来需要确定状态之间的转移关系。在鸡蛋掉落问题中,我们需要考虑两种情况:鸡蛋碎了和鸡蛋没碎。 a. 当鸡蛋碎了时,我们需要继续在比当前楼层更低的楼层继续尝试。因此,状态转移方程可以表示为
dp[i - 1][x - 1] + 1
,其中x
是当前楼层。 b. 当鸡蛋没碎时,我们需要继续在比当前楼层更高的楼层继续尝试。因此,状态转移方程可以表示为dp[i][j - x] + 1
,其中x
是当前楼层。在每一层楼进行尝试,计算鸡蛋碎了和没碎的情况下的最小操作次数,并取两者的最大值。最后,将最大值更新为当前状态的最小操作次数。 3. 边界条件:在动态规划问题中,边界条件是十分重要的。在鸡蛋掉落问题中,当楼层数为 0 时,不需要操作,次数为 0。当只有一枚鸡蛋时,最坏情况下需要操作次数等于楼层数。
创建一个 k 行 n 列的二维数组,用于记录状态转移次数
填充第一列,当楼层数为 0 时,不需要操作,次数为 0:
dp[i][0] = 0;
填充第一行,当只有一枚鸡蛋时,最坏情况下需要操作次数等于楼层数:
dp[1][j] = j;
4. 最优解:根据状态转移方程和边界条件,计算出最后一个状态的值,即为最优解,即最小操作次数。 通过以上思想,我们可以将鸡蛋掉落问题转化为一个二维动态规划问题,并使用动态规划算法来解决。该思想可以用来解决各种鸡蛋掉落问题,例如确定鸡蛋恰好会碎的楼层,或者给定鸡蛋个数和操作次数的情况下,求解可以确定楼层的最大鸡蛋数等等。
/**
* @param {number} k
* @param {number} n
* @return {number}
*/
var superEggDrop1 = function (K, N) {
// 不选择dp[K][M]的原因是dp[M][K]可以简化操作
// 题目要求最少的扔的次数,假设有一个函数 f(k, i),他的功能是求出 k 个鸡蛋,扔 i 次所能检测的最高楼层。
// 只需要返回第一个返回值为 true 的 m 即可
const dp = Array(N + 1)
.fill(0)
.map((_) => Array(K + 1).fill(0));
let m = 0;
while (dp[m][K] < N) {
m++;
for (let k = 1; k <= K; ++k) {
// 摔碎的情况,可以检测的最高楼层是f(m - 1, k - 1) + 1。因为碎了嘛,我们多检测了摔碎的这一层。
// 没有摔碎的情况,可以检测的最高楼层是f(m - 1, k)。因为没有碎,也就是说我们啥都没检测出来(对能检测的最高楼层无贡献)。
dp[m][k] = dp[m - 1][k - 1] + 1 + dp[m - 1][k];
}
}
return m;
};
function superEggDrop(k, n) {
// 创建一个 k 行 n 列的二维数组,用于记录状态转移次数
const dp = Array(k + 1).fill(0).map(() => Array(n + 1).fill(0));
// 填充第一列,当楼层数为 0 时,不需要操作,次数为 0
for (let i = 1; i <= k; i++) {
dp[i][0] = 0;
}
// 填充第一行,当只有一枚鸡蛋时,最坏情况下需要操作次数等于楼层数
for (let j = 1; j <= n; j++) {
dp[1][j] = j;
}
// 填充剩下的格子
for (let i = 2; i <= k; i++) {
for (let j = 1; j <= n; j++) {
dp[i][j] = Infinity; // 初始化为正无穷大
// 在每一层楼进行尝试
for (let x = 1; x <= j; x++) {
// 鸡蛋碎了,需要往下面的楼层继续尝试,次数加一
const breakEgg = dp[i - 1][x - 1] + 1;
// 鸡蛋没碎,需要往上面的楼层继续尝试,次数加一
const notBreakEgg = dp[i][j - x] + 1;
// 取两者中的最大值,因为要确定最坏情况下的最小次数
const attempts = Math.max(breakEgg, notBreakEgg);
// 更新状态转移次数
dp[i][j] = Math.min(dp[i][j], attempts);
}
}
}
// 返回最后一个格子的值,即为最小操作次数
return dp[k][n];
}
给定一个整数数组 arr,找到 min(b) 的总和,其中 b 的范围为 arr 的每个(连续)子数组。
由于答案可能很大,因此 返回答案模 10^9 + 7 。
示例 1:
输入:arr = [3,1,2,4]
输出:17
解释:
子数组为 [3],[1],[2],[4],[3,1],[1,2],[2,4],[3,1,2],[1,2,4],[3,1,2,4]。
最小值为 3,1,2,4,1,1,2,1,1,1,和为 17。
示例 2:
输入:arr = [11,81,94,43,3]
输出:444
提示:
1 <= arr.length <= 3*10^4
1 <= arr[i] <= 3*10^4
使用单调栈的方法来计算最小值的总和。具体的解题思路如下:
- 首先,我们定义一个变量
mod
,用于表示取模的值,即10^9 + 7
。- 然后,我们创建一个空栈
monoStack
,用于保存递增元素的索引。- d[i] 是每个以 i 为最右的子数组最小值之和
- 遍历数组,把当前元素放入单调栈,在这之前:
如果栈顶的元素大于等于当前元素 arr[i] ,则弹出栈顶
故以当前值为最右且最小的子序列的长度 k:const k = monoStack.length === 0 ? (i + 1) : (i - monoStack[monoStack.length - 1]);
每个以 i 为最右的子数组最小值之和d[i]:dp[i] = k * arr[i] + (monoStack.length === 0 ? 0 : dp[i - k]);
也即:以第
i
个元素为结尾的子序列的最小值之和等于以第i
个元素为结尾的子序列的最小值k * arr[i]
加上以第i - k
个元素为结尾的子序列的最小值之和dp[i - k]
(如果存在)。
k * arr[i]
:表示以第i
个元素为结尾的子序列的最小值。
(monoStack.length === 0 ? 0 : dp[i - k])
:表示以第i
个元素为结尾的子序列的最小值之和。如果单调栈为空(即
monoStack.length === 0
),说明当前元素是当前子序列中的最小值,此时以第i
个元素为结尾的子序列的最小值之和为0。如果单调栈不为空,说明当前元素不是当前子序列中的最小值,此时以第
i
个元素为结尾的子序列的最小值之和为dp[i - k]
,即以第i - k
个元素为结尾的子序列的最小值之和。确保在计算每个元素的最小值之和时,正确地考虑了当前元素是否是当前子序列中的最小值,以及左边的子序列是否存在。
计算每个以当前值为最右且最小的子序列的最小值之和的和,并取模防止值溢出: ans = (ans + dp[i]) % MOD;
var sumSubarrayMins1 = function (arr) {
const mod = Math.pow(10, 9) + 7; // 取模的值
let sum = 0; // 最小值总和
for (let i = 0; i < arr.length; i++) {
let min = arr[i]; // 初始化当前子数组的最小值为第一个元素
for (let j = i; j < arr.length; j++) {
min = Math.min(min, arr[j]); // 更新最小值
sum += min; // 将最小值累加到总和中
sum %= mod; // 对总和取模
}
}
return sum;
}
/**
* @param {number[]} arr
* @return {number}
*/
// 我们维护一个单调栈,很容易求出元素 x 的左边第一个比它小的元素,即求出以 x 为最右且最小的子序列的最大长度,子数组的最小值之和即为答案。
// 从左向右遍历数组并维护一个单调递增的栈,如果栈顶的元素大于等于当前元素 arr[i] 则弹出栈,此时栈顶的元素即为左边第一个小于小于当前值的元素;
// 我们求出以当前值为最右且最小的子序列的长度 k,根据上述递推公式求出 dp[i],最终的返回值即为答案。
var sumSubarrayMins = function(arr) {
const n = arr.length;
let ans = 0;
const MOD = 1000000007;
const monoStack = [];
const dp = new Array(n).fill(0);
for (let i = 0; i < n; i++) {
// 如果栈顶的元素大于等于当前元素 arr[i] 则弹出栈
while (monoStack.length !== 0 && arr[monoStack[monoStack.length - 1]] > arr[i]) {
monoStack.pop();
}
// 以当前值为最右且最小的子序列的长度 k
const k = monoStack.length === 0 ? (i + 1) : (i - monoStack[monoStack.length - 1]);
// d[i] 是每个以 i 为最右的子数组最小值之和
dp[i] = k * arr[i] + (monoStack.length === 0 ? 0 : dp[i - k]);
// 返回答案模MOD的结果,防止值溢出
ans = (ans + dp[i]) % MOD;
// 把当前元素放入单调栈
monoStack.push(i);
}
return ans;
};
在给定的 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 。
提示:
m == grid.length
n == grid[i].length
1 <= m, n <= 10
grid[i][j]
仅为 0、1 或 2
函数使用了BFS算法来模拟橘子腐烂的过程,具体步骤如下:
遍历整个网格,找出新鲜橘子和腐烂橘子的位置。通过两个变量
fresh
和queue
分别记录新鲜橘子的数量和腐烂橘子的坐标。定义一个变量
time
来记录经过的分钟数。使用BFS算法,当队列存在腐烂橘子 且 存在新鲜橘子,模拟橘子腐烂的过程。在每一分钟内,遍历当前腐烂队列中橘子的坐标,然后遍历其四个方向上的相邻单元,如果是新鲜橘子将其设为腐烂状态,并加入到腐烂队列当中,并更新新鲜橘子的数量。
更新时间
全部完成后,如果没有新鲜的橘子则返回全部腐烂所需时间,仍有橘子未腐烂返回-1
/**
* @param {number[][]} grid
* @return {number}
*/
var orangesRotting = function(grid) {
const m = grid.length
const n = grid[0].length
// 记录腐烂橘子坐标
const queue = []
// 记录有多少新鲜橘子
let fresh = 0
// 记录时间
let time = 0
// 遍历所有橘子
for (let i = 0; i < m; i++) {
for (let j = 0; j < n; j++) {
// 记录腐烂橘子坐标和新鲜橘子数量
if (grid[i][j] === 2) {
queue.push([i, j])
} else if (grid[i][j] === 1) {
fresh ++
}
}
}
// 记录一个位置上下左右位置坐标偏移
const del = [[0, 1], [0, -1], [-1, 0], [1, 0]]
// 队列存在腐烂橘子 且 存在新鲜橘子
while (queue.length && fresh) {
const len = queue.length
// 依次从腐烂橘子队首中冲取出腐烂橘子
for (let i = 0; i < len; i++) {
// 腐烂橘子队列中取出
const item = queue.shift()
// 当前腐烂橘子分别感染其四周橘子使其腐烂
for (let j = 0; j < 4; j++) {
const x = item[0] + del[j][0]
const y = item[1] + del[j][1]
// 判断当前橘子会不会受周围橘子感染(当前橘子是不是新鲜橘子)
if (shouldInfect(grid, x, y)) {
// 感染则新鲜橘子数量减一
fresh --
// 把当前橘子加入腐烂橘子队列
queue.push([x, y])
// 修改原坐标橘子状态为腐烂状态
grid[x][y] = 2
}
}
}
// 每轮腐烂花费的时间增加
time ++
}
// 如果没有新鲜的橘子则返回全部腐烂所需时间,仍有橘子未腐烂返回-1
return fresh === 0 ? time : -1
};
// 判断一个橘子是不是会被感染
function shouldInfect (grid, x, y) {
return grid[x] && grid[x][y] && grid[x][y] === 1
}
给定两个字符串 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 。
提示:
1 <= text1.length, text2.length <= 1000
text1 和 text2 仅由小写英文字符组成。
动态规划:
假设字符串 text1 和 text2 的长度分别为 m 和 n
创建 m+1 行 n+1 列的二维数组 dp,其中
dp[i][j]
表示 text1[0:i] 和 text2[0:j] 的最长公共子序列的长度。text1[0:i] 表示 text1的长度为 i 的前缀,text2[0:j] 表示 text 2的长度为 j 的前缀。
当 i=0 时,text1[0:i] 为空,空字符串和任何字符串的最长公共子序列的长度都是 0,因此对任意 0≤j≤n,有
dp[0][j]=0;
当 j=0 时,text 2[0:j] 为空,同理可得,对任意 0≤i≤m,有
dp[i][0]=0
当 i>0 且 j>0 时,考虑
dp[i][j]
的计算:
当
text 1[i−1]=text2[j−1]
时,将这两个相同的字符称为公共字符,考虑text 1[0:i−1]
和text2[0:j−1]
的最长公共子序列,再增加一个字符(即公共字符)即可得到text [0:i]
和text 2[0:j]
的最长公共子序列,因此dp[i][j]=dp[i−1][j−1]+1
。
当text1[i−1] ≠ text2[j−1]
时,考虑以下两项: a.text1[0:i−1]
和text2[0:j]
的最长公共子序列 b.text1[0:i]
和text2[0:j−1]
的最长公共子序列要得到
text1[0:i]
和text2[0:j]
的最长公共子序列,应取两项中的长度较大的一项,因此dp[i][j] = max(dp[i−1][j], dp[i][j−1])
。最终计算得到
dp[m][n]
即为 text1 和 text2 的最长公共子序列的长度
/**
* @param {string} text1
* @param {string} text2
* @return {number}
*/
var longestCommonSubsequence = function(text1, text2) {
// 字符串1和字符串2的长度
// 假设字符串 text1 和 text2 的长度分别为 m 和 n
const m = text1.length, n = text2.length;
// 创建 m+1 行 n+1 列的二维数组 dp,其中 dp[i][j] 表示 text1[0:i] 和 text2[0:j] 的最长公共子序列的长度。
// text1[0:i] 表示 text1的长度为 i 的前缀,text2[0:j] 表示 text 2的长度为 j 的前缀。
// 当 i=0 时,text1[0:i] 为空,空字符串和任何字符串的最长公共子序列的长度都是 0,因此对任意 0≤j≤n,有 dp[0][j]=0;
// 当 j=0 时,text 2[0:j] 为空,同理可得,对任意 0≤i≤m,有 dp[i][0]=0
// 因此动态规划的边界情况是:当 i=0 或 j=0 时,dp[i][j]=0
const dp = new Array(m + 1).fill(0).map(() => new Array(n + 1).fill(0));
// 当 i>0 且 j>0 时,考虑 dp[i][j] 的计算:
for (let i = 1; i <= m; i++) {
const c1 = text1[i - 1];
for (let j = 1; j <= n; j++) {
const c2 = text2[j - 1];
// 1.当 text 1[i−1]=text2[j−1] 时,将这两个相同的字符称为公共字符,考虑 text 1[0:i−1] 和 text2[0:j−1] 的最长公共子序列,再增加一个字符(即公共字符)即可得到 text [0:i] 和 text 2[0:j] 的最长公共子序列,因此 dp[i][j]=dp[i−1][j−1]+1。
if (c1 === c2) {
dp[i][j] = dp[i - 1][j - 1] + 1;
} else {
// 2.当text1[i−1] ≠ text2[j−1] 时,考虑以下两项:
// (1)text1[0:i−1] 和 text2[0:j] 的最长公共子序列
// (2)text1[0:i] 和 text2[0:j−1] 的最长公共子序列
// 要得到 text1[0:i] 和 text2[0:j] 的最长公共子序列,应取两项中的长度较大的一项,因此 dp[i][j] = max(dp[i−1][j], dp[i][j−1])。
dp[i][j] = Math.max(dp[i - 1][j], dp[i][j - 1]);
}
}
}
// 最终计算得到 dp[m][n] 即为 text1 和 text2 的最长公共子序列的长度
return dp[m][n];
// 时间复杂度:O(mn),其中 m 和 n 分别是字符串 text1 和 text2 的长度。二维数组 dp 有 m+1 行和 n+1 列,需要对 dp 中的每个元素进行计算。
// 空间复杂度:O(mn),其中 m 和 n 分别是字符串 text1 和 text2 的长度。创建了 m+1 行 n+1 列的二维数组 dp。
};
给你一个数组 nums 。数组「动态和」的计算公式为:runningSum[i] = sum(nums[0]…nums[i]) 。
请返回 nums 的动态和。
示例 1:
输入:nums = [1,2,3,4]
输出:[1,3,6,10]
解释:动态和计算过程为 [1, 1+2, 1+2+3, 1+2+3+4]。
示例 2:
输入:nums = [1,1,1,1,1]
输出:[1,2,3,4,5]
解释:动态和计算过程为 [1, 1+1, 1+1+1, 1+1+1+1, 1+1+1+1+1]。
示例 3:
输入:nums = [3,1,2,10,1]
输出:[3,4,6,16,17]
提示:
1 <= nums.length <= 1000
-10^6 <= nums[i] <= 10^6
使用reduce求和,在return每轮的和之前,push该和到结果数组中
/**
* @param {number[]} nums
* @return {number[]}
*/
var runningSum = function(nums) {
if (!nums) {
return []
}
const ret = []
nums.reduce((sum, item) => {
const s = sum += item
ret.push(s)
return s
}, 0)
return ret
};
var runningSum1 = function(nums) {
if (!nums) {
return []
}
const len = nums.length
let temp = 0, ret = []
for(let i = 0; i < len; i++) {
temp += nums[i]
ret.push(temp)
}
return ret
}
有一个立方体房间,其长度、宽度和高度都等于 n 个单位。请你在房间里放置 n 个盒子,每个盒子都是一个单位边长的立方体。放置规则如下:
你可以把盒子放在地板上的任何地方。
如果盒子 x 需要放置在盒子 y 的顶部,那么盒子 y 竖直的四个侧面都 必须 与另一个盒子或墙相邻。
给你一个整数 n ,返回接触地面的盒子的 最少 可能数量。
示例 1:
输入:n = 3
输出:3
解释:上图是 3 个盒子的摆放位置。
这些盒子放在房间的一角,对应左侧位置。
示例 2:
输入:n = 4
输出:3
解释:上图是 3 个盒子的摆放位置。
这些盒子放在房间的一角,对应左侧位置。
示例 3:
输入:n = 10
输出:6
解释:上图是 10 个盒子的摆放位置。
这些盒子放在房间的一角,对应后方位置。
提示:
1 <= n <= 10^9
根据贪心思想,接触地面的盒子构成的总体形状应该是一个左上三角,这样才可以使得内部的盒子垒起来的高度更高,以保证接触地面盒子数量最小的情况下容纳更多的盒子。我们画出前四层的盒子增长情况,来试探一下有什么规律存在:
var minimumBoxes = function(n) {
let cur = 1, i = 1, j = 1;
while (n > cur) {
n -= cur;
i++;
cur += i;
}
cur = 1;
while (n > cur) {
n -= cur;
j++;
cur++;
}
return (i - 1) * i / 2 + j;
};
/**
* @param {number} n
* @return {number}
*/
/**
* @param {number} n
* @return {number}
*/
var minimumBoxes1 = function(n) {
// cur记录目前所放置的盒子数,i为当前第j层的盒子数,j为层数
let cur= 0;
let i = 0;
let j = 0;
while(cur < n) {
//层数加一
j++;
//计算当前层的盒子数
i += j;
//计算总共放置的盒子数
cur += i;
}
//如果正好等于,则i就是最底层所放置的盒子数
if (cur == n) {
return i;
}
// 不相等,就就接着放,可以尝试放1,2,3,4,知道curr > n
// 因为curr > n,说明放多了,要减去放的i
cur -= i;
// 因为j层的i不符合,所以退回到上一层的i
i -= j;
// j此时代表,继续放置盒子的第几次,第i次可以放i个
j = 0;
while(cur < n) {
j++;
cur += j;
}
// 一共放了j次,最底层放了j个盒子,直接加上之前的i返回
return i+j;
};