← General program design CS Skill Tree

Order-related manipulations

Sorted is a promise about a whole collection: walk it from the front and no value ever turns up before a value that belonged earlier. Nothing in Java enforces that promise. It holds because every method that changes the collection keeps it, and it stops holding the first time one of them does not. Three methods live around that promise. addOrdered keeps it while values arrive, a sort restores it, and a search spends it.

What "earlier" means, before any code

You cannot sort values you cannot compare, and Java will not guess how. For a primitive, < and > are built in. For an object, comparing is a method call, and the method is compareTo.

int sign = a.compareTo(b);

Read what comes back as a sign and nothing else. Negative means a comes before b. Zero means they tie. Positive means a comes after. The size of the number is not part of the contract, so the only comparisons to write are against 0.

if (a.compareTo(b) < 0)    // a comes first
if (a.compareTo(b) == 0)   // they tie
if (a.compareTo(b) > 0)    // b comes first

Every ordering method below that works on objects is built out of that one call, which is why a class has to implement Comparable before any of them can touch it. Writing that method belongs to the data abstraction area, worked in Implementing Comparable on a Student class. This page assumes a class already has it and uses what it returns.

addOrdered: never let it get out of order

Adding at the front is easy because you already know which node the new one attaches to. Adding in sorted position is a different problem, because the right spot depends on the value itself, and you do not know where it is until you go looking. addOrdered goes looking, one comparison at a time, and stops the instant it finds the spot.

public void addOrdered(E item)
{
    Node fresh = new Node(item);
    Node prev = null;
    Node cur = this.head;
    while (cur != null && cur.data.compareTo(item) < 0)
    {
        prev = cur;
        cur = cur.next;
    }
    fresh.next = cur;             // aim before you break
    if (prev == null)
    {
        this.head = fresh;        // the front, or the list was empty
    }
    else
    {
        prev.next = fresh;        // splice
    }
    this.size++;
}

Two cursors move together. cur does the looking, walking forward while the node it is standing on still holds a value smaller than the one you are inserting. prev trails one node behind it the whole way, because when cur stops, prev is the node whose next has to change.

The order inside the loop test is a safety rule. Java checks cur != null first, and only runs cur.data.compareTo(item) if that was true. Write the halves the other way round and a walk that reaches the end of the list calls compareTo on nothing and dies. Checking cur first, every time, is what keeps the second half safe.

The loop stops on the first node the new value does not belong in front of. Either half can end it: cur ran off the end, or the node cur is on is no longer smaller. Both leave the cursors exactly where the wiring needs them.

The comparison is strict. < 0, not <= 0. A value equal to the one you are inserting does not count as smaller, so the loop stops on it rather than walking past, and the new value lands immediately in front of the equal one already there. The list is sorted either way, so nothing breaks. It is worth knowing which way it goes so the output does not surprise you.

One branch covers 2 situations. prev starts as null and the only line that changes it is inside the loop, so prev is still null exactly when the loop body never ran: the list was empty, or the new value belongs in front of everything. Both want the same fix, which is that the new node becomes the head, and if (prev == null) is the one test both satisfy. On a list that keeps a sentinel node at the front, prev starts on the sentinel instead of on null, so that branch disappears completely.

Trace it 4 times from empty and the whole method is in front of you.

CallWhere the loop stopsWhich branchList after
addOrdered(30)cur is null on the first check, so the body never runsprev == null30
addOrdered(10)cur on 30, and 30 is not smaller than 10, so the body never runsprev == null10 30
addOrdered(20)one step: prev on 10, cur on 30prev.next = fresh10 20 30
addOrdered(20) againcur on the first 20, because 20 is not smaller than 20prev.next = fresh10 20 20 30

Call it once for every value in an unsorted pile and the list you end up with is sorted. That is insertion sort, the same algorithm you have seen on an array, written as pointer edits on a chain instead of shifts inside an array. It costs what insertion sort costs, because every call still has to walk to find its spot.

A sort you can write from memory

One sort is enough, and selection sort is the one to have cold, because you can reconstruct it from a sentence: find the smallest value that is not yet placed, put it in the next position, repeat. It works the same on an array and on a chain of nodes. The only thing that changes is what a swap reaches into.

On an array a swap trades 2 slots. A linked list has no slots. It has nodes joined by next references, each carrying its value in a data field. The tempting move is to move the node: unhook the one holding the smallest value and hook it in at the front instead. That is real surgery. You would touch the node before it, the node after it, the node currently in front, and possibly head, and you would need separate branches for the smallest value already being in front and for it sitting right next to the front.

None of that is necessary, because you do not need the smallest value's node in front. You need its value there. So find the node holding the smallest remaining value and trade the 2 data fields. Draw the list before and after and every arrow lands in the same place. Only the contents of 2 boxes changed.

before:  4 -> 9 -> 1 -> 6
after:   1 -> 9 -> 4 -> 6
         ^         ^
         the only 2 things that changed

Here is the algorithm on an int array, where a swap trades 2 slots.

public static void sort(int[] array)
{
    for (int i = 0; i < array.length; i++)
    {
        int min = i;
        for (int j = i + 1; j < array.length; j++)
        {
            if (array[j] < array[min])
            {
                min = j;
            }
        }
        if (min != i)
        {
            int temp = array[i];
            array[i] = array[min];
            array[min] = temp;
        }
    }
}

Two loops, nested, with 2 different jobs. The outer one marks the position about to be filled and steps forward one index at a time. The inner one hunts from the very next index onward for the smallest value not yet placed. min starts equal to i, which is a way of saying nothing has beaten this one yet, and it only moves when j finds a smaller value.

Follow 4 slots through every pass and the whole thing is visible. Only values are shown, because only values move.

Passi is onInner loop checksmin ends onArray after
1position 0, holding 49, 1, 6index 2, holding 11 9 4 6
2position 1, holding 94, 6index 2, holding 41 4 9 6
3position 2, holding 96index 3, holding 61 4 6 9
4position 3, holding 9nothing, the inner loop never runsi itself1 4 6 9

Two small rules separate a correct selection sort from one that passes the easy cases.

The comparison is strict: <, never <=. Equal values already tie, and swapping values that tie changes nothing about how the array reads. Writing <= tells the inner loop to move min onto a slot that is not smaller, and the swap that follows trades 2 values that were already in a fine order. On an array full of duplicates that is a swap per pass doing nothing.

Guard the swap with min != i. When the outer position already holds the smallest remaining value, the inner loop finds nothing better and min never moves. Swapping a slot with itself through a temporary reads one slot, writes it straight back, and writes the same value a second time. Three operations that accomplish nothing, on every pass where the array was already in order at that position.

Empty, one, many, as always. An empty array has a length of 0, the outer test fails immediately, and sort returns having touched nothing, which is correct. An array of 1 runs the outer loop once, the inner loop starts at index 1, which is already past the end, so it never runs, min is still i, and the guard skips the swap.

Search: linear on anything, halving only on an array

Searching is the reason to keep things in order, and the 2 kinds of search want 2 different things from you.

A linear search looks at values from the front until it finds a match or runs out. It needs nothing: no order, no ability to jump. It works on an array and on a chain, and on a chain it is the only kind available.

public int indexOf(Object target)
{
    int index = 0;
    Node cur = this.head;
    while (cur != null)
    {
        if (cur.data.equals(target))
        {
            return index;
        }
        cur = cur.next;
        index++;
    }
    return -1;
}

Three things in there are the whole pattern. The comparison is equals, not ==, because == on objects asks whether 2 references point at the same object and reads no fields at all. The loop test is cur != null, the one that visits every node including the last, because a search has to be able to run out. And there has to be an answer for not found, which by convention is -1 for an index and null for an object, chosen because no real index is negative.

A binary search throws away half the remaining range on every comparison, the way you find a word in a dictionary. It needs 2 things a linear search does not: the values in order, and the ability to jump straight to the middle one. An array gives you both, and Arrays.binarySearch is written for exactly that. A chain of nodes gives you only the order, because reaching the middle node means walking to it, and walking to the middle costs as much as the linear search you were trying to avoid.

On an array, sorting first can pay for itself many times over. On a linked list, sorting buys you ordered output and a slightly earlier stop when you are looking for something that is not there, and it does not buy you a faster lookup.

What "grossly inefficient" points at

This is the one area where the official topic list attaches a penalty, and it is worth reading the sentence as written.

Penalty points can be assessed in this category for grossly inefficient code

Official site: FAQs About the APE, item 15.

Grossly inefficient, not merely slow. Nobody is counting instructions or asking you to pick the theoretically best algorithm. The word points at a program doing the same work repeatedly for no reason, and in this material it takes a small number of recognizable shapes.

The shapeWhat it costsWhat to do instead
Sorting the whole list again after every inserta full sort per value addedaddOrdered, which walks once and splices
Walking the list to count it when the class keeps sizea full walk for a number you already haveread the field, and keep it correct on every path
Building output with result += ... inside a long loopa fresh copy of everything built so far, on every passfine at these sizes; know that it is the shape people mean
A second full walk inside a walk, for something the first walk knewone pass becomes as many passes as there are valuescarry the running answer in a variable
Sorting in order to find one largest valuea whole sort for one comparison per valueone walk with a running best

The check that catches all 5 takes 30 seconds. After you write a method, count how many times it looks at each value, and ask what the job actually required. Selection sort looks at each value about once per pass and there are as many passes as values, and that is the algorithm working as designed. A single walk that turns into 2 nested walks for an answer one walk could have produced is the other thing.

In the practice APE

The Practice APE Exam Instructions put one ordering problem in this area: a sort method on a linked list that keeps a sentinel node at the front. The class is given to you complete except for that one method, and reading what is given is most of the work.

private class Node
{
    private Comparable data;
    private Node next;
    // constructors
}

private Node head;
private int size;

public LinkedListDummySort()
{
    this.head = new Node(null, null);
    this.size = 0;
}

public void addFirst(Comparable data)
{
    this.head.next = new Node(data, this.head.next);
    this.size++;
}

public void sort()
{
    // yours
}

Four facts are sitting in that code, and each one decides something about the method you write.

The constructor builds a node before anything is added, so head is never null and the first real value is in head.next. An empty list is head.next == null. A walk that starts at head starts on a node holding nothing.

The field is declared Comparable, not a type parameter, so data.compareTo(other.data) is how 2 values compare and the temporary in your swap is a Comparable.

addFirst puts each value in front of the last one, so a list built by calling it reads back in the reverse of the order it was built. That is why the test that adds first then second starts out as second first and needs a real swap, while the one that adds second then first is already sorted.

toString prints the size on its own first line and then one value per line. Perfect ordering with a stale size is still wrong output, and a sort does not change the size at all.

The file also carries its own rule for this problem, in its own words: you must write the algorithm to sort, and no calls to sort methods that are part of the Java API are allowed. So Collections.sort and Arrays.sort are not available here, which is the reason to have one sort you can write from a blank file. Copying the values out into an ArrayList, sorting that, and writing them back is the same call with extra steps, so it breaks the same rule. The rule names no algorithm, so which sort you write is up to you. Selection sort swaps values rather than rewiring nodes.

The case worth checking your work against is the 10-value one. Adding these with addFirst, in this order, gives a list in no particular order:

apple, cherry, persimmon, orange, tomato,
kumquat, lemon, grape, lime, banana

And after sort(), what toString prints:

List size is: 10
apple
banana
cherry
grape
kumquat
lemon
lime
orange
persimmon
tomato

Strings compare alphabetically, because String already implements Comparable and its compareTo works character by character. There is nothing to write for that part.

Readiness check

Work each one on paper and write the answer down before you open it.

TraceaddOrdered, 4 calls from empty

A list with no sentinel, empty to start. The calls are addOrdered(50), addOrdered(20), addOrdered(50), addOrdered(35), in that order.

  1. What does the list read after all 4, and what is size?
  2. Which of the 4 calls took the prev == null branch?
  3. On the third call, which node does cur stop on, and why does it stop there rather than walking past?
  4. How many times does compareTo run on the fourth call?
Answers

1. 20 35 50 50, and size is 4.

2. The first 2. The first because the list was empty so cur was null immediately, the second because 50 is not smaller than 20 so the loop body never ran.

3. On the existing 50. The test is strictly less than, and 50 is not smaller than 50, so the loop stops and the new 50 goes in front of the old one. The list reads the same either way.

4. Twice. The list is 20 50 50, so cur compares 20 against 35 and steps forward, then compares 50 against 35 and stops. The nodes after the stop are never compared at all, which is the point of stopping early.

TraceSelection sort, pass by pass

An array holds 7 3 9 3 1, in that order, and the sort method above runs on it.

  1. Write the array after each of the 5 passes of the outer loop.
  2. How many passes actually performed a swap?
  3. On the pass where i is on the value 3 that came first, which index does min end on, and would <= have chosen a different one?
  4. Which positions hold a different value at the end than at the start?
Answers

1. After pass 1, 1 3 9 3 7. After pass 2, 1 3 9 3 7, unchanged. After pass 3, 1 3 3 9 7. After pass 4, 1 3 3 7 9. After pass 5, 1 3 3 7 9, unchanged.

2. Three of the 5. Pass 2 found nothing smaller than the 3 already in position 1, and pass 5 has no index after it to look at, so both were skipped by the min != i guard.

3. min stays equal to i, at index 1, because nothing later is strictly smaller than 3. With <=, the other 3 would have looked like a reason to move min, and the swap that followed would have traded 2 values that already tied.

4. Positions 0, 2, 3 and 4. Position 1 held a 3 before and after, and no pass ever swapped it.

TraceCount the work

A list holds 200 values and you need the largest one.

  1. Version A walks once with a running best. How many comparisons?
  2. Version B calls sort() and then reads the last value. Is the answer right, and what did it cost?
  3. Version C walks the list, and for each node walks the whole list again to check whether anything is bigger, returning the first node where nothing is. How many comparisons, and is the answer right?
  4. Which 2 of the 3 would you describe using the word in the official sentence, and why not the third?
Answers

1. 199. One per value after the first, since the first is the starting best.

2. The answer is right, and it cost a full selection sort, which is on the order of 200 times 200 comparisons, plus it changed the order of a list the caller did not ask you to reorder. That second part is a correctness problem on top of the cost.

3. About 200 times 200 again, and the answer is right. It produces the correct value by doing 200 walks where one would have done.

4. B and C. Both do a number of comparisons proportional to the square of the list length for an answer that needs one pass, and in B's case it also alters the caller's data. A is not in that category, because 199 comparisons is what the job costs: you cannot know the largest value without looking at every value once.

Further reading
  • Comparable (Java SE 19 & JDK 19). One method, and a page of contract around it. Read what it says compareTo must return and what it says about consistency with equals.
  • String (Java SE 19 & JDK 19). Its compareTo entry says exactly how 2 strings are ordered, which is why sorting words needs nothing from you.
  • Collections (Java SE 19 & JDK 19). The library's own sort, binarySearch, max and min. Read what each one requires of the list before it will work.
  • Arrays (Java SE 19 & JDK 19). The array versions. The binarySearch entry states the precondition in one sentence: the array must be sorted first, or the result is undefined.
  • Object Ordering (The Java Tutorials). Oracle's own walk through natural order, what a sort reads, and what happens to a class that has not implemented the interface.
  • Lesson: Algorithms (The Java Tutorials). What the library's sorting and searching methods actually do, in the library authors' words, including which sort algorithm each uses and why.
  • Selection sort (Wikipedia). The algorithm written out, with the cost worked and a comparison to its neighbors.
  • Insertion sort (Wikipedia). What repeated addOrdered adds up to, and why it is fast on data that is nearly sorted already.
  • Linear search (Wikipedia) and Binary search (Wikipedia). The 2 searches side by side. The binary search article is explicit that it needs random access, which is the reason it does not help on a chain.
  • Sorting algorithm (Wikipedia). The whole family, with a table comparing what each one costs. Useful for seeing where selection sort sits.
  • Time complexity (Wikipedia). The vocabulary for saying how expensive a method is without timing it, which is the language the efficiency question is really asked in.
  • Elementary Sorts, from Sedgewick and Wayne's Algorithms at Princeton. Selection sort and insertion sort built from scratch in Java, with the comparison counts measured rather than asserted.
  • Analysis of Algorithms, from the same book. How to count what a method does, which is the tool the efficiency question wants.
  • 11.1 Comparison-Based Sorting, from Pat Morin's open textbook Open Data Structures. Sorting with compareTo as the only operation, in Java, with the proofs.
  • Lecture 3: Sets and Sorting, MIT 6.006, on MIT OpenCourseWare. Video and notes on why sorted data can be searched faster, and what it costs to get there.
  • The same 2 topics for the course before this one: Sorting: the Swap, Selection Sort, and Insertion Sort and Searching an Array: Linear and Binary, both on arrays.
  • One thing that exists and is not the shape taught here: Java also has a second ordering interface, written as a separate class or as a shorthand expression, for sorting the same type more than one way. It is worth knowing about later. The official topic list names Comparable and no other interface, so it is not what these problems are built on.

The one thing to keep

Order is a promise every method has to keep, and compareTo read as a sign is the only tool that decides what "earlier" means. addOrdered walks while the node in front is still smaller, with prev trailing, checks cur != null first, and wires the new node forward before it breaks the old arrow. Selection sort scans for the smallest value not yet placed and swaps the data, never a link, with a strict comparison so ties do not move and a min != i guard so a position never swaps with itself. Search with equals, visit every node with the cur != null test, and have an answer for not found. Then count the passes, because a second walk inside the first one is what the penalty sentence is about.