《数据结构与算法分析》排序算法总结

前言:

      排序算法在本科的时候就学习过冒泡法,也没有想过如何去计算算法的复杂度,现在想想之前所用的排序算法实在是太小儿科了。当时课本上最先进的2路插入排序也是O(n^2)的复杂度,当时还觉得特别麻烦。就是想的太多,做的太少。

在这里所列出来的排序算法都是内部排序算法,并且不包括

我的github:

我实现的代码全部贴在我的github中,欢迎大家去参观。

https://github.com/YinWenAtBIT

插入排序:

思想:

从小到大:

插入第P个数字,假设前面第0至第P-1个数已经是按照从小到大排序。

那么只要找到第P个数字在前P-1个数字中的位置即可。

 实现的方式是从P-1个数字的最后一个数字开始寻找,如果P小于最后一个数字,则最后一个数字往后挪一位。

直到找到P的位置,再讲P放入空出来的位置上。

复杂度与稳定性

       由于嵌套循环的每一个都要发生N次迭代,因此时间复杂度为O(N^2)。最好的情况下,如果输入数据已经是预排序的,那么每一次循环只需要一次对比,那么复杂度是O(N)。插入排序是稳定的排序。

编码实现:


/*插入排序*/
void InsertionSort(ElementType A[], int N)
{
	int i,j;
	ElementType temp;

	for(i=1; i<N; i++)
	{
		temp = A[i];

		for(j=i; j>0; j--)
		{
			if(temp <A[j-1])
				A[j] = A[j-1];
			else
				break;
		}
		A[j] = temp;
	}
}

希尔排序:

思想:

从小到大:

希尔排序的原理与插入排序基本相同,不同之处在于希尔排序使用的比较间隔不同与插入排序,插入排序只使用1作为比较间隔。

希尔排序的比较间隔从大到小,最后为1。

希尔排序的比较间隔有许多的设置方法,发明人使用的为1,2,4,8这样两倍的间隔。现在最好的间隔为Sedgewick提出的增量序列1,5,9,41,109这样的序列。在这里我的实现就使用的这样的序列。

复杂度与稳定性

      使用希尔排序最坏情况为O(N^2),Sedgewick的增量序列的下界为O(N^(4/3)),平均时间为O(N^(7/6))。希尔排序不是稳定的排序。

编码实现:

/*希尔排序*/
void ShellSort(ElementType A[], int N)
{
	/*Sedgewick序列*/
	int Sedgewick[5] = {109, 41, 19, 5, 1};
	
	int i, j;
	int Index, Increament;
	ElementType temp;

	for(Index=0; Index<5; Index++)
		if(Sedgewick[Index] < N)
			break;
	if(Index == 5)
		return;

	for(; Index<5; Index++)
	{
		Increament = Sedgewick[Index];
		for(i=Increament; i<N; i++)
		{
			temp = A[i];
			for(j = i; j>Increament-1; j--)
			{
				if(temp < A[j-Increament])
					A[j] = A[j-Increament];
				else
					break;
			}
			A[j] = temp;
		}
	}
}

堆排序:

思想:

从小到大:

将数据构建成二叉堆,然后依次删除最小值,就可以得到排序的结果。缺点是需要使用额外的空间。

复杂度与稳定性

      构建堆花费时间O(N),每次删除使用O(LogN)时间。一共N次,所以时间复杂度为O(N LogN)。堆排序也是不稳定排序。

编码实现:

/*堆排序,直接调用了之前编写的二叉堆代码*/
void HeapSort(ElementType A[], int N)
{
	int i;
	PriorityQueue H = BuildHeap(A, N);
	for(i=0; i<N; i++)
		A[i] = DeleteMin(H);

	Destroy(H);
}

归并排序:

思想:

从小到大:

归并排序是建立在归并操作上的一种有效的排序算法。该算法是采用分治法(Divide and Conquer)的一个非常典型的应用。首先考虑下如何将二个有序数列合并。这个非常简单,只要从比较二个数列的第一个数,谁小就先取谁,取了后就在新的数列里放入这个数。然后再进行比较,如果有数列为空,那直接将另一个数列的数据依次取出即可。

解决了上面的合并有序数列问题,再来看归并排序,其的基本思路就是将数组分成二组A,B,如果这二组组内的数据都是有序的,那么就可以很方便的将这二组数据进行排序。如何让这二组组内数据有序了?

 

可以将A,B组各自再分成二组。依次类推,当分出来的小组只有一个数据时,可以认为这个小组组内已经达到了有序,然后再合并相邻的二个小组就可以了。这样通过先递归的分解数列,再合并数列就完成了归并排序。

复杂度与稳定性

      它的最坏情况下时间复杂度为O(N LogN),属于速度很快的排序了,但是缺点是也需要一个同样大小的辅助数组。归并排序是稳定的排序算法。

编码实现:

在这里我把归并排序分成了3个部分,启动部分建立一个辅助的数组,然后启动排序的真正部分Msort,并且提取出了Merge函数。

/*归并排序*/
void Merge(ElementType A[], ElementType TmpArray[], int Lpos, int Rpos, int RightEnd)
{
	int i, LeftEnd, Num, TmpPos;
	LeftEnd = Rpos -1;
	Num = RightEnd - Lpos +1;
	TmpPos = Lpos;

	while(Lpos <= LeftEnd && Rpos <= RightEnd)
	{
		if(A[Lpos] < A[Rpos])
			TmpArray[TmpPos++] = A[Lpos++];
		else
			TmpArray[TmpPos++] = A[Rpos++];
	}

	/*复制剩下的数据*/
	while(Lpos <= LeftEnd)
		TmpArray[TmpPos++] = A[Lpos++];

	while(Rpos <= RightEnd)
		TmpArray[TmpPos++] = A[Rpos++];

	/*拷贝回原来的数组*/
	for(i =0; i<Num; i++, RightEnd--)
	{
		A[RightEnd] = TmpArray[RightEnd];
	}
}



void Msort(ElementType A[], ElementType TmpArray[], int Left, int Right)
{
	int Center;
	if(Left < Right)
	{
		Center = (Left+Right)/2;
		Msort(A, TmpArray, Left, Center);
		Msort(A, TmpArray, Center+1, Right);
		Merge(A, TmpArray, Left, Center+1, Right);
	}
}



void MergeSort(ElementType A[], int N)
{
	ElementType * TmpArray = (ElementType *)malloc(N*sizeof(ElementType));
	if(TmpArray == NULL)
	{
		fprintf(stderr, "not enough memory\n");
		exit(1);
	}

	Msort(A, TmpArray, 0, N-1);
	free(TmpArray);
}

快速排序:

思想:

从小到大:

快速排序是C.R.A.Hoare于1962年提出的一种划分交换排序。它采用了一种分治的策略,通常称其为分治法(Divide-and-ConquerMethod)。

 该方法的基本思想是:

 1.先从数列中取出一个数作为基准数。

 2.分区过程,将比这个数大的数全放到它的右边,小于或等于它的数全放到它的左边。

 3.再对左右区间重复第二步,直到各区间只有一个数(这一步可以提前停止,在只有3-5个数的时候使用插入排序更快)。

虽然算法描述起来挺简单的,但是里面有许许多多的细节,要解决这些坑,还是得花不少的功夫。

1.选择哪一个数作为基准

书上提供的方法我觉得基本上时最优了。为了避免遇上已经排序的序列,选择第一个数是肯定不可取的(这样导致所有的数据都分在一边,另一边没有数,时间复杂度变成O(N^2))。然后直接选择中间的数?这样同样难免遇上最大值或者最小值。由于在选择哪一个数作为基准并不花费太多的功夫,并且可以大大改善算法时间的稳定性,所以在这里可以选择复杂一点的做法。


书上提供的做法是,提取第一个,最后一个,已经正中间的数,让它们从小到大排列,然后选择中间的数作为基准。然后,把中间的基准数和倒数第二个数交换,这样做的目的是为了第二步的方便。接下来的叙述就会说明这一点。


2.如何交换:

交换的方式为:

使用两个指针,一个指向数组开始处,另一个指向末尾,然后两个指针开始运动,指向开头的指针在遇到比基准大的数时停下来。指向末尾的指针在遇到比基准小的数停下来,然后交换所指的数,重复这个过程,直到最初指向末尾的指针跑到了指向开始指针的前面为止。这样就完成了将数据划分两部分的工作。最后再把基准(倒数第二个数)与指向开头指针所指的数交换(这个数大于基准,它前面的数都小于基准)。这样基准就处于中间。然后可以对左右两部分再进行快排。这里把基准放在末尾的原因是,作为指向开头指针的提醒处,避免开头指针一直滑动以至于超过数组界限。(这里对于等于基准的数,两个指针都停下来)。

复杂度与稳定性

      它的最坏情况下时间复杂度为O(N^2),但是优化了选择基准的方式,一般难以遇上。平均速度为O(N LogN)属于速度很快的排序了,并且不需要额外的空间。快速排序是不稳定的排序算法。

编码实现:

在这里我把快速排序分成了4个部分,启动部分启动快排,然后选择基准部分提取出来作为单独的函数,交换作为单独的函数。以及最为核心的滑动交换,并且对子数组进行快排作为一部分。

/*快速排序*/

void Swap(ElementType *A, ElementType *B)
{
	ElementType temp;
	temp = *A;
	*A = *B;
	*B = temp;
}

ElementType Median3(ElementType A[], int Left, int Right)
{
	int Center = (Left + Right) / 2;

	/*从左到右,从小到大排列*/
	if(A[Left] > A[Center])
		Swap(&A[Left], &A[Center]);
	if(A[Left] > A[Right])
		Swap(&A[Left], &A[Right]);
	if(A[Center] > A[Right])
		Swap(&A[Center], &A[Right]);

	Swap(&A[Center], &A[Right-1]);
	return A[Right-1];
}

#define Cutoff (3)

void Qsort(ElementType A[], int Left, int Right)
{
	int i,j;
	ElementType Pivot;
	
	Pivot = Median3(A, Left, Right);

	if(Left + Cutoff <Right)
	{
		i = Left;
		j = Right-1;
		while(1)
		{
			while(A[++i] < Pivot);
			while(A[--j] > Pivot);
			if(i < j)
				Swap(&A[i], &A[j]);
			else
				break;
		}
		/*将枢纽元放回中间,此时枢纽元左边的数据都比它小
		右边的数据都比它大,再对左右数据排序即可*/
		Swap(&A[i], &A[Right-1]);

		Qsort(A, Left, i-1);
		Qsort(A, i+1, Right);
	}
	else
		/*少于3个数据就直接使用插入排序更快*/
		InsertionSort(A+Left, Right-Left+1);
}

void QuickSort(ElementType A[], int N)
{
	Qsort(A, 0, N-1);
}

总结:

排序算法我是提前学习了,隔了两个星期之后才来亲手实现。最基本的排序算法,插入,希尔,以及堆排序,由于方法简单,没有再参考课本就写了出来。归并排序稍微复习了一下原理以及伪代码,也写了出来。最后的快排真是一点都写不出来了。因为它的思想虽然简单,实现上的细节确实不容易,只能在重新学习一遍之后,才写出了快排到算法。

核心还是要理解这些算法,只有在理解了算法之后,才能做到自己想什么时候写出来就能写出来。而不是简单的默写。


你可能感兴趣的:(《数据结构与算法分析》排序算法总结)