关闭

[置顶] 排序算法大总结

标签: 排序算法插入排序算法快速排序
226人阅读 评论(0) 收藏 举报
分类:

前面在看STL源码剖析时,发现自己对排序算法掌握得不好,遂花时间彻底的学习了一番,并做个全面的总结如下。

一.直接插入排序

插入排序的基本思想是:每步将一个待排序的纪录,按其关键码值的大小插入前面已经排序的文件中适当位置上,直到全部插入完为止。

算法的伪代码:

for j ←2 to n 
          do key ← A[ j] 
          i ← j – 1 
          while i > 0 and A[i] > key 
               do A[i+1] ← A[i] 
                    i ← i – 1 
          A[i+1] = key

分析:每次选择一个元素A[j]插入到之前已排好序的部分A[1…j-1]中,先将A[j]赋值到key变量,在插入过程中,key依次由后向前与A[1…j-1]中的元素进行比较,即从i=j-1、j-2,直到第一个元素0,若比较中发现key<A[i],则将A[i]往后移动,当key<A[i]不成立时,将key值放到i+1位置,即key应该插入的位置。

注意:此处是从后往前比较然后进行插入的,这样在数据基本有序的情况下算法的执行时间会很快。此算法是稳定的。

具体的复杂度分析如下:

最好情况:数据为完全正序,比较次数为n次,不需要移动元素,复杂度为O(n)。

最坏情况:数据为完全逆序,比较次数为1+2+..+n-1,复杂度为O(n­2)。

平均情况:O(n­2)。

下面是C++实现代码:

void insertsort(int a[],int n)
{
	int key;int i,j;
	for(i=1;i<n;++i)
	{
		key=a[i];
		j=i-1;
		while(j>=0&&key<a[j])
		{
			a[j+1]=a[j];
			j--;
		}	
		a[j+1]=key;
	}
}

二、希尔排序(插入排序)

希尔排序思想:先取一个小于n的整数d1作为第一个增量,把文件的全部记录分组。所有距离为d1的倍数的记录放在同一个组中。先在各组内进行直接插入排序;然后,取第二个增量d2<d1重复上述的分组和排序,直至所取的增量 d=1,即所有记录放在同一组中进行直接插入排序为止。
该方法实质上是一种分组插入方法。是直接插入排序的改进。

希尔排序是基于插入排序的以下两点性质而提出改进方法的:
插入排序在对几乎已经排好序的数据操作时,效率高,即可以达到线性排序的效率。
插入排序一般来说是低效的,因为插入排序每次只能将数据移动一位。

最好情况:由于希尔排序的好坏和步长d的选择有很多关系,不能确切求出最好的情况下的算法时间复杂度。  
       最坏情况:O(N*logN)  
       平均情况:O(N*logN)

下面是希尔排序的c++代码:

void shellsort(int *a,int len)
{
	int i,j;
	int d=len/2;
	while(d>0)
	{
		for(i=d;i<len;++i)
		{
			j=i-d;
			while(j>=0&&a[j+d]<a[j])
			{
				swap(a[j+d],a[j]);
				j=j-d;
			}
			  
		}
		d=d/2;
	}
}

三、冒泡排序(交换排序)

基本思想:通过无序区中相邻记录关键字间的比较和位置的交换,使关键字最小的记录如气泡一般逐渐往上“漂浮”直至“水面”。 

算法实现步骤:

  1. 比较相邻的元素。如果第一个比第二个大,就交换他们两个。
  2. 对每一对相邻元素作同样的工作,从开始第一对到结尾的最后一对。结束后,最后的元素应该会是最大的数。
  3. 针对所有的元素重复以上的步骤,除了最后一个。
  4. 持续每次对越来越少的元素重复上面的步骤,直到没有任何一对数字需要比较。
最好情况:复杂度为O(n)
最坏情况和平均情况:复杂度为O(n­2)
此时需要注意的是,最好情况下的复杂度和代码实现有一定关系,下面对此作出了分析。
实现代码一:
void bubblesort(int *a,int len)
{
	int i,j;
	for(i=0;i<len-1;++i)
	{
		for(j=1;j<len-i;++j)
		{
			if(a[j]<a[j-1])
			  swap(a[j-1],a[j]);
		}
	}
}
此代码实现时,复杂度总为O(n­2),原因是此实现算法没有优化,当正序有序时,外循环指向n次,内循环也要执行n-i次。此实现中,也可先选出最小的放在最前面,接着次小的放在第二个位置。

实现代码二:

void bubblesort(int *a,int len)
{
	int i,j;
	bool didswap=false;
	for(i=0;i<len-1;++i)
	{	
		didswap=false;
		for(j=1;j<len-i;++j)
		{
			if(a[j]<a[j-1])
			{
				swap(a[j-1],a[j]);
				didswap=true;
			}
		}
		if(didswap==false)
		  return;
	}
}
代码二可以实现最佳情况下复杂度为O(n),代码中加入了一个判断,当内层循环中没有进行交换操作,则说明序列已经有序了,因此,算法结束,返回结果。

四、快速排序(交换排序)

基本思想:通过一趟排序将要排序的数据分割成独立的两部分,其中一部分的所有数据都比另外一部分的所有数据都要小,然后再按此方法对这两部分数据分别进行快速排序,整个排序过程可以递归进行,以此达到整个数据变成有序序列

算法实现步骤:设要排序的数组是A[0]……A[N-1],首先任意选取一个数据(通常选用数组的第一个数)作为关键数据,然后将所有比它小的数都放到它前面,所有比它大的数都放到它后面,这个过程称为一趟快速排序。值得注意的是,快速排序不是一种稳定的排序算法,也就是说,多个相同的值的相对位置也许会在算法结束时产生变动。

一趟快速排序的算法是:
1)设置两个变量i、j,排序开始的时候:i=0,j=N-1;
2)以第一个数组元素作为关键数据,赋值给key,即key=A[0];
3)从j开始向前搜索,即由后开始向前搜索(j--),找到第一个小于key的值A[j],将A[j]和A[i]互换;
4)从i开始向后搜索,即由前开始向后搜索(i++),找到第一个大于key的A[i],将A[i]和A[j]互换;
5)重复第3、4步,直到i=j; (3,4步中,没找到符合条件的值,即3中A[j]不小于key,4中A[i]不大于key的时候改变j、i的值,使得j=j-1,i=i+1,直至找到为止。找到符合条件的值,进行交换的时候i, j指针位置不变。另外,i==j这一过程一定正好是i+或j-完成的时候,此时令循环结束)。

一般情况:复杂度为 O(N*logN)

最坏情况:基本有序时,退化为冒泡排序,几乎要比较N*N次,故为O(N*N)。

快速排序算法实现有很多种,下面对各种实现一一进行分析。

实现一:递归实现,key为数组第一个元素

void quicksort(int *a,int first,int last)
{
	if(first>=last)
		return;	
	int low=first,high=last;
	int key=a[first];
	while(low<high)
	{
		while(low<high&&key<=a[high])
		  --high;
		a[low]=a[high];
		while(low<high&&key>=a[low])
		  ++low;
		a[high]=a[low];
	}
	a[low]=key;
	quicksort(a,first,low-1);
	quicksort(a,high+1,last);
}
实现一是用第一个元素作为关键元素(枢纽),当第一个数据为极值时,会出现最坏的情况,算法的复杂度没有得到改进。在算法导论上,看到快速排序的实现和此方式有点不同,虽然也是递归算法,但二分区间的方式不同,于是也实现了一下。有点不太直观,借用了这个博客中的图方便理解:http://blog.sina.com.cn/s/blog_73428e9a01017f9x.html

快速排序-算法导论版本

实现代码为:

int partition(int *a,int first,int last)
{
	int key=a[last];
	int i=first-1;
	int j;
	for(j=first;j<last;j++)
	{
		if(a[j]<=key)
		{
			++i;
			swap(a[i],a[j]);
		}
	}
	swap(a[i+1],a[j]);
	return i+1;
}
void quicksort(int *a,int first,int last)
{
	if(first>=last)
		return;	
	int mid=partition(a,first,last);
	quicksort(a,first,mid-1);
	quicksort(a,mid+1,last);
}
上面都是递归实现的快速排序,但是基于非递归算法比对应的递归算法速度快的思想,因此尝试实现了非递归的快速排序。

实现二.非递归:

void quicksort(int *a,int size)
{
	stack<int> st;
	st.push(0);
	st.push(size-1);
	while(!st.empty())
	{
		int end=st.top;st.pop();
		int start=st.top;st.pop();
		int key=a[end];
		int i=start-1;
		int j;
		for(j=start;j<end;j++)
		{
			if(a[j]<x)
			{
				++i;
				swap(a[i],a[j];
			}
		}
		swap(a[i+1],a[end]);
		if(start<1)
		{
			st.push(start);
			st.push(i);
		}
		if(i+2<end)
		{
			st.push(i+2);
			st.push(end);
		}
	}
}

但测试结果却是此非递归算法更慢,原因是STL容器是stack效率太低,因此,使用时最好用递归算法实现的代码。


实现三:随机化版本

快速排序大多时选择第一个元素或者最后一个作为主元,此处使用随机化选取主元的方式,这种情况下虽然最坏情况仍然是O(n^2),但最坏情况不再依赖于输入数据,而是由于随机函数取值不佳。实际上,随机化快速排序得到理论最坏情况的可能性仅为1/(2^n)。所以随机化快速排序可以对于绝大多数输入数据达到O(nlogn)的期望时间复杂度

int rand(int low,int high)
{
	int size=high-low+1;
	return low+rand()%size;
}

void quicksort(int *a,int first,int last)
{
	if(first>=last)
		return;	
	swap(a[rand(first,last)],a[first]);
	int low=first,high=last;
	int key=a[first];
	while(low<high)
	{
		while(low<high&&key<=a[high])
		  --high;
		a[low]=a[high];
		while(low<high&&key>=a[low])
		  ++low;
		a[high]=a[low];
	}
	a[low]=key;
	quicksort(a,first,low-1);
	quicksort(a,high+1,last);
}

实现四:三平均分区法

是选择待排数组的第一个数作为中轴,而是选用待排数组最左边、最右边和最中间的三个元素的中间值作为中轴。这一改进对于原来的快速排序算法来说,主要有两点优势:
  (1) 首先,它使得最坏情况发生的几率减小了。
  (2) 其次,未改进的快速排序算法为了防止比较时数组越界,在最后要设置一个哨点。


五、直接选择排序

对于每一趟,选择未排序序列中的最小的数放到排序序列的最后面。即第一趟,选择数组所有元素中的最小值放到第一个位置,第二趟,选择第二个到数组尾中最小值(即次小值)放到第二个位置...直到最后一个。算法实现如下:

void selectsort(int a[],int first,int last)
{
	for(int i=0;i<last;++i)
	{
		int min=i;
		for(int j=i+1;j<=last;++j)
		{
			if(a[j]<a[min])
				min=j;
		}
		if(min!=i)swap(a[i],a[min]);
	}
}


六、堆排序(选择排序)

直接看算法实现,分为三个子程序.

1、调整堆,对于一个存在子节点的节点,调整此节点的位置,使此节点所在的树满足大顶堆或者小顶堆的定义。下面程序时按升序排列,因此建立大顶堆。

void heapadjust(int a[],int first,int last)
{
	if(first>=last)return;
	int child=2*first+1;
	while(child<=last)
	{
		if(child+1<=last&&a[child+1]>a[child])//选择子节点中较大的一个
			++child;
		if(a[first]<a[child])//父节点小于子节点
		{
			swap(a[first],a[child]);//交换
			first=child;
			child=2*child+1;
		}
		else
			break;
	}
	for(int i=0;i<8;++i)cout<<a[i]<<" "; //方便查看结果
	cout<<endl;
}
对于上述程序,可以有一点改进,如果父节点值较小,需要多次和子节点、子节点的子节点...进行交换,此处将父节点的值保存下来,等到最后其插入位置确定后才进行赋值操作,减少赋值操作次数。改进的调整堆程序如下:

void heapadjust(int a[],int first,int last)
{
	if(first>=last)return;
	int child=2*first+1;
	int temp=a[first];
	while(child<=last)
	{
		if(child+1<=last&&a[child+1]>a[child])
			++child;
		if(temp<a[child])  //此处和上述程序不同,需注意
		{
			a[first]=a[child];
			first=child;
			child=2*child+1;
		}
		else
			break;
	}
	if(a[first]!=temp)a[first]=temp;  //最后才进行赋值。
	for(int i=0;i<8;++i)cout<<a[i]<<" ";
	cout<<endl;
}
2、建立堆

在堆排序中,建堆是第一步,但是建堆也需要使用调整堆程序,原因是建堆过程是从最后一个非叶子节点开始进行调整的,直到根节点。

void buildheap(int a[],int first,int last)
{
	for(int i=(last-1)/2;i>=0;--i)
		heapadjust(a,i,last);
}
3、排序过程

先建立堆,然后将堆顶元素与最后一个未排序元素交换,然后对堆顶元素进行调整。如下:

void heapsort(int a[],int first,int last)
{
	buildheap(a,first,last);
	for(int i=last;i>0;--i)
	{
		swap(a[i],a[0]);
		heapadjust(a,0,i-1);
	}
}












0
0

查看评论
* 以上用户言论只代表其个人观点,不代表CSDN网站的观点或立场
    个人资料
    • 访问:77609次
    • 积分:1855
    • 等级:
    • 排名:千里之外
    • 原创:107篇
    • 转载:15篇
    • 译文:0篇
    • 评论:26条
    最新评论