c++ 使用helper函数为数据结构(如try和链表)构建节点的目的是什么?

ibps3vxo  于 11个月前  发布在  其他
关注(0)|答案(1)|浏览(97)

我一直在做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;
    }


这两种实现方式似乎工作和执行相同的方式。那么为什么第一种比第二种更受欢迎呢?

8ljdwjyq

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_ptrstd::array避开了整个清理问题。我不仅避免了编写析构函数的任务,还完全避免了编写任何类型的“构建”或“拆除”代码的需要。我避开了一大堆容易出错的繁琐工作!
我的代码遵循Wikipedia上的示例,因此它使用名称is_terminal而不是isWord。除此之外,我们的insert例程基本上是相同的。
我的代码增加了一个有效字符的检查,但这可能是一个缺陷,因为规范承诺word中的所有字符都是小写字母。我的双重检查只会拖慢速度。如果我能证明Trie类的所有客户端都遵守了先决条件,我会删除额外的检查。
作为练习,我添加了一个erase函数和一个流插入操作符。函数erase与Wikipedia上的函数有很大不同。当节点不再使用时,它会更积极地释放节点。operator<<输出存储在Trie中的键列表。在测试类Trie时,它很有帮助。

// StackOverflow_77491953_Trie_Answer.ixx
// What is the purpose of using a helper function to build nodes for 
// data structures like tries and linked lists?
// https://stackoverflow.com/q/77491953
// https://stackoverflow.com/a/77505923
// 
// LeetCode 208. Implement Trie (Prefix Tree)
// https://leetcode.com/problems/implement-trie-prefix-tree/
//
// Wikipedia
// https://en.wikipedia.org/wiki/Trie#Operations
// 

export module main;
import std;

// This using declaration allows the LeetCode `Trie` to be used 
// without modifying its function signatures.
using std::string;

class Trie
{
    // This implementation anticipates that the letters 'a' to 'z' 
    // have consectutive code points in the character set used 
    // by std::string. 
    // 
    // When such is not true, it will still work, albeit, with a 
    // lot of wasted storage, so long as 'a' precedes all other 
    // lower-case letters, and `z` comes after all others.
    struct Node
    {
        enum : std::size_t { alphabet_size = 'z' - 'a' + 1u };
        std::array<std::unique_ptr<Node>, alphabet_size> child;
        bool is_terminal{ false };
        bool is_leaf_node()
        {
            for (auto const& c : child) if (c) return false;
            return true;
        }
        std::size_t static subscript(char const ch)
        {
            auto const i{ std::tolower(static_cast<unsigned char>(ch)) };
            if (!std::isalpha(i))
                throw std::runtime_error(std::format(
                    "ERROR: Trie character is not alphabetic: '{}'", ch));
            return static_cast<std::size_t>(i - 'a');
        }
    };
    std::unique_ptr<Node> root;
public:
    Trie()
        : root{ std::make_unique<Node>() }
    {}
    bool contains(std::string_view key) const noexcept
    {
        Node* p{ root.get() };
        for (auto const& ch : key)
        {
            auto const i{ Node::subscript(ch) };
            if (!p->child[i])
                return false;
            p = p->child[i].get();
        }
        return p->is_terminal;
    }
    bool contains_prefix(std::string_view prefix) const noexcept
    {
        Node* p{ root.get() };
        for (auto const& ch : prefix)
        {
            auto const i{ Node::subscript(ch) };
            if (!p->child[i])
                return false;
            p = p->child[i].get();
        }
        return true;
    }
    void erase(std::string_view key)
    {
        erase(key, root.get());
    }
private:
    bool erase(std::string_view key, Node* const p)
    {
        if (key.empty())
        {
            // found key
            if (!p->is_terminal)
                return false;        // key is not in trie
            p->is_terminal = false;  // erase key
        }
        else
        {
            auto const i{ Node::subscript(key.front()) };
            if (!p->child[i])
                return false;  // key is not in trie
            if (erase(key.substr(1u), p->child[i].get()))
                p->child[i] = nullptr;  // deallocate non-terminal leaf nodes
            if (p->is_terminal)
                return false;  // partial key is also a key
        }
        return p->is_leaf_node();  // deallocate non-terminal leaf nodes
    }
public:
    void insert(string word)
    {
        Node* p{ root.get() };
        for (auto const& ch : word)
        {
            auto const i{ Node::subscript(ch) };
            if (!p->child[i])
                p->child[i] = std::make_unique<Node>();
            p = p->child[i].get();
        }
        p->is_terminal = true;
    }
    bool search(string word)
    {
        return contains(word);
    }
    bool startsWith(string prefix)
    {
        return contains_prefix(prefix);
    }
    friend auto operator<< (std::ostream& ost, Trie const& trie)
        -> std::ostream&
    {
        std::string key;
        trie.put(ost, key, trie.root.get());
        return ost;
    }
private:
    void put(
        std::ostream& ost,
        std::string key,
        Node* const p) const
    {
        // Precondition: On entry, `p != nullptr`.
        if (p->is_terminal)
            ost << "{ key: \"" << key << "\" }\n";
        for (char ch{ 'a' }; ch <= 'z'; ++ch)
        {
            auto const i{ Node::subscript(ch) };
            if (p->child[i])
                put(ost, key + ch, p->child[i].get());
        }
    }
};

void LeetCode_0208_Implement_Trie_Prefix_Tree() {
    std::cout << std::boolalpha
        << "LeetCode 208. Implement Trie (Prefix Tree)"
        "\nhttps://leetcode.com/problems/implement-trie-prefix-tree/"
        "\n"
        "\nExample 1"
        "\n\n";
    Trie trie;
    std::cout << "Trie contents after construction\n" << trie << "\n"
        << "   trie.search(\"apple\") returns " << trie.search("apple") << "\n"
        << "   trie.search(\"app\") returns " << trie.search("app") << "\n"
        << "   trie.startsWith(\"app\") returns " << trie.startsWith("app") << "\n\n";
    trie.insert("apple");
    std::cout
        << "trie contents after `trie.insert(\"apple\")`\n" << trie << "\n"
        << "   trie.search(\"apple\") returns " << trie.search("apple") << "\n"
        << "   trie.search(\"app\") returns " << trie.search("app") << "\n"
        << "   trie.startsWith(\"app\") returns " << trie.startsWith("app") << "\n\n";
    trie.insert("app");
    std::cout
        << "trie contents after `trie.insert(\"app\")`\n" << trie << "\n"
        << "   trie.search(\"app\") returns " << trie.search("app") << "\n"
        << "   trie.startsWith(\"app\") returns " << trie.startsWith("app") << "\n\n";
    trie.erase("app");
    std::cout
        << "trie contents after `trie.erase(\"app\")`\n" << trie << "\n"
        << "   trie.search(\"app\") returns " << trie.search("app") << "\n"
        << "   trie.startsWith(\"app\") returns " << trie.startsWith("app") << "\n\n";
    trie.erase("apple");
    std::cout
        << "trie contents after `trie.erase(\"apple\")`\n" << trie << "\n"
        << "   trie.search(\"apple\") returns " << trie.search("apple") << "\n"
        << "   trie.startsWith(\"a\") returns " << trie.startsWith("a") << "\n\n";
}
export int main()
{
    LeetCode_0208_Implement_Trie_Prefix_Tree();
    return 0;
}
// end file: StackOverflow_77491953_Trie_Answer.ixx

字符串
这里是输出。

LeetCode 208. Implement Trie (Prefix Tree)
https://leetcode.com/problems/implement-trie-prefix-tree/

Example 1

Trie contents after construction

   trie.search("apple") returns false
   trie.search("app") returns false
   trie.startsWith("app") returns false

trie contents after `trie.insert("apple")`
{ key: "apple" }

   trie.search("apple") returns true
   trie.search("app") returns false
   trie.startsWith("app") returns true

trie contents after `trie.insert("app")`
{ key: "app" }
{ key: "apple" }

   trie.search("app") returns true
   trie.startsWith("app") returns true

trie contents after `trie.erase("app")`
{ key: "apple" }

   trie.search("app") returns false
   trie.startsWith("app") returns true

trie contents after `trie.erase("apple")`

   trie.search("apple") returns false
   trie.startsWith("a") returns false

相关问题