八种排序方式

八种排序

排序的稳定性

假定在待排序的记录序列中,存在多个具有相同的关键字的记录,若经过排序,这些记录的相对次序保持不变,即在原序列中,A1=A2,且A1在A2之前,而在排序后的序列中,A1仍在A2之前,则称这种排序算法是稳定的

稳定性本质是维持具有相同属性的数据的插入顺序 ,如果后面需要使用该插入顺序排序,则稳定性排序可以避免这次排序

例如在答题竞赛中,只有三枚奖牌,金银铜,金牌已经选出,如果两者都得99分,在分值上一样但是在提交顺序上有先后之分,这时排序的稳定性就体现出来了
在这里插入图片描述

插入排序

直接插入排序

核心思想:将一个数插入一段有序区间,保持这段区间有序,不断的将一个数插入进去,最开始让第一个有序,插入进去后前两个有序,前两个有序了再插入进去,前三个就有序了,以此类推整体就有序了。在实际生活中我们在玩扑克牌时就会进行插入排序。

  • 插入排序时间复杂度:排升序时最坏的情况是数据是降序排列,这时时间复杂度为O(N^2),接近有序时时间复杂度最好的情况下为O(N)

在这里插入图片描述

void InsertSort(int* a, int n)
{
	assert(a);
	//end 为 n个数的最后一个数的下标
	//n-2是只用比到倒数第二个就已经比完了,超过就越界了
	//i每次加一,从头开始,假设只有一个数据,先把他变成前n-1个有序
	for (int i = 0; i < n - 2; i++)
	{
		int end = i;
		int tmp = a[end + 1];//保留end之后的数据,防止end+1被end覆盖

		while (end >= 0 )
		{
			if (tmp < a[end])
			{
				a[end + 1] = a[end];
				--end;
			}
			else
			{
				break;
			}
		}
		a[end + 1] = tmp;//这里结束的标志有两种
		//一种是比最后一个大直接放入到end后面。一种是end到-1,就将要插入的值放到0的位置
	}
}

希尔排序

在插入排序上进行优化,当数据是随机或者逆序的排序下,

  1. 欲排序,先让数据接近有序(接近升序)【对间隔为gap的,分成一组,进行插入排序】
  2. 直接插入排序

在这里插入图片描述

当gap=3,逆序排序时,先进行欲排序使其接近有序。

欲排序一次不是走一步,而是一次走gap步,数据挪动更快,gap越小越接近有序,gap越大越不接近有序,但是gap越小,挪动越慢,gap越大,挪动越快。当gap==1时就是直接插入排序

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-v0tNs5zy-1633948307179)(C:\Users\86181\AppData\Roaming\Typora\typora-user-images\image-20211006174732385.png)]

多组并排,原本希尔排序是要一组一组进行,间隔为gap的数据,进行欲排序,现在只用一层循环控制并行走循环
++i的意思,原本是要排完一组了才去排另一组,现在++i是一组没排完给赋给end,end就直接去排下一组


void ShellSort(int *a, int n)
{
	int gap = n;//适用于给的数据很多时,gap要是3就显得很小
	while (gap > 1)
	{
		gap = gap / 3 + 1;
//加一是让gap保证最后一次一定是1,gap大于1就是欲排序,等于1就是直接插入排序

//多组并排,原本希尔排序是要一组一组进行,间隔为gap的数据,进行欲排序,现在只用一层循环控制并行走循环
//++i的意思,原本是要排完一组了才去排另一组,现在++i是一组没排完给赋给end,end就直接去排下一组
		for (int i = 0; i < n - gap; i++)//单趟排序
		{
			int end = i;
			int tmp = a[end + gap];//保留end之后的数据,防止end+1被end覆盖

			while (end >= 0)
			{
				if (tmp < a[end])
				{
					a[end + gap] = a[end];
					end -= gap;
				}
				else
				{
					break;
				}
			}
			a[end + gap] = tmp;//这里结束的标志有两种
			//一种是比最后一个大直接放入到end后面。一种是end到0
		}
	}	
}

运行结果的时间差距很大
在这里插入图片描述

交换排序

冒泡排序

后一个比前一个大就交换

在这里插入图片描述

void BubbleSort(int* a, int n)
{
	for (int j = 0; j < n; j++)
	{
		int flag = 0;//最好的情况下顺序有序升序,设立标志位以此来判断是否交换
		for (int i = 0; i < n-j; i++)
		{
			if (a[i - 1]>a[i])
			{
				Swap(&a[i - 1], &a[i]);
				flag = 1;
			}
		}
		if (flag == 0)
		{
			break;
		}
	}
}

快速排序

  1. hoare版本:左右指针的方式

    一般选定左边最开始的位置作为key,左右两个指针Left,Right,让Right先走找比key小的,找到后停下来,再让Left走找比key大的,找到后停下来,交换Left和Right,继续让Right先走找比key小的,Left找比key大的,直到两个相遇,再把key交换到相遇的位置。这样子左边就比key小,右边比key大,让Right先走就保证了相遇位置一定比key小,(让右边做key也是一样的逻辑)

  • 相遇有两种情况:
  • 右遇左,右边找比key小的,相遇位置的左边一定比key小,
  • 左遇右,Right停在了比key小的位置,再一交换左边一定比key小,右边比key大

当数组本身有序的情况下,快速排序的时间复杂度就是O(N^2);,由于排一趟就要选key,第一趟走完,走到左边没有比他大的,再走第二趟,重新选key,再走一遍,逐渐走完,造成时间复杂度这么高的原因主要是选key导致的,这时就又有了新的优化

  • 三数取中法,找left,mild,right中不是最大也不是最小的数,这样就保证了不会取值做key,取到有序数组的头的问题
int GetMidIndex(int *a, int left, int right)//三数取中法,通过获取下标来找到中间值
{
	int mild = left + (right - left) / 2;
	if (a[left] < a[mild])
	{
		if (a[mild] < right)
		{
			return mild;
		}
		else if (a[left] >a[right])
		{
			return left;
		}
		else
		{
			return right;
		}
	}
	else        // (a[left] > a[mild])
	{
		if (a[left] < a[right])
		{
			return left;
		}
		else if (a[mild]>a[right])
		{
			return mild;
		}
		else
		{
			return right;
		}
	}
}

int  PartSort1(int *a, int left, int right)
{
	int mild = GetMidIndex(a, left, right);

	Swap(&a[left], &a[mild]);
	int keyi = left;
	
	while (left < right)
	{
		while (a[right] >= a[keyi] && left<right)//左边做key,右边先走找比key小的//相遇了就停止,没有相遇才继续找
		{
			--right;
		}
		while (a[left] <= a[keyi] && left<right)//左边再走找比key大的
		{
			++left;
		}
		Swap(&a[left], &a[right]);
	}
	Swap(&a[keyi], &a[right]);//相遇位置和key交换 
	return left;
}

void QuickSort(int * a, int left,int right)
{
	if (left >= right)
	{
		return;
	}
	int keyi = PartSort1(a, left, right);//一趟排好了,再将其分成多个子问题去解决
	//分成两段,思路类似二叉树的前序遍历,层层递归下去;

	//[0-keyi-1][keyi][keyi+1,right]
	QuickSort(a, 0, keyi - 1);
	QuickSort(a, keyi + 1, right);

}

2.挖坑法

将左边的第一个值作为坑,把值赋给keyi那个位置就形成了一个新的坑位
right从后往前找比原来的坑位的值小的,找到后填到坑里,不用交换,直接覆盖,right找到的值的位置又形成了新的坑位
left从前往后找比key大的,找到后填到坑里

int PartSort2(int* a, int left, int right)//挖坑法
{
	int key = a[left];

	int hole = left;
	while (left<right)
	{
		if (a[right] >= key && left<right)
		{
			right--;
		}
		//找比原来的坑位的值小的,找到后填到坑里
		a[hole] = a[right];
		hole = right;
		while (left < right)
		{
 			if ( a[left] < key && left < right)
			{
				left++;
			}
			//找比key大的,找到后填到坑里
			a[hole] = a[left];
			hole = left;
		}
	}
    a[hole] = key;
}

3.前后指针法

定义prev,cur。

  1. cur往前走,找比keyi小的数据,
  2. 找到比keyi小的数据后停下来,++prev
  3. 交换prev和cur所指向位置的值,直到cur走到数据的结尾
void PartSort3(int* a, int left, int right)
{
	int keyi = left;
	int prev = left;
	int cur = prev + 1;
	/*while (cur <= right)
	{
		if (a[cur]>a[keyi])
		{
			cur++;
		}
		if (a[cur]<a[keyi])
		{
			Swap(&a[prev], &a[cur]);
			prev++;
			cur++;
		}
	}*/

	while (cur <= right)
	{
		//prev != cur 
		//cur碰到比key小的也prev++,实际上就是自己跟自己交换索性不换反正curd都要++
		if (a[cur] > a[keyi] && prev != cur)
			Swap(&a[prev], &a[cur]);
		cur++;
	}
	Swap(&a[keyi], &a[prev]);
	return prev;
}
选择排序

简单选择排序

在一个待排序的数组中,从中找出最大(小)的元素,如果找到的元素不在最后一位,就与最后一(第一)位交换,重复上述动作,直到全部待排序的数据元素排完.

在这里插入图片描述

void SelectSort(int * a, int n)//选择排序
{
	int begin = 0;
	int end = n - 1;
	while (begin < end)
	{
		int mini = begin;
		int maxi = begin;
		//传统选择一次只找一个最小的或者最大的,优化后同时找最大和最小的
		for (int i = begin; i <= end; i++)
		{
			if (a[i]>a[maxi])
				maxi = i;

			if (a[i] < a[mini])
				mini = i;
		}

		Swap(&a[begin], &a[mini]);//把一趟中最小的放到左边
		
		if (begin == maxi)
			maxi = mini;

		Swap(&a[end], &a[maxi]);//一趟中最大的放到右边
		++begin;
		--end;
	}
}

堆排序

堆排序看起来像是交换第一和最后一位最后应该归到交换类排序,但是核心的思路还是选择,建堆向下调整的过程中就会进行选择根节点还是叶子节点。升序建大堆,降序建小堆

排序按照生活中的思维是从小到大排序,应该是建小堆,但是如果是建小堆可以解决第一个数是最小的,那么第二小的怎么找呢,再向下调整一下建小堆找最小的吗,那显然时间复杂度更复杂,如果是建大堆,建好堆后第一个是最大的数,第一个数再跟最后一个数交换一下,左右子树依然保持是个大堆的结构,只需要再向下调整一下就好了,不需要像建小堆选出最小的数再重新建堆。

排降序也是一样的思路

void Swap(int * px, int * py)
{
	int tmp = *px;
	*px = *py;
	*py = tmp;
}
//左右子树都是大堆或者小堆
void AjustDown(int * a, int n, int parent)
{
	int child = parent * 2 + 1;//默认child是左孩子
	while (child < n)
	{
		if (child + 1 < n && a[child + 1] > a[child])//默认child是左孩子,如过比左孩子大就认为child为右孩子
		{
			++child;
		}
		if (a[child] > a[parent])
		{
			Swap(&a[child], &a[parent]);
				parent = child;
				child = parent * 2 + 1;
		}
		else
		{
			break;
		}
	}
}

void Ajustup(int *a, int child)
//向上调整,当建好堆后如果插入一个数据假设是大堆
//放到最后,比父节点要大,它只会对它所在的父亲节点的路径造成影响,不用管左右子树谁大的问题,,左右子树整体还是一个大堆
{
	int parrent = (child - 1) / 2;
	while (child > 0)
	{
		if (a[child] > a[parrent])
		{
			Swap(&a[child], &a[parrent]);
			parrent = child;
			parrent = (child - 1) / 2;
		}
		else
		{
			break;
		}
	}
}

void HeapSort(int *a,int n)
{
	//排升序建大堆
	//排降序建小堆
	for (int i = (n - 1 - 1) / 2; i >= 0;--i)//i为parent,要调整几对子树
	{
		AjustDown(a, n, i);//通过swap函数来控制大堆或者小堆
	}

	int end = n - 1;
	while (end > 0)
	{
		Swap(&a[0], &a[end]);
		AjustDown(a, end, 0 );
		--end;
	}
}

归并排序

递归版本

  一个区间最开始是无序的,如果他的左右区间有序,那么他就有序。
  找中间值,把一个整区间划分多个子区间,通过子区间来实现有序,左右区间又可以划分成更小的区间,直到只剩一个值返回

在这里插入图片描述

//归并排序
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;

	//划分为有序的后再依次比大小放到tmp数组中
	//一方走完了,另一方原本也已经是有序的,直接拷贝到tmp数组

	while (begin1 <= end1 && begin2 <= end2)
	{
		if (begin1 < begin2)
		{
			tmp[index++] = a[begin1++];
		}
		else
		{
			tmp[index++] = a[begin2++];
		}
	}
	//现在只有一个结束才会走到这一步,左右选只会选出一个值
	//现在不清楚是begin1走完还是begin2走完,索性判断他结束的标志
	//begin1结束就把begin2里的全拷贝到tmp中,反之亦然	
	while (begin1 <= end1)//第一段没结束(就说明第二段结束了)就进去,结束了就不会进去,进去后全部拷贝
	{
		tmp[index++] = a[begin1++];
	}

	while (begin2 <= end2)
	{
		tmp[index++] = a[begin2++];
	}

	//从临时数组拷贝到原数组
	for (int i = left; i < right; left++)
	{
		a[i] = tmp[i];
	}

}

void MergeSort(int *a, int n)
{
	int * tmp = (int *)malloc(sizeof(int)*n);
	_MergeSort(a, 0, n - 1, tmp);  

	free(tmp);
	tmp = NULL;
}

非递归版本的实现

void MergeSortNonR(int *a, int n)
{
	int tmp = (int *)malloc(sizeof(int));
	//控制组的个数,一组有1,2,4,8...
	int groupnumber = 1;
	//[[begin1,end1],[begin2,end2]]
	while (groupnumber<n)
	{
		for (int i = 1; i < n; i += 2 * groupnumber)
		{
			int begin1 = i, end1 = i + groupnumber - 1;
			int begin2 = i + groupnumber, end2 = i + 2 * groupnumber - 1;
			int * index = begin1;

			if (begin2 >= n)//第二段越界,控制边界
			{
				begin2 = n + 1;
				end1 = n;
			}

			if (begin1 >= n)//begin1越界,后面整体都越界
			{
				end1 = n - 1;
			}

			if (end2 >= n)//END2越界,后半段越界
			{
				end2 = n - 1;
			}
			while (begin1 <= end1 && begin2 <= end2)
			{
				if (begin1 < begin2)
				{
					tmp[index++] = a[begin1++];
				}
				else
				{
					tmp[index++] = a[begin2++];
				}
			}
			while (begin1 <= end1)//第一段没结束(就说明第二段结束了)就进去,结束了就不会进去,进去后全部拷贝
			{
				tmp[index++] = a[begin1++];
			}

			while (begin2 <= end2)
			{
				tmp[index++] = a[begin2++];
			}

		}
		int groupnumber *= groupnumber * 2;
	}

	for (int i = 0; i < n; i++)
	{
		a[i] = tmp[i];
	}

计数排序

统计每个数出现的次数,出现几次就在映射的位置++几次,再根据数组下标排序

在这里插入图片描述

统计排序:差值较大不太实用,适用局部范围集中的数
//绝对映射会有空间浪费,采取相对映射关系,a[i]-min
void countsort(int * a, int n)
{
	//找出最大值和最小值
	int max = a[0], min = a[0];
	for (int i = 1; i < n; i++)
	{
		if (a[i] < min)
		{
			min = a[i];
		}
		if (a[i]>max)
		{
			max = a[i];
		}
	}
	int range = max - min + 1;
	int * count = (int *)calloc(range, sizeof(int));
	//统计每个数出现的次数,出现几次就在映射的位置++几次
	for (int i = 0; i < n; i++)
	{
		count[a[i] - min]++;

	}
	//根据数组下标排序
	int i = 0;
	for (int j = 0; j < n; j++)
	{
		while (count[j]--)
		{
			a[i++] = j + min;
		}
	}
}
统计排序:差值较大不太实用,适用局部范围集中的数
//绝对映射会有空间浪费,采取相对映射关系,a[i]-min
void countsort(int * a, int n)
{
	//找出最大值和最小值
	int max = a[0], min = a[0];
	for (int i = 1; i < n; i++)
	{
		if (a[i] < min)
		{
			min = a[i];
		}
		if (a[i]>max)
		{
			max = a[i];
		}
	}
	int range = max - min + 1;
	int * count = (int *)calloc(range, sizeof(int));
	//统计每个数出现的次数,出现几次就在映射的位置++几次
	for (int i = 0; i < n; i++)
	{
		count[a[i] - min]++;

	}
	//根据数组下标排序
	int i = 0;
	for (int j = 0; j < n; j++)
	{
		while (count[j]--)
		{
			a[i++] = j + min;
		}
	}
}
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值