【八大排序详解~C语言版】直接插入排序-希尔排序- 直接选择排序-堆排序-冒泡排序-快速排序-归并排序-计数排序

八大排序

      • 1.直接插入排序
      • 2.希尔排序
      • 3.直接选择排序
        • 直接选择排序改进
      • 4.堆排序
        • 1.建堆
        • 2.利用堆删除思想来进行排序
      • 5.冒泡排序
      • 6.快速排序
        • 递归实现
        • 非递归实现
      • 7.归并排序
        • 递归实现
        • 非递归实现
      • 8.计数排序
  • – the End –

排序算法想必大家不陌生,今天就来详细的做个总结,包括排序算法的复杂度,稳定性,实现方式。

【八大排序详解~C语言版】直接插入排序-希尔排序- 直接选择排序-堆排序-冒泡排序-快速排序-归并排序-计数排序_第1张图片
开始之前我们先来了解下什么是排序算法的稳定性:

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

那么就要问了,稳定型的意义何在呢,即使r[i]和r[j]的相对位置互换了,但r[i]和r[j]值相同,好像也不影响排序的结果啊。
我们来举个简单的例子:

假如现在要给本次参加考试的学生的成绩排名,然后给前六名颁奖,但是如果前六名中有两名及以上的同学的成绩相同,那该怎么排名次呢?
因为排序前的原始序列是根据学生交卷子次序得来的(可能不符合实际,现在交卷子一般都是按考号大小来放的,哈哈!)此时如果用的排序算法是稳定的,即成绩相同的卷子在排序时相对位置不会改变,那么最后就可以根据排序结果来找出谁先交卷谁后交卷来排名,相对会比较公平。

存在即合理,某些情景下,稳定性也有它自己的意义。
**特别注意**:以下排序稳定性的判断是根据排序算法是否可以实现稳定来断定稳定性的,理论上在算法设计上稳定的都可以通过代码写成不稳定的。

下面进入正文-------》


注:下列八大排序均以排升序为例

1.直接插入排序

插入排序,又叫直接插入排序

基本思想:

在待排序的元素中,假设前n-1个元素已有序,现将第n个元素插入到前面已经排好的序列中,使得前n个元素有序。按照此法对所有元素进行插入,直到整个序列有序,即通过构建有序序列,对于未排序数据,在已排序序列中从后向前扫描,找到相应位置并插入。

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

动图演示:

代码:

//插入排序
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])
			{
				a[end + 1] = a[end];
				end--;
			}
			else
				break;
		}
		//本逻辑包含如果待插入的元素与有序序列中的某个元素相等,则将待插入元素插入到相等元素的后面
		a[end + 1] = tmp;
	}

}

复杂度:
在这里插入图片描述
稳定性:
稳定

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

2.希尔排序

希尔排序法又称缩小增量法

希尔排序以其设计者希尔的名字命名,他对直接插入排序的时间复杂度进行分析,得出了以下结论:

  • 1.插入排序的时间复杂度最坏情况下为O(N^2),此时待排序列为逆序,或者说接近逆序。
  • 2.插入排序的时间复杂度最好情况下为O(N),此时待排序列为升序,或者说接近升序。

于是希尔就想:

若是能先将待排序列进行一次预排序,使待排序列接近有序(接近我们想要的顺序),然后再对该序列进行一次直接插入排序。那么只要控制预排序阶段的时间复杂度不超过O(N^2),那么整体的时间复杂度就比直接插入排序的时间复杂度低了,即要比直接插入排序最坏情况更优。

基本思想:
1.先选定一个小于N的整数gap作为第一增量,然后将距离为gap的元素分在同一组,并对每一组的元素进行直接插入排序。然后再取一个比第一增量小的整数作为第二增量,重复上述操作….gap逐渐减为1,当为1时,整个序列将完全有序。

为什么要使增量gap由大到小呢?
gap越大,数据挪动得越快;gap越小,数据挪动得越慢。前期让gap较大,可以让数据更快得移动到自己对应的位置附近,减少挪动次数。

以下面这组数为例:采用的gap为[4,2,1]
【八大排序详解~C语言版】直接插入排序-希尔排序- 直接选择排序-堆排序-冒泡排序-快速排序-归并排序-计数排序_第2张图片

gap=4时: 【八大排序详解~C语言版】直接插入排序-希尔排序- 直接选择排序-堆排序-冒泡排序-快速排序-归并排序-计数排序_第3张图片
gap=2时: 【八大排序详解~C语言版】直接插入排序-希尔排序- 直接选择排序-堆排序-冒泡排序-快速排序-归并排序-计数排序_第4张图片
当gap=1时: (gap=1,即相当于插入排序) 【八大排序详解~C语言版】直接插入排序-希尔排序- 直接选择排序-堆排序-冒泡排序-快速排序-归并排序-计数排序_第5张图片

动图演示:
【八大排序详解~C语言版】直接插入排序-希尔排序- 直接选择排序-堆排序-冒泡排序-快速排序-归并排序-计数排序_第6张图片
代码:

//希尔排序
void Shellsort(int* a, int n)
{
	int gap = n;
	while (gap > 1)
	{
		gap = gap/2;//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;
		}
	}
}

复杂度:
【八大排序详解~C语言版】直接插入排序-希尔排序- 直接选择排序-堆排序-冒泡排序-快速排序-归并排序-计数排序_第7张图片
稳定性:
不稳定

3.直接选择排序

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

步骤:

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

动图演示:
【八大排序详解~C语言版】直接插入排序-希尔排序- 直接选择排序-堆排序-冒泡排序-快速排序-归并排序-计数排序_第8张图片
代码:

//选择排序(一次选一个数)
void SelectSort(int* a, int n)
{
	int i = 0;
	for (i = 0; i < n; i++)//i代表参与该趟选择排序的第一个元素的下标
	{
		int start = i;
		int min = start;//记录最小元素的下标
		while (start < n)
		{
			if (a[start] < a[min])
				min = start;//最小值的下标更新
			start++;
		}
		Swap(&a[i], &a[min]);//最小值与参与该趟选择排序的第一个元素交换位置
	}
}

直接选择排序改进

在每次遍历时同时找到子数组的最大值和最小值,
把最小值放到begin,最大值放到end,然后++begin;--end;,再次遍历寻找次小值和次大值,以此类推直到排序完成。

代码:

void SelectSort(int* a, int n)
{
	int begin = 0;
	int end = n - 1;
	while (begin<end)
	{
		int mini = begin;
		int maxi = begin;
		for (int i = begin; i <=end; i++)
		{
			
			if (a[i] < a[mini])
			{
				mini = i;
			}
			if (a[i] > a[maxi])
			{
				maxi = i;
			}
		}
		swap(&a[begin], &a[mini]);
		if (begin == maxi)//最大值位于序列开头
		{
		//如果原先最大值位于序列开头,那此时最大值已经被换到了mini的位置,此时需要更新最大值的位置
			maxi = mini;
		}
		swap(&a[end], &a[maxi]);
		++begin;
		--end;
	}

}

复杂度:
【八大排序详解~C语言版】直接插入排序-希尔排序- 直接选择排序-堆排序-冒泡排序-快速排序-归并排序-计数排序_第9张图片
稳定性:
不稳定

4.堆排序

堆排序即利用堆的思想来进行排序,总共分为两个步骤:

1.建堆

  • 排升序:建大堆
  • 排降序:建小堆

2.利用堆删除思想来进行排序

建堆和堆删除中都用到了向下调整,因此掌握了向下调整,就可以完成堆排序

1.建堆

我们要建堆,首先就要理解堆的概念:
【八大排序详解~C语言版】直接插入排序-希尔排序- 直接选择排序-堆排序-冒泡排序-快速排序-归并排序-计数排序_第10张图片
然后要学习堆的向下调整算法,因为要用堆排序,你首先得建堆,而建堆需要执行多次堆的向下调整算法

堆向下调整算法
【八大排序详解~C语言版】直接插入排序-希尔排序- 直接选择排序-堆排序-冒泡排序-快速排序-归并排序-计数排序_第11张图片
代码:堆的向下调整(小堆)

//交换函数
void Swap(int* x, int* y)
{
	int tmp = *x;
	*x = *y;
	*y = tmp;
}

//堆的向下调整(小堆)
void AdjustDown(int* a, int n, int parent)
{
	//child记录左右孩子中值较小的孩子的下标
	int child = 2 * parent + 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 = 2 * parent + 1;
		}
		else//成堆
		{
			break;
		}
	}
}

使用堆的向下调整算法,最坏的情况下(即一直需要交换结点),需要循环的次数为:h - 1次(h为树的高度)。而h = log2(N+1)(N为树的总结点数)。所以堆的向下调整算法的时间复杂度为:O(logN) 。

建堆:
【八大排序详解~C语言版】直接插入排序-希尔排序- 直接选择排序-堆排序-冒泡排序-快速排序-归并排序-计数排序_第12张图片
代码实现:

//建堆
for (int i = (n - 1 - 1) / 2; i >= 0; i--)
{
	AdjustDown(php->a, php->size, i);//向下调整
}

向下调整建堆的时间复杂度是O(N)

2.利用堆删除思想来进行排序

2.1堆删除原理:
堆的删除,删除的是堆顶的元素,但是这个删除过程可并不是直接删除堆顶的数据,而是先将堆顶的数据与最后一个结点的位置交换,然后再删除最后一个结点,再对堆进行一次向下调整。

原因:我们若是直接删除堆顶的数据,那么原堆后面数据的父子关系就全部打乱了,需要全体重新建堆,时间复杂度为 O ( N )。
若是用上述方法,那么只需要对堆进行一次向下调整即可,因为此时根结点的左右子树都是小堆,我们只需要在根结点处进行一次向下调整即可,时间复杂度为 O ( log ⁡ ( N ) ) 。

2.2利用堆删除思想来进行排序:

步骤如下:

  • 1、将堆顶数据与堆的最后一个数据交换,然后对根位置进行一次堆的向下调整,但是调整时被交换到最后的那个最大的数不参与向下调整。
  • 2、完成步骤1后,这棵树除最后一个数之外,其余数又成一个大堆,然后又将堆顶数据与堆的最后一个数据交换,这样一来,第二大的数就被放到了倒数第二个位置上,然后该数又不参与堆的向下调整…反复执行下去,直到堆中只有一个数据时便结束。此时该序列就是一个升序。

堆排序代码:

//堆排序
void HeapSort(int* a, int n)
{
	//排升序,建大堆
	//从倒数第一个非叶子结点开始采用向下调整,一直到根
	int i = 0;
	for (i = (n - 1 - 1) / 2; i >= 0; i--)
	{
		AdjustDown(a, n, i);
	}
	//利用堆删除思想来进行排序
	int end = n - 1;//记录堆的最后一个数据的下标
	while (end)
	{
		Swap(&a[0], &a[end]);//将堆顶的数据和堆的最后一个数据交换
		AdjustDown(a, end, 0);//对根进行一次向下调整
		end--;//堆的最后一个数据的下标减一
	}
}

动图演示:
【八大排序详解~C语言版】直接插入排序-希尔排序- 直接选择排序-堆排序-冒泡排序-快速排序-归并排序-计数排序_第13张图片


复杂度:
时间复杂度: O ( N l o g N )  空间复杂度: O ( 1 )

稳定性:
不稳定

5.冒泡排序

冒泡排序可能是最简单粗暴的排序算法。冒泡排序不断遍历整个数组比较相邻的两个元素的大小是否符合最终要求的顺序,如果不符合则交换两个元素的位置,一直向后遍历,直到遍历完数组,这个过程就像泡泡浮出水面一样,所以被称为冒泡排序。

需要注意循环的终止条件的选择,其他这里就不过多解释。
动图演示:
【八大排序详解~C语言版】直接插入排序-希尔排序- 直接选择排序-堆排序-冒泡排序-快速排序-归并排序-计数排序_第14张图片
代码:

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

//法2
void BubbleSort(int* a, int n)
{
	for (int j = n; j > 0; j--)
	{
		for (int i = 0; i <j-2; i++)
		{
			if (a[i] > a[i + 1])
			{
				swap(&a[i], &a[i + 1]);
			}
		}
	}
}

//优化--以法2为例
void BubbleSort(int* a, int n)
{
	int exchange = 0;
	for (int j = n; j > 0; j--)//控制终止条件
	{
		for (int i = 0; i < j - 2; i++)
		{
			if (a[i] > a[i + 1])
			{
				swap(&a[i], &a[i + 1]);
				exchange = 1;
			}
		}
		if (exchange == 0)//一趟下来,如果enchange==0,说明原序列已经是升序,不用接下来比较,直接退出
		{
			break;
		}
	}
}

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

稳定性:
稳定

6.快速排序

快排是一种高效的排序方式,是Hoare于1962年提出的一种二叉树结构的交换排序算法。

基本思想:
从无序队列中挑取一个元素作为基准值,把无序队列分割成独立的两部分,其中一部分的所有数据都比另外一部分的所有数据都要小,然后再按此方法对这两部分数据分别进行分割,整个排序过程可以递归进行,以此达到整个数据变成有序序列。

简单来说:挑元素、划分组、分组重复前两步

对于如何按照基准值将待排序列分为两子序列,常见的方式有:

  • 1、Hoare版本
  • 2、挖坑法
  • 3、前后指针法

递归实现

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

  • 1.选出一个key,一般是最左边或是最右边的。

  • 2.定义一个L和一个R,L从左向右走,R从右向左走。

  • 3.(我们选取最左边的值作为key进行分析在走的过程中,若R遇到小于key的数,则停下,L开始走,直到L遇到一个大于key的数时,将L和R的内容交换,R再次开始走,如此进行下去,直到L和R最终相遇,此时将相遇点的内容与key交换即可。经过一次单趟排序,最终使得key左边的数据全部都小于key,key右边的数据全部都大于key。

需要注意的是:若选择最左边的数据作为key,则需要R先走,这样可以保证相遇点的值比key小,与key交换后满足条件;若选择最右边的数据作为key,则需要L先走,这样可以保证相遇点的值比key大,与key交换后满足条件。

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

动图演示
【八大排序详解~C语言版】直接插入排序-希尔排序- 直接选择排序-堆排序-冒泡排序-快速排序-归并排序-计数排序_第15张图片

代码:
//快排–Hoare版本

//(单趟排序)
int PartSort1(int* a, int left, int right)
{
	
	int keyi = left;
	while (left < right)
	{
		// 左边做key,右边先走找小
		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;
}

//递归
void QuickSort(int* a, int left, int right)
{
	if (left >= right)//当只有一个数据或是序列不存在时,不需要进行操作
		return;

	int keyi = PartSort1(a, left, right);
	
	QuickSort(a, left, keyi - 1);
	QuickSort(a, keyi + 1, right);
}

快排优化–>三数取中法
快速排序的时间复杂度是O(NlogN),是我们在理想情况下计算的结果。在理想情况下,我们每次进行完单趟排序后,key的左序列与右序列的长度都相同,即key为序列的中间值。
【八大排序详解~C语言版】直接插入排序-希尔排序- 直接选择排序-堆排序-冒泡排序-快速排序-归并排序-计数排序_第16张图片

但你不能保证你每次选取的key都是正中间的那个数,当待排序列本就是一个有序的序列时,我们若是依然每次都选取最左边或是最右边的数作为key,那么快速排序的效率将达到最低:
【八大排序详解~C语言版】直接插入排序-希尔排序- 直接选择排序-堆排序-冒泡排序-快速排序-归并排序-计数排序_第17张图片
可以看出选取的key越接近中间位置,则效率越高。

于是有人想到了三数取中,它不会取到最大值或最小值作为key,那么也就避免了上面的极端情况(理想的时间复杂度是(N*logN),如果原序列本身是有序的,即最坏情况,那么就变为了N^2)。

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

int GetMidIndex(int* a, int left, int right)
{
	//int mid = (left + right) / 2;
	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;
		}
	}

}

以后用快排时只要将用三数取中选出的值与最左端的值进行交换,就保证了我们选取的key不会导致最坏情况,我们只需在单趟排序代码开头加上以下两句代码即可:

int midi = GetMidIndex(a, left, right);
swap(&a[left], &a[midi]);

// Hoare方法有不少容易出错细节
// 有人就进行了优化,对于单趟给了以下两种方式:

挖坑法
前后指针法

2.挖坑法
挖坑法的单趟排序的基本步骤如下:

  • 1、选出一个数据(一般是最左边或是最右边的)存放在key变量中,在该数据位置形成一个坑。
  • 2、定义一个L和一个R,L从左向右走,R从右向左走。(若在最左边挖坑,则需要R先走;若在最右边挖坑,则需要L先走)。
  • 3、(选取最左边的作为坑位)在走的过程中,若R遇到小于key的数,则将该数抛入坑位,并在此处形成一个坑位,这时L再向后走,若遇到大于key的数,则将其抛入坑位,又形成一个坑位,如此循环下去,直到最终L和R相遇,这时将key抛入坑位即可。

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

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

动图演示:

代码:

//单趟
int PartSort1(int* a, int left, int right)
{
	//三数取中
	int midi = GetMidIndex(a, left, right);
	swap(&a[left], &a[midi]);
	//挖坑法
	int key = a[left];
	int hole = left;//在最左边形成一个坑位
	while (left < right)
	{
		// 右边去找小,填到左边的坑里面
		while (left < right && a[right] >= key)
		{
			--right;
		}

		// 把右边找的小的,填到左边的坑,自己形成新的坑
		a[hole] = a[right];
		hole = right;

		// 左边去找大,填到右边的坑里面
		while (left < right && a[left] <= key)
		{
			++left;
		}

		// 把左边找的大的,填到右边的坑,自己形成新的坑
		a[hole] = a[left];
		hole = left;
	}

	a[hole] = key;
	return hole;
}

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

  • 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的左序列和右序列再次进行这种单趟排序,如此反复操作下去,直到左右序列只有一个数据,或是左右序列不存在时,便停止操作。

动图演示:

代码:

//单趟
int PartSort1(int* a, int left, int right)
{
	//三数取中
	int midi = GetMidIndex(a, left, right);
	swap(&a[left], &a[midi]);
	//前后指针法
	int keyi = left;
	int prev = left;
	int cur = prev + 1;

	while (cur <= right)
	{
		if (a[cur] < a[keyi] && ++prev != cur)//cur指向的内容小于key
			//如果(++prev==cur),那么也不必要进行Swap(&a[prev], &a[cur]);这一步了。

			Swap(&a[prev], &a[cur]);

		++cur;
	}

	Swap(&a[prev], &a[keyi]);

	return prev;
}

总结:下面这三种快排的递归方法时间复杂度都是O(NlogN)

  • 1、Hoare版本
  • 2、挖坑法
  • 3、前后指针法

————————————————

非递归实现

递归有缺陷--当数据量大即深度足够大时,会导致栈溢出。 非递归-利用栈(数据结构中的栈是在堆区开辟的,堆区所占内存空间要比栈区大,一般不会导致溢出)
我们要将快排改为非递归,一般需要借用一个数据结构,那就是栈。将Hoare版本、挖坑法以及前后指针法的快速排序改为非递归版本,其实主体思想一致,只是调用的单趟排序的算法不同而已。

快速排序的非递归算法基本思路:

  • 1、先将待排序列的最后一个元素的下标(右)和第一个元素的下标(左)入栈。 -----(出栈单趟排时先出左,后出右)
  • 2、当栈不为空时,读取栈中的信息(一次读取两个:一个是L,另一个是R),然后调用某一版本的单趟排序,排完后获得了key的下标,然后判断key的左序列和右序列是否还需要排序,若还需要排序,就将相应序列的L和R入栈;若不需排序了(序列只有一个元素或是不存在),就不需要将该序列的信息入栈。
  • 3、反复执行步骤2,直到栈为空为止。

【八大排序详解~C语言版】直接插入排序-希尔排序- 直接选择排序-堆排序-冒泡排序-快速排序-归并排序-计数排序_第18张图片

用栈实现需要大家先实现一个栈及栈相关功能,具体可以参考:
栈的介绍与实现

代码:

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 keyi = PartSort1(a, begin, end); //以Hoare版本为例,单趟排
		//  [begin, keyi-1] keyi [keyi+1, end]
		if (keyi + 1 < end)//右边至少还有一个数时进入条件---否则右边不需要再单趟排了
		{
			StackPush(&st, end);
			StackPush(&st, keyi + 1);
		}

		if (begin < keyi - 1)//左边至少还有一个数时进入条件---否则左边不需要再单趟排了
		{
			StackPush(&st, keyi - 1);
			StackPush(&st, begin);
		}
	}

	StackDestroy(&st);//销毁栈
}

时间复杂度: O ( N l o g N )
稳定性:不稳定
————————————————

7.归并排序

基本思想:
归并排序(MERGE-SORT)是建立在归并操作上的一种有效的排序算法,该算法是采用分治法(Divide andConquer)的一个非常典型的应用。

递归实现

归并排序对序列的元素进行逐层折半分组,然后从最小分组开始比较排序,合并成一个大的分组,逐层进行,最终所有的元素都是有序的。

归并排序核心步骤:
【八大排序详解~C语言版】直接插入排序-希尔排序- 直接选择排序-堆排序-冒泡排序-快速排序-归并排序-计数排序_第19张图片
动图演示:
【八大排序详解~C语言版】直接插入排序-希尔排序- 直接选择排序-堆排序-冒泡排序-快速排序-归并排序-计数排序_第20张图片

代码:



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

	int mid = (right + left) / 2;
	// [left, mid][mid+1, right]
	_MergeSort(a, left, mid, tmp);
	_MergeSort(a, mid + 1, right, tmp);
	// 归并
	int begin1 = left, end1 = mid;
	int begin2 = mid + 1, end2 = right;
	int index = left;
	while (begin1 <= end1 && begin2 <= end2)
	{
		if (a[begin1] < a[begin2])
		{
			tmp[index++] = a[begin1++];
		}
		else
		{
			tmp[index++] = a[begin2++];
		}
	}
//当遍历完其中一个区间,将另一个区间剩余的数据直接放到tmp的后面
	while (begin1 <= end1)
	{
		tmp[index++] = a[begin1++];
	}

	while (begin2 <= end2)
	{
		tmp[index++] = a[begin2++];
	}

	// 归并后的结果,拷贝回到原数组
	for (int i = left; i <= right; ++i)
	{
		a[i] = tmp[i];
	}
}
void MergeSort(int* a, int n)
{
	//申请一个与待排序列大小相同的数组用于合并过程两个有序的子序列,合并完毕后再将数据拷贝回原数组
	int* tmp = (int*)malloc(sizeof(int) * n);
	_MergeSort(a, 0, n - 1, tmp);
	free(tmp);
}

时间复杂度: O ( N l o g N )  空间复杂度: O ( N )
————————————————

非递归实现

原来的递归过程是将待排序集合一分为二,直至排序集合就剩下一个元素位置,然后不断的合并两个排好序的数组。所以非递归思想为,将数组中的相邻元素两两配对。将他们排序,构成n/2组长度为2的排序好的子数组段,然后再将他们排序成长度为4的子数组段,如此继续下去,直至整个数组排好序。

动图演示:
【八大排序详解~C语言版】直接插入排序-希尔排序- 直接选择排序-堆排序-冒泡排序-快速排序-归并排序-计数排序_第21张图片
代码:

void _MergeSortNonR(int* a, int n)//非递归
{
	int* tmp = (int*)malloc(sizeof(int) * n);
	int groupNum = 1;
	while (groupNum < n)
	{
		
		for (int i = 0; i < n; i += 2 * groupNum)
		{
			// [begin1][end1] [begin2,end2] 
			// 归并
			int begin1 = i, end1 = i + groupNum - 1;
			int begin2 = i + groupNum, end2 = i + groupNum * 2 - 1;
			int index = begin1;

			// 数组数据个数,并不一定是按整数倍,所以划分的分组可能越界或者不存在
			// 1、[begin2,end2] 不存在, 修正为一个不存在的区间
			if (begin2 >= n)
			{
				begin2 = n + 1;
				end2 = n;
			}

			// 2、end1越界,修正一下
			if (end1 >= n)
			{
				end1 = n - 1;
			}

			// 3、end2越界,需要修正后归并
			if (end2 >= n)
			{
				end2 = n - 1;
			}
			
			while (begin1 <= end1 && begin2 <= end2)
			{
				if (a[begin1] < a[begin2])
				{
					tmp[index++] = a[begin1++];
				}
				else
				{
					tmp[index++] = a[begin2++];
				}
			}
			//当遍历完其中一个区间,将另一个区间剩余的数据直接放到tmp的后面
			while (begin1 <= end1)
			{
				tmp[index++] = a[begin1++];
			}

			while (begin2 <= end2)
			{
				tmp[index++] = a[begin2++];
			}
		}
		// 拷贝回原数组
		for (int i = 0; i < n; ++i)
		{
			a[i] = tmp[i];
		}

		groupNum *= 2;
	}

	free(tmp);
}

void TestMergeSort()
{
	
	int	a[] = { 10, 6, 7, 1, 8, 9, 4,2 };
	_MergeSortNonR(a, sizeof(a) / sizeof(int));
}

时间复杂度: O ( N l o g N )  空间复杂度: O ( N )

归并排序稳定性:
稳定

归并的缺点在于需要O(N)的空间复杂度,归并排序的思考更多的是解决在磁盘中的外排序问题
————————————————

8.计数排序

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

思想:计数排序又称为鸽巢原理,是对哈希直接定址法的变形应用。

基本思路:
我们先给一个数组:

int	a[] = { 25302303 };

首先,我们遍历一次数组,找到最大值和最小值,然后根据最大值MAX与最小值MIN创建一个MAX-MIN+1长度的数组count.为什么创建这样长度的数组呢,因为只有创建了这样长度的数组,[MIN,MAX]区间内的每个元素才有对应的位置进行存放。
接下来通过图来理解:
【八大排序详解~C语言版】直接插入排序-希尔排序- 直接选择排序-堆排序-冒泡排序-快速排序-归并排序-计数排序_第22张图片
我们看到上面用了映射方法中的绝对映射,即arr数组中的元素是几就在count数组中下标为几的位置++

很显然,其实上面这个思路有缺陷,假如我们要排序的数最大为100,最小为50,即位于[50,100]之间,那我们就要建一个大小为51的count数组,下标从0-50,这样就没办法做到绝对映射。

因此,我们应该使用相对映射,将数组中的最小值对应count数组中的0下标,数组中的最大值对应count数组中的最后一个下标。此时count数组中下标为i的位置记录的实际上是50+i这个数出现的次数。
动图演示:

代码:

void CountSort(int* a, int n)
{
	//遍历找最大最小值
	int min = a[0], max = a[0];
	for (int i = 1; i < n; ++i)
	{
		if (a[i] < min)
		{
			min = a[i];
		}

		if (a[i] > max)
		{
			max = a[i];
		}
	}
	//创建count数组
	int range = max - min + 1;
	int* count = (int*)calloc(range, sizeof(int));

	// 统计arr中每个数出现次数,并根据相对映射记录到count中
	for (int i = 0; i < n; ++i)
	{
		count[a[i] - min]++;
	}

	// 根据count数组排序
	int i = 0;
	for (int j = 0; j < range; ++j)
	{
		while (count[j]--)
		{
			a[i++] = j + min;
		}
	}
}

void TestCountSort()
{
	int	a[] = { 25302303 };
	CountSort(a, sizeof(a) / sizeof(int));
}

总结: 计数排序需要的额外空间比较大,并且空间浪费的情况也会比较严重,因为一旦序列中MAX与MIN的差距过大,那么需要的内存空间就会非常大 。
缺点:只适合整数排序、浮点数/字符串等等就不行

复杂度:
时间复杂度: O ( N + r a n g e )   空间复杂度: O ( r a n g e )
稳定性:
稳定

– the End –

以上就是我分享的八大排序相关内容,感谢阅读!

本文收录于专栏:数据结构与算法
关注作者,持续阅读作者的文章,学习更多知识!
https://blog.csdn.net/weixin_53306029?spm=1001.2014.3001.5343

2022/2/21
————————————————

你可能感兴趣的:(数据结构与算法,c语言,排序算法,算法)