二叉树的所有路径
- 这道题如果在空节点return不好写,所以选择在叶子节点return,就是在一个节点的左右孩子均为空时return
- 递归函数的参数除了节点之外,还要有一个字符串curpath保存当前的路径
- 单层递归的逻辑就是给curpath加上node.val+"->"
var binaryTreePaths = function(root) {
//递归遍历+递归三部曲
let res=[];
//1. 确定递归函数 函数参数
const getPath=function(node,curPath){
//2. 确定终止条件,到叶子节点就终止
if(node.left===null&&node.right===null){
curPath+=node.val;
res.push(curPath);
return ;
}
//3. 确定单层递归逻辑
curPath+=node.val+'->';
node.left&&getPath(node.left,curPath);
node.right&&getPath(node.right,curPath);
}
getPath(root,'');
return res;
};
左叶子节点之和
注意一点,只有遍历父节点时,才能知道子节点是不是叶子节点,所以可以省略if(!node.left&&!node.right) return 0
let res = 0
const fn = function(node) {
if(!node) return 0
//if(!node.left&&!node.right) return 0
if(node.left && !node.left.left && !node.left.right) {
res += node.left.val
}
fn(node.left)
fn(node.right)
}
fn(root)
return res
找树左下角的值
首先明确一点,左下角不是左孩子
- 迭代法就是层序遍历后找到最后一层的第一个节点,注意找最后一层的第一个节点的方法是遍历每一层的节点时都保存第一个节点即可
var findBottomLeftValue = function(root) {
const queue = []
let result = null
queue.unshift(root)
while(queue.length) {
let len = queue.length
for(let i = 0; i < len; i++) {
let node = queue.pop()
if(i === 0) result = node.val
if(node.left) queue.unshift(node.left)
if(node.right) queue.unshift(node.right)
}
}
return result
};
- 递归,只需保证左节点优先遍历,并保存深度最大的节点,并回溯
var findBottomLeftValue = function(root) {
//首先考虑递归遍历 前序遍历 找到最大深度的叶子节点即可
let maxPath = 0,resNode = null;
// 1. 确定递归函数的函数参数
const dfsTree = function(node,curPath){
// 2. 确定递归函数终止条件
if(node.left===null&&node.right===null){
if(curPath>maxPath){
maxPath = curPath;
resNode = node.val;
}
// return ;
}
node.left&&dfsTree(node.left,curPath+1);
node.right&&dfsTree(node.right,curPath+1);
}
dfsTree(root,1);
return resNode;
};
路径总和
这道题的遍历顺序不重要,因为中节点没有操作,这题技巧就是用targetSum - 每次遍历的node.val,如果遍历到叶子节点且count === 0,则为true
var hasPathSum = function(root, targetSum) {
if(!root) return false
const fn = function(node, count) {
if(!node.left && !node.right && count === 0) return true
if(!node.left && !node.right) return false
if(node.left) {
count -= node.left.val
if(fn(node.left, count)) return true //递归函数是有返回值的,如果递归函数返回true,说明找到了合适的路径,应该立刻返回
count += node.left.val
}
if(node.right) {
count -= node.right.val
if(fn(node.right, count)) return true
count += node.right.val
}
return false
}
return fn(root, targetSum - root.val)
};
中序后序求树 106
- 第一步:如果数组大小为零的话,说明是空节点了。
- 第二步:如果不为空,那么取后序数组最后一个元素作为节点元素。
- 第三步:找到后序数组最后一个元素在中序数组的位置,作为切割点
- 第四步:切割中序数组,切成中序左数组和中序右数组 (顺序别搞反了,一定是先切中序数组)
- 第五步:切割后序数组,切成后序左数组和后序右数组
- 第六步:递归处理左区间和右区间 在写代码时可以在每次递归时将postorder里要找到中间节点pop出,而在inorder数组里要找的中间节点的INDEX就等于leftInorder的长度,可以用这个INDEX代替长度
var buildTree = function(inorder, postorder) {
if(!postorder.length) return null
let rootVal = postorder.pop()
let rootIndex = inorder.indexOf(rootVal)
const root = new TreeNode(rootVal)
//rootIndex === leftInorder.length
root.left = buildTree(inorder.slice(0, rootIndex), postorder.slice(0, rootIndex))
root.right = buildTree(inorder.slice(rootIndex + 1), postorder.slice(rootIndex))
return root
};
前序中序求树 105
同理
/**
* 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) {
if(!inorder.length) return null
let rootVal = preorder.shift()
let rootIndex = inorder.indexOf(rootVal)
let root = new TreeNode(rootVal)
root.left = buildTree(preorder.slice(0, rootIndex), inorder.slice(0, rootIndex))
root.right = buildTree(preorder.slice(rootIndex), inorder.slice(rootIndex + 1))
return root
};
最大二叉树
最大二叉树的定义:
- 二叉树的根是数组中的最大元素。
- 左子树是通过数组中最大值左边部分构造出的最大二叉树。
- 右子树是通过数组中最大值右边部分构造出的最大二叉树。 第一步:确定函数返回类型及函数参数,可以直接用题目的 第二步:确定终止条件,当递归到数组只有一个元素时,就是遍历到叶子节点,返回这个值创建的节点 第三步:单层递归逻辑:找到最大值及其下标,分割左右数组,注意,在分割数组时,需要判断左右数组的大小,即最大值的下标不能等于0、length-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[]} nums
* @return {TreeNode}
*/
var constructMaximumBinaryTree = function(nums) {
if(nums.length === 1) return new TreeNode(nums[0])
let rootVal = 0, rootIndex = 0, maxVal = 0
for(let i = 0; i < nums.length; i++) {
if(nums[i] > maxVal) {
maxVal = nums[i]
rootIndex = i
}
}
rootVal = maxVal
// get rootVal rootIndex
const root = new TreeNode(rootVal)
if(rootIndex > 0) {
const leftArr = nums.slice(0, rootIndex)
root.left = constructMaximumBinaryTree(leftArr)
}
if(rootIndex < nums.length - 1) {
const rightArr = nums.slice(rootIndex + 1)
root.right = constructMaximumBinaryTree(rightArr)
}
return root
};
合并二叉树
这道题需要同时递归两条二叉树,其实就是在确定终止条件和单层递归要判断两条二叉树,同时这里直接在root1二叉树上修改,省去了新建二叉树。
终止条件:因为是传入了两个树,那么就有两个树遍历的节点t1 和 t2,如果t1 == NULL 了,两个树合并就应该是 t2 了啊(如果t2也为NULL也无所谓,合并之后就是NULL)。
反过来如果t2 == NULL,那么两个数合并就是t1(如果t1也为NULL也无所谓,合并之后就是NULL)。
var mergeTrees = function(root1, root2) {
if(!root1) return root2
if(!root2) return root1
root1.val += root2.val
root1.left = mergeTrees(root1.left, root2.left)
root1.right = mergeTrees(root1.right, root2.right)
return root1
};
77组合
一个非常经典的回溯问题,思路就是从数字1开始取,递归下去取到k个就保存并return
去重思路:定义一个startIndex作为单层循环的起点,每当你往下一层就让startIndex+1。
剪枝思路:在单层循环中,若剩余的数量不足以让这个组合长度到k个,就剪去这一枝。
/**
* @param {number} n
* @param {number} k
* @return {number[][]}
*/
var combine = function(n, k) {
let resChild = []
let result = []
const backtracking = function(n, k, startIndex) {
if(resChild.length === k) {
result.push([...resChild])
return
}
for(let i = startIndex; i <= (n - (k - resChild.length) + 1); i ++) {
resChild.push(i)
backtracking(n, k, i + 1)
resChild.pop()
}
}
backtracking(n, k, 1)
return result
};
216 组合总和
剪枝思路:
- 首先是for循环,本来是
i<=n,但是可以加上个数缩小范围,k - resChild.length是还需要的个数,n - (k - resChild) + 1是最大的终点,我的理解是如果reschild里每个都是1,那么i最大就是这个值 - 其次在for循环内加上对和的判断,如果大于n就continue
/**
* @param {number} k
* @param {number} n
* @return {number[][]}
*/
var combinationSum3 = function(k, n) {
const result = []
const resChild = []
let sum = 0
const backTracking = function (k, n, startIndex) {
if(resChild.length === k) {
let sum = 0
resChild.forEach((item) => {
sum += item
})
if(sum === n) {
result.push([...resChild])
}
return
}
for(let i = startIndex; i <= 9 - (k - resChild.length) + 1; i ++) {
if(sum > n || (sum + i) > n) continue
sum += i
resChild.push(i)
backTracking(k, n, i + 1)
resChild.pop()
sum -= i
}
}
backTracking(k, n, 1)
return result
};
17电话号码字母组合
今天写这道题陷入的误区就是想用两层for循环,一层遍历digits的长度,一层遍历每个digit映射的字符串,但是仔细想想,像回溯这种无法在单层函数作用中遍历两层,也就是说,在每层递归函数调用中,只能遍历一层for循环,就是横向遍历一层,纵向的则依赖递归回溯。
还有一个坑就是不能直接对字符串进行减法,这样在js是不行的,会返回NAN
var letterCombinations = function(digits) {
if(!digits) return []
const result = []
const path = []
const stringArr = ['', '', 'abc', 'def', 'ghi', 'jkl', 'mno', 'pqrs', 'tuv', 'wxyz']
const digitsArr = digits.split('').map((item) => {
return stringArr[item]
}) // 数字映射成字符串后的数组
const backTracking = function (digitsArr, path, index) {
if (path.length === digits.length) {
result.push(path.join(''))
return
}
let str = digitsArr[index]
for (let i = 0; i < str.length; i++) {
path.push(str[i])
backTracking(digitsArr, path, index + 1)
path.pop()
}
}
backTracking(digitsArr, path, 0)
return result
};
39 组合总和
剪枝思路:排序后如果和大于target就直接不进入循环
var combinationSum = function(candidates, target) {
const result = []
const path = []
let sum = 0
candidates = candidates.sort((a, b) => a-b)
const backTracking = function(candidates, target, index) {
if(sum > target) return
if(sum === target) {
result.push([...path])
return
}
for(let i = index; i < candidates.length && (sum + candidates[i]) <= target; i++) {
sum += candidates[i]
path.push(candidates[i])
backTracking(candidates, target, i)
path.pop()
sum -= candidates[i]
}
}
backTracking(candidates, target, 0)
return result
};
131 分割回文串
这题分为两步:
- 判断回文串
- 分割字符串
主要讲一下如何分割,首先我们需要一个startIdx作为每层遍历的起点,在遍历过程中,我们判断起点startIdx到i是否为回文串,如果不是就直接continue,如果是,则放进数组后继续往下递归,结合图理解
40 组合问题2
这道题的难点在于,你需要保证同一树层的选取的元素不重复,而同一树枝上的元素可以重复,这句话有点绕,下面解析。
首先这些都是基于先对原数组进行排序的前提下。所谓同一树层,就是起点相同的一层for循环。不能重复的条件是什么?第一点,你不是同一树枝下的,也就是说你不是同一个结果里的,不是基于最上方那层(基于第一次选取元素往下递归所得答案)的重复。第二点,因为数组排序过,所以要重复,必定是相邻的重复,而且不是同一树枝,那就是同一层的。举个例子,【1,1,2】,求和为2,第一层第一次选取1,第二层第一次选取1,第一层第二次选取1,此时这两个就重复了,因为此时他们首先不是同一树枝,第二他们i都等于1,所以是同一层,不能重复。
这里创建一个used数组,具体配合代码随想录思考
贪心 455分发饼干
这题主要的问题就是不要用两层for循环,用变量维护饼干下标就好了
贪心 376摆动序列
这道题的思路是维护一个curdiff和一个prediff,不要维护下标,然后还有一个就是处理数组左右两边的技巧,在判断出加上prediff=0的情况,因为curdiff只有在大于0或小于0的情况才会赋值给prediff,所以是只处理特殊情况的
贪心 53最大子序和
这道题首先要想贪心那肯定不能用两层for循环了,不然就是暴力了。用一层for循环,我们就要用一个变量维护和,我这里用sum,当然还要一个变量维护最大的和,我这里用max,我们在每层循环进入时就记录max,然后如果sum是小于0的,直接舍弃,这里舍弃的方式就是把它设为0就好了。这两个顺序不能调换。
贪心 122股票2
总利润最大值 = 每两天的正利润之和
贪心 55跳跃游戏
这题没想到的点就是遍历的范围应该是每一个元素自己能到的范围(nums[i] + i)
贪心 45跳跃2
一开始的错误思路是记录每一步能覆盖的最大范围,如果前一次的最大范围小于这次的覆盖范围,就算作步数加1,这肯定是不合理的
正确的应该是记录当前的覆盖范围,和未达到当前覆盖范围终点前,下一步的最大范围。
值得一提的是,可以只把遍历的范围到达倒数第二个元素,可以省去对最后一步的判断
贪心 1005k次取反
这道题的思路是从小到大排序后让下标小的(从0开始),且为负的取反,同时记录次数(用k--),若取反后全为正数且k不等于0,就再次排序找到最小值取反它消耗次数。
动态规划 509斐波那契
- 确认dp数组下标及其含义:第i个数的斐波那契数值为dp[i]
- 确认递推公式:dp[i] = dp[i-1] + dp[i-2]
- 初始化dp:dp[0] = 0, dp[1] = 1
- 确认递归顺序:从前往后
- 举例推导
动态规划 746爬楼梯最小花费
- 确认dp数组下标及其含义:第i层楼梯的最小花费为dp[i]
- 确认递推公式:dp[i] = Math.min(dp[i-1] + cost[i-1], dp[i-2] + cost[i-2])
- 初始化:dp[0] = 0, dp[1] = 0
- 确认递归顺序:从前往后,注意最后一层
- 举例推导
动态规划 6263路径问题
63我的错误思路是先进行正常计算,算出总路数,再减去起点到障碍物乘障碍物到终点,这样的缺点就是多个障碍物根本没法写,其实把dp数组里障碍物的位置设为0就好了,初始化时也要对障碍物进行判断
动态规划 343 整数拆分
dp[i]的意思就是拆分i得到的最大乘积,递推公式为dp[i] = max(dp[i], max(dp[i-j] * j, (i-j) * j), 遍历的过程中,j已经算被拆分过,所以无需dp[j]。
动态规划 96 不同的二叉搜索树
dp[n]的意思是n个不同的节点组成的二叉搜索树的数量,需要通过左边节点组成的二叉搜索树的数量 乘以 右边节点二叉搜索树的数量,那么假定变量i,dp[i] = dp[0]*dp[n-1] + dp[1]*dp[n-2]....+dp[n-1]*dp[0], 所以递推公式为dp[i] = for(j){dp[i] += dp[j-1]*dp[i-j]}
动态规划 416 分割等和子集 1049 最后一块石头重量
0-1背包问题,这两道题的关键思路都是将原数组分割成两个尽量相同的部分,即求dp[原数组总和的一半]所能得到的最大价值(数组和或石头重量总和)。
动态规划 494目标和 474一和零
主要还是怎么把问题转化成01背包,目标和的思路就是分割成正left 和 负right部分 left-right=target left+right = sum, 得到 left = (target + sum)/2 那么问题就转化为这个背包能装多少了,一和零主要就是一个二维的背包,就是其背包大小要用二维表示
动态规划 377组合总和 518零钱兑换
这两个是完全背包问题,即物品可以无限次拿取,在518中是一个组合问题,组合问题的公式即为dp[j]+=dp[j-weight[i]]。需要注意的点:
- 组合问题需要将dp[0]初始化为1
- 组合问题(即不考虑顺序)为外层遍历物品,内层遍历背包大小;排列问题为外层遍历背包大小,内层遍历物品大小。
动态规划 322零钱兑换 279完全平方数
这两个也是完全背包问题,我犯的一个错误就是想使用其他变量来取得最小值,其实在装进背包的过程中用Math.min就可以了
动态规划 139单词划分
这道题dp[i] 含义为 s.slice(0, i)中包含一个或多个wordDict内的元素,注意遍历顺序,由于是排列,所以外层遍历背包,内层遍历物品
动态规划 337打家劫舍3
树形dp, dp数组是一个长度为2的数组,意思是当前节点能获取的最大值,dp[0]代表不选择当前节点的最大值,dp[1]代表选择,所以递推公式为dp[0] = Math.max(left[0], left[1]) + Math.max(right[0], right[1]) dp[1] = root.val + left[0] + right[0]
动态规划 买卖股票最佳时机3\4\冷冻期
买卖股票的题主要就是要对状态进行划分,即dp数组为一个二维数组,其二维坐标表示股票状态,以含冷冻期为例,包括三种状态,1当前为持有股票状态, 2 当天卖出股票 3 保持卖出股票 4 冷冻期
动态规划 300 最长递增子序列
dp数组的含义是dp[i]表示以nums[i]结尾的最长递增子序列,递推公式则为if(nums[i] > nums[j]) { dp[i] = Math.max(dp[i], dp[j] + 1) 从而找到最长的dp[j] + 1} 并在遍历的过程中记录dp[i]最大值。
动态规划 718最长重复子数组 1143最长公共子序列
最长重复子数组是连续的,而最长公共子序列是可以非连续的,所以最长公共子序列的递推公式需要多判断当末尾字符不相同时的递推公式,递推公式想不出来时可以举例子画数组的值来想
动态规划 392 判断子序列
这题的关键在于删除,如果子序列当前元素和父序列不匹配,则删除父序列当前元素
接雨水
这道题的算法:首先按列算,每一列接的雨水 = 左侧最高柱子和右侧最高柱子中较矮的柱子 - 当前这一列的柱子高度,其结果就为所求。
84. 柱状图中最大的矩形
这道题的算法是找到左边第一个比当前柱子低的,以及右边第一个比当前柱子低的,那么在这个区域中都是比当前柱子高或者相等的,所以这一块的矩形高度就是当前柱子,然后宽度就是右边-左边+1,因为是要找比当前低的,所以栈应该是栈头到栈尾递减的状态。
二分搜索
34
首先二分搜索就是在最后一次循环找到一个[left, right]区间,这个区间包含target(如果能包含的话), 那么以找答案的左区间为例子,当在这个区间包含target时,我们将right 更新为 middle - 1,那么此时这个右区间就是最接近target的但小于target的值,右区间同理
动态窗口
动态窗口分为最小窗口和最大窗口,最大窗口如lc904,最小窗口如lc76,模板可见904. 水果成篮 - 力扣(Leetcode)
螺旋矩阵
通用模拟模板,设定top,bottom,left,right,每次循环赋值一圈,赋值完一条边要移动相应的如top,left等
链表相关
链表结构的定义
class ListNode {
val;
next = null;
constructor(value) {
this.val = value;
this.next = null;
}
}
虚拟头结点
const dummyNode = new ListNode(0, head)
在涉及头结点操作的情况,为统一方便遍历可以使用
反转链表
需要一个cur指针初始化为head,一个pre指针初始化为null,一个temp指针用来保存cur.next,步骤如下:
- temp保存cur.next
- cur.next 指向 pre
- pre移动至cur
- cur移动至temp
需注意最后返回pre,即最后一个节点。
两两交换链表中的节点
首先创造虚拟头结点,用cur指向该虚拟头结点,然后按以下步骤:
- temp1保存cur.next
- temp2保存cur.next.next.next
- cur.next = cur.next.next
- cur.next.next = temp1
- cur.next.next.next = temp2
- cur移动两位 cur = cur.next.next
删除链表的倒数第n个节点
定义fast,slow指针,fast指针移动n+1,然后slow、fast同时移动,直到fast指针移动到null,则slow指向倒数第n-1个。
链表相交
让较长的链表移动两个链表长度的差值,然后两个链表定义指针同时移动,判断是否相等。
环形链表
环形链表的判断方法:定义一个fast指针,一次移动两个节点,一个slow指针,一次移动一个节点,如果fast和slow能够相遇,则为环形链表
找到环形入口:定义一个指针一index1从相遇地点出发,一个指针index2从head出发,相遇的节点即为入口。
哈希
242. 有效的字母异位词
思路就是维护一个哈希数组判断,重点在于charCodeAt() api获取ASCII码
49. 字母异位词分组
重点在于统计每一个字符串的哈希数组count,然后创建一个map,以这个count为key,维护value为一个存储字符串的数组。
454. 四数相加 II
四个数组,要使得四个数组的和为0,ab数组的和为i,那么cd数组的和为-i,那么维护一个map,统计ab数组的和及其出现次数,然后在cd中寻找-i,找到就➕map.get(i)
三数相加
模板: 定义i,left,right;left=i+1,right=len-1,遍历时先对nums[i]进行去重,需注意是判断nums[i] == nums[i-1],然后判断和的大小
四数之和
同上,加多一个j 判断ij去重
字符串
541. 反转字符串II
i每次移动2k,通过i得到left和right
05.替换空格
拓展数组,从尾到头则无需移动元素
151 反转字符串里的单词
- 移除多余空格
- 将整个字符串反转
- 将每个单词反转
剑指Offer58-II.左旋转字符串
先整体反转后分别反转
KMP
kmp的关键在于找到最长相等前后缀,前缀为不包含最后一个字符的连续子串,后缀则是不包含第一个字符的连续子串
找到最长相等前后缀,匹配时,当找到位于模板串的j与位于文本串i不匹配时,i前的后缀与j前的前缀相同,那么j跳转至前缀后一位在进行匹配即可。
一般在计算时,我们会维护一个next数组,next[0] = -1,next数组代表了不匹配时j应该跳转的位置,当然在模式串和文本串进行比较时,我们比较的是j+1和i。
栈和队列
滑动窗口最大值
这道题需要维护一个单调队列,每次窗口移动的时候,调用que.pop(滑动窗口中移除元素的数值),que.push(滑动窗口添加元素的数值),然后que.front()就返回我们要的最大值。
问题在于想要使得每次的最大值都是队首元素,第一我们需要使得这个队列内元素递减,第二这个队列不能维护每个滑动窗口的值,所以我们就维护可能成为最大值的值,怎么维护呢?如果遍历时这个元素比前一个元素大,就将前一个元素弹出。后一个元素可以比前一个元素小,因为可能不处于同一个窗口。
这个队列的push要点在于,如果push的元素比队首元素大,则一直弹出直到队列递减。
定义好后,先push第一个窗口,然后定义两个指针ij遍历,i指向窗口前一个元素,j指向窗口最后一个元素,遍历过程中队列每次push(nums[j] pop(nums[i],然后获取当前窗口最大值queue.front(),最后移动指针i++ j++
二叉树
迭代遍历
迭代遍历都是用栈实现,按遍历顺序分类
- 前序遍历: 在遍历过程中,先push根节点,然后pop,保存其值,然后先push其右节点,再push左节点,以保证中左右的弹出顺序
- 后序遍历,先push左节点,再push右节点,然后reverse即可,中右左->左右中
- 中序遍历,我们先从根节点往其左节点往下push进栈,相当于处理左,直到其左叶子为空,然后保存其值,相当于处理中,然后遍历其右子节点继续遍历,相当于处理右。
对称二叉树
递归,终止条件即判断当前节点的情况,当当前节点相同,继续往下递归,返回外侧节点情况和内侧节点相等情况
二叉树的最小深度
这题问题在于不能直接求左右深度最小值+1,因为有一侧可能为空,另一侧不为空,则需继续往下求深度。
平衡二叉树
后序遍历,递归求取最大高度,如果左右高度差超过1,则该节点最大高度设置成-1,如果根节点得到的为-1,则说明不是平衡二叉树
二叉树的所有路径
这道题的终止条件应该是遇到叶子节点,然后将路径push进结果中,使用前序遍历,注意,需要回溯。
左叶子节点之和
终止条件应为判断当前节点的左子节点不为空且左子节点为叶子节点
找树左下角的值
层序遍历非常简单,不赘述,递归方式求思路是判断最大深度,当遇到最大深度时,将结果更新为当前节点的值,因为是第一次更新,所以当前节点一定是该层最左边的值。
路径总和1 & 路径总和2
两道题终止条件都是判断是否为叶子节点以及总和是否为目标值,区别在于总和1这道题,遇到符合条件的需要立即返回true,否则结果可能会在回溯中被覆盖
二叉搜索树
二叉搜索树的性质是,左子树的所有节点值小于根节点,右子树所有节点值大于根节点,所有有两种验证方法,一是递归判断当前节点的值小于左节点大于右节点,可以定义区间(l,r),每次递归,左子树更新右边界,右子树更新左边界;
第二种是中序遍历,中序遍历二叉搜索树后得到的是一个有序序列,判断是否有序即可
二叉搜索树的众数/最小绝对差
这种问题统一将二叉搜索树中序遍历转化成有序序列后求解
二叉树的公共祖先
这题使用后序遍历,达到回溯的效果,即自下而上,注意,由于需要遍历整棵树,所以不能一拿到返回值就立即返回,而是需要接收返回值,在处理中间节点时进行逻辑判断
二叉搜索树的公共祖先
这道题分三种情况,1没找到直接返回null, 2当前节点比目标大,向左递归,3当前节点比目标小向左递归,找到返回当前节点即可,要点在于此题是遍历一条边而非整棵树,就是找到了就直接返回
二叉搜索树的插入
这道题我的难点在于终止条件如何将这个节点和上一个节点连接,实际上只需要返回当前节点,然后每层递归的节点都接收即可。即if(终止) {... return root} root.left = xxxfn(root.left) 即可
二叉搜索树的删除
这道题难点在于每层的逻辑,分为4种情况(找到对应节点后),每种情况还是返回一个节点,让每层节点接收,1.都为叶子,返回null, 2.有一侧子树,返回这个子树头结点,3.有两侧子树,将左子树移到右子树最左边,也就是最小节点的左子树,然后返回右子树
二叉搜索树转累加树
由于累加树不是搜索树,所以不能转化成数组再组装成搜索树
有序数组转二叉搜索树
数组转树的统一递归方法,fn(nums, left, right) 每次递归left 和 right
修剪二叉搜索树
单层递归逻辑:区间[l,r],如果当前节点比l小,那么返回其右孩子,因为右孩子比当前节点大,然后让root.left接收,右子树同理
回溯
回溯的思想在于先思考每一层的处理逻辑,通过回溯添加每一层的区别条件
40组合总和2
这题的问题在于去重,对于去重而言,回溯选取相同的数字并不会导致重复,因为他们会被放到一个数组中,但是同一树层选取相同的数字就不行,这就会造成重复。避免方法是直接arr[i] !== arr[i - 1],因为回溯过程中不会在同一层遍历两个数字。
分割回文串
这题每层遍历对当前字符串进行1位的切割,当切割的节点已经超出字符串长度,即终止,在切割过程中,判断当前切割的字符串是否为回文串,如果不是直接continue
递增子序列
这道题难度还是在终止条件上,我们除了要判断当前值是否比path最后一个大即保持递增外,还要判断当前值是否在同一树层使用过,因为相同值仅可在同一树枝上使用,所以我们在每一个树层定义一个数组,每次回溯都储存,类似一个map判断是否出现过这个值,保证每一树层不会出现重复的值
N皇后
- 终止条件,遍历列、两个对角线,由于回溯,每次for树层只会选中第一个,所以不会重复
- 回溯条件是 首先指为‘q' 后保存为’。‘
贪心
摆动序列
处理差值,首先默认第一个元素和第0个有一个差值(可以试想【2,2,5】),计算前一个元素左右差值,如果这个值为一个峰,那么count++
最大子序和
- 贪心 如果当前和小于0,则将和置为0,相当于从下一个元素开始算和
- 动态规划 dp[i]表示以nums[i-1]结尾的最大和,即选取了nums[i-1]的最大和,递推公式为dp[i] = Math.max(dp[i-1] + nums[i - 1], nums[i - 1])
01背包
每个物品选取一次,先遍历物品后遍历背包,背包容量从大到小,从而使得不会多次选取一个物品,区别与完全背包(物品可被选取多次),其背包容量从小到大遍历,这样后面的背包就可以依赖前面的背包进行选取,也就是可以物品选取多次
单调栈
适用场景
在一维数组内,找到左右两边第一个比某个元素大或者比某个元素小 的场景。
模板
const stack = [0] // stack 内存放下标
for(// 遍历数组) {
if(nums[i] < nums[stack[stack.length - 1]]) {//
//如果是求两边比当前栈顶元素的第一个 小的元素,就在这里处理逻辑
// 比如
while(stack.length !== 0 && nums[i]<nums[stack[stack.length-1]]) {
//...
}
}
}
回文子串
动态规划
二维dp数组, dp[i][j]表示[i, j]字符串是否为回文,递推公式为:当[i,j]长度<=2 dp[i][j] = s[i] === s[j] 其余情况dp[i][j] = dp[i+1][j-1] && s[i] === s[j]
中心扩散
从中心扩散,若中心对称的左=右,则该范围内字符串为回文,中心包括两种情况,即回文串为奇数和偶数,我们遍历是只需以(i,i) 和 (i, i+1)的情况为遍历即可囊括