排序算法(C语言版)

前言

排序作为生产环境中常见的需求之一,对整个产品有举足轻重的影响,可以说使用一个合适的排序算法是业务逻辑中比较重要的一部分。今天我们就来介绍常见的排序算法以及实现

排序

所谓排序无非就是按照特定的规则对一组数据就行顺序化。

常见的排序有升序和降序。

对于数字类型的数据常见的规则是按照其大小排序。如果学过Java的朋友应该知道,Java引入了对象的概念,对象的排序规则一般需要程序员自己定义,实现Comparable或者Comparator接口,在在接口内部实现排序的逻辑

除排序本身的定义外,我们还需要了解一点关于排序的性质

  • 稳定性:当“大小”一致的两个数据应该如何规定两者的顺序呢?比如一个班级中有两位考100分的同学,我们应该如何规定两者的顺序呢?显然,一个常见的想法是谁先交卷谁是第一名。这种保证“交卷顺序”的排序算法,我们可以描述为稳定的排序算法。稳定性即保证排序后“大小”一致的数据顺序与排序前一致
  • 内/外排序:这是一组相对的概念。内部排序要求排序的数据全部在内存中完成排序。外部排序要求排序的数据在硬盘内存中移动来完成排序,常用于数据量巨大或者内存不够的时候使用。

常见的排序算法

常见的排序算法有比较类的排序:冒泡排序,插入排序,希尔排序,选择排序,堆排序,快速排序,归并排序

非比较类的排序:基数排序,计数排序,桶排序

下面我们就一个一个来了解上述算法的思想和代码实现,以下笔者均以排升序举例。

笔者在排序算法的实现中可能会用到交换算法,在这里先实现,后面不在介绍

//交换元素
void swap(int* p1, int* p2) {
	int tmp = *p1;
	*p1 = *p2;
	*p2 = tmp;
}

这里是一个简答的交换,我们只是把它封装一层,实现代码的复用

冒泡排序

冒牌排序,思路是相邻两个数据就行比较,遍历一遍为一趟排序。一趟排序后可以保证本趟最大值位于合适的位置。由此,每趟排序后下一趟的比较次数可以优化。也可以定义一个标记,如果本趟没有交换,整个序列均有序

实现

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

void BubbleSort(int* a, int n)
{
	for (int y = 0; y < n - 1; y++)
	{
		int flag = 1;
		int len = n - y;
		for (int i = 0; i < len - 1; i++)
		{
			if (a[i] > a[i + 1])
			{
				swap(a + i + 1, a + i);
				flag = 0;
			}
		}
		if (flag) return;
	}
}

1. 冒泡排序是一种非常容易理解的排序
2. 时间复杂度:O(N^2)
3. 空间复杂度:O(1)
4. 稳定性:稳定

插入排序

插入排序,思路是假设前n个数据已经有序,第n+1个数据插入到有序的序列中。这个算法的适应性很强

实现

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

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

1. 元素集合越接近有序,直接插入排序算法的时间效率越高
2. 时间复杂度:O(N^2)
3. 空间复杂度:O(1)
4. 稳定性:稳定
 

希尔排序

希尔排序是对插入排序的优化。插入排序的缺陷在于,在将降序排序成升序时,后面的较小值越来越难以移动到前面的位置,因为移动的次数由1,2,3……往上自增。希尔排序的优化在于增加跨度的方让序列更快的接近有序。 引入一个gap变量,即数据分为gap组,以组为单位进行插入排序。不断改变gap的值,有两个常用的gap算式 gap=gap/2 和 gap=gap/3+1。这两个算式均可以保证最后一次gap为1,即保证最后一次希尔排序转换为插入排序(此时序列接近有序,前面介绍插入排序时介绍插入排序的适应性很强,表现在排序一个接近有序的序列时效率很高)。当然也可以自定义gap算式,但是需要保证gap最后一次的取值是1

实现

//希尔排序
void ShellSort(int* a, int n);

void ShellSort(int* a, int n)
{
	int gap = n;
	while (gap > 1)
	{
		gap = gap / 3 + 1;
		int i = 0;
		while (i < gap)
		{
			int y = i;
			while (1)
			{
				if (y >= n || y + gap >= n)
					break;
				if (a[y + gap] < a[y]) {
					int x = y;
					int tmp = a[y + gap];
					while (x >= 0 && tmp < a[x])
					{
						a[x + gap] = a[x];
						x -= gap;
					}
					a[x + gap] = tmp;
				}
				y += gap;
			}
			i++;
		}
	}
}

1. 可以理解为gap!=1时是预排序,gap=1时是插入排序。希尔排序是对插入排序的优化
2. 时间复杂度:O(N^1.3)
3. 空间复杂度:O(1)
4. 稳定性:不稳定

选择排序

选择排序是一个比较简单的排序算法,主要的逻辑就是每一趟找出最小的值,和本趟开头位置的元素呼唤。这里可以有一点优化,就是在一趟遍历的时候,同时找最大值和最小值。

实现

这里我们提供两个版本的实现,优化和非优化的版本(说是优化,其实性能并没有提升多少)

//选择排序
void SelectSort(int* a, int n);//优化版,同时选择大小
void SelectSort1(int* a, int n);//只选择小

void SelectSort1(int* a, int n)//取小交换
{
	for (int y = 0; y < n - 1; y++) {
		int i = y;
		int tmp = y + 1;
		for (; tmp < n; tmp++)
		{
			if (a[tmp] < a[i])
				i = tmp;
		}
		swap(a + i, a + y);
	}
}
void SelectSort(int* a, int n)//优化,取大小交换
{
	int begin = 0;
	int end = n - 1;
	while (begin < end)
	{
		int min = begin;
		int max = begin;
		for (int i = begin + 1; i <= end; i++)
		{
			if (a[i] < a[min])
				min = i;
			else if (a[i] > a[max])
				max = i;
		}
		swap(a + begin, a + min);
		if (begin == max)
			max = min;
		swap(a + end, a + max);
		begin++;
		end--;
	}
}

1. 直接选择排序思考非常好理解,但是效率不是很好。实际中很少使用
2. 时间复杂度:O(N^2)
3. 空间复杂度:O(1)
4. 稳定性:不稳定
 

堆排序

堆排序是依赖于堆这个数据结构实现的一种排序算法。什么是堆这里不再展开。我们想要用堆排序算法实现升序,需要用所有的数据建立一个大根堆,然后不断的pop堆顶的元素与堆末尾(逻辑上是堆,实际上是数组)元素交换,调整堆……知道堆中只剩一个元素,此时排序完成。

实现

//堆排序
void HeapSort(int* a, int n);

void xiangXiaTiaoZheng(int* arr, int pr, int k) {//向下调整
	int ch = pr * 2 + 1;
	while (ch < k) {
		if (ch + 1 < k && arr[ch + 1] > arr[ch]) ch++;
		if (arr[ch] > arr[pr]) {
			swap(arr + ch, arr + pr);
			pr = ch;
			ch = pr * 2 + 1;
		}
		else return;
	}
}
void jianDui(int* arr, int k)//建堆
{
	for (int i = (k - 2) / 2; i >= 0; i--) {
		xiangXiaTiaoZheng(arr, i, k);
	}
}
void HeapSort(int* a, int n)
{
	jianDui(a, n);
	int end = n - 1;
	while (end > 0) {
		swap(a, a + end);
		xiangXiaTiaoZheng(a, 0, end);
		end--;
	}
}

1. 堆排序使用堆来选数,效率就高了很多(topK问题)。向下调整建堆的算法要比向上调整建堆的算法效率高
2. 时间复杂度:O(N*logN)
3. 空间复杂度:O(1)
4. 稳定性:不稳定
 

快速排序

快速排序的思想是以每趟开始的元素为基准,将大于基准的元素放在基准数的右边,将小于基准的元素放在基准的左边,此时可以保证基准位于合适的位置。然后再分别处理基准的左半边和右半边,知道所有的元素都位于合适的位置上。

我们可以将实现分类为递归实现和非递归实现

递归实现

递归实现的大体逻辑是相同的,只不过在每一趟的实现有可以分为不同的版本。这里我们介绍三种不同的实现方式——hoare版本、挖坑版本、双指针版本

hoare版本

这里单趟的逻辑是以左侧开始位置的元素为基准元素,右边开始向左遍历在第一个小于基准元素的位置停下,左边开始向右遍历在第一个大于基准元素的位置停下,交换左右的元素,以上逻辑循环直到左右相遇,退出循环再交换相遇位置和起始位置的元素。

void QuickSort(int* a, int begin, int end);//hoare版本

void QuickSort(int* a, int begin, int end)//hoare版本
{
	if (begin >= end)
		return;
	/*
    if ((end - begin + 1) < 10)
	{
		InsertSort(a + begin, end - begin + 1);
		return;
	}
	int z = QuZhong(a, begin, end);
	swap(a + begin, a + z);
    */
	int tmp = a[begin];
	int left = begin;
	int right = end;
	while (left < right)
	{
		while (left < right && a[right] >= tmp)
		{
			right--;
		}
		while (left < right && a[left] <= tmp)
		{
			left++;
		}
		swap(a + left, a + right);
	}
	swap(a + begin, a + left);
	QuickSort(a, begin, left - 1);
	QuickSort(a, left + 1, end);
}

此版本是最初实现快速排序是使用的版本。

为了方便理解,后面再开发出双指针法和挖坑法

挖坑版本

挖坑法单趟的思路是以左侧开始位置的元素为基准元素,同时将左侧开始位置的元素记录到tmp中,设置此位置为坑。右边开始向左遍历在第一个小于基准元素的位置停下,将此位置的元素放到坑中,再将此位置设置为坑。将左边开始向右遍历在第一个大于基准元素的位置停下,将此位置的元素放到坑中,再将此位置设置为坑。以上逻辑循环直到左右相遇,退出循环,此时相遇的位置是坑位,将tmp添入坑位中

void QuickSort1(int* a, int begin, int end);//挖坑法版本

void QuickSort1(int* a, int begin, int end)
{
	if (begin >= end)
		return;
    /*
	if ((end - begin + 1) < 10)
	{
		InsertSort(a + begin, end - begin + 1);
		return;
	}
	int z = QuZhong(a, begin, end);
	swap(a + begin, a + z);
    */
	int tmp = a[begin];
	int tmpi = begin;
	int left = begin;
	int right = end;
	while (left < right)
	{
		while (left < right && a[right] >= tmp)
		{
			right--;
		}
		if (left < right)
		{
			swap(a + tmpi, a + right);
			tmpi = right;
			left++;
		}
		while (left < right && a[left] <= tmp)
		{
			left++;
		}
		if (left < right)
		{
			swap(a + tmpi, a + left);
			tmpi = left;
			right--;
		}
	}
	QuickSort1(a, begin, left - 1);
	QuickSort1(a, left + 1, end);
}

双指针版本

以本趟开始位置的元素为基准元素。定义两个指针变量,第一个指针prev指向本趟开始的元素,第二个指针cut指向本趟的第二个元素。如果cut指向的元素小于等于基准,则prev指针+1,prev与cut元素交换,cut指针+1:否则cut指针+1。直到cut指向空指针,退出循环,将基准元素与prev指向的元素互换。

void QuickSort2(int* a, int begin, int end);//双指针版本

void QuickSort2(int* a, int begin, int end)
{
	if (begin >= end)
		return;
    /*
	if ((end - begin + 1) < 10)
	{
		InsertSort(a + begin, end - begin + 1);
		return;
	}
	int z = QuZhong(a, begin, end);
	swap(a + begin, a + z);
    */
	int prev = begin;
	int cut = begin + 1;
	int tmp = a[begin];
	while (cut <= end)
	{
		while (cut <= end && a[cut] <= tmp)
		{
			prev++;
			cut++;
		}
		while (cut <= end && a[cut] > tmp)
		{
			cut++;
		}
		if (cut <= end)
		{
			prev++;
			swap(a + prev, a + cut);
		}
	}
	swap(a + begin, a + prev);
	QuickSort2(a, begin, prev - 1);
	QuickSort2(a, prev + 1, end);
}

非递归实现

递归版本在内存的开销上有可能会造成栈溢出,此时改成非递归是常见的需求。该非递归的核心思路就是用栈模拟递归的过程,利用栈存储关键的信息。

非递归需要依赖于栈这个数据结构,这里也不在展开

栈:

#pragma once
#include <stdio.h>
#include <stdlib.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);

#define _CRT_SECURE_NO_WARNINGS 1
#include "Stack.h"

void StackInit(Stack* ps)
{
	ps->_a = NULL;
	ps->_top = -1;
	ps->_capacity = 0;
}

void StackPush(Stack* ps, STDataType data)
{
	if (ps->_capacity == 0) {
		ps->_a = (STDataType*)malloc(sizeof(STDataType) * 4);//默认初始化长度为4
		ps->_capacity = 4;
		ps->_top = 0;
	}
	else if (ps->_top == ps->_capacity)
	{
		ps->_capacity *= 2;
		ps->_a = (STDataType*)realloc(ps->_a, sizeof(STDataType) * ps->_capacity);//默认扩容至原先的两倍

	}
	(ps->_a)[ps->_top++] = data;
}

void StackPop(Stack* ps)
{
	if (ps->_top <= 0) return;
	ps->_top--;
}

STDataType StackTop(Stack* ps) {
	if (ps->_top <= 0) return 0;
	return (ps->_a)[--ps->_top];
}

int StackSize(Stack* ps)
{
	return ps->_top;
}

int StackEmpty(Stack* ps)
{
	if (ps->_top <= 0) return 1;
	return 0;
}

void StackDestroy(Stack* ps)
{
	free(ps->_a);
	ps->_a = NULL;
	ps = NULL;
}
void QuickSortNonR(int* a, int begin, int end);//非递归版本

void QuickSortNonR(int* a, int begin, int end) 	//非递归版本
{
	Stack sk;
	StackInit(&sk);
	StackPush(&sk, end);
	StackPush(&sk, begin);
	while (!StackEmpty(&sk))
	{
		int left = StackTop(&sk);
		int right = StackTop(&sk);
		int rembegin = left;
		int remend = right;
		int tmp = a[left];

		while (left < right)
		{
			while (left < right && a[right] >= tmp)
			{
				right--;
			}
			while (left < right && a[left] <= tmp)
			{
				left++;
			}
			swap(a + left, a + right);
		}
		swap(a + rembegin, a + left);
		if (left + 1 < remend) {
			StackPush(&sk, remend);
			StackPush(&sk, left + 1);
		}
		if (rembegin < left - 1) {
			StackPush(&sk, left - 1);
			StackPush(&sk, rembegin);
		}
	}
	StackDestroy(&sk);
}

优化

在此基础上可以对快速排序展开两个简单的优化

  • 小区间优化(当排序元素小于10可以转换为插入排序,减少递归的深度,减少开销)

将代码中注释的部分放开即可以实现小区间优化

  • 三数取中(避免出现向一边递归,算法退化为N^2的情况)
int QuZhong(int* arr, int first, int last)//优化,三数取中
{
	int z = (first + last) / 2;
	int a = arr[first];
	int b = arr[z];
	int c = arr[last];
	if (b > c)
	{
		if (c > a)
			return last;
		else
		{
			if (a < b)
				return first;
			else
				return z;
		}
	}
	else
	{
		if (b > a)
			return z;
		else
		{
			if (a < c)
				return first;
			else
				return last;
		}
	}
}

1. 快速排序整体的综合性能和使用场景都是比较好的,所以才敢叫快速排序
2. 时间复杂度:O(N*logN)
3. 空间复杂度:O(logN)
4. 稳定性:不稳定
 

归并排序

归并排序是一种典型的分治思想,将大问题拆分成不能再分割的小问题。思路在将待排序不断对半分割分割,分割到不能再分割时将相邻两个组合并为一个有序的大组,再将两个相邻的两个大组合并为一个有序的大大组……

这里依然是提供两个版本的实现,递归版和非递归版

这里非递归的实现,是直接将单个元素视为递归下来不能再分割的最小单位,实现往上回归的部分

//归并排序
void MergeSort(int* a, int begin, int end);//递归版本
void MergeSortNonR(int* a, int begin, int end);//非递归版本

void MergeSort(int* a, int begin, int end)//归并排序
{
	int n = end - begin + 1;
	if (n < 2) return;
	int min = begin + n / 2;
	MergeSort(a, begin, min - 1);
	MergeSort(a, min, end);
	int* arr = (int*)malloc(sizeof(int) * n);
	int left = begin;
	int right = min;
	int i = 0;
	while (left < min && right <= end)
	{
		if (a[left] < a[right])
		{
			arr[i++] = a[left++];
		}
		else
		{
			arr[i++] = a[right++];
		}
	}
	if (left == min) {
		while (right <= end) {
			arr[i++] = a[right++];
		}
	}
	else
	{
		while (left < min) {
			arr[i++] = a[left++];
	 	}
	}
	i = 0;
	for (int y = begin; y <= end; y++)
	{
		a[y] = arr[i++];
	}
	free(arr);
}

void MergeSortNonR(int* a, int begin, int end)//非递归版本
{
	int gap = 1;
	int n = end - begin + 1;
	int* arr = (int*)malloc(sizeof(int) * n);
	while (gap < n)
	{
		for (int y = 0; y < n; y += gap * 2)
		{
			int left = y;
			int right = y + gap;
			if (right >= n) {
				for (int z = left; z < n; z++)
				{
					arr[z] = a[z];
				}
				break;
			}
			int i = y;
			int end1 = y + gap;
			int end2 = right + gap;
			if (end2 >= n)
				end2 = n;
			while (left < end1 && right < end2)
			{
				if (a[left] < a[right])
				{
					arr[i++] = a[left++];
				}
				else
				{
					arr[i++] = a[right++];
				}
			}
			if (left == end1) {
				while (right < end2) {
					arr[i++] = a[right++];
				}
			}
			else
			{
				while (left < end1) {
					arr[i++] = a[left++];
				}
			}
		}
		for (int z = 0; z < n; z++)
		{
			a[z] = arr[z];
		}
		gap *= 2;
	}
	free(arr);
}

1. 归并的缺点在于需要O(N)的空间复杂度,归并排序的思考更多的是解决在磁盘中的外排序问题。
2. 时间复杂度:O(N*logN)
3. 空间复杂度:O(N)
4. 稳定性:稳定
 

计数排序

计数排序是一种非比较的排序,适用于对整数进行排序。思想是得到序列中的最大值和最小值,计算得到中间最多有多少个元素,遍历一遍序列,将元素映射到计数表中,再从计数表中依次读取出元素。

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

void CountSort(int* a, int n)
{
	int min = a[0];
	int max = a[0];
	for (int i = 1; i < n; i++)
	{
		if (a[i] < min)
			min = a[i];
		else if (a[i] > max)
			max = a[i];
	}
	int countN = max - min + 1;
	int* arr = (int*)calloc(countN, sizeof(int));
	for (int i = 0; i < n; i++)
	{
		arr[a[i] - min]++;
	}
	int y = 0;
	for (int i = 0; i < countN&&y<n; i++)
	{
		int sz = arr[i];
		while (sz--)
		{
			a[y++] = i + min;
		}
	}
}

1. 计数排序在数据范围集中时,效率很高,但是适用范围及场景有限。
2. 时间复杂度:O(MAX(N,范围))
3. 空间复杂度:O(范围)
4. 稳定性:稳定

桶排序和基数排序

桶排序和基数排序不常用,这里只简单介绍思想不介绍具体实现

桶排序工作的原理是将数组分到有限数量的桶里。每个桶再分别排序,最后依次把各个桶中的记录列出来记得到有序序列

基数排序是一种非比较型整数排序算法,其原理是将整数按位数切割成不同的数字,然后按每个位数分别比较

排序算法的比较

 

 

结语

排序这部分重点掌握不同算法的思想,把握一趟是如何实现的,其他每一趟都是一样的思想

以上便是今天的全部内容。如果有帮助到你,请给我一个免费的赞。

因为这对我很重要。

编程世界的小比特,希望与大家一起无限进步。

感谢阅读!

  • 29
    点赞
  • 30
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值