数据结构 第8章(排序)

目录

  • 1. 基本概念和排序方法概述
    • 1.1 排序的基本概念
      • 1.1.1 排序
      • 1.1.2 排序的稳定性
      • 1.1.3 内部排序和外部排序
    • 1.2 内部排序方法的分类
    • 1.3 待排序记录的存储方式
    • 1.4 排序算法效率的评价指标
  • 2. 插入排序
    • 2.1 直接插入排序
    • 2.2 折半插入排序
    • 2.3 希尔排序
    • 测试代码
  • 3. 交换排序
    • 3.1 冒泡排序
    • 3.2 快速排序
    • 测试代码
  • 4. 选择排序
    • 4.1 简单选择排序
    • 4.2 树形选择排序
    • 4.3 堆排序
      • 4.3.1 调整堆
      • 4.3.2 建初堆
      • 4.3.3 堆排序算法的实现
    • 测试代码
  • 5. 归并排序(有问题)
  • 6. 基数排序(后期补充)
  • 7. 外部排序(后期补充)
    • 7.1 外部排序的基本方法
    • 7.2 多路平衡归并的实现

1. 基本概念和排序方法概述

1.1 排序的基本概念

1.1.1 排序

  • 排序(Sorting):按关键字的非递减或非递增顺序对一组记录重新进行排序的操作

1.1.2 排序的稳定性

  • 当排序记录中的关键字都不相同时,则任何一个记录的无序序列经排序后得到的结果唯一

1.1.3 内部排序和外部排序

  • 内部排序:待排序记录全部存放在计算机内存中进行排序的过程
  • 外部排序:待排序记录的数量很大,以致内存一次不能容纳全部记录,在排序过程中尚需对外存进行访问的排序过程

1.2 内部排序方法的分类

  • 排序记录区

    • 有序序列区
    • 无序序列区
  • 内部排序方法

    • 插入类:将无序子序列中的一个或几个记录 “插入” 到有序序列中,从而增加记录的有序子序列的长度
      • 主要包括:
        • 直接插入排序
        • 折半插入排序
        • 希尔排序
    • 交换类:通过 “交换” 无序序列中的一个或几个记录 “插入” 到有序序列中,从而增加记录的有序子序列的长度
      • 主要包括:
        • 冒泡排序
        • 快速排序
    • 选择类:从记录的无序子序列中 “选择” 关键字最小或最大的记录,并将它加入到有序子序列中,以此方法增加记录的有序子序列的长度
      • 主要包括
        • 选择排序
        • 树形选择排序
        • 堆排序
    • 归并类:通过 “归并” 两个或两个以上的记录有序子序列,逐步增加记录有序序列的长度
      • 主要包括
        • 2-路归并排序
    • 分配类:是唯一一类不需要进行关键字比较的排序方法,排序时主要利用分配和收集两个基本操作完成
      • 主要包括
        • 基数排序

1.3 待排序记录的存储方式

  • 顺序表:记录之间的次序关系由其存储位置决定,实现排序需要移动记录

  • 链表排序:记录之间的次序关系由指针指示,实现排序不需要移动记录,仅需修改指针即可

  • 地址排序:待排序记录本身存储在一组地址连续的存储单元内,同时另设一个指示各个记录存储位置的地址向量,在排序过程中不移动记录本身,而移动地址向量中那些记录的 “地址” ,在排序结束之后再按照地址向量中的值调整记录的存储位置

#define MAXSIZE 10									// 顺序表的最大长度

typedef struct {
    int key;										// 关键字项
    int value;										// 其他数据项
} RedType;											// 记录类型

typedef struct {
    RedType r[MAXSIZE+1];							// r[0] 闲置或用做哨兵单元
    int length;										// 顺序表长度
} SQList;											// 顺序表类型

1.4 排序算法效率的评价指标

  • 执行时间:对于排序操作,时间主要消耗在关键字之间的比较和记录的移动上,排序算法的时间复杂度由这两个指标决定
  • 辅助空间:除了存放待排序记录占用的空间之外,执行算法所需要的其他存储空间,空间复杂度由排序算法的辅助空间决定

2. 插入排序

2.1 直接插入排序

  • 直接插入排序(Straight Insertion Sort):将一条记录插入到已排好的有序表中,从而得到一个新的、记录数量增 1 的有序表
void InsertSort(SQList* L) {
	for (int i = 2; i < MAXSIZE + 1; i++)
	{
		if (L->r[i].key < L->r[i-1].key)
		{
			L->r[0] = L->r[i];
			for (int j = 1; j < i; j++)
			{
				if (L->r[0].key < L->r[j].key)
				{
					L->r[i] = L->r[j];
					L->r[j] = L->r[0];
					L->r[0] = L->r[i];
				}
			}
		}
	}
	L->r[0].key = L->r[0].value = 0;
	printf("InsertSort Success\n");
}

数据结构 第8章(排序)_第1张图片

  • 时间复杂度

    • 最好情况:比较次数 = n-1 ,记录不需移动

    • 最坏情况:

      • 关键字比较次数 KCN
        K C N = ∑ i = 2 n i = ( n + 2 ) ( n − 1 ) 2 ≈ n 2 2 KCN = \sum_{i=2}^{n}i = \frac{(n+2)(n-1)}{2} ≈ \frac{n^2}{2} KCN=i=2ni=2(n+2)(n1)2n2

      • 记录移动次数 RMN
        R M N = ∑ i = 2 n ( i + 1 ) = ( n + 4 ) ( n − 1 ) 2 ≈ n 2 2 RMN = \sum_{i=2}^{n}(i+1) = \frac{(n+4)(n-1)}{2} ≈ \frac{n^2}{2} RMN=i=2n(i+1)=2(n+4)(n1)2n2

    • 时间复杂度为 O ( n 2 ) O(n^2) O(n2)

  • 空间复杂度

    • 只需要一个记录的辅助空间 r[0]
    • 空间复杂度为 O ( 1 ) O(1) O(1)
  • 算法特点

    • 稳定排序
    • 算法简便,且容易实现
    • 适用于链式存储结构,只是单链表上无需移动记录,只需修改相应的指针
    • 适用于初始记录基本有序(正序)的情况,当初始记录无序,n 较大时,此算法时间复杂度较高,不宜采用

2.2 折半插入排序

  • 折半插入排序(Binary Insertion Sort):使用 “折半查找” 查找记录的直接插入排序
void BInsertSort(SQList* L) {
	for (int i = 2; i < MAXSIZE + 1; i++)
	{
		int low = 1;
		int high = i - 1;
		L->r[0] = L->r[i];							// 将待插入的记录暂存到监视哨中
        
		while (low <= high)							// 在 r[low~high] 中折半查找插入的位置
		{
			int m = (low + high) / 2;				// 折半
			if (L->r[0].key < L->r[m].key)
			{
				high = m - 1;						// 插入点在前一子表
			}
			else
			{
				low = m + 1;						// 插入点在后一子表
			}
		}

		for (int j = i - 1; j >= high + 1; j--)
		{
            L->r[j + 1] = L->r[j];					// 记录后移
		}
		
		L->r[high + 1] = L->r[0];					// 将原 r[i] 插入到正确位置
	}
	L->r[0].key = L->r[0].value = 0;
	printf("BInsertSort Success\n");
}
  • 时间复杂度

    • 比较的关键字次数与待排序序列的初始排序无关,仅依赖于记录的个数
    • 时间复杂度为 O ( n 2 ) O(n^2) O(n2)
  • 空间复杂度

    • 只需要一个记录的辅助空间 r[0]
    • 空间复杂度为 O ( 1 ) O(1) O(1)
  • 算法特点

    • 稳定排序
    • 要进行折半查找,只能用于顺序存储,不能用于链式存储
    • 适合初始记录无序、n 较大时的情况

2.3 希尔排序

  • 希尔排序(Shell’s Sort)(缩小增量排序(Diminishing Increment Sort)):分组插入
void ShellInsert(SQList* L, int step) {
	for (int i = step + 1; i < MAXSIZE + 1; i++)
	{
		if (L->r[i].key < L->r[i - step].key)		// 将 L->r[i] 插入有序增量子表
		{
			L->r[0] = L->r[i];						// 暂存 L->r[i] 于 L->r[0]
            
			int j = 0;
			for (j = i - step; j > 0 && L->r[0].key < L->r[j].key; j -= step)
			{
				L->r[j + step] = L->r[j];			// 记录后移,直到找到插入位置
			}
			L->r[j + step] = L->r[0];				// 将原 r[i] 插入到正确位置
		}
	}
}

void ShellSort(SQList* L, int dt[], int num) {
	for (int i = 0; i < num; i++)
	{
		ShellInsert(L, dt[i]);
	}
	printf("ShellSort Success\n");
}

数据结构 第8章(排序)_第2张图片

  • 时间复杂度

    • 当增量序列为 d t [ k ] = 2 t − k + 1 − 1 dt[k] = 2^{t-k+1} - 1 dt[k]=2tk+11 时,希尔排序的时间复杂度为 O ( n 3 / 2 ) O(n^{3/2}) O(n3/2)
      • t 为排序趟数, 1 ≤ k ≤ t ≤ ⌊ l o g 2 ( n + 1 ) ⌋ 1 ≤ k ≤ t ≤ ⌊log_2(n+1)⌋ 1ktlog2(n+1)
    • 当 n 在某个特定范围内,希尔排序所需的比较和移动次数约为 n 1.3 n^{1.3} n1.3,当 n → ∞ n→∞ n 时,可减少到 n ( l o g 2 n ) 2 n(log_2n)^2 n(log2n)2
  • 空间复杂度

    • 只需要一个记录的辅助空间 r[0]
    • 空间复杂度为 O ( 1 ) O(1) O(1)
  • 算法特点

    • 记录跳跃式地移动导致排序方法是不稳定的
    • 只能用于顺序结构,不能用于链式结构
    • 增量序列可以有各种取法,但应该使增量序列中的值没有除 1 之外的公因子,并且最后一个增量必须等于 1
    • 记录总的比较次数和移动次数都比直接插入排序要少,n 越大时,效果越明显
    • 适合初始记录无序、n 较大时的情况

测试代码

#include 
#include 

#define MAXSIZE 10

void InsertSort(SQList);
void BInsertSort(SQList);
void ShellInsert(SQList);
void ShellSort(SQList);
void PrintList(SQList);

typedef struct {
	int key;
	int value;
} RedType;

typedef struct {
	RedType r[MAXSIZE + 1];
	int length;
} SQList;

int main() {
	int keys[MAXSIZE] = { 2,4,6,8,5,3,7,9,10,1 };
	SQList L;
	SQList Copy;
	L.r[0].key = L.r[0].value = 0;
	L.length = 0;
	
	for (int i = 1; i < MAXSIZE + 1; i++)
	{
		L.r[i].key = L.r[i].value = keys[i-1];
		L.length++;
	}

	Copy = L;

	InsertSort(&L);
	PrintList(L);
	L = Copy;
	printf("****************\n");

	BInsertSort(&L);
	PrintList(L);
	L = Copy;
	printf("****************\n");

	int dt[] = { 5,3,1 };
	ShellSort(&L, dt, 3);
	PrintList(L);
	L = Copy;
	printf("****************\n");
}

void InsertSort(SQList* L) {
	for (int i = 2; i < MAXSIZE + 1; i++)
	{
		if (L->r[i].key < L->r[i-1].key)
		{
			L->r[0] = L->r[i];
			for (int j = 1; j < i; j++)
			{
				if (L->r[0].key < L->r[j].key)
				{
					L->r[i] = L->r[j];
					L->r[j] = L->r[0];
					L->r[0] = L->r[i];
				}
			}
		}
	}
	L->r[0].key = L->r[0].value = 0;
	printf("InsertSort Success\n");
}

void BInsertSort(SQList* L) {
	for (int i = 2; i < MAXSIZE + 1; i++)
	{
		int low = 1;
		int high = i - 1;
		L->r[0] = L->r[i];

		while (low <= high)
		{
			int m = (low + high) / 2;
			if (L->r[0].key < L->r[m].key)
			{
				high = m - 1;
			}
			else
			{
				low = m + 1;
			}
		}

		for (int j = i - 1; j >= high + 1; j--)
		{
			L->r[j + 1] = L->r[j];
		}
		
		L->r[high + 1] = L->r[0];
	}
	L->r[0].key = L->r[0].value = 0;
	printf("BInsertSort Success\n");
}

void ShellInsert(SQList* L, int step) {
	for (int i = step + 1; i < MAXSIZE + 1; i++)
	{
		if (L->r[i].key < L->r[i - step].key)
		{
			L->r[0] = L->r[i];

			int j = 0;
			for (j = i - step; j > 0 && L->r[0].key < L->r[j].key; j -= step)
			{
				L->r[j + step] = L->r[j];
			}
			L->r[j + step] = L->r[0];
		}
	}
}

void ShellSort(SQList* L, int dt[], int num) {
	for (int i = 0; i < num; i++)
	{
		ShellInsert(L, dt[i]);
	}
	printf("ShellSort Success\n");
}

void PrintList(SQList L) {
	for (int i = 1; i < MAXSIZE + 1; i++)
	{
		printf("%d ", L.r[i].key);
	}
	printf("\n");
}

3. 交换排序

3.1 冒泡排序

  • 冒泡排序(Bubble Sort):两两比较相邻记录的关键字,如果发生逆序,则进行交换,从而使关键字小的记录如气泡一般逐渐往上 “漂浮” (左移),或者使关键字大的记录如石块一样逐渐向下 “坠落” (右移)
void BubbleSort(SQList* L) {
	int flag = 0;									// flag 标记某一趟排序是否发生交换
	int index = MAXSIZE;

	while ((flag == 0) && (index > 1))
	{
		flag = 1;									// flag 置为 1 ,如果本趟没有发生交换,则不会执行下一趟排序

		for (int i = 1; i < index; i++)
		{
			if (L->r[i].key > L->r[i+1].key)
			{
				flag = 0;							// flag 置为 0 ,表示本趟排序发生了交换
				
				L->r[0] = L->r[i];					// 交换前后两个记录
				L->r[i] = L->r[i + 1];
				L->r[i + 1] = L->r[0];
			}
		}

		index--;
	}
	L->r[0].key = L->r[0].value = 0;
	printf("BubbleSort Success\n");
}

数据结构 第8章(排序)_第3张图片

  • 时间复杂度

    • 最好情况(初始序列为正序):只需进行一趟排序,在排序过程中进行 n-1 次关键字间的比较,且不移动记录

    • 最坏情况(初始序列为逆序):需进行 n-1 趟排序

      总的关键字比较次数 KCN
      K C N = ∑ i = n 2 ( i − 1 ) = n ( n − 1 ) 2 ≈ n 2 2 KCN = \sum_{i=n}^{2}{(i-1)} = \frac{n(n-1)}{2} ≈ \frac{n^2}{2} KCN=i=n2(i1)=2n(n1)2n2
      总的记录移动次数 RMN(每次交换都要移动 3 次记录)
      R M N = 3 ∑ i = n 2 ( i − 1 ) = 3 n ( n − 1 ) 2 ≈ 3 n 2 2 RMN = 3\sum_{i=n}^{2}{(i-1)} = \frac{3n(n-1)}{2} ≈ \frac{3n^2}{2} RMN=3i=n2(i1)=23n(n1)23n2

    • 平均情况下, K C N = n 2 4 , R M N = 3 n 2 4 KCN = \frac{n^2}{4},RMN = \frac{3n^2}{4} KCN=4n2RMN=43n2

    • 时间复杂度为 O ( n 2 ) O(n^2) O(n2)

  • 空间复杂度

    • 只有在两个记录交换位置时需要一个辅助空间用做暂存记录
    • 空间复杂度为 O ( 1 ) O(1) O(1)
  • 算法特点

    • 稳定排序
    • 可用于链式存储结构
    • 移动记录次数较多,算法平均时间性能比直接插入排序差
    • 当初始记录无序,n 较大时,此算法不宜采用

3.2 快速排序

  • 快速排序(Quick Sort):通过两个(不相邻)记录的交换,消除多个逆序
int Partition(SQList* L, int low, int high) {
	L->r[0] = L->r[low];							// 用子表的第一个记录做枢轴记录

	int key = L->r[low].key;						// 枢轴记录关键字保存在 key 中
    
	while (low < high)								// 从表的两端交替地向中间扫描
	{
		while ((low < high) && (L->r[high].key >= key))
		{
			high--;
		}

		L->r[low] = L->r[high];						// 将比枢轴小的记录移动到低端

		while ((low < high) && (L->r[low].key <= key))
		{
			low++;
		}

		L->r[high] = L->r[low];						// 将比枢轴大的记录移动到高端

	}
	L->r[low] = L->r[0];							// 枢轴记录到位

	return low;										// 返回枢轴位置
}

void QSort(SQList* L, int low, int high) {
	if (low < high)
	{
		int middle = Partition(L, low, high);		// 将 L->r[low···high] 一分为二,midddle 是枢轴位置
		QSort(L, low, middle - 1);					// 对左子表递归排序
		QSort(L, middle + 1, high);					// 对右子表递归排序
	}
}

void QuickSort(SQList* L) {
	QSort(L, 1, MAXSIZE);
	printf("QuickSort Success\n");
}

数据结构 第8章(排序)_第4张图片

  • 时间复杂度

    • 最好情况:每一趟排序后都能将记录序列均匀地分割成两个长度大致相等的子表,类似折半查找

      • 在 n 个元素的序列中,对枢轴定位所需时间为 O ( n ) O(n) O(n)
      • 对 n 个元素的序列进行排序所需的时间 T ( n ) ≤ n l o g 2 n + n T ( 1 ) T(n) ≤ nlog_2n + nT(1) T(n)nlog2n+nT(1)
    • 最坏情况:在待排序序列已经排好序的情况下,其递归树成为单支树,每次划分只得到一个比上一次少一个记录的子序列

      • 必须经过 n-1 趟才能将所有记录定位,而且第 i 趟需要经过 n-i 次比较

        总的关键字比较次数 KCN
        K C N = ∑ i = 1 n − 1 ( n − i ) = n ( n − 1 ) 2 ≈ n 2 2 KCN = \sum_{i=1}^{n-1}{(n - i)} = \frac{n(n-1)}{2} ≈ \frac{n^2}{2} KCN=i=1n1(ni)=2n(n1)2n2

    • 时间复杂度为 O ( n l o g 2 n ) O(nlog_2n) O(nlog2n)

  • 空间复杂度

    • 执行时需要一个栈来存放相应的数据
    • 最大递归调用次数与递归树的深度一致
    • 最好情况下的空间复杂度为 O ( l o g 2 n ) O(log_2n) O(log2n)
    • 最坏情况下的空间复杂度为 O ( n ) O(n) O(n)
  • 算法特点

    • 记录非顺次的移动导致排序方法是不稳定的
    • 排序过程中需要定位表的上界和下界,所以适合用于顺序结构,很难用于链式结构
    • 当 n 较大时,在平均情况下快速排序是所有内部排序方法中速度最快的一种
    • 适合初始记录无序、n 较大时的情况

测试代码

#include 
#include 

#define MAXSIZE 10

void BubbleSort(SQList);
int Partition(SQList);
void QSort(SQList);
void QuickSort(SQList);
void PrintList(SQList);

typedef struct {
	int key;
	int value;
} RedType;

typedef struct {
	RedType r[MAXSIZE + 1];
	int length;
} SQList;

int main() {
	int keys[MAXSIZE] = { 2,4,6,8,5,3,7,9,10,1 };
	SQList L;
	SQList Copy;
	L.r[0].key = L.r[0].value = 0;
	L.length = 0;
	
	for (int i = 1; i < MAXSIZE + 1; i++)
	{
		L.r[i].key = L.r[i].value = keys[i-1];
		L.length++;
	}

	Copy = L;

	BubbleSort(&L);
	PrintList(L);
	L = Copy;
	printf("****************\n");

	QuickSort(&L);
	PrintList(L);
	L = Copy;
	printf("****************\n");
}

void BubbleSort(SQList* L) {
	int flag = 0;
	int index = MAXSIZE;

	while ((flag == 0) && (index > 1))
	{
		flag = 1;

		for (int i = 1; i < index; i++)
		{
			if (L->r[i].key > L->r[i+1].key)
			{
				flag = 0;
				
				L->r[0] = L->r[i];
				L->r[i] = L->r[i + 1];
				L->r[i + 1] = L->r[0];
			}
		}

		index--;
	}
	L->r[0].key = L->r[0].value = 0;
	printf("BubbleSort Success\n");
}

int Partition(SQList* L, int low, int high) {
	L->r[0] = L->r[low];

	int key = L->r[low].key;

	while (low < high)
	{
		while ((low < high) && (L->r[high].key >= key))
		{
			high--;
		}

		L->r[low] = L->r[high];

		while ((low < high) && (L->r[low].key <= key))
		{
			low++;
		}

		L->r[high] = L->r[low];

	}
	L->r[low] = L->r[0];

	return low;
}

void QSort(SQList* L, int low, int high) {
	if (low < high)
	{
		int middle = Partition(L, low, high);
		QSort(L, low, middle - 1);
		QSort(L, middle + 1, high);
	}
}

void QuickSort(SQList* L) {
	QSort(L, 1, MAXSIZE);
	printf("QuickSort Success\n");
}

void PrintList(SQList L) {
	for (int i = 1; i < MAXSIZE + 1; i++)
	{
		printf("%d ", L.r[i].key);
	}
	printf("\n");
}

4. 选择排序

4.1 简单选择排序

  • 简单选择排序(Simple Selection Sort)(直接选择排序)
void SelectSort(SQList* L) {
	for (int i = 1; i < MAXSIZE; i++)				// 从 L->r[low···high] 中选择关键字最小的记录
	{
		int index = i;

		for (int j = i + 1; j < MAXSIZE + 1; j++)
		{
			if (L->r[j].key < L->r[index].key)
			{
				index = j;							// index 指向此趟排序中关键字最小的记录
			}
		}

		if (index != i)
		{
			L->r[0] = L->r[i];						// 交换 r[i] 与 r[index]
			L->r[i] = L->r[index];
			L->r[index] = L->r[0];
		}
	}
	printf("SelectSort Success\n");
}

数据结构 第8章(排序)_第5张图片

  • 时间复杂度

    • 最好情况(正序):不移动

    • 最坏情况(逆序):移动 3 ( n − 1 ) 3(n-1) 3(n1)

    • 总的关键字比较次数 KCN
      K C N = ∑ i = 1 n − 1 ( n − i ) = n ( n − 1 ) 2 ≈ n 2 2 KCN = \sum_{i=1}^{n-1}{(n - i)} = \frac{n(n-1)}{2} ≈ \frac{n^2}{2} KCN=i=1n1(ni)=2n(n1)2n2

    • 时间复杂度为 O ( n 2 ) O(n^2) O(n2)

  • 空间复杂度

    • 只有在两个记录交换时需要一个辅助空间
    • 空间复杂度为 O ( 1 ) O(1) O(1)
  • 算法特点

    • 是一种稳定的排序方法
    • 可用于链式存储结构
    • 移动记录次数较少,当每一记录占用的空间较多时,此方法比直接插入排序快

4.2 树形选择排序

  • 树形选择排序(Tree Selection Sort)(锦标赛排序(Tournament Sort)):按照锦标赛的思想进行选择排序的方法

    • 首先对 n 个记录的关键字进行两两比较,然后在其中 ⌈ n 2 ⌉ ⌈\frac{n}{2}⌉ 2n 个较小者之间再进行两两比较,如此重复,直至选出最小关键字的记录为止
  • 时间复杂度

    • 时间复杂度为 O ( n l o g 2 n ) O(nlog_2n) O(nlog2n)

4.3 堆排序

  • 堆排序(Heap Sort):将待排序的记录 r[ 1 ··· n ] 看成是一棵完全二叉树的顺序存储结构,利用完全二叉树中双亲结点和孩子结点之间的内在关系,在当前无序的序列中选择关键字最大(或最小)的记录

  • 堆:n 个元素的序列 k 1 , k 2 , ⋅ ⋅ ⋅ , k n {k_1,k_2,···,k_n} k1,k2,,kn 满足以下条件时才称之为堆
    k i ≥ k 2 i 且 k i ≥ k 2 i + 1 或 k i ≤ k 2 i 且 k i ≤ k 2 i + 1 ( 1 ≤ i ≤ ⌊ n 2 ⌋ ) k_i ≥ k_{2i} 且 k_i ≥ k_{2i + 1} \quad 或 \quad k_i ≤ k_{2i} 且 k_i ≤ k_{2i + 1} \quad (1 ≤ i ≤ ⌊\frac{n}{2}⌋) kik2ikik2i+1kik2ikik2i+1(1i2n)

  • 若将和此序列对应的一维数组(以一维数组做此序列的存储结构)看成是一个完全二叉树,则堆实质上是满足以下性质的完全二叉树

    • 树中所有非终端结点的值均不大于(或不小于)其左、右孩子结点的值
  • 堆排序利用大根堆(或小根堆)堆顶记录的关键字最大(或最小)这一特征,使得当前无序的序列中选择关键字最大(或最小)的记录变得简单

4.3.1 调整堆

  • 筛选法:把较小的关键字逐层筛下去,将较大的关键字逐层选上来
void HeapAdjust(SQList* L, int s, int m) {
	L->r[0] = L->r[s];

	for (int i = 2 * s; i <= m; i *= 2)					// 沿 key 较大的孩子结点向下筛选
	{
		if ((i < m) && (L->r[i].key < L->r[i+1].key))	// i 为 key 较大的记录的下标
		{
			i++;
		}

		if (L->r[0].key >= L->r[i].key)					// L->r[0] 应插入在位置 s 上
		{
			break;
		}

		L->r[s] = L->r[i];
		s = i;
	}

	L->r[s] = L->r[0];
}

4.3.2 建初堆

  • 对无序序列 r[ 1 ··· n ] ,从 i = n 2 i=\frac{n}{2} i=2n 开始,反复调用筛选法,一次将以 r[ i ],r[ i-1 ],··· ,r[ 1 ] 为根的子树调整为堆
void CreatHeap(SQList* L) {
	for (int i = MAXSIZE / 2; i > 0; i--)
	{
		HeapAdjust(L, i, MAXSIZE);
	}
}

数据结构 第8章(排序)_第6张图片

4.3.3 堆排序算法的实现

void HeapSort(SQList* L) {
	CreatHeap(L);								// 把无序序列 L->r[1···MAXSIZE] 建成大根堆 

	for (int i = MAXSIZE; i > 1; i--)
	{
		L->r[0] = L->r[1];						// 将堆顶记录和当前未经排序子序列 L->r[1···i] 中最后一个记录互换
		L->r[1] = L->r[i];
		L->r[i] = L->r[0];
		HeapAdjust(L, 1, i - 1);				// 将 L->r[1···i-1] 重新调整为大根堆
	}

	printf("HeapSort Success\n");
}

数据结构 第8章(排序)_第7张图片

  • 时间复杂度

    • 设有 n 个记录的初始序列所对应的完全二叉树的深度为 h ,建初堆时,每个非终端结点都要自上而下进行 “筛选”

    • 由于第 i 层上的结点数小于等于 2 i − 1 2^{i-1} 2i1 ,且第 i 层结点最大下移的深度为 h − i h-i hi ,每下移一层要做两次比较

    • 总的关键字比较次数 KCN
      K C N = ∑ i = h − 1 1 2 i − 1 ∗ 2 ( h − i ) = ∑ i = h − 1 1 2 i ( h − i ) = ∑ j = 1 h − 1 2 h − j ∗ j ≤ 2 n ∑ j = 1 h − 1 j 2 j ≤ 4 n KCN = \sum_{i=h-1}^{1}{2^{i-1}*2(h - i)} = \sum_{i=h-1}^{1}{2^{i}(h - i)} = \sum_{j=1}^{h-1}{2^{h-j} * j} ≤ 2n\sum_{j=1}^{h-1}{\frac{j}{2^{j}}} ≤ 4n KCN=i=h112i12(hi)=i=h112i(hi)=j=1h12hjj2nj=1h12jj4n

    • n 个结点的完全二叉树的深度为 ⌊ l o g 2 n ⌋ + 1 ⌊log_2n⌋ + 1 log2n+1 ,则重建堆时关键字总的比较次数不超过
      2 ( ⌊ l o g 2 ( n − 1 ) ⌋ + ⌊ l o g 2 ( n − 2 ) ⌋ + 4 + l o g 2 2 ) < 2 n ( ⌊ l o g 2 n ⌋ ) 2(⌊log_2{(n-1)}⌋ + ⌊log_2{(n-2)}⌋ + 4 + log_2{2}) < 2n(⌊log_2n⌋) 2(log2(n1)+log2(n2)+4+log22)<2n(log2n)

    • 最坏情况:时间复杂度为 O ( n l o g 2 n ) O(nlog_2n) O(nlog2n)

    • 平均性能接近于最坏性能

  • 空间复杂度

    • 仅需一个记录大小供交换用的辅助辅助空间
    • 空间复杂度为 O ( 1 ) O(1) O(1)

测试代码

#include 
#include 

#define MAXSIZE 10

void SelectSort(SQList);
void HeapAdjust(SQList);
void CreatHeap(SQList);
void HeapSort(SQList);
void PrintList(SQList);

typedef struct {
	int key;
	int value;
} RedType;

typedef struct {
	RedType r[MAXSIZE + 1];
	int length;
} SQList;

int main() {
	int keys[MAXSIZE] = { 2,4,6,8,5,3,7,9,10,1 };
	SQList L;
	SQList Copy;
	L.r[0].key = L.r[0].value = 0;
	L.length = 0;
	
	for (int i = 1; i < MAXSIZE + 1; i++)
	{
		L.r[i].key = L.r[i].value = keys[i-1];
		L.length++;
	}

	Copy = L;

	SelectSort(&L);
	PrintList(L);
	L = Copy;
	printf("****************\n");

	HeapSort(&L);
	PrintList(L);
	L = Copy;
	printf("****************\n");
}

void SelectSort(SQList* L) {
	for (int i = 1; i < MAXSIZE; i++)
	{
		int index = i;

		for (int j = i + 1; j < MAXSIZE + 1; j++)
		{
			if (L->r[j].key < L->r[index].key)
			{
				index = j;
			}
		}

		if (index != i)
		{
			L->r[0] = L->r[i];
			L->r[i] = L->r[index];
			L->r[index] = L->r[0];
		}
	}
	printf("SelectSort Success\n");
}

void HeapAdjust(SQList* L, int s, int m) {
	L->r[0] = L->r[s];

	for (int i = 2 * s; i <= m; i *= 2)
	{
		if ((i < m) && (L->r[i].key < L->r[i+1].key))
		{
			i++;
		}

		if (L->r[0].key >= L->r[i].key)
		{
			break;
		}

		L->r[s] = L->r[i];
		s = i;
	}

	L->r[s] = L->r[0];
}

void CreatHeap(SQList* L) {
	for (int i = MAXSIZE / 2; i > 0; i--)
	{
		HeapAdjust(L, i, MAXSIZE);
	}
}

void HeapSort(SQList* L) {
	CreatHeap(L);

	for (int i = MAXSIZE; i > 1; i--)
	{
		L->r[0] = L->r[1];
		L->r[1] = L->r[i];
		L->r[i] = L->r[0];
		HeapAdjust(L, 1, i - 1);
	}

	printf("HeapSort Success\n");
}

void PrintList(SQList L) {
	for (int i = 1; i < MAXSIZE + 1; i++)
	{
		printf("%d ", L.r[i].key);
	}
	printf("\n");
}

5. 归并排序(有问题)

  • 归并排序(Merging Sort):将两个或两个以上的有序表合并成一个有序表的过程

  • 2-路归并:将两个有序表合并成一个有序表的过程

  • 算法思想

    • 假设初始序列含有 n 个记录,则可看成是 n 个有序的子序列,每个子序列的长度为 1 ,然后两两归并,得到 ⌈ n 2 ⌉ ⌈\frac{n}{2}⌉ 2n 个长度为 2 或 1 的有序子序列
    • 两两归并,··· ,如此重复,直至得到一个长度为 n 的有序序列为止
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++];
	}
}

void MSort(RedType R[], RedType T[], int low, int high) {
	if (low != high)
	{
		int mid = (low + high) / 2;
		RedType S[MAXSIZE + 1];
		MSort(R, S, low, mid);
		MSort(R, S, mid + 1, high);
		Merge(S, T, low, mid, high);
	}
	else {
		T[low] = R[low];
	}
}

void MergeSort(SQList* L) {
	MSort(L->r, L->r, 1, MAXSIZE);
	printf("MergeSort Success\n");
}

数据结构 第8章(排序)_第8张图片

  • 时间复杂度

    • 当有 n 个记录时,需进行 ⌈ l o g 2 n ⌉ ⌈log_2n⌉ log2n 趟归并排序,每一趟归并,其关键字比较次数不超过 n ,元素移动次数都是 n
    • 时间复杂度为 O ( n l o g 2 n ) O(nlog_2n) O(nlog2n)
  • 空间复杂度

    • 需要和待排序记录个数相等的辅助存储空间
    • 空间复杂度为 O ( n ) O(n) O(n)
  • 算法特点

    • 稳定排序
    • 可用于链式结构,且不需要附加存储空间,但递归实现时仍需要开辟相应的递归工作栈

6. 基数排序(后期补充)

7. 外部排序(后期补充)

7.1 外部排序的基本方法

  • 外部排序由两个相对独立的阶段组成

    • 按可用内存大小,将外存上含 n 个记录的文件分成若干长度为 l 的子文件或段(segment),依次读入内存并利用有效的内部排序方法堆它们进行排序,并将排序后得到的有序子文件重新写入外村,通常称这些有序子文件为归并段顺串(run)
    • 对这些归并段进行逐趟归并,使归并段(有序的子文件)逐渐由小至大,直至得到整个有序文件为止
  • 2-路平衡归并:每一趟从 m 个归并段得到 ⌈ m 2 ⌉ ⌈\frac{m}{2}⌉ 2m 个归并段

  • 一般情况下
    外 部 排 序 所 需 总 的 时 间 = 内 部 排 序 ( 产 生 初 始 归 并 段 ) 所 需 的 时 间 ( m ∗ t I S ) + 外 存 信 息 读 写 的 时 间 ( d ∗ t I O ) + 内 部 归 并 所 需 的 时 间 ( s ∗ u t m g ) 外部排序所需总的时间 = 内部排序(产生初始归并段) 所需的时间 (m*t_{IS}) + \\ 外存信息读写的时间 (d * t_{IO}) + \\ 内部归并所需的时间 (s * ut_{mg}) =()(mtIS)+(dtIO)+(sutmg)
    t I S t_{IS} tIS :为得到一个初始归并段进行内部排序所需时间的均值

    t I O t_{IO} tIO :进行一次外存读/写时间的均值

    u t m g ut_{mg} utmg :对 u u u 个记录进行内部归并所需时间

    m m m :经过内部排序之后得到的初始归并段的个数

    s s s :归并的趟数

    d d d :总的读/写次数

  • 一般情况下,对 m m m 个初始归并段进行 k-路 平衡归并时,归并的趟数
    s = ⌈ l o g k m ⌉ s = ⌈log_km⌉ s=logkm

  • 为了减少归并趟数 s ,可以从以下两个方面进行改进

    • 增加归并段的个数 k k k
    • 减少初始归并段的个数 m m m

7.2 多路平衡归并的实现

你可能感兴趣的:(数据结构,数据结构)