【基础算法】八大排序算法:直接插入排序,希尔排序,选择排序,堆排序,冒泡排序,快速排序(快排),归并排序,计数排序

news2024/11/15 8:31:34

文章目录

  • ✔️前言
  • 直接插入排序
  • 希尔排序
  • 选择排序
    • 1. 选择排序基础
    • 2. 选择排序优化
    • 3. 复杂度的分析
  • 堆排序【⭐重点掌握⭐】
    • 1. 对堆的认识和数组建堆
    • 2. 对数组进行堆排序操作
    • 3. 复杂度的分析
  • 冒泡排序
  • 快速排序【⭐重点掌握⭐】
    • 1. 霍尔法
    • 2. 挖坑法
    • 3. 前后指针法
    • 4. 快速排序优化
      • 💯三数取中选keyi值
      • 💯小区间优化
    • 5. 非递归实现
    • 6. 复杂度分析
  • 归并排序【⭐重点掌握⭐】
    • 1. 常规实现
    • 2. 非递归实现
    • 3. 复杂度分析
  • 计数排序
    • 📖复杂度分析
  • 排序算法复杂度及稳定性
  • 整体代码【随意取】
  • ✔️写在最后

✔️前言

🚩排序可谓是老生常谈了,在这里,我给大家带来一些常用的排序算法。
🚩常用的排序算法有八个:直接插入排序,希尔排序,选择排序,堆排序,冒泡排序,快速排序(快排),归并排序,计数排序。每一个排序算法都有其独特的思想,我们不仅要学会它的思想,还要能够在合适的场景中选出合适的排序算法。因此,这一块,要很熟练很熟练。
🚩本章所有的排序均以升序为例来讲解,弄懂了升序,降序也是不在话下。

直接插入排序

直接插入排序其实我们从小就在接触了,我们之前打的跑得快,斗地主,抓牌整理牌的过程就是类似于插入排序。

在这里插入图片描述

对于插入排序,它的过程是如何的呢?我们以下面这组乱序数组为例(假设这里以升序为例):

5    1    2    4    6    3

由于数组的第一个元素是有序的,所以我们要从数组的第二个元素开始,也就是从下标为1的元素开始执行插入排序。

当插入第i (i >= 1) 个元素时,前面的array[0],array[1],…,array[i-1] 已经排好序,此时将array[i] 的值与array[ i - 1 ] , array[ i - 2 ] , … 的值依次进行比较,比array[i]大就将其往后移,找到正确的插入位置将array[i] 插入即可。

i1(定义一个变量tmp存储此时要插入的值,再定义一个变量end指向要插入的元素的前一个位置),也就是上面数组对元素1的插入,此时将1与前面一个元素5比较,发现51大,这时就将5后移放在1的位置,此时前面的有序序列已经遍历完毕,将tmp放入正确的位置,1就插入完毕。1插入后,数组变为:1 5 2 4 6 3,此时对元素2(下标也为2)进行插入,同样的进行上面的操作,5被挪动到下标为2的位置,5处理完后,元素2与元素1比较,1 < 2,此时说明数组已经有序,然后将元素2插入到元素1的后面的位置就OK了。2插入完后数组变为:1 2 5 4 6 3,同样的,后面对元素4,6,3的插入也是如此,由后向前依次与前面的有序序列的元素进行比较,比较出无序就将被比较的元素往后挪动一个位置,比较出有序,说明该元素应该被插入在被比较的元素的后面。

图示:

在这里插入图片描述

动图演示:

在这里插入图片描述

代码实现:

void InsertSort(int* a, int n)
{
	assert(a);
	
	// 从下标为1的元素开始
	for (int i = 1; i < n; i++)
	{
		// end 指向要插入的元素的前一个元素(前面是一个有序序列)
		int end = i - 1;
		// tmp 存放要插入的元素的值
		int tmp = a[i];
		while (end >= 0)
		{	
			// 如果大于 tmp 就将其向后挪动一位
			if (a[end] > tmp)
			{
				// 挪动,相当于覆盖后面的值
				a[end + 1] = a[end];
				// 减减将 tmp 再与前面的元素进行比较
				end--;
			}
			else break;  // 如果找到一个元素使其能够有序,就跳出循环,在其后插入
		}
		// 由后向前 在第一个比 tmp 小的数的后面的位置插入 tmp 
		a[end + 1] = tmp;
	}
}
  • 对于直接插入排序的时间复杂度,从最坏的角度看,每一趟插入的次数相加是一个等差数列,因此直接插入排序的时间复杂度为 O(logN^2)。而最好的情况,就是数组有序,此时每一个元素的插入排序都不会进行,所以相当于只是遍历了一遍数组,时间复杂度为O(N)

  • 很明显,空间复杂度为 O(1)

  • 直接插入排序是稳定的。(除排序交换了两个元素的初始相对位置顺序之外,并没有改变其它任意两个元素初始的相对位置(开始谁在前,现在还是谁在前)。)


希尔排序

希尔排序是直接插入排序的一个优化版。

那么它是如何优化的呢?

希尔排序法又称缩小增量法。希尔排序法的基本思想是:一个数组,它的长度为n,先选定一个整数gap(gap < n),把待排序的数组的所有元素分成若干个组,所有距离为gap的分在同一组内,并对每一组内的元素进行距离为gap的插入排序。当一个gap排完每一组后,gap按一定规律变小,并重复上述分组和排序的工作。当gap == 1时,相当于直接插入排序,最后数组就有序了。

图示排序过程:

在这里插入图片描述

每个gap值都对应着一趟距离为gap的插入排序,每一趟排序会使数组越来越接近有序,直到最后gap1,相当于是一趟直接插入排序。当gap > 1时都是预排序,目的是让数组更接近于有序。当gap == 1时,数组已经接近有序的了,就会很快。这样整体而言,可以达到优化的效果。

在这里插入图片描述

关于gap,每次排完一趟到下一趟排序可以将他除以2作为这趟排序的分组排序距离,这样循环往复,最终,gap会变成1,数组会有序。

代码实现:

// 希尔排序
void SherSort(int* a, int n)
{
	assert(a);

	int gap = n;
	while (gap > 1)
	{
		// 这里是 gap 每次除以 2
		// 当然其它取法也行,只要 gap 最后能够变为 1 即可
		gap /= 2;
		for (int i = 0; i < n - gap; i++)
		{
			int end = i;
			// tmp 为要进行距离为gap的插入的值
			int tmp = a[i + gap];
			while (end >= 0)
			{
				if (a[end] > tmp)
				{
					a[end + gap] = a[end];
					// 每一次挪动一个位置,end(下标)要减 gap 距离
					// 因为是按 gap 来分组的
					end -= gap;
				}
				else break;
			}
			// 插入合适的位置
			a[end + gap] = tmp;
		}
	} 
}
  • 时空复杂度分析:

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

在这里插入图片描述

在这里插入图片描述
对于空间复杂度,很明显,为O(1)

稳定性:不稳定。


选择排序

1. 选择排序基础

基本的选择排序思路如下:

每一次从数组的待排序的数据元素中找出最小或者最大的元素放在起始位置,直到所有待排序的数据元素放在应在的位置为止。

如图 ,为选择排序操作升序的情况:
在这里插入图片描述

  • 首先在没有排序的序列中找到最小(大)元素,并与排序序列的起始位置元素交换;
  • 再继续从剩余没有排序的序列续寻找最小(大)元素,然后与没有排序的序列的起始位置元素交换;
  • 重复第二步,直到所有元素均排序完毕即可。

代码实现(这里以升序为例):

void swap(int* a, int* b)
{
	int tmp = *a;
	*a = *b;
	*b = tmp;
}

void SelectSort(int* a, int n)
{
	// 确认数组的有效性
	assert(a);
	
	// 这里的 n - 1 是表示选择只需要选择 n - 1 次
	// 因为最后一次只剩下一个元素已经是有序的了
	// 当然 n 也可以,最后一次循环不进入嘛
	for (int i = 0; i < n - 1; i++)
	{
		// mini 表示:假设待排序序列的起始元素为最小元素
		int mini = i;
		
		// j = i + 1,表示待排序序列起始元素位置的下一个位置
		// 说明从待排序序列起始元素位置的下一个位置开始选择
		for (int j = i + 1; j < n; j++)
		{
			// 找到比 a[mini] 还小的元素就将mini更新为这个元素的下标
			if (a[j] < a[mini])
			{
				mini = j;
			}
		}
		
		// 最后将 mini(此时为待排序序列中最小的元素)指向的元素与待排序序列的起始位置的元素交换
		swap(&a[i], &a[mini]);
		// 到了这里一趟选择排序就完成了
	}
}

测试:

void testSelectSort()
{
	int arr[] = { 9,8,7,6,5,4,3,2,1,0 };
	int n = sizeof(arr) / sizeof(arr[0]);
	SelectSort(arr, n);
	PrintArray(arr, n);
}

运行结果:
在这里插入图片描述


2. 选择排序优化

以升序为例

  • 这个优化其实也就那样,表面看起来算是优化了,实际上也差不多。
  • 在上面的基础上,多加些操作:在遍历未排序序列时,不仅找到最小值,还要找到最大值,然后最小值与未排序序列的初始位置交换,最大值与未排序序列的最后一个位置交换,当两个数都到了相应位置后,未排序序列两边同时缩减,重复前面的操作,直到序列缩没了。

图示:

在这里插入图片描述

由上面的分析可以知道,我们需要两个指针指向未排序序列的两端,每次未排序序列两边缩小1,都需要遍历一遍未排序序列找最小最大值往两边甩,这样从两端开始向内逐渐有序,最终数组就会有序。

代码实现(这里以升序为例):

// 选择排序
void SelectSort(int* a, int n)
{
	// 判断数组有效性
	assert(a);
	
	// 未排序序列的头尾边界,l 为左边界,r 为右边界
	int l = 0, r = n - 1;
	// 如果 l < r ,说明中间可能还存在未排序序列
	while (l < r)
	{
		// 一开始假设最小和最大值为未排序序列的头位置
		int mini = l, maxi = l;
		
		// 从 l 到 r 遍历未排序序列 依次寻找最小和最大值
		for (int i = l; i <= r; i++)
		{
			if (a[i] < a[mini])
			{
				mini = i;
			}
			if (a[i] > a[maxi])
			{
				maxi = i;
			}
		}
		// 将最小和最大值往两边甩
		swap(&a[l], &a[mini]);
		// 这个操作是因为:可能未排序序列头就是最大值,所以maxi指向头位置,而最小值需要甩到头位置来
		// 当最小值放过来后,最大值(刚好指向刚放过来的最小值的位置)又要甩到尾的位置,这就会出现冲突
		// 所以这里判断一下,如果maxi是指向未排序序列的头位置,就需要更新一下maxi
		if (maxi == l)
		{
			maxi = mini;
		}
		swap(&a[r], &a[maxi]);

		// 两端有序后,将未排序序列区间缩小
		l++;
		r--;
	}
}

3. 复杂度的分析

  • 对于选择排序的时间复杂度,为 O(N ^ 2),优化与不优化都是如此,每次放置一个元素或者两个元素到有效的位置都需要遍历数组,第一次可能是 n - 1 次,第二次就是 n - 2,依次类推,就是一个等差数列,所以为O(N ^ 2)。那最好的情况呢?如果此时数组已经有序,它还是要跟不有序一样依次遍历数组,老实的很捏,所以还是O(N ^ 2),这也就是选择排序是八大排序中最烂排序的原因。

  • 而空间复杂度毫无疑问是O(1)


堆排序【⭐重点掌握⭐】

1. 对堆的认识和数组建堆

  • 对堆的认识可以看看博主之前的文章:-> 传送门 <-
  • 总之,堆在逻辑结构上是一棵完全二叉树,在物理结构上是用数组来存储的。

对数组排序,首先就是要对这个数组建堆,如果是要将数组升序,就建为大堆,如果是要将数组降序,就建为小堆

  • 如何建堆?我们从最后一个结点的父节点开始,依次执行向下调整算法,直到根节点执行完全后,便建成了堆。当然我们也可以从第二个结点开始,依次执行向上调整算法,直到最后一个结点执行完后便建成了堆,不过这样的时间复杂度为O(n * logn),而前面的向下调整算法的方式的时间复杂度为O(n),所以这里我们采用向下调整算法的方式来建堆。至于这两个调整算法的时间复杂度是如何计算出来的,这里就不做讨论,它的本质其实是有关数列求和的计算。

  • 对于向下调整算法,我们先要找到该结点(假设下标为parent)的孩子结点,而孩子结点又分为左孩子结点(下标为parent * 2 + 1)和右孩子结点(下标为parent * 2 + 2),所以我们需要找出两个孩子结点当中较大的那个,如果该节点的数据比较大的那个孩子结点的数据要小,那就进行交换,然后循环往复继续向下寻找孩子结点重整堆。

  • 整个操作,我们可以先比较两个孩子的大小找出大的那个,然后在与大的这个孩子结点进行比较,如果父结点比他小(以大堆为例),说明这个孩子结点该上位了。然后继续向下执行这个操作。

向下调整算法方式建堆图示:
在这里插入图片描述

在这里插入图片描述

向下调整算法代码实现(这里以升序为例):

void adjustdown(int* a, int n, int parent)
{
	// 先假设大的那个孩子结点为左孩子结点
	int child = parent * 2 + 1;
	while (child < size)  // 如果child小于此时数组的长度就继续
	{
		// 第一个判断是防止没有右孩子结点的情况
		// 第二个判断是如果右孩子存在并且右孩子结点的数据大于左孩子结点的数据,就child加一指向右孩子结点
		if (child + 1 < size && a[child + 1] > a[child]) child++;
		// 如果父节点数据小于child结点数据,就交换重整堆
		if (a[child] > a[parent])
		{
			swap(&a[child], &a[parent]);
			parent = child;
			child = parent * 2 + 1;
		}
		else break;  // 如果父节点数据大于child结点数据,说明堆已经调整完毕,直接跳出循环不在调整
	}
}

2. 对数组进行堆排序操作

  • 有了建堆的认识,后面的操作也不难了,只不过需要注意几个细节。

  • 当数组建成大堆形式后,堆顶元素是最大的,此时我们可以将堆顶元素与最后一个元素进行交换,这样最大的元素就到了数组的末尾了。然后我们对这个处在数组最后一个位置的最大元素视而不见,将交换过去的堆顶元素执行向下调整算法,这时,第二大的元素就到了堆顶,然后此时的堆顶元素继续与最后一个元素进行交换 (注意第一个交换过去的最大的元素已经不在范围内了,也就是说每将一个当前最大的数交换过去后,可视作n(数组的长度)减一一次) ,然后再将交换过去的堆顶元素执行向下调整算法…这样循环往复,最终该数组就变成了升序。

动图过程展示
在这里插入图片描述

堆排序整体代码实现:

// 堆排序
void adjustdown(int* a, int n, int parent)
{
	// 先假设大的那个孩子结点为左孩子结点
	int child = parent * 2 + 1;
	while (child < size)  // 如果child小于此时数组的长度就继续
	{
		// 第一个判断是防止没有右孩子结点的情况
		// 第二个判断是如果右孩子存在并且右孩子结点的数据大于左孩子结点的数据,就child加一指向右孩子结点
		if (child + 1 < size && a[child + 1] > a[child]) child++;
		// 如果父节点数据小于child结点数据,就交换重整堆
		if (a[child] > a[parent])
		{
			swap(&a[child], &a[parent]);
			parent = child;
			child = parent * 2 + 1;
		}
		else break;  // 如果父节点数据大于child结点数据,说明堆已经调整完毕,直接跳出循环不在调整
	}
}
void HeapSort(int* a, int n)
{
	assert(a);
	
	// 向下调整, 这里是建大堆
	for (int i = (n - 2) / 2; i >= 0; i--) adjustdown(a, n, i);

	// 排序(建的大堆就是升序)
	int end = n - 1;
	while (end > 0)
	{
		swap(&a[end], &a[0]);
		adjustdown(a, end, 0);
		end--;
	}
}

3. 复杂度的分析

  • 堆排序的时间复杂度为 O(N * logN):排序前对数组建堆的向下调整算法整个过程为O(N),后面排序阶段的操作相当于遍历了一遍数组,每一次都需要从根节点(数组开头)执行一次向下调整算法,因此排序阶段的时间复杂度为O(N * logN),所以整体就是O(N * logN)。而最好的情况也是O(N * logN),可以说,堆排序也是很老实的,尽管数组开始有序,在建堆的过程中,就先需要将数组打乱,后面的操作也就一样了。

  • 堆排序没有创建额外的空间,所以空间复杂度为O(1)


冒泡排序

  • 冒泡排序的过程就跟冒泡一样(这话好像并没什么卵用),每次都是相邻两个元素进行比较,如果前面一个元素比后面一个元素数据大,就将这两个元素进行交换,然后继续比对下一组相邻的元素,一趟过后,最大的那个元素会处在数组的最后一个位置。到了下一趟排序,第二大的元素就会到数组的倒数第二个位置,再下一趟就是第三大的元素到正确的位置,接着就是第四大,第五大,直到数组有序为止。

  • 这里的每一趟排序都有一个元素到达正确的位置,也就是说,每次排完一趟后,下一趟需排序的元素就要减一。所以这里我们可以控制每趟排序相邻两个元素比较的次数以及总共要排的趟数:假设数组的元素个数为n,那么总共只需要排n - 1趟即可(因为排完n - 1次后,n - 1个元素都到了正确的位置,那么剩下的那个元素肯定也是到达了正确的位置),而每一趟排序,因为是两两比较,所以只需要比较该趟排序的元素个数减一次。有了这样的控制,我们只需要两个for循环即可完成。

图示理解冒泡排序:

在这里插入图片描述

在这里插入图片描述

  • 如果在排序的时候数组有序了,就可以直接停止排序,这里可以设一个flag来控制。

代码实现:

// 交换函数
void swap(int* a, int* b)
{
	int tmp = *a;
	*a = *b;
	*b = tmp;
}

void BubbleSort(int* a, int n)
{
	assert(a);
	
	// 外层循环控制趟数 (n - 1 趟)
	for (int i = 0; i < n - 1; i++)
	{
		// 立一个 flag
		bool flag = true;
		
		// j < n - i,表示每次需排序的元素个数受 i 控制
		// 当 i 为 0 时,此次排序的个数为 n 个
		// 当 i 为 1 时,此次排序的个数为 n - 1 个
		// ......
		// j 从 1 开始是为了控制前边界。
		// 并且也表示该趟排序只比较 p - 1 次(p 表示该趟排序的元素个数)
		for (int j = 1; j < n - i; j++)
		{
			// 相邻两个元素比较
			// 如果前一个元素比后一个元素大,就交换
			if (a[j - 1] > a[j])
			{
				swap(&a[j - 1], &a[j]);
				// 进了 if 说明数组还没有序,将 flag 置为 false
				flag = false;
			}
		}
		// 如果前面没有进入 if ,说明数组以及有序了,直接结束排序
		// 如果前面进入了 if ,说明还没有序,需要进行下一趟
		if (flag) break;
	}
}

快速排序【⭐重点掌握⭐】

  1. 快速排序俗称快排,是我们在排序中一定要掌握的。既然是叫快排,那么就有它快的道理。
  2. 在这里,给大家提供快排的三种实现方法,每种方法都有它的特性,因此,我们要能够区分开来。
  3. 单单纯纯的快排做不到真的快,还需要一些优化,例如随机选keyi,三数取中(与随机选keyi有同样的作用),小区间优化和三路划分。这里三路划分就不做讲解,感兴趣的可以参考一下这篇文章:快速排序优化。

1. 霍尔法

hoare版本的快速排序为什么叫hoare呢?因为快速排序就是由Hoare1962年提出的一种二叉树结构的交换排序方法。 此实现方法也可以说是开山鼻祖。

霍尔法动图过程展示

在这里插入图片描述

  1. 【核心思路】:任取待排序元素序列中的某元素作为基准值(一般我们选择取最左边),按照该排序码将待排序集合分割成两个子序列,左子序列中所有元素均小于基准值,右子序列中所有元素均大于基准值,然后最左右子序列重复该过程(递归),直到所有元素都排列在相应位置上为止。

  2. 【整个过程】:我们取最左边的数为一个基准值,然后两个指针分别从序列的两端开始走。右边先走,找比基准值小的数,然后左边再走,找比基准值大的数,两个指针都找完后,交换两个指针指向的值,这个意思就是:将比基准值小的数往前放置,比基准值大的数往后放置,直到两个指针相遇,再将基准值与相遇的这个位置的值交换(这个基准值就到了正确的位置,不用再改了),此时整个序列被分割成两个子序列,左边的序列都是比基准值小的数,右边的序列都是比基准值大的数,然后分别递归左序列和右序列,依次这样操作,最终,整个序列就会变得有序。

图示:

第一趟排序,对整个序列进行上述操作:

在这里插入图片描述

然后以排完第一趟的6的位置为基准,分别递归左和右:

在这里插入图片描述
就这样递归下去,每递归一次就排序一次,如果序列只有一个元素或者没有元素,就是有序,此时就返回。

代码实现:

void QuickSort1(int* a, int l, int r)
{
	// 如果序列只有一个元素或者没有元素就返回
    if (l >= r) return;
	
	// 这是三数取中的优化,取三个数大小排中间的那个数作为基准值
    int mid = mid_num(a, l, r);
    // 找到优化的数后与最左边的数交换一下,便于上面思路的操作
    swap(&a[l], &a[mid]);
    // 定义基准值和指向左右两端的指针
    int keyi = l, begin = l, end = r;
	
	// 如果两个指针相遇就停止
    while (begin < end)
    {	
    	// 右边先走找小
        while (begin < end && a[end] >= a[keyi]) end -- ;
        // 然后左边走找大
        while (begin < end && a[begin] <= a[keyi]) begin ++ ;
		
		// 交换两个指针指向的值
        swap(&a[begin], &a[end]);
    }
    // 最后将基准值与两指针相遇位置的值进行交换,此时基准值有序
    swap(&a[keyi], &a[begin]);
	
	// 递归左边所有元素大小都比基准值小的序列
    QuickSort1(a, l, begin - 1);
    // 递归右边所有元素大小都比基准值大的序列
    QuickSort1(a, begin + 1, r);
}

一个问题?为什么要右边先走呢?

实际上,如果取最左边的值为基准值,就右边先走;如果取最右边的值为基准值,就左边先走;我们拿右边先走的例子来说:如果取最左边的值为基准值让右边先走,会发现,两个指针相遇的位置的值始终是比基准值小的,最后与基准值交换后正好符合规律,小的值往左边放。如果左边先走的话,相遇的位置的值是始终都比基准值大的,这就不符合规律了(当然有一种情况是:基准值是最大的数,左边先走的话会一直走到底,最终还是将小的数换过来了,不过经过三数取中优化后,这种情况不会出现)。


2. 挖坑法

参考文章:传送门。

① 直接将最左端的值选出来作为key值,然后 【右边找小】 ,放入坑位,然后更新坑位值为右侧找到的那个数所在的下标;

② 出现了新的坑位后,【左边找大】 ,找到之后将数字放到新的坑位中,然后继续更新坑位。

③ 循环往复上面的步骤,直到两者相遇为止,更新相遇处为最新的坑位,然后将key值放入坑位即可,保证左边比key小,右边比key大。

【整个过程】: 以最左边的值为基准值,以最左边的位置为第一个坑位,然后右边开始找小,找到后将这个小于基准值的数放入当前的坑位,然后更新坑位为当前右边找到的那个小的数的位置。接着左边开始找大,找到后将这个大于基准值的数放入当前的坑位,然后更新坑位为当前左边找到的那个大的数的位置。这样循环往复,直到左指针和右指针相遇,相遇的这个位置就是最新的坑位,最后将基准值放入这个坑位,再递归左序列和右序列即可。

动图过程展示

在这里插入图片描述

在这里插入图片描述

代码实现:

void QuickSort2(int* a, int l, int r)
{
	// 如果序列只有一个元素或者没有元素就返回
    if (l >= r) return;
	
	// 这是三数取中的优化,取三个数大小排中间的那个数作为基准值
    int mid = mid_num(a, l, r);
    // 找到优化的数后与最左边的数交换一下,便于上面思路的操作
    swap(&a[l], &a[mid]);
    // 最开始定义坑位为最左边,基准值为最左端的值
    int hole = l, key = a[l];
    // 左指针和右指针
    int begin = l, end = r;

    while (begin < end)
    {
    	// 右边先走,找小放入坑位,再更新坑位
        while (begin < end && a[end] >= key) end -- ;
        a[hole] = a[end];
        hole = end;

		// 然后左边走,找大放入坑位,再更新坑位
        while (begin < end && a[begin] <= key) begin ++ ;
        a[hole] = a[begin];
        hole = begin;
    }
    // 两个指针相遇后,相遇位置就是最新的坑位,最后将基准值放入最新的坑位当中
    a[hole] = key;

	// 在最后的坑位位置划分左序列和右序列并递归
    QuickSort2(a, l, hole - 1);
    QuickSort2(a, hole + 1, r);
}

3. 前后指针法

动图过程展示

在这里插入图片描述

  • 这种方法被叫做【前后指针法】是因为该方法定义的两个指针都从序列的左边开始走,由于限制条件不同,因此两个指针走的速度也不同,所以被称之为【前后指针法】。

  • 整体思路:

    1. 定义一个prev指针位于起始端,再定义一个cur指针就位于它的后方,记录当前 位置上的key值。

    2. cur指针向后找比key小的值,若是找不到,则一直++;若是cur找到了比key 小的值,prev++,然后交换二者的值之后cur++

    3. 直到cur超过右边界之后,退出循环逻辑,将此时prev位置上的值与key值做一个交换,保证左边比key小,右边比key大。

一过程如图:

在这里插入图片描述

代码实现:

void QuickSort3(int* a, int l, int r)
{
	// 如果序列只有一个元素或者没有元素就返回
    if (l >= r) return;
	
	// 这是三数取中的优化,取三个数大小排中间的那个数作为基准值
    int mid = mid_num(a, l, r);
    // 找到优化的数后与最左边的数交换一下,便于上面思路的操作
    swap(&a[l], &a[mid]);
    // 基准值为最左端的值
    // 两个指针指向左边,cur指向最左端的下一个位置开始走
    int keyi = l, prev = l, cur = l + 1;
	
	// 如果cur还在序列当中就继续
    while (cur <= r)
    {
    	// 如果cur指向的值小于基准值并且prev加一后与cur不相等就交换两个指针指向的值
        if (a[cur] < a[keyi] && ++ prev != cur)
            swap(&a[prev], &a[cur]);
		
		// 无论如何cur要加加
        cur ++ ;
    }
    // 最后prev指向的位置就是基准值应该处在的位置,交换一下
    swap(&a[keyi], &a[prev]);
	
	// 以prev为分界点,递归左序列和右序列
    QuickSort3(a, l, prev - 1);
    QuickSort3(a, prev + 1, r);
}

4. 快速排序优化

  • 快速排序的优化,可以参考这位大佬的文章:传送门。(含有较为复杂的三路划分)

💯三数取中选keyi值

  • 当然也可以随机数选keyi,个人认为三数取中更好。

相关代码:

int mid_num(int* a, int l, int r)
{
    int mid = (l + r) / 2;
    
    if (a[l] > a[r])
    {
        if (a[r] > a[mid]) return r;
        else if (a[l] < a[mid]) return l;
        else return mid;
    }
    else 
    {
        if (a[l] > a[mid]) return l;
        else if (a[r] < a[mid]) return r;
        else return mid;
    }
}

💯小区间优化

  • 在子序列比较小的时候,其实插排是比较快的,因为对于有序的序列,插排可以达到O(n)的复杂度,如果序列比较小,则和大序列比起来会更加有序,这时候使用插排效率要比快排高。其实现方法也很简单:快排是在子序列元素个数变成1时才停止递归,我们可以设置一个阈值n,假设为10,则大于10个元素,子序列继续递归,否则选用插排。(其实在C++STL中,归并算法就是采用了这个思路,当子序列小到一定程度的时候,直接选用插排对子序列进行排序)

  • 快排是在待排数列越趋近于有序时变得越慢,复杂度越高,调用插排可以很好的解决这个问题。

  • 具体实现只需要条件判断控制,然后调用插排相关接口即可,比较简单。


5. 非递归实现

参考文章:数据结构 | 十大排序超硬核八万字详解【附动图演示、算法复杂度性能分析】,找到 #💪快速排序方法的“非递归写法”【校招要求✍】#,该博主讲的很详细,大家看他的很容易懂,我如何写都达不到该博主的意境。

相关代码:

void QuickSortNonR(int* a, int l, int r)
{
    Stack st;
    StackInit(&st);

    STPush(&st, r);
    STPush(&st, l);

    while (!STEmpty(&st))
    {
        int begin = STTop(&st);
        STPop(&st);
        int end = STTop(&st);
        STPop(&st);

        int mid = QuickSort33(a, begin, end);

        if (mid + 1 < end)
        {
            STPush(&st, end);
            STPush(&st, mid + 1);
        }
        if (mid - 1 > begin)
        {
            STPush(&st, mid - 1);
            STPush(&st, begin);
        }
    }

    STDestroy(&st);
}

6. 复杂度分析

参考文章:快速排序及其时间复杂度和空间复杂度。

  • 快速排序涉及到递归调用,所以该算法的时间复杂度还需要从递归算法的复杂度开始说起;

  • 递归算法的时间复杂度公式:T[n] = aT[n/b] + f(n) ;对于递归算法的时间复杂度这里就不展开来说了;

  • 最优情况下时间复杂度

    快速排序最优的情况就是每一次取到的元素都刚好平分整个数组(很显然我上面的不是);

    此时的时间复杂度公式则为:T[n] = 2T[n/2] + f(n);T[n/2]为平分后的子数组的时间复杂度,f[n] 为平分这个数组时所花的时间;

    下面来推算下,在最优的情况下快速排序时间复杂度的计算(用迭代法):
                                         T[n] =  2T[n/2] + n                                                                       ----------------第一次递归
               令:n = n/2       =  2 { 2 T[n/4] + (n/2) }  + n                                               -  ---------------第二次递归
    
                                          =  2^2 T[ n/ (2^2) ] + 2n
    
              令:n = n/(2^2)   =  2^2  {  2 T[n/ (2^3) ]  + n/(2^2)}  +  2n                         ----------------第三次递归  
    
                                          =  2^3 T[  n/ (2^3) ]  + 3n
    
              ......................................................................................                        
    
              令:n = n/(  2^(m-1) )    =  2^m T[1]  + mn                                                  ----------------第m次递归(m次后结束)
    
             当最后平分的不能再平分时,也就是说把公式一直往下跌倒,到最后得到T[1]时,说明这个公式已经迭代完了(T[1]是常量了)。
    
             得到:T[n/ (2^m) ]  =  T[1]    ===>>   n = 2^m   ====>> m = logn;
    
             T[n] = 2^m T[1] + mn ;其中m = logn;
    
             T[n] = 2^(logn) T[1] + nlogn  =  n T[1] + nlogn  =  n + nlogn  ;其中n为元素个数
    
             又因为当n >=  2时:nlogn  >=  n  (也就是logn > 1),所以取后面的 nlogn;
    
             综上所述:快速排序最优的情况下时间复杂度为:O( nlogn )
    
  • 最差情况下时间复杂度

    最差的情况就是每一次取到的元素就是数组中最小/最大的,这种情况其实就是冒泡排序了(每一次都排好一个元素的顺序)

    这种情况时间复杂度就好计算了,就是冒泡排序的时间复杂度:T[n] = n * (n-1) = n^2 + n;

    综上所述:快速排序最差的情况下时间复杂度为:O( n^2 )

  • 平均时间复杂度

      快速排序的平均时间复杂度也是: O(nlogn)
    
  • 空间复杂度

    其实这个空间复杂度不太好计算,因为有的人使用的是非就地排序,那样就不好计算了(因为有的人用到了辅助数组,所以这就要计算到你的元素个数了);我就分析下就地快速排序的空间复杂度吧;

    首先就地快速排序使用的空间是O(1)的,也就是个常数级;而真正消耗空间的就是递归调用了,因为每次递归就要保持一些数据;

    最优的情况下空间复杂度为:O(logn) ;每一次都平分数组的情况
    最差的情况下空间复杂度为:O( n ) ;退化为冒泡排序的情况


归并排序【⭐重点掌握⭐】

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

归并排序体现了一个很重要的思想:分治思想

1. 常规实现

归并可以理解为先递归在合并。

将数组以类似二叉树的形式一直分,一分二,二分四,四分八…直到最后分到只有一个元素,由于一个元素可以看作是有序的,所以从最后的一个元素开始,依次有序合并并返回。

在这里插入图片描述

归并思想:

在这里插入图片描述

合并过程:

在这里插入图片描述

动图过程展示

在这里插入图片描述

代码实现:

// 接口
void MergeSort(int* a, int n)
{
    assert(a);
	
	// 开辟临时数组用来归并两段有序序列
    int* tmp = (int*)malloc(sizeof(int) * n);

    MergeSortY(a, 0, n - 1, tmp);
}

void MergeSortY(int* a, int l, int r, int* tmp)
{
	// 如果最后只剩一个元素或者没有元素就停止归
    if (l >= r) return;

	// 定义mid指向序列中间,并从中间划开
    int mid = (l + r) / 2;

	// 分别递归mid的左边和右边,递归后又是一样的操作
    MergeSortY(a, l, mid, tmp);
    MergeSortY(a, mid + 1, r, tmp);
	
	// i 和 j 分别指向两段有序序列开头开始合并
    int i = l, j = mid + 1;
    // k 为tmp数组的索引
    // 合并两端有序序列,先将合并的序列临时存放在tmp数组
    int k = l;
	
	// 合并过程
    while (i <= mid && j <= r)
    {
        if (a[i] < a[j])
        {
            tmp[k ++ ] = a[i ++ ];
        }
        else 
        {
            tmp[k ++ ] = a[j ++ ];
        }
    }
    // 如果某一段序列还没合并完,就直接插在后面
    // 两个都判断一下,只进去一个
    while (i <= mid) tmp[k ++ ] = a[i ++ ];
    while (j <= r) tmp[k ++ ] = a[j ++ ];
	
	// 最后将合并的序列拷贝到原数组
	// 注意 a + l , tmp + l
    memcpy(a + l, tmp + l, sizeof(int) * (r - l + 1));
}

2. 非递归实现

  • 非递归实现的思想与递归实现的思想是类似的。

  • 既然是非递归,那当然只有循环才可以解决。

  • 这里我们先1个元素为一组,两组为一个合并组依次进行合并;然后再以2个元素为一组,两组为一个合并组依次进行合并…往后每次一组的元素个数都是上一次的一组的元素个数的两倍。

当然,待排序序列的元素个数并不一定是2的次方数,所以这里需要对每两组合并的序列进行一个区间合理范围判断,以此来处理越界或者某些其它的问题。

  1. 如果合并的第一段区间的末尾超出整个数组范围,直接跳出本次循环合并;(区间是的值为下标)
  2. 如果合并的第二段区间的开头超出整个数组范围,第一段没超,也不用合并了,直接跳出本次循环合并。值得注意的是,第一段区间的末尾超出整个数组范围也就意味着第二段区间的开头超出整个数组范围,所以这两种情况只需要判断第二段区间的开头是否超出整个数组范围即可。
  3. 如果合并的第二段区间的开头没有超出整个数组范围,而是第二段区间的末尾超出了整个数组范围,此时只需要将第二段区间的末尾调整为(n (n为数组的元素个数) - 1)即可。

整个过程图示如下:

在这里插入图片描述

代码实现(C语言):

// 接口
void MergeSortNonR(int* a, int n)
{
    assert(a);
	
	// 开辟临时数组用来归并两段有序序列
    int* tmp = (int*)malloc(sizeof(int) * n);

    MergeSortNonRY(a, 0, n - 1, tmp);
}

void MergeSortNonRY(int* a, int l, int r, int* tmp)
{
	// 一开始每组为1个元素
    int gap = 1;
	
	// 求出数组的长度(元素个数)
    int n = r - l + 1;

	// 如果 gap 小于 n,说明至少还有两个带合并的序列
    while (gap < n)
    {
    	// i 每次跳一个合并组的长度
        for (int i = 0; i < n; i += 2 * gap )
        {	
        	// 分别定义两端区间的首和尾
            int begin1 = i, end1 = i + gap - 1;
            int begin2 = i + gap, end2 = i + 2 * gap - 1;
			
			// 对两端区间是否超出数组范围进行判断
            if (begin2 >= n)
            {
                break;
            }
            if (end2 >= n)
            {
                end2 = n - 1;
            }
			
			// 合并过程
            int k = i;

            while (begin1 <= end1 && begin2 <= end2)
            {
                if (a[begin1] < a[begin2])
                {
                    tmp[k ++ ] = a[begin1 ++ ];
                }
                else 
                {
                    tmp[k ++ ] = a[begin2 ++ ];
                }
            }
            while (begin1 <= end1) tmp[k ++ ] = a[begin1 ++ ];
            while (begin2 <= end2) tmp[k ++ ] = a[begin2 ++ ];
			
			// 每次一趟合并完,就拷贝进入元素组
            memcpy(a + i, tmp + i, sizeof(int) * (end2 - i + 1));
        }
        // 每次gap以二倍增长
        gap *= 2;
    }
}

3. 复杂度分析

参考文章:

  1. 时间复杂度。
  2. 空间复杂度。

计数排序

参考文章:传送门。

Counting Sort 计数排序,顾名思义,就是统计待排序数组元素的出现次数。其基本思想比较简单:

  1. 根据待排序元素的数值范围大小k(max-min+1),建立一个k大小的频数统计数组counts。对于counts数组而言,其索引范围 0 ~ k-1,正好可以对应到待排序元素的取值范围min~max上 。
  2. 统计待排序元素element的次数,并其存储到counts数组中,即counts[ elemet - min ] ++
  3. 待计数统计完成后遍历counts数组,根据次数值来输出原待排序元素值,此时即完成排序

动图过程展示

在这里插入图片描述

代码实现(C语言):

void CountSort(int* a, int n)
{
    assert(a);
	
	// 先假设最小和最大值为数组的第一个element
    int min = a[0];
    int max = a[0];
	
	// 找出最大最小值
    for (int i = 0; i < n; i ++ )
    {
        if (a[i] < min)
        {
            min = a[i];
        }
        if (a[i] > max)
        {
            max = a[i];
        }
    }

	// 求出待排序数据的数值范围大小(max - min + 1)
    int num = max - min + 1;
	
	// 开辟频数统计数组
    int* countA = (int*)calloc(num, sizeof(int));
    assert(countA);

	// 将每一个元素映射到计数数组并计数每一个元素出现的次数
    for (int i = 0; i < n; i ++ )
    {
        countA[a[i] - min] ++ ;
    }

	// 根据计数数组每个映射的下标对应的次数大小,往原数组输出元素
    int k = 0;
    for (int i = 0; i < num; i ++ )
    {
        while (countA[i] -- )
        {
        	// 反映射输出
            a[k ++ ] = i + min;
        }
    }
	
	// 最后释放开辟的空间
    free(countA);
}

缺陷:

  1. 计数排序只能适用待排序元素为整数的场景。
  2. 待排序元素的数值范围(极差)过大的情况下,计数排序会浪费大量空间,故一般不推荐使用计数排序。

📖复杂度分析

  • 更详细的分析搓此:传送门。
  1. 计数排序的时间复杂度
  • 计数排序的时间复杂度是O(n)(不过很多情况数据都是随机的,因此很难到达), 当然这个前提是数据范围不大的时候,即数据的最大值max与数据的最小值min之差不会比数组的总数据个数n大很多。
  1. 计数排序的稳定性
  • 当正向遍历的时候,计数排序不是稳定的排序算法,数据的位置会发生改变。
  • 当反向遍历的时候,计数排序就是稳定的排序算法,数据的位置不会改变。

在这里插入图片描述

  1. 计数排序的应用场景
  • 计数排序适合在数据范围不大的时候使用,如果数据范围最大值max与最小值min之差比数据总个数n大很多就不适合了。

排序算法复杂度及稳定性

如下图:

在这里插入图片描述

在这里插入图片描述

整体代码【随意取】

Sort.h

#include <stdio.h>
#include <assert.h>
#include <stdlib.h>
#include <stdbool.h>
#include <string.h>

// 打印数组
void PrintArray(int* a, int n);
// 插入排序
void InsertSort(int* a, int n);
// 希尔排序
void SherSort(int* a, int n);
// 选择排序
void SelectSort(int* a, int n);
// 堆排序
void HeapSort(int* a, int n);
// 冒泡排序
void BubbleSort(int* a, int n);
// 快速排序
void QuickSort(int* a, int l, int r);
// 霍尔法
void QuickSort1(int* a, int l, int r);
// 挖坑法
void QuickSort2(int* a, int l, int r);
// 前后指针法
void QuickSort3(int* a, int l, int r);
int QuickSort11(int* a, int l, int r);
int QuickSort22(int* a, int l, int r);
int QuickSort33(int* a, int l, int r);
// 快排非递归
void QuickSortNonR(int* a, int l, int r);
// 归并排序
void MergeSort(int* a, int n);
void MergeSortY(int* a, int l, int r, int* tmp);
// 归并排序非递归
void MergeSortNonR(int* a, int n);
void MergeSortNonRY(int* a, int l, int r, int* tmp);
// 计数排序
void CountSort(int* a, int n);

// 栈,用来辅助实现快速排序非递归
typedef int STDataType;
typedef struct Stack
{
	STDataType* a;
	int top;
	int capacity;
}Stack;
void StackInit(Stack* ps);
void STPush(Stack* ps, STDataType x);
void STPop(Stack* ps);
bool STEmpty(Stack* ps);
STDataType STTop(Stack* ps);
void STDestroy(Stack* ps);

sort.c

#include "Sort.h"

// 打印数组
void PrintArray(int* a, int n)
{
	assert(a);

	for (int i = 0; i < n; i++)
	{
		printf("%d ", a[i]);
	}
	printf("\n");
}

void swap(int* a, int* b)
{
	int tmp = *a;
	*a = *b;
	*b = tmp;
}

// 插入排序
void InsertSort(int* a, int n)
{
	assert(a);

	for (int i = 1; i < n; i++)
	{
		int end = i - 1;
		int tmp = a[i];
		while (end >= 0)
		{
			if (a[end] > tmp)
			{
				a[end + 1] = a[end];
				end--;
			}
			else break;
		}
		a[end + 1] = tmp;
	}
}

// 希尔排序
void SherSort(int* a, int n)
{
	assert(a);

	int gap = n;
	while (gap > 1)
	{
		gap /= 2;

		for (int i = 0; i < n - gap; i++)
		{
			int end = i;
			int tmp = a[i + gap];
			while (end >= 0)
			{
				if (a[end] > tmp)
				{
					a[end + gap] = a[end];
					end -= gap;
				}
				else break;
			}
			a[end + gap] = tmp;
		}
	}
}

// 选择排序
void SelectSort(int* a, int n)
{
	assert(a);

	int l = 0, r = n - 1;
	while (l < r)
	{
		int mini = l, maxi = l;

		for (int i = l; i <= r; i++)
		{
			if (a[i] < a[mini])
			{
				mini = i;
			}
			if (a[i] > a[maxi])
			{
				maxi = i;
			}
		}

		swap(&a[l], &a[mini]);
		if (maxi == l)
		{
			maxi = mini;
		}
		swap(&a[r], &a[maxi]);

		l++;
		r--;
	}
}

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;
	}
}
// 堆排序
void HeapSort(int* a, int n)
{
	assert(a);

	for (int i = (n - 2) / 2; i >= 0; i--)
	{
		adjustdown(a, n, i);
	}

	int end = n - 1;
	while (end > 0)
	{
		swap(&a[0], &a[end]);
		adjustdown(a, end, 0);
		end--;
	}
}

// 冒泡排序
void BubbleSort(int* a, int n)
{
	assert(a);

	for (int i = 0; i < n - 1; i++)
	{
		bool flag = true;
		for (int j = 1; j < n - i; j++)
		{
			if (a[j - 1] > a[j])
			{
				swap(&a[j - 1], &a[j]);
				flag = false;
			}
		}
		if (flag) break;
	}
}

int mid_num(int* a, int l, int r)
{
	int mid = (l + r) / 2;
	if (a[l] > a[r])
	{
		if (a[r] > a[mid]) return r;
		else if (a[l] < a[mid]) return l;
		else return mid;
	}
	else
	{
		if (a[l] > a[mid]) return l;
		else if (a[r] < a[mid]) return r;
		else return mid;
	}
}
// 快速排序
void QuickSort(int* a, int l, int r)
{
	if (l >= r)
	{
		return;
	}

	if (r - l + 1 < 10)
	{
		InsertSort(a + l, r - l + 1);
	}
	else
	{
		int mid = QuickSort22(a, l, r);

		QuickSort(a, l, mid - 1);
		QuickSort(a, mid + 1, r);
	}
}
// 霍尔法
void QuickSort1(int* a, int l, int r)
{
	if (l >= r) return;

	int mid = mid_num(a, l, r);
	swap(&a[mid], &a[l]);
	int keyi = l, begin = l, end = r;

	while (begin < end)
	{
		while (begin < end && a[end] >= a[keyi]) end--;
		while (begin < end && a[begin] <= a[keyi]) begin++;

		swap(&a[begin], &a[end]);
	}
	swap(&a[keyi], &a[begin]);

	QuickSort1(a, l, begin - 1);
	QuickSort1(a, begin + 1, r);
}
// 挖坑法
void QuickSort2(int* a, int l, int r)
{
	if (l >= r) return;

	int mid = mid_num(a, l, r);
	swap(&a[mid], &a[l]);
	int key = a[l], hole = l;
	int begin = l, end = r;

	while (begin < end)
	{
		while (begin < end && a[end] >= key) end--;
		a[hole] = a[end];
		hole = end;

		while (begin < end && a[begin] <= key) begin++;
		a[hole] = a[begin];
		hole = begin;
	}
	a[hole] = key;

	QuickSort2(a, l, hole - 1);
	QuickSort2(a, hole + 1, r);
}
// 前后指针法
void QuickSort3(int* a, int l, int r)
{
	if (l >= r) return;

	int mid = mid_num(a, l, r);
	swap(&a[mid], &a[l]);
	int keyi = l, prev = l, cur = l + 1;

	while (cur <= r)
	{
		if (a[cur] < a[keyi] && ++prev != cur)
			swap(&a[prev], &a[cur]);

		cur++;
	}
	swap(&a[prev], &a[keyi]);

	QuickSort3(a, l, prev - 1);
	QuickSort3(a, prev + 1, r);
}
int QuickSort11(int* a, int l, int r)
{
	int mid = mid_num(a, l, r);
	swap(&a[mid], &a[l]);
	int keyi = l, begin = l, end = r;

	while (begin < end)
	{
		while (begin < end && a[end] >= a[keyi]) end--;
		while (begin < end && a[begin] <= a[keyi]) begin++;

		swap(&a[begin], &a[end]);
	}
	swap(&a[keyi], &a[begin]);

	return begin;
}
int QuickSort22(int* a, int l, int r)
{
	int mid = mid_num(a, l, r);
	swap(&a[mid], &a[l]);
	int key = a[l], hole = l;
	int begin = l, end = r;

	while (begin < end)
	{
		while (begin < end && a[end] >= key) end--;
		a[hole] = a[end];
		hole = end;

		while (begin < end && a[begin] <= key) begin++;
		a[hole] = a[begin];
		hole = begin;
	}
	a[hole] = key;

	return hole;
}
int QuickSort33(int* a, int l, int r)
{
	int mid = mid_num(a, l, r);
	swap(&a[mid], &a[l]);
	int keyi = l, prev = l, cur = l + 1;

	while (cur <= r)
	{
		if (a[cur] < a[keyi] && ++prev != cur)
			swap(&a[prev], &a[cur]);

		cur++;
	}
	swap(&a[prev], &a[keyi]);

	return prev;
}
// 快排非递归
void QuickSortNonR(int* a, int l, int r)
{
	Stack st;
	StackInit(&st);

	STPush(&st, r);
	STPush(&st, l);

	while (!STEmpty(&st))
	{
		int begin = STTop(&st);
		STPop(&st);
		int end = STTop(&st);
		STPop(&st);

		int mid = QuickSort11(a, begin, end);

		if (mid + 1 < end)
		{
			STPush(&st, end);
			STPush(&st, mid + 1);
		}
		if (mid - 1 > begin)
		{
			STPush(&st, mid - 1);
			STPush(&st, begin);
		}
	}

	STDestroy(&st);
}

// 归并排序
void MergeSort(int* a, int n)
{
	assert(a);

	int* tmp = (int*)malloc(sizeof(int) * n);

	MergeSortY(a, 0, n - 1, tmp);
}
void MergeSortY(int* a, int l, int r, int* tmp)
{
	if (l >= r) return;

	int mid = (l + r) / 2;

	MergeSortY(a, l, mid, tmp);
	MergeSortY(a, mid + 1, r, tmp);

	int begin1 = l, begin2 = mid + 1;
	int k = l;

	while (begin1 <= mid && begin2 <= r)
	{
		if (a[begin1] < a[begin2])
		{
			tmp[k++] = a[begin1++];
		}
		else
		{
			tmp[k++] = a[begin2++];
		}
	}
	while (begin1 <= mid)
	{
		tmp[k++] = a[begin1++];
	}
	while (begin2 <= r)
	{
		tmp[k++] = a[begin2++];
	}

	memcpy(a + l, tmp + l, sizeof(int) * (r - l + 1));
}
// 归并排序非递归
void MergeSortNonR(int* a, int n)
{
	assert(a);

	int* tmp = (int*)malloc(sizeof(int) * n);

	MergeSortNonRY(a, 0, n - 1, tmp);
}
void MergeSortNonRY(int* a, int l, int r, int* tmp)
{
	int gap = 1;
	int n = r - l + 1;

	while (gap < n)
	{
		for (int i = 0; i < n; i += 2 * gap)
		{
			int begin1 = i, end1 = i + gap - 1;
			int begin2 = i + gap, end2 = i + 2 * gap - 1;

			if (begin2 >= n)
			{
				break;
			}
			if (end2 >= n)
			{
				end2 = n - 1;
			}

			int k = i;

			while (begin1 <= end1 && begin2 <= end2)
			{
				if (a[begin1] < a[begin2])
				{
					tmp[k++] = a[begin1++];
				}
				else
				{
					tmp[k++] = a[begin2++];
				}
			}
			while (begin1 <= end1)
			{
				tmp[k++] = a[begin1++];
			}
			while (begin2 <= end2)
			{
				tmp[k++] = a[begin2++];
			}

			memcpy(a + i, tmp + i, sizeof(int) * (end2 - i + 1));
		}
		gap *= 2;
	}
}

// 计数排序
void CountSort(int* a, int n)
{
	assert(a);

	int min = a[0], max = a[0];

	for (int i = 0; i < n; i++)
	{
		if (a[i] < min) min = a[i];
		if (a[i] > max) max = a[i];
	}

	int len = max - min + 1;
	int* countA = (int*)calloc(len, sizeof(int));

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

	int j = 0;
	for (int i = 0; i < len; i++)
	{
		while (countA[i] --)
		{
			a[j++] = i + min;
		}
	}

	free(countA);
}

stack.c

#include "Sort.h"

void StackInit(Stack* ps)
{
	assert(ps);

	ps->a = NULL;
	ps->capacity = ps->top = 0;
}

void STPush(Stack* ps, STDataType x)
{
	assert(ps);

	if (ps->top == ps->capacity)
	{
		int newcapacity = ps->capacity == 0 ? 4 : ps->capacity * 2;
		STDataType* tmp = (STDataType*)realloc(ps->a, sizeof(STDataType) * newcapacity);
		assert(tmp);
		ps->a = tmp;
		ps->capacity = newcapacity;
	}

	ps->a[ps->top++] = x;
}

void STPop(Stack* ps)
{
	assert(ps && !STEmpty(ps));

	ps->top--;
}

bool STEmpty(Stack* ps)
{
	assert(ps);

	return ps->top == 0;
}

int STTop(Stack* ps)
{
	assert(ps && !STEmpty(ps));

	return ps->a[ps->top - 1];
}

void STDestroy(Stack* ps)
{
	assert(ps);

	free(ps->a);
	ps->capacity = ps->top = 0;
}

test.c

#include "Sort.h"

void testInsertSort()
{
	int arr[] = { 20,19,18,17,16,15,14,13,12,11,10,9,8,7,6,5,4,3,2,1,0 };
	int n = sizeof(arr) / sizeof(arr[0]);
	InsertSort(arr, n);
	PrintArray(arr, n);
}

void testSherSort()
{
	int arr[] = { 20,19,18,17,16,15,14,13,12,11,10,9,8,7,6,5,4,3,2,1,0 };
	int n = sizeof(arr) / sizeof(arr[0]);
	SherSort(arr, n);
	PrintArray(arr, n);
}

void testSelectSort()
{
	int arr[] = { 20,19,18,17,16,15,14,13,12,11,10,9,8,7,6,5,4,3,2,1,0 };
	int n = sizeof(arr) / sizeof(arr[0]);
	SelectSort(arr, n);
	PrintArray(arr, n);
}

void testHeapSort()
{
	int arr[] = { 20,19,18,17,16,15,14,13,12,11,10,9,8,7,6,5,4,3,2,1,0 };
	int n = sizeof(arr) / sizeof(arr[0]);
	HeapSort(arr, n);
	PrintArray(arr, n);
}

void testBubbleSort()
{
	int arr[] = { 20,19,18,17,16,15,14,13,12,11,10,9,8,7,6,5,4,3,2,1,0 };
	int n = sizeof(arr) / sizeof(arr[0]);
	BubbleSort(arr, n);
	PrintArray(arr, n);
}

void testQuickSort()
{
	int arr[] = { 20,19,18,17,16,15,14,13,12,11,10,9,8,7,6,5,4,3,2,1,0 };
	int n = sizeof(arr) / sizeof(arr[0]);
	QuickSort(arr, 0, n - 1);
	PrintArray(arr, n);
}

void testQuickSort1()
{
	int arr[] = { 20,19,18,17,16,15,14,13,12,11,10,9,8,7,6,5,4,3,2,1,0 };
	int n = sizeof(arr) / sizeof(arr[0]);
	QuickSort1(arr, 0, n - 1);
	PrintArray(arr, n);
}

void testQuickSort2()
{
	int arr[] = { 20,19,18,17,16,15,14,13,12,11,10,9,8,7,6,5,4,3,2,1,0 };
	int n = sizeof(arr) / sizeof(arr[0]);
	QuickSort2(arr, 0, n - 1);
	PrintArray(arr, n);
}

void testQuickSort3()
{
	int arr[] = { 20,19,18,17,16,15,14,13,12,11,10,9,8,7,6,5,4,3,2,1,0 };
	int n = sizeof(arr) / sizeof(arr[0]);
	QuickSort3(arr, 0, n - 1);
	PrintArray(arr, n);
}

void testQuickNonR()
{
	int arr[] = { 20,19,18,17,16,15,14,13,12,11,10,9,8,7,6,5,4,3,2,1,0 };
	int n = sizeof(arr) / sizeof(arr[0]);
	QuickSortNonR(arr, 0, n - 1);
	PrintArray(arr, n);
}

void testMergeSort()
{
	int arr[] = { 20,19,18,17,16,15,14,13,12,11,10,9,8,7,6,5,4,3,2,1,0 };
	int n = sizeof(arr) / sizeof(arr[0]);
	MergeSort(arr, n);
	PrintArray(arr, n);
}

void testMergeSortNonR()
{
	int arr[] = { 20,19,18,17,16,15,14,13,12,11,23,877,144,90,33,77,10,9,8,7,6,5,4,3,2,1,0 };
	int n = sizeof(arr) / sizeof(arr[0]);
	MergeSortNonR(arr, n);
	PrintArray(arr, n);
}

void testCountSort()
{
	int arr[] = { 50,40,35,30,25,24,22,20,19,18,17,16,15,14,13,12,11,10,9,8,7,6,5,4,3,2,1,0 };
	int n = sizeof(arr) / sizeof(arr[0]);
	CountSort(arr, n);
	PrintArray(arr, n);
}

int main()
{
	testInsertSort();
	testSherSort();
	testSelectSort();
	testHeapSort();
	testBubbleSort();

	testQuickSort();
	testQuickSort1();
	testQuickSort2();
	testQuickSort3();
	testQuickNonR();

	testMergeSort();
	testMergeSortNonR();

	testCountSort();

	return 0;
}

小结:

参考文章:数据结构 | 十大排序超硬核八万字详解【附动图演示、算法复杂度性能分析】。

直接插入排序】:使用得比较广泛,虽属于O(N2)的排序算法,但是在数据接近有序时能体现出优势,需要掌握。

希尔排序】:看起来不起眼,但是性能不错,平均时间复杂度也能达到O(NlogN),至于O(N1.3)了解一下即可,主要还是记住它排序的这个过程。

选择排序】:如果能想得起其他排序算法,就不要用这个了,在各种场合测试下它都是最差劲的,无论如何都是在选数然后交换的过程。

堆排序】:性能不错,重点掌握的是【向下调整算法】以及如何建堆的过程,在数据量特大的情况下可以使用。

冒泡排序】:大学生最喜欢用排序算法,性能不高,只有在序列已然有序或者接近有序的情况下才能展现出优势。

快速排序】:综合性能最优,包含【左右指针法】【挖坑法】【前后指针法】【三路划分法】(本文没提供),学有余力都应掌握,重点在理解Hoare版本的思路。对于要参加校招的同学还要求掌握非递归的写法。

归并排序】:即使内排序也是排序,性能较高又可以达到稳定,常用于文件外排序。也有递归和非递归两种写法,最好是都要掌握。

计数排序】:唯一一个可能达到O(N)时间复杂度的排序算法,若是序列中没有很极端的数据出现,那用它还是不错的。

✔️写在最后

💝掌握八大排序,排序这一块就够用了,当然还有桶排序,基数排序,文件外排序之类的,大家可以查阅相关资料进行学习。
❤️‍🔥后续将会继续输出有关数据结构与算法的文章,你们的支持就是我写作的最大动力!

感谢阅读本小白的博客,错误的地方请严厉指出噢~

请添加图片描述

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/464867.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

每日一个小技巧:1招教你提取伴奏怎么做

伴奏是指在演唱或演奏时&#xff0c;用来衬托或补充主唱或乐器的音乐声音。而伴奏提取是一种技术&#xff0c;它可以帮助我们从歌曲中将人声和乐器分离出来。当我们听到一些喜欢的歌曲时&#xff0c;往往会被它的旋律深深吸引&#xff0c;想要将其作为自己的演唱曲目&#xff0…

国考只考一门?免试入学还好毕业的在职研究生专业有哪些

读同等学力申硕的同学想要拿学位证&#xff0c;那么首先要过的坎就是国考。修满学分和通过校考一般都不会很难&#xff0c;只要按时上课、根据院校安排的课程复习即可。而国考是全国统一命题、考试&#xff0c;大部分专业要考2门&#xff0c;对于有的同学来说&#xff0c;备考压…

从零开始学Python第13课:常用数据结构之字典

迄今为止&#xff0c;我们已经为大家介绍了Python中的三种容器型数据类型&#xff0c;但是这些数据类型仍然不足以帮助我们解决所有的问题。例如&#xff0c;我们需要一个变量来保存一个人的信息&#xff0c;其中包含了这个人的姓名、年龄、身高、体重、家庭住址、本人手机号、…

Springboot 自定义缓存配置 CacheManager 及redis集成

目录 前言 集成 maven依赖 CacheManagerConfig配置 redis配置 使用 Springboot 集成使用缓存 Cacheable CacheEvict 前言 现有项目中经常遇到的缓存集成问题&#xff0c;Springboot提供了统一的接口抽象与缓存管理器&#xff0c;可集成多种缓存类型&#xff0c;如 Co…

Java阶段二Day10

Java阶段二Day10 文章目录 Java阶段二Day10DQLGROUP BY 分组按单字段分组例 按多字段分组例 按照聚合函数的结果排序例 HAVING子句问题错误原因HAVING子句的应用HAVING和WHERE的区别例 子查询 (SubQuery)概念应用场景子查询分类在DQL中使用子查询单行单列子查询例 多行单列子查…

hook函数

什么是hook函数 在计算机编程中&#xff0c;hook函数是指在特定的事件发生时被调用的函数&#xff0c;用于在事件发生前或后进行一些特定的操作。通常&#xff0c;hook函数作为回调函数被注册到事件处理器中&#xff0c;当事件发生时&#xff0c;事件处理器会自动调用相应的ho…

QtDAY 2

代码&#xff1a; 头文件&#xff1a; #ifndef WIDGET_H #define WIDGET_H#include <QWidget> #include <QDebug> #include <QString> #include <QTextToSpeech>QT_BEGIN_NAMESPACE namespace Ui { class Widget; } QT_END_NAMESPACEclass Widget : …

【Hello Network】网络编程套接字(二)

作者&#xff1a;小萌新 专栏&#xff1a;网络 作者简介&#xff1a;大二学生 希望能和大家一起进步 本篇博客简介&#xff1a;简单介绍网络的基础概念 网络编程套接字&#xff08;二&#xff09; 简单的TCP网络程序服务端创建套接字服务端绑定服务器监听服务端获取连接服务端处…

Qt Quick Qml-Rectangle案例

Qt Quick - Qml 1.Rectangle //组件 IShadow.qml import QtQuick import QtQuick.ControlsItem {id:rootanchors.fill: parentanchors.margins: -4property color color: "#999999"property int radius: 4Rectangle{width: root.widthheight: root.heightanchors.ce…

原型图都可以用什么软件做?分享这9款给你

设计师在进行原型设计师时&#xff0c;会使用原型图软件&#xff0c;从产生想法到向开发人员提交项目。无论是构建基本线框还是功能齐全的原型&#xff0c;原型图软件都可以为你节省大量的时间和精力。 如果你是这个领域的新手或者想更新你的原型图软件包&#xff0c;请快速看…

iOS App的生命周期

App的生命周期 App从启动到退出的过程中&#xff0c;iOS应用程序不断从系统接收各种事件&#xff0c;如&#xff1a;用户点击了屏幕、用户点击了Home键&#xff0c;并对这些事件进行响应。接受事件是UIApplication对象的工作&#xff0c;但是&#xff0c;响应事件就需要由程序…

Dubbo详解

一、基础知识 1、 RPC RPC【Remote Procedure Call】是指远程过程调用&#xff0c;是一种进程间通信方式&#xff0c;他是一种技术的思想&#xff0c;而不是规范。它允许程序调用另一个地址空间(通常是共享网络的另一台机器上)的过程或函数&#xff0c; 而不用程序员显式编码…

【unity细节】—(Can‘t add script)脚本文件无法拖拽到对象的问题

&#x1f468;‍&#x1f4bb;个人主页&#xff1a;元宇宙-秩沅 hallo 欢迎 点赞&#x1f44d; 收藏⭐ 留言&#x1f4dd; 加关注✅! 本文由 秩沅 原创 收录于专栏&#xff1a;unity细节和bug ⭐关于脚本文件无法拖拽到对象的问题⭐ 文章目录 ⭐关于脚本文件无法拖拽到对象的…

分治算法(Divide and Conquer)

本文已收录于专栏 《算法合集》 一、简单释义 1、分治算法 字面上的解释是“分而治之”&#xff0c;就是把一个复杂的问题拆分成两个或更多的相同或相似的子问题&#xff0c;再把子问题分成更小的子问题……直到最后子问题可以简单的直接求解&#xff0c;原问题的解即子问题的…

什么是FAQ页面?如何设计一个优秀的FAQ页面?

随着互联网技术的迅猛发展&#xff0c;越来越多的企业开始将在线客户支持服务作为一种重要的业务方式&#xff0c;以提供更好的服务体验。然而&#xff0c;在线客户支持服务除了提供实时的沟通方式外&#xff0c;一个功能齐全、易于使用的FAQ页面也是必不可少的&#xff0c;这可…

Go 语言进阶与依赖管理

作者&#xff1a;非妃是公主 专栏&#xff1a;《Golang》 博客主页&#xff1a;https://blog.csdn.net/myf_666 个性签&#xff1a;顺境不惰&#xff0c;逆境不馁&#xff0c;以心制境&#xff0c;万事可成。——曾国藩 文章目录 一、语言进阶1. 并发和并行2. 协程(Goroutine…

软件or硬件?硬件的前途到底在哪里?

一、硬件明明比软件更难&#xff0c;国内的硬件技术也不如软件&#xff0c;为什么硬件工程师待遇还不如软件&#xff1f; 1、不需要太高层次的硬件设计&#xff0c;比如大部分小家电企业&#xff0c;简单的电子产品企业&#xff0c;单片机简单外围设计就够了&#xff0c;单片机…

java网络编程——NIO架构

目录 1.什么是NIO 2.NIO结构 3.基于NIO下的聊天系统实现 4.Netty 1.什么是NIO NIO&#xff1a;java non-blocking IO&#xff0c;同步非阻塞IO。 BIO是阻塞IO&#xff0c;即每一个事件都需要分配一个进程给他&#xff0c;如果客户端没有连接上&#xff0c;则一直阻塞等待…

Java基础--->并发部分(1)

文章目录 线程基本概念线程的创建方式线程调度-------常用的方法线程的生命周期和状态并发编程的根本原因Java内存模型(JMM)多线程核心的根本问题volatile关键字保障原子性synchronized和ReentrantLock的区别 线程基本概念 ​ 进程是程序的一次执行过程&#xff0c;是系统运行程…

【Linux】1.4 基本权限

文章目录 一、shell 命令以及运行原理二、Linux 权限的概念三、Linux 权限管理01.文件类型和访问权限&#xff08;事物属性&#xff09;02.文件访问的分类&#xff08;人&#xff09;①用户分类②角色划分 03.文件权限值的表现方法04.文件访问权限的相关设置方法&#xff08;a.…