745. 前綴和后綴搜索 : 常規(guī) Trie 運(yùn)用題

題目描述

這是 LeetCode 上的 745. 前綴和后綴搜索 嗤朴,難度為 困難

Tag : 「字典樹」

設(shè)計(jì)一個(gè)包含一些單詞的特殊詞典虫溜,并能夠通過前綴和后綴來(lái)檢索單詞雹姊。

實(shí)現(xiàn) WordFilter 類:

  • WordFilter(string[] words) 使用詞典中的單詞 words 初始化對(duì)象。
  • f(string pref, string suff) 返回詞典中具有前綴 prefix 和后綴 suff 的單詞的下標(biāo)衡楞。如果存在不止一個(gè)滿足要求的下標(biāo)吱雏,返回其中 最大的下標(biāo) 。如果不存在這樣的單詞寺酪,返回 -1 坎背。

示例:

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

輸出
[null, 0]

解釋
WordFilter wordFilter = new WordFilter(["apple"]);
wordFilter.f("a", "e"); // 返回 0 ,因?yàn)橄聵?biāo)為 0 的單詞:前綴 prefix = "a" 且 后綴 suff = "e" 寄雀。

提示:

  • 1 <= words.length <= 10^4
  • 1 <= words[i].length <= 7
  • 1 <= pref.length, suff.length <= 7
  • words[i]prefsuff 僅由小寫英文字母組成
  • 最多對(duì)函數(shù) f 執(zhí)行 10^4 次調(diào)用

基本分析

為了方便陨献,我們令 wordsss盒犹,令 prefsuff 分別為 ab

搜索某個(gè)前綴(后綴可看做是反方向的前綴)容易想到字典樹眨业,但單詞長(zhǎng)度數(shù)據(jù)范圍只有 7急膀,十分具有迷惑性,使用暴力做法最壞情況下會(huì)掃描所有的 ss[i]龄捡,不考慮任何的剪枝操作的話卓嫂,計(jì)算量也才為 2 \times \ 7 \times 1e4 = 1.4 \times 10^5,按道理是完全可以過的聘殖。

但不要忘記 LC 是一個(gè)具有「設(shè)定每個(gè)樣例時(shí)長(zhǎng)晨雳,同時(shí)又有總時(shí)長(zhǎng)」這樣奇怪機(jī)制的 OJ行瑞。

暴力(TLE or 雙百)

于是有了 Java 總時(shí)間超時(shí),TypeScripe 雙百的結(jié)果(應(yīng)該是 TypeScript 提交不多餐禁,同時(shí)設(shè)限寬松的原因):

image.png

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
    }
}
  • 時(shí)間復(fù)雜度:初始化操作復(fù)雜度為 O(1)血久,檢索操作復(fù)雜度為 O(\sum_{i = 0}^{n - 1} ss[i].length)
  • 空間復(fù)雜度:O(\sum_{i = 0}^{n - 1} ss[i].length)

Trie

使用字典樹優(yōu)化檢索過程也是容易的,分別使用兩棵 Trie 樹來(lái)記錄 ss[i] 的前后綴帮非,即正著存到 tr1 中氧吐,反著存到 Tr2 中。

還不了解 Trie 的同學(xué)可以先看前置 ??:【設(shè)計(jì)數(shù)據(jù)結(jié)構(gòu)】實(shí)現(xiàn) Trie (前綴樹)
前置 ?? 通過圖解形式講解了 Trie 的結(jié)構(gòu)與原理末盔,以及提供了兩種實(shí)現(xiàn) Trie 的方式

同時(shí)對(duì)于字典樹的每個(gè)節(jié)點(diǎn)筑舅,我們使用數(shù)組 idxs 記錄經(jīng)過該節(jié)點(diǎn)的字符串 ss[i] 所在 ss 中的下標(biāo) i,若某個(gè)字典樹節(jié)點(diǎn)的索引數(shù)組 tr.idxs[a_1, a_2, ..., a_k] 則代表「從根節(jié)點(diǎn)到 tr 節(jié)點(diǎn)所對(duì)應(yīng)的字符串」為 ss[a_1], ss[a_2], ..., ss[a_k] 的前綴陨舱。

這樣我們可以即可在掃描前后綴 ab 時(shí)翠拣,得到對(duì)應(yīng)的候選下標(biāo)列表 l1l2,由于我們將 ss[i] 添加到兩棵 tr 中是按照下標(biāo)「從小到大」進(jìn)行隅忿,因此我們使用「雙指針」算法分別從 l1l2 結(jié)尾往后找到第一個(gè)共同元素即是答案(滿足條件的最大下標(biāo))心剥。

使用 Trie 優(yōu)化后,JavaTLEAC背桐,TypeScript 耗時(shí)為原本的 \frac{1}{7}

image.png

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] == null) return -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] == null) return -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] == null) return -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] == null) return -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] == nullptr) return -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] == nullptr) return -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);
    }
};
  • 時(shí)間復(fù)雜度:初始化操作復(fù)雜度為 O(\sum_{i = 0}^{n - 1} ss[i].length)兽肤,檢索過程復(fù)雜度為 O(a + b + n),其中 a = b = 7 為前后綴的最大長(zhǎng)度啃炸,n = 1e4 為初始化數(shù)組長(zhǎng)度谐算,代表最多有 n 個(gè)候選下標(biāo)(注意:這里的 n 只是粗略分析,實(shí)際上如果候選集長(zhǎng)度越大的話弊仪,說(shuō)明兩個(gè)候選集是重合度是越高的熙卡,從后往前找的過程是越快結(jié)束的,可以通過方程算出一個(gè)雙指針的理論最大比較次數(shù) k励饵,如果要將 n 卡滿成 1e4 的話驳癌,需要將兩個(gè)候選集設(shè)計(jì)成交替下標(biāo),此時(shí) f 如果仍是 1e4 次調(diào)用的話役听,必然會(huì)面臨大量的重復(fù)查詢颓鲜,可通過引入 map 記錄查詢次數(shù)來(lái)進(jìn)行優(yōu)化)
  • 空間復(fù)雜度:O(\sum_{i = 0}^{n - 1} ss[i].length)

最后

這是我們「刷穿 LeetCode」系列文章的第 No.745 篇,系列開始于 2021/01/01典予,截止于起始日 LeetCode 上共有 1916 道題目甜滨,部分是有鎖題,我們將先把所有不帶鎖的題目刷完瘤袖。

在這個(gè)系列文章里面衣摩,除了講解解題思路以外,還會(huì)盡可能給出最為簡(jiǎn)潔的代碼捂敌。如果涉及通解還會(huì)相應(yīng)的代碼模板艾扮。

為了方便各位同學(xué)能夠電腦上進(jìn)行調(diào)試和提交代碼既琴,我建立了相關(guān)的倉(cāng)庫(kù):https://github.com/SharingSource/LogicStack-LeetCode

在倉(cāng)庫(kù)地址里栏渺,你可以看到系列文章的題解鏈接呛梆、系列文章的相應(yīng)代碼、LeetCode 原題鏈接和其他優(yōu)選題解磕诊。

更多更全更熱門的「筆試/面試」相關(guān)資料可訪問排版精美的 合集新基地 ????

本文由mdnice多平臺(tái)發(fā)布

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末填物,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子霎终,更是在濱河造成了極大的恐慌滞磺,老刑警劉巖,帶你破解...
    沈念sama閱讀 217,657評(píng)論 6 505
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件莱褒,死亡現(xiàn)場(chǎng)離奇詭異击困,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)广凸,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,889評(píng)論 3 394
  • 文/潘曉璐 我一進(jìn)店門阅茶,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái),“玉大人谅海,你說(shuō)我怎么就攤上這事脸哀。” “怎么了扭吁?”我有些...
    開封第一講書人閱讀 164,057評(píng)論 0 354
  • 文/不壞的土叔 我叫張陵撞蜂,是天一觀的道長(zhǎng)。 經(jīng)常有香客問我侥袜,道長(zhǎng)蝌诡,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,509評(píng)論 1 293
  • 正文 為了忘掉前任枫吧,我火速辦了婚禮浦旱,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘九杂。我一直安慰自己闽寡,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,562評(píng)論 6 392
  • 文/花漫 我一把揭開白布尼酿。 她就那樣靜靜地躺著,像睡著了一般植影。 火紅的嫁衣襯著肌膚如雪裳擎。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,443評(píng)論 1 302
  • 那天思币,我揣著相機(jī)與錄音鹿响,去河邊找鬼羡微。 笑死,一個(gè)胖子當(dāng)著我的面吹牛惶我,可吹牛的內(nèi)容都是我干的妈倔。 我是一名探鬼主播,決...
    沈念sama閱讀 40,251評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼绸贡,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼盯蝴!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起听怕,我...
    開封第一講書人閱讀 39,129評(píng)論 0 276
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤捧挺,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后尿瞭,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體闽烙,經(jīng)...
    沈念sama閱讀 45,561評(píng)論 1 314
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,779評(píng)論 3 335
  • 正文 我和宋清朗相戀三年声搁,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了黑竞。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 39,902評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡疏旨,死狀恐怖很魂,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情充石,我是刑警寧澤莫换,帶...
    沈念sama閱讀 35,621評(píng)論 5 345
  • 正文 年R本政府宣布,位于F島的核電站骤铃,受9級(jí)特大地震影響拉岁,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜惰爬,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,220評(píng)論 3 328
  • 文/蒙蒙 一喊暖、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧撕瞧,春花似錦陵叽、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,838評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至页畦,卻和暖如春胖替,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 32,971評(píng)論 1 269
  • 我被黑心中介騙來(lái)泰國(guó)打工独令, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留端朵,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 48,025評(píng)論 2 370
  • 正文 我出身青樓燃箭,卻偏偏與公主長(zhǎng)得像冲呢,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子招狸,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,843評(píng)論 2 354

推薦閱讀更多精彩內(nèi)容