Bootstrap

八大排序算法(C语言实现)

目录

直接插入排序

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

选择排序

堆排序

冒泡排序

快速排序

递归实现

Hoare版本

前后指针版本

快速排序优化

三数取中选key

随机数选key

小区间优化

非递归实现

归并排序

递归实现

非递归实现

计数排序

排序算法复杂度以及稳定性分析


首先我会介绍7种常见的排序以及一种小众的/局限的计数排序

本文排序算法全部采用升序!

排序的概念:

  1. 排序:所谓排序,就是使一串记录,按照其中的某个或某些关键字的大小,递增或递减的排列起来的操作
  2. 稳定性:假定在待排序的记录序列中,存在多个具有相同的关键字的记录,若经过排序,这些记录的相对次序保持不变,即在原序列中,r[i]=r[j],且r[i]在r[j]之前,而在排序后的序列中,r[i]仍在r[j]之前,则称这种排序算法是稳定的;否则称为不稳定的。(这个概念先了解一下文章最后章节会对其进行解释说明)

直接插入排序

直接插入排序是一种简单的插入排序法,其基本思想是:

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

实际中我们玩扑克牌时,就用了插入排序的思想。

动图演示:

基本思想:
 在待排序的元素中,假设前n-1个元素已有序,现将第n个元素插入到前面已经排好的序列中,使得前n个元素有序。按照此法对所有元素进行插入,直到整个序列有序。

但我们并不能确定待排元素中究竟哪一部分是有序的,所以我们一开始只能认为第一个元素是有序的,依次将其后面的元素插入到这个有序序列中来,直到整个序列有序为止。

代码实现

//插入排序
void InsertSort(int* a, int n)
{
	for (int i = 0; i < n - 1; i++)
	{
		//end记录的是有序序列最后一个元素的下标
		int end = i;
		//tmp记入的是代插入的元素
		int tmp = a[end + 1];
		//如果后面的元素比end位置的元素还要小,那end位置的元素就要一次向后移动
		//并且如果tmp比下标为0位置的元素还要小,那end为0的元素就要覆盖end为1位置上的元素
		//所以循环继续的条件就是 end>= 0
		while (end >= 0)
		{
			if (a[end] > tmp)
			{
				a[end + 1] = a[end];
				end--;
			}
			else
			{
				//如果比较过程中发现这一轮数据已经是有序的,就可以提前退出这一轮比较
				break;
			}
		}
		//代码执行到此位置有两种情况:
		//1.待插入元素找到应插入位置(break跳出循环到此)。
		//2.待插入元素比当前有序序列中的所有元素都小(while循环结束后到此)。
		//end为什么需要+1,我说第二种情况,第一种情况大差不差
		//如果tmp比end位置的元素还大,那就比end位置前面的元素都要大
		//所以tmp就已经在正确的位置上了,而tmp就是在end后一个位置上,所以这里要+1
		a[end + 1] = tmp;
	}
}

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

1. 元素集合越接近有序,直接插入排序算法的时间效率越高

2. 时间复杂度:O(N^2)

3. 空间复杂度:O(1),它是一种稳定的排序算法

4. 稳定性:稳定

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

希尔排序(Shell Sort)又称缩小增量排序,是插入排序的一种更高效的改进版本,其基本思想是通过将原始数据分成多个子序列来改善插入排序的性能,先让序列中的元素在局部范围内有序,随着增量逐渐减小,子序列的元素越来越多,整个序列逐渐接近有序,最终当增量为 1 时,完成整个序列的排序。

预排序:分组插入排序

目标:大的更快换到后面的位置,小的数更快换到前面的位置

希尔排序的特性总结:

1. 希尔排序是对直接插入排序的优化。

2. 当gap > 1时都是预排序,目的是让数组更接近于有序。当gap == 1时,数组已经接近有序的了,这样就会很快。这样整体而言,可以达到优化的效果。我们实现后可以进行性能测试的对比。

3. 希尔排序的时间复杂度不好计算,因为gap的取值方法很多,导致很难去计算,因此在好些书中给出的希尔排序的时间复杂度都不固定:

《数据结构(C语言版)》--- 严蔚敏

《数据结构-用面相对象方法与C++描述》--- 殷人昆

因为咋们的gap是按照Knuth提出的方式取值的,而且Knuth进行了大量的试验统计,我们暂时就按照:O(N^1.3)

代码1实现:

// 预排序  -- 目标:接近有序  gap > 1
// 插入排序  -- 目标:有序    gap == 1
//希尔排序
void ShellSort(int* a, int n)
{
	int gap = 3;
	while (gap)
	{
		int end = 0;
		int tmp = a[end + gap];
		//j = 0控制的是红色一组
		//j = 1控制的是紫色一组
		//j = 2控制的是蓝色一组
		for (int j = 0; j < gap; j++)
		{
			for (int i = j; i < n - gap; i += gap)
			{
				end = i;
				tmp = a[end + gap];
				while (end >= 0)
				{
					if (a[end] > tmp)
					{
						a[end + gap] = a[end];
						end -= gap;
					}
					else
					{
						break;
					}
				}
				a[end + gap] = tmp;
			}
		}
		gap--;
	}
}

那其实上面的2层for循环也有人优化成一个for循环,效果一样,时间复杂度不变

// 预排序  -- 目标:接近有序  gap > 1
// 插入排序  -- 目标:有序    gap == 1
//希尔排序
void ShellSort(int* a, int n)
{
	int gap = 3;
	while (gap)
	{
		int end = 0;
		int tmp = a[end + gap];
		//i = 0走红
		//i = 1走紫
		//i = 2走蓝
        //i+1走的是颜色对应组的一小步
		for (int i = 0; i < n - gap; i++)
		{
			end = i;
			tmp = a[end + gap];
			while (end >= 0)
			{
				if (a[end] > tmp)
				{
					a[end + gap] = a[end];
					end -= gap;
				}
				else
				{
					break;
				}
			}
			a[end + gap] = tmp;
		}
		gap--;
	}
}

上面a数组有9个元素,所以gap给3没有问题,那如果a数组有100万个数据,gap给3合适吗?

我认为是不合适的,因为当数据量很大的时候,gap给很小,那数据就跳转的很慢(虽然可以这样写,但是不建议)。

所以gap给多少其实与n有关,n也就是数组的大小。

  1. gap越大,数据跳得越快,大的更快到后面的位置,小的更快到前面,但是越不接近有序。
  2. gap越小,数据跳得越慢,但是越接近有序,gap == 1就是插入排序 -->得有序

最推荐写的代码!

// 预排序  -- 目标:接近有序  gap > 1
// 插入排序  -- 目标:有序    gap == 1
//希尔排序
void ShellSort(int* a, int n)
{
	int gap = n;
	while (gap)
	{
		gap /= 2;
		//gap = gap / 3 + 1;
		int end = 0;
		int tmp = a[end + gap];
		//i++是对代码进行优化,
		for (int i = 0; i < n - gap; i++)
		{
			end = i;
			tmp = a[end + gap];
			while (end >= 0)
			{
				if (a[end] > tmp)
				{
					a[end + gap] = a[end];
					end -= gap;
				}
				else
				{
					break;
				}
			}
			a[end + gap] = tmp;
		}
	}
}

注意:

gap /= 2或者gap = gap / 3 + 1这2种写法都可以!

选择排序

选择排序我们可以从每遍历一遍数组开始选出每一轮中的最大值和最小值,我们把最小值往前挪,最大值往后挪动。

//选择排序
void SelectSort(int* a, int n)
{
	//begin是第一个元素的下标
	int begin = 0;
	//end是最后一个元素的下标
	int end = n - 1;

	while (begin < end)
	{

		int mini = begin;
		int maxi = begin;
 		for (int i = begin + 1; i <= end; i++)
		{
			if (a[i] < a[mini])
			{
				mini = i;
			}
			if (a[i] > a[maxi])
			{
				maxi = i;
			}
		}
		//将最小值和begin位置的值进行交换,把最小值往前挪
		Swap(&a[begin], &a[mini]);
		if (begin == maxi)
			maxi = end;
		//将最大值和end位置的值进行交换,把最大值往后挪
		Swap(&a[end], &a[maxi]);
		begin++;
		end--;
	}
}

这里重点说下Swap交换语句下为什么需要加if语句进行判断

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

堆排序

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

对于堆排序我上一个博客我是有写的,并且还讲了关于树的一些概念,大家感兴趣的可以点击链接自行跳转过去 -->堆排序的实现

冒泡排序

冒泡排序,该排序的命名非常形象,即一个个将气泡冒出。冒泡排序一趟冒出一个最大(或最小)值。

代码:

//冒泡
void BubbleSort(int* a, int n)
{
	for (int i = 0; i < n - 1; i++)
	{
		int flag = 1;
		for (int j = 0; j < n - i - 1; j++)
		{
			if (a[j] > a[j + 1])
			{
				Swap(&a[j], &a[j + 1]);
				flag = 0;
			}
		}
		if (flag)
			break;
	}
}

冒泡排序的特性总结:

  1. 冒泡排序是一种非常容易理解的排序
  2. 时间复杂度:O(N^2)
  3. 空间复杂度:O(1)
  4. 稳定性:稳定

快速排序

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

对于如何按照基准值将待排序列分为两子序列,常见的方式有:
 1、Hoare版本
 2、挖坑法
 3、前后指针法

但是我这边只会介绍Hoare版本和前后指针法。

递归实现

Hoare版本

单趟的动图演示:

Hoare版本的单趟排序的基本步骤如下:

  1. 选出一个key,一般是最左边或是最右边的(本文选左边)

  2. 定义一个L和一个R,L从左向右走,R从右向左走。(需要注意的是:若选择最左边的数据作为key,则需要R先走;若选择最右边的数据作为key,则需要L先走)

  3. 在走的过程中,若R遇到小于key的数,则停下,L开始走,直到L遇到一个大于key的数时,将L和R的内容交换,R再次开始走,如此进行下去,直到L和R最终相遇,此时将相遇点的内容与key交换即可。(选取最左边的值作为key)

key选在最左边,那R就要先走找小,L后走找大(升序)

经过一次单趟排序,最终使得key左边的数据全部都小于key,key右边的数据全部都大于key。

然后我们在将key的左序列和右序列再次进行这种单趟排序,如此反复操作下去,直到左右序列只有一个数据,或是左右序列不存在时,便停止操作,因为这种序列可以认为是有序的。

代码:

//快速排序
void QuickSort(int* a, int left, int right)
{
	//等于就是只有一个节点可以认为是有序的,因此返回
	//大于就是没有节点了,也直接返回
	if (left >= right)
		return;
	int begin = left;
	int end = 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]);
	}
	//将keyi位置的节点与相遇节点进行数据交换
	//让原keyi位置节点的值到最终因该到的位置
	Swap(&a[left], &a[keyi]);
	//更新keyi
	keyi = left;
	//[begin, keyi-1]keyi[keyi+1, end];
	QuickSort(a, begin, keyi - 1);
	QuickSort(a, keyi + 1, end);
}

L:找比key大的数,所以比key小L才会走,直到比key大或者L与r相遇才会停下

r:找比key小的数,所以比key大r才会走,直到比key小或者与L相遇才会停下来

有人肯定会问r与L相遇的位置一定比a[keyi]小吗?

为什么会有这种疑问,因为最后当right等于left的时候,循环退出,a[keyi]和a[left]会交换数据,使得a[keyi]排到最正确的位置,而a[keyi]左边的值都是小于等于a[keyi],而a[keyi]右边的值都是大于等于a[keyi],所以r与L相遇的位置一定比a[keyi]小吗?

答案:是的(右边先走保证的)


L遇R:R先走,R在比key小的位置停下来了,L没有找到比key大的,就会跟R相遇,相遇位置就是R停下来的位置,就是比key小的位置


R遇L:第一轮以后的,先交换了,L位置的值小于key,R位置的值大于key,R启动找小,没有找到,跟L相遇了,相遇位置就是L停下的位置,这个位置比key小

R遇L:第一轮R遇L,那么就是R没有找到小的,直接就一路左移,遇到L,也就是key的位置

快排理想是这种情况:有N个数据,然后我们只需要走高度次也就是logN,就能排好数据,所以时间复杂度是O(NlogN)

时间复杂度:O(NlogN)

前后指针版本

单趟的动图演示:

前后指针法的单趟排序的基本步骤如下:

  1. 选出一个key,一般是最左边或是最右边的。(这里选最左边)

  2. 起始时,prev指针指向序列开头,cur指针指向prev+1。

  3. 若cur指向的内容小于key,则prev先向后移动一位,然后交换prev和cur指针指向的内容,然后cur指针++;若cur指向的内容大于key,则cur指针直接++。如此进行下去,直到cur指针越界,此时将key和prev指针指向的内容交换即可。

经过一次单趟排序,最终也能使得key左边的数据全部都小于key,key右边的数据全部都大于key。

然后也还是将key的左序列和右序列再次进行这种单趟排序,如此反复操作下去,直到左右序列只有一个数据,或是左右序列不存在时,便停止操作。

void QuickSort2(int* a, int left, int right)
{
	//等于就是只有一个节点可以认为是有序的,因此返回
	//大于就是没有节点了,也直接返回
	if (left >= right)
		return;
	int prev = left;
	int cur = left + 1;
	int begin = left;
	int end = right;
	//这里就不对key进行优化
	int keyi = left;
	while (cur <= end)
	{
		if (a[cur] < a[keyi] && ++prev != cur)//cur指向的内容小于key
		{
			//prev要是等于cur我们就可以不交换
			Swap(&a[prev], &a[cur]);
		}
		cur++;
	}
	Swap(&a[prev], &a[keyi]);
	keyi = prev;
	QuickSort2(a, begin, keyi - 1);
	QuickSort2(a, keyi + 1, end);
}

快速排序优化

  1. 三数取中法选key

  2. 取区间中的随机数做key

  3. 递归到小的子区间时,可以考虑使用插入排序

三数取中选key

快速排序的时间复杂度是O(NlogN),是我们在理想情况下计算的结果。在理想情况下,我们每次进行完单趟排序后,key的左序列与右序列的长度都相同:

若每趟排序所选的key都正好是该序列的中间值,即单趟排序结束后key位于序列正中间,那么快速排序的时间复杂度就是O(NlogN)

可是谁能保证你每次选取的key都是正中间的那个数呢?当待排序列本就是一个有序的序列时,我们若是依然每次都选取最左边或是最右边的数作为key,那么快速排序的效率将达到最低:

可以看到,这种情况下,快速排序的时间复杂度退化为O(N^2)。其实,对快速排序效率影响最大的就是选取的key,若选取的key越接近中间位置,则则效率越高。

为了避免这种极端情况的发生,于是出现了三数取中选key:

三数取中,当中的三数指的是:最左边的数、最右边的数以及中间位置的数。三数取中就是取这三个数当中,值的大小居中的那个数作为该趟排序的key。这就确保了我们所选取的数不会是序列中的最大或是最小值了。

// 三数取中  left  mid  right
// 大小居中的值,也就是不是最大也不是最小的
int GetMidi(int* a, int left, int right)
{
	int mid = (left + right) / 2;
	if (a[left] > a[mid])
	{
		if (a[mid] > a[right])
		{
			return mid;
		}
		else if (a[right] > a[left])
		{
			return left;
		}
		else
		{
			return right;
		}
	}
	else
	{
		if (a[left] > a[right])
		{
			return left;
		}
		else if (a[right] > a[mid])
		{
			return mid;
		}
		else
		{
			return right;
		}
	}
}

//快速排序优化方式1(三数取中)
void QuickSort(int* a, int left, int right)
{
	//等于就是只有一个节点可以认为是有序的,因此返回
	//大于就是没有节点了,也直接返回
	if (left >= right)
		return;
	int begin = left;
	int end = right;
	int midi = GetMidi(a, left, right);
	Swap(&a[left], &a[midi]);
	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]);
	}
	//将keyi位置的节点与相遇节点进行数据交换
	//让原keyi位置节点的值到最终因该到的位置
	Swap(&a[left], &a[keyi]);
	//更新keyi
	keyi = left;
	//[0, keyi-1]keyi[keyi+1, end];
	QuickSort(a, begin, keyi - 1);
	QuickSort(a, keyi + 1, end);

}

随机数选key

之所以会出现随机数选key的原因和三数取中选key是一样的,都是为了避免当数组是降序的(有序)然后我们排升序,key因为在最左边的位置,所以r往左找小是一直找不到,所以right的值是不变的,而left往右找大,而因为是降序所以left会一直往右走直到与right相遇,因此随机数选key就是在left和right区间中随机选一个值做key,但这个方法我认为不如三数取中哈。

随机数选key中的注意点:

首先rand()需要%上(right - left),括号里是否加1都可以,这里区间不加1是为了不想选到最后一个数,如果最后一个数是最大值,那交换完后右边找比他小的就会找不到。但是加1也可以,因为也不会每次都那么衰。

随机数用randi变量接收后还需要加上left,因为左区间left不一定是从0开始

代码:

//快速排序优化方式1(选[left, right]区间中的随机数做key)
void QuickSort(int* a, int left, int right)
{
	//等于就是只有一个节点可以认为是有序的,因此返回
	//大于就是没有节点了,也直接返回
	if (left >= right)
		return;
	int begin = left;
	int end = right;
	//取区间中的随机数做key
	int randi = rand() % (right - left);
	//randi还需要加left的原因是左区间的left不一定是从0开始
	randi += left;
	Swap(&a[left], &a[randi]);
	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]);
	}
	//将keyi位置的节点与相遇节点进行数据交换
	//让原keyi位置节点的值到最终因该到的位置
	Swap(&a[left], &a[keyi]);
	//更新keyi
	keyi = left;
	//[0, keyi-1]keyi[keyi+1, end];
	QuickSort(a, begin, keyi - 1);
	QuickSort(a, keyi + 1, end);
}

小区间优化

我们可以看到,就算是上面理想状态下的快速排序,也不能避免随着递归的深入,每一层的递归次数会以2倍的形式快速增长。
为了减少递归树的最后几层递归,我们可以设置一个判断语句,当序列的长度小于某个数的时候就不再进行快速排序,转而使用其他种类的排序。小区间优化若是使用得当的话,会在一定程度上加快快速排序的效率,而且待排序列的长度越长,该效果越明显。

这里小区间优化使用的排序是插入排序。

代码:

//快速排序优化方式3(三数取中和小区间优化)
void QuickSort3(int* a, int left, int right)
{
	//等于就是只有一个节点可以认为是有序的,因此返回
	//大于就是没有节点了,也直接返回
	if (left >= right)
		return;
	int begin = left;
	int end = right;
	int midi = GetMidi(a, left, right);
	Swap(&a[left], &a[midi]);
	int keyi = left;
	//当区间的值小于10个元素我们就可以用小区间优化
	//当然了,小于几用小区间优化都可以,自己控制
	if (right - left < 10)
	{
		//插入排序第二个参数传的是要排的元素个数
		InsertSort(a, right - left + 1);
	}
	else
	{
		while (left < right)
		{
			//右边先走找小
			while (left < right && a[right] >= a[keyi])
			{
				right--;
			}
			//左边后走找大
			while (left < right && a[left] <= a[keyi])
			{
				left++;
			}
			Swap(&a[left], &a[right]);
		}
		//将keyi位置的节点与相遇节点进行数据交换
		//让原keyi位置节点的值到最终因该到的位置
		Swap(&a[left], &a[keyi]);
		//更新keyi
		keyi = left;
		//[0, keyi-1]keyi[keyi+1, end];
		QuickSort(a, begin, keyi - 1);
		QuickSort(a, keyi + 1, end);
	}
}

非递归实现

递归改非递归一般有2种方法,第一种就是改循环,第二种就是使用数据结构中学过的栈。

而快排改非递归我这边是用栈来实现,并且快速排序使用的是前后指针法

// 快速排序非递归
void QuickSortNonR(int* a, int left, int right)
{
	ST st;
	STInit(&st);
	STPush(&st, right);
	STPush(&st, left);
	while (!STEmpty(&st))
	{
		int begin = STTop(&st);
		STPop(&st);
		int end = STTop(&st);
		STPop(&st);
		// 单趟排序
		int keyi = begin;
		int prev = begin;
		int cur = begin + 1;
		while (cur <= end)
		{
			if (a[cur] < a[keyi] && ++prev != cur)
			{
				Swap(&a[prev], &a[cur]);
			}
			cur++;
		}
		Swap(&a[keyi], &a[prev]);
		keyi = prev;
		//[begin, keyi - 1] keyi[keyi + 1, end]
		//入右区间
		//keyi + 1 < end满足条件则区间内至少有2个元素
		if (keyi + 1 < end)
		{
			STPush(&st, end);
			STPush(&st, keyi + 1);
		}
		if (begin < keyi - 1)
		{
			STPush(&st, keyi - 1);
			STPush(&st, begin);
		}
	}
}

归并排序

递归实现

归并排序基本思想:

  1. 分解:将待排序序列分成两个子序列,递归对子序列继续分解,直到子序列长度为 1。
  2. 合并:将两个已经排好序的子序列合并成一个有序序列。

归并排序核心算法步骤

  1. 将数组从中间分成两部分,分别对这两部分进行归并排序。
  2. 递归完成后,利用辅助空间将两个已排序的子序列合并成一个有序序列。
  3. 重复上述过程,直到整个数组有序。

动图:

如果上面动图不太直观,我们就先来看2段有序序列如何排成一段有序序列

void _MergeSort(int* a, int begin, int end, int* tmp)
{
	if (begin == end)
	{
		return;
	}
	int mid = (begin + end) / 2;
	//[begin,mid][mid+1,end]
	_MergeSort(a, begin, mid, tmp);
	_MergeSort(a, mid + 1, end, tmp);
	int begin1 = begin;
	int end1 = mid;
	int begin2 = mid + 1;
	int end2 = end;
	int i = begin;
    //将两段子区间进行归并,归并结果放在tmp中
	//有一个不满足就跳出循环
	while (begin1 <= end1 && begin2 <= end2)
	{
        //将较小的数据优先放入tmp
		if (a[begin1] < a[begin2])
		{
			tmp[i++] = a[begin1++];
		}
		else
		{
			tmp[i++] = a[begin2++];
		}
	}
    //当遍历完其中一个区间,将另一个区间剩余的数据直接放到tmp的后面
	while (begin1 <= end1)
	{
		tmp[i++] = a[begin1++];
	}

	while (begin2 <= end2)
	{
		tmp[i++] = a[begin2++];
	}
    //因为begin不一定是从0开始,所以这里需要跳过begin位置开始将排完序的数据拷贝回原数组
	memcpy(a + begin, tmp + begin, sizeof(int) * (end - begin + 1));
}

//归并排序
void MergeSort(int* a, int n)
{
	int* tmp = (int*)malloc(sizeof(int) * n);
	if (tmp == NULL)
	{
		perror("malloc fail");
		return;
	}
	//这里需要调用归并排序的子函数
	//因为tmp如果在不传过去的话,那每次入栈的时候都需要开空间
	_MergeSort(a, 0, n - 1, tmp);

	free(tmp);
	tmp = NULL;
}

时间复杂度:O(NlogN)  空间复杂度:O(N)

归并排序缺点就是需要一个辅助数组,所以空间复杂度是O(N)

非递归实现

归并排序非递归写法我这边使用循环解决:

归并排序的非递归算法并不需要借助栈来完成,我们只需要控制每次参与合并的元素个数即可,最终便能使序列变为有序:

代码(有误)

//归并排序非递归
void MergeSortNonR(int* a, int n)
{
	int* tmp = (int*)malloc(sizeof(int) * n);
	if (tmp == NULL)
	{
		perror("malloc fail");
		return;
	}
	int gap = 1;
	while (gap < n)
	{
		//printf("gap:%d->", gap);
		for (int j = 0; j < n; j += 2 * gap)
		{
			int begin1 = j;
			int end1 = begin1 + gap - 1;
			int begin2 = end1 + 1;
			int end2 = begin2 + gap - 1;
			int i = begin1;
			//printf("[%d,%d][%d,%d]", begin1, end1, begin2, end2);
			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++];
			}
			memcpy(a + j, tmp + j, sizeof(int) * (end2 - j + 1));

		}
		gap *= 2;
		//printf("\n");
	}
}

当我们这么写的时候,如果数据是偶数倍,那依然可以排序成功,如果数据是奇数倍,那就会出问题

偶数倍

奇数倍

然后我们在调试的时候加上输出语句打印出边界看看

我们可以发现用上面的写法遇到奇数倍数据的时候就会出现越界的问题

因此我们若是想写出一个广泛适用的程序,必定需要考虑到某些极端情况:

情况一:
 当最后一个小组进行合并时,第二个小区间存在,但是该区间元素个数不够gap个,这时我们需要在合并序列时,对第二个小区间的边界进行控制。

情况二:
 当最后一个小组进行合并时,第二个小区间不存在,此时便不需要对该小组进行合并。

情况三:
 当最后一个小组进行合并时,第二个小区间不存在,并且第一个小区间的元素个数不够gap个,此时也不需要对该小组进行合并。(可与情况二归为一类)

优化改正后的代码:

//归并排序非递归
void MergeSortNonR(int* a, int n)
{
	int* tmp = (int*)malloc(sizeof(int) * n);
	if (tmp == NULL)
	{
		perror("malloc fail");
		return;
	}
	int gap = 1;
	while (gap < n)
	{
		//printf("gap:%d->", gap);
		for (int j = 0; j < n; j += 2 * gap)
		{
			int begin1 = j;
			int end1 = begin1 + gap - 1;
			int begin2 = end1 + 1;
			int end2 = begin2 + gap - 1;
			int i = begin1;
			//printf("[%d,%d][%d,%d]", begin1, end1, begin2, end2);
			if (end1 >= n || begin2 >= n)
				break;
			//等于n就已经越界了
			if (end2 >= n)
				//把end2调整到数组最后一个位置
				end2 = n - 1;
			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++];
			}
			//此时j看做是begin1,只不过begin1在判断的时候已经被修改了
			//下标0~9一共有10个元素,所以我们求要拷贝多少个元素过去
			//大-小后还需要加1
			memcpy(a + j, tmp + j, sizeof(int) * (end2 - j + 1));

		}
		gap *= 2;
		//printf("\n");
	}
}

计数排序

计数排序,又叫非比较排序。顾名思义,该算法不是通过比较数据的大小来进行排序的,而是通过统计数组中相同元素出现的次数,然后通过统计的结果将序列回收到原来的序列中。

具体步骤如下:

  1. 确定待排序列中的最大元素和最小元素。

  2. 确立辅助数据的大小(最大元素-最小元素+1)

  3. 统计最小至最大元素之间,每个元素出现的次数。(将每个元素减去最小值得到的数据存放在辅助数组对应的下标中。打印的时候在对辅助数组的i加上最大值)

我们我们要将数组:1020,1021,1018,进行排序,难道我们要开辟1022个整型空间吗?
 若是使用计数排序,我们应该使用相对映射,简单来说,数组中的最小值就相对于count数组中的0下标,数组中的最大值就相对于count数组中的最后一个下标。这样,对于数组:1020,1021,1018,我们就只需要开辟用于储存4个整型的空间大小了,此时count数组中下标为i的位置记录的实际上是1018+i这个数出现的次数。

取数据我们只需要遍历一遍count数组,如果数组内的值不为0的话,我们就输出i+min

代码:

//计数排序
void CountSort(int* a, int n)
{
	int max = a[0];
	int min = a[0];
	for (int i = 1; i < n; i++)
	{
		if (max < a[i])
		{
			max = a[i];
		}
		if (min > a[i])
		{
			min = a[i];
		}
	}
	//[1,2,2,1,1]
	//最大-最小为1,所以还要额外在加1,要开2个数组空间的大小
	int rang = (max - min + 1);
	int* tmp = (int*)calloc(rang, sizeof(int));
	//如果是负数的话,那最小的负数会出现在下标为0的位置上
	for (int i = 0; i < n; i++)
	{
		tmp[a[i] - min]++;
	}
	for (int i = 0; i < rang; i++)
	{
		while (tmp[i]--)
		{
			printf("%d ", i + min);
		}
	}
	printf("\n");
}

注:计数排序只适用于数据范围较集中的序列的排序,若待排序列的数据较分散,则会造成空间浪费,并且计数排序只适用于整型排序,不适用与浮点型排序。所以计数排序是小众的受限制的排序
时间复杂度:O(N+range)  空间复杂度:O(range)

排序算法复杂度以及稳定性分析

;