排序总结及详讲

排序的概念

所谓排序,就是使一串记录,按照其中的某个或某些关键字的大小,递增或递减的排列起来的操作。
在我们的日常生活中,也应用到很多地方,例如某地区考试完的全部排名。

1、 插入排序

1.1 直接插入排序

直接插入排序的基本思想就是把待排序的数值,插入到已经排序好的有序序列中,直到所有的数值都插入完为止,得到一个新的有序序列;实际上就跟我们平时玩扑克牌时是一样的,都是引用的插入排序的思想。
在这里插入图片描述

void InsertSort(int* a, int n)
{
	for (int i = 1; i < n; i++)
	{
		int end = i-1;
		int tmp = a[i];

		while (end>=0)
		{
			if (a[end]>tmp)
			{
				a[end + 1] = a[end];
				end--;
			}
			else
			{
				break;
			}
		}

		a[end + 1] = tmp;
	}
}

就比如刚开始给了这样一个序列数值

在这里插入图片描述

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
以此类推,从上面的过程我们可以看出来,插入排序的时间复杂度最复杂的是O(n^2),最好情况为O(n)。

1.2 希尔排序(缩小增量排序)

希尔排序法又称缩小增量法。希尔排序法的基本思想是:先选定一个整数,把待排序文件中所有记录分成个组,所有距离为的记录分在同一组内,并对每一组内的记录进行排序。然后,取,重复上述分组和排序的工作。当到达= 1时,所有记录在统一组内排好序。
在这里插入图片描述

//希尔排序
void ShellSort(int* a, int n)
{
int gap = n;
	while (gap>1)
	{
		gap = gap / 2;
		for (int i = 0; i < n-gap; i++)
		{
			int end = i;
			int tmp = a[i + gap];

			while (end>=0)
			{
				if (a[end]>tmp)
				{
					a[end + gap] = a[end];
					end -= gap;
				}
				else
				{
					break;
				}
			}
			a[end + gap] = tmp;
		}
	}
}

希尔排序的特点:
1、希尔排序是对直接插入排序的优化。
2、当gap >1 时都是预排序,目的是让数组更接近于有序。当gap == 1时,数据已经接近有序的了,这样就会很快。这样整体而言,可以达到优化的效果。
3、希尔排序的时间复杂度不是很好计算,因为gap的取值方法很多,导致很难去计算。暂时是按照O(n^1.25)来计算的。

2、选择排序

2.1选择排序

概念:
每一次从待排序的数据元素中选出最小(或最大)数据元素,存放在序列的起始或者末尾位置,直到全部待排序的数据元素排完。

//交换数据元素函数
void Swap(int* p1, int* p2)
{
	int tmp = *p1;
	*p1 = *p2;
	*p2 = tmp;
}

//选择排序
//时间复杂度永远为o(n^2)
void SelectSort(int* a, int n)
{
	int left = 0;
	int right = n - 1;
	while (left < right)
	{
		int mini = left;
		int maxi = left;
		for (int i = left; i <= right; i++)
		{
			if (a[i] < a[mini])
			{
				mini = i;
			}
			if (a[i] > a[maxi])
			{
				maxi = i;
			}
		}
		Swap(&a[left], &a[mini]);

		if (maxi == left)
		{
			maxi = mini;
		}

		Swap(&a[right], &a[maxi]);
		left++;
		right--;
	}
}

while循环的作用是用来找数组中最小值和最大值坐标的,每次都是循环遍历一遍未排序过的数组。
当结束while循环后,开始交换数据。
在这里插入图片描述

在这里插入图片描述

这里来解释一下while循环里头的if语句

在这里插入图片描述

  • 在元素集合 array[i] – array[n-1] 中选择关键码最大(小)的数据元素
  • 若它不是这组元素中的最后一个(第一个)元素,则将它与这组元素中的最后一个(第一个)元素交换
  • 在剩余的array[i]–array[n-2](array[i+1]–array[n-1])集合中,重复上述步骤,直到集合剩余1个元素
    由此看来它的时间复杂度最好最坏的情况都是O(n^2)
2.2 堆排序
  • 概念:堆排序是指利用堆积树这种数据结构所设计的一种排序算法,它是选择排序的一种。它是通过堆来进行选择选择数据。需要注意的是排升序要建大堆,排降序建小堆。
//堆排序
void AdjustDown(int* a, int n, int parent)
{
	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;
	}
}
void HeapSort(int* a, int n)
{
	int i;
	for (i = (n - 2) / 2; i >= 0; i--)
	{
		AdjustDown(a, n, i);
	}

	//自己实现排序
	int end = n - 1;
	while (end > 0)
	{
		Swap(&a[end], &a[0]);
		AdjustDown(a, end, 0);
		end--;
	}
}

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
略…

从上面的简介图的过程来看,向下取整是从最后一个孩子结点的父亲结点开始向下取整,直到所有的父亲结点都过完一次才结束,大的数字往上浮,小的数字往下面沉,这种操作也叫建大堆
向下取整的时间复杂度为o(N)

  • 当堆建好后,我们就可以自己来利用堆来建立有序数组了
    在这里插入图片描述
  1. 堆排序使用堆来选数,效率就高了很多。
  2. 时间复杂度:O(N*logN)

3、交换排序

基本思想:所谓交换,就是根据序列中两个记录数组下标的元素大小,来对换这两个元素在数组中的位置,交换排序的特点是:将数据元素较大的记录向数组尾部移动,较小的往数组前部移动。

3.1 冒泡排序

冒泡排序就跟气泡往上浮一个倒立,每次运行把数组中最大的数据元素移动到数组尾部。最大的往上浮,最小的往下浮。

void BubbleSort(int* a, int n)
{
	for (int i = 0; i < n-1; i++)
	{
		bool exchange = false;
		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 = true;
			}
		}
		if (exchange == false)
		{
			break;
		}
	}
}

在这里插入图片描述
每次都会选择两个数进行比较,最大的数往后移动,将数据元素较大的记录向数组尾部移动,较小的往数组前部移动。当内部的for循环完成一整次遍历后,就会排好一个最大数,所以第二次进入内部排序的时候,是不需要跟数组最后一个元素比较的,所以是 n - i。

3.2 快速排序

快速排序是从冒泡排序演变过来的,但是它的效率要比冒泡排序高效的多,它的算法和冒泡排序类似,像冒泡排序是将大的数或者小的数往上浮,而快速排序是选一个数组,然后大于这个数的数值放到这个数的右边,小于这个数的数值放到左边。

冒泡排序
冒泡排序是讲最大的数往最后放
在这里插入图片描述

快速排序
快速排序是提前选一个值,然后把这个值排到它有序的位置,它的左边的数值都比它小,它的右边都比它大。
在这里插入图片描述

冒泡排序我们上面已经讲述过是怎么移动的了,接下来,我们来讲述一下快速排序是怎么一步一步移动的,直到它为有序数组。

3.2.1 快速排序(霍尔Hoare)
void QuickSort(int* a, int left, int right)
{
	if (left >= right)
	{
		return;
	}
	
	int begin = left;
	int end = right;
	int keyi = left;
	while (left < right)
	{
		//右边找小
		while (left < right && a[right] >= a[keyi])
		{
			--right;
		}
		//左边找大
		while (left < right && a[left] <= a[keyi])
		{
			++left;
		}

		Swap(&a[right], &a[left]);
	}
	Swap(&a[keyi], &a[left]);
	keyi = left;
	QuickSort1(a, begin, keyi - 1);
	QuickSort1(a, keyi + 1, end);
}

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

在这里插入图片描述

过程图
在这里插入图片描述

在这里插入图片描述

在这里插入图片描述
在这里插入图片描述

这时我们完成了第一趟排序,并使得基准值左子树都是小于它的,右子树都是大于它的,接下来,我们通过递归,来实现序列完全有序。

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

这时所有的左子树全部递归排序完
接下来,我们来考虑一下效率问题

在这里插入图片描述
上图看来,采用三数去中的方法效率是较为高的,我们这里是通过计算它的结束时间 减去 它的开始时间来分析程序的运行效率。

//选三个数,取中间数为key,并返回
int GetMidNumi(int* a, int left, int right)
{
	int mid = (left + right) / 2;
	if (a[left]>a[right])
	{
		if (a[right]>a[mid])
		{
			return right;
		}
		else
		{	//a[right]<a[mid]
			if (a[mid]>a[left])
			{
				return left;
			}
			else
			{
				return mid;
			}
		}
	}
	else
	{
		//a[left]<a[right]
		if (a[left]>a[mid])
		{
			return left;
		}
		else
		{//a[left]<a[mid]
			if (a[right] < a[mid])
			{
				return right;
			}
			else
			{
				return mid;
			}
		}
	}
}
//快速排序
void PartSort1(int* a, int left, int right)
{
	if (left >= right)
	{
		return;
	}
	//随机选一个key

	/*int randi = left + (rand() % (right - left));
	Swap(&a[left], &a[randi]);*/

	//选三个数,取中间数为key
	int midi = GetMidNumi(a, left, right);
	if (left!=midi)
	{
		Swap(&a[left], &a[midi]);
	}
	int begin = left;
	int end = right;
	int keyi = left;
	while (left < right)
	{
		//右边找小
		while (left < right && a[right] >= a[keyi])
		{
			--right;
		}
		//左边找大
		while (left < right && a[left] <= a[keyi])
		{
			++left;
		}

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

因为每次都需要递归调用,所以我们把这个递归调用放到了单个的函数中,方便其他方法调用

//递归实现快速排序
void QuickSort(int* a, int left, int right)
{
	if (left >= right)
		return;

	int keyi = PartSort1(a, left, right);
	QuickSort(a, left, keyi - 1);
	QuickSort(a, keyi + 1, right);
}
3.2.2 快速排序(挖坑法)

在这里插入图片描述
首先先选出一个基准值,将这个基准值拷贝后,剩下的步骤跟霍尔排序逻辑一样,设置两个指针,一个指向尾部,一个指向头部,右边先走并且找比基准值小的,左边后走,并且找比基准值大的数。当右边找到比基准值小的数值的时候,直接将该数值放到指定的坑位,并且该值所在的位置成为最新的坑位,以此类推,直到while循环条件不成立,将基准值添到最后的坑位。

//挖坑法
int PartSort2(int* a, int left, int right)
{
	//选三个数,取中间数为key
	int midi = GetMidNumi(a, left, right);
	if (midi!= left)
	{
		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;

}
3.2.3 快速排序(前后指针)

加粗样式

采用前后指针也是可以完成快速排序的,当cur遇到了比基准值小的时候,prev后挪一位,并且交换两个所指向的值,当cur>rightde的时候,交换基准值和prev,并且更新基准值的下标,再进行下一次递归调用,以此类推,来实现有序数组。

//前后指针法
int PartSort3(int* a, int left, int right)
{
	//选三个数,取中间数为key
	int midi = GetMidNumi(a, left, right);
	if (midi!=left)
	{
		Swap(&a[left], &a[midi]);
	}
	int keyi = left;
	int prev = left;
	int cur = prev + 1;
	while (cur <= right)
	{
		/*if (a[cur]<a[keyi])
		{
			++prev;
			if (cur != prev)
			{
				Swap(&a[cur], &a[prev]);
				++cur;
			}
			else
			{
				++cur;
			}
		}
		else
		{
			cur++;
		}*/

		if (a[cur] < a[keyi] && ++prev != cur)
			Swap(&a[cur], &a[prev]);

		++cur;
	}
	Swap(&a[keyi], &a[prev]);
	keyi = prev;
	return keyi;
}
3.2.3 快速排序(非递归实现,利用栈)

根据栈的特点,先进后出,来模拟实现递归。原理根递归实现的原理基本相似,这里需要注意的是入栈的顺序。

//非递归实现快速排序(运用栈)
void QuickSortNonR(int* a, int left, int right)
{
	ST st;
	STInit(&st);
	STPush(&st, right);
	STPush(&st, left);
	while (!(STEmpty(&st)))
	{
		int begin = STTop(&st);
		STPop(&st);
		int end = STTop(&st);
		STPop(&st);

		int keyi = PartSort3(a, begin, end);
		if (keyi + 1 < end)
		{
			STPush(&st, end);
			STPush(&st, keyi + 1);
		}

		if (begin < keyi - 1)
		{
			STPush(&st, keyi - 1);
			STPush(&st, begin);
		}
	}

	STDestroy(&st);
}

4、归并排序

归并算法是采用分治法的一个非常典型的应用。将已有序的子序列合并,得到完全有序的序列,即先使每个子序列有序,再是子序列段间有序。若将两个有序表合并成一个有序表,称为二路归并。
在这里插入图片描述

4.1、归并排序(递归实现)
//递归实现归并排序
void _MergeSort(int* a, int begin, int end, int* tmp)
{
	if (begin >= end)
	{
		return;
	}

	int mid = (begin + end) / 2;
	_MergeSort(a, begin, mid, tmp);
	_MergeSort(a, mid+1, end, tmp);

	int begin1 = begin;
	int end1 = mid;
	int begin2 = mid+1;
	int 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,sizeof(int)* (end - begin + 1));
}

//归并排序
void MergeSort(int* a, int n)
{
	int tmp = (int*)malloc(sizeof(a) * n);
	assert(tmp);

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

	free(tmp);
	tmp = NULL;
}

在这里插入图片描述
在这里插入图片描述

在这里插入图片描述

4.2.1、归并排序(非递归实现,所有数值比较完后拷贝回去)
//归并排序非递归,for循环完成后,直接把tmp拷贝给数组a
void MergeSortNonR(int* a, int n)
{
	int* tmp = (int*)malloc(sizeof(int) * n);
	assert(tmp);
	int gap = 1;

	while (gap < n)
	{
		for (int i = 0; i < n; i += 2 * gap)
		{
			int begin1 = i, end1 = i + gap - 1;// 
			int begin2 = i + gap, end2 = i + 2 * gap - 1;
			int j = i;
			if (end1 >= n)
			{
				end1 = n - 1;
				begin2 = n;
				end2 = n - 1;
			}
			else
			{
				if (begin2 >= n)
				{
					begin2 = n;
					end2 = n - 1;
				}
			}

			if (end2 >= n)
			{
				end2 = n - 1;
			}
			while (begin1<= end1 && begin2 <= end2)
			{
				if (a[begin1] <= a[begin2])
				{
					tmp[j++] = a[begin1++];
				}
				else
				{
					tmp[j++] = a[begin2++];
				}
			}

			while (begin1 <= end1)
			{
				tmp[j++] = a[begin1++];
			}

			while (begin2 <= end2)
			{
				tmp[j++] = a[begin2++];
			}
		}
		//for循环完成后,直接把tmp拷贝给数组a
		memcpy(a, tmp, sizeof(int) * n);
		gap = gap * 2;
	}
	free(tmp);
	tmp = NULL;
}

在这里插入图片描述
此时将tmp数组的所有元素全部拷贝给a数组,并且让gap的值放大两倍
在这里插入图片描述

此时将tmp数组的所有元素全部拷贝给a数组,并且让gap的值再放大两倍
在这里插入图片描述

此时,数组已经有序,并且gap再放大二倍的话while循环条件也不成立了,所以程序会直接结束。
在这里我们要思考的问题是,当数组时偶数个的时候,这个方法是可行的,如果数组是奇数个时,是不是会存在指针所指向的位置不存在呢,答案是肯定的,这里我们也做了对应的解决办法,那就是修正路线。

在这里插入图片描述

在没有修正的时候,红色框框标注的都是越界了的,导致程序不能正常运行,这时我们就需要对程序进行修正了

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

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

在这里插入图片描述

这就是我们修正以后的效果,当end1越界后,我们就不需要在对后边的值进行排序了,因为没有可比较的,直接拷贝回去即可,同样begin2也是一样的,但是单end2越界后,我们要将它修正到数组尾部,这样尾部的几个元素也就参与到排序了,就不会出现越界问题了

4.2.2、归并排序(非递归实现,比较以此拷贝一次)
//归并排序非递归,每比较一次,拷贝一次
void MergeSortNonR(int* a, int n)
{
	int* tmp = (int*)malloc(sizeof(int) * n);
	assert(tmp);
	int gap = 1;

	while (gap < n)
	{
		for (int i = 0; i < n; i += 2 * gap)
		{
			int begin1 = i, end1 = i + gap - 1;
			int begin2 = i + gap, end2 = i + 2 * gap - 1;
			int j = i;
			if (end1 >= n || begin2 >= n)
			{
				break;
			}

			if (end2 >= n)
			{
				end2 = n - 1;
			}
			while (begin1 <= end1 && begin2 <= end2)
			{
				if (a[begin1] <= a[begin2])
				{
					tmp[j++] = a[begin1++];
				}
				else
				{
					tmp[j++] = a[begin2++];
				}
			}

			while (begin1 <= end1)
			{
				tmp[j++] = a[begin1++];
			}

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

			//每比较一次,拷贝一次
			memcpy(a, tmp, sizeof(int) *j);
		}
		gap = gap * 2;
	}
	free(tmp);
	tmp = NULL;
}

4、计数排序

概念:
计数排序是一种非比较排序,其核心是将序列中的元素作为键存储在额外的数组空间中,而该元素的个数作为值存储在空间中,通过遍历该数组排序。
这里值得注意的是:数组空间的大小是通过最大数值减去最小数值来计算数组的范围的,这个范围就是数组的大小,所以,这个差值不能过大,这主要是防止建立数组时造成内存的浪费。
序列中存在的元素是整数,因为我们使用的是该元素作为键存储在额外的数组空间中,如果不是整数,不能作为键。
当然,如果这个序列的数值都比较接近话,排序的效率还是很高的,反之,它的空间也会浪费很多

void CountSort(int* a, int n)
{
	int min = a[0], max = a[0];
	for (int i = 0; i < n; i++)
	{
		if (a[i]<min)
		{
			min = a[i];
		}

		if (a[i]>max)
		{
			max = a[i];
		}
	}
	int range = max - min + 1;

	int* countA = (int*)malloc(sizeof(int) * range);
	assert(countA);

	memset(countA, 0, sizeof(int) * range);

	//统计各位数字出现的次数
	for (int i = 0; i < n; i++)
	{
		countA[a[i] - min]++;
	}

	//排序
	int j = 0;
	for (int i = 0; i < range; i++)
	{
		while (countA[i]--)
		{
			a[j++] = i + min;
		}
	}

	free(countA);
	countA = NULL;
}

首先,我们会从已知的序列中找出最大值和最小值,来计算开辟多大的数组空间。

在这里插入图片描述
这时我们发现数组中的元素并没有12个,缺开辟了12位的数组,这就造成了一定的浪费,但如果元素都是接近的,效率会高很多。

在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值