Lecture 16: Mergesort

The second fast sorting algorithm is mergesort. Mergesort is a divide and conquer algorithms. A divide and conquer algorithm has the following general form:
1. Split the problem into subproblems.
2. Recusively solve each subproblem
3. Combine the solutions into a solution of the original problem.

High-level code

int[] mergesort(int[] A) {
    if (A.length == 1) return A;   // Base case of the recursion
    B1 = first half of A;         //  Divide
    B2 = second half of A;
    C1 = mergesort(B1);           // Recursively solve on the subproblems
    C2 = mergesort(B2);
    D = merge C1 and C2           // Combine the solutions
         using the two-fingered method for combining ordered lists;
    return D;
  }

Example 1:

A = [31,41,59,26,53,58,27,18,28,45,9,14,42,12,17]
A.length=15
mergesort(A)
   B1 = [31,41,59,26,53,58,27,18]
   B2 = [28,45,9,14,42,12,17]

   C1 = mergesort(B1)
       A = [31,41,59,26,53,58,27,18]
       B1 = [31,41,59,26]
       B2 = [53,58,27,18]

       C1 = mergesort(B1)
           A = [31,41,59,26]
           B1 = [31,41]
           B2 = [59,26]

           C1 = mergesort(B1)
              A = [31,41]
              B1 = [31]
              B2 = [41]
            
              C1 = mergesort(B1) return [31]
              C2 = mergesort(B2) return [41]
              D = merge([31],[41]) = [31,41]
           C1 = [31,41]

           C2 = mergesort(B2)
              A = [59,26]
              B1 = [59]
              B2 = [26]
               
              C1 = mergesort(B1) return [59]
              C2 = mergesort(B2) return [26]
              D = merge([26],[59]) return [26,59]
           C2 = [26,59]
           D = merge([31,41],[26,59])
       
       C1 = [26,31,41,59]     
// From here down I'll stop the recurrence at 4
       C2 = mergesort([53,58,27,18]) = [18,27,53,58]
       D = merge([26,31,41,59],[18,27,53,58]) = [18,26,27,31,41,53,58,59]

   C1 = [18,26,27,31,41,53,58,59]   
   C2 = mergesort([28,45,9,14,42,12,17])
       A = [28,45,9,14,42,12,17])
       B1 = [28,45,9,14]
       B2 = [42,12,17]

       C1 = mergesort([28,45,9,14]) = [9,14,28,45]
       C2 = mergesort([42,12,17]) = [12,17,42]. 
       D = merge(C1,C2) = [9,12,14,17,28,42,45]
   C2 = [9,12,14,17,28,42,45]
   D = merge([18,26,27,31,41,53,58,59], [9,12,14,17,28,42,45]) = 
       [9,12,14,17,18,26,27,28,31,41,42,45,53,58,59]

Coding tricks for efficiency: part 1

Top level call: mergesort(A, 0, A.length-1);

int[] mergesort2(int[] A, int start, int end)
    if (end-start < = 7)                    // Base case of the recursion
       return insertionSort(A,start,end);   
    m = (start+end)/2;
    C1 = mergesort2(A,0,m);           // Recursively solve on the subproblems
    C2 = mergesort2(A,m+1,end);
    D = merge(C1,C2)
    return D;
  }

More optimization

static int smallSize = 7;

mergesort3(int[] A) {
  l = A.length;
  for (s = 0; s < A.length; s = s+smallSize)
     insertionSort(A,s,min(s+SmallSize,A.length-1));
  groupSize = smallSize;
  slosh = true;                     // direction to move in
  int [] D = new int[A.length];
  while (groupSize < A.length) {
     for (s = 0; s < A.length-groupSize-1; s = s+2*groupSize) {
         if (slosh)
            merge(A,D,s,groupSize);
          else
            merge(D,A,s,groupSize);
         slosh = !slosh;
        }
     }
  if (!sloshDirection) 
      for (i=0; i < A.length; i++) 
        A[i]=D[i];
}`
 
merge(B,C,s,groupSize)   {
   i = s;           // finger through the first part of B;
   j = s+groupSize: // finger through the second part of B;
   top = min(j+groupSize,B.length) // upper limit on second part.
   for (k = s; k < top; k++) {    // finger through C
     if (i >= s+groupSize) {      // come to the end of the first half
       C[k] = B[j];
       j++;
       }
     else if (j >= top)  {        // come to the end of the second half
       C[k] = B[i];
       i++;
     else                         // working on both halfs
       if (B[i] < B[j]) {         // merge item from first half
         C[k] = B[i];
         i++;
        }
       else {                     // merge item from second half
         C[k] = B[j];
         j++
      }
   }  // end for loop
}
Note: We've turned the recursive procedure into an iterative procedure, in a rather unusual way.

Example 2

Take smallSize = 3. Example:
A = [31,41,59,26,53,58,27,18,28,45,9,14,42,12,17,32,30]

Insertion sort in groups of 3:
A = [31,41,59 | 26,53,58 | 27,18,28 | 45,9,14 | 42,12,17 | 32,30] input
A = [31,41,59 | 26,53,58 | 18,27,28 | 9,14,45 | 12,17,42 | 30,32] result.

groupSize = 3
A = [31,41,59|  26,53,58 | 18,27,28 | 9,14,45 | 12,17,42 | 30,32] result.
        |           |          |         |          |         |
        |---merge---|          |--merge--|          |--merge--|
                                                               slosh=true
D = [26,31,41,53,58,59   | 9,14,18,27,38,45   | 12,17,30,32,42]    
             |                     |                    groupSize=6
             |---------merge-------|                    slosh=false

A = [9,14,18,26,27,31,38,41,45,53,58,59       | 12,17,30,32,42]
             |                                         |    groupSize=12
             |---------------------merge---------------|    slosh=true

D = [9,12,14,17,18,26,27,30,31,32,38,41,42,45,53,58,59]

External sort

Very good for sorting on external media.

Magnetic tape: Two tapes for A; first and second half. Two tapes for D: first and second half. Merge groups in corresponding position on the two tapes, rather than consecutive groups, thus:

A (tape1) = [31,41,59|  26,53,58 | 18,27,28 | 
A (tape2)    9,14,45 |  12,17,42 | 30,32] 
Merge the groups in the same column.

The sloshing technique allows you to do this with 4 tapes. On each pass, you are reading/writing each tape in forward order, which is how tapes like to be read/written.

Disk: smallSize is the size of a disk block. The advantage of this sort is that data is read and written in blocks.

Time Analysis

Construct the tree of recursive calls:
Each call to mergesort requires time O(N) for the merge plus the time for the recursive calls. So we can calculate the overall time by labelling each node with the time for the merge, and adding these up over the whole tree.

If we add these across levels of constant depth:

At the root, there is one node of size N.
At level 1, there are two nodes of size N/2, for a total of N.
At level 2, there are 4 nodes of size N/4, for a total of N .
... At level H, there are N nodes of size 1, for a total of N .
So the overall total is N*H. But H = log(N) + 1, so the overall time is O(N*log(N)).