双指针算法入门:移除元素、去重与合并有序数组的经典解法
2026/9/16 2:22:58 网站建设 项目流程

作为一个刷了三百多道算法题、也在面试中反复被双指针拷打的过来人,我可以很负责任地说:双指针算法是 LeetCode 上性价比最高的一类套路。它上手快、代码短、思路清晰,而且覆盖的场景极广——从数组操作到链表判断、从滑动窗口到有序序列合并,全都离不开它。

这篇文章想和你聊透双指针最经典的几个入门题目:移除元素、删除有序数组中的重复项、合并两个有序数组。这三个题看起来很基础,但它们恰好对应了双指针的三种不同玩法——快慢指针覆盖、快慢指针去重、逆向双指针合并。把这三道题吃透,你就掌握了双指针最核心的骨架,之后再遇到变体题,比如移动零、有序数组的平方、甚至三数之和,都会有熟悉的感觉。

这篇文章适合谁看?刚开始刷题、对数组原地操作比较懵的新手,或者刷过一些题但遇到双指针总是边界条件写错的同学,都值得认真过一遍。我不只给你代码,还会把每一步的思考过程、为什么这么写、有哪些坑都讲清楚。毕竟面试官真正想看的,不是你背下了答案,而是你能不能讲明白每一步的逻辑。

1. 双指针算法的核心设计思路:先搞懂指针到底在“指”什么

很多人一开始学双指针,容易被“指针”这两个字吓住,觉得这是什么高深的链表操作。其实在数组题里,指针就是一个数组下标而已。双指针,说白了就是用两个下标变量去扫描同一个数组,通过控制这两个下标的移动节奏,把原本需要两层循环才能解决的问题,优化成单层循环就能搞定。这背后隐藏的思路是:让一个循环干两件事

1.1 双指针的三种基本形态

我在实际刷题中,习惯把双指针分成三种形态,理解了这个分类,后面做题会轻松很多。

**第一种是快慢指针。**快指针在前面“探路”,慢指针在后面“写结果”。最典型的场景就是“原地去重”和“移除指定值”。快指针逐格移动,检查每个元素是否符合保留条件;慢指针指向的位置,就是下一个要写入的位置。这种模式通常用来处理“原地修改数组”类的题目,空间复杂度能做到 O(1)。

**第二种是左右对撞指针。**一个指针放在数组头部,另一个放在尾部,根据条件判断让左边指针往右移或者右边指针往左移,两个指针相向而行,直到相遇。这种模式常见于“有序数组里找两个数满足某个条件”的题,比如两数之和、三数之和、回文判断、反转数组等。

**第三种是逆向双指针。**从数组的末尾开始往前移动,通常用于处理合并类问题,因为从后往前写可以避免覆盖掉还没被处理的元素。合并两个有序数组就是这种模式的经典代表。两种指针一个指向第一个数组的有效内容尾部,一个指向第二个数组的尾部,然后从整个数组的末尾开始填充结果。

这三种形态不是互相排斥的,有些题甚至会用两种以上的指针思路组合。但作为入门,先把这三种模式各自吃透,再谈组合。

1.2 怎么判断一道题能不能用双指针

有一个非常朴素的判断标准:如果一道题要求你“原地”修改数组,并且最终的答案顺序和原数组的相对顺序有关,那么大概率可以用快慢指针。如果题目给出的是有序数组,并且要求查找满足某种条件的一对元素,那左右对撞指针往往是首选。如果题目需要把两个有序数组合并,而且允许你在较大数组上操作,那逆向双指针就非常合适。

这些都是我在做题中总结的“手感”。说实话,算法这东西,刷题前期需要的是“背套路”,刷多了以后就会形成条件反射。就好比你学做饭,最开始照着菜谱一步一步来,做多了以后看到食材就知道该切丝还是切块。双指针也是一样,前期把经典题目吃透,后面关键是识别题目结构,而不是重新发明解法。

2. 移除元素(LeetCode 27):快慢指针最经典的入门场景

先来看第一道题,题目要求是这样的:给你一个数组nums和一个值val,你需要原地移除所有数值等于val的元素,并返回移除后数组的新长度。不需要考虑数组中超出新长度后面的元素,也不允许使用额外的数组空间,只能使用 O(1) 的额外空间。

注意审题:它说的“移除”其实不是真的删除内存中的元素,而是让你把要保留的元素覆盖到数组前面,然后返回一个新的长度k,表示前k个位置是有效数据。数组后面是什么内容并不重要。

2.1 为什么暴力解法不行

看到这道题,新手的第一反应往往是:遍历数组,遇到等于val的元素就把它删掉——但数组删除元素要“搬移”后续元素,这本身就涉及循环;更麻烦的是,删除后索引会变化,需要维护一个偏移量。如果你用erase之类的操作,在 C++ 里时间复杂度会退化;在 Python 里虽然可以remove循环,但效率也很差。

暴力解法的典型写法是:遍历数组,碰到目标值就把它后面的所有元素整体前移一位,同时让总长度减一。这个操作的时间复杂度是 O(n^2),空间复杂度虽然是 O(1),但对于长度较大的测试用例会超时。更深层的麻烦在于:当数组里有大量等于 val 的连续元素时,每次删除都要移动后续全部元素,而这个“移动”本身大部分是冗余的——因为目标值后面的元素可能马上又会被删除。

我自己刚刷题时写过这种“看似没错,一跑就超时”的代码。后来才意识到,数组原地操作的核心思想不是“删”,而是“覆盖”。你不需要真的把元素从数组里弄出去,只要让不需要的元素被需要元素覆盖掉,再声明数组的有效长度变了就行。

2.2 快慢指针实现:用“覆盖”代替“删除”

快慢指针的思路非常直观:定义两个变量slowfast,初始都指向 0。fast负责遍历整个数组,判断当前位置的元素是否等于valslow负责记录“下一个应该写入的位置”。当fast指向的元素不等于val时,就把nums[fast]赋给nums[slow],然后slowfast都加一。当fast指向的元素等于val时,slow不动,只有fast加一,相当于跳过了这个元素。

我用 C++ 写是这样:

class Solution { public: int removeElement(vector<int>& 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; } };

这段代码看起来短,但每一步都对应着一个关键决策:

为什么slow要指向“下一个写入位置”而不是“当前元素位置”?因为在遍历时,slow前面的区域都是已经处理好的“有效区”,slow本身是有效区末尾的下一个空位。这样做的好处是,每次覆盖都发生在有效区的末尾,不会破坏已经处理好的部分。

为什么当fast指向val时,只需要fast++因为等于val的元素不需要保留,它该占的那个“有效位置”不应该给它。slow停在原地,就是为了等下一个不等于val的元素来填这个洞。

走一遍示例nums = [3,2,2,3], val = 3

  • 初始:slow = 0, fast = 0nums[0] = 3,等于val,跳过,fast = 1
  • fast = 1nums[1] = 2,不等于valnums[0] = 2slow = 1, fast = 2
  • fast = 2nums[2] = 2,不等于valnums[1] = 2slow = 2, fast = 3
  • fast = 3nums[3] = 3,等于val,跳过,fast = 4
  • 循环结束,返回slow = 2

最终前两位是[2, 2],数组后面原本是什么还留着,不影响结果。这个“覆盖”的思想贯穿了所有数组原地操作类题目,务必记住。

2.3 这道题的边界条件与易错点

我踩过的坑主要有三个:

**第一个坑:忽视了空数组的情况。**如果nums长度为 0,上面的循环压根不会执行,直接返回slow = 0就对了。这个在逻辑上是天然正确的,所以你只要别一开始就手动处理void的情况写错分支就行。有些同学喜欢先特判if (nums.size() == 0) return 0;,虽然不影响结果,但其实是多余的,双指针写法天然兼容边界。

**第二个坑:slow最后是否要加一。**注意slow维护的是“下一个写入位置”,所以当处理完所有元素后,slow正好等于有效元素的个数,也就是应该返回的长度。举个例子你就明白了:有效区里有 3 个元素,写入完第三个时slow从 0 加到了 3,所以slow就是长度,不需要再额外加一。

**第三个坑:面试时回答“为什么这样可以保证前 k 个元素都是有效元素”。**因为fast每发现一个不等于val的元素,就把它放到slow指向的位置,然后slow前进一格。所以从位置 0 到slow-1每一个格子都被有效元素填满,不可能出现空位。这个点我在面试时被连续追问过,后来总结了一个更好的说法:快指针负责选择,慢指针负责落位。

3. 删除有序数组中的重复项(LeetCode 26):把“去重”变成“原地覆盖”

第二题是删除有序数组中的重复项。你看题名就知道,它和移除元素是“亲兄弟”。题目要求是:给你一个非严格递增排列的数组nums,请你原地删除重复出现的元素,使每个元素只出现一次,返回删除后数组的新长度。元素的相对顺序应该保持一致。

这道题和上一题唯一的区别是:上一题我们要“移除指定值”,这道题我们要“移除所有重复值”。但它们本质上做的是同一件事——用快慢指针遍历,把符合条件的元素一个接一个写在数组前面。

3.1 为什么“有序”是解题的大前提

强调一下题目里“非严格递增”这个条件。如果你拿到的是一个无序数组,要求去重且保持相对顺序,直接套双指针是不行的,你得先排序或者用哈希表。而这道题恰好给的是有序数组,所以相邻元素如果有重复,它们一定是紧挨着的。这一个特性,让双指针解法变得极其简洁。

我在做题时经常提醒自己:很多题目不是靠算法硬解,而是靠题目条件“送分”的。有序数组的重复元素相邻,就意味着我可以只比较相邻元素,而不是把每个元素和前面所有元素都比较一遍。这相当于是双指针能用的根本原因。如果题目改成无序数组,那复杂度就不是 O(n) 而是 O(n^2) 甚至更差了。

3.2 双指针去重的标准写法

定义slow = 1fast = 1。为什么是从 1 开始而不是从 0?因为第一个元素无论如何都会被保留,不需要比较它和它自己。slow在这里依然表示“下一个需要写入的位置”,而fast从第二个元素开始往后扫描。当nums[fast] != nums[slow - 1]时,说明遇到了新元素,就把它写到nums[slow],然后slow++。如果不相等则不满足,就让fast继续往前探索。

C++ 代码:

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

这里有一个非常精妙的点:比较对象不是nums[fast]nums[fast - 1],而是nums[fast]nums[slow - 1]。为什么?

举个具体例子:nums = [1, 1, 1, 2, 2, 3]。如果比较的是nums[fast]nums[fast-1],那当fast = 3时,nums[3] = 2nums[2] = 1,虽然发现了新元素,但这个 2 应该写到哪个位置?你不知道前面有多少个重复的 1。而和nums[slow - 1]比较就完全不一样了,slow - 1指向的是“已经处理好的有效区最后一位”,nums[slow-1]就是有效区的当前最后一个值。只要nums[fast]和它不一样,就说明fast遇到了一个全新的值,把它放到slow位置准没错。

拿上面的例子走一遍:

  • slow = 1, fast = 1nums[1] = 1nums[0] = 1,相等,跳过,fast = 2
  • fast = 2nums[2] = 1nums[0] = 1,相等,跳过,fast = 3
  • fast = 3nums[3] = 2nums[0] = 1,不相等,nums[1] = 2slow = 2, fast = 4
  • fast = 4nums[4] = 2nums[1] = 2,相等,跳过,fast = 5
  • fast = 5nums[5] = 3nums[1] = 2,不相等,nums[2] = 3slow = 3
  • 返回slow = 3,前三位为[1, 2, 3]

站在面试官的角度,他其实特别希望听到你解释清楚为什么要和nums[slow-1]比较,而不是nums[fast-1]。这个问题一答出来,说明你是真的理解了慢指针的含义,而不是背的模板。

3.3 推广:如果允许每个元素最多出现两次怎么办

这是 LeetCode 80 题的变体,也是面试中喜欢追问的方向。思路很简单:把比较对象从nums[slow - 1]改成nums[slow - 2]。为什么?因为slow - 1只能保证有序数组里相邻元素不重复,无法保证某个值最多出现两次;而nums[slow-2]表示有效区倒数第二个元素的位置。当新元素和它不相等时,说明这个元素在有效区里出现的次数还没有达到 2 次,可以写入。

这背后的通用逻辑是:如果你允许每个元素出现 k 次,就把比较对象改成nums[slow - k]这个规律我从刷题中总结出来后,后面遇到“最多出现 k 次”的变体题,直接套模板就行。

4. 合并两个有序数组(LeetCode 88):逆向双指针的艺术

第三道题是合并两个有序数组。题目的描述是:给你两个按非递减顺序排列的整数数组nums1nums2,另有两个整数mn,分别表示nums1nums2中的元素数目。请你合并nums2nums1中,使合并后的数组同样按非递减顺序排列。注意:nums1的初始长度为m + n,其中前m个元素代表应合并的元素,后n个元素为 0,应忽略。

这道题和前两题的区别在于:前两题是“一个数组内原地修改”,这一题是“把第二个数组合并进第一个数组”,而且要求结果仍然保存在nums1里,不能额外开数组。

4.1 正向合并最大的坑:会覆盖还没处理的元素

很多新手拿到这道题,第一反应是“这不就是归并排序的合并过程吗”,于是新建一个临时数组,把nums1的前m个元素和nums2n个元素按从小到大合并到临时数组里,最后再拷回nums1。这个思路完全正确,逻辑也没毛病,时间复杂度 O(m+n),空间复杂度 O(m+n)。但题目要求额外空间必须为 O(1),所以这种做法过不了。

那能不能直接在nums1上从前往后合并?举个反例:nums1 = [1, 2, 3, 0, 0, 0]m = 3nums2 = [2, 5, 6]n = 3。如果你从前往后比较,nums1[0] = 1nums2[0] = 2小,于是你把1保留在原地;接着nums1[1] = 2nums2[0] = 2比较,也保留原值;到了nums1[2] = 3,发现 3 大于 2,需要把 2 插入到 3 前面,这时候你就得把 3 往右移动一位——但nums1[3]的位置原本是 0,是预留的空位,移动之后确实没问题。然而如果nums1[3]不是空位而是另一个待合并的有效元素呢?在更复杂的情况下,正向移动会覆盖掉还没参与比较的有效元素。

核心教训是:当两个数组合并到第一个数组中、且第一个数组后面有预留空间时,从前往后合并会有覆盖风险,从后往前合并才是最安全的。

4.2 逆向双指针:从后往前填空

既然正向有覆盖问题,我们反过来想:nums1末尾有 n 个空位,这 n 个空位正是为合并结果预留的。如果从后往前填,比较nums1nums2中当前最大的有效元素,谁大就把谁放到nums1的末尾,这样永远不会覆盖还没处理的元素,因为末尾的位置总是“空”的或者已经填好的。

需要三个指针:

  • p1 = m - 1:指向nums1有效部分的最后一个元素
  • p2 = n - 1:指向nums2的最后一个元素
  • p = m + n - 1:指向nums1数组的最后一个位置,即待填充位置

每一步操作:比较nums1[p1]nums2[p2],把大的那个放到nums1[p],同时对应的指针往前移动一格,p也往前移动一格。

C++ 代码:

class Solution { public: void merge(vector<int>& nums1, int m, vector<int>& nums2, int n) { int p1 = m - 1; int p2 = n - 1; int p = m + n - 1; while (p2 >= 0) { if (p1 >= 0 && nums1[p1] > nums2[p2]) { nums1[p] = nums1[p1]; p1--; } else { nums1[p] = nums2[p2]; p2--; } p--; } } };

注意我这里的循环条件是p2 >= 0而不是p >= 0。这里有一个很妙的逻辑:如果p2 < 0,说明nums2已经全部合并完毕,而nums1剩余的元素本来就在正确位置上,不需要再动。反过来,如果p1 < 0p2 >= 0,那就说明nums1的有效部分已经用完了,剩下的位置全部由nums2来填,而循环会继续执行else分支,把nums2剩余元素依次放到前面的位置。

很多初学时看不懂为什么循环里还带p1 >= 0这个判断。我当时也卡了很久。其实它是为了防止p1变成 -1 时仍然去访问nums1[-1]导致越界。一旦p1 < 0,说明nums1原始有效元素已经全部被比较过且放入更靠后的位置了,此时剩下的空位理所当然应该全部填入nums2中剩余的元素。

4.3 边界条件的精细化处理

这道题最容易出错的地方在于:当p1p2其中一个先耗尽时,循环如何正确收尾。

nums1 = [1, 2, 3, 0, 0, 0]nums2 = [2, 5, 6]来走一遍:

  • p1 = 2, p2 = 2, p = 5nums1[2] = 3nums2[2] = 6,6 更大,nums1[5] = 6p2 = 1, p = 4
  • p1 = 2, p2 = 1, p = 4nums1[2] = 3nums2[1] = 5,5 更大,nums1[4] = 5p2 = 0, p = 3
  • p1 = 2, p2 = 0, p = 3nums1[2] = 3nums2[0] = 2,3 更大,nums1[3] = 3p1 = 1, p = 2
  • p1 = 1, p2 = 0, p = 2nums1[1] = 2nums2[0] = 2,这里条件是>,相等时走 else,nums1[2] = 2p2 = -1, p = 1
  • 循环结束,nums1[1, 2, 2, 3, 5, 6]

再考虑一个特殊情况:nums1 = [0]m = 0nums2 = [1]n = 1。这时p1 = -1, p2 = 0, p = 0。循环条件p2 >= 0满足,进入循环后p1 >= 0为 false,所以走 else 分支,nums1[0] = 1。完美。

另一个特殊情况:nums1 = [2, 0]m = 1nums2 = [1]n = 1p1 = 0, p2 = 0, p = 1nums1[0] = 2大于nums2[0] = 1nums1[1] = 2p1 = -1, p = 0。进入下一轮:p1 >= 0为 false,走 else,nums1[0] = 1。结果[1, 2],正确。

可以看到,这个else分支天然承担了“照看剩余元素”的任务,设计得非常优雅。从后往前的思路不仅省空间,还让边界处理变得异常干净。

4.4 为什么很多教材推荐先写循环条件while (p2 >= 0)

我见过不少人把循环写成while (p1 >= 0 || p2 >= 0),然后里面还要分四种情况讨论,代码长且容易出错。其实不需要。因为nums1的前 m 个元素始终保留在nums1中,并且一旦自己的位置确定下来以后,它不需要再移动。

所以正确的思路是:只关注nums2有没有合并完。如果nums2合并完了,整个合并就完成了。这句看似简单的话,是这道题真正考察的思维深度。理解了这个,循环条件和内部判断就都顺理成章了。

5. 三题串讲:从一道题到一类题的双指针套路总结

这三个题目放一起学,价值远大于单独刷三五遍。因为它们的解题思路完全是一脉相承的:都是通过两个指针协同移动,在一个循环里完成“扫描”和“写入”两个动作,从而避免不必要的元素移动。

5.1 三种指针移动模式对比

很多时候面试官会追问:这三个题有什么区别和联系?用表格整理一下会非常清晰:

题目指针类型起始位置写入位置比较对象核心思想
移除元素快慢指针slow=0, fast=0slownums[fast] 与 val跳过所有不等于 val 的元素
删除有序数组中的重复项快慢指针slow=1, fast=1slownums[fast] 与 nums[slow-1]遇到新值就写入
合并两个有序数组逆向双指针p1=m-1, p2=n-1m+n-1 从后往前nums1[p1] 与 nums2[p2]谁大谁放末尾,避免覆盖

注意前两个题的slow起始位置不同,是因为第一个元素(或前 k 个元素)要保留,所以从 1 开始;移除元素因为每个位置都可能被移除,所以从 0 开始。

如果把这三个题的代码放在一起对比,你会发现都遵循一个通用模板:初始化指针 -> 循环扫描 -> 根据条件写入 -> 移动指针 -> 返回或处理结果。所谓算法能力,其实就是在这些模板的基础上,根据题目条件做微调。

5.2 面试答题的推荐节奏与代码规范

面试时遇到这三类题,我建议按下面的节奏来:

首先,审题时要明确一个关键问题:允不允许使用额外空间?有的题目明确要求 O(1) 空间,那就是没有商量余地,必须用原地覆盖;但有的版本没有限制额外空间,那你可以先提一个简单方案,再优化到最优解。我面试时习惯先把这两个方案都说出来,让面试官看到我考虑了不同的取舍。

其次,动手前先口头描述一遍思路,比如:”我准备用快慢指针,fast 负责遍历,slow 记录下一个写入位置,当 fast 指向的元素满足条件时写入并移动 slow,最后返回 slow。“ 这句话一说完,面试官基本就知道你懂了。很多时候,刷题没过,不是代码写不对,而是不会表达。

最后,写代码时要注意变量命名。不要用ij这种没有含义的命名,建议用slowfastp1p2p,或者注释标明每个指针的含义。在面试高压环境下,清晰的命名能帮你避免很多低级错误。

5.3 我踩过的一些坑和调试技巧

说几个我在刷题中真正踩过的坑,你们写代码时留意一下。

坑一:用for循环的时候,在循环体里误改动了fast移除元素这么简单的题,我也见过有人写成for (int fast = 0; fast < nums.size(); fast++),然后在等于 val 的时候执行fast++,导致一次循环跳了两个位置。正确做法是,等于 val 时不操作,fast交给循环自己递增就行。

坑二:数组为空时直接访问nums[slow-1]在去重那道题里,如果你没加if (nums.empty()) return 0;的特判,当数组为空时,slow - 1 = -1,访问就会越界。虽然 LeetCode 的测试用例大概率不会给空数组,但面试官可能会故意问,所以特判一定不要忘。

坑三:合并两个有序数组时忘记处理nums2剩余元素。我在白板上写代码时,经常出现一种情况:nums2的元素比nums1的某些元素小,所以它一直没被填入,循环结束时p2却还没小于 0。如果循环条件是while (p1 >= 0),那nums2中较小的元素就无缘无故丢了。我后来养成了一个习惯:合并题写完第一个 while 后,一定要顺手加一个处理剩余元素的 while。

调试技巧方面,我强烈建议你在刷题时养成“画表格”的习惯。拿一张纸,画三行,分别表示slowfast、数组内容,每一步都更新一次。虽然看起来慢,但对于理清指针的移动逻辑非常有效。尤其是边界条件复杂的时候,干瞪眼是瞪不出来的,动手画一遍就通了。

另外一个小建议:这三道题在 LeetCode 上都有非常多的测试用例,提交代码后如果报错,别急着看答案,先用错误的那个用例手动走一遍你的代码,找到是哪一步覆盖错了。这个排查过程本身,就是算法能力提升最快的方式。我刷这三道题时曾经各提交过四五次才过,每错一次,对指针的理解就加深一层。

5.4 后续可以继续挑战的变体题

把这三道题吃透以后,你会惊喜地发现,双指针的变形题非常多,而且很多都是面试高频题:

  • 移动零:把数组里所有 0 移到末尾,其实就是在“移除元素”的基础上,把“不等于 val 的元素”改成“不等于 0 的元素”,而这道题的写法甚至可以直接在移除元素的思路上加一步:末尾补 0。
  • 有序数组的平方:给你一个有序数组,返回每个数字的平方组成的新数组,要求也按非递减排序。因为有负数存在,平方后最大的数一定在两端,所以用左右对撞指针从两端向中间填。
  • 三数之和:固定一个数,再用左右对撞指针在剩余区间寻找两个数。这里用到的就是对撞指针思想。
  • 盛最多水的容器:左右对撞指针,每次移动较矮的那一侧,时间复杂度 O(n)。
  • 长度最小的子数组:快慢指针维护一个滑动窗口,寻找满足和大于等于 target 的最短子数组。

你会发现,这些题的核心,很多都是这三道题的思路延伸。我经常跟身边的朋友说:基础题不刷透,难题很难真正理解。双指针这个专题,最有价值的入门组合恰恰就是这三道题,别看它们难度低,其中的思维含量一点都不低——快慢指针的“覆盖”思想、逆向指针的“避覆盖”思想,理解了这两点,以后数组类题目的路就宽了很多。

提示:刷题时别只满足于“通过了”,试着问自己三个问题:为什么 slow 从 0 开始或从 1 开始?为什么比较对象是 slow-1 而不是 fast-1?为什么从后往前合并?这三个问题答清楚了,才算真正吃透了双指针。

我个人在实际操作中的体会是,双指针算法的代码往往不超过十五行,但每一行的位置、每一个比较的先后顺序都极其讲究。刷这三道题时,如果你能写到不看题解、独立给出正确的边界处理,那面试考到同类问题基本就稳了。它们就像是算法世界里的“基本功”,练好之后,后面的路会顺很多。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询