C Programming data structures Applications Expression Evaluation, Undo Feature Step by step Implementation and Top 10 Questions and Answers
 .NET School AI Teacher - SELECT ANY TEXT TO EXPLANATION.    Last Update: April 01, 2025      27 mins read      Difficulty-Level: beginner

C Programming: Data Structures Applications - Expression Evaluation and Undo Feature

Data structures in C programming are fundamental tools used to organize and store data efficiently. They play a critical role in developing applications that require efficient data manipulation, retrieval, and storage. Two notable applications of data structures in C programming are expression evaluation and implementing an undo feature. Let's delve into these applications in detail.

Expression Evaluation

Expression evaluation is a common task in computer science, where we evaluate mathematical expressions given in the form of a string. Expressions can include numbers, operators, and parentheses. For effective expression evaluation, data structures like stacks and trees are used.

Stacks for Expression Evaluation

Stacks are ideal for evaluating postfix expressions (Reverse Polish Notation) and infix-to-postfix conversion, which are prerequisites for evaluating infix expressions (e.g., traditional mathematical expressions).

  • Infix to Postfix Conversion:

    • Convert an infix expression to a postfix expression using a stack to hold operators. Follow these rules:
      1. Scan the infix expression from left to right.
      2. If an operand is encountered, print it to the postfix expression.
      3. If a left parenthesis '(' is encountered, push it onto the stack.
      4. If a right parenthesis ')' is encountered, pop and print the top operators from the stack until a left parenthesis '(' is encountered.
      5. If an operator is encountered, pop operators from the stack to the postfix expression until the top of the stack has an operator of lower precedence or the stack is empty. Then push the encountered operator onto the stack.
      6. Repeat steps 1-5 until the infix expression is scanned.
      7. Pop the remaining operators from the stack to the postfix expression.
  • Evaluating Postfix Expressions:

    • Evaluate the postfix expression using a stack.
      1. Scan the postfix expression from left to right.
      2. If an operand is encountered, push it onto the stack.
      3. If an operator is encountered, pop two operands from the stack, apply the operator, and push the result back onto the stack.
      4. Repeat steps 1-3 until the postfix expression is scanned.
      5. The result of the postfix expression is the top of the stack.

Here's a code snippet demonstrating the conversion of an infix expression to a postfix expression:

#include <stdio.h>
#include <stdlib.h>
#include <ctype.h>
#include <string.h>

#define MAX 100

char stack[MAX];
int top = -1;

int isEmpty() {
    return top == -1;
}

char peek() {
    return stack[top];
}

void push(char op) {
    stack[++top] = op;
}

char pop() {
    return stack[top--];
}

int precedence(char op) {
    switch (op) {
        case '+':
        case '-':
            return 1;
        case '*':
        case '/':
            return 2;
        case '^':
            return 3;
    }
    return -1;
}

int isOperator(char op) {
    return (op == '+' || op == '-' || op == '*' || op == '/' || op == '^');
}

int isOperand(char ch) {
    return isdigit(ch);
}

void infixToPostfix(char* infix, char* postfix) {
    int i, j;
    for (i = 0, j = 0; infix[i]; ++i) {
        if (isOperand(infix[i])) {
            postfix[j++] = infix[i];
        } else if (infix[i] == '(') {
            push(infix[i]);
        } else if (infix[i] == ')') {
            while (!isEmpty() && peek() != '(') {
                postfix[j++] = pop();
            }
            if (!isEmpty() && peek() != '(') {
                printf("Invalid Expression");
                return;
            } else {
                pop();
            }
        } else if (isOperator(infix[i])) {
            while (!isEmpty() && precedence(peek()) >= precedence(infix[i])) {
                postfix[j++] = pop();
            }
            push(infix[i]);
        }
    }

    while (!isEmpty()) {
        postfix[j++] = pop();
    }

    postfix[j] = '\0';
}

int main() {
    char infix[] = "a+b*(c^d-e)^(f+g*h)-i";
    char postfix[100];
    infixToPostfix(infix, postfix);
    printf("Postfix: %s\n", postfix);
    return 0;
}

Undo Feature

An undo feature is essential in applications such as text editors, word processors, and IDEs. Implementing an undo feature involves tracking the history of operations performed by the user. Data structures like stacks and doubly linked lists are commonly used for this purpose.

Stacks for Undo Operations

Using a stack to track the history of operations is a straightforward approach to implement an undo feature. However, it has limitations, especially for operations with parameters.

  • Push Operation into Stack:
    • Each operation performed by the user can be pushed onto a stack. When an undo is requested, the top operation is popped from the stack, and its effects are reversed.

Here’s an example using a stack for an undo feature in a simple text editor:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

#define MAX 100

typedef struct {
    char operation[MAX]; // e.g., "insert", "delete"
    char data[MAX];      // e.g., "text to insert", "position to delete from"
} Operation;

Operation stack[MAX];
int top = -1;

void push(Operation op) {
    if (top == MAX - 1) {
        printf("Stack overflow\n");
        return;
    }
    stack[++top] = op;
}

Operation pop() {
    if (top == -1) {
        printf("Stack underflow\n");
        Operation defaultOp;
        memset(&defaultOp, 0, sizeof(Operation));
        return defaultOp;
    }
    return stack[top--];
}

void performOperation(const char* operation, const char* data) {
    Operation op;
    strcpy(op.operation, operation);
    strcpy(op.data, data);
    push(op);
    printf("Operation performed: %s, Data: %s\n", operation, data);
}

void undo() {
    if (top == -1) {
        printf("No operations to undo\n");
        return;
    }

    Operation undoneOp = pop();
    printf("Undo operation: %s, Data: %s\n", undoneOp.operation, undoneOp.data);

    // Reverse the operation
    if (strcmp(undoneOp.operation, "insert") == 0) {
        printf("Deleted: %s\n", undoneOp.data);
    } else if (strcmp(undoneOp.operation, "delete") == 0) {
        printf("Inserted: %s\n", undoneOp.data);
    }
}

int main() {
    performOperation("insert", "hello world");
    performOperation("delete", "world");
    undo();
    undo();
    return 0;
}

Doubly Linked Lists for Undo Operations

For more complex applications, backward and forward navigation, and significantly different operations, a doubly linked list can be more suitable. Each node in the list represents a state of the application.

  • Creating a Node:

    • Each node holds the application state (e.g., document content) and pointers to the previous and next states.
  • Inserting a Node:

    • When an operation is performed, a new node is inserted after the current node, and the next pointer is updated.
    • All nodes after the current node (future states) are discarded to ensure the undo history reflects only the actual operations performed.

Here’s an example using a doubly linked list for an undo feature:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

typedef struct Node {
    char document[1000];
    struct Node* prev;
    struct Node* next;
} Node;

Node* head = NULL;
Node* current = NULL;

Node* createNode(const char* document) {
    Node* newNode = (Node*)malloc(sizeof(Node));
    strcpy(newNode->document, document);
    newNode->prev = NULL;
    newNode->next = NULL;
    return newNode;
}

void addOperation(const char* document) {
    Node* newNode = createNode(document);
    if (current == NULL) {
        head = newNode;
        current = newNode;
    } else {
        newNode->prev = current;
        current->next = newNode;
        current = newNode;
    }
}

void undo() {
    if (current == NULL || current->prev == NULL) {
        printf("Nothing to undo\n");
        return;
    }
    current = current->prev;
    printf("Undo to: %s\n", current->document);
}

void redo() {
    if (current == NULL || current->next == NULL) {
        printf("Nothing to redo\n");
        return;
    }
    current = current->next;
    printf("Redo to: %s\n", current->document);
}

int main() {
    addOperation("hello");
    addOperation("hello world");
    addOperation("hello world!");
    undo();
    redo();
    undo();
    return 0;
}

Conclusion

Data structures are indispensable in C programming, enabling the efficient implementation of complex features like expression evaluation and undo mechanisms. Utilizing stacks for postfix expression evaluation and undo features with limited operations is straightforward, while doubly linked lists offer more flexibility for complex applications. Understanding these concepts and their practical implementations is essential for any C programmer looking to develop robust and feature-rich applications.




C Programming Data Structures: Applications - Expression Evaluation & Undo Feature

Understanding data structures in C programming is crucial as it provides an essential framework to organize, manage, and utilize data efficiently within applications. Two prominent applications of data structures are expression evaluation and implementing an undo feature. In this guide, we'll explore these topics step-by-step, ensuring clarity for beginners.

Expression Evaluation

Expression evaluation is a fundamental application of data structures used in compilers and interpreters to compute the value of an arithmetic expression given in string format. The most common expressions include infix (standard mathematical notation), prefix (Reverse Polish Notation), and postfix (Polish Notation). For simplicity, we will focus on evaluating infix expressions using the stack data structure.

Step-by-Step Approach for Evaluating Infix Expressions:

  1. Understand the Problem: We need to evaluate a infix expression like 3 + (2 * 4) / 2.

  2. Convert Infix to Postfix (Optional): It’s often easier to evaluate postfix expressions since they eliminate the requirement for operator precedence handling. This step can be skipped if you directly evaluate the infix expression.

  3. Use Stack for Operators: Utilize a stack to hold operators and another stack to hold operands.

  4. Tokenize the Infix Expression: Break the expression into numbers (operands) and operators (characters like +, -, *, /, (, and )).

  5. Operator Precedence and Associativity:

    • Precedence: Higher precedence operators are evaluated before lower ones.
    • Associativity: Defines how operators with equal precedence should be grouped; usually left-to-right (+, -, *, /).
  6. Evaluate Postfix Expression:

    • Iterate through each token of the postfix expression.
    • If the token is an operand, push it onto the operand stack.
    • If the token is an operator, pop the required number of operands from the stack, perform the operation, and push the result back onto the stack.
    • The final result remains as the only element in the stack.

Implementation Example in C:

Let's write a simple program that directly evaluates an infix expression without converting it to postfix.

#include <stdio.h>
#include <stdlib.h>
#include <ctype.h>
#include <string.h>

#define MAX 100

typedef struct {
    int items[MAX];
    int top;
} Stack;

void initStack(Stack *s) {
    s->top = -1;
}

int isEmpty(Stack *s) {
    return s->top == -1;
}

int isFull(Stack *s) {
    return s->top == MAX - 1;
}

void push(Stack *s, int value) {
    if (isFull(s)) {
        printf("Stack is full\n");
        return;
    }
    s->items[++s->top] = value;
}

int pop(Stack *s) {
    if (isEmpty(s)) {
        printf("Stack is empty\n");
        return -1;
    }
    return s->items[s->top--]; 
}

int peek(Stack *s) {
    if (isEmpty(s)) 
        return -1;
    return s->items[s->top];
}

int precedence(char op) {
    if(op == '+' || op == '-') 
        return 1;
    if(op == '*' || op == '/') 
        return 2;
    return 0;
}

int applyOp(int a, int b, char op)
{
    switch(op) 
    {
        case '+': return a + b;
        case '-': return a - b;
        case '*': return a * b;
        case '/': return a / b;
    }
}

int evaluate(char *tokens) {
    Stack values;
    Stack ops;
    initStack(&values);
    initStack(&ops);
  
    for (int i = 0; i < strlen(tokens); i++) {
   
        if(tokens[i] == ' ')
            continue;
   
        else if(tokens[i] == '(') {
            push(&ops, tokens[i]);
        }
   
        else if(isdigit(tokens[i])) {
            int val = 0;
            while(i < strlen(tokens) && isdigit(tokens[i])) {
                val = (val * 10) + (tokens[i]-'0');
                i++;
            }
            push(&values, val);
            i--;
        }
   
        else if(tokens[i] == ')') {
            while(!isEmpty(&ops) && peek(&ops) != '(') {
                int val2 = pop(&values);
                int val1 = pop(&values);
                char op = pop(&ops);
                push(&values, applyOp(val1, val2, op));
            }
          
            if (!isEmpty(&ops) && peek(&ops) != '(') {
                return -1; 
            }
            else {
                pop(&ops);
            }
        }
   
        else {
          
            while(!isEmpty(&ops) && precedence(peek(&ops)) >= precedence(tokens[i]) ) {
                int val2 = pop(&values);
                int val1 = pop(&values);
                char op = pop(&ops);
                push(&values, applyOp(val1, val2, op));
            }
            push(&ops, tokens[i]);
        }
    }
   
    while(!isEmpty(&ops)) {
        int val2 = pop(&values);
        int val1 = pop(&values);
        char op = pop(&ops);
        push(&values, applyOp(val1, val2, op));
    }
    
    return pop(&values);
}

int main() {
    char exp[MAX];
    printf("Enter the infix expression: ");
    fgets(exp, sizeof(exp), stdin);
    exp[strcspn(exp, "\n")] = '\0'; // Remove newline character
    int result = evaluate(exp);
    printf ("Result: %d\n", result);
    return 0;
}

Step-by-step Process:

  1. Initialize Stacks: Create two stacks for values and operators, initializing both.

  2. Parse Tokens: Use a loop to iterate through expression characters.

  3. Spaces: Ignore spaces between tokens.

  4. Parentheses: Push opening parentheses onto the operator stack. Upon encountering closing parentheses, solve all operators inside until finding the matching opening parenthesis.

  5. Operands: Convert consecutive digits to integers and push them onto the value stack.

  6. Operators: Pop operators from the stack when their precedence is less than or equal to the current operator. Apply operators on recently popped operands and push the result back to the values stack.

  7. Remaining Operators: After parsing, solve any remaining operators in the stack.

  8. Final Result: Extract and display the final result from the values stack.

Implementing an Undo Feature

An undo feature allows users to reverse their last action. This is commonly found in text editors, web browsers, and drawing applications. Here, we'll create a basic undo feature for a program that manipulates strings by adding characters. The operations will be stored in a stack, enabling us to easily reverse the last operation.

Step-by-Step Approach:

  1. Data Structure Choice: Use a stack to store actions.

  2. Define Operation: Each action corresponds to adding a character at a specific position.

  3. Push Operations onto Stack: As you modify the string, save the operation details onto the stack.

  4. Undo Operation:

    • Pop the stack to retrieve the last action.
    • Reverse the last action on the string.

Implementation Example in C:

Here's a small program to demonstrate this concept in a basic string editor:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

#define MAX 200

typedef struct {
    char str[MAX];
    int index;
} Action;

typedef struct {
    Action actions[MAX];
    int top;
} UndoStack;

void initUndoStack(UndoStack *stack) {
    stack->top = -1;
}

int isEmpty(UndoStack *stack) {
    return stack->top == -1;
}

void push(UndoStack *stack, Action action) {
    if(stack->top >= MAX - 1) {
        printf("UndoStack overflow\n");
        return;
    }
    stack->actions[++stack->top] = action;
}

Action pop(UndoStack *stack) {
    if(isEmpty(stack)) {
        printf("UndoStack is empty\n");
        Action emptyAction = {"", -1};
        return emptyAction;
    }
    return stack->actions[stack->top--];
}

void appendString(char *str, char ch) {
    int len = strlen(str);
    str[len] = ch;
    str[len + 1] = '\0';
}

void undo(UndoStack *stack, char *str) {
    if(isEmpty(stack)) {
        printf("No action to undo!\n");
        return;
    }

    Action lastAction = pop(stack);
    int len = strlen(str);
    if(len > lastAction.index) {
        str[lastAction.index] = '\0';
    }
}

int main() {
    char str[MAX] = "";
    UndoStack stack;
    initUndoStack(&stack);

    printf("Type 'add ch' to add character 'ch' to the end of the string.\n");
    printf("Type 'undo' to undo the last action.\n");

    char input[MAX];
    while(1) {
        printf("\nCurrent string: %s\n", str);
        printf("Enter command:\n");
        fgets(input, sizeof(input), stdin);
        input[strcspn(input, "\n")] = '\0'; // Remove newline character
        
        if(strcmp(input, "undo") == 0) {
            undo(&stack, str);
        }
        else if(strncmp(input, "add ", 4) == 0 && strlen(input) == 5) {  // add followed by one space and one char
            char ch = input[4];
            Action action;
            strcpy(action.str, str);
            action.index = strlen(action.str);
            push(&stack, action);
            appendString(str, ch);
        }
        else if(strcmp(input, "exit") == 0) {
            break;
        }
        else {
            printf("Invalid command!\n");
        }
    }
    return 0;
}

Step-by-Step Process:

  1. Initialize Action and UndoStack: Define Action structure to capture each modification and initialize UndoStack.

  2. String Modification Operations:

    • Prompt user with commands add ch to add a new character to the string.
    • Before adding, push the current state of the string and the next position where the character will be added onto the undo stack.
  3. Handle User Commands: Use a loop to accept commands. Check for add ch (where ch is a character) and undo.

  4. Perform Add Operation:

    • Extract the character from command.
    • Push current string and length onto undo stack.
    • Append the extracted character to the string.
  5. Perform Undo Operation:

    • Pop the newest Action from the undo stack.
    • Restore the string to its previous state by truncating it at the saved index.
  6. Exit Handling: Allow the user to type exit to quit the program.

Data Flow Overview

Expression Evaluation:

  • Input: String of an infix expression.
  • Process: Tokenization, operator precedence management, stack-based computation.
  • Output: Final computed integer result.

Undo Feature:

  • Input: User commands to modify a string.
  • Process: Store modifications in a stack, handle commands to add characters or undo actions.
  • Output: Updated string or revert to previous string upon undo commands.

By carefully organizing these steps and understanding the role of each data structure (stack in particular), you lay down the foundation necessary for building more complex applications requiring robust data handling techniques. Practice coding these examples to build your confidence and skill set.




Top 10 Questions and Answers on C Programming: Data Structures Applications, Expression Evaluation, Undo Feature

C programming is a foundational language that plays a crucial role in understanding fundamental topics such as data structures and their applications. Among various applications of data structures in C, expression evaluation and implementing an undo feature are particularly interesting exercises that highlight the power and versatility of these structures. Let's delve into the top 10 questions and answers related to these topics.

1. What are some common data structures used in C, and why are they important?

Answer: In C, common data structures include arrays, linked lists, stacks, queues, trees, graphs, and hash tables. These structures are essential because they help manage large volumes of data efficiently, making programs faster, more organized, and easier to understand. For example, stacks are useful for problems involving recursion or undo/redo operations, and hash tables provide very fast search capabilities which are beneficial in large datasets.

2. How can a stack be used in C to evaluate infix expressions to postfix (Reverse Polish Notation)?

Answer: A stack can be utilized in C to convert infix expressions to postfix by adhering to the Shunting Yard algorithm developed by Edsger Dijkstra. The basic idea is to use one stack to hold operators and another to build the postfix expression. As you scan through the infix expression from left to right, operands are added to the postfix expression directly, while operators are pushed onto the operator stack. Operator precedence and associativity rules dictate when and how operators are popped from the stack and appended to the postfix expression.

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <ctype.h>

#define MAX_SIZE 100

typedef struct {
    char items[MAX_SIZE];
    int top;
} Stack;

void initStack(Stack *s) {
    s->top = -1;
}

int isFull(Stack *s) {
    return s->top == MAX_SIZE - 1;
}

int isEmpty(Stack *s) {
    return s->top == -1;
}

void push(Stack *s, char item) {
    if (!isFull(s))
        s->items[++s->top] = item;
}

char pop(Stack *s) {
    if (!isEmpty(s))
        return s->items[s->top--];
    return '\0';
}

int precedence(char op) {
    switch (op) {
        case '+':
        case '-':
            return 1;
        case '*':
        case '/':
            return 2;
        case '^':
            return 3;
    }
    return 0;
}

int isOperator(char symbol) {
    return (symbol == '+' || symbol == '-' || symbol == '*' || symbol == '/');
}

char peek(Stack *s) {
    return s->items[s->top];
}

int main() {
    char infix[] = "a+b*(c^d-e)^(f+g*h)-i";
    char postfix[MAX_SIZE];
    Stack operatorStack;
    initStack(&operatorStack);

    int i, j = 0;
    for(i = 0; infix[i] !='\0'; i++) {
        char symbol = infix[i];

        if(isalnum(symbol)) {
            postfix[j++] = symbol;
        } else if (symbol == '(') {
            push(&operatorStack, symbol);
        } else if (symbol == ')') {
            while(!isEmpty(&operatorStack) && peek(&operatorStack) != '(') {
                postfix[j++] = pop(&operatorStack);
            }
            pop(&operatorStack); // pop '('
        } else if(isOperator(symbol)) {
            while(!isEmpty(&operatorStack) && precedence(peek(&operatorStack)) >= precedence(symbol)) {
                postfix[j++] = pop(&operatorStack);
            }
            push(&operatorStack, symbol);
        }
    }

    while(!isEmpty(&operatorStack)) {
        postfix[j++] = pop(&operatorStack);
    }

    postfix[j] = '\0';
    printf("Postfix: %s\n", postfix);

    return 0;
}

3. Can you explain how to evaluate a postfix expression using a stack in C?

Answer: Evaluating a postfix expression in C involves using a stack to store operands. As you scan through the expression from left to right, each operand is pushed onto the stack. When an operator is encountered, the required number of operands (typically two) are popped from the stack. The result of the operation is then pushed back onto the stack. At the end of the expression, the only value remaining on the stack will be the final result.

#include <stdio.h>
#include <stdlib.h>
#include <ctype.h>

#define MAX_SIZE 100

typedef struct {
    int items[MAX_SIZE];
    int top;
} Stack;

void initStack(Stack *s) {
    s->top = -1;
}

int isFull(Stack *s) {
    return s->top == MAX_SIZE - 1;
}

int isEmpty(Stack *s) {
    return s->top == -1;
}

void push(Stack *s, int item) {
    if (!isFull(s))
        s->items[++s->top] = item;
}

int pop(Stack *s) {
    if (!isEmpty(s))
        return s->items[s->top--];
    return -1;
}

int evaluatePostfixExpression(char* postfix) {
    Stack evalStack;
    initStack(&evalStack);

    int i;
    for (i = 0; postfix[i]; i++) {
        char symbol = postfix[i];

        if (isdigit(symbol)) {
            push(&evalStack, symbol - '0');
        } else if(isOperator(symbol)) {
            int operand2 = pop(&evalStack);
            int operand1 = pop(&evalStack);
            int result;
            
            switch(symbol) {
                case '+':
                    result = operand1 + operand2;
                    break;
                case '-':
                    result = operand1 - operand2;
                    break;
                case '*':
                    result = operand1 * operand2;
                    break;
                case '/':
                    result = operand1 / operand2;
                    break;
                default: 
                    result = 0;
            }
            push(&evalStack, result);
        }
    }

    return pop(&evalStack);
}

int isOperator(char symbol) {
    return symbol == '+' || symbol == '-' || symbol == '*' || symbol == '/';
}

int main() {
    char postfix[] = "4572+-*";
    int result = evaluatePostfixExpression(postfix);
    printf("Result: %d\n", result);

    return 0;
}

4. What are the advantages and disadvantages of using linked lists over arrays in C?

Answer: Linked lists and arrays have distinct characteristics:

  • Advantages of Linked Lists:

    • Dynamic size: Memory allocation and deallocation occur runtime according to user requirement.
    • Insertions and deletions in the middle of the list are easy.
    • No need for contiguous memory allocation.
  • Disadvantages of Linked Lists:

    • Increased memory usage due to storing pointers.
    • Direct access not supported, hence accessing an element takes O(n) time complexity.
    • Random access is slow because it requires sequential traversal of elements.

On the other hand, arrays:

  • Advantages:

    • Contiguous memory allocation makes accessing elements faster with an O(1) time complexity.
    • Simple to implement and can be used for fixed-size data.
  • Disadvantages:

    • Size fixed at compile time.
    • Insertions and deletions can be costly, especially for large arrays.

5. How would you implement a stack-based solution for a problem where you need to reverse a string in C?

Answer: Reversing a string using a stack in C is straightforward. You can push all characters of the string onto a stack and then pop them out one by one into a new string. Here’s a simple implementation:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

#define MAX_SIZE 100

typedef struct {
    char items[MAX_SIZE];
    int top;
} Stack;

void initStack(Stack *s) {
    s->top = -1;
}

int isFull(Stack *s) {
    return s->top == MAX_SIZE - 1;
}

int isEmpty(Stack *s) {
    return s->top == -1;
}

void push(Stack *s, char item) {
    if (!isFull(s))
        s->items[++s->top] = item;
}

char pop(Stack *s) {
    if (!isEmpty(s))
        return s->items[s->top--];
    return '\0';
}

void reverseString(const char* input) {
    Stack s;
    initStack(&s);
    int i;

    for (i = 0; i < strlen(input); i++) {
        push(&s, input[i]);
    }

    printf("Reversed string: ");
    while (!isEmpty(&s)) {
        printf("%c", pop(&s));
    }

    printf("\n");
}

int main() {
    char str[] = "Hello, world!";
    reverseString(str);

    return 0;
}

6. What is a queue in C, and how can it be implemented?

Answer: A queue is a linear data structure that follows the FIFO (First In First Out) principle. Elements are added at the rear end (enqueue) and removed from the front end (dequeue). Queues can be implemented using arrays or linked lists. Here is an example using an array:

#include <stdio.h>
#define MAX_SIZE 100

typedef struct {
    int items[MAX_SIZE];
    int head;
    int tail;
    int count;
} Queue;

void initQueue(Queue *q) {
    q->count = 0;
    q->head = 0;
    q->tail = -1;
}

int isFull(Queue *q) {
    return q->count == MAX_SIZE;
}

int isEmpty(Queue *q) {
    return q->count == 0;
}

void enqueue(Queue *q, int item) {
    if (!isFull(q)) {
        q->items[++q->tail] = item;
        q->count++;
    }
}

int dequeue(Queue *q) {
    if (!isEmpty(q)) {
        q->count--;
        return q->items[q->head++];
    }
    return -1;
}

int main() {
    Queue q;
    initQueue(&q);

    enqueue(&q, 10);
    enqueue(&q, 20);
    enqueue(&q, 30);

    printf("Dequeued: %d\n", dequeue(&q));     
    printf("Dequeued: %d\n", dequeue(&q));

    return 0;
}

7. What is the difference between a singly linked list and a doubly linked list in C?

Answer: The primary difference between a singly linked list and a doubly linked list lies in their node structure and the operations they allow:

  • Singly Linked List: Every node contains only one pointer that points to the next node. This makes it simpler to implement but more restrictive during traversal since you cannot go backward without re-traversing the list from the start.
  • Doubly Linked List: Each node contains two pointers—one pointing to the next node and the other pointing to the previous node. This allows bidirectional traversal, which can be advantageous in certain scenarios, albeit with increased memory usage per node.

8. How can a tree data structure be utilized to sort data efficiently in C?

Answer: Trees, particularly binary search trees (BSTs), are used for sorting data efficiently by maintaining a specific ordering property. In a BST, for any given node, all elements in the left subtree are smaller, and all elements in the right subtree are larger. By inserting elements into a BST, the data gets sorted inherently. Later, an in-order traversal of the BST yields the elements in sorted order.

9. What is an AVL Tree, and why is it important in C?

Answer: An AVL tree is a self-balancing binary search tree where the difference between the heights of the left and right subtrees is no more than one for all nodes. This ensures that operations like insertion, deletion, and lookup run in O(log n) time complexity, making AVL trees critical for applications requiring balanced and efficient data search and retrieval.

10. How can you implement an undo feature in a text editor using stacks in C?

Answer: Implementing an undo feature in a text editor involves tracking user actions and reverting changes as necessary. A stack is well-suited for representing the sequence of actions performed. Each action (like insert character, delete line) can be stored on a stack as a command object. When the user chooses to undo an action, the command is popped from the stack and its execution is reversed (or undone). Optionally, the undone commands can also be pushed onto a separate “redo” stack to enable redo functionality. Here’s a simplified version:

#include <stdio.h>
#include <string.h>

#define MAX_ACTION_BUFFER 1000
#define MAX_ACTIONS       100

typedef struct {
    char buffer[MAX_ACTION_BUFFER];
    int index;
} Action;

typedef struct {
    Action items[MAX_ACTIONS];
    int top;
} Stack;

void initStack(Stack *s) {
    s->top = -1;
}

int isFull(Stack *s) {
    return s->top == MAX_ACTIONS - 1;
}

int isEmpty(Stack *s) {
    return s->top == -1;
}

void push(Stack *s, Action item) {
    if (!isFull(s))
        s->items[++s->top] = item;
}

Action pop(Stack *s) {
    if (!isEmpty(s))
        return s->items[s->top--];
    
    // Return an action with -1 index indicating error.
    Action invalidAction = { "", -1 }; 
    return invalidAction;
}

void performAction(Action action, char* content) {
    // This function simulates performing an action.
    // In a real application, it could involve modifying files, buffers etc.
    // Here we just display action details.
    printf("Performed Action - Index: %d, Buffer: %s\n", action.index, action.buffer);
    // Append the action buffer to the content.
    strcat(content, action.buffer);
}

void undoAction(Stack *undoStack, char* content) {
    if (!isEmpty(undoStack)) {
        Action action = pop(undoStack);
        printf("Undone Action - Index: %d, Buffer: %s\n", action.index, action.buffer);
        // Here we simulate undoing an action; for this example, we'll just truncate 'content' based on action's index.
        content[action.index] = '\0';
    } else {
        printf("Nothing to undo.\n");
    }
}

void recordAction(Stack *undoStack, char* content) {
    Action newAction;
    strcpy(newAction.buffer, "Insert text here"); 
    newAction.index = strlen(content); // Record current position.

    // Simulate recording action before performing it.
    printf("Recording Action - Index: %d, Buffer: %s\n", newAction.index, newAction.buffer);
    push(undoStack, newAction);
}

int main() {
    char content[MAX_ACTION_BUFFER] = "";
    Stack undoStack;
    initStack(&undoStack);

    recordAction(&undoStack, content);
    performAction(undoStack.items[undoStack.top], content);
    printf("Current Content: %s\n", content);

    recordAction(&undoStack, content);
    performAction(undoStack.items[undoStack.top], content);
    printf("Current Content: %s\n", content);

    undoAction(&undoStack, content);
    printf("Current Content after Undo: %s\n", content);

    undoAction(&undoStack, content);
    printf("Current Content after Undo: %s\n", content);

    undoAction(&undoStack, content);
    printf("Current Content after Undo: %s\n", content);

    return 0;
}

This simplified model demonstrates how actions might be managed to enable undo functionality through stacks in C.

Conclusion

Data structures form the backbone of efficient programming, and understanding how to use them for tasks such as expression evaluation and feature implementation like undo is crucial. Whether you're converting infix expressions to postfix or planning features for a sophisticated text editor, mastering these concepts and their applications in C can significantly improve your problem-solving capabilities. Always remember to choose the data structure that best fits the needs of the problem at hand, considering the trade-offs in terms of memory usage, speed, and complexity.