剑指OfferⅡ题集-专项突击版(力扣)

笔试就是 数组 + 字符串模拟 + 数学问题 + 图论 + 数论

面试就是 剑指Offer + 链表 + 基本排序算法
题单没了

文章目录

剑指Offer-专项突击版(力扣题单

1.求和(二进制加法、二数之和、三数之和)

https://leetcode.cn/problems/add-two-numbers-ii/solution/fu-xue-ming-zhu-xiang-jie-qiu-jia-fa-xue-ofb5/

加法是我们上小学的时候开始学习的第一种数学运算。

在算法题中,「求加法」问题大多考察「列竖式」求和。

题目中,「两数之和」通常与其他形式表示的数字结合起来:

  • 两个字符串形式的数字相加(第 415 题)

  • 两个链表形式的数字相加(第 2 、445、369 题)

  • 数组形式的数字相加(第 66 、989题)

  • 两个二进制形式的数字相加(第 67 题)

做法都是非常类似的,本质是在考察各种数据表示形式:字符串,链表,数组,二进制。

剑指 Offer II 001. 整数除法

难度简单251

给定两个整数 ab ,求它们的除法的商 a/b ,要求不得使用乘号 '*'、除号 '/' 以及求余符号 '%'

注意:

  • 整数除法的结果应当截去(truncate)其小数部分,例如:truncate(8.345) = 8 以及 truncate(-2.7335) = -2
  • 假设我们的环境只能存储 32 位有符号整数,其数值范围是 [−231, 231−1]。本题中,如果除法结果溢出,则返回 231 − 1

示例 1:

输入:a = 15, b = 2
输出:7
解释:15/2 = truncate(7.5) = 7

示例 2:

输入:a = 7, b = -3
输出:-2
解释:7/-3 = truncate(-2.33333..) = -2

示例 3:

输入:a = 0, b = 1
输出:0

示例 4:

输入:a = 1, b = 1
输出:1

提示:

  • -231 <= a, b <= 231 - 1
  • b != 0

题解:yukiyama【倍增思想】

**由于题目要求不能使用乘法、除法以及求余,因此考虑用加法代替乘法。**对于 a / b, a、b都是整数,为了缩小讨论范围,假设a、b都是正数,那么商的范围为[0, a],当a < b或b = 0(无意义)时为0。可以通过不断倍增b并将倍增结果与a比较来找到商,这实际上是一个二分搜索的过程。关键代码如下(a / b = c)。应当注意,b在倍增过程中,若超过最大值的一半,那么b + b会因为溢出得到负数,此时 <= a的判断将导致错误的结果,因此在while条件中要使得b <= Integer.MAX_VALUE / 2。例如a = Integer.MAX_VALUE, b = 1时,b会倍增到1073741824 > Integer.MAX_VALUE / 2 = 1073741823,不满足上述条件跳出循环(短路判断)。

int c = 1; // 商
while(b <= Integer.MAX_VALUE >> 1 && b + b <= a){
  b += b; // 除数倍增
  c += c; // 商相应倍增
}

现在我们从一个例子出发,逐步完善求解过程。例如求解a = 100, b = 7。按照前述while,得到b, c的变化(b = 14, c = 2), (b = 28, c = 4), (b = 56, c = 8),之后由于b + b = 112 > a,跳出循环。至此得到了a中56的部分被7除的结果,**a剩余的部分100 - 56 = 44 > 7,因此仍然可继续被7除。**由于在有剩余的情况下余下部分的大小需要与b进行比较,因此用d = b来表示除数的倍增变化,用ans来累计商。

public int divide(int a, int b) {
    int ans = 0; // 最终的商
    while(a >= b) {
        int d = b, c = 1; // 当前倍增的部分商
        while(d <= Integer.MAX_VALUE >> 1 && d + d <= a) {
            d += d; // 除数倍增
            c += c; // 当前商倍增
        } 
        a -= d; // a剩余部分
        ans += c; // 累计商
    }
    return ans;
}

本题的求解框架如上,但这是基于a、b均为正整数的情况,**当a、b不同号时,一个自然的想法是按正整数求解,返回结果时再取反即可。**按照这个想法,现在来考虑a、b的符号以及edge cases。a、b的范围均为[-2^31, 2^31 - 1],为方便,定义MIN = -2^31, MAX = 2^31 - 1。可以看到MIN的绝对值比MAX更大,当a = MIN时,对a取反会导致溢出。**因此我们反其道而行之,按a、b都为负数处理,这样就可以覆盖所有a、b取值的情形。**将前述假设a、b均为正数的代码修正为假设a、b均为负数的版本。另外对于第二个while中的(d + d <= a),有经验的话不难察觉到d + d的写法可能导致加法溢出,因此改写为d <= a - d。但由于第一个条件已经避免了d + d溢出的情形,因此无需改写。

题目已经声明b != 0,因此无需考虑这个edge case。唯一需要处理的edge case是a = -2^31, b = -1,此时会溢出,按题目要求应该返回MAX。至此我们可以写出完整代码,见「题解代码」。

【时间复杂度】

时间复杂度:O((logc)^2)。从1开始倍增直到c的过程中,内层while内的语句执行次数由此式得到:log(c - x) + log(x - y) + log(y - z)…。x, y, z表示每次剩余的部分,且每次剩余的部分均小于当前部分的一半,最多共有logc项。可以看到虽然为对数平方阶,但随着项数增加,log中的真数快速下降,因此实际效率要好得多。也可以粗略地通过下式观察。

log(c - x) + log(x - y) + log(y - z)… < logc + log(c/2)+ log(c/4)… = logc * logc - log2 - log4…

class Solution {
    public int divide(int a, int b) {
        int MIN = Integer.MIN_VALUE, MAX = Integer.MAX_VALUE, MIN_LIMIT = MIN >> 1; // -1073741824
        if(a == MIN && b == -1) return MAX; // 特判
        boolean isPos = (a < 0 && b > 0) || (a > 0 && b < 0) ? false : true;
        if(a > 0) a = -a;
        if(b > 0) b = -b;
        int ans = 0; // 最终的商
        while(a <= b) {
            int d = b, c = 1; // d为当前除数,c为当前商
            while(d >= MIN_LIMIT && d + d >= a) { // 通过第一个条件防止d + d溢出
                d += d; // 当前除数倍增,也可以用 d <<= 1;
                c += c; // 当前商倍增,也可以用 c <<= 1;
            } 
            a -= d; // a剩余部分
            ans += c; // 累计当前商
        }
        return isPos ? ans : -ans;
	}
}

剑指 Offer II 002. 二进制加法

难度简单67

给定两个 01 字符串 ab ,请计算它们的和,并以二进制字符串的形式输出。

输入为 非空 字符串且只包含数字 10

示例 1:

输入: a = "11", b = "10"
输出: "101"

示例 2:

输入: a = "1010", b = "1011"
输出: "10101"

提示:

  • 每个字符串仅由字符 '0''1' 组成。
  • 1 <= a.length, b.length <= 10^4
  • 字符串如果不是 "0" ,就都不含前导零。

模拟:

class Solution {
    public String addBinary(String a, String b) {
        char[] ca = a.toCharArray();
        char[] cb = b.toCharArray();
        StringBuilder sb = new StringBuilder();
        int d = 0; // 进位符
        int i = ca.length-1, j = cb.length-1;
        for(; i >= 0 && j >= 0; i -= 1, j -= 1){
            int s = (ca[i] - '0') + (cb[j] - '0') + d;
            if(s == 3){
                sb.append('1');
                d = 1;
            }else if(s == 2){
                sb.append('0');
                d = 1;
            }else if(s == 1){
                sb.append('1');
                d = 0;
            }else{
                sb.append('0');
                d = 0;
            }
        }
        while(i >= 0){
            int s = (ca[i] - '0') + d;
            if(s == 3){
                sb.append('1');
                d = 1;
            }else if(s == 2){
                sb.append('0');
                d = 1;
            }else if(s == 1){
                sb.append('1');
                d = 0;
            }else{
                sb.append('0');
                d = 0;
            }
            i -= 1;
        }
        while(j >= 0){
            int s = (cb[j] - '0') + d;
            if(s == 3){
                sb.append('1');
                d = 1;
            }else if(s == 2){
                sb.append('0');
                d = 1;
            }else if(s == 1){
                sb.append('1');
                d = 0;
            }else{
                sb.append('0');
                d = 0;
            }
            j -= 1;
        }
        if(d == 1) sb.append('1');
        return sb.reverse().toString();
    }

}

优雅一点的写法:

https://leetcode.cn/problems/JFETK5/solution/fu-xue-ming-zhu-er-jin-zhi-jia-fa-xiang-bu5dt/

class Solution {
    public String addBinary(String a, String b) {
        StringBuilder res = new StringBuilder(); // 返回结果
        int i = a.length() - 1; // 标记遍历到 a 的位置
        int j = b.length() - 1; // 标记遍历到 b 的位置
        int carry = 0; // 进位
        // //按照2进制相加的思路,每一位有0,1,2三种情况
        while (i >= 0 || j >= 0 || carry != 0) { // a 没遍历完,或 b 没遍历完,或进位不为 0
            int digitA = i >= 0 ? a.charAt(i) - '0' : 0; // 当前 a 的取值
            int digitB = j >= 0 ? b.charAt(j) - '0' : 0; // 当前 b 的取值
            int sum = digitA + digitB + carry; // 当前位置相加的结果
            carry = sum >= 2 ? 1 : 0; // 是否有进位
            sum = sum >= 2 ? sum - 2 : sum; // 去除进位后留下的数字
            res.append(sum); // 把去除进位后留下的数字拼接到结果中
            i --;  // 遍历到 a 的位置向左移动
            j --;  // 遍历到 b 的位置向左移动
        }
        return res.reverse().toString(); // 把结果反转并返回
    }
}

python

class Solution:
    def addBinary(self, a: str, b: str) -> str:
        res = ""
        carry = 0
        i, j = len(a)-1, len(b)-1
        while i >= 0 or j >= 0 or carry != 0:
            da = ord(a[i]) - ord('0') if i >= 0 else 0
            db = ord(b[j]) - ord('0') if j >= 0 else 0
            s = da + db + carry
            carry = 1 if s >= 2 else 0
            s = s - 2 if carry == 1 else s
            res += str(s)
            i, j = i-1, j-1
        return res[::-1]

1073. 负二进制数加法

难度中等56

给出基数为 -2 的两个数 arr1arr2,返回两数相加的结果。

数字以 数组形式 给出:数组由若干 0 和 1 组成,按最高有效位到最低有效位的顺序排列。例如,arr = [1,1,0,1] 表示数字 (-2)^3 + (-2)^2 + (-2)^0 = -3数组形式 中的数字 arr 也同样不含前导零:即 arr == [0]arr[0] == 1

返回相同表示形式的 arr1arr2 相加的结果。两数的表示形式为:不含前导零、由若干 0 和 1 组成的数组。

示例 1:

输入:arr1 = [1,1,1,1,1], arr2 = [1,0,1]
输出:[1,0,0,0,0]
解释:arr1 表示 11,arr2 表示 5,输出表示 16 。

示例 2:

输入:arr1 = [0], arr2 = [0]
输出:[0]

示例 3:

输入:arr1 = [0], arr2 = [1]
输出:[1]

提示:

  • 1 <= arr1.length, arr2.length <= 1000
  • arr1[i]arr2[i] 都是 01
  • arr1arr2 都没有前导0

模拟

( − 2 ) n + ( − 2 ) n = − ( − 2 ) n + 1 (−2)^n+(−2)^n=−(−2)^{n+1} (2)n+(2)n=(2)n+1 ,因此满足逢2进位−1。

考虑特殊情况:如果当前数位的相加结果cur为−1,由于

− ( − 2 ) n = ( − 2 ) n + ( − 2 ) n + 1 −(−2)^n=(−2)^n+(−2)^{n+1} (2)n=(2)n+(2)n+1,因此可将相加结果cur写为1,同时进位1。

class Solution {
    public int[] addNegabinary(int[] arr1, int[] arr2) {
        int n1 = arr1.length, n2 = arr2.length;
        int carry = 0;
        int i = n1-1, j = n2-1;
        List<Integer> list = new ArrayList<>();
        //按照-2进制相加的思路,每一位有-1,0,1,2,3五种情况
        while(i >= 0 || j >= 0 || carry != 0){
            int digitA = i >= 0 ? arr1[i--] : 0;
            int digitB = j >= 0 ? arr2[j--] : 0;
            int sum = digitA + digitB + carry;
            if(sum == -1){ // sum == -1 当前位无法表示 在基数为-2的进制下要表示为11 ((-2)^1 + (-2)^0 = -1)
                carry = 1;
                list.add(1);
            }else{ // 否则就是常规的-2进制计算, 类似于2进制加法一样
                carry = -(sum / 2);
                list.add(sum % 2);
            }
        }
        // 得到的结果是逆序的, 结果去除前导0
        while(list.size() > 1 && list.get(list.size() - 1) == 0) list.remove(list.size() - 1);
        int[] ans = new int[list.size()];
        for(int k = 0; k < list.size(); k++){
            ans[ans.length - 1 - k] = list.get(k);
        }
        return ans;
    }
}

剑指 Offer II 003. 前 n 个数字二进制中 1 的个数

难度简单130

给定一个非负整数 n ,请计算 0n 之间的每个数字的二进制表示中 1 的个数,并输出一个数组。

示例 1:

输入: n = 2
输出: [0,1,1]
解释: 
0 --> 0
1 --> 1
2 --> 10

示例 2:

输入: n = 5
输出: [0,1,1,2,1,2]
解释:
0 --> 0
1 --> 1
2 --> 10
3 --> 11
4 --> 100
5 --> 101

说明 :

  • 0 <= n <= 105

进阶:

  • 给出时间复杂度为 O(n*sizeof(integer)) 的解答非常容易。但你可以在线性时间 O(n) 内用一趟扫描做到吗?
  • 要求算法的空间复杂度为 O(n)
  • 你能进一步完善解法吗?要求在C++或任何其他语言中不使用任何内置函数(如 C++ 中的 __builtin_popcount )来执行此操作。

方法一:使用API写法

class Solution {
    public int[] countBits(int n) {
        int[] res = new int[n+1];
        for(int i = 0; i <= n; i++){
            res[i] = Integer.bitCount(i);
        }
        return res;
    }
}

方法二:动态规划 +位运算

https://leetcode.cn/problems/w3tCBm/solution/rang-ni-miao-dong-de-shuang-bai-ti-jie-b-84hh/

对于所有的数字,只有奇数和偶数两种:

  • 奇数:二进制表示中,奇数一定比前面那个偶数多一个 1,因为多的就是最低位的 1。

  • 偶数:二进制表示中,偶数中 1 的个数一定和除以 2 之后的那个数一样多。因为最低位是 0,除以 2 就是右移一位,也就是把那个 0 抹掉而已,所以 1 的个数是不变的。

所以我们可以得到如下的状态转移方程:

  • dp[i] = dp[i-1],当i为奇数

  • dp[i] = dp[i/2],当i为偶数

上面的方程还可进一步合并为:

  • dp[i] = dp[i/2] + i % 2

通过位运算进一步优化:

  • i / 2 可以通过 i >> 1 得到;

  • i % 2 可以通过 i & 1 得到;

class Solution:
    def countBits(self, n: int) -> List[int]:
        ans = [0] * (n+1)
        for i in range(n+1):
            ans[i] = ans[i >> 1] + (i & 1) # 对于奇数而言,(i-1)/2等价于i/2
        return ans

剑指 Offer II 004. 只出现一次的数字

难度中等133

给你一个整数数组 nums ,除某个元素仅出现 一次 外,其余每个元素都恰出现 **三次 。**请你找出并返回那个只出现了一次的元素。

示例 1:

输入:nums = [2,2,3,2]
输出:3

示例 2:

输入:nums = [0,1,0,1,0,1,100]
输出:100

提示:

  • 1 <= nums.length <= 3 * 104
  • -231 <= nums[i] <= 231 - 1
  • nums 中,除某个元素仅出现 一次 外,其余每个元素都恰出现 三次

**进阶:**你的算法应该具有线性时间复杂度。 你可以不使用额外空间来实现吗?

class Solution {
    // 如果一个数字出现3次,它的二进制每一位也出现的3次。
    // 如果把所有的出现三次的数字的二进制表示的每一位都分别加起来,那么每一位都能被3整除。 
    // 我们把数组中所有的数字的二进制表示的每一位都加起来。如果某一位能被3整除,那么这一位对只出现一次的那个数的这一肯定为0。
    // 如果某一位不能被3整除,那么只出现一次的那个数字的该位置一定为1.
    public int singleNumber(int[] nums) {
        int[] cnt = new int[32];
        for(int num : nums){
            for(int j = 0; j < 32; j++){
                if((num >> j & 1) == 1){
                    cnt[j] += 1;
                }
            }
        }
        int res = 0;
        for(int i = 31; i >= 0; i--){
            res = res << 1; // 左移一位
            if(cnt[i] % 3 == 1){
                res = res | 1;
            }
        }
        return res;
    }
}

剑指 Offer II 005. 单词长度的最大乘积

难度中等144

给定一个字符串数组 words,请计算当两个字符串 words[i]words[j] 不包含相同字符时,它们长度的乘积的最大值。假设字符串中只包含英语的小写字母。如果没有不包含相同字符的一对字符串,返回 0。

示例 1:

输入: words = ["abcw","baz","foo","bar","fxyz","abcdef"]
输出: 16 
解释: 这两个单词为 "abcw", "fxyz"。它们不包含相同字符,且长度的乘积最大。

示例 2:

输入: words = ["a","ab","abc","d","cd","bcd","abcd"]
输出: 4 
解释: 这两个单词为 "ab", "cd"。

示例 3:

输入: words = ["a","aa","aaa","aaaa"]
输出: 0 
解释: 不存在这样的两个单词。

提示:

  • 2 <= words.length <= 1000
  • 1 <= words[i].length <= 1000
  • words[i] 仅包含小写字母

方法一:二进制压缩 + 枚举

class Solution:
    def maxProduct(self, words: List[str]) -> int:
        n = len(words)
        cnt = [0] * n # cnt[i] 把单词转换成二进制属性数字
        for i, word in enumerate(words):
            cur = 0
            for v in word:
                cur |= 1 << (ord(v) - ord('a'))
            cnt[i] = cur
        ans = 0
        for i in range(n):
            for j in range(i+1, n):
                if cnt[i] & cnt[j] == 0:
                    ans = max(ans, len(words[i]) * len(words[j]))
        return ans

时间复杂度:O((n + m)* n)n是words的长度,处理每个words需要m时间

剑指 Offer II 006. 排序数组中两个数字之和

难度简单66

给定一个已按照 升序排列 的整数数组 numbers ,请你从数组中找出两个数满足相加之和等于目标数 target

函数应该以长度为 2 的整数数组的形式返回这两个数的下标值*。*numbers 的下标 从 0 开始计数 ,所以答案数组应当满足 0 <= answer[0] < answer[1] < numbers.length

假设数组中存在且只存在一对符合条件的数字,同时一个数字不能使用两次。

示例 1:

输入:numbers = [1,2,4,6,10], target = 8
输出:[1,3]
解释:2 与 6 之和等于目标数 8 。因此 index1 = 1, index2 = 3 。

示例 2:

输入:numbers = [2,3,4], target = 6
输出:[0,2]

示例 3:

输入:numbers = [-1,0], target = -1
输出:[0,1]

提示:

  • 2 <= numbers.length <= 3 * 104
  • -1000 <= numbers[i] <= 1000
  • numbers非递减顺序 排列
  • -1000 <= target <= 1000
  • 仅存在一个有效答案

方法一:相向双指针

func twoSum(numbers []int, target int) []int {
    left, right := 0, len(numbers)-1
    for left < right {
        if (numbers[left] + numbers[right]) > target {
            right -= 1
        }else if (numbers[left] + numbers[right]) < target {
            left += 1
        }else{
            return []int{left, right}
        }
    }
    return []int{-1, -1}
}

时间复杂度O(n)

方法二:固定一个边界,另一个边界二分查找

func twoSum(numbers []int, target int) []int {
    n := len(numbers)
    for i := 0; i < n; i++ {
        // 二分找到target-numbers[i]的数
        left, right := i, len(numbers)
        for left < right {
            mid := (left + right) >> 1
            if numbers[mid] == target - numbers[i] {
                return []int{i, mid}
            } else if numbers[mid] < target - numbers[i] {
                left = mid + 1
            } else {
                right = mid
            }
        }
    }
    return []int{-1, -1}
}

时间复杂度:O(nlogn)

🎉剑指 Offer II 007. 数组中和为 0 的三个数【15.三数之和】

难度中等113

给你一个整数数组 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] 。
注意,输出的顺序和三元组的顺序并不重要。

示例 2:

输入:nums = [0,1,1]
输出:[]
解释:唯一可能的三元组和不为 0 。

示例 3:

输入:nums = [0,0,0]
输出:[[0,0,0]]
解释:唯一可能的三元组和为 0 。

提示:

  • 3 <= nums.length <= 3000
  • -105 <= nums[i] <= 105
func threeSum(nums []int) [][]int {
    sort.Ints(nums)
    n := len(nums)
    res := [][]int{} // res := make([][]int, 0)
    // 枚举一个边界(左边界或者右边界),然后二数之和查找区间内的答案,注意去重
    for i:= 0; i < n-2; i++ {
        if nums[i] > 0 {return res}
        if i > 0 && nums[i] == nums[i-1] {
            continue // 去重
        }
        left, right := i+1, n-1 // 二数之和,目标为 `-nums[i]`
        for left < right {
            if nums[i] + nums[left] + nums[right] < 0 {
                left += 1
            } else if nums[i] + nums[left] + nums[right] > 0 {
                right -= 1
            } else { // 找到了一个和=0的答案
                res = append(res, []int{nums[i], nums[left], nums[right]})
                // 去重(跳过重复数字)
                for left < right && nums[right] == nums[right-1] {right -= 1}
                for left < right && nums[left] == nums[left+1] {left += 1}
                left += 1
                right -= 1
            }  
        }
    }
    return res
}

剑指 Offer II 008. 和大于等于 target 的最短子数组

难度中等113

给定一个含有 n 个正整数的数组和一个正整数 target

找出该数组中满足其和 ≥ target 的长度最小的 连续子数组 [numsl, numsl+1, ..., numsr-1, numsr] ,并返回其长度**。**如果不存在符合条件的子数组,返回 0

示例 1:

输入:target = 7, nums = [2,3,1,2,4,3]
输出:2
解释:子数组 [4,3] 是该条件下的长度最小的子数组。

示例 2:

输入:target = 4, nums = [1,4,4]
输出:1

示例 3:

输入:target = 11, nums = [1,1,1,1,1,1,1,1]
输出:0

提示:

  • 1 <= target <= 109
  • 1 <= nums.length <= 105
  • 1 <= nums[i] <= 105

进阶:

  • 如果你已经实现 O(n) 时间复杂度的解法, 请尝试设计一个 O(n log(n)) 时间复杂度的解法。

方法一:双指针模拟

class Solution {
    public int minSubArrayLen(int target, int[] nums) {
        int s = 0, n = nums.length;
        int left = 0, right = 0;
        while(right < n && s < target){
            s += nums[right];
            right++;
        }
        while(left < right && s - nums[left] >= target){
            s -= nums[left];
            left++;
        }
        if(s < target) return 0;
        int ans = right - left;
        while(right < n){
            s += nums[right];
            while(s - nums[left] >= target){
                s -= nums[left];
                left++;
            }
            ans = Math.min(ans, right - left + 1);
            right++;
        }
        return ans;
    }
}

优雅一点的模拟:

class Solution:
    def minSubArrayLen(self, target: int, nums: List[int]) -> int:
        ans = inf # 初始化最小值
        s = 0 # 初始化数组和
        i, j = 0, 0
        # 枚举右边界,更新左边界
        while j < len(nums):
            s += nums[j] # 扩大窗口
            while i <= j and s >= target: # 缩小左边界
                ans = min(ans, j - i + 1) # 更新答案
                s -= nums[i]
                i += 1
            j += 1
        return ans if ans != inf else 0

剑指 Offer II 009. 乘积小于 K 的子数组

难度中等137

给定一个正整数数组 nums和整数 k ,请找出该数组内乘积小于 k 的连续的子数组的个数。

示例 1:

输入: nums = [10,5,2,6], k = 100
输出: 8
解释: 8 个乘积小于 100 的子数组分别为: [10], [5], [2], [6], [10,5], [5,2], [2,6], [5,2,6]。
需要注意的是 [10,5,2] 并不是乘积小于100的子数组。

示例 2:

输入: nums = [1,2,3], k = 0
输出: 0

提示:

  • 1 <= nums.length <= 3 * 104
  • 1 <= nums[i] <= 1000
  • 0 <= k <= 106
class Solution {
    public int numSubarrayProductLessThanK(int[] nums, int k) {
        if(k <= 1) return 0;
        int n = nums.length, ans = 0;
        int prod = 1, left = 0;
        for(int right = 0; right < n; right++){
            prod *= nums[right];
            while(prod >= k){
                prod /= nums[left++];
            }
            ans += right - left + 1; // 对每个固定的右端点,有 right - left + 1 这么多的子数组。
        }
        return ans;
    }
}

2.前缀和

🎉剑指 Offer II 010. 和为 k 的子数组

难度中等156

给定一个整数数组和一个整数 k **,**请找到该数组中和为 k 的连续子数组的个数。

示例 1:

输入:nums = [1,1,1], k = 2
输出: 2
解释: 此题 [1,1] 与 [1,1] 为两种不同的情况

示例 2:

输入:nums = [1,2,3], k = 3
输出: 2

提示:

  • 1 <= nums.length <= 2 * 104
  • -1000 <= nums[i] <= 1000
  • -107 <= k <= 107

题解:滑动窗口无法解决负数案例

前缀和 + 哈希表

class Solution:
    # 求解以某一个 nums[i] 为结尾的,和为 k 的子数组数量,
    # 本质上就是求解在区间 [0, i] 中, preSum 数组中有多少个值为 preSum[i + 1] - k的元素。
    # 这里可以采用哈希表存储前缀和中的元素以及对应的个数
    def subarraySum(self, nums: List[int], k: int) -> int:
        n = len(nums)
        ans = 0
        pre = [0] * (n+1)
        for i in range(n):
            pre[i+1] = pre[i] + nums[i]
        m = defaultdict(lambda: 0) # 定义哈希表,若元素不存在,默认值为0
        for i in range(n+1):
            ans += m[pre[i] - k] # 寻找目标值 pre[i] - k
            m[pre[i]] += 1 # 记录前缀和数组元素的个数
        return ans

🎉剑指 Offer II 011. 0 和 1 个数相同的子数组

难度中等137

给定一个二进制数组 nums , 找到含有相同数量的 01 的最长连续子数组,并返回该子数组的长度。

示例 1:

输入: nums = [0,1]
输出: 2
说明: [0, 1] 是具有相同数量 0 和 1 的最长连续子数组。

示例 2:

输入: nums = [0,1,0]
输出: 2
说明: [0, 1] (或 [1, 0]) 是具有相同数量 0 和 1 的最长连续子数组。

提示:

  • 1 <= nums.length <= 105
  • nums[i] 不是 0 就是 1
class Solution:
    # 前缀和 + 枚举
    # 1. 将0替换为-1,遍历求前缀和
    # 2. 在遍历过程中,把前缀和和下标进行映射(多个相同前缀和时只记录最小的下标)
    # 3. 每遍历一个元素,就用「当前前缀和」去前面已经统计的前缀和中找到一个使得两者之间区间为0的,并计算这个区间长度
    def findMaxLength(self, nums: List[int]) -> int:   
        n = len(nums)
        pre = [0] * (n+1)
        for i in range(n):
            pre[i+1] = pre[i] + (1 if nums[i] == 1 else -1)
        m = {}
        ans = 0
        # 举例:[1, 1, 0, 0, 1]
        # 前缀和:[0, 1, 2, 1, 0, 1]
        # 当遍历到第3为1时,此时s[1:3]是符合题意的子数组,即查找map[pre[i]]的值
        for i in range(n+1): # 遍历前缀和数组
            if pre[i] in m:
                ans = max(ans, i - m[pre[i]])
            else:
                m[pre[i]] = i
        return ans

剑指 Offer II 012. 左右两边子数组的和相等

难度简单66

给你一个整数数组 nums ,请计算数组的 中心下标

数组 中心下标 是数组的一个下标,其左侧所有元素相加的和等于右侧所有元素相加的和。

如果中心下标位于数组最左端,那么左侧数之和视为 0 ,因为在下标的左侧不存在元素。这一点对于中心下标位于数组最右端同样适用。

如果数组有多个中心下标,应该返回 最靠近左边 的那一个。如果数组不存在中心下标,返回 -1

示例 1:

输入:nums = [1,7,3,6,5,6]
输出:3
解释:
中心下标是 3 。
左侧数之和 sum = nums[0] + nums[1] + nums[2] = 1 + 7 + 3 = 11 ,
右侧数之和 sum = nums[4] + nums[5] = 5 + 6 = 11 ,二者相等。

示例 2:

输入:nums = [1, 2, 3]
输出:-1
解释:
数组中不存在满足此条件的中心下标。

示例 3:

输入:nums = [2, 1, -1]
输出:0
解释:
中心下标是 0 。
左侧数之和 sum = 0 ,(下标 0 左侧不存在元素),
右侧数之和 sum = nums[1] + nums[2] = 1 + -1 = 0 。

提示:

  • 1 <= nums.length <= 104
  • -1000 <= nums[i] <= 1000

题解:前后缀分解

class Solution:
    def pivotIndex(self, nums: List[int]) -> int:
        n = len(nums)
        pre, suf = [0] * (n+1), [0] * (n+1)
        for i in range(n):
            pre[i+1] = pre[i] + nums[i]
        for i in range(n-1, -1, -1):
            suf[i] = suf[i+1] + nums[i]
        # 原数组:[1,7,3,6,5,6]
        print(pre) # [0, 1, 8, 11, 17, 22, 28]
        print(suf) # [28, 27, 20, 17, 11, 6, 0]
        for i in range(n):
            if pre[i+1] == suf[i]:
                return i
        return -1

剑指 Offer II 013. 二维子矩阵的和【二维前缀和】

难度中等82

给定一个二维矩阵 matrix,以下类型的多个请求:

  • 计算其子矩形范围内元素的总和,该子矩阵的左上角为 (row1, col1) ,右下角为 (row2, col2)

实现 NumMatrix 类:

  • NumMatrix(int[][] matrix) 给定整数矩阵 matrix 进行初始化
  • int sumRegion(int row1, int col1, int row2, int col2) 返回左上角 (row1, col1) 、右下角 (row2, col2) 的子矩阵的元素总和。

示例 1:

输入: 
["NumMatrix","sumRegion","sumRegion","sumRegion"]
[[[[3,0,1,4,2],[5,6,3,2,1],[1,2,0,1,5],[4,1,0,1,7],[1,0,3,0,5]]],[2,1,4,3],[1,1,2,2],[1,2,2,4]]
输出: 
[null, 8, 11, 12]

解释:
NumMatrix numMatrix = new NumMatrix([[3,0,1,4,2],[5,6,3,2,1],[1,2,0,1,5],[4,1,0,1,7],[1,0,3,0,5]]]);
numMatrix.sumRegion(2, 1, 4, 3); // return 8 (红色矩形框的元素总和)
numMatrix.sumRegion(1, 1, 2, 2); // return 11 (绿色矩形框的元素总和)
numMatrix.sumRegion(1, 2, 2, 4); // return 12 (蓝色矩形框的元素总和)

提示:

  • m == matrix.length
  • n == matrix[i].length
  • 1 <= m, n <= 200
  • -105 <= matrix[i][j] <= 105
  • 0 <= row1 <= row2 < m
  • 0 <= col1 <= col2 < n
  • 最多调用 104sumRegion 方法
type NumMatrix struct {
    sum [][]int
}


func Constructor(matrix [][]int) NumMatrix {
    m, n := len(matrix), len(matrix[0])
    presum := make([][]int, m+1)
    for i := 0; i <= m; i++ {
        presum[i] = make([]int, n+1)
    }
    // 当前格子(和) = 上方的格子(和) + 左边的格子(和) 
    //					- 左上角的格子(和) + 当前格子(值)【和是指对应的前缀和,值是指原数组中的值】
    for i := 0; i < m; i++ {
        for j := 0; j < n; j++ {
            presum[i+1][j+1] = presum[i][j+1] + presum[i+1][j] - presum[i][j] + matrix[i][j]
        }
    } 
    return NumMatrix{sum: presum}
}


func (this *NumMatrix) SumRegion(row1 int, col1 int, row2 int, col2 int) int {
    // 前缀和是从 1 开始,原数组是从 0 开始,上来先将原数组坐标全部 +1,转换为前缀和坐标
    x1, y1, x2, y2 := row1 + 1, col1 + 1, row2 + 1, col2 + 1
    // 记作 22 - 12 - 21 + 11,然后 不减,减第一位,减第二位,减两位
    // 也可以记作 22 - 12(x - 1) - 21(y - 1) + 11(x y 都 - 1)
    return this.sum[x2][y2] - this.sum[x1-1][y2] - this.sum[x2][y1 - 1] + this.sum[x1-1][y1-1];
}


/**
 * Your NumMatrix object will be instantiated and called as such:
 * obj := Constructor(matrix);
 * param_1 := obj.SumRegion(row1,col1,row2,col2);
 */

3.滑动窗口专题

🎉剑指 Offer II 014. 字符串中的变位词

难度中等93

给定两个字符串 s1s2,写一个函数来判断 s2 是否包含 s1 的某个变位词。

换句话说,第一个字符串的排列之一是第二个字符串的 子串

示例 1:

输入: s1 = "ab" s2 = "eidbaooo"
输出: True
解释: s2 包含 s1 的排列之一 ("ba").

示例 2:

输入: s1= "ab" s2 = "eidboaoo"
输出: False

提示:

  • 1 <= s1.length, s2.length <= 104
  • s1s2 仅包含小写字母

题解:https://leetcode.cn/problems/permutation-in-string/solution/zhu-shi-chao-xiang-xi-de-hua-dong-chuang-rc7d/

滑动窗口 + 字典

  • 分析一: 题目要求 s1 的排列之一是 s2 的一个子串。而子串必须是连续的,所以要求的 s2 子串的长度跟 s1 长度必须相等。
  • 分析二: 那么我们有必要把 s1 的每个排列都求出来吗?当然不用。如果字符串 ab 的一个排列,那么当且仅当它们两者中的每个字符的个数都必须完全相等。

所以,根据上面两点分析,我们已经能确定这个题目可以使用 滑动窗口 + 字典 来解决。

我们使用一个长度和 s1 长度相等的固定窗口大小的滑动窗口,在 s2 上面从左向右滑动,判断 s2 在滑动窗口内的每个字符出现的个数是否跟 s1 每个字符出现次数完全相等。

class Solution {
    /**
     滑窗+数组统计
    与LC76的最小覆盖串非常类似,用滑窗的思路可以解决,这题更加简单一些
        时间复杂度:O(m+n) 看很复杂的:O(1)
     */
    public boolean checkInclusion(String s1, String s2) {
        int n1 = s1.length(), n2 = s2.length();
        int[] cnt1 = new int[26], cnt2 = new int[26];
        for(int i = 0; i < n1; i++)
            cnt1[s1.charAt(i) - 'a'] += 1;
        int l = 0, r = 0, cnt = 0; // 使用cnt记录 s2字符串中 符合s1字符串内字符的元素
        while(r < n2){
            // s2[r]进入窗口
            if(++cnt2[s2.charAt(r) - 'a'] <= cnt1[s2.charAt(r) - 'a'])
                cnt++;	// sr[r]的加入没有超出cnt1[r]字符的个数,是个合法字符
            // 一直右移l指针直至不满足条件
            while(cnt == n1){
                if(r - l + 1 == n1)
                    return true;
                if(--cnt2[s2.charAt(l) - 'a'] < cnt1[s2.charAt(l) - 'a']) 
                    cnt--;
                l++;
            }
            r++;
        }
        return false;
    }
}
/**
举例 s1 = "ab" s2 = "eidbaooo"
当s2枚举到right=4时,此时cnt == n1,进入循环,缩小窗口,若窗口长度=s1长度,则找到了答案
*/

python

class Solution:
    def checkInclusion(self, s1: str, s2: str) -> bool:
        n1, n2 = len(s1), len(s2)
        cnt1, cnt2 = [0]*26, [0]*26
        for i in range(n1):
            cnt1[ord(s1[i]) - ord('a')] += 1
        left, cnt = 0, 0
        for right in range(n2):
            c = ord(s2[right]) - ord('a')
            cnt2[c] += 1
            if cnt2[c] <= cnt1[c]:
                cnt += 1
            while cnt == n1:
                if right - left + 1 == n1:
                    return True
                cnt2[ord(s2[left]) - orad('a')] -= 1
                if cnt2[ord(s2[left]) - ord('a')] < cnt1[ord(s2[left]) - ord('a')]:
                    cnt -= 1
                left += 1
        return False

剑指 Offer II 015. 字符串中的所有变位词

难度中等53

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

变位词 指字母相同,但排列不同的字符串。

示例 1:

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

示例 2:

输入: s = "abab", p = "ab"
输出: [0,1,2]
解释:
起始索引等于 0 的子串是 "ab", 它是 "ab" 的变位词。
起始索引等于 1 的子串是 "ba", 它是 "ab" 的变位词。
起始索引等于 2 的子串是 "ab", 它是 "ab" 的变位词。

提示:

  • 1 <= s.length, p.length <= 3 * 104
  • sp 仅包含小写字母
class Solution:
    def findAnagrams(self, s: str, p: str) -> List[int]:
        n1, n2 = len(p), len(s)
        cnt1, cnt2 = [0]*26, [0]*26
        for i in range(n1):
            cnt1[ord(p[i]) - ord('a')] += 1
        left, cnt = 0, 0
        ans = []
        for right in range(n2):
            c = ord(s[right]) - ord('a')
            cnt2[c] += 1
            if cnt2[c] <= cnt1[c]:
                cnt += 1
            while cnt == n1:
                if right - left + 1 == n1:
                    ans.append(left)
                cnt2[ord(s[left]) - ord('a')] -= 1
                if cnt2[ord(s[left]) - ord('a')] < cnt1[ord(s[left]) - ord('a')]:
                    cnt -= 1
                left += 1
        return ans

剑指 Offer II 016. 不含重复字符的最长子字符串

难度中等80

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

示例 1:

输入: s = "abcabcbb"
输出: 3 
解释: 因为无重复字符的最长子字符串是 "abc",所以其长度为 3。

示例 2:

输入: s = "bbbbb"
输出: 1
解释: 因为无重复字符的最长子字符串是 "b",所以其长度为 1。

示例 3:

输入: s = "pwwkew"
输出: 3
解释: 因为无重复字符的最长子串是 "wke",所以其长度为 3。
     请注意,你的答案必须是 子串 的长度,"pwke" 是一个子序列,不是子串。

示例 4:

输入: s = ""
输出: 0

提示:

  • 0 <= s.length <= 5 * 104
  • s 由英文字母、数字、符号和空格组成
class Solution:
    def lengthOfLongestSubstring(self, s: str) -> int:
        cnt = [0] * 128
        ans, left = 0, 0
        for right in range(len(s)):
            cnt[ord(s[right])] += 1
            while cnt[ord(s[right])] > 1:
                cnt[ord(s[left])] -= 1
                left += 1
            ans = max(ans, right - left + 1)
        return ans

剑指 Offer II 017. 含有所有字符的最短字符串

难度困难98

给定两个字符串 st 。返回 s 中包含 t 的所有字符的最短子字符串。如果 s 中不存在符合条件的子字符串,则返回空字符串 ""

如果 s 中存在多个符合条件的子字符串,返回任意一个。

注意: 对于 t 中重复字符,我们寻找的子字符串中该字符数量必须不少于 t 中该字符数量。

示例 1:

输入:s = "ADOBECODEBANC", t = "ABC"
输出:"BANC" 
解释:最短子字符串 "BANC" 包含了字符串 t 的所有字符 'A'、'B'、'C'

示例 2:

输入:s = "a", t = "a"
输出:"a"

示例 3:

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

提示:

  • 1 <= s.length, t.length <= 105
  • st 由英文字母组成
class Solution:
    def minWindow(self, s: str, t: str) -> str:
        n1, n2 = len(s), len(t)
        cnt1, cnt2 = [0]*128, [0]*128
        for i in range(n2):
            cnt2[ord(t[i])] += 1
        ans = s + " "
        cnt, left = 0, 0
        for right in range(n1):
            cnt1[ord(s[right])] += 1
            if cnt1[ord(s[right])] <= cnt2[ord(s[right])]:
                cnt += 1
            while cnt == n2:
                ans = ans if len(ans) < (right-left+1) else s[left:right+1]
                cnt1[ord(s[left])] -= 1
                if cnt1[ord(s[left])] < cnt2[ord(s[left])]:
                    cnt -= 1
                left += 1
        return ans if len(ans) <= n1 else ""
        

4.回文系列

剑指 Offer II 018. 有效的回文

难度简单53

给定一个字符串 s ,验证 s 是否是 回文串 ,只考虑字母和数字字符,可以忽略字母的大小写。

本题中,将空字符串定义为有效的 回文串

示例 1:

输入: s = "A man, a plan, a canal: Panama"
输出: true
解释:"amanaplanacanalpanama" 是回文串

示例 2:

输入: s = "race a car"
输出: false
解释:"raceacar" 不是回文串

提示:

  • 1 <= s.length <= 2 * 105
  • 字符串 s 由 ASCII 字符组成

相向双指针:

class Solution {
    public boolean isPalindrome(String S) {
        String s = S.toLowerCase();
        int left = 0, right = s.length() - 1;
        while(left < right){
            while(left < right && 
                    !(s.charAt(left) >= 'a' && s.charAt(left) <= 'z') &&
                    !(s.charAt(left) >= '0' && s.charAt(left) <= '9'))
                left += 1;
            while(left < right && 
                    !(s.charAt(right) >= 'a' && s.charAt(right) <= 'z') &&
                    !(s.charAt(right) >= '0' && s.charAt(right) <= '9'))
                right -= 1;
            if(!(s.charAt(left) == s.charAt(right)))
                return false;
            left += 1;
            right -= 1;
        }
        return true;
    }
}

剑指 Offer II 019. 最多删除一个字符得到回文

难度简单75

给定一个非空字符串 s,请判断如果 最多 从字符串中删除一个字符能否得到一个回文字符串。

示例 1:

输入: s = "aba"
输出: true

示例 2:

输入: s = "abca"
输出: true
解释: 可以删除 "c" 字符 或者 "b" 字符

示例 3:

输入: s = "abc"
输出: false

提示:

  • 1 <= s.length <= 105
  • s 由小写英文字母组成

题解:首先看到范围10^5,就不能枚举删除每个字符剩下的字符串是不是回文,脑筋急转弯:既然只能删一次,那么就模拟

class Solution:
    def validPalindrome(self, s: str) -> bool:
        def isPalindrome(s: str) -> bool:
            start, end = 0, len(s)-1
            while start < end:
                if s[start] != s[end]:
                    return False
                start += 1
                end -= 1
            return True

        l, r = 0, len(s)-1
        while l < r:
            if s[l] == s[r]:
                l += 1
                r -= 1
                continue
            # s[l] != s[r],可以选择删除l或删除r,如果是回文串的话,答案就是True
            if isPalindrome(s[l+1: r+1]) or isPalindrome(s[l:r]):
                return True
            else:
                return False
        return True

剑指 Offer II 020. 回文子字符串的个数

难度中等93

给定一个字符串 s ,请计算这个字符串中有多少个回文子字符串。

具有不同开始位置或结束位置的子串,即使是由相同的字符组成,也会被视作不同的子串。

示例 1:

输入:s = "abc"
输出:3
解释:三个回文子串: "a", "b", "c"

示例 2:

输入:s = "aaa"
输出:6
解释:6个回文子串: "a", "a", "a", "aa", "aa", "aaa"

提示:

  • 1 <= s.length <= 1000
  • s 由小写英文字母组成

方法一:中心扩散

class Solution:
    def countSubstrings(self, s: str) -> int:
        # 中心扩散法
        ans = 0
        for center in range(2 * len(s)):
            left = int(center / 2)
            right = int(left + center % 2)
            while left >= 0 and right < len(s) and s[left] == s[right]:
                ans += 1
                left -= 1
                right += 1
        return ans

方法二:动态规划

首先这一题可以使用动态规划来进行解决:

  • 状态:dp[i][j] 表示字符串s[i,j]区间的子串是否是一个回文串。
  • 状态转移方程:当 s[i] == s[j] && (j - i < 2 || dp[i + 1][j - 1]) 时,dp[i][j]=true,否则为false

这个状态转移方程是什么意思呢?

  1. 当只有一个字符时,比如 a 自然是一个回文串。
  2. 当有两个字符时,如果是相等的,比如 aa,也是一个回文串。
  3. 当有三个及以上字符时,比如 ababa 这个字符记作串 1,把两边的 a 去掉,也就是 bab 记作串 2,可以看出只要串2是一个回文串,那么左右各多了一个 a 的串 1 必定也是回文串。所以当 s[i]==s[j] 时,自然要看 dp[i+1][j-1] 是不是一个回文串。
class Solution:
    # 状态:dp[i][j] 表示字符串s在[i,j]区间的子串是否是一个回文串。
    # 状态转移方程:当 s[i] == s[j] && (j - i < 2 || dp[i + 1][j - 1]) 时,dp[i][j]=true,否则为false
    def countSubstrings(self, s: str) -> int:
        n = len(s)
        dp = [[0 for _ in range(n)] for _ in range(n)]
        ans = 0
        for j in range(n):
            for i in range(j+1):
                if s[i] == s[j] and (j-i < 2 or dp[i+1][j-1]):
                    dp[i][j] = True
                    ans += 1
        return ans

5.链表问题

剑指 Offer II 021. 删除链表的倒数第 n 个结点

难度中等75

给定一个链表,删除链表的倒数第 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]

提示:

  • 链表中结点的数目为 sz
  • 1 <= sz <= 30
  • 0 <= Node.val <= 100
  • 1 <= n <= sz
class Solution {
    public ListNode removeNthFromEnd(ListNode head, int n) {
        ListNode dummy = new ListNode(-1, head);
        ListNode pre = dummy;
        while(n-- > 0){
            pre = pre.next;
        }
        ListNode p = dummy;
        while(pre.next != null){
            pre = pre.next;
            p = p.next;
        }
        p.next = p.next.next;
        return dummy.next;
    }
}

方法二:一次遍历(递归)

class Solution:

    count = 0
    def removeNthFromEnd(self, head: ListNode, n: int) -> ListNode:
        # 使用递归,递归地返回当前节点位于倒数第几个,若为倒数第n个,直接删除节点
        if not head:
            return head
        tmp = head
        head.next = self.removeNthFromEnd(head.next, n)
        self.count += 1
        return head.next if self.count == n else head

剑指 Offer II 022. 链表中环的入口节点

难度中等108

给定一个链表,返回链表开始入环的第一个节点。 从链表的头节点开始沿着 next 指针进入环的第一个节点为环的入口节点。如果链表无环,则返回 null

为了表示给定链表中的环,我们使用整数 pos 来表示链表尾连接到链表中的位置(索引从 0 开始)。 如果 pos-1,则在该链表中没有环。注意,pos 仅仅是用于标识环的情况,并不会作为参数传递到函数中。

**说明:**不允许修改给定的链表。

示例 1:

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

示例 2:

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

示例 3:

输入:head = [1], pos = -1
输出:返回 null
解释:链表中没有环。

提示:

  • 链表中节点的数目范围在范围 [0, 104]
  • -105 <= Node.val <= 105
  • pos 的值为 -1 或者链表中的一个有效索引
class Solution:
    def detectCycle(self, head: ListNode) -> ListNode:
        fast, slow = head, head
        while fast and fast.next:
            fast = fast.next.next
            slow = slow.next
            if fast == slow:
                p = head
                while p != slow:
                    p = p.next
                    slow = slow.next
                return p
        return None

剑指 Offer II 023. 两个链表的第一个重合节点

难度简单78

给定两个单链表的头节点 headAheadB ,请找出并返回两个单链表相交的起始节点。如果两个链表没有交点,返回 null

class Solution:
    # a + c + b = b + c + a
    # 若无交集,则a + b = b + a
    def getIntersectionNode(self, headA: ListNode, headB: ListNode) -> ListNode:
        if not headA or not headB: return None
        pa, pb = headA, headB
        while pa != pb:
            pa = pa.next if pa else headB
            pb = pb.next if pb else headA
        return pa

剑指 Offer II 024. 反转链表

难度简单138

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

class Solution:
    def reverseList(self, head: ListNode) -> ListNode:
        if not head or not head.next:
            return head
        newhead = self.reverseList(head.next)
        head.next.next = head
        head.next = None
        return newhead

🎉剑指 Offer II 025. 链表中的两数相加【002.二进制加法链表版】

难度中等93

给定两个 非空链表 l1l2 来代表两个非负整数。数字最高位位于链表开始位置。它们的每个节点只存储一位数字。将这两数相加会返回一个新的链表。

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

# Definition for singly-linked list.
# class ListNode:
#     def __init__(self, val=0, next=None):
#         self.val = val
#         self.next = next
class Solution:
    def addTwoNumbers(self, l1: ListNode, l2: ListNode) -> ListNode:
        # 反转链表
        def reverse(node: ListNode) -> ListNode:
            if not node or not node.next:
                return node
            newhead = reverse(node.next)
            node.next.next = node
            node.next = None
            return newhead
        h1, h2 = reverse(l1), reverse(l2)
        # 进位符号
        carry = 0
        dummy = ListNode(-1, None)
        p = dummy
        # 流程如 剑指 Offer II 002. 二进制加法
        while h1 or h2 or carry:
            a = 0 if not h1 else h1.val
            b = 0 if not h2 else h2.val
            s = a + b + carry
            if s >= 10:
                carry = 1
                s -= 10
            else:
                carry = 0
            tmp = ListNode(s, None)
            p.next = tmp
            p = tmp
            if h1: h1 = h1.next
            if h2: h2 = h2.next
        tail = dummy.next
        return reverse(tail)

🎉剑指 Offer II 026. 重排链表

难度中等114

给定一个单链表 L 的头节点 head ,单链表 L 表示为:

L0 → L1 → … → Ln-1 → Ln 

请将其重新排列后变为:

L0 → Ln → L1 → Ln-1 → L2 → Ln-2 → …

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

题解:①找链表中间节点、②反转后半部分链表、③合并链表

class Solution:
    def reorderList(self, head: ListNode) -> None:
        # 反转链表
        def reverse(node: ListNode) -> ListNode:
            if not node or not node.next:
                return node
            newhead = reverse(node.next)
            node.next.next = node
            node.next = None
            return newhead
        # 找链表中间节点
        def findmiddle(node: ListNode) -> ListNode:
            fast, slow = node, node
            while fast and fast.next:
                fast, slow = fast.next.next, slow.next
            return slow
        
        mid = findmiddle(head)
        head2 = reverse(mid)
        while head2.next:
            nxt, nxt2 = head.next, head2.next
            head.next = head2
            head2.next = nxt
            head, head2 = nxt, nxt2
        return

剑指 Offer II 027. 回文链表

难度简单113

给定一个链表的 头节点 head **,**请判断其是否为回文链表。

如果一个链表是回文,那么链表节点序列从前往后看和从后往前看是相同的。

题解:同剑指 Offer II 026. 重排链表

class Solution:
    def isPalindrome(self, head: ListNode) -> bool:
        # 反转链表
        def reverse(node: ListNode) -> ListNode:
            if not node or not node.next:
                return node
            newhead = reverse(node.next)
            node.next.next = node
            node.next = None
            return newhead
        # 找链表中间节点
        def findmiddle(node: ListNode) -> ListNode:
            fast, slow = node, node
            while fast and fast.next:
                fast, slow = fast.next.next, slow.next
            return slow
        
        mid = findmiddle(head)
        head2 = reverse(mid)
        while head and head2:
            if head.val != head2.val:
                return False
            head, head2 = head.next, head2.next
        return True

🎉剑指 Offer II 028. 展平多级双向链表

难度中等71

多级双向链表中,除了指向下一个节点和前一个节点指针之外,它还有一个子链表指针,可能指向单独的双向链表。这些子列表也可能会有一个或多个自己的子项,依此类推,生成多级数据结构,如下面的示例所示。

给定位于列表第一级的头节点,请扁平化列表,即将这样的多级双向链表展平成普通的双向链表,使所有结点出现在单级双链表中。

输入:head = [1,2,null,3]
输出:[1,3,2]
解释:

输入的多级列表如下图所示:

  1---2---NULL
  |
  3---NULL

方法一:迭代

题解:https://leetcode.cn/problems/Qv1Da2/solution/by-zealous-volhardrla-zxgs/

首先定义变量 pre 表示上一个节点,cur 表示当前节点

  1. 每步操作:
pre.next = cur;
cur.prev = pre;
pre = cur;
cur = cur.next;
  1. 如果发现cur有child,那么下一个节点就是child,并且要把cur.next存入辅助栈中,然后cur.next = child

  2. 如果cur == null则将栈顶元素取出,作为cur

class Solution {
    public Node flatten(Node head) {
        Deque<Node> st = new ArrayDeque<>();
        Node pre = null, cur = head;
        while(cur != null || !st.isEmpty()){
            // 如果cur == null则将栈顶元素取出,作为cur
            if(cur == null) cur = st.pop();
            // 如果发现cur有child,那么下一个节点就是child,并且要把cur.next存入辅助栈中,然后cur.next = child
            if(cur.child != null){
                if(cur.next != null) st.push(cur.next);
                cur.next = cur.child;
                cur.child = null;
            }
            // 构建双向链表
            cur.prev = pre;
            if(pre != null) pre.next = cur; // 初始 pre == null
            pre = cur;
            cur = cur.next;
        }
        return head;
    }
}

方法二:迭代(立即处理当前child层)

使用辅助栈的迭代是自下而上的处理,而立即处理当前child层则是自上而下的处理

一遇到child,我们就立即处理当前child层,将其塞进上一级的链表

class Solution {
    public Node flatten(Node head) {
        Node cur = head;
        while(cur != null){
            if(cur.child != null){ // 立即处理当前child所在层
                Node next = cur.next;
                cur.next = cur.child; // cur的next更新为child
                cur.child.prev = cur; // child的prev更新为cur
                cur.child = null; // 置空child
                Node last = cur.next; // 找到child层的最后一个节点
                while(last.next != null) last = last.next;
                if(next != null){ // 如果next不为空,则要连接next和last
                    next.prev = last;
                    last.next = next;
                }
            }
            cur = cur.next;
        }
        return head;
    }
}

方法三:递归

递归方法也是自下而上的处理方式,递归函数返回当前层的最后一个节点

遇到child的时候,可以利用递归函数得到child层的最后一个节点,进行链表的更新

class Solution {
    public Node flatten(Node head) {
        dfs(head);
        return head;
    }

    public Node dfs(Node cur){ // 返回当前层的最后一个节点
        Node last = cur; // 记录当前层的最后一个节点
        while(cur != null){
            Node next = cur.next; // 记录cur的next节点,如果cur有child节点,child节点的最后一个节点要接在该next节点前面
            if(cur.child != null){
                Node childLast = dfs(cur.child); // 递归获取child层的最后一个节点
                cur.next = cur.child; // 进行更新
                cur.next.prev = cur;
                cur.child = null;
                if(next != null){
                    childLast.next = next;
                    next.prev = childLast;
                }
                last = childLast; //last 更新为child层的最后一个节点,因为不知道next是否为空,而childLast是当前已知的最后一个节点
            }else last = cur;
            cur = next;
        }
        return last;
    }
}

😑剑指 Offer II 029. 排序的循环链表

难度中等151

给定循环单调非递减列表中的一个点,写一个函数向这个列表中插入一个新元素 insertVal ,使这个列表仍然是循环升序的。

给定的可以是这个列表中任意一个顶点的指针,并不一定是这个列表中最小元素的指针。

如果有多个满足条件的插入位置,可以选择任意一个位置插入新的值,插入后整个列表仍然保持有序。

如果列表为空(给定的节点是 null),需要创建一个循环有序列表并返回这个节点。否则。请返回原先给定的节点。

class Solution {
    public Node insert(Node head, int insertVal) {
        Node insertNode = new Node(insertVal);
        // 1. head 为 nullptr
        if(head == null){
            insertNode.next = insertNode;
            return insertNode;
        }   
        // 2. head 不为 nullptr
        // 找最大节点
        // 最大节点的下一个节点为最小节点
        Node p = head;
        while(p.next != head && p.val <= p.next.val){
            p = p.next;
        }
        Node q = p.next; // q最小节点  p最大节点 p -> q
        // 大于最大值,小于最小值  或者 只有一个节点
        if(p.val <= insertVal || q.val >= insertVal || p == q){
            insertNode.next = q;
            p.next = insertNode;
        }else{
            // 介于最大值与最小值之间,找插入点
            while(q.next.val < insertVal){
                q = q.next;
            }
            insertNode.next = q.next;
            q.next = insertNode;
        }
        return head;
    }
}

🎉剑指 Offer II 077. 链表排序

难度中等121

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

提示:

  • 链表中节点的数目在范围 [0, 5 * 104]
  • -105 <= Node.val <= 105

https://leetcode.cn/problems/7WHec2/solution/hua-luo-yue-que-fen-er-zhi-zhi-tu-jie-li-95mj/

题解:链表归并排序

链表和数组不同,在数组中,交换两个元素的值是非常简单的,而在链表中,不论是随机读取链表中的第 i 个节点还是交换节点都是比较麻烦的。

基于链表的这一特点,直接将大部分的排序方式拒之门外。

考虑归并排序的原理:

将链表分成两个子链表,将两个子链表排序后,再合并为一个链表。

合并两个有序链表是非常简单的,只要不断的比较链表的头结点的值,将较短的放入合并后的链表中,并更新头结点就可以了。

综上所述,我们选择归并排序完成本题。

还有一个小的难点,就是如何将一个链表均等的分为两个子链表?

答案是,使用快慢指针遍历链表,当快指针刚好走到链表尾端时,慢指针的位置就是链表的中间位置。

class Solution {
    public ListNode sortList(ListNode head) {
        if(head == null || head.next == null)
            return head;
        // list 为链表后半段的头结点,在 splitList 函数中完成了将链表从中间截断的处理
        ListNode list = splitList(head);
        head = sortList(head);
        list = sortList(list);
        return mergeList(head, list);
    }

    public ListNode splitList(ListNode head){
        // 当 fast 达到尾端时,slow 刚好指向前半段链表的最后一个节点
        ListNode dummy = new ListNode(-1, head);
        ListNode fast = dummy, slow = dummy;
        while(fast != null && fast.next != null){
            fast = fast.next.next;
            slow = slow.next;
        }
        ListNode res = slow.next;
        slow.next = null;
        return res;
    }

    public ListNode mergeList(ListNode head1, ListNode head2){
        // 增加虚拟头结点,可以统一操作, 不用刻意考虑链表中存在空节点的情况,也不用特意增加判断合并后的链表的头结点是什么
        ListNode dummy = new ListNode();
        ListNode cur = dummy;
        ListNode node1 = head1, node2 = head2;
        while(node1 != null && node2 != null){
            // 将两个链表中较小的加入到合并后的链表中
            if(node1.val <= node2.val){
                cur.next = node1;
                node1 = node1.next;
            }else{
                cur.next = node2;
                node2 = node2.next;
            }
            cur = cur.next;
        }
        if(node1 != null) cur.next = node1;
        if(node2 != null) cur.next = node2;
        return dummy.next;
    }
}

🎉剑指 Offer II 078. 合并排序链表

难度困难87

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

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

示例 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

示例 2:

输入:lists = []
输出:[]

示例 3:

输入:lists = [[]]
输出:[]

提示:

  • k == lists.length
  • 0 <= k <= 10^4
  • 0 <= lists[i].length <= 500
  • -10^4 <= lists[i][j] <= 10^4
  • lists[i]升序 排列
  • lists[i].length 的总和不超过 10^4

题解:多路归并排序

class Solution {
    public ListNode mergeKLists(ListNode[] lists) {
        ListNode dummy = new ListNode(-1);
        ListNode cur = dummy;
        int n = lists.length;
        PriorityQueue<ListNode> pq = new PriorityQueue<>((a, b) -> a.val - b.val);
        // 将每个链表的头节点入队列
        for(int i = 0; i < lists.length; i++){
            if(lists[i] != null)
                pq.add(lists[i]);
        }
        while(!pq.isEmpty()){
            // 每次弹出队列中最小的节点,判断该节点后面有没有节点,有就加入队列
            ListNode p = pq.poll();
            cur.next = p;
            cur = p;
            if(p.next != null) pq.add(p.next);
        }
        cur.next = null;
        return dummy.next;
    }
}

方法二:归并排序

其实这道题也可以使用归并排序的思想考虑,输入的 k 个链表可以分成两部分。如果将前 k/2 和后 k/2 个链表分别合并成两个排序链表,再将这两个链表合成一个链表,那么问题就解决了。合并前 k/2 和后 k/2 个链表和合并 k 个链表属于同一个问题,可以调用递归函数解决。

完整代码如下,其中 merge 函数与面试题 77 中一样,都是实现合并两个两个有序链表。因为递归调用的深度为 O(logk),总节点数为 n,那么时间复杂度为 O(nlogk),空间复杂度为 O(logk)。

class Solution {
    public ListNode mergeKLists(ListNode[] lists) {
        if(lists.length == 0) return null;
        return merge(lists, 0, lists.length-1);
    }

    public ListNode merge(ListNode[] lists, int start, int end){
        if(start == end){
            return lists[start];
        }
        int mid = start + ((end - start) >> 1);
        ListNode head1 = merge(lists, start, mid);
        ListNode head2 = merge(lists, mid+1, end);
        return mergeList(head1, head2);
    }

    public ListNode mergeList(ListNode head1, ListNode head2){
        ListNode dummy = new ListNode();
        ListNode cur = dummy;
        ListNode node1 = head1, node2 = head2;
        while(node1 != null && node2 != null){
            // 将两个链表中较小的加入到合并后的链表中
            if(node1.val <= node2.val){
                cur.next = node1;
                node1 = node1.next;
            }else{
                cur.next = node2;
                node2 = node2.next;
            }
            cur = cur.next;
        }
        if(node1 != null) cur.next = node1;
        if(node2 != null) cur.next = node2;
        return dummy.next;
    }
}

6.模拟

剑指 Offer II 032. 有效的变位词

难度简单41

给定两个字符串 st ,编写一个函数来判断它们是不是一组变位词(字母异位词)。

注意:*s**t* 中每个字符出现的次数都相同且字符顺序不完全相同,则称 *s**t* 互为变位词(字母异位词)。

示例 1:

输入: s = "anagram", t = "nagaram"
输出: true

示例 2:

输入: s = "rat", t = "car"
输出: false

示例 3:

输入: s = "a", t = "a"
输出: false

提示:

  • 1 <= s.length, t.length <= 5 * 104
  • s and t 仅包含小写字母

进阶: 如果输入字符串包含 unicode 字符怎么办?你能否调整你的解法来应对这种情况?

class Solution:
    def isAnagram(self, s: str, t: str) -> bool:
        if s == t:
            return False
        cnt = [0] * 128
        for c in s:
            cnt[ord(c) - ord('a')] += 1
        for c in t:
            cnt[ord(c) - ord('a')] -= 1
        for val in cnt:
            if val != 0: 
                return False
        return True

剑指 Offer II 033. 变位词组

难度中等55

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

**注意:**若两个字符串中每个字符出现的次数都相同,则称它们互为变位词。

示例 1:

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

示例 2:

输入: strs = [""]
输出: [[""]]

示例 3:

输入: strs = ["a"]
输出: [["a"]]

提示:

  • 1 <= strs.length <= 104
  • 0 <= strs[i].length <= 100
  • strs[i] 仅包含小写字母
class Solution {
    public List<List<String>> groupAnagrams(String[] strs) {
        Map<String, List<String>> map = new HashMap<>();
        for(String s : strs){
            char[] c = s.toCharArray();
            Arrays.sort(c);
            String ns = new String(c);
            if(!map.containsKey(ns))
                map.put(ns, new ArrayList<>());
            map.get(ns).add(s);
        }
        return new ArrayList<>(map.values());
    }
}

🎉剑指 Offer II 034. 外星语言是否排序

难度简单45

某种外星语也使用英文小写字母,但可能顺序 order 不同。字母表的顺序(order)是一些小写字母的排列。

给定一组用外星语书写的单词 words,以及其字母表的顺序 order,只有当给定的单词在这种外星语中按字典序排列时,返回 true;否则,返回 false

示例 1:

输入:words = ["hello","leetcode"], order = "hlabcdefgijkmnopqrstuvwxyz"
输出:true
解释:在该语言的字母表中,'h' 位于 'l' 之前,所以单词序列是按字典序排列的。

示例 2:

输入:words = ["word","world","row"], order = "worldabcefghijkmnpqstuvxyz"
输出:false
解释:在该语言的字母表中,'d' 位于 'l' 之后,那么 words[0] > words[1],因此单词序列不是按字典序排列的。

示例 3:

输入:words = ["apple","app"], order = "abcdefghijklmnopqrstuvwxyz"
输出:false
解释:当前三个字符 "app" 匹配时,第二个字符串相对短一些,然后根据词典编纂规则 "apple" > "app",因为 'l' > '∅',其中 '∅' 是空白字符,定义为比任何其他字符都小(更多信息)。

提示:

  • 1 <= words.length <= 100
  • 1 <= words[i].length <= 20
  • order.length == 26
  • words[i]order 中的所有字符都是英文小写字母。
class Solution {
    Map<Character, Integer> map;
    public boolean isAlienSorted(String[] words, String order) {
        map = new HashMap<>();
        char[] arrs = order.toCharArray();
        for(int i = 0; i < arrs.length; i++){
            map.put(arrs[i], i);
        }
        // 按照Order的顺序,逐个比对每个单词的字母
        // 如果两个单词的字典序相等,也认为当前的排序是正确的。
        for(int i = 1; i < words.length; i++){
            // compareOrder : a < b 返回true,否则返回false
            if(!compareOrder(words[i-1], words[i]))
                return false;
        }
        return true;
    }

    public boolean compareOrder(String a, String b){
        int ptr = 0;
        while(ptr < a.length() && ptr < b.length()){
            if(map.get(a.charAt(ptr)) > map.get(b.charAt(ptr)))
                return false;
            else if(map.get(a.charAt(ptr)) < map.get(b.charAt(ptr)))
                return true;
            else ptr++;
        }
        // a 和 b至少有一个遍历完了
        if(ptr == a.length() && ptr <= b.length())
            return true;
        else return false;
    }
}

剑指 Offer II 035. 最小时间差

难度中等43

给定一个 24 小时制(小时:分钟 “HH:MM”)的时间列表,找出列表中任意两个时间的最小时间差并以分钟数表示。

示例 1:

输入:timePoints = ["23:59","00:00"]
输出:1

示例 2:

输入:timePoints = ["00:00","23:59","00:00"]
输出:0

提示:

  • 2 <= timePoints <= 2 * 104
  • timePoints[i] 格式为 “HH:MM”
class Solution {
    /**
    排序+鸽巢原理
    将时间进行排序后,最小间隔一定在相邻两个时间之间,或者在首位之间。
    24小时乘60分钟,一共有1440种时间可能,因此一旦时间个数大于1440,则最小间隔一定是0.
     */
    public int findMinDifference(List<String> timePoints) {
        int n = timePoints.size();
        int[] nums = new int[n];
        for(int i = 0; i < n; i++){
            nums[i] = hourtominute(timePoints.get(i));
        }
        Arrays.sort(nums);
        int ans = nums[0] + 1440 - nums[n-1];
        for(int i = 1; i < n; i++){
            ans = Math.min(ans, nums[i] - nums[i-1]);
        }
        return ans;
    }

    public int hourtominute(String s){
        int res = 0;
        String[] strs = s.split(":");
        res += Integer.valueOf(strs[0]) * 60;
        res += Integer.valueOf(strs[1]);
        return res % (60*24);
    }
}

剑指 Offer II 036. 后缀表达式【逆波兰表达式】

难度中等48

根据 逆波兰表示法,求该后缀表达式的计算结果。

有效的算符包括 +-*/ 。每个运算对象可以是整数,也可以是另一个逆波兰表达式。

说明:

  • 整数除法只保留整数部分。
  • 给定逆波兰表达式总是有效的。换句话说,表达式总会得出有效数值且不存在除数为 0 的情况。

示例 1:

输入:tokens = ["2","1","+","3","*"]
输出:9
解释:该算式转化为常见的中缀算术表达式为:((2 + 1) * 3) = 9

示例 2:

输入:tokens = ["4","13","5","/","+"]
输出:6
解释:该算式转化为常见的中缀算术表达式为:(4 + (13 / 5)) = 6

示例 3:

输入:tokens = ["10","6","9","3","+","-11","*","/","*","17","+","5","+"]
输出:22
解释:
该算式转化为常见的中缀算术表达式为:
  ((10 * (6 / ((9 + 3) * -11))) + 17) + 5
= ((10 * (6 / (12 * -11))) + 17) + 5
= ((10 * (6 / -132)) + 17) + 5
= ((10 * 0) + 17) + 5
= (0 + 17) + 5
= 17 + 5
= 22

提示:

  • 1 <= tokens.length <= 104
  • tokens[i] 要么是一个算符("+""-""*""/"),要么是一个在范围 [-200, 200] 内的整数

逆波兰表达式:

逆波兰表达式是一种后缀表达式,所谓后缀就是指算符写在后面。

  • 平常使用的算式则是一种中缀表达式,如 ( 1 + 2 ) * ( 3 + 4 )
  • 该算式的逆波兰表达式写法为 ( ( 1 2 + ) ( 3 4 + ) * )

逆波兰表达式主要有以下两个优点:

  • 去掉括号后表达式无歧义,上式即便写成 1 2 + 3 4 + * 也可以依据次序计算出正确结果。
  • 适合用栈操作运算:遇到数字则入栈;遇到算符则取出栈顶两个数字进行计算,并将结果压入栈中。
class Solution {
    public int evalRPN(String[] tokens) {
        Deque<Integer> dq = new ArrayDeque<>();
        for(String t : tokens){
            if(isnum(t)){
                dq.addLast(Integer.valueOf(t));
                continue;
            }
            int a = dq.pollLast(), b = dq.pollLast();
            switch(t){
                case "+":
                    dq.addLast(a + b);
                    break;
                case "-":
                    dq.addLast(b - a);
                    break;
                case "*":
                    dq.addLast(a * b);
                    break;
                case "/":
                    dq.addLast(b / a);
                    break;
            }
        }
        return dq.pollLast();
    }
	// 判断String是否为数字
    public boolean isnum(String s){
        int p = 0;
        if(s.charAt(0) == '-' && s.length() > 1)
            p = 1; //负数
            
        for(;p < s.length(); p++){
            char c = s.charAt(p);
            if(!(c >= '0' && c <= '9'))
                return false;
        }
        return true;
    }
}

🎉剑指 Offer II 037. 小行星碰撞

难度中等65

给定一个整数数组 asteroids,表示在同一行的小行星。

对于数组中的每一个元素,其绝对值表示小行星的大小,正负表示小行星的移动方向(正表示向右移动,负表示向左移动)。每一颗小行星以相同的速度移动。

找出碰撞后剩下的所有小行星。碰撞规则:两个行星相互碰撞,较小的行星会爆炸。如果两颗行星大小相同,则两颗行星都会爆炸。两颗移动方向相同的行星,永远不会发生碰撞。

示例 1:

输入:asteroids = [5,10,-5]
输出:[5,10]
解释:10 和 -5 碰撞后只剩下 10 。 5 和 10 永远不会发生碰撞。

示例 2:

输入:asteroids = [8,-8]
输出:[]
解释:8 和 -8 碰撞后,两者都发生爆炸。

示例 3:

输入:asteroids = [10,2,-5]
输出:[10]
解释:2 和 -5 发生碰撞后剩下 -5 。10 和 -5 发生碰撞后剩下 10 。

示例 4:

输入:asteroids = [-2,-1,1,2]
输出:[-2,-1,1,2]
解释:-2 和 -1 向左移动,而 1 和 2 向右移动。 由于移动方向相同的行星不会发生碰撞,所以最终没有行星发生碰撞。 

提示:

  • 2 <= asteroids.length <= 104
  • -1000 <= asteroids[i] <= 1000
  • asteroids[i] != 0
class Solution:
    def asteroidCollision(self, asteroids: List[int]) -> List[int]:
        st = []
        for n in asteroids:
            if n > 0: # 大于0 直接入栈
                st.append(n)
            else:
                alive = True # 标记当前元素n是否存活
                # 当前元素存活 并且栈不是空 并且栈顶元素大于0
                while alive and st and st[-1] > 0:
                    # 只有栈顶元素小于当前元素的绝对值,才会存活
                    alive = st[-1] < abs(n)
                    # 栈顶元素爆炸(出栈)
                    if st[-1] <= abs(n):
                        st.pop()
                if alive:
                    st.append(n)
        return st

java

class Solution {
    public int[] asteroidCollision(int[] asteroids) {
        Deque<Integer> dq = new ArrayDeque<>();
        for(int a : asteroids){
            if(a > 0){
                dq.addLast(a);
            }else{
                boolean alive = true;
                while(alive && !dq.isEmpty() && dq.peekLast() > 0){
                    alive = dq.peekLast() < Math.abs(a);
                    if(dq.peekLast() <= Math.abs(a))
                        dq.pollLast();
                }
                if(alive) dq.addLast(a);
            }
        }
        int[] res = new int[dq.size()];
        int i = 0;
        while(!dq.isEmpty())
            res[i++] = dq.pollFirst();
        return res;
    }
}

7.单调栈问题

剑指 Offer II 038. 每日温度

难度中等92

请根据每日 气温 列表 temperatures ,重新生成一个列表,要求其对应位置的输出为:要想观测到更高的气温,至少需要等待的天数。如果气温在这之后都不会升高,请在该位置用 0 来代替。

示例 1:

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

示例 2:

输入: temperatures = [30,40,50,60]
输出: [1,1,1,0]

示例 3:

输入: temperatures = [30,60,90]
输出: [1,1,0]

提示:

  • 1 <= temperatures.length <= 105
  • 30 <= temperatures[i] <= 100
class Solution {
    public int[] dailyTemperatures(int[] temperatures) {
        int n = temperatures.length;
        int[] res = new int[n];
        // 维护一个单调递减栈
        // 当temperatures[i] 大于 dq[-1]位置的元素时,表示找到了 dq[-1]位置 的下一个更高气温
        Deque<Integer> dq = new ArrayDeque<>();
        for(int i = 0; i < n; i++){
            while(!dq.isEmpty() && temperatures[i] > temperatures[dq.peekLast()]){
                int idx = dq.pollLast();
                res[idx] = i - idx;
            }
            dq.addLast(i);
        }
        return res;
    }
}

🎉剑指 Offer II 039. 直方图最大矩形面积【水桶的短板效应】

难度困难96

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

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

示例1:

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

实例2:

输入: heights = [2,4]
输出: 4

题解:https://leetcode.cn/problems/largest-rectangle-in-histogram/solution/bao-li-jie-fa-zhan-by-liweiwei1419/

新木桶原理:木桶能盛多少水不在于短板多短,而是长板多长

class Solution {
    public int largestRectangleArea(int[] heights) {
        int max = 0;
        Deque<Integer> dq = new ArrayDeque<>(); 
        // 维护一个单调递增栈,当 heights[deque.peekLast()] > heights[i]时
        // 说明以 deque.peekLast() 为高的矩形需要弹出了 宽就是 i 到 左边界(栈中下一个元素)的长度
        for(int i = 0; i < heights.length; i++){
            while(!dq.isEmpty() && heights[dq.peekLast()] > heights[i]){
                // 枚举以每个柱形为高度的最大矩形的面积
                int h = dq.pollLast();
                int left = dq.isEmpty() ? -1 : dq.peekLast(); // 空为-1 是方便计算区间[left, right]
                max = Math.max(max, (i - (left+1)) * heights[h]);
            }
            dq.addLast(i);
        }
        // 栈中剩余元素都是单调递增的,依次弹出
        // 左边界是栈中前一个元素,右边界是heights数组的长度
        while(!dq.isEmpty()){
            int h = dq.pollLast();
            int left = dq.isEmpty() ? -1 : dq.peekLast();
            max = Math.max(max, (heights.length - (left + 1)) * heights[h]);
        }
        return max;
    }
}

🎉🎉剑指 Offer II 040. 矩阵中最大的矩形

难度困难79

给定一个由 01 组成的矩阵 matrix ,找出只包含 1 的最大矩形,并返回其面积。

**注意:**此题 matrix 输入格式为一维 01 字符串数组。

示例 1:

输入:matrix = ["10100","10111","11111","10010"]
输出:6
解释:最大矩形如上图所示。

示例 2:

输入:matrix = []
输出:0

示例 3:

输入:matrix = ["0"]
输出:0

示例 4:

输入:matrix = ["1"]
输出:1

示例 5:

输入:matrix = ["00"]
输出:0

提示:

  • rows == matrix.length
  • cols == matrix[0].length
  • 0 <= row, cols <= 200
  • matrix[i][j]'0''1'

这题也可以归约为求解直方图的最大矩形面积。

短板效应的新花样:https://leetcode.cn/problems/PLYXKQ/solution/hua-luo-yue-que-zhi-fang-tu-zui-da-ju-xi-cvaj/

每列的连续 1 的个数可以认为是板子的长度。

统计第 i 行每列向下的板子长度 height,也就是每列从第 i 行开始的连续 1 的个数

然后遍历矩阵的每一行得到相应的 heights 数组就好了,取整个过程中的最大矩形面积。

class Solution {
    public int maximalRectangle(String[] matrix) {
        if(matrix.length == 0) return 0;
        int maxArea = 0;
        int m = matrix.length, n = matrix[0].length();
        int[] heights = new int[n]; 
        // 在遍历每一行的同时更新每列向下的板子长度 heights
        for(String row : matrix){
            for(int i = 0; i < n; i++){
                heights[i] = row.charAt(i) == '0' ? 0 : heights[i] + 1;
            }
            maxArea = Math.max(maxArea, maximalRectangle(heights));
        }
        return maxArea;
    }

    public int maximalRectangle(int[] heights){
        int n = heights.length;
        // 同剑指 Offer II 039. 直方图最大矩形面积
        // 维护一个单调递增栈
        Deque<Integer> dq = new ArrayDeque<>();
        int area = 0;
        for(int i = 0; i < n; i++){
            while(!dq.isEmpty() && heights[dq.peekLast()] > heights[i]){
                int h = dq.pollLast();
                int left = dq.isEmpty() ? -1 : dq.peekLast();
                area = Math.max(area, (i - (left+1)) * heights[h]);
            }
            dq.addLast(i);
        }
        while(!dq.isEmpty()){
            int h = dq.pollLast();
            int left = dq.isEmpty() ? -1 : dq.peekLast();
            area = Math.max(area, (n - (left+1)) * heights[h]);
        }
        return area;
    }
}

剑指 Offer II 041. 滑动窗口的平均值

难度简单96

给定一个整数数据流和一个窗口大小,根据该滑动窗口的大小,计算滑动窗口里所有数字的平均值。

实现 MovingAverage 类:

  • MovingAverage(int size) 用窗口大小 size 初始化对象。
  • double next(int val) 成员函数 next 每次调用的时候都会往滑动窗口增加一个整数,请计算并返回数据流中最后 size 个值的移动平均值,即滑动窗口里所有数字的平均值。

示例:

输入:
inputs = ["MovingAverage", "next", "next", "next", "next"]
inputs = [[3], [1], [10], [3], [5]]
输出:
[null, 1.0, 5.5, 4.66667, 6.0]

解释:
MovingAverage movingAverage = new MovingAverage(3);
movingAverage.next(1); // 返回 1.0 = 1 / 1
movingAverage.next(10); // 返回 5.5 = (1 + 10) / 2
movingAverage.next(3); // 返回 4.66667 = (1 + 10 + 3) / 3
movingAverage.next(5); // 返回 6.0 = (10 + 3 + 5) / 3

提示:

  • 1 <= size <= 1000
  • -105 <= val <= 105
  • 最多调用 next 方法 104
class MovingAverage {
    // 双端队列模拟题,如果队列超出容量大小,则弹出最早加入队列的元素
    Deque<Integer> dq;
    int size, sum;

    /** Initialize your data structure here. */
    public MovingAverage(int size) {
        dq = new ArrayDeque<>();
        this.size = size;
        this.sum = 0;
    }
    
    public double next(int val) {
        dq.addLast(val);
        sum += val;
        if(dq.size() > size){
            sum -= dq.pollFirst();
        }
        return 1.0 * sum / dq.size();
    }
}

剑指 Offer II 042. 最近请求次数

难度简单42

写一个 RecentCounter 类来计算特定时间范围内最近的请求。

请实现 RecentCounter 类:

  • RecentCounter() 初始化计数器,请求数为 0 。
  • int ping(int t) 在时间 t 添加一个新请求,其中 t 表示以毫秒为单位的某个时间,并返回过去 3000 毫秒内发生的所有请求数(包括新请求)。确切地说,返回在 [t-3000, t] 内发生的请求数。

保证 每次对 ping 的调用都使用比之前更大的 t 值。

示例:

输入:
inputs = ["RecentCounter", "ping", "ping", "ping", "ping"]
inputs = [[], [1], [100], [3001], [3002]]
输出:
[null, 1, 2, 3, 3]

解释:
RecentCounter recentCounter = new RecentCounter();
recentCounter.ping(1);     // requests = [1],范围是 [-2999,1],返回 1
recentCounter.ping(100);   // requests = [1, 100],范围是 [-2900,100],返回 2
recentCounter.ping(3001);  // requests = [1, 100, 3001],范围是 [1,3001],返回 3
recentCounter.ping(3002);  // requests = [1, 100, 3001, 3002],范围是 [2,3002],返回 3

提示:

  • 1 <= t <= 109
  • 保证每次对 ping 调用所使用的 t 值都 严格递增
  • 至多调用 ping 方法 104

题解:单调队列

class RecentCounter {

    // 维护一个单增队列,每次元素 t 加入队列,去除比 t-3000 还小的元素
    Deque<Integer> dq;

    public RecentCounter() {
        dq = new ArrayDeque<>();
        dq.addLast(-3000);
    }
    
    public int ping(int t) {
        while(!dq.isEmpty() && dq.peekFirst() < (t-3000))
            dq.pollFirst();
        dq.addLast(t);
        return dq.size();
    }
}

8.二叉树和二叉搜索树问题

剑指 Offer II 043. 往完全二叉树添加节点

难度中等59

完全二叉树是每一层(除最后一层外)都是完全填充(即,节点数达到最大,第 n 层有 2n-1 个节点)的,并且所有的节点都尽可能地集中在左侧。

设计一个用完全二叉树初始化的数据结构 CBTInserter,它支持以下几种操作:

  • CBTInserter(TreeNode root) 使用根节点为 root 的给定树初始化该数据结构;
  • CBTInserter.insert(int v) 向树中插入一个新节点,节点类型为 TreeNode,值为 v 。使树保持完全二叉树的状态,并返回插入的新节点的父节点的值
  • CBTInserter.get_root() 将返回树的根节点。

示例 1:

输入:inputs = ["CBTInserter","insert","get_root"], inputs = [[[1]],[2],[]]
输出:[null,1,[1,2]]

示例 2:

输入:inputs = ["CBTInserter","insert","insert","get_root"], inputs = [[[1,2,3,4,5,6]],[7],[8],[]]
输出:[null,3,4,[1,2,3,4,5,6,7,8]]

提示:

  • 最初给定的树是完全二叉树,且包含 11000 个节点。
  • 每个测试用例最多调用 CBTInserter.insert 操作 10000 次。
  • 给定节点或插入节点的每个值都在 05000 之间。

方法一:使用数组保存序列化二叉树

class CBTInserter {

    // 类似用数组表示的二叉树方法,编号从1开始,list[i] 的父节点是list[i%2]
    List<TreeNode> list = new ArrayList<>();

    public CBTInserter(TreeNode root) {
        list.add(new TreeNode(-1)); // 完全二叉树节点编号从1开始
        Deque<TreeNode> dq = new ArrayDeque<>();
        dq.addLast(root);
        while(!dq.isEmpty()){
            TreeNode cur = dq.pollFirst();
            list.add(cur);
            if(cur.left != null) dq.addLast(cur.left);
            if(cur.right != null) dq.addLast(cur.right);
        }
    }
    
    // 先将当前插入节点挂载到父节点的下面,然后再加入list中
    public int insert(int v) {
        TreeNode node = new TreeNode(v);
        if(list.size() % 2 == 0)
            list.get(list.size() / 2).left = node;
        else
            list.get(list.size() / 2).right = node;
        list.add(node);
        return list.get((list.size()-1) / 2).val;
    }
    
    public TreeNode get_root() {
        return list.size() == 1 ? null : list.get(1);
    }
}

方法二:使用队列,只保存左右子节点不满的节点

class CBTInserter {

    TreeNode root;
    Deque<TreeNode> dq = new ArrayDeque<>();

    public CBTInserter(TreeNode root) {
        this.root = root;
        dq.addLast(root);
        //BFS,只保存左右子树不全的节点
        while(dq.peekFirst().left != null && dq.peekFirst().right != null){
            TreeNode node = dq.pollFirst();
            dq.addLast(node.left);
            dq.addLast(node.right);
        }
    }
    
    // 此时队列中保存的都是子树不全的节点,队首的元素为nums[n/2]
    // 待插入元素的父节点就是队首元素
    // 判断左子树为空则直接挂左子树
    // 否则右子树为空,挂右子树,将该节点弹出,然后将该节点的左右子树加入队列
    public int insert(int v) {
        TreeNode front = dq.peekFirst();
        if(front.left == null){
            front.left = new TreeNode(v);
        }else{
            front.right = new TreeNode(v);
            dq.pollFirst();
            dq.addLast(front.left);
            dq.addLast(front.right);
        }
        return front.val;
    }
    
    public TreeNode get_root() {
        return root;
    }
}

剑指 Offer II 044. 二叉树每层的最大值

难度中等43

给定一棵二叉树的根节点 root ,请找出该二叉树中每一层的最大值。

示例1:

输入: root = [1,3,2,5,3,null,9]
输出: [1,3,9]
解释:
          1
         / \
        3   2
       / \   \  
      5   3   9 

示例2:

输入: root = [1,2,3]
输出: [1,3]
解释:
          1
         / \
        2   3

示例3:

输入: root = [1]
输出: [1]

示例4:

输入: root = [1,null,2]
输出: [1,2]
解释:      
           1 
            \
             2     

示例5:

输入: root = []
输出: []

提示:

  • 二叉树的节点个数的范围是 [0,104]
  • -231 <= Node.val <= 231 - 1

题解:bfs层序遍历

class Solution:
    def largestValues(self, root: TreeNode) -> List[int]:
        ans = []
        if not root: return ans
        q = [root]
        while q:
            tmp = []
            mx = -inf
            for node in q:
                mx = max(mx, node.val)
                if node.left: tmp.append(node.left)
                if node.right: tmp.append(node.right)
            ans.append(mx)
            q = tmp
        return ans

剑指 Offer II 045. 二叉树最底层最左边的值

难度中等43

给定一个二叉树的 根节点 root,请找出该二叉树的 最底层 最左边 节点的值。

假设二叉树中至少有一个节点。

提示:

  • 二叉树的节点个数的范围是 [1,104]
  • -231 <= Node.val <= 231 - 1

方法一:DFS

class Solution {
    int depth;
    boolean find;
    int ans;
    public int findBottomLeftValue(TreeNode root) {
        ans = 0;
        find = false;
        depth = getdepth(root);
        dfs(root, 1);
        return ans;
    }
    public int getdepth(TreeNode node){
        if(node == null) return 0;
        return Math.max(getdepth(node.left), getdepth(node.right)) + 1;
    }

    public void dfs(TreeNode node, int cur_depth){
        if(cur_depth == depth){
            find = true;
            ans = node.val;
            return;
        }
        if(!find && node.left != null)
            dfs(node.left, cur_depth + 1);
        if(!find && node.right != null)
            dfs(node.right, cur_depth + 1);
    }
}

方法二:层序遍历,从右往左层序遍历,最后一个节点值即为所求答案值

剑指 Offer II 046. 二叉树的右侧视图

难度中等45

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

class Solution {
    public List<Integer> rightSideView(TreeNode root) {
        List<Integer> ans = new ArrayList<>();
        Deque<TreeNode> dq = new ArrayDeque<>();
        if(root == null) return ans;
        dq.addLast(root);
        while(!dq.isEmpty()){
            int size = dq.size();
            ans.add(dq.peekFirst().val);
            while(size-- > 0){
                TreeNode cur = dq.pollFirst();
                if(cur.right != null) dq.addLast(cur.right);
                if(cur.left != null) dq.addLast(cur.left);
            }
        }
        return ans;
    }
}

剑指 Offer II 047. 二叉树剪枝

难度中等73

给定一个二叉树 根节点 root ,树的每个节点的值要么是 0,要么是 1。请剪除该二叉树中所有节点的值为 0 的子树。

节点 node 的子树为 node 本身,以及所有 node 的后代。

class Solution {
    // 递 + 归
    // 后序遍历dfs
    // 归的时候如果当前节点左右子树都为空并且节点值为0,则当前节点也可以减去
    // 练习:1080. 根到叶路径上的不足节点
    // https://blog.csdn.net/qq_42958831/article/details/130806614
    public TreeNode pruneTree(TreeNode root) {
        if(root.left == null && root.right == null){
            if(root.val == 0) return null;
            else return root;
        }
        if(root.left != null) 
            root.left = pruneTree(root.left);
        if(root.right != null)
            root.right = pruneTree(root.right);
        return (root.val == 0 && root.left == null && root.right == null) ? null : root;
    }
}

剑指 Offer II 048. 序列化与反序列化二叉树

难度困难75

序列化是将一个数据结构或者对象转换为连续的比特位的操作,进而可以将转换后的数据存储在一个文件或者内存中,同时也可以通过网络传输到另一个计算机环境,采取相反方式重构得到原数据。

请设计一个算法来实现二叉树的序列化与反序列化。这里不限定你的序列 / 反序列化算法执行逻辑,只需要保证一个二叉树可以被序列化为一个字符串并且将这个字符串反序列化为原始的树结构。

提示:

  • 输入输出格式与 LeetCode 目前使用的方式一致,详情请参阅 LeetCode 序列化二叉树的格式。你并非必须采取这种方式,也可以采用其他的方法解决这个问题。
  • 树中结点数在范围 [0, 104]
  • -1000 <= Node.val <= 1000

题解:只要记住层序遍历,初始str值为root节点值,在遍历每个节点时序列化保存它的左孩子和有孩子

public class Codec {

    // Encodes a tree to a single string.
    public String serialize(TreeNode root) {
        if(root == null) return "";
        String str = Integer.toString(root.val);
        Deque<TreeNode> dq = new ArrayDeque<>();
        dq.addLast(root);
        while(!dq.isEmpty()){
            TreeNode cur = dq.pollFirst();
            str += "," + (cur.left == null ? "null" : Integer.toString(cur.left.val));
            str += "," + (cur.right == null ? "null" : Integer.toString(cur.right.val));
            if(cur.left != null) dq.addLast(cur.left);
            if(cur.right != null) dq.addLast(cur.right);
        }
        return str;
    }

    // Decodes your encoded data to tree.
    public TreeNode deserialize(String data) {
        if(data.length() == 0) return null;
        String[] arr = data.split(",");
        if(arr.length == 0) return null;
        TreeNode root = new TreeNode(Integer.valueOf(arr[0]));
        Deque<TreeNode> dq = new ArrayDeque<>();
        dq.addLast(root);
        int idx = 0;
        while(!dq.isEmpty()){
            TreeNode cur = dq.pollFirst();
            cur.left = arr[++idx].equals("null") ? null : new TreeNode(Integer.valueOf(arr[idx]));
            cur.right = arr[++idx].equals("null") ? null : new TreeNode(Integer.valueOf(arr[idx]));
            if(cur.left != null) dq.addLast(cur.left);
            if(cur.right != null) dq.addLast(cur.right);
        }
        return root;
    }
}

剑指 Offer II 049. 从根节点到叶节点的路径数字之和

难度中等52

给定一个二叉树的根节点 root ,树中每个节点都存放有一个 09 之间的数字。

每条从根节点到叶节点的路径都代表一个数字:

  • 例如,从根节点到叶节点的路径 1 -> 2 -> 3 表示数字 123

计算从根节点到叶节点生成的 所有数字之和

叶节点 是指没有子节点的节点。

示例 1:

输入:root = [1,2,3]
输出:25
解释:
从根到叶子节点路径 1->2 代表数字 12
从根到叶子节点路径 1->3 代表数字 13
因此,数字总和 = 12 + 13 = 25

示例 2:

输入:root = [4,9,0,5,1]
输出:1026
解释:
从根到叶子节点路径 4->9->5 代表数字 495
从根到叶子节点路径 4->9->1 代表数字 491
从根到叶子节点路径 4->0 代表数字 40
因此,数字总和 = 495 + 491 + 40 = 1026

提示:

  • 树中节点的数目在范围 [1, 1000]
  • 0 <= Node.val <= 9
  • 树的深度不超过 10
class Solution {
    int res = 0;
    public int sumNumbers(TreeNode root) {
        if(root == null) return 0;
        dfs(root, 0);
        return res;
    }

    public void dfs(TreeNode root, int sum){
        if(root.left == null && root.right == null){
            res += sum * 10 + root.val;
            return;
        }
        if(root.left != null) dfs(root.left, sum * 10 + root.val);
        if(root.right != null) dfs(root.right, sum * 10 + root.val);
    }
}

🎉剑指 Offer II 050. 向下的路径节点之和

难度中等87

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

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

提示:

  • 二叉树的节点个数的范围是 [0,1000]
  • -109 <= Node.val <= 109
  • -1000 <= targetSum <= 1000
class Solution {
    int res = 0;
    public int pathSum(TreeNode root, int targetSum) {
        // 1. 以每个节点作为根节点 判断 路径和 = targetSum
        if(root == null) return 0;
        dfs(root, targetSum);
        return res;
    }
    
    public void dfs(TreeNode node, double targetSum){
        if(node == null) return;
        // 求以node为根节点的路径和
        getPathSum(node, targetSum - node.val);
        dfs(node.left, targetSum);
        dfs(node.right, targetSum);
    }
    // 注意到节点值[-10^9,10^9],用double记录防止溢出
    public void getPathSum(TreeNode node, double sum){
        // 因为节点值有正有负,当sum = 0 时还要递归下去,直到节点为空
        if(sum == 0.0) res += 1;
        if(node.left != null)
            getPathSum(node.left, sum - node.left.val);
        if(node.right != null) 
            getPathSum(node.right, sum - node.right.val);
    }
}

剑指 Offer II 051. 节点之和最大的路径【二叉树的直径问题】

难度困难76

路径 被定义为一条从树中任意节点出发,沿父节点-子节点连接,达到任意节点的序列。同一个节点在一条路径序列中 至多出现一次 。该路径 至少包含一个 节点,且不一定经过根节点。

路径和 是路径中各节点值的总和。

给定一个二叉树的根节点 root ,返回其 最大路径和,即所有路径上节点值之和的最大值。

示例 1:

输入:root = [1,2,3]
输出:6
解释:最优路径是 2 -> 1 -> 3 ,路径和为 2 + 1 + 3 = 6

示例 2:

输入:root = [-10,9,20,null,null,15,7]
输出:42
解释:最优路径是 15 -> 20 -> 7 ,路径和为 15 + 20 + 7 = 42

提示:

  • 树中节点数目范围是 [1, 3 * 104]
  • -1000 <= Node.val <= 1000
class Solution {
    // 求树的最大直径问题
    // 递 过程中返回两个值,左子树的最大路径,右子树的最大路径
    // 归 过程中更新答案,左子树最大路径 + 右子树最大路径
    // 最后返回max(0, 左右子树最大路径 + 1)
    int ans = Integer.MIN_VALUE;
    public int maxPathSum(TreeNode root) {
        if(root == null) return 0;
        dfs(root);
        return ans;
    }

    public int dfs(TreeNode node){
        if(node == null) return 0;
        int left = dfs(node.left);
        int right = dfs(node.right);
        ans = Math.max(ans, left + right + node.val);
        return Math.max(0, Math.max(left, right) + node.val); 
    }
}

剑指 Offer II 052. 展平二叉搜索树

难度简单64

给你一棵二叉搜索树,请 按中序遍历 将其重新排列为一棵递增顺序搜索树,使树中最左边的节点成为树的根节点,并且每个节点没有左子节点,只有一个右子节点。

提示:

  • 树中节点数的取值范围是 [1, 100]
  • 0 <= Node.val <= 1000
class Solution {
    TreeNode ans = null;
    TreeNode cur = ans;
    public TreeNode increasingBST(TreeNode root) {
        dfs(root);
        return ans;
    }
    
    public void dfs(TreeNode node){
        if(node == null) return;
        dfs(node.left);
        if(ans == null){
            ans = new TreeNode(node.val);
            cur = ans;
        }
        else{
            cur.right = new TreeNode(node.val);
            cur = cur.right;
        }
        dfs(node.right);
    }
}

剑指 Offer II 053. 二叉搜索树中的中序后继

难度中等81

给定一棵二叉搜索树和其中的一个节点 p ,找到该节点在树中的中序后继。如果节点没有中序后继,请返回 null

节点 p 的后继是值比 p.val 大的节点中键值最小的节点,即按中序遍历的顺序节点 p 的下一个节点。

提示:

  • 树中节点的数目在范围 [1, 104] 内。
  • -105 <= Node.val <= 105
  • 树中各节点的值均保证唯一。

题解:递归

  1. 如果 root -> val <= p -> val 答案一定在root的右子树
  2. 否则,答案一定在 root 的左子树 或者 就是root
class Solution {
    public TreeNode inorderSuccessor(TreeNode root, TreeNode p) {
        if(root == null) return root;
        if(root.val <= p.val)
            return inorderSuccessor(root.right, p);
        TreeNode left = inorderSuccessor(root.left, p);
        return left == null ? root : left;
    }
}

剑指 Offer II 054. 所有大于等于节点的值之和

难度中等60

给定一个二叉搜索树,请将它的每个节点的值替换成树中大于或者等于该节点值的所有节点值之和。

提醒一下,二叉搜索树满足下列约束条件:

  • 节点的左子树仅包含键 小于 节点键的节点。
  • 节点的右子树仅包含键 大于 节点键的节点。
  • 左右子树也必须是二叉搜索树。

方法一:先求所有节点的和,然后中序遍历剪掉前缀和

class Solution {
    // 中序遍历二叉树 = 遍历有序数组
    int presum = 0, sum = 0; // 记录中序遍历时的前缀和presum 和 所有节点值
    public TreeNode convertBST(TreeNode root) {
        // 1. 获取二叉树所有节点的和
        sum = getsum(root);
        // 2. 修改二叉树的节点值,中序遍历剪掉前缀和
        dfs(root);
        return root;
    }

    public int getsum(TreeNode node){
        if(node == null) return 0;
        return getsum(node.left) + getsum(node.right) + node.val;
    }

    public void dfs(TreeNode node){
        if(node == null) return;
        dfs(node.left);
        int tmp = node.val;
        node.val = sum - presum;
        presum += tmp;
        dfs(node.right);
    }
}

方法二:反中序遍历

class Solution {
    // 反中序遍历就变成了求前缀和问题
    int sum = 0;
    public TreeNode convertBST(TreeNode root) {
        dfs(root);
        return root;
    }

    public void dfs(TreeNode node){
        if(node == null) return;
        dfs(node.right);
        sum += node.val;
        node.val = sum;
        dfs(node.left);
    }
}

剑指 Offer II 055. 二叉搜索树迭代器

难度中等49

实现一个二叉搜索树迭代器类BSTIterator ,表示一个按中序遍历二叉搜索树(BST)的迭代器:

  • BSTIterator(TreeNode root) 初始化 BSTIterator 类的一个对象。BST 的根节点 root 会作为构造函数的一部分给出。指针应初始化为一个不存在于 BST 中的数字,且该数字小于 BST 中的任何元素。
  • boolean hasNext() 如果向指针右侧遍历存在数字,则返回 true ;否则返回 false
  • int next()将指针向右移动,然后返回指针处的数字。

注意,指针初始化为一个不存在于 BST 中的数字,所以对 next() 的首次调用将返回 BST 中的最小元素。

可以假设 next() 调用总是有效的,也就是说,当调用 next() 时,BST 的中序遍历中至少存在一个下一个数字。

示例:

输入
inputs = ["BSTIterator", "next", "next", "hasNext", "next", "hasNext", "next", "hasNext", "next", "hasNext"]
inputs = [[[7, 3, 15, null, null, 9, 20]], [], [], [], [], [], [], [], [], []]
输出
[null, 3, 7, true, 9, true, 15, true, 20, false]

解释
BSTIterator bSTIterator = new BSTIterator([7, 3, 15, null, null, 9, 20]);
bSTIterator.next();    // 返回 3
bSTIterator.next();    // 返回 7
bSTIterator.hasNext(); // 返回 True
bSTIterator.next();    // 返回 9
bSTIterator.hasNext(); // 返回 True
bSTIterator.next();    // 返回 15
bSTIterator.hasNext(); // 返回 True
bSTIterator.next();    // 返回 20
bSTIterator.hasNext(); // 返回 False

提示:

  • 树中节点的数目在范围 [1, 105]
  • 0 <= Node.val <= 106
  • 最多调用 105hasNextnext 操作

进阶:

  • 你可以设计一个满足下述条件的解决方案吗?next()hasNext() 操作均摊时间复杂度为 O(1) ,并使用 O(h) 内存。其中 h 是树的高度。
class BSTIterator {

    List<Integer> list;
    int idx;

    public BSTIterator(TreeNode root) {
        idx = 0;
        list = new ArrayList<>();
        dfs(root);
    }
    
    public int next() {
        return list.get(idx++);
    }
    
    public boolean hasNext() {
        return idx < list.size();
    }

    public void dfs(TreeNode node){
        if(node == null) return;
        dfs(node.left);
        list.add(node.val);
        dfs(node.right);
    }
}

剑指 Offer II 056. 二叉搜索树中两个节点之和

难度简单63

给定一个二叉搜索树的 根节点 root 和一个整数 k , 请判断该二叉搜索树中是否存在两个节点它们的值之和等于 k 。假设二叉搜索树中节点的值均唯一。

示例 1:

输入: root = [8,6,10,5,7,9,11], k = 12
输出: true
解释: 节点 5 和节点 7 之和等于 12

示例 2:

输入: root = [8,6,10,5,7,9,11], k = 22
输出: false
解释: 不存在两个节点值之和为 22 的节点

提示:

  • 二叉树的节点个数的范围是 [1, 104].
  • -104 <= Node.val <= 104
  • root 为二叉搜索树
  • -105 <= k <= 105

方法一:两数之和二叉搜素树版

class Solution {
    Set<Integer> set;
    int k;
    boolean res;
    public boolean findTarget(TreeNode root, int k) {
        this.k = k;
        res = false;
        set = new HashSet<>();
        dfs(root);
        return res;
    }

    public void dfs(TreeNode node){
        if(node == null) return;
        dfs(node.left);
        if(set.contains(k - node.val))
            res = true;
        set.add(node.val);
        dfs(node.right);
    }
}

🎉剑指 Offer II 057. 值和下标之差都在给定的范围内

难度中等83

给你一个整数数组 nums 和两个整数 kt 。请你判断是否存在 两个不同下标 ij,使得 abs(nums[i] - nums[j]) <= t ,同时又满足 abs(i - j) <= k

如果存在则返回 true,不存在返回 false

示例 1:

输入:nums = [1,2,3,1], k = 3, t = 0
输出:true

示例 2:

输入:nums = [1,0,1,1], k = 1, t = 2
输出:true

示例 3:

输入:nums = [1,5,9,1,5,9], k = 2, t = 3
输出:false

提示:

  • 0 <= nums.length <= 2 * 104
  • -231 <= nums[i] <= 231 - 1
  • 0 <= k <= 104
  • 0 <= t <= 231 - 1
class Solution {
    public boolean containsNearbyAlmostDuplicate(int[] nums, int k, int t) {
        TreeSet<Integer> set = new TreeSet<>();
        for(int i = 0; i < nums.length; i++){
            Integer cur = nums[i];
            // 寻找 <= cur 的最大数 和 >= cur 的最小数
            Integer l = set.floor(cur);
            Integer r = set.ceiling(cur);
            if(l != null && (long) cur - l <= t) return true;
            if(r != null && (long) r - cur <= t) return true;
            set.add(cur);
            if(i >= k) set.remove(nums[i-k]);
        }
        return false;
    }
}

9.一些数据结构题(线段树、单调栈、字典树)

剑指 Offer II 058. 日程表

难度中等56

请实现一个 MyCalendar 类来存放你的日程安排。如果要添加的时间内没有其他安排,则可以存储这个新的日程安排。

MyCalendar 有一个 book(int start, int end)方法。它意味着在 start 到 end 时间内增加一个日程安排,注意,这里的时间是半开区间,即 [start, end), 实数 x 的范围为, start <= x < end

当两个日程安排有一些时间上的交叉时(例如两个日程安排都在同一时间内),就会产生重复预订。

每次调用 MyCalendar.book方法时,如果可以将日程安排成功添加到日历中而不会导致重复预订,返回 true。否则,返回 false 并且不要将该日程安排添加到日历中。

请按照以下步骤调用 MyCalendar 类: MyCalendar cal = new MyCalendar(); MyCalendar.book(start, end)

示例:

输入:
["MyCalendar","book","book","book"]
[[],[10,20],[15,25],[20,30]]
输出: [null,true,false,true]
解释: 
MyCalendar myCalendar = new MyCalendar();
MyCalendar.book(10, 20); // returns true 
MyCalendar.book(15, 25); // returns false ,第二个日程安排不能添加到日历中,因为时间 15 已经被第一个日程安排预定了
MyCalendar.book(20, 30); // returns true ,第三个日程安排可以添加到日历中,因为第一个日程安排并不包含时间 20 

提示:

  • 每个测试用例,调用 MyCalendar.book 函数最多不超过 1000次。
  • 0 <= start < end <= 109

题解:线段树

class MyCalendar {

    public MyCalendar() {
    }
    
    public boolean book(int start, int end) {
        // 注意,这里的时间是半开区间,即 [start, end), 实数 x 的范围为,  start <= x < end。
        int val = query(root, 0, N, start, end-1);
        if(val > 0) return false;
        update(root, 0, N, start, end-1, 1);
        return true;
    }

    class Node{
        int val, add;
        Node left, right;
    }
    private int N = (int)1e9;
    private Node root = new Node();
    
    public void update(Node node, int start, int end, int l, int r, int val){
        if(l <= start && end <= r){
            node.val += val;
            node.add += val;
            return;
        }
        int mid = (start + end) >> 1;
        pushDown(node);
        if(l <= mid) update(node.left, start, mid, l, r, val);
        if(mid < r) update(node.right, mid+1, end, l, r, val);
        pushUp(node);
    }

    public int query(Node node, int start, int end, int l, int r){
        if(l <= start && end <= r){
            return node.val;
        }
        int mid = (start + end) >> 1;
        pushDown(node);
        int ans = 0;
        if(l <= mid) ans += query(node.left, start, mid, l, r);
        if(mid < r) ans += query(node.right, mid+1, end, l, r);
        return ans; 
    }

    public void pushUp(Node node){
        node.val = node.left.val + node.right.val;
    }

    public void pushDown(Node node){
        if(node.left == null) node.left = new Node();
        if(node.right == null) node.right = new Node();
        if(node.add == 0) return;
        node.left.val += node.add;
        node.right.val += node.add;
        node.left.add += node.add;
        node.right.add += node.add;
        node.add = 0;
    }

}

剑指 Offer II 059. 数据流的第 K 大数值

难度简单47

设计一个找到数据流中第 k 大元素的类(class)。注意是排序后的第 k 大元素,不是第 k 个不同的元素。

请实现 KthLargest 类:

  • KthLargest(int k, int[] nums) 使用整数 k 和整数流 nums 初始化对象。
  • int add(int val)val 插入数据流 nums 后,返回当前数据流中第 k 大的元素。

示例:

输入:
["KthLargest", "add", "add", "add", "add", "add"]
[[3, [4, 5, 8, 2]], [3], [5], [10], [9], [4]]
输出:
[null, 4, 5, 5, 8, 8]

解释:
KthLargest kthLargest = new KthLargest(3, [4, 5, 8, 2]);
kthLargest.add(3);   // return 4
kthLargest.add(5);   // return 5
kthLargest.add(10);  // return 5
kthLargest.add(9);   // return 8
kthLargest.add(4);   // return 8

提示:

  • 1 <= k <= 104
  • 0 <= nums.length <= 104
  • -104 <= nums[i] <= 104
  • -104 <= val <= 104
  • 最多调用 add 方法 104
  • 题目数据保证,在查找第 k 大元素时,数组中至少有 k 个元素
class KthLargest {

    // 第k大,维护一个小根堆
    PriorityQueue<Integer> pq;
    int size;

    public KthLargest(int k, int[] nums) {
        pq = new PriorityQueue<>();
        this.size = k;
        for(int num : nums){
            // 注意不是pq.add(num),在这里卡了半个小时
            add(num);
        };
    }
    
    public int add(int val) {
        if(pq.size() < size)
            pq.add(val);
        else{
            if(pq.peek() < val){
                pq.poll();
                pq.add(val);
            }
        }
        return pq.peek();
    }
}

剑指 Offer II 060. 出现频率最高的 k 个数字

难度中等58

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

示例 1:

输入: nums = [1,1,1,2,2,3], k = 2
输出: [1,2]

示例 2:

输入: nums = [1], k = 1
输出: [1]

提示:

  • 1 <= nums.length <= 105
  • k 的取值范围是 [1, 数组中不相同的元素的个数]
  • 题目数据保证答案唯一,换句话说,数组中前 k 个高频元素的集合是唯一的

方法一:哈希表 + 优先队列

class Solution {
    public int[] topKFrequent(int[] nums, int k) {
        // map统计每个元素出现的频率
        Map<Integer, Integer> map = new HashMap<>();
        for(int num : nums){
            map.put(num, map.getOrDefault(num, 0) + 1);
        }
        // 前k大,用小根堆,按照出现次数排序
        PriorityQueue<Integer> pq = new PriorityQueue<>((a, b) -> map.get(a) - map.get(b));
        for(Map.Entry<Integer, Integer> entry : map.entrySet()){
            if(pq.size() < k){
                pq.add(entry.getKey());
            }else{
                if(map.get(pq.peek()) < entry.getValue()){
                    pq.poll();
                    pq.add(entry.getKey());
                }
            }
        }
        int[] ans = new int[k];
        for(int i = 0; i < k; i++){
            ans[i] = pq.poll();
        }
        return ans;
    }
}

🎉剑指 Offer II 061. 和最小的 k 个数对

难度中等78

给定两个以升序排列的整数数组 nums1nums2 , 以及一个整数 k

定义一对值 (u,v),其中第一个元素来自 nums1,第二个元素来自 nums2

请找到和最小的 k 个数对 (u1,v1), (u2,v2)(uk,vk)

示例 1:

输入: nums1 = [1,7,11], nums2 = [2,4,6], k = 3
输出: [1,2],[1,4],[1,6]
解释: 返回序列中的前 3 对数:
    [1,2],[1,4],[1,6],[7,2],[7,4],[11,2],[7,6],[11,4],[11,6]

示例 2:

输入: nums1 = [1,1,2], nums2 = [1,2,3], k = 2
输出: [1,1],[1,1]
解释: 返回序列中的前 2 对数:
     [1,1],[1,1],[1,2],[2,1],[1,2],[2,2],[1,3],[1,3],[2,3]

示例 3:

输入: nums1 = [1,2], nums2 = [3], k = 3 
输出: [1,3],[2,3]
解释: 也可能序列中所有的数对都被返回:[1,3],[2,3]

提示:

  • 1 <= nums1.length, nums2.length <= 104
  • -109 <= nums1[i], nums2[i] <= 109
  • nums1, nums2 均为升序排列
  • 1 <= k <= 1000
class Solution {
    public List<List<Integer>> kSmallestPairs(int[] nums1, int[] nums2, int k) {
        List<List<Integer>> ans = new ArrayList<>();
        // 维护一个小顶堆
        PriorityQueue<int[]> pq = new PriorityQueue<>((a, b) -> 
                            (nums1[a[0]] + nums2[a[1]]) - (nums1[b[0]] + nums2[b[1]]));
        // 先存放第一个数组的所有元素与第二个数组首元素的集合
        for(int i = 0; i < nums1.length; i++){
            pq.add(new int[]{i, 0});
        }
        // 多路归并
        while(ans.size() < k && !pq.isEmpty()){
            // 取出最小的和,并根据对应的'路'找到对应的下一个元素放入队列。
            int[] p = pq.poll(); 
            ans.add(List.of(nums1[p[0]], nums2[p[1]]));
            if(p[1] < nums2.length-1){
                pq.add(new int[]{p[0], p[1] + 1});
            }
        }
        return ans;
    }
}

剑指 Offer II 062. 实现前缀树

难度中等52

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

请你实现 Trie 类:

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

示例:

输入
inputs = ["Trie", "insert", "search", "search", "startsWith", "insert", "search"]
inputs = [[], ["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 <= word.length, prefix.length <= 2000
  • wordprefix 仅由小写英文字母组成
  • insertsearchstartsWith 调用次数 总计 不超过 3 * 104
class Trie {

    class TrieNode{//字典树的结点数据结构
		boolean end;//是否是单词末尾的标识
		TrieNode[] child; //26个小写字母的拖尾
		public TrieNode(){
			end = false;
			child = new TrieNode[26];
		}
	}

    TrieNode root;

    /** Initialize your data structure here. */
    public Trie() {
        root = new TrieNode();
    }
    
    /** Inserts a word into the trie. */
    public void insert(String word) {
        TrieNode p = root;
        for(int i = 0; i < word.length(); i++){
            int u = word.charAt(i) - 'a';
            if(p.child[u] == null) p.child[u] = new TrieNode();
            p = p.child[u];
        }
        p.end = true;
    }
    
    /** Returns if the word is in the trie. */
    public boolean search(String word) {
        TrieNode p = root;
        for(int i = 0; i < word.length(); i++){
            int u = word.charAt(i) - 'a';
            if(p.child[u] == null) return false;
            p = p.child[u];
        }
        return p.end;
    }
    
    /** Returns if there is any word in the trie that starts with the given prefix. */
    public boolean startsWith(String prefix) {
        TrieNode p = root;
        for(int i = 0; i < prefix.length(); i++){
            int u = prefix.charAt(i) - 'a';
            if(p.child[u] == null) return false;
            p = p.child[u];
        }
        return true;
    }
}

剑指 Offer II 063. 替换单词

难度中等40

在英语中,有一个叫做 词根(root) 的概念,它可以跟着其他一些词组成另一个较长的单词——我们称这个词为 继承词(successor)。例如,词根an,跟随着单词 other(其他),可以形成新的单词 another(另一个)。

现在,给定一个由许多词根组成的词典和一个句子,需要将句子中的所有继承词词根替换掉。如果继承词有许多可以形成它的词根,则用最短的词根替换它。

需要输出替换之后的句子。

示例 1:

输入:dictionary = ["cat","bat","rat"], sentence = "the cattle was rattled by the battery"
输出:"the cat was rat by the bat"

示例 2:

输入:dictionary = ["a","b","c"], sentence = "aadsfasf absbs bbab cadsfafs"
输出:"a a b c"

示例 3:

输入:dictionary = ["a", "aa", "aaa", "aaaa"], sentence = "a aa a aaaa aaa aaa aaa aaaaaa bbb baba ababa"
输出:"a a a a a a a a bbb baba a"

示例 4:

输入:dictionary = ["catt","cat","bat","rat"], sentence = "the cattle was rattled by the battery"
输出:"the cat was rat by the bat"

示例 5:

输入:dictionary = ["ac","ab"], sentence = "it is abnormal that this solution is accepted"
输出:"it is ab that this solution is ac"

提示:

  • 1 <= dictionary.length <= 1000
  • 1 <= dictionary[i].length <= 100
  • dictionary[i] 仅由小写字母组成。
  • 1 <= sentence.length <= 10^6
  • sentence 仅由小写字母和空格组成。
  • sentence 中单词的总量在范围 [1, 1000] 内。
  • sentence 中每个单词的长度在范围 [1, 1000] 内。
  • sentence 中单词之间由一个空格隔开。
  • sentence 没有前导或尾随空格。

题解:search时查看有没有到end

class Solution {
    public String replaceWords(List<String> dictionary, String sentence) {
        StringBuilder sb = new StringBuilder();
        Trie trie = new Trie();
        for(String dict : dictionary){
            trie.insert(dict);
        }
        for(String s : sentence.split(" ")){
            sb.append(trie.search(s));
            sb.append(" ");
        }
        return sb.toString().trim();
    }
}

class Trie {
    class TrieNode{//字典树的结点数据结构
		boolean end;//是否是单词末尾的标识
		int pass; // 经过这个结点的次数(根据需要设置这个变量)
		TrieNode[] child; //26个小写字母的拖尾
		public TrieNode(){
			end = false;
			pass = 0;
			child = new TrieNode[26];
		}
	}

	TrieNode root;//字典树的根节点。
	
    public Trie() {
        root = new TrieNode();
    }

    public void insert(String s) {
        TrieNode p = root;
        for(int i = 0; i < s.length(); i++) {
            int u = s.charAt(i) - 'a';
			//若当前结点下没有找到要的字母,则新开结点继续插入
            if (p.child[u] == null) p.child[u] = new TrieNode();
            p = p.child[u]; 
            p.pass++;
        }
        p.end = true;
    }

    public String search(String s) {
        TrieNode p = root;
        for(int i = 0; i < s.length(); i++) {
            int u = s.charAt(i) - 'a';
            if(p.end) return s.substring(0, i);
            if(p.child[u] == null) return s;
            p = p.child[u]; 
        }
        return s;
    }

    public boolean startsWith(String s) {
        TrieNode p = root;
        for(int i = 0; i < s.length(); i++) {
            int u = s.charAt(i) - 'a';
            if (p.child[u] == null) return false;
            p = p.child[u]; 
        }
        return true;
    }
}

剑指 Offer II 064. 神奇的字典

难度中等49

设计一个使用单词列表进行初始化的数据结构,单词列表中的单词 互不相同 。 如果给出一个单词,请判定能否只将这个单词中一个字母换成另一个字母,使得所形成的新单词存在于已构建的神奇字典中。

实现 MagicDictionary 类:

  • MagicDictionary() 初始化对象
  • void buildDict(String[] dictionary) 使用字符串数组 dictionary 设定该数据结构,dictionary 中的字符串互不相同
  • bool search(String searchWord) 给定一个字符串 searchWord ,判定能否只将字符串中 一个 字母换成另一个字母,使得所形成的新字符串能够与字典中的任一字符串匹配。如果可以,返回 true ;否则,返回 false

示例:

输入
inputs = ["MagicDictionary", "buildDict", "search", "search", "search", "search"]
inputs = [[], [["hello", "leetcode"]], ["hello"], ["hhllo"], ["hell"], ["leetcoded"]]
输出
[null, null, false, true, false, false]

解释
MagicDictionary magicDictionary = new MagicDictionary();
magicDictionary.buildDict(["hello", "leetcode"]);
magicDictionary.search("hello"); // 返回 False
magicDictionary.search("hhllo"); // 将第二个 'h' 替换为 'e' 可以匹配 "hello" ,所以返回 True
magicDictionary.search("hell"); // 返回 False
magicDictionary.search("leetcoded"); // 返回 False

提示:

  • 1 <= dictionary.length <= 100
  • 1 <= dictionary[i].length <= 100
  • dictionary[i] 仅由小写英文字母组成
  • dictionary 中的所有字符串 互不相同
  • 1 <= searchWord.length <= 100
  • searchWord 仅由小写英文字母组成
  • buildDict 仅在 search 之前调用一次
  • 最多调用 100search
class MagicDictionary {

    Trie trie;

    public MagicDictionary() {
        trie = new Trie();
    }
    
    public void buildDict(String[] dictionary) {
        for(String d : dictionary)
            trie.insert(d);
    }
    
    public boolean search(String searchWord) {
        TrieNode cur = trie.root;
        return trie.dfs(cur, searchWord, 0, 0);
    }
}

class TrieNode{//字典树的结点数据结构
    boolean end;//是否是单词末尾的标识
    TrieNode[] child; //26个小写字母的拖尾
    public TrieNode(){
        end = false;
        child = new TrieNode[26];
    }
}

class Trie {

	TrieNode root;//字典树的根节点。
	
    public Trie() {
        root = new TrieNode();
    }

    public void insert(String s) {
        TrieNode p = root;
        for(int i = 0; i < s.length(); i++) {
            int u = s.charAt(i) - 'a';
			//若当前结点下没有找到要的字母,则新开结点继续插入
            if (p.child[u] == null) p.child[u] = new TrieNode();
            p = p.child[u]; 
        }
        p.end = true;
    }

    public boolean dfs(TrieNode cur, String word, int idx, int cnt) {
        if(idx == word.length()){
            if(cnt == 1 && cur.end) return true;
            else return false;
        }
        // 遍历cur节点的所有子树
        for(int i = 0; i < 26; i++){
            if(cur.child[i] == null) continue;
            if(word.charAt(idx)-'a' == i){ // 与word[idx]字母相同,不用替换
                if(dfs(cur.child[i], word, idx+1, cnt)) 
                return true;
            }else if(cnt == 0 && (word.charAt(idx)-'a' != i)){
                if(dfs(cur.child[i], word, idx+1, cnt+1))
                    return true;
            }
        }
        return false;
    }
}

剑指 Offer II 065. 最短的单词编码

难度中等44

单词数组 words有效编码 由任意助记字符串 s 和下标数组 indices 组成,且满足:

  • words.length == indices.length
  • 助记字符串 s'#' 字符结尾
  • 对于每个下标 indices[i]s 的一个从 indices[i] 开始、到下一个 '#' 字符结束(但不包括 '#')的 子字符串 恰好与 words[i] 相等

给定一个单词数组 words ,返回成功对 words 进行编码的最小助记字符串 s 的长度 。

示例 1:

输入:words = ["time", "me", "bell"]
输出:10
解释:一组有效编码为 s = "time#bell#" 和 indices = [0, 2, 5] 。
words[0] = "time" ,s 开始于 indices[0] = 0 到下一个 '#' 结束的子字符串,如加粗部分所示 "time#bell#"
words[1] = "me" ,s 开始于 indices[1] = 2 到下一个 '#' 结束的子字符串,如加粗部分所示 "time#bell#"
words[2] = "bell" ,s 开始于 indices[2] = 5 到下一个 '#' 结束的子字符串,如加粗部分所示 "time#bell#"

示例 2:

输入:words = ["t"]
输出:2
解释:一组有效编码为 s = "t#" 和 indices = [0] 。

提示:

  • 1 <= words.length <= 2000
  • 1 <= words[i].length <= 7
  • words[i] 仅由小写字母组成
class Solution {
    // 将words中的字符串都逆序,然后按字典序从大到小排序
    // 遍历words数组,判断是否存在以words[i]为前缀的字符串,存在则跳过,不存在则插入字典树并计算答案
    public int minimumLengthEncoding(String[] words) {
        Trie trie = new Trie();
        for(int i = 0; i < words.length; i++){
            words[i] = new StringBuilder(words[i]).reverse().toString();
        }
        Arrays.sort(words, (a, b) -> b.compareTo(a)); // 按照字典序从大到小排序
        int ans = 0;
        for(String word : words){
            if(trie.startsWith(word)) continue;
            trie.insert(word);
            ans += word.length() + 1;
        }
        return ans;
    }
}

class Trie {
    class TrieNode{//字典树的结点数据结构
		boolean end;//是否是单词末尾的标识
		int pass; // 经过这个结点的次数(根据需要设置这个变量)
		TrieNode[] child; //26个小写字母的拖尾
		public TrieNode(){
			end = false;
			pass = 0;
			child = new TrieNode[26];
		}
	}

	TrieNode root;//字典树的根节点。
	
    public Trie() {
        root = new TrieNode();
    }

    public void insert(String s) {
        TrieNode p = root;
        for(int i = 0; i < s.length(); i++) {
            int u = s.charAt(i) - 'a';
			//若当前结点下没有找到要的字母,则新开结点继续插入
            if (p.child[u] == null) p.child[u] = new TrieNode();
            p = p.child[u]; 
            p.pass++;
        }
        p.end = true;
    }

    public boolean search(String s) {
        TrieNode p = root;
        for(int i = 0; i < s.length(); i++) {
            int u = s.charAt(i) - 'a';
            if (p.child[u] == null) return false;//变化点(根据题意)
            p = p.child[u]; 
        }
        return p.end;
    }

    public boolean startsWith(String s) {
        TrieNode p = root;
        for(int i = 0; i < s.length(); i++) {
            int u = s.charAt(i) - 'a';
            if (p.child[u] == null) return false;
            p = p.child[u]; 
        }
        return true;
    }
}

剑指 Offer II 066. 单词之和

难度中等28

实现一个 MapSum 类,支持两个方法,insertsum

  • MapSum() 初始化 MapSum 对象
  • void insert(String key, int val) 插入 key-val 键值对,字符串表示键 key ,整数表示值 val 。如果键 key 已经存在,那么原来的键值对将被替代成新的键值对。
  • int sum(string prefix) 返回所有以该前缀 prefix 开头的键 key 的值的总和。

示例:

输入:
inputs = ["MapSum", "insert", "sum", "insert", "sum"]
inputs = [[], ["apple", 3], ["ap"], ["app", 2], ["ap"]]
输出:
[null, null, 3, null, 5]

解释:
MapSum mapSum = new MapSum();
mapSum.insert("apple", 3);  
mapSum.sum("ap");           // return 3 (apple = 3)
mapSum.insert("app", 2);    
mapSum.sum("ap");           // return 5 (apple + app = 3 + 2 = 5)

提示:

  • 1 <= key.length, prefix.length <= 50
  • keyprefix 仅由小写英文字母组成
  • 1 <= val <= 1000
  • 最多调用 50insertsum
class MapSum {
    // 用一个map保存已经插入的键值对,使用字典树保存插入的字符串,插入字符串时,更新路径和
    // 插入字符串时先进行判断,如果当前key已经插入过了,再次插入时值需要更新为对应的变化量 v2-v1
    // 求以prefix为前缀的路径和时直接返回p.pass

    Trie trie;
    Map<String, Integer> map = new HashMap<>();

    /** Initialize your data structure here. */
    public MapSum() {
        trie = new Trie();
    }
    
    public void insert(String key, int val) {
        if(map.containsKey(key)){
            // 如果键key已经存在,那么再次插入时插入对应的变化量 v2 - v1
            trie.insert(key, val - map.get(key));
        }else{
            trie.insert(key, val);
        }
        map.put(key, val);
    }
    
    public int sum(String prefix) {
        return trie.startsWith(prefix);
    }
}

class Trie {
    class TrieNode{//字典树的结点数据结构
		boolean end;//是否是单词末尾的标识
		int pass; // 经过这个节点的值的和
		TrieNode[] child; //26个小写字母的拖尾
		public TrieNode(){
			end = false;
			pass = 0;
			child = new TrieNode[26];
		}
	}

	TrieNode root;//字典树的根节点。
	
    public Trie() {
        root = new TrieNode();
    }

    public void insert(String s, int val) {
        TrieNode p = root;
        for(int i = 0; i < s.length(); i++) {
            int u = s.charAt(i) - 'a';
			//若当前结点下没有找到要的字母,则新开结点继续插入
            if (p.child[u] == null) p.child[u] = new TrieNode();
            p = p.child[u]; 
            p.pass += val;
        }
        p.end = true;
    }

    public boolean search(String s) {
        TrieNode p = root;
        for(int i = 0; i < s.length(); i++) {
            int u = s.charAt(i) - 'a';
            if (p.child[u] == null) return false;//变化点(根据题意)
            p = p.child[u]; 
        }
        return p.end;
    }

    public int startsWith(String s) {
        TrieNode p = root;
        int sum = 0;
        for(int i = 0; i < s.length(); i++) {
            int u = s.charAt(i) - 'a';
            // 不存在以 s 为前缀的字符串
            if (p.child[u] == null) return 0;
            p = p.child[u]; 
        }
        return p.pass;
    }
}

🎉剑指 Offer II 067. 最大的异或

难度中等74

给你一个整数数组 nums ,返回 nums[i] XOR nums[j] 的最大运算结果,其中 0 ≤ i ≤ j < n

示例 1:

输入:nums = [3,10,5,25,2,8]
输出:28
解释:最大运算结果是 5 XOR 25 = 28.

示例 2:

输入:nums = [14,70,53,83,49,91,36,80,92,51,66,70]
输出:127

提示:

  • 1 <= nums.length <= 2 * 105
  • 0 <= nums[i] <= 231 - 1

题解:yukiyama

【前缀树+贪心思想,102ms,击败76%】

非常好的一道前缀树非典型应用题。顺便也利用这题复习一下位运算。另外针对官方题解的前缀树代码,我提一点建议,即让search方法(官解中的check方法)返回匹配num的具有最多相反位的数,而不是它们异或的结果。毕竟还是要松耦合,让代码看起来更合理流畅。

【讲解】

将num转换为二进制数来思考,要求num1 ^ num2尽量大,即希望num1和num2相反的对应位尽可能多。遍历nums,对当前num寻找与他对应相反位最多的数字求异或。因为是沿着位搜索,这引导我们采用前缀树处理。在本问题中前缀树为二叉树,每个结点具有两个儿子结点,分别表示0和1。遍历nums,将对每一个num执行前缀树的insert操作,完成前缀树的构建。然后再遍历一次nums,对每一个num,查找「与num对应相反位」最多的前缀。与常规前缀树search方法不同的是,本题中要沿着0-1相反的路径搜索。没有相反结点时才沿着唯一结点前进。每次search完更新最大值max。

实际上我们可以一边insert一遍search来完成max的更新,当insert结束时即完成求解。这实际上是贪心思想的体现,此题「贪心」的正确性可以这么理解:假设x ^ y为答案,且x在数组中的位置位于y之前,由于我们一边把数字存入前缀树中,一边更新最大值max,那么在将y存入字典树时,必然会找到x(x必然已在树中)来更新当前的max,此时就会取得max。

  • 时间复杂度:O(31*n),insert和search一个数需要的步数都是固定的31步(for中的30到0),因此插入n个数时间复杂度为 O(31 * n),搜索一个数的时间复杂度为O(31)。
  • 空间复杂度:O(31 * 2 * n),n个数,每个数在前缀树上至多需要31*2来表示。说「至多」是因为,若两个数字的二进制表示的有相同前缀,则前缀部分是「复用」的。
class Solution {
    public int findMaximumXOR(int[] nums) {
        Trie trie = new Trie();
        int ans = 0; // 两个非负数的异或必为非负数
        for(int num : nums){
            trie.insert(num);
            ans = Math.max(ans, trie.search(num) ^ num);
        }
        return ans;
    }
}

class Trie {
    class TrieNode{//字典树的结点数据结构
		TrieNode[] child; // 二进制表示拖尾,只有 0 和 1 
		public TrieNode(){
			child = new TrieNode[2];
		}
	}

	TrieNode root;//字典树的根节点
	
    public Trie() {
        root = new TrieNode();
    }

    public void insert(int num) {
        TrieNode p = root;
        for(int i = 30; i >= 0; i--){ // 题目范围为非负数,高31位移动到低1位只要右移30位
            int bit = (num >> i) & 1;
            if(p.child[bit] == null){
                p.child[bit] = new TrieNode();
            }
            p = p.child[bit];
        }
    }

    // 返回当前前缀树中与num做异或能够取得最大值的数字。取出后再在外部做异或运算。
    public int search(int num) {
        TrieNode p = root;
        int ans = 0;
        for(int i = 30; i >= 0; i--){
            int bit = (num >> i) & 1; // 取得第 i 位 (0或1)
            // 与bit相反(指0-1相反)的节点若不存在,bit不变,若存在,取相反
            bit = p.child[bit ^ 1] == null ? bit : bit ^ 1; 
            ans += bit << i; // 累计ans
            p = p.child[bit];
        }
        return ans;
    }
}

/*
    本题中心思想:要找出异或最大的数,就要找出,从最高位开始尽量和x二进位相反的节点
    举例介绍:  解释int getVal(int x)方法
    在x=5之前的数为(1,3,5)
    建立了一个这样的树
        0              1
     0     1         0 
       1     1         1
    当x=5(即101)时
    1. 第一层选择0(与x的第一位不同)             对应代码if(p.ns[b] != null)
    2. 第二层选择1(与x的第二位不同),指向右子树  对应代码if(p.ns[b] != null)
    3. 第三层只能选择1(因为只有1),指向右子树    对应代码else
    最后选择的时011(即3),此时最大值3^5=6
*/

10.二分查找问题

剑指 Offer II 068. 查找插入位置

难度简单47

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

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

示例 1:

输入: nums = [1,3,5,6], target = 5
输出: 2

示例 2:

输入: nums = [1,3,5,6], target = 2
输出: 1

提示:

  • 1 <= nums.length <= 104
  • -104 <= nums[i] <= 104
  • nums无重复元素升序排列数组
  • -104 <= target <= 104
class Solution {
    // 返回小于 target 的第一个下标
    public int searchInsert(int[] nums, int target) {
        int left = 0, right = nums.length;
        while(left < right){
            int mid = (left + right) >> 1;
            if(nums[mid] < target) left = mid + 1;
            else right = mid;
        }
        return right;
    }
}

剑指 Offer II 069. 山峰数组的顶部

难度简单110

符合下列属性的数组 arr 称为 山峰数组山脉数组)

  • arr.length >= 3
  • 存在i(0 < i < arr.length - 1)使得:
    • arr[0] < arr[1] < ... arr[i-1] < arr[i]
    • arr[i] > arr[i+1] > ... > arr[arr.length - 1]

给定由整数组成的山峰数组 arr ,返回任何满足 arr[0] < arr[1] < ... arr[i - 1] < arr[i] > arr[i + 1] > ... > arr[arr.length - 1] 的下标 i ,即山峰顶部。

示例 1:

输入:arr = [0,1,0]
输出:1

示例 2:

输入:arr = [1,3,5,4,2]
输出:2

示例 3:

输入:arr = [0,10,5,2]
输出:1

示例 4:

输入:arr = [3,4,5,1]
输出:2

示例 5:

输入:arr = [24,69,100,99,79,78,67,36,26,19]
输出:2

提示:

  • 3 <= arr.length <= 104
  • 0 <= arr[i] <= 106
  • 题目数据保证 arr 是一个山脉数组
class Solution {
    public int peakIndexInMountainArray(int[] arr) {
        // 不要在两端点设置起始指针
        int left = 1, right = arr.length-2;
        while(left <= right){
            int mid = (left + right) >> 1;
            if(arr[mid] > arr[mid-1]) left = mid + 1;
            else right = mid - 1;
        }
        return right;
    }   
}

🎉剑指 Offer II 070. 排序数组中只出现一次的数字

难度中等67

给定一个只包含整数的有序数组 nums ,每个元素都会出现两次,唯有一个数只会出现一次,请找出这个唯一的数字。

你设计的解决方案必须满足 O(log n) 时间复杂度和 O(1) 空间复杂度。

示例 1:

输入: nums = [1,1,2,3,3,4,4,8,8]
输出: 2

示例 2:

输入: nums =  [3,3,7,7,10,11,11]
输出: 10

提示:

  • 1 <= nums.length <= 105
  • 0 <= nums[i] <= 105

解题思路

第一种就是异或运算。遍历一次O(n)

第二种就是二分查找。

  • 一对出现的情况,下标为:偶数i,奇数i+1比如 [0]==[1], [2]==[3]
  • 如果出现了一个单个的数字,那么情况就变成了相反的情况,相等的两个元素下标为:奇数i,偶数i+1比如 [5]==[6]而不是 [4]==[5]
  • 寻找规律,通过 1^0=1 1^1=0 2^1=3 3^1=2 可以知道,通过异或就能找到与之一对的另一个下标。
  • 如果这两个下标相等,就说明还没有出现单个的数。在右侧
  • 如果这两个下标的值不相等,说明已经出现了,在左侧,
  • 单个的数出现的下标一定是偶数下标。
class Solution {
    public int singleNonDuplicate(int[] nums) {
        // left到right都可能是答案
        int left = 0, right = nums.length-1;
        while(left < right){
            int mid = (left + right) / 2;
            // 相等,说明还没出现单个数,右移
            if(nums[mid] == nums[mid ^ 1])
                left = mid + 1;
            else right = mid;
        }
        // 关键点,跳出的情况,是left==right
        // 单个数出现在偶数下标,就是left,其实right也可以。
        return nums[left];
    }
}

🎉剑指 Offer II 071. 按权重生成随机数

难度中等55

给定一个正整数数组 w ,其中 w[i] 代表下标 i 的权重(下标从 0 开始),请写一个函数 pickIndex ,它可以随机地获取下标 i,选取下标 i 的概率与 w[i] 成正比。

例如,对于 w = [1, 3],挑选下标 0 的概率为 1 / (1 + 3) = 0.25 (即,25%),而选取下标 1 的概率为 3 / (1 + 3) = 0.75(即,75%)。

也就是说,选取下标 i 的概率为 w[i] / sum(w)

示例 1:

输入:
inputs = ["Solution","pickIndex"]
inputs = [[[1]],[]]
输出:
[null,0]
解释:
Solution solution = new Solution([1]);
solution.pickIndex(); // 返回 0,因为数组中只有一个元素,所以唯一的选择是返回下标 0。

示例 2:

输入:
inputs = ["Solution","pickIndex","pickIndex","pickIndex","pickIndex","pickIndex"]
inputs = [[[1,3]],[],[],[],[],[]]
输出:
[null,1,1,1,1,0]
解释:
Solution solution = new Solution([1, 3]);
solution.pickIndex(); // 返回 1,返回下标 1,返回该下标概率为 3/4 。
solution.pickIndex(); // 返回 1
solution.pickIndex(); // 返回 1
solution.pickIndex(); // 返回 1
solution.pickIndex(); // 返回 0,返回下标 0,返回该下标概率为 1/4 。

由于这是一个随机问题,允许多个答案,因此下列输出都可以被认为是正确的:
[null,1,1,1,1,0]
[null,1,1,1,1,1]
[null,1,1,1,0,0]
[null,1,1,1,0,1]
[null,1,0,1,0,0]
......
诸若此类。

提示:

  • 1 <= w.length <= 10000
  • 1 <= w[i] <= 10^5
  • pickIndex 将被调用不超过 10000

题解:构建一个前缀和列表,然后随机生成一个数,二分查找该数应该插入的位置,返回该位置的下标,这样就能实现按照权重返回随机值

class Solution {
    // 构建一个前缀和列表,然后随机生成一个数,二分查找该数应该插入的位置,返回该位置的下标,这样就能实现按照权重返回随机值
    int[] pre;
    int total;

    public Solution(int[] w) {
        this.pre = new int[w.length+1];
        for(int i = 0; i < w.length; i++){
            pre[i+1] = pre[i] + w[i];
        }
        this.total = pre[w.length];
    }
    
    public int pickIndex() {
        int x = (int)(Math.random() * total + 1); 
        // 二分找到插入x的位置
        int left = 0, right = pre.length;
        while(left < right){
            int mid = (left + right) / 2;
            if(pre[mid] < x) left = mid + 1;
            else right = mid;
        }
        return left - 1; // 前缀和数组下标从1开始,这里返回值减一
    }
}

🎉剑指 Offer II 072. 求平方根

难度简单51

给定一个非负整数 x ,计算并返回 x 的平方根,即实现 int sqrt(int x) 函数。

正数的平方根有两个,只输出其中的正数平方根。

如果平方根不是整数,输出只保留整数的部分,小数部分将被舍去。

示例 1:

输入: x = 4
输出: 2

示例 2:

输入: x = 8
输出: 2
解释: 8 的平方根是 2.82842...,由于小数部分将被舍去,所以返回 2

提示:

  • 0 <= x <= 231 - 1

题解:对正负性的二段分

class Solution {
    // 对于连续区间[1, x],我们可以把它分成两部分
    // i ∈ [start, mid] i^2 - x < 0,那么新区间为[mid, end]
    // i ∈ [mid, end] i^2 - x > 0,那么新区间为[start, end-1]
    public int mySqrt(int x) {
        if(x <= 1) return x;
        int start = 0, end = x / 2;
        while(start < end){
            int mid = (start + end) / 2 + 1; // 向上取整
            if((double) mid * mid < x) 
                start = mid;
            else if((double) mid * mid > x)
                end = mid - 1;
            else 
                return mid;
        }
        return start;
    }
}

剑指 Offer II 073. 狒狒吃香蕉

难度中等64

狒狒喜欢吃香蕉。这里有 n 堆香蕉,第 i 堆中有 piles[i] 根香蕉。警卫已经离开了,将在 h 小时后回来。

狒狒可以决定她吃香蕉的速度 k (单位:根/小时)。每个小时,她将会选择一堆香蕉,从中吃掉 k 根。如果这堆香蕉少于 k 根,她将吃掉这堆的所有香蕉,然后这一小时内不会再吃更多的香蕉,下一个小时才会开始吃另一堆的香蕉。

狒狒喜欢慢慢吃,但仍然想在警卫回来前吃掉所有的香蕉。

返回她可以在 h 小时内吃掉所有香蕉的最小速度 kk 为整数)。

示例 1:

输入:piles = [3,6,7,11], h = 8
输出:4

示例 2:

输入:piles = [30,11,23,4,20], h = 5
输出:30

示例 3:

输入:piles = [30,11,23,4,20], h = 6
输出:23

提示:

  • 1 <= piles.length <= 104
  • piles.length <= h <= 109
  • 1 <= piles[i] <= 109
class Solution {
    public int minEatingSpeed(int[] piles, int h) {
        int left = 1, right = (int)1e9;
        while(left < right){
            int mid = (left + right) / 2;
            if(check(piles, h, mid))
                right = mid;
            else left = mid + 1;
        }
        return left;
    }

    public boolean check(int[] piles, int h, int speed){
        double cur = 0.0;
        for(int p : piles){
            // 如果这堆香蕉少于 k 根,她将吃掉这堆的所有香蕉,然后这一小时内不会再吃更多的香蕉
            cur += p / speed + (p % speed == 0 ? 0 : 1);
            if(cur > h) return false;
        }
        return true;
    }
}

11.其他?

剑指 Offer II 030. 插入、删除和随机访问都是 O(1) 的容器

难度中等69

设计一个支持在平均 时间复杂度 O(1) 下,执行以下操作的数据结构:

  • insert(val):当元素 val 不存在时返回 true ,并向集合中插入该项,否则返回 false
  • remove(val):当元素 val 存在时返回 true ,并从集合中移除该项,否则返回 false
  • getRandom:随机返回现有集合中的一项。每个元素应该有 相同的概率 被返回。

示例 :

输入: inputs = ["RandomizedSet", "insert", "remove", "insert", "getRandom", "remove", "insert", "getRandom"]
[[], [1], [2], [2], [], [1], [2], []]
输出: [null, true, false, true, 2, true, false, 2]
解释:
RandomizedSet randomSet = new RandomizedSet();  // 初始化一个空的集合
randomSet.insert(1); // 向集合中插入 1 , 返回 true 表示 1 被成功地插入

randomSet.remove(2); // 返回 false,表示集合中不存在 2 

randomSet.insert(2); // 向集合中插入 2 返回 true ,集合现在包含 [1,2] 

randomSet.getRandom(); // getRandom 应随机返回 1 或 2 
  
randomSet.remove(1); // 从集合中移除 1 返回 true 。集合现在包含 [2] 

randomSet.insert(2); // 2 已在集合中,所以返回 false 

randomSet.getRandom(); // 由于 2 是集合中唯一的数字,getRandom 总是返回 2 

提示:

  • -231 <= val <= 231 - 1
  • 最多进行 2 * 105insertremovegetRandom 方法调用
  • 当调用 getRandom 方法时,集合中至少有一个元素

哈希表 + 删除交换

对于 insertremove 操作容易想到使用「哈希表」来实现 O(1) 复杂度,但对于 getRandom 操作,比较理想的情况是能够在一个数组内随机下标进行返回。

将两者结合,我们可以将哈希表设计为:以入参 val 为键,数组下标 loc 为值。

为了确保严格 O(1),我们不能「使用拒绝采样」和「在数组非结尾位置添加/删除元素」。

因此我们需要申请一个足够大的数组 nums(利用数据范围为 2∗105),并使用变量 idx 记录当前使用到哪一位(即下标在 [0,idx] 范围内均是存活值)。

对于几类操作逻辑:

  • insert 操作:使用哈希表判断 val 是否存在,存在的话返回 false,否则将其添加到 nums,更新 idx,同时更新哈希表;
  • remove 操作:使用哈希表判断 val 是否存在,不存在的话返回 false,否则从哈希表中将 val 删除,同时取出其所在 nums 的下标 loc,然后将 nums[idx] 赋值到 loc 位置,并更新 idx(含义为将原本处于 loc 位置的元素删除),同时更新原本位于 idx 位置的数在哈希表中的值为 loc(若 locidx 相等,说明删除的是最后一个元素,这一步可跳过);
  • getRandom 操作:由于我们人为确保了 [0,idx] 均为存活值,因此直接在 [0,idx+1) 范围内进行随机即可。
class RandomizedSet {
    static int[] nums = new int[200010];
    Random random = new Random();
    Map<Integer, Integer> map = new HashMap<>();
    int idx = -1;
    public RandomizedSet() {       
    }

    public boolean insert(int val) {
        if(map.containsKey(val)) return false;
        nums[++idx] = val;
        map.put(val, idx);
        return true;
    }

    public boolean remove(int val) {
        if(!map.containsKey(val)) return false;
        int loc = map.remove(val);
        if(loc != idx) map.put(nums[idx], loc);
        nums[loc] = nums[idx--];
        return true;
    }

    public int getRandom() {
        return nums[random.nextInt(idx+1)];
    }
}

剑指 Offer II 031. 最近最少使用缓存【146. LRU缓存】

难度中等92

运用所掌握的数据结构,设计和实现一个 LRU (Least Recently Used,最近最少使用) 缓存机制

实现 LRUCache 类:

  • LRUCache(int capacity) 以正整数作为容量 capacity 初始化 LRU 缓存
  • int get(int key) 如果关键字 key 存在于缓存中,则返回关键字的值,否则返回 -1
  • void put(int key, int value) 如果关键字已经存在,则变更其数据值;如果关键字不存在,则插入该组「关键字-值」。当缓存容量达到上限时,它应该在写入新数据之前删除最久未使用的数据值,从而为新的数据值留出空间。

示例:

输入
["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 <= capacity <= 3000
  • 0 <= key <= 10000
  • 0 <= value <= 105
  • 最多调用 2 * 105getput

思路

题解:https://leetcode.cn/problems/lru-cache/solution/lru-ce-lue-xiang-jie-he-shi-xian-by-labuladong/

要让 putget 方法的时间复杂度为 O(1),我们可以总结出 cache 这个数据结构必要的条件:

1、显然 cache 中的元素必须有时序,以区分最近使用的和久未使用的数据,当容量满了之后要删除最久未使用的那个元素腾位置。

2、我们要在 cache 中快速找某个 key 是否已存在并得到对应的 val

3、每次访问 cache 中的某个 key,需要将这个元素变为最近使用的,也就是说 cache 要支持在任意位置快速插入和删除元素。

那么,什么数据结构同时符合上述条件呢?哈希表查找快,但是数据无固定顺序;链表有顺序之分,插入删除快,但是查找慢。所以结合一下,形成一种新的数据结构:哈希链表 LinkedHashMap

借助这个结构,我们来逐一分析上面的 3 个条件:

1、如果我们每次默认从链表尾部添加元素,那么显然越靠尾部的元素就是最近使用的,越靠头部的元素就是最久未使用的。

2、对于某一个 key,我们可以通过哈希表快速定位到链表中的节点,从而取得对应 val

3、链表显然是支持在任意位置快速插入和删除的,改改指针就行。只不过传统的链表无法按照索引快速访问某一个位置的元素,而这里借助哈希表,可以通过 key 快速映射到任意一个链表节点,然后进行插入和删除。

也许读者会问,为什么要是双向链表,单链表行不行?另外,既然哈希表中已经存了 key,为什么链表中还要存 keyval 呢,只存 val 不就行了

想的时候都是问题,只有做的时候才有答案。这样设计的原因,必须等我们亲自实现 LRU 算法之后才能理解,所以我们开始看代码吧~


从0开始实现

注意我们实现的双链表 API 只能从尾部插入,也就是说靠尾部的数据是最近使用的,靠头部的数据是最久为使用的

class LRUCache {

    // key -> Node(key, val)
    private HashMap<Integer, Node> map;
    // Node(k1, v1) <-> Node(k2, v2)...
    private DoubleList cache;
    // 最大容量
    private int cap;

    public LRUCache(int capacity) {
        this.cap = capacity;
        map = new HashMap<>();
        cache = new DoubleList();
    }
    
    public int get(int key) {
        if (!map.containsKey(key)) {
        return -1;
        }
        // 将该数据提升为最近使用的
        makeRecently(key);
        return map.get(key).val;
    }
    
    public void put(int key, int val) {
        if (map.containsKey(key)) {
            // 删除旧的数据
            deleteKey(key);
            // 新插入的数据为最近使用的数据
            addRecently(key, val);
            return;
        }
    
        if (cap == cache.size()) {
            // 删除最久未使用的元素
            removeLeastRecently();
        }
        // 添加为最近使用的元素
        addRecently(key, val);
}

    //  删除某个 key 时,在 cache 中删除了对应的 Node,但是却忘记在 map 中删除 key。
    // 解决这种问题的有效方法是:在这两种数据结构之上提供一层抽象 API。
    //      尽量让 LRU 的主方法 get 和 put 避免直接操作 map 和 cache 的细节
    
    /*  将某个 key 提升为最近使用的 */
    private void makeRecently(int key) {
        Node x = map.get(key);
        // 先从链表中删除这个节点
        cache.remove(x);
        // 重新插到队尾
        cache.addLast(x);
    }

    /* 添加最近使用的元素 */
    private void addRecently(int key, int val) {
        Node x = new Node(key, val);
        // 链表尾部就是最近使用的元素
        cache.addLast(x);
        // 别忘了在 map 中添加 key 的映射
        map.put(key, x);
    }

    /* 删除某一个 key */
    private void deleteKey(int key) {
        Node x = map.get(key);
        // 从链表中删除
        cache.remove(x);
        // 从 map 中删除
        map.remove(key);
    }

    /* 删除最久未使用的元素 */
    private void removeLeastRecently() {
        // 链表头部的第一个元素就是最久未使用的
        Node deletedNode = cache.removeFirst();
        // 同时别忘了从 map 中删除它的 key
        int deletedKey = deletedNode.key;
        map.remove(deletedKey);
    }
}

class Node {
    public int key, val;
    public Node next, prev;
    public Node(int k, int v) {
        this.key = k;
        this.val = v;
    }
}

class DoubleList {  
    // 头尾虚节点
    private Node head, tail;  
    // 链表元素数
    private int size;
    
    public DoubleList() {
        // 初始化双向链表的数据
        head = new Node(0, 0);
        tail = new Node(0, 0);
        head.next = tail;
        tail.prev = head;
        size = 0;
    }

    // 在链表尾部添加节点 x,时间 O(1)
    public void addLast(Node x) {
        x.prev = tail.prev;
        x.next = tail;
        tail.prev.next = x;
        tail.prev = x;
        size++;
    }

    // 删除链表中的 x 节点(x 一定存在)
    // 由于是双链表且给的是目标 Node 节点,时间 O(1)
    public void remove(Node x) {
        x.prev.next = x.next;
        x.next.prev = x.prev;
        size--;
    }
    
    // 删除链表中第一个节点,并返回该节点,时间 O(1)
    public Node removeFirst() {
        if (head.next == tail)
            return null;
        Node first = head.next;
        remove(first);
        return first;
    }

    // 返回链表长度,时间 O(1)
    public int size() { return size; }

}

使用LinkedHashMap实现

class LRUCache {

    int cap;
    LinkedHashMap<Integer,Integer> cache = new LinkedHashMap<>();

    public LRUCache(int capacity) {
        this.cap = capacity;
    }
    
    public int get(int key) {
        if(!cache.containsKey(key)){
            return -1;
        }
        // 将key变为最近使用的
        makeRecently(key);
        return cache.get(key);
    }
    
    public void put(int key, int value) {
        if(cache.containsKey(key)){
            // 修改 key 的值
            cache.put(key, value);
            // 将 key 变为最近使用
            makeRecently(key);
            return;
        }
        if(cache.size() >= this.cap){
            // 链表头部就是最久未使用的 key
            int oldestKey = cache.keySet().iterator().next();
            cache.remove(oldestKey);
        }
        // 将新的 key 添加链表尾部
        cache.put(key, value);
    }

    public void makeRecently(int key){
        int val = cache.get(key);
        cache.remove(key);
        cache.put(key, val);
    }
}

拓展:460. LFU 缓存

剑指 Offer II 074. 合并区间【重要】

难度中等55

以数组 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].

示例 2:

输入:intervals = [[1,4],[4,5]]
输出:[[1,5]]
解释:区间 [1,4] 和 [4,5] 可被视为重叠区间。

提示:

  • 1 <= intervals.length <= 104
  • intervals[i].length == 2
  • 0 <= starti <= endi <= 104

题解:

2022.08.27.14:00完美世界

2022.09.20 小米

2023.03.02 阿里

2023.3.27美团暑期二面

2023.3.27拼多多

class Solution {
    public int[][] merge(int[][] intervals) {
        Arrays.sort(intervals, (a, b) -> a[0] == b[0] ? a[1]-b[1] : a[0]-b[0]);
        List<int[]> list = new ArrayList<>();
        int i = 0;
        while(i < intervals.length){
            int left = intervals[i][0];
            int right = intervals[i][1];
            while(i+1 < intervals.length && right >= intervals[i+1][0]){
                right = Math.max(right, intervals[i+1][1]);
                i++;
            }
            list.add(new int[]{left, right});
            i++;
        }
        int[][] res = new int[list.size()][2];
        for(int k = 0; k < list.size(); k++){
            res[k] = list.get(k);
        }
        return res;
    }
}

🎉剑指 Offer II 075. 数组相对排序

难度简单61

给定两个数组,arr1arr2

  • arr2 中的元素各不相同
  • arr2 中的每个元素都出现在 arr1

arr1 中的元素进行排序,使 arr1 中项的相对顺序和 arr2 中的相对顺序相同。未在 arr2 中出现过的元素需要按照升序放在 arr1 的末尾。

示例:

输入:arr1 = [2,3,1,3,2,4,6,7,9,2,19], arr2 = [2,1,4,3,9,6]
输出:[2,2,2,1,4,3,3,9,6,7,19]

提示:

  • 1 <= arr1.length, arr2.length <= 1000
  • 0 <= arr1[i], arr2[i] <= 1000
  • arr2 中的元素 arr2[i] 各不相同
  • arr2 中的每个元素 arr2[i] 都出现在 arr1
class Solution {
    public int[] relativeSortArray(int[] arr1, int[] arr2) {
        // 计数排序, 统计个元素出现的次数
        int[] cnt = new int[1001];
        for(int num : arr1){
            cnt[num]++;
        }
        int idx = 0;
        //遍历 arr2,以 arr2的顺序填arr1数组
        for(int num : arr2){
            //得到 num 元素的个数,并往arr1填充
            for(int i = 0; i < cnt[num]; i++){
                arr1[idx++] = num;
            }
            cnt[num] = 0;
        }
        //将剩下的数字按序填入arr1
        for(int i = 0; i < cnt.length; i++){
            for(int j = 0; j < cnt[i]; j++){
                arr1[idx++] = i;
            }
        }
        return arr1;
    }
}

剑指 Offer II 076. 数组中的第 k 大的数字

难度中等73

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

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

示例 1:

输入: [3,2,1,5,6,4] 和 k = 2
输出: 5

示例 2:

输入: [3,2,3,1,2,4,5,5,6] 和 k = 4
输出: 4

提示:

  • 1 <= k <= nums.length <= 104
  • -104 <= nums[i] <= 104
class Solution {
    public int findKthLargest(int[] nums, int k) {
        PriorityQueue<Integer> pq = new PriorityQueue<>();
        for(int num : nums){
            if(pq.size() < k){
                pq.add(num);
            }else{
                if(pq.peek() < num){
                    pq.poll();
                    pq.add(num);
                }
            }
        }
        return pq.peek();
    }
}

12.回溯问题(子集/组合/排列+带重复)

剑指 Offer II 079. 所有子集

难度中等67

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

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

示例 1:

输入:nums = [1,2,3]
输出:[[],[1],[2],[1,2],[3],[1,3],[2,3],[1,2,3]]

示例 2:

输入:nums = [0]
输出:[[],[0]]

提示:

  • 1 <= nums.length <= 10
  • -10 <= nums[i] <= 10
  • nums 中的所有元素 互不相同

方法一:枚举第i位选还是不选

class Solution {
    List<List<Integer>> res;
    List<Integer> cur;
    int[] nums;
    public List<List<Integer>> subsets(int[] nums) {
        res = new ArrayList<>();
        cur = new ArrayList<>();
        this.nums = nums;
        dfs(0);
        return res;
    }

    // 1. 枚举第i位选还是不选
    public void dfs(int i){
        if(i == nums.length){
            res.add(new ArrayList<>(cur));
            return;
        }
        dfs(i+1);
        cur.add(nums[i]);
        dfs(i+1);
        cur.remove(cur.size()-1);
        return;
    }
}

方法二:枚举第i位选哪个

class Solution {
    List<List<Integer>> res;
    List<Integer> cur;
    int[] nums;
    public List<List<Integer>> subsets(int[] nums) {
        res = new ArrayList<>();
        cur = new ArrayList<>();
        this.nums = nums;
        dfs(0);
        return res;
    }

    // 2. 枚举第i位选哪个
    public void dfs(int i){
        res.add(new ArrayList<>(cur));
        if(i == nums.length)
            return;
        for(int j = i; j < nums.length; j++){
            cur.add(nums[j]);
            dfs(j+1);
            cur.remove(cur.size()-1);
        }
    }
}

剑指 Offer II 080. 含有 k 个元素的组合

难度中等46

给定两个整数 nk,返回 1 ... n 中所有可能的 k 个数的组合。

示例 1:

输入: n = 4, k = 2
输出:
[
  [2,4],
  [3,4],
  [2,3],
  [1,2],
  [1,3],
  [1,4],
]

示例 2:

输入: n = 1, k = 1
输出: [[1]]

提示:

  • 1 <= n <= 20
  • 1 <= k <= n

方法一:枚举i位置选还是不选

class Solution {
    List<List<Integer>> res;
    List<Integer> cur;
    int n, k;
    public List<List<Integer>> combine(int n, int k) {
        res = new ArrayList<>();
        cur = new ArrayList<>();
        this.n = n; this.k = k;
        dfs(n);
        return res;
    }

    public void dfs(int i){
        int d = k - cur.size(); // 还要选d个数
        if(d == 0){
            res.add(new ArrayList<>(cur));
            return;
        }
        if(i > d) dfs(i-1); // 不选i
        // 选i
        cur.add(i);
        dfs(i-1);
        cur.remove(cur.size()-1);
    }
}

方法二:枚举i位置选哪个

class Solution {
    List<List<Integer>> res;
    List<Integer> cur;
    int n, k;
    public List<List<Integer>> combine(int n, int k) {
        res = new ArrayList<>();
        cur = new ArrayList<>();
        this.n = n; this.k = k;
        dfs(n);
        return res;
    }

    public void dfs(int i){
        int d = k - cur.size(); // 还要选d个数
        if(d == 0){
            res.add(new ArrayList<>(cur));
            return;
        }
        for(int j = i; j >= d; j--){
            cur.add(j);
            dfs(j-1);
            cur.remove(cur.size()-1);
        }
    }
}

方法三:枚举所有大小为k的子集

class Solution {
    public List<List<Integer>> combine(int n, int k) {
        List<List<Integer>> res = new ArrayList<>();
        for(int i = 1; i < (1 << n); i++){
            if(Integer.bitCount(i) == k){
                List<Integer> cur = new ArrayList<>();
                for(int j = 0; j < n; j++){
                    if(((i >> j) & 1) == 1){
                        cur.add(j+1);
                    }
                }
                res.add(cur);
            }
        }
        return res;
    }
}

剑指 Offer II 081. 允许重复选择元素的组合

难度中等56

给定一个无重复元素的正整数数组 candidates 和一个正整数 target ,找出 candidates 中所有可以使数字和为目标数 target 的唯一组合。

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

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

示例 1:

输入: candidates = [2,3,6,7], target = 7
输出: [[7],[2,2,3]]

示例 2:

输入: candidates = [2,3,5], target = 8
输出: [[2,2,2,2],[2,3,3],[3,5]]

示例 3:

输入: candidates = [2], target = 1
输出: []

示例 4:

输入: candidates = [1], target = 1
输出: [[1]]

示例 5:

输入: candidates = [1], target = 2
输出: [[1,1]]

提示:

  • 1 <= candidates.length <= 30
  • 1 <= candidates[i] <= 200
  • candidate 中的每个元素都是独一无二的。
  • 1 <= target <= 500
class Solution {
    List<List<Integer>> res;
    List<Integer> cur;
    int[] candidates;
    int target;
    public List<List<Integer>> combinationSum(int[] candidates, int target) {
        res = new ArrayList<>();
        cur = new ArrayList<>();
        this.candidates = candidates;
        this.target = target;
        dfs(0, 0);
        return res;
    }
    
    public void dfs(int i, int sum){
        if(i == candidates.length || sum > target)
            return;
        if(sum == target){
            res.add(new ArrayList<>(cur));
            return;
        }
        for(int j = i; j < candidates.length; j++){
            cur.add(candidates[j]);
            dfs(j, sum + candidates[j]);
            cur.remove(cur.size()-1);
        }
        return;
    }
}

剑指 Offer II 082. 含有重复元素集合的组合

难度中等53

给定一个可能有重复数字的整数数组 candidates 和一个目标数 target ,找出 candidates 中所有可以使数字和为 target 的组合。

candidates 中的每个数字在每个组合中只能使用一次,解集不能包含重复的组合。

示例 1:

输入: candidates = [10,1,2,7,6,1,5], target = 8,
输出:
[
[1,1,6],
[1,2,5],
[1,7],
[2,6]
]

示例 2:

输入: candidates = [2,5,2,1,2], target = 5,
输出:
[
[1,2,2],
[5]
]

提示:

  • 1 <= candidates.length <= 100
  • 1 <= candidates[i] <= 50
  • 1 <= target <= 30

题解:为了避免重复的答案,首先我们要做的就是给数组排序,如果说我在同一级递归中,遇到两个相同的数,我们应该只dfs靠前的那一个一次。原因的话,我们可以这样理解,如果现在遇到下标位idxidx + 1的两个数是相同的,那么对于集合dfs(idx, target)dfs(idx + 1, target),后者就是前者的一个子集,所以我们在同一级递归中,对于相同的数,只应该dfs一次,并且是下标最小的那一个

class Solution {
    List<List<Integer>> res;
    List<Integer> cur;
    int[] candidates;
    public List<List<Integer>> combinationSum2(int[] candidates, int target) {
        Arrays.sort(candidates);
        res = new ArrayList<>();
        cur = new ArrayList<>();
        this.candidates = candidates;
        dfs(0, target);
        return res;
    }

    public void dfs(int i, int sum){
        if(sum == 0){
            res.add(new ArrayList<>(cur));
            return;
        }
        for(int j = i; j < candidates.length; j++){
            if(sum - candidates[j] < 0)
                break; // 剪枝优化
            if(j > i && candidates[j] == candidates[j-1])
                // 因为前面那个同样的数已经经历过dfs,再拿同样的数dfs就会得到重复的答案
                continue; // 处理重复的情况,避免重复组合
            cur.add(candidates[j]);
            dfs(j+1, sum - candidates[j]);
            cur.remove(cur.size() - 1);
        }
    }
}

剑指 Offer II 083. 没有重复元素集合的全排列

难度中等58

给定一个不含重复数字的整数数组 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]]

示例 3:

输入:nums = [1]
输出:[[1]]

提示:

  • 1 <= nums.length <= 6
  • -10 <= nums[i] <= 10
  • nums 中的所有整数 互不相同
class Solution {
   List<List<Integer>> res;
   List<Integer> cur;
   int[] nums;
   boolean[] visited;
   public List<List<Integer>> permute(int[] nums) {
       this.nums = nums;
       res = new ArrayList<>();
       cur = new ArrayList<>();
       visited = new boolean[nums.length];
       dfs(0);
       return res;
   }

   public void dfs(int i){
       if(i == nums.length){
           res.add(new ArrayList<>(cur));
           return;
       }
       for(int j = 0; j < nums.length; j++){
           if(!visited[j]){
               visited[j] = true;
               cur.add(nums[j]);
               dfs(i+1);
               cur.remove(cur.size() - 1);
               visited[j] = false;
           }
       }
   }
}

剑指 Offer II 084. 含有重复元素集合的全排列

难度中等52

给定一个可包含重复数字的整数集合 nums按任意顺序 返回它所有不重复的全排列。

示例 1:

输入:nums = [1,1,2]
输出:
[[1,1,2],
 [1,2,1],
 [2,1,1]]

示例 2:

输入:nums = [1,2,3]
输出:[[1,2,3],[1,3,2],[2,1,3],[2,3,1],[3,1,2],[3,2,1]]

提示:

  • 1 <= nums.length <= 8
  • -10 <= nums[i] <= 10
class Solution {
   List<List<Integer>> res;
   List<Integer> cur;
   int[] nums;
   boolean[] visited;
   public List<List<Integer>> permuteUnique(int[] nums) {
       Arrays.sort(nums);
       this.nums = nums;
       res = new ArrayList<>();
       cur = new ArrayList<>();
       visited = new boolean[nums.length];
       dfs(0);
       return res;
   }

   public void dfs(int i){
       if(i == nums.length){
           res.add(new ArrayList<>(cur));
           return;
       }
       for(int j = 0; j < nums.length; j++){
           // visit[i - 1] == true,说明同⼀树⽀nums[i - 1]使⽤过
           // visit[i - 1] == false,说明同⼀树层nums[i - 1]使⽤过
           // 如果同⼀树层nums[i - 1]使⽤过则直接跳过
           if(j > 0 && nums[j] == nums[j-1] && visited[j-1] == false){
               // 如果要对树层中前一位去重,就用used[i - 1] == false,如果要对树枝前一位去重用used[i - 1] == true。
               // 对于排列问题,树层上去重和树枝上去重,都是可以的,但是树层上去重效率更高!
               continue;
           }
           if(!visited[j]){
               visited[j] = true;
               cur.add(nums[j]);
               dfs(i+1);
               cur.remove(cur.size() - 1);
               visited[j] = false;
           }
       }
   }
}

剑指 Offer II 085. 生成匹配的括号

难度中等73

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

示例 1:

输入:n = 3
输出:["((()))","(()())","(())()","()(())","()()()"]

示例 2:

输入:n = 1
输出:["()"]

提示:

  • 1 <= n <= 8
class Solution {
   int n;
   char[] path;
   List<String> res = new ArrayList<>();
   public List<String> generateParenthesis(int n) {
       this.n = n;
       path = new char[n * 2];
       dfs(0, 0);
       return res;
   }

   public void dfs(int i, int open){
       if(i == n * 2){
           res.add(new String(path));
       }
       if(open < n){ // 可以填左括号
           path[i] = '(';
           dfs(i+1, open+1);
       }
       if(i - open < open){ // 不可以填左括号了,只能填右括号}
           path[i] = ')';
           dfs(i+1, open);
       }
   }
}

剑指 Offer II 086. 分割回文子字符串

难度中等58

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

回文串 是正着读和反着读都一样的字符串。

示例 1:

输入:s = "google"
输出:[["g","o","o","g","l","e"],["g","oo","g","l","e"],["goog","l","e"]]

示例 2:

输入:s = "aab"
输出:[["a","a","b"],["aa","b"]]

示例 3:

输入:s = "a"
输出:[["a"]]

提示:

  • 1 <= s.length <= 16
  • s 仅由小写英文字母组成
class Solution {
    List<List<String>> res;
    List<String> cur;
    String s;
    public String[][] partition(String s) {
        cur = new ArrayList<>();
		res = new ArrayList<>();
        this.s = s;
        dfs(0);
        String[][] resArr = new String[res.size()][];
        for(int i = 0; i < res.size(); i++){
            List<String> list = res.get(i);
            resArr[i] = list.toArray(new String[list.size()]);
        }
        return resArr;
    }
    
    public void dfs(int i){
		if(i == s.length()){
			res.add(new ArrayList<>(cur));
            return;
        }
        for(int j = i; j < s.length(); j++){
            String t = s.substring(i, j+1);
            if(isrev(t)){
                cur.add(t);
                dfs(j+1);
                cur.remove(cur.size() - 1);
            }
        }
    }
    
    public boolean isrev(String t){
        int left = 0, right = t.length()-1;
        while(left < right){
			if(t.charAt(left) != t.charAt(right)) return false;
            left += 1; right -= 1;
        }
        return true;
    }
}

剑指 Offer II 087. 复原 IP

难度中等52

给定一个只包含数字的字符串 s ,用以表示一个 IP 地址,返回所有可能从 s 获得的 有效 IP 地址 。你可以按任何顺序返回答案。

有效 IP 地址 正好由四个整数(每个整数位于 0 到 255 之间组成,且不能含有前导 0),整数之间用 '.' 分隔。

例如:“0.1.2.201” 和 “192.168.1.1” 是 有效 IP 地址,但是 “0.011.255.245”、“192.168.1.312” 和 “192.168@1.1” 是 无效 IP 地址。

示例 1:

输入:s = "25525511135"
输出:["255.255.11.135","255.255.111.35"]

示例 2:

输入:s = "0000"
输出:["0.0.0.0"]

示例 3:

输入:s = "1111"
输出:["1.1.1.1"]

示例 4:

输入:s = "010010"
输出:["0.10.0.10","0.100.1.0"]

示例 5:

输入:s = "10203040"
输出:["10.20.30.40","102.0.30.40","10.203.0.40"]

提示:

  • 0 <= s.length <= 3000
  • s 仅由数字组成
class Solution {
   List<String> res = new ArrayList<>();
   List<String> tmp = new ArrayList<>();
   String s;
   public List<String> restoreIpAddresses(String s) {
   	this.s = s;
       dfs(0);
       return res;
   }
   
   public void dfs(int i){
       if(tmp.size() == 4 && i != s.length()) return;
       if(tmp.size() == 4 && i == s.length()){
   		res.add(String.join(".", tmp));
           return;
       }
       // 枚举分割点
       for(int j = i; j < s.length() && j < i + 3; j++){
           String str = s.substring(i, j+1);
           // 每个整数位于 0 到 255 之间组成
           if(Integer.parseInt(str) <= 255){
               // 不能有前导0
               if(str.length() > 1 && str.charAt(0) == '0')
                   return;
              	tmp.add(str);
               dfs(j+1);
               tmp.remove(tmp.size()-1);
           }else return;
       }
   }
}

13.动态规划问题

剑指 Offer II 088. 爬楼梯的最少成本

难度简单77

数组的每个下标作为一个阶梯,第 i 个阶梯对应着一个非负数的体力花费值 cost[i](下标从 0 开始)。

每当爬上一个阶梯都要花费对应的体力值,一旦支付了相应的体力值,就可以选择向上爬一个阶梯或者爬两个阶梯。

请找出达到楼层顶部的最低花费。在开始时,你可以选择从下标为 0 或 1 的元素作为初始阶梯。

示例 1:

输入:cost = [10, 15, 20]
输出:15
解释:最低花费是从 cost[1] 开始,然后走两步即可到阶梯顶,一共花费 15 。

示例 2:

输入:cost = [1, 100, 1, 1, 1, 100, 1, 1, 100, 1]
输出:6
解释:最低花费方式是从 cost[0] 开始,逐个经过那些 1 ,跳过 cost[3] ,一共花费 6 。

提示:

  • 2 <= cost.length <= 1000
  • 0 <= cost[i] <= 999
class Solution {
   public int minCostClimbingStairs(int[] cost) {
       int n = cost.length;
       int[] f = new int[n+1];
       f[0] = f[1] = 0;
       for(int i = 1; i < n; i++){
           f[i+1] = Math.min(f[i] + cost[i], f[i-1] + cost[i-1]); 
       }
       return f[n];
   }
}

剑指 Offer II 089. 房屋偷盗

难度中等37

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

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

示例 1:

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

示例 2:

输入:nums = [2,7,9,3,1]
输出:12
解释:偷窃 1 号房屋 (金额 = 2), 偷窃 3 号房屋 (金额 = 9),接着偷窃 5 号房屋 (金额 = 1)。
     偷窃到的最高金额 = 2 + 9 + 1 = 12 。

提示:

  • 1 <= nums.length <= 100
  • 0 <= nums[i] <= 400
class Solution {
    public int rob(int[] nums) {
        int n = nums.length;
        // 定义f[i][j]表示偷盗第 i 间房屋时,偷\不偷(0\1)第i间房屋能偷窃到的最高金额
        // 转移: 枚举第i间房子偷还是不偷
        //     如果第i间房子偷,那么i-1不能偷
        //          f[i][0] = f[i-1][1] + nums[i]
        //      如果第i间房子不偷,那么i-1可以偷可以不偷
        //          f[i][1] = Math.max(f[i-1][0], f[i-1][1])
        // 初始值 f[0][0] = -inf, f[0][1] = 0
        // 返回值 max(f[n][0], f[n][1])
        int[][] f = new int[n+1][2];
        f[0][0] = Integer.MIN_VALUE;
        f[0][1] = 0;
        for(int i = 0; i < n; i++){
            f[i+1][0] = f[i][1] + nums[i];
            f[i+1][1] = Math.max(f[i][0], f[i][1]); 
        }
        return Math.max(f[n][0], f[n][1]);
    }
}

剑指 Offer II 090. 环形房屋偷盗

难度中等42

一个专业的小偷,计划偷窃一个环形街道上沿街的房屋,每间房内都藏有一定的现金。这个地方所有的房屋都 围成一圈 ,这意味着第一个房屋和最后一个房屋是紧挨着的。同时,相邻的房屋装有相互连通的防盗系统,如果两间相邻的房屋在同一晚上被小偷闯入,系统会自动报警

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

示例 1:

输入:nums = [2,3,2]
输出:3
解释:你不能先偷窃 1 号房屋(金额 = 2),然后偷窃 3 号房屋(金额 = 2), 因为他们是相邻的。

示例 2:

输入:nums = [1,2,3,1]
输出:4
解释:你可以先偷窃 1 号房屋(金额 = 1),然后偷窃 3 号房屋(金额 = 3)。
     偷窃到的最高金额 = 1 + 3 = 4 。

示例 3:

输入:nums = [0]
输出:0

提示:

  • 1 <= nums.length <= 100
  • 0 <= nums[i] <= 1000

相比较之前的房屋偷盗就多了一个限制条件,最后一个不能和第一个连接。 分类讨论 1.去掉第一个 2.去掉最后一个 最后max一下

class Solution {
    public int rob(int[] nums) {
        int n = nums.length;
        if(n == 1) return nums[0];
        int[] arr = new int[n-1];
        System.arraycopy(nums, 0, arr, 0, n-1);
        int res1 = roboffer89(arr);
        System.arraycopy(nums, 1, arr, 0, n-1);
        int res2 = roboffer89(arr);
        return Math.max(res1, res2);
    }

    public int roboffer89(int[] nums) {
        int n = nums.length;
        // 定义f[i][j]表示偷盗第 i 间房屋时,偷\不偷(0\1)第i间房屋能偷窃到的最高金额
        // 转移: 枚举第i间房子偷还是不偷
        //     如果第i间房子偷,那么i-1不能偷
        //          f[i][0] = f[i-1][1] + nums[i]
        //      如果第i间房子不偷,那么i-1可以偷可以不偷
        //          f[i][1] = Math.max(f[i-1][0], f[i-1][1])
        // 初始值 f[0][0] = -inf, f[0][1] = 0
        // 返回值 max(f[n][0], f[n][1])
        int[][] f = new int[n+1][2];
        f[0][0] = Integer.MIN_VALUE;
        f[0][1] = 0;
        for(int i = 0; i < n; i++){
            f[i+1][0] = f[i][1] + nums[i];
            f[i+1][1] = Math.max(f[i][0], f[i][1]); 
        }
        return Math.max(f[n][0], f[n][1]);
    }
}

剑指 Offer II 091. 粉刷房子

难度中等153

假如有一排房子,共 n 个,每个房子可以被粉刷成红色、蓝色或者绿色这三种颜色中的一种,你需要粉刷所有的房子并且使其相邻的两个房子颜色不能相同。

当然,因为市场上不同颜色油漆的价格不同,所以房子粉刷成不同颜色的花费成本也是不同的。每个房子粉刷成不同颜色的花费是以一个 n x 3 的正整数矩阵 costs 来表示的。

例如,costs[0][0] 表示第 0 号房子粉刷成红色的成本花费;costs[1][2] 表示第 1 号房子粉刷成绿色的花费,以此类推。

请计算出粉刷完所有房子最少的花费成本。

示例 1:

输入: costs = [[17,2,17],[16,16,5],[14,3,19]]
输出: 10
解释: 将 0 号房子粉刷成蓝色,1 号房子粉刷成绿色,2 号房子粉刷成蓝色。
     最少花费: 2 + 5 + 3 = 10。

示例 2:

输入: costs = [[7,6,2]]
输出: 2

提示:

  • costs.length == n
  • costs[i].length == 3
  • 1 <= n <= 100
  • 1 <= costs[i][j] <= 20

题解:状态机DP

记忆化搜索

class Solution {
    int[][] costs, cache;
    public int minCost(int[][] costs) {
        this.costs = costs;
        int n = costs.length;
        cache = new int[n][3];
        for(int i = 0; i < n; i++)
            Arrays.fill(cache[i], -1);
        int res = Integer.MAX_VALUE;
        for(int i = 0; i < 3; i++)
            res = Math.min(res, dfs(n-1, i));
        return res;
    }

    // 定义dfs(i, j) 表示还要粉刷i个房屋,第i号的房屋刷的颜色是j, 需要的最少花费成本
    // 转移: 枚举第i号房屋粉刷不同的颜色
    //      如果j = 0,则dfs(i, j) = max(dfs(i-1, 1), dfs(i-1, 2)) + cost[i][j]
    //      如果j = 1,则dfs(i, j) = max(dfs(i-1, 0), dfs(i-1, 2)) + cost[i][j]
    //      如果j = 2,则dfs(i, j) = max(dfs(i-1, 1), dfs(i-1, 0)) + cost[i][j]
    //      返回最大值
    // 递归边界:i < 0, 表示所有房子都刷了,返回0
    // 递归入口:dfs(n-1, k) + cost[n-1][k] for k in range(3)
    public int dfs(int i, int j){
        if(i < 0) return 0;
        if(cache[i][j] >= 0) return cache[i][j];
        int res = 0;
        if(j == 0){
            res = Math.min(dfs(i-1, 1), dfs(i-1, 2)) + costs[i][j];
        }else if(j == 1){
            res = Math.min(dfs(i-1, 0), dfs(i-1, 2)) + costs[i][j];
        }else{
            res = Math.min(dfs(i-1, 0), dfs(i-1, 1)) + costs[i][j];
        }
        return cache[i][j] = res;
    }
}

转为递推

class Solution {
    public int minCost(int[][] costs) {
        int n = costs.length;
        int[][] f = new int[n+1][3];
        for(int i = 0; i < n; i++){
            f[i+1][0] = Math.min(f[i][1], f[i][2]) + costs[i][0];
            f[i+1][1] = Math.min(f[i][0], f[i][2]) + costs[i][1];
            f[i+1][2] = Math.min(f[i][0], f[i][1]) + costs[i][2];
        }
        return Math.min(f[n][0], Math.min(f[n][1], f[n][2]));
    }
}

🎉剑指 Offer II 092. 翻转字符

难度中等77

如果一个由 '0''1' 组成的字符串,是以一些 '0'(可能没有 '0')后面跟着一些 '1'(也可能没有 '1')的形式组成的,那么该字符串是 单调递增 的。

我们给出一个由字符 '0''1' 组成的字符串 s,我们可以将任何 '0' 翻转为 '1' 或者将 '1' 翻转为 '0'

返回使 s 单调递增 的最小翻转次数。

示例 1:

输入:s = "00110"
输出:1
解释:我们翻转最后一位得到 00111.

示例 2:

输入:s = "010110"
输出:2
解释:我们翻转得到 011111,或者是 000111。

示例 3:

输入:s = "00011000"
输出:2
解释:我们翻转得到 00000000。

提示:

  • 1 <= s.length <= 20000
  • s 中只包含字符 '0''1'

方法一:贪心

因为要求当前最小的翻转使得单调递增 因为只存在于 0 1

⭐ 那么如果顺序遍历到当前字符为 1 时,此时不会对结果产生贡献,

但是如果顺序遍历到当前字符为 0 时 , 有两种情况:

1️⃣ 如果需要保留当前 这个 0 则需要将其前面所有出现的 1 都要进行翻转置为 0 保证当前出现 0 的前面全部为 0

2️⃣ 将当前这个 0 置为 1 即当前需要翻转的翻转次数 + 1

上述两种情况显然取最小 ,依次顺序遍历 求取贡献度 遍历完成 即完成 故时间复杂度为 O(n)

class Solution {
    public int minFlipsMonoIncr(String s) {
        int f = 0, cnt1 = 0;
        for(char c : s.toCharArray()){
            if(c == '1') cnt1++;
            else{
                f = Math.min(f+1, cnt1);
            } 
        }
        return f;
    }

方法二:动态规划

class Solution {
    public int minFlipsMonoIncr(String s) {
        int n = s.length();
        // dp[i][0]表示第i个字符是0的变换次数,dp[i][1]表示第i个字符是1的变换次数。
        // 根据单调性:
        //  若s[i-1] == '0',s[i]是0或者1都可以保持单调性。
        //  若s[i-1] == '1',s[i]则必须为1才可以保持单调性(必须满足i-1是1)。
        //  所以dp[i][0] = dp[i-1][0] + (s[i] == '1' ? 1 : 0);(自己是0,则前边都是0)
        //  dp[i][1] = Math.min(dp[i-1][0],dp[i-1][1]) + (s[i] == '0' ? 1 : 0);(自己是1,前边0或者1都可以)
        // 最后result = Math.min(dp[i][0],dp[i][1])
        int[][] dp = new int[n+1][2];
        for(int i = 0; i < n; i++){
            // 把第i个字符变成0
            dp[i+1][0] = dp[i][0] + (s.charAt(i) == '1' ? 1 : 0);
            // 把第i个字符变成1
            dp[i+1][1] = Math.min(dp[i][0], dp[i][1]) + (s.charAt(i) == '0' ? 1 : 0); 
        }
        return Math.min(dp[n][0], dp[n][1]);
    }
}

🎉剑指 Offer II 093. 最长斐波那契数列

难度中等77

如果序列 X_1, X_2, ..., X_n 满足下列条件,就说它是 斐波那契式 的:

  • n >= 3
  • 对于所有 i + 2 <= n,都有 X_i + X_{i+1} = X_{i+2}

给定一个严格递增的正整数数组形成序列 arr ,找到 arr 中最长的斐波那契式的子序列的长度。如果一个不存在,返回 0 。

(回想一下,子序列是从原序列 arr 中派生出来的,它从 arr 中删掉任意数量的元素(也可以不删),而不改变其余元素的顺序。例如, [3, 5, 8][3, 4, 5, 6, 7, 8] 的一个子序列)

示例 1:

输入: arr = [1,2,3,4,5,6,7,8]
输出: 5
解释: 最长的斐波那契式子序列为 [1,2,3,5,8] 。

示例 2:

输入: arr = [1,3,7,11,12,14,18]
输出: 3
解释: 最长的斐波那契式子序列有 [1,11,12]、[3,11,14] 以及 [7,11,18] 。

提示:

  • 3 <= arr.length <= 1000
  • 1 <= arr[i] < arr[i + 1] <= 10^9

【dp定义 & 状态转移方程】

关于dp[][]的定义不多说,问就是不会。总之dp[i][j]定义为以arr[i]arr[j]作为斐波那契式子序列最后两个数的子序列长度。例如arr = {2,3,4,5,7,8},那么dp[3][5] = 4,表示斐式子序列{2,3,5,8}的长度为4。dp[2][4] = 3,表示斐式子序列{3,4,7}的长度为3。

于是状态转移方程为,在满足当arr[i] + arr[j] = arr[k]时, dp[j][k] = dp[i][j] + 1。每次执行转移后,要及时地更新max。max初始为0,若max有过更新,则说明至少存在3个数满足斐波那契关系,由于max一开始是0,所以最终要返回max + 2。但是如果max一直没有更新过,说明不存在斐波那契关系(最低要求三个数),此时要返回0。总之要么返回0(不存在斐波那契关系时),要么返回大于等于3的数(存在斐波那契关系时)。

  • 关于循环的的细节。首先,常见二维dp的主体部分由两个嵌套的for构成,直观来说是通过i,j下标的变化,从边界出发逐渐填满一个二维矩阵。本题的特殊性在于,dp[i][j]的值并不由(显式的)边界递推而来,而是需要主动搜索斐波那契关系,即需要找到第一个arr[i] + arr[j] = arr[k],然后才能得到第一个递推,即dp[j][k] = dp[i][j] + 1。递推趋势或者说for中下标的变化,总是使得当前dp值,是用已有的dp值来更新的。**根据i,j,k的前后关系,i与k将j夹在中间,并且dp的值要从前往后更新(下标从小到达变化)。i从0到n,j的上界是k,因此k的定义要在j之前,而i与k之间必须容纳下至少一个j,因此k从i+2开始。j从i+1开始,上界为k。**于是写出如下三重for。
for(int i = 0; i < n; i++){
    for(int k = i + 2; k < n; k++){
        for(int j = i + 1; j < k; j++){
}}}

【方法一:通过遍历寻找三个数的斐波那契关系(三个for的超时版本)】

class Solution {
    public int lenLongestFibSubseq(int[] arr) {
        int max = 0, n = arr.length;
        int[][] dp = new int[n - 1][n]; // i一定小于j,所以第一维大小可以设为n-1
        for(int i = 0; i < n; i++){
            for(int k = i + 2; k < n; k++){ // k至少比i大2,因为中间要放下j
                for(int j = i + 1; j < k; j++){ // 因为要考察的i,j,k依次递增,因此先定义k再定义j,才可以写出j < k
                    if(arr[i] + arr[j] == arr[k]){ // 只有满足此条件才转移
                        dp[j][k] = dp[i][j] + 1; 
                        max = Math.max(max, dp[j][k]); // 每次发生dp赋值时都要更新max
                    }
                }
            }
        }
        return max == 0 ? 0 : max + 2;
    }
}

【方法二:动态规划 + 哈希表】

在理解了方法一的基础上,将第三个for去掉,确定i和k的基础上,寻找斐波那契关系借助哈希表实现,即arr[k] - arr[i]作为key如果在map中存在的话,value就是下标j。其余写法与方法一相同,很容易能够得到如下代码。另外,由于arr是严格递增的,因此也可以用二分法来寻找arr[j]。二分法代码也一并在最后给出。

class Solution {
    public int lenLongestFibSubseq(int[] arr) {
        int max = 0, n = arr.length;
        Map<Integer, Integer> map = new HashMap<>();
        for(int i = 0; i < n; i++) { // 得到值与下标的映射
            map.put(arr[i], i);
        }
        int[][] dp = new int[n - 1][n]; // i一定小于j,所以第一维大小可以设为n-1
        for(int i = 0; i < n; i++){
            for(int k = i + 2; k < n; k++){ // k至少比i大2,因为中间要放下j
                int j = map.getOrDefault(arr[k] - arr[i], -1); // 获取满足arr[i]+arr[j]=arr[k]的j,不满足时j=-1
                if(i < j && j < k) { // j在i和k之间
                    dp[j][k] = dp[i][j] + 1;
                    max = Math.max(max, dp[j][k]);
                }
            }
        }
        return max == 0 ? 0 : max + 2;
    }
}

【方法三:动态规划 + 二分】

与方法二只在寻找arr[j]上有区别,方法二通过哈希表寻找,本方法通过二分来寻找。

class Solution {
    public int lenLongestFibSubseq(int[] arr) {
        int max = 0, n = arr.length;
        int[][] dp = new int[n - 1][n]; // i一定小于j,所以第一维大小可以设为n-1
        for(int i = 0; i < n; i++){
            for(int k = i + 2; k < n; k++){ // k至少比i大2,因为中间要放下j
                int j = search(arr, arr[k] - arr[i], i + 1, k - 1); // 获取满足arr[i]+arr[j]=arr[k]的j,不满足时j=-1
                if(i < j && j < k) { // j在i和k之间
                    dp[j][k] = dp[i][j] + 1;
                    max = Math.max(max, dp[j][k]);
                }
            }
        }
        return max == 0 ? 0 : max + 2;
    }

    private int search(int[] arr, int target, int l, int r){
        while(l <= r){
            int c = (r - l) / 2 + l;
            if(arr[c] == target) return c;
            if(arr[c] < target) l = c + 1;
            else r = c - 1;
        }
        return -1;
    }
}

【方法四: 动态规划 + 双指针】

由于arr严格递增,外层for的k从2开始每次前进一位,在每次确定arr[k]时,以i = 0, j = k - 1为左右指针寻找斐波那契关系。类似排序数组的两数之和的解法。本方法在四种方法中最快,只需61ms,击败81%。

class Solution {
    public int lenLongestFibSubseq(int[] arr) {
        int max = 0, n = arr.length;
        int[][] dp = new int[n - 1][n]; // i一定小于j,所以第一维大小可以设为n-1
        for(int k = 2; k < n; k++){
            int i = 0, j = k - 1;
            while(i < j){ // 确定arr[k],控制双指针i和j寻找斐波那契关系
                int sum = arr[i] + arr[j];
                if(sum == arr[k]) {
                    dp[j][k] = dp[i][j] + 1;
                    max = Math.max(max, dp[j][k]);
                    i++;
                    j--;
                }
                if(sum < arr[k]) i++;
                if(sum > arr[k]) j--;
            }
        }
        return max == 0 ? 0 : max + 2;
    }
}

剑指 Offer II 094. 最少回文分割

难度困难67

给定一个字符串 s,请将 s 分割成一些子串,使每个子串都是回文串。

返回符合要求的 最少分割次数

示例 1:

输入:s = "aab"
输出:1
解释:只需一次分割就可将 s 分割成 ["aa","b"] 这样两个回文子串。

示例 2:

输入:s = "a"
输出:0

示例 3:

输入:s = "ab"
输出:1

提示:

  • 1 <= s.length <= 2000
  • s 仅由小写英文字母组成
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值