找出数组中的第一个回文字符串 图解题解
这道题到底在问什么
- 输入
- words = ["abc","car","ada","racecar","cool"]
- 输出
- "ada"(racecar 也是回文,但 ada 在前)
- 输入
- words = ["def","ghi"]
- 输出
- ""(一个回文都没有)
先想最直接的笨办法
这是数组 words,一共四个字符串:cat、abca、noon、kayak。我们的活儿是从最左边开始,一个一个拿去判断是不是回文,谁先中,谁就是答案。先记住这个顺序,因为题目要的是第一个,顺序错不得。(动画第 4 步)
最优解:为什么这么做
一句话答案:LeetCode 2108 找出数组中的第一个回文字符串:从左往右遍历 words,每个词用首尾双指针向中间夹着判回文,先中的那个立即返回、一个都没有就返回空串,时间 O(L)、空间 O(1)。
words 里第一个回文串,返回的是哪个
给一个字符串数组 words,从前往后找,返回里面第一个正读反读都一样的词——也就是回文;一个回文都没有就返回空串 ""。题眼卡在「第一个」:哪怕后面有更长的回文,只要前面先冒出一个,答案就是前面那个,顺序动不得。
题面例子 words=["abc","car","ada","racecar","cool"] 返回 "ada"——racecar 也是回文,但 ada 排在它前面,先中先赢;words=["def","ghi"] 里两个都不是回文,返回空串 ""。
怎么判一个词是不是回文
回文就是正着读反着读一模一样,像 "ada"、"racecar",也就是第一个和最后一个相同、第二个和倒数第二个相同,一路对到中间。直接的写法是把整串反过来跟原串比,相等就是回文,Python 一行 w==w[::-1] 就干这事;但反转要临时开一个等长副本,多花 O(k) 空间(k 是词长)。
首尾两根指针往中间夹,一对不等就出局
不反转也能判:摆两个下标,左指针 l 从第 0 位、右指针 r 从最后一位,每轮比 w[l] 和 w[r]。相等就 l 右挪、r 左挪,比下一对;只要有一对不等,这个词当场判死,不用再往里比。两指针在中间碰头(l 不再小于 r)都没撞上不等的,就是回文。
循环条件写 while l<r 就够:两指针相遇时中间那个字符是跟自己比、恒等,不必再看。这样只用两个下标、不开副本,空间 O(1),奇数长度的词也照样能过。
为什么碰到第一个回文就得立刻返回
外层从左往右一个词一个词过,内层用上面的双指针判。判出是回文的那一刻必须马上 return、别再往后扫。题目要「第一个」,你要是扫完全程只留最后遇到的那个,返回的就可能是排在后面的回文,答案错位。反过来,前面的词都判了不是回文、外层才往右挪,所以第一个中的一定顺序最靠前。
题面那五个词,从头手算一遍
第一个词 "abc":l=0 指 a、r=2 指 c,不等,出局。第二个 "car":l=0 指 c、r=2 指 r,不等,出局。第三个 "ada":l=0 指 a、r=2 指 a,相等,l 挪到 1、r 挪到 1,此时 l<r 不再成立、两指针相遇,没撞上任何不等的一对,判定是回文——它又是从左数第一个中的,立刻返回 "ada",后面的 racecar、cool 根本不看。
再走 words=["def","ghi"]:"def" 的 d 和 f 不等出局,"ghi" 的 g 和 i 不等出局,两个都判死,外层扫完没 return,返回空串 ""。
无解返回 null、条件写成 l≤r,都会埋雷
没有回文时题目要的是空串 "" 而非 null,返回 null 语义对不上、下游取值可能报错。内层条件写 l≤r 只是多比一次中心字符和自己(恒等、不出错),l<r 更干净。别以为回文非得偶数长度,"ada"、"level" 都是奇数长度的回文,双指针在中心处相遇自然通过。复杂度上最坏每个词从头比到尾、每个字符至多看一次,时间 O(L)(L 是总字符数);双指针版只用两个下标,空间 O(1)。
▶ 动画逐步走查(共 24 步)——想跟着动画一帧帧对照就展开
- 3记牢这套「一个词一个词过,每个词用首尾双指针向中间夹,先中的那个回文就是答案」,下面每一帧都在套它。
- 4这是数组 words,一共四个字符串:cat、abca、noon、kayak。我们的活儿是从最左边开始,一个一个拿去判断是不是回文,谁先中,谁就是答案。先记住这个顺序,因为题目要的是第一个,顺序错不得。
- 5外层循环从最左边开始,第一个词是 "cat"。把它单独拎出来,用首尾双指针检查它是不是回文。接下来切到字符视角,看这个词的每个字母。
- 6把 "cat" 的字母一个个铺开。左指针 l 站在最左的第 0 位,右指针 r 站在最右的第 2 位。回文的判法就是让这两根指针一对一对地比,同时往中间走。
- 7先把两根指针指的字符读出来:左边 l 指着 "c",右边 r 指着 "t"。回文的判断就是逐对看这两个字符相不相等,下一帧就来比它们。
- 8比 l 指的 c 和 r 指的 t,两个字母不一样,标红。回文只要有一对首尾对不上,整个串就出局了,不用再往里比,可以直接判它不是回文。
- 9"cat" 判定不是回文,在词表里把它标灰淘汰。外层循环往右挪一格,拿下一个词继续同样的检查。到目前为止还没找到回文,不能停。
- 10前面的词都淘汰了,外层往后挪一格,现在检查 "abca"。同样的办法,首尾双指针夹一遍。接下来切到字符视角,看这个词的每个字母。
- 11把 "abca" 的字母一个个铺开。左指针 l 站在最左的第 0 位,右指针 r 站在最右的第 3 位。回文的判法就是让这两根指针一对一对地比,同时往中间走。
- 12先把两根指针指的字符读出来:左边 l 指着 "a",右边 r 指着 "a"。回文的判断就是逐对看这两个字符相不相等,下一帧就来比它们。
- 13比 l 指的 a 和 r 指的 a,两个字母一样,这一对配上了,标绿。回文要求每一对首尾都相等,目前为止没问题,继续往里夹。
- 14这一对过关,左指针往右挪一格到第 1 位,右指针往左挪一格到第 2 位,接着比下一对。
- 15比 l 指的 b 和 r 指的 c,两个字母不一样,标红。回文只要有一对首尾对不上,整个串就出局了,不用再往里比,可以直接判它不是回文。
- 16"abca" 判定不是回文,在词表里把它标灰淘汰。外层循环往右挪一格,拿下一个词继续同样的检查。到目前为止还没找到回文,不能停。
- 17前面的词都淘汰了,外层往后挪一格,现在检查 "noon"。同样的办法,首尾双指针夹一遍。接下来切到字符视角,看这个词的每个字母。
- 18把 "noon" 的字母一个个铺开。左指针 l 站在最左的第 0 位,右指针 r 站在最右的第 3 位。回文的判法就是让这两根指针一对一对地比,同时往中间走。
- 19先把两根指针指的字符读出来:左边 l 指着 "n",右边 r 指着 "n"。回文的判断就是逐对看这两个字符相不相等,下一帧就来比它们。
- 20比 l 指的 n 和 r 指的 n,两个字母一样,这一对配上了,标绿。回文要求每一对首尾都相等,目前为止没问题,继续往里夹。
- 21这一对过关,左指针往右挪一格到第 1 位,右指针往左挪一格到第 2 位,接着比下一对。
- 22比 l 指的 o 和 r 指的 o,两个字母一样,这一对配上了,标绿。回文要求每一对首尾都相等,目前为止没问题,继续往里夹。
- 23两根指针在中间碰头了,l 已经不小于 r,说明从外到内每一对都比过、而且都相等,再没有要比的了。
- 24"noon" 的每一对首尾都相等,正着读反着读一模一样,它就是回文。它又是从左往右第一个中的,那答案就是它,程序在这里立刻返回 "noon",后面还没看的词全都不用管了。
- 25回到词表,"noon" 标绿,它就是第一个回文。注意它右边的 "kayak" 其实也是回文,但因为我们在 "noon" 这里就返回了,"kayak" 根本没被检查过。这正是「第一个」的含义:先中先赢,后面不看。
- 26回放整个过程:"cat" 首尾 c 和 t 不等淘汰,"abca" 比到第二对 b 和 c 不等淘汰,"noon" 两对全等,是第一个回文,直接返回。灰色的是淘汰的,绿色的是答案。判定始终只看两条:外层顺序从前往后,内层首尾双指针一对不等就出局。
⚠️ 容易写错的地方
✗ 错:找到回文后继续往后扫,最后返回找到的某一个
✓ 对:碰到第一个回文就立即返回
题目要的是「第一个」,继续扫可能把后面的回文覆盖上去,返回错的那个
✗ 错:双指针循环条件写成 i ≤ j
✓ 对:写成 i < j 即可
奇数长度时中间那个字符和自己比恒等,没必要;写成 ≤ 只是多比一次不出错,但 i < j 更干净
✗ 错:没有回文时返回 null 或不返回
✓ 对:返回空串 ""
题目明确规定不存在时返回空字符串,返回 null 会和空串语义不符、下游取值可能报错
✗ 错:以为回文必须偶数长度
✓ 对:奇偶都行,奇数中心对称也是回文
像 "ada"、"level" 是奇数长度的回文,双指针在中间那个字符处 l 与 r 相遇,自然通过
完整代码(Python / C++ / Java)
Python
from __future__ import annotations
from typing import *
from collections import *
from functools import *
from itertools import *
from math import *
from heapq import *
from bisect import *
class TreeNode:
def __init__(self, val=0, left=None, right=None):
self.val = val
self.left = left
self.right = right
class ListNode:
def __init__(self, val=0, next=None):
self.val = val
self.next = next
class Solution:
def firstPalindrome(self, words: List[str]) -> str:
return next((w for w in words if w == w[::-1]), "")C++
#include <algorithm>
#include <array>
#include <cctype>
#include <climits>
#include <cmath>
#include <deque>
#include <functional>
#include <iostream>
#include <limits>
#include <map>
#include <numeric>
#include <optional>
#include <queue>
#include <set>
#include <sstream>
#include <stack>
#include <string>
#include <tuple>
#include <unordered_map>
#include <unordered_set>
#include <utility>
#include <vector>
using namespace std;
struct TreeNode {
int val;
TreeNode *left;
TreeNode *right;
TreeNode() : val(0), left(nullptr), right(nullptr) {}
TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
TreeNode(int x, TreeNode *left, TreeNode *right) : val(x), left(left), right(right) {}
};
struct ListNode {
int val;
ListNode *next;
ListNode() : val(0), next(nullptr) {}
ListNode(int x) : val(x), next(nullptr) {}
ListNode(int x, ListNode *next) : val(x), next(next) {}
};
class Solution {
public:
string firstPalindrome(vector<string>& words) {
for (auto& w : words) {
bool ok = true;
for (int i = 0, j = w.size() - 1; i < j; ++i, --j) {
if (w[i] != w[j]) {
ok = false;
}
}
if (ok) {
return w;
}
}
return "";
}
};Java
import java.util.*;
class TreeNode { int val; TreeNode left; TreeNode right; TreeNode(){} TreeNode(int val){this.val=val;} TreeNode(int val, TreeNode left, TreeNode right){this.val=val;this.left=left;this.right=right;} }
class ListNode { int val; ListNode next; ListNode(){} ListNode(int val){this.val=val;} ListNode(int val, ListNode next){this.val=val;this.next=next;} }
class Solution {
public String firstPalindrome(String[] words) {
for (String w : words) {
boolean ok = true;
for (int i = 0, j = w.length() - 1; i < j && ok; ++i, --j) {
if (w.charAt(i) != w.charAt(j)) {
ok = false;
}
}
if (ok) {
return w;
}
}
return "";
}
}复杂度
时间
O(L)
L 是所有字符串的总长度。最坏情况每个词都要从头比到尾,每个字符至多被看一次,整体和总字符数成线性;命中越靠前实际越快
空间
O(1)
双指针只用两个下标,常数额外空间。要提醒的是 Python 那版用整串反转,会为当前词临时生成一个等长的反转副本,那一步是 O(k),k 为该词长度,不过是逐词临时占用,不累加
看不够?换成动画再走一遍
上面的推演每一步都对应一帧动画。点开交互动画版,能一步步看着 找出数组中的第一个回文字符串 的数据怎么变、指针怎么走,还能切 Python / Java / C++ 跟着练。
面试官可能追问
找到一个回文后继续往后扫、最后返回,为什么是错的?+
题目要的是「第一个」回文,不是任意一个。你要是不立刻 return、而是一路扫到底再返回最后记下的那个,遇到 words 里靠后还有回文时,返回值就被后面的覆盖成了排在后面的词,答案错位。正确做法是内层一判出是回文就当场 return,外层从左往右扫保证了先 return 的必是顺序最靠前的那个。
双指针循环条件用 while l+
结果都对,差在中间那一次多余的比较。串长是奇数时,l 和 r 会走到同一个位置,那是拿中心字符跟它自己比、必然相等,写 l≤r 就多比这一次、不影响正确性;写 l
Python 一行反转和 C++ / Java 的双指针,判回文有优劣吗?+
思路一致、只是判回文的手段不同。Python 直接拿整串和它的反转切片 w[::-1] 比相等,写起来最短,但反转会为当前词临时开一个等长副本,多占 O(k) 空间(k 是词长)。C++ / Java 用首尾双指针一对对比,不开副本、空间 O(1)。两者时间都是 O(L),L 是所有词的总字符数;面试里手写通常给双指针版,省空间也更能体现你懂回文的判定过程。
想听吴师兄把这道题讲给你听?
文字版和动画都随便看。开通图解算法年卡,可以听吴师兄把 找出数组中的第一个回文字符串 一步步讲透(全站已上线 905 份讲透,持续扩充), 卡住的地方还有 AI 私教小欧就着动画帮你拆到懂。
本平台为独立第三方培训机构,与华为技术有限公司无任何关联;课程的服务内容与权益以购买协议为准,学习效果因个人情况而异。「华为 OD」「华为可信」等仅为对岗位与考试方向的客观描述,相关商标归各自权利人所有。