前端宝典十八:高频算法排序之冒泡、插入、选择、归并和快速

news2024/9/20 22:45:27

前言

十大经典排序算法的 时间复杂度与空间复杂度 比较。
在这里插入图片描述
名词解释:

  • n:数据规模;
  • k:桶的个数;
  • In-place: 占用常数内存,不占用额外内存;
  • Out-place: 占用额外内存。

本文主要探讨高频算法排序中的几个常见的冒泡、插入、选择、归并和快速

  • 冒泡排序和选择排序是最常见的两种排序,语法简单,容易实现,冒泡排序、插入排序和选择排序虽然在时间复杂度上相对较高,但对于小规模数据或者部分已排序的数据,它们可能更加高效,因为它们的算法简单,不需要额外的内存空间。

  • 归并排序和快速排序在平均情况下具有较好的时间复杂度,归并排序的时间复杂度始终为O(nlogn),快速排序在平均情况下也是O(nlogn),并且它们可以对大规模数据进行高效排序。

有的下盆友会提出疑问,为什么js语法中有了sort函数给数组排序了,为什么还要研究和使用冒泡、插入、选择、归并和快速排序方法?

原因也很简单
sort方法的性能在不同的 JavaScript 引擎中可能有所不同,并且其实现方式通常是比较通用的,不一定针对特定的数据类型或场景进行优化。

例如,对于基本类型数据(如数字)的排序,自定义的快速排序等算法在某些情况下可能比sort方法更快,尤其是对于大规模数据的排序。

对于包含复杂对象的数组,可能需要提供自定义的比较函数,而不同的排序算法在处理自定义比较逻辑时的性能表现也可能不同。

一、排序算法

1、 如何分析一个排序算法

复杂度分析是整个算法学习的精髓。

  • 时间复杂度: 一个算法执行所耗费的时间。
  • 空间复杂度: 运行完一个程序所需内存的大小。

学习排序算法,我们除了学习它的算法原理、代码实现之外,更重要的是要学会如何评价、分析一个排序算法。
分析一个排序算法,要从 执行效率、内存消耗、稳定性 三方面入手。

2、执行效率

  1. 最好情况、最坏情况、平均情况时间复杂度
    我们在分析排序算法的时间复杂度时,要分别给出最好情况、最坏情况、平均情况下的时间复杂度。
    除此之外,你还要说出最好、最坏时间复杂度对应的要排序的原始数据是什么样的。
  2. 时间复杂度的系数、常数 、低阶
    我们知道,时间复杂度反应的是数据规模 n 很大的时候的一个增长趋势,所以它表示的时候会忽略系数、常数、低阶。
    但是实际的软件开发中,我们排序的可能是 10 个、100 个、1000 个这样规模很小的数据,所以,在对同一阶时间复杂度的排序算法性能对比的时候,我们就要把系数、常数、低阶也考虑进来。
  3. 比较次数和交换(或移动)次数
    基于比较的排序算法的执行过程,会涉及两种操作,一种是元素比较大小,另一种是元素交换或移动。
    所以,如果我们在分析排序算法的执行效率的时候,应该把比较次数和交换(或移动)次数也考虑进去。

3、内存消耗

也就是看空间复杂度。
还需要知道如下术语:

  • 内排序:所有排序操作都在内存中完成;
  • 外排序:由于数据太大,因此把数据放在磁盘中,而排序通过磁盘和内存的数据传输才能进行;
  • 原地排序:原地排序算法,就是特指空间复杂度是 O(1) 的排序算法。

4、稳定性

  • 稳定:如果待排序的序列中存在值相等的元素,经过排序之后,相等元素之间原有的先后顺序不变。
    比如: a 原本在 b 前面,而 a = b,排序之后,a 仍然在 b 的前面;
  • 不稳定:如果待排序的序列中存在值相等的元素,经过排序之后,相等元素之间原有的先后顺序改变。
    比如:a 原本在 b 的前面,而 a = b,排序之后, a 在 b 的后面;

二、冒泡排序(Bubble Sort)

1、思想

  • 冒泡排序只会操作相邻的两个数据。
  • 每次冒泡操作都会对相邻的两个元素进行比较,看是否满足大小关系要求。如果不满足就让它俩互换。
  • 一次冒泡会让至少一个元素移动到它应该在的位置,重复 n 次,就完成了 n 个数据的排序工作。

2、特点

  • 优点:排序算法的基础,简单实用易于理解。
  • 缺点:比较次数多,效率较低。

3、实现

// 冒泡排序(已优化)
const bubbleSort2 = arr => {
        console.time('改进后冒泡排序耗时');
        const length = arr.length;
        if (length <= 1) return;
        // i < length - 1 是因为外层只需要 length-1 次就排好了,第 length 次比较是多余的。
        for (let i = 0; i < length - 1; i++) {
                let hasChange = false; // 提前退出冒泡循环的标志位
                // j < length - i - 1 是因为内层的 length-i-1 到 length-1 的位置已经排好了,不需要再比较一次。
                for (let j = 0; j < length - i - 1; j++) {
                        if (arr[j] > arr[j + 1]) {
                                const temp = arr[j];
                                arr[j] = arr[j + 1];
                                arr[j + 1] = temp;
                                hasChange = true; // 表示有数据交换
                        }
                }

                if (!hasChange) break; // 如果 false 说明所有元素已经到位,没有数据交换,提前退出
        }
        console.log('改进后 arr :', arr);
        console.timeEnd('改进后冒泡排序耗时');
};

上面代码中通过参数hasChange进行了优化:当某次冒泡操作已经没有数据交换时,说明已经达到完全有序,不用再继续执行后续的冒泡操作。

4、冒泡排序的时间复杂度是多少 ?

最佳情况:T(n) = O(n),当数据已经是正序时。
最差情况:T(n) = O(n(2)),当数据是反序时。
平均情况:T(n) = O(n(2))。

三、插入排序

插入排序又为分为 直接插入排序 和优化后的 拆半插入排序 与 希尔排序(下文讲),我们通常说的插入排序是指直接插入排序。

1、思想

一般人打扑克牌,整理牌的时候,都是按牌的大小(从小到大或者从大到小)整理牌的,那每摸一张新牌,就扫描自己的牌,把新牌插入到相应的位置。
插入排序的工作原理:通过构建有序序列,对于未排序数据,在已排序序列中从后向前扫描,找到相应位置并插入。

2、步骤

  • 从第一个元素开始,该元素可以认为已经被排序;
  • 取出下一个元素,在已经排序的元素序列中从后向前扫描;
  • 如果该元素(已排序)大于新元素,将该元素移到下一位置;
  • 重复步骤 3,直到找到已排序的元素小于或者等于新元素的位置;
  • 将新元素插入到该位置后;
  • 重复步骤 2 ~ 5。

3、实现

// 插入排序
const insertionSort = array => {
        const len = array.length;
        if (len <= 1) return

        let preIndex, current;
        for (let i = 1; i < len; i++) {
                preIndex = i - 1; //待比较元素的下标
                current = array[i]; //当前元素
                while (preIndex >= 0 && array[preIndex] > current) {
                        //前置条件之一: 待比较元素比当前元素大
                        array[preIndex + 1] = array[preIndex]; //将待比较元素后移一位
                        preIndex--; //游标前移一位
                }
                if (preIndex + 1 != i) {
                        //避免同一个元素赋值给自身
                        array[preIndex + 1] = current; //将当前元素插入预留空位
                        console.log('array :', array);
                }
        }
        return array;
};

4、插入排序的时间复杂度是多少 ?

最佳情况:T(n) = O(n),当数据已经是正序时。
最差情况:T(n) = O(n(2)),当数据是反序时。
平均情况:T(n) = O(n(2))。

四、选择排序

冒泡排序和选择排序是最常见的两种排序,语法简单,容易实现,冒泡排序、插入排序和选择排序虽然在时间复杂度上相对较高,但对于小规模数据或者部分已排序的数据,它们可能更加高效,因为它们的算法简单,不需要额外的内存空间。

1、思路

选择排序算法的实现思路有点类似插入排序,也分已排序区间和未排序区间。但是选择排序每次会从未排序区间中找到最小的元素,将其放到已排序区间的末尾。

2、步骤

  1. 首先在未排序序列中找到最小(大)元素,存放到排序序列的起始位置。
  2. 再从剩余未排序元素中继续寻找最小(大)元素,然后放到已排序序列的末尾。
  3. 重复第二步,直到所有元素均排序完毕。

3、实现

const selectionSort = array => {
        const len = array.length;
        let minIndex, temp;
        for (let i = 0; i < len - 1; i++) {
                minIndex = i;
                for (let j = i + 1; j < len; j++) {
                        if (array[j] < array[minIndex]) {
                                // 寻找最小的数
                                minIndex = j; // 将最小数的索引保存
                        }
                }
                temp = array[i];
                array[i] = array[minIndex];
                array[minIndex] = temp;
                console.log('array: ', array);
        }
        return array;
};

4、选择排序的时间复杂度是多少 ?

无论是正序还是逆序,选择排序都会遍历 n(2) / 2 次来排序,所以,最佳、最差和平均的复杂度是一样的。
最佳情况:T(n) = O(n(2))。
最差情况:T(n) = O(n(2))。
平均情况:T(n) = O(n(2))。

五、归并排序

归并排序采用的是分治思想
分治,顾名思义,就是分而治之,将一个大问题分解成小的子问题来解决。小的子问题解决了,大问题也就解决了。

1、归并排序有以下优点:

1. 时间复杂度稳定

归并排序的时间复杂度始终为(O(n log n)),其中(n)是待排序数组的长度。无论输入数据的初始状态如何,归并排序都能在相对较短的时间内完成排序任务。这使得它在处理大规模数据时非常高效,不会因为数据的特殊分布而导致性能急剧下降。

相比一些时间复杂度较差的排序算法(如冒泡排序、选择排序、插入排序的平均和最差时间复杂度为O(n^2)),归并排序的效率更高。

2. 稳定排序

归并排序是一种稳定的排序算法。这意味着当对包含相等元素的数组进行排序时,相等元素的相对顺序在排序前后保持不变。在某些应用场景中,数据的相对顺序具有重要意义,归并排序的稳定性就显得尤为重要。

3.适用于外部排序

对于非常大的数据集,可能无法一次性加载到内存中进行排序。归并排序可以很容易地应用于外部排序,即将数据分成较小的块进行排序,然后逐步合并这些已排序的块。这种方法可以有效地处理超出内存容量的大数据集。

4. 易于并行化

归并排序的分治策略使其易于并行化。可以将不同的子问题分配给不同的处理器或线程进行处理,然后再将结果合并。在现代多核处理器和分布式计算环境中,这一特性可以大大提高排序的效率。

5. 适用于多种数据类型

归并排序可以应用于各种数据类型,包括基本数据类型(如整数、浮点数等)和复杂的数据结构(如对象、结构体等)。只需要定义合适的比较函数,就可以对不同类型的数据进行排序。
在这里插入图片描述

2、代码实现

const mergeSort = arr => {
        //采用自上而下的递归方法
        const len = arr.length;
        if (len < 2) {
                return arr;
        }
        // length >> 1 和 Math.floor(len / 2) 等价
        let middle = Math.floor(len / 2),
                left = arr.slice(0, middle),
                right = arr.slice(middle); // 拆分为两个子数组
        return merge(mergeSort(left), mergeSort(right));
};

const merge = (left, right) => {
        const result = [];

        while (left.length && right.length) {
                // 注意: 判断的条件是小于或等于,如果只是小于,那么排序将不稳定.
                if (left[0] <= right[0]) {
                        result.push(left.shift());
                } else {
                        result.push(right.shift());
                }
        }

        while (left.length) result.push(left.shift());

        while (right.length) result.push(right.shift());

        return result;
};

六、快速排序 (Quick Sort)

快速排序的特点就是快,而且效率高!它是处理大数据最快的排序算法之一。

1、思想

  • 先找到一个基准点(一般指数组的中部),然后数组被该基准点分为两部分,依次与该基准点数据比较,如果比它小,放左边;反之,放右边。
  • 左右分别用一个空数组去存储比较后的数据。
  • 最后递归执行上述操作,直到数组长度 <= 1;

2、特点:

快速,常用。

3、缺点:

(1)需要另外声明两个数组,浪费了内存空间资源。
(2)快速排序是一种不稳定的排序算法。这意味着在排序过程中,相等元素的相对顺序可能会发生改变。在某些对稳定性有要求的场景中,这可能是一个缺点。

4、实现

const quickSort1 = arr => {
        if (arr.length <= 1) {
                return arr;
        }
        //取基准点
        const midIndex = Math.floor(arr.length / 2);
        //取基准点的值,splice(index,1) 则返回的是含有被删除的元素的数组。
        const valArr = arr.splice(midIndex, 1);
        const midIndexVal = valArr[0];
        const left = []; //存放比基准点小的数组
        const right = []; //存放比基准点大的数组
        //遍历数组,进行判断分配
        for (let i = 0; i < arr.length; i++) {
                if (arr[i] < midIndexVal) {
                        left.push(arr[i]); //比基准点小的放在左边数组
                } else {
                        right.push(arr[i]); //比基准点大的放在右边数组
                }
        }
        //递归执行以上操作,对左右两个数组进行操作,直到数组长度为 <= 1
        return quickSort1(left).concat(midIndexVal, quickSort1(right));
};
const array2 = [5, 4, 3, 2, 1];
console.log('quickSort1 ', quickSort1(array2));
// quickSort1: [1, 2, 3, 4, 5]

七、 归并排序和快速排序的区别

在这里插入图片描述

  • 归并排序的处理过程是由下而上的,先处理子问题,然后再合并。
  • 而快排正好相反,它的处理过程是由上而下的,先分区,然后再处理子问题。
  • 归并排序虽然是稳定的、时间复杂度为 O(nlogn) 的排序算法,但是它是非原地排序算法。
  • 归并之所以是非原地排序算法,主要原因是合并函数无法在原地执行。
  • 快速排序通过设计巧妙的原地分区函数,可以实现原地排序,解决了归并排序占用太多内存的问题。

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

docker安装ES(Elasticsearch)的IK分词器

大家可以先看我的Docker中部署Kibana&#xff1a; “Docker中部署Kibana&#xff1a;步骤与指南“-CSDN博客 其实这几篇博文都是有关系的&#xff0c;希望大家学有所成。 问题&#xff1a;命令中有一个unzip没有安装&#xff0c;需要先安装一下&#xff0c;安装命令&#xf…

计算机毕业设计 高校学术交流平台 Java+SpringBoot+Vue 前后端分离 文档报告 代码讲解 安装调试

&#x1f34a;作者&#xff1a;计算机编程-吉哥 &#x1f34a;简介&#xff1a;专业从事JavaWeb程序开发&#xff0c;微信小程序开发&#xff0c;定制化项目、 源码、代码讲解、文档撰写、ppt制作。做自己喜欢的事&#xff0c;生活就是快乐的。 &#x1f34a;心愿&#xff1a;点…

前端宝典十七:算法之复杂度、链表、队列、栈的代码实现

引文 从本文开始主要探讨前端的算法&#xff0c;这一篇主要涉及&#xff1a; 时间复杂度&空间复杂度&#xff1b;链表&#xff1b;队列&#xff1b;栈&#xff1b; 希望通过学习能掌握好 具体代码时间复杂度&空间复杂度的算法&#xff1b;链表、队列、栈的JavaScri…

Nginx - 反向代理、缓存详解

概述 本篇博客对配置Nginx的第二篇&#xff0c;主要介绍Nginx设置反向代理、缓存、和负载均衡三个知识点&#xff0c;在之前的生产实践中遇到的问题进行归纳和总结&#xff0c;分享出来&#xff0c;以方便同学们有更好的成长。 Nginx 核心参数配置 在写Nginx反向代理时&…

公众号(H5)及小程序的发布流程

⚠️ Web平台本API之前调用了腾讯地图的gcj02坐标免费转换接口&#xff0c;该接口从2024年7月18日起被腾讯逐步下线&#xff0c;导致老版本中本API无法使用。请立即升级到 uni-app 4.24版。 ⚠️ 这里说的 uniapp 升级到 4.24 版的意思&#xff0c;就是使用 4.24 版本 HBuilde…

【JVM】亿级流量调优(一)

亿级流量调优 oop模型 前面的klass模型&#xff0c;它是Java类的元信息在JVM中的存在形式。这个oop模型是Java对象在JVM中的存在形式 内存分配策略: 1.空闲列表2.指针碰撞(jvm采用的) 2.1 top指针:执行的是可用内存的起始位置 2.2 采用CAS的方式3.TLAB 线程私有堆4.PLAB 老年…

使用PhaGCN2/vConTACT2进行病毒分类注释

Introduction 在微生物群落的研究中&#xff0c;分类和注释数量庞大的未培养古细菌和细菌病毒一直是一个难题&#xff0c;主要原因是缺乏统一的分类框架。 目前&#xff0c;用于病毒分类的多种基于基因组的方法已经被提出&#xff0c;主要集中在细菌、古细菌和真核生物病毒的…

隧道代理ip使用

简介 隧道代理&#xff08;Tunnel Proxy&#xff09;是一种特殊的代理服务&#xff0c;它的工作方式是在客户端与远程服务器之间建立一条“隧道”。这种技术常被用来绕过网络限制或提高网络安全性。 主要功能 IP地址变换&#xff1a;隧道代理能够改变客户端的IP地址&#xf…

《javaEE篇》--线程池

线程池是什么 线程的诞生是因为进程创建和销毁的成本太大&#xff0c;但是也是相对而言&#xff0c;如果频繁的创建和销毁线程那么这个成本就不能忽略了。 一般有两种方法来进一步提高效率&#xff0c;一种是协程(这里不多做讨论),另一种就是线程池 假如说有一个学校食堂窗口…

【Node】【2】创建node应用

创建node应用 node应用&#xff0c;不仅可以实现web应用&#xff0c;也能实现http服务器。 如果是php写后端&#xff0c;还需要有http服务器&#xff0c;比如apache 或者 nginx。 但是现在主流都是java写后端&#xff0c;也可以像 Node.js 一样用于实现 Web 应用和 HTTP 服务…

chapter08-面向对象编程(包、访问修饰符、封装、继承)day08

目录 277-包的使用细节 278-访问修饰符规则 279-访问修饰符细节 281-封装介绍 282-封装步骤 283-封装快速入门 284-封装与构造器 285-封装课堂练习 286-为什么需要继承 277-包的使用细节 1、需要哪个包就导入哪个包&#xff0c;不建议全部导入* 2、包的使用细节&…

宵暗的妖怪

宵暗的妖怪 错误代码和正确代码对比 #include <iostream> #include <string.h> using namespace std; const int N1e510; long long a[N],f[N],g[N]; // f 以i为结尾&#xff0c;饱食度最大值 // g 0-i中&#xff0c;饱食度最大值int main() {int n;cin>>…

Linux 内核源码分析---IPv6 数据包

IPv6是英文“Internet Protocol Version 6”&#xff08;互联网协议第6版&#xff09;的缩写&#xff0c;是互联网工程任务组&#xff08;IETF&#xff09;设计的用于替代IPv4的下一代IP协议&#xff0c;其地址数量号称可以为全世界的每一粒沙子编上一个地址。 由于IPv4最大的…

看图学sql之sql中的子查询

&#xfeff;&#xfeff; &#xfeff;where子句子查询 语法&#xff1a; SELECT column_name [, column_name ] FROM table1 [, table2 ] WHERE column_name OPERATOR(SELECT column_name [, column_name ]FROM table1 [, table2 ][WHERE]) 子查询需要放在括号( )内。O…

课后作业-第四次

1.将web-ssrfme.zip解压缩在Ubuntu下 Docker-compose up -d 更新后的镜像重新启动容器 2.拉取成功ssrfme镜像 3.使用端口访问文件&#xff0c; 可以看到有一个过滤条件&#xff0c;它限制了file&#xff0c;dict协议&#xff0c;127.0.0.1和localhost 也不能用&#xff0c;…

中仕公考怎么样?2025年国考现在准备来得及吗?

2025年国考时间线 2024年10月发布公告——2024年11月报名确认——2024年11月打印准考证——2024年12月笔试——2024年3月现场资格审查——2024年3-4月面试——2025年5月补录环节 考试科目&#xff1a; 笔试的主要科目是:行测申论 ①行测:常识、言语理解、判断推理、数量关系…

2025智能电动窗帘推荐!电动窗帘应该如何选择?看一篇窗帘省钱攻略!一体化电动窗帘

史新华 智能家居已经走进我们的生活。 智能马桶、智能穿衣镜、语音茶吧机、小爱音箱、天猫精灵这些家居好物让生活更加精致&#xff0c;智能窗帘走进千家万户还会远吗&#xff1f;&#xff01; 我最开始关注电动窗帘&#xff0c;就是从住酒店开始的&#xff0c;经常出差的同学…

uni-app里引入阿里彩色矢量图标(Symbol),却发现图标显示为黑白

当使用uniapp并尝试引入阿里iconfont的彩色图标时&#xff0c;发现图标显示为黑白。原因是Fontclass模式不支持彩色图标。 解决方法是下载Symbol模式的SVG文件&#xff0c;使用iconfont-tools进行转换&#xff0c;然后在项目中全局引入转换后的CSS文件&#xff0c;最终在组件中…

探索联邦学习:保护隐私的机器学习新范式

探索联邦学习&#xff1a;保护隐私的机器学习新范式 前言联邦学习简介联邦学习的原理联邦学习的应用场景联邦学习示例代码结语 前言 在数字化浪潮的推动下&#xff0c;我们步入了一个前所未有的数据驱动时代。海量的数据不仅为科学研究、商业决策和日常生活带来了革命性的变化&…

Datawhale X 李宏毅苹果书 AI夏令营_深度学习基础学习心得

本次学习了深度学习中的局部最小值 1、书上说有时候模型一开始就训练不起来&#xff0c;不管怎么更新参数损失都不下降。我之前遇到过这种情况&#xff0c;大概是做一个数据很不平衡的二分类&#xff0c;正负样本比例大概为9&#xff1a;1&#xff0c;模型倾向于全部预测为正样…