常见的排序算法

1.排序的概念及其运用

1.1排序的概念

1.排序:排序简而言之就是使一串记录,按照其中某个或者某些关键词的大小对其进行升序或降序排列

2.稳定性:在一串序列中需要根据某些关键词大小进行排序的序列,但两个关键词大小相同,在不将原来序列中关键词大小相同的两个的前后顺序调换的排序就是稳定的

3.内部排序:数据元素全部放在内存中的排序

4.外部排序:数据元素太多不能同时放在内存中,根据排序过程的要求不断地在内外存之间移动数据的排序。

1.2排序的运用

在生活中很多都需要用到排序算法,比如学生成绩的排序,手机销量的排序,抖音热榜的排序

2.常见的排序算法

2.1冒泡排序

算法思想:

将最大或者最小的数据元素排到最后

算法实现:

c 复制代码
void BubbleSort(int* a, int n)
{
	int flag = 0;
	for (int i = 0; i < n - 1; i++)
	{
		flag = 0;
		for (int j = 0; j < n - i - 1; j++)
		{
			if (a[j] > a[j + 1])
			{
				Swap(&a[j], &a[j + 1]);
				flag = 1;
			}
		}
		if (flag == 0)
		{
			break;
		}
	}
}

复杂度分析

时间复杂度:

最好:O(N)

最坏:O(N^2)

空间复杂度:O(1)

2.2选择排序

算法思想:

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

这里我将其优化了一下,及在待排序序列中找到最大和最小的数据元素,分别将其排到序列的起始位置和最后位置。

算法实现:

c 复制代码
void SelectSort(int* a, int n)
{
	int end = n - 1;
	int begin = 0;
	while (begin < end)
	{
		int mini = begin;
		int maxi = begin;
		for (int i = begin + 1; i <= end; i++)
		{
			if (a[mini] > a[i])
			{
				mini = i;
			}
			if (a[maxi] < a[i])
			{
				maxi = i;
			}
		}
		Swap(&a[mini], &a[begin]);
		if (maxi == begin)
		{
			maxi = mini;
		}
		Swap(&a[maxi], &a[end]);
		end--;
		begin++;
	}
}

复杂度分析

时间复杂度

最好:O(N^2)

最坏:O(N^2)

空间复杂度:O(1)

2.3直接插入排序

算法思想:

将为排序的数据元素插入到已排序序列中进行排序,如:排升序时,排到第i个元素时,前面i-1个元素已经排好序了,将第i个元素与第i-1个元素相比较,如果大于第i-1个元素,就不需要继续比较,如果小于第i-1个元素,交换两个元素,在将其于第i-2个元素比较,以此类推直到整个序列排序完成。

算法实现:

c 复制代码
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;
	}
}

复杂度分析

时间复杂度:

最好:O(N)

最坏:O(N^2)

空间复杂度:O(1)

2.4希尔排序

算法思想:

希尔排序的思想和直接插入排序的思想有异曲同工之妙,希尔排序就是在直接插入排序上做了一些优化,先对代拍序列进行预排序,减小预排序之间gap的大小,知道为1,为直接插入排序,但由于序列i已经部分有序,直接插入排序的时间复杂度也大幅度降低

算法实现:

c 复制代码
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(NlogN)(具体来说是O(N^1.3)根据大量实验测得)

最坏:O(N^2)

空间复杂度·:O(1)

2.5快速排序

2.5.1hoare版本

算法思想:

定义两个变量left和right,分别指向区间的开头和结尾,定义一个变量key值,key值为数组所在区间的第一个值a[left],在从right开始想左找比key值小的元素,找到了再从left开始向右找比key值大的元素,找到了在将两个数交换,直到left>=right就跳出循环

算法实现:

c 复制代码
void QuickSort(int* a , int left, int right)
{
	if (left >= right)
	{
		return;
	}
	int begin = left;
	int end = right;
	int keyi = left;
	while (left < right)
	{
		while (left < right && a[right] >= a[keyi])
		{
			right--;
		}
		while (left < right && a[left] <= a[keyi])
		{
			left++;
		}
		Swap(&a[left],&a[right]);
	}
	Swap(&a[keyi], &a[left]);
	keyi = left;
	QuickSort(a, begin, keyi - 1);
	QuickSort(a, keyi + 1, end);
}

但这个快速排序还有一个弊端,在其为降序要将其排为升序时,时间复杂度为O(N^2),于是就可以有两种方法将其时间复杂度稳定在O(NlogN)

1.随机数法:
c 复制代码
void QuickSort(int* a , int left, int right)
{
	if (left >= right)
	{
		return;
	}
	int begin = left;
	int end = right;
	int randi = rand() % (right - left + 1);
	randi += left;
	Swap(&a[left], &a[randi]);
	int keyi = left;
	while (left < right)
	{
		while (left < right && a[right] >= a[keyi])
		{
			right--;
		}
		while (left < right && a[left] <= a[keyi])
		{
			left++;
		}
		Swap(&a[left],&a[right]);
	}
	Swap(&a[keyi], &a[left]);
	keyi = left;
	QuickSort(a, begin, keyi - 1);
	QuickSort(a, keyi + 1, end);
}
2.三数取中法(加了小区间优化):
c 复制代码
int getmid(int* a, int left, int right)
{
	int mid = (left + right) / 2;
	if (a[mid] < a[left])
	{
		if (a[right] < a[mid])
		{
			return mid;
		}
		else if (a[left] < a[right])
		{
			return left;
		}
		else
		{
			return right;
		}
	}
	else
	{
		if (a[right] > a[mid])
		{
			return mid;
		}
		else if (a[right] < a[left])
		{
			return left;
		}
		else
		{
			return right;
		}
	}
}
void QuickSort(int* a, int left, int right)
{
	if (left >= right)
	{
		return;
	}
	//小区间优化
	if (right - left + 1 < 10)//可以减少90%左右的递归
	{
		InsertSort(a + left, right - left + 1);
		return;
	}
	int begin = left;
	int end = right;
	int midi = getmid(a, left, right);
	Swap(&a[midi], &a[left]);
	int keyi = left;
	while (left < right)
	{
		while (a[right] >= a[keyi] && left < right)
		{
			right--;
		}
		while (a[left] <= a[keyi] && left < right)
		{
			left++;
		}
		Swap(&a[left], &a[right]);
	}
	Swap(&a[keyi], &a[left]);
	keyi = left;
	QuickSort(a, begin, keyi - 1);
	QuickSort(a, keyi + 1, end);
}

2.5.2前后指针法

算法思想:

排升序时:定义两个变量prev,cur,区间的开头和结尾为left,right,prev指向left,cur指left+1,keyi=left,cur小于等于end时,进入循环,当a[cur]>=a[keyi],cur++,否则,prev++,交换a[cur]和a[prev],cur++,直到循环结束,交换a[prev]和a[keyi]

算法实现:

c 复制代码
void quicksort(int* a, int left, int right)
{
	while (left >= right)
	{
		return;
	}
	int keyi = left;
	int prev = left;
	int cur = left;
	while (cur <= right)
	{
		if (a[cur] < a[keyi] && ++prev != cur)
		{
			swap(&a[cur], &a[prev]);
		}
		cur++;
	}
	swap(&a[keyi], &a[prev]);
	keyi = prev;
	quicksort(a, left, keyi - 1);
	quicksort(a, keyi + 1, right);
}

2.5.3挖坑法

算法思想:

  1. 选择基准值(Pivot):从待排序序列中挑出一个元素,称为"基准"(pivot),这个元素可以按照不同的策略来选择,例如选择序列的第一个、最后一个、中间一个或者随机选择一个元素。
  2. 分区(Partition) :重新排序数列,所有比基准值小的元素摆放在基准前面,所有比基准值大的元素摆在基准的后面(相同的数可以到任一边)。在这个分区退出之后,该基准就处于数列的中间位置。这个称为分区(partition)操作。
    • "挖坑法"的分区过程通常涉及几个指针或"坑"的概念:
      • low 指针:指向当前已排序序列(即小于基准值的元素序列)的末尾。
      • high 指针:指向未排序序列的开始。
      • pivotIndex:基准值原本的位置,可以理解为"坑"的位置,基准值先被移出(放入临时变量中),待分区完成后放回。
    • 分区过程中,通过比较和交换,逐步将小于基准值的元素移动到左侧,大于基准值的元素移动到右侧。同时,low 和 high 指针会向中间移动,直到它们相遇或交错。

算法实现:

c 复制代码
void QuickSort(int* a, int left, int right)
{
	if (left >= right)
	{
		return;
	}
	int begin = left;
	int end = right;
	int key = a[begin];
	int keyi = begin;
	while (left < right)
	{
		while (a[right] >= key && left < right)
		{
			right--;
		}
		a[keyi] = a[right];
		keyi = right;
		while (a[left] <= key && left < right)
		{
			left++;
		}
		a[keyi] = a[left];
		keyi = left;
	}
	a[keyi] = key;
	QuickSort(a, begin, keyi - 1);
	QuickSort(a, keyi + 1, end);
}

2.5.4快速排序的非递归实现

算法思想:

利用来实现对递归左右子区间的存储和弹出,利用弹出的区间进行快徐排序(随便用一种方法)

算法实现(这里用的是C语言手搓的栈):

c 复制代码
void QuickSort(int* a, int left, int right)
{
	Stack s;
	StackInit(&s);
	StackPush(&s, right);
	StackPush(&s, left);
	while (!StackEmpty(&s))
	{
		int begin = StackTop(&s);
		StackPop(&s);
		int end = StackTop(&s);
		StackPop(&s);
		int prev = begin;
		int cur = begin + 1;
		int keyi = begin;
		while (cur <= end)
		{
			if (a[cur] < a[keyi] && ++prev != cur)
			{
				Swap(&a[prev], &a[cur]);
			}
			cur++;
		}
		Swap(&a[prev], &a[keyi]);
		keyi = prev;
		if (keyi + 1 < end)
		{
			StackPush(&s, end);
			StackPush(&s, keyi + 1);
		}
		if (begin < keyi - 1)
		{
			StackPush(&s, keyi - 1);
			StackPush(&s, begin);
		}
	}
	StackDestroy(&s);
}

复杂度分析

时间复杂度:

最好:O(NlogN)

最坏:O(N^2)

空间复杂度:O(logN)(递归了logN层)

2.6归并排序

算法思想:

将区间不断二分,直到区间中只剩一个元素,在将子区间进行归并,用一个临时数组存储起来,然后在将其拷贝到原数组中

算法实现:

c 复制代码
void _MergeSort(int* a, int* tmp, int left, int right)
{
	if (left == right)
		return;
	int mid = (left + right) / 2;
	int begin1 = left, end1 = mid;
	int begin2 = mid + 1, end2 = right;
	_MergeSort(a, tmp, begin1, end1);
	_MergeSort(a, tmp, begin2, end2);
	int i = left;
	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 + left, tmp + left, sizeof(int) * (right - left + 1));
}

void MergeSort(int* a, int n)
{
	int* tmp = (int*)malloc(sizeof(int) * n);
	if (tmp == NULL)
	{
		perror("malloc fail!");
		return;
	}
	_MergeSort(a, tmp, 0, n - 1);
	free(tmp);
	tmp = NULL;
}

2.7归并排序的非递归实现

算法思想:

归并排序的非递归实现用栈和队列实现都有点复杂,因为在弹出左右子区间将其合并后,再下一次合并时并不知道其之前的区间范围,于是就可以用循环控制两个子区间的距离来实现归并排序

算法实现:

c 复制代码
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 = j + gap, end2 = begin2 + gap - 1;
			int i = j;
			if (end1 >= n || begin2 >= n)
			{
				break;
			}
			if (end2 >= n)
			{
				end2 = n - 1;
			}
			while (begin1 <= end1 && begin2 <= end2)
			{
				if(a[begin1]<a[begin2])
				{
					tmp[i++] = a[begin1++];
				}
				else
				{
					tmp[i++] = a[begin2++];
				}
			}
			while (begin1 <= end1)
			{
				tmp[i++] = a[begin1++];
			}
			while (begin2 <= end2)
			{
				tmp[i++] = a[begin2++];
			}
			memcpy(a + j, tmp + j, sizeof(int) * (end2 - j + 1));
		}
		gap *= 2;
	}
	free(tmp);
	tmp = NULL;
}

注意事项:

需要注意区间是否越界,当end1和begin2越界时就不需要合并,当end2越界时,就需要将end2改为n-1,再对区间进行合并,以及拷贝时从原数组和临时数组的起始地址

复杂度分析

时间复杂度:

最好:O(NlogN)

最坏:O(NlogN)

空间复杂度:

创建了一个临时数组,空间复杂度为O(N)

各种排序的复杂度和稳定性总结:

相关推荐
闻缺陷则喜何志丹4 分钟前
【C++动态规划 图论】3243. 新增道路查询后的最短距离 I|1567
c++·算法·动态规划·力扣·图论·最短路·路径
Lenyiin23 分钟前
01.02、判定是否互为字符重排
算法·leetcode
鸽鸽程序猿38 分钟前
【算法】【优选算法】宽搜(BFS)中队列的使用
算法·宽度优先·队列
Jackey_Song_Odd39 分钟前
C语言 单向链表反转问题
c语言·数据结构·算法·链表
Watermelo61742 分钟前
详解js柯里化原理及用法,探究柯里化在Redux Selector 的场景模拟、构建复杂的数据流管道、优化深度嵌套函数中的精妙应用
开发语言·前端·javascript·算法·数据挖掘·数据分析·ecmascript
乐之者v1 小时前
leetCode43.字符串相乘
java·数据结构·算法
A懿轩A2 小时前
C/C++ 数据结构与算法【数组】 数组详细解析【日常学习,考研必备】带图+详细代码
c语言·数据结构·c++·学习·考研·算法·数组
古希腊掌管学习的神2 小时前
[搜广推]王树森推荐系统——矩阵补充&最近邻查找
python·算法·机器学习·矩阵
云边有个稻草人2 小时前
【优选算法】—复写零(双指针算法)
笔记·算法·双指针算法
半盏茶香2 小时前
在21世纪的我用C语言探寻世界本质 ——编译和链接(编译环境和运行环境)
c语言·开发语言·c++·算法