• children is a hash map to store the alphabets which leads to new nodes
  • endOfWord inndicates whether a word ends at the particular node
class TrieNode:
    def __init__(self):
        self.children = {}
        self.endOfWord = False

class Trie:
    def __init__(self):
        self.root = TrieNode()

    def insert(self, word):
        cur = self.root
        for c in word:
            if c not in cur.children:
                cur.children[c] = TrieNode()  # Fixed: added () to instantiate
            cur = cur.children[c]
        cur.endOfWord = True

    def delete(self, word):
        def dfs(node, i):
            # Reached the node representing the complete word
            if i == len(word):
                if not node.endOfWord:
                    return False, False  # word doesn't exist
                node.endOfWord = False
                # Parent can delete this node if it has no children
                return True, len(node.children) == 0

            if word[i] not in node.children:
                return False, False

            child = node.children[word[i]]
            deleted, shouldDeleteChild = dfs(child, i + 1)

            if not deleted:
                return False, False

            # Child has become useless β†’ remove it
            if shouldDeleteChild:
                node.children.pop(word[i])

            # Current node can now also be pruned
            return True, len(node.children) == 0 and not node.endOfWord

        dfs(self.root, 0)

    def search(self, word):
        cur = self.root
        for c in word:
            if c not in cur.children:
                return False
            cur = cur.children[c]
        return cur.endOfWord

    def startsWith(self, word):
        cur = self.root
        for c in word:
            if c not in cur.children:
                return False
            cur = cur.children[c]
        return True

Trie β€” Implementation

Tags: #DSA #Trie #Tree #String #PrefixTree #DataStructures #Insert #Search #Delete #Recursion


1. What is a Trie?

A Trie (Prefix Tree) is a tree-like data structure used to store strings where:

  • Each edge represents a character.

  • A path from the root represents a string/prefix.

  • endOfWord = True marks that a complete word ends at that node.

  • children stores the next possible characters.

Example

Insert:

cat
car
cart

The Trie becomes conceptually:

root
 └── c
      └── a
           β”œβ”€β”€ t [EOW]
           └── r [EOW]
                └── t [EOW]

[EOW] means endOfWord = True.

Notice that car and cart share the path:

c β†’ a β†’ r

This is the main advantage of a Trie: common prefixes are shared.


2. Node Structure

class TrieNode:
    def __init__(self):
        self.children = {}
        self.endOfWord = False

Every Trie node contains two pieces of information.

children

self.children = {}

A dictionary mapping:

character β†’ TrieNode

For example:

{
    'a': TrieNode,
    'b': TrieNode
}

means that from the current node we can move to either a or b.

Using a dictionary also means we can check whether a character exists in approximately O(1)O(1) average time.

endOfWord

self.endOfWord = False

This tells us whether the current node represents the end of a complete inserted word.

This distinction is important.

Suppose we insert:

car
cart

The node representing r has:

endOfWord = True

because "car" is a complete word.

It also has a child:

r β†’ t

because "cart" exists.

Therefore, a node can simultaneously be the end of one word and a prefix of another word.


3. Trie Initialization

class Trie:
    def __init__(self):
        self.root = TrieNode()

The Trie always starts with an empty root node.

root

The root does not represent a character.

It simply acts as the starting point for every word.


4. Insert

def insert(self, word):
    cur = self.root

    for c in word:
        if c not in cur.children:
            cur.children[c] = TrieNode()

        cur = cur.children[c]

    cur.endOfWord = True

Idea

For every character:

  1. Check whether the current node already has that character as a child.

  2. If not, create a new node.

  3. Move cur to that child.

  4. After processing the entire word, mark the final node as endOfWord.


Example: Insert "cat"

Initially:

root

Process 'c'

c does not exist:

if 'c' not in cur.children:
    cur.children['c'] = TrieNode()

Now:

root
 └── c

Move:

cur = cur.children['c']

Process 'a'

Create a:

root
 └── c
      └── a

Process 't'

Create t:

root
 └── c
      └── a
           └── t

After the loop:

cur.endOfWord = True

Therefore:

root
 └── c
      └── a
           └── t [EOW]

Now "cat" exists in the Trie.


Inserting a word with an existing prefix

Suppose "cat" already exists and we insert:

car

We already have:

c β†’ a

so we reuse those nodes.

Only r needs to be created:

root
 └── c
      └── a
           β”œβ”€β”€ t [EOW]
           └── r [EOW]

This is the prefix sharing property of a Trie.


5. Search

def search(self, word):
    cur = self.root

    for c in word:
        if c not in cur.children:
            return False

        cur = cur.children[c]

    return cur.endOfWord

Important idea

Searching has two conditions:

  1. The entire character path must exist.

  2. The final node must have endOfWord = True.

The second condition is essential.


Example

Suppose the Trie contains:

car

Then:

root
 └── c
      └── a
           └── r [EOW]

Search "car"

We successfully follow:

c β†’ a β†’ r

At r:

cur.endOfWord == True

Therefore:

search("car") β†’ True

Search "ca"

We successfully follow:

c β†’ a

But:

a.endOfWord == False

Therefore:

search("ca") β†’ False

Even though "ca" is a valid prefix, it was not inserted as a complete word.

Key distinction

Path exists β‰  Word exists

A path represents a prefix.

endOfWord = True tells us that the prefix is also a complete word.


6. Starts With / Prefix Search

def startsWith(self, word):
    cur = self.root

    for c in word:
        if c not in cur.children:
            return False

        cur = cur.children[c]

    return True

Unlike search(), we do not check endOfWord.

We only care whether the entire prefix path exists.


Example

Trie contains:

car
cart
cat

Conceptually:

root
 └── c
      └── a
           β”œβ”€β”€ r [EOW]
           β”‚    └── t [EOW]
           β”‚
           └── t [EOW]

startsWith("ca")

Path exists:

c β†’ a

Therefore:

True

even though "ca" itself isn’t necessarily a word.

startsWith("car")

Path exists:

c β†’ a β†’ r

Therefore:

True

startsWith("cab")

There is no b after ca.

Therefore:

False

7. Delete

Deletion is the most interesting operation because simply removing the characters can break other words that share the same prefix.

Our implementation uses DFS + backtracking.

def delete(self, word):
    def dfs(node, i):
        if i == len(word):
            if not node.endOfWord:
                return False, False

            node.endOfWord = False

            return True, len(node.children) == 0

        if word[i] not in node.children:
            return False, False

        child = node.children[word[i]]

        deleted, shouldDeleteChild = dfs(child, i + 1)

        if not deleted:
            return False, False

        if shouldDeleteChild:
            node.children.pop(word[i])

        return True, len(node.children) == 0 and not node.endOfWord

    dfs(self.root, 0)

8. Why Can’t We Simply Remove the Word?

Suppose the Trie contains:

car
cart
root
 └── c
      └── a
           └── r [EOW]
                └── t [EOW]

If we delete "cart", we should get:

root
 └── c
      └── a
           └── r [EOW]

We only remove t.

But if we delete "car":

root
 └── c
      └── a
           └── r [EOW]
                └── t [EOW]

we must not remove r, because cart still needs it.

Therefore deletion must determine:

β€œAfter deleting this word, is this node still needed?”


9. The Two Return Values of DFS

Our DFS returns:

(deleted, shouldDelete)

These mean two different things.

deleted

Was the requested word actually found and deleted?

shouldDelete

Can this node now be removed by its parent?

So:

(deleted, shouldDelete)

is essentially:

(word was deleted?, this node can be pruned?)

This allows information to propagate from the bottom of the Trie back toward the root.


10. Delete Example β€” "cart"

Consider:

car
cart

Trie:

root
 └── c
      └── a
           └── r [EOW]
                └── t [EOW]

We call:

delete("cart")

DFS travels downward:

root
  ↓
 c
  ↓
 a
  ↓
 r
  ↓
 t

At t:

i == len(word)

We have reached the node representing "cart".

Since:

t.endOfWord == True

we execute:

t.endOfWord = False

Now:

t
endOfWord = False
children = {}

Therefore:

return True, True

Meaning:

True  β†’ "cart" was deleted.
True  β†’ t can be deleted.

11. Returning to the Parent

We return to r.

We received:

deleted = True
shouldDeleteChild = True

Therefore:

if shouldDeleteChild:
    node.children.pop('t')

So:

r

no longer has a t child.

Now r looks like:

r
β”œβ”€β”€ endOfWord = True
└── children = {}

Can r be deleted?

No.

Because:

not node.endOfWord

is False.

Therefore:

return True, False

Meaning:

"cart" was deleted,
but r must remain because "car" still exists.

The final Trie is:

root
 └── c
      └── a
           └── r [EOW]

12. Delete Example β€” "car"

Now suppose the Trie contains:

car

only:

root
 └── c
      └── a
           └── r [EOW]

Call:

delete("car")

DFS reaches r.

At the terminal node:

r.endOfWord = False

Now:

r
β”œβ”€β”€ endOfWord = False
└── children = {}

Therefore:

return True, True

r can be deleted.


Backtracking

Parent a receives:

(True, True)

so:

a.children.pop('r')

Now a has no children and is not the end of a word.

Therefore:

return True, True

The same thing happens for c.

Eventually:

root

has no children.

The entire path has been pruned.


13. Why not node.endOfWord Matters

This condition:

return True, len(node.children) == 0 and not node.endOfWord

is critical.

Consider:

car
cart

After deleting "cart":

r
β”œβ”€β”€ endOfWord = True
└── children = {}

Although:

len(r.children) == 0

we cannot delete r.

Why?

Because r represents the word "car".

Therefore:

len(node.children) == 0

alone is insufficient.

We need:

len(node.children) == 0 and not node.endOfWord

Meaning:

β€œThis node has no children AND does not represent the end of another word.”

Only then is the node completely useless.


14. Delete a Non-existent Word

Suppose Trie contains:

cat

and we call:

delete("car")

DFS follows:

c β†’ a

At a, we look for:

r

but only:

t

exists.

Therefore:

if word[i] not in node.children:
    return False, False

This propagates:

False, False

all the way back to the root.

Nothing is modified.


15. Complete Implementation

class TrieNode:
    def __init__(self):
        self.children = {}
        self.endOfWord = False


class Trie:

    def __init__(self):
        self.root = TrieNode()


    def insert(self, word):
        cur = self.root

        for c in word:
            if c not in cur.children:
                cur.children[c] = TrieNode()

            cur = cur.children[c]

        cur.endOfWord = True


    def delete(self, word):

        def dfs(node, i):

            # Reached the node representing the complete word
            if i == len(word):

                if not node.endOfWord:
                    return False, False

                node.endOfWord = False

                # Parent can delete this node if it has no children
                return True, len(node.children) == 0


            if word[i] not in node.children:
                return False, False


            child = node.children[word[i]]

            deleted, shouldDeleteChild = dfs(child, i + 1)


            if not deleted:
                return False, False


            # Child became useless β†’ remove it
            if shouldDeleteChild:
                node.children.pop(word[i])


            # Current node can now also be pruned
            return True, len(node.children) == 0 and not node.endOfWord


        dfs(self.root, 0)


    def search(self, word):

        cur = self.root

        for c in word:

            if c not in cur.children:
                return False

            cur = cur.children[c]

        return cur.endOfWord


    def startsWith(self, word):

        cur = self.root

        for c in word:

            if c not in cur.children:
                return False

            cur = cur.children[c]

        return True

16. Complexity

Let LL be the length of the word/prefix.

OperationTimeAuxiliary Space
insertO(L)O(L)O(1)O(1)
searchO(L)O(L)O(1)O(1)
startsWithO(L)O(L)O(1)O(1)
deleteO(L)O(L)O(L)O(L)

Why is deletion space O(L)O(L)?

Deletion uses recursive DFS:

root
 ↓
 c
 ↓
 a
 ↓
 r
 ↓
 t

There can be at most LL recursive calls.

Therefore recursion stack:

O(L)O(L)

The Trie nodes themselves are not counted as auxiliary space because they are the existing data structure.


17. Core Invariants to Remember

Insert

Every character corresponds to a node along the path.
Final node β†’ endOfWord = True
Entire path must exist
AND
final node must have endOfWord = True

StartsWith

Entire prefix path must exist.
endOfWord does not matter.

Delete

A node can be pruned only when:

No children
AND
Not endOfWord

The recursive deletion communicates:

(deleted, shouldDelete)

where:

deleted
    ↓
Was the requested word successfully deleted?

shouldDelete
    ↓
Can the parent safely remove this node?

18. Mental Model for Trie Deletion

The easiest way to remember the deletion algorithm is:

Go down to the end of the word, unmark it, then walk back upward pruning nodes that have become useless.

At every node during the return:

Did deletion succeed?
        β”‚
        β”œβ”€β”€ No β†’ stop; nothing to modify
        β”‚
        └── Yes
              β”‚
              β”œβ”€β”€ Child useless? β†’ remove child
              β”‚
              └── Am I useless?
                    β”‚
                    β”œβ”€β”€ no children
                    └── not endOfWord

This is essentially post-order processing of the word’s Trie path:

Go down:
root β†’ c β†’ a β†’ r β†’ t

Come back:
t β†’ r β†’ a β†’ c β†’ root

The downward phase finds the word.

The upward phase performs pruning.

Local Graph View

Start typing to search
Try: two sum or #Arrays or #Amazon