Greedy Algorithms: Practice Problems with Solutions
Welcome to the practice problems for greedy algorithms. If you need a refresher on the theory, the concept guide covers the greedy choice property and when the pattern applies. For the code, the code templates have the patterns in all 6 languages. Each problem below includes a hint, a visual walkthrough, and the full solution.
Recommended Study Order
The problems are ordered by difficulty, but the progression matters as much as the individual solutions.
- Assign Cookies teaches the sort-then-scan shape with two sorted arrays and two pointers. Master this before anything with a cooldown or a formula.
- Best Time to Buy and Sell Stock II drops the sorting entirely. The greedy rule is a single comparison per step.
- Jump Game introduces a running state that must never go stale. It is the furthest-reach template from the template page.
- Gas Station keeps a running balance and adds a restart rule. Understanding why the restart skips every index before it is the key.
- Task Scheduler moves from scans to counting. The greedy rule produces a formula instead of a simulation.
- Candy is the hardest. Two passes run in opposite directions, and the final answer must satisfy both constraints at once.
Easy Problems
1. Assign Cookies
LeetCode 455 | Difficulty: Easy
Brief: Given child greed factors and cookie sizes, maximize the number of satisfied children.
Why this pattern: This is greedy matching. Give the smallest cookie that satisfies the current child. A larger cookie helps no one else more, so wasting it on an easy child is never optimal.
Key Insight: Sort both arrays first. Then the two-pointer scan works because the greedy choice is always the cheapest unused cookie that clears the child’s threshold.
Visual:
graph LR
A["g = [1,2,3], s = [1,1]"] --> B["cookie 1 >= greed 1: satisfied"]
B --> C["cookie 1 < greed 2: skip cookie"]
C --> D["no cookies left"]
D --> E["Result: 1 child satisfied"]
Code:
var findContentChildren = function(g, s) {
g.sort((a, b) => a - b);
s.sort((a, b) => a - b);
let child = 0;
let cookie = 0;
while (child < g.length && cookie < s.length) {
// Give the smallest cookie that clears the greed
// factor. A larger cookie would help no one else.
if (s[cookie] >= g[child]) child++;
cookie++;
}
return child;
};Both pointers advance in the same direction, and each step consumes exactly one cookie. When a cookie fails a child, that cookie can never satisfy any later child either, because the children are sorted, so it is safe to discard it. This is why the greedy matching never needs to backtrack.
Medium Problems
2. Best Time to Buy and Sell Stock II
LeetCode 122 | Difficulty: Medium
Brief: Maximize profit from buying and selling a stock, with unlimited transactions and no overlapping positions.
Why this pattern: Every price rise is a profit opportunity, and capturing every rise is never worse than skipping one. No lookahead is needed because the decision at each step depends only on the current and previous prices.
Key Insight: Sum every positive difference between consecutive prices. Each positive difference is a buy and sell pair. The greedy rule is a single comparison per step and runs in
Visual:
graph LR
A["7 1 5 3 6 4"] --> B["1 to 5: +4"]
B --> C["3 to 6: +3"]
C --> D["total profit 7"]
Code:
var maxProfit = function(prices) {
let profit = 0;
for (let i = 1; i < prices.length; i++) {
// Every rise is a profit opportunity. Buying at
// the low and selling at the high of each rise
// captures all of them.
if (prices[i] > prices[i - 1]) {
profit += prices[i] - prices[i - 1];
}
}
return profit;
};The solution never simulates a holding period. Buying on day i and selling on day j earns the same as buying and selling on every intermediate rise, because the price differences telescope. A flat or falling price contributes zero, which is exactly what the condition filters out. A single-element array returns 0 because the loop body never runs.
3. Jump Game
LeetCode 55 | Difficulty: Medium
Brief: Given an array of maximum jump lengths, determine whether the last index is reachable from the first.
Why this pattern: The furthest-reach scan is the greedy loop in its purest form. The state is a single number, the furthest index reachable so far, and it only grows.
Key Insight: If an index lies beyond the current reach, a gap exists and the end is unreachable. No later jump can help, because every later jump starts from a position you can never stand on.
Visual:
graph TD
A["nums = [2,3,1,1,4]"] --> B["i=0: reach = 2"]
B --> C["i=1: reach = max(2, 4) = 4"]
C --> D["i=2: reach stays 4"]
D --> E["i=4: reach >= last index"]
E --> F["Return true"]
Code:
var canJump = function(nums) {
let maxReach = 0;
for (let i = 0; i < nums.length; i++) {
// An index beyond our reach is a gap that no
// later jump can bridge.
if (i > maxReach) return false;
// Keep the larger reach. Widening it can only
// add options, never remove them.
maxReach = Math.max(maxReach, i + nums[i]);
}
return true;
};The single-element case returns true because the last index is index 0, which is reachable from itself. The case [0, 2] returns false because index 1 lies beyond the reach of 0. The reach update is a max, not a sum, which is the whole trick. Intermediate jumps that fall short of the current reach are simply dominated and skipped.
4. Gas Station
LeetCode 134 | Difficulty: Medium
Brief: Find the starting station that lets a car complete a circular route, given gas available and gas cost per station.
Why this pattern: A running balance decides the start. The greedy restart rule skips whole prefixes at once instead of trying each station as a candidate.
Key Insight: If the balance drops below zero at station k, then no station from the current start up to k can be a valid start either. The car that reaches station k is the one with the most fuel left, so a weaker start only fails earlier. This is why the restart jumps to k + 1 instead of trying the next candidate one by one.
Visual:
graph LR
A["net = [-2,-2,-2,3,3]"] --> B["balance drops below 0 twice"]
B --> C["restart at index 3"]
C --> D["balance stays positive from 3 on"]
D --> E["total >= 0: return 3"]
Code:
var canCompleteCircuit = function(gas, cost) {
let total = 0;
let balance = 0;
let start = 0;
for (let i = 0; i < gas.length; i++) {
const net = gas[i] - cost[i];
total += net;
balance += net;
// If the balance drops below zero, every station
// between start and i is disqualified as a start.
if (balance < 0) {
start = i + 1;
balance = 0;
}
}
return total >= 0 ? start : -1;
};Two conditions must both hold. The total net gas must be non-negative, otherwise the circuit is impossible regardless of the start. And the start candidate is the index right after the last time the running balance went negative. If the balance never drops below zero, index 0 is the answer. The greedy restart is what keeps this at
5. Task Scheduler
LeetCode 621 | Difficulty: Medium
Brief: Given a task list and a cooldown n, return the minimum total time to finish all tasks, where the same task must be separated by n other units.
Why this pattern: The most frequent task dictates the pace. The greedy rule produces a count of frames instead of a simulation of the schedule.
Key Insight: The most frequent task appears maxFreq times, so it needs maxFreq - 1 full frames of n + 1 slots plus one tail frame. Tasks tied for the most frequent each occupy one slot in the tail frame. The answer is the larger of the task count and this frame total, because when the cooldown is small, the tasks themselves fill the frames completely.
Visual:
graph TD
A["A A A B B B, n = 2"] --> B["maxFreq = 3"]
B --> C["frames = (3-1) * 3 = 6"]
C --> D["two tasks tie for max: +2"]
D --> E["max(6, 8) = 8"]
Code:
var leastInterval = function(tasks, n) {
const freq = new Array(26).fill(0);
for (const task of tasks) {
freq[task.charCodeAt(0) - 65]++;
}
const maxFreq = Math.max(...freq);
// Tasks tied for the most frequent each take
// one slot in the final frame.
const maxCount = freq.filter(f => f === maxFreq).length;
// maxFreq - 1 full frames of n + 1 slots, plus
// the tail frame holding the tied tasks.
return Math.max(tasks.length, (maxFreq - 1) * (n + 1) + maxCount);
};The frequency array has a fixed size of 26, so the time is
A A A B B B and n = 2, a formula that ignores ties returns 7, but the schedule needs 8, because both A and B need a slot in the tail frame. When the cooldown is small, like n = 1 with many distinct tasks, the frame total falls below the task count and the task count wins.Hard Problems
6. Candy
LeetCode 135 | Difficulty: Hard
Brief: Distribute candy so every child with a higher rating than a neighbor gets more candy than that neighbor, using the minimum total.
Why this pattern: Two-pass comparison. One pass enforces the left constraint, the second pass enforces the right constraint. The greedy rule is local: match the neighbor, nothing more.
Key Insight: Start every child with 1. The left pass raises a child when the left neighbor has a lower rating. The right pass does the same from the other side, and the max of the two values keeps the left constraint intact.
Visual:
graph LR
A["ratings = [1,0,2]"] --> B["left pass: [1,1,2]"]
B --> C["right pass: [2,1,2]"]
C --> D["total = 5"]
Code:
var candy = function(ratings) {
const n = ratings.length;
const candies = new Array(n).fill(1);
// Left pass: a child rated higher than the left
// neighbor gets more candy than that neighbor.
for (let i = 1; i < n; i++) {
if (ratings[i] > ratings[i - 1]) {
candies[i] = candies[i - 1] + 1;
}
}
// Right pass: same rule from the right. The max
// keeps the left-pass values intact.
for (let i = n - 2; i >= 0; i--) {
if (ratings[i] > ratings[i + 1]) {
candies[i] = Math.max(candies[i], candies[i + 1] + 1);
}
}
return candies.reduce((a, b) => a + b, 0);
};The two passes each run in
These six problems cover the full range of greedy shapes. Assign Cookies is the sort-then-scan. Best Time to Buy and Sell Stock II and Jump Game are pure scans with one state variable. Gas Station adds the restart rule. Task Scheduler turns the greedy rule into a formula. Candy layers two directional passes on top of each other. By the end, you should be able to name the greedy choice for a new problem, test it against a counterexample, and reach for the right template.