文章目录
- 1.整数除法
- 2.二进制加法
- 3.前n个数字二进制中1的个数
- 4.只出现一次的数字
- 5.单词长度的最大乘积
- 6.排序数组中两个数字之和
- 7.数组中和为0的三个数
- 8.和大于等于target的最短子数组
- 9.乘积小于K的子数组
- 10.和为K的子数组
- 11.01个数相同的子数组
- 12.左右两边子数组的和相等
- 13.二维子矩阵的和
- 14.字符串中的变位词
- 15.字符串中的所有变位词
- 16.不含重复字符的最长子字符串
- 17.含有所有字符的最短字符
- 18.有效的回文串
- 19.最多删除一个字符得到回文
- 20.回文字符串的个数
- 21.删除链表的倒数第n个结点
- 22.链表中环的入口节点
- 23.两个链表的第一个重合节点
- 24.反转链表
- 25.链表中的两数相加
- 26.重排链表
- 27.回文链表
- 28.展平多级双向链表
- 29.排序的循环链表
- 30.插入删除和随机访问都是O(1)的容器
- 31.最近最少使用缓存
- 32.有效的变位词
- 33.变位词组
- 34.外星语言是否排序
- 35.最小的时间差
- 36.后缀表达式
- 37.小行星碰撞
- 38.每日温度
- 39.直方图最大矩形面积
- 40.矩阵中的最大矩形
- 41.滑动窗口的平均值
- 42.最近请求次数
- 43.往完全二叉树添加节点
- 44.二叉树每层的最大值
- 45.二叉树最底层最左边的值
- 46.二叉树的右侧视图
- 47.二叉树剪枝
- 48.二叉树的序列化和反序列化
- 49.从根节点到叶节点的路径数字之和
- 50.向下的路径节点之和
- 51.节点之和最大的路径
- 52.展平二叉搜索树
- 53.二叉搜索树中的中序后继
- 54.所有大于等于节点的值之和
- 55.二叉搜索树迭代器
- 56.二叉搜索树中两个节点之和
- 57.值和下标之差都在给定的范围内
- 58.日程表
- 59.数据流的第K大数值
- 60.出现频率最高的k个数字
- 61.和最小的k个数对
- 68.查找插入位置
- 69.山峰数组的顶部
- 70.排序数组中只出现一次的数字
- 71.按权重生成随机数
- 72.求平方根
- 73.狒狒吃香蕉
- 74.合并区间
- 75.数组相对排序
- 76.数组中的第k大的数字
- 77.链表排序
- 78.合并排序链表
- 79.所有子集
- 80.含有k个元素的组合
- 81.允许重复选择元素的组合
- 82.含有重复元素集合的组合
- 83.没有重复元素集合的全排列
- 84.含有重复元素集合的全排列
- 85.生成匹配的括号
- 86.分割回文子字符串
- 87.复原IP
- 88.爬楼梯的最少成本
- 89.房屋偷盗
- 90.环形房屋偷盗
- 91.粉刷房子
- 92.翻转字符
- 93.最长斐波那契数列
- 94.最少回文分割
- 95.最长公共子序列
- 96.字符串交织
- 97.子序列的数目
- 98.路径的数目
- 99.最小路径之和
- 100.三角形中最小路径之和
- 101.分割等和子集
- 102.加减的目标值
- 103.最少的硬币数
- 104.排列的数目
- 105.岛屿最大面积
- 106.二分图
- 107.矩阵中的距离
- 108.单词演变
- 109.开密码锁
- 110.所有路径
- 111.计算除法
- 112.最长递增路径
- 113.课程顺序
- 114.外星文字典
- 116.省份数量
- 117.相似的字符串
- 118.多余的边
- 119.最长连续序列
本文参考:
本文与我的另一篇刷题笔记算法学习-剑指 Offer(第 2 版)同步更新,同样是程序员的经典刷题题库,需要尽快将其掌握住。
1.整数除法
二分法和快速乘法。
class Solution {
public int divide(int a, int b) {
boolean flag=(a<0&&b>0)||(a>0&&b<0)?false:true;
//取long是为了防止2^31取反溢出
long x=a;
long y=b;
if(a<0){
x=-x;
}
if(b<0){
y=-y;
}
long left=0;
//a一定是最终结果可能的最大值
long right=x;
while(left<right){
long mid=(left+right+1)/2;
if(mul(mid,y)>x){
right=mid-1;
}else{
left=mid;
}
}
long res=flag?left:-left;
return res<Integer.MIN_VALUE||res>Integer.MAX_VALUE?Integer.MAX_VALUE:(int)res;
}
//快速乘法
public long mul(long x,long y){
long res=0;
while(y!=0){
if((y&1)==1){
res+=x;
}
y>>>=1;
//增大乘数x
x+=x;
}
return res;
}
}
2.二进制加法
有两种做法,其一是参考lilyunoke大神的加法模板,这是我比较喜欢的做法,题解如下:
class Solution {
public String addBinary(String a, String b) {
int i=a.length()-1;
int j=b.length()-1;
int carry=0;
StringBuilder sb=new StringBuilder();
while(i>=0||j>=0){
int digitA=i>=0?a.charAt(i)-'0':0;
int digitB=j>=0?b.charAt(j)-'0':0;
int sum=digitA+digitB+carry;
carry=sum/2;
int digit=sum%2;
sb.append(digit);
i--;
j--;
}
if(carry!=0) sb.append(carry);
return sb.reverse().toString();
}
}
其中可以总结出来的加法模板是:
while ( A 没完 || B 没完){
取到A 的当前位
取到B 的当前位
和 = A 的当前位 + B 的当前位 + 进位carry
当前位 = 和 % 10;
进位 = 和 / 10;
A左移调整
B左移调整
}
判断进位是否为0,不为0额外加上
将结果反转
或者参考负雪明烛大佬的题解,不同的是循环条件,需要注意的是while循环
结束条件,注意遍历完两个「加数」,以及进位不为0。
class Solution {
public String addBinary(String a, String b) {
int i=a.length()-1;
int j=b.length()-1;
int carry=0;
StringBuilder sb=new StringBuilder();
while(i>=0||j>=0||carry!=0){
int digitA=i>=0?a.charAt(i)-'0':0;
int digitB=j>=0?b.charAt(j)-'0':0;
int sum=digitA+digitB+carry;
carry=sum>=2?1:0;
int digit=sum>=2?sum-2:sum;
sb.append(digit);
i--;
j--;
}
return sb.reverse().toString();
}
}
3.前n个数字二进制中1的个数
这题考虑到1的个数从0开始到n是有规律的就容易了,奇偶性按照动态规划做是非常巧妙的。
class Solution {
public int[] countBits(int n) {
int[]dp=new int[n+1];
for(int i=1;i<=n;i++){
if(i%2==1){
dp[i]=dp[i-1]+1;
}else{
dp[i]=dp[i/2];
}
}
return dp;
}
}
4.只出现一次的数字
数位统计+按位计算二进制数
class Solution {
public int singleNumber(int[] nums) {
int[]bit=new int[32];
for(int i=0;i<32;i++){
for(int n:nums){
if((n>>i&1)==1) bit[i]++;
}
}
int ans=0;
for(int i=0;i<32;i++){
if(bit[i]%3!=0){
ans|=1<<i;
}
}
return ans;
}
}
5.单词长度的最大乘积
如果遍历每一对单词,然后每次判断是否有重复元素,复杂度为O(N^2*L),约为O(10^9)
,TLE。因此尝试是否能将判断公共字母的时间复杂度降为O(1)
,所以我们需要进行一步预处理。针对每个单词words[i]
,采用位掩码记录该单词中出现过的字母,即如果那个字母出现,对应位置1, masks[i] |= 1 << (word.charAt(j) - 'a');
,时间复杂度O(N*L)
,总时间复杂度O(N*L+N^2),约为O(10^6)
。
class Solution {
public int maxProduct(String[] words) {
int len=words.length;
int[]states=new int[len];
for(int i=0;i<len;i++){
String w=words[i];
int wlen=w.length();
for(int j=0;j<wlen;j++){
//掩码记录出现字母
states[i]|=1<<w.charAt(j)-'a';
}
}
int res=0;
for(int i=0;i<len;i++){
for(int j=i+1;j<len;j++){
if((states[i]&states[j])==0){
res=Math.max(res,words[i].length()*words[j].length());
}
}
}
return res;
}
}
6.排序数组中两个数字之和
这题用双指针解答,主要是要理解双指针的单向移动不会错过答案。
同时这也是我开始用cpp刷的第一题,cpp刷题的入门知识同步更新于我的另一篇文章算法学习-以刷题为导向需要掌握的C++知识。
class Solution {
public:
vector<int> twoSum(vector<int>& numbers, int target) {
int i=0;
int j=numbers.size()-1;
while(i<j){
int sum=numbers[i]+numbers[j];
if(sum==target){
return {i,j};
}else if(sum>target){
j--;
}else{
i++;
}
}
//前面一定会返回答案
return {0,0};
}
};
7.数组中和为0的三个数
常规暴力解法为O(N^3)
超时,固定两个数通过二分查找第三个数O(N^2logN)
可以过,最优的解法是固定一个数,内部两个数的确定采用双指针,类似思路同上 6.排序数组中两个数字之和,整体时间复杂度为O(N^2)
,但是去重处理需要注意,通常是将数组「排序」后方便处理。
#include<iostream>
class Solution {
public:
vector<vector<int>> threeSum(vector<int>& nums) {
sort(nums.begin(),nums.end());
vector<vector<int>> res;
int len=nums.size();
for(int i=0;i<len;i++){
int a=nums[i];
//这里进行去重,因为针对第一个重复的元素来说,我们已经将包含重复的组合以及不重复的组合都枚举过了
if(i>0&&nums[i]==nums[i-1]) continue;
int left=i+1;
int right=len-1;
while(left<right){
if(nums[i]+nums[left]+nums[right]>0) right--;
else if(nums[i]+nums[left]+nums[right]<0) left++;
else{
res.push_back({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--;
}
}
}
return res;
}
};
8.和大于等于target的最短子数组
经典滑动窗口,学会cpp解法。
class Solution {
public:
int minSubArrayLen(int target, vector<int>& nums) {
int len=nums.size();
int sum=0;
int left=0;
int right=0;
int ans=INT_MAX;
while(right<len){
sum+=nums[right];
while(sum>=target){
ans=min(ans,right-left+1);
sum-=nums[left];
left++;
}
right++;
}
return ans==INT_MAX?0:ans;
}
};
9.乘积小于K的子数组
参考题解,应用了滑动窗口思想,但是是通过每次left或者right
的改变求满足条件的连续子数组数量。
class Solution {
public:
int numSubarrayProductLessThanK(vector<int>& nums, int k) {
int ans = 0;
int left = 0;
int right=0;
int len = nums.size();
int mul = 1;
while (right < len) {
mul *= nums[right];
// 不满足条件的时候
while(left<=right&&mul >= k){
mul /= nums[left];
left++;
}
ans += right - left + 1;
right++;
}
return ans;
}
};
10.和为K的子数组
class Solution {
public int subarraySum(int[] nums, int k) {
//key:前缀和 value:该前缀和的个数
HashMap<Integer,Integer> map=new HashMap<>();
//前缀和为0的时候初始化为1,方便后面preSum-k的get
//细节,这里需要预存前缀和为 0 的情况,否则会漏掉前几位就满足的情况
//例如输入[1,1,0],k = 2 如果没有这行代码,则会返回0,漏掉了1+1=2,和1+1+0=2的情况
map.put(0,1);
int preSum = 0;
int ans = 0;
for(int i:nums){
preSum += i;
//当前前缀和已知,判断是否含有 presum - k的前缀和,那么我们就知道某一区间的和为 k 了。
if (map.containsKey(preSum-k)) {
ans += map.get(preSum-k); //获取次数
}
//不断维护(preSum,value)的哈希表
map.put(preSum,map.getOrDefault(preSum,0)+1);
}
return ans;
}
}
11.01个数相同的子数组
class Solution {
public int findMaxLength(int[] nums) {
int cur=0;
HashMap<Integer,Integer> map=new HashMap<>();
map.put(0,-1);
//可能有多种符合题目的情况,我们取最大值
int ans=0;
for(int i=0;i<nums.length;i++){
cur += nums[i]==0?-1:1;
if(map.containsKey(cur)){
ans=Math.max(ans,i-map.get(cur));
//else是一种贪心,当哈希表中存在相同的cur时,仍然放之前最小的那个
}else{
map.put(cur,i);
}
}
return ans;
}
}
12.左右两边子数组的和相等
class Solution {
public int pivotIndex(int[] nums) {
int sum=0;
for(int i:nums){
sum+=i;
}
int temp=0;
for(int i=0;i<nums.length;i++){
temp+=nums[i];
int left=temp-nums[i];
int right=sum-temp;
if(left==right) return i;
}
return -1;
}
}
13.二维子矩阵的和
这题直接按题意模拟也能做,但考虑到会调用 104 次 sumRegion
方法,每次都要O(M*N)
的时间复杂度,我们可以进行前缀和优化,用一次O(M*N)
的时间算出前缀子矩阵和,然后每次调用sumRegion
都只需要O(1)
的复杂度。参考题解,前缀子矩阵和按列算出。
左上角 (row1, col1) 、右下角 (row2, col2)这个下标按找到的方块理解,不然会陷入索引里去。
class NumMatrix {
//preSum[i][j]表示以(i,j)块为右下边界的子矩阵前缀和
int [][]preSum;
int row;
int col;
public NumMatrix(int[][] matrix) {
row=matrix.length;
col=matrix[0].length;
preSum=new int[row+1][col+1];
//按列计算
for(int j=0;j<col;j++){
int colsum=0;
for(int i=0;i<row;i++){
colsum+=matrix[i][j];
preSum[i+1][j+1]=preSum[i+1][j]+colsum;
}
}
}
public int sumRegion(int row1, int col1, int row2, int col2) {
return preSum[row2+1][col2+1]-preSum[row2+1][col1]-preSum[row1][col2+1]+preSum[row1][col1];
}
}
/**
* Your NumMatrix object will be instantiated and called as such:
* NumMatrix obj = new NumMatrix(matrix);
* int param_1 = obj.sumRegion(row1,col1,row2,col2);
*/
14.字符串中的变位词
固定大小的滑动窗口题
class Solution {
public boolean checkInclusion(String s1, String s2) {
int[] target=new int[26];
for(char c:s1.toCharArray()){
target[c-'a']++;
}
int left=0;
int[] temp=new int[26];
for(int right=0;right<s2.length();right++){
temp[s2.charAt(right)-'a']++;
if(right-left+1==s1.length()){
if(Arrays.equals(temp,target)){
return true;
}
temp[s2.charAt(left)-'a']--;
left++;
}
}
return false;
}
}
15.字符串中的所有变位词
固定大小的滑动窗口
class Solution {
public List<Integer> findAnagrams(String s, String p) {
int[]target=new int[26];
for(char c:p.toCharArray()){
target[c-'a']++;
}
ArrayList<Integer>ans=new ArrayList<>();
int len=s.length();
int left=0;
int[]temp=new int[26];
for(int right=0;right<len;right++){
temp[s.charAt(right)-'a']++;
if(right-left+1==p.length()){
if(Arrays.equals(target,temp)){
ans.add(left);
}
temp[s.charAt(left)-'a']--;
left++;
}
}
return ans;
}
}
16.不含重复字符的最长子字符串
窗口长度可变求最大值
class Solution {
public int lengthOfLongestSubstring(String s) {
HashSet<Character> window=new HashSet<>();
int len=s.length();
int left=0;
int ans=0;
for(int right=0;right<len;right++){
while(window.contains(s.charAt(right))){
window.remove(s.charAt(left++));
}
window.add(s.charAt(right));
ans=Math.max(ans,right-left+1);
}
return ans;
}
}
17.含有所有字符的最短字符
滑动窗口最小值
方法类似于 438.找到字符串中所有字母异位词,用target数组
记录要匹配的值,大小写加上中间的字母总共58的大小。用check()
检查是否包含,包含则调整左边界,缩小字符串长度。
class Solution {
public String minWindow(String s, String t) {
int[]target=new int[58];
for(char c:t.toCharArray()){
target[c-'A']++;
}
String res="";
int left=0;
int len=s.length();
int[]temp=new int[58];
int ans=0x3f3f3f3f;
for(int right=0;right<len;right++){
temp[s.charAt(right)-'A']++;
while(check(temp,target)){
int value=right-left+1;
if(value<ans){
ans=value;
res=s.substring(left,right+1);
}
temp[s.charAt(left)-'A']--;
left++;
}
}
return res;
}
public boolean check(int[]temp,int[]target){
for(int i=0;i<temp.length;i++){
if(temp[i]<target[i]) return false;
}
return true;
}
}
18.有效的回文串
class Solution {
public boolean isPalindrome(String s) {
int i=0;
int j=s.length()-1;
while(i<=j){
if(!Character.isLetterOrDigit(s.charAt(i))){
i++;
continue;
}
if(!Character.isLetterOrDigit(s.charAt(j))){
j--;
continue;
}
char a=Character.toLowerCase(s.charAt(i));
char b=Character.toLowerCase(s.charAt(j));
if(a!=b) return false;
i++;
j--;
}
return true;
}
}
19.最多删除一个字符得到回文
如果双指针不匹配,则尝试删除左指针元素或者右指针元素,再分别判断回文串。
class Solution {
//如果双指针不匹配,则尝试删除左指针元素或者右指针元素,再分别判断回文串
public boolean validPalindrome(String s) {
int i=0;
int j=s.length()-1;
while(i<=j){
if(s.charAt(i)==s.charAt(j)){
i++;
j--;
}else{
return isValid(s,i+1,j)||isValid(s,i,j-1);
}
}
return true;
}
public boolean isValid(String s,int i,int j){
while(i<=j){
if(s.charAt(i)==s.charAt(j)){
i++;
j--;
}else{
return false;
}
}
return true;
}
}
20.回文字符串的个数
可以采用两种做法,一种是双指针暴力枚举O(N^2)
,但是枚举的过程中不断记录正向和反向的字符串,比较字符串是否相等O(1)
,而不需要额外来一次回文比较O(N)
,空间换时间。
class Solution {
public int countSubstrings(String s) {
int len=s.length();
int cnt=0;
for(int i=0;i<len;i++){
String s1="";
String s2="";
for(int j=i;j<len;j++){
s1+=s.charAt(j);
s2=s.charAt(j)+s2;
if(s1.equals(s2)) cnt++;
}
}
return cnt;
}
}
还有一种是中心拓展法,不用担心会不会记录重复的串,由于左右边界如果要展开都是双向同时展开的,串的一部分有重复不会影响最后整个串重复。考虑奇偶的展开是不一样的情况。
class Solution {
public int countSubstrings(String s) {
int cnt=0;
int len=s.length();
for(int k=0;k<=len-1;k++){
cnt += countValid(s,k,k);
cnt += countValid(s,k,k+1);
}
return cnt;
}
public int countValid(String s,int i,int j){
int cnt=0;
while(i>=0&&j<=s.length()-1){
if(s.charAt(i--)==s.charAt(j++)){
cnt++;
}else break;
}
return cnt;
}
}
21.删除链表的倒数第n个结点
快慢指针法,快指针先提前走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 dummyHead=new ListNode(0);
dummyHead.next=head;
ListNode fast=dummyHead;
ListNode slow=dummyHead;
while(n--!=0){
fast=fast.next;
}
while(fast.next!=null){
slow=slow.next;
fast=fast.next;
}
slow.next=slow.next.next;
return dummyHead.next;
}
}
22.链表中环的入口节点
快慢指针的运用,快指针每次走两步,慢指针每次走一步,能相遇则一定有环否则无环。设链表共有 a+b
个节点,其中 链表头部到链表入口 有 a
个节点(不计链表入口节点), 链表环 有 b
个节点,快指针步数fast=2*slow
,fast=slow+n*b
,如果两者在任何时刻相遇,都能得到此时slow=n*b
。记下相遇时慢指针走了nb
步,由于所有指针走到环的入口节点时的步数是k=a+nb
,因此慢指针再走a
步就能到环入口,这正是链表头开始走,可以和慢指针slow碰头的步数。
总而言之,并没有用到链表中行走长度的记录,而是用了两次指针的碰撞。第一次快慢指针碰撞记下slow的位置;然后重新定义个cur指针从head出发,继续尝试和slow碰撞,从而找到环的入口。
/**
* 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 fast=head;
ListNode slow=head;
// 如果出现null就说明无环
while(fast!=null&&fast.next!=null){
fast=fast.next.next;
slow=slow.next;
if(fast==slow){
ListNode cur=head;
while(slow!=cur){
slow=slow.next;
cur=cur.next;
}
// 有环并找到为cur
return cur;
}
}
// 无环返回null
return null;
}
}
23.两个链表的第一个重合节点
参考题解,本质上就是要一直找到两个链表「地址相同的两个节点」,但两链表节点数不一样,没法对应。可以尝试拼接的做法,让p1遍历完链表A之后开始遍历链表B,让p2遍历完链表B之后开始遍历链表A,这样相当于「逻辑上」两条链表接在了一起,这样子就让相交节点前面长度补齐了。即使最后两链表没有相交,则p1=p2=null
退出循环。两条拼接链表都只遍历一次。
/**
* Definition for singly-linked list.
* public class ListNode {
* int val;
* ListNode next;
* ListNode(int x) {
* val = x;
* next = null;
* }
* }
*/
public class Solution {
public ListNode getIntersectionNode(ListNode headA, ListNode headB) {
ListNode p1=headA;
ListNode p2=headB;
while(p1!=p2){
p1=p1==null?headB:p1.next;
p2=p2==null?headA:p2.next;
}
return p1;
}
}
24.反转链表
/**
* 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) {
ListNode pre=null;
ListNode cur=head;
while(cur!=null){
ListNode temp=cur.next;
cur.next=pre;
pre=cur;
cur=temp;
}
return pre;
}
}
25.链表中的两数相加
先用栈将两个链表的数字反转存储,然后双对象双指针的加法模板+头插法构建链表
/**
* 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 addTwoNumbers(ListNode l1, ListNode l2) {
Stack<Integer> st1=new Stack<>();
Stack<Integer> st2=new Stack<>();
while(l1!=null){
st1.push(l1.val);
l1=l1.next;
}
while(l2!=null){
st2.push(l2.val);
l2=l2.next;
}
int carry=0;
ListNode anshead=null;
while(!st1.isEmpty()||!st2.isEmpty()){
int digitA=st1.isEmpty()?0:st1.peek();
int digitB=st2.isEmpty()?0:st2.peek();
int sum=digitA+digitB+carry;
int res=sum%10;
carry=sum/10;
ListNode newNode=new ListNode(res);
newNode.next=anshead;
anshead=newNode;
if(!st1.isEmpty()) st1.pop();
if(!st2.isEmpty()) st2.pop();
}
if(carry!=0){
ListNode newNode=new ListNode(carry);
newNode.next=anshead;
anshead=newNode;
}
return anshead;
}
}
26.重排链表
这题涉及到的知识点非常多,参考题解,总体思路是中间分割,后半部分反转链表,然后合并链表。先中间分割链表,while(fast.next!=null&&fast.next.next!=null)
在偶数节点情况下找到的是前面的中间节点,奇数情况下是正中间节点,我们都通过mid.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 void reorderList(ListNode head) {
ListNode mid= split(head);
ListNode headb= reverse(mid.next);
//这一步很关键,要将两个链表分开来
mid.next=null;
merge(head,headb);
}
public ListNode split(ListNode head){
ListNode slow=head;
ListNode fast=head;
while(fast.next!=null&&fast.next.next!=null){
fast=fast.next.next;
slow=slow.next;
}
return slow;
}
public ListNode reverse(ListNode head){
ListNode pre=null;
ListNode cur=head;
while(cur!=null){
ListNode temp=cur.next;
cur.next=pre;
pre=cur;
cur=temp;
}
return pre;
}
public void merge(ListNode heada,ListNode headb){
ListNode cura=heada;
ListNode curb=headb;
while(curb!=null){
ListNode nexta=cura.next;
ListNode nextb=curb.next;
curb.next=nexta;
cura.next=curb;
cura=nexta;
curb=nextb;
}
}
}
27.回文链表
从后向前找到链表中心点,偶数找到左边中心点,然后将后半部分链表反转,最后比较后小半部分是否和从head
开始的前半部分相同。
/**
* 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 boolean isPalindrome(ListNode head) {
ListNode fast=head;
ListNode slow=head;
//找到中心点
while(fast.next!=null&&fast.next.next!=null){
slow=slow.next;
fast=fast.next.next;
}
//后半部分反转
ListNode pre=null;
ListNode cur=slow.next;
while(cur!=null){
ListNode temp=cur.next;
cur.next=pre;
pre=cur;
cur=temp;
}
//后半部分偏小,比较反转的后半部分和前半部分是否回文
while(pre!=null){
if(pre.val!=head.val) return false;
pre=pre.next;
head=head.next;
}
return true;
}
}
28.展平多级双向链表
遍历第一级节点,将后面的节点逐层拼接到第一级上,做法是将head.child!=null
的孩子孩子节点头尾改变指向,放到第一级上去。
/*
// Definition for a Node.
class Node {
public int val;
public Node prev;
public Node next;
public Node child;
};
*/
class Solution {
public Node flatten(Node head) {
Node cur=head;
while(cur!=null){
//可能后面级插入上来的child也不为null
if(cur.child!=null){
//保存cur的下一个节点,便于插入后的拼接
Node temp=cur.next;
//改变cur的指向,双向改变
Node childnode=cur.child;
cur.next=childnode;
childnode.prev=cur;
cur.child=null;
//找到child的最后一个节点
Node last=cur;
while(last.next!=null){
last=last.next;
}
//将child最后一个节点连接到第一级上
last.next=temp;
if(temp!=null) temp.prev=last;
}
//同时可以查看后面拼接上来的节点的child
cur=cur.next;
}
return head;
}
}
29.排序的循环链表
参考题解,考虑到五种情况,while(ptr->next != head)
是循环链表循环查找的方式。从头开始查找插入的位置ptr
,当查到1.val在当前节点ptr
下一个点中间 2.最大值最小值 3.循环到头结束(1个元素或者都是相同元素),就在ptr
后面插入元素,实际上就是找到链表插入基本操作里的pre
节点了。
/*
// Definition for a Node.
class Node {
public int val;
public Node next;
public Node() {}
public Node(int _val) {
val = _val;
}
public Node(int _val, Node _next) {
val = _val;
next = _next;
}
};
*/
class Solution {
public Node insert(Node head, int insertVal) {
//列表为空
if(head==null){
Node newNode=new Node(insertVal);
newNode.next=newNode;
return newNode;
}
Node ptr=head;
//循环到头结束
while(ptr.next!=head){
//val在当前节点ptr和下一个点中间
if(insertVal>=ptr.val&&insertVal<=ptr.next.val) break;
//在链表首尾相连处找到最大值最小值
if(ptr.val>ptr.next.val&&(insertVal>=ptr.val||insertVal<=ptr.next.val)) break;
ptr=ptr.next;
}
Node newNode=new Node(insertVal);
newNode.next=ptr.next;
ptr.next=newNode;
return head;
}
}
30.插入删除和随机访问都是O(1)的容器
HashMap+数组,HashMap在插入和删除时都为 O ( 1 ) O(1) O(1),但是删除的时候没法保证,因此本题将两者的优势结合起来,同时更新一个元素的HashMap和数组,它们的桥梁就是元素在数组中的下标,因此HashMap的value存的也是下标。
class RandomizedSet {
int[]array=new int[(int)2e5];
int idx=-1;
HashMap<Integer,Integer>map=new HashMap<>();
Random random=new Random();
public RandomizedSet() {
}
public boolean insert(int val) {
if(map.containsKey(val)) return false;
else{
// array和map同时记录
array[++idx]=val;
map.put(val,idx);
return true;
}
}
public boolean remove(int val) {
if (!map.containsKey(val)) return false;
else{
int loc=map.remove(val);
// 需要进行元素位置交换,填补删去的loc
if (loc==idx){ // 删去的元素本身就在最后
idx--;
}else{ // array用最后一个元素填补删去的loc位置,map放上最后一个元素
map.put(array[idx],loc);
array[loc]=array[idx];
idx--;
}
return true;
}
}
public int getRandom() {
return array[(int)random.nextInt(idx+1)];
}
}
/**
* Your RandomizedSet object will be instantiated and called as such:
* RandomizedSet obj = new RandomizedSet();
* boolean param_1 = obj.insert(val);
* boolean param_2 = obj.remove(val);
* int param_3 = obj.getRandom();
*/
31.最近最少使用缓存
HashMap+造轮子实现双向链表
hashmap查找一个节点的性能为O(1)-->get
;链表对一个节点增删操作的时间为O(1)
,在已经得到要删除的节点的引用时,双向链表对一个节点的增删最方便,直接改变该节点的两个指向,性能为O(1)-->put、delete
。两个数据结构增删改查的时候同步更新,联系就是hashmap为HashMap<Integer,Node>,value是key在双向链表中代表的节点引用Node。
get()和put()都是需要进行使用更新的操作,本质上get()就是将原来的key-value再重新put()一次,因此get()可以复用put()进行更新
。put()在更新时,始终要将最近使用的节点通过addFirst()放在第一个节点上,如果有相同的key节点存在则需要先删除原先的节点,如果没有相同的key的存在,则判断是否已经满了,满了的话就先删除最后一个节点,然后将新节点放在双向链表头上。
双向链表的delete()和addFirst()
方法传入参数都是Node引用,便于双向链表直接操作。其中delete()
可以返回删除节点的key,便于紧接着根据key删除其在map里对应的node。
class LRUCache {
HashMap<Integer,Node> map;
DoubleLinkedList cache;
int cap;
public LRUCache(int capacity) {
map=new HashMap<>();
cache=new DoubleLinkedList();
cap=capacity;
}
public class Node{
int k,v;
Node pre,next;
public Node(int _k, int _v){
k=_k;
v=_v;
}
}
public class DoubleLinkedList{
Node head,tail;
public DoubleLinkedList(){
head=new Node(-1,-1);
tail=new Node(-1,-1);
head.next=tail;
tail.pre=head;
}
public int delete(Node n){
if(head.next==tail) return -1;
n.next.pre=n.pre;
n.pre.next=n.next;
return n.k;
}
public int deleteLast(){
return delete(tail.pre);
}
public void addFirst(Node newnode){
newnode.next=head.next;
newnode.pre=head;
head.next.pre=newnode;
head.next=newnode;
}
}
public int get(int key) {
if(!map.containsKey(key))return -1;
else{
int value=map.get(key).v;
//往双向链表put相同的value
put(key,value);
return value;
}
}
public void put(int key, int value) {
Node newnode=new Node(key,value);
//有相同key的节点进行更新替换
if(map.containsKey(key)){
//删除原先双向链表中的该节点,进行更新
cache.delete(map.get(key));
}else{ // 没有相同key的节点
//已经存满了,删除最后未使用的节点
if(map.size()==cap){
int k=cache.deleteLast();
map.remove(k);
}
}
//一份newnode关联了两个数据结构
map.put(key,newnode);
cache.addFirst(newnode);
}
}
/**
* 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);
*/
32.有效的变位词
直接哈希表记录记录两字符串各个字符是否相等,当仅包含小写字母的时候,所有字符是已知的,字典大小初始化为小写字母个数26。当为Unicode字符的时候,则字符很难全部表示,可以建立HashMap。其中题目要求不完全相同,因此需要把相同的字符排除。
class Solution {
public boolean isAnagram(String s, String t) {
HashMap<Character,Integer> map=new HashMap<>();
int len1=s.length();
int len2=t.length();
//不完全相同,因此需要把相同的字符排除
if(len1!=len2||s.equals(t)) return false;
for(int i=0;i<len1;i++){
char chs=s.charAt(i);
map.put(chs,map.getOrDefault(chs,0)+1);
}
for(int i=0;i<len2;i++){
char cht=t.charAt(i);
map.put(cht,map.getOrDefault(cht,0)-1);
if(map.get(cht)<0) return false;
}
return true;
}
}
33.变位词组
采用HashMap key记录相同的变位词,每个字符出现次数相同而不要求顺序,那么我们可以对每个词组内部排序,然后去查找是否存在对应的key,有则在此key对应的value内增加,否则新增一条记录。最终将map的value全部返回。
class Solution {
public List<List<String>> groupAnagrams(String[] strs) {
HashMap<String,List<String>> map=new HashMap<>();
for(String s:strs){
char[] str=s.toCharArray();
//返回void
Arrays.sort(str);
String key=new String(str);
List<String> list=map.getOrDefault(key,new ArrayList<String>());
list.add(s);
map.put(key,list);
}
return new ArrayList<List<String>>(map.values());
}
}
34.外星语言是否排序
用HashMap记录字典顺序的方法很精妙,然后采用双对象双指针的加法模板对两个单词进行逐位比较,从前到后按顺序一一比较。
class Solution {
public boolean isAlienSorted(String[] words, String order) {
HashMap<Character,Integer> map=new HashMap<>();
for(int i=0;i<order.length();i++){
map.put(order.charAt(i),i);
}
int len=words.length;
for(int i=0;i<len-1;i++){
String word1=words[i];
String word2=words[i+1];
int len1=word1.length();
int len2=word2.length();
int m=0;
int n=0;
while(m<len1||n<len2){
int cm=m<len1?map.get(word1.charAt(m)):-1;
int cn=n<len2?map.get(word2.charAt(n)):-1;
//只要前面的小了,就可以不看后面的了
if(cm<cn) break;
//大于才需要return false,特殊情况是一直等于就一直往后看
if(cm>cn) return false;
m++;
n++;
}
}
return true;
}
}
35.最小的时间差
将时间转换为分钟数,方便排序和做差,同时将最小时间加上24*60,以便循环做差。
class Solution {
public int findMinDifference(List<String> timePoints) {
int len=timePoints.size();
ArrayList<Integer> time=new ArrayList<>();
for(int i=0;i<len;i++){
String[] hm=timePoints.get(i).split(":");
time.add(Integer.parseInt(hm[0])*60+Integer.parseInt(hm[1]));
}
Collections.sort(time);
//最小值+24*60可以计算循环的相差时间
time.add(time.get(0)+24*60);
int ans=0x3f3f3f3f;
for(int i=0;i<len;i++){
int res=time.get(i+1)-time.get(i);
ans=Math.min(ans,res);
}
return ans;
}
}
36.后缀表达式
经典栈
class Solution {
public int evalRPN(String[] tokens) {
Stack<Integer>st=new Stack<>();
for(String t:tokens){
if(t.equals("+")){
st.push(st.pop()+st.pop());
}else if(t.equals("-")){
st.push(-st.pop()+st.pop());
}else if(t.equals("*")){
st.push(st.pop()*st.pop());
}else if(t.equals("/")){
int a=st.pop();
int b=st.pop();
st.push(b/a);
}else{
st.push(Integer.valueOf(t));
}
}
return st.pop();
}
}
37.小行星碰撞
栈+标志位,控制元素的进出。
class Solution {
public int[] asteroidCollision(int[] asteroids) {
Stack<Integer> st=new Stack<>();
for(int a:asteroids){
boolean flag=true;
while(flag&&!st.isEmpty()&&st.peek()>0&&a<0){
int mover=st.peek();
int movel=-a;
if(mover<=movel) st.pop();
//a不加入,查看下一个a
if(mover>=movel) flag=false;
}
if(flag) st.push(a);
}
int[]res=st.stream().mapToInt(x->x).toArray();
return res;
}
}
38.每日温度
单调递减栈应用,存储的是数组下标。
class Solution {
public int[] dailyTemperatures(int[] temperatures) {
int len=temperatures.length;
int[]res=new int[len];
Stack<Integer> st=new Stack<>();
for(int i=0;i<len;i++){
while(!st.isEmpty()&&temperatures[i]>temperatures[st.peek()]){
int top=st.pop();
res[top]=i-top;
}
st.push(i);
}
return res;
}
}
39.直方图最大矩形面积
单调递增栈
class Solution {
public int largestRectangleArea(int[] heights) {
int len=heights.length;
int[] newHeight=new int[len+2];
for(int i=0;i<len;i++){
newHeight[i+1]=heights[i];
}
long res=0;
Stack<Integer> st=new Stack<>();
for(int i=0;i<len+2;i++){
while(!st.isEmpty()&&newHeight[i]<newHeight[st.peek()]){
int top=st.pop();
int h=newHeight[top];
int w=i-st.peek()-1;
res=Math.max(res,1L*h*w);
}
st.push(i);
}
return (int)res;
}
}
40.矩阵中的最大矩形
不同于用DFS求最大的1的面积,这里需要保证它是矩形。转换为,针对每一行,计算以该行为底部向上的柱状图中连续的最大的矩形面积,整体复杂度为O(mn)
。
class Solution {
public int maximalRectangle(String[] matrix) {
int m=matrix.length;
if(m==0) return 0;
int n=matrix[0].length();
int res=0;
int[]heights=new int[n];
for(int i=0;i<m;i++){
for(int j=0;j<n;j++){
if(matrix[i].charAt(j)=='0') heights[j]=0;
else heights[j]+=1;
}
res=Math.max(res,largestRectangleArea(heights));
}
return res;
}
//柱状图中的最大面积
public int largestRectangleArea(int[] heights) {
int len=heights.length;
int[] newHeight=new int[len+2];
for(int i=0;i<len;i++){
newHeight[i+1]=heights[i];
}
long res=0;
Stack<Integer> st=new Stack<>();
for(int i=0;i<len+2;i++){
while(!st.isEmpty()&&newHeight[i]<newHeight[st.peek()]){
int top=st.pop();
int h=newHeight[top];
int w=i-st.peek()-1;
res=Math.max(res,1L*h*w);
}
st.push(i);
}
return (int)res;
}
}
41.滑动窗口的平均值
双端队列,用数组模拟。
class MovingAverage {
int[]que=new int[10010];
int start;
int end;
int n;
int sum=0;
/** Initialize your data structure here. */
public MovingAverage(int size) {
n=size;
}
public double next(int val) {
que[end++]=val;
sum+=val;
if(end-start>n) sum-=que[start++];
return 1.0*sum/(end-start);
}
}
/**
* Your MovingAverage object will be instantiated and called as such:
* MovingAverage obj = new MovingAverage(size);
* double param_1 = obj.next(val);
*/
42.最近请求次数
注意到调用时间严格递增,因此可以用双端队列先进先出的性质,将时间小于t-3000的出队列,最后返回队列长度。
class RecentCounter {
int slot;
Deque<Integer> que;
public RecentCounter() {
slot=3000;
que=new ArrayDeque<>();
}
public int ping(int t) {
que.offer(t);
int start=t-3000;
while(!que.isEmpty()&&que.peek()<start) que.poll();
return que.size();
}
}
/**
* Your RecentCounter object will be instantiated and called as such:
* RecentCounter obj = new RecentCounter();
* int param_1 = obj.ping(t);
*/
43.往完全二叉树添加节点
采用和搜索层数无关的广度优先搜索,找到某个节点的左节点或者右节点为空,则将新节点插入到其左节点或者右节点上。后面的查询可以从该点开始查,不需要每次重新入队,将前面已经出队列的点再入队一次,因此可以将该点放入队头。
/**
* Definition for a binary tree node.
* public class TreeNode {
* int val;
* TreeNode left;
* TreeNode right;
* TreeNode() {}
* TreeNode(int val) { this.val = val; }
* TreeNode(int val, TreeNode left, TreeNode right) {
* this.val = val;
* this.left = left;
* this.right = right;
* }
* }
*/
class CBTInserter {
TreeNode root;
ArrayDeque<TreeNode> que;
public CBTInserter(TreeNode _root) {
root=_root;
que=new ArrayDeque<>();
}
public int insert(int v) {
TreeNode newNode=new TreeNode(v);
que.offer(root);
while(!que.isEmpty()){
TreeNode top=que.poll();
if(top.left==null){
top.left=newNode;
que.offerFirst(top);
}else if(top.right==null){
top.right=newNode;
que.offerFirst(top);
}else{
que.offer(top.left);
que.offer(top.right);
continue;
}
return top.val;
}
return -1;
}
public TreeNode get_root() {
return root;
}
}
/**
* Your CBTInserter object will be instantiated and called as such:
* CBTInserter obj = new CBTInserter(root);
* int param_1 = obj.insert(v);
* TreeNode param_2 = obj.get_root();
*/
44.二叉树每层的最大值
经典层序遍历
/**
* Definition for a binary tree node.
* public class TreeNode {
* int val;
* TreeNode left;
* TreeNode right;
* TreeNode() {}
* TreeNode(int val) { this.val = val; }
* TreeNode(int val, TreeNode left, TreeNode right) {
* this.val = val;
* this.left = left;
* this.right = right;
* }
* }
*/
class Solution {
public List<Integer> largestValues(TreeNode root) {
ArrayList<Integer> ans=new ArrayList<>();
if(root==null) return ans;
Deque<TreeNode> que=new LinkedList<>();
que.offer(root);
while(!que.isEmpty()){
int size=que.size();
int maxV=Integer.MIN_VALUE;
for(int i=0;i<size;i++){
TreeNode top=que.poll();
maxV=Math.max(maxV,top.val);
if(top.left!=null) que.offer(top.left);
if(top.right!=null) que.offer(top.right);
}
ans.add(maxV);
}
return ans;
}
}
45.二叉树最底层最左边的值
同样是层次遍历,记录最后一层最左边的数字,只需要用一个局部变量ans每次都更新为最左边的数字,最后就是最后一层最左边的数字。
/**
* Definition for a binary tree node.
* public class TreeNode {
* int val;
* TreeNode left;
* TreeNode right;
* TreeNode() {}
* TreeNode(int val) { this.val = val; }
* TreeNode(int val, TreeNode left, TreeNode right) {
* this.val = val;
* this.left = left;
* this.right = right;
* }
* }
*/
class Solution {
public int findBottomLeftValue(TreeNode root) {
Deque<TreeNode> que=new ArrayDeque<>();
que.offer(root);
int ans=-1;
while(!que.isEmpty()){
int size=que.size();
for(int i=0;i<size;i++){
TreeNode top=que.poll();
if(i==0) ans=top.val;
if(top.left!=null) que.offer(top.left);
if(top.right!=null) que.offer(top.right);
}
}
return ans;
}
}
46.二叉树的右侧视图
经典层次遍历
/**
* Definition for a binary tree node.
* public class TreeNode {
* int val;
* TreeNode left;
* TreeNode right;
* TreeNode() {}
* TreeNode(int val) { this.val = val; }
* TreeNode(int val, TreeNode left, TreeNode right) {
* this.val = val;
* this.left = left;
* this.right = right;
* }
* }
*/
class Solution {
public List<Integer> rightSideView(TreeNode root) {
Deque<TreeNode> que=new ArrayDeque<>();
ArrayList<Integer> ans=new ArrayList<>();
if(root==null) return ans;
que.offer(root);
while(!que.isEmpty()){
int size=que.size();
for(int i=0;i<size;i++){
TreeNode top=que.poll();
if(i==size-1) ans.add(top.val);
if(top.left!=null) que.offer(top.left);
if(top.right!=null) que.offer(top.right);
}
}
return ans;
}
}
47.二叉树剪枝
后序遍历,从底向上进行二叉树剪枝。
/**
* Definition for a binary tree node.
* public class TreeNode {
* int val;
* TreeNode left;
* TreeNode right;
* TreeNode() {}
* TreeNode(int val) { this.val = val; }
* TreeNode(int val, TreeNode left, TreeNode right) {
* this.val = val;
* this.left = left;
* this.right = right;
* }
* }
*/
class Solution {
public TreeNode pruneTree(TreeNode root) {
if(root==null) return null;
root.left=pruneTree(root.left);
root.right=pruneTree(root.right);
if(root.val==0&&root.left==null&&root.right==null) return null;
return root;
}
}
48.二叉树的序列化和反序列化
树的先序遍历+递归,序列化按照"root,A,B,null,…"的形式存入,其中将null也存入,这样子可以唯一确定一棵树。反序列化的时候也是根据前面序列化的结果,先根据,
进行分割成数组,然后递归建树。
/**
* Definition for a binary tree node.
* public class TreeNode {
* int val;
* TreeNode left;
* TreeNode right;
* TreeNode(int x) { val = x; }
* }
*/
public class Codec {
// Encodes a tree to a single string.
public String serialize(TreeNode root) {
if(root==null){
return "null,";
}
// 递归构建
StringBuilder sb=new StringBuilder();
sb.append(root.val+",");
sb.append(serialize(root.left));
sb.append(serialize(root.right));
return sb.toString();
}
// Decodes your encoded data to tree.
public TreeNode deserialize(String data) {
LinkedList<String> revertList= new LinkedList<>(Arrays.asList(data.split(",")));
return buildTree(revertList);
}
// LinkedList<String> l传入的是引用,所有递归函数共用一个
public TreeNode buildTree(LinkedList<String> l){
// base情况
if (Objects.equals(l.getFirst(),"null")){
l.removeFirst();
return null;
}
String s=l.removeFirst();
TreeNode root=new TreeNode(Integer.parseInt(s));
root.left=buildTree(l);
root.right=buildTree(l);
return root;
}
}
// Your Codec object will be instantiated and called as such:
// Codec ser = new Codec();
// Codec deser = new Codec();
// TreeNode ans = deser.deserialize(ser.serialize(root));
49.从根节点到叶节点的路径数字之和
采用有返回值的dfs,该题还有无返回值dfs以及回溯的做法。
/**
* Definition for a binary tree node.
* public class TreeNode {
* int val;
* TreeNode left;
* TreeNode right;
* TreeNode() {}
* TreeNode(int val) { this.val = val; }
* TreeNode(int val, TreeNode left, TreeNode right) {
* this.val = val;
* this.left = left;
* this.right = right;
* }
* }
*/
class Solution {
public int sumNumbers(TreeNode root) {
return dfs(root,0);
}
// 代表root节点的树的所有到叶子节点的路径值
public int dfs(TreeNode root,int num){
if(root==null) return 0;
// 叶子结点返回路径数字num
if (root.left==null&&root.right==null){
return num*10+root.val;
}
// 非叶子结点返回它以下能达到的路径的数字的和
return dfs(root.left,num*10+root.val)+dfs(root.right,num*10+root.val);
}
}
50.向下的路径节点之和
双重dfs,将寻找路径之和的dfs2放入先序遍历的dfs1中,从每个节点开始找符合条件的值。
/**
* Definition for a binary tree node.
* public class TreeNode {
* int val;
* TreeNode left;
* TreeNode right;
* TreeNode() {}
* TreeNode(int val) { this.val = val; }
* TreeNode(int val, TreeNode left, TreeNode right) {
* this.val = val;
* this.left = left;
* this.right = right;
* }
* }
*/
class Solution {
int cnt=0;
public int pathSum(TreeNode root, int targetSum) {
dfs1(root,targetSum);
return cnt;
}
public void dfs1(TreeNode root, int target){
if (root==null) return;
dfs2(root,target);
dfs1(root.left,target);
dfs1(root.right,target);
}
public void dfs2(TreeNode root,long target){
long res=target-root.val;
if(res==0){
cnt++;
}
if (root.left!=null) dfs2(root.left,res);
if (root.right!=null) dfs2(root.right,res);
}
}
51.节点之和最大的路径
后序遍历,全局变量res记录结果,递归函数返回一侧最大值。
/**
* Definition for a binary tree node.
* public class TreeNode {
* int val;
* TreeNode left;
* TreeNode right;
* TreeNode() {}
* TreeNode(int val) { this.val = val; }
* TreeNode(int val, TreeNode left, TreeNode right) {
* this.val = val;
* this.left = left;
* this.right = right;
* }
* }
*/
class Solution {
int res=-0x3f3f3f3f;
public int maxPathSum(TreeNode root) {
dfs(root);
return res;
}
public int dfs(TreeNode root){
if(root==null) return 0;
// 只保留左右递归贡献为正的部分
int l=dfs(root.left);
l=Math.max(0,l);
int r=dfs(root.right);
r=Math.max(0,r);
// 记录答案
res=Math.max(res,l+r+root.val);
// 返回左右两侧的较大结果,别忘了加上根节点
return Math.max(l,r)+root.val;
}
}
52.展平二叉搜索树
中序遍历,通过dummyhead构建树免去了头节点的特殊判断
/**
* Definition for a binary tree node.
* public class TreeNode {
* int val;
* TreeNode left;
* TreeNode right;
* TreeNode() {}
* TreeNode(int val) { this.val = val; }
* TreeNode(int val, TreeNode left, TreeNode right) {
* this.val = val;
* this.left = left;
* this.right = right;
* }
* }
*/
class Solution {
TreeNode temp;
public TreeNode increasingBST(TreeNode root) {
// dummyHead一直不改变,让temp一直向下改变指向
TreeNode dummyHead=new TreeNode(-1);
temp=dummyHead;
dfs(root);
return dummyHead.right;
}
public void dfs(TreeNode root){
if(root==null){
return;
}
dfs(root.left);
// 这里开辟了新空间,所以下面left=null可以删掉,如果直接=root
temp.right=new TreeNode(root.val);
//System.out.println(temp.left);
//temp.left=null; // =root的话这里需要清理root.left
temp=temp.right;
dfs(root.right);
}
}
53.二叉搜索树中的中序后继
- 从根节点 root 开始,比较当前 cur 节点的值和节点 p 的值的大小关系
- 如果当前 cur 节点的值小于或等于节点 p 的值,那么比节点 p 的值大的节点应该在它的右子树
- 如果当前 cur 节点的值大于节点 p 的值,那么当前节点有可能是 p 的下一个节点,还需要判断当前 cur 节点的左节点是否满足以上条件
/**
* Definition for a binary tree node.
* public class TreeNode {
* int val;
* TreeNode left;
* TreeNode right;
* TreeNode(int x) { val = x; }
* }
*/
class Solution {
public TreeNode inorderSuccessor(TreeNode root, TreeNode p) {
TreeNode res=null;
TreeNode cur=root;
while(cur!=null){
System.out.println(cur.val);
if (cur.val<=p.val){
cur=cur.right;
}else{ // cur.val>p.val cur就有可能是要找的「下一个节点」
res=cur;
cur=cur.left; // 继续往下看是否有满足条件的更小值
}
}
return res;
}
}
54.所有大于等于节点的值之和
巧妙地利用了二叉搜索树的性质,对之前的中序遍历次序进行了调换,先dfs(right)再dfs(left)可以降序输出值,只需要用一个sum累加结果就可以。
/**
* Definition for a binary tree node.
* public class TreeNode {
* int val;
* TreeNode left;
* TreeNode right;
* TreeNode() {}
* TreeNode(int val) { this.val = val; }
* TreeNode(int val, TreeNode left, TreeNode right) {
* this.val = val;
* this.left = left;
* this.right = right;
* }
* }
*/
class Solution {
int sum;
public TreeNode convertBST(TreeNode root) {
dfs(root);
return root;
}
public void dfs(TreeNode root){
if (root==null) return;
dfs(root.right);
sum+=root.val;
root.val=sum;
dfs(root.left);
}
}
55.二叉搜索树迭代器
迭代法的二叉树中序遍历。
/**
* Definition for a binary tree node.
* public class TreeNode {
* int val;
* TreeNode left;
* TreeNode right;
* TreeNode() {}
* TreeNode(int val) { this.val = val; }
* TreeNode(int val, TreeNode left, TreeNode right) {
* this.val = val;
* this.left = left;
* this.right = right;
* }
* }
*/
class BSTIterator {
Stack<TreeNode> st;
TreeNode cur;
public BSTIterator(TreeNode root) {
cur=root;
st=new Stack<>();
}
public int next() {
while(cur!=null) {
st.push(cur);
cur=cur.left;
}
// 类比中序遍历,这里相当于既做了cur!=null入栈也做了cur==null弹栈的工作
TreeNode top=st.pop();
cur=top.right;
return top.val;
}
public boolean hasNext() {
return !(cur==null&&st.isEmpty());
}
}
/**
* Your BSTIterator object will be instantiated and called as such:
* BSTIterator obj = new BSTIterator(root);
* int param_1 = obj.next();
* boolean param_2 = obj.hasNext();
*/
56.二叉搜索树中两个节点之和
二叉树的先序递归遍历+Set,在遍历的过程中用类似两数之和的方法存下root.val的补数,如果刚好是补数就return true,否则存入补数并继续往下遍历。
这点很奇怪的点是,没用上二叉搜索树性质直接遍历+Set复杂度是 O ( N ) O(N) O(N),而用上搜索树性质中序遍历建list+双指针反倒是 2 ∗ O ( N ) 2*O(N) 2∗O(N)。
class Solution {
// 两数之和,存某个数的「补数」
HashSet<Integer> s=new HashSet<>();
public boolean findTarget(TreeNode root, int k) {
if(root==null) return false;
if(s.contains(root.val)) return true;
s.add(k-root.val);
return findTarget(root.left,k)||findTarget(root.right,k);
}
}
57.值和下标之差都在给定的范围内
桶排序+滑动窗口,整体还是滑动窗口,不过这个窗口维护了一个用HashMap实现的桶,能够在O(1)
的时间内找到是否存在abs(nums[i] - nums[j]) <= t
的值。
class Solution {
public boolean containsNearbyAlmostDuplicate(int[] nums, int k, int t) {
int n=nums.length;
// 桶中最大间隔为t,那么能容纳w个元素
int w=t+1;
// 用哈希表记录桶,每个桶内只需要暂存一个元素,因为一旦出现两个元素就返回true了
HashMap<Long,Long> mp=new HashMap<>();
for(int i=0;i<n;i++){
long id=getId(nums[i],w);
if(mp.containsKey(id)&&Math.abs(nums[i]-mp.get(id))<=t) return true;
if(mp.containsKey(id-1)&&Math.abs(nums[i]-mp.get(id-1))<=t) return true;
if(mp.containsKey(id+1)&&Math.abs(nums[i]-mp.get(id+1))<=t) return true;
mp.put(getId(nums[i],w),(long)nums[i]);
// 滑动窗口,只考虑下标差在k之间的数字所放置的桶
if(i>=k) mp.remove(getId(nums[i-k],w));
}
return false;
}
// 获取桶的key
public long getId(long x,long w){
if(x>=0) return x/w;
else return (x+1)/w-1;
}
}
58.日程表
通过构建二叉搜索树可以很巧妙地解决这题,将第一个book作为根节点,每次查询的时候进行建立。
class TreeNode{
int start;
int end;
TreeNode left;
TreeNode right;
public TreeNode(int start,int end){
this.start=start;
this.end=end;
}
}
public class MyCalendar {
TreeNode root;
public MyCalendar() {
root=null;
}
public boolean book(int start, int end) {
if(root==null){
root=new TreeNode(start,end);
return true;
}
TreeNode cur=root;
while(true){
if(end<=cur.start){
if(cur.left==null){
cur.left=new TreeNode(start,end);
return true;
}
cur=cur.left;
}else if(start>=cur.end){
if(cur.right==null){
cur.right=new TreeNode(start,end);
return true;
}
cur=cur.right;
}else{
return false;
}
}
}
}
/**
* Your MyCalendar object will be instantiated and called as such:
* MyCalendar obj = new MyCalendar();
* boolean param_1 = obj.book(start,end);
*/
59.数据流的第K大数值
维护一个K大小的小顶堆,在往里面add的时候不断进行检查维护,最后堆顶元素就是答案。
class KthLargest {
PriorityQueue<Integer> q=new PriorityQueue<>();
int k;
public KthLargest(int _k, int[] nums) {
k=_k;
for(int i:nums){
q.offer(i);
}
}
public int add(int val) {
q.offer(val);
// 维护一个K大小的小顶堆
while(q.size()>k){
q.poll();
}
// 堆顶元素就是第K大元素
return q.peek();
}
}
/**
* Your KthLargest object will be instantiated and called as such:
* KthLargest obj = new KthLargest(k, nums);
* int param_1 = obj.add(val);
*/
60.出现频率最高的k个数字
哈希表+最小堆。
class Solution {
public int[] topKFrequent(int[] nums, int k) {
// 哈希表存频率
HashMap<Integer,Integer> mp=new HashMap<>();
for(int i:nums){
mp.put(i,mp.getOrDefault(i,0)+1);
}
// 最小堆存频率最高的K个数
PriorityQueue<int[]> pq=new PriorityQueue<>((a,b)->(a[1]-b[1]));
for(Map.Entry<Integer,Integer> m:mp.entrySet()){
if(pq.size()==k){
if(m.getValue()>=pq.peek()[1]){
pq.poll();
pq.offer(new int[]{m.getKey(),m.getValue()});
}
}else{
pq.offer(new int[]{m.getKey(),m.getValue()});
}
}
int[]ans=new int[k];
for(int i=0;i<k;++i){
ans[i]=pq.poll()[0];
}
return ans;
}
}
61.和最小的k个数对
最大堆,根据数对和进行排序。
class Solution {
public List<List<Integer>> kSmallestPairs(int[] nums1, int[] nums2, int k) {
PriorityQueue<int[]> pq=new PriorityQueue<>((a,b)->(b[0]+b[1])-(a[0]+a[1]));
int len1=Math.min(nums1.length,k);
int len2=Math.min(nums2.length,k);
for(int i=0;i<len1;i++){
for(int j=0;j<len2;j++){
if(pq.size()==k){
if(nums1[i]+nums2[j]<pq.peek()[0]+pq.peek()[1]){
pq.poll();
pq.offer(new int[]{nums1[i],nums2[j]});
}
}else{
pq.offer(new int[]{nums1[i],nums2[j]});
}
}
}
List<List<Integer>> res=new ArrayList<>();
while(!pq.isEmpty()){
int[] top=pq.poll();
res.add(Arrays.asList(top[0],top[1]));
}
return res;
}
}
68.查找插入位置
二分查找
class Solution {
//查找第一个大于等于target的位置
public int searchInsert(int[] nums, int target) {
int left=0;
int right=nums.length;
while(left<right){
int mid=(left+right)/2;
if(nums[mid]>=target){
right=mid;
}else{
left=mid+1;
}
}
return left;
}
}
69.山峰数组的顶部
需要注意二分查找的唯一一种写法。
class Solution {
public int peakIndexInMountainArray(int[] arr) {
int len=arr.length;
int left=1;
int right=len-2;
while(left<right){
int mid=(left+right+1)/2;
if(arr[mid-1]<=arr[mid]){
left=mid;
}else{
// arr[mid-1]>arr[mid] right=mid-1合理,因此只能有left=mid的写法
// 否则会存在arr[mid-1]<arr[mid] left=mid+1的情况,忽略了mid也是正确的
right=mid-1;
}
}
return left;
}
}
70.排序数组中只出现一次的数字
二分查找,运用奇偶两段性
class Solution:
def singleNonDuplicate(self, nums: List[int]) -> int:
left=0
right=len(nums)-1
while left<right:
mid =(left+right)//2
# 奇数和前一个比较,偶数和后一个比较,如果比较的前面都是出现两次的话,两个值应该是相等的
if nums[mid]!=nums[mid^1]:
right=mid
else:
left=mid+1
return nums[left]
71.按权重生成随机数
二分查找,找大于等于preSum的第一个数
class Solution:
def __init__(self, w: List[int]):
self.preSum=[0]*(len(w)+1)
self.preSum[0]=0
for i in range(len(w)):
self.preSum[i+1]=self.preSum[i]+w[i]
def pickIndex(self) -> int:
# 产生一个随机数
seed = random.randint(1,self.preSum[-1])
# 在前缀和数组preSum中找到第一个大于等于seed的下标-1
left=0
right=len(self.preSum)
while left<right:
mid = (left+right)//2
if self.preSum[mid]<seed:
left=mid+1
else:
right=mid
return left-1
# Your Solution object will be instantiated and called as such:
# obj = Solution(w)
# param_1 = obj.pickIndex()
72.求平方根
二分查找只能right=mid-1
class Solution:
def mySqrt(self, x: int) -> int:
left=0
right=x
while left<right:
mid=left+(right-left+1)//2
if mid>x//mid:
right=mid-1
else:
left=mid
return left
73.狒狒吃香蕉
二分查找,对速度进行二分,找<=h的第一个数
class Solution:
def minEatingSpeed(self, piles: List[int], h: int) -> int:
def check(piles: List[int],speed:int):
cnt=0
for p in piles:
if p <= speed:
cnt +=1
else:
cnt += math.ceil(p/speed)
return cnt
left=1
right=max(piles)
while left < right:
mid= (left+right)//2
hour=check(piles,mid)
if hour>h:
left=mid+1
else:
right=mid
return left
74.合并区间
排序+模拟
class Solution {
public int[][] merge(int[][] intervals) {
ArrayList<int[]> ans=new ArrayList<>();
Arrays.sort(intervals,(a,b)->Integer.compare(a[0],b[0]));
int len=intervals.length;
int start=intervals[0][0];
int end=intervals[0][1];
for(int i=1;i<len;i++){
if(intervals[i][0]>end){
ans.add(new int[]{start,end});
start=intervals[i][0];
end=intervals[i][1];
}else{
end=Math.max(end,intervals[i][1]);
}
}
ans.add(new int[]{start,end});
return ans.toArray(new int[0][0]);
}
}
75.数组相对排序
自定义比较函数,ans.sort(),时间复杂度 O ( N l o g N ) O(NlogN) O(NlogN)
class Solution {
public int[] relativeSortArray(int[] arr1, int[] arr2) {
HashMap<Integer,Integer> mp=new HashMap<>();
int len=arr2.length;
for(int i=0;i<len;i++){
mp.put(arr2[i],i);
}
// int[]转List<Integer>
List<Integer> ans= Arrays.stream(arr1).boxed().collect(Collectors.toList());
ans.sort(new Comparator<Integer>(){
@Override
public int compare(Integer a,Integer b){
if(mp.containsKey(a)&&mp.containsKey(b)){
return mp.get(a)-mp.get(b);
}else if(mp.containsKey(a)){
return -1;
}else if(mp.containsKey(b)){
return 1;
}else{
return a-b;
}
}
});
// List<Integer> 转int[]
return ans.stream().mapToInt(x->x).toArray();
}
}
或者计数排序,时间复杂度 O ( N ) O(N) O(N)
class Solution {
public int[] relativeSortArray(int[] arr1, int[] arr2) {
// 计数排序
int[]count=new int[1001];
for(int i:arr1){
count[i]++;
}
int[]ans=new int[arr1.length];
int k=0;
// 先按顺序排arr2的
for(int a:arr2){
while(count[a]!=0){
ans[k++]=a;
count[a]--;
}
}
// 然后按照数字大小排arr1剩下的
for(int i=0;i<count.length;i++){
while(count[i]!=0){
ans[k++]=i;
count[i]--;
}
}
return ans;
}
}
76.数组中的第k大的数字
二分减治+快速选择,根据主定理 T ( n ) = T ( n / 2 ) + O ( n ) T(n)=T(n/2)+O(n) T(n)=T(n/2)+O(n)得到时间复杂度为 O ( N ) O(N) O(N)。
class Solution {
public int findKthLargest(int[] nums, int k) {
int len=nums.length;
int target=len-k;
int left=0;
int right=len-1;
// 二分减治
while(left<=right){
int mid=partition(nums,left,right);
if(mid==target){
return nums[target];
}else if(mid>target){
right=mid-1;
}else{
left=mid+1;
}
}
return 0;
}
// 快速选择可以确定一个元素的最终位置
// 就是[left,right]这个区间内和pivot进行比较,从前往后、从后往前找到冲突swap
public int partition(int[]nums,int left,int right){
int rd=new Random().nextInt(right-left+1)+left;
swap(nums,left,rd);
int pivot=nums[left];
int le=left+1;
int ge=right;
while(true){
while(le<=ge&&nums[le]<=pivot)le++;
while(le<=ge&&nums[ge]>=pivot)ge--;
if(le>=ge) break;
swap(nums,le,ge);
le++;
ge--;
}
swap(nums,left,ge);
return ge;
}
public void swap(int[]nums,int left,int right){
int temp=nums[right];
nums[right]=nums[left];
nums[left]=temp;
}
}
77.链表排序
链表的归并排序,需要实现「找中间节点」的拆分以及两个排序链表的合并函数
/**
* 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 sortList(ListNode head) {
if(head==null) return null;
if(head.next==null) return head;
// 划分成两半
ListNode mid=split(head);
ListNode list2=mid.next;
mid.next=null;
//归并排序
ListNode left=sortList(head);
ListNode right=sortList(list2);
return merge(left,right);
}
// 找到中间节点进行划分
public ListNode split(ListNode head){
ListNode fast=head;
ListNode slow=head;
while(fast.next!=null&&fast.next.next!=null){
fast=fast.next.next;
slow=slow.next;
}
return slow;
}
// 合并两个已经排序的列表
public ListNode merge(ListNode list1,ListNode list2){
ListNode dummyhead=new ListNode(-1);
ListNode cur=dummyhead;
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 dummyhead.next;
}
}
78.合并排序链表
归并排序,前面77是针对一个链表进行排序,这里是针对K个链表整合起来进行排序,粒度不同。拆分函数是将多个链表进行拆分,而合并函数还是合并两个排序链表。时间复杂度 O ( n k l o g ( n k ) ) O(nklog(nk)) O(nklog(nk))
class Solution {
public ListNode mergeKLists(ListNode[] lists) {
if(lists==null||lists.length==0){
return null;
}
if(lists.length==1){
return lists[0];
}
return splitSort(lists,0,lists.length-1);
}
// 归并排序
public ListNode splitSort(ListNode[] lists, int start, int end){
if(start>end) return null;
if(start==end){
return lists[start];
}
int mid=start+((end-start)>>1);
ListNode left=splitSort(lists,start,mid);
ListNode right=splitSort(lists,mid+1,end);
// 合并两个有序链表
return merge(left,right);
}
// 合并两个链表
public ListNode merge(ListNode list1,ListNode list2){
ListNode dummyhead=new ListNode(-1);
ListNode cur=dummyhead;
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 dummyhead.next;
}
}
79.所有子集
经典回溯,子集问题,数组中元素互不相同
class Solution {
LinkedList<Integer> path;
List<List<Integer>> ans;
public List<List<Integer>> subsets(int[] nums) {
path=new LinkedList<>();
ans=new ArrayList<>();
backTracing(nums,0);
return ans;
}
public void backTracing(int[]nums,int startIndex){
// 叶子节点和非叶子节点直接加入
ans.add(new ArrayList<Integer>(path));
// 叶子节点返回
if(startIndex==nums.length){
return;
}
for(int i=startIndex;i<=nums.length-1;i++){
path.add(nums[i]);
backTracing(nums,i+1);
path.removeLast();
}
}
}
80.含有k个元素的组合
经典回溯,组合问题,找满足个数的组合
class Solution {
LinkedList<Integer> path;
List<List<Integer>> res;
public List<List<Integer>> combine(int n, int k) {
path=new LinkedList<>();
res=new ArrayList<>();
backTracing(n,k,1);
return res;
}
public void backTracing(int n,int k, int startIndex){
// 改条件下return,因为已经没有搜索下去的必要了
if(path.size()==k){
res.add(new ArrayList<>(path));
return;
}
// 适当的剪枝
for(int i=startIndex;i<=n-(k-path.size())+1;i++){
path.add(i);
backTracing(n,k,i+1);
path.removeLast();
}
}
}
81.允许重复选择元素的组合
回溯,组合总和,可以重复选择元素
class Solution {
LinkedList<Integer> path;
int sum;
List<List<Integer>> res;
public List<List<Integer>> combinationSum(int[] candidates, int target) {
path=new LinkedList<>();
res=new ArrayList<>();
sum=0;
backTracing(candidates,0,target);
return res;
}
public void backTracing(int[]candidates,int startIndex, int target){
if(target==sum){
res.add(new ArrayList<>(path));
return;
}
// 多了个return的条件
if(target<sum){
return;
}
for(int i=startIndex;i<candidates.length;i++){
path.add(candidates[i]);
sum+=candidates[i];
// 可以重复选取元素,则下一次递归的startIndex可以不用+1
backTracing(candidates,i,target);
sum-=candidates[i];
path.removeLast();
}
}
}
82.含有重复元素集合的组合
回溯,组合总和,重复元素,需要先对数组进行排序,然后避免同层之间的元素被反复取到,甚至不需要用used数组标记。
带used的做法:
class Solution {
boolean[] used;
List<List<Integer>> res;
LinkedList<Integer> path;
int sum;
public List<List<Integer>> combinationSum2(int[] candidates, int target) {
Arrays.sort(candidates);
used=new boolean[candidates.length];
res=new ArrayList<>();
path=new LinkedList<>();
backTracing(candidates,target,0);
return res;
}
public void backTracing(int[] candidates, int target,int startIndex){
if(sum==target){
res.add(new ArrayList<>(path));
return;
}
if(sum>target){
return;
}
for(int i=startIndex;i<candidates.length;i++){
// 保证在同一层比较
if(i>startIndex&&candidates[i]==candidates[i-1]&&used[i-1]==false) continue;
else{
sum+=candidates[i];
used[i]=true;
path.addLast(candidates[i]);
backTracing(candidates,target,i+1);
path.removeLast();
used[i]=false;
sum-=candidates[i];
}
}
}
}
不带used做法:
class Solution {
List<List<Integer>> res;
LinkedList<Integer> path;
int sum;
public List<List<Integer>> combinationSum2(int[] candidates, int target) {
Arrays.sort(candidates);
res=new ArrayList<>();
path=new LinkedList<>();
backTracing(candidates,target,0);
return res;
}
public void backTracing(int[] candidates, int target,int startIndex){
if(sum==target){
res.add(new ArrayList<>(path));
return;
}
if(sum>target){
return;
}
for(int i=startIndex;i<candidates.length;i++){
if(i>startIndex&&candidates[i]==candidates[i-1]) continue;
else{
sum+=candidates[i];
path.addLast(candidates[i]);
backTracing(candidates,target,i+1);
path.removeLast();
sum-=candidates[i];
}
}
}
}
83.没有重复元素集合的全排列
回溯+排列问题+没有重复元素,排列问题不需要startIndex,每轮都从0开始取数,但是需要有used数组标记已经取过的数。
class Solution {
LinkedList<Integer> path;
List<List<Integer>> res;
boolean[] used;
public List<List<Integer>> permute(int[] nums) {
path=new LinkedList<>();
res=new ArrayList<>();
used=new boolean[nums.length];
backTracing(nums);
return res;
}
public void backTracing(int[] nums){
if(path.size()==nums.length){
res.add(new ArrayList<>(path));
return;
}
// 排列问题不用startIndex
for(int i=0;i<nums.length;i++){
// 需要跳过已经使用过的元素
if(used[i]==true) continue;
path.add(nums[i]);
used[i]=true;
backTracing(nums);
used[i]=false;
path.removeLast();
}
}
}
84.含有重复元素集合的全排列
回溯+排列问题+有重复元素,对于同一层的重复元素需要continue(不同于组合问题,这里需要used来判断的原因是,nums[i]==nums[i-1]可能出现在不同层中,因为82题i>startIndex已经限制在同一层上了),两题都是相信前面的树枝已经取到过所有情况了,排列问题没有startIndex。
class Solution {
LinkedList<Integer> path=new LinkedList<>();
List<List<Integer>> res=new ArrayList<>();
boolean[]used;
public List<List<Integer>> permuteUnique(int[] nums) {
used=new boolean[nums.length];
// 重复元素需要排序
Arrays.sort(nums);
backTracing(nums);
return res;
}
public void backTracing(int[]nums){
if(path.size()==nums.length){
res.add(new ArrayList<>(path));
return;
}
for(int i=0;i<nums.length;i++){
// 碰到同一层的重复元素(nums[i]==nums[i-1]&&used[i-1]==false)则continue
if(i>0&&nums[i]==nums[i-1]&&used[i-1]==false) continue;
// 元素已经使用过也continue
if(used[i]==true) continue;
path.add(nums[i]);
used[i]=true;
backTracing(nums);
used[i]=false;
path.removeLast();
}
}
}
85.生成匹配的括号
回溯+排列问题,这里需要检验括号的有效性,因此在回溯的过程中需要进行数量的判断。
class Solution {
int cur;
LinkedList<Character> path;
List<String> res;
public List<String> generateParenthesis(int n) {
path=new LinkedList<>();
res=new ArrayList<>();
backTracing(n,n);
return res;
}
public void backTracing(int left,int right){
// 括号使用完记录结果
if(left==0&&right==0){
StringBuilder sb=new StringBuilder();
for(Character c:path){
sb.append(c);
}
res.add(sb.toString());
return;
}
// 右边使用的括号不能多于左边使用的括号
if(right<left){
return;
}
// 记录左边使用的括号
if(left>0){
left--;
path.addLast('(');
backTracing(left,right);
path.removeLast();
left++;
}
// 记录右边使用的括号
if(right>0){
right--;
path.addLast(')');
backTracing(left,right);
path.removeLast();
right++;
}
}
}
86.分割回文子字符串
回溯+分割问题,对各种分割情况进行回文判断,只有满足回文才继续递归下去,最坏的情况为 O ( N ∗ 2 N ) O(N*2^N) O(N∗2N).
class Solution {
LinkedList<String> path;
List<List<String>> res;
public String[][] partition(String s) {
path=new LinkedList<>();
res=new ArrayList<>();
backTracing(s,0);
// 转换成结果的形式
String[][]ans=new String[res.size()][];
for(int i=0;i<res.size();i++){
int len=res.get(i).size();
ans[i]=new String[len];
for(int j=0;j<len;j++){
ans[i][j]=res.get(i).get(j);
}
}
return ans;
}
public void backTracing(String s, int startIndex){
if(startIndex>=s.length()){
res.add(new ArrayList<>(path));
return;
}
// 字符串截取,end范围是[startIndex+1,s.length()]
for(int i=startIndex+1;i<=s.length();i++){
if(huiwen(s.substring(startIndex,i))){
path.add(s.substring(startIndex,i));
// 下一次从i开始递归
backTracing(s,i);
path.removeLast();
}
}
}
// 每次截取判断回文
public boolean huiwen(String s){
int i=0;
int j=s.length()-1;
while(i<=j){
if(s.charAt(i)!=s.charAt(j)) return false;
i++;
j--;
}
return true;
}
}
由于是否字符子串是否回文有很多重复状态,因此可以采用记忆化搜索。
class Solution {
LinkedList<String> path;
List<List<String>> res;
int[][]cache;
public String[][] partition(String s) {
path=new LinkedList<>();
res=new ArrayList<>();
cache=new int[s.length()][s.length()];
backTracing(s,0);
// 转换成结果的形式
String[][]ans=new String[res.size()][];
for(int i=0;i<res.size();i++){
int len=res.get(i).size();
ans[i]=new String[len];
for(int j=0;j<len;j++){
ans[i][j]=res.get(i).get(j);
}
}
return ans;
}
public void backTracing(String s, int startIndex){
if(startIndex>=s.length()){
res.add(new ArrayList<>(path));
return;
}
// 字符串截取,end范围是[startIndex+1,s.length()]
for(int i=startIndex+1;i<=s.length();i++){
if(huiwen(s,startIndex,i-1)==1){
path.add(s.substring(startIndex,i));
// 下一次从i开始递归
backTracing(s,i);
path.removeLast();
}
}
}
// 记忆化搜索
public int huiwen(String s,int i, int j){
if(cache[i][j]!=0) return cache[i][j];
else if(i>=j){
cache[i][j]=1;
}else if(s.charAt(i)!=s.charAt(j)){
cache[i][j]=-1;
}else{
cache[i][j]=huiwen(s,i+1,j-1);
}
return cache[i][j];
}
}
87.复原IP
回溯+分割问题,需要进行一下长度的特判,每次分割判断合理性,当首字母是0的时候直接return。
class Solution {
LinkedList<String> path;
List<String> res;
public List<String> restoreIpAddresses(String s) {
path=new LinkedList<>();
res=new ArrayList<>();
// 需要特殊判断一下长度,不然超出时间限制
if(s.length()>12) return res;
backTracing(s,0);
return res;
}
public void backTracing(String s, int startIndex){
if(startIndex>=s.length()){
if(path.size()==4){
StringBuilder sb=new StringBuilder();
for(int i=0;i<4;i++){
sb.append(path.get(i));
if(i!=3) sb.append('.');
}
res.add(sb.toString());
}
return;
}
//在同一层上进行限制
for(int i=startIndex+1;i<=Math.min(s.length(),startIndex+3);i++){
int c=check(s.substring(startIndex,i));
// 当首字母是0,本层只能分成“0”,直接return
if(c==1){
path.add(s.substring(startIndex,i));
backTracing(s,i);
path.removeLast();
return;
}
if(c==2){
path.add(s.substring(startIndex,i));
backTracing(s,i);
path.removeLast();
}
}
}
public int check(String s){
if(s.charAt(0)=='0') return 1;
if(Integer.parseInt(s)>=0&&Integer.parseInt(s)<=255) return 2;
return -1;
}
}
或者当首字母是0的时候不return,继续把剩下同一层的搜索完(虽然都不成立且最多2种情况),但是可以简化判断和回溯的代码。
class Solution {
LinkedList<String> path;
List<String> res;
public List<String> restoreIpAddresses(String s) {
path=new LinkedList<>();
res=new ArrayList<>();
// 需要特殊判断一下长度,不然超出时间限制
if(s.length()>12) return res;
backTracing(s,0);
return res;
}
public void backTracing(String s, int startIndex){
if(startIndex>=s.length()){
if(path.size()==4){
StringBuilder sb=new StringBuilder();
for(int i=0;i<4;i++){
sb.append(path.get(i));
if(i!=3) sb.append('.');
}
res.add(sb.toString());
}
return;
}
//在同一层上进行限制
for(int i=startIndex+1;i<=Math.min(s.length(),startIndex+3);i++){
if(check(s.substring(startIndex,i))){
path.add(s.substring(startIndex,i));
backTracing(s,i);
path.removeLast();
}
}
}
// boolean check
public boolean check(String s){
if(s.length()>=2&&s.charAt(0)=='0') return false;
int i=Integer.parseInt(s);
if(i>=0&&i<=255) return true;
else return false;
}
}
88.爬楼梯的最少成本
线性动态规划,dp[i]定义为爬上第i个台阶,但还未从当前台阶往上走所花费的费用
class Solution {
public int minCostClimbingStairs(int[] cost) {
int n=cost.length;
int[]dp=new int[n+1];
dp[0]=0;
dp[1]=0;
for(int i=2;i<=n;i++){
dp[i]=Math.min(dp[i-1]+cost[i-1],dp[i-2]+cost[i-2]);
}
return dp[n];
}
}
89.房屋偷盗
用二维数组定义了房屋偷与没偷的状态
class Solution {
public int rob(int[] nums) {
int len=nums.length;
int[][]dp=new int[len][2];
dp[0][0]=0;
dp[0][1]=nums[0];
for(int i=1;i<len;i++){
dp[i][1]=dp[i-1][0]+nums[i];
dp[i][0]=Math.max(dp[i-1][1],dp[i-1][0]);
}
return Math.max(dp[len-1][0],dp[len-1][1]);
}
}
用一维数组dp[i]记录偷窃到下标为i的屋子能够产生的最大价值,偷窃第i间屋子和不偷窃第i间屋子能够产生的最大价值。
class Solution {
public int rob(int[] nums) {
int len=nums.length;
// 长度为1直接返回
if(len==1) return nums[0];
int[]dp=new int[len];
dp[0]=nums[0];
dp[1]=Math.max(nums[0],nums[1]);
for(int i=2;i<len;i++){
dp[i]=Math.max(dp[i-2]+nums[i],dp[i-1]);
}
return dp[len-1];
}
}
90.环形房屋偷盗
线性dp,基本思路同上,考虑首尾元素不能同时被偷的情况,将两者取最大值。
class Solution {
public int rob(int[] nums) {
int len=nums.length;
// 特殊情况
if(len==1) return nums[0];
int[]dp=new int[len];
int left=partition(nums,0,len-2);
int right=partition(nums,1,len-1);
return Math.max(left,right);
}
public int partition(int[]nums,int s,int e){
// 特殊情况
if(s==e) return nums[s];
int[]dp=new int[nums.length];
// 前几项dp
dp[s]=nums[s];
dp[s+1]=Math.max(nums[s],nums[s+1]);
for(int i=s+2;i<=e;i++){
dp[i]=Math.max(dp[i-2]+nums[i],dp[i-1]);
}
return dp[e];
}
}
91.粉刷房子
线性状态dp,定义定义 dp[i][j] 为考虑下标不超过i 的房子,且最后一间房子颜色为 j 时的最小成本。定义出状态和状态转换方程
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][0],dp[i-1][1])+costs[i][2];
}
return Math.min(dp[len-1][0],Math.min(dp[len-1][1],dp[len-1][2]));
}
}
考虑到后面的状态只依赖于前面的状态,因此可以用三个变量来代替动归数组。
class Solution {
public int minCost(int[][] costs) {
int len=costs.length;
int a=costs[0][0];
int b=costs[0][1];
int c=costs[0][2];
for(int i=1;i<len;i++){
int d=Math.min(b,c)+costs[i][0];
int e=Math.min(a,c)+costs[i][1];
int f=Math.min(a,b)+costs[i][2];
a=d;b=e;c=f;
}
return Math.min(a,Math.min(b,c));
}
}
92.翻转字符
线性状态dp,重点在于相邻元素间的大小关系,定义
dp[i][0]为考虑s[0,i]翻转后为以0结尾的s[0,i]为递增序列最少翻转次数
dp[i][1]为考虑s[0,i]翻转后为以1结尾的s[0,i]为递增序列最少翻转次数
其中状态转换有一点贪心的想法,如果要考虑dp[i][0],那么只能考虑dp[i-1][0]和当前的关系,因为这样才能保持单调性,至于dp[i][1]则前面是0或者1都可以,只需要取最小。
class Solution {
public int minFlipsMonoIncr(String s) {
int len=s.length();
char[]ss=s.toCharArray();
int[][]dp=new int[len][2];
// 初始化
dp[0][0]=ss[0]=='0'?0:1;
dp[0][1]=ss[0]=='1'?0:1;
for(int i=1;i<len;i++){
// 当前元素为1,保持单调的最小翻转次数
dp[i][1]=Math.min(dp[i-1][0],dp[i-1][1])+(ss[i]=='0'?1:0);
// 当前元素为0,保持单调的最小翻转次数
dp[i][0]=dp[i-1][0]+(ss[i]=='0'?0:1);
}
return Math.min(dp[len-1][1],dp[len-1][0]);
}
}
93.最长斐波那契数列
序列dp,定义二维状态,dp[i][j]表示以序号为i,j结尾的序列中最长的斐波那契数列,其中用到了哈希表查值优化。
class Solution {
public int lenLongestFibSubseq(int[] arr) {
int len=arr.length;
int[][] dp=new int[len][len];
// 初始化为长度2
for(int i=0;i<len;i++){
for(int j=i+1;j<len;j++){
dp[i][j]=2;
}
}
int ans=0;
HashMap<Integer,Integer> map=new HashMap<>();
for(int i=0;i<len;i++){
map.put(arr[i],i);
}
for(int i=2;i<len;i++){
for(int j=0;j<i;j++){
int target=arr[i]-arr[j];
// 哈希表进行查找优化
if(map.containsKey(target)){
int k=map.get(target);
if(k<j){ // 这个条件很重要
dp[j][i]=dp[k][j]+1;
// 找到了满足条件的值才更新答案
ans=Math.max(ans,dp[j][i]);
// System.out.println(arr[k]+" "+arr[j]+" "+arr[i]+" "+dp[j][i]);
}
}
}
}
return ans;
}
}
94.最少回文分割
一维序列dp,可以看成是最长递增子序列和判断回文子串个数的综合。 d p [ i ] dp[i] dp[i]表示以下标i结尾的子串中符合要求的最小分割数。需要用到某个区间是否为回文串的结论,因此可以先进行预处理。
class Solution {
public int minCut(String s) {
char[]sc=s.toCharArray();
int len=sc.length;
int[]dp=new int[len];
// 初始化为最大值
Arrays.fill(dp,0x3f3f3f3f);
boolean[][]f=huiWen(s);
int ans=0;
// 下面类似最长递增子序列
for(int i=0;i<len;i++){
// [0,i]已经是回文串了,则分割数是0
if(f[0][i]){
dp[i]=0;
continue;
}
// 否则尝试分割
for(int j=0;j<i;j++){
if(f[j+1][i]) dp[i]=Math.min(dp[i],dp[j]+1);
}
}
return dp[len-1];
}
// 回文判断预处理
public boolean[][] huiWen(String s){
char[]sc=s.toCharArray();
int len=sc.length;
boolean [][] dp=new boolean[len][len];
for(int i=len-1;i>=0;i--){
for(int j=i;j<len;j++){
if(sc[i]==sc[j]&&(j-i<2||dp[i+1][j-1])){
dp[i][j]=true;
}
}
}
return dp;
}
}
95.最长公共子序列
二维序列dp,带padding为0
class Solution {
public int longestCommonSubsequence(String text1, String text2) {
char[]s1=text1.toCharArray();
char[]s2=text2.toCharArray();
int n1=s1.length,n2=s2.length;
int[][]dp=new int[n1+1][n2+1];
for(int i=0;i<n1;i++){
for(int j=0;j<n2;j++){
if(s1[i]==s2[j]){
dp[i+1][j+1]=dp[i][j]+1;
}else{
dp[i+1][j+1]=Math.max(dp[i][j+1],dp[i+1][j]);
}
}
}
return dp[n1][n2];
}
}
96.字符串交织
参考题解, d p [ i + 1 ] [ j + 1 ] dp[i+1][j+1] dp[i+1][j+1]代表以下标i结尾的s1和以下标j结尾的s2(严格包括)能否构成s3。
class Solution {
public boolean isInterleave(String s1, String s2, String s3) {
int len1=s1.length();
int len2=s2.length();
int len3=s3.length();
if(len1+len2!=len3) return false;
boolean[][] dp=new boolean[len1+1][len2+1];
dp[0][0]=true;
for(int i=0;i<len1;i++){
dp[i+1][0]=dp[i][0]&&s1.charAt(i)==s3.charAt(i);
}
for(int j=0;j<len2;j++){
dp[0][j+1]=dp[0][j]&&s2.charAt(j)==s3.charAt(j);
}
for(int i=0;i<len1;i++){
for(int j=0;j<len2;j++){
dp[i+1][j+1]=(dp[i][j+1]&&s1.charAt(i)==s3.charAt(i+j+1))||
(dp[i+1][j]&&s2.charAt(j)==s3.charAt(i+j+1));
}
}
return dp[len1][len2];
}
}
97.子序列的数目
二维序列dp, d p [ i + 1 ] [ j + 1 ] dp[i+1][j+1] dp[i+1][j+1]表示以下标i结尾的s中包含多少以下标j结尾的t(不一定包括)。
class Solution {
public int numDistinct(String s, String t) {
int len1=s.length();
int len2=t.length();
int[][]dp=new int[len1+1][len2+1];
for(int i=0;i<=len1;i++){
dp[i][0]=1;
}
for(int j=1;j<=len2;j++){
dp[0][j]=0;
}
for(int i=0;i<len1;i++){
for(int j=0;j<len2;j++){
if(s.charAt(i)==t.charAt(j)){
dp[i+1][j+1]=dp[i][j+1]+dp[i][j];
}else{
dp[i+1][j+1]=dp[i][j+1];
}
}
}
return dp[len1][len2];
}
}
98.路径的数目
二维线性dp, d p [ i ] [ j ] dp[i][j] dp[i][j]表示从起点开始到坐标(i,j)的路径数目,先要初始化最左侧和最上侧的路径数目,只有一种走法,所以是1。时间复杂度 O ( M ∗ N ) O(M*N) O(M∗N),将所有位置路径数目状态根据转移方程枚举了一遍,最后输出终点。
class Solution {
public int uniquePaths(int m, int n) {
int[][]dp=new int[m][n];
for(int i=0;i<m;i++) dp[i][0]=1;
for(int j=0;j<n;j++) dp[0][j]=1;
for(int i=1;i<m;i++){
for(int j=1;j<n;j++){
dp[i][j]=dp[i-1][j]+dp[i][j-1];
}
}
return dp[m-1][n-1];
}
}
99.最小路径之和
二维线性dp, d p [ i ] [ j ] dp[i][j] dp[i][j]表示从起点开始到坐标(i,j)的所需要的最小路径和,先要初始化最左侧和最上侧的路径和,只有一种走法,所以是累加。时间复杂度 O ( M ∗ N ) O(M*N) O(M∗N),将所有位置路径和状态根据转移方程枚举了一遍,最后输出终点。
class Solution {
public int minPathSum(int[][] grid) {
int m=grid.length;
int n=grid[0].length;
int[][]dp=new int[m][n];
int sum=0;
for(int i=0;i<m;i++){
sum+=grid[i][0];
dp[i][0]=sum;
}
sum=0;
for(int i=0;i<n;i++){
sum+=grid[0][i];
dp[0][i]=sum;
}
for(int i=1;i<m;i++){
for(int j=1;j<n;j++){
dp[i][j]=grid[i][j]+Math.min(dp[i-1][j],dp[i][j-1]);
}
}
return dp[m-1][n-1];
}
}
变形的题目是输出该最短路径,那就是从右下角到左上角依次找到dp值较小的方向逆向记录,再把结果反转。
class Solution {
public int minPathSum(int[][] grid) {
int m=grid.length;
int n=grid[0].length;
int[][]dp=new int[m][n];
int sum=0;
for(int i=0;i<m;i++){
sum+=grid[i][0];
dp[i][0]=sum;
}
sum=0;
for(int i=0;i<n;i++){
sum+=grid[0][i];
dp[0][i]=sum;
}
for(int i=1;i<m;i++){
for(int j=1;j<n;j++){
dp[i][j]=grid[i][j]+Math.min(dp[i-1][j],dp[i][j-1]);
}
}
// 反向输出路径
List<int[]> res=new ArrayList<>();
int x=m-1,y=n-1;
res.add(new int[]{x,y});
while(x!=0||y!=0){
if(x==0){
res.add(new int[]{x,y-1});
y=y-1;
}
else if(y==0){
res.add(new int[]{x-1,y});
x=x-1;
}
else if(dp[x-1][y]<dp[x][y-1]){
res.add(new int[]{x-1,y});
x=x-1;
}else{
res.add(new int[]{x,y-1});
y=y-1;
}
}
Collections.reverse(res);
for(int[] r:res){
System.out.print(grid[r[0]][r[1]]+" ");
}
return dp[m-1][n-1];
}
}
100.三角形中最小路径之和
二维DP
class Solution {
public int minimumTotal(List<List<Integer>> triangle) {
int len=triangle.size();
int[][]dp=new int[len][len];
dp[0][0]=triangle.get(0).get(0);
for(int i=1;i<len;i++){
for(int j=0;j<=i;j++){
if (j==0) dp[i][j]=dp[i-1][j]+triangle.get(i).get(j);
else if (j==i) dp[i][j]=dp[i-1][j-1]+triangle.get(i).get(j);
else dp[i][j]=Math.min(dp[i-1][j],dp[i-1][j-1])+triangle.get(i).get(j);
}
}
int ans=0x3f3f3f3f;
for(int j=0;j<len;j++){
ans=Math.min(ans,dp[len-1][j]);
}
return ans;
}
}
101.分割等和子集
01背包问题,有点夹逼的思想在里面,找到和的一半作为背包容量,往里面放数,一个数字的价值和其大小1:1。 d p [ j ] dp[j] dp[j]代表目标和为j时,能够凑出的最大和,极限情况下能找到的话肯定 d p [ b a g ] = = b a g dp[bag]==bag dp[bag]==bag,也就是刚好分割了等和子集。
class Solution {
public boolean canPartition(int[] nums) {
int sum=0;
for(int n:nums){
sum+=n;
}
if(sum%2!=0) return false;
int bag=sum/2;
int[]dp=new int[bag+1];
for(int i=0;i<nums.length;i++){
for(int j=bag;j>=nums[i];j--){
dp[j]=Math.max(dp[j],dp[j-nums[i]]+nums[i]);
}
}
return dp[bag]==bag;
}
}
102.加减的目标值
01背包组合问题,参考官方题解,能够将neg抽象成背包容量的思想很巧妙。
二维dp的解法:
d
p
[
i
+
1
]
[
j
]
dp[i+1][j]
dp[i+1][j]代表在选择下标为[0,i]的nums去组合结果j时,总共的组合数
class Solution {
public int findTargetSumWays(int[] nums, int target) {
//(sum-neg)-neg=target
int sum=0;
for(int n:nums){
sum+=n;
}
// 必须保证neg为偶数且sum-target>=0
if(target>sum||(sum-target)%2==1) return 0;
int neg=(sum-target)/2;
int len=nums.length;
// dp[i+1][j]代表在选择下标为[0,i]的nums去组合结果j时,总共的组合数
int[][]dp=new int[len+1][neg+1];
// 由于是方法数,作为最小单元必须为1
dp[0][0]=1;
for(int i=0;i<nums.length;i++){
for(int j=neg;j>=0;j--){
// 用上nums[i]和不用上nums[i]的两种组合策略加起来
if(j>=nums[i])dp[i+1][j]=dp[i][j]+dp[i][j-nums[i]];
// 只能不用上nums[i]
else dp[i+1][j]=dp[i][j];
}
}
return dp[len][neg];
}
}
改造成一维dp滚动数组:
class Solution {
public int findTargetSumWays(int[] nums, int target) {
//(sum-neg)-neg=target
int sum=0;
for(int n:nums){
sum+=n;
}
// 必须保证neg为偶数且sum-target>=0
if(target>sum||(sum-target)%2==1) return 0;
int neg=(sum-target)/2;
int len=nums.length;
// dp[j]代表去组合结果j时,总共的组合数
int[]dp=new int[neg+1];
dp[0]=1;
for(int i=0;i<nums.length;i++){
for(int j=neg;j>=0;j--){
// 用上nums[i]和不用上nums[i]的两种组合策略加起来
if(j>=nums[i])dp[j]=dp[j]+dp[j-nums[i]];
}
}
return dp[neg];
}
}
103.最少的硬币数
完全背包问题,求的是在指定容量内放无限次物品,所需要的最少物品数目。
class Solution {
public int coinChange(int[] coins, int amount) {
//dp[i]表示要达到重量为i时,最少的硬币个数
int[]dp=new int[amount+1];
int len=coins.length;
Arrays.fill(dp,Integer.MAX_VALUE);
dp[0]=0;
for(int i=0;i<len;i++){
for(int j=coins[i];j<=amount;j++){
if(dp[j-coins[i]]!=Integer.MAX_VALUE)
dp[j]=Math.min(dp[j],dp[j-coins[i]]+1);
}
}
return dp[amount]==Integer.MAX_VALUE?-1:dp[amount];
}
}
104.排列的数目
完全背包的排列问题,必须容量在外循环,物品在内循环。
class Solution {
public int combinationSum4(int[] nums, int target) {
// dp[i]代表组成目标i的元素组合个数
int[]dp=new int[target+1];
int len=nums.length;
dp[0]=1;
for(int i=1;i<=target;i++){
for(int j=0;j<len;j++){
if(i>=nums[j]){
dp[i]+=dp[i-nums[j]];
}
}
}
return dp[target];
}
}
105.岛屿最大面积
深度优先遍历中的经典岛屿问题
class Solution:
def maxAreaOfIsland(self, grid: List[List[int]]) -> int:
res=0
for i in range(len(grid)):
for j in range(len(grid[0])):
if grid[i][j]==1:
area=self.dfs(grid,i,j)
res=max(res,area)
return res
def dfs(self,grid,i,j):
dx=[0,1,0,-1]
dy=[-1,0,1,0]
if i<0 or i>=len(grid) or j<0 or j>=len(grid[0]) or grid[i][j]==-1 or grid[i][j]==0:
return 0
grid[i][j]=-1
area=0
for k in range(4):
# 直接将越界情况抛给下一层,不同于广度优先遍历
x=i+dx[k]
y=j+dy[k]
area += self.dfs(grid,x,y)
return 1+area
106.二分图
DFS+邻接表的数组形式存图,每个节点相邻的点都已经列出来了,要对当前的点染色,同时判断相邻的点能否进一步染色。
class Solution:
def isBipartite(self, graph: List[List[int]]) -> bool:
n = len(graph)
colors= [0]*n
for i in range(n):
# 一种情况不满足就return
if colors[i]==0 and not self.dfs(graph,colors,1,i):
return False
return True
def dfs(self,graph,colors,color,i):
colors[i]=color
# 对于相邻的点
for j in graph[i]:
if colors[j]==color:
return False
if colors[j]==0 and not self.dfs(graph,colors,-1*color,j):
return False
return True
107.矩阵中的距离
和层数相关的多源BFS,求1到0的最近距离,这里转换思想,将所有0一视同仁,反而求所有0到最近的1的距离。distance初始化为0,因为节点在进队前就判断是否为1赋值了,已走路径只算了一个端点。
class Solution:
def updateMatrix(self, mat: List[List[int]]) -> List[List[int]]:
q=deque()
res=[[0]*len(mat[0]) for _ in range(len(mat))]
for i in range(len(mat)):
for j in range(len(mat[0])):
if mat[i][j]==0:
mat[i][j]=-1
q.append((i,j))
#初始化为0,是因为后面进队之前就赋值了
distance=0
while q:
size=len(q)
distance +=1
for i in range(size):
top= q.popleft()
topx=top[0]
topy=top[1]
dx=[0,1,0,-1]
dy=[1,0,-1,0]
for k in range(4):
x=topx+dx[k]
y=topy+dy[k]
if x<0 or x>=len(mat) or y<0 or y>=len(mat[0]) or mat[x][y]==-1:
continue
# 进队之前就赋值
if mat[x][y]==1:
res[x][y]=distance
mat[x][y]=-1
q.append((x,y))
return res
108.单词演变
双向BFS或者单向BFS都可以,在起点和终点都知道的情况下,我们用双向BFS作为第二解法。
双向BFS:
class Solution:
def ladderLength(self, beginWord: str, endWord: str, wordList: List[str]) -> int:
wordset=set(wordList)
# 合法的单词序列
if beginWord in wordset:
wordset.remove(beginWord)
# 如果endWord没有出现在合法的单词序列中
if endWord not in wordset:
return 0
q1=deque()
vis1=set()
q1.append(beginWord)
vis1.add(beginWord)
q2=deque()
vis2=set()
q2.append(endWord)
vis2.add(endWord)
level=0
while q1 and q2:
if len(q1)>len(q2):
temp1=q1
temp2=vis1
q1=q2
vis1=vis2
q2=temp1
vis2=temp2
level +=1
size=len(q1)
# 针对每个单词
for _ in range(size):
top=q1.popleft()
# 枚举其每一位上26个字母的可能性
listword=list(top)
for i,c in enumerate(top):
for k in range(26):
listword[i]=chr(ord('a')+k)
nextword=''.join(listword)
# 在合法的单词序列中
if nextword in wordset:
# 下一个单词出现交集
if nextword in q2:
return level+1
# 下一个单词没有交集且没有被访问
if nextword not in vis1:
vis1.add(nextword)
q1.append(nextword)
# 将当前被替换位复原
listword[i]=c
return 0
单向BFS:
class Solution:
def ladderLength(self, beginWord: str, endWord: str, wordList: List[str]) -> int:
wordset=set(wordList)
# 合法的单词序列
if beginWord in wordset:
wordset.remove(beginWord)
q=deque()
vis=set()
q.append(beginWord)
vis.add(beginWord)
level=0
while q:
level +=1
size=len(q)
# 针对每个单词
for _ in range(size):
top=q.popleft()
# 枚举其每一位上26个字母的可能性
listword=list(top)
for i,c in enumerate(top):
for k in range(26):
listword[i]=chr(ord('a')+k)
nextword=''.join(listword)
# 在合法的单词序列中
if nextword in wordset:
# 下一个单词就是终点词
if nextword == endWord:
return level+1
# 下一个单词不是终点次且没有被访问
if nextword not in vis:
vis.add(nextword)
q.append(nextword)
# 将当前被替换位复原
listword[i]=c
return 0
109.开密码锁
双向BFS:
class Solution:
def openLock(self, deadends: List[str], target: str) -> int:
# 特殊情况考虑
if '0000'in deadends:
return -1
if '0000' == target:
return 0
deadset=set(deadends)
q1=collections.deque()
vis1=set()
vis1.add('0000')
q1.append('0000')
q2=collections.deque()
vis2=set()
vis2.add(target)
q2.append(target)
level=-1
while q1 and q2:
level +=1
if len(q1)>len(q2):
temp1=q1
q1=q2
q2=temp1
temp2=vis1
vis1=vis2
vis2=temp2
size=len(q1)
# 所有可行元素逐层出队
for _ in range(size):
top=q1.popleft()
listword=list(top)
# 针对一个word的所有位置上的元素
for i,c in enumerate(listword):
# 该位置上的两种情况枚举
for k in {1,-1}:
listword[i]=str((int(c)+k+10)%10)
nextword=''.join(listword)
# 当nextword不在deadset中时
if nextword not in deadset:
# nextword在下面出现交集
if nextword in q2:
return level +1
if nextword not in vis1:
q1.append(nextword)
vis1.add(nextword)
# 回溯还原,避免对下一次枚举产生影响
listword[i]=c
return -1
单向BFS:
class Solution:
def openLock(self, deadends: List[str], target: str) -> int:
# 特殊情况考虑
if '0000'in deadends:
return -1
if '0000' == target:
return 0
deadset=set(deadends)
q=collections.deque()
vis=set()
vis.add('0000')
q.append('0000')
level=-1
while q:
level +=1
size=len(q)
# 所有可行元素逐层出队
for _ in range(size):
top=q.popleft()
listword=list(top)
# 针对一个word的所有位置上的元素
for i,c in enumerate(listword):
# 该位置上的两种情况枚举
for k in {1,-1}:
listword[i]=str((int(c)+k+10)%10)
nextword=''.join(listword)
# 当nextword不在deadset中时
if nextword not in deadset:
if nextword==target:
return level +1
if nextword not in vis:
q.append(nextword)
vis.add(nextword)
# 回溯还原,避免对下一次枚举产生影响
listword[i]=c
return -1
110.所有路径
DFS暴搜
class Solution {
LinkedList<Integer>path;
List<List<Integer>> res;
int n;
int[][] graph;
public List<List<Integer>> allPathsSourceTarget(int[][] g) {
n=g.length-1;
path=new LinkedList<>();
res=new ArrayList<>();
graph=g;
path.add(0);
backTracing(0);
return res;
}
public void backTracing(int cur){
if(cur==n){
res.add(new ArrayList<>(path));
return;
}
for(int i:graph[cur]){
path.add(i);
backTracing(i);
path.removeLast();
}
}
}
111.计算除法
HashMap建图+DFS,其中boolean found来处理不能访问到的query,如果在某一个DFS分支中将found置为true,则提前剪枝结束。
class Solution {
// 存图 Map.Entry[a,Pair(b,2)]
HashMap<String,List<Pair>> map=new HashMap<>();
// 表明每个query是否找到了答案
boolean found=false;
// 存储答案
List<Double> res=new ArrayList<>();
// 用于存储节点是否被访问过
HashSet<String> vis = new HashSet<>();
public double[] calcEquation(List<List<String>> equations, double[] values, List<List<String>> queries) {
drawMap(equations,values);
for(List<String> q:queries){
// ["x","x"]不是图中的节点
if(!map.containsKey(q.get(0))){
res.add(-1.0);
continue;
}
// 每次query刚开始都是没找到
found=false;
// ["a","a"]这种直接能返回1的结果
vis.add(q.get(0));
dfs(q.get(0),1.0,q.get(1));
vis.remove(q.get(0));
// 在搜索过后没有找到结果
if(!found) res.add(-1.0);
}
double[] ans= new double[res.size()];
for(int i=0;i<res.size();i++){
ans[i]=res.get(i);
}
return ans;
}
// 根据equations和values建图
public void drawMap(List<List<String>> equations, double[] values){
for(int i=0;i<equations.size();i++){
List<Pair> l1=map.getOrDefault(equations.get(i).get(0),new ArrayList<Pair>());
l1.add(new Pair(equations.get(i).get(1),values[i]));
map.put(equations.get(i).get(0),l1);
List<Pair> l2=map.getOrDefault(equations.get(i).get(1),new ArrayList<Pair>());
l2.add(new Pair(equations.get(i).get(0),1/values[i]));
map.put(equations.get(i).get(1),l2);
}
}
// 从curp节点出发, 当前权值为curw, 目标点为target
public void dfs(String curp,double curw, String target){
// 比较两个字符串相等不能==
if(Objects.equals(curp,target)){
res.add(curw);
found=true;
return;
}
for(Pair p:map.get(curp)){
// 已经被访问过,不再访问
if(vis.contains(p.s)) continue;
vis.add(p.s);
dfs(p.s,curw*p.w,target);
vis.remove(p.s);
// 前面的分支已经找到了,直接对同层的进行剪枝
if(found) return;
}
}
// 用于存储边权和节点 w---s
public class Pair{
String s;
double w;
public Pair(String s,double w){
this.s=s;
this.w=w;
}
}
}
112.最长递增路径
记忆化搜索DFS
class Solution {
int[][]map;
int[][]dir={{1,0},{-1,0},{0,1},{0,-1}};
int m,n;
int[][]cache;
public int longestIncreasingPath(int[][] matrix) {
map=matrix;
int res=1;
m=matrix.length;
n=matrix[0].length;
cache=new int[m][n];
// 每个点出发进行dfs
for(int i=0;i<m;i++){
for(int j=0;j<n;j++){
// 前面遍历节点的时候已经把后面遍历过了(尽可能搜索到底了)
// 因此只有后面还没有被搜索过的起点有可能变大
if(cache[i][j]==0) res=Math.max(res,dfs(i,j));
}
}
return res;
}
// 从matrix[px,py]出发的最长递增的长度
public int dfs(int px,int py){
if(cache[px][py]!=0) return cache[px][py];
int ans=1;
for(int i=0;i<4;i++){
int nextx=px+dir[i][0];
int nexty=py+dir[i][1];
// 没有把return base放到下一层去,直接上一层全部判断完
// 主要是因为只能走递增路径,放到下一层不太好判断了
if(nextx<0||nextx>=m||nexty<0||nexty>=n) continue;
if(map[nextx][nexty]<=map[px][py]) continue;
ans=Math.max(ans,1+dfs(nextx,nexty));
}
// 当没有更max的ans直接return 1
cache[px][py]=ans;
return ans;
}
}
113.课程顺序
拓扑排序,队列实现,和队列层数无关,因此不需要一次弹出一层。
class Solution {
public int[] findOrder(int numCourses, int[][] prerequisites) {
// ArrayList存图
ArrayList<Integer>[] map=new ArrayList[numCourses];
int[]indegree = new int[numCourses];
for(int i=0;i<numCourses;i++){
map[i]=new ArrayList<Integer>();
}
// [ai,bi]表示bi->ai
for(int[]p:prerequisites){
map[p[1]].add(p[0]);
indegree[p[0]]++;
}
Deque<Integer> que=new ArrayDeque<>();
ArrayList<Integer> res=new ArrayList<>();
for(int i=0;i<numCourses;i++){
if(indegree[i]==0) que.offer(i);
}
while(!que.isEmpty()){
int top=que.poll();
res.add(top);
for(Integer next:map[top]){
indegree[next]--;
if(indegree[next]==0){
que.offer(next);
}
}
}
int[] ans = res.stream().mapToInt(x->x).toArray();
if(ans.length!=numCourses) return new int[]{};
else return ans;
}
}
114.外星文字典
拓扑排序,根据字母之间的前后关系进行拓扑中边的建立,记录所有出现过的字母,至于那些不具有前后关系的字母也需要对他们进行记录(只不过初始化入度in都为0),在建立拓扑排序的时候也将它们纳入第一层,但是不影响已经存在的先后关系。
按字母递增排序,就根据拓扑排序记录每层出队字母以及它的层数,在同层内按字母递增排序。
import java.util.*;
public class Solution {
// HashMap存图
HashMap<Character, ArrayList<Character>> map=new HashMap<>();
int[]in=new int[26];
HashSet<Character> set=new HashSet<>(); // 记录出现的字母
public String alienOrder(String[] words) {
// 记录所有出现的字母
for(String w:words){
for(char c:w.toCharArray()) set.add(c);
}
// 建图,只能把显然happen-before的关系定下来
for(int k=0;k<words.length-1;k++){
//找到第一个差异点
String s1=words[k];
String s2=words[k+1];
int len1=s1.length(),len2=s2.length();
int i=0,j=0;
// 表示还未找到差异点
int index=-1;
while(i<len1&&j<len2){
if(s1.charAt(i)!=s2.charAt(j)){
index=i;
break;
}else{
i++;
j++;
}
}
// 不具有差异
if(index==-1&&len1>len2) return ""; // 用例错误 前缀一样结果前面长度大
if(index==-1) continue;
ArrayList<Character> l=map.getOrDefault(s1.charAt(i),new ArrayList<Character>());
l.add(s2.charAt(j));
map.put(s1.charAt(i),l);
in[s2.charAt(j)-'a']++;
}
// 拓扑排序
ArrayDeque<Character> que=new ArrayDeque<>();
for(Character c:set){
// 即使在不存在前后关系,刚开始初始化set里的字母in就都为0
if(in[c-'a']==0)que.offer(c);
}
// 存储字母和他的权值,在权值相同时按字母递增顺序排列
ArrayList<int[]> res=new ArrayList<>();
int depth=-1;
while(!que.isEmpty()){
depth++;
int len=que.size();
for(int i=0;i<len;i++){
Character top=que.poll();
res.add(new int[]{top-'a',depth});
if(map.get(top)==null) continue;
for(Character next:map.get(top)){
in[next-'a']--;
if(in[next-'a']==0) que.offer(next);
}
}
}
if(res.size()!=set.size()) return "";
Collections.sort(res,(a, b)-> a[1]==b[1]?Integer.compare(a[0],b[0]):Integer.compare(a[1],b[1]));
StringBuilder sb=new StringBuilder();
for(int i=0;i<res.size();i++){
sb.append((char)('a'+res.get(i)[0]));
}
return sb.toString();
}
}
116.省份数量
DFS统计图中连通域的个数,访问标记很关键。
class Solution:
def findCircleNum(self, isConnected: List[List[int]]) -> int:
vis=set()
res=0
# 从某个节点开始将与其连接的所有点都进行访问
def dfs(i):
for j in range(len(isConnected[i])):
if j not in vis and isConnected[i][j]==1:
vis.add(j)
dfs(j)
# 从每个未被访问过的节点开始进行搜索
for i in range(len(isConnected)):
if i not in vis:
vis.add(i)
dfs(i)
res +=1
return res
117.相似的字符串
DFS或者BFS找连通分量,一组单词就是相似的所有单词组成的一个连通分量。所以本质上就是上面的省份数量,只不过这里的isConnected是相似字符串的判断,相似则connect.
DFS查找连通分量个数,时间复杂度 O ( N 2 ) O(N^2) O(N2),循环dfs N次,每次都尝试标记所有的N个节点。
class Solution {
HashSet<Integer> vis=new HashSet<>();
public int numSimilarGroups(String[] strs) {
int ans=0;
for(int i=0;i<strs.length;i++){
if(!vis.contains(i)){
vis.add(i);
dfs(i,strs);
ans++;
}
}
return ans;
}
public void dfs(int k,String[] strs){
for(int i=0;i<strs.length;i++){
// 从前往后检查后面未被访问过的单词i
if(!vis.contains(i)&&isConnect(strs[k],strs[i])){
vis.add(i);
dfs(i,strs);
}
}
}
public boolean isConnect(String a, String b){
int count=0;
for(int i=0;i<a.length();i++){
if(a.charAt(i)!=b.charAt(i)) count++;
}
return count<=2; // 必定成立,必然有0,2,4...个位置不同
}
}
BFS也可以解决连通数量问题,时间复杂度 O ( N 2 ) O(N^2) O(N2),循环bfs N次,每次在一次一次出队入队的过程中都尝试标记所有的N个节点。
class Solution {
HashSet<Integer> vis=new HashSet<>();
public int numSimilarGroups(String[] strs) {
int ans=0;
for(int i=0;i<strs.length;i++){
if(!vis.contains(i)){
bfs(i,strs);
ans++;
}
}
return ans;
}
public void bfs(int k,String[] strs){
Deque<Integer> que=new ArrayDeque<>();
vis.add(k);
que.offer(k);
while(!que.isEmpty()){
int top=que.poll();
// 从前往后检查后面未被访问过的单词i
for(int i=0;i<strs.length;i++){
if(!vis.contains(i)&&isConnect(strs[top],strs[i])){
vis.add(i);
que.offer(i);
}
}
}
}
public boolean isConnect(String a, String b){
int count=0;
for(int i=0;i<a.length();i++){
if(a.charAt(i)!=b.charAt(i)) count++;
}
return count<=2; // 必定成立,必然有0,2,4...个位置不同
}
}
118.多余的边
并查集,原来无向无环的图加入了一条边以后存在了一个环,现在要删除一条边让它继续无环,当有多种删除方案时,选择 edges 中最后出现的边。
聚焦于点,从前向后遍历每一条边,边的两个节点如果不在同一个集合,就将这条边的两个节点加入同一个集合,当遍历到某条边却发现其两个节点已经在同一个集合里,再加入这条边就会成环。
class Solution {
public int[] findRedundantConnection(int[][] edges) {
UnionFind unionFind=new UnionFind(edges.length);
for(int[]e:edges){
if(unionFind.isConnect(e[0],e[1])){
return e;
}
unionFind.union(e[0],e[1]);
}
return new int[0];
}
class UnionFind{
private int count;
private int[] parent;
public UnionFind(int n){
this.count=n;
parent=new int[n+1];
for(int i=1;i<=n;i++){
parent[i]=i;
}
}
public void union(int a, int b){
int parentA=find(a);
int parentB=find(b);
if(parentA==parentB) return;
parent[parentA]=parentB;
this.count--;
}
// public int find(int x){
// if(parent[x]==x) return parent[x];
// else return find(parent[x]);
// }
public int find(int x){
if(parent[x]!=x){
parent[x]=find(parent[x]);
}
return parent[x];
}
public boolean isConnect(int a,int b){
return find(a)==find(b);
}
public int getCount(int x){
return this.count;
}
}
}
119.最长连续序列
哈希表,参考题解,其中最重要的是时间复杂度 O ( N ) O(N) O(N)的分析,并不是for, while嵌套就是 O ( N 2 ) O(N^2) O(N2)复杂度,外层循环中有些直接跳过,重点看内层循环遍历的元素数。
最坏情况是整个都可以连起来,总共循环了2N次(外层N次,不是最小数之间跳过,内层从最小的那个数开始循环N次,最小数就一个,所以只能进入内部这个循环一次,所以N+1N),最好情况下是一个都连不起来,总共循环了N次(内层虽然每层都要循环,但只循环0次,所以N+0)。最坏2N,最好N,所以时间复杂度O(N).
class Solution {
public int longestConsecutive(int[] nums) {
HashSet<Integer> set=new HashSet<>();
int res=0;
for(int n:nums){
set.add(n);
}
// 并不是for,while嵌套就是O(N^2)复杂度,外层循环中有些直接跳过,重点看内层循环遍历的元素数
for(int v:nums){
// 当存在v-1时,最长序列的起点肯定不是当前v
// 有可能前面最长序列的中间元素用过了v,这一步之需要跳过
if(set.contains(v-1)) continue;
int temp=1;
int cur=v;
// 该序列可以不断变长
while(set.contains(cur+1)){
temp++;
cur++;
}
res=Math.max(res,temp);
}
return res;
}
}