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. 总结

  • 核心思路:后缀转前缀 + 字典树,通过逆序字符串将后缀匹配问题转化为前缀匹配问题。
  • 优化关键:在字典树节点中提前维护"最优"下标,避免查询时二次比较,直接返回结果。
  • 技巧:根节点存储全局最优,处理无公共后缀的边界情况,简化查询逻辑。
相关推荐
dnfdsaa11 小时前
【如何在Ubuntu 22上安装Claude Code并配置跳过官方引导】
linux·运维·ubuntu
墨白曦煜11 小时前
算法实战笔记:链表的底层逻辑与指针的高阶玩法(二)
笔记·算法·链表
AOwhisky11 小时前
Ceph系列第一期:Ceph分布式存储核心概念与架构初识
linux·运维·笔记·分布式·ceph·学习·架构
谷雨不太卷11 小时前
进程如何加载文件
linux·运维·服务器
hjjdebug11 小时前
linux 如何读取的cpu 温度? (真实平台)
linux·driver·thermal_zone
shandianchengzi11 小时前
【记录】LosslessCut|Linux下配置开源无损剪辑软件 LosslessCut AppImage 命令行启动和设置图标
linux·运维·服务器·音视频·视频·剪辑
小猫咪0111 小时前
Linux 查看端口占用:netstat、ss、lsof 谁更好用?
linux·运维·网络
都在酒里11 小时前
Linux字符设备驱动开发(八):中断底半部——tasklet与工作队列实现按键消抖
linux·运维·驱动开发·交互
cui_ruicheng11 小时前
Linux网络编程(十):自定义协议与网络计算器
linux·服务器·网络·tcp/ip