我一直在做Leetcode问题,试图提高我的编码技能,当我做这个问题https://leetcode.com/problems/implement-trie-prefix-tree/时,我对这个问题感到好奇。
我在网上找到的大多数答案都是通过创建一个节点结构体,然后使用一个构建方法来示例化节点并用默认值填充它来实现trie。但我想知道为什么这是必要的,他们不只是用默认值定义节点?我试过了,没有构建方法的情况下,它的工作原理似乎完全一样,所以我不得不假设这是某种最佳实践的原因。为什么构建方法更适合于更复杂的节点结构,但是,我真正感兴趣的是我所做的事情可能会出错,因为我还没有找到任何结论性的答案。
典型解决方案:
class Trie {
private:
struct Node {
bool isWord = false;
Node* children[26];
};
Node* buildNode(){// helper method to build the default node
Node* root = new Node();
root-> isWord = false;
for (int i = 0; i<26; i++){
root->children[i] = NULL;
}
return root;
}
Node* root;
public:
Trie() {
root = buildNode();
}
void insert(string word) {
Node* curr = root;
for (char ch : word){
int index = ch - 'a'; // convert char to int 0-25
if (!curr->children[index]){//if NULL
curr->children[index] = buildNode();
}
curr = curr->children[index];
}
curr->isWord = true;
}
字符串
我的解决方案:
class Trie {
private:
struct Node {
bool isWord = false;
Node* children[26] = {};
};
Node* root;
public:
Trie() {
root = new Node();
}
void insert(string word) {
Node* curr = root;
for (char ch : word){
int index = ch - 'a'; // convert char to int 0-25
if (!curr->children[index]){//if NULL
curr->children[index] = new Node();
}
curr = curr->children[index];
}
curr->isWord = true;
}
型
这两种实现方式似乎工作和执行相同的方式。那么为什么第一种比第二种更受欢迎呢?
1条答案
按热度按时间8ljdwjyq1#
LeetCode有问题
正如在评论中提到的,你的代码完成了任务。我希望它通过所有LeetCode测试。
但是它肯定没有通过我的!虽然我更喜欢你的解决方案,而不是一个单独的“构建”例程,如果你把你的代码提交给面试官,你可能会遇到麻烦。不幸的是,整个事情是一个巨大的内存泄漏。如果你想使用操作符
new
和原始指针,你有责任写一个适当的析构函数,当你完成时清理。事实上,LeetCode允许你跳过这一点,这让人们说:
.因为leetcode不教C和良好的C实践。
大多数leetcode解决方案(不幸的是,包括你的)永远不会在一个像样的工程环境中投入生产。
学习语言时避免竞争性的编码网站。许多(大多数)促进可怕的基本编码实践...
即使你提供了一个析构函数,我认为你的解决方案仍然应该被LeetCode拒绝,因为它没有使用标准库中的智能指针。在生产环境中,类
Trie
几乎肯定会使用std::unique_ptr
编码。LeetCode并没有要求程序员编写一个函数来擦除
Trie
中的键,这会带来删除节点的问题。使用智能指针,这并不太困难(见下文),但是使用原始指针,你会发现自己编写了某种类似于OP中引用的“build”函数的“tear-down”例程。因此,在原始指针实现中缺少的不仅仅是析构函数,还有释放
Node
的“tear-down”函数。好消息是,一旦你有了一个tear-down例程,你就可以从析构函数和erase
函数中调用它作为助手!使用标准库工具
当我解决这个问题时,我通过使用
std::unique_ptr
和std::array
避开了整个清理问题。我不仅避免了编写析构函数的任务,还完全避免了编写任何类型的“构建”或“拆除”代码的需要。我避开了一大堆容易出错的繁琐工作!我的代码遵循Wikipedia上的示例,因此它使用名称
is_terminal
而不是isWord
。除此之外,我们的insert
例程基本上是相同的。我的代码增加了一个有效字符的检查,但这可能是一个缺陷,因为规范承诺
word
中的所有字符都是小写字母。我的双重检查只会拖慢速度。如果我能证明Trie
类的所有客户端都遵守了先决条件,我会删除额外的检查。作为练习,我添加了一个
erase
函数和一个流插入操作符。函数erase
与Wikipedia上的函数有很大不同。当节点不再使用时,它会更积极地释放节点。operator<<
输出存储在Trie
中的键列表。在测试类Trie
时,它很有帮助。字符串
这里是输出。
型