ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

最长公共前缀算法精解:横向与纵向扫描的实战剖析

最长公共前缀算法精解:横向与纵向扫描的实战剖析

1. 项目概述:从“最长公共前缀”看字符串处理的基石

在算法面试和日常编程中,字符串处理是绕不开的经典话题。今天要聊的这道题——LeetCode第14题“最长公共前缀”,可以说是字符串处理领域的“敲门砖”。乍一看,题目很简单:给你一个字符串数组,找出所有字符串共有的、最长的前缀。比如["flower","flow","flight"]的公共前缀就是"fl"。但正是这种看似简单的题目,最能考验一个程序员对基础数据结构的理解、对边界条件的把控,以及优化算法的思维。我见过不少朋友在面试时栽在这道“简单题”上,不是思路不对,而是细节没处理好,导致代码冗长或者效率不佳。

这道题的价值在于,它完美串联了数组遍历、字符串比较、循环控制以及时间复杂度分析等多个基础知识点。无论是刚接触LeetCode的新手,还是想巩固基础的老手,深入剖析这道题都能带来新的收获。网络上常见的解法往往只给代码,缺少对“为什么这么写”以及“还能怎么写”的深度拆解。接下来,我会结合自己刷题和面试官的经验,用两种最核心的解法,配上详细的图解和步骤拆解,带你彻底吃透这个问题。我们不仅追求AC(通过),更追求写出清晰、高效、健壮的代码。

2. 核心思路拆解:横向扫描与纵向扫描的哲学

面对“最长公共前缀”问题,我们的目标是在字符串集合中寻找一个最大的交集。这个交集必须从每个字符串的头部开始,并且连续。理解这一点后,最直接的思路通常有两种:要么一个个字符串比过去,像“接力赛”一样不断缩小公共前缀(横向扫描);要么像“同时检阅多列队伍”一样,从第一个字符开始,一列一列地比较所有字符串(纵向扫描)。这两种思路构成了本题最经典、最高效的解法。

2.1 思路一:横向扫描法——迭代缩小公共范围

横向扫描的思路非常符合人类的直觉:先假设第一个字符串就是最终的公共前缀,然后让它去和第二个字符串比较,找出它们俩的公共前缀,用这个结果再去和第三个字符串比较,如此迭代,直到遍历完所有字符串或公共前缀被缩减为空。

为什么选择这种思路?它的优势在于逻辑清晰,易于理解和实现。我们不需要同时处理所有字符串的同一位置,只需要关注当前累积的“公共前缀候选”和下一个字符串的关系。这个过程就像一个过滤器,不断筛掉不匹配的部分。

核心步骤与状态变化:假设数组为strs = ["flower","flow","flight"]

  1. 初始化前缀prefix = strs[0],即"flower"
  2. prefixstrs[1]("flow") 比较:
    • 找出两者共同的前缀。比较发现,“flower”和“flow”的共同前缀是“flow”。
    • 更新prefix = "flow"
  3. 将新的prefix("flow") 与strs[2]("flight") 比较:
    • 找出“flow”和“flight”的共同前缀。比较发现是“fl”。
    • 更新prefix = "fl"
  4. 遍历结束,最终prefix = "fl"即为答案。

这个过程中,公共前缀的范围从"flower"->"flow"->"fl"逐步缩小。如果中途prefix被缩减为空字符串,就可以立即返回,因为不可能再有更长的公共前缀了。

2.2 思路二:纵向扫描法——按列同步比较

纵向扫描采取了另一种视角:既然公共前缀要从头开始连续,那么我们可以从第一个字符开始,依次检查所有字符串的第一位、第二位、第三位……是否相同。

为什么选择这种思路?这种方法的空间复杂度直觉上可能更低(在某些实现中),并且当字符串很长但公共前缀很短时,可能提前结束比较,避免不必要的遍历。它更贴近“同时比较”的原始问题定义。

核心步骤与状态变化:同样以strs = ["flower","flow","flight"]为例。

  1. 以第一个字符串"flower"为基准,取其长度6作为最大可能列数。
  2. 比较第0列(第一个字符):
    • 检查strs[0][0](‘f’),strs[1][0](‘f’),strs[2][0](‘f’)。全部相同,继续。
  3. 比较第1列(第二个字符):
    • 检查strs[0][1](‘l’),strs[1][1](‘l’),strs[2][1](‘l’)。全部相同,继续。
  4. 比较第2列(第三个字符):
    • 检查strs[0][2](‘o’),strs[1][2](‘o’),strs[2][2](‘i’)。发现strs[2][2]是 ‘i’, 与 ‘o’ 不同。
    • 比较在此中断。公共前缀即为前2列字符组成的子串:"fl"

纵向扫描像是一把垂直的尺子,从左向右移动,一旦在某一个刻度上发现高度(字符)不一致,就停止测量。

注意:两种方法在最坏情况下的时间复杂度都是 O(S),其中 S 是所有字符串中字符的总数。但在实际表现上,如果公共前缀非常短,纵向扫描可能更快退出;如果数组第一个字符串非常短,横向扫描可能更快。通常两者差异不大,选择自己更容易理解和编码清晰的即可。

3. 解法一详解:横向扫描编码实现与图解

理解了横向扫描的思想后,我们来看如何将它转化为健壮的代码。这里的关键在于实现一个辅助函数,用于比较两个字符串,并返回它们的公共前缀。

3.1 代码实现与逐行解析

我们先给出Python版本的完整代码,然后逐一拆解。

class Solution: def longestCommonPrefix(self, strs: List[str]) -> str: # 边界情况处理:如果数组为空,则没有公共前缀 if not strs: return "" # 初始化前缀为第一个字符串 prefix = strs[0] # 遍历数组中的其他字符串 for i in range(1, len(strs)): # 关键:调用辅助函数,获取当前prefix与当前字符串的公共前缀 prefix = self._get_common_prefix(prefix, strs[i]) # 如果公共前缀在比较中变为空串,可以立即返回,无需继续比较 if not prefix: break return prefix def _get_common_prefix(self, str1: str, str2: str) -> str: """ 辅助函数:返回两个字符串的公共前缀。 思路:同时遍历两个字符串,直到遇到第一个不同的字符或任一字符串结束。 """ # 计算最小长度,避免索引越界 min_length = min(len(str1), len(str2)) index = 0 # 逐个字符比较 while index < min_length and str1[index] == str2[index]: index += 1 # 返回从0到index-1的子串(即相同的部分) return str1[:index]

逐行解析与设计理由:

  1. 边界检查 (if not strs:): 这是编写健壮代码的第一步。如果输入是空数组[],后续操作会出错。直接返回空字符串符合逻辑定义。
  2. 初始化前缀 (prefix = strs[0]): 以第一个字符串作为比较的起点。这里隐含了一个假设:公共前缀不可能比第一个字符串更长。如果数组只有一个字符串,那么它本身就是自己的公共前缀,循环不会执行,直接返回。
  3. 主循环 (for i in range(1, len(strs)):): 从第二个字符串开始遍历。每次循环的目标是用当前prefixstrs[i]碰撞,产生一个更短(或不变)的新prefix
  4. 调用辅助函数 (self._get_common_prefix): 这是核心逻辑的封装。将两个字符串的比较独立出来,使主函数逻辑更清晰。
  5. 提前终止 (if not prefix: break): 这是一个重要的优化。一旦公共前缀变为空字符串,说明已经不可能有公共前缀了,继续遍历后面的字符串没有意义,直接跳出循环。
  6. 辅助函数中的min_length: 比较时,公共前缀的长度不可能超过两个字符串中较短的那个。先计算出这个长度,作为循环的上限,既安全又高效。
  7. 辅助函数中的while循环: 条件index < min_length and str1[index] == str2[index]确保了在安全范围内(不越界)且字符相等时,才增加index。一旦条件不满足,循环停止。
  8. 切片返回 (return str1[:index]):index停在了第一个不相等字符的位置(或较短字符串的末尾)。因此,从开头到这个位置之前的子串就是公共前缀。Python的切片操作非常高效。

3.2 图解算法执行过程

让我们用strs = ["interspecies", "interstellar", "interstate"]这个例子来可视化横向扫描的过程。

初始状态:

prefix = "interspecies" 待比较列表: ["interstellar", "interstate"]

第一轮比较:prefixvs"interstellar"

  • 调用_get_common_prefix("interspecies", "interstellar")
  • 两字符串逐字符比较:
    • i==i-> 继续
    • n==n-> 继续
    • t==t-> 继续
    • e==e-> 继续
    • r==r-> 继续
    • s==s-> 继续
    • p!=t-> 停止
  • 此时index = 6,返回"interspecies"[:6]"inters"
  • 更新prefix = "inters"

第二轮比较:prefixvs"interstate"

  • 调用_get_common_prefix("inters", "interstate")
  • 两字符串逐字符比较:
    • i==i-> 继续
    • n==n-> 继续
    • t==t-> 继续
    • e==e-> 继续
    • r==r-> 继续
    • s==s-> 继续
    • "inters"已到末尾,循环停止。
  • 此时index = 6,返回"inters"[:6]"inters"
  • 更新prefix = "inters"

最终结果:"inters"

通过图解可以清晰看到,prefix像雪球一样滚动,每经过一个字符串,就被“削去”不匹配的部分,变得越来越精炼。

3.3 横向扫描的变体与优化

除了上述标准实现,横向扫描还有一些常见的变体写法,理解它们有助于加深对问题的认识。

变体1:使用find方法Python的字符串find方法可以查找子串,如果找不到则返回-1。我们可以利用它来不断调整前缀。

def longestCommonPrefix(self, strs): if not strs: return "" prefix = strs[0] for s in strs[1:]: # 当s不以prefix开头时,循环缩短prefix while s.find(prefix) != 0: # find返回0表示prefix在s的起始位置 prefix = prefix[:-1] # 去掉最后一个字符 if not prefix: # 如果prefix被删空了 return "" return prefix

优劣分析:代码非常简洁。但find方法内部也是进行字符串比较,且每次while循环都可能调用一次find,在极端情况下(如第一个字符串很长,且与后续字符串毫无共同前缀),时间复杂度可能退化到 O(n*m),其中n是字符串个数,m是第一个字符串长度。不如双指针逐字符比较稳定。

变体2:递归分治法将问题分解:数组的公共前缀 = 左半部分数组的公共前缀 与 右半部分数组的公共前缀 的公共前缀。

def longestCommonPrefix(self, strs): def common_prefix(left, right): # 类似_get_common_prefix函数 min_len = min(len(left), len(right)) for i in range(min_len): if left[i] != right[i]: return left[:i] return left[:min_len] def divide_and_conquer(strs, l, r): if l == r: # 只有一个字符串 return strs[l] else: mid = (l + r) // 2 lcp_left = divide_and_conquer(strs, l, mid) lcp_right = divide_and_conquer(strs, mid+1, r) return common_prefix(lcp_left, lcp_right) if not strs: return "" return divide_and_conquer(strs, 0, len(strs)-1)

优劣分析:这是一个非常漂亮的递归解法,体现了分治思想。时间复杂度也是 O(S)。但在实际运行中,由于递归调用栈的开销,对于本题通常不如迭代法高效。不过,这是一种重要的思维训练,在解决更复杂的问题时很有用。

实操心得:在面试或竞赛中,我推荐使用标准的双指针横向扫描法。它效率稳定,代码清晰,几乎不会出错。find变体虽然简洁,但可能引发关于时间复杂度的追问。分治法可以作为展示你算法深度的加分项,但不要作为首选。

4. 解法二详解:纵向扫描编码实现与图解

纵向扫描从另一个维度切入问题,代码结构同样清晰。它的核心是同时遍历所有字符串的同一列(索引位置)。

4.1 代码实现与逐行解析

class Solution: def longestCommonPrefix(self, strs: List[str]) -> str: # 边界情况处理 if not strs: return "" # 以第一个字符串为基准,遍历它的每一个字符(列) for i in range(len(strs[0])): # 获取当前要比较的基准字符 char_to_compare = strs[0][i] # 遍历数组中其他所有字符串 for j in range(1, len(strs)): # 关键条件判断: # 1. 当前字符串strs[j]的长度是否已经 <= i?如果是,说明这个字符串比基准字符串短,已经到头了。 # 2. 当前字符串strs[j]在第i位的字符是否不等于基准字符? # 只要满足以上任一条件,说明公共前缀到此为止。 if i == len(strs[j]) or strs[j][i] != char_to_compare: return strs[0][:i] # 返回从0到i-1的子串 # 如果循环完整执行完毕,说明第一个字符串本身就是整个数组的公共前缀 return strs[0]

逐行解析与设计理由:

  1. 外层循环 (for i in range(len(strs[0])):): 这个循环控制着我们比较的“列数”。我们以第一个字符串strs[0]的长度为最大可能列数进行遍历。i代表当前正在比较的字符索引。
  2. 获取基准字符 (char_to_compare = strs[0][i]): 在每一列,我们都以第一个字符串在该位置的字符作为比较的基准。
  3. 内层循环 (for j in range(1, len(strs)):): 对于每一列,我们需要检查数组中其他所有字符串(从strs[1]开始)在同一位置i的字符是否与基准字符一致。
  4. 核心条件判断 (if i == len(strs[j]) or strs[j][i] != char_to_compare:):
    • i == len(strs[j]): 这是长度边界检查,至关重要!如果当前遍历的字符串strs[j]的长度小于等于i,意味着这个字符串已经结束了(没有第i个字符)。既然公共前缀要求所有字符串都有的连续字符,那么有一个字符串已经到头了,公共前缀自然也就到此为止。如果不做这个检查,尝试访问strs[j][i]会导致索引越界错误。
    • strs[j][i] != char_to_compare: 这是字符相等性检查。如果当前字符串在第i位的字符与基准字符不同,公共前缀也在i处终止。
    • 这两个条件用or连接,只要有一个为真,就立即返回结果。
  5. 返回结果 (return strs[0][:i]): 当发现不匹配时,i指向了第一个不匹配的列(或某个字符串的末尾)。因此,公共前缀是strs[0]从开头到i-1的子串。注意切片[:i]是取前i个字符(索引0到i-1)。
  6. 循环完整结束后的返回 (return strs[0]): 如果外层for循环顺利执行完毕,没有在中间return,说明第一个字符串的每一个字符都成功通过了所有其他字符串的检验。这意味着第一个字符串本身就是整个数组的公共前缀。

4.2 图解算法执行过程

我们使用一个包含空字符串的案例来演示,这能更好地展示边界条件处理:strs = ["ab", "a", ""]。注意,第三个字符串是空串。

初始状态:

基准字符串: "ab" (长度2) 比较列索引 i = 0 开始

第一轮比较 (i=0):

  • 基准字符char_to_compare = strs[0][0] = 'a'
  • 内层循环j=1,比较strs[1][0]:
    • strs[1] = "a",len(strs[1])=1i=0小于长度1,继续。
    • strs[1][0] = 'a',等于'a',通过。
  • 内层循环j=2,比较strs[2][0]:
    • strs[2] = "",len(strs[2])=0
    • 判断条件:i == len(strs[2])0 == 0True
    • 条件触发,立即执行return strs[0][:0],即返回空字符串""

最终结果:""

这个例子清晰地展示了为什么必须要有i == len(strs[j])这个判断。如果没有它,代码在尝试执行strs[2][0]时会直接崩溃(索引越界)。有了这个判断,我们就能安全、正确地处理字符串长度不一,甚至存在空串的情况。

4.3 纵向扫描的边界与细节处理

纵向扫描法有几个细节需要特别注意,这些地方是代码正确性的保障,也是面试官喜欢考察的点。

1. 空输入数组和单元素数组:

  • if not strs: return ""处理了输入为[]的情况。
  • 如果输入是["abc"],外层循环会遍历"abc"的每个字符。内层循环for j in range(1, 1)不会执行(因为range(1,1)是空的)。因此循环直接结束,执行最后的return strs[0],正确返回"abc"

2. 第一个字符串是空串:

  • 如果strs = ["", "abc", "ab"],那么len(strs[0]) = 0
  • 外层循环for i in range(0)根本不会进入,直接跳过,执行最后的return strs[0],即返回空串""。这是正确的,因为空串与其他任何字符串的公共前缀只能是空串。

3. 使用zipset的优雅写法(Python特有):Python的zip(*strs)函数可以将多个列表(或字符串)的对应元素打包成元组。利用这个特性,可以写出非常简洁的纵向扫描代码。

def longestCommonPrefix(self, strs): if not strs: return "" # zip(*strs) 会产生类似 [('f','f','f'), ('l','l','l'), ('o','o','i'), ...] 的迭代器 for i, column in enumerate(zip(*strs)): # 使用set去重,如果set的长度大于1,说明这一列字符不完全相同 if len(set(column)) > 1: return strs[0][:i] # 如果所有列都相同,则最短的字符串就是公共前缀 return min(strs, key=len)

优劣分析:这段代码极其简洁,利用了Python的高级特性。zip(*strs)会自动以最短的字符串为准进行打包,完美处理了长度不一致的问题。set(column)用来快速判断一列字符是否全部相同。最后返回最短的字符串。这种写法的可读性对于熟悉Python的人来说很高,但可能掩盖了算法的一些底层细节(如边界处理),在向不熟悉Python的面试官解释时需要多费口舌。

注意事项:在手动实现纵向扫描时,务必把i == len(strs[j])的判断放在strs[j][i] != char之前。因为如果i已经等于字符串长度,意味着索引i是无效的,再尝试访问strs[j][i]就会出错。利用逻辑运算符or的短路特性(如果第一个条件为真,就不再判断第二个条件),我们可以安全地写出这个判断。

5. 复杂度分析与方法对比

在掌握了两种解法的实现后,我们需要从理论层面分析它们的效率,并指导在何种场景下如何选择。

5.1 时间复杂度分析

两种方法的时间复杂度在最坏情况下都是O(S),其中S是输入数组中所有字符串的字符总数。

  • 横向扫描:主循环遍历 n-1 个字符串。每次比较两个字符串,最坏情况下需要比较min(len(prefix), len(current_string))个字符。在最坏情况下(所有字符串都相同,且很长),第一次比较 m 个字符,第二次比较 m 个字符……第 n-1 次比较 m 个字符,总比较次数约为m + m + ... + m = (n-1)*m。由于S = n * m,所以时间复杂度为 O(S)。
  • 纵向扫描:外层循环最多执行 m 次(第一个字符串的长度)。内层循环每次执行 n-1 次。在最坏情况下(所有字符串都相同,且很长),总操作次数为m * (n-1),同样也是 O(S)。

结论:从渐进时间复杂度(大O表示法)上看,两种方法没有区别。

5.2 空间复杂度分析

两种方法的额外空间复杂度都是O(1),如果不考虑存储答案所需的空间(因为答案字符串是必须返回的,通常不计入额外空间复杂度)。

  • 横向扫描:只使用了常数个变量(prefix,i, 辅助函数中的index,min_length)。
  • 纵向扫描:只使用了常数个变量(i,j,char_to_compare)。

结论:空间效率上两者打平。

5.3 实际性能与场景考量

虽然理论复杂度相同,但在实际运行中,性能会受到数据特点的微妙影响。

特性横向扫描法纵向扫描法
代码逻辑直观,像“接力赛”直观,像“检阅列队”
提前终止当某次比较后prefix为空时,可立即终止。当在某一列发现不匹配或遇到最短字符串结尾时,可立即终止。
最坏情况所有字符串完全相同且很长。需要完整进行n-1次两两比较。所有字符串完全相同且很长。需要完整比较所有字符。
最佳情况第一个字符串与第二个字符串就完全不同。只需一次比较。所有字符串的第一个字符就不同。只需比较第一列。
对空串/短串敏感度不敏感。以第一个字符串为起点,后续比较会自动处理长度。敏感。需要显式检查i == len(strs[j])来处理短字符串。
适用场景当预计公共前缀较长,或者字符串长度差异较大时,表现稳定。当预计公共前缀很短,或者字符串数量很多但长度相近时,可能提前结束。

选择建议:

  1. 追求代码稳定与清晰:选择横向扫描。它的逻辑流非常直接,边界情况处理简单(主要依赖辅助函数),不容易出错。这是我个人在面试和实际编码中最常使用的方法。
  2. 处理超大规模数据且预计前缀极短:可以考虑纵向扫描。如果你知道数据中字符串几乎不可能有共同前缀,纵向扫描可能在第一列就返回,而横向扫描至少需要完成一次完整的字符串比较(prefixvsstrs[1])。
  3. 展示语言特性:在Python中,使用zipset的纵向扫描写法非常优雅,可以展示你对语言特性的掌握,但务必能解释清楚其原理和边界处理逻辑。

实操心得:在绝大多数LeetCode场景和面试中,两种方法都是完全可以接受的。面试官更关注的是你对算法的理解、代码的健壮性(边界处理!)和清晰的沟通。我通常会先说出两种思路,然后选择一种进行实现,并主动分析其时间/空间复杂度。如果时间允许,可以再提一下另一种思路作为对比,这能展现你思维的全面性。

6. 常见错误与排查技巧实录

即便思路清晰,在实现“最长公共前缀”时,依然有几个高频“坑点”。下面是我在自己刷题和看别人代码时总结的常见错误及解决方法。

6.1 错误一:索引越界(IndexError)

这是纵向扫描法中最容易犯的错误,尤其是在手动比较字符时忘记检查字符串长度。

错误代码示例:

# 错误的纵向扫描 def longestCommonPrefix(strs): if not strs: return "" for i in range(len(strs[0])): c = strs[0][i] for s in strs[1:]: # 如果s比strs[0]短,s[i]就会导致IndexError! if s[i] != c: return strs[0][:i] return strs[0]

问题:s的长度小于等于i时,s[i]是无效访问。修正:必须在比较字符之前,先判断索引i是否已经超出了当前字符串s的范围。即使用if i == len(s) or s[i] != c:

6.2 错误二:错误理解公共前缀的定义

公共前缀必须是所有字符串都有的、从开头连续的字符子串。

错误案例:

  • 输入:["car", "race", "arc"]
  • 错误理解:认为公共前缀是"r""c",因为它们都出现了。
  • 正确答案:""。因为第一个字符串以'c'开头,第二个以'r'开头,第三个以'a'开头,开头字符都不同,所以没有公共前缀。

排查技巧:牢牢记住“从开头连续”这个条件。你的算法必须从索引0开始比较,一旦中断,后面的部分即使相同也不再考虑。

6.3 错误三:对输入的特殊情况处理不足

只考虑了“正常”输入,没有考虑边界情况,导致程序崩溃或返回错误结果。

需要处理的特殊情况:

输入预期输出常见错误处理
[](空数组)""未做判断,访问strs[0]导致崩溃。
[""](仅一个空串)""可能进入循环导致错误,或正确处理。
["", "abc"]""在纵向扫描中,如果以第一个字符串""为基准,循环不会进入,应返回""
["a"](仅一个字符串)"a"应直接返回该字符串本身。
["abc", "ab", "a"](长度递减)"a"算法需要能正确处理长度不同的字符串。

健壮性检查清单:

  1. 函数开头,检查if not strs:,返回""
  2. 在横向扫描中,如果使用find变体,注意while循环的终止条件,防止死循环。
  3. 在纵向扫描中,内层循环务必先判断索引是否有效 (i == len(s)),再访问字符。
  4. 思考:如果输入数组非常大(例如10^4个字符串),你的算法是否会超时或超内存?O(S)的复杂度通常是可接受的。

6.4 错误四:使用内置函数的陷阱

以Python的os.path.commonprefix函数为例。这个函数确实是用来找公共前缀的,但它不是为这个问题设计的!

import os strs = ["flower", "flow", "flight"] print(os.path.commonprefix(strs)) # 输出:'fl' (这次对了) strs2 = ["interspecies", "interstellar", "interstate"] print(os.path.commonprefix(strs2)) # 输出:'inters' (这次也对了) strs3 = ["dog", "racecar", "car"] print(os.path.commonprefix(strs3)) # 输出:'' (这次也对了)

虽然在一些情况下它能得到正确结果,但强烈不建议在面试或解题中使用。原因有二:1. 这显得你只是在调用API,没有展示算法能力;2. 这个函数是用于文件路径前缀的,其行为在极端情况下可能不符合本题定义(尽管本题的测试用例可能碰巧通过)。面试官想考察的是你自己的实现逻辑。

6.5 调试与测试技巧

自己编写测试用例是验证代码正确性的最好方法。

一个简单的测试框架思路:

def test(): solution = Solution() test_cases = [ (["flower","flow","flight"], "fl"), (["dog","racecar","car"], ""), ([], ""), ([""], ""), (["a"], "a"), (["ab", "a"], "a"), (["abc", "abcde", "abcdef"], "abc"), (["", "abc", "ab"], ""), (["same", "same", "same"], "same"), ] for i, (input_strs, expected) in enumerate(test_cases): result = solution.longestCommonPrefix(input_strs) if result == expected: print(f"Test case {i+1} PASSED: {input_strs} -> {result}") else: print(f"Test case {i+1} FAILED: {input_strs} -> expected {expected}, got {result}") if __name__ == "__main__": test()

运行这个测试,可以快速验证你的代码是否覆盖了各种边界情况。养成自己写测试的习惯,能极大提高一次写出正确代码的概率。

最后,关于这道题,我个人最深的体会是:“简单题”不简单。它像一面镜子,能照出一个程序员对细节的掌控力。无论是横向扫描中辅助函数的设计,还是纵向扫描中那个先判断长度的or条件,都体现了严谨的思维。在平时练习时,不要满足于通过(Accept),要多问自己几个“为什么”:为什么这个循环要这么写?为什么这个判断要放在前面?还有没有更优或更清晰的写法?把这些想明白了,你的基础才算真正扎实。这道题掌握好了,再面对更复杂的字符串问题,比如KMP、字典树(Trie)等,你也会更有底气。

返回列表