LeetCode 每日一题笔记 日期:2026.05.28 题目:3093. 最长公共后缀查询

LeetCode 每日一题笔记

0. 前言

  • 日期:2026.05.28
  • 题目:3093. 最长公共后缀查询
  • 难度:困难
  • 标签:字典树、字符串

1. 题目理解

问题描述

给定两个字符串数组 wordsContainerwordsQuery

对每个查询字符串 wordsQuery[i],在 wordsContainer 中找到与它有最长公共后缀 的字符串,返回其下标。

如果有多个字符串满足条件:

  • 优先选择长度更短的;
  • 若长度也相同,选择出现更早的(下标更小)。

示例

输入:wordsContainer = ["abcd","bcd","xbcd"], wordsQuery = ["cd","bcd","xyz"]

输出:[1,1,1]

2. 解题思路

核心观察

  • 后缀匹配可以通过将字符串反转,转化为前缀匹配问题,用字典树(Trie)解决。
  • 字典树的每个节点存储当前路径下"最优"字符串的下标:
    1. 能匹配到该后缀;
    2. 长度最短;
    3. 长度相同时下标最小。

算法步骤

  1. 构建字典树:将 wordsContainer 中的每个字符串逆序插入字典树;
  2. 插入时,在每个节点维护当前路径下的"最优"下标;
  3. 查询时,将 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. 总结

  • 核心思路:后缀转前缀 + 字典树,通过逆序字符串将后缀匹配问题转化为前缀匹配问题。
  • 优化关键:在字典树节点中提前维护"最优"下标,避免查询时二次比较,直接返回结果。
  • 技巧:根节点存储全局最优,处理无公共后缀的边界情况,简化查询逻辑。
相关推荐
用户97183563346621 小时前
银河麒麟 KY10 申威(SW64) 安装 nginx-1.16.1-2.p01.ky10.sw_64.rpm 详细步骤
linux
猪脚踏浪1 天前
linux 拷贝文件或目录到指定的位置
linux
摇滚侠2 天前
Linux CentOS7 rpm 安装 MySQL 5.7
linux·运维·mysql
bush42 天前
嵌入式linux学习记录十四、术语
linux·嵌入式
载数而行5202 天前
Linux 11 动态监控指令top
linux
不会C语言的男孩2 天前
Linux 系统编程 · 第 8 章:进程基础
linux·c语言
闪闪发亮的小星星2 天前
高斯光以及高斯光公式解释
笔记
古城小栈2 天前
Unix 与 Linux 异同小叙
linux·服务器·unix
cqbzcsq2 天前
CellFlow虚拟细胞论文阅读
论文阅读·人工智能·笔记·学习·生物信息
凡人叶枫2 天前
Effective C++ 条款42:了解 typename 的双重意义
java·linux·服务器·c++