数据结构——排序

目录

  • [1 排序相关概念](#1 排序相关概念)
  • [2 内部排序](#2 内部排序)
    • [2.1 直接插入排序](#2.1 直接插入排序)
    • [2.2 希尔排序](#2.2 希尔排序)
    • [2.3 简单选择排序](#2.3 简单选择排序)
    • [2.4 堆排序](#2.4 堆排序)
    • [2.5 冒泡排序](#2.5 冒泡排序)
    • [2.6 快速排序](#2.6 快速排序)
    • [2.7 归并排序](#2.7 归并排序)
    • [2.8 计数排序](#2.8 计数排序)
  • [3 排序的总结](#3 排序的总结)

1 排序相关概念

  • 排序 :对数据进行排序,就是将数据以某个值为标准,按照升序或降序的方式重新组织起来。比如,将 10 6 7 1 3 9 4 2 这一组数据进行升序排序,就会得到 1 2 3 4 6 7 9 10 的结果
  • 稳定性 :排序前和排序后,数据的相对位置没有变化,那么该排序就是稳定的,否则是不稳定的。比如,数据 6 6 1 5 2 4 排序完后为 1 2 4 5 6 6 ,两个 6 的相对位置没有发生变化,该排序是稳定的
  • 内部排序:排序时,全部数据都位于内存中
  • 外部排序:排序时,内存中无法存放所有外存中的数据,需要在内存,外存之间来回移动元素的一种排序

2 内部排序

内部排序主要包括了插入排序,选择排序,交换排序,归并排序,计数排序
插入排序 包括了 直接插入排序希尔排序
选择排序 包括了简单选择排序堆排序
交换排序 包括了冒泡排序快速排序

2.1 直接插入排序

直接插入排序的思想是:
取出数组中的值 nums[i],将 nums[i] 与其前面的值(nums[0],nums[1]...nums[i-1])进行比较,遇到比 nums[i] 要大或要小的值,将它向后进行移动,当遇到值不再大于或小于 nums[i] 的值,将 nums[i] 插入至它的后方,直到数组有序为止

时间复杂度:O(N2)
空间复杂度:O(1)
稳定性:稳定

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 (a[end] > tmp) //比较tmp(原end+1)与end指向的元素,若前面更大,则将前面的元素后移
			{
				a[end + 1] = a[end];
				end--;
			}
			else
			{
				break;
			}
		}

		a[end + 1] = tmp; //当前面元素更小,插入至后方
	}
}

2.2 希尔排序

希尔排序是基于直接插入排序来实现的,它的思想是:
设定一个数 gap 来对数据进行分组,对这些组内的数据使用直接插入排序,排完所有组内的数据后,使 gap 进行缩小,继续进行以上的操作,当最终 gap 为 1 时,就可以使得数据有序

希尔排序的时间复杂度测量起来十分复杂,所以在此给出大致的时间复杂度
时间复杂度:O(N1.3)
空间复杂度:O(1)
稳定性:不稳定

c 复制代码
void ShellSort(int* a, int n)
{
	int gap = n;
	while (gap > 1)
	{
		gap = gap / 2;
		//并行地对一组进行直接插入排序
		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;
		}
	}
}

2.3 简单选择排序

一般来说,简单选择排序的思想是:
每次选出剩下数据当中最大或最小的,将其放在当前区间的开头,直至最后有序

在这里给出简单选择排序的优化版本,它的基本思想是:
每次选出当前区间[begin, end]内最大和最小的数据,将最小的数据与当前区间最前面的值进行交换,将最大的数据与当前区间最后方的值交换,交换完成后,缩小区间并重复上述操作,直到最后有序

时间复杂度:O(N2)
空间复杂度:O(1)
稳定性:不稳定

c 复制代码
void Swap(int* a, int* b)
{
	int tmp = *a;
	*a = *b;
	*b = tmp;
}
//选择排序
void SelectSort(int* a, int n)
{
	//区间[begin, end]
	int begin = 0;
	int end = n - 1;
	while (begin < end)
	{
		int mini = begin;
		int maxi = begin;
		for (int i = begin + 1;i <= end;++i)
		{
			if (a[i] < a[mini]) //寻找最小值的下标
			{
				mini = i;
			}

			if (a[i] > a[maxi]) //寻找最大值的下标
			{
				maxi = i;
			}
		}
		
		if (begin == maxi) //检查最大值是否与要替换的值一致
			maxi = mini;
		Swap(&a[begin], &a[mini]);
		Swap(&a[end], &a[maxi]);
		begin++;
		end--;
	}
}

2.4 堆排序

堆排序需要使用到一种数据结构: ,它本质上是顺序存储的完全二叉树

堆分为小堆和大堆
小堆:每个子树的根都小于它的左右孩子
大堆:每个子树的根都大于它的左右孩子

堆排序的思想是:
将待排序的数据集合看成是一个堆,通过对其不停地进行建堆,调整来使得堆顶值为当前区间内最小或最大的值,将该值与当前区间的最后一个值进行交换,就可以使该值位于最终位置上,重复以上操作直到最后数据有序


时间复杂度:O(NlogN)
空间复杂度:O(N)
稳定性:不稳定

c 复制代码
void Swap(int* a, int* b)
{
	int tmp = *a;
	*a = *b;
	*b = tmp;
}
//向下调整
void AdjustDown(int* a, int n, int root)
{
	while (root * 2 + 1 < n)
	{
		int child = 2 * root + 1; //左孩子
		if (child + 1 < n && a[child] > a[child + 1]) //比较左右孩子,取小的
		{
			child = child + 1;
		}
		if (a[child] < a[root]) //孩子比父亲小,父亲下调
		{
			Swap(&a[child], &a[root]);
			root = child;
		}
		else
		{
			break;
		}
	}
}
//堆排序
void HeapSort(int* a, int n)
{
	//调整为大堆
	for (int i = (n - 2) / 2;i >= 0;--i)
	{
		AdjustDown(a, n, i);
	}

	//排序
	for (int i = 1;i < n;++i)
	{
		Swap(&a[0], &a[n - i]);
		AdjustDown(a, n - i, 0);
	}
}

2.5 冒泡排序

冒泡排序的思想是:
通过比较前后两个值来获得它们的大小关系,若顺序不符合要求,则将两个值交换,当一趟排序结束时,就会有一个值位于最终位置上,对剩下的值进行同样的操作直到有序


时间复杂度:O(N2)
空间复杂度:O(1)
稳定性:稳定

c 复制代码
void Swap(int* a, int* b)
{
	int tmp = *a;
	*a = *b;
	*b = tmp;
}
void BubbleSort(int* nums, int n)
{
	for (int i = 0;i < n;++i)
	{
		int flag = 0; //假设已经有序
		for (int j = 0;j < n - i - 1;++j)
		{
			if (nums[j] > nums[j + 1])
			{
				Swap(&nums[j], &nums[j + 1]);
				flag = 1; //进行了交换,不是有序
			}
		}

		if (flag == 0)
			break;
	}
}

2.6 快速排序

快速排序的思想是:
选定一个值作为 key,不断地将其它值与 key 作比较,将比 key 小的值留在它的左侧,将比 key 大的值留在它的右侧,以此来划分左右区间,对左右区间进行同样的操作,直到最后有序

由于快速排序是递归实现的,所以排序的操作雷同,在这里只给出第一趟划分

时间复杂度:O(NlogN)
空间复杂度:O(logN)
稳定性:不稳定

c 复制代码
void Swap(int* a, int* b)
{
	int tmp = *a;
	*a = *b;
	*b = tmp;
}
//快速排序
void QuickSort(int* nums, int begin, int end)
{
	//只有一个元素,或者没有元素时
	if (begin >= end)
	{
		return;
	}

	int keyi = begin;
	int left = begin;
	int right = end;

	while (left < right)
	{
		//找小
		while (left < right && nums[right] >= nums[keyi])
		{
			right--;
		}

		//找大
		while (left < right && nums[left] <= nums[keyi])
		{
			left++;
		}

		Swap(&nums[left], &nums[right]);
	}

	Swap(&nums[left], &nums[keyi]);
	keyi = left;

	QuickSort(nums, begin, keyi - 1);
	QuickSort(nums, keyi + 1, end);
}

快速排序的优化方案

1.在进行快速排序时,数据可能已经有序,此时快速排序的效率会被降低至 O(N2) 的量级,为了避免这种问题的出现,我们可以使用三数取中来解决,具体方法为:
选定区间最左侧和最右侧的值,求出可能的中间值的位置,将三个值进行比较,选出中间值,将中间值与第一个数据进行交换后再进行快速排序

2.在快速排序接近结束时区间内的数据量较少,继续使用快速排序效率会有所下降,可以使用直接插入排序来进行相应优化

优化后的快速排序

c 复制代码
void Swap(int* a, int* b)
{
	int tmp = *a;
	*a = *b;
	*b = tmp;
}
//三数取中值
int findMid(int* nums, int begin, int end)
{
	int midi = (begin + end) / 2;
	if (nums[begin] < nums[midi])
	{
		if (nums[midi] < nums[end])
			return midi;
		else if (nums[end] > nums[begin])
			return end;
		else
			return begin;
	}
	else
	{
		if (nums[midi] > nums[end])
			return midi;
		else if (nums[end] > nums[begin])
			return begin;
		else
			return end;
	}
}
//快速排序
void QuickSort(int* nums, int begin, int end)
{
	//只有一个元素,或者没有元素时
	if (begin >= end)
	{
		return;
	}

	//处理有序数组,找中间值
	int midi = findMid(nums, begin, end);
	Swap(&nums[midi], &nums[begin]);

	//对最后的排序进行优化
	if (end - begin + 1 < 10)
	{
		InsertSort(nums + begin, end - begin + 1);
	}

	int keyi = begin;
	int left = begin;
	int right = end;

	while (left < right)
	{
		//找小
		while (left < right && nums[right] >= nums[keyi])
		{
			right--;
		}

		//找大
		while (left < right && nums[left] <= nums[keyi])
		{
			left++;
		}

		Swap(&nums[left], &nums[right]);
	}

	Swap(&nums[left], &nums[keyi]);
	keyi = left;

	QuickSort(nums, begin, keyi - 1);
	QuickSort(nums, keyi + 1, end);
}

2.7 归并排序

如果有一组数据,它的左区间有序,它的右区间有序,那么就可以通过将左右区间的值进行比较的方式来使得数组有序

而归并排序的思想就是:
通过将一组数据划分为左右两个区间,对左右两个区间的数据分别进行排序,再合并左右两个区间的数据来使得一组数据有序

最后一趟排序如图,由于归并排序是递归实现的,所以它和其它几趟的排序类似
时间复杂度:O(NlogN)
空间复杂度:O(1)
稳定性:稳定

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

	int mid = (begin + end) / 2;

	//[begin, mid][mid+1, end]
	//左区间归并
	_MergeSort(nums, tmp, begin, mid);
	//右区间归并
	_MergeSort(nums, tmp, mid + 1, end);

	//左右区间有序,进行归并
	int leftBegin = begin, leftEnd = mid;
	int rightBegin = mid + 1, rightEnd = end;
	int i = begin;

	while (leftBegin <= leftEnd && rightBegin <= rightEnd)
	{
		if (nums[leftBegin] <= nums[rightBegin])
		{
			tmp[i++] = nums[leftBegin++];
		}
		else
		{
			tmp[i++] = nums[rightBegin++];
		}
	}

	while (leftBegin <= leftEnd)
	{
		tmp[i++] = nums[leftBegin++];
	}

	while (rightBegin <= rightEnd)
	{
		tmp[i++] = nums[rightBegin++];
	}

	memcpy(nums + begin, tmp + begin, (end - begin + 1) * sizeof(int));

}

void MergeSort(int* nums, int n)
{
	int* tmp = (int*)malloc(sizeof(int) * n);
	if (tmp == NULL)
	{
		perror("malloc fail");
		return;
	}

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

	free(tmp);
	tmp = NULL;
}

2.8 计数排序

计数排序是一种不需要进行比较的排序方法,它的思想为:
将一组数据中每个值的出现次数记录下来,再通过它们的出现次数,向原数据写入对应数量的数据

在实现时,为了防止数据差过大,动态开辟过多的空间导致空间浪费,选择使用相对位置进行映射,做法为:
找出数组中最大和最小的值,确定要开辟的空间大小,再使用数组内的每个数减去最小值,获得相对位置,通过相对位置记录出现次数,再通过出现次数 + 最小值的方式,还原值并将值写入原数组

注:这里的蓝色箭头表示相对位置和值的改变情况

时间复杂度:O(N + range) -- range为count数组的大小
空间复杂度:O(range)
稳定性:稳定

c 复制代码
void CountSort(int* nums, int n)
{
	int max = nums[0];
	int min = nums[0];

	//找最大最小值
	for (int i = 1;i < n;++i)
	{
		if (nums[i] < min)
			min = nums[i];
		if (nums[i] > max)
			max = nums[i];
	}

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

	//记录出现次数
	for (int i = 0;i < n;++i)
	{
		count[nums[i] - min]++;
	}

	//排序
	int j = 0;
	for (int i = 0;i < range;++i)
	{
		while (count[i]--)
		{
			nums[j++] = i + min;
		}
	}
}

3 排序的总结

相关推荐
tumu_C3 小时前
无用知识研究:用sfinae实现函数模板的overload [一]
开发语言·c++·算法
西望云天3 小时前
Trie树实战:三道典型例题
数据结构·算法·icpc
dragoooon344 小时前
[优选算法专题三.二分查找——NO.20搜索插入位置 ]
算法·leetcode·动态规划
红尘客栈24 小时前
ELK 企业级日志分析系统实战指南
数据结构
hn小菜鸡4 小时前
LeetCode 1089.复写零
算法·leetcode·职场和发展
与己斗其乐无穷4 小时前
算法(一)双指针法
数据结构·算法·排序算法
努力努力再努力wz5 小时前
【C++进阶系列】:位图和布隆过滤器(附模拟实现的源码)
java·linux·运维·开发语言·数据结构·c++
eSoftJourney5 小时前
C 语言核心关键字与数据结构:volatile、struct、union 详解
c语言·数据结构
(❁´◡`❁)Jimmy(❁´◡`❁)5 小时前
【Trie】 UVA1401 Remember the Word
算法·word·图论