Assignment
Assignment
Sabah Nushra
ID: 190041227
CSE-2A
12 February 2024
Task 1
Implement the algorithms (python is preferred) discussed in the class:
class TwoStacks :
def __init__ ( self , size ) :
self . size = size
self . array = [ None ] * size
self . top1 = - 1
self . top2 = size
# output
print ( " Popped from Stack 1 : " , two_stacks . pop1 () )
print ( " Popped from Stack 2 : " , two_stacks . pop2 () )
• 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 :
class StackUsingQueues :
def __init__ ( self ) :
self . queue1 = Queue ()
self . queue2 = Queue ()
# 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 )
• 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.
.
class ListNode :
def __init__ ( self , value =0 , next = None ) :
self . value = value
self . next = next
while current :
stack . append ( current )
current = current . next
while stack :
current . next = stack . pop ()
current = current . next
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 )
• 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]
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 )
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.
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
for i in range ( k ) :
while dq and arr [ i ] > = arr [ dq [ - 1 ] ] :
dq . pop ()
dq . append ( i )
dq . append ( i )
return result
# Input :
arr = [1 , 2 , 3 , 1 , 4 , 5 , 2 , 3 , 6 ]
K = 3
output = max_of_subarrays ( arr , K )
print ( " Output : " , output )
• 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.
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
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 )
• 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.
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.