C语言实现排序算法

文章目录

  • 前言
  • 一、插入排序
    • 1.1 直接插入排序
    • 1.2 直接插入排序特性
    • 1.3 希尔排序
    • 1.4 希尔排序特性
  • 二、选择排序
    • 2.1 基本思想
    • 2.2 直接选择排序
    • 2.3 直接选择排序特性
    • 2.4 堆排序
    • 2.5 堆排序特性
  • 三、交换排序
    • 3.1 冒泡排序
    • 3.2 冒泡排序特性
    • 3.3 快速排序
    • 3.4 递归实现快速排序
    • 3.4.1 左右指针法
    • 3.4.2 挖坑法
    • 3.4.3 前后指针法
    • 3.4.4 快排的优化
    • 3.5 非递归实现快速排序
    • 3.6 快速排序特性
  • 四、归并排序
    • 4.1 递归实现
    • 4.2 非递归实现
  • 四、计数排序
    • 4.1 计数排序特性
  • 五、排序算法复杂度及稳定性分析

前言

这里实现几种常见的排序算法:
C语言实现排序算法_第1张图片

一、插入排序

把待排序的记录按其关键码值的大小逐个插入到一个已经排好序的有序序列中,直到所有的记录插入完为止,得到一个新的有序序列

1.1 直接插入排序

当插入第i个元素时,前面的i个元素已经排好序,此时array[i]与array[i-1],array[i-2],…进行比较,找到插入位置将array[i]插入,原来位置上的元素顺序后移
C语言实现排序算法_第2张图片具体代码实现:

typedef int ISType;

void InsertSort1(ISType* nums, int size)
{
	int end2 = 0;
	int end1 = end2 + 1;
	while (end1 < size)
	{
		if (nums[end1] < nums[end2])
		{
			ISType temp = nums[end1];
			while (end2 >= 0 && temp < nums[end2])
			{
				nums[end2 + 1] = nums[end2];
				end2--;
			}
			nums[end2 + 1] = temp;
		}
		end2++;
		end1 = end2+1;	
	}
}

调用直接插入排序:

int main()
{
	int nums[] = { 3,5,15,36,38,44,47,26,27,2,46,4,19,50,48 };
	int size = sizeof(nums) / sizeof(nums[0]);
	InsertSort(nums, size);
	for (int i = 0; i < size; i++)
	{
		printf("%d ", nums[i]);
	}
	return 0;
}

运行结果:
在这里插入图片描述

1.2 直接插入排序特性

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

1.3 希尔排序

希尔排序法的基本思想是:先选定一个整数,把待排序文件中所有记录分成个组,所有距离为的记录分在同一组内,并对每一组内的记录进行排序。然后,取,重复上述分组和排序的工作。当到达=1时,所有记录在统一组内排好序
C语言实现排序算法_第3张图片C语言实现排序算法_第4张图片C语言实现排序算法_第5张图片

具体代码实现:

void ShellSort(int* nums, int size)
{
	int gap = size;
	while (gap > 1)
	{
		gap = (gap / 3) + 1;
		int end2 = 0;
		int end1 = end2 + gap;
		while (end1 < size)
		{
			if (nums[end1] < nums[end2])
			{
				ISType temp = nums[end1];
				while (end2 >= 0 && temp < nums[end2])
				{
					nums[end2 + gap] = nums[end2];
					end2 -= gap;
				}
				nums[end2 + gap] = temp;
			}
			end2++;
			end1 = end2 + gap;
		}
	}
}

1.4 希尔排序特性

  1. 希尔排序是对直接插入排序的优化
  2. 当gap>1时都是预排序,目的时让数组更接近有序。当gap==1时,数组已经接近有序了,这样就会很快,对整体而言,可以达到优化的效果
  3. 希尔排序的时间复杂度不好计算,因为gap的取值方法很多,导致很难去计算。
    *时间复杂度平均为O(N^1.3), 最坏O(log3(N)N)( 以3为底N的对数)
  4. 稳定性:不稳定

二、选择排序

2.1 基本思想

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

2.2 直接选择排序

  • 在元素集合array[i]—array[n-1]中选择值最大(小)的数据元素
  • 若它不是这组元素中的最后一个(第一个)元素,则将它与这组元素中的最后一个(第一个元素)交换
  • 在剩余的array[i]—array[n-2](array[i+1]—array[n-1])集合中,重复上述步骤,直到集合剩余1个元素

举个栗子(第一趟的排序过程)

原始序列:49、38、65、97、76、13、27、49

1)在进行选择排序过程中分成有序和无序两个部分,开始都是无序序列

结果:49、38、65、97、76、13、27、49

2)从无序序列中取出最小的元素13,将13同无序序列第一个元素交换,此时产生仅含一个元素的有序序列,无序序列减一

结果:{13、} {38、65、97、76、49、27、49}

3)从无序序列中取出最小的元素27,将27同无序序列第一个元素交换,此时产生仅两个元素的有序序列,无序序列减一

结果:{13、27、} {65、97、76、49、38、49}
。。。。。

具体代码实现:

void SelectSort(int* nums, int size)
{
	for (int i = 0; i < size - 1; i++)
	{
		int min = nums[i];
		int j = 0,tip = 0;
		for (j = i+1; j < size; j++)
		{
			if (min > nums[j])
			{
				tip = j;
				min = nums[j];
			}
		}
		if (min < nums[i])
		{
			int temp = nums[i];
			nums[i] = nums[tip];
			nums[tip] = temp ;
		}
	}
}

2.3 直接选择排序特性

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

2.4 堆排序

堆排序是指利用堆这种数据结构所设计的一种排序算法,它是选择排序的一种。通过堆来进行选择数据。需要注意的是排升序要建大堆,排降序建小堆

基本思想:

  1. 首先将待排序的数组构造成一个大堆,此时,整个数组的最大值就是堆结构的顶端
  2. 将顶端的数与末尾的数交换,此时,末尾的数为最大值,剩余待排序数组个数为n-1
  3. 将剩余的n-1个数再构造成大堆,再将顶端数与n-1位置的数交换,如此反复执行,便能得到有序数组

构造大堆在之前关于《C语言实现堆》介绍过,这里就不多介绍了,数交换后,左右子树还是大堆,所以可以使用向下调整算法再次建成大堆

具体代码实现:

void swap(int* a, int* b)
{
	int temp = *a;
	*a = *b;
	*b = temp;
}
AdjustDown(int* nums, int size, int parent)
{
	int child = parent * 2 + 1;
	while (child < size)
	{
		if (child+1 < size  && nums[child] < nums[child + 1])
			child = child + 1;
		
		if (nums[child] > nums[parent])
		{
			swap(&nums[child], &nums[parent]);
		}
		parent = child;
		child = parent * 2 + 1;
	}	

}
void HeapSort(int* nums, int size)
{
	//建大堆
	for (int i = (size - 1 - 1) / 2; i >= 0; i--)
	{
		AdjustDown(nums, size, i);
	}
	for (int n = size-1; n >= 1; n--)
	{
		swap(&nums[0], &nums[n]);
		AdjustDown(nums, n - 1, 0);
	}
}

2.5 堆排序特性

  1. 堆排序使用堆来选数,效率高了很多
  2. 时间复杂度O(N*logN)
  3. 空间复杂度O(1)
  4. 稳定性:不稳定

三、交换排序

基本思想:所谓交换,就是根据序列中两个记录键值的比较结果来对换这两个记录在序列中的位置,交换排序的特点是:将键值较大的记录向序列的尾部移动,键值较小的记录向序列的前部移动。

3.1 冒泡排序

可以点开冒泡排序的动图,查看冒泡排序的轨迹:

  1. 从下标为0开始,依次与后一位相比较,谁大谁在后
  2. 第一轮完成交换,最后一个元素必定是集合最大的元素
  3. 再依次进行n-1、n-2…个元素的比较,最后得到有序数组

具体代码实现:

void swap(int* a, int* b)
{
	int temp = *a;
	*a = *b;
	*b = temp;
}

void BubbleSort(int* nums, int size)
{
	for (int i = 0; i < size - 1; i++)
	{
		for (int j = 0; j < size - i -1 ; j++)
		{
			if (nums[j] > nums[j + 1])
			{
				swap(&nums[j] ,&nums[j + 1]);
			}
		}
	}
}

3.2 冒泡排序特性

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

3.3 快速排序

基本思想: 任取待排序元素序列中的某元素作为基准值,按照该排序码将排序集合分割成两子序列,左子序列中所有元素均小于基准值,右子序列中所有元素均大于基准值,然后最左右子序列重复该过程,直到所有元素都排列在相应位置上为止

3.4 递归实现快速排序

3.4.1 左右指针法

C语言实现排序算法_第6张图片

  1. 以第一个元素为基准值key,L从下标为0++,R从下标为size-1--
  2. R先出发,直到遇见比key小的数停下来
  3. L再出发,直到遇见比key大的数停下来,交换L和R的值
  4. 直到L和R相遇,交换key和相遇点的值
  5. key会到达自己的位置,以key为中点,分为左右子序列,重复上述操作,直到没有子序列为止

具体代码实现:

void swap(int* a, int* b)
{
	int temp = *a;
	*a = *b;
	*b = temp;
}
void QuickSort(int* nums, int begin, int end)
{
	if (end <= begin )
	{
		return ;
	}
	int left = begin;
	int right = end;
	int key = left;
	while (left < right)
	{
		while (left < right && nums[right] >= nums[key])
		{
			--right;
		}
		while (left < right && nums[left] <= nums[key])
		{
			++left;
		}
		swap(&nums[left], &nums[right]);
	}
	int meet = left;
	swap(&nums[key], &nums[left]);
	//递归
	QuickSort(nums,begin, meet);
	QuickSort(nums,meet+1, end);
}

3.4.2 挖坑法

C语言实现排序算法_第7张图片

  1. 取数组第一个位置保存在key变量中,L从下标为0++,R从下标为size-1--
  2. R先出发,直到遇见比key小的数停下来,并将R所指向的数交换到key所指向的位置中(坑位)
  3. L再出发,直到遇见比key大的数停下来,并将L所指向的数交换到R之前所交换的"坑位"
  4. 直到L和R在坑位相遇,将key保存的值放入“坑位"
  5. key会到达自己的位置,以key为中点,分为左右子序列,重复上述操作,直到没有子序列为止

具体代码实现:

void QuickSort2(int* nums, int begin, int end)
{
	if (end <= begin)
	{
		return;
	}
	int left = begin;
	int right = end;
	int key = begin;
	while (left < right)
	{
		while (left < right && nums[right] >= nums[key])
		{
			--right;
		}
		nums[left] = nums[right];
		while (left < right && nums[left] <= nums[key])
		{
			++left;
		}
		nums[right] = nums[left];

	}
	nums[left] = nums[key];
	int meet = left;

	QuickSort2(nums, begin, meet);
	QuickSort2(nums, meet + 1, end);
}

3.4.3 前后指针法

C语言实现排序算法_第8张图片

  1. 定义prev和cur开始一前一后,key指向数组第一个元素
  2. cur先出发找比key所指值小的数,找到之后停下来
  3. prev++,交换prev和cur所指向的值
  4. 再继续重复上述操作,直到cur到达数组尾,再交换key和prev所指向的值
  5. key会到达自己的位置,以key为中点,分为左右子序列,重复上述操作,直到没有子序列为止

具体代码实现:

void QuickSort3(int* nums, int begin, int end)
{
	if (end <= begin)
	{
		return;
	}
	int prev = begin;
	int cur = prev+1;
	int keyi = begin;
	while (cur <= end)
	{
		if (nums[cur] < nums[keyi] && ++prev != cur)
		{
			swap(&nums[cur], &nums[prev]);
		}
		++cur;
	}
	swap(&nums[keyi], &nums[prev]);
	int meet = prev;
	QuickSort3(nums, begin, meet);
	QuickSort3(nums, meet + 1, end);
}

3.4.4 快排的优化

  1. 三数取中法选key
  2. 递归到小的子区间时,可以考虑使用插入排序

1.三数取中法选key
C语言实现排序算法_第9张图片C语言实现排序算法_第10张图片

所以key的指向的值越接近中位数越接近二分,key最好是位于数组元素的中间位置,这样可以避免最坏的情况发生

解决方案:加入一个三数取中函数,然后交换首元素和所取中位数的位置,key依然指向第一个元素

具体代码实现: (挖坑法为例)

//交换
void swap(int* a, int* b)
{
	int temp = *a;
	*a = *b;
	*b = temp;
}
//三数取中
int GetMidIndex(int* nums, int left, int right)
{
	int mid = (left + right) >> 1;
	if (nums[left] < nums[mid])
	{
		if (nums[mid] < nums[right])
		{
			return mid;
		}
		else if (nums[left] > nums[right])
		{
			return left;
		}
		else
		{
			return right;
		}
	}
	else
	{
		if (nums[mid] > nums[right])
		{
			return mid;
		}
		else if (nums[left] < nums[right])
		{
			return left;
		}
		else
		{
			return right;
		}
	}
}
//挖坑法快排
void QuickSort2(int* nums, int begin, int end)
{
	int mid = GetMidIndex(nums, begin, end);
	swap(&nums[begin], &nums[mid]);
	if (end <= begin)
	{
		return;
	}
	int left = begin;
	int right = end;
	int key = begin;
	while (left < right)
	{
		while (left < right && nums[right] >= nums[key])
		{
			--right;
		}
		nums[left] = nums[right];
		while (left < right && nums[left] <= nums[key])
		{
			++left;
		}
		nums[right] = nums[left];

	}
	nums[left] = nums[key];
	int meet = left;

	QuickSort2(nums, begin, meet);
	QuickSort2(nums, meet + 1, end);
}

2.小的子区间,使用插入排序

  1. 如果这个子区间数据较多,继续选择key单躺,分割子区间分治递归
  2. 如果这个子区间数据较少,去分治递归很浪费

解决方案: 设定一个子区间的范围,到达这个范围时,不再递归而是使用插入排序

具体代码实现: (挖坑法为例)

void QuickSort2(int* nums, int begin, int end)
{
	int mid = GetMidIndex(nums, begin, end);
	swap(&nums[begin], &nums[mid]);


	if (end <= begin)
	{
		return;
	}
	//子区间范围大于10时,继续递归分治
	if (end - begin > 10)
	{
		int left = begin;
		int right = end;
		int key = begin;
		while (left < right)
		{
			while (left < right && nums[right] >= nums[key])
			{
				--right;
			}
			nums[left] = nums[right];
			while (left < right && nums[left] <= nums[key])
			{
				++left;
			}
			nums[right] = nums[left];

		}
		nums[left] = nums[key];
		int meet = left;

		QuickSort2(nums, begin, meet);
		QuickSort2(nums, meet + 1, end);
	}
	//子区间小于10,使用插入排序
	else
	{
		InsertSort(nums + begin, end - begin + 1);
	}
}

3.5 非递归实现快速排序

基本思想: 用Stack存储数据模拟递归过程

  1. 将总区间放入栈中,如:10个数,先放入0,后放入9
  2. 再依次出栈9和0使用右区间right和左区间left去接收
  3. 三位取中,使用挖坑法得到meet所在的位置,然后再将meet之前和meet之后的子区间放入栈中,依次循环先出右区间,再出左区间,反反复复
  4. 直到栈为空,排序就完成了

具体代码实现:(这里的栈沿用之前文章中C语言实现的栈)

void swap(int* a, int* b)
{
	int temp = *a;
	*a = *b;
	*b = temp;
}

//三数取中
int GetMidIndex(int* nums, int left, int right)
{
	int mid = (left + right) >> 1;
	if (nums[left] < nums[mid])
	{
		if (nums[mid] < nums[right])
		{
			return mid;
		}
		else if (nums[left] > nums[right])
		{
			return left;
		}
		else
		{
			return right;
		}
	}
	else
	{
		if (nums[mid] > nums[right])
		{
			return mid;
		}
		else if (nums[left] < nums[right])
		{
			return left;
		}
		else
		{
			return right;
		}
	}
}

void QuickSortNonR(int* nums, int left, int right)
{
	Stack st;
	StackInit(&st);
	StackPush(&st, left);   //先入栈
	StackPush(&st, right);
	//栈为空时结束
	while (!StackEmpty(&st))
	{
		int left, right;
		right = StackTop(&st);
		StackPop(&st);

		left = StackTop(&st);
		StackPop(&st);
		
		int mid = GetMidIndex(nums, left, right);
		swap(&nums[left], &nums[mid]);
		int key = left;
		while (left < right)
		{
			while (left < right && nums[right] >= nums[key])
			{
				--right;
			}
			nums[left] = nums[right];
			while (left < right && nums[left] <= nums[key])
			{
				++left;
			}
			nums[right] = nums[left];

		}
		nums[left] = nums[key];
		int meet = left;
		if (left < meet - 1)
		{
			StackPush(&st, left);
			StackPush(&st, meet-1);
		}
		if (meet + 1 < right)
		{
			StackPush(&st, meet+1);
			StackPush(&st, right);
		}

	}
	StackDestrory(&st);
}

3.6 快速排序特性

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

四、归并排序

基本思想: 是建立在归并操作上的一种有效的排序算法,该算法是采用分治法的一个非常典型的应用。将已有序的子序列合并,得到完全有序的序列;即先使每个子序列有序,再使子序列段间有序。若将两个有序表合并成一个有序表,称为二路归并
C语言实现排序算法_第11张图片

4.1 递归实现

具体代码实现:

void _MergeSort(int* nums, int left, int right, int* temp)
{
	if (left >= right)
		return;
	int mid = (left + right) >> 1;
	//分解
	_MergeSort(nums, left, mid, temp);
	_MergeSort(nums, mid+1, right, temp);
	//归并
	int begin1 = left, end1 = mid;
	int begin2 = mid+1, end2 = right;
	int i = left;
	while (begin1 <= end1 && begin2 <= end2)
	{
		if (nums[begin1] < nums[begin2])
		{
			temp[i++] = nums[begin1++];
		}
		else
		{
			temp[i++] = nums[begin2++];
		}
	}
	while (begin1 <= end1)
	{
		temp[i++] = nums[begin1++];
	}
	while (begin2 <= end2)
	{
		temp[i++] = nums[begin2++];
	}
	//拷贝
	for (int j = left; j <= right; j++)
	{
		nums[j] = temp[j];
	}
}
void MergeSort(int* nums, int size)
{
	int* temp = (int*)malloc(sizeof(int) * size);
	if (temp == NULL)
	{
		perror("malloc fail\n");
		exit(-1);
	}
	_MergeSort(nums, 0, size - 1, temp);
	free(temp);
	temp = NULL;
}

4.2 非递归实现

具体代码实现:


void _MergeSort2(int* nums, int* temp, int begin1, int end1, int begin2, int end2)
{
	int j = begin1;
	int i = begin1;
	while (begin1 <= end1 && begin2 <= end2)
	{
		if (nums[begin1] < nums[begin2])
		{
			temp[i++] = nums[begin1++];
		}
		else
		{
			temp[i++] = nums[begin2++];
		}
	}
	while (begin1 <= end1)
	{
		temp[i++] = nums[begin1++];
	}
	while (begin2 <= end2)
	{
		temp[i++] = nums[begin2++];
	}

	//拷贝
	for (; j <= end2; j++)
	{
		nums[j] = temp[j];
	}

}
void MergeSort2(int* nums, int size)
{
	int* temp = (int*)malloc(sizeof(int) * size);
	if (temp == NULL)
	{
		perror("malloc fail\n");
		exit(-1);
	}
	int gap = 1;
	while (gap < size)
	{
		for (int i = 0; i < size; i += 2 * gap)
		{
			//如果第二个小区间不存在就不需要归并了,结束本次循环
			int begin1 = i, end1 = i + gap - 1, begin2 = i + gap, end2 = i +\
			2 * gap - 1;
			if (begin2 >= size)
				break; 
			//如果第二个小区间存在,但是第二个小区间不够gap个,结束位置越界了,需要修正
			if (end2 >= size)
			{
				end2 = size - 1;
			}
			_MergeSort2(nums, temp, begin1, end1, begin2, end2);
		}
		gap *= 2;
	}
}

四、计数排序

基本思想: 是对哈希直接定址法的变形应用

  1. 统计相同元素出现次数
  2. 根据统计的结果将序列回收到原来的序列中

具体代码实现:

void CountSort(int* nums, int size)
{
	int min = nums[0], max = nums[0];
	for (int i = 0; i < size; i++)
	{
		if (nums[i] > max)
			max = nums[i];
		if (nums[i] < min)
			min = nums[i];
	}
	int range = max - min + 1;
	int* countnums = (int*)calloc(range,sizeof(int));
	for (int i = 0; i < size; i++)
	{
		countnums[nums[i] - min]++;
	}
	int i = 0;
	for (int j = 0; j < range; ++j)
	{
		while (countnums[j]--)
		{
			nums[i++] = j + min;
		}
	}
}

4.1 计数排序特性

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

五、排序算法复杂度及稳定性分析

C语言实现排序算法_第12张图片C语言实现排序算法_第13张图片

稳定性: 是指数组中相同的值,排完序以后,相对顺序不变,就是稳定的,否则就是不稳定的

你可能感兴趣的:(c++,数据结构,c语言)