数据结构——排序

今日言:有朋自远方来,不亦乐乎!总有一天,我要让我正在学习的知识成为我的“朋”!

😋前言

今天介绍八大排序。

1. 排序的概念及其运用💬

1.1 排序的分类

在这里插入图片描述

2. 常见的7个比较排序💬

2.1 插入排序

2.1.1 直接插入排序
思路:

(以升序为例)将待排序的数据逐个插入到一个已经有序的序列当中,直到所有数据插入完成。我们认为第一个数是有序的,那么从第二个数开始往前插入。就像我们平时玩的扑克牌,我们一张一张地抓牌、理牌就是直接插入排序的思想。

图解:

在这里插入图片描述

总结:
  1. 对于直接插入排序而言,序列越接近有序,那么它的时间效率越高。
  2. 时间复杂度:O(N2)
  3. 空间复杂度:O(1)
  4. 稳定性1:稳定
代码实现
// 直接插入排序
void InsertSort(int* a, int sz)
{
	for (int i = 1; i < sz; ++i)
	{
		// 单趟排序
		int tmp = a[i];
		int end = i - 1;
		while (end >= 0)	
		{
			if (tmp < a[end])
			{
				a[end + 1] = a[end];
				end--;
			}
			else
				break;
		}
		a[end + 1] = tmp;
	}
}
2.1.2 希尔排序
思路:

(以升序为例)与直接插入排序类似,但不同的是:希尔排序先将序列分为n个小序列,然后分别将n个小序列以直接插入排序的方法排完序,再回头将这已经有序的n个小序列整合到一起。需要注意的是,小序列内的每两个数的间隔是gap,序列内数据并非左右紧密相连的。

图解:

在这里插入图片描述

总结:
  1. 希尔排序是直接插入排序的优化
  2. gap>1时的排序都是预排序,目的是让序列更加接近于有序。最后gap=1时的排序是真正让序列有序的。我们从直接插入排序的知识可以知道,当序列接近于有序的时候,其时间效率极高,因此希尔排序的性能很好。
  3. 时间复杂度:希尔排序的时间复杂度没有定论,因为对于gap的取值方法并没有最优解,这涉及数学问题。Shell的取法是gap=n/2,gap=gap/2,直到gap为1。后来Knuth提出取gap=gap/3+1,直到gap>1。还有很多取法,这些都可以,我们使用Knuth的方法,经过统计,它的时间复杂度从O(N^1.25)到O(1.6 *N^ 1.25)之间。大概可以记为O(N^1.3)。
  4. 稳定性:不稳定
代码实现:
// 希尔排序
void ShellSort(int* a, int n)
{
	int gap = n;
	while (gap > 1)
	{
		gap = gap / 3 + 1;
		for (int j = 0; j < gap; j++)
		{
			for (int i = j; i < n - gap; i += gap)
			{
				// 单趟排序
				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.2 选择排序

2.2.1 直接选择排序
思路:

(以升序为例)从一个序列中找出最小的和最大的那两个数,将其分别放在首位和末位,然后这两个数不再参与排序,在剩下的n-2个数中找出最小的和最大的那两个,将其放在首位和末位。以此类推…直到最终排序完成。(下图仅演示找最小的数)

图解:

在这里插入图片描述

总结:
  1. 效率不是很高,实际应用中不多。
  2. 时间复杂度:O(N2)
  3. 空间复杂度:O(1)
  4. 稳定性:不稳定
代码实现:
void Swap(int* i, int* j)
{
	int tmp = *i;
	*i = *j;
	*j = tmp;
}

// 选择排序
void SelectSort(int* a, int n)
{
	int left = 0;
	int right = n - 1;
	while (left < right)
	{
		int min = left;
		int max = right;

		for (int i = left; i <= right; i++)
		{
			if (a[i] < a[min])
				min = i;
			if (a[i] > a[max])
				max = i;	
		}

		Swap(&a[min], &a[left]);
		// 防止max在left的位置,被交换走了
		if (max == left)
			max = min;
		Swap(&a[max], &a[right]);
		left++;
		right--;
	}
}
2.2.2 堆排序
思路:

(以升序为例)充分利用堆的性质,建立大堆,就是选出最大的数,将其与最后一个数交换位置,就完成了一个数的排序。再重新建立大堆,以此类推…最终就可以得出一个升序序列。

图解:

在这里插入图片描述

总结:
  1. 效率较高

  2. 时间复杂度:O(N*log2N)

  3. 空间复杂度:O(1)

  4. 稳定性:不稳定

代码实现:
// 向下调整
void AdjustDown(HeapDataType* a, int size, int parent)
{
	int child = parent * 2 + 1;// 左孩子
	while (child < size)
	{
		// 判断左右孩子哪个大,大孩子是chlid
		if (child + 1 < size && a[child] < a[child + 1])
		{
			++child;
		}
		// 判断大孩子和父亲哪个大
		if (a[child] > a[parent])
		{
			Swap(a, child, parent);
			parent = child;
			child = parent * 2 + 1;
		}
		else
			break;
	}
}

// 堆排序
void HeapSort(int* a, int n)
{
	// 向下调整建大堆
	for (int i = (n - 2) / 2; i > 0; --i)
		AdjustDown(a, n, i);

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

2.3 交换排序

2.3.1 冒泡排序
思路:

(以升序为例)左右两个数比较交换,从头到尾,每遍历完一次后,就会将一个最大数放置在最后。重复遍历,即可完成排序。其特点是将数值较大的向序列的尾部移动,数值较小的向序列的前部移动。

图解:

在这里插入图片描述

总结:
  1. 易理解,但效率低
  2. 时间复杂度:O(N2)
  3. 空间复杂度:O(1)
  4. 稳定性:稳定
代码实现:
// 冒泡排序
void BubbleSort(int* a, int n)
{
	for (int i = 0; i < n - 1; ++i)
	{
		for (int j = 0; j < n - 1 - i; ++j)
		{
			if (a[j + 1] < a[j])
			{
				Swap(&a[j], &a[j + 1]);
			}
		}
	}
}
2.3.2 快速排序
思路:

(以升序为例)在序列中任意取一个值,将其作为基准值key,以key为中心分为左右两个序列,使key的左边全部比key小,key的右边全部比key大。再分别对左右两个序列同样操作。以此类推…直到最终排序完成。

图解:

快速排序有三种实现方法,分别是:

Hoare:
在这里插入图片描述

前后指针:
在这里插入图片描述

挖坑法:
在这里插入图片描述

总结:
  1. 快排存在一些问题,比如当我想要排升序,但序列是降序,此时快排的性能极低,因为每次取的key是序列的第一个,因为它本来是降序,key最后会被放在最后一个位置,也就是说这趟排序仅仅排好了key一个数的位置,它无法划分出左右两个子序列,因此效率很低。这种情况是可以有解决方案的,详细代码见下方代码实现区。
  2. 我们使用的是递归的方法,但是假若递归到一定程度(比如序列剩十几个数),此时若继续递归,将会很繁琐,尽管电脑程序自己运行,不用我们一步一步繁琐地走,但其会影响运行效率。因此,在序列剩余的数不多时,我们可以使用直接插入排序来代替剩下的递归。
  3. 快速排序的性能很好,使用场景也很多。
  4. 时间复杂度:O(N*log2N)
  5. 空间复杂度:O(log2N)
  6. 稳定性:不稳定
代码实现:
Hoare:
void QuickSort(int* a, int left, int right)
{
	if (left >= right)
		return;
	int begin = left;
	int end = right;
	// 一定要先走右边,后走左边,这样能保证key左侧小于key,右侧大于key
	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[left], &a[keyi]);
	keyi = left;
	QuickSort(a, begin, keyi - 1);
	QuickSort(a, keyi + 1, end);
}
前后指针:
// 快排前后指针
void QuickSort2(int* a, int left, int right)
{
	if (left >= right)
		return;

	int begin = left;
	int end = right;

	int keyi = left;
	int prev = left;
	int cur = left;
	while (cur++ < right)
	{
		if (a[cur] < a[keyi] && ++prev != cur)
			Swap(&a[cur], &a[prev]);
	}
	Swap(&a[keyi], &a[prev]);
	keyi = prev;
	QuickSort2(a, begin, keyi - 1);
	QuickSort2(a, keyi + 1, end);
}
挖坑法:
void QuickSort3(int* a, int left, int right)
{
	if (left >= right)
		return;

	int begin = left;
	int end = right;

	// 第一个数存在临时变量中,形成一个坑位
	int keyi = left;
	int key = a[left];
	int hole = left;

	while (left < right)
	{
		// 右边找小
		while (a[right] >= key && left < right)
			right--;
		a[hole] = a[right];
		hole = right;

		// 左边找大
		while (a[left] <= key && left < right)
			left++;
		a[hole] = a[left];
		hole = left;

	}
	a[hole] = key;
	keyi = hole;

	QuickSort3(a, begin, keyi - 1);
	QuickSort3(a, keyi + 1, end);
}
快排优化:
	// 方案一:随机选keyi
	int randi = left + (rand() % (right - left));
	Swap(&a[randi], &a[left]);

	// 三数取中
	int GetMidi(int* a, int left, int right)
	{
        int mid = (right + left) / 2;
        if (a[left] < a[mid])
        {
            if (a[left] > a[right])
                return left;
            else if (a[mid] < a[right])
                return mid;
            else
                return right;
        }
        else // a[left] >= a[mid]
        {
            if (a[left] < a[right])
                return left;
            else if (a[mid] > a[right])
                return mid;
            else
                return right;
        }
    }
	// 方案二:三数取中
	int Midi = GetMidi(a, left, right); 
	Swap(&a[Midi], &a[left]);
快排的非递归:
void QuickSortNonR(int* a, int left, int right)
{
	ST st;
	STInit(&st);
	STPush(&st, right);
	STPush(&st, left);

	while (!STEmpty(&st))
	{
		int begin = STTop(&st);
		STPop(&st);
		int end = STTop(&st);
		STPop(&st);

		int BEGIN = begin;
		int END = end;

		// Hoare
		int keyi = begin;
		while (begin < end)
		{
			while (a[end] >= a[keyi] && begin < end)
				end--;
			while (a[begin] <= a[keyi] && begin < end)
				begin++;
			Swap(&a[begin], &a[end]);
		}
		Swap(&a[begin], &a[keyi]);
		keyi = begin;

		if (keyi + 1 < END)
		{
			STPush(&st, END);
			STPush(&st, keyi + 1);
		}
		if (BEGIN < keyi - 1)
		{
			STPush(&st, keyi - 1);
			STPush(&st, BEGIN);
		}
	}
	STDestory(&st);
}

2.4 归并排序

基本思想:分治

2.4.1 归并排序
思路:

(以升序为例)我们设想一个方案:将一个序列的左右子序列分别排序至有序,然后再将两个子序列合并为一个总体有序的序列。这就是归并排序。其实就是将序列无限划分,例如8个数分为4+4,4又分为2+2,2再分为1+1,然后让让1和1合并成一个有序的2个数,再让2和2合并为一个有序的4个数,以此类推…就可以实现整个序列的排序。

图解:

在这里插入图片描述

总结:
  1. 缺点是需要O(N)的空间复杂度,它更多是用来解决在磁盘中的外排序问题。
  2. 时间复杂度:O(N*log2N)
  3. 空间复杂度:O(N)
  4. 稳定性:稳定
代码实现:
递归版本:
void _MergeSort(int* a, int begin, int end, int* tmp)
{
	if (begin >= end)
		return;
	int mid = (begin + end) / 2;
	// 分割
	int begin1 = begin;
	int end1 = mid;
	int begin2 = mid + 1;
	int end2 = end;
	_MergeSort(a, begin1, end1, tmp);
	_MergeSort(a, begin2, end2, tmp);

	// 合并
	int j = begin;
	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+begin, tmp+begin, sizeof(int) * (end - begin + 1));
}

// 归并排序
void MergeSort(int* a, int n)
{
	int* tmp = (int*)malloc(sizeof(int) * n);
	if (NULL == tmp)
	{
		perror("Malloc fail!");
		return;
	}
	_MergeSort(a, 0, n - 1, tmp);
}
非递归版本:
// 非递归归并
void MergeSortNonR(int* a, int n)
{
	int* tmp = (int*)malloc(sizeof(int) * n);
	if (NULL == tmp)
	{
		perror("malloc fail!");
		return;
	}
	int gap = 1;
	while (gap < n)
	{
		for (int i = 0; i < n; i += 2 * gap)
		{
			int begin1 = i, end1 = i + gap - 1;
			int begin2 = i + gap, end2 = i + 2 * gap - 1;

			if (end1 >= n || begin2 >= n)
			{
				break;
			}
			else if (end2 >= n)
			{
				end2 = n - 1;
			}

			int j = i;
			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));
		}
		gap *= 2;
	}
}

3. 非比较排序💬

3.1 计数排序

思路:

统计每个数出现的次数,然后从该序列的最小数值到最大数值遍历,将出现次数不为0的数依次填入序列中。

图解:

在这里插入图片描述

代码实现:
// 计数排序
void CountSort(int* a, int n)
{
	// 1. 统计每个数据出现的次数
	int max = a[0], min = a[0];
	for (int i = 0; i < n; i++)
	{
		if (a[i] > max)
			max = a[i];
		if (a[i] < min)
			min = a[i];
	}
	int range = max - min + 1;
	int* countA = (int*)calloc(range, sizeof(int));
	if (!countA)
	{
		perror("calloc fail\n");
		return;
	}
	memset(countA, 0, sizeof(int) * range);
	for (int i = 0; i < n; i++)
	{
		countA[a[i] - min]++;
	}

	// 2. 排序
	int j = 0;
	for (int i = 0; i < range; i++)
	{
		while (countA[i]--)
		{
			a[j++] = i + min;
		}
	}
	free(countA);
}

4. 排序总结💬

各大排序的时间复杂度、空间复杂度、稳定性对比:

排序方法平均情况最好情况最坏情况辅助空间稳定性
直接插入排序O(n2)O(n)O(n2)O(1)稳定
希尔排序O(nlog2n) ~ O(n2)O(n1.3)O(n2)O(1)不稳定
简单选择排序O(n2)O(n2)O(n2)O(1)不稳定
堆排序O(nlog2n)O(nlog2n)O(nlog2n)O(1)不稳定
冒泡排序O(n2)O(n)O(n2)O(1)稳定
快速排序O(nlog2n)O(nlog2n)O(n2)O(nlog2n) ~ O(n2)不稳定
归并排序O(nlog2n)O(nlog2n)O(nlog2n)O(n)稳定

  1. 稳定性解释:所谓稳定,就是指两个数据,在排序前后它们之间的先后次序不变。 ↩︎

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值