/* Csu213, Wednesday 4/04/07 Now for Big-Oh review and a few sorting algorithms Last time we reviewed a little big-O notation. For homework you've been working on implementing a few different sorting algorithms. To show the (big) difference between the running times of O(n), O(n log n), and O(n^2) functions we'll time the sorting of large lists using Insertion Sort (O(n^2)) Quick Sort (O(n log n)) and Radix Sort (O(n)) [** Wikipedia is your friend] And, of course, you'll get to see an implementation of a really fast sort, Radix Sort, and a pretty fast randomized sort, Quick Sort, which performs well on average. */ import java.util.ArrayList; import java.util.Comparator; import java.util.Random; // The classic Algorithms class for static method implementations class Algs{ // The swap we've seen in lab and in class (and in HW) static void swap(ArrayList list, int i, int j){ list.set(i, list.set(j, list.get(i))); } // Basic insertion sort algorithm // Starting from the back, insert the current element into the // sorted 'rest' of the list. The last element is trivially sorted static void insertionSort(ArrayList list, Comparator comp){ for(int i = list.size()-2; i >= 0; i--) insert(list, i, comp); } // Insert as described in clas... swap the given element into place // in a sorted list starting at index (i+1) static void insert(ArrayList list, int i, Comparator comp){ while(i < list.size()-1 && comp.compare(list.get(i+1), list.get(i)) < 0){ swap(list, i, i+1); i++; } } // Quick Sort algorithm... only has to do with partitioning a section of // the ArrayList into lessThan and greaterThan parts then recuring. We // assume that a random pick will give us a decent pivot element. static void quickSort(ArrayList list, Comparator comp){ partition(list, comp, 0, list.size()); } /* Partition elements [start..end) of the list based on a randomly chosen pivot point. We keep track of to 'regions', the lessThan, and greaterThan regions are created, then we swap the pivot to it's right place in the ArrayList. Note that the 'end' is exclusive, not inclusive. */ static void partition(ArrayList list, Comparator comp, int start, int end){ // Only one element... it's already sorted if(start >= end)return; // Random pivot, and starting indexes for the regions int pivot = randInt(start, end), less = start+1, greater = less; // Move the pivot to the front of the ArrayList swap(list, start, pivot); // Cache the pivot... X target = list.get(start); // While the regions are not the whole ArrayList while(greater < end){ // If the current element is less than the pivot then // it belongs in the 'left' region. So we may need // to swap it there to mantain the regions if(comp.compare(list.get(greater), target) < 0){ if(less != greater) swap(list, less, greater); // The 'lessThan' region has gotten bigger less++; } // The greater region always moves back since the lessThan // region is in front of it. greater++; } // Swap the pivot back into a good spot... maintaining our regions swap(list, start, less-1); partition(list, comp, start, less-1); partition(list, comp, less, end); } // Return a nice random integer between: [low, high) static Integer randInt(int low, int high){ return new Random().nextInt(high-low)+low; } // Radix Sort Implementation /* Sorts numerical type data by sorting on each digit and combining results. It's a little messy, but it can be better if we used other Java features, but this is done with what you know so far NOTE: This is *not* a comparison based sort, which is why it can be faster than all the others. */ // We could make this faster by doing a few 'bit twiddles' instead of using // Base 10 and multiplications, we could use a power of 2 with shifts static final int BASE = 10; // Max number of digits to sort on static final int NUM_DIGITS = 6; // radixSort: Doesn't use mutation on the list passed in static ArrayList radixSort(ArrayList list){ ArrayList> newbuckets, oldbuckets = null; newbuckets = initBuckets(); // Initialize the buckets with the least signif. digit // ie. the 'ones' column for(int i = 0; i < list.size(); i++){ Integer elem = list.get(i); int digit = getDigit(elem, 0); newbuckets.get(digit).add(elem); } // For each digit after the 'ones' we put them into new // buckets, being sure to keep the oreder of the buckets // as we pull them out of the old one for(int i = 1; i < NUM_DIGITS; i++){ oldbuckets = newbuckets; newbuckets = initBuckets(); for(int j = 0; j < BASE; j++){ ArrayList curr = oldbuckets.get(j); for(int k = 0; k < curr.size(); k++){ Integer elem = curr.get(k); int digit = getDigit(elem, i); newbuckets.get(digit).add(elem); } } } // Just convert the nested lists into a single list, inorder list = new ArrayList(); for(int j = 0; j < BASE; j++){ ArrayList curr = oldbuckets.get(j); for(int k = 0; k < curr.size(); k++) list.add(curr.get(k)); } return list; } // Helper Functions... // Get digit number 'which' from 'num', Examples: // getDigit(245, 1) = 4 // getDigit(245, 2) = 2 // getDigit(245, 3) = 0 static int getDigit(int num, int which){ if(which == 0)return num % BASE; return (num / (which*BASE)) % BASE; } // Initializes a (List-of (List-of Integers)) that we will use to store // our numbers while sorting static ArrayList> initBuckets(){ ArrayList> ret = new ArrayList>(); for(int i = 0; i < BASE; i++) ret.add(new ArrayList()); return ret; } } // Simple Integer Comparator class IntComp implements Comparator{ public int compare(Integer a, Integer b){ return a.compareTo(b); } } // Timing class to wrap Java's System timer calls class Timing{ long start; // Sets the starting time Timing(){ reset(); } // Current time in milliseconds static long current(){ return System.currentTimeMillis(); } // Reset the starting time void reset(){ start = current(); } // Give the time diference long diff(){ return current()-start; } // Simple toString() public String toString(){ return " Timing("+diff()+")"; } } // Abstract a little, so I can write my Test/Print functions without // relying one a specific sorting algorithm abstract class Sorter{ String name; Sorter(String n){ name = n; } abstract ArrayList sort(ArrayList list, Comparator comp); } // Wraps Algorithm's insertionSort class Insert extends Sorter{ Insert(){ super("insert"); } ArrayList sort(ArrayList list, Comparator comp){ Algs.insertionSort(list, comp); return list; } } // Wraps Algorithm's quickSort class Quick extends Sorter{ Quick(){ super("quick"); } ArrayList sort(ArrayList list, Comparator comp){ Algs.quickSort(list, comp); return list; } } // Wraps Algorithm's RadixSort class Radix extends Sorter{ Radix(){ super("radix"); } ArrayList sort(ArrayList list, Comparator comp){ return Algs.radixSort(list); } } // Tests and Examples public class notes_4_04_07{ // Generate a random list of integers of given size using max for a bound // on the actual numbers generated static ArrayList fillData(int size, int max){ ArrayList list = new ArrayList(size); for(int i = 0; i < size; i++) list.add(randomInt(max)); return list; } // Just a wrapper function for Algs randInt(...) static Integer randomInt(int top){ return new Integer(Algs.randInt(0, top)); } // Wrapper for System.out.println public static void println(String s){ print(s+'\n'); } // Wrapper for System.out.print public static void print(String s){ System.out.print(s); } // Normal main(...) for running Tests public static void main(String[] args) { // Number of times to run our timings // 'final' means we can't change (mutate) these ints final int loops = 5, runs = 100; // Current size of the lists being sorted int size = 64; // Easy visual sort tests simpleTest(new Insert()); simpleTest(new Quick()); simpleTest(new Radix()); // Print out the Timings for each of the sorts in a kind of table println("\nTimings:"); println(" N\tinsert\tquick\tradix"); for(int j = 0; j < loops; j++){ print(" "+size+"\t "); print(timeSort(new Insert(), runs, size)+"\t "); print(timeSort(new Quick(), runs, size)+"\t "); println(""+timeSort(new Radix(), runs, size)); size *= 2; } } // Abstract Test methods incase I add more Sorters // Visual check of the sorter methods static void simpleTest(Sorter sorter){ ArrayList list = fillData(10, 40); println(" Start\t: "+list); list = sorter.sort(list, new IntComp()); println(" Sort("+sorter.name+")\t: "+list); } // Timing of a number of runs of a sorter method static long timeSort(Sorter sorter, int runs, int size){ ArrayList list; long total = 0; Timing time; for(int i = 0; i < runs; i++){ list = fillData(size, 1000); time = new Timing(); sorter.sort(list, new IntComp()); total += time.diff(); } return total; } } /* When you run the above code, you should notice that the Quick Sort starts to win pretty big after a while and Radix wins even more. This has to do with the *order* of running times of the three sort algorithms. As discussed, Insertion Sort is O(n^2) and Quick Sort is O(N log N) on average, and Radix is O(n). The reason I keep saying 'on average' for Quick Sort is because it actually depends on the order of the list before sorting. If you hand the method an already sorted list, then the partition method would divide the array into an empty region, and a full region which would make the algorithm run in O(n^2) time. Here's a typical run on my machine of the above code... Note the difference that Quick and Radix Sorts make on these randomly created ArrayLists Start : [5, 12, 13, 3, 37, 34, 20, 4, 16, 4] Sort(insert) : [3, 4, 4, 5, 12, 13, 16, 20, 34, 37] Start : [31, 21, 37, 29, 8, 9, 30, 21, 14, 30] Sort(quick) : [8, 9, 14, 21, 21, 29, 30, 30, 31, 37] Start : [33, 31, 37, 18, 3, 38, 34, 32, 11, 17] Sort(radix) : [3, 11, 17, 18, 31, 32, 33, 34, 37, 38] Timings: N insert quick radix 64 72 53 54 128 176 57 41 256 677 144 84 512 2746 320 149 1024 12496 682 307 2048 45721 1513 621 */ /* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * Graph Algorithms There will be more on these in tomrorrow's notes but we will discuss strategies for representing general graphs. Below is a simple Graph: A --- B --- D \ / / \ / / C --- E As a definition of a Graph, we have a set of Vertices (or Nodes) and Edges, where an edge is usually a Pair of Vertices with an associated Weight (with some meaning). Usually the Weight is some cost of 'traveling' down that portion of the Graph. This could be actual distance, or just the dollar cost of a flight. If we say that the above graph has Weights of 1 then our abstract representation would be something like: V = {A,B,C,D,E} E = {(A,B,1),(A,C,1), (B,A,1),(B,C,1),(B,D,1), (C,A,1),(C,B,1)(C,E,1), (D,B,1),(D,E,1), (E,C,1)(E,D,1)} How do we translate this abstract definition into Java classes or Scheme structs? Well there are three different ways it is usually done. 1) If we translate it directly, then we get a set representation which is a little inefficient to write algorithms for, but its simple to discuss. 2) We could use an Adjacency List representation where we have a List (or HashMap) of Vertices, and each Vertex has a list of (Vertex, Weight) pairs. That would look like: L = ((A, ((B,1),(C,1))) (B, ((A,1),(C,1),(D,1))), (C, ((A,1),(B,1),(E,1))), (D, ((B,1),(E,1))), (E, ((C,1),(D,1)))) 3) There are some algorithms which benefit from a different representation known as an Adjacency Matrix. Where each row/column contains the weight of the edge between the corresponding Vertices, infinity (or some marker) otherwise. If we use '-' for 'no-edge', then that would look like: A B C D E A - 1 1 - - B 1 - 1 1 - C 1 1 - - 1 D - 1 - - 1 E - - 1 1 - We can translate these into actual datadefinitions to be used in actual algorithms, but It's better to make an interface to design our algorithms, then we can make the implementation of our datastructure(s) more efficient later. */