【数据结构初阶】九、五种比较排序的讲解和实现(直接插入 \ 希尔 \ 直接选择 \ 堆 \ 冒泡 — C语言)

=========================================================================

相关代码gitee自取

C语言学习日记: 加油努力 (gitee.com)

 =========================================================================

接上期

【数据结构初阶】八、非线性表里的二叉树(二叉树的实现 — C语言链式结构)-CSDN博客

 =========================================================================

                     

排序

排序的概念

所谓排序,就是使一串记录
按照其中的某个或某些关键字的大小递增或递减排列起来的操作
                 

稳定性

假定在待排序的记录序列中存在多个具有相同关键字的记录
经过排序这些记录的相对次序保持不变
即在原序列中 r [ i ] = r [ j ] , r [ i ] r [ j ] 之前
在排序后的序列中r [ i ] 仍在 r [ j ] 之前
称这种排序算法是稳定的否则称为不稳定的

                 

内部排序(内排序)

数据元素全部放在内存中的排序内存中进行排序

属于内排序的排序算法
  • 插入排序
    直接插入排序 、希尔排序
                   
  • 选择排序
    直接选择排序 、排序
                    
  • 交换排序
    冒泡排序 、快速排序
                   
  • 并归排序(即属于内排序又属于外排序

                 

外部排序(外排序)

数据元素太多不能同时放在内存中

根据排序过程的要求不能在内外存之间移动数据的排序

即在磁盘中(“顺序写顺序读”)进行排序

属于外排序的排序算法
  • 并归排序(即属于内排序又属于外排序

                

~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

                 

常见排序算法的实现

(详细解释在图片的注释中,代码分文件放下一标题处)

                   

一、插入排序

                        

插入排序 — 直接插入排序

直接插入排序是一种简单的插入排序法

                 

该算法基本思想:

待排序的记录按其关键码值的大小逐个插入到一个已经排好序的有序序列中

直到所有的记录插入完为止得到一个新的有序序列
(想象我们玩扑克牌时给扑克牌大小排序就用了插入排序的思想

                      

实现思路:

插入第 i(i>=1) 个元素时
前面的 array[0]array[1],…,array[i-1] 已经排好序
此时 array[i] 的排序码 array[i-1]array[i-2],… 的排序码顺序进行比较
找到插入位置后 array[i] 插入原来位置上的元素顺序后移覆盖

                     

直接插入排序的特性总结:
  • 元素越接近有序直接插入排序算法时间效率越高最高可达O(N)
                            
  • 该算法时间复杂度O(N^2) 最坏的情况下(逆序)
                                
  • 该算法空间复杂度O(1)
                          
  • 该算法稳定性稳定
                      

———————————————————————————————

                       

InsertSort函数 — 直接插入排序实现函数

  • 使用for循环进行循环插入排序
    设置前元素” 和 “后元素
                           
  • for循环中
    使用while循环后元素寻找合适位置
    找到后再将后元素插入
    之后再次进行for循环让下个后元素插入合适位置
图示:

该函数执行逻辑图:

对应函数测试:

            

            

———————————————————————————————

            

插入排序 — 希尔排序(缩小增量排序)

希尔排序又称缩小增量法

             

该算法基本思想:

先选定一个整数gap值待排序文件中所有记录分成gap个组

所有距离为gap的记录分在同一组内,并对每一组内的记录进行直接插入排序

然后换个整数gap值再取gap组重复上述分组和排序的工作

gap=1 所有记录在同一组内时已经排好序了

               

希尔排序的特性总结:
  • 希尔排序直接插入排序的优化
                  
  • gap>1 时都是在进行预排序,目的是让数组更接近于有序
    gap==1 数组已经接近有序的了,这样再进行直接插入排序就会很快
    这样整体而言,可以达到优化的效果
                                  
  • 希尔排序的时间复杂度不好计算,因为gap的取值方法很多导致很难去计算
    因此在一些书中给出的希尔排序的时间复杂度也不固定,
    该函数大概的时间复杂度为:O(N^1.3)
                           
  • 该算法稳定性不稳定
                             

———————————————————————————————

                       

ShellSort函数 — 希尔排序实现函数

  • 核心实现操作还是直接插入排序
    但是在对所有值进行直接插入排序先进行多次预排序操作
    预排序也是用直接插入排序完成)
    让数组达到尽量有序的状态
                  
  • 定义gap值变量,使用while循环控制多次预排序
    (当gap==1时,就相当于直接插入排序
                    
  • while循环中内嵌for循环完成当前gap组”的预排序
                     
  • 再内嵌for循环完成当前gap组其中一组的预排序
    (通过直接插入排序的方式实现)
                    
    • 最后再内嵌while循环配合完成直接插入排序
图示:

该函数执行逻辑图:

对应函数测试:

                     

                     

                    

二、选择排序

基本思想:

每一趟排序从待排序的数组元素中分别选择出最小最大的一个元素
再分别存放在数组的起始位置尾部位置
直到全部待排序的数组元素排序完成

                

                

选择排序 — 直接选择排序

                    

该算法基本思想:

元素集合 array[i] — array[n-1] 选择获取当前最小值元素当前最大值元素
当前最小值元素交换后放到当前数组起始位置
当前最大值元素交换后放到当前数组尾部位置

在剩余的 array[i+1] — array[n-2] 集合中,重复上述操作直到集合剩余1个元素
               

直接选择排序的特性总结:
  • 直接选择排序比较好理解,但是效率不高实际很少使用
                      
  • 该算法时间复杂度O(N^2)
                         
  • 该算法空间复杂度O(1)
                       
  • 该算法稳定性不稳定
                         

———————————————————————————————

                       

SelectSort函数 — 直接选择排序实现函数

  • 定义 数组开始(初始)位置下标begin 数组尾部(结束)位置下标end 变量
    使用while循环控制多趟直接选择排序
                        
  • (在while循环中完成一趟直接选择排序
    定义 存放当前最小值下标mini 存放当前最大值下标maxi 变量
    内嵌for循环遍历一次当前数组找到当前最大值当前最小值
                       
  • while循环中for循环外:)
    将找到的当前数组最小值放入数组起始左边位置
    当前数组最大值放入数组尾部右边位置,
    完成两值的排序后调整 begin end 变量缩小数组范围准备下趟直接选择排序
图示:

该函数执行逻辑图:

【数据结构初阶】九、五种比较排序的讲解和实现(直接插入 \ 希尔 \ 直接选择 \ 堆 \ 冒泡 -- C语言)

对应函数测试:

            

            

———————————————————————————————

            

选择排序 — 堆排序

是一种完全二叉树
可以使用堆中的向下调整操作对普通数组进行排序升序降序

               

堆排序的特性总结:
  • 堆排序使用进行选择排序效率高了很多
                      
  • 该算法时间复杂度O(N*logN)
                         
  • 该算法空间复杂度O(1)
                       
  • 该算法稳定性不稳定
                         

———————————————————————————————

                       

堆排序的实现往期博客中已有详细描述:

【数据结构初阶】七、非线性表里的二叉树(堆的实现 — C语言顺序结构)-CSDN博客

(包含堆排序实现函数图示对应函数执行逻辑图函数测试原代码

                     

                     

                    

三、交换排序

基本思想:

所谓交换,就是根据序列中两个记录键值的比较结果交换这两个记录在序列中的位置

             

交换排序的特点:

键值较大的记录序列的尾部移动键值较小的记录序列的前部移动

                      

                      

交换排序 — 冒泡排序

                     

该算法基本思想:

数组中的元素1元素2进行比较,如果元素1大于元素2,则交换两者位置

比较此时的元素2元素3,如果元素2大于元素3,则交换两者位置

再比较此时的元素3元素4……

一趟冒泡排序完成后,就能完成当前数组中最大值的排序排在尾部),
再进行下一趟冒泡排序前缩小数组排序范围排除已经排序好的最大值),

这样多趟冒泡排序完成后就能完成数组的排序

                     

冒泡排序的特性总结:
  • 冒泡排序是一种非常容易理解的排序
                      
  • 该算法时间复杂度O(N^2)
                  
  • 该算法空间复杂度O(1)
                          
  • 该算法稳定性稳定
                           

———————————————————————————————

                       

BubbleSort函数 — 冒泡排序实现函数

  • 定义嵌套for循环完成冒泡排序
    外层for循环控制当前数组范围数组范围慢慢减小
    减到变成1时排序完成至少有两个数才能比较
                            
  • 内层for循环循环一趟完成当前数组范围内的最值的排序
                        
  • 当前冒泡排序过程中
    设置一个变量exchange记录当前一趟冒泡排序过程中是否有发生元素的交换
    趟冒泡排序后,如果没有任何元素发生交换
    说明目前已经排好序了,那就直接终止之后的冒泡排序
图示:

该函数执行逻辑图:

【数据结构初阶】九、五种比较排序的讲解和实现(直接插入 \ 希尔 \ 直接选择 \ 堆 \ 冒泡 -- C语言)

对应函数测试:

                 

~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

                      

对应代码:

Sort.h — 排序头文件

#pragma once

//包含之后所需头文件:
#include <stdio.h>

//打印数组函数:
void PrintArray(int* a, int n);


//直接插入排序函数:
//第一个参数:接收要排序的数组首元素地址(a)
//第二个参数:接收该数组的长度(n)
void InsertSort(int* a, int n);


//希尔排序函数:
//第一个参数:接收要排序的数组首元素地址(a)
//第二个参数:接收该数组的长度(n)
void ShellSort(int* a, int n);
//1、预排序:分组排,间隔为gap(间距间隔)分为一组
//				预排序意义:
// 让大的数更快的到数组后面,小的数更快的到数组前面,
// gap值越大“跳”得越快,但是越不接近有序
// gap值越小“跳”得越慢,但是越接近有序
// 当gap==1时,就相当于直接插入排序
//2、直接插入排序


//冒泡排序函数:
//第一个参数:接收要排序的数组首元素地址(a)
//第二个参数:接收该数组的长度(n)
void BubbleSort(int* a, int n);	


//直接选择排序函数:
//第一个参数:接收要排序的数组首元素地址(a)
//第二个参数:接收该数组的长度(n)
void SelectSort(int* a, int n);

            

            

———————————————————————————————

            

Sort.c — 排序函数实现文件

#define _CRT_SECURE_NO_WARNINGS 1

//包含排序头文件:
#include "Sort.h"

//打印数组函数:
void PrintArray(int* a, int n)
{
	for (int i = 0; i < n; i++)
	{
		printf("%d ", a[i]);
	}

	printf("");
}



//直接插入排序函数(升序):
//第一个参数:接收要排序的数组首元素地址(a)
//第二个参数:接收该数组的长度(n)
void InsertSort(int* a, int n)
{
	//使用for循环进行循环插入排序:
		/*
		要取“前后元素”,“后元素”要依次和“前面所有元素”进行比较,
		直到“后元素”找到合适位置并插入,而“后元素”下标最大为[n-1],
		所以“前元素”下标最大为[n-2]
		*/
	for (int i = 0; i < n - 1; i++)
		//循环到 i = n-2  --  “前元素”下标最大为[n-2]:
	{
		//遍历下标的起始位置从 0 开始,到 n-2 结束:
		int end = i; //“前元素”

		//保存end下标元素的后一个元素:
		int tmp = a[end + 1]; //“后元素”

		//使用while循环为“后元素”寻找合适位置:
		//(“后元素”循环和其前面所有元素进行比较,直到找到合适位置)
		while (end >= 0)
			//“前面所有元素”:下标 >=0 时就还有 “前元素”,
			//那“后元素”就需要继续进行比较:
		{
			if (tmp < a[end])
				//“后元素” 小于 “前元素”:
			{
				//那就将“前元素(较大)”往后插入(覆盖):
				a[end + 1] = a[end];
				//a[end + 1],被覆盖的元素还保留在tmp中,
				//所以不用担心覆盖后找不到“后元素”
			}
			else
				//"后元素" 大于等于 "前元素":
			{
				//说明“后元素”的“前面较大元素”都已经往后覆盖完成了,
				//找到了"后元素"的恰当位置,
				//break退出循环将“后元素”tmp插入该位置
				break;
			}

			end--;
			//当前end--,下次循环判断“前前元素”需不需要再往前覆盖,
			//end减到-1退出while循环的话,说明“后元素”是数组中最小的元素,
			//导致其前面所有元素都需要往后覆盖挪出位置
		}

		//注意,合适的位置是[end + 1],
		//因为"后元素" 大于等于 "前元素"的话,
		//那“后元素”就理应排在“前元素”后面,即[end + 1],
		a[end + 1] = tmp;
		//如果是end减到-1退出的while循环,
		//那[end + 1]就等于0,即数组首元素位置,
		//让“后元素”排在数组首元素位置
	}
}
//时间复杂度:
//O(N^2) -- 逆序(最坏情况)
//O(N) -- 顺序有序
//冒泡排序和插入排序论时间复杂度它们是同一档次,
//但在细节上,如局部有序,插入排序会更优



//希尔排序函数:
//第一个参数:接收要排序的数组首元素地址(a)
//第二个参数:接收该数组的长度(n)·
void ShellSort(int* a, int n)
{
	//定义分组间隔和分组个数:
	int gap = n;
	
	//只要当前预排序gap值还没到1,
	//while循环就继续循环进行预排序:
	while (gap > 1)
	{
		//变化gap值进行多次预排序,
		//让数组越来越接近有序:
		gap = gap / 2;
		//当 gap > 1 时就是预排序
		//当 gap == 1 时就是插入排序

		//这个for循环控制 “当前gap组” 的排序:
		//(一组“gap分”组排完再排另一组gap分组)
		for (int j = 0; j < gap; j++)
		/*
		* 假设gap取3,
		* 就在数组中以3(gap)为间隔取数为一组(gap分组)
		* 总共取3(gap)组
		*/
		{
			//这个for循环控制gap组中其中一组的排序:
			for (int i = 0; i < n - gap; i += gap)
			/*
			*					循环条件:n - gap  
			*控制“前元素end”范围,保证“后元素tmp”通过end取值时不出界
			*					迭代条件:i += gap
			*i每次迭代时就+gap,实现以gap为间隔取数为一组(gap分组)
			*/
			{
				//保存当前分组的“前元素”(记录):
				int end = i;

				//保存当前分组的“后元素”(记录):
				int tmp = a[end + gap];

				//使用while循环为“后元素”寻找合适位置:
				//(“后元素”循环和其前面所有元素进行比较,直到找到合适位置)
				//(和直接插入类似,只是值与值间隔不同)
				while (end >= 0)
					//“前面所有元素”:下标 >=0 时就还有 “前元素”,
					//那“后元素”就需要继续进行比较:
				{
					if (tmp < a[end])
						//“后元素” 小于 “前元素”:
					{
						//(gap分组中:)
						//那就将“前元素(较大)”往后插入(覆盖):
						a[end + gap] = a[end];
						//a[end + gap],被覆盖的元素还保留在tmp中,
						//所以不用担心覆盖后找不到“后元素”

						end -= gap;
						//(gap分组中:)
						//当前end-=gap,下次循环判断“前前元素”需不需要再往前覆盖,
						//end减到<0时,退出while循环的话,说明“后元素”是数组中最小的元素,
						//导致其前面所有元素都需要往后覆盖挪出位置
					}
					else
						//"后元素" 大于等于 "前元素":
					{
						//说明“后元素”的“前面较大元素”都已经往后覆盖完成了,
						//找到了"后元素"的恰当位置,
						//break退出循环将“后元素”tmp插入该位置
						break;
					}
				}

				//注意,合适的位置是[end + gap],
				//因为"后元素" 大于等于 "前元素"的话,
				//那“后元素”就理应排在“前元素”后面,即[end + gap],
				a[end + gap] = tmp;
				//如果是 end减到<0 退出的while循环,
				//那[end + gap]就找到当前gap组首元素位置,
				//让“后元素”排在当前gap组首元素位置
			}
		}
	}
}



//两值交换函数:
void Swap(int* x, int* y)
{
	//创建中间值进行值的交换:
	int tmp = *x;
	*x = *y;
	*y = tmp;
}



//冒泡排序函数:
//第一个参数:接收要排序的数组首元素地址(a)
//第二个参数:接收该数组的长度(n)
void BubbleSort(int* a, int n)
{
	/*
	* 定义嵌套for循环完成冒泡排序:
	* 内层for循环一趟完成当前数组范围内的最值的排序
	* 外层for循环则控制当前数组范围
	* 数组范围慢慢减小,减到变成1时排序完成
	* (至少有两个数才能比较)
	*/

	//这个for循环控制内嵌for循环的数组范围:
	for (int j = 0; j < n; j++)
	{
		int exchange = 0; 
		//如果没发生交换该变量就为0

		//内嵌for循环进行循环比较交换:
		for (int i = 1; i < n-j; i++)
		/*
		*n-j :通过判断条件控制数组范围
		*因为一趟下来就完成一个当前最值的排序,
		*下次再排序的数组范围就要排除这个已经排好序的最值,
		*通过外部for循环的j来控制数组范围:
		*/
		{
			if (a[i - 1] > a[i])
				//如果“前元素” 大于 “当前元素”:
				//( i 从1开始)
			{
				//进行交换:
				Swap(&a[i - 1], &a[i]);

				exchange = 1;
				//如果发生了变化该变量就为1
			}
		}
		/*
		* 内嵌for循环整个执行完就是一趟,
		* 当遇到最大值后,因为最大所以会一直被交换
		* 直到被交换至数组尾部,
		* 这就完成了当前数组最大值的排序
		*/

		if (exchange == 0)
			//一趟冒泡排序后,如果没有发生交换:
		{
			//说明目前已经排好序了,
			//就没必要再进行之后的冒泡排序了:
			break;
		} 
	}
}



//直接选择排序函数:
//第一个参数:接收要排序的数组首元素地址(a)
//第二个参数:接收该数组的长度(n)
void SelectSort(int* a, int n)
{
	/*
	* 思路:
	* 排序一趟选出当前最小值下标和当前最大值下标,
	* 通过对应下标将最小值放“左边”,最大值放“右边”
	*/

	//数组开始位置下标 -- 0
	int begin = 0;
	
	//数组尾部(结束)位置下标 -- n-1
	int end = n - 1;

	//使用while循环控制多趟直接选择排序:
	while (begin < end)
	/*
	* 只要 begin 还小于 end,左右下标就还没重合
	* begin 和 end 之间就还有元素要被排序
	*/
	{
		//对当前数组范围进行一趟直接选择排序:

		//存放当前最小值下标:
		int mini = begin; //默认为下标0

		//存放当前最大值下标:
		int maxi = begin; //默认为下标0

		//使用for循环遍历一次当前数组,
		//找到当前最大值和当前最小值:
		for (int i = begin+1; i <= end; i++)
		/*
		* mini 和 maxi 默认下标都是0
		* 所以循环时下标从[begin+1]开始,等于[n-1]结束
		*/
		{
			//选出当前最大值下标:
			if (a[i] > a[maxi])
				//如果“i下标元素”大于“maxi下标元素”:
			{
				//将较大值下标i赋给maxi下标:
				maxi = i;
			}

			//选出当前最小值下标:
			if (a[i] < a[mini])
				//如果“i下标元素”小于“mini下标元素”:
			{
				//将较小值下标i赋给mini下标:
				mini = i;
			}

		} //(for循环中)

		//将找到的当前数组最小值放入数组起始(左边)位置:
		Swap(&a[begin], &a[mini]);
		/*
		* 注:如果begin下标元素为最大值,
		* 那么maxi下标也会指向该元素,即begin == maxi,
		* 当begin元素和mini交换后(只是值交换),
		* begin 和 maxi下标还是重叠在原位置,指向交换后的mini元素
		* 此时如果再执行 Swap(&a[end], &a[maxi]);
		* 就反而会将最小值移到数组尾部去,
		* 所以要调整 begin==maxi 重叠的情况:
		*/
		if (maxi == begin)
			//排除maxi和begin下标重叠的情况:
		{
			//maxi==begin等于当前最大值下标,上面交换后,
			//当前最大值就在mini下标处,
			//那就把maxi最大值下标指向mini下标处即可:
			maxi = mini;
		}

		//将找到的当前数组最大值放入数组尾部(右边)位置:
		Swap(&a[end], &a[maxi]);
	
		/*
		* 执行到这就选出了两个最值并放到了合适位置(一趟直接选择排序),
		* 调整 begin 和 end 下标(调整数组范围),数组范围往中间缩小,
		* 再开始新一趟直接选择排序:
		*/
		++begin;
		--end;

	} //(while循环中)
}
//时间复杂度:O(N^2)

            

            

———————————————————————————————

            

Test.c — 排序测试文件

#define _CRT_SECURE_NO_WARNINGS 1

//包含排序头文件:
#include "Sort.h"

//插入排序测试:
void ISTest()
{
	//创建要进行插入排序的数组:
	int a[] = { 3,4,2,1,5 };

	//调用插入排序函数进行排序:
	InsertSort(a, 5);

	//循环打印排序后的数组:
	printf("使用直接插入排序后的数组:> ");
	for (int i = 0; i < 5; i++)
	{
		printf("%d ", a[i]);
	}
}


//希尔排序测试:
void SSTest()
{
	//创建要进行插入排序的数组:
	int a[] = { 9,1,2,5,7,4,8,6,3,5 };

	//调用希尔排序函数进行排序:
	ShellSort(a, sizeof(a) / sizeof(int));

	//使用自定义打印函数打印排序后数组:
	printf("使用希尔排序后的数组:> ");
	PrintArray(a, sizeof(a) / sizeof(int));
}


//冒泡排序测试:
void BSTest()
{
	//创建要进行插入排序的数组:
	int a[] = { 9,1,2,5,7,4,8,6,3,5 };

	//调用冒泡排序函数进行排序:
	BubbleSort(a, sizeof(a) / sizeof(int));

	//使用自定义打印函数打印排序后数组:
	printf("使用冒泡排序后的数组:> ");
	PrintArray(a, sizeof(a) / sizeof(int));
}


//直接选择排序测试:
void SlSTest()
{
	//创建要进行插入排序的数组:
	int a[] = { 9,1,2,5,7,4,8,6,3,5 };

	//调用直接选择排序函数进行排序:
	SelectSort(a, sizeof(a) / sizeof(int));

	//使用自定义打印函数打印排序后数组:
	printf("使用直接选择排序后的数组:> ");
	PrintArray(a, sizeof(a) / sizeof(int));
}


int main()
{
	//ISTest();
	//SSTest();
	//BSTest();
	SlSTest();

	return 0;
}

文章出处登录后可见!

已经登录?立即刷新

共计人评分,平均

到目前为止还没有投票!成为第一位评论此文章。

(0)
扎眼的阳光的头像扎眼的阳光普通用户
上一篇 2023年12月19日
下一篇 2023年12月19日

相关推荐