排序算法:插入、希尔、选择、推排、冒泡、快速、归并排序

排序算法

今天为我们来通过C语言来实现常见排序算法:直接插入排序、希尔排序、选择排序、堆排序、冒泡排序、快速排序、堆排序,了解算法的具体实现和算法的性能。


一、排序的概念

1.1排序的概念

排序:所谓排序,就是使一串记录,按照其中的某个或某些关键字的大小,递增或递减的排列起来的操作。
稳定性:假定在待排序的记录序列中,存在多个具有相同的关键字的记录,若经过排序,这些记录的相对次
序保持不变,即在原序列中,r[i]=r[j],且r[i]在r[j]之前,而在排序后的序列中,r[i]仍在r[j]之前,则称这种排
序算法是稳定的;否则称为不稳定的。
内部排序:数据元素全部放在内存中的排序。
外部排序:数据元素太多不能同时放在内存中,根据排序过程的要求不能在内外存之间移动数据的排序。

1.2 常见的排序算法

二、常见排序算法的实现

2.1 插入排序

思想:把第一个元素当成有序,然后把下一个元素插入到这个有序序列里,使得保持有序,重复步骤我们可以n-1个元素排成有序,把第n个元素插入到有序序列里。
步骤:1.先把第一个元素当成有序
           2.取有序元素的最后下标end的下一个元素temp
           3.将temp从最后一个往前比较(end--),比temp大则元素往后移动,小则插入到end的后面。
           4.重复步骤2,3 循环n-1(n为数组长度)次
             

动画图解如下:

//插入排序
void InsertSort(int* a, int n)
{
	for (int i = 0; i < n - 1; i++) 
	{
		int end=i;
		int temp = a[end + 1];
		while (end >= 0)
		{
            //比较与end位置元素大小
			if (temp < a[end])
			{
				a[end + 1] = a[end];
				end--;
			}
            //大于end位置元素,跳出
			else
			{
				break;
			}
		}
        //1.end大于零,temp放在end元素后一个
        //2.end小于零,temp也是放在end元素后,只不过此时为第一个元素
		a[end + 1] = temp;
	}
}

直接插入排序
时间复杂度:O(N^2)

空间复杂度:O(1)

稳定性:稳定
最坏情况:逆序
最好情况是多少:顺序有序或者接近有序  O(N)

2.2 希尔排序

思想:插入排序最好的情况是有序,我们可以在插入排序之前进行预排序,使得序列变得接近有序

,先选定一个整数,把待排序文件中所有记录分成个组,所有距离为的记录分在同一组内,并对每一组内的记录进行排序,重复上述步骤。然后排序就变得简单了。

步骤:1.我们定义一个gap为每组元素的之间的距离。
           2.把每个元素距离为gap的分为一组,进行插入排序
           3.gap减小(通常除2),再进行步骤2
           4.最后为gap减小到1,即每个元素各自为一组,即为插入排序
动画图解如下:
             
//希尔排序
void ShellSort(int* a, int n) {

	int gap=n;
	while (gap > 1) 
	{
		gap/=2;
        //循环到n-gap-1元素
		for (int i = 0; i < n-gap; i++)
		{
			int end = i;
			int temp = a[end + gap];
			while (end >= 0)
			{
				if (temp < a[end])
				{
					a[end + gap] = a[end];
					//end移动距离为gap
                    end -= gap;
				}
				else
				{
					break;
				}
			}
            //各自插入到每组元素
			a[end + gap] = temp;
		}
	}

}
1. 希尔排序是对 直接插入排序的优化
2. 当gap > 1时都是预排序,目的是让数组更接近于有序。当 gap == 1 时,数组已经接近有序的了,这样就会很快。这样整体而言,可以达到优化的效果。我们实现后可以进行性能测试的对比。
3. 希尔排序的时间复杂度不好计算,因为gap的取值方法很多,导致很难去计算,因此在好些树中给出的希尔排序的时间复杂度都不固定
希尔排序

时间复杂度:O(N^1.3)

空间复杂度:O(1)

稳定性:不稳定

2.3 选择排序

思路:每一次从待排序的数据元素中选出最小(或最大)的一个元素,存放在序列的起始位置,直到全部待排序的数据元素排完 。同时我们还可以更加精简算法,再选出最小的同时选出最大的,把最小的放在最左边,最大的放在最右边。
步骤:1.从头到尾遍历序列找出最大最小的值
           2.把最大最小值放在序列的尾和头
           3.序列向中间收,头加1,尾减1,重复1,2步骤
只选最小的选择排序的图解
动画图解如下:
//选择排序
void SelectSort(int* a, int n) {
	int begin = 0, end = n - 1;
	while (begin < end)
	{
		int maxi = begin, mini = begin;
		for (int i = begin + 1; i <= end; i++)
		{
			if (a[i] < a[mini])
			{
				mini = i;
			}
			if (a[i] > a[maxi])
			{
				maxi = i;
			}
		}
		Swap(&a[begin], &a[mini]);
		//避免二次交换
        if (begin == maxi)
			maxi = mini;
		Swap(&a[end], &a[maxi]);
		
        //缩小范围
		begin++;
		end--;

	}

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

2.4 堆排序

思路: 堆排序(Heapsort)是指利用堆积树(堆)这种数据结构所设计的一种排序算法,它是选择排序的一种。它是通过 来进行选择数据。需要注意的是排升序要建大堆,排降序建小堆。
步骤:1.通过向下调整建堆( 排升序要建大堆,排降序建小堆)
           2.交换第一个和最后一个元素
           3.从零向下调整
           4.从堆的长度减一,开始重复步骤2,3
图解:
//堆排序
//向下调整
void AdjustDown(int* a, int n, int parent) {
	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[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--)
	{
		AdjustDown(a, n, i);
	}

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


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

2.5 冒泡排序

思路:从头开始依次遍历整个序列,如果遇到大的就交换,最后我们就把大的交换到最后了。
步骤:1.遍历序列(遍历n-1次)
           2.与下一个进行比较,比后一个大的就交换。
动画图解如下:
//冒泡排序
void BubbleSort(int* a, int n) {
	for (int i = 0; i < n - 1; i++)
	{
		int exchang = 0;
		for (int j = 1; j < n - i; j++)
		{
            //前一个比后一个大就交换
			if (a[j] < a[j - 1])
			{
				Swap(&a[j], &a[j - 1]);
				exchang = 1;
			}
		}
		if (exchang == 0)
			break;
	}
}

冒泡排序

时间复杂度: O(N^2)
空间复杂度: O(1)
稳定性:稳定

2.6 快速排序

思路:任取待排序元素序列中的某元素作为基准值,按照该排序码将待排序集合分割成两子序列,左子序列中所有元素均小于基准值,右子序列中所有元素均大于基准值,然后最左右子序列重复该过程,直到所有元素都排列在相应位置上为止。

选择基准值有多种方法:

1.直接选择头或者尾为基准值(但是序列为有序或者接近有序时间复杂度大)

2.随机数取基准值

//随机取基准值
int randi = rand() % (right - left + 1);
randi += left;
Swap(&a[left], &a[randi]);//基准值放到序列头

3.三位数取中

取序列头尾和中间三个数,让不是最大的,也不是最小的作为基准值。

//三位数取中
int midi = GetMidi(a, left, right);//取中间值函数
Swap(&a[midi], &a[left]);//基准值放到序列头

将区间按照基准值划分为左右两半部分的常见方式有:

2.6.1 hoare版本

思路:选择key(基准值),从尾向头开始找比key小的,然后再从头找比key大的,交换两者,然后继续整个步骤,直到它们两个相遇,交换key位置到相遇位置处,这样key前面就全为比key小的,后面全为比key大的,此时key为正确位置,然后分割序列,分为key前面的和key后面,重复上面所有步骤。

我们这直接选择最左边为key

步骤:1.最左边为key,定义left,right代表key下一个位置,序列尾

           2.让right向前走,找到比key小的,找到了停止

           3.让left向后走,找到比key大的,找到了停止

           4.交换此时righ和left的位置上的元素,重复2,3步骤

           5.直到相遇,交换此时相遇位置和key的。

           6.分割序列,分为keyi前面的序列和keyi后面的位置,重复以上所有步骤。

动画图解如下:

//快速排序 hoare版本
void QuickSort1(int* a, int left, int right) {
	//当只有一个元素和为空序列返回空
    if (left >= right)
		return;

	int begin = left, end = right;

    //取序列开头为key
	int keyi = left;
	
	while (left < right)
   	{    
        //找比key小的
		while (left < right && a[keyi] <= a[right])
		{
			right--;
		}
        //找比key大的
		while (left < right && a[keyi] >= a[left])
		{
			left++;
		}
        
		Swap(&a[left], &a[right]);
	}

	Swap(&a[left], &a[keyi]);
	keyi = left;

    //递归[begin,keyi-1] 和 [keyi+1,end]
	QuickSort1(a, begin, keyi - 1);
	QuickSort1(a, keyi+1, end);

}

2.6.2 前后指针版本

思路:选择最左或者最右为key,定义两个指针prev和cur分别指向序列开头,prev下一个位置。然后向后走,如果遇到比key小的cur走,prev也向后走。直到遇到比key大的,只有cur走,prev不走,当再次遇到比key小的,prev先走,然后交换此时cur和prev的值,cur再走。重复上面步骤,直到cur走到序列尾,然后交换prev和key的值。通过上面操作我们可以让cur和prev中间的值为比key大的,此方法就是把比key大的往后推,小的往前移。

步骤:1.选择最左key,prev和cur分别指向序列开头,开头下一个

           2.cur先走,prev后走

           3.遇到比key大的,只有cur走

          4.再遇到key小的,prev先走,然后交换此时prev和cur的值,cur在走

          5.cur走到序列尾,交换prev和key的值

          6..分割序列,分为keyi前面的序列和keyi后面的位置,重复以上所有步骤。

动画图解如下:

//快速排序
//前后指针法
void QuickSort2(int* a, int left, int right) {
	
	if (left >= right)
		return;

	
	int prev = left, cur = left+1;

	int keyi = left;
	while (cur <= right)
	{    
        //如果遇到比key小的才交换
		if (a[cur] < a[keyi] && ++prev != cur)
		{
			Swap(&a[cur], &a[prev]);
		}
		cur++;
	}
	Swap(&a[prev], &a[keyi]);
	keyi = prev;

    //分割[left,keyi-1] [keyi+1,right]
	QuickSort2(a, left, keyi - 1);
	QuickSort2(a, keyi+1, right);

}

2.6.3 非递归版本

思路:之前我们都是通过递归来解决的,我们需要的是排序的左右子区间,所以我们可以通过一个栈来存储排序的左右子空间,根据栈先进后出的特性。左右子区间依次进栈出栈从而达到递归的效果。

void QuickSortNonR(int* a, int left, int right) {
	ST st;
	StackInit(&st);
	
	//左右下标入栈
    //右边先进  左边后进
	StackPush(&st,right);
	StackPush(&st,left);

	while (!StackEmpty(&st))
	{

		left = StackTop(&st);
		StackPop(&st);

		right = StackTop(&st);
		StackPop(&st);

		//单趟
		int prev = left, cur = left + 1;

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

		//[left,key-1] && [key+1,right]
        
        //进左右子区间
		if (keyi + 1 < right)
		{
			StackPush(&st, right);
			StackPush(&st, keyi + 1);
		}

		if (keyi - 1 > left)
		{
			StackPush(&st, keyi - 1);
			StackPush(&st, left);
		}
	}
	
	StackDestroy(&st);

}
快速排序整体的综合性能和使用场景都是比较好的,所以才敢叫快速排序
快速排序
时间复杂度: O(N*logN)
空间复杂度: O(logN)
稳定性:不稳定

2.7 归并排序

思路:归并排序(MERGE-SORT)是建立在归并操作上的一种有效的排序算法,该算法是采用分治法(Divide andConquer)的一个非常典型的应用。将已有序的子序列合并,得到完全有序的序列;即先使每个子序列有序,再使子序列段间有序。若将两个有序表合并成一个有序表,称为二路归并。 归并排序核心步骤:

动画图解如下:
//归并排序
void _MergeSort(int* a, int begin, int end, int* temp) {
	//只有一个元素就返回
    if (begin == end)
		return;

    //从中间开始分割
	int mini = (begin + end) / 2;

    //指向两个序列起始位置
	int begin1 = begin, end1 = mini;
	int begin2 = mini + 1, end2 = end;

    
	_MergeSort(a, begin1, end1, temp);
	_MergeSort(a, begin2, end2, temp);
	
    //单趟
    //选下的插入到新的序列中
	int i = begin;//插入位置要保持相对位置
	while (begin1 <= end1 && begin2 <= end2)
	{
		if (a[begin1] < a[begin2]) 
		{
			temp[i++] = a[begin1++];
		}
		else
		{
			temp[i++] = a[begin2++];
		}

	}
    
    //插入没有插入完的
	while (begin1 <= end1)
	{
		temp[i++] = a[begin1++];
	}

	while (begin2 <= end2)
	{
		temp[i++] = a[begin2++];
	}

    //拷贝位置也要保持相对位置
	memcpy(a + begin, temp + begin, sizeof(int) * (end - begin + 1));

}

void MergeSort(int* a, int n) {

    //开辟新的序列
	int* temp = (int*)malloc(sizeof(int) * n);
	if (temp == NULL)
	{
		perror("malloc fail");
		return;
	}
	
	_MergeSort(a, 0, n-1, temp);

	free(temp);
	temp = NULL;

}

非递归版本

我们之前是用递归先进行单个排序,在到组排。我们也可以不通过递归,通过回溯的思想,先定义一个gap,gap从1开始,即先单个为一组,然后两两排序,然后进行循环gap×2来进行gap数量为一组在进行归并。

void MergeSortNonR(int* a, int n)
{
	int* temp = (int*)malloc(sizeof(int) * n);
	if (temp == NULL)
	{
		perror("malloc fail");
		return;
	}

	int gap = 1;

	while (gap < n) 
	{
		for (int j = 0; j < n; j += gap * 2)
		{
            //两个区间的起始位置
			int begin1 = j, end1 = j + gap - 1;
			int begin2 = begin1 + gap, end2 = begin2 + gap - 1;
			
            //判断是否有越界

			if (end1 >= n || begin2 >= n)
				break;

			if (end2 >= n)
				end2 = n - 1;
			
			int i = j;

			while (begin1 <= end1 && begin2 <= end2)
			{
				if (a[begin1] <= a[begin2])
				{
					temp[i++] = a[begin1++];
				}
				else
				{
					temp[i++] = a[begin2++];
				}

			}

			while (begin1 <= end1)
			{
				temp[i++] = a[begin1++];
			}

			while (begin2 <= end2)
			{
				temp[i++] = a[begin2++];
			}

            //相对位置
			memcpy(a + j, temp + j, sizeof(int) * (end2 - j + 1));
		}
		gap *= 2;
	}
	
	free(temp);
	temp = NULL;
}

归并排序

时间复杂度:O(N*long N)

空间复杂度:O(N)

稳定性:稳定

2.8 计数排序

上面的排序,我们都是通过比较来排序的,而计数排序是非比较排序算法。
思路:遍历序列,找到最大最小的值,通过它们的差值来新开一个序列,在遍历序列,通过序列的值减去最小值作为下标在新开序列的位置计数。最后在遍历序列,该新序列有值则新序列的下标加最小值即为该序列上的值。
//计数排序
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)
	{
		perror("malloc fail");
		return;
	}

	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 < n; i++)
	{
		while (count[i]--)
		{
			a[i] = i + min;
		}
	}
}

计数排序

时间复杂度:O(N+range)
空间复杂度:O(range)

三、算法性能

算法性能再不同场景下有不一样的性能,下图参考:

总结

上述文章,我们介绍了各种排序算法,希望对你有所帮助。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值