8.5 归并排序 (Merging Sort)
【基本思想】
将两个或两个以上的有序表合并成一个有序表的过程。
将两个有序表合并成一个有序表的过程称为2-路归并,2-路归并最为简单和常用。
假设初始序列含有n个记录,则可看成是n个有序的子序列,每个子序列的长度为1,然后两两归并,得到⌈n/2⌉(n/2向上取整)个长度为2 或1 的有序子序列;再两两归并,……, 如此重复,直至得到一 个长度为n 的有序序列为止。
【算法特点】
(1)是稳定排序。
(2)可用于链式结构, 且不需要附加存储空间,但递归实现时仍需要开辟相应的递归工作栈。
#include <stdio.h>
#include <stdlib.h>
#define MAXSIZE 26
typedef int KeyType;
typedef char InfoType;
typedef struct
{
KeyType key;
InfoType otherinfo;
}RedType;
typedef struct
{
RedType r[MAXSIZE + 1]; //r[O]闲置或用做哨兵单元
int length;
}SqList;
void CreateSqList(SqList& L);
void Merge(RedType R[], RedType T[], int low, int mid, int high);
void MSort(RedType R[], RedType T[], int low, int high);
void MergeSort(SqList& L);
void printSqList(SqList L);
int main()
{
SqList L = { {0},0 };
CreateSqList(L);
MergeSort(L);
printSqList(L);
return 0;
}
void CreateSqList(SqList& L)
{
int i = 0;
printf("请输入顺序表的元素个数:");
scanf_s(" %d", &L.length);
for (i = 1; i <= L.length; i++)
{
printf("请输入第%d个关键字:", i);
scanf_s(" %d", &L.r[i].key);
}
}
//算法8.10 相邻两个有序子序列的归并
//将有序表 R[low...mid]和R[mid+l...high]归并为有序表 T[low...high]
void Merge(RedType R[], RedType T[], int low, int mid, int high)
{
int i = low;
int j = mid + 1;
int k = low;
while (i <= mid && j <= high)
{
if (R[i].key <= R[j].key)
{
T[k++] = R[i++];
}
else
{
T[k++] = R[j++];
}
}
while (i <= mid)
{
T[k++] = R[i++];
}
while (j <= high)
{
T[k++] = R[j++];
}
}
//算法8.11 归并排序
// R[low ... high]归并排序后放人 T[low ... high]中
void MSort(RedType R[], RedType T[], int low, int high)
{
if (low == high)
{
T[low] = R[low];
}
else
{
int mid = (low + high) / 2; //将当前序列一分为二, 求出分裂点 mid
RedType S[MAXSIZE]; //辅助存储空间S
MSort(R, S, low, mid); //对子序列 R[low.. mid]递归归并排序, 结果放入S[low .. mid]
MSort(R,S,mid+1,high); //对子序列 R[mid+1.. high]递归归并排序, 结果放人 S[mid+1. . high]
Merge(S,T,low,mid,high); //将S[low .. mid]和S[mid+1. .high]归并到 T[low .. high]
}
}
//对顺序表 L 做归并排序
void MergeSort(SqList& L)
{
MSort(L.r, L.r, 1, L.length);
}
void printSqList(SqList L)
{
int i = 0;
printf("\n\n排序后的序列为:");
for (i = 1; i <= L.length; i++)
{
printf("\nr[%d].key = %d", i, L.r[i].key);
}
}
8.6 基数排序(分配类)
基本思想:
前述各类排序方法都是建立在关键字比较的基础上,而分配类排序不需要比较关键字的大小,它是根据关键字中各位的值,通过对待排序记录进行若干趟“分配“ 与 ”收集 ” 来实现排序的,是一种借助于多关键字排序的思想对单关键字排序的方法。
8.6.1 多关键字的排序
8.6.2 链式基数排序
【基本思想】
(1) 以静态链表存储待排记录,并令表头指针指向第一个记录;
从最低位关键字(对整个值影响最小的关键字)起:
(2) “分配” 时,按当前“关键字位”所取值,将记录分配到不同的 “链队列” 中,每个队列中记录的 “关键字位” 相同;
(3) “收集”时,按当前关键字位取值从小到大将各队列首尾相链成一个链表;
(4) 从低位到高位,对每个关键字位均重复 2 和 3 两步。
【算法特点】
(1)是稳定排序。
(2)可用于链式结构, 也可用于顺序结构。
(3)时间复杂度可以突破基于关键字比较一类方法的下界 O(nlog2n), 达到 O(n)。
(4)基数排序使用条件有严格的要求:需要知道各级关键字的主次关系和各级关键字的取值范围。
8.6.2.1 当逻辑关键字由数字复合而成时
将代码scanf_s 和 printf中的” %c“ 占位符,都改为” %d“
宏定义中#define RADIX 26 改为 10
创建静态链表的函数CreateSLList 中
//初始化rd数组
for (i = 0; i < RADIX; i++)
{
rd[i] = 0;
}
打印函数中必须加引用符
void printSLList(SLList &L);
个位(优先级最低):影响最小,即使个位数很大,也不会显著改变元素在整个数组中的位置。
十位(优先级中等):影响较大,十位数的变化会开始影响元素的位置。
百位(优先级最高):影响最大,百位数决定了元素的最终位置。
如当关键字是数字,且每个逻辑关键字由三个关键字复合而成时,百位数的地位高于十位数,十位数的地位又高于个位数。
那么最左边的百位数就是最高位数(优先级最高的)关键字,最右边的个位数就是最低位数(优先级最低的)关键字。
#include <stdio.h>
#include <stdlib.h>
#define MAXNUM_KEY 8
#define RADIX 10
#define MAX_SPACE 10000
typedef int KeysType;
typedef char InfoType;
typedef struct
{
KeysType keys[MAXNUM_KEY]; //关键字
InfoType otheritems;
int next;
}SLCell; //静态链表的结点类型
typedef struct
{
SLCell r[MAX_SPACE]; //静态链表的可利用空间,r[O]为头结点
int keynum;
int recnum;
}SLList; //静态链表类型
KeysType rd[RADIX]; //每个关键字的所有可能取值,数组rd的大小即为此基数排序的基为RADIX
typedef int ArrType[RADIX]; //指针数组类型
void CreateSLList(SLList& L);
void Distribute(SLCell r[], int i, int f[], int e[]);
int ord(SLCell r[], int i, int p);
void Collect(SLCell r[], int i, int f[], int e[]);
void RadixSort(SLList& L);
void printSLList(SLList &L);
int main()
{
SLList L = { {0},0,0 };
CreateSLList(L);
RadixSort(L);
printf("\n\n排序后的序列为:");
printSLList(L);
return 0;
}
void CreateSLList(SLList& L)
{
int i = 0;
int j = 0;
printf("请输入静态链表中,逻辑关键字的记录个数:");
scanf_s(" %d", &L.recnum);
printf("请输入每个逻辑关键字的关键字项数:");
scanf_s(" %d", &L.keynum);
//初始化rd数组
for (i = 0; i < RADIX; i++)
{
rd[i] = 0;
}
/* 当关键字是数字时,基数RADIX=10,每个关键字可能取的数字为:0,1,...,9,指针数组rd中每个元素的类型为int
当关键字是字母时,基数RADIX=26,每个关键字可能取的字母为:A,B,...,Z,指针数组rd中每个元素的类型为char
每个 关键字可能取的值 就是一个子表。 */
printf("基数(即每个关键字的最大取值范围):%d", RADIX);
printf("\n\n请按从小到大的顺序,依次输入关键字可取的值: \n");
for (i = 0; i < RADIX; i++)
{
printf("请输入关键字可能取的第%d个值:", i + 1);
scanf_s(" %d", &rd[i]);
}
/*基数排序的思想是低位优先,而在下面代码实现过程中,RadixSort函数是下标从0开始依次往后,正序依次读取keys数组中的数据的。
因此这里存储的时候,必须将按关键字位数从高到低倒着存储,使得最低位关键字在最前面。*/
for (i = 1; i <= L.recnum; i++) //r[O]为头结点,不保存数据
{
printf("\n请输入第%d个逻辑关键字:\n", i);
for (j = L.keynum - 1; j >= 0; j--)
{
printf("请输入第%d个逻辑关键字的第%d个关键字:", i, L.keynum - j);
scanf_s(" %d", &L.r[i].keys[j]);
}
}
}
//算法8.12 基数排序
/* 前提:静态链表 L 的 r域中记录已按 (keys[O], …, keys[i-1]) 有序
【 keys[i]前面的关键字(keys[O], …, keys[i-1]) 位数都比它低,但已经都经过分配和收集,变得有序了。
每个关键字记录中的keys[i],是待分配的最低位关键字。 】
本算法按第i个关键字keys[i]建立 RADIX 个子表,使同一子表中记录的keys[i]相同。 */
void Distribute(SLCell r[], int i, int f[], int e[])
{
//f[O ...RADIX - 1]和 e[O ... RADIX - 1]分别指向各子表中第一个和最后一个记录
int j = 0;
int p = 0;
for (j = 0; j < RADIX; ++j)
{
f[j] = 0; //各子表初始化为空表
e[j] = 0;
}
//p初始值指向静态链表L中第一个结点r[1](序列中的第一个逻辑关键字)
//静态链表L中最后一个结点的后继为0,即指向头结点r[0](头结点中不保存数据)
for (p = r[0].next; p; p = r[p].next)
{
j = ord(r, i, p);
//ord将记录r[p]中第i个关键字映射到[0 .. RADIX-1]
//f[j]指向 数组rd中下标为j的关键字 子表中的第一个记录
if (!f[j])
{
f[j] = p;
//【*】例如keys[i]为8,当在关键字8(在rd数组中的下标也为8)的子表中还没有记录时,使得f[8]指向 该第一个 keys[i]为8记录p
}
else
{
//【*】否则的话,就使得该记录p 成为 指针e[8]的后继结点(指针e[8]指向该子表的最后一个记录)
r[e[j]].next = p;
//如此,就将所有记录中第i个关键字keys[i]相同的记录,都放在该子表keys[i]中
//直接在静态链表L 的r域中进行修改,使得 第i个关键字keys[i]相同的记录 可以通过静态链表L结点的next域被连续获取到
}
//令e[j]重新指向子表keys[i]中的最后一个记录p
e[j] = p;
}
}
//将记录r[p]中第i个关键字映射到[0 .. RADIX-1]
int ord(SLCell r[], int i, int p)
{
KeysType k = r[p].keys[i];
int j = 0;
//寻找k在rd数组中的下标
for (j = 0; j < RADIX; j++)
{
if (rd[j] == k)
{
return j;
}
}
return -1;
}
//本算法按 keys[i]自小至大地将f[O.. RADIX-1]所指各子表依次链接成一个链表
void Collect(SLCell r[], int i, int f[], int e[])
{
//e[O .. RADIX-1]为各子表的尾指针
int j = 0;
int t = 0;
//找第一个非空子表
for (j = 0; j < RADIX && !f[j]; j++)
{
;
}
//使r[O].next指向第一个非空子表中第一个结点
r[0].next = f[j];
t = e[j];
while (j < RADIX)
{
//找下一个非空子表
for (j = j + 1; j <= RADIX - 1 && !f[j]; j++) //注意是两个条件,及其准确性
{
;
}
//链接两个非空子表
if (f[j] && j <= RADIX - 1) //注意是两个条件,及其准确性
{
r[t].next = f[j];
t = e[j];
}
else if (j >= RADIX) //一定要增加推出while循环的语句
{
break;
}
}
//t指向最后一个非空子表中的最后一个结点
r[t].next = 0;
}
//L是采用静态链表表示的顺序表
//对 L 做基数排序,使得 L 成为按关键字自小到大的有序静态链表, L.r[O]为头结点,不存储记录
void RadixSort(SLList& L)
{
int i = 0;
int f[RADIX] = { 0 };
int e[RADIX] = { 0 };
for (i = 0; i < L.recnum; ++i)
{
L.r[i].next = i + 1; //将 L 改造为静态链表
}
L.r[L.recnum].next = 0;
//最后一个记录关键字的后继置为0【静态链表中第一个结点r[0]为头结点,不保存任何数据,因此这里相当于最后一个结点的后继为空】
for (i = 0; i < L.keynum; ++i) //按最低位优先依次对各关键字进行分配和收集
{
Distribute(L.r, i, f, e); //这里的第一个i = 0,而不是L.keynum-1,说明最低为关键字在keys数组中的第一位
Collect(L.r, i, f, e); //没有用到i,仅仅是一个标志,说明讨论的是第i个关键字/进行的是第i趟收集
printf("\n第%d趟分配和收集之后,序列的顺序为:", i + 1);
printSLList(L);
}
}
void printSLList(SLList &L)
{
int i = 1;
int j = 0;
int p = 0;
for (p = L.r[0].next; p ; p = L.r[p].next) //r[O]为头结点,不保存数据
{
printf("\n第%d个逻辑关键字:", i);
for (j = L.keynum - 1; j >= 0; j--)
{
printf("%d", L.r[p].keys[j]);
}
i++;
}
}
8.6.2.2 当逻辑关键字由字母复合而成时
#include <stdio.h>
#include <stdlib.h>
#define MAXNUM_KEY 8
#define RADIX 26
#define MAX_SPACE 10000
typedef char KeysType;
typedef char InfoType;
typedef struct
{
KeysType keys[MAXNUM_KEY]; //关键字
InfoType otheritems;
int next;
}SLCell; //静态链表的结点类型
typedef struct
{
SLCell r[MAX_SPACE]; //静态链表的可利用空间,r[O]为头结点
int keynum;
int recnum;
}SLList; //静态链表类型
KeysType rd[RADIX]; //每个关键字的所有可能取值,数组rd的大小即为此基数排序的基为RADIX
typedef int ArrType[RADIX]; //指针数组类型
void CreateSLList(SLList& L);
void Distribute(SLCell r[], int i, int f[], int e[]);
int ord(SLCell r[], int i, int p);
void Collect(SLCell r[], int i, int f[], int e[]);
void RadixSort(SLList& L);
void printSLList(SLList L);
int main()
{
SLList L = { {0},0,0 };
CreateSLList(L);
RadixSort(L);
printf("\n\n排序后的序列为:");
printSLList(L);
return 0;
}
void CreateSLList(SLList& L)
{
int i = 0;
int j = 0;
printf("请输入静态链表中,逻辑关键字的记录个数:");
scanf_s(" %d", &L.recnum);
printf("请输入每个逻辑关键字的关键字项数:");
scanf_s(" %d", &L.keynum);
//初始化rd数组
for (i = 0; i < RADIX; i++)
{
rd[i] = '\0';
}
/* 当关键字是数字时,基数RADIX=10,每个关键字可能取的数字为:0,1,...,9,指针数组rd中每个元素的类型为int
当关键字是字母时,基数RADIX=26,每个关键字可能取的字母为:A,B,...,Z,指针数组rd中每个元素的类型为char
每个 关键字可能取的值 就是一个子表。 */
printf("基数(即每个关键字的最大取值范围):%d", RADIX);
printf("\n\n请按从小到大的顺序,依次输入关键字可取的值: \n");
for (i = 0; i < RADIX; i++)
{
printf("请输入关键字可能取的第%d个值:", i + 1);
scanf_s(" %c", &rd[i]);
}
/* 基数排序的思想是低位优先,而在下面代码实现过程中,RadixSort函数是下标从0开始依次往后,正序依次读取keys数组中的数据的。
因此这里存储的时候,必须将按关键字位数从高到低倒着存储,使得最低位关键字在最前面。 */
for (i = 1; i <= L.recnum; i++) //r[O]为头结点,不保存数据
{
printf("\n请输入第%d个逻辑关键字:\n", i);
for (j = L.keynum - 1; j >= 0; j--)
{
printf("请输入第%d个逻辑关键字的第%d个关键字:", i, L.keynum - j);
scanf_s(" %c", &L.r[i].keys[j]);
}
}
}
//算法8.12 基数排序
/* 前提:静态链表 L 的 r域中记录已按 (keys[O], …, keys[i-1]) 有序
【 keys[i]前面的关键字(keys[O], …, keys[i-1]) 位数都比它低,但已经都经过分配和收集,变得有序了。
每个关键字记录中的keys[i],是待分配的最低位关键字。 】
本算法按第i个关键字keys[i]建立 RADIX 个子表,使同一子表中记录的keys[i]相同。 */
void Distribute(SLCell r[], int i, int f[], int e[])
{
//f[O ...RADIX - 1]和 e[O ... RADIX - 1]分别指向各子表中第一个和最后一个记录
int j = 0;
int p = 0;
for (j = 0; j < RADIX; ++j)
{
f[j] = 0; //各子表初始化为空表
e[j] = 0;
}
//p初始值指向静态链表L中第一个结点r[1](序列中的第一个逻辑关键字)
//静态链表L中最后一个结点的后继为0,即指向头结点r[0](头结点中不保存数据)
for (p = r[0].next; p; p = r[p].next)
{
j = ord(r, i, p);
//ord将记录r[p]中第i个关键字映射到[0 .. RADIX-1]
//f[j]指向 数组rd中下标为j的关键字 子表中的第一个记录
if (!f[j])
{
f[j] = p;
//【*】例如keys[i]为8,当在关键字8(在rd数组中的下标也为8)的子表中还没有记录时,使得f[8]指向 该第一个 keys[i]为8记录p
}
else
{
//【*】否则的话,就使得该记录p 成为 指针e[8]的后继结点(指针e[8]指向该子表的最后一个记录)
r[e[j]].next = p;
//如此,就将所有记录中第i个关键字keys[i]相同的记录,都放在该子表keys[i]中
//直接在静态链表L 的r域中进行修改,使得 第i个关键字keys[i]相同的记录 可以通过静态链表L结点的next域被连续获取到
}
//令e[j]重新指向子表keys[i]中的最后一个记录p
e[j] = p;
}
}
//将记录r[p]中第i个关键字映射到[0 .. RADIX-1]
int ord(SLCell r[], int i, int p)
{
KeysType k = r[p].keys[i];
int j = 0;
//寻找k在rd数组中的下标
for (j = 0; j < RADIX; j++)
{
if (rd[j] == k)
{
return j;
}
}
return -1;
}
//本算法按 keys[i]自小至大地将f[O.. RADIX-1]所指各子表依次链接成一个链表
void Collect(SLCell r[], int i, int f[], int e[])
{
//e[O .. RADIX-1]为各子表的尾指针
int j = 0;
int t = 0;
//找第一个非空子表
for (j = 0; j < RADIX && !f[j]; j++)
{
;
}
//使r[O].next指向第一个非空子表中第一个结点
r[0].next = f[j];
t = e[j];
while (j < RADIX)
{
//找下一个非空子表
for (j = j + 1; j <= RADIX - 1 && !f[j]; j++) //注意是两个条件,及其准确性
{
;
}
//链接两个非空子表
if (f[j] && j <= RADIX - 1) //注意是两个条件,及其准确性
{
r[t].next = f[j];
t = e[j];
}
else if (j >= RADIX) //一定要增加推出while循环的语句
{
break;
}
}
//t指向最后一个非空子表中的最后一个结点
r[t].next = 0;
}
//L是采用静态链表表示的顺序表
//对 L 做基数排序,使得 L 成为按关键字自小到大的有序静态链表, L.r[O]为头结点,不存储记录
void RadixSort(SLList& L)
{
int i = 0;
int f[RADIX] = { 0 };
int e[RADIX] = { 0 };
for (i = 0; i < L.recnum; ++i)
{
L.r[i].next = i + 1; //将 L 改造为静态链表
}
L.r[L.recnum].next = 0;
//最后一个记录关键字的后继置为0【静态链表中第一个结点r[0]为头结点,不保存任何数据,因此这里相当于最后一个结点的后继为空】
for (i = 0; i < L.keynum; ++i) //按最低位优先依次对各关键字进行分配和收集
{
Distribute(L.r, i, f, e); //这里的第一个i = 0,而不是L.keynum-1,说明最低为关键字在keys数组中的第一位
Collect(L.r, i, f, e); //没有用到i,仅仅是一个标志,说明讨论的是第i个关键字/进行的是第i趟收集
printf("\n第%d趟分配和收集之后,序列的顺序为:",i+1);
printSLList(L);
}
}
void printSLList(SLList L)
{
int i = 1;
int j = 0;
int p = 0;
for (p = L.r[0].next; p; p = L.r[p].next) //r[O]为头结点,不保存数据
{
printf("\n第%d个逻辑关键字:", i);
for (j = L.keynum - 1; j >= 0; j--)
{
printf("%c ", L.r[p].keys[j]);
}
i++;
}
}
8.7 外部排序
基本思想:
外部排序基本上由两个相对独立的阶段组成。
(1)首先, 按可用内存大小, 将外存上含n个记录 的文件分成若干长度为 l 的子文件或段 (segment), 依次读入内存并利用有效的内部排序方法对它们进行排序, 并将排序后得到的有序子文件重新写入外存, 通常称这些有序子文件为归并段或 顺串(run);
(2)然后, 对这些归并段进行逐趟归并, 使归并段(有序的子文件)逐渐由小至大, 直至得到整个有序文件为止。
为减少归并中外存读写的次数,提高外排序的效率,一般通过增大归并路数和减少初始归并段个数两种方案对归并算法进行改进,
8.7.2 多路平衡归并排序的实现—增大归并路数
归并段的个数/路数,即k值的选择并非越大越好, 如何选择合适的k是一个需要综合考虑的问题。
主要是要用 败者树 这种方法,从k个记录中选出最小的,来实现k-路归并。