大话数据结构第9章笔记(排序)

9.1开场白
9.2排序的基本概念和分类
      9.2.1排序的稳定性
      9.2.2内排序与外排序
      9.2.3排序用到的结构与函数
9.3冒泡排序
      9.3.1最简单排序实现
      9.3.2冒泡排序算法
      9.3.3冒泡排序优化
      9.3.4冒泡排序复杂度分析

9.4简单选择排序
      9.4.1简单选择排序算法
      9.4.2简单选择排序复杂度分析

9.5直接插入排序
      9.5.1直接插入排序算法
      9.5.2直接插入排序复杂度分析

9.6直接插入排序
      9.6.1希尔排序原理
      9.6.2希尔排序算法
      9.6.3希尔排序复杂度分析

9.8 归并排序
      9.8.1归并排序算法
      9.8.2归并排序复杂度分析
      9.8.3非递归实现归并排序

9.7堆排序
      9.7.1堆排序算法
      9.7.2堆排序复杂度分析

9.9快速排序
      9.9.1快速排序算法
      9.9.2快速排序复杂度分析
      9.9.3快速排序优化

9.2排序的基本概念和分类
1.排序就是将无序的数据集合,按照某一定顺序进行排列
2.对于有很多关键字的排序,可以将多个关键字的排序转化成单个关键字的排序,如下图所示:
在这里插入图片描述
在这里插入图片描述
9.2.1排序的稳定性
如图:
在这里插入图片描述

9.2.2内排序与外排序
     1.什么叫内排序,外排序:(如下图)

在这里插入图片描述
     2.排序算法的性能主要是受3个方面影响:

           1.时间性能:(如下图)
在这里插入图片描述
           2.辅助空间:(如下图)
在这里插入图片描述
           3.算法的复杂性:(如下图) 在这里插入图片描述

9.2.3排序用到的结构与函数
1.排序用到的顺序表结构:

#define MAXSIZE 10     //用于要排序数组个数最大值,可根据需要修改
typedef struct         
{
	int r[MAXSIZE+1];     //用于存储要排序的数组,r[0]用作哨兵或临时变量
	int length;           //用于记录顺序表长度
}SqList;    

2.排序所用到的交换函数

//交换L中数组r的下标为i和j的值
void swap(SqList *L,int i,int j)
{
	int temp=L->r[i];
	L->r[i]=L->r[j];
	L->r[j]=temp;
}

9.3冒泡排序
      9.3.1最简单的排序实现

冒泡排序一种交换排序,它的基本思想:两两比较相邻记录的关键字,如果反序则交换,直到没有反序的记录为止

//对顺序表L做交换排序(冒泡排序初级版)
void BubbleSort0(SqList *L)
{
	int i,j;
	for(i=1; i<L->length; i++)
	{
		for(j=i+1; j<=L->length; j++)
		{
			if(L->r[i]>L->r[j])
			{
				swap(L,i,j);    //交换L->r[i]与L->r[j]的值
			}
		}
	}
}//详见代码如下图

上述代码的思路:让每一关键字,都和它后面的每一个关键字比较,如果前面的大于后面的,则交换,这样第一位置的关键字在一次循环后一定是最小的
上述代码缺陷:每次只能一个一个去排列,这样对其余关键字没有任何帮助
在这里插入图片描述

      9.3.2冒泡排序算法

此冒泡排序的思想:i从第一个位置开始,然后j从最后向前冒泡,不断地将小的值放到前面,这样不断的循环下去,便于将一部分小的数值移动一些位置

//对顺序表L做冒泡排序
void BubbleSort(SqList *L)
{
	int i,j;
	for(i=1; i<L->length; i++)
	{
		for(j=L->length-1; j>=i; j--)     //注意j是从后往前循环
		{
			if(L->r[j]>L->r[j+1])   //若前者大于后者
			{
				swap(L,j,j+1);   //交换L->r[j]与L->r[j+1]的值
			}
		}
	}
}//详见代码如下图:

在这里插入图片描述
在这里插入图片描述

      9.3.3冒泡排序优化

为什么要有冒泡排序的优化?如下图中,如果不优化,还要不断地进行比较,浪费时间

在这里插入图片描述

//对顺序表L做改进冒泡算法
void BubbleSort2(SqList *L)
{
	int i,j;
	Status flag=TRUE;     //flag用来作为标记
	for(i=1; i<L->length && flag; i++)    //若flag为false则循环结束
	{
		flag=FALSE;    //初始为false
		for(j=L->length-1; j>=i; j--)
		{
			if(L->r[j] > L->[j+1])
			{
				swap(L,j,j+1);    //交换L->r[j]与L->r[j+1]的值
				flag=TRUE;       //如果有数据交换,则flag改true
			}
		}
	}
}//详见如下图:

此代码的思想是:加入一个flag判断,flag初始值为TRUE,进入循环后如果后面循环中一但有交换则改flag值为TRUE,再次循环;如果循环中已经是升序排列,不会有前面的值大于后面的值,则flag为false,循环结束

在这里插入图片描述

9.3.4冒泡排序复杂度分析
最好情况是o(n)
最坏情况是o(n^2)

9.4简单选择排序

      9.4.1简单选择排序算法

简单选择排序的思想:从第一个位置开始,循环找到后面的最小值,然后与第一个位置的值进行交换,如此进行下去,则就是简单选择排序

//对顺序表L做简单选择排序
void SelectSort(SqList *L)
{
	int i,j,min;
	for(i=1; i<L->length; i++)
	{
		min=i;       //将当前下标定义为最小值下标
		for(j=i+1; j<=L->length; j++)   //循环之后的数据
		{	
			if(L->r[min]>L->r[j])   //如果有小于当前最小值的关键字
				min=j;         //将此关键字的下标赋值给min
		}
		if(i!=min)                     //若min不等于i,说明找到最小值了,交换
			swap(L,i,min);    //交换L->r[i]与L->r[min]的值
	}
}

在这里插入图片描述

      9.4.2简单选择排序复杂度分析

1.对上面代码中第二层的for(就是找最小值下标)的那个代码而言,对第一个值其比较次数为n-1,对于第2个值而言,其比较次数为n-2,则一共的比较次数为n(n-1)/2,后面中就是交换次序的代码,最坏情况需要交换n-1次,则一共的时间复杂度为o(n^2)

9.5直接插入排序
      9.5.1 直接插入排序算法

直接插入排序的基本操作是将一个记录插入到已经排好序的有序表中,从而得到一个新的,记录数增1的有序表

//对顺序表L作直接插入操作
void InsertSort(SqList *L)
{
	int i,j;
	for(i=2; i<=L->length; i++)
	{
		if(L->r[i]<L->r[i-1])          //需将L->r[i]插入有序子表
		{
			L->r[0]=L->r[i];            //设置哨兵
			for(j=i-1; L->r[j]>L->r[0]; j--)
				L->r[j+1]=L->r[j];              //记录后移
			L->r[j+1]=L->r[0];            //插入到正确位置
		}
	}
}

在这里插入图片描述
在这里插入图片描述

      9.5.2直接插入排序复杂度分析
在这里插入图片描述
在这里插入图片描述
9.6希尔排序

      9.6.1希尔排序原理

1.希尔排序就是为了突破O(n^2)的时间复杂度,思想就是每次循环一次后都将原来的数组排序成基本有序(P.S.基本有序就是大的数字基本在后面,小的数字基本在前面,不大不小的基本在中间)
2.采用的是跳跃分割的策略:将相距某个“增量”的记录组成一个子序列,这样才能保证在自序列内分别进行直接插入排序后得到的结果时基本有序而不是局部有序

在这里插入图片描述

      9.6.2 希尔排序算法

//对顺序表L做希尔排序
void ShellSort(SqList *L)
{
	int i,j;
	int increment=L->length;
	do
	{
		increment=increment/3+1;    //增量序列
		for(i=increment+1; i<=L->length; i++)
		{
			if(L->r[i]<L->r[i-increment])
			{//需将L->r[i]插入有序增量子表中
				L->r[0]=L->r[i];          //暂存到L->r[0]
				for(j=i-increment; j>0 && L->r[0]<L->r[j]; j-=increment)
					L->r[j+increment]=L->r[j];        //记录向后移,查找插入位置
				L->r[j+increment]=L->r[0];          //插入
			}
		}
	}while(increment>1);
}//详见如下图

1.待排数组
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

      9.6.3希尔排序时间复杂度分析

1.时间复杂度为O(n^3/2),要直接好于O(n的平方)
2.增量序列的最后一个增量值必须等于1才行

9.8归并排序

说白了就是对数组进行两两合并,详见下图

在这里插入图片描述
      9.8.1归并排序算法

归并排序的原理,见下图:

在这里插入图片描述

总的归并排序代码:

//对顺序表L做归并排序
void MergeSort(SqList *L)
{
	MSort(L->r, L->r, 1, L->length);
}

上述代码,为了和前面的排序算法统一,用了同样的参数定义SqList *L,MSort的代码实现如下:

递归拆分代码:

void MSort(int SR[], int TR1[], int s, int t)  //SR[]数组相当于原始数组,TR1[]相当于排好序的数组
{
	int m;
	int TR2[MAXSIZE+1];
	if(s==t)
		TR1[s]=SR[s];    
	else
	{
		m=(s+t)/2;     //相当于从中间开始分
		MSort(SR,TR2,s,m);    //将原始数组SR[]的前半部分递归到TR2[]中
		MSort(SR,TR2,m+1,t);     //将原始数组SR的后半部分放到TR2[]中
		Merge(TR2,TR1,s,m,t);   //将TR2[]递归到已排好序的数组TR1[]中
	}
}//详见如下图

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

Merge函数如何实现(归并算法):

//将数组直接归并为有序的
void Merge(int SR[], int TR[], int i,int m,int n)
{
	int j,k,l;
	for(j=m+1,k=i; i<=m && j<=n; k++)   //将SR中记录有小到大归并入TR中
	{
		if(SR[i]<SR[j])
			TR[k]=SR[i++];
		else
			TR[k]=SR[j++];
	}
	if(i<=m)                                 //将剩余的归并到TR中
	{
		for(l=0; l<=m-i; l++)
			TR[K+l]=SR[i+1];
	}
	if(j<=n)                                 //将剩余的归并到TR中
	{
		for(l=0; l<=n-j; l++)
			TR[k+l]=SR[j+l];
	}
} //详见如下图:

在这里插入图片描述
在这里插入图片描述

      9.8.2 归并排序复杂度分析

1.时间复杂度分析:
对于上面那个merge函数中,将原始数列SR[]放入到TR1[]中,由于要遍历循环所有的记录所以时间复杂度为O(n),而由完全二叉树的深度可知,整个总的归并排序需要进行log2n次,总的时间复杂度为O(nlogn)
2.空间复杂度分析:
由于归并排序在归并过程中需要与原始记录序列同样数量的存储空间存放归并结果,所以空间复杂度为o(n)。同时还有递归时深度为log2n的栈空间,因此空间复杂度为O(n+logn)
3.Merge函数中有if(SR[i]<SR[j])语句,这就说明他需要两两比较,不存在跳跃,因此归并排序是一种稳定的排序算法

      9.8.3 非递归实现归并排序

直接进行归并排序代码如下:

//对顺序表L作归并非递归排序
void MergeSort2(SqList *L)
{
	int* TR=(int *) malloc (L->length*sizeof(int));    //申请额外空间
	int k=1;
	while(k<L->length)
	{
		MergePass(L->r,TR,k,L->length);
		k=2*k;                           //子序列长度加倍
		MergePass(TR,L->r,k,L->length);
	    k=2*k;                    	//子序列长度加倍
	}
} //详见代码如下图:

在这里插入图片描述

非递归算法和递归算法的区别

1.非递归算法:对数组直接归并
2.递归算法: 先递归拆分数组然后再归并退出递归

上面的MergePass(归并排序)代码如下:

void MergePass(int SR[],int TR[],int s,int n)
{
	int i=1;
	int j;
	while(i<=n-2*s+1)
	{
		Merge(SR,TR,i,i+s-1,i+2*s-1);       //两两归并
		i=i+2*s;
	}
	if(i<n-s+1)         //归并最后两个序列
		Merge(SR,TR,i,i+s-1,n);
	else
		for(j=i; j<=n; j++)
			TR[j]=SR[j];
}//详见如下图:

在这里插入图片描述
在这里插入图片描述

非递归归并排序的时间复杂度如下图:

在这里插入图片描述

9.7堆排序算法

大顶堆+小顶堆如下图:

在这里插入图片描述

如果按照层序遍历的方式给结点从1开始编号,有如下关系:

在这里插入图片描述
在这里插入图片描述

将大顶堆和小顶堆用层序遍历存入数组,如下图:

在这里插入图片描述

      9.7.1堆排序算法

堆排序的基本思想如下:

在这里插入图片描述
在这里插入图片描述
堆排序的整体代码:

//对顺序表L进行堆排序
void HeapSort(SqList *L)
{
	int i;
	for(i=L->length/2; i>0; i--)         //将原来的数组构建为一个大顶堆
		HeapAdjust(L,i,L->length);
	
	for(i=L->length; i>1; i--)
	{
		swap(L,1,i);          //将堆顶记录和当前未经排序子序列的最后一个记录交换
		HeapAdjust(L,1,i-1);       //将其重新调整为大顶堆
	}
}//详见如下图

在这里插入图片描述
在这里插入图片描述
上述代码中HeapAdjust函数如何实现:

void HeapAdjust(SqList *L,int s,int m)
{
	int temp,j;
	temp=L->r[s];
	for(j=2*s; j<=m; j*=2)   //沿关键字较大的孩子结点向下筛选
	{
		if(j<m && L->r[j]<L->r[j+1])
			++j;            //j为关键字中较大的记录的下标
		if(temp>L->r[j])
			break;
		L->r[s]=L->r[j];
		s=j;
	}
	L->r[s]=temp;   //插入
}//详见如下图

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
接下来就是HeapSort函数中正式的排序过程

	for(i=L->length; i>1; i--)
	{
		swap(L,1,i);          //将堆顶记录和当前未经排序子序列的最后一个记录交换
		HeapAdjust(L,1,i-1);       //将其重新调整为大顶堆
	}//详见如下图:

在这里插入图片描述

      9.7.2堆排序复杂度分析

1.在第一次构建堆的过程中,时间复杂度如下图:

在这里插入图片描述
在这里插入图片描述

2.在正式排序的过程中,时间复杂度如下:

在这里插入图片描述

9.9快速排序

1.希尔排序相当于直接插入排序的升级,他们都属于插入排序类
2.堆排序相当于简单选择排序的升级,他们都属于选择排序类
3.快速排序就是冒泡排序的升级,他们都属于交换排序类

      9.9.1快速排序算法

快速思想的思想,如下图:

在这里插入图片描述
快速排序的代码:

void QuickSort(SqList *L)
{
	QSort(L,1,L->length);
}

QSort函数的代码:

//对顺序表L中的子序列做快速排序
void QSort(SqList *L,int low,int high)
{
	int pivot;
	if(low<high)
	{
		pivot=Partition(L,low,high);        //将原来数组一分为2,算出枢轴pivot
		QSort(L,low,pivot-1);             //对低字表递归排序
		QSort(L,pivot+1,high);            //对高子表递归排序
	}
}//详见如下图

在这里插入图片描述

上述代码中Partition函数(将原始数组一分为2的数组)如下:

//交换顺序表L中子表的记录,使枢轴记录到位,并返回其所在位置
//此时在他之前的记录均不大于他,在后面的记录均不小于他
int Partition(SqList *L,int low,int high)
{
	int pivotkey;                          //用子表的第一个记录做枢轴记录
	pivotkey=L->r[low];					//从表的两端交替向中间扫描
	while(low<high)
	{
		while(low<high && L->r[high]>=pivotkey)
			high--;
		swap(L,low,high);                 //将比枢轴记录小的记录交换到低端
		while(low<high && L->r[low]<=pivotkey)
			low++;
		swap(L,low,high);                      //将比枢轴记录大的记录交换到高端
	}
	return low;              //返回枢轴所在位置
}//详见如下图

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

      9.9.2快速排序复杂度分析
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

      9.9.3快速排序优化
            1.优化选取枢轴
                  1.为啥要优化选取枢轴?
在这里插入图片描述
在这里插入图片描述

                  2.三数取中法:

在这里插入图片描述
在Partition函数中的第3行与第4行之间加上如下代码:

3 int Pivotkey;
	int m=low+(high-low)/2;           //计算数组中间的元素的下标
	if(L->r[low]>L->r[high])
		swap(L,low,high);        //交换左端与右端,保证左端较小
	if(L->r[m]>L->r[high])
		swap(L,high,m);             //交换中间与右端数据,保证中间较小
	if(L->r[m]>L->r[low])
		swap(L,m,low);               //交换中间与左端数据,保证左端较小
	//此时L.r[low]已经是整个序列做左中右三个关键字的中间值
4 pivotkey=L->r[low];          //还是用子表的第一个记录做枢轴记录
//详见如下图

在这里插入图片描述
                  3.优化不必要的交换:
1.原因如下图:
在这里插入图片描述
2.详细代码:

//快速排序优化算法
int Partition1(SqList *L,int low,int high)
{
	int pivotkey;                          //用子表的第一个记录做枢轴记录
	//这里省略三数取中的代码
	pivotkey=L->r[low];					//从表的两端交替向中间扫描
	L-r[0]=L->r[high];              //增加1:叫枢轴关键字备份到L->r[0]
	while(low<high)
	{
		while(low<high && L->r[high]>=pivotkey)
			high--;
		L->r[low]=L->r[high];              //增加2:采用的是替换而不是交换
		while(low<high && L->r[low]<=pivotkey)
			low++;
		L->r[high]=L->r[low];        //增加3:采用的是替换而不是交换
	}
	L->r[low]=L->r[0];              //增加4:将枢轴数值替换回L.r[low]
	return low;              //返回枢轴所在位置

在这里插入图片描述

                  4.优化小数组的排序方案
1.原因如下:
在这里插入图片描述
2.详见代码:

#define MAX_LENGTH_INSERT_SORT 7            //数组长度阈值
void QSort(SqList *L,int low,int high)
{
	int pivot;
	if((high-low)>MAX_LENGTH_INSERT_SORT)
	{//当high-low大于常数时用快速排序
		pivot=Partition(L,low,high);        //将原来数组一分为2,算出枢轴pivot
		QSort(L,low,pivot-1);             //对低字表递归排序
		QSort(L,pivot+1,high);            //对高子表递归排序
	}
	else
		InsertSort(L);       //当high-low小于等于常数时用直接插入排序
}//详见如下图

在这里插入图片描述

                  5.优化递归的操作
1.原因如下图:
在这里插入图片描述
于是对QSort进行尾递归操作:

void QSort1(SqList *L,int low,int high)
{
	int pivot;
	if((high-low)>MAX_LENGTH_INSERT_SORT)
	{//当high-low大于常数时用快速排序
		while(low<high)
		{
			pivot=Partition(L,low,high);        //将原来数组一分为2,算出枢轴pivot
			QSort1(L,low,pivot-1);             //对低字表递归排序
			low=pivot+1;              //尾递归			
		}

	}
	else
		InsertSort(L);       //当high-low小于等于常数时用直接插入排序
}//详见如下图

在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值