尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

双指针算法详解:左右指针、快慢指针与滑动窗口实战

双指针算法详解:左右指针、快慢指针与滑动窗口实战 双指针这个名号在算法面试和实际工程里出现的频率有多高不用我多说。凡是刷过LeetCode、牛客或者看过几本算法书的朋友基本都跟它打过照面。但说实话很多人对双指针的理解停留在“两个下标来回动”这个层面遇到具体题目还是发懵——什么时候用左右指针什么时候用快慢指针什么时候该上滑动窗口边界到底怎么处理这些问题不搞清楚刷多少题都是白刷。我最早接触双指针是在处理数组去重和链表环检测的时候当时觉得这玩意儿不就是两个循环变量嘛有啥可讲究的。后来被几道经典题反复教做人才意识到双指针的真正价值在于它能把暴力解法里“重复扫描”的那部分冗余计算给消掉把O(n²)的复杂度干到O(n)。这篇文章我不打算泛泛而谈而是把双指针最常见的三大形态——左右指针、快慢指针、滑动窗口——逐个拆开配合完整代码和踩坑记录讲清楚每种形态适合什么场景、写的时候要注意什么。无论你是刚入门的小白还是刷题卡在中间段的选手这篇文章都能给你一些实在的参考。1. 双指针的核心思路与应用场景先想一个问题在一个有序数组里找两个数让它们的和等于目标值你会怎么写最朴素的做法是两层循环暴力枚举每个数都跟后面的数加一遍时间复杂度O(n²)。数据量小还好一旦数组长度到了10的4次方以上基本就跑不动了。双指针的思路是既然是排好序的数组那我直接让一个指针站在最左边另一个站在最右边然后把两个指针指的数加起来看结果。如果和大于目标值说明右边那个数太大了右指针往左挪一步如果和小于目标值说明左边那个数太小了左指针往右挪一步。整个过程两个指针只往中间走不会回头所以每个元素最多被访问一次时间复杂度只有O(n)。这个例子很好地概括了双指针的本质利用数据结构本身的顺序特性让指针的移动方向具备明确的信息量从而淘汰掉大量无效的组合检查。它不像二分查找那样要求严格的有序性也不像动态规划那样需要维护状态表它更多是靠“左右夹逼”或者“一快一慢”这种朴素但高效的移动策略来缩减搜索空间。双指针适合处理的问题大概有这么几类有序或部分有序数组中的查找类问题比如两数之和、三数之和、最接近的三数之和数组原地操作的题型比如移除元素、去重、移动零链表结构中的环检测、找中点、找倒数第K个节点子串和子数组类的问题通过滑动窗口维护一段连续区间从逻辑上看双指针可以理解为“用更少的遍历次数去完成原本需要多次遍历才能完成的事情”。它省去的不是思考而是实实在在的循环开销。很多看起来跟“指针”八竿子打不着的题比如盛最多水的容器、接雨水本质上也能用左右指针写出很漂亮的O(n)解法。1.1 为什么双指针能比暴力快这么多核心在于它改变了遍历的组合性质。暴力解法是在所有可能的数对里找答案数对的数量是n×(n−1)/2这就是O(n²)的来源。双指针从头尾双向逼近时每一步都会排除掉跨越当前指针位置的一整批组合而不是仅仅排除一个数对。我经常跟朋友打一个比方暴力解法像你去超市买两件商品试遍货架上任意两种商品的搭配双指针则是你事先知道商品按价格排了序太贵的那一批直接不看太便宜的也不用回去找。每一步淘汰的不是一个商品而是“一整段不满足条件的可能性”。这才是省时间的本质。1.2 双指针的三种基本形态按移动方式双指针大致可以分成三类这也是这篇文章后面要重点展开的内容左右对撞指针一个从左往右一个从右往左最终相遇或交叉。典型场景有序数组、回文串判断、容器盛水。快慢指针两个指针同向移动但速度不同。典型场景链表成环检测、链表找中点、数组中的重复数。滑动窗口两个指针同向移动右指针负责扩展区间左指针负责收缩区间维护一个“窗口”。典型场景最长无重复子串、最小覆盖子串、长度最小的子数组。三种形态各有各的边界处理和移动逻辑不能混着用。下面我分三章把每种形态的思考方式和代码模板都过一遍。2. 左右对撞指针从两端夹逼才是经典左右指针最常见的应用场景是“在一个区间里找满足某种条件的两个元素”。它要求数据具备某种单调性质或者说在指针移动的过程中你能明确判断出“下一步该移动哪一边”。拿最经典的两数之和 II输入有序数组来说题目给一个已经升序排列的数组和一个目标值要求找到两个数使它们的和等于目标值返回这两个数的下标。代码大概是这样的#include vector using namespace std; vectorint twoSum(vectorint numbers, int target) { int left 0; int right numbers.size() - 1; while (left right) { int sum numbers[left] numbers[right]; if (sum target) { return {left 1, right 1}; // 题目要求下标从1开始 } else if (sum target) { left; // 和太小左边往右走让和变大 } else { right--; // 和太大右边往左走让和变小 } } return {-1, -1}; }注意看核心逻辑sum target时左指针右移sum target时右指针左移。这个判断依赖于一个前提——数组是升序的。因为只有在升序数组里右移左指针才能让“和”严格变大左移右指针才能让“和”严格变小。如果没有这个单调性做保证双指针的移动方向就没有依据算法直接失效。这就是左右指针的灵魂每一步你都明确知道自己为什么要移动某个指针而不是靠感觉瞎移。如果你写左右指针题的时候发现自己在用if判断某个指针该不该动却说不清理由那多半是思路还没理清楚。2.1 三数之和把一个双指针套进循环里两数之和会了之后三数之和就是顺水推舟的事情。题目要求找到所有不重复的三元组使得三个数之和为0。暴力解法需要三层循环O(n³)直接爆炸。常见的优化思路是先排序然后固定一个数剩下的区间就变成了“两数之和”问题。#include vector #include algorithm using namespace std; vectorvectorint threeSum(vectorint nums) { vectorvectorint result; sort(nums.begin(), nums.end()); int n nums.size(); for (int i 0; i n - 2; i) { // 跳过重复元素 if (i 0 nums[i] nums[i - 1]) continue; int left i 1; int right n - 1; while (left right) { int sum nums[i] nums[left] nums[right]; if (sum 0) { result.push_back({nums[i], nums[left], nums[right]}); // 跳过左边重复元素 while (left right nums[left] nums[left 1]) left; // 跳过右边重复元素 while (left right nums[right] nums[right - 1]) right--; left; right--; } else if (sum 0) { left; } else { right--; } } } return result; }这里有个很容易踩的坑去重必须放在找到一个结果之后而不是在移动指针之前。如果你先跳过了所有重复元素再存结果那确实是没重复了但可能把正确答案也跳没了。正确的做法是先存一个合法三元组然后把左右指针都移到不重复的位置上再继续下一轮查找。另一个坑是循环外层的去重条件。if (i 0 nums[i] nums[i - 1]) continue;这个判断用的是nums[i] nums[i - 1]而不是nums[i] nums[i 1]。别小看这个差别如果写成后者你会把i刚选中的固定数直接跳过导致漏掉一部分合法组合。我见过不少朋友在这里摔跟头刷题最怕的就是这种细节。2.2 盛最多水的容器与接雨水左右指针的进阶玩法这两道题都很经典也都属于“一眼看不出双指针”的类型但本质上都是通过左右指针不断缩小搜索区间同时记录当前最优值。盛最多水的容器题目给了一个数组每个数字代表对应位置挡板的高度要你找两根挡板使得它们和底部构成的容器能装最多的水。面积公式是min(height[left], height[right]) * (right - left)。每次移动较短的那根挡板因为如果你移动较高的那根新的面积只会更小底边变短了高最多不变。代码写出来非常短#include vector using namespace std; int maxArea(vectorint height) { int left 0; int right height.size() - 1; int maxWater 0; while (left right) { int currentWater min(height[left], height[right]) * (right - left); if (currentWater maxWater) maxWater currentWater; if (height[left] height[right]) { left; } else { right--; } } return maxWater; }接雨水那道题其实更复杂一点有很多种解法包括动态规划、单调栈、双指针。双指针解法的关键在于你要知道当前位置能存多少水取决于它左边最大值和右边最大值中较小的那个。维护leftMax和rightMax两个变量哪边的最大值更小就处理哪边因为较小那个已经确定了当前列的储水上限。这类题说白了就是“对撞贪心”的组合。它不仅仅是在移动指针其实是在不断地证明当前这一步的选择已经排除了其他所有可能更优的情况所以可以放心大胆地收缩搜索范围。这种证明意识很重要它能让你的解法不是“凑出来的”而是“推出来的”。3. 快慢指针链表题的常客跟左右指针不同快慢指针是两个指针同向而行但速度不一样。最常见的场景是链表。一个最经典的问题是判断链表中是否有环。你想如果链表有环你让一个指针每次走两步另一个指针每次走一步那它们最终一定会在环里相遇。这背后的思想是两个人绕着操场跑步跑得快的人总有一天会追上跑得慢的人。struct ListNode { int val; ListNode *next; ListNode(int x) : val(x), next(nullptr) {} }; bool hasCycle(ListNode *head) { ListNode *slow head; ListNode *fast head; while (fast ! nullptr fast-next ! nullptr) { slow slow-next; // 慢指针走一步 fast fast-next-next; // 快指针走两步 if (slow fast) { return true; } } return false; }这里有两点值得注意。第一循环条件必须同时判断fast ! nullptr和fast-next ! nullptr否则快指针在移动的时候可能访问空指针。第二如果链表无环快指针会先到达链表末尾循环自然退出如果有环两指针必然相遇不会死循环。3.1 环形链表 II找到环的入口进阶的问题是不光要判断有没有环还要找到环的入口节点。这个问题的推导很有意思。假设链表起点到环入口的距离是a环入口到快慢指针相遇点的距离是b相遇点再走到环入口的距离是c也就是说环的周长是b c。慢指针走了a b快指针走了a b k*(bc)其中k是快指针在环里多绕的圈数。因为快指针速度是慢指针的两倍所以2 * (a b) a b k*(b c)化简得a k*(b c) - b (k-1)*(bc) c也就是说从链表起点到环入口的距离等于从相遇点继续走若干圈环之后再到环入口的距离。代码上最简洁的实现是找到相遇点后把一个指针指向头节点然后两个指针都每次走一步再次相遇的地方就是环入口。ListNode *detectCycle(ListNode *head) { ListNode *slow head; ListNode *fast head; while (fast ! nullptr fast-next ! nullptr) { slow slow-next; fast fast-next-next; if (slow fast) { // 有环寻找入口 ListNode *ptr head; while (ptr ! slow) { ptr ptr-next; slow slow-next; } return ptr; } } return nullptr; }这个推导过程如果不亲手算一遍很容易忘记。我建议你拿出纸笔画一个带环的链表自己推一遍这个等式理解为什么ptr和slow再次相遇时一定是在入口。3.2 寻找链表的中间节点与倒数第K个节点快慢指针的另一个常见用途是找链表的中间节点。让快指针每次走两步慢指针每次走一步当快指针走到链尾时慢指针刚好在中间。用这种方式可以避免先遍历一遍链表数长度、再遍历一遍找中间节点的“两趟扫描”一趟就搞定。找倒数第K个节点也类似先让快指针走K步然后快慢指针同步走当快指针到达末尾时慢指针刚好在倒数第K个节点。ListNode* getKthFromEnd(ListNode* head, int k) { ListNode *fast head; ListNode *slow head; // 快指针先走 k 步 for (int i 0; i k; i) { if (fast nullptr) return nullptr; // k 大于链表长度 fast fast-next; } // 快慢指针同步前进 while (fast ! nullptr) { fast fast-next; slow slow-next; } return slow; }这类题的共同点非常明显快指针本质上是“探路者”负责确定边界慢指针负责停在答案需要的位置。你不需要关心具体走了多少步只需要保证快慢指针之间的步差是固定的就能利用这个步差准确定位。这种思想在工程里也很有用比如设计某个“延迟执行N秒”或者“只保留最近N条消息”的逻辑时都可以借鉴。4. 滑动窗口处理子串与子数组问题的利器如果说左右指针解决的是“从数组中选元素”的问题那滑动窗口解决的就是“连续区间”的问题。它的应用场景非常广泛几乎所有“最长子串”“最短子数组”“满足某条件的连续区间”类问题都可以用滑动窗口来处理。滑动窗口的核心思路是维护一个左指针left和一个右指针right两者之间的区间就是所谓的“窗口”。右指针负责扩展窗口左指针负责收缩窗口。当窗口满足条件时记录结果并尝试收缩当窗口不满足条件时扩展窗口寻找新的可能。我拿一个最经典的题来演示给定一个字符串找出其中不含有重复字符的最长子串的长度。#include string #include unordered_map #include algorithm using namespace std; int lengthOfLongestSubstring(string s) { unordered_mapchar, int window; // 记录窗口内每个字符的最新位置 int left 0; int maxLen 0; for (int right 0; right s.size(); right) { char c s[right]; // 如果字符已经在窗口内更新左指针到重复位置的下一位 if (window.find(c) ! window.end() window[c] left) { left window[c] 1; } window[c] right; maxLen max(maxLen, right - left 1); } return maxLen; }这里的关键判断是window[c] left意思是如果这个重复字符出现在当前窗口的有效范围内才需要移动左指针。如果它虽然出现过但位置已经在left左边了那它对当前窗口没有影响不需要收缩窗口。4.1 滑动窗口的通用模板上面那道题很多人会认为只是特例但滑动窗口其实有一个比较通用的模板掌握了就能应付相当一部分子串和子数组问题初始化 left 0, right 0 初始化窗口状态比如哈希表、计数器 while (right 数组或字符串长度) { 将 s[right] 加入窗口更新窗口状态 while (窗口不满足题目要求) { 将 s[left] 移出窗口更新窗口状态 left } 记录或更新结果此时窗口是满足要求的 right }以“长度最小的子数组”为例给定一个数组和一个正整数target找出满足其和 ≥target的长度最小的连续子数组。#include vector #include climits using namespace std; int minSubArrayLen(int target, vectorint nums) { int left 0; int sum 0; int minLen INT_MAX; for (int right 0; right nums.size(); right) { sum nums[right]; // 当窗口和满足条件时尝试收缩窗口 while (sum target) { minLen min(minLen, right - left 1); sum - nums[left]; left; } } return minLen INT_MAX ? 0 : minLen; }这个模板的打法非常固定右指针不断扩展一旦窗口满足条件就进入内层while收缩左指针同时更新答案。等窗口不再满足条件了右指针继续往前走。这样每个元素最多被加入一次、删除一次整体时间复杂度 O(n)。4.2 滑动窗口的经典变体最小覆盖子串如果要选一道最能体现滑动窗口水平的题我个人会选“最小覆盖子串”。题目是给你一个字符串s和一个字符串t返回s中涵盖t所有字符的最小子串。如果不存在就返回空字符串。这道题的难点在于“涵盖”的定义——不是简单成串匹配而是s的子串里必须包含t中每个字符至少一次且不计顺序。处理方式是用两个哈希表一个存t中每个字符需要的次数另一个存当前窗口中每个字符出现的次数。用一个formed变量记录窗口中已经满足“不低于t中需求”的字符种类数。当formed等于t中不重复字符总数时说明窗口已经覆盖了t此时尝试收缩左指针找最短覆盖。#include string #include unordered_map #include climits using namespace std; string minWindow(string s, string t) { if (s.empty() || t.empty()) return ; unordered_mapchar, int need; for (char c : t) need[c]; unordered_mapchar, int window; int left 0; int formed 0; int required need.size(); int minLen INT_MAX; int minLeft 0; for (int right 0; right s.size(); right) { char c s[right]; if (need.find(c) ! need.end()) { window[c]; if (window[c] need[c]) { formed; } } while (formed required) { if (right - left 1 minLen) { minLen right - left 1; minLeft left; } char leftChar s[left]; if (need.find(leftChar) ! need.end()) { window[leftChar]--; if (window[leftChar] need[leftChar]) { formed--; } } left; } } return minLen INT_MAX ? : s.substr(minLeft, minLen); }这段代码我建议你反复看几遍有几个细节特别容易出问题一是formed的增减条件。它增加的前提是窗口中某个字符的出现次数刚好达到need中的要求减少的前提是移除左指针字符后该字符次数降到need之下。这里用的是“刚好达到”和“刚到之下”这两个临界点不是每一次增删都动formed否则计数会错乱。二是收缩窗口时要在while循环里不断更新最小结果而不是收缩完之后才更新一次。因为窗口收缩过程中可能中间某个时刻长度更短但继续收缩下去就又不满足条件了。三是leftChar不属于t时可以直接忽略。因为不相关的字符不影响窗口的覆盖状态直接移出即可。4.3 滑动窗口的时间复杂度分析很多初学者会担心外层for和内层while套在一起是不是又是 O(n²)其实不是。关键在于左指针left在整个过程中只会增加不会减少而且最多从 0 走到 n。也就是说内层while的执行总次数不会超过 n。外层for右指针也最多走 n 步。整体遍历次数是 O(2n)也就是 O(n)。这个“均摊分析”的思路在滑动窗口里非常核心。你可以把整个过程理解为右指针往右跑左指针被条件“拉”着往右追。每个元素最多被右指针扫进窗口一次再被左指针提出窗口一次总共两次访问复杂度自然就线性了。面试时如果你能把这一步讲清楚面试官对你的印象会好很多因为这说明你真的理解算法的时间复杂度是怎么来的而不只是背了个结论。5. 实操中的常见问题与避坑指南双指针的代码模板看似简单真正上手写就会发现全是细节。我自己在刷题和带人刷题时总结出了一些常见的坑在这里集中整理一下。5.1 边界条件到底什么时候用什么时候用这是双指针题里问得最多的问题。以对撞指针为例循环条件到底写left right还是left right完全取决于题意。如果是两数之和这类问题两个指针指向的元素不能是同一个那就必须用left right。如果题目允许一个元素用两次或者判断回文串时允许中间单个字符自己匹配那可以考虑left right但回文串那道题通常也是用就够了因为中间字符自己跟自己比本来就是相等的不影响结果。快慢指针里while (fast ! nullptr fast-next ! nullptr)是环形链表题的标准写法。这个条件能保证fast每次跳两步时不会跳到空指针上。如果你不小心只写了while (fast ! nullptr)那fast-next可能为空再访问fast-next-next就直接空指针异常了。这个问题在很多在线评判系统上跑的时候不一定会触发但一旦链表长度是奇数就很容易现场翻车。5.2 滑动窗口的去重与重复计数问题滑动窗口里有一类题型是“窗口内不能有重复字符”还有一类是“窗口内必须包含某些字符”。这两类问题的计数方式完全不同。不重复字符那类用的是字符位置记录重复了就直接把左指针跳到重复字符的下一位。而必须包含某些字符那类需要两个哈希表做精确计数并且要用一个变量跟踪“满足条件的字符种类数”。如果你把两种写法搞混代码很快会逻辑混乱。举个例子如果你在“最小覆盖子串”里用“字符位置”而不是“字符计数”来判断覆盖那结果一定是错的因为覆盖一个串需要完整统计出现次数而不是记录单个位置就能说明问题。反过来“最长无重复子串”里如果用字符计数那一套逻辑也会绕得很远因为你要的不只是“包含全部不重复”而是“每个字符最多出现一次”。5.3 指针移动的顺序问题对撞指针中先判断条件还是先移动指针顺序不能搞反。正确的逻辑通常是先判断当前两个指针指向的元素是否满足条件满足就直接返回或记录不满足再判断该移动哪一边。如果你先移动了指针再判断那你可能错过当前指针位置这一组解。在找完一组解、需要跳过重复元素时也要注意跳过逻辑的位置。三数之和就是一个典型的例子找到sum 0的组合后不能先left再跳过而是应该先跳过所有与当前元素相同的值再left/right--。写反了的话去重会去不干净。5.4 有序性是左右指针的前提左右指针能成立靠的是数据的单调性。如果你拿到的数组是无序的你别期望左右指针能跑出正确结果。三数之和必须先排序两数之和 II 题目已经给定了有序数组盛最多水的容器虽然数组无序但它靠的并不是严格单调而是“移动短边不会让面积变大”这一局部证明所以依然能跑。我见过太多人把左右指针套到无序数组上然后百思不得其解最后才发现忘了排序。先看题目的前提条件再决定用哪种双指针形态这是第一步。前提没搞清楚就上手写代码后面全是白忙活。5.5 快慢指针可能不止两个指针有些题目快慢指针不够用需要三个甚至更多。比如“链表的倒数第K个节点”本质上需要两个指针保持K步的间距这其实算两指针。而像“有序链表转二叉搜索树”这种题可能会用到快慢指针找中点然后额外用递归分割区间这时候变量会更多但核心思想还是一样通过指针对位置的精确把控避免额外的完整遍历。写这类需要多个指针的题目时我建议你把每个指针的用途写在注释里。一个指针负责探路一个指针负责定位一个指针负责记录前驱或边界。这样代码读起来很清晰调试时也知道该打印哪个变量比全靠变量名硬记要省心得多。6. 从刷题到实战双指针在真实代码里的影子很多人觉得双指针是面试专属实际工程里用不上。这话不太对。你去看很多开源库的源码尤其是字符串处理、数组归并、内存拷贝相关的部分双指针思想无处不在。6.1 数组原地去重与移除元素刷题时经常见到的“原地移除指定元素”和“数组去重”在真实代码仓库里就是一些底层工具函数在做的事情。思路是把一个指针当作“慢指针”用于指向最终保留的位置另一个指针当作“快指针”用于遍历原数组把符合条件的值往前搬。#include vector using namespace std; int removeElement(vectorint nums, int val) { int slow 0; for (int fast 0; fast nums.size(); fast) { if (nums[fast] ! val) { nums[slow] nums[fast]; slow; } } return slow; }这段代码里慢指针维护的是“新数组”的写入位置快指针负责探索原数组。虽然是同一个数组但逻辑上你可以把它理解为两个数组在并行操作只不过它们共用了同一块内存。这种就地操作能省掉一次新数组分配的开销在内存敏感的系统里很有价值。6.2 二分查找中的双指针影子二分查找其实也是标准的左右指针只不过每次移动都直接砍掉一半区间。它跟普通左右指针的区别在于二分查找每一步不是只移动一步而是把mid作为新边界一次跳掉一半。但代码结构上同样是left和right两个指针在往中间靠。我自己在写二分查找的时候会把边界更新写清楚if (nums[mid] target) { left mid 1; } else { right mid - 1; }这个1和-1的细节非常关键。写二分查找最怕的就是“死循环”大多数死循环都是因为边界更新写成了left mid或者right mid导致区间永远无法收缩。如果加上1/-1就能保证每次循环区间长度严格减小不会卡死在某个状态上。6.3 排序算法里的指针思想冒泡排序每轮把最大元素“冒”到末尾下一轮就不需要再管末尾那个位置了。如果你在实现时用right指针标记无序区的右边界每轮结束后让它左移一位其实也是在用指针缩小搜索范围。快速排序的partition更是典型的双指针操作一个指针从左往右找比基准大的元素另一个从右往左找比基准小的元素找到后交换位置。归并排序虽然用的是分治加合并但合并两个有序数组时依然需要两个指针分别在两个数组里移动比较。所以双指针不是某几道题专用的技巧而是一种通用的“减少重复遍历”的思维方式。你掌握了它再去学排序、二分、滑动窗口相关的算法会感觉很多地方都是相通的。7. 学习路径与实操建议如果你现在刚开始接触双指针我建议你按下面的顺序练手不要一上来就挑战最难的题两数之和 II输入有序数组——理解左右指针的基本框架移除元素 / 移动零——理解快慢指针的原地操作模式反转字符串 / 验证回文串——理解对撞指针的对称移动环形链表 / 环形链表 II——理解快慢指针的追击问题合并两个有序数组——体会双指针在归并场景中的应用长度最小的子数组——入门滑动窗口无重复字符的最长子串——滑动窗口加哈希表最小覆盖子串——滑动窗口的高阶版理解formed计数三数之和 / 盛最多水的容器——综合运用排序、对撞、去重每道题写完以后我强烈建议你手动走一遍示例把left、right每一步的变化写下来。不要觉得这个步骤浪费时间我见过太多人代码能跑但不知道自己写了什么一换用例就翻车。双指针的可视化模拟能力决定了你遇到变形题时能不能快速调整。调试时也有一个小技巧在关键循环里打印left、right和当前窗口或当前区间的状态观察指针移动是否符合预期。很多问题一眼看不出来但你把每一步的变量变化打印出来逻辑漏洞立刻就暴露了。等你面试的时候这种“自己会验证”的能力会给你加分不少。我个人练习时的经验是尽量用同一种语言把模板背熟然后在心里模拟它为什么能跑通。模板不是让死记硬背而是帮你建立一个稳定的分析框架。比如滑动窗口你只要记住right负责扩展、left负责收缩、窗口内每次变化后检查条件这三步再难的题也能往这个框架里套。框架对了剩下的就是细节问题。双指针这个主题说难不难但想掌握得扎实确实需要多写多调。希望这篇文章能把你的思路理得更清楚少走一些我当初走过的弯路。下次再遇到数组、链表、字符串的问题不妨先想想能不能用两个指针来减少遍历次数很多时候答案就在这个念头里。
返回列表