Skip to lab content

WEEK 10 · ADVANCED DATA STRUCTURES LAB

Standard trie — words and prefixes

B.Tech CSE & Allied Branches · II Year, I Semester · AR25C173 tested sample runs

THE SYLLABUS TASK

What you need to solve

Implement a Standard Trie.
Input format & conventions

Commands: 1 word = insert; 2 word = exact search; 3 prefix = prefix check; 0 = exit. Nonempty lowercase a–z tokens, at most 50 letters; at most 100 distinct words.

UNDERSTAND THE IDEA

Explanation

A standard trie stores one character per edge. Each node has 26 child positions indexed by letter−a, and words share their common prefix nodes.

A terminal marker distinguishes a complete stored word from a path that is only a prefix. Inserting hello creates a path for hell but does not store hell as a word.

This version exposes both exact-word lookup and prefix lookup so that distinction is visible. Characters outside lowercase a–z are rejected before child-array indexing.

TURN THE IDEA INTO STEPS

Step-by-step algorithm

  1. Validate lowercase token length and capacity.
  2. Follow each character’s child index, creating missing nodes during insertion.
  3. Mark the final node as a stored word.
  4. For exact search require both a complete path and terminal marker.
  5. For prefix search require only the path, leaving the stored word count unchanged.

SEE THE CONTROL FLOW

Flowcharts

Diamonds show decisions; labeled arrows select a branch. Returning arrows repeat work. Helper-function steps group the operations explained above and in the code. These charts focus on the named core operation. On a phone, scroll within the diagram or open the full-size version.

Standard trie insertion

Standard trie insertion

Open full-size flowchart ↗

C17

Complete C program

Download .c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <ctype.h>
#include <errno.h>

/* Read one entire token; reject malformed, oversized and out-of-range input. */
int readToken(char text[], size_t capacity) {
    int ch; size_t used = 0; int fits = 1;
    while ((ch = getchar()) != EOF && isspace((unsigned char)ch)) { }
    if (ch == EOF) return 0;
    do {
        if (ch == 0) fits = 0;
        if (used + 1 < capacity) text[used++] = (char)ch;
        else fits = 0;
        ch = getchar();
    } while (ch != EOF && !isspace((unsigned char)ch));
    text[used] = '\0';
    return fits;
}
int readInt(int *value) {
    char token[64], *end;
    if (!readToken(token, sizeof token)) return 0;
    errno = 0; long result = strtol(token, &end, 10);
    if (errno || end == token || *end || result < -1000000 || result > 1000000) return 0;
    *value = (int)result; return 1;
}
int invalid(void) { puts("Invalid or incomplete input."); return 1; }

#define MAX_WORDS 100
#define MAX_LENGTH 50
#define MAX_NODES (1 + MAX_WORDS * MAX_LENGTH)
typedef struct Node { struct Node *child[26]; int terminal; } Node;
Node pool[MAX_NODES]; int used = 1, count = 0;
Node *root = &pool[0];
int validWord(const char word[]) {
    if (!word[0]) return 0;
    for (int i = 0; word[i]; ++i) if (word[i] < 'a' || word[i] > 'z') return 0;
    return 1;
}
const Node *follow(const char word[]) {
    const Node *p = root;
    for (int i = 0; word[i]; ++i) {
        p = p->child[word[i] - 'a']; if (!p) return NULL;
    }
    return p;
}
void insert(const char word[]) {
    Node *p = root;
    for (int i = 0; word[i]; ++i) {
        int index = word[i] - 'a';
        if (!p->child[index]) p->child[index] = &pool[used++];
        p = p->child[index];
    }
    p->terminal = 1; ++count;
}
/* 1 word: insert, 2 word: exact search, 3 prefix: prefix check, 0: exit.
   Nonempty lowercase a-z tokens, length at most 50; 100 distinct words. */
int main(void) {
    int command; char word[MAX_LENGTH + 1];
    while (readInt(&command)) {
        if (!command) return 0;
        if (command < 1 || command > 3 || !readToken(word,sizeof word) || !validWord(word)) return invalid();
        const Node *p = follow(word);
        if (command == 2) puts(p && p->terminal ? "Found." : "Not found.");
        else if (command == 3) puts(p ? "Prefix exists." : "Prefix absent.");
        else if (p && p->terminal) puts("Duplicate ignored.");
        else if (count == MAX_WORDS) puts("Trie is full.");
        else { insert(word); puts("Inserted."); }
    }
    return invalid();
}

Local compile: gcc -std=c17 standard-trie.c -o program -lm

Open in C compiler transfers the source. Copy the sample input into the compiler’s input box separately before running.

UNDERSTAND THE CODE

Line-by-line explanation

Follow the exact source above, from input handling through the algorithm and its result. Line numbers count blank lines; related statements are grouped into steps. Start with main to follow execution, then open the functions it calls.

Start with main → · Algorithm and data structure →

Lines 1–5Include input, conversion and validation declarations
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <ctype.h>
#include <errno.h>

stdio.h supplies terminal input/output. stdlib.h declares conversion and allocation functions. string.h supplies string and memory utilities. ctype.h supplies isspace, and errno.h supplies conversion error reporting. Headers declare facilities; including them does not execute an operation.

Lines 7–9Define a complete-token reader and initialise its state
/* Read one entire token; reject malformed, oversized and out-of-range input. */
int readToken(char text[], size_t capacity) {
    int ch; size_t used = 0; int fits = 1;

readToken receives a writable buffer and its capacity. ch is int so it can represent EOF as well as input characters. used counts stored characters, and fits starts as a success flag. One slot must remain for the null terminator.

Lines 10–11Skip leading whitespace and recognise end-of-file
    while ((ch = getchar()) != EOF && isspace((unsigned char)ch)) { }
    if (ch == EOF) return 0;

Read until a non-whitespace character appears. The unsigned-char cast supplies an allowed value to isspace. Return 0 if EOF arrives before a token begins.

Lines 12–15Collect a token without silently truncating it
    do {
        if (ch == 0) fits = 0;
        if (used + 1 < capacity) text[used++] = (char)ch;
        else fits = 0;

The do body handles the character already read. An embedded zero byte marks the token invalid. Store a character only while there is room for it and a null terminator; otherwise clear fits. Continue reading even after the buffer fills.

Lines 16–20Consume the full token and terminate the buffer
        ch = getchar();
    } while (ch != EOF && !isspace((unsigned char)ch));
    text[used] = '\0';
    return fits;
}

Read through the next whitespace or EOF, add a null character after the stored prefix, then return fits. A too-long token returns 0 rather than pretending that its stored prefix is the complete token.

Lines 21–23Wrap token input as a checked integer read
int readInt(int *value) {
    char token[64], *end;
    if (!readToken(token, sizeof token)) return 0;

value is an output address. A 64-slot local buffer receives the token; a failed token read returns 0 before attempting conversion.

Lines 24–25Convert base-ten text and check the whole result
    errno = 0; long result = strtol(token, &end, 10);
    if (errno || end == token || *end || result < -1000000 || result > 1000000) return 0;

Reset errno before strtol. &end receives the stopping address. Reject conversion errors, no converted digits, leftover characters, or values outside -1,000,000 to 1,000,000. Thus 12x is not accepted as the integer 12.

Lines 26–28Return the accepted value or a standard error status
    *value = (int)result; return 1;
}
int invalid(void) { puts("Invalid or incomplete input."); return 1; }

Store a validated integer through *value and return 1 from readInt. invalid is a separate helper that prints the error and returns 1 as main’s failure status; the same number has different roles in these two function contracts.

Lines 30–35Reserve a lowercase word trie
#define MAX_WORDS 100
#define MAX_LENGTH 50
#define MAX_NODES (1 + MAX_WORDS * MAX_LENGTH)
typedef struct Node { struct Node *child[26]; int terminal; } Node;
Node pool[MAX_NODES]; int used = 1, count = 0;
Node *root = &pool[0];

Each node has 26 letter links and a terminal marker. The pool is large enough for root plus the maximum allowed characters across 100 words of at most 50 letters each. Static storage begins zero-initialised.

Lines 36–40Require a nonempty lowercase word
int validWord(const char word[]) {
    if (!word[0]) return 0;
    for (int i = 0; word[i]; ++i) if (word[i] < 'a' || word[i] > 'z') return 0;
    return 1;
}

Reject an empty string or any character outside a through z. Return 1 only after every character passes; later child indexes therefore stay within 0–25.

Lines 41–47Follow a path without requiring a complete word
const Node *follow(const char word[]) {
    const Node *p = root;
    for (int i = 0; word[i]; ++i) {
        p = p->child[word[i] - 'a']; if (!p) return NULL;
    }
    return p;
}

Starting at root, map each letter to word[i] - 'a' and follow its child. A missing child returns NULL. Otherwise return the final node, whose terminal flag can distinguish a whole word from a prefix.

Lines 48–54Reuse prefixes and create only missing letter nodes
void insert(const char word[]) {
    Node *p = root;
    for (int i = 0; word[i]; ++i) {
        int index = word[i] - 'a';
        if (!p->child[index]) p->child[index] = &pool[used++];
        p = p->child[index];
    }

For each letter obtain its 0–25 index. Allocate a zero-initialised pool entry only when that child link is absent, then advance p. Shared prefixes therefore share the same nodes.

Lines 55–56Mark the end of a newly inserted word
    p->terminal = 1; ++count;
}

Set terminal at the final node and increase the distinct-word count. The driver avoids calling insert for an already terminal word.

Lines 57–63Read bounded lowercase tokens under the documented commands
/* 1 word: insert, 2 word: exact search, 3 prefix: prefix check, 0: exit.
   Nonempty lowercase a-z tokens, length at most 50; 100 distinct words. */
int main(void) {
    int command; char word[MAX_LENGTH + 1];
    while (readInt(&command)) {
        if (!command) return 0;
        if (command < 1 || command > 3 || !readToken(word,sizeof word) || !validWord(word)) return invalid();

The interface supports insert, exact search and prefix check. main validates the command, token capacity and lowercase rule before using any character as a child index. Word input is a token, not a line with spaces.

Lines 64–66Separate exact word search from prefix existence
        const Node *p = follow(word);
        if (command == 2) puts(p && p->terminal ? "Found." : "Not found.");
        else if (command == 3) puts(p ? "Prefix exists." : "Prefix absent.");

follow can return a node for a prefix that was never inserted as a whole word. Exact search therefore needs both p and p->terminal. Prefix check needs only the path node p.

Lines 67–72Ignore duplicates, enforce the word limit or insert
        else if (p && p->terminal) puts("Duplicate ignored.");
        else if (count == MAX_WORDS) puts("Trie is full.");
        else { insert(word); puts("Inserted."); }
    }
    return invalid();
}

An existing terminal path is a duplicate. Otherwise reject a full trie or create the new word. End-of-input without command 0 returns failure; the static pool requires no heap cleanup.

FOLLOW THE VALUES

Dry run

  1. Insert hello, then help: both share h,e,l.
  2. Exact search hel fails because its node is not terminal.
  3. Prefix check hel succeeds because its path exists.
  4. After inserting hel, that same node becomes terminal and exact search succeeds.

CHECK THE BEHAVIOUR

Sample input & output

These outputs were captured by running the exact C source shown above. Commands use the stated input format; the programs omit repeated menu prompts so the same sample can be pasted and run.

Sample 1

INPUT
1 hello
1 help
2 hel
3 hel
1 hel
2 hel
0
OUTPUT
Inserted.
Inserted.
Not found.
Prefix exists.
Inserted.
Found.

Sample 2

INPUT
1 world
1 world
2 world
2 hi
3 wor
0
OUTPUT
Inserted.
Duplicate ignored.
Found.
Not found.
Prefix exists.

Sample 3

INPUT
3 a
1 a
1 ab
2 a
2 ab
0
OUTPUT
Prefix absent.
Inserted.
Inserted.
Found.
Found.

Common mistakes

  • A prefix path is not proof that the prefix was inserted as a complete word.
  • Validate characters before subtracting a.
  • Duplicate words must not consume another distinct-word slot.

WHY THIS GROWTH RATE?

Time and space complexity

Insert/exact-search/prefix-check O(L); at most O(kL×26) child-pointer storage for k words of length ≤L.

Each operation follows at most one child per character, so work depends on token length rather than scanning all stored words.

At most one new node is created per inserted character. Each node reserves 26 pointers, even if few are used, so the constant alphabet has a substantial memory cost.

The example reserves 5001 nodes for 100 words of length 50. For the fixed 26-letter alphabet, the conceptual storage is commonly written O(kL).

Big-O describes growth in the stated input quantity. The educational limits keep examples safe; complexity discussions state which bounds depend on a generalized input size. Arithmetic on the accepted native numeric types is treated as constant-time.

PREPARE FOR YOUR LAB VIVA

Related viva questions & answers

6 focused questions

Try answering aloud, then expand the answer to check your reasoning. These questions focus on the concepts used in this program.

Why is a terminal marker necessary even when a path exists?

A longer word creates paths for all its prefixes. The marker records which of those paths were actually inserted as complete words.

Why is hel a valid prefix after inserting hello but not automatically a word?

The h-e-l path exists as part of hello, but its final node has not been marked terminal. Prefix lookup and exact lookup therefore return different answers.

How does inserting help reuse the path for hello?

The h, e and l edges are shared. A different fourth-character edge is created, so the common prefix requires no duplicate nodes.

Why must uppercase letters be rejected under this input contract?

The expression letter - 'a' is designed only for lowercase a through z. Other bytes can yield invalid child indexes; case conversion would need an explicit additional rule.

Does checking a prefix create or mark any trie nodes?

No. It follows existing pointers and reports whether the path exists. The operation leaves all stored words and terminal markers unchanged.

What memory tradeoff comes from reserving 26 children in every node?

Child lookup is direct and simple, but many pointer slots can be unused. A sparse child representation could save memory while adding lookup overhead.