专题一:数组

一、基础

1、数组是存放在连续内存空间上的相同类型数据的集合。

2、需要两点注意的是

  • 数组下标都是从0开始的。
  • 数组内存空间的地址是连续的

正是因为数组的在内存空间的地址是连续的,所以我们在删除或者增添元素的时候,就难免要移动其他元素的地址。

3、如果使用C++的话,要注意vector 和 array的区别,vector的底层实现是array,严格来讲vector是容器,不是数组

数组的元素是不能删的,只能覆盖。

二、二分法

前提:

数组为有序数组,同时题目还强调数组中无重复元素,因为一旦有重复元素,使用二分查找法返回的元素下标可能不是唯一的,这些都是使用二分法的前提条件,当大家看到题目描述满足如上条件的时候,可要想一想是不是可以用二分法了。

技巧:

以后大家「只要看到题里给出的数组是有序数组,都可以想一想是否可以使用二分法。」

例题一:

题目链接:https://leetcode-cn.com/problems/binary-search/

给定一个 n 个元素有序的(升序)整型数组 nums 和一个目标值 target ,写一个函数搜索 nums 中的 target,如果目标值存在返回下标,否则返回 -1。

示例 1:
输入: nums = [-1,0,3,5,9,12], target = 9
输出: 4
解释: 9 出现在 nums 中并且下标为 4

示例 2:
输入: nums = [-1,0,3,5,9,12], target = 2
输出: -1
解释: 2 不存在 nums 中因此返回 -1

提示:

你可以假设 nums 中的所有元素是不重复的。
n 将在 [1, 10000]之间。
nums 的每个元素都将在 [-9999, 9999]之间。

思路:

二分查找涉及的很多的边界条件,逻辑比较简单,但就是写不好。例如到底是 while(left < right) 还是 while(left <= right),到底是right = middle呢,还是要right = middle - 1呢?

写二分法,区间的定义一般为两种,左闭右闭即[left, right],或者左闭右开即[left, right)。

二分法第一种写法

第一种写法,我们定义 target 是在一个在左闭右闭的区间里,也就是**[left, right]** (这个很重要非常重要)。

区间的定义这就决定了二分法的代码应该如何写,因为定义target在[left, right]区间,所以有如下两点:

  • while (left <= right) 要使用 <= ,因为left == right是有意义的,所以使用 <=
  • if (nums[middle] > target) right 要赋值为 middle - 1,因为当前这个nums[middle]一定不是target,那么接下来要查找的左区间结束下标位置就是 middle - 1

例如在数组:1,2,3,4,7,9,10中查找元素2,如图所示:

在这里插入图片描述
代码如下:(详细注释)

// 版本一
class Solution {
public:
    int search(vector<int>& nums, int target) {
        int left = 0;
        int right = nums.size() - 1; // 定义target在左闭右闭的区间里,[left, right]
        while (left <= right) { // 当left==right,区间[left, right]依然有效,所以用 <=
            int middle = left + ((right - left) / 2);// 防止溢出 等同于(left + right)/2
            if (nums[middle] > target) {
                right = middle - 1; // target 在左区间,所以[left, middle - 1]
            } else if (nums[middle] < target) {
                left = middle + 1; // target 在右区间,所以[middle + 1, right]
            } else { // nums[middle] == target
                return middle; // 数组中找到目标值,直接返回下标
            }
        }
        // 未找到目标值
        return -1;
    }
};

二分法第二种写法

如果说定义 target 是在一个在左闭右开的区间里,也就是**[left, right)** ,那么二分法的边界处理方式则截然不同。

有如下两点:

  • while (left < right),这里使用 < ,因为left == right在区间[left, right)是没有意义的
  • if (nums[middle] > target) right 更新为 middle,因为当前nums[middle]不等于target,去左区间继续寻找,而寻找区间是左闭右开区间,所以right更新为middle,即:下一个查询区间不会去比较nums[middle]

在数组:1,2,3,4,7,9,10中查找元素2,如图所示:(注意和方法一的区别

在这里插入图片描述
代码如下:(详细注释)

// 版本二
class Solution {
public:
    int search(vector<int>& nums, int target) {
        int left = 0;
        int right = nums.size(); // 定义target在左闭右开的区间里,即:[left, right),size不用减1
        while (left < right) { // 因为left == right的时候,在[left, right)是无效的空间,所以使用 <
            int middle = left + ((right - left) >> 1);
            if (nums[middle] > target) {
                right = middle; // target 在左区间,在[left, middle)中
            } else if (nums[middle] < target) {
                left = middle + 1; // target 在右区间,在[middle + 1, right)中
            } else { // nums[middle] == target
                return middle; // 数组中找到目标值,直接返回下标
            }
        }
        // 未找到目标值
        return -1;
    }
};

补充:

「二分」有一个比较容易混淆的点是:当需要找目标值第一次出现的下标时,条件应该写成 nums[mid] >= targetnums[mid]>=target 还是 nums[mid] <= targetnums[mid]<=target。

由于二分是从中间开始找起的,所以找的必然是条件区间中靠近中心的的边界值。

文字不好理解,我们结合图片来看:

在这里插入图片描述

例题二:

编号35:搜索插入位置

题目链接:https://leetcode-cn.com/problems/search-insert-position/submissions/

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

你可以假设数组中无重复元素。

示例 1:
输入: [1,3,5,6], 5
输出: 2

示例 2:
输入: [1,3,5,6], 2
输出: 1

示例 3:
输入: [1,3,5,6], 7
输出: 4

示例 4:
输入: [1,3,5,6], 0
输出: 0

代码:

class Solution {
public:
    int searchInsert(vector<int>& nums, int target) {
        int left=0;
        int right=nums.size()-1;
        while(left<=right){
            int middle=left+(right-left)/2;
            if(nums[middle]>target)
                right=middle-1;
            else if(nums[middle]<target)
                left=middle+1;
            else
                return middle;
        }
        return left;
        // return right + 1;
    }
};

注意:

return left和return right+1都可以。

例题三:

  1. 在排序数组中查找元素的第一个和最后一个位置

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

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

进阶:

你可以设计并实现时间复杂度为 O(log n) 的算法解决此问题吗?

示例 1:

输入:nums = [5,7,7,8,8,10], target = 8
输出:[3,4]
示例 2:

输入:nums = [5,7,7,8,8,10], target = 6
输出:[-1,-1]
示例 3:

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

提示:

0 <= nums.length <= 105
-109 <= nums[i] <= 109
nums 是一个非递减数组
-109 <= target <= 109

链接:https://leetcode-cn.com/problems/find-first-and-last-position-of-element-in-sorted-array

思路:

由于数组已经排序,因此整个数组是单调递增的,我们可以利用二分法来加速查找的过程。

考虑 target 开始和结束位置,其实我们要找的就是数组中「第一个等于 target 的位置」(记为 leftIdx)和「第一个大于 target 的位置减一」(记为 rightIdx)。

二分查找中,寻找 leftIdx 即为在数组中寻找第一个大于等于 target 的下标,寻找rightIdx 即为在数组中寻找第一个大于 target 的下标,然后将下标减一。两者的判断条件不同,为了代码的复用,我们定义 binarySearch(nums, target, lower) 表示在 nums 数组中二分查找target 的位置,如果lower 为true,则查找第一个大于等于target 的下标,否则查找第一个大于 target 的下标。

最后,因为target 可能不存在数组中,因此我们需要重新校验我们得到的两个下标leftIdx 和 rightIdx,看是否符合条件,如果符合条件就返回[leftIdx,rightIdx],不符合就返回 [−1,−1]。

代码:

class Solution {
public:
    int erfen(vector<int>& nums, int target){
        int left=0;
        int right=nums.size()-1;
        while(left<=right){
            int  middle=left+(right-left)/2;
            if(nums[middle]>=target)//注意这里是找首次出现
                right=middle-1;
            else if(nums[middle]<target)
                left=middle+1;
        }
        return left;
    }
    vector<int> searchRange(vector<int>& nums, int target) {
        int a=erfen(nums,target);
        int b=erfen(nums,target+1);
        // cout<<a<<" "<<b<<endl;
        if(a==nums.size()||nums[a]!=target)
            return vector<int>{-1,-1};
        return vector<int>{a,b-1};
    }
};

复杂度分析

  • 时间复杂度: O(logn) ,其中 n 为数组的长度。二分查找的时间复杂度为 O(logn),一共会执行两次,因此总时间复杂度为 O(logn)。
  • 空间复杂度:O(1) 。只需要常数空间存放若干变量。

例题四:

  1. x 的平方根

实现 int sqrt(int x) 函数。

计算并返回 x 的平方根,其中 x 是非负整数。

由于返回类型是整数,结果只保留整数的部分,小数部分将被舍去。

示例 1:

输入: 4
输出: 2

示例 2:

输入: 8
输出: 2

说明: 8 的平方根是 2.82842…,
由于返回类型是整数,小数部分将被舍去。

思路:

方法一:袖珍计算器算法
「袖珍计算器算法」是一种用指数函数 \expexp 和对数函数 \lnln 代替平方根函数的方法。我们通过有限的可以使用的数学函数,得到我们想要计算的结果。
在这里插入图片描述

在这里插入图片描述
因此在得到结果的整数部分ans 后,我们应当找出ans 与 ans+1 中哪一个是真正的答案。

class Solution {
public:
    int mySqrt(int x) {
        if (x == 0) {
            return 0;
        }
        int ans = exp(0.5 * log(x));
        return ((long long)(ans + 1) * (ans + 1) <= x ? ans + 1 : ans);
    }
};

复杂度分析

  • 时间复杂度:O(1),由于内置的 exp 函数与 log 函数一般都很快,我们在这里将其复杂度视为 O(1)。
  • 空间复杂度:O(1)。

方法二:二分查找

在这里插入图片描述

class Solution {
public:
    int mySqrt(int x) {
        int l = 0, r = x, ans = -1;
        while (l <= r) {
            int mid = l + (r - l) / 2;
            if ((long long)mid * mid <= x) {
                ans = mid;
                l = mid + 1;
            } else {
                r = mid - 1;
            }
        }
        return ans;
    }
};

在这里插入图片描述
方法三:牛顿迭代
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

class Solution {
public:
    int mySqrt(int x) {
        if (x == 0) {
            return 0;
        }

        double C = x, x0 = x;
        while (true) {
            double xi = 0.5 * (x0 + C / x0);
            if (fabs(x0 - xi) < 1e-7) {
                break;
            }
            x0 = xi;
        }
        return int(x0);
    }
};

在这里插入图片描述

例题五:

  1. 有效的完全平方数

给定一个 正整数 num ,编写一个函数,如果 num 是一个完全平方数,则返回 true ,否则返回 false 。

进阶:不要 使用任何内置的库函数,如 sqrt 。

示例 1:

输入:num = 16
输出:true
示例 2:

输入:num = 14
输出:false

提示:

1 <= num <= 2^31 - 1

代码:

class Solution {
public:
    bool isPerfectSquare(int num) {
        int l=0,r=num;
        while(l<=r){
            int mid=l+(r-l)/2;
            if((long long)mid*mid>num){//注意long long这里
                r=mid-1;
            }
            else if((long long)mid*mid<num){
                l=mid+1;
            }
            else
                return true;
        }
        return false;
       
    }
};

三、移除元素

例题一:

  1. 移除元素
    题目地址:https://leetcode-cn.com/problems/remove-element/

给你一个数组 nums 和一个值 val,你需要 原地 移除所有数值等于 val 的元素,并返回移除后数组的新长度。

不要使用额外的数组空间,你必须仅使用 O(1) 额外空间并原地修改输入数组。

示例 1:
给定 nums = [3,2,2,3], val = 3,
函数应该返回新的长度 2, 并且 nums 中的前两个元素均为 2。
你不需要考虑数组中超出新长度后面的元素。

示例 2:
给定 nums = [0,1,2,2,3,0,4,2], val = 2,
函数应该返回新的长度 5, 并且 nums 中的前五个元素为 0, 1, 3, 0, 4。

你不需要考虑数组中超出新长度后面的元素。

思路:

有的同学可能说了,多余的元素,删掉不就得了。

要知道数组的元素在内存地址中是连续的,不能单独删除数组中的某个元素,只能覆盖。

暴力解法

这个题目暴力的解法就是两层for循环,一个for循环遍历数组元素 ,第二个for循环更新数组。很明显暴力解法的时间复杂度是O(n^2),这道题目暴力解法在leetcode上是可以过的。

代码如下:

// 时间复杂度:O(n^2)
// 空间复杂度:O(1)
class Solution {
public:
    int removeElement(vector<int>& nums, int val) {
        int size = nums.size();
        for (int i = 0; i < size; i++) {
            if (nums[i] == val) { // 发现需要移除的元素,就将数组集体向前移动一位
                for (int j = i + 1; j < size; j++) {
                    nums[j - 1] = nums[j];
                }
                i--; // 因为下表i以后的数值都向前移动了一位,所以i也向前移动一位
                size--; // 此时数组的大小-1
            }
        }
        return size;

    }
};

双指针法

双指针法(快慢指针法):通过一个快指针和慢指针在一个for循环下完成两个for循环的工作。

双指针法(快慢指针法)在数组和链表的操作中是非常常见的,很多考察数组、链表、字符串等操作的面试题,都使用双指针法。

class Solution {
public:
    int removeElement(vector<int>& nums, int val) {
        int slowIndex=0;
        for(int fastIndex=0;fastIndex<nums.size();fastIndex++){
            if(val!=nums[fastIndex])
              nums[slowIndex++] = nums[fastIndex]; 
        }
        return slowIndex;
    }

};

例题二:

  1. 删除有序数组中的重复项
    给你一个有序数组 nums ,请你 原地 删除重复出现的元素,使每个元素 只出现一次 ,返回删除后数组的新长度。

不要使用额外的数组空间,你必须在 原地 修改输入数组 并在使用 O(1) 额外空间的条件下完成。

说明:

为什么返回数值是整数,但输出的答案是数组呢?

请注意,输入数组是以「引用」方式传递的,这意味着在函数里修改输入数组对于调用者是可见的。

你可以想象内部操作如下:

// nums 是以“引用”方式传递的。也就是说,不对实参做任何拷贝
int len = removeDuplicates(nums);

// 在函数里修改输入数组对于调用者是可见的。
// 根据你的函数返回的长度, 它会打印出数组中 该长度范围内 的所有元素。
for (int i = 0; i < len; i++) {
print(nums[i]);
}

示例 1:

输入:nums = [1,1,2]
输出:2, nums = [1,2]
解释:函数应该返回新的长度 2 ,并且原数组 nums 的前两个元素被修改为 1, 2 。不需要考虑数组中超出新长度后面的元素。
示例 2:

输入:nums = [0,0,1,1,1,2,2,3,3,4]
输出:5, nums = [0,1,2,3,4]
解释:函数应该返回新的长度 5 , 并且原数组 nums 的前五个元素被修改为 0, 1, 2, 3, 4 。不需要考虑数组中超出新长度后面的元素。

提示:

0 <= nums.length <= 3 * 104
-104 <= nums[i] <= 104
nums 已按升序排列

链接:https://leetcode-cn.com/problems/remove-duplicates-from-sorted-array

在这里插入图片描述

class Solution {
public:
    int removeDuplicates(vector<int>& nums) {
        int n=nums.size();
        if(n==0)
           return 0;
        int slowIndex=1;
        for(int fastIndex=1;fastIndex<n;fastIndex++){
            if(nums[fastIndex]!=nums[fastIndex-1]){
                nums[slowIndex++]=nums[fastIndex];
            }
        }
        return slowIndex;
    }
};

在这里插入图片描述

例题三:

  1. 移动零

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

示例:

输入: [0,1,0,3,12]
输出: [1,3,12,0,0]
说明:

必须在原数组上操作,不能拷贝额外的数组。
尽量减少操作次数。

链接:https://leetcode-cn.com/problems/move-zeroes

代码:

class Solution {
public:
    void moveZeroes(vector<int>& nums) {
         int slowIndex=0,fastIndex=0;
         for(;fastIndex<nums.size();fastIndex++){
             if(nums[fastIndex]!=0)
                 nums[slowIndex++]=nums[fastIndex];
         }
         for(int i=slowIndex;i<nums.size();i++)
             nums[i]=0;
    }
};

例题四:

  1. 比较含退格的字符串

给定 S 和 T 两个字符串,当它们分别被输入到空白的文本编辑器后,判断二者是否相等,并返回结果。 # 代表退格字符。

注意:如果对空文本输入退格字符,文本继续为空。

示例 1:

输入:S = “ab#c”, T = “ad#c”
输出:true
解释:S 和 T 都会变成 “ac”。
示例 2:

输入:S = “ab##”, T = “c#d#”
输出:true
解释:S 和 T 都会变成 “”。
示例 3:

输入:S = “a##c”, T = “#a#c”
输出:true
解释:S 和 T 都会变成 “c”。
示例 4:

输入:S = “a#c”, T = “b”
输出:false
解释:S 会变成 “c”,但 T 仍然是 “b”。

提示:

1 <= S.length <= 200
1 <= T.length <= 200
S 和 T 只含有小写字母以及字符 ‘#’。

链接:https://leetcode-cn.com/problems/backspace-string-compare

代码:

class Solution {
public:
    bool backspaceCompare(string s, string t) {
        int len1=s.length();
        int len2=t.length();
        int slowIndex1=0,slowIndex2=0,fastIndex1=0,fastIndex2=0;
        for(;fastIndex1<len1;fastIndex1++){
        if(s[fastIndex1]=='#'&&slowIndex1!=0)//注意如果slowIndex1本身就为0的话,不用移动指针
            slowIndex1--;
        else if(s[fastIndex1]=='#')
            continue;
        else
            s[slowIndex1++]=s[fastIndex1];
        }

        for(;fastIndex2<len2;fastIndex2++){
            if(t[fastIndex2]=='#'&&slowIndex2!=0)
                slowIndex2--;
            else if(t[fastIndex2]=='#')
                continue;
            else
                t[slowIndex2++]=t[fastIndex2];
        }
        if(slowIndex1==0&&slowIndex2==0)
            return true;
        if(slowIndex1!=slowIndex2)
            return false;
        else{
            for(int i=0;i<slowIndex2;i++)
               if(s[i]!=t[i])
                   return false;
        }
        return true;
    }
};

例题五:

  1. 有序数组的平方
    给你一个按 非递减顺序 排序的整数数组 nums,返回 每个数字的平方 组成的新数组,要求也按 非递减顺序 排序。

示例 1:

输入:nums = [-4,-1,0,3,10]
输出:[0,1,9,16,100]
解释:平方后,数组变为 [16,1,0,9,100]
排序后,数组变为 [0,1,9,16,100]
示例 2:

输入:nums = [-7,-3,2,3,11]
输出:[4,9,9,49,121]

提示:

1 <= nums.length <= 104
-104 <= nums[i] <= 104
nums 已按 非递减顺序 排序

进阶:

请你设计时间复杂度为 O(n) 的算法解决本问题

链接:https://leetcode-cn.com/problems/squares-of-a-sorted-array

代码:

class Solution {
public:
    vector<int> sortedSquares(vector<int>& nums) {
        int n=nums.size();
        vector<int> ans(n);
        for(int i=0,j=n-1,pos=n-1;i<=j;){
            if(nums[i]*nums[i]>nums[j]*nums[j]){
                ans[pos]=nums[i]*nums[i];
                ++i;
            }else{
                ans[pos]=nums[j]*nums[j];
                --j;
            }
            --pos;
        }
        return ans;
    }
};

四、滑动窗口

例题一:

209.长度最小的子数组
题目链接:https://leetcode-cn.com/problems/minimum-size-subarray-sum/

给定一个含有 n 个正整数的数组和一个正整数 s ,找出该数组中满足其和 ≥ s 的长度最小的 连续 子数组,并返回其长度。如果不存在符合条件的子数组,返回 0。

示例:

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

方法一:暴力

方法二:滑动窗口

所谓滑动窗口,就是不断的调节子序列的起始位置和终止位置,从而得出我们要想的结果。

在本题中实现滑动窗口,主要确定如下三点:

  • 窗口内是什么?
  • 如何移动窗口的起始位置?
  • 如何移动窗口的结束位置?

窗口就是 满足其和 ≥ s 的长度最小的 连续 子数组。

窗口的起始位置如何移动:如果当前窗口的值大于s了,窗口就要向前移动了(也就是该缩小了)。

窗口的结束位置如何移动:窗口的结束位置就是遍历数组的指针,窗口的起始位置设置为数组的起始位置就可以了。

解题的关键在于 窗口的起始位置如何移动,如图所示:
在这里插入图片描述

可以发现滑动窗口的精妙之处在于根据当前子序列和大小的情况,不断调节子序列的起始位置。从而将O(n^2)的暴力解法降为O(n)。

class Solution {
public:
    int minSubArrayLen(int s, vector<int>& nums) {
        int result = INT32_MAX;
        int sum = 0; // 滑动窗口数值之和
        int i = 0; // 滑动窗口起始位置
        int subLength = 0; // 滑动窗口的长度
        for (int j = 0; j < nums.size(); j++) {
            sum += nums[j];
            // 注意这里使用while,每次更新 i(起始位置),并不断比较子序列是否符合条件
            while (sum >= s) {
                subLength = (j - i + 1); // 取子序列的长度
                result = result < subLength ? result : subLength;
                sum -= nums[i++]; // 这里体现出滑动窗口的精髓之处,不断变更i(子序列的起始位置)
            }
        }
        // 如果result没有被赋值的话,就返回0,说明没有符合条件的子序列
        return result == INT32_MAX ? 0 : result;
    }
};

在这里插入图片描述

例题二:

  1. 水果成篮

在一排树中,第 i 棵树产生 tree[i] 型的水果。
你可以从你选择的任何树开始,然后重复执行以下步骤:

把这棵树上的水果放进你的篮子里。如果你做不到,就停下来。
移动到当前树右侧的下一棵树。如果右边没有树,就停下来。
请注意,在选择一颗树后,你没有任何选择:你必须执行步骤 1,然后执行步骤 2,然后返回步骤 1,然后执行步骤 2,依此类推,直至停止。

你有两个篮子,每个篮子可以携带任何数量的水果,但你希望每个篮子只携带一种类型的水果。

用这个程序你能收集的水果树的最大总量是多少?

示例 1:

输入:[1,2,1]
输出:3
解释:我们可以收集 [1,2,1]。
示例 2:

输入:[0,1,2,2]
输出:3
解释:我们可以收集 [1,2,2]
如果我们从第一棵树开始,我们将只能收集到 [0, 1]。
示例 3:

输入:[1,2,3,2,2]
输出:4
解释:我们可以收集 [2,3,2,2]
如果我们从第一棵树开始,我们将只能收集到 [1, 2]。
示例 4:

输入:[3,3,3,1,2,1,1,2,3,3,4]
输出:5
解释:我们可以收集 [1,2,1,1,2]
如果我们从第一棵树或第八棵树开始,我们将只能收集到 4 棵水果树。

提示:

1 <= tree.length <= 40000
0 <= tree[i] < tree.length

链接:https://leetcode-cn.com/problems/fruit-into-baskets

思路:

本质就是求最多包含两个(k=2)不同字符的滑窗窗口大小。

使用map来保证只有俩个不同的字符,同时当一个字符的数量变成1的时候,将这个字符从map中清除。

代码:

class Solution {
public:
    int totalFruit(vector<int>& tree) {
        unordered_map<int,int> window;//使用map来完成这一操作,第一个int代表对应的字符,第二个int代表这个字符的数量,当为0的时候,就要清除,并且最多只能由俩个不为0的字符。
        const int k=2;//最多只能有俩中元素
        int res=0;//res的初值为0
        for(int left=0,right=0;right<tree.size();right++){
            window[tree[right]]++;
            //注意这里是while循环
            while(window.size()>k){
                window[tree[left]]--;
                //当为0的时候,将这个元素从其中清除
                if(window[tree[left]]==0){
                    window.erase(tree[left]);
                }
                left++;
            }
            //res取最大
            res=max(res,right-left+1);
        }
        return res;
    }
};

例题三:

  1. 最小覆盖子串

给你一个字符串 s 、一个字符串 t 。返回 s 中涵盖 t 所有字符的最小子串。如果 s 中不存在涵盖 t 所有字符的子串,则返回空字符串 “” 。

注意:如果 s 中存在这样的子串,我们保证它是唯一的答案。

示例 1:

输入:s = “ADOBECODEBANC”, t = “ABC”
输出:“BANC”
示例 2:

输入:s = “a”, t = “a”
输出:“a”

提示:

1 <= s.length, t.length <= 105
s 和 t 由英文字母组成

进阶:你能设计一个在 o(n) 时间内解决此问题的算法吗?

链接:https://leetcode-cn.com/problems/minimum-window-substring

代码:

class Solution {
        
public:
    unordered_map<char,int> ori,cnt;
    
    bool check(){
        for(const auto &p:ori){
            if(cnt[p.first]<p.second)
                return false;
        }
        return true;
    }

    string minWindow(string s, string t) {
        for(const auto &c:t){
            ++ori[c];
        }
        int l=0,r=-1;
        int len=INT_MAX,ansL=-1,ansR=-1;
        while(r<int(s.size())){
            if(ori.find(s[++r])!=ori.end()){
                ++cnt[s[r]];
            }
            while(check()&&l<=r){
                if(r-l+1<len){
                    len=r-l+1;
                    ansL=l;
                }
                if(ori.find(s[l])!=ori.end()){
                    --cnt[s[l]];
                }
                ++l;
            }
        }
        return ansL == -1 ? string() : s.substr(ansL, len);
    }
};

思路:

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

五、数组中的循环

59.螺旋矩阵II
题目地址:https://leetcode-cn.com/problems/spiral-matrix-ii/
给定一个正整数 n,生成一个包含 1 到 n2 所有元素,且元素按顺时针顺序螺旋排列的正方形矩阵。

示例:

输入: 3
输出:
[
[ 1, 2, 3 ],
[ 8, 9, 4 ],
[ 7, 6, 5 ]
]

思路:

模拟顺时针画矩阵的过程:

  • 填充上行从左到右
  • 填充右列从上到下
  • 填充下行从右到左
  • 填充左列从下到上

由外向内一圈一圈这么画下去。

这里一圈下来,我们要画每四条边,这四条边怎么画,每画一条边都要坚持一致的左闭右开,或者左开又闭的原则,这样这一圈才能按照统一的规则画下来。

在这里插入图片描述
这里每一种颜色,代表一条边,我们遍历的长度,可以看出每一个拐角处的处理规则,拐角处让给新的一条边来继续画。

这也是坚持了每条边左闭右开的原则。

一些同学做这道题目之所以一直写不好,代码越写越乱。

就是因为在画每一条边的时候,一会左开又闭,一会左闭右闭,一会又来左闭右开,岂能不乱。

代码如下,已经详细注释了每一步的目的,可以看出while循环里判断的情况是很多的,代码里处理的原则也是统一的左闭右开。

class Solution {
public:
    vector<vector<int>> generateMatrix(int n) {
        vector<vector<int>> res(n,vector<int>(n,0));// 使用vector定义一个二维数组
        int startx=0,starty=0;// 定义每循环一个圈的起始位置
        int loop=n/2;// 每个圈循环几次,例如n为奇数3,那么loop = 1 只是循环一圈,矩阵中间的值需要单独处理
        int mid=n/2;// 矩阵中间的位置,例如:n为3, 中间的位置就是(1,1),n为5,中间位置为(2, 2)
        int count=1;// 用来给矩阵中每一个空格赋值
        int offset=1;// 每一圈循环,需要控制每一条边遍历的长度
        int i,j;
        while(loop--){
            i=startx;
            j=starty;

            // 下面开始的四个for就是模拟转了一圈
            // 模拟填充上行从左到右(左闭右开)
            for(j=starty;j<starty+n-offset;j++){
                res[startx][j]=count++;
            }
             // 模拟填充右列从上到下(左闭右开)
             for(i=startx;i<startx+n-offset;i++){
                 res[i][j]=count++;
             }
              // 模拟填充下行从右到左(左闭右开)
              for(;j>starty;j--){
                  res[i][j]=count++;
              }
               // 模拟填充左列从下到上(左闭右开)
               for(;i>startx;i--){
                   res[i][j]=count++;
               }

                // 第二圈开始的时候,起始位置要各自加1, 例如:第一圈起始位置是(0, 0),第二圈起始位置是(1, 1)
                startx++;
                starty++;

              // offset 控制每一圈里每一条边遍历的长度
            offset += 2;   

        }
         // 如果n为奇数的话,需要单独给矩阵最中间的位置赋值
         if(n%2){
             res[mid][mid]=count;
         }
         return res;
    }
};

例题二:

  1. 螺旋矩阵

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

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
思路:
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
代码:

class Solution {
public:
    vector<int> spiralOrder(vector<vector<int>>& matrix) {
        if(matrix.size()==0||matrix[0].size()==0){
            return {};
        }
        int rows=matrix.size(),columns=matrix[0].size();
        vector<int> order;
        int left=0,right=columns-1,top=0,bottom=rows-1;
        while(left<=right&&top<=bottom){
            for(int column=left;column<=right;column++){
                order.push_back(matrix[top][column]);           
            }
            for(int row=top+1;row<=bottom;row++){
                order.push_back(matrix[row][right]);
            }
            if(left<right&&top<bottom){
                for(int column=right-1;column>left;column--){
                    order.push_back(matrix[bottom][column]);
                }
                for(int row=bottom;row>top;row--){
                    order.push_back(matrix[row][left]);
                }
            }
            left++;
            right--;
            top++;
            bottom--;
        }  
        return order;  
    }
};

总结

二分查找

在这道题目中我们讲到了循环不变量原则,只有在循环中坚持对区间的定义,才能清楚的把握循环中的各种细节。

双指针法

双指针法(快慢指针法):通过一个快指针和慢指针在一个for循环下完成两个for循环的工作。

暴力解法时间复杂度:O(n^2)
双指针时间复杂度:O(n)

这道题目迷惑了不少同学,纠结于数组中的元素为什么不能删除,主要是因为一下两点:

  • 数组在内存中是连续的地址空间,不能释放单一元素,如果要释放,就是全释放(程序运行结束,回收内存栈空间)。
  • C++中vector和array的区别一定要弄清楚,vector的底层实现是array,所以vector展现出友好的一些都是因为经过包装了。

双指针法(快慢指针法)在数组和链表的操作中是非常常见的,很多考察数组和链表操作的面试题,都使用双指针法。

滑动窗口

本题介绍了数组操作中的另一个重要思想:滑动窗口。

暴力解法时间复杂度:O(n^2)
滑动窗口时间复杂度:O(n)

本题中,主要要理解滑动窗口如何移动 窗口起始位置,达到动态更新窗口大小的,从而得出长度最小的符合条件的长度。

滑动窗口的精妙之处在于根据当前子序列和大小的情况,不断调节子序列的起始位置。从而将O(n^2)的暴力解法降为O(n)。

模拟行为

在这道题目中,我们再一次介绍到了循环不变量原则,其实这也是写程序中的重要原则。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值