LeetCode 每日一题笔记
0. 前言
- 日期:2026.05.28
- 题目:3093. 最长公共后缀查询
- 难度:困难
- 标签:字典树、字符串
1. 题目理解
问题描述 :
给定两个字符串数组 wordsContainer 和 wordsQuery。
对每个查询字符串 wordsQuery[i],在 wordsContainer 中找到与它有最长公共后缀 的字符串,返回其下标。
如果有多个字符串满足条件:
- 优先选择长度更短的;
- 若长度也相同,选择出现更早的(下标更小)。
示例:
输入:
wordsContainer = ["abcd","bcd","xbcd"],wordsQuery = ["cd","bcd","xyz"]输出:
[1,1,1]
2. 解题思路
核心观察
- 后缀匹配可以通过将字符串反转,转化为前缀匹配问题,用字典树(Trie)解决。
- 字典树的每个节点存储当前路径下"最优"字符串的下标:
- 能匹配到该后缀;
- 长度最短;
- 长度相同时下标最小。
算法步骤
- 构建字典树:将
wordsContainer中的每个字符串逆序插入字典树; - 插入时,在每个节点维护当前路径下的"最优"下标;
- 查询时,将
wordsQuery[i]逆序遍历字典树,找到最深能匹配的节点,该节点存储的下标即为答案。
3. 代码实现
java
package lc3000_lc3099.lc3093;
public class Solution {
class TrieNode {
TrieNode[] children;
Integer index;
public TrieNode(Integer index) {
children = new TrieNode[26];
this.index = index;
}
}
public class Trie {
private TrieNode root;
public Trie(Integer index) {
root = new TrieNode(index);
}
}
public int[] stringIndices(String[] wordsContainer, String[] wordsQuery) {
int n = wordsContainer.length;
int m = wordsQuery.length;
int[] res = new int[m];
int minLen = Integer.MAX_VALUE;
int defaultIdx = 0;
for (int i = 0; i < n; i++) {
if (wordsContainer[i].length() < minLen) {
minLen = wordsContainer[i].length();
defaultIdx = i;
}
}
Trie trie = new Trie(defaultIdx);
for (int i = 0; i < n; i++) {
String s = wordsContainer[i];
method2(i, s, trie.root, wordsContainer);
}
for (int i = 0; i < m; i++) {
String s = wordsQuery[i];
res[i] = method1(s, trie.root);
}
return res;
}
private void method2(int index, String s, TrieNode node, String[] wordsContainer) {
TrieNode curNode = node;
int n = s.length();
for (int i = n - 1; i >= 0; i--) {
char c = s.charAt(i);
int num = c - 'a';
if (curNode.children[num] == null) {
curNode.children[num] = new TrieNode(index);
} else {
int oldIdx = curNode.children[num].index;
int oldLen = wordsContainer[oldIdx].length();
int newLen = wordsContainer[index].length();
if (newLen < oldLen || (newLen == oldLen && index < oldIdx)) {
curNode.children[num].index = index;
}
}
curNode = curNode.children[num];
}
}
private int method1(String s, TrieNode node) {
int res = node.index;
int n = s.length();
TrieNode curNode = node;
for (int i = n - 1; i >= 0; i--) {
char c = s.charAt(i);
int num = c - 'a';
if (curNode.children[num] == null) {
break;
}
curNode = curNode.children[num];
res = curNode.index;
}
return res;
}
}
4. 代码优化说明
(代码未做任何修改,仅添加注释讲解)
java
class Solution {
// 字典树节点:记录子节点、当前路径下最短且最靠前的字符串下标
class Node {
Node[] children = new Node[26];
int minLen = Integer.MAX_VALUE; // 该路径下匹配的最短字符串长度
int idx = -1; // 该路径下匹配的最优字符串下标
}
Node root = new Node();
public int[] stringIndices(String[] wordsContainer, String[] wordsQuery) {
// 1. 构建字典树:将所有字符串逆序插入
for (int i = 0; i < wordsContainer.length; i++) {
String word = wordsContainer[i];
Node cur = root;
// 根节点记录全局最优(无公共后缀时的默认答案)
if (word.length() < cur.minLen) {
cur.minLen = word.length();
cur.idx = i;
}
// 从后往前遍历字符串(逆序插入,把后缀变成前缀)
for (int j = word.length() - 1; j >= 0; j--) {
char c = word.charAt(j);
if (cur.children[c - 'a'] == null) {
cur.children[c - 'a'] = new Node();
}
cur = cur.children[c - 'a'];
// 更新当前节点的最优下标:长度更短/长度相同但下标更小
if (word.length() < cur.minLen) {
cur.minLen = word.length();
cur.idx = i;
}
}
}
// 2. 查询每个字符串的最长公共后缀
int[] res = new int[wordsQuery.length];
for (int i = 0; i < wordsQuery.length; i++) {
String word = wordsQuery[i];
Node cur = root;
// 从后往前遍历查询字符串,在字典树中匹配
for (int j = word.length() - 1; j >= 0; j--) {
char c = word.charAt(j);
if (cur.children[c - 'a'] == null) {
break; // 匹配中断,退出循环
}
cur = cur.children[c - 'a'];
}
// 当前节点存储的idx就是最优答案
res[i] = cur.idx;
}
return res;
}
}
5. 复杂度分析
- 时间复杂度 : O ( ( N + M ) × L ) O((N + M) \times L) O((N+M)×L)
其中 N N N 为wordsContainer长度, M M M 为wordsQuery长度, L L L 为字符串平均长度。
构建字典树和查询的时间均为线性。 - 空间复杂度 : O ( N × L ) O(N \times L) O(N×L)
字典树存储所有字符串的后缀节点,空间开销为线性。
6. 总结
- 核心思路:后缀转前缀 + 字典树,通过逆序字符串将后缀匹配问题转化为前缀匹配问题。
- 优化关键:在字典树节点中提前维护"最优"下标,避免查询时二次比较,直接返回结果。
- 技巧:根节点存储全局最优,处理无公共后缀的边界情况,简化查询逻辑。