Pull to refresh

Algorithms in Go: Merge Intervals

Reading time 4 min
Views 3.5K

This is the third part of a series covering the implementation of algorithms in Go. In this article, we discuss the Merge Intervals algorithm. Usually, when you start learning algorithms you have to deal with some problems like finding the least common denominator or finding the next Fibonacci number. While these are indeed important problems, it is not something that we solve every day. Actually, in the vast majority of cases, we only solve such kinds of algorithms when we learn how to solve the algorithms. What I like about the Merge Interval algorithm is that we apply it in our everyday life, usually without even noticing that we are solving an algorithmic problem.

Let's say that we need to organize a meeting for our team. We have three colleagues Jay, May, and Ray and their time schedule look as follows (a colored line represents an occupied timeslot):

Which timeslot should we pick up for a new meeting? One could look at the picture, find a gap in Ray's schedule, and check whether the others a gap there as well.

How can we implement this logic? Most straightforwardly, we can iterate through every minute of the day and check whether someone is having a meeting during that time. If none of the colleagues are occupied at that time, we find an available minute.

How can we simplify this approach? Instead of checking all employees for every minute, we can merge their schedules and find the available slots in the resulting schedule.

After the merge, we can iterate through the array of the merged meetings and find the gaps.

How can we implement the algorithm above? Let's create type Slot that represents a time slot in the schedule; for simplicity, we use integers to denote the start and the end of the time slot instead of time.Time type.

type Slot struct {
  start int
  end int
}

For each of the employees will have a sorted array of Slots (staring from the earliest time slot) that represent occupied time slots in their schedule. Our function will take an array of Slot arrays that represents the schedule for the whole team as an input parameter.

[][]Slot{
  {{9, 10}},         // occupied time slots for John
  {{1, 3}, {5, 6}},  // occupied time slots for James
  {{2, 3}, {6, 8}},  // ...
  {{4, 6}
}

Our function will return an array of Slots that represent the commonly available slots for each member of the team.

We merge the schedules of two employees by merging their arrays of Slots. How do we do the merge of two arrays? We need to iterate through both arrays and see whether we have overlapping time slots. How can we know that the slots are overlapping? In general, we have two options for overlapping intervals A and B:

If neither of the two above conditions is satisfied, then the intervals do not overlap.

We can iterate through both of the arrays and see whether we have an overlap. Let's arr1 and arr2 represent the occupied time slots for two employees. We start with i1 and i2 equal to zero and continue the iteration till we exhaust either of the arrays. At each step, we check whether the current intervals from each of the arrays overlap.

// Merge two array of slots.
func Merge(arr1, arr2 []Slot) []Slot {
	out := make([]Slot, 0)
	i1, i2 := 0, 0
	for i1 < len(arr1) && i2 < len(arr2) {
		v1, v2 := arr1[i1], arr2[i2]

If the intervals overlap then we merge them. Let's say interval v1 from arr1 ends earlier than v2 from arr2 In this case, the next interval from arr1 can have an overlap with the merged interval of v1 and v2. Therefore, we merge v1 into v2, i.e. v2.Start = min(v2.Start, v1.Start) and v2.Stop = max(v2.Stop, v1.Stop) and increase i1. i2 stays the same, therefore at the next iteration, we will check whether the next interval from arr1 overlaps with the merged interval.

overlap12 := (v2.Start >= v1.Start) && (v2.Start <= v1.Stop)
overlap21 := (v1.Start >= v2.Start) && (v1.Start <= v2.Stop)
if overlap12 || overlap21 {
	merged := Slot{
		Start: min(v1.Start, v2.Start),
		Stop:  max(v1.Stop, v2.Stop),
  }
  if v1.Stop < v2.Stop {
    arr2[i2] = merged
    i1++
  } else {
    arr1[i1] = merged
    i2++
  }
  continue
}

If there is no overlap, we save the interval with the earliest stop (let's say an interval v2 from arr2) to the output array and increase index i2 for arr2. The other interval v1 from arr1 can still have an overlap with the next interval from arr2 so we don't increase i1.

if v1.Stop < v2.Stop {
	out = append(out, v1)
	i1++
} else {
	out = append(out, v2)
	i2++
}

When i1 or i2 becomes equal to the length of the corresponding array we stop the iteration. As no more overlaps are possible, we simply add the remaining intervals to the output array.

The full listing for the function:

// Merge two arrays of meetings.
func Merge(arr1, arr2 []Slot) []Slot {
  out := make([]Slot, 0)
  i1, i2 := 0, 0
  for i1 < len(arr1) && i2 < len(arr2) {
    v1, v2 := arr1[i1], arr2[i2]
    
    overlap12 := (v2.Start >= v1.Start) && (v2.Start <= v1.Stop)
    overlap21 := (v1.Start >= v2.Start) && (v1.Start <= v2.Stop)
    if overlap12 || overlap21 {
      merged := Slot{
        Start: min(v1.Start, v2.Start),
        Stop:  max(v1.Stop, v2.Stop),
      }
      if v1.Stop < v2.Stop {
        arr2[i2] = merged
        i1++
      } else {
        arr1[i1] = merged
        i2++
      }
      continue
    }

    // no overlap; save the earliest of the intervals
    if v1.Stop < v2.Stop {
      out = append(out, v1)
      i1++
      } else {
      out = append(out, v2)
      i2++
      }
  }
  
  out = append(out, arr1[i1:]...)
  out = append(out, arr2[i2:]...)

  return out
}

We merged the schedules of two colleagues. We need to merge all the schedules and then find the gaps within the merged schedule. How do find the gaps? We need to invert a Slot array considering the start and the end of the working day.

// Given an array of occupied time slots,
// returns available time slots in range from 1 to 12.
// We consider that working day starts at 1 and ends at 12.
func inverseSchedule(schedule []Slot) []Slot {
  start := 1
  var out []Slot
  for ind, appointment := range schedule {
    if ind == 0 && appointment.Start == 1 {
      start = appointment.Stop
      continue
    }
    out = append(out, Slot{Start: start, Stop: appointment.Start})
    start = appointment.Stop
  }
  if start < 12 {
    out = append(out, Slot{Start: start, Stop: 12})
  }
  return out
}

The resulting function looks as follows:

// AvailableSlots for all employees.
// Working hours starts at 1 ends at 12.
func AvailableSlots(schedule [][]Slot) []Slot {
	if len(schedule) == 0 {
		return []Slot{{1, 12}}
	}
	if len(schedule) == 1 {
		return inverseSchedule(schedule[0])
	}
	merged := Merge(schedule[0], schedule[1])
	for _, s := range schedule[2:] {
		merged = Merge(merged, s)
	}

	return inverseSchedule(merged)
}

Here you can find the source code and the tests for the solution.

In this post, we implemented a solution for the Merge Intervals problem. This is one of the most popular algorithmic patterns that solve an entire class of problems. More algorithmic patterns such as Sliding Window or Iterative Postorder Traversal can be found in the series Algorithms in Go.

Tags:
Hubs:
+2
Comments 0
Comments Leave a comment

Articles