Skip to content

Commit e5caaa0

Browse files
committed
feat(algorithms, top-k-elements): smallest range covering alements from k lists, two pointer and brute force
1 parent a18e3cb commit e5caaa0

12 files changed

Lines changed: 249 additions & 2 deletions

algorithms/top_k_elements/smallest_range_covering_k_lists/README.md

Lines changed: 153 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,69 @@ Output: [1,1]
5353

5454
### Optimal Brute Force
5555

56+
We need to find the smallest range that contains at least one number from each of the k sorted lists. At first glance,
57+
a simple brute force solution comes to mind, i.e., checking every combination of elements from the lists to find the
58+
smallest range. However, that would involve too many comparisons and will lead to TLE. Instead, we can refine this
59+
process into something more manageable.
60+
61+
At any moment, we need to select one number from each list. So, to find the smallest range, we need to minimize the
62+
difference between the largest and smallest numbers chosen at each step. The important point here is that, at any time,
63+
our range is defined by the smallest number chosen and the largest number chosen.
64+
65+
So we need to select the smallest number among the current numbers picked from each list and move forward by choosing the
66+
next number from the same list that gave us this smallest number. This makes sense because moving forward in any other
67+
list would only increase the range, which we want to avoid. We repeat this process of updating the smallest number and
68+
checking if the new range is smaller than our previously found range. If it is, we update the range.
69+
70+
We continue this until we reach the end of one of the lists because, at that point, it’s no longer possible to select a
71+
number from each list.
72+
73+
Algorithm steps:
74+
75+
- Initialize k to the number of lists in nums and create an array indices to keep track of the current index of each
76+
list, initializing all to 0.
77+
- Initialize an array range to store the smallest range, starting with {0, INT_MAX}.
78+
- Enter an infinite loop:
79+
- Initialize curMin to INT_MAX, curMax to INT_MIN, and minListIndex to 0.
80+
- Iterate over each list to find the current minimum and maximum values:
81+
- For each list i, retrieve the current element using indices[i].
82+
- Update curMin if the current element is less than curMin, and set minListIndex to i.
83+
- Update curMax if the current element is greater than curMax.
84+
- After checking all lists, if the difference curMax - curMin is smaller than the current range (range[1] - range[0]),
85+
update range to {curMin, curMax}.
86+
- Move to the next element in the list that had the minimum value by incrementing indices[minListIndex].
87+
- If the updated index equals the size of nums[minListIndex], break the loop (all elements have been processed).
88+
- Return the smallest range stored in range.
89+
90+
> Note: Due to Python's relatively slower execution speed, the optimal brute-force solution will lead to a Time Limit
91+
> Exceeded (TLE) error when using Python3. However, this same solution will perform adequately in other programming
92+
> languages.
93+
94+
#### Complexity Analysis
95+
96+
Let `n` be the total number of elements across all lists and k be the number of lists.
97+
98+
##### Time Complexity
99+
100+
In each iteration of the while (true) loop, we traverse all k lists to find the current minimum and maximum. This
101+
takes O(k) time.
102+
103+
The loop continues until at least one of the lists is fully traversed. In the worst case, every element from every list
104+
is visited, and the total number of elements across all lists is n. Therefore, the loop runs O(n) times.
105+
106+
Overall, the time complexity becomes O(n⋅k).
107+
108+
##### Space Complexity
109+
110+
The space complexity is dominated by the indices and range arrays, both of which have size proportional to k, the number
111+
of lists.
112+
113+
The indices array stores the current index of each list, so it takes O(k) space.
114+
115+
The range array also stores two integers, so it takes O(1) space.
116+
117+
Hence, the overall space complexity is O(k).
118+
56119
### Heap (Priority Queue)
57120

58121
The core idea of this solution is to find the smallest range that includes at least one number from each of the k sorted
@@ -140,3 +203,93 @@ the output range (two integers) is negligible and does not contribute to the ove
140203

141204
### Two-Pointer
142205

206+
Since we need a range that includes one number from each of the k lists, we can think of this as a subarray problem.
207+
However, the numbers are spread across multiple lists. To simplify, we can combine all the lists into a single sorted
208+
list of numbers. When merging, we also keep track of which list each number came from, since the problem requires at
209+
least one number from each original list in the final range.
210+
211+
Once we have the merged list, the problem becomes finding the smallest range (or subarray) in this list that contains at
212+
least one element from each of the original k lists. This is a common scenario for a sliding window or two-pointer
213+
approach: we want to expand and shrink the window (subarray) dynamically to find the minimum range that meets the criteria.
214+
215+
The right pointer will expand the window by moving forward in the merged list, and the left pointer will shrink the
216+
window once we know the window contains at least one element from each list.
217+
218+
As the right pointer moves through the merged list, we need to ensure that the current subarray includes at least one
219+
number from each list. So we keep track of how many lists are "covered" by the current subarray (i.e., how many of the
220+
k lists have at least one number in the current window).
221+
222+
Once all lists are covered, the window between the left and right pointers represents a valid range. We then check if
223+
this range is the smallest we've found so far.
224+
225+
After finding a valid range, we need to shrink the window (move the left pointer forward) to see if we can make the range
226+
even smaller while still keeping one number from each list in the subarray. As we move the left pointer forward, we check
227+
if we lose coverage from any list. If we do, we stop shrinking and start expanding the window again by moving the right
228+
pointer.
229+
230+
We will continue this until we can no longer expand the window (i.e., the right pointer reaches the end of the merged list).
231+
By this point, we have explored all possible ranges, and the smallest valid range is our final answer.
232+
233+
![Solution 2.1](./images/solutions/smallest_range_covering_elements_from_k_lists_two_pointer_solution_1.png)
234+
![Solution 2.2](./images/solutions/smallest_range_covering_elements_from_k_lists_two_pointer_solution_2.png)
235+
![Solution 2.3](./images/solutions/smallest_range_covering_elements_from_k_lists_two_pointer_solution_3.png)
236+
![Solution 2.4](./images/solutions/smallest_range_covering_elements_from_k_lists_two_pointer_solution_4.png)
237+
![Solution 2.5](./images/solutions/smallest_range_covering_elements_from_k_lists_two_pointer_solution_5.png)
238+
![Solution 2.6](./images/solutions/smallest_range_covering_elements_from_k_lists_two_pointer_solution_6.png)
239+
![Solution 2.7](./images/solutions/smallest_range_covering_elements_from_k_lists_two_pointer_solution_7.png)
240+
![Solution 2.8](./images/solutions/smallest_range_covering_elements_from_k_lists_two_pointer_solution_8.png)
241+
![Solution 2.9](./images/solutions/smallest_range_covering_elements_from_k_lists_two_pointer_solution_9.png)
242+
243+
Algorithm steps:
244+
245+
- Initialize an empty array `merged` to store pairs of numbers and their respective list indices.
246+
- Merge all lists into `merged`:
247+
- For each list in `nums`, iterate through its numbers and add each number along with its list index to merged.
248+
- Sort the `merged` array to facilitate the two-pointer technique.
249+
- Initialize a frequency map `freq` to keep track of how many times each list is represented in the current window.
250+
- Set the `left` pointer to 0, `count` to 0, and initialize `rangeStart` to 0 and `rangeEnd` to INT_MAX.
251+
- Use a `right` pointer to iterate through the merged array:
252+
- Increment the count for the list index in `freq` for `merged[right]`.
253+
- If the count for this list index becomes 1, increment `count` (indicating a new list is represented).
254+
- When all lists are represented (i.e., `count == nums.size()`):
255+
- Calculate the current range as `curRange = merged[right].first - merged[left].first`.
256+
- If `curRange` is smaller than the previously found range (`rangeEnd - rangeStart`):
257+
- Update rangeStart and rangeEnd to the current numbers.
258+
- Decrement the frequency count for the leftmost number (i.e., `merged[left]`).
259+
- If this list index's frequency becomes 0, decrement `count` (indicating that a list is no longer represented).
260+
- Move the `left` pointer to the right to attempt shrinking the window.
261+
- After completing the iteration, return the smallest range as a array containing `rangeStart` and `rangeEnd`.
262+
263+
#### Complexity Analysis
264+
265+
Let `n` be the total number of elements across all lists and `k` be the number of lists.
266+
267+
##### Time Complexity
268+
269+
The first nested loop iterates over `k` lists, and for each list, it iterates through its elements. In the worst case,
270+
this requires `O(n)` time since we are processing all elements once.
271+
272+
After merging, we sort the merged array which contains n elements. Sorting has a time complexity of `O(nlog(n))`.
273+
274+
The two-pointer approach iterates through the merged list once (with the right pointer) and may also move the left
275+
pointer forward multiple times. In total, each pointer will traverse the merged list at most `n` times.
276+
277+
Combining these steps, the overall time complexity is: `O(nlog(n))`
278+
279+
##### Space Complexity
280+
281+
We create a merged array to hold n elements, which requires `O(n)` space.
282+
283+
We use an unordered map (`freq`) that can potentially store `k` elements (one for each list). Thus, this requires `O(k)`
284+
space.
285+
286+
Some extra space is used when we sort an array. The space complexity of the sorting algorithm (S) depends on the
287+
programming language.
288+
289+
- In Python, the sort method sorts a list using the Timsort algorithm which is a combination of Merge Sort and Insertion
290+
Sort and has a space complexity of `O(n)`.
291+
- In C++, the sort() function is implemented as a hybrid of Quick Sort, Heap Sort, and Insertion Sort, with a worst-case
292+
space complexity of `O(log(n))`.
293+
- In Java, Arrays.sort() is implemented using a variant of the Quick Sort algorithm which has a space complexity of `O(log(n))`.
294+
295+
Combining these, the overall space complexity is: `O(n)`

algorithms/top_k_elements/smallest_range_covering_k_lists/__init__.py

Lines changed: 77 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
from typing import List, Tuple
1+
from collections import defaultdict
2+
from typing import List, Tuple, DefaultDict
23
import heapq
34

45

@@ -44,3 +45,78 @@ def smallest_range(nums: List[List[int]]) -> List[int]:
4445

4546
# Return the smallest range found
4647
return [range_start, range_end]
48+
49+
50+
def smallest_range_two_pointer(nums: List[List[int]]) -> List[int]:
51+
merged: List[Tuple[int, int]] = []
52+
53+
# merge all lists with their list index
54+
for list_idx, num_list in enumerate(nums):
55+
for num in num_list:
56+
merged.append((num, list_idx))
57+
58+
# sor the merged list
59+
merged.sort()
60+
61+
# Two pointers to track the smallest range
62+
freq: DefaultDict[int, int] = defaultdict(int)
63+
left, count = 0, 0
64+
range_start, range_end = 0, float("inf")
65+
66+
for right in range(len(merged)):
67+
val = merged[right][1]
68+
freq[val] += 1
69+
if freq[val] == 1:
70+
count += 1
71+
72+
# when all lists are represented, try to shrink the window
73+
while count == len(nums):
74+
current_range = merged[right][0] - merged[left][0]
75+
if current_range < range_end - range_start:
76+
range_start = merged[left][0]
77+
range_end = merged[right][0]
78+
79+
freq[merged[left][1]] -= 1
80+
if freq[merged[left][1]] == 0:
81+
count -= 1
82+
83+
left += 1
84+
85+
return [range_start, range_end]
86+
87+
88+
def smallest_range_brute_force(nums: List[List[int]]) -> List[int]:
89+
k = len(nums)
90+
# Stores the current index of each list
91+
indices = [0] * k
92+
# To track the smallest range
93+
range_list = [0, float("inf")]
94+
95+
while True:
96+
cur_min, cur_max = float("inf"), float("-inf")
97+
min_list_index = 0
98+
99+
# Find the current minimum and maximum values across the lists
100+
for i in range(k):
101+
current_element = nums[i][indices[i]]
102+
103+
# Update the current minimum
104+
if current_element < cur_min:
105+
cur_min = current_element
106+
min_list_index = i
107+
108+
# Update the current maximum
109+
if current_element > cur_max:
110+
cur_max = current_element
111+
112+
# Update the range if a smaller one is found
113+
if cur_max - cur_min < range_list[1] - range_list[0]:
114+
range_list[0] = cur_min
115+
range_list[1] = cur_max
116+
117+
# Move to the next element in the list that had the minimum value
118+
indices[min_list_index] += 1
119+
if indices[min_list_index] == len(nums[min_list_index]):
120+
break
121+
122+
return range_list
Loading
Loading
Loading
Loading
Loading
Loading
Loading
Loading

0 commit comments

Comments
 (0)