截至2020年6月2号,牛客+LeetCode,一共刷了170道左右。从3月底开始每天早上雷打不动地刷两道算法,已经成为了个习惯,即使以后上班了也会保持这个习惯,但是题量可能会降到每天一道。也许日常开发中算法用的不多,但是刷多了算法,自然而然的就养成了一个写代码非常严谨、追求简洁的心态。而且对各种数据结构和位运算的应用也越来越熟悉。
虽然很多大佬说吃透LeetCode Top100+剑指Offer,解决开发岗的笔面试中的算法题问题应该不大。但是问题就在于这个吃透,我现在回过头去做以前做过的题依旧很多不会,但比起第一次刷明显有个区别就是:起码有一些思路了。但是这还不够,下一步我就打算分类刷题,比如DFS、BFS、动态规划、双指针等等,每个类型的吃透。
刚开始刷真的几度怀疑自己的智商,自己没思路,很多题解也看不懂,即使看懂了也得花半天,这里的半天是真半天,不是形容词。后来想了想,这么下去不行,2道算法题就弄一早上,其他的任务还做不做了。后来想了个方法:如果题目有思路就自己先尝试写,如果想了一会还是完全没思路就直接看题解,题解一看就懂的,明白题解意思后自己去敲,如果题解都看的费劲直接Copy代码到IDEA一步一步调试着看。
下面就简单的记录一下DFS、BFS等算法的总结,注:这篇不是正式算法总结,过段时间我会先看看其他大神写的这些算法类型的博客,然后结合自己刷过的题,再写一篇正式的DFS、动态规划等等类型的总结博客。
1.双指针
双指针一般用在数组或链表当中,还要求在原数组中进行操作,也就是额外空间复杂度为O(1)。双指针顾名思义就是定义两个指针,在Java代码中就是两个索引变量。可以看看下面这道题(剑指Offer21题):
public class No21调整数组顺序使奇数位于偶数前面 { /** * 输入一个整数数组,实现一个函数来调整该数组中数字的顺序, * 使得所有奇数位于数组的前半部分,所有偶数位于数组的后半部分。 * 示例: * 输入:nums = [1,2,3,4] * 输出:[1,3,2,4] * 注:[3,1,2,4] 也是正确的答案之一。 * */ /** * 思路:双指针。 * */ public int[] exchange(int[] nums) { //左指针 int left=0; //右指针 int right=nums.length-1; while(left<right){ if((nums[left]&1)==0){ nums[left]^=nums[right]; nums[right]^=nums[left]; nums[left]^=nums[right]; right--; }else{ left++; } } return nums; } }
左右指针定义在两头,当左右指针相遇,也就是left
其中用到了两个位运算技巧,下面就分享几个常用的位运算:
2的n次方:
2<<2=8//n=3,后面的数为n-1 2<<3=16//n=4
判断奇偶性
10&1=0//n=10 9&1=1//n=9
不用辅助参数交换两个数
a=a^b; b=b^a; a=a^b;
整数除以2
num>>1//等同于num/2
上面那道示例题两个指针是从头和尾出发的,还有的可能都从头出发,实际的要看不同的题目。可以在LeetCode题库页面的标签分类中选择双指针类型,自己练习一下。
有的双指针可能还分为快慢指针,如下题,注释就算题解说明:
public class No234回文链表 { /** * 请判断一个链表是否为回文链表。 * 示例 1: * 输入: 1->2 * 输出: false * 示例 2: * 输入: 1->2->2->1 * 输出: true * */ /** * 思路:定义一个快指针一个慢指针 * 慢指针一次走一步,快指针一次走两步 * 当快指针都到链表结尾,慢指针一定在链表中间,为了确保链表长度为偶数时,慢指针在左边,所以快指针从第2个节点开始 * 等快指针到达结尾,创建一个栈把慢指针后面的入栈,这样出栈顺序如果和慢指针前面的数一样,那就是回文 * */ public boolean isPalindrome(ListNode head) { if(head==null) return true; if(head.next==null) return true; Stackstack=new Stack<>(); ListNode lower=head; ListNode quick=head.next; while (quick.next!=null){ lower=lower.next; if(quick.next.next==null) break; quick=quick.next.next; } ListNode left=lower.next; while (left!=null){ stack.push(left); left=left.next; } while (!stack.isEmpty()){ if(stack.pop().val!=head.val){ return false; } head=head.next; } return true; } }
2.动态规划
动态规划一般都会定义一个dp数组,根据不同题目的需求,有可能是二维的有可能是一维的。dp数组的意思一般都是当前索引位置满足要求的值,那么数组最后一个值就是最终结果。比如经典的跳台阶问题,即给你n阶台阶,一次只能跳1阶或2阶,问你一共有多少种方法跳完n阶台阶。做过这道题的朋友可能知道,这其他就是个斐波那契数列,但是还可以用动态规划来做,即定义一个长度为n+1的dp数组,为什么是n+1?因为我们要用到索引n,而数组索引是从0开始的,所以得+1。定义完数组后,dp[1]=1,代表到1阶台阶只有一种方法,dp[2]=2,代表到2阶台阶有两种方法,即:跳两个1阶或一个2阶。dp[3]=dp[1]+dp[2],为什么是dp[1]+dp[2],因为在第1阶台阶上跳2阶就到3阶了,在第2阶台阶上跳1阶就到3阶了。以此类推,dp[n]就是最终结果。
这么说可能不明白,看看这一题:
public class No198打家劫舍 { /** * 你是一个专业的小偷,计划偷窃沿街的房屋。每间房内都藏有一定的现金,影响你偷窃的唯一制约因素就是相邻的房屋装有相互连通的防盗系统,如果两间相邻的房屋在同一晚上被小偷闯入,系统会自动报警。 * 给定一个代表每个房屋存放金额的非负整数数组,计算你在不触动警报装置的情况下,能够偷窃到的最高金额。 * 输入: [2,7,9,3,1] * 输出: 12 * 解释: 偷窃 1 号房屋 (金额 = 2), 偷窃 3 号房屋 (金额 = 9),接着偷窃 5 号房屋 (金额 = 1)。 * 偷窃到的最高金额 = 2 + 9 + 1 = 12 。 * */ /** * 思路:动态规划 * dp数组代表当前位置,能偷盗的最大值 * 如2 7 9 3 1 * 初始化dp[0]=2.dp[1]=7。前两个位置很好确定。 * 当i=2,因为不能连着偷,所以只能用前两个位置的dp加上本位置和前一个dp比较 * 如果小于就取前一个dp,如果大于就取dp[i-2]+nums[i] * */ public static int rob(int[] nums) { int len=nums.length; if(len==0) return 0; if(len==1) return nums[0]; int[] dp=new int[len]; dp[0]=nums[0]; dp[1]=nums[1]>nums[0]?nums[1]:nums[0]; for(int i=2;i){ if(dp[i-2]+nums[i]>dp[i-1]) dp[i]=dp[i-2]+nums[i]; else dp[i]=dp[i-1]; } return dp[len-1]; } public static void main(String[] args) { int[] nums={2,7,9,3,1}; System.out.println(rob(nums)); } }
3.DFS和BFS
DFS:深度优先搜索。BFS:广度优先搜索。一般用在图、树、矩阵中。深度优先搜索就是一条路走到底,要么得到正确结果,要么路径错误,进行回溯。广度优先搜索就是在岔路口记录每个可行的路口,然后广撒网,同时铺开。这个同时铺开是怎么做到的?这里就用到了队列。以二叉树为例。当前节点设为n1,它的两个子树节点n2和n3都符合要求,如果是DFS的话,会选择其中一条走到底,如果不符合要求会一直回溯到n1,然后再选择另一个节点。如果是BFS会把n2和n3入队。然后n2出队,又得到n2的两个子树节点n4和n5,它们也符合要求,接着把它们也入队。此时队列为:n3,n4,n5。接着n3出队,以此类推,是不是就类似于广度上的铺开了。
这题以DFS为例:
public class No13机器人的运动范围 { /** * 地上有一个m行n列的方格,从坐标 [0,0] 到坐标 [m-1,n-1] 。 * 一个机器人从坐标 [0, 0] 的格子开始移动,它每次可以向左、右、上、下移动一格(不能移动到方格外), * 也不能进入行坐标和列坐标的数位之和大于k的格子。例如,当k为18时,机器人能够进入方格 [35, 37] , * 因为3+5+3+7=18。但它不能进入方格 [35, 38],因为3+5+3+8=19。请问该机器人能够到达多少个格子? * 示例 1: * 输入:m = 2, n = 3, k = 1 * 输出:3 * */ /** * 思路:DFS * DFS:关键在于选择、标记和终止条件,这道题比较特殊,不用回溯,只需要统计,有的题只有一种正确路径的,就需要在递归后回溯一下 * 所谓回溯,就是删除前一步的改变,比如前一步令变量count+1了,或者令标记数组发生改变,如果递归后发现前一步不满足要求,return以后 * 就要令count-1,复原标记数组。如果一直没发生回溯,一直到满足最终要求,说明当前就是正确路径。 * 接着开头说的,关键在于选择、标记和终止条件。标记就是一个数组,标记走过的地方,因为一般都不能重复走同一个点,但在二叉树中一般不需要标记,因为只能往子树走。 * 选择就是有几条路可以走,比如这题中的上下左右,但并不是都能满足,所以一定要认真找出判断条件 * 终止条件其实就是正确路径的判断,比如count==10,就算正确路径,那么一般就要在dfs方法最前就判断一下,满足的话直接返回并改变某个变量,如上一题中的result * * BFS:除了DFS的一些关键点,还需要用队列来满足其特性,即广度,因为需要走满足条件的所有条件。如,我发现1 2 3可以走 * 那么就可以在队列中加入1 2 3,然后根据出队顺序依次去探索1 2 3,如果在1里面又发现了路径4 5,接着入队4 5,此时队列是:2 3 4 5 * 接着探索2,如果发现了其他路径,接着入队。 * 可以发现,BFS就如其名,广度优先搜索,探索的路径都是广度铺开的。而深度优先搜索就是一条路走到低,直到满足条件或者不满足条件回溯。 * */ static int count=1; static boolean[][] flag; public static int movingCount(int m, int n, int k) { flag=new boolean[m][n]; flag[0][0]=true; dfs(0,0,m,n,k); return count; } private static void dfs(int curM,int curN,int m,int n,int k){ //右 if(curN+1,k)){ flag[curM][curN+1]=true; count++; dfs(curM,curN+1,m,n,k); } //左 if(curN-1>=0&&!flag[curM][curN-1]&&helper(curM,curN-1,k)){ flag[curM][curN-1]=true; count++; dfs(curM,curN-1,m,n,k); } //上 if(curM-1>=0&&!flag[curM-1][curN]&&helper(curM-1,curN,k)){ flag[curM-1][curN]=true; count++; dfs(curM-1,curN,m,n,k); } //下 if(curM+1 ,curN,k)){ flag[curM+1][curN]=true; count++; dfs(curM+1,curN,m,n,k); } return; } private static boolean helper(int curM,int curN,int k){ int m=0; int n=0; while (curM>0){ m+=curM%10; curM/=10; } while (curN>0){ n+=curN%10; curN/=10; } return m+n>k?false:true; } public static void main(String[] args) { System.out.println(movingCount(8,5,6)); } }
就写这么多吧。可能很多地方表达的不是很规范严谨,毕竟我也才是刚刷两个多月的菜鸟。这篇也不指望能帮到什么人,只是看到很多人都说刚开始刷算法心态都或多或少崩过,但只要坚持下来还是会有一些改变的。