Greedy algorithms make the locally optimal choice at each step, hoping it leads to a globally optimal solution. They are simpler than dynamic programming and often faster. But they only work when the greedy choice is provably correct. In this article, you will learn when greedy works, classic greedy problems, and how to tell greedy apart from DP.
We show core examples in Kotlin, Python, and Go.
What is a Greedy Algorithm?
A greedy algorithm makes the best possible choice at each step without looking ahead. It never reconsiders previous choices.
Simple example: making change with the fewest coins. If you have coins of 25, 10, 5, and 1, and you need to make 41 cents:
- Take a 25 (remaining: 16)
- Take a 10 (remaining: 6)
- Take a 5 (remaining: 1)
- Take a 1 (remaining: 0)
- Total: 4 coins
This greedy approach works for US coins. But it does NOT work for all coin systems. If coins are [1, 3, 4] and target is 6, greedy picks 4+1+1=3 coins, but the optimal is 3+3=2 coins. That is why coin change uses DP in the general case.
Greedy vs Dynamic Programming
| Greedy | Dynamic Programming | |
|---|---|---|
| Approach | Best local choice | Try all options, pick best |
| Speed | Usually faster | Usually slower |
| Correctness | Only when provably correct | Always correct |
| Implementation | Simpler | More complex |
| When to use | Greedy choice property holds | Overlapping subproblems |
How to tell: if making the locally best choice at each step always leads to the globally best solution, use greedy. If you need to consider multiple options and compare, use DP.
Interval Scheduling
Interval problems are the most classic greedy problems.
Activity Selection (Maximum Non-Overlapping Intervals)
Given a list of intervals with start and end times, find the maximum number of non-overlapping intervals.
Greedy strategy: Always pick the interval that ends earliest. This leaves the most room for future intervals.
Python:
def max_non_overlapping(intervals):
intervals.sort(key=lambda x: x[1]) # Sort by end time
count = 0
last_end = float("-inf")
for start, end in intervals:
if start >= last_end:
count += 1
last_end = end
return count
Kotlin:
fun maxNonOverlapping(intervals: Array<IntArray>): Int {
intervals.sortBy { it[1] }
var count = 0
var lastEnd = Int.MIN_VALUE
for ((start, end) in intervals) {
if (start >= lastEnd) {
count++
lastEnd = end
}
}
return count
}
Go:
import "sort"
func maxNonOverlapping(intervals [][]int) int {
sort.Slice(intervals, func(i, j int) bool {
return intervals[i][1] < intervals[j][1]
})
count := 0
lastEnd := -1 << 63
for _, interval := range intervals {
if interval[0] >= lastEnd {
count++
lastEnd = interval[1]
}
}
return count
}
Time: O(n log n) for sorting. Space: O(1).
Minimum Number of Meeting Rooms (Meeting Rooms II)
Given meeting intervals, find the minimum number of rooms needed.
Python:
import heapq
def min_meeting_rooms(intervals):
if not intervals:
return 0
intervals.sort(key=lambda x: x[0]) # Sort by start time
rooms = [] # Min-heap of end times
heapq.heappush(rooms, intervals[0][1])
for start, end in intervals[1:]:
if start >= rooms[0]:
heapq.heappop(rooms) # Reuse the room
heapq.heappush(rooms, end)
return len(rooms)
Time: O(n log n). Space: O(n).
Merge Intervals
Sort by start time, then merge overlapping intervals.
Python:
def merge_intervals(intervals):
intervals.sort(key=lambda x: x[0])
merged = [intervals[0]]
for start, end in intervals[1:]:
if start <= merged[-1][1]:
merged[-1][1] = max(merged[-1][1], end)
else:
merged.append([start, end])
return merged
Jump Game
Given an array where each element is the maximum jump length from that position, determine if you can reach the last index.
Greedy strategy: Track the farthest position you can reach. If you can reach or pass the last index, return true.
Python:
def can_jump(nums):
farthest = 0
for i in range(len(nums)):
if i > farthest:
return False # Cannot reach this position
farthest = max(farthest, i + nums[i])
return True
Kotlin:
fun canJump(nums: IntArray): Boolean {
var farthest = 0
for (i in nums.indices) {
if (i > farthest) return false
farthest = maxOf(farthest, i + nums[i])
}
return true
}
Go:
func canJump(nums []int) bool {
farthest := 0
for i := range nums {
if i > farthest {
return false
}
if i+nums[i] > farthest {
farthest = i + nums[i]
}
}
return true
}
Time: O(n). Space: O(1).
Jump Game II (Minimum Jumps)
Find the minimum number of jumps to reach the last index.
Python:
def jump(nums):
jumps = 0
current_end = 0
farthest = 0
for i in range(len(nums) - 1):
farthest = max(farthest, i + nums[i])
if i == current_end:
jumps += 1
current_end = farthest
return jumps
Time: O(n). Space: O(1).
Assign Cookies
You have children with greed factors and cookies with sizes. Each child gets at most one cookie, and a child is satisfied only if cookie size >= greed factor. Maximize the number of satisfied children.
Greedy strategy: Sort both arrays. Give the smallest sufficient cookie to the least greedy child.
Python:
def find_content_children(children, cookies):
children.sort()
cookies.sort()
child = cookie = 0
while child < len(children) and cookie < len(cookies):
if cookies[cookie] >= children[child]:
child += 1
cookie += 1
return child
Time: O(n log n + m log m). Space: O(1).
Task Scheduler
Given tasks with cooldown period n between same tasks, find the minimum time to execute all tasks.
Greedy strategy: The most frequent task determines the minimum time. Fill gaps with other tasks.
Python:
from collections import Counter
def least_interval(tasks, n):
freq = Counter(tasks)
max_freq = max(freq.values())
max_count = sum(1 for f in freq.values() if f == max_freq)
# Formula: (max_freq - 1) * (n + 1) + max_count
result = (max_freq - 1) * (n + 1) + max_count
return max(result, len(tasks))
Kotlin:
fun leastInterval(tasks: CharArray, n: Int): Int {
val freq = IntArray(26)
for (task in tasks) freq[task - 'A']++
val maxFreq = freq.max()
val maxCount = freq.count { it == maxFreq }
val result = (maxFreq - 1) * (n + 1) + maxCount
return maxOf(result, tasks.size)
}
Time: O(n). Space: O(1) (fixed 26 letters).
Fractional Knapsack
You have items with weights and values, and a knapsack with capacity W. You can take fractions of items. Maximize total value.
Greedy strategy: Take items with the highest value-to-weight ratio first.
Python:
def fractional_knapsack(items, capacity):
# items = [(value, weight), ...]
items.sort(key=lambda x: x[0] / x[1], reverse=True)
total_value = 0
for value, weight in items:
if capacity >= weight:
total_value += value
capacity -= weight
else:
total_value += value * (capacity / weight)
break
return total_value
Note: the 0/1 knapsack (cannot take fractions) requires DP. Greedy only works for fractional.
Huffman Coding (Simplified)
Huffman coding assigns shorter codes to more frequent characters. It uses a greedy approach:
- Create a node for each character with its frequency
- Repeatedly merge the two lowest-frequency nodes into a new node
- The result is an optimal binary tree for encoding
This is greedy because at each step, we merge the two cheapest nodes. The proof uses the exchange argument: if you merge anything else, the total cost is equal or higher.
How to Prove Greedy Works
In interviews, you usually do not need a formal proof. But these strategies help convince yourself and the interviewer:
- Exchange argument: Show that swapping the greedy choice for any other choice does not improve the solution
- Stays ahead: Show that after each step, the greedy solution is at least as good as any other
- Try counterexamples: If you cannot find a case where greedy fails, it probably works
Common Mistakes
Using greedy when DP is needed. If the greedy choice can lead to a suboptimal solution, use DP. Test with small examples.
Forgetting to sort. Most greedy algorithms require sorted input. Always check if sorting is needed.
Not handling ties correctly. When two choices have the same value, the tie-breaking rule matters.
Assuming greedy works without verification. Always test your greedy approach against a brute force solution on small inputs.
Practice Problems
| Problem | Type | LeetCode # |
|---|---|---|
| Jump Game | Farthest reach | #55 |
| Jump Game II | Min jumps | #45 |
| Non-overlapping Intervals | Interval scheduling | #435 |
| Meeting Rooms II | Interval + heap | #253 |
| Merge Intervals | Interval merge | #56 |
| Assign Cookies | Two sorted arrays | #455 |
| Task Scheduler | Frequency-based | #621 |
| Gas Station | Circular greedy | #134 |
What’s Next?
Now you know when to use greedy and when to use DP. The next article takes a deep dive into BFS and DFS patterns — Dijkstra’s algorithm, multi-source BFS, and grid-based graph problems that frequently appear in interviews.
Next: DSA Tutorial #18: BFS and DFS Deep Dive
Full series: DSA from Zero to Interview Ready