解法一:哈希表实现(暴力解法)
题目要求实现字符串的「插入」、「查找」、「查找前缀」等功能,一个直观的想法是利用「以空间换时间」的数据结构:哈希表。哈希表在「查找」操作上的时间复杂度为,可以作为此题的解法。
利用哈希表实现的思路如下:
定义哈希表hash,其保存的键值对为,即以「单词」作为key,以「该单词出现的次数」作为value。
对于插入操作:首先判断该单词是否存在在哈希表中,若存在,将其value加1;若不存在,将其插入哈希表中,并将value置为1。
对于删除操作:由于被删除的单词一定存在于哈希表中(题目明确说明),故直接将该单词的value减1即可。
对于查找操作:直接在哈希表中查找该单词,若该单词存在在哈希表的key中、且value大于0,说明查找成功,否则查找失败。
对于查找前缀操作:查找前缀利用到c++的库函数,该函数在字符串中查找字符串,若找到,返回其起始下标。因此可以通过判断该函数的返回值是否为0(即从第一个位置开始)来判断是否存在前缀。
基于上述思路,实现的代码如下:(注意:此方法超出时间限制)
class Solution { public: /** * * @param operators string字符串vector<vector<>> the ops * @return string字符串vector */ unordered_map <string, int> hash; // 定义哈希表 void insert(string word) { if (hash.find(word) != hash.end()) // 单词存在在哈希表中 hash[word] += 1; else // 单词不存在在哈希表中 hash[word] = 1; return; } void remove(string word) { hash[word]--; // 直接将该单词的value减1 return; } bool search(string word) { if (hash.find(word) != hash.end()) // 单词存在在哈希表中 return hash[word] != 0; // 若该单词的value不为0,说明存在 return false; } int searchPrefix(string word) { int res = 0; for (auto it = hash.begin(); it != hash.end(); it++) { // 遍历哈希表 string hashKey = it->first; if (hashKey.find(word) == 0) // 利用find函数查找前缀 res += it->second; } return res; } vector<string> trieU(vector<vector<string> >& operators) { vector<string> res; for (int i = 0; i < operators.size(); i++) { string op = operators[i][0], word = operators[i][1]; if (op == "1") { // 插入 insert(word); } else if (op == "2") { // 删除 remove(word); } else if (op == "3") { // 查找 int searchRes = search(word); if (searchRes) res.push_back("YES"); else res.push_back("NO"); } else if (op == "4") { // 查找前缀 int searchPrefixRes = searchPrefix(word); res.push_back(to_string(searchPrefixRes)); } } return res; } };
时间复杂度:对于插入操作,哈希表首先查找该单词是否存在(时间复杂度为),随后改变其value(时间复杂度为); 对于查找操作,哈希表查找的时间复杂度为;对于删除操作,仅是将该单词的value减1,故时间复杂度为;对于查找前缀操作,需要遍历前缀,设前缀的长度为,则时间复杂度为。
空间复杂度:哈希表为每个单词维护一个键值对,故空间复杂度为,其中为单词长度。
解法二:TrieNode实现
解法一利用哈希表需要「为每个单词单独维护一个键值对」,较为复杂。此题还可以通过定义树实现。该树的每个结点包含:一个长度为26的数组(数组的每一个元素为指向类型的指针)、一个int类型变量(统计当前字母被访问了多少次,为方便实现删除操作和查找前缀操作)、一个bool类型变量(用来表示当前结点是否是某一个单词的结尾),如下所示。
struct TrieNode { bool isEnd; // 用来表示当前结点是否是单词末尾 int cnt; // 统计当前字母被访问了多少次 TrieNode* next[26]; TrieNode() { cnt = 0; // 初始化为0 isEnd = false; // 初始化为false for (int i = 0; i < 26; i ++) { next[i] = NULL; // 初始化为NULL } } };
注意:在树的每个结点中,并不「实际储存某个字母」,而是记录「该字母所对应的索引」,具体如下图所示。
下图展示「插入」和「查找」的过程:
对于插入操作:
- 若待插入的字母已经存在在某个结点中了,则将其变量加1,表示「多一次访问」;
- 反之,则为「该字母所对应的数组元素(也就是指针)」一个空间,即:该位置的指针不再为。
- 在插入到「最后一个字母」时,将该结点的标志置位,代表这是最后一个字符。
对于删除操作:
- 由于所删除的字母一定事先存在于树中,故可以简单地通过改变变量的取值来实现:即将变量减1,代表「访问该结点的次数减少一次」;
- 若某个结点的变量为0,代表该字母全部删除。
对于查找操作:
- 查找过程即遍历整个树,若「结点指针为空」或「结点的变量为0」,则说明该字母不存在,直接返回;
- 遍历完整个单词后到达最后一个结点,若该结点的变量为,说明「是某一个单词的结尾」,此时返回,否则返回。
对于查找前缀操作:
- 查找前缀过程即遍历整个树,若「结点指针为空」或「结点的变量为0」,则说明该字母不存在,直接返回;
- 在遍历过程中,记录所有结点的变量的最小值,即为「共同前缀」。
根据上述代码,实现的思路如下:
struct TrieNode { bool isEnd; // 用来表示当前结点是否是单词末尾 int cnt; // 统计当前字母被访问了多少次 TrieNode* next[26]; TrieNode() { cnt = 0; // 初始化为0 isEnd = false; // 初始化为false for (int i = 0; i < 26; i ++) { next[i] = NULL; // 初始化为NULL } } }; class Solution { public: /** * * @param operators string字符串vector<vector<>> the ops * @return string字符串vector */ TrieNode* root = new TrieNode(); void insert(string str) { TrieNode *p = root; for (int i = 0; i < str.size(); i ++) { int idx = str[i] - 'a'; if (!p->next[idx]) // 结点指针为空 p->next[idx] = new TrieNode(); p->next[idx]->cnt ++; // cnt变量加1 p = p->next[idx]; // 向下移动 } p->isEnd = true; // 单词的末尾 } bool search(string str) { TrieNode *p = root; for (int i = 0; i < str.size(); i ++) { int idx = str[i] - 'a'; if (!p->next[idx] || !p->next[idx]->cnt) // 若指针为空,或者cnt为0 return false; p = p->next[idx]; // 向下移动 } return p->isEnd; // 是否为某个单词的末尾字母 } int searchPrefix(string str) { TrieNode *p = root; int res = INT_MAX; for (int i = 0; i < str.size(); i ++) { int idx = str[i] - 'a'; if (!p->next[idx] || !p->next[idx]->cnt) // 指针为空,或cnt为0 return 0; res = min(res, p->next[idx]->cnt); // 记录cnt最小值 p = p->next[idx]; } return res; } void remove(string str) { TrieNode *p = root; for (int i = 0; i < str.size(); i ++) { int idx = str[i] - 'a'; p->next[idx]->cnt --; // cnt变量减1 p = p->next[idx]; } } vector<string> trieU(vector<vector<string> >& operators) { vector<string> res; for (int i = 0; i < operators.size(); i ++) { string op = operators[i][0], str = operators[i][1]; if (op == "1") { // 插入 insert(str); } else if (op == "2") { // 删除 remove(str); } else if (op == "3") { // 查找 bool searchRes = search(str); if (searchRes) res.push_back("YES"); else res.push_back("NO"); } else if (op == "4") { // 查找前缀 int prefixNum = searchPrefix(str); res.push_back(to_string(prefixNum)); } } return res; } };
时间复杂度:在插入、删除、查找等操作时需要遍历整个单词,故时间复杂度为,其中为单词长度;
空间复杂度:设树的结点个数为,组成所有单词的字母个数为(此题为26个字母),则每个树结点需要的空间,因此整个空间复杂度为。