力扣100题解(Java版)

一、哈希

1、两数之和:

给定一个整数数组 nums 和一个整数目标值 target,请你在该数组中找出 和为目标值 target 的那 两个 整数,并返回它们的数组下标。

你可以假设每种输入只会对应一个答案,并且你不能使用两次相同的元素。

你可以按任意顺序返回答案。

示例 1:

复制代码
输入:nums = [2,7,11,15], target = 9
输出:[0,1]
解释:因为 nums[0] + nums[1] == 9 ,返回 [0, 1] 。

思路:使用一个HashMap,遍历数组,检查是否已存在 target-numsi,有则直接返回,无则将当前数放入Map中。

复制代码
class Solution {
    public int[] twoSum(int[] nums, int target) {
        Map<Integer,Integer> map=new HashMap<>();
        int len=nums.length;
        for(int i=0;i<len;i++){
            if(map.containsKey(target-nums[i])){
                return new int[]{map.get(target-nums[i]),i};
            }
            map.put(nums[i],i);
        }
        return new int[0];
    }
}

2、字母异位词分组:

给你一个字符串数组,请你将 字母异位词 组合在一起。可以按任意顺序返回结果列表。

示例 1:

输入: strs = "eat", "tea", "tan", "ate", "nat", "bat"

输出:\["bat","nat","tan","ate","eat","tea"]

解释:

  • 在 strs 中没有字符串可以通过重新排列来形成 "bat"
  • 字符串 "nat""tan" 是字母异位词,因为它们可以重新排列以形成彼此。
  • 字符串 "ate""eat""tea" 是字母异位词,因为它们可以重新排列以形成彼此。

思路:

1)字符串处理:遍历strs,将strs的各个字符串转为字符数组,从而对它们进行排序,再重新转为字符串

2)哈希:将排序后生成的字符串放入HashMap,这里Map的value要用List来存下标数组。

语法上:首先是String的**toCharArray()**转化为字符数组,Arrays.sort来排序,

然后map.getOrDefault(s,new ArrayList<String>())

根据map.value直接创建数组:return new ArrayList<List<String>>(map.values())

复制代码
class Solution {
    public List<List<String>> groupAnagrams(String[] strs) {
        Map<String,List<String>> map=new HashMap<>();
        for(String str:strs){
            char[] chars=str.toCharArray();
            Arrays.sort(chars);
            String s=new String(chars);
            List<String> list=map.getOrDefault(s,new ArrayList<String>());
            list.add(str);
            map.put(s,list);
        }
        return new ArrayList<List<String>>(map.values());
    }
}

3、最长连续序列:

给定一个未排序的整数数组 nums ,找出数字连续的最长序列(不要求序列元素在原数组中连续)的长度。

请你设计并实现时间复杂度为 O(n)的算法解决此问题。

示例 1:

复制代码
输入:nums = [100,4,200,1,3,2]
输出:4
解释:最长数字连续序列是 [1, 2, 3, 4]。它的长度为 4。

思路:首先应该对数组进行去重 ,所以先转为HashSet,

然后将Set转为List ,对List进行排序(Collection.sort())

最后遍历以排序的数组,找到最长连续序列。

复制代码
class Solution {
    public int longestConsecutive(int[] nums) {
        if(nums.length==0){
            return 0;
        }
        Set<Integer> set=new HashSet<>();
        for(Integer i:nums){
            set.add(i);
        }
        List<Integer> list=new ArrayList<>(set);
        Collections.sort(list);
        int len=1;
        int max=1;
        for(int i=0;i<list.size()-1;i++){
            if(list.get(i)==list.get(i+1)-1){
                len++;
                if(max<len)
                    max=len;
            }
            else{
                
                len=1;
            }
        }
        return max;
    }
}

二、双指针

1、移动零

给定一个数组 nums,编写一个函数将所有 0 移动到数组的末尾,同时保持非零元素的相对顺序。

请注意 ,必须在不复制数组的情况下原地对数组进行操作。

示例 1:

复制代码
输入: nums =[0,1,0,3,12]
输出:[1,3,12,0,0]

思路:

1)慢指针记录非 0 元素应该放的位置

2)快指针遍历数组 ,把所有非 0 数依次挪到慢指针位置,慢指针跟着后移

3)遍历完后,慢指针后面全部填 0

复制代码
class Solution {
    public void moveZeroes(int[] nums) {
        int slow=0;
        for(int fast=0;fast<nums.length;fast++){
            if(nums[fast]!=0){
                nums[slow]=nums[fast];
                slow++;
            }
        }
        while(slow<nums.length){
            nums[slow]=0;
            slow++;
        }
    }
}

2、乘最多水容器

给定一个长度为 n 的整数数组 height 。有 n 条垂线,第 i 条线的两个端点是 (i, 0)(i, height[i])

找出其中的两条线,使得它们与 x 轴共同构成的容器可以容纳最多的水。

返回容器可以储存的最大水量。

**说明:**你不能倾斜容器。

示例 1:

复制代码
输入:[1,8,6,2,5,4,8,3,7]
输出:49 
解释:图中垂直线代表输入数组 [1,8,6,2,5,4,8,3,7]。在此情况下,容器能够容纳水(表示为蓝色部分)的最大值为 49。

思路:

1、左右双指针分别指向数组两端,计算当前容器面积

2、核心规则:谁更矮,就移动谁 (保留高的边,才有机会得到更大面积)

3、不断更新最大面积,直到两指针相遇

复制代码
class Solution {
    public int maxArea(int[] height) {
        int i = 0;
        int j = height.length - 1;
        int res = 0;
        
        while (i < j) {
            // 计算当前面积
            int area = (j - i) * Math.min(height[i], height[j]);
            res = Math.max(res, area);
            
            // 关键:谁矮移动谁
            if (height[i] < height[j]) {
                i++;
            } else {
                j--;
            }
        }
        return res;
    }
}

3、三数之和

给你一个整数数组 nums ,判断是否存在三元组 [nums[i], nums[j], nums[k]] 满足 i != ji != kj != k ,同时还满足 nums[i] + nums[j] + nums[k] == 0 。请你返回所有和为 0 且不重复的三元组。

**注意:**答案中不可以包含重复的三元组。

示例 1:

复制代码
输入:nums = [-1,0,1,2,-1,-4]
输出:[[-1,-1,2],[-1,0,1]]
解释:
nums[0] + nums[1] + nums[2] = (-1) + 0 + 1 = 0 。
nums[1] + nums[2] + nums[4] = 0 + 1 + (-1) = 0 。
nums[0] + nums[3] + nums[4] = (-1) + 2 + (-1) = 0 。
不同的三元组是 [-1,0,1] 和 [-1,-1,2] 。
注意,输出的顺序和三元组的顺序并不重要。

思路(排序 + 双指针)

1、先排序 :给数组排序,方便去重和双指针夹逼

2、固定第一个数 :遍历每个元素 numsi 作为三元组第一个数

3、双指针找另外两个数

4、左指针 j 从 i+1 开始,右指针 k 从末尾 开始

5、三数和 = 0 → 记录结果,同时跳过重复元素
和 < 0 → 左指针右移(增大和)
和 > 0 → 右指针左移(减小和)

6、全程去重:第一个数、左指针、右指针遇到重复都跳过,保证结果不重复

复制代码
class Solution {
    public List<List<Integer>> threeSum(int[] nums) {
        List<List<Integer>> list =new ArrayList<List<Integer>>();
        Arrays.sort(nums);
        int len=nums.length;
        int i=0;
        while(i<len-2){
            if(nums[i]>0)
                break;
       
            int j=i+1;
            int k=len-1;
            while(j<k){
                if((nums[i]+nums[j]+nums[k])==0){
                    list.add(Arrays.asList(nums[i],nums[j],nums[k]));
                  
                    while (j < k && nums[j] == nums[j + 1]) j++;
               
                   while (j < k && nums[k] == nums[k-1]) k--;
                    j++;
                    k--;
                }
                else if((nums[i]+nums[j]+nums[k])<0)
                    j++;
                else  
                    k--;
            } 
            while(i<len-3&&nums[i]==nums[i+1])
                i++;
            i++;
        }
        return list;
    }
}

4、接雨水

给定 n 个非负整数表示每个宽度为 1 的柱子的高度图,计算按此排列的柱子,下雨之后能接多少雨水。

示例 1:

复制代码
输入:height = [0,1,0,2,1,0,1,3,2,1,2,1]
输出:6
解释:上面是由数组 [0,1,0,2,1,0,1,3,2,1,2,1] 表示的高度图,在这种情况下,可以接 6 个单位的雨水(蓝色部分表示雨水)。 

思路:

1、左右双指针从两端往中间走谁矮移动谁 (移动矮的才能计算存水量:只有移动矮的那一边,才能确定当前位置能存多少水。)

2、记录左边最高柱子left_max、右边最高柱子right_max

3、移动指针时:

当前柱子 < 最高边 → 能存水,累加水量

当前柱子 ≥ 最高边 → 更新最高边

4、直到两指针相遇,返回总水量

三、滑动窗口

(滑动窗口可以理解为一种特定的双指针,用来处理连续区间的问题)

1、无重复字符的最长子串

给定一个字符串 s ,请你找出其中不含有重复字符的 最长 子串 的长度。

示例 1:

复制代码
输入: s = "abcabcbb"
输出: 3 
解释: 因为无重复字符的最长子串是"abc",所以其长度为 3。注意 "bca" 和 "cab" 也是正确答案。

思路:

1、用左右指针形成一个窗口,右指针不断向右走;

2、如果遇到重复字符,就把**左指针跳到重复位置的下一位,**保证窗口内永远无重复;

3、全程记录窗口最大长度。

具体实现技巧:使用一个HashMap来记录某个字符的最后出现的位置,通过该位置与left的比较来判定是否由重复字符,从而把时间压倒O(n).

复制代码
class Solution {
    public int lengthOfLongestSubstring(String s) {
        int left=0;
        int len=s.length();
        
        int maxsize=0;
       Map<Character,Integer> h=new HashMap<>();
        for(int right=0;right<len;right++){
            char c=s.charAt(right);
            if(h.containsKey(c)&&h.get(c)>=left){
                
                left=h.get(c)+1;       
            }
            h.put(c,right);
            int size=right-left+1;
             maxsize=Math.max(maxsize,size);

        }
        return maxsize;
    }
};

2、找到字符串中所有的字母异位词

给定两个字符串 sp,找到 s中所有 p异位词的子串,返回这些子串的起始索引。不考虑答案输出的顺序。

示例 1:

复制代码
输入: s = "cbaebabacd", p = "abc"
输出: [0,6]
解释:
起始索引等于 0 的子串是 "cba", 它是 "abc" 的异位词。
起始索引等于 6 的子串是 "bac", 它是 "abc" 的异位词。

思路 :在 s 上滑动一个和 p 一样长的窗口,统计窗口内字母出现次数; 只要次数和 p 完全一样,就是异位词,记录窗口起始下标。

关键步骤:

  1. 统计字母数量 用两个 26 位数组,分别存 p 和当前窗口的字母计数。

  2. 初始化第一个窗口 先把最左边、长度等于 p 的窗口统计好。

  3. 窗口向右滑动(核心)

    • 右边进一个新字符 → 计数 + 1
    • 左边出一个旧字符 → 计数 - 1
    • 每次判断两个数组是否相等 → 相等就是异位词

    class Solution {
    public List findAnagrams(String s, String p) {
    List list=new ArrayList<>();
    int[] scount=new int[26];
    int[] pcount=new int[26];
    int slen=s.length();
    int plen=p.length();
    if(plen>slen)
    return list;
    for(int i=0;i<plen;i++){
    scount[s.charAt(i)-'a']++;
    pcount[p.charAt(i)-'a']++;
    }
    if(Arrays.equals(scount,pcount))
    list.add(0);
    for(int i=plen;i<slen;i++){
    scount[s.charAt(i-plen)-'a']--;
    scount[s.charAt(i)-'a']++;
    if(Arrays.equals(scount,pcount))
    list.add(i-plen+1);
    }
    return list;
    }
    }

四、子串

1、和为k的子数组

给你一个整数数组 nums 和一个整数 k ,请你统计并返回 该数组中和为 k的子数组的个数

子数组是数组中元素的连续非空序列。

示例 1:

复制代码
输入:nums = [1,1,1], k = 2
输出:2

思路:

1、子数组要求是连续的,而连续序列可以用大前缀减去小前缀得到

2、通过遍历求出各个前缀和 sum,同时遍历过程中找到有多少个和为sum-k的前缀,就代表这中间有多少个和为k的连续子序列。(和为x的前缀出现的次数用HashMap来存储)

复制代码
class Solution {
    public int subarraySum(int[] nums, int k) {
       Map<Integer,Integer> map=new HashMap<>();
       map.put(0,1);
       int len=nums.length;
       int sum=0;
       int count=0;
       for(int i=0;i<len;i++){
        sum+=nums[i];
        if(map.containsKey(sum-k))
            count+=map.get(sum-k);
        map.put(sum,map.getOrDefault(sum,0)+1);

       }
       return count;
    }
}

2、滑动窗口最大值

给你一个整数数组 nums,有一个大小为 k的滑动窗口从数组的最左侧移动到数组的最右侧。你只可以看到在滑动窗口内的 k 个数字。滑动窗口每次只向右移动一位。

返回 滑动窗口中的最大值

示例 1:

复制代码
输入:nums = [1,3,-1,-3,5,3,6,7], k = 3
输出:[3,3,5,5,6,7]
解释:
滑动窗口的位置                最大值
---------------               -----
[1  3  -1] -3  5  3  6  7       3
 1 [3  -1  -3] 5  3  6  7       3
 1  3 [-1  -3  5] 3  6  7       5
 1  3  -1 [-3  5  3] 6  7       5
 1  3  -1  -3 [5  3  6] 7       6
 1  3  -1  -3  5 [3  6  7]      7

思路:维护一个单调递减 的双端队列:

  1. 新元素进来时,把队列里比它小的全部删掉(保证递减)
  2. 把新元素加入队头
  3. 窗口移动时,移除离开窗口的旧元素
  4. 队列最后一个元素永远是当前窗口的最大值
  5. 不断把最大值放进结果数组

双端队列:Deque

方法:peekFirst(),peekLast(),pollFirst(),pollLast,offerFirst()

复制代码
class Solution {
    public int[] maxSlidingWindow(int[] nums, int k) {
        int n = nums.length;
        Deque<Integer> q=new LinkedList<>();
        int[] result=new int[n-k+1];
        for(int i=0;i<k;i++){
            while(!q.isEmpty()&&q.peekFirst()<nums[i]){
                q.pollFirst();
            }
            q.offerFirst(nums[i]);
        }
        result[0]=q.peekLast();
        for(int i=k;i<n;i++){
            while(!q.isEmpty()&&q.peekFirst()<nums[i]){
                q.pollFirst();
            }
            q.offerFirst(nums[i]);
            if(q.peekLast()==nums[i-k]){
                q.pollLast();
            }
            result[i-k+1]=q.peekLast();
        }
        return result;
    }
}

3、最小覆盖子串

给定两个字符串 st,长度分别是 mn,返回 s 中的 最短窗口 子串 ,使得该子串包含 t 中的每一个字符(包括重复字符 )。如果没有这样的子串,返回空字符串""

测试用例保证答案唯一。

示例 1:

复制代码
输入:s = "ADOBECODEBANC", t = "ABC"
输出:"BANC"
解释:最小覆盖子串 "BANC" 包含来自字符串 t 的 'A'、'B' 和 'C'。

示例 2:

复制代码
输入:s = "a", t = "a"
输出:"a"
解释:整个字符串 s 是最小覆盖子串。

示例 3:

复制代码
输入: s = "a", t = "aa"
输出: ""
解释: t 中两个字符 'a' 均应包含在 s 的子串中,
因此没有符合条件的子字符串,返回空字符串。

思路:

滑动窗口 + 字符计数 右指针不断扩大

窗口 → 满足条件就收缩左指针找最短 → 记录最小窗口

具体实现:

  1. 准备工作
  • need[128]:记录字符串 t 中每个字符需要多少个
  • window[128]:记录当前窗口里每个字符有多少个

(128是因为字母的ASCII在128以内)

  • count:t 里有多少种不同字符(比如 t=ABC → count=3)
  • valid:当前窗口已经满足数量要求的字符种类数

  1. 右指针扩大窗口(扩张阶段)
  • 右指针一直往右走,把字符一个个加入窗口
  • 如果这个字符是 t 需要的
    • 窗口计数 +1
    • 如果数量刚好达标valid += 1

  1. 窗口满足条件(收缩阶段,核心!)

valid == count当前窗口已经包含 t 所有字符开始不断收缩左指针,尽量缩到最短!

收缩时:

  • 每次缩之前先记录最小窗口
  • 把左边字符移出窗口
  • 如果移出后某个字符不满足数量了
    • valid--
    • 停止收缩

  1. 最终结果
  • 记录最短窗口的起始位置 start

  • 最后截取 s.substring(start, start + minlen)

    class Solution {
    public String minWindow(String s, String t) {
    int[] need=new int[128];
    int[] window=new int[128];
    for(char c:t.toCharArray()){
    need[c]++;
    }
    int minlen=Integer.MAX_VALUE;
    int count=0;//需要的种类
    for(int i=0;i<128;i++){
    if(need[i]>0)
    count++;
    }
    int valid=0;//当前窗口的种类;
    int left=0;
    int right=0;
    int n=s.length();
    int start=-1;
    while(right<n){
    char c=s.charAt(right);
    right++;
    if(need[c]>0){
    window[c]++;
    if(window[c]==need[c]){
    valid++;
    }
    }
    if(valid==count){
    while(left<right){
    if(valid==count){
    int len=right-left;
    if(len<minlen){
    start=left;
    minlen=len;

    复制代码
                          }
                      }
                      char cc=s.charAt(left);
                      left++;
                      if(need[cc]>0){
                          window[cc]--;
                          if(window[cc]<need[cc]){
                              valid--;
                              break;
                          }
                      }
                      
                  }
              }
          }
          return start==-1?"":s.substring(start,start+minlen);
      }

    }

五、普通数组

1、最大子数组和

给你一个整数数组 nums ,请你找出一个具有最大和的连续子数组(子数组最少包含一个元素),返回其最大和。

子数组是数组中的一个连续部分。

示例 1:

复制代码
输入:nums = [-2,1,-3,4,-1,2,1,-5,4]
输出:6
解释:连续子数组 [4,-1,2,1] 的和最大,为 6 。

思路

遍历数组时,不断累加当前子数组和:

  • 如果累加后比之前的最大值更大,就更新最大值

  • 如果累加后变成负数 ,说明这个子数组不可能成为后续最优解 ,直接清空重新开始

    class Solution {
    public int maxSubArray(int[] nums) {
    int len=nums.length;
    int max=nums[0];
    int curmax=0;
    int m=0;
    while(m<len){
    curmax+=nums[m];
    max=Math.max(max,curmax);
    if(curmax<0)
    curmax=0;

    复制代码
               m++;  
          }
          return max;
      }

    }

2、合并区间

以数组 intervals 表示若干个区间的集合,其中单个区间为 intervals[i] = [starti, endi] 。请你合并所有重叠的区间,并返回 一个不重叠的区间数组,该数组需恰好覆盖输入中的所有区间

示例 1:

复制代码
输入:intervals = [[1,3],[2,6],[8,10],[15,18]]
输出:[[1,6],[8,10],[15,18]]
解释:区间 [1,3] 和 [2,6] 重叠, 将它们合并为 [1,6].

思路:

1、先按区间的左端点从小到大排序

二维数组sort方法 :(Arrays.sort(intervals,(v1,v2)->v10-v20);)

2、遍历每个区间,能合并就合并,不能合并就直接保留

3、截断返回

(return Arrays**.copyOf**(mer, cur+1))

复制代码
class Solution {
    public int[][] merge(int[][] intervals) {
        Arrays.sort(intervals,(v1,v2)->v1[0]-v2[0]);
        int len =intervals.length;
        int[][] mer=new int[len][2];
        mer[0]=intervals[0];
        int cur=0;
        for(int i=1;i<len;i++){
            if(mer[cur][1]>=intervals[i][0]){
                mer[cur][1]=Math.max(mer[cur][1],intervals[i][1]);

            }
            else{
                cur++;
                mer[cur]=intervals[i];
            }
        }
        return Arrays.copyOf(mer, cur+1);
    }
}

3、轮转数组:

给定一个整数数组 nums,将数组中的元素向右轮转 k个位置,其中 k是非负数。

示例 1:

复制代码
输入: nums = [1,2,3,4,5,6,7], k = 3
输出:
复制代码
[5,6,7,1,2,3,4]
复制代码
解释:
向右轮转 1 步:
复制代码
[7,1,2,3,4,5,6]
复制代码
向右轮转 2 步:
复制代码
[6,7,1,2,3,4,5]
复制代码
向右轮转 3 步:
[5,6,7,1,2,3,4]
复制代码
class Solution {
    public void rotate(int[] nums, int k) {
        int[] temp= new int[k];
        int len=nums.length;
        if(k>len)
            k=k%len;
        for(int i=len-k;i<len;i++){
            temp[i-len+k]=nums[i];
        }
        for(int i=len-1;i>=k;i--){
            nums[i]=nums[i-k];
        }
        for(int i=0;i<k;i++){
            nums[i]=temp[i];
        }
    }
}

4、除了自身以外数组的乘积

给你一个整数数组 nums,返回 数组 answer ,其中 answer[i] 等于 nums 中除了 nums[i] 之外其余各元素的乘积 。

题目数据 保证 数组 nums之中任意元素的全部前缀元素和后缀的乘积都在 32 位 整数范围内。

不要使用除法, 且在 O(n) 时间复杂度内完成此题。

示例 1:

复制代码
输入: nums =[1,2,3,4]
输出:[24,12,8,6]

示例 2:

复制代码
输入: nums = [-1,1,0,-3,3]
输出: [0,0,9,0,0]

思路:

1、先算总乘积

2、重点判断 0 的数量

2 个 0 → 全 0

1 个 0 → 只有 0 位置有值

无 0 → 总乘积 / 当前数

复制代码
class Solution {
    public int[] productExceptSelf(int[] nums) {
        int mul = 1;           // 存储所有非0数字的总乘积
        int len = nums.length;
        int haszero = -1;      // 记录0出现的位置,-1=没有0
        int[] answer = new int[len];

        // 第一次遍历:算总乘积,同时检查有没有0、有几个0
        for(int i=0; i<len; i++){
            if(nums[i]==0){
                if(haszero == -1)
                    haszero = i;  // 第一个0
                else 
                    return answer; // 出现两个0 → 所有结果都是0
            } else {
                mul *= nums[i];   // 累乘非0数字
            }
        }

        // 情况1:数组里有 1 个 0
        if(haszero >= 0){
            for(int i=0; i<len; i++) answer[i]=0;
            answer[haszero] = mul; // 只有0的位置 = 总乘积
        }
        // 情况2:数组里没有 0
        else{
            for(int i=0; i<len; i++){
                answer[i] = mul / nums[i]; // 总乘积 / 当前数
            }
        }
        return answer;
    }
}

5、缺失的第一个正数

给你一个未排序的整数数组 nums ,请你找出其中没有出现的最小的正整数。

请你实现时间复杂度为 O(n) 并且只使用常数级别额外空间的解决方案。

示例 1:

复制代码
输入:nums = [1,2,0]
输出:3
解释:范围 [1,2] 中的数字都在数组中。

用一个 "计数数组" 标记哪些正整数出现过,然后从 1 开始找,第一个没出现的数就是答案

复制代码
class Solution {
    public int firstMissingPositive(int[] nums) {
        int n=nums.length;
        int[] arr=new int[n+1];
        for(int i=0;i<n;i++){
            if(nums[i]>0&&nums[i]<=n){
                arr[nums[i]]++;
            }
        }
        for(int i=1;i<=n;i++){
            if(arr[i]==0)
                return i;
        }
        return n+1;

    }
}

六、矩阵

1、矩阵置零

给定一个 mxn 的矩阵,如果一个元素为 0 ,则将其所在行和列的所有元素都设为 0 。请使用 原地 算法**。**

示例 1:

复制代码
输入:matrix = [[1,1,1],[1,0,1],[1,1,1]]
输出:[[1,0,1],[0,0,0],[1,0,1]]
复制代码
class Solution {
    public void setZeroes(int[][] matrix) {
        int hlen = matrix.length;
        int llen = matrix[0].length;
        List<Integer> h=new ArrayList<>();
        List<Integer> l=new ArrayList<>();
        for(int i=0;i<hlen;i++){
            for(int j=0;j<llen;j++){
                if(matrix[i][j]==0){
                    h.add(i);
                    l.add(j);
                }
            }
        }
        int hc=h.size();
        int lc=l.size(); 
        
        for(int i=0;i<hc;i++){
            for(int j=0;j<llen;j++){
                matrix[h.get(i)][j]=0;
            }
        }
         for(int i=0;i<hc;i++){
            for(int j=0;j<hlen;j++){
                matrix[j][l.get(i)]=0;
            }
        }
       
    
    }
}

2、螺旋矩阵

给你一个 mn 列的矩阵 matrix ,请按照 顺时针螺旋顺序 ,返回矩阵中的所有元素。

示例 1:

复制代码
输入:matrix = [[1,2,3],[4,5,6],[7,8,9]]
输出:[1,2,3,6,9,8,7,4,5]
复制代码
class Solution {
    public List<Integer> spiralOrder(int[][] matrix) {
        int top=0;
        int bottom=matrix.length-1;
        int left=0;
        int right=matrix[0].length-1;
        List<Integer> list=new ArrayList<>();
        while(top<=bottom&&left<=right){
            for(int i=left;i<=right;i++){
                list.add(matrix[top][i]);
            }
            top++;
            for(int i=top;i<=bottom;i++){
                list.add(matrix[i][right]);
            }
            right--;
            if (top <= bottom) {
                for (int i = right; i >= left; i--) {
                    list.add(matrix[bottom][i]);
                }
                bottom--; // 下边界上移
            }

            // 4. 从下到上遍历左边界(防止只剩一列时重复遍历)
            if (left <= right) {
                for (int i = bottom; i >= top; i--) {
                    list.add(matrix[i][left]);
                }
                left++; // 左边界右移
            }
        }
        return list;
    }
}

3、旋转图像

给定一个 n × n 的二维矩阵 matrix 表示一个图像。请你将图像顺时针旋转 90 度。

你必须在**原地** 旋转图像,这意味着你需要直接修改输入的二维矩阵。请不要使用另一个矩阵来旋转图像。

示例 1:

复制代码
输入:matrix = [[1,2,3],[4,5,6],[7,8,9]]
输出:[[7,4,1],[8,5,2],[9,6,3]]

旋转问题的思路:

  1. 转置 = 行列互换

  2. 行翻转 = 左右颠倒

    class Solution {
    public void rotate(int[][] matrix) {
    int n=matrix.length;
    for(int i=0;i<n;i++){
    for(int j=i;j<n;j++){
    int temp=matrix[i][j];
    matrix[i][j]=matrix[j][i];
    matrix[j][i]=temp;
    }
    }

    复制代码
         for(int i=0;i<n;i++){
             int left=0;
             int right=n-1;
             while(left<right){
                 int temp=matrix[i][left];
                 matrix[i][left]=matrix[i][right];
                 matrix[i][right]=temp;
                 left++;
                 right--;
             }
         }
     }

    }

4、搜索二维矩阵

编写一个高效的算法来搜索 m xn 矩阵 matrix 中的一个目标值 target 。该矩阵具有以下特性:

  • 每行的元素从左到右升序排列。
  • 每列的元素从上到下升序排列。

示例 1:

复制代码
输入:matrix = [[1,4,7,11,15],[2,5,8,12,19],[3,6,9,16,22],[10,13,14,17,24],[18,21,23,26,30]], target = 5
输出:true

思路:

从矩阵右上角 出发:

目标 > 当前数 → 向下走(这一行都太小)

目标 < 当前数 → 向左走(这一列都太大)

相等 → 找到

走出边界 → 没找到

复制代码
class Solution {
    public boolean searchMatrix(int[][] matrix, int target) {
        int m=matrix.length;   //行数
        int n=matrix[0].length;//列数
       
        int h=0; 
        int l=n-1;
        while(h<m&&l>=0){
            if(target==matrix[h][l])
                return true;
            else if(target>matrix[h][l]){
                h++;
            }
            else{
                l--;
            }
        }
       return false;
    }
}

七、链表

1、相交链表

给你两个单链表的头节点 headAheadB ,请你找出并返回两个单链表相交的起始节点。如果两个链表不存在相交节点,返回 null

图示两个链表在节点 c1 开始相交**:**

​编辑

题目数据 保证 整个链式结构中不存在环。

注意 ,函数返回结果后,链表必须 保持其原始结构

自定义评测:

评测系统 的输入如下(你设计的程序 不适用 此输入):

  • intersectVal - 相交的起始节点的值。如果不存在相交节点,这一值为 0
  • listA - 第一个链表
  • listB - 第二个链表
  • skipA - 在 listA 中(从头节点开始)跳到交叉节点的节点数
  • skipB - 在 listB 中(从头节点开始)跳到交叉节点的节点数

评测系统将根据这些输入创建链式数据结构,并将两个头节点 headAheadB 传递给你的程序。如果程序能够正确返回相交节点,那么你的解决方案将被 视作正确答案

示例 1:

​编辑

复制代码
输入:intersectVal = 8, listA = [4,1,8,4,5], listB = [5,6,1,8,4,5], skipA = 2, skipB = 3
输出:Intersected at '8'

思路:

两个人从不同起点出发,速度相同,走完自己的路就去走对方的路 ,一定会在「交点」相遇

(如果不相交,就会同时走到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 curA=headA;
        ListNode curB=headB;

        while(curA!=curB){
            curA=(curA==null)?headB:curA.next;
            curB=(curB==null)?headA:curB.next;
        }
        return curA;
    }
}

2、反转链表

给你单链表的头节点 head ,请你反转链表,并返回反转后的链表。

示例 1:

复制代码
输入:head = [1,2,3,4,5]
输出:[5,4,3,2,1]

思路:

维护两个指针一前一后遍历链表,不断把当前节点的 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 reverseList(ListNode head) {
        if(head==null)
            return head;
        ListNode pre=head;
        ListNode cur=head.next;
        head.next=null;
        while(cur!=null){
            ListNode nex=cur.next;
            cur.next=pre;
            pre=cur;
            cur=nex;
        }
        return pre;
    }
}

3、回文链表

给你一个单链表的头节点 head ,请你判断该链表是否为回文链表。如果是,返回 true ;否则,返回 false

示例 1:

复制代码
输入:head = [1,2,2,1]
输出:true

思路:先反转链表 ,再逐个结点比较是否相等

注意反转链表的时候要创建新节点,不然会对原链表进行修改,所以这里使用copyList先对原链表进行copy)

复制代码
/**
 * 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 copy=copyList(head);
        ListNode rev=reverse(copy);
        ListNode start=head;
        while(start!=null){
            if(start.val!=rev.val)
                return false;
            start=start.next;
            rev=rev.next;
        }
        return true;
    }
    public ListNode copyList(ListNode head){
        ListNode copy=new ListNode();
        ListNode curc=copy;
        ListNode curh=head;
        while(curh!=null){
            curc.val=curh.val;                  
            curh=curh.next;
            if(curh!=null){
                curc.next=new ListNode();
               
                curc=curc.next;
            }        
           
        }
        return copy;
    }
    public ListNode reverse(ListNode head){
        ListNode pre=head;
        ListNode cur=head.next;
        head.next=null;
        while(cur!=null){
            ListNode nex=cur.next;
            cur.next=pre;
            pre=cur;
            cur=nex;
        }
        return pre;
    }

}

4、环形链表

给你一个链表的头节点 head ,判断链表中是否有环。

如果链表中有某个节点,可以通过连续跟踪 next 指针再次到达,则链表中存在环。 为了表示给定链表中的环,评测系统内部使用整数 pos 来表示链表尾连接到链表中的位置(索引从 0 开始)。注意:pos 不作为参数进行传递。仅仅是为了标识链表的实际情况。

如果链表中存在环 ,则返回 true 。 否则,返回 false

示例 1:

复制代码
输入:head = [3,2,0,-4], pos = 1
输出:true
解释:链表中有一个环,其尾部连接到第二个节点。

思路:**快慢指针,**快指针步长为2,慢指针步长为1.

因为快指针与慢指针速度差是1,所以有环的话它们一定会相遇。

复制代码
/**
 * Definition for singly-linked list.
 * class ListNode {
 *     int val;
 *     ListNode next;
 *     ListNode(int x) {
 *         val = x;
 *         next = null;
 *     }
 * }
 */
public class Solution {
    public boolean hasCycle(ListNode head) {
        if(head==null||head.next==null)
            return false;
        ListNode slow=head;
        ListNode fast=head;
        while(fast!=null&&fast.next!=null){
            slow=slow.next;
            fast=fast.next.next;
            if(slow==fast)
                return true;
        }
        return false;
    }
}

5、环形链表II

给定一个链表的头节点 head ,返回链表开始入环的第一个节点。 如果链表无环,则返回 null

如果链表中有某个节点,可以通过连续跟踪 next 指针再次到达,则链表中存在环。 为了表示给定链表中的环,评测系统内部使用整数 pos 来表示链表尾连接到链表中的位置(索引从 0 开始 )。如果 pos-1,则在该链表中没有环。注意:pos 不作为参数进行传递,仅仅是为了标识链表的实际情况。

不允许修改链表。

示例 1:

复制代码
输入:head = [3,2,0,-4], pos = 1
输出:返回索引为 1 的链表节点
解释:链表中有一个环,其尾部连接到第二个节点。

思路:

第一步 :快慢指针判断是否有环

慢指针每次走 1 步

快指针每次走 2 步

如果相遇 → 有环

如果快指针走到 null → 无环

第二步 :找到环的入口节点

相遇后,把慢指针放回链表头

快慢指针都每次走 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) {
        if(head==null||head.next==null)
            return null;
        ListNode slow=head;
        ListNode fast=head;
        boolean hasCycle=false;
        while(fast!=null&&fast.next!=null){
            slow=slow.next;
            fast=fast.next.next;
            if(slow==fast){
                hasCycle=true;
                break;
            }
            
        }
        if(!hasCycle)
            return null;
        slow=head;
        while(slow!=fast){
            slow=slow.next;
            fast=fast.next;
        }
        return slow;
    }
}

6、合并两个有序链表

将两个升序链表合并为一个新的 升序 链表并返回。新链表是通过拼接给定的两个链表的所有节点组成的。

示例 1:

复制代码
输入:l1 = [1,2,4], l2 = [1,3,4]
输出:[1,1,2,3,4,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 merge=new ListNode(-1);
        ListNode cur1=list1;
        ListNode cur2=list2;
        ListNode cur=merge;
        while(cur1!=null&&cur2!=null){
            if(cur1.val<=cur2.val){
                cur.next=cur1;
                cur1=cur1.next;
            }
            else{
                cur.next=cur2;
                cur2=cur2.next;
            }
            cur=cur.next;
        }
        while(cur1!=null){
            cur.next=cur1;
            cur1=cur1.next;
            cur=cur.next;
        }
         while(cur2!=null){
            cur.next=cur2;
            cur2=cur2.next;
            cur=cur.next;
        }
        return merge.next;
    }
    
}

7、两数相加

给你两个 非空 的链表,表示两个非负的整数。它们每位数字都是按照 逆序 的方式存储的,并且每个节点只能存储 一位 数字。

请你将两个数相加,并以相同形式返回一个表示和的链表。

你可以假设除了数字 0 之外,这两个数都不会以 0 开头。

示例 1:

复制代码
输入:l1 = [2,4,3], l2 = [5,6,4]
输出:[7,0,8]
解释:342 + 465 = 807.

示例 2:

复制代码
输入:l1 = [0], l2 = [0]
输出:[0]

示例 3:

复制代码
输入:l1 = [9,9,9,9,9,9,9], l2 = [9,9,9,9]
输出:[8,9,9,9,0,0,0,1]

思路:从低位到高位进行相加,维护一个carry来标识是否进位

复制代码
/**
 * 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) {
        ListNode l=new ListNode(-1);
        ListNode add=l;
        int carry=0;
        while(l1!=null&&l2!=null){
            int val=l1.val+l2.val;
            if(carry==1){
                val++;
                carry=0;
            }
            if(val>9){
                carry=1;
                add.next=new ListNode(val-10);
            }
            else{
                add.next=new ListNode(val);
            }
            l1=l1.next;
            l2=l2.next;
            add=add.next;
        }
        while(l1!=null){
            int val=l1.val;
            if(carry==1){
                val++;
                carry=0;
            }
            if(val>9){
                carry=1;
                add.next=new ListNode(val-10);
            }
            else{
                add.next=new ListNode(val);
            }
            l1=l1.next;
            add=add.next;
        }
        while(l2!=null){
            int val=l2.val;
            if(carry==1){
                val++;
                carry=0;
            }
            if(val>9){
                carry=1;
                add.next=new ListNode(val-10);
            }
            else{
                add.next=new ListNode(val);
            }
            l2=l2.next;
            add=add.next;
        }
        if(carry==1){
            add.next=new ListNode(1);
        }
        return l.next;

    }
}

8、删除链表的第N个结点

给你一个链表,删除链表的倒数第 n个结点,并且返回链表的头结点。

示例 1:

复制代码
输入:head = [1,2,3,4,5], n = 2
输出:[1,2,3,5]

示例 2:

复制代码
输入:head = [1], n = 1
输出:[]

示例 3:

复制代码
输入:head = [1,2], n = 1
输出:[1]

思路:

1、先遍历一遍,算出链表总长度

2、把「倒数第 n 个」转换成「正数 第几个」

3、删除

复制代码
/**
 * 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 h=head;
        int count=0;
        while(h!=null){
            count++;
            h=h.next;
        }
        n=count+1-n;
        if(n==1){
            return head.next;
        }
        ListNode pre=head,cur,post;
        for(int i=0;i<n-2;i++){
            pre=pre.next;
        }
        cur=pre.next;
        post=cur.next;
        pre.next=post;
        return head;
    }
}

9、两两交换链表的结点

给你一个链表,两两交换其中相邻的节点,并返回交换后链表的头节点。你必须在不修改节点内部的值的情况下完成本题(即,只能进行节点交换)。

示例 1:

复制代码
输入:head = [1,2,3,4]
输出:[2,1,4,3]

示例 2:

复制代码
输入:head = []
输出:[]

示例 3:

复制代码
输入:head = [1]
输出:[1]

思路

1、虚拟头结点 ,规避表头交换的边界问题。

2、循环判断后续是否有两个节点 ,有则交换。

3、三步改指针完成两节点互换,再把前驱指针移到本组尾节点,继续处理下一组。

4、返回虚拟头结点的下一个节点。

复制代码
class Solution {
    public ListNode swapPairs(ListNode head) {
        ListNode dummy = new ListNode(0, head);
        ListNode pre = dummy;

        while (pre.next != null && pre.next.next != null) {
            // 取出当前两个节点
            ListNode fir = pre.next;
            ListNode sec = fir.next;

            // 两两交换指针
            pre.next = sec;
            fir.next = sec.next;
            sec.next = fir;

            // pre 前进,处理下一组
            pre = fir;
        }
        return dummy.next;
    }
}

10、K个一组翻转链表

给你链表的头节点 head ,每 k个节点一组进行翻转,请你返回修改后的链表。

k 是一个正整数,它的值小于或等于链表的长度。如果节点总数不是 k的整数倍,那么请将最后剩余的节点保持原有顺序。

你不能只是单纯的改变节点内部的值,而是需要实际进行节点交换。

示例 1:

复制代码
输入:head = [1,2,3,4,5], k = 2
输出:[2,1,4,3,5]

示例 2:

复制代码
输入:head = [1,2,3,4,5], k = 3
输出:[3,2,1,4,5]

思路:

1、前置校验:先往后遍历k个节点,不足k个直接返回当前头结点,无需反转。

2、局部反转:对当前连续k个节点执行链表原地反转。

3、递归拼接:原组的头结点(反转后变尾结点)指向下一组反转后的结果,返回当前组反转后的新头结点。

复制代码
class Solution {
    public ListNode reverseKGroup(ListNode head, int k) {
        ListNode check = head;
        // 节点不足 k 个,直接返回
        for (int i = 0; i < k; i++) {
            if (check == null) return head;
            check = check.next;
        }

        // 反转当前 k 个节点
        ListNode curr = head;
        ListNode prev = null;
        for (int i = 0; i < k; i++) {
            ListNode temp = curr.next;
            curr.next = prev;
            prev = curr;
            curr = temp;
        }

        // 递归处理后续链表,并拼接
        head.next = reverseKGroup(curr, k);
        return prev;
    }
}

11、随机链表的复制

给你一个长度为 n 的链表,每个节点包含一个额外增加的随机指针 random ,该指针可以指向链表中的任何节点或空节点。

构造这个链表的 深拷贝 。 深拷贝应该正好由 n全新 节点组成,其中每个新节点的值都设为其对应的原节点的值。新节点的 next 指针和 random 指针也都应指向复制链表中的新节点,并使原链表和复制链表中的这些指针能够表示相同的链表状态。复制链表中的指针都不应指向原链表中的节点

例如,如果原链表中有 XY 两个节点,其中 X.random --> Y 。那么在复制链表中对应的两个节点 xy ,同样有 x.random --> y

返回复制链表的头节点。

用一个由 n 个节点组成的链表来表示输入/输出中的链表。每个节点用一个 [val, random_index] 表示:

  • val:一个表示 Node.val 的整数。
  • random_index:随机指针指向的节点索引(范围从 0n-1);如果不指向任何节点,则为 null

你的代码 接受原链表的头节点 head 作为传入参数。

示例 1:

复制代码
输入:head = [[7,null],[13,0],[11,4],[10,2],[1,0]]
输出:[[7,null],[13,0],[11,4],[10,2],[1,0]]

解决方法:哈希映射法

复制代码
/*
// Definition for a Node.
class Node {
    int val;
    Node next;
    Node random;

    public Node(int val) {
        this.val = val;
        this.next = null;
        this.random = null;
    }
}
*/

class Solution {
    public Node copyRandomList(Node head) {
        Map<Node,Node> map=new HashMap<>();
        Node cur=head;
        while(cur!=null){
            map.put(cur,new Node(cur.val));
            cur=cur.next;
        }
        cur=head;
        while(cur!=null){
            Node node=map.get(cur);
            node.next=map.get(cur.next);
            node.random=map.get(cur.random);
            cur=cur.next;
        }
        return map.get(head);
    }
}

12、排序链表

给你链表的头结点 head ,请将其按 升序 排列并返回 排序后的链表

示例 1:

复制代码
输入:head = [4,2,1,3]
输出:[1,2,3,4]

(要求时间复杂度为 O(nlogn))

思路:因为有时间复杂度的要求,所以这里使用归并排序

递归拆两半 → 分别排序 → 合并有序链表

复制代码
/**
 * 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||head.next==null)    return head;
        ListNode mid=findMiddle(head);
        ListNode right=mid.next;
        mid.next=null;

        ListNode left=sortList(head);
        right=sortList(right);

        return merge(left,right);
    }
    ListNode findMiddle(ListNode head){
        ListNode slow=head;
        ListNode fast=head.next;
        while(fast!=null&&fast.next!=null){
            slow=slow.next;
            fast=fast.next.next;
        }
        return slow;
    }
    ListNode merge(ListNode left,ListNode right){
        ListNode dummy=new ListNode();
        ListNode cur=dummy;
        while(left!=null&&right!=null){
            if(left.val<right.val){
                cur.next=left;
                left=left.next;
            }
            else{
                cur.next=right;
                right=right.next;
            }
            cur=cur.next;
        }
        while(left!=null){
            cur.next=left;
            cur=cur.next;
            left=left.next;
        }
        while(right!=null){
            cur.next=right;
            cur=cur.next;
            right=right.next;
        }
        return dummy.next;
    }
}

13、合并k个升序链表

给你一个链表数组,每个链表都已经按升序排列。

请你将所有链表合并到一个升序链表中,返回合并后的链表。

示例 1:

复制代码
输入:lists = [[1,4,5],[1,3,4],[2,6]]
输出:[1,1,2,3,4,4,5,6]
解释:链表数组如下:
[
  1->4->5,
  1->3->4,
  2->6
]
将它们合并到一个有序链表中得到。
1->1->2->3->4->4->5->6

思路(分治 + 两两合并)

  1. 递归终止 :数组为空直接返回null;只剩一个链表,直接返回该链表。

  2. 两两合并:遍历数组,将相邻两个有序链表合并,存入新数组。

  3. 递归迭代:对新数组重复上述流程,直到最终只剩一个有序链表并返回。

    /**

    • 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) {
      // 终止条件1:空数组
      if (lists == null || lists.length == 0) {
      return null;
      }
      // 终止条件2:只剩1个链表,直接返回
      if (lists.length == 1) {
      return lists[0];
      }

      复制代码
       int n = lists.length;
       ListNode[] newList = new ListNode[(n + 1) / 2];
       int index = 0;
      
       // 两两合并
       for (int i = 0; i < n; i += 2) {
           ListNode l1 = lists[i];
           ListNode l2 = (i + 1 < n) ? lists[i + 1] : null;
           newList[index++] = merge(l1, l2);
       }
      
       // 递归合并
       return mergeKLists(newList);

      }

      // 合并两个有序链表(最优写法,不新建节点)
      public ListNode merge(ListNode left, ListNode right) {
      ListNode dummy = new ListNode(0);
      ListNode cur = dummy;

      复制代码
       while (left != null && right != null) {
           if (left.val < right.val) {
               cur.next = left;
               left = left.next;
           } else {
               cur.next = right;
               right = right.next;
           }
           cur = cur.next;
       }
      
       cur.next = left != null ? left : right;
       return dummy.next;

      }
      }

14 LRU缓存

请你设计并实现一个满足 LRU (最近最少使用) 缓存 约束的数据结构。

实现 LRUCache 类:

  • LRUCache(int capacity)正整数 作为容量 capacity 初始化 LRU 缓存
  • int get(int key) 如果关键字 key 存在于缓存中,则返回关键字的值,否则返回 -1
  • void put(int key, int value) 如果关键字 key 已经存在,则变更其数据值 value ;如果不存在,则向缓存中插入该组 key-value 。如果插入操作导致关键字数量超过 capacity ,则应该 逐出 最久未使用的关键字。

函数 getput 必须以 O(1) 的平均时间复杂度运行。

示例:

复制代码
输入
["LRUCache", "put", "put", "get", "put", "get", "put", "get", "get", "get"]
[[2], [1, 1], [2, 2], [1], [3, 3], [2], [4, 4], [1], [3], [4]]
输出
[null, null, null, 1, null, -1, null, -1, 3, 4]

解释
LRUCache lRUCache = new LRUCache(2);
lRUCache.put(1, 1); // 缓存是 {1=1}
lRUCache.put(2, 2); // 缓存是 {1=1, 2=2}
lRUCache.get(1);    // 返回 1
lRUCache.put(3, 3); // 该操作会使得关键字 2 作废,缓存是 {1=1, 3=3}
lRUCache.get(2);    // 返回 -1 (未找到)
lRUCache.put(4, 4); // 该操作会使得关键字 1 作废,缓存是 {4=4, 3=3}
lRUCache.get(1);    // 返回 -1 (未找到)
lRUCache.get(3);    // 返回 3
lRUCache.get(4);    // 返回 4

思路:

1.首先是关于LRU的理解:当内存空间满了的时候删除最久没使用的

2、实现方法HashMap + 双向链表(虚拟头尾节点)

  • HashMap: 按 key 查节点

  • 双向链表:维护访问顺序,头 = 最近使用,尾 = 最久未使用

    import java.util.HashMap;
    import java.util.Map;

    class LRUCache {

    复制代码
      // 双向链表节点定义
      public class ListNode {
          public ListNode pre;
          public ListNode nex;
          public int val;
          public int key;
          
          public ListNode() {}
          public ListNode(int v, int k) {
              val = v;
              key = k;
          }
      }
      
      // 核心数据结构
      public Map<Integer, ListNode> map = new HashMap<>();
      public int size;
      public int capacity;
      public ListNode head, tail; // 虚拟头尾哨兵节点
    
      public LRUCache(int capacity) {
          this.capacity = capacity;
          this.size = 0;
          head = new ListNode();
          tail = new ListNode();
          head.nex = tail;
          tail.pre = head;
      }
      
      public int get(int key) {
          ListNode node = map.get(key);
          if (node == null) {
              return -1;
          }
          // 访问过了,移动到链表头部
          moveToHead(node);
          return node.val;
      }
      
      public void put(int key, int value) {
          ListNode node = map.get(key);
          if (node == null) {
              // 新增节点
              node = new ListNode(value, key);
              map.put(key, node);
              addHead(node);
              size++;
              
              // 超过容量,淘汰最久未使用的尾部节点
              if (size > capacity) {
                  ListNode removedNode = removeTail(); // 移除链表尾部并返回该节点
                  map.remove(removedNode.key);         // 依据节点内的 key 精准移除 Map 中的记录
                  size--;
              }
          } else {
              // 修改已有节点的值,并移动到头部
              node.val = value;
              moveToHead(node);
          }
      }
    
      /**
       * 将已有节点移动到头部
       */
      public void moveToHead(ListNode node) {
          remove(node);
          addHead(node);
      }
    
      /**
       * 在链表头部插入新节点
       */
      public void addHead(ListNode node) {
          node.nex = head.nex;
          node.nex.pre = node;
          head.nex = node;
          node.pre = head;
      }
    
      /**
       * 从双向链表中彻底断开某个节点的指针连接(通用删除方法)
       */
      public void remove(ListNode node) {
          node.pre.nex = node.nex;
          node.nex.pre = node.pre;
      }
    
      /**
       * 淘汰末尾节点(直接复用 remove 方法,杜绝重复代码)
       * @return 被删除的尾部节点,以便主程序清理 Map
       */
      public ListNode removeTail() {
          ListNode node = tail.pre; // tail 是虚拟尾节点,tail.pre 才是真正的最后一个有效节点
          remove(node);             // 直接复用上面的 remove 方法
          return node;
      }

    }

八、二叉树

1、中序遍历

给定一个二叉树的根节点 root ,返回 它的 中序 遍历

示例 1:

复制代码
输入:root = [1,null,2,3]
输出:[1,3,2]
复制代码
/**
 * 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 void inOrder(TreeNode t,List<Integer> result){
        if(t==null)
            return;
        inOrder(t.left,result);
        result.add(t.val);
        inOrder(t.right,result);
    }
    public List<Integer> inorderTraversal(TreeNode root) {
        List<Integer> result=new ArrayList<>();
        
        inOrder(root,result);
        return result;
    }
}

2、二叉树的最大深度

给定一个二叉树 root ,返回其最大深度。

二叉树的 最大深度 是指从根节点到最远叶子节点的最长路径上的节点数。

示例 1:

复制代码
输入:root = [3,9,20,null,null,15,7]
输出:3

3、翻转二叉树

给你一棵二叉树的根节点 root ,翻转这棵二叉树,并返回其根节点。

示例 1:

复制代码
输入:root = [4,2,7,1,3,6,9]
输出:[4,7,2,9,6,3,1]
复制代码
/**
 * 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 invertTree(TreeNode root) {
        
        if(root==null||(root.left==null&&root.right==null))
           return root;
        TreeNode l=root.right;
        TreeNode r=root.left;
        root.left=invertTree(l);
        root.right=invertTree(r);
        return root;
    }
}

4、对称二叉树

给你一个二叉树的根节点 root , 检查它是否轴对称。

示例 1:

复制代码
输入:root = [1,2,2,3,4,4,3]
输出:true

思路:

  • 根节点为空,直接判定对称。
  • 递归比对左子树右子树 是否互为镜像:
    • 两节点都为空:对称;一个空一个非空:不对称。

    • 值相等,且左左配右右、左右配右左继续递归校验

      /**

      • 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 boolean isSymmetric(TreeNode root) {
        if(root==null)
        return true;
        return isMirror(root.left,root.right);
        }

        private boolean isMirror(TreeNode left, TreeNode right) {
        if(left==null&&right==null)
        return true;
        if(left==null||right==null)
        return false;
        return left.val==right.val&&isMirror(left.left,right.right)&&isMirror(left.right,right.left);
        }
        }

5、二叉树的直径

给你一棵二叉树的根节点,返回该树的 直径

二叉树的 直径 是指树中任意两个节点之间最长路径的 长度 。这条路径可能经过也可能不经过根节点 root

两节点之间路径的 长度 由它们之间边数表示。

示例 1:

复制代码
输入:root = [1,2,3,4,5]
输出:3
解释:3 ,取路径 [4,2,1,3] 或 [5,2,1,3] 的长度。

思路 (维护maxD表示最大直径)

1、核心定义 :二叉树直径 = 任意节点左子树深度 + 右子树深度的最大值。

2、后序遍历 求深度:递归计算每个节点左右子树深度。

3、实时更新最大直径:遍历每个节点时,用左右深度之和更新全局最大直径。

复制代码
/**
 * 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 {
    private int maxD=0;
    public int diameterOfBinaryTree(TreeNode root) {
        maxDepth(root);
        return maxD;
    }
    public int maxDepth(TreeNode root){
        if(root==null)
           return 0;
        int leftD=maxDepth(root.left);
        int rightD=maxDepth(root.right);
        maxD=Math.max(maxD,leftD+rightD);
        return Math.max(leftD,rightD)+1;
    }

}

6、二叉树的层序遍历

给你二叉树的根节点 root ,返回其节点值的 层序遍历 。 (即逐层地,从左到右访问所有节点)。

示例 1:

复制代码
输入:root = [3,9,20,null,null,15,7]
输出:[[3],[9,20],[15,7]]

思路 :层序遍历的关键在于队列

复制代码
/**
 * 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<List<Integer>> levelOrder(TreeNode root) {
        List<List<Integer>> result=new ArrayList<>();
        Queue<TreeNode> q=new LinkedList<>();
        if(root==null){
            return result;
        }
        q.add(root);
       

        while(!q.isEmpty()){
            List<Integer> list=new ArrayList<>();
            int leversize=q.size();
            for(int i=0;i<leversize;i++){

                TreeNode tn=q.poll();
                list.add(tn.val);
                if(tn.left!=null){
                    q.add(tn.left);
                
                }
                if(tn.right!=null){
                    q.add(tn.right);

                }
            }
            result.add(list);
        }
        return result;
    
    }
}

7、将有序数组转化为二叉搜索树

给你一个整数数组 nums ,其中元素已经按 升序 排列,请你将其转换为一棵 平衡 二叉搜索树。

示例 1:

复制代码
输入:nums = [-10,-3,0,5,9]
输出:[0,-3,9,-10,null,5]
解释:[0,-10,5,null,-3,null,9] 也将被视为正确答案:

思路:

1、取区间中点 作为当前根节点,保证树平衡。

2、左区间递归构建左子树,右区间递归构建右子树

复制代码
/**
 * 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 sortedArrayToBST(int[] nums) {
        return build(nums,0,nums.length-1);
    }

    public TreeNode build(int[] nums,int left,int right){
        if(left>right)
            return null;
        int mid=(left+right)/2;
        TreeNode t=new TreeNode(nums[mid]);
        t.left=build(nums,left,mid-1);
        t.right=build(nums,mid+1,right);
        return t;
    }
}

8、验证二叉搜索树

给你一个二叉树的根节点 root ,判断其是否是一个有效的二叉搜索树。

有效 二叉搜索树定义如下:

  • 节点的左子树只包含严格小于当前节点的数。
  • 节点的右子树只包含 严格大于 当前节点的数。
  • 所有左子树和右子树自身必须也是二叉搜索树。

示例 1:

复制代码
输入:root = [2,1,3]
输出:true

思路:使用min和max 表示当前节点的上下界递归验证

复制代码
/**
 * 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 boolean isValidBST(TreeNode root) {
        return helper(root, Long.MIN_VALUE, Long.MAX_VALUE);
    }

    // 辅助函数:传入当前节点,以及它必须满足的上下界
   public boolean helper(TreeNode root,long min,long max){
        if(root==null)
            return true;
        if(root.val<=min||root.val>=max)
            return false;
        return helper(root.left,min,root.val)&&helper(root.right,root.val,max);
   }
}

9、二叉搜索树中第K小的元素

给定一个二叉搜索树的根节点 root ,和一个整数 k ,请你设计一个算法查找其中第 k小的元素(k 从 1 开始计数)。

示例 1:

复制代码
输入:root = [3,1,4,null,2], k = 1
输出:1

思路 :二叉搜索树特点:中序遍历为递增序列

复制代码
/**
 * 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 {
    private int count=0;
    private int result=0;
    public int kthSmallest(TreeNode root, int k) {
        inOrder(root,k);
        return result;
    }
    public void inOrder(TreeNode root,int k){
        if(root==null)
            return;
        inOrder(root.left,k);
        count++;
        if(count==k)
            result=root.val;
        inOrder(root.right,k);
        return;
    }
}

10、二叉树右视图

给定一个二叉树的 根节点 root,想象自己站在它的右侧,按照从顶部到底部的顺序,返回从右侧所能看到的节点值。

示例 1:

**输入:**root = 1,2,3,null,5,null,4

输出:1,3,4

解释:

示例 2:

**输入:**root = 1,2,3,4,null,null,null,5

输出:1,3,4,5

解释:

思路:本质是层序遍历,找到每一层的最右点

如何找到最右点:在将下一层的节点逐步加入队列的时候会同时弹出队列中上一层的点,最后一个被弹出的就是这层的最右点

复制代码
/**
 * 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) {
        List<Integer> list=new ArrayList<>();
        Deque<TreeNode> q=new LinkedList<>();
        if(root==null)
            return list;
        q.add(root);
        while(!q.isEmpty()){
            int ls=q.size();
            TreeNode last=new TreeNode();
            for(int i=0;i<ls;i++){
                last=q.poll();
                if(last.left!=null)
                    q.offer(last.left);
                if(last.right!=null)
                    q.offer(last.right);
            }
            list.add(last.val);
        }
        return list;
    }
}

11、二叉树展开为链表

给你二叉树的根结点 root ,请你将它展开为一个单链表:

  • 展开后的单链表应该同样使用 TreeNode ,其中 right 子指针指向链表中下一个结点,而左子指针始终为 null
  • 展开后的单链表应该与二叉树 先序遍历 顺序相同。

示例 1:

复制代码
输入:root = [1,2,5,3,4,null,6]
输出:[1,null,2,null,3,null,4,null,5,null,6]
复制代码
/**
 * 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 void flatten(TreeNode root) {
        if(root==null)
            return;
        List<TreeNode> list=new ArrayList<>();
        preOrder(root,list);
       
        int ls=list.size();
        for(int i=0;i<ls-1;i++){
            list.get(i).left=null;
            list.get(i).right=list.get(i+1);
        } 
        root=list.get(0);
    }
    public void preOrder(TreeNode root,List<TreeNode> list){
        if(root==null)
            return;
        list.add(root);
        preOrder(root.left,list);
        preOrder(root.right,list);
    }
}

12、从前序和中序遍历构造二叉树

给定两个整数数组 preorderinorder ,其中 preorder 是二叉树的先序遍历inorder 是同一棵树的中序遍历,请构造二叉树并返回其根节点。

示例 1:

复制代码
输入: preorder = [3,9,20,15,7], inorder = [9,3,15,20,7]
输出: [3,9,20,null,null,15,7]

思路:

  • 前序首元素 = 根节点,通过哈希表定位根在中序的位置;

  • 中序数组以根划分为左子树区间右子树区间

  • 根据左子树节点数量,划分前序数组区间,递归构造左右子树。

    /**

    • 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 {
      private Map<Integer,Integer> indexMap=new HashMap<>();//目的是快速找到根节点的值在中序遍历序列的位置
      public TreeNode buildTree(int[] preorder, int[] inorder) {
      for(int i=0;i<inorder.length;i++){
      indexMap.put(inorder[i],i);
      }
      return build(preorder,0,preorder.length-1,0,inorder.length-1);
      }

      public TreeNode build(int[] preorder,int prestart,int preend,int instart,int inend){
      if(prestart>preend||instart>inend){
      return null;
      }
      TreeNode root=new TreeNode(preorder[prestart]);
      int index=indexMap.get(preorder[prestart]);
      int len=index-instart;
      root.left=build(preorder,prestart+1,prestart+len,instart,instart+len-1);
      root.right=build(preorder,prestart+len+1,preend,instart+len+1,inend);

      复制代码
       return root;

      }
      }

13、路径总和III

给定一个二叉树的根节点 root ,和一个整数 targetSum ,求该二叉树里节点值之和等于 targetSum路径 的数目。

路径 不需要从根节点开始,也不需要在叶子节点结束,但是路径方向必须是向下的(只能从父节点到子节点)。

示例 1:

复制代码
输入:root = [10,5,-3,3,2,null,11,3,-2,null,1], targetSum = 8
输出:3
解释:和等于 8 的路径有 3 条,如图所示。

这里有点像四.1求和为k的子数组:

数组前缀和

数组 [1,3,1,2] 一条直线,从头到尾,前缀和一直往后累加,哈希不用删。 \(prej-prei=target\)

核心思路:用一个哈希表来保留当前走过的路径所有前缀和及其出现的次数

实现:

  1. 前缀和定义 :根→当前节点累加和preSum,若preSum - preOld = targetpreOld=preSum-target即存在合法路径。

  2. 哈希存历史 :Map 记录当前路径前缀和出现次数,map[preSum-target]就是本条结尾的路径条数。

  3. 先存再递归:前缀和入 Map,递归左右子树,累加左右答案。

  4. 回溯撤销:左右走完删掉当前 preSum,隔离左右分支数据。

    class Solution {
    public int pathSum(TreeNode root, int targetSum) {
    Map<Long,Integer> preMap = new HashMap<>();
    preMap.put(0L,1);
    return dfs(root,0L,targetSum,preMap);
    }

    复制代码
     private int dfs(TreeNode cur, long preSum, int target, Map<Long,Integer> map){
         if(cur == null) return 0;
    
         preSum += cur.val;
         int cnt = map.getOrDefault(preSum-target,0);
         map.put(preSum, map.getOrDefault(preSum,0)+1);
    
         // 左右结果累加到cnt
         cnt += dfs(cur.left,preSum,target,map);
         cnt += dfs(cur.right,preSum,target,map);
    
         // 回溯
         map.put(preSum, map.get(preSum)-1);
         if(map.get(preSum)==0){
             map.remove(preSum);
         }
         return cnt;
     }

    }

14二叉树的最近公共祖先

给定一个二叉树, 找到该树中两个指定节点的最近公共祖先。

百度百科中最近公共祖先的定义为:"对于有根树 T 的两个节点 p、q,最近公共祖先表示为一个节点 x,满足 x 是 p、q 的祖先且 x 的深度尽可能大(一个节点也可以是它自己的祖先)。"

示例 1:

复制代码
输入:root = [3,5,1,6,2,0,8,null,null,7,4], p = 5, q = 1
输出:3

思路

  1. 当前节点为空 / 碰到 p / 碰到 q → 直接返回本节点;

  2. 递归左、右子树;

    • 左空→答案在右;
    • 右空→答案在左;
    • 左右都不为空:当前就是 LCA,return root。

    /**

    • Definition for a binary tree node.
    • public class TreeNode {
    • 复制代码
      int val;
    • 复制代码
      TreeNode left;
    • 复制代码
      TreeNode right;
    • 复制代码
      TreeNode(int x) { val = x; }
    • }
      */
      class Solution {
      public TreeNode lowestCommonAncestor(TreeNode root, TreeNode p, TreeNode q) {
      if(root==null||root==p||root==q)
      return root;
      TreeNode left=lowestCommonAncestor(root.left,p,q);
      TreeNode right=lowestCommonAncestor(root.right,p,q);
      if(left!=null&&right!=null){
      return root;
      }
      return left==null?right:left;
      }
      }

九、图论

图论重点是掌握深度优先搜索和广度优先搜索

深度优先搜索使用的是递归

广度优先搜索使用队列,类似于二叉树层序遍历的思想

1、岛屿数量

给你一个由 '1'(陆地)和 '0'(水)组成的的二维网格,请你计算网格中岛屿的数量。

岛屿总是被水包围,并且每座岛屿只能由水平方向和/或竖直方向上相邻的陆地连接形成。

此外,你可以假设该网格的四条边均被水包围。

示例 1:

复制代码
输入:grid = [
  ['1','1','1','1','0'],
  ['1','1','0','1','0'],
  ['1','1','0','0','0'],
  ['0','0','0','0','0']
]
输出:1

思路:深度 优先搜索,对于每一个岛屿,找出它的所有结点并置零

复制代码
class Solution {
    public int numIslands(char[][] grid) {
        if(grid==null||grid.length==0||grid[0].length==0)
            return 0;
        int rows=grid.length;
        int cols=grid[0].length;
        int count=0;
        for(int i=0;i<rows;i++){
            for(int j=0;j<cols;j++){
                if(grid[i][j]=='1'){
                    count++;
                    dfs(grid,i,j);
                }
            }
        }
        return count;
    }
    public void dfs(char[][]grid,int i,int j){
        int rows=grid.length;
        int cols=grid[0].length;
        if(i<0||j<0||i>=rows||j>=cols)
            return;
        if(grid[i][j]!='1')
            return;
        grid[i][j]='0';
        dfs(grid,i,j+1);
        dfs(grid,i,j-1);
        dfs(grid,i+1,j);
        dfs(grid,i-1,j);
    }
}

2、腐烂的橘子

思路( 模拟逐层腐烂)

1、每轮先标记当前腐烂橘子四周待腐烂的新鲜橘子,不立即修改网格。

2、统一将标记位置的橘子腐烂,若本轮无橘子腐烂则结束循环。

3、每成功一轮腐烂,计时 + 1。

4、循环结束后检查,仍有新鲜橘子则返回 - 1,否则返回总时长。

复制代码
class Solution {
    public int orangesRotting(int[][] grid) {
        int rows = grid.length;
        int cols = grid[0].length;
        boolean isFresh;
        int count = 0;

        while (true) {
            isFresh = false;
            // 用来标记这一轮要腐烂的橘子(关键修复)
            boolean[][] toRot = new boolean[rows][cols];

            // 第一步:标记谁会腐烂(不修改原数组)
            for (int i = 0; i < rows; i++) {
                for (int j = 0; j < cols; j++) {
                    if (grid[i][j] == 2) {
                        // 上下左右标记
                        if (i + 1 < rows && grid[i + 1][j] == 1) toRot[i + 1][j] = true;
                        if (i - 1 >= 0 && grid[i - 1][j] == 1) toRot[i - 1][j] = true;
                        if (j + 1 < cols && grid[i][j + 1] == 1) toRot[i][j + 1] = true;
                        if (j - 1 >= 0 && grid[i][j - 1] == 1) toRot[i][j - 1] = true;
                    }
                }
            }

            // 第二步:统一腐烂(不会干扰本轮遍历)
            boolean hasRot = false;
            for (int i = 0; i < rows; i++) {
                for (int j = 0; j < cols; j++) {
                    if (toRot[i][j]) {
                        grid[i][j] = 2;
                        hasRot = true;
                    }
                }
            }

            // 这一轮没有腐烂任何橘子,退出循环
            if (!hasRot) break;

            count++;
        }

        // 最后检查是否还有新鲜橘子(必须返回-1的情况)
        for (int i = 0; i < rows; i++) {
            for (int j = 0; j < cols; j++) {
                if (grid[i][j] == 1) return -1;
            }
        }

        return count;
    }
}

3、课程表

你这个学期必须选修 numCourses 门课程,记为 0numCourses - 1

在选修某些课程之前需要一些先修课程。 先修课程按数组 prerequisites 给出,其中 prerequisites[i] = [ai, bi] ,表示如果要学习课程 ai必须 先学习课程 bi

  • 例如,先修课程对 [0, 1] 表示:想要学习课程 0 ,你需要先完成课程 1

请你判断是否可能完成所有课程的学习?如果可以,返回 true ;否则,返回 false

示例 1:

复制代码
输入:numCourses = 2, prerequisites = [[1,0]]
输出:true
解释:总共有 2 门课程。学习课程 1 之前,你需要完成课程 0 。这是可能的。

示例 2:

复制代码
输入:numCourses = 2, prerequisites = [[1,0],[0,1]]
输出:false
解释:总共有 2 门课程。学习课程 1 之前,你需要先完成​课程 0 ;并且学习课程 0 之前,你还应先完成课程 1 。这是不可能的。

简洁思路(BFS,判环)

  1. 建图 + 入度统计b→a(先修 b 再学 a),邻接表存边,inDegree[a]++记录 a 的前置课程数。

  2. 入度为 0 入队:没有前置约束的课程先入队列。

  3. 逐层消边 :出队一门课x,遍历其后继,后继入度 - 1;入度变 0 就入队,统计已修课程count

  4. 判环count==总课程无环可以全修完,否则存在环修不完。

    class Solution {
    public boolean canFinish(int numCourses, int[][] prerequisites) {
    List<List>list = new ArrayList<>();
    int[] inDegree=new int[numCourses];
    for(int i=0;i<numCourses;i++){
    list.add(new ArrayList<>());
    }
    for(int[] arr:prerequisites){
    int a=arr[0];
    int b=arr[1];
    list.get(b).add(a);
    inDegree[a]++;
    }
    Deque queue=new LinkedList<>();
    int count=0;
    for(int i=0;i<numCourses;i++){
    if(inDegree[i]==0){
    queue.offerFirst(i);
    }
    }
    while(!queue.isEmpty()){
    int x=queue.pollLast();
    for(int cur:list.get(x)){
    inDegree[cur]--;
    if(inDegree[cur]==0){
    queue.offerFirst(cur);
    }
    }
    count++;
    }
    return count == numCourses;
    }
    }

4、实现Trie(前缀树)

Trie (发音类似 "try")或者说 前缀树 是一种树形数据结构,用于高效地存储和检索字符串数据集中的键。这一数据结构有相当多的应用情景,例如自动补全和拼写检查。

请你实现 Trie 类:

  • Trie() 初始化前缀树对象。
  • void insert(String word) 向前缀树中插入字符串 word
  • boolean search(String word) 如果字符串 word 在前缀树中,返回 true(即,在检索之前已经插入);否则,返回 false
  • boolean startsWith(String prefix) 如果之前已经插入的字符串 word 的前缀之一为 prefix ,返回 true ;否则,返回 false

示例:

复制代码
输入
["Trie", "insert", "search", "search", "startsWith", "insert", "search"]
[[], ["apple"], ["apple"], ["app"], ["app"], ["app"], ["app"]]
输出
[null, null, true, false, true, null, true]

解释
Trie trie = new Trie();
trie.insert("apple");
trie.search("apple");   // 返回 True
trie.search("app");     // 返回 False
trie.startsWith("app"); // 返回 True
trie.insert("app");
trie.search("app");     // 返回 True

思路:

1、TrieNode结点的构造:26个字母子节点数组 + isEnd标记(单词结尾)

2、创建一个root结点作为起始

复制代码
class Trie {
    public class TrieNode{
        public TrieNode[] children=new TrieNode[26];
        public boolean isEnd=false; 
    }
    public TrieNode root;

    public Trie() {
        root=new TrieNode();
    }
    
    public void insert(String word) {
        TrieNode cur=root;
        for(char c:word.toCharArray()){
            if(cur.children[c-'a']==null){
                cur.children[c-'a']=new TrieNode();
            }
            cur=cur.children[c-'a'];
        }
        cur.isEnd=true;
    }
    
    public boolean search(String word) {
        TrieNode cur=root;
        for(char c:word.toCharArray()){
            if(cur.children[c-'a']==null){
                return false;
            }
            cur=cur.children[c-'a'];
            
        }
        return cur.isEnd==true?true:false;
    }
    
    public boolean startsWith(String prefix) {
        TrieNode cur=root;
        for(char c:prefix.toCharArray()){
            if(cur.children[c-'a']==null){
                return false;
            }
            cur=cur.children[c-'a'];
            
        }
        return true;
    }
}

/**
 * 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);
 */

十、回溯法

回溯法精髓

核心公式:选择 → 递归深入 → 回溯撤销 本质就是暴力枚举所有可行解 + 及时剪枝,深度优先搜索 (DFS) 的经典应用。

通用四步模板

  1. 终止条件:满足要求,保存当前方案
  2. 遍历选择:循环枚举每一个可选元素 / 位置
  3. 做出选择:选当前元素,标记状态
  4. 递归 + 回溯 :递归下一层;返回后撤销选择、恢复状态

一句话口诀

有路就往前走,走不通就回头,恢复现场再试下一条路。


1、全排列

给定一个不含重复数字的数组 nums ,返回其 所有可能的全排列 。你可以 按任意顺序 返回答案。

示例 1:

复制代码
输入:nums = [1,2,3]
输出:[[1,2,3],[1,3,2],[2,1,3],[2,3,1],[3,1,2],[3,2,1]]

示例 2:

复制代码
输入:nums = [0,1]
输出:[[0,1],[1,0]]

思路

1、isUsed 数组标记数字是否已选,避免重复选取。

2、递归 :没选用数字→入集合、标记占用,递归往下。

3、回溯 :递归返回后撤销选择(删末尾元素、取消标记)。

4、集合长度等于数组长度:拷贝集合存入结果。

复制代码
class Solution {
    public List<List<Integer>> result=new ArrayList<>();
    public List<List<Integer>> permute(int[] nums) {  
        int len=nums.length;
        boolean[] isUsed=new boolean[len];
        List<Integer>list=new ArrayList<>();
        backTrack(nums,list,isUsed);
        return result;
    }
    public void backTrack(int[] nums,List<Integer> list,boolean[] isUsed){
        if(list.size()==nums.length){
            result.add(new ArrayList(list));
        }

        for(int i=0;i<nums.length;i++){
            if(isUsed[i]==false){
                isUsed[i]=true;
                list.add(nums[i]);
                backTrack(nums,list,isUsed);
                list.remove(list.size()-1);
                isUsed[i]=false;
            }
        }
    }
}

2、子集

给你一个整数数组 nums ,数组中的元素 互不相同 。返回该数组所有可能的子集(幂集)。

解集 不能 包含重复的子集。你可以按 任意顺序 返回解集。

示例 1:

复制代码
输入:nums = [1,2,3]
输出:[[],[1],[2],[1,2],[3],[1,3],[2,3],[1,2,3]]

示例 2:

复制代码
输入:nums = [0]
输出:[[],[0]]

思路:

1、直接先存当前集合 :每层递归进来立刻把当前子集存入答案(空集也收录)。

2、start 控制下标 :从start向后选元素,杜绝重复 组合。

3、选numsi入集合,递归i+1,递归返回后回溯删掉末尾。

在需要去重的回溯问题中,维护start是关键(包括后续的回溯法题型)

复制代码
class Solution {
    public List<List<Integer>> result=new ArrayList<>();
    public List<List<Integer>> subsets(int[] nums) {
        int len=nums.length;
       int start=0;
        List<Integer> list=new ArrayList<>();
        backTrack(list,start,nums);
        return result;
    }
    public void backTrack(List<Integer> list,int start,int[] nums){
        result.add(new ArrayList(list));
   
        for(int i=start;i<nums.length;i++){     
            list.add(nums[i]);
            backTrack(list,i+1,nums);
            list.remove(list.size()-1);     
        }
    }
}

3、电话号码的字母组合

给定一个仅包含数字 2-9 的字符串,返回所有它能表示的字母组合。答案可以按 任意顺序 返回。

给出数字到字母的映射如下(与电话按键相同)。注意 1 不对应任何字母。

示例 1:

复制代码
输入:digits = "23"
输出:["ad","ae","af","bd","be","bf","cd","ce","cf"]

简洁思路

  1. 预存映射:下标 0 对应数字 2 (abc),建立数字→字母列表。

  2. 当前拼接串长度 = 号码总长:拼接完成,存入结果。

  3. 按当前位数字取对应字母,逐个追加字符递归;递归完删除末尾字符回溯

    class Solution {
    private static final List<List> phoneChar = Arrays.asList(
    Arrays.asList('a', 'b', 'c'),
    Arrays.asList('d', 'e', 'f'),
    Arrays.asList('g', 'h', 'i'),
    Arrays.asList('j', 'k', 'l'),
    Arrays.asList('m', 'n', 'o'),
    Arrays.asList('p', 'q', 'r', 's'),
    Arrays.asList('t', 'u', 'v'),
    Arrays.asList('w', 'x', 'y', 'z')
    );
    public List result=new ArrayList<>();
    public List letterCombinations(String digits) {

    复制代码
         backTrack(new StringBuilder(),digits);
         return result;
     }
     public void backTrack(StringBuilder s,String digits){
         int len=s.length();
         if(len==digits.length()){
             result.add(s.toString());
             return;
         }
         int index=digits.charAt(len)-'0'-2;
         List<Character> chars=phoneChar.get(index);
         for(char c:chars){
             s.append(c);
             backTrack(s,digits);
             s.deleteCharAt(s.length()-1);
         }
     }

    }

4、组合总和

给你一个 无重复元素 的整数数组 candidates 和一个目标整数 target ,找出 candidates 中可以使数字和为目标数 target 的 所有不同组合 ,并以列表形式返回。你可以按 任意顺序 返回这些组合。

candidates 中的 同一个 数字可以 无限制重复被选取 。如果至少一个数字的被选数量不同,则两种组合是不同的。

对于给定的输入,保证和为 target 的不同组合数少于 150 个。

示例 1:

复制代码
输入:candidates = [2,3,6,7], target = 7
输出:[[2,2,3],[7]]
解释:
2 和 3 可以形成一组候选,2 + 2 + 3 = 7 。注意 2 可以使用多次。
7 也是一个候选, 7 = 7 。
仅有这两种组合。

思路

1、start=i :当前数字可以重复复用,只向后遍历 ,避免2,3 3,2重复组合。

2、剩余值remain<num跳过;可选就加入集合,remain-num递归。

3、递归回溯删掉末尾元素;remain==0拷贝集合存入答案。

复制代码
class Solution {
    public List<List<Integer>> result=new ArrayList<>();
    public List<List<Integer>> combinationSum(int[] candidates, int target) {
        List<Integer> list=new ArrayList<>();
        int remain=target;
        backTrack(remain,list,candidates,0);
        return result;
    }
    public void backTrack(int remain,List<Integer> list,int[] candidates,int start){
        if(remain==0){
            result.add(new ArrayList<>(list));
            return;
        }
        for(int i=start;i<candidates.length;i++){
            int num=candidates[i];
            if(num>remain)
               continue;
            list.add(num);
            backTrack(remain-num,list,candidates,i);
            list.remove(list.size()-1);
        }
    }
}

5、括号生成

数字 n 代表生成括号的对数,请你设计一个函数,用于能够生成所有可能的并且 有效的括号组合。

示例 1:

复制代码
输入:n = 3
输出:["((()))","(()())","(())()","()(())","()()()"]

示例 2:

复制代码
输入:n = 1
输出:["()"]

思路:

  • open 左括号数,close 右括号数

  • 左括号没加满open<n:加(递归、回溯删尾

  • 右括号少于左close<open:加)递归、回溯删尾

    class Solution {
    public List result=new ArrayList<>();
    public List generateParenthesis(int n) {
    backTrack(n,new StringBuilder(),0,0);
    return result;
    }
    public void backTrack(int n,StringBuilder sb,int open,int close){
    if(open==n&&close==n){
    result.add(sb.toString());
    return;
    }
    if(open<n){
    sb.append('(');
    backTrack(n,sb,open+1,close);
    sb.deleteCharAt(sb.length()-1);
    }
    if(close<open){
    sb.append(')');
    backTrack(n,sb,open,close+1);
    sb.deleteCharAt(sb.length()-1);
    }
    }
    }

6、单词搜索

给定一个 m x n 二维字符网格 board 和一个字符串单词 word 。如果 word 存在于网格中,返回 true ;否则,返回 false

单词必须按照字母顺序,通过相邻的单元格内的字母构成,其中"相邻"单元格是那些水平相邻或垂直相邻的单元格。同一个单元格内的字母不允许被重复使用。

示例 1:

复制代码
输入:board = [['A','B','C','E'],['S','F','C','S'],['A','D','E','E']], word = "ABCCED"
输出:true

思路

1、遍历棋盘每个格子当作起点;

2、当前坐标越界 / 已访问 / 字符不对→直接失败;

3、匹配就标记占用,往上下左右递归找下一位;

4、递归完撤销标记(回溯);

5、index走到单词末尾代表全部匹配成功。

复制代码
class Solution {
    public boolean exist(char[][] board, String word) {
        int rows = board.length;
        int cols = board[0].length;
        boolean[][] isUsed = new boolean[rows][cols];
        
        for (int i = 0; i < rows; i++) {
            for (int j = 0; j < cols; j++) {
                // 从每个起点开始搜索
                if (backTrack(board, word, isUsed, i, j, 0)) {
                    return true;
                }
            }
        }
        return false;
    }

    // 返回值:是否找到
    private boolean backTrack(char[][] board, String word, boolean[][] isUsed,
                             int i, int j, int index) {
        // 成功条件:已经匹配完所有字符
        if (index == word.length()) {
            return true;
        }

        int rows = board.length;
        int cols = board[0].length;

        // 越界 / 已使用 / 字符不匹配 → 直接返回false
        if (i < 0 || i >= rows || j < 0 || j >= cols 
            || isUsed[i][j] 
            || board[i][j] != word.charAt(index)) {
            return false;
        }

        // 标记已使用
        isUsed[i][j] = true;

        // 上下左右四个方向搜索
        boolean found = backTrack(board, word, isUsed, i - 1, j, index + 1)
                      || backTrack(board, word, isUsed, i + 1, j, index + 1)
                      || backTrack(board, word, isUsed, i, j - 1, index + 1)
                      || backTrack(board, word, isUsed, i, j + 1, index + 1);

        // 回溯:撤销标记
        isUsed[i][j] = false;

        return found;
    }
}

7、分割回文串

给你一个字符串 s,请你将s分割成一些 子串,使每个子串都是 回文串 。返回 s 所有可能的分割方案。

示例 1:

复制代码
输入:s = "aab"
输出:[["a","a","b"],["aa","b"]]

示例 2:

复制代码
输入:s = "a"
输出:[["a"]]

思路

1、start :当前截取起始下标,i不断右扩截取 start,i子串。

2、子串是回文:加入临时集合,递归从i+1继续往后分割。

(使用substring提取子串,左开右闭)

3、递归 回溯:弹出末尾字符串,枚举下一种切割方案。

4、start == s.length():整串分割完毕,保存当前组合。

复制代码
class Solution {
    public List<List<String>> result=new ArrayList<>();
    public List<List<String>> partition(String s) {
        List<String> list=new ArrayList<>();
        backTrack(s,0,list);
        return result;
    }
    public void backTrack(String s,int start,List<String> list){
        int n=s.length();
        if(start==n){
            result.add(new ArrayList(list));
            return;
        }
        for(int i=start;i<n;i++){
            String ss=s.substring(start,i+1);
            if(isHuiwen(ss)){
                list.add(ss);
                backTrack(s,i+1,list);
                list.remove(list.size()-1);
            }       
        }
    }
    public boolean isHuiwen(String ss){
        int n=ss.length();
        int left=0;
        int right=n-1;
        while(left<=right){
            if(ss.charAt(left)!=ss.charAt(right)){
                return false;
            }
            left++;
            right--;
        }
        return true;
    }
}

8、N皇后

按照国际象棋的规则,皇后可以攻击与之处在同一行或同一列或同一斜线上的棋子。

n 皇后问题 研究的是如何将 n 个皇后放置在 n×n 的棋盘上,并且使皇后彼此之间不能相互攻击。

给你一个整数 n ,返回所有不同的 n皇后问题 的解决方案。

每一种解法包含一个不同的 n 皇后问题 的棋子放置方案,该方案中 'Q''.' 分别代表了皇后和空位。

示例 1:

复制代码
输入:n = 4
输出:[[".Q..","...Q","Q...","..Q."],["..Q.","Q...","...Q",".Q.."]]
解释:如上图所示,4 皇后问题存在两个不同的解法。

简洁思路(关键在于2用三个数组判冲突)

1、row 代表当前要放皇后的行,逐行递归,一行只放一个皇后;

2、遍历所有列colIdx,用三个数组判冲突:
col\[\]:同列不能重复;
diag1row-col+n:左上 - 右下主斜线;
diag2row+col:右上 - 左下副斜线;

3、无冲突:标记三条线占用、构造.+Q 的行字符串入集合,递归下一行row+1;

4、回溯:递归返回,撤销标记、删掉末尾行;

5、row==n:棋盘填满,拷贝当前组合存入结果。

复制代码
class Solution {
    public List<List<String>> result=new ArrayList<>();
    boolean[] col, diag1, diag2;

    public List<List<String>> solveNQueens(int n) {
        col = new boolean[n];
        diag1 = new boolean[2 * n];
        diag2 = new boolean[2 * n];
        List<String> list=new ArrayList<>();
        backTrack(0,n,list);
        return result;
    }

    public void backTrack(int row,int n,List<String> list){
        if(row==n){
            result.add(new ArrayList(list));
            return;
        }
        // 枚举每一列
        for(int colIdx=0;colIdx<n;colIdx++){
            int d1 = row - colIdx + n;
            int d2 = row + colIdx;
            // 列、两条斜线都没被占用
            if(!col[colIdx] && !diag1[d1] && !diag2[d2]){
                col[colIdx]=true;
                diag1[d1]=true;
                diag2[d2]=true;

                // 生成一行字符串
                StringBuilder sb=new StringBuilder();
                for(int j=0;j<n;j++){
                    sb.append(j==colIdx?'Q':'.');
                }
                list.add(sb.toString());

                backTrack(row+1,n,list);

                // 回溯
                list.remove(list.size()-1);
                col[colIdx]=false;
                diag1[d1]=false;
                diag2[d2]=false;
            }
        }
    }
}

十一、二分查找

1、搜索插入位置

给定一个排序数组和一个目标值,在数组中找到目标值,并返回其索引。如果目标值不存在于数组中,返回它将会被按顺序插入的位置。

请必须使用时间复杂度为 O(log n) 的算法。

示例 1:

复制代码
输入: nums = [1,3,5,6], target = 5
输出: 2
复制代码
class Solution {
    public int searchInsert(int[] nums, int target) {
        int left=0;
        int right=nums.length-1;
        while(left<=right){
            int mid=(left+right)/2;
            if(nums[mid]==target)
                return mid;
            if(nums[mid]>target)
                right=mid-1;
            else
                left=mid+1;
        }
        return left;
    }
}

2、搜索二维矩阵

给你一个满足下述两条属性的 m x n 整数矩阵:

  • 每行中的整数从左到右按非严格递增顺序排列。
  • 每行的第一个整数大于前一行的最后一个整数。

给你一个整数 target ,如果 target 在矩阵中,返回 true ;否则,返回 false

示例 1:

复制代码
输入:matrix = [[1,3,5,7],[10,11,16,20],[23,30,34,60]], target = 3
输出:true

思路 :将**二维数组转化为一维数组,**进行二分查找

注意区分六.4的搜索二维矩阵II,两道题的矩阵特点不同

复制代码
class Solution {
    public boolean searchMatrix(int[][] matrix, int target) {
        int m=matrix.length;
        int n=matrix[0].length;
        int right=m*n-1;
        int left=0;
        while(left<=right){
            int mid=(left+right)/2;
            int row=mid/n;
            int col=mid%n;
            if(matrix[row][col]==target)
                return true;
            if(target>matrix[row][col])
                left=mid+1;
            else
                right=mid-1;
        }
        return false;
    }
}

3、在排序数组中查找元素的第一个和最后一个

给你一个按照非递减顺序排列的整数数组 nums,和一个目标值 target。请你找出给定目标值在数组中的开始位置和结束位置。

如果数组中不存在目标值 target,返回 [-1, -1]

你必须设计并实现时间复杂度为 O(log n) 的算法解决此问题。

示例 1:

复制代码
输入:nums = [5,7,7,8,8,10], target = 8
输出:[3,4]

思路

1、先用二分找第一个 等于 target的位置left;

2、校验:越界或值不等,直接返回-1,-1

3、从left向后遍历,找到最后一个target的下一位;

4、首尾存入数组返回。

复制代码
class Solution {
    public int[] searchRange(int[] nums, int target) {
        int[] result={-1,-1};
        int right=nums.length-1;
        int left=0;
        while(left<=right){
            int mid=(left+right)/2;
            if(target>nums[mid])
                left=mid+1;
            else
                right=mid-1;
        }
        if(left==nums.length||nums[left]!=target)
            return result;
        int r=left+1;
        while(r!=nums.length&&nums[r]==target)
            r++;
        result[0]=left;
        result[1]=r-1;
        return result;
    }
}

4、搜索旋转排序数组

整数数组 nums 按升序排列,数组中的值 互不相同

在传递给函数之前,nums 在预先未知的某个下标 k0 <= k < nums.length)上进行了 向左旋转 ,使数组变为 [nums[k], nums[k+1], ..., nums[n-1], nums[0], nums[1], ..., nums[k-1]](下标 从 0 开始 计数)。例如, [0,1,2,4,5,6,7] 下标 3 上向左旋转后可能变为 [4,5,6,7,0,1,2]

给你 旋转后 的数组 nums 和一个整数 target ,如果 nums 中存在这个目标值 target ,则返回它的下标,否则返回 -1

你必须设计一个时间复杂度为**O(log n)**的算法解决此问题。

示例 1:

复制代码
输入:nums = [4,5,6,7,0,1,2], target = 0
输出:4

示例 2:

复制代码
输入:nums = [4,5,6,7,0,1,2], target = 3
输出:-1

思路

数组局部有序、整体旋转,二分查找:

  1. 取中点 mid,命中直接返回下标;

  2. 判断左区间是否有序

    • 有序且目标在左区间 → 收缩右边界;否则查右区间;
  3. 左区间无序则右区间一定有序

    • 有序且目标在右区间 → 收缩左边界;否则查左区间;
  4. 循环结束未找到,返回 -1

    class Solution {
    public int search(int[] nums, int target) {
    int left = 0;
    int right = nums.length - 1;

    复制代码
         while (left <= right) {
             int mid = left + (right - left) / 2;
             
             if (nums[mid] == target) {
                 return mid;
             }
             
             // 左半边有序
             if (nums[left] <= nums[mid]) {
                 if (target >= nums[left] && target < nums[mid]) {
                     right = mid - 1;
                 } else {
                     left = mid + 1;
                 }
             } 
             // 右半边有序
             else {
                 if (target > nums[mid] && target <= nums[right]) {
                     left = mid + 1;
                 } else {
                     right = mid - 1;
                 }
             }
         }
         return -1;
     }

    }

5、寻找旋转排序数组中的最小值

已知一个长度为 n 的数组,预先按照升序排列,经由 1n旋转 后,得到输入数组。例如,原数组 nums = [0,1,2,4,5,6,7] 在变化后可能得到:

  • 若旋转 4 次,则可以得到 [4,5,6,7,0,1,2]
  • 若旋转 7 次,则可以得到 [0,1,2,4,5,6,7]

注意,数组 [a[0], a[1], a[2], ..., a[n-1]] 旋转一次 的结果为数组 [a[n-1], a[0], a[1], a[2], ..., a[n-2]]

给你一个元素值 互不相同 的数组 nums ,它原来是一个升序排列的数组,并按上述情形进行了多次旋转。请你找出并返回数组中的 最小元素

你必须设计一个时间复杂度为 O(log n) 的算法解决此问题。

示例 1:

复制代码
输入:nums = [3,4,5,1,2]
输出:1
解释:原数组为 [1,2,3,4,5] ,旋转 3 次得到输入数组。

思路:在旋转数组中,数组分为左右两段有序数组,最小值在右边的有序数组的起始

1、比较 numsmid 与 numsright

2、numsmid > numsright:最小值在右半段,left = mid + 1

反之:最小值在左半段(含 mid),right = mid

3、循环结束 left == right,该位置即为最小值。

复制代码
class Solution {
    public int findMin(int[] nums) {
    int left = 0, right = nums.length - 1;
    while (left < right) {
        int mid = left + (right - left) / 2;
        if (nums[mid] > nums[right]) {
            left = mid + 1;
        } else {
            right = mid;
        }
    }
    return nums[left];
}
}

6、寻找两个正序数组的中位数

给定两个大小分别为 mn 的正序(从小到大)数组 nums1nums2。请你找出并返回这两个正序数组的 中位数

算法的时间复杂度应该为 O(log (m+n))

示例 1:

复制代码
输入:nums1 = [1,3], nums2 = [2]
输出:2.00000
解释:合并数组 = [1,2,3] ,中位数 2

示例 2:

复制代码
输入:nums1 = [1,2], nums2 = [3,4]
输出:2.50000
解释:合并数组 = [1,2,3,4] ,中位数 (2 + 3) / 2 = 2.5

思路:

1、把求中位数转为找两有序数组第 k 小元素

2、总长度奇数:直接取中间第 k 小值;偶数:取中间两个数求平均,强转浮点避免整数除出错。

3、递归findKth:始终以短数组为基准,每次对比两数组 k/2 位置,舍去数值更小的前半段 ,缩小 k 继续查找。

4、边界 :短数组遍历完则取另一数组元素;k=1 直接取两数组头部较小值。

时间复杂度 O(log(m+n))。

复制代码
class Solution {
    // 求两个有序数组的中位数
    public double findMedianSortedArrays(int[] nums1, int[] nums2) {
        int len1 = nums1.length;
        int len2 = nums2.length;
        int totalLen = len1 + len2; // 两数组总长度

        // 总长度为奇数:中位数 = 第 (总长度/2 + 1) 小的数
        if ((totalLen) % 2 == 1) {
            return findKth(nums1, 0, nums2, 0, totalLen / 2 + 1);
        }
        // 总长度为偶数:中位数 = 中间两个数的平均值,强转double避免整数除法
        else {
            return (double) (findKth(nums1, 0, nums2, 0, totalLen / 2) 
                    + findKth(nums1, 0, nums2, 0, totalLen / 2 + 1)) / 2;
        }
    }

    /**
     * 递归查找两个有序数组中第 k 小的元素
     * @param nums1 数组1
     * @param s1    数组1起始查找下标
     * @param nums2 数组2
     * @param s2    数组2起始查找下标
     * @param k     目标:找第k小元素
     * @return 第k小的数值
     */
    public int findKth(int[] nums1, int s1, int[] nums2, int s2, int k) {
        // 当前两个查找区间的长度
        int len1 = nums1.length - s1;
        int len2 = nums2.length - s2;

        // 保证 nums1 始终是较短的数组,简化边界判断
        if (len1 > len2) {
            return findKth(nums2, s2, nums1, s1, k);
        }

        // 短数组已全部遍历完,直接从长数组取第k个元素
        if (len1 == 0) {
            return nums2[s2 + k - 1];
        }

        // k=1 即找最小元素,取两数组当前首位较小值
        if (k == 1) {
            return Math.min(nums1[s1], nums2[s2]);
        }

        // 分别取两个数组中 k/2 位置(防止越界,最多取到数组末尾)
        int p1 = s1 + Math.min(k / 2, len1) - 1;
        int p2 = s2 + Math.min(k / 2, len2) - 1;

        // nums1[p1] 更小,说明前半部分不可能有第k小元素,舍弃这部分
        if (nums1[p1] < nums2[p2]) {
            return findKth(nums1, p1 + 1, nums2, s2, k - (p1 - s1 + 1));
        }
        // 舍弃 nums2 前半部分
        else {
            return findKth(nums1, s1, nums2, p2 + 1, k - (p2 - s2 + 1));
        }
    }
}

十二、栈

关于栈的问题比较常见的是维护递增或递减栈

1、有效的括号

给定一个只包括 '('')''{''}''['']' 的字符串 s ,判断字符串是否有效。

有效字符串需满足:

  1. 左括号必须用相同类型的右括号闭合。
  2. 左括号必须以正确的顺序闭合。
  3. 每个右括号都有一个对应的相同类型的左括号。

示例 1:

**输入:**s = "()"

**输出:**true

简洁思路

  1. 核心思想 :利用的后进先出特性,匹配成对括号。

  2. 遍历字符:

    • 遇到左括号 ,向栈压入对应右括号
    • 遇到右括号 :栈空(无匹配左括号)或弹出元素与当前字符不等 → 直接返回 false
  3. 遍历结束:栈为空说明所有括号全部匹配,返回 true;否则存在多余左括号,返回 false

    class Solution {
    public boolean isValid(String s) {
    Stack stack=new Stack<>();
    int len=s.length();
    for(int i=0;i<len;i++){
    char c=s.charAt(i);
    if(c=='(')
    stack.push(')');
    else if(c=='{')
    stack.push('}');
    else if(c=='[')
    stack.push(']');
    else{
    if(stack.isEmpty()||stack.pop()!=c)
    return false;
    }

    复制代码
         }
         return stack.isEmpty();
    
     }

    }

2、最小栈

设计一个支持 pushpoptop 操作,并能在常数时间内检索到最小元素的栈。

实现 MinStack 类:

  • MinStack() 初始化堆栈对象。
  • void push(int value) 将元素 value 推入堆栈。
  • void pop() 删除堆栈顶部的元素。
  • int top() 获取堆栈顶部的元素。
  • int getMin() 获取堆栈中的最小元素。

示例 1:

复制代码
输入:
["MinStack","push","push","push","getMin","pop","top","getMin"]
[[],[-2],[0],[-3],[],[],[],[]]

输出:
[null,null,null,null,-3,null,0,-2]

核心思路 :两个栈分工

普通栈stack :正常存所有入栈元素,实现push/pop/top。

最小栈minStack:只存当前序列的最小值,保证栈顶永远是全局最小值,实现getMin。

为什么要用minStack来维护最小元素:可能有多个元素等于最小值

复制代码
class MinStack {
    private Stack<Integer> stack;
    private Stack<Integer> minStack;

    public MinStack() {
        stack=new Stack<>();
        minStack=new Stack<>();
    }
    
    public void push(int val) {
        stack.push(val);
        if(minStack.isEmpty()||minStack.peek()>=val)
            minStack.push(val);
    }
    
    public void pop() {
        int value=stack.pop();
        if(minStack.peek()==value)
            minStack.pop();
    }
    
    public int top() {
        return stack.peek();
    }
    
    public int getMin() {
        if(minStack.isEmpty())
            return 0;
        return minStack.peek();
    }
}

/**
 * Your MinStack object will be instantiated and called as such:
 * MinStack obj = new MinStack();
 * obj.push(val);
 * obj.pop();
 * int param_3 = obj.top();
 * int param_4 = obj.getMin();
 */

3、字符串解码

给定一个经过编码的字符串,返回它解码后的字符串。

编码规则为: k[encoded_string],表示其中方括号内部的 encoded_string 正好重复 k 次。注意 k 保证为正整数。

你可以认为输入字符串总是有效的;输入字符串中没有额外的空格,且输入的方括号总是符合格式要求的。

此外,你可以认为原始数据不包含数字,所有的数字只表示重复的次数 k ,例如不会出现像 3a2[4] 的输入。

测试用例保证输出的长度不会超过 105

示例 1:

复制代码
输入:s = "3[a]2[bc]"
输出:"aaabcbc"

示例 2:

复制代码
输入:s = "3[a2[c]]"
输出:"accaccacc"

核心思路(双栈解法)

利用两个栈 分别暂存倍数数字前置字符串,遇到括号时保存现场、括号结束后拼接还原,适配嵌套编码。

  1. 变量与栈作用
  • numStack:存储重复倍数
  • strStack:存储[前已拼接的字符串
  • curr:记录当前括号内 / 当前段的字符
  • num:累计解析多位数

2、遍历字符串

  • 数字 :拼接多位数 num = num * 10 + 个位数字
  • [ :把当前字符串、数字压栈,重置currnum,开始解析括号内内容
  • ] :弹出栈中前置字符串与倍数,将当前字符串重复对应次数,拼接到前置字符串,赋值给curr
  • 字母 :直接追加到curr

3、最终 遍历完成,curr即为解码结果。

为什么要用栈来保存前置字符串而不用变量?因为可能会出现多层嵌套

复制代码
class Solution {
    public String decodeString(String s) {
        // 存数字
        Stack<Integer> numStack = new Stack<>();
        // 存字符串
        Stack<StringBuilder> strStack = new Stack<>();
        StringBuilder curr = new StringBuilder();
        int num = 0;

        for (char c : s.toCharArray()) {
            if (Character.isDigit(c)) {
                // 处理多位数 100 → 1*10+0=10 → 10*10+0=100
                num = num * 10 + (c - '0');
            } 
            else if (c == '[') {
                // 把当前结果和数字压栈,保存现场
                strStack.push(curr);
                numStack.push(num);
                // 重置
                curr = new StringBuilder();
                num = 0;
            } 
            else if (c == ']') {
                // 弹出:之前的字符串 + 倍数
                StringBuilder prev = strStack.pop();
                int k = numStack.pop();
                // 把当前字符串重复 k 次,接回去
                while (k-- > 0) {
                    prev.append(curr);
                }
                curr = prev;
            } 
            else {
                // 普通字母
                curr.append(c);
            }
        }
        return curr.toString();
    }
}

4、每日温度

给定一个整数数组 temperatures ,表示每天的温度,返回一个数组 answer ,其中 answer[i] 是指对于第 i 天,下一个更高温度出现在几天后。如果气温在这之后都不会升高,请在该位置用 0 来代替。

示例 1:

复制代码
输入: temperatures = [73,74,75,71,69,72,76,73]
输出: [1,1,4,2,1,1,0,0]

核心 :维护一个单调递减栈

复制代码
class Solution {
    public int[] dailyTemperatures(int[] temperatures) {
        int n = temperatures.length;
        Stack<Integer> stack = new Stack<>();
        int[] res = new int[n];

        for (int i = 0; i < n; i++) {
            // 维持单调递减栈
            while (!stack.isEmpty() && temperatures[i] > temperatures[stack.peek()]) {
                int day = stack.pop();
                res[day] = i - day;
            }
            stack.push(i);
        }
        return res;
    }
}

5、柱状图中的最大矩形

给定 n 个非负整数,用来表示柱状图中各个柱子的高度。每个柱子彼此相邻,且宽度为 1 。

求在该柱状图中,能够勾勒出来的矩形的最大面积。

示例 1:

复制代码
输入:heights = [2,1,5,6,2,3]
输出:10
解释:最大的矩形为图中红色区域,面积为 10

思路

使用单调递增栈存放柱子下标,快速定位左右边界:

  1. 遍历数组,当前柱高度小于栈顶高度时,弹出栈顶下标。

  2. 以弹出下标对应高度为矩形高,左边界 为弹出后的栈顶(栈空取-1),右边界 为当前下标,按 宽度=右边界-左边界-1 算面积,更新最大值。

  3. 遍历结束后,栈中剩余元素右侧无更矮柱,右边界设为数组长度,重复计算面积。

  4. 最终返回最大矩形面积。

    import java.util.Stack;

    class Solution {
    public int largestRectangleArea(int[] heights) {
    Stack stack = new Stack<>();
    int n = heights.length;
    int maxArea = 0;
    for (int i = 0; i < n; i++) {
    while (!stack.isEmpty() && heights[i] < heights[stack.peek()]) {
    int j = stack.pop();
    // 左边界:弹出后新栈顶,栈空则为 -1
    int left = stack.isEmpty() ? -1 : stack.peek();
    int h = heights[j];
    int w = i - left - 1;
    maxArea = Math.max(h * w, maxArea);
    }
    stack.push(i);
    }
    // 处理栈中剩余元素
    while (!stack.isEmpty()) {
    int j = stack.pop();
    int left = stack.isEmpty() ? -1 : stack.peek();
    int h = heights[j];
    int w = n - left - 1;
    maxArea = Math.max(h * w, maxArea);
    }
    return maxArea;
    }
    }

十三、堆

可以直接使用Java的PriorityQueue 类,默认是小顶堆

如果要使用大顶堆,则可以使用Lambda指定

PriorityQueue<Integer> maxHeap = new PriorityQueue<>((a, b) -> b - a);

1、数组中的第k个最大元素

给定整数数组 nums 和整数 k,请返回数组中第 k 个最大的元素。

请注意,你需要找的是数组排序后的第 k 个最大的元素,而不是第 k 个不同的元素。

你必须设计并实现时间复杂度为 O(n) 的算法解决此问题。

示例 1:

复制代码
输入:[3,2,1,5,6,4],k = 2
输出: 5

思路 :利用容量为 k 的小顶堆 ,筛选出数组里前 k 大的数,最终堆顶就是第 k 大元素

复制代码
import java.util.PriorityQueue;

class Solution {
    public int findKthLargest(int[] nums, int k) {
        // Java 的 PriorityQueue 默认是小顶堆
        PriorityQueue<Integer> minHeap = new PriorityQueue<>(k);

        for (int num : nums) {
            if (minHeap.size() < k) {
                minHeap.offer(num);
            } else if (num > minHeap.peek()) {
                minHeap.poll(); // 弹出堆顶最小的
                minHeap.offer(num); // 加入更大的
            }
        }

        // 堆顶就是第 k 个最大的元素
        return minHeap.peek();
    }
}

2、前k个高频元素

给你一个整数数组 nums 和一个整数 k ,请你返回其中出现频率前 k 高的元素。你可以按 任意顺序 返回答案。

示例 1:

**输入:**nums = 1,1,1,2,2,3, k = 2

输出:1,2

思路:

  1. 哈希表统计每个数字的出现频率。
  2. 小顶堆 (按频率排序),维持堆大小为 k
    • 堆未满直接入队;
    • 元素频率大于堆顶,则替换堆顶。
  3. 最终堆内即为前 k 个高频元素,转为数组返回。

代码技巧:使用Lambda指定堆按照key对应的value进行排序,同时直接遍历map.keySet()防止重复

复制代码
PriorityQueue<Integer> heap=new PriorityQueue<>((a,b)->map.get(a)-map.get(b));
       for(int num : map.keySet()){
            if(heap.size() < k){
                heap.offer(num);
            }
            // 当前数字频率 > 堆顶频率,替换
            else if(map.get(num) > map.get(heap.peek())){
                heap.poll();
                heap.offer(num);
            }
        }

class Solution {
    public int[] topKFrequent(int[] nums, int k) {
       Map<Integer,Integer> map=new HashMap<>();
       int n=nums.length;
       for(int i:nums){
          map.put(i,map.getOrDefault(i,0)+1);
       }
       PriorityQueue<Integer> heap=new PriorityQueue<>((a,b)->map.get(a)-map.get(b));
       for(int num : map.keySet()){
            if(heap.size() < k){
                heap.offer(num);
            }
            // 当前数字频率 > 堆顶频率,替换
            else if(map.get(num) > map.get(heap.peek())){
                heap.poll();
                heap.offer(num);
            }
        }
       int[] result=new int[k];
       for(int i=0;i<k;i++){
          result[i]=heap.poll();
       }
       return result;
    
    }
}

3、数据流的中位数

中位数是有序整数列表中的中间值。如果列表的大小是偶数,则没有中间值,中位数是两个中间值的平均值。

  • 例如 arr = [2,3,4] 的中位数是 3
  • 例如 arr = [2,3] 的中位数是 (2 + 3) / 2 = 2.5

实现 MedianFinder 类:

  • MedianFinder() 初始化 MedianFinder 对象。

  • void addNum(int num) 将数据流中的整数 num 添加到数据结构中。

  • double findMedian() 返回到目前为止所有元素的中位数。与实际答案相差 10-5 以内的答案将被接受。

示例 1:

复制代码
输入
["MedianFinder", "addNum", "addNum", "findMedian", "addNum", "findMedian"]
[[], [1], [2], [], [3], []]
输出
[null, null, null, 1.5, null, 2.0]

解释
MedianFinder medianFinder = new MedianFinder();
medianFinder.addNum(1);    // arr = [1]
medianFinder.addNum(2);    // arr = [1, 2]
medianFinder.findMedian(); // 返回 1.5 ((1 + 2) / 2)
medianFinder.addNum(3);    // arr[1, 2, 3]
medianFinder.findMedian(); // return 2.0

思路:使用两个堆

  • 大顶堆 :存左半部分较小元素,堆顶是左半最大值;

  • 小顶堆 :存右半部分较大元素,堆顶是右半最小值;

  • 维持规则:

    1. 大顶堆元素数量 = 小顶堆数量 大顶堆多 1 个;
    2. 总数奇数 → 中位数 = 大顶堆堆顶;
    3. 总数偶数 → 中位数 = 两堆堆顶平均值。

    import java.util.PriorityQueue;

    class MedianFinder {
    // 大顶堆:左半区间
    private PriorityQueue left;
    // 小顶堆:右半区间
    private PriorityQueue right;

    复制代码
      public MedianFinder() {
          left = new PriorityQueue<>((a, b) -> b - a);
          right = new PriorityQueue<>();
      }
      
      public void addNum(int num) {
          // 优先加入左堆
          if (left.isEmpty() || num <= left.peek()) {
              left.offer(num);
          } else {
              right.offer(num);
          }
    
          // 平衡两个堆大小
          // 左堆元素过多
          if (left.size() > right.size() + 1) {
              right.offer(left.poll());
          }
          // 右堆元素过多
          else if (right.size() > left.size()) {
              left.offer(right.poll());
          }
      }
      
      public double findMedian() {
          if (left.size() > right.size()) {
              return left.peek();
          } else {
              return (left.peek() + right.peek()) / 2.0;
          }
      }

    }

十四、贪心算法

贪心算法核心思想:每一步都做局部最优选择,最终得到全局最优解,不回溯、不试探,只向前决策。

1、买卖股票的最佳时机

给定一个数组 prices ,它的第 i 个元素 prices[i] 表示一支给定股票第 i 天的价格。

你只能选择 某一天 买入这只股票,并选择在 未来的某一个不同的日子 卖出该股票。设计一个算法来计算你所能获取的最大利润。

返回你可以从这笔交易中获取的最大利润。如果你不能获取任何利润,返回 0

示例 1:

复制代码
输入:[7,1,5,3,6,4]
输出:5
解释:在第 2 天(股票价格 = 1)的时候买入,在第 5 天(股票价格 = 6)的时候卖出,最大利润 = 6-1 = 5 。
注意利润不能是 7-1 = 6, 因为卖出价格需要大于买入价格;同时,你不能在买入前卖出股票。

思路:遍历过程中记录历史最低价,实时计算当前收益并更新最大利润。

复制代码
class Solution {
    public int maxProfit(int[] prices) {
        int max=0;
        int len=prices.length;
        int minPrice=prices[0];
        for(int i=1;i<len;i++){
            if(prices[i]<minPrice){
                 minPrice=prices[i];
            }
            else if(prices[i]-minPrice>max)
                max=prices[i]-minPrice;
                     
        }
        return max;
    }
}

2、跳跃游戏

给你一个非负整数数组 nums ,你最初位于数组的 第一个下标 。数组中的每个元素代表你在该位置可以跳跃的最大长度。

判断你是否能够到达最后一个下标,如果可以,返回 true ;否则,返回 false

示例 1:

复制代码
输入:nums = [2,3,1,1,4]
输出:true
解释:可以先跳 1 步,从下标 0 到达下标 1, 然后再从下标 1 跳 3 步到达最后一个下标。

思路:遍历数组,实时更新可到达的最远位置

复制代码
class Solution {
    public boolean canJump(int[] nums) {
        int maxReach=0;
        int len=nums.length;
        for(int i=0;i<len;i++){
            if(i>maxReach)
                break;
            maxReach=Math.max(i+nums[i],maxReach);
        }
        return maxReach>=len-1?true:false;
    }
   
}

3、跳跃游戏II

给定一个长度为 n0 索引 整数数组 nums。初始位置在下标 0。

每个元素 nums[i] 表示从索引 i 向后跳转的最大长度。换句话说,如果你在索引 i 处,你可以跳转到任意 (i + j) 处:

  • 0 <= j <= nums[i]
  • i + j < n

返回到达 n - 1 的最小跳跃次数。测试用例保证可以到达 n - 1

示例 1:

复制代码
输入: nums = [2,3,1,1,4]
输出: 2

思路:维护三个变量:jumps跳跃次数、curEnd当前跳跃的边界、maxReach全局最远可达位置。

统计每一次跳跃能到达的区间,寻找该区间中能一步跳到的最远位置,循环。

复制代码
class Solution {
    public int jump(int[] nums) {
        int jumps = 0;
        int maxReach = 0;
        int curEnd = 0;
        
        // 注意:这里是 i < nums.length - 1
        // 我们不需要遍历最后一个元素,因为只要能到达/越过倒数第二个元素,
        // 我们的 curEnd 和 jumps 就会正确反映出到达终点所需的步数。
        for (int i = 0; i < nums.length - 1; i++) {
            maxReach = Math.max(maxReach, nums[i] + i);
            
            // 遇到了当前这一步能走的最远极限,必须再跳一次
            if (i == curEnd) {
                jumps++;
                curEnd = maxReach;
            }
        }
        
        return jumps;
    }
}

4、划分字母区间

给你一个字符串 s 。我们要把这个字符串划分为尽可能多的片段,同一字母最多出现在一个片段中。例如,字符串 "ababcc" 能够被分为 ["abab", "cc"],但类似 ["aba", "bcc"]["ab", "ab", "cc"] 的划分是非法的。

注意,划分结果需要满足:将所有划分结果按顺序连接,得到的字符串仍然是 s

返回一个表示每个字符串片段的长度的列表。

示例 1:

复制代码
输入:s = "ababcbacadefegdehijhklij"
输出:[9,7,8]
解释:
划分结果为 "ababcbaca"、"defegde"、"hijhklij" 。
每个字母最多出现在一个片段中。
像 "ababcbacadefegde", "hijhklij" 这样的划分是错误的,因为划分的片段数较少。 

思路

  1. 先遍历字符串,记录每个字符最后出现的位置

  2. 再次遍历,依据当前遍历的字符最后出现的位置不断更新当前片段的最远边界**end**。

  3. 当遍历下标等于end,说明当前片段划分完成,记录长度并开启新片段。

    class Solution {
    public List partitionLabels(String s) {
    // 用 26 长度的数组代替 HashMap
    int[] lastIndex = new int[26];
    int len = s.length();

    复制代码
         // 1. 记录每个字符最后一次出现的下标
         for (int i = 0; i < len; i++) {
             lastIndex[s.charAt(i) - 'a'] = i;
         }
         
         List<Integer> result = new ArrayList<>();
         int start = 0;
         int end = 0;
         
         // 2. 单次循环搞定区间划分
         for (int i = 0; i < len; i++) {
             // 不管三七二十一,每到一个新字符,就尝试扩大当前的边界边界
             end = Math.max(end, lastIndex[s.charAt(i) - 'a']);
             
             // 当当前的索引 i 已经追上了我们扩大的边界 end
             // 说明 [start, end] 区间内的所有字母,其最后一次出现的位置都不会超出 end 了
             if (i == end) {
                 result.add(end - start + 1);
                 start = end + 1; // 开启下一个区间的起点
             }
         }
         
         return result;
     }

    }

十五、动态规划

动态规划核心思想

一、核心定义与本质

动态规划(DP)的核心是:把大问题拆解成重复的小问题,通过存储子问题的解,避免重复计算,最终推导出全局最优解

它的三大关键特征:

  1. 最优子结构:大问题的最优解可以由子问题的最优解推导而来(比如爬楼梯,第 n 阶的方法数由 n-1 和 n-2 阶的解得到)
  2. 重复子问题:不同的决策路径会反复遇到相同的子问题(暴力递归会重复计算,DP 通过数组 / 哈希表存解来避免)
  3. 无后效性:一旦子问题的解确定,就不会被后续决策影响(比如打家劫舍,选了 i 位置的解,就不用再回头修改前面的选择)

二、通用解题四步模板

  1. 定义状态 dp[i] :明确 dp[i] 代表什么(比如 dp[i] 表示爬到第 i 阶的方法数、凑成金额 i 的最少硬币数)
  2. 找状态转移方程 :写出 dp[i] 如何由前面的状态推导而来(核心步骤)
  3. 初始化边界条件dp[0]dp[1] 等初始值(比如爬楼梯 dp[1]=1dp[2]=2
  4. 遍历计算并返回结果 :按顺序填充 dp 数组,最后返回目标值(比如 dp[n]

1、爬楼梯

假设你正在爬楼梯。需要 n 阶你才能到达楼顶。

每次你可以爬 12 个台阶。你有多少种不同的方法可以爬到楼顶呢?

示例 1:

复制代码
输入:n = 2
输出:2
解释:有两种方法可以爬到楼顶。
1. 1 阶 + 1 阶
2. 2 阶

思路:

a 代表 dpi-2:到达第 i-2 阶的方法数

b 代表 dpi-1:到达第 i-1 阶的方法数

c 代表 dpi:到达第 i 阶的方法数

复制代码
class Solution {
    public int climbStairs(int n) {
        int a=1;
        int b=1;
        int c=1;
        for(int i=2;i<=n;i++){
            c=a+b;
            a=b;
            b=c;
        }
        return c;
    }
}

2、杨辉三角

给定一个非负整数 numRows 生成「杨辉三角」的前 *numRows*行。

在**「杨辉三角」**中,每个数是它左上方和右上方的数的和。

示例 1:

复制代码
输入: numRows = 5
输出: [[1],[1,1],[1,2,1],[1,3,3,1],[1,4,6,4,1]]
复制代码
class Solution {
    public List<List<Integer>> generate(int numRows) {
        List<List<Integer>> result=new ArrayList<>();
        for(int i=0;i<numRows;i++){
            List<Integer> list=new ArrayList<>();
            list.add(1);
            if(i>0){
                List<Integer> lastList=result.get(i-1);
                for(int j=1;j<i;j++){
                    list.add(lastList.get(j-1)+lastList.get(j));
                }
                list.add(1);
            }
            result.add(list);
       
        }
        return result;
    }
}

3、打家劫舍

你是一个专业的小偷,计划偷窃沿街的房屋。每间房内都藏有一定的现金,影响你偷窃的唯一制约因素就是相邻的房屋装有相互连通的防盗系统,如果两间相邻的房屋在同一晚上被小偷闯入,系统会自动报警

给定一个代表每个房屋存放金额的非负整数数组,计算你不触动警报装置的情况下,一夜之内能够偷窃到的最高金额。

示例 1:

复制代码
输入:[1,2,3,1]
输出:4
解释:偷窃 1 号房屋 (金额 = 1) ,然后偷窃 3 号房屋 (金额 = 3)。
     偷窃到的最高金额 = 1 + 3 = 4 。

思路和爬楼梯差不多

复制代码
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]);
        int a=nums[0];
        int b=Math.max(a,nums[1]);
        int c=0;
        for(int i=2;i<len;i++){
            c=Math.max(a+nums[i],b);
            a=b;
            b=c;
        }
        return c;
    }
}

4、完全平方数

给你一个整数 n ,返回 和为 n 的完全平方数的最少数量

完全平方数 是一个整数,其值等于另一个整数的平方;换句话说,其值等于一个整数自乘的积。例如,14916 都是完全平方数,而 311 不是。

示例 1:

复制代码
输入:n =12
输出:3 

思路(动态规划,完全背包思想

这是求和为 n 的最少完全平方数个数的经典 DP 题,核心是「完全背包模型」。

  1. 定义状态dp[i] 表示凑成和为 i 所需的最少完全平方数个数。

  2. 初始化 :最坏情况用 i 个 1 相加,所以 dp[i] = i

  3. 状态转移 : 对每个数 i,枚举所有不超过它的完全平方数 dp[i] = min(dp[i], dp[i - j²] + 1) (用一个 ,再加上凑成 i-j² 的最少个数)

  4. 返回结果dp[n] 就是答案。

    class Solution {
    public int numSquares(int n) {
    int[] dp = new int[n + 1];
    // 初始化:最坏情况是用n个1相加
    for (int i = 0; i <= n; i++) {
    dp[i] = i;
    }

    复制代码
         // 状态转移
         for (int i = 1; i <= n; i++) {
             // 尝试所有小于等于i的完全平方数
             for (int j = 1; j * j <= i; j++) {
                 dp[i] = Math.min(dp[i], dp[i - j * j] + 1);
             }
         }
         
         return dp[n];
     }

    }

5、零钱兑换

给你一个整数数组 coins ,表示不同面额的硬币;以及一个整数 amount ,表示总金额。

计算并返回可以凑成总金额所需的 最少的硬币个数 。如果没有任何一种硬币组合能组成总金额,返回 -1 。

你可以认为每种硬币的数量是无限的。

示例 1:

输入:coins = 1, 2, 5, amount = 11

输出:3

解释:11 = 5 + 5 + 1

复制代码
class Solution {
    public int coinChange(int[] coins, int amount) {
        int[] dp=new int[amount+1];
        int n=coins.length;
        dp[0]=0;
        for(int i=1;i<=amount;i++){
            dp[i]=amount+1;
        }
        for(int i=1;i<=amount;i++){
            for(int coin:coins){
                if(i>=coin)
                   dp[i]=Math.min(dp[i],dp[i-coin]+1);
            }
        }
        return dp[amount]==amount+1?-1:dp[amount];
        
    }
}

6、单词拆分

给你一个字符串 s 和一个字符串列表 wordDict 作为字典。如果可以利用字典中出现的一个或多个单词拼接出 s 则返回 true

**注意:**不要求字典中出现的单词全部都使用,并且字典中的单词可以重复使用。

示例 1:

复制代码
输入: s = "leetcode", wordDict = ["leet", "code"]
输出: true
解释: 返回 true 因为 "leetcode" 可以由 "leet" 和 "code" 拼接成。

思路

  1. 定义 dp[i]:表示字符串 s 前 i 个字符能否被字典单词拆分。

  2. 初始状态:dp[0] = true,空串默认可拆分。

  3. 遍历每个长度 i(1~n),再枚举分割点 j

    • 若前 j 位可拆分(dp[j]=true),且 s[j,i) 子串在字典中,则 dp[i]=true
  4. 最终返回 dp[n](整个字符串是否可拆分)。

    class Solution {
    public boolean wordBreak(String s, List wordDict) {
    Set wordSet=new HashSet<>(wordDict);
    int n=s.length();
    boolean[] dp=new boolean[n+1];
    dp[0]=true;
    for(int i=1;i<=n;i++){
    for(int j=0;j<i;j++){
    if(dp[j]&&wordSet.contains(s.substring(j,i))){
    dp[i]=true;
    break;
    }
    }
    }
    return dp[n];
    }
    }

7、最长递增子序列

给你一个整数数组 nums ,找到其中最长严格递增子序列的长度。

子序列 是由数组派生而来的序列,删除(或不删除)数组中的元素而不改变其余元素的顺序。例如,[3,6,2,7] 是数组 [0,3,1,6,2,2,7] 的子序列。

示例 1:

复制代码
输入:nums = [10,9,2,5,3,7,101,18]
输出:4
解释:最长递增子序列是 [2,3,7,101],因此长度为 4 。

思路:定义**dpi**来表示以i结尾的递增子序列长度

复制代码
class Solution {
    public int lengthOfLIS(int[] nums) {
        int n=nums.length;
        int[] dp=new int[n];
        for(int i=0;i<n;i++)
            dp[i]=1;
        for(int i=1;i<n;i++){
            for(int j=0;j<i;j++){
                if(nums[i]>nums[j])
                    dp[i]=Math.max(dp[i],dp[j]+1);
            }
        }
        int max=dp[0];
        for(int i:dp){
            max=Math.max(max,i);
        }
        return max;
    }
}

8、乘积最大子数组

给你一个整数数组 nums ,请你找出数组中乘积最大的非空连续 子数组(该子数组中至少包含一个数字),并返回该子数组所对应的乘积。

测试用例的答案是一个 32-位 整数。

请注意,一个只包含一个元素的数组的乘积是这个元素的值。

示例 1:

复制代码
输入: nums = [2,3,-2,4]
输出:6
解释: 子数组 [2,3] 有最大乘积 6。

思路

  1. 核心难点 :负数相乘会反转大小,所以同时维护当前最大值 maxDp当前最小值 minDp

  2. 初始maxDpminDp、结果result都等于首个元素。

  3. 遍历数组

    • 当前数非负:最大值继续乘大数,最小值继续乘小数。
    • 当前数负数:交换前后最值,用旧最小值乘当前数得到新最大值。
  4. 每轮更新全局最大结果,最终返回。

    class Solution {
    public int maxProduct(int[] nums) {
    int n=nums.length;
    int maxDp=nums[0];
    int minDp=nums[0];
    int result=maxDp;
    for(int i=1;i<n;i++){
    if(nums[i]>=0){
    maxDp=Math.max(maxDpnums[i],nums[i]);
    minDp=Math.min(minDp
    nums[i],nums[i]);
    }
    else{
    int temp=maxDp;
    maxDp = Math.max(minDp * nums[i], nums[i]);
    minDp = Math.min(temp * nums[i], nums[i]);
    }
    result=Math.max(result,maxDp);
    }
    return result;
    }
    }

十六、多维动态规划

1、不同路径

一个机器人位于一个 m x n网格的左上角 (起始点在下图中标记为 "Start" )。

机器人每次只能向下或者向右移动一步。机器人试图达到网格的右下角(在下图中标记为 "Finish" )。

问总共有多少条不同的路径?

示例 1:

复制代码
输入:m = 3, n = 7
输出:28

2、最小路径和

给定一个包含非负整数的 m xn 网格 grid ,请找出一条从左上角到右下角的路径,使得路径上的数字总和为最小。

**说明:**每次只能向下或者向右移动一步。

示例 1:

复制代码
输入:grid = [[1,3,1],[1,5,1],[4,2,1]]
输出:7
解释:因为路径 1→3→1→1→1 的总和最小。

3、最长回文子串

给你一个字符串 s,找到 s 中最长的 回文 子串。

示例 1:

复制代码
输入:s = "babad"
输出:"bab"
解释:"aba" 同样是符合题意的答案。

示例 2:

复制代码
输入:s = "cbbd"
输出:"bb"

思路:这里我使用的是中心扩展法

  • 思路:回文串中心对称,逐个以每个字符、每两个相邻字符为中心,向左右扩散判断回文。

  • 遍历每个中心点 → 左右双指针向外延伸,是暴力优化版的双指针思想

    class Solution {
    public int start=0;
    public int max=0;
    public String longestPalindrome(String s) {
    for(int i=0;i<s.length();i++){
    expand(s,i,i);
    expand(s,i,i+1);
    }
    return s.substring(start,start+max);

    复制代码
      }
      public void expand(String s,int l,int r){
          while(l>=0&&r<s.length()&&s.charAt(l)==s.charAt(r)){
              l--;
              r++;
          }
          int len=r-l-1;
          if(len>max){
              start=l+1;
              max=len;
          }
      }

    }

4、最长公共子序列

给定两个字符串 text1text2,返回这两个字符串的最长 公共子序列 的长度。如果不存在 公共子序列 ,返回 0

一个字符串的 子序列 是指这样一个新的字符串:它是由原字符串在不改变字符的相对顺序的情况下删除某些字符(也可以不删除任何字符)后组成的新字符串。

  • 例如,"ace""abcde" 的子序列,但 "aec" 不是 "abcde" 的子序列。

两个字符串的 公共子序列 是这两个字符串所共同拥有的子序列。

示例 1:

复制代码
输入:text1 = "abcde", text2 = "ace" 
输出:3  
解释:最长公共子序列是 "ace" ,它的长度为 3 。
复制代码
class Solution {
    public int longestCommonSubsequence(String text1, String text2) {
        int m=text1.length();
        int n=text2.length();
        int[][] dp=new int[m+1][n+1];
        dp[0][0]=0;
        for(int i=1;i<=m;i++){
            for(int j=1;j<=n;j++){
                if(text1.charAt(i-1)==text2.charAt(j-1)){
                    dp[i][j]=dp[i-1][j-1]+1;
                }
                else{
                    dp[i][j]=Math.max(dp[i-1][j],dp[i][j-1]);
                }
            }
        }
        return dp[m][n];
    }
}

5、编辑距离

给你两个单词 word1word2请返回将 word1 转换成 word2 所使用的最少操作数

你可以对一个单词进行如下三种操作:

  • 插入一个字符
  • 删除一个字符
  • 替换一个字符

示例 1:

复制代码
输入:word1 = "horse", word2 = "ros"
输出:3
解释:
horse -> rorse (将 'h' 替换为 'r')
rorse -> rose (删除 'r')
rose -> ros (删除 'e')

思路

一、状态定义

dp[i][j]word1i 个字符 转为 word2j 个字符的最少操作次数

二、状态转移

  1. 字符相等word1[i-1] == word2[j-1] 当前字符无需操作: \(dpij = dpi-1j-1\)
  2. 字符不等 ,取三种操作最小值:
    • 替换:dp[i-1][j-1] + 1
    • 删除 word1 字符:dp[i-1][j] + 1
    • 插入字符(等价删除 word2):dp[i][j-1] + 1

三、初始边界

  • dp[i][0] = iword1i 个字符转为空串,需要删除 i 次

  • dp[0][j] = j:空串转为 word2j 个字符,需要插入 j 次

    class Solution {
    public int minDistance(String word1, String word2) {
    int m = word1.length();
    int n = word2.length();
    int[][] dp = new int[m + 1][n + 1];

    复制代码
          // 初始化边界
          for (int i = 0; i <= m; i++) dp[i][0] = i;
          for (int j = 0; j <= n; j++) dp[0][j] = j;
    
          for (int i = 1; i <= m; i++) {
              for (int j = 1; j <= n; j++) {
                  if (word1.charAt(i - 1) == word2.charAt(j - 1)) {
                      dp[i][j] = dp[i - 1][j - 1];
                  } else {
                      dp[i][j] = Math.min(
                          Math.min(dp[i - 1][j], dp[i][j - 1]),
                          dp[i - 1][j - 1]
                      ) + 1;
                  }
              }
          }
          return dp[m][n];
      }

    }

十七、技巧

1、只出现一次的数字

给你一个 非空 整数数组 nums ,除了某个元素只出现一次以外,其余每个元素均出现两次。找出那个只出现了一次的元素。

你必须设计并实现线性时间复杂度的算法来解决此问题,且该算法只使用常量额外空间。

示例 1 :

**输入:**nums = 2,2,1

**输出:**1

思路:异或

复制代码
class Solution {
    public int singleNumber(int[] nums) {
        int res=0;
        for(int num:nums){
            res^=num;
        }
        return res;  
    }
}

2、多数元素

给定一个大小为 n的数组 nums ,返回其中的多数元素。多数元素是指在数组中出现次数 大于 ⌊ n/2 ⌋ 的元素。

你可以假设数组是非空的,并且给定的数组总是存在多数元素。

示例 1:

复制代码
输入:nums = [3,2,3]
输出:3

示例 2:

复制代码
输入:nums = [2,2,1,1,1,2,2]
输出:2

思路:摩尔投票法(Boyer-Moore 多数投票算法)

核心原理

  1. 抵消规则 遍历数组,维护一个候选数计数

    • 计数 count = 0:更换当前候选数为遍历到的数字
    • 当前数 == 候选数:计数 +1
    • 当前数!= 候选数:计数 -1(两两抵消
  2. 为什么能找到多数元素? 因为多数元素出现次数严格大于数组一半,哪怕不断被其他元素抵消,最后剩下的候选数一定就是它。

    class Solution {
    public int majorityElement(int[] nums) {
    int candidate=0;
    int count=0;
    for(int num:nums){
    if(count==0){
    candidate=num;
    }
    count+=(candidate==num)?1:-1;
    }
    return candidate;
    }
    }

3、颜色分类

给定一个包含红色、白色和蓝色、共 n个元素的数组 nums ,**原地**对它们进行排序,使得相同颜色的元素相邻,并按照红色、白色、蓝色顺序排列。

我们使用整数 012 分别表示红色、白色和蓝色。

必须在不使用库内置的 sort 函数的情况下解决这个问题。

示例 1:

复制代码
输入:nums = [2,0,2,1,1,0]
输出:[0,0,1,1,2,2]

示例 2:

复制代码
输入:nums = [2,0,1]
输出:[0,1,2]

思路:维护r和l 来表示0,1,2之间的分界

复制代码
class Solution {
    public void sortColors(int[] nums) {
        int n=nums.length;
        int l=0;
        int r=n-1;
        
        for(int num:nums){
            if(num==0){
                l++;
            }
            else if(num==2){
                r--;
            }
        }
        for(int i=0;i<l;i++){
            nums[i]=0;
        }
        for(int i=l;i<=r;i++){
            nums[i]=1;
        }
        for(int i=r+1;i<n;i++){
            nums[i]=2;
        }
       
    }
}

4、下一个排列

整数数组的一个 排列 就是将其所有成员以序列或线性顺序排列。

  • 例如,arr = [1,2,3] ,以下这些都可以视作 arr 的排列:[1,2,3][1,3,2][3,1,2][2,3,1]

整数数组的 下一个排列 是指其整数的下一个字典序更大的排列。更正式地,如果数组的所有排列根据其字典顺序从小到大排列在一个容器中,那么数组的 下一个排列 就是在这个有序容器中排在它后面的那个排列。如果不存在下一个更大的排列,那么这个数组必须重排为字典序最小的排列(即,其元素按升序排列)。

  • 例如,arr = [1,2,3] 的下一个排列是 [1,3,2]
  • 类似地,arr = [2,3,1] 的下一个排列是 [3,1,2]
  • arr = [3,2,1] 的下一个排列是 [1,2,3] ,因为 [3,2,1] 不存在一个字典序更大的排列。

给你一个整数数组 nums ,找出 nums 的下一个排列。

必须**原地**修改,只允许使用额外常数空间。

示例 1:

复制代码
输入:nums = [1,2,3]
输出:[1,3,2]

示例 2:

复制代码
输入:nums = [3,2,1]
输出:[1,2,3]

示例 3:

复制代码
输入:nums = [1,1,5]
输出:[1,5,1]

思路: 从后往前找降序断点 → 找右侧比它大的最小数 → 交换 → 反转后缀

eg:

1. 找 "下降点" i(从后往前)

  • 图里的蓝色曲线,从右往左看:
    • 后面三个点是持续下降的(↓↓↓),直到某一点开始往上拐
    • 这个 "拐的地方" 就是 i,也就是第一个满足 nums[i] < nums[i+1] 的位置。
  • 图里最左边的红色叉号,就是这个 nums[i],它是整个数组中最后一个 "可以变大" 的位置

2. 找右侧比 nums[i] 大的最小数 j

  • 从数组末尾往前找,第一个比 nums[i] 大的元素(图里右边的红色叉号)。
  • 这个元素是 nums[i] 右侧所有比它大的数里,最小的那个,交换它俩能保证 "字典序刚好变大一点点"。

3. 交换 nums[i]nums[j]

  • 图里的两个红色叉号交换了位置,现在 nums[i] 变成了右边的红色圆圈,整体字典序已经比原来大了。
  • 但此时 i 右侧的部分,仍然是降序排列(曲线是往下走的),也就是 "字典序最大的后缀"。

4. 反转 i 右侧的所有元素

  • i 后面的部分从降序反转成升序,让它变成字典序最小的后缀

  • 这样就得到了 "比原排列大、且是所有更大排列中最小的那一个"------ 也就是题目要求的「下一个排列」

    class Solution {
    public void nextPermutation(int[] nums) {
    int n=nums.length;
    int i=n-2;
    while(i>=0&&nums[i]>=nums[i+1]){
    i--;
    }
    if(i>=0){
    int j=n-1;
    while(j>=0&&nums[j]<=nums[i]){
    j--;
    }
    swap(nums,i,j);
    }
    reverse(nums,i+1,n-1);

    复制代码
      }
      public void swap(int[] nums,int i,int j){
          int temp=nums[i];
          nums[i]=nums[j];
          nums[j]=temp;
      }
      public void reverse(int[] nums,int l,int r){
          while(l<=r){
              int temp=nums[l];
              nums[l]=nums[r];
              nums[r]=temp;
              l++;
              r--;
          }
      }

    }

5、寻找重复数

给定一个包含 n + 1 个整数的数组 nums ,其数字都在 [1, n] 范围内(包括 1n),可知至少存在一个重复的整数。

假设 nums 只有 一个重复的整数 ,返回 这个重复的数

你设计的解决方案必须 不修改 数组 nums 且只用常量级 O(1) 的额外空间。

示例 1:

复制代码
输入:nums = [1,3,4,2,2]
输出:2

思路:快慢指针 (弗洛伊德判圈算法)

把数组看成有环链表

下标为节点,nums下标 指向 next 节点

因为存在重复数,链表必然有 ,环的入口就是重复数字

步骤:

1、快慢指针:慢指针走 1 步,快指针走 2 步,直到相遇

2、快指针回到起点,两指针同速前进,再次相遇即为环入口(重复数)

复制代码
class Solution {
    public int findDuplicate(int[] nums) {
        int slow=0,fast=0;
        do{
            slow=nums[slow];
            fast=nums[nums[fast]];
        }while(slow!=fast);
        fast=0;
        while(slow!=fast){
            slow=nums[slow];
            fast=nums[fast];
        }
        return slow;
    }
}
相关推荐
暖阳华笺2 小时前
【数据结构与算法】哈希专题
数据结构·c++·算法·leetcode·哈希算法
LuminousCPP2 小时前
数据结构 - 单链表第二篇:单链表进阶操作
c语言·数据结构·笔记·链表
玖玥拾2 小时前
C/C++ 数据结构(三)链表核心算法
c语言·数据结构·c++·链表
变量未定义~2 小时前
摆放小球 、dp求解组合数、求解组合数2
数据结构·算法
一头老黄牛@2 小时前
飞书 × OpenClaw 接入指南:不用服务器,用长连接把机器人跑起来
数据结构·人工智能·程序人生·算法·决策树·自动化·推荐算法
Zhan8611244 小时前
数据接口的序列号机制与丢包检测:西班牙行情数据IBEX指数实时行情接入笔记
大数据·数据结构·笔记·区块链
退休倒计时13 小时前
【每日一题】LeetCode 53. 最大子数组和 TypeScript
数据结构·算法·leetcode·typescript
2601_9618752414 小时前
法考资料2026|全套|资料已整理
数据结构·算法·链表·贪心算法·eclipse·线性回归·动态规划
dtq042417 小时前
C语言刷题数组5,6(求平均值,求最大值)
c语言·数据结构·算法