首页 > 编程语言 >【数据结构与算法】:直接插入排序和希尔排序

【数据结构与算法】:直接插入排序和希尔排序

时间:2024-04-06 16:29:39浏览次数:24  
标签:tmp arr 排序 end int 插入排序 gap 数据结构

1. 排序的概念及其意义

1.1 排序的概念

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

1.2 排序的稳定性

  • 假定在待排序的记录序列中,存在多个具有相同的关键字的记录,若经过排序,这些记录的相对次序保持不变,即在原序列中,r[i]=r[j],且r[i]在r[j]之前,而在排序后的序列中,r[i]仍在r[j]之前,则称这种排序算法是稳定的;否则称为不稳定的。
  • 在排序算法中,稳定性是一个十分重要的评判标准。它在我们生活中的某些方面有着作用,比如一个有时间限制的竞赛项目,当两人分数相同时,先提交的选手的排名要比后提交的选手的排名高。这就必须使用具有稳定的排序了。

1.3 常见的排序算法

常见的排序有冒泡排序,插入排序,希尔排序,选择排序,堆排序,快速排序,归并排序,计数排序等,其中我们最常用的是快速排序
在这里插入图片描述

插入排序

基本思想:

把待排序的记录按其关键码值的大小逐个插入到一个已经排好序的有序序列中,直到所有的记录插入完为止,得到一个新的有序序列 。

实际中我们玩扑克牌时,就用了插入排序的思想
在这里插入图片描述

2. 直接插入排序

基本思路:

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

如下图所示:
假设我们要排升序5 2 4 6 1 3
在这里插入图片描述

  1. 把首元素5看成是有序的,我们用tmp保存下一个元素,即tmp = 2,再让tmp与前面已经有序的数据进行比较,此时tmp < 5,所以把5后挪,把tmp插入到5的前面,保持有序;
  2. 接着再让tmp保存第三个元素,即tmp = 4,再让tmp与前面已经有序的数据进行比较,此时tmp < 5但是tmp > 2,所以把5后挪,把tmp插入到5的前面,2的位置不动,保持有序;
  3. ……重复上述上述步骤,以此类推……
  • 先考虑单趟排序的实现,代码如下:
int end ;
int tmp = arr[end + 1];//保存下一个值

while (end >= 0)
{
	if (tmp < arr[end])
	{
		arr[end + 1] = arr[end];//把前面的数按顺序往后挪
		end--;
	}
	else
	{
		break; //比前一个数要大时,说明找到了位置
	}
}

arr[end+1] = tmp;//包含了两种情况:1是在比较的过程中比前一个数要大,就把tmp放进去
                     //2是把前面的数都比完了,此时end==-1了,就把tmp放到最前面

这里的跳出循环包括两种情况:

  1. 在tmp与前面的有序数据比较的过程中,tmp的值比最后一个有序数据大,进入了else语句,break直接跳出循环。例如上图中的第3次排序过程。
  2. tmp比前面的有序数据都要小,一直end–,直到end < 0时,不满足循环条件,跳出循环。例如上图中的第4次排序过程。

而把arr[end+1] = tmp放在循环外面也是由于这两种情况,无论是第1种情况还是第2种情况,在循环结束后都要把待排序的元素放到指定是位置上。所以抽离这条语句放在循环外。

  • 再考虑排序的整体过程,代码实现如下:

i是控制已经有序的数据的最后一个数据,是一个边界。

void InsertSort(int* arr, int sz)
{
	for (int i = 0; i < sz - 1; i++)
	{
		int end = i;
		int tmp = arr[end + 1];//保存下一个值

		while (end >= 0)
		{
			if (tmp < arr[end])
			{
				arr[end + 1] = arr[end];//把前面的数按顺序往后挪
				end--;
			}
			else
			{
				break; //比前一个数要大时,说明找到了位置
			}
		}
		arr[end+1] = tmp;
	}
}

void PrintArray(int* arr, int sz)
{
	for (int i = 0; i < sz; i++)
	{
		printf("%d ", arr[i]);
	}
	printf("\n");
}

int main()
{
	int arr[] = { 6,7,9,2,4,3,5,1,0,8,-1};
	int sz = sizeof(arr) / sizeof(int);

     InsertSort(arr, sz);
     PrintArray(arr, sz);
     
     return 0;
}

排序结果为:
在这里插入图片描述

2.2 时间复杂度的分析

  1. 最好:当待排序的元素为顺序时,时间复杂度0(N)。
  2. 最坏:当待排序的元素为逆序时,时间复杂度0(N*N)。

所以,直接插入排序的时间复杂度是0(N*N)。

2.3 排序的稳定性分析

  • 在上述代码排序过程中,当遇到两个相同数据时,会进入else语句,直接跳出循环,不会发生位置的挪动,即两个相同数据的相对位置依旧保持不变,所以直接插入排序是稳定的。

3. 希尔排序(缩小增量排序)

希尔排序是一种基于插入排序的改进算法。通过引入间隔的概念来改进插入排序的性能。

3.1 基本思想:

通过设置间隔,而对于每个间隔上的元素再进行插入排序,一趟排序后,间隔变小,再继续对此次间隔上的元素进行插入排序,直到间隔为1,此时就是一次完整的直接插入排序

在这里插入图片描述

  • 下面的排序代码与图解都是降序。

希尔排序分为两个步骤:

  1. 预排序
    预排序是让数组接近有序,需要通过分组来排,间隔为gap的是一组。 间隔为3时的一组预排序图解:
    ![外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传](/i/ll/?i=direct/f6fe8bbfab294df69ae114525b900e07.png
    所以要进行多组间隔为gap的预排序,gap由大变小。

  2. 直接插入排序
    当gap = 1时,就是一次直接插入排序。

3.2 对预排序的代码实现:

  • 首先考虑间隔gap = 3时的单趟排序
int gap = 3;
int end ;
int tmp = arr[end + gap];

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

这里跳出循环的情况与上文的直接插入排序的两种情况一模一样。

图解如下:
![![外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传](https://img-home.csdnimg.cn/images/20230724024159.png?origin_url=https%3A%2F%2Fimgblog.csdnimg.cn%2Fdirect%2F7ff366ecbedf469bb5eeb644b0598c93.png&pos_id=img-ahdsKvHT-1712387936626](/i/ll/?i=direct/3afc22d489bf4076830300497b5d8ffa.png

  • 然后把间隔为gap = 3的多组数据同时排:

int gap = 3;
for (int i = 0; i < sz - gap; i++)
{
	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;
}

部分图解如下:
每趟都是间隔为3的元素之间的一次直接插入排序。其中 i 控制end的走法,endgap控制每次排序的走法。
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

  • 接下来考虑如何取gap的值,就是如何让gap的值由大变小:
    gap越大,小的数可以越快到后面,大的数可以越快到前面,同时,gap越大,相对于gap越小而言预排完后越不接近有序。
void ShellSort(int* arr, int sz)
{
	int gap = sz;
	while (gap > 1)
	{
		gap = gap / 2;         // O(log2N)
		//gap = gap / 3 + 1;   // O(log3N)

		//对多组间隔为gap的数据同时排序
		for (int i = 0; i < sz - gap; i++)
		{
			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;
		}
	}
	
}

gap的取值有两种形式,首先它与数组元素个数挂钩,让gap = sz

  1. gap = gap / 2 ;当满足gap > 1时,任何数除2最后结果都是1,对应了gap = 1时就是直接插入排序。
  2. gap = gap / 3 + 1;当满足gap > 1时,为了使最后 gap= 1,所以要+1。

排序结果为:
在这里插入图片描述

3.3 时间复杂度分析:
希尔排序的时间复杂度与数组元素的个数和和gap的取值有关。假设元素个数为N个。
当gap很大时,下面两层循环预排序接近O(N)
当gap很小时,数组已经很接近有序了,差不多也是O(N)

gap = gap / 2时,第一个循环大致执行log2N次(以2为底N的对数)
gap = gap / 3 + 1时,第一个循环大致执行log3N次(以3为底N的对数)

所以希尔排序的时间复杂度是O(N * log2N)或是O(N * log3N)。

3.4 稳定性分析:

不稳定,考虑序列(2,2,1),排序后序列为(1,2,2),我们发现2的相对位置发生了变化(粗体2与非粗体2),所以是不稳定的排序算法。

4. 两种排序的性能对比

clock() 函数是 <time.h> 头文件中的一个函数,用来返回程序启动到函数调用时之间的CPU时钟周期数。这个值通常用来帮助衡量程序或程序的某个部分的性能

我们可以用这个函数进一步对比两种排序占用的CPU时间

代码实现为:

void TestOP()
{
	    srand(time(0));
		const int N = 100000;
		int* a1 = (int*)malloc(sizeof(int) * N);
		int* a2 = (int*)malloc(sizeof(int) * N);
		
	for (int i = 0; i < N; ++i)
	{
		a1[i] = rand();
		a2[i] = a1[i];
	}
	
	int begin1 = clock();
	InsertSort(a1, N);
	int end1 = clock();
	
	int begin2 = clock();
	ShellSort(a2, N);
	int end2 = clock();
	
	printf("InsertSort:%d\n", end1 - begin1);
	printf("ShellSort:%d\n", end2 - begin2);
	
	free(a1);
	free(a2);
}

这里让随机生成十万个随机数,分别用希尔排序和直接插入排序来进行排序,测试两种算法的执行时间:
在这里插入图片描述

通过上面的执行时间的对比,可以发现希尔排序的效率远远快于插入排序。

标签:tmp,arr,排序,end,int,插入排序,gap,数据结构
From: https://blog.csdn.net/2301_77900444/article/details/137423269

相关文章

  • Java数据结构队列
    队列(Queue) 概念队列的使用注意:Queue是个接口,在实例化时必须实例化LinkedList的对象,因为LinkedList实现了Queue接口。importjava.util.LinkedList;importjava.util.Queue;publicclassTest{publicstaticvoidmain(String[]args){Queue<Integ......
  • 数据结构之顺序表的相关知识点及应用
     个人主页(找往期文章包括但不限于本期文章中不懂的知识点):我要学编程(ಥ_ಥ)-CSDN博客目录顺序表的概念及结构顺序表的分类顺序表的实现 在顺序表中增加数据 在顺序表中删除数据 在顺序表中查找数据 顺序表源码 顺序表的概念及结构在了解顺序表之前,得先知道......
  • AOV网络与拓扑排序
    活动网络可以用来描述生产计划、施工过程、生产流程、程序流程等工程中各子工程的安排问题。活动网络可分为两种:AOV网络和AOE网络。1.AOV网络与有向无环图 一般一个工程可以分成若干个子工程,这些子工程称为活动。完成了这些活动,整个工程就完成了。实际上,可以用有向图来表......
  • (数据结构——比特)算法的时间复杂度和空间复杂度
    目录1.算法效率如何衡量一个算法的好坏算法的复杂度复杂度在校招中的考察2.时间复杂度时间复杂度的概念 请计算一下Func1中++count语句总共执行了多少次?Func1执行的基本操作次数: 大O的渐进表示法推导大O阶方法:使用大O的渐进表示法以后,Func1的时间复杂度为: 另......
  • 结构体+排序——OpenJudge 1.10 07:合影效果
    描述小云和朋友们去爬香山,为美丽的景色所陶醉,想合影留念。如果他们站成一排,男生全部在左(从拍照者的角度),并按照从矮到高的顺序从左到右排,女生全部在右,并按照从高到矮的顺序从左到右排,请问他们合影的效果是什么样的(所有人的身高都不同)?输入第一行是人数n(2<=n<=40,且至少有1......
  • 【数据结构】时间和空间复杂度
    摘要:时间和空间复杂度是评估算法效率的两个重要指标,它们分别关注算法在执行过程中所消耗的时间和空间资源。本文将介绍时间和空间复杂度的概念、计算方法以及它们在算法设计与分析中的重要性,以及如何在实际应用中平衡时间和空间复杂度,以达到最佳的算法性能。1.引言在计......
  • 数据结构篇:跳跃表与B+树的对比与优劣分析
       本文旨在探讨跳跃表的特性及其在实际应用场景中的作用,同时对其与B+树进行比较,以帮助更好地理解和运用这两种数据结构。跳跃表什么是跳跃表(skiplist)        跳跃表是一种基于跳跃链表的有序数据结构,它是一种多层链表,每一层都是一个有序的链表。表的每一层......
  • C++数据结构——顺序表
    C++数据结构——顺序表以下代码可以作为一个顺序表的模板,从顺序表的初始化创建到增删改查,都有详细的过程,供学习参考。#include<iostream>#include<stdio.h>usingnamespacestd;#defineelemTypeintstructSequentialList{elemType*elements;intsiz......
  • 数据结构 第六章(图)【下】
    写在前面:本系列笔记主要以《数据结构(C语言版)》为参考(本章部分图片来源于王道),结合下方视频教程对数据结构的相关知识点进行梳理。所有代码块使用的都是C语言,如有错误欢迎指出。视频链接:第01周a--前言_哔哩哔哩_bilibili四、图的应用1、最小生成树(1)在一个连通网的所有生成树......
  • 数据结构 第五章(树和二叉树)【下】
    写在前面:本系列笔记主要以《数据结构(C语言版)》为参考(本章部分图片以及知识点整理来源于王道),结合下方视频教程对数据结构的相关知识点进行梳理。所有代码块使用的都是C语言,如有错误欢迎指出。视频链接:第01周a--前言_哔哩哔哩_bilibili哈夫曼树部分的代码参考了一位小伙伴分享的......