常见排序详解(历时四天,哭了,必须释放一下)

news2024/11/27 8:26:10

目录

1、插入排序

1.1 基本思想

1.2 直接插入排序

1.2.1 思路

1.2.2 代码实现

1.2.3 性质

1.3 希尔排序

1.3.1 思路

1.3.2 代码实践

1.3.3 性质

2、选择排序

2.1 基本思想

2.2 直接选择排序

2.2.1 思路

2.2.2 代码实践

2.2.3 性质

2.3 堆排序

2.3.1 思路

2.3.2 代码实践

2.3.3 性质

3、交换排序

3.1 基本思想

3.2 冒泡排序

3.2.1 思路

3.2.2 代码实践

3.2.3 性质

3.3 快速排序

3.3.1 思路

3.3.2 代码实践

3.3.2.1 hoare版本

3.3.2.2 挖坑法

3.3.2.3 前后指针法

3.3.2.4 快排优化

3.3.2.4.1 三数取中

3.3.2.4.2 小区间优化

3.3.2.4.3 代码实践

3.3.2.5 三路划分法和自省排序(用于重复值较多的)

3.2.2.6 非递归快排(利用栈)

3.3.3 性质

4、归并排序

4.1 基本思想

4.2 代码实践

4.2.1 递归版本

4.2.2 非递归版本

 4.3 性质

5、计数排序

5.1 基本思想

5.2 代码实践

5.3 性质


 

排序的稳定性:如果一个排序算法是稳定的,当有两个相等键值的纪录R和S,且在原本的列表中R出现在S之前,在排序过的列表中R也将会是在S之前。

相同的数无所谓相对次序,但在实际应用中,比的是结构体,需要保持相对次序不变

如:学生成绩这个结构体,要求总分相同时,语文成绩高的排在前面

那么先按语文成绩排,再用稳定性的排序排总分(整体是按总分排,那么总分最后排)

排序的代码实践,建议先单趟再整体

以下默认升序

1、插入排序

1.1 基本思想

将一个数 插入到 有序的序列中(类似于玩扑克牌时的插入)

注意:挪动数据是进行覆盖

1.2 直接插入排序

1.2.1 思路

开始时,认为arr[0]是有序的,插入arr[1]……最后是arr[n-1]插入到前[0,n-2]

1.2.2 代码实现

// 直接插入排序
// O(N^2)
void InsertSort(int* a, int n)
{
	for (int i = 0; i < n - 1; i++)
	{   // [0,end]是有序的,插入end+1位置的值
		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;
	}
}

1.2.3 性质

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

2. 时间复杂度:O(N^2)(逆序的时候),但通常不是逆序,所以还行,有实践意义

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

4. 稳定性:稳定

1.3 希尔排序

1.3.1 思路

先进行预排序(让其接近有序):

gap为一个间隔,分成n(总元素个数)/gap个组,分别进行直接插入排序

如:下面以 gap = 3 为例,10/3,分成3组

代码如下:

三层循环是 先完成一组,再完成下一组

	int gap = 3;
	for (int j = 0; j < gap; j++)//多组排序
	{
		for (int i = j; i < n - gap; i += gap)// 一组排序
		{   // 一次直接插入排序
			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;
		}
	}

三层循环看着别扭,下面改为两层循环(书上都是两层循环),但效率没变,都是完成了多组排序,

两层循环是 三个组(n/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;
		}

此时gap = 1时,为直接插入排序,

1.3.2 代码实践

gap取法很多,目前没有最优取法,下面这种取法较好

// 希尔排序
// O(N^1.3)
void ShellSort(int* a, int n)
{
	int gap = n;
	while (gap > 1)
	{
		// +1,使最后一次执行gap为1
		// gap > 1,预排序
		// 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 (tmp < a[end])
				{
					a[end + gap] = a[end];
					end -= gap;
				}
				else
					break;
			}
			a[end + gap] = tmp;
		}
	}
}

1.3.3 性质

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

2. 当gap > 1时都是预排序,目的是让数组更接近于有序。当gap == 1时,数组已经接近有序的了,这样就会很快。这样整体而言,可以达到优化的效果。

3. 希尔排序的时间复杂度不好计算,可以估计为O(N^1.3),但还是比快排弱一点

4. 稳定性:不稳定

2、选择排序

2.1 基本思想

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

选最值 与相应位置进行交换

2.2 直接选择排序

2.2.1 思路

遍历找最值,与起始位置进行交换

遍历该数组,把最大(小)的数与该组的第一个位置交换,

这里优化一下,遍历该数组,把最小的数的下标和最大的数的下标找到(因为要交换,要找下标),分别与第一个位置的数,最后一个的数交换

2.2.2 代码实践

// 直接选择排序
// O(N^2)
void SelectSort(int* a, int n)
{
	int begin = 0;
	int end = n - 1;
	while (begin<end)
	{
		int maxi = begin;
		int mini = begin;
		for (int i = begin + 1; i <= end; ++i)//begin+1,不用和自己比
		{
			if (a[maxi] < a[i])
				maxi = i;
			if (a[mini] > a[i])
				mini = i;
		}
		Swap(&a[mini], &a[begin]);
		if (maxi == begin)
			maxi = mini;
		Swap(&a[maxi], &a[end]);
		++begin;
		--end;
	}
}

2.2.3 性质

1. 直接选择排序思考非常好理解,但是效率不是很好,实际中很少使用

2. 时间复杂度:O(N^2),教学意义

3. 空间复杂度:O(1)

4. 稳定性:不稳定

2.3 堆排序

2.3.1 思路

建堆找最值,与结尾位置进行交换

堆排序(Heapsort)是指利用堆,这种数据结构所设计的一种排序算法,它是选择排序的一种。它是 通过堆来进行选择数据。

注意:升序建大堆,降序建小堆

想要详细了解堆+堆排序+堆的应用,可以看看我这篇博客

堆+堆排序+topK问题_大顶堆小顶堆java-CSDN博客

2.3.2 代码实践

/// 堆排序
// O(N*logN)
void AdjustDown(int* a, int n, int root)
{
	int child = 2 * root + 1;
	while (child < n)
	{
		if (child + 1 < n && a[child + 1] > a[child])// child+1防止越界
			++child;
		if (a[child] > a[root])
		{
			Swap(&a[root], &a[child]);
			root = child;
			child = 2 * child + 1;
		}
		else
			return;// 不发生交换时,已经成堆了,直接结束
	}
}
void HeapSort(int* a, int n)
{
	int i = 0;
	for (i = (n - 1 - 1) / 2; i >= 0; --i)
	{
		AdjustDown(a, n, i);
	}
	i = n - 1;
	while (i > 0)
	{
		Swap(&a[0], &a[i]);
		--i;
		AdjustDown(a, i+1, 0);
	}
}

2.3.3 性质

1. 堆排序使用堆来选数,效率就高了很多

2. 时间复杂度:O(N*logN),但还是比快排弱一点

3. 空间复杂度:O(1)

4. 稳定性:不稳定

3、交换排序

3.1 基本思想

所谓交换,就是根据序列中两个数的比较结果 来对换这两个数在序列中的位置

通过交换,完成排序

3.2 冒泡排序

3.2.1 思路

相邻的数两两比较,进行交换,把最值交换到结尾位置

3.2.2 代码实践

// 冒泡排序
// O(N^2)
void BubbleSort(int* a, int n)
{
	for (int j = 0; j < n - 1; ++j)
	{
		int flag = 0;
		for (int i = 0; i < n - 1 - j; ++i)
		{
			if (a[i] > a[i + 1])// > 为了稳定性,相等的值不交换
			{
				Swap(&a[i], &a[i + 1]);
				flag = 1;
			}
		}
		if (flag == 0)
			break;
	}
}

3.2.3 性质

1. 冒泡排序是一种非常容易理解的排序

2. 时间复杂度:O(N^2),教学意义

3. 空间复杂度:O(1)

4. 稳定性:稳定

3.3 快速排序

3.3.1 思路

快速排序是Hoare于1962年提出的一种二叉树结构的交换排序方法,其基本思想为:

任取待排序元素序列中的某元素作为基准值(key),一般取最左边的值为key,通过某种方法,将待排序集合分割成左右子序列,左子序列中所有元素均 < key,右子序列中所有元素均 > key,然后最左右子序列重复该过程,直到所有元素都排列在相应位置上为止

3.3.2 代码实践

left和right分别为 指向对应数组首尾元素的下标,是闭的

3.3.2.1 hoare版本

keyi = left,begin = left,end = right(分别为L和R的起始位置)  

右边先走,左找大,右找小,进行交换

当begin == end(即相遇),循环结束,再Swap(&a[keyi],&a[begin]),keyi = begin(让keyi指向key)

左边做key,右边先走,相遇时可以保证相遇位置的值 < key

证明:

L遇R(R停L动):R停的位置的值 < key

R遇L(L停R动):若之前发生了交换,L位置下的值 < key,

                          若没有发生交换,key自己交换(有序情况下,都是自己交换,时间复杂度为O(N^2),后面三数取中,会进行优化)

反之,右边做key,左边先走,相遇时可以保证相遇位置的值 > key

// 快速排序hoare版本
int PartSort1(int* a, int left, int right)
{
	int keyi = left;
	int begin = left, end = right;// 初始的范围,后面要用,不能改变
	while (begin < end)// 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;
}

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

	int keyi = PartSort1(a,left,right);

	// [left,keyi-1] keyi [keyi+1,right]
	QuickSort(a, left, keyi - 1);
	QuickSort(a, keyi + 1, right);
}
3.3.2.2 挖坑法

(并没有用交换,而是覆盖坑位)

key存储左边的值,左边形成坑位

右边先走,右找小,找到后,该值填入坑位,当前位置形成新坑

左找大,找到后,该值填入坑位,当前位置形成新坑……

当相遇时,把key的值填入坑位

注意:左边是坑,左边先走,大的值就填到左边了

// 快速排序挖坑法
int PartSort2(int* a, int left, int right)
{
	int key = a[left];
	int hole = left;
	int begin = left, end = right;
	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;
}

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

	int keyi = PartSort2(a,left,right);

	// [left,keyi-1] keyi [keyi+1,right]
	QuickSort(a, left, keyi - 1);
	QuickSort(a, keyi + 1, right);
}
3.3.2.3 前后指针法

prev = left,cur = left + 1 (prev和cur的起始位置) 

若cur的值 < key,++prev,prev与cur进行交换,++cur

若cur的值 >= key,++cur,当cur > right循环结束,

再Swap(&a[keyi],&a[prev]),keyi = prev(让keyi指向key)

// 快速排序前后指针法
int PartSort3(int* a, int left, int right)
{
	int keyi = left;
	int prev = left, cur = prev + 1;
	while (cur <= right)
	{
		if (a[cur] < a[keyi] && ++prev != cur)// prev+1 == cur不交换
			Swap(&a[prev], &a[cur]);
		++cur;
	}

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

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

	int keyi = PartSort3(a,left,right);

	// [left,keyi-1] keyi [keyi+1,right]
	QuickSort(a, left, keyi - 1);
	QuickSort(a, keyi + 1, right);
}
3.3.2.4 快排优化
3.3.2.4.1 三数取中

决定快排性能的关键点是每次单趟排序后,key对数组的分割

如果每次选key基本二分居中,那么快排的递归树就是颗均匀的满⼆叉树,性能最佳。但是实践中虽然不可能每次都是二分居中,但是性能也还是可控的。

但是如果出现每次选到最小值/最大值,划分为0个和N-1的子问题时,时间复杂度为 O(N^2),数组序列有序时就会出现这样的问题,可以用三数取中或者随机选key解决了这个问题。

3.3.2.4.2 小区间优化

不再递归分割排序,减少递归的次数

3.3.2.4.3 代码实践
// 快速排序前后指针法
int PartSort3(int* a, int left, int right)
{
	int keyi = left;
	int prev = left, cur = prev + 1;
	while (cur <= right)
	{
		if (a[cur] < a[keyi] && ++prev != cur)// prev+1 == cur不交换
			Swap(&a[prev], &a[cur]);
		++cur;
	}

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

int GetMidi(int* a, int left, int right)
{
	int midi = (left + right) / 2;
	// left midi right
	if (a[left] < a[midi])
	{
		if (a[midi] < a[right])
		{
			return midi;
		}
		else if (a[left] < a[right])
		{
			return right;
		}
		else
		{
			return left;
		}
	}
	else // a[left] > a[midi]
	{
		if (a[midi] > a[right])
		{
			return midi;
		}
		else if (a[left] < a[right])
		{
			return left;
		}
		else
		{
			return right;
		}
	}
}

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

	// 小区间优化,不再递归分割排序,减少递归的次数
	if ((right - left + 1) < 10)
	{
		InsertSort(a + left, right - left + 1);
	}
	else
	{
		// 三数取中
		int midi = GetMidi(a, left, right);
		Swap(&a[left], &a[midi]);

		int keyi = PartSort3(a, left, right);

		// [left,keyi-1] keyi [keyi+1,right]
		QuickSort(a, left, keyi - 1);
		QuickSort(a, keyi + 1, right);
	}
}
3.3.2.5 三路划分法和自省排序(用于重复值较多的)

虽然用了三数取中,但是现在还是有一些场景没解决(数组中有大量重复数据时),如:

// 数组中有多个跟key相等的值
int a[] = { 6,1,7,6,6,6,4,9 };
int a[] = { 3,2,3,3,3,3,2,3 };
// 数组中全是相同的值
int a[] = { 2,2,2,2,2,2,2,2 };

三路划分:用于重复值较多的,但重复值没那么多时,效率会比前面的方法低一些

三路划分核心思想:把数组中的数据分为三段【<key的值】 【=key的值】【>key的值】

实现思想:

1. key默认取left位置的值

2. left指向区间最左边,right指向区间最后边,cur指向left+1位置

3. cur遇到 < key的值,跟left位置的值交换,left++,cur++

4. cur遇到 > key的值,跟right位置交换,换到右边,right--,无脑与right换,cur都会判断

5. cur遇到 = key的值,cur++

6. 直到cur > right结束

// 快速排序三路划分法
void PartSort3Way(int* a, int left, int right)
{
	if (left >= right)
		return;

	// 小区间优化,不再递归分割排序,减少递归的次数
	if ((right - left + 1) < 10)
	{
		InsertSort(a + left, right - left + 1);
	}
	else
	{
		// 三数取中
		int midi = GetMidi(a, left, right);
		Swap(&a[left], &a[midi]);

		int key = a[left];
		int cur = left + 1;
		int begin = left, end = right;//保存之前的left和right
		while (cur <= right)
		{
			if (a[cur] < key)
			{
				Swap(&a[cur], &a[left]);
				++left;
				++cur;
			}
			else if (a[cur] > key)
			{
				Swap(&a[cur], &a[right]);
				--right;
			}
			else
				++cur;
		}

		// [begin,left-1] [left,right] [right+1,end]
		PartSort3Way(a, begin, left - 1);
		PartSort3Way(a, right + 1, end);
	}
}

自省排序:重复值多与不多,效率都还行(C++STL库里的 sort 就是用这种方法)

当发现递归层数过多时(可能就是重复值导致),就停止使用快排,改用堆排

递归函数多了两个参数(每次递归层数++,使用堆排的层数),和一个使用堆排的判断,

3.2.2.6 非递归快排(利用栈)

我对递归的理解是,自己调用自己,自己会给自己传参

那么改非递归,可以把参数存储到栈里,通过循环取参

// 快速排序前后指针法
int PartSort3(int* a, int left, int right)
{
	int keyi = left;
	int prev = left, cur = prev + 1;
	while (cur <= right)
	{
		if (a[cur] < a[keyi] && ++prev != cur)// prev+1 == cur不交换
			Swap(&a[prev], &a[cur]);
		++cur;
	}

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

// 快速排序 非递归实现
void QuickSortNonR(int* a, int left, int right)
{
	Stack stack;
	StackInit(&stack);
	StackPush(&stack, right);
	StackPush(&stack, left);

	int begin, end, keyi;
	while (!StackEmpty(&stack))
	{
		begin = StackTop(&stack);
		StackPop(&stack);
		end = StackTop(&stack);
		StackPop(&stack);

		keyi = PartSort3(a, begin, end);

		// [begin, keyi-1] keyi [keyi+1, end]
		if (begin < keyi - 1)
		{
			StackPush(&stack, keyi-1);
			StackPush(&stack, begin);
		}

		if (keyi + 1 < end)
		{
			StackPush(&stack, end);
			StackPush(&stack, keyi+1);
		}
	}
	StackDestroy(&stack);// 非常容易忘记
}

stack.h

#pragma once
#include <stdio.h>
#include <stdlib.h>
#include <assert.h>


// 支持动态增长的栈
typedef int STDataType;
typedef struct Stack
{
	STDataType* _a;
	int _top;		// 栈顶
	int _capacity;  // 容量 
}Stack;
// 初始化栈 
void StackInit(Stack* ps);
// 入栈 
void StackPush(Stack* ps, STDataType data);
// 出栈 
void StackPop(Stack* ps);
// 获取栈顶元素 
STDataType StackTop(Stack* ps);
// 获取栈中有效元素个数 
int StackSize(Stack* ps);
// 检测栈是否为空,如果为空返回非零结果,如果不为空返回0 
int StackEmpty(Stack* ps);
// 销毁栈 
void StackDestroy(Stack* ps);

stack.c

#include "Stack.h"

// 初始化栈 
void StackInit(Stack* ps)
{
	assert(ps);
	ps->_a = NULL;
	ps->_top = ps->_capacity = 0;
}

//动态申请
void Stackcapacity(Stack* ps)
{
	if (ps->_top == ps->_capacity)
	{
		ps->_capacity = ps->_capacity == 0 ? 4 : 2 * ps->_capacity;
		STDataType* tmp = (STDataType*)realloc(ps->_a, ps->_capacity * sizeof(STDataType));
		if (tmp == NULL)
		{
			perror("Stackcapacity()::realloc()");
			return;
		}
		ps->_a = tmp;
	}
}

// 入栈 
void StackPush(Stack* ps, STDataType data)
{
	assert(ps);
	Stackcapacity(ps);//动态申请
	ps->_a[ps->_top] = data;
	ps->_top++;
}

// 出栈 
void StackPop(Stack* ps)
{
	assert(ps);
	if (ps->_top == 0)
	{
		printf("栈已空\n");
		return;
	}
	ps->_top--;
}

// 获取栈顶元素 
STDataType StackTop(Stack* ps)
{
	assert(ps);
	assert(ps->_top != 0);
	return ps->_a[ps->_top - 1];
}

// 获取栈中有效元素个数 
int StackSize(Stack* ps)
{
	assert(ps);
	return ps->_top;
}

// 检测栈是否为空,如果为空返回非零结果,如果不为空返回0 
int StackEmpty(Stack* ps)
{
	assert(ps);
	if (ps->_top == 0)
		return 1;
	else
		return 0;
}

// 销毁栈 
void StackDestroy(Stack* ps)
{
	assert(ps);
	free(ps->_a);
	ps->_a = NULL;
	ps->_top = ps->_capacity = 0;
}

3.3.3 性质

1. 快速排序整体的综合性能和使用场景都是比较好的,所以才敢叫快速排序

2. 时间复杂度:O(N*logN)  平均情况下

3. 空间复杂度:O(logN)  递归调用的层数(每层要O(1)的额外空间,存储划分点)

4. 稳定性:不稳定

4、归并排序

4.1 基本思想

已有序的序列合并,得到完全有序的序列,单个元素可以认为是有序的

若将两个有序表合并成一个有序表,称为二路归并。 归并排序核心步骤:

4.2 代码实践

begin和end分别为 指向对应数组首尾元素的下标,是闭的

4.2.1 递归版本

类似于后序,

先"递",分到单个元素(认为单个元素是有序序列),然后归并两个有序序列,memcpy,再"归"

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

	int mid = (begin + end) / 2;
	// [begin,mid] [mid+1,end]
	_MergeSort(a, tmp, begin, mid);
	_MergeSort(a, tmp, mid + 1, end);

	int begin1 = begin, end1 = mid;
	int begin2 = mid + 1, end2 = end;
	int i = begin;// memcpy要用begin算个数,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++];

	// 注意+begin,下标指向的位置的地址 等于 数组名+下标
	memcpy(a + begin, tmp + begin, sizeof(int) * (end - begin + 1));
}

// 归并排序递归实现
// 时间复杂度:O(N*logN)
// 空间复杂度:O(N)
void MergeSort(int* a, int n)
{
	int* tmp = (int*)malloc(sizeof(int) * n);
	if (tmp == NULL)
	{
		perror("MergeSort()::malloc()");
		return;
	}

	_MergeSort(a, tmp, 0, n - 1);

	free(tmp);
	tmp = NULL;
	return;
}

两个为什么:

为什么要这样分[begin,mid] [mid+1,end][begin,mid-1],[mid,end]不行吗

两个元素的区间为,[奇数,奇数+1],或者,[偶数,偶数+1],

两种情况一样,下面以[奇数,奇数+1]再分为例

[begin,mid-1],[mid,end]再分[奇数,奇数+1]

mid = (奇数 + 奇数 + 1)/2 = 奇数,分为两个区间,[奇数,奇数-1]和[奇数,奇数+1]

[奇数,奇数],begin > end,直接return;

[奇数,奇数+1],又会分出,[奇数,奇数-1]和[奇数,奇数+1]……所以一直在"递",最后栈溢出

[begin,mid] [mid+1,end]再分[奇数,奇数+1]

mid = (奇数 + 奇数 + 1)/2 = 奇数,分为两个区间,[奇数,奇数]和[奇数+1,奇数+1],

[奇数,奇数],begin == end,return;

[奇数+1,奇数+1],begin == end,return;就结束了

为什么每次归并后就要memcpy

因为归并的序列必须是有序序列,单个元素可以认为是有序序列

tmp是把a中的序列进行归并,每次归并成有序序列就copy到a中,就保证下次取a中的序列是有序的

4.2.2 非递归版本

用一个栈模拟,从栈里取出两个区间([begin1,end1],[begin2,end2]),进行归并,可是begin1,end1和begin2,end2已经取出来了,要归并到哪里呢,栈里面没有与之相关的数据了

那么直接在数组上进行操作,类似于层序

引入gap(所要归并的有序序列的元素个数)

 归并的时候会出现的情况

// 归并排序非递归实现
// 时间复杂度:O(N*logN)
// 空间复杂度:O(N)
void MergeSortNonR(int* a, int n)
{
	int* tmp = (int*)malloc(sizeof(int) * n);
	if (tmp == NULL)
	{
		perror("MergeSort()::malloc()");
		return;
	}

	int gap = 1;// 所要归并的有序序列的元素个数
	while (gap < n)
	{
		// i = 0,指向第一个有序序列的首元素
		// i+=2*gap,i跳过了两个gap大小的有序序列,指向下一个有序列的首元素
		for (int i = 0; i < n; i += 2 * gap)
		{
			// 与递归相比,非递归里的i就是递归里的begin
			int begin1 = i, end1 = i + gap - 1;
			int begin2 = i + gap, end2 = i + gap + gap - 1;
			if (begin2 >= n)
				break;// [begin2,end2]都越界了,不用归并,也不用memcpy了
			if (end2 >= n)
				end2 = n - 1;// end2越界了,进行修正
			int j = i;
			while (begin1 <= end1 && begin2 <= end2)
			{
				if (a[begin1] <= a[begin2])// = 为了稳定性,相等的值左边先插入
					tmp[j++] = a[begin1++];
				else
					tmp[j++] = a[begin2++];
			}
			while (begin1 <= end1)
				tmp[j++] = a[begin1++];
			while (begin2 <= end2)
				tmp[j++] = a[begin2++];
			// 注意+begin,下标指向的位置的地址 等于 数组名+下标
			memcpy(a + i, tmp + i, sizeof(int) * (end2 - i + 1));
		}
		gap *= 2;
	}

	free(tmp);
	tmp = NULL;
	return;
}

 4.3 性质

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

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

3. 空间复杂度:O(N)

4. 稳定性:稳定

5、计数排序

5.1 基本思想

1. 统计相同元素出现次数

2. 根据统计的结果将序列回收到原来的序列中

如:

5.2 代码实践

使用相对映射:count[a[i] - min]++; 

// 计数排序
// 时间复杂度:O(N+range)
// 只适合整数/适合范围集中,可以排负数,-min,下标不会是负数
// 空间范围度:O(range)
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 (min > a[i])
			min = a[i];
	}

	int range = max - min + 1;
	int* count = (int*)calloc(range, sizeof(int));
	if (count == NULL)
	{
		perror("CountSort()::calloc()");
		return;
	}

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

	int j = 0;
	for (int i = 0; i < range; ++i)
	{
		while (count[i]--)// 执行count[i]次
		{
			a[j++] = i + min;
		}
	}

	free(count);
	count = NULL;
	return;
}

5.3 性质

1.只适合整数/适合范围集中,(可以排负数,因为-min,下标不会是负数)

在某些方面非常快,如排集中的整数

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

3. 空间范围度:O(range)

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

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

相关文章

No.5 笔记 | 网络端口协议概览:互联网通信的关键节点

1. 常用端口速览表 端口范围主要用途1-1023系统或特权端口1024-49151注册端口49152-65535动态或私有端口 远程访问类&#xff08;20-23&#xff09; 端口服务记忆技巧安全风险21FTP"File Transfer Port"爆破、嗅探、溢出、后门22SSH"Secure Shell"爆破、…

基于 STM32F407 的 SPI Flash下载算法

目录 一、概述二、自制 FLM 文件1、修改使用的芯片2、修改输出算法的名称3、其它设置4、修改配置文件 FlashDev.c5、文件 FlashPrg.c 的实现 三、验证算法 一、概述 本文将介绍如何使用 MDK 创建 STM32F407 的 SPI Flash 下载算法。 其中&#xff0c;SPI Flash 芯片使用的是 W…

人工智能专业就业方向与前景

随着产业结构升级的持续推进&#xff0c;未来行业领域对于人工智能专业人才的需求量会逐渐增加&#xff0c;一部分高校也开始陆续在本科阶段开设人工智能专业&#xff0c;以缓解人工智能领域人才缺口较大的问题。下面是小编整理的人工智能专业就业方向与前景&#xff0c;欢迎阅…

Leecode热题100-41.缺失的第一个正数

给你一个未排序的整数数组 nums &#xff0c;请你找出其中没有出现的最小的正整数。 请你实现时间复杂度为 O(n) 并且只使用常数级别额外空间的解决方案。 示例 1&#xff1a; 输入&#xff1a;nums [1,2,0] 输出&#xff1a;3 解释&#xff1a;范围 [1,2] 中的数字都在数组…

C++面向对象:继承!

前言 继承是面向对象三大特性之一&#xff0c;所有的面向对象的语言都具备这三个性质&#xff0c;我们之前已经介绍过了封装的相关概念&#xff0c;今天我们来学习一下第二大特性&#xff1a;继承。 一.继承的概念 什么是继承&#xff1f; 定义&#xff1a;继承&#xff08;…

AI相关的整理

AI相关的整理 初体验记不住如何部署如何微调 整理AI学习&#xff0c;AI小白&#xff0c;业余爱好。持续更新&#xff0c;谨慎参考&#xff01; 初体验 试了一下本地直接下载安装ollama和open-webui&#xff0c;然后运行指定的模型&#xff0c;跟着文档做&#xff0c;很简单。但…

(Linux驱动学习 - 7).阻塞IO和非阻塞IO

一.阻塞IO和非阻塞IO定义 1.阻塞IO 当应用程序对设备驱动进行操作的时候&#xff0c;如果不能获取到设备资源&#xff0c;那么阻塞式IO就会将应用程序对应的线程挂起&#xff0c;直到设备资源可以获取为止。 在应用程序中&#xff0c;用户调用 open 函数默认是以阻塞式打开设备…

54.二叉树的最大深度

迭代 class Solution {public int maxDepth(TreeNode root) {if(rootnull){return 0;}int de0;Queue<TreeNode> qunew LinkedList<>();TreeNode tn;int le;qu.offer(root);while(!qu.isEmpty()){lequ.size();while(le>0){tnqu.poll();if(tn.left!null){qu.offe…

RTA-OS Port Guide学习(四)-基于S32K324 OS

文章目录 前言PerformanceMeasurement EnvironmentRAM and ROM Usage for OS ObjectsSingle CoreMulti Core Stack UsageLibrary Module SizesSingle CoreMulti Core Execution TimeContext Switching Time 总结 前言 前面一篇文章介绍了硬件的一些特性&#xff0c;本文为最后…

国内目前顶级的哲学教授颜廷利:全球公认十个最厉害的思想家

国内目前顶级的哲学教授颜廷利&#xff1a;全球公认十个最厉害的思想家 颜廷利&#xff0c;字弃安&#xff0c;号求前&#xff0c;山东济南人&#xff0c;当代著名思想家、哲学家、教育家、易经心理学家、中国第一起名大师、国际权威易学大师、中国汉字汉语研究专家、现代最著…

什么是数字化智能工厂的组成

二、数字化智能工厂的主要功能组成 数字化智能工厂主要由以下几个功能部分组成&#xff1a; 自动化生产设备&#xff1a;包括机器人、智能传感器、可编程逻辑控制器&#xff08;PLC&#xff09;等&#xff0c;用于实现生产过程的自动化操作&#xff0c;减少人力依赖&#xff0…

[C#]C# winform部署yolov11-pose姿态估计onnx模型

【算法介绍】 在C# WinForms应用中部署YOLOv11-Pose姿态估计ONNX模型是一项具有挑战性的任务。YOLOv11-Pose结合了YOLO&#xff08;You Only Look Once&#xff09;的高效物体检测算法和Pose Estimation&#xff08;姿态估计&#xff09;专注于识别人体关键点的能力&#xff0…

移动WSL到其他盘

1、首先下载 Move WSL 工具包&#xff0c;并解压。&#xff08;https://github.com/pxlrbt/move-wsl/archive/refs/heads/master.zip&#xff09; 2、管理员身份运行Windows PowerShell。 3、在PowerShell中运行如下命令&#xff0c;停止正在运行的Linux子系统。 wsl --shutd…

柯桥商务英语口语-work-shy 是什么意思?不要理解成“工作害羞”!

ork工作&#xff0c;shy是害羞&#xff0c;那么&#xff0c;work-shy是什么意思&#xff1f; 其实在 "work-shy" 这个短语中&#xff0c;"shy" 的意思并不是害羞&#xff0c;而是表达一种躲避、逃避的意思。 "work-shy" 表示对工作有一种躲避、…

深度学习基础—交并比与非极大值抑制

1.交并比 &#xff08;1&#xff09;定义 交并比是用来衡量目标检测算法的表现的函数。定义如下&#xff1a; 用预测框和真实框的面积的交集除以预测框和真实框的面积的并集&#xff0c;得到的结果本次算法预测的交并比。研究函数可以发现&#xff0c;交并比的范围为[0,1]&…

cnn突破七(四层bpnet网络公式与卷积核bpnet公式相关)

我们要有一个概念&#xff0c;就是卷积核就是我们的w1&#xff0c;w12&#xff0c;w2 那么我们的5*5卷积核怎么表达&#xff0c;当他在14*14的图像中流动时&#xff0c;对应的像素也在变化 这个和我们的上面w1&#xff0c;w12&#xff0c;w2不同&#xff0c;因为这几个都是全…

测绘地理信息赋能新质生产力

在信息化与智能化浪潮的推动下&#xff0c;测绘地理信息作为连接现实世界与数字空间的桥梁&#xff0c;正逐步成为驱动经济社会发展的新质生产力。本文旨在深入探讨测绘地理信息如何通过技术创新与应用拓展&#xff0c;为各行各业赋能&#xff0c;塑造智慧社会的新面貌。 一、…

word无法复制粘贴

word无法复制粘贴 使用word时复制粘贴报错 如下&#xff1a; 报错&#xff1a;运行时错误‘53’&#xff0c;文件未找到&#xff1a;MathPage.WLL 这是mathtype导致的。 解决方法 1&#xff09;在mathtype下载目录下找到"\MathType\MathPage\64"下的"mathpa…

第T3周:CNN实现天气识别

&#x1f368; 本文为&#x1f517;365天深度学习训练营 中的学习记录博客&#x1f356; 原作者&#xff1a;K同学啊 目标&#xff1a; 搭建CNN网络模型实现多云、下雨、晴、日出四种天气状态的识别&#xff0c;并用真实天气做预测 具体实现&#xff1a; &#xff08;一&#x…

Win10照片查看器不见了怎么办?

刚换了电脑&#xff0c;发现查看图片默认打开是window画图工具&#xff0c;看图竟然需要一张一张打开&#xff0c;超级不方便。右键图片选择打开方式也不见照片查看器&#xff0c;window自带的看图工具去哪儿了&#xff1f; 不要着急&#xff0c;我们可以把它找回来&#xff0…