【数据结构】常见排序

前言

什么是排序

  • 排序 :所谓排序,就是使一串记录,按照其中的某个或某些关键字的大小,递增或递减的排列起来的操作。
  • 内部排序:数据元素全部存在于内存中的排序
  • 外部排序: 数据元素太多不能同时存放于内存中,根据排序过程的要求不能在内外存之间移动数据的排序

排序的作用

在现实生活中就存在着非常多的事和物以及人上有着明确的排序区分,例如:

  • 学生集合列队的高矮顺序
  • 每个省/市的GDP排名
  • 中国富豪榜排名········等等各类排名顺序

在程序员的眼里,排序的意义就是对于一堆数据的管辖筛选排列,按照需求把它进行排序即可

但是在众多生活软件使用中,每一位程序员写的排序对于用户软件使用体验可是有着极大的便利性,例如

  • 淘宝商品的各项筛选排名
  • 直播平台热度排名
  • 游戏区服玩家排名·······等等各类排名顺序

这些,就是排序在生活中的使用所带来的便利性

1.常见的排序及实现

  • 插入排序

基本思想:

直接插入排序是一种简单的插入排序法,其基本思想是:把待排序的记录按其关键码值的大小逐个插入到一个已经排好序的有序序列中,直到所有的记录插入完为止,得到一个新的有序序列

类似于生活中玩扑克牌斗地主的时候,对于抽的新牌进行插入自己对应思想的其位置排序

  • 直接插入排序

思想:
当插入第 i(i>=1) 个元素时,前面的array[0],array[1],…,array[i-1]已经排好序,此时用 array[i] 的排序码与array[i-1],array[i-2],...的排序码顺序进行比较,找到插入位置即将 array[i] 插入,原来位置上的元素顺序后移

思想动图化

直接插入排序的特性总结:
1. 元素集合越接近有序 ,直接插入排序算法的时间效率越高
2. 时间复杂度:最坏情况时 O(N^2),最好情况时O(N)
3. 空间复杂度: O(1)

代码实现:

插入排序
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 (a[end] > tmp)
			{
				a[end + 1] = a[end];
				end--;
			}
			else
			{
				break;
			}
		}
		a[end + 1] = tmp;
	}
}

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

思想:
希尔排序实际上是直接插入排序的一种优化,在直接插入排序的基础上增添了一个步骤: 预排序
预排序的基本思想是: 选定一个整数gap作为待排序文件数据的区间关系,这个待排序文件数据被分多组,每一组中每一个数据的区间距离一定为被选定的整数,并对每一组中的数据进行排序,直到所有组别都被排序完毕,每一次所有组别排序完毕,gap都会被缩小从而形成新的区间组,一直重复上述分组和排序工作,直至gap=1时,整个数据已经非常接近有序从而降低插入排序的消耗
思想动图化

希尔排序的特性总结:
1. 希尔排序是对直接插入排序的优化。
2. gap > 1 时都是预排序,目的是让数组更接近于有序。当 gap == 1 时,数组已经接近有序的了,这样就会很快。这样整体而言,可以达到优化的效果。
3. 希尔排序的时间复杂度不好计算,因为gap的取值方法很多,一些书籍中给出的希尔排序时间复杂度都不一致,所以按照Knuth书籍中大量测试的结果当N很大时,关键码平均比较次数和对象平均移动次数大约在 n^1.25-1.6n^1.25 范围内,结合实际中gap也是按照Knuth提出的方式来进行取值,大概算出预排序的时间 复杂度约为O(log3N) ,而gap=1时变为直接插入排序,经过预排序的处理已经不存在最坏情况,所以 估算gap为1时直接插入排序的复杂度为O(N) ,预排序和直接插入排序的处理次数相合后得出的结果大概 时间复杂度为O(n^1.3)
4. 希尔排序的 空间复杂度为 O(1)

代码实现:

希尔排序
void ShellSort(int* a, int n)
{
	int gap = n;gap为预排序的组数以及每组中每个数据的间长
	希尔排序写法1:
	for (gap/=3+1; gap > 1; gap/=3+1)
	  {	用于控制gap, gap越大 越不接近有序,gap越小越接近有序,gap如果是1就是直接插入排序
		for (int j = 0; j < gap; j++)
		{	用j来控制每一组的预排序
			for (int i = j; i < n - gap; i += gap)
			{
				int end = i;
				int tmp = a[end + gap];
				while (end >= 0)
				{
					if (a[end] > tmp)
					{
						a[end + gap] = a[end];
						end -= gap;
					}
					else
					{
						break;
					}
				}
				a[end + gap] = tmp;
			}
		}
	}

	希尔排序写法2(简化版):
	while (gap>1)
	{
		gap = gap / 3 + 1;
		for (int i = 0; i < n - gap; i++)
		{ i从0的位置开始,把它限制在小于size-gap的范围内,每次+1都是在对各组的不同数据进行预排
			int end = i;
			int tmp = a[end + gap];
			while (end >= 0)
			{
				if (a[end] > tmp)
				{
					a[end + gap] = a[end];
					end -= gap;
				}
				else
				{
					break;
				}
			}
			a[end + gap] = tmp;
		}
	}
}

  • 选择排序

选择排序基本思想:

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

思想:
  • 在元素集合array[i] 至 array[n-1]中选择关键码最大()的数据元素
  • 若它不是这组元素中的最后一个(或第一个)元素,则将它与这组元素中的最后一个(或第一个)元素交换
  • 在剩余的array[i] 至 array[n-2](array[i+1] 至 array[n-1])集合中,重复上述步骤,直到集合剩余1个元素

 思想动图化

直接选择排序的特性总结:
1. 直接选择排序思考非常好理解,但是效率不是很好。实际中很少使用
2. 时间复杂度:最好和最坏情况都是 O(N^2),因为它不具备判断数据是否已经如条件设置的有序,只会每次不停的去进行选数判定,直至数据组走完
3. 空间复杂度: O(1)

代码实现:

一次选出最大数和最小数的选择排序
void SelectSort(int* a, int n)
{
	int left = 0, right = n - 1;
	while (left < right)
	{
		int maxi = left, mini = left;
		for (int i = left + 1; i <= right; i++)
		{
			if (a[i] < a[mini])
				mini = i;

			if (a[i] > a[maxi])
				maxi = i;
		}

		Swap(&a[left], &a[mini]);
		if (maxi == left)
			maxi = mini;
		Swap(&a[right], &a[maxi]);

		left++;
		right--;
	}
}

  • 堆排序

思想:
堆排序 是指利用堆积树(堆)这种数据结构所设计的一种排序算法
它是选择排序的一种。它是通过堆来进行选择数据
已知堆不一定有序,但是堆每一次插入/删除都能够筛选出最大/最小的数据
根据这个特性进行以下实现
每次将堆顶数据和堆底数据进行交换后对剩余Size-1个堆数据进行重新排序,依次重复以上步骤得出最后有序数据组
需要注意的是排升序要建大堆,排降序建小堆

思想动图化

直接选择排序的特性总结:
1. 堆排序使用堆来选数,效率就高了很多。
2. 时间复杂度: O(N*logN)
3. 空间复杂度: O(1)
代码实现:
向下调整
void ADjustdown(int* ps, size_t parent, size_t size)
{
	assert(ps);
	默认孩子为左子节点
	size_t child = parent * 2 + 1;

	如果子节点下标不大于数组长度下标,则与父节点进行对比交换
	while (child < size)
	{
		1.两个孩子节点选最(小/大)的那个,并且右孩子不能超过数组长度
		if (child + 1 < size && ps[child] < ps[child + 1])
		{
			child++;
		}

		2.如果父亲满足比孩子(小 / 大), 进行交换, 交换后把孩子位置赋予父亲, 并以此为根
		求出新的子节点进行新一轮比较
		if (ps[parent] < ps[child])
		{
			Swap(&ps[parent], &ps[child]);
			parent = child;
			child = parent * 2 + 1;
		}
		else
		{
			break;
		}
	}
}


堆排序
void HeapSort(int* a, int size)
{
	先建堆
	int parent = size - 2 / 2;
	while (parent-- >= 0)
	{
		ADjustdown(a, parent, size);
	}
	//Print(a, size);

	排序
	while (size > 0)
	{
		Swap(&a[0], &a[size - 1]);
		--size;
		ADjustdown(a, 0, size);
	}
}

  • 交换排序

基本思想:
交换,就是根据序列中两个记录值的比较结果来对换这两个记录在序列中的位置
交换排序的特点是:将值较大的记录向序列的尾部移动,值较小的记录向序列的前部移动。
  • 冒泡排序

思想:
array[i]和array[i+1]每次数据进行两两比较,数值较小的一位放在前面,把数值较大的放在后面进行后续对比,直至i=n-1,单趟排序完成,选出此趟中最大的数据放到末尾数,而后依次循环上述步骤,每趟排序走完n都-1,形成i<n,i<n-1,i<n-2.....直至n=1时数据组已经有序.
思想动图化

冒泡排序的特性总结:
1. 冒泡排序是一种非常容易理解的排序
2. 时间复杂度:最坏情况 O(N^2),最好情况O(N)
3. 空间复杂度: O(1)
代码实现:
冒泡排序
void BubbleSort(int* a, int n)
{
	for (int j = 0; j < n; j++)
	{
		int trn = 0;
		for (int i = 0; i < n - 1 - j; i++)
		{
			if (a[i] > a[i + 1])
			{
				Swap(&a[i], &a[i + 1]);
				trn = 1;
			}
		}
		if (!trn)
			break;
	}
}

  • 快速排序

思想:
快速排序是 Hoare 1962 年提出的一种二叉树结构的交换排序方法
其基本思想为: 任取待排序元素序列中 的某元素作为基准值,按照该排序码将待排序集合分割成两子序列,左子序列中所有元素均小于基准值,右 子序列中所有元素均大于基准值,然后最左右子序列重复该过程,直到所有元素都排列在相应位置上为止
概括:
1.每次选出一个中间数keyi,比这个keyi小的数值都将其放在keyi的左区间,比keyi大的都放在keyi的右区间
2.而后分别对keyi的左区间,右区间进行重复的选中间数分区间的分治法排序
思想动图化(hoare版本)

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

代码实现:

包含三个版本的排序,主体代码都是一样,其中的辅助排序函数不一样

主体函数:
主体函数
快排1.0:
void QuickSort1(int* a, int begin, int end)
{
	//4.如果begin大于或者等于end,则代表这段区间只有一个数值,则无需排序
	if (begin >= end)
		return;

	//1.用key来接收每一次的中间值,并利用分治思想把key左边小于key的数值和右边大于key的数值进行排序
	int key = PartSort3(a, begin, end);

	//2.对key左边小于key的数值组进行排序
	QuickSort1(a, begin, key - 1);
	//3.对key右边大于key的数值组进行排序
	QuickSort1(a, key + 1, end); 
}
hoare版本排序函数:
快排的单趟排序方法1:hoare版
hoare版本快排每单次排序都要求key的左边比key小右边比key大(使用的是闭区间)
int PartSort1(int* a, int left, int right)
{
	把key设置为左边,则右先走找小,后左走找大
	如果key设为右边,则左先走找大右后走找小
	int key = left;
	while (left < right)
	{
		情况: 1.right和key的值可能相等 2.right的值小于key
		解:如果大于或者相等,则不进行任何处理,right--, 如果小于则停住,让left走
		while (left < right && a[right] >= a[key])
			right--;

		情况: 1.left和key的值可能相等 2.left的值大于key
		解:如果相等,则不进行任何处理,left++, 如果大于则停住,把left和right进行交换
		while (left < right && a[left] <= a[key])
			left++;

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

	走到这里代表left==right,走到这里相等时,这个数值一定比key小,和key进行交换
	Swap(&a[key], &a[left]);
	此时left和right一定相等,随便返回哪个都可以,都是中间值
	return left;
}
hoare版本优化而来的挖坑法
快排的单趟排序方法2:挖坑法
int PartSort2(int* a, int left, int right)
{
	1.设定key为left,用一个变量存住key的下标
	int keyi = a[left];
	
	while (left < right)
	{
		2.key的数值被存起来之后,left位置的数值就可以被覆盖,称其为坑位,right按照排序特性找小对left进行覆盖
		while (left < right && a[right] >= keyi)
		{
			--right;
		}
		a[left] = a[right];

		3.left的坑位被覆盖后,right随机就变成了另一个坑位,此时left要做的就是找比keyi大的数值并把其覆盖到right的坑位中
		while (left < right && a[left] <= keyi)
		{
			++left;
		}
		a[right] = a[left];
	}

	4.left和right相遇后的位置就是最终坑位,把keyi放入坑位形成 [小于keyi] keyi [大于keyi]
	a[left] = keyi;
	return left;
}
前后指针法排序
快排的单趟排序方法3:前后指针法
int PartSort3(int* a, int left, int right)
{
	int keyi = left;
	int prev = left;
	int cur = prev + 1;
	while (cur <= right)
	{
		如果两者都小于begin位置的数值,则代表两个元素都小于begin,那么就往后继续走去找大
		if (a[cur] < a[keyi] && a[++prev]>a[keyi])
		{
			Swap(&a[prev], &a[cur]);
		}
		走到这里代表prev位置数值小于begin,并且prev后一位的数值大于begin,
		而cur位置数值也大于begin,这时候prev停住不动,cur往后继续走找小
		++cur;
	}
	Swap(&a[prev], &a[keyi]);
	return prev;

}

  • 快排的延伸优化
  • 三数取中

快排分两种情况
1.最好的情况: 每次选的key都是中位数 O(N*logN)
2.最坏的情况:每次选的key都是最大或者最小的数O(N^2)
如本身数据就有序的情况下,且数据体量较大的情况下会爆栈(栈溢出),这种情况下希尔排序的优势就比快排大很多

那么快排就要做出一个动作,针对有序的情况下的解决方式,让快排依然稳坐钓鱼台
解:既然有序的情况下,递归选key 不管选最左边或者最右边都不好,那么就针对选key来进行处理
                                        一个数组: { 1 , 2 , 3 , 4 , 5 , 6 , 7 , 8 , 9 }

解决方法:
1.随机选key(达成一个随机数的条件,但是这种情况下就像掷骰子,把自己的命运交给天,不够稳妥)
2.三数取中
    2.1取第一个数和最后一个数以及中间位置那个数,选出满足三数中非最大同时也非最小的数为keyi
    2.2针对如果数组有序的情况下,直接选择中间数那么肯定就是二分处理
    2.3针对随机数的情况选中间数那么这个keyi一定不是最大也不是最小,这时候yi避免掉最坏的O(N^2)的情况

  • 实现代码
int GetMidIndex(int* a, int left, int right) //选中间数函数
{
	//int mid = (left + right) / 2;
	//防溢出写法
	int mid = left + (right - left) / 2;
	//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;
		}
	}

	//return a[left] < a[mid] ? (a[mid] < a[right] ? mid : (a[left] < a[right] ? left : right))
	//	: (a[mid] > a[right] ? mid : (a[left] < a[right] ? left : right));
}
  • 三数取中的运用
三数取中法,这个方法三种单趟排序都可以使用,这里使用指针法做案例
int PartSort4(int* a, int left, int right)
{
	先把中间数取出,再把这个中间数和要做key的位置数值进行交换,其它逻辑不变,这里采用left做key
	int midi = GetMidIndex(a, left, right);
	Swap(&a[midi], &a[left]);

	int key = left;
	int prev = left, cur = prev + 1;
	while (cur <= right)
	{
		如果cur位置的值小于key位置的值,并且prev下一位的值不等于cur的情况下说明cur和prev中间
		必然有一个区间,此时进行交换即可
		if (a[cur] < a[key] && ++prev != cur)
			Swap(&a[prev], &a[cur]);

		++cur;
	}
	Swap(&a[prev], &a[key]);
	return prev;
}

  • 区间优化概念

其实快速排序,以逻辑性画图递归展开来看,其实就是一颗二叉树,把大问题层层分割成小问题进行处理直到问题彻底被分割完毕
在快排中使用了分割数据进行排序,缩小每次递归时数据体量进行逐一排序,那么假设一组数据使用快排,画图来看这个递归树展开了10层
第10层就是最终的不可分割子问题,第十层展开的栈都有2^9 = 512
第10层中每一个栈的数据体量都是1个,第9层每组栈数据体量都在3个,第8层的数据体量在7个,第七层15个数据体量..*2+1的体量计算
这种处理方法下引出一个问题:那么针对这个情况,对于这些小体量数据组有必要进行递归展开这么多栈空间对其进行处理计算吗?
答:没必要,如果处理的数据体量小,完全可以利用小区间优化对其进行处理以达到省空间以及防止栈溢出的情况出现

小区间优化顾名思义,每次选出一个key要对其左边和右边的数值进行排序
如果排序的数据体量大一些还好,那么如果排序的体量小呢?  
比如key的左右都是5个数值,对它们进行递归 假设每次key是中间值,那么5个要分成2组2个,2组2个再分成4组1个数值的递归再返还
总计7次递归排序调用,那么5个数值对它进行7次排序调用,是否有意义呢?  5个数据或者类似这种小体量的数据是否使用直接插入排序有更好的效率呢?
小区间优化的概念:
区间很小时,不再使用递归划分的思路让他有序,而是直接使用插入排序对小区间进行排序,从而减少递归调用

  • 实现代码
	小区间优化
	if (end - begin + 1 <= 30)
	{
		传指针需要a+begin,因为如果不这样的话每次排序的只是从整个数组的a[0]开始,此时你排的是右区间起始位置就错了
		所以要传当前区间的起始位置过去
		InsertSort(a + begin, end - begin + 1);
	}
	else
	{
		1.用key来接收每一次的中间值,并利用分治思想把key左边小于key的数值和右边大于key的数值进行排序
		int key = PartSort4(a, begin, end);
		
		2.对key左边小于key的数值组进行排序
		QuickSort2(a, begin, key - 1);
		3.对key右边大于key的数值组进行排序
		QuickSort2(a, key + 1, end);
	}
  • 小区间优化运用
void QuickSort2(int* a, int begin, int end)
{
	4.如果begin大于或者等于end,则代表这段区间只有一个数值,则无需排序
	if (begin >= end)
		return;

	小区间优化
	if (end - begin + 1 <= 30)
	{
		传指针需要a+begin,因为如果不这样的话每次排序的只是从整个数组的a[0]开始,此时你排的是右区间起始位置就错了
		所以要传当前区间的起始位置过去
		InsertSort(a + begin, end - begin + 1);
	}
	else
	{
		1.用key来接收每一次的中间值,并利用分治思想把key左边小于key的数值和右边大于key的数值进行排序
		int key = PartSort4(a, begin, end);
		
		2.对key左边小于key的数值组进行排序
		QuickSort2(a, begin, key - 1);
		3.对key右边大于key的数值组进行排序
		QuickSort2(a, key + 1, end);
	}
}

  • 非递归版本快排

以上版本快排虽然效率不错,但是致命缺点依然存在,那就是递归,过量数据可能会导致爆栈(栈溢出)


分析下递归版本 每次递归展开的每个栈帧中存的是什么?  

存的其实是排序过程当中每一步要控制处理的一个数据区间


那么栈帧中最重要的就是存储处理数据所需的区间,那么对症下药,进行模拟非递归时和递归展开存储的核心一样就可以了

非递归快排实现需要用到 数据结构:栈

  • 实现代码
    void QuickSortNon(int* a, int begin, int end)
    {
    	Stack ST;
    	StackInit(&ST);
    	StackPush(&ST, begin);
    	StackPush(&ST, end);
    
    	while (!StackEmpty(&ST))
    	{
    		利用栈后进先出的特性,实现右边先动
    		int right = StackFront(&ST);
    		StackPop(&ST);
    		int left = StackFront(&ST);
    		StackPop(&ST);
    
    		每次选出一个基准数
    		int key = PartSort3(a, left, right);
    
    		判断左右区间是否已经排序完毕
    		if (left < key - 1)
    		{
    			StackPush(&ST, left);
    			StackPush(&ST, key - 1);
    		}
    
    		if (right > key + 1)
    		{
    			StackPush(&ST, key + 1);
    			StackPush(&ST, right);
    		}
    
    	}
    }


  • 归并排序

思想:
归并排序( MERGE-SORT )是建立在归并操作上的一种有效的排序算法 ,该算法是采用分治法(Divide and Conquer)的一个非常典型的应用。将已有序的子序列合并,得到完全有序的序列;即先使每个子序列有序,再使子序列段间有序。若将两个有序表合并成一个有序表,称为二路归并。
1.利用分治思想,将其分解至不可分解的子问题后,每一个栈帧中的数据量为1,只有1个数据的情况下就认为在这个栈帧中它是有序的
2.在有序的情况下,回到上一层中对本层的所有有序数据组进行排序处理,直至回到起点栈帧中将左右区间组进行合并排序,即可完成整体排序
形象化一些如下思想图化
思想图化

归并排序的特性总结:
1. 归并的缺点在于需要 O(N) 的空间复杂度,归并排序的思考更多的是解决在磁盘中的外排序问题。
2. 时间复杂度: O(N*logN)
3. 空间复杂度: O(N)

代码实现:


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

这个思想和之前的链式二叉树分治递归思想概念一样,例如求二叉树的深度
先把每个区间分割,分割到不可分割的子问题后返回上一层时进行排序

例:  一个数组{ 1,6,8,7,5,4,3,9}
	分割1:{1,6,8,7},{5,4,3,9}
	分割2:{1,6},{8,7},{5,4},{3,9}
	分割3:{1},{6},{8},{7},{5},{4},{3},{9}
	到分割3时已经处于不可分割的子问题,并且分割3中每一个栈帧都有序
	此时栈帧进行返回并对两个有序数据组进行有序数组合并
	合并1:{1,6},{8,7},{4,5},{3,9}
	合并2:{1,6,7,8},{3,4,5,9}
	合并3:{ 1,3,4,5,6,7,8,9 };
	结果:{ 1,3,4,5,6,7,8,9 };



void _MergeSort(int* a, int begin, int end, int* tmp)
{
	如果begin==end,则代表到这一层,就只有一个元素,一个元素则有序,返还回去进行排序
	if (begin >= end)
	{
		return;
	}

	规划区间
	int keyi = (begin + end ) / 2;

	_MergeSort(a, begin, keyi,tmp);
	_MergeSort(a, keyi + 1, end, tmp);

	合并排序
	//printf("[%d %d],[%d %d]\n", begin, keyi, keyi + 1, end);
	int begin1 = begin, end1 = keyi;
	int begin2 = keyi + 1, end2 = end;
	int index = begin;
	对比两个区间,进行合并排序放入tmp数组直到有一边区间走干净为止
	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++];

	把临时数组里面排序后的数据按照区间放回
	memcpy(a + begin, tmp + begin, (end - begin + 1) * sizeof(int));
}


归并排序递归版
void MergeSort(int* a, int n)
{
	//设置一个临时数组用于合并
	int* tmp = (int*)malloc(sizeof(int) * n);
	assert(tmp);
	_MergeSort(a, 0,n - 1,tmp);

	free(tmp);
}
  • 非递归版归并排序 
    归并排序非递归版
    
    递归版的归并排序有一个很明显的规律
    那就是每次比较都是两个元素进行比较,只不过对于区间有一个调整
    利用希尔排序的预排序中的gap区间概念即可进行控制每次的区间
    难点:在于控制数据边界,没控制好一下就会越界
    
    
    
    void MergeSortNon(int* a, int n)
    {
    	int* tmp = (int*)malloc(sizeof(int) * n);
    	assert(tmp);
    	int gap = 1;
    	while (gap < n)
    	{
    		for (int i = 0; i < n; i += gap * 2)
    		{
    			设两个要合并排序的区间
    			int begin1 = i, end1 = i + gap - 1;//区间1
    			int begin2 = i + gap, end2 = i + gap * 2 - 1;//区间2
    
    			
    			边界设置要点: 
    			区间1中的begin是肯定不会越界,但是end1和区间2都会有可能越界
    			end1 begin2 end2 这三者几乎是相关联的
    			end1越界了,那么begin2肯定也越界了,如果end1没越界,那么begin2会越界吗?
    			答:不一定,如果end1处于边界,那么begin2肯定会越界
    			begin2越界了,那么end2肯定也越界了,如果begin2没越界那么end2会越界吗?
    			答:不一定,如果begin2处于n/2+1的位置,那么end2一定会越界!
    			对于这种情况就要进行三次判断
    			1.如果end1越界了,那么代表区间1涵盖了数据的所有或者后半部分,
                区间2肯定是不存在的
    			2.如果end1处于边界,则要对begin2进行单独判断,如果begin2越界了,
                那么区间2也是不存在的
    			3.end1和begin都没越界,只有end2越界了
                说明数据体的后半部分区间小于gap设定区间,只需要校准end2到数据体的边界即可
    			
    
    			1.end1是否越界
    			if (end1 >= n)
    			{
    				end1 = n - 1;
    			}
    
    			2.begin2是否越界
    			if (begin2 >= n)
    			{
    				//让它进不去下面的交换循环体即可
    				begin2 = n;
    				end2 = n - 1;
    			}
    
    			3.end2是否越界
    			if (end2 >= n)
    			{
    				end2 = n - 1;
    			}
    
    			int index = i;
    			对比两个区间,进行合并排序放入tmp数组直到有一边区间走干净为止
    			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++];
    			把临时数组里面排序后的数据按照区间放回
    		}
    		memcpy(a, tmp, n * sizeof(int));
    		gap *= 2;
    	}
    }


 非比较排序

  • 计数排序

思想:
计数排序又称为鸽巢原理,是对哈希直接定址法的变形应用
采用绝对映射法进行处理,例如一段数值中最大的数为i,那么就开一段i+1的数组
然后对这段数组进行遍历,遍历一个数据时在额外数组中该数值位置+1,即可进行计数
遍历原数组后,再把额外数组中下标为0开始依次写出,即可得到排序后的有序数组
这种方法称其为: 绝对映射计数排序
但是这样同样有个致命缺点,假设数据最大的数值为10000,那就要开10001个数组,非常的浪费空间
所以对其做出修改,例如这个数据组中最大的为9000,最小的为5000,那则开最大数值-最小数值+1的辅助数组即可,后入数组时,每个数据在入辅助数组时减最小数值后入到数组中,出的时候再加上最小数值即可做到以最小的代价存入这些数据
这种方法称其为: 相对映射计数排序
计数排序的特性总结:
1. 计数排序在数据范围集中时,效率很高,但是适用范围及场景有限。
2. 时间复杂度: O(MAX(N, 范围 ))
3. 空间复杂度: O( 范围 )
代码实现:
计数排序(相对映射法)
void CountSort(int* a, int n)
{
	int max = a[0], min = a[0];
	找出最大和最小的数
	for (int i = 1; i < n; i++)
	{
		if (a[i] > max)
			max = a[i];

		if (a[i] < min)
			min = a[i];
	}

	创建辅助数组并映射
	int range = max - min + 1;
	int* tmp = (int*)calloc(range, sizeof(int));
	assert(tmp);

	for (int i = 0; i < n; i++)
	{
		tmp[a[i] - min]++;
	}

	排序写回
	int j = 0;
	for (int i = 0; i < range; ++i)
	{
		while (tmp[i]--)
		{
			a[j++] = i + min;
		}
	}

}

 2.排序之间的细节对比

  • 直接插入排序和冒泡排序做对比

细化的来说,直接插入排序要优于冒泡排序
因为冒泡排序针对有序不交换的情况下才会停
它的条件相对于来说是非常苛刻的,它是针对整体数据的,整体达到几乎有序或接近有序才可以达到O(N)
而直接插入排序针对的是每一段有序或无序,适应性非常的强

  • 选择排序和冒泡排序做对比

选择排序的优势在于数据量大时,我们实现的选择排序是一次选两个值,所以这里选择排序的数据增量缩小是以N-2->N-4->N-6...这种等差数列方式进行排序,而冒泡排序则是以固定的N-1进行缩小排序
但是冒泡排序在一种情况下会比选择排序要好,那就是数据有序的情况下,因为冒泡排序会进行判断数据是否有序,而选择排序不会,它是逐渐减小增量
得出结论,选择排序在数据量大且无序的情况下优于冒泡排序, 冒泡排序在数据有序前提下优于选择排序 但是在都是N^2的排序中,但是这两种排序的适应力都不如直接选择排序

  • 一般情况下希尔排序的时间复杂度都优于插入排序

只有一种情况下直接插入排序和希尔排序会相差无几,那么就是数据原本接近有序或者有序的情况下,这种情况下希尔排序的预排序几乎是没有什么作用


3.排序的稳定性

假定在待排序的记录序列中,存在多个具有相同的关键字的记录,若经过排序,这些记录的相对次序保持不变,即在原序列中,r[i]=r[j] ,且 r[i] r[j] 之前,而在排序后的序列中, r[i] 仍在 r[j] 之前,则称这种排序算法是稳定的;否则称为不稳定的。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值