Skip to content

Commit 9d24af5

Browse files
committed
refactor(algorithms, intervals): merge intervals
1 parent ca1518f commit 9d24af5

File tree

10 files changed

+170
-68
lines changed

10 files changed

+170
-68
lines changed
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
# Merge Intervals
2+
3+
Given an array of intervals where intervals[i] = [starti, endi], merge all overlapping intervals, and return an array
4+
of the non-overlapping intervals that cover all the intervals in the input.
5+
6+
## Constraints
7+
8+
- 1 ≤ `intervals.length` ≤ 10^3
9+
- `intervals[i].length` == 2
10+
- 0 ≤ `starti``endi` ≤ 10^4
11+
12+
13+
Example 1:
14+
```text
15+
Input: intervals = [[1,3],[2,6],[8,10],[15,18]]
16+
Output: [[1,6],[8,10],[15,18]]
17+
Explanation: Since intervals [1,3] and [2,6] overlaps, merge them into [1,6].
18+
```
19+
20+
Example 2:
21+
```text
22+
Input: intervals = [[1,4],[4,5]]
23+
Output: [[1,5]]
24+
Explanation: Intervals [1,4] and [4,5] are considered overlapping.
25+
```
26+
27+
## Solution
28+
29+
### Naive Approach
30+
31+
The naive approach to merging intervals involves comparing each interval with all the intervals that come after it. If
32+
two intervals overlap, they are merged into a single interval by updating the start to the minimum of both start times
33+
and the end to the maximum of both end times. After merging, the second interval (the one being compared) is removed
34+
from the list, as its range is now included in the updated interval. The algorithm then checks for further overlaps with
35+
the updated interval from the same position. This process repeats until all overlapping intervals have been merged,
36+
resulting in a final list of non-overlapping intervals.
37+
38+
This process continues until all overlapping intervals are merged. While this method is easy to understand and doesn’t
39+
require extra space, it can be inefficient for large inputs because it compares each interval with many others, leading
40+
to a time complexity of O(n^2).
41+
42+
### Optimized approach using the intervals pattern
43+
44+
The optimized approach starts by sorting the intervals in ascending order based on their start times. This step ensures
45+
overlapping intervals are positioned next to each other, making identifying and merging them easier. Once sorted, the
46+
algorithm initializes a result list with the first interval. It then iterates through the remaining intervals one by
47+
one, comparing each to the last interval in the result list.
48+
49+
If the current interval overlaps with the last one in the result (i.e., its start time is less than or equal to the end
50+
time of the last interval), the two intervals are merged by updating the end time to the maximum of both intervals’ end
51+
times. If there is no overlap, the current interval is added to the result list as a new entry. This process continues
52+
until all intervals have been processed. In the end, the result list contains the merged, non-overlapping intervals.
53+
54+
#### Solution summary
55+
56+
To recap, the solution to this problem can be divided into the following two parts:
57+
58+
1. Sort the intervals list according to the start time.
59+
2. Add the first interval from the sorted list to the output list.
60+
3. Then, iterate through the remaining intervals one by one. For each interval, check if it overlaps with the last
61+
interval in the output list:
62+
- If they overlap, merge them by updating the end time to the maximum of both end times, and update the last interval
63+
in the output list.
64+
- If they don’t overlap, simply append the current interval to the output list as a separate entry.
65+
66+
#### Time Complexity
67+
68+
The time complexity of this solution is O(n log n) where n is the number of intervals. This is because we need to sort
69+
the intervals list, which takes O(n log n) time.
70+
71+
#### Space Complexity
72+
73+
The space complexity is O(n) for storing the result list. However, we do not count the space used merely for input and
74+
output in the space complexity calculation. Apart from the space required by the built-in sorting algorithm, the space
75+
complexity is O(1).
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
from typing import List
2+
3+
4+
def merge(intervals: List[List[int]]) -> List[List[int]]:
5+
"""
6+
Merges overlapping intervals in a list of intervals.
7+
Args:
8+
intervals (List[List[int]]): A list of intervals, where each interval is represented as a list of two integers [start, end].
9+
Returns:
10+
List[List[int]]: A list of merged intervals.
11+
"""
12+
# no intervals to merge
13+
if not intervals:
14+
return []
15+
16+
# copy the `intervals` array to avoid mutating the input array
17+
closed_intervals = intervals[:]
18+
# sort the closed intervals array in place using the start time as the key. This sorts in ascending order
19+
closed_intervals.sort(key=lambda x: x[0])
20+
21+
# the final result array
22+
merged = []
23+
24+
for interval in closed_intervals:
25+
# if the merged array is empty or the last interval in the merged array does not overlap with the current interval
26+
if not merged or merged[-1][1] < interval[0]:
27+
# add it to the merged list
28+
merged.append(interval)
29+
else:
30+
# else we merge the intervals, by updating the max end time of the last interval in the merged list
31+
merged[-1][1] = max(merged[-1][1], interval[1])
32+
33+
return merged
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
import unittest
2+
from typing import List
3+
from parameterized import parameterized
4+
from algorithms.intervals.merge_intervals import merge
5+
6+
7+
class MergeIntervalsTestCase(unittest.TestCase):
8+
@parameterized.expand(
9+
[
10+
([[10, 12], [12, 15]], [[10, 15]]),
11+
([[1, 3], [2, 6], [8, 10], [15, 18]], [[1, 6], [8, 10], [15, 18]]),
12+
([[1, 4], [4, 5]], [[1, 5]]),
13+
([[14, 20]], [[14, 20]]),
14+
([[1, 5], [4, 6], [3, 7], [6, 8]], [[1, 8]]),
15+
(
16+
[[1, 3], [2, 6], [15, 18], [8, 10], [18, 20]],
17+
[[1, 6], [8, 10], [15, 20]],
18+
),
19+
([[4, 6], [3, 7], [1, 5]], [[1, 7]]),
20+
([[4, 6], [3, 7], [1, 5]], [[1, 7]]),
21+
([[1, 5], [4, 6], [11, 15], [6, 8]], [[1, 8], [11, 15]]),
22+
([[1, 5]], [[1, 5]]),
23+
([[1, 9], [3, 8], [4, 4]], [[1, 9]]),
24+
([[1, 2], [8, 8], [3, 4]], [[1, 2], [3, 4], [8, 8]]),
25+
]
26+
)
27+
def test_merge_intervals(self, intervals: List[List[int]], expected: List[int]):
28+
actual = merge(intervals)
29+
self.assertEqual(expected, actual)
30+
31+
32+
if __name__ == "__main__":
33+
unittest.main()

algorithms/sorting/__init__.py

Whitespace-only changes.

algorithms/sorting/merge_intervals/README.md

Lines changed: 0 additions & 13 deletions
This file was deleted.

algorithms/sorting/merge_intervals/__init__.py

Lines changed: 0 additions & 15 deletions
This file was deleted.

puzzles/arrays/candy/test_candy.py

Lines changed: 12 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -5,16 +5,18 @@
55

66

77
class CandyTestCase(unittest.TestCase):
8-
@parameterized.expand([
9-
([1], 1),
10-
([1, 0, 2], 5),
11-
([1, 2, 2], 4),
12-
([1,3,4,5,2], 11),
13-
([1,3,4,5,2], 11),
14-
([1,2,3,4,5], 15),
15-
([5,4,3,2,1], 15),
16-
([5,5,5,5,5,5,5,5], 8),
17-
])
8+
@parameterized.expand(
9+
[
10+
([1], 1),
11+
([1, 0, 2], 5),
12+
([1, 2, 2], 4),
13+
([1, 3, 4, 5, 2], 11),
14+
([1, 3, 4, 5, 2], 11),
15+
([1, 2, 3, 4, 5], 15),
16+
([5, 4, 3, 2, 1], 15),
17+
([5, 5, 5, 5, 5, 5, 5, 5], 8),
18+
]
19+
)
1820
def test_candy(self, ratings: List[int], expected: int):
1921
actual = candy(ratings)
2022
self.assertEqual(expected, actual)

puzzles/arrays/max_consecutive_ones/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ def longest_ones(nums: List[int], k: int) -> int:
2121

2222
return right - left + 1
2323

24+
2425
def find_max_consecutive_ones(nums: List[int]) -> int:
2526
"""
2627
Finds the maximum consecutive ones in a binary array and returns it.

puzzles/arrays/max_consecutive_ones/test_max_consecutive_ones.py

Lines changed: 16 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -5,23 +5,26 @@
55

66

77
class MaxConsecutiveOnesTestCase(unittest.TestCase):
8-
9-
@parameterized.expand([
10-
([1, 1, 1, 0, 0, 0, 1, 1, 1, 1, 0], 2, 6),
11-
([0, 0, 1, 1, 0, 0, 1, 1, 1, 0, 1, 1, 0, 0, 0, 1, 1, 1, 1], 3, 10),
12-
])
8+
@parameterized.expand(
9+
[
10+
([1, 1, 1, 0, 0, 0, 1, 1, 1, 1, 0], 2, 6),
11+
([0, 0, 1, 1, 0, 0, 1, 1, 1, 0, 1, 1, 0, 0, 0, 1, 1, 1, 1], 3, 10),
12+
]
13+
)
1314
def test_longest_ones(self, nums: List[int], k: int, expected: int):
1415
actual = longest_ones(nums, k)
1516
self.assertEqual(expected, actual)
1617

17-
@parameterized.expand([
18-
([1, 1, 1, 0, 0, 0, 1, 1, 1, 1, 0], 4),
19-
([0, 0, 1, 1, 0, 0, 1, 1, 1, 0, 1, 1, 0, 0, 0, 1, 1, 1, 1], 4),
20-
([1,1,0,0,1,1,1,0,0,1,0], 3),
21-
([1,1,0,0,1,1], 2),
22-
([1,1,1,0,1,1,1,1], 4),
23-
([0,0,0,0], 0),
24-
])
18+
@parameterized.expand(
19+
[
20+
([1, 1, 1, 0, 0, 0, 1, 1, 1, 1, 0], 4),
21+
([0, 0, 1, 1, 0, 0, 1, 1, 1, 0, 1, 1, 0, 0, 0, 1, 1, 1, 1], 4),
22+
([1, 1, 0, 0, 1, 1, 1, 0, 0, 1, 0], 3),
23+
([1, 1, 0, 0, 1, 1], 2),
24+
([1, 1, 1, 0, 1, 1, 1, 1], 4),
25+
([0, 0, 0, 0], 0),
26+
]
27+
)
2528
def test_find_max_consecutive_ones(self, nums: List[int], expected: int):
2629
actual = find_max_consecutive_ones(nums)
2730
self.assertEqual(expected, actual)

tests/algorithms/sorting/test_merge_intervals.py

Lines changed: 0 additions & 17 deletions
This file was deleted.

0 commit comments

Comments
 (0)