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