经典排序 插入、希尔、选择、堆排、冒泡、快排、归并

排序的引入

当进行数据处理时,经常需要进行查找操作,而为了查的快、找到准,通常要求待处理的数据按关键字大小有序排列,以便采用效率较高的查找方法。

排序的基本概念

1.排序
排序是计算机内经常进行的一种操作,其目的是将一组“无序”的记录序列调整为“有序”的记录序列,通俗的说就是将杂乱无章的数据元素,通过一定的方法按关键字顺序排列的过程叫做排序。
2.内部排序和外部排序
根据排序时数据所占用存储器的不同,可将排序分为两类。
内部排序:整个排序过程完全在内存中进行;
外部排序:由于待排序的记录数据量太大,内存无法容纳全部数据,排序需要借助外部内存设备才能完成。
3.排序的稳定性
稳定性:假设在待排序的文件中,存在两个或两个以上的记录具有相同的关键字,在
用某种排序法排序后,若这些相同关键字的元素的相对次序仍然不变,则这种排序方法
是稳定的。反之,若这些相同关键字的元素的相对次序发生变化,则所用的排序方法是不稳定的。
例子说明:一组学生记录已按学号排好序,现在又需要根据期末成绩进行排序,当成绩相同时,要求小的学号排到前面。显然这种情况下,必须选用稳定的排序方法。
说明:冒泡,插入,基数,归并属于稳定排序,选择,快速,希尔,归属于不稳定排序。

排序的分类及复杂度分析

在这里插入图片描述
时间、空间复杂度及稳定性
《时间空间复杂度讲解》

常用排序的详解

插入排序

1.直接插入排序
基本思想:
在要排序的一组数中,假定前n-1个数已经排好序,现在将第n个数插到前面的有序数列中,使得这n个数也是排好顺序的。如此反复循环,直到全部排好顺序。
优点:稳定,快。
缺点:比较次数不一定,比较次数越多,插入点后的数据移动越多,特别是当数据总量庞大的时候,但用链表可以解决这个问题,虽然也会有额外的内存开销。
过程:
在这里插入图片描述
代码实现:

// 直接插入排序
void InsertSort(int* a, int n)
{
	for (int i = 0; i < n - 1; i++)
	{
		int end = i;
		int next = a[end + 1];//保存下一个数的值,避免被下一次覆盖
		while (end >= 0 && a[end]>next)//对前面排好序的一部分从后往前再比较,有大的就往后覆盖,下一个还大就覆盖,直到没有大于要判断插入的这个next,跳出循环,
		{
			a[end+1] = a[end];
			end--;
		}
		a[end + 1] = next; //再在终止的位置把要插入的next赋值进去
	}
}

直接插入排序的特性总结:

  1. 元素集合越接近有序,直接插入排序算法的时间效率越高
  2. 时间复杂度:O(N^2)
  3. 空间复杂度:O(1)
  4. 稳定性:稳定
  5. 适用性:插入排序不适合对于数据量比较大的排序应用。但是,如果需要排序的数据量很小,那么插入排序还是一个不错的选择。

2.希尔排序
基本思想:
在要排序的一组数中,根据某一增量分为若干子序列,并对子序列分别进行插入排序。
然后逐渐将增量减小,并重复上述过程。直至增量为1,此时数据序列基本有序,最后进行插入排序。
过程:
在这里插入图片描述
代码实现

// 希尔排序
void ShellSort(int* a, int n)
{
	int gap = n;
	//不能写成大于0,因为gap的值始终>=1,
	while (gap > 1)
	{
		gap = gap / 3 + 1;   //设置增量  //应当gap最后变成1了,才是排完序了 所以这里要加1
		for (int i = 0; i < n - gap; i++)
		{
			    // 这里只是把插入排序的1换成gap即可
				//但是这里不是排序完一个分组,再去
				//排序另一个分组,而是整体只过一遍
				//这样每次对于每组数据只排一部分
				//整个循环结束之后,所有组的数据排序完成
			int end = i;
			int next = a[end + gap];
			while (end>=0 && a[end] > next)
			{
				a[end + gap] = a[end];
				end -= gap;
			}
			a[end + gap] = next;
		}
	}
}

希尔排序的特性总结:

  1. 希尔排序是对直接插入排序的优化。
  2. 当gap > 1时都是预排序,目的是让数组更接近于有序。当gap == 1时,数组已经接近有序的了,这样就会很快。这样整体而言,可以达到优化的效果。
  3. 希尔排序的时间复杂度不好计算,需要进行推导,推导出来平均时间复杂度: O(N1.3—N2)
  4. 稳定性:不稳定

选择排序

1.简单选择排序
基本思想:
每一次从待排序的数据元素中选出最小(或最大)的一个元素,存放在序列的起始位置,直到全部待排序的数据元素排完 。
过程:
在这里插入图片描述

代码实现:

 选择排序
void SelectSort(int* a, int n)
{
	for (int i = 0; i < n; i++)             
	{
		int min = i;
		for (int j = i+1; j < n; j++)
		{
			if (a[j] < a[min])//直到最左边找到最小值的下标
			{
				min = j;
			}
		}
		swap(&a[i], &a[min]);//小的就交换到前面去,接着下一轮的比较选择
	}

}

直接选择排序的特性总结:

  1. 直接选择排序思考非常好理解,但是效率不是很好。实际中很少使用
  2. 时间复杂度:O(N^2)
  3. 空间复杂度:O(1)
  4. 稳定性:不稳定

2.堆排序
基本思想:
堆排序(Heapsort)是指利用堆积树(堆)这种数据结构所设计的一种排序算法,它是选择排序的一种。它是通过堆来进行选择数据。需要注意的是排升序要建大堆,排降序建小堆。
过程:
排升序,建大堆,再选择判断
在这里插入图片描述
代码实现

// 堆排序
void AdjustDwon(int* a, int n, int root)//向下调整算法 大堆
{
	int parent = root;
	int child = parent * 2 + 1;
	while (child < n)
	{
		if ((child + 1) < n && a[child] < a[child + 1])
		{
			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)
{
	// 建堆,先从最后两个叶子上的根(索引为(n - 2) / 2开始建堆
	// 先建最小的堆,直到a[0](最大的堆)
	// 这就相当于在已经建好的堆上面,新加入一个
	// 根元素,然后向下调整,让整个完全二叉树
	// 重新满足堆的性质
	for (int i = (n - 2) / 2; i >= 0; i--)//建大堆
	{
		AdjustDwon(a, n, i);
	}
	int end = n - 1;//最后面的数与堆顶的大数交换,在调整,次大的又调整到堆顶,在交换,在调整
	while (end > 0)
	{
		swap(&a[0], &a[end]);
		AdjustDwon(a, end, 0);
		end--;
	}
}

堆选择排序的特性总结:

  1. 堆排序使用堆来选数,效率就高了很多。
  2. 时间复杂度:O(N*logN)
  3. 空间复杂度:O(1)
  4. 稳定性:不稳定

交换排序

1.冒泡排序
基本思想:两个数比较大小,较大的数下沉,较小的数冒起来
过程:
在这里插入图片描述
实现代码:

// 冒泡排序
void BubbleSort(int* a, int n)
{
	int end = n - 1;
	while (end > 0)// 控制趟数
	{
		int flag = 0;//可能要排序的数组本趟就已经达到有序的,后面的趟数就可以没必要跑了,所以设置一个目标值来记录这这种情况
		for (int i = 1; i <= end; i++)//一一对比,直到这一趟结束
		{
			if (a[i - 1] > a[i])
			{
				swap(&a[i - 1], &a[i]);
				flag = 1;
			}
		}
		if (flag == 0)//已经是有序的了,就不用再接着下一躺了,直接跳出
		{
			break;
		}
		end--;
	}
}

冒泡排序的特性总结:

  1. 冒泡排序是一种非常容易理解的排序
  2. 时间复杂度:O(N^2)
  3. 空间复杂度:O(1)
  4. 稳定性:稳定

2.快速排序
快速排序是Hoare于1962年提出的一种二叉树结构的交换排序方法。
基本思想:(分治)
1.先从数列中取出一个数作为key值;
2.将比这个数小的数全部放在它的左边,大于或等于它的数全部放在它的右边;
3.对左右两个小数列重复第二步,直至各区间只有1个数。
过程:
在这里插入图片描述
将区间按照基准值划分为左右两半部分的常见方式有:

  1. 三数取中法
  2. 挖坑法
  3. 前后指针版本
    《快排三种版本详解》
    第一趟具体过程(最普通的划分:选第一个或最后一个元素做基准值):

在这里插入图片描述
代码实现
普通版本实现:

//普通版本  注意:选最左边为基准值,必须从最右边开始作比较
int PartSort(int *a, int left, int right)
{
	int key = a[left];
	int start = left;
	while (left < right)
	{
		while (left < right && a[right]>=key)
		{
			right--;
		}
		while (left < right && a[left] <= key)
		{
			left++;
		}
		swap(&a[left], &a[right]);
	}
	swap(&a[left], &a[start]);
	return  left;
}
void QuickSort(int* a, int left, int right)
{
	//if (left >= right)//特殊情况  1 3 5 4, 当排第一趟时1为基准,end没找到比它小的一直减减,直到它本身了,此时left=right,返回keyindex为没动的left; 在递归下一趟时left为0,大于keyindex-1的-1,就不做左边无意义的递归趟数了   说明如果区域不存在或只有一个数据则不递归排序
	//	return;
	if (left < right)
	{
		if (left-right +1 < 10)//小区间优化:   区间内数据量小于一定值了,就没必要用快排的递归下去了,排序量小的数据还是可以用插入排序的
		{
			InsertSort(a+left, right-left + 1); //相当于也是传区间
		}
		else
		{
			int keyindex = PartSort3(a, left, right);
			// [left,keyindex-1]    keyindex   [keyindex+1,right]   二叉树结构  左区间 基准值 右区间  继续递归
			QuickSort(a, left, keyindex - 1);//基准值左边(大于它的)继续划分
			QuickSort(a, keyindex + 1, right);//基准值右边(小于它的)也继续
			//直到最小的都已经有序(划分好了)
		}
	}
}

快排非递归排序(调用栈实现)
思路:
1.先把整个[0,7]这两个代表区间的下标压入栈,栈是先进后出,注意取栈顶时候的逻辑(后入的尾区间下标先取,先入头区间下标后取);
2.用自己的栈保留每个区间的头和尾下标,在循环处理单趟排序并再次分区,直到没有下标还可以划分,栈空。排序结束。
在这里插入图片描述

// 快速排序 非递归实现
void QuickSortNonR(int* a, int left, int right)
{
	Stack s;
	StackInit(&s);
	StackPush(&s, left);
	StackPush(&s, right);
	while (!StackEmpty(&s))
	{
		int end = StackTop(&s);
		StackPop(&s);
		int begin = StackTop(&s);
		StackPop(&s);
		int midkey = PartSort(a, begin, end);
		if (begin<midkey - 1)//只有一个元素时,如最后分到[1,1]的情况,不用再划分
		{
			StackPush(&s, begin);
			StackPush(&s, midkey - 1);
		}
		if (end > midkey + 1)
		{
			StackPush(&s, midkey + 1);
			StackPush(&s, end);
		}
	}
}

快速排序的特性总结:

  1. 快速排序整体的综合性能和使用场景都是比较好的
  2. 时间复杂度:O(N*logN)
  3. 空间复杂度:O(logN)
  4. 稳定性:不稳定
    缺点:在最坏的情况为初始记录就是排好序了的情况,,如下图所示,一直有序,基准值右边一直没有,相当于每趟排n-1个数据,为等差数列n(n-1)/2,此时时间复杂度为O(N^2)。事实上,快速排序对初始记录很“乱”的情况下更有效。
    在这里插入图片描述

归并排序

基本思路:
将数组分成二组 A,B,如果这二组组内的数据都是有序的,那么就可以很方便的将这二组数据进行排序。
可以先将 A,B 组各自再分成二组。依次类推,当分出来的小组只有一个数据时,可以认为这个小组组内已经达到了有序,然后再合并相邻的二个小组就可以了。这样通过先递归的分解数列,再合并数列就完成了归并排序。
过程:
在这里插入图片描述

递归实现代码:

//归并排序 先划分在合并
void _MergeSort(int* a, int left, int right, int* tmp)
{
	if (left >= right)  //一直分到每组只剩一个元素就停止划分,准备开始归并
	{
		return;
	}
	int mid = left + ((right - left) >> 1);
	_MergeSort(a, left, mid, tmp);     //先递归划分,再出栈合并
	_MergeSort(a, mid + 1, right, tmp);
	// [left, mid]
	// [mid+1, right]
	int i = left, j = mid;
	int x = mid + 1, z = right;
	int k = left;
	
	//将有二个有序数列a[left...mid]和a[mid...right]合并。
	while (i <= j && x<=z)
	{
		if (a[i] <=a[x])
		{
			tmp[k++] = a[i++];
		}
		else
		{
			tmp[k++] = a[x++];
		}
	}
	while(i <= j)
		tmp[k++] = a[i++];
	while(x <= z)
		tmp[k++] = a[x++];
	for (i = 0; i<k; i++)
		a[left + i] = tmp[i];
	//memcpy(a + left, tmp + left, sizeof(int)*(right - left+1 ));
}
// 归并排序递归实现
void MergeSort(int* a, int n)
{
	int* tmp = (int *)malloc(sizeof(int)*n);
	_MergeSort(a, 0, n - 1, tmp);
	free(tmp);

}

假设有八个数下标[0,7],归并排序的基本过程分析:
在这里插入图片描述
归并排序的特性总结:
归并排序的效率是比较高的,设数列长为 N,将数列分开成小数列一共要 logN 步,每步都是一个合并有序数列的过程,时间复杂度可以记为 O(N),故一共为 O(NlogN)。因为归并排序每次都是在相邻的数据中进行操作,所以归并排序在 O(NlogN) 的几种排序方法(快速排序,归并排序,堆排序)也是效率比较高的。

  1. 归并的缺点在于需要O(N)的空间复杂度,归并排序的思考更多的是解决在磁盘中的外排序问题。
  2. 时间复杂度:O(N*logN)
  3. 空间复杂度:O(N)
  4. 稳定性:稳定

小结

综合分析和比较各种排序方法,可以得出以下结论:
1.简单排序法一般只用于n 较小的情况(例如n<30)。当序列中的记录“基本有序”时,直接插人排序是最佳的排序方法。如果记录中的数据较多,则应采用移动次数较少的简单选择排序法。
2.快速排序、堆排序和归并排序的平均时间复杂度均为0(nlogn),但实验结果表明,就平均时间性能而言,快速排序是所有排序方法中最好的。遗憾的是,快速排序在最坏情况下的时间
性能为0(n^2)。堆排序和归并排序的最坏时间复杂度仍为0( nlogn),当n较大时,归并排序的时间性能优于堆排序,但它所需的辅助空间最多。
3.可以将简单排序法与性能较好的排序方法结合使用。例如,在快速排序中,当划分子区间的长度小于某值时,可以转而调用直接插人排序法;或者先将待排序序列划分成若干子序列,分别进行直接插人排序,然后再利用归并排序法,将有序子序列合并成一一个完整的有序序列。
4.基数排序的时间复杂度可以写成0(dn)。因此,它最适用于n值很大而关键字的位数d较小的序列。当d远小于n时,其时间复杂度接近于0(n)。
5.从排序的稳定性上来看,在所有简单排序法(插入、简单选择、冒泡)中,简单选择排序是不稳定的,其他各种简单排序法都是稳定的。然而,在那些时间性能较好的排序方法中,希尔排序、快速排序、堆排序都是不稳定的,只有归并排序基数排序是稳定的。

综上所述,每一种排序方法各有特点,没有哪一种方法是绝对最优的。应根据具体情况选择合适的排序方法,也可以将多种方法结合起来使用。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值