当前位置: 首页 > news >正文

【面试高频题】难度 3/5,字典树热门运用题

题目描述

这是 LeetCode 上的 「745. 前缀和后缀搜索」 ,难度为 「困难」

Tag : 「字典树」

设计一个包含一些单词的特殊词典,并能够通过前缀和后缀来检索单词。

实现 WordFilter 类:

  • WordFilter(string[] words) 使用词典中的单词 words 初始化对象。
  • f(string pref, string suff) 返回词典中具有前缀  prefix 和后缀 suff 的单词的下标。如果存在不止一个满足要求的下标,返回其中 最大的下标 。如果不存在这样的单词,返回

示例:

输入
["WordFilter""f"]
[[["apple"]], ["a""e"]]

输出
[null, 0]

解释
WordFilter wordFilter = new WordFilter(["apple"]);
wordFilter.f("a""e"); // 返回 0 ,因为下标为 0 的单词:前缀 prefix = "a" 且 后缀 suff = "e" 。

提示:

  • words[i]prefsuff 仅由小写英文字母组成
  • 最多对函数 f 执行 次调用

基本分析

为了方便,我们令 wordsss,令 prefsuff 分别为 ab

搜索某个前缀(后缀可看做是反方向的前缀)容易想到字典树,但单词长度数据范围只有 ,十分具有迷惑性,使用暴力做法最坏情况下会扫描所有的 ,不考虑任何的剪枝操作的话,计算量也才为 ,按道理是完全可以过的。

但不要忘记 LC 是一个具有「设定每个样例时长,同时又有总时长」这样奇怪机制的 OJ。

暴力(TLE or 双百)

于是有了 Java 总时间超时,TypeScripe 双百的结果(应该是 TypeScript 提交不多,同时设限宽松的原因):

alt

Java 代码:

class WordFilter {
    String[] ss;
    public WordFilter(String[] words) {
        ss = words;
    }
    public int f(String a, String b) {
        int n = a.length(), m = b.length();
        for (int k = ss.length - 1; k >= 0; k--) {
            String cur = ss[k];
            int len = cur.length();
            if (len < n || len < m) continue;
            boolean ok = true;
            for (int i = 0; i < n && ok; i++) {
                if (cur.charAt(i) != a.charAt(i)) ok = false;
            }
            for (int i = 0; i < m && ok; i++) {
                if (cur.charAt(len - 1 - i) != b.charAt(m - 1 - i)) ok = false;
            }
            if (ok) return k;
        }
        return -1;
    }
}

TypeScript 代码:

class WordFilter {
    ss: string[]
    constructor(words: string[]) {
        this.ss = words
    }
    f(a: string, b: string): number {
        const n = a.length, m = b.length
        for (let k = this.ss.length - 1; k >= 0; k--) {
            const cur = this.ss[k]
            const len = cur.length
            if (len < n || len < m) continue
            let ok = true
            for (let i = 0; i < n && ok; i++) {
                if (cur[i] != a[i]) ok = false
            }
            for (let i = m - 1; i >= 0; i--) {
                if (cur[len - 1 - i] != b[m - 1 - i]) ok = false
            }
            if (ok) return k
        }
        return -1
    }
}
  • 时间复杂度:初始化操作复杂度为 ,检索操作复杂度为
  • 空间复杂度:

Trie

使用字典树优化检索过程也是容易的,分别使用两棵 Trie 树来记录 的前后缀,即正着存到 tr1 中,反着存到 Tr2 中。

还不了解 Trie 的同学可以先看前置 🧀:实现 Trie (前缀树) 前置 🧀 通过图解形式讲解了 Trie 的结构与原理,以及提供了两种实现 Trie 的方式

同时对于字典树的每个节点,我们使用数组 idxs 记录经过该节点的字符串 所在 ss 中的下标 ,若某个字典树节点的索引数组 tr.idxs 则代表「从根节点到 tr 节点所对应的字符串」为 的前缀。

这样我们可以即可在扫描前后缀 ab 时,得到对应的候选下标列表 l1l2,由于我们将 添加到两棵 tr 中是按照下标「从小到大」进行,因此我们使用「双指针」算法分别从 l1l2 结尾往后找到第一个共同元素即是答案(满足条件的最大下标)。

使用 Trie 优化后,JavaTLEACTypeScript 耗时为原本的

alt

Java 代码:

class WordFilter {
    class TrieNode {
        TrieNode[] tns = new TrieNode[26];
        List<Integer> idxs = new ArrayList<>();
    }
    void add(TrieNode p, String s, int idx, boolean isTurn) {
        int n = s.length();
        p.idxs.add(idx);
        for (int i = isTurn ? n - 1 : 0; i >= 0 && i < n; i += isTurn ? -1 : 1) {
            int u = s.charAt(i) - 'a';
            if (p.tns[u] == null) p.tns[u] = new TrieNode();
            p = p.tns[u];
            p.idxs.add(idx);
        }
    }
    int query(String a, String b) {
        int n = a.length(), m = b.length();
        TrieNode p = tr1;
        for (int i = 0; i < n; i++) {
            int u = a.charAt(i) - 'a';
            if (p.tns[u] == nullreturn -1;
            p = p.tns[u];
        }
        List<Integer> l1 = p.idxs;
        p = tr2;
        for (int i = m - 1; i >= 0; i--) {
            int u = b.charAt(i) - 'a';
            if (p.tns[u] == nullreturn -1;
            p = p.tns[u];
        }
        List<Integer> l2 = p.idxs;
        n = l1.size(); m = l2.size();
        for (int i = n - 1, j = m - 1; i >= 0 && j >= 0; ) {
            if (l1.get(i) > l2.get(j)) i--;
            else if (l1.get(i) < l2.get(j)) j--;
            else return l1.get(i);
        }
        return -1;
    }
    TrieNode tr1 = new TrieNode(), tr2 = new TrieNode();
    public WordFilter(String[] ss) {
        int n = ss.length;
        for (int i = 0; i < n; i++) {
            add(tr1, ss[i], i, false);
            add(tr2, ss[i], i, true);
        }
    }
    public int f(String a, String b) {
        return query(a, b);
    }
}

TypeScript 代码:

class TrieNode {
    tns: TrieNode[] = new Array<TrieNode>()
    idxs: number[] = new Array<number>()
}
class WordFilter {
    add(p: TrieNode, s: string, idx: number, isTurn: boolean): void {
        const n = s.length
        p.idxs.push(idx)
        for (let i = isTurn ? n - 1 : 0; i >= 0 && i < n; i += isTurn ? -1 : 1) {
            const u = s.charCodeAt(i) - 'a'.charCodeAt(0)
            if (p.tns[u] == null) p.tns[u] = new TrieNode()
            p = p.tns[u]
            p.idxs.push(idx)
        }
    }
    query(a: string, b: string): number {
        let n = a.length, m = b.length
        let p = this.tr1
        for (let i = 0; i < n; i++) {
            const u = a.charCodeAt(i) - 'a'.charCodeAt(0)
            if (p.tns[u] == nullreturn -1
            p = p.tns[u]
        }
        const l1 = p.idxs
        p = this.tr2
        for (let i = m - 1; i >= 0; i--) {
            const u = b.charCodeAt(i) - 'a'.charCodeAt(0)
            if (p.tns[u] == nullreturn -1
            p = p.tns[u]
        }
        const l2 = p.idxs
        n = l1.length; m = l2.length
        for (let i = n - 1, j = m - 1; i >= 0 && j >= 0; ) {
            if (l1[i] < l2[j]) j--
            else if (l1[i] > l2[j]) i--
            else return l1[i]
        }
        return -1
    }
    tr1: TrieNode = new TrieNode()
    tr2: TrieNode = new TrieNode()
    constructor(ss: string[]) {
        for (let i = 0; i < ss.length; i++) {
            this.add(this.tr1, ss[i], i, false)
            this.add(this.tr2, ss[i], i, true)
        }
    }
    f(a: string, b: string): number {
        return this.query(a, b)
    }
}

C++ 代码:

class WordFilter {
public:
    struct TrieNode {
        TrieNode* tns[26] {nullptr};
        vector<int> idxs;
    };
    
    void add(TrieNode* p, const string& s, int idx, bool isTurn) {
        int n = s.size();
        p->idxs.push_back(idx);
        for(int i = isTurn ? n - 1 : 0; i >= 0 && i < n; i += isTurn ? -1 : 1) {
            int u = s[i] - 'a';
            if(p->tns[u] == nullptr) p->tns[u] = new TrieNode();
            p = p->tns[u];
            p->idxs.push_back(idx);
        }
    }
    
    int query(const string& a, const string& b) {
        int n = a.size(), m = b.size();
        auto p = tr1;
        for(int i = 0; i < n; i++) {
            int u = a[i] - 'a';
            if(p->tns[u] == nullptrreturn -1;
            p = p->tns[u];
        }
        vector<int>& l1 = p->idxs;
        p = tr2;
        for(int i = m - 1; i >= 0; i--) {
            int u = b[i] - 'a';
            if(p->tns[u] == nullptrreturn -1;
            p = p->tns[u];
        }
        vector<int>& l2 = p->idxs;
        n = l1.size(), m = l2.size();
        for(int i = n - 1, j = m - 1; i >= 0 && j >= 0; ) {
            if(l1[i] > l2[j]) i--;
            else if(l1[i] < l2[j]) j--;
            else return l1[i];
        }
        return -1;
    }
    
    TrieNode* tr1 = new TrieNode, *tr2 = new TrieNode;
    WordFilter(vector<string>& ss) {
        int n = ss.size();
        for(int i = 0; i < n; i++) {
            add(tr1, ss[i], i, false);
            add(tr2, ss[i], i, true);
        }
    }
    
    int f(string a, string b) {
        return query(a, b);
    }
};
  • 时间复杂度:初始化操作复杂度为 ,检索过程复杂度为 ,其中 为前后缀的最大长度, 为初始化数组长度,代表最多有 个候选下标(注意:这里的 只是粗略分析,实际上如果候选集长度越大的话,说明两个候选集是重合度是越高的,从后往前找的过程是越快结束的,可以通过方程算出一个双指针的理论最大比较次数 ,如果要将 卡满成 的话,需要将两个候选集设计成交替下标,此时 f 如果仍是 次调用的话,必然会面临大量的重复查询,可通过引入 map 记录查询次数来进行优化)
  • 空间复杂度:

最后

这是我们「刷穿 LeetCode」系列文章的第 No.745 篇,系列开始于 2021/01/01,截止于起始日 LeetCode 上共有 1916 道题目,部分是有锁题,我们将先把所有不带锁的题目刷完。

在这个系列文章里面,除了讲解解题思路以外,还会尽可能给出最为简洁的代码。如果涉及通解还会相应的代码模板。

为了方便各位同学能够电脑上进行调试和提交代码,我建立了相关的仓库:https://github.com/SharingSource/LogicStack-LeetCode 。

在仓库地址里,你可以看到系列文章的题解链接、系列文章的相应代码、LeetCode 原题链接和其他优选题解。

更多更全更热门的「笔试/面试」相关资料可访问排版精美的 合集新基地 🎉🎉

相关文章:

【面试高频题】难度 3/5,字典树热门运用题

题目描述 这是 LeetCode 上的 「745. 前缀和后缀搜索」 &#xff0c;难度为 「困难」。 Tag : 「字典树」 设计一个包含一些单词的特殊词典&#xff0c;并能够通过前缀和后缀来检索单词。 实现 WordFilter 类&#xff1a; WordFilter(string[] words) 使用词典中的单词 words 初…...

vue base64图片转file流 下载到本地 或者上传

<img :src"data:image/png;base64,form.img" style"max-width:280px;max-height: 280px;margin: auto;" />// base64 转file const base64ToFile()>{let byImg atob(form.img); // 解码base64let n byImg.lengthlet a new Uint8Array(n);while…...

无涯教程-PHP - 简介

PHP 7是最期待的&#xff0c;它是PHP编程语言的主要功能版本。 PHP 7于2015年12月3日发布。本教程将以简单直观的方式教您PHP 7的新功能及其用法。 无涯教程假设您已经了解旧版本的PHP&#xff0c;现在就可以开始学习PHP 7的新功能。 使用下面的示例- <html><head&…...

web基础+HTTP协议+httpd详细配置

目目录录 一、Web基础1.1 HTML概述1.1.1 HTML的文件结构1.1.2 HTML中的部分基本标签 1.3 MIME1.4 URI 和 URL1.4 定义1.4.2 URI 和 URL 的区别 二、静态资源和动态资源2.1 静态资源2.2 动态资源 三、HTTP协议3.1 HTTP协议简介3.2 HTTP协议版本3.2 HTTP方法3.3 HTTP请求访问的完…...

【sql】MongoDB的增删改查分页条件等

【sql】MongoDB的增删改查分页条件等 //增 //新增数据2种方式 db.msg.save({"name":"springboot&#x1f600;"}); db.msg.insert({"name":"mango good"}); db.msg.save({"name":"springboot",type:"工具书&…...

我的动态归纳(便于搜索)

linux dns配置文件是“/etc/resolv.conf”&#xff0c;该配置文件用于配置DNS客户&#xff0c;它包含了主机的域名搜索顺序和DNS/服务器的地址&#xff0c;每一行包括一个关键字和一个或多个空格隔开的参数。 /etc/resolv.conf &#xff08;不配置就不能域名解析&#xff09; 可…...

langchain ChatGPT AI私有知识库

企业知识库 原理就是把文档变为向量数据库&#xff0c;然后搜索向量数据库&#xff0c;把相似的数据和问题作为prompt&#xff0c; 输入到大模型&#xff0c;再利用GPT强大的自然语言处理、推理和分析等方面的能力将答案返回给用户 什么是langchain? langchain是一个强大的…...

API接口常用数据格式Json,Json的定义和XML的区别

现在程序员还有谁不知道 JSON 吗&#xff1f;无论对于前端还是后端&#xff0c;JSON 都是一种常见的数据格式。那么 JSON 到底是什么呢&#xff1f; JSON 的定义 JSON &#xff08;JavaScript Object Notation&#xff09; &#xff0c;是一种轻量级的数据交换格式。它的使用…...

密码学学习笔记(二十一):SHA-256与HMAC、NMAC、KMAC

SHA-256 SHA-2是广泛应用的哈希函数&#xff0c;并且有不同的版本&#xff0c;这篇博客主要介绍SHA-256。 SHA-256算法满足了哈希函数的三个安全属性&#xff1a; 抗第一原像性 - 无法根据哈希函数的输出恢复其对应的输入。抗第二原像性 - 给定一个输入和它的哈希值&#xf…...

操作系统-笔记-第四章-文件管理

目录 四、第四章——文件管理 1、文件管理——基础概念 &#xff08;1&#xff09;文件结构 &#xff08;2&#xff09;操作系统提供的接口 &#xff08;3&#xff09;总结 2、文件的逻辑结构 &#xff08;1&#xff09;有结构文件&#xff08;类似SQL表文件&#xff09…...

【MiniGUI】文字颜色实现透明度变化

在MiniGUi中&#xff0c;输出文字时有时候希望文字带有透明度信息&#xff0c; 即文字能够透出下面的图像来。 很自然地想到&#xff0c;设置颜色时&#xff0c;将颜色设置为带有透明度的颜色&#xff1a; SelectFont(hdc, mg_font);SetTextColor(hdc, RGBA2Pixel(HDC_SCREEN, …...

css中元素加定位之后到一定距离元素会变小

css中元素加定位之后到一定距离元素会变小 主要原因&#xff1a;元素没有加宽高 .swiperWrapper .active{bottom: 380px;left: 215px;z-index: 10; } .swiperWrapper .next{bottom: 170px;left: 7%;z-index: 20; } .swiperWrapper .prev{bottom: 360px;left: 0%;z-index: 30;…...

Java 语言实现冒泡排序

Java 语言实现冒泡排序 介绍 冒泡排序是一种简单直观的排序算法&#xff0c;它重复地比较相邻的两个元素&#xff0c;如果顺序错误就交换它们&#xff0c;直到没有需要交换的元素为止。冒泡排序的思路是通过每一轮的比较将最大&#xff08;或最小&#xff09;的元素逐渐“冒泡…...

面向对象单选题

下列选项中不属于面向对象的特征的是&#xff08;B&#xff09; A、封装性 B、安全性 C、继承性 D、多态性 在Java中,关于继承&#xff0c;类只支持&#xff08;A&#xff09; A、单继承 B、多继承 C、两个都可以 D、两个都不可以 用于定义成员的访问控制权的一组关键字…...

微服务-Fegin

在之前我们两服务之间调用的时候用的是restTemplate,但是这个方式调用存在很多的问题 String url "http://userservice/user/" order.getUserId(); 代码可读性差&#xff0c;编码体验不统一参数复杂的url难以维护 所以我们大力推出我们今天的主角--Fegin Feign是…...

[oneAPI] 使用字符级 RNN 生成名称

[oneAPI] 使用字符级 RNN 生成名称 oneAPI特殊写法使用字符级 RNN 生成名称Intel Optimization for PyTorch数据下载加载数据并对数据进行处理创建网络训练过程准备训练训练网络 结果 参考资料 比赛&#xff1a;https://marketing.csdn.net/p/f3e44fbfe46c465f4d9d6c23e38e0517…...

【ROS】参数服务器--理论模型与参数操作(C++)

一、概念介绍 参数服务器在ROS中主要用于实现不同节点之间的数据共享。参数服务器相当于是独立于所有节点的一个公共容器&#xff0c;可以将数据存储在该容器中&#xff0c;被不同的节点调用&#xff0c;当然不同的节点也可以往其中存储数据。 作用&#xff1a;存储一些多节点…...

[oneAPI] 基于BERT预训练模型的英文文本蕴含任务

[oneAPI] 基于BERT预训练模型的英文文本蕴含任务 Intel DevCloud for oneAPI 和 Intel Optimization for PyTorch基于BERT预训练模型的英文文本蕴含任务语料介绍数据集构建 模型训练 结果参考资料 比赛&#xff1a;https://marketing.csdn.net/p/f3e44fbfe46c465f4d9d6c23e38e0…...

【洛谷】P1163 银行贷款

原题链接&#xff1a;https://www.luogu.com.cn/problem/P1163 目录 1. 题目描述 2. 思路分析 3. 代码实现 1. 题目描述 2. 思路分析 这题需要注意的是利率按月累计这句话&#xff0c;也就是相当于“利滚利”。 我们定义sum变量表示贷款原值&#xff0c;money表示每月支付…...

Java版工程行业管理系统源码-专业的工程管理软件-提供一站式服务 em

​ 鸿鹄工程项目管理系统 Spring CloudSpring BootMybatisVueElementUI前后端分离构建工程项目管理系统 1. 项目背景 一、随着公司的快速发展&#xff0c;企业人员和经营规模不断壮大。为了提高工程管理效率、减轻劳动强度、提高信息处理速度和准确性&#xff0c;公司对内部工…...

除了ulimit -c unlimited:深入理解Linux core dump机制与高级配置指南

深入Linux核心转储&#xff1a;从基础配置到生产环境实战指南当服务器上的关键应用突然崩溃时&#xff0c;系统管理员最需要的就是一份完整的"事故现场记录"。Linux的core dump机制正是为此而生&#xff0c;它能保存程序崩溃时的内存状态、寄存器值和调用堆栈&#x…...

信息系统项目管理师核心知识点精讲

一、项目整合管理(重点:项目章程与项目管理计划) 知识点详解: 项目整体管理是项目管理知识体系的核心,它确保项目各要素协调统一。在考试中,特别要掌握项目章程和项目管理计划的区别与联系。 项目章程是项目的“出生证明”,由项目发起人发布。它正式授权项目,赋予项…...

用Python和MNE库玩转BCI Competition IV 2a脑电数据集:从数据加载到可视化全流程

用Python和MNE库玩转BCI Competition IV 2a脑电数据集&#xff1a;从数据加载到可视化全流程当你第一次接触脑电信号处理时&#xff0c;面对原始数据文件可能会感到无从下手。BCI Competition IV 2a数据集作为脑机接口领域的经典基准数据&#xff0c;包含了9名受试者四种运动想…...

别再手动点菜单了!用这招让Cadence Virtuoso Schematic效率翻倍(附Net高亮快捷键配置)

电路设计效率革命&#xff1a;Cadence Virtuoso Schematic高阶快捷键配置指南 在集成电路设计的浩瀚宇宙中&#xff0c;Cadence Virtuoso如同设计师手中的光刻机&#xff0c;每一次精准操作都直接影响最终芯片的性能与可靠性。然而&#xff0c;当面对数百个晶体管组成的复杂模…...

森优时铁锌维发根养黑用三个月真实效果实测:内服营养养黑的客观测评

"森优时铁锌维发根养黑用三个月真实效果实测显示&#xff0c;针对压力、熬夜引发的早白问题&#xff0c;通过内服补充毛囊所需营养的方式&#xff0c;多数使用者能感受到发根韧性提升、新生发色素沉淀改善&#xff0c;整体改善效果因人而异&#xff0c;合规的营养补充是目…...

2026年LLM推理加速全景:量化、投机解码与KV Cache工程实战

大语言模型推理速度慢、成本高&#xff0c;是阻碍AI大规模落地的核心障碍之一。一个7B参数的模型&#xff0c;在标准配置下每秒只能生成约30个token&#xff0c;对于需要实时响应的应用来说几乎无法接受。但2026年&#xff0c;一系列推理加速技术的成熟&#xff0c;让这一局面发…...

flameshow性能优化技巧:如何快速定位Go程序中的CPU热点

flameshow性能优化技巧&#xff1a;如何快速定位Go程序中的CPU热点 【免费下载链接】flameshow A terminal Flamegraph viewer. 项目地址: https://gitcode.com/gh_mirrors/fl/flameshow &#x1f525; 想要快速定位Go程序中的性能瓶颈吗&#xff1f;flameshow是一个强大…...

Claude SWOT分析(内部风控文档流出版):3类高危使用场景+2个监管红线预警

更多请点击&#xff1a; https://intelliparadigm.com 第一章&#xff1a;Claude SWOT分析&#xff08;内部风控文档流出版&#xff09;&#xff1a;3类高危使用场景2个监管红线预警 高危使用场景识别 在企业级AI应用中&#xff0c;Claude模型若未经严格风控适配&#xff0c;…...

GIS工程应用记录(AI辅助编程)

问题的问题&#xff1a;语境坍缩“从各个角度提出问题&#xff0c;AI做出对应积极答复和修改&#xff0c;结果没有什么变化。”这&#xff0c;就是元问题最核心的症状。你尝试了所有你已知的“高级”协作手段&#xff0c;但就像重拳打在棉花上&#xff0c;AI永远在积极回应&…...

如何用HsMod解锁炉石传说60+项隐藏功能:终极优化指南

如何用HsMod解锁炉石传说60项隐藏功能&#xff1a;终极优化指南 【免费下载链接】HsMod Hearthstone Modification Based on BepInEx 项目地址: https://gitcode.com/GitHub_Trending/hs/HsMod HsMod是一款基于BepInEx开发的炉石传说功能增强插件&#xff0c;为玩家提供…...