13.4 Inserting Methods

The sorting methods in this section work in a different manner from those described in section 13.2. Rather than compare adjacent items, or search for the largest one in some range, they operate on the premise that some portion of the array is already sorted, and some additional item must be inserted into the correct place in that portion. The methods of finding the proper place for an item therefore become important in this discussion. The intention of this section is to push the ideas of finding and inserting to their limits, analyze them fully, and explore every possible avenue for clever improvements. Some of the resulting code may seem to the reader to be too complex to be worth the trouble, and if so, invitations are extended in the exercises at the end of the chapter to check on the actual performance of the code produced.

To start with, consider the problem of sorting data into an array as it arrives from some source (the source could be another part of the same array, but this is not important at the moment.) At any given step, the portion of the array into which the data is being placed is already sorted, and it is necessary to find the correct place for the new item and put it there. If the existing array is sorted from lowest to highest, it is necessary to find the first position in the array with a value greater than that of the item being inserted. If there are none, the correct position is after the last item being examined. The new item will go at this place, so it is necessary to move everything from that index and beyond to the next greater index position and then copy the new value into its correct spot.

Example:

Suppose that at some point in the process the array contained:

A [0] = 0, A [1] =5, A [2] = 8, A [3] = 12, A [4] = 15

and 7 is the value of the new item to be inserted. The suggested algorithm searches through the items A [0 .. i] until it finds the first one with a value greater than 7 (which in this case is the value 8). The index of that item ([2]) is the place to put the new item. First, though, A [4], A [3], and A [2] must be moved along to the next higher indexed position, and in that order. Do you see why?

When these steps are complete, the array looks like this:

If the number 17 were now to be inserted, an examination of the items already active would reveal that all were less than the new one, so it would go in the last array position (six) and no moves would have to be made.

This informal examination of the situation then leads to a more formal expression of the problem and its solution:

Problem:

To sort a cardinal new value into an existing array [0 .. n].

Pseudocode:

Sort To Array
  Find Position
    set pos to 0
    while (pos < number in use) and (array item [pos] <= new value)
      increment pos
    end while
  Make Room
    if  pos < number in use then
      for counter = last position used to pos by -1
        set array item [counter + 1] to array item [counter]
      end for
    end if
  Insert New Item
    set array item [pos] to value

Discussion:

The insertion procedure requires a variable parameter to hold the array being altered and a value parameter to pass it the new number to insert. It also requires the index of the highest assigned element, so that it can both stop at the right place when doing comparisons, and start at the right one when doing moves. Because it is important to test such code carefully, the insert procedure has been enclosed in a module that serves as a test harness.

Code:

MODULE TestInsert;

(* By R. Sutcliffe  Last Modified 1995 04 19 *)

FROM STextIO IMPORT
  WriteLn ;
FROM SWholeIO IMPORT
  WriteCard;

PROCEDURE Insert (item : CARDINAL;  (* # to insert *)
        VAR source : ARRAY OF CARDINAL; (* array to put it in *)
        lBound, (* position from zero to start using places *)
        uBound : CARDINAL  (*  position from zero of last useable place *)  );

(* inserts the item in the sorted array from internal position lBound to uBound
Pre: the actual array sorted for the first uBound-1 positions and is large enough to insert an item and do whatever moves are required
Post: the item is inserted at the first position where the old array item is greater, or at internal position uBound if it is larger than them all  Then, items from the insert position through to position uBound-1 are moved along along by one *)

VAR
  pos,    (* will contain where to put it *)
  count : CARDINAL;     (* loop counter *)

BEGIN
  (* Find Position *)
  pos := lBound;
  WHILE (pos < uBound) AND (source [pos] <= item )
    DO
     INC (pos)
  END;    (* while *)

  (* Make Room *)
  IF pos < uBound (* otherwise goes at the end; no moves needed *)
    THEN (* this loop also skipped if howMany is zero *)
      FOR count := uBound - 1 TO pos BY -1 (* start at top end *)
        DO
          source [count + 1] := source [count];
        END;    (* for *)
    END; (* if *)

  (*  Insert New Item *)
  source [pos] := item;

END Insert;

PROCEDURE PrintIt (theArray : ARRAY OF CARDINAL; numToPrint : CARDINAL); (* print the whole array *)

VAR
  count : CARDINAL;
  
BEGIN
FOR count := 0 TO numToPrint - 1
  DO
    WriteCard (theArray [count], 5);
  END;
  WriteLn;
END PrintIt;
  
VAR (* for main program *)
  theStuff : ARRAY [1 .. 10] OF CARDINAL;

BEGIN
  theStuff [1] := 34; theStuff [2] := 37; theStuff [3] := 44;
  theStuff [4] := 134; theStuff [5] := 744; theStuff [6] := 824;
  theStuff [7] := 937; theStuff [8] := 984; theStuff [9] := 1039;
  PrintIt (theStuff, 9); (* the original *)
  Insert (5, theStuff, 0, 0); (* kills first item; no moves *)
  PrintIt (theStuff, 9);
  Insert (3500, theStuff, 7, 9); (* appends new item at the end *)
  PrintIt (theStuff, 10); (* so print one more to see it *)
  Insert (0, theStuff, 0, 1); (* insert before first, move one *)
  PrintIt (theStuff, 10);
  Insert (400, theStuff, 0, 9); (* find place in overall list *)
  PrintIt (theStuff, 10);
  Insert (7, theStuff, 0, 1); (* in after first, none moved *)
  PrintIt (theStuff, 10);
END TestInsert.

Output:

   34   37   44  134  744  824  937  984 1039
    5   37   44  134  744  824  937  984 1039
    5   37   44  134  744  824  937  984 1039 3500
    0    5   44  134  744  824  937  984 1039 3500
    0    5   44  134  400  744  824  937  984 1039
    0    7   44  134  400  744  824  937  984 1039

Observe that this procedure can be used even to insert the first item in the array, because if it starts with no active items, then the value of lastPosNum is zero and neither loop is entered, so the only statement that would be executed is source [pos] := item with pos equalling zero, which is exactly what is desired.

It should also be clear by now that the searching method presented in the above code is not the most efficient, for it is linear, requiring the examination of each element in turn until the correct location is discovered. The insert procedure can be re-cast so that it too uses the more efficient binary search discussed in the last section.

PROCEDURE InsertB (item : CARDINAL; (* # to insert *)
       VAR source : ARRAY OF CARDINAL;(* array to put it in *)
        lBound, (* position from zero to start using places *)
        uBound : CARDINAL  (*  position from zero of last useable place *)  );

(* inserts the item in the sorted array from internal position lBound to uBound
Pre: the actual array sorted for the first uBound-1 positions and is large enough to insert an item and do whatever moves are required
Post: the item is inserted at the first position where the old array item is greater, or at internal position uBound if it is larger than them all  Then, items from the insert position through to position uBound-1 are moved along along by one *)

VAR
  pos, (* will contain where to put it *)
  count : CARDINAL;     (* loop counter *)
  bottom, top : CARDINAL;

BEGIN
  (* Find Position *)
  bottom := lBound;
  IF uBound # lBound
    THEN (* else using only one item & also must avoid negatives *)
      top := uBound - 1;
      WHILE (bottom < top)
        DO
          pos := (top + bottom + 1) DIV 2;
          IF item < source [pos]
            THEN   (* ignore top half *)
              top := pos - 1;
          ELSE
            bottom := pos    (* ignore bottom half *)
          END; (* if *)
        END (* while *);
    END;
  pos := bottom;
  (* the insert position is just before, or just after this position and one comparison has yet to be made *)
  IF  (item >= source [pos]) AND (pos < uBound)
    THEN (* goes after *)
       INC (pos); 
    END; (* if *)

  (* Make Room *)
  IF pos < uBound (* otherwise it goes at the end; no moves needed *)
    THEN (* this loop also skipped if uBound is zero *)
      FOR count := uBound - 1 TO pos BY -1 (* start at high end *)
        DO
          source [count + 1] := source [count];
        END;    (* for *)
    END; (* if *)

  (*  Insert New Item *)
  source [pos] := item;

END InsertB;

This code has the advantage of being much faster at execution time; it has the disadvantage of requiring somewhat more work to plan, write, and debug. Notice the differences between this and the earlier binary find routine. Here, the concern is to locate the position to do the insert. There may already be items in the array with the same value as the one being inserted. If so, this routine places the new item after the last of these. When run as an enclosure to the same program as used to test the previous insert routine, the results were identical.

Note that, as before, the procedure Strings.Compare, or an equivalent user-written one, could be used in conjunction with the ideas presented in this section to input and sort, say, a teacher's class list by last name. However, since fewer than a hundred students would likely fall to a single teacher for marking in a given class, it is possible that a linear search and insert would be adequate.

13.4.1 The Insert Sort

So far, all that has accomplished is the insertion of single items into an already sorted array. While this is a little more than just finding out if it was already there, it is short of the goal of arriving at that sorted condition in the first place--or is it?

In fact, the insert method can be used to sort an array that starts out initially in random order. The sort is planned like this:

1. Assume that the first element of the source array constitutes a sorted list. This list one has one thing in it, so it is sorted in a trivial sense.
2. Start with the next unsorted item source [count] (initially count would equal one.)
3. Use the procedure above to insert this next item into the sorted part of the array (which ends at the position count - 1) at the correct point.
4. The sorted portion of the array now has two items. Repeat the above process with a sorted part that grows by one item at each step until the sorted section is the entire array.

Here is what the procedure to sort the array would then look like:

PROCEDURE InsertSort (VAR source : ARRAY OF CARDINAL;  lBound, uBound : CARDINAL);
  
VAR
  count : CARDINAL;
BEGIN
  FOR count := lBound TO uBound
  (* count on indices from second to last for insertions *)
    DO
      InsertB (source [count], source, lBound, count);
    END; 
END InsertSort;

This sort was tested with a similar test harness to that in the previous module and the data:

  theStuff [1] := 113; theStuff [2] := 77; theStuff [3] := 0;
  theStuff [4] := 50; theStuff [5] := 113; theStuff [6] := 114;
  theStuff [7] := 900; theStuff [8] := 113; theStuff [9] := 15;
  theStuff [10] := 300; theStuff [11] := 13; theStuff [12] := 135;
  theStuff [13] := 1;

The procedure was invoked by:

theSort (theStuff,0,13)

was made, and a line

  PrintIt (theStuff,13);  WriteLn;

was placed on the last line of the FOR loop in the procedure InsertSort to follow the progress. The output is given below with the portion of the list that is sorted (within itself) underlined. The identical output was obtained using Insert in InsertSort rather than InsertB.

  113   77    0   50  113  114  900  113   15  300   13  135    1
   77  113    0   50  113  114  900  113   15  300   13  135    1
    0   77  113   50  113  114  900  113   15  300   13  135    1
    0   50   77  113  113  114  900  113   15  300   13  135    1
    0   50   77  113  113  114  900  113   15  300   13  135    1
    0   50   77  113  113  114  900  113   15  300   13  135    1
    0   50   77  113  113  114  900  113   15  300   13  135    1
    0   50   77  113  113  113  114  900   15  300   13  135    1
    0   15   50   77  113  113  113  114  900  300   13  135    1
    0   15   50   77  113  113  113  114  300  900   13  135    1
    0   13   15   50   77  113  113  113  114  300  900  135    1
    0   13   15   50   77  113  113  113  114  135  300  900    1
    0    1   13   15   50   77  113  113  113  114  135  300  900

If one wished instead to do the entire job in a single procedure, (cutting down on the overhead of procedure calls,) the plan could be modified as follows:

1. Assign the value of the item to be sorted to a temporary variable temp. Its current position in the array is now expendible.
2. Proceed through the sorted part of list, comparing temp to the current item at each step. (This is linear.)
3. As this is done, move items that are greater than temp one position closer to the high index item, filling the space made available at the last step.
4. Stop when the correct position for temp is reached.
5. Put temp in this position.
6. Continue on to the next index ( the next unsorted item).
7. Repeat until the last indexed item is processed.

Here is the code for this version:

PROCEDURE InsertSort (VAR source : ARRAY OF CARDINAL; lBound, uBound : CARDINAL);
VAR
  count, moveCount, temp : CARDINAL;

BEGIN
  IF uBound > lBound
    THEN
      FOR count := lBound TO uBound
        DO
          temp := source [count];   (* set aside unsorted one *)
          moveCount := count;   (* start new counter there *)
          WHILE (moveCount > lBound) AND (source [moveCount - 1] > temp)
            DO
              source [moveCount] := source [moveCount - 1];
              (* move along to make room *)
              DEC (moveCount);   (* count back through sorted part *)
            END;    (* while *)
          source [moveCount] := temp;    (* do insert *)
        END;   (* for *)
  END;

END InsertSort;

The count went backwards from higher indices in doing comparisons, starting each time just below the point at which resided the next item to insert. The auxiliary variable temp was necessary, because the first position to be copied over will always be that of the one being inserted. This insert sort, while it is simpler to code, takes longer to find the correct position at which to insert something than one based on a binary search, and it makes just as many actual moves in the array as does the previous two-procedure version. The two-procedure version is also modularized, and may be easier to follow, because the searching and the sorting are compartmentalized into two program units instead of being intermingled.

Despite the improvements in searching, however, the insert sort is still O(n2). This is because the potential number of moves is O(n2) even though the searching has become more efficient.

13.4.2 The Shell Sort

There are a variety of techniques that can improve on the efficiency of the inserting method of sorting. Most of these are based on the observation that if items that are farther apart in the original array (rather than next to each other) are interchanged to achieve a move, many shorter moves may be saved. This is especially so if interchanges of items far apart are done early in the sorting process, and interchanges of items close together are done later.

One way of doing this is to sort sub-lists of the original items that are initially separated by some fixed amount, call it k. If one used an insert sort with every occurrence of an index step of 1 replaced by a step of k (and count := 1 with count := k + 1), the code would arrange the array elements so that taking every kth element (starting anywhere) produces a sorted list. That is, the sub list at indices 1, 1 + k, 1 + 2k, 1 + 3k ... is sorted, and so is the sub-list at indices 2, 2 + k, 2 + 2k, 2 + 3k, ... and so on up to the sub list at indices k - 1, 2k - 1, 3k - 1, .... There are k of these sub lists.

A list is k-sorted if every sub-list of elements separated by k positions In the original list is sorted.

Another way to look at this technique is to view the array to be sorted as if it were two-dimensional with rows of length k. A k-sort can be thought of as a sort on the columns of this rearrangement. If the number k does not divide the original number of items, not all the columns of the rearrangement will have the same number of items in them.

	An array for sorting:
23, 1, 47, 134, 89, 76, 35, 342, 987, 3, 82, 27, 15, 10, 99, 151, 121, 2, 18, 96, 753, 13, 472, 111, 58, 390, 26, 32, 100, 58, 21, 190, 56, 345, 67, 88, 32, 102

	The array rearranged for 13-sorting:
23	1	47	134	89	76	35	342	987	3	82	27	15
10	99	151	121	2	18	96	753	13	472	111	58	390
26	32	100	58	21	190	56	345	67	88	32	102

	The array 13-sorted:
10	1	47	58	2	18	35	342	13	3	32	27	15
23	32	100	121	21	76	56	345	67	88	82	58	390
26	99	151	134	89	190	96	753	987	472	111	102

Notice that "k-sorted" is the same as "sorted" if and only if k = 1. However, if one k-sorts an array for a large value of k (moving elements long distances first) and then for progressively smaller values of k (where the sort will become easier as fewer moves are required) and finally ending with a one-sort (which is a straight insertion pass), then fewer moves in total need to be made.

A sequence of k-sorts ending in k = 1 is called a Shell sort.
	The 13- sorted array arranged for 4-sorting:
10	1	47	58
2	18	35	342
13	3	32	27
15	23	32	100
121	21	76	56
345	67	88	82
58	390	26	99
151	134	89	190
96	753	987	472
111	102

	The array also 4-sorted:

2	1	26	27
10	3	32	56
13	18	32	58
15	21	35	82
58	23	47	99
96	67	76	100
111	102	88	190
121	134	89	342
151	390	987	472
345	753

Observe how the smaller items are now clustered toward the beginning, and the larger ones toward the end. An insertion sort (a one-sort) will now have far less work to do to complete the task than if the array had not been 13-sorted and 4-sorted first.

The following table illustrates how a collection of characters is sorted, also using the k-sequence 13, 4, 1. This time, the array has not been written in rows of 13 and in rows of four, so it may be a little more difficult to verify the sort by sight.

# 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 
  H E R E A R E S O  M  E  L  E  T  T  E  R  S  T  O  S  O  R  T 
  H E E E A R E S O  M  E  L  E  T  T  R  R  S  T  O  S  O  R  T
  A E E E E M E L H  O  E  O  O  R  R  R  R  S  T  S  S  T  T  T
  A E E E E E E H L  M  O  O  O  R  R  R  R  S  S  S  T  T  T  T

The first pass organizes the sub-lists at positions 1 and 14, 2 and 15, 3 and 16, etc. The second organizes the sub-lists at positions 1, 5, 9, 13, 17, and 21; at 2, 6, 10, 14, 18, and 22, and so on. The plain insertion sort conducted at the last step does not have to move any one object very far, so there are fewer reassignments overall.

Various k-sequences have been examined and used for the Shell sort, and, as might be expected, some work better than others. For simplicity, some people generate the k-sequence by starting at lastPosNum DIV 2 and thereafter taking k = k DIV 2.

This is fairly satisfactory if lastPosNum equals, say, 38. Then, the sequence of values for k is 19, 9, 4, 2, and 1. "So what?" one might say. Observe that a list that is 2-sorted is necessarily also 4-sorted as well, for if a sub-sequence of every second item is in order, the sequence of every fourth one certainly is. Some of the comparisons are therefore wasted, because there is no need to do a 4-sort if there is later going to be a 2-sort as well. In the worst case, if the position of the last item in the source array happens to be a power of 2, so will every number in the k-sequence, and the sort will rapidly lose efficiency. In general, if a j-sort is to be conducted at a later stage of a Shell sort, there is no point in doing an n*j-sort for any n at an earlier stage.

To maximize the effectiveness of a Shell sort, it is even better if there are no factors at all common among the numbers used for the values of k in the k-sequence. That is, the terms of the k-sequence should be relatively prime. (The greatest common divisor of any pair should equal one.)

A sequence that works fairly well is the one generated by the formula k(i) = 3 * k(i-1) + 1, namely 1093, 364, 121, 40, 13, 4, 1, although it does certain a subsequence of multiples of 4. The code would now look like this:

PROCEDURE ShellSort1 (VAR source: ARRAY OF CARDINAL; lBound, uBound : CARDINAL);
VAR
  k, listCount, count, moveCount, temp : CARDINAL;
  
BEGIN
  k := 1;
  REPEAT   (* get first "k" in sequence *)
    k := 3 * k + 1
  UNTIL k > uBound - lBound;
  
  REPEAT
    k := k DIV 3;   (* work backwards on same sequence *)
    FOR listCount := lBound TO lBound + k - 1
      (* Start as many k-lists as possible. *)
      DO
        count := listCount + k;
        WHILE count <= uBound 
          DO  (* each k-sort starts here *)
            temp := source [count];   (* set aside unsorted one *)
            moveCount := count;
            WHILE (moveCount >= lBound + k) AND (source [moveCount - k] > temp)
              DO
                source [moveCount] := source [moveCount - k];
               (*  move along to make room *)
                DEC (moveCount, k)   (* count backward in k-list *)
              END;
            source [moveCount] := temp;   (* insert new item *)
            INC (count, k);
          END;   (* while count *)
      END;   (* for listCount *) 
  UNTIL k = 1
END ShellSort1;

There are two additional observations that can be made at this point, some important observations and some further refinements based on these.

First, it is useful to keep the k-sequence on hand instead of generating it with a formula. One must ensure that the sequence runs high enough to sort the largest array, but this method allows for some testing that will enable one to find an optimum k-sequence by trial and error.

Second, the k-sorts in this section were accomplished by a linear search method through each of the k lists. That can be modified to utilize a binary search, even though this is more difficult to write. The key is formulating the search correctly. It is easy to examine only every kth element of a list, but the computation of the middle index takes a little thought. Suppose one has a sequence of indices in the array starting at a0 and separated by k positions. These indices themselves form an arithmetic sequence, a1, a2, a3, a4, ... ai ... whose terms are:

a1, (a1 + k) , (a1 + 2k) , (a1 + 3k) , ... (a1 + (i - 1) k )

Note the formula for the ith term

a1 = (a1 + (i - 1) k)

To get the index of the middle one of these, first note that the number of k's added is

i - 1 = (ai - a1) DIV k, or i = 1 + (ai - a1) DIV k

Half of this number is

m = i DIV 2 = (1 + (ai - a1) DIV k ) DIV 2

The actual term in the sequence of indices when a total of m k's have been added to the first index is:

tm+1 = a1 + mk = a1 + ((1 + (ai - a1) DIV k) DIV 2) k

To illustrate, suppose one was examining the 3-list of terms starting at index 4, that is, the terms t4, t7, t10, t13, t16, t19, t22, .... This list may or may not end at an index that can be expressed in the form 4 + 3j. Looking at the sequence of indices 4, 7, 10, 13, 16, 19, 22, ... and various ending points, the above formula gives for the middle index:

top index	middle
	22	13
	16	10
	21	13

all of which are satisfactory. In the event that k = 1 (the last k-sort done,) the formula above simplifies to:

tm+1 = a1 + ((1 + (ai + a1) DIV 1) DIV 2) 1

or

tm+1 = a1 + 1 + (ai - a1) DIV 2 = a1 + (1 + (ai - a1)) DIV 2

so that

2tm+1 = 2a1 + 1 + ai + a1

or

tm+1 = (a1 + ai + 1) DIV 2

which gives the same result as (a1 + ai) DIV 2 for an odd number of terms. If there are an even number of terms, there is no true middle, and either of two terms can be selected with equal effectiveness. (a1 + ai + 1) DIV 2 gives the one of these two with the higher index, and (a1 + ai) DIV 2 gives the one with the lower. Since the latter also has one less calculation, using it will save time.

With suitable changes in notation, these formulas can be used in the find portion of the sorting routine.

With these points in mind, here is another version of the Shell sort. This one conducts the binary search in a slightly different manner than did previous versions.

VAR
  kSeq : ARRAY [1 .. 10] OF CARDINAL;
  (* Permits use of a predefined sequence for k-step values. *)

PROCEDURE NewPos (low, high, step : CARDINAL) : CARDINAL;

BEGIN
  IF step = 1
    THEN
      RETURN (low + high + 1) DIV 2
    ELSIF high = low THEN
      RETURN high
    ELSE
      RETURN low + ((1 + (high - low) DIV step) DIV 2) * step
      (* calculate "middle position" as described above *)
    END;
END NewPos;
  
PROCEDURE KFindB (item : CARDINAL;
    VAR source : ARRAY OF CARDINAL;
     first, last, kStep : CARDINAL) : CARDINAL;

(* This procedure is a modification of the earlier binary search.  It searches through a sub list of the supplied array considering as consecutive those positions separated by "kStep" using a binary method and returning the index at which the new item must be inserted. *)

VAR
  bottom, top, pos : CARDINAL;

BEGIN
  bottom := first;
  top := last;
  IF kStep = 1
    THEN
      DEC (top)
    END;
  WHILE (bottom < top)
    DO
      pos := NewPos (bottom, top, kStep);
      IF item < source [pos]
        THEN   (* discard top half *)
          top := pos - kStep;
        ELSE
          bottom := pos   (* discard bottom half *)
        END; (* if *)
    END; (* while *)
  pos := bottom;
(* the insert position is just before, or just after this position and one comparison has yet to be made *)

  IF  (item >= source [pos]) AND (pos < last)
    THEN (* goes after *)
      RETURN (pos + kStep); 
    ELSE
      RETURN (pos)
    END; (* if *)

END KFindB;

PROCEDURE KInsert (VAR source: ARRAY OF CARDINAL;
     toIndex, fromIndex, kStep : CARDINAL);

(* This procedure takes source [fromIndex] and inserts it at source [atIndex] moving things along in kStep increments to fill the gap. *)

VAR
  temp, count : CARDINAL;
  swap : BOOLEAN;

BEGIN
  temp := source [fromIndex]; 
  count := fromIndex;
     
  WHILE count >= (toIndex + kStep)
    DO
      source [count] := source [count - kStep];
      DEC (count, kStep)
    END;    
      source [toIndex] := temp;   (* actual insert *)
END KInsert;

(* finally, here is the actual sort procedure that the outside world uses *)

PROCEDURE ShellSort (VAR source: ARRAY OF CARDINAL;
        lBound, uBound : CARDINAL);
VAR
  kIndex, k, listCount, count, putIndex, temp : CARDINAL;

(* compute an appropriate value of k from the sequence initialized below *)
BEGIN
  kIndex := 0;
  REPEAT
    INC (kIndex)
  UNTIL kSeq [kIndex] > (uBound DIV 2);

  REPEAT
    DEC (kIndex);
    k := kSeq [kIndex];
    FOR listCount := lBound TO lBound + k - 1
      (* Start as many k-lists as possible. *)
      DO
        count := listCount + k;
        WHILE count <= uBound
          DO   (* each k-sort starts here *)
            putIndex := KFindB (source [count], source, listCount, count, k);
            IF putIndex # count
              THEN
                KInsert (source, putIndex, count, k);
              END;
            INC (count, k)
          END;   (* while count *) 
    END  (* for listCount *)
  UNTIL kIndex = 1
END ShellSort;

This was encapsulated in a library module, with the following initialization of the k-sequence values:

BEGIN   (* initialization section *)
  kSeq [1] := 1;
  kSeq [2] := 4;
  kSeq [3] := 11;
  kSeq [4] := 23;
  kSeq [5] := 53;
  kSeq [6] := 111;
  kSeq [7] := 223;
  kSeq [8] := 451;
  kSeq [9] := 1003;
  kSeq [10] := 2029;

END Sort.

and the following modified version of PrintIt:

PROCEDURE PrintIt (theArray : ARRAY OF CARDINAL; numToPrint, numPerLine, flen : CARDINAL);

VAR
  count : CARDINAL;
  
BEGIN
FOR count := 0 TO numToPrint - 1
  DO
    WriteCard (theArray [count], flen);
    IF (count + 1) MOD numPerLine = 0
      THEN
        WriteLn;
      END;
  END;
END PrintIt;

A simple test harness to check the library module follows:

MODULE TestShellSort;

(*  By R. Sutcliffe
   To illustrate shell sorting
   last modified 1995 05 02 *)

FROM STextIO IMPORT
  WriteChar, WriteString, WriteLn;

FROM Sorts IMPORT
  PrintIt, ShellSort;
  
VAR
  theStuff : ARRAY [1 .. 13] OF CARDINAL;

BEGIN
  theStuff [1] := 113; theStuff [2] := 77; theStuff [3] := 0;
  theStuff [4] := 50; theStuff [5] := 113; theStuff [6] := 114;
  theStuff [7] := 900; theStuff [8] := 113; theStuff [9] := 15;
  theStuff [10] := 300; theStuff [11] := 13; theStuff [12] := 135;
  theStuff [13] := 1;

  PrintIt (theStuff,13,13,5);  WriteLn;
  ShellSort (theStuff,2,2); (* sort one; does nothing *)
  PrintIt (theStuff,13,13,5);  WriteLn;
  ShellSort (theStuff,1,2);  (* sort two *)
  PrintIt (theStuff,13,13,5);  WriteLn;
  ShellSort (theStuff,2,9); (* all but first two, last three *)
  PrintIt (theStuff,13,13,5);  WriteLn;
  ShellSort (theStuff,0,12); (* whole thing *)
  PrintIt (theStuff,13,13,5);  WriteLn;
 
END TestShellSort.

This produced the following output:

  113   77    0   50  113  114  900  113   15  300   13  135   1
  113   77    0   50  113  114  900  113   15  300   13  135   1
  113    0   77   50  113  114  900  113   15  300   13  135   1
  113    0   15   50   77  113  113  114  300  900   13  135   1
    0    1   13   15   50   77  113  113  113  114  135  300 900

At this point, the reader should at least be suspicious that the extensive modifications to the routine to achieve a binary search on the k-sequences may not be worth the trouble. Indeed, they may introduce so much arithmetic that they actually degrade the performance of the sort. Whether this is so, and to what extent, has been left to the exercises.

The performance of a Shell sort in comparison with the other simple sorts is difficult to analyze. It depends on the k-sequence employed, and no general solution to the problem of finding the optimum k-sequence, or analyzing performance for a given k-sequence is known. For the k-sequence 1, 4, 13, 40, 121, ... a Shell sort never does more than N3/2 comparisons, and this limit also holds for other k-sequences. One conjecture is that a Shell sort is O(n (log2n)2) Various sequences can be tested by having them stored in a separate file, and making the Shell sort obtain them from there. The one employed above was obtained with a little trial and error and found to be reasonably effective. Some of the literature on the Shell sort indicates that starting with the number of items and dividing by a shrink factor of about 1.7 for the first and each subsequent pass produces optimum results in most cases. For instance, if there were 100 items in the array to be sorted, k would first be initialized to 100, and using

k := TRUNC (FLOAT (k) / 1.7);

or, better yet (to avoid floating point operations)

k := 10 * k / 17

thereafter, at the beginning of the sorting loop, the following k-sequence (in order of use) is obtained:

58, 34, 20, 12, 7, 4, 2, 1.

However, in view of the comments above, there are clearly inefficiencies in this sequence, and some further fine tuning perhaps ought to be done. Again, the student is invited to experiment.

13.4.3 The Combsort

The Shell sort rearranges all the k-lists that it can form with a given k. Another approach to the comparison of elements that are relatively far apart in the list is to sort only the k-list starting at position zero for each k. One might immediately suspect that this approach would require somewhat more steps in the sequence for optimum performance, and this turns out to be the case.

A sequence of sorts of only the k-lists starting at position zero for k decreasing by a constant factor is called a comb sort.

The comb sort was first published by Richard Box and Stephen Lacey in the April 1991 issue of Byte magazine. They found that using a sequence for the gaps that decreased by a shrink factor of 1.3 gave the best results. Trial and error produced the refinement that an eventual gap size of 11, (rather than 9 or 10) gave a more efficient sequence of gaps below that point. Their published sort was based on a modification of a bubble sort routine; here it is presented using an insert technique:

PROCEDURE CombSort (VAR source: ARRAY OF CARDINAL; lBound, uBound : CARDINAL);
                              
VAR
  gap, count, moveCount, temp : CARDINAL;
  
BEGIN
  gap := uBound - lBound;
  IF gap = 0  (* check hostile cases outside loop *)
    (* note: can't hit zero any other way *)	
    THEN
      RETURN (* nothing to sort *)
    ELSIF gap = 1 THEN (* special case sorting two *)
      IF source [lBound] > source [uBound]
        THEN
          Swap (source [lBound], source [uBound])
        END; (* if source *)
      RETURN (* in either case *)
    END; (* if gap *)

  REPEAT
    gap :=  gap * 10 / 13;
    IF (gap = 9) OR (gap = 10)
      THEN
        gap := 11
      END;

    count :=  lBound + gap;
    WHILE count <= uBound 
      DO  
        temp := source [count];   (* set aside unsorted one *)
        moveCount := count;
        WHILE (moveCount >= gap) AND (source [moveCount - gap] > temp)
          DO
            source [moveCount] := source [moveCount - gap];
           (*  move along to make room *)
            DEC (moveCount, gap)   (* count backward in list *)
          END;
         source [moveCount] := temp;   (* insert new item *)
         INC (count, gap);
      END;   (* while count *)     
  UNTIL gap = 1
END CombSort;

The difference between operation of the Shell sort and the comb sort can be illustrated by examining the following table, constructed from successive passes of the two sorts with the array printed out at each pass. The underlined items are the ones compared and sorted.

	The Combsort
Position  0    1    2    3    4    5    6    7    8    9   10   11

Initial 234   77    0  113  404   94  900  113   15  300   13  135

gap=11: 135   77    0  113  404   94  900  113   15  300   13  234
gap= 8:  15   77    0  113  404   94  900  113  135  300   13  234
gap= 6:  15   77    0  113  404   94  900  113  135  300   13  234
gap= 4:  15   77    0  113  135   94  900  113  404  300   13  234
gap= 3:  15   77    0  113  135   94  300  113  404  900   13  234
gap= 2:   0   77   13  113   15   94  135  113  300  900  404  234
gap= 1:   0   13   15   77   94  113  113  135  234  300  404  900


	The Shell sort
Position  0    1    2    3    4    5    6    7    8    9   10   11

Initial 234   77    0  113  404   94  900  113   15  300   13  135

k = 4:   15   77    0  113  234   94  900  113  404  300   13  135
k = 4:   15   77    0  113  234   94  900  113  404  300   13  135
k = 4:   15   77    0  113  234   94   13  113  404  300  900  135
k = 4:   15   77    0  113  234   94   13  113  404  300  900  135
k = 1:    0   13   15   77   94  113  113  135  234  300  404  900

The Shell sort makes more sub-lists and does more comparisons in each one, while the comb sort makes more passes, but only constructs a single sub-list at each pass. Box and Lacey claim that their comb sort achieved the best balance of the two between short times and predictability, as it sorts random lists and previously sorted lists with about the same efficiency. They also claim that actual trials show the comb sort to approximate O(n log2n) as n gets large.


Contents