2
\$\begingroup\$

Intro

A sort is called super linearithmic if its running time is \$\omega(N \log N)\$. For example, \$f(N) = \omega(g(N))\$ means that \$f(N)\$ grows "faster" than \$g(N)\$. In this post, I will compare four (4) such algorithms against java.util.Arrays.sort:


Code

io.github.coderodde.util.Arrays.java:

package io.github.coderodde.util;

/**
 * This class contains some comparison sorts.
 * 
 * @author Rodion "rodde" Efremov
 * @version 1.0.0 (Oct 31, 2025)
 * @since 1.0.0 (Oct 31, 2025)
 */
public final class Arrays {

    private static final int[] SHELL_SORT_GAPS =
    { 701, 301, 132, 57, 23, 10, 4, 1 };
    
    private Arrays() {
        
    }
    
    public static <E extends Comparable<? super E>> 
        void insertionSort(E[] array) {
        for (int i = 1; i < array.length; ++i) {
            E x = array[i];
            int j = i;
            
            while (j > 0 && array[j - 1].compareTo(x) > 0) {
                array[j] = array[j - 1];
                --j;
            }
            
            array[j] = x;
        }
    }
        
    public static <E extends Comparable<? super E>> void bubbleSort(E[] array) {
        boolean swapped;
        int n = array.length;
        
        do {
            swapped = false;
            
            for (int i = 1; i < n; ++i) {
                if (array[i - 1].compareTo(array[i]) > 0) {
                    swap(array, i - 1, i);
                    swapped = true;
                }
            }
            
            --n;
        } while (swapped);
    }
    
    public static <E extends Comparable<? super E>> void combSort(E[] array) {
        int gap = array.length;
        float shrinkFactor = 1.3f;
        boolean sorted = false;
        
        while (!sorted) {
            gap = (int)(gap / shrinkFactor);
            
            if (gap <= 1) {
                gap = 1;
                sorted = true;
            } else if (gap == 9 || gap == 10) {
                gap = 11;
            }
            
            for (int i = 0; i + gap < array.length; ++i ){
                if (array[i].compareTo(array[i + gap]) > 0) {
                    swap(array, i, i + gap);
                    sorted = false;
                }
            }
        }
    }
    
    public static <E extends Comparable<? super E>> void shellSort(E[] array) {
        for (int gap : SHELL_SORT_GAPS) {
            for (int i = gap; i < array.length; ++i) {
                E tmp = array[i];
                int j = i;
                
                for (; 
                    (j >= gap) && (array[j - gap].compareTo(tmp) > 0); 
                     j -= gap) {
                 
                    array[j] = array[j - gap];
                }
                
                array[j] = tmp;
            }
        }
    }
    
    private static <E> void swap(E[] array, int i, int j) {
        E tmp = array[i];
        array[i] = array[j];
        array[j] = tmp;
    }
}

io.github.coderodde.util.demo.Demo.java:

package io.github.coderodde.util.demo;

import io.github.coderodde.util.Arrays;
import java.util.Random;

public final class Demo {

    private static final int ARRAY_LENGTH = 50_000;
    
    public static void main(String[] args) {
        Integer[] arr1 = getRandomArray();
        Integer[] arr2 = arr1.clone();
        Integer[] arr3 = arr1.clone();
        Integer[] arr4 = arr1.clone();
        Integer[] arr5 = arr1.clone();
        
        long ta = System.currentTimeMillis();
        Arrays.insertionSort(arr1);
        long tb = System.currentTimeMillis();
        
        System.out.println(
                "Insertion sort in: " + (tb - ta) + " milliseconds.");
        
        ta = System.currentTimeMillis();
        Arrays.bubbleSort(arr2);
        tb = System.currentTimeMillis();
        
        System.out.println(
                "Bubble sort in:    " + (tb - ta) + " milliseconds.");
        
        ta = System.currentTimeMillis();
        Arrays.combSort(arr3);
        tb = System.currentTimeMillis();
        
        System.out.println(
                "Comb sort in:      " + (tb - ta) + " milliseconds.");
        
        ta = System.currentTimeMillis();
        Arrays.combSort(arr4);
        tb = System.currentTimeMillis();
        
        System.out.println(
                "Shell sort in:     " + (tb - ta) + " milliseconds.");
        
        ta = System.currentTimeMillis();
        java.util.Arrays.sort(arr5);
        tb = System.currentTimeMillis();
        
        System.out.println(
                "java.util.Arrays.sort in: " 
                        + (tb - ta)
                        + " milliseconds.");
        
        boolean agree = java.util.Arrays.equals(arr1, arr2) &&
                        java.util.Arrays.equals(arr2, arr3) &&
                        java.util.Arrays.equals(arr3, arr4) &&
                        java.util.Arrays.equals(arr4, arr5);
        
        System.out.println("Sorts agree: " + agree);
    }
    
    private static Integer[] getRandomArray() {
        Random random = new Random(13L);
        Integer[] array = new Integer[ARRAY_LENGTH];
        
        for (int i = 0; i < array.length; ++i) {
            array[i] = random.nextInt();
        }
        
        return array;
    }
}

Typical output

Insertion sort in: 4090 milliseconds.
Bubble sort in:    12530 milliseconds.
Comb sort in:      37 milliseconds.
Shell sort in:     28 milliseconds.
java.util.Arrays.sort in: 45 milliseconds.
Sorts agree: true

Critique request

As always, I am eager to receive any constructive critique on my work.

\$\endgroup\$
1
  • 1
    \$\begingroup\$ I seem to remember benchmarking efforts from you I found more convincing: what gives? \$\endgroup\$ Commented 2 days ago

2 Answers 2

5
\$\begingroup\$

I think the key issue is to make sure you're gathering sufficient information. You're timing one call of each method and using that to compare them. I would suggest you call each method many times1, collect that data, and then perform statistical analysis on it.

Just looking at an average time is the most basic thing you could do, but you may wish to go further and calculate information like median time and/or standard deviations and ensure your data makes sense.

In addition to random arrays you may wish to include best and worst case scenarios for each sorting algorithm. For instance, sorting an already sorted (or mostly sorted) array.


1 Possibly thousands of times.

\$\endgroup\$
1
  • 1
    \$\begingroup\$ Good advice, especially for Java where the JVM's JIT compiler typically only optimises code that is 'hot' at runtime. Once a method has been JIT compiled, its subsequent execution times (for similar problems) is likely to reduce drastically. \$\endgroup\$ Commented 2 days ago
1
\$\begingroup\$

(I acknowledge title&introduction read benchmarking and comparison.
(I suggest to think compare implementations of several algorithms rather than compare several algorithms.)
For now going neither into benchmarking nor into how that or the sorting algorithms are coded.)

There are peculiarities with the sort implementations:

  • shellSort() uses a finite sequence of gaps.
    There are sorts that should start below 701 (not that I expect it to make a lot of difference). For lots of items Ciura's sequence is to be extended; the suggestion is using a factor of 2.25. Avoiding consecutive even numbers (if by luck, 91008 being the 1st inviting intervention):
    1578, 3551, 7990, 17977, 40448 to stay < 50_000.
    (Don't know the best first gap to use, I've seen the biggest one below #items used. Don't know whether/how to modify the sequence where #items is deemed unfortunate. I might try length/2|1.)
  • neither combSort() nor bubbleSort() get shellSort()'s & insertionSort()'s source-level short-cut ("straight insertion"):
    postponing the array write 'til all elements in what would have been a series of swaps have been moved, instead.
    (consecutive items might get moved en block/using System.arrayCopy().)
  • both shellSort() and insertionSort() use a "two-comparison" nested loop.
  • for a different organisation of nested loops with different access patterns (→memory hierarchy) see Shell-sort algorithm complexity.
\$\endgroup\$

You must log in to answer this question.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.