刷题常用算法总结
一、动态规划
动态规划是算法中相对较难的部分,所以放在前面讲。
1、何时采用动态规划
整个数组或在固定大小的滑动窗口中找到总和或最大值或最小值的问题可以通过动态规划(DP)在线性时间内解决。
2、动态规划解题步骤
总体上分为四步:定义状态、状态转移方程、初始化、输出
2.1 定义状态
什么状态好转移就定义什么状态。
常见的状态定义方法:
一维动态规划:
(1) dp[i]定义为数组前i个元素的最值或者总和
(2) dp[i]表示以nums[i]作为结尾元素的最值或总和
二维动态规划:
(1) dp[i][j]定义为数组或字符串从nums[i…j]之间的最值
(2) dp[i][j]定义为以nums[i]开始并且以nums[j]结尾的子数组的最值
(3) dp[i][j]定义为两个数组分别以nums[i]和nums[j]结尾的最值
(4) dp[i][j]在01背包问题中表示添加前i个数值后剩余的容量为j
2.1 状态转移方程
1. 01背包问题
从数组中选出一些数值,使其满足特定的容量,从而求其最大值。包含有容量大小V,价值还有物品v,剩余容量大于物品重量 w时:
初始时:i=1,j=1
dp(i,j)=max(dp(i-1,j),dp(i-1,j-w(i))+v(i))
2. 无限背包问题
与01背包不同之处在于,数组中的元素可以重复选择。比如:硬币找零问题、切割钢条、剪绳子等。
初始时:i=1…length,j = 0…nums2.length(或者i)
dp[i] = max(dp[i],dp[i-len[j]]+price[j])
3. 回文子系列与最长字符串系列
(1)子序列问题
由于子序列不要求数组元素连续。
最长回文子序列
由于含有i+1,所以初始时需要倒序遍历i:
i = length-1…0
由于包含j-1所以正序遍历j:
j = i+1…length
dp [i][j] = dp [i + 1][j - 1] + 2 (s[i]==s[j])
dp [i][j] = max(dp[i + 1][j], dp[i][j - 1]) (s[i]!=s[j])
最长公共子序列
初始时:i=1…s1.length,j=2…s2.length
如果s1[i]==s2[j] 则该字符需要添加到最长公共子序列中
dp[i][j]=dp[i-1][j-1]+1
如果s1[i]!=s2[j] 则:
dp[i][j] = max(dp[i-1][j],dp[i][j-1])
最长递增子序列(定差)
只需要判断后一个值是否大于前一个值就行了
如果nums[j] < nums[i],就
dp[i]=max(dp[i],dp[j]+1)
(2)子字符串问题
要求数组元素必须连续
求回文子串可以定义dp数组为boolean型,是否为回文
最长回文子串
初始时:I = length-1 … 0, j = i…length
dp[i][j] = (s[i] == s[j]) && (dp[i + 1][j - 1] || j-i<3)
必须要保证j大于i
最长公共子串
初始时:i=1…s1.length,j=2…s2.length
如果s1[i]和s2[j]相等,那么:
dp[i][j] = dp[i-1][j-1]+1;
如果s1[i]和s2[j]不相等,则:
dp[i][j] = 0;
最长递增子串问题:不采用动态规划,采用滑动窗口的方式更加好求解
子串最大和:还可以采用滑动窗口的方式
初始时:i=1…s1.length
dp[i] = max(dp[i-1]+nums[i],nums[i])
3、例题
3.1 01背包问题
3.1 无限背包
3.3 回文子系列
3.4 最长子字符串系列
- leetcode 300:最长上升子序列
- leetcode 1218: 最长定差子序列
- leetcode 1027:最长等差子序列
- leetcode 674:最长连续递增子序列
- leetcode 1143:最长公共子序列
- leetcode 最长公共子串
- leetcode 718:最长重复子数组
3.5 其他
二、滑动窗口
1. 滑动窗口法用于解决的问题
经常是用来执行数组或是链表上某个区间(窗口)上的操作。比如找最长的全为1的子数组长度。滑动窗口一般从第一个元素开始,一直往右边一个一个元素挪动。当然了,根据题目要求,我们可能有固定窗口大小的情况,也有窗口的大小变化的情况。滑动窗口经常用于寻找连续的子串和数组。
下面是一些我们用来判断我们可能需要上滑动窗口策略的方法:
(1)这个问题的输入是一些线性结构:比如链表呀,数组啊,字符串啊之类的
(2)让你去求最长/最短子字符串或是某些特定的长度要求
2. 解题步骤
2.1 通常需要左右两个指针,left和right
2.2 循环结束条件:首先保持左指针不动,移动右指针,右指针遍历整个数组
3. 例题
- 剑指Offer 和为S的连续正数序列
- 剑指Offer 删除链表中重复的结点
- leetcode 340 最多带有K个不同字符的最长子字符串
- leetcode 713. 乘积小于K的子数组
- leetcode 28. 实现strStr
三、双指针法
1. 双指针模式
两个指针朝着左右方向移动(双指针分为同向双指针和异向双指针),直到他们有一个或是两个都满足某种条件。双指针通常用在排好序的数组或是链表中寻找对子。比如,你需要去比较数组中每个元素和其他元素的关系时,你就需要用到双指针了。
使用双指针策略的方法:
(1)一般来说,数组或是链表是排好序的,你得在里头找一些组合满足某种限制条件
(2)这种组合可能是一对数,三个数,或是一个子数组
对于未排好序的数组,需要先排序
2. 解题步骤
2.1 通常左右两个指针分别为left和right,左右指针的初始位置不一定是在0和length-1,还可能为0和1。
2.2 循环结束条件:while(left <= right)
2.3 比如求两数之和、三数之和、四数之和
在三数之和中,先选择一个target目标值,可以遍历整个数组作为两数之和。而left指针从i+1开始,right指针从length-1开始。计算方式与两数之和类似。
去重。在求多数之和中最常见的就是要去重,需要考虑两部。
(1)target去重,去除重复的target目标和
(2)左右指针去重,去除遍历重复的做指针和右指针
3. 例题
四、堆栈法
1、堆栈法
在解决具有回文或字符匹配等问题的时候,可以采用堆栈数据类型来解决。
2、解题步骤
2.1 常见问题类型:
比如:比较含有退格的字符串、有效的括号、最长有效括号
2.2 通常定义一个堆栈Stack或者ArrayList数据类型。
2.3 循环结束条件:遍历整个数组或者字符串
3、例题
五、快慢指针法
1、快慢指针模式
有一个非常出门的名字,叫龟兔赛跑。还是再解释一下快慢指针:这种算法的两个指针的在数组上(或是链表上,序列上)的移动速度不一样。还别说,这种方法在解决有环的链表和数组时特别有用。通过控制指针不同的移动速度,这最终两个指针肯定会相遇的,快的一个指针肯定会追上慢的一个。
怎么知道需要用快慢指针模式?
(1)问题需要处理环上的问题,比如环形链表和环形数组
(3) 当你需要知道链表的长度或是某个特别位置的信息的时候
2、解题步骤
2.1 常见问题:链表的中间节点、链表的倒数第N个节点、判断是否为环形链表、快乐数
2.2 采用快慢两个指针,fast和slow,快指针移动2步,满指针移动一步
2.3 循环结束条件:遍历整个链表
3、例题
六、哈希表法
1、哈希表
是根据关键码值(Key value)而直接进行访问的数据结构。也就是说,它通过把关键码值映射到表中一个位置来访问记录,以加快查找的速度。这个映射函数叫做散列函数,存放记录的数组叫做散列表。
用于查找数组中缺失的数值,hashset数据有去重的功能。
2、解题步骤
2.1 将数组元素添加进hashset数据结构中,就可以查找缺失的数值
七、区间合并法
1、区间合并模式
是一个用来处理有区间重叠的很高效的技术。在涉及到区间的很多问题中,通常咱们需要要么判断是否有重叠,要么合并区间,如果他们重叠的话。
理解和识别这六种情况,非常重要。因为这能帮你解决一大堆问题。这些问题从插入区间到优化区间合并都有。怎么识别啥时候用合并区间模式呀?
(1)当你需要产生一堆相互之间没有交集的区间的时候
(2)当你听到重叠区间的时候
2、解题步骤
2.1 首先需要对每个区间的开始进行排序,需要重写比较器Comparator。将所有区间按每个区间的开始位置从小到大排序。
2.2 再判断前一个区间的结尾intervals[i][1]和后一个区间的开始intervals[i+1][0],如果intervals[i][1]大于intervals[i+1][0],说明两个区间相交,反之如果intervals[i][1]小于intervals[i+1][0],说明两个区间不相交。
八、循环排序
1、循环排序法
这种模式讲述的是一直很好玩的方法:可以用来处理数组中的数值限定在一定的区间的问题。这种模式一个个遍历数组中的元素,如果当前这个数它不在其应该在的位置的话,咱们就把它和它应该在的那个位置上的数交换一下。你可以尝试将该数放到其正确的位置上,但这复杂度就会是O(n^2)。这样的话,可能就不是最优解了。因此循环排序的优势就体现出来了。
如何鉴别这种模式?
(1)这些问题一般设计到排序好的数组,而且数值一般满足于一定的区间
(2)如果问题让你需要在排好序/翻转过的数组中,寻找丢失的/重复的/最小的元素
2、解题步骤:
1、采用循环排序遍历的方法,这就好比一个萝卜一个坑。将nums[i]所对应的索引位置的数据标记为负数,最终查看不是负数的数据索引就是缺失的数据或者重复的数据。
2、循环结束标志:遍历整个数组
3、例题
九、链表反转
1、链表反转
题目可能需要你去翻转链表中某一段的节点。通常,要求都是你得原地翻转,就是重复使用这些已经建好的节点,而不使用额外的空间。这个时候,原地翻转模式就要发挥威力了。
这种模式每次就翻转一个节点。一般需要用到多个变量,一个变量指向头结点(下图中的current),另外一个(previous)则指向咱们刚刚处理完的那个节点。在这种固定步长的方式下,你需要先将当前节点(current)指向前一个节点(previous),再移动到下一个。同时,你需要将previous总是更新到你刚刚新鲜处理完的节点,以保证正确性。
2、解题步骤
2.1 链表反转首先需要创建3个节点:
pre=null ,cur=head,next=cur.next
2.2 依次遍历节点使:
更新下一个节点:next=cur.next
当前节点指向前一个节点:cur.next=pre
更新前一个节点:pre = cur
更新当前节点:cur=next
2.3 循环结束条件:cur节点为空
3、例题
十、树的BFS
1、树的层次遍历
这种模式基于宽搜(Breadth First Search (BFS)),又叫广度优先搜索,适用于需要遍历一颗树。借助于队列数据结构,从而能保证树的节点按照他们的层数打印出来。打印完当前层所有元素,才能执行到下一层。所有这种需要遍历树且需要一层一层遍历的问题,都能用这种模式高效解决。
这种树上的BFS模式是通过把根节点加到队列中,然后不断遍历直到队列为空。每一次循环中,我们都会把队头结点拿出来(remove),然后对其进行必要的操作。在删除每个节点的同时,其孩子节点,都会被加到队列中。
识别树上的BFS模式:
如果你被问到去遍历树,需要按层操作的方式(也称作层序遍历)
2、解题步骤
2.1 用于解决二叉树按层进行遍历的情况、二叉树的最大和最小深度
2.2 采用队列数据结构,从树的根节点开始,每次将树的每一层节点添加进队列,再进行操作。每次将每一层的节点poll弹出来,将该节点的左右子节点添加进队列。
2.3 循环结束条件:while循环,直到队列为空跳出循环
3、例题
十一、树上DFS
1、树形DFS
树形DFS基于深搜(Depth First Search (S))技术来实现树的遍历。咱们可以用递归(或是显示栈,如果你想用迭代方式的话)来记录遍历过程中访问过的父节点。
该模式的运行方式是从根节点开始,如果该节点不是叶子节点,我们需要干三件事:
(1)需要区别我们是先处理根节点(pre-order,前序),处理孩子节点之间处理根节点(in-order,中序),还是处理完所有孩子再处理根节点(post-order,后序)。
(2)递归处理当前节点的左右孩子。
识别树形DFS:
你需要按前中后序的DFS方式遍历树
如果该问题的解一般离叶子节点比较近。
2、解题步骤
2.1 通常用来处理二叉树的前序、中序、后序遍历、遍历二叉树的所有路径
2.2 可以采用栈数据结构存储二叉树的节点(采用栈就是迭代遍历),还可以采用递归的方法。
2.3 如果遍历二叉树的所有路径和:node存储当前遍历的节点,allSum存储当前的和(这与递归时,返回上一层递归还能够保存上一层递归的总和一样)。每次遍历时弹出node的最后一个节点,并添加最后一个节点的左右节点,先将右节点添加进去。
如果是二叉树的前中后序遍历:思路一样,采用栈保存节点,再依次添加栈的右子节点和左子节点,而输出节点的语句再左右添加之前就是前序遍历,再左右添加之间就是中序遍历,在左右添加之后就是后序遍历。递归的方法也是一样。
3、例题
十二、子集问题
1、解决排列组合的方法
许多的编程面试问题都会涉及到排列和组合问题。
方法1:子集问题模式讲的是用宽度优先搜索BFS来处理这些问题。
这个模式是这样的:
给一组数字 [1, 5, 3]
我们从空集开始:[[]]
把第一个数(1),加到之前已经存在的集合中:[[], [1]];
把第二个数(5),加到之前的集合中得到:[[], [1], [5], [1,5]];
再加第三个数(3),则有:[[], [1], [5], [1,5], [3], [1,3], [5,3], [1,5,3]].
方法2:采用深度优先搜索的DFS的遍历方式
每次从空集开始,遍历每一个nums数组的元素,每次该元素为选择和不选择,最终形成一个树形结构。
方法3:回溯法(递归)
回溯法就是采用递归的思想,其实回溯算法关键在于:不合适就退回上一步。仍然是那颗状态树,不过不再走遍所有的节点,而是通过回溯,跳过一些节点。前面那种标准的二叉树中序遍历,虽然更容易理解,其实实用性有限,非常不利于剪枝。
2、解题步骤
2.1 组合
2.1.1 BFS方法:先从空集开始,每次将集合中的元素与新添加的元素nums[i]得到的笛卡尔乘积加入到集合中,知道遍历所有的nums为止
2.1.2 回溯法:
(1)首先创建一个函数用于回溯递归调用,函数包含了4个形参(数组索引、数组、结果alllist、中间变量保存的list),回溯的时候在递归调用的时候需要采用new的方式复制原list,保证不再原list内存上修改数据。
(2)循环结束条件:当中间变量list长度与数组索引i相等时,保存list,返回空。
(3)将nums[i]添加进list
(4)选择该元素:递归执行digui(i+1,nums,alllist,new ArrayList<>(list))
(5)不选择该元素:弹出最后一个元素,list.remove(list.size()-1)
(6)递归执行digui(i+1,nums,alllist,new ArrayList<>(list))
注意:对于重复字符要进行剪枝,比如j>I && nums[j]==nums[j-1]
2.2 排列
2.2.1 回溯法
比如找到{1,2,3}数组的全排列
(1)首先创建一个函数用于回溯递归调用,函数包含了4个形参(访问过的元素的数组visited、数组nums、结果alllist、中间变量保存的list)
(2)循环结束条件:遍历整个nums数组,当list长度与nums长度相等时,return
(3)visited初始全为0
(4)如果添加某个元素,并且visited[i]==0:则执行list.add(nums[i]),并且将visited[i]置1,再递归执行digui(visited,nums,alllist,new ArrayList<>(list))
(5)如果不添加该元素,先将visited[i]置0,再执行digui(visited,nums,alllist,new ArrayList<>(list))
2.2.2 BFS广度优先遍历法
(1) 建立一个队列Queue<List>,从空集开始,首先添加一个空的list
(2) 循环结束条件:如果队列que不为空
(3) 获取队列当前的长度len,内部循环次数也就是len,给队列中的len个元素添加下一个元素
(4) 弹出队列的元素poll,如果当前list长度与nums相等,存储起来
(5) 再给list添加一个元素,遍历nums列表,从nums中依次添加,如果nums[i]再list中存在就不添加,反之添加进去,并将list添加到que,本次循环结束。
3、例题
十三、二分查找
1、二分查找
当你需要解决的问题的输入是排好序的数组,链表,或是排好序的矩阵,要求咱们寻找某些特定元素。这个时候的不二选择就是二分搜索。这种模式是一种超级牛的用二分来解决问题的方式。
计算模式:
(1)首先,算出左右端点的中点。最简单的方式是这样的:middle = (start + end) / 2。但这种计算方式有不小的概率会出现整数越界。因此一般都推荐另外这种写法:middle = start + (end — start) / 2
(2)如果要找的目标改好和中点所在的数值相等,我们返回中点的下标就行
(3)如果目标不等的话:我们就有两种移动方式了。如果目标比中点在的值小(key < arr[middle]):将下一步搜索空间放到左边(end = middle - 1);如果比中点的值大,则继续在右边搜索,丢弃左边:left = middle + 1
2、解题步骤
2.1 通常用于查找某个target值、或者查找比target大或小的值
2.2 采用左右两个指针left、right以及mid指针
如果中间值nums[mid]<=target,left = mid+1,反之right=mid-1
3、例题
十四、前K大数
1、前k大数
任何让我们求解最大/最小/最频繁的K个元素的题,都遵循这种模式。用来记录这种前K类型的最佳数据结构就是堆了,在Java中叫优先队列(PriorityQueue)。这种模式借助堆来解决很多这种前K个数值的问题。
(1)根据题目要求,将K个元素插入到最小堆或是最大堆。
(2)遍历剩下的还没访问的元素,如果当前出来到的这个元素比堆顶元素大,那咱们把堆顶元素先删除,再加当前元素进去。
注意这种模式下,咱们不需要去排序数组,因为堆具有这种良好的局部有序性,这对咱们需要解决问题就够了。
将数组分为最大和最小的两堆数这种模式也可以采用该方法:
双堆模式堆中保留的是最大的K个值,弹出的就是最小的K个值
2、解题步骤
(1)求最小的k个数,采用堆的数据jieg ,对应了java中的Queue接口的PriorityQueue实现类。
(2)首先在堆que中添加前arr.length-k个元素,该堆中的元素是自动局部排序的,依次遍历剩余的每一个元素。
(3)如果该元素不小于堆顶元素则弹出堆顶元素,将该元素添加进最小k个数的集合中;反之,如果该元素小于堆顶元素,则直接将该元素添加进最小k个数的集合中。
(4)对于要统计频次的排序,采用hashmap数据结构存储字符串出现的频次,如果添加前arr.length-k个元素也即是说要添加hashmap的键keys中的前k个,这要不好写程序,所以我们换一种思路。将最频繁的k个元素直接保留再队列中,由于队列是排序的,如果队列长度大于k,就将堆顶元素弹出,剩余再队列中的元素就是我们要的结果。
3、例题
十五、K路归并
1、K路归并
K路归并能帮咱们解决那些涉及到多组排好序的数组的问题。
每当你的输入是K个排好序的数组,你就可以用堆来高效顺序遍历其中所有数组的所有元素。你可以将每个数组中最小的一个元素加入到最小堆中,从而得到全局最小值。当我们拿到这个全局最小值之后,再从该元素所在的数组里取出其后面紧挨着的元素,加入堆。如此往复直到处理完所有的元素。
该模式是这样的运行的:
(1)把每个数组中的第一个元素都加入最小堆中
(2)取出堆顶元素(全局最小),将该元素放入排好序的结果集合里面
(3)将刚取出的元素所在的数组里面的下一个元素加入堆
(4)重复步骤2,3,直到处理完所有数字
该问题的关键需要找到弹出的堆顶元素所在的数组。
识别K路归并:
(1)该问题的输入是排好序的数组,链表或是矩阵
(2)如果问题让咱们合并多个排好序的集合,或是需要找这些集合中最小的元素
2、解题步骤
2.1 采用最小堆的方法,还是选择优先队列的数据结构PriorityQueue
2.2 依次将每个数组的第一个元素添加进优先队列中,弹出堆顶元素。注意:放入堆的元素怎么才能找到它的下一个元素这是最重要的,所以放入堆的元素可以采用二维数组保存元素的行和列的位置pos[i,j]。这样下一个元素就是pos[i,j+1]
十六、二叉树系列
包括二叉树的遍历:前序、中序、后序遍历
递归遍历和非递归遍历
二叉树的非递归遍历可以借助栈数据结构。
对于二叉树的重建、二叉树的子结构二叉搜索树与链表的转换等问题
对于二叉树系列的递归解法也是具有一定套路的:
(1)确定递归的结束条件
通常是:if(root==null) return null;
(2)递归遍历左子树和右子树
digui(root.left);
digui(root.right);
注意:对于递归的思考方法,我们通常都会陷入一个误区。我们通常会去思考递归的每一步执行的过程,但是通常会使被复杂的递归调用给绕晕。
递归的一个非常重要的点就是:不去管函数的内部细节是如何处理的,我们只看其函数作用以及输入与输出
。
比如1:对于求解二叉树的最大深度
我们可以转换成一下几步:
- 创建一个递归函数,返回该树的最大深度
- 找到左子树的最大深度
- 找到右子树的最大深度
- 返回左子树的最大深度和右子树最大深度中的较大值
比如2:leetcode 114. 在原二叉树的基础上将其展开为链表
- 创建一个递归函数,返回该树的链表
- 首先将根节点的左子树变成链表
- 其次将根节点的右子树变成链表
- 最后将变成链表的右子树放在变成链表的左子树的最右边
例题1
例题2
十七、链表
各类链表的问题,找出链表的公共节点
十八、进制转换和位运算
对于int型数据转换成其他进制,比如二进制:
1、除基倒取余法
对于正数可以用除基倒取余法得到二进制:由于是整数类型,所以二进制必须为32位,不够补零。
对于负数:先求出正数的二进制,再取反加1。
2、利用“移位”操作实现
例题
十九、整数溢出判断
整数溢出的判断有很多种,这里我们介绍在整数相加过程中的溢出判断。对于两个数num1和num2之和是否溢出判断方法:
1、 不能直接将两个数相加,再与MAX和MIN进行比较,因为两数相加后如果和大于MAX,就会溢出变成-MAX,进而无法比较
2、 判断是否溢出最大值:num1 > Integer.MAX_VALUE - num2
3、 判断是否溢出最小值:num1 < Integer.MIN_VALUE - num2
例题
二十、贪心算法
二十一、数组内的上下左右移动
我们通常会遇到这种类型的题目,给定一个数组,每次只能移动一个位置,也就是上下左右,问我们是否能够找到给定的字符串。或者从数组的左上角移动到右下角一共有多少种路线等问题。
对于这类问题,我们可以采用回溯法或者动态规划的方式来求解,由于这种类型的题目出现比较多,并且比较特殊,所以我单独拿出来分析。