jario_
Back to overview

Algorithms and Data Structures · Chapter 4

Classic algorithms

Some algorithms are older than any computer and still show up in almost every exam: Euclid, primes, number systems, check digits and making change. They are short, easy to trace on paper and show how much running time a good idea saves.

GCD and LCM with Euclid

You can find the greatest common divisor (GCD) without trial and error. Euclid uses this: the GCD of a and b is the same as that of b and a % b. So you keep replacing (a, b) with (b, a % b) until b is 0. Then a holds the GCD.

(1071, 462)  1071 % 462 = 147(462, 147)   462 % 147 = 21(147, 21)    147 % 21 = 0(21, 0)      b is 0, GCD = 21

If a is smaller than b, the first step only swaps the two. The numbers shrink fast: the number of steps only grows with the number of digits of the smaller number, so O(log n).

The least common multiple follows from it: lcm(a, b) = a / gcd(a, b) · b. Divide first, otherwise a · b quickly overflows.

public class Main {    public static int gcd(int a, int b) {        while (b != 0) {            int r = a % b;            System.out.println("a = " + a + ", b = " + b + ", r = " + r);            a = b;            b = r;        }        return a;    }
    public static long lcm(int a, int b) {        return (long) (a / gcd(a, b)) * b;    }
    public static void main(String[] args) {        System.out.println("gcd: " + gcd(1071, 462));        System.out.println("lcm: " + lcm(4, 6));    }}

Primes and the sieve

A prime is greater than 1 and only divisible by 1 and itself. For the test it is enough to try divisors up to √n: if n = a · b, one of the two factors is at most √n. Instead of t <= Math.sqrt(n) you usually write t * t <= n. The test then needs O(√n) steps instead of O(n).

If you need all primes up to n, the Sieve of Eratosthenes is faster. You create a boolean array and, for every prime i, cross out its multiples starting at i · i. Whatever is not crossed out at the end is prime. The sieve needs O(n log log n) steps, almost linear in practice, but it needs an array of size n.

import java.util.ArrayList;import java.util.List;
public class Main {    public static boolean isPrime(int n) {        if (n < 2) {            return false;        }        for (int t = 2; t * t <= n; t++) {            if (n % t == 0) {                return false;            }        }        return true;    }
    public static List<Integer> sieve(int n) {        boolean[] composite = new boolean[n + 1];        List<Integer> primes = new ArrayList<>();        for (int i = 2; i <= n; i++) {            if (!composite[i]) {                primes.add(i);                for (long k = (long) i * i; k <= n; k += i) {                    composite[(int) k] = true;                }            }        }        return primes;    }
    public static void main(String[] args) {        System.out.println(isPrime(91) + " " + isPrime(97));        System.out.println(sieve(50));    }}

Number systems

A number in the system with base b has the place values 1, b, b², b³, …, counted from the right. 1101 in binary is 1·8 + 1·4 + 0·2 + 1·1 = 13.

  • Decimal → base b (remainder method): divide by b repeatedly and note the remainders. The first remainder is the last digit, so reverse at the end.
  • Base b → decimal (Horner's method): go from left to right and compute value = value · b + digit.

Both need as many steps as the number has digits, so O(log n). Hexadecimal uses the digits 0–9 and A–F. Java has ready-made methods like Integer.toHexString and Integer.parseInt(text, base). In the exam, though, you should be able to do it yourself.

public class Main {    public static String toBinary(int n) {        if (n == 0) {            return "0";        }        StringBuilder bits = new StringBuilder();        while (n > 0) {            bits.append(n % 2);            n = n / 2;        }        return bits.reverse().toString();    }
    public static int fromBinary(String bits) {        int value = 0;        for (int i = 0; i < bits.length(); i++) {            value = value * 2 + (bits.charAt(i) - '0');        }        return value;    }
    public static void main(String[] args) {        System.out.println(toBinary(13));        System.out.println(fromBinary("1101"));        System.out.println(Integer.toHexString(255));        System.out.println(Integer.parseInt("FF", 16));    }}

Caesar and check digits

In the Caesar cipher, every letter is shifted by n places. You can calculate with char: c - 'a' is the position in the alphabet, % 26 makes it continue at a after z. Decrypting means shifting by -n. The + 26 prevents negative remainders. It is not secure: there are only 25 keys, and you can try them all.

A check digit does not protect against attacks, but against typing errors. In the Luhn algorithm (credit cards), every second digit from the right is doubled, and 9 is subtracted from results above 9. If the sum of all digits is divisible by 10, the number is valid. This catches every single wrong digit and most swaps of two neighbours. Both methods go through each character once: O(n).

public class Main {    public static String caesar(String text, int shift) {        StringBuilder result = new StringBuilder();        for (char c : text.toCharArray()) {            if (c >= 'a' && c <= 'z') {                result.append((char) ('a' + (c - 'a' + shift + 26) % 26));            } else if (c >= 'A' && c <= 'Z') {                result.append((char) ('A' + (c - 'A' + shift + 26) % 26));            } else {                result.append(c);            }        }        return result.toString();    }
    public static boolean luhn(String digits) {        int sum = 0;        for (int i = 0; i < digits.length(); i++) {            int digit = digits.charAt(digits.length() - 1 - i) - '0';            if (i % 2 == 1) {                digit = digit * 2;                if (digit > 9) {                    digit = digit - 9;                }            }            sum += digit;        }        return sum % 10 == 0;    }
    public static void main(String[] args) {        String secret = caesar("Hello, World!", 3);        System.out.println(secret);        System.out.println(caesar(secret, -3));        System.out.println(luhn("79927398713"));        System.out.println(luhn("79927398710"));    }}

Greedy and its limits

A greedy algorithm makes the choice that looks best right now in every step and never takes it back. For change, that means: always the largest coin that still fits. With euro coins this gives the fewest coins.

With other coin systems it goes wrong. With 4, 3 and 1, greedy pays 6 as 4 + 1 + 1, although 3 + 3 would do. And with 5 and 2 it finds no solution at all for 6, although 2 + 2 + 2 works. Greedy is fast, but only optimal for suitable problems. To find the best solution in every case you need other methods such as backtracking (chapter 6).

public class Main {    public static void change(int amount, int[] coins) {        System.out.print(amount + " =");        for (int coin : coins) {            while (amount >= coin) {                System.out.print(" " + coin);                amount -= coin;            }        }        System.out.println(amount == 0 ? "" : " (rest " + amount + ")");    }
    public static void main(String[] args) {        int[] euro = {200, 100, 50, 20, 10, 5, 2, 1};        change(388, euro);
        int[] odd = {4, 3, 1};        change(6, odd);    }}

Running times at a glance

  • GCD with Euclid: O(log n)
  • Prime test up to √n: O(√n)
  • Sieve of Eratosthenes: O(n log log n)
  • Converting to another base: O(log n)
  • Caesar, Luhn (text of length n): O(n)
  • Greedy change with k sorted coin values: O(k), with / and % per coin value

Common mistakes

  • int overflow

    a * b with two ints is computed as int, even if the result ends up in a long variable. For 100000 * 99999 you get a wrong, even negative number. Divide first (a / gcd(a, b) * b) or make one factor a long. The same applies to t * t <= n when n is close to Integer.MAX_VALUE.

  • Digits in the wrong order

    The remainder method delivers the last digit first. If you simply append the remainders, you get 011 instead of 110 for 6. The other way round, when reading, the first character is the highest place. So never test only with numbers like 101 that read the same forwards and backwards.

  • Character instead of digit

    '7' is a char with the numeric value 55, not 7. If you calculate with text.charAt(i), you get nonsense. The digit value is text.charAt(i) - '0'. In the same way, c == 1 is something other than c == '1'.