Thanks to visit codestin.com
Credit goes to www.scribd.com

0% found this document useful (0 votes)
23 views14 pages

Data Structures. MOD - 2

The document provides an overview of Abstract Data Types (ADT) focusing on Stacks and Queues, explaining their definitions, primary operations, and implementations in C using both arrays and linked lists. It also discusses common applications of stacks, expression evaluation techniques, and algorithms for converting infix expressions to postfix and prefix formats. Additionally, it covers different types of queues, including simple, circular, and priority queues, along with their implementations and insertion algorithms.

Uploaded by

2301109157cse
Copyright
© © All Rights Reserved
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as PDF, TXT or read online on Scribd
0% found this document useful (0 votes)
23 views14 pages

Data Structures. MOD - 2

The document provides an overview of Abstract Data Types (ADT) focusing on Stacks and Queues, explaining their definitions, primary operations, and implementations in C using both arrays and linked lists. It also discusses common applications of stacks, expression evaluation techniques, and algorithms for converting infix expressions to postfix and prefix formats. Additionally, it covers different types of queues, including simple, circular, and priority queues, along with their implementations and insertion algorithms.

Uploaded by

2301109157cse
Copyright
© © All Rights Reserved
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as PDF, TXT or read online on Scribd
You are on page 1/ 14

Abstract Data Types (ADT) and Stacks.

1. What is an Abstract Data Type (ADT) Stack?

An Abstract Data Type (ADT) Stack is a collection of elements that follows the Last In, First
Out (LIFO) principle. This means that the last element added to the stack is the first one to be
removed. An ADT defines the behavior of the data type without specifying its implementation.

2. Primary Operations of a Stack

The primary operations of a stack include:

●​ Push: Add an element to the top of the stack.


●​ Pop: Remove and return the top element of the stack.
●​ Peek (or Top): Return the top element without removing it.
●​ IsEmpty: Check if the stack is empty.
●​ Size: Return the number of elements in the stack.

3. Implementing a Stack Using an Array

Here’s a simple implementation of a stack using an array in C:

1.​ #define MAX 100


2.​
3.​ struct Stack {
4.​ int top;
5.​ int items[MAX];
6.​ };
7.​
8.​ void initStack(struct Stack* s) {
9.​ s->top = -1;
10.​}
11.​
12.​int isFull(struct Stack* s) {
13.​ return s->top == MAX - 1;
14.​}
15.​
16.​int isEmpty(struct Stack* s) {
17.​ return s->top == -1;
18.​}
19.​
20.​void push(struct Stack* s, int item) {
21.​ if (!isFull(s)) {
22.​ s->items[++s->top] = item;
23.​ }
24.​}
25.​
26.​int pop(struct Stack* s) {
27.​ if (!isEmpty(s)) {
28.​ return s->items[s->top--];
29.​ }
30.​ return -1; // or some error value
31.​}
32.​
33.​int peek(struct Stack* s) {
34.​ if (!isEmpty(s)) {
35.​ return s->items[s->top];
36.​ }
37.​ return -1; // or some error value
38.​}

4. Implementing a Stack Using a Linked List

Here’s a simple implementation of a stack using a linked list in C:

39.​struct Node {
40.​ int data;
41.​ struct Node* next;
42.​};
43.​
44.​struct Stack {
45.​ struct Node* top;
46.​};
47.​
48.​void initStack(struct Stack* s) {
49.​ s->top = NULL;
50.​}
51.​
52.​int isEmpty(struct Stack* s) {
53.​ return s->top == NULL;
54.​}
55.​
56.​void push(struct Stack* s, int item) {
57.​ struct Node* newNode = (struct Node*)malloc(sizeof(struct Node));
58.​ newNode->data = item;
59.​ newNode->next = s->top;
60.​ s->top = newNode;
61.​}
62.​
63.​int pop(struct Stack* s) {
64.​ if (!isEmpty(s)) {
65.​ struct Node* temp = s->top;
66.​ int poppedValue = temp->data;
67.​ s->top = s->top->next;
68.​ free(temp);
69.​ return poppedValue;
70.​ }
71.​ return -1; // or some error value
72.​}
73.​
74.​int peek(struct Stack* s) {
75.​ if (!isEmpty(s)) {
76.​ return s->top->data;
77.​ }
78.​ return -1; // or some error value
79.​}

1. Common Applications of Stacks in Programming

●​ Function Call Management: Stacks are used to manage function calls and returns in
programming languages (call stack).
●​ Expression Evaluation: Stacks are used to evaluate expressions (infix, postfix, prefix).
●​ Backtracking Algorithms: Stacks help in algorithms like depth-first search (DFS) and
backtracking (e.g., solving mazes).
●​ Undo Mechanisms: Applications like text editors use stacks to implement undo
functionality.
●​ Syntax Parsing: Compilers use stacks for parsing expressions and checking for
balanced parentheses.

2. Stacks in Expression Evaluation

Stacks can be used to evaluate expressions by converting them into a format that is easier to
compute, such as postfix (Reverse Polish Notation). The evaluation process involves:

●​ Pushing operands onto the stack.


●​ Popping operands for operations when an operator is encountered.
●​ Pushing the result back onto the stack.

3. Algorithm for Converting Infix Expressions to Postfix Expressions


The Shunting Yard Algorithm by Edsger Dijkstra is commonly used for this conversion. Here’s
a simplified version:

1.​ Initialize:​

○​ Create an empty stack for operators.


○​ Create an empty list for the output.
2.​ Process each token:​

○​ If the token is an operand, add it to the output.


○​ If the token is an operator:
■​ While there is an operator at the top of the stack with greater or equal
precedence, pop operators from the stack to the output.
■​ Push the current operator onto the stack.
○​ If the token is a left parenthesis, push it onto the stack.
○​ If the token is a right parenthesis:
■​ Pop from the stack to the output until a left parenthesis is at the top of the
stack.
■​ Pop the left parenthesis from the stack.
3.​ End of expression:​

○​ Pop all operators from the stack to the output.

4. Algorithm for Converting Infix Expressions to Prefix Expressions

The conversion to prefix (Polish Notation) can be done similarly:

1.​ Reverse the infix expression.


2.​ Replace '(' with ')' and vice versa.
3.​ Use the same algorithm as for postfix conversion to convert the modified expression
to postfix.
4.​ Reverse the postfix expression to get the prefix expression.

5. Time Complexity of Conversion Algorithms

●​ The time complexity for both the infix to postfix and infix to prefix conversion algorithms
is O(n), where n is the number of tokens in the expression. This is because each token
is processed a constant number of times.

1. How Does Recursion Utilize the Stack Data Structure?

Recursion inherently uses the call stack to keep track of function calls. Each time a recursive
function is called:
●​ A new frame is pushed onto the stack, containing the function's parameters, local
variables, and the return address.
●​ When the function completes, its frame is popped from the stack, and control returns to
the previous function call.

2. What Happens to the Stack During Recursive Function Calls?

During recursive function calls:

●​ Each call creates a new stack frame, which holds the state of that particular call.
●​ The stack grows with each recursive call until a base case is reached.
●​ Once the base case is hit, the stack starts to unwind as each function call completes,
returning control to the previous call.

3. Example of a Recursive Algorithm That Uses a Stack

A classic example of a recursive algorithm is the calculation of the factorial of a number. Here’s
how it works:

1.​ int factorial(int n) {


2.​ if (n == 0) { // Base case
3.​ return 1;
4.​ } else {
5.​ return n * factorial(n - 1); // Recursive call
6.​ }
7.​ }

Stack Behavior:

●​ For factorial(5), the stack will look like this:


○​ factorial(5) is called, pushing it onto the stack.
○​ factorial(4) is called, pushing it onto the stack.
○​ factorial(3) is called, pushing it onto the stack.
○​ factorial(2) is called, pushing it onto the stack.
○​ factorial(1) is called, pushing it onto the stack.
○​ factorial(0) is called, reaching the base case and returning 1.
●​ The stack then unwinds, calculating the result as each call returns.

1. What is an ADT Queue?


An Abstract Data Type (ADT) Queue is a collection of elements that follows the First In, First
Out (FIFO) principle. This means that the first element added to the queue will be the first one to
be removed. An ADT defines the behavior of the queue without specifying its implementation.

2. Differences Between Queue Types

●​ Simple Queue:​

○​ Structure: Linear structure.


○​ Operations: Supports enqueue (adding) and dequeue (removing) operations.
○​ Limitations: Once an element is dequeued, that space cannot be reused until
the queue is reset.
●​ Circular Queue:​

○​ Structure: Circular structure that connects the end of the queue back to the front.
○​ Operations: Similar to a simple queue but allows for efficient use of space.
○​ Advantages: Prevents wastage of space by reusing empty slots.
●​ Priority Queue:​

○​ Structure: Elements are ordered based on priority rather than the order they
were added.
○​ Operations: Supports enqueue and dequeue operations, but dequeue removes
the element with the highest priority.
○​ Use Cases: Useful in scenarios like scheduling tasks where certain tasks have
higher importance.

3. Implementing a Simple Queue Using an Array

Here’s a simple implementation of a queue using an array in C:

1.​ #define MAX 100


2.​
3.​ struct Queue {
4.​ int items[MAX];
5.​ int front, rear;
6.​ };
7.​
8.​ void initQueue(struct Queue* q) {
9.​ q->front = -1;
10.​ q->rear = -1;
11.​}
12.​
13.​int isFull(struct Queue* q) {
14.​ return q->rear == MAX - 1;
15.​}
16.​
17.​int isEmpty(struct Queue* q) {
18.​ return q->front == -1;
19.​}
20.​
21.​void enqueue(struct Queue* q, int item) {
22.​ if (!isFull(q)) {
23.​ if (q->front == -1) {
24.​ q->front = 0; // First element
25.​ }
26.​ q->items[++q->rear] = item;
27.​ }
28.​}
29.​
30.​int dequeue(struct Queue* q) {
31.​ if (!isEmpty(q)) {
32.​ int item = q->items[q->front];
33.​ if (q->front >= q->rear) { // Reset queue
34.​ q->front = q->rear = -1;
35.​ } else {
36.​ q->front++;
37.​ }
38.​ return item;
39.​ }
40.​ return -1; // or some error value
41.​}

4. Implementing a Circular Queue Using an Array

Here’s a simple implementation of a circular queue in C:

42.​struct CircularQueue {
43.​ int items[MAX];
44.​ int front, rear;
45.​};
46.​
47.​void initCircularQueue(struct CircularQueue* q) {
48.​ q->front = -1;
49.​ q->rear = -1;
50.​}
51.​
52.​int isFull(struct CircularQueue* q) {
53.​ return (q->rear + 1) % MAX == q->front;
54.​}
55.​
56.​int isEmpty(struct CircularQueue* q) {
57.​ return q->front == -1;
58.​}
59.​
60.​void enqueue(struct CircularQueue* q, int item) {
61.​ if (!isFull(q)) {
62.​ if (q->front == -1) {
63.​ q->front = 0; // First element
64.​ }
65.​ q->rear = (q->rear + 1) % MAX;
66.​ q->items[q->rear] = item;
67.​ }
68.​}
69.​
70.​int dequeue(struct CircularQueue* q) {
71.​ if (!isEmpty(q)) {
72.​ int item = q->items[q->front];
73.​ if (q->front == q->rear) { // Reset queue
74.​ q->front = q->rear = -1;
75.​ } else {
76.​ q->front = (q->front + 1) % MAX;
77.​ }
78.​ return item;
79.​ }
80.​ return -1; // or some error value
81.​}

5. Implementing a Priority Queue

A priority queue can be implemented using an array or a linked list. Here’s a simple
implementation using an array:

82.​struct PriorityQueue {
83.​ int items[MAX];
84.​ int priorities[MAX];
85.​ int size;
86.​};
87.​
88.​void initPriorityQueue(struct PriorityQueue* pq) {
89.​ pq->size = 0;
90.​}
91.​
92.​int isEmpty(struct PriorityQueue* pq) {
93.​ return pq->size == 0;
94.​}
95.​
96.​void enqueue(struct PriorityQueue* pq, int item, int priority) {
97.​ int i;
98.​ if (pq->size < MAX) {
99.​ for (i = pq->size - 1; (i >= 0 && pq->priorities[i] < priority); i--) {
100.​ pq->items[i + 1] = pq->items[i];
101.​ pq->priorities[i + 1] = pq->priorities[i];
102.​ }
103.​ pq->items[i + 1] = item;
104.​ pq->priorities[i + 1] = priority;
105.​ pq->size++;
106.​ }
107.​ }
108.​
109.​ int dequeue(struct PriorityQueue* pq) {
110.​ if (!isEmpty(pq)) {
111.​ return pq->items[--pq->size]; // Remove the highest priority item
112.​ }
113.​ return -1; // or some error value
114.​ }

1. Insertion Algorithm for a Simple Queue

The insertion algorithm (enqueue) for a simple queue involves the following steps:

1.​ Check if the Queue is Full:​

○​ If the rear index is at the maximum size, the queue is full.


2.​ Update the Front Index:​

○​ If the queue is empty (front is -1), set the front index to 0.


3.​ Insert the Element:​

○​ Increment the rear index and place the new element at that position.
4.​ Example Code:​

1.​ void enqueue(struct Queue* q, int item) {


2.​ if (!isFull(q)) {
3.​ if (q->front == -1) {
4.​ q->front = 0; // First element
5.​ }
6.​ q->items[++q->rear] = item; // Insert item
7.​ }
8.​ }

2. Deletion Algorithm for a Simple Queue

The deletion algorithm (dequeue) for a simple queue involves the following steps:

1.​ Check if the Queue is Empty:​

○​ If the front index is -1, the queue is empty.


2.​ Retrieve the Element:​

○​ Store the element at the front index for return.


3.​ Update the Front Index:​

○​ Increment the front index to remove the element.


○​ If the front index exceeds the rear index, reset both to -1 (indicating the queue is
empty).
4.​ Example Code:​

9.​ int dequeue(struct Queue* q) {


10.​ if (!isEmpty(q)) {
11.​ int item = q->items[q->front]; // Get front item
12.​ if (q->front >= q->rear) { // Reset queue
13.​ q->front = q->rear = -1;
14.​ } else {
15.​ q->front++; // Move front index
16.​ }
17.​ return item;
18.​ }
19.​ return -1; // or some error value
20.​}

3. Differences in Insertion and Deletion in a Circular Queue

●​ Insertion (Enqueue):​
○​ In a circular queue, the rear index wraps around to the beginning of the array
when it reaches the end, allowing for efficient use of space.
○​ The condition to check if the queue is full is (rear + 1) % MAX == front.
●​ Deletion (Dequeue):​

○​ Similar to insertion, the front index also wraps around when it reaches the end of
the array.
○​ The condition to check if the queue is empty remains the same (front == -1).

4. How Priority is Determined in a Priority Queue

In a priority queue, priority is determined based on the following:

●​ Priority Value: Each element is associated with a priority value. Higher values may
indicate higher priority, or vice versa, depending on the implementation.
●​ Ordering: When elements are enqueued, they are placed in the queue based on their
priority. The queue maintains an order such that the element with the highest priority is
always at the front.
●​ Example: If you enqueue elements with priorities 1, 3, and 2, the queue will reorder
them so that the element with priority 3 is dequeued first.

1. Common Applications of Queues in Computer Science

●​ Task Scheduling: Queues are used in operating systems to manage processes and
tasks, ensuring that they are executed in the order they arrive.
●​ Print Spooling: In printers, print jobs are queued to ensure they are printed in the order
they were received.
●​ Data Buffering: Queues are used in data streaming applications (like video or audio) to
buffer data before processing.
●​ Breadth-First Search (BFS): In graph algorithms, queues are used to explore nodes
level by level.
●​ Network Traffic Management: Queues manage packets in routers to ensure orderly
processing and transmission.

2. Queues in Scheduling Algorithms

●​ First-Come, First-Served (FCFS): Tasks are processed in the order they arrive in the
queue.
●​ Round Robin: Each task is assigned a fixed time slice in a cyclic order, using a queue to
manage the order of execution.
●​ Priority Scheduling: Tasks are placed in a queue based on their priority, ensuring
higher priority tasks are executed first.

3. Queues in Breadth-First Search (BFS) Algorithms


●​ Level Order Traversal: BFS explores nodes in a graph or tree level by level. A queue is
used to keep track of nodes to be explored.
●​ Process:
1.​ Start from the root node (or any starting node) and enqueue it.
2.​ Dequeue a node, process it, and enqueue all its adjacent (or child) nodes.
3.​ Repeat until all nodes are processed.

This ensures that nodes are explored in the order they are discovered, making BFS effective for
finding the shortest path in unweighted graphs.

1. Advantages and Disadvantages of Using Arrays

Advantages:

●​ Fast Access: Arrays provide O(1) time complexity for accessing elements, making push
and pop operations efficient.
●​ Memory Efficiency: Arrays have a contiguous memory allocation, which can lead to
better cache performance.

Disadvantages:

●​ Fixed Size: The size of the array must be defined at the time of creation, which can lead
to overflow if the stack or queue exceeds this size.
●​ Wasted Space: If the maximum size is set too high, memory can be wasted when the
stack or queue is not full.

2. Handling Overflow and Underflow Conditions

Stack:

●​ Overflow: Check if the top index equals the maximum size of the array before pushing
an element. If it does, the stack is full.
●​ Underflow: Check if the top index is -1 before popping an element. If it is, the stack is
empty.

Queue:

●​ Overflow: Check if the rear index is at the maximum size (or if (rear + 1) % MAX ==
front in a circular queue) before enqueuing. If true, the queue is full.
●​ Underflow: Check if the front index is -1 before dequeuing. If it is, the queue is empty.

3. Modifications for Dynamic Stack or Queue Using Arrays


To implement a dynamic stack or queue using arrays, consider the following modifications:

●​ Dynamic Resizing: Use a dynamic array (like a list in Python or ArrayList in Java) that
can grow or shrink as needed. When the array is full, create a new larger array, copy the
elements, and update the reference.
●​ Reallocation: When the number of elements decreases significantly, you can shrink the
array to save memory.
●​ Maintain Indices: Keep track of the front and rear indices (for queues) or the top index
(for stacks) to manage the current size and position of elements effectively.

1. Performance Comparison

Array Implementation:

●​ Stack:
○​ Time Complexity: O(1) for push and pop operations.
○​ Access Time: Fast access due to contiguous memory allocation.
●​ Queue:
○​ Time Complexity: O(1) for enqueue and dequeue operations (with circular
implementation).
○​ Access Time: Fast access, but resizing may be needed if the array is full.

Linked List Implementation:

●​ Stack:
○​ Time Complexity: O(1) for push and pop operations.
○​ Access Time: Slower than arrays due to pointer dereferencing.
●​ Queue:
○​ Time Complexity: O(1) for enqueue and dequeue operations.
○​ Access Time: Slower than arrays, but no need for resizing.

2. Memory Usage Comparison

Array Implementation:

●​ Stack:
○​ Memory Usage: Fixed size; if the maximum size is not fully utilized, memory can
be wasted.
○​ Overhead: Minimal overhead, just the array itself.
●​ Queue:
○​ Memory Usage: Similar to stacks; can waste memory if not fully utilized.
○​ Overhead: Minimal overhead, but may require resizing.

Linked List Implementation:


●​ Stack:
○​ Memory Usage: Dynamic size; uses memory proportional to the number of
elements.
○​ Overhead: Each element requires additional memory for pointers (next node).
●​ Queue:
○​ Memory Usage: Dynamic size; uses memory proportional to the number of
elements.
○​ Overhead: Similar to stacks, with additional memory for pointers.

Summary

●​ Performance: Both implementations provide O(1) time complexity for operations. Arrays
offer faster access due to contiguous memory, while linked lists avoid overflow issues.
●​ Memory Usage: Arrays can waste memory if not fully utilized, while linked lists use
memory proportional to the number of elements but incur overhead due to pointers.

You might also like