Topic 2.10
Putting It Together: Control-Flow Problems
In one line
Classic small problems (FizzBuzz, primes, GCD, Fibonacci, digit tricks, binary conversion) solved with only the tools from this phase. Each one shows a reusable pattern: test the special case first, peel digits, stop early, and keep a running result.
Think of it like this
Learning to cook. Knowing what a knife, a pan and an oven do doesn't make a meal; you learn by making a few classic dishes, and each dish teaches a technique you reuse forever. These problems are the classic dishes of programming, and their techniques reappear in every later phase and in the DSA course (/dsa).
Words you'll meet
New words in this topic, in plain English. Come back here whenever one feels fuzzy.
- Edge case
- An unusual input at the limits, such as 0, 1, a negative number or an empty array, where code often breaks.
- Divisor
- A number that divides another exactly. 3 is a divisor of 12 because
12 % 3 == 0. - Prime number
- A whole number greater than 1 whose only divisors are 1 and itself: 2, 3, 5, 7, 11...
- GCD
- Greatest common divisor: the largest number that divides both of two numbers. The GCD of 48 and 18 is 6.
- LCM
- Least common multiple: the smallest number both divide into. LCM(a, b) = a / GCD(a, b) × b.
- Fibonacci sequence
- 0, 1, 1, 2, 3, 5, 8...: each number is the sum of the two before it.
- Armstrong number
- A number equal to the sum of its digits each raised to the power of the digit count, like 153 = 1³ + 5³ + 3³.
- Binary
- Base 2: numbers written with only 0 and 1. 13 in binary is 1101.
Step by step
01FizzBuzz: order of checks
Print 1 to 15, but Fizz for multiples of 3, Buzz for multiples of 5, and FizzBuzz for multiples of both. It's famous because so many people get the order wrong.
The most specific case, divisible by both (that is, by 15), must come first. A switch can't express this cleanly (it needs conditions, not constants), so an else-if ladder is the natural fit.
for (int i = 1; i <= 15; i++) {
if (i % 15 == 0) {
System.out.println("FizzBuzz");
} else if (i % 3 == 0) {
System.out.println("Fizz");
} else if (i % 5 == 0) {
System.out.println("Buzz");
} else {
System.out.println(i);
}
}02Is it prime? Stop at the square root
Naive idea: try every divisor from 2 to n - 1. Better: if n = a × b with a ≤ b, then a ≤ √n. So if nothing up to √n divides n, nothing above does either.
Write the test as d * d <= n instead of d <= Math.sqrt(n): no floating point, no rounding surprises. (For n near Integer.MAX_VALUE, d * d can overflow; use long there.) And break (or return) at the first divisor found.
int n = 97;
boolean prime = n > 1;
for (int d = 2; d * d <= n; d++) {
if (n % d == 0) {
prime = false;
break; // one divisor is enough
}
}
System.out.println(n + (prime ? " is prime" : " is not prime"));03Euclid's GCD: a loop with an unknown count
GCD(48, 18): replace the pair (a, b) by (b, a % b) until b is 0. Then a is the answer. (48, 18) → (18, 12) → (12, 6) → (6, 0), so the GCD is 6.
Why it works: any number that divides both a and b also divides a % b, so the common divisors never change while the numbers shrink fast. The number of steps grows only with the number of digits (logarithmically), which is why it's used in cryptography on numbers hundreds of digits long.
int a = 48, b = 18;
while (b != 0) {
int t = a % b;
a = b;
b = t;
}
System.out.println("GCD = " + a); // 604Fibonacci: two variables walking forward
Keep the last two values, prev and curr. Each step, the next value is their sum; then shift both forward. No array needed.
Use long: the 47th Fibonacci number (2,971,215,073) is bigger than Integer.MAX_VALUE, and an int would silently overflow to a negative number. long lasts until the 93rd. Beyond that, BigInteger (Phase 8).
long prev = 0, curr = 1;
for (int i = 0; i < 10; i++) {
System.out.print(prev + " ");
long next = prev + curr;
prev = curr;
curr = next;
}
// 0 1 1 2 3 5 8 13 21 3405Digits and bases
n % 10 is the last decimal digit and n / 10 removes it. In the same way, n % 2 is the last binary digit and n / 2 removes it. The digits come out last-first, so you either prepend them or reverse at the end.
13 → remainder 1, 6 → 0, 3 → 1, 1 → 1. Reading the remainders backwards gives 1101. Check: Integer.toBinaryString(13) returns "1101".
int n = 13;
String bits = "";
while (n > 0) {
bits = (n % 2) + bits; // prepend the new digit
n /= 2;
}
System.out.println(bits); // 110106A checklist for any loop problem
1. Write 3–5 example inputs with their outputs, including 0, 1 and a negative number. 2. Decide what changes each iteration and what stays. 3. Pick the loop (for for a count, while for a condition). 4. Pick the accumulator and its start value. 5. Decide the stop condition, and whether you can stop early. 6. Trace the smallest example on paper. 7. Run every example.
Bugs almost always hide in steps 4 and 5: a product starting at 0, <= instead of <, or a missing early exit.
Try it yourself
- 1
Break FizzBuzz on purpose
Move the
i % 15 == 0test to the bottom of the ladder. Predict what 15 prints, then run. (Fizz, because15 % 3 == 0is checked first.) - 2
Measure the square-root trick
In 'Primes up to 50', change the inner test to
d < n(no square root). Run it and compare the number of divisions with 95. Then setlimitto 10000 for both versions and compare again. - 3
Find the overflow point
In 'GCD, LCM and Fibonacci', change 47 to 46 and run. F(46) = 1,836,311,903 still fits in an int. Then try 47 again with
long f1, f2.
Code & diagrams
Expected output
1
2
Fizz
4
Buzz
Fizz
7
8
Fizz
Buzz
11
Fizz
13
14
FizzBuzzExpected output
Primes: 2 3 5 7 11 13 17 19 23 29 31 37 41 43 47
15 primes, 95 divisionsF(47) is 2,971,215,073, which doesn't fit in an int (max 2,147,483,647), so it wraps to a negative number.
Expected output
GCD(48, 18) = 6
LCM(48, 18) = 144
Fibonacci: 0 1 1 2 3 5 8 13 21 34 55 89
47th Fibonacci as int: -1323752223 (overflowed)0 is the edge case: the while loop never runs, so it is handled before the loop.
Expected output
3-digit Armstrong numbers: 153 370 371 407
13 in binary: 1101 (check: 1101)
0 in binary: 0 (check: 0)
255 in binary: 11111111 (check: 11111111)Break it on purpose
Errors are the best teachers. Make each change, read the error, guess what went wrong, then reveal the answer.
Break #1
Start a product at zero
Compute 5! with long fact = 0; instead of 1.
public class Main {
public static void main(String[] args) {
long fact = 0;
for (int i = 1; i <= 5; i++) {
fact *= i;
}
System.out.println("5! = " + fact);
}
}Break #2
Forget that 0 and 1 aren't prime
Start the prime flag as boolean prime = true; and test n = 1.
public class Main {
public static void main(String[] args) {
int n = 1;
boolean prime = true;
for (int d = 2; d * d <= n; d++) {
if (n % d == 0) { prime = false; break; }
}
System.out.println(n + " prime? " + prime);
}
}Myth vs fact
Myth
Checking divisors up to n / 2 is the efficient prime test.
Fact
Up to √n is enough. For n = 1,000,000 that's 1,000 checks instead of 500,000.
Myth
FizzBuzz is too easy to matter.
Fact
It tests ordering of conditions, the % operator and clean loop structure. Interviewers still use it as a first filter.
Myth
If the program prints the right answer for one input, it's correct.
Fact
Most bugs live at edge cases: 0, 1, negatives, the largest values. Test those on purpose.
Interview problem
The problem
Count the primes below n
Given an int n (up to 5,000,000), return how many prime numbers are strictly less than n. Explain how your solution scales.
You're given
- n can be 0, 1 or 2 (answer 0).
- Must finish well under a second for n = 5,000,000.
- Use only loops, arrays and conditions.
The interviewer follows up
Why can the inner marking loop start at i * i?
Why does the outer loop stop when i * i >= n?
How would you test this?
Pro corner
Extra depth for experienced readers. New to this? Skip it for now and come back later.
- ▸
a / gcd * binstead ofa * b / gcdavoids intermediate overflow for LCM. Java'sMath.multiplyExactandMath.addExact(Java 8) throwArithmeticExceptionon overflow instead of wrapping, useful when you can't rule it out. - ▸
The JDK already has many of these:
Integer.toBinaryString,Integer.bitCount,BigInteger.gcd,BigInteger.isProbablePrime(Miller-Rabin). Know how to write them, then use the library in production. - ▸
%is relatively expensive (tens of cycles for a 32-bitidiv), but the JIT replaces% 10,/ 10and other constant divisors with multiply-and-shift sequences. Division by a variable can't be optimised that way. - ▸
For the sieve, memory access dominates: a
boolean[]of 5 million is 5 MB, larger than most L2 caches. Segmented sieves process cache-sized blocks and are several times faster for big n. The DSA course covers sieves and number theory in depth.
Remember this
- 1
Plan before typing. Write a few inputs and their expected outputs, including edge cases (0, 1, negative numbers, the biggest value). Describe the steps in plain words. Then choose the loop:
forwhen you know the count,whilewhen you loop until something happens. - 2
Order of checks matters (Topic 2.1). In FizzBuzz, test 'divisible by 15' (or both 3 and 5) before 3 and 5 alone, or 15 prints
Fizz. - 3
Stop early when you can (Topic 2.8). To test whether n is prime, you only need to try divisors up to √n, because any factor larger than √n pairs with one smaller than √n. Stop at the first divisor you find. This turns n steps into √n steps: for one million, 1,000 instead of 1,000,000.
- 4
Peel off digits with
% 10and/ 10(Topic 2.5) for digit sums, reversal, palindromes and Armstrong numbers. Repeated division with% 2and/ 2gives binary digits. - 5
Keep a running result in an accumulator: a sum starts at 0, a product at 1, a maximum at the first element (or
Integer.MIN_VALUE). Choose the type for the largest possible answer: Fibonacci numbers passint's limit at the 47th term (Topic 1.3). - 6
Euclid's algorithm for the greatest common divisor,
while (b != 0) { int t = a % b; a = b; b = t; }, is over 2,000 years old and still the fastest simple method. It's a perfect example of a loop whose count you don't know in advance.
Explain it without notes
Why is it enough to test divisors up to √n when checking whether n is prime?
In FizzBuzz, why must the 'divisible by both' check come first?
How does Euclid's algorithm work, and why does it finish quickly?
What start value should an accumulator have for a sum, a product and a maximum, and why?
Practice
Print the sum of all multiples of 3 or 5 below 1000.
Print all perfect numbers below 10,000 (a perfect number equals the sum of its divisors excluding itself, like 6 = 1 + 2 + 3).
Print the first 10 numbers that are both even and have a digit sum of 10.
Count how many steps Euclid's algorithm takes for GCD(1071, 462), and print the GCD.
Trade-offs
- ↔
Trial division needs no memory and is best for testing a few numbers; a sieve costs O(n) memory but answers 'all primes below n' far faster.
- ↔
Building strings with
+in a loop (as in the binary example) is clear but creates a new String each time; for long outputs useStringBuilder(Phase 8). - ↔
intis fast and enough for most counters;longdelays overflow;BigIntegernever overflows but is much slower. Pick the smallest type that is certainly big enough.
Done when you can
Done when you can write FizzBuzz, an is-prime check with √n and early exit, and Euclid's GCD without looking.
Done when you can peel digits for sums, reversal, palindromes and binary conversion.
Done when you choose accumulator start values and types deliberately, including overflow limits.
Done when you test every solution with edge cases (0, 1, negatives, largest values).
Done when you can explain the sieve of Eratosthenes and its trade-off against trial division.