核心算法思想
遍历:深度优先遍历、广度优先遍历
子问题:动态规划、贪心(维护最值问题)、分治(快排;用的很少)
双指针:快慢指针、左右指针、滑动窗口(可变、固定)
二分法:二分查找
空间换时间:前缀和(经常与HashMap搭配)、线段树(需要背诵构建过程)、动态规划、记忆化搜索(提前存储)
暴力:遍历(深度优先遍历最常用的就是递归)
输入提示:如果输入的n不大,可以将所有结果提前存储起来,相当于空间换时间.
**逆向思维:**从右向左;先卖出后买入
(ps:仅仅列出相关思想名称,具体思路、常见应用场景请看另一篇)
常用数据结构
哈希表:HashMap 、 HashSet
栈:普通栈、单调栈(应用场景:在一维数组中找第一个满足某种条件的数)
队列:普通队列、单调队列、优先队列(大/小根堆)
(ps:一般的栈与队列在java中底层结构一致,只不过用的方法不同)
特殊类型(链表、树)题目技巧
链表
链表题目
总结:
方法及结构:
空间换时间:list、数组
双指针(尤其是快慢指针)
递归:
两种具体的编写原则:
1.选择:本次有n种选择,通过遍历所有选择,找到最优解,一般返回值都是void.
此外,一般选择问题都需要-1回到之前的状态。
2.目的(这种比较难理解,需要多做题):本次局部目的,一般返回值不是void,基本上都有返回值因为是局部目的。
目的问题不需要回到之前的状态,而是所有选择都需要。
共同点:(1)边界条件(一般都是null或者等于某个值) (2)具体处理方式
题目实例:
选择:
树(二叉树)
前中后遍历常见含义:
中序:对于二叉搜索树,升序遍历
前序:先获取(操作)根,再获取左右子节点。适合复制一棵树。(一般的深度优先遍历就是前序遍历,操作然后递归到左右子节点)。
后序遍历:先获取左右子节点,在获取根。适合删除一棵树。
图论
常见数组:
(1)visited[] : 访问数组用来标记某个位置是否被使用
(2)int[][] directions = {{0,1}, {1,0}, {0,-1}, {-1,0}};
java
int[][] dirs = {{0,1}, {1,0}, {0,-1}, {-1,0}};方向数组,分别表示右下上左
for (int[] dir : dirs) {
int ni = i + dir[0]; // 新的行坐标
int nj = j + dir[1]; // 新的列坐标
}
递归
模板:
思路:
(1)
(2)
(3)
二分查找
目的:在logn复杂度下实习查找,前提有数组需要具有一定的规律。
模板:
java
class Solution {
public int searchInsert(int[] nums, int target) {
int n = nums.length;
int left = 0, right = n - 1, ans = n;
while (left <= right) {
int mid = ((right - left) / 1) + left;
if(target == nums[mid]){
ans = mid;
return ans;
}
else if (target < nums[mid]) {
ans = mid;
right = mid - 1;
} else {
left = mid + 1;
}
}
return ans;
}
}
贪心算法
常见场景:最值
目的:通过局部最优解得到全局最优解。
模板: 维护一个最值(局部最优解),根据该最值,计算下一步
动态规划
常见场景:最值;状态转移?
状态定义:
(1)以第i个为终点(不必选),目前最值是多少。
状态转移方程:第i个选不选,然后取最大值。
(2)以第i个为终点(必选),目前最值是多少。
状态转移方程:dp[i]由前面经过操作得到。
(2)以第i个为终点(必选),目前最值是多少。
状态转移方程:dp[i]由前面经过操作得到。
初始状态:
状态转移函数:
输出:
状态的数量:dp[n]还是dp[n+1]
遍历方式:从前往后、从后往前、交叉
未完成
108. 将有序数组转换为二叉搜索树 - 力扣(LeetCode)
105. 从前序与中序遍历序列构造二叉树 - 力扣(LeetCode)
208. 实现 Trie (前缀树) - 力扣(LeetCode)
4. 寻找两个正序数组的中位数 - 力扣(LeetCode)
215. 数组中的第K个最大元素 - 力扣(LeetCode)快排未完成
85.编辑距离
86.多数元素
题目总结
核心算法思路:简单推理
数据结构:HashMap
技巧:
(1)由于不能用相同的数,所以不用全部直接放进去,可以在放入该值之前就进行一次判断,这样就不用担心取到同一个数了。
注意点:
(1)不能重复使用同一个数,因此一开始不能将所有数全部存储哈希表中。(ps:存储也可以,但需要增加一步校验(i!=j)确保该值没有被(自己)使用过。)
核心算法思路:简单推理
数据结构:HashMap
技巧:
(1)与两数之和,基本上一模一样,只不过加一层循环,这层循环的便是target.
(2)常见函数:
List排序:
Collections.sort(list);//升序排序
Collections.sort(list,Collections.reverseOrder());//降序排序
针对List是数组(两个元素)的排序
Collections.sort(list,(a,b) -> b-a);//降序排序
HashSet转化为List:List<List<>> list = new ArrayList<>(set);
数组转为List: list = Arrays.asList(array)
核心算法思路:简单推理
数据结构:HashMap
技巧:
(1)利用26字母有限的特点,将异位词统一。
具体方式:
(1)记录字符串26字符的个数:'char' - 'a'
(2)将字符数组转化:如果数量不为0,则输出'a'+int
注意点:
(1)不能重复使用同一个数,因此一开始不能将所有数全部存储哈希表中。(ps:存储也可以,但需要增加一步校验(i!=j)确保该值没有被(自己)使用过。)
核心算法思路:简单推理:找到起点方式
数据结构:HashSet
技巧:利用HashSet在O(1)复杂度下找到某个序列的起点,然后确定该起点的长度,最后对比获取最大长度。
核心算法思路:双指针
数据结构:快慢指针
技巧:
要求"原地"的一般只能是双指针来完成交换。
具体方式:
(1)slow指的是0的位置,也就是slow指针等待fast找到最近的非零数与其交换。
核心算法思路:双指针
数据结构:左右指针
技巧:
面积题目:长 与宽的确定,一般来说都是固定其中一个然后遍历另一个,但是这种方式的时间复杂度较高,不符合题目要求。
具体方式:
我认为这个题目算是一个背诵题,不是太容易想到。
左右指针的移动条件:高较小的指针进行移动,因为只有变化较小的,才可能获取比当前更大的面积。
核心算法思路:空间换时间(动态规划);单调栈;双指针
数据结构:记忆化搜索(数组)
技巧:
方法一:动态规划
(1)计算每个位置左右两侧的最值问题:可以用记忆化搜索两次遍历将最值提前获取,避免每次都要针对某个位置获取其最值。
具体方式:
(1)从左向右找到找到每个位置左侧的最值
(2)从右向左找到找到每个位置右侧的最值
方法二:单调栈
搁置,这个真想不到用栈呀。o(╥﹏╥)o
方法三:双指针
如果用双指针我认为便是一个背诵题目,极难想到。
基本目标:找到某个位置两侧的最高值中较低的一个。
官方题解双指针思路解释:
以左侧为例(left指针<=right指针时):left指针能走到当前位置,说明右侧一定存在一个值比我左边所有值都大,因此说明我左侧最大值肯定是最值中较小的一个,这样便能确定该位置对应的雨水量。
7.3. 无重复字符的最长子串 - 力扣(LeetCode)
核心算法思路:双指针
数据结构:HashMap
技巧:
(1)左右指针确定一个滑动窗口。
(2)HashMap用来确定最新的重复字符的位置。
具体方式:
(1)左指针是起点,右指针指的是最新的位置
(2)当右指针所指的字符在HashMap中存在时,left跳跃到该位置的下一个,并且计算刷新current的值,并检验是否更新最大值。
注意点:
(1)如果fast指向的字符在hashmap中存在,还需要判断在这个map中该字符的位置是否大于等于 left**,**只有大于等于才需要跳转,否则就跑到前面了,没有意义。
8.438. 找到字符串中所有字母异位词 - 力扣(LeetCode)
核心算法思路:双指针(固定滑动窗口)
数据结构:List(固定长度)
技巧:
(1)利用字母有限性:将字符串相等转换为数组是否相等。
数组判断函数:Arrays.equal(arr1,arr2);
具体方式:
(1)先将初始长度的数组构建起来。
(2)for循环遍历,将数组第一个位置对应字符--,下一个位置(i+len)对应字符数++
9.560. 和为 K 的子数组 - 力扣(LeetCode)
核心算法思路:空间换时间(前缀和)
数据结构:HashMap
技巧:
(1)利用HashMap查找前面前缀和符和要求的数量。
具体方式:
(1)将所有前缀和存储起来,key是前缀和,value是数量。
注意点:
(1)不要将全部的前缀和提前存储,否则可能是查询到后面的前缀和,不符合要求。因此想要只查询前面的前缀和,需要遍历加入。
(2)千万别忘记map.put(0,1),需要提前将一个前缀和为0,次数为1的提前放进去。
10.239. 滑动窗口最大值 - 力扣(LeetCode)
核心算法思路:简单推理
数据结构:优先队列(大根堆);单调队列
技巧:
方法一(优先队列):
(1)构建大根堆,存储的为(value,index)
具体方式:
(1)利用前k个值构建初始窗口(大根堆)。
(2)每次来新数,便peek堆顶的元素,判断是否过期,如果没有过期则为答案,如果过期则pop,直到找到没有过期的最大值。
注意点:
(1)优先队列(大根堆)构建方法。
java
PriorityQueue<int[]> pq = new PriorityQueue<int[]>(
new Comparator<int[]>(){
public int compare(int[] pair1,int[] pair2){
return pair1[0] != pair2[0] ? pair2[0] - pair1[0] : pair2[1] - pair2[1];
}
}
);
方法二(单调队列):
具体方式:
(1)for k次来构建初始双端队列。
(2)每次来新数,while循环队列不为空并且队尾的值小于新值,则pop队尾元素,最后加入该新值,while循环peak队首元素是否为过期,如果过期则pop,否则作为答案。
核心算法思路:双指针(快慢指针)
数据结构:HashMap
技巧:
(1)相较于之前的利用26个小写字母作为判断字符串是否相等的方式,存在一个弊端,那就是必须字符串str必须全部为小写或者全部为大写。用HashMap可以进行优化,这样可以存储大小写甚至数字。
具体方法:
(1)快指针先找到包括所有子串字符的字符串。
(2)慢指针开始移动,缩小到当前最短位置。记录刷新答案子串的strat与end.
(3)慢指针移动到下一个位置。
注意点:
(1)遍历HashMap的方式:
java
for(Map.Entry<> entry : map.entrySet()){
entry.getKet();
entry.getValue();
}
核心算法思路:动态规划;线段树
数据结构:无(数组)
技巧:
动态规划使用条件:当前值能否由上一个状态计算得来
核心:
(1)dp的定义:以i结尾的最大值
(2)状态转移方程:dp[i] = dp[i-1] + nums[i]; //dp[i-1]>0
(3)遍历方式:从左向右递增即可
补充:线段树非常适合解决这个问题
核心算法思路:简单推理
数据结构:无(数组)
技巧:
(1)将数组按照第一个元素的大小进行升序排序。
(2)for遍历,并与答案列表中最后一个元素进行对比。
注意点:
(1)单个元素为数组的排序方式:
java
Arrays.sort(nums,new Comparator<int[]>(){
public int compare(int[] p1, int[]p2){
return p1[0] != p2[0] ? p2[0]-p1[0] : p2[1]-p1[1];
}
});
(2)将list转化为数组的方式:
java
int[][] arr = merged.toArray(new int[m][]);
核心算法思路:简单推理
数据结构:无(数组)
技巧:
方法1:取模构建新数组
方法2:双指针原地交换
方法3:翻转数组(背诵题):三次翻转
核心算法思路:空间换时间
数据结构:无(数组)
技巧:
(1)提前存储好当前位置左右两侧的乘积即可。
核心算法思路:简单推理
数据结构:HashSet
技巧:
(1)利用HashSey在O(1)时间复杂度下判断i(1-n)是否存在,返回结果。
注意点:
(1)技巧存在一个问题:空间复杂度是O(n)不符合要求。
以后再认真解决这个问题吧,感觉有些难o(╥﹏╥)o
核心算法思路:简单推理
数据结构:无
技巧:
(1)for循环遍历,找到需要置零的行数组与列数组即可。
(2)优化空间复杂度,将第一行与第一列作为标记数组,遇到等于0的则,打上标记也就是对应第一行/列置0。值得注意的是第一行与第一列需要单独判断,其本身是否包括0,如果不包括就不变,如果包括就全部置零。
核心算法思路:简单推理
数据结构:无
技巧:
(1)设计一个上下左右的二维数组,即右:(1,0) 下 :(0,1) 左: (-1,0) 上: (0,-1)
具体过程:
(1)到达边界后,切换方向也就是x=ans[(i+1)%4][0]; y = ans[(i+1)%4];
(2)结束条件是list中的数量等于amount.
注意点:
(1)技巧存在一个问题:空间复杂度是O(n)不符合要求。
核心算法思路:简单推理
数据结构:无
技巧:
(1)这个题目是一个公式推理题,推出了i与j转换后的公式,遍历替换即可。
具体过程:
arr[i][j] --> arr[j][cow-i]
核心算法思路:简单推理;二分法
数据结构:无
技巧:
方法1:Z字查找:从右上角开始遍历
方法2:先确定在行数,在进行二分查找
具体过程:
(1)到达边界后,切换方向也就是x=ans[(i+1)%4][0]; y = ans[(i+1)%4];
(2)结束条件是list中的数量等于amount.
注意点:
(1)技巧存在一个问题:空间复杂度是O(n)不符合要求。
核心算法思路:简单推理;
数据结构:HashSet
技巧:
HashSet辅助查找是否存在某个节点。
具体过程:
将其中一个链表的所有节点存储到HashSet,然后遍历另一个链表,如果有存在的则返回该节点,如果没有直接返回null.
核心算法思路:简单推理
数据结构:链表(题目)
技巧:
(1)前节点(prev),当前节点(curr),后节点(nex)。
(2)核心思想就是让当前节点指向前节点,但不能丢失后节点。
注意点:
(1)while(cur != null) 作为循环条件,如果以cur.next作为循环条件,最后一个为cur的时候,next为null,便进入不了循环,也就意味者,最后一个节点没办法指向前一个节点。(其实也可以以cur.next作为条件,只不过需要再额外写一步)
核心算法思路:简单推理
数据结构:链表(题目);普通数组
技巧:
方法1:反转链表,可以抛弃比较麻烦。
方法2:将链表的值复制到数组中,然后从后向前遍历比较
核心算法思路:简单推理;双指针
数据结构:HashSet;快慢指针
技巧:
**方法1:**存储节点到哈希表中,判断是否存在(while(head))
方法2: 可以想到快慢指针 ,但是比较难,算是一个背诵题,目前不打算背诵,如果后面用到再进行背诵。
核心算法思路:简单推理
数据结构:List
技巧:
方法1:空间换时间:List<ListNode> 存储所有节点,利用List排序解决。
方法2:简单推理,依次比较两个链表的节点值,值小的向后移动。
循环条件:(while L1 !=null && L2!=null)
核心算法思路:简单推理
数据结构:链表(题目)
技巧:
(1)设计进位标记值jin与哨兵节点。
(2)
while(L1!=null || L2!=null) 都为null才退出循环,提前为null的值设置为0;(这种循环更便捷)
while(L1!=null && L2!=null) 其中一个为null就退出,但是需要额外判断哪一个为null,需要写两个相同的循环。
核心算法思路:简单推理;空间换时间;双指针
数据结构:List;HashMap;快慢指针
技巧:
方法1(空间换时间):所有节点存储到List中,直接遍历即可
方法2(HashMap):所有节点存储到Map中,key为位置,value为节点。
方法2(双指针):让快指针前k+1个slow位置循环条件while(fast !=null)
核心算法思路:简单推理;空间换时间
数据结构:List;
技巧:
方法1(空间换时间):所有节点存储到List中,在List中遍历交换,然后遍历输出。
方法2(简单推理):prev、curr、nex三个节点交换;
循环条件 while( curr !=null && curr.next !=null)
注意点:
(1)循环条件的确立,需要先判断迭代一次需要哪些节点参与,比如本题需要四个节点参与。
此外最好不要在循环前提前写next(哨兵节点除外),用现有的条件直接完成循环即可。
核心算法思路:简单推理(模拟);空间换时间
数据结构:List;
技巧:
方法1(空间换时间):所有节点存储到List中,在List中遍历倒序,然后遍历输出。
方法2(模拟):可以想到,但是有很多细节问题,当做一个背诵题吧
(1)reverse(startNode,endNode)翻转链表中,循环while(pre !=end),因为需要将最后一个节点也翻转,如果以curr作为循环,则会缺少一个节点。
(2)判断剩余节点是否够k个,提前写 ListNode tail = pre,然后循环k次,找到最后一个节点end。
注意点:
(1)前置节点pre 一般在while循环前写好,因为pre会在while中用到.
(2)pre永远不可能为null
(3)求k个node,需要从pre开始遍历k次.
核心算法思路:简单推理(模拟)
数据结构:HashMap
技巧:
两次遍历:
(1)第一次遍历创建新旧节点映射表,具体为HashMap<Node,Node> 旧节点 与新节点映射表
(2)第二次遍历:clone.next = map.get(curr.next);
核心算法思路:空间换时间;归并排序
数据结构:数组;
技巧:
方法1:将节点的值存储为数组,然后排序,之后创建链表返回。
方法2:背诵题(自底向上归并排序):非常麻烦,目前不打算完成了。
核心算法思路:空间换时间;模拟
数据结构:List<Node>;优先队列
技巧:
方法1(List):将所有节点全部放入List,按照val进行排序后,最后遍历返回。
方法2(模拟):顺序合并,依次合并,可以编写一个合并函数,返回合并的头结点。用ans来维护即可。
方法3(优先队列):优先队列维护每个list最小的节点
具体方法:
(1)将k个list的头节点全部入队。
(2)根据优先队列的性质,如果队列不为null,从中取出第一个节点,加入ans中。
(3)然后如果该节点的下一个节点存在则入队。
树
核心算法思路:递归;迭代
数据结构:栈
技巧:
方法1(递归):采用根左右的递归方式返回即可。
方法2(栈):背诵题
具体方法:
(1)将某一个节点的所有左节点入栈,然后pop出栈中的元素,将其加入ans中。
(2)root = root.right继续重复。循环条件while( root != null && !stack.isEmpty() )
注意点:
(1)针对递归可以设计一个全局变量ans用来保存结果,因此递归函数可以是void.
核心算法思路:遍历(广度优先遍历)、深度优先遍历
数据结构:队列;递归
技巧:
方法一: 利用队列先进先出的特点,进行广度优先遍历,记录层数即可。ps:需要记录每层的节点数量,需要poll出全部的节点才能进入下一层。
**方法二:**深度优先遍历,每次都有两种走向,当访问到null,取最大值即可。
核心算法思路:遍历(广度优先遍历)、深度优先遍历
数据结构:队列;递归
技巧:
方法一: 利用队列先进先出的特点,进行广度优先遍历,针对每个节点交换其左右子节点即可。
**方法二:**深度优先遍历,每次都有两种走向,当访问到null,每次到一个节点交换即可。
核心算法思路:遍历(广度优先遍历)、深度优先遍历
数据结构:队列;递归
技巧:
方法一(队列):同样还是队列,将节点的左节点、右节点同时poll出,然后比较,最后入队。while(!queue.isEmpty())
方法二(递归):判断u.left 与 v.right 以及其相反,其本身是否相等。
注意点:
(1)如果节点是null,还是能放入队列,具体效果如下:{1,2,null,2,3}
(2)null与int比较会报错,注意校验。
核心算法思路:深度优先遍历
数据结构:递归
技巧:
目的:设计一个全局遍历在递归过程中记录,求某个节点左子树的最大深度与右子树的最大深度+1.
递归的目标:求子树的深度(背诵题)
核心算法思路:广度优先搜索
数据结构:队列(背诵题)
核心算法思路 :中序遍历(两种方式也可以背诵一下)
数据结构:递归
技巧:
- 中序遍历->升序遍历,之后一一校验即可。
核心算法思路:中序遍历
数据结构:递归
技巧:
- 中序遍历->升序遍历,之后返回第k个即可(比如用List辅助)
核心算法思路:层序遍历
数据结构:队列
技巧:
(1)存储List<List<Node>>,返回每层的最后一个节点即可。
核心算法思路:前序遍历
数据结构:迭代,List
技巧:
(1)存储List<Node>,存储前序遍历的节点,最后构造单链表即可。
核心算法思路:深度优先遍历;前缀和
数据结构:递归;HashMap
技巧:
方法一(前缀和):
前序遍历(深度优先搜索)找到前面节点中符合条件的前缀和节点数量。
方法二(暴力遍历):
以每个节点为起点进行深度优先遍历,用全局变量ans记录答案。
可以先用一个list记录所有的节点,然后for循序进行遍历求解记录ans.
核心算法思路:深度优先遍历;
数据结构:HashMap+HashSet
技巧:
(1)通过深度优先遍历再利用HashMap存储所有的父子节点,其中key是子节点,value是父节点。
(2)然后将其中一个节点的parent全部放入HashSet中,然后另一个节点通过HashMap找到最先存在的公共祖先节点。
注意点:
(1)java中hashmap中,如果get(key),如果这个key不存在会返回null.
核心算法思路:(后序遍历)深度优先遍历+动态规划;
数据结构:无
技巧:
(1)利用动态规划的思想,后序遍历得到子节点的最大值,才能确定当前节点(下一个节点)的值。
图论
核心算法思路:深度优先遍历
数据结构:visited数组、dirs[][]方向数组
技巧:
(1)没有技巧直接双重for所有节点,判断该节点是否被访问&&是否等于1,如果是则岛屿数量+1。注意每次+1后都需要深度优先遍历更新visited.
注意点:
(1)可以不用visited,直接修改原数组为1即可。
核心算法思路:广度优先遍历
数据结构:队列;visited;direction
技巧:
(1)利用队列实现广度优先遍历,最后判断感染的节点数与所有非0节点数量是否相同(直接遍历是否还存在新鲜橘子也可以,更加安全)
具体步骤:
(1)队列存储所有初始被感染节点的位置({x,y})。
(2)循环while(!queue.isEmpty()) 计算其所有节点的数量n并poll出来n次,进行感染,重新入队。注意利用visited保障不要重复入队(也可以不用visited,因为每次在入队前都需要先判断该位置是否为新鲜节点1,如果是则修改为感染节点2,然后将其入队。)。
(3)最后计算所有非0节点数量与所有被感染的数量是否相同。(直接遍历是否还存在新鲜橘子也可以,更加安全)
多种方法:
(1)队列存储位置方式:
- 直接存储数组{x,y}
- 存储数值(x*C + y),获取时x=value/C; y = value%C
(2)ans计算方式
- 每次循环时ans+1,然后计算队列中的元素数,然后poll相应的次数,进行感染。
- 利用HashMap<Integer,Integer>,key是节点(位置),value是时间。每次被感染后存储位置ncode;时间=map.get(node)+1.
核心算法思路:广度优先遍历;
数据结构:队列;HashMap
技巧:
拓扑排序**背诵题:**入度与出度
(1)第一步找到入度为0,也就是起始节点,将它们全部入队。
(2)循环(队列不等于null),找到该节点为入度的list(这里需要提前存储key是前置节点,value是一个list表示以key为前置节点的集合),将其入度-1,同时判断减1后,入队是否为0,如果为0则入队。
(3)每次出队一个便是课程数+1,最后返回判断。
回溯
核心算法思路:回溯
数据结构:visited[]
技巧:
(1)输出的长度确定:每个位置(固定的)有n种选择机会,但选择时需要用访问数组确认这个是否被选择过。
核心算法思路:回溯
数据结构:无
技巧:
(1)输出长度不确定:输入每个位置(输入的组)有2种选择机会,要么选要么不选。
核心算法思路:回溯
数据结构:HashMap<String,String>
技巧:
(1)输出的长度确定:每个位置(输入的组)有对应3-4种选择机会,分别遍历。
核心算法思路:回溯
数据结构:无
技巧:
(1)输出的长度确定:每个位置(输入的组)有对应3-4种选择机会,分别遍历。
核心算法思路:回溯
数据结构:无
技巧:
(1)输出的长度确定:每个位置(输入的组)有对应2种选择机会,分别遍历.但是需要注意的是每次选择需要进行判断,如果当前"(" 不大于n,则可以放,")"不大于左括号可以放。
核心算法思路:回溯
数据结构:visited[][];dirctions[][]
技巧:
(1)**长度固定:**纯遍历+回溯,需要注意的是判断边界与是否访问。
核心算法思路:动态规划+回溯
数据结构:无
技巧:
这种长度完全不固定,并且有多种结果,需要for循环遍历每种可能。
(1)动态规划求出从i到j的字符串是否回文,进行记录。
从后往前遍历进行状态转移:
java
for (int i = n - 1; i >= 0; --i) {
for (int j = i + 1; j < n; ++j) {
f[i][j] = (s.charAt(i) == s.charAt(j)) && f[i + 1][j - 1];
}
}
(2)回溯以每一个i为起点,j为终点的字符串,如果true则进行进行下一步。
核心算法思路:回溯
数据结构:visited[][]
技巧:
(1)按行遍历,而不是按格子遍历;并且每次n种选择选择。
(2)难点就是对角线的判断。
(3)因为需要进行回退,所以需要将visited进行填充的时候,需要时+-1,而不是直接设置0/1来判断该位置是否能被选择。
java
public static boolean areOnSameDiagonal(int n, int x1, int y1, int x2, int y2) {
// 检查是否在同一正对角线(左上到右下):行列差相等
boolean sameMainDiagonal = (x1 - y1 == x2 - y2);
// 检查是否在同一反对角线(右上到左下):行列和相等
boolean sameAntiDiagonal = (x1 + y1 == x2 + y2);
return sameMainDiagonal || sameAntiDiagonal;
}
二分查找
核心算法思路:二分查找
数据结构:无
技巧:
(1)按照模板遍历查询,大于目标值的时候,赋予ans=mid即可。
核心算法思路:二分查找
数据结构:无
技巧:
(1)方法一:找到目标行,二分查找即可。
(2)方法二:Z字搜索之前做过。
核心算法思路:二分查找
数据结构:无
技巧:
(1)分别寻找两侧边界,按照模板写即可,在找到一个值之后,根据寻找的是左边界还是右边界继续寻找,而不是结束。
核心算法思路:二分查找
数据结构:无
技巧:
(1)先根据nums[n-1]确定mid落地,也就是落在前半段还是后半段。
(2)根据nums[0]与nums[n-1]进行条件判断,right与left移动。
核心算法思路:二分查找
数据结构:无
技巧:
核心目标:找到前半段,然后再找到第一个数。
(1)先根据nums[n-1]确定mid落地,也就是落在前半段还是后半段。
(2)根据nums[0]与nums[n-1]进行条件判断,right与left移动。
栈
核心算法思路:逻辑推理
数据结构:栈
技巧:
栈能够优先处理当前最新的事务,正好与本题契合,每次需要判断左括号的类型,并且需要将之前没有处理的进行存储。
(1)每次遇到右括号,判断最新存储的左括号否匹配,匹配则继续,否则返回false.
核心算法思路:逻辑推理
数据结构:栈
技巧:
栈 能够优先 处理当前最新的事务,正好与本题契合,每次需要判断左括号的类型,并且需要将之前没有处理的进行存储。
(1)创建两个栈,一个栈存储数字,另一个栈存储string
(2)每次遇到"["代表新元素的到来,将前面的的num与str分别存储到两个栈中。
(3)当遇到"]"时代表需要将数字pop出,然后将合成的str循环拼接,之后pop字符串栈中最新的数据再进行拼接。
核心算法思路:逻辑推理;空间换时间
数据结构:单调栈
技巧:
(1)方案1:单调栈 其实不算一种特意 处理的一种结构,它是根据逻辑推理自动具有了单调性。
(2)方案2: 根据输入范围 进行优化,温度范围是30-100,所以可以提取存储所有温度的位置,注意从后往前存储。
单调栈具体方案
(1)每次将遇到的值进行判断,如果大于栈顶元素,则pop,并将其ans置为当前位置。
(2)如果小于则进栈。
(3)最后将栈中剩余的元素的ans置0.
空间换时间具体方案
(1)从后往前存储各个温度的最考前的位置。
(2)遍历大于当前温度的位置。
注意点:
(1)单调栈存储的元素,可以是数组,arr[0]是温度值,arr[1]是位置.或者新建一个tems数组存储温度,而栈存储位置。每次判断就是t>tems[stack.peek()].
核心算法思路:逻辑推理
数据结构:单调栈(递增)
技巧:
暴力面积题目:
(1)固定高:向两边扩散,分别找到两侧第一个小于当前高的位置。
(2)固定起点:找到最矮的一个高,算面积。
优化暴力方案一:
(1)目的是找到该位置两侧首次小于当前高度的位置。
(2)两次遍历:从左向右遍历,每到一个位置就与栈顶元素比较,如果小于栈顶元素,则出栈,直到找到不小于该高度的值,并记录。从右向左遍历同理。
堆
核心算法思路:分治(快排);空间换时间(看输入提示)
数据结构:堆;
技巧:
方案一:大根堆,但是时间复杂度为n*logn
方案二:桶排序
方案三:快排优化(背诵)
核心算法思路:逻辑推理
数据结构:大根堆
技巧:
方案一:大根堆,将元素放入大根堆中,然后poll出k次即可。大根堆创建如下
java
// int[] 的第一个元素代表数组的值,第二个元素代表了该值出现的次数
PriorityQueue<int[]> queue = new PriorityQueue<int[]>(new Comparator<int[]>() {
public int compare(int[] m, int[] n) {
return m[1] - n[1];
}
});
方案二:逻辑推理:将hashmap转换为list,然后进行sort排序,输出k次即可。具体代码如下
java
import java.util.*;
public class HashMapSortByValue {
public static void main(String[] args) {
HashMap<Integer, Integer> map = new HashMap<>();
map.put(1, 50);
map.put(2, 30);
map.put(3, 80);
map.put(4, 10);
// 将entrySet转换为List
List<Map.Entry<Integer, Integer>> list = new ArrayList<>(map.entrySet());
// 按value升序排序
Collections.sort(list, new Comparator<Map.Entry<Integer, Integer>>() {
@Override
public int compare(Map.Entry<Integer, Integer> o1, Map.Entry<Integer, Integer> o2) {
return o1.getValue().compareTo(o2.getValue());
}
});
// 转换为LinkedHashMap保持顺序 也可以不转化
LinkedHashMap<Integer, Integer> sortedMap = new LinkedHashMap<>();
for (Map.Entry<Integer, Integer> entry : list) {
sortedMap.put(entry.getKey(), entry.getValue());
}
System.out.println("升序排序: " + sortedMap);
}
}
贪心算法
核心算法思路:贪心(逆向思维)
数据结构:无
技巧:
(1)逆向思维:假设第i天卖出,而不是买入再遍历。
(2)维护当前的一个**最值(小),**根据这个最值进行下一个计算。
注意点:
(1)每个位置都要遍历,因为需要更新最值,确保每一步都能得到一个最值。
核心算法思路:贪心
数据结构:无
技巧:
(1)维护当前的一个**最值(大),**根据这个最值进行下一个计算。
注意点:
(1)每个位置都要遍历,因为需要更新最值,确保每一步都能得到一个最值。
核心算法思路:贪心
数据结构:无
技巧:
(1)维护当前的一个**最值(大),**根据这个最值进行下一个计算(类似for循环中的i<max),但是这个max是会更新的,每次都取最值。
(2)在维护一个跳跃点(staM),每次到达该跳跃点则ans++,同时更新下一个跳跃点(curM)。
核心算法思路:贪心
数据结构:hashMap
技巧:
目标:找到子串的结束位置,但这个位置是会改变的,所以维护一个end位置(最值),这个最值是当前最好的位置,但是到达该最好位置之前,真正的最值会不断更新优化。
动态规划
核心算法思路:动态规划
数据结构:无
技巧:
状态定义:以i个为终点(必选)
初始状态:dp[0] dp[1](根据状态转移函数确定)
状态转移函数:dp[i] = dp[i-1] + dp[i-2]
输出:dp[n]
核心算法思路:动态规划
数据结构:无
技巧:
唯一难点:就是处理数据,思路很简单。
核心算法思路:动态规划
数据结构:无
技巧:
状态定义:以i个为终点(不必选)
初始状态:dp[0] dp[1](根据状态转移函数确定)
状态转移函数: dp[i] = Math.max(nums[i]+dp[i-2],dp[i-1]);
输出:dp[n]
核心算法思路:动态规划
数据结构:无
技巧:
任务:每个i肯定是dp[i-j*j] + 1(j*j)
状态定义:以i个为最少组成n的完全平方数(i为终点)
初始状态:dp[1] (根据状态转移函数确定)
状态转移函数:
minn = Math.min(minn, f[i - j * j]);
输出:dp[n]
核心算法思路:动态规划
数据结构:无
技巧:
任务:每个i肯定是dp[i-j] + 1(j)
状态定义:以i个为最少组成n的数量(i为终点)
初始状态:dp[0] (根据状态转移函数确定)
状态转移函数: dp[i] = Math.min(dp[i], dp[i - coins[j]] + 1);
输出:dp[n]
核心算法思路:动态规划
数据结构:无
技巧:
状态定义:以i为结尾的子串是否存在(i为终点)
初始状态:dp[0] (根据状态转移函数确定)空串肯定存在
状态转移函数:
if (dp[j] && wordDictSet.contains(s.substring(j, i))) --> dp[i] = true;
输出:dp[n]
注意点:
(1)快捷转化(List->set)
Set<String> wordDictSet = new HashSet(wordDict);
76.最长增长子序列
核心算法思路:动态规划
数据结构:无
技巧:
状态定义:以i为结尾的长度
初始状态:dp[0] (根据状态转移函数确定)
状态转移函数:
dp[i] = Math.max(dp[i], dp[j] + 1);
输出:dp[n]
时间复杂度:O(n*n)
**优化:**贪心+二分
辅助数组:p[i]代表长度为i+1,最后一个最小的数值
主循环遍历一次,每次遇到严格大于最后一个p[i]时,增加一个长度。
遇到严格小于p[i]时,二分查找替换第一个大于新值的位置。
注意点:
(1)贪心不容易理解:要让序列上升的更慢,才能找到更长的递增子序列。
(2)二分查找到第一个大于该新值的位置。
java
int l = 1, r = len, pos = 0,flag=0;
while (l <= r) {
int mid = (l + r) >> 1;
if(d[mid] == nums[i]){
flag=1;
break;
}
else if (d[mid] < nums[i]) {
pos = mid;
l = mid + 1;
} else {
r = mid - 1;
}
}
if(flag==0){
d[pos + 1] = nums[i];
}
核心算法思路:动态规划
数据结构:无
技巧:
状态定义:以i为结尾的长度
初始状态:dp[0] (根据状态转移函数确定)
状态转移函数:
dp[i] = Math.max(dp[i], dp[j] + 1);
输出:dp[n]
时间复杂度:O(n*n)
**优化:**贪心+二分
辅助数组:p[i]代表长度为i+1,最后一个最小的数值
主循环遍历一次,每次遇到严格大于最后一个p[i]时,增加一个长度。
遇到严格小于p[i]时,二分查找替换第一个大于新值的位置。
注意点:
核心算法思路:动态规划
数据结构:无
技巧:
状态定义:以i为结尾的最大最小值
初始状态:dp[0] (根据状态转移函数确定)
状态转移函数:
java
maxF[i] = Math.max(maxF[i - 1] * nums[i], Math.max(nums[i], minF[i - 1] * nums[i]));
minF[i] = Math.min(minF[i - 1] * nums[i], Math.min(nums[i], maxF[i - 1] * nums[i]));
输出:for循环最大值
时间复杂度:O(n)
注意点:
(1)结果范围是32整数,所以中间过程用long比较好一些,最后结果(int) ans;
80.最长有效括号
核心算法思路:动态规划
数据结构:无
技巧:
状态定义:以i为结尾的最大最小值
初始状态:dp[0] (根据状态转移函数确定)
状态转移函数:
java
maxF[i] = Math.max(maxF[i - 1] * nums[i], Math.max(nums[i], minF[i - 1] * nums[i]));
minF[i] = Math.min(minF[i - 1] * nums[i], Math.min(nums[i], maxF[i - 1] * nums[i]));
输出:for循环最大值
时间复杂度:O(n)
注意点:
(1)结果范围是32整数,所以中间过程用long比较好一些,最后结果(int) ans;
二维动态规划
核心算法思路:动态规划
数据结构:无
技巧:
状态定义:以i,j位置为终点,有多少种方式(必选)
初始状态:dp[0] [1-j]与dp[1-i][0](根据状态转移函数确定;或者理解)
状态转移函数:dp[i][j] = dp[i-1][j] + dp[i][j-1]
输出:dp[i][j]
核心算法思路:动态规划
数据结构:无
技巧:
状态定义:以i,j位置为终点,最小值(必选)
初始状态:dp[0] [1-j]与dp[1-i][0](根据状态转移函数确定;或者理解)
状态转移函数:dp[i][j] = dp[i-1][j] + dp[i][j-1]
输出:dp[i][j]
核心算法思路:动态规划
数据结构:无
技巧:
状态定义:以i,j位置为分别为起点与终点,是否为回文串
初始状态:dp[i] [i]=true(根据状态转移函数确定;或者理解)
状态转移函数:dp[i][j] = dp[i-1][j+1]
输出:dp[i][j]
遍历方式:
(1)起点是从后往前遍历;
(2)终点是从起点往后遍历。
核心算法思路:动态规划
数据结构:无
技巧:
状态定义:以i,j位置为分别为两个字符串的位置
初始状态:dp[0 ][0]、dp[0][1]、dp[1][0](根据状态转移函数确定)
状态转移函数:dp[i][j] = Math.max( dp[i-1][j] ,dp[i][j-1] ) 或者 (dp[i-1][j-1]+1)
输出:dp[i][j]
技巧
核心算法思路:技巧
数据结构:无
技巧:
异或运算,最后剩下的就是出现一次的。
java
int single = 0;
for (int num : nums) {
single ^= num;
}
核心算法思路:双指针
数据结构:无
技巧:
方法1:双指针
两次遍历:第一次将所有0放到最左边,第二次将所有2放到最右边
以第一次遍历为例:
(1)left指针位置为第一个不为0的位置。
(2)right指针一直向右遍历。
方法2:三指针:l、mid、r
循环:while( mid <= r)
遍历一遍也可以,l指针指向放0的地方,r指针指向放2的地方。
核心算法思路:双指针
数据结构:无
技巧:
方法1:双指针
两次遍历:第一次将所有0放到最左边,第二次将所有2放到最右边
以第一次遍历为例:
(1)left指针位置为第一个不为0的位置。
(2)right指针一直向右遍历。
方法2:三指针:l、mid、r
循环:while( mid <= r)
遍历一遍也可以,l指针指向放0的地方,r指针指向放2的地方。
注意点:
(1)结果范围是32整数,所以中间过程用long比较好一些,最后结果(int) ans;