刷题总结
动态规划
子序列问题
子序列问题就用动态规划求解,套路都是定义dp[n],以i结尾的字符串的最长/短是dp[i],定义base case,进行状态转移其实就是选择的过程
子序列问题主要分为三种
-
一维dp数组
// 以nums[i]结尾的最长子序列是dp[i] int[] dp = new int[n]; // base case dp数组全都初始化为1,因为子序列至少包含自己 Arrays.fill(dp,1); for (int i = 1; i < n; i++) { // 从nums[0,i) 选择符合条件的里最大的 for (int j = 0; j < i; j++) { dp[i] = 最值(dp[i], dp[j] + ...) } }
-
二维dp数组:涉及两个数组
找两个字符串的最长公共子序列,这种一般都从1开始,base case是i为0的情况+j为0的情况// dp[1..i][1...j] 是以s1[i-1]和s2[j-1]结尾的最值 int[][] dp = new dp[m+1][n+1]; // base case 填充 dp[..][0] 和dp[0][..] // 转移 for (int i = 1; i <= m; i++) { for (int j = 1; j <= n; j++) { if (arr[i-1] == arr[j-1]) dp[i][j] = dp[i-1][j-1] + ... else dp[i][j] = 最值(dp[i-1][j],dp[i][j-1]) } }
-
二维数组dp数组:涉及一个数组(查找最长回文串序列)
dp[n][n]的定义是查找字符串array[i,j]的范围的最值,base case 填充的是i==j和i>j的情况,转移方程要反着遍历 也就是i从字符串的最后开始// dp[0..i][0...j] 是以s[i...j]的最值 int[][] dp = new dp[n][n]; // base case 填充 i==j i>j // 转移 // 反着遍历 for (int i = n-1; i >= 0; i--) { for (int j = i+1; j < n; j++) { if (arr[i] == arr[j]) // 回文串 dp[i][j] = dp[i+1][j-1] +2; else dp[i][j] = 最值(dp[i+1][j],dp[i][j-1]) } }
最大子数组
dp定义:以 nums[i] 为结尾的「最大子数组和」为 dp[i],结果就是max(dp[i])
dp[i] 有两种「选择」,要么与前面的相邻子数组连接,形成一个和更大的子数组;要么不与前面的子数组连接,自成一派,自己作为一个子数组。
题目:
53. 最大子数组和:给你一个整数数组 nums ,请你找出一个具有最大和的连续子数组
class Solution {
public int maxSubArray(int[] nums) {
int n = nums.length;
// dp[i] 是以i结尾的最大和是dp[i]
int[]dp = new int[n];
dp[0] = nums[0];
for (int i = 1; i < n; i++) {
dp[i] = Math.max(nums[i], dp[i-1]+nums[i]);
}
return Arrays.stream(dp).max().getAsInt();
}
}
背包问题
自己的总结
给定一个背包容量target,再给定一个数组nums(物品),能否按一定方式选取nums中的元素得到target。【本质就是选出m个数是否满足target】
- 根据选取方式的不同决定背包类型
- 01背包:每个元素选一次
外循环nums,内循环target,target倒序且target>=nums[i]; - 完全背包:每个元素可以重复选择
外循环nums,内循环target,target正序且target>=nums[i];,是排列问题内外循环可以颠倒 - 组合背包:背包中的物品要考虑顺序
外循环target,内循环nums,target正序且target>=nums[i]; - 分组背包:
这个比较特殊,需要三重循环:外循环背包bags,内部两层循环根据题目的要求转化为1,2,3三种背包类型的模板
- 01背包:每个元素选一次
- 题目要求分类
- 最值问题:求最大值最小值
dp[i] = max/min(dp[i], dp[i-nums]+1)或dp[i] = max/min(dp[i], dp[i-num]+nums); - 是否存在/满足
dp[i]=dp[i]||dp[i-num]; - 组合问题:求所有满足……的排列组合
dp[i]+=dp[i-num]
- 最值问题:求最大值最小值
- target变形:题目不会直接问,我们需要一些转换技巧
- 目标和:对nums数字前可以+,- 最终的和为target的方案数
可以转化成:从nums数字中选m个数,和为(sum+target)/2。 因为x-y = target x+y=sum - 416.分割等和子集:是否可以把只有正数的nums数字分隔成和相等的两部分
和相等 就是 从nums数字中选m个数,和为sum/2 - 1049.最后一块石头的重量
把一堆石头分成两堆A和B,求两堆石头重量差最小值, 也就是A<sum/2.B>sum/2,将一堆stone放进最大容量为sum/2的背包,求放进去的石头的最大重量MaxWeight,最终答案即为sum-2*MaxWeight;、
- 目标和:对nums数字前可以+,- 最终的和为target的方案数
原始背包问题模板
给你一个可装载重量为 W 的背包和 N 个物品,每个物品有重量和价值两个属性。其中第 i 个物品的重量为 nums[i],价值为 val[i],现在让你用这个背包装物品,最多能装的价值是多少?
// dp[i][j]表示 从[0..i]个物品中选取物品重量不超过target重量的最大价值
int[][]dp = new int[n+1][wieghtSum+1];
// 初始化:第一列都是0,第一行表示只选取0号物品最大价值
for (int j = wieghtSum; j >= nums[0]; j--){
dp[0][j] = dp[0][j - nums[0]] + value[0];
}
for (int i = 1; i < n; i++) {
for (int j = 0; j <= wieghtSum; j++) {
if (j - nums[i] < 0) {
// 背包容量不足,不能装入第 i 个物品
dp[i][j] = dp[i - 1][j];
} else {
// 装入或不装入背包 取最大值
dp[i][j] = Math.max(dp[i-1][j],dp[i-1][j-nums[i]]+val[i]);
}
}
}
return dp[n][wieghtSum];
转成1维:dp[j]表示容量为j的背包能放下东西的最大价值
// dp[j]表示容量为j的背包能放下东西的最大价值
int[] dp = new int[wieghtSum+1];
// base case
dp[0] = 1;
for (int i=0;i<n;i++) {
for (int j = wieghtSum;j>=nums[i];j--){
dp[j] = Math.max(dp[j], dp[j-num[i]]+val[i]);
}
}
return dp[wieghtSum];
-
dp定义:
- int [][] dp [i][j]: 表示从nums[0…i]中选m个数(m<=i)使其和为j的方案数
- boolean[][]dp 从nums[0…i]中选m个数(m<=i) 是否能满足和为j
- 以上两个二维的都可以压缩成一维dp[j]:dp[j] 表示使得和为j的方案数 或是否能使其和为j,因为i只和前一个相关,所以就去掉了,但是需要外循环遍历 arrs,内循环遍历 target,且内循环倒序
-
模板
方案数模板// 目标为i的表达式数目是dp[i]种 int[] dp = new int[target+1]; // base case dp[0] = 1; for (int num : nums) { for (int j = t;j>=num;j--){ dp[j] = dp[j] + dp[j-num]; } }
boolean值模板
// dp[i] 是否存在和为i的num组合 boolean[] dp = new boolean[target+1]; // Arrays.fill(dp,false); dp[0] = true; for(int num:nums){ for (int j = target; j >= num; j--) { // 不装入背包或装入 dp[j] = dp[j] || dp[j-num]; } }
股票
模板:
状态: dp[i][k][0或1] 第i天 交易上限为k的最大利润
0 <= i <= n - 1, 1 <= k <= K
n 为天数,大 K 为交易数的上限,0 和 1 代表是否持有股票。
选择: 有三种:买入、卖出、不操作
- do[i][k][0] 今天是没有股票的状态,那今天可以是没操作或者卖出了
dp[i][k][0] = max(dp[i-1][k][0], dp[i-1][k][1] + prices[i])
max( 今天选择 rest, 今天选择 sell )
- dp[i][k][0] 今天持有股票的状态,那今天可以是没操作或者买入了
dp[i][k][1] = max(dp[i-1][k][1], dp[i-1][k-1][0] - prices[i])
max( 今天选择 rest, 今天选择 buy )
base case:
dp[-1][...][0] = dp[...][0][0] = 0
dp[-1][...][1] = dp[...][0][1] = -infinity=-prices[i]
你只能选择 某一天 买入这只股票,并选择在 未来的某一个不同的日子 卖出该股票
这些都可以进行状态压缩
对于不同题目套入上述公式:
- 最多买卖k次,直接套模板
188.买卖股票的最佳时机public int maxProfit(int k, int[] prices) { int n = prices.length; int max_k = k; int [][][] dp = new int[n][max_k+1][2]; for (int i = 0; i < n; i++) { for (int j = max_k; j>=1;j--){ if (i-1==-1){ dp[i][j][0] = 0; dp[i][j][1] = -prices[i]; continue; } dp[i][j][0] = Math.max(dp[i-1][j][0], dp[i-1][j][1]+prices[i]); dp[i][j][1] = Math.max(dp[i-1][j][1], dp[i-1][j-1][0]-prices[i]); } } return dp[n-1][max_k][0]; }
- 最多买卖2次, 把上述max_k 改成2
123. 买卖股票的最佳时机3 - 你只能选择 某一天 买入这只股票,并选择在未来的某一个不同的日子卖出该股票,即最多买卖1次,即k=1,这时不用穷举k了
121.买卖股票的最佳时机dp[i][0] = Math.max(dp[i-1][k][0], dp[i-1][k][1]+prices[i]) = Math.max(dp[i-1][0], dp[i-1][1]+prices[i]) dp[i][1] = Math.max(dp[i-1][k][1],dp[i-1][k-1][0]-prices[i]); = Math.max(dp[i-1][1][1], dp[i-1][0][0]-prices[i]); // 因为dp[i-1][0][0]=0 = Math.max(dp[i-1][1], -prices[i])
- 没有k的限制,也就是k为无穷,可以直接去掉k
122. 买卖股票的最佳时机 2dp[i][k][0] = Math.max(dp[i-1][k][0], dp[i-1][k][1]+prices[i]) = Math.max(dp[i-1][0], dp[i-1][1]-prices[i]); dp[i][k][1] = Math.max(dp[i-1][k][1], dp[i-1][k-1][0]-prices[i]) = Math.max(dp[i-1][1], dp[i-1][0]-prices[i]);
- 没有k的限制,加了冷冻期,不能买前一天的股票 所以买入的时候改成i-2
309. 买卖股票的最佳时机含冷冻期
base case 也要新增i-2=-1的情况dp[i][0] = Math.max(dp[i-1][0], dp[i-1][1]+prices[i]); // 买入有一天冷冻期 dp[i][1] = Math.max(dp[i-1][1], dp[i-2][0]-prices[i]);
if (i-1==-1){ dp[i][0] = 0; dp[i][1] = -prices[i]; continue; } if (i-2==-1){ // base case 2 dp[i][0] = Math.max(dp[i-1][0], dp[i-1][1] + prices[i]); // i - 2 小于 0 时根据状态转移方程推出对应 base case dp[i][1] = Math.max(dp[i-1][1], -prices[i]); // dp[i][1] // = max(dp[i-1][1], dp[-1][0] - prices[i]) // = max(dp[i-1][1], 0 - prices[i]) // = max(dp[i-1][1], -prices[i]) continue; }
- 没有k的限制,但有手续费,这个很简单买入的时候减去手续费,base case也减去
714.买卖股票的最佳时机含手续费public int maxProfit(int[] prices, int fee) { int n = prices.length; int[][] dp = new int[n][2]; for (int i = 0; i < n; i++) { if (i-1==-1){ dp[i][0] = 0; dp[i][1] = -prices[i]-fee; continue; } dp[i][0] = Math.max(dp[i-1][0], dp[i-1][1]+prices[i]); dp[i][1] = Math.max(dp[i-1][1], dp[i-1][0]-prices[i]-fee); } return dp[n-1][0]; }
链表
基础
- 虚拟节点:dummy节点
当你需要创造一条新链表的时候,可以使用虚拟头结点简化边界情况的处理。
ListNode dummy = new ListNode(-1);
// head就是dummy.next
反转链表
递归
反转全部
记住三个基本:
- base case 也就是递归到底的返回结果:到底就是headnull || head.nextnull,直接返回head,也就是反转到最后一个节点就返回这个节点,这个节点也就是反转后的起始节点
- 递归:反转head.next,返回值其实是反转后的头节点
- 反转: 真正进行反转的过程
public ListNode reverse(ListNode head){
// 到最后一个节点
if (head==null || head.next==null){
return head;
}
// 得到下一个节点
ListNode newHead = reverse(head.next);
// 反转
head.next.next = head;
head.next = null;
return newHead;
}
反转前N
和反转全部的区别其实是要记住【不参与反转的第一个节点】,如何得到不参与反转的第一个节点,就是递归到basecase也就是n=1的下一个节点
- 区别1:到basecase 记录不反转的第一个节点
- 区别2:之前置head.next为空,现在head.next=不反转的第一个节点
// 不反转的第一个节点
ListNode successor = null;
public ListNode reverseN(ListNode head, int n){
if (n==1){
// 区别1
successor = head.next;// 记录不参与反转的第一个节点
return head;
}
ListNode cur = reverseN(head.next, n-1);
// 反转
head.next.next = head;
// 区别2
head.next = successor;// 要连接不反转的第一个节点
return cur;
}
反转M-N
反转M-N可以转换成 反转前N,只需将head向前移动到left为1就是反转前N了
public ListNode reverseBetween(ListNode head, int left, int right) {
// 反转前N个节点
if (left==1){
return reverseN(head, right);
}
head.next= reverseBetween(head.next, left -1, right - 1);
return head;
}
k个一组反转
首先我们用递归进行k个一组反转,那么对于每次反转我们要找到
- 反转的起始节点 tail,也就是上次反转后第一个没反转的节点,并把这次的和下次的连接起来
head.next = reverseKGroup(tail, k); - 本次递归要做的是:进行前k反转
ListNode newHead = reverseN(head, k);
class Solution {
public ListNode reverseKGroup(ListNode head, int k) {
// base case
if (head == null || head.next == null){
return head;
}
// 找到本次递归要反转的起始节点
ListNode tail = head;
for (int i=0;i<k;i++){
if (tail==null){
return head;
}
tail = tail.next;
}
// 反转前N
ListNode newHead = reverseN(head, k);
// 下一次递归
head.next = reverseKGroup(tail, k);
return newHead;
}
ListNode last = null;
private ListNode reverseN(ListNode head, int n){
if (n==1){
last = head.next;
return head;
}
ListNode end = reverseN(head.next, n-1);
head.next.next = head;
head.next = last;
return end;
}
}
迭代
设置两个指针prev和cur,prev指向head的前一个节点,cur指向head
class Solution {
public ListNode reverseList(ListNode head) {
ListNode prev = null;
ListNode curr = head;
while(curr!=null){
ListNode tmp = curr.next;
curr.next = prev;
prev = curr;
curr = tmp;
}
return prev;
}
}
环形链表
基础知识:
- 判断是否有环
通过快慢指针,如果fast==slow则说明有环 - 返回链表开始入环的第一个节点。如果链表无环,则返回null。
用快慢指针:- 第一步:快指针走两步慢指针走一步找到相遇点
- 第二步:head和相遇点同时向后走,相遇点即入环节点
代码:
判断是否有环
public class Solution {
public boolean hasCycle(ListNode head) {
ListNode fast = head;
ListNode low = head;
while(fast!=null && fast.next!=null){
fast = fast.next.next;
low = low.next;
if(fast==low){ //快的追上慢的了 说明有环
return true;
}
}
return false;
}
}
返回链表开始入环的第一个节点
环形链表 II ( LeetCode 142 )
public class Solution {
public ListNode detectCycle(ListNode head) {
// 第一步:先找环
ListNode fast = head;
ListNode slow = head;
boolean hasCycle = false;
while (fast!=null && fast.next!=null){
fast = fast.next.next;
slow = slow.next;
if (fast==slow){
// 慢的追上快的 说明有环
hasCycle=true;
break;
}
}
if (hasCycle){
slow = head;
while (fast!=slow){
slow = slow.next;
fast = fast.next;
}
return slow;
}else{
return null;
}
}
}
相交链表
判断两条链表是否相交:
谁先走到头就走另一条路
public class Solution {
public ListNode getIntersectionNode(ListNode headA, ListNode headB) {
ListNode a = headA;
ListNode b = headB;
while(a!=b){
if (a==null){
a=headB;
}else{
a=a.next;
}
if (b==null){
b=headA;
}else{
b=b.next;
}
}
return a;
}
}
栈
我总结的栈题目
单调递增栈:从栈顶到栈底的元素是严格递增
解决的问题:下一个更大元素
维护单调递增栈模板:
public int[] nextGreaterElements(int[] nums) {
int n = nums.length;
// 存放答案的数组
int[] ans = new int[n];
Arrays.fill(ans,-1);
// 存放的是下标
Stack<Integer> stack = new Stack<Integer>();
for(int i=0;i<n; i++){
// 找栈顶元素的下一个更大元素:即值比栈顶元素大
while(!stack.empty() && nums[i]>nums[stack.peek()]){
// 栈顶元素
int top = stack.pop();
// 当前元素是栈顶元素的下一个最大元素
ans[top] = nums[i];
}
// 比栈顶元素小直接入栈
stack.push(i);
}
return ans;
}
数组
双指针
快慢指针
原地修改数组
数据有序,一个指针(slow)用来维护结果集,一个指针(fast)来遍历
删除有序数组的k个重复项
public int removeDuplicates(int[] nums) {
// k代表重复的元素数
int k=2;
int n = nums.length;
if (n<k){
return n;
}
int slow=k;
int fast=k;
while (fast<n){
if (nums[fast]!=nums[slow-k]){
nums[slow] = nums[fast];
slow++;
}
fast++;
}
return slow;
}
滑动窗口
滑动窗口适用题型: 寻找子串
滑动窗口[left,right)就是不断扩大right指针扩大窗口,直到窗口内符合要求;然后扩大left指针缩小窗口,直到窗口内不符合要求。同时注意,每次增加left都要更新答案。
- 【寻找可行解】窗口右移时要做的事:
- 判断是否为答案需要的,是need需要的就更新进窗口
- 要注意更新目前进度,用来判断窗口内数据是否已满足条件
- 当窗口内的数据满足条件 窗口左移,如何判断窗口内的数据满足条件
- 这个条件就是 扩张窗口和收缩窗口时要更新的
比如找最小字符串时用valid来记录窗口内满足条件的字符个数,扩张窗口的时候valid++,收缩窗口时valid–
- 这个条件就是 扩张窗口和收缩窗口时要更新的
- 【优化可行解】窗口满足条件后,进行窗口左移
- 左移过程中,如果左移后就不满足条件了说明找到一个解了
滑动窗口java模板:
- 长度固定:题目最后的结果是固定长度的,其实就省去收缩的部分,只用两步
- 填充初始窗口
- 窗口整体向右移 ,right和left同时++ 后更新答案
- 长度不固定
void slidingWindow(String s,String t){
HashMap<Character,Integer> need = new HashMap<Character, Integer>();//need 是需要凑齐的字符
HashMap<Character,Integer> window = new HashMap<Character, Integer>(); // window是窗口中的字符
// 把t中 每个字符出现的次数记录到need中
for(int i = 0;i<t.length();i++){
need.put(t.charAt(i),need.getOrDefault(t.charAt(i),0)+1);
}
int left=0,right=0; // 滑动窗口的边界
int valid = 0; //窗口中满足need条件的字符个数
//找到最右边
while(right<s.length()){
//c是即将移入窗口的字符
Character c = s.charAt(right);
//增加right指针扩大窗口
right++;
// 1、判断是否加入窗口
// 2、加入窗口后,进行【条件】的更新
//满足【条件】后需要收缩窗口
while(收缩窗口的条件){
//答案可能更新的位置1
...
//d是即将移出窗口的字符
Character d = s.charAt(left);
//增加left指针缩小窗口
left++;
// 1、判断是否移出窗口
// 2、移出窗口后,进行【条件】的更新,与扩张时对称
//答案可能更新的位置2
...
}
}
}
套模板熟悉后最难的是收缩窗口的条件,因此总结一下常见的条件:
- 最小覆盖子串
while(valid==need.size()) 也就是窗口的有效字符个数和need的相等,就是满足条件的,但不一定是最优解 - 是否字符串的排列/异位词
当**right-left>=s.length()**时收缩窗口,此时的条件并不一定是排列,只是至少窗口的长度要大于等于子串的长度才有可能是排列 - 最长无重复子串
当 window.get©>1 时收缩,注意的是跳出收缩循环后更新答案,因为最终是要无重复子串,收缩是是有重复子串,收缩后才是无重复 - 1423 可获得的最大点数
这个主要是转换题目:要左右两边拿的k张最大,也就是不被拿的n-k张最小
对于找滑动窗口的最小值问题:需要维护一个单调队列:
labuladong单调队列解决滑动窗口问题:
例题:239. 滑动窗口最大值
class Solution {
class MonotonicQueue{
LinkedList<Integer> maxq = new LinkedList<>();
public int max(){
return maxq.getFirst();
}
// 单调队列的队首元素永远 保持是最大值。 在push的时候要把比自己小的都删除
public void push(int n){
while (!maxq.isEmpty() && maxq.getLast()<n){
maxq.pollLast();
}
maxq.addLast(n);
}
public void pop(int n){
if (n==maxq.getFirst()){
maxq.pollFirst();
}
}
}
public int[] maxSlidingWindow(int[] nums, int k) {
// 单调队列
MonotonicQueue queue = new MonotonicQueue();
List<Integer> res = new ArrayList<>();
int n = nums.length;
int left=0,right=0;
while(right<n){
if (right<k-1){
queue.push(nums[right]);
right++;
}else{
queue.push(nums[right]);
res.add(queue.max());
queue.pop(nums[left]);
left++;
right++;
}
}
int []resArray = new int[res.size()];
for (int i=0;i<res.size();i++){
resArray[i] = res.get(i);
}
return resArray;
}
}
左右指针
二分
二分适用题型:
- 具有二段性
- 一些让你O(logn)实现的
类型:
- 完全有序,直接套模板
- 不完全有序,一般是排序数组
排序数组且无重复元素,则需要用到特性如果nums[i]<=nums[j],那么i到j一定是单调递增的;如果有重复也不用怕,只用加一个在nums[left]==nums[mid]==nums[right]时 进行left++,right–- 33.搜索旋转排序数组
由于是找旋转后数组的target,所以先要找到有序序列,关键点就是我们每次将数组分成两半后,一定有一半是有序的,即[left,mid]有序,或(mid,right]有序,我们判断target属于哪一半进行二分即可 - 162.寻找峰值
比较nums[mid]和nums[mid+1],如果较nums[mid]>nums[mid+1]则峰值一定在左边,相反峰值一定在右边
- 33.搜索旋转排序数组
- 二维数组
将二维数组直接转成一维数组,int midValue = matrix[mid/y][mid%y]
我现在觉得二分最重要的是明确你搜索的区间是闭区间还是开区间[left,right] 还是[left,right),只要明确 二分时的边界条件就很清晰
模板:
- 闭区间:[left,right]
我们初始化right为n-1 也就是搜索范围包含了右区间,因此配套的:- while 返回条件为:left<=right
left<=right则while的跳出条件是left=right+1即[right+1,right],如果不写等于号则跳出条件是left=right即[right,right],这样right位置的索引就被遗漏了 - right=mid-1,left=mid+1
同样由于是闭区间,当我们发现索引 mid 不是要找的 target 时,下一步应该去的是[mid+1,right] 或[left,mid-1],因为 mid 已经搜索过,应该从搜索区间中去除。int n = nums.length; int left = 0,right = n-1; while (left<=right){ int mid = left + (right-left)/2; if (nums[mid]==target){ return mid; } else if (target<nums[mid]){ right = mid-1; }else if(target>nums[mid]){ left = mid+1; } }
- while 返回条件为:left<=right
- 左闭右开区间:[left,right)
- 搜索左侧边界(寻找第一个大于等于target)
int left_bound(int[] nums, int target) { int left = 0; int right = nums.length; // 注意 左闭右开[left,right) while (left < right) { // 注意: 跳出条件是left==right int mid = left + (right - left) / 2; if (nums[mid] >= target) { right = mid; } else if (nums[mid] < target) { left = mid + 1; } } // left和right是一样的 if (left<0 || left>=n){ return -1; } return nums[left]==target?left:-1; }
- 搜索右侧边界
最需要注意的变化是返回结果是left-1int right_bound(int[] nums, int target) { int left = 0; int right = nums.length; // 注意 左闭右开[left,right) while (left < right) { // 注意: 跳出条件是left==right int mid = left + (right - left) / 2; if (nums[mid] <= target) { left = mid + 1; } else if (nums[mid] > target) { right = mid; } } // left和right是一样的,由于找到target时left赋值为mid+1,while 循环结束时,nums[left] 一定不等于 target,而 nums[left-1] 可能是 target。 if (left - 1 < 0 || left - 1 >= n) { return -1; } return nums[left-1]==target?left-1:-1; }
- 搜索左侧边界(寻找第一个大于等于target)
nSum求和
自己总结的求和
labuladong
两数之和模板:排序后用左右指针求解,注意去重
三数之和和四数之和都可以再两数之和的基础上增加for循环,四数之和要注意内存溢出问题
public int[] twoSum(int[] nums, int target) {
// 1、没排好序的先排序
Arrays.sort(nums);
// 2、左右指针查找target
int n = nums.length;
int left = 0, right = n-1;
// 答案:所有符合条件的元素值
List<List<Integer>> ans = new ArrayList<>();
while (left<right){
int sum = nums[left] + nums[right];
int leftValue = nums[left];
int rightValue = nums[right];
if (sum==target){
List<Integer> curAns = new ArrayList<>();
curAns.add(nums[left]);
curAns.add(nums[right]);
ans.add(curAns);
// 去除重复值
while (left<right && nums[left]==leftValue){
left++;
}
while (left<right && nums[right]==rightValue){
right--;
}
}else if(sum>target){
// 去除重复值
while (left<right && nums[right]==rightValue){
right--;
}
}else{
// 去除重复值
while (left<right && nums[left]==leftValue){
left++;
}
}
}
return ans;
}
区间问题
所谓区间问题,就是线段问题,让你合并所有线段、找出线段的交集等等
-
- 合并区间
首先按start从小到大排序
如果重叠则找最大end
class Solution { public int[][] merge(int[][] intervals) { if (intervals==null || intervals.length==0){ return new int[][]{}; } // 按start从小到大排序 Arrays.sort(intervals, Comparator.comparingInt(a->a[0])); List<int[]> res = new ArrayList<>(); res.add(intervals[0]); for (int i=1;i<intervals.length;i++){ int[] cur = intervals[i]; int[] last = res.get(res.size()-1); // 如果区间重叠 则 找最大的end if (cur[0]<=last[1]){ last[1] = Math.max(cur[1],last[1]); }else{ // 区间不重叠,则是新的区间 res.add(cur); } } return res.toArray(new int[res.size()][]); }
- 合并区间
}
```
- 986.区间列表的交集
先判断是否存在交集if(a2>=b1 && a1<=b2),如果存在交集则取交集res.add(new int[]{Math.max(a1,b1), Math.min(a2,b2)})。移动指针只取决于a2和b2
class Solution {
public int[][] intervalIntersection(int[][] firstList, int[][] secondList) {
int i=0,j=0;
List<int[]> res = new ArrayList<>();
while (i<firstList.length && j<secondList.length){
int a1 =firstList[i][0], a2 = firstList[i][1];
int b1 =secondList[j][0], b2 = secondList[j][1];
// 有交集
if(a2>=b1 && a1<=b2){
res.add(new int[]{Math.max(a1,b1), Math.min(a2,b2)});
}
// 移动指针
if (a2<b2){
i++;
}else{
j++;
}
}
return res.toArray(new int[res.size()][]);
}
}
- 1288.删除被覆盖区间
class Solution {
public int removeCoveredIntervals(int[][] intervals) {
// 被删除的区间
int res = 0;
// 按start从小到大排序,start一样则end从大到小排序
Arrays.sort(intervals, (a,b)->{
if (a[0]==b[0]){
// start一样,按end倒序排
return b[1]-a[1];
}
return a[0]-b[0];
});
// 记录合并区间的起点和终点
int left = intervals[0][0];
int right = intervals[0][1];
for (int i = 1; i < intervals.length; i++) {
int[] inv = intervals[i];
// 情况一:区间被覆盖
if (inv[0]>=left && inv[1]<=right){
res++;
}
// 情况二:区间相交
if (inv[0]<=right && right<=inv[1]){
right = inv[1];
}
// 情况三:完全不相交,更新新起点和终点
if (right<inv[1]){
left = inv[0];
right = inv[1];
}
}
return intervals.length-res;
}
}
二叉树
void traverse(TreeNode root) {
if (root == null) {
return;
}
// 前序位置
traverse(root.left);
// 中序位置
traverse(root.right);
// 后序位置
}
前中后序是遍历二叉树过程中处理每一个节点的三个特殊时间点:
- 前序位置的代码在刚刚进入一个二叉树节点的时候执行;
前序位置的代码执行是自顶向下的 - 后序位置的代码在将要离开一个二叉树节点的时候执行;
后序位置的代码执行是自底向上的,所以可以获取到子树通过函数返回值传递回来的数据对于和子树有关的题目需要在后序位置写代码 - 中序位置的代码在一个二叉树节点左子树都遍历完,即将开始遍历右子树的时候执行。
两种思维模式:遍历和分解
- 遍历:定义一个没返回值的函数,遍历一遍就得到答案了
- 分解:将题目分解成子问题
- 一种直接用原函数分解
- 一种定义的函数一般有返回值,在后序遍历的位置 根据左右子树的返回结果得到答案
构造二叉树:其实就是分解的思想,根据先序/后序的遍历顺序找到根节点,构造其左右子树
二叉搜索树: 最关键的是知道其特点是中序遍历顺序的升序的
路径问题:
- 从根节点到叶节点的路径
- 用maxDepth计算最大深度
- 在【叶子节点】进行判断和选择
- 任意节点间的路径:基本都是定一个新的函数oneSideMax算出一边的最值,在后序遍历的位置更新全局变量
关键思想就是定一个新函数算左右子树的最大值,在递归的同时在后序遍历的位置 把left+right 更新最终题目要的值。
-
首先要定义出新的oneSide函数计算什么:我们知道了左右子树返回的什么值 就能求出要的那个res
543.二叉树的直径其他的不过根节点的题都是在这个模板基础上修改
如687.最长同值路径:只用在此基础上价格father的值,返回当前节点时判断下是否和farther相等,不相等就返回0。
124.二叉树中的最大路径和:只是把maxLen换成当前节点的值
687.最长同值路径:定义从 root 开始值为 parentVal 的最长树枝长度,这样以root为根节点的最长同值路径就是左右子树加起来 -
在【后序遍历的位置】 更新全局变量:进行将左右子树的结果相加+root.val,因为后序的位置已经知道当前root左右子树的遍历结果
回溯
经典题目:全排列、N皇后
写 backtrack 函数时,需要维护走过的「路径」和当前可以做的「选择列表」,当触发「结束条件」时,将「路径」记入结果集。
result = []
def backtrack(路径, 选择列表):
if 满足结束条件:
result.add(路径)
return
for 选择 in 选择列表:
1. 排除不合法选择
2. 做选择
3. backtrack(路径, 选择列表)
4. 撤销选择
46.全排列
给定一个不含重复数字的数组 nums ,返回其 所有可能的全排列 。你可以 按任意顺序 返回答案。
class Solution {
List<List<Integer>> res = new LinkedList<>();
public List<List<Integer>> permute(int[] nums) {
LinkedList<Integer> track = new LinkedList<>();
// 初始值都是false
boolean[] used = new boolean[nums.length];
backtrack(nums, track, used);
return res;
}
private void backtrack(int[] nums, LinkedList<Integer> track, boolean[] used){
// 到叶子节点
if (track.size()==nums.length){
res.add(new LinkedList(track));
return ;
}
for (int i=0;i<nums.length;i++){
// 排除不合法选择
if (used[i]){
continue;
}
// 做出选择
track.add(nums[i]);
used[i] = true;
// 下一层决策树
backtrack(nums, track, used);
// 撤回选择
track.removeLast();
used[i] = false;
}
}
}
78.子集【元素无重不可复选】
给你一个整数数组 nums ,数组中的元素 互不相同 。返回该数组所有可能的
子集
(幂集)。
解集 不能 包含重复的子集。你可以按 任意顺序 返回解集。
示例 1:
输入:nums = [1,2,3]
输出:[[],[1],[2],[1,2],[3],[1,3],[2,3],[1,2,3]]
示例 2:
输入:nums = [0]
输出:[[],[0]]
class Solution {
List<List<Integer>> res = new LinkedList<>();
// 记录回溯算法的递归路径
LinkedList<Integer> track = new LinkedList<>();
// 主函数
public List<List<Integer>> subsets(int[] nums) {
backtrack(nums, 0);
return res;
}
// 回溯算法核心函数,遍历子集问题的回溯树
void backtrack(int[] nums, int start) {
// 前序位置,每个节点的值都是一个子集
res.add(new LinkedList<>(track));
// 回溯算法标准框架
for (int i = start; i < nums.length; i++) {
// 做选择
track.addLast(nums[i]);
// 通过 start 参数控制树枝的遍历,避免产生重复的子集
backtrack(nums, i + 1);
// 撤销选择
track.removeLast();
}
}
}
N皇后
51.N 皇后
每一种解法包含一个不同的 n 皇后问题 的棋子放置方案,该方案中 ‘Q’ 和 ‘.’ 分别代表了皇后和空位。
输入:n = 4
输出:[[“.Q…”,“…Q”,“Q…”,“…Q.”],[“…Q.”,“Q…”,“…Q”,“.Q…”]]
class Solution {
List<List<String>> res = new LinkedList<>();
public List<List<String>> solveNQueens(int n) {
// 初始化棋盘
List<String> board = new LinkedList<>();
for (int i=0;i<n;i++){
StringBuilder sb = new StringBuilder();
for (int j=0;j<n;j++){
sb.append(".");
}
board.add(sb.toString());
}
backtrack(board, 0);
return res;
}
// 一行的选择其实就是 决策树的一层
private void backtrack(List<String> board, int row){
// 结束条件 最后一行遍历完是board.size()-1,最后一行的下一行就应该结束了
if (board.size()==row){
res.add(new LinkedList(board));
return ;
}
int n = board.get(row).length();
for (int col=0;col<n;col++){
// 排除不合法选择
if (!isValid(board, row, col)){
continue;
}
// 做出选择:也就是把皇后Q放到当前col
StringBuilder sb = new StringBuilder(board.get(row));
sb.setCharAt(col, 'Q');
board.set(row, sb.toString());
// 去遍历下一层
backtrack(board, row+1);
// 撤销选择
sb.setCharAt(col, '.');
board.set(row, sb.toString());
}
}
private boolean isValid(List<String> board, int row, int col){
int n = board.size();
// 看每一行的当前列 是否有皇后
for (int i = 0; i < n; i++) {
if (board.get(i).charAt(col)=='Q'){
return false;
}
}
// 看左上方 斜线是否有皇后
for (int i=row-1,j=col-1;i>=0&& j>=0;i--,j--){
if (board.get(i).charAt(j)=='Q'){
return false;
}
}
/* 检查右上方是否有皇后互相冲突 */
for (int i = row - 1, j = col + 1;
i >= 0 && j < n; i--, j++) {
if (board.get(i).charAt(j) == 'Q') {
return false;
}
}
return true;
}
}
52.n皇后 ②
给你一个整数 n ,返回 n 皇后问题 不同的解决方案的数量。
int res = 0;
private void backtrack(List<String> board, int row){
// 结束条件 最后一行遍历完是board.size()-1,最后一行的下一行就应该结束了
if (board.size()==row){
res++;
return ;
}
字符串
回文串
5.最长回文串
给你一个字符串 s,找到 s 中最长的回文子串。
如果字符串的反序与原始字符串相同,则该字符串称为回文字符串。
示例 1:
输入:s = “babad”
输出:“bab”
解释:“aba” 同样是符合题意的答案。
示例 2:
输入:s = “cbbd”
输出:“bb”
- 找到以i为中心的最长回文串
回文串有可能是奇数串或偶数串,所以以i为中心可能是i,i 也可能是i,i+1。 找回文串从中间到两边扩展,直到不满足相等条件或越界结束。 - 遍历i
class Solution {
public String longestPalindrome(String s) {
String res = "";
int n = s.length();
for (int i = 0; i < n; i++) {
// 寻找以i为中心的 最长回文串
String s1 = findPalindrome(s,i,i);
String s2 = findPalindrome(s,i,i+1);
res = s1.length()>res.length() ? s1:res;
res = s2.length()>res.length()?s2:res;
}
return res;
}
private String findPalindrome(String s, int l, int r){
while (l>=0 && r<s.length() && s.charAt(l)==s.charAt(r)){
// 向两边扩展
l--;
r++;
}
return s.substring(l+1,r);
}
}
647. 回文子串
给你一个字符串 s ,请你统计并返回这个字符串中 回文子串 的数目。
回文字符串 是正着读和倒过来读一样的字符串。
子字符串 是字符串中的由连续字符组成的一个序列。
具有不同开始位置或结束位置的子串,即使是由相同的字符组成,也会被视作不同的子串。
示例 1:
输入:s = “abc”
输出:3
解释:三个回文子串: “a”, “b”, “c”
示例 2:
输入:s = “aaa”
输出:6
解释:6个回文子串: “a”, “a”, “a”, “aa”, “aa”, “aaa”
中心法
class Solution {
public int countSubstrings(String s) {
int n = s.length();
int res = 0;
for (int i = 0; i < n; i++) {
int jsCount = countHwc(s, i, i);
int osCount = countHwc(s, i, i+1);
res = res + jsCount + osCount;
}
return res;
}
private int countHwc(String s, int left, int right){
int count = 0;
int n = s.length();
while(left>=0 && right<n && s.charAt(left)==s.charAt(right)){
left--;
right++;
count++;
}
return count;
}
}