手撕八大排序(下)

news2024/11/15 13:38:32

目录

交换排序

冒泡排序:

快速排序

Hoare法

挖坑法

前后指针法【了解即可】

优化 

 再次优化(插入排序)

迭代法

其他排序

归并排序

计数排序

排序总结


结束了上半章四个较为简单的排序,接下来的难度将会大幅度上升,那么就开始本章的排序吧!

交换排序

基本思想:所谓交换,就是根据序列中两个记录键值的比较结果来对换这两个记录在序列中的位置,交换排序的特点是:将键值较大的记录向序列的尾部移动,键值较小的记录向序列的前部移动。

冒泡排序:

冒泡排序是相对来说是最简单的排序,我们从c语言开始就接触过了冒泡排序,其主要思想如下:

  • 它的基本思想是对所有相邻记录的关键字值进行比效,如果是逆顺(a[j]>a[j+1]),则将其交换,最终达到有序化。

具体的就不多说了,直接跳过;

代码如下:

/**
     * 冒泡排序
     */
    public static void bubbleSort1(int[] array) {
        for (int i = 0; i < array.length; i++) {
            for (int j = 0; j < array.length; j++) {
                if (array[j] > array[j+1]) {
                    swap(array,j,j+1);
                }
            }
        }
    }
    //优化
    public static void bubbleSort(int[] array) {
        for (int i = 0; i < array.length - 1; i++) {
            boolean flg = false;
            for (int j = 0; j < array.length - 1; j++) {
                if (array[j] > array[j+1]) {
                    swap(array,j,j+1);
                    flg = true;
                }
            }
            if ( !flg) {
                return;
            }
        }
    }

【冒泡排序的特性总结】

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

而接下来要介绍的就是交换排序的大哥:快速排序

快速排序

快速排序是Hoare于1962年提出的一种二叉树结构的交换排序方法。

无论是什么版本的快排,都是遵循一个原则:选 key划分,选取一个 key ,使 key 左边的值小于等于 key ,右边的值大于 key ,这是快排的核心思想。

Hoare提出的快排又叫做Hoare法。

Hoare法

其基本思想为:

任取待排序元素序列中的某元素作为基准值,按照该排序码将待排序集合分割成两子序列,左子序列中所有元素均小于基准值,右子序列中所有元素均大于基准值,然后最左右子序列重复该过程,直到所有元素都排列在相应位置上为止。

步骤如下:

  • 选取最左边的值为 key,比较时右边先走
  • 因选的是左边,所以右边会先走(向左走),当右边在走的过程中遇到小于等于 key 的值时停下
  • 右边走完后,换左边走(向右走),当遇到大于 key 的值时停止
  • 此时交换左右两处的值
  • 当左遇到右时(必定相遇,因为一次走一步),终止循环
  • 执行最后一步,交换此时左(右)与 key 值,此时就完成了需求:右边值 <= key < 左边值

这时单次排序;

为了方便动图演示,我就直接抄一个动图

霍尔版

单次循环结束后,我们将整个区域分为了【begin,key - 1】 和 【key + 1, end】两个区域,将其中的两块区域传入函数,继续执行递归,当所有数据都排序完成后,快排就结束了

具体代码如下:

    public static void quickSort(int[] array) {
        quick(array,0,array.length - 1);
    } 
    public static void quick(int[] array,int start, int end) {
        if(start >= end) { // 为什么要大于end?如果是有序的情况,== 就会失效
            return;
        }
        int pivot = partition(array,start,end);
        quick(array,0,pivot - 1);
        quick(array,pivot+1,end);
    }
     private static int partition(int[] array,int left,int right) {
        int tmp = array[left];
        int i = left;
        while (left < right) { // 一趟排序
            while (left< right && array[right] >= tmp) {
                right--;
            }
            while (left< right && array[left] <= tmp) {
                left++;
            }
            swap(array,left,right);
        }
        swap(array,left,i);
        return left;
    }

挖坑法

基本思想:

Hoare法的另一种思想,不过挖坑法为了便于理解,引入了坑位这个概念,简单来说就是先在 key 处挖坑,然后右边先走(假设 key 在最左边),找到小于等于 key 的值,就将此值放入到坑中,并在这里形成新坑;然后是左边走,同样的,找到值 -> 填入坑 -> 挖新坑,如此重复,直到左右相遇,此时的相遇点必然是一个未填充的坑,当然这个坑就是给 key 值准备的。

动图演示

挖坑法

 代码如下:


    private static int partition2(int[] array,int left,int right) {
        int temp = array[left];
        while (left < right) { // 一趟排序
            while (left < right && array[right] >= temp) {
                right--;
            }
            array[left] =array[right];
            while (left < right && array[left] <= temp) {
                left++;
            }
            array[right] =array[left];
        }
        array[left] = temp;
        return left;
    }
    private static void swap(int[] array,int i,int j) {
        int tmp = array[i];
        array[i] = array[j];
        array[j] = tmp;
    }

前后指针法【了解即可】

基本思想:

前后指针法不同于之前的两种思想,但是其核心依旧是利用 key   ,找一个基准,创建两个指针,一个prev 指向 key 下标,一个cur指向 key 的下一个位置;

判断 cur 处的值是否小于等于 key 值,如果是,则先 ++prev 后再交换 prev 与 cur 处的值,如此循环,直到 cur 移动至数据尾,最后一次交换为 key 与 prev 间的交换,交换完后就达到快排的目的。

动图演示:

双指针法

 代码如下:

//前后指针法 【了解即可】
    private static int partition3(int[] array,int left,int right) {
        int prev = left ;
        int cur = left+1;
        while (cur <= right) {
            if(array[cur] < array[left] && array[++prev] != array[cur]) {
                swap(array,cur,prev);
            }
            cur++;
        }
        swap(array,prev,left);
        return prev;
    }
     private static void swap(int[] array,int i,int j) {
        int tmp = array[i];
        array[i] = array[j];
        array[j] = tmp;
    }

【快速排序的特性总结】

快速排序不同于其他排序,快排需要区分最好情况和最坏情况;如果我们给定一个有序的数组,对其进行快排,无论是利用以上哪种方法,结果都如下:

由于有序,每一次排序都需要全部遍历一遍;如果是无序情况,每一次排序过程中都会使一部分数据趋近有序。

最好情况(无序):

  • 时间复杂度:O(n*logn)
  • 空间复杂度:O(logN)

类似于一颗满二叉树:

每次 key 都恰好是中间位置。

最坏情况(有序):

  • 时间复杂度:O(n^2)
  • 空间复杂度:O(n)

 记住一句话,快速排序,越有序越慢,越无序越快。

  • 稳定性:不稳定

对于最坏情况明显不能满足我们的需求,由于jdk默认分配的内存很小,对于一个很大的数据使用快排,递归太多了很可能照成栈溢出的情况,故此我们需要对其进行优化。

假设是一个升序的情况,没有左树:

我们对其进行优化:

优化 

方法1: 随机选取基准法

思想:

3的左边都小于3,3的右边都大于3;

再对左右进行递归。

 但是随机选取基准法,太随机了,也并不理想不是最好的优化方法。

方法2:三数取中法:

从最左边,中间和最右边三个数中选取中间的值作为基准:

这样做就可能变成二分查找,那么就不会出现最坏情况了。

代码如下:

 //(初次优化)优化方法之一:三数取中法
    private static int midThree(int[] array,int left,int right) {
        int mid = (left + right) / 2;

        if (array[left] < array[right]) {
            if (array[mid] < array[left]) {
                return left;
            } else if (array[mid] > array[right]) {
                return right;
            } else {
                return mid;
            }
        } else {
            //array[left] > array[right]
            if (array[mid] < array[right]) {
                return right;
            } else if (array[mid] > array[left]) {
                return left;
            } else {
                return mid;
            }
        }
    }

 再次优化(插入排序)

对于一颗完全二叉树而言,最后两层占了大多数数据:

代码如下:

//再次优化
    private static void insertSort2(int[] array,int left,int right) {
        for (int i = left+1; i <= right; i++) {
            int tmp = array[i];
            int j = i-1;
            for (; j >= left ; j--) {
                if(array[j] > tmp) {
                    array[j+1] = array[j];
                }else {
                    break;
                }
            }
            array[j+1] = tmp;
        }
    }

迭代法

利用栈的性质来完成快速排序

核心思想:

参照之前的代码:

  • 迭代法同样对此进行划分,【start,pivot - 1 】,【pivot + 1,end】,我们将下标压入栈中
  • 对栈中元素进行弹出,每次弹出两个,分别用right和left来接收

排序完后:

我们同样需要压入下标入栈,但是当左右有一边只有一个元素时,不用入栈。

如此反复就可以排序完。

代码如下:

//栈(利用栈的性质)
    public static void quickSort2(int[] array) {
        Deque<Integer> stack = new LinkedList<>();
        int left = 0;
        int right = array.length-1;
        int pivot = partition(array,left,right);
        if(pivot > left+1) {
            stack.push(left);
            stack.push(pivot-1);
        }
        if(pivot < right-1) {
            stack.push(pivot+1);
            stack.push(right);

        }
        while (!stack.isEmpty() ) {
            stack.pop();
            stack.pop();
            pivot = partition(array,left,right);
            if(pivot > left+1) {
                stack.push(left);
                stack.push(pivot-1);
            }
            if(pivot < right-1) {
                stack.push(pivot+1);
                stack.push(right);
            }
        }
    }

其他排序

归并排序

归并排序(MERGE-SORT)是建立在归并操作上的一种有效的排序算法,该算法是采用分治法(Divide andConquer)的一个非常典型的应用。将已有序的子序列合并,得到完全有序的序列;即先使每个子序列有序,再使子序列段间有序。若将两个有序表合并成一个有序表,称为二路归并。

基本思想:

每次将数据均分为两组,直不可再分;每次再将两组数据进行合并,如下图所示:

归并排序

归并排序多用于外排序,即对磁盘中的大数据进行排序。

动图演示:

归并排序

 思路:首先要得到左右皆有序的数组,然后合并,显然这个需要借助递归实现,将大问题化小问题:将原数据分为左右两个区间,交给递归让他们变得有序,最后再执行有序数组合并。依靠递出,区间会慢慢变小,直到区间内只有两个数,执行合并,然后逐渐向上回归,回归的过程就是不断合并的过程,数据最开始的左右区间会逐渐变得有序,当回归到第一层时,执行最后一次有序数组合并,数据整体就变得有序了。

代码如下:

public static void mergeSort1(int[] array) {//保证接口的统一性,所以需要借组mergeSortFunc
        mergeSortFunc(array,0,array.length-1);
    }
    private  static void mergeSortFunc(int[] array,int left,int right) {
        if(left >= right) {
            return;
        }
        int mid = (left+right) / 2;
        mergeSortFunc(array,left,mid);//将数据进行拆分
        mergeSortFunc(array,mid+1,right);
        merge(array,left,right,mid);//将数据合并

    }
    private static void merge(int[] array,int start,int end,int mid) {
        int s1 = start;
        //int e1 = mid;
        int s2 = mid+1;
        //int e2 = end;
        int[] tmp = new int[end-start+1];
        int k = 0;//tmp数组的下标
        while (s1 <= mid && s2 <= end) {
            if(array[s1] <= array[s2]) {
                tmp[k++] = array[s1++];
            }else {
                tmp[k++] = array[s2++];
            }
        }
        while (s1 <= mid) {
            tmp[k++] = array[s1++];
        }
        while (s2 <= end) {
            tmp[k++] = array[s2++];
        }

        for (int i = 0; i < tmp.length; i++) {
            array[i+start] = tmp[i];
        }

    }
    public static void mergeSort(int[] array) {
        int gap = 1;
        while (gap < array.length) {
            // i += gap * 2 当前gap组的时候,去排序下一组
            for (int i = 0; i < array.length; i += gap * 2) {
                int left = i;
                int mid = left + gap - 1;//有可能会越界
                if (mid >= array.length) {
                    mid = array.length - 1;
                }
                int right = mid + gap;//有可能会越界
                if (right >= array.length) {
                    right = array.length - 1;
                }
                merge(array, left, right, mid);
            }
            //当前为2组有序  下次变成4组有序
            gap *= 2;
        }
    }

【归并排序总结】

  • 归并的缺点在于需要O(N)的空间复杂度,归并排序的思考更多的是解决在磁盘中的外排序问题。
  • 时间复杂度:O(N*logN)
  • 空间复杂度:O(N)
  • 稳定性:稳定

计数排序

 基本思想:

  • (1)找出待排序的数组中最大和最小的元素
  • (2)统计数组中每个值为i的元素出现的次数,存入数组C的第i项
  • (3)对所有的计数累加(从C中的第一个元素开始,每一项和前一项相加)
  • (4)反向填充目标数组:将每个元素i放在新数组的第C(i)项,每放一个元素就将C(i)减去1

复制一个动图演示:

代码如下:

public static void countSort(int[] array) {
        //1. 遍历数组 找到最大值 和 最小值
        int max = array[0];
        int min = array[0];
        //O(N)
        for (int i = 1; i < array.length; i++) {
            if(array[i] < min) {
                min = array[i];
            }
            if(array[i] > max) {
                max = array[i];
            }
        }
        //2. 根据范围 定义计数数组的长度
        int len = max - min + 1;
        int[] count = new int[len];
        //3.遍历数组,在计数数组当中 记录每个数字出现的次数 O(N)
        for (int i = 0; i < array.length; i++) {
            count[array[i]-min]++;
        }
        //4.遍历计数数组
        int index = 0;// array数组的新的下标 O(范围)
        for (int i = 0; i < count.length; i++) {
            while (count[i] > 0) {
                //这里要加最小值  反应真实的数据
                array[index] = i+min;
                index++;
                count[i]--;
            }
        }
    }

参考链接:

1.8 计数排序 | 菜鸟教程 (runoob.com)

【计数排序总结】

时间复杂度:O(n + 范围)

空间复杂度:O(范围)

稳定性:稳定

计数排序只使用于数据范围集中的情况,使用场景较少

排序总结

排序分类:

排序方法最好平均最坏空间复杂度稳定性
冒泡排序O(n)O(n^2)O(n^2)O(1)稳定
插入排序O(n)O(n^2)O(n^2)O(1)稳定
选择排序O(n^2)O(n^2)O(n^2)O(1)不稳定
希尔排序O(n)O(n^1.3)O(n^2)O(1)不稳定
堆排序O(n * log(n))O(n * log(n))O(n * log(n))O(1)不稳定
快速排序O(n * log(n))O(n * log(n))O(n^2)O(log(n)) ~ O(n)不稳定
归并排序O(n * log(n))O(n * log(n))O(n * log(n))O(n)稳定

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

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

相关文章

安卓逆向学习及APK抓包(二)--Google Pixel一代手机的ROOT刷入面具

注意:本文仅作参考勿跟操作&#xff0c;root需谨慎&#xff0c;本次测试用的N手Pixel&#xff0c;因参考本文将真机刷成板砖造成的损失与本人无关 1 Google Pixel介绍 1.1手机 google Pixel 在手机选择上&#xff0c;优先选择谷歌系列手机&#xff0c;Nexus和Pixel系列&…

mac系统上hdfs java api的简单使用

文章目录1、背景2、环境准备3、环境搭建3.1 引入jar包3.2 引入log4j.properties配置文件3.3 初始化Hadoop Api4、java api操作4.1 创建目录4.2 上传文件4.3 列出目录下有哪些文件4.4 下载文件4.5 删除文件4.6 检测文件是否存在5、完整代码1、背景 在上一节中&#xff0c;我们简…

五分钟进步系列之nginx(一)

学习方式&#xff1a;先读英文的原版&#xff0c;如果你能看懂就可以到此为止的了。如果你看不懂&#xff0c;可以再看一下我给的较高难度的英文单词的翻译。如果还是看不懂可以去最下面看我翻译的汉语。下面是我在nginx官网中找到的一段话&#xff0c;它给我们描述了nginx的负…

JAVA中加密与解密

BASE64加密/解密 Base64 编码会将字符串编码得到一个含有 A-Za-z0-9/ 的字符串。标准的 Base64 并不适合直接放在URL里传输&#xff0c;因为URL编码器会把标准 Base64 中的“/”和“”字符变为形如 “%XX” 的形式&#xff0c;而这些 “%” 号在存入数据库时还需要再进行转换&…

软件自动化测试工程师面试题集锦

以下是部分面试题目和我的个人回答&#xff0c;回答比较简略&#xff0c;仅供参考。不对之处请指出 1.自我介绍 答&#xff1a;姓名&#xff0c;学历专业&#xff0c;技能&#xff0c;近期工作经历等&#xff0c;可以引导到最擅长的点&#xff0c;比如说代码或者项目 参考&a…

Qt音视频开发19-vlc内核各种事件通知

一、前言 对于使用第三方的sdk库做开发&#xff0c;除了基本的操作函数接口外&#xff0c;还希望通过事件机制拿到消息通知&#xff0c;比如当前播放进度、音量值变化、静音变化、文件长度、播放结束等&#xff0c;有了这些才是完整的播放功能&#xff0c;在vlc中要拿到各种事…

ImportError: Can not find the shared library: libhdfs3.so解决方案

大家好,我是爱编程的喵喵。双985硕士毕业,现担任全栈工程师一职,热衷于将数据思维应用到工作与生活中。从事机器学习以及相关的前后端开发工作。曾在阿里云、科大讯飞、CCF等比赛获得多次Top名次。喜欢通过博客创作的方式对所学的知识进行总结与归纳,不仅形成深入且独到的理…

WSL2通过OpenCV调用并展示本机摄像头的RTSP视频流

文章目录前言安装 CMake安装 OpenCV 和 FFmpeg启动 Windows 本机的 RTSP 视频流查看本机摄像头设备开始推流开放本机防火墙用 OpenCV 接收视频流结果展示前言 本篇博客的由来如上图哈哈&#xff0c;WSL2 相关安装教程可以参考我之前的博客&#xff1a;Win11安装WSL2和Nvidia驱动…

如果我只有一个奔腾CPU,怎么加速推理神经网络?

前言 有人说当下的AI热潮不过是算力堆砌的产物。现在层出不穷的各种大模型大训练集&#xff0c;使用复杂精致的技术在排行榜上不断刷新分数&#xff0c;这些人似乎忘了一件事情&#xff0c;AI模型最终是要落地的&#xff0c;是要用的&#xff0c;如果不能普及开去那和在象牙塔…

2023年最新软著申请流程(一):软件著作权说明、国家版权官网的账号注册与实名认证

若该文为原创文章&#xff0c;转载请注明原文出处 本文章博客地址&#xff1a;https://hpzwl.blog.csdn.net/article/details/129230460 红胖子(红模仿)的博文大全&#xff1a;开发技术集合&#xff08;包含Qt实用技术、树莓派、三维、OpenCV、OpenGL、ffmpeg、OSG、单片机、软…

如何使用Cliam测试云端环境IAM权限安全

关于Cliam Cliam是一款针对云端安全的测试工具&#xff0c;在该工具的帮助下&#xff0c;广大研究人员可以轻松枚举目标云端环境的IAM权限。当前版本的Cliam支持下列云端环境&#xff1a;AWS、Azure、GCP和Oracle。 Cliam同时也是一个云端权限识别工具&#xff0c;该工具是一…

Mapper代理开发——书接MaBatis的简单使用

在这个mybatis的普通使用中依旧存在硬编码问题,虽然静态语句比原生jdbc都写更少了但是还是要写&#xff0c;Mapper就是用来解决原生方式中的硬编码还有简化后期执行SQL UserMapper是一个接口&#xff0c;里面有很多方法&#xff0c;都是一一和配置文件里面的sql语句的id名称所对…

HEC-HMS和HEC-RAS快速入门、防洪评价报告编制及洪水建模、洪水危险性评价等应用

目录 ①HEC-RAS一维、二维建模方法及实践技术应用 ②HEC-HMS水文模型实践技术应用 ③新导则下的防洪评价报告编制方法及洪水建模实践技术应用 ④基于ArcGIS水文分析、HEC-RAS模拟技术在洪水危险性及风险评估 ⑤山洪径流过程模拟及洪水危险性评价 ①HEC-RAS一维、二维建模方…

Torch同时训练多个模型

20230302 引言 在进行具体的研究时&#xff0c;利用Torch进行编程&#xff0c;考虑到是不是能够同时训练两个模型呢&#xff1f;&#xff01;而且利用其中一个模型的输出来辅助另外一个模型进行学习。这一点&#xff0c;在我看来应该是很简单的&#xff0c;例如GAN网络同时训…

docker安装rabbitmq并挂载

1、拉取镜像 management&#xff1a;表示可以通过web页面管理。 alpine&#xff1a;表示是linux最小版本&#xff0c;不推荐新手安装。 docker pull rabbitmq:management2、创建用于挂载的目录 mkdir -p /mydata/rabbitmq/{data,conf,log} # 创建完成之后要对所创建文件授权…

从菜鸟程序员到高级架构师,竟然是因为这个字final

final实现原理 简介 final关键字&#xff0c;实际的含义就一句话&#xff0c;不可改变。什么是不可改变&#xff1f;就是初始化完成之后就不能再做任何的修改&#xff0c;修饰成员变量的时候&#xff0c;成员变量变成一个常数&#xff1b;修饰方法的时候&#xff0c;方法不允…

23种设计模式之简单工厂模式

一、场景简介 1、引入场景 订餐流程简单描述 食品抽象类,规定食品的基础属性操作 鱼类,鸡肉类食品类扩展 订餐流程类,根据食品名称,加工指定类型食品 模拟客户端预定操作 2、源代码实现 关系图谱 代码实现 /*** 简单工厂模式引入场景*/ public class C01_InScene { p…

【word】论文排版思路

1、 首先把所有中文的字体都按照要求改一下&#xff0c;记住都改成正文的字号和字体&#xff0c;后面再修改标题的&#xff0c;然后再改英文的&#xff0c;不要把顺序弄错了&#xff0c;不然得回头再改标题 然后定位文章里所有英文方法如下&#xff1a; 按CTRLF打开替换对话…

记录--虚拟滚动探索与封装

这里给大家分享我在网上总结出来的一些知识&#xff0c;希望对大家有所帮助 1. 介绍 什么是虚拟滚动&#xff1f;虚拟滚动就是通过js控制大列表中的dom创建与销毁&#xff0c;只创建可视区域dom&#xff0c;非可视区域的dom不创建。这样在渲染大列表中的数据时&#xff0c;只创…

快速生成QR码的方法:教你变成QR Code Master

目录 简介: 具体实现步骤&#xff1a; 一、可以使用Python中的qrcode和tkinter模块来生成QR码。以下是一个简单的例子&#xff0c;演示如何在Tkinter窗口中获取用户输入并使用qrcode生成QR码。 1&#xff09;首先需要安装qrcode模块&#xff0c;可以使用以下命令在终端或命令…