常用排序算法之【插入,希尔,快排,选择,堆排,归并,计数】详解

常用的排序算法

插入排序


插入排序的基本思想是:

  把待排序的记录按其关键码值的大小逐个插入到一个已经排好序的有序序列中,直到所有的记录插入完为止,得到一个新的有序序列 。

  将整个数组a分为有序无序的两个部分。前者在左边,后者在右边。开始有序的部分只有a[0] , 其余都属于无序的部分。每次取出无序部分的第一个(最左边)元素,把它加入有序部分。假设插入合适的位置p,则原p位置及其后面的有序部分元素都向右移动一个位置,有序的部分即增加了一个元素。一直做下去,直到无序的部分没有元素,排序完成。

演示:

初始状态: i = 0,end = 0;

在这里插入图片描述

temp > a[end] 已经有序 ,无需移动
i 自增 1

i = 1 ,end = 1;
在这里插入图片描述

temp < a[end] ,需要移动元素
在这里插入图片描述

第一步 :

a [ end+1 ] = a [ end ]
在这里插入图片描述

第二步 :
end 后移, end = 0
比较 temp < a[end] , a[end] < temp
将 temp 插入到 a[end+1]
在这里插入图片描述
在这里插入图片描述

之后的过程如上述所示,依次从后向前插入排序,直到数列全部有序

时间复杂度&空间复杂度

时间复杂度:

  在完全有序的情况下,插入排序每个未排序区间元素只需要比较1次,所以时间复杂度是O(n)。
而在极端情况完全逆序,时间复杂度为O(n^2).就等于每次都把未排序元素插入到数组第一位。在数组中插入1个元素的时间复杂度为O(n),那插入n个就是o(n ^2)了。

空间复杂度:
  常量空间,存储空间大小固定,和输入没有关系,空间复杂度是O(1)

代码实现:

void InsertSort(int a[], int length)
{
	int temp = 0, end = 0;     //初始将a[0]当作有序
	for (int i = 0; i < length - 1; i++)
	{            
		end = i;
		temp = a[i + 1];//记录有序数组右边的元素,即无序数组中第一个元素
		while (end>=0)
		{
			if (temp < a[end])//如果比有序数组的最后一个元素小,寻找合适的位置插入
			{
				a[end + 1] = a[end];
				end--;
			}
			else
				break;   //找到合适的位置,跳出循环
		}
		a[end+1] = temp;  //插入完成
	}
}

直接插入排序的特性总结:

  1. 元素集合越接近有序,直接插入排序算法的时间效率越高
  2. 时间复杂度:O(N^2)
  3. 空间复杂度:O(1),它是一种稳定的排序算法
  4. 稳定性:稳定

希尔排序

  希尔排序(Shellsort)也是一种插入排序,它是简单插入排序经过改进之后的一个更高效的版本,也称为缩小增量排序。希尔(Donald Shell)于1959年提出这种排序算法。 希尔排序是非稳定排序算法。

希尔排序基本思想:
  希尔排序是对插入排序的优化,基本思路是先选定一个整数作为增量,把待排序文件中的所有数据分组,以每个距离的等差数列为一组,对每一组进行排序,然后将增量缩小,继续分组排序,重复上述动作,直到增量缩小为1时,排序完正好有序。
​   希尔排序原理是每一对分组进行排序后,整个数据就会更接近有序,当增量缩小为1时,就是插入排序,但是现在的数组非常接近有序,移动的数据很少,所以效率非常高,所以希尔排序又叫缩小增量排序。
  ​ 每次排序让数组接近有序的过程叫做预排序,最后一次插入是直接插入排序

演示:

初始状态:gap = gap / 3 + 1 = 4 ; i = 0;

在这里插入图片描述
  以4为增量,从 i=0处每隔4个区元素,并选中的的元素看作一个子数组(这里为 1 2 7 )进行直接插入排序,这里已经有序

在这里插入图片描述
i = 1;

  同理,i自增1后,从i = 1 处开始,每隔4个元素当作子数组,以(6 6 5)看作一个子数组,进行直接插入排序,排序后为(5 6 6),如下图:

在这里插入图片描述
  之后的步骤依次类推。

时间复杂度&空间复杂度

时间复杂度:
  增量序列的选择会极大地影响希尔排序的效率。 希尔排序时间复杂度非常难以分析,它的平均复杂度界于 O(n) 到 O(n^2) 之间,普遍认为它最好的时间复杂度为 O(n ^1.3)

空间复杂度:
  常量空间,存储空间大小固定,和输入没有关系,空间复杂度是O(1)

代码实现:

void ShellSort(int a[], int length)
{
	int gap = length;
	while (gap > 1)
	{
		gap = gap / 3 + 1;//希尔排序每一趟需要一个增量,该增量会逐渐减小,当gap为1时就是直接插入排序
		for (int i = 0; i < length - gap; i++)  //这里length - gap 是防止后序temp = a[end + gap] 时越界  
		{
			int end = i;
			int temp = a[end + gap]; 
			while (end >= 0)
			{
				if (a[end] > temp)
				{
					a[end + gap] = a[end];
					end -= gap;
				}
				else
					break;	
			}
			a[end+gap] = temp;
		}
	}
}

希尔排序的特性总结:

  1. 希尔排序是对直接插入排序的优化。
  2. 当gap > 1时都是预排序,目的是让数组更接近于有序。当gap == 1时,数组已经接近有序的了,这样就
    会很快。这样整体而言,可以达到优化的效果。我们实现后可以进行性能测试的对比。
  3. 希尔排序的时间复杂度不好计算,因为gap的取值方法很多,导致很难去计算,因此在好些树中给出的
    希尔排序的时间复杂度都不固定:
    因为咋们的gap是按照Knuth提出的方式取值的,而且Knuth进行了大量的试验统计,我们暂时就按照:
    到 来算。
  4. 稳定性:不稳定

快速排序

  快速排序是Hoare于1962年提出的一种二叉树结构的交换排序方法,其基本思想为:
  任取待排序元素序列中的某元素作为基准值,按照该排序码将待排序集合分割成两子序列,左子序列中所有元素均小于基准值,右子序列中所有元素均大于基准值,然后最左右子序列重复该过程,直到所有元素都排列在相应位置上为止。
  通过一趟排序将要排序的数据分割成独立的两部分,其中一部分的所有数据都比另外一部分的所有数据都要小,然后再按此方法对这两部分数据分别进行快速排序,整个排序过程可以递归进行,以此达到整个数据变成有序序列。

  快速排序算法通过多次比较和交换来实现排序,其流程如下:

  1. 首先设定一个分界值,通过该分界值将数组分成左右两部分。
  2. 将大于或等于分界值的数据集中到数组右边,小于分界值的数据集中到数组的左边。此时,左边部分中各元素都小于或等于分界值,而右边部分中各元素都大于或等于分界值。
  3. 然后,左边和右边的数据可以独立排序。对于左侧的数组数据,又可以取一个分界值,将该部分数据分成左右两部分,同样在左边放置较小值,右边放置较大值。右侧的数组数据也可以做类似处理。
  4. 重复上述过程,可以看出,这是一个递归定义。通过递归将左侧部分排好序后,再递归排好右侧部分的顺序。当左、右两个部分各数据排序完成后,整个数组的排序也就完成了。

演示:

程序开始时数组元素

在这里插入图片描述
a [ start ] > a [ end ] ,交换对应的值,从反方向开始start++
(注:每次交换值后,遍历的方向就会换向,程序开始是从end处 - -,交换首尾后,从start处 + +,之后每次交换都需要变换)

在这里插入图片描述
a [ start ] < a [ end ] ; start ++;

在这里插入图片描述a [ start ] < a [ end ] ; start ++;

在这里插入图片描述
a [ start ] < a [ end ] ; start ++;

在这里插入图片描述
a [ start ] < a [ end ] ; start ++;

在这里插入图片描述
注:这里交换了元素,遍历方向会变换,从 start++ 变为 end- -,再继续遍历

在这里插入图片描述
a [ start ] < a [ end ] ; end - -;

以此类推,直到:
在这里插入图片描述
这里再进行交换后变为:
在这里插入图片描述
start++ 此时start==end,退出循环,返回start值,并记录为mid,继续递归进行下一次快速排序。
后序递归快速排序分为(0到mid)和(mid+1到end),依次类推,直到数组全部有序.
后续过程类似,不再展示。

时间复杂度&空间复杂度

时间复杂度:
  第一轮遍历排好 1 个基数,第二轮遍历排好 2 个基数,第三轮遍历排好 4 个基数,以此类推。总遍历次数为 logn~n 次,每轮遍历的时间复杂度为 O(n),最坏的时间复杂度为 O(n^ 2),所以很容易分析出快速排序的时间复杂度为 O(nlogn) ~ O(n^2),平均时间复杂度为 O(nlogn)。

空间复杂度:
  空间复杂度与递归的层数有关,每层递归会生成一些临时变量,所以空间复杂度为 O(logn)~O(n),平均空间复杂度为 O(logn)。

代码实现 :

//快速排序
void QuickSort(int a[],int first,int end)
{
	if (first == end)
		return;
	int mid = partition(a, first, end);
	QuickSort(a,first,mid);
	QuickSort(a, mid+1, end);
}

int partition(int a[],int first,int last)
{
	int start = first, end = last, temp;
	while (start < end)
	{
		while (start < end && a[start] <= a[end])
			end--;
		if (start < end)
		{
			temp = a[start];
			a[start] = a[end];
			a[end] = temp;
			start++;
		}                           //加等号
		while (start < end && a[start] <= a[end])
			start++;
		if (start < end)
		{
			temp = a[start];
			a[start] = a[end];
			a[end] = temp;
			end--;
		}
	}
	return start;
}

快速排序的特性总结:

  1. 快速排序整体的综合性能和使用场景都是比较好的,所以才敢叫快速排序
  2. 时间复杂度:O(N*logN)
  3. 空间复杂度:O(logN)
  4. 稳定性:不稳定

选择排序

选择排序的基本思想是:

  如果有N个元素需要排序,那么首先从N个元素中找到最小的那个元素与第0位置上的元素交换然后再从剩下的N-1个元素中找到最小的元素与第1位置上的元素交换,之后再从剩下的N-2个元素中找到最小的元素与第2位置上的元素交换,直到所有元素都排序好。

简单选择排序

直接上代码,原理就是每次在无序区选择最小的排列在有序区的最后,思想很简单,不过多解释
代码实现:

void SelectionSort(int a[], int numsize)
{
	for(int i = 0;i < numsize-1;i++)
	{
		int min = i;
		for(int j = i+1;j < numsize;j++)
		{
			if(a[j] < a[min])
				min = j;		
		}
		int temp = a[i];
		a[i] = a[min];
		a[min] = temp; 
	}
}

时间复杂度&空间复杂度

时间复杂度:
  简单选择排序在最好情况下也就是数组完全有序的时候,无需任何交换移动;在最差情况下,也就是数组倒序的时候,交换次数为n-1次。
  综合下来:时间复杂度为O(n2)

空间复杂度:
  空间复杂度为O(1)


堆排序

  堆排序的基本思想是:
  将待排序序列构造成一个大顶堆,此时,整个序列的最大值就是堆顶的根节点。将其与末尾元素进行交换,此时末尾就为最大值。然后将剩余n - 1个元素重新构造成一个堆,这样会得到n个元素的次大值,并与n - 1位置的元素再进行交换,此时n - 1位置就为次大值。如此反复执行,便能得到一个有序序列了。

  堆排序(Heapsort)利用堆积树(堆)这种数据结构所设计的一种排序算法,它是选择排序的一种。它是通过堆来进行选择数据。需要注意的是排升序要建大堆,排降序建小堆。

在将堆排序时,先要知道大根堆和小根堆的概念

  • 根结点比左右孩子都则叫做大根堆。其中,左右孩子的根结点,又会比它们的左右孩子都大。
  • 根结点比左右孩子都则叫做小根堆。其中,左右孩子的根结点,又会比它们的左右孩子都小。

大根堆:
在这里插入图片描述
小根堆:
在这里插入图片描述

下面演示堆排序的升序排序:

初始化堆,首先建立大堆
将数组中的元素依次填入堆
在这里插入图片描述

数组一共10个元素,numsize=10,(10-1-1) / 2 = 4 ,得出最后一个元素5的双亲节点下标为4
(注:从堆顶开始为数组下标0,即元素6;第二层元素1和3下标分别为1和2;依次类推)
从下标4开始依次向下调整,再从下标3开始向下调整,…直到调整到下标 0 结束(即从最后一个元素的双亲节点开始调整,再从该双亲节点的上一个元素开始,即下标3,再下标2…直到0(堆顶))

调整为大堆

从下标4开始调整
2 < 5
2 与 5 进行交换
孩子节点(下标9)已经没有孩子,结束此次调整
在这里插入图片描述

再从下标3(元素 7)开始调整
选取下标3(元素 7)的孩子(9和7)较大者,即9
与下标3(元素 7)相比较,7 < 9 ,交换
孩子节点已经没有孩子,结束此次调整
在这里插入图片描述

从下标2(元素 3)开始调整
选取下标2(元素 3)的孩子(6和8)较大者,即8
与下标2(元素 3)相比较,3 < 8 ,交换
孩子节点已经没有孩子,结束此次调整在这里插入图片描述

从下标1(元素 1)开始调整
选取下标1(元素 1)的孩子(9和5)较大者,即9
与下标1(元素 1)相比较,1 < 9 ,交换
在这里插入图片描述

孩子节点(元素1)还有孩子节点(元素7和7),则继续调整(继续调整时,原孩子节点(即元素1)变为双亲,新的双亲(元素1)的孩子更新为新的孩子(元素7与7))
更新后从下标4(元素 1)开始调整
选取下标4(元素 1)的孩子(7和7)较大者,这里一样大,随机选取一个,即7
与下标4(元素 1)相比较,1 < 7 ,交换
孩子节点已经没有孩子,结束此次调整
在这里插入图片描述

从下标0(元素 6)开始调整
选取下标0(元素 6)的孩子(9和8)较大者,即9
与下标1(元素 6)相比较,6 < 9 ,交换
在这里插入图片描述

孩子节点(元素6)还有孩子节点(元素7和5),则继续调整(继续调整时,原孩子节点(即元素6)变为双亲,新的双亲(元素6)的孩子更新为新的孩子(元素7与5))
更新后从下标1(元素 6)开始调整
选取下标1(元素 6)的孩子(7和5)较大者,即7
与下标1(元素 6)相比较,6 < 7 ,交换
孩子节点已经没有孩子,结束此次调整
在这里插入图片描述

孩子节点(元素6)还有孩子节点(元素1和7),则继续调整(继续调整时,原孩子节点(即元素6)变为双亲,新的双亲(元素6)的孩子更新为新的孩子(元素1与7))
更新后从下标4(元素 6)开始调整
选取下标4(元素 6)的孩子(1和7)较大者,即7
与下标4(元素 6)相比较,6 < 7 ,交换
孩子节点已经没有孩子,结束此次调整
在这里插入图片描述

全部调整完毕,最终调整后的大堆为
在这里插入图片描述

上述过程完成建堆并且调整成大堆,接下来进行堆排序

  在堆排序时,将将堆顶元素与下标为numsize-1的元素交换,交换后numsize减1。
  交换后数组中下标为numsize-1的元素即为整个堆中的最大值,此时numsize --,意味着数组长度减一,将刚交换的元素排除在堆外,因为交换到numsize-1处的元素已经在最终位置,不需要继续参与后续的排序。
  重新调整堆,重复此过程直到数组中全部元素都有序。

将9与堆最后一个元素交换,numsize减一,此时numsize = 9
在这里插入图片描述
  调换后,若堆不满足大堆,选择堆顶元素进行向下调整,将堆顶元素(下标为0)与其孩子(下标1和2的元素)的较大者交换,交换后若满足大堆,则结束调整,否则孩子变为双亲,新的双亲与其孩子继续向下调整,直到调整后是大堆为止(这里步骤不再演示)

调整后结果为
在这里插入图片描述
将8与堆最后一个元素交换,numsize减一,此时numsize = 8

在这里插入图片描述
将堆顶元素6向下调整(调整规则如上述加红部分)


调整后结果为
在这里插入图片描述
将7与堆最后一个元素交换,numsize减一,此时numsize = 7


在这里插入图片描述

接下来重复该步骤,直到数组中全部序列按升序排序完毕,后需过程不再演示。

代码实现:

typedef int Datatype;
typedef struct Heap
{
	int size;    //个数
	int capacity;  //容量
	Datatype* nums;
}Heap;

//堆初始化
void HeapInit(Heap* heap)
{
	assert(heap);
	heap->capacity = heap->size = 0;
	heap->nums = NULL;
}

void AdjustDown(Datatype* a, int n, int parent)
{
	assert(a);
	int child = parent * 2 + 1;
	while (child < n)
	{                                  //始终选择较大的元素,使其排在堆的上方
		if (child + 1 < n && a[child + 1] < a[child])  
		{
			child++;
		}          
		if (a[child] < a[parent])
		{
			swap(&a[child], &a[parent]);
			parent = child;
			child = parent * 2 + 1;
		}
		else
			break;
	}
}

void HeapSort(Datatype* a, int n)
{
//这里n-1代表了数组中最后一个元素的下标,再-1是为了计算双亲节点而减的,因为在堆中已知一个节点的下标,那么其双亲节点的下标就为该节点减1除以2;
 	for (int i = (n - 1 - 1) / 2; i >= 0; --i)
	{
		AdjustDown(a, n, i);
	}
	//上面的循环将数组进行初始建堆
	//下面的代码进行堆排序
	int end = n - 1;
	while (end > 0)
	{
		swap(&a[0], &a[end]);
		AdjustDown(a, end, 0);
		--end;
	}
}

时间复杂度&空间复杂度

时间复杂度:
  初始化建堆的时间复杂度为O(n),排序重建堆的时间复杂度为nlog(n),所以总的时间复杂度为O(n+nlogn)=O(nlogn)。另外堆排序的比较次数和序列的初始状态有关,但只是在序列初始状态为堆的情况下比较次数显著减少,在序列有序或逆序的情况下比较次数不会发生明显变化。

空间复杂度:
  因为堆排序是就地排序,空间复杂度为常数:O(1)

归并排序

  1945年,约翰·冯·诺依曼(John von Neumann)发明了归并排序,这是典型的分治算法的应用。

定义
  归并排序(Merge sort)是建立在归并操作上的一种有效的排序算法,该算法是采用分治法(Divide and Conquer)的一个非常典型的应用。将已有序的子序列合并,得到完全有序的序列;即先使每个子序列有序,再使子序列段间有序。

算法思路

  • 归并排序算法有两个基本的操作,一个是分,也就是把原数组划分成两个子数组的过程。另一个是治,它将两个有序数组合并成一个更大的有序数组。
  • 将待排序的线性表不断地切分成若干个子表,直到每个子表只包含一个元素,这时,可以认为只包含一个元素的子表是有序表。
  • 将子表两两合并,每合并一次,就会产生一个新的且更长的有序表,重复这一步骤,直到最后只剩下一个子表,这个子表就是排好序的线性表。

演示:
在这里插入图片描述

代码实现:

//归并排序辅助函数1
void Merges(int a[], int start, int mid, int end, int temp[])
{
	int first = start, mids = mid + 1, last = end, k = 0;
	while (first <= mid && mids <= last)
	{
		if (a[first] < a[mids])
			temp[k++] = a[first++];
		else
			temp[k++] = a[mids++];
	}
	while (first <= mid)
		temp[k++] = a[first++];
	while (mids <= last)
		temp[k++] = a[mids++];
	for (int i = 0; i < k; i++)//在以上代码运行完毕后,k就为当前数组元素个数,这时需要将辅助数组temp里面的内容复制到原数组中去,k不能取等号
		a[start + i] = temp[i];
}

//归并排序辅助函数2  (递归)
void Merge(int a[], int start, int end, int temp[])
{
	if (start >= end)
		return;
	int mid = start + (end - start) / 2;  //取中,防止溢出
	Merge(a, start, mid, temp);         //左右分治
	Merge(a, mid + 1, end, temp);    //左右分治
	Merges(a, start, mid, end, temp);   //最终排序
}

//归并排序
void Merge_sort(int a[], int length)
{
	int* temp = (int*)malloc(sizeof(int) * length);
	Merge(a, 0, length - 1, temp);
	//Merge(a, length, temp);
	free(temp);
}

时间复杂度&空间复杂度

时间复杂度:
  假设我们需要对一个包含n个数的序列使用归并排序,并且使用的是递归的实现方式,那么过程如下:

递归的第一层,将n个数划分为2个子区间,每个子区间的数字个数为n/2;
递归的第二层,将n个数划分为4个子区间,每个子区间的数字个数为n/4;
递归的第三层,将n个数划分为8个子区间,每个子区间的数字个数为n/8;

递归的第logn层,将n个数划分为n个子区间,每个子区间的数字个数为1;
  在整个归并排序的过程中,每一层的子区间,长度都是上一层的1/2。分析可知,当子区间的长度为1时,共划分了logn层。而归并排序的merge操作,则是从最底层开始(子区间为1的层),对相邻的两个子区间进行合并,过程如下:

在第logn层(最底层),每个子区间的长度为1,共n个子区间,每相邻两个子区间进行合并,总共合并n/2次。n个数字都会被遍历一次,所有这一层的总时间复杂度为O(n);
  …

在第2层,每个子区间长度为n/4,总共有4个子区间,每相邻两个子区间进行合并,总共合并2次。n个数字都会被遍历一次,所以这一层的总时间复杂度为O(n);
在第1层,每个子区间长度为n/2,总共有2个子区间,只需要合并一次。n个数字都会被遍历一次,所以这一层的总时间复杂度为O(n);
  通过上面的过程我们可以发现,对于每一层来说,在合并所有子区间的过程中,n个元素都会被操作一次,所以每一层的时间复杂度都是O(n),共有logn层,所以归并排序的时间复杂度就是O(nlogn)。

空间复杂度:
  因为归并排序需要一个与原数组等长的辅助数组,空间复杂度为:O(n)

计数排序

概述
  计数排序是一个非基于比较的排序算法,元素从未排序状态变为已排序状态的过程,是由额外空间的辅助和元素本身的值决定的。该算法于1954年由 Harold H. Seward 提出。它的优势在于在对一定范围内的整数排序时,它的复杂度为Ο(n+k)(其中k是整数的范围),快于任何比较排序算法。当然这是一种牺牲空间换取时间的做法,而且当 的时候其效率反而不如基于比较的排序,因为基于比较的排序的时间复杂度在理论上的下限是 。

算法思路
计数排序对输入的数据有附加的限制条件:

1、输入的线性表的元素属于有限偏序集 S;

2、设输入的线性表的长度为 n,|S|=k(表示集合 S 中元素的总数目为 k),则 k=O(n)。

在这两个条件下,计数排序的复杂性为O(n)。

  计数排序的基本思想是对于给定的输入序列中的每一个元素 x,确定该序列中值小于 x 的元素的个数(此处并非比较各元素的大小,而是通过对元素值的计数和计数值的累加来确定)。一旦有了这个信息,就可以将 x 直接存放到最终的输出序列的正确位置上。例如,如果输入序列中只有 17 个元素的值小于 x 的值,则 x 可以直接存放在输出序列的第 18 个位置上。当然,如果有多个元素具有相同的值时,我们不能将这些元素放在输出序列的同一个位置上,因此,上述方案还要作适当的修改。

算法过程

  • 根据待排序集合中最大元素和最小元素的差值范围,申请额外空间;
  • 遍历待排序集合,将每一个元素出现的次数记录到元素值对应的额外空间内;
  • 对额外空间内数据进行计算,得出每一个元素的正确位置;
  • 将待排序集合每一个元素移动到计算得出的正确位置上。

总体就是记录各个元素的个数,再将其从小到大一 一 罗列。

void Count_sort(int a[], int numsize)
{
	int max = a[0], min = a[0];
	for (int i = 0; i < numsize; i++)
	{
		if (max < a[i])
			max = a[i];
		else if (min > a[i])
			min = a[i];
	}
	int length = max - min + 1, k = 0;
	int* ret = (int*)calloc(length, sizeof(int));
	for (int i = 0; i < numsize; i++)
	{
		ret[a[i] - min]++;
	}
	for (int i = 0; i < length; i++)
	{
		while (ret[i]--)
		{
			a[k++] = i + min;
		}
	}
}

时间复杂度&空间复杂度

时间复杂度:
  时间复杂度:O(N+range)。

空间复杂度:
  空间复杂度:O(range) range 是指数组中最大值到最小值的范围 。

各个排序算法性能比较

插入排序(InsertSort)
  插入排序通过把序列中的值插入一个已经排序好的序列中,直到该序列的结束。插入排序是对冒泡排序的改进。它比冒泡排序快2倍。一般不用在数据大于1000的场合下使用插入排序,或者重复排序超过200数据项的序列。

希尔排序(ShellSort)
  Shell排序通过将数据分成不同的组,先对每一组进行排序,然后再对所有的元素进行一次插入排序,以减少数据交换和移动的次数。平均效率是O(nlogn)。其中分组的合理性会对算法产生重要的影响。现在多用D.E.Knuth的分组方法。 Shell排序比冒泡排序快5倍,比插入排序大致快2倍。Shell排序比起QuickSort,MergeSort,HeapSort慢很多。但是它相对比较简单,它适合于数据量在5000以下并且速度并不是特别重要的场合。它对于数据量较小的数列重复排序是非常好的。
快速排序(QuickSort)
  快速排序是一个就地排序,分而治之,大规模递归的算法。从本质上来说,它是归并排序的就地版本。快速排序可以由下面四步组成。

选择排序(SelectSort)
  这两种排序方法都是交换方法的排序算法,效率都是 O(n2)。在实际应用中处于和冒泡排序基本相同的地位。它们只是排序算法发展的初级阶段,在实际中使用较少。
堆排序(HeapSort)
  堆排序适合于数据量非常大的场合(百万数据)。 堆排序不需要大量的递归或者多维的暂存数组。这对于数据量非常巨大的序列是合适的。比如超过数百万条记录,因为快速排序,归并排序都使用递归来设计算法,在数据量非常大的时候,可能会发生堆栈溢出错误。 堆排序会将所有的数据建成一个堆,最大的数据在堆顶,然后将堆顶数据和序列的最后一个数据交换。接下来再次重建堆,交换数据,依次下去,就可以排序所有的数据。
归并排序(MergeSort)
  归并排序先分解要排序的序列,从1分成2,2分成4,依次分解,当分解到只有1个一组的时候,就可以排序这些分组,然后依次合并回原来的序列中,这样就可以排序所有数据。合并排序比堆排序稍微快一点,但是需要比堆排序多一倍的内存空间,因为它需要一个额外的数组。
冒泡排序(BubbleSort)
  冒泡排序是最慢的排序算法。在实际运用中它是效率最低的算法。它通过一趟又一趟地比较数组中的每一个元素,使较大的数据下沉,较小的数据上升。它是O(n2)的算法。
计数排序(CountSort)
  计数排序是一个非基于比较的排序算法,它的优势在于在对一定范围内的整数排序时,它的复杂度为Ο(n+k)(其中k是整数的范围),快于任何比较排序算法。当然这是一种牺牲空间换取时间的做法。

各种排序方法的时空性能表

排序名称平均情况最好情况最坏情况空间性能
直接插入排序O(n2)O(n)O(n2)O(1)
希尔排序O( n l o g 2 n nlog_2n nlog2n)~O(n2)O(n1.3)O(n2)O(1)
冒泡排序O(n2)O(n)O(n2)O(1)
快速排序O( n l o g 2 n nlog_2n nlog2n)O( n l o g 2 n nlog_2n nlog2n)O(n2)O( l o g 2 n log_2n log2n))~O(n)
简单选择排序O(n2)O(n2)O(n2)O(1)
堆排序O( n l o g 2 n nlog_2n nlog2n)O( n l o g 2 n nlog_2n nlog2n)O( n l o g 2 n nlog_2n nlog2n)O(1)
归并排序O( n l o g 2 n nlog_2n nlog2n)O( n l o g 2 n nlog_2n nlog2n)O( n l o g 2 n nlog_2n nlog2n)O(n)
计数排序O(N+range)O(N+range)O(N+range)O(range)

时间复杂度

从平均情况看,有三类排序方法
  直接插入排序,简单选择排序和起泡排序属于一类。时间复杂度为O(n2),其中以直接插入排序方法最常用,特别是对于基本有序的记录序列。
  堆排序、快速排序和归并排序属于第二类,时间复杂度为O( n l o g 2 n nlog_2n nlog2n),其中快速排序目前被认为是最快的一种排序方法。在待排序记录个数较多的情况下,归并排序较堆,排序更快
  希尔排序介于O(n) 和O( n l o g 2 n nlog_2n nlog2n) 之间,从最好情况看,直接插入排序和起泡排序最好,时间复杂度为O(n) ,其他排序算法的最好情况与平均情况相同。从最坏情况看,快速排序的时间复杂度为O(n2) ,直接插入排序和起泡排序虽然与平均情况相同,但系数大约增加一倍,所以运行速度将降低一半。最坏情况对简单选择排序、堆排序和归并排序影响不大。
  由此可知,在最好情况下,直接插入排序和起泡排序最快。在平均情况下,快速排序最快,在最坏情况下堆排序和归并排序最快。

空间复杂度

  从空间性能看,所有排序方法分为三类:

  • 归并排序单独属于一类,空间复杂度为O(n) ;
  • 快速排序单独属于一类,空间复杂度为O( l o g 2 n log_2n log2n))~O(n);
  • 其他排序方法归为一类,空间复杂度为O(1);

稳定性

  所有排序方法可以分为两类:

  • 一类是稳定的,包括直接插入排序,起泡排序和归并排序。
  • 另一类是不稳定的,包括希尔排序,快速排序,简单选择排序和堆排序。

算法简单性

  从算法简单性看:

  • 一类是简单算法,包括直接插入排序,简单选择排序和起泡排序。
  • 另一类是改进算法,包括希尔排序堆排序,快速排序和归并排序。这些算法都很复杂。

待排序的记录个数

  从待排序的记录个数n的大小看,n越小采用简单排序方法最合适,n越大,采用改进的排序方法最合适,因为n越小,O( n l o g 2 n nlog_2n nlog2n)和O(n2) 的差距越小,并且属于并且输入和调试简单算法比输入和调试改进算法要少用许多时间

记录本身信息量的大小

  记录本身信息量的大小,从记录本身信息量的大小看,记录本身信息量越大,表明占用的空间就越多。移动记录所花费的时间就越多,所以对记录的移动次数较多的算法不利。
  当记录本身的信息量较大时,对简单选择排序算法有利。而对其他两种排序算法,不利本身记录本身信息量的大小对改进算法的影响不大
  下表给出了三种排序,算法中记录的移动次数的比较。

排序方法平均情况最好情况最坏情况
直接插入排序O(n2)O(n)O(n2)
起泡排序O(n2)0O(n2)
简单选择排序O(n)0O(n)

初始记录的分布情况

  当待排序序列为正序时,直接插入排序和起泡排序能够达到 O(n) 的时间复杂度。而对于快速排序而言,这是最快的情况。此时的时间性能退化为O(n2) ,简单选择排序、堆排序和归并排序的时间的性能不随序列中的记录分布而改变。

  • 4
    点赞
  • 12
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Ryan.Alaskan Malamute

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值