Skip to content

Commit b3facec

Browse files
committed
refactor(algorithms, binary-search): find k closest elements
1 parent 5b5bb62 commit b3facec

14 files changed

+179
-6
lines changed

algorithms/heap/kclosestelements/README.md

Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,3 +27,116 @@ k = 2
2727
Output:
2828
[8, 9]
2929
```
30+
31+
## Constraints
32+
33+
- 1 <= target <= nums.length
34+
- 1 <= nums.length <= 104
35+
- nums is sorted in ascending order.
36+
- -10^4 <= nums[i], x <= 10^4
37+
38+
## Topics
39+
40+
- Array
41+
- Two Pointers
42+
- Binary Search
43+
- Sliding Window
44+
- Sorting
45+
- Heap (Priority Queue)
46+
47+
## Solution
48+
49+
### Using Optimized Binary Search
50+
51+
The key idea behind this algorithm is to efficiently locate the k numbers in a sorted array closest to a given target
52+
value by minimizing unnecessary comparisons. First, the algorithm uses binary search to find the element nearest to the
53+
target. Once this element is identified, the algorithm employs a two-pointer approach to establish a sliding window of
54+
size k around this element. Starting with this element’s previous and next elements of this element, keep expanding
55+
toward the left and right boundaries, choosing the closest element to the target value at each step.
56+
57+
Now, let’s look at the workflow of the implementation:
58+
59+
Before we proceed to the optimized approach, a few points need some consideration:
60+
61+
- If the length of nums is the same as the value of k, return all the elements.
62+
- If target is less than or equal to the first element in nums, the first k elements in nums are the closest integers to
63+
target. For example, if nums= [1, 2, 3], target= 0, and k = 2, then the two closest integers to target are [1, 2].
64+
- If target is greater than or equal to the last element in nums, the last k elements in nums are the closest integers to
65+
target. For example, if nums= [1, 2, 3], target= 4, and k = 2, then the two closest integers to target are [2, 3].
66+
- Otherwise, we search for the k closest elements in the whole array.
67+
68+
When we have to find k elements in the complete array, instead of traversing the whole array, we can use binary search
69+
to limit our search to the relevant parts. The optimized approach can be divided into two parts:
70+
71+
- Use binary search to find the index of the first closest integer to target in nums.
72+
- Use two pointers, window_left and window_right, to maintain a sliding window. We move the pointers conditionally,
73+
either towards the left or right, to expand the window until its size gets equal to k. The k elements in the window are
74+
the k closest integers to target.
75+
76+
Here’s how we’ll implement this algorithm:
77+
78+
- If the length of nums is the same as k, return nums.
79+
- If target ≤ nums[0], return the first k elements in nums.
80+
- If target ≥ nums[len(nums) - 1], return the last k elements in nums.
81+
- Use binary search to find the index, first_closest, of the closest integer to target.
82+
- Initialize two pointers, left and right, to 0 and len(nums)-1, respectively.
83+
- Calculate the index of the middle pointer, mid, and check:
84+
- If the value pointed to by mid is equal to target, i.e., nums[mid] = target, return mid.
85+
- If nums[mid] < target, move left toward the right.
86+
- If nums[mid] > target, move right toward the left.
87+
- Once we have found the closest element to target, return the index, first_closest, which points to it.
88+
- Create two pointers, window_left and window_right. The window_left pointer initially points to the index of the element
89+
that is to the left of nums[first_closest], and window_right points to the element that is to the right of window_left.
90+
This means window_left = nums[first_closest] - 1, and window_right = window_left + 1.
91+
- Traverse nums while the sliding window size is less than k. In each loop, adjust the window size by moving the pointers
92+
as follows:
93+
- If nums[window_left] is closer to target than nums[window_right], or if both are at equal distance, that is,
94+
|nums[window_left] - target| ≤ |nums[window_right] - target|, then window_left = window_left - 1.
95+
- If nums[window_right] is closer to target than nums[window_left], that is, |nums[window_right] - target| <
96+
|nums[window_left] - target|, then window_right = window_right + 1.
97+
- Once we have k elements in the window, return them as the output.
98+
99+
![Solution 1](./images/solutions/k_closest_elements_with_modified_binary_search_solution_1.png)
100+
![Solution 2](./images/solutions/k_closest_elements_with_modified_binary_search_solution_2.png)
101+
![Solution 3](./images/solutions/k_closest_elements_with_modified_binary_search_solution_3.png)
102+
![Solution 4](./images/solutions/k_closest_elements_with_modified_binary_search_solution_4.png)
103+
![Solution 5](./images/solutions/k_closest_elements_with_modified_binary_search_solution_5.png)
104+
![Solution 6](./images/solutions/k_closest_elements_with_modified_binary_search_solution_6.png)
105+
![Solution 7](./images/solutions/k_closest_elements_with_modified_binary_search_solution_7.png)
106+
![Solution 8](./images/solutions/k_closest_elements_with_modified_binary_search_solution_8.png)
107+
![Solution 9](./images/solutions/k_closest_elements_with_modified_binary_search_solution_9.png)
108+
![Solution 10](./images/solutions/k_closest_elements_with_modified_binary_search_solution_10.png)
109+
110+
To summarize, we use binary search to locate the first closest element to target, then create a sliding window using two
111+
pointers to select the k closest elements. The window adjusts its size by moving the pointers based on which adjacent
112+
element is closer to the target. Eventually, the window will have the required k elements, which are then returned.
113+
114+
#### Time Complexity
115+
116+
The time complexity of the binary search is O(logn), where n is the length of the input array nums. The sliding window
117+
step involves traversing the array once while adjusting the window size, and it takes O(k) time. The overall time
118+
complexity becomes O(logn+k).
119+
120+
#### Space Complexity
121+
122+
The space complexity of this solution is O(1).
123+
124+
### Alternative Solution
125+
126+
Now, let’s see another way to solve this problem with slightly better time complexity. In this approach, we focus on
127+
choosing the left bound for binary search such that the search space reduces to n - k.
128+
129+
We initialize the left pointer to 0 and the right pointer to len(nums) - k. These values are assigned based on the
130+
observation that the left bound can’t exceed len(nums) - k to ensure we have enough elements for the window.
131+
132+
Next, while left < right, we perform a binary search to find the optimal position for the left bound of the sliding
133+
window. We calculate mid and compare the distances between target and the elements at nums[mid] and nums[mid + k]. If
134+
|nums[mid] - target| is greater than |nums[mid + k] - target|, it means the element at nums[mid] is farther from target
135+
compared to the element at nums[mid + k]. In this case, it updates left to mid + 1, shifting the left bound to the right.
136+
Otherwise, it updates the right to mid, moving the right bound closer to the left.
137+
138+
Once the while loop completes, return the elements of nums starting from left and including the next k elements. These
139+
elements represent the k closest elements to target. The creation of this list will take O(k) time.
140+
141+
Since the initial search space has a size of n - k, the binary search takes O(log(n−k)). Therefore, the time complexity
142+
of this solution is O(log(n−k)+k). The space complexity remains O(1).

algorithms/heap/kclosestelements/__init__.py

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
from typing import List
22
import heapq
3+
from algorithms.search.binary_search import binary_search
34

45

56
def k_closest(nums: List[int], k: int, target: int):
@@ -16,3 +17,49 @@ def k_closest(nums: List[int], k: int, target: int):
1617
distances = [pair[1] for pair in heap]
1718
distances.sort()
1819
return distances
20+
21+
22+
def find_closest_elements(nums: List[int], k: int, target: int):
23+
# If the length of 'nums' is the same as k, return 'nums'
24+
if len(nums) == k:
25+
return nums
26+
27+
# if target is less than or equal to first element in 'nums',
28+
# return the first k elements from 'nums'
29+
if target <= nums[0]:
30+
return nums[0:k]
31+
32+
# if target is greater than or equal to last element in 'nums',
33+
# return the last k elements from 'nums'
34+
if target >= nums[-1]:
35+
return nums[len(nums) - k : len(nums)]
36+
37+
# find the first closest element to target using binary search
38+
first_closest = binary_search(nums, target, modified=True)
39+
40+
# initilaize the sliding window pointers
41+
window_left = first_closest - 1
42+
window_right = window_left + 1
43+
44+
# expand the sliding window untill its size becomes equal to k
45+
while (window_right - window_left - 1) < k:
46+
# if window's left pointer is going out of bounds move the window rightward
47+
if window_left == -1:
48+
window_right += 1
49+
continue
50+
51+
# if window's right pointer is going out of bounds
52+
# OR if the element pointed to by window's left pointer is closer to target than
53+
# the element pointed to by window's right pointer, move the window leftward
54+
if window_right == len(nums) or abs(nums[window_left] - target) <= abs(
55+
nums[window_right] - target
56+
):
57+
window_left -= 1
58+
59+
# if the element pointed to by window's right pointer is closer to target than
60+
# the element pointed to by window's left pointer, move the window rightward
61+
else:
62+
window_right += 1
63+
64+
# return k closest elements present inside the window
65+
return nums[window_left + 1 : window_right]
38.8 KB
Loading
69.8 KB
Loading
48 KB
Loading
61.1 KB
Loading
66.5 KB
Loading
64.1 KB
Loading
54.3 KB
Loading
55.2 KB
Loading

0 commit comments

Comments
 (0)