This notebook was prepared by Donne Martin. Source and license info is on GitHub.

# Solution Notebook¶

## Problem: Given a list of tuples representing ranges, condense the ranges.¶

Example: [(2, 3), (3, 5), (7, 9), (8, 10)] -> [(2, 5), (7, 10)]

## Constraints¶

• Are the tuples in sorted order?
• No
• Are the tuples ints?
• Yes
• Will all tuples have the first element less than the second?
• Yes
• Is there an upper bound on the input range?
• No
• Is the output a list of tuples?
• Yes
• Is the output a new array?
• Yes
• Can we assume the inputs are valid?
• No, check for None
• Can we assume this fits memory?
• Yes

## Test Cases¶

* None input -> TypeError
* [] - []
* [(2, 3), (7, 9)] -> [(2, 3), (7, 9)]
* [(2, 3), (3, 5), (7, 9), (8, 10)] -> [(2, 5), (7, 10)]
* [(2, 3), (3, 5), (7, 9), (8, 10), (1, 11)] -> [(1, 11)]
* [(2, 3), (3, 8), (7, 9), (8, 10)] -> [(2, 10)]


## Algorithm¶

• Sort the tuples based on start time
• Check each adjacent tuple to see if they can be merged
Case: * [(2, 3), (3, 8), (7, 9), (8, 10)] -> [(2, 10)]

* Sort by start time (already sorted)
* Add the first tuple to the merged_array
* Loop through each item in sorted_array starting at index 1
* If there is no overlap
* Add the current item to merged_array
* Else
* Update the last item in merged_array
* The end time will be the max of merged_array[-1][1] and sorted_array[i][1]

Start:
i
0       1       2       3
sorted_array = [(2, 3), (3, 8), (7, 9), (8, 10)]
merged_array = [(2, 3)]

Overlap with (2, 3), (3, 8):
i
0       1       2       3
sorted_array = [(2, 3), (3, 8), (7, 9), (8, 10)]
merged_array = [(2, 8)]

Overlap with (2, 8), (7, 9):
i
0       1       2       3
sorted_array = [(2, 3), (3, 8), (7, 9), (8, 10)]
merged_array = [(2, 9)]

Overlap with (2, 9) (8, 10):
i
0       1       2       3
sorted_array = [(2, 3), (3, 8), (7, 9), (8, 10)]
merged_array = [(2, 10)]


Complexity:

• Time: O(n log(n))
• Space: O(n)

## Code¶

In [1]:
class Solution(object):

def merge_ranges(self, array):
if array is None:
raise TypeError('array cannot be None')
if not array:
return array
sorted_array = sorted(array)
merged_array = [sorted_array[0]]
for index, item in enumerate(sorted_array):
if index == 0:
continue
start_prev, end_prev = merged_array[-1]
start_curr, end_curr = item
if end_prev < start_curr:
# No overlap, add the entry
merged_array.append(item)
else:
# Overlap, update the previous entry's end value
merged_array[-1] = (start_prev, max(end_prev, end_curr))
return merged_array


## Unit Test¶

In [2]:
%%writefile test_merge_ranges.py
import unittest

class TestMergeRanges(unittest.TestCase):

def test_merge_ranges(self):
solution = Solution()
self.assertRaises(TypeError, solution.merge_ranges, None)
self.assertEqual(solution.merge_ranges([]), [])
array = [(2, 3), (7, 9)]
expected = [(2, 3), (7, 9)]
self.assertEqual(solution.merge_ranges(array), expected)
array = [(3, 5), (2, 3), (7, 9), (8, 10)]
expected = [(2, 5), (7, 10)]
self.assertEqual(solution.merge_ranges(array), expected)
array = [(2, 3), (3, 5), (7, 9), (8, 10), (1, 11)]
expected = [(1, 11)]
self.assertEqual(solution.merge_ranges(array), expected)
print('Success: test_merge_ranges')

def main():
test = TestMergeRanges()
test.test_merge_ranges()

if __name__ == '__main__':
main()

Overwriting test_merge_ranges.py

In [3]:
%run -i test_merge_ranges.py

Success: test_merge_ranges