八大排序--高质量总结 干净又卫生

八大排序--高质量总结 干净又卫生

  • 0.Intro
  • 1. 插入排序
    • 1.1直接插入排序
      • 1.1.1 实现插入排序
      • 1.1.2 直接插排复杂度
    • 1.2 希尔排序
      • 1.2.1 实现希尔排序
      • 1.2.2 希尔排序时间复杂度
  • 2. 选择排序
    • 2.1 直接选择排序
      • 2.1.1 直接选择排序的想法
      • 2.1.2 实现选择排序(不完全对)
      • 2.1.3 改正选择排序
      • 2.1.4 正确的选择排序
    • 2.2 堆排序
      • 2.2.1 实现堆排序
      • 2.2.2 堆排序的时间复杂度
  • 3. 交换排序
    • 3.1 冒泡排序
      • 3.1.1 基本想法
      • 3.1.2 实现冒泡排序
      • 3.1.3 分析冒泡复杂度
    • 3.2.快速排序
      • 3.2.1 基本想法(Hoare)
      • 3.2.2 实现快速排序
      • 3.2.3 实现多种方法
        • 3.2.3.1 Hoare法(左右指针法)
        • 3.2.3.2 挖坑法
        • 3.2.3.3 前后指针法
      • 3.2.4 时间复杂度测试
      • 4.4.1 理想时间复杂度计算
      • 3.2.5 优化快排
        • 3.2.5.1三数取中法(通过采取改进选key的方法来优化)
          • 3.2.5.1.1 三数取中法+Hoare
          • 3.2.5.1.2 三数取中法+前后指针
        • 3.2.5.2 小区间排序优化
        • 3.2.5.3 非递归快排
  • 4. 归并排序
    • 4.1 基本想法
    • 4.2 实现归并排序
    • 4.3 归并的复杂度
    • 4.4 非递归实现归并排序
    • 4.5 补充
  • 5. 计数排序
    • 5.1 基本想法
    • 5.2 实现计数排序
    • 5.3 时间复杂度分析
  • 6. 分析八大排序
    • 6.1 八大排序分析
    • 6.2 稳定性
    • 6.2 高效性

八大排序--高质量总结 干净又卫生_第1张图片

0.Intro

数据结构和算法是不分家的,接下来的排序算法中,有的算法可能会用到某种数据结构

排序算法是什么呢?百度百科一下

排序就是把集合中的元素按照一定的次序排序在一起。一般来说有升序排列和降序排列2种排序,在算法中有8种基本排序

往往评价一个排序算法的好坏往往可以从几个方面入手:

(1)时间复杂度:即从序列的初始状态到经过排序算法的变换移位等操作变到最终排序好的结果状态的过程所花费的时间度量。

(2)空间复杂度:就是从序列的初始状态经过排序移位变换的过程一直到最终的状态所花费的空间开销。

(3)使用场景:排序算法有很多,不同种类的排序算法适合不同种类的情景,可能有时候需要节省空间对时间要求没那么多,反之,有时候则是希望多考虑一些时间,对空间要求没那么高,总之一般都会必须从某一方面做出抉择。

(4)稳定性:稳定性是不管考虑时间和空间必须要考虑的问题,往往也是非常重要的影响选择的因素。

就像Crash Course Computer Science 13. Intro to Algorithm所说,记载最多的算法之一是“排序”,我们在生活中也常常会排序,比如名字,比如数字Computer sort all the time,比如说找出最便宜的机票,按最新时间排邮件,按姓氏排联系……

八大排序--高质量总结 干净又卫生_第2张图片

本图片来源于Crash Course Computer Science,举了飞机飞到印第安纳波利斯的机票价钱排序

那么接下来开始介绍八大排序,之后的排序算法实例都是以排升序为示例

1. 插入排序

插入排序其实可以联想到当我们在打扑克牌的时候,把同样大的摆在一起,把大的排在小的后面,每次摸一张牌的时候,就插入手中有序的拍的序列之中,找到合适的位置插入进去

1.1直接插入排序

假设插入前的排序已经排好顺序,用要插入的数据进行一一比较,找到相应位置将其插入

八大排序--高质量总结 干净又卫生_第3张图片

1.1.1 实现插入排序

实现排序要想到先拆解问题,然后再画图思考

先对问题的拆解

  1. 单趟怎么走?

  2. 单趟实现了,那多趟怎么走?

  3. 先写单趟排序

    • 先确定要插入的数组的最后一个是下标是end,然后要插入的值是tmp
    • 然后比较数组中下标为end的值和tmp的大小
      • 如果tmp小,那么当前指向的值向后移动一位,然后end往前走一个
      • 如果tmp大,就放入当前end位置的后面一个
      • 如果一样大就放在你这个数的后面
      • 如果end==-1走出数组,那么还是放在end位置的后面一个,也就是数组下标为0的位置
    int tmp;
    int end;
    	while (end >= 0)
    	{
    		if (tmp<end)
    		{
    			a[end + 1] = a[end];
                	--end;
    		}
    		else
    		{
    			break;
    		}
    	}
    	//循环结束或者end==-1
    	a[end + 1] = tmp;
    
  4. 再写多趟,控制end的位置和插入的值

    • end的值等于i
    • tmp相当于end的后一个位置,所以是end+1
    for (int i = 0; i < n - 1; ++i)
    	{
    		//把tmp插入到数组的[0,end]的有序区间
    		int end=i;
    		int tmp = a[end + 1];
    		while (end >= 0)
    		{
    			if (tmp < a[end])
    			{
    				a[end + 1] = a[end];
    				--end;
    			}
    			else
    			{
    				break;
    			}
    		}
    		//循环结束或者end==-1
    		a[end + 1] = tmp;
    	}
    

1.1.2 直接插排复杂度

最好与最坏 复杂度 发生情况
最坏的情况 O(N2 逆序的一个数组
最好的情况 O(N) 顺序有序的数组

分析这样一个表格说明:直接插入排序比较适合接近于顺序有序的数组

1.2 希尔排序

八大排序--高质量总结 干净又卫生_第4张图片
希尔排序也叫缩小增量排序,希尔他分析了一下直接插入排序时间复杂度,他发现了问题,也解决了问题,实现了对直接插入排序的优化

希尔排序分为了两个目的:

  1. 预排序,使得数组接近有序
  2. 直接插入排序

效果:只要最后结果的方法复杂度小于O(N2),就是有效的进步

1.2.1 实现希尔排序

  • 想法:

间隔为gap分为一组数据,对一组插入排序,假如说如上图的gap=5

这样之后相比原来的会更加接近有序,并且大的数会被更快的挪到后面,小的更快挪到后面

  • gap越大,大的和小的数可以更快地挪到对应的方向,但是越不接近有序
  • gap越小,大的和小的就会越慢地挪到对应的方向,但是gap越小就会越接近有序

八大排序--高质量总结 干净又卫生_第5张图片

希尔排序的思想就是一开始gap要相对于整个数据地总量来说是一个大的值,要求快速接近相应位置,然后gap要减小,为了接近有序, 最后gap==1的时候本质就是一个直接插入排序

还是先对复杂的问题进行拆解

  1. 先写一次希尔排序
  2. 再把多趟排序完成
  • 一次希尔排序
int gap;
	int end;
	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;

可以看到其实gap==1的时候就是直接插入排序

  • 完成多次排序,改变gap让其越来越接近顺序

我们按照gap==3来举一个栗子

八大排序--高质量总结 干净又卫生_第6张图片

我们的想法是这样的,先对i所在位置的蓝色区间9排列,排完一次之后立马往后移一个i然后对红区间的1,然后是绿区间的2,然后再是蓝5,然后再是……,最后到size-gap即蓝8排完的时候结束算排序完

这样就可以把gap和end相应的赋值

	int gap=3;
	for (int i = 0; i < sz - 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;
	}

但是gap肯定不能只是3不变,这里我们使用推荐的做法,每次gap都gap=gap/3+1,为的是使得最后一次能够保证最后一次一定是gap==1

//gap>1的时候,预排序
	//gap==1的时候,直接插入排序
	int gap=sz;
	while (gap > 1)
	{
		gap = (gap / 3+1);//保证最后一次是1
		for (int i = 0; i < sz - 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;
		}
	}

当然gap每次/5,/7都是可以的

用一个逆序的栗子可以看希尔排序更直接

	int a[] = { 10,9,8,7,6,5,4,3,2,1,-1,-2,-3,-4,-5,-6,-7,-8,-9,-10,-11 };

八大排序--高质量总结 干净又卫生_第7张图片

1.2.2 希尔排序时间复杂度

对于时间复杂度的问题上面来看,这里我们通过100000个数的伪随机数来试试看排序的速度(单位是毫秒)

srand(time(0));
const int N = 100000;
int* a1 = (int*)malloc(sizeof(int) * N);
int* a2 = (int*)malloc(sizeof(int) * N);
for (int i = 0; i < N; ++i)
{
		a1[i] = rand();
		a2[i] = a1[i];
}
int begin1 = clock();
InsertSort(a1, N);
int end1 = clock();

int begin2 = clock();
ShellSort(a2, N);
int end2 = clock();

printf("InsertSort:%d\n", end1 - begin1);
printf("ShellSort:%d\n", end2 - begin2);
free(a1);
free(a2);

image-20211205134311210

image-20211205135007396

image-20211205135051245

可见差别还是很明显的

希尔排序的复杂度非常难算,平均来说是O(N1.3)

当取gap/3的时候是
O ( l o g 2 N ∗ N ) O(log_2^N*N) O(log2NN)

2. 选择排序

2.1 直接选择排序

八大排序--高质量总结 干净又卫生_第8张图片

2.1.1 直接选择排序的想法

直接选择排序也就是暴力排序一波,排升序的时候找到剩下数组中最小的那一个放在剩下区间的头

当然现在我们可以稍稍优化一下,就是每遍历一遍数组,把最大和最小放在合适的位置,然后再去选择次小的和次大的,依次这样走,直到该区间只剩一个值或没有

优化了一点,但还是
O ( N 2 ) O(N^2) O(N2)

2.1.2 实现选择排序(不完全对)

int left = 0, right = sz - 1;
	while (left<right)
	{
		//找出=最大的和最小的
		int minIndex = left, maxIndex = left;
		for (int i = left; i <= right; ++i)
		{
			if (a[i] < a[minIndex])
			{
				minIndex = i;
			}
			if (a[i] > a[maxIndex])
			{
				maxIndex = i;
			}
		}
		Swap(&a[left], a[minIndex]);
		Swap(&a[right], a[maxIndex]);
		++left;
		--right;
	}

但是我们用逆序数组的栗子发现这样仍然不对,需要找一下问题

int a[] = { 10,9,8,7,6,5,4,3,2,1,-1,-2,-3,-4,-5,-6,-7,-8,-9,-10,-11 };

image-20211205142131357

2.1.3 改正选择排序

其实我们的选择排序缺的是很重要的一步,通过调试我们可以发现,如果当a[left]正好就是数组中max的时候,因为有这样的一步

		Swap(&a[left], a[minIndex]);

所以相当于a[left]的值被交换到了minIndex,那个数组中的最大值已经不是原先记录在maxIndex的下标,所以导致执行下一步的时候

		Swap(&a[right], a[maxIndex]);

a[right]换成了a[minIndex],如果遇到left == maxIndex的情况时,会产生大问题,遇到逆序数组更是白排序了一遍

于是我们需要下面一步排除问题

		if (left == maxIndex)
		{
			maxIndex = minIndex;
		}

2.1.4 正确的选择排序

void Swap(int* p1, int* p2)
{
	int tmp = *p1;
	*p1 = *p2;
	*p2 = tmp;
}
void SelectSort(int* a, int sz)
{
	int left = 0, right = sz - 1;
	while (left<right)
	{
		//找出=最大的和最小的
		int minIndex = left, maxIndex = left;
		for (int i = left; i <= right; ++i)
		{
			if (a[i] < a[minIndex])
			{
				minIndex = i;
			}
			if (a[i] > a[maxIndex])
			{
				maxIndex = i;
			}
		}
		Swap(&a[left], &a[minIndex]);
		if (left == maxIndex)
		{
			maxIndex = minIndex;
		}
		Swap(&a[right], &a[maxIndex]);
		++left;
		--right;
	}
}

image-20211205143418661

2.2 堆排序

还有一种选择排序就是通过堆的特性来选择排序

堆排序就还是通过建堆,然后向上调整和向下调整来实现,具体的实现思路与方法会在数据结构堆中仔细展开,到时候可以参见那篇专门谈堆的博客

八大排序--高质量总结 干净又卫生_第9张图片
注:本图片来源于https://www.codesdope.com/staticroot/images/algorithm

2.2.1 实现堆排序

一定要记住,排升序要建大堆,排降序要建小堆

  • 分解问题
    1. 先建大堆
    2. 向下调整排序
void Swap(int* p1, int* p2)
{
	int tmp = *p1;
	*p1 = *p2;
	*p2 = tmp;
}
void AdjustDown(int* a, int sz,int root)
{
	int parent = root;
	int child = 2 * parent + 1;
	while (child < sz)
	{
		if (child + 1 < sz && 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(int *a,int sz)
{
	for (int i = (sz - 1 - 1) / 2; i >= 0; --i)
	{
		AdjustDown(a, sz, i);//向下调整建一个大堆
	}
	int end = sz - 1;//头放到最后,次大的放到倒数第二个……
	while(end>0)
	{
		Swap(&a[0], &a[end]);
		AdjustDown(a, end, 0);
		--end;
	}
}

2.2.2 堆排序的时间复杂度

O ( N ∗ l o g 2 N ) O(N*log_2 ^N) O(Nlog2N)
这时候我们再通过100000个数的伪随机数来试试看排序的速度(单位是毫秒)

效果很明显

八大排序--高质量总结 干净又卫生_第10张图片

八大排序--高质量总结 干净又卫生_第11张图片

3. 交换排序

3.1 冒泡排序

3.1.1 基本想法

起源于水里的气泡随着上升,越来越大

八大排序--高质量总结 干净又卫生_第12张图片

3.1.2 实现冒泡排序

  • 问题拆解
    1. 单趟
    2. 多趟

单趟

	for (int i = 1; i < sz; ++i)
	{
		if (a[i-1] > a[i])
		{
			Swap(&a[i], &a[i - 1]);
		}
	}

多趟

void BubbleSort(int* a, int sz)
{
	for (int j = 0; j < sz; ++j)
	{
		for (int i = 1; i < sz-j; ++i)
		{
			if (a[i - 1] > a[i])
			{
				Swap(&a[i], &a[i - 1]);
			}
		}
	}
}

优化

创建一个优化的标志,某一次如果没有交换,直接跳出循环,因为已经有序

void BubbleSort(int* a, int sz)
{
	for (int j = 0; j < sz; ++j)
	{
		int exchange = 0;
		for (int i = 1; i < j; ++i)
		{
			if (a[i - 1] > a[i])
			{
				Swap(&a[i], &a[i - 1]);
				exchange = 1;
			}
		}
		if (exchange == 0)
		{
			break;
		}
	}
}

3.1.3 分析冒泡复杂度

最好与最坏 复杂度 发生情况
最坏的情况 O(N2 逆序的一个数组
最好的情况 O(N) 顺序有序的数组

看上去好像和插入排序是一样的,那么到底谁更好呢?

顺序有序的话,一样好

接近有序的话,插入好

3.2.快速排序

八大排序--高质量总结 干净又卫生_第13张图片

注:本图片来源于https://commons.wikimedia.org

快排,如其名说明很快,且了解后发现其变形多

3.2.1 基本想法(Hoare)

Hoare的实现

  • 单趟排序

    选出一个key,一般是最左边的或者是最右边的,key放到正确的位置去,左边比key要小,右边的比key要大

这里看图来举个例子:

八大排序--高质量总结 干净又卫生_第14张图片

假设我们把这个数组里面的最左边的6选作为key让right先走

其中right找小数,left找大数,目的其实是为了让左边的比key小右边的比key大

找完之后互相交换

八大排序--高质量总结 干净又卫生_第15张图片

继续找,继续交换

八大排序--高质量总结 干净又卫生_第16张图片

直到相遇,发现该值比key小,交换一下

八大排序--高质量总结 干净又卫生_第17张图片

这样的话就走完一趟了

  • 这里有人会提出疑问
  1. 我们在之前为什么要求让right先走?
  2. 那如果我发现相遇的时候的值比key大怎么办?

其实我们让right先走这样设置的目的就是为了防止相遇的时候的值比key大的情况产生,就能保证始终比key小了

八大排序--高质量总结 干净又卫生_第18张图片

当走到最后一步时,如果先走的是left而不是right的话,left在找大的时候直接略过了3,使得相遇的时候的数字变成9,那最后一步要让比key小的数字和key互换就实现不了,所以最左边数字为key的时候,要让右边的right先走 。也正是右边先走才能使得两种相遇形式,即左遇右(右边先找到小数就停,左边没找到继续走使得相遇)和右遇左(右边找不到小数直接走使得和左所在位置相遇),这两种情况都会使right找到小的(仔细画图分析)

接下来,我们来完成多趟

在确定了之前的key之后我们把中间的现在放着key的左右两边拆开,就像分成左子树和右子树一样

八大排序--高质量总结 干净又卫生_第19张图片

对左一段和右一段分别重复上述操作,左边和右边就成了这样

八大排序--高质量总结 干净又卫生_第20张图片

继续如上操作

发现已经是有序的了

八大排序--高质量总结 干净又卫生_第21张图片

当最后只剩下一个的时候就是有序的

3.2.2 实现快速排序

对于单次排序,我们需要排除一种可能,也就是顺序数组可能会使得right一直找不到,使得right最后越界也没有完成任务

开始进入的时候while (left,并不能排除right越界,要在找大和找小的时候再判断一次

while (left<right&&a[right] > a[keyi])
while (left<right&&a[left]<a[keyi])

八大排序--高质量总结 干净又卫生_第22张图片

还要注意在找大和找小的时候=不能忘记,如果忘记就会使得当left和right遇到同样大的时候不++和–,最后直接交换陷入死循环,所以找大别带上等于,找小也别带上等于

	while (left<right&&a[right] >= a[keyi])
	while (left < right&&a[left]<=a[keyi])

八大排序--高质量总结 干净又卫生_第23张图片

所以细节很多,最后正确的单趟应该是这样

void QuickSort(int* a, int sz)
{
	int left = 0, right = sz - 1;
	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]);//最后交换一下key的值和相遇的值
}

接下来完成多趟,完整实现

首先为了要递归,那么函数的参数肯定还用sz是不合适的,那么改成start和end,同时增加一个递归终止条件

那么这样就是交换版的快速排序

void QuickSort(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]);
	}
	int meeti = left;
	Swap(&a[keyi], &a[left]);//最后交换一下key的值和相遇的值
	//左[begin,meeti-1] 右[meeti+1,end]
	QuickSort(a, begin, meeti - 1);
	QuickSort(a, meeti+1, end);
}

那么这个思想就是类似于二叉树的–“分治”排序

3.2.3 实现多种方法

这里我们把单趟排序抽离出来,将单趟返回参数来递归,那么

PartSort(a, begin, end)其实是由多种写法的,接下来分析多种写法

首先这个本体是不变的

void QuickSort(int* a, int begin,int end)
{
	if (begin >= end)
	{
		return;
	}
	int keyi = PartSort(a, begin, end);//传key值
	//左[begin,meeti-1] 右[meeti+1,end]
	QuickSort(a, begin, keyi - 1);
	QuickSort(a, keyi+1, end);
}

3.2.3.1 Hoare法(左右指针法)

这里我们称之前所讲的方法为Hoare法,或许命名可能是最早发现这个方法的人

int PartSort1(int* a, int left, int right)
{
	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]);//最后交换一下key的值和相遇的值
	return left;
}

试试看其他几种方法实现单趟排序

3.2.3.2 挖坑法

  1. 还是取出left的值用key保存,比如说下图的6

  2. 这里不同的点在于当前left所指向的变成了一个坑

八大排序--高质量总结 干净又卫生_第24张图片

  1. 同样的还是右边的right开始走,找小,找到小数之后这次不一样了,不是交换,而是把right的值放到坑里面,于是形成了新的坑

八大排序--高质量总结 干净又卫生_第25张图片

  1. 再然后left找大,填到坑里面,再产生新坑

八大排序--高质量总结 干净又卫生_第26张图片

  1. 最后会相遇,然后放进去key就好了

八大排序--高质量总结 干净又卫生_第27张图片

八大排序--高质量总结 干净又卫生_第28张图片

int PartSort2(int* a, int left, int right)
{
	int key = a[left];
	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;
	return left; 
}

那么这就是挖坑法

3.2.3.3 前后指针法

  1. 先是有两个指针,分别是cur和prev,开始的时候分别在一前一后

    八大排序--高质量总结 干净又卫生_第29张图片

  2. 然后cur去找比key小的值,找到以后++prev,再交换prev和cur位置的值

  • 前两次的交换之后没什么变化,因为cur==prev,所以等会写代码的时候可以排除这个可能性

    八大排序--高质量总结 干净又卫生_第30张图片

  • 第三次之后看到了变化,其实观察这个操作的意义是在把大的往后放,小的往前放
    八大排序--高质量总结 干净又卫生_第31张图片

  1. 直到cur走出数组尾之后停止

八大排序--高质量总结 干净又卫生_第32张图片

  1. 最后把key位置和prev位置互相交换

    八大排序--高质量总结 干净又卫生_第33张图片

这样的单趟的目的是什么呢,就是使得prev左边的数都比key小,右边的数都比key大

int PartSort3(int* a, int left, int right)
{
	int prev = left;
	int cur = left+1;
	int keyi = left;
	while (cur <= right)
	{
		if (a[cur] < a[keyi]&&++prev!=cur)
		{
			Swap(&a[left], &a[right]);
		}
		++cur;//不进或者交换之后都要往后走
	}
	Swap(&a[keyi], &a[prev]);
	return prev;//返回分隔的值 
}

3.2.4 时间复杂度测试

接下来用同样的方法,随机生成数组感受一下快排的速度

debug版在优化递归的时候不太好,性能不会太明显,建议用release来测性能

debug版 100000

八大排序--高质量总结 干净又卫生_第34张图片

release版 1000000

image-20211206225131593

4.4.1 理想时间复杂度计算

最好的情况是:

理想的快排应该是每次的key恰巧是中位数,然后相当于二分法一样的走下去

八大排序--高质量总结 干净又卫生_第35张图片

时间复杂度是
O ( l o g 2 N ∗ N ) O(log_2^N*N) O(log2NN)

最坏的情况是

有序的数组,则此时每个排序都取最左边为key

八大排序--高质量总结 干净又卫生_第36张图片

这样的时间复杂度是
O ( N 2 ) O(N^2) O(N2)
因此这样的话来看,我们之前写的快排太不稳定,这样不是一个合格排序,我们要对这个快排优化,让它对所有的情况都好用

于是我们可以采取

  1. 三数取中法(通过采取改进选key的方法来优化)
  2. 小区间排序优化

3.2.5 优化快排

3.2.5.1三数取中法(通过采取改进选key的方法来优化)

既然问题出在选key,那么我们使得选恰好中间的数能够最优时间复杂度,于是我们可以每次找出头尾和中间那个三个数字中的中间数,并返回给key,使key的选取变得优化

int GetMidIndex(int* a, int left, int right)
{//找出头尾和中间那个三个数字中的中间数
	int mid = (left + right) >> 1;
	if (a[left] < a[mid]){
		if(a[mid]<a[right]){
			return mid;
		}else if(a[left]>a[right]){
			return left;
		}else{
			return right;
		}
	} else{
		if (a[mid] > a[right]){
			return mid;
		}else if (a[left] < a[right]){
			return left;
		}else {
			return right;
		}
	}
}
void QuickSort(int* a, int begin,int end)
{
	if (begin >= end)
	{
		return;
	}
	int keyi = PartSort1(a, begin, end);
	//左[begin,meeti-1] 右[meeti+1,end]
	QuickSort(a, begin, keyi - 1);
	QuickSort(a, keyi+1, end);
}
3.2.5.1.1 三数取中法+Hoare
int PartSort1(int* a, int left, int right)
{
	int midIndex = GetMidIndex(a, left, right);
	Swap(&a[left], &a[midIndex]);//到这步left里的值将会使头中尾的中间值
	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]);//最后交换一下key的值和相遇的值
	return left;
}
3.2.5.1.2 三数取中法+前后指针
int PartSort3(int* a, int left, int right)
{
	int midIndex = GetMidIndex(a, left, right);
	Swap(&a[left], &a[midIndex]);//到这步left里的值将会使头中尾的中间值
	int prev = left;
	int cur = left+1;
	int keyi = left;
	while (cur <= right)
	{
		if (a[cur] < a[keyi]&&++prev!=cur)
		{
			Swap(&a[left], &a[right]);
		}
		++cur;//不进或者交换之后都要往后走
	}
	Swap(&a[keyi], &a[prev]);
	return prev;//返回分隔的值 
}

下图是对100000个数量的有序数组排序的比较,可以看出三数取中法有 效解决了左右指针的缺陷, 甚至当遇到顺序数组的时候效率反而最高

八大排序--高质量总结 干净又卫生_第37张图片

3.2.5.2 小区间排序优化

也就是递归型的快排,是从头至尾一直是递归的,不断左右分段,像二叉树,不过即使到了最后几个数没有顺序,都要一直递归,于是想法是最后分割到数据到10个的时候用插排或选择解决,毕竟再用希尔和堆排序还要建堆和产生gap,那么又因为之前测过的插排优于选排,所以最后一个小区间里面就使用插排

那么我们改变这个本体

void QuickSort(int* a, int begin,int end)
{
	if (begin >= end)
	{
		return;
	}
	int keyi = PartSort(a, begin, end);//传key值
	//左[begin,meeti-1] 右[meeti+1,end]
	QuickSort(a, begin, keyi - 1);
	QuickSort(a, keyi+1, end);
}
void QuickSort(int* a, int begin, int end)
{
	if (begin >= end)
	{
		return;
	}
	//1.如果这个区间是数据多,继续选key单趟,分割子区间
	//2.如果子区间数据量太小,再去递归不合适,不划算
	if (end - begin > 100)
	{
		int keyi = PartSort3(a, begin, end);
		//左[begin,meeti-1] 右[meeti+1,end]
		QuickSort(a, begin, keyi - 1);
		QuickSort(a, keyi + 1, end);
	}
	else
	{
		InsertSort(a + begin, end - begin + 1);
	}
}

当然如果用编译器其实优化效果不是很明显,因为 编译器已经就递归做到了一些优化,但不像三数取中,优化来的好

注意:优化可以通过改变end-begin的值,具体多少要还是看数据的数量

3.2.5.3 非递归快排

递归,现代编译器优化的比较好,性能问题不是很大,最大的问题在于,递归的深度太深,程序本身没问题,但是栈空间不够,导致栈溢出,只能改非递归stack overflow警告

改成非递归有两种方式:

  1. 直接改循环,比如斐波那契数列
  2. 树遍历非递归和快排非递归,只能用栈模拟递归过程

接下来我们用栈来模拟递归过程

其实分治递归的思想和从栈中放入数据再取出来一样,不是递归,但是和递归很像

我们把单趟排序的区间给入栈,然后依次取栈里面的区间出来单趟排序,再循环需要处理的子区间入栈,直到栈为空。

void QuickSortNonR(int* a, int begin, int end)
{
	Stack st;
	StackInit(&st);
	StackPush(&st, begin);
	StackPush(&st, end);

	while (!StackEmpty(&st))
	{
		int left, right;
		right = StackTop(&st);
		StackPop(&st);

		left = StackTop(&st);
		StackPop(&st);

		int keyi = PartSort1(a, left, right);
		if (left < keyi - 1)
		{
			StackPush(&st, left);
			StackPush(&st, keyi - 1);
		}

		if (keyi + 1 < right)
		{
			StackPush(&st, keyi + 1);
			StackPush(&st, right);
		}
	}
	StackDestroy(&st);
}

注:非递归方法中栈的实现就放在GitHub中,码上来显得文章冗长,所以不写上来了,那么后期也会就栈这个数据结构仔细展开

4. 归并排序

八大排序--高质量总结 干净又卫生_第38张图片

4.1 基本想法

先把这个区间分成两半,然后均分成两个数组进行排序,最后得出两个有序的子区间之后再归并在一起就成为了有序数组

八大排序--高质量总结 干净又卫生_第39张图片

怎么归并?

设置左右指针两个互相比一比,取小的尾插到下面的数据,然后往后走,直到一个区间结束,再把另外一个区间尾插到结束

八大排序--高质量总结 干净又卫生_第40张图片

但是要归并的前提是有序,如果我还是没有序怎么办?就继续拆拆拆

八大排序--高质量总结 干净又卫生_第41张图片

拆到一个的时候就是有序的,然后归并就可以了

那么也就是说这样一个区间经过这样的过程就是有序的了

八大排序--高质量总结 干净又卫生_第42张图片

4.2 实现归并排序

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 sz)
{
	int* tmp = (int*)malloc(sizeof(int) * sz);
	if (tmp == NULL)
	{
		printf("malloc fail\n");
		exit(-1);
	}
	_MergeSort(a, 0, sz - 1,tmp);
	free(tmp);
        tmp = NULL;
}

4.3 归并的复杂度

排序算法 时间复杂度 空间复杂度
归并排序 O(N*logN) O(N)

release版海量数据排序,所需时间如下

注:这里的快排是三数取中+Hoare

100000数据

八大排序--高质量总结 干净又卫生_第43张图片

八大排序--高质量总结 干净又卫生_第44张图片

1000000数据

八大排序--高质量总结 干净又卫生_第45张图片

4.4 非递归实现归并排序

类似的,我们知道递归会带给快排的问题,也会给归并排序带来,所以我们尝试用非递归方式实现归并排序

  • 想法

要一半一半分开这个区间,放入tmp中,然后再取回来放到a中,使开始的gap=1,然后每次gap翻一倍,每次以gap为大小的区间是有序的,直到最后总长一半

  • 注意事项

image-20211208214444692

  1. 随着两两划分是有可能到最后一个只有一个数,[i+gap,i+2*gap-1],也就是说第二个区间可能是不存在的

    那修改一下,不存在就不要归了

    八大排序--高质量总结 干净又卫生_第46张图片

  2. 还有一种可能性是第二组区间有但是不完整

    八大排序--高质量总结 干净又卫生_第47张图片

  3. 还有一种可能性是第二组区间没有,第一组区间也不完整

把1和3归为一类去处理,直接不去归并了

要检查的是第二种可能,要修正一下

void MergeSortNonR(int* a, int sz)
{
	int* tmp = (int*)malloc(sizeof(int) * sz);
	if (tmp == NULL){
		printf("malloc fail\n");
		exit(-1);
	}
	int left;
	int right;
	int gap = 1;
	while (gap < sz){
		for (int i = 0; i < sz; i += (2 * gap)){
			int begin1 = i, end1 = i + gap - 1;
			int begin2 = i + gap, end2 = i + 2 * gap - 1;
			//第二个区间不存在就不需要归并了,结束循环
			if (begin2>=sz){
				break;
			}
			//第二个小区间存在,但是第二个小区间不够gap个,结束位置越界,需要修正
			if (end2 >= sz){
				end2 = sz - 1;
			}
			//[i,i+gap-1] [i+gap,i+2*gap-1]
			left = begin1;
			right = end2;
			int index = begin1;
			while (begin1 <= end1 && begin2 <= end2) {
				if (a[begin1] < a[begin2]) {
					tmp[index++] = a[begin1++];
				}
				else {
					tmp[index++] = a[begin2++];
				}
			}
			//走到这里一定有一边没有走完
			while (begin1 <= end1) {
				tmp[index++] = a[begin1++];
			}
			while (begin2 <= end2) {
				tmp[index++] = a[begin2++];
			}
			//归并完之后,拷回原数组
			for (int j = left; j <=right; ++j)
			{
				a[j] = tmp[j];
			}
		}
		gap *= 2;
	}
	free(tmp);
	tmp = NULL;
}

4.5 补充

我们这里要提到外排序和内排序,如何对大数据文件进行排序

image-20211208225649476

归并排序天然支持外排序

  • 举个栗子:

假如我们有10亿个整数在文件中,需要排序,计算后大小约等于4个G,不能直接放在内存中运算假如我们的内存是0.5G

  1. 我们可以对文件每次读1/8,也就是512M到内存中去排序(这里的排序不能用归并,因为归并空间复杂度O(N),可以用快排),然后写到文件里,再继续重复这个过程,最后将这几个有序小文件分别读数进行归并,两两归就成为4个,再两两归,两两归,最后合成一个
    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Fa7WQ30Y-1639028292067)(…/…/…/…/AppData/Roaming/Typora/typora-user-images/image-20211209085241562.png)]
  1. 第二种可能性是第一个文件和第二个文件归并,然后第二个和第三个文件归并大文件,最后使得

八大排序--高质量总结 干净又卫生_第48张图片

5. 计数排序

计数排序有成为鸽巢原理,是对哈希直接定址法的变形应用

八大排序--高质量总结 干净又卫生_第49张图片

之前的排序都是比较大小来进行排序的,计数排序采取一种新思路,他的思想是

  1. 统计相同元素出现个数
  2. 根据统计结果将序列回收到原来的序列中

5.1 基本想法

如何统计每个数的次数
这里我们有一个A[i]数组,A[i]的值就是对Count数组该位置++

for(int i-0;i<sz;++i)
{
	++Count[A[i]];
}

八大排序--高质量总结 干净又卫生_第50张图片
然后根据count数组按照count数组的数据的下标和值是多少,产生相应值数量的下标区间
这个叫做绝对映射
注:
八大排序--高质量总结 干净又卫生_第51张图片
加入我给了如上几个数字,有一半多都没有映射,那我不可能去浪费0-9存空
所以我为什么不把下标为0的保存10然后下标为5的保存15,跳过1-9呢?
这就叫相对映射

那么因为绝对映射还是存在浪费的,所以在这里的计数排序我们采用相对映射

5.2 实现计数排序

void CountSort(int* a, int sz)
{
	int max = a[0], min = a[0];
	for (int i = 0; i < sz; ++i)
	{
		if (a[i] > max)
		{
			max = a[i];
		}
		if (a[i] < min)
			min = a[i];
	}
	int range = max - min + 1;
	int* count = (int *)malloc(sizeof(int) * range);
	memset(count, 0, sizeof(int) * range);
	for (int i = 0; i < sz; i++)
	{
		count[a[i] - min]++;
	}
	int i = 0;
	for (int j = 0; j < range; ++j)
	{
		while (count[j]--)
		{
			a[i++] = j+min;
		}
	}
}

5.3 时间复杂度分析

计数排序的复杂度

排序名 时间复杂度 空间复杂度
计数排序 O(N+range) O(range)

所以说其实是和range有关的,最好是这组数据中,数据的范围都比较集中,很适合用计数排序,这个排序还是很优秀的,也有局限性,该排序只适合整数,浮点数字符串都不行,数据不集中也不行

6. 分析八大排序

6.1 八大排序分析

八大排序--高质量总结 干净又卫生_第52张图片

该表格不建议去背,而是应该去理解

6.2 稳定性

  • 什么是稳定性?

数组中相同的值排完序之后,其相对位置不变,就是稳定,否则就是不稳定
eg:下面的数组中如果排完序之后橙5被换到蓝5后面去了,就是不稳定的,反之是稳定的
image-20211209124724412

  • 那稳定的价值是什么呢?

比如说考试系统,考完之后要排名,考试的时候提交试卷以后自动判卷拿到成绩,成绩按照交卷顺序排到数组中,我们对数组排序,进行排名

要求:分数相同,先交卷的排在前面

如果稳定性差的排序肯定是可能会颠倒顺序,这不满足我们的要求

注:

  1. 直接选择排序是不保证稳定的(保证选大的时候只选后一个大的,选小的可以选前面的那一个)

    如果我们按照图中的要求去排序

八大排序--高质量总结 干净又卫生_第53张图片

  1. 希尔排序是不稳定的,因为有可能相同大小的值被放预排到了不同组里面去,很有可能变
  2. 堆排序不稳定,当建完堆之后,把root和最后一个叶子一换,就把顺序变了
  3. 快排不稳定,由于找大找小的过程中可能存在交换,一换顺序有可能发生变化

6.2 高效性

就高效性来看的话,快排,堆排和归并都是
O ( N ∗ l o g 2 N ) O(N*log_2^N) O(Nlog2N)
综合来说最好的还是快排,堆排效率总体来说还是略慢快排,然后归并需要更多空间复杂度,另外希尔排序gap取值不同,效果不同

然后就是插入->冒泡->选择,选择很可惜是最次的

对于八大排序算法的源代码如有需要,可以到我的GitHub查看八大排序算法,如果大家觉得有收获的话,请给我点赞,关注和收藏吧,谢谢支持
注:部分图片来源于
Crash Course Computer Science
https://www.codesdope.com/staticroot/images/algorithm
https://commons.wikimedia.org
图片仅供参考学习,不涉及商业用途,如有侵权,请联系删除如有侵权请联系我删除

你可能感兴趣的:(数据结构初阶,排序算法,算法,数据结构,堆排序,快速排序)