0Pricing
Coding Interview Prep · 课时

字符串编码、反转与回文

实现单词原地反转、游程编码和回文检测,包括向中心扩展的技巧。

字符串编码、反转与回文 是 CoddyKit 上的免费 Coding Interview Prep 课时。 这是第 4 节课,共 4 节。 你可以在下方免费阅读本课时的完整内容 — 然后在浏览器中使用内置代码编辑器和全天候 AI 导师进行实践。 这是 Coding Interview Prep 学习路径的一部分,你的进度在网页和 CoddyKit 应用中同步。 Coding Interview Prep 课程共包含 4 节课。

原地反转字符串

Python 字符串不可变,因此“原地”反转意味着先将其转换为字符列表,再使用双指针交换字符,最后使用 join 拼接。经典的双指针交换方法是:将 left 放在索引 0,将 right 放在最后一个索引;交换字符并让两个指针向内移动,直到它们相遇或交叉。字符列表需要 O(n) 时间和 O(n) 空间(由于字符串不可变,这是无法避免的)。

def reverse_string(s):
    chars = list(s)
    left, right = 0, len(chars) - 1
    while left < right:
        chars[left], chars[right] = chars[right], chars[left]
        left  += 1
        right -= 1
    return ''.join(chars)

print(reverse_string('hello'))   # 'olleh'
print(reverse_string('Hannah'))  # 'hannaH'

# Pythonic shortcut (creates new string):
print('hello'[::-1])  # 'olleh'

反转句子中的单词

请反转单词的顺序,同时去除多余空格。简洁的 Python 解法是:使用 split(可处理多个空格),反转列表,再使用 join。对字符数组进行原地反转时:先使用 reverse 反转整个数组,再反转每个单独的单词。这种双遍方法的时间复杂度为 O(n),空间复杂度为 O(n)(由于 Python 字符串不可变,这是无法避免的)。

def reverse_words(s):
    words = s.split()       # split and strip whitespace
    words.reverse()         # in-place reverse
    return ' '.join(words)  # single space between words

print(reverse_words('  hello   world  '))  # 'world hello'
print(reverse_words('a good example'))     # 'example good a'

# One-liner:
print(' '.join('  hello   world  '.split()[::-1]))

回文检测:简单方法

如果一个字符串等于其 reverse,它就是回文。最快的 Python 检查方式是:s == s[::-1]。对于不区分大小写且仅包含字母数字字符的回文(面试中最常见的变体),请先规范化字符串:过滤非字母数字字符并转换为小写,然后进行比较。这两种方法的时间复杂度都是 O(n)。

def is_palindrome(s):
    # Filter and normalise
    cleaned = ''.join(c.lower() for c in s if c.isalnum())
    return cleaned == cleaned[::-1]

print(is_palindrome('A man, a plan, a canal: Panama'))  # True
print(is_palindrome('race a car'))                       # False
print(is_palindrome('Was it a car or a cat I saw?'))     # True

回文检测:双指针

如果希望将额外空间降至 O(1),请使用双指针而不是切片来检查回文。将 left 放在 0,将 right 放在末尾。跳过非字母数字字符,以不区分大小写的方式比较其余字符,并在不匹配时返回假值。这种方法虽然更加冗长,但完全避免了创建清理后的字符串——当内存受限时,这一点非常重要。

def is_palindrome_twoptr(s):
    left, right = 0, len(s) - 1
    while left < right:
        while left < right and not s[left].isalnum():
            left += 1
        while left < right and not s[right].isalnum():
            right -= 1
        if s[left].lower() != s[right].lower():
            return False
        left += 1; right -= 1
    return True

print(is_palindrome_twoptr('A man, a plan, a canal: Panama'))  # True

围绕中心扩展查找最长回文

围绕中心扩展技术可以在 O(n²) 时间和 O(1) 额外空间内找到最长回文子串。对于每个字符(奇数长度回文)以及字符之间的每个间隙(偶数长度回文),在字符匹配时向外 expand。请记录遇到的最佳(start,end)区间。共有 2n-1 个中心,每次扩展在最坏情况下需要 O(n) 时间。

def longest_palindrome(s):
    best_start = best_end = 0

    def expand(left, right):
        while left >= 0 and right < len(s) and s[left] == s[right]:
            left -= 1; right += 1
        return left + 1, right - 1  # last valid bounds

    for i in range(len(s)):
        l, r = expand(i, i)      # odd-length
        if r - l > best_end - best_start:
            best_start, best_end = l, r
        l, r = expand(i, i + 1)  # even-length
        if r - l > best_end - best_start:
            best_start, best_end = l, r

    return s[best_start:best_end+1]

print(longest_palindrome('babad'))    # 'bab' or 'aba'
print(longest_palindrome('cbbd'))     # 'bb'

Manacher 算法预览

Manacher 算法利用这样的规律,在一个更大的回文内部的回文可以根据其镜像位置进行初始化,从而在 O(n) 时间内找到最长回文子串。面试中很少要求实现它,但了解它的存在仍然很有价值。大多数面试官都会接受 O(n²) 的围绕中心扩展方法,认为它“已经足够优化”——如果面试官要求进一步改进,请提及 Manacher 算法这一理论上的 O(n) 解法。

# Manacher's: O(n) longest palindromic substring
def manacher(s):
    # Transform s into '#a#b#a#' to handle even/odd uniformly
    t = '#' + '#'.join(s) + '#'
    n = len(t)
    P = [0] * n  # P[i] = palindrome radius at i
    center = right = 0
    for i in range(n):
        mirror = 2 * center - i
        if i < right:
            P[i] = min(right - i, P[mirror])
        while (i + P[i] + 1 < n and i - P[i] - 1 >= 0
               and t[i+P[i]+1] == t[i-P[i]-1]):
            P[i] += 1
        if i + P[i] > right:
            center, right = i, i + P[i]
    max_len = max(P)
    center_idx = P.index(max_len)
    start = (center_idx - max_len) // 2
    return s[start:start+max_len]

print(manacher('babad'))   # 'bab'

游程编码

游程编码(RLE)会压缩连续重复的字符:'aaabbc' 会变成 'a3b2c1'。实现时,请使用快指针扫描,以找到每个游程的末尾;将字符和计数写入输出列表,然后使用 join。对于较短的游程,输入可能比编码后的输出更短——返回编码结果前,请始终检查编码版本是否更短。

def encode_rle(s):
    if not s: return ''
    parts = []
    i = 0
    while i < len(s):
        char = s[i]
        j = i
        while j < len(s) and s[j] == char:
            j += 1
        count = j - i
        parts.append(char + (str(count) if count > 1 else ''))
        i = j
    encoded = ''.join(parts)
    return encoded if len(encoded) < len(s) else s

print(encode_rle('aaabbc'))    # 'a3b2c'
print(encode_rle('abc'))       # 'abc'  (no compression gain)

解码游程编码字符串

RLE 解码会读取字符及其后面的数字序列,并展开每个游程。面试官有时会给出 LeetCode 变体,其中使用 k[encoded_string] 表示重复的子字符串,例如 3[ab] → ababab。这种嵌套变体需要使用栈来处理多层嵌套。

def decode_rle(s):
    result = []
    i = 0
    while i < len(s):
        char = s[i]; i += 1
        num_str = ''
        while i < len(s) and s[i].isdigit():
            num_str += s[i]; i += 1
        count = int(num_str) if num_str else 1
        result.append(char * count)
    return ''.join(result)

print(decode_rle('a3b2c'))    # 'aaabbc'
print(decode_rle('a2b3c1'))   # 'aabbbc'

# Nested bracket decode (LeetCode 394)
def decode_bracket(s):
    stack = []
    for c in s:
        if c != ']':
            stack.append(c)
        else:
            chars = []
            while stack[-1] != '[':
                chars.append(stack.pop())
            stack.pop()  # remove '['
            k = int(stack.pop())
            stack.append(''.join(reversed(chars)) * k)
    return ''.join(stack)
print(decode_bracket('3[ab]'))  # 'ababab'

有效回文 II:允许删除一个字符

给定一个字符串,如果最多删除一个字符后可以将其变成回文,则返回真值。请使用双指针;在首次不匹配时,检查 s[left+1:right+1] 或 s[left:right] 是否为回文(也就是尝试跳过两个不匹配字符中的每一个)。如果任一部分是回文,则返回真值。该贪心方法有效,因为跳过不匹配字符是唯一有用的操作。

def valid_palindrome(s):
    def is_pal(l, r):
        while l < r:
            if s[l] != s[r]: return False
            l += 1; r -= 1
        return True

    left, right = 0, len(s) - 1
    while left < right:
        if s[left] != s[right]:
            # Try skipping either character
            return is_pal(left+1, right) or is_pal(left, right-1)
        left += 1; right -= 1
    return True

print(valid_palindrome('aba'))    # True
print(valid_palindrome('abca'))   # True  (delete 'c')
print(valid_palindrome('abc'))    # False

回文分割 I

请将字符串分割为所有由回文组成的子串。使用回溯:每一步都尝试剩余字符串的所有前缀;如果某个前缀是回文,就对剩余部分递归处理。请使用区间 DP 预先计算二维布尔表 is_pal[i][j],使回文检查的时间复杂度降为 O(1),从而将整体回溯复杂度从 O(n² × 2^n) 降至 O(n × 2^n)——由于生成所有分割方案本质上是指数级的,这样的复杂度是可以接受的。

def partition(s):
    n = len(s)
    dp = [[False]*n for _ in range(n)]
    for i in range(n):
        dp[i][i] = True
    for length in range(2, n+1):
        for i in range(n-length+1):
            j = i + length - 1
            if s[i] == s[j]:
                dp[i][j] = length == 2 or dp[i+1][j-1]

    result = []
    def backtrack(start, path):
        if start == n: result.append(path[:]); return
        for end in range(start, n):
            if dp[start][end]:
                path.append(s[start:end+1])
                backtrack(end+1, path)
                path.pop()
    backtrack(0, [])
    return result

print(partition('aab'))  # [['a','a','b'],['aa','b']]

最短回文:字符串哈希

请找出通过在字符串前面添加字符所能得到的最短回文。关键思路是:找到 s 的最长回文前缀,然后将剩余后缀的 reverse 添加到前面。为了高效找到最长回文前缀,请在字符串 s + '#' + reverse(s) 上使用 KMP 的失配函数。失配函数的最后一个值就是最长回文前缀的长度。

def shortest_palindrome(s):
    rev = s[::-1]
    combined = s + '#' + rev  # '#' prevents overlap
    n = len(combined)
    kmp = [0] * n
    j = 0
    for i in range(1, n):
        while j > 0 and combined[i] != combined[j]:
            j = kmp[j-1]
        if combined[i] == combined[j]:
            j += 1
        kmp[i] = j
    # kmp[-1] = length of longest palindromic prefix
    to_add = rev[:len(s) - kmp[-1]]
    return to_add + s

print(shortest_palindrome('aacecaaa'))  # 'aaacecaaa'
print(shortest_palindrome('abcd'))      # 'dcbabcd'

快速检查

请检验您对本课中数据结构与算法——编程面试准备相关概念的理解。

课程回顾

本课中,您学习了:使用双指针检测回文的时间复杂度为 O(n)、空间复杂度为 O(1)——当空间重要时,应始终优先使用基于索引的检查,而不是分配一个反转后的 copy,围绕中心扩展将 2n-1 个位置中的每一个视为潜在回文中心,从而在 O(n²) 时间内找到最长回文子串,以及游程编码在 O(n) 时间内压缩连续游程,而解码嵌套括号变体时需要使用栈。接下来我们将学习冒泡排序和插入排序。

常见问题解答

「字符串编码、反转与回文」课时是免费的吗?

是的 — 「字符串编码、反转与回文」的完整文本可在网页上免费阅读。要进行交互式练习(内置代码编辑器和全天候 AI 导师)并解锁 Coding Interview Prep 课程的其余内容,请升级到 CoddyKit PRO。 Coding Interview Prep 课程共包含 4 节课。

「字符串编码、反转与回文」这节课中我会学到什么?

实现单词原地反转、游程编码和回文检测,包括向中心扩展的技巧。 你通过在浏览器中直接运行的动手代码来练习 Coding Interview Prep,全天候 AI 导师会在你学习这节课的过程中回答你的问题。

学习 Coding Interview Prep 需要有经验吗?

无需任何先前经验。CoddyKit 上的 Coding Interview Prep 课程适合初学者到高级学习者,你可以从这里开始或从头开始,按照自己的节奏学习。 这是第 4 节课,共 4 节。

「字符串编码、反转与回文」课时需要多长时间?

大多数 CoddyKit 课程大约需要 5–10 分钟。每节课都很精短且互动,所以你能稳步进步,并在网页和应用中从离开的地方继续。

我能在这节 Coding Interview Prep 课中编写并运行代码吗?

能。每节 Coding Interview Prep 课都包含内置代码编辑器,你可以在浏览器中直接编写并运行真实代码,并获得即时 AI 反馈 — 无需本地设置。

此课程中的所有课时

  1. 面试必备的 Python 字符串 API
  2. 子串的滑动窗口
  3. 字母异位词与字符频率映射
  4. 字符串编码、反转与回文
← 返回 Coding Interview Prep