Skip to content

Commit 71ffd3a

Browse files
committed
feat(algorithms, sliding window): frequency of most frequent element
1 parent 9c87943 commit 71ffd3a

6 files changed

Lines changed: 195 additions & 2 deletions

File tree

algorithms/graphs/number_of_connected_components/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,7 @@ def dfs(node: int) -> int:
7575

7676
return total
7777

78+
7879
def count_components_dfs_iterative(n: int, edges: List[List[int]]) -> int:
7980
"""
8081
Count the number of connected components in an undirected graph.

algorithms/graphs/number_of_connected_components/test_number_of_connected_components.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
from algorithms.graphs.number_of_connected_components import (
66
count_components_union_find,
77
count_components_dfs,
8-
count_components_dfs_iterative
8+
count_components_dfs_iterative,
99
)
1010

1111
COUNT_COMPONENTS_TEST_CASES = [
@@ -31,7 +31,9 @@ def test_count_components_dfs(self, n: int, edges: List[List[int]], expected: in
3131
self.assertEqual(expected, actual)
3232

3333
@parameterized.expand(COUNT_COMPONENTS_TEST_CASES, name_func=custom_test_name_func)
34-
def test_count_components_dfs_iterative(self, n: int, edges: List[List[int]], expected: int):
34+
def test_count_components_dfs_iterative(
35+
self, n: int, edges: List[List[int]], expected: int
36+
):
3537
actual = count_components_dfs_iterative(n, edges)
3638
self.assertEqual(expected, actual)
3739

Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
# Frequency of the Most Frequent Element
2+
3+
The frequency of an element is the number of times it occurs in an array.
4+
5+
You are given an integer array nums and an integer k. In one operation, you can choose an index of nums and increment the
6+
element at that index by 1.
7+
8+
Return the maximum possible frequency of an element after performing at most k operations.
9+
10+
## Example
11+
12+
Example 1:
13+
14+
```text
15+
Input: nums = [1,2,4], k = 5
16+
Output: 3
17+
Explanation: Increment the first element three times and the second element two times to make nums = [4,4,4].
18+
4 has a frequency of 3.
19+
```
20+
21+
Example 2:
22+
```text
23+
Input: nums = [1,4,8,13], k = 5
24+
Output: 2
25+
Explanation: There are multiple optimal solutions:
26+
- Increment the first element three times to make nums = [4,4,8,13]. 4 has a frequency of 2.
27+
- Increment the second element four times to make nums = [1,8,8,13]. 8 has a frequency of 2.
28+
- Increment the third element five times to make nums = [1,4,13,13]. 13 has a frequency of 2.
29+
```
30+
31+
Example 3:
32+
33+
```text
34+
Input: nums = [3,9,6], k = 2
35+
Output: 1
36+
```
37+
38+
## Constraints
39+
40+
- 1 <= nums.length <= 10^5
41+
- 1 <= nums[i] <= 10^5
42+
- 1 <= k <= 10^5
43+
44+
## Topics
45+
46+
- Array
47+
- Binary Search
48+
- Greedy
49+
- Sliding Window
50+
- Sorting
51+
- Prefix Sum
52+
53+
## Solution
54+
55+
The best way to solve this problem is to conver the smaller numbers into the largest possible number while using the least
56+
number of operations. To achieve this, we use the sliding window technique, which maintains a group of numbers that can
57+
be adjusted to match a target value (the largest number in the window). THe window size is increased or decreased dynamically
58+
based on the sum of its elements, allowing us to quickly determine whether we can increase all numbers in the window to
59+
match the target value within the allowed `k` operations. The largest window wehre this condition holds gives use the
60+
maximum possible frequency.
61+
62+
We first sort `nums` so that we always transform smaller numbers first, which requires fewer operations than modifying
63+
larger ones. This makes it easier to maintain a valid window where all elements can equal to the rightmost(largest) element.
64+
65+
For any particular window, we decided to expand it further or shrink it based on the following condition:
66+
67+
> window size * target value <= window sum + k
68+
69+
This means that for a window to be valid, the total sum (if all elements were target) should not exceed the actual sum
70+
of elements in the current window plus `k` allowed operations.
71+
72+
- If the condition is true and the window is valid, we add the next element of `nums` to the current window.
73+
- Otherwise, if the condition is false, we shrink the window.
74+
75+
This process continues until we find the largest valid window within which all numbers can be equal. The illustration below
76+
helps us understand the concept better.
77+
78+
![Solution Sample](./images/solutions/freq_of_most_freq_element.png)
79+
80+
Now, let's look at the algorithm steps:
81+
82+
- Sort the array `nums`
83+
- Initialize the following:
84+
- The left pointer of the sliding window is `left = 0`. The `right` pointer will be initialized later because `right`
85+
expands with window dynamically as we iterate over `nums`
86+
- A variable `max_freq = 0` that stores the maximum frequency found so far.
87+
- A variable `window_sum = 0`, which keeps track of the sum of elements within the current window
88+
- Iterate through `nums` using a sliding window (the `right` pointer is initialized here):
89+
- Calculate the `target` value, which is the rightmost element in the current window, `target = nums[right]`.
90+
- Expand the window by adding `nums[right]` to `window_sum`
91+
- Check if the current window gives us the maximum frequency we are looking for using the condition
92+
`(right - left + 1) * target > window_sum + k`. If it is true, it means we need more than `k` operations to make all
93+
elements equal to the `target`, so we must shrink the window as follows:
94+
- Substrcting `nums[left]` from `window_sum`
95+
- Incrementing `left` to move the left boundary forward
96+
- After adjusting the window, update `max_freq`
97+
- Once we have traversed the `nums` completely, return the maximum frequency found
98+
99+
### Complexity Analysis
100+
101+
#### Time Complexity
102+
103+
- Soring the array takes `O(n log(n))` where `n` is the length of `nums`
104+
- The sliding window traversal takes `O(n)` time because the right pointer moves 0 to `n-1`
105+
- During the sliding window traversal, the `left` pointer only moves forward when needed, ensuring each element is processed
106+
at most once. In the worst case, `left` moves n times across all iterations
107+
108+
If we sum these up, the overall time complexity simplifies to:
109+
110+
`O(n) + O(n log(n)) = O(n log(n))`
111+
112+
#### Space Complexity
113+
114+
Depending on the language used, the sorting may incur additional space complexity. In Java, `sort()` method typically
115+
requires `O(log(n))` space. Additionally, a few variables using constant space are used.
116+
The overall space complexity is `O(log(n))`.
117+
118+
In Python, the sorting method has the worst-case space complexity of `O(n)`.
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
from typing import List
2+
3+
4+
def max_frequency(nums: List[int], k: int) -> int:
5+
"""
6+
Returns the maximum possible frequency of a element after performing at most k operations
7+
Args:
8+
nums (List[int]): The list of numbers
9+
k (int): The number of frequencies to return.
10+
Returns:
11+
int: The largest frequency.
12+
"""
13+
# Sort the nums first, this incurs a time complexity of O(n log(n)) and space complexity of O(n) due to the Timsort
14+
# algorithm used
15+
sorted_nums = sorted(nums)
16+
17+
# left pointer of the window
18+
left = 0
19+
# Stores the maximum frequency found
20+
max_freq = 0
21+
# Sum of the elements within the current window
22+
window_sum = 0
23+
24+
# expand the window by moving the right pointer
25+
for right in range(len(sorted_nums)):
26+
# Target element to make frequent
27+
target = sorted_nums[right]
28+
29+
# Update the sum of elements in the window
30+
window_sum += target
31+
32+
# Check if the total required increments exceed k
33+
while (right - left + 1) * target > window_sum + k:
34+
# Remove the leftmost element
35+
window_sum -= sorted_nums[left]
36+
37+
# Shrink the window from the left
38+
left += 1
39+
40+
# Update the maximum frequency found
41+
max_freq = max(max_freq, right - left + 1)
42+
43+
return max_freq
50.3 KB
Loading
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
import unittest
2+
from typing import List
3+
from parameterized import parameterized
4+
from utils.test_utils import custom_test_name_func
5+
from algorithms.sliding_window.frequency_of_the_most_frequent_element import (
6+
max_frequency,
7+
)
8+
9+
MAX_FREQUENCY_TEST_CASES = [
10+
([1, 2, 4], 5, 3),
11+
([1, 4, 8, 13], 5, 2),
12+
([3, 9, 6], 2, 1),
13+
([1, 1, 2], 2, 3),
14+
([4, 2, 10, 6], 2, 2),
15+
([2, 3, 5], 3, 2),
16+
([4, 6, 8, 10], 1, 1),
17+
([1, 2, 3, 4], 5, 3),
18+
]
19+
20+
21+
class FrequencyOfMostFrequentElementTestCase(unittest.TestCase):
22+
@parameterized.expand(MAX_FREQUENCY_TEST_CASES, name_func=custom_test_name_func)
23+
def test_max_frequency(self, nums: List[int], k: int, expected: int) -> None:
24+
actual = max_frequency(nums, k)
25+
self.assertEqual(expected, actual)
26+
27+
28+
if __name__ == "__main__":
29+
unittest.main()

0 commit comments

Comments
 (0)