Chapter 21: Greedy Algorithms and Exchange Arguments
21.05Intermediate4 topics19 min4 interactive animations

String and frequency greedy

Greedy on strings and frequencies: Huffman coding, reorganising a string, minimum adjacent swaps for a palindrome and lexicographically smallest constructions.

Learning goals

  • Build a Huffman tree and derive prefix-free codes
  • Rearrange characters with a max-heap so neighbours differ
  • Count the adjacent swaps needed to form a palindrome
  • Build the lexicographically smallest result with a monotonic stack

Prerequisites

Greedy fundamentalsStringsHeaps and priority queues

When the data is a multiset of symbols, counting frequencies and repeatedly acting on the most or least frequent one is the greedy move, and a heap makes it cheap.

21.05.1

Huffman coding

Huffman coding assigns shorter bit strings to more frequent symbols so that the encoded text is as short as possible, with the **prefix-free** property that no code is the beginning of another, so the stream decodes without separators. The algorithm: put each symbol in a min-heap by frequency; repeatedly remove the two lightest trees, join them under a new node whose weight is their sum, and push it back. The last tree is the code tree: left edges mean 0 and right edges mean 1.

Run “abracadabra”: 5 distinct symbols need 4 merges, and the encoded length is 23 bits against 33 bits for a fixed 3-bit code on 11 characters. “Equal frequencies” (abcd) gets 8 bits against 8: when all frequencies match, there is nothing to gain. “Very skewed” saves the most relative to its size.

The greedy choice is justified by exchange: in an optimal tree the two least frequent symbols can be assumed to be deepest siblings, so merging them first is safe, and the merged node acts as one new symbol for the smaller problem.

Java
1record HuffNode(int weight, char symbol, HuffNode left, HuffNode right) {
2 boolean leaf() { return left == null; }
3}
4
5static Map<Character, String> huffman(String text) {
6 Map<Character, Integer> freq = new TreeMap<>();
7 for (char c : text.toCharArray()) freq.merge(c, 1, Integer::sum);
8 PriorityQueue<HuffNode> pq = new PriorityQueue<>(Comparator.comparingInt(HuffNode::weight));
9 for (var e : freq.entrySet()) pq.add(new HuffNode(e.getValue(), e.getKey(), null, null));
10 while (pq.size() > 1) {
11 HuffNode a = pq.poll(), b = pq.poll(); // the two lightest trees
12 pq.add(new HuffNode(a.weight() + b.weight(), '\0', a, b));
13 }
14 Map<Character, String> codes = new TreeMap<>();
15 if (!pq.isEmpty()) assign(pq.poll(), "", codes);
16 return codes;
17}
18
19static void assign(HuffNode n, String prefix, Map<Character, String> codes) {
20 if (n.leaf()) { codes.put(n.symbol(), prefix.isEmpty() ? "0" : prefix); return; } // one-symbol text gets code "0"
21 assign(n.left(), prefix + "0", codes);
22 assign(n.right(), prefix + "1", codes);
23}

Best

O(n log k)

Average

O(n log k)

Worst

O(n log k)

Space

O(k) for k distinct symbols

Huffman coding: merge the two rarest symbols

Interactive · edit the input

Your input

Try

Huffman forest · 5 nodes

a:5
b:2
c:1
d:1
r:2
Merged nowLeafNew internal node

priority queue (lightest first)

c:1
d:1
b:2
r:2
a:5
Setup

Count the symbols: a×5, b×2, c×1, d×1, r×2. Each symbol starts as a one-node tree; repeatedly merge the two lightest trees. Rare symbols end up deep (long codes), frequent ones near the root (short codes).

Step 1 / 6
Java
1static Map<Character, String> huffman(String text) {
2 Map<Character, Integer> freq = new TreeMap<>();
3 for (char c : text.toCharArray()) freq.merge(c, 1, Integer::sum);
4 PriorityQueue<Node> pq = new PriorityQueue<>(Comparator.<Node>comparingInt(n -> n.weight).thenComparingInt(n -> n.order));
5 int order = 0; for (var e : freq.entrySet()) pq.add(new Node(e.getKey(), e.getValue(), order++));
6 while (pq.size() > 1) {
7 Node a = pq.poll(), b = pq.poll(); // the two rarest subtrees
8 pq.add(new Node(a, b, a.weight + b.weight, order++)); // merge: a becomes the left (0) child, b the right (1)
9 }
10 Map<Character, String> codes = new TreeMap<>();
11 assign(pq.poll(), "", codes); // left edge = 0, right edge = 1
12 return codes;
13}

Live state

0 merges

symbols

5

length

11

No condition evaluated on this step
Execution trace (dry run table)Show 6 steps
StepLineStateWhat happens
1L5symbols=5 length=11Count the symbols: a×5, b×2, c×1, d×1, r×2. Each symbol starts as a one-node tree; repeatedly merge the two lightest trees. Rare symbols end up deep (long codes), frequent ones near the root (short codes).
2L8merged=1 + 1 weight=2 trees=4Merge the lightest two (c:1 and d:1) into a node of weight 2: the first becomes the 0-branch, the second the 1-branch.
3L8merged=2 + 2 weight=4 trees=3Merge the lightest two (b:2 and r:2) into a node of weight 4: the first becomes the 0-branch, the second the 1-branch.
4L8merged=2 + 4 weight=6 trees=2Merge the lightest two (2 and 4) into a node of weight 6: the first becomes the 0-branch, the second the 1-branch.
5L8merged=5 + 6 weight=11 trees=1Merge the lightest two (a:5 and 6) into a node of weight 11: the first becomes the 0-branch, the second the 1-branch.
6L12bits=23 fixed=33 return=a=0, b=110, c=100, d=101, r=111Encoded length 23 bits, against 33 bits with fixed-length codes. No code is a prefix of another, so decoding is unambiguous.
21.05.2

Reorganize string

Rearrange the characters so no two equal characters are adjacent, or report that it is impossible. A rearrangement exists exactly when the most frequent character occurs at most (n + 1) / 2 times (integer division). The greedy construction uses a max-heap of counts: repeatedly take the most frequent character that is not the one just placed, place it, and push it back with its count reduced.

“aab” gives “aba”, “aabbcc” gives “abcabc”, and “aaab” is impossible because a occurs 3 times and (4 + 1) / 2 = 2. Holding the previously placed character out of the heap for one step is what prevents equal neighbours.

Java
1static String reorganize(String s) {
2 int[] count = new int[26];
3 for (char c : s.toCharArray()) count[c - 'a']++;
4 PriorityQueue<int[]> heap = new PriorityQueue<>((x, y) -> y[1] - x[1]); // {letter, count}, largest count first
5 for (int i = 0; i < 26; i++) if (count[i] > 0) heap.add(new int[]{i, count[i]});
6 StringBuilder sb = new StringBuilder();
7 int[] held = null; // previous letter, kept out of the heap for one step
8 while (!heap.isEmpty()) {
9 int[] cur = heap.poll();
10 sb.append((char) ('a' + cur[0]));
11 cur[1]--;
12 if (held != null) heap.add(held);
13 held = cur[1] > 0 ? cur : null;
14 }
15 return held == null ? sb.toString() : ""; // a letter left over means the input was impossible
16}

Best

O(n log k)

Average

O(n log k)

Worst

O(n log k)

Space

O(k) for k distinct letters

Reorganize a string so no neighbours match

Interactive · edit the input

Your input

Try

heap (most frequent first)

a×2
b×1

result so far

none
Setup

A valid arrangement exists only if no letter appears more than ⌈3/2⌉ = 2 times. The most frequent letter here appears 2 times.

Step 1 / 4
Java
1static String reorganize(String s) {
2 int[] count = new int[26]; for (char c : s.toCharArray()) count[c - 'a']++;
3 PriorityQueue<int[]> pq = new PriorityQueue<>((a, b) -> b[1] != a[1] ? b[1] - a[1] : a[0] - b[0]); // most frequent first
4 for (int i = 0; i < 26; i++) if (count[i] > 0) pq.add(new int[]{i, count[i]});
5 StringBuilder sb = new StringBuilder();
6 while (pq.size() >= 2) {
7 int[] a = pq.poll(), b = pq.poll(); // the two most frequent letters differ, so they can sit side by side
8 sb.append((char) ('a' + a[0])).append((char) ('a' + b[0]));
9 if (--a[1] > 0) pq.add(a);
10 if (--b[1] > 0) pq.add(b);
11 }
12 if (!pq.isEmpty()) { int[] last = pq.poll(); if (last[1] > 1) return ""; sb.append((char) ('a' + last[0])); }
13 return sb.toString();
14}

Live state

length

3

max count

2

limit

2

No condition evaluated on this step
Execution trace (dry run table)Show 4 steps
StepLineStateWhat happens
1L3length=3 max count=2 limit=2A valid arrangement exists only if no letter appears more than ⌈3/2⌉ = 2 times. The most frequent letter here appears 2 times.
2L8took=ab result=abTake one of each of the two most frequent letters, "a" and "b": they are different, so adding them side by side cannot create a repeat.
3L12result=abaOne letter ("a") remains: append it.
4L13return="aba"Result "aba": no two neighbours are equal.
21.05.3

Minimum adjacent operations

What is the minimum number of adjacent swaps that turn a string into a palindrome? First check feasibility: at most one letter may have an odd count (none when the length is even). Then work from both ends: for the left character at position i, find the matching character nearest the right end j; swap it to position j by adjacent swaps (the number of swaps is the distance), then shrink the window. If the left character has no match, it is the odd-count letter: swap it one step toward the middle and retry.

“aabb” becomes “abba” with 2 swaps, “letelt” becomes “lettel” with 2, “racecar” is already a palindrome (0), and “abc” is impossible because three letters have an odd count and a length-3 palindrome allows one.

Java
1// returns -1 when no palindrome can be formed
2static int minSwapsToPalindrome(String s) {
3 int[] count = new int[256];
4 for (char c : s.toCharArray()) count[c]++;
5 int odd = 0;
6 for (int c : count) odd += c & 1;
7 if (odd > (s.length() & 1)) return -1;
8 char[] a = s.toCharArray();
9 int swaps = 0;
10 for (int i = 0, j = a.length - 1; i < j; i++, j--) {
11 int k = j;
12 while (k > i && a[k] != a[i]) k--; // matching character nearest the right end
13 if (k == i) { // a[i] is the odd one out: nudge it toward the centre
14 char t = a[i]; a[i] = a[i + 1]; a[i + 1] = t;
15 swaps++;
16 i--; j++; // retry the same pair of positions
17 } else {
18 for (; k < j; k++) { char t = a[k]; a[k] = a[k + 1]; a[k + 1] = t; swaps++; }
19 }
20 }
21 return swaps;
22}

Best

O(n)

Average

O(n²)

Worst

O(n²)

Space

O(n) for the character array

Fewest adjacent swaps to make a palindrome

Interactive · edit the input

Your input

Try

string · length 4

a
a
b
b
0123
Ends being matchedMatching letterJust swappedSettled pair
Setup

Work from the outside in. For the left end, find its nearest twin from the right and slide it to the right end with adjacent swaps; then settle that pair and move inward.

Step 1 / 8
Java
1static int minSwaps(char[] s) { // returns -1 if no palindrome is possible
2 int swaps = 0, l = 0, r = s.length - 1;
3 while (l < r) {
4 int k = r;
5 while (k > l && s[k] != s[l]) k--; // nearest match for s[l], searching from the right
6 if (k == l) { swap(s, l, l + 1); swaps++; continue; } // s[l] is the odd letter: nudge it toward the middle
7 for (int i = k; i < r; i++) { swap(s, i, i + 1); swaps++; } // bring the match to the right end
8 l++; r--;
9 }
10 return swaps;
11}

Live state

0 swaps

swaps

0

No condition evaluated on this step
Execution trace (dry run table)Show 8 steps
StepLineStateWhat happens
1L2swaps=0Work from the outside in. For the left end, find its nearest twin from the right and slide it to the right end with adjacent swaps; then settle that pair and move inward.
2L5l=0 r=3 swaps=0The nearest twin of 'a' from the right is at index 1.
3L7swaps=1Swap positions 1 and 2: the twin moves one step toward the right end.
4L7swaps=2Swap positions 2 and 3: the twin moves one step toward the right end.
5L8swaps=2The outer pair matches: it is settled. Continue with the inner part.
6L5l=1 r=2 swaps=2The nearest twin of 'b' from the right is at index 2.
7L8swaps=2The outer pair matches: it is settled. Continue with the inner part.
8L10return=2"abba" is a palindrome after 2 adjacent swaps.
21.05.4

Lexicographic construction

To get the lexicographically smallest result, make the earliest positions as small as possible. For 'remove k digits to make the smallest number', scan left to right with a stack: while the top digit is larger than the incoming digit and removals remain, pop it (a larger digit before a smaller one can only hurt). If the digits are increasing nothing pops and the leftover removals come off the end.

The “1432219, k = 3” preset gives 1219: the 4, 3 and 2 each pop in turn. “Leading zeros removed” turns 10200 with k = 1 into 200 after stripping the zero left in front, and removing the only digit gives 0 by convention.

The same stack pattern solves 'smallest subsequence of distinct letters' and 'largest number after removals' with the comparison reversed.

Java
1static String removeKDigits(String num, int k) {
2 StringBuilder stack = new StringBuilder();
3 for (char c : num.toCharArray()) {
4 while (k > 0 && stack.length() > 0 && stack.charAt(stack.length() - 1) > c) {
5 stack.setLength(stack.length() - 1); // pop a larger digit that precedes a smaller one
6 k--;
7 }
8 stack.append(c);
9 }
10 stack.setLength(Math.max(0, stack.length() - k)); // digits were non-decreasing: remove from the end
11 int start = 0;
12 while (start < stack.length() - 1 && stack.charAt(start) == '0') start++; // strip leading zeros
13 String result = stack.substring(start);
14 return result.isEmpty() ? "0" : result;
15}

Best

O(n)

Average

O(n)

Worst

O(n)

Space

O(n) for the stack

Smallest number after removing k digits (monotonic stack)

Interactive · edit the input

Your input

Try

digits

10
41
32
23
24
15
96

stack · k left 3

none
Setup

Remove 3 digits to make the smallest number. A digit that is larger than the digit after it should go first: it makes the number bigger at an earlier place.

Step 1 / 12
Java
1static String removeKDigits(String num, int k) {
2 Deque<Character> stack = new ArrayDeque<>();
3 for (char d : num.toCharArray()) {
4 while (k > 0 && !stack.isEmpty() && stack.peekLast() > d) { stack.pollLast(); k--; } // a larger digit before a smaller one: drop it
5 stack.addLast(d);
6 }
7 while (k-- > 0) stack.pollLast(); // still removals left: drop from the end
8 StringBuilder sb = new StringBuilder();
9 for (char c : stack) if (sb.length() > 0 || c != '0') sb.append(c); // strip leading zeros
10 return sb.length() == 0 ? "0" : sb.toString();
11}

Live state

k

3

No condition evaluated on this step
Execution trace (dry run table)Show 12 steps
StepLineStateWhat happens
1L2k=3Remove 3 digits to make the smallest number. A digit that is larger than the digit after it should go first: it makes the number bigger at an earlier place.
2L5pushed=1 k=3Keep 1.
3L5pushed=4 k=3Keep 4.
4L4removed=4 k=2 next=34 is larger than the next digit 3: remove it (k = 2 left).
5L5pushed=3 k=2Keep 3.
6L4removed=3 k=1 next=23 is larger than the next digit 2: remove it (k = 1 left).
7L5pushed=2 k=1Keep 2.
8L5pushed=2 k=1Keep 2.
9L4removed=2 k=0 next=12 is larger than the next digit 1: remove it (k = 0 left).
10L5pushed=1 k=0Keep 1.
11L5pushed=9 k=0Keep 9.
12L10return="1219"Smallest number: 1219.

Practice ladder

Now try it yourself

Easy

  • Valid Palindrome
  • Largest 3-Same-Digit Number in String

Medium

  • Reorganize String
  • Remove K Digits
  • Task Scheduler

Advanced

  • Minimum Number of Moves to Make Palindrome
  • Remove Duplicate Letters

Key takeaways

  • Huffman: repeatedly merge the two lightest trees; the result is an optimal prefix-free code.
  • Reorganize a string only if the top frequency is at most (n + 1) / 2; build it with a max-heap and a one-step hold.
  • Adjacent-swap palindromes: pair each left character with its nearest match from the right.
  • Smallest construction: a monotonic stack that pops larger digits before smaller ones.