Here is a more detailed version of the notes, expanding on the theoretical concepts from all
the presentations.
📚 Unit 3: Stacks
Session 1: Stack Concepts & Array Implementation
1. Stack Definition
A Stack is a fundamental linear data structure, meaning its elements are stored in a
sequential, one-after-another order.
● Principle: Its defining characteristic is the LIFO (Last In, First Out) principle. This means
the very last element added to the stack will be the very first one removed.
● Analogy: The classic analogy is a stack of plates. You add (push) a new plate onto the
top, and when you need a plate, you remove (pop) the one from the top. You can't easily
access a plate in the middle without removing all the ones on top of it first.
● Structure: A stack is an "ordered list" because the elements are maintained in the order
they were pushed. It's a list of "homogeneous" elements, meaning all elements in the
stack are typically of the same data type (e.g., a stack of integers, or a stack of strings).
● Operations: All insertions (push) and deletions (pop) happen at a single end, known as
the "top" of the stack.
2. Stack as an ADT (Sequential/Array)
● ADT (Abstract Data Type): An ADT defines a set of operations (like push, pop, isEmpty,
isFull, peek) without specifying how those operations are implemented. It's a logical
description, not a physical one.
● Sequential Implementation: This is a concrete implementation of the Stack ADT using a
fixed-size, one-dimensional array.
● top Variable: A crucial integer variable (an index) is used to keep track of the location of
the top element. It is initialized to -1 to signify that the stack is empty (as index 0 is the
first valid position in the array).
● Stack Overflow: This is an error condition that occurs when you try to push a new
element onto a stack that is already full. In the array implementation, this happens when
top is equal to MAX-1 (the last valid index).
● Stack Underflow: This is the opposite error, occurring when you try to pop an element
from a stack that is already empty. This is detected by checking if top == -1.
3. Algorithms: Array-Based Stack
Algorithm: push(val) (Insert)
This operation adds an element to the top of the stack.
// 1. Check if the stack is full (Overflow)
if top == MAX-1
print "Stack Overflow"
else
// 2. Increment the top pointer to the next empty slot
top = top + 1
// 3. Store the value at the new top position
stack[top] = val
Algorithm: pop() (Remove)
This operation removes and returns the element from the top of the stack.
// 1. Check if the stack is empty (Underflow)
if top == -1
print "Stack Underflow"
else
// 2. Retrieve the value from the top position
val = stack[top]
// 3. Decrement the top pointer. (The old value is still
// in the array but is now inaccessible and will be
// overwritten by the next push).
top = top - 1
// 4. Return the retrieved value
return val
4. Application Algorithms (from Session 1)
Algorithm: Reverse a String
The LIFO nature of a stack is perfect for reversing sequences.
void reverse(char str[10])
{
char S[10];
int top = -1;
// 1. Push: Read the string from left to right,
// pushing each character onto the stack.
for(i=0; str[i] != '\0'; i++)
{
S[++top] = str[i]; // push
}
// 2. Pop: Pop characters from the stack. Since the
// last character pushed is the first one popped,
// the string will be printed in reverse.
while(top >= 0)
{
cout << S[top--]; // pop
}
}
Algorithm: Convert Decimal to Binary
This works because the remainders in binary conversion are found in reverse order (the first
remainder is the last bit).
C
// 1. Push remainders: Repeatedly divide the decimal
// number by 2. The remainder (0 or 1) of each
// division is pushed onto the stack.
while(n != 0)
{
[Link](n % 2);
n = n / 2;
}
// 2. Pop remainders: Once the number is 0, pop all
// the remainders from the stack. They will come out
// in the correct binary order.
while(![Link]())
{
cout << [Link]();
}
Session 2: Linked Stack & Infix to Postfix
1. Linked Representation of Stack
● Concept: Instead of a fixed-size array, we can use a dynamic linked list. This
implementation avoids the "Stack Overflow" problem (as long as there is memory) and
doesn't waste space.
● Structure: A pointer top (or head) points to the first node in the linked list.
● Operations:
○ push: A push operation creates a new node and inserts it at the beginning (head) of
the list. The new node's next pointer is set to the old top, and then top is updated to
point to the new node.
○ pop: A pop operation removes the node from the beginning (head) of the list. The
top pointer is simply moved to top->next, and the old node is deleted.
● Efficiency: Both push and pop are O(1) operations (constant time), which is ideal.
Inserting/deleting at the end of a singly-linked list would be an O(n) operation, making it
unsuitable for an efficient stack.
2. Algorithms: Linked-List-Based Stack
Algorithm: push(val) (Linked List)
1. Allocate memory for a new node (newNode).
2. Set newNode->data = val.
3. Set newNode->next = top. // Point new node to old top
4. Set top = newNode. // Update top to be the new node
Algorithm: pop() (Linked List)
1. if top == NULL
print "Stack Underflow"
2. else
temp = top // Store current top
val = top->data // Get data
top = top->next // Move top to the next node
delete temp // Free the old top node
return val
3. Expression Conversion
● Infix: The standard human-readable format (e.g., A + B). It's difficult for computers to
parse directly because of operator precedence (e.g., * before +) and parentheses ().
● Postfix (Reverse Polish): Operators follow their operands (e.g., A B +). This format is
trivial for a computer to evaluate using a stack. It needs no parentheses or precedence
rules.
● Prefix (Polish): Operators precede their operands (e.g., + A B). Also easy for a computer
to evaluate.
4. Algorithm: Infix to Postfix Conversion
This algorithm uses a stack to hold operators and parentheses temporarily.
1. Initialize an empty stack S.
2. Scan the infix expression from left to right.
3. If an operand (e.g., A, B, 5): Append it directly to the output postfix string.
4. If an operator (e.g., +, *):
○ While the stack S is not empty AND the operator on top of S has precedence greater
than or equal to the current operator:
■ Pop the operator from S and append it to the output.
○ Push the current operator onto S.
5. If an opening parenthesis (: Push it onto S.
6. If a closing parenthesis ):
○ Pop operators from S and append them to the output until an opening parenthesis (
is encountered.
○ Pop and discard the (. (They are not needed in postfix).
7. After the entire infix expression is scanned, pop any remaining operators from S and
append them to the output.
Session 3: Infix to Prefix & Postfix Evaluation
1. Algorithm: Infix to Prefix Conversion
This is a clever trick that re-uses the Infix-to-Postfix algorithm.
1. Reverse the entire infix expression. (While reversing, also swap all ( with ) and all ) with ().
2. Find the postfix expression of this new reversed string using the exact algorithm from
Session 2.
3. Reverse the resulting postfix expression. This final string is the correct prefix expression.
2. Algorithm: Postfix Expression Evaluation
This is the reason we convert to postfix. It's incredibly simple to evaluate.
1. Initialize an empty stack S (this stack will hold operands/numbers).
2. Scan the postfix expression from left to right.
3. If an operand is encountered:
○ Push it onto the stack S.
4. If an operator (+, -, *, /) is encountered:
○ Pop the top two operands from S. Let them be Op2 (popped first, so it's the
right-hand operand) and Op1 (popped second).
○ Perform the operation: result = Op1 <operator> Op2.
○ Push the result back onto the stack S.
5. After the entire expression is scanned, the final result will be the only value left on the
stack. Pop it and return.
Session 4: Prefix Evaluation & Other Applications
1. Algorithm: Prefix Expression Evaluation
This is very similar to postfix evaluation, but scanned in reverse.
1. Initialize an empty stack S (to hold operands).
2. Scan the prefix expression from right to left.
3. If an operand is encountered:
○ Push it onto the stack S.
4. If an operator (+, -, *, /) is encountered:
○ Pop the top two operands from S. Let them be Op1 (popped first, so it's the left-hand
operand) and Op2 (popped second).
○ Perform the operation: result = Op1 <operator> Op2.
○ Push the result back onto the stack S.
5. After the entire expression is scanned, the final result is on top of the stack.
2. Application: Recursion
● Concept: Recursion is when a function calls itself. To manage this, the computer uses an
internal stack called the "call stack" or "system stack".
● Stack Frame: Every time a function is called (even non-recursive ones), a stack frame is
created and pushed onto the call stack. This frame contains:
○ The function's parameters.
○ The function's local variables.
○ The return address (where to resume execution in the calling function when this one
finishes).
● How it Works: For factorial(3):
1. factorial(3) is called. Its frame is pushed. It needs factorial(2).
2. factorial(2) is called. Its frame is pushed on top of fact(3)'s. It needs factorial(1).
3. factorial(1) is called. Its frame is pushed. It needs factorial(0).
4. factorial(0) is called. Its frame is pushed. It hits the base case and returns 1. Its frame
is popped.
5. Control returns to factorial(1). It calculates 1 * 1 = 1 and returns. Its frame is popped.
6. Control returns to factorial(2). It calculates 2 * 1 = 2 and returns. Its frame is popped.
7. Control returns to factorial(3). It calculates 3 * 2 = 6 and returns. Its frame is popped.
3. Application: Parenthesis Checker
● Logic: This application checks if brackets (), {}, [] are properly balanced.
● Algorithm:
1. Scan the expression from left to right.
2. If an opening bracket ((, {, [) is found, it's a "promise" to have a closing one later.
Push this "promise" onto the stack.
3. If a closing bracket (), }, ]) is found, it must "fulfill" the most recent promise.
■ Check the stack. If it's empty, this closer has no opener. Error (Unbalanced).
■ If not empty, pop the top.
■ If the popped bracket is not the matching opener (e.g., you found a } but popped
a (), Error (Unbalanced).
4. After scanning the whole string:
■ If the stack is empty, all promises were fulfilled. Success (Balanced).
■ If the stack is not empty, there are unfulfilled promises (unclosed opening
brackets). Error (Unbalanced).
4. Application: Multi-Stack
● Concept: A memory-efficiency technique to implement two stacks (Stack A and Stack B)
within a single array.
● Implementation:
○ Stack A's topA starts at -1 and grows right (e.g., pushA increments topA).
○ Stack B's topB starts at MAX and grows left (e.g., pushB decrements topB).
● Benefit: A true "Overflow" condition only occurs when topA + 1 == topB. This means the
entire array is full. This is much better than partitioning the array in half, where one stack
could overflow while the other is still half-empty.
🚃 Unit 3: Queues
Session 6: Queues & Circular Queues
1. Queue Concepts
● A Queue is a linear data structure, just like a stack.
● Principle: It follows the FIFO (First In, First Out) principle.
● Analogy: The classic analogy is a queue (or "line") of people at a movie theater. The first
person to get in line is the first person to get a ticket.
● Operations:
○ Enqueue: Adds a new element to the rear (or tail/back) of the queue.
○ Dequeue: Removes an element from the front (or head) of the queue.
2. Circular Queue
● Problem with Linear Array Queue: If you use a simple array with front and rear
pointers, you run into a serious "wasted space" problem.
○ Imagine an array of size 5 (MAX=5). front=0, rear=4. The queue [A, B, C, D, E] is full.
○ Now, dequeue() twice. front moves to 2. The queue is [_, _, C, D, E]. rear is still 4.
○ Now, if you try to enqueue(F), the rear == MAX-1 check will say the queue is full, even
though slots 0 and 1 are empty! This is inefficient.
● Solution: A Circular Queue treats the array as a circle. When rear or front hits the end
(MAX-1), it "wraps around" to index 0. This is typically done with the modulo operator (%)
or if statements. This allows the queue to fill up all empty spaces, including the "wasted"
ones at the beginning.
3. Algorithms: Circular Queue
(Using front and rear indices, both initialized to -1, and MAX as array size)
Algorithm: insert(val) (Circular Enqueue)
// 1. Check for Overflow
if (front == 0 && rear == MAX-1) OR (front == rear + 1)
print "Queue Overflow"
else
// 2. Handle first-ever insertion
if front == -1
front = 0
rear = 0
// 3. Handle rear wrap-around
else if rear == MAX-1
rear = 0
// 4. Handle normal case (increment rear)
else
rear = rear + 1
// 5. Insert the element
queue[rear] = val
Algorithm: delete() (Circular Dequeue)
// 1. Check for Underflow
if front == -1
print "Queue Underflow"
return -1
else
// 2. Get the value from the front
val = queue[front]
// 3. Handle case of last element being removed
if front == rear
front = -1
rear = -1
// 4. Handle front wrap-around
else if front == MAX-1
front = 0
// 5. Handle normal case (increment front)
else
front = front + 1
return val
Session 7: Circular Queue Code & Dequeue
1. C Code Implementation: Circular Queue
#include <stdio.h>
#define MAX 10
int queue[MAX];
int front = -1, rear = -1;
void insert(void);
int delete_element(void);
void display(void);
// Enqueue operation
void insert()
{
int num;
printf("\n Enter the number to be inserted: ");
scanf("%d", &num);
// Check for overflow
if((front == 0 && rear == MAX-1) || (front == rear+1))
{
printf("\n OVERFLOW");
}
else if(front == -1 && rear == -1) // Queue is empty
{
front = rear = 0;
queue[rear] = num;
}
else if(rear == MAX-1 && front != 0) // Wrap around
{
rear = 0;
queue[rear] = num;
}
else // Normal case
{
rear++;
queue[rear] = num;
}
}
// Dequeue operation
int delete_element()
{
int val;
if(front == -1 && rear == -1) // Check for underflow
{
printf("\n UNDERFLOW");
return -1;
}
val = queue[front];
if(front == rear) // Only one element in queue
{
front = rear = -1;
}
else if(front == MAX-1) // Wrap around
{
front = 0;
}
else // Normal case
{
front++;
}
return val;
}
// Display operation (must handle wrap-around)
void display()
{
int i;
if(front == -1 && rear == -1)
printf("\n Queue is Empty");
else
{
if(front <= rear) // No wrap-around
{
for(i=front; i<=rear; i++)
printf("%d ", queue[i]);
}
else // Wraparound case
{
// Print from front to end
for(i=front; i<MAX; i++)
printf("%d ", queue[i]);
// Print from beginning to rear
for(i=0; i<=rear; i++)
printf("%d ", queue[i]);
}
}
}
2. Dequeue (Double-Ended Queue)
● Concept: A "Dequeue" (pronounced "deck") is a more flexible version of a queue. It
stands for Double-Ended Queue.
● Functionality: It allows you to insert (enqueue) and remove (dequeue) from both the
front and the rear of the queue.
● Analogy: It's like a line where people can join or leave from both the front and the back.
● Types:
○ Input-Restricted Deque: You can only insert at one end (e.g., the rear), but you can
delete from both ends.
○ Output-Restricted Deque: You can insert at both ends, but you can only delete
from one end (e.g., the front).
● Operations: enqueueR() (at rear), enqueueF() (at front), dequeueR() (from rear),
dequeueF() (from front).
Session 8: Priority Queue
1. Priority Queue Concept
● Concept: A Priority Queue is an abstract data type that acts like a queue, but with a
twist: each element is assigned a priority.
● Principle: Unlike a FIFO queue, elements are dequeued based on their priority, not
their arrival time. The element with the highest priority is removed first.
● FIFO for Ties: If two elements have the same priority, they are typically served in FIFO
order (based on when they arrived).
● Example: A hospital emergency room. Patients are not treated in the order they arrive
(FIFO). They are triaged based on severity: "Serious" (high priority), "Non-serious"
(medium priority), "General checkup" (low priority). A "Serious" patient who arrives after
a "General checkup" patient will be seen first.
2. C Code Implementation: Priority Queue (Linked List)
● Implementation: One way to implement this is with a linked list that is kept sorted by
priority.
● Insert Operation: When inserting a new element, you must traverse the list to find the
correct spot to maintain the sorted order (e.g., highest priority at the front). This is an
O(n) operation.
● Delete Operation: Since the list is kept sorted, the highest priority item is always at the
front. Deletion is a simple pop from the front, which is an O(1) operation.
C
// Node structure
struct node
{
int priority;
char name[20];
struct node *next;
};
node *front = NULL;
// Insert based on priority (maintains sorted list)
void queue::Insert()
{
node *temp, *t;
temp = new node;
cout << "Enter name: ";
cin >> temp->name;
cout << "Enter priority: ";
cin >> temp->priority;
// If list is empty OR new node has higher priority than the front
if(front == NULL || temp->priority > front->priority)
{
temp->next = front;
front = temp;
}
else
{
// Traverse to find correct insertion point
t = front;
while(t->next != NULL && t->next->priority >= temp->priority)
{
t = t->next;
}
temp->next = t->next;
t->next = temp;
}
}
// Delete from the front (always the highest priority)
void queue::Delete()
{
if(front == NULL)
{
cout << "Queue is empty";
return;
}
node *temp = front;
front = front->next;
// Displaying the priority category
cout << "Patient ";
switch(temp->priority)
{
case 1: cout << "General checkup"; break;
case 2: cout << "Non-serious"; break;
case 3: cout << "Serious"; break;
default: cout << "Unknown";
}
cout << " " << temp->name << " got the treatment";
delete temp;
}
// Display the queue
void queue::Display()
{
node *temp = front;
while(temp != NULL)
{
cout << temp->name << " (Priority: " << temp->priority << ")" << endl;
temp = temp->next;
}
}