八大排序算法

Posted 北川_

tags:

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

目录

直接插入排序

直接插入排序是一种简单的插入排序法,其基本思想是:
把待排序的记录按其关键码值的大小逐个插入到一个已经排好序的有序序列中,直到所有的记录插入完为止,得到一个新的有序序列 。
当插入第i(i>=1)个元素时,前面的array[0],array[1],…,array[i-1]已经排好序,此时用array[i]的排序码与array[i-1],array[i-2],…的排序码顺序进行比较,找到插入位置即将array[i]插入,原来位置上的元素顺序后移。

代码:

void InsertSort(int* a, int n)

	for (int i = 0; i < n - 1; ++i)
	
		// 单趟插入
		int end = i;
		int tmp = a[end + 1];	// 待插入的元素
		while (end >= 0)
		
			if (tmp < a[end])
			
				// tmp < a[end] 说明tmp应该在a[end]的前面
				a[end + 1] = a[end];
				--end;
			
			else    // 找到插入位置
			
				break;
			
		
		a[end + 1] = tmp;
	

时间复杂度O(N2)。
最差情况待排序数组逆序,每个数都要与前面所有的数比较,时间复杂度O(N2)。
最好的情况升序,时间复杂度O(N)。
空间复杂度:O(1)

希尔排序(缩小增量排序)

希尔排序是D.L.Shell于1959年提出来的一种排序算法,在这之前排序算法的时间复杂度基本都是O(n2)的,希尔排序算法是突破这个时间复杂度的第一批算法之一。
希尔排序法又称缩小增量法。希尔排序法的基本思想是:
将原本有大量记录数的记录进行分组。分割成若干个子序列,此时每个子序列待排序的记录个数就比较少了,然后在这些子序列内分别进行直接插入排序,不断重复上述分组和排序的工作,当整个序列都基本有序时,再对全体记录进行一次直接插入排序。

代码:

void ShellSort(int* a, int n)

	// gap > 1 预排序
	// gap == 1 直接插入排序
	int gap = n;
	while (gap > 1)
	
		gap = gap / 3 + 1;
		// 间隔为gap的多组并排
		for (int i = 0; i < n - gap; ++i)
		
			int end = i;
			int tmp = a[end + gap];
			while (end >= 0)
			
				if (tmp < a[end])
				
					a[end + gap] = a[end];
					end -= gap;
				
				else
				
					break;
				
			
			a[end + gap] = tmp;
		
	

时间复杂度:
当增量大于1时,关键字较小的记录就不是一步一步地挪动,而是跳跃式地移动,从而使得在进行最后一趟增量为1的插入排序中,序列已基本有序,只要做记录的少量比较和移动即可完成排序,因此希尔排序的时间复杂度较直接插入排序低。但要具体进行分析,则是一个复杂的问题,因为希尔排序的时间复杂度是所取“增量”序列的函数,这涉及一些数学上尚未解决的难题。因此,到目前为止尚未有人求得一种最好的增量序列,但大量的研究已得出一些局部的结论。如有人指出,当增量序列为dt[k]=2t−k+1−1时,希尔排序的时间复杂度为O(n3/2),其中t为排序趟数, 。还有人在大量的实验基础上推出:当n在某个特定范围内,希尔排序所需的比较和移动次数约为n1.3,当n→∞时,可减少到n(log2n)2

选择排序

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

遍历一遍数组不但能选择一个最小的,还可以选择最大的,所以一次选两个数,对选择排序优化。
代码:

void SelectSort(int* a, int n)

	int begin = 0, end = n - 1;
	while (begin < end)
	
		int mini = begin, maxi = begin;
		for (int i = begin; i <= end; ++i)
			// 记录最大值和最小值的小标
			if (a[i] > a[maxi])
				maxi = i;
			if (a[i] < a[mini])
				mini = i;
		
		Swap(&a[begin], &a[mini]);
		// 最大值maxi在begin的位置,经过上一行代码,begin位置的值和mini位置的值换了
		// 所以此时最大值maxi在mini位置处
		if (begin == maxi)	
			maxi = mini;
		Swap(&a[end], &a[maxi]);
		++begin;
		--end;
	

时间复杂度:O(N2)
空间复杂度:O(1)

堆排序

堆向下调整算法

给出一个数组,逻辑上看做一颗完全二叉树。通过从根节点开始的向下调整算法可以把它调整成一个小堆。向下调整算法有一个前提:左右子树必须是一个堆,才能调整。

int array[] = 27,15,19,18,28,34,65,49,25,37;


建小堆向下调整,每次选左右子树小的和父节点交换,更新父节点,再选小的再交换,一直到左右孩子的值都比父节点大了,或者没有没有孩子节点为止。
建大堆就找大的。
向下调整代码(建大堆):

void AdjustDown(int* a, int n, int parent)

	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;
		
	

堆排序(Heap Sort)就是利用堆(假设利用大堆排升序)进行排序的方法。它的基本思想是,将待排序的序列构造成一个大顶堆。此时,整个序列的最大值就是堆顶的根结点。将它移走(其实就是将其与堆数组的末尾元素交换,此时末尾元素就是最大值),然后将剩余的n-1个序列重新构造成一个堆,这样就会得到n个元素中的次小值。如此反复执行,便能得到一个有序序列了。
代码:

void HeapSort(int* a, int n)

	// 建大堆O(N)
	for (int i = (n - 2) / 2; i >= 0; --i)
	
		AdjustDown(a, n, i);
	
	// O(N*logN)
	int end = n - 1;
	while (end > 0)
	
		Swap(&a[0], &a[end]);
		AdjustDown(a, end, 0);
		--end;
	

时间复杂度:O(N*logN)
空间复杂度:O(1)

冒泡排序

冒泡排序(Bubble Sort)一种交换排序,它的基本思想是:两两比较相邻记录的关键字,如果反序则交换,直到没有反序的记录为止。

代码:

void BubbleSort(int* a, int n)

	for (int i = 0; i < n - 1; ++i)
	
		int flag = 0;
		for (int j = 0; j < n - i - 1; ++j)
		
			if (a[j] > a[j + 1])
			
				Swap(&a[j], &a[j + 1]);
				flag = 1;
			
		
		if (flag == 0)
			break;
	

时间复杂度:O(N2)
空间复杂度:O(1)

快速排序

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

hoare版本

选择第一个数为基准值,记录一个左指针和右指针。从右边开始找比基准值小的,左边找比基准值大的,将这两个数交换。当左右指针相遇时,将相遇位置的数和基准值交换。形成了基准值左侧区间都比它小,右侧区间都比它大,左区间和右区间递归执行排序,直到排序完成。
代码:

void QuickSort1(int* a, int begin, int end)

	if (begin >= end)	// 只有一个数或一个没有
		return;
	int left = begin, right = end;
	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]);	// 把left和right相遇位置的值和keyi交换
	QuickSort1(a, begin, left - 1);		// 递归排左区间
	QuickSort1(a, left + 1, end);		// 递归排右区间

时间复杂度:O(N*logN)
最坏的情况当数组有序时,快速排序的时间复杂度达到O(N2)。

三数取中法优化

取left,mid,right这三个数中不是最大的,也不是最小的值。因为在有序的情况下,mid正好是中间那个不大不小的值,刚好能二分。
代码:

int GetMidNum(int* a, int left, int right)

	int mid = left + (right - left) / 2;
	if (a[left] < a[mid])
	
		if (a[mid] < a[right])
			return mid;
		else if (a[left] < a[right])
			return right;
		else
			return left;
	
	else    // a[left] > a[mid]
	
		if (a[mid] > a[right])
			return mid;
		else if (a[left] < a[right])
			return left;
		else
			return right;
	

优化后的快速排序代码:

void QuickSort1(int* a, int begin, int end)

	if (begin >= end)
		return;
	int left = begin, right = end;
	int mid = GetMidNum(a, left, right);
	Swap(&a[left], &a[mid]);
	int key = left;
	while (left < right)
	
		while (left < right && a[right] >= a[key])
			--right;
		while (left < right && a[left] <= a[key])
			++left;
		Swap(&a[left], &a[right]);
	
	Swap(&a[key], &a[left]);
	QuickSort1(a, begin, left - 1);
	QuickSort1(a, right + 1, end);

挖坑法


挖坑法代码:

void QuickSort2(int* a, int begin, int end)

	if (begin >= end)
		return;
	int left = begin, right = end;
	// 三数取中
	int mid = GetMidNum(a, left, right);
	Swap(&a[left], &a[mid]);
	int hole = left;
	int tmp = a[hole];
	while (left < right)
	
		// 右边找小,填到左边的坑里面
		while (left < right && a[right] >= tmp)
			--right;
		// 右边自己形成新的坑
		a[hole] = a[right];
		hole = right;
		// 左边找大,填到右边的坑里面
		while (left < right && a[left] <= tmp)
			++left;
		// 左边形成新的坑
		a[hole] = a[left];
		hole = left;
	
	a[hole] = tmp;	// left和right相遇的位置赋值最初保存的值
	QuickSort2(a, begin, left - 1);		// 相同的方式排左区间
	QuickSort2(a, right + 1, end);		// 右区间

前后指针版本


1.cur往前走,找比key小的数据
2.找到比key小的数据以后,停下来,++prev
3.交换prev和cur指向位置的值
直到cur指向数组的结尾,将key与prev交换

cur还没遇到比key大的值之前,prev紧跟着cur,
cur遇到比key大的值后,prev和cur之间间隔一段比key大的数据。
代码:

void QuickSort3(int* a, int left, int right)

	if (left >= right)
		return;
	int mid = GetMidNum(a, left, right);
	Swap(&a[left], &a[mid]);
	int prev = left;
	int cur = left + 1;
	int key = left;
	while (cur <= right)
	
		if (a[cur] < a[key] && ++prev != cur)
			Swap(&a[prev], &a[cur]);
		++cur;
	
	Swap(&a[key], &a[prev]);
	QuickSort3(a, left, prev - 1);
	QuickSort3(a, prev + 1, right);

快速排序非递归

借助一个栈,依次把需要单趟排的区间入栈,依次取栈里面的区间出来单趟排,再把需要处理的子区间入栈,本质上是在模拟递归的过程。
前后指针法单趟排序:

int PartSort3(int* a, int left, int right)

	int prev = left;
	int cur = left + 1;
	int mid = GetMidNum(a, left, right);
	Swap(&a[left], &a[mid]);
	int key = left;
	while (cur <= right)
	
		if (a[cur] < a[key] && ++prev != cur)
			Swap(&a[prev], &a[cur]);
		++cur;
	
	Swap(&a[key], &a[prev]);
	return prev;

快速排序非递归代码:

void QuickSortNonR(int* a, int left, int right)

	ST st;
	StackInit(&st);
	StackPush(&st, right);	// 右下标入栈
	StackPush(&st, left);	// 左下标入栈
	while (!StackEmpty(&st))
	
		int begin = StackTop(&st);	// 拿到左下标
		StackPop(&st);
		int end = StackTop(&st);	// 拿到右下标
		StackPop(&st);
		int key = PartSort3(a, begin, end);	// 单趟排
		if (key + 1 < end)	// 右区间右左下标入栈
		
			StackPush(&st, end);
			StackPush(&st, key + 1);
		
		if (key - 1 > begin)	// 左区间右左下标入栈
		
			StackPush(&st, key - 1);
			StackPush(&st, begin);
		
	
	StackDestory(&st);

归并排序

归并排序算法的思想是:假设初始序列含有n个记录,则可看成是n个有序的子序列,每个子序列的长度为1,然后两两归并,得到 个长度为2或1的有序子序列;再两两归并,……,如此重复,直至得到一个长度为n的有序序列为止。


代码:

void _MergeSort(int* a, int left, int right, int* tmp)

	if (left >= right)
		return;以上是关于八大排序算法的主要内容,如果未能解决你的问题,请参考以下文章

八大排序算法及其比较

八大排序算法总结

C语言编程八大排序之堆排序

八大排序算法总结

算法基础——经典八大排序算法的Java及Python实现

八大排序算法