Find Next Biggest
Problem Statement
First we define the notion of next biggest element: for element in an array, the next biggest element is the first element that comes after and is strictly bigger than . You are given an array of integers of size , and let’s call it the input array. There could be duplicate elements in the input array. Design an algorithm which will return an output array that contains the next biggest element for each element in the input array. If a next biggest element does not exist for an element in the input array, repalce the corresponding entry in the output array with . There is a working example below. You have two tasks:
(1) First come up with a naive algorithm that runs in time
(2) Then come up with an efficient algorithm that runs in time.
Follow up question:
Instead of outputting the next biggest element in the output array, what if you are asked to output the distance between the current element and the next biggest element in the output array? Still, if the next biggest element does not exist, the distance will be .
For reference, we have included some python starter code for you to use.
# return an array that contains the next biggest element for each element in the input array using a naive approach
def find_next_biggest_naive(num_list):
# Your code here
# return an array that contains the next biggest element for each element in the input array using a more efficient approach
def find_next_biggest_efficient(num_list):
# Your code here
Working example
Suppose we are given an input array .
(1) If we look at the element of the input array, which is . Its next biggest element is , so the element of the output array is .
(2) If we look at the element of the input array, which is . It does not have a next biggest element, so the element of the output array is .
(3) If we look at the element of the input array, which is . Its next biggest element is , so the element of the output array is .
(4) If we look at the element of the input array, which is . It does not have a next biggest element, so the element of the output array is .
Therefore, the output array is .
Test Cases
Test Case 1
Input: [2, 4, 12, 1, 6, 3]
Output: [4, 12, -1, 6, -1, -1]
Test Case 2
Input: [10, 9, 8, 7, 6, 5, 4, 3, 2, 1]
Output: [-1, -1, -1, -1, -1, -1, -1, -1, -1, -1]
Test Case 3
Input: [4, 4, 12, 1, 6, 3]
Output: [12, 12, -1, 6, -1, -1]
Hint
Click Me
1. What data structure should we use?
A naive algorithm could be to start from the element and go down the array to find the next biggest element, and then we repeat the same procedure for the element, element, and all the way till the end of the array. However, this approach will take in an array like since we have to propagate through the entire array each time. In order to improve our runtime from to , we need to use a data structure to keep track of the elements that have not found their next biggest elements yet. This data structure should achieve two goals. First, we will keep an element in this data structure if we have not found a next biggest element for it yet. Second, we will remove the element from the data structure and update the value in the output array if we have found a next biggest element for it; we will also check if this next biggest element we just found is a next biggest element for any other elements in the data structure and update those elements accordingly if so. This way, we only need to propagate through the entire array once. What data structure could help us achieve these two goals? Also, since we could have multiple elements in this data structure, we might lose track of their relative positions. Besides the elements themselves, what other information should we also keep track of?
Click Below to See the Solution
Click Me
Part 1: Code it Up
Naive Algorithm
For the naive algorithm we start from the element and go down the array to find the next biggest element. If we can find one, we append it to our output array; otherwise, we append to our output array. We repeat this same procedure for the element, element, and all the way till the end of the array.
Code for the above algorithm can be seen below:
def find_next_biggest_naive(num_list):
output = []
i = 0
#use a while loop to iterate through all elements in the input array
while (i < len(num_list)):
#create a flag for each element indicating whether we can find a next biggest element for it
if_next_biggest_exist = False
j = i + 1
#use another while loop to iterate through all elements to the right of the current element
while (j < len(num_list)):
#if we find a next biggest element, update the output array and the flag
if num_list[j] > num_list[i]:
output.append(num_list[j])
if_next_biggest_exist = True
break
j += 1
#if the lfag is not updated, that means we have not found a next biggest element
if not if_next_biggest_exist:
output.append(-1)
i += 1
return output
Efficient Algorithm
As suggested in the hint, we will use a stack to keep track of the elements that have not found their next biggest elements yet. The stack stores pairs of index and value as they are in the input array. Every time when we process a new element in the input array, we pop one pair off the stack and compare its value with the new element. If the new element is strictly larger than the value of the pair, the corresponding entry in the output array will be replaced with the new element. We then pop another pair off the stack and compare its value with the new element and update the output array if needed. We repeat this process until there is nothing left on the stack. If the new element is not larger than the value of the pair, we put the pair back on the stack. We now add the current index and current value to the stack together as a pair. We then move on to the next element in the array. After we have propagated through the entire array this way, for every pair on the stack, we get their indices and set the corresponding entires in the output array to .
Code for the above algorithm can be seen below:
def find_next_biggest_efficient(num_list):
#copy the input array into the output array
output = num_list[:]
#create a stack that keeps track of elements in the format (index, value)
stack = [(0, num_list[0])]
i = 1
#use a while loop to iterate through the entire array
while (i < len(num_list)):
#current_val holds the value of the new element in the input array
current_val = num_list[i]
#use a while loop to iterate through all elements on the stack
while stack:
#pop the pair off top of the stack
pair = stack.pop()
#compare the value of the pair to the current value
if current_val > pair[1]:
#update the output array
index = pair[0]
output[index] = current_val
else:
#put the pair back on stack and move on to the next element in the array
stack += [pair]
break
#add the current index and current value to the stack together as a pair
stack += [(i, num_list[i])]
i += 1
#for elements still on the stack, update their corresponding entries in the output array to -1
while stack:
pair = stack.pop()
index = pair[0]
output[index] = -1
return output
Part 2: Runtime Analysis
For the naive algorithm, if we are given an array whose elements are listed in decreasing order, we have to propagate through the remaining array for each element in the array. Therefore, the runtime of the naive algorithm is .
For the efficient algorithm, by implementing stack in our algorithm, we only need to go through the entire array exactly once. Therefore, the runtime of the efficient algorithm is .
Part 3: Answer to Follow Up Question
In order to output the distance between the current element and the next biggest element, we just need to change one line inside our efficient algorithm. Change output[index] = current_val
to output[index] = i - index
.