Sorting and Searching Algorithms
Sorting and Searching Algorithms
Sorting and Searching Algorithms
This is a collection of algorithms for sorting and searching. Descriptions are brief and intuitive,
with just enough theory thrown in to make you nervous. I assume you know a high-level
language, such as C, and that you are familiar with programming concepts including arrays and
pointers.
The first section introduces basic data structures and notation. The next section presents several
sorting algorithms. This is followed by a section on dictionaries, structures that allow efficient
insert, search, and delete operations. The last section describes algorithms that sort data and
implement dictionaries for very large files. Source code for each algorithm, in ANSI C, is
included.
Most algorithms have also been coded in Visual Basic. If you are programming in Visual Basic, I
recommend you read Visual Basic Collections and Hash Tables, for an explanation of hashing
and node representation. A search for Algorithms at amazon.com returned over 18,000 entries.
Here is one of the best.
Insertion Sort
One of the simplest methods to sort an array is an insertion sort. An example of an insertion sort
occurs in everyday life while playing cards. To sort the cards in your hand you extract a card,
shift the remaining cards, and then insert the extracted card in the correct place. This process is
repeated until all the cards are in the correct sequence. Both average and worst-case time is
O(n2). For further reading, consult Knuth [1998].
Theory
Starting near the top of the array in Figure 2-1(a), we extract the 3. Then the above elements are
shifted down until we find the correct place to insert the 3. This process repeats in Figure 2-1(b)
with the next number. Finally, in Figure 2-1(c), we complete the sort by inserting 2 in the correct
place.
Implementation in C
An ANSI-C implementation for insertion sort is included. Typedef T and comparison operator
compGT should be altered to reflect the data stored in the table.
/* insert sort */
#include <stdio.h>
#include <stdlib.h>
typedef int T;
typedef int tblIndex;
return 0;
Shell Sort
Shell sort, developed by Donald L. Shell, is a non-stable in-place sort. Shell sort improves on the
efficiency of insertion sort by quickly shifting values to their destination. Average sort time is
O(n7/6), while worst-case time is O(n4/3). For further reading, consult Knuth [1998].
Theory
In Figure 2-2(a) we have an example of sorting by insertion. First we extract 1, shift 3 and 5
down one slot, and then insert the 1, for a count of 2 shifts. In the next frame, two shifts are
required before we can insert the 2. The process continues until the last frame, where a total of 2
+ 2 + 1 = 5 shifts have been made.
In Figure 2-2(b) an example of shell sort is illustrated. We begin by doing an insertion sort using
aspacingof two. In the first frame we examine numbers 3-1. Extracting 1, we shift 3 down one
slot for a shift count of 1. Next we examine numbers 5-2. We extract 2, shift 5 down, and then
insert 2. After sorting with a spacing of two, a final pass is made with a spacing of one. This is
simply the traditional insertion sort. The total shift count using shell sort is 1+1+1 = 3. By using
an initial spacing larger than one, we were able to quickly shift values to their proper destination.
These calculations result in values (h0,h1,h2,) = (1,5,19,41,109,209,). Calculate h until 3ht >=
N, the number of elements in the array. Then choose ht-1 for a starting value. For example, to sort
150 items, ht = 109 (3109 >= 150), so the first spacing is ht-1, or 41. The second spacing is 19,
then 5, and finally 1.
Implementation in C
An ANSI-C implementation for shell sort is included. Typedef T and comparison operator
compGT should be altered to reflect the data stored in the array. The central portion of the
algorithm is an insertion sort with a spacing of h.
/* shell sort */
#include <stdio.h>
#include <stdlib.h>
typedef int T;
typedef int tblIndex;
*
*
*/
shl 2000
sort 2000 records
maxnum = atoi(argv[1]);
lb = 0; ub = maxnum - 1;
if ((a = malloc(maxnum * sizeof(T))) == 0) {
fprintf (stderr, "insufficient memory (a)\n");
exit(1);
}
fill(a, lb, ub);
shellSort(a, lb, ub);
}
return 0;
Quicksort
Although the shell sort algorithm is significantly better than insertion sort, there is still room for
improvement. One of the most popular sorting algorithms is quicksort. Quicksort executes in
O(n lgn) on average, and O(n2) in the worst-case. However, with proper precautions, worst-case
behavior is very unlikely. Quicksort is a non-stable sort. It is not an in-place sort as stack space is
required. For further reading, consult Cormen [2009].
Theory
The quicksort algorithm works by partitioning the array to be sorted, then recursively sorting
each partition. In Partition (Figure 2-3), one of the array elements is selected as a pivot value.
Values smaller than the pivot value are placed to the left of the pivot, while larger values are
placed to the right.
int function Partition (Array A, int Lb, int Ub);
begin
select a pivot from A[Lb]...A[Ub];
reorder A[Lb]...A[Ub] such that:
all values to the left of the pivot are <= pivot
all values to the right of the pivot are >= pivot
return pivot position;
end;
procedure QuickSort (Array A, int Lb, int Ub);
begin
if Lb < Ub then
M = Partition (A, Lb, Ub);
QuickSort (A, Lb, M - 1);
QuickSort (A, M, Ub);
end;
In Figure 2-4(a), the pivot selected is 3. Indices are run starting at both ends of the array. One
index starts on the left and selects an element that is larger than the pivot, while another index
starts on the right and selects an element that is smaller than the pivot. In this case, numbers 4
and 1 are selected. These elements are then exchanged, as is shown in Figure 2-4(b). This
process repeats until all elements to the left of the pivot <= the pivot, and all elements to the right
of the pivot are >= the pivot. QuickSort recursively sorts the two subarrays, resulting in the array
shown in Figure 2-4(c).
Implementation in C
An ANSI-C implementation of quicksort is included. Typedef T and comparison operator
compGT should be altered to reflect the data stored in the array. Two version of quicksort are
included: quickSort, and quickSortImproved. Enhancements include:
For short arrays, insertSort is called. Due to recursion and other overhead,
quicksort is not an efficient algorithm to use on small arrays. Consequently,
any array with fewer than 50 elements is sorted using an insertion sort.
Cutoff values of 12-200 are appropriate.
Tail recursion occurs when the last statement in a function is a call to the
function itself. Tail recursion may be replaced by iteration, resulting in a
better utilization of stack space.
After an array is partitioned, the smallest partition is sorted first. This results
in a better utilization of stack space, as short partitions are quickly sorted and
dispensed with.
/* quicksort */
#include <stdio.h>
#include <stdlib.h>
typedef int T;
typedef int tblIndex;
x[j+1] = x[j];
/* insert */
x[j+1] = t;
}
tblIndex partition(T *x, tblIndex lb, tblIndex ub) {
/* select a pivot */
double pivot = x[(lb+ub)/2];
/* work from both ends, swapping to keep
*/
/* values less than pivot to the left, and */
/* values greater than pivot to the right */
tblIndex i = lb - 1;
tblIndex j = ub + 1;
while (1) {
T t;
while (compGT(x[--j], pivot));
while (compLT(x[++i], pivot));
if (i >= j) break;
return j;
}
void quickSort(T *x, tblIndex lb, tblIndex ub) {
tblIndex m;
}
void fill(T *a, tblIndex lb, tblIndex ub) {
tblIndex i;
srand(1);
for (i = lb; i <= ub; i++) a[i] = rand();
}
int main(int argc, char *argv[]) {
tblIndex maxnum, lb, ub;
T *a;
/* command-line:
*
*
qui maxnum
*
*
qui 2000
*
sorts 2000 records
*
*/
maxnum = atoi(argv[1]);
lb = 0; ub = maxnum - 1;
if ((a = malloc(maxnum * sizeof(T))) == 0) {
fprintf (stderr, "insufficient memory (a)\n");
exit(1);
}
fill(a, lb, ub);
quickSortImproved(a, lb, ub);
return 0;
}
External Sort
One method for sorting a file is to load the file into memory, sort the data in memory, then write
the results. When the file cannot be loaded into memory due to resource limitations, an external
sort applicable. We will implement an external sort using replacement selection to establish
initial runs, followed by a polyphase merge sort to merge the runs into one sorted file. I highly
recommend you consult Knuth [1998], as many details have been omitted.
Theory
For clarity, I'll assume that data is on one or more reels of magnetic tape. Figure 4-1 illustrates a
3-way polyphase merge. Initially, in phase A, all data is on tapes T1 and T2. Assume that the
beginning of each tape is at the bottom of the frame. There are two sequential runs of data on
T1: 4-8, and 6-7. Tape T2 has one run: 5-9. At phase B, we've merged the first run from tapes
T1 (4-8) and T2 (5-9) into a longer run on tape T3 (4-5-8-9). Phase C is simply renames the
tapes, so we may repeat the merge again. In phase D we repeat the merge, with the final output
on tape T3.
Phase
T1
T2
7
6
8
4
9
5
9
8
5
4
7
6
9
8
5
4
T3
7
6
9
8
7
6
5
4
Several interesting details have been omitted from the previous illustration. For example, how
were the initial runs created? And, did you notice that they merged perfectly, with no extra runs
on any tapes? Before I explain the method used for constructing initial runs, let me digress for a
bit.
In 1202, Leonardo Fibonacci presented the following exercise in his Liber Abbaci (Book of the
Abacus): "How many pairs of rabbits can be produced from a single pair in a year's time?" We
may assume that each pair produces a new pair of offspring every month, each pair becomes
fertile at the age of one month, and that rabbits never die. After one month, there will be 2 pairs
of rabbits; after two months there will be 3; the following month the original pair and the pair
born during the first month will both usher in a new pair, and there will be 5 in all; and so on.
This series, where each number is the sum of the two preceeding numbers, is known as the
Fibonacci sequence:
0, 1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89, ... .
Curiously, the Fibonacci series has found wide-spread application to everything from the
arrangement of flowers on plants to studying the efficiency of Euclid's algorithm. There's even a
Fibonacci Quarterly journal. And, as you might suspect, the Fibonacci series has something to
do with establishing initial runs for external sorts.
Recall that we initially had one run on tape T2, and 2 runs on tape T1. Note that the numbers
{1,2} are two sequential numbers in the Fibonacci series. After our first merge, we had one run
on T1 and one run on T2. Note that the numbers {1,1} are two sequential numbers in the
Fibonacci series, only one notch down. We could predict, in fact, that if we had 13 runs on T2,
and 21 runs on T1 {13,21}, we would be left with 8 runs on T1 and 13 runs on T3 {8,13} after
one pass. Successive passes would result in run counts of {5,8}, {3,5}, {2,3}, {1,1}, and {0,1},
for a total of 7 passes. This arrangement is ideal, and will result in the minimum number of
passes. Should data actually be on tape, this is a big savings, as tapes must be mounted and
rewound for each pass. For more than 2 tapes, higher-order Fibonacci numbers are used.
Initially, all the data is on one tape. The tape is read, and runs are distributed to other tapes in
the system. After the initial runs are created, they are merged as described above. One method
we could use to create initial runs is to read a batch of records into memory, sort the records,
and write them out. This process would continue until we had exhausted the input tape. An
alternative algorithm, replacement selection, allows for longer runs. A buffer is allocated in
memory to act as a holding place for several records. Initially, the buffer is filled. Then, the
following steps are repeated until the input is exhausted:
Select the record with the smallest key that is >= the key of the last record
written.
If all keys are smaller than the key of the last record written, then we have
reached the end of a run. Select the record with the smallest key for the first
record of the next run.
Figure 4-2 illustrates replacement selection for a small file. To keep things simple, I've allocated
a 2-record buffer. Typically, such a buffer would hold thousands of records. We load the buffer
in step B, and write the record with the smallest key (6) in step C. This is replaced with the next
record (key 8). We select the smallest key >= 6 in step D. This is key 7. After writing key 7, we
replace it with key 4. This process repeats until step F, where our last key written was 8, and all
keys are less than 8. At this point, we terminate the run, and start another.
Step
Input
Buffer
Output
5-3-4-8-6-7
5-3-4-8
6-7
5-3-4
8-7
5-3
8-4
6-7
3-4
6-7-8
5-4
6-7-8 | 3
6-7-8 | 3-4
6-7-8 | 3-4-5
Figure 4-2: Replacement Selection
This strategy simply utilizes an intermediate buffer to hold values until the appropriate time for
output. Using random numbers as input, the average length of a run is twice the length of the
buffer. However, if the data is somewhat ordered, runs can be extremely long. Thus, this method
is more effective than doing partial sorts.
When selecting the next output record, we need to find the smallest key >= the last key written.
One way to do this is to scan the entire list, searching for the appropriate key. However, when
the buffer holds thousands of records, execution time becomes prohibitive. An alternative
method is to use a binary tree structure, so that we only compare lgnitems.
Implementation in C
An ANSI-C implementation of an external sort is included. Function makeRuns calls readRec to
read the next record. Function readRec employs the replacement selection algorithm (utilizing a
binary tree) to fetch the next record, and makeRuns distributes the records in a Fibonacci
distribution. If the number of runs is not a perfect Fibonacci number, dummy runs are simulated
at the beginning of each file. Function mergeSort is then called to do a polyphase merge sort on
the runs.
/* external sort */
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
/****************************
* implementation dependent *
****************************/
/* template for workfiles (8.3 format) */
#define FNAME "_sort%03d.dat"
#define LNAME 13
/* comparison operators */
#define compLT(x,y) (x < y)
#define compGT(x,y) (x > y)
/* define the record to be sorted here */
#define LRECL 100
typedef int keyType;
typedef struct recTypeTag {
keyType key;
#if LRECL
char data[LRECL-sizeof(keyType)];
#endif
} recType;
/******************************
* implementation independent *
******************************/
typedef enum {false, true} bool;
typedef struct tmpFileTag {
FILE *fp;
char name[LNAME];
recType rec;
int dummy;
bool eof;
bool eor;
bool valid;
int fib;
} tmpFileType;
/*
/*
/*
/*
/*
/*
/*
/*
file pointer */
filename */
last record read */
number of dummy runs */
end-of-file flag */
end-of-run flag */
true if rec is valid */
ideal fibonacci number */
static
static
static
static
/*
/*
/*
/*
tmpFileType **file;
int nTmpFiles;
char *ifName;
char *ofName;
/* level of runs */
/* number of nodes for selection tree */
void deleteTmpFiles(void) {
int i;
/* delete merge files and free resources */
if (file) {
for (i = 0; i < nTmpFiles; i++) {
if (file[i]) {
if (file[i]->fp) fclose(file[i]->fp);
if (*file[i]->name) remove(file[i]->name);
free (file[i]);
}
}
free (file);
}
}
void termTmpFiles(int rc) {
/* cleanup files */
remove(ofName);
if (rc == 0) {
int fileT;
/* file[T] contains results */
fileT = nTmpFiles - 1;
fclose(file[fileT]->fp); file[fileT]->fp = NULL;
if (rename(file[fileT]->name, ofName)) {
perror("io1");
deleteTmpFiles();
exit(1);
}
*file[fileT]->name = 0;
}
deleteTmpFiles();
}
void cleanExit(int rc) {
}
void initTmpFiles(void) {
int i;
tmpFileType *fileInfo;
/* initialize merge files */
if (nTmpFiles < 3) nTmpFiles = 3;
file = safeMalloc(nTmpFiles * sizeof(tmpFileType*));
fileInfo = safeMalloc(nTmpFiles * sizeof(tmpFileType));
for (i = 0; i < nTmpFiles; i++) {
file[i] = fileInfo + i;
sprintf(file[i]->name, FNAME, i);
if ((file[i]->fp = fopen(file[i]->name, "w+b")) == NULL) {
perror("io2");
cleanExit(1);
}
}
}
recType *readRec(void) {
typedef struct iNodeTag {
/* internal node */
struct iNodeTag *parent;/* parent of internal node */
struct eNodeTag *loser; /* external loser */
} iNodeType;
typedef struct eNodeTag {
/*
struct iNodeTag *parent;/*
recType rec;
/*
int run;
/*
bool valid;
/*
} eNodeType;
external node */
parent of external node */
input record */
run number */
input record is valid */
/* internal node */
/* external node */
/*
/*
/*
/*
/*
/*
/*
/*
/*
while (1) {
/* replace previous winner with new record */
if (!eof) {
if (fread(&win->rec, sizeof(recType), 1, ifp) == 1) {
if ((!lastKeyValid || compLT(win->rec.key, lastKey))
&& (++win->run > maxRun))
maxRun = win->run;
win->valid = true;
} else if (feof(ifp)) {
fclose(ifp);
eof = true;
win->valid = false;
win->run = maxRun + 1;
} else {
perror("io4");
cleanExit(1);
}
} else {
win->valid = false;
win->run = maxRun + 1;
}
/* adjust loser and winner pointers */
p = win->parent;
do {
bool swap;
swap = false;
if (p->loser->run < win->run) {
swap = true;
} else if (p->loser->run == win->run) {
if (p->loser->valid && win->valid) {
if (compLT(p->loser->rec.key, win->rec.key))
swap = true;
} else {
swap = true;
}
}
if (swap) {
/* p should be winner */
eNodeType *t;
t = p->loser;
p->loser = win;
win = t;
}
p = p->parent;
} while (p != &node[0].i);
/* end of run? */
if (win->run != curRun) {
/* win->run = curRun + 1 */
if (win->run > maxRun) {
/* end of output */
free(node);
return NULL;
}
curRun = win->run;
}
}
void makeRuns(void) {
recType *win;
int fileT;
int fileP;
int j;
/*
/*
/*
/*
winner */
last file */
next to last file */
selects file[j] */
while (win) {
bool anyrun;
anyrun = false;
for (j = 0; win && j <= fileP; j++) {
bool run;
run = false;
if (file[j]->valid) {
if (!compLT(win->key, file[j]->rec.key)) {
/* append to an existing run */
run = true;
} else if (file[j]->dummy) {
/* start a new run */
file[j]->dummy--;
run = true;
}
} else {
/* first run in file */
file[j]->dummy--;
run = true;
}
if (run) {
anyrun = true;
/* flush run */
while(1) {
if (fwrite(win, sizeof(recType), 1, file[j]->fp) != 1) {
perror("io3");
cleanExit(1);
}
file[j]->rec.key = win->key;
file[j]->valid = true;
if ((win = readRec()) == NULL) break;
if (compLT(win->key, file[j]->rec.key)) break;
}
}
/* if no room for runs, up a level */
if (!anyrun) {
int t;
level++;
t = file[0]->fib;
for (j = 0; j <= fileP; j++) {
file[j]->dummy = t + file[j+1]->fib - file[j]->fib;
file[j]->fib = t + file[j+1]->fib;
}
}
}
void rewindFile(int j) {
/* rewind file[j] and read in first record */
file[j]->eor = false;
file[j]->eof = false;
rewind(file[j]->fp);
if (fread(&file[j]->rec, sizeof(recType), 1, file[j]->fp) != 1) {
if (feof(file[j]->fp)) {
file[j]->eor = true;
file[j]->eof = true;
} else {
perror("io5");
cleanExit(1);
}
}
void mergeSort(void) {
int fileT;
int fileP;
int j;
tmpFileType *tfile;
/* polyphase merge sort */
fileT = nTmpFiles - 1;
fileP = fileT - 1;
/* prime the files */
for (j = 0; j < fileT; j++) {
rewindFile(j);
}
/* each pass through loop merges one run */
while (level) {
while(1) {
bool allDummies;
bool anyRuns;
/* scan for runs */
allDummies = true;
anyRuns = false;
for (j = 0; j <= fileP; j++) {
if (!file[j]->dummy) {
allDummies = false;
if (!file[j]->eof) anyRuns = true;
}
}
if (anyRuns) {
int k;
keyType lastKey;
/* merge 1 run file[0]..file[P] --> file[T] */
while(1) {
/* each pass thru loop writes 1 record to file[fileT] */
/* find smallest key */
k = -1;
for (j
if
if
if
(k
>rec.key)))
k = j;
}
if (k < 0) break;
/* write record[k] to file[fileT] */
if (fwrite(&file[k]->rec, sizeof(recType), 1,
file[fileT]->fp) != 1) {
perror("io6");
cleanExit(1);
}
/* replace record[k] */
lastKey = file[k]->rec.key;
if (fread(&file[k]->rec, sizeof(recType), 1,
file[k]->fp) == 1) {
/* check for end of run on file[s] */
if (compLT(file[k]->rec.key, lastKey))
file[k]->eor = true;
} else if (feof(file[k]->fp)) {
file[k]->eof = true;
file[k]->eor = true;
} else {
perror("io7");
cleanExit(1);
}
/* fixup dummies */
for (j = 0; j <= fileP; j++) {
if (file[j]->dummy) file[j]->dummy--;
if (!file[j]->eof) file[j]->eor = false;
}
} else if (allDummies) {
for (j = 0; j <= fileP; j++)
file[j]->dummy--;
file[fileT]->dummy++;
}
/* end of run */
if (file[fileP]->eof && !file[fileP]->dummy) {
/* completed a fibonocci-level */
level--;
if (!level) {
/* we're done, file[fileT] contains data */
return;
}
/* fileP is exhausted, reopen as new */
fclose(file[fileP]->fp);
}
}
void extSort(void) {
initTmpFiles();
makeRuns();
mergeSort();
termTmpFiles(0);
}
int main(int argc, char *argv[]) {
/* command-line:
*
*
ext ifName ofName nTmpFiles nNodes
*
*
ext in.dat out.dat 5 2000
*
reads in.dat, sorts using 5 files and 2000 nodes, output to
out.dat
*/
if (argc != 5) {
printf("%s ifName ofName nTmpFiles nNodes\n", argv[0]);
cleanExit(1);
}
ifName = argv[1];
ofName = argv[2];
nTmpFiles = atoi(argv[3]);
nNodes = atoi(argv[4]);
printf("extSort: nFiles=%d, nNodes=%d, lrecl=%d\n",
nTmpFiles, nNodes, sizeof(recType));
extSort();
return 0;
In the introduction we used the binary search algorithm to find data stored in an array. This
method is very effective, as each iteration reduced the number of items to search by one-half.
However, since data was stored in an array, insertions and deletions were not efficient. Binary
search trees store data in nodes that are linked in a tree-like fashion. For randomly inserted data,
search time is O(lgn). Worst-case behavior occurs when ordered data is inserted. In this case the
search time is O(n). See Cormen [2001] for a more detailed description.
Theory
A binary search tree is a tree where each node has a left and right child. Either child, or both
children, may be missing. Figure 3-2 illustrates a binary search tree. Assuming krepresents the
value of a given node, then a binary search tree also has the following property: all children to
the left of the node have values smaller than k, and all children to the right of the node have
values larger than k. The top of a tree is known as the root, and the exposed nodes at the bottom
are known as leaves. In Figure 3-2, the root is node 20 and the leaves are nodes 4, 16, 37, and
43. The height of a tree is the length of the longest path from root to leaf. For this example the
tree height is 2.
Implementation in C
An ANSI-C implementation for a binary search tree is included. Typedefs recType, keyType,
and comparison operators compLT and compEQ should be altered to reflect the data stored in the
tree. Each Node consists of left, right, and parent pointers designating each child and the
parent. The tree is based atroot, and is initially NULL. Function insert allocates a new node
and inserts it in the tree. Function delete deletes and frees a node from the tree. Function find
searches the tree for a particular value.
/* binary search tree */
#include <stdio.h>
#include <stdlib.h>
#define compLT(a,b) (a < b)
#define compEQ(a,b) (a == b)
/* implementation dependent declarations */
typedef enum {
STATUS_OK,
STATUS_MEM_EXHAUSTED,
STATUS_DUPLICATE_KEY,
STATUS_KEY_NOT_FOUND
} statusEnum;
typedef int keyType;
/* user data stored in tree */
typedef struct {
int stuff;
} recType;
/* type of key */
/*
/*
/*
/*
/*
left child */
right child */
parent */
key used for searching */
user data */
return STATUS_OK;
Merge Sort
Merge sort is based on the divide-and-conquer paradigm. Its worst-case running time has a
lower order of growth than insertion sort. Since we are dealing with subproblems, we state each
subproblem as sorting a subarray A[p .. r]. Initially, p = 1 and r = n, but these values change as
we recurse through subproblems.
To sort A[p .. r]:
1. Divide Step
If a given array A has zero or one element, simply return; it is already sorted. Otherwise, split
A[p .. r] into two subarrays A[p .. q] and A[q + 1 .. r], each containing about half of the elements
of A[p .. r]. That is, q is the halfway point of A[p .. r].
2. Conquer Step
Conquer by recursively sorting the two subarrays A[p .. q] and A[q + 1 .. r].
3. Combine Step
Combine the elements back in A[p .. r] by merging the two sorted subarrays A[p .. q] and A[q +
1 .. r] into a sorted sequence. To accomplish this step, we will define a procedure MERGE (A, p,
q, r).
Note that the recursion bottoms out when the subarray has just one element, so that it is trivially
sorted.
IF p < r
THEN q = FLOOR[(p + r)/2]
MERGE (A, p, q)
MERGE (A, q + 1, r)
MERGE (A, p, q, r)
Merging
What remains is the MERGE procedure. The following is the input and output of the MERGE
procedure.
INPUT: Array A and indices p, q, r such that p q r and subarray A[p .. q] is sorted and
subarray A[q + 1 .. r] is sorted. By restrictions on p, q, r, neither subarray is empty.
OUTPUT: The two subarrays are merged into a single sorted subarray in A[p .. r].
We implement it so that it takes (n) time, where n = r p + 1, which is the number of elements
being merged.
Once one input pile empties, just take the remaining input pile and place it face-down
onto the output pile.
Each basic step should take constant time, since we check just the two top cards. There are at
most n basic steps, since each basic step removes one card from the input piles, and we started
with n cards in the input piles. Therefore, this procedure should take (n) time.
Now the question is do we actually need to check whether a pile is empty before each basic
step?
The answer is no, we do not. Put on the bottom of each input pile a special sentinel card. It
contains a special value that we use to simplify the code. We use , since that's guaranteed to
lose to any other value. The only way that cannot lose is when both piles have exposed as
their top cards. But when that happens, all the nonsentinel cards have already been placed into
the output pile. We know in advance that there are exactly r p + 1 nonsentinel cards so stop
once we have performed r p + 1 basic steps. Never a need to check for sentinels, since they
will always lose. Rather than even counting basic steps, just fill up the output array from index
p up through and including index r .
The pseudocode of the MERGE procedure is as follow:
MERGE (A, p, q, r )
1.
n1 q p + 1
2.
n2 r q
3.
Create arrays L[1 . . n1 + 1] and R[1 . . n2 + 1]
4.
FOR i 1 TO n1
5.
DO L[i] A[p + i 1]
6.
FOR j 1 TO n2
7.
DO R[j] A[q + j ]
8.
L[n1 + 1]
9.
R[n2 + 1]
10. i 1
11. j 1
12. FOR k p TO r
13.
DO IF L[i ] R[ j]
14.
THEN A[k] L[i]
15.
ii+1
16.
ELSE A[k] R[j]
17.
jj+1
Example [from CLRS-Figure 2.3]: A call of MERGE(A, 9, 12, 16). Read the following figure
row by row. That is how we have done in the class.
The first part shows the arrays at the start of the "for k p to r" loop, where A[p . . q] is
copied into L[1 . . n1] and A[q + 1 . . r ] is
copied into R[1 . . n2].
Entries in A with slashes have had their values copied to either L or R and have not had a
value copied back in yet. Entries in L and R with slashes have been copied back into A.
The last part shows that the subarrays are merged back into A[p . . r], which is now
sorted, and that only the sentinels () are exposed in the arrays L and R.]
Running Time
The first two for loops (that is, the loop in line 4 and the loop in line 6) take (n1 + n2) = (n)
time. The last for loop (that is, the loop in line 12) makes n iterations, each taking constant time,
Divide: Just compute q as the average of p and r, which takes constant time i.e. (1).
Summed together they give a function that is linear in n, which is (n). Therefore, the
recurrence for merge sort running time is
Recursion Tree
We can understand how to solve the merge-sort recurrence without the master theorem. There is
a drawing of recursion tree on page 35 in CLRS, which shows successive expansions of the
recurrence.
The following figure (Figure 2.5b in CLRS) shows that for the original problem, we have a cost
of cn, plus the two subproblems, each costing T (n/2).
The following figure (Figure 2.5c in CLRS) shows that for each of the size-n/2 subproblems, we
have a cost of cn/2, plus two subproblems, each costing T (n/4).
The following figure (Figure: 2.5d in CLRS) tells to continue expanding until the problem sizes
get down to 1.
The next level down has 2 subproblems, each contributing cost cn/2.
Each time we go down one level, the number of subproblems doubles but the cost per
subproblem halves. Therefore, cost per level stays the same.
Mathematical Induction
Implementation
void mergeSort(int numbers[], int temp[], int array_size)
{
m_sort(numbers, temp, 0, array_size - 1);
}
void merge(int numbers[], int temp[], int left, int mid, int right)
{
int i, left_end, num_elements, tmp_pos;
left_end = mid - 1;
tmp_pos = left;
num_elements = right - left + 1;
tmp_pos = tmp_pos + 1;
left = left +1;
}
else
{
temp[tmp_pos] = numbers[mid];
tmp_pos = tmp_pos + 1;
mid = mid + 1;
}
}
mid = mid + 1;
tmp_pos = tmp_pos + 1;
}
Merge sort
From Wikipedia, the free encyclopedia
Jump to: navigation, search
Merge sort
O(n) natural
variant
O(n log n)
O(n) auxiliary
Merge sort (also commonly spelled mergesort) is an O(n log n) comparison-based sorting
algorithm. Most implementations produce a stable sort, which means that the implementation
preserves the input order of equal elements in the sorted output. Merge sort is a divide and
conquer algorithm that was invented by John von Neumann in 1945.[1] A detailed description
and analysis of bottom-up mergesort appeared in a report by Goldstine and Neumann as early as
1948.[2]
Merge Sorting
The fourth class of sorting algorithm we consider comprises algorithms that sort by merging .
Merging is the combination of two or more sorted sequences into a single sorted sequence.
Figure illustrates the basic, two-way merge operation. In a two-way merge, two sorted
sequences are merged into one. Clearly, two sorted sequences each of length n can be merged
into a sorted sequence of length 2n in O(2n)=O(n) steps. However in order to do this, we need
space in which to store the result. I.e., it is not possible to merge the two sequences in place in
O(n) steps.
and
Program
2. Repeat the process using n bins, placing ai into bin floor(ai/n), being careful
to append to the end of each bin.
This results in a sorted list.
n is 10 and the numbers all lie in (0,99). After the first phase, we will have:
Bin
Content
1
81
2
-
3
-
64
4
25
36
16
9
49
Note that in this phase, we placed each item in a bin indexed by the least significant decimal
digit.
Repeating the process, will produce:
Bin
16
25
36
49
64
81
Content
0
1
4
9
In this second phase, we used the leading decimal digit to allocate items to bins, being careful to
add each item to the end of the bin.
We can apply this process to numbers of any size expressed to any suitable base or radix.
7.5.1 Generalised Radix Sorting
We can further observe that it's not necessary to use the same radix in each phase,
suppose that the sorting key is a sequence of fields, each with bounded ranges, eg
the key is a date using the structure:
typedef
int
int
int
} date;
struct t_date {
day;
month;
year;
If the ranges for day and month are limited in the obvious way, and the range for
year is suitably constrained, eg 1900 < year <= 2000, then we can apply the same
procedure except that we'll employ a different number of bins in each phase. In all
cases, we'll sort first using the least significant "digit" (where "digit" here means a
field with a limited range), then using the next significant "digit", placing each item
after all the items already in the bin, and so on.
Assume that the key of the item to be sorted has k fields, fi|i=0..k-1, and that each fi has si
discrete values, then a generalised radix sort procedure can be written:
radixsort( A, n ) {
for(i=0;i<k;i++) {
for(j=0;j<si;j++) bin[j] = EMPTY;
O(si)
for(j=0;j<n;j++) {
move Ai
to the end of bin[Ai->fi]
}
O(n)
for(j=0;j<si;j++)
concatenate bin[j] onto the end of A;
}
O(si)
Total
Now if, for example, the keys are integers in (0,bk-1), for some constant k, then the keys can be
viewed as k-digit base-b integers.
Thus, si = b for all i and the time complexity becomes O(n+kb) or O(n). This result depends on
k being constant.
If k is allowed to increase with n, then we have a different picture. For example, it takes log2n
binary digits to represent an integer <n. If the key length were allowed to increase with n, so that
k = logn, then we would have:
.
Another way of looking at this is to note that if the range of the key is restricted to (0,bk-1), then
we will be able to use the radixsort approach effectively if we allow duplicate keys when n>bk.
However, if we need to have unique keys, then k must increase to at least logbn. Thus, as n
increases, we need to have logn phases, each taking O(n) time, and the radix sort is the same as
quick sort!
Sample code
This sample code sorts arrays of integers on various radices: the number of bits
used for each radix can be set with the call to SetRadices. The Bins class is used in
each phase to collect the items as they are sorted. ConsBins is called to set up a set
of bins: each bin must be large enough to accommodate the whole array, so
RadixSort can be very expensive in its memory usage!