八种常见排序算法细讲

Posted 流浪孤儿

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了八种常见排序算法细讲相关的知识,希望对你有一定的参考价值。

目录

常见的八种排序

直接插入排序

希尔排序

直接选择排序

堆排序

冒泡排序 

快速排序

hoare版本 

挖坑法

前后指针版

快速排序代码

归并排序 

计数排序


 

常见的八种排序

 直接插入排序

⾸先,我们将数组中的数据分为两个区间,已排序区间和未排序区间。初始已排序区间只有⼀个元素,就是数组的第⼀个元素。插⼊算法的核⼼思想是取未排序区间中的元素,在已排序区间中找到合适的插⼊位置将其插⼊,并保证已排序区间数据⼀直有序。重复这个过程,直到未排序区间中元素为空,算法结束

如图所示,要排序的数据是4,5,6,1,3,2,其中左侧为已排序区间,右侧是未排序区间。

 插⼊排序包含两种操作,⼀种是元素的⽐较,⼀种是元素的移动。比如我们需要将⼀个数据3插⼊
到已排序区间[1,4,5,6]时,需要拿3与已排序区间的元素6,5,4,1依次⽐较⼤⼩,找到合适的插⼊位置。找到插⼊点之后,我们还需要将插⼊点之后的元素顺序往后移动⼀位,这样才能腾出位置给元素3插⼊。比如3和6比较,此时就可以将3和6的位置进行交换,依次类推,3再和5交换,3再和4交换,当3和1比较发现3比1大就不用再交换了,完成了3的插入过程了

C代码

 输出结果

 时间复杂度O(N^2),空间复杂度O(1)

稳定性:稳定

稳定性的说明

 图中红色的5在排完序后依旧在蓝色的5后面,这就是稳定的表现

希尔排序

 希尔排序可以看成是对直接插入排序的优化:我们可以看到直接插入排序的缺点即对一个降序的数组进行升序那么时间复杂度为O(N^2),但是对该数组进行降序那么时间复杂度就为O(N)了,此时大家会想该数组都是降序的了,你再对其降序图啥呢?这里想阐述的观点是如果将该数组变为

接近有序的状态那么使用直接插入排序其时间复杂度不就降下来了吗,希尔排序就是用了这个思想

对直接插入排序进行了优化!!!

 

 执行结果

 时间复杂度O(N^logN),空间复杂度O(1)

稳定性:不稳定

直接选择排序

 基本思想:一次挑一个数据:每一次从待排序的数据元素中选出最小(或最大)的一个元素,存放在序列的起始位置,直到全部待排序的数据元素排完

 排序结果:

 堆排序

 注意:使用堆排序首先需要理解什么是堆,大堆与小堆的区别,这里就不对堆的概念进行说明

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

将数组key=[20,17,4,16,5,3]建立一个大堆,对该数组排升序

 

执行结果:

 上面代码中数组arr已经是个大堆了,这里只是对堆排序的过程进行了说明,

如何将一个普通的数组建立成一个大堆这里就不作讲述

  1. 时间复杂度:O(N*logN)
  2. 空间复杂度:O(1)
  3. 稳定性:不稳定

冒泡排序 

 冒泡排序只会操作相邻的两个数据。每次冒泡操作都会对相邻的两个元素进⾏⽐较,看是否满⾜⼤⼩关系要求。如果不满⾜就让它俩互换。图中相邻的元素如果左边的元素大于右边的元素,那么就进行交换,即相邻的两个元素右边总是较大的。

 

  1. 时间复杂度:O(N^2)
  2. 空间复杂度:O(1)
  3. 稳定性:稳定

 快速排序

快速排序是Hoare1962年提出的一种二叉树结构的交换排序方法,其基本思想为:任取待排序元素序列中 的某元素作为基准值,按照该排序码将待排序集合分割成两子序列,左子序列中所有元素均小于基准值,右子序列中所有元素均大于基准值,然后对左右子序列重复该过程,直到所有元素都排列在相应位置上为止。

将区间按照基准值划分为左右两半部分的常见方式有:

  1. hoare版本
  2. 挖坑法
  3. 前后指针版本

 

  1. 三数取中法选key可以保证不会出现最坏的情况,而且当数据有序的时候就是最好的情况)
  2. 递归到小的子区间时,可以考虑使用插入排序

//快排,时间复杂度,最好的情况O(N*log2(N)),最坏O(N^2)
//优化方法1:三数取中,避免快排出现最坏的情况
int GetMidIndex(int* a, int left, int right)
{
	int mid = (left + right) >> 1;//移位的效率比除以2的效率要高一点
	// left  mid  right
	if (a[left] < a[mid])
	{
		if (a[mid] < a[right])
		{
			return mid;
		}
		else if (a[left] > a[right])
		{
			return left;
		}
		else
		{
			return right;
		}
	}
	else // a[left] >= a[mid]
	{
		if (a[mid] > a[right])
		{
			return mid;
		}
		else if (a[left] < a[right])
		{
			return left;
		}
		else
		{
			return right;
		}
	}
}

hoare版本 

 

int PartSort1(int* a, int left, int right)//排序一趟就返回相遇点
{
	int midIndex = GetMidIndex(a, left, right);//使用三数取中
	Swap(&a[left], &a[midIndex]);//将三数取中后的结果与最左边的值进行交换

	int keyi = left;
	while (left < right)
	{
		// 找小
		while (left < right && a[right] >= a[keyi])
			--right;

		// 找大
		while (left < right && a[left] <= a[keyi])
			++left;

		Swap(&a[left], &a[right]);
	}
	Swap(&a[keyi], &a[left]);

	return left;//返回相遇的位置,也就是keyi
}

挖坑法

 

int PartSort2(int* a, int left, int right)
{
	int midIndex = GetMidIndex(a, left, right);//使用三数取中
	Swap(&a[left], &a[midIndex]);//将三数取中后的结果与最左边的值进行交换
	int key = a[left];//将最左边的值给key,然后将最左边的视为坑(没有数据的意思)
	while (left < right)
	{
		// 找小
		while (left < right && a[right] >= key)
		{
			--right;
		}

		// 放到左边的坑位中,右边就形成新的坑
		a[left] = a[right];

		// 找大
		while (left < right && a[left] <= key)
		{
			++left;
		}

		// 放到右边的坑位中,左边就形成新的坑
		a[right] = a[left];
	}

	a[left] = key;//最后相遇点一定是坑,将key放到坑中

	return left;//返回相遇点,也就是key值所在的位置
}

前后指针版

 

int PartSort3(int* a, int left, int right)
{
	int midIndex = GetMidIndex(a, left, right);
	Swap(&a[left], &a[midIndex]);

	int keyi = left;
	int prev = left, cur = left + 1;
	while (cur <= right)
	{
		if (a[cur] < a[keyi] && ++prev != cur)//cur找比keyi小的数
		{
			Swap(&a[cur], &a[prev]);
		}
		++cur;
	}
	Swap(&a[keyi], &a[prev]);
	return prev;//最后返回prev的位置,也就是keyi,这里的keyi表示的是下标
}

快速排序代码

 上面的都是各版本进行单趟排序的代码

void QuickSort(int* a, int begin, int end)//(用的是hoare法)
{
	if (begin >= end)//[begin,end]区间为0或者区间不存在则返回
		return;

	// 1、如果这个子区间是数据较多,继续选key单趟,分割子区间分治递归
	// 2、如果这个子区间是数据较小,再去分治递归不太划算
	//此时越往后递归,子区间就越多,每个子区间的数据就越少,每个子区间都要递归就不划算,
	//可以在后面进行插入排序,因为此时每个子区间是接近有序的,接近于希尔排序了

	if (end - begin > 0)//小区间优化的效果没那么明显,如果对相应数据量级进行针对性的调
		//往往数据量越大,比如将20换成1000效果就明显了,20是官方给的,官方不敢给大了
	{
		int keyi = PartSort1(a, begin, end);
		//int keyi = PartSort2(a, begin, end);
		//int keyi = PartSort3(a, begin, end);


		// [begin, keyi-1] keyi [keyi+1, end]
		QuickSort(a, begin, keyi - 1);//递归
		QuickSort(a, keyi + 1, end);//递归
	}
	else
	{
		InsertSort(a + begin, end - begin + 1);
		//HeapSort(a + begin, end - begin + 1);也可以换成其如堆排,希尔排序效果会更好
	}
}
  1. 时间复杂度:O(N*logN)
  2. 空间复杂度:O(logN)
  3. 稳定性:不稳定

归并排序 

归并排序(MERGE-SORT)是建立在归并操作上的一种有效的排序算法,该算法是采用分治法(Divide and Conquer)的一个非常典型的应用。将已有序的子序列合并,得到完全有序的序列;即先使每个子序列有序,再使子序列段间有序。若将两个有序表合并成一个有序表,称为二路归并。 归并排序核心步骤:

 

void _MergeSort(int* a, int left, int right, int* tmp)
{
	if (left >= right)
		return;

	int mid = (left + right) >> 1;
	// [left, mid][mid+1,right]
	_MergeSort(a, left, mid, tmp);//先递归进行分治
	_MergeSort(a, mid + 1, right, tmp);

	// 两段有序子区间归并tmp,并拷贝回去
	int begin1 = left, end1 = mid;
	int begin2 = mid + 1, end2 = right;
	int i = left;
	while (begin1 <= end1 && begin2 <= end2)
	{
		if (a[begin1] < a[begin2])
			tmp[i++] = a[begin1++];
		else
			tmp[i++] = a[begin2++];
	}

	while (begin1 <= end1)
		tmp[i++] = a[begin1++];

	while (begin2 <= end2)
		tmp[i++] = a[begin2++];

	// 归并完成以后,拷贝回到原数组
	for (int j = left; j <= right; ++j)
		a[j] = tmp[j];
}

void MergeSort(int* a, int n)
{
	int* tmp = (int*)malloc(sizeof(int)*n);//创建临时数组
	if (tmp == NULL)
	{
		printf("malloc fail\\n");
		exit(-1);
	}

	_MergeSort(a, 0, n - 1, tmp);

	free(tmp);
}
  1. 时间复杂度:O(N*logN)
  2. 空间复杂度:O(N)
  3. 稳定性:稳定

 计数排序

 

//计数排序
void CountSort(int* a, int n)
{
	int min = a[0];//记录数组中的最小值
	int max = a[0];//记录数组中的最大值
	for (int i = 0; i < n; i++)
	{
		if (a[i] < min)
			min = a[i];
		if (a[i] > max)
			max = a[i];
	}
	int range = max - min + 1;//min和max之间的自然数个数(包括min和max本身)
	int* count = (int*)calloc(range, sizeof(int));//开辟可储存range个整型的内存空间,并将内存空间置0
	if (count == NULL)
	{
		printf("malloc fail\\n");
		exit(-1);
	}
	//统计相同元素出现次数(相对映射)
	for (int i = 0; i < n; i++)
	{
		count[a[i] - min]++;
	}
	int i = 0;
	//根据统计结果将序列回收到原来的序列中
	for (int j = 0; j < range; j++)
	{
		while (count[j]--)
		{
			a[i++] = j + min;
		}
	}
	free(count);//释放空间
}
  1. 时间复杂度:O(MAX(N+范围))
  2. 空间复杂度:O(范围)
  3. 稳定性:稳定

以上是关于八种常见排序算法细讲的主要内容,如果未能解决你的问题,请参考以下文章

数据结构-八种排序算法

数据结构-八种排序算法

JS排序算法总结:八种算法对比

常见的七种排序算法(Java实现)

常见的七种排序算法(Java实现)

常见的七种排序算法(Java实现)