Notes - Stack with Linked List

Notes - Stack with Linked List

Intro

In this blog, we will explore the implementation of a stack using a linked list and discuss the advantages it offers over other approaches. We will also delve into the practical applications of stacks, particularly in handling expressions and evaluating their values. By the end, you'll have a better understanding of how stacks work and how they can be utilized effectively. Let's get started!

Linked List For stack

When implementing a stack using a linked list, we can choose either end of the linked list to perform the push and pop operations. Let's consider the following options:

  1. Push at the start, Pop at the start:

    • In this approach, we insert new elements at the beginning of the linked list and remove elements from the beginning as well.

      • Insertion at the start of a linked list can be done in constant time by updating the head pointer.

      • However, removing an element from the start of a linked list is also constant time.

      • This approach is efficient for push-and-pop operations.

  1. Push at the end, Pop at the start:

    • In this approach, we insert new elements at the end of the linked list and remove elements from the beginning.

      • Insertion at the end of a linked list requires traversing the list until the last node and then adding the new element.

      • However, removing an element from the start of a linked list is constant time.

      • This approach is less efficient for push operations due to the traversal required for each insertion.

Based on these considerations, it is generally more efficient to push elements at the start of the linked list and pop elements from the start as well. This approach provides constant-time complexity for both push and pop operations, making it the preferred choice for implementing a stack using a singly-linked list.

Avoids the overhead of shifting elements in an array-based implementation.

  1. Using a linked list allows us to avoid the size limitation of a stack implemented with an array.

  2. In a linked list, we have the flexibility to choose where to insert and remove elements for optimal push and pop operations.

  3. For a singly-linked list, insertion at the start or end takes constant time using the head and current pointers, respectively.

  4. Removing an element at the start of a singly-linked list is constant time, while removal at the end requires traversing the list to the node one before the last.

  5. To achieve the LIFO (Last-In-First-Out) behavior of a stack, we choose to push elements at the start and pop elements from the start of the linked list.

  6. By inserting elements at the start of the linked list, we can efficiently perform the push operation without the need for shifting elements.

  7. Removing elements from the start of the linked list is also efficient and provides constant-time complexity for the pop operation.

  8. Singly-linked lists are suitable for implementing a stack, as there is no need for doubly or circular linked lists.

  9. The overall implementation of a stack using a linked list allows for constant-time complexity for both push and pop operations.

  10. Visualizing the stack implemented with a linked list, the elements are stored as nodes in the list, with the most recent element added at the start.

Reasons to prefer one implementation (array-based stack) over the other (linked list-based stack)

  1. Time Efficiency: The array-based implementation may be more time-efficient as it avoids the overhead of dynamic memory allocation and deallocation required by the linked list.

  2. Memory Efficiency: The linked list-based implementation is more memory-efficient as it only allocates memory for the actual number of nodes used, while an array requires pre-allocation of memory for a fixed number of elements, potentially resulting in unused memory.

  3. Memory Overhead: The linked list-based implementation incurs additional memory overhead due to the pointers (e.g., head, next) stored in each node, while the array-based implementation directly accesses elements using array indices.

  4. Upper Limit: Arrays have a fixed upper limit determined by their initial size, while linked lists can dynamically allocate memory, allowing for a potentially larger number of elements limited only by the address space of the machine.

Overall, the choice between an array-based stack implementation and a linked list-based stack implementation depends on the specific requirements of the application. If time efficiency and fixed memory allocation are crucial, an array-based implementation may be preferred. On the other hand, if memory efficiency and dynamic memory allocation are more important, a linked list-based implementation could be a better choice.

What Stack is used for?

A stack is commonly used to traverse and evaluate expressions in various forms: infix, prefix, and postfix.

  1. Infix:

In infix expressions, operators are placed between operands (e.g., A + B).

  1. Prefix:

while prefix expressions have the operator before the operands (e.g., +AB).

  1. PostFix:

postfix expressions have the operator after the operands (e.g., AB+).

what stack do?

The stack helps in converting infix and prefix expressions to postfix form using the shunting yard algorithm. It also aids in evaluating postfix expressions by storing operands and performing operations based on the encountered operators.

By using a stack, expressions can be properly handled, operands and operators can be manipulated, and the precedence of operations can be maintained, resulting in accurate expression evaluation.

Conversion of Infix to Postfix:

(A + B) \ C infix form*

(A B +) \ C convert addition*

(A B +) C \ convert multiplication*

A B + C \ postfix form*

Key Points For Conversion:

  • In infix expressions, operators are placed between operands based on precedence rules.

  • To convert infix to postfix, we use an algorithm with a stack:

    • Scan the expression and add operands to the output.

    • Compare operator precedence with the top of the stack.

    • Pop higher or equal precedence operators from the stack and add them to the output.

    • Push the current operator onto the stack.

    • Pop the remaining operators from the stack to the output.

  • The resulting postfix expression represents the original expression with the correct order of operations.

  • Evaluating postfix expressions involves using a stack to store operands and perform operations.

  • The algorithm enables efficient conversion and evaluation of expressions.

Infix Example

Consider the infix expression: A + B * C

Here's how we can convert it to postfix form using the algorithm:

  1. Start with an empty output string and an empty stack.

  2. Scan the infix expression from left to right.

  3. The first token encountered is operand 'A', so we add it to the output string.

  4. The next token is the operator '+'. We compare its precedence with the top of the stack (which is empty), so we push it onto the stack.

  5. Next, we encounter operand 'B'. We add it to the output string.

  6. Now we encounter the operator '*', which has higher precedence than '+'. Since the stack is empty, we push it onto the stack.

  7. The next token is operand 'C', so we add it to the output string.

  8. At this point, we have scanned the entire expression. We pop the operators from the stack (in this case, '*') and add them to the output string.

  9. Lastly, we pop the remaining operator from the stack (which is '+') and add it to the output string.

The resulting postfix expression is: A B C * +

Postfix Example

Consider the postfix expression: 5 2 * 3 +

Here's how we can evaluate it using the algorithm:

  1. Start with an empty stack.

  2. Scan the postfix expression from left to right.

  3. The first token encountered is operand '5', so we push it onto the stack.

  4. The next token is operand '2', so we push it onto the stack.

  5. Now we encounter the operator '*', which indicates multiplication. We pop the top two operands from the stack ('2' and '5'), perform the multiplication operation (2 * 5 = 10), and push the result ('10') back onto the stack.

  6. The next token is operand '3', so we push it onto the stack.

  7. Finally, we encounter the operator '+', which indicates addition. We pop the top two operands from the stack ('3' and '10'), perform the addition operation (3 + 10 = 13), and push the result ('13') back onto the stack.

  8. At this point, we have scanned the entire expression, and the final result is the top element of the stack, which is '13'.

Therefore, the evaluation of the postfix expression '5 2 * 3 +' is equal to '13'.

In this example, the stack-based algorithm allows us to evaluate the postfix expression by performing the operations in the correct order. The stack acts as a temporary storage for operands, and we apply the operators when they are encountered. By following this algorithm, we can efficiently evaluate postfix expressions of varying complexity.

Precedence of operators

Operator precedence rules determine the order in which operators are evaluated within an expression. The precedence rules for common binary operators are as follows:

  1. Exponentiation (ˆ) has the highest precedence.

  2. Multiplication (*) and division (/) have the next highest precedence.

  3. Addition (+) and subtraction (-) have the lowest precedence.

When operators have the same precedence, the left-to-right rule is applied. For exponentiation, the right-to-left rule is used.

Importance of parentheses

  1. Infix: 4+3*5 postfix: 435*+ //operator Precedence

  2. Infix: (4+3)*5 postfix: 43+5* //sorted acc to parenthesis

    • In the postfix form, parentheses are not used

    • In case of not using the parenthesis in the infix form, you have to see the precedence rule before evaluating the expression. In the above example, if we want to add first then we have to use the parenthesis.

    • In the postfix form, we do not need to use parenthesis. The position of operators and operands in the expression makes it clear in which order we have to do the multiplication and addition.

Conclusion

when implementing a stack using a linked list, we have options regarding where to perform push and pop operations. After evaluating the different approaches, it is generally more efficient to push elements at the start of the linked list and pop elements from the start as well. This approach ensures constant-time complexity for both push and pop operations, making it the preferred choice for implementing a stack using a singly-linked list.

The use of a linked list for implementing a stack provides advantages such as avoiding the overhead of shifting elements in an array-based implementation and the ability to dynamically allocate memory, allowing for a potentially larger number of elements. Additionally, using a stack is particularly helpful when it comes to traversing and evaluating expressions, such as converting infix to postfix or prefix form and evaluating postfix expressions.

In the next blog, we will delve into examples with code for implementing the stack data structure, specifically exploring how it can be used for converting infix expressions to postfix form using the shunting yard algorithm. Stay tuned for the next installment to gain a better understanding of stack implementation and its practical applications in handling expressions effectively.

Remember, the choice between an array-based stack implementation and a linked list-based stack implementation depends on the specific requirements of the application. Consider factors such as time efficiency, memory efficiency, memory overhead, and upper limit when deciding which implementation is best suited for your needs.