几种经典排序算法

几种经典排序算法

  • 插入排序
    • 折半插入排序法
  • 选择排序
  • 冒泡排序
  • 希尔排序
  • 堆排序
  • 二路归并排序
  • 快速排序

在介绍排序之前,先来说说,研究不同的排序主要是要研究他们的哪些不同:

  1. 时间性能。即排序过程中元素之间的比较次数与元素移动次数。我们此次讨论各种排序方法的时间复杂度时,主要按照最差情况下所需要的比较次数来进行。
  2. 空间性能。除了存放参加排序的元素之外,排序过程中所需要的其他辅助空间。
  3. 排序稳定性。对于值相同的两个元素,排序前后的先后次序不变,则称这种排序方法稳定,否则称其不稳定。

然后还有一个概念我们后面会经常提到:。排序需要若干遍循环完成,每循环一遍,就叫一趟。

然后,我们今天的例子中,都是以从小到大排序为例的。

另外,因为排序免不了需要交换位置,因此我们先写好一个Swap函数用于交换:

void Swap(int* pa,int* pb)
{
int tmp=*pa;
*pa=*pb;
*pb=tmp;
}

插入排序

动画演示
在这里插入图片描述
核心思想:
在第i趟排序中,将序列的第i+1个元素,按照值大小,插入到前i个已排好序的序列中。

静态演示:
将【49,38,97,76,65,13,27,50】进行插入排序:
其中红色部分表示已经排好序的部分,花圈元素表示下一趟要排序的元素。
在这里插入图片描述可以看出来,n个元素,需要n-1趟插入排序才能完全排好。

算法
我们先写一趟排序的算法,然后再加循环。加循环无外就是判断一下起始和结束条件。

void insertSort(int k[],int n)//传入要进行排序的数组以及元素数
{
    int i,j;//i表示第i趟,也表示当前要调整的元素的下标
    //j用来遍历已排好序的序列,用于和第i个元素比较
    int tmp=k[i];
    for(i=1;i<n;i++)
    {
        for(j=i-1;j>=0&&k[j]>tmp;j--)
            k[j+1]=k[j];
        //以上两步用于把比当前元素大的往后调
        //一旦检测到已排序好序列中有比当前元素小的,则把当前元素插到该元素后面
        k[j+1]=tmp;    
    }
   
}

我们用【49,38,97,76,65,13,27,50】这个数组检测一下效果:
在这里插入图片描述
没毛病,跟我们刚刚手动计算的一样。

排序的时间效率
时间效率主要和排序过程中元素之间的比较次数直接有关。

  1. 原始序列按值递增:那么每一趟排序,第一次比较,也就是和当前需要调整元素的前一个元素比较时,就跳出循环了,因为比当前需要调整元素小。因此每个元素进行调整时,都只需要比较1次,那么总共有n-1个元素需要调整,就一共比较n-1次。
  2. 原始序列按值递减:第i趟排序需要和前i个元素都比较一次,因此,整个n-1趟排序需要比较(1+n-1)*(n-1)/ 2=n(n-1)/ 2次

如果以最坏的情况,也就是原始序列递减情况,则插入排序算法的时间复杂度为O(n²)。

由于我们只把比当前元素的元素向后调,相等的元素不调,因此调整之后,值相等的两个元素的相对位置在排序前后并未改变,则插入排序算法是稳定的。

由于没有开辟新的空间,因此插入排序算法的空复杂度为O(1)。

折半插入排序法

这个算法的整体思想和插入法一样,只是在找插入位置的时候,用了折半查找的方法,基于前i-1个元素是有序的。
找到位置的标志,也就是某一趟排序中循环停止的标志为high<low,插入位置即为low。

void BinInsertSort(int k[],int n)
{
    int i,j;
    int high,low,mid;
    for(i=1;i<n;i++)
    {
        int tmp=k[i];
        high=i-1;
        low=0;
        while(high>=low)
        {
            mid=(high+low)/2;
            if(tmp>=k[mid])//将相等情况也纳入,保证了稳定性 
                low=mid+1;
            else
                high=mid-1;
        }
        for(j=i;j>low;j--)
            k[j]=k[j-1];
        k[low]=tmp;
    }
}

选择排序

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

核心思想
第i趟排序,从序列的后n-i+1个元素中选择一个值最小的元素,将其置于该n-i+1个元素的最前面(与第i个元素交换)

静态演示
红框框起来的表示这一趟中判断出的最小元素,红色部分表示已经排好序的部分。
在这里插入图片描述共有8个元素,但只排7次。和插入排序一样。

void selectsort(int k[],int n)
{
    int i,j,d;//d用于记录最小元素的下标
    int temp;
    for(i=0;i<n-1;i++)
    {
        d=i;
        for(j=i+1;j<n;j++)
        {
            if(k[j]<k[d])
                d=j;
        }
        Swap(&k[d],&k[i]);
    }
}

方法2

void selectsort2(int k[],int n)
{
    int i,j,tmp;
    for(i=0;i<n;i++)
       for(j=i+1;j<n;j++)
           if(k[j]<k[i])
               Swap(&k[j],&k[i]);
}

冒泡排序

动画演示
在这里插入图片描述
核心思想:第i趟中,对序列的**前n-i+1个元素,从第一个元素开始,相邻的两个元素比较大小,若前者大于后者,则交换。

静态演示
在这里插入图片描述

我们发现,选择和插入排序都进行了n-1次,但是为什么冒泡排序小于n-1呢?前两者循环结束的条件都是i,j和n比较,但是冒泡排序,我们需要定义一个变量flag,用于记录,在某趟排序中,是否进行了元素交换。如果进行了,哪怕只有一次,那下一趟循环继续;如果一次也没有,就说明此时序列已经有序,不需要再进行下一趟了。

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

效率分析
冒泡排序算法的排序趟数和原始序列的排序有关,因此排序趟数为【1,n-1】。
排序趟数最少为1次,即原始序列本身就有序。
最多为n-1次,原始序列为逆序。

因此,冒泡排序的时间复杂度,在最少次数下为O(n),即只进行一轮;在最多次数下为O(n²)

冒泡排序过程中,如果两个数相等,则不进行交换,因此是稳定的。

希尔排序

把整个序列分成若干子序列,每个子序列内部先排序。
在这里插入图片描述在每一趟排序结果均是按照上一趟分好的组(颜色一眼的)进行组内排序的结果(组内排序可以用各种排序方法,插入、冒泡都行)。
每组元素的间隔数满足:
在这里插入图片描述
即每一次的gap都是上一次的一半。gap等于1时,整个数组为一个组进行排序,也就是最后一次排序。

每进行一趟排序,数组都会更加接近有序。

我们这里先展示一种组内排序为冒泡排序的希尔排序方法:

void shellsort1(int k[],int n)
{
    int i,j,flag,gap=n;
    //flag用于检测每一趟冒泡排序是否有进行交换,如果没有进行交换,就说明
    //数组已经有序了,不用继续进行排序了
    int temp;
    while(gap>1)
    {
        gap=gap/2;
        do
        {
            flag=0;
            for(i=0;i<n-gap;i++)
            {
                j=i+gap;
                if(k[j]<k[i])
                    Swap(&k[i],&k[j]);
            }     
        } while (flag==1);   
    }
}

再来展示一种内部排序为插入排序的希尔排序:

void shellsort2(int k[],int n)
{
    int i,j,temp,gap=n;
    while(gap>1)
    {
        gap=gap/2;
        for(i=gap;i<n;i++)
        {
            temp=k[i];
            for(j=i;j>=gap&&k[j-gap]>temp;j-=gap)
                k[j]=k[j-gap];
            k[j]=temp;
        }
    }
}

效率分析
举个反例就发现,希尔排序是不稳定的了:
【2a,3,4,1,2b,5,6,8】我们让gap=3,则【2a,1,6】是一组,【3,2b,8】是一组,【4,5】一组。
那么第一趟排序的结果就是:
【1,2b,4,2a,3,5,6,8】我们发现,两个2的相对位置发生了变化,因此不稳定。

希尔排序的时间复杂度比较难搞,我们认为在O(nlogn)和O(n²)之间。

堆排序

之前写过一篇专门讲堆排序的文章:link,但是在本次排序专题里,再把它写一次。

动画演示
在这里插入图片描述
核心思想:先对原始数组进行向下调整,从第一个非叶子结点开始进行。待所有结点都调整完毕之后,形成了一个大堆,然后,依次将堆顶和当前堆的最后个元素交换位置,换完之后进行向下调整

void AjustDown(int k[],int n,int parent)
{
    int child=2*parent+1;
    while(child<n)
    {
        if(child+1<n&&k[child+1]>k[child])
            ++child;
        if(k[child]>k[parent])
        {
            Swap(&k[child],&k[parent]);
            parent=child;
            child=child*2+1;
        }
        else
            break;
    }
}
void heapsort(int k[],int n)
{
    for(int i=(n-1-1)/2;i>=0;i--)
    {
        AjustDown(k,n,i);
    }
    //建好堆了,下面开始排序
    int end=n-1;
    while(end>0)
    {
        Swap(&k[end],&k[0]);
        AjustDown(k,end,0);
        end--;
    }
}

效率分析
时间主要消耗在向下调整了,那么每个元素,都要经过向下调整,调整的次数最多为高度次,即logn,有n个元素,则时间复杂度为O(nlogn)。

堆排序显然也不稳定,调整算法压根没有考虑过数组之间的关系,只考虑了堆的结构。

二路归并排序

动画演示
在这里插入图片描述核心思想
第i趟排序将序列的n/2^ (i-1) 个长度为 2^ (i-1) 的有序子序列依次两两合并为n/2^ i 个长度为2^i的有序子序列。

静态演示
在这里插入图片描述
如果n不等于2^ k,则:
在这里插入图片描述
最后一趟在不等长的两个序列中进行就可以。

大家看到这个基本思路,就知道它大概率是要用到递归的思路的,这种一环套一环,每一环的都基于上一环的结果。

从动态图中我们也能看出来,归并排序是需要一个临时数组的,辅助我们为子序列排序。

void _mergesort(int k[],int begin,int end,int* tmp)
{
    if(begin==end)
        return;
    
    int mid=(begin+end)/2;
    //[begin,mid][mid+1,end]
    _mergesort(k,begin,mid,tmp);
    _mergesort(k,mid+1,end,tmp);
    //左右都排好以后,我们来合并:
    int begin1=begin,end1=mid;
    int begin2=mid+1,end2=end;
    int i=begin1;
    while(begin1<=end1&&begin2<=end)
    {
        if(k[begin1]<k[begin2])
            tmp[i++]=k[begin1++];
        else
            tmp[i++]=k[begin2++];
    }
    while(begin1<=end1)
        tmp[i++]=k[begin1++];
    while(begin2<=end2)
        tmp[i++]=k[begin2++];

    memcpy(k+begin,tmp+begin,sizeof(int)*(end-begin+1));
}
void mergesort(int k[],int n)
{
    int* tmp=(int*)malloc(sizeof(int)*n);
    _mergesort(k,0,n-1,tmp);

    free(tmp);
    tmp=NULL;
}

效率分析
当子序列个数为1(n不是2的次幂时为2个),即n/2^ i 为1的时候,排序完成,即排了logn趟。子序列内部排序的时间复杂度主要来源于归并部分的while循环,时间复杂度为O(n),因此,归并排序的时间复杂度为O(nlogn)

由于开辟了tmp数组,因此空间复杂度时O(n)。

是稳定的。

快速排序

动画演示
在这里插入图片描述为什么右边的先走呢?因为右边的先走可以保证L和R相遇的位置处元素,一定小于key。

void quicksort(int k[],int left,int right)
{
    if(left>=right)
        return;
    int begin=left,end=right;
    int keyi=left;

    while(left<right)
    {
        //right先走,找比key小的
        while(left<right&&k[keyi]<=k[right])//为了防止整个序列里面keyi的右边全部比它大,加一个left<right,以防right一股脑冲出数组头
            right--;
        //left再走,找比key大的
        while(left>right&&k[keyi]>=k[left])
            left++;
        
        Swap(&k[left],&k[right]);
    }
    Swap(&k[left],&k[keyi]);
    keyi=left;
    //[begin,keyi-1][keyi+1,end]
    quicksort(k,begin,keyi-1);
    quicksort(k,keyi+1,end);
}

效率分析
在这里插入图片描述

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/714351.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

【最新鸿蒙应用开发】——鸿蒙中的“Slot插槽”?@BuilderParam

构建函数-BuilderParam 传递 UI 1. 引言 BuilderParam 该装饰器用于声明任意UI描述的一个元素&#xff0c;类似slot占位符。 简而言之&#xff1a;就是自定义组件允许外部传递 UI Entry Component struct Index {build() {Column({ space: 15 }) {SonCom() {// 直接传递进来…

IPv6 ND 协议功能概述

ND 协议功能概述 ND&#xff08;Neighbor Discovery&#xff0c;邻居发现&#xff09;协议是 IPv6 的一个关键协议&#xff0c;它综合了 IPv4 中的 ARP&#xff0c;ICMP 路由发现和 ICMP 重定向等协议&#xff0c;并对它们做了改进。 作为 IPv6 的基础性协议&#xff0c;ND 协…

ppt添加圆角矩形,并调整圆角弧度方法

一、背景 我们看的论文&#xff0c;许多好看的图都是用PPT做的&#xff0c;下面介绍用ppt添加圆角矩形&#xff0c;并调整圆角弧度方法。 二、ppt添加圆角矩形&#xff0c;并调整圆角弧度 添加矩形&#xff1a; 在顶部工具栏中&#xff0c;点击“插入”选项卡。 在“插图”…

冒泡排序知识点

排序的基本概念 排序是计算机内经常进行的一种操作&#xff0c;其目的是将一组“无序”的记录调整为“有序”的记录序列。 常用的排序例子 8 7 1 5 4 2 6 3 9 把上面的这个无序序列变为有序&#xff08;升序或者降序&#xff09;序列的过程。 1 2 3 4 5 6 7 8 9&#xff0…

Spring运维之boo项目表现层测试加载测试的专用配置属性以及在JUnit中启动web服务器发送虚拟请求

测试表现层的代码如何测试 加载测试的专用属性 首先写一个测试 假定我们进行测试的时候要加一些属性 要去修改一些属性 我们可以写一个只在本测试有效的测试 写在配置里 测试 打印输出 我们把配置文件里面的配置注释掉后 我们同样可以启动 package com.example.demo;impo…

代码随想录——组合总和Ⅱ(Leetcode 40)需要回顾

题目链接 回溯 本题的难点在于&#xff1a;集合&#xff08;数组candidates&#xff09;有重复元素&#xff0c;但还不能有重复的组合。 思想&#xff1a;元素在同一个组合内是可以重复的&#xff0c;怎么重复都没事&#xff0c;但两个组合不能相同。所以要去重的是同一树…

购物车店铺列表查询流程

购物车店铺列表查询流程 购物车结算流程图

嵌入式门槛高不高,工资怎么样?

一般来说&#xff0c;嵌入式岗位的准入门槛其实并不是特别高。通常情况下&#xff0c;只要能够熟练掌握 C 语言编程以及单片机相关知识&#xff0c;就能够去制作一些较为简单的电子产品&#xff0c;由此可见其门槛相对而言是比较低的&#xff0c;相应的薪水可能也不会特别高。 …

I2C 总线通信技术基础

1.0 I2C 技术基础 使用总线的目的&#xff1a;采用串行总线技术可以使系统的硬件设计大大简化、系统的体积减小、可靠性提高&#xff0c;同时&#xff0c;系统的更改和扩充变的极为容易。 通信中常用的串行拓展总线 I2C&#xff08;Inter-Integrated Circuit &#xff09;总线…

C语言程序设计-6 循环控制

C语言程序设计-6 循环控制 循环结构是程序中一种很重要的结构。其特点是&#xff0c;在给定条件成立时&#xff0c;反复执行某程序 段&#xff0c;直到条件不成立为止。给定的条件称为循环条件&#xff0c;反复执行的程序段称为循环体。&#xff23;语 言提供了多种循环语句&a…

计算机网络知识点全面总结回顾

物理层 OSI模型&#xff1a;数据链路层&#xff08;流量控制&#xff09;&#xff0c;从传输层开始端到端&#xff1b;每一层的元素都称为实体&#xff0c;同一层的是对等实体&#xff1b;三个重要概念&#xff1a;服务&#xff08;下层为上层提供调用&#xff09;&#xff0c…

【Linux】进程间通信1——管道概念,匿名管道

1.进程间通信介绍 进程是计算机系统分配资源的最小单位&#xff08;严格说来是线程&#xff09;。每个进程都有自己的一部分独立的系统资源&#xff0c;彼此是隔离的。为了能使不同的进程互相访问资源并进行协调工作&#xff0c;才有了进程间通信。 进程间通信&#xff0c;顾名…

1055 集体照(测试点3, 4, 5)

solution 从后排开始输出&#xff0c;可以先把所有的学生进行排序&#xff08;身高降序&#xff0c;名字升序&#xff09;&#xff0c;再按照每排的人数找到中间位置依次左右各一个进行排列测试点3&#xff0c; 4&#xff0c; 5&#xff1a;k是小于10的正整数&#xff0c;则每…

记录一次root过程

设备: Redmi k40s 第一步&#xff0c; 解锁BL&#xff08;会重置手机系统&#xff01;&#xff01;&#xff01;所有数据都会没有&#xff01;&#xff01;&#xff01;&#xff09; 由于更新了澎湃OS系统, 解锁BL很麻烦, 需要社区5级以上还要答题。 但是&#xff0c;这个手机…

人工智能历史与现状

1 人工智能历史与现状 1.1 人工智能的概念和起源 1.1.1 人工智能的概念 人工智能 (Artificial Intelligence ,AI)是一门研究如何使计算机 能够模拟人类智能行为的科学和技术,目标在于开发能够感知、理解、 学习、推理、决策和解决问题的智能机器。人工智能的概念主要包含 以…

理解DDD设计

DDD的理解 领域驱动设计&#xff08;Domain-Driven Design&#xff0c;DDD&#xff09;是一种软件开发方法论&#xff0c;强调将业务领域作为软件设计的核心&#xff0c;以便更好地满足业务需求。DDD认为&#xff0c;软件开发的核心是理解业务&#xff0c;而不是实现技术。在D…

容器镜像外网同步方案

目录 一、目的 二、安装nexus 1、购买香港云主机​编辑 2、安装nexus 3、启动nexus 服务 4、放行安全组 三、配置nexus 1、登录nexus管理页面 2、修改nexus密码 3、创建 Blob 存储空间(可选) 4、创建 镜像代理仓库 5、Realms配置 四、拉取镜像 1、配置docker 2、…

【Python】Python实现解压rar文件

Python实现解压rar文件 零、需求 最近在开发一个填分数的应用&#xff0c;需要用到selenium&#xff0c;那么自然需要用到浏览器&#xff0c;浏览器内置到应用中&#xff0c;但是上传到GitCode的时候被限制了&#xff0c;单个文件大小只能是10M以内。所以只能压缩&#xff0c…

免费个人站 独立站 wordpress 自建网站

制作免费网站 | 免费网站构建器 | WordPress.com https://bioinformatics7.wordpress.com WordPress.com

运算符分为哪几类?哪些运算符常用作判断?简述运算符的优先级

运算符包含6大类&#xff1a;算术运算符、赋值运算符、比较运算符、逻辑运算符、位运算符、三元&#xff08;目&#xff09;运算符。 逻辑运算符常用作布尔判断 typeof 运算符: typeof 运算符用于确定变量或表达式的数据类型&#xff0c;并返回一个表示类型的字符串。 typeof …