1. 双指针
1.0 移动零
思路:快慢指针,快指针依次遍历数组,当遇到的是0,跳过,继续往后遍历,当遇到的不是0,将这个位置元素复制到慢指针,同时快慢指针都++,遍历完后,慢指针指向的位置依次填充0.
1.1 两数之和
思路:因为是有序数组,
1.2 三数之和
题目要求不能重复
思路:三数之和可以变为两数之和,固定一个数字,看两数之和,此时target变为target-固定的数字,要先对数组排序,然后依次固定一个数求两数之和。
避免重复解:
重复解有两种情况:
- 固定数字的时候 【-1,-1】已经固定过-1,得出了一组解,又有一个-1,得出得还是同一组解,重复-----> 解决:在固定数字的时候,判断前一个是不是和自己一样,一样就跳过(这样做是因为数组已经有序)
- 两数之和双指针寻找的时候,【-1,0,0,1,1】 -------> 解决:在双指针移动的时候,当左指针和它前面元素一样时,i++,当右指针和它后面一个元素一样时,j–
1.3 盛最多水的容器
思路:双指针解决:两个指针分别指向最左和最右,计算容量,因为最左和最右宽度是最大的,每次都是移动较低的那一边,这样宽度减小了但是高度增加了才有可能容量变大,每次移动后计算容量,做max,取大的。
1.4 接雨水
思路:单调栈解决:将柱子依次入栈,要入栈的柱子小于栈顶柱子高度时,可以入栈,当要入栈的柱子高度大于栈顶柱子的高度时(此时表示形成凹陷,可以接水),此时计算凹陷:将栈顶的这个柱子出栈,该出栈柱子的左边也就是现在的栈顶就是凹陷的左边,入栈的这个柱子就是凹陷的右边,取左右的最小高度,该最小高度减去出栈柱子的高度就是凹陷的高,左右之差就是凹陷的宽,算盛水量。这样依次尝试将所有柱子加入单调栈中,有凹陷就出栈,并计算容量,最后sum,得到总容量。
2. 字串
2.1 滑动窗口最大值
思路:单调队列解决(对头就是最大值,队列存索引),遍历数组将每个元素入队,小的话直接入,大的话吧队列中比当前元素小的出队,再入队。窗口移动的过程中,当左边界和队头一样时将队头出队。
2.2 最小覆盖子串
思路:统计target串中各个字符出现的次数,遍历源字符串(两个指针),统计各个元素的出现次数,当满足target要求时,右边界 j 指针停止,左边界 i
指针右移,每次右移都要更新出现的次数,若还满足要求则 i 指针继续右移缩小范围,不满足则 j 指针右移扩大范围。在每次满足要求时记录长度,取最小长度。
使用distance优化,当右指针向右移的过程中,当这个字符在窗口内出现的次数小于目标串出现的次数时,distance++,表示窗口内元素与目标串更接近了,注意等于的时候distance就不加了,以这样的方式当distance等于目标串的长度时,表示全覆盖了目标串,此时更新覆盖字串的位置,并让左指针右移,来尝试再找最小覆盖字串,当左指针指向的字符在窗口中出现的次数刚好等于目标串这个元素的个数时,distance–,表示窗口中与目标串相似程度变小了。
2.3 和为k的子数组
思路:用前缀和,依次算每个元素的前缀和,当两个前缀和相差为6时,表示有子数组和为6。将前缀和放入map中,key为前缀和,value为此前缀和出现的次数,依次遍历map中key,当此key - 6 在map中存在时,表示存在子数组和为6,注意:可能前缀和有重复的,当此key前缀和的value > 1 时,子数组个数为value,当此key的前缀和 - 6 的前缀和的value > 1时,子数组个数为此key的前缀和 - 6 的前缀和的value 。
3. 动态规划
3.1 爬楼梯
思路:动规五部曲:
- 定义dp[ ]数组 ---------到达 i 阶有dp【i】种走法
- 递推公式 ------dp【i】= dp【i-1】+dp【i-2】
- 初始化数组 ------边界
- 确定遍历顺序
- 打印dp数组
3.2 杨辉三角
3.3 打家劫舍
思路:五部曲
- 定义dp[ ]数组 ---------考虑第 i 个房子偷的最大钱数为dp【i】(0-i个房子)
- 递推公式 ------dp【i】= MAX(dp【i-2】+ num【i】,dp【i-1】)
- 初始化数组 ------边界为dp【0】和dp【1】,dp【0】=num【0】,现在就有0号房子,那就必偷了;dp【1】= max(num【0】,num【1】)表示现在有0号和1号房子,dp【1】表示考虑到第1个房子了也就是1之前的都考虑了,那咋偷?最大钱数就是哪个房子多偷哪个
- 确定遍历顺序
- 打印dp数组
递推公式:考虑第 i 个房子,第 i 个房子有两种状态(偷与不偷),偷的话:那前一个房子肯定不能偷,这时候最大钱数就是dp【i-2】+num【i】,表示考虑第i-2个房子再偷第i个房子的最大钱数,不偷的话:最大钱数就是dp【i-1】表示考虑到第 i -1 个房子所偷的最大钱数,不一定说第 i -1 个房子一定偷只是考虑到第i -1个了。
3.4 分隔等和子集
思路:要分为两个 子集,每个子集和一样并且加起来等于数组总和,可以这样想:原数组总和除2,计为target,使用0-1背包问题,此时重量和价值相同,看看能不能找到最大价值为target的子集,0-1又是不能重复的,那剩余的刚好也是等于target,找到。
3.5 零钱兑换–组合数
思路:完全背包问题(正序),可重复,并且求的是组合(两个for谁先)
组合:先遍历物品再遍历背包
排列数:先遍历背吧再遍历物品
3.6 零钱兑换—最小个数
4. 多维动态规划
4.1 最长回文子串
思路:不用动态规划,用中心开发的思想
两种情况:
- 回文子串是奇数(中点有一个字符 aba):依次遍历字符串,并认为该字符是中心,判断中心的前后字符是否一样,一样再判断前面的前面和后面的后面是否一样,并且用两个变量记录回文的left 和 right
- 回文字串是偶数(中点两个字符 abba):依次遍历字符串,每次取两个字符并且要相等,再以中心扩展,一样则记录回文的left 和 right
4.2 不同路径
思路:i,j 位置只能由上方或者左方来,i,j 位置的走法就是上方走法+左方走法
改进:二维变一维
4.3 最小路径和
思路:从左上走到右下的最短路径, i,j 位置只能由上方和左方而来,到 i,j 的最短路径就是左方和上次的较小值 + 本位置权值,只要考虑左边界和上边界就ok,左边界某个位置的最短路径=上面一个位置的最短路径+本权值,上边界=左边+本权值,起始位置就是原值,其他位置用表达式。
4.4 最长公共子序列
思路:动态规划思想,两个串的最长公共子序列,将问题分解,一个字符和串的最长公共子序列,–>两个字符和串的最长公共子序列,进而求出两个串的最长子序列,在初步扩大的过程中,只有两种情况,比较的两个字符相同,比较的两个字符不同,相同:取上一行上一列的值 + 1,表示在比当前长度小1的情况下最长公共子序列+1,不同的情况下取左边和上边值的最小值,表示要维持原来的最长公共子序列。
4.5 编辑距离
思路:求两个串的最短编辑距离,肯定是要遍历串的、要比较字符的,会有两种结果,两个字符一样或者俩个字符不一样。定义dp二维数组,dp【i】【j】表示串A的前 i 个字符和串B 的前 j 个字符的最短编辑距离,比较元素的时候,当 i 位置 和 j 位置 一样时,表明这个位置不用动,该位置的最短距离就是 i -1, j-1 位置的值,当比较的两个字符不一样时:考虑增、删、替换操作,其实就是三种决策,
增:增的是B串中 j 位置的字符,表明我们认为 A串中前 i 个和 B 串中前 j-1 个,这两个串是有最短编辑 距离
删:删的是A串中 i 位置的字符,表明我们认为 A串中前 i -1个和 B 串中前 j个,这两个串是有最短编辑 距离
替换:替换的是 A 串中的 i 位置和 B串中的 j 位置,表明我们认为 A串中前 i -1个和 B 串中前 j-1个,这两个串是有最短编辑 距离
5. 链表
5.1 LRU缓存:最久没使用的删掉
思路:map+双向链表(map存key和node,链表存node)
get 和 put方法都要在o(1)内完成,双向链表三个方法:头部插入、尾部删除、已知节点删除
get:先由key在map中找,有的话获取到node对象,然后在链表中将这个node节点删除,然后头部插入
put:先在map中由key找,看有没有,有的话就更新没有的话就新增,更新:在map中拿到node,更新node中的value,将node在链表中删除,再头部插入;新增:在map中没有的话,new一个node放入map中,同时链表中头部插入node,但是插入可能超出容量,判断一下容量是不是超过了,超过的话就删除链表尾部(LRU)的node,并且根据node中的key找到map中的这个node删除。
5.2 相交链表
思路:双指针,一个指针先遍历A再遍历B,另一个指针先 遍历 B 再遍历A,
两指针的碰头就是相交位置。
5.3 反转链表
从链表中依次取节点—>>头插入新链表,为了避免元素丢失,需要记录原链表没插入节点
5.4 判断回文链表
思路:快慢指针 + 反转链表
快慢指针找中间位置,并且找的过程顺便实现链表反转,当找到中间位置后,依次判断反转后链表的每个元素和中间位置后的每个元素。易错点:注意个数:奇偶。
5.5 判断链表是否有环
思路:龟兔赛跑算法,如果链表上存在环,那么在环上以不同速度前进的两个指针必定会在某个时刻相遇。
5.6 环形链表找入口
思路:
5.7 合并两个有序链表
思路:依次比较两个链表的值,谁小则吧谁加入新链表,当两个链表有一个为空时,因为链表有序,直接将不为空的加入新链表即可
5.8 合并K个升序链表
思路1:分治,链表两两合并
思路2:优先级队列,小顶堆,K个链表创建K个元素的小顶堆,依次拿堆顶元素
5.9 两数相加
思路:相加 + 进位
依次将两链表数相加,没有则补0(因为是逆序存储的,高位补0)
相加完后考虑进位,大于等于10的数向后进一位(next找到后一位,因为逆序存储,越往后越是高位)。
5.9 删除链表倒数第K个节点
思路:删除倒数第 K 个要找到倒数第 K+1个,定义两个指针,相距 K +1个,一个指针遍历完了,另一个指向的就是
5.10 两两交换链表中的节点
思路:要两两交换,就要知道交换的两个节点的前一个节点,然后操作next完成交换后两个节点,易错点:看是奇数节点还是偶数节点,来决定终止条件。
5.11 K个一组翻转链表
5.12 对链表排序
要求在O(nlogn时间复杂度和O(1)空间复杂度完成)
思路:自顶向下的递归不满足空间复杂度,系统栈为O(logn)
用自底向上的归并排序。
题外话:链表适合归并
用归并排序,每次只要找到归并的左链表和右链表就能完成归并,首先间隔从1开始,每次间隔扩大一倍:1->2->4->8,当间隔大于等于链表length时,划分间隔完毕,对每次划分的间隔,找出以该间隔大小的左链和右链,然后进行两个链表的归并,
6.栈
6.1 最小栈
思路1:栈中存要存的值和最小值,每次要存的话,最小值取栈顶中存的最小值和本次值两者的较小者,和当前要存的值一起存入栈中
思路2:定义一个栈用来存最小值。
7. 树
7.1 二叉树前中后序非递归实现
绕树一周:
前、中统一代码:
前中后续非递归统一代码:
LinkedList<TreeNode> stack = new LinkedList<>();
TreeNode curr = root; // 代表当前节点
TreeNode pop = null; // 最近一次弹栈的元素
while (curr != null || !stack.isEmpty()) {
if (curr != null) {
colorPrintln("前: " + curr.val, 31);
stack.push(curr); // 压入栈,为了记住回来的路
curr = curr.left;
} else {
TreeNode peek = stack.peek();
// 右子树可以不处理, 对中序来说, 要在右子树处理之前打印
if (peek.right == null) {
colorPrintln("中: " + peek.val, 36);
pop = stack.pop();
colorPrintln("后: " + pop.val, 34);
}
// 右子树处理完成, 对中序来说, 无需打印
else if (peek.right == pop) {
pop = stack.pop();
colorPrintln("后: " + pop.val, 34);
}
// 右子树待处理, 对中序来说, 要在右子树处理之前打印
else {
colorPrintln("中: " + peek.val, 36);
curr = peek.right;
}
}
}
public static void colorPrintln(String origin, int color) {
System.out.printf("\033[%dm%s\033[0m%n", color, origin);
}
7.2 二叉树的最大深度
参考笔记:树
思路1:后续递归
思路2:使用后序非递归实现,栈的size最大就是最大深度
思路3:层序遍历,level最大就是最大深度
强调:区分高度和深度
求二叉树的最大深度,即求的是根节点的高度,深度用前序,高度用后续
根的高度即二叉树的最大深度:后续,左右根,左右遍历完再处理根,根就是+1,这个根再返回给根的父亲,根的父亲再+1,这样层层传递,最后求得根的高度,即二叉树的最大深度。
7.3 翻转二叉树
思路:遍历的过程中交换左右子树
7.4 对称二叉树
public boolean isSymmetric(TreeNode root) {
return check(root.left, root.right);
}
public boolean check(TreeNode left, TreeNode right) {
// 若同时为 null
if (left == null && right == null) {
return true;
}
// 若有一个为 null (有上一轮筛选,另一个肯定不为 null)
if (left == null || right == null) {
return false;
}
if (left.val != right.val) {
return false;
}
return check(left.left, right.right) && check(left.right, right.left);
}
7.5 二叉树两节点间最长长度
思路:最长长度的两个端点是叶子节点,最长长度=左字数深度 + 右子树深度
class Solution {
int ans = 0;
public int diameterOfBinaryTree(TreeNode root) {
depth(root);
return ans;
}
public int depth(TreeNode node) {
if (node == null) {
return 0; // 访问到空节点了,返回0
}
int L = depth(node.left); // 左儿子为根的子树的深度
int R = depth(node.right); // 右儿子为根的子树的深度
ans = Math.max(ans, L+R);
return Math.max(L, R) + 1; // 返回该节点为根的子树的深度
}
}
7.6 二叉树层序遍历
7.7 将有序数组转换为二叉搜索树
思路:有序数组,其实就是中序的结果,将中序----> 二叉搜索树,根左右
class Solution {
public TreeNode sortedArrayToBST(int[] nums) {
return dfs(nums, 0, nums.length - 1);
}
private TreeNode dfs(int[] nums, int lo, int hi) {
if (lo > hi) {
return null;
}
// 以升序数组的中间元素作为根节点 root。
int mid = (hi + lo) / 2;
TreeNode root = new TreeNode(nums[mid]);
// 递归的构建 root 的左子树与右子树。
root.left = dfs(nums, lo, mid - 1);
root.right = dfs(nums, mid + 1, hi);
return root;
}
}
7.8验证二叉搜索树
思路:中序遍历后是递增的,左根右遍历,定义pre指针指向前一个节点,递归的终止条件是pre大于等于当前节点。
7.9 查找二叉树第k小元素
思路:二叉搜索树,第k小的元素对应中序遍历的第k个元素,用中序遍历的非递归实现:迭代方式,每次出栈计数+1,当计数为k时,break退出循环。
7.10 从前序和中序构造二叉树
思路:从前序遍历结果中可以拿到根,在中序遍历结果中,以该根为划分点,分为左子树,右子树,在递归的构建根的左右子树。
7.11 二叉树的最近公共祖先
思路:直观思路:由叶子节点由下往上找,找到同属于的最近一个祖先。由下往上,想到用后序遍历+回溯,左右根,由左右子树有没有要找的两个节点,由左右子树来作为根的处理依据,由下往上层层传递。
7.12 二叉树中的最大路径和
注意点:有负数
思路:后序遍历,根据左右子树返回值来作为处理root的依据,维护一个全部变量sum表示记录的最大路径和,左右处理完后,处理根:当根的子树有负数时,不加,有一个是正数就加这个,两个都为正数就都加,sum做记录,最后return返回给root的父节点,即为newRoot,newRoot的判断逻辑也是如此,看newRoot的左右子树,然后********,由此看出,左右根,根:最后return时return的是单链,表示以该节点形成的链条的最大路径,因为当前root返回的是单链,返回给newRoot,newRoot再处理它的左右子树。
7.12 二叉树展开为链表
思路:从root开始一直向右遍历二叉树,当root有左子树时候,将左子树插入到root和root的右孩子之间。
8. 哈希表
8.1 最长连续序列
思路:定位到连续序列的起点,以该起点找最长的序列,要定位到连续序列的起点,用到hash表,将带查找数组存入hash表,依次遍历数组,先找起点,判断比当前位置元素值小1的在不在hash表中,不在说明是起点,再依次遍历该起点之后的元素,判断的逻辑是起点+1在不在hash表中,找到最长序列就跟新最长序列长度;如果不是起点,那就跳过当前元素,依次遍历后序数组看看是不是起点。
9. 滑动窗口
9.1 无重复字符的最长字串长度
思路:双指针,分别指向窗口的开始和结束,依次遍历字符串,伴随着end++,窗口不断扩大,在扩大的过程中当窗口内元素重复时,要调整左边界,使得满足窗口内的都是没有重复元素。判断当前窗口内有没有这个元素:判断有没有用map来记录,key为字符,value为该字符所在索引的下一个位置。
10 数组
10.1 最大子数组和
思路:动态规划思路,到 i 位置的最大子数组和为dp【i】,i位置的状态只与前一个位置状态有关系,前一个状态dp【i-1】小于等于0,说明前面的对自己没贡献,dp【i】=num【i】,当前一个位置的dp【i - 1】>0,说明前一个位置对自己有贡献,dp【i】=dp【i-1】 + num【i】。
10.2 合并区间
思路:将数组按照左边界进行排序,排序后左边界就是有序的,每次我们判断数组中 i 位置的左边界和 数组 i - 1 位置的右边界,如果当前的左边界 > 前一个的右边界,那说明没有重合,将上一个区间放入result,如果当前的左边界 <= 前一个的右边界,那说明重合了,需要合并区间,合并区间的逻辑就是【前一个的左边界,max(前一个的右边界,当前的右边界)】 。
11 矩阵
11.1 螺旋矩阵
思路:定义上下左右边界,依次遍历上、右、下、左;每次遍历完一个边界让边界缩小,当边界超过时break。
11.2 旋转图像
思路:先按对角线翻转,再暗中间进行左右翻转。
11.3 矩阵置 0
思路:用两个数组:行数组和列数组当作标记数组,遍历矩阵,当遍历到 0 时将对应的行数组和列数组标记,表示哪些行需要置为零哪些列需要置为零。
优化:用原数组的第一行和第一列当作标记数组,流程同上还是遍历矩阵,置标记位,但是这样第一行和第一列做标记数组了,如果第一行和第一列有 0 ,设置两个标记位,分别表示第一行有没有零、第一列有没有零,然后最后特殊处理第一行和第一列。
12 堆
12.1 数组中的第K个最大元素
12.2 前K个高频元素
思路:定义一个map来存数组中每个元素的频率即出现的次数,这时候要对map中的value排序,来取前 k 个高频元素,用小顶堆实现,最后堆里面就维护了 k 个容量的前 k 个高频元素。
13 技巧
13.1 寻找重复数
思路:题目中说只有一个重复元素,数组中存的有一个元素存了两次,找到它,可以想象成链表的形式,存的元素表示下次去哪个索引位置找,这样的话找这个重复元素就相当于找环的入口,快慢指针,先追上,再吧龟放起点,下次相遇就是入口。
14 图论
14.1 岛屿数量
思路:依次遍历二维数组,如果遍历的点是陆地并且这个点没访问过,则岛屿数量 + 1,并且将该点的周围是陆地的将其置为访问过。
14.2 腐烂的橘子
思路:定义一个队列将烂橘子先放入(索引),当队列不空时,依次将队列中的烂橘子取出,判断该位置的上下左右位置,入如果是好橘子则变成烂橘子,并将烂橘子放入队列,每遍历一层将minutes + 1(遍历一层就是将队列所有元素取出并判断上下左右)。