超详细八大排序+基数排序(图文并茂+动图演示+C语言代码演示)

插入排序-直接插入排序

思想引入

直接插入排序是一种简单的插入排序法,其基本思想是:把待排序的记录按其关键码值的大小逐个插入到一个已经排好序的有序序列中,直到所有的记录插入完为 止,得到一个新的有序序列 。 实际中我们玩扑克牌时,就用了插入排序的思想

动图演示:
在这里插入图片描述
插入排序比较好理解,就是将数组中的每一个数字拿出来往前扫描,若大于则插在其后,一直反复操作,直到拿完所有的数为止

不再做过多赘述,上代码

void InsertSort(int* a, int n)
{
	for (int i = 0;i < n - 1;i++)
	{
		int end = i;//end用来做扫描时的下标
		int x = a[end + 1];//便是此趟插入拿出来的数据
		while (end >= 0)//一直扫描到数组的头
		{
			if (a[end] > x)
			{
				a[end + 1] = a[end];
				end--;
			}
			else
			{
				break;
			}
		}
		a[end + 1] = x;

	}
}

代码逻辑动图如下:
在这里插入图片描述

在最坏情况下,也就是数组为降序时,此时要进行升序操作,时间复杂度将达到O(N^2),而在最好的情况下,插入排序的时间复杂度仅为O(N),因此数学家希尔想了一个办法,若通过某种办法使得待排序列接近有序,此时再来进行一次直接插入排序,是不是更有效了呢?于是就有了下面的希尔排序

插入排序-希尔排序

思想引入

希尔排序法又称缩小增量法。希尔排序法的基本思想是:先选定一个小于N的整数gap作为一个增量,把待排序序列中所有数据为gap的数放在一组,并对每一组进行直接插入排序,然后缩小增量,重复上述过程,直至gap==1,此时我们得到一个接近有序的序列,因此对整体数据进行一次插入排序时,时间复杂度将大大缩减。

增量gap呈现从大到小的变化,使得序列一步步接近有序

现在已知的有几种设定增量变化的方法:

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

《数据结构(C语言版)》— 严蔚敏在这里插入图片描述

《数据结构-用面相对象方法与C++描述》— 殷人昆在这里插入图片描述

下面给出多组并排的例子

在这里插入图片描述

代码:

//**希尔排序**O(N^1.3)--算是很优化的一种排序
void ShellSort(int* a, int n)
{
	//插入排序的优化版本(非常优化)
	int gap = n;
	while(gap>1)
	{
		gap = gap / 3 + 1;	 
		for (int i = 0;i < n - gap;i++)
		{
			int end = i;
			int x = a[end + gap];
			while (end >= 0)
			{
				if (a[end] > x)
				{
					a[end + gap] = a[end];
					end -= gap;
				}
				else
					break;
			}
			a[end + gap] = x;
		}
	}
}

选择排序-直接选择排序

动图演示
在这里插入图片描述

思路很简单,从待排序列中选出最小值,放在序列的起始位置,直到全部扫描完

下面代码我进行了优化,选择最小值的同时选择出了最大值放在序列的结束位置

代码如下:

// 选择排序
void SelectSort(int* a, int n)
{
	int begin = 0;
	int end = n - 1;
	while (begin < end)
	{
		int maxi = begin;
		int mini = 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 (maxi == begin)
		{
			maxi = mini;
		}
		Swap(&a[end], &a[maxi]);
		begin++;
		end--;
	}
}

选择排序-堆排序

首先,我们得明白,什么是堆?

堆,是一种完全二叉树的结构,分为大根堆和小根堆,堆排序是基于堆的二叉树结构的一种相对来说较好的排序方法。

下面用一张图简单介绍一下什么是大根堆和小根堆(以下都简称大堆和小堆)

大根堆:每个节点的值都大于或者等于他的左右孩子节点的值
在这里插入图片描述

小根堆:每个结点的值都小于或等于其左孩子和右孩子结点的值

现在我们可以引入思想

堆排序(Heapsort)是指利用堆积树(堆)这种数据结构所设计的一种排序算法,它是选择排序的一种。它是 通过堆来进行选择数据。需要注意的是排升序要建大堆,排降序建小堆

我们从思想又引入一个词——建堆

通俗点讲,就是在数组原来的物理结构的序列上,通过改变物理结构的序列,使得其逻辑结构是大堆或小堆

而建堆我们也分两种方法——自下而上建堆法和自上而下建堆法(简称向上建堆和向下建堆)

我们以向上建堆,建大堆使得物理结构是升序为例

向上建堆法使用的是向下调整算法,该算法能够保证堆一直维持着大堆或小堆的状态

向下调整算法的基本思路(以大堆为例):从根点开始,我们找到其左右孩子中较大的一个孩子与父亲比较,若父亲小于孩子,则交换,若父亲大于孩子,证明这个子树已经是大堆态,只需将较大的孩子作为父亲如此迭代下去,直到遇到叶子结点,向下调整就结束了。

已下图为例
在这里插入图片描述

在最坏的情况下,我们一共需要向下调整高度次,而二叉树的高度为log(N+1),因此时间复杂度为O(logN)

这就是一次向下调整,我们做以下思考:如果以17为根结点的子树不是一个大堆,那么这个堆还能叫大堆吗?

因此向下调整算法应该自下往上去建堆,这样才能保证每一棵子树都是大堆。

因此,我们应该从最后一个非叶子结点开始,才用向下调整去改变物理结构的序列将堆建成。(叶子结点无左右子树,因此不用考虑)

而每一对父子的下标是有规律的:

child1=parent*2+1

child2=parent*2+2

parent=(child-1)/2

下面将动图演示一下建堆过程
在这里插入图片描述
在这里插入图片描述

这样建堆就完成了,由于最后一排的叶子结点不用考虑,因此时间复杂度为每个做父亲的结点个数向下调整的高度次的次数,这是一个等差 *等比的数列求和,因此要用数学的错位相减法来求

而要完成堆排序,就是在大堆的前提上,将堆顶与堆的最后一个数据交换,并进行一次向下调整再次选出堆顶(也就是次大),调整时,最后一个数据不在调整范围内,不然又会被调回堆顶。

此时,除了最后一个数是最大数以外,其他数的逻辑结构仍然是一个大堆,于是我们就将堆顶与倒数第二个数交换,如此迭代


我们进行一下简单总结:

  1. 想要完成堆排序,前提是此连续存储的数据在逻辑结构上是一个大堆或者小堆
  2. 如果想要完成升序,则前提是大堆,降序则前提是小堆
  3. 而若逻辑结构不是堆的数据我们可以通过向下调整算法来建堆
  4. 向下调整算法可以形象的认为在向上建堆(从最后一个非叶子结点开始,保证每个子树都是堆)
  5. 在原本已经是排好序的结构中,尾插数据后,可以通过向上调整算法重新调整有序数据,使其保持原本的堆结构。

这样,堆排序就完成了

代码如下

// 堆排序
void AdjustDown(int* data, int size, int parent)
{
	assert(data);

	int child = parent * 2 + 1;

	while (child < size)
	{
		//小堆控制小于号
		//大堆控制大于号
		//比较左右孩子,再决定跟哪个孩子交换
		if (child + 1 < size && data[child + 1] > data[child])
		{
			child++;
		}
		//大堆,堆顶是最大的,因此孩子比父亲大的就交换
		if (data[child] > data[parent])
		{
			Swap(&data[child], &data[parent]);
			parent = child;
			child = parent * 2 + 1;

		}
		else
		{
			break;
		}
	}
}

void HeapSort(int* a, int n)
{
	//先向上建个大堆
	for (int i = (n - 1 - 1) / 2;i >= 0;i--)
	{
		AdjustDown(a, n, i);
	}
    //将堆顶依次往后面放
	for (int i = n-1;i > 0;i--)
	{
		Swap(&a[0], &a[i]);
		AdjustDown(a, i, 0);

	}

}

交换排序-冒泡排序

思想引入

“冒泡排序(Bubble Sort),是一种计算机科学领域的较简单的排序算法。 它重复地走访过要排序的元素列,依次比较两个相邻的元素,如果顺序(如从大到小、首字母从Z到A)错误就把他们交换过来。走访元素的工作是重复地进行,直到没有相邻元素需要交换,也就是说该元素列已经排序完成。 这个算法的名字由来是因为越小的元素会经由交换慢慢“浮”到数列的顶端(升序或降序排列),就如同碳酸饮料中二氧化碳的气泡最终会上浮到顶端一样,故名“冒泡排序”。
在这里插入图片描述

上代码:

void BubbleSort(int* a, int n)
{
	for (int i = 0;i < n - 1;i++)
	{
		int flag = 1;
		for (int j = 0;j < n - 1 - i;j++)
		{
			if (a[j] > a[j + 1])
			{
				flag = 0;
				Swap(&a[j], &a[j + 1]);
			}
		}
		if (flag)
		{
			break;
		}
	}
}

交换排序-快速排序-三种方法(重点)

快速排序分区法-hoare版本

思想引入

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

hoare版本的核心思路通俗来讲就是选取待排数组范围内的一个数作为关键字(一般是最左边或者最右边)将其放到他在有序数组中的位置上,也就是他本来应该在的位置,并且此时关键字的左边一定都是比关键字小的数,右边一定是比关键字大的数,然后再以此类推,直到所有的数都做为关键字放在了正确的位置上

现在给出图文解释比较好懂

在这里插入图片描述

left和right停下之后就进行交换

在这里插入图片描述

重复上述过程

在这里插入图片描述

当到达这一步时
在这里插入图片描述

也就是left指针与right指针相遇了,此时交换关键字key和相遇位置的值
在这里插入图片描述

到这一步时,大家有没有发现,6的左边已经都是小于6的数,而右边都是大于6的数

现在解释一下为什么最左边做key时最右边要先走:

我们知道,left指针是去找大于key的数,因此停下来的位置的值一定比key大,若左边先出发,右边后出发且此时相遇了,那么相遇点的值一定是大于key的值,此时再交换,则达不到key的左右两边要么大要么小的目的。因此我们要保证提前相遇的位置与key交换后是想要的效果,若最左边做key,则右边先走,最右边做key则左边先走。

这样,我们快速排序的单趟排序就完成了,接下来只需不断改变左右区间,使得每个数字都落到正确的位置上,直到key的左右序列排序就完成啦,我们要记住一个结论,那就是数据只有一个时,便是有序的。

对于实现快速排序的方法,我们有两种思路:

  1. 递归

  2. 非递归(栈)

    下面将逐一用代码演示

    快速排序-递归实现
    void QuickSort(int* a, int left, int right)
    {
    	//递归返回条件
    	if (left >= right)
    	{
    		return;
    	}
    	else
    	{
    		//获取第一次调用或上一次递归相遇时的下标
    		int keyi = Partion1(a, left, right);
    		//分区间分别hoare
    		QuickSort(a, left, keyi - 1);
    		QuickSort(a, keyi + 1, right);
    	}
    }
    //快速排序分区法1-hoare版本
    int Partion1(int* a, int left,int right)
    {
    	int keyi = left;
    	while (left < right)
    	{
    		//右边找小(key在最左边,考虑提前相遇的情况,右边先走)-升序,降序则相反
    		while(left < right && a[right] >= a[keyi])
    			right--;
    		//左边找大,找比keyi的值大的数
    		while(left < right && a[left] <= a[keyi])
    			left++;
    		Swap(&a[left], &a[right]);//交换
    	}
    	Swap(&a[left], &a[keyi]);
    	return left;
    }
    

在这里插入图片描述

以我的代码为例,我们对刚刚为排序完的序列做思路分析

经过上轮筛选,返回keyi=5

更新范围为[0,4],返回keyi=2——数值6的左边(同学们用纸手动模拟一下)
在这里插入图片描述

更新范围为[0,1],返回keyi=0——数值3的左边
在这里插入图片描述

更新范围为[0,0],达到返回条件(默认有序)返回上层函数——数值2的左边

更新范围[1,1],达到返回条件,返回上层函数——数值2的右边

在这里插入图片描述

此层函数调用完成,返回上层函数

更新范围[3,4],返回keyi=3——数值3的右边

在这里插入图片描述

更新范围[3,3]——数值4的左边,更新范围[4,4]——数值4的右边,此层调用结束,一直返回到进入数值6的右边

后面就不再做解释,大家有没有发现左边已经有序了?

快速排序-非递归实现

非递归实现需要用到栈来完成,这里给出的代码不包含栈,大家自行完成

由于栈的特点便是先进后出,后进先出,非常符合快速排序,下面我用一张图简单模拟过程

大家一定要自己模拟一遍

在这里插入图片描述

void QuickSortNonR(int* a, int left, int right)
{
	//用栈模拟实现
	Stack st;
	InitStack(&st);
    //先进栈
	PushStack(&st, left);
	PushStack(&st, right);
    //栈不为空就拿数据
	while (!EmptyStack(&st))
	{
        //更新区间
		int end = TopStack(&st);
		PopStack(&st);
		int begin = TopStack(&st);
		PopStack(&st);
        //拿返回值
		int keyi = Partion1(a, begin, end);//分区法代码与上面一致
        //单个数据时不再进栈
		if (keyi - 1 > begin)
		{
			PushStack(&st, begin);
			PushStack(&st, keyi - 1);
		}
		if (keyi + 1 < end)
		{
			PushStack(&st, keyi + 1);
			PushStack(&st, end);
		}
	}
	DestroyStack(&st);
}

快速排序分区法-挖坑法

下面介绍的方法将不再一一演示过程,思路都是一样的,只是方法上有些许不同,大家自行模拟,我将给出动图演示整个过程

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-EywZahhB-1680439440641)(D:\小比特\数据结构初阶\picture\快速11.gif)]

上代码:

//快速排序分区法2-挖坑法
int Partion2(int* a, int left, int right)
{
	int mini = GetMiddle(a, left, right);
	Swap(&a[mini], &a[left]);
	int keyi_val = a[left];
	int hole = left;	
	while (left < right)
	{
		//找小
		while (left < right && a[right] >= keyi_val)
		{	
			right--;
		}
		//将比keyi_val小的值扔进坑里
		a[hole] = a[right];
		hole = right;
		//找大
		while(left < right && a[left] <= keyi_val)
		{
			left++;
		}
		//将比keyi_val大的值扔进坑里
		a[hole] = a[left];
		hole = left;
	}
	//最后用keyi_val填坑
	a[hole] = keyi_val;
	return hole;

}

快速排序分区法3-前后指针法

话不多说,直接上动图😎

注:三种方法全部以最左边作为keyi,右边也一样,可自行尝试😆

在这里插入图片描述

当cur走完时,我们直接交换prev与key的值,但若最右边做key,就稍微不同了,需要++prev再交换,可以自己尝试一下

上代码😍

//快速排序分区法3-前后指针法
int Partion3(int* a, int left, int right)
{
	int prev = left;
	int cur = left + 1;
	int keyi = left;
	while (cur <= right)
	{
		if (a[cur] < a[keyi] && (++prev) != cur)
		{
			Swap(&a[cur], &a[prev]);
		}
			cur++;
	}
	Swap(&a[keyi], &a[prev]);
	return prev;

}

到此,快速排序的三种方法就都介绍完啦:happy:

快速排序-两大优化

快速排序顾名思义,排序速度听着就很快,时间复杂度最好情况为O(N*logN)

但若遇到这两种特殊情况——数组原本就有序或者数组内数据全部一样,时间复杂度会上升到O(N^2)。

我们都知道,在内存中,栈的空间只有4兆左右,因此面对那两种特殊情况,对于递归的方法来说,**递归调用的次数太多,栈溢出的可能性非常之大,**因此,除了用非递归的方法解决栈溢出的问题之外,前辈们还提出了两种优化递归方法的思路——小区间优化和三数取中

(模拟栈使用的是动态申请空间,用的是堆区的内存,堆区的空间很大,因此不是很大很大的数据量一般都不会溢出,但时间复杂度仍然是那么高,因此还是会很慢),

快速排序优化一—小区间优化

考虑到快排越到后面时,小区间的递归次数越多,我们可以采用与其他排序法相结合的方式来减小递归次数

考虑到特殊情况,区间内已经接近有序且区间范围小,我们选用插入排序来优化小区间,代码如下

小区间优化的效果不是很明显,面对大数据量排序时,效果可能更容易看得出来,否则很难看出来

//交换排序-快速排序
void QuickSort(int* a, int left, int right)
{
	//递归返回条件
	if (left >= right)
	{
		return;
	}
	//小区间优化
	if ((right - left+1) <= 10)
	{
		InsertSort(a + left, (right - left)+1);
	}
	else
	{
		//获取第一次调用或上一次递归相遇时的下标
		int keyi = Partion3(a, left, right);
        //三种方法任选一种
		//int keyi = Partion2(a, left, right);
		//int keyi = Partion1(a, left, right);
        
		//分区间分别hoare
		QuickSort(a, left, keyi - 1);
		QuickSort(a, keyi + 1, right);
	}

}
快速排序优化二—三数取中

一般地,我们都选用最左边的数值当key,因为我们只考虑了数组是无序的情况,也就是key很大程度上正确位置是处在中间部分,如下图

在这里插入图片描述

这种情况是快排的理想情况,类似于二叉树的结构,将快排的时间复杂度控制在了O(N*logN)

但倘若此时数组已有序,或者数组内都是一样的内容,那效果可大不相同

在这里插入图片描述

为了优化这种极端情况,我们采用三数取中法,使得选中的key大机率上的正确位置在中间

方法:在数组的最左,中,右中选取中间数充当key,将最坏情况下的快排优化到接近最好

代码如下:

//keyi取值最优函数
int GetMiddle(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;//返回的是下标
	}

}
//其他两种分区方法一样做法
int Partion1(int* a, int left,int right)
{
	//若数组是(默认)无序的情况
	//int keyi = left;
	//若数组是有序,则此时的快排是最坏情况,时间复杂度达到了O(N^2)
	//为了规避这种情况,使得快排遇到最坏时变成最好,最好时还是最好,对keyi的取值有讲究
	//在左,中,右三者间取最中间大的那个给keyi,达到了效果
	//选出来的值有可能是中间有可能是后面那个,而我们实现的函数是假设keyi是最左边,于是做如下处理
	int mini = GetMiddle(a, left, right);
	Swap(&a[mini], &a[left]);
	int keyi = left;
	while (left < right)
	{
		//右边找小(key在最左边,考虑提前相遇的情况,右边先走)-升序,降序则相反
		while(left < right && a[right] >= a[keyi])
			right--;
		//左边找大,找比keyi的值大的数
		while(left < right && a[left] <= a[keyi])
			left++;
		Swap(&a[left], &a[right]);
	}
	Swap(&a[left], &a[keyi]);
	return left;
}

归并排序-两种实现思路+外排序

归并排序

思想引入

归并排序(MERGE-SORT)是利用归并的思想实现的排序方法,该算法采用经典的分治(divide-and-conquer)策略(分治法将问题(divide)成一些小的问题然后递归求解,而**治(conquer)**的阶段则将分的阶段得到的各答案"修补"在一起,即分而治之)。

老样子,我们先用一张动图演示全过程,再分解过程来细嗦

在这里插入图片描述

这个过程现在看不懂没关系,我们一步一步来分析

假如现在有两个有序序列让你排序,应该自然会想到合并两个有序子序列,也就是新开辟一块空间,并依次比较分别比较两个子序列的大小然后放入新空间中,如下图

在这里插入图片描述

有了这样一个思路,我们就要有以合并两个有序序列从而得到一个新的有序序列为目的的思想(细品这句话),也就是说,给你一个完全无序的数组,我们才用分而治之的方法,将一个无序数组变成两个有序数组再变回一个有序数组,那么就有的同学会问了,**要是数组一直是无序怎么办呢?**但如果是下面这种情况,是否会让你恍然大悟?

=

现在来细品只有一个数据,是否就很清晰呢,是的,一个数据,就是一个有序数组!

于是,我们就有了下面这张图:

在这里插入图片描述

(该图来自)归并排序–这篇博客博主的图画的很好,大家可以做参考,因为小编画工还在修炼,就直接借用啦~

分治思想的介绍就如此啦,还是不理解的同学建议在纸上自己操练一下试试

总结:这样我们的思路就显而易见:给定一个无序数组,我们将其不断划左右区间,知道区间内只有一个数据时,我们开始合并两个有序数组,层层递进,直到全部有序,想要达成目的,我们有两种思路来解决:

思路一—递归思想

思路二—非递归(循环)思想

归并排序-递归

话不多说,上代码

void MergeSort(int* a, int n)
{
	int* temp = (int*)malloc(sizeof(int)*n);
	assert(temp);
    //用子函数来接收待排序列的左右区间,再在子函数中不断划分左右区间范围
	_MergeSort(temp, a, 0, n - 1);
	free(temp);
	temp = NULL;
}
void _MergeSort(int *temp,int* a, int left, int right)
{
    //返回条件:只有一个数据
	if (left >= right)
	{
		return;
	}
    //先递归左序列,再递归右序列,划分左右区间
	int mid = left + (right - left) / 2;
	_MergeSort(temp, a, left, mid);
	_MergeSort(temp, a, mid+1, right);
	//合并两个有序数组
	int begin1 = left;
	int begin2 = mid + 1;
	int end1 = mid;
	int end2 = right;
	int i = left;
	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 + left, temp + left, sizeof(int) * (right - left + 1));

若递归的思路不好理解的话,大家可以拿纸一步一步跟着代码思路走 ,下面我给一张图做理解参考

在这里插入图片描述

时间复杂度:O(N*logN)空间复杂度:O(N)

归并排序-非递归

递归搞明白了,非递归其实也一样,无非就是控制合并两个有序数组时数组的范围从小到大一步一步变成有序

我们用下图来理解
在这里插入图片描述

以上例子举的是数组元素恰好是2的3次方个个数,若数组不为2的幂关系,则除了i一定<n不会越界之外,其他都很容易会越界,因此我们要进行一定的边界控制

下面我们直接上代码

void MergeSortNonR(int* a, int n)
{
	int* temp = (int*)malloc(sizeof(int) * n);
	assert(temp);
	memset(temp, 0, sizeof(int) * n);
	int gap = 1;
	while (gap < n)
	{
		for (int i = 0;i < n;i += 2*gap)
		{
			int begin1 = i;
			int end1 = i + gap - 1;
			int begin2 = i + gap;
			int end2 = i + 2 * gap - 1;
			int index = i;
			if (begin1 == 8)
			{
				int x = 0;
			}
			//边界越界处理
			if (end1 >= n||begin2>=n)
			{
                //前面的已拷贝回原数组,这种情况先不做处理仍放在原数组中,等最后gap饱和时再处理
				break;
			}
			if (end2 >= n)
			{
                //修正到n-1,缩小范围防止越界
				end2 = n - 1;
			}
            //合并有序数组
			while (begin1 <= end1 && begin2 <= end2)
			{
				if (a[begin1] <= a[begin2])
				{
					temp[index++] = a[begin1++];
				}
				else
				{
					temp[index++] = a[begin2++];
				}
			}
			while (begin1 <= end1)
			{
				temp[index++] = a[begin1++];
			}
			while (begin2 <= end2)
			{
				temp[index++] = a[begin2++];
			}
            //每合并一次就拷贝回去一次
			for (int j = i;j <= end2 ;j++)
			{
				a[j] = temp[j];
			}
		}
		gap *= 2;
    }
	free(temp);
}

对于边界处理的情况,若有不明白的参考下图

在这里插入图片描述

同学们可以根据代码思路在纸上模拟一下过程

归并排序-外排序

若有待排数据十亿个

1G=1024MB

1024MB=1024*1024KB

1024KB1024=10241024*1024Byte

约等于十亿个字节

而十亿个数据相当于4个G,内存肯定是放不下的,因此有了外排序,归并排序的外排序就是来应对海量数据排序的

在这里插入图片描述

简易之,就是将数据放在文件中,并将一个大文件分成若干份小文件存放若干等分的有序数据并再次归并成一个有序大文件,如下图所示,这一块同学们只要理解这个思想就ok

在这里插入图片描述

下面提供代码仅供参考

#define _CRT_SECURE_NO_WARNINGS 1
#include <stdio.h>
#include <assert.h>
#include <stdlib.h>
#include <string.h>
//先实现一个快速排序
void Swap(int* p1, int* p2)
{
	int tmp = *p1;
	*p1 = *p2;
	*p2 = tmp;
}
int GetMidIndex(int* a, int begin, int end)
{
	int mid = (begin + end) / 2;
	if (a[begin] < a[mid])
	{
		if (a[mid] < a[end])
			return mid;
		else if (a[begin] > a[end])
			return begin;
		else
			return end;
	}
	else // a[begin] > a[mid]
	{
		if (a[mid] > a[end])
			return mid;
		else if (a[begin] < a[end])
			return begin;
		else
			return end;
	}
}
void QuickSort(int* a, int left, int right)
{
	assert(a);
	if (left >= right)
		return;

	int midIndex = GetMidIndex(a, left, right);
	Swap(&a[midIndex], &a[right]);

	int prev = left - 1;
	int cur = left;
	int keyindex = right;

	while (cur < right)
	{
		if (a[cur] < a[keyindex] && ++prev != cur)
			Swap(&a[prev], &a[cur]);

		++cur;
	}
	Swap(&a[++prev], &a[keyindex]);

	int div = prev;

	QuickSort(a, left, div - 1);
	QuickSort(a, div + 1, right);
}
//文件排序子函数,用来归并两个已经有序的文件
void _MergeFile(const char* file1, const char* file2, const char* mfile)
{
		FILE* fout1 = fopen(file1, "r");
		if (fout1 == NULL)
		{
			printf("FailedOpen File");
			exit(-1);
		}
		FILE* fout2 = fopen(file2, "r");
		if (fout2 == NULL)
		{
			printf("FailedOpen File");
			exit(-1);
		}
		FILE* mfin = fopen(mfile, "w");
		if (mfin == NULL)
		{
			printf("FailedOpen File");
			exit(-1);
		}
		int num1 = 0;
		int num2 = 0;
		int ret1 = fscanf(fout1, "%d", &num1);
		int ret2 = fscanf(fout2, "%d", &num2);
		while (ret1 != EOF && ret2 != EOF)
		{
			if (num1 < num2)
			{
				fprintf(mfin, "%d\n", num1);
				ret1 = fscanf(fout1, "%d", &num1);
			}
			else
			{
				fprintf(mfin, "%d\n", num2);
				ret2 = fscanf(fout2, "%d", &num2);
			}
		}
		while (ret1 != EOF)
		{
			fprintf(mfin, "%d\n", num1);
			ret1 = fscanf(fout1, "%d", &num1);
		}
		while (ret2 != EOF)
		{
			fprintf(mfin, "%d\n", num2);
			ret2 = fscanf(fout2, "%d", &num2);
		}
		fclose(fout1);
		fclose(fout2);
		fclose(mfin);
}
//主函数,用来将文件内的无序数据利用快排变得有序
void MergeFile(const char* file)
{
	FILE* fout = fopen(file, "r");
	if (fout == NULL)
	{
		printf("FileOpen Failed");
		exit(-1);
	}

	int a[10];
	int n = 10;
	memset(a, 0, sizeof(int) * n);
	int i = 0;
	int num = 0;
	char subfile[20];
	int filei = 1;
        //将无序变得有序
	while (fscanf(fout, "%d\n", &num) != EOF)
	{
		if (i < n - 1)
		{
			a[i++] = num;
		}
		else
		{
			a[i] = num;
			QuickSort(a, 0, n - 1);
			sprintf(subfile, "sub\\sub_file%d", filei++);
			FILE*fin=fopen(subfile, "w");
			if (fin == NULL)
			{
				printf("FailedOpen File");
				exit(-1);
			}
			for (int j = 0;j < n;j++)
			{
				fprintf(fin, "%d\n", a[j]);
			}
			fclose(fin);
			i = 0;
			memset(a, 0, sizeof(int) * n);
		}


	}
    //开始归并
	char file1[100]= "sub\\sub_file1";
	char file2[100]= "sub\\sub_file2";
	char mfile[100] ="sub\\sub_file12";
	for (int i = 2;i <= n;i++)
	{
		_MergeFile(file1,file2,mfile);
		strcpy(file1, mfile);
		sprintf(file2, "sub\\sub_file%d", i+1);
		sprintf(mfile, "%s%d",mfile, i + 1);
	}
}
int main()
{
	MergeFile("SortData.txt");


	return 0;
}

这里数据只有一百个,平均分成十个文件,SortData.txt里面存放一百个数据

演示效果:

在这里插入图片描述

计数排序

思想引入

计数排序是一种非比较排序,其核心是将序列中的元素作为键存储在额外的数组空间中,而该元素的个数作为值存储在数组空间中,通过遍历该数组排序。

看思想太无聊了,我们直接上图

在这里插入图片描述

通过以上例子,我们不难发现,计数排序存在以下痛点:

1. 待排序列中最大值与最小值差距不能过大,由于是根据范围开辟新空间,因此很容易很容易造成内存浪费

(通过相对映射能够小小优化,下面会介绍)

2.只能排序整数,而浮点数不能排序

(无法解决)

相对映射

我们从前面知道,在计数时,我们通过直接开辟最大值+1个空间,通过下标与数值的直接对应依次统计次数,这种方法叫——绝对映射

但是如果我们存在以下待排序列
在这里插入图片描述

面对这种情况,如果开辟1666个整型空间去对他进行排序,那真是奢侈她妈给奢侈开门,奢侈到家了

为优化绝对映射的痛点,我们引入了相对映射

什么叫相对映射呢?我们用图来给大家解答:

在这里插入图片描述

下面将给出代码实现:

void CountSort(int* a, int n)
{
    //选出最大值和最小值
	int max = a[0];
	int 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);
	assert(count);
	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 < range;i++)
	{
		while (count[i]--)
		{
			a[j++] = i + min;
		}
	}
}

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

基数排序(队列)

思想引入
基数排序(radix sort)属于“分配式排序”(distribution sort),又称“桶子法”(bucket sort)或bin sort,顾名思义,它是透过键值的部份资讯,将要排序的元素分配至某些“桶”中,藉以达到排序的作用,基数排序法是属于稳定性的排序,其时间复杂度为O (nlog®m),其中r为所采取的基数,而m为堆数,在某些时候,基数排序法的效率高于其它的稳定性排序法。

看思想还是很困难的一件事,下面我们先看一下基数排序的动图演示,再去看概念可能好理解一点

最高位优先(Most Significant Digit first)法,简称MSD法:先按k1排序分组,同一组中记录,关键码k1相等,再对各组按k2排序分成子组,之后,对后面的关键码继续这样的排序分组,直到按最次位关键码kd对各子组排序后。再将各组连接起来,便得到一个有序序列。

最低位优先(Least Significant Digit first)法,简称LSD法:先从kd开始排序,再对kd-1进行排序,依次重复,直到对k1排序后便得到一个有序序列。

第一步:我们将待排序数字做如下处理,按最低位优先原则(也就是个位开始),依次将数字放入“桶”中

在这里插入图片描述

如下图

在这里插入图片描述

第二步:我们将“桶中的数据”按从小到大的顺序依次拿出,若桶中有多个数据,我们按照先进先出(类似于队列的数据结构,后面我会用C语言的队列来实现)原则拿出数据并排放好,如下图

在这里插入图片描述

在这里插入图片描述

第三步:现在个位排完了,便按此前的方法处理十位和百位,下面十位,百位,千位…的方法和个位一样(低位优先),下面我将用一张动图一次性演示完。注:没有高位的全部以0代替,如:8的百位为0

在这里插入图片描述

在这里插入图片描述

排序完成,是不是很简单,大家可以试着在纸上模拟一下,会发现很神奇的就排序成功了,现在再回去看思想,是不是觉得简单多了

下面我将给出C语言的代码提供参考(队列结构将不做展示,会C++的同学直接用库即可)

我们不难发现,整个过程无非在做两件事:分发,回收,分发,回收…因此我们需要两个函数来共同完成基数排序过程:

  1. 分发函数-Distribute
  2. 回收函数Rescyle
//基数排序
void Radix(int* a, int left, int right);
//分发函数
void Distribute(Quene q[],int* a, int left, int right, int k);
//回收函数
void Rescyle(Quene q[], int* a);
 //十个桶
#define RADIX 10
 //这里假设我们要排序的数据中,最大的一个数据最高位是3,同学们灵活运用这一块
#define K 3

前面我们提到过,在**“桶”上的数据遵循先进先出原则,此原则让我们容易联想到数据结构中的队列**,因此我们需要十个队列来充当这十个(这里的队列是我已封装好的,会C++的同学可以直接用库,用能力的同学也可以用C语言模拟实现一个)

void Radix(int* a, int left, int right)
{
	Quene q[RADIX];
    //初始化队列
	for (int i = 0;i < RADIX;i++)
	{
		InitQuene(&q[i]);
	}
	//假设最高位是K,那么一共要分发K次,回收K次
	for (int i = 0;i < K;i++)
	{
		//分发
		Distribute(q,a, left,right,i);
		//回收
		Rescyle(q,a);
	}
}
void Distribute(Quene q[],int* a, int left, int right, int k)
{
    //范围(即数组中需要排序的数据对应的下标范围):[left,right)
	for (int i = left;i < right;i++)
	{
        //通过GetKey函数来获取此趟位数上的值
		int key = Getkey(a[i], k);
		PushQuene(&q[key], a[i]);
	}
}
int Getkey(int val, int k)
{
	int key = 0;
	while (k >= 0)
	{
		key = val % 10;
		val /= 10;
		k--;
	}
	return key;
}
void Rescyle(Quene q[], int* a)
{
	int j = 0;
	for (int i = 0;i < RADIX;i++)
	{
		while (!EmptyQuene(&q[i]))
		{
			a[j++] = FrontQuene(&q[i]);//依次回收队列并放回原数组
			PopQuene(&q[i]);
		}
	}
}

很多同学可能不理解GetKey那一块,我这里稍作说明:

K代表最大数据的最高数位,也就是说,我们需要对所有数据进行分发K次,回收K次的操作,因此,当k为0时,

说明我们现在还在对数据的最低位进行分发与回收,分发时我们需要取到这些数据的最低位,根据最低位来进行分发,因此GetKey函数就是在做这样一件事。

我这里只给出了函数实现,同学们可以自行用数据去测试一下

在这里插入图片描述

其中d表示分发与回收的次数,即k,r表示回收的趟数RADIX,n表示分发的次数即数组大小n

各大排序特性总结

先给出一个名词:稳定性的概念

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

完结撒花感谢耐心阅读如果对您有帮助请点赞加收藏支持一下捏~

  • 35
    点赞
  • 77
    收藏
    觉得还不错? 一键收藏
  • 12
    评论
### 回答1: C语言大学教程第八版pdf是一本比较经典的C语言教材,主要内容包括C语言基础语法、指针、数组、结构体、函数、文件操作等方面的内容。该教材适合初学者和有一定编程基础的人进行学习和练习。 该教材采用了通俗易懂、图文并茂的讲解方式,对于初学者来说很友好。通过逐步深入的学习和实践,可以从实际中获取知识和经验,加深对C语言的理解和掌握。 此外,该教材还提供了一些比较实用的例子和练习题,帮助读者更全面地掌握C语言的应用,并培养编程思维和能力。同时,该教材也对一些可能出现的问题进行了详细的解答和说明,可以帮助读者更快地解决疑惑和困难。 总的来说,C语言大学教程第八版pdf是一本比较全面、详细以及实用的C语言教材,尤其适合初学者进行学习和练习。 ### 回答2: C语言大学教程第八版是一本详细的教材,适合初学者及有一定编程基础的人士学习。该教程主要涉及C语言的基础知识,例如数据类型、运算符、流程控制语句、函数、指针等,同时还介绍了一些高级特性,例如结构体、联合体以及文件操作等内容。这本教程通过实例演示代码实践等方式,非常生动形象,有利于学生理解和掌握相关知识。 除此之外,该教程的作者在书中详细讲解了C语言的设计思路、运行机制和编程规范等,这些内容对学习者来说也非常有益,可以更好地帮助他们掌握C语言的风格和精神。此外,该教材还提供了丰富的习题和实验,可以帮助学生巩固所学知识,提高编程水平。 总之,C语言大学教程第八版是一本非常优秀的教材,对于希望学习编程的人来说是非常值得阅读的。通过学习该教材,学生可以全面掌握C语言的基础知识和高级特性,为之后学习其他编程语言打下坚实的基础。 ### 回答3: c语言大学教程第八版pdf是一本非常经典的c语言教材。该书在学习c语言方面有非常丰富的知识内容,从c语言的基础知识开始,配合大量简单易懂的示例,深入浅出地逐步讲解了c语言的各个方面,包括c语言的数据类型、变量、表达式、流程控制、数组、函数、指针等。同时,该书还涵盖了c语言的重要特性,如结构体、联合体及位域、高级指针、动态内存分配、文件操作等等。 此外,该书还提供了一些实战操作经验,为读者提供了从实战应用到深入了解c语言的完整过程。同时,该书也包含了许多练习题和答案,可以帮助读者更好地巩固知识,提高编程水平。 总的来说,c语言大学教程第八版pdf是一本经典而实用的c语言教材。它不仅可以作为学习c语言的入门教材,也可以为想要进一步掌握c语言的读者提供丰富的知识和实践经验。同时,它也是广大程序员必备的参考书之一,是不可多得的优秀的学习资料。
评论 12
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值