如题,一些常用的内部排序算法。
所谓内部排序也就是排序之前需要把所有的数据都一次性读入内存,适用于小数据量(不会导致内存条爆炸)的排序。
关于排序算法的复杂度分析自己看了挺久才迷迷糊糊的懂了,数学不好是硬伤啊,各种公式纠结了很久……
话说回来,程序共演示了4 种排序算法,具体请查看相应的函数,对于算法的思想和流程注释里都有详尽的描述:
doQuickSort: 该函数演示了快速排序算法,使用递归完成。
doHeapSort:该函数演示了(大根)堆排序算法,其中使用doHeapSortAdjust 函数来进行(大根)堆调整。
doMergeSort:该函数演示了归并排序算法,对数据递归分割后进行二路归并,其中使用doMerge 函数归并两个有序列表。
doInsertSort:该函数演示了插入排序算法,其中插入位置的确定鉴于该算法让人忧伤的时间复杂度,改用折半查找代替了通常使用的顺序查找,事实证明折半算法的使用能够极大的减少插入排序过程的运行时间(虽然还是很长……)。
下面是一个演示程序的使用方法:
下面是四种排序算法的时间评测:
上图中四种算法测评的输入数据是同一个33.5 MiB 的文本文件,其中包含大约46 万个长度不等的字符串,大约3300 万个字符。
四种排序算法的实际排序时间如下(user + system):
快速排序:1.50 秒
堆排序: 0.82 秒
归并排序:0.34 秒
插入排序:71.55 秒
演示代码如下:
/*
* 文件:sort.c
* By iSpeller
* 编译:gcc -O3 sort.c -o sort
* 使用:./sort --help
*
* 内部排序算法集合,包括:
* 1. 堆排序
* 2. 插入排序
* 3. 快速排序
* 4. 归并排序
*/
#include <stdio.h>
#include <stdlib.h>
#include <stdbool.h>
#include <string.h>
/*
* 调试输出宏:
* DEBUG 宏设置非1 为非调试状态,不执行调试语句;
* DEBUG 宏设置为1 为调试状态,执行调试语句。
*/
#ifdef DEBUG
#undef DEBUG
#endif
#define DEBUG 0
#if 1 == DEBUG
#define DO_DEBUG(s) {s;}
#else
#define DO_DEBUG(s)
#endif
typedef char *string;
typedef enum
/* 文件读入 管道读入 */
{ FILE_MODE, PIPE_MODE, } IOMode;
typedef enum
/* 插入排序 快速排序 归并排序 堆排序 */
{ INSERT_SORT, QUICK_SORT, MERGE_SORT, HEAP_SORT, } SortMode;
/* 让strcmp 函数的行为符合正常思维 */
#define STRCMP(a,r,b) (strcmp((a),(b)) r (0))
/* 待排序和排序之后的若干字符串 */
string *toSort = NULL;
/*
* 从管道或者文件中读入数据
*/
bool
readIn (IOMode mode, string fileInName)
{
unsigned long size = 0, begin = 0, end = 0;
unsigned long stringCount = 0, stringNumber = 0;
FILE *fileIn = NULL;
string in = NULL, inTemp = NULL;
/*
* 设置数据源数据
*/
if (PIPE_MODE == mode)
{
/* 数据源为管道 */
fileIn = stdin;
}
else if (FILE_MODE == mode)
{
/* 数据源为文件 */
if (!(fileIn = fopen (fileInName, "r")))
{
perror ("fopen ():");
return false;
}
}
else
{
/* 不能解析的参数 */
fprintf (stderr, "Unkown arg in readIn (), exit!\n");
exit (EXIT_FAILURE);
}
DO_DEBUG (printf ("输入设置完成。\n"));
/*
* 开始读入数据到inTemp
* 每次读入1<<16 字节
* 防止读入串的字数过长导致溢出或者不全
* 其后会对其进行处理并分割出字符串
*/
if (fileIn)
{
size = 1 << 16;
begin = 0;
in = (string) malloc (size);
size = fread (in, sizeof (char), size, fileIn);
end = size;
inTemp = (string) malloc (size);
while (0 < size)
{
memcpy (&inTemp[begin], in, size);
begin += size;
size = fread (in, sizeof (char), size, fileIn);
end += size;
inTemp = (string) realloc (inTemp, end);
}
}
/*
* 文件指针设置失败则报错并退出
*/
else
{
fprintf (stderr, "Unset FILE pointer in readIn (), exit!\n");
return false;
}
DO_DEBUG (printf ("读入数据完成\n"));
/*
* 根据换行符分割字符串到toSort
* toSort 数组中每一个指针指向一个分割后的字符串
* inTemp 在过程完成之后销毁
*/
size = end;
begin = 0;
stringCount = 0;
stringNumber = 1 << 10;
toSort = (string *) malloc (stringNumber * sizeof (string *));
/* 默认把指针设置为空以标记结尾 */
memset (&toSort[stringCount], 0, stringNumber - stringCount);
for (end = 0; end < size; ++end)
{
/* 换行符作为分割的关键字 */
if ('\n' == inTemp[end])
{
/* 在需要的时候按照倍数扩充指针数组 */
if (stringNumber == stringCount)
{
stringNumber *= 2;
toSort = (string *) realloc (toSort,
stringNumber * sizeof (string *));
/* 指针默认设置为空以标记结尾 */
memset (&toSort[stringCount], 0, stringNumber - stringCount);
}
/* 2: '\n'+'\0' */
toSort[stringCount] = (string) malloc (end - begin + 2);
memcpy (toSort[stringCount], &inTemp[begin], end - begin + 1);
/* 随后加上结尾 */
toSort[stringCount][end - begin + 1] = '\0';
begin = end + 1;
++stringCount;
}
}
/*
* 如果toSort 分配的空间恰好被全部用光
* 再其后设置一个NULL 指针用来标记结束
*/
if (stringCount == stringNumber)
{
toSort = (string *) realloc (toSort,
(stringNumber + 1) * sizeof (string *));
toSort[stringNumber] = 0;
}
DO_DEBUG (printf ("分割数据完成。\n"));
DO_DEBUG (printf ("分割后的字符串个数:%ld\n循环遍历的字符个数:%ld\n",
stringCount, end));
/*
* 如果FILE_MODE == ioMode 则关闭打开的文件
*/
if (stdin != fileIn)
{
fclose (fileIn);
}
free (in);
free (inTemp);
return true;
}
/*
* 快速排序,时间复杂度为O(nlogn),不稳定排序
* 参数为有符号型
* 算法思想:
* 不断的重复一个递归过程,使得列表为有序
* 递归过程为:
* 将第0 个位置的元素设为key,定位和交换数据
* 使得key 左边的元素均不大于key
* 使得key 右边的元素均不小于key
*/
bool
doQuickSort (long begin, long length)
{
long left = 0, right = 0;
string key = NULL;
/*
* 每次将第一个元素作为关键字来分割数组
* 使得左边的元素都小于关键字
* 右边的元素都大于关键字
*/
left = begin;
right = length - 1;
key = toSort[begin]; /* 排序范围内的第一个元素作为Key */
/*
* 循环交换数据使得小者在左大者在右
* 其中交换数据利用一次递归中长度的第一个位置进行
*/
while (left < right)
{
/* 定位到右边小于Key 的位置 */
while ((left < right) && STRCMP (toSort[right], >, key))
{
--right;
}
/* 将这个位置的元素换到左边已保存数据的位置 */
if (left < right)
{
toSort[left] = toSort[right];
++left;
}
/* 定位到左边大于key 的位置 */
while ((left < right) && STRCMP (toSort[left], <, key))
{
++left;
}
/* 将这个位置的元素换到右边已保存数据的位置 */
if (left < right)
{
toSort[right] = toSort[left];
--right;
}
}
/*
* 将key 插入到元素数组当中left 的位置(循环结束后left <= right)
* 此时元素数组被分割为两部分
* key 左边的元素均不大于key,key 右边的元素均不小于key
*/
toSort[left] = key;
DO_DEBUG (printf ("key 被设定到位置:%5ld\n", right));
/*
* 递归整个过程,不断分割元素数组
* 使得每次处理的长度中均用key 把元素数组分割为两部分
* 直到递归到足够小的规模,该递归的输入序列就已经是有序的
* 递归结束后整个序列也变为有序的
*/
if (right - 1 > begin)
{
doQuickSort (begin, right);
}
if (length - 1 > right + 1)
{
doQuickSort (right + 1, length);
}
return true;
}
/*
* 插入排序,时间复杂度为O(n^2),稳定排序
* 算法思想:
* 把第0 个元素当作只有一个元素的有序表
* 重复一个循环过程,使得列表整体变为有序
* 该循环过程为:
* 每次将有序表其后的一个元素插入有序表中合适的位置
* 使得有序表的长度加一
*/
bool
doInsertSort (unsigned long length)
{
long left = 0, right = 0, middle = 0;
int compairResult = 0;
long count = 0, count2 = 0;
string key = NULL;
/*
* 从1 开始逐渐增加有序表的长度
* 直到有序表的长度和待排序的元素数量相同
* key 为待决定插入位置的元素
*/
for (count = 1; count < length; ++count)
{
key = toSort[count];
left = 0; /* 有序表头 */
right = count - 1; /* 有序表尾 */
/*
* 二分法快速寻找合适的插入点
* 当key 和middle 位置的元素不相等时循环查找
* 直到找到一个相等或者合适的位置
*/
while (left < right)
{
middle = (left + right) / 2;
compairResult = strcmp (key, toSort[middle]);
/* key 小于middle 位置的元素,重新设定查找范围的右侧 */
if (compairResult < 0)
{
right = middle - 1;
}
/* 大于重新设置左侧 */
if (compairResult > 0)
{
left = middle + 1;
}
}
/*
* 找到合适的位置后
* 从middle 到count-1 的列表整体后移一个位置
* 之后插入key
*/
for (count2 = count - 1; count2 > left - 1; --count2)
{
toSort[count2 + 1] = toSort[count2];
}
/*
* 插入key 并增加有序表的长度
* 此时left == right + 1
*/
toSort[left] = key;
DO_DEBUG (printf ("第%5ld 个元素被插入到位置%5ld \n", count, middle));
}
return true;
}
/*
* 堆调整函数,用于堆排序
* 调整无序部分为大根堆,使得:
* toSort[n] 不小于 toSort[2 * n]
* toSort[n] 不小于 toSort[2 * n + 1]
* key 为等待调整元素的位置
*/
bool
doHeapSortAdjust (unsigned long key, unsigned long length)
{
long child = 0;
string temp = NULL;
DO_DEBUG (printf ("正在调整的无序表范围:%5ld到 %5ld\n",
key, length - 1));
/*
* 右孩子为2*key+1,如果不是子节点则不用调整
* 不断把孩子中的较大者和父节点交换
* temp 中临时储存父节点,用于其后的子、父节点交换
*/
for (; 2 * key + 1 < length; key = child)
{
/* child 首先定位到左孩子 */
child = 2 * key;
/* 定位child 到孩子中的较大者 */
if ((child < length - 2)
&& (STRCMP (toSort[child], <, toSort[child + 1])))
{
++child;
}
/* 如果子节点大于父节点则交换两个节点 */
if (STRCMP (toSort[key], <, toSort[child]))
{
temp = toSort[key];
toSort[key] = toSort[child];
toSort[child] = temp;
doHeapSortAdjust (key, length);
}
/* 否则跳出循环 */
else
{
break;
}
}
return true;
}
/*
* 堆排序,时间复杂度O(nlogn),不稳定排序
* 算法思想:
* 利用大根堆的堆顶元素为最大的关键字这一特性
* 首先将初始无序表构建成一棵大根堆
* 重复一个过程,直到有序表的长度为原始表长度减一,此时列表全部有序
* 该过程为:
* 将堆顶元素和最后的元素交换位置
* 此时无序表的长度减一,有序表的长度自后往前加一
* 防止交换造成大根堆性质的破坏,重新将无序区构造为大根堆
*/
bool
doHeapSort (unsigned long length)
{
string temp = NULL;
long count = 0;
/*
* length / 2 - 1 为第一个非子叶节点
* 不断把父节点当作key 进行调整以建立大根堆
*/
for (count = length / 2 - 1; count > -1; --count)
{
doHeapSortAdjust (count, length);
}
for (count = length - 1; count > 0; --count)
{
/*
* 交换元素使得有序表的长度加一
*/
temp = toSort[count];
toSort[count] = *toSort;
*toSort = temp;
/* 重新调整无序表为大根堆 */
doHeapSortAdjust (0, count + 1);
}
return true;
}
/*
* 归并函数,把两个有序表合成为一个有序表
* 需要额外的同等大小的内存空间做辅助
* PS:此处额外空间是指针数组的空间
* 而不包括字符串本身占据的空间
*/
bool
doMerge (string * firstPart, unsigned long firstPartLength, string * secondPart,
unsigned long secondPartLength)
{
unsigned long count1 = 0, count2 = 0, count3 = 0;
string *temp = NULL;
temp = (string *) malloc ((firstPartLength + secondPartLength) *
sizeof (string *));
count1 = count2 = count3 = 0;
/*
* 合并两个字串,直到某一个字串达到尾部
*/
while ((count1 < firstPartLength) && (count2 < secondPartLength))
{
/*
* 因为是升序排序,小者先入临时数组
*/
if (STRCMP (firstPart[count1], <, secondPart[count2]))
{
temp[count3] = firstPart[count1];
++count1;
}
else
{
temp[count3] = secondPart[count2];
++count2;
}
/* 定位到下一个移动位置 */
++count3;
}
/*
* 把未处理完的串的剩余部分移动到&temp[count3] 开始的位置
* 注意下面两个while 循环有且只有一个会被执行
*/
while (count1 < firstPartLength)
{
temp[count3] = firstPart[count1];
/* 下一次移动位置定位 */
++count1;
++count3;
}
while (count2 < secondPartLength)
{
temp[count3] = secondPart[count2];
/* 下一次移动位置定位 */
++count2;
++count3;
}
/*
* 把temp 中的数据复制到firstPart
* 注意firstPart 和secondPart 在内存中相邻
* 所以这样复制也就是说把有序数组覆盖了原本的无序数组
*/
memcpy (firstPart, temp,
(firstPartLength + secondPartLength) * sizeof (string));
free (temp);
return true;
}
/*
* 归并排序,时间复杂度O(nlogn),为稳定排序
* 排序思想:
* 先*归*再*并*,不断的递归分割无序表
* 直到分割为单个元素,这时每个子表(只含一个元素)都是有序的
* 对字表进行归并,逐步合成有序表
* 直到合成后有序表的长度等于起初无序表的长度
* 这时整个表变为有序
*/
bool
doMergeSort (string * toSort, unsigned long length)
{
string *firstPart = NULL;
string *secondPart = NULL;
unsigned long firstPartLength = 0;
unsigned long secondPartLength = 0;
/*
* 如果多余一个元素才进行归并,否则返回
*/
if (length > 1)
{
/* 将toSort 分为两个部分 */
firstPart = toSort;
firstPartLength = length / 2;
secondPart = &toSort[length / 2];
secondPartLength = length - firstPartLength;
/* 分别进行递*归*分割 */
doMergeSort (firstPart, firstPartLength);
doMergeSort (secondPart, secondPartLength);
DO_DEBUG (printf ("合并的长度为:%5ld到 %5ld\n",
firstPartLength, length));
/* 然后进行合*并* */
doMerge (firstPart, firstPartLength, secondPart, secondPartLength);
}
return true;
}
/*
* doSort 函数,执行排序动作
* 具体算法会根据sortMode 设置
*/
bool
doSort (SortMode sortMode)
{
unsigned long length = 0;
/*
* 确定待排序的字符串数量
*/
for (length = 0; toSort[length]; ++length);
DO_DEBUG (printf ("待排序的字符串个数:%ld\n", length));
DO_DEBUG (printf ("排序模式:%d\n", sortMode));
/*
* 根据参数执行相应排序过程
*/
switch (sortMode)
{
/* 快速排序 */
case QUICK_SORT:
if (!doQuickSort (0, length))
{
return false;
}
break;
/* 插入排序 */
case INSERT_SORT:
if (!doInsertSort (length))
{
return false;
}
break;
/* 归并排序 */
case MERGE_SORT:
if (!doMergeSort (toSort, length))
{
return false;
}
break;
/* 堆排序 */
case HEAP_SORT:
if (!doHeapSort (length))
{
return false;
}
break;
/* 无法解析的参数 */
default:
fprintf (stderr,
"EE: In doSort (),不能解析的参数,退出!\n");
return false;
}
return true;
}
/*
* printResult 函数打印结果到stdout
*/
void
printResult (void)
{
unsigned long stringCount = 0;
/*
* 在遇到NULL 之前打印所有的字符串
*/
for (stringCount = 0; toSort[stringCount]; ++stringCount)
{
printf ("%s", toSort[stringCount]);
}
return;
}
/*
* 打印帮助
*/
void
printHelp (void)
{
puts ("用法:./sort [参数]");
puts
("\t程序默认从标准输入读入,并接受到文件结束符后执行快速排序并打印结果");
puts ("参数:");
puts ("\t-mode:指定排序模式,其中");
puts ("\t\tis 为插入排序");
puts ("\t\tqs 为快速排序");
puts ("\t\tms 为归并排序");
puts ("\t\ths 为堆排序");
puts ("\t\t未指定或未知的参数默认执行快速排序");
puts ("\t-file:指定从文件中读入数据,后跟文件名\n");
puts ("可能的示例:");
puts
("\t从管道中读入find 的结果对其进行快速排序并输出");
puts ("\t\tfind . | ./sort -mode qs");
puts
("\t读入文件fileIn 的数据并对其进行归并排序后输出结果到文件fileOut");
puts ("\t\t./sort -file fileIn -mode ms > fileOut\n");
puts
("!!注意:这并不是一个实用程序,只是为了演示排序算法,请勿用于任何实际工作!");
puts ("\t\t\t\t\t\t\t—— by iSpeller");
return;
}
/*
* MAIN: Fast is Good ;)
*/
int
main (int argc, char *argv[])
{
unsigned long count = 0;
IOMode ioMode = PIPE_MODE; /* 数据读入模式 */
SortMode sortMode = QUICK_SORT; /* 排序模式 */
string fileInName = (string) malloc (1 << 9);
/*
* 扫描参数列表,默认从stdin 读入数据,使用快速排序
*/
ioMode = PIPE_MODE;
sortMode = QUICK_SORT;
for (count = 1, ioMode = PIPE_MODE; count < argc; ++count)
{
/*
* 检查是否指定从文件读入数据
*/
if (STRCMP (argv[count], ==, "-file"))
{
ioMode = FILE_MODE;
++count;
if (!argv[count])
{
fprintf (stderr, "EE: -file 参数之后未找到文件名.\n");
return 1;
}
strncpy (fileInName, argv[count], strlen (argv[count]));
continue;
}
/*
* 检查排序算法的设置,默认为快速排序
*/
if (STRCMP (argv[count], ==, "-mode"))
{
++count;
/* 未指定排序模式仍然使用默认的快速排序 */
if (!argv[count])
{
sortMode = QUICK_SORT;
continue;
}
/* is 匹配插入排序 */
else if (STRCMP (argv[count], ==, "is"))
{
sortMode = INSERT_SORT;
}
/* ms 匹配归并排序 */
else if (STRCMP (argv[count], ==, "ms"))
{
sortMode = MERGE_SORT;
}
/* hs 匹配堆排序 */
else if (STRCMP (argv[count], ==, "hs"))
{
sortMode = HEAP_SORT;
}
/* qs 和其他任何情况都使用快速排序 */
else
{
sortMode = QUICK_SORT;
}
continue;
}
/*
* 匹配--help 参数,当发现--help 参数时
* 停止一切工作,打印帮助信息并退出程序
*/
if (STRCMP (argv[count], ==, "--help"))
{
printHelp ();
return 0;
}
}
/*
* 读入待排序的数据
*/
if (!readIn (ioMode, fileInName))
{
fprintf (stderr, "EE: In readIn (),读入数据失败,退出!\n");
return 1;
}
/*
* 依照指定的算法执行排序动作
*/
if (!doSort (sortMode))
{
fprintf (stderr, "EE: In doSort (),排序过程失败,退出!\n");
return 1;
}
/*
* 打印排序的结果
*/
printResult ();
free (fileInName);
free (toSort);
return 0;
}