排序:万物皆有序

1.排序的概念及其运用

排序以及相关术语的概念
  • 排序:让一串记录能够按照某个或某些关键字的大小,递增或递减排列起来

  • 稳定性:记录中相等元素的相对顺序保持不变,即为稳定,反之则不稳定

  • 内部排序:数据元素放在内存中的排序

  • 外部排序:数据元素太多不能同时放在内存,不能在内外存之间移动数据的排序

常见的排序算法

2.排序算法的实现

插入排序

思想:按照值的大小,把待排序的元素插入到已经排序好的元素中,得到一个新的有序序列

代码实现:

c 复制代码
void InsertSort(int* a, int n)
{
	for (int i = 0; i < n - 1; i++)
	{
		int end = i;
		int temp = a[end + 1];
		while (end >= 0)
		{
			//当前元素小于下一个位置的元素时,将当前元素后移
			//直到让当前元素不大于此元素
			if (a[end] > temp)
			{
				a[end + 1] = a[end];
				--end;
			}
			else//当当前元素不大于此元素时,就可以弹出
			{
				break;
			}
		}
		//让end指向元素的下一个元素即为此元素
		a[end + 1] = temp;

	}
}

其特性:

  • 时间复杂度:O(N^2)
  • 空间复杂度:O(1) - 没有创建额外的空间
  • 稳定性:稳定
    • 遇到和当前end指向的元素时,不会继续挪动
  • 元素集合越接近有序,插入排序的时间效率越高
希尔排序

思想:选定一个整数,按照这个整数以距离分组,对每一组的记录进行排序,然后再重新取一个小的整数,重复排序,直到=1时,所有记录排序完成

代码实现

c 复制代码
void ShellSort(int* a, int n)
{
	int gap = n;
	while (gap > 1)
	{
		gap = gap/3+1; //每一次的gap都进行缩减
		//每一个gap都走一个插入排序
		//i < n - gap:如果=n-gap,就会越界了
		for (int i = 0; i < n - gap; i++)
		{
			int end = i;
			int temp = a[end + gap];

			while (end >= 0)
			{
				if (temp < a[end])
				{
					a[end + gap] = a[end];
					end -= gap;
				}
				else
				{
					break;
				}
			}
			a[end + gap] = temp;
		}
	}
}

特性总结:

  • 希尔排序 = 不同gap情况下的插入排序,然后汇总
  • gap >1 - 做预排序,让其接近有序,gap=1时已经接近有序了
  • 其时间复杂度约为O(N^1.3)左右(n在某个特定范围内)
选择排序

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

代码实现:下述的实现是升级版,同时选出最大和最小的值

c 复制代码
void SelectSort(int* a, int n)
{
	//分别设置好开头和结尾
	int begin = 0, end = n - 1;
	while (begin < end)
	{
		//刚开始,都把第一个元素设置为最大/最小值
		int max = a[begin], min = a[begin];
		//从当前元素的下一个元素开始,进行挑选
		//记住当前排序的最大和最小元素的下标
		for (int i = begin + 1; i < end; i++)
		{
			if (a[i] > max)
				max = i;
			if (a[i] < min)
				min = begin;
		}
		//分别放置到当前循环的末和尾
		Swap(&a[min], &a[begin]);
		if (max == begin) //避免max的位置在开头
		{
			max = min;
		}
		Swap(&a[max], &a[end]);

		//缩小区间范围
		++begin;
		--end;
	}
}

特性选择:

  • 时间复杂度:O(N^2)
  • 空间复杂度:O(1)
  • 稳定性:不稳定
堆排序

思想:根据数据结构设计的算法,通过堆来选择数据

  • 升序 - 建大堆
  • 降序 - 建小堆
  • 总结就是:堆排序每一轮都把堆顶元素交换到末尾,所以想让最大值取末尾,最后得到升序,就需要建立大堆;想让最小值去末尾,最后得到降序,就需要建立小堆

代码实现:

c 复制代码
//升序 - 建立大堆
void AdjustDown(int* a, int size, int parent)
{
	int child = parent * 2 + 1;
	while (child < size)
	{
		if (child + 1 < size && a[child + 1] > a[child])
			child = child + 1;
		
		if (a[child] > a[parent])
		{
			Swap(&a[child], &a[parent]);
			parent = child;
			child = parent * 2 + 1;
		}
		else
		{
			break;
		}
	}

}

//堆排序
//升序:建大堆
//降序:建小堆
void HeapSort(int* a, int n)
{
	//从有子节点的树开始建起
	for (int i = (n - 1 - 1) / 2; i >= 0; --i)
	{
		AdjustDown(a, n, i);
	}

	//步骤
	//1.每次先把堆顶元素放到末尾
	//2.然后进行向下调整到合适的位置
	//3.末尾元素为最大的元素,已经是有序的了,所以--end
	//范围由[0,end]->[0,end-1],
	int end = n - 1;
	while (end > 0)
	{
		Swap(&a[0], &a[end]);
		AdjustDown(a, end, 0);
		--end;
	}
}

特性总结:

  • 选数的效率高高
  • 时间复杂度:O(N*logN)
  • 空间复杂度:O(1)
  • 稳定性:不稳定
交换排序之冒泡排序

思想:交换,就是元素本身的大小比较交换二者在序列中的位置,将大的元素往后移,小的元素往前移

代码实现:

c 复制代码
void BubbleSort(int* a, int n)
{
	//每次都选出一个最大值交换到最后
	//注:不是一次性选出,而是相邻元素交换得到的
	for (int i = 0; i < n; i++)
	{
		int exchange = 0;//判断本轮是否产生交换
		for (int j = 1; j < n - i; j++)
		{
			if (a[j - 1] > a[j])
			{
				exchange = 1;
				Swap(&a[j - 1], &a[j]);
			}
		}
		if (!exchange)//本轮没产生交换,就直接结束
			break;
	}
}

特性总结:

  • 冒泡排序是一种容易理解的排序
  • 时间复杂度:O(N^2)
  • 空间复杂度:O(1)
  • 稳定性:稳定
交换排序之快速排序

思想:取待排序元素中的某个元素为基准值,然后将待排序元素分割为两个子序列,左边小于它,右边大于它,然后重复上述过程,直到所有元素都在相应位置上

1.hoare版

代码实现:

c 复制代码
int GetMidi(int* a, int begin, int end)
{
	int midi = (begin + end) / 2;
	// begin end midi三个数选中位数
	if (a[begin] < a[midi])
	{
		if (a[midi] < a[end])
			return midi;
		else if (a[begin] > a[end])
			return begin;
		else
			return end;
	}
	else //a[begin] > a[midi]
	{
		if (a[begin] < a[end])
			return begin;
		else if (a[midi] > a[end])
			return midi;
		else
			return end;
	}
}


void QuickSort(int* a, int begin, int end)
{
	//说明排序已完成
	if (begin >= end)
		return;

	//寻找中间数,把中间数放在开头
	int midi = GetMidi(a, begin, end);
	Swap(&a[midi], &a[begin]);

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

	while (left < right)
	{
		//往右边找,一直找到比key小的数
		while(left < right && a[right] >= a[key])
		{
			--right;
		}
		//往左边找,一直找到比key大的数
		while (left < right && 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);
}
2.挖坑版

代码实现:

c 复制代码
int PartSort2(int* a, int begin, int end)
{
	int mid = GetMidi(a, begin, end);
	Swap(&a[mid], &a[begin]);

	int key = a[begin];
	int hole = begin;
	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;
}
3.前后指针版
c 复制代码
int PartSort3(int* a, int begin, int end)
{
	int mid = GetMidi(a, begin, end);
	Swap(&a[mid], &a[begin]);
	int key = begin;

	int prev = begin;
	int cur = prev + 1;
	while (cur <= end)
	{
		
		//cur的值小于基准值key,并且与prev不相邻
		if (a[cur] < a[key] )
		{
			prev++;
			if (prev != cur)//prev与cur不重合
			{
				Swap(&a[prev], &a[cur]);
			}
		}
		++cur;
	}

	Swap(&a[prev], &a[key]);
	key = prev;
	return key;
}
快速排序之非递归

递归快排的基本逻辑其实是:

  • 先对区间 [begin, end] 做一次 Partition
  • 假设枢轴 keyi 最后落到了位置 div
  • 那么数组被分成两段:
    • 左边 [begin, div-1]
    • 右边 [div+1, end]
  • 然后再分别对这两段继续做同样的事

递归调用的时候,系统会帮你保存接下来要处理的区间:

  • 先处理 [0, 9]
  • 分完后,后面要处理 [0, 4] 和 [6, 9]
  • 再从其中拿一个出来处理
  • 一直反复

所以我们完全可以自己建一个栈,把这些"待处理区间"存起来,非递归调用就可以通过一个栈来记录这些区间。栈里通常存一个区间的所有边界

  • begin
  • end
  • 0,9\] - 就是把0和9压栈

第一步:整个区间压栈

c 复制代码
[0, n-1]

第二步:循环处理栈顶区间

只要栈不为空

c 复制代码
取出一个区间 [left, right]
如果这个区间里元素个数小于等于 1,就不用排
否则做一次 Partition
枢轴确定后,会分成左右两个子区间
把有效的子区间继续压栈
  • 每次压栈都先压右区间,再压左区间,因为栈是后进先出的结构
  • 当然,还要判断压栈区间是否合法
    • 4,4\] - 只有一个元素,不需要再排

    • 只有left<right时,这个区间至少有两个元素,才可以继续划分
代码实现
c 复制代码
void QuickSortNonR(int* a, int begin, int end)
{
	ST st;
	STInit(&st);
	//先插入最开始的首位:[end,begin]
	STPush(&st, end);
	STPush(&st, begin);

	while (!STEmpty(&st))
	{
		int left = STTop(&st);
		STPop(&st); //弹出begin
		int right = STTop(&st);
		STPop(&st); //弹出end

		//选出基准元素 - 左边比它小,右边比它大
		int key = PartSort3(a, left, right);

		//存在合法范围时,就开始分割
		if (left < key - 1)
		{
			//先插入右区间,再插入左区间(栈是从栈顶弹出的)
			STPush(&st, key - 1);
			STPush(&st, left);
		}
		if (key + 1 < right)
		{
			STPush(&st, right);
			STPush(&st, key + 1);
		}
	}
	STDestroy(&st);
}

性能总结:

  • 时间复杂度:O(NlogN)
  • 空间复杂度:O(logN)
    • 注意:空间复杂度看的是同时占用多少空间 ,而不是调用的次数,递归情况下未返回的递归链,也就是递归深度,就是空间复杂度
  • 稳定性:不稳定
归并排序

思想:分而治之,随后合并有序

代码实现:

c 复制代码
void _MergeSort(int* a, int begin, int end, int* tmp)
{
	if (begin >= end)
		return;

	int mid = (begin + end) / 2;
	_MergeSort(a, begin, mid, tmp);
	_MergeSort(a, mid + 1, end, tmp);
	
	// [begin, mid][mid+1, end]归并
	int begin1 = begin, end1 = mid;
	int begin2 = mid + 1, end2 = end;
	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++];
	}
	
	//+begin主要是考虑到可能不同区间的起始位置不同
	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);
}
非递归的归并排序
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)
	{
		printf("gap:%2d->", gap);
		for (size_t i = 0; i < n; i += 2 * gap)
		{
			int begin1 = i, end1 = i + gap - 1;
			int begin2 = i + gap, end2 = i + 2 * gap - 1;
			// [begin1, end1][begin2, end2] 归并
			//printf("[%2d,%2d][%2d, %2d] ", begin1, end1, begin2, end2);


			//边界情况1:不需要再排了 - 因为都构不成一个区间
			if (begin2 >= n || end1 >= n)
			{
				break;
			}
			//边界情况2:把第二部分的区间调整为n-1即可
			if (end2 >= n)
			{
				end2 = n - 1;
			}

			int j = begin1;
			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++];
			}

			memcpy(a + i, tmp + i, sizeof(int) * (end2 - i + 1));
		}
		printf("\n");
		gap *= 2;
	}
	free(tmp);
}
计数排序

思想:统计相同元素出现的次数,然后把统计的结果将序列收回原来的序列当中

  • 但是当本身序列的取值范围特别大时,就有可能出现严重的空间浪费问题(例如有5个数的范围是1~5,但是第六个数的值是1000,那可能你要设置的统计序列的范围就要在1-1000,那就有百分之90的空间被浪费了)
  • 所以,我们可以计算出当前数值序列的最大和最小值,利用二者的差值,通过相对位置存放

代码实现:

c 复制代码
void CountSort(int* a, int n)
{
	//本次排序为改编版
	//1.选出最大和最小的数
	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];
	}
	//2.得到当前序列的值的区间范围,通过相对位置放置到对应的桶
	int range = max - min + 1;
	int* count = (int*)calloc(range, sizeof(int));
	if (count == NULL)
	{
		printf("calloc fail\n");
		return;
	}

	//3.统计次数
	for (int i = 0; i < n; i++)
	{
		//a[i]-min - 采用相对位置,放到特定位置中
		count[a[i] - min]++;
	}

	//4.排序
	int i = 0;
	for (int j = 0; j < range; j++)
	{
		//打印每个桶对应的数据
		//j+min - 得到原本的值
		while (count[j]--)
		{
			a[i++] = j + min;
		}
	}
}

特性总结:

  • 时间复杂度:O(N+range)
  • 空间复杂度:O(range)
  • 稳定性:稳定

3.算法复杂度及稳定性分析


相关推荐
其实秋天的枫2 小时前
2025年12月英语六级真题及答案解析完整版(第一、二、三套全PDF)
经验分享·算法
2401_874732532 小时前
C++并发编程中的死锁避免
开发语言·c++·算法
2301_792308252 小时前
C++编译期数学计算
开发语言·c++·算法
hetao17338372 小时前
2025-03-13~22 hetao1733837 的刷题记录
c++·算法
sqyno1sky2 小时前
C++中的契约编程
开发语言·c++·算法
优化控制仿真模型2 小时前
2026年最新驾考科目一考试题库2309道全。电子版pdf
经验分享·算法·pdf
qq_334903152 小时前
嵌入式C++驱动开发
开发语言·c++·算法
阿贵---2 小时前
C++代码规范化工具
开发语言·c++·算法
暮冬-  Gentle°2 小时前
自定义内存检测工具
开发语言·c++·算法