【数据结构】排序

  排序就是按照某种比较方式对一堆数据进行处理,处理之后的数据变得有序。

  • 本文所讲的排序都是升序的。

直接插入排序 — InsertSort

   基本思想:将一个数插入到有序序列中,保持序列的有序,得到一个新的有序序列
在这里插入图片描述

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)
			{
				a[end + 1] = a[end];
				end--;
			}
			else
				break;
		}
		a[end + 1] = tmp;
		//走到这里,有两种情况:
		//1.break跳出
		//2.while循环结束
	}
}

时间复杂度 O( N 2 N^2 N2)
最好情况:序列顺序有序或接近有序,此时时间复杂度为O(N)
最坏情况:序列逆序或接近逆序,每次插入时,每个位置数据都要往后挪,此时时间复杂度为O( N 2 N^2 N2)
空间复杂度O(1)

希尔排序 — ShellSort

  上面我们说到,直接插入排序在序列逆序或接近逆序时,非常坏。一个叫希尔的人对直接插入排序进行了优化。
  预排序:让大的数快速到后面,小的数快速到前面,从而让数组接近有序。
   基本思想:先进行预排序,最后进行一次直接插入排序。选一个gap,把序列分成gap个组,每个组进行预排序,然后gap减小,重复上述的分组和排序,当gap == 1 时,就是直接插入排序。

在这里插入图片描述

void ShellSort(int* a, int n)
{
	int gap = n;
	while (gap > 1)
	{
		gap = gap / 3 + 1;
		//在取gap值时,可以和我写的一样,也可以用下面这种写法
		// gap = gap/2; 
		//但要保证除到最后gap == 1
		//gap/3+1这种写法可以让让gap循环次数减少一些
		for (int i = 0; i < gap; i++) //把序列分为gap组
		{
			for (int j = i; j < n - gap; j += gap)//对每一组经行预排序 
			{
				int end = j;
				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;
			}
		}
	}
}

  有人对上面的写法进行了优化

void ShellSort(int* a, int n)
{
	int gap = n;
	while (gap > 1)
	{
		gap = gap / 3 + 1;
		for (int j = 0; j < n - gap; j++) 
		//这种写法和上面的写法一样,上面的写法是依次对每一组进行预排序,这里是每一组同时来进行预排序
		{
			int end = j;
			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;
		}
		
	}
}

  算一下时间复杂度:
  1.刚开始时,gap较大,此时的时间复杂度为O(N)
  2.当gap较小时,原序列因为预排序已经接近有序了,所以时间复杂度也为O(N)

时间复杂度 O( N 1.3 N^{1.3} N1.3)要推导比较麻烦,直接记结论
空间复杂度O(1)

直接选择排序 — SelectSort

  基本思想:遍历原序列,每次找出一个最大值或最小值,这里写代码时可以优化一下,每次找一对最大值和最小值

void Swap(int* a, int* b)
{
	int tmp = *a;
	*a = *b;
	*b = tmp;
}
void SelectSort(int* a, int n)
{
	int begin = 0;
	int end = n - 1;
	while (begin < end)//begin == end,就剩最后一个数据,他就在他正确的位置上
	{
		int min = begin;
		int max = begin;
		for (int i = begin; i <= end; i++)
		{
			if (a[i] < a[min])
				min = i;
			if (a[i] > a[max])
				max = i;
		}
		Swap(&a[begin], &a[min]);
		if (begin == max) // 如果最大值在begin位置上,max = min;
			max = min;
		Swap(&a[end], &a[max]);
		begin++;
		end--;
	}
}

时间复杂度 O( N 2 N^2 N2)直接选择排序是一个非常差的排序,即使数据有序,它也要不断遍历序列,也就是说不论最好还是最坏,他的时间复杂度都是O(N^2)
空间复杂度O(1)

堆排序 — HeapSort

  基本思想:利用堆的特性,进行排序

void AdjustDown(int* a, int n, int parent)
{
	int child = parent * 2 + 1;
	while (child < n)
	{
		if (child + 1 < n && a[child] < a[child + 1])
			child++;
		if (a[parent] < a[child])
		{
			Swap(&a[parent], &a[child]);
			parent = child;
			child = 2 * parent + 1;
		}
		else
			break;
	}
}

void HeapSort(int* a, int n)
{
	for (int i = (n - 1 - 1) / 2; i >= 0; i--) // 建堆 O(logN)
	{
		AdjustDown(a, n, i);
	}
	int end = n - 1;
	while (end > 0)
	{
		Swap(&a[0], &a[end]);
		AdjustDown(a, end, 0);
		end--;
	}
}

时间复杂度O(N* l o g 2 N log^N_2 log2N)
空间复杂度O(1)

冒泡排序 — BubbleSort

  基本思想:遍历n-1次,每次把最大的值交换到最后

void BubbleSort(int* a, int n)
{
	for (int i = 0; i < n - 1; i++)
	{
		int flag = 1;
		for (int j = 0; j < n - 1 - i; j++)
		{
			if (a[j] > a[j + 1])
			{
				Swap(&a[j], &a[j + 1]);
				flag = 0;
			}
		}
		if (flag)
			break;
	}
}

时间复杂度O( N 2 N^2 N2)
优化之后,如果顺序有序,时间复杂度为O(N)
空间复杂度O(1)

快速排序 — QuickSort

  基本思路:把一个数排到它在原序列的最终位置上,那这个数就需要参与之后的排序了,重复递归这个过程,最终整个序列就排好序了。

hoare版本(左右指针法)

  基本思路:右边找小,左边找大,交换,重复这个过程。相遇时交换keyi和left/right

//hoare
void QuickSort1(int* a, int left, int right)
{
	//如果区间不存在或者只有一个数的时候,就可以返回了
	if (left >= right)
		return;
	int keyi = left;
	int beign = left;
	int end = right;
	while (beign < end)
	{
		while (beign < end && a[end] >= a[keyi])//右边找小
			end--;
		while (beign < end && a[beign] <= a[keyi])//左边找大
			beign++;
		Swap(&a[beign], &a[end]);
	}
	Swap(&a[keyi], &a[end]);

	//[left, end-1] end [end+1, right]
	QuickSort1(a, left, end - 1);
	QuickSort1(a, end + 1, right);

}

  这里是怎么能保证最后相遇时的数(a[begin/end])要小于a[keyi]的呢
只要end先走就可以保证
1.begin不动end动,end往左走找小的,结果没找到,和begin相遇了,此时的begin和end在经过上轮交换之后,a[begin]<a[keyi]
2.end不动begin动,begin往右走找大的,结果没找到,和end相遇了,此时的end已经找到小了,所以a[begin] < a[keyi]

挖坑法

  挖坑法是对hoare法的思路上的优化
  基本思路:povit是坑,右边找小,a[povit] = a[end], end成为新的坑,右边找大,a[povit] = a[begin],begin成为新的坑,重复这个过程。

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

	//[left, end-1] end [end+1, right]
	QuickSort2(a, left, end - 1);
	QuickSort2(a, end + 1, right);
}

前后指针法

  基本思路:curr不断遍历序列,把比a[keyi]小的换到左边,大的换到右边

void QuickSort3(int* a, int left, int right)
{
	if (left >= right)
		return;
	int keyi = left;
	int prev = left;
	int curr = left + 1;
	while (curr <= right)
	{
		if (a[curr] < a[keyi] && ++prev != curr)
			Swap(&a[curr], &a[prev]);
		curr++;
	}
	Swap(&a[keyi], &a[prev]);

	//[left, prev-1] prev [prev+1, right]
	QuickSort3(a, left, prev - 1);
	QuickSort3(a, prev + 1, right);
}

优化

三数取中

  快排在一些特殊情况下会很坏,比如一个有序序列,从右边找小就会遍历一遍序列,此时快排的时间复杂度为O( N 2 N^2 N2),在加了三数取中之后,就不会有这么坏的情况了。

int GetMid(int* a, int left, int right)
{
	int mid = (left + right) / 2;
	if (a[mid] > a[left])
	{
		if (a[right] > a[mid])
			return mid;
		else // 此时mid是最大数的下标,中间数是left和right的较大值
			return a[left] > a[right] ? left : right;
	}
	else // a[mid] < a[left]
	{
		if (a[mid] > a[right])
			return  mid;
		else //此时mid是最小数的下标,中间数是left和right的较小值
			return a[left] > a[right] ? right : left;
	}
}
小区间优化

  快速排序的递归过程类似于一颗二叉树,二叉树的倒数一层的节点占总节点的50%,倒数二层的节点占总节点的25%,倒数三层的节点占总节点的12.5%,所以快速排序的最后三层递归占80%多,当递归到小的子区间时,可以使用其他排序,减少栈帧的消耗。
在这里插入图片描述

void QuickSort1(int* a, int left, int right)
{
	//如果区间不存在或者只有一个数的时候,就可以返回了
	if (left >= right)
		return;
	int index = GetMid(a, left, right);
	Swap(&a[index], &a[left]);
	int keyi = left;
	int beign = left;
	int end = right;
	while (beign < end)
	{
		while (beign < end && a[end] >= a[keyi])//右边找小
			end--;
		while (beign < end && a[beign] <= a[keyi])//左边找大
			beign++;
		Swap(&a[beign], &a[end]);
	}
	Swap(&a[keyi], &a[end]);

	//[left, end-1] end [end+1, right]
	//小区间优化
	if (end - 1 - left > 10)
		QuickSort1(a, left, end - 1);
	else
		InsertSort(a + left, end - left);
	if (right - end - 1 > 10)
		QuickSort1(a, end + 1, right);
	else
		InsertSort(a + end + 1, right - end);

}

快速排序非递归 — QuickSortNonR

int PartSort(int* a, int left, int right)
{
	int index = GetMid(a, left, right);
	Swap(&a[index], &a[left]);
	int keyi = left;
	int prev = left;
	int curr = left + 1;
	while (curr <= right)
	{
		if (a[curr] < a[keyi] && ++prev != curr)
			Swap(&a[curr], &a[prev]);
		curr++;
	}
	Swap(&a[keyi], &a[prev]);
	return prev;
}
void QuickSortNonR(int* a, int n)
{
	Stack st;
	StackInit(&st);
	StackPush(&st, n - 1);
	StackPush(&st, 0);
	while (!StackEmpty(&st))
	{
		int left = StackTop(&st);
		StackPop(&st);
		int right = StackTop(&st);
		StackPop(&st);
		int index = PartSort(a, left, right);
		// [left, index-1] index [index+1, right]
		if (right > index+1)
		{
			StackPush(&st, right);
			StackPush(&st, index + 1);
		}
		if (index - 1 > left)
		{
			StackPush(&st, index - 1);
			StackPush(&st, left);
		}

	}
	StackDestroy(&st);
}

时间复杂度O(N* l o g 2 N log^N_2 log2N)
空间复杂度O( l o g 2 N log^N_2 log2N)

归并排序 — MergeSort

  基本思路:如果两个序列时有序,那么将子序列合并,就可以得到完全有序的序列,即每个子序列有序,再使子序列段间有序。

void _MergeSort(int* a, int left, int right, int* tmp)
{
	if (left >= right)
		return;
	int mid = (left + right) / 2;
	_MergeSort(a, left, mid, tmp);
	_MergeSort(a, mid + 1, right, tmp);

	int begin1 = left, end1 = mid;
	int begin2 = mid + 1, end2 = right;
	int index = left;
	while (begin1 <= end1 && begin2 <= end2)
	{
		if (a[begin1] < a[begin2])
			tmp[index++] = a[begin1++];
		else
			tmp[index++] = a[begin2++];
	}
	while (begin1 <= end1)
		tmp[index++] = a[begin1++];
	while (begin2 <= end2)
		tmp[index++] = a[begin2++];
	memcpy(a + left, tmp + left, sizeof(int) * (right - left + 1));
}

void MergeSort(int* a, int n)
{
	int* tmp = (int*)malloc(sizeof(int)*n);
	if(!tmp)
	{
		perror("malloc fail");
		exit(-1);
	}
	_MergeSort(a, 0, n - 1, tmp);
	free(tmp);
}

归并排序非递归 — MergeSortNonR

void MergeSortNonR(int* a, int n)
{
	int* tmp = (int*)malloc(sizeof(int) * n);
	if (!tmp)
	{
		perror("malloc fail");
		exit(-1);
	}
	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;
			int index = i;
			// 因为 i < n, 所以begin1永远不会越界
			// end1, begin2, end2, 都可能会越界
			// 当end1/begin2越界时,右半区间就不存在,左边区间就不用动了
			// 当end2越界时,右半区间还有值,就要调整end2的位置
			
			//if(end1 >= n || begin2 >= n)
			if (begin2 >= n) //end1越界,那begin2一定越界
				break;
			if (end2 >= n)
				end2 = n - 1;
			while (begin1 <= end1 && begin2 <= end2)
			{
				if (a[begin1] <= a[begin2])
					tmp[index++] = a[begin1++];
				else
					tmp[index++] = a[begin2++];
			}
			while (begin1 <= end1)
				tmp[index++] = a[begin1++];
			while (begin2 <= end2)
				tmp[index++] = a[begin2++];
			memcpy(a + i, tmp + i, sizeof(int) * (end2 - i + 1));
		}
		gap *= 2;
	}
	free(tmp);
}

时间复杂度 O(N* l o g 2 N log^N_2 log2N)
空间复杂度 O(N)

稳定性

  稳定性是指两个相同的值,在排完序后,相对位置不变。

InsertSort:稳定 — 在比较时,比tmp大才往后挪,相等就放在后面。
ShellSort:不稳定 — 预处理时相同的值可能在不同的组中
SelectSort:不稳定
   例如: 3,3,1,…
   如果最小数是1,那3的相对位置就改变了
HeapSort:不稳定 — 如果堆中都是相同的数,在交换时,相对位置就变了
BubbleSort:稳定 — 前一个比后一个大,交换, 相等,不交换,相对位置不变
QuickSort:不稳定
   例如: 5,3,5,…,5…
   如果最左边的5是keyi,那么相对位置就会发生改变
MergeSort:稳定 — 左区间和右区间的值相等时,左区间先入

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

LRBORRR

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值