前缀树
是N叉树
的一种特殊形式。一般来讲,一个前缀树是用来存储字符串
的。前缀树的每个节点表明一个字符串
(前缀
)。每个节点会有多个子节点,通往不一样子节点的路径上有着不一样的字符。子节点表明的字符串是由节点自己的原始字符串
,以及通往该子节点路径上全部的字符
组成的。html
下面是前缀树的一个例子:node
在上图示例中,咱们在节点中标记的值是该节点对应表示的字符串。例如,咱们从根节点开始,选择第二条路径 'b',而后选择它的第一个子节点 'a',接下来继续选择子节点 'd',咱们最终会到达叶节点 "bad"。节点的值是由从根节点开始,与其通过的路径中的字符按顺序造成的。ios
值得注意的是,根节点表示空字符串
。c++
前缀树的一个重要的特性是,节点全部的后代都与该节点相关的字符串有着共同的前缀。这就是前缀树
名称的由来。正则表达式
咱们再来看这个例子。例如,以节点 "b" 为根的子树中的节点表示的字符串,都具备共同的前缀 "b"。反之亦然,具备公共前缀 "b" 的字符串,所有位于以 "b" 为根的子树中,而且具备不一样前缀的字符串来自不一样的分支。算法
前缀树有着普遍的应用,例如自动补全,拼写检查等等。咱们将在后面的章节中介绍实际应用场景。数组
在前面的文章中,咱们介绍了前缀树的概念。在这篇文章中,咱们将讨论如何用代码表示这个数据结构。数据结构
在阅读一下内容前,请简要回顾N叉树的节点结构。app
前缀树的特别之处在于字符和子节点之间的对应关系。有许多不一样的表示前缀树节点的方法,这里咱们只介绍其中的两种方法。函数
方法一 - 数组
第一种方法是用数组
存储子节点。
例如,若是咱们只存储含有字母 a
到 z
的字符串,咱们能够在每一个节点中声明一个大小为26的数组来存储其子节点。对于特定字符 c
,咱们可使用 c - 'a'
做为索引来查找数组中相应的子节点。
// change this value to adapt to different cases #define N 26 struct TrieNode { TrieNode* children[N]; // you might need some extra values according to different cases }; /** Usage: * Initialization: TrieNode root = new TrieNode(); * Return a specific child node with char c: (root->children)[c - 'a'] */
访问子节点十分快捷
。访问一个特定的子节点比较容易
,由于在大多数状况下,咱们很容易将一个字符转换为索引。但并不是全部的子节点都须要这样的操做,因此这可能会致使空间的浪费
。
方法二 - Map
第二种方法是使用 Hashmap
来存储子节点。
咱们能够在每一个节点中声明一个Hashmap。Hashmap的键是字符,值是相对应的子节点。
struct TrieNode { unordered_map<char, TrieNode*> children; // you might need some extra values according to different cases }; /** Usage: * Initialization: TrieNode root = new TrieNode(); * Return a specific child node with char c: (root->children)[c] */
经过相应的字符来访问特定的子节点更为容易
。但它可能比使用数组稍慢一些
。可是,因为咱们只存储咱们须要的子节点,所以节省了空间
。这个方法也更加灵活
,由于咱们不受到固定长度和固定范围的限制。
补充
咱们已经提到过如何表示前缀树中的子节点。除此以外,咱们也须要用到一些其余的值。
例如,咱们知道,前缀树的每一个节点表示一个字符串,但并非全部由前缀树表示的字符串都是有意义的。若是咱们只想在前缀树中存储单词,那么咱们可能须要在每一个节点中声明一个布尔值(Boolean)做为标志,来代表该节点所表示的字符串是否为一个单词。
咱们已经在另外一张卡片中讨论了 (如何在二叉搜索树中实现插入操做)。
提问:
你还记得如何在二叉搜索树中插入一个新的节点吗?
当咱们在二叉搜索树中插入目标值时,在每一个节点中,咱们都须要根据 节点值
和 目标值
之间的关系,来肯定目标值须要去往哪一个子节点。一样地,当咱们向前缀树中插入一个目标值时,咱们也须要根据插入的 目标值
来决定咱们的路径。
更具体地说,若是咱们在前缀树中插入一个字符串 S
,咱们要从根节点开始。 咱们将根据 S[0]
(S中的第一个字符),选择一个子节点或添加一个新的子节点。而后到达第二个节点,并根据 S[1]
作出选择。 再到第三个节点,以此类推。 最后,咱们依次遍历 S 中的全部字符并到达末尾。 末端节点将是表示字符串 S 的节点。
下面是一个例子:
咱们来用伪代码总结一下以上策略:
1. Initialize: cur = root 2. for each char c in target string S: 3. if cur does not have a child c: 4. cur.children[c] = new Trie node 5. cur = cur.children[c] 6. cur is the node which represents the string S
一般状况状况下,你须要本身构建前缀树。构建前缀树实际上就是屡次调用插入函数。但请记住在插入字符串以前要 初始化根节点
。
搜索前缀
正如咱们在前缀树的简介中提到的,全部节点的后代都与该节点相对应字符串的有着共同前缀。所以,很容易搜索以特定前缀开头的任何单词。
一样地,咱们能够根据给定的前缀沿着树形结构搜索下去。一旦咱们找不到咱们想要的子节点,搜索就以失败终止。不然,搜索成功。为了更具体地解释搜索的过程,咱们提供了下列示例:
咱们来用伪代码总结一下以上策略:
1. Initialize: cur = root 2. for each char c in target string S: 3. if cur does not have a child c: 4. search fails 5. cur = cur.children[c] 6. search successes
搜索单词
你可能还想知道如何搜索特定的单词,而不是前缀。咱们能够将这个词做为前缀,并一样按照上述一样的方法进行搜索。
提示:往每一个节点中加入布尔值可能会有效地帮助你解决这个问题。
实现一个 Trie (前缀树),包含 insert
, search
, 和 startsWith
这三个操做。
示例:
Trie trie = new Trie(); trie.insert("apple"); trie.search("apple"); // 返回 true trie.search("app"); // 返回 false trie.startsWith("app"); // 返回 true trie.insert("app"); trie.search("app"); // 返回 true
说明:
a-z
构成的。递归解法
#include <iostream> #include <vector> #include <map> using namespace std; /// Trie Recursive version class Trie{ private: struct Node{ map<char, int> next; bool end = false; }; vector<Node> trie; public: Trie(){ trie.clear(); trie.push_back(Node()); } /** Inserts a word into the trie. */ void insert(const string& word){ insert(0, word, 0); } /** Returns if the word is in the trie. */ bool search(const string& word){ return search(0, word, 0); } /** Returns if there is any word in the trie that starts with the given prefix. */ bool startsWith(const string& prefix) { return startsWith(0, prefix, 0); } private: void insert(int treeID, const string& word, int index){ if(index == word.size()) { trie[treeID].end = true; return; } if(trie[treeID].next.find(word[index]) == trie[treeID].next.end()){ trie[treeID].next[word[index]] = trie.size(); trie.push_back(Node()); } insert(trie[treeID].next[word[index]], word, index + 1); } bool search(int treeID, const string& word, int index){ if(index == word.size()) return trie[treeID].end; if(trie[treeID].next.find(word[index]) == trie[treeID].next.end()) return false; return search(trie[treeID].next[word[index]], word, index + 1); } bool startsWith(int treeID, const string& prefix, int index){ if(index == prefix.size()) return true; if(trie[treeID].next.find(prefix[index]) == trie[treeID].next.end()) return false; return startsWith(trie[treeID].next[prefix[index]], prefix, index + 1); } }; void printBool(bool res){ cout << (res ? "True" : "False") << endl; } int main() { Trie trie1; trie1.insert("ab"); printBool(trie1.search("a")); // false printBool(trie1.startsWith("a")); // true; cout << endl; // --- Trie trie2; trie2.insert("a"); printBool(trie2.search("a")); // true printBool(trie2.startsWith("a")); // true; return 0; }
非递归解法
#include <iostream> #include <vector> #include <map> using namespace std; /// Trie Recursive version class Trie{ private: struct Node{ map<char, int> next; bool end = false; }; vector<Node> trie; public: Trie(){ trie.clear(); trie.push_back(Node()); } void insert(const string& word){ int treeID = 0; for(char c: word){ //若未找到该节点 if(trie[treeID].next.find(c) == trie[treeID].next.end()){ trie[treeID].next[c] = trie.size(); trie.push_back(Node()); } treeID = trie[treeID].next[c]; } trie[treeID].end = true; } bool search(const string& word){ int treeID = 0; for(char c: word){ if(trie[treeID].next.find(c)==trie[treeID].next.end()) return false; treeID = trie[treeID].next[c]; } return trie[treeID].end; } bool startsWith(const string& prefix){ int treeID = 0; for(char c: prefix){ if(trie[treeID].next.find(c)==trie[treeID].next.end()) return false; treeID = trie[treeID].next[c]; } return true; } }; void printBool(bool res){ cout << (res? "True" : "False") << endl; } int main() { Trie trie1; trie1.insert("ab"); printBool(trie1.search("a")); // false printBool(trie1.startsWith("a")); // true; cout << endl; // --- Trie trie2; trie2.insert("a"); printBool(trie2.search("a")); // true printBool(trie2.startsWith("a")); // true; return 0; }
实现一个 MapSum 类里的两个方法,insert
和 sum
。
对于方法 insert
,你将获得一对(字符串,整数)的键值对。字符串表示键,整数表示值。若是键已经存在,那么原来的键值对将被替代成新的键值对。
对于方法 sum
,你将获得一个表示前缀的字符串,你须要返回全部以该前缀开头的键的值的总和。
示例 1:
输入: insert("apple", 3), 输出: Null 输入: sum("ap"), 输出: 3 输入: insert("app", 2), 输出: Null 输入: sum("ap"), 输出: 5
参考http://www.javashuo.com/article/p-agweyvri-ep.html
#include <iostream> #include <bits/stdc++.h> using namespace std; //这道题让咱们实现一个MapSum类,里面有两个方法,insert和sum,其中inser就是插入一个键值对,而sum方法比较特别,是在找一个前缀,须要将全部有此前缀的单词的值累加起来返回。看到这种玩前缀的题,照理来讲是要用前缀树来作的。可是博主通常想偷懒,不想新写一个结构或类,因而就使用map来代替前缀树啦。博主开始想到的方法是创建前缀和一个pair之间的映射,这里的pair的第一个值表示该词的值,第二个值表示将该词做为前缀的全部词的累加值,那么咱们的sum函数就异常的简单了,直接将pair中的两个值相加便可。关键就是要在insert中把数据结构建好,构建的方法也不难,首先咱们suppose本来这个key是有值的,咱们更新的时候只须要加上它的差值便可,就算key不存在默认就是0,算差值也没问题。而后咱们将first值更新为val,而后就是遍历其全部的前缀了,给每一个前缀的second都加上diff便可,参见代码以下: class MapSum{ private: unordered_map<string, pair<int, int>> m; public: MapSum(){} void insert(string key, int val){ //diff的做用防止重复插入 int diff = val - m[key].first, n = key.size(); m[key].first = val; for(int i=n-1; i>0; --i) m[key.substr(0, i)].second += diff; } int sum(string prefix){ return m[prefix].first + m[prefix].second; } }; //下面这种方法是论坛上投票最高的方法,感受很叼,用的是带排序的map,insert就是把单词加入map。在map里会按照字母顺序自动排序,而后在sum函数里,咱们根据prefix来用二分查找快速定位到第一个不小于prefix的位置,而后向后遍历,向后遍历的都是以prefix为前缀的单词,若是咱们发现某个单词不是以prefix为前缀了,直接break;不然就累加其val值,参见代码以下: class MapSum{ private: map<string, int> m; public: MapSum(){} void insert(string key, int val){ m[key] = val; } int sum(string prefix){ int res = 0, n = prefix.size(); for(auto it = m.lower_bound(prefix); it != m.end(); ++it){ if(it->first.substr(0, n) != prefix) break; res += it->second; } return res; } };
在英语中,咱们有一个叫作 词根
(root)的概念,它能够跟着其余一些词组成另外一个较长的单词——咱们称这个词为 继承词
(successor)。例如,词根an
,跟随着单词 other
(其余),能够造成新的单词 another
(另外一个)。
如今,给定一个由许多词根组成的词典和一个句子。你须要将句子中的全部继承词
用词根
替换掉。若是继承词
有许多能够造成它的词根
,则用最短的词根替换它。
你须要输出替换以后的句子。
示例 1:
输入: dict(词典) = ["cat", "bat", "rat"] sentence(句子) = "the cattle was rattled by the battery" 输出: "the cat was rat by the bat"
注:
参考https://www.cnblogs.com/grandyang/p/7423420.html
#include <iostream> #include <vector> #include <sstream> using namespace std; //这道题最好的解法实际上是用前缀树(Trie / Prefix Tree)来作,关于前缀树使用以前有一道很好的入门题Implement Trie (Prefix Tree)。了解了前缀树的原理机制,那么咱们就能够发现这道题其实很适合前缀树的特色。咱们要作的就是把全部的前缀都放到前缀树里面,并且在前缀的最后一个结点的地方将标示isWord设为true,表示从根节点到当前结点是一个前缀,而后咱们在遍历单词中的每个字母,咱们都在前缀树查找,若是当前字母对应的结点的表示isWord是true,咱们就返回这个前缀,若是当前字母对应的结点在前缀树中不存在,咱们就返回原单词,这样就能完美的解决问题了。因此啊,之后遇到了有关前缀或者相似的问题,必定不要忘了前缀树这个神器哟~ class Solution{ public: class TrieNode{ public: bool isWord; TrieNode *child[26]; // TrieNode(){}; TrieNode(){ isWord = false; for(auto &a : child) a = NULL; }; }; string replaceWords(vector<string>& dict, string sentence){ string res = "", t=""; istringstream is(sentence); TrieNode* root = new TrieNode(); for(string word: dict){ insert(root, word); } while(is >> t){ if(!res.empty()) res += " "; res += findPrefix(root, t); } return res; } void insert(TrieNode* node, string word){ for(char c: word){ if(!node->child[c-'a']) node->child[c-'a'] = new TrieNode(); node = node->child[c-'a']; } node->isWord = true; } string findPrefix(TrieNode* node, string word){ string cur = ""; for(char c: word){ if(!node->child[c-'a']) break; cur.push_back(c); node = node->child[c - 'a']; if(node->isWord) return cur; } return word; } };
设计一个支持如下两种操做的数据结构:
void addWord(word) bool search(word)
search(word) 能够搜索文字或正则表达式字符串,字符串只包含字母 .
或 a-z
。 .
能够表示任何一个字母。
示例:
addWord("bad") addWord("dad") addWord("mad") search("pad") -> false search("bad") -> true search(".ad") -> true search("b..") -> true
说明:
你能够假设全部单词都是由小写字母 a-z
组成的。
#include <iostream> #include <vector> using namespace std; class WordDictionary{ private: struct TrieNode{ bool isWord; vector<TrieNode*> children; TrieNode(): isWord(false), children(26, nullptr){} ~TrieNode(){ for(TrieNode* child: children) if(child) delete child; } }; TrieNode* trieRoot; bool myFind(string &str, TrieNode* nowPtr, int nowIndex){ int strSize = str.size(); if(nowPtr == NULL){ return false; } if(nowIndex >= strSize){ if(nowPtr->isWord){ return true; } return false; } else if(str[nowIndex] != '.'){ if(nowPtr->children[str[nowIndex] - 'a'] != NULL){ return myFind(str, nowPtr->children[str[nowIndex] - 'a'], nowIndex+1); } return false; } else{ for(int i=0; i<26; ++i){ if(nowPtr->children[i] != NULL && myFind(str, nowPtr->children[i], nowIndex+1 )){ return true; } } } return false; } public: WordDictionary(){ trieRoot = new TrieNode(); } void addWord(string word){ TrieNode * ptr = trieRoot; for(auto ch : word){ if(ptr->children[ch - 'a'] == NULL){ ptr->children[ch - 'a'] = new TrieNode(); } ptr = ptr->children[ch - 'a']; } ptr->isWord = true; } bool search(string word){ return myFind(word, trieRoot, 0); } };
给定一个非空数组,数组中元素为 a0, a1, a2, … , an-1,其中 0 ≤ ai < 2^31 。
找到 ai 和aj 最大的异或 (XOR) 运算结果,其中0 ≤ i, j < n 。
你能在O(n)的时间解决这个问题吗?
示例:
输入: [3, 10, 5, 25, 2, 8] 输出: 28 解释: 最大的结果是 5 ^ 25 = 28.
//https://blog.csdn.net/weijiechenlun133/article/details/70135937 class SolutionA { public: int findMaximumXOR(vector<int> &nums) { if (nums.size() < 2) return 0; int maxNum = 0; int flag = 0; for(int i = 31; i>=0; --i){ set<int> hash; flag |= (1 << i); for(int x:nums) hash.insert(flag & x); int tmp = maxNum | (1<<i); for(int x:hash){ if(hash.find(x^tmp)!=hash.end()){ maxNum = tmp; break; } } } return maxNum; } }; struct Node{ Node* next[2]; Node(){ next[0] = nullptr; next[1] = nullptr; } }; class SolutionB{ public: void buildTrieTree(Node* root, int x){ for(int i = 31; i>=0; --i){ int flag = (x & (1<<i) )? 1:0; if(root->next[flag] == nullptr){ root->next[flag] = new Node(); } root = root->next[flag]; } } int findMaxXorInTire(Node* root, int x){ int result = 0; for(int i = 31; i>=0; --i){ int flag = (x & (1<<i) )? 0:1; if(root->next[flag] != nullptr){ result |= (1<<i); //result = result | (1<<i) root = root->next[flag]; } else root = root->next[1-flag]; } return result; } int findMaximumXOR(vector<int>& nums){ if(nums.size()<2) return 0; Node head; for(int x : nums) buildTrieTree(&head, x); int maxNum = 0; for(int x: nums){ int m = findMaxXorInTire(&head, x); maxNum = max(maxNum, m); } return maxNum; } };
给定一个二维网格 board 和一个字典中的单词列表 words,找出全部同时在二维网格和字典中出现的单词。
单词必须按照字母顺序,经过相邻的单元格内的字母构成,其中“相邻”单元格是那些水平相邻或垂直相邻的单元格。同一个单元格内的字母在一个单词中不容许被重复使用。
示例:
输入: words = ["oath","pea","eat","rain"] and board = [ ['o','a','a','n'], ['e','t','a','e'], ['i','h','k','r'], ['i','f','l','v'] ] 输出: ["eat","oath"]
说明:
你能够假设全部输入都由小写字母 a-z
组成。
提示:
参考:https://blog.csdn.net/qq_41855420/article/details/88064909
#include <iostream> #include <vector> using namespace std; //前缀树的程序表示 class TrieNode { public: bool isWord;//当前节点为结尾是不是字符串 vector<TrieNode*> children; TrieNode() : isWord(false), children(26, nullptr) {} ~TrieNode() { for (TrieNode* child : children) if (child) delete child; } }; class Solution { private: TrieNode * trieRoot;//构建的单词前缀树 //在树中插入一个单词的方法实现 void addWord(string word) { TrieNode *ptr = trieRoot;//扫描这棵树,将word插入 //将word的字符逐个插入 for (auto ch : word) { if (ptr->children[ch - 'a'] == NULL) { ptr->children[ch - 'a'] = new TrieNode(); } ptr = ptr->children[ch - 'a']; } ptr->isWord = true;//标记为单词 } public: int rowSize;//board的行数 int colSize;//board的列数 vector<vector<bool>> boardFlag;//标记board[row][col]是否已使用 //以board[row][col]为中心点,四个方向进行尝试搜索 void dfs(vector<vector<char>>& board, vector<string> &result, string &tempRes, TrieNode * nowRoot, int row, int col) { if (nowRoot == NULL) { return; } if (nowRoot->isWord) {//若是这个单词成功找到 result.push_back(tempRes);//放入结果 nowRoot->isWord = false;//将这个单词标记为公共后缀 防止重复 } string tempResAdd; //上方测试 //若是上方未出界,没有被使用,且nowRoot->children中存在相等的节点 if (row - 1 >= 0 && !boardFlag[row - 1][col] && nowRoot->children[board[row - 1][col] - 'a'] != NULL) { boardFlag[row - 1][col] = true;//标记使用 tempResAdd = tempRes + char(board[row - 1][col]); dfs(board, result, tempResAdd, nowRoot->children[board[row - 1][col] - 'a'], row - 1, col); boardFlag[row - 1][col] = false;//取消标记 } //下方测试 //若是下方未出界,没有被使用,且nowRoot->children中存在相等的节点 if (row + 1 < rowSize && !boardFlag[row + 1][col] && nowRoot->children[board[row + 1][col] - 'a'] != NULL) { boardFlag[row + 1][col] = true;//标记使用 tempResAdd = tempRes + char(board[row + 1][col]); dfs(board, result, tempResAdd, nowRoot->children[board[row + 1][col] - 'a'], row + 1, col); boardFlag[row + 1][col] = false;//取消标记 } //左方测试 //若是左方未出界,没有被使用,且nowRoot->children中存在相等的节点 if (col - 1 >= 0 && !boardFlag[row][col - 1] && nowRoot->children[board[row][col - 1] - 'a'] != NULL) { boardFlag[row][col - 1] = true;//标记使用 tempResAdd = tempRes + char(board[row][col - 1]); dfs(board, result, tempResAdd, nowRoot->children[board[row][col - 1] - 'a'], row, col - 1); boardFlag[row][col - 1] = false;//取消标记 } //右方测试 //若是右方未出界,没有被使用,且nowRoot->children中存在相等的节点 if (col + 1 < colSize && !boardFlag[row][col + 1] && nowRoot->children[board[row][col + 1] - 'a'] != NULL) { boardFlag[row][col + 1] = true;//标记使用 tempResAdd = tempRes + char(board[row][col + 1]); dfs(board, result, tempResAdd, nowRoot->children[board[row][col + 1] - 'a'], row, col + 1); boardFlag[row][col + 1] = false;//取消标记 } } vector<string> findWords(vector<vector<char>>& board, vector<string>& words) { rowSize = board.size(); if (rowSize == 0) { return {}; } colSize = board[0].size(); boardFlag = vector<vector<bool>>(rowSize, vector<bool>(colSize, false));//构建标记容器 trieRoot = new TrieNode();//单词后缀树 //将单词都放入前缀树中 for (auto word : words) { addWord(word); } vector<string> result;//用于存储结果 string tempRes; for (int row = 0; row < rowSize; ++row) { for (int col = 0; col < colSize; ++col) { if (trieRoot->children[board[row][col] - 'a'] != NULL) {//搜索 tempRes = ""; tempRes += char(board[row][col]); boardFlag[row][col] = true;//标记使用 dfs(board, result, tempRes, trieRoot->children[board[row][col] - 'a'], row, col); boardFlag[row][col] = false;//取消使用 } } } return result; } };
给定一组惟一的单词, 找出全部不一样 的索引对(i, j)
,使得列表中的两个单词, words[i] + words[j]
,可拼接成回文串。
示例 1:
输入: ["abcd","dcba","lls","s","sssll"] 输出: [[0,1],[1,0],[3,2],[2,4]] 解释: 可拼接成的回文串为 ["dcbaabcd","abcddcba","slls","llssssll"]
示例 2:
输入: ["bat","tab","cat"] 输出: [[0,1],[1,0]] 解释: 可拼接成的回文串为 ["battab","tabbat"]
大多数解法都是基于hash表,看着很复杂,我找到一个可读性比较高的版本,以后还得拿出来温习。
#include <iostream> #include <vector> #include <bits/stdc++.h> #include <string> using namespace std; class Solution{ public: bool isPalindrome(string& s, int start, int end){ while(start < end) if(s[start++] != s[end--]) return false; return true; } vector<vector<int>> palindromePairs(vector<string> words){ vector<vector<int>> ans; unordered_map<string, int> dict; int len = words.size(); for(int i=0; i<len; i++) dict[words[i]] = i; for(int i=0; i<len; i++){ string cur = words[i]; int clen = cur.size(); for(int j=0; j<=clen; j++){ //找后缀 if(isPalindrome(cur, j, clen - 1)){ string suffix = cur.substr(0, j); reverse(suffix.begin(), suffix.end()); if(dict.find(suffix)!=dict.end() && i!=dict[suffix]) ans.push_back({i, dict[suffix]}); } //找前缀 if(j>0 && isPalindrome(cur, 0, j-1)){ string prefix = cur.substr(j); reverse(prefix.begin(), prefix.end()); if(dict.find(prefix) != dict.end() && i!=dict[prefix]) ans.push_back({dict[prefix], i}); } } } return ans; } }; int main(){ vector<string> a = {"lls", "s", "sssll"}; Solution s = Solution(); vector<vector<int>> v = s.palindromePairs(a); };