Beispiel #1
0
 def test_child_methods(self):
     # Create node 'A' and verify it does not have any children
     node_A = PrefixTreeNode('A')
     assert node_A.num_children() == 0
     assert node_A.has_child('B') is False
     # Verify getting child from node 'A' raises error
     with self.assertRaises(ValueError):
         node_A.get_child('B')
     # Create node 'B' and add it as child to node 'A'
     node_B = PrefixTreeNode('B')
     node_A.add_child('B', node_B)
     # Verify node 'A' has node 'B' as child
     assert node_A.num_children() == 1
     assert node_A.has_child('B') is True
     assert node_A.get_child('B') is node_B
     # Verify adding node 'B' as child to node 'A' again raises error
     with self.assertRaises(ValueError):
         node_A.add_child('B', node_B)
     # Create node 'C' and add it as another child to node 'A'
     node_C = PrefixTreeNode('C')
     node_A.add_child('C', node_C)
     # Verify node 'A' has both nodes 'B' and 'C' as children
     assert node_A.num_children() == 2
     assert node_A.has_child('B') is True
     assert node_A.has_child('C') is True
     assert node_A.get_child('C') is node_C
     # Verify adding node 'C' as child to node 'A' again raises error
     with self.assertRaises(ValueError):
         node_A.add_child('C', node_C)
Beispiel #2
0
 def test_init_and_properties(self):
     character = 'a'
     node = PrefixTreeNode(character)
     # Verify node character
     assert isinstance(node.character, str)
     print(node.character)
     assert node.character == 'a'
     # Verify children nodes structure
     assert isinstance(node.children, PrefixTreeNode.CHILDREN_TYPE)
     assert len(node.children) == 26
     assert node.children == [None] * 26
     assert node.num_children() == 0
     # Verify terminal boolean
     assert node.terminal is False
     assert node.is_terminal() is False
class PrefixTree:
    """PrefixTree: A multi-way prefix tree that stores strings with efficient
    methods to insert a string into the tree, check if it contains a matching
    string, and retrieve all strings that start with a given prefix string.
    Time complexity of these methods depends only on the number of strings
    retrieved and their maximum length (size and height of subtree searched),
    but is independent of the number of strings stored in the prefix tree, as
    its height depends only on the length of the longest string stored in it.
    This makes a prefix tree effective for spell-checking and autocompletion.
    Each string is stored as a sequence of characters along a path from the
    tree's root node to a terminal node that marks the end of the string."""

    # Constant for the start character stored in the prefix tree's root node
    START_CHARACTER = ''

    def __init__(self, strings=None):
        """Initialize this prefix tree and insert the given strings, if any."""
        # Create a new root node with the start character
        self.root = PrefixTreeNode(PrefixTree.START_CHARACTER)
        # Count the number of strings inserted into the tree
        self.size = 0
        # Insert each string, if any were given
        if strings is not None:
            for string in strings:
                self.insert(string)

    def __repr__(self):
        """Return a string representation of this prefix tree."""
        return f'PrefixTree({self.strings()!r})'

    def is_empty(self):
        """Return True if this prefix tree is empty (contains no strings)."""
        return self.root.num_children() == 0

    def contains(self, string):
        """Return True if this prefix tree contains the given string."""
        node = _find_node(string)

        if node not None:
           return True

        return False
Beispiel #4
0
class PrefixTree:
    """PrefixTree: A multi-way prefix tree that stores strings with efficient
    methods to insert a string into the tree, check if it contains a matching
    string, and retrieve all strings that start with a given prefix string.
    Time complexity of these methods depends only on the number of strings
    retrieved and their maximum length (size and height of subtree searched),
    but is independent of the number of strings stored in the prefix tree, as
    its height depends only on the length of the longest string stored in it.
    This makes a prefix tree effective for spell-checking and autocompletion.
    Each string is stored as a sequence of characters along a path from the
    tree's root node to a terminal node that marks the end of the string."""

    # Constant for the start character stored in the prefix tree's root node
    START_CHARACTER = ''

    def __init__(self, strings=None):
        """Initialize this prefix tree and insert the given strings, if any."""
        # Create a new root node with the start character
        self.root = PrefixTreeNode(PrefixTree.START_CHARACTER)
        # Count the number of strings inserted into the tree
        self.size = 0
        # Insert each string, if any were given
        if strings is not None:
            for string in strings:
                self.insert(string)

    def __repr__(self):
        """Return a string representation of this prefix tree."""
        return f'PrefixTree({self.strings()!r})'

    def is_empty(self):
        """Return True if this prefix tree is empty (contains no strings)."""
        return self.root.num_children() == 0

    def contains(self, string):
        """Return True if this prefix tree contains the given string."""
        if len(string) == 0:
            return True

        node = self.root
        for character in string.lower():
            index = ord(character) - 97

            if node.children[index] is not None:
                node = node.children[index]
            else:
                return False
        return node.terminal

    def insert(self, string):
        """Insert the given string into this prefix tree."""

        node = self.root

        for character in string.lower():
            if character == '-':
                continue

            index = ord(character) - 97
            if node.children[index] is None:
                node.children[index] = PrefixTreeNode(character)
                node = node.children[index]
            else:
                node = node.children[index]

        if not node.terminal:
            node.terminal = True
            self.size += 1

    def _find_node(self, string):
        """Return a pair containing the deepest node in this prefix tree that
        matches the longest prefix of the given string and the node's depth.
        The depth returned is equal to the number of prefix characters matched.
        Search is done iteratively with a loop starting from the root node."""
        # Match the empty string
        if len(string) == 0:
            return self.root, 0
        # Start with the root node
        node = self.root
        depth = 0
        for character in string.lower():
            if character == '-':
                continue

            index = ord(character) - 97

            if node.children[index] is not None:
                node = node.children[index]
                depth += 1
            else:
                return None, depth

        return node, depth

    def complete(self, prefix):
        """Return a list of all strings stored in this prefix tree that start
        with the given prefix string."""
        # Create a list of completions in prefix tree

        node = self._find_node(prefix)[0]

        if node is None:
            return []

        completions = []

        if node.terminal:
            completions.append(prefix)

        self._traverse(node, prefix, completions.append)
        return completions

    def strings(self):
        """Return a list of all strings stored in this prefix tree."""
        # Create a list of all strings in prefix tree
        all_strings = []
        self._traverse(self.root, PrefixTree.START_CHARACTER,
                       all_strings.append)
        return all_strings

    def _traverse(self, node, prefix, visit):
        """Traverse this prefix tree with recursive depth-first traversal. (pre-order)
        Start at the given node and visit each node with the given function."""

        for child in node.children:
            if child is not None:
                if child.terminal:
                    visit(prefix + child.character)

                self._traverse(child, prefix + child.character, visit)
Beispiel #5
0
class PrefixTree:
    """PrefixTree: A multi-way prefix tree that stores strings with efficient
    methods to insert a string into the tree, check if it contains a matching
    string, and retrieve all strings that start with a given prefix string.
    Time complexity of these methods depends only on the number of strings
    retrieved and their maximum length (size and height of subtree searched),
    but is independent of the number of strings stored in the prefix tree, as
    its height depends only on the length of the longest string stored in it.
    This makes a prefix tree effective for spell-checking and autocompletion.
    Each string is stored as a sequence of characters along a path from the
    tree's root node to a terminal node that marks the end of the string."""

    # Constant for the start character stored in the prefix tree's root node
    START_CHARACTER = '^'
    END_CHARACTER = '$'

    def __init__(self, strings=None):
        """Initialize this prefix tree and insert the given strings, if any."""
        # Create a new root node with the start character
        self.root = PrefixTreeNode(PrefixTree.START_CHARACTER)
        # Count the number of strings inserted into the tree
        self.size = 0
        # Insert terminal character
        self.insert('')
        self.size -= 1
        # Insert each string, if any were given
        if strings is not None:
            for string in strings:
                self.insert(string)

    def __repr__(self):
        """Return a string representation of this prefix tree."""
        return f'PrefixTree({self.strings()!r})'

    def is_empty(self):
        """Return True if this prefix tree is empty (contains no strings).
        Time: Θ(1) | Space: Θ(1)
        """
        return self.root.num_children() == 1

    def contains(self, string):
        """Return True if this prefix tree contains the given string.
        Time: O(kn) n = # of strings (width of tree), k = len(string)
        Space: Θ(1)
        """
        cur_node = self.root

        for char in string + '$':
            try:
                cur_node = cur_node.get_child(char)  # Update cur_node
            except ValueError:
                ## char wasn't found, so the exact string doesn't exist
                return False
        return True

    def insert(self, string):
        """Insert the given string into this prefix tree.
        Time: O(kn) n = # of strings (width of tree), k = len(string)
        Space: Θ(1)
        """

        cur_node = self.root

        for char in string:
            try:
                ## Try to update cur_node with existing child for char
                cur_node = cur_node.get_child(char)
            except ValueError:
                ## Child for char doesn't exist, so add new child then update
                ## cur_child
                new_node = PrefixTreeNode(char)
                cur_node.add_child(char, new_node)
                cur_node = new_node

        try:
            ## Check if terminate character exists
            cur_node.get_child('$')
        except ValueError:
            ## Terminate character ($) wasn't found, so this must be a new
            ## string
            cur_node.add_child('$', PrefixTreeNode('$'))  # Terminal node
            self.size += 1  # Increment because a new string has been inserted

    def _find_node(self, string):
        """Return a pair containing the deepest node in this prefix tree that
        matches the longest prefix of the given string and the node's depth.
        The depth returned is equal to the number of prefix characters matched.
        Search is done iteratively with a loop starting from the root node.
        Time: O(kn) n = # of strings (width of tree), k = len(string)
        Space: Θ(1)
        """
        # Start with the root node
        node = self.root
        depth = 0

        for char in string:
            try:
                node = node.get_child(char)  # Update node with child
                depth += 1
            except ValueError:
                ## Child node wasn't found. End of prefix has been reached
                break
        return node, depth

    def complete(self, prefix):
        """Return a list of all strings stored in this prefix tree that start
        with the given prefix string.
        Time: O(n) | Space: O(n)"""
        # Create a list of completions in prefix tree
        completions = []
        ## BFS Search for all completions (maintain relative order of nodes)
        node, depth = self._find_node(prefix)

        if depth < len(prefix):
            return []
        q = deque([(prefix, node)])

        while len(q):
            cur_string, node = q.pop()

            for child in node.children:

                if child.character == '$':
                    ## End of string found
                    completions.append(cur_string)
                else:
                    q.appendleft((cur_string + child.character, child))
        return completions

    def strings(self):
        """Return a list of all strings stored in this prefix tree.
        Time: Θ(n) | Space: O(n)"""
        # Create a list of all strings in prefix tree
        all_strings = []
        q = deque([('', self.root)])

        while len(q):
            cur_string, node = q.pop()

            for child in node.children:
                if cur_string != '' and child.character == '$':
                    all_strings.append(cur_string)
                else:
                    q.appendleft((cur_string + child.character, child))
        return all_strings

    def _traverse(self, node, prefix, visit):
        """Traverse this prefix tree with recursive depth-first traversal.
        Start at the given node with the given prefix representing its path in
        this prefix tree and visit each node with the given visit function.
        Time: Θ(n) | Space: O(lg n)"""

        if child:
            for child in node.children:
                visit(child)
                self._traverse(child, prefix + child.character, visit)
class PrefixTree:
    """PrefixTree: A multi-way prefix tree that stores strings with efficient
    methods to insert a string into the tree, check if it contains a matching
    string, and retrieve all strings that start with a given prefix string.
    Time complexity of these methods depends only on the number of strings
    retrieved and their maximum length (size and height of subtree searched),
    but is independent of the number of strings stored in the prefix tree, as
    its height depends only on the length of the longest string stored in it.
    This makes a prefix tree effective for spell-checking and autocompletion.
    Each string is stored as a sequence of characters along a path from the
    tree's root node to a terminal node that marks the end of the string."""

    # Constant for the start character stored in the prefix tree's root node
    START_CHARACTER = ''

    def __init__(self, strings=None):
        """Initialize this prefix tree and insert the given strings, if any."""
        # Create a new root node with the start character
        self.root = PrefixTreeNode(PrefixTree.START_CHARACTER)
        # Count the number of strings inserted into the tree
        self.size = 0
        # Insert each string, if any were given
        if strings is not None:
            for string in strings:
                self.insert(string)

    def __repr__(self):
        """Return a string representation of this prefix tree."""
        return f'PrefixTree({self.strings()!r})'

    def is_empty(self):
        """Return True if this prefix tree is empty (contains no strings)."""
        return self.root.num_children() == 0

    def contains(self, string):
        """Return True if this prefix tree contains the given string."""
        node, _ = self._find_node(string)
        if node is not None:
            return node.is_terminal()
        else:
            return False

    def insert(self, string):
        """Insert the given string into this prefix tree."""
        node = self.root
        added = False
        for c in string:
            if not node.has_child(c):
                node.add_child(c, PrefixTreeNode(c))
                node = node.get_child(c)
                added = True
            else:
                node = node.get_child(c)
        node.terminal = True
        if added:
            self.size += 1

    def _find_node(self, string):
        """Return a tuple containing the node that terminates the given string
        in this prefix tree and the node's depth, or if the given string is not
        completely found, return None and the depth of the last matching node.
        Search is done iteratively with a loop starting from the root node."""
        # Match the empty string
        if len(string) == 0:
            return self.root, 0
        # Start with the root node
        node = self.root
        depth = 0
        for c in string:
            if node.has_child(c):
                node = node.get_child(c)
                depth += 1
            else:
                return None, depth
        return node, depth

    def complete(self, prefix):
        """Return a list of all strings stored in this prefix tree that start
        with the given prefix string."""
        # Create a list of completions in prefix tree

        completions = []

        node, _ = self._find_node(prefix)

        def visit(n, p):
            if n.is_terminal():
                completions.append(p)

        if node is not None:
            self._traverse(node, prefix, visit)

        return completions

    def strings(self):
        """Return a list of all strings stored in this prefix tree."""
        # Create a list of all strings in prefix tree
        return self.complete('')

    def _traverse(self, node, prefix, visit):
        """Traverse this prefix tree with recursive depth-first traversal.
        Start at the given node and visit each node with the given function."""
        visit(node, prefix)
        for child in node.all_children():
            self._traverse(child, prefix + child.character, visit)
Beispiel #7
0
class PrefixTree:
    """PrefixTree: A multi-way prefix tree that stores strings with efficient
    methods to insert a string into the tree, check if it contains a matching
    string, and retrieve all strings that start with a given prefix string.
    Time complexity of these methods depends only on the number of strings
    retrieved and their maximum length (size and height of subtree searched),
    but is independent of the number of strings stored in the prefix tree, as
    its height depends only on the length of the longest string stored in it.
    This makes a prefix tree effective for spell-checking and autocompletion.
    Each string is stored as a sequence of characters along a path from the
    tree's root node to a terminal node that marks the end of the string."""

    # Constant for the start character stored in the prefix tree's root node
    START_CHARACTER = ''

    def __init__(self, strings=None):
        """Initialize this prefix tree and insert the given strings, if any."""
        # Create a new root node with the start character
        self.root = PrefixTreeNode(PrefixTree.START_CHARACTER)
        # Count the number of strings inserted into the tree
        self.size = 0
        # Insert each string, if any were given
        if strings is not None:
            for string in strings:
                self.insert(string)

    def __repr__(self):
        """Return a string representation of this prefix tree."""
        return f'PrefixTree({self.strings()!r})'

    def is_empty(self):
        """Return True if this prefix tree is empty (contains no strings)."""
        return self.root.num_children() == 0


    def contains(self, string):
        """Return True if this prefix tree contains the given string."""
        current = self.root
        for character in string:
            if not current.has_child(character):
                return False

            current = current.get_child(character)

        if current.terminal is True:
            return True
        
        return False


    def insert(self, string):
        """Insert the given string into this prefix tree."""
        current = self.root
        new_string = False
        for character in string:
            if not current.has_child(character):
                node = PrefixTreeNode(character)
                current.add_child(character, node)
                new_string = True

            current = current.get_child(character)
        
        if new_string:  # In case they try to add the same string
            self.size += 1

        current.terminal = True

    def _find_node(self, string):
        """Return a pair containing the deepest node in this prefix tree that
        matches the longest prefix of the given string and the node's depth.
        The depth returned is equal to the number of prefix characters matched.
        Search is done iteratively with a loop starting from the root node."""
        # Match the empty string
        if len(string) == 0:
            return self.root, 0
        # Start with the root node
        current = self.root
        depth = 0
        for character in string:
            if not current.has_child(character):
                return None, depth
            
            current = current.get_child(character)
            depth += 1
        
        return current, depth

    def complete(self, prefix):
        """Return a list of all strings stored in this prefix tree that start
        with the given prefix string."""
        # Create a list of completions in prefix tree
        completions = []
        starting_node, depth = self._find_node(prefix)
        if starting_node is not None:
            self._traverse(starting_node, prefix[:-1], completions.append)
            
        return completions

    def strings(self):
        """Return a list of all strings stored in this prefix tree."""
        # Create a list of all strings in prefix tree
        return self.complete('')

    def _traverse(self, node, prefix, visit):
        """Traverse this prefix tree with recursive depth-first traversal.
        Start at the given node with the given prefix representing its path in
        this prefix tree and visit each node with the given visit function."""
        prefix += node.character
        if node.terminal:
            visit(prefix)
        
        for child in node.children.values():
            self._traverse(child, prefix, visit)
class PrefixTree:
    """PrefixTree: A multi-way prefix tree that stores strings with efficient
    methods to insert a string into the tree, check if it contains a matching
    string, and retrieve all strings that start with a given prefix string.
    Time complexity of these methods depends only on the number of strings
    retrieved and their maximum length (size and height of subtree searched),
    but is independent of the number of strings stored in the prefix tree, as
    its height depends only on the length of the longest string stored in it.
    This makes a prefix tree effective for spell-checking and autocompletion.
    Each string is stored as a sequence of characters along a path from the
    tree's root node to a terminal node that marks the end of the string."""

    # Constant for the start character stored in the prefix tree's root node
    START_CHARACTER = ''

    def __init__(self, strings=None):
        """Initialize this prefix tree and insert the given strings, if any."""
        # Create a new root node with the start character
        self.root = PrefixTreeNode(PrefixTree.START_CHARACTER)
        # Count the number of strings inserted into the tree
        self.size = 0
        # Insert each string, if any were given
        if strings is not None:
            for string in strings:
                self.insert(string)

    def __repr__(self):
        """Return a string representation of this prefix tree."""
        return f'PrefixTree({self.strings()!r})'

    def is_empty(self):
        """Return True if this prefix tree is empty (contains no strings)."""
        # TODO
        if self.root.num_children() == 0:
            return True
        return False

    def contains(self, string):
        """Return True if this prefix tree contains the given string."""
        # TODO
        node = self.root
        for i in range(len(string)):
            if (i == len(string) - 1) and (node.has_child(string[i])):
                node = node.get_child(string[i])
                if node.terminal == True:
                    return True
                else:
                    return False
            elif (node.has_child(string[i])):
                node = node.get_child(string[i])
            else:
                return False
        return True

    def insert(self, string):
        """Insert the given string into this prefix tree."""
        # TODO
        if not self.contains(string):
            self.size += 1

        node = self.root

        for i in range(len(string)):
            if not node.has_child(string[i]):
                new_node = PrefixTreeNode(string[i])
                node.add_child(string[i], new_node)
            node = node.get_child(string[i])
            if i == len(string) - 1:
                node.terminal = True

    def _find_node(self, string):
        """Return a pair containing the deepest node in this prefix tree that
        matches the longest prefix of the given string and the node's depth.
        The depth returned is equal to the number of prefix characters matched.
        Search is done iteratively with a loop starting from the root node."""
        # Match the empty string
        if len(string) == 0:
            return self.root, 0
        # Start with the root node
        node = self.root
        # TODO
        depth = 0
        for i in range(len(string)):
            if node.has_child(string[i]):
                node = node.get_child(string[i])
                depth += 1
        return node, depth

    def _get_string(self, node, curr_string, strings):
        first_node = node
        for child in node.children:
            node = node.get_child(child)
            curr_string += node.character
            if node.terminal == True:
                strings.append(curr_string)
            self._get_string(node, curr_string, strings)
            node = first_node
        return strings

    def complete(self, prefix):
        """Return a list of all strings stored in this prefix tree that start
        with the given prefix string."""
        # Create a list of completions in prefix tree
        completions = []
        # TODO
        node, depth = self._find_node(prefix)

        # early exit
        if depth < len(prefix):
            return completions

        self._traverse(node, prefix[:depth], completions.append)

        return completions

    def strings(self):
        """Return a list of all strings stored in this prefix tree."""
        # Create a list of all strings in prefix tree
        all_strings = []
        # TODO
        self._traverse(self.root, '', all_strings.append)

        return all_strings

    def _traverse(self, node, prefix, visit):
        """Traverse this prefix tree with recursive depth-first traversal.
        Start at the given node with the given prefix representing its path in
        this prefix tree and visit each node with the given visit function."""
        # TODO
        if node.terminal == True:
            visit(prefix)

        for child in node.children:
            temp_node = node.get_child(child)
            self._traverse(temp_node, prefix + temp_node.character, visit)
Beispiel #9
0
class PrefixTree:
    """PrefixTree: A multi-way prefix tree that stores strings with efficient
    methods to insert a string into the tree, check if it contains a matching
    string, and retrieve all strings that start with a given prefix string.
    Time complexity of these methods depends only on the number of strings
    retrieved and their maximum length (size and height of subtree searched),
    but is independent of the number of strings stored in the prefix tree, as
    its height depends only on the length of the longest string stored in it.
    This makes a prefix tree effective for spell-checking and autocompletion.
    Each string is stored as a sequence of characters along a path from the
    tree's root node to a terminal node that marks the end of the string."""

    # Constant for the start character stored in the prefix tree's root node
    START_CHARACTER = ''

    def __init__(self, strings=None):
        """Initialize this prefix tree and insert the given strings, if any."""
        # Create a new root node with the start character
        self.root = PrefixTreeNode(PrefixTree.START_CHARACTER)
        # Count the number of strings inserted into the tree
        self.size = 0
        # Insert each string, if any were given
        if strings is not None:
            for string in strings:
                self.insert(string)

    def __repr__(self):
        """Return a string representation of this prefix tree."""
        return f'PrefixTree({self.strings()!r})'

    def is_empty(self):
        """Return True if this prefix tree is empty (contains no strings)."""
        return self.root.num_children() == 0

    def contains(self, string):
        """Return True if this prefix tree contains the given string."""
        if self._find_terminal_node(string)[0] != None:
            return True
        else: return False

    def insert(self, string):
        """Insert the given string into this prefix tree."""
        if self.contains(string): return
        # Insert string into tree
        curr_node = self.root
        for indx, char in enumerate(string):
            # if char DNE in tree, increase size

            # Traverse to next child in char is alread in string
            if curr_node.has_child(char):
                curr_node = curr_node.get_child(char)

            # Otherwise, add the child
            else:
                curr_node.add_child(char, PrefixTreeNode(char))
                curr_node = curr_node.get_child(char)

            # Change last node in str to terminal
            if indx == len(string) - 1:
                curr_node.terminal = True
                self.size += 1


    def _find_node(self, string):
        """Return a pair containing the deepest node in this prefix tree that
        matches the longest prefix of the given string and the node's depth.
        The depth returned is equal to the number of prefix characters matched.
        Search is done iteratively with a loop starting from the root node."""
        depth = 0
        # Match the empty string
        if len(string) == 0:
            return self.root, depth

        # Start with the root node
        curr_node = self.root
        for char in string:
            if curr_node.has_child(char):
                curr_node = curr_node.get_child(char)
                depth += 1
            else:
                # If full prefix is not found return depth of last found node
                return None, depth

        # if prefix is found, return node and depth
        if curr_node.is_terminal(): return curr_node, depth
        # Otherwise return none and depth
        return curr_node, depth

    def _find_terminal_node(self, string):
        """Return a tuple containing the node that terminates the given string
        in this prefix tree and the node's depth, or if the given string is not
        completely found, return None and the depth of the last matching node.
        Search is done iteratively with a loop starting from the root node."""
        depth = 0
        # Match the empty string
        if len(string) == 0:
            return self.root, depth

        # Start with the root node
        curr_node = self.root
        for char in string:
            if curr_node.has_child(char):
                curr_node = curr_node.get_child(char)
                depth += 1
            else:
                # If full prefix is not found return depth of last found node
                return None, depth

        # if prefix is found, return node and depth
        if curr_node.is_terminal(): return curr_node, depth
        # Otherwise return none and depth
        return None, depth

    def complete(self, prefix=''):
        """Return a list of all strings stored in this prefix tree that start
        with the given prefix string."""

        # Create a list of completions in prefix tree
        completions = []

        node = self._find_node(prefix)[0]
        
        if node == None:
            return completions

        # Traverse through tree to complete prefix
        if not self.is_empty():
            self._traverse(node, prefix, completions.append)

        return completions

    def strings(self):
        """Return a list of all strings stored in this prefix tree."""
        # Create a list of all strings in prefix tree
        return self.complete()

    def _traverse(self, node, prefix, visit):
        """Traverse this prefix tree with recursive depth-first traversal.
        Start at the given node and visit each node with the given function."""

        if node.is_terminal() and node is not None:
            visit(prefix)
        
        for child_id in node.children:
            child = node.get_child(child_id)
            if child:
                self._traverse(child, prefix + child.character, visit)
class PrefixTree:
    """PrefixTree: A multi-way prefix tree that stores strings with efficient
    methods to insert a string into the tree, check if it contains a matching
    string, and retrieve all strings that start with a given prefix string.
    Time complexity of these methods depends only on the number of strings
    retrieved and their maximum length (size and height of subtree searched),
    but is independent of the number of strings stored in the prefix tree, as
    its height depends only on the length of the longest string stored in it.
    This makes a prefix tree effective for spell-checking and autocompletion.
    Each string is stored as a sequence of characters along a path from the
    tree's root node to a terminal node that marks the end of the string."""

    # Constant for the start character stored in the prefix tree's ROOT NODE

    START_CHARACTER = ''

    def __init__(self, strings=None):
        """Initialize this prefix tree and insert the given strings, if any."""
        # Create a new root node with the start character
        self.root = PrefixTreeNode(PrefixTree.START_CHARACTER)
        # Count the number of strings inserted into the tree
        self.size = 0
        # Insert each string, if any were given
        if strings is not None:
            for string in strings:
                self.insert(string)

    def __repr__(self):
        """Return a string representation of this prefix tree."""
        return f'PrefixTree({self.strings()!r})'

    def is_empty(self):
        """Return True if this prefix tree is empty (contains no strings)."""
        return self.root.num_children() == 0
        # return self.size == 0 #maybe?

    def contains(self, string):
        """Return True if this prefix tree contains the given string."""
        return self.root.has_child(string)

    def insert(self, string):
        """Insert the given string into this prefix tree."""
        self.root.add_child(string)

    def _find_node(self, string):
        """Return a tuple containing the node that terminates the given string
        in this prefix tree and the node's depth, or if the given string is not
        completely found, return None and the depth of the last matching node.
        Search is done iteratively with a loop starting from the root node."""
        # Match the empty string
        if len(string) == 0:
            return self.root, 0
        # Start with the root node
        node = self.root
        # TODO

    def complete(self, prefix):
        """Return a list of all strings stored in this prefix tree that start
        with the given prefix string."""
        # Create a list of completions in prefix tree
        completions = []
        # TODO

    def strings(self):
        """Return a list of all strings stored in this prefix tree."""
        # Create a list of all strings in prefix tree
        all_strings = []
        # TODO

    def _traverse(self, node, prefix, visit):
        """Traverse this prefix tree with recursive depth-first traversal.