常见七大排序(汇总)

目录

引言

交换函数

直接插入排序

思想

时间复杂度

希尔排序

思想

时间复杂度

选择排序

思想

时间复杂度

堆排序

思想

时间复杂度

冒泡排序

思想

时间复杂度

快速排序(递归)

霍尔法

前后指针法

三数取中 & 随机值法

第一种是随机值法

第二种是三数取中

快速排序(非递归)

归并排序(递归)

思想

时间复杂度

归并排序(非递归)

测试验证(100万个数测试看强度)

总代码(gitee)

结语


引言

本篇文章会对常见的七大排序进行讲解:

  • 直接插入排序   InsertSort
  • 希尔排序   ShellSort
  • 选择排序   SelectSort
  • 堆排序   HeapSort
  • 冒泡排序   BubbleSort
  • 快速排序   QuickSort
  • 归并排序   MergeSort

其中,快排归并排序除了讲解递归版本之外,还会讲解一下非递归的版本

交换函数

这是一个小的函数,只是后面会频繁用到,这里就简单做一下逻辑的分离

Swap函数代码如下:

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

直接插入排序

思想

直接插入排序就好像我们平时打扑克牌,每次拿到牌的时候都会洗牌

我们每张牌都会用到,我们抽取出其中一张牌,然后将其插入到对应的位置上,当我们把每一张牌都如上操作插入一遍之时,我们就排序好了

如下图:

如上我们会看到:在换到 2 的时候最为特殊,他在交换了一次之后并没有停下,而是不断让前面的数跟他比较,在前面没有比他大的数字的时候,才插入下来

而这个逻辑的实现也较为容易,我们只需要创建一个变量(end),这个变量代表着这是数组中的第几个数,再将该变量的下一个数给存起来,记作 int tmp = a [ end + 1 ]

如果 end 变量的下一个(是跟tmp比较)比他小(假设排升序,左小右大),那么我们就让 end 变量代表的数将其覆盖,随后 end--。设置一个循环,当 end 减小到 <0 时,就代表 tmp 存的数是整个数组里面最小的一个

这时我们再让 tmp 将 end + 1 位置的数给覆盖

注:因为不是每一个数都是最小的,我们用 end 的数 将 end + 1 位置的数给覆盖了之后,原本 end 位置的数就空出来了,但是我们的 end 是先减减,然后才判断的,所以 tmp 覆盖的位置应该是 end + 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;
	}
}

因为 end + 1 位置的数据会被覆盖,所以需要提前存一份

我们 while 循环的条件是 end >= 0,如果 end + 1 位置的数大于 tmp 代表的数,就覆盖,并end--

时间复杂度

这个排序的时间复杂度也比较好算,每个数都要由第一个 for 循环遍历一遍

遍历每一个数时还要将每个数再遍历一遍

所以该数的时间复杂度为 O(N^2)

希尔排序

思想

我们学完了插入排序之后,我们会发现每个数都是拿出来,找位置,交换,就像是打扑克牌刚拿到牌时洗牌一样,拿一张,挑合适的位置,插入,如此反复

如上图,每一次交换都是两张两张之间进行的

但是,大佬 希尔 发现了不对劲

如果需要排序的数组接近有序的话,那么直接插入排序的效率将会非常高

所以大佬就想着,能不能在直接插入排序的基础上进行改进,在两张两张之间排序之前,先将其变成接近有序的

这就是——预排序

不一定非要两个两个,让其跨度大一点呢?

多来几组呢?

让这些个组先粗略地排一遍,那么不就接近有序了!

但是只是跨一个,或是两个,抑或是三个之间交换,这样子排似乎不够

最后希尔大佬发现,先让间隔 gap 等于 排序数组总数的一半,在进行完一次预排序之后,再让 gap 除等 2,一直循环,直到 gap<1 ,这时候的效率是最高的,时间复杂度接近 O(N*logN) 

这是非常快的!

拿N^2 与 N*logN对比一下

假设有100万个数据

2^20 为1’048‘576,接近100万,且当作100万来算

N^2  要排序  100万 * 100万次

N*logN 大概要排序   100万*20次

快了不是一点半点!!!

但是后来,有人发现,我们不需要分那么多组,太麻烦了

我们可以只用一组,让这一组拍完之后不断向后移动即可,如下:

我们先将其中一次的逻辑实现一遍:

先假设 gap = 3

那么我们就是将每个间隔对应位置的数给排好

假如我们现在要排的是如下图所示的 3

那么我们就需要先将 3 和 1 进行对比

如果 3 大于 1,那么我们就用 3 将 1 给覆盖,由于要被覆盖,所以我们需要先将 1 位置的数据先存起来,记作 tmp

由于数据是从前往后排的,我们只是截取了 3 这个点作为例子,因此,3 前面的数据肯定是已经排序好了的

接着,我们让 1 和 2 进行比较,2 > 1,所以 2 将原本 3 的位置给覆盖

最后当 1 无法向后再走的时候,1 再将原本 2 的位置给覆盖掉

这其实和我们上文提到的 直接插入排序 的思路是一摸一样的,如果有疑惑的,可以将上文插入排序的逻辑理一理

单次预排序代码如下:

int gap = 3;

int end = i;//假设i位置就代表我们说的3的位置
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;

在加上希尔大佬的想法:先设gap为总数的一半,再不断将其除等 2

当 gap 为 1 的时候,其实就是 直接插入排序

综上,总代码如下:

//希尔排序
void ShellSort(int* a, int n)
{
	int gap = n;
	while (gap > 1)
	{
		gap = gap / 3 + 1;
		for (int i = 0; i < n - gap; i++)
		{
			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;
		}
	}
}

时间复杂度

具体时间复杂度的算法涉及到数学中的概率等等知识点,讲明白的时候已经是数学问题了,所以各位可以直接记结论

接近 O(N*logN)

选择排序

思想

其实选择排序很好理解,其主要思想就是:

定义出变量begin和end,begin初始化为0,end为n-1(共有n个元素)

不断地遍历数组,每遍历一次,就找出最小值和最大值,再将这两个分别放到begin和end位置上

最后begin++,end--

每一次都找到begin与end范围内的最大与最小

代码(有坑的,待会儿填)如下:

int begin = 0, end = n - 1;
while (begin < end)
{
	int mini = begin, maxi = begin;
	for (int i = begin + 1; i <= end; i++)
	{
		if (a[i] < a[mini])mini = i;
		if (a[i] > a[maxi])maxi = i;
	}
	Swap(&a[begin], &a[mini]);
	Swap(&a[end], &a[maxi]);
	begin++, end--;
}

如果这时候你去运行代码,你会发现结果是错误的,而且出错的是最中间的两个元素

我们用图解的方式来了解一下为什么:

我们会发现,到了最后一步的时候,begin先和min换一次,但是end和max又换回来了

也就是说,本来是换好了的,但是换了两遍又换回来了

所以我们可以加个判断:当begin等于max的时候,我们就将max的值赋值为min

最后的结果就是end自己和自己换了一次,结果就正确了

完整代码如下:

//选择排序
void SelectSort(int* a, int n)
{
	int begin = 0, end = n - 1;
	while (begin < end)
	{
		int mini = begin, maxi = begin;
		for (int i = begin + 1; 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)maxi = mini;
		Swap(&a[end], &a[maxi]);
		begin++, end--;
	}
}

时间复杂度

不难看出,每个数都遍历一遍的同时,都会再找一遍最大值和最小值

所以时间复杂度是  O(N^2)

堆排序

思想

堆排序的核心思想就是建堆,升序建大堆,降序建小堆

我们先来说一下堆已经建好了的情况下,我们该怎么排序:(这里假设我们排的是升序)

这是一个已经建好了的大堆

我们先将最末尾的数字与最顶上的数字进行交换,由于顶上的数字是最大的,所以交换完之后,就代表这个数字已经被排好了

你可以理解成是从后面开始往前排

交换完了之后,我们让控制总数的那个变量减减一下,那么总数就少了一个,代表最后一个已经排序好的数字就不会再受到干扰了

最后对这个大堆进行向下调整,再一次选出最大的那个数字放在堆顶,交换,减减,向下调整,如此往复

图解如下:

建堆和排序代码如下:

void HeapSort(int* a, int n)
{
	//建大&小堆
	for (int i = (n - 2) / 2; i >= 0; i--)
		AdjustDown(a, n, i);

	//首尾交换 + 排序
	int end = n - 1;
	while (end)
	{
		Swap(&a[0], &a[end]);
		AdjustDown(a, end--, 0);//向下调整
	}
}

接着我们再来讲一讲向下调整法:

向下调整法本质上,设当前节点为父节点,先在两个子节点里面找到大的那一个(假设此处是建大堆)

对比父节点与大的那个子节点,父节点大就终止循环,子节点大就交换两节点,以子节点为新的父节点,往下n*2+1个位置的节点作为新的字节点

当子节点 > 数组总个数 时,退出循环

补充一个点,我们可以使用假设法找到两个子节点中大的那个节点

先假设其中一个是大的那个

再 if 判断一下:如果该节点没有另一个大,就换成另一个为子节点,相反则不做处理

向下调整代码如下:

//堆排序(升序建大堆√,降序建小堆)
void AdjustDown(int* a, int n, int parent)
{
	int child = parent * 2 + 1;

	while (child < n)
	{
		//假设法找大孩子
		if (child + 1 && a[child] < a[child + 1])child++;

		if (a[child] > a[parent])
		{
			Swap(&a[child], &a[parent]);
			parent = child;
			child = child * 2 + 1;
		}
		else
			break;
	}
}

时间复杂度

由于堆排序是二叉树结构,树的高度是logN

我们可以推理一下公式:

树的总数:N = 2^0 + 2^1 +......+ 2^(n-1)

树的总高度为 n-1

N = 2^0 + 2^1 +......+ 2^(n-1)

2N = 2^1 + 2^2 +......+ 2^n

上述两式相减:N = 2^n - 2^0 = 2^n - 1

所以树的高度大致为 logN

所以就是遍历了 N*logN 次

时间复杂度就是   O(N*logN)

冒泡排序

思想

冒泡排序本质上就是一个 for 循环嵌套

外层的for循环代表的是一共要进行多少趟冒泡

嵌套在里面的那一层代表的是每一趟冒泡的具体过程

每一趟冒泡就是(假设要升序):如果后一个比前一个小,那么就交换

第一趟冒泡结束之后,最大的那个数字就被排好了;第二趟冒泡之后,第二大的就被排好了,如此往复

代码如下:

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

时间复杂度

O(N^2)

快速排序(递归)

霍尔法

霍尔法的本质思想就是:先将最左边的数定义为key

然后两个变量begin和end,分别从左边与右边出发

为了保证最后停下位置的数一定比key要小,所以需要end先从右边走

如果遇到比 key 小的数,那么 end 就停下来

接着 begin 走,如果遇到的数比 key 要大,也停下来

最后交换 begin 和 end 

接着,一个从右边出发,一个从左边出发,不断反复,直到 begin >= end,终止循环

当循环终止的时候,就代表着 begin 和 end 指向一个地方

这样子做完之后,最后的结果就是key位置的数排好了,key左边的数都比key代表的数要小,右边的都比key代表的数要大

图解如下:

当我们将上图中的5给排好了之后,我们可以使用递归的方法,左边和右边都使用上述同样的方法,我们需要传的是一个范围

原函数开始是传 0 和 7(一共8个数)

排序完了之后,我们就分别传 0     key-1,key+1     7

每一个排序完了之后,我们都按这种方法不断递归下去

图解如下:

既然是递归,那么就肯定需要返回条件,如若不然则会无限递归下去

由上图可得,返回条件有两种情况:

此时 2 是key

  1. key 的左边还有数,所以传过去的范围就是一样的,即 begin == key-1
  2. key 的右边没有数,所以传过去的范围是 key-1 > end

综上,我们可以知道返回条件是:if(left >= right)    return;

综上,代码如下:

//快排
void QuickSort(int* a, int left,int right)
{
	if (left >= right)
		return;

	int begin = left, end = right;
	int key = left;

	while (left < right)
	{
		//right先走
		while (right > left && a[right] >= a[key])right--;
        //left走
		while (right > left && a[left] <= a[key])left++;

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

	QuickSort(a, begin, key - 1);
	QuickSort(a, key + 1, end);
}

前后指针法

实际上,这种方法比霍尔法更为方便,只不过毕竟是霍尔大佬整出的快排,学快排肯定要学一下霍尔法

而前后指针的逻辑也很简单

我们有一前一后两个指针,我们最后的目的是让两个指针之间的数为大于 key 的数(升序),而前指针左边的数都是小于 key 的数

最后将 key 与前指针交换,就达到了和霍尔法一样的效果

其主要思想如下:

前面的指针为 prev,后面的指针为 cur,记最左边的那个数为 key

如果 cur 指向的值 < key 指向的值(升序),那么我们就将 prev++,接着交换 prev 和 cur,最后 cur++,如果 cur 指向的值 > key 指向的值,那么直接 ++cur 即可

因为我们需要达到的结果是:prev 和 cur 之间包含的是 > key 的值,而由于最后的步骤是将 prev 和 key 进行一个交换,之后再进行递归,所以 prev 的值是应该小于 key 代表的值的

这时我们需要分几种情况的分析:

  1. prev 的下一个就是 cur(cur 代表的值 < key 代表的值):由于 prev 是需要小于 key 的,所以如果 prev 的下一个就是 cur 的话,那么就意味着 prev 和 cur 之间可以不包括这个数据,按照如上的做法,我们先++prev,然后交换 prev 和 cur(自己和自己换),再++cur,结果是对的
  2. prev 与 cur 之间有值(cur 代表的值 > key 代表的值):由于 prev 和 cur 之间包含的值是大于 key 的,所以直接 cur++ 即可
  3. prev 与 cur 之间有值(cur 代表的值 < key 代表的值):我们先++prev,由于 prev 和 cur 之间的值都是大于 key 的值,所以++prev 之后的就是大于 key 的值,由于现在 cur 指向的值 < key,所以我们就将 prev 和 cur 交换一下,这样就达成了我们的目的

  图解如下:

上述逻辑的代码如下(未改进前的版本):

int key = left;
int prev = left;
int cur = right;

while(cur <= right)
{
    if(a[cur] < a[key])
    {
        ++prev;
        Swap(&a[prev], &a[cur]);
        ++cur;
    }
    else
    {
        ++cur;
    }
    ++cur;
}
a[key] = a[prev];
key = prev;

但是有人觉得这样子写太啰嗦了,反正 cur 都是要++的,干脆合在一起算了

所以就有了下面的代码:

int prev = left, cur = prev + 1;

int key = left;
while (cur <= right)
{
	if (a[cur] < a[key] && ++prev != cur)
		Swap(&a[prev], &a[cur]);
	++cur;
}
Swap(&a[prev], &a[key]);
key = prev;

再加上递归和返回条件

总代码如下:

void QuickSort(int* a, int left, int right)
{
	if (left < right)
		return;
	int prev = left, cur = prev + 1;

	int key = left;
	while (cur <= right)
	{
		if (a[cur] < a[key] && ++prev != cur)
			Swap(&a[prev], &a[cur]);
		++cur;
	}
	Swap(&a[prev], &a[key]);
	key = prev;

	QuickSort(a, left, key - 1);
	QuickSort(a, key + 1, right);
}

三数取中 & 随机值法

快排好是好,但是他也分情况

由于快排每一次都是排序好一个位置的数,然后再将左边和右边分为两个区间,再不断递归下去的

快排最好的情况就是:

每次都是在最中间的位置分出左右区间,随后递归,这时时间复杂度为O(N*logN)

快排最坏的情况就是:

每次都是在最边边的位置选区间,也就是接近有序的情况,这时时间复杂度接近  O(N^2)

所以如果面临的要排序的数组接近有序的话,我们该如何处理这种情况呢?

第一种是随机值法

随机值法就是:从数组中随机选一个数,将这个数与 key 位置的数字进行交换,以这个数作为新的 key

代码如下:

int randi = rand() % (right - left);
randi += left;
Swap(&a[left], &a[randi]);

由于我们写的是递归,我们选的数字可能并不是从 0 位置开始的,所以 randi 要 += left 保证 randi 在的位置是待排序那一段数组的起始位置

第二种是三数取中

有人觉得按随机值法不太可靠,不太稳定,不是很喜欢,所以就发明了这种三数取中的方法

三数取中法的本质就是:

将待排序数组的头、尾、中间位置的数进行比较,将排在中间位置的值返回

如果待排序数组接近有序的话,我们用一手三数取中,不就可以尽可能地保证,快排在中间位置分左右区间再递归吗?这多有效率啊

具体逻辑就是比较,这里不做过多解释

三数取中代码如下:

int GetMid(int* a, int left, int right)
{
	int mid = (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
	{
		if (a[mid] > a[right])
			return mid;
		else if (a[left] < a[right])
			return left;
		else
			return right;
	}
}

快速排序(非递归)

快速排序的非递归版本,主体上和递归版本没有什么区别,逻辑还是那个逻辑,还是前后指针法、霍尔法完成选数字分区域

唯一不同的就在于:

以前我们对分完区域后的数组段是递归处理

现在我们是用   栈  +  循环  的方式来模拟实现递归的

我们将区域压入栈内,比如先压 0  9  ,后面可能会压  0   5,7   9  ,不断重复下去

我们每排完一次序,我们就从 栈 里面取数据,再用取出的这两个数据进行压栈

你会发现,我们每从栈里面取一次数据,就相当于进行了一次递归的过程

而当栈里面没有数据了的时候,也就代表我们已经排序好了

由于C语言不像C++有 stl库可以直接使用,所以我们只能现场手撕一个

为了不耽误大家时间,我在这里就简单介绍一下我会使用到的关于栈的函数名:

  • STInit  栈的初始化
  • STPush  压栈
  • STPop  出栈
  • STEmpty  判断栈是否为空
  • STDestroy  销毁

由于主题逻辑和递归是一摸一样的,这里就直接放代码了

代码如下:

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 key = begin;
		int prev = begin;
		int cur = begin + 1;

		while (cur <= end)
		{
			if (a[cur] < a[key] && ++prev != cur)
				Swap(&a[prev], &a[cur]);

			++cur;
		}

		Swap(&a[key], &a[prev]);
		key = prev;

		if (key + 1 < end)
		{
			STPush(&st, end);
			STPush(&st, key + 1);
		}

		if (begin < key-1)
		{
			STPush(&st, key-1);
			STPush(&st, begin);
		}
	}
	STDestroy(&st);
}

归并排序(递归)

思想

我们先通过一张图片来看一看,归并排序是一个什么东西:

你可以理解成,先将待排序数组以二叉树的形式分好,再从底层开始一个一个排

当然你也可以说,这是  后序

同时我们还需要另外 malloc 一块空间,因为我们是通过选数的方式,将小的数(升序)先放到数组里面,然后再 memcpy 回原数组,中间需要一个不被销毁的中转站

我们将 malloc 出来的这块空间且指向这块空间的指针称为 tmp

我们将这一段待排序数组分成以 mid 为中心的两个区域,begin->mid     mid+1->end

随后我们将这两段进行排序,设左边的那段为左数组,右边那段为右数组

我们这样判断:左数组的第一个和右数组的第一个比,谁小就先将其放在 tmp 数组里面

再不断地比较下去,直到有一边没有数字了,循环终止

接着我们还需要各自判断一遍:左数组还有没有数,如果有,就证明左数组里剩下的数比右数组的都要大,就将其依次放入 tmp 中

右数组同理

图解如下:

如果 begin 和 end 相等的话,就代表现在递归到只剩一个数了,这时就返回

所以我们的递归判断条件是:if(end == begin)return;

代码如下:

void _MergeSort(int* a, int begin, int end, int* tmp)
{
	if (begin == end)
		return;

	int mid = (begin + end) / 2;
	int begin1 = begin, end1 = mid;
	int begin2 = mid + 1, end2 = end;

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

	int i = begin;

	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 + 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;
	}
	_MergeSort(a, 0, n - 1, tmp);

	free(tmp);
	tmp = NULL;
}

时间复杂度

由于这是一个树结构,树的高度为 logN,待排序数组有 N 个数

所以时间复杂度为   O(N*logN)

归并排序(非递归)

由于递归给 ban 掉了,所以我们现在首要的问题就是:如何先两两排,再四四排等等,就像递归一样

我们核心逻辑不变,还是两个数组段之间进行比较,还是需要 malloc 出一块 tmp 数组作为中转站

这时我们可以引入一个 gap

gap 代表的是当前是多少个数之间进行比较,图解如下:

但是由于并不是每一个数组都是偶数,而且即使是偶数个,之后也会出现越界的情况,所以我们需要分开讨论

我们对区域的划分创建了四个变量:

  • begin1
  • end1
  • begin2
  • end2

其中,除了 begin1 不会出现越界情况之外,其他的每一个变量都会出现越界的情况

如果是 end1 或 begin2 越界了的话,我们就不用再排序下去了

  • end1:假设有数组段 0  3,4  8,9  10。这时我们就可以让 0  3,4  8 这两个数组之间排,排完了之后再让 0  8,9  10 两个数组之间排,所以 end1 越界无需接着排序下去
  • begin2:假设有数组段 0  3,4  8,9  13,begin2 越界就代表着在场的数组段为奇数段,有一段没有对应的数组两两比较,所以 begin2 越界也无需接着排序下去
  • end2:如果是 end2 越界,而其他变量没有越界的话,那么就意味着场上的数组为奇数个,只是最后两个数组可能一个有 6 个数,另一个有 4 个数,不一样而已,所以 end2 越界我们还是要接着排序下去的,只是我们需要改一下 end2 的值,如果共有 n 个数的话,我们就需要将 end2 赋值为 n-1(因为 end2 代表的是一个位置,end2 越界时,数组最后一个数的位置就是最后一个数组的边界)

综上:我们要将这几个变量控制好,排序才不会出错,控制代码如下:

if (begin2 >= n || end1 >= n)
	break;
if (end2 >= n)
	end2 = n - 1;

而我们第一次排序时,每组都为 gap=1 个数,而后不断将其乘等2

而循环结束的条件就是:当 gap < 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)
	{
		for (int j = 0; j < n; j += 2 * gap)
		{
			int begin1 = j, end1 = begin1 + gap - 1;
			int begin2 = begin1 + gap, end2 = begin2 + gap - 1;

			if (begin2 >= n || end1 >= n)
				break;
			if (end2 >= n)
				end2 = n - 1;

			int i = j;

			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;
	}

	free(tmp);
	tmp = NULL;
}

测试验证(100万个数测试看强度)

总代码(gitee)

今天用到的全部代码都在下方链接中(除了快排的非递归)

Sort-All-blog

结语

隔了很长一段时间没有写博客(中间的时间都拿去学线代了doge)

如果觉得这篇博客能给你带来帮助的话,希望各位能多多支持呀!!!

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

C++ 核心编程 - 内存分区模型

文章目录 1.1 程序运行前1.2 程序运行后1.3 new 操作符 C 程序在执行时&#xff0c;将内存大致划分为 4个区域&#xff1a; 代码区&#xff1a;存放函数体的二进制代码&#xff0c;由操作系统进行管理&#xff1b;全局区&#xff1a;存放全局变量和静态变量以及常量&#xff1…

【CSS】CSS实现元素逐渐消失(实现元素透明逐渐消失/模糊)

mask-image: linear-gradient(to bottom, rgba(0, 0, 0, 0) 0%, rgba(0, 0, 0, 1) 10%);mask-image 属性用于定义一个遮罩&#xff0c;它可以隐藏元素的一部分或全部内容。在这个示例中&#xff0c;我们使用 mask-image 属性来定义一个线性渐变的遮罩&#xff0c;使得列表项的内…

nginx配置ip_hash负载均衡策略

一、nginx配置ip_hash负载均衡策略 nginx默认的负载均衡策略为轮询&#xff0c;某些场景需要使用ip_hash负载策略&#xff0c;即&#xff1a;同一个ip地址&#xff0c;永远访问nginx后面同一台tomcat。配置示例如下&#xff0c;主要是设置ip_hash&#xff1a; upstream www.ab…

机器视觉系统-工业光源什么是高角度光,以及发光角度得分类

光路描述&#xff1a;光线与水平面角度>45称为高角度光。 效果分析&#xff1a;高角度照射&#xff0c;光线经 被测物表面平整部分反射后进入镜头&#xff0c;图像效果表现为灰度值较高&#xff1b;不平整部分反射光进入不了镜头&#xff0c;图像效果表现为灰度值较低。 主要…

【Vue】自定义事件实现组件之间的通信(案例讲解)

一、前言 这是部分哔哩哔哩上跟着一个博主【遇见狂神说】学习的&#xff0c;当然自己也是才开始学习的vue&#xff0c;在学到这个Vue的自定义事件的时候&#xff0c;虽然知识点很绕&#xff0c;但是在理解后又觉得很意思&#xff0c;觉得Vue真的很强大。这里博主将自己学习到的…

2、选择什么样的机器人本体

如果说世界是物质的&#xff0c;那么应该先制造出机器人的本体&#xff0c;再让她产生灵魂。如果是精神的呢&#xff0c;世界是无中生有的呢&#xff0c;那就先在仿真中研究算法吧。 而我比较崇尚初中哲学的一句话&#xff0c;世界是物质的&#xff0c;物质是运动的&am…

[已测试]TVBox二次开发影视系统酷点1.4.4反编译版本

支持p2p, 磁力等播放 支持多仓切换vip线路 自动换源开关 启动时直接进入直播 原始轮播图和幻灯片切换 加大防抓包的可能性 支持安卓4.x版本 源码下载&#xff1a;https://download.csdn.net/download/m0_66047725/89084105 更多资源下载&#xff1a;关注我。

科技改变视听4K 120HZ高刷新率的投影、电视、电影终有用武之地

早在1888年&#xff0c;法国生理学家埃蒂安朱尔马莱就发明了一套盒式摄像机&#xff0c;能以120帧/s的速度在一条纸膜上曝光照片&#xff0c;但是当时没有相匹配的放映设备。而马莱的另一套拍摄设备是60帧/s的规格&#xff0c;并且图像质量非常好。 受此启发&#xff0c;雷诺的…

CobaltStrike使用插件实现免杀

免责声明&#xff1a;本工具仅供安全研究和教学目的使用&#xff0c;用户须自行承担因使用该工具而引起的一切法律及相关责任。作者概不对任何法律责任承担责任&#xff0c;且保留随时中止、修改或终止本工具的权利。使用者应当遵循当地法律法规&#xff0c;并理解并同意本声明…

编译一个基于debian/ubuntu,centos,arhlinux第三方系统的问题解答

如果是开机卡boot注意看前面几行会有错误提示&#xff0c;一般会比较好找&#xff0c;下面是过了kernel内核加载后出现的问题 目录 上一篇文章 第一个问题 错误原因 解决办法 第二个问题 注意 第三个问题 上一篇文章 编译一个基于debian/ubuntu,centos,arhlinux第三方系…

【Linux网络】FTP服务

目录 一、FTP简介 1.FTP-文件传输协议 2.FTP的两种模式 二、安装配置FTP 1.安装环境 2.对文件的操作 3.切换目录 4.设置匿名用户 5.图形化界面登录 6.白名单与黑名单 重点与难点 一、FTP简介 1.FTP-文件传输协议 FTP是FileTransferProtocol&#xff08;文件传输协…

(十) 盘古UI,详情小标题副标题的实现,方便快速开发详情PanguFormTitle!

(十) 盘古UI,详情小标题副标题的实现,方便快速开发详情PanguFormTitle! 盘古UI,较为全面的自定义UI框架,帮助你绝对的快速开发!(长期维护中) 控件位置: com.smart.pangu_ui_lib.widget.PanguFormTitle demo地址,点击查看github 详情|表单的小标题 可以在详情或者表单的页面…

MapMagic 2 Biomes and Functions

MapMagic 2(免费)世界生成器官方模块。支持基于遮罩混合几个图形,从而可以在无限地形上混合不同的生物群落。也随附函数节点,从而可以在子图中执行复杂的生成过程。将它们视作含有输入和输出连接器的生物群落。请注意,必须使用 MapMagic 2 的现有安装才能使用该模块。 下…

护眼台灯哪个牌子最好?消费者信赖之选护眼灯十大品牌推荐

孩子入学后&#xff0c;对其的关注和照料必须加倍。特别是眼睛的健康&#xff0c;在学习过程中显得尤为关键。为了减轻孩子的眼睛疲劳&#xff0c;降低近视的风险&#xff0c;选择一款优质的护眼台灯至关重要。护眼台灯哪个牌子最好&#xff1f;目前市场上书客、飞利浦、松下等…

微信小程序中,plugins 配置项如何配置

在微信小程序中&#xff0c;plugins 配置项用于声明小程序需要使用的第三方插件。以下是如何配置 plugins 的基本步骤&#xff1a; 获取插件的AppID和版本信息&#xff1a; 首先&#xff0c;你需要在微信开放平台或微信公众平台上找到你需要的插件&#xff0c;并获取其AppID和版…

【漏洞复现】泛微e-office系统ajax.php接口存在任意文件上传漏洞

漏洞描述 泛微e-office系统是标准、易用、快速部署上线的专业协同OA软件。泛微 E-Office 9.5版本存在代码问题漏洞,泛微e-office系统ajax.php接口存在任意文件上传漏洞 免责声明 技术文章仅供参考,任何个人和组织使用网络应当遵守宪法法律,遵守公共秩序,尊重社会公德,…

大模型推理优化之 KV Cache

原文&#xff1a;https://zhuanlan.zhihu.com/p/677660376 目录 收起 KV Cache 定义 KV Cache 原理 KV Cache 实现 KV Cache 显存占用分析 KV Cache 优化方法 在语言模型推理的过程中&#xff0c;性能优化一直是一个备受关注的话题。LLM&#xff08;Large Language Mode…

基于stm32的UART高效接收DMA+IDLE编程示例

目录 基于stm32的UART高效接收DMAIDLE编程示例实验目的场景使用原理图UART的三种编程方式IDLE程序设计串口配置配置中断配置DMA代码片段本文中使用的测试工程 基于stm32的UART高效接收DMAIDLE编程示例 本文目标&#xff1a;基于stm32_h5的freertos编程示例 按照本文的描述&am…

训练营第三十三天贪心(第五部分重叠区间问题)

训练营第三十三天贪心&#xff08;第五部分重叠区间问题&#xff09; 435.无重叠区间 力扣题目链接 题目 给定一个区间的集合 intervals &#xff0c;其中 intervals[i] [starti, endi] 。返回 需要移除区间的最小数量&#xff0c;使剩余区间互不重叠 。 示例 1: 输入: …

MySQL多版本并发控制mvcc原理浅析

文章目录 1.mvcc简介1.1mvcc定义1.2mvcc解决的问题1.3当前读与快照读 2.mvcc原理2.1隐藏字段2.2版本链2.3ReadView2.4读视图生成原则 3.rc和rr隔离级别下mvcc的不同 1.mvcc简介 1.1mvcc定义 mvcc(Multi Version Concurrency Control)&#xff0c;多版本并发控制&#xff0c;是…