七大排序算法详解(思路+源代码)C语言,数据结构

目录

排序分类:

1)冒泡排序

2)简单选择排序

3)直接插入排序

4)希尔排序 

5)堆排序

6)归并排序

①递归实现

②非递归实现

7)快速排序

①快速排序算法

 ②快速排序优化

8)总结


排序分类:

1)按主要操作

内排序:插入排序,交换排序,选择排序,归并排序

外排序:是大文件的排序,即待排序的记录存储在外存储器上,待排序的文件无法一次装入内存,需要在内存和外部存储器之间进行多次数据交换,以达到排序整个文件的目的。

2)按算法复杂度

简单算法:冒泡排序,简单选择排序,直接插入排序

改进算法:希尔排序,堆排序,归并排序,快速排序

准备:会用到的结构:

typedef struct
{
	int r[MAXSIZE+1];	/* 用于存储要排序数组,r[0]用作哨兵或临时变量 */
	int length;			/* 用于记录顺序表的长度 */
}SqList;

/* 交换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; 
}

void print(SqList L)
{
	int i;
	for(i=1;i

1)冒泡排序

基本思想:

两两比较相邻的关键字,如果反序则交换,知道没有反序的记录为止。

初级:逐个交换,且每次只对该关键字有效。算法效率较低。

冒泡:j从后往前前循环,逐个比较,将较小值交换到前面,知道最后找到最小值放置在了第一的位置。排序的过程中会帮助移动其他关键字。

优化:当序列已经有序,可不必再重复排序,减少比较的次数。可增加一个标记变量flag来实现。可以避免已经有序的情况下无意义的循环判断。

代码:

/* 对顺序表L作交换排序(冒泡排序初级版) */
void BubbleSort0(SqList *L)
{ 
	int i,j;
	for(i=1;ilength;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]的值 */
			}
		}
	}
}

/* 对顺序表L作冒泡排序 */
void BubbleSort(SqList *L)
{ 
	int i,j;
	for(i=1;ilength;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]的值 */
			}
		}
	}
}

/* 对顺序表L作改进冒泡算法 */
void BubbleSort2(SqList *L)
{ 
	int i,j;
	Status flag=TRUE;			/* flag用来作为标记 */
	for(i=1;ilength && flag;i++) /* 若flag为true说明有过数据交换,否则停止循环 */
	{
		flag=FALSE;				/* 初始为False */
		for(j=L->length-1;j>=i;j--)
		{
			if(L->r[j]>L->r[j+1])
			{
				 swap(L,j,j+1);	/* 交换L->r[j]与L->r[j+1]的值 */
				 flag=TRUE;		/* 如果有数据交换,则flag为true */
			}
		}
	}
}


//冒泡排序
void BubbleSort(int* arr, int n)
{
	int end = n;
	while (end)
	{
		int flag = 0;
		for (int i = 1; i < end; ++i)
		{
			if (arr[i - 1] > arr[i])
			{
				int tem = arr[i];
				arr[i] = arr[i - 1];
				arr[i - 1] = tem;
				flag = 1;
			}
		}
		if (flag == 0)
		{
			break;
		}
		--end;
	}
}

 时间复杂度:

最优:排序表本身有序:O(n)

最坏:排序表逆序:O(n2)

2)简单选择排序

基本思想:

选择排序图:(不是简单选择排序)

就是通过n-i次关键字间的比较,从n-i-1个记录中选出关键字最小的记录,并和第i(1<=i<=n)个记录交换。

代码:

/* 对顺序表L作简单选择排序 */
void SelectSort(SqList *L)
{
	int i,j,min;
	for(i=1;ilength;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]的值 */
	}
}


//选择排序
void SelectSort(int* arr, int n)
{
	//保存参与单趟排序的第一个数和最后一个数的下标
	int begin = 0, end = n - 1;
	while (begin < end)
	{
		//保存最大值的下标
		int maxi = begin;
		//保存最小值的下标
		int mini = begin;
		//找出最大值和最小值的下标
		for (int i = begin; i <= end; ++i)
		{
			if (arr[i] < arr[mini])
			{
				mini = i;
			}
			if (arr[i] > arr[maxi])
			{
				maxi = i;
			}
		}
		//最小值放在序列开头
		swap(&arr[mini], &arr[begin]);
		//防止最大的数在begin位置被换走
		if (begin == maxi)
		{
			maxi = mini;
		}
		//最大值放在序列结尾
		swap(&arr[maxi], &arr[end]);
		++begin;
		--end;
	}
}

时间复杂度:

简单选择排序的特点:交换移动数据次数相当少。

无论是最优还是最差,其比较次数都是一样多,第i趟排序需要进行n-i次关键字的比较。为n(n-1)/2次。对于交换次数,最好为零,最差为n-1.

时间复杂度为o(n2)

3)直接插入排序

基本操作:

 1.从第一个元素开始,该元素可以认为已经被排序
2.取下一个元素tem,从已排序的元素序列从后往前扫描
3.如果该元素大于tem,则将该元素移到下一位
4.重复步骤3,直到找到已排序元素中小于等于tem的元素
5.tem插入到该元素的后面,如果已排序所有元素都大于tem,则将tem插入到下标为0的位置
6.重复步骤2~5

将一个记录插入到已经排好的有序表中,从而得到一个新的,记录数增一的有序表。

代码:


/* 对顺序表L作直接插入排序 */
void InsertSort(SqList *L)
{ 
	int i,j;
	for(i=2;i<=L->length;i++)//从二开始,表示我们假设r[1]已经放好位置了,后面的其实就是插入他的左侧还是右侧的问题
	{
		if (L->r[i]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]; /* 插入到正确位置 */
		}
	}
}



void InsertSort(int* arr, int n)
{
	for (int i = 0; i < n - 1; ++i)
	{
		//记录有序序列最后一个元素的下标
		int end = i;
		//待插入的元素
		int tem = arr[end + 1];
		//单趟排
		while (end >= 0)
		{
			//比插入的数大就向后移
			if (tem < arr[end])
			{
				arr[end + 1] = arr[end];
				end--;
			}
			//比插入的数小,跳出循环
			else
			{
				break;
			}
		}
		//tem放到比插入的数小的数的后面
		arr[end  + 1] = tem;
		//代码执行到此位置有两种情况:
		//1.待插入元素找到应插入位置(break跳出循环到此)
		//2.待插入元素比当前有序序列中的所有元素都小(while循环结束后到此)
	}
}

时间复杂度:

空间上只需要一个记录的辅助空间。时间复杂度为o(n2),但是直接插入排序比冒泡和简单选择排序的性能要好

4)希尔排序 

思路:

对直接插入排序改进

如何让待排序的记录个数较少呢?进行分组。分割成若干个子序列,此时每个子序列待排序的记录个数就比较少了,然后在这些子序列内分别进行直接插入排序,当整个序列都基本有序时,再对全体记录进行一次直接插入排序。

基本有序:就是小的关键字基本在前面,大的基本在后面,不大不小的在中间。

策略:将相距某个增量的记录组成一个子序列,这样才能保证在子序列内分别进行直接插入排序后的得到的结果是基本有序而不是局部有序。

 代码:

/* 对顺序表L作希尔排序 */
void ShellSort(SqList *L)
{
	int i,j,k=0;
	int increment=L->length;
	do
	{
		increment=increment/3+1;/* 增量序列 */
		for(i=increment+1;i<=L->length;i++)
		{
			if (L->r[i]r[i-increment])/*  需将L->r[i]插入有序增量子表 */ 
			{ 
				L->r[0]=L->r[i]; /*  暂存在L->r[0] */
				for(j=i-increment;j>0 && L->r[0]r[j];j-=increment)
					L->r[j+increment]=L->r[j]; /*  记录后移,查找插入位置 */
				L->r[j+increment]=L->r[0]; /*  插入 */
			}
		}
		printf("	第%d趟排序结果: ",++k);
		print(*L);
	}
	while(increment>1);//增量为一时停止循环

}

//希尔排序
void ShellSort(int* arr, int n)
{
	int gap = n;
	while (gap>1)
	{
		//每次对gap折半操作
		gap = gap / 2;
		//单趟排序
		for (int i = 0; i < n - gap; ++i)
		{
			int end = i;
			int tem = arr[end + gap];
			while (end >= 0)
			{
				if (tem < arr[end])
				{
					arr[end + gap] = arr[end];
					end -= gap;
				}
				else
				{
					break;
				}
			}
			arr[end + gap] = tem;
		}
	}
}

我们在完成一次循环时,已经整个序列基本有序:

希尔排序的精华所在:他将关键字较小的记录,不是一步一步的往前挪动,而是跳跃式的往前移。

时间复杂度:

O(n 的1.5次方),需要注意的是:增量序列的最后一个增量值必须等于一才行。

5)堆排序

 堆:

堆是具有下列性质的完全二叉树

每个结点的值都大于或者等于其左右孩子结点的值,称为大顶堆

七大排序算法详解(思路+源代码)C语言,数据结构_第1张图片

或者每个节点的值都小于或者等于左右孩子结点的值,称为小顶堆

七大排序算法详解(思路+源代码)C语言,数据结构_第2张图片

 注意:

根结点一定是堆中所有结点最大(小)者。

较大(小)的结点靠近根节点(不绝对)。

 至于为什么i要小于n/2:

七大排序算法详解(思路+源代码)C语言,数据结构_第3张图片

二叉树的性质五:一颗完全二叉树,如果i等于1,则结点i是二叉树的根,无双亲;如果i>1,则双亲是结点【i/2].那么对于有n个结点的二叉树而言,他的i值自然就是小于等于【n/2】了。

 七大排序算法详解(思路+源代码)C语言,数据结构_第4张图片

七大排序算法详解(思路+源代码)C语言,数据结构_第5张图片

基本思想:

如果可以做到每次在选择到最小记录的同时,并根据比较结果对其他记录做出相应的调整,那排序的效率就会很高。堆排序是对简单选择排序的一种改进。

 将排序的序列构造成一个大顶堆。此时,整个序列的最大值就是堆顶的根节点。将他移走(其实就是将其与堆数组的末尾元素交换,此时末尾元素就是最大值),然后将剩余的n-1个序列重新构造成一个堆,这样就会得到n个元素的次大值。如此反复执行,就可以得到一个有序序列了。

也就是说,我们一开始吧排序数据构建成一个大顶堆,然后每次找到一个较大值进行一次排序交换时,要让剩余的数据仍旧曼珠大顶堆的结构。

需要解决的问题:
1)如何由一个无序序列构建成一个堆?

2)如何在输出堆顶元素后,调整剩余元素成为一个新的堆?

代码如下:


/* 已知L->r[s..m]中记录的关键字除L->r[s]之外均满足堆的定义, */
/* 本函数调整L->r[s]的关键字,使L->r[s..m]成为一个大顶堆 */
void HeapAdjust(SqList *L,int s,int m)
{ 
	int temp,j;
	temp=L->r[s];
//从最后一个非叶子结点开始,以此向前调整
	for(j=2*s;j<=m;j*=2) /* 沿关键字较大的孩子结点向下筛选 */
//为什么j要从2*s开始,且按照j*=2递增:还是性质五:当前结点序号是s,其左孩子的序号一定是2s,游孩子的序号一定是2s+1,他们的孩子当然也是以2的位数序号增加。
	{
		if(jr[j]r[j+1])
			++j; /* j为关键字中较大的记录的下标 */
		if(temp>=L->r[j])
			break; /* rc应插入在位置s上 */
		L->r[s]=L->r[j];
		s=j;
	}
	L->r[s]=temp; /* 插入 */
}

/*  对顺序表L进行堆排序 */
void HeapSort(SqList *L)
{
	int i;
	for(i=L->length/2;i>0;i--) /*  把L中的r构建成一个大顶堆 */  //特点:他们都是有孩子的结点
/*我们所谓的将待排序的序列构建成为一个大顶堆,其实就是从下往上,从右到左,将每个非终端结点(非叶节点)当作根节点,将其和其子树调整为大顶堆。*/
		 HeapAdjust(L,i,L->length);

	for(i=L->length;i>1;i--)
	{ 
		 swap(L,1,i); /* 将堆顶记录和当前未经排序子序列的最后一个记录交换 */
		 HeapAdjust(L,1,i-1); /*  将L->r[1..i-1]重新调整为大顶堆 */
	}
}

时间复杂度:
构建整个堆的时间复杂度为o(n)

在正式排序时,重建堆的时间复杂度为o(nlogn)

总体来说:O(nlogn)且堆排序堆原始记录的排序状态不敏感,所以无论好坏平均都是相同的。

注意:由于初始构建堆所需的比较次数较多,因此,不适合待排序序列个数较少的情况。

声明:如果不够了解堆的构造可参考他人博客进行了解(仅学习使用)

6)归并排序

基本原理:

利用归并的思想实现的排序方法。

假设初始序列含有n个记录,则可以看成是n个有序的子序列,每个子序列的长度为一,然后两两归并,得到【n/2】([x]表示不小于x的最小整数)个长度为2或者一的有序子序列;再两两归并........;

如此重复,直到得到一个长度为n的有序序列为止,这种排序方法称为2路归并排序

①递归实现

下图为递归代码流程:(帮助理解)

七大排序算法详解(思路+源代码)C语言,数据结构_第6张图片


/* 将有序的SR[i..m]和SR[m+1..n]归并为有序的TR[i..n] */
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]r,L->r,1,L->length);
}

归并排序(递归)复杂度分析:

一趟归并需要将sr【1】到sr【n】中相邻的长度为h的有序序列进行两两归并,耗费O(n),而由完全二叉树的深度可知,整个归并需要进行【log2n】次

总的时间复杂度为:O(nlogn)

特点:我们可以发现Merge函数中if(sr[i]

②非递归实现

归并排序大量引用了递归,造成了时间和空间上的性能损耗,我们可以将递归转化成迭代,且性能会更高。

做法:从最小的序列开始归并直至完成,不需要像归并的递归算法一样,需要先拆分递归,再归并并退出递归。

代码:

/* 非递归法 */
/* 将SR[]中相邻长度为s的子序列两两归并到TR[] */
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(ilength * sizeof(int));/* 申请额外空间 */
    int k=1;
	while(klength)//不断归并有序序列,注意k值变化
	{
		MergePass(L->r,TR,k,L->length);
		k=2*k;/* 子序列长度加倍 */
		MergePass(TR,L->r,k,L->length);
		k=2*k;/* 子序列长度加倍 */       
	}
}

归并排序(非递归)复杂度分析: 

空间复杂度为O(n)

综上所诉,使用递归排序时,尽量考虑用非递归方法。

7)快速排序

①快速排序算法

快速排序是冒泡排序的升级,都属于交换排序类。即他也是通过不断比较和移动交换来实现排序的,不过他的实现,增大了记录的比较和移动的距离,将关键字较大的记录从前面直接移动到后面,关键字较小的从后面直接移动到前面,从而减少了总的比较次数和移动交换次数。

 基本思想:

通过一趟排序将代排记录分割成独立的两部分,其中一部分记录的关键字均比另一部分记录的关键字小,则可分别对这两部分记录继续进行排序,以达到整个序列有序的目的。

代码如下:


/* 交换顺序表L中子表的记录,使枢轴记录到位,并返回其所在位置 */
/* 此时在它之前(后)的记录均不大(小)于它。 */
//该函数要做的,就是先选取当中的一个关键字,让后想尽办法将他放到一个位置,是的他左边的值都比他小。右边的值比他大,我们将这样的关键字称为枢轴
int Partition(SqList *L,int low,int high)
{ 
	int pivotkey;

	pivotkey=L->r[low]; /* 用子表的第一个记录作枢轴记录 */
	while(lowr[high]>=pivotkey)
			high--;
		 swap(L,low,high);/* 将比枢轴记录小的记录交换到低端 */
		 while(lowr[low]<=pivotkey)
			low++;
		 swap(L,low,high);/* 将比枢轴记录大的记录交换到高端 */
	}
	return low; /* 返回枢轴所在位置 */
}

/* 对顺序表L中的子序列L->r[low..high]作快速排序 */
void QSort(SqList *L,int low,int high)
{ 
	int pivot;
	if(lowr[low..high]一分为二,算出枢轴值pivot */
			QSort(L,low,pivot-1);		/*  对低子表递归排序 */
			QSort(L,pivot+1,high);		/*  对高子表递归排序 */
	}
}

/* 对顺序表L作快速排序 */
void QuickSort(SqList *L)
{ 
	QSort(L,1,L->length);
}

Partition()函数,其实就是将选取的pivotkey不断交换,将比他小的换到他的左边,比它大的换到他的右边,他也在交换中不断更改自己的位置,直到完全满足这个要求为止。

复杂度分析:

快排的空间复杂度为O(logn)

时间复杂度O(nlogn)

注意:由于关键字的比较和交换是跳跃进行的,所以快排是一种不稳定的排序方法。

 ②快速排序优化

1)优化选取枢轴:
如果我们选取的pivotkey是处于整个序列的中间位置,那么我们可以将整个序列分成小数集合和大数集合了。但是只是如果。

上述代码的pivotkey=L->r[low]变成了一个潜在的性能瓶颈,排序速度的快慢取决于L.r[1]的关键字处在整个序列的位置,太大或者太小都会影响性能。因为在现实中,待排序的系列极有可能是基本有序的,此时,总是固定选取第一个关键字作为首个枢轴变得不合理。

改进方法:

三数取中法:即去三个关键字先进行排序,将中间数作为枢轴,一般是取左端,右端和中间三个数。


	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);		/* 交换中间与左端数据,保证左端较小 */
//此时r.low已经是整个序列左中右三个关键字的中间值
	
	pivotkey=L->r[low]; /* 用子表的第一个记录作枢轴记录 */

 注意:三数去中对小数组来说有很大的概率选择一个比较好的枢轴,但对于非常大的待排序的序列来说不能保证。因此还有一个九数取中,他先从数组中分三次取样,每次去三个数,三个样品中各取出中数。

2)优化不必要的交换

/* 快速排序优化算法 */
int Partition1(SqList *L,int low,int high)
{ 
	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);		/* 交换中间与左端数据,保证左端较小 */
//此时r.low已经是整个序列左中右三个关键字的中间值
	
	pivotkey=L->r[low]; /* 用子表的第一个记录作枢轴记录 */
	L->r[0]=pivotkey;  /* 将枢轴关键字备份到L->r[0] */
	while(lowr[high]>=pivotkey)
			high--;
		 L->r[low]=L->r[high];
		 while(lowr[low]<=pivotkey)
			low++;
		 L->r[high]=L->r[low];
	}
	L->r[low]=L->r[0];
	return low; /* 返回枢轴所在位置 */
}

 事实上,我们将pivotkey备份到L.r[0]中,然后在之前是swap时,只是替换的工作,最终当low与high会合,即找到了枢轴的位置时,再将Lr[0]的数值赋值回L.r[low].

3)优化小数组时的排序方案

如果数组非常小,其实快速排序反而不如直接插入排序更快(直接插入是简单排序中性能最好的)

 因为快排用到了递归。

因此我们需要改进一下Qsort()函数:

void QSort1(SqList *L,int low,int high)
{ 
	int pivot;
	if((high-low)>MAX_LENGTH_INSERT_SORT)
	{
		pivot=Partition1(L,low,high); /*  将L->r[low..high]一分为二,算出枢轴值pivot */
		QSort1(L,low,pivot-1);		/*  对低子表递归排序 */
		QSort1(L,pivot+1,high);		/*  对高子表递归排序 */
	}
	else
		InsertSort(L);
}
/* 对顺序表L作快速排序 */
void QuickSort1(SqList *L)
{ 
	QSort1(L,1,L->length);
}

 我们增加了一个判断:当high-low不大于某个常数时,就用直接插入排序,这样可以保证最大化的利用两种排序的优势。

4)优化递归操作

对Qsort实施尾递归优化:
 


/* 尾递归 */
void QSort2(SqList *L,int low,int high)
{ 
	int pivot;
	if((high-low)>MAX_LENGTH_INSERT_SORT)
	{
		while(lowr[low..high]一分为二,算出枢轴值pivot */
			QSort2(L,low,pivot-1);		/*  对低子表递归排序 */
			low=pivot+1;	/* 尾递归 */
		}
	}
	else
		InsertSort(L);
}

/* 对顺序表L作快速排序(尾递归) */
void QuickSort2(SqList *L)
{ 
	QSort2(L,1,L->length);
}

 当我们将if改成while后,因为第一次递归以后,变量low就没有用处了,所以可以将Pivot赋值给low,再循环后,来一次Parttiton(L,low,high),其效果等同于QSort(L,pivot+1,high);结果相同,但因为采用迭代而不是递归的方法,可以缩减堆栈深度。

不同方法可借鉴他人的博客

快速排序

8)总结

七大排序算法详解(思路+源代码)C语言,数据结构_第7张图片

 七大排序算法详解(思路+源代码)C语言,数据结构_第8张图片

你可能感兴趣的:(算法---基础算法,排序算法,算法,数据结构)