数据结构 第八章(排序算法)【下】

写在前面:

  1. 本系列笔记主要以《数据结构(C语言版)》为参考(本章部分图片来源于王道),结合下方视频教程对数据结构的相关知识点进行梳理。所有代码块使用的都是C语言,如有错误欢迎指出。
  2. 视频链接:第01周a--前言_哔哩哔哩_bilibili
  3. 基数排序部分的代码参考了一位小伙伴分享的代码,特此说明一下,其它C代码均由笔者根据书上的类C代码进行编写。

五、归并排序

(1)归并排序就是将两个或两个以上的有序表合并成一个有序表的过程。将两个有序表合并成一个有序表的过程称为2-路归并,2-路归并最为简单和常用。下面以2-路归并为例,介绍归并排序算法。

(2)归并排序算法的思想是:假设初始序列含有n个记录,则可将其看成n个有序的子序列,每个子序列的长度为1,然后两两归并,得到\left \lceil n/2 \right \rceil个长度为2或1的有序子序列,再两两归并,如此重复,直至得到一个长度为n的有序序列为止

(3)算法实现:

①相邻两个有序子序列归并:

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)    //将R中的记录由小到大地并入T中
	{
		if (R[i].key <= R[j].key)
			T[k++] = R[i++];
		else
			T[k++] = R[j++];
	}
	while (i <= mid)
		T[k++] = R[i++];    //把R中剩余的元素复制到T中
	while (j <= high)
		T[k++] = R[j++];    //把R中剩余的元素复制到T中
}

②核心部分:

void MSort(RedType R[], RedType T[], int low, int high)
{
	if (low == high)
		T[low] = R[low];
	else
	{
		RedType S[MAXSIZE + 1];
		int mid = (low + high) / 2;
		MSort(R, S, low, mid);
		MSort(R, S, mid + 1, high);
		Merge(S, T, low, mid, high);
	}
}
void MergeSort(SqList* L)    //2-路归并排序
{
	MSort(L->r, L->r, 1, L->length);
}

(4)该算法的时间复杂度为O(nlog_{2}n),空间复杂度为O(n)。

(5)该算法能实现稳定排序,可用于链式结构。

六、基数排序

1、概述

(1)分配类排序不需要比较关键字的大小,它是根据关键字中各位的值,通过对待排序记录进行若干趟“分配”与“收集”来实现排序的,是一种借助于多关键字排序的思想对单关键字进行排序的方法。

(2)假设记录的逻辑关键字由d个关键字组成,每个关键字可能取rd个值,只要从最低数位关键字起,按关键字的不同值将序列中记录分配到rd个队列中后再收集,如此重复d次完成排序,按这种方法实现排序称之为基数排序,其中“基”指的是rd的取值范围。基数排序是典型的分配类排序,又叫桶排序或箱排序。

2、链式基数排序

(1)举例:首先以链表存储n个待排记录,并令表头指针指向第一个记录,如下图所示,然后通过以下3趟分配和收集操作来完成排序。

①第一趟分配对最低数位关键字(个位数)进行,改变记录的指针值将链表中的记录分配至10个链队列中,每个队列中记录的关键字的个位数相等。

②第一趟收集是改变所有非空队列的队尾记录的指针域,令其指向下一个非空队列的队头记录,重新将10个队列中的记录链成一个链表。

③第二趟分配对次低数位关键字(十位数)进行,改变记录的指针值将链表中的记录分配至10个链队列中,每个队列中记录的关键字的十位数相等。

④第二趟收集是改变所有非空队列的队尾记录的指针域,令其指向下一个非空队列的队头记录,重新将10个队列中的记录链成一个链表。

⑤第三趟分配对最高数位关键字(百位数)进行,改变记录的指针值将链表中的记录分配至10个链队列中,每个队列中记录的关键字的百位数相等。

⑥第三趟收集是改变所有非空队列的队尾记录的指针域,令其指向下一个非空队列的队头记录,重新将10个队列中的记录链成一个链表。

(2)基数排序算法的说明如下:

(3)基数排序算法的实现(以对3位数排序为例):

①静态链表的定义:

typedef int InfoType;
typedef int KeyType;

#define MAXBIT 3  //排序的关键字为3位
#define RADIX 10  //对10进制数进行排序
#define MAX_SPACE 100   //最多可以有99个待排序元素,因为有一个头结点
struct SLCell   //元素类型
{ 
	KeyType keys[MAXBIT]; //关键字,存储个位、十位、百位
	InfoType other;       //其它信息
	int next;             //存放下一个元素在数组中的位置
};
struct SLList   //静态链表类型
{
	SLCell r[MAX_SPACE]; //r[0]不存放数据,类似于链表的头指针
	int bitnumber;       //当前的关键字个数,表示此静态链表对n位数排序
	int length;          //链表当前长度
};
typedef int RadixArr[RADIX]; //用于创建first, end数组

②分配函数:

void Distrubute(SLCell *r, int i, RadixArr first, RadixArr end)  //分配
{
	//r表示SLCell数组的首地址,i=0、i=1、i=2 分别表示对百位、十位、个位进行分配
	//first数组存放首个被分配的下标,end存放first指向的最后元素
	memset(first, 0, sizeof(int) * RADIX);
	memset(end, 0, sizeof(int) * RADIX);
	for (int p = r[0].next; p; p = r[p].next) 
	{
		//因为r[0]为头指针,所以p指向表中第一个元素
		int j = r[p].keys[i]; //j为下标,等式右边则表示映射关系
		if (!first[j])  //如果first[j]==0,说明first指向任何元素,直接把p赋给first[j]
			first[j] = p;
		else            // first[j]已经有指向,那么需要找到end[j],并把它们连起来
			r[end[j]].next = p;  
		end[j] = p;     //因为p指向新加入的元素,所以最后一个元素变成p
	}
}

③收集函数:

void Collect(SLCell *r, int i, RadixArr first, RadixArr end)  //收集
{
	//此时分配已经完成,需要做的是按顺序把分配的元素连起来,即收集
	int j = 0;
	while (!first[j])  //寻找第一个非空的first子表
		j++;
	//此时j指向第一个非空子表
	r[0].next = first[j]; //让头指针指向此子表
	int tail = end[j];    //tail代表此子表最后元素的下标
	for (j = j + 1; j < RADIX; j++)   //寻找第2个非空子表,依此类推,直到j>=Radix
	{
		if (!first[j])   //如果子表为空则跳过
			continue;
		else             //当子表不为空时
		{ 
			r[tail].next = first[j]; //让上一个子表的最后一个元素指向first[j]
			tail = end[j];           //此时更新尾部下标
		}
	}
	//收集完毕
	r[tail].next = 0;
}

④核心部分:

void RadixSort(SLList *L)  //基数排序
{
	RadixArr first, end;  //创建first,end数组,不需要初始化,因为Distrubute函数会对其进行初始化
	for (int i = 0; i < L->length; ++i) 
	{
		L->r[i].next = i + 1;  //更新next
	}
	L->r[L->length].next = 0; //设置结束表示0
	for (int i = L->bitnumber - 1; i >= 0; --i)   //依次对个位、十位、百位进行分配并收集
	{
		Distrubute(L->r, i, first, end);
		Collect(L->r, i, first, end);
	}
}

(3)该算法的时间复杂度为O(d(n+rd)),空间复杂度为O(n+rd)。

(4)该算法能实现稳定排序,可用于链式结构和顺序结构,时间复杂度能达到O(n)。该算法的使用有严格的要求,必须要知道各级关键字的主次关系和各级关键字的取值范围。

七、外部排序

1、概述

        如果待排序的记录数目很大,无法一次性调入内存,整个排序过程就必须借用外存分批调入内存才能完成,需要为该过程设计外部排序算法。

2、外部排序的基本方法

        首先,按可用内存大小,将外存上含n个记录的文件分成若干长度为l的子文件或段,将其依次读入内存并利用有效的内部排序方法对它们进行排序,并将排序后得到的有序子文件重新写入外存,通常称这些有序子文件为归并段或顺串;然后,对这些归并段进行逐趟归并,使归并段(有序的子文件)逐渐由小至大,直至得到整个有序文件为止

        假设有一个含10000个记录的文件,首先通过10次内部排序得到10个初始归并段R1~R10,其中每一段都含1000个记录,然后对它们进行如下图所示的两两归并,直至得到一个有序文件为止。从下图可见,由10个初始归并段到一个有序文件,共进行了4趟归并,每一趟从m个归并段得到\left \lceil m/2 \right \rceil个归并段,这种归并方法称为2-路平衡归并。

        k路平衡归并排序:

        (1)硬件配置:在内存中分配k个输入缓冲区和1个输出缓冲区

        (2)算法步骤:首先生成m个初始归并段(对L个记录进行内部排序,组成一个有序的初始归并段),然后进行S趟归并(S=\left \lceil log_{k}m \right \rceil),其中进行k路归并的方法为如下

        ①把k个归并段的块读入k个输入缓冲区

        ②用“归并排序”的方法从k个归并段中选出几个最小记录暂存到输出缓冲区中

        ③当输出缓冲区满时,写出外存

        为了减少归并趟数,可以从两个方面改进,分别是增加归并段的个数k和减少初始归并段的个数m。

3、使用败者树实现k路平衡归并

(1)败者树可视为一棵完全二叉树(多了一个头结点记录“冠军”),k个叶结点分别是当前参加比较的元素,非叶子结点用来记忆左右子树比较的“失败者”,而让胜者往上继续进行比较,一直到根结点

(2)使用多路平衡归并可减少归并趟数,但是用老土方法从k个归并段选出一个最小/最大元素需要对比关键字k-1次,而构造败者树可以使关键字对比次数减少到\left \lceil log_{2}k \right \rceil

4、置换-选择排序

(1)置换-选择排序的特点是在整个排序(得到所有初始归并段)的过程中,选择最小(或最大)关键字和输入、输出交叉或平行进行。使用置换-选择排序,可以让每个初始归并段的长度超越内存工作区大小的限制。

(2)设初始待排文件为FI,初始归并段输出文件为FO,内存工作区为WA、FO和WA的初始状态为空,WA可容纳w个记录。置换-选择算法的步骤如下:

从FI输入w个记录到工作区WA

从WA中选出其中关键字取最小值的记录,记为MINIMAX记录

将MINIMAX记录输出到FO中去

若FI不空,则从FI输入下一个记录到WA中

从WA中所有关键字⽐MINIMAX记录的关键字大的记录中选出最⼩关键字记录,作为新的MINIMAX记录

重复步骤3~步骤5,直至在WA中选不出新的MINIMAX记录为止,由此得到一个初始归并段,输出一个归并段的结束标志到FO中去

重复步骤2~步骤6,直至WA为空,由此得到全部初始归并段

5、最佳归并树

(1)假设由置换-选择得到9个初始归并段,其长度(记录数)依次为9、30、12、18、3、17、2、6、24。现对其进行3-路平衡归并,其归并树(表示归并过程的图)如下图所示,图中每个圆圈表示一个初始归并段,圆圈中数字表示归并段的长度。假设每个记录占一个物理块,则两趟归并所需对外存进行读/写的次数为(9+30+12+18+3+17+2+6+24) * 2*2 = 484。

(2)若对长度不等的m个初始归并段,构造一棵哈夫曼树作为归并树,便可使在进行外部归并时所需对外存进行读/写的次数达到最少。例如,对上述9个初始归并段可构造一棵下图所示的归并树,按此树进行归并,仅需对外存进行((2+3+6)*3 + (9+12+17+24+18)*2 + 30*1)*2=446次读/写,这棵归并树便称作最佳归并树。

(3)假若只有8个初始归并段,例如在前面例子中少了一个长度为30的归并段:

        如果在设计归并方案时,缺额的归并段留在最后,即除了最后一次进行2-路归并外,其它各次归并都是3-路归并,容易看出此归并方案的外存读/写次数为386,显然这不是最佳方案。

        正确的做法是,当初始归并段的数目不足时,需附加长度为0的“虚段”,按照哈夫曼树构造的原则,权为0的叶子应离树根最远,因此,这个只有8个初始归并段的归并树应如下图所示。

        添加虚段数目的判断:

        ①若(初始归并段数量 - 1)% (k-1)= 0,说明刚好可以构成严格k叉树,此时不需要添加虚段。

        ②若(初始归并段数量 - 1)% (k-1)= u ≠ 0,则需要补充(k-1) - u个虚段。

(4)k叉的最佳归并树一定是一棵严格的k叉树,即树中只包含度为k、度为0的结点。设度为k的结点有n_{k}个,度为0的结点有n_{0}个 ,归并树总结点数为n ,则有:

八、各种内部排序方法性能的比较

九、算法设计举例

1、例1

(1)问题描述:试以单链表为存储结构,实现简单选择排序算法。

(2)代码:

void T1(LinkList &L)
{
	for (LinkList p = (*L).next; p->next; p = p->next)
	{
		LinkList q = p;
		LinkList r;
		for (r = p->next; r; r = r->next)
		{
			if (r->key < q->key)
				q = r;
		}
		if (q != p)
		{
			ElemType tmp = q->key;
			q->key = p->key;
			p->key = tmp;
		}
	}
}

2、例2

(1)问题描述:有n个记录存储在带头结点的双向链表中,利用双向冒泡排序法对其按升序进行排序(双向冒泡排序即相邻两趟排序向相反方向冒泡)。

(2)代码:

void T2(DuLinkList &L)
{
	int exchange = 1;                        //是否发生交换的标记
	DuLinkList head = L;                     //双向链表头,向下冒泡的开始结点
	DuLinkList tail = NULL;                  //双向链表尾,向上冒泡的开始结点
	while (exchange)
	{
		if (head->next == tail)
			break;
		DuLinkList p = head->next;
		exchange = 0;
		while (p->next != tail)
		{
			if (p->key > p->next->key)
			{
				DuLinkList tmp = p->next;
				exchange = 1;
				p->next = tmp->next;
				if (tmp->next != NULL)
					tmp->next->prior = p;
				tmp->next = p;
				p->prior->next = tmp;
				tmp->prior = p->prior;
				p->prior = tmp;
			}
			else
				p = p->next;
		}
		tail = p;
		p = tail->prior;
		while (exchange&&p->prior != head)
		{
			if (p->key < p->prior->key)
			{
				DuLinkList tmp = p->prior;
				exchange = 1;
				p->prior = tmp->prior;
				tmp->prior->next = p;
				tmp->prior = p;
				p->next->prior = tmp;
				tmp->next = p->next;
				p->next = tmp;
			}
			else
				p = p->prior;
		}
		head = p;
	}
}

3、例3

(1)问题描述:设有顺序放置的n个桶,每个桶中装有一粒砾石,每粒砾石的颜色是红、白、蓝之一,要求重新安排这些砾石,使得所有红色砾石在前,所有白色砾石居中,所有蓝色砾石在后,重新安排时对每粒砾石的颜色只能看一次,并且只允许用交换操作来调整砾石的位置

(2)代码:

typedef struct
{
	color key;
}ElemType_color;

typedef struct SqList_T3
{
	ElemType_color* elem;  //存储空间的基地址
	int length;            //当前长度
}SqList_T3;

void T3(SqList_T3 &L)
{
	int right = L.length;
	int left = 1;
	int i = 1;
	while (i <= L.length)
	{
		if (L.elem[i].key == red)
		{
			color tmp = L.elem[i].key;
			L.elem[i].key = L.elem[left].key;
			L.elem[left].key = tmp;
			left++;
			i++;
		}
		else if (L.elem[i].key == white)
		{
			i++;
		}
		else if (L.elem[i].key == blue)
		{
			color tmp = L.elem[i].key;
			L.elem[i].key = L.elem[right].key;
			L.elem[right].key = tmp;
			right--;
		}
	}
}
  • 15
    点赞
  • 28
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Zevalin爱灰灰

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值