算法系列2:滑动窗口

滑动窗口

1、长度最小的子数组

长度最小的子数组

1.1、题目理解

对于序列2, 3, 1, 2, 4, 3,有以下子序列符合和 >= 7:

  • 2, 3, 1, 2\]、\[2, 3, 1, 2, 4\]、\[2, 3, 1, 2, 4, 3

  • 3, 1, 2, 4\]、\[3, 1, 2, 4, 3

  • 1, 2, 4\]、\[1, 2, 4, 3

  • 2, 4, 3

  • 4, 3

其中,4, 3序列长度最小,返回2。

对于序列1, 4, 4,有以下子序列符合和 >= 4:

  • 1, 4\]、\[1, 4, 4

  • 4\]、\[4, 4

  • 4

其中,4序列长度最小,返回1。

对于序列1, 1, 1, 1, 1, 1, 1, 1,由于没有连续的子序列满足和 >= 11,所以返回0。

1.2、原理分析

第一种方法就是暴力枚举

我们定义下标left, right,都指向下标0位置。

接着right一步一步遍历序列,直到结尾:

而right每跳一步,统计当前区间left, right中所有元素的和,用来比较大小,进而判断是否更新最小长度。

当right到达结尾,left向右一步,right又返回至left位置,重复上面操作:

这种利用left和right双指针遍历的方式,时间复杂度为O(N2);而求和的操作,又需要遍历序列,时间复杂度为O(N)。那么整个算法的时间复杂度就为O(N3)。

我们可以对求和操作进行优化。

right每跳一步,序列和就跟着变化;left向右,right返回,序列和也跟着变化。这样时间复杂度就降为了O(N2)。

我们可以进一步优化。

当left, right从起始位置,到right向右跳时第一次遇到满足条件的序列。

此时right就没必要继续向右了,因为题目规定给出的总序列元素都是正整数,意味着right继续向右时,left, right序列都满足和 >= target条件(单调递增),但是序列长度也增加了,不符合题目要求的最小长度。

这时,更新和,判断是否 >= target,再判断是否更新序列长度(比小)。接着left向右。

此时,left, right序列和不满足要求,那么right向右。

符合,更新和,判断是否 >= target,再判断再判断是否更新序列长度。接着left向右。

不断重复操作。

由于区间left, right在变化过程中,整体是不断向右的,区间长度是不断变化的。所以我们称这种优化方法为滑动窗口

本体采用滑动窗口方法,最坏情况下left, right都将总序列遍历了一遍(可以想象一根皮筋,尾部拉长到一端,头部放回到此时的尾部),时间复杂度为O(2*N),即O(N)。

1.3、代码演示

cpp 复制代码
class Solution {
public:
    int minSubArrayLen(int target, vector<int>& nums) {
        int n = nums.size(), sum = 0, len = INT_MAX;
        //                  由于len的更新是比小,所以我们用最大值赋值
        for (int left = 0, right = 0; right < n; ++right)
        {//                           外层循环的进行
            sum += nums[right];// 更新子序列和
            while (sum >= target)
            {// 找到一种情况
                len = min(len, right - left + 1);// 更新len
                sum -= nums[left];// 更新子序列和
                ++left;// 内层循环的进行
            }
        }
        return len == INT_MAX ? 0 : len;
    }
};

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

无重复的最长子串

既然需要无重复的子串,那么我们就需要一个可以去重的容器------哈希表。

但是我们并不需要一个真的哈希表,只需开辟一块静态数组hash,每个字符的ASCII值作为下标映射到数组中即可。

接着使用滑动窗口:

  • 定义left, right指针
  • right向右进窗口,遇到一个字符就入映射入hash
  • 当right遇到重复字符,即right指向字符在hash中出现了两次,开始出窗口:
    • left指向的字符滑出hash,然后left向右
    • 当right指向字符在hash中只出现了一次时,出窗口结束
  • 更新序列长度。

left一直出窗口的过程中,当某一个left指向数滑出hash后,right指向数在hash内出现次数恢复为1,说明此时left已经滑出了一段无重复的子串,这个子串是

  • 边界与重复序列
  • 或者重复序列与重复序列

之间的最长无重复子串。也就是说当前题目的序列长度是随着right移动一步就更新一次的。当left再向前到重复序列的下一个字符位置时,开始下一轮滑动窗口。

cpp 复制代码
class Solution {
public:
    int lengthOfLongestSubstring(string s) {
        int ret = 0;         // 返回结果
        int hash[128] = {0}; // 哈希表

        for (int left = 0, right = 0; right < s.size(); ++right)
        {
            hash[s[right]]++; // 进窗口

            while (hash[s[right]] == 2) // 判断
                hash[s[left++]]--; // 出窗口

            ret = max(ret, right - left + 1); // 更新结果
        }

        return ret;
    }
};

3、最大连续1的个数 III

最大连续1的个数 III

我们直接翻转0为1去解题,似乎很难下手,所以我们将题目转化为:

对于给出序列,找出最长子序列 ,其中0的个数小于等于k。所以我们就需要统计0的个数,做为出窗口的判断依据。

此时就可以使用滑动窗口方法解题:

  • right向右进窗口,遇到0就++zero
  • 当zero > k,开始出窗口:
    • left向右
    • 如果left遇到0,先zero自减,再left向右
  • 更新序列长度
cpp 复制代码
class Solution {
public:
    int longestOnes(vector<int>& nums, int k) {
        int ret = 0; // 序列长度
        zero = 0; // 统计0的个数
        for (int left = 0, right = 0; right < nums.size(); ++right)
        {
            if (nums[right] == 0) zero++;

            while (zero > k)
                if (nums[left++] == 0)
                    zero--;
            
            ret = max(ret, right - left + 1);
        }

        return ret;
    }
};

4、将 x 减到 0 的最小操作数

将 x 减到 0 的最小操作数

乍一看,我们无从下手,因为序列两边都可以出数据。

我们可以对题目进行进一步转化,转化的思想是正难则反:

假设序列左边存在子序列arr1,子序列和为a;序列右边存在子序列arr2,子序列和为b,且有:
a + b = x
此时剩余序列arr3,其序列和target满足:
target = 总序列和 - x 当arr1与arr2拼起来最短,arr3就最长。

所以问题转化为:

对于给出序列,找出最长序列,最长序列的序列和满足:
最长序列的序列和 = 总序列和 - x

我们就可以使用滑动窗口方法:

cpp 复制代码
class Solution {
public:
    int minOperations(vector<int>& nums, int x) {
        // 求出target
        int total = 0;
        for (auto& e : nums)
            total += e;
        int target = total - x;

        // 如果x比total还大,就不可能找到了
        if (target < 0) return -1;

        int ret = -1, sum = 0;
        for (int left = 0, right = 0; right < nums.size(); right++)
        {
            sum += nums[right];

            while (sum > target)
                sum -= nums[left++];
            
            if (sum == target) // 只有相等才交换
                ret = max(ret, right - left + 1);
        }

        if (ret == -1) return ret;
        else 
        	return nums.size() - ret;
    }
};

5、水果成篮

水果成篮

阅读题干,提炼分析:

  • 每棵树上只有一种水果
  • 两个篮子,意味着最多只能装两种水果
  • 摘水果的过程,是向右一棵一棵接着摘的,意味着过程是连续的
  • 遇到了第三种水果的树,就停下

所以问题可以转换成:

对于给出序列,找出最长子序列,这个子序列只能包含两种数。

滑动窗口的思路并不是怕凭空想出来的,而是从暴力解法出发,一步步分析出来的。

5.1、暴力解法

right探路,遇到两种以内的值都向前,此时right与left之间都是合法区间。可以更新需要返回的区间长度ret:
ret = right - left + 1

当right遇到第三种数,right停止探路,left向右一步,right返回来继续向前探路:

然后left每次向后一格,重复之前操作,直到left遍历完整个序列。

某一种数可能有多个,所以我们定义一个hash,right向右每遇到一种数就存入hash,从而达到去重的目的。

cpp 复制代码
class Solution {
public:
    int totalFruit(vector<int>& fruits) {
        int ret = 0;
        unordered_set<int> um;
        for (int left = 0; left < fruits.size(); left++)
        {
            int right = left;
            while (right < fruits.size())
            {
                um.insert(fruits[right]);
                if (um.size() == 3)
                {
                    um.clear();
                    break;
                }
                ret = max(ret, right - left + 1);
                right++;
            }
        }

        return ret;
    }
};

5.2、滑动窗口

不难看出,left每向后一位,最差情况下right都要从left遍历到结尾,那么时间复杂度就是O(N2)。当然上面代码也出现了超时的问题,需要思考如何优化。

right一定要往回走吗?

如果left往前一步,right回到left处再向右探路,那么还是会遇到红叉处的第三种数,right还得回去。此时left值下得出的序列肯定比前一个left值下的序列要短。

在一个序列中,第三种数与边界,第三种数与第三种数之间,都存在一个各自独有的最长序列。暴力解法将每一个最长序列的,从左开始缩进一位的子序列也遍历到了。

我们为什么找到了其中一个最长序列,就直接找下一个最长序列呢?那就让left向右吧。

当right指向了第三数,left就可以不断出数、向右,直到hash里只有两种数,这就又是一个合法的区间,right就又可以向右了。

这里有一个问题,如果我们只存数本身,即只存数的种类,当一个数有多个的时候,我们如何保证left能出完所有的数,达到正确的位置呢?

所以我们应该使用一个存数的种类与数量的键值对的容器:unordered_map。

上面优化思路中,left与right指针的移动是同向的,所以我们可以采用滑动窗口的方法。

cpp 复制代码
class Solution {
public:
    int totalFruit(vector<int>& fruits) {
        int ret = 0;
        unordered_map<int, int> hash;
        for (int left = 0, right = 0; right < fruits.size(); ++right)
        {
            hash[fruits[right]]++;

            while (hash.size() == 3)
            {
                hash[fruits[left]]--;
                if (hash[fruits[left]] == 0)
                    hash.erase(fruits[left]); // 某一个数出现次数为0了,即出窗口出完了,就要滑出hash了
                
                left++;
            }

            ret = max(ret, right - left + 1);
        }

        return ret;
    }
};

5.3、进一步优化

提交上面代码,结果却不尽人意:执行用时不太优秀。

其实滑动窗口本身的效率是比较优秀的,但是unordered_map的相关操作是比较耗时的。

有没有更好的办法?

题中给出的序列长度,及每个数,都不超过105

所以我们可以不用unordered_map,而是只用一个数组hash进行映射存储,然后另外定义一个种类计数器kinds:

cpp 复制代码
class Solution {
public:
    int totalFruit(vector<int>& fruits) {
        int ret = 0;
        int kinds = 0; // 数的种类
        int hash[100001] = {0};
        for (int left = 0, right = 0; right < fruits.size(); ++right)
        {
            if (hash[fruits[right]] == 0)
                kinds++;
            hash[fruits[right]]++;

            while (kinds == 3)
            {
                hash[fruits[left]]--;
                if (hash[fruits[left]] == 0)
                    kinds--;
                
                left++;
            }

            ret = max(ret, right - left + 1);
        }

        return ret;
    }
};

这时我们可以来分析一下时间复杂度和空间复杂度:

  • 空间复杂度:使用了常数级额外空间,O(1)。
  • 时间复杂度:right向右遍历序列,当遇到第三种数时,left才开始向右遍历序列。最坏情况下,left与right各自都把序列遍历了一遍,时间复杂度是O(N)

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

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

什么是异或词?

比如,

  • abc
  • acb
  • bac
  • bca
  • cab
  • cba

他们都互为异或词。异或词就是字符的排列顺序不一样

如何判断是否互为异或词?

排序 + 遍历判断?不太行,排序的时间复杂度是O(NlogN),遍历的时间复杂度是O(N),合起来就是O(NlogN),效率不是很高。

我们可以将两个异或词,分别装入两个哈希表 :hash1, hash2。然后遍历哈希表判断字符出现的次数是否相等,即判断哈希表是否相等即可。

暴力解法?

依旧是right右去探路,探完路后left向右,right返回。

但是,依旧可以做优化:right有必要回来吗?

当right向右,走到超过字符串p的长度的时候,我们不妨让left向右一步,此时长度恢复与p相同,这时我们只需每次right一步、left一步,即left, right维护的窗口的长度是固定的

滑动窗口:

  1. left = 0, right = 0
  2. 将字符串p每个字符存入hash1
  3. right向右遇到字符就入hash2 ------ 入窗口
  4. 如果left, right之间的长度超过了字符串p,出窗口:
    • 只需left出掉当前指向字符,然后向右一步
  5. 比较hash1, hash2是否相等,更新结果
cpp 复制代码
class Solution {
public:

    bool Check(int* hash1, int* hash2) // 比较哈希表的函数
    {
        for (int i = 0; i < 26; ++i)
        {
            if (hash1[i] != hash2[i])
                return false;
        }
        return true;
    }

    vector<int> findAnagrams(string s, string p) {
        vector<int> ret;

        int hash1[26] = {0};
        for (auto& ch : p)
            hash1[ch - 'a']++; // p入hash1
        
        int hash2[26] = {0};
        int sz = p.size();
        for (int left = 0, right = 0; right < s.size(); ++right)
        {
            hash2[s[right] - 'a']++; // 入窗口

            if (right - left + 1 > sz) // 判断
                hash2[s[left++] - 'a']--; // 出窗口

            if (Check(hash1, hash2))
                ret.push_back(left);// 更新结果
        }

        return ret;
    }
};

优化?

我们这里对哈希表进行比较的方法是直接遍历比较,有没有更好的方法?

我们可以定义count:记录hash2中有效字符的个数。

我们用下面序列c,与p = "abc",来具体演示一遍count到底是怎么用的:
c c b a e b a b a c d

hash1中a, b, c三个字符分别出现了1次。hash2中无非出现a, b, c, d, e五个字符。

right指向c,先入c进hash2。此时hash2中c的个数是1,(小于)等于hash1中c的个数,说明此时入的c是有效的,count++。

right向右指向c,先入c。此时hash2中c的个数是2,大于hash1中c的个数,说明此时入的c是无效的,count不动。

right向右指向b,先入b。此时hash2中b的个数是1,(小于)等于hash1中b的个数,说明此时入的b是有效的,count++。

right向右指向a,先入a。此时hash2中a的个数是1,(小于)等于hash1中a的个数,说明此时入的a是有效的,count++。

但是,此时left与right之间的序列,超过了p的长度,需要left出字符。

left需要出此时的c,出之前我们先看一看,c在hash2的个数为2,超过hash1中c的个数,说明此时出的c是无效的(多余的),count不动。

此时我们发现,count正好等于p的长度,而left与right之间的序列,正好是abc的异或词。我们就可以记录left所在的位置。

至此,我们可以提炼出四种规则:

  • right入字符后,如果right指向的字符在hash2出现的次数,小于等于在hash1出现的次数,count++
  • left出字符前,如果left指向的字符在hash2出现的次数,小于等于在hash1出现的次数,count++
  • 其余情况count不动
  • 当count == p.size(),就找到了异或词,记录left

那么我们就可以对代码做进一步优化:

cpp 复制代码
class Solution {
public:
    vector<int> findAnagrams(string s, string p) {
        vector<int> ret;
        int count = 0; // 记录有效字符个数

        int hash1[26] = {0};
        for (auto& ch : p)
            hash1[ch - 'a']++; // p入hash1
        
        int hash2[26] = {0};
        int sz = p.size();
        for (int left = 0, right = 0; right < s.size(); ++right)
        {
            hash2[s[right] - 'a']++; // 入窗口
            if (hash2[s[right] - 'a'] <= hash1[s[right] - 'a']) // 判断是否有效
                count++;

            if (right - left + 1 > sz) // 判断
            {
                if (hash2[s[left] - 'a'] <= hash1[s[left] - 'a']) // 判断是否有效
                    count--;
                hash2[s[left++] - 'a']--; // 出窗口
            }

            if (count == sz)
                ret.push_back(left);// 更新结果
        }

        return ret;
    }
};

我们甚至可以把代码改得再精简一点:

cpp 复制代码
class Solution {
public:
    vector<int> findAnagrams(string s, string p) {
        vector<int> ret;
        int count = 0; // 记录有效字符个数

        int hash1[26] = {0};
        for (auto& ch : p)
            hash1[ch - 'a']++; // p入hash1
        
        int hash2[26] = {0};
        int sz = p.size();
        for (int left = 0, right = 0; right < s.size(); ++right)
        {
            if (++hash2[s[right] - 'a'] <= hash1[s[right] - 'a']) count++;

            if (right - left + 1 > sz)
            {
                char out = s[left++];

                if (hash2[out - 'a']-- <= hash1[out - 'a']) count--;
            }

            if (count == sz)
                ret.push_back(left);// 更新结果
        }

        return ret;
    }
};

7、串联所有单词的子串

串联所有单词的子串

我们在第6题中,对于一段长序列找的是异或词 ,组成异或词的是一个个字符。现在一个个字符变成了字符串

由于题目要求这一个个字符串的长度相等 ,所以我们不妨将字符串打包在一起

问题就转化成了寻找异或词的问题

但是串联子串问题的解决,与之前寻找异或词的问题解决方式有三点不同:

哈希表的选取与使用

由于异或词是一个个字符组成的,我们可以将字符直接按照ASCII值映射到一个数组中。

而串联子串面对的不再是字符,而是字符串。所以我们使用unordered_map,记录string与int的键值对,即字符串的出现次数。

left与right的读取与移动

left与right读取的不再只是当前指向的一个字符,而是从当前位置开始,数(shu三声)字符串长度个字符,再拼成字符串入哈希表。

left与right的移动,也应该跳过一个字符串的长度

滑动窗口执行的次数

对于这个序列,left与right从下标0开始就能找到串联子串。

可如果是这个序列呢?

left与right就必须向后一位开始进行滑动窗口,才能找到串联子串。

所以,当left, right从下标0开始滑动窗口,结束后还必须返回;接着left, right从下标1开始滑动窗口...

什么时候结束呢,即滑动窗口总共执行多少次呢?

答案是字符串有多长,就执行多少次。

同时我们也可以观察到,每一次滑动窗口中,right走到尽头时,只要再跨过一个字符串长度,right就一定会达到或者大于总字符串长度(s.size())。所以我们可以依据这个现象,设计right的遍历范围

我们就可以开始编写代码了:

cpp 复制代码
class Solution {
public:
    vector<int> findSubstring(string s, vector<string>& words) {
        vector<int> ret;
        int len = words[0].size(), num = words.size(); // 字符串长度 & 字符串数量

        unordered_map<string, int> hash1;
        for (auto& w : words)
            hash1[w]++; // 将words的字符串入哈希表hash1

        for (int i = 0; i < len; ++i)
        {
            unordered_map<string, int> hash2; // hash2一定要建在里面,每次滑动窗口结束后,要刷新
            // count也要放在里面,每次滑动窗口结束后要刷新
            for (int left = i, right = i, count = 0; right + len <= s.size(); right += len)
            {
                string in = s.substr(right, len); 
                hash2[in]++; // 入窗口
                if (hash2[in] <= hash1[in]) count++; // 判断,维护count

                if (right - left + len > len*num)
                {
                    string out = s.substr(left, len);
                    left += len;
                    if (hash2[out] <= hash1[out]) count--; // 判断,维护count判断
                    hash2[out]--; //出窗口
                }

                if (count == num) ret.push_back(left);
            }
        }

        return ret;
    }
};

但是结果不是很好。因为unordered_map的重载 ,对于不存在的字符串也会先插入,有一定的损耗。所以我们可以做一步限制:当字符串在hash1中不存在时,就不进行判断。

cpp 复制代码
public:
    vector<int> findSubstring(string s, vector<string>& words) {
        vector<int> ret;
        int len = words[0].size(), num = words.size();

        unordered_map<string, int> hash1;
        for (auto& w : words)
            hash1[w]++;

        for (int i = 0; i < len; ++i)
        {
            unordered_map<string, int> hash2;
            
            for (int left = i, right = i, count = 0; right + len <= s.size(); right += len)
            {
                string in = s.substr(right, len); 
                hash2[in]++;
                if (hash1.count(in) && hash2[in] <= hash1[in]) count++; // 不存在的就不插入

                if (right - left + len > len*num)
                {
                    string out = s.substr(left, len);
                    left += len;
                    if (hash2.count(out) && hash2[out] <= hash1[out]) count--; // 不存在的就不插入
                    hash2[out]--;
                }

                if (count == num) ret.push_back(left);
            }
        }

        return ret;
    }
};

8、最小覆盖子串

最小覆盖子串

首先我们要知道,对于s与t,s的最小覆盖子串满足的条件是:

最小覆盖子串包含t中所有种类 的字符,并且最小覆盖子串每种字符的数量均需大于等于t中对应字符的数量。

8.1、暴力解法

我们定义left, right,找到符合要求的子串就停下;如果子串比之前的要短,记录子串的初始位置与最短长度;然后left向前一位,right返回,继续找符合要求的子串...

能不能对暴力解法作一步优化------right真的要返回吗?

8.2、滑动窗口

  • 进窗口:hash2in++,right向右
  • 判断:Check(hash1, hash2)
    • 更新:起始位置,最短长度
    • 出窗口:hash2out++,left向右

对于判断的Check(hash1, hash2),如果我们遍历判断,那么时间复杂度比较大。能不能作进一步优化?

8.3、进一步优化

创建hash1放入t,此时t有:

  • A : 1
  • B : 1
  • C : 1

创建hash2针对s进行进窗口与出窗口。

当right找到一个符合的序列时,做好记录:

  • 如果比之前短,更新初始位置与最短长度
  • 否则不记录

此时,s中left, right之间有:

  • A : 1
  • B : 1
  • C : 1

left要出窗口,此时A在hash1的数量等于在hash2的数量;如果left出A,那么A在hash2的数量变为0,小于A在hash1的数量。

此时left出窗口停止,right向右进窗口。

找到了下一个序列,此时left, right之间有:

  • A : 1
  • B : 2
  • C : 1

left出窗口,直到指向B:

left出之前,B在hash2的数量是2,大于B在hash1的数量,所以left还可以继续向右出窗口。直到left指向C:

此时,left, right之间有:

  • A : 1
  • B : 1
  • C : 1

left出之前,C在hash2的数量是1,等于B在hash1的数量;当left出了C,C在hash2的数量就小于hash1中的数量,那么right就要向右找符合数量的C。

根据以上过程,我们就可以提炼出优化的方法:

  • 定义count,记录有效种类
  • right进窗口向右。当进完一次后right指向的字符在hash2的数量等于在hash1的数量,此时这个字符就是有效种类,count++。
  • left出窗口向右。当出完一次前left指向的字符在hash2的数量等于在hash1的数量,此时这个字符刚好就是有效种类,出了一个就不是有效种类了,count自减。
  • 当count等于t中字符种类数时,就可以进行初始位置与最小长度的更新。

代码

cpp 复制代码
class Solution {
public:
    string minWindow(string s, string t) {
        int begin = -1, minlen = INT_MAX;
        // 起始可能在开头    长度要求比小

        int kinds = 0;
        int hash1[128] = {0};
        for (auto& ch : t)
        {
            if (hash1[ch] == 0) kinds++; // 统计种类数
            hash1[ch]++; // 入hash1
        }

        int hash2[128] = {0};
        for (int left = 0, right = 0, count = 0; right < s.size(); ++right)
        {
            char in = s[right];
            hash2[in]++; // 进窗口
            if (hash2[in] == hash1[in]) // 维护count
                count++;

            while (count == kinds) // 判断
            {
                if (right - left + 1 < minlen) // 更新
                {
                    begin = left;
                    minlen = right - left + 1;
                }

                char out = s[left++];
                if (hash2[out] == hash1[out]) // 维护count
                    count--;
                hash2[out]--; // 出窗口
            }
        }

        if (begin == -1) return ""; // begin没有变动,说明不存在最小覆盖子串,返回空串
        else return s.substr(begin, minlen);
    }
};
相关推荐
Herbert_hwt1 小时前
C语言零基础入门:循环控制与数据类型详解
c语言·数据结构·算法
Tisfy3 小时前
LeetCode 3090.每个字符最多出现两次的最长子字符串:二重循环 / 滑动窗口
算法·leetcode·字符串·题解·模拟·双指针·滑动窗口
-dzk-3 小时前
【技巧】LC 136.只出现一次的数字
算法·异或
有点。3 小时前
C++二叉树二(练习题)
数据结构·c++·算法·图论
专注仿真4 小时前
问答大模型技术方案算法实现-RAPTOR树构建算法与BEG集成使用
python·算法
zlinear数据采集卡4 小时前
数据采集卡从入门到精通(10):采样率与分辨率的核心关系——反比律、架构分布与过采样
arm开发·嵌入式硬件·算法·fpga开发·架构·开源
GeekZHR5 小时前
C语言指针进阶补充6:动态内存管理、mem系列内存函数、复杂指针声明,一次补齐指针的“三大盲区“
java·c语言·算法·指针
Herbert_hwt5 小时前
第七章 Java深入理解枚举类型
java·开发语言·算法
.道阻且长.6 小时前
8.LeetCode算法习题讲解--滑动窗口--长度最小的子数组
算法·leetcode·职场和发展