0% found this document useful (0 votes)
8 views

Assignment

The document describes code implementations of various algorithms discussed in class, including: 1. Implementing two stacks in a single array, with O(1) time complexity for push and pop operations. 2. Implementing a stack using two queues, with O(1) time complexity for push but O(n) for pop. 3. Reversing a linked list using a stack, with O(n) time and space complexity. It also provides code to delete the middle element of a stack without additional data structures or size operations, using a recursive approach with O(n) time complexity.

Uploaded by

sabah nushra
Copyright
© © All Rights Reserved
Available Formats
Download as PDF, TXT or read online on Scribd
0% found this document useful (0 votes)
8 views

Assignment

The document describes code implementations of various algorithms discussed in class, including: 1. Implementing two stacks in a single array, with O(1) time complexity for push and pop operations. 2. Implementing a stack using two queues, with O(1) time complexity for push but O(n) for pop. 3. Reversing a linked list using a stack, with O(n) time and space complexity. It also provides code to delete the middle element of a stack without additional data structures or size operations, using a recursive approach with O(n) time complexity.

Uploaded by

sabah nushra
Copyright
© © All Rights Reserved
Available Formats
Download as PDF, TXT or read online on Scribd
You are on page 1/ 11

Algorithm Engineering Lab 1

Sabah Nushra
ID: 190041227
CSE-2A
12 February 2024

Task 1
Implement the algorithms (python is preferred) discussed in the class:

1. Implement two stacks in an array.


Here is the code implemention of the following problem :

class TwoStacks :
def __init__ ( self , size ) :
self . size = size
self . array = [ None ] * size
self . top1 = - 1
self . top2 = size

def push1 ( self , value ) :


if self . top1 < self . top2 - 1 :
self . top1 + = 1
self . array [ self . top1 ] = value
else :
print ( " Stack Overflow - Stack 1 " )

def push2 ( self , value ) :


if self . top1 < self . top2 - 1 :
self . top2 - = 1
self . array [ self . top2 ] = value
else :
print ( " Stack Overflow - Stack 2 " )

def pop1 ( self ) :


if self . top1 > = 0 :
value = self . array [ self . top1 ]
self . top1 - = 1
return value
else :
print ( " Stack 1 is empty " )

def pop2 ( self ) :


if self . top2 < self . size :
value = self . array [ self . top2 ]
self . top2 + = 1
return value
else :
print ( " Stack 2 is empty " )
# input
two_stacks = TwoStacks ( 5 )
two_stacks . push1 ( 1 )
two_stacks . push1 ( 2 )
two_stacks . push2 ( 3 )
two_stacks . push2 ( 4 )
two_stacks . push2 ( 5 )

# output
print ( " Popped from Stack 1 : " , two_stacks . pop1 () )
print ( " Popped from Stack 2 : " , two_stacks . pop2 () )

Explanation of the code :

• Initialization:
(a) The constructor init initializes the two stacks with a specified size.
(b) It creates an array with size slots, initializes top1 to -1, and top2 to the size of the
array.
• Push Operations:
(a) push1 method is used to push an element onto Stack 1 (top1 is incremented, and the
value is placed at the new top1 position).
(b) push2 method is used to push an element onto Stack 2 (top2 is decremented, and the
value is placed at the new top2 position).
(c) The push operations include checks to prevent stack overflow.
• Pop Operations:
(a) pop1 method pops an element from Stack 1 (top1 is decremented, and the value at the
new top1 is returned).
(b) pop2 method pops an element from Stack 2 (top2 is incremented, and the value at the
new top2 is returned).
(c) The pop operations include checks to prevent popping from an empty stack.
• Time Complexity:
– The time complexity of each operation (push and pop) is O(1). The checks for stack
overflow and underflow are constant time operations.
– The overall best time complexity for each operation is O(1), making this implementa-
tion efficient for stack operations. The constant time complexity is achieved because
all operations involve simple array index manipulations.
.
2. Implement stack using queues.
Here is the code implemention of the following problem :

from queue import Queue

class StackUsingQueues :
def __init__ ( self ) :
self . queue1 = Queue ()
self . queue2 = Queue ()

def push ( self , value ) :


self . queue1 . put ( value )

def pop ( self ) :


if self . queue1 . qsize () = = 0 :
print ( " Stack is empty " )
return None

while self . queue1 . qsize () > 1 :


self . queue2 . put ( self . queue1 . get () )

popped_value = self . queue1 . get ()

# Swap queues
self . queue1 , self . queue2 = self . queue2 , self . queue1

return popped_value

st ac k_ usin g_ qu eu es = StackUsingQueues ()
st ac k_ usin g_ qu eu es . push ( 1 )
st ac k_ usin g_ qu eu es . push ( 2 )
st ac k_ usin g_ qu eu es . push ( 3 )

print ( " Popped : " , st ac k_ us in g_ qu eu es . pop () )

Explanation of the code :

• Initialization:
(a) The constructor init initializes two queues (queue1 and queue2).
• Push Operations:
(a) push method is used to push an element onto the stack.
(b) The element is enqueued into queue1.
• Pop Operations:
(a) pop method is used to pop an element from the stack.
(b) If queue1 is empty, it prints a message indicating that the stack is empty.
(c) If queue1 is not empty, it transfers elements from queue1 to queue2 until there is only
one element left in queue1.
(d) The last element in queue1 is dequeued and returned as the popped value.
(e) The queues (queue1 and queue2) are then swapped, making queue2 the new queue1.
• Time Complexity:
– The time complexity of the push operation is O(1), as it involves a single enqueue
operation.
– The time complexity of the pop operation is O(N), where N is the number of elements
in the stack.
– The pop operation involves transferring elements between two queues, and in the worst
case, all elements need to be moved.
– The overall time complexity is O(N) for the pop operation, making it less efficient than
a stack implemented with two arrays. If the stack has a large number of elements, the
pop operation can be costly.
.

3. Reverse a link list using stack.


Here is the code implemention of the following problem :

class ListNode :
def __init__ ( self , value =0 , next = None ) :
self . value = value
self . next = next

def r e ver s e_ l i nk e d _l i s t ( head ) :


stack = [ ]
current = head

while current :
stack . append ( current )
current = current . next

new_head = stack . pop () if stack else None


current = new_head

while stack :
current . next = stack . pop ()
current = current . next

current . next = None

return new_head

# Input
# 1 -> 2 -> 3 -> 4
linked_list = ListNode (1 , ListNode (2 , ListNode (3 , ListNode ( 4 ) ) ) )
reversed_head = r e v er s e _l i n ke d _ li s t ( linked_list )

# Print the reversed linked list


while reversed_head :
print ( reversed_head . value , end = " " )
reversed_head = reversed_head . next

Explanation of the code :

• ListNode Class:
(a) ListNode is a simple class representing a node in a singly-linked list.
(b) Each node has a value attribute representing the node’s value and a next attribute
pointing to the next node in the list.
• Reverse Linked List Function:
(a) reverse linked list takes the head of a linked list as input and reverses the linked list
using a stack.
(b) It uses a stack to store nodes in the original order while traversing the linked list.
(c) After pushing all nodes onto the stack, it pops nodes from the stack to build the
reversed linked list.
(d) The new head variable is assigned the last node popped from the stack, and current is
used to traverse the reversed list.
(e) The original links are updated, and the last node’s next is set to None to mark the end
of the reversed list.
(f) The reversed head (new head) is returned.
• Example Usage:
(a) The code creates a linked list with values 1, 2, 3, and 4.
(b) It then calls the reverse linked list function to reverse the linked list.
(c) Finally, it prints the reversed linked list.
• Time Complexity:
– The time complexity of building the stack while traversing the linked list is O(N),
where N is the number of nodes in the linked list.
– The time complexity of popping nodes from the stack and updating links is also O(N).
– Therefore, the overall time complexity is O(N), making it efficient for reversing linked
lists of any size.
– The space complexity is also O(N) due to the stack used to store the nodes.
.
Task 2
Given a stack with push(), pop(), and empty() operations. The task is to delete the middle element
of it without using any additional data structure. Remember you can not use size() operations.

Test case 1 :
Input : [34, 3, 31, 40, 98, 92, 23]
Output : [34, 3, 31, 98, 92, 23]

Here is the code implemention of the following problem :

def delete_middle ( stack ) :


if not stack :
return

mid = len ( stack ) // 2


d e l e t e _ m i d d l e _ r e c u r s i v e ( stack , mid )

def d e l e t e _ m i d d l e _ r e c u r s i v e ( stack , mid , current_index = 0 ) :


if not stack :
return

popped_element = stack . pop ()

if current_index ! = mid :
d e l e t e _ m i d d l e _ r e c u r s i v e ( stack , mid , current_index + 1 )

if current_index ! = mid :
stack . append ( popped_element )

# Input
stack = [ 34 , 3 , 31 , 40 , 98 , 92 , 23 ]
delete_middle ( stack )

print ( " Output : " , stack )

Explanation of the code :

• delete middle Function:

1. The function takes a stack as input and calculates the middle index (mid) of the stack.
2. It then calls the recursive helper function delete middle recursive to remove the middle
element.

• delete middle recursive Function:

1. This recursive function pops elements from the stack until it reaches the middle element.
2. Once the middle element is reached, it starts pushing back the popped elements to the
stack.
3. The purpose of popping and pushing back is to delete the middle element without using
additional data structures.
4. The recursion stops when the end of the stack is reached.

• Example Usage:

1. The code creates a stack with elements [34, 3, 31, 40, 98, 92, 23].
2. It then calls the delete middle function to remove the middle element from the stack.
3. The result is printed.

• Time Complexity:

– The time complexity of the code is O(N), where N is the number of elements in the stack.
– The recursive function makes N calls, and in each call, it either pops or pushes an element
to the stack, resulting in a linear time complexity.
– The space complexity of the recursive calls is O(N) due to the depth of the recursion stack.
– The overall time complexity is O(N), which is the best achievable in this case without using
additional data structures.

.
Task 3
Given an array and an integer K, find the maximum for every contiguous sub array of size K.

Test case 1 :
Input: [1, 2, 3, 1, 4, 5, 2, 3, 6] and K = 3
Output: 3 3 4 5 5 5 6

Here is the code implemention of the following problem :

from collections import deque

def max_of_subarrays ( arr , k ) :


result = [ ]
dq = deque ()

for i in range ( k ) :
while dq and arr [ i ] > = arr [ dq [ - 1 ] ] :
dq . pop ()
dq . append ( i )

for i in range (k , len ( arr ) ) :


result . append ( arr [ dq [ 0 ] ] )

while dq and dq [ 0 ] < = i - k :


dq . popleft ()

while dq and arr [ i ] > = arr [ dq [ - 1 ] ] :


dq . pop ()

dq . append ( i )

result . append ( arr [ dq [ 0 ] ] )

return result

# Input :
arr = [1 , 2 , 3 , 1 , 4 , 5 , 2 , 3 , 6 ]
K = 3
output = max_of_subarrays ( arr , K )
print ( " Output : " , output )

Explanation of the code :

• Function Explanation:

1. The function takes two parameters: arr, the input array, and K, the size of the subarrays.
2. It uses a deque (dq) to keep track of indices of elements in the current window, ensuring
that the front of the deque always stores the index of the maximum element in the current
window.
3. It iterates through the array, maintaining the deque to store indices of elements in descend-
ing order of their values.

• First Loop (Initialization):

1. It processes the first K elements separately to initialize the deque.


• Second Loop (Sliding Window):

1. It iterates from index K to the end of the array.


2. For each index i, it appends the maximum element of the current window to the result list.
3. It then removes elements from the front of the deque that are outside the current window.
4. It removes elements from the back of the deque that are smaller than or equal to the current
element arr[i].
5. It appends the index i to the deque.

• Last Element Handling:

1. After the loop, the maximum of the last window is appended to the result.

• Example Usage:

1. The code provides an example usage with the input array arr = [1, 2, 3, 1, 4, 5, 2, 3, 6]
and K = 3.
2. It prints the result, which represents the maximum element for every contiguous subarray
of size 3.

• Time Complexity:

– The time complexity of this code is O(N), where N is the length of the input array.
– Both loops iterate through the array once, and the deque operations (appending, popping)
take constant time.
– The overall time complexity is linear, making it an efficient solution for finding the maxi-
mum of subarrays.

.
Task 4
Given two lists sorted in increasing order, create and return a new list representing the intersection of
the two lists. The new list should be made with its memory — the original lists should not be changed.

Test case 1 :
Input:
First linked list: 1 2 3 4 6
Second linked list: 2 4 6 8
Output: 2 4 6

Here is the code implemention of the following problem :

def f ind_in tersec tion ( list1 , list2 ) :


result = [ ]
i, j = 0, 0

while i < len ( list1 ) and j < len ( list2 ) :


if list1 [ i ] = = list2 [ j ] :
result . append ( list1 [ i ] )
i += 1
j += 1
elif list1 [ i ] < list2 [ j ] :
i += 1
else :
j += 1

return result

# Input :
list1 = [1 , 2 , 3 , 4 , 6 ]
list2 = [2 , 4 , 6 , 8 ]
intersection = fi nd_int ersect ion ( list1 , list2 )
print ( " Output : " , intersection )

Explanation of the code :

• Function Explanation:

1. The function takes two sorted lists, list1 and list2, as input.
2. It initializes an empty list result to store the intersection elements.
3. Two pointers, i and j, are initialized at the beginning of list1 and list2, respectively.

• While Loop (Finding Intersection):

1. The loop continues as long as both pointers (i and j) are within the bounds of their respec-
tive lists.
2. If the elements at the current positions are equal, it means an intersection is found. The
common element is appended to the result list, and both pointers are incremented.
3. If the element in list1 is less than the element in list2, the pointer in list1 (i) is incremented.
4. If the element in list2 is less than the element in list1, the pointer in list2 (j) is incremented.

• Result:

1. The result list contains the elements that are common in both input lists.
• Example Usage:

1. The code provides an example usage with two sorted lists, list1 = [1, 2, 3, 4, 6] and list2
= [2, 4, 6, 8].
2. It prints the result, which represents the intersection of the two lists.

• Time Complexity:

– The time complexity of this code is O(min(M,N)), where M and N are the lengths of the
input lists.
– The while loop iterates until one of the pointers reaches the end of its respective list.
– In the worst case, where both lists are of length N, the time complexity is O(N).
– In scenarios where the lists have different lengths, the time complexity is determined by
the shorter list, resulting in O(min(M,N)).
– The space complexity is O(1) as no additional data structures are used to store the inter-
section elements.

You might also like