LeetCode笔记
- 数组哈希表
- [1.两数之和]( https://leetcode-cn.com/problems/two-sum/)
- [167.两数之和 II - 输入有序数组](https://leetcode-cn.com/problems/two-sum-ii-input-array-is-sorted/)
- 15.三数之和
- [1010. 总持续时间可被 60 整除的歌曲](https://leetcode-cn.com/problems/pairs-of-songs-with-total-durations-divisible-by-60/)
- [1011. 在 D 天内送达包裹的能力](https://leetcode-cn.com/problems/capacity-to-ship-packages-within-d-days/)
- [面试题04.04. 检查平衡性](https://leetcode-cn.com/problems/check-balance-lcci/)
- [面试题 04.05. 合法二叉搜索树](https://leetcode-cn.com/problems/legal-binary-search-tree-lcci/)
- [22. 括号生成](https://leetcode-cn.com/problems/generate-parentheses/)
- [136. 只出现一次的数字](https://leetcode-cn.com/problems/single-number/)
- [1319. 连通网络的操作次数](https://leetcode-cn.com/problems/number-of-operations-to-make-network-connected/)
- [11. 盛最多水的容器](https://leetcode-cn.com/problems/container-with-most-water/)
- [523. 连续的子数组和](https://leetcode-cn.com/problems/continuous-subarray-sum/)
- [105. 从前序与中序遍历序列构造二叉树](https://leetcode-cn.com/problems/construct-binary-tree-from-preorder-and-inorder-traversal/)
- [974. 和可被 K 整除的子数组](https://leetcode-cn.com/problems/subarray-sums-divisible-by-k/)
- [238. 除自身以外数组的乘积](https://leetcode-cn.com/problems/product-of-array-except-self/)
- 分治
- 动态规划
- 300.最长上升子序列
- 1143.最长公共子序列
- [5. 最长回文子](https://leetcode-cn.com/problems/longest-palindromic-substring/)
- [198. 打家劫舍](https://leetcode-cn.com/problems/house-robber/)
- [213. 打家劫舍 II](https://leetcode-cn.com/problems/house-robber-ii/)
- [面试题 08.11. 硬币](https://leetcode-cn.com/problems/coin-lcci/)
- [139. 单词拆分](https://leetcode-cn.com/problems/word-break/)
- [面试题 17.13. 恢复空格](https://leetcode-cn.com/problems/re-space-lcci/submissions/)
- 174.地下城勇士
- 单调栈
- 回溯问题
- 滑动窗口
- [3. 无重复字符的最长子串](https://leetcode-cn.com/problems/longest-substring-without-repeating-characters/)
- [424. 替换后的最长重复字符](https://leetcode-cn.com/problems/longest-repeating-character-replacement/)
- [209. 长度最小的子数组](https://leetcode-cn.com/problems/minimum-size-subarray-sum/)
- [76. 最小覆盖子串](https://leetcode-cn.com/problems/minimum-window-substring/)
- dfs、bfs
- 236.二叉树的最近公共祖先
数组哈希表
1.两数之和
暴力解法需要两遍遍历,时间复杂度O(n2)。使用哈希表的思想将目标差值对应的数组序号存储起来。如果当前的nums[i]没有找到目标数字时将nums[i]的位置i存储在数组arr[target - nums[i]]的位置,也就是说若存在数字target - nums[i],则进行匹配时可以直接找到与其匹配的nums[i]的位置i
var twoSum = function(nums, target) {
var arr = []
for(i = 0; i < nums.length;i++){
if(arr[nums[i]] != undefined && arr[nums[i]] != i){
//保证数字不重复使用,但是这里只遍历一次可以不需要
return[arr[nums[i]], i]
}
else{
arr[target - nums[i]] = i
}
}
};
167.两数之和 II - 输入有序数组
利用有序数组的性质,从数组的两端逼近目标target
var twoSum = function(nums, target) {
var left = 0
var right = nums.length - 1
while(left < right){
if(nums[left] + nums[right] == target){
return[left + 1, right + 1]
}
else if (nums[left] + nums[right] > target){
right--
}
else if (nums[left] + nums[right] < target){
left++
}
}
};
15.三数之和
哈希表
如果考虑先挑取一个数字,再找剩下两数之和为当前数字相反数可以预估时间复杂度为O(n2)。因此不妨先将数据排序O(logn)对整体时间复杂度不会产生太多影响。之后运用167中的想法,有序数组寻找两数之和为target可以使用指针前后逼近的方法,时间复杂度为O(n),空间复杂度O(1),这样总体时间复杂度也为O(n2),需要注意去重。
js排序
array.sort(compare_fun)
compare_fun(a,b){//升序
a - b
}
function compare(a,b){
return a-b
}
var threeSum = function(nums) {
nums.sort(compare)
var ans = []
for(var i = 0;i < nums.length; i++){
if(nums[i] > 0) //最小的数大于0之后就不用考虑了
break
if(nums[i] == nums[i - 1]) //若有连续相同数字则跳过
continue
var left = i + 1
var right = nums.length - 1
while(left < right){
if(nums[i] + nums[left] + nums[right] == 0){
ans.push([nums[i], nums[left], nums[right]])
do{
//找到一组之后需要继续移动指针,跳过那些重复的数字,同时保证数组不越界
left++
}while(left < right && nums[left] == nums[left - 1])
do{
right--
}while(left < right && nums[right] == nums[right + 1])
}
else if (nums[i] + nums[left] + nums[right] > 0){
right--
}
else if(nums[i] + nums[left] + nums[right] < 0){
left++
}
}
}
return ans
};
1010. 总持续时间可被 60 整除的歌曲
使用一个大小为60的数组将所有歌曲时长%60的情况都保存下来,找到两数之和能被60整除也就相当于找到两数分别除60的余数之和等于60。余数等于30和0的情况要单独考虑。
var numPairsDivisibleBy60 = function(time) {
var map = []
var ans = 0
for(let j = 0; j <= 60;j++){
map[j] = 0
}
for(let i = 0; i < time.length;i++){
map[time[i] % 60] ++
}
for(let i = 0; i < 60; i++){
let temp = 60 - (i % 60)
if(temp == i || temp == 60){
ans += (map[i] - 1) * map[i]
}
else ans += map[temp] * map[i]
}
return ans/2
};
1011. 在 D 天内送达包裹的能力
贪心+二分查找
因为weights本身是无序且固定的,所以重量上没有规律,这时装载货物需要使用贪心策略。但又不知道每天装载能力,所以需要对装载能力进行尝试,如果假定的装载能力所需天数少于指定天数说明应减小装载能力,如果大于则需要增加,如果等于时也应该继续减小,看更小的装载能力能否完成同样的任务。而寻找合适的装载能力时采用二分查找的策略。
function canship(weights, vol, D){ //D天内能否运完
var day = 1, temp = 0
for(let i = 0;i < weights.length; i++){
if(temp + weights[i] <= vol){
temp += weights[i]
}
else{
day++
temp = weights[i]
}
}
return day <= D
}
var shipWithinDays = function(weights, D) {
var min = 0,max = 0
for(let i = 0;i < weights.length; i++){
if(weights[i] > min)
min = weights[i]
max += weights[i]
}
while(min < max){
mid = parseInt((min + max) / 2)
if(canship(weights, mid, D)){
max = mid
}
else if(!canship(weights, mid, D)){
min = mid + 1
}
}
return min
};
面试题04.04. 检查平衡性
dfs
递归的方式实现深度遍历,边界条件是如果树不存在则高度是0,否则数的高度等于max(左子树高度, 右子树高度) + 1。在深度遍历过程中判断如果左子树高度和右子树高度差是否大于1。
var isBalanced = function(root) {
flag = true
checkHeight(root)
return flag
};
function checkHeight(tree){
if(tree === null)
return 0
else{
let l = checkHeight(tree.left)
let r = checkHeight(tree.right)
if(Math.abs(l - r) > 1){
flag = false
}
return Math.max(l, r) + 1
}
}
面试题 04.05. 合法二叉搜索树
二叉搜索树:对于树中的每个节点X,它的左子树中所有节点值小于X的关键字值,而它的右子树中所有节点值大于X的节点值。
判断一个树是否为二叉搜索树可以进行中序遍历,对二叉搜索树进行中序遍历会得到一个单调递增的数列。
var isValidBST = function(root) {
var arr = []
check(root, arr)
if(arr.length == 0 || arr.length == 1)
return true
else{
for(let i = 1; i < arr.length; i++){
if(arr[i - 1] >= arr[i])
return false
}
}
return true
};
function check(node, arr){
if(node === null) return
else{
if(node.left !== null) check(node.left, arr)
arr.push(node.val)
if(node.right !== null) check(node.right, arr)
}
}
22. 括号生成
dfs
方法一
递归实现DFS,因为只有2种符号,所以所有可能的字符串可以用二叉树来表示,左子树代表’(’,右子树代表’)’,通过深度遍历生成所有可能的字符串。
首先确定递归的边界条件,如果剩余的’(‘和’)‘都为0时递归结束,得到的就是目标字符串,由于括号匹配的性质,满足要求的字符串一定保证随时’(‘的个数大于’)'的个数,也就是剩余左括号的个数小于右括号的个数。满足这个要求的情况下可以任意添加括号。
var generateParenthesis = function(n) {
var ans = []
dfs('', ans, n, n)
return ans
};
function dfs(str, ans, left, right){
if(left == 0 && right == 0){
ans.push(str)
return
}
if(right < left){
return
}
if(left > 0){
dfs(str + '(', ans, left - 1, right)
}
if(right > 0 && left < right){
dfs(str + ')', ans, left, right - 1)
}
}
方法二
动态规划
136. 只出现一次的数字
位运算
方法一
使用哈希表,时间复杂度O(n),但是要使用额外的内存存储哈希表。
方法二
位运算,使用亦或进行运算,因为异或满足交换律所以异或得到的结果就是只出现一次的数字
var singleNumber = function(nums) {
var ans = nums[0]
for(let i = 1; i < nums.length; i++){
ans ^= nums[i]
}
return ans
};
1319. 连通网络的操作次数
方法一
深度遍历
方法二
并查集
题目给了一些点,给了这些点间的连通关系,需要找出共有几个独立的连通图,也就是图中和另外一个图不相连。
并查集的思想就是开始时所有的点均为独立的集合,然后根据给出的连通关系将这些点合并,每一次将两个集合合并,独立的集合就-1。最终得到共有几个独立集合。
为了能够分辨两个点是不是属于一个集合,我们需要根据连通图的性质将连通图内所有的点都指向其中一个点,这样只要通过find(),如果a和b在一个集合内,那么find(a)等于find(b)。
如果两个点间存在一条边,但是find(a)!=find(b)那么这条边将a所在的集合和b所在的集合合并成为一个集合。
var makeConnected = function(n, connections) {
var fa = [], count = n
if(connections.length < n - 1) return -1
for(let i = 0; i < n; i++){
fa[i] = i
}
for(let i = 0; i < connections.length; i++){
let a = find(connections[i][0],fa)
let b = find(connections[i][1],fa)
if(a != b){
fa[a] = b
count --
}
}
return count - 1
};
function find(root, fa){
var temp = root
while(root != fa[root]){
root = fa[root]
}
while(temp != root){
//路径压缩,避免a->b->c->d的情况,改为a->d,b->d,c->d,这样减少查找时间
let last = temp
temp = fa[temp]
fa[last] = root
}
return root
}
11. 盛最多水的容器
双指针
使用双指针的思想,从左右不断向中间逼近。每次比较left和right的高度,然后选择较短的向中间靠近。因为只有使较短的一边更长才有可能是整体容积更大,使较长的一边更长但是短的一边不变无法增大容积。
var maxArea = function(height) {
var left = 0, right = height.length - 1
var ans = Math.min(height[left], height[right]) * (right - left)
while(left < right){
if(height[left] < height[right]){
let templ = left
while(height[++templ] <= height[left] && templ < right){}
left = templ
}
else{
let tempr = right
while(height[--tempr] <= height[right] && tempr > left){}
right = tempr
}
let temp = Math.min(height[left], height[right]) * (right - left)
if(temp > ans)
ans = temp
}
return ans
};
523. 连续的子数组和
使用前缀+哈希表的方法,题目中目标数连续子数组和为目标nk,如果存在这样的连续子数组i到j,那么sum(i - 1) = 前i - 1项的和,sum(j) = sum(i - 1) + nk。因此sum(i - 1) % k = sum(j) % k,所以我们可以转化为使用哈希表存储key:sum(i) % k, val: i,如果存在i 和 j使得j - i大于1,那么就符合题目要求。注意对k=0进行特殊判断,0不能为除数。
var checkSubarraySum = function(nums, k) {
var map = [], sum = 0
if(k == 0){
let count = 0
for(let i = 0; i < nums.length; i++){
if(nums[i] == 0)
count++
else
count = 0
if(count > 1) return true
}
return false
}
map[0] = -1 //如果子数组i到j中i=0时,i - 1要存在即为-1,nums[-1]不存在相当于0,所以余数也为0
for(let i = 0; i < nums.length; i++){
sum += nums[i]
if(map[sum % k] == undefined){
map[sum % k] = i
}
else if(i - map[sum % k] > 1){
return true
}
}
return false
};
105. 从前序与中序遍历序列构造二叉树
通过前序遍历确定根节点,因为前序遍历总是先遍历根节点。再在中序遍历中找到根节点位置,根节点左边就是左子树,右边就是右子树。这样就能知道左右子树的节点数量,再在前序遍历中分别找到左子树和右子树的根节点,重复上述操作,边界条件是preorder长度为0。
var buildTree = function(preorder, inorder) {
if(preorder.length == 0){
return null
}
var root = new TreeNode(preorder[0])
let left = inorder.indexOf(preorder[0])
root.left = buildTree(preorder.slice(1, left + 1), inorder.slice(0, left))
root.right = buildTree(preorder.slice(1 + left), inorder.slice(left + 1))
return root
};
974. 和可被 K 整除的子数组
通过寻找连续子数组的要求可以想到使用哈希表记录前缀和的方式,题目中要求子数组sum[i-j] % K == 0, sum[ i - j ] = sum[ j ] - sum[ i - 1],所以可以转为(sum[ j ] - sum[ i - 1])% K = 0。即寻找sum[ j ] % K = sum[ i ] % K的个数。
通过分析一次遍历将相同余数的个数进行记录,注意题目中余数可能为负数,这时候需要通过(余数 + K) % K的方式将余数取正。
var subarraysDivByK = function(A, K) {
var mod = [],sum = 0, ans = 0
mod[0] = 1 //当i = 0时sum[i - 1] = sum[-1] = 0,sum[-1] % k = 0
for(let i = 0; i < A.length; i++){
sum += A[i]
let temp = (sum % K + K) % K
if(mod[temp] == undefined)
mod[temp] = 0
mod[temp]++
}
for(num of mod){
if(num > 1){
ans += (num - 1) * num / 2
}
}
return ans
};
238. 除自身以外数组的乘积
第i个位置的乘积 = 左边元素乘积 * 右边元素乘积
所以只要一次遍历记录左边元素乘积和右边元素乘积即可
var productExceptSelf = function(nums) {
var left = [1], right = [1], len = nums.length
var ans = []
for(let i = 0; i < len; i++){
left[i + 1] = left[i] * nums[i]
right[i + 1] = right[i] * nums[len - i - 1]
}
for(let i = 0; i < len; i++){
ans.push(left[i] * right[len - i - 1])
}
return ans
};
分治
数组中的逆序对
使用归并排序
的思想,当merge时如果右边arr2大于左边arr1,那么这个时候说明arr1中剩余未合并的数字都要比arr2中即将要合并的数字大,就在count上加上左边剩余数字个数
var count
var reversePairs = function(nums) {
count = 0
mergeSort(nums, 0, nums.length - 1)
// console.log(nums)
return count
};
function mergeSort(nums, left, right){
if(left < right){
let mid = parseInt((left + right) / 2)
mergeSort(nums, left, mid)
mergeSort(nums, mid + 1, right)
merge(nums, left, mid, right)
}
}
function merge(nums, left, mid, right){
let arr1 = nums.slice(left, mid + 1)
let arr2 = nums.slice(mid + 1, right + 1)
let p1 = 0, p2 = 0
while(left <= right){
if((p1 < arr1.length && arr1[p1] <= arr2[p2]) || p2 == arr2.length){
nums[left] = arr1[p1++]
}
else if((p2 < arr2.length && arr1[p1] > arr2[p2]) || p1 == arr1.length){
nums[left] = arr2[p2++]
count += (arr1.length - p1)
}
left++
}
}
312. 戳气球
将减少气球转变为添加气球进行计算。向区间(left, right)中添加i,这时候值增加nums[left] * nums[right] * nums[i],并将问题分为(left, i), (i, right)两个子问题,再进行计算。
var maxCoins = function(nums) {
let score = [], val
val = [1, ...nums, 1]
for(let i = 0; i < nums.length + 3; i++){
score[i] = new Array(nums.length + 2).fill(-1)
}
return devide(val, score, 0, nums.length + 1)
};
function devide(nums, score, left, right){
if(left >= right - 1)
return 0
if(score[left][right] != -1)
return score[left][right]
for(let i = left + 1; i < right; i++){
let sum = nums[left] * nums[i] * nums[right]
sum += devide(nums, score, left, i) + devide(nums, score, i, right)
score[left][right] = Math.max(sum, score[left][right])
}
return score[left][right]
}
动态规划
300.最长上升子序列
var lengthOfLIS = function(nums) {
if (!nums.length) {
return 0
}
let dp = []
dp[0] = 1
for (let i = 1; i < nums.length; i++) {
let max = 0
dp.forEach((val, index) => {
if (nums[index] < nums[i] ) {
max = Math.max(max, dp[index])
}
})
dp[i] = max + 1
}
return Math.max(...dp)
};
1143.最长公共子序列
var longestCommonSubsequence = function(text1, text2) {
let m = text1.length, n = text2.length
let dp = new Array(m + 1)
for (let i = 0; i < dp.length; i++) {
dp[i] = new Array(n + 1).fill(0)
}
dp[0][0] = dp[1][0] = dp[0][1] = 0
for (let i = 1; i <= m; i++) {
for (let j = 1; j <= n; j++) {
if (text1[i - 1] === text2[j - 1]) {
dp[i][j] = dp[i - 1][j - 1] + 1
} else {
dp[i][j] = Math.max(dp[i - 1][j], dp[i][j - 1])
}
}
}
return dp[m][n]
};
5. 最长回文子
dp[ i ][ j ]记录子串 str[ i - j ] 是否为回文串,
当子串长度为1时,dp[ i ][ i ]均为回文串;
当子串长度为2时,判断 str[ i ] 是否等于 str[ j ],如果等于则为回文串;
当子串长度大于2时,状态转移方程dp[ i ][ j ] = str[ i ] == str[ j ] && dp[ i + 1][ j - 1]
var longestPalindrome = function(s) {
var max = 0, l
var dp = []
for(let len = 1; len <= s.length; len++){
for(let i = 0; i < s.length - len + 1; i++){
j = i + len - 1
if(len == 1){
if(dp[i] == undefined) dp[i] = []
dp[i][j] = true
}
else if(len == 2){
dp[i][j] = (s[i] == s[j]) ? true : false
}
else{
dp[i][j] = (s[i] == s[j] && dp[i + 1][j - 1])
}
if(len > max && dp[i][j]){
max = len
l = i
}
}
}
return s.slice(l, l + max)
};
198. 打家劫舍
动态规划+滚动数组
dp[ k ] = max(dp[ k - 2 ] + num[ k ], dp[ k - 1 ])
数组长度为0和1时单独处理
因为dp[ k ]只由dp[ k - 1 ]和dp[ k - 2 ]决定,所以只需要两个变量保存最新的dp[ k - 1 ]和dp[ k - 2 ],这样空间复杂度为O(1)
var rob = function(nums) {
var len = nums.length
var first = 0, second = 0
if(len == 0) return 0
if(len == 1) return nums[0]
first = Math.max(nums[0], nums[1])
second = nums[0]
for(let i = 2; i < len; i++){
let temp = first
first = Math.max(first, second + nums[i])
second = temp
}
return first
};
相似题目
740. 删除与获得点数
面试题46. 把数字翻译成字符串
213. 打家劫舍 II
这道题和前一道题区别在于头和尾连在了一起,所以头和尾之间只能选择一个。那么只要考虑sum(0, n - 1)和sum(1, n)就可以了,结果等于两个中更大的。
面试题 08.11. 硬币
dp[ i ]记录组成i有多少种方法,
状态转移方程:dp[ j ] = (dp[ j ] + dp[ j - coins[ i ] ])
需要注意的是如果外层循环是1 - n内层循环是4种硬币会造成6可能是1+5,也可能是5 + 1。为了避免这种情况,外层遍历硬币种类,先将只由1组成的情况计算,再加上由1和5组成的,再加上由1、5、10组成的,以此类推这样可以避免重复。
var waysToChange = function(n) {
var dp = [], coins = [1, 5, 10, 25]
dp[0] = 1
for(let i = 0; i < 4; i++){
for(let j = coins[i]; j <= n; j++){
if(dp[j] == undefined)
dp[j] = 0
dp[j] = (dp[j] + dp[j - coins[i]]) % 1000000007
}
}
return dp[n]
};
139. 单词拆分
dp[ i ]记录前i个字母是否能拆分成功,若dp[ i ]可拆分则需要前j个字母可拆分,即dp[ j ] == true且字母j + 1到字母i需要是wordDict中的一个词。
var wordBreak = function(s, wordDict) {
let map = []
for(let i = 0; i < wordDict.length; i++){
let start = 0
while(s.indexOf(wordDict[i], start) != -1){
let temp = s.indexOf(wordDict[i], start)
if(map[temp + 1] == undefined)
map[temp + 1] = []
map[temp + 1].push(temp + wordDict[i].length)
start = temp + 1
}
}
let dp = []
dp[0] = true
for(let i = 1; i <= s.length; i++){
for(let j = 0; j < i; j++){
if(dp[j] && map[j + 1] && map[j + 1].indexOf(i) != -1){
dp[i] = true
}
}
}
return dp[s.length] == undefined ? false : true
};
面试题 17.13. 恢复空格
var respace = function(dictionary, sentence) {
let dp = new Array(sentence.length + 1).fill(0) // dp[i]保存前i个字符匹配到的最长的字符数
let root = new TrieNode()
let map = []
for(let i = 0; i < dictionary.length; i++){ //遍历dictionary建立字典树
buildTrie(dictionary[i], root)
map[dictionary[i].length] = true
}
return dp[dp.length - 1];
for(let i = 1; i <= sentence.length; i++){
dp[i] = dp[i - 1]
for(let j = 0; j < i; j++){
if(map[i - j]){
if(checkTrie(sentence.substring(j, i), root)){
dp[i] = Math.max(dp[i], dp[j] + i - j)
}
}
}
}
return sentence.length - dp[sentence.length]
};
function TrieNode(char, isEnd){ //字典树节点
this.val = char
this.isEnd = isEnd //当前节点是否为出口
this.children = [] //子节点集合
}
function buildTrie(str, root){ //将str加入字典树中
let start = 0
while(true){ // 尝试在当前字典树中匹配str,得到str在字典树中能匹配的最大长度
let flag = false
for(let item of root.children){
if(item.val == str[start]){
start++
root = item
flag = true
break
}
}
if(!flag || start == str.length) break //如果出现不匹配或者完全匹配退出循环
}
for(; start < str.length; start++){ //将str不匹配的部分从当前位置加入字典树
let node = new TrieNode(str[start], false)
root.children.push(node)
root = node
}
root.isEnd = true //将字典树中str匹配的结尾设置为一个出口
}
function checkTrie(str, root){ //检查str是否在字典树中
let start = 0
while(true){
let flag = false
for(let item of root.children){
if(item.val == str[start]){
start++
root = item
flag = true
break
}
}
if(start == str.length) //如果成功匹配且当前节点是一个出口返回true,如果不是出口说明字典树中没有这个词返回false
return root.isEnd ? true : false
if(!flag) return false //匹配不成功返回false
}
}
174.地下城勇士
反向dp,因为正向dp过程中不满足后无效性。即当前位置的最优状态不仅由之前的状态决定,还由之后的状态决定。
var calculateMinimumHP = function(dungeon) {
let dp = [], x = dungeon.length, y = dungeon[0].length
for(let i = 0; i <= x; i++){
dp[i] = new Array(y + 1).fill(999999)
}
dp[x - 1][y] = 1, dp[x][y - 1] = 1
for(let i = x - 1; i >= 0; i--){
for(let j = y - 1; j >= 0; j--){
let min = Math.min(dp[i][j + 1], dp[i + 1][j])
dp[i][j] = Math.max(min - dungeon[i][j], 1)
}
}
return dp[0][0]
};
单调栈
739. 每日温度
单调栈+哈希表
当温度开始下降时将温度和索引压栈。当出现上升时,此时栈中为单调递减,当栈顶值大于当前值时依次出栈。最后留在栈内的就是无法上升的。
var dailyTemperatures = function(T) {
if(T.length == 1) return 0
var stack = [], ans = []
for(let i = 0; i < T.length - 1; i++){
if(T[i + 1] <= T[i]){
stack.push({val: T[i], idx: i})
}
else{
ans[i] = 1
while(stack.length && stack[stack.length - 1].val < T[i + 1]){
ans[stack[stack.length - 1].idx] = i + 1 - stack[stack.length - 1].idx
stack.pop()
}
}
}
while(stack.length){
ans[stack[stack.length - 1].idx] = 0
stack.pop()
}
ans[T.length - 1] = 0
return ans
};
84. 柱状图中最大的矩形
var largestRectangleArea = function(heights) {
if(heights.length == 0) return 0
heights = [0, ...heights, 0]
var stack = [0], ans = 0, top = 0
for(let i = 1; i < heights.length; i++){
if(heights[i] >= heights[stack[top]]){
stack.push(i)
top++
}
else{
while(heights[stack[top]] > heights[i]){
let h = heights[stack[top]]
stack.pop()
top--
let width = i - stack[top] - 1
if(h * width > ans)
ans = h * width
}
stack.push(i)
top++
}
}
return ans
};
回溯问题
46. 全排列
回溯问题重点是记录上一步的状态,当递归结束时能重新返回到上一步的状态。
在本题中通过栈的出栈实现,每个子递归结束后pop出栈还原前一步的状态
var permute = function(nums) {
var ans = [],arr = []
generate(nums, arr, ans)
return ans
};
function generate(nums, arr, ans){
if(arr.length == nums.length){
ans.push([...arr])
return
如果直接ans.push(arr),这时ans中存放的是arr的地址,最后输出会为[[],[],[],[],[]]
因为递归结束后arr为[],所以ans中也为[]
这里通过解构赋值对数据进行拷贝
ps:
let a = []
let b = [1,2,3]
a.push(b)
b = [1,2]
console.log(a) // [1,2,3] 因为b = [1,2]的过程相当于new了一个新数组,
//b的地址指向已经改变了,而a中的数组地址所指向的内存并没有改变,所以a还是[1,2,3]
}
for(let i = 0; i < nums.length; i++){
if(arr.includes(nums[i])) continue
arr.push(nums[i])
generate(nums, arr, ans)
arr.pop()
}
}
滑动窗口
3. 无重复字符的最长子串
滑动窗口
使用滑动窗口的思想,这里直接在原字符串上进行裁剪并使用indexOf会节约空间和时间,每当发现出现同样的字符即字符在原字符串中索引不同时对字符串进行滑动至上一次出现该字符的下一个字符位置。
var lengthOfLongestSubstring = function(s) {
var max = 0,str = ''
for(let i = 0;i < s.length; ){
let idx = s.indexOf(s[i])
if(idx != i){
s = s.slice(idx + 1)
i = i - idx
}
else{
if(i + 1 > max) max = i + 1
i++
}
}
return max
};
424. 替换后的最长重复字符
通过哈希表来记录当前滑动窗口中出现的字母个数
通过维护 max
来记录当前滑动窗口中出现的最多的字母个数
当快慢指针之间的距离 - max > k 说明已经达到最多替换次数,于是将慢指针右移
var characterReplacement = function(s, k) {
let a = b = 0, len = s.length, max = 0
let map = []
while (b < len) {
map[s[b]] = map[s[b]] === undefined ? 1 : map[s[b]] + 1
max = Math.max(max, map[s[b]])
if (b - a + 1 - max > k) {
map[s[a]]--
a++
}
b++
}
return b - a
};
209. 长度最小的子数组
滑动窗口
- 当当前和小于s时,向右扩张right++
- 当当前和小于等于s时左边收缩left++,并更新ans
var minSubArrayLen = function(s, nums) {
if(nums.length == 0) return 0
let left = 0, right = 0, sum, ans = nums.length + 1
sum = nums[0]
while(right < nums.length){
while(sum < s && right < nums.length){
right++
sum += nums[right]
}
while(sum >= s && left <= right){
if(right - left + 1 < ans){
ans = right - left + 1
}
sum -= nums[left]
left++
}
}
return ans == nums.length + 1 ? 0 : ans
};
76. 最小覆盖子串
总体思路是通过双指针实现滑动窗口,如果左右指针间的字符串不能够包含目标字符串t则使右指针right++向右滑动扩大窗口,如果当前字符串已经包含t则使左指针left++向右滑动缩小窗口,这样重复操作能够遍历所有包含目标t的最短子串,在这些子串中记录最短的。
如何判断当前子串包含目标字符串t是一个涉及细节较多的地方,首先在初始化时使用哈希表map记录t中字母,值为在当前子串中出现过几次,规定默认为0次,若map[i]值为1则子串中包含该字符。但是对于t中出现次数大于1的字符比如“aaa”,在初始化时将其值设为-2,这样同样保证当map[i]值为1时包含3个a。
在滑动窗口的过程中统计已包含的目标字符串中字符个数,如果等于t.length则包含完整的t。map[i] >= 1说明子串中包含字符i,可能不止一个。map[i] <= 0说明子串中字符i个数仍不够,可能差不止一个。
var minWindow = function(s, t) {
var map = []
for(let i = 0; i < t.length; i++){
if(map[t[i]] != undefined){
map[t[i]]--
}
else map[t[i]] = 0
}
var left = -1, right = -1, count = 0, ansleft = -1, ansright = -1, min = s.length + 1
while(right < s.length){
if(count < t.length){
right++
if(map[s[right]] != undefined){
if(map[s[right]] <= 0) count++
map[s[right]]++
}
}
else{
if(right - left < min){
min = right - left
ansleft = left
ansright = right
}
if(map[s[left + 1]] >= 1){
if(map[s[left + 1]] == 1) count--
map[s[left + 1]]--
}
left++
}
}
return s.slice(ansleft + 1, ansright + 1)
};
dfs、bfs
非递归二叉树前序遍历
var preorderTraversal = function(root) {
let stack = [], ans = []
while (root || stack.length) {
while (root) {
ans.push(root.val)
stack.push(root)
root = root.left
}
if (stack.length) {
root = stack.pop()
root = root.right
}
}
return ans
};
非递归二叉树中序遍历
var inorderTraversal = function(root) {
let stack = [], ans = []
while(root || stack.length){
while(root){
stack.push(root)
root = root.left
}
if(stack.length){
root = stack.pop()
ans.push(root.val)
root = root.right
}
}
return ans
};
非递归二叉树后序遍历
var postorderTraversal = function(root) {
let stack = [], ans = []
let p = root, r = null
while(p || stack.length){
if(p){
stack.push(p)
p = p.left
}
else {
p = stack[stack.length - 1]
if(p.right && p.right != r){
p = p.right
}
else {
p = stack.pop()
ans.push(p.val)
r = p
p = null
}
}
}
return ans
};
329. 矩阵中的最长递增路径(记忆化dfs)
dfs搜索过程中每搜完一个格就把结果记录下来,避免重复dfs
var longestIncreasingPath = function(matrix) {
let memo = new Array(matrix.length)
for (let i = 0; i < memo.length; i++) {
memo[i] = new Array(matrix[0].length).fill(0)
}
let max = 0
for(let i = 0; i < matrix.length; i++) {
for (let j = 0; j < matrix[0].length; j++) {
let temp = dfs(matrix, memo, i, j)
if(temp > max)
max = temp
}
}
return max
};
function dfs(matrix, memo, i, j) {
let max = 1
if(i > 0){
if(matrix[i - 1][j] > matrix[i][j]) {
let temp = memo[i - 1][j] == 0 ? dfs(matrix, memo, i - 1, j) + 1 : memo[i - 1][j] + 1
if(temp > max) max = temp
}
}
if(i < matrix.length - 1) {
if(matrix[i + 1][j] > matrix[i][j]) {
let temp = memo[i + 1][j] == 0 ? dfs(matrix, memo, i + 1, j) + 1 : memo[i + 1][j] + 1
if(temp > max) max = temp
}
}
if(j > 0){
if(matrix[i][j - 1] > matrix[i][j]) {
let temp = memo[i][j - 1] == 0 ? dfs(matrix, memo, i , j - 1) + 1 : memo[i][j - 1] + 1
if(temp > max) max = temp
}
}
if(j < matrix[0].length - 1) {
if(matrix[i][j + 1] > matrix[i][j]) {
let temp = memo[i][j + 1] == 0 ? dfs(matrix, memo, i , j + 1) + 1 : memo[i][j + 1] + 1
if(temp > max) max = temp
}
}
memo[i][j] = max
return max
}
236.二叉树的最近公共祖先
find
函数寻找当前树中是否存在p
或q
l && r
: 如果当前节点的左子树存在p或q,且右子树存在p或q,那么说明当前节点是公共祖先
((root.val === p.val || root.val === q.val) && (l || r))
: 如果当前节点是p或q,且左右子树中存在p或q,那么当前节点就是公共祖先
var lowestCommonAncestor = function(root, p, q) {
let ans = null
function find (root, p, q) {
if (root === null) {
return false
}
let l = find (root.left, p, q)
let r = find (root.right, p, q)
if ((l && r) || ((root.val === p.val || root.val === q.val) && (l || r))) {
ans = root
}
return l || r || (root.val === p.val || root.val === q.val)
}
find(root, p, q)
return ans
};
function merge (left, mid, right, nums) {
let arr1, arr2
arr1 = nums.slice(left, mid + 1)
arr2 = nums.slice(mid + 1, right + 1)
let p1 = 0, p2 = 0
while(left <= right) {
if ((arr1[p1] <= arr2[p2] && p1 < arr1.length) || p2 === arr2.length) {
nums[left] = arr1[p1]
p1++
}
else if ((arr1[p1] > arr2[p2] && p2 < arr2.length) || p1 === arr1.length) {
nums[left] = arr2[p2]
p2++
}
left++
}
}
function mergeSort(nums, left, right) {
if (left < right) {
let mid = Math.floor((left + right) / 2)
mergeSort(nums, left, mid)
mergeSort(nums, mid + 1, right)
merge(left, mid, right, nums)
}
}
function quicksort(arr, low, high){
var left, right, temp
left = low
right = high
if(low < high){
temp = arr[low]
while(left != right){
while(arr[right] >= temp && right > left){
right--
}
if(left < right){
arr[left] = arr[right]
left++
}
while(arr[left] <= temp && left < right){
left++
}
if(left < right){
arr[right] = arr[left]
right--
}
}
arr[left] = temp
quicksort(arr, low, left - 1)
quicksort(arr, left + 1, high)
}
}