Lecture 12 Sorting, Searching

Joseph Haugh

University of New Mexico

Sorting

  • Sorting an array is a common operation
  • For example, say you have an array of grades and you want to print them in ascending order
    • You would first need to sort the array
  • Or say you needed to find a specific value in an array
    • You would be better off sorting it first
  • How can we accomplish this?

Sorting Example

  • Let’s stop and think about doing sorting in real life
  • How would you approach sorting a given suit from a deck of playing cards?

Many Sorts

Bubble Sort

  • Perhaps the most natural way, albeit inefficient, to sort an array is to use a bubble sort
  • Bubble sort is the process of “bubbling” the highest value to the end of the array
  • Then you repeat this for the second highest and so on
  • Let’s try to implement this!

Bubble Sort: Step 1 Write Our Plan

void bubbleSort(int[] xs) {
    // loop over the array
    // loop backward until we get to the next slot
    // compare adjacent elements
    // move the smaller element toward the front if necessary
    // repeat, skipping the slot with the new minimum
}

Bubble Sort: Step 2 Define The Easier Parts

void bubbleSort(int[] xs) {
    // loop over the array
    for (int i = 1; i < xs.length; i++) {
        // loop backward until we get to the next slot
        for (int j = xs.length - 1; j >= i; j--) {
            // compare adjacent elements
            // move the smaller element toward the front if necessary
        }
        // repeat, skipping the slot with the new minimum
    }
}

Bubble Sort: Step 3 Define The Harder Parts

void bubbleSort(int[] xs) {
    // loop over the array
    for (int i = 1; i < xs.length; i++) {
        // loop backward until we get to the next slot
        for (int j = xs.length - 1; j >= i; j--) {
            // compare adjacent elements
            // move the smaller element toward the front if necessary
            if (xs[j-1] > xs[j]) {
                int temp = xs[j-1];
                xs[j-1] = xs[j];
                xs[j] = temp;
            }
        }
        // repeat, skipping the slot with the new minimum
    }
}

Bubble Sort: Step 4 Test

void main() {
    int[] xs = { 4, 6, 1, 3, 8, 3, 1, 2 };
    IO.println("xs = " + Arrays.toString(xs));
    bubbleSort(xs);
    IO.println("sorted xs = " + Arrays.toString(xs));
}

Sorting Efficiency

  • As you might surmise, bubbleSort is not terribly efficient
  • In fact we say that it has a time complexity of O(n2)
    • This means given an array of length 1000 it would take 1,000,000 “steps” to complete
  • Better sorting algorithms such as merge sort or quicksort have a time complexity of O(nlog (n))
    • This means given an array of length 1000 it would take ~3000 “steps” to complete

Illustrating Sorting Efficiency

  • It might be hard to grasp just how big a difference a good time complexity makes
  • To illustrate this we will need a function to generate an n sized array of random ints:
int[] randomIntArray(Random rand, int len) {
    int[] xs = new int[len];
    for (int i = 0; i < len; i++) {
        xs[i] = rand.nextInt(0, 100);
    }
    return xs;
}

Illustrating Sorting Efficiency

  • Then we can use this to generate a:
    • small array (10 elements)
    • medium array (10000 elements)
    • large array (10000000 elements)
  • We will then observe how long each takes to complete

Illustrating Sorting Efficiency

void bubbleSort(int[] xs) {
    // loop over the array
    for (int i = 1; i < xs.length; i++) {
        // loop backward until we get to the next slot
        for (int j = xs.length - 1; j >= i; j--) {
            // compare adjacent elements
            // move the smaller element toward the front if necessary
            if (xs[j-1] > xs[j]) {
                int temp = xs[j-1];
                xs[j-1] = xs[j];
                xs[j] = temp;
            }
        }
        // repeat, skipping the slot with the new minimum
    }
}

int[] randomIntArray(Random rand, int len) {
    int[] xs = new int[len];
    for (int i = 0; i < len; i++) {
        xs[i] = rand.nextInt(0, 100);
    }
    return xs;
}

String showDuration(String name, long duration) {
    return name + " ran for " + TimeUnit.NANOSECONDS.toMillis(duration) + " ms";
}

void main() {
    Random rand = new Random();
    // Small example
    int[] xs = randomIntArray(rand, 10);
    // Let's time it
    long startTime = System.nanoTime();
    bubbleSort(xs);
    long endTime = System.nanoTime();
    IO.println(showDuration("Small example", endTime - startTime));
    // Medium example
    xs = randomIntArray(rand, 10000);
    startTime = System.nanoTime();
    bubbleSort(xs);
    endTime = System.nanoTime();
    IO.println(showDuration("Medium example", endTime - startTime));
    // Large example
    xs = randomIntArray(rand, 10000000);
    startTime = System.nanoTime();
    bubbleSort(xs);
    endTime = System.nanoTime();
    IO.println(showDuration("Large example", endTime - startTime));
}

Illustrating Sorting Efficiency

Output:

Small example ran for 0 ms
Medium example ran for 146 ms
Large example ran for 146000000 ms

Illustrating Sorting Efficiency

Same code but with the built-in sort

void bubbleSort(int[] xs) {
    // loop over the array
    for (int i = 1; i < xs.length; i++) {
        // loop backward until we get to the next slot
        for (int j = xs.length - 1; j >= i; j--) {
            // compare adjacent elements
            // move the smaller element toward the front if necessary
            if (xs[j-1] > xs[j]) {
                int temp = xs[j-1];
                xs[j-1] = xs[j];
                xs[j] = temp;
            }
        }
        // repeat, skipping the slot with the new minimum
    }
}

int linearSearch(int x, int[] xs) {
    // loop over the array element by element
    for (int i = 0; i < xs.length; i++) {
        // check if each element is equal to x
        if (xs[i] == x) {
            // if it is return the current index
            return i;
        }
        // else keep looping
    }
    // if not found return -1
    return -1;
}

int[] randomIntArray(Random rand, int len) {
    int[] xs = new int[len];
    for (int i = 0; i < len; i++) {
        xs[i] = rand.nextInt(0, 100);
    }
    return xs;
}

String showDuration(String name, long duration) {
    return name + " ran for " + TimeUnit.NANOSECONDS.toMillis(duration) + " ms";
}

void main() {
    Random rand = new Random();
    // Small example
    int[] xs = randomIntArray(rand, 10);
    // Let's time it
    long startTime = System.nanoTime();
    Arrays.sort(xs);
    long endTime = System.nanoTime();
    IO.println(showDuration("Small example", endTime - startTime));
    // Medium example
    xs = randomIntArray(rand, 10000);
    startTime = System.nanoTime();
    Arrays.sort(xs);
    endTime = System.nanoTime();
    IO.println(showDuration("Medium example", endTime - startTime));
    // Large example
    xs = randomIntArray(rand, 10000000);
    startTime = System.nanoTime();
    Arrays.sort(xs);
    endTime = System.nanoTime();
    IO.println(showDuration("Large example", endTime - startTime));
}

Illustrating Sorting Efficiency

Output:

Small example ran for 2 ms
Medium example ran for 6 ms
Large example ran for 419 ms

Linear Search Step 1: Write our plan

int linearSearch(int x, int[] xs) {
    // loop over the array element by element
    // check if each element is equal to x
    // if it is return the current index
    // else keep looping
    // if not found return -1
}

Linear Search Step 2: Define the easy parts

int linearSearch(int x, int[] xs) {
    // loop over the array element by element
    for (int i = 0; i < xs.length; i++) {
        // check if each element is equal to x
        // if it is return the current index
        // else keep looping
    }
    // if not found return -1
    return -1;
}

Linear Search Step 3: Define the hard parts

int linearSearch(int x, int[] xs) {
    // loop over the array element by element
    for (int i = 0; i < xs.length; i++) {
        // check if each element is equal to x
        if (xs[i] == x) {
            // if it is return the current index
            return i;
        }
        // else keep looping
    }
    // if not found return -1
    return -1;
}

Linear Search Step 4: Test

void main() {
    int[] xs = { 2, 4, 5, 6, 1, 2, 9, 3, 2 };
    IO.println(linearSearch(2, xs));  // 0
    IO.println(linearSearch(9, xs));  // 6
    IO.println(linearSearch(10, xs)); // -1
}

Linear Search Complexity

  • Linear search is also not efficient
  • It has a time complexity of O(n)
    • This means given an array of length 1000, in the worst case, it will have to perform 1000 comparisons
  • Can we do better than this?
  • We can but we need a pre-sorted array!

Searching Sorted Arrays

  • If the array is sorted we have a lot more freedom when searching
  • We can compare the number we desire to find against the number in the middle of the array
    • If it is less then we search the first half
    • If it is more then we search the second half
    • If it is equal then we found it!
  • We repeat this until we find the number
  • This is called a binary search since we split the array in 2 parts each time

Implementing Binary Search Step 1

int binarySearch(int x, int[] xs) {
    // keep track of beginning and end of section of array
    // loop until we find the number or search everywhere
    // find the midpoint
    // compare it to x
    // if it is less, then search first half
    // if it is more, then search second half
    // if it is equal, then we found it!
    // repeat until we find the number
    // if not found return -1
}

Implementing Binary Search Step 2

int binarySearch(int x, int[] xs) {
    // keep track of beginning and end of section of array
    int beg = 0;
    int end = xs.length - 1;
    // loop until we find the number or search everywhere
    while (beg <= end) {
        // find the midpoint
        int mid = (beg + end) / 2;
        int midValue = xs[mid];
        // compare it to x
        // if it is less, then search first half
        // if it is more, then search second half
        // if it is equal, then we found it!
        // repeat until we find the number
    }
    // if not found return -1
    return -1;
}

Implementing Binary Search Step 3

int binarySearch(int x, int[] xs) {
    // keep track of beginning and end of section of array
    int beg = 0;
    int end = xs.length - 1;
    // loop until we find the number or search everywhere
    while (beg <= end) {
        // find the midpoint
        int mid = (beg + end) / 2;
        int midValue = xs[mid];
        // compare it to x
        if (x < mid) {
            // if it is less, then search first half
            end = mid - 1;
        } else if (x > mid) {
            // if it is more, then search second half
            beg = mid + 1;
        } else {
            // if it is equal, then we found it!
            return mid;
        }
        // repeat until we find the number
    }
    // if not found return -1
    return -1;
}

Implementing Binary Search Step 4

void main() {
    int[] xs = { 2, 4, 5, 6, 1, 2, 9, 3, 2 };
    IO.println(linearSearch(2, xs));  // 0
    IO.println(linearSearch(9, xs));  // 6
    IO.println(linearSearch(10, xs)); // -1
}

Illustrating Searching Efficiency

  • Let’s run the same tests of efficiency for our two searching algorithms on various sized random sorted arrays
  • We will also need more of a variety of numbers in these arrays
  • I am also going to “cheat” a bit and insert a specific number at the end of the array so we can showcase worst case performance

Illustrating Searching Efficiency

  • Here is the function to generate the “rigged sorted” arrays
int[] randomSortedIntArray(Random rand, int len) {
    int[] xs = new int[len];
    for (int i = 0; i < len; i++) {
        xs[i] = rand.nextInt(Integer.MIN_VALUE, Integer.MAX_VALUE - 1);
    }
    Arrays.sort(xs);
    xs[len - 1] = Integer.MAX_VALUE;
    return xs;
}

Illustrating Searching Efficiency

int linearSearch(int x, int[] xs) {
    // loop over the array element by element
    for (int i = 0; i < xs.length; i++) {
        // check if each element is equal to x
        if (xs[i] == x) {
            // if it is return the current index
            return i;
        }
        // else keep looping
    }
    // if not found return -1
    return -1;
}

int[] randomSortedIntArray(Random rand, int len) {
    int[] xs = new int[len];
    for (int i = 0; i < len; i++) {
        xs[i] = rand.nextInt(Integer.MIN_VALUE, Integer.MAX_VALUE - 1);
    }
    Arrays.sort(xs);
    xs[len - 1] = Integer.MAX_VALUE;
    return xs;
}

String showDuration(String name, long duration) {
    return name + " ran for " + TimeUnit.NANOSECONDS.toMillis(duration) + " ms";
}

void main() {
    Random rand = new Random();
    // Small example
    int[] xs = randomSortedIntArray(rand, 100);
    // Let's time it
    long startTime = System.nanoTime();
    linearSearch(Integer.MAX_VALUE, xs);
    long endTime = System.nanoTime();
    IO.println(showDuration("Small example", endTime - startTime));
    // Medium example
    xs = randomSortedIntArray(rand, 100000);
    startTime = System.nanoTime();
    linearSearch(Integer.MAX_VALUE, xs);
    endTime = System.nanoTime();
    IO.println(showDuration("Medium example", endTime - startTime));
    // Large example
    xs = randomSortedIntArray(rand, 100000000);
    startTime = System.nanoTime();
    linearSearch(Integer.MAX_VALUE, xs);
    endTime = System.nanoTime();
    IO.println(showDuration("Large example", endTime - startTime));
}

Illustrating Searching Efficiency

Output:

Small example ran for 0 ms
Medium example ran for 1 ms
Large example ran for 39 ms

Illustrating Searching Efficiency

int binarySearch(int x, int[] xs) {
    // keep track of beginning and end of section of array
    int beg = 0;
    int end = xs.length - 1;
    // loop until we find the number or search everywhere
    while (beg <= end) {
        // find the midpoint
        int mid = (beg + end) / 2;
        int midValue = xs[mid];
        // compare it to x
        if (x < mid) {
            // if it is less, then search first half
            end = mid - 1;
        } else if (x > mid) {
            // if it is more, then search second half
            beg = mid + 1;
        } else {
            // if it is equal, then we found it!
            return mid;
        }
        // repeat until we find the number
    }
    // if not found return -1
    return -1;
}

int[] randomSortedIntArray(Random rand, int len) {
    int[] xs = new int[len];
    for (int i = 0; i < len; i++) {
        xs[i] = rand.nextInt(Integer.MIN_VALUE, Integer.MAX_VALUE - 1);
    }
    Arrays.sort(xs);
    xs[len - 1] = Integer.MAX_VALUE;
    return xs;
}

String showDuration(String name, long duration) {
    return name + " ran for " + TimeUnit.NANOSECONDS.toMillis(duration) + " ms";
}

void main() {
    Random rand = new Random();
    // Small example
    int[] xs = randomSortedIntArray(rand, 100);
    // Let's time it
    long startTime = System.nanoTime();
    binarySearch(Integer.MAX_VALUE, xs);
    long endTime = System.nanoTime();
    IO.println(showDuration("Small example", endTime - startTime));
    // Medium example
    xs = randomSortedIntArray(rand, 100000);
    startTime = System.nanoTime();
    binarySearch(Integer.MAX_VALUE, xs);
    endTime = System.nanoTime();
    IO.println(showDuration("Medium example", endTime - startTime));
    // Large example
    xs = randomSortedIntArray(rand, 100000000);
    startTime = System.nanoTime();
    binarySearch(Integer.MAX_VALUE, xs);
    endTime = System.nanoTime();
    IO.println(showDuration("Large example", endTime - startTime));
}

Illustrating Searching Efficiency

Output:

Small example ran for 0 ms
Medium example ran for 0 ms
Large example ran for 0 ms