刷题yyds
author: Zilin Xu
date: 2023.05.25
reference: Labuladong, Leetcode, ChatGPT etc.
This file records the questions of leetcode questions. The content is divided types of datasturctures and algorithms.
目录
第一章,基础数据结构
二分搜索
34. 在排序数组中查找元素的第一个和最后一个位置
class Solution {
public int[] searchRange(int[] nums, int target) {
return new int[]{left_bound(nums,target),right_bound(nums,target)};
}
//use binary search to find left and right bound of result
int left_bound(int[] nums, int target){
int left = 0, right = nums.length-1;
while(left <= right){
int mid = left + (right - left) / 2;
//which means target is in right half area of array
if(target > nums[mid]){
left = mid + 1;
}
else if(target < nums[mid]){
right = mid - 1;
}
//shrink right window to find most left index
//如果说,找到了target,但我们的while loop还没结束的话,说明这不是最左边的
//那么我们就要继续收缩右窗口
else if(target == nums[mid]){
right = mid -1;
}
}
//check if the left is valid
if(left >= nums.lengthnums[left] || != target){
return -1;
}
return left;
}
//same as above
int right_bound(int[] nums, int target){
int left = 0, right = nums.length - 1;
while(left <= right){
int mid = left + (right - left)/2;
if(nums[mid] == target){
left = mid + 1;
}
else if (nums[mid] > target){
right = mid - 1;
}
else if(nums[mid] < target){
left = mid + 1;
}
}
if (right < 0 || nums[right] != target){
return -1;
}
return right;
}
}
这道题目的要求是在一个排序数组中找到目标值的左右边界。由于数组是排序的,我们可以利用二分查找的思想来解决这个问题。
二分查找算法可以在排序数组中高效地查找目标值,时间复杂度为 O(log n),其中 n 是数组的长度。这种算法通过反复将查找范围缩小为一半来快速定位目标值。
在这段代码中,我们使用了两次二分查找来找到目标值的左右边界。具体思路如下:
-
左边界的查找:
- 使用二分查找方法找到目标值的位置。
- 如果找到目标值,我们继续向左收缩右边界,直到找到最左边的目标值。
- 如果最终左边界超出了数组范围或者左边界对应的值不等于目标值,说明数组中不存在目标值,返回 -1。
-
右边界的查找:
- 使用二分查找方法找到目标值的位置。
- 如果找到目标值,我们继续向右收缩左边界,直到找到最右边的目标值。
- 如果最终右边界越界(小于 0)或者右边界对应的值不等于目标值,说明数组中不存在目标值,返回 -1。
通过这种方法,我们可以找到目标值在排序数组中的左右边界。由于二分查找的时间复杂度较低,这种解决方案具有较好的效率。
该解决方案的时间复杂度是 O(log n),其中 n 是数组的长度。 这是因为解决方案使用了两次二分查找来找到目标值的左右边界。每次二分查找都会将搜索范围缩小一半,因此在最坏情况下,每次二分查找都需要遍历数组的一半,直到找到目标值或者搜索范围为空。所以每次二分查找的时间复杂度是 O(log n)。 整体的解决方案包含两次二分查找,因此时间复杂度是 O(log n) + O(log n) = O(log n)。 对于空间复杂度,该解决方案只使用了常数级别的额外空间,因此空间复杂度是 O(1)。 这是因为解决方案并没有使用任何与输入数组大小相关的额外空间。它只使用了一些变量来存储搜索范围的左右边界、中间位置等信息,这些变量的数量是固定的,与输入数组的大小无关。因此,空间复杂度是 O(1)。
704. 二分搜索
class Solution {
public int search(int[] nums, int target) {
int left = 0, right = nums.length -1;
while(left <= right){
int mid = left + (right - left)/2;
if(nums[mid] == target){
return mid;
}
else if (nums[mid] < target){
left = mid + 1;
}
else if(nums[mid] > target){
right = mid - 1;
}
}
return -1;
}
}
1、为什么 while 循环的条件中是 <=,而不是 < ?
答:因为初始化 right 的赋值是 nums.length - 1,即最后一个元素的索引,而不是 nums.length。
这二者可能出现在不同功能的二分查找中,区别是:前者相当于两端都闭区间 [left, right],后者相当于左闭右开区间 [left, right)。因为索引大小为 nums.length 是越界的,所以我们把 right 这一边视为开区间。
我们这个算法中使用的是前者 [left, right] 两端都闭的区间。这个区间其实就是每次进行搜索的区间。
什么时候应该停止搜索呢?当然,找到了目标值的时候可以终止:
if(nums[mid] == target)
return mid;
但如果没找到,就需要 while 循环终止,然后返回 -1。那 while 循环什么时候应该终止?搜索区间为空的时候应该终止,意味着你没得找了,就等于没找到嘛。
while(left <= right) 的终止条件是 left == right + 1,写成区间的形式就是 [right + 1, right],或者带个具体的数字进去 [3, 2],可见这时候区间为空,因为没有数字既大于等于 3 又小于等于 2 的吧。所以这时候 while 循环终止是正确的,直接返回 -1 即可。
while(left < right) 的终止条件是 left == right,写成区间的形式就是 [right, right],或者带个具体的数字进去 [2, 2],这时候区间非空,还有一个数 2,但此时 while 循环终止了。也就是说区间 [2, 2] 被漏掉了,索引 2 没有被搜索,如果这时候直接返回 -1 就是错误的。
当然,如果你非要用 while(left < right) 也可以,我们已经知道了出错的原因,就打个补丁好了:
//...
while(left < right) {
// ...
}
return nums[left] == target ? left : -1;
2、为什么 left = mid + 1,right = mid - 1?我看有的代码是 right = mid 或者 left = mid,没有这些加加减减,到底怎么回事,怎么判断?
答:这也是二分查找的一个难点,不过只要你能理解前面的内容,就能够很容易判断。
刚才明确了「搜索区间」这个概念,而且本算法的搜索区间是两端都闭的,即 [left, right]。那么当我们发现索引 mid 不是要找的 target 时,下一步应该去搜索哪里呢?
当然是去搜索区间 [left, mid-1] 或者区间 [mid+1, right] 对不对?因为 mid 已经搜索过,应该从搜索区间中去除。
3、此算法有什么缺陷?
答:至此,你应该已经掌握了该算法的所有细节,以及这样处理的原因。但是,这个算法存在局限性。
比如说给你有序数组 nums = [1,2,2,2,3],target 为 2,此算法返回的索引是 2,没错。但是如果我想得到 target 的左侧边界,即索引 1,或者我想得到 target 的右侧边界,即索引 3,这样的话此算法是无法处理的。
这样的需求很常见,你也许会说,找到一个 target,然后向左或向右线性搜索不行吗?可以,但是不好,因为这样难以保证二分查找对数级的复杂度了。
我们后续的算法就来讨论这两种二分查找的算法。
TC: O(log n)
剑指 Offer 53 - I. 在排序数组中查找数字 I
class Solution {
public int search(int[] nums, int target) {
//same with 34, find left bound and right bound
//get left and right bound
int left = left_bound(nums, target);
//if left index is -1, means we didn't have target in the array
if(left == -1){
return 0;
}
int right = right_bound(nums,target);
return right - left + 1;
}
int left_bound(int[] nums, int target){
int left = 0, right = nums.length - 1;
while(left <= right){
int mid = left + (right - left)/2;
if(nums[mid] == target){
right = mid - 1;
}
else if (nums[mid] < target){
left = mid + 1;
}
else if (nums[mid] > target){
right = mid - 1;
}
}
if(left > nums.length - 1 || nums[left] != target){
return -1;
}
return left;
}
int right_bound(int[] nums, int target){
int left = 0, right = nums.length - 1;
while(left <= right){
int mid = left + (right - left)/2;
if(nums[mid] == target){
left = mid + 1;
}
else if(nums[mid] < target){
left = mid + 1;
}
else if(nums[mid] > target){
right = mid - 1;
}
}
if(right < 0 || nums[right] != target){
return -1;
}
return right;
}
}
和Leetcode 34 类似,我们先找到left bound 和 right bound, 在主函数中我们要判断left bound是不是 -1。
35. 搜索插入位置
class Solution {
public int searchInsert(int[] nums, int target) {
//找到left bound
return left_bound(nums, target);
}
int left_bound(int[] nums, int target){
int left = 0, right = nums.length - 1;
while(left <= right){
int mid = left + (right - left)/2;
if (nums[mid] == target){
right = mid - 1;
}
else if(nums[mid] < target){
left = mid + 1;
}
else if(nums[mid] > target){
right = mid - 1;
}
}
//直接返回
return left;
}
}
基本思路
这道题就是考察搜索左侧边界的⼆分算法的细节理解,前⽂ ⼆分搜索详解 着重讲了数组中存在⽬标元素重复
的情况,没仔细讲⽬标元素不存在的情况。
当⽬标元素 target 不存在数组 nums 中时,搜索左侧边界的⼆分搜索的返回值可以做以下⼏种解读:
1、返回的这个值是 nums 中⼤于等于 target 的最⼩元素索引。
2、返回的这个值是 target 应该插⼊在 nums 中的索引位置。
3、返回的这个值是 nums 中⼩于 target 的元素个数。
⽐如在有序数组 nums = [2,3,5,7] 中搜索 target = 4,搜索左边界的⼆分算法会返回 2,你带⼊上⾯
的说法,都是对的。
所以以上三种解读都是等价的,可以根据具体题⽬场景灵活运⽤,显然这⾥我们需要的是第⼆种。
240. 搜索二维矩阵 II
class Solution {
public boolean searchMatrix(int[][] matrix, int target) {
//binary search,从右上角开始
int i = 0, j = matrix[0].length -1;
while(i < matrix.length && j >= 0 ){
if(matrix[i][j] == target){
return true;
}
if(matrix[i][j] < target){
i ++;
}
else{
j --;
}
}
return false;
}
}
基本思路
作为 74. 搜索⼆维矩阵,更像 ⼀个⽅法秒杀所有 N 数之和问题,因为它们的思想上有些类似。
这道题说 matrix 从上到下递增,从左到右递增,显然左上⻆是最⼩元素,右下⻆是最⼤元素。我们如果想
⾼效在 matrix 中搜索⼀个元素,肯定需要从某个⻆开始,⽐如说从左上⻆开始,然后每次只能向右或向下
移动,不要⾛回头路。
如果真从左上⻆开始的话,就会发现⽆论向右还是向下⾛,元素⼤⼩都会增加,那么到底向右还是向下?不
确定,那只好⽤类似 动态规划算法 的思路穷举了。
但实际上不⽤这么麻烦,我们不要从左上⻆开始,⽽是从右上⻆开始,规定只能向左或向下移动。
你注意,如果向左移动,元素在减⼩,如果向下移动,元素在增⼤,这样的话我们就可以根据当前位置的元
素和 target 的相对⼤⼩来判断应该往哪移动,不断接近从⽽找到 target 的位置。
时间复杂度: 在最坏情况下,即当目标值位于矩阵的左下角时,算法的时间复杂度为O(m+n),其中m为矩阵的行数,n为矩阵的列数。这是因为每次比较都会使得行索引i增加1或列索引j减少1,而最多进行m+n次比较即可找到目标值或确定其不存在。
空间复杂度: 该算法的空间复杂度为O(1),因为除了使用常量级别的变量i、j和输入的matrix外,没有使用额外的空间。
74. 搜索二维矩阵
class Solution {
public boolean searchMatrix(int[][] matrix, int target) {
//把这个二维数组当作一位数组来看
int m = matrix.length;
int n = matrix[0].length;
int left = 0, right = m * n - 1;
//start loop
while(left <= right){
//这时候的mid意思是,matrix中第几个元素(从0开始数)
int mid = left + (right - left)/2;
//找到当左右边界为left, right的时候,对应到matrix里面元素的位置
int i = mid / n;
int j = mid % n;
//start finding
if(matrix[i][j] == target){
return true;
}else if(matrix[i][j] < target){
left = mid + 1;
}else {
right = mid - 1;
}
}
return false;
}
}
时间复杂度分析:
- 获取矩阵的行数和列数的操作的时间复杂度为 O(1),因为它们只需要访问矩阵的常量个元素。
- 在二分搜索过程中,每次循环将搜索范围减半,因此最多需要进行 O(log(m * n)) 次迭代。
- 在每次迭代中,计算中间索引以及获取对应的行索引和列索引的操作都只需要常量时间 O(1)。
- 综上所述,整个搜索过程的时间复杂度为 O(log(m * n))。
空间复杂度分析:
- 空间复杂度取决于所使用的额外空间。在该算法中,除了输入的矩阵和几个整数变量之外,没有使用额外的空间。
- 因此,额外空间的使用为 O(1),是常量级别的。
392-判断子序列
class Solution {
public boolean isSubsequence(String s, String t) {
//双指针
int i = 0, j = 0;
while(i < s.length() && j < t.length()){
if(s.charAt(i) == t.charAt(j)){
i ++;
}
j ++;
}
return i == s.length();
}
}
该解决方案使用了双指针方法来判断字符串s是否为字符串t的子序列。下面是对时间和空间复杂度的分析:
时间复杂度分析: 该算法的时间复杂度取决于字符串s和t的长度,分别为s.length()和t.length()。在最坏的情况下,指针i和j都需要遍历整个字符串s和t,因此时间复杂度为O(s.length() + t.length())。
空间复杂度分析: 该算法的空间复杂度是O(1),因为它只使用了常数级别的额外空间。无论输入的字符串s和t有多长,算法所使用的额外空间都保持不变。
综上所述,该算法的时间复杂度为O(s.length() + t.length()),空间复杂度为O(1)。
658.找到 K 个最接近的元素
class Solution {
public List<Integer> findClosestElements(int[] arr, int k, int x) {
//find left bound position of x
int p = left_bound(arr,x);
int left = p - 1, right = p;
//result linkdedlist
LinkedList<Integer> res = new LinkedList<>();
while(right - left - 1 < k){
if(left == -1){
res.addLast(arr[right]);
right ++;
}else if(right == arr.length){
res.addFirst(arr[left]);
left --;
}else if(x - arr[left] > arr[right] - x){
res.addLast(arr[right]);
right ++;
}else{
res.addFirst(arr[left]);
left --;
}
}
return res;
}
//找到左侧边界
int left_bound(int[] arr, int target){
int left = 0, right = arr.length - 1;
while(left < right){
int mid = left + (right - left)/2;
if(arr[mid] == target){
right = mid;
}else if(arr[mid] < target){
//find on right part
left = mid + 1;
}else if(arr[mid] > target){
right = mid;
}
}
return left;
}
}
为什么要用LinkedList?
因为题目要求结果也是升序排列,所以我们要用到addFirst和addLast
时间复杂度分析:
在
left_bound方法中,使用二分查找找到左侧边界位置,时间复杂度为 O(log n),其中 n 是数组arr的长度。在
findClosestElements方法中,循环执行的次数为 k,每次循环的操作都是常数时间复杂度,因此循环的时间复杂度为 O(k)。
- 在每次循环中,执行插入操作
res.addLast(arr[right])或res.addFirst(arr[left]),插入操作的时间复杂度为 O(1)。- 执行
right++和left--的操作也是常数时间复杂度。- 因此,整个
while循环的时间复杂度为 O(k)。综上所述,算法的总时间复杂度为 O(log n + k)。
空间复杂度分析:
- 空间复杂度主要取决于存储结果的链表
res,以及方法中使用的常数级别的额外空间。- 结果链表
res的空间复杂度为 O(k),因为最多存储 k 个元素。- 其他常数级别的额外空间的使用不会随输入规模的增加而增加,可以忽略不计。
综上所述,算法的总空间复杂度为 O(k)。
852. 山脉数组的峰顶索引
class Solution {
public int peakIndexInMountainArray(int[] arr) {
//binary search
int left = 0, right = arr.length - 1;
while(left < right){
int mid = left + (right - left)/2;
if(arr[mid] > arr[mid + 1]){
//means we should find on left part
right = mid;
}else{
left = mid + 1;
}
}
return left;
}
}
剑指 Offer 53 - II. 0~n-1中缺失的数字
class Solution {
public int missingNumber(int[] nums) {
//binary search
int left = 0, right = nums.length - 1;
while(left <= right){
int mid = left + (right - left)/2;
if(nums[mid] > mid){
//means the missing number is on the left part
right = mid - 1;
}else{
//means right part
left = mid + 1;
}
}
return left;
}
}
这道题考察二分查找算法。常规的二分搜索让你在 nums 中搜索目标值 target,但这道题没有给你一个显式的 target,怎么办呢?
其实,二分搜索的关键在于,你是否能够找到一些规律,能够在搜索区间中一次排除掉一半。比如让你在 nums 中搜索 target,你可以通过判断 nums[mid] 和 target 的大小关系判断 target 在左边还是右边,一次排除半个数组。
所以这道题的关键是,你是否能够找到一些规律,能够判断缺失的元素在哪一边?
其实是有规律的,你可以观察 nums[mid] 和 mid 的关系,如果 nums[mid] 和 mid 相等,则缺失的元素在右半边,如果 nums[mid] 和 mid 不相等,则缺失的元素在左半边。
在二分查找算法中,有一种常见的应用是搜索左侧边界的二分查找,我们可以借鉴这种思路来定位缺失的元素的位置。
滑动窗口
3. 无重复字符的最长子串
class Solution {
public int lengthOfLongestSubstring(String s) {
//Java solution
int n = s.length();
int left = 0, right = 0;
int result = 0;
HashSet<Character> set = new HashSet<>();
//start sliding
while(right < n){
//add the character into window if it is not already in window
char c = s.charAt(right);
char d = s.charAt(left);
if(!set.contains(c)){
set.add(c);
//update the result with the window size
result = Math.max(result,right - left + 1);
right ++;
}else{
//means set has c, shrink the window
set.remove(d);
left ++;
}
}
return result;
}
}
具体流程如下:
-
初始化一个空的 HashSet,用于存储当前窗口中的字符。
-
每次右指针右移时,判断当前字符是否已经存在于 HashSet 中:
- 如果不存在,将当前字符加入 HashSet,并继续向右移动右指针。
- 如果存在,说明出现了重复字符,需要移动左指针来缩小窗口的大小,同时从 HashSet 中移除左指针对应的字符,继续右移右指针。
-
在每次移动右指针或左指针后,都可以通过计算右指针和左指针之间的距离来更新最长子串的长度。
-
重复上述步骤,直到右指针达到字符串的末尾。
这个算法的时间复杂度是 O(n),其中 n 是字符串的长度。在最坏的情况下,我们需要遍历整个字符串一次。
空间复杂度是 O(min(n, m)),其中 n 是字符串的长度,m 是字符集的大小(在这个问题中为 256,即 ASCII 字符集的大小)。在 HashSet 中最多存储 m 个字符,即字符集的大小。
因此,该算法使用了线性的时间复杂度和额外的空间来存储 HashSet,以判断字符是否重复。
438. 找到字符串中所有字母异位词
class Solution {
public List<Integer> findAnagrams(String s, String p) {
int left = 0, right = 0;
int valid = 0;
List<Integer> res = new ArrayList<>();
//corner case
if(p.length() > s.length()){
return res;
}
//one map for window, one for our need
Map<Character, Integer> need = new HashMap<>();
Map<Character, Integer> window = new HashMap<>();
for(char c: p.toCharArray()){
need.put(c, need.getOrDefault(c,0) + 1);
}
while(right < s.length()){
char rightChar = s.charAt(right);
window.put(rightChar, window.getOrDefault(rightChar, 0) + 1);
//update valid
if(need.containsKey(rightChar) && need.get(rightChar).equals(window.get(rightChar))){
valid ++;
}
//当窗口大小等于我们p的长度的时候,我们就要收缩窗口并检查是不是异位词
if(right - left + 1 == p.length()){
//check if we are going to update result
if(valid == need.size()){
res.add(left);
}
char leftChar = s.charAt(left);
//update valid
if(need.containsKey(leftChar) && need.get(leftChar).equals(window.get(leftChar))){
valid --;
}
window.put(leftChar, window.get(leftChar) - 1);
left ++;
}
right ++;
}
return res;
}
}
为什么要用equals,不能用==么
在 Java 中,对于 Integer 类型的对象比较,不能直接使用 == 运算符,而是要使用 equals() 方法来进行比较。
这是因为 == 运算符在比较引用类型时,比较的是对象的引用地址,而不是对象的内容。而 equals() 方法被重写,用于比较对象的内容。
在上述代码中,windowCount.get(rightChar) 返回的是一个 Integer 对象,而 pCount.get(rightChar) 也返回一个 Integer 对象。我们需要比较这两个对象的内容是否相等,而不仅仅是比较它们的引用地址。
因此,需要使用 equals() 方法来判断这两个 Integer 对象的内容是否相等。如果内容相等,表示当前字符在窗口中的出现次数与 p 中的出现次数相同,我们才将 matched 计数器加一。
如果使用 == 运算符进行比较,将会比较两个 Integer 对象的引用地址,而不是它们的内容,这不符合我们的需求。
所以,在这种情况下,我们应该使用 equals() 方法来进行比较,以确保比较的是对象的内容。
希望这样的解释能够帮助你理解为什么要使用 equals() 方法而不是 == 运算符。如果还有其他问题,请随时提问。
我们什么时候要收缩window呢
具体来说,当窗口的大小等于字符串 p 的长度时,我们需要检查当前窗口是否是一个字母异位词。如果是,我们将窗口的左指针添加到结果列表中。无论是否是字母异位词,我们都需要收缩窗口。
在这个算法中,时间复杂度和空间复杂度如下:
时间复杂度:
- 遍历字符串
p并构建need哈希表的时间复杂度为 O(p),其中 p 是字符串p的长度。- 在滑动窗口的过程中,我们遍历字符串
s一次,每个字符只遍历一次,因此时间复杂度为 O(s),其中 s 是字符串s的长度。- 综合起来,算法的总时间复杂度为 O(p + s)。
空间复杂度:
- 需要额外的空间来存储
need和window两个哈希表。在最坏的情况下,字符串p中的字符都是唯一的,因此需要存储p中的每个字符及其出现次数,所以need哈希表的空间复杂度为 O(p)。- 窗口的大小最多为字符串
p的长度,因此window哈希表的空间复杂度也为 O(p)。- 综合起来,算法的总空间复杂度为 O(p)。
综上所述,该算法的时间复杂度为 O(p + s),空间复杂度为 O(p)。
567. 字符串的排列
class Solution {
public boolean checkInclusion(String s1, String s2) {
//滑动窗口
int left = 0, right = 0;
int valid = 0;
Map<Character, Integer> need = new HashMap<>();
Map<Character, Integer> window = new HashMap<>();
//corner case
if(s1.length() > s2.length()){
return false;
}
//fill in the need
for(char c: s1.toCharArray()){
need.put(c, need.getOrDefault(c,0) + 1);
}
//sliding
while(right < s2.length()){
char rightChar = s2.charAt(right);
window.put(rightChar, window.getOrDefault(rightChar,0) + 1);
//update valid
if(need.containsKey(rightChar) && need.get(rightChar).equals(window.get(rightChar))){
valid ++;
}
//if the length of window is length of s1, check and shrink
if(right - left + 1 == s1.length()){
if(valid == need.size()){
return true;
}
char leftChar = s2.charAt(left);
if(need.containsKey(leftChar) && need.get(leftChar).equals(window.get(leftChar))){
valid --;//update valid
}
window.put(leftChar,window.get(leftChar) - 1);
left ++;
}
//move right
right ++;
}
return false;
}
}
注意
再想到收缩窗口的同时,也要注意在if语句中检查是否valid然后决定return的值
在这个算法中,时间复杂度和空间复杂度如下:
时间复杂度:
- 填充
need哈希表的时间复杂度为 O(s1),其中 s1 是字符串s1的长度。- 在滑动窗口的过程中,我们遍历字符串
s2一次,每个字符只遍历一次,因此时间复杂度为 O(s2),其中 s2 是字符串s2的长度。- 综合起来,算法的总时间复杂度为 O(s1 + s2)。
空间复杂度:
- 需要额外的空间来存储
need和window两个哈希表。在最坏的情况下,字符串s1中的字符都是唯一的,因此需要存储s1中的每个字符及其出现次数,所以need哈希表的空间复杂度为 O(s1)。- 窗口的大小最多为字符串
s1的长度,因此window哈希表的空间复杂度也为 O(s1)。- 综合起来,算法的总空间复杂度为 O(s1)。
综上所述,该算法的时间复杂度为 O(s1 + s2),空间复杂度为 O(s1)。
希望这个分析对你有所帮助。如果你还有其他问题,请随时提问。
76. 最小覆盖子串
class Solution {
public String minWindow(String s, String t) {
int left = 0, right = 0;
//the start of the string
int start = 0;
int valid = 0;
//the length of the string
int len = Integer.MAX_VALUE;
Map<Character, Integer> need = new HashMap<>();
Map<Character, Integer> window = new HashMap<>();
for (char c : t.toCharArray()) {
need.put(c, need.getOrDefault(c, 0) + 1);
}
while (right < s.length()) {
char rightChar = s.charAt(right);
right++;
//if the char is what we need, we put it into the window
if (need.containsKey(rightChar)) {
window.put(rightChar, window.getOrDefault(rightChar, 0) + 1);
//update valid
if (need.get(rightChar).equals(window.get(rightChar))) {
valid++;
}
}
//if we find the window is valid, we shrink the window
while (valid == need.size()) {
char leftChar = s.charAt(left);
//update len and start
if (right - left + 1 < len) {
start = left;
len = right - left + 1;
}
left++;
if (need.containsKey(leftChar)) {
//update valid
if (need.get(leftChar).equals(window.get(leftChar))) {
valid--;
}
//move the element out of the window
window.put(leftChar, window.get(leftChar) - 1);
}
}
}
return len == Integer.MAX_VALUE ? "" : s.substring(start, start + len - 1);
}
}
注意
- Java中substring用法。
- 这道题中window只要添加符合要求的char就行。
上面提供的代码的时间复杂度为O(n),其中n是输入字符串
s的长度。这是因为代码使用两个指针(left和right)遍历字符串s,并执行依赖于s长度的操作。在最坏情况下,内部的while循环可能运行整个字符串s的长度,导致线性时间复杂度。代码的空间复杂度为O(m),其中m是输入字符串
t中不同字符的数量。这是因为代码使用两个哈希映射(need和window)来存储字符串t中字符的频率以及当前窗口s中的字符。哈希映射所需的空间取决于字符串t中不同字符的数量,即m。因此,代码的总体时间复杂度为O(n),空间复杂度为O(m)。
239. 滑动窗口最大值
class Solution {
public int[] maxSlidingWindow(int[] nums, int k) {
Monotonicqueue window = new Monotonicqueue();
List<Integer> res = new ArrayList<>();
for(int i = 0; i < nums.length; i ++){
if(i < k - 1){//add first k-2 elements
window.push(nums[i]);
}else{
window.push(nums[i]);
res.add(window.max());
//用得到这句话的情况有[最大的,比这个小的其他元素],我们需要把最大的去掉
window.pop(nums[i - k + 1]);
}
}
int[] result = new int[res.size()];
for(int i = 0; i < result.length; i ++){
result[i] = res.get(i);
}
return result;
}
}
class Monotonicqueue{
LinkedList<Integer> maxq = new LinkedList<>();
//保证队列的头是最大的元素,并且先进先出
public void push(int n){
while(!maxq.isEmpty() && maxq.getLast() < n){
maxq.pollLast();//把小的元素都去掉,没用了
}
maxq.addLast(n);
}
public int max(){
return maxq.getFirst();
}
public void pop(int n){
//我们的window要么是有且只有一个最大的元素,要么就是[最大的,比这个小的其他元素]
if(maxq.getFirst() == n){
maxq.pollFirst();
}
}
}
为什么要用单调队列
保证了队列始终维持递减的顺序,并且可以在 O(1) 的时间复杂度内获取当前队列的最大值。这样,在一系列元素的动态更新过程中,我们可以高效地跟踪最大值的变化。
- 时间复杂度:整个算法的时间复杂度是 O(n),其中 n 是数组
nums的长度。遍历一次数组需要 O(n) 的时间,每个元素最多进出队列一次,而队列的操作复杂度是 O(1)。所以总体时间复杂度是 O(n)。- 空间复杂度:算法使用了一个
res列表来存储结果,其最大长度是 n-k+1,所以空间复杂度是 O(n-k+1)。另外,Monotonicqueue类内部使用了一个双向链表来存储元素,其长度最大不超过 k,所以空间复杂度是 O(k)。综合起来,算法的空间复杂度是 O(max(n-k+1, k))。因此,这段代码实现了一个时间复杂度为 O(n),空间复杂度为 O(max(n-k+1, k)) 的滑动窗口最大值算法。
其他题目
26. 删除有序数组中的重复项
class Solution {
public int removeDuplicates(int[] nums) {
//双指针
if(nums.length == 0){
return 0;
}
int slow = 0, fast = 0;
while(fast < nums.length){
if(nums[slow] != nums[fast]){
nums[slow + 1] = nums[fast];
slow ++;
}
fast ++;
}
return slow + 1;
}
}
这道题的思路就是,双指针遍历:如果我遇到相同元素(包括最开始两个指针都在0号位的情况),我就只动fast;如果不同,就说明slow+1的元素应该是我fast元素。
这段代码实现了一个时间复杂度为 O(n),空间复杂度为 O(1) 的移除有序数组重复元素的算法。
27. 移除元素
class Solution {
public int removeElement(int[] nums, int val) {
//双指针
int slow = 0, fast = 0;
while(fast < nums.length){
if(nums[fast] != val){
nums[slow] = nums[fast];
slow ++;
}
fast ++;
}
return slow;
}
}
这道题的思路是, 如果fast != val,就说明这是我们要的值,把它给slow并且移动slow。如果fast == val,那么只要动fast。
283. 移动零
class Solution {
public void moveZeroes(int[] nums) {
int fast = 0, slow = 0;
while(fast < nums.length){
if(nums[fast] != 0){
nums[slow] = nums[fast];
slow ++;
}
fast ++;
}
while(slow < nums.length){
nums[slow] = 0;
slow ++;
}
}
}
和上一道题一样,只不过后面多了一个加0的步骤
83. 删除排序链表中的重复元素
class Solution {
public ListNode deleteDuplicates(ListNode head) {
ListNode slow = head, fast = head;
while(fast != null){
if(slow.val != fast.val){
slow.next = fast;
slow = fast;
}
fast = fast.next;
}
slow.next = null;
return head;
}
}
注意
slow.next = null; 这句话少不了
这段代码实现了一个时间复杂度为 O(n),空间复杂度为 O(1) 的删除链表中重复元素的算法。
剑指 Offer 21. 调整数组顺序使奇数位于偶数前面
class Solution {
public int[] exchange(int[] nums) {
int slow = 0, fast = 0 ;
while(fast < nums.length){
if(nums[fast] % 2 == 1){
int temp = nums[fast];
nums[fast] = nums[slow];
nums[slow] = temp;
slow ++;
}
fast ++;
}
return nums;
}
}
解题思路
如果遇到奇数就和slow调换位置
剑指 Offer 57. 和为s的两个数字
class Solution {
public int[] twoSum(int[] nums, int target) {
int left = 0 , right = nums.length - 1;
//左右指针
while(left < right){
//get the sum
int sum = nums[left] + nums[right];
if( sum == target){
return new int[]{nums[left],nums[right]};
}else if(sum < target){
left ++;
}else{
right --;
}
}
return null;
}
}
时间复杂度: O(N)
82. 删除排序链表中的重复元素 II
class Solution {
public ListNode deleteDuplicates(ListNode head) {
ListNode dummy = new ListNode(-1);
ListNode slow = dummy, fast = head;
while(fast != null){
if(fast.next != null && fast.next.val == fast.val){
while(fast.next != null && fast.next.val == fast.val){
fast = fast.next;
}
fast = fast.next;
if(fast == null){
slow.next = fast;
}
}else{
slow.next = fast;
fast = fast.next;
slow = slow.next;
}
}
return dummy.next;
}
}
注意
- 这个题目是要把重复的元素去掉,也就是不留下来。所以当我们发现重复元素时,需要多加一个while,然后下一次走if的时候保证fast指针在重复元素的next;
- 不排除一种情况是 1 2 2 2, 这样子的话需要在判断里面多加个if来让slow指向null
给定的代码是用于删除链表中重复元素的问题。以下是对代码的复杂度分析:
时间复杂度:
- 初始化 dummy 节点和两个指针 slow 和 fast:O(1)。
- while 循环:最坏情况下,需要遍历整个链表,即 O(n),其中 n 是链表的长度。
- 在 while 循环中,执行常数时间的操作:比较节点值,更新指针。
- 内层 while 循环(用于跳过重复元素)的总体时间复杂度为 O(k),其中 k 是重复元素的数量,但 k <= n。
- 总体时间复杂度为 O(n)。
空间复杂度:
- 创建了一个 dummy 节点:O(1)。
- 使用了两个指针 slow 和 fast:O(1)。
- 没有使用额外的数据结构,只使用了常数级的额外空间。
综上所述,给定代码的时间复杂度为 O(n),空间复杂度为 O(1)。
986. 区间链表的交集
class Solution {
public int[][] intervalIntersection(int[][] firstList, int[][] secondList) {
//define result list
List<int[]> res = new ArrayList<>();
//define two pointers
int i = 0, j = 0;
while (i < firstList.length && j < secondList.length) {
int start_i = firstList[i][0], end_i = firstList[i][1];
int start_j = secondList[j][0], end_j = secondList[j][1];
if (end_i >= start_j && end_j >= start_i) {
res.add(new int[] {
Math.max(start_i, start_j), Math.min(end_i, end_j)
});
}
//move the pointer
if(end_j > end_i){
i ++;
} else {
j ++;
}
}
return res.toArray(new int[0][0]);
}
}
把所有的情况都画出来就可以得到条件
时间复杂度(TC):O(M + N)
- 其中,M表示第一个区间列表(firstList)的长度,N表示第二个区间列表(secondList)的长度。
- 在算法中,我们使用两个指针(i和j)分别遍历两个区间列表,最坏情况下,我们可能需要遍历整个列表,因此时间复杂度是线性的,即O(M + N)。
空间复杂度(SC):O(1)
- 空间复杂度通常用来描述算法的额外空间使用,不包括输入的空间。在这个算法中,除了结果列表(res)之外,我们没有使用额外的数据结构或分配额外的空间。
- 结果列表(res)用于存储交集,但不会超过输入列表的长度,因此它的空间复杂度可以视为常数级别,即O(1)。
总结:这个算法的时间复杂度是O(M + N),其中M和N分别是两个输入列表的长度。空间复杂度是O(1),因为除了结果列表外,没有使用其他额外的数据结构。
上岸进行时!
-
Dynamic programming
-
回溯算法
-
ListNode
-
other algorithms
- [presum](# presum)
- 力扣384 打乱数组
-
array
-
sliding window
-
stack
-
dfs
-
bfs
-
Binary tree
- 力扣100 相同的树
- 力扣572 另一棵树的子数
- 力扣102 二叉树层序遍历
- 力扣103 二叉树的矩形层序遍历
- 力扣1161 最大层内元素和
- 力扣1302 层数最深的叶子结点之和
- 力扣1609 奇偶树
- 力扣637 二叉树层的平均值
- 力扣958 二叉树完全性验证
- 力扣104 二叉树最大深度
- 力扣114 前序遍历
- 力扣543 二叉树直径
- 力扣559 N叉树最大深度
- 力扣105 从前序和中序生成二叉树
- 力扣106 中后序生成二叉树
- 力扣654 最大二叉树
- 力扣111 二叉树最小深度
- 力扣114 将二叉树展开为链表
- 力扣116 填充每一个节点的右侧节点
- 力扣226 反转二叉树
- 力扣117 为每一个节点填充nextll
- 力扣145 后序遍历
- 力扣222 完全二叉树节点个数
- 力扣297 二叉树序列化和反序列化
- 力扣124 二叉树中最大路径和
- 力扣687 最长相同路径
- 力扣814 二叉树剪枝
- 力扣1325 删除给定值的叶子结点
- 力扣589 N叉树前序遍历
- 力扣652 寻找重复的子树
- 力扣965 单值二叉树
- 力扣255 验证前序遍历是否是BST
- 力扣450 删除BST中节点
- 力扣700 BST搜索
- 力扣701 插入BST
- 力扣98 验证BST
- 力扣1038 从BST得到累加树
- 力扣230 BST中第K小的元素
- 剑指54 BST中第K大的元素
- 力扣530 BST最小绝对差
- 力扣270 最接近的BST的值
- 力扣285 BST的中序后继
Leetcode 931
class Solution {
public int minFallingPathSum(int[][] matrix) {
//dp[]: record the minFallingPathSum from last level of matrix
int m = matrix.length, n = matrix.length;
int [][] dp = new int[m][m];
//fill the first level of dp[][]: the original value of matrix
for(int i = 0; i < m; i ++){
dp[0][i] = matrix[0][i];
}
//fill th rest level of dp[][], we have three cases
for(int i = 1; i < m; i ++){
for (int j = 0; j < n; j ++){
//first case, left column of matrix
if(j == 0){
dp[i][j] = Math.min((dp[i-1][j] + matrix[i][j]),(dp[i-1][j+1])+matrix[i][j] ) ;
}
//second case: right column of matrix
else if(j == n-1){
dp[i][j] =Math.min((matrix[i][j]+dp[i-1][j-1]),(matrix[i][j]+dp[i-1][j]));
}
//normal case: in the middle: min of three
else{
dp[i][j] = min_value((matrix[i][j]+dp[i-1][j-1]),(matrix[i][j]+dp[i-1][j]),(matrix[i][j]+ dp[i-1][j+1]));
}
}
}
//answer is the min of last level of dp
int res = Integer.MAX_VALUE;
for(int i = 0; i <m; i ++){
res = Math.min(res,dp[n-1][i]);
}
return res;
}
int min_value(int a, int b, int c){
int res = Math.min(a,b);
return Math.min(res,c);
}
}
更优的解法:
class Solution {
public int minFallingPathSum(int[][] A) {
int n = A.length;
int m = A[0].length;
int[] dp = new int[m];
// 初始化 dp 数组第一行
for(int j = 0; j < m; j++){
dp[j] = A[0][j];
}
for(int i = 1; i < n; i++){
//每次都有一个新的dp数组
int[] newDp = new int[m];
for(int j = 0; j < m; j++){
if(j == 0){
//从dp[]里面拿数据类似于上面二维dp的上一层
newDp[j] = A[i][j] + Math.min(dp[j], dp[j+1]);
} else if(j == m-1){
newDp[j] = A[i][j] + Math.min(dp[j-1], dp[j]);
} else {
newDp[j] = A[i][j] + Math.min(dp[j-1], Math.min(dp[j], dp[j+1]));
}
}
dp = newDp;
}
int minSum = Integer.MAX_VALUE;
for(int j = 0; j < m; j++){
minSum = Math.min(minSum, dp[j]);
}
return minSum;
}
}
在这个解法中,我们使用一个长度为 m 的一维数组 dp 来记录到达当前行每个位置时的最小路径和,初始化 dp 数组为矩阵 A 的第一行。在遍历每一行时,我们使用一个新的一维数组 newDp 来记录更新后的最小路径和,然后将其赋值给 dp 数组,继续处理下一行。在遍历完 dp 数组最后一行时,我们遍历 dp 数组,找出最小值即为所求的最小路径和。
由于我们只需要保存前一行的 dp 值来更新当前行的 dp 值,因此在遍历每一行时,我们可以使用一个新的一维数组来保存更新后的 dp 值。这样我们就可以将空间复杂度降低到 O(m)。
优化后的算法时间复杂度为 ,其中 是矩阵的行数, 是矩阵的列数。
Leetcode 72 编辑距离
题目描述:
给你两个单词 word1 和 word2,请你找到使得 word1 转换成 word2 所使用的最少操作数的操作次数。
你可以对一个单词进行如下三种操作:
插入一个字符 删除一个字符 替换一个字符
示例 1:
输入:word1 = "horse", word2 = "ros" 输出:3 解释: horse -> rorse (将 'h' 替换为 'r') rorse -> rose (删除 'r') rose -> ros (删除 'e')
示例 2:
输入:word1 = "intention", word2 = "execution" 输出:5 解释: intention -> inention (删除 't') inention -> enention (将 'i' 替换为 'e') enention -> exention (将 'n' 替换为 'x') exention -> exection (将 'n' 替换为 'c') exection -> execution (插入 'u')
class Solution {
public int minDistance(String word1, String word2) {
//dp[]: 记录从前往后的最少编辑距离
int m = word1.length();
int n = word2.length();
int[][] dp = new int[m+1][n+1];
//把dp的第一行和第一列填满,填上word1和word2对应的length
for(int i =0; i <= n; i ++){
dp[0][i] = i;
}
for(int i = 0; i <= m; i ++){
dp[i][0] = i;
}
//fill in dp
for(int i = 1 ; i <= m; i ++){
for(int j = 1; j <= n; j ++){
if(word1.charAt(i-1) == word2.charAt(j-1)){
dp[i][j] = dp[i-1][j-1]; //因为不用变了
}
else{
dp[i][j] = Math.min(dp[i-1][j-1],Math.min(dp[i-1][j],dp[i][j-1]))+1;
}
}
}
return dp[m][n];
}
}
将word1放在dp的第一行, word2放到dp的第一列。注意一开始放的时候条件要是<=。 如果遍历word1和word2相等,那就从左上角去找dp的值。不然的话就要从三个方向去找最小值。
Leetcode 300
题目描述:
给你一个整数数组 nums ,找到其中最长严格递增子序列的长度。
示例 1:
输入:nums = [10,9,2,5,3,7,101,18] 输出:4 解释:最长递增子序列是 [2,3,7,101],因此长度为 4 。
示例 2:
输入:nums = [0,1,0,3,2,3] 输出:4
示例 3:
输入:nums = [7,7,7,7,7,7,7] 输出:1
class Solution {
public:
int lengthOfLIS(vector<int>& nums) {
vector<int> dp(nums.size(),1);
for(int i = 0; i <dp.size(); i++ ){
//如果发现nums里面在递增的话,去dp里面找
for(int j = 0; j < i; j++ ){
if(nums[j] < nums[i]){
dp[i] = max(dp[i], dp[j] + 1);
}
}
}
return *max_element(dp.begin(), dp.end());
}
};
每次loop到i位的时候,要回过头去找最大的dp值
时间复杂度为 O(n^2),空间复杂度为 O(n),其中 n 是数组 nums 的长度。
1Leetcode 53
问题描述: 给定一个整数数组nums,找到具有最大和的连续子数组(至少包含一个元素)并返回其和。
示例: 输入:nums = [-2,1,-3,4,-1,2,1,-5,4] 输出:6 解释:连续子数组 [4,-1,2,1] 的和最大,为 6。
class Solution {
public int maxSubArray(int[] nums) {
//dp: 维护两个变量
int currSum = nums[0];//当前元素和dp【i-1】,比较出最大值
int maxSum = nums[0];//最大值
for(int i = 1;i < nums.length; i ++){
currSum = Math.max(nums[i], currSum + nums[i]);
maxSum = Math.max(currSum,maxSum);
}
return maxSum;
}
}
Leetcode 1143 最长公共子序列
class Solution {
public int longestCommonSubsequence(String text1, String text2) {
int m = text1.length(); // 计算 text1 的长度
int n = text2.length(); // 计算 text2 的长度
int[][] dp = new int[m + 1][n + 1]; // 创建一个二维数组 dp,其中 dp[i][j] 表示 text1 前 i 个字符和 text2 前 j 个字符的最长公共子序列的长度
// 计算最长公共子序列的长度
for (int i = 1; i <= m; i++) { // 遍历 text1 的每个字符
for (int j = 1; j <= n; j++) { // 遍历 text2 的每个字符
if (text1.charAt(i - 1) == text2.charAt(j - 1)) { // 如果字符相等 注意这里是i-1!!!
dp[i][j] = dp[i - 1][j - 1] + 1; // 当前位置的最长公共子序列长度为左上角的值加 1
} else { // 如果字符不相等
dp[i][j] = Math.max(dp[i - 1][j], dp[i][j - 1]); // 当前位置的最长公共子序列长度为上方和左方的最大值
}
}
}
return dp[m][n]; // 返回最长公共子序列的长度
}
}
代码中使用动态规划思想,首先定义一个
dp数组,其中dp[i][j]表示text1的前i个字符和text2的前j个字符的最长公共子序列的长度。然后进行循环,如果text1[i-1]和text2[j-1]相等,那么当前位置的最长公共子序列长度就是dp[i-1][j-1]加上 1,否则就是dp[i-1][j]和dp[i][j-1]中较大的那个。最后返回dp[m][n]就是text1和text2的最长公共子序列的长度。这个算法的时间复杂度为 ,其中 和 分别是两个字符串的长度。这是因为我们需要遍历两个字符串的所有字符,并且对于每个字符,需要进行一次常数时间的比较和状态转移操作。
这个算法的空间复杂度为 ,因为我们需要创建一个 行、 列的二维数组来保存状态值。在实际的算法实现中,我们可以使用滚动数组或者原地 DP 的方法来将空间复杂度优化到 。
class Solution:
def longestCommonSubsequence(self, text1: str, text2: str) -> int:
m, n = len(text1), len(text2)
# 创建二维列表,用于存储动态规划的中间结果
# dp[i][j] 表示 text1[0:i] 和 text2[0:j] 的最长公共子序列的长度
dp = [[0] * (n + 1) for _ in range(m + 1)]
# 遍历 text1 和 text2 的所有子串,进行动态规划
for i in range(1, m + 1):
for j in range(1, n + 1):
if text1[i - 1] == text2[j - 1]:
dp[i][j] = dp[i - 1][j - 1] + 1
else:
dp[i][j] = max(dp[i - 1][j], dp[i][j - 1])
# 返回 text1 和 text2 的最长公共子序列的长度
return dp[m][n]
Leetcode 583 两个字符串的删除操作
给定两个单词 word1 和 word2 ,返回使得 word1 和 word2 相同所需的最小步数。
每步 可以删除任意一个字符串中的一个字符。
示例 1:
输入: word1 = "sea", word2 = "eat" 输出: 2 解释: 第一步将 "sea" 变为 "ea" ,第二步将 "eat "变为 "ea" 示例 2:
输入:word1 = "leetcode", word2 = "etco" 输出:4
class Solution(object):
def minDistance(self, word1, word2):
"""
:type word1: str
:type word2: str
:rtype: int
"""
lcs = self.LCS(word1,word2)
return len(word1) - lcs + len(word2) - lcs
def LCS(self,text1,text2) -> int:
m, n = len(text1), len(text2)
dp = [[0]*(n+1) for i in range (m+1)]
for i in range(1, m+1):
for j in range(1, n+1):
if text1[i-1] == text2[j-1]:
dp[i][j] = dp[i-1][j-1] + 1
else:
dp[i][j] = max(dp[i-1][j], dp[i][j-1])
return dp[m][n]
这道题就是上一道题的变种,先求出两个str的LCS就可以得到答案
Leetcode 712 最小 ASCII 删除和
给定两个字符串s1 和 s2,返回 使两个字符串相等所需删除字符的 ASCII 值的最小和 。
示例 1:
输入: s1 = "sea", s2 = "eat" 输出: 231 解释: 在 "sea" 中删除 "s" 并将 "s" 的值(115)加入总和。 在 "eat" 中删除 "t" 并将 116 加入总和。 结束时,两个字符串相等,115 + 116 = 231 就是符合条件的最小和。
class Solution {
public int minimumDeleteSum(String s1, String s2) {
int m = s1.length();
int n = s2.length();
int[][] dp = new int [m+1][n+1];
//fill int the first row and column
for(int i = 1; i <= m; i ++){
dp[i][0] = dp[i-1][0]+s1.codePointAt(i-1);
}
for(int j = 1; j <=n; j ++){
dp[0][j] = dp[0][j-1]+s2.codePointAt(j-1);
}
for(int i = 1; i <=m; i ++){
for(int j = 1; j <=n; j ++){
if(s1.charAt(i-1) == s2.charAt(j-1)){
dp[i][j] = dp[i-1][j-1];
}
else{
dp[i][j] = Math.min((dp[i-1][j] +s1.codePointAt(i-1)),(dp[i][j-1] + s2.codePointAt(j-1)));
}
}
}
return dp[m][n];
}
}
和上一道题不同要注意的是,我们第一行第一列要储存s1和s2到i位置的ascii值,左上角是空的。
其次,我们发现不想等的时候,应该是dp前面的值加上s1/s2的ascii值。
用到了Java codepointat
class Solution:
def minimumDeleteSum(self, s1: str, s2: str) -> int:
m ,n= len(s1),len(s2)
dp = [[0]* (n+1) for i in range (m+1)]
#initialize dp
for i in range (1,m+1):
dp[i][0] = dp[i-1][0] + ord(s1[i-1])
for j in range (1,n+1):
dp[0][j] = dp[0][j-1] + ord(s2[j-1])
for i in range (1, m+1):
for j in range (1, n+1):
if s1[i-1] == s2[j-1]:
dp[i][j] = dp[i-1][j-1]
else:
dp[i][j] = min((dp[i][j-1] + ord(s2[j-1])),(dp[i-1][j] + ord(s1[i-1])))
return dp[m][n]
Leetcode516 最长回文子串
题目描述:
给定一个字符串,找到它的最长回文子序列。可以假设字符串的最大长度为1000。
示例:
输入:"bbbab" 输出:4 解释:一个可能的最长回文子序列为 "bbbb"。
输入:"cbbd" 输出:2 解释:一个可能的最长回文子序列为 "bb"。
class Solution {
public int longestPalindromeSubseq(String s) {
//2D dp
int m = s.length();
int[][] dp = new int[m][m];
//fill in dp[][]
for(int i = 0; i < m; i ++){
dp[i][i] = 1;//since every single element is a LPS
}
//loop from the end
for(int i = m-2 ; i >=0; i --){
for(int j = i+1; j <m; j ++){
//if equals,means both s[i] and s[j] are on the LPS, so we +2 to the middle of it
if(s.charAt(i) == s.charAt(j)){
dp[i][j] = dp[i+1][j-1] + 2;
}
else{
//not equal: means either s[i] or s[j] isn't on the LPS
dp[i][j] = Math.max(dp[i+1][j],dp[i][j-1]);
}
}
}
return dp[0][m-1];
}
}
首先要注意,为了避免边界问题,这个loop从后往前,注意一下i和j的定义。
状态转移: s【i】 和s【j】做比较从尾部开始。如果想等,说明两人都在LPS上,就把窗口里边一个+2(看代码)。
如果不想等,说明至少有一个不在LPS上,退回dp【】【】去找。
class Solution:
def longestPalindromeSubseq(self, s: str) -> int:
#2d dp
m = len(s)
dp = [[0]* m for i in range (m)]
#fill in dp
for i in range (m):
dp[i][i] = 1
#loop from the end: avoid out of bound error
for i in range (m-2,-1,-1):
for j in range (i+1,m):
if s[i] == s[j]:
dp[i][j] = dp[i+1][j-1] + 2
else:
dp[i][j] = max(dp[i+1][j], dp[i][j-1])
return dp[0][m-1]
Notice: the usgae of for loop!!!
背包问题
N = 3, W = 4 wt = [2, 1, 3] val = [4, 2, 3]
算法返回 6,选择前两件物品装进背包,总重量 3 小于 W,可以获得最大价值 6。
public int knapsack(int W, int[] wt, int[] val, int n) {
int[][] dp = new int[n+1][W+1]; // 创建二维数组,用于记录最大价值
for (int i = 0; i <= n; i++) { // 初始化边界条件
for (int j = 0; j <= W; j++) {
if (i == 0 || j == 0) { // 背包容量为0或者没有物品时,最大价值都为0
dp[i][j] = 0;
} else if (wt[i-1] <= j) { // 当前物品可以放入背包中
dp[i][j] = Math.max(dp[i-1][j], val[i-1] + dp[i-1][j-wt[i-1]]); // 选择放入或者不放入物品
} else { // 当前物品不能放入背包中
dp[i][j] = dp[i-1][j];
}
}
}
return dp[n][W]; // 返回最大价值
}
i:背包的第i个元素
j: 多少重量
状态转移:如果发现第i个元素不能放到背包(超重):那么就找i-1元素对应的dp
如果能放,那么就看Math.max(dp[i-1][j], val[i-1] + dp[i-1][j-wt[i-1]]);
Leetcode416分割等和子集
给你一个 只包含正整数 的 非空 数组 nums 。请你判断是否可以将这个数组分割成两个子集,使得两个子集的元素和相等。
示例 1:
输入:nums = [1,5,11,5] 输出:true 解释:数组可以分割成 [1, 5, 5] 和 [11] 。
class Solution {
public boolean canPartition(int[] nums) {
//将问题转换成,把nums里面的元素值看成重量,放到sum/2的背包中能不能放满
//compute sum
int n = nums.length;
int sum = 0;
for(int num: nums){
sum += num;
}
if(sum%2 == 1) return false;
sum = sum/2;
boolean[][] dp = new boolean[n+1][sum+1];
//initialize first column in dp: for 0 weight bag,we can fill in with any element
for(int i = 0;i <= n; i ++){
dp[i][0] = true;
}
//dp[i][j]看看前i个元素在j的重量下能不能装满
for(int i = 1; i <=n; i ++){
for(int j = 1;j <= sum; j ++){
if(j - nums[i - 1] < 0){
//means we cannot put nums[i] into bag
dp[i][j] = dp[i-1][j];
}
else{
//看去掉i元素或者去掉i元素的重量
dp[i][j] = dp[i-1][j] || dp[i-1][j-nums[i-1]];
}
}
}
return dp[n][sum];
}
}
题目变成:前n个元素能不能把sum/2的背包放满
注意:dp的索引
class Solution:
def canPartition(self, nums: List[int]) -> bool:
n = len(nums)
sums = sum(nums)
if sums % 2 == 1:
return False
target = sums // 2
dp = [[False] * (target + 1) for _ in range(n+1)]
for i in range(n+1):
dp[i][0] = True
for i in range(1, n+1):
for j in range(1, target+1):
if j - nums[i-1] >= 0:
dp[i][j] = dp[i-1][j] or dp[i-1][j-nums[i-1]]
else:
dp[i][j] = dp[i-1][j]
return dp[n][target]
Leetcode 518 零钱兑换ll
class Solution {
public int change(int amount, int[] coins) {
//dp[][]背包问题: dp[i][j] use ith-element can get j solutions
int n = coins.length;
int[][] dp = new int [n + 1][amount + 1];
//fill in dp[][]
for (int i = 0;i <= n; i ++){
dp[i][0] = 1;//means if we do nothing we can compute value of 0, so solution be 1
}
for (int i = 1; i <=n; i ++){
for(int j = 1;j <= amount; j ++){
if(coins[i-1] <= j){
//means coins[i-1] is possible solution, so dp[i][j] depends on 扣掉i这个元素能有几种解法 + 前i-1个元素有几种解法
dp[i][j] = dp[i-1][j] + dp[i][j - coins[i-1]];
}
else{
//cannot fill in package
dp[i][j] = dp[i-1][j];
}
}
}
return dp[n][amount];
}
}
注意两层for loop中的if条件
class Solution:
def change(self, amount: int, coins: List[int]) -> int:
n = len(coins)
dp = [[0]* (amount + 1) for i in range (n+1)]
for i in range(n+1):
dp[i][0] = 1
for i in range(1,n+1):
for j in range(1,amount + 1):
if coins[i-1] <= j:
dp[i][j] = dp[i-1][j] + dp[i][j-coins[i-1]]
else:
dp[i][j] = dp[i-1][j]
return dp[n][amount]
0Leetcode 493 目标和
给你一个整数数组 nums 和一个整数 target 。
向数组中的每个整数前添加 '+' 或 '-' ,然后串联起所有整数,可以构造一个 表达式 :
例如,nums = [2, 1] ,可以在 2 之前添加 '+' ,在 1 之前添加 '-' ,然后串联起来得到表达式 "+2-1" 。 返回可以通过上述方法构造的、运算结果等于 target 的不同 表达式 的数目。
示例 1:
输入:nums = [1,1,1,1,1], target = 3 输出:5 解释:一共有 5 种方法让最终目标和为 3 。 -1 + 1 + 1 + 1 + 1 = 3 +1 - 1 + 1 + 1 + 1 = 3 +1 + 1 - 1 + 1 + 1 = 3 +1 + 1 + 1 - 1 + 1 = 3 +1 + 1 + 1 + 1 - 1 = 3
//回溯算法
class Solution {
int result = 0;
public int findTargetSumWays(int[] nums, int target) {
//use backtrack solution
backtrack(nums, 0, target);//our goal is change target to 0
return result;
}
void backtrack(int[] nums, int start, int remain){
//base case
if(start == nums.length){
if (remain == 0){//we find a possibile answer
result++;
}
return;
}
remain += nums[start];
backtrack(nums,start+1,remain);
remain -= nums[start];
remain -= nums[start];
backtrack(nums, start+1, remain);
remain += nums[start];
}
}
注意base case 中: 不管我们remain是不是等于0,都要return。
下面的撤销选择!
class Solution:
result = 0
def findTargetSumWays(self, nums: List[int], target: int) -> int:
# revise backtrack
self.backtrack(nums, 0,target)
return self.result
def backtrack(self,nums:List[int], i : int, remain: int):
if i == len(nums):
if remain == 0:
self.result += 1
return
remain += nums[i]
self.backtrack(nums, i + 1, remain)
remain -= nums[i]
remain -= nums[i]
self.backtrack(nums, i + 1, remain)
remain += nums[i]
Caution: using self.
优化:dp
class Solution {
//首先,如果我们把 nums 划分成两个子集 A 和 B,分别代表分配 + 的数和分配 - 的数,那么他们和 target 存在如下关系:
/*sum(A) - sum(B) = target
sum(A) = target + sum(B)
sum(A) + sum(A) = target + sum(B) + sum(A)
2 * sum(A) = target + sum(nums) */
public int findTargetSumWays(int[] nums, int target) {
//问题变成了,nums里有多少个子集A能装满target + sum(nums)/2的背包
int sum = 0;
for(int num: nums){
sum += num;
}
//two conditions we have no answer
if(sum < Math.abs(target) || (target + sum)%2 == 1) return 0;
return subset(nums,(target + sum)/2);
}
int subset(int[] nums, int sum){
//dp
int n = nums.length;
int[][] dp = new int[n+1][sum+1];
for(int i = 0; i <= n ; i ++){
dp[i][0] = 1;//do nothing count as one solution
}
for(int i = 1; i <=n; i ++){
for(int j = 1; j <= sum; j ++){
if(nums[i-1] > j){
//means nums[i-1] cannot fill bag
dp[i][j] = dp[i-1][j];
}
else{
dp[i][j] = dp[i-1][j] + dp[i-1][j-nums[i-1]];//如果可以装进背包。我们看去掉这个选项的答案和去掉这个重量的答案
}
}
}
return dp[n][sum];
}
}
理解 dp[i][j] = dp[i-1][j] + dp[i-1][j-nums[i-1]];:如果我当前元素是可以放的,那么我就去看前i-1个元素能放进去的解决方法和凑出重量-当前元素重量的解决方法
比如[ 1, 2, 3] target= 5
我现在前两个元素一共有三种方法凑出5, 加了3之后,结果 = 前两元素凑出5 + 前两元素凑出 5-3
时间复杂度:该算法使用了一个二维数组dp进行动态规划,需要遍历整个数组,因此时间复杂度为O(n * sum),其中n是nums的长度,sum是nums数组中所有元素的和。
空间复杂度:该算法使用了一个二维数组dp进行动态规划,其大小为(n+1) * (sum+1),因此空间复杂度为O(n * sum)
Leetcode 64 最小路径和
给定一个包含非负整数的 m x n 网格 grid ,请找出一条从左上角到右下角的路径,使得路径上的数字总和为最小。
说明:每次只能向下或者向右移动一步。
示例 1:
输入:grid = [[1,3,1],[1,5,1],[4,2,1]] 输出:7 解释:因为路径 1→3→1→1→1 的总和最小。
class Solution {
public int minPathSum(int[][] grid) {
//the variables we need
int m = grid.length, n = grid[0].length;
//do it with one dimension dp
int[] dp = new int[n];
Arrays.fill(dp,Integer.MAX_VALUE);
dp[0] = 0;
//loop from level 2
for(int i = 0; i < m; i ++){
dp[0] += grid[i][0];
for(int j = 1; j < n; j++ ){
dp[j] = Math.min(dp[j], dp[j-1]) + grid[i][j];
}
}
return dp[n-1];
}
}
注意: 为了第一波能找到min,我们要先拿max填满dp;然后要把第一个元素设成0.
注意状态转移。
class Solution:
def minPathSum(self, grid: List[List[int]]) -> int:
m ,n = len(grid), len(grid[0])
dp = [0] * n
dp[0] = grid[0][0]
for i in range (1,n):
dp[i] = dp[i-1] + grid[0][i]
for i in range (1, m):
dp[0] += grid[i][0]
for j in range(1,n):
dp[j] = grid[i][j] + min(dp[j],dp[j-1])
return dp[-1]
python貌似没有max_value这个东西。
Leetcode198 打家劫舍
示例 1:
输入:[1,2,3,1] 输出:4 解释:偷窃 1 号房屋 (金额 = 1) ,然后偷窃 3 号房屋 (金额 = 3)。 偷窃到的最高金额 = 1 + 3 = 4 。 示例 2:
输入:[2,7,9,3,1] 输出:12 解释:偷窃 1 号房屋 (金额 = 2), 偷窃 3 号房屋 (金额 = 9),接着偷窃 5 号房屋 (金额 = 1)。 偷窃到的最高金额 = 2 + 9 + 1 = 12 。
class Solution {
public int rob(int[] nums) {
//dp: the max money for nums[i]
int[] dp = new int[nums.length];
//base case
int n = dp.length;
if(n == 1){
return nums[0];
}
if(n ==2){
return Math.max(nums[0],nums[1]);
}
dp[0] = nums[0];
dp[1] = Math.max(nums[0],nums[1]);
for(int i = 2; i < dp.length; i ++){
dp[i] = Math.max(dp[i-1],dp[i-2] + nums[i]);
}
return dp[dp.length-1];
}
}
注意:我们for loop从索引2开始,所以要判断两个cornor case;
状态转移: i索引要么打劫,那么结果就是前前个格子加上nums本身,要么不打劫就是前面一个格子的值。
该函数的时间复杂度为 ,其中 是输入数组 的长度,因为需要遍历整个数组一次。
该函数的空间复杂度为 ,因为需要创建一个长度为 的数组 来存储每个位置的最大收益。
优化版:不需要dp数组
class Solution {
public int rob(int[] nums) {
//dont need to use dp[]
//cornor case
int n = nums.length;
if(n == 1) return nums[0];
if(n == 2) return Math.max(nums[0], nums[1]);
int dp_0 = nums[0];
int dp_1 = Math.max(nums[0], nums[1]);
int dp_2 = -1;
for(int i = 2; i < n; i ++){
dp_2 = Math.max(dp_0 + nums[i],dp_1);
//update these variables
dp_0 = dp_1;
dp_1 = dp_2;
}
return dp_2;
}
}
class Solution:
def rob(self, nums: List[int]) -> int:
n = len(nums)
#base case
if n == 1: return nums[0]
if n == 2: return max(nums[0],nums[1])
dp_0 = nums[0]
dp_1 = max(nums[0],nums[1])
dp_2 = -1
for i in range(2,n):
dp_2 = max(dp_0 + nums[i] , dp_1)
dp_0 = dp_1
dp_1 = dp_2
return dp_2
Leetcode213 打家劫舍ll
213. 打家劫舍 II
难度中等1279收藏分享切换为英文接收动态反馈
你是一个专业的小偷,计划偷窃沿街的房屋,每间房内都藏有一定的现金。这个地方所有的房屋都 围成一圈 ,这意味着第一个房屋和最后一个房屋是紧挨着的。同时,相邻的房屋装有相互连通的防盗系统,如果两间相邻的房屋在同一晚上被小偷闯入,系统会自动报警 。
给定一个代表每个房屋存放金额的非负整数数组,计算你 在不触动警报装置的情况下 ,今晚能够偷窃到的最高金额。
示例 1:
输入:nums = [2,3,2]
输出:3
解释:你不能先偷窃 1 号房屋(金额 = 2),然后偷窃 3 号房屋(金额 = 2), 因为他们是相邻的。
class Solution:
def rob(self, nums: List[int]) -> int:
n = len(nums)
if n == 1: return nums[0]
return max(self.robRange(nums, 0, n - 2), self.robRange(nums, 1, n - 1))
def robRange(self, nums: List[int], start: int, end: int) -> int:
n = len(nums)
dp_i_1 = dp_i_2 = dp_i = 0
for i in range(end, start - 1, -1):
dp_i = max(dp_i_1, nums[i] + dp_i_2)
dp_i_2 = dp_i_1
dp_i_1 = dp_i
return dp_i
把问题变成,(self.robRange(nums, 0, n - 2), self.robRange(nums, 1, n - 1)) 算这两个区间的rob
Leetcode 337 打家劫舍lll
打家劫舍在binary tree里面
/**
* Definition for a binary tree node.
* public class TreeNode {
* int val;
* TreeNode left;
* TreeNode right;
* TreeNode() {}
* TreeNode(int val) { this.val = val; }
* TreeNode(int val, TreeNode left, TreeNode right) {
* this.val = val;
* this.left = left;
* this.right = right;
* }
* }
*/
class Solution {
public int rob(TreeNode root) {
int[] res = helper(root);
return Math.max(res[0], res[1]);
}
/**
arr[0] the max value by rob the root
arr[1] no rob the root */
int[] helper(TreeNode root){
if(root == null){
return new int[]{0,0};
}
int[] left = helper(root.left);
int[] right = helper(root.right);
int rob_root = root.val + left[1] + right[1];
int not_rob = Math.max(left[0],left[1])+ Math.max(right[0],right[1]);
return new int[]{rob_root,not_rob};
}
}
时间复杂度: 这段代码使用了递归来遍历整棵树,对于每个节点都只访问了一次,因此时间复杂度是 O(N),其中 N 是节点数。
空间复杂度: 这段代码使用了递归来遍历整棵树,因此递归的深度是树的高度 h,最坏情况下为 N(退化成链表的情况)。对于每个递归层次,需要使用一个长度为2的整数数组,因此空间复杂度是 O(h)。由于 h 最坏情况下为 N,因此空间复杂度是 O(N)。
这段代码是用来解决 "House Robber III" 问题,这个问题要求在二叉树结构的房子里,不能同时抢劫相邻的节点,求最大的抢劫价值。
这段代码的解决思路是采用递归的方式对每个节点进行处理,对于每个节点,有两种情况:
- 抢劫当前节点:由于不能同时抢劫相邻节点,因此抢劫当前节点就必须跳过其子节点,但是可以抢劫其孙子节点。所以当前节点的价值为 root.val + left[1] + right[1],其中 left[1] 和 right[1] 分别表示左右子节点不抢劫的最大价值。
- 不抢劫当前节点:由于不抢劫当前节点,可以选择抢劫其左右子节点或者不抢劫。所以当前节点的价值为 Math.max(left[0], left[1]) + Math.max(right[0], right[1])。
递归的基准情况是节点为空,此时抢劫价值为0。
最终,将根节点的两种情况的最大值返回即可。
其中,helper() 函数返回一个长度为2的整数数组,arr[0] 表示抢劫当前节点的最大价值,arr[1] 表示不抢劫当前节点的最大价值。
# Definition for a binary tree node.
# class TreeNode:
# def __init__(self, val=0, left=None, right=None):
# self.val = val
# self.left = left
# self.right = right
class Solution:
def rob(self, root: Optional[TreeNode]) -> int:
res = self.helper(root)
return max(res[0], res[1])
def helper(self,root: Optional[TreeNode]) -> List[int]:
if not root:
return [0,0]
left = self.helper(root.left)
right = self.helper(root.right)
rob_root = root.val + left[1] + right[1]
not_rob = max(left[0],left[1]) + max(right[0], right[1])
return [rob_root, not_rob]
力扣46 全排列
class Solution {
boolean[] used;
List<List<Integer>> res;
LinkedList<Integer> track;
public List<List<Integer>> permute(int[] nums) {
track = new LinkedList<>();
res = new LinkedList<>();
used = new boolean[nums.length];
backtrack(nums,track);
return res;
}
void backtrack(int[] nums, LinkedList<Integer> track){
//base case
if(track.size() == nums.length){
res.add(new LinkedList(track));
}
for(int i = 0; i < nums.length; i ++){
if(used[i]){
continue;
}
used[i] = true;
track.add(nums[i]);
backtrack(nums,track);
track.removeLast();;
used[i] = false;
}
}
}
track要用LinkedList类型,因为要用到removeLast这个api。
该算法的时间复杂度为O(NN!),其中N为数组的长度。因为对于每个元素,都需要进行一次回溯,而回溯的总次数是N!,同时还需要在每次回溯时,遍历数组中未被使用的元素,这需要花费O(N)的时间。所以总时间复杂度为O(NN!)。
空间复杂度方面,该算法使用了一个布尔型数组used和一个链表track,以及一个结果列表res。其中used数组和track链表的长度都是N,所以它们的总空间复杂度为O(N)。而结果列表res的长度为N!,因为全排列的总数为N!,所以它的空间复杂度为O(N!)。因此,该算法的总空间复杂度为O(N*N!)。
class Solution:
#global variables
def __init__(self):
self.used = None
self.track = None
self.res = None
def permute(self, nums: List[int]) -> List[List[int]]:
#initialize variables
self.track = []
self.res = []
self.used = [False] * len(nums)
self.backtrack(nums, self.track)
return self.res
def backtrack(self, nums: List[int], track:List[int]):
#base case
n = len(nums)
if n == len(track):
self.res.append(track[:])
return
for i in range(n):
if self.used[i]:
continue
self.used[i] = True
track.append(nums[i])
self.backtrack(nums, track)
self.used[i] = False
track.pop()
注意这里面track在backtrack中不需要self因为函数里面调用的不是全局的track
两数相加
# Definition for singly-linked list.
# class ListNode(object):
# def __init__(self, val=0, next=None):
# self.val = val
# self.next = next
class Solution(object):
def addTwoNumbers(self, l1, l2):
"""
:type l1: ListNode
:type l2: ListNode
:rtype: ListNode
"""
head = None
tail = None
carry = 0
while l1 or l2:
n1 = l1.val if l1 else 0
n2 = l2.val if l2 else 0
sum = n1 + n2 + carry
carry = sum // 10
i = sum % 10
curr = ListNode(i)
if not head:
head = tail = curr
else:
tail.next = curr
tail = tail.next
if l1:
l1 = l1.next
if l2:
l2 = l2.next
if carry > 0:
tail.next = ListNode(carry)
return head
注意我们的逻辑!
presum
class NumArray(object):
def __init__(self, nums):
"""
:type nums: List[int]
"""
self.presum = [0]
for num in nums:
self.presum.append(self.presum[-1] + num)
def sumRange(self, left, right):
"""
:type left: int
:type right: int
:rtype: int
"""
return self.presum[right+1] - self.presum[left]
# Your NumArray object will be instantiated and called as such:
# obj = NumArray(nums)
# param_1 = obj.sumRange(left,right)
区间加法
假设你有一个长度为 n 的数组,初始情况下所有的数字均为 0,你将会被给出 k 个更新的操作。
其中,每个操作会被表示为一个三元组:[startIndex, endIndex, inc],你需要将子数组 A[startIndex ... endIndex](包括 startIndex 和 endIndex)增加 inc。
请你返回 k 次操作后的数组。
示例:
输入: length = 5, updates = [[1,3,2],[2,4,3],[0,2,-2]] 输出: [-2,0,3,5,3]
class Solution(object):
def getModifiedArray(self, length, updates):
"""
:type length: int
:type updates: List[List[int]]
:rtype: List[int]
"""
diff = [0 for i in range(length+1)]
for update in updates:
start, end = update[0], update[1]
diff[start] += update[2]
if end + 1 < length:
diff[end + 1] -= update[2]
num = [0 for i in range(length)]
num[0] = diff[0]
for i in range(1,length):
num[i] = num[i-1] + diff[i]
return num
差分数组