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

排序有内部排序和外部排序,内部排序是数据记录在内存中进行排序,而外部排序是因排序的数据很大,一次不能容纳全部的排序记录,在排序过程中需要访问外存。


一、冒泡排序

基本思想依次比较相邻的两个数,将小数放在前面,大数放在后面。即首先比较第1个和第2个数,将小数放前,大数放后。然后比较第2个数和第3个数,将小数放前,大数放后,如此继续,直至比较最后两个数,将小数放前,大数放后。重复以上过程,仍从第一对数开始比较,将小数放前,大数放后,一直比较到倒数第二个数,将小数放前,大数放后,第二趟结束,在倒数第二个数中得到一个新的最大数。如此下去,直至最终完成排序。由于在排序过程中总是小数往前放,大数往后放,每次循环中一个最大的数沉底成为最后一个元素,一些较小的数如同气泡一样上浮一个位置,所以称作冒泡排序。

冒泡排序的最大时间复杂度,最小时间复杂度和平均时间复杂度均为O(n²)。通过添加标志位可将最小时间复杂度降为O(n)。

	void BubbleSort(vector& a) {
		for (int i = 0; i < a.size() - 1; ++i) {
			for (int j = 0; j < a.size() - i - 1; ++j) {
				if (a[j] > a[j + 1]) 
					swap(a[j], a[j + 1]);
			}
		}
	}

	void BubbleSort(vector& a) {
		bool didSwap;
		for (int i = 0; i < a.size() - 1; ++i) {
			didSwap = false;
			for (int j = 0; j < a.size() - i - 1; ++j) {
				if (a[j] > a[j + 1]) {
					swap(a[j], a[j + 1]);
					didSwap = true;
				}
			}
			if (!didSwap)
				break;
		}
	}
二、直接插入排序

基本思想将一个数插入到已排序好的有序数组中,从而得到一个新的元素数量增1的有序数组。即:先将数组的第1个元素看成是一个有序的子数组,然后从第2个数逐个进行插入,直至整个数组有序为止。

最差情况下,直接插入排序的最大时间复杂度为O(n²),最小时间复杂度为O(n),平均时间复杂度为O(n²)。

	void InsertSort(vector& a) {
		for (int i = 1; i < a.size(); ++i) {
			int key = a[i];  //复制为哨兵,即存储待排序元素
			int j = i - 1;
			while (j >= 0 && a[j]>key) { //查找在有序表的插入位置  
				a[j+1] = a[j];  //元素后移 
				--j;
			}
			a[j + 1] = key;  //插入到正确位置
		}
	}

三、简单选择排序

基本思想:在要排序的一组数中,选出最小的个数与第1个位置的数交换;然后在剩下的数当中再找最小的与第2个位置的数交换,依次类推,直到第n-1个元素(倒数第二个数)和第n个元素(最后个数)比较为止。

简单选择排序最大的特点是交换移动数据的次数相当少,这样也节约了时间。简单选择排序的最大时间复杂度,最小时间复杂度和平均时间复杂度均为O(n²)。尽管复杂度与冒泡排序相同,但简单选择排序的性能还是要略优于冒泡排序。

	void SelectSort(vector& a) {
		for (int i = 0; i < a.size() - 1; ++i) {
			int min = i;  //将当前下标定义为最小值下标
			for (int j = i + 1; j < a.size(); ++j) { //找出剩下的数中的最小值下标
				if (a[j] < a[min])
					min = j;
			}
			if (min != i)
				swap(a[i], a[min]);
		}
	}
四、快速排序

基本思想
1)选择一个基准元素,通常选择第一个元素或者最后一个元素,
2)通过一趟排序将待排序的记录分割成独立的两部分,其中一部分记录的元素值均比基准元素值小。另一部分记录的元素值比基准值大。
3)此时基准元素在其排好序后的正确位置

4)然后分别对这两部分记录用同样的方法继续进行排序,直到整个序列有序。

快速排序的最大时间复杂度为O(n²),最小时间复杂度为O(n*logn),平均时间复杂度为O(n*logn)。注意:快速排序是一种不稳定的排序方式,其性能依赖于原始数组的有序程度,更进一步分析,就是依赖于轴值元素的选择。快速排序是通常被认为在同数量级(O(nlog2n))的排序方法中平均性能最好的。但若初始序列有序或基本有序时,快速排序反而退化为冒泡排序。

	int partition(vector& a, int low, int high) {
		int pivotkey = a[low];
		while (low < high) {
			while (low < high && a[high] >= pivotkey)
				--high;
			swap(a[low], a[high]);
			while (low < high && a[low] <= pivotkey)
				++low;
			swap(a[low], a[high]);
		}
		return low;
	}
	void QuickSort(vector& a, int low, int high) {
		if (low < high) {
			int pivot = partition(a, low, high);
			QuickSort(a, low, pivot - 1);
			QuickSort(a, pivot + 1, high);
		}	
	}
五、归并排序
基本思想:归并(Merge)排序遵循分治法的思想,是将两个(或两个以上)有序表合并成一个新的有序表,即把待排序序列分为若干个子序列,每个子序列是有序的。然后再把有序子序列合并为整体有序序列。

归并排序的最大时间复杂度,最小时间复杂度和平均时间复杂度均为O(n*logn)。归并排序不依赖于原始数组的有序程度。

	void Merge(vector& a, int low, int mid, int high, vector& temp) {
		int i = low, j = mid + 1, k = 0;
		while (i <= mid && j <= high) {
			if (a[i] <= a[j]) 
				temp[k++] = a[i++];
			else
				temp[k++] = a[j++];
		}
		while (i <= mid)
			temp[k++] = a[i++];
		while (j <= high)
			temp[k++] = a[j++];
		for (i = 0; i < k; ++i)
			a[low + i] = temp[i];
	}
	void MergeSort(vector& a, int low, int high, vector& temp) {
		if (low < high) {
			int mid = (low + high) / 2;
			MergeSort(a, low, mid, temp);  //左边有序
			MergeSort(a, mid + 1, high, temp);  //右边有序
			Merge(a, low, mid, high, temp);  //将两个有序序列合并
		}
	}
六、堆排序

堆排序是一种树形选择排序,是对直接选择排序的有效改进。堆排序的最大时间复杂度,最小时间复杂度和平均时间复杂度均为O(n*logn)。堆排序和归并排序一样,不依赖于原始数组的有序程度。

基本思想
堆的定义如下:具有n个元素的序列(k1,k2,...,kn),当且仅当满足


时称之为堆。由堆的定义可以看出,堆顶元素(即第一个元素)必为最小项(小顶堆)。
若以一维数组存储一个堆,则堆对应一棵完全二叉树,且所有非叶结点的值均不大于(或不小于)其子女的值,根结点(堆顶元素)的值是最小(或最大)的。如:

(a)大顶堆序列:(96, 83,27,38,11,09)
(b) 小顶堆序列:(12,36,24,85,47,30,53,91)


初始时把要排序的n个数的序列看作是一棵顺序存储的二叉树(一维数组存储二叉树),调整它们的存储序,使之成为一个堆,将堆顶元素输出,得到n 个元素中最小(或最大)的元素,这时堆的根节点的数最小(或者最大)。然后对前面(n-1)个元素重新调整使之成为堆,输出堆顶元素,得到n 个元素中次小(或次大)的元素。依此类推,直到只有两个节点的堆,并对它们作交换,最后得到有n个节点的有序序列。称这个过程为堆排序。 因此,实现堆排序需解决两个问题: 1. 如何将n 个待排序的数建成堆; 2. 输出堆顶元素后,怎样调整剩余n-1 个元素,使其成为一个新堆。

首先讨论第二个问题:输出堆顶元素后,对剩余n-1元素重新建成堆的调整过程。 调整小顶堆的方法:

1)设有m 个元素的堆,输出堆顶元素后,剩下m-1 个元素。将堆底元素送入堆顶((最后一个元素与堆顶进行交换),堆被破坏,其原因仅是根结点不满足堆的性质。

2)将根结点与左、右子树中较小元素的进行交换。

3)若与左子树交换:如果左子树堆被破坏,即左子树的根结点不满足堆的性质,则重复方法 (2).

4)若与右子树交换,如果右子树堆被破坏,即右子树的根结点不满足堆的性质。则重复方法 (2).

5)继续对不满足堆性质的子树进行上述交换操作,直到叶子结点,堆被建成。

称这个自根结点到叶子结点的调整过程为筛选。如图:

再讨论对n 个元素初始建堆的过程。 建堆方法:对初始序列建堆的过程,就是一个反复进行筛选的过程。

1)n 个结点的完全二叉树,则最后一个结点是第个结点的子树。

2)筛选从第个结点为根的子树开始,该子树成为堆。

3)之后向前依次对各结点为根的子树进行筛选,使之成为堆,直到根结点。

如图建堆初始过程:无序序列:(49,38,65,97,76,13,27,49)

	void HeapAdjust(vector& a, int parent, int length) {
		int tmp = a[parent];
		int child = 2 * parent + 1; //左孩子节点的位置
		while (child < length) {
			if (child + 1 < length && a[child] < a[child + 1])
				++child;//找到比当前待调整节点大的孩子节点
			if ( a[parent] < a[child]){// 如果父结点小于较大的子结点 
				a[parent] = a[child];  // 那么把较大的子结点往上移动,替换它的父结点 
				parent = child;        // 重新设置s ,即待调整的下一个结点的位置  
				child = 2 * parent + 1;
			}
			else
				break; // 如果当前待调整结点大于它的左右孩子,则不需要调整,直接退出
			a[parent] = tmp;// 当前待调整的结点放到比其大的孩子结点位置上  
		}
	}
	void HeapBuild(vector& a) {
		//最后一个有孩子的节点的位置为(a.size()-1)/2
		for (int i = (a.size() - 1) / 2; i >= 0; --i)
			HeapAdjust(a, i, a.size());
	}
	void HeapSort(vector& a) {
		HeapBuild(a); //构建初始堆
		for (int i = a.size()-1; i >=0 ; --i) {//从最后一个元素开始对序列进行调整 
			swap(a[0], a[i]);			
			HeapAdjust(a, 0, i);//每次交换堆顶元素和堆中最后一个元素之后,都要对堆进行调整 
		}
	}
总结

说明:

当原表有序或基本有序时,直接插入排序和冒泡排序将大大减少比较次数和移动记录的次数,时间复杂度可降至On);

而快速排序则相反,当原表基本有序时,将蜕化为冒泡排序,时间复杂度提高为On2);

原表是否有序,对简单选择排序、堆排序、归并排序和基数排序的时间复杂度影响不大。

选择排序算法准则: 每种排序算法都各有优缺点。因此,在实用时需根据不同情况适当选用,甚至可以将多种方法结合起来使用。 选择排序算法的依据 影响排序的因素有很多,平均时间复杂度低的算法并不一定就是最优的。相反,有时平均时间复杂度高的算法可能更适合某些特殊情况。同时,选择算法时还得考虑它的可读性,以利于软件的维护。一般而言,需要考虑的因素有以下四点:
1.待排序的记录数目n的大小;
2.记录本身数据量的大小,也就是记录中除关键字外的其他信息量的大小;
3.关键字的结构及其分布情况;
4.对排序稳定性的要求。

设待排序元素的个数为n. 1)当n较大,则应采用时间复杂度为O(nlog2n)的排序方法:快速排序、堆排序或归并排序。 快速排序:是目前基于比较的内部排序中被认为是最好的方法,当待排序的关键字是随机分布时,快速排序的平均时间最短; 堆排序 : 堆排序适合于数据量非常大的场合(百万数据)。 堆排序不需要大量的递归或者多维的暂存数组。这对于数据量非常巨大的序列是合适的。比如超过数百万条记录,因为快速排序,归并排序都使用递归来设计算法,在数据量非常大的时候,可能会发生堆栈溢出错误。 堆排序会将所有的数据建成一个堆,最大的数据在堆顶,然后将堆顶数据和序列的最后一个数据交换。接下来再次重建堆,交换数据,依次下去,就可以排序所有的数据。 归并排序:它有一定数量的数据移动,所以我们可能过与插入排序组合,先获得一定长度的序列,然后再合并,在效率上将有所提高。 2)当n较大,内存空间允许,且要求稳定性 =》归并排序 3)当n较小,可采用直接插入或直接选择排序。 直接插入排序:当元素分布有序,直接插入排序将大大减少比较次数和移动记录的次数。 直接选择排序 :元素分布有序,如果不要求稳定性,选择直接选择排序 4)一般不使用或不直接使用传统的冒泡排序。

文章部分内容摘自http://blog.csdn.net/hguisu/article/details/7776068



你可能感兴趣的:(C++,数据结构和算法)