文章目录
一. 二分查找
一般而言,防止溢出 mid = l + (r-l)/2;其次 为了防止死循环 mid = (l+r+1)/2 left < right 还是 left <= right0. 二段性:模板题
1. 山峰数组的顶部
- 求左边红色区域
条件:arr[i]>arr[i-1]
class Solution {
public int peakIndexInMountainArray(int[] arr) {
int l = 0;
int r = arr.length - 1;
while(l<r){
int mid = (l+r+1)/2;
if(arr[mid]>arr[mid-1]){
l = mid;
}else{
r = mid - 1;
}
}
return l;
}
}
- 求右边绿色区域
class Solution {
public int peakIndexInMountainArray(int[] arr) {
int l = 0;
int r = arr.length - 1;
while(l<r){
int mid = (l+r)/2;
if(arr[mid]>arr[mid+1]){
r = mid;
}else{
l = mid + 1;
}
}
return l;
}
}
2. 狒狒吃香蕉
添加链接描述
求的是满足条件的最小值,绿色区域
class Solution {
public int minEatingSpeed(int[] piles, int h) {
Arrays.sort(piles);
int len = piles.length;
if(len==h) return piles[len-1];
int left = 1;
int right = piles[len-1];
//求的是左边界 k最小
while(left<right){
int mid = (left+right)>>1;
if(check(piles,mid,h)){
right = mid;
}else{
left = mid + 1;
}
}
return left;
}
public boolean check(int[] piles,int mid,int h){
int sum = 0;
for(int num:piles){
sum += (num-1)/mid + 1;
}
if(sum<=h) return true;
else return false;
}
}
3. 按权重生成随机数
前缀和 + 二分
题意比较难懂,暴力法也可以写,时间复杂度太大
- 二分
class Solution {
int[] prev;
int size;
private final static Random RND = new Random();
public Solution(int[] w) {
prev = new int[w.length];
prev[0] = w[0];
for(int i=1;i<prev.length;i++){
prev[i] = prev[i-1] + w[i];
}
this.size = prev[w.length-1];
}
public int pickIndex() {
int num = RND.nextInt(size) + 1;
return binarySearch(num);
}
//右边界
public int binarySearch(int num){
int l = 0;
int r = prev.length-1;
while(l<r){
int mid = (r-l)/2 + l;
if(prev[mid]>=num){
r = mid;
}else{
l = mid +1;
}
}
return l;
}
}
/**
* Your Solution object will be instantiated and called as such:
* Solution obj = new Solution(w);
* int param_1 = obj.pickIndex();
*/
- 暴力法
class Solution {
private int[] w;
private int size = 0;
private static final Random RND = new Random();
public Solution(int[] w) {
this.w = w;
for(int num: w){
size += num;//求和
}
}
public int pickIndex() {
int base = RND.nextInt(size);
int sum = 0;
for(int i=0;i<size;i++){
sum += w[i];
if(sum>base){
return i;
}
}
return 0;
}
}
/**
* Your Solution object will be instantiated and called as such:
* Solution obj = new Solution(w);
* int param_1 = obj.pickIndex();
*/
4. 查找插入位置
添加链接描述
分析: 难点在于不存在时,返回什么;
首先,while判断条件必须取等号,left和right可能在相等时,取到target;
其次,若找不到结果,必然会进入left==right循环,mid,left,right三者相等。如果nums[mid]>target,即target会在mid的下一位(mid+1);nums[mid]<target,即target会在mid的位置(mid)。故返回left
class Solution {
public int searchInsert(int[] nums, int target) {
int left = 0;
int right = nums.length - 1;
while(left<=right){
int mid = (right-left)/2+left;
if(nums[mid]==target){
return mid;
}else if(nums[mid]>target){
right = mid-1;
}else if(nums[mid]<target){
left = mid + 1;
}
}
return left;
}
}
5. 求平方根
- 解法一:
变成二段性问题处理,分类讨论,缩小范围至x/2,避免溢出
class Solution {
public int mySqrt(int x) {
if(x<=1) return x;
if(x<=3) return 1;
if(x==4) return 2;
int l = 1;
int r = x/2;//由数学知识得当x>4时,x/2的平方一定比x大
while(l<r){//求左区间
int mid = (l+r+1)/2;
if((long)mid*mid<=x){
l = mid;
}else if((long)mid*mid>x){
r = mid-1;
}
}
return l;
}
}
- 解法二:
class Solution {
public int mySqrt(int x) {
if(x==0) return 0;
int l = 1;
int r = x;
while(l<r){
int mid = l+(r-l)/2+1;//必须写成减 避免溢出
if((long)mid*mid>x){
r = mid - 1;
}else if((long)mid*mid<x){
l = mid ;
}else{
return mid;
}
}
return l;
}
}
- 解法三:牛顿迭代
class Solution {
public int mySqrt(int x) {
if(x==0) return 0;
double x0=x,C=x;
while(true){
double xi = 0.5 * (x0+C/x0);
if(Math.abs(xi-x0)<1e-7){
break;
}
x0 = xi;
}
return (int)x0;
}
}
6. 在排序数组中查找数字 I
- 解法1:1次二分查找
class Solution {
public int search(int[] nums, int target) {
int count = 0;
int start = binarySearch(nums,target);
while(start<nums.length&&nums[start++]==target){
count++;
}
return count;
}
public int binarySearch(int[] nums,int target){
//二段性右边界
int left = 0;
int right = nums.length-1;
while(left<right){
int mid = (left+right)/2;
if(nums[mid]>=target){
right = mid;
}else{
left = mid + 1;
}
}
return left;
}
}
- 解法2:2次二分查找
class Solution {
public int search(int[] nums, int target) {
if(nums.length==0) return 0;
int start = binarySearch(nums,target);
int end = binarySearch(nums,target+1);
if(end==nums.length-1&&nums[end]==target){
return end-start+1;
}else{
return end-start;
}
}
public int binarySearch(int[] nums,int target){
//二段性右边界
int left = 0;
int right = nums.length-1;
while(left<right){
int mid = (left+right)/2;
if(nums[mid]>=target){
right = mid;
}else{
left = mid + 1;
}
}
return left;
}
}
7. 0~n-1中缺失的数字
class Solution {
//二分法
//考虑缺少n的情况
public int missingNumber(int[] nums) {
int left = 0;
int right = nums.length-1;
while(left<right){
int mid = (left+right) >> 1;
if(nums[mid]!=mid){
right = mid ;
}else{
left = mid+1;
}
}
return left == nums.length-1&&nums[left]==left?left+1:left;
}
}
二. 整数
1. 模拟技巧类
整数除法
- 解法思路一: 减数倍增法模拟
考虑特殊情况,最大值或最小值
class Solution {
public int divide(int a, int b) {
//设a/b=ans
//当a取最小
if(a == Integer.MIN_VALUE){
if(b==1){
return Integer.MIN_VALUE;
}
if(b == -1){
return Integer.MAX_VALUE;
}
}
//当b最小
if(b == Integer.MIN_VALUE){
return a == Integer.MIN_VALUE ? 1 : 0;
}
//当a为0
if(a == 0){
return 0;
}
//设a/b=ans
//把正数变负数(处理),负数变正数会越界
boolean flag = true;
if(a>0){
a=-a;
flag = !flag;
}
if(b>0){
b = -b;
flag = !flag;
}
//进行处理:a/b = ans
int res = 0;
while(a<=b){
int base = 1;//b扩大的倍数
int val = b;//b扩大后的值
while(val >= Integer.MIN_VALUE >> 1 && a <= val<<1 ){//防止val + 满足a的条件
base += base;//b翻倍的倍数
val += val;//值翻倍
}
res += base;
a -= val;//a经过一轮循环后得到的值
}
return flag?res:-res;
}
}
2. 位运算相关
二进制加法
添加链接描述
竖式运算,从字符串的最后一位加法开始计算,保留进位carry
小技巧:异或运算 ^ 两个为1,结果为0;否则为1
与运算 & 两个为1,结果才会为1
class Solution {
//利用与&和异或^运算
public String addBinary(String a, String b) {
int lenA = a.length();
int lenB = b.length();
StringBuilder sb = new StringBuilder();
int n = Math.max(lenA,lenB);//求最大长度
int temp=0,carry=0;
for(int i=0;i<n;i++){
int x = i<lenA ? (a.charAt(lenA-i-1)-'0') : 0;
int y = i<lenB ? (b.charAt(lenB-i-1)-'0') : 0;
temp = x ^ y ^ carry;//没有进位的结果
if(x == 0 || y==0){
carry = (x^y) & carry;
}else{
carry = 1;//两个数为1
}
sb.append( (char) (temp + '0'));
}
if(carry>0){
sb.append('1');
}
return sb.reverse().toString();//反转 转字符串
}
}
前 n 个数字二进制中 1 的个数
class Solution {
public int[] countBits(int n) {
int[] res = new int[n+1];
for(int i=0;i<=n;i++){
res[i] = countOne(i);
}
return res;
}
public int countOne(int num){
int count = 0;
while(num>0){
num = (num-1) & num;
count++;
}
return count;
}
}
只出现一次的数字
添加链接描述
利用位运算实现十进制和二进制的转换
class Solution {
public int singleNumber(int[] nums) {
//32位数组
int[] bitsmap = new int[32];
//每个数变为二进制,范围为32位
for(int num:nums){
for(int i=0;i<32;i++){
if( ( (num >>i ) & 1 )== 1){
bitsmap[i]++;
}
}
}
//对每个值对3取模,要么是0 要么是1
int res =0;
for(int i=0;i<32;i++){
if(bitsmap[i]%3 == 1){
res += (1<<i);//二进制变为十进制
}
}
return res;
}
}
单词长度的最大乘积
添加链接描述
比较字符串是否含有相同元素的技巧:位运算
用十进制数组,用 | 或运算,设置对应的二进制的位数为1.记录保留数组
然后再将两个数进行 与&运算,只有全部不相同才会出现1
class Solution {
public int maxProduct(String[] words) {
int res = 0;
int len = words.length;
int[] nums = new int[len];
int index = 0;
//把字符串变成二进制,比如 acd == 1101
for(String word:words){
for(int i=0;i<word.length();i++){
char c = word.charAt(i);
nums[index] = nums[index] | (1 << (c-'a'));
}
index++;
}
//位与运算:两个互不相同意味着不可能出现两个1,也就是结果必须为0
for(int i=0;i<len;i++){
for(int j=i;j<len;j++){
if( (nums[i] & nums[j]) == 0){
res = Math.max(words[i].length()*words[j].length(),res);
}
}
}
return res;
}
}
3. 大数的加减乘除
大数相加
class Solution {
//竖式运算
public String addStrings(String num1, String num2) {
StringBuilder res = new StringBuilder();
int i = num1.length()-1;
int j = num2.length()-1;
int carry = 0;//进位
while(i >= 0 || j>=0){
//i或者j存在就取其值 不在就为0
int n1 = i>=0 ? num1.charAt(i) - '0' : 0;
int n2 = j>=0 ? num2.charAt(j) - '0' : 0;
int temp = n1 + n2 + carry;
carry = temp / 10;
res.append(temp % 10);
i--;
j--;
}
//最后一位是否有仅为
if(carry == 1){res.append(1);}
return res.reverse().toString();
}
}
大数相减
- 保证大数减小数
- 减法不同于加法需要借位
num=n1-n2-borrow,若出现num为负数就说明需要借位,num+10,borrow=1 - 最终结果可能有前置0,需要去掉,特殊情况两数相等不能去掉全部0
/**
* 大数相减
*/
public class Solution17 {
public static void main(String[] args) {
String num1 = "120";
String num2 = "220";
System.out.println(substractString(num1,num2));
}
public static String substractString(String num1,String num2){
char sign = '+';
//保证大数减小数
if(!compare(num1,num2)){
sign = '-';
String temp = num2;
num2 = num1;
num1 = temp;
}
int len1 = num1.length()-1;
int len2 = num2.length()-1;
int borrow = 0;//借位
StringBuilder res = new StringBuilder();
while(len1>=0||len2>=0){
int n1 = len1>=0 ? num1.charAt(len1)-'0' : 0;
int n2 = len2>=0 ? num2.charAt(len2)-'0' : 0;
int num = n1-n2-borrow;
if(num<0){//需要借位
borrow = 1;
num += 10;
}
res.append(num);
len1--;
len2--;
}
res.reverse().toString();//反向
//去前置0
int index = 0;
while(index<res.length()&&res.charAt(index)=='0'){
index++;
}
//特殊情况:结果为0
if(index==res.length()){
return "0";
}
if(sign=='+'){
return res.substring(index);
}else{
return sign+res.substring(index);
}
}
public static boolean compare(String num1,String num2){
int len1 = num1.length();
int len2= num2.length();
if(len1<len2){
return false;
}else if(len1>len2){
return true;
}else{
return num1.compareTo(num2)>0;//字符串形式的数字大小比较
}
}
}
大数相乘
- 解法一
- m位数*n位数最多m+n位,开一个数组保留相乘的结果
- 依次处理相乘后的结果
- 去掉前置0
class Solution {
public String multiply(String num1, String num2) {
if(num1.equals("0") || num2.equals("0")) return "0";
char[] c1 = num1.toCharArray();
char[] c2 = num2.toCharArray();
int len1 = num1.length();
int len2 = num2.length();
int[] value = new int[len1+len2];//开一个数组保存结果
//计算第一次结果
for(int i=len1-1;i>=0;i--){
for(int j=len2-1;j>=0;j--){
int num = (c1[i]-'0')*(c2[j]-'0');
value[len1-i-1+len2-j-1] += num;
}
}
//处理进位
for(int i=0;i<value.length-1;i++){
value[i+1] += value[i]/10;
value[i] = value[i]%10;
}
//去掉前置0
int index = value.length-1;
while(value[index]==0){
index--;
}
StringBuilder res = new StringBuilder();
while(index>=0){
res.append(value[index]);
index--;
}
return res.toString();
}
}
大数相除
处理起来很麻烦,且除法不能整除,模拟除法可以参考下题
添加链接描述
三. 数组
1. 双指针法
数组中和为 0 的三个数 (三数之和)
添加链接描述
双指针,去重
class Solution {
public List<List<Integer>> threeSum(int[] nums) {
List<List<Integer>> res = new ArrayList<>();
Arrays.sort(nums);
for(int i=0;i<nums.length;i++){
if(i>=1 && nums[i] == nums[i-1]) continue;//去重
int left = i+1;
int right = nums.length - 1;
while(left<right){
int sum = nums[i] + nums[left] + nums[right];
if(sum == 0){
res.add(Arrays.asList(nums[i],nums[left],nums[right]));
while(left<right && nums[left] == nums[left+1]) {left++;}
while(left<right && nums[right] == nums[right-1]){right--;}
left++;
right--;
}else if(sum>0){
right--;
}else if(sum<0){
left++;
}
}
}
return res;
}
}
和为s的两个数字
添加链接描述
两数之和升级版,数组排序好,且答案不唯一
在这里插入代码片
移动零
class Solution {
public void moveZeroes(int[] nums) {
if(nums.length==1) return;
int pointer = 0;
for(int i=0;i<nums.length;i++){
if(nums[i]!=0){
int temp = nums[i];
nums[i] = nums[pointer];
nums[pointer] = temp;
pointer++;
}
}
return;
}
}
盛最多水的容器
class Solution {
public int maxArea(int[] height) {
int i = 0;
int j = height.length - 1;
int res = 0;
while(i<j){
res = height[i]<height[j] ? Math.max(res,(j-i)*height[i++]) :
Math.max(res,(j-i)*height[j--]);
}
return res;
}
}
2. 滑动窗口
和大于等于 target 的最短子数组 (最小滑动窗口)
添加链接描述
方法一:最小滑动窗口,时间复杂度o(n)
class Solution {
public int minSubArrayLen(int target, int[] nums) {
int len = nums.length;
int left = 0;
int right = 0;
int sum = 0;
int res = Integer.MAX_VALUE;
while(right<len){
sum += nums[right];
while(sum>=target){
res = Math.min(res,right-left+1);
sum -= nums[left];
left++;
}
right++;
}
return res == Integer.MAX_VALUE ? 0:res;
}
}
方法二:前缀和+二分,时间复杂度o(nlogn)
乘积小于 K 的子数组 (最小滑动窗口)
添加链接描述
最小滑窗:当满足条件左边压缩
区别于其他的点,在于如何统计数目,固定右端点,开始计数
class Solution {
public int numSubarrayProductLessThanK(int[] nums, int k) {
if(k==0||k==1) return 0;
int left = 0;
int right = 0;
int len = nums.length;
int res = 0;
int sum = 1;
while(right<len){
sum = sum*nums[right];
while(sum>=k){
sum /= nums[left];
left += 1;
}
res += right-left+1;//固定右边 有多少个符合条件的数组
right += 1;
}
return res;
}
}
值和下标之差都在给定的范围内 (小于某长度的滑窗)
3. 前缀和
和为k的子数组
添加链接描述
区间和,然后求差 : 前缀和+哈希优化(二数之和的思想)
class Solution {
public int subarraySum(int[] nums, int k) {
int[] prev = new int[nums.length+1];
prev[0] = 0;
for(int i=1;i<nums.length+1;i++){
prev[i] = prev[i-1] + nums[i-1];
}
int res = 0;
Map<Integer,Integer> map = new HashMap<>();
for(int i=0;i<nums.length+1;i++){
if(map.containsKey(prev[i]-k)){
res += map.get(prev[i]-k);
}
map.put(prev[i],map.getOrDefault(prev[i],0)+1);
}
return res;
}
}
0和1个数相同的子数组
添加链接描述
把0变成-1后,题目转化为求和为0的子数组: 前缀和+哈希表优化
- 如何求长度:
求的是最长长度,因此只需要保存第一次出现的索引。
class Solution {
public int findMaxLength(int[] nums) {
for(int i=0;i<nums.length;i++){
if(nums[i]==0){
nums[i] = -1;
}
}
int res = 0;
int[] prev = new int[nums.length+1];
prev[0] = 0;
for(int i=1;i<nums.length+1;i++){
prev[i] = prev[i-1] + nums[i-1];
}
Map<Integer,Integer> map = new HashMap<>();
map.put(0,0);
for(int i=1;i<nums.length+1;i++){
if(map.containsKey(prev[i])){
res = Math.max(res,i-map.get(prev[i]));
}else{
map.put(prev[i],i);
}
}
return res;
}
}
左右两边子数组的和相等
添加链接描述
较简单,前缀和
class Solution {
public int pivotIndex(int[] nums) {
int len = nums.length;
int[] prev = new int[len+1];
prev[0] = 0;
for(int i=1;i<len+1;i++){
prev[i] = nums[i-1] + prev[i-1];
}
int sum = 0;//数组和
for(int i=0;i<len;i++){
sum += nums[i];
}
for(int i=0;i<len;i++){
if(prev[i]*2+nums[i] == sum){
return i;
}
}
return -1;
}
}
二维子矩阵的和
添加链接描述
对每一行分别求前缀和即可
class NumMatrix {
int[][] preSum;
public NumMatrix(int[][] matrix) {
int m = matrix.length;
int n = matrix[0].length;
preSum = new int[m][n+1];//i表示行数,
for(int i=0;i<m;i++){
Arrays.fill(preSum[i],0);
}
for(int i=0;i<m;i++){
for(int j=0;j<n;j++){
preSum[i][j+1] = preSum[i][j] + matrix[i][j];
}
}
}
public int sumRegion(int row1, int col1, int row2, int col2) {
int res = 0;
for(int i=row1;i<=row2;i++){
res += (preSum[i][col2+1]- preSum[i][col1]);
}
return res;
}
}
/**
* Your NumMatrix object will be instantiated and called as such:
* NumMatrix obj = new NumMatrix(matrix);
* int param_1 = obj.sumRegion(row1,col1,row2,col2);
*/
4. 区间问题
合并区间
class Solution {
public int[][] merge(int[][] intervals) {
List<int[]> res = new ArrayList<>();
Arrays.sort(intervals,(a,b)->(a[0]-b[0]));
int left = intervals[0][0];
int right = intervals[0][1];
for(int i=1;i<intervals.length;i++){
if(intervals[i][0]>intervals[i-1][1]){//大于前一个右边界,无重叠
res.add(new int[]{left,right});
left = intervals[i][0];
right = intervals[i][1];
}else{
intervals[i][1] = Math.max(intervals[i-1][1],intervals[i][1]);
right = intervals[i][1];
}
}
res.add(new int[]{left,right});
return res.toArray(new int[res.size()][2]);
}
}
5. 子串问题
最小覆盖子串
三. 字符串
1. 变位词 (固定大小滑窗)
固定大小的滑动窗口+两个map
- 假设只有小写字母的情况下,先创建两个数组,记录子串和目标串(对应子串长度)的所有字符,进行判断;
- 进行滑窗,同时更新数组对应的左右边界,判断两个数组是否相等。滑窗开始,右指针指向目标串的第i个字符(i为字串的长度)
字符串中的变位词
class Solution {
//固定大小的滑动窗口
public boolean checkInclusion(String s1, String s2) {
int[] map1 = new int[26];
int[] map2 = new int[26];
int len1 = s1.length();
int len2 = s2.length();
if(len1>len2) return false;
for(int i=0;i<len1;i++){
map1[s1.charAt(i)-'a']++;
map2[s2.charAt(i)-'a']++;
}
if(Arrays.equals(map1,map2)){
return true;
}
int left = 0;
int right = len1;
while(right<len2){
map2[s2.charAt(left)-'a']--;
map2[s2.charAt(right)-'a']++;
if(Arrays.equals(map1,map2)){
return true;
}
left++;
right++;
}
return false;
}
}
字符串中的所有变位词
class Solution {
public List<Integer> findAnagrams(String s, String p) {
List<Integer> res = new ArrayList<>();
int[] map1 = new int[26];
int[] map2 = new int[26];
int len1 = s.length();
int len2 = p.length();
if(len1<len2) return res;
for(int i=0;i<len2;i++){
map1[s.charAt(i)-'a']++;
map2[p.charAt(i)-'a']++;
}
if(Arrays.equals(map1,map2)){
res.add(0);
}
int left = 0;
int right = len2;
while(right<len1){
map1[s.charAt(left)-'a']--;
map1[s.charAt(right)-'a']++;
left++;
right++;
if(Arrays.equals(map1,map2)){
res.add(left);
}
}
return res;
}
}
2. 最长/短字符串 (最大/小滑窗)
最大滑动窗口
不含重复字符的最长子字符串
class Solution {
public int lengthOfLongestSubstring(String s) {
int left = 0;
int right = 0;
int len = s.length();
Set<Character> set = new HashSet<>();
int res = Integer.MIN_VALUE;
while(right<len){
char c = s.charAt(right);
while(set.contains(c)){//不满足条件,更新左边界
set.remove(s.charAt(left));
left++;
}
set.add(s.charAt(right));
res = Math.max(res,right-left+1);
right++;
}
return res==Integer.MIN_VALUE ? 0 : res;
}
}
最小滑动窗口
含有所有字符的最短字符串 ***
添加链接描述
大写小写字母同时出现,初始化数组大小为150,char - ‘A’
变量need记录所需要的字符个数,当字符个数等于0时,满足条件,开始更新
class Solution {
public String minWindow(String s, String t) {
int m = s.length();
int n = t.length();
int need = n;//记录需要的字符数
if(m<n) return "";
int[] cnt = new int[150];//记录每个字母出现的频率,大于0,表明在t出现
for(int i=0;i<t.length();i++){
cnt[t.charAt(i)-'A']++;
}
int left = 0;
int right = 0;
int minLen = Integer.MAX_VALUE;
int start = 0;
int end = -1;
while(right<m){
char c = s.charAt(right);
if(cnt[c-'A']>0){//表明c是所需要的
need--;
}
cnt[c-'A']--;//对应字符频率减1
while(need==0){//满足了要求
if(right-left+1<minLen){//长度满足要求
minLen = right-left+1;
start = left;
end = right;
}
char out = s.charAt(left);//需要滑出的字符
if(cnt[out-'A']>=0){//表明字符对于target需要
need++;
}
cnt[out-'A']++;
left++;
}
right++;
}
return s.substring(start,end+1);
}
}
3. 回文字串
有效的回文
添加链接描述
双指针判断 + 字符串处理常用API
- 判断字符和数字:
- 转小写字母
class Solution {
public boolean isPalindrome(String s) {
//去空格
StringBuilder sb = new StringBuilder();
for(int i=0;i<s.length();i++){
char c = s.charAt(i);
if(Character.isLetterOrDigit(c)){
sb.append(c);
}
}
String target = sb.toString();
//判断回文串
int left = 0;
int right = target.length() -1;
while(left<right){
char c1 = target.charAt(left);
char c2 = target.charAt(right);
if(Character.toLowerCase(c1) != Character.toLowerCase(c2)){
return false;
}else{
left++;
right--;
}
}
return true;
}
}
最多删除一个字符得到回文
添加链接描述
双指针+递归(贪心策略)
如果字符相同,下一轮;字符不同,则删除左或者右一个字符,进行判断
class Solution {
public boolean validPalindrome(String s) {
int left = 0;
int right = s.length() - 1;
while(left<right){
if(s.charAt(left)==s.charAt(right)){
left++;
right--;
}else{
return isValid(s,left+1,right) || isValid(s,left,right-1);
}
}
return true;
}
public boolean isValid(String s,int left,int right){
while(left<right){
if(s.charAt(left)==s.charAt(right)){
left++;
right--;
}else{
return false;
}
}
return true;
}
}
回文子字符串的个数
添加链接描述
动态规划求回文串
class Solution {
public int countSubstrings(String s) {
int len = s.length();
boolean[][] dp = new boolean[len][len];
for(int i=len-1;i>=0;i--){
for(int j=i;j<len;j++){
if(s.charAt(i) == s.charAt(j)){
if(j-i<=1){
dp[i][j] = true;
}else{
dp[i][j] = dp[i+1][j-1];
}
}
}
}
int res = 0;
for(int i=0;i<len;i++){
for(int j=0;j<len;j++){
if(dp[i][j]){
res++;
}
}
}
return res;
}
}
4. 字符串与数值
表示数值的字符串
添加链接描述
难点在于总结出各种情况
class Solution {
public boolean isNumber(String s) {
if(s ==null || s.length()==0) return false;
//去掉首尾空格
s = s.trim();
//1. e或E:没有出现e,并且出现过数字,且e后面也要有数字
//2. +- 第一位或者紧接e后哦面
//3. . 需要没出现过,且没出现过e
//4.必须出现数字
boolean numFlag = false;
boolean dotFlag = false;
boolean eFlag = false;
for(int i=0;i<s.length();i++){
char c = s.charAt(i);
if(c>='0'&&c<='9'){
numFlag = true;//出现过数字
}else if(c=='.'&&!dotFlag&&!eFlag){
dotFlag = true;
}else if( (c=='e'||c=='E') && !eFlag && numFlag){
eFlag = true;
numFlag = false;
}else if ((c=='+'||c=='-') && (i==0||s.charAt(i-1)=='e'||s.charAt(i-1)=='E')){
continue;
}else{
return false;
}
}
return numFlag;//必须出现一个数字
}
}
把字符串变为整数
四. 链表
1. 带环链表
链表中环的入口节点
/**
* Definition for singly-linked list.
* class ListNode {
* int val;
* ListNode next;
* ListNode(int x) {
* val = x;
* next = null;
* }
* }
*/
//快慢指针
public class Solution {
public ListNode detectCycle(ListNode head) {
ListNode slow = head;
ListNode fast = head;
while(fast!=null && fast.next != null){
slow = slow.next;
fast = fast.next.next;
if(slow == fast){
ListNode cur = slow;
ListNode prev = head;
while(cur!=prev){
cur = cur.next;
prev = prev.next;
}
return cur;
}
}
return null;
}
}
2. 删除节点
删除链表节点
删除链表的倒数第n个结点
/**
* Definition for singly-linked list.
* public class ListNode {
* int val;
* ListNode next;
* ListNode() {}
* ListNode(int val) { this.val = val; }
* ListNode(int val, ListNode next) { this.val = val; this.next = next; }
* }
*/
class Solution {
public ListNode removeNthFromEnd(ListNode head, int n) {
ListNode dummy = new ListNode(-1);
dummy.next = head;
int len = 0;
ListNode cur = head;
while(cur!=null){
cur = cur.next;
len++;
}
int target = len - n;
cur = dummy;
for(int i=0;i<len-n;i++){
cur = cur.next;
}
cur.next = cur.next.next;
return dummy.next;
}
}
3. 相交链表
两个链表的第一个重合节点
/**
* Definition for singly-linked list.
* public class ListNode {
* int val;
* ListNode next;
* ListNode(int x) {
* val = x;
* next = null;
* }
* }
*/
public class Solution {
//相交:a+c+b = b+c+a
//不相交:a+b = b+a
public ListNode getIntersectionNode(ListNode headA, ListNode headB) {
if(headA==null||headB==null) return null;
ListNode pointA = headA;
ListNode pointB = headB;
while(pointA!=pointB){
pointA = pointA == null? headB:pointA.next;
pointB = pointB == null ? headA:pointB.next;
}
return pointA;
}
}
4. 合并链表
合并两个有序链表
- 方法一:虚拟结点
/**
- Definition for singly-linked list.
- public class ListNode {
- int val;
- ListNode next;
- ListNode() {}
- ListNode(int val) { this.val = val; }
- ListNode(int val, ListNode next) { this.val = val; this.next = next; }
- }
*/
class Solution {
public ListNode mergeTwoLists(ListNode list1, ListNode list2) {
ListNode dummy = new ListNode(-1);
ListNode cur = dummy;
ListNode p1 = list1;
ListNode p2 = list2;
while(p1!=null&&p2!=null){
if(p1.val>p2.val){
cur.next = p2;
p2 = p2.next;
cur = cur.next;
}else{
cur.next = p1;
p1 = p1.next;
cur = cur.next;
}
}
cur.next = p1==null?p2:p1;
return dummy.next;
}
}
- 递归法
class Solution {
public ListNode mergeTwoLists(ListNode list1, ListNode list2) {
if(list1==null){
return list2;
}else if(list2==null){
return list1;
}else if(list1.val<list2.val){
list1.next = mergeTwoLists(list1.next,list2);
return list1;
}else{
list2.next = mergeTwoLists(list1,list2.next);
return list2;
}
}
}
合并 K 个升序链表
- 暴力法
/**
* Definition for singly-linked list.
* public class ListNode {
* int val;
* ListNode next;
* ListNode() {}
* ListNode(int val) { this.val = val; }
* ListNode(int val, ListNode next) { this.val = val; this.next = next; }
* }
*/
class Solution {
//合并两个
public ListNode mergeKLists(ListNode[] lists) {
ListNode res = null;
for(int i = 0;i<lists.length;i++){
res = mergeTwoList(res,lists[i]);
}
return res;
}
public ListNode mergeTwoList(ListNode a,ListNode b){
if(a==null||b==null){
return a!=null ? a:b;//出现空的 就返回非空
}
ListNode dummy = new ListNode(-1);
ListNode cur = dummy;
ListNode pointA = a;
ListNode pointB = b;
while(pointA!=null&&pointB!=null){
if(pointA.val>pointB.val){
cur.next = pointB;
pointB = pointB.next;
}else{
cur.next = pointA;
pointA = pointA.next;
}
cur = cur.next;
}
//可能还有剩下的
cur.next = pointA != null?pointA:pointB;
return dummy.next;
}
}
- 优先队列
/**
* Definition for singly-linked list.
* public class ListNode {
* int val;
* ListNode next;
* ListNode() {}
* ListNode(int val) { this.val = val; }
* ListNode(int val, ListNode next) { this.val = val; this.next = next; }
* }
*/
class Solution {
public ListNode mergeKLists(ListNode[] lists) {
PriorityQueue<ListNode> pq = new PriorityQueue<>((o1,o2)->(o1.val-o2.val));
for(ListNode node:lists){
if(node!=null){
pq.add(node);
}
}
ListNode dummy = new ListNode(-1);
ListNode cur = dummy;
while(!pq.isEmpty()){
cur.next = pq.poll();
cur = cur.next;
if(cur.next!=null){
pq.add(cur.next);
}
}
return dummy.next;
}
}
链表中的两数相加
5. 多级链表/循环链表
展平多级双向链表
/*
// Definition for a Node.
class Node {
public int val;
public Node prev;
public Node next;
public Node child;
};
*/
//dfs搜索所有的节点
class Solution {
List<Node> list = new ArrayList<>();
public Node flatten(Node head) {
dfs(head);
if(list.size()==0) return null;
for(int i=0;i<list.size()-1;i++){
Node prev = list.get(i);
Node next = list.get(i+1);
prev.next = next;
next.prev = prev;
prev.child = null;
}
return head;
}
public void dfs(Node head){
if(head == null) return;
list.add(head);
dfs(head.child);
dfs(head.next);
}
}
排序的循环链表
5. 反转链表
反转链表
/**
* Definition for singly-linked list.
* public class ListNode {
* int val;
* ListNode next;
* ListNode() {}
* ListNode(int val) { this.val = val; }
* ListNode(int val, ListNode next) { this.val = val; this.next = next; }
* }
*/
class Solution {
public ListNode reverseList(ListNode head) {
if(head==null) return head;
ListNode prev = null;
ListNode cur = head;
while(cur!=null){
ListNode temp = cur.next;
cur.next = prev;
prev = cur;
cur = temp;
}
return prev;
}
}
K个一组翻转链表
分组去 反转链表 难点在于如何模拟过程
/**
* Definition for singly-linked list.
* public class ListNode {
* int val;
* ListNode next;
* ListNode() {}
* ListNode(int val) { this.val = val; }
* ListNode(int val, ListNode next) { this.val = val; this.next = next; }
* }
*/
class Solution {
public ListNode reverseKGroup(ListNode head, int k) {
ListNode dummyNode = new ListNode(-1);
dummyNode.next = head;
ListNode prev = dummyNode;//指向start的前一个
ListNode end = dummyNode;//指向反转的最后一个
while(end.next!=null){
for(int i=0;i<k&&end!=null;i++){
end = end.next;
}
if(end==null) break;//终止条件
ListNode start = prev.next;//反转的第一个元素
ListNode next = end.next;//指向末尾的下一个元素,用于反转后的拼接
//反转
end.next = null;//先把该部分断开
prev.next = reverse(start);
//反转后的拼接处理
start.next = next;
prev = start;
end = prev;
}
return dummyNode.next;
}
public ListNode reverse(ListNode head){
ListNode prev = null;
ListNode cur = head;
ListNode temp = null;//保留下一个指向的元素
while(cur!=null){
temp = cur.next;
cur.next = prev;
prev = cur;
cur = temp;
}
return prev;
}
}
重排链表
回文链表
五. 哈希表
1. 变位词
有效的变位词
变位词组
- 利用变位词排序后对应的key相同,构建hashmap
- 一些字符串,hashmap的处理api
class Solution {
public List<List<String>> groupAnagrams(String[] strs) {
List<List<String>> res = new ArrayList<>();
HashMap<String,ArrayList<String>> map = new HashMap<>();//key:排序后的元素,List<String> 对应的变位词
for(int i=0;i<strs.length;i++){
char[] chArray = strs[i].toCharArray();
Arrays.sort(chArray);
String key = String.valueOf(chArray);//key
ArrayList<String> value = map.getOrDefault(key,new ArrayList<String>());
value.add(strs[i]);//取出并更新 value
map.put(key,value);
}
for(Map.Entry<String,ArrayList<String>> entry:map.entrySet()){
res.add(entry.getValue());
}
return res;
}
}
2. 设计类
LRU缓存
class LRUCache {
int capacity;
int size;//记录大小
Map<Integer,Node> cache;//存储key和value
DoubleLinkedList dList;//保证key的顺序
public LRUCache(int capacity) {
this.capacity = capacity;
this.size = 0;
cache = new HashMap<>();
dList = new DoubleLinkedList();
}
public int get(int key) {
Node node = cache.get(key);
if(node==null) {
return -1;
}else{
//删除指定节点
dList.removeNode(node);
//将节点移动到首部
dList.addHead(node);
return node.val;
}
}
public void put(int key, int value) {
Node node = cache.get(key);//判断是否存在
if(node!=null){
node.val = value;
dList.removeNode(node);
dList.addHead(node);
}else{
Node newNode = new Node(key,value);
//判断容量
if(size>=capacity){
Node last = dList.getLast();
cache.remove(last.key);
dList.removeNode(last);
size--;
}
cache.put(key,newNode);
dList.addHead(newNode);
size++;
}
}
}
class Node{
int key;
int val;
Node prev;
Node next;
public Node(){}
public Node(int key,int val){
this.key = key;
this.val = val;
}
}
class DoubleLinkedList{
Node head;
Node tail;
public DoubleLinkedList(){
head = new Node();
tail = new Node();
head.next = tail;
tail.prev = head;
}
//插入到头部
public void addHead(Node node){
Node temp = head.next;
node.next = temp;
temp.prev = node;
head.next = node;
node.prev = head;
}
//删除节点
public void removeNode(Node node){
node.prev.next = node.next;
node.next.prev = node.prev;
}
//最后一个节点
public Node getLast(){
return tail.prev;
}
}
/**
* Your LRUCache object will be instantiated and called as such:
* LRUCache obj = new LRUCache(capacity);
* int param_1 = obj.get(key);
* obj.put(key,value);
*/
LFU缓存
class LFUCache {
int size;
int capacity;
Map<Integer,Node> cache;//缓存,key:节点
Map<Integer,DoubleLinkedList> freqMap;//频率:双向链表
int minFreq;//最小频率
public LFUCache(int capacity) {
this.capacity = capacity;
size = 0;
cache = new HashMap<>();
freqMap = new HashMap<>();
}
public int get(int key) {
Node node = cache.get(key);
if(node==null){
return -1;
}
updateFreq(node);
return node.val;
}
public void put(int key, int value) {
Node node = cache.get(key);
if(node!=null){
node.val = value;
updateFreq(node);
}else{
if(size==capacity){
DoubleLinkedList list = freqMap.get(minFreq);
cache.remove(list.tail.prev.key);//必须先删缓存后删链表
list.removeNode(list.tail.prev);
size--;
}
Node newNode = new Node(key,value);
cache.put(key,newNode);
DoubleLinkedList minList = freqMap.get(1);
if(minList==null){
minList = new DoubleLinkedList();
freqMap.put(1,minList);
}
minList.addNode(newNode);
size++;
minFreq = 1;
}
}
//更新频率
public void updateFreq(Node node){
int freq = node.freq;
DoubleLinkedList list = freqMap.get(freq);
list.removeNode(node);
if(freq==minFreq && list.head.next == list.tail){
minFreq += 1;//特殊情况
}
node.freq += 1;
list = freqMap.get(node.freq);//获取新频率的双向链表
if(list==null){
list = new DoubleLinkedList();
freqMap.put(node.freq,list);
}
list.addNode(node);
}
}
class Node{
int key;
int val;
int freq=1;
Node prev;
Node next;
public Node(){}
public Node(int key,int val){
this.key = key;
this.val = val;
}
}
class DoubleLinkedList{
Node head;
Node tail;
public DoubleLinkedList(){
head = new Node();
tail = new Node();
head.next = tail;
tail.prev = head;
}
//增加节点到头部
public void addNode(Node node){
node.next = head.next;
head.next.prev = node;
head.next = node;
node.prev = head;
}
//删除节点
public void removeNode(Node node){
node.prev.next = node.next;
node.next.prev = node.prev;
}
}
/**
* Your LFUCache object will be instantiated and called as such:
* LFUCache obj = new LFUCache(capacity);
* int param_1 = obj.get(key);
* obj.put(key,value);
*/
六. 栈
1. 栈模拟
2. 单调栈
直方图最大矩形面积
-
解法一:记忆化数组(好处理)
-
解法二:单调栈模拟
添加链接描述
class Solution {
public int largestRectangleArea(int[] heights) {
Deque<Integer> stack = new LinkedList<>();
stack.push(-1);
int res = Integer.MIN_VALUE;
for(int i=0;i<heights.length;i++){
while(stack.peek()!=-1&&heights[stack.peek()]>=heights[i]){
//求以每个节点中心的面积;寻找左右两边第一个小于该高度的元素
//stack.peek()左边第一个小于
//i右边第一个小于
res = Math.max(res,heights[stack.pop()]*(i-stack.peek()-1));
}
stack.push(i);
}
//还剩下元素
while(stack.peek()!=-1){
res = Math.max(res,heights[stack.pop()]*(heights.length-stack.peek()-1));
}
return res;
}
}
矩阵中最大的矩形 hard
题目
以上一题为基础,构造每一层柱子,然后遍历求解
七. 队列
1. 树的层序遍历
2. 设计类题目
滑动窗口的平均值
最近请求次数
往完全二叉树添加节点
添加链接描述
核心思路:队列只保存左右子树不完整的节点
3. 单调队列
滑动窗口最大值
双端队列实现单调队列
class Solution {
public int[] maxSlidingWindow(int[] nums, int k) {
if(nums.length == 1){return nums;}
int len = nums.length - k + 1;//滑窗数组的长度
int[] res = new int[len];//存放结果
//初始化结果
MyQueue que = new MyQueue();
for(int i =0;i<k;i++){
que.add(nums[i]);
}
res[0] = que.peek();
int num = 1;
for(int i = k;i<nums.length;i++){//i记录每次滑窗的最右端
que.pop(nums[i-k]);//移动滑窗,删除第一个元素
que.add(nums[i]);//把最后的元素加进去队列
res[num] = que.peek();
num++;
}
return res;
}
}
//单调队列
//每次用peek取出的元一定是该队列的最大值
class MyQueue{
Deque<Integer> deque = new LinkedList<>();
//入队操作:保证单调递减。while循环 如果该值比最后一个元素大,那就弹出最后一个元素,继续比较,知道满足条件
void add(int val){
while(!deque.isEmpty() && val > deque.getLast()){
deque.removeLast();
}
deque.add(val);
}
//出队操作:并不是针对队列出队,而是需要将队列与数组进行比较。仅仅当peek出来的元素与需要弹出的数据相等时,才将其弹出。因为仅仅peek出来的数据影响取最大值
void pop(int val){
if(!deque.isEmpty() && val == deque.peek()){
deque.poll();
}
}
//返回最大值
int peek(){
return deque.peek();
}
}
八. 树
1. 路径问题
节点之和最大的路径
class Solution {
int res = Integer.MIN_VALUE;
public int maxPathSum(TreeNode root) {
dfs(root);
return res;
}
public int dfs(TreeNode node){
if(node==null) return 0;
int leftSum = Math.max(dfs(node.left),0);
int rightSum = Math.max(dfs(node.right),0);
int sum = leftSum + rightSum + node.val;
res = Math.max(sum,res);
return node.val + Math.max(leftSum,rightSum);
}
}
向下的路径节点之和
class Solution {
Map<Long,Integer> map = new HashMap<>();
public int pathSum(TreeNode root, int targetSum) {
map.put(0L,1);
return dfs(root,0,targetSum);
}
public int dfs(TreeNode root,long sum,long targetSum){
if(root==null) return 0;
int res = 0;
sum = root.val + sum;
res = map.getOrDefault(sum-targetSum,0);
map.put(sum,map.getOrDefault(sum,0)+1);
res += dfs(root.left,sum,targetSum);
res += dfs(root.right,sum,targetSum);
map.put(sum,map.getOrDefault(sum,0)-1);
return res;
}
}
2. 二叉搜索树
二叉搜索树中的中序后继
所有大于等于节点的值之和
添加链接描述
关键在于选对遍历顺序
二叉搜索树迭代器
3. 设计树解决问题
值和下标之差都在给定的范围内
日程表
4. 树的修改与改造
二叉树的镜像
对称的二叉树
5.深度/高度问题
二叉树的最大深度
- dfs:
class Solution {
int res = 0;
public int maxDepth(TreeNode root) {
if(root==null) return 0;
int depth = 1;
dfs(root,depth);
return res;
}
public void dfs(TreeNode root,int depth){
res = Math.max(res,depth);
if(root.left==null&&root.right==null){
return;
}
if(root.left!=null){
depth++;
dfs(root.left,depth);
depth--;
}
if(root.right!=null){
depth++;
dfs(root.right,depth);
depth--;
}
}
}
- 层序遍历
class Solution {
public int maxDepth(TreeNode root) {
if(root==null) return 0;
int res = 0;
Queue<TreeNode> que = new LinkedList<>();
que.offer(root);
while(!que.isEmpty()){
int size = que.size();
while(size>0){
size--;
TreeNode node = que.poll();
if(node.left!=null){
que.offer(node.left);
}
if(node.right!=null){
que.offer(node.right);
}
}
res++;
}
return res;
}
}
九. 堆
数据流的第 K 大数值
添加链接描述
如何保证每次取出第k个大的元素:维护一个大小为k的小顶堆,每次取出来的元素为所求
出现频率最高的 k 个数字
添加链接描述
大顶堆
和最小的 k 个数对
添加链接描述
小顶堆(语法)+优化思路,如何减少搜索次数
十. 前缀树
实现前缀树
class Trie {
Trie[] children;
boolean isEnd;//是否为单词末尾
/** Initialize your data structure here. */
public Trie() {
children = new Trie[26];//26个字母
isEnd = false;
}
/** Inserts a word into the trie. */
public void insert(String word) {
Trie node = this;
for(int i=0;i<word.length();i++){
char ch = word.charAt(i);//取出字符
int index = ch - 'a';
if(node.children[index]==null){//该字符为空
node.children[index] = new Trie();
}
node = node.children[index];
}
node.isEnd = true;
}
/** Returns if the word is in the trie. */
public boolean search(String word) {
Trie node = searchPrefix(word);//搜索前缀
return node!=null&&node.isEnd;//节点非空且为结束
}
/** Returns if there is any word in the trie that starts with the given prefix. */
public boolean startsWith(String prefix) {
return searchPrefix(prefix) != null;
}
//找前缀
public Trie searchPrefix(String prefix){
Trie node = this;
for(int i=0;i<prefix.length();i++){
char c = prefix.charAt(i);
int index = c - 'a';
if(node.children[index] == null){
return null;
}
node = node.children[index];
}
return node;
}
}
/**
* Your Trie object will be instantiated and called as such:
* Trie obj = new Trie();
* obj.insert(word);
* boolean param_2 = obj.search(word);
* boolean param_3 = obj.startsWith(prefix);
*/
最短的单词编码(后缀树)
添加链接描述
记录每个单词的后缀,构建后缀树。后缀树的特点是只有每一个分支的最后一个字母的end标记为true,因此遍历每个单词判断其end标记
class Solution {
Trie trie = new Trie();
public int minimumLengthEncoding(String[] words) {
for(String word:words){
trie.insert(word);//构建后缀树
}
int res = 0;
Set<String> set = new HashSet<>();//去重
for(String word:words){
if(set.contains(word)) continue;
set.add(word);
//判断当前单词end是否为true,end为true就加长度
if(trie.isLastEnd(word)){
res += word.length() + 1;
}
}
return res;
}
}
class Trie{
Trie[] children;
boolean isEnd;//该分支的最后一个才设置为true
public Trie(){
children = new Trie[26];
isEnd = false;
}
public void insert(String word){
Trie node = this;
boolean newBranch = false;//是否开辟新分支
for(int i=word.length()-1;i>=0;i--){
char c = word.charAt(i);
int index = c - 'a';
if(node.children[index]==null){
node.children[index] = new Trie();
newBranch = true;
}
node = node.children[index];
if(node.isEnd&&i!=0) node.isEnd = false;//撤销之前的end标记
}
if(newBranch){
node.isEnd = true;
}
}
public boolean isLastEnd(String word){
Trie node = this;
for(int i=word.length()-1;i>=0;i--){
char c = word.charAt(i);
int index = c -'a';
node = node.children[index];
}
return node.isEnd;
}
}
十一. 排序
1. 归并排序
链表排序
2. 排序应用
把数组排成最小的数
- 内置排序
class Solution {
public String minNumber(int[] nums) {
String[] numsStr = new String[nums.length];
for(int i=0;i<nums.length;i++){
numsStr[i] = String.valueOf(nums[i]);
}
//30,3
//303<330 -30在3前面
Arrays.sort(numsStr,(x,y)->(x+y).compareTo(y+x));
String res = new String();
for(String str:numsStr){
res += str;
}
return res;
}
}
合并K个升序链表 (分治) TODO
十二. 回溯
1. 全排列
含有重复元素集合的全排列 TODO
2. 括号问题
生成匹配的括号
class Solution {
List<String> res = new ArrayList<>();
StringBuilder path = new StringBuilder();
public List<String> generateParenthesis(int n) {
backTracking(n,0,0);
return res;
}
public void backTracking(int n,int left,int right){
if(path.length()==2*n){
res.add(path.toString());
return;
}
if(left<n){
path.append('(');
backTracking(n,left+1,right);
path.deleteCharAt(path.length()-1);
}
if(left>right){
path.append(')');
backTracking(n,left,right+1);
path.deleteCharAt(path.length()-1);
}
}
}
十三. 动态规划
1. 二维动态规划问题
最长公共子序列 TODO
2. 背包问题
排列的数目 TODO
3. 取不取问题
环形房屋偷盗
class Solution {
public int rob(int[] nums) {
int len = nums.length;
if(len==1){return nums[0];}
if(len==2){
return Math.max(nums[0],nums[1]);
}
return Math.max(robHelper(nums,0,len-2),robHelper(nums,1,len-1));
}
public int robHelper(int[] nums,int start,int end){
int[] dp = new int[end-start+1];
dp[0] = nums[start];
dp[1] = Math.max(nums[start],nums[start+1]);
for(int i=2;i<=end-start;i++){
if(start==0){
dp[i] = Math.max(dp[i-2]+nums[i],dp[i-1]);
}
if(start==1){
dp[i] = Math.max(dp[i-2]+nums[i+1],dp[i-1]);
}
}
return dp[end-start];
}
}
粉刷房子
class Solution {
public int minCost(int[][] costs) {
int len = costs.length;
int[][] dp = new int[len][3];
dp[0][0] = costs[0][0];
dp[0][1] = costs[0][1];
dp[0][2] = costs[0][2];
for(int i=1;i<len;i++){
dp[i][0] = Math.min(dp[i-1][1],dp[i-1][2]) + costs[i][0];
dp[i][1] = Math.min(dp[i-1][0],dp[i-1][2]) + costs[i][1];
dp[i][2] = Math.min(dp[i-1][1],dp[i-1][0]) + costs[i][2];
}
return Math.min(Math.min(dp[len-1][0],dp[len-1][1]),dp[len-1][2]);
}
}
4. 回文串
最少回文分割
5. 字符串操作
字符串交织 TODO
6. 其他
n个骰子的点数
class Solution {
public double[] dicesProbability(int n) {
//概率=投出和为x的方法数/总方法数 (看成排列数,1,2和2,1算两种情况)
double[] res = new double[6*n-n+1];
//dp[i][j] i个骰子投出和为j的方法数
//i-1个骰子投出 (j-k) 的方法数之和(k为1,2,3,4,5,6)
int[][] dp = new int[n+1][n*6+1];
for(int i=1;i<=6;i++){
dp[1][i] = 1;
}
for(int i=2;i<=n;i++){
for(int j=i;j<=i*6;j++){
for(int k=1;k<=6;k++){
if(j<k) break;
dp[i][j] += dp[i-1][j-k];
}
}
}
//计算最后结果
double total = Math.pow(6,n);
for(int i=0;i<=5*n;i++){
res[i] = dp[n][i+n]/total;
}
return res;
}
}
十四. 图论
1. 二分图
二分图 TODO
2. BFS最短路
开密码锁(板子题) TODO
单词接龙(双向BFS减少时间复杂度) TODO
矩阵中的距离
- 解法一:板子,从0反向搜索
class Solution {
public int[][] updateMatrix(int[][] mat) {
int m = mat.length;
int n = mat[0].length;
int[][] res = new int[m][n];
boolean[][] visited = new boolean[m][n];
Queue<int[]> que = new LinkedList<>();
for(int i=0;i<m;i++){
for(int j=0;j<n;j++){
if(mat[i][j] == 0){
visited[i][j] = true;
que.offer(new int[]{i,j});
}
}
}
int[][] dirs = {{-1,0},{1,0},{0,-1},{0,1}};
while(!que.isEmpty()){
int[] temp = new int[2];
temp[0] = que.peek()[0];
temp[1] = que.poll()[1];
for(int[] dir:dirs){
int nx = temp[0] + dir[0];
int ny = temp[1] + dir[1];
if(nx<0||ny<0||nx>=m||ny>=n||visited[nx][ny]||mat[nx][ny]!=1){
continue;
}
visited[nx][ny] = true;
res[nx][ny] = res[temp[0]][temp[1]] + 1;
que.offer(new int[]{nx,ny});
}
}
return res;
}
}
- 解法二:动态规划
机器人的运动范围
class Solution {
public int movingCount(int m, int n, int k) {
if(k==0) return 1;
Queue<int[]> que = new LinkedList<>();
boolean[][] visited = new boolean[m][n];
visited[0][0] = true;
que.offer(new int[]{0,0});
int[][] dirs = {{1,0},{-1,0},{0,1},{0,-1}};
int res = 1;
while(!que.isEmpty()){
int[] temp = que.poll();
int x = temp[0];
int y = temp[1];
for(int[] dir:dirs){
int nx = x + dir[0];
int ny = y + dir[1];
if(nx<0||ny<0||nx>=m||ny>=n||visited[nx][ny]||!isValid(nx,ny,k)){
continue;
}
que.offer(new int[]{nx,ny});
visited[nx][ny] = true;
res++;
}
}
return res;
}
public boolean isValid(int m,int n,int k){
int res = 0;
while(m>0||n>0){
res += m%10+n%10;
m /= 10;
n /= 10;
}
if(res>k){
return false;
}else{
return true;
}
}
}
3. 岛屿问题
岛屿最大面积 TODO
岛屿数量
class Solution {
int[][] dirs = {{1,0},{-1,0},{0,1},{0,-1}};
public int numIslands(char[][] grid) {
int res = 0;
for(int i=0;i<grid.length;i++){
for(int j=0;j<grid[0].length;j++){
if(grid[i][j]=='1'){
dfs(grid,i,j);
res++;
}
}
}
return res;
}
public void dfs(char[][] grid,int x,int y){
grid[x][y] = '0';
for(int[] dir:dirs){
int nx = x + dir[0];
int ny = y + dir[1];
if(nx<0||ny<0||nx>=grid.length||ny>=grid[0].length||grid[nx][ny]!='1'){
continue;
}
dfs(grid,nx,ny);
}
}
}
4. 拓扑排序 (无向图找环)
构建入度表,邻接表
度最小的入队
出队,遍历对应的邻接表,更新状态
课程顺序 TODO
多余的边
class Solution {
public int[] findRedundantConnection(int[][] edges) {
//先找环,然后从后往前遍历,删除边
int len = edges.length;
int[] indegree = new int[len];
List<List<Integer>> joinList = new ArrayList<>();
for(int i=0;i<len;i++){
joinList.add(new ArrayList<>());
}
for(int[] edge:edges){
indegree[edge[0]-1]++;
indegree[edge[1]-1]++;
joinList.get(edge[0]-1).add(edge[1]-1);
joinList.get(edge[1]-1).add(edge[0]-1);
}
//入度为1的入队
Queue<Integer> que = new LinkedList<>();
for(int i=0;i<len;i++){
if(indegree[i]==1){
que.offer(i);
}
}
while(!que.isEmpty()){
int item = que.poll();
for(int index:joinList.get(item)){
indegree[index]--;
if(indegree[index]==1){
que.offer(index);
}
}
}
//处理 度不为1 说明是环 删除
for(int i=len-1;i>=0;i--){
if(indegree[edges[i][0]-1]>1&&indegree[edges[i][1]-1]>1){
return edges[i];
}
}
return new int[0];
}
}
5. 有向无环图
所有路径 TODO
6. 并查集
最长连续序列 (高频题)
class Solution {
public int longestConsecutive(int[] nums) {
UnionFind uf = new UnionFind(nums);//初始化
int res = 0;
for(int num:nums){//合并
if(uf.findRoot(num+1) != null){
uf.union(num,num+1);
}
}
for(int num:nums){
int end = uf.findRoot(num);
res = Math.max(res,end-num+1);
}
return res;
}
}
//并查集
class UnionFind{
Map<Integer,Integer> parent = new HashMap<>();//节点:父节点
//初始化
public UnionFind(int[] nums){
for(int num:nums){
parent.put(num,num);//节点本身是自己的父节点
}
}
//找父节点
public Integer findRoot(int num){
if(!parent.containsKey(num)){//假设不存在该数
return null;
}
while(num!=parent.get(num)){
parent.put(num,parent.get(parent.get(num)));
num = parent.get(num);
}
return num;
}
//合并两个连通分量: num并入到num+1
public void union(int x,int y){
int rootX = findRoot(x);
int rootY = findRoot(y);
if(rootX==rootY){
return;
}
parent.put(rootX,rootY);
}
}
- 方法二:哈希 o(n) o(n)
省份数量
class Solution {
int[] parent ;
public int findCircleNum(int[][] isConnected) {
int len = isConnected.length;
parent = new int[len];
for(int i=0;i<len;i++){
parent[i] = i;
}
for(int i=0;i<len;i++){
for(int j=i+1;j<len;j++){
if(isConnected[i][j]==1){
union(j,i);
}
}
}
int res = 0;
for(int i=0;i<parent.length;i++){
if(parent[i]==i){
res++;
}
}
return res;
}
public int findRoot(int num){
if(parent[num]!=num){
parent[num] = findRoot(parent[num]);
}
return parent[num];
}
//x合并到y
public void union(int x,int y){
int rootX = findRoot(x);
int rootY = findRoot(y);
parent[rootX] = rootY;
}
}
十五. 查找算法
1. 找重复数字
数组中重复的数字 (任意次数)
class Solution {
public int findRepeatNumber(int[] nums) {
int index = 0;
while(index<nums.length){
if(nums[index]==index){
index++;
continue;
}
if(nums[nums[index]]==nums[index]){
return nums[index];
}
int temp = nums[index];
nums[index] = nums[temp];
nums[temp] = temp;
}
return -1;
}
}
只出现一次的数字(3次中找1次) TODO
只出现一次的数字(2次中找1次) TODO
class Solution {
public int singleNumber(int[] nums) {
int res = nums[0];
for(int i=1;i<nums.length;i++){
res ^= nums[i];
}
return res;
}
}
十六. 数学
数组中出现次数超过一半的数字
- 方法一:排序取中 o(nlogn) o(logn)
- 方法二:选票法 o(n) o(1)