Skip to content

Commit d9d1d2a

Browse files
committed
feat: add merge intervals algorithm to greedy_methods
Add a greedy algorithm implementation for merging overlapping intervals. The algorithm sorts intervals by start time then merges overlapping ones in a single pass, achieving O(n log n) time complexity. Includes comprehensive doctests covering edge cases such as empty input, single intervals, fully overlapping intervals, and input validation.
1 parent 68473af commit d9d1d2a

File tree

1 file changed

+93
-0
lines changed

1 file changed

+93
-0
lines changed

greedy_methods/merge_intervals.py

Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
"""
2+
Given a collection of intervals, merge all overlapping intervals.
3+
4+
This is a classic greedy algorithm problem. The key insight is that after sorting
5+
the intervals by their start time, we can merge overlapping intervals in a single
6+
pass by comparing each interval's start with the previous interval's end.
7+
8+
Reference: https://en.wikipedia.org/wiki/Interval_scheduling#Interval_Partitioning
9+
10+
For doctests run the following command:
11+
python -m doctest -v merge_intervals.py
12+
"""
13+
14+
15+
def merge_intervals(intervals: list[list[int]]) -> list[list[int]]:
16+
"""
17+
Merge all overlapping intervals and return the non-overlapping intervals
18+
that cover all the intervals in the input.
19+
20+
Args:
21+
intervals: A list of intervals where each interval is [start, end].
22+
23+
Returns:
24+
A list of merged non-overlapping intervals sorted by start time.
25+
26+
Examples:
27+
>>> merge_intervals([[1, 3], [2, 6], [8, 10], [15, 18]])
28+
[[1, 6], [8, 10], [15, 18]]
29+
>>> merge_intervals([[1, 4], [4, 5]])
30+
[[1, 5]]
31+
>>> merge_intervals([[1, 4], [0, 4]])
32+
[[0, 4]]
33+
>>> merge_intervals([[1, 10], [2, 3], [4, 5], [6, 7]])
34+
[[1, 10]]
35+
>>> merge_intervals([[1, 2]])
36+
[[1, 2]]
37+
>>> merge_intervals([])
38+
[]
39+
>>> merge_intervals([[3, 5], [1, 2], [6, 8], [2, 4]])
40+
[[1, 5], [6, 8]]
41+
>>> merge_intervals([[1, 3], [2, 6], [8, 10], [15, 18], [17, 20]])
42+
[[1, 6], [8, 10], [15, 20]]
43+
>>> merge_intervals([[1, 1]])
44+
[[1, 1]]
45+
>>> merge_intervals("not a list")
46+
Traceback (most recent call last):
47+
...
48+
TypeError: intervals must be a list of intervals
49+
>>> merge_intervals([[1, 2], "not an interval"])
50+
Traceback (most recent call last):
51+
...
52+
TypeError: each interval must be a list of two integers
53+
>>> merge_intervals([[1, 2], [3]])
54+
Traceback (most recent call last):
55+
...
56+
TypeError: each interval must be a list of two integers
57+
>>> merge_intervals([[2, 1]])
58+
Traceback (most recent call last):
59+
...
60+
ValueError: interval start must not exceed end: [2, 1]
61+
"""
62+
if not isinstance(intervals, list):
63+
raise TypeError("intervals must be a list of intervals")
64+
65+
for interval in intervals:
66+
if not isinstance(interval, list) or len(interval) != 2:
67+
raise TypeError("each interval must be a list of two integers")
68+
if interval[0] > interval[1]:
69+
msg = f"interval start must not exceed end: {interval}"
70+
raise ValueError(msg)
71+
72+
intervals.sort(key=lambda x: x[0])
73+
74+
merged: list[list[int]] = []
75+
for interval in intervals:
76+
# If merged is empty or current interval does not overlap with previous
77+
if not merged or merged[-1][1] < interval[0]:
78+
merged.append(interval)
79+
else:
80+
# There is overlap, so merge the current and previous intervals
81+
merged[-1][1] = max(merged[-1][1], interval[1])
82+
83+
return merged
84+
85+
86+
if __name__ == "__main__":
87+
import doctest
88+
89+
doctest.testmod()
90+
91+
example = [[1, 3], [2, 6], [8, 10], [15, 18]]
92+
print(f"Intervals: {example}")
93+
print(f"Merged: {merge_intervals(example)}")

0 commit comments

Comments
 (0)