刷题记录 LeetCode Hot 100
代码思路
- 将出现过的值存储在Map中
- 若当前遍历循环到num,那么只需要在map中找到是否曾经遍历过targer-num,即可找到答案
- 总而言之,维护一个[过去存在的]值的映射
class Solution {
public int[] twoSum(int[] nums, int target) {
Map<Integer,Integer> dic = new HashMap<>();
int n = nums.length;
for(int i=0;i<n;i++)
{
int find = target - nums[i];
if(dic.containsKey(find)) return new int[]{i,dic.get(find)};
dic.put(nums[i],i);
}
return new int[]{-1,-1};
}
}
代码思路: 主要考察数值相加,主要是要记得更新进位数值,以及记得进位末尾添加
- 遍历L1,L2节点,若节点为空,则用0填充值
- 主要是为了看起来简约,其实都是N的时间复杂度,就不搞那么麻烦了
- 构建当前节点,更新进位
- 最后查看进位是否余存
- 返回头节点
class Solution {
public ListNode addTwoNumbers(ListNode l1, ListNode l2) {
ListNode head = new ListNode();
ListNode cur = head;
int carry=0,a=0,b=0;
while(l1!=null || l2!=null)
{
a=0;
b=0;
if(l1!=null)
{
a = l1.val;
l1 = l1.next;
}
if(l2!=null)
{
b = l2.val;
l2 = l2.next;
}
int curval = (a+b+carry)%10;
carry = (a+b+carry)/10;
cur.next = new ListNode(curval,null);
cur = cur.next;
}
if(carry!=0) cur.next = new ListNode(carry);
return head.next;
}
}
代码思路
- 模拟:由于窗口内的值都是不重复的,可以用set模拟窗口大小
- 双指针:用left表示窗口最左指针,right表示正进入窗口的指针
- 当right进入窗口前,判断窗口(set)是否存在该[r]元素
- 若存在,则left指针移动(模拟窗口移动),将[l]元素移出窗口,直到窗口内不存在需要加入的[r]
- 窗口内,每次添加[r]元素
- 记录set.size()最大值
- 返回max
class Solution {
public int lengthOfLongestSubstring(String s) {
int l=0,n=s.length();
Set<Character> set = new HashSet<>();
int max = 0;
for(int r=0;r<n;r++)
{
char cur = s.charAt(r);
while(!set.isEmpty() && set.contains(cur)) set.remove(s.charAt(l++));
set.add(cur);
max = Math.max(set.size(),max);
}
return max;
}
}
代码思路:
- 将两个数组合并,并找出中位数
- 若数组长度为偶数,需要找到mid = n/2后,继续寻找mid-1的元素
- 如n=4,mid=2 --> mid-1=1
- 找中位数采用快排的思路,加速寻找
class Solution {
public double findMedianSortedArrays(int[] nums1, int[] nums2) {
int n = nums1.length,m=nums2.length,index=0;
int total = n+m;
int a=0,b=0;
int[] res = new int[total];
int l=0,r=total-1,tar = total/2;
for(int num : nums1) res[index++]=num;
for(int num : nums2) res[index++]=num;
while(l<=r)
{
int cur = partition(res, l, r);
if(cur==tar)
{
if(total%2==0)
{
a = res[cur];
break;
}
return res[cur];
}
if(cur>tar)
{
r = cur-1;
}else l = cur+1;
}
tar--;
l = 0;
r = tar;
while(l<=r)
{
int cur = partition(res, l, r);
if(cur==tar)
{
return (res[cur]+a)/2.0;
}
if(cur>tar)
{
r = cur-1;
}else l = cur+1;
}
return 0;
}
int partition(int[] arr , int lo,int ro)
{
if(lo>ro) return -1;
int l=lo,r=ro,divi=arr[lo];
while(true)
{
while(l<=r && arr[l]<=divi) l++;
while(l<=r && arr[r]>divi) r--;
if(l>r) break;
swap(arr,l,r);
}
swap(arr,lo,r);
return r;
}
void swap(int[] arr,int r,int l)
{
int temp = arr[l];
arr[l]=arr[r];
arr[r]=temp;
}
}
代码思路:
这个问题有著名算法【Manacher 算法】,但是如果是做题,没有学习过该算法,基本写不出来,所有使用动态规划的思路解决
动态规划
bp[i][j] : 表示str[i,j]是否是回文串,长度为len 当[i]==[j],bp[i][j]只由bp[i+1][j-1]决定 初始化: bp[0][0]=true,表示空串默认为回文串
区间DP:以长度循环条件,遍历左边界-->确定右边界 为了不进行判断,当[i]==[j](元素相同),区间长度小于4,bp[i][j] = true
class Solution {
boolean[][] bp;
int n;
public String longestPalindrome(String s) {
if(s==null) return "";
this.n = s.length();
int maxL=0;
int[] max = new int[2];
bp = new boolean[n+1][n+1];
for(int len=1;len<=n;len++)
{
for(int i=0;i<=n-len;i++)
{
int j = i+len-1;
if(s.charAt(i)==s.charAt(j))
{
if(len<=3) bp[i][j]=true;
else bp[i][j] = bp[i+1][j-1];
if(bp[i][j])
{
if(maxL<len)
{
maxL = len;
max[0]=i;
max[1]=j;
}
}
}
}
}
if(maxL==0) return "";
return s.substring(max[0],max[1]+1);
}
}
代码思路:p是模式串,s是待匹配串
- 动态规划,主要注意点在于如何初始化bp
- 当p长度为0,结果一定是false
- 当s长度为0,结果不一定为false,这是p中的*是可以使得p删除前一个元素,使得p表示空字符
若s[i-1]==p[j-1] or p[j-1]=='.' bp[i][j] = bp[i-1][j-1]; 若p[j]=='*' and j-2>=0; bp[i][j] = bp[i][j-2]; // 删除 若s[i-1]==p[j-2] bp[i][j] |= bp[i-1][j]; // 当*可以匹配,则可以匹配多个,相当于没匹配不变,i-1
class Solution {
public boolean isMatch(String s, String p) {
int n = s.length(),m=p.length();
boolean[][] bp = new boolean[n+1][m+1];
bp[0][0] = true;
for(int i=0;i<=n;i++)
{
for(int j=0;j<=m;j++)
{
if(j==0)
{
continue;
}
if(i==0)
{
if(p.charAt(j-1)=='*' && j-2>=0) bp[i][j] = bp[i][j-2];
continue;
}
char curs = s.charAt(i-1),curp=p.charAt(j-1);
if(curs==curp || curp=='.')
{
bp[i][j] = bp[i-1][j-1];
}else if(curp=='*' && j-2>=0){
bp[i][j] = bp[i][j-2];
if(p.charAt(j-2)=='.' || p.charAt(j-2)==curs)
{
bp[i][j] |= bp[i-1][j];
}
}
}
}
return bp[n][m];
}
}
代码思路:双指针
- 左右指针向中间靠拢,计算左右指针能构成的矩形 -> s = (r-l)*min{h[l],h[r]}
- 每次,移动高度较小的指针,宽度为0时直到指针相遇
class Solution {
public int maxArea(int[] height) {
int l=0,r=height.length-1;
int max=0;
while(l<r)
{
max = Math.max(max,(r-l)*Math.min(height[r],height[l]));
if(height[l]>height[r]) r--;
else l++;
}
return max;
}
}
经典降低维度的思路,将三数之和,降低为俩数之和,注意点在于去重
- 去重
- 首先将数组排序
- 第一个数去重,同时,可以使用双指针,缩小答案范围
- 由于三数之和必须为0,所以确定两个数,就能唯一确定第三个数。当第二个数重复时,第三个数也一定重复,那么,在寻找到合适答案组合后,需要去元素进行去重,也就是左右指针分别找到与答案组不同的值
class Solution {
List<List<Integer>> res = new ArrayList<>();
public List<List<Integer>> threeSum(int[] nums) {
Arrays.sort(nums);
int n = nums.length;
for(int i=0;i<=n-3;i++)
{
if(i!=0 && nums[i]==nums[i-1]) continue;
int find = -nums[i];
int l=i+1,r=n-1;
while(l<r)
{
int cur = nums[l]+nums[r];
if(cur==find)
{
List<Integer> path = new ArrayList<>();
path.add(nums[i]);path.add(nums[l]);path.add(nums[r]);
res.add(path);
l++;r--;// 左右指针分别探测
// 若与答案组元素相同,则跳过
while(l<r && nums[l]==nums[l-1]) l++;
while(l<r && nums[r]==nums[r+1]) r--;
}else if(cur>find)
{
r--;
}else l++;
}
}
return res;
}
}
代码思路
- 递归问题,主要考虑
- 出口
- 树层(当前可以选择的元素集合)
- 树高
- 处理和回溯
- 出口:当路径上的元素个数==str的长度,证明已找到叶子节点
- 树层:每一层树层都是对应字符串下标所表示的字符串
- 树高:本题为字符串长度
- 处理:每次选择一个元素,记录路径,进入递归
- 回溯:将路径值弹出
class Solution {
static HashMap<Integer,String> dic = new HashMap<>(16);
static
{
dic.put(2,"abc");
dic.put(3,"def");
dic.put(4,"ghi");
dic.put(5,"jkl");
dic.put(6,"mno");
dic.put(7,"pqrs");
dic.put(8,"tuv");
dic.put(9,"wxyz");
}
List<String> res = new LinkedList<>();//结果集
StringBuilder path = new StringBuilder();//路径
String digits;
int n;
public List<String> letterCombinations(String digits) {
if(digits==null || digits.length()==0) return res;
this.digits = digits;
this.n = digits.length();
dsf();
return res;
}
void dsf()
{
if(path.length()==n)
{
res.add(path.toString());
return;
}
// 下标其实就是路径的长度
String cur = dic.get(digits.charAt(path.length())-'0');
for(int i=0;i<cur.length();i++)
{
path.append(cur.charAt(i));
dsf();
// path.delete(path.length()-1,path.length());
path.setLength(path.length()-1);
}
}
}
代码思路:
- 通过虚拟头节点删除倒数第N个节点
- 倒数第N个节点其实就是正向链表长度Len-N+1个节点
- 虚拟节点向后移动Len-N个节点,到达第Len-N+1个节点的pre节点,链表元素删除即可
class Solution {
public ListNode removeNthFromEnd(ListNode head, int n) {
if(head==null) return null;
ListNode pre = new ListNode(-1,head);
ListNode cur = pre;
int len = getLen(head);
for(int i=0;i<len-n;i++)
{
if(cur==null) return head;
cur = cur.next;
}
if(cur==null || cur.next==null) return head;
cur.next = cur.next.next;
return pre.next;
}
int getLen(ListNode head)
{
int len=0;
while(head!=null)
{
len++;
head = head.next;
}
return len;
}
}
代码思路:
- 像这种从里向外处理判断的题目,可以使用栈[后进先出]
- 栈中保存的是下一个期待遇到的值
- 也就是栈只保存右括号
- 当遇到左括号
- 期待下一个遇到的是相应的右括号,将相应的右括号压入栈中
- 当遇到右括号
- 期待栈中有对应的值
- 当栈为空或者不是当前相应的右括号,都是非法的
class Solution {
public boolean isValid(String s) {
Stack<Character> stack = new Stack<>();
for(char c : s.toCharArray())
{
if(c=='('){
stack.add(')');
}else if(c=='[')
{
stack.add(']');
}else if(c=='{')
{
stack.add('}');
}else{
// System.out.println(stack);
if(stack.isEmpty() || c!=stack.pop()) return false;
}
}
return stack.isEmpty();
}
}
代码思路
- 每次选择较小的节点,接入结果链表head的尾部tail
- 当L1或者L2为空时,break
- 将剩余的链表接入tail
- 返回head
class Solution {
public ListNode mergeTwoLists(ListNode list1, ListNode list2) {
ListNode head = new ListNode();
ListNode cur = head;
while(list1!=null && list2!=null)
{
if(list1.val<=list2.val)
{
cur.next = list1;
list1 = list1.next;
}else{
cur.next = list2;
list2 = list2.next;
}
cur = cur.next;
}
cur.next = list1==null?list2:list1;
return head.next;
}
}
括号生成,递归实现
- 当前状态需要保证:左括号一定是大于或等于右括号的
- 所以,在选择策略上,当左括号剩余数量大于零,选择左括号
- 只有当左括号数量小于右括号时,才选择右括号
- 当括号剩余总数等于0时,跳出循环
class Solution {
List<String> res = new LinkedList<>();
StringBuilder path = new StringBuilder();
public List<String> generateParenthesis(int n) {
dsf(n,n);
return res;
}
void dsf(int l,int r)
{
if(l+r==0)
{
res.add(path.toString());
return;
}
if(l>0)
{
path.append("(");
dsf(l-1,r);
path.setLength(path.length()-1);
}
if(l<r){
path.append(")");
dsf(l,r-1);
path.setLength(path.length()-1);
}
}
}
代码思路:
- 贪心
- 当一个数是最大数时,元素呈现降序,不存在前一个数大于后一个数【如:4321】
- 当一个数不是最大值时,存在nums[i]
- 从后往前,寻找大于最小影响点的值,与之交换,得到的数一定比原来的数值大【保证了较大要求】
- 为了保证最小,影响点后面的元素保证最小,需要进行排列【保证较大中最小的要求】
class Solution {
public void nextPermutation(int[] nums) {
int n = nums.length;
if(n<=1) return;
int l = -1;
for(int i=n-2;i>=0;i--)
{
if(nums[i]<nums[i+1])
{
l = i;
break;
}
}
if(l==-1)
{
Arrays.sort(nums);
return;
}
for(int i=n-1;i>l;i--)
{
if(nums[l]<nums[i])
{
int temp = nums[i];
nums[i] = nums[l];
nums[l] = temp;
Arrays.sort(nums,l+1,n);
return;
}
}
}
}
代码思路:
- 使用栈来存储合法括号的下标
- 规定左括号为合法括号
- 右括号消耗合法括号,同时计算右括号与上一个合法括号的距离,作为该右括号能够构成的最长合法距离
- 当右括号消耗完后,没有对应的前一个合法下标[stack为空],那么合法下标最左,更新为当前右括号下标
- 初始化
- 合法括号下标-1,假设第一个元素合法,那么合法元素的前一个合法下标为-1
- 含义
- 保存最左合法下标
class Solution {
public int longestValidParentheses(String s) {
Stack<Integer> stack = new Stack<>();
stack.add(-1);
int max=0;
for(int i=0;i<s.length();i++)
{
char cur = s.charAt(i);
if(cur=='(')
{
stack.add(i);
}else{
Integer pre = stack.pop();
if(stack.isEmpty()) stack.add(i);
else
max = Math.max(max,i-stack.peek());
}
}
return max;
}
}
代码思路:
- 通过旋转的数组,被分为俩段递增子数组
- 极端情况,就是一段递增数组
- 主要思路:二分查找
- 二分查找核心
- 有序
- 双指针不断查找范围【重点】
- 如何不断缩小范围
- 判断中点mid是否在递增子数组中
- 若nums[0]<=nums[mid] 证明[0,mid]递增
- 若target==nums[mid] 返回
- 若nums[0]<=nums[mid]成立,判断中点值是否在递增子数组[0,mid)
- 若在,则移动r=mid-1
- 若不在,则移动l=mid+1
- 若nums[0]<=nums[mid]不成立成立,判断mid是否在递增子数组[mid,n-1]中
- 若在,则移动l=mid+1
- 若不在,则移动r=mid-1
class Solution {
public int search(int[] nums, int target) {
int n = nums.length;
int l=0,r=n-1;
while(l<=r)
{
int mid = l+(r-l)/2;
if(nums[mid]==target)
{
return mid;
}
if(nums[0]<=nums[mid])
{
if(nums[0]<=target && target<nums[mid])
{
r = mid-1;
}else{
l = mid+1;
}
}else{
if(nums[mid]<target && target<=nums[n-1])
{
l = mid+1;
}else r = mid-1;
}
}
return -1;
}
}
代码思路
- 通过二分法,先找到最右目标下标,然后重新二分寻找最左下标
- 红蓝二分法可以解决
class Solution {
public int[] searchRange(int[] nums, int target) {
int l=-1;
int r = nums.length;
while(l+1!=r){
int mid = l+(r-l)/2;
if(nums[mid]<target){
l = mid;
}else r = mid;
}
if(r==nums.length || nums[r]!=target) return new int[]{-1,-1};
int L = r;
l = L-1;
r = nums.length;
while(l+1!=r){
int mid = l+(r-l)/2;
if(nums[mid]==target){
l = mid;
}else r = mid;
}
int R = l;
return new int[]{L,R};
}
}
代码思路
- 深搜DFS,
- 元素可以无限次使用,所以可以对数组进行排序,每次取当前最小index,直到target为0或者小于零时,为出口
- 树层:当前最小元素开始为树层
- 树高:最低为当前取最小值,直到target小于等于零
- 出口:target小于等于零
class Solution {
List<List<Integer>> res = new LinkedList<>();
Deque<Integer> path = new LinkedList<>();
int[] candidates;
public List<List<Integer>> combinationSum(int[] candidates, int target) {
Arrays.sort(candidates);
this.candidates = candidates;
dsf(0,target);
return res;
}
void dsf(int index,int target)
{
if(target==0)
{
res.add(new LinkedList<>(path));
return;
}
if(target<0) return;
for(int i=index;i<candidates.length && target>=candidates[i];i++)
{
path.addLast(candidates[i]);
dsf(i,target-candidates[i]);
path.removeLast();
}
}
}
代码思路:
- 单调栈stack:如何考虑单调栈问题
- 递增还是递减?换句话说,什么时候需要进行处理,什么时候不需要进行处理?处理结果是什么?
- 接雨水属于单调递减栈,意味着当bigger element coming need deal
- 为何要选单调递减?
- 这是由于,当单调递减时,并不积雨,就是没有对结果造成影响
- 当单调递减时,突然遇到一个比最小值(栈顶)大的元素,此时会形成凹槽,只需要收集凹槽里的雨水即可
- 这里有一个误区,认为凹槽是一次性算出来的,其实凹槽是一步一步累和的
- 如何计算凹槽?
- 但遇到较大元素B,此时栈顶元素Low的高度一定小于栈顶第二元素Pre,同时小于B (Pre>Low && Low
- 我们每次计算的是,此时由栈顶Low元素为“托盘”所能积攒的雨水
- 当前雨水 = w*h = (B-Pre-1) * (Min{[B],[Pre]}-[Low]): 单调栈存储的是数组下标
- 当stack为空,pre不存在时?
- 那证明本次处理,没有左柱子,将较大元素B压入栈中,作为左柱子参考
class Solution {
public int trap(int[] height) {
Stack<Integer> stack = new Stack<>();
int n = height.length;
int res = 0;
for(int i=0;i<n;i++)
{
if(stack.isEmpty())
{
stack.add(i);
continue;
}
int num = height[i];
while(!stack.isEmpty() && num>height[stack.peek()])
{
int mid = stack.pop();
if(stack.isEmpty()) continue;
int pre = stack.peek();
int w = i-pre-1;
int h = Math.min(num,height[pre]) - height[mid];
res += h*w;
}
stack.add(i);
}
return res;
}
}
代码思路
- 全排列问题作为经典的DFS题目,有两个特点
- 无序,且不重复使用
- 没有重复元素
- 根据这两个特点
- 无序:需要使用used[] 来标记当前树层中的元素哪一些时处理过的
- 没有重复元素:不用考虑排序后的去重问题
class Solution {
List<List<Integer>> res = new LinkedList<>();
Deque<Integer> path = new LinkedList<>();
int[] nums;
public List<List<Integer>> permute(int[] nums) {
int n = nums.length;
this.nums = nums;
boolean[] used = new boolean[n];
dsf(used);
return res;
}
void dsf(boolean[] used)
{
if(path.size()==nums.length)
{
res.add(new LinkedList(path));
return;
}
for(int i=0;i<nums.length;i++)
{
if(!used[i])
{
used[i] = true;
path.add(nums[i]);
dsf(used);
path.removeLast();
used[i] = false;
}
}
}
}
代码思路
数组移动问题
- 数组拷贝 O(N)
- 原地反转,找规律
题目顺时针旋转90度后,呈现规律
- (当n=4时)
- (0,0)->(0,3)
- (0,1)->(1,3)
- (0,2)->(2,3)
- (1,1)->(1,2)
- (i,j)->(j,n-j-1)
顺时针顺序
[i][j] --> [j][n-i-1] --> [n-i-1][n-j-1] -->[n-j-1][i]
当顺时针旋转后
[i][j]被赋值的是最后一个[n-j-1][i]
旋转个数
- 当N为偶数,行和列都遍历N/2个元素
- 当N为奇数,行+1==列,这是(N/2+1)+N/2 == N,此时选择行大1,旋转后刚好等于N/2–>N
class Solution {
public void rotate(int[][] matrix) {
// [i][j] -> [n-j-1][i] [0][1] - > [2][0]
int n = matrix.length;
for(int i=0;i<(n+1)/2;i++)
{
for(int j=0;j<n/2;j++)
{
int t = matrix[i][j];
matrix[i][j] = matrix[n-j-1][i];
matrix[n-j-1][i] = matrix[n-i-1][n-j-1];
matrix[n-i-1][n-j-1] = matrix[j][n-i-1];
matrix[j][n-i-1] = t;
}
}
}
}
代码思路
- 利用Map进行分组
- 分组条件:将str转换为字典序最小的字符串strMin,保证同类字母异位词,strMin唯一
- 以strMin为key,put到Map中,遍历map.values即可
class Solution {
List<List<String>> res = new LinkedList<>();
public List<List<String>> groupAnagrams(String[] strs) {
Map<String,List<String>> dic = new HashMap<>();
for(String str : strs)
{
String cur = getCode(str);
if(!dic.containsKey(cur)) dic.put(cur,new ArrayList<>());
dic.get(cur).add(str);
}
res.addAll(dic.values());
return res;
}
String getCode(String str)
{
int[] arr = new int[26];
StringBuilder res = new StringBuilder();
for(char c : str.toCharArray()) arr[c-'a']++;
for(int i=0;i<arr.length;i++)
{
if(arr[i]==0) continue;
String cur = ((char)('a'+i))+"";
res.append(cur.repeat(arr[i]));
}
return res.toString();
}
}
代码思路
- 动态规划dp[i] = (nums[i],dp[i-1]+nums[i-1]),其中dp[i]表示必须以第i个元素结尾最大的连续子数组和
- 但是,bp[i]的状态只有bp[i-1]和nums[i-1]有关,可以采用递推的发生维护最大值
- 遍历元素ele,利用pre,ele,不断更新最大值
- pre表示前子元素最大子数组和
class Solution {
public int maxSubArray(int[] nums) {
int n = nums.length;
if(n==0) return 0;
int max = nums[0];
int pre = nums[0];
for(int i=1;i<n;i++)
{
max = Math.max(max,Math.max(nums[i],nums[i]+pre));
pre = Math.max(nums[i],nums[i]+pre);
}
return max;
}
}
贪心:
- 最大值表示:当前可以达到的最远距离
- 初始化: max = 0 + nums[0],表示当前从0开始最大能走到max下标位置
- 通过能到达的点,不断更新max
- 当max>=n-1时,证明可以走到终点
class Solution {
public boolean canJump(int[] nums) {
int n = nums.length;
if(n<=1) return true;
int max=nums[0];
for(int i=1;i<n;i++)
{
if(i<=max)
{
max = Math.max(max,i+nums[i]);
if(max>=n-1) return true;
}
}
return max>=n-1;
}
}
双指针:
- l,r表示的是,当前可能的区间长度
- 如何确定当前区间[l,r]是独立?
- 对区间进行排序 --> 为了能够确定当前区间[l,r]是否是独立的
- 当最小起点L,延申出去R,都达不到比L稍小的排序后的下一区间next的起点next[0],那么证明当前l,r是独立的,脱节的。[l,r]为答案的一个独立区间,更新l,r从下一个起点开始合并,l = next[0],r=next[1]
- 当最小起点L,延申出去R,达到了next[0],证明当前俩个区间是交融在一起的,更新l,r,r=Max{r,next[1]} (其实l不用更新)
class Solution {
public int[][] merge(int[][] intervals) {
Arrays.sort(intervals,(arr,brr)->arr[0]-brr[0]);
List<int[]> res = new ArrayList<>();
int n = intervals.length;
if(n<=1) return intervals;
int l=intervals[0][0],r = intervals[0][1];
for(int i=1;i<n;i++)
{
int[] next = intervals[i];
if(r<next[0])
{
res.add(new int[]{l,r});
l = next[0];
r = next[1];
}else{
r = Math.max(r,next[1]);
}
}
res.add(new int[]{l,r});
return res.toArray(new int[0][]);
}
}
动态规划:
状态转移:bp[i][j]=bp[i-1][j-1]; // i,j表示第i行j列,并且从一开始 这是因为机器人只能向下或者向右移动,只由两个状态决定-->爬楼梯问题; 初始化:bp[i][j]=1;bp[0][j]=0;bp[i][0]=0;
class Solution {
public int uniquePaths(int m, int n) {
int[][] bp = new int[m+1][n+1];
bp[1][1] = 1;
for(int i=1;i<=m;i++){
for(int j=1;j<=n;j++){
if(i==1 && j==1) continue;
bp[i][j] = bp[i-1][j] + bp[i][j-1];
}
}
return bp[m][n];
}
}
动态规划
1. 因为只能向下或者向右移动,只由两个状态决定-->爬楼梯问题; 2. 状态转移方程 bp[i][j] = Max{bp[i-1][j],bp[i][j-1]} + nums[i-1][j-1]; 3. 初始化 bp[1][1] = nums[0][0]
class Solution {
int n,m;
public int minPathSum(int[][] grid) {
n = grid.length;
m = grid[0].length;
int[][] bp = new int[n+1][m+1];
for(int[] arr : bp) Arrays.fill(arr,100*200);
bp[1][1] = grid[0][0];
for(int i=1;i<=n;i++)
{
for(int j=1;j<=m;j++)
{
if(i==1 && j==1) continue;
bp[i][j] = Math.min(bp[i-1][j],bp[i][j-1])+grid[i-1][j-1];
}
}
return bp[n][m];
}
}
爬楼梯问题
class Solution {
public int climbStairs(int n) {
if(n<3) return n;
int[] bp = new int[n];
bp[0] = 1;
bp[1] = 2;
for (int i = 2; i < n; i++) {
bp[i] = bp[i-1]+bp[i-2];
}
return bp[n-1];
}
}
动态规划
如何考虑状态?
两个字符串一般bp[i][j]: i表示前一个字符串前i位,j表示后一个字符串前j位,bp[i][j]表示将s[0,i]-->p[0,j]编辑距离最短值,i表示的是字符串的元素个数也可以理解为从零开始的长度,不是下标
如何考虑状态转移?
当[i]==[j] bp[i][j] = bp[i-1][j-1] 当[i]!=[j] bp[i][j] = min{bp[i-1][j],bp[i-1][j-1],bp[i][j-1]}+1 1. word1选择删除 --> bp[i-1][j] 2. word1选择替换(一定替换成与[j]相同的最小) --> bp[i-1][j-1] 3. word1选择插入(一定插入成与[j]相同的最小) --> bp[i][j-1] 4. 每个操作对应一次操作,结果+1
初始化
bp[0][j] = j;bp[i][0]=i;//对应删除与插入
class Solution {
public int minDistance(String word1, String word2) {
int n = word1.length(),m = word2.length();
int[][] bp = new int[n+1][m+1];
for(int i=0;i<=n;i++) bp[i][0] = i;
for(int i=0;i<=m;i++) bp[0][i] = i;
for(int i=1;i<=n;i++)
{
for(int j=1;j<=m;j++)
{
char cur1 = word1.charAt(i-1);
char cur2 = word2.charAt(j-1);
if(cur1==cur2)
{
bp[i][j] = bp[i-1][j-1];
}else{
int remove = Math.min(bp[i-1][j],bp[i][j-1])+1;
int replace = bp[i-1][j-1]+1;
int insert = Math.min(bp[i][j-1],bp[i-1][j])+1;
bp[i][j] = Math.min(remove,Math.min(replace,insert));
}
}
}
return bp[n][m];
}
}
快排:直接排序
class Solution {
int[] nums;
public void sortColors(int[] nums) {
this.nums = nums;
partition(0,nums.length-1);
}
void partition(int lo,int ro)
{
if(lo>ro) return;
int l=lo,r=ro,divi=nums[lo];
while(true)
{
while(l<=r && nums[l]<=divi) l++;
while(l<=r && nums[r]>divi) r--;
if(l>r) break;
swap(l,r);
}
swap(lo,r);
partition(lo,r-1);
partition(r+1,ro);
}
void swap(int l,int r)
{
int t = nums[l];
nums[l] = nums[r];
nums[r] = t;
}
}
代码思路:
- 子集,将所有路径都保存
- 树层:当前元素以及后续元素
- 树高:当当前元素下标index大于等于数组长度,跳出循环
class Solution {
List<List<Integer>> res = new LinkedList<>();
Deque<Integer> deque = new LinkedList<>();
int[] nums;
public List<List<Integer>> subsets(int[] nums) {
this.nums = nums;
dsf(nums.length,0);
return res;
}
void dsf(int n,int index)
{
res.add(new LinkedList(deque));
if(index>=n)
{
return;
}
for(int i = index;i<n;i++)
{
deque.add(nums[i]);
dsf(n,i+1);
deque.removeLast();
}
}
}
深搜DFS:从某一点出发,累计当前匹配个数N
树层:上下左右四个方向,且当前未被访问过的方向
出口:当当前匹配个数N==str.length()
class Solution {
char[][] board;
String word;
int n,m,len;
public boolean exist(char[][] board, String word) {
this.board = board;
this.word = word;
this.n=board.length;
this.m = board[0].length;
boolean[][] used = new boolean[n][m];
this.len = word.length();
for(int i=0;i<n;i++)
{
for(int j=0;j<m;j++)
{
if(dsf(i,j,0,used)) return true;
}
}
return false;
}
boolean dsf(int x,int y,int cnt,boolean[][] used)
{
if(cnt==len)
{
return true;
}
if(x<0 || y<0 || x>=n || y>=m) return false;
if(board[x][y]!=word.charAt(cnt) || used[x][y]) return false;
used[x][y] = true;
boolean res = dsf(x,y+1,cnt+1,used) || dsf(x+1,y,cnt+1,used) || dsf(x,y-1,cnt+1,used) || dsf(x-1,y,cnt+1,used);
used[x][y] = false;
return res;
}
}
单调栈:
- 与接雨水类似,但是最大矩形是当最小值出现时,处理stack
- 且为了方便,前后添加两个0高度的虚拟柱子
- 首个0,计算时,不会出现stack为空的情况
- 尾巴0,为了在结束的时候,自动计算stack中的矩形面积
- 计算公式
- h = [stack.pop()];
- w = (i-stack.peek()-1)
class Solution {
public int largestRectangleArea(int[] heights) {
// coming small deal
int len=heights.length,res = 0;
if(len==0) return 0;
int[] copy = new int[len+2];
System.arraycopy(heights,0,copy,1,len);
heights = copy;
len+=2;
Stack<Integer> stack = new Stack<>();
stack.add(0);
for (int i = 1; i < len; i++) {
while(!stack.isEmpty() && heights[stack.peek()]>heights[i])
{
int h = heights[stack.pop()];
int w = i-stack.peek()-1;
res = Math.max(res,h*w);
}
stack.add(i);
}
return res;
}
}
代码思路:
从列上看,累和当前元素高度
if(g[i][j]=='1') bp[i][j] = bp[i-1][j]+1;
累和过程中,从右到左,计算该结点往左边[k,0],能构成的矩形的最大值
维护一个minH,表示当前矩形中最小的高度,若minH>0,则找到一个可行的矩形; int curS[k,j] = (j-k+1)*minH
class Solution {
int n,m;
public int maximalRectangle(char[][] matrix) {
this.n = matrix.length;
this.m = matrix[0].length;
int[][] bp = new int[n][m];
int max=0;
for(int i=0;i<n;i++)
{
for(int j=0;j<m;j++)
{
if(matrix[i][j]=='1')
{
bp[i][j] = 1;
if(i-1>=0) bp[i][j] += bp[i-1][j];
int minH = bp[i][j];
for(int k=j;(k>=0 && matrix[i][k]=='1');k--)
{
minH = Math.min(minH,bp[i][k]);
max = Math.max(max,(j-k+1)*minH);
}
}
}
}
return max;
}
}
考察二叉树的中序遍历
class Solution {
List<Integer> res = new ArrayList<>();
public List<Integer> inorderTraversal(TreeNode root) {
if(root==null) return res;
inorderTraversal(root.left);
res.add(root.val);// 中序
inorderTraversal(root.right);
return res;
}
}
代码思路
- 一个树,如果呈对称性质
- 那么对称节点A,B相同
- 对称节点的A左节点与B右节点对称
- 对称节点的A右节点与B左节点对称
class Solution {
public boolean isSymmetric(TreeNode root) {
if(root==null || (root.left==null && root.right==null)) return true;
return reverse(root,root);
}
public boolean reverse(TreeNode left,TreeNode right)
{
if(left==null && right==null) return true;
if(left==null || right==null) return false;
if(left.val!=right.val) return false;
return reverse(left.left,right.right) && reverse(left.right,right.left);
}
}
代码思路
- 一个树的节点,如果不是叶子节点,那它的深度H=Max{左子树深度,右子树深度}+1
class Solution {
public int maxDepth(TreeNode root) {
if(root==null) return 0;
if(root.left==null && root.right==null) return 1;
return 1 + Math.max(maxDepth(root.left),maxDepth(root.right));
}
}
代码思路
- 也可以采用DFS深搜的方式,记录叶子节点的深度,取最大
class Solution {
int max;
public int maxDepth(TreeNode root) {
dsf(root,0);
return max;
}
void dsf(TreeNode root,int h){
if(root==null){
max = Math.max(max,h);
return;
}
dsf(root.left,h+1);
dsf(root.right,h+1);
}
}
树的层序遍历,一般借助队列完成
class Solution {
List<List<Integer>> res = new ArrayList<>();
Deque<TreeNode> queue = new LinkedList<>();
public List<List<Integer>> levelOrder(TreeNode root) {
if(root==null) return res;
queue.add(root);
while(!queue.isEmpty())
{
int size = queue.size();
ArrayList<Integer> path = new ArrayList<>();
for(int i=0;i<size;i++)
{
TreeNode top = queue.pop();
path.add(top.val);
if(top.left!=null) queue.add(top.left);
if(top.right!=null) queue.add(top.right);
}
res.add(path);
}
return res;
}
}
贪心:
- 买卖股票,且只买卖一次,那么一定是在低点买入,高点卖出时获取到最大利润
- 不断记录当前股市的最小买入点
- 每次尝试卖出,更新最大利润
- 同时,更新最小值
- 从几何上看,其实求解的是每个点,往前看(这是因为买入时一定比卖出时早),所能构成的最大正数差值
class Solution {
public int maxProfit(int[] prices) {
int n = prices.length;
if(n<=1) return 0;
int min = prices[0];
int ans = 0;
for(int num : prices)
{
ans = Math.max(num-min,ans);
min = Math.min(min,num);
}
return ans;
}
}
代码思路
- 在无序重复的数组中,找到等差数列公差等于一的序列,可任意排序,要求不重复
- 不重复,可用set来去重,同时由set获取元素复杂度为O(1)
- 公差等于一,如何确定策列
- 遍历set,获取到当前元素num,公差为1,那么next=num+1
- 判断next是否在set中
- 如果在set中,那么num++,继续判断下一个next
- 直到next不在set中,记录循环次数即可获取以num为起点的等差队列的长度
- 时间复杂度
- O(n^2),每个元素都要循环一遍,超时
- 优化
- 不需要,等差数列上的每个值都作为起点,遍历循环
- 判断当该点是等差数列起点,才进行循环,也就是当num-1不存在set中
- 时间复杂度(优化)
- O(N)
class Solution {
public int longestConsecutive(int[] nums) {
if(nums.length<=1) return nums.length;
Set<Integer> set = new HashSet<>();
for(int num : nums)
{
set.add(num);
}
int max=1;
for(int num : set)
{
int findLen=1;
if(!set.contains(num - 1))
{
while(set.contains(num+1))
{
findLen++;
num++;
}
max = max>findLen?max:findLen;
}
}
return max;
}
}
代码思路
- 统计元素出现次数,暴力hashMap,时间复杂度O(2*N)
- 一次遍历能否搞定?
- 异或操作 (^)
- 特点:两数相同异或为0,任何数与0异或不变
- 初始化为0,不改变结果
class Solution {
public int singleNumber(int[] nums) {
int ans=0;
for(int num : nums)
{
ans ^= num;
}
return ans;
}
}
动态规划:
- 动态规划,解决字符串问题,要么bp[i]表示从0开始到i,要么**bp[i][j]**表示从i到j的结果
- 本题只需要解决整个字符串能否被表示,即可,bp[i]表示: 从第0个字符开始,到第i个字符结束的字符串能否被word表示
- bp[i] = bp[j] && word.contains(str.substring(j,i)); 0<=j<=i
- j==i时,表示bp[i]能否被word组成
- j
- 其中bp[j]是集合的体现,看则bp[i]被分为两段bp[j]和str[j::i]
- 实则bp[j]表示的是str[0::j]被切分为多块,能否被word表示
class Solution {
public boolean wordBreak(String s, List<String> wordDict) {
int n = s.length();
boolean[] bp = new boolean[n+1];
bp[0] = true;
for(int i=1;i<=n;i++)
{
for(int j=i;j>=0;j--)
{
bp[i] = bp[j] && wordDict.contains(s.substring(j,i));
if(bp[i]==true) break;
}
}
return bp[n];
}
}
快慢指针的经典题目
- 如果存在环,那么可以指针是走不出来的,也就是进入了死循环,路程为无穷
- 如果指针到达了终点,那么不存在环
- 如果存在环,两个指针A、B,A一次走一格,B一次走两格,每次B与A的差值都为减一(1列表的距离单元),存在环路程无穷,当A迟早会被B追上
- 如果没有环,B根本不会走回头路,没有相遇的可能
public class Solution {
public boolean hasCycle(ListNode head) {
if(head==null){
return false;
}
ListNode fast = head;
ListNode slow = head;
while (fast.next!=null && fast.next.next!=null){
fast = fast.next.next;
slow = slow.next;
if(fast==slow) return true;
}
return false;
}
}
代码思路
- 找环入口的算法
- 第一次相遇,slow = nb
- a+nb = 入口点
- slow再走a = 入口
- head走到入口 = a
- 起始距离入口 = 第一次相遇位置 + a
- 俩次循环
- 第一次找到环,也就是快慢指针相碰,此时,slow指针距离入口为a路程,head指针距离入口为a路程
- 第二次循环,head与slow同时向前走,直到相碰,此时都走了a,且为入口
public class Solution {
public ListNode detectCycle(ListNode head) {
ListNode fast = head, slow = head;
while (true) {
if (fast == null || fast.next == null) return null;
fast = fast.next.next;
slow = slow.next;
if (fast == slow) break;
}
fast = head;
while (slow != fast) {
slow = slow.next;
fast = fast.next;
}
return fast;
}
}
代码思路
- 用Set来存储slow指针是否往回走,往回走的点为入口
public class Solution {
ListNode fast,slow;
public ListNode detectCycle(ListNode head) {
fast = head;
slow = head;
int pos=0;
Set<ListNode> set = new HashSet<>(10000);
while(fast!=null && fast.next!=null)
{
if(set.contains(slow)) return slow;
if(!set.contains(slow)) set.add(slow);
fast = fast.next.next;
slow = slow.next;
pos++;
}
return null;
}
}
考点:双向链表 + HashMap
- 使用双向链表来存储缓存数据(key,value)
- hashMap用于定位元素(key,value),加快数据更新
class LRUCache {
// 数据存储
Node head,tail;
// 管理定位数据[key::value]
Map<Integer,Node> dic;
// 容量
int capacity;
int size;
public LRUCache(int capacity) {
this.head = new Node();
this.tail = new Node();
head.next = tail;
tail.pre = head;
dic = new HashMap<>(capacity+10);
this.capacity = capacity;
}
public int get(int key) {
if(!dic.containsKey(key)) return -1;
Node node = dic.get(key);
moveToHead(node);
return node.value;
}
void moveToHead(Node node)
{
removeNode(node);
addToHead(node);
}
void removeNode(Node node)
{
if(node==null || !dic.containsKey(node.key)) return;
node.pre.next = node.next;
node.next.pre = node.pre;
}
void addToHead(Node node)
{
node.pre = head;
node.next = head.next;
head.next = node;
node.next.pre = node;
}
public void put(int key, int value) {
Node node = dic.get(key);
if(node==null)
{
node = new Node(key,value);
size++;
}
// 更新
node.value = value;
// 最新数据
moveToHead(node);
dic.put(key,node);
if(size>capacity)
{
Node last = removeLast();
dic.remove(last.key);
size--;
}
}
Node removeLast()
{
Node last = tail.pre;
removeNode(last);
return last;
}
class Node {
int capacity;
int size;
int key;
int value;
Node pre;
Node next;
public Node(){}
public Node(int key,int value)
{
this.key = key;
this.value = value;
}
}
}
归并排序
- 排序出口:当链表长度<=1时,return;
- 使用快慢指针,将链表切割成两段(mid.next = null),分别排序,排序后合并链表
class Solution {
public ListNode sortList(ListNode head) {
if(head==null || head.next==null) return head;
// 记得,fast=head.next,向下取整(奇数个节点找到中点,偶数个节点找到中心左边的节点)
ListNode mid = getMid(head);
ListNode head2 = mid.next;
// 将链表分段
mid.next = null;
ListNode left = sortList(head);
ListNode right = sortList(head2);
return merge(left,right);
}
ListNode getMid(ListNode head)
{
ListNode slow=head,fast=head.next;
while(fast!=null && fast.next!=null)
{
fast = fast.next.next;
slow = slow.next;
}
return slow;
}
public ListNode merge(ListNode head1, ListNode head2) {
ListNode left = head1,right=head2;
ListNode temp = new ListNode(0);
ListNode cur = temp;
while(left!=null && right!=null)
{
if(left.val<=right.val)
{
cur.next = new ListNode(left.val);
left = left.next;
}else
{
cur.next = new ListNode(right.val);
right = right.next;
}
cur = cur.next;
}
cur.next = left==null?right:left;
return temp.next;
}
}
代码思路
- 数组存在负数,最小值乘积可能遇到负数摇身一变变成最大值,最大值也可能乘积后变成最小值
- 维护两个乘积dp,分别记录当前状态可能的最大值和最小值
- dmax[i] = Math.max(nums[i],Math.max(nums[i]*dmax[i-1],nums[i]*dmin[i-1]));
- dmin[i] = Math.min(nums[i],Math.min(nums[i]*dmax[i-1],nums[i]*dmin[i-1]));
class Solution {
int max;
public int maxProduct(int[] nums) {
int n = nums.length;
int[] dmax = new int[n];
int[] dmin = new int[n];
dmax[0] = nums[0];
dmin[0] = nums[0];
max = nums[0];
for(int i=1;i<n;i++)
{
dmax[i] = Math.max(nums[i],Math.max(nums[i]*dmax[i-1],nums[i]*dmin[i-1]));
dmin[i] = Math.min(nums[i],Math.min(nums[i]*dmax[i-1],nums[i]*dmin[i-1]));
max = Math.max(max,Math.max(dmin[i],dmax[i]));
}
return max;
}
}
代码思路:
- 要保存最小值信息,除了维护一个正常工作的栈Daily,还要一个单调递减(不严格)的单调栈Min保留最小值
- 具体操作
- 当一个元素入栈时
- 如果不大于Min的栈顶元素,压入Min
- 为什么较大元素MAX,Min不处理
- 这是由于栈是一个先进后出的结构,Min维护的是当前最小值。晚来的MAX,如果较早来的元素MIN比MAX小,那么在MIN存在的时候,MAX不可能成为最小值,而MAX又比MIN先出栈,所有MAX是被丢弃是数据,min不进行保留
class MinStack {
Stack<Integer> daily;
Stack<Integer> min;
public MinStack() {
this.daily = new Stack<>();
this.min = new Stack<>();
}
public void push(int val) {
daily.add(val);
if(min.isEmpty() || min.peek()>=val)
min.add(val);
}
public void pop() {
if(daily.isEmpty()) return;
int top = daily.pop();
if(top==min.peek()) min.pop();
}
public int top() {
if(daily.isEmpty()) return -1;
return daily.peek();
}
public int getMin() {
if(min.isEmpty()) {
return -1;
}
return min.peek();
}
}
代码思路
- 两条链表的长度分别为a,b
- A链表走完A链表后走B链表,一共走了a+b
- B链表走完B链表后走A链表,一共走了a+b
- 同样的速度,走完同样的路程
- 如果有相交的路段,一定会相遇
- 如果没有相交的路段,那么同样的状态,A,B都会走到终点null,返回null
public class Solution {
public ListNode getIntersectionNode(ListNode headA, ListNode headB) {
if(headA==null && headB==null) return null;
if(headA==null || headB==null) return null;
ListNode ha = headA,hb = headB;
while(ha!=hb)
{
ha = ha==null?headB:ha.next;
hb = hb==null?headA:hb.next;
}
return ha;
}
}
代码思路
- 摩根投票
- 多数元素的个数大于其他元素的个数,用多数元素的个数减去其它元素的个数,数值大于零
- 具体操作
- 选举一个元素ele
- 如果元素出现,vo++
- 如果其他元素出现,vo–
- 如果vo==0,更换ele
class Solution {
public int majorityElement(int[] nums) {
int ele = nums[0];
int vo=0;
for(int num : nums)
{
if(vo==0)
{
ele = num;
}
vo+=num==ele?1:-1;
}
return ele;
}
}
动态规划
- 抢与不抢
- bp[i]:抢劫[0,i]区间,能获取到的最大利润,i表示个数不是下标
- bp[i] = Max{bp[i-1],bp[i-2]+nums[i]}
class Solution {
public int rob(int[] nums) {
int n = nums.length;
if(n<=1) return nums[0];
int[] bp = new int[n+1];
bp[1] = nums[0];
for(int i=2;i<=n;i++)
{
bp[i] = Math.max(bp[i-1],bp[i-2]+nums[i-1]);
}
return bp[n];
}
}
代码思路:
- 遇到岛屿,感染岛屿成海洋,ans++
- DSF感染岛屿即可
class Solution {
char[][] grid;
int res;
public int numIslands(char[][] grid) {
this.grid = grid;
for(int i=0;i<grid.length;i++)
{
for(int j=0;j<grid[0].length;j++)
{
if(grid[i][j]=='1')
{
dsf(i,j);
res++;
}
}
}
return res;
}
void dsf(int x,int y)
{
if(x<0 || y<0 || x>=grid.length || y>=grid[0].length || grid[x][y]!='1')
{
return;
}
grid[x][y] = '0';
dsf(x,y+1);
dsf(x+1,y);
dsf(x,y-1);
dsf(x-1,y);
}
}
递归反转:
- 反转下一个链表节点,处理前一个链表与反转后的链表的关系
class Solution {
public ListNode reverseList(ListNode head) {
if(head==null || head.next==null) return head;
ListNode temp = reverseList(head.next);
head.next.next = head;
head.next = null;
return temp;
}
}
代码思路
外层数据依赖里面数据展开得到的
- 例如 ab2[ef]–> 需要由ef展开后得到 efef并与ab拼接后,才能得到答案
- 那么在计算ef时需要提前记录好的数据就需要 一个数字表示repeat [ef] 多少次,并且还需要知道ef前面的数据ab
如何保留[ef]前边数据,以便于在计算ef时可以用到呢?
- 一般化表达式 ab2[ef]–>.[…[ab2[ef]]…].,我们知道ab2[ef]有可能并不是最外层,那么我们需要在进入[ef]时,就记录前一个[到[ef的数据,也就是res,mul保留的其实是前一个[到[的数字和字符串信息
如果不好理解为什么res=new StringBuilder(strs.pop().toString() + res.toString().repeat(nums.pop()));
- 可以将每一个字符串看成 3[a]2[bc]a–>1[3[a]2[bc]a]
- 那么当[3**[**–>自然res压入的是""
- 1[3[a**]2[b: 展开是res = aaa, 因为res指的是1[3[a]2[**bc]a] 这两个现阶段的字符串信息(加粗表示)
为什么使用栈记录res,mul?
- 从里到外,恢复字符串时,最里层用到的时次里层,次里层又是较晚出现的,满足后进先出的规律–>用栈来表达更好
class Solution {
public String decodeString(String s) {
Stack<Integer> nums = new Stack<>();
Stack<StringBuilder> strs = new Stack<>();
StringBuilder res = new StringBuilder();
int mul = 0;
for(char c : s.toCharArray())
{
if(Character.isDigit(c))
{
mul = mul * 10 +(c-'0');
}else if(c=='[') // 记录、清除数据
{
nums.add(mul);
strs.add(new StringBuilder(res));
res.setLength(0);
mul = 0;
}else if(c==']') // 展开数据
{
res = new StringBuilder(strs.pop().toString() + res.toString().repeat(nums.pop()));
}else{
res.append(c);
}
}
return res.toString();
}
}
- 可以使用大根堆,弹出k个元素即可
- 可以使用快排partition,partition作用返回元素在数组排序后正确的下标,那么第k大元素,相当于下标位n-k
- 使用双指针根据partition的返回值,定位可能的区间,快速获取n-k的元素
class Solution {
public int findKthLargest(int[] nums, int k) {
// sorted in index=n-k
int n = nums.length;
int tarIndex = n-k;
int l=0,r=n-1;
while(l<=r)
{
int cur = partition(nums,l,r);
if(cur==tarIndex){
return nums[cur];
}else if(cur>tarIndex)
{
r = cur-1;
}else l = cur+1;
}
return -1;
}
public int partition(int[] nums,int lo,int ro)
{
if(lo>ro) return -1;
int l = lo,r=ro,fir=nums[lo];
while(true)
{
while(l<=r && nums[l]<=fir) l++;
while(l<=r && nums[r]>fir) r--;
if(l>r) break;
swap(nums,l,r);
}
swap(nums,lo,r);
return r;
}
public void swap(int[] nums,int l,int r)
{
int temp = nums[l];
nums[l] = nums[r];
nums[r] = temp;
}
}
动态规划
- 首先一个点A要想构成正方形(指的是A向左边和上边扩展能达到的最大正方形),要由上边点B、左边点C、左上点D的点共同决定
- 为什么?
- 假设A想要构成一个边长为N的正方形
- 上边B点需要向上补齐最少N-1
- 左边C点需要向左补齐最少N-1
- 左上角D点需要向左补齐最少N-1
- 当某一个条件不满足,A就不能向左扩展出一个N的正方形
- 同理,A能扩展的最大值,要BCD同时满足,那么BCD取最小值的时候,可以同时满足
- Max{A} = Min{B,C,D}+1
class Solution {
public int maximalSquare(char[][] matrix) {
int n = matrix.length,m=matrix[0].length;
int[][] bp = new int[n][m];
int max = 0;
for(int i=0;i<n;i++)
{
for(int j=0;j<m;j++)
{
if(matrix[i][j]=='1')
{
bp[i][j] = 1;
if(i-1>=0 && j-1>=0) bp[i][j] += Math.min(bp[i-1][j-1],Math.min(bp[i-1][j],bp[i][j-1]));
max = Math.max(max,bp[i][j]);
}
}
}
return max*max;
}
}
代码思路
- 左右子树反转后,调整父节点
class Solution {
public TreeNode invertTree(TreeNode root) {
if(root==null || (root.left==null && root.right==null))
return root;
TreeNode left = invertTree(root.left);
TreeNode right = invertTree(root.right);
root.left = right;
root.right = left;
return root;
}
}
动态规划:
- 先求左边数组的乘积,保留在数组里: left[i] = left[i-1] * nums[i-1] (i指的是下标)
- right也可以同理求得,当是right不需要记录,所以可以用迭代的方式处理
- right初始化为1,表示第n-1层right的乘积
- 逆序, 每次right*nums[i+1] (i从n-2开始)
- 结果left[i]*right
class Solution {
public int[] productExceptSelf(int[] nums) {
int n = nums.length;
int[] left = new int[n];
left[0]=1;
for(int i=1;i<n;i++)
{
left[i] = nums[i-1] * left[i-1];
}
int res = 1;
for(int i=n-2;i>=0;i--)
{
res = res * nums[i+1];
left[i] *= res;
}
return left;
}
}
滑动窗口/双指针 + 单调队列
- 窗口长度为k,初始化left、right指针
- right指针指向0,表示要出窗口的值
- left指针初始化1-k
- r-l+1=k–> l = k-1
- 当r值要入窗口前,维护单调队列(不严格递减队列),也就是但较大值coming,维护队列单调性(双端队列)
- r要入窗口,要将queue尾部所有小于r的值弹出,才能入窗口
- l-1>=0代表当r入窗口,要出窗口的值
- 若出窗口的l-1的值为max(queue.peekFirst),那么队列移除最大值
- 当l==0,代表已经初始化窗口,可以开始收割结果queue.peekFirst
class Solution {
Deque<Integer> queue = new LinkedList<>();
public int[] maxSlidingWindow(int[] nums, int k) {
int r=0,l=1-k,n=nums.length;
int[] res = new int[n-k+1];
int index = 0;
for(int i=0;i<n;i++)
{
if(l-1>=0 && nums[l-1]==queue.peekFirst())
{
queue.pollFirst();
}
while(!queue.isEmpty() && nums[r]>queue.peekLast())
{
queue.pollLast();
}
queue.add(nums[r]);
if(l>=0)
{
res[index++] = queue.peek();
}
l++;
r++;
}
return res;
}
}
代码思路
- 二分查找的关键是确定可能区间,然后再是单调性
- 为什么这么说?题33. 搜索旋转排序数组
- 如何不断缩小值可能出现的范围,并且保证丢弃的范围是绝不可能存在目标值的,这才是二分的思想
- 每行的元素从左到右升序排列
- 每列的元素从上到下升序排列
- 利用单调性可以不断缩小搜索区间
- 初始化时,选择一个当前元素的中点,也就是nums[n-1][0],这是由于我们的策略时从左下到右上搜索,做到不遗漏
- 也可以从右上角到左下角进行搜索
- 具体步骤
- 当前搜索到的元素ele==target ,return
- 当前搜索到的元素ele>target ,到右边搜索,y++
- 当前搜索到的元素ele
class Solution {
public boolean searchMatrix(int[][] matrix, int target) {
int n = matrix.length,m=matrix[0].length;
int x = n-1,y=0;
while(x>=0 && y<m)
{
int cur = matrix[x][y];
if(cur==target) return true;
if(cur>target)
{
x--;
}else y++;
}
return false;
}
}
快速幂
- 一个十进制的数可以用二进制的数唯一表示,例如5 = 1*1+2*0+4*1
- 那么x^5 --> x^1 * x^4
- 那么要计算x^5,只需要计算log5次
class Solution {
public double myPow(double x, int n) {
long y = n;
double res = 1;
if(y<0)
{
y = -y;
x = 1/x;
}
while(y!=0)
{
if((y&1)==1) res*=x;
x*=x;
y = y>>1;
}
return res;
}
}
- 本质上是完全背包问题,如何使用物品1,4,9,16…装满容量为V的背包的最少选择
- bp[i]–> 表示装满背包容量为i的最少选择
- bp[i] = Min{bp[j],bp[j-v]+1} ;(j->[v,V])
class Solution {
public int numSquares(int n) {
if(n<0) return 0;
int[] bp = new int[n+1];
Arrays.fill(bp,10000);
bp[0]=0;
for(int i=1;i*i<=n;i++)
{
for(int j=i*i;j<=n;j++)
{
bp[j] = Math.min(bp[j],bp[j-i*i]+1);// 每次选择完i*i后,选择次数加一
}
}
return bp[n]==10000?0:bp[n];
}
}
双指针L,R
- L表示当前可被插入非零数组的位置,从零开始
- R遍历数组
- 当R遍历到非零的数字时,swap(R,L),同时L++
class Solution {
public void moveZeroes(int[] nums) {
int l=0;
for(int r=0;r<nums.length;r++)
{
if(nums[r]==0)
{
continue;
}
swap(nums,l,r);
l++;
}
}
void swap(int[] arr , int l,int r)
{
int t = arr[l];
arr[l] = arr[r];
arr[r] = t;
}
}
代码思路
- 反转确定
- 如果一个元素出现,那么将元素状态反转
- 当元素状态被反转两次,那么出现重复数字
class Solution {
public int findDuplicate(int[] nums) {
int n=nums.length;
boolean[] used = new boolean[n+1];
for(int num : nums)
{
used[num] = !used[num];
if(!used[num]) return num;
}
return -1;
}
}
动态规划
- bp[i]表示: 以下标i结尾的的数组,最长递增子序列的长度
- bp[n-1]: 表示以下标n-1结尾的的数组,最长递增子序列的长度,但是最长递增子序列不一定以n-1为结尾,所以不是答案
- bp[i] = Math{bp[0::j]}+1, 0<=j
- 初始化bp[0] = 1
class Solution {
public int lengthOfLIS(int[] nums) {
int n = nums.length;
int[] bp = new int[n];
bp[0] = 1;
int res=bp[0];
for (int i = 1; i < n; i++) {
int max = 0;
for (int j = 0; j < i; j++) {
if(nums[i]>nums[j]){
max = Math.max(max,bp[j]);
}
}
bp[i] = Math.max(max,bp[i])+1;
res = Math.max(res,bp[i]);
}
return res;
}
}
代码思路:
- 用set来存储当前可能正确的括号组合,遍历set获取所有正确的括号组合
- 若set没有出现正确的括号组合怎么办?
- 对set可能的括号组合–> 删除一个字符,形成新的括号组合
- 为了保证不漏
- 获取可能组合时,获取新的括号组合是获取每个位置都删除一个字符的所有可能
class Solution {
public List<String> removeInvalidParentheses(String s) {
List<String> ans = new ArrayList<String>();
Set<String> currSet = new HashSet<String>();
currSet.add(s);
while (true) {
for (String str : currSet) {
if (isValid(str)) {
ans.add(str);
}
}
if (ans.size() > 0) {
return ans;
}
Set<String> nextSet = new HashSet<String>();
for (String str : currSet) {
for (int i = 0; i < str.length(); i ++) {
if (str.charAt(i) == '(' || str.charAt(i) == ')') {
nextSet.add(str.substring(0, i) + str.substring(i + 1));
}
}
}
currSet = nextSet;
}
}
private boolean isValid(String str) {
char[] ss = str.toCharArray();
int count = 0;
for (char c : ss) {
if (c == '(') {
count++;
} else if (c == ')') {
count--;
if (count < 0) {
return false;
}
}
}
return count == 0;
}
}
完全背包
- coins表示物品,可以任意拿
- amount表示背包容量
- bp[i] = Min{bp[i],bp[i-coin]+1}
- 初始化bp[i]=MAX,bp[0]=0
class Solution {
public int coinChange(int[] coins, int amount) {
int[] bp = new int[amount+1];
Arrays.fill(bp,10000000);
bp[0]=0;
for(int coin : coins)
{
for(int i=coin;i<=amount;i++)
{
bp[i] = Math.min(bp[i],bp[i-coin]+1);
}
}
return bp[amount]==10000000?-1:bp[amount];
}
}
class Solution {
public int rob(TreeNode root) {
HashMap<TreeNode,Info> map = new HashMap<>();
return Math.max(p(root,map).get,p(root,map).no);
}
private Info p(TreeNode root,HashMap<TreeNode,Info> map) {
if(root==null) {
return new Info(0,0);
}
if(map.containsKey(root)){
return map.get(root);
}
Info info = new Info();
info.get = root.val+p(root.left,map).no+p(root.right,map).no;
info.no = Math.max(p(root.left,map).no,p(root.left,map).get)+Math.max(p(root.right,map).get,p(root.right,map).no);
map.put(root,info);
return info;
}
}
class Info{
int get;
int no;
public Info() {
}
public Info(int get, int no) {
this.get = get;
this.no = no;
}
}
对于任意整数 x,令 x=x&(x-1),该运算将 x的二进制表示的最后一个 1 变成 0。
class Solution {
public int[] countBits(int n) {
int[] bp = new int[n+1];
for(int i=1;i<=n;i++)
{
bp[i] = getCount(i);
}
return bp;
}
int getCount(int num)
{
int res = 0;
while(num!=0)
{
num = (num)&(num-1);
res++;
}
return res;
}
}
快排 + hashMap
- 通过hashMap将数组元素和出现次数统计后形成新的数组Node[]
- 使用快排,前k的高频元素指的就是下标位置为k-1的前面的数据
class Solution {
public int[] topKFrequent(int[] nums, int k) {
HashMap<Integer,Integer> dic = new HashMap<>();
for(int num : nums)
{
dic.put(num,dic.getOrDefault(num,0)+1);
}
int size = dic.size();
Node[] nodes = new Node[size];
int index = 0;
for(int key : dic.keySet())
{
nodes[index++] = new Node(key,dic.get(key));
}
// Arrays.sort(nodes,(a,b)->b.cnt-a.cnt);
int tar = k-1,l=0,r=size-1;
while(l<=r)
{
int cur = partition(nodes,l,r);
if(cur==tar) break;
else if(cur>tar)
{
r = cur-1;
}else{
l = cur+1;
}
}
int[] res = new int[k];
for(int i=0;i<k;i++)
{
res[i] = nodes[i].val;
}
return res;
}
int partition(Node[] nodes , int lo,int ro)
{
if(lo>ro) return -1;
Node divi=nodes[lo];
int l=lo,r=ro;
while(true)
{
while(l<=r && nodes[l].cnt>=divi.cnt) l++;
while(l<=r && nodes[r].cnt<divi.cnt) r--;
if(l>r) break;
swap(nodes,l,r);
}
swap(nodes,lo,r);
return r;
}
void swap(Node[] nodes , int l,int r)
{
Node temp = nodes[l];
nodes[l]=nodes[r];
nodes[r] = temp;
}
}
class Node{
int val;
int cnt;
public Node(int val,int cnt)
{
this.val = val;
this.cnt = cnt;
}
}
当然可以直接排序node[]数组
class Solution {
public int[] topKFrequent(int[] nums, int k) {
HashMap<Integer,Integer> dic = new HashMap<>();
for(int num : nums)
{
dic.put(num,dic.getOrDefault(num,0)+1);
}
int size = dic.size();
Node[] nodes = new Node[size];
int index = 0;
for(int key : dic.keySet())
{
nodes[index++] = new Node(key,dic.get(key));
}
Arrays.sort(nodes,(a,b)->b.cnt-a.cnt);
int[] res = new int[k];
for(int i=0;i<k;i++)
{
res[i] = nodes[i].val;
}
return res;
}
}
class Node{
int val;
int cnt;
public Node(int val,int cnt)
{
this.val = val;
this.cnt = cnt;
}
}
- 按照排序规则:先按照身高逆序排,身高相同按照位置正序排
- 排完之后,按照位置插入即可
class Solution {
public int[][] reconstructQueue(int[][] people) {
Arrays.sort(people,(arr,brr)->{
return arr[0]!=brr[0]?brr[0]-arr[0]:arr[1]-brr[1];
});
List<int[]> res = new LinkedList<>();
for(int[] arr : people)
{
res.add(arr[1],arr);
}
return res.toArray(new int[0][]);
}
}
01背包
- 假设可以分平为等量数据,那么sum一定是偶数
- 平分为俩分,一份占V,其实就是背包容量
- 尽量装满背包V,若背包被刚好装满,则可以被等量拆分
class Solution {
public boolean canPartition(int[] nums) {
int sum = Arrays.stream(nums).sum();
if(sum%2!=0) return false;
int tar = sum/2;
int[] bp = new int[tar+1];
for(int num : nums)
{
for(int i=tar;i>=num;i--)
{
bp[i] = Math.max(bp[i],bp[i-num]+num);
}
}
return bp[tar]==tar;
}
}
前序遍历
class Solution {
public int pathSum(TreeNode root, int targetSum) {
if(root==null) return 0;
return dsf(root,targetSum)+pathSum(root.left,targetSum)+ pathSum(root.right,targetSum);
}
int dsf(TreeNode root,long targetSum)
{
int res=0;
if(root==null)
{
return 0;
}
targetSum-=root.val;
if(targetSum==0)
{
res++;
}
res += dsf(root.left,targetSum);
res += dsf(root.right,targetSum);
targetSum+=root.val;
return res;
}
}
字母异位词,可以有一个唯一的按照字母序排列的字符串A与之对应
- 通过数组获取每个字符串的唯一对应的A,作为key,存储在hashMap中
- 遍历map.values
class Solution {
List<Integer> res = new ArrayList<>();
public List<Integer> findAnagrams(String s, String p) {
if(p.length()==0 || s.length()==0 || p.length()>s.length()) return res;
int[] tar = new int[26];
int[] window = new int[26];
int pl = p.length();
int sl = s.length();
for(int i=0;i<pl;i++)
{
tar[p.charAt(i)-'a']++;
window[s.charAt(i)-'a']++;
}
if(Arrays.equals(tar,window)) res.add(0);
for(int i=1;i<=sl-pl;i++)
{
// i-1 out
window[s.charAt(i-1)-'a']--;
// i+1 in
window[s.charAt(i+pl-1)-'a']++;
if(Arrays.equals(tar,window)) res.add(i);
}
return res;
}
}
策略
- 将数组下标作为元素的映射,例如num–>num-1的数组下标上
- 每次找到一个num,将对应数组下标的值取负数,意味着该数组单元的值找到
- 遍历完成,只需要取出元素的值等于0的下标值+1就是消失的数字
class Solution {
List<Integer> res = new ArrayList<>();
public List<Integer> findDisappearedNumbers(int[] nums) {
int n = nums.length;
for(int i=0;i<nums.length;i++)
{
int find = Math.abs(nums[i]);
nums[find-1] = -Math.abs(nums[find-1]);
}
for(int i=0;i<n;i++)
{
if(nums[i]>=0) res.add(i+1);
}
return res;
}
}
取出数字2进制位置上的最后一位
每次加上两两异或的结果(不同为1,相同为0)
class Solution {
public int hammingDistance(int x, int y) {
int ans=0;
while(x!= 0|| y!=0)
{
int xa = (x&1),ya=(y&1);
ans += (xa^ya)==1?1:0;
x = x>>1;
y = y>>1;
}
return ans;
}
}
- 和为sum,假设正数和为left,负数和为right(不考虑符号)
- sum = left+right
- left - right = k
- 可以的出,left = (sum+k)/2
- 那么实际上求的是,在nums元素中取,构成left的组合数
- 01背包求组合数问题
- bp[j] += bp[j-nums[i]];
- 初始化bp[0]=1
- 当(sum+k)不能被2整除,代表不能构成left,返回0
class Solution {
public int findTargetSumWays(int[] nums, int target) {
int sum = Arrays.stream(nums).sum();
int bag = (sum+target)/2;
if((sum+target)%2!=0) return 0;
if(bag<0) bag = -bag;
int[] bp = new int[bag+1];
bp[0]=1;
for(int i=0;i<nums.length;i++)
{
for(int j=bag;j>=nums[i];j--)
{
bp[j] += bp[j-nums[i]];
}
}
return bp[bag];
}
}
class Solution {
int ans = 0;
public int diameterOfBinaryTree(TreeNode root) {
int h = dsf(root);
return ans;
}
private int dsf(TreeNode root) {
if(root==null){
return 0;
}
int left = dsf(root.left);
int right = dsf(root.right);
int max = Math.max(left, right);
ans = Math.max(ans,left+right);
return max+1;
}
}
前缀和 + hashMap
- 搞定清楚hashMap中存储的是什么?
- 本道题,hashMap存储的是【和为key的子数组的个数】
- 前缀和:sum[i::j] = sum[j+1]-sum[i]
class Solution {
public int subarraySum(int[] nums, int k) {
// dic : how many sum coming times
Map<Integer,Integer> dic = new HashMap<>();
int n = nums.length;
int sum=0;
dic.put(0,1);// 表示前缀和为0的子数组的个数现在有一个
int ans=0;
for(int num : nums)
{
sum += num;
int tar = sum-k;
ans += dic.getOrDefault(tar,0); // 查看是否存在tar,能够使得当前前缀和sum-tar==k
dic.put(sum,dic.getOrDefault(sum,0)+1);// 维护dic
}
return ans;
}
}
拷贝排序后,记录出现不一致位置的最大值和最小值
class Solution {
public int findUnsortedSubarray(int[] nums) {
int n = nums.length;
int[] cp = Arrays.copyOf(nums,n);
Arrays.sort(nums);
int res = 0;
int l=n,r=-1;
for(int i=0;i<n;i++)
{
if(nums[i]!=cp[i]) {
l = Math.min(l,i);
r = Math.max(r,i);
}
}
if(l==n || r==-1) return 0;
return r-l+1;
}
}
区间dp
当s[i]==s[j] dp[i][j] = dp[i+1][j-1]
class Solution {
public int countSubstrings(String s) {
int n = s.length();
boolean[][] bp = new boolean[n+1][n+1];
bp[0][0] = true;
int res=1;
for(int len=1;len<=n;len++)
{
for(int i=0;i<=n-len;i++)
{
int j = i+len-1;
if(s.charAt(i)==s.charAt(j))
{
if(len<=3) bp[i][j] = true;
else bp[i][j] = bp[i+1][j-1];
if(bp[i][j]) res++;
}
}
}
return res-1;
}
}
单调栈:
- 单调递减的栈(不严格)
- 遇到高温度H,将可以确定的日期弹出即可
- 可以确定的日期指的是,温度值小于H的值
class Solution {
public int[] dailyTemperatures(int[] temperatures) {
// deal when the bigger ele coming
Stack<Integer> stack = new Stack<>();
int n = temperatures.length;
int[] res = new int[n];
for(int i=0;i<n;i++)
{
if(stack.isEmpty())
{
stack.add(i);
continue;
}
while(!stack.isEmpty() && temperatures[i]>temperatures[stack.peek()])
{
int pre = stack.pop();
res[pre] = i - pre;
}
stack.add(i);
}
return res;
}
}