Difficulty: Medium, Asked-in: Facebook, Amazon, Microsoft.
Key takeaway: An excellent problem to learn problem-solving using hashing and sliding window approach.
You are given an array X[] of size n and an integer k, write a program to count the distinct elements present in every k sized window of the array.
Input: X[ ] = [1, 1, 1, 3, 4, 2, 3], k = 4, Output: [2, 3, 4, 3].
Explanation:
Input: X[ ] = [1, 2, 4, 4, 2], k = 3, Output: [3, 2, 2].
Explanation:
The basic idea would be to consider each window of size k while traversing the array and simultaneously counting the distinct elements in each window.
We declare an output array of size n - k + 1 to store the number of distinct elements in each window. An array of size n contains n - k + 1 windows of size k.
Now we traverse the array using nested loops to explore each window.
getDistinctCount(X, n, k)
output = array of size n - k + 1
// Process each window of size k
for i from 0 to n - k
distCount = 0
// Check each element in the current window
for j from i to i + k - 1
distCount = distCount + 1
// Check whether X[j] already appears earlier in this window
for p from i to j - 1
if X[j] == X[p]
distCount = distCount - 1
break
output[i] = distCount
return outputHere we are using three nested loops: the first loop runs n - k + 1 times, the second loop runs k times, and the third loop runs j - i times or k times in the worst case. So total number of loop iterations in the worst case = (n - k + 1) * k * k = O(n*k^2). At each step of the iteration, we are doing O(1) operations, so time complexity = O(n*k^2)* O(1) = O(n*k^2).
Space complexity = O(1), we are using constant extra space to generate the output. Think!
Now, the critical question is: Can we improve the time complexity of the above approach? In the inner loop, searching for duplicates is a key operation. We check whether X[j] appears in the prefix [i, j - 1] of the current window. Could we sort each window and use binary search? Let’s think!
The idea is simple: sort each window, then use binary search to check whether X[j] appears in the window’s prefix, from index i to j - 1. If X[j] does not appear in the prefix, we increment distCount for that window.
getDistinctCount(X, n, k)
output = array of size n - k + 1
// Process each window of size k
for i from 0 to n - k
distCount = 0
// Sort the current window
sort X from index i to i + k - 1
// Count values that do not appear earlier in the sorted window
for j from i to i + k - 1
distCount = distCount + 1
index = binarySearch(X, i, j - 1, X[j])
if index != -1
distCount = distCount - 1
output[i] = distCount
return outputTime complexity = (n - k + 1) * (Time complexity of sorting k size window + Time complexity of searching X[j] using binary search) = (n - k + 1) * (O(k*logk) + k * O(logk)) = O(n) * O(k*logk) = O(n*k*logk)
Space complexity = O(1), we are using constant extra space to generate the output. Think!
Can we further optimise the above approach? Let's think!
After sorting the window in the above approach, all duplicate elements will be placed adjacent to each other. So rather than applying binary search for each window element in a loop, we can count distinct elements using a single loop or single traversal of the window. Here, we can use the idea of the fast and slow pointer used in this blog: Remove duplicates from sorted array.
getDistinctCount(X, n, k)
output = array of size n - k + 1
// Process each window of size k
for i from 0 to n - k
// Sort the current window
sort X from index i to i + k - 1
slow = i
fast = i + 1
distCount = 1
// Count changes between consecutive values
while fast < i + k
if X[slow] != X[fast]
distCount = distCount + 1
slow = slow + 1
fast = fast + 1
output[i] = distCount
return outputTime complexity = (n - k + 1) * (Time complexity of sorting k size window + Time complexity of counting unique elements in the current window) = (n - k + 1) * (O(klogk) + O(k)) = O(n) * O(klogk) = O(n*k*logk). Here time complexity looks similar to the above approach but it is slightly better in terms of optimzation.
Space complexity = O(1), we are using constant extra space to generate the output. Think!
Based on the above approach, sorting contributes a significant factor in the time complexity. Can we think of counting unique elements in a window without sorting? Can we apply the idea of hashing in a window to improve the time complexity of searching distinct elements? Let's think!
The idea is: we traverse each window of size k using a loop and use a hash table to store unique elements of that window. Whenever a window element is not present in the hash table, we insert it there and increase the duplicate count by 1. Think!
getDistinctCount(X, n, k)
output = array of size n - k + 1
// Count distinct elements in each window of size k
for i from 0 to n - k
H = empty hash table
distCount = 0
for j from i to i + k - 1
if H does not contain X[j]
H.insert(X[j])
distCount = distCount + 1
output[i] = distCount
return outputTime complexity = (n - k + 1) * Time complexity of counting unique elements using hash table in k size window = O(n) * k * O(1) = O(n*k). Note: the time complexity of searching and insertion in a hash table would be O(1) average. Space complexity = O(k), for using the O(k) size hash table to count distinct elements in the k size window.
Now the critical question is can we solve this problem in O(n) time? Can we use some additional insight from the problem to get the idea of an efficient solution?
From the observation, k size windows are placed linearly adjacent to each other. For example, the first window is [0, k-1], the second window is [1, k], the third window is [2, k+1], and so on. If we observe, in any k size window, the k-1 element would be the same as the previous adjacent window i.e. ignoring the first element of the previous window and including one new element at the end of it.
The idea behind a sliding window is: we can easily calculate the unique element count of any current window if we know the unique element count of the previous window. So we find the count of distinct elements in each window of size k by shifting the window by one right and using the computation done in the previous step.
Now the critical question is: how do we track the unique element count of the current window using the unique element count of the previous window? The idea is: we can use a hash table to store the frequency count of elements in each window. Suppose elements from i to i + k – 1 are stored in a hash table as an element-frequency pair. So for updating the Hash Map in range i + 1 to i + k, we reduce the frequency count of the ith element by one and increase the frequency of (i + k)th element by 1.
We create an empty hash table freq to store the element-frequency pair and Initialize the count of distinct elements to 0 i.e. distCount = 0.
Now we traverse the first window using a loop and store the frequency count of every element in the hash table. Also, keep updating the value of distCount. By end of the loop, we store the distinct count for the first window. Now we traverse through the remaining array and access each window of size k. For each window:
By end of the code, the distinct count of each window will get stored in the output array and we return it.
getDistinctCount(X, n, k)
output = array of size n - k + 1
freq = empty hash table
distCount = 0
// Count distinct elements in the first window
for i from 0 to k - 1
if freq does not contain X[i]
distCount = distCount + 1
freq[X[i]] = freq[X[i]] + 1
output[0] = distCount
// Slide the window across the array
for i from k to n - 1
outgoing = X[i - k]
// Remove the element leaving the window
if freq[outgoing] == 1
distCount = distCount - 1
freq[outgoing] = freq[outgoing] - 1
incoming = X[i]
// Add the element entering the window
if freq[incoming] == 0
distCount = distCount + 1
freq[incoming] = freq[incoming] + 1
output[i - k + 1] = distCount
return outputTime complexity = Time complexity of counting unique elements of the first window + Time complexity of counting unique elements of remaining n - k window using sliding window = O(k) + O(n - k) = O(n). Note: the time complexity of searching and insertion in a hash table would be O(1) average.
Space complexity = O(k), for using the O(k) size hash table.
Please write comments if you find an error or you want to share more insights about the topic. Enjoy learning, Enjoy coding, Enjoy algorithms!