【数据结构八大排序算法】

前言

排序,就是一串数据,按照其中的某个或某些关键字的大小,递增或者递减的排列起来的操作。一个优秀的算法可以节省大量的资源

一、.直接插入排序

直接插入排序,它是一种简单的插入排序法,它的基本思想是:把待排序的元素逐个插入到一个已经排好序的有序序列中,直到所有的记录插入完为止,然后得到一个新的有序序列 。
生活中的例子有很多,比如打扑克牌的时候,就能运用插入排序的思想。
在这里插入图片描述
当插入第i(i>=1)个元素时,前面的array[0],array[1],…,array[i-1]已经排好序,此时用array[i]的排序码与array[i-1],array[i-2],…的排序码顺序进行比较,找到插入位置即将array[i]插入,原来位置上的元素顺序后移。
为了更形象的展现插入排序的思想,这里用动图来展现。
插入排序动图演示
代码实现:
在这里可以把第一个元素看成有序序列,,一个元素序列一定是有序的,然后进行多次多趟排序。

void Insertsort(int* arr,int size)
{
//arr是数组名,size是数组元素个数
	for(int i=0;i<size-1;i++)//整趟排序的循环
	{
	//单趟插入
	//让[0,end]区间值为有序 
	
		int end=i;
		int temp=arr[end+1];//排序元素放在一个tmp变量里
		while(end>=0)让循环停止的条件有两个,end<0和arr[end] <= temp
		{
			if(arr[end]>temp)
			{
				arr[end+1]=arr[end];//后挪元素
				end--;
				end--;
			}
			else
			{
				break;
			}
		}
		arr[end+1]=temp;//在end+1的位置赋值为temp
	}
}

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

1.时间复杂度:O(N^2) (第一次最坏移动一次元素,第二次最坏移动两次元素,以此类推,第n次最坏移动n次元素,所以计算公式为( 1 + n ) ∗ n / 2 近似于O(N ^ 2))
2.空间复杂度:O(1)
3.稳定性:稳定
4.初始数据集的排列顺序对算法的性能有无影响:有影响。 解释:有序的情况下就不需要往前移动元素了,但是整趟排序最好的情况下外面的for循环也要进行n次。(元素集合越接近有序,直接插入排序算法的时间效率越高)

二、希尔排序

希尔排序法又称缩小增量法
希尔排序法的基本思想是:先选定一个整数,设这个整数为gap,把待排序文件中所有记录分成gap个组,所有距离为gap的元素分在同一组内,并对每一组内的元素进行排序。然后再取下一组重复排序的工作。然后缩小gap,当gap到达为1时,所有记录在统一组内的数据就排序完成了。
希尔排序
注意:
1.当gap越大的时候,预排序越快,但是越不接近有序。
2.当gap越小,数据处理的越慢,但越接近有序。
3.当gap为1就是直接插入排序。
代码实现:

void ShellSort(int* arr, int size)//希尔排序
{
	int gap = size;
	//多次预排+最后一次直接插入排序
	while (gap > 1)
	{
		gap = gap / 3 + 1;//控制最后一次进来gap为1进行直接插入排序
		//gap = gap / 3 + 1,为了让gap从很大变到1,gap = gap / 2+ 1也可以
		for (int i = 0; i < size-1 - gap; i++)//end+gap<size,则所以i最大为size - 1 - gap
		{
			int end = i;
			int tmp = arr[end + gap];
			while (end >= 0)
			{
				if (tmp < arr[end])
				{
					arr[end + gap] = arr[end];
					end -= gap;
				}
				else
				{
					break;
				}
			}
			arr[end + gap] = tmp;
		}
		if (gap == 1)//一旦gap等于1,就证明刚才的排序就是直接插入排序了,循环停止
		{
			break;
		}
	}
}

希尔排序的特性总结:

1.希尔排序是对直接插入排序的优化。
2.当gap > 1时都是预排序,目的就是让数组更接近于有序。当gap == 1时,数组已经接近有序的了,这样就会很快。这样整体而言,可以达到优化的效果。
3.最坏:O(N ^ 1.3).。希尔排序的时间复杂度不好计算,因为gap的取值方法很多,导致很难去计算,因此在很多书中给出的希尔排序的时间复杂度都不固定:查阅相关资料得出,时间复杂度为O(N^1.25)~O(1.6*N
^1.25)。(《数据结构-用面相对象方法与C++描述》—— 殷人昆)
4.稳定性:不稳定。
5.初始数据集的排列顺序对算法的性能有无影响:有影响。 解释:希尔是对插入排序的优化,这种优化是在无序的序列中才有明显的效果,如果序列接近有序,反而是插入最优。

三、选择排序

选择排序,是在未排序序列中找到最小(大)元素,存放到排序序列的两端位置,然后以此类推,直到所有元素均排序完毕。
为了更形象的展现排序过程。下面进行动态展示:(未优化的,只找最小,代码是优化后的,同时找最小和最大)
选择排序过程
代码实现:

void SelectSort(int* arr, int size)//优化选择排序
{
	int begin = 0;
	int end = size - 1;
	while (begin < end)
	{
		int mini = begin, maxi = begin;
		for (int i = begin + 1; i <= end; i++)
		{
			if (arr[i] < arr[mini])
			{
				mini = i;
			}
			if (arr[i] > arr[maxi])
			{
				maxi = i;
			}
		}
		Swap(&arr[mini], &arr[begin]);
		//如果maxi = begin,上一步交换了begin和mini的值,会影响maxi指向的值
		if (maxi == begin)
		{
			maxi = mini;
		}
		Swap(&arr[maxi], &arr[end]);
		begin++;
		end--;
	}
}

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

1.思考好理解,但效率不是很好,实际中较少使用
2. 时间复杂度:(最坏)O( N^2)
3. 空间复杂度:O(1)
4. 稳定性:不稳定
5.初始数据集的排列顺序对算法的性能有无影响:没有影响。 解释:无论有序还是无序,都要全部进行比较。

四、堆排序

堆排序详细分析

  1. 堆排序使用堆来选数,效率就高了很多。
  2. 时间复杂度:O(N*logN)(N是建堆的时间,logN是在堆中查找的时间)
  3. 空间复杂度:O(1)
  4. 稳定性:不稳定
    5.初始数据集的排列顺序对算法的性能有无影响:没有影响。 解释:雷打不动的就是一直排序到最后一个元素,无论有序还是无序。

五、冒泡排序

冒泡排序,它是通过遍历比较左右值的大小,例如排升序即左值大于右值交换,最后最大值即排到最右边。
它是一种简单直观的排序算法。它重复地走访过要排序的数列,一次比较两个元素,如果他们的顺序错误就把他们交换过来。一趟冒泡排序就可以把一个最大或者最小的挑出来,重复地进行直到没有再需要交换,也就是说该数列已经排序完成了。
冒泡排序还有一种优化算法,就是立一个 flag,当在一趟序列遍历中元素没有发生交换,则证明该序列已经有序。但这种改进对于提升性能来说并没有什么太大作用。

冒泡排序
代码实现:

void BubbleSort(int* arr, int size)//冒泡排序
{
	for (int i = 1; i < size; i++)
	{
		int flag = 0;
		for (int j = 0; j < size - i; j++)
		{
			if (arr[j] > arr[j + 1])
			{
				Swap(&arr[j], &arr[j + 1]);
				flag++;
			}
		}
		if (flag == 0)
		{
			break;
		}
	}
}

冒泡排序的特性总结:

1.容易理解的排序
2.时间复杂度:O(N^2)
3.空间复杂度:O(1)
4.稳定性:稳定
5.初始数据集的排列顺序对算法的性能有无影响:有影响。 解释:我们可以设置一个flag,若一趟排序中没有数据交换的情况下,根据flag的值就可以控制结束整个程序的进行。

六、快速排序

这是一种二叉树结构的交换排序方法,其基本思想为:任取待排序元素序列中的某元素作为基准值,按照该排序码将待排序集合分割成两子序列,左子序列中所有元素均小于基准值,右子序列中所有元素均大于基准值,然后最左右子序列重复该过程,直到所有元素都排列在相应位置上为止。

1.交换排序方法(hoare法)
背景:创始人:Hoare,时间:1962年
在这里插入图片描述
完整的快速排序思路分析
1.这样的一趟排序返回的这个meeti其实就是a[key]在数组中的正确位置,而且a[key]的左边都是小于a[key]的数了,a[key]的右边都是大于a[key]的数了,相当于这样的一种状态[begin, keyi-1] keyi [keyi+1, end],
2.下一次就是再把[begin, keyi-1]这个区间和 [keyi+1, end]放入单趟排序函数里再排序,直到这个区间最后只剩下0个或者一个元素的时候递归就可以停止了。
代码实现:
单趟排序
1、先选一个key(数组下标)。(一般是第一个或者是最后一个)
2、单趟排序,要求小在的key的左边,大的在key的右边
代码实现:

// hoare
int PartSort1(int* a, int left, int right)
{
	int keyi = left;
	while (left < right)
	{
		// R找小
		while (left < right && a[right] >= a[keyi])
		{
			--right;
		}
		// L找大
		while (left < right && a[left] <= a[keyi])
		{
			++left;
		}

		if (left < right)
			Swap(&a[left], &a[right]);
	}
	int meeti = left;
	Swap(&a[meeti], &a[keyi]);
	return meeti;
}

完整的快速排序思路分析
1.这样的一趟排序返回的这个meeti其实就是a[key]在数组中的正确位置,而且a[key]的左边都是小于a[key]的数了,a[key]的右边都是大于a[key]的数了,相当于这样的一种状态[begin, keyi-1] keyi [keyi+1, end],
2.下一次就是再把[begin, keyi-1]这个区间和 [keyi+1, end]放入单趟排序函数里再排序,直到这个区间最后只剩下0个或者一个元素的时候递归就可以停止了。
代码实现:

// [begin, end]
void QuickSort(int* a, int begin, int end)
{
	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);
}

对快速排序算法优化
上述方法其实还有缺陷,由于我们每次选定key的都是待排序列的最左边第一个,然后经过一次排序就把它放到了它的最终位置。下面是理想情况的递归展开图,这样的前提条件就是整个序列处于一种混乱无序的状态。
在这里插入图片描述

假如待排序列是有序或者是接近有序的递归展开图是怎样的呢?如下图,我们从最左边选一个数最后经过排序放到了最左边。
在这里插入图片描述
把它想象成二叉树,就是一颗没有左子树只有右子树的二叉树,执行次数是从N到1递减的,递归深度由理想状态的logN变为N,这样的递归深度数据量大的情况下可能会导致栈溢出。
所以我们要对选key进行优化:
1、随机选一个位置做key
2、针对有序,选定中间值做key
3、 三数取中。在待排序数列的第一个位置和中间位置以及最后一个位置 选出中间值,然后把这个中间值与第一个位置交换

优化实现代码(三数取中)


int GetMidIndex(int* a, int left, int right)
{
	int mid = left + (right - left) / 2;
	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;
		}
	}
}


问题延伸:
假设最后某一层的每一个待排序列只剩下8个数的时候,这一个序列就要递归调用7到8次左右才能完成排序,这样做很不划算,而数据量大的时候等到这一层有很多个这样的序列在等着我们呢
在这里插入图片描述

解决方法:
小区间优化:当待排序列小于或者等于8时就采用插入排序的算法。
代码实现:

void QuickSort(int* a, int begin, int end)
{
	if (begin >= end)
	{
		return;
	}

	if (end - begin <= 8)//待排序列小于或者等于8时我们就采用插入排序
	{
		InsertSort(a + begin, end - begin + 1);
	}
	else
	{
	int keyi = PartSort3(a, begin, end);
	//[begin, keyi-1] keyi [keyi+1, end]

	QuickSort(a, begin, keyi - 1);
	QuickSort(a, keyi + 1, end);
	}
}

2…其他快速排序单趟排序方法——挖坑法
在这里插入图片描述
解释:把左边第一个数定义为key并且从L开始先将其定义为坑位,然后R - - 找小(小于key的数),放入左边的坑位,更新坑位到R的位置。之后L++找大(大于key的数)放入右边的坑位,再更新坑位到L的位置,这样循环往复。
直到L和R相遇,再把key填到L和R相遇的位置,然后返回这个相遇位置,为下一次递归做准备。
实现代码:

// 挖坑法
int PartSort2(int* a, int left, int right)
{
	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;
}

3.前后指针法:
过程:
1.首先选择一个keyi位置,一般为序列首。
2.创建两个指针,prev指向keyi,cur指向prev+1
3.cur往右找小于keyi位置的值,prev往前找大于keyi位置的值,都找到了,然后交换cur和prev位置的值。
4.最后cur走完序列,再把keyi和prev位置值交换,这样keyi左边都会比他小,右边都会比他大
5.再将区间分为[begin,keyi-1],[keyi+1,end]继续递归直至有序。
动图展示:
前后指针
代码实现:

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])
		{
			++prev;
			Swap(&a[cur], &a[prev]);
		}
		
		++cur;
	}

	Swap(&a[keyi], &a[prev]);

	return prev;
}

快速排序特性总结:

  1. 快速排序整体的综合性能和使用场景比较好的
  2. 时间复杂度:O(N*logN)
  3. 空间复杂度:O(logN)(递归一边(左边)建立了logN层栈帧,退回去再调用另一边(右边),用的是之前的空间。
  4. 稳定性:不稳定
    5.初始数据集的排列顺序对算法的性能有无影响:有影响。

七、归并排序

归并排序将已有序的子序列合并,得到完全有序的序列;即先使每个子序列有序,再使子序列段间有序。若将两个有序表合并成一个有序表,称为二路归并。
在这里插入图片描述

实现:
先创建一个和a大小相同的数组,因为有数组a和个数n是不够的,所以创建一个_MergeSort()作为MergeSort()的一个子函数来控制开头(begin)和结尾(end),先取中间值坐标,若中间值坐标的左右两个数组都有序,那么取两个数组中小的尾插,最后再拷贝回原数组归并哪部分就拷贝哪部分回去。
在这里插入图片描述

代码实现:

void _MergeSort(int* a, int begin, int end, int* tmp)
{
	if (begin == end)
		return;

	int mid = (end + begin) / 2;
	// [begin, mid] [mid+1, end]

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

	// 取小的尾插
	// [begin, mid] [mid+1, end]
	int begin1 = begin, end1 = mid;
	int begin2 = mid+1, end2 = end;
	int i = begin;
	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++];
	}

	// 拷贝回原数组 -- 归并哪部分就拷贝哪部分回去
	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 fail");
		return;
	}

	_MergeSort(a, 0, n - 1, tmp);

	free(tmp);
	tmp = NULL;
}

在这里插入图片描述
在这里插入图片描述
特性总结:
1.时间复杂度:O(N*logN)
2.空间复杂度:O(N)
3.稳定性:稳定。上面递归的代码,加上一个等于号,就能控制它是稳定的
4.归并的缺点在于需要O(N)的空间复杂度,归并排序的思考更多的是解决在磁盘中的外排序问题。

八、计数排序

1.统计相同元素出现次数根据
2.统计的结果将序列回收到原来的序列中
3.计数排序只适用于范围集中且重复数据较高的数据
在这里插入图片描述

代码实现:

//计数排序只适用于范围集中且重复数据较高的数据
void CountSort(int* arr, int size)//计数排序
{
	int min = arr[0];
	int max = arr[0];
	for (int i = 1; i < size; i++)
	{
		if (arr[i] < min)
		{
			min = arr[i];
		}
		if (arr[i] > max)
		{
			max = arr[i];
		}
	}
	
	//计数数组count
	int range = max - min + 1;
	int* count = (int*)malloc(sizeof(int) * range);
	if (count == NULL)
	{
		perror("malloc:fail");
		exit(-1);
	}
	memset(count, 0, sizeof(int) * range);

	//开始计数
	for (int i = 0; i < size; i++)
	{
		count[arr[i] - min]++;
	}
	
	//回写排序
	int j = 0;
	for (int i = 0; i < range; i++)
	{
		while (count[i]--)
		{
			arr[j++] = i + min;
		}
	}
}

九、八大排序算法总结

在这里插入图片描述
以上就是关于数据结构八大排序算法的全部内容,后期博主会继续分享关于数据结构的更深层次知识,关注不迷路呦。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值