INTRODUCTION OF STACK IN DATA STRUCTURE

 INTRODUCTION OF STACK IN DATA STRUCTURE

A stack is a fundamental data structure in computer science that follows the Last In, First Out (LIFO) principle.

In a stack, the last element added is the first one to be removed.

A stack, is a collection of items where new items are added or removed from the same end, commonly referred to as the "top" of the stack.




Pictures:- From web resource 

Key characteristics of a stack: (Features of the stack in the data structure.)

The stack is a fundamental data structure with several key features that make it useful in various computing applications.

The main features of a stack:

 1. Last In, First Out (LIFO) Principle:

   - The primary characteristic of a stack is that it follows the Last In, First Out (LIFO) order. The last element added to the stack is the first one to be removed. This behavior is similar to a stack of plates or a pile of books.

 2. Dynamic Size:

   - Stacks can dynamically grow or shrink in size as elements are pushed onto or popped off the stack. The size is not fixed, and it can change during the program's execution.

 3. Operations:

   - Push Operation: Adds an element to the top of the stack.

   - Pop Operation: Removes the element from the top of the stack.

   - Peek or Top Operation: Retrieves the element at the top of the stack without removing it.

   - IsEmpty Operation: Checks if the stack is empty.

   - Size Operation: Returns the number of elements in the stack.

 4. Applications in Memory Management:

   - Stacks are crucial for managing function calls and local variables in memory during program execution. The call stack is a specific implementation of a stack in memory.

 5. Expression Evaluation:

   - Stacks are used for evaluating expressions, especially in converting infix expressions to postfix or prefix form. The stack helps maintain the correct order of operations.

 6. Undo Mechanisms:

   - Stacks are employed in applications to implement undo functionalities. Each action is recorded on the stack, allowing for easy reversal.

 7. Backtracking:

   - In algorithms and recursion, stacks are often used to store the state for backtracking. The stack allows the program to return to previous states or choices.

 8. Data Storage in Reverse Order:

   - Stacks are used to reverse the order of data. For example, if you push a sequence of elements onto a stack and then pop them off, you get the elements in reverse order.

 9. Efficient Implementation:

   - Stacks can be implemented using arrays or linked lists. Both implementations provide efficient access to the top of the stack, making push-and-pop operations fast.

 10. Nested Structures:

    - Stacks are used to manage nested structures, such as nested function calls or nested loops. Each level of nesting corresponds to a level in the stack.

 11. Memory Efficiency:

    - Stacks have relatively low memory overhead, making them efficient for certain applications where memory usage is a concern.

 12. Clear and Simple Abstraction:

    - The simplicity of the stack's operations makes it a clear and straightforward abstraction. This simplicity is valuable in algorithm design and understanding.

----------------------------------------------------

 

Advantage of the stack 

Stacks offer several advantages in data structure and algorithm design, making them a valuable and versatile tool in various computing applications. Here are some of the key advantages of using stacks:

 1. Last In, First Out (LIFO) Order:

   - The LIFO property of stacks simplifies the organization of data, making it easier to manage and retrieve the most recently added elements.

 2. Simple and Efficient Operations:

   - Stack operations (push, pop, peek) are simple and can be implemented efficiently using arrays or linked lists. This simplicity leads to faster execution and better performance.

 3. Memory Efficiency:

   - Stacks have low memory overhead, as they only need to store the elements and a reference to the top of the stack. This efficiency is crucial in memory-constrained environments.

4. Function Call Management:

   - Stacks are used to manage function calls and local variables during program execution. Each function call and its associated data are pushed onto the call stack, facilitating function invocation and return.

 5. Expression Evaluation:

   - Stacks are employed in evaluating expressions, especially in converting infix expressions to postfix or prefix form. This simplifies the order of operations and enhances the efficiency of expression evaluation algorithms.

 6. Undo Mechanism:

   - Stacks are essential for implementing undo functionalities in applications. Each action is recorded on the stack, allowing users to reverse the sequence of operations.

 7. Backtracking:

   - In algorithms and recursive procedures, stacks are used to store the state of the program. This enables backtracking, allowing the program to revisit and explore alternative paths or choices.

 8. Nested Structures:

   - Stacks are used to manage nested structures, such as nested function calls, loops, and conditional statements. Each level of nesting corresponds to a level in the stack.

 9. Data Reversal:

   - Stacks provide an easy and efficient way to reverse the order of data. By pushing elements onto a stack and then popping them off, the elements are retrieved in reverse order.

 10. History Tracking:

    - Stacks can be employed to maintain a history of operations or user interactions. This is useful in applications where users may need to navigate backward through a sequence of actions.

 11. Parsing and Syntax Checking:

    - Stacks are utilized in parsing algorithms and syntax checking to ensure the correct nesting of symbols and the validity of expressions.

 12. Memory Management:

    - In embedded systems and resource-constrained environments, stacks are advantageous for managing limited memory efficiently.

 13. Algorithmic Simplicity:

    - The use of stacks often simplifies the design and implementation of algorithms, making them easier to understand and maintain.

---------------------------------------------

 

The disadvantage of the stack 

some of the disadvantages of using stacks in data structures:

 

1. Limited Access:

   - Stacks provide access to only the top element. If there is a need to access elements in the middle or at the bottom of the stack, a different data structure, such as an array or linked list, may be more suitable.

 

2. Fixed Size (in Array Implementation):

   - In array-based implementations of stacks, the size of the stack is often fixed at the time of creation. This limitation can lead to overflow or underflow issues if the number of elements exceeds the predefined size.

 

3. No Random Access:

   - Stacks do not support random access to elements. Accessing elements at arbitrary positions requires popping elements off the stack until the desired element is reached, which can be inefficient.

 

4. Memory Constraints:

   - Stacks are not suitable for storing large datasets or when memory efficiency is a critical concern. The dynamic nature of certain data structures, like linked lists, may be more appropriate in such cases.

 

5. Function Call Overhead:

   - While stacks efficiently manage function calls, excessive recursion or deep function call hierarchies can lead to a stack overflow, causing the program to crash. Some programming languages have limitations on the maximum stack depth.

 

6. Limited Search Capability:

   - Searching for a specific element in a stack requires popping elements until the desired element is found. This linear search can be inefficient compared to other data structures that support direct access or have better search capabilities.

 

7. Sequential Processing:

   - Stacks are inherently sequential structures, and parallel processing or concurrent access to elements is not straightforward. Other data structures, like queues, may be more suitable for parallel processing.

 

8. Inefficient for Some Operations:

   - Certain operations, such as finding the middle element or reversing the order of elements, can be less efficient when using a stack compared to other data structures designed for these specific tasks.

 

9. Difficulty in Handling Multiple Data Types:

   - Stacks are typically designed to store elements of a single data type. Handling multiple data types in a stack can be challenging and may require additional type-checking mechanisms.

 

10. Overhead in Dynamic Memory Allocation:

    - In linked list implementations, dynamic memory allocation for each node can introduce overhead, leading to increased memory usage and potential fragmentation.

 

11. Potential for Stack Overflow:

    - In recursive algorithms or deep function call chains, there is a risk of a stack overflow if the available stack space is exhausted. This can lead to program termination.

----------------------------------------

 

Applications of the stack 

Stacks find applications in various areas of computer science and software development due to their Last In, First Out (LIFO) nature and efficient operations.

Some common applications of stacks:

 

1. Function Call Management:

   - Stacks are used to manage function calls and local variables in programs. When a function is called, its parameters and local variables are pushed onto the stack, and when the function completes, the stack is popped.

 

2. Expression Evaluation:

   - Stacks are utilized in evaluating expressions, especially in converting infix expressions to postfix or prefix form. This simplifies the order of operations and enhances the efficiency of expression evaluation algorithms.

 

3. Undo Mechanism:

   - Stacks are essential for implementing undo functionalities in applications. Each action is recorded on the stack, allowing users to reverse the sequence of operations.

 

4. Backtracking:

   - Stacks are used to store the state of a program, enabling backtracking in algorithms. This is particularly useful in search algorithms and puzzles.

 

5. Memory Management:

   - The call stack is employed for memory management in programming languages. It keeps track of function calls, parameters, and local variables, allowing for efficient memory allocation and deallocation.

 

6. Syntax Parsing:

   - Stacks are used in the parsing phase of compilers to check the correctness of the syntax in source code. They help ensure that opening and closing brackets, braces, and parentheses are correctly matched.

 

7. Algorithm Implementations:

   - Stacks are used in various algorithms, such as depth-first search (DFS) in graph traversal, where they help in tracking and backtracking through the nodes.

 

8. Parenthesis Matching:

   - Stacks are employed to check the correctness of parenthesis expressions by keeping track of the opening and closing brackets.

 

9. Expression Conversion:

   - Stacks are used to convert expressions between different forms, such as converting infix expressions to postfix or prefix notation.

 

10. History Tracking:

    - Stacks can be used to maintain a history of operations or user interactions. This is useful in applications where users may need to navigate backward through a sequence of actions.

 

11. Task Scheduling:

    - Stacks are used in managing task scheduling in operating systems. For example, when interrupts occur, the system can save the current state on the stack before handling the interrupt.

 

12. Text Editors:

    - Stacks are utilized in text editors to keep track of the cursor position and maintain a history of changes for undo and redo functionalities.

 

13. Expression Matching:

    - Stacks are used in matching delimiters and expressions, such as validating HTML tags or ensuring proper nesting in programming constructs.

 

14. Postfix Evaluation:

    - Stacks are employed in evaluating postfix expressions efficiently. The operands are pushed onto the stack, and operators are applied when encountered.

 

15. Browser History:

    - Stacks are used to implement the back and forward navigation in web browsers, allowing users to revisit previously viewed pages.

 

These applications demonstrate the versatility of stacks in solving various problems across different domains. The simplicity and efficiency of stack operations make them a fundamental and widely used data structure.

----------------------------------------

 Structure of stack

The structure of a stack in a computer's memory can be implemented using either an array or a linked list. Components and characteristics of a stack structure:

 

 Array-Based Implementation:

In an array-based implementation, a fixed-size array is used to represent the stack.

Pictures:- From web resource 
 The key components include:

 

1. Array:

   - An array to hold the elements of the stack.

   - The size of the array is fixed during the stack's creation.

 

2. Top Pointer:

   - A variable (often called "top") that keeps track of the index of the top element in the stack.

   - Initially set to -1 when the stack is empty.

 

3. Operations:

   - Push Operation:

     - Increment the top pointer and place the new element at the updated top index.

   - Pop Operation:

     - Remove the element at the top index and decrement the top pointer.

   - Peek (or Top) Operation:

     - Retrieve the element at the top index without modifying the stack.

 

 Linked List-Based Implementation:

In a linked list-based implementation, a linked list is used to represent the stack.


Pictures:- From web resource 

The key components include:

 

1. Node:

   - A node structure that holds the data element and a pointer to the next node.

   - The top of the stack corresponds to the head of the linked list.

 

2. Top Pointer:

   - A variable (often called "top") that points to the head node of the linked list.

   - Initially set to null when the stack is empty.

 

3. Operations:

   - Push Operation:

     - Create a new node, set its data, and make it the new head of the linked list. Update the top pointer.

   - Pop Operation:

     - Remove the head node from the linked list. Update the top pointer to the next node.

   - Peek (or Top) Operation:

     - Retrieve the data from the head node without modifying the stack.

 

Example of a stack structure in C++ using an array-based implementation:

#include <iostream>

#define MAX_SIZE 100

 

class Stack {

private:

    int arr[MAX_SIZE];

    int top;

 

public:

    Stack() {

        top = -1;

    }

 

    void push(int value) {

        if (top < MAX_SIZE - 1) {

            arr[++top] = value;

            std::cout << "Pushed: " << value << std::endl;

        } else {

            std::cout << "Stack Overflow: Cannot push " << value << "." << std::endl;

        }

    }

 

    void pop() {

        if (top >= 0) {

            std::cout << "Popped: " << arr[top--] << std::endl;

        } else {

            std::cout << "Stack Underflow: Cannot pop from an empty stack." << std::endl;

        }

    }

 

    int peek() {

        if (top >= 0) {

            return arr[top];

        } else {

            std::cout << "Stack is empty." << std::endl;

            return -1; // Placeholder for an empty stack

        }

    }

};

 

int main() {

    Stack stack;

 

    stack.push(1);

    stack.push(2);

    stack.push(3);

 

    std::cout << "Top element: " << stack.peek() << std::endl;

 

    stack.pop();

    stack.pop();

    stack.pop();

    stack.pop(); // Trying to pop from an empty stack

 

    return 0;

}

------------------------------------------

Basic overview of the operations:

  ----------------------------------------

In a stack data structure, several primitive operations are commonly defined to manipulate and manage the stack.

The fundamental operations include:

 

1. Push Operation:

   - Description: Adds an element to the top of the stack.

   - Algorithm:

          Algorithm Push(stack, element):

         1. Increment the top pointer.

         2. Set the value of the top index to the new element.

    

2. Pop Operation:

   - Description: Removes the element from the top of the stack.

   - Algorithm:

          Algorithm Pop(stack):

         1. If the stack is not empty:

              a. Retrieve the value at the top index.

              b. Decrement the top pointer.

              c. Return the retrieved value.

         2. Otherwise, indicate that the stack is empty.

    

 

3. Peek (or Top) Operation:

   - Description: Retrieves the element at the top of the stack without removing it.

   - Algorithm:

          Algorithm Peek(stack):

         1. If the stack is not empty:

              a. Return the value at the top index.

         2. Otherwise, indicate that the stack is empty.

    

 

4. IsEmpty Operation:

   - Description: Checks if the stack is empty.

   - Algorithm:

          Algorithm IsEmpty(stack):

         1. Return true if the top pointer is -1 (indicating an empty stack); otherwise, return false.

    

 

5. Size Operation:

   - Description: Returns the number of elements currently in the stack.

   - Algorithm:

          Algorithm Size(stack):

         1. Return the value of the top pointer + 1.

     ------------------------------

Examples of primitive operations of the stack  according to c++ and Python

C++ Example:

#include <iostream>

#define MAX_SIZE 5

 

class Stack {

private:

    int arr[MAX_SIZE];

    int top;

 

public:

    Stack() {

        top = -1;

    }

 

    void push(int element) {

        if (top < MAX_SIZE - 1) {

            arr[++top] = element;

            std::cout << "Pushed: " << element << std::endl;

        } else {

            std::cout << "Stack Overflow: Cannot push " << element << "." << std::endl;

        }

    }

 

    int pop() {

        if (top >= 0) {

            int poppedValue = arr[top--];

            std::cout << "Popped: " << poppedValue << std::endl;

            return poppedValue;

        } else {

            std::cout << "Stack Underflow: Cannot pop from an empty stack." << std::endl;

            return -1; // Placeholder for an empty stack

        }

    }

 

    int peek() {

        if (top >= 0) {

            return arr[top];

        } else {

            std::cout << "Stack is empty." << std::endl;

            return -1; // Placeholder for an empty stack

        }

    }

 

    bool isEmpty() {

        return top == -1;

    }

 

    int size() {

        return top + 1;

    }

};

 

int main() {

    Stack stack;

 

    stack.push(1);

    stack.push(2);

    stack.push(3);

 

    std::cout << "Top element: " << stack.peek() << std::endl;

 

    stack.pop();

    stack.pop();

    stack.pop();

    stack.pop(); // Trying to pop from an empty stack

 

    return 0;

}

-------------------------------------------

 

 Python Example:

class Stack:

    def __init__(self):

        self.arr = []

   

    def push(self, element):

        self.arr.append(element)

        print(f"Pushed: {element}")

 

    def pop(self):

        if not self.is_empty():

            popped_value = self.arr.pop()

            print(f"Popped: {popped_value}")

            return popped_value

        else:

            print("Stack Underflow: Cannot pop from an empty stack.")

            return None  # Placeholder for an empty stack

   

    def peek(self):

        if not self.is_empty():

            return self.arr[-1]

        else:

            print("Stack is empty.")

            return None  # Placeholder for an empty stack

   

    def is_empty(self):

        return len(self.arr) == 0

 

    def size(self):

        return len(self.arr)

 

# Example Usage

stack = Stack()

 

stack.push(1)

stack.push(2)

stack.push(3)

 

print("Top element:", stack.peek())

 

stack.pop()

stack.pop()

stack.pop()

stack.pop()  # Trying to pop from an empty stack

 

-------------------------------------------------

INFIX, POSTFIX, PREFIX IN STACK.

In computer science and mathematics, infix, postfix, and prefix notations are ways to represent mathematical expressions.

These notations describe the placement of operators and operands within an expression.

Stacks are often used in the conversion between these notations.

 

 1. Infix Notation:

In infix notation, operators are written between their operands. This is the typical mathematical notation that we use, where operators are placed in the middle of expressions.

 

Example:

Infix:  3 + 4 * (2 - 7)

 

 2. Postfix (or Reverse Polish Notation) Notation:

In postfix notation, also known as Reverse Polish Notation (RPN), operators are placed after their operands. To evaluate a postfix expression, you can use a stack data structure.

 

Example:

Postfix: 3 4 2 7 - * +

 

 3. Prefix (or Polish Notation) Notation:

In prefix notation, also known as Polish Notation, operators are placed before their operands. Similar to postfix notation, a stack can be used to evaluate a prefix expression.

 

Example:

Prefix: + 3 * 4 - 2 7

 

 Conversion between Notations:

1. Infix to Postfix:

   - Use a stack to convert an infix expression to postfix.

The stack helps maintain the correct order of operators.

   - Algorithm:

     - Scan the infix expression from left to right.

     - Use a stack to keep track of operators.

     - Output operands as they arrive.

     - For each operator encountered, pop operators from the stack and output them until the stack is empty or an operator with lower precedence is encountered. Then, push the current operator onto the stack.

     - At the end, pop any remaining operators from the stack and output them.

 

2. Infix to Prefix:

   - Similar to infix to postfix conversion but in reverse order. Use a stack to keep track of operators and operands.

   - Algorithm:

     - Reverse the infix expression.

     - Use a stack to convert the reversed infix expression to postfix.

     - Reverse the result obtained to get the final prefix expression.

 

3. Postfix to Infix:

   - Use a stack to convert a postfix expression to infix. Operand values are pushed onto the stack, and operators are popped and applied when encountered.

   - Algorithm:

     - Scan the postfix expression from left to right.

     - Use a stack to keep track of operands.

     - When an operand is encountered, push it onto the stack.

     - When an operator is encountered, pop the required number of operands from the stack, apply the operator, and push the result back onto the stack.

 

4. Prefix to Infix:

   - Similar to postfix to infix conversion but in reverse order.

   - Algorithm:

     - Reverse the prefix expression.

     - Use a stack to convert the reversed prefix expression to postfix.

     - Reverse the result obtained to get the final infix expression.

 

These notations are useful in evaluating expressions and can be preferred in certain situations, such as for parsing or when building calculators.

----------------------------------

Examples of Infix, postfix, prefix in stack with c++ and python

 

 C++ Examples: # Infix to Postfix Conversion:

#include <iostream>

#include <stack>

#include <unordered_map>

 

bool isOperator(char c) {

    return c == '+' || c == '-' || c == '*' || c == '/';

}

 

int getPrecedence(char op) {

    std::unordered_map<char, int> precedence;

    precedence['+'] = precedence['-'] = 1;

    precedence['*'] = precedence['/'] = 2;

    return precedence[op];

}

 

std::string infixToPostfix(const std::string& infix) {

    std::stack<char> stack;

    std::string postfix;

 

    for (char c : infix) {

        if (isalnum(c)) {

            postfix += c; // Operand, append to the result

        } else if (c == '(') {

            stack.push(c);

        } else if (c == ')') {

            while (!stack.empty() && stack.top() != '(') {

                postfix += stack.top();

                stack.pop();

            }

            stack.pop(); // Discard '('

        } else if (isOperator(c)) {

            while (!stack.empty() && getPrecedence(stack.top()) >= getPrecedence(c)) {

                postfix += stack.top();

                stack.pop();

            }

            stack.push(c);

        }

    }

 

    while (!stack.empty()) {

        postfix += stack.top();

        stack.pop();

    }

 

    return postfix;

}

 

int main() {

    std::string infixExpression = "3 + 4 * (2 - 7)";

    std::string postfixExpression = infixToPostfix(infixExpression);

 

    std::cout << "Infix Expression: " << infixExpression << std::endl;

    std::cout << "Postfix Expression: " << postfixExpression << std::endl;

 

    return 0;

}

 

# Postfix to Infix Conversion:

#include <iostream>

#include <stack>

 

bool isOperator(char c) {

    return c == '+' || c == '-' || c == '*' || c == '/';

}

 

std::string postfixToInfix(const std::string& postfix) {

    std::stack<std::string> stack;

 

    for (char c : postfix) {

        if (isalnum(c)) {

            stack.push(std::string(1, c)); // Operand, push to stack

        } else if (isOperator(c)) {

            std::string operand2 = stack.top();

            stack.pop();

            std::string operand1 = stack.top();

            stack.pop();

            std::string result = "(" + operand1 + c + operand2 + ")";

            stack.push(result);

        }

    }

 

    return stack.top();

}

 

int main() {

    std::string postfixExpression = "3427-*+";

    std::string infixExpression = postfixToInfix(postfixExpression);

 

    std::cout << "Postfix Expression: " << postfixExpression << std::endl;

    std::cout << "Infix Expression: " << infixExpression << std::endl;

 

    return 0;

}

 

  Python Examples: # Infix to Postfix Conversion:

def infix_to_postfix(infix):

    stack = []

    postfix = ""

    precedence = {'+': 1, '-': 1, '*': 2, '/': 2}

 

    for c in infix:

        if c.isalnum():

            postfix += c

        elif c == '(':

            stack.append(c)

        elif c == ')':

            while stack and stack[-1] != '(':

                postfix += stack.pop()

            stack.pop()  # Discard '('

        elif c in precedence:

            while stack and precedence[stack[-1]] >= precedence[c]:

                postfix += stack.pop()

            stack.append(c)

 

    while stack:

        postfix += stack.pop()

 

    return postfix

 

infix_expression = "3 + 4 * (2 - 7)"

postfix_expression = infix_to_postfix(infix_expression)

 

print("Infix Expression:", infix_expression)

print("Postfix Expression:", postfix_expression)

 

# Postfix to Infix Conversion:

def postfix_to_infix(postfix):

    stack = []

 

    for c in postfix:

        if c.isalnum():

            stack.append(c)

        elif c in {'+', '-', '*', '/'}:

            operand2 = stack.pop()

            operand1 = stack.pop()

            result = f"({operand1}{c}{operand2})"

            stack.append(result)

 

    return stack[0]

 

postfix_expression = "3427-*+"

infix_expression = postfix_to_infix(postfix_expression)

 

print("Postfix Expression:", postfix_expression)

print("Infix Expression:", infix_expression)

-----------------------------------------------------

 

Conversions of Infix, postfix, prefix.

In Python implement functions for each conversion:

 

 Infix to Postfix Conversion:

def infix_to_postfix(infix):

    stack = []

    postfix = ""

    precedence = {'+': 1, '-': 1, '*': 2, '/': 2}

 

    for c in infix:

        if c.isalnum():

            postfix += c

        elif c == '(':

            stack.append(c)

        elif c == ')':

            while stack and stack[-1] != '(':

                postfix += stack.pop()

            stack.pop()  # Discard '('

        elif c in precedence:

            while stack and precedence[stack[-1]] >= precedence[c]:

                postfix += stack.pop()

            stack.append(c)

 

    while stack:

        postfix += stack.pop()

 

    return postfix

 

# Example

infix_expression = "3 + 4 * (2 - 7)"

postfix_expression = infix_to_postfix(infix_expression)

print("Infix to Postfix:")

print("Infix Expression:", infix_expression)

print("Postfix Expression:", postfix_expression)

 

 Infix to Prefix Conversion:

def infix_to_prefix(infix):

    stack = []

    prefix = ""

    precedence = {'+': 1, '-': 1, '*': 2, '/': 2}

 

    for c in infix[::-1]:  # Reverse the infix expression

        if c.isalnum():

            prefix += c

        elif c == ')':

            stack.append(c)

        elif c == '(':

            while stack and stack[-1] != ')':

                prefix += stack.pop()

            stack.pop()  # Discard ')'

        elif c in precedence:

            while stack and precedence[stack[-1]] > precedence[c]:

                prefix += stack.pop()

            stack.append(c)

 

    while stack:

        prefix += stack.pop()

 

    return prefix[::-1]  # Reverse the result to get the prefix expression

 

# Example

infix_expression = "3 + 4 * (2 - 7)"

prefix_expression = infix_to_prefix(infix_expression)

print("\nInfix to Prefix:")

print("Infix Expression:", infix_expression)

print("Prefix Expression:", prefix_expression)

 

 Postfix to Infix Conversion:

def postfix_to_infix(postfix):

    stack = []

 

    for c in postfix:

        if c.isalnum():

            stack.append(c)

        elif c in {'+', '-', '*', '/'}:

            operand2 = stack.pop()

            operand1 = stack.pop()

            result = f"({operand1}{c}{operand2})"

            stack.append(result)

 

    return stack[0]

 

# Example

postfix_expression = "3427-*+"

infix_expression = postfix_to_infix(postfix_expression)

print("\nPostfix to Infix:")

print("Postfix Expression:", postfix_expression)

print("Infix Expression:", infix_expression)

 

 Prefix to Infix Conversion:

def prefix_to_infix(prefix):

    stack = []

 

    for c in reversed(prefix):  # Reverse the prefix expression

        if c.isalnum():

            stack.append(c)

        elif c in {'+', '-', '*', '/'}:

            operand1 = stack.pop()

            operand2 = stack.pop()

            result = f"({operand1}{c}{operand2})"

            stack.append(result)

 

    return stack[0]

 

# Example

prefix_expression = "+3*42-7"

infix_expression = prefix_to_infix(prefix_expression)

print("\nPrefix to Infix:")

print("Prefix Expression:", prefix_expression)

print("Infix Expression:", infix_expression)

 

-----------------------------------------------

Examples of Conversions of Infix, postfix, prefix in stack with c++ and python.

 C++ Examples: # Infix to Postfix Conversion:

#include <iostream>

#include <stack>

#include <unordered_map>

 

bool isOperator(char c) {

    return c == '+' || c == '-' || c == '*' || c == '/';

}

 

int getPrecedence(char op) {

    std::unordered_map<char, int> precedence;

    precedence['+'] = precedence['-'] = 1;

    precedence['*'] = precedence['/'] = 2;

    return precedence[op];

}

 

std::string infixToPostfix(const std::string& infix) {

    std::stack<char> stack;

    std::string postfix;

 

    for (char c : infix) {

        if (isalnum(c)) {

            postfix += c; // Operand, append to the result

        } else if (c == '(') {

            stack.push(c);

        } else if (c == ')') {

            while (!stack.empty() && stack.top() != '(') {

                postfix += stack.top();

                stack.pop();

            }

            stack.pop(); // Discard '('

        } else if (isOperator(c)) {

            while (!stack.empty() && getPrecedence(stack.top()) >= getPrecedence(c)) {

                postfix += stack.top();

                stack.pop();

            }

            stack.push(c);

        }

    }

 

    while (!stack.empty()) {

        postfix += stack.top();

        stack.pop();

    }

 

    return postfix;

}

 

int main() {

    std::string infixExpression = "3 + 4 * (2 - 7)";

    std::string postfixExpression = infixToPostfix(infixExpression);

 

    std::cout << "Infix Expression: " << infixExpression << std::endl;

    std::cout << "Postfix Expression: " << postfixExpression << std::endl;

 

    return 0;

}

 

# Postfix to Infix Conversion:

#include <iostream>

#include <stack>

 

bool isOperator(char c) {

    return c == '+' || c == '-' || c == '*' || c == '/';

}

 

std::string postfixToInfix(const std::string& postfix) {

    std::stack<std::string> stack;

 

    for (char c : postfix) {

        if (isalnum(c)) {

            stack.push(std::string(1, c)); // Operand, push to stack

        } else if (isOperator(c)) {

            std::string operand2 = stack.top();

            stack.pop();

            std::string operand1 = stack.top();

            stack.pop();

            std::string result = "(" + operand1 + c + operand2 + ")";

            stack.push(result);

        }

    }

 

    return stack.top();

}

 

int main() {

    std::string postfixExpression = "3427-*+";

    std::string infixExpression = postfixToInfix(postfixExpression);

 

    std::cout << "Postfix Expression: " << postfixExpression << std::endl;

    std::cout << "Infix Expression: " << infixExpression << std::endl;

 

    return 0;

}

 

 Python Examples: # Infix to Postfix Conversion:

def infix_to_postfix(infix):

    stack = []

    postfix = ""

    precedence = {'+': 1, '-': 1, '*': 2, '/': 2}

 

    for c in infix:

        if c.isalnum():

            postfix += c

        elif c == '(':

            stack.append(c)

        elif c == ')':

            while stack and stack[-1] != '(':

                postfix += stack.pop()

            stack.pop()  # Discard '('

        elif c in precedence:

            while stack and precedence[stack[-1]] >= precedence[c]:

                postfix += stack.pop()

            stack.append(c)

 

    while stack:

        postfix += stack.pop()

 

    return postfix

 

# Example

infix_expression = "3 + 4 * (2 - 7)"

postfix_expression = infix_to_postfix(infix_expression)

print("Infix Expression:", infix_expression)

print("Postfix Expression:", postfix_expression)

 

# Postfix to Infix Conversion:

def postfix_to_infix(postfix):

    stack = []

 

    for c in postfix:

        if c.isalnum():

            stack.append(c)

        elif c in {'+', '-', '*', '/'}:

            operand2 = stack.pop()

            operand1 = stack.pop()

            result = f"({operand1}{c}{operand2})"

            stack.append(result)

 

    return stack[0]

 

# Example

postfix_expression = "3427-*+"

infix_expression = postfix_to_infix(postfix_expression)

print("Postfix Expression:", postfix_expression)

print("Infix Expression:", infix_expression)

-----------------------------------------------------------------

Recursion Array in stack.

 

1. Recursion:

   - Recursion is a programming concept where a function calls itself in its definition. In the context of data structures, recursive algorithms are often used to solve problems by breaking them down into simpler subproblems.

   - Recursive functions typically have a base case that defines the termination condition, and they make progress toward reaching the base case.

 

2. Array:

   - An array is a data structure that stores elements of the same type in contiguous memory locations. Elements in an array can be accessed using an index.

 

3. Stack:

   - A stack is a data structure that follows the Last In, First Out (LIFO) principle. It supports two main operations: push (to add an element to the top) and pop (to remove the top element).

depth-first search (DFS) algorithms often use recursion to explore elements in an array-like structure, and the call stack is effectively used to keep track of the recursive calls.

-------------------------------------------------------------------

 

Recursion Array in stack.

A recursive algorithm involving arrays and a stack-like structure.

Example:- a depth-first search (DFS) on a graph or tree.

Recursion is often used, and the call stack implicitly acts as a stack to keep track of the traversal.

 

Algorithm for a recursive DFS using an array and a stack-like structure:

Algorithm RecursiveDFS(graph, startNode, visited):

    1. If the startNode is not visited:

        a. Mark the startNode as visited.

        b. Process the startNode (perform any desired operation).

 

        // Recursive step for each adjacent node

        c. For each neighborNode of startNode:

             i. Recursively call RecursiveDFS(graph, neighborNode, visited).

 

    2. End.

 

// Example usage

visited = Array of size |V| (initialized to all false, where |V| is the number of nodes)

RecursiveDFS(graph, startNode, visited)

  

In this algorithm:

 - `graph` represents the graph (adjacency list or matrix).

- `startNode` is the node from which the DFS starts.

- `visited` is an array to keep track of visited nodes.

 The algorithm starts the DFS from the `startNode` and recursively explores its neighbors. The `visited` array is used to avoid processing the same node multiple times.


Example:-  implementation in Python for an undirected graph represented as an adjacency list:

def recursive_dfs(graph, node, visited):

    if not visited[node]:

        visited[node] = True

        print(f"Visited node: {node}")

 

        for neighbor in graph[node]:

            recursive_dfs(graph, neighbor, visited)

 

# Example usage

graph = {0: [1, 2], 1: [0, 3], 2: [0, 4], 3: [1], 4: [2]}

visited = [False] * len(graph)

 

recursive_dfs(graph, 0, visited)

 

  C++ Example:

#include <iostream>

#include <vector>

#include <stack>

 void recursiveDFS(const std::vector<std::vector<int>>& graph, int startNode, std::vector<bool>& visited) {

    if (!visited[startNode]) {

        visited[startNode] = true;

        std::cout << "Visited node: " << startNode << std::endl;

 

        for (int neighbor : graph[startNode]) {

            recursiveDFS(graph, neighbor, visited);

        }

    }

}

 

int main() {

    std::vector<std::vector<int>> graph = {{1, 2}, {0, 3}, {0, 4}, {1}, {2}};

    std::vector<bool> visited(graph.size(), false);

 

    recursiveDFS(graph, 0, visited);

 

    return 0;

}

 

 Python Example:

def recursive_dfs(graph, start_node, visited):

    if not visited[start_node]:

        visited[start_node] = True

        print(f"Visited node: {start_node}")

 

        for neighbor in graph[start_node]:

            recursive_dfs(graph, neighbor, visited)

 

# Example usage

graph = {0: [1, 2], 1: [0, 3], 2: [0, 4], 3: [1], 4: [2]}

visited = [False] * len(graph)

 

recursive_dfs(graph, 0, visited)

---------------------------------------------------

 Linked Representation of Stack

The linked representation of a stack involves implementing a stack using a linked list data structure.

In this representation, each element of the stack is a node in a linked list, and the top of the stack is the head of the linked list.

 

Key components and characteristics of the linked representation of a stack:

1. Node Structure:

   - Each node in the linked list contains two parts: a data part to store the element, and a link (or pointer) part to reference the next node in the list.

 

2. Top of the Stack:

   - The top of the stack corresponds to the head of the linked list.

 

3. Push Operation:

   - To push an element onto the stack, a new node is created, the data is stored in the new node, and the new node becomes the new head of the linked list.

 

4. Pop Operation:

   - To pop an element from the stack, the head node is removed, and the next node (if any) becomes the new head.

 

5. Peek Operation:

   - The peek operation can be implemented by accessing the data in the head node without modifying the stack.

 

Example:- linked representation of a stack in C++:

#include <iostream>

 

// Node structure

template <typename T>

struct Node {

    T data;

    Node* next;

 

    Node(T value) : data(value), next(nullptr) {}

};

 

// Stack class using linked representation

template <typename T>

class LinkedStack {

private:

    Node<T>* top;

 

public:

    LinkedStack() : top(nullptr) {}

 

    void push(T value) {

        Node<T>* newNode = new Node<T>(value);

        newNode->next = top;

        top = newNode;

    }

 

    void pop() {

        if (!isEmpty()) {

            Node<T>* temp = top;

            top = top->next;

            delete temp;

        } else {

            std::cout << "Stack Underflow: Cannot pop from an empty stack." << std::endl;

        }

    }

 

    T peek() const {

        if (!isEmpty()) {

            return top->data;

        } else {

            std::cout << "Stack is empty." << std::endl;

            return T(); // Placeholder for an empty stack

        }

    }

 

    bool isEmpty() const {

        return top == nullptr;

    }

};

 

int main() {

    LinkedStack<int> stack;

 

    stack.push(1);

    stack.push(2);

    stack.push(3);

 

    std::cout << "Top element: " << stack.peek() << std::endl;

 

    stack.pop();

    stack.pop();

    stack.pop();

    stack.pop(); // Trying to pop from an empty stack

 

    return 0;

}

 Example:-

#include <iostream>

 template <typename T>

struct Node {

    T data;

    Node* next;

 

    Node(T value) : data(value), next(nullptr) {}

};

 

template <typename T>

class LinkedStack {

private:

    Node<T>* top;

 

public:

    LinkedStack() : top(nullptr) {}

 

    void push(T element) {

        Node<T>* newNode = new Node<T>(element);

        newNode->next = top;

        top = newNode;

    }

 

    void pop() {

        if (!isEmpty()) {

            Node<T>* temp = top;

            top = top->next;

            delete temp;

        } else {

            std::cout << "Stack Underflow: Cannot pop from an empty stack." << std::endl;

        }

    }

 

    T peek() const {

        if (!isEmpty()) {

            return top->data;

        } else {

            std::cout << "Stack is empty." << std::endl;

            return T(); // Placeholder for an empty stack

        }

    }

 

    bool isEmpty() const {

        return top == nullptr;

    }

};

 

int main() {

    LinkedStack<int> stack;

 

    stack.push(1);

    stack.push(2);

    stack.push(3);

 

    std::cout << "Top element: " << stack.peek() << std::endl;

 

    stack.pop();

    stack.pop();

    stack.pop();

    stack.pop(); // Trying to pop from an empty stack

 

    return 0;

}

 

  C++ Example:

#include <iostream>

 

template <typename T>

struct Node {

    T data;

    Node* next;

 

    Node(T value) : data(value), next(nullptr) {}

};

 

template <typename T>

class LinkedStack {

private:

    Node<T>* top;

 

public:

    LinkedStack() : top(nullptr) {}

 

    void push(T element) {

        Node<T>* newNode = new Node<T>(element);

        newNode->next = top;

        top = newNode;

    }

 

    void pop() {

        if (!isEmpty()) {

            Node<T>* temp = top;

            top = top->next;

            delete temp;

        } else {

            std::cout << "Stack Underflow: Cannot pop from an empty stack." << std::endl;

        }

    }

 

    T peek() const {

        if (!isEmpty()) {

            return top->data;

        } else {

            std::cout << "Stack is empty." << std::endl;

            return T(); // Placeholder for an empty stack

        }

    }

 

    bool isEmpty() const {

        return top == nullptr;

    }

};

 

int main() {

    LinkedStack<int> stack;

 

    stack.push(1);

    stack.push(2);

    stack.push(3);

 

    std::cout << "Top element: " << stack.peek() << std::endl;

 

    stack.pop();

    stack.pop();

    stack.pop();

    stack.pop(); // Trying to pop from an empty stack

 

    return 0;

}

 

----------------------------------------------------------

 Python Example:

 class Node:

    def __init__(self, data):

        self.data = data

        self.next = None

 

class LinkedStack:

    def __init__(self):

        self.top = None

 

    def push(self, element):

        new_node = Node(element)

        new_node.next = self.top

        self.top = new_node

 

    def pop(self):

        if not self.is_empty():

            temp = self.top

            self.top = self.top.next

            del temp

        else:

            print("Stack Underflow: Cannot pop from an empty stack.")

 

    def peek(self):

        if not self.is_empty():

            return self.top.data

        else:

            print("Stack is empty.")

            return None

 

    def is_empty(self):

        return self.top is None

 

# Example usage

stack = LinkedStack()

 

stack.push(1)

stack.push(2)

stack.push(3)

 

print("Top element:", stack.peek())

 

stack.pop()

stack.pop()

stack.pop()

stack.pop()  # Trying to pop from an empty stack

 

The linked representation of a stack is widely used in various applications where a dynamic and flexible data structure is required.

Some common use cases and scenarios where the linked representation of a stack is beneficial:

 

1. Dynamic Memory Management:

   - Linked representation allows for dynamic memory allocation and deallocation, making it suitable for scenarios where the size of the stack is not known in advance.

 

2. Function Call Management (Call Stack):

   - In programming languages that support recursion or function calls, a stack is used to manage the execution context of each function call. The linked representation is well-suited for this purpose.

 

3. Undo Mechanism in Applications:

   - In applications where an "undo" mechanism is needed (e.g., text editors, graphic design tools), a stack can be used to maintain the history of operations. The linked representation facilitates easy undo and redo operations.

 

4. Expression Evaluation:

   - The linked stack is commonly used in expression evaluation algorithms, such as converting infix expressions to postfix or prefix notations and evaluating postfix or prefix expressions.

 

5. Backtracking Algorithms:

   - Backtracking algorithms, such as those used in solving puzzles or searching for solutions, often rely on a stack to keep track of choices. The linked representation supports the dynamic nature of these algorithms.

 

6. Memory Management in Operating Systems:

   - Operating systems use stacks for managing function calls, interrupt handling, and storing local variables. The linked representation allows for efficient memory usage.

 

7. Undo/Redo in Software Applications:

   - In software applications, especially graphic design tools or document editors, the linked representation of a stack can be employed to implement undo and redo functionality.

 

8. Parenthesis Matching and Syntax Parsing:

   - Linked stacks are useful in algorithms that involve checking for balanced parentheses, syntax parsing, and validation of expressions.

 

9. Transaction Management in Databases:

   - In database systems, linked stacks can be used to manage transactions. The ability to roll back transactions and maintain a transaction history is crucial.

 

10. Algorithmic Problem Solving:

    - Various algorithmic problems, especially those involving depth-first search, backtracking, and tree traversal, can be efficiently solved using a linked stack.

======================


 


Post a Comment

0 Comments