Adding a meaning marks the end of word. You don't need an extra boolean value.
Also, the root node has no associated character or meaning. All words begin with children of the root.
I have left this thread alone a few times without expressing the disquiet deep, deep in my soul for seeing a C++ container used with
manual memory management. It hurts.
Especially as an
unordered_map
automagically does it for you. Whomever wrote that original piece of code is actually going out of his|her way to avoid letting C++ do things for you.
Here is how I would do it. Pretty sure this is C++11, but the oldest I could check it against was C++14. You should be compiling against the latest standard you can anyway.
1 2 3 4 5 6 7 8 9 10 11 12 13 14
|
#include <string>
#include <unordered_map>
#include <vector>
using Meaning = std::string; // I would personally use a more interesting structure...
// but for this example simple strings will do.
struct Words
{
using Children = std::unordered_map <char, Words> ; // (behold, automatic self-referential memory management!)
using Meanings = std::vector <Meaning> ; // (because words often have multiple meanings)
Children children;
Meanings meanings;
};
| |
As a matter of The Stupid™ making life stupid, the GNU Standard Library has a bug in this regard. If you are compiling against it (for example, if you are compiling on Linux) then it won’t work. Either use
std::map
or use Boost’s implementation.
1 2 3
|
#include <map>
...
using Children = std::map <char, Words> ;
| |
1 2 3
|
#include <boost/unordered_map.hpp>
...
using Children = boost::unordered_map <char, Words> ;
| |
Maybe someday they’ll fix it. Anyhow...
This tree structure is called an "n-ary tree", meaning that each node may have any number of children, including zero.
A node with zero children is a "leaf", of course, which clearly signifies the end of a word. But as already said in this thread, a word also may end somewhere
other than a leaf node.
Your desire to add information about the meaning of a word serves a good purpose: the presence of associated meaning(s) also clearly signifies the end of a word.
With those preliminaries out of the way, let us revisit the
Insert
function and fix a couple things:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
|
void Insert( Words & words, const std::string & word, const Words::Meanings & meanings )
{
// Seek (add, if necessary) nodes for each letter in the word.
// In the end, node will point to the final letter in the word
// (which may or may not be a leaf node).
Words * node = &words;
for (auto c : word)
{
Words & parent = *node; // the current node is a parent node
node = &(parent.children[ c ]); // (or if it isn't already then make it one)
}
// Now that we have our 'last letter in the word' node, add the
// word's meanings. Again C++ makes life really, really easy here.
node->meanings = meanings;
}
| |
That parent node bit makes things a little easier to grok, but it could have just as easily been written as:
1 2
|
for (auto c : word)
node = &(node->children[ c ]);
| |
It compiles to exactly the same thing. The important part is the
std::unordered_map::operator []
, which will automatically
insert a new node if the argument index does not already exist. Convenient, since that is exactly the behavior we want for the Insert function.
Finding a word is similar, but with one significant difference:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26
|
const Words::Meanings & FindWord( Words & words, const std::string & word )
{
// If the 'word' is not in our 'words', then we have no meanings to return.
// We cannot return a null reference, so here is our empty list of meanings:
static Words::Meanings no_meanings;
// Finding the last node in the word is similar to before, except now
// we need to be a little more careful not to add a node if it isn't
// already there.
//
// The original code made the find and add separate actions, but all that
// did was make the lookup happen _twice_. Instead we will just lookup once.
//
Words * node = &words;
for (auto c : word)
{
auto child = node->children.find( c );
if (child == node->children.end())
return no_meanings;
node = &(child->second); // (this is how std::unordered_map iterators work)
}
// If we get this far then we can return the word's list of meanings.
// (If it is a word, it will have a list of meanings. If it is NOT a word,
// it will have an empty list of meanings. Get it? Life is easy.)
return node->meanings;
}
| |
While I am making a bit of a stink about performing two lookups, in the case of
unordered_map
it doesn’t actually matter much. A lookup is an O(1) (constant time) operation.
Let us put it all to the test!
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51
|
#include <iostream>
int main()
{
Words words;
Insert( words, "hello",
{
"excl. A greeting, usually spoken.",
"n. Reference to such a greeting.",
} );
Insert( words, "hell",
{
"n. Spiritual suffering, or a place where such suffering occurs.",
"n. The place where wicket souls go to suffer after death (religious).",
"excl. Utterance used to express annoyance, surprise, and/or emphasis.",
} );
Insert( words, "jello",
{
"n. a fruit-flavored gelatin dessert made from a commercially prepared powder. See \"Jell-O\"."
} );
Insert( words, "Jell-O",
{
" ...is delicious. Eat some:\n"
" 1 pkg Green Jell-O\n"
" 1 cup cottage cheese\n"
" 8 oz crushed pineapple\n"
" Make the jello as per instructions.\n"
" Refrigerate for 30 minutes to 1 hour.\n"
" Stir in cottage cheese and pineapple.\n"
" Refrigerate until fully set.\n"
" Serve on a lettuce leaf."
} );
auto PrintMeanings = [&words]( auto word )
{
auto meanings = FindWord( words, word );
if (meanings.empty())
std::cout << "\n\"" << word << "\" is not a word in my vocabulary.\n";
else
{
std::cout << "\n" << word << ":\n";
for (auto meaning : meanings)
std::cout << " " << meaning << "\n";
}
};
PrintMeanings( "hello" );
PrintMeanings( "hell" );
PrintMeanings( "jello" );
PrintMeanings( "jell" );
PrintMeanings( "Jell-O" );
}
| |
Enjoy!