资讯动态

滑动窗口算法实战:从最小覆盖子串到变体题通解

发布时间:2026/10/15 1:23:53 来源:尧图企业网站定制
1. 这是一道让我刷了三遍才真正懂的题先说个题外话。我在某公司带过几个实习生每次聊到算法面试十个有八个会栽在滑动窗口上。不是没刷过题而是刷完之后过两周再遇到变体——“字符串排列”“最长无重复子串”“找到所有字母异位词”——又像没见过一样。我一直觉得问题就出在很多人只记住了代码模板没理解窗口本质上在做什么。今天想用“最小覆盖子串”这道题把滑动窗口这件事尽可能讲透。题目背景是经典的LeetCode第76题给你一个字符串s一个字符串t要求在s中找出包含t所有字符的最短子串。注意t中重复字符也要被覆盖。比如tABC那子串必须同时有A、B、C各至少一个如果tAABC那子串必须至少有两个A、一个B、一个C。这道题考的不只是“双指针”三个字。它背后是一整套窗口管理的思路什么时候扩右边界、什么时候收缩左边界、用什么数据结构统计字符、如何判断窗口是否“覆盖”了目标串、如何在收缩过程中保持最优解。任何一个环节想得不够清楚代码就很容易写出那种“测试一过、换组数据就崩”的状态。这篇文章不会只丢一段标准答案。我会把这道题从暴力解开始推演讲清楚为什么必须用滑动窗口再一步步拆解窗口的实现细节最后给出完整代码、调试过程、常见坑点以及面试时怎么讲才能让面试官觉得你是真的理解而不是背题。2. 为什么暴力解法撑不住滑动窗口到底解决了什么2.1 暴力解是怎么做的以及它慢在哪先用最直接的方式想要找到s中包含t所有字符的最短子串最简单的方法当然是枚举s中所有子串。假设s的长度是n。s中总共有n(n1)/2个不同子串这个数量级是O(n²)。对每个子串你要统计它里面是否包含t的全部字符这一步如果也用遍历计数的方式做那就是O(n)。整体复杂度瞬间变成O(n³)。稍微长一点的字符串比如几百个字符这个算法就已经肉眼可见地卡顿了。如果你没听说过O(n³)意味着什么简单感受一下n是1000的时候理论上要做10亿次级别的字符统计操作这已经不是你本地跑着玩能忍受的级别了。当然你可以优化一下判断覆盖的过程比如提前把t的字符统计存好每个子串用哈希表增量统计但即使这样你仍然要枚举所有子串整体复杂度还是O(n²)起步而且每换一个起点都要重新开始统计之前的统计结果完全废掉浪费了大量重复计算。这里有个很关键的反省点暴力解到底浪费在哪里答案是它把每一个子串都当成一个孤立的对象没有利用相邻子串之间的重叠关系。举个例子sADOBECODEBANC当你已经统计了子串s[0:5] ADOBEC的字符分布想去判断s[1:6] DOBECO时理论上只需要把A拿掉、把O加上就行。但暴力解会把整个“DOBECO”重新数一遍。这种重复劳动是滑动窗口要消灭的核心浪费。2.2 双指针窗口如何把复杂度压到O(n)滑动窗口的思路其实一句话就能说清楚用一个连续区间从s的左边逐步向右移动区间右端负责“扩”左端负责“缩”全程保证区间内的字符统计是动态维护的。每当窗口覆盖了t的所有字符就尝试左移左边界来缩短窗口记录最短长度一旦收缩到不满足覆盖条件就继续右移右边界循环往复。这样做的妙处在于每个字符至多被右边界扫描一次、被左边界扫描一次所以总体的字符访问次数是O(n)级别窗口内的字符计数也不需要重复计算——右边界扩进一个字符计数器加一左边界缩出一个字符计数器减一。整个过程是“增量式”维护而不是每次重新统计。这也是为什么滑动窗口类题目几乎都有一个共性题目问的是“连续子串/子数组”并且涉及“满足某个条件的最小/最大长度”时优先考虑双指针维护动态窗口。你以后做变体题时把“连续”“最值”“条件覆盖”这三个关键词抓住基本可以快速定位到位方法。提示滑动窗口不是银弹。如果题目不要求连续或者条件满足后不能通过单方向移动来判断收缩那这种双指针结构就不适用。判断时要注意题目里的“连续”二字。2.3 覆盖条件的本质别只看“有”还要看“够”很多人第一次写这道题的判断逻辑时会想得很简单把t的字符放进一个集合窗口内只要包含集合里的所有字符就行。写成代码可能是“for c in t: if c not in window: return false”。这种写法在t没有重复字符的测试用例上能跑对但一旦tAABC局面就尴尬了——窗口里有A、B、C各一个时你的集合判断会认为已经覆盖了实际上A还缺一个。根子在于覆盖的本质不是一个“有没有”的问题而是一个“计数够不够”的问题。t中每个字符都有一个需求量窗口内对应字符的数量必须不小于这个需求量才算覆盖。所以正确的做法是维护两个计数结构一个记录t中每个字符的需求量need另一个记录当前窗口内每个字符的实际数量window。判断覆盖时比较的是每个相关字符在两个结构中的数量大小而不是单纯看存在性。那怎么高效判断“每个字符都够”又不需要每次遍历所有字符类型这里就用上了“满足类型数计数器”——一般记为matched或valid或count。它统计的是当前窗口里有多少种字符已经达到了需求量。比如tAABC字符类型有A、B、C三种当窗口的计数在某一时刻变成A≥2、B≥1、C≥1valid就等于3此时窗口满足覆盖条件。这个优化很关键。如果没有valid计数器每次判断覆盖都要遍历一遍need表的所有键效率会退化有了valid判断覆盖变成了O(1)的等值比较。3. 滑动窗口的完整设计从数据结构到收缩时机3.1 用什么结构存字符统计哈希表还是数组这道题里的字符范围如果只是大小写字母那你完全可以用长度为128的数组来做计数字符的ASCII码直接当下标。用数组的好处是写起来直观、访问速度快而且不用处理哈希表里那些“键不存在”的边界。比如c风格的写法可以直接int need[128] {0}window[128] {0}然后window[s[right]]这种操作在哈希表里还得先判断键存不存在在数组里完全不用纠结。如果你面对的是更复杂的字符集比如Unicode全量字符或者题目明确说字符范围很大那就用哈希表。语言不同写法略有差异Java是HashMapCharacter, IntegerPython是dict。核心思路一样实现上多几个getOrDefault、containsKey的调用。我的建议是笔试或面试时如果题目没明确说字符范围先用代码里方便的方式实现——大多数情况下用数组就够了。但你要能说出“如果字符集会很大我会改用哈希表”这句话这会让面试官觉得你考虑了扩展性而不是只会套模板。3.2 窗口的循环结构先扩后缩并且每一步都问“还满不满足”完整窗口管理的骨架如下初始化left0right0窗口区间记作[left, right)这里推荐左闭右开。不断把right向右移动每次移动都把s[right]纳入窗口并更新window计数。如果某个字符在窗口中的计数恰好等于need中的需求量valid加1。只要valid等于需要满足的字符种类数need中值大于0的key数量说明窗口已经覆盖了t。在满足覆盖条件时尝试收缩先把当前窗口长度记录下来如果比已有答案短就更新答案然后把left向右移动每移出一个字符就更新window计数并判断移出后是否导致该字符不再满足需求量如果是valid减1。重复2~5直到right遍历完整个s。第5步是很多人的盲区。请注意收缩窗口的时机是“满足覆盖条件”时而不是“找到一个之后停一下再继续”。因为我们要的是最短长度所以满足条件时就应该尽可能收缩收缩到刚好无法覆盖然后再继续右移寻找下一个可能的更短答案。这个“满足即收缩”的模式是这类题的通法。还有一种容易犯迷糊的写法在right扩展的过程中也检查是否覆盖一旦覆盖就立即更新答案。这种写法不是不行但如果你在扩展过程中更新那可能漏掉“扩展后窗口覆盖、但收缩一下仍然覆盖且更短”的情形——因为你并没有在收缩路径上去记录更短答案。所以标准做法是每次覆盖后进入收缩循环在收缩的每一轮都记录一次答案这样保证答案是覆盖状态下最靠右压缩的窗口。3.3 窗口区间用左闭右开还是左闭右闭我很推荐在写这种窗口题时统一使用左闭右开区间[left, right)理由有两点。第一初始化时窗口为空left0、right0正好表示空区间不需要额外赋初值漏讨论“空窗口”的边界。第二right表示“下一个待加入的字符下标”所以右移时先加字符再rightleft表示“当前窗口最左字符下标”所以左移时先移除s[left]再left。两个边界的操作逻辑在语义上不会打架。如果你习惯左闭右闭[left, right]也不是不能写但要注意初始化left0, right-1才是空区间右移时是right后加入s[right]左移时是移除s[left]后left。这种写法容易在“空区间”和“左右交换”这种边界上出错很多人写着写着就懵了。我的建议是如果你还在初学阶段直接统一成左闭右开能省不少内耗。注意无论用什么区间定义必须前后一致。最怕的是代码里一会儿用right - left 1算长度一会儿又用right - left这种前后不一致的错误非常难排查。4. 手把手跑通完整代码Python与Java双版本4.1 Python完整实现与逐行解释先说实现整体思路。我们用need字典记录t中每个字符的需求量用window字典记录当前窗口内字符的实际数量。整个代码骨架可以分为外层右移循环、内层收缩循环、判断和更新答案三大部分。下面这段我直接贴完整代码你可以照着跑def minWindow(s: str, t: str) - str: from collections import defaultdict need defaultdict(int) for c in t: need[c] 1 # 需要覆盖的字符类型总数 need_cnt len(need) window defaultdict(int) # valid 表示当前窗口中有多少种字符已经达到需求量 valid 0 left 0 right 0 # 答案初始化最小长度设为一个很大的数 min_len float(inf) ans_start 0 while right len(s): # 右边进入窗口的字符 c s[right] right 1 # 只有当前字符在 need 中才更新 window 和 valid if c in need: window[c] 1 if window[c] need[c]: valid 1 # 当 valid 等于 need_cnt说明所有字符都已满足需求量 while valid need_cnt: # 当前窗口长度 cur_len right - left if cur_len min_len: min_len cur_len ans_start left # 准备收缩移除左边字符 d s[left] left 1 if d in need: if window[d] need[d]: valid - 1 window[d] - 1 return s[ans_start: ans_start min_len] if min_len ! float(inf) else 几个关键点我拆开说。一是字符进入窗口时只有c存在于need中才需要更新window和valid。为什么因为如果c不在t里它对覆盖条件没有任何贡献它进入窗口只会增加窗口长度不可能让valid增长。我们最终寻找的是最短覆盖子串那些无关字符当然是越少越好——它们会让窗口变长但对覆盖没有正向作用。所以代码里用“if c in need”直接跳过能省下不必要的哈希操作。二是valid增加的条件是window[c] need[c]而不是window[c] need[c]。因为valid记录的是“已经达到需求量的字符种类数”一旦等于之后就算再加也不应该再次增加valid。比如窗口里A有3个need里A需要2个在window[A]从2变3的过程中valid已经加过了就不能再加。同理收缩时window[d] need[d]时valid减1说明这字符刚好从满足变成不满足。三是最短的答案记录在收缩的每一轮里。注意我用的窗口长度是right - left因为在左闭右开区间下这正好表示[left, right)的元素个数。比如left0, right6那区间长度是6。这个细节我曾经看到不止一个人写成right - left 1导致长度恒多1输出会截多一个字符。四是最短长度初始化为inf。这里有个小坑如果用0初始化那任何窗口长度都比0大永远得不到更新结果会一直输出空串。这个细节笔试时容易翻车建议直接用浮点inf或者很大的数。4.2 Java版本与哈希表细节处理Java版本思路完全一致主要差别在哈希表操作上。HashMap的getOrDefault是高频操作很多时候你忘了判断键是否存在就直接get()会返回null然后你试图对null做加法直接NPE。我写了不少遍之后习惯了统一使用getOrDefault。class Solution { public String minWindow(String s, String t) { MapCharacter, Integer need new HashMap(); for (char c : t.toCharArray()) { need.put(c, need.getOrDefault(c, 0) 1); } int needCnt need.size(); MapCharacter, Integer window new HashMap(); int valid 0; int left 0, right 0; int minLen Integer.MAX_VALUE; int ansStart 0; while (right s.length()) { char c s.charAt(right); right; if (need.containsKey(c)) { window.put(c, window.getOrDefault(c, 0) 1); if (window.get(c).intValue() need.get(c).intValue()) { valid; } } while (valid needCnt) { if (right - left minLen) { minLen right - left; ansStart left; } char d s.charAt(left); left; if (need.containsKey(d)) { if (window.get(d).intValue() need.get(d).intValue()) { valid--; } window.put(d, window.get(d) - 1); } } } return minLen Integer.MAX_VALUE ? : s.substring(ansStart, ansStart minLen); } }这里有几个Java特有的注意点。一是window.get(c).intValue() need.get(c).intValue()比较的是int值而非Integer引用。因为Java中Integer在-128到127之间有缓存超出这个范围的两个Integer对象用比较可能为false造成valid更新错误。虽然本题中字符计数大概率不会超过127但养成用intValue()或equals判断的习惯没有坏处。二是每次收缩后window.put(d, window.get(d) - 1)。有人会写window.put(d, window.getOrDefault(d, 0) - 1)这其实也没问题只是语义上略显不明。更重要的时不能忘了put回去。Map里的值并不是引用型对象put之后你必须把更新后的值塞回去否则下次get到的还是旧值。三是String.substring的参数区间是[start, end)用ansStart和ansStart minLen来切割正好。4.3 用一个具体例子完整走一遍窗口流程纸上得来终觉浅我们用s ADOBECODEBANCt ABC完整过一遍算法流程。这个例子恰好是题目自带的经典样例非常能说明问题。初始化need {A:1, B:1, C:1}needCnt3window为空valid0left0right0minLeninf。第一轮right指向s[0]A。A在need中window[A]1正好等于need[A]valid变成1。此时valid1不等于3不进入收缩循环。窗口为A。第二轮right指向s[1]D。D不在need中跳过valid仍为1。窗口为AD。第三轮right指向s[2]O。同上窗口变为ADO。第四轮right指向s[3]B。B在need中window[B]1等于need[B]valid变成2。窗口变为ADOB此时已包含A、B缺C。第五轮right指向s[4]E。窗口变为ADOBE。第六轮right指向s[5]C。C在need中window[C]1等于need[C]valid变成3。注意此刻valid needCnt进入收缩循环。收缩循环开始当前区间left0, right6窗口ADOBEC长度为6记录答案ansStart0minLen6。然后移除左边s[0]A。A在need中window[A]本来是1等于need[A]于是valid减1变成2然后window[A]减为0。收缩后valid2不满足覆盖了退出循环。第七轮right指向s[6]O。O不在need中窗口DOBECOvalid保持2。第八轮right指向s[7]D。D不在need中窗口变为DOBECODvalid仍2。第九轮right指向s[8]E。E不在need中窗口DOBECODE。第十轮right指向s[9]B。B在need中window[B]原来是1现在加1变成2。注意window B 不等于need B 所以valid不变仍是2。此时窗口DOBECODEB中B有两个但A和C还是不够。第十一轮right指向s[10]A。A在need中window[A]从0变成1等于need[A]valid变成3。再次进入收缩循环。此刻状态left1, right11窗口区间是s[1:11]OBECODEBA。长度为10比已有的6大不更新答案。然后尝试收缩移除s[left]s[1]O。O不在need中valid不变left变成2。继续收缩移除s[2]B。B在need中window[B]当前为2不等于need[B]所以valid不变window[B]减为1left变成3。继续收缩移除s[3]EE不在need中left变成4。继续收缩移除s[4]C。C在need中window[C]当前为1等于need[C]valid减1变成2window[C]减为0left变成5。此时valid2不满足覆盖退出收缩循环。第十二轮right指向s[11]N。N不在need中窗口继续向右扩。第十三轮right指向s[12]C。C在need中window[C]从0变成1等于need[C]valid变成3。此时窗口区间是s[5:13]BANC长度4比之前的6小更新答案为ansStart5minLen4。然后收缩移除s[5]B。B在need中window[B]当前1等于need[B]valid减1变成2window[B]减为0。退出循环。此时right13等于s长度循环结束。最终返回s[5:54] BANC。流程跑完答案确实正确。这里有一个细节值得再说一次第二次扩张到满足条件时为什么答案是“BANC”而不是“OBECODEBA”或者中间的某个区间因为收缩循环中我们每次都先判断当前长度再移除左边字符。第一次进入收缩循环时left1right11长度是10大于已有最小6所以不更新。移除到left5时区间CODEBA已经不足覆盖缺C。直到再次扩到right13、窗口CODEBANC覆盖后从left5收缩到left6时区间BANC长度4更新答案。整个过程中答案永远是覆盖状态下的最左侧位置。5. 这套思路如何延伸到五类高频变体题5.1 变体一找到字符串中所有字母异位词LeetCode第438题。给定两个字符串s和p返回s中所有p的异位词的起始索引。比如pabc那么在s里找所有包含一个a、一个b、一个c的等长子串位置。这道题和最小覆盖子串的区别在于不再需要“最短”和“收缩”而是要求窗口长度固定等于p的长度。思路几乎相同只需要在valid needCnt时判断窗口长度是否正好等于p的长度如果是记录left然后无论如何都收缩一次把left右移一格保持窗口长度恒定滑动。换句话说最小覆盖子串的收缩条件是“满足覆盖且尽量缩”而异位词题目的收缩条件是“固定长度强制缩”。理解了两者的差异后代码改动其实只差几行。5.2 变体二无重复字符的最长子串LeetCode第3题。这个变体不走“覆盖”路线判断条件变成了“窗口内是否没有重复字符”。实现那题时很多人直接用HashSet记录窗口内字符但更通用的思路其实是维护一个窗口计数器当某个字符的计数超过1时说明窗口内有重复开始收缩。这里甚至不需要need字典valid概念变成了“有重复就收缩直到恢复无重复”。本质上还是一个“满足条件时尽量扩、不满足条件时收缩”的滑动窗口框架。如果你已经理解最小覆盖子串的valid机制再看这道题会感觉非常轻松——无非就是valid的触发条件从“所有字符都达到需求量”变成了“某个字符计数不大于1”。5.3 变体三字符串的排列LeetCode第567题。判断s2中是否存在s1的某个排列。比如s1abs2eidbaooo返回true因为ba出现在s2中。这题几乎是字母异位词题目的换皮版本只不过返回类型从“所有起始索引列表”变成了布尔值。核心逻辑还是固定窗口长度等于s1的长度滑动过程中判断valid是否等于needCnt。如果你把最小覆盖子串的代码改成固定窗口版本这题只需要再改返回值即可基本上五分钟内就能写完。5.4 变体四最长覆盖子串手动反向思考有人会问既然76题是最短覆盖子串那如果题目改成“最长覆盖子串”呢其实最长覆盖问题通常没有唯一解——因为一旦窗口覆盖了t窗口往右扩任意长度它仍然覆盖t最长会变成整个s。所以这类题往往会加额外条件比如“不含多余重复”“不超过k种字符”等让它变成另一种约束下的最值问题。理解了这一点你就会明白最短和最长虽然听起来对称但解题策略完全不同最短需要“满足即收缩”最长往往需要“不满足再收缩”。5.5 变体五最多包含k种字符的最长子串这题也是经典面试题思路是窗口内维护不同字符的种类数。当种类数超过k时收缩左边界否则右移扩大窗口。本质上还是“什么时候收缩”的选择题。从最小覆盖子串到这些变体你会发现核心代码骨架都长得差不多右移right、更新计数器、判断条件、决定收缩时机、记录答案。真正需要训练的是看到不同题目后快速确定三件事——窗口维护的数据是什么、满足条件是什么、收缩的时机是什么。6. 常见错误与调试实录这些坑我全都踩过6.1 坑一valid的加减时机写反了这是我最初写这题时最常犯的错误。最容易出错的场景是扩展字符时写成if window[c] need[c]: valid。这样会导致同一个字符在超过需求量后继续触发valid增长valid会大于needCnt最终收缩条件失效。正确的时机是window[c] need[c]时valid因为这是该字符从“不够”变成“刚好够”的临界点。同理收缩时必须是window[d] need[d]时valid--因为那是它从“刚好够”变成“不够”的临界点。很多人写成window[d] need[d]或者window[d] need[d]结果valid跳来跳去调试到怀疑人生。6.2 坑二窗口长度计算错误左闭右开区间下窗口长度是right - left。我曾经在代码里写成了right - left 1然后在测试用例上跑了很久发现输出老是多一个字符。排查方式也很简单打印每一轮left和right的值以及当前窗口内容很快就发现了。如果你也遇到输出长度不对劲第一件事就看你的区间定义和长度计算是否自洽。左闭右闭就处处用right - left 1左闭右开就处处用right - left千万不要混用。6.3 坑三忘记处理“s中不存在覆盖子串”的情况如果遍历完整个s都没能得到一个覆盖窗口minLen还是初始的inf代码如果直接substring就会出问题。标准做法是最后判断一下minLen是否还是inf如果是返回空字符串。这个错误在绝大多数人能跑过的测试用例里不会触发所以你很可能写完样例测试就交上去了。但面试官或在线判题系统一旦给了sa, taa这种用例就直接暴露问题。提前加一行判断能堵住这种边界漏洞。6.4 坑四把无关字符也加进了window统计有些初学者为了让“窗口统计”逻辑统一对所有进入窗口的字符都执行window[c]包括那些不在need里的字符。这样虽然不会导致valid计算错误因为那些字符永远不会触发valid但会白白增加哈希表的大小并且代码里到处需要判断need.containsKey逻辑变得更乱。规范做法是只在字符属于need时更新window。有人担心这样会丢掉“无关字符”的信息——但想一想覆盖条件只关心t里的字符无关字符数量多少都不影响判断所以根本没有维护的必要。6.5 调试实录我用一个极简样例把valid变化画了出来当初我自己彻底想明白这题是拿一个极简单的例子手推了五遍sABAtAA。那时候我还没画过valid变化表总是觉得valid像玄学。后来我列了一张表每轮记录right、left、window、valid、minLen走完三遍之后整个逻辑就刻在脑子里了。这里我把那个推演过程简要说一下供你参考初始化need{A:2}needCnt1。第一轮right0字符A进窗口window[A]1不等于need A valid不变仍是0。窗口A不足覆盖。第二轮right1字符B进窗口B不在need中跳过。窗口ABvalid仍0。第三轮right2字符A进窗口window[A]变成2等于need[A]valid变成1进入收缩。记录当前窗口A B A长度3minLen3。收缩移除s[0]Awindow[A]从2减到1因为window[A]等于need[A]是2现在不等于2所以valid减为0。此时left1窗口BA不覆盖退出收缩。循环结束返回答案是ABA。如果你亲手画过这个流程你会发现valid的本质就是一个“覆盖状态指示器”它只有在临界点才变化。理解了这一点后续所有变体题都会顺很多。6.6 面试讲解的节奏建议这道题在面试里出现频率不低我建议你在面试官面前讲思路时按这个节奏走第一句先亮本质“这道题是典型的滑动窗口题我用两个指针维护窗口区间用哈希表统计t的字符需求量和窗口内的实际字符量。”第二句讲清楚valid的作用“为了避免每次判断都遍历哈希表我维护一个valid计数器统计已经满足需求量的字符种类数。当valid等于t中出现的字符种类数时窗口覆盖了t。”第三句讲收缩逻辑“每次覆盖后我尝试收缩左边界不断寻找更短的覆盖窗口直到不满足覆盖为止。每轮收缩前记录一次答案。”第四句讲复杂度“每个字符进出窗口各一次所以时间复杂度O(n)空间复杂度O(k)k是字符集大小。”这套话术能让人快速抓住你确实理解了算法本质而不是单纯背题。7. 为什么这道题值得二刷三刷以及我的个人建议如果你现在正在准备算法面试我建议这道题至少亲手写三遍。第一遍照抄标准答案跑通所有测试用例。第二遍关掉答案手写完整代码直到你能一次性编译通过。第三遍换个变体题比如字母异位词那题用同样的框架去解看看你能不能在上面的骨架基础上改出来。我自己带过的人里凡是老老实实走完这三遍的后面遇到滑动窗口题目基本不会再慌。相反那些只刷一遍、背下代码的过两周连valid为什么要先加后减都说不清楚。还有一种更牢固的练习方式拿一个白板或者纸笔把第4节那个推演过程自己走一遍每一步都写下left、right、window、valid的当前值。这个过程虽然看起来很基础但能真正帮你建立对“临界点”的直觉——什么时候valid变化为什么变化以及收缩条件是怎么触发的。有了这种直觉以后写窗口类型的题你根本不需要临时去回忆模板。最后再分享一个小技巧遇到这类“求连续子串最值”的题先别急着写代码花一分钟在纸面上确定三件事——窗口里维护什么统计信息、满足条件是什么、什么时候收缩。三件事想清楚了代码只是翻译工作。这比直接上手敲然后陷入debug泥潭要高效得多。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑