最新国产好看的视频,伊人天堂AV在线,国产Aaaaaa视频,蜜臀视频在线观看一区,人妻av色图,密臀久久久精品影片,青青视频免费观看毛片,久草在线观看视,国产三级精品色情在线

C++前綴樹字典樹的學(xué)習(xí)與模擬實(shí)現(xiàn)代碼示例

 更新時(shí)間:2023年07月12日 09:28:30   作者:魏天樂大帥哥  
這篇文章主要介紹了C++前綴樹字典樹的學(xué)習(xí)與模擬實(shí)現(xiàn)代碼示例,Trie又被稱為前綴樹、字典樹,所以當(dāng)然是一棵樹,上面這棵Trie樹包含的字符串集合是{in,inn,int,tea,ten,to},每個(gè)節(jié)點(diǎn)的編號(hào)是我們?yōu)榱嗣枋龇奖慵由先サ?需要的朋友可以參考下

前綴樹介紹

在計(jì)算機(jī)科學(xué)中,trie,又稱前綴樹字典樹,是一種有序樹,用于保存關(guān)聯(lián)數(shù)組,其中的鍵通常是字符串。常用與拼寫檢查自動(dòng)補(bǔ)全等; 是一種查找結(jié)構(gòu)他的存儲(chǔ)邏輯類似于哈希表,但是它相對(duì)于哈希表來說,限制更多,通用性較差,但是它的功能更加強(qiáng)大,可定制性也更強(qiáng)。 leetcode指路:實(shí)現(xiàn)Trie(前綴樹)

如圖可以查看trie樹的基本結(jié)構(gòu):

在這里插入圖片描述

與二叉查找樹不同,鍵不是直接保存在節(jié)點(diǎn)中,而是由節(jié)點(diǎn)在樹中的位置決定。(這個(gè)是前綴樹的精髓,也是難理解的地方,他不用存儲(chǔ)某個(gè)節(jié)點(diǎn)的實(shí)際字符,他的對(duì)應(yīng)下標(biāo)映射出了需要存儲(chǔ)的字符!)

一個(gè)節(jié)點(diǎn)的所有子孫都有相同的前綴,也就是這個(gè)節(jié)點(diǎn)對(duì)應(yīng)的字符串,而根節(jié)點(diǎn)對(duì)應(yīng)空字符串。

一般情況下,不是所有的節(jié)點(diǎn)都有對(duì)應(yīng)的值(他們只是前綴),只有葉子節(jié)點(diǎn)和部分內(nèi)部節(jié)點(diǎn)所對(duì)應(yīng)的鍵才有相關(guān)的值。

C++實(shí)現(xiàn)

核心思想

單鏈表:通過Node中封裝的Node* next來找下一個(gè)連著的節(jié)點(diǎn);

二叉樹:通過Node中封裝的Node* left 和 Node* right來找左右孩子節(jié)點(diǎn);

前綴樹?

因?yàn)榫S護(hù)了26個(gè)可能的Node*節(jié)點(diǎn),所以跟上面一個(gè)道理,只是不能枚舉出來了,用了一個(gè)Node*arr[26]來維護(hù),恰巧把這個(gè)數(shù)組的下標(biāo)設(shè)計(jì)成了某個(gè)字符的種類,就可以達(dá)到利用邏輯下標(biāo)存儲(chǔ)一個(gè)實(shí)際字符的作用了!

前綴樹插入示意圖

在這里插入圖片描述

上圖中,每經(jīng)過一個(gè)節(jié)點(diǎn),將該節(jié)點(diǎn)的pass值加一,將末尾節(jié)點(diǎn)的end值加一。通過這種操作記錄所有經(jīng)過的數(shù)據(jù)記錄

前綴樹的插入是靈魂所在,搞懂了之后結(jié)構(gòu)就明白了,不管是查找還是刪除無非就是類似對(duì)鏈表的相關(guān)操作;

前綴樹的大致框架

假設(shè)只存小寫字符:

#include <iostream>
#include <vector>
#include <string>
using namespace std;
//26 個(gè)小寫英文字母
#define NUM 26
class Trie{
private:
    Trie* arr[NUM];//多叉樹(最大26個(gè)分支,因?yàn)?6個(gè)小寫字母嘛)
    int end;  //代表word完整單詞的個(gè)數(shù),0就是無此單詞,只是一個(gè)前綴;insert一個(gè)單詞的時(shí)候,這個(gè)單詞的end首次出現(xiàn)就置為1;之后重復(fù)插入就end++;
    int pass; //代表以此前綴為公共前綴的節(jié)點(diǎn)個(gè)數(shù),每次insert的時(shí)候會(huì)調(diào)整;
public:
    Trie() {
        memset(arr,0,sizeof(arr));//每個(gè)新節(jié)點(diǎn)的映射數(shù)組內(nèi)容置nullptr
	    end = 0;
        ncount = 0; //初始化時(shí)以該映射信息字符為前綴的個(gè)數(shù)為1(這個(gè)字符本身算的1)
    }
    //插入單詞
    void Insert(string &x)
    {
    }
    //查找 完全匹配字符串x 的數(shù)量
    int Find(string &x)
    {
    }
    //查找 前綴為字符串x 的數(shù)量
    int FindContain(string& x)
    {
    }
    //刪除某個(gè)單詞(前綴)
    bool Erase(string &x)
    {
    }
    ~Trie()
    {
        //因?yàn)閚ew了26個(gè)連續(xù)的tire*空間,不要某個(gè)節(jié)點(diǎn),不要把它對(duì)應(yīng)給下層準(zhǔn)備的26個(gè)空間也delete掉 防止內(nèi)存泄漏
        for (int i = 0; i < NUMBER; i++)
		{
			if (nexts[i]){
                delete nexts[i];
            }
		}
    }
};
  • 二叉樹,父節(jié)點(diǎn)之下包含兩個(gè)節(jié)點(diǎn),分別為左右子節(jié)點(diǎn),分別開辟空間,進(jìn)行數(shù)據(jù)存儲(chǔ)。
  • 前綴樹的結(jié)構(gòu)也是類似的,它的每個(gè)節(jié)點(diǎn)包含兩個(gè)部分: 值部分和指針部分。
  • 它的存儲(chǔ)方式為:在一棵樹上,從根到子節(jié)點(diǎn),分別存儲(chǔ)所有目標(biāo)數(shù)據(jù)的每一個(gè)下標(biāo)位置上的數(shù)據(jù)
  • 值部分主要又包含兩個(gè)數(shù)據(jù): 路過該節(jié)點(diǎn)的數(shù)量為pass, 以該節(jié)點(diǎn)為結(jié)尾的數(shù)量為end。
  • 指針部分主要包含它的所有子節(jié)點(diǎn)(比如26個(gè)小寫字母),記為arr

下面的實(shí)現(xiàn)給出了四個(gè)接口:

  • Insert插入字符串,給前綴樹添加一組數(shù)據(jù)

其中Insert方法需要注意插入新單詞的過程中,路徑所有前綴的個(gè)數(shù)pass+1

  • Find查找已存入的完整匹配的字符串個(gè)數(shù)
  • FindContain 查找已存入的前綴匹配的字符串個(gè)數(shù)
  • Erase 從前綴樹中擦除一個(gè)完整字符串

Erase方法需要注意的是:

  • 需Find先檢查字符串是否存在;
  • pass == 1 時(shí),代表其下沒有任何可能存在的字符子串,所以直接將這個(gè)節(jié)點(diǎn)delete刪除即可;
  • pass不為1且存在這個(gè)字符串,我們把end有效字符串個(gè)數(shù)-1就行
  • 移除節(jié)點(diǎn)時(shí),需要提前寫好析構(gòu)函數(shù),將其節(jié)點(diǎn)開辟的26個(gè)Node*內(nèi)存全部釋放,以免出現(xiàn)內(nèi)存泄漏;

前綴樹插入字符串

另外,下面的功能代碼不過多解釋,代碼注釋自行理解,核心要點(diǎn)就是用數(shù)組下表映射的方式確定前往哪一條鏈表,之后的尋找和插入等操作都有點(diǎn)像鏈表的操作,搞個(gè)cur指針向后遍歷等;

//插入單詞
    void Insert(const string &x)
    {
        int size = x.size();
        Node* cur = root;
        cur->pass++;//不管insert啥,我們r(jià)oot空節(jié)點(diǎn)的pass相當(dāng)于必須+1,最終這個(gè)root->pass可以代表一共insert了幾次=-=
        for (int i = 0; i<size; i++) {
            char index = x[i] - 'a';
            //該字符在當(dāng)前分支下的映射為空,沒有那就new
            if (cur->arr[index] == nullptr) {
                cur->arr[index] = new Node;
            }
            cur->arr[index]->pass++; //不管 cur->arr[index] 是new的還是本來就有,insert要路過他了,他的pass+1,
            cur = cur->arr[index];//同時(shí)cur下指過去,進(jìn)行下一個(gè)字符的insert
        }
        cur->end++;//insert最終將完整字符串個(gè)數(shù)end+1
    }

前綴樹查找完整的字符串

  //查找 完全匹配字符串 x 的數(shù)量 -->end
    int Find(const string &x)
    {
        int size = x.size();
        Node* cur = root;
        for (int i = 0; i < size; i++) {
            char index = x[i] - 'a';
            //搜索到某個(gè)字符斷了,沒有這個(gè)完整的字符串x 返回0
            if (cur->arr[index] == nullptr) return 0;
            //沒斷,繼續(xù)向下一個(gè)字符搜索;
            cur = cur->arr[index];
        }
        return cur -> end;//返回完整字符串x的有效個(gè)數(shù)end
    }

前綴樹查找前綴匹配的字符串

//查找 前綴為字符串 x 的數(shù)量 -->pass
    int FindContain(const string& x)
    {
        //與Find一模一樣的邏輯,只是最后的return 變了,這體現(xiàn)了Node*封裝 int end  和int pass的好處了吧
        int size = x.size();
        Node* cur = root;
        for (int i = 0; i < size; i++) {
            char index = x[i] - 'a';
            if (cur->arr[index] == nullptr) return 0;
            cur = cur->arr[index];
        }
        return cur -> pass;
    }

前綴樹刪除完整字符串

 //刪掉某個(gè)完整單詞-以及這個(gè)單詞后面可能存在的所有路徑;(end>1 出現(xiàn)了很多次時(shí),只需要end--一下)
    bool Delete(const string &x)
    {
        if (Find(x) == 0) return false;//壓根沒這個(gè)單詞,刪除失敗;
        //有這個(gè)單詞,我們需要找到他的prev前一個(gè),delete掉他,prev的arr[x]=nullptr!
        Node* cur = root;
        Node* prev = root;
        int size = x.size();
        for (int i = 0; i < size; i++) {
            char index = x[i]-'a';
            //if (cur->arr[index] == nullptr) return false;這句不需要,都過了Find 肯定x每個(gè)字符都存在于字典樹中
            cur->pass--;//別忘了路過的路徑都得-1;
            prev = cur;
            cur = cur->arr[index];
        }
        if (cur->end==1) {//代表x所在字符串的個(gè)數(shù)只有1,直接遞歸delete刪掉它 和 他后面的子串
            delete cur;//這個(gè)delete調(diào)析構(gòu),我們析構(gòu)已經(jīng)寫好了,防止內(nèi)存泄漏;
            prev->arr[x[size - 1]-'a'] = nullptr; // prev的arr[x最后一個(gè)字符-'a'] = nullptr!
        }
        else  cur->end--;//end>1 :end--代表這個(gè)節(jié)點(diǎn)的有效字符串個(gè)數(shù)-1,end==1 end-- == 0,這個(gè)節(jié)點(diǎn)的字符有效個(gè)數(shù)為0了,但是他因?yàn)閜ass>1,暫時(shí)保存 后面的不刪;
        //end>1,那就end有效個(gè)數(shù)-1就行了;
        return true;
    }

總結(jié)

  • 這個(gè)字典樹的樹結(jié)構(gòu)可以根據(jù)需求來進(jìn)行多樣式的處理;比如我為了實(shí)現(xiàn)設(shè)計(jì)的功能,每個(gè)節(jié)點(diǎn)都保存了pass和end倆int方便功能記錄和函數(shù)內(nèi)的使用;
  • 字典樹本質(zhì)上是犧牲空間,換查找同前綴的字符串提升時(shí)間效率的一種措施,但是我們這種每個(gè)節(jié)點(diǎn)都開26個(gè)數(shù)組的方式是非常浪費(fèi)空間的,比如只有一個(gè)有效字符下標(biāo),其他25個(gè)nullptr都浪費(fèi)了,而且在大量相同的前綴下就是單鏈表,浪費(fèi)更嚴(yán)重。
  • 這時(shí)候我們可以把Node*arr[26]數(shù)組換成map<char,Node*>這樣搞(每層某一個(gè)char只會(huì)出現(xiàn)一次,所以char做key沒問題),需要配對(duì)啥就insert給紅黑樹中添加啥,大大節(jié)省空間;

所以說這個(gè)樹沒有啥固定玩法,可塑性太強(qiáng)了…這可能也是不刷題的我不常見的愿因?

完整代碼

#include <iostream>
#include <vector>
#include <string>
using namespace std;
//26 個(gè)小寫英文字母
#define NUM 26
//前綴樹節(jié)點(diǎn)
class Node {
public:
    Node* arr[NUM];//多叉樹(最大26個(gè)分支,因?yàn)?6個(gè)小寫字母嘛)
    int end;  //代表
    int pass; //代表以此前綴為公共前綴的節(jié)點(diǎn)個(gè)數(shù),每次insert的時(shí)候會(huì)調(diào)整
    Node()
    {
        end = 0;
        pass = 0;
        memset(arr, 0, sizeof(arr));
    }
    ~Node()
    {
        //這里有點(diǎn)遞歸的意思,除了刪除當(dāng)前節(jié)點(diǎn),更重要的是如果當(dāng)前節(jié)點(diǎn)的子節(jié)點(diǎn)還有非空。遞歸delete掉!
        for (int i = 0; i < NUM; i++)
        {
            if (arr[i]!=nullptr) delete arr[i];
        }
    }
};
//前綴樹主體
class Trie{
public:
    Node* root = nullptr;
public:
    Trie() {
        root = new Node;
    }
    //插入單詞
    void Insert(const string &x)
    {
        int size = x.size();
        Node* cur = root;
        cur->pass++;//不管insert啥,我們r(jià)oot空節(jié)點(diǎn)的pass相當(dāng)于必須+1,最終這個(gè)root->pass可以代表一共insert了幾次=-=
        for (int i = 0; i<size; i++) {
            char index = x[i] - 'a';
            //該字符在當(dāng)前分支下的映射為空,沒有那就new
            if (cur->arr[index] == nullptr) {
                cur->arr[index] = new Node;
            }
            cur->arr[index]->pass++; //不管 cur->arr[index] 是new的還是本來就有,insert要路過他了,他的pass+1,
            cur = cur->arr[index];//同時(shí)cur下指過去,進(jìn)行下一個(gè)字符的insert
        }
        cur->end++;//insert最終將完整字符串個(gè)數(shù)end+1
    }
    //查找 完全匹配字符串 x 的數(shù)量 -->end
    int Find(const string &x)
    {
        int size = x.size();
        Node* cur = root;
        for (int i = 0; i < size; i++) {
            char index = x[i] - 'a';
            //搜索到某個(gè)字符斷了,沒有這個(gè)完整的字符串x 返回0
            if (cur->arr[index] == nullptr) return 0;
            //沒斷,繼續(xù)向下一個(gè)字符搜索;
            cur = cur->arr[index];
        }
        return cur -> end;//返回完整字符串x的有效個(gè)數(shù)end
    }
    //查找 前綴為字符串 x 的數(shù)量 -->pass
    int FindContain(const string& x)
    {
        //與Find一模一樣的邏輯,只是最后的return 變了,這體現(xiàn)了Node*封裝 int end  和int pass的好處了吧
        int size = x.size();
        Node* cur = root;
        for (int i = 0; i < size; i++) {
            char index = x[i] - 'a';
            if (cur->arr[index] == nullptr) return 0;
            cur = cur->arr[index];
        }
        return cur -> pass;
    }
    //刪掉某個(gè)完整單詞-以及這個(gè)單詞后面可能存在的所有路徑;(end>1 出現(xiàn)了很多次時(shí),只需要end--一下)
    bool Delete(const string &x)
    {
        if (Find(x) == 0) return false;//壓根沒這個(gè)單詞,刪除失敗;
        //有這個(gè)單詞,我們需要找到他的prev前一個(gè),delete掉他,prev的arr[x]=nullptr!
        Node* cur = root;
        Node* prev = root;
        int size = x.size();
        for (int i = 0; i < size; i++) {
            char index = x[i]-'a';
            //if (cur->arr[index] == nullptr) return false;這句不需要,都過了Find 肯定x每個(gè)字符都存在于字典樹中
            cur->pass--;//別忘了路過的路徑都得-1;
            prev = cur;
            cur = cur->arr[index];
        }
        if (cur->end==1) {//代表x所在字符串的個(gè)數(shù)只有1,直接遞歸delete刪掉它 和 他后面的子串
            delete cur;//這個(gè)delete調(diào)析構(gòu),我們析構(gòu)已經(jīng)寫好了,防止內(nèi)存泄漏;
            prev->arr[x[size - 1]-'a'] = nullptr; // prev的arr[x最后一個(gè)字符-'a'] = nullptr!
        }
        else  cur->end--;//end>1 :end--代表這個(gè)節(jié)點(diǎn)的有效字符串個(gè)數(shù)-1,end==1 end-- == 0,這個(gè)節(jié)點(diǎn)的字符有效個(gè)數(shù)為0了,但是他因?yàn)閜ass>1,暫時(shí)保存 后面的不刪;
        //end>1,那就end有效個(gè)數(shù)-1就行了;
        return true;
    }
};

到此這篇關(guān)于C++前綴樹字典樹的學(xué)習(xí)與模擬實(shí)現(xiàn)代碼示例的文章就介紹到這了,更多相關(guān)C++前綴樹字典樹內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!

相關(guān)文章

  • C++實(shí)現(xiàn)LeetCode(兩個(gè)有序數(shù)組的中位數(shù))

    C++實(shí)現(xiàn)LeetCode(兩個(gè)有序數(shù)組的中位數(shù))

    這篇文章主要介紹了C++實(shí)現(xiàn)LeetCode(兩個(gè)有序數(shù)組的中位數(shù)),本篇文章通過簡(jiǎn)要的案例,講解了該項(xiàng)技術(shù)的了解與使用,以下就是詳細(xì)內(nèi)容,需要的朋友可以參考下
    2021-07-07
  • 淺析ORB、SURF、SIFT特征點(diǎn)提取方法以及ICP匹配方法

    淺析ORB、SURF、SIFT特征點(diǎn)提取方法以及ICP匹配方法

    這篇文章主要為大家介紹了常用的特征點(diǎn)提取方法(ORB、SURF、SIFT)和ICP匹配方法,文中的示例代碼講解詳細(xì),感興趣的小伙伴可以跟隨小編一起學(xué)習(xí)一下
    2021-12-12
  • C語言函數(shù)的基本使用和遞歸小結(jié)

    C語言函數(shù)的基本使用和遞歸小結(jié)

    這篇文章主要介紹了C語言函數(shù)的基本使用和遞歸小結(jié),本文給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下
    2021-09-09
  • 淺談c++11線程的互斥量

    淺談c++11線程的互斥量

    互斥量是個(gè)類對(duì)象,理解成一把鎖(保護(hù)共享數(shù)據(jù),其他想操作共享數(shù)據(jù)的線程必須等待解鎖),互斥量使用要小心,保護(hù)數(shù)據(jù)不多也不少,少了則沒達(dá)到保護(hù)效果,多了則影響效率。本文將介紹c++11線程的互斥量,感興趣的同學(xué),可以參考下。
    2021-06-06
  • C++語法中的函數(shù)重載和默認(rèn)參數(shù)

    C++語法中的函數(shù)重載和默認(rèn)參數(shù)

    這篇文章主要介紹了C++語法中的函數(shù)重載和默認(rèn)參數(shù),本文從語法角度通過實(shí)例代碼給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下
    2020-03-03
  • C++填坑的重寫,重載和隱藏的詳解

    C++填坑的重寫,重載和隱藏的詳解

    這篇文章主要介紹了C++中重載、重寫(覆蓋)和隱藏的區(qū)別,是C++面向?qū)ο蟪绦蛟O(shè)計(jì)非常重要的概念,需要的朋友可以參考下,希望能夠給你帶來幫助
    2021-09-09
  • QT使用Http協(xié)議通信的實(shí)現(xiàn)示例

    QT使用Http協(xié)議通信的實(shí)現(xiàn)示例

    使用QT進(jìn)行應(yīng)用開發(fā)時(shí),有時(shí)候需要進(jìn)行客戶端和服務(wù)端的網(wǎng)絡(luò)通信,本文主要介紹了QT使用Http協(xié)議通信的實(shí)現(xiàn)示例,具有一定的參考價(jià)值,感興趣的可以了解一下
    2023-12-12
  • C++重載的奧義之函數(shù)重載詳解

    C++重載的奧義之函數(shù)重載詳解

    函數(shù)重載是C++多態(tài)(靜態(tài)多態(tài))的特征體現(xiàn),它可以允許重復(fù)使用同一個(gè)函數(shù)名(籃子)的函數(shù),但是函數(shù)的參數(shù)列表(籃子裝的東西)是可以不一樣的。下面就簡(jiǎn)單講講C++中函數(shù)重載的相關(guān)應(yīng)用吧
    2023-04-04
  • C++實(shí)例分析講解臨時(shí)對(duì)象與右值引用的用法

    C++實(shí)例分析講解臨時(shí)對(duì)象與右值引用的用法

    對(duì)性能來說,許多的問題都需要和出現(xiàn)頻率及本身執(zhí)行一次的開銷掛鉤,有些問題雖然看似比較開銷較大,但是很少會(huì)執(zhí)行到,那也不會(huì)對(duì)程序有大的影響;同樣一個(gè)很小開銷的函數(shù)執(zhí)行很頻繁,同樣會(huì)對(duì)程序的執(zhí)行效率有很大影響。本章中作者主要根據(jù)臨時(shí)對(duì)象來闡述這樣一個(gè)觀點(diǎn)
    2022-08-08
  • C/C++實(shí)現(xiàn)獲取完整路徑下的文件名及后綴

    C/C++實(shí)現(xiàn)獲取完整路徑下的文件名及后綴

    這篇文章主要為大家詳細(xì)介紹了C/C++實(shí)現(xiàn)獲取完整路徑下的文件名及后綴的完整方法,文中的示例代碼講解詳細(xì),感興趣的小伙伴可以跟隨小編一起學(xué)習(xí)一下
    2025-11-11

最新評(píng)論

云南省| 石城县| 新安县| 察雅县| 武威市| 含山县| 册亨县| 栾城县| 格尔木市| 霞浦县| 沈阳市| 视频| 宁强县| 咸阳市| 佛教| 攀枝花市| 徐闻县| 虎林市| 于田县| 安仁县| 阿城市| 扶风县| 疏附县| 确山县| 汾西县| 吴川市| 泰兴市| 蒲城县| 景德镇市| 尖扎县| 江孜县| 广昌县| 阿鲁科尔沁旗| 恩施市| 卓尼县| 贺州市| 会宁县| 滁州市| 华坪县| 沛县| 孝昌县|