数据结构与算法:七种排序算法总结(冒泡排序、选择排序、直接插入排序、希尔排序、堆排序、归并排序、快速排序)

最近复习了一些排序算法,在这做一个总结。本文中部分内容参考博客:http://blog.csdn.net/morewindows/article/details/7961256这个博客中介绍的算法等知识详细易理解,以后可以多参考学习。

总结的排序算法包括:冒泡排序、选择排序、直接插入排序、希尔排序、堆排序、归并排序以及快速排序。下面依次介绍。(本文全部排序算法源代码可下载,下载链接:http://download.csdn.net/detail/dabusideqiang/7180021)

1、冒泡排序

通常在学习编程语言时,都会介绍一种排序算法作为例子,而这个算法一般就是冒泡排序。主要是因为冒泡排序思路简单,容易理解。冒泡排序是一种交换排序,基本思想是:两两比较相邻数据的值,如果反序则交换,直到没有反序的数据为止。

冒泡排序步骤如下:(设数组长度为N,以从小到大为例)。

1.比较相邻的前后二个数据,如果前面数据大于后面的数据,就将二个数据交换。

2.这样对数组的第0个数据到N-1个数据进行一次遍历后,最大的一个数据就到数组第N-1个位置。

3N=N-1,如果N不为0就重复前面二步,否则排序完成。

对应代码如下:

/**
* 冒泡排序初始程序
* @param [in] a:数组名
* @param [in] n:数组大小
* @return
*/ 
void BubbleSort1(int a[], int n)  
{  
	for (int i = 0; i < n; i++)  
		for (int j = 1; j < n - i; j++)  
			if (a[j - 1] > a[j])  //若顺序不合适则交换数据
				swap(a[j - 1], a[j]);  
}


上述的代码还可以进一步优化,试想一下,如果我们待排序的是{2,1,3,4,5,6,7,8,9,10},也就是说,只有前面两个需要排序,后面已经排好序。这样只需要将2和1交换就完成排序了,上述的代码会从2到10一直循环下去,这样就多余了。为了解决这个问题,我们设置一个标志位:bSwap。在循环时,一轮中只要发生过交换就置bSwap为true,继续下一轮循环。如果一轮中没有发生交换,则置bSwap为false,排序完成。

对应代码如下:

/**
* 冒泡排序改进程序
* @param [in] a:数组名
* @param [in] n:数组大小
* @return
*/  
void BubbleSort2(int a[], int n)  
{    
	bool bSwap=true;  
	while (bSwap)  //bSwap为false退出循环
	{  
		bSwap = false;  
		for (int i = 1; i < n; i++)  
		{
			if (a[i - 1] > a[i])  
			{  
				swap(a[i - 1], a[i]);  
				bSwap = true;  //有数据交换则置bSwap为true
			}  
		}
		n--;  
	}  
} 


2、选择排序

冒泡排序的思想就是不断地交换,通过交换完成最终的排序。选择排序则是找到合适的关键字时再做交换。简单选择排序法就是通过n-i次关键字间的比较,从n-i+1个记录中选出关键字最小的记录,并和第i个记录交换。

选择排序步骤如下:(设数组为a[0…n-1])

1.初始时,数组全为无序区为a[0..n-1]。令i=0

2.在无序区a[i…n-1]中选取一个最小的元素,将其与a[i]交换。交换之后a[0…i]就形成了一个有序区。

3. i++并重复第二步直到i==n-1。排序完成。

对应代码如下:

/**
* 选择排序程序
* @param [in] a:数组名
* @param [in] n:数组大小
* @return
*/ 
void Selectsort(int a[], int n)
{
	int i, j, nMinIndex;
	for (i = 0; i < n; i++)
	{
		nMinIndex = i; //将当前下标定义为最小值下标
		for (j = i + 1; j < n; j++)
		{
			if (a[j] < a[nMinIndex])//如果有小于当前最小值的数据
				nMinIndex = j;//将此下标赋给 nMinIndex
		}
		if(i!=nMinIndex) 
			swap(a[i], a[nMinIndex]); //若找到最小值则交换数据
	}
}

三、直接插入排序

直接插入排序的基本操作是将一个数据插入到已经排好序的有序表中,从而得到一个新的、数据数增1的有序表。就相当于打牌时理牌的方法,将不合适位置的数据插入到合适的位置排好顺序。

具体步骤如下:(数组为a[0…n-1])

1、初始时,a[0]自成1个有序区,无序区为a[1..n-1]。令i=1

2、将a[i]并入当前的有序区a[0…i-1]中形成a[0…i]的有序区间。

3、i++并重复第二步直到i==n-1。排序完成。

代码如下:

/**
* 直接插入排序程序
* @param [in] a:数组名
* @param [in] n:数组大小
* @return
*/ 
void InsertSort(int a[], int n)
{	
	int i,j;
	for(i=1; i < n; i++)
	{
		if(a[i] < a[i-1])//a[i]=0 && a[j] > temp; j--)//将比a[i]大的数据后移
				a[j+1] = a[j];
			a[j+1] =temp;//插入到正确位置
		}
	}
}


四、希尔排序

优秀排序算法的首要条件就是速度,上述的三种常用排序算法时间复杂度都是O(n2),接下来我们介绍算法时间复杂度为O(nlogn)的几种排序算法。首先介绍希尔排序,希尔排序是D.L.Shell于1959年提出来的,是突破O(n2)时间复杂度的第一批算法之一。

希尔排序其实就相当于对直接插入排序的改进,直接插入排序在数据本身基本有序或数据较少时效率很高。因此就可以对原本数据比较多的待排序序列进行分组,分成若干个子序列,然后分别对这些子序列进行直接插入排序,当整个序列基本有序时,再对整个序列进行一次直接插入排序。

注意:这里说的基本有序的意思是:小的数据基本在前面,大的数据基本在后面,不大不小的基本在中间。像{2,1,3,6,4,7,5,8,9}这样可以称为基本有序,而{1,5,9,3,7,8,2,4,6}这样就不是基本有序。因此如何分组能达到基本有序很重要。这里采取跳跃分割策略:将相距某个“增量”的数据组成一个子序列,然后依次缩减增量,直至为1。这样才能保证在子序列内分别进行直接插入排序后得到的结果基本有序而不是局部有序。

 

代码如下:

/**
* 希尔排序程序
* @param [in] a:数组名
* @param [in] n:数组大小
* @return
*/ 
void ShellSort(int a[], int n)
{
	int i,j;
	int increment=n;
	do
	{
		increment = increment/3+1;//增量
		for(i=increment; i < n; i++)//分组进行直接插入排序,
		{
			if(a[i] < a[i-increment])
			{
				int temp = a[i];
				for(j=i-increment;j>=0 && a[j] > temp; j-=increment)
					a[j+increment]=a[j];
				a[j+increment] = temp;
			}
		}
	}while(increment > 1);//直到增量为1结束
}

注:希尔排序的关键并不是随便分组后各自排序,而是将相隔某个增量的数据组成子序列,实现跳跃式的移动,提高排序效率。在这里增量的选取很关键。目前也没有一个最好的增量序列。但是增量序列的最后一个增量值必须等于1。


五、堆排序

再说堆排序之前,要先介绍堆结构。堆是具有下列性质的完全二叉树:每个结点的值都大于或等于其左右孩子结点的值,称为大顶堆(图1);或者每个结点的值小于或等于其左右孩子结点的值,称为小顶堆(图2)。

数据结构与算法:七种排序算法总结(冒泡排序、选择排序、直接插入排序、希尔排序、堆排序、归并排序、快速排序)_第1张图片

                                   图1 大顶堆                                                  图2 小顶堆

 

由堆的性质可知,根结点一定是堆中最大或最小者。并且根据二叉树的性质。若结点为i,则该结点的左右子树为2*i+1和2*i+2;

堆排序就是利用堆进行排序的方法。它的基本思想是,将待排序的序列构造成一个大顶堆。此时,整个序列的最大值就是堆顶的根结点。将它移走(其实就是将其与堆数组的末尾元素交换,此时末尾元素就是最大值),然后将剩余的n-1个序列重新构造成一个堆,这样就会得到n个元素中的次小值。如此反复执行,便能得到一个有序序列了。

排序步骤:

1、将待排序的序列构建成一个大顶堆。

2、逐步将每个最大值的根结点与末尾元素交换,并且再调整其成为大顶堆


对应代码如下:

/************堆排序******************/
/**
* 堆调整函数
* 将a[s..m]调整成为大顶堆
* @param [in] a:数组名
* @param [in] s:待调整序列的起始位置
* @param [in] n:待调整序列的结束位置
* @return
*/ 
void HeapAdjust(int a[], int s, int m)
{
	int temp,j;
	temp=a[s];
	for(j=2*s+1;j<=m ;j=2*j+1)//从上向下,从左到右,将每个非叶结点及其子树调整为大顶堆
	{
		if(j= a[j]) //若根节点大于左右子树,则跳出
			break;
		a[s]=a[j];//若根节点小于左右子树,则跟较大值交换
		s=j;
	}
	a[s]=temp; // 交换数据
}

/**
* 堆排序程序
* 排序过程:1、将待排序的序列构建成一个大顶堆。
*           2、逐步将每个最大值的根结点与末尾元素交换,并且再调整其成为大顶堆
* @param [in] a:数组名
* @param [in] n:数组大小
* @return
*/ 
void HeapSort(int a[], int n)
{
	int i;
	for(i=n/2-1; i>=0; i--)//将数组a构建成大顶堆
		HeapAdjust(a,i,n);

	for(i=n-1;i>0;i--)//
	{
		swap(a[0],a[i]);//将堆顶数据与当前未排序子序列的最后一个数据交换
		HeapAdjust(a,0,i-1);//将a[0..i-1]调整为大顶堆
	}
} 

六、归并排序

讨论归并排序之前先介绍下如何将两个有序序列合并。基本思想是:依次比较两个序列的数据,将较小的数据取出存放到一个新的序列中。然后去掉取出的数据再依次比较剩下的数据。如果有一个序列为空,则直接将另一个序列依次存放到新序列中即可。

代码如下:

/**
* Merge函数:将有序的两个序列a[first..mid]和a[mid+1..last]
* 合并为一个有序序列temp[first..last]
* @param [in] a:待合并数组
* @param [in] temp:临时数组存放归并结果
* @param [in] first:起始位置
* @param [in] mid:中间位置
* @param [in] last:结束位置
* @return
*/ 
void Merge(int a[], int temp[],int first, int mid, int last)
{
	int i=first,j=mid+1;
	int m=mid,n=last;
	int k=0;
	while(i<=m && j<=n)
	{
		if(a[i] <= a[j])//依次比较两个序列的数,谁小取谁,将a中数据从小到大并入temp中
			temp[k++] = a[i++];
		else
			temp[k++] = a[j++];
	}
	while(i <= m) //将剩余的a[i..m]并入到temp中
		temp[k++] = a[i++];
	while(j <= n) //将剩余的a[j..n]并入到temp中
		temp[k++] = a[j++];
	for(i=0;i

归并排序就是利用归并的思想实现的排序方法。它的原理是假设初始序列含有n个数据,则可以看成是n个有序的子序列,每个子序列的长度为1,然后两两归并,得到n/2个长度为2或1的有序子序列;再两两归并,。。。,如此重复,直至得到一个长度为n的有序序列为止。

递归实现代码如下:

/*******************归并排序:递归法***********************/
/**
* mergesort函数:将a[first..last]归并排序为temp[first..last]
* @param [in] a:待排序数组
* @param [in] temp:临时数组存放排序结果
* @param [in] first:起始位置
* @param [in] last:结束位置
* @return
*/ 
void mergesort(int a[],int temp[], int first, int last)
{
	if(first = last) //递归结束条件:当分组后只有一个数据时,表示有序,递归结束
		temp[first]=a[first];
	else
	{
		int mid=(first+last)/2;//分组
		mergesort(a,temp,first,mid);//递归将左边归并为有序
		mergesort(a,temp,mid+1,last);//递归将右边归并为有序
		Merge(a,temp,first,mid,last);//合并有序序列
	}
}

/**
* 递归归并排序
* @param [in] a:待排序数组
* @param [in] n:数组大小
* @return
*/ 
void MergeSort(int a[], int n)
{
	int *p=new int[n];//新建临时数组
	if(p==NULL)
		return;
	mergesort(a,p,0,n-1);
	delete[] p;
} 


非递归实现:基本思想是用迭代方法,从最小序列长度开始依次将相邻子序列两两归并。实现过程如下图所示(待排序序列为{50,10,90,30,70,40,80,60,20})

数据结构与算法:七种排序算法总结(冒泡排序、选择排序、直接插入排序、希尔排序、堆排序、归并排序、快速排序)_第2张图片

对应代码如下:

/***************************归并排序:非递归法****************/

/**
* mergesort2函数:将a中相邻长度为s的子序列两两归并
* @param [in] a:待排序数组
* @param [in] temp:临时数组存放归并结果
* @param [in] s:子序列长度
* @param [in] n:数组大小
* @return
*/ 
void mergesort2(int a[], int temp[],int s,int n)
{
	int i=0;
	while(i<=n-2*s)
	{
		Merge(a,temp,i,i+s-1,i+2*s-1);//两两归并
		i=i+2*s;
	}
	if(i


七、快速排序

快速排序其实就是我们前面介绍的冒泡排序的升级,它们都属于交换排序类,即它也是通过不段比较和移动交换来实现排序,只不过它的实现,增大了数据比较和移动的距离,将数据较大的值从前面直接移动到后面,数据较小的值从后面直接移动到前面,从而减少了总的比较和移动交换次数。

快速排序的基本思想是:通过一趟排序将待排序列分割成独立的两部分,其中一部分序列的数据比另一部分序列的数据小,则可分别对这两部分序列继续进行排序,以达到整个序列有序的目的。

详细解释可参考这篇博客:http://blog.csdn.net/morewindows/article/details/6684558

这里简单较少下方法步骤:

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

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

3.再对左右区间重复第二步,直到各区间只有一个数。

代码如下:

/*****************快速排序*******************/
/**
* quick_sort函数:对s[l..r]进行排序
* @param [in] s:待排序数组
* @param [in] l:起始位置
* @param [in] n:结束位置
* @return
*/ 
void quick_sort(int s[], int l, int r)
{
    if (l < r)
    {
	//int m=(l+r)/2;
	//if(s[l]>s[r])
	//	swap(s[l],s[r]);
	//if(s[m]>s[r])
	//	swap(s[m],s[r]);
	//if(s[m]>s[l])
	//	swap(s[m],s[l]);
        int i = l, j = r, x = s[l];
        while (i < j)
        {
            while(i < j && s[j] >= x) // 从右向左找第一个小于x的数
				j--;  
            if(i < j) 
				s[i++] = s[j];
			
            while(i < j && s[i] < x) // 从左向右找第一个大于等于x的数
				i++;  
            if(i < j) 
				s[j--] = s[i];
        }
        s[i] = x;
        quick_sort(s, l, i - 1); // 递归调用 
        quick_sort(s, i + 1, r);
    }
}
/**
* 快速排序
* @param [in] a:待排序数组
* @param [in] n:数字大小
* @return
*/ 
void QuickSort(int a[], int n)
{
	quick_sort(a,0,n-1);
}


快速排序改进优化:

1、优化基准数的选取

上述中选取第一个数作为基准数,基准数的选取对算法性能有一定的影响。总是固定选取第一个数作为基准数不太合理。下面介绍一种三数取中法:即取三个数据进行排序,将中间数作为基准数,一般取开始、中间、结尾三个数据。

代码如下:

int m=(l+r)/2;
if(s[l]>s[r])
	swap(s[l],s[r]);
if(s[m]>s[r])
	swap(s[m],s[r]);
if(s[m]>s[l])
	swap(s[m],s[l]);

2、优化小数组时的排序方案

在数组非常小的情况下,其实快速排序反而不如直接插入排序好,其原因是快速排序用到了递归操作,在大量数据排序时,这点性能影响可以忽略,但数组很小时则优势不明显。在程序中我们可以增加一个判断,当r-l不大于某个常数时,就可用直接插入排序,这样就能保证最大化利用两种排序的优势来完成排序。

3、优化递归操作

递归对性能有一定的影响,快速排序中尾部有两次递归操作。每次递归操作都会耗费一定的栈空间,如果能减少递归,将会提高性能。我们考虑到第一次递归quick_sort1(s,l, i - 1)后,变量l就没有用了,所以将i+1赋给l,将原来的if (l < r)改为while(l < r),当quick_sort1(s, l, i - 1)语句执行完后,l变成i+1,r还是r,再进行循环相当于执行quick_sort1(s, i+1, r)。这样结果相同,但采用迭代代替递归可以缩减堆栈深度,提高整体性能。


上述2、3两点对应的代码如下:

/*****************快速排序改进程序*******************/
/**
* quick_sort1函数:对s[l..r]进行排序
* @param [in] s:待排序数组
* @param [in] l:起始位置
* @param [in] n:结束位置
* @return
*/ 
const int MAXSIZE=7;
void quick_sort1(int s[], int l, int r)
{
	if((r-l) > MAXSIZE)
	{
		while (l < r)
		{
			int m=(l+r)/2;
			if(s[l]>s[r])
				swap(s[l],s[r]);
			if(s[m]>s[r])
				swap(s[m],s[r]);
			if(s[m]>s[l])
				swap(s[m],s[l]);
			int i = l, j = r, x = s[l];
			while (i < j)
			{
				while(i < j && s[j] >= x) // 从右向左找第一个小于x的数
					j--;  
				if(i < j) 
					s[i++] = s[j];
			
				while(i < j && s[i] < x) // 从左向右找第一个大于等于x的数
					i++;  
				if(i < j) 
					s[j--] = s[i];
			}
			s[i] = x;
			quick_sort1(s, l, i - 1); // 递归调用 
			//quick_sort(s, i + 1, r);
			l=i+1;
		}
	}
	else
		InsertSort(s,r-l+1);
}

/**
* 快速排序改进程序
* @param [in] a:待排序数组
* @param [in] n:数字大小
* @return
*/ 
void QuickSort1(int a[], int n)
{
	quick_sort1(a,0,n-1);
}

总结:本文主要介绍几种常用的排序算法,可以分为四类:插入排序类(直接插入排序、希尔排序),选择排序类(简单选择排序、堆排序),交换排序类(冒泡排序,快速排序),归并排序类(归并排序)。最后给出7种算法的各种指标对比图。对比图来自<<大话数据结构>>。

数据结构与算法:七种排序算法总结(冒泡排序、选择排序、直接插入排序、希尔排序、堆排序、归并排序、快速排序)_第3张图片



你可能感兴趣的:(数据结构及算法)