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
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.
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.
1record HuffNode(int weight, char symbol, HuffNode left, HuffNode right) {2boolean leaf() { return left == null; }3}45static Map<Character, String> huffman(String text) {6Map<Character, Integer> freq = new TreeMap<>();7for (char c : text.toCharArray()) freq.merge(c, 1, Integer::sum);8PriorityQueue<HuffNode> pq = new PriorityQueue<>(Comparator.comparingInt(HuffNode::weight));9for (var e : freq.entrySet()) pq.add(new HuffNode(e.getValue(), e.getKey(), null, null));10while (pq.size() > 1) {11HuffNode a = pq.poll(), b = pq.poll(); // the two lightest trees12pq.add(new HuffNode(a.weight() + b.weight(), '\0', a, b));13}14Map<Character, String> codes = new TreeMap<>();15if (!pq.isEmpty()) assign(pq.poll(), "", codes);16return codes;17}1819static void assign(HuffNode n, String prefix, Map<Character, String> codes) {20if (n.leaf()) { codes.put(n.symbol(), prefix.isEmpty() ? "0" : prefix); return; } // one-symbol text gets code "0"21assign(n.left(), prefix + "0", codes);22assign(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 inputHuffman forest · 5 nodes
priority queue (lightest first)
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).
1static Map<Character, String> huffman(String text) {2Map<Character, Integer> freq = new TreeMap<>();3for (char c : text.toCharArray()) freq.merge(c, 1, Integer::sum);4PriorityQueue<Node> pq = new PriorityQueue<>(Comparator.<Node>comparingInt(n -> n.weight).thenComparingInt(n -> n.order));5int order = 0; for (var e : freq.entrySet()) pq.add(new Node(e.getKey(), e.getValue(), order++));6while (pq.size() > 1) {7Node a = pq.poll(), b = pq.poll(); // the two rarest subtrees8pq.add(new Node(a, b, a.weight + b.weight, order++)); // merge: a becomes the left (0) child, b the right (1)9}10Map<Character, String> codes = new TreeMap<>();11assign(pq.poll(), "", codes); // left edge = 0, right edge = 112return codes;13}
Live state
symbols
5
length
11
Execution trace (dry run table)Show 6 stepsHide
| Step | Line | State | What happens |
|---|---|---|---|
| 1 | L5 | symbols=5 length=11 | 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). |
| 2 | L8 | merged=1 + 1 weight=2 trees=4 | Merge 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. |
| 3 | L8 | merged=2 + 2 weight=4 trees=3 | Merge 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. |
| 4 | L8 | merged=2 + 4 weight=6 trees=2 | Merge the lightest two (2 and 4) into a node of weight 6: the first becomes the 0-branch, the second the 1-branch. |
| 5 | L8 | merged=5 + 6 weight=11 trees=1 | Merge the lightest two (a:5 and 6) into a node of weight 11: the first becomes the 0-branch, the second the 1-branch. |
| 6 | L12 | bits=23 fixed=33 return=a=0, b=110, c=100, d=101, r=111 | Encoded length 23 bits, against 33 bits with fixed-length codes. No code is a prefix of another, so decoding is unambiguous. |
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.
1static String reorganize(String s) {2int[] count = new int[26];3for (char c : s.toCharArray()) count[c - 'a']++;4PriorityQueue<int[]> heap = new PriorityQueue<>((x, y) -> y[1] - x[1]); // {letter, count}, largest count first5for (int i = 0; i < 26; i++) if (count[i] > 0) heap.add(new int[]{i, count[i]});6StringBuilder sb = new StringBuilder();7int[] held = null; // previous letter, kept out of the heap for one step8while (!heap.isEmpty()) {9int[] cur = heap.poll();10sb.append((char) ('a' + cur[0]));11cur[1]--;12if (held != null) heap.add(held);13held = cur[1] > 0 ? cur : null;14}15return held == null ? sb.toString() : ""; // a letter left over means the input was impossible16}
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 inputheap (most frequent first)
result so far
A valid arrangement exists only if no letter appears more than ⌈3/2⌉ = 2 times. The most frequent letter here appears 2 times.
1static String reorganize(String s) {2int[] count = new int[26]; for (char c : s.toCharArray()) count[c - 'a']++;3PriorityQueue<int[]> pq = new PriorityQueue<>((a, b) -> b[1] != a[1] ? b[1] - a[1] : a[0] - b[0]); // most frequent first4for (int i = 0; i < 26; i++) if (count[i] > 0) pq.add(new int[]{i, count[i]});5StringBuilder sb = new StringBuilder();6while (pq.size() >= 2) {7int[] a = pq.poll(), b = pq.poll(); // the two most frequent letters differ, so they can sit side by side8sb.append((char) ('a' + a[0])).append((char) ('a' + b[0]));9if (--a[1] > 0) pq.add(a);10if (--b[1] > 0) pq.add(b);11}12if (!pq.isEmpty()) { int[] last = pq.poll(); if (last[1] > 1) return ""; sb.append((char) ('a' + last[0])); }13return sb.toString();14}
Live state
length
3
max count
2
limit
2
Execution trace (dry run table)Show 4 stepsHide
| Step | Line | State | What happens |
|---|---|---|---|
| 1 | L3 | length=3 max count=2 limit=2 | A valid arrangement exists only if no letter appears more than ⌈3/2⌉ = 2 times. The most frequent letter here appears 2 times. |
| 2 | L8 | took=ab result=ab | Take 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. |
| 3 | L12 | result=aba | One letter ("a") remains: append it. |
| 4 | L13 | return="aba" | Result "aba": no two neighbours are equal. |
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.
1// returns -1 when no palindrome can be formed2static int minSwapsToPalindrome(String s) {3int[] count = new int[256];4for (char c : s.toCharArray()) count[c]++;5int odd = 0;6for (int c : count) odd += c & 1;7if (odd > (s.length() & 1)) return -1;8char[] a = s.toCharArray();9int swaps = 0;10for (int i = 0, j = a.length - 1; i < j; i++, j--) {11int k = j;12while (k > i && a[k] != a[i]) k--; // matching character nearest the right end13if (k == i) { // a[i] is the odd one out: nudge it toward the centre14char t = a[i]; a[i] = a[i + 1]; a[i + 1] = t;15swaps++;16i--; j++; // retry the same pair of positions17} else {18for (; k < j; k++) { char t = a[k]; a[k] = a[k + 1]; a[k + 1] = t; swaps++; }19}20}21return 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 inputstring · length 4
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.
1static int minSwaps(char[] s) { // returns -1 if no palindrome is possible2int swaps = 0, l = 0, r = s.length - 1;3while (l < r) {4int k = r;5while (k > l && s[k] != s[l]) k--; // nearest match for s[l], searching from the right6if (k == l) { swap(s, l, l + 1); swaps++; continue; } // s[l] is the odd letter: nudge it toward the middle7for (int i = k; i < r; i++) { swap(s, i, i + 1); swaps++; } // bring the match to the right end8l++; r--;9}10return swaps;11}
Live state
swaps
0
Execution trace (dry run table)Show 8 stepsHide
| Step | Line | State | What happens |
|---|---|---|---|
| 1 | L2 | swaps=0 | 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. |
| 2 | L5 | l=0 r=3 swaps=0 | The nearest twin of 'a' from the right is at index 1. |
| 3 | L7 | swaps=1 | Swap positions 1 and 2: the twin moves one step toward the right end. |
| 4 | L7 | swaps=2 | Swap positions 2 and 3: the twin moves one step toward the right end. |
| 5 | L8 | swaps=2 | The outer pair matches: it is settled. Continue with the inner part. |
| 6 | L5 | l=1 r=2 swaps=2 | The nearest twin of 'b' from the right is at index 2. |
| 7 | L8 | swaps=2 | The outer pair matches: it is settled. Continue with the inner part. |
| 8 | L10 | return=2 | "abba" is a palindrome after 2 adjacent swaps. |
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.
1static String removeKDigits(String num, int k) {2StringBuilder stack = new StringBuilder();3for (char c : num.toCharArray()) {4while (k > 0 && stack.length() > 0 && stack.charAt(stack.length() - 1) > c) {5stack.setLength(stack.length() - 1); // pop a larger digit that precedes a smaller one6k--;7}8stack.append(c);9}10stack.setLength(Math.max(0, stack.length() - k)); // digits were non-decreasing: remove from the end11int start = 0;12while (start < stack.length() - 1 && stack.charAt(start) == '0') start++; // strip leading zeros13String result = stack.substring(start);14return 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 inputdigits
stack · k left 3
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.
1static String removeKDigits(String num, int k) {2Deque<Character> stack = new ArrayDeque<>();3for (char d : num.toCharArray()) {4while (k > 0 && !stack.isEmpty() && stack.peekLast() > d) { stack.pollLast(); k--; } // a larger digit before a smaller one: drop it5stack.addLast(d);6}7while (k-- > 0) stack.pollLast(); // still removals left: drop from the end8StringBuilder sb = new StringBuilder();9for (char c : stack) if (sb.length() > 0 || c != '0') sb.append(c); // strip leading zeros10return sb.length() == 0 ? "0" : sb.toString();11}
Live state
k
3
Execution trace (dry run table)Show 12 stepsHide
| Step | Line | State | What happens |
|---|---|---|---|
| 1 | L2 | k=3 | 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. |
| 2 | L5 | pushed=1 k=3 | Keep 1. |
| 3 | L5 | pushed=4 k=3 | Keep 4. |
| 4 | L4 | removed=4 k=2 next=3 | 4 is larger than the next digit 3: remove it (k = 2 left). |
| 5 | L5 | pushed=3 k=2 | Keep 3. |
| 6 | L4 | removed=3 k=1 next=2 | 3 is larger than the next digit 2: remove it (k = 1 left). |
| 7 | L5 | pushed=2 k=1 | Keep 2. |
| 8 | L5 | pushed=2 k=1 | Keep 2. |
| 9 | L4 | removed=2 k=0 next=1 | 2 is larger than the next digit 1: remove it (k = 0 left). |
| 10 | L5 | pushed=1 k=0 | Keep 1. |
| 11 | L5 | pushed=9 k=0 | Keep 9. |
| 12 | L10 | return="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.