14.11 Sorting Plus Greedy
Many greedy algorithms begin with a sort.
14.11 Sorting Plus Greedy
Many greedy algorithms begin with a sort. The sort is not an implementation detail. It is often the step that exposes the structure needed for the greedy rule to work.
Without sorting, a greedy algorithm may see decisions in an arbitrary order and make choices before it has enough context. With sorting, the algorithm processes candidates according to the property that matters: finish time, deadline, profit, density, weight, start time, or some derived key. Once the right order is chosen, the remaining loop is often simple.
This recipe focuses on the design pattern:
Sort by the right key.
Scan once.
Make local decisions.
Problem
You have a collection of objects and need to choose, merge, discard, or arrange them optimally.
Examples:
| Problem | Sort Key | Greedy Action |
|---|---|---|
| Interval scheduling | Finish time | Select compatible intervals |
| Fractional knapsack | Value density | Take highest-density items |
| Job sequencing | Profit | Schedule jobs late |
| Prefix constraints | Deadline | Remove longest job on violation |
| Merge intervals | Start time | Extend current interval |
| Minimum arrows for balloons | End coordinate | Shoot at earliest end |
The hard part is not writing the loop. The hard part is choosing the sort key.
Why Sorting Matters
A greedy algorithm commits to decisions incrementally. Sorting gives those commitments a meaningful order.
For interval scheduling, sorting by finish time ensures that the first selected interval leaves the maximum space for the future.
For fractional knapsack, sorting by value density ensures that every unit of capacity is spent on the best available value rate.
For prefix constraints, sorting by deadline turns a global feasibility problem into a sequence of prefix checks.
In each case, sorting converts the problem into an ordered stream where local choices become valid.
The Basic Recipe
Use this template when a greedy problem appears to involve ordering.
Choose a candidate ordering.
Sort the input by that ordering.
Initialize an empty answer or state.
For each item in sorted order:
If the item improves or preserves feasibility:
accept it
Otherwise:
reject it or repair the state
Return the answer
The design question is:
What ordering makes a local choice safe?
Example 1: Merge Intervals
Given intervals:
[1,3], [2,6], [8,10], [15,18]
Merge overlapping intervals.
The correct sort key is start time.
Sorted input:
[1,3], [2,6], [8,10], [15,18]
Scan from left to right.
Maintain the current merged interval.
current = [1,3]
Next interval:
[2,6]
It overlaps because:
2 <= 3
Merge:
current = [1,6]
Next interval:
[8,10]
It does not overlap.
Emit:
[1,6]
Start new current interval:
[8,10]
Final result:
[1,6], [8,10], [15,18]
Sorting by start time guarantees that once an interval does not overlap the current merged interval, no later interval can begin earlier and repair that gap.
Go Implementation
type Interval struct {
Start int
End int
}
func MergeIntervals(intervals []Interval) []Interval {
if len(intervals) == 0 {
return nil
}
sort.Slice(intervals, func(i, j int) bool {
return intervals[i].Start < intervals[j].Start
})
result := make([]Interval, 0, len(intervals))
current := intervals[0]
for i := 1; i < len(intervals); i++ {
next := intervals[i]
if next.Start <= current.End {
if next.End > current.End {
current.End = next.End
}
} else {
result = append(result, current)
current = next
}
}
result = append(result, current)
return result
}
Example 2: Minimum Arrows to Burst Balloons
Each balloon is an interval on a line. One arrow shot at coordinate x bursts every balloon whose interval contains x.
Given:
[10,16], [2,8], [1,6], [7,12]
Goal:
Find the minimum number of arrows.
The right sort key is interval end.
Sorted by end:
[1,6], [2,8], [7,12], [10,16]
Shoot the first arrow at the earliest end:
x = 6
This bursts:
[1,6], [2,8]
Next unburst balloon:
[7,12]
Shoot:
x = 12
This bursts:
[7,12], [10,16]
Answer:
2
This is the same core idea as interval scheduling: choose the earliest possible endpoint that preserves maximum flexibility.
Go Implementation
func MinArrows(points []Interval) int {
if len(points) == 0 {
return 0
}
sort.Slice(points, func(i, j int) bool {
return points[i].End < points[j].End
})
arrows := 1
arrowPos := points[0].End
for i := 1; i < len(points); i++ {
if points[i].Start > arrowPos {
arrows++
arrowPos = points[i].End
}
}
return arrows
}
Example 3: Non-Overlapping Intervals
Given a list of intervals, remove the minimum number of intervals so that the rest do not overlap.
This can be reframed as:
Keep the maximum number of non-overlapping intervals.
That is exactly interval scheduling.
Sort by end time.
Accept an interval if it starts after the last accepted end.
Count the rejected intervals.
func EraseOverlapIntervals(intervals []Interval) int {
if len(intervals) == 0 {
return 0
}
sort.Slice(intervals, func(i, j int) bool {
return intervals[i].End < intervals[j].End
})
kept := 1
lastEnd := intervals[0].End
for i := 1; i < len(intervals); i++ {
if intervals[i].Start >= lastEnd {
kept++
lastEnd = intervals[i].End
}
}
return len(intervals) - kept
}
Choosing the Sort Key
A useful way to select a sort key is to ask what the algorithm is trying to preserve.
| Goal | Usually Sort By | Reason |
|---|---|---|
| Leave maximum future time | Earliest finish | Frees future capacity |
| Spend capacity efficiently | Highest value density | Maximizes value per unit |
| Respect deadlines | Earliest deadline | Makes prefix checks meaningful |
| Prefer valuable jobs | Highest profit | Tries best opportunities first |
| Merge ranges | Earliest start | Reveals contiguous overlap |
| Minimize points covering intervals | Earliest end | Places point as early as safely possible |
The sort key should align with the exchange argument. If no exchange argument appears, the sort key is probably only a heuristic.
Proof Pattern
Sorting plus greedy proofs usually have this shape.
First, show that the first item in sorted order is safe to process.
For interval scheduling, the earliest finishing interval can replace the first interval in some optimal schedule.
For fractional knapsack, the highest-density item can replace lower-density capacity usage.
For deadline scheduling, processing by deadline ensures that a violation at the current job captures the tightest relevant constraint.
Second, show that after accepting, rejecting, or repairing, the remaining problem has the same form.
This gives the algorithm its recursive justification, even if the implementation is iterative.
Common Mistakes
Sorting by the wrong key often produces plausible but incorrect algorithms.
For interval scheduling, sorting by start time fails.
For fractional knapsack, sorting by value fails.
For prefix constraints, processing without deadline order breaks the invariant.
For merging intervals, sorting by end time makes the scan harder and error-prone.
Another common mistake is using sorting as a substitute for proof. A sorted greedy algorithm can still be wrong. The sort key must be justified by a local-safety argument.
Complexity Analysis
Most sorting plus greedy algorithms run in:
O(n log n)
because sorting dominates.
The scan is usually:
O(n)
Space is often:
O(1)
excluding the output, unless the algorithm also uses a heap, map, or auxiliary structure.
Takeaway
Sorting plus greedy is one of the most common algorithmic patterns. The sort arranges decisions so that a local rule becomes meaningful, and the scan commits to choices while preserving an invariant. The essential skill is choosing a sort key that supports an exchange argument. Once that key is found, the implementation is usually a short loop over sorted input.