Jc-alt logo
jc
20 min read
#data structures and algorithms

Intervals Intro

What are Intervals

Numbers!

Intervals IRL

Numbers!

Intervals Application: Intervals

Pattern: Numbers!

Ex: Numbers numbers!!

    def wow!(n: int) -> int:

        return n+1

57. Insert Interval ::2:: - Medium

Topics: Array

Intro

You are given an array of non-overlapping intervals intervals where intervals[i] = [starti, endi] represent the start and the end of the ith interval and intervals is sorted in ascending order by starti. You are also given an interval newInterval = [start, end] that represents the start and end of another interval. Insert newInterval into intervals such that intervals is still sorted in ascending order by starti and intervals still does not have any overlapping intervals (merge overlapping intervals if necessary). Return intervals after the insertion. Note that you don't need to modify intervals in-place. You can make a new array and return it.

Example InputOutput
intervals = [[1,3],[6,9]], newInterval = [2,5][[1,5],[6,9]]
intervals = [[1,2],[3,5],[6,7],[8,10],[12,16]], newInterval = [4,8][[1,2],[3,10],[12,16]]

Constraints:

0 ≤ intervals.length ≤ 104

intervals[i].length == 2

0 ≤ starti ≤ endi ≤ 105

intervals is sorted by starti in ascending order

newInterval.length == 2

0 ≤ start ≤ end ≤ 105

Abstraction

Given a list of sorted, non-overlapping intervals and a new interval, insert the new interval and merge any overlaps so that the resulting list remains sorted and non-overlapping.

Space & Time Complexity

SolutionTime ComplexitySpace ComplexityTime RemarkSpace Remark
BugError

Brute Force:

AspectTime ComplexitySpace ComplexityTime RemarksSpace Remarks

Find the Bug:

Solution 1: [Greedy] Greedy Merge - Intervals/Intervals

    def insert(self, intervals: List[List[int]], newInterval: List[int]) -> List[List[int]]:
        
        # Interval Insertion / Greedy Merge:
        # We are inserting a new interval into a list of non-overlapping, sorted intervals
        # and returning a merged list of non-overlapping intervals.

        # Greedy Merge Idea:
        # 1. Intervals ending before the new interval -> no overlap, add directly
        # 2. Intervals overlapping with new interval -> merge into one
        # 3. Intervals starting after the new interval ends -> add directly
        # We iterate once over all intervals, maintaining a merged interval along the way.

        # num of intervals
        # sc: O(1)
        n = len(intervals)

        # sc: list of n intervals O(n)
        res = []

        # new interval to be added
        # sc: O(1)
        start = newInterval[0]
        end = newInterval[1]

        # iterate over list of sorted intervals
        # sc: O(1)
        i = 0

        # Base Case: intervals before newInterval
        # Check: interval_end < newInterval_start 
        # Then: append to new list
        # tc: O(n)
        while i < n and intervals[i][1] < start:

            # simply add interval
            res.append(intervals[i])

            # iterate to next interval
            i += 1

        # Merge Case: interval overlaps with newInterval
        # Check: Interval overlaps
        # Then: merge into interval + newInterval
        # tc: O(n)
        while i < n and intervals[i][0] <= end:

            # Determine new start and end of merged interval

            # update start to be min between interval and newInterval
            start = min(start, intervals[i][0])
            # update end to be max between interval and newInterval
            end = max(end, intervals[i][1])

            # iterate to next interval
            i += 1

        # Append merged interval
        res.append([start, end])

        # Base Case: intervals after newInterval
        # Check: newInterval_end < interval_star
        # Then: append to new list
        # tc: O(n)
        while i < n:

            # simply add interval
            res.append(intervals[i])

            # iterate to next interval
            i += 1

        # overall: tc O(n)
        # overall: sc O(1)
        return res

56. Merge Intervals ::1:: - Medium

Topics: Array, Sorting

Intro

Given an array of intervals where intervals[i] = [starti, endi], merge all overlapping intervals, and return an array of the non-overlapping intervals that cover all the intervals in the input.

Example InputOutput
intervals = [[1,3],[2,6],[8,10],[15,18]][[1,6],[8,10],[15,18]]
intervals = [[1,4],[4,5]][[1,5]]
intervals = [[4,7],[1,4]][[1,7]]

Constraints:

1 ≤ intervals.length ≤ 104

intervals[i].length == 2

0 ≤ starti ≤ endi ≤ 105

Abstraction

Given a list of non sorted, overlapping intervals, merge all overlapping intervals.

Space & Time Complexity

SolutionTime ComplexitySpace ComplexityTime RemarkSpace Remark
BugError

Brute Force:

AspectTime ComplexitySpace ComplexityTime RemarksSpace Remarks

Find the Bug:

Solution 1: [Sorting] [Greedy] Sort Intervals By Start Time Greedy Linear Scan Last Interval Merge - Intervals/Intervals

    def merge(self, intervals: List[List[int]]) -> List[List[int]]:
        
        # Interval Merging / Greedy + Sorting:
        # Goal: merge all overlapping intervals into a list of non-overlapping intervals

        # This is the same as Merge Intervals, just that the list is not sorted
        # and the intervals may overlap

        # Linear Greedy Scan:
        # The current greedy merge works because intervals are sorted by start time.
        # The properties of the intervals sorted by start time, allow us to assume:
        #   - All previous intervals have already been merged into merged[-1], the last interval
        #   - No earlier intervals can overlap the current interval
        #   - Two possibilities exist for the current interval
        #       - Overlaps with merged[-1] => merge by updated the end
        #       - No overlap with merged[-1] => append as new interval

        # Key Idea (Sort + Greedy Linear Scan):
        # 1. Process Root -> sort intervals by start time
        # 2. Build -> iterate through sorted intervals, merging overlapping ones
        # 3. Explore Choices -> if current interval overlaps previous (last interval in merged list), 
        #    merge then; else, append as a new last interval

        # Empty check: no intervals, return []
        if not intervals:
            return []

        # Sort intervals by start time
        # tc: python (2.3-3.10) TimSort, Python (3.11+) PowerSort Worst/Avg: O(n log n), Best: O(n)
        # sc: sorts in place, no extra memory O(1)
        intervals.sort(key=lambda x: x[0])

        # Initialize merged list with lowest start time interval,
        # at the top of the minHeap
        # tc: O(1)
        merged = [intervals[0]]

        # tc: terate over all intervals O(n)
        for i in range(1, len(intervals)):

            # Current interval
            current = intervals[i]

            # rightmost interval
            last_merged = merged[-1]

            # Current interval either:
            #   - does overlap with last interval on the merged list
            #   - does not overlap with last interval on the merged list

            # if intervals overlap, merge by updated the end time
            if current[0] <= last_merged[1]:
                last_merged[1] = max(last_merged[1], current[1])

            # no overlap, append new interval to merged list
            else:
                merged.append(current)

        # overall: tc O(n log n)
        # overall: sc O(n)
        return merged 

Solution 2: [No Sorting] [Greedy] MinHeap By Interval Start Time Greedy Linear Scan Last Interval Merge - Intervals/Intervals

    def merge(self, intervals: List[List[int]]) -> List[List[int]]:
        
        # Interval Merging / Greedy + Heap:
        # Goal: merge all overlapping intervals into a list of non-overlapping intervals

        # Linear Greedy Scan:
        # The current greedy merge works because intervals are sorted by start time.
        # The properties of the intervals sorted by start time, allow us to assume:
        #   - All previous intervals have already been merged into merged[-1], the last interval
        #   - No earlier intervals can overlap the current interval
        #   - Two possibilities exist for the current interval
        #       - Overlaps with merged[-1] => merge by updated the end
        #       - No overlap with merged[-1] => append as new interval

        # MinHeap / Priority Queue
        # Maintains a binary heap where the smallest element is at the root.
        # Allows for efficient access to the minimum element
        #   - Insertion(): parent less than or equal to children
        #   - Pop(): restructures heap to keep new min at top
        #   - Push()
        #       - New element < current top => bubble up to maintain heap property
        #       - New element >= current top => placed in proper position via bubble up
        # Guarantees root has current min with O(1) access and insertion/removal in O(log n)

        # Heapify vs Sequential Insertions
        # A heap can be build in two ways:
        #
        #   1. Sequential Insertions:
        #       - pushes n elements one by one, each in O(log k) => O(n log n)
        #
        #   2. Heapify (bottom-up):
        #       - adjusts n elements at once starting from the last parent up to the root
        #       - lower levels require fewer comparisons, so total work => O(n)

        # Key Idea (Greedy + Heap):
        # 1. Push all intervals into a min-heap ordered by start time
        # 2. Pop intervals one by one
        # 3. Explore Choices:
        #      - If no overlap with last interval in result, append
        #      - If overlap, merge intervals
        # 4. Result -> list of merged intervals

        # Empty check: no intervals, return []
        if not intervals:
            return []

        # Sort Intervals By Start Time:
        # Push all intervals into a minHeap (ordered by start)
        # tc: heapify O(n)
        # sc: n intervals in heap O(n)
        heap = [(s, e) for s, e in intervals]
        heapq.heapify(heap)

        # final interval list
        # sc: O(n)
        merged = []

        # Pop intervals one by one from low start to high start time
        # tc: n removals in log n leading to O(n log n) 
        while heap:

            # Grab interval with lowest start time
            # tc: O(log n)
            start, end = heapq.heappop(heap)

            # Current interval either:
            #   - does overlap with last interval on the merged list
            #   - does not overlap with last interval on the merged list

            # if intervals overlap, merge by updated the end time
            if not merged or merged[-1][1] < start:
                merged.append([start, end])

            # no overlap, append new interval to merged list
            else:
                merged[-1][1] = max(merged[-1][1], end)

        # overall: tc O(n log n)
        # overall: sc O(n)
        return merged

435. Non Overlapping Intervals ::1:: - Medium

Topics: Array, Dynamic Programming, Greedy, Sorting

Intro

Given an array of intervals intervals where intervals[i] = [starti, endi], return the minimum number of intervals you need to remove to make the rest of the intervals non-overlapping. Note that intervals which only touch at a point are non-overlapping. For example, [1, 2] and [2, 3] are non-overlapping.

Example InputOutput
intervals = [[1,2],[2,3],[3,4],[1,3]]1
intervals = [[1,2],[1,2],[1,2]]2
intervals = [[1,2],[2,3]]0

Constraints:

1 ≤ intervals.length ≤ 105

intervals[i].length == 2

-5 * 104 ≤ starti < endi ≤ 5 * 104

Abstraction

Given a list intervals, determine the minimum number of intervals needed to be removed ot make the intervals non overlapping

Space & Time Complexity

SolutionTime ComplexitySpace ComplexityTime RemarkSpace Remark
BugError

Brute Force:

AspectTime ComplexitySpace ComplexityTime RemarksSpace Remarks

Find the Bug:

Solution 1: Greedy Sort By End - Intervals/Intervals

    def eraseOverlapIntervals(self, intervals: List[List[int]]) -> int:
        
        # Note:
        # Greedy / Sorting approach
        # 0. Empty or single interval: no removals needed
        # 1. Sort intervals by end time
        # 2. Track end of last kept interval
        # 3. Iterate through intervals
        # 4. If current interval overlaps last, remove it
        # 5. If no overlap, update end to end of new interval
        # Result -> count of removed intervals

        # Empty check
        if not intervals:
            return 0

        # Sort intervals by end time (earliest finishing first)
        intervals.sort(key=lambda x: x[1])

        # Track end of first interval
        end = intervals[0][1]
        
        removals = 0

        # Iterate over intervals
        for i in range(1, len(intervals)):

            # curr interval
            start_i, end_i = intervals[i]

            # Interval overlaps -> remove
            if start_i < end:
                removals += 1

            # No overlap -> update end to current interval end
            else:
                end = end_i

        # overall: time complexity O(n)
        # overall: space complexity O(1)
        return removals

Solution 2: Greedy Sort By Start - Intervals/Intervals

    def eraseOverlapIntervals(self, intervals: List[List[int]]) -> int:
        
        # Note:
        # Greedy / Sorting approach
        # 0. Empty or single interval: no removals needed
        # 1. Sort intervals by start time
        # 2. Track end of last kept interval
        # 3. Iterate through intervals
        # 4. Overlap with last, keep the interval with smaller end
        # 5. No overlap, update end to end of new interval
        # Result -> count of removed intervals

        # sort
        intervals.sort()
        
        res = 0

        # end of first interval
        prevEnd = intervals[0][1]

        # Iterate over intervals
        for start, end in intervals[1:]:

            # no overlap -> update prevEnd
            if start >= prevEnd:
                prevEnd = end

            # overlap -> remove one interval, keep the one with smaller end
            else:
                res += 1
                prevEnd = min(end, prevEnd)

        # overall: time complexity
        # overall: space complexity 
        return res

252. Meeting Rooms ::1:: - Easy

Topics: Array, Dynamic Programming, Greedy, Sorting

Intro

Given an array of meeting time interval objects consisting of start and end times [[start_1,end_1],[start_2,end_2],...] (start_i < end_i) of the intervals non-overlapping. determine if a person could add all meetings to their schedule without any conflicts.

Example InputOutput
intervals = [(0,30),(5,10),(15,20)]false
intervals = [(5,8),(9,15)]false

Constraints:

0 ≤ intervals.length ≤ 500

0 ≤ intervals[i].start < intervals[i].end ≤ 1000000

Abstraction

Given a list of intervals representing meetings, determine if a person can make it to all meetings without conflicts.

Space & Time Complexity

SolutionTime ComplexitySpace ComplexityTime RemarkSpace Remark
BugError

Brute Force:

AspectTime ComplexitySpace ComplexityTime RemarksSpace Remarks

Find the Bug:

Solution 1: Greedy Sort By Start - Intervals/Intervals

    def canAttendMeetings(self, intervals: List[Interval]) -> bool:
        
        # Note:
        # 1. Sort intervals by start time
        # 2. Iterate through sorted intervals
        # 3. Curr meeting starts before previous -> conflict
        # Result -> Check if conflict exists
        
        # Empty Check
        if not intervals:
            return True

        # Sort intervals by start time
        intervals.sort(key=lambda x: x.start)

        # Iterate through sorted intervals
        for i in range(1, len(intervals)):

            # Curr meeting starts before previous -> conflict
            if intervals[i].start < intervals[i-1].end:
                return False

        # overall: time complexity O(n)
        # overall: space complexity O(1)
        return True

253. Meeting Rooms II ::1:: - Medium

Topics: Array, Dynamic Programming, Greedy, Sorting

Intro

Given an array of meeting time interval objects consisting of start and end times [[start_1,end_1],[start_2,end_2],...] (start_i < end_i), find the minimum number of days required to schedule all meetings without any conflicts.

Example InputOutput
intervals = [(0,40),(5,10),(15,20)]2
intervals = [(4,9)]1

Constraints:

0 ≤ intervals.length ≤ 500

0 ≤ intervals[i].start < intervals[i].end ≤ 1000000

Abstraction

Given a list of intervals representing meetings, determine the minimum number of rooms or 'days' required to schedule all the meetings without conflicts.

Space & Time Complexity

SolutionTime ComplexitySpace ComplexityTime RemarkSpace Remark
BugError

Brute Force:

AspectTime ComplexitySpace ComplexityTime RemarksSpace Remarks

Find the Bug:

Solution 1: Greedy MinHeap - Intervals/Intervals

    def minMeetingRooms(self, intervals: List[Interval]) -> int:
        
        # Note:
        # Greedy Min-Heap
        # 1. Sort intervals by start time
        # 2. MinHeap to track meeting end times
        # 3. Iterate over intervals:
        #    Curr starts after earliest ending -> reuse room (pop heap)
        #    Curr starts before earliest ending -> new room needed, push curr end time to heap
        # Result -> size of heap at end = min rooms required

        # Empty Check
        if not intervals:
            return 0

        # Sort intervals by start time
        intervals.sort(key=lambda x: x.start)

        # MinHeap to track meeting end times
        heap = []

        for interval in intervals:

            # Curr starts after earliest ending -> reuse room (pop heap)
            if heap and interval.start >= heap[0]:
                heapq.heappop(heap)

            # Curr starts before earliest ending -> new room needed, push curr end time to heap
            heapq.heappush(heap, interval.end)

        # size of heap at end = min rooms required
        res = len(heap)
        
        # overall: time complexity O(n)
        # overall: space complexity O(1)
        return res

1851. Minimum Interval to Include Each Query ::3:: - Hard

Topics: Array, Binary Search, Line Sweep, Sorting, Heap (Priority Queue)

Intro

You are given a 2D integer array intervals, where intervals[i] = [lefti, righti] describes the ith interval starting at lefti and ending at righti (inclusive). The size of an interval is defined as the number of integers it contains, or more formally righti - lefti + 1. You are also given an integer array queries. The answer to the jth query is the size of the smallest interval i such that lefti < = queries[j] < = righti. If no such interval exists, the answer is -1. Return an array containing the answers to the queries.

Example InputOutput
intervals = [[1,4],[2,4],[3,6],[4,4]], queries = [2,3,4,5][3,3,1,4]
intervals = [[2,3],[2,5],[1,8],[20,25]], queries = [2,19,5,22][2,-1,4,6]

Constraints:

1 ≤ intervals.length ≤ 105

1 ≤ queries.length ≤ 105

intervals[i].length == 2

1 ≤ lefti < righti ≤ 107

1 ≤ queries[j] ≤ 107

Abstraction

For a list of intervals and a list of queries, determine for each query the smallest interval that covers it. If no interval covers a query, return -1.

Space & Time Complexity

SolutionTime ComplexitySpace ComplexityTime RemarkSpace Remark
BugError

Brute Force:

AspectTime ComplexitySpace ComplexityTime RemarksSpace Remarks

Find the Bug:

Solution 1: Greedy MinHeap 1 - Intervals/Intervals

    def minInterval(self, intervals: List[List[int]], queries: List[int]) -> List[int]:
        
        # Note:
        # Uses sorted queries with indices for mapping results.

        # Note:
        # Greedy + Min-Heap
        # 1. Sort intervals by start
        # 2. Sort queries with original indices
        # 3. Iterate queries in ascending order
        # 4. Intervals <= query -> push to heap
        # 5. Intervals > query -> pop
        # 6. record result
        # Result -> top of heap is smallest interval covering query, else -1

        # Sort intervals by start
        intervals.sort(key=lambda x: x[0])

        # Sort queries with original indices
        sorted_queries = sorted([(q, i) for i, q in enumerate(queries)])

        res = [-1] * len(queries)

        # MinHeap: (size, end)
        heap = []
        
        # pointer for intervals
        j = 0

        # Iterate queries in ascending order
        for q_val, q_idx in sorted_queries:

            # push all intervals starting <= query into heap
            while j < len(intervals) and intervals[j][0] <= q_val:
                start, end = intervals[j]
                size = end - start + 1
                heapq.heappush(heap, (size, end))
                j += 1

            # pop intervals ending before query
            while heap and heap[0][1] < q_val:
                heapq.heappop(heap)

            # record result
            if heap:
                res[q_idx] = heap[0][0]

        # overall: time complexity O(n log n + q log n)
        # overall: space complexity O(n)
        return res

Solution 2: Greedy MinHeap 2 - Intervals/Intervals

    def minInterval(self, intervals: List[List[int]], queries: List[int]) -> List[int]:
        
        # Note:
        # Uses a defaultdict map for clarity.

        # Note:
        # Greedy + Min-Heap
        # 0. Direct Boundary -> empty inputs handled implicitly
        # 1. Sort intervals by start
        # 2. Iterate queries in ascending order
        # 3. Push intervals whose start <= query into min-heap (ordered by size)
        # 4. Remove intervals whose end < query
        # 5. Result -> top of heap is smallest interval covering query, else -1
        # 6. Map results back to original query order

        # Sort intervals by start
        intervals.sort(key=lambda x: x[0])

        res_map = defaultdict(int)
        # MinHeap of (size, end)
        min_heap = []
        # Pointer for intervals
        i = 0

        # Iterate queries in ascending order
        for q in sorted(queries):

            # Push all intervals starting <= query into heap
            while i < len(intervals) and intervals[i][0] <= q:
                l, r = intervals[i]
                heapq.heappush(min_heap, (r - l + 1, r))  # (size, end)
                i += 1

            # Pop intervals ending before query
            while min_heap and min_heap[0][1] < q:
                heapq.heappop(min_heap)

            # Record result
            res_map[q] = min_heap[0][0] if min_heap else -1

        # Map results back to original query order
        res = [res_map[q] for q in queries]

        # overall: time complexity O(n log n + q log n)
        # overall: space complexity O(n)
        return res

Solution 3: Greedy MinHeap 3 - Intervals/Intervals

    def minInterval(self, intervals: List[List[int]], queries: List[int]) -> List[int]:
        
        # Note:
        # Uses a hash to avoid recomputation and pushes intervals 
        # into heap lazily.
        
        soq = sorted(queries)
        out = []
        intervals.sort(key=lambda x: x[0])
        hsh = {}
        i=0
        pq = []
        for q in soq:
            if hsh.get(q,-1) != -1:
                continue
            
            while i<len(intervals) and q>=intervals[i][0]:
                l,r = intervals[i]
                heapq.heappush(pq,(r-l+1,r))
                i+=1
            
            while pq:
                pr,end = pq[0]
                if q>end:
                    heapq.heappop(pq)
                else:
                    hsh[q]=pr
                    break

            
        
        return [hsh.get(p,-1) for p in queries]