当前达成目标力扣100道,持续努力中······
刷题是锻炼逻辑思维和coding能力的有效途径,一时刷题一时爽,一直刷题一直爽。
一、目标
每周刷5道题,本周未达成。。。。。。
二、思维导图
三、总结
每种类型的题,同一类的都是有代码模版的,适当的借助模版可以节省力气。下面,大体总结一下,刷题过程中的类似的模版(大体分为,dfs:借助于stack,bfs:借助于queue,动态规划:借助于临时数组dp[][], 递归借助于List等等)。
3.1 动态规划
动态规划类的题,主要有几类:0-1背包,最长子序列(有序和连续),最小路径和,博弈论,接雨水等等。
本文的总结,主要提供代码模版,现拿一道比较简单的动态规划为例:
最小路径和:https://leetcode-cn.com/problems/minimum-path-sum/
动态规划类型的题目,最初的解体思路就是找到动态转移方程,基于动态转移方程,去实现具体的代码,本文重点在代码实现上,具体的动态转移方程的分析,还是要基于读者对该子任务类型(自底向上,自顶向下)的理解。要想找到状态转移方程算是一个比较麻烦的操作,借助于dp[][],将其作为将要构造的状态转移方程的出发点。至于简化的降维成一维的思路可以先不考虑,而更简单的dp[]类型的题目可以简单了解一下(这类题基本都很简单,可以先不考虑)。
class Solution {
/*本题构造的dp[i][j],i表示row,j表示col,而dp[i][j]表示【0,0】到【i,j】的最小距离。
这是本题的思路,最有意思的可以看看0-1背包问题的i,j的含义,还有博弈论相关的i,j的含义以及状态转移方程的构建。
*/
public int minPathSum(int[][] grid) {
int [][] dp = new int[grid.length][grid[0].length];
/*求row的从0到n的dp[i][0],作为自底向上的基础。
*/
for(int i=0;i<grid.length;i++){
if(i==0){
dp[0][0]=grid[0][0];
}else{
dp[i][0] = dp[i-1][0]+grid[i][0];
}
}
/*求col的从0到n的dp[0][j],作为自底向上的基础。
*/
for(int i=0;i<grid[0].length;i++){
if(i==0){
dp[0][0] = grid[0][0];
}else{
dp[0][i]= dp[0][i-1]+grid[0][i];
}
}
/*状态转移方程,
Min(dp[i-1][j], dp[i][j-1]);
*/
for(int i=1;i<grid.length;i++){
for(int j=1;j<grid[0].length;j++){
dp[i][j]= Math.min(dp[i-1][j],dp[i][j-1])+grid[i][j];
}
}
/*
返回基于i,j含义的dp结果。
*/
return dp[grid.length-1][grid[0].length-1];
}
}
3.2 递归+剪枝
递归算是最常见的一种解题方法,在刷力扣的时候,本人遇见很多道可以用改递归+剪枝的方法来处理的题型。
现举例一道典型题:
组合总和:https://leetcode-cn.com/problems/combination-sum/
递归类型的题解的处理主要包括,递归+剪枝+回溯(可以通过下面的模版来适配所有类型的该类问题,具体请好好理解);
所谓递归就是求全局变量,如果对时间复杂度要求较高的场景中,不建议使用该方法,毕竟递归的时间复杂度是指数级。
class Solution {
public List<List<Integer>> combinationSum(int[] candidates, int target) {
/*本方法中,利用list作为递归返回值的保存数组
*/
List<List<Integer>> list = new ArrayList();
/*利用l作为递归的过程中数据的记录的临时保存数组
*/
List<Integer> l = new ArrayList();
/*利用count作为判断终止条件,if(count==target)作为终止。
*/
int count =0;
inSum(candidates, list, l, count, target);
return list;
}
public void inSum(int[] candidates,List<List<Integer>> list, List<Integer> l, int count, int target){
if(count == target){
/*由于java对象在堆中的特性决定的,只能重新申请对象,这样返回的list能保存所有的枚举
*/
List<Integer> t = new ArrayList();
for(Integer t1:l){
t.add(t1);
}
Collections.sort(t);
if(!list.contains(t))
list.add(t);
} else if(count>target){
return;
}
for(int i=0;i<candidates.length;i++){
/*此处可以做一些剪枝的操作,无外乎一些if else的逻辑判断;
通过l保存此次低谷的数据,如果满足终止条件,该l的数据会被重新赋予list见上面逻辑,如果失败,则继续递归遍历
*/
l.add(candidates[i]);
count+=candidates[i];
inSum(candidates,list,l,count, target);
/*利用list的方法,释放上次递归遍历的数据,也就是理论上的回溯
*/
l.remove(l.size()-1);
count-=candidates[i];
}
}
}
深搜,可以基于上面的递归方式来实现,试具体情况,是否需要回溯+剪枝。
广搜,建议使用queue来处理。
3.3 二叉树
3.3.1 二叉树的遍历
二叉树的四种遍历,前置、中置、后置、层次遍历需要都掌握,包括递归(见二中的模版,二叉树的递归,是堆左子树和右子树的判断)和基于堆的方式(可以代码实现一遍,其实理解好了还是很简单的)。
下面提供一个基于堆的前置、中置、后置的变量代码:
前序:
class Solution {
public List<Integer> preorderTraversal(TreeNode root) {
Stack<TreeNode> stack = new Stack();
List<Integer> list = new ArrayList();
stack.push(root);
while(stack.size()>0){
TreeNode r = stack.pop();
if(r == null){
return list;
}
list.add(r.val);
if(r.right != null){
stack.push(r.right);
}
if(r.left!=null){
stack.push(r.left);
}
}
return list;
}
}
中序:
public class Solution {
public List < Integer > inorderTraversal(TreeNode root) {
List < Integer > res = new ArrayList < > ();
Stack < TreeNode > stack = new Stack < > ();
TreeNode curr = root;
while (curr != null || !stack.isEmpty()) {
while (curr != null) {
stack.push(curr);
curr = curr.left;
}
curr = stack.pop();
res.add(curr.val);
curr = curr.right;
}
return res;
}
}
后序:
class Solution {
public List<Integer> postorderTraversal(TreeNode root) {
Stack<TreeNode> stack = new Stack();
stack.push(root);
Map<TreeNode, Integer> map = new HashMap<>();
List<Integer> list = new ArrayList();
if(root == null){
return list;
}
while(stack.size()>0){
TreeNode r = stack.peek();
if(r.left !=null&&map.get(r.left)==null){
stack.push(r.left);
} else{
if(r.right != null&& map.get(r.right)==null){
stack.push(r.right);
}else{
list.add(r.val);
stack.pop();
map.put(r, 1);
}
}
}
return list;
}
}
基于栈的前中后二叉树遍历要善于利用list来作为回溯的工具。
二叉树变量相关的题型主要包括,求最长层数、最小枝路径、是否对称等等,可以善于利用递归的思想
3.3.2 重建二叉树
二叉树还有比较难理解的几种类型,比如通过两种遍历来重建二叉树、二叉搜索树、平衡二叉树
比如通过前序和中序重建二叉树,基本思想还是通过递归,但是前提要理解前、中、后的特点,比如前序的第一个节点肯定是根节点,而中序的前序的根节左边的数组一定是根节点的左子树,而右边的数组一定是根节点的右子树,而后序的根节点一定是数组的最后一个节点,基于此种考虑,可以通过前序+中序,中序+后序来重新恢复二叉树:
1、递归的思路是先在前序或者后序的数组中找到根节点
2、再在中序的数组中找到左子树和右子树数组
3、分别在中序的左子树数组和右子树数组中重复1、2的步骤。
具体事例代码如下:
class Solution {
public TreeNode buildTree(int[] preorder, int[] inorder) {
return buildSubTree(preorder, 0, preorder.length, inorder, 0, inorder.length);
}
public TreeNode buildSubTree(int[] preorder, int start1, int end1,int[] inorder, int start2,int end2){
if(start1>preorder.length-1 || start1>end1 || start2>end2){
return null;
}
int top = preorder[start1];
TreeNode tree = new TreeNode(top);
for(int i=start2;i<end2;i++){
if(inorder[i]==top){
TreeNode leftNode=buildSubTree(preorder, start1+1, start1+i-start2, inorder,start2, i);
TreeNode rightNode = buildSubTree(preorder, start1+i-start2+1,end1,inorder, i+1, end2);
tree.left = leftNode;
tree.right = rightNode;
return tree;
}
}
tree.left = null;
tree.right = null;
return tree;
}
}
3.3.3 搜索二叉树
搜索二叉树就是有序的二叉树,既左子树一定小于根节点,右子树一定大于根节点
二叉搜索树主要是通过,构建有序数组,然后按照一定规则去重建二叉树,具体上代码:
class Solution {
public TreeNode sortedListToBST(ListNode head) {
int length =0;
ListNode h = head;
while(h!=null){
length++;
h= h.next;
}
int[] nums = new int[length];
int i=0;
while(head != null){
nums[i++]=head.val;
head= head.next;
}
return sortedArray(nums, 0, nums.length-1);
}
public TreeNode sortedArray(int[] nums, int start, int end){
/*具体规则可以具情况而定,当前以medium作为根节点
*/
int medium = (start + end+1)/2;
if(start>end || start > nums.length-1 ){
return null;
}
TreeNode tree = new TreeNode(nums[medium]);
if(start == end){
tree.left = null;
tree.right= null;
return tree;
}
TreeNode left = sortedArray(nums, start, medium-1);
TreeNode right = sortedArray(nums, medium+1, end);
tree.left = left;
tree.right = right;
return tree;
}
}
常见的题目就是恢复二叉搜索树,至于判断是否为二叉搜索树通过递归判断左子树是否小于根节点,右子树是否大于根节点来判断,比较简单
3.3.4 平衡二叉树
平衡二叉树是指树的高度差小于等于1的二叉树。
平衡二叉树也就是传说中的AVL树
通过数组来恢复成平衡二叉树
代码同3.3.3 中的代码,具体不同处在于:
int mid = start + (end - start )>> 1;
同上面的代码实现,既把中间节点作为根节点,既可构造平衡二叉树
3.3.5 平衡二叉搜索树
平衡二叉树和二叉搜索树的结合,即有序和中间节点作为根节点而构建的二叉树
3.3.6 红黑树
研究过红黑树,感觉除了背具体的实现,比较麻烦,理解上还好,但是实现上确实复杂度较高,不建议去背
3.4 其他类型
leetCode上有很多有趣的类型的题目,比如链表(成环、倒数第k个数遍历、链表倒序、跳表、复制跳表、k个一组翻转链表、有序链表合并(推导到n个有序链表合并,大顶堆)等等)、字符串(字符串的处理有很多比较经典的题型,可以适当研究,比如回文数等)、数组(力扣,难度差距最大的类型题目,接雨水等等,最适合动态规划类型的题型,动态规划每次出现都是medium以上的题目,是比较难的,建议好好研究研究这种类型的题目)、排序(归并、快排、堆、基类、选择、插入、冒泡、桶排序,前三个比较重要,尤其堆排序,实现起来很有意思,可以研究研究)等。
四、力扣
力扣进度
五、好题推荐
接雨水:接雨水是很经典的题目,面试考的几率非常大,可以研究一下
买卖股票的最佳时机:买卖股票的题目可以无限扩展,有一种可以使用于所有的买卖股票的题的方法模版,可以研究一下
求最小的k个数:首先理解一下该题的时间复杂度,看什么方法能优化时间复杂度,小顶堆思想,nlogk,复杂度
Nim 游戏:博弈论的题目
首先选题:田忌赛马的思路,即贪心算法
盛最多水的容器:第一次出去面试互联网的题目,记忆犹新,动态规划类型题目,临时推导的