`
lingyibin
  • 浏览: 196243 次
  • 性别: Icon_minigender_1
  • 来自: 长沙
社区版块
存档分类
最新评论

各种排序算法的实现及其比较

阅读更多

排序算法是笔试和面试中最喜欢考到的内容,今晚花了好几个小时的时间把之前接触过的排序算法都重新实现了一遍。

 

主要是作为复习用。当然也希望能够给大家帮上点忙。

对各种排序算法比较熟悉的朋友可以直接跳过。

 

常用的内部排序算法主要分为五类:插入、交换、选择、归并、基数排序。

文章的最后可能还会稍微分析一下外部排序。。。内/外部排序的区别就是 外部排序指的是大文件的排序,即待排序的记录存储在外存储器上,在排序过程中需要多次的内/外存之间的交换。

下面一个一个分析。

 

(注意,本篇中讲的 lg(n) 都是以2为底的)

 

一、插入排序

下面讲到的这些插入排序的时间复杂度,除希尔排序是O(n的3/2次方)外,其它的都是O(n平方)。另一方面,除希尔排序外,其它的排序都是稳定的。(稳定是指相同的两个数在排序之后它们的相对位置不变。)

1、直接插入排序。

这是最简单的排序方法,它的基本基本操作是将一个记录插入到已排好序的有序表中,从而得到一个新的、记录数增加1的有序表。

 

#include<iostream>
using namespace std;

int main()
{
	int src[] = {49,38,65,97,76,13,27,49};

	int i,j,cnt = sizeof(src)/4;
	int guard = src[0]; //设置一个哨兵,用来记录当前值
	for(i = 1; i < cnt; i ++)
	{
		if(src[i] < src[i-1])
		{
			guard = src[i];
			src[i] = src[i-1];
			for(j = i - 2; src[j]>guard; j --) src[j+1] = src[j];
			src[j+1] = guard;
		}
	}
	for(i = 0; i < cnt; i ++) cout<<src[i]<<endl;
	//getchar();
	return 0;
}

 

 2、折半插入排序

这种插入排序是上一种排序的改进,就是在查找的时候不用一个一个对比,因为前面已经有序,所以可以用折半法。

 

#include<iostream>
using namespace std;

int main()
{
	int src[] = {49,38,65,97,76,13,27,49};

	int i,j,low,high,mid,cnt = sizeof(src)/4;
	int guard = src[0]; 
	for(i = 1; i < cnt; i ++)
	{
		if(src[i] < src[i-1])
		{
			guard = src[i];
			low = 0, high = i - 1; 
			while(low <= high)
			{
				mid = (low+high)/2;
				if(guard < src[mid]) high = mid - 1;
				else low = mid+1;
			}
			for(j = i - 1; j >= high +1; j --) src[j+1] = src[j]; //后移
			src[j+1] = guard;
		}
	}
	for(i = 0; i < cnt; i ++) cout<<src[i]<<endl;
	//getchar();
	return 0;
}

 

 3、2-路插入排序

先读入前面两个数,大的放队首,小的放队尾,并分别用final和first作为指针指向它们,两个都向中间延伸,first向右增长,final向左减小。

如对 int src[] = {49,38,65,97,76,13,27,49_} 进行排序(这里为了区分两个49,

我给第二个49加了一个下划线。。。)

第一步得:

 

final                      first 
49 x x x x x x 38

放入第三个数时,和队首first和队尾final分别进行比较,如果比first大则放它右边,并用first指向它。如果比final小,则放final的左边,并用final指向它。如果大小final,小于first,则插入到当前的first或final的位置。

第二步得:

 


 final                 first 
49   65 x x x x x 38

第三步:

 


   final               first 
49  65   97 x x x x 38

第四步:

 


     final          first 
49  65   76    97 x x  x 38

第五步:

 


     final         first  
49  65   76    97 x x   13 38

第六步:

 


     final     first   
49  65   76    97 x  13  27 38

第七步:

 


       final   first   
49  49_  65  76    97  13  27 38

这样做的好处是可以减少移动的次数。。。具体的程序实现留给读者去实现。。。

 

4、希尔排序

又称为 缩小增量排序,它的基本思想是,先将整个待排记录序列分割成为若干子序列分别进行直接插入排序,待整个序列中的记录“基本”有序时,再对全体记录进行一次直接插入排序。

 int src[] = {49,38,65,97,76,13,27,49_,55,4} 为例,共有10个数,

我们先以10/2=5为跨度,进行第一趟排序。

第一趟:

49和13比较,38和27比较,65和49_比较,97和55比较,76和4比较,得:

13,27,49_,55,4,49,38,65,97,76 (从这里可以看出,希尔排序是不稳定的,当然下结论前还要看最后的结果。)

第二趟,我们以5-2=3为跨度,进行排序,可得:

13,4,49_,38,27,49,55,65,97,76 (到这时一看,有序多了!)

第三趟,心3-2=1为跨度,进行排序,得:

4,13,27,38,49_,49,55,65,76,97

当跨度减小到1时,就算是排序结束了。由结果可以断定,希尔排序是不稳定的排序。

 

#include<iostream>
using namespace std;

int main()
{
	int src[] = {49,38,65,97,76,13,27,49,55,4}; //大家可试一试奇数个的情况

	int i,j,tmp,cnt = sizeof(src)/4,dk = (cnt+1)/2,c=1;

	while(dk > 0)
	{
		cout<<c++<<endl;
		for(i = 0; i < cnt; i ++) cout<<src[i]<<" ";
		cout<<endl;
		for(i = dk; i < cnt; i ++)
		{
			if(src[i] < src[i-dk])
			{
				tmp = src[i];
				src[i] = src[i-dk];
				src[i-dk] = tmp;
			}
		}
		dk = dk - 2 == 0 ? 1:dk-2; 
	}
	cout<<c<<endl;
	for(i = 0; i < cnt; i ++) cout<<src[i]<<" ";
	cout<<endl;

	getchar();
	return 0;
}

 

 

二、交换排序

1、冒泡排序

很简单,就是相邻的两个数,两两相互比较,其特点是:每比较一次就可以得出最小/大的一个数 放到队首或队尾。它的时间复杂度也比较大:O(nlgn),冒泡排序算法是稳定的排序方式。

 

#include<iostream>
using namespace std;

int main()
{
	int src[] = {49,38,65,97,76,13,27,49};

	int i,j,cnt = sizeof(src)/4;

	for(i = 0; i < cnt; i ++)
		for(j = cnt-1; j > i; j --)
			if(src[j] < src[j-1]) swap(src[j],src[j-1]);

	//输出
	for(i = 0; i < cnt; i ++) cout<<src[i]<<" ";
	cout<<endl;

	getchar();
	return 0;
}
 

2、快速排序

快速排序的时间复杂度达到O(nlgn),被公认为最快的排序方法之一。在所有同数量级(O(nlgn))的排序当中,其平均性能最好。它其实是冒泡排序的改进,当一列数据基本有序的时候,快速排序将为蜕化为冒泡排序,时间复杂度为O(n平方)。基本思想是 取一个数作为中间数,比它小的都排左边,比它大的都排右边(如果是从大到小排序的话,就反过来),再对每一边用同样的思路进行递归求解。快速排序是不稳定的排序方式。

 

#include<iostream>
using namespace std;

void quickSort(int *src, int low,int high)
{
	if(src == NULL) return ;
	int i,j,pivot;
	if(low < high)
	{
		pivot = src[low];
		i = low,j = high;
		while(i < j)
		{
			while(i<j && src[j] >= pivot) j--;
			src[i] = src[j];

			while(i<j && src[i] <= pivot) i++;
			src[j] = src[i];
		}

		src[i] = pivot;

		quickSort(src,low,i-1);
		quickSort(src,i+1,high);
	}
}

int main()
{
	int src[] = {49,38,65,97,76,13,27,49};

	int i,low,high,cnt = sizeof(src)/4;

	quickSort(src,0,cnt-1);

	//输出
	for(i = 0; i < cnt; i ++) cout<<src[i]<<" ";
	cout<<endl;

	getchar();
	return 0;
}

 

三、选择排序

1、简单选择排序

思路很简单,就是每次选出最小/大的数,和前面的第i个数交换。时间复杂度也是 O(n平方),是不稳定的排序方式,因为在交换的过程中,相同的两个数,前者有可能被交换到后面去,举个例子,序列5 8 5 2 9, 我们知道第一遍选择第1个元素5会和2交换,那么原序列中2个5的相对前后顺序就被破坏了,所以选择排序不是一个稳定的排序算法

 

#include<iostream>
using namespace std;

int main()
{
	int src[] = {49,38,65,97,76,13,27,49};

	int i,j,cnt = sizeof(src)/4,min ,index;

	for(i = 0; i < cnt; i ++)
	{
		min = INT_MAX,index = -1;
		for(j = i; j < cnt; j ++)
		{
			if(min > src[j])
			{
				min = src[j];
				index = j;
			}
		}
		if(index != i)
		{
			src[index] = src[i];
			src[i] = min;
		}
	}

	//输出
	for(i = 0; i < cnt; i ++) cout<<src[i]<<" ";
	cout<<endl;

	getchar();
	return 0;
}
 

 

2、堆排序

先用给定的数构造一棵完全二叉树,然后从下标为cnt/2(cnt指给定的数的个数)开始一个一个和它的孩子结点比较,小的就往上挪,最后得到一个小顶堆,取出堆顶,并把最后一个数放入堆顶,进行同样的操作,直到所有的数都已取完为止。我们可以用一个数组来顺序地表示这棵树,左孩子可以通过2*n来找到,右孩子可以通过2*n+1来找到。 下面给一个例子:

int src[] = {49,38,65,97,76,13,27,49};



 堆排序的时间复杂度是O(nlgn),也是最快的排序方法之一,在最坏的情况下,其时间复杂度还是O(nlgn),相对于快速排序来说,这是堆排序的最大优点。此外,堆排序仅需要一个记录大小供交换用的辅助存储空间。堆排序也是不稳定的排序。

 

#include<iostream>
using namespace std;

void heapAdjust(int *src, int s,int m)
{   //从s开始进行一次调整,其中m指向需要进行排序的数据中最大的下标
	if(src == NULL) return;
	int i,rc = src[s];
	for(i = 2*s+1; i <= m; i = 2*i+1) //一层一层往下走
	{
		if(i<m && src[i] < src[i+1]) i++; //让i指向最大的那个孩子
		if(rc >= src[i]) break;

		src[s] = src[i];
		s = i;
	}
	src[s] = rc;
}

int main()
{
	int src[] = {49,38,65,97,76,13,27,49,1};

	int i,j,cnt = sizeof(src)/4;

	//先构建一个小顶堆
	for(i = cnt/2-1; i >-1; i --)
		heapAdjust(src,i,cnt-1);

	//每次取出顶部最大的那个数放后面,再进行一次顶点调整
	for(i = cnt-1; i > 0; i --)
	{
		swap(src[0],src[i]);
		heapAdjust(src,0,i-1);
	}

	//输出
	for(i = 0; i < cnt; i ++) cout<<src[i]<<" ";
	cout<<endl;

	getchar();
	return 0;
}

 

四、归并排序

归并的意思就是两个或两个以上的有序表组合成一个新的有序表。整个归并排序需要进行【lgn取上限】次,总的时间复杂度为O(nlgn)。与快速排序相比,归并排序的最大特点是:它是一种稳定的排序方法。

如上图,这是一种2-路归并排序,通常用递归方法来实现,递归的形式的算法在形式上较简洁,但实用性很差。

#include<iostream>
using namespace std;


void merge(int* src,int i,int m,int n)
{
	//开辟一个临时的空间来存放归并后的结果
	int *des = (int*)malloc(sizeof(int)*n);

	int k = i,mm = m++,low = i;
	//把数组的两部分,一个一个地放入临时数组中,小的先放
	while(i <= mm && m <= n)
		if(src[i] <= src[m]) des[k++] = src[i++];
		else des[k++] = src[m++];

	//把剩余的放入数组
	while(i <= mm) des[k++] = src[i++];
	while(m <= n) des[k++] = src[m++];

	//把结果拷贝回原数组
	for(k = low; k <= n; k++) src[k] = des[k];
}

void mergeSort(int* src, int s, int t)
{
	if(src == NULL) return ;
	int m;
	if(s < t) 
	{
		m = (s+t)/2; //取中间值
		mergeSort(src,s,m); //递归地将src[s..m]归并为有序的des[s..m]
		mergeSort(src,m+1,t); //递归地将src[m+1..t]归并为有序的des[m+1..t]
		merge(src,s,m,t);
	}
}

int main()
{
	int src[] = {49,38,65,97,76,13,27,49};

	int i,j,cnt = sizeof(src)/4;
	
	mergeSort(src,0,cnt-1);

	//输出
	for(i = 0; i < cnt; i ++) cout<<src[i]<<" ";
	cout<<endl;

	getchar();
	return 0;
}
 

五、基数排序

所谓的基数排序 其实就是一种多关键字的排序,最经典的例子就是英语字典的编排,先按第一个字母排列,分成26堆,再按第二个字母排列,……以此类推。。。更复杂的基本排序作者本人也没有研究过,有兴趣的朋友可以去网上找相关的资料。

 

六、各种内部排序的比较

1、时间复杂度达到O(nlgn) 的排序算法有:快速排序、堆排序、归并排序。

2、上面前四大类排序中,不稳定的排序有:希尔排序、快速排序、堆排序、简单的选择排序。

稳定的排序有:插入排序(除希尔外)、冒泡排序、归并排序。

3、从平均时间性能而言,快速排序最佳,其所需要的时间最少,但快速排序在最坏的情况下,时间性能还不如堆排序和归并排序。

 

七、外部排序

外部排序指的是大文件的排序,面试的时候,面试官喜欢问,给你一个非常非常大的文件(比如1T),一行一个数(或者一个单词),内存最多只有8G,硬盘足够大,CPU很高级……然后要你给这个文件里面的数据排序。你要怎么办?

这其实就要用到外部排序。就是说要借助外存储器进行多次的内/外存数据的交换,因为内存不足以加载所有的数据,所以只能一部分一部分地加载。

所以外部排序的思想就是:分两个独立的阶段。

首先,可按内存的大小,将外存上含n个记录的文件分成若干长度为 的子文件或段,依次读入内存,并利用有效的内部排序方法对它们进行排序,并将排序后得到的有序子文件 重新写入外存,通常称这些有序的子文件为归并段或顺串。然后,对这些归并段进行逐趟归并,使归并段逐渐由小到大,直至得到整个有序文件为止。

因此现在的问题就转化为如何归并两个大文件。这个读者朋友们想一下就明白了。就是把这两个文件按内存的大小,一部分一部分从小到大加载出来并,再写回外存。

当然归并的方法有很多种,作者本人也没怎么去研究。。。

 

说明:本篇日志欢迎转载,但请标明出处:http://lingyibin.iteye.com/blog/1043886

  • 大小: 43.8 KB
  • 大小: 23.5 KB
2
3
分享到:
评论
6 楼 Arlrn 2013-12-27  
博主你好,最近在学习排序算法,看了你的博客,你的直接插入排序,有个地方好像有问题,当i=1时,i-2=(-1),这样会数组越界吧,但我很奇怪你的代码运行结果是正确的,可是换成数组 {3,4,2,6,9,4,1},结果就会错误,应该是要src[j]>guard&&j>=0。
5 楼 sharong 2013-06-25  
有一个明显错误,很显然冒泡排序的时间复杂度是O(n^2)
4 楼 julydave 2013-04-22  
希尔排序不太对吧。。
3 楼 A517269253 2012-09-12  
在堆排序中lz构造一个大顶堆,注释却写成了小顶堆。。。
2 楼 lingyibin 2011-05-14  
贾懂凯 写道
bing哥,我最近也看算法,有时间交流。

好地!
1 楼 贾懂凯 2011-05-14  
bing哥,我最近也看算法,有时间交流。

相关推荐

    各种排序算法比较

    ### 各种排序算法比较 #### 一、稳定性比较 稳定性是排序算法中一个重要的特性,指的是相等的元素在排序前后保持原有的相对位置不变。根据文档提供的信息,我们可以总结出以下结论: - **稳定排序**:插入排序、...

    C语言6种排序算法及其实现

    C语言作为一门底层且高效的编程语言,常用于实现各种排序算法。以下将详细阐述标题和描述中提到的6种排序算法及其C语言实现。 1. 冒泡排序(Bubble Sort): 冒泡排序是一种简单的排序算法,通过重复遍历数组,比较...

    各种常用排序算法的C语言实现

    本资源提供了各种常用排序算法的C语言实现,源自严蔚敏的经典教材《数据结构》。下面将详细介绍这些排序算法及其在C语言中的实现原理。 1. 冒泡排序(Bubble Sort) 冒泡排序是最基础的排序方法,通过不断交换相邻...

    查找与排序算法的实现和应用

    查找与排序算法的实现和应用 ...查找和排序算法是计算机科学中的一种基本算法,广泛应用于各种领域。了解查找和排序算法的实现和应用是软件技术基础的关键,可以帮助我们更好地理解和应用这些算法。

    Java 七种排序算法实现

    本资源包含了七种经典的排序算法实现,它们分别是冒泡排序、插入排序、递归排序(这里可能是指归并排序)、基数排序、快速排序、选择排序和希尔排序。下面我们将详细探讨这些排序算法的工作原理和实现方式。 1. **...

    各种排序算法c语言实现

    根据给定文件的信息,我们可以详细地探讨几种不同的排序算法及其在C语言中的实现方式。这里主要涉及的选择排序、直接插入排序、冒泡排序以及堆排序是计算机科学领域中非常基础且重要的排序方法。 ### 一、选择排序 ...

    7大排序算法实现程序(快速排序,冒泡排序,选择排序,归并排序,插入排序,希尔排序,堆排序)

    本资源提供了七大经典排序算法的实现程序,包括快速排序、冒泡排序、选择排序、归并排序、插入排序、希尔排序和堆排序。下面将逐一详细介绍这些排序算法及其原理。 1. 快速排序:由C.A.R. Hoare提出,是一种采用...

    C# 各种排序算法实现与对比

    在编程领域,排序算法是数据结构与算法课程中的核心部分,尤其在C#这样的面向对象编程语言中,理解和掌握各种排序算法的实现及其性能特点至关重要。本文将详细讲解C#中实现的几种常见排序算法,并对它们的执行效率...

    数据结构课程设计(内部排序算法比较_C语言)

    本课题旨在通过比较各种内部排序算法的性能,帮助开发者在实际应用中做出合理的选择。 #### 二、内部排序算法概述 内部排序算法是指所有待排序的数据全部存放在内存中进行排序的过程。常见的内部排序算法包括但不...

    C语言程序设计实现各排序算法比较

    ### 排序算法实现及比较 #### 冒泡排序 - **原理**:通过重复地遍历列表,比较每对相邻项并交换顺序错误的项。 - **实现步骤**: - 遍历整个数组,比较相邻的元素。 - 如果第一个元素大于第二个元素,则交换它们...

    排序技术,整合各种排序算法思想

    冒泡排序是一种简单的排序算法,通过重复遍历待排序的数列,一次比较两个元素,如果他们的顺序错误就把他们交换过来。遍历数列的工作是重复地进行直到没有再需要交换,也就是说该数列已经排序完成。这种算法的名字...

    各种排序算法时间性能的比较

    ### 各种排序算法时间性能的比较 #### 需求分析 1. **需求描述** 本次设计的主要目标是对多种常见的排序算法(直接插入排序、希尔排序、起泡排序、快速排序、直接选择排序、堆排序、归并排序)的时间性能进行...

    常用的排序算法总结(各种内部排序算法和外部排序算法)

    排序算法是计算机科学中最基础和重要的算法之一,用于将一组数据按照特定的顺序进行排列。本文将对几种常见的内部排序算法和外部排序算法...理解这些基本的排序算法及其特性,对于任何IT专业人员来说都是非常必要的。

    数据结构内部排序算法比较.doc

    本次实验的主要目的是通过实际操作来比较六种常用的内部排序算法(起泡排序、直接插入排序、简单选择排序、快速排序、希尔排序、堆排序)在处理随机数据时的关键字比较次数和关键字移动次数,以便直观地了解每种算法...

    四种算法实现排序

    以上就是关于四种排序算法的基本介绍及其在C#中的实现方法,这些知识对于理解和编写高效的代码至关重要。在学习和实践中,理解每种排序算法的运作机制,能帮助我们更好地优化代码,提高程序性能。

    各种排序算法大全

    下面我们将深入探讨这些排序算法的核心概念及其应用。 1. 冒泡排序(Bubble Sort) 冒泡排序是一种简单直观的排序算法,通过不断交换相邻的未排序元素来逐步将最大或最小的元素“冒”到数组的一端。虽然效率较低,...

    各种内部排序方法及其比较实验报告

    "各种内部排序方法及其比较实验报告" 内部排序是指在计算机内存中对数据进行排序的方法。内部排序方法有很多,常见的有直接插入排序、折半插入排序、希尔排序、冒泡排序、快速排序、堆排序、归并排序等。 直接插入...

    希尔排序算法原理及其Java实现详解

    内容概要:本文详细介绍了希尔排序(Shell Sort)算法的...通过理解并实现希尔排序,可以提升对排序算法及其实现的理解。 阅读建议:建议结合示例代码逐步调试,理解每一步的逻辑,从而更好地掌握希尔排序算法的精髓。

    数据科学入门:排序算法详解及其在Java中的实现

    适合人群:具备基础编程知识,希望深入了解排序算法及其实现的技术爱好者、学生或初级开发者。 使用场景及目标:适用于需要对大量数据进行高效排序的实际工程项目。目标是帮助读者理解和掌握各种排序算法的实现原理...

    数据结构课程设计(C++)实现各种排序算法

    数据结构课程设计中,C++实现的八种排序算法涵盖了数据结构的核心概念,这些排序算法在实际编程中具有广泛的应用。下面将详细解释每一种排序算法的设计思想和性能特点。 1. **交换排序**: - **冒泡排序**:通过...

Global site tag (gtag.js) - Google Analytics