【数据结构】排序

CSDN:@与黎明同行
gitee:@code

先单趟在再整体

插入排序

直接插入排序(InsertSort)

思想

  • 举例——>摸牌

在有序的数组中摸一张牌插入到前面合适的位置中去
插入排序的逻辑,摸到一张牌后和最后的比较大小,大则插入在后面,使得总体有序。

实现

  • 单趟

先排单趟,比较到要插入数据与前一个数据(end)比较大小,比该数大则插入到该数后面,直到比较到第一个数据为止插入到首元素位置(循环条件为end>=0)。
先将要插入元素保存到tmp中,该位置为end+1,等到合适位置后插入到end+1中去。
为处理end比较完没找到的(tmp最小)的情况,当tmp>=end时,跳出while,在循环外交换。
每次比较完后end--向前比较
单趟如下![[Pasted image 20230925213125.png]]

  • 总体

共有n个数据,至少比较n次,故在用for循环,由于end+1的存在,i<=n-2/i<n-1;
拿到一个乱序数组,从第一个数据向后排序,故end=i,

代码

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 (tmp < a[end])
			{
				a[end + 1] = a[end];	//往后挪动
			}
			else
			{
				break;
			}
			end--;
		}
		a[end + 1] = tmp;
	}
}
int main()
{
	int arr[] = { 1,3,5,7,4,1,0,2,6 };
	InsertSort(arr,sizeof(arr)/sizeof(arr[0]));
	for (int i = 0; i < sizeof(arr) / sizeof(arr[0]); i++)
	{
		printf("%d ", arr[i]);
	}
	return 0; 
}

![[Pasted image 20230925213339.png]]

复杂度分析

  • 时间复杂度
    最优:
  • 有序:O(n)
    最劣:
  • 完全逆序:O(n^2)
  • 空间复杂度
    O(1)

希尔排序

  • 引入

①在插入排序中从第一个数开始向后遍历,②每到一个数(x)就与之前的m个数从后往前进行(比较–交换),若完全逆序,则每个数的要与前n个数交换,时间复杂度提升至O(n^2),完全有序则为O(n)。直到遍历完全后有序。
而希尔在此基础上思考,遍历是少不了的,那能否通过减少从后往前比较时的交换次数进行(蓝色内容)来提高时间效率呢?。
于是希尔排序的核心为先进行分组预排序,再进行直接插入排序来优化。
![[Pasted image 20230927113722.png]]

  • 以下是从0到1的希尔排序的过程

1.预排序

单元素

该元素往前比较,如果小于前面数,则交换,并将下标前移。直到n<0时退出循环,完成一个数的位置排序

//单个数据单趟
gap = 3;
int end = 0;
int tmp = a[end + 1];
while (end >= n)
{
	if (a[end] > tmp)
	{
		a[end + gap] = a[end];
		end -= gap;
	}
	else
		break;
}
a[end + gap] = tmp;
单趟元素

对组内单元素的排序,每隔gap个数据进行比较,直到单趟元素排序完成。

int  gap = 3;
for (int i = 0; i < n - gap; i += gap)
{
	int end = i;
	int tmp = a[end + gap];
	//单个数据单趟
	while (end >= n)
	{
		if (a[end] > tmp)
		{
			a[end + gap] = a[end];
			end -= gap;
		}
		else
			break;
	}
	a[end + gap] = tmp;
}
多组

为使每组元素动起来,在最外层套一层循环,每组元素有gap个,故循环gap次。

void InsertSort(int*a, int n)
{
	int  gap = 3;
		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 >= n)
				{
					if (a[end] > tmp)
					{
						a[end + gap] = a[end];
						end -= gap;
					}
					else
						break;
				}
				a[end + gap] = tmp;
			}
		}
}
多组并排

最后一步多组可以按如上分次进行,也可以按如下多组并行排序,效果相同,但代码更加整洁。
对与gap的

void InsertSort(int*a, int n)
{
	int  gap = 3;
	for (int i = 0; i < n - gap; ++i)
	{
		int end = i;
		int tmp = a[end + gap];
		//单个数据单趟
		while (end >= n)
		{
			if (a[end] > tmp)
			{
				a[end + gap] = a[end];
				end -= gap;
			}
			else
				break;
		}
		a[end + gap] = tmp;
	}
}
分析

多组并排

意义:让大的数更快地到后面去,小的数更快的到前面去
gap越大,跳的越快,越不接近有序。
gap越小,跳的越慢,越解决有序,gap=1,直接有序

2.直接插入排序

  • 以上逻辑对比直接插入排序我们可得,当gap=1时,该排序为直接插入排序。
  • 当数据过多gap=3时预排过多,优化不明显,所以gap随n的变化而变化(gap=gap/2或gap=gap/3+1(效率更高,为保证最后1次为1,故+1))。
  • 所以这样定义gap:第一趟gap=n每组分1个元素,之后分2、4、8……n个元素(gap=n/2、n/4……1),到1时相当于预排序后的直接插入排序,实现有序。
void ShellSort(int* a, int n)
{
	//多组并排
	int  gap = n;
	while (gap > 1)
	{
		gap = gap / 2;
		//gap = gap / 3 + 1;
		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;
		}
	}
}

复杂度计算

  • 时间复杂度

gap=3,每组n/3个数据,每趟比较3次(1+2),一共比较(n/3)*3=n次
gap=9,每组n/9个数据,每趟比较36(1+2+3+…+9)次,一共比较(n/9)*36=4n次
………………(组数x每组的次数)上一次排序对这次有所增益
目前在严蔚敏教材中时间复杂度约为O(n1.3)<O(nlog2N);该准确数据仍在研究暂未有定论;

  • 空间复杂度为1
  • 不稳定

选择排序

直接选择排序

遍历一遍直接选出 最大/最小 的值

void Swap(int* a, int* b)
{
	int tmp = *a;
	*a = *b;
	*b = tmp;
}
//直接选择排序
void SelectSort(int* a, int n)
{
	int begin = 0, end = n-1;
	while(end > begin)
	{
		int mini = begin, maxi = end;
		for (int i = begin+1; i <= end ; i++)
		{
			if (a[i] > a[maxi])
			{
				maxi = i;
			}
			if (a[i] < a[mini])
			{
				mini = i;
			}
		}
		
		Swap(&a[mini], &a[begin]);
		
		if (maxi == begin)
		{
			maxi = mini;
		}
		
		Swap(&a[maxi], &a[end]);
		begin++;
		end--;
	}
}

堆排序

排升序建小堆,排降序建大堆

交换排序

冒泡排序

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

快速排序

Honre

将第一个数设为key,
找key,左边找比key小,右边找比key大,交换。
比key小的放到左边,比key大的换到右边,最终达到左边比key小、右边比key大的效果。
当左右相遇时,key与右边交换。
至此,key为它要到的最终位置。

单趟

![[Pasted image 20231004171541.png]]

int PartSort(int* a, int left , int right)
{
	//*防止局部变量交换无效
	int keyi = left;
	while(left < 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;
}

单趟时间复杂度为O(n);

递归

![[Pasted image 20231004171711.png]]

void QuickSort(int* a, int begin, int end)
{
	if (begin >= end)
		return ;
	int keyi = PartSort(a, begin, end);
	//[begin, keyi-1] keyi [key+1, end]
	QuickSort(a, begin, keyi-1);
	QuickSort(a, keyi + 1, end);
}
  • 为什么相遇位置比key小?
    右边先走做到的
    - 右边先走找小,找到比key小数
  • 左边找大
    • 找到
    • 交换——>右边走
      • 未找到
        • R=L,相遇位置是右边所找的比key小的数
三数取中
//三数取中
int GetMid(int* a, int left, int right)
{
	int mid = (left + right) / 2;
	if (a[left] < a[right])
	{
		if (a[left] > a[mid])
			return mid;
		else if (a[left] < a[mid])
			return left;
		else
			return left;
	}
	else   //right 大
	{
		if (a[right] > a[mid])	
			return mid;
		else if (a[right] < a[mid])	
			return right;
		else
			return right;
	}
}

挖坑法

//挖坑法
int PartSort2(int* a, int left, int right)
{
	int midi = GetMid(a, left, right);
	Swap(&a[left], &a[midi]);

	int key = a[left];
	int hole = left;
	while (left < right)
	{
		while (left < right && a[right] >= key)
		{
			right--;
		}
		a[hole] = a[right];
		hole = right;

		while (left < right && a[left] <= key)
		{
			left++;
		}
		a[hole] = a[left];
		hole = left;
	}
	a[hole] = key;
	return hole;
}

前后指针法

cur找比key小的数,++prev,交换

int PartSort3(int* a, int left, int right)
{
	int keyi = left;
	int prev = left;
	int cur = left + 1;
	while (cur <= right)
	{
		if (a[cur] < a[keyi])
		{
			Swap(&a[cur], &a[prev]);
		}
		cur++;
	}
	Swap(&a[prev], &a[keyi]);
	return keyi;
}

小区间优化

降低递归次数——>直接插入

递归太深可能会产生栈溢出,所以我们来学习非递归(当然快排不太可能会溢出,1000000000个数据30层,)

非递归——栈

void QuickSortNonR(int* a, int begin, int end)
{
	ST st;
	STInit(&st);
	STPush(&st, end);	//入栈先右后左
	STPush(&st, begin);

	while(!STEmpty(&st))
	{
		//出栈先左后右
		int left = STTop(&st);
		STPop(&st);

		int right = STTop(&st);
		STPop(&st);

		int keyi = PartSort1(a, left, right);
		// [left, keyi-1] keyi [keyi+1, right]
		
		//先入右再入左
		if (right > keyi + 1)	//有位置
		{
			STPush(&st, right);
			STPush(&st, keyi + 1);
		}
		
		if (left < keyi - 1)
		{
			STPush(&st, keyi-1);
			STPush(&st, left);
		}
	}
	STDestory(&st);
}

二叉树结构的排序
右边找比key小的,左边找比key大的。

归并排序

递归

开空间

  • 分左右区间(递进)
    直到递进空返回,直到1个元素
  • 归并
    将 左|右 进行大小比较,大的元素插入到tmp数组中。
    判断 左|右 是否已经空,有一个空即停止。
    后续将剩下的元素插入到tmp中去。
  • 拷贝
    tmp中数组插入到原数组a中,顺序与tmp中相同
  • (回归)
    回归后继续 归并-拷贝- 回归

代码

void _MergeSort(int* a,int* tmp, int begin, int end)
{
	if (begin >= end)
		return;
	int mid = (begin + end) / 2;

	//递归
	_MergeSort(a, tmp, begin, mid);
	_MergeSort(a, tmp, mid+1, end);

	//归并到tmp中,并拷贝到原数组
	//[begin1, end1][begin2, end2]
	int begin1 = begin, end1 = mid;
	int begin2 = mid + 1, end2 = end;
	int index = begin;
	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 + begin, tmp + begin, (end - begin + 1) * sizeof(int));
}

void MergeSort(int* a, int n)
{
	int* tmp = (int*)malloc(sizeof(int) * n);
	if (tmp==NULL)
	{
		perror("malloc error\n");
		return ;
	}
	_MergeSort(a, tmp, 0, n-1);
	free(tmp);
}  

空间复杂度:O(n)
时间复杂度:O(n*log2n)

非递归

void MergeSortNonr(int* a, int n)
{
	int* tmp = (int*)malloc(sizeof(int) * n);
	if (tmp==NULL)
	{
		perror("malloc error\n");
		return ;
	}
	int gap = 1;
	while (gap < n)
	{
		for (int i = 0; i < n; i += 2 * gap)
		{
			//[begin1, end1] [begin2, end2]
			int begin1 = i, end1 = gap + i - 1;
			int begin2 = i + gap, end2 = 2 * gap + i - 1;
			//printf("[%d,%d][%d,%d]", begin1, end1, begin2, end2);

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

			int index = i;
			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, (end2-i+1) * sizeof(int));
		}
		//printf("\n");
		gap *= 2;
	}
	free(tmp);
}

复杂度

空间复杂度:O(n)
时间复杂度:O(n*log2n)

内/外排序
磁盘不支持下标访问
顺序写顺序读

计数排序

统计每个数字出现的次数,每个值是多少就对对应位置++;
局限:数值得是相对集中
相对映射

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);
	//将空间内数据初始化为0
	memset(count, 0, sizeof(int) * range);
	//统计数据出现的次数
	for (int i = 0; i < n; i++)
	{
		count[a[i] - min]++;
	}
	//排序(回写)
	int j = 0;
	for (int i = 0; i < range; i++)
	{
		while (count[i]--)
		{
			a[j++] = i + min;
		}
	}
}

复杂度

时间复杂度:O(n+range)
空间复杂度:O(range)
适合范围集中的排序

请添加图片描述

本篇内容就到这里了
如有不足之处欢迎在评论区补充交流

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值