5.排序算法 [C语言]

插入

InsertSort

//1.先写单趟
//2.再写多趟
//画图!!
void InsertSort(int* a, int n)
{
	assert(a);
	assert(n >= 0);
	//多躺排序
	//end下标从0~n-2
	for (int i = 0; i < n-1; i++)
	{
		//1.先写单趟排序
	//把tmp插入到数组[0,end]的有序区间中
		int end = i;
		int tmp = a[end+1];
		while (end >= 0)
		{
			if (tmp < a[end])
			{
				a[end + 1] = a[end];
				end--;
			}
			else
			{
				break;
			}
		}
		//end移到-1时,需要把tmp放到a[0]里面
		//tmp>=a[end]也需要把tmp放到a[end]后面
		a[end + 1] = tmp;
	}
}

//时间复杂度O(N^2)

image-20220216122449357

ShellSort

gap越大,大的数和小的1数可以更快的挪动到对应的方向
gap越大,越不接近有序

gap越小,大的数和小的1数可以更慢的挪动到对应的方向
gap越小,越接近有序

gpa==1 就是插入排序,接近O(N)

1.预排序->接近有序(先分组,对分组的数据插入排序)
2.直接插入排序

void ShellSort(int* a, int n)
{
	int gap = n;
	while (gap > 1) // n/3/3/3.../3 == 1  -》 3^x = n  x就是这个while循环跑的次数
	{
		//+1保证最后依次gap是1
		gap = (gap / 3 + 1);
		//先预排序,gap=1时直接插入排序
		// 最坏的情况:逆序,gap很大的时-》O(N)
		// ...
		//gap很小时本来应该是O(N*N),但是经过前面的预排序,数组已经很接近有序的,所这里还是O(N)
		for (int i = 0; i < n - gap; i++)
		{
			int end = i;
			int tmp = a[end + gap];
			while (end >= 0)
			{
				if (tmp < a[end])
				{
					a[end + gap] = a[end];
					end -= gap;
				}
				else
				{
					break;
				}
				//end移到<0时,需要把tmp放到a[end+gap]里面
				//tmp>=a[end]也需要把tmp放到a[end+gap]后面
				a[end + gap] = tmp;
			}
		}
	}
}
//平均复杂度O(N^1.3)  因为gap不确定
// gap为3的情况:O(log3(N) * N) 这里log3(N)是以3为底N的对数

image-20220216132015236

对比测试

void TestOP()
{
	srand(time(0));
	const int N = 100000;
	int* a1 = (int*)malloc(sizeof(int) * N);
	int* a2 = (int*)malloc(sizeof(int) * N);
	int* a3 = (int*)malloc(sizeof(int) * N);
	int* a4 = (int*)malloc(sizeof(int) * N);
	int* a5 = (int*)malloc(sizeof(int) * N);
	int* a6 = (int*)malloc(sizeof(int) * N);

	for (int i = 0; i < N; ++i)
	{
		a1[i] = rand();
		a2[i] = a1[i];
		a3[i] = a1[i];
		a4[i] = a1[i];
		a5[i] = a1[i];
		a6[i] = a1[i];

	}

	int begin1 = clock();
	InsertSort(a1, N);
	int end1 = clock();

	int begin2 = clock();
	ShellSort(a2, N);
	int end2 = clock();

	//int begin3 = clock();
	//SelectSort(a3, N);
	//int end3 = clock();

	//int begin4 = clock();
	//HeapSort(a4, N);
	//int end4 = clock();

	//int begin5 = clock();
	//QuickSort(a5, 0, N - 1);
	//int end5 = clock();

	//int begin6 = clock();
	//MergeSort(a6, N);
	//int end6 = clock();

	printf("InsertSort:%d\n", end1 - begin1);
	printf("ShellSort:%d\n", end2 - begin2);
	//printf("SelectSort:%d\n", end3 - begin3);
	//printf("HeapSort:%d\n", end4 - begin4);
	//printf("QuickSort:%d\n", end5 - begin5);
	//printf("MergeSort:%d\n", end6 - begin6);

	free(a1);
	free(a2);
	free(a3);
	free(a4);
	free(a5);
	free(a6);
}

InsertSort:1118
ShellSort:8

注意:要在release版本下测试才公平,这样编译器优化发挥到极致

选择

SelectSort

void SelectSort(int* a, int n)
{
	int left = 0;
	int right = n - 1;
	while (left < right)
	{
		int minIndex = left;
		int maxIndex = left;
		//选出最大的值和最小的值
		for (int i = left; i <= right; i++)
		{
			if (a[i] < a[minIndex])
			{
				minIndex = i;
			}
			if (a[i] > a[maxIndex])
			{
				maxIndex = i;
			}
		}
		Swap(&a[left], &a[minIndex]);
		//极端情况下需要修正,第一个数恰好是最大的数,但它又要进行交换,改变了maxIndex的位置
		if (left == maxIndex)
		{
			maxIndex = minIndex;
		}
		Swap(&a[right], &a[maxIndex]);
		++left;
		--right;
	}
}
//N N-2 N-4
//O()

HeapSort

//排大根堆
void AdjustDown(int* a, int n, int root)
{
	int parent = root;
	int child = parent * 2 + 1;
	while (child < n)
	{
		//选出大的孩子
		//先检查是否有越界再访问
		if (child+1 < n && a[child+1] > a[child])
		{
			++child;
		}
		if (a[child]>a[parent])
		{
			Swap(&a[child], &a[parent]);
			parent = child;
			child = parent * 2 + 1;
		}
		else
		{
			break;
		}
	}
}

void HeapSort(int* a, int n)
{
	//排升序,建大堆
	for (int i = (n-1-1)/2; i >= 0 ; i--)
	{
		AdjustDown(a, n, i);
	}
	//选出最大的数,交换到最后去
	int end = n - 1;
	while (end > 0)
	{
		Swap(&a[0], &a[end]);
		//选次大的
		//左右子树均为大堆,只需一次向下调整就能从n-1选出原来n里面次大的数
		AdjustDown(a, end, 0);
		end--;
	}

对比测试

InsertSort:1144
ShellSort:8
SelectSort:5162
HeapSort:7

交换

BubbleSort

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

	//或者优化一下
	for (int end = n; end > 0; end--)
	{
		int exchange = 0;
		for (int i = 1; i < end; i++)
		{
			if (a[i - 1] > a[i])
			{
				Swap(&a[i - 1], &a[i]);
				exchange = 1;
			}
		}
		if (exchange == 0)
		{
			//说明没有发生交换,也就是前面的恰好是有序的
			break;
		}
	}
}

冒泡和插入相比谁更好?

顺序有序,一样好
接近有序,插入更好

QuickSort

单趟排序
选出一个key,一般是最左边的,或者是最右边的
key放到他正确的位置上,左边的比key小,右边的比key大

选左边做key,让right先走
right找小,left找大,交换
直到相遇

void QuickSort(int* a, int begin, int end)
{
	if (begin >= end)
		return;
	int left = begin, right = end;
	int keyi = left;
	while (left < right)
	{
		//要保证相遇位置的值一定比key先小,要让right先走
		//找小
		//left < right防止越界 
		while (left < right && a[right] >= a[keyi])
		{
			--right;
		}
		//找大
		while (left < right && a[left] <= a[keyi])
		{
			++left;
		}
		//交换
		Swap(&a[left], &a[right]);
	}
	int meeti = left;
	Swap(&a[keyi], &a[left]);

    //分治思想
	//[begin,meeti-1] meeti [meeti+1,end]
	QuickSort(a, begin, meeti - 1);
	QuickSort(a, meeti+1, end);
}

PartSort

void QuickSort(int* a, int begin, int end)
{
	//区间有多个值时才继续,一个值都没有就return
	if (begin >= end)
		return;
	int keyi = PartSort1(a, begin, end);

	//[begin,keyi-1] keyi [keyi+1,end]
	QuickSort(a, begin, keyi - 1);
	QuickSort(a, keyi +1, end);
}
//时间复杂度理想情况 O(N*logN)
//最坏情况 O(N^2) 
//空间复杂度O(logN) 树的高度

用QuickSort去排一个有序的序列,比InsertSort还慢
InsertSort:1143
ShellSort:7
SelectSort:5092
HeapSort:7
QuickSort:1309
左右指针法
//单趟排序 hoare版本 -- 左右指针法
int PartSort1(int* a, int left, int right)
{
	int keyi = left;
	while (left < right)
	{
		//要保证相遇位置的值一定比key先小,要让right先走
		//找小
		//left < right防止越界 
		while (left < right && a[right] >= a[keyi])
		{
			--right;
		}
		//找大
		while (left < right && a[left] <= a[keyi])
		{
			++left;
		}
		//交换
		Swap(&a[left], &a[right]);
	}
	Swap(&a[keyi], &a[left]);
	return left;
}
挖坑法
//挖坑法
int PartSort2(int* a, int left, int right)
{
	//先把left作为key保存起来,left的位置天生是一个坑
	int key = a[left];
	while (left < right)
	{
		//找小
		while (left < right && a[right] >= key)
		{
			--right;
		}
		//放到左边的hole中,右边就形成新的hole
		a[left] = a[right];

		//找大
		while (left < right && a[left] <= key)
		{
			++left;
		}
		//放到右边的hole中,左边就形成新的hole
		a[right] = a[left];
	}
	//相遇
	a[left] = key;
	return left;
}

image-20220216221213007

前后指针法
cur prev 一前一后
cur去找比keyi位置小的值
找到小之后,++prev,再交换prev和cur位置的值
直到数组尾
最后交换prev和keyi位置的值
    
思想: 把小的往左边留,大的往右边留,prev++是为了跟上cur

image-20220217184512797

//前后指针法
int PartSort3(int* a, int left, int right)
{
	//三数取中优化
	int midIndex = GetMidIndex(a, left, right);
	Swap(&a[left], &a[midIndex]);
	int keyi = left;
	int prev = left, cur = left + 1;
	while (cur <= right)
	{
		if (a[cur] < a[keyi] && ++prev != cur)
		{
			//prev++;
			//prev和cur相同时,交换也没影响
			Swap(&a[prev], &a[cur]);
		}
		cur++;
	}
	Swap(&a[keyi], &a[prev]);
	return prev;
}

优化

三数取中优化

三数取中 key如果越接近中位数,效率就越高

int GetMidIndex(int* a, int left, int right)
{
	int mid = (left + right) >> 1;//位运算效率比除法高
	//left mid right
	if (a[left] < a[mid])
	{
		if (a[mid] < a[right])
		{
			return mid;
		}
		else if (a[left] > a[right])
		{
			return left;
		}
		else
		{
			return right;
		}
	}
	else //a[left] > a[mid]
	{
		if (a[mid] > a[right])
		{
			return mid;
		}
		else if (a[left] < a[right])
		{
			return left;
		}
		else
		{
			return right;
		}
	}
}
//单趟排序 hoare版本 -- 左右指针法
int PartSort1(int* a, int left, int right)
{
	int midIndex = GetMidIndex(a, left, right);
	Swap(&a[left], &a[midIndex]);

	int keyi = left;
	while (left < right)
	{
		//要保证相遇位置的值一定比key先小,要让right先走
		//找小
		//left < right防止越界 
		while (left < right && a[right] >= a[keyi])
		{
			--right;
		}
		//找大
		while (left < right && a[left] <= a[keyi])
		{
			++left;
		}
		//交换
		Swap(&a[left], &a[right]);
	}
	Swap(&a[keyi], &a[left]);
	return left;
}
三数取中优化后,恰好是有序的数据
InsertSort:1134
ShellSort:8
SelectSort:5144
HeapSort:7
QuickSort:1

1000w的随机数据排序:
InsertSort:0
ShellSort:1217
SelectSort:0
HeapSort:2530
QuickSort:593
小区间优化
void QuickSort(int* a, int begin, int end)
{
	//区间有多个值时才继续,一个值都没有就return
	if (begin >= end)
		return;

	//1.如果这个子区间数据较多,继续选key单趟,分割子区间分治递归
	//2.如果子区间数据较少,再去分治递归不太划算
	if (end-begin > 10)
	{
		int keyi = PartSort3(a, begin, end);

		//[begin,keyi-1] keyi [keyi+1,end]
		QuickSort(a, begin, keyi - 1);
		QuickSort(a, keyi + 1, end);
	}
	
	else
	{
		//考虑到分割中间的较小子区间
		InsertSort(a + begin, end - begin + 1);
	}
}

面试时写快排,不用写三数取中的优化,小区间优化
写完了可以讲一下优化方案
小区间优化优化效果不明显, 本质是减少递归树的最后几层,减少递归调用的次数,但是使用插入排序也是有消耗的
三数取中 本质是防止最坏的情况发生

排数据时,可以根据数据量的大小调整小区间优化,用ShellSort HeapSort等也行

非递归

递归
现代编译器优化很好,性能已经不是大问题
最大的问题–>递归深度太深,程序本身没问题,但是栈空间不够,导致栈溢出

改成非递归:

1.直接改循环–>斐波那契数列
2.树遍历非递归和快排非递归,只能用Stack存储数据模拟非递归过程

void QuickSortNonR(int* a, int begin, int end)
{
	Stack st;
	StackInit(&st);
	StackPush(&st, begin);
	StackPush(&st, end);

	while (!StackEmpty(&st))
	{
		int left, right;
		//先入左后入右,出来时就先出右后出左
		right = StackTop(&st);
		StackPop(&st);
		left = StackTop(&st);
		StackPop(&st);

		int keyi = PartSort3(a, left, right);
		if (left<keyi-1)
		{
			StackPush(&st, left);
			StackPush(&st, keyi-1);
		}
		if (keyi+1<right)
		{
			StackPush(&st, keyi + 1);
			StackPush(&st, right);
		}
	}
	StackDestroy(&st);
}

对比测试

InsertSort:1137
ShellSort:8
SelectSort:5149
HeapSort:8
QuickSort:5

    //优化过后:
InsertSort:1129
ShellSort:8
SelectSort:5112
HeapSort:8
QuickSort:1

归并

MergeSort

假设如果左边右边均有序了,那么一归并,整体就有序了
取小的尾插到下面的数据,直到一个区间结束
再把另一个区间剩下的数据尾插到最后
归并完之后拷回去

分治思想

image-20220217215653597

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

	//两段有序子区间归并到tmp再拷贝回去
	int begin1 = left, end1 = mid;
	int begin2 = mid+1, end2 = right;
	int i = left;
	while (begin1 <= end1 && begin2 <= end2)
	{
		if (a[begin1] < a[begin2])
		{
			tmp[i++] = a[begin1++];
		}
		else
		{
			tmp[i++] = a[begin2++];
		}
	}
	//一个先结束,就把另一个的全放进去
	while (begin1 <= end1)
	{
		tmp[i++] = a[begin1++];
	}
	while (begin2 <= end2)
	{
		tmp[i++] = a[begin2++];
	}
	//拷贝回去
	for (int j = left; j <= right; j++)
	{
		a[j] = tmp[j];
	}
}
void MergeSort(int* a, int n)
{
	int* tmp = (int*)malloc(sizeof(int) * n);
	if (tmp == NULL)
	{
		printf("malloc fail\n");
		exit(-1);
	}
	_MergeSort(a, 0, n - 1, tmp);
	free(tmp);
}
时间复杂度O(N*logN)
每一层归并都是N
logN层

空间复杂度O(N)
InsertSort:1141
ShellSort:7
SelectSort:5085
HeapSort:7
QuickSort:1
MergeSort:8

MergeSortNonR

一一归并 二二归并 四四归并

image-20220217225434686

void _Merge(int* a, int* tmp, int begin1, int end1, int begin2, int end2)
{
	//两段有序子区间归并到tmp再拷贝回去
	int i = begin1;
	int j = begin1;
	while (begin1 <= end1 && begin2 <= end2)
	{
		if (a[begin1] < a[begin2])
		{
			tmp[i++] = a[begin1++];
		}
		else
		{
			tmp[i++] = a[begin2++];
		}
	}
	//一个先结束,就把另一个的全放进去
	while (begin1 <= end1)
	{
		tmp[i++] = a[begin1++];
	}
	while (begin2 <= end2)
	{
		tmp[i++] = a[begin2++];
	}
	//拷贝回去
	for (; j <= end2; j++)
	{
		a[j] = tmp[j];
	}
}
void MergeSortNonR(int* a, int n)
{
	int* tmp = (int*)malloc(sizeof(int) * n);
	if (tmp == NULL)
	{
		printf("malloc fail\n");
		exit(-1);
	}

	int gap = 1;//gap=1表示一一归并
	while (gap < n)
	{
		for (int i = 0; i < n; i += 2 * gap)
		{
			//[i,i+gap-1] [i+gap, i+2*gap-1]
			_Merge(a, tmp, i, i + gap - 1, i + gap, i + 2 * gap - 1);
		}
		gap *= 2;
	}
	free(tmp);
}

程序小bug:

1.最后一个小组归并时,第二个小区间不存在,不需要归并
2.最后一个小组归并时,第二个小区间存在,但不够gap个
3.最后一个小组归并时,第一个小区间不够gap个,不需要归并
13问题可以合并,第二个小区间不存在即可判断

//非递归
void MergeSortNonR(int* a, int n)
{
	int* tmp = (int*)malloc(sizeof(int) * n);
	if (tmp == NULL)
	{
		printf("malloc fail\n");
		exit(-1);
	}

	int gap = 1;//gap=1表示一一归并
	while (gap < n)
	{
		for (int i = 0; i < n; i += 2 * gap)
		{
			//[i,i+gap-1] [i+gap, i+2*gap-1]
			int begin1 = i, end1 = i + gap - 1, begin2 = i + gap, end2 = i + 2 * gap - 1;
			//分析知,如果第二个小区间不存在就不需要归并
			if (begin2 >= n)
			{
				break;
			}
			//如果第二个小区间存在,但是不够gap个,结束位置越界
			//修正一下
			if (end2 >= n)
			{
				end2 = n - 1;
			}
			_Merge(a, tmp, begin1, end1, begin2, end2);
		}
		gap *= 2;
	}
	free(tmp);
}

image-20220217232321910

非比较排序

CountSort

绝对映射

统计出每个数出现的次数
A[i]是几就对Count数组对应位置的值++

统计出次数再排序

相对映射

映射时减去最小的那个值

image-20220218124130379

void CountSort(int* a, int n)
{
	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* count = (int*)malloc(sizeof(int) * range);
	if (count == NULL)
	{
		printf("malloc fail\n");
		exit(-1);
	}
	//数组初始化为0
	memset(count, 0, sizeof(int) * range);
	//统计次数
	for (int i = 0; i < n; i++)
	{
		//相对映射
		count[a[i] - min]++;
	}
	//把数据写回去 count数组里放的是数据出现的次数
	int i = 0;
	for (int j = 0; j < range; j++)
	{
		while (count[j]--)
		{
			a[i++] = j + min;
		}
	}
	free(count);
}
//时间复杂度O(2*N+range)
//只适合一组数据,数据的范围比较集中
//空间复杂度O(range)
//如果数据集中,效率很高
//并且只适合整数 浮点数,字符串不行 因为没有用到比较

内排序

数据量相对少一些,可以放到内存中排序

外排序

数据量较大,内存中放不下,数据放到磁盘文件中排序

归并排序既可以用于内排序,也可以用于外排序

十亿个整数,放到文件A中,需要排序?
假设只有512M内存
10亿int 约等于4G (实际上小于4G)
每次读文件A的1/8 也就是512M左右到内存中,进行排序,然后写到一个小文件,再继续读1/8,重复过程
再将8个有序小文件归并排序

核心:在内存中排序效率会很高
在内存中排序时不能用归并,归并有O(N)的空间复杂度 用快排就行

总结

image-20220218124621778

稳定性

数组中相同的值,排完序以后,相对顺序不变,就是稳定的,否则就是不稳定的
可以做到稳定就是稳定

注意: 简单选择排序是不稳定的,很多书上都是错的
选择排序找到最大的后,会去交换,可能影响其他值

image-20220218125421203

希尔排序不稳定,相同的值在预排时有可能分到不同的组里面

快排不稳定,选的key要换到中间,有可能原来左边有和key相同的值,排完之后就被换过去了

冒泡插入归并是稳定

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值