排序的简单理解(下)

news2024/11/24 13:52:47

4.交换排序

        基本思想:所谓交换,就是根据序列中两个记录键值的比较结果来对换这两个记录在序列中的位置

        交换排序的特点是:将键值较大的记录向序列的尾部移动,键值较小的记录向序列的前部移动。

4.1 冒泡排序

        冒泡排序(Bubble Sorting)即:通过对待排序的序列从前往后,依次比较相邻元素的值,若发现逆序则交换位置,使较大的元素逐渐移动到后部

4.1.1 算法分析

        下面的分析以将序列{2, 9, 7, 10, 30}从小到大排序为例!

        基本思想就是,在每一趟排序最终实现将该趟得到的最大的数移到序列的最后端

        核心操作就是通过比较相邻两个元素实现当相邻的两个元素逆序的时候,我们就交换它们。

第1趟排序:
        第1趟排序共比较了4次,将最大的数30冒泡到了序列的尾部。

第2趟排序:
        由于第一趟排序已经将最大是数30给冒泡到了最末端,因此在本次排序中,不需要再比较最后一个元素,故此本趟共比较了3次,将子序列(前四个元素)中最大的数10(整个序列中倒数第二大的数)冒泡到了子序列的尾端(原序列的倒数第二个位置)。

第3趟排序:
        在第三趟排序时,同理,倒数两个元素位置已经确定,即第一、第二大的数已经排好位置,只需要再将倒数第三大的数确认即可。故比较2次,实现倒数第三大的数9的位置确定。

第4趟排序:
        在第四趟排序时,只有第一、第二个元素的位置还不确定,只需要比较一次,若逆序,则交换即可。到此,排序算法完成,原序列已经排序成为一个递增的序列!

小结

  • 一共进行了数组大小-1次趟排序,即外层循环arr.length-1次;
  • 每趟排序进行了逐趟减小次数的比较,即内层循环arr.length-i-1次,i从0依次增加。

4.1.2 代码实现

        详细代码如下:

/**
     * @version 1.0
     * 冒泡排序
     */
        public static void main(String[] args) {
            int[] array = {2,9,7, 10, 30};
            //排序前
            System.out.println("排序前:" + Arrays.toString(array));
            //冒泡排序
            for (int i = 0; i < array.length - 1; i++) {
                System.out.println("第" + (i+1) + "趟排序开始!");
                for (int j = 0; j < array.length - i - 1; j++) {
                    //如果前面的数比后面的数大,则交换
                    if(array[j] > array[j+1]){
                        //交换
                        int temp = array[j];
                        array[j] = array[j+1];
                        array[j+1] = temp;
                    }
                    System.out.println("------第" + (j+1) + "趟排序: " + Arrays.toString(array));
                }
                System.out.println("第" + (i+1) + "趟排序完成: " + Arrays.toString(array));
                System.out.println("================================================");
            }

            //输出排序后的结果
            System.out.println("排序后:" + Arrays.toString(array));
        }

        结果展示:

排序前:[2, 9, 7, 10, 30]
第1趟排序开始!
------第1趟排序: [2, 9, 7, 10, 30]
------第2趟排序: [2, 7, 9, 10, 30]
------第3趟排序: [2, 7, 9, 10, 30]
------第4趟排序: [2, 7, 9, 10, 30]
第1趟排序完成: [2, 7, 9, 10, 30]
================================================
第2趟排序开始!
------第1趟排序: [2, 7, 9, 10, 30]
------第2趟排序: [2, 7, 9, 10, 30]
------第3趟排序: [2, 7, 9, 10, 30]
第2趟排序完成: [2, 7, 9, 10, 30]
================================================
第3趟排序开始!
------第1趟排序: [2, 7, 9, 10, 30]
------第2趟排序: [2, 7, 9, 10, 30]
第3趟排序完成: [2, 7, 9, 10, 30]
================================================
第4趟排序开始!
------第1趟排序: [2, 7, 9, 10, 30]
第4趟排序完成: [2, 7, 9, 10, 30]
================================================
排序后:[2, 7, 9, 10, 30]

进程已结束,退出代码0

4.1.3 算法的不足及优化

        我们仔细观察一下我们上述数组在进行冒泡法时每一个外循环后的结果:

                 

        我们发现其实在第一次外循环结束之后,我们的当前数组已经完成了冒泡法整个算法的最终结果,但是由于我们的算法规定,它依旧要跑array.length-1次外循环,故此我们需要完成代码的优化,当我们的数组提前完成排序后,就要提前结束外循环,终止我们的冒泡排序;

        我们可以发现一个无序的数组在经过冒泡算法的排序之后,这些元素的位置在最后都是固定的,每一次的内循环,相应的元素可以理解为都是往那个自己最终的位置上跑,但是当一次内循环之后,发现我们当前的元素位置和该次内循环开始之前的元素位置一样,没有发生变化,这时候我们可以确认我们的元素都已经到达了自己的最终位置,可以提前终止冒泡算法,不在进行其他的外循环;

        所以最终的解决方案就是我们设置一个flag标志位,来判断当前内循环前后数组的元素有没有发生顺序的变化,优化代码如下图所示;

 public static void main(String[] args) {
        int[] array = {5, 1, 2, 3, 4};
        //排序前
        System.out.println("排序前:" + Arrays.toString(array));

        boolean flag = false; //用于标记是否进行了交换,true则说明进行了交换,false表示无

        //冒泡排序
        for (int i = 0; i < array.length - 1; i++) {
            System.out.println("第" + (i+1) + "趟排序开始!");
            for (int j = 0; j < array.length - i - 1; j++) {
                //如果前面的数比后面的数大,则交换
                if(array[j] > array[j+1]){
                    //交换
                    flag = true; //标记进行了交换
                    int temp = array[j];
                    array[j] = array[j+1];
                    array[j+1] = temp;
                }
                System.out.println("------第" + (j+1) + "趟排序: " + Arrays.toString(array));
            }
            System.out.println("第" + (i+1) + "趟排序完成: " + Arrays.toString(array));
            System.out.println("================================================");
            if (!flag){
                //如果没有进行交换则直接退出,说明排序已经完成
                break;
            }else {
                //回退
                flag = false;
            }
        }
        //输出排序后的结果
        System.out.println("排序后:" + Arrays.toString(array));
    }

        测试结果: 

                          

        如图所示,经过优化后,我们相比于之前的代码,减少了两次内循环,提前结束了冒泡算法,大大的节省了时间资源; 

4.1.4 冒泡排序的特性总结

1、冒泡排序是一种非常容易理解的排序

        时间复杂度:O(N^2)

        空间复杂度:O(1)

        稳定性:稳定

2、什么时候最快?
        当输入的数据已经是正序时,我们的内循环和外循环的执行次数比较少;

4.2 快速排序

        快速排序是对冒泡排序的一种改进。基本思想为:通过一趟排序将要排序的数据分割为独立的两个部分,其中一部分的所有数据比另外一部分的所有数据要小,然后按照此方法对这两部分分别进行快速排序,整个过程可以递归进行,以此达到整个数据变成有序序列。

4.2.1 算法分析(递归)

        快速排序算法通过多次比较和交换来实现排序,其排序流程如下:
        (1)首先设定一个分界值,通过该分界值将数组分成左右两部分。
        (2)将大于或等于分界值的数据集中到数组右边,小于分界值的数据集中到数组的左边。此时,左边部分中各元素都小于分界值,而右边部分中各元素都大于或等于分界值。
        (3)然后,左边和右边的数据可以独立排序。对于左侧的数组数据,又可以取一个分界值,将该部分数据分成左右两部分,同样在左边放置较小值,右边放置较大值。右侧的数组数据也可以做类似处理。
        (4)重复上述过程,可以看出,这是一个递归定义。通过递归将左侧部分排好序后,再递归排好右侧部分的顺序。当左、右两个部分各数据排序完成后,整个数组的排序也就完成了。

        下面来举例来详细的分析:

        首先,我们会把数组中的一个数当作基准数,然后从两边进行检索。

        其次,按照如下步骤进行:

                1、先从右边检索比基准数小的
                2、再从左边检索比基准数大的
                3、一旦检索到,就停下,并将检索到的两个元素进行交换
                4、重复上述步骤,直到检索相遇,则替换基准数,并更新区间,递归进行,最终序列会变得有序

        接下来就以{6,1,8,0,2,9,5,3,7}为例详细来分析一下步骤,具体分析一下第一趟排序:以6为基准数的步骤:

1、红色块标识基准数,left、right初始位置如图所示

2、right不断向左移动,寻找比基准数6小的数,如图所示,找到了3

3、此时left开始移动,不断向右移动,寻找比基准数大的数,找到了8,这时,left、right都找到了对应的数,进行交换:

4、right继续向左寻找比基准数6小的数,找到后停止移动,此时left继续向右寻找比基准数大的数,当left与right都找到对应的数后,再次将二者的数值进行交换。

5、重复上述步骤,一直到left与right相遇,二者共同指向了5的位置,则将基准数与该位置的数进行交换,这样就可以观察到,6的左边都是比6小的,右边都是比6大的。

6、该过程需要递归进行,直到序列有序。即以5为基准数,递归6左边的区间,再以9为基数递归6右边的区间,反复进行,直到left >right退出。

4.2.2 代码实现

        冒泡法代码如下:

public static void quickSort(int[] arr, int left, int right) {
            //边界条件
            if (left > right){
                return;
            }

            //定义基准数和左右指针
            int l = left;
            int r = right;
            int base = arr[left];

            //循环,将比基准数小的放在左边,比基准数大的放在右边
            while (l != r){
                //先从右边找比基准数小的,停下
                while (arr[r] >= base && l < r){
                    r--;
                }
                //从左边找比基准数大的,停下
                while (arr[l] <= base && l < r){
                    l++;
                }
                //此时已经找到对应的l 和 r,进行交换
                int temp = arr[l];
                arr[l] = arr[r];
                arr[r] = temp;
            }
            //至此,基准数两边都按照需要排好了,只需要将基准数与lr相遇的位置进行交换
            arr[left] = arr[l];
            arr[l] = base;
            //打印中间结果
            System.out.println(Arrays.toString(arr));
            //先向左找
            quickSort(arr, left, r-1);
            //向右递归
            quickSort(arr, l+1, right);
        }

        测试代码:

public static void main(String[] args) {
     int[] arr = {6,1,8,0,2,9,5,3,7};
     quickSort(arr, 0, arr.length-1);
     System.out.println("排序后: " + Arrays.toString(arr));
}

        测试结果:

4.2.3 快速排序非递归实现  

思路:

  • 建立一个栈

  • 先让一组数据的起点入栈

  • 再让一组数据的终点出栈

  • 然后两次出栈,分别作为该数据的起点与终点

  • 然后经过我们上面所写的方法进行排序后

  • 再将两组数据进行入栈

  • 以此循环直到栈为空

        代码实现: 

    //快速排序递归实现
    public int[] quickSortPlus(int[] array) {
        int[] arr = Arrays.copyOf(array,array.length);
        Deque<Integer> stack = new LinkedList<>();
        int left = 0;
        int right = array.length-1;
        int pivot = 0;
        stack.push(left);
        stack.push(right);
        while (!stack.isEmpty()) {
            right= stack.pop();
            left = stack.pop();
            pivot = partition(arr,left,right);
            if(pivot > left+1) {
                stack.push(left);
                stack.push(pivot-1);
            }
            if(pivot < right-1) {
                stack.push(pivot+1);
                stack.push(right);
            }
        }
        return arr;
    }
    private  int partition(int[] array,int left,int right) {
        int tmp = array[left];
        while (left < right) {
            while (left< right && array[right] >= tmp) {
                right--;
            }
            array[left] = array[right];
            while (left< right && array[left] <= tmp) {
                left++;
            }
            array[right] = array[left];
        }
        array[left] = tmp;
        return left;
    }

4.2.4 特性总结

1. 快速排序整体的综合性能和使用场景都是比较好的,所以才敢叫 快速 排序

2. 时间复杂度: O(N*logN)

3. 空间复杂度:O(logN)

4. 稳定性:不稳定

5. 归并排序

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

5.1算法分析 

合并相邻有序子序列 

...以此类推

...

...

如果当其中的一个子序列全部转移到temp数组中时,另外未空的子序列的元素直接全部按当前顺序移入到temp中即可。

5.2 代码实现

        代码部分:

static int count = 0;
        public static void main(String[] args) {
            int[] arr = {10, 6, 7, 1, 3, 4, 2,9};
            int[] temp = new int[arr.length];
            mergeSort(arr, 0, arr.length - 1, temp);
            System.out.println("归并排序后: arr[] = " + Arrays.toString(arr));
        }

        //归并排序
        public static void mergeSort(int[] arr, int left, int right, int[] temp){
            if (left < right){
                int mid = left - (left - right) / 2;
                //向左递归分解
                mergeSort(arr, left, mid, temp);
                //向右递归分解
                mergeSort(arr, mid + 1, right, temp);
                //排序 合并
                merge(arr, left, mid, right, temp);
            }
        }
        /**
         * 合并的方法
         * @param arr  排序的原始数组
         * @param left  左边有序序列的初始索引
         * @param mid  中间索引
         * @param right  右边索引
         * @param temp  中转数组
         */
        public static void merge(int[] arr, int left, int mid, int right, int[] temp){
            int i = left; //初始化i,左边有序序列的初始索引
            int j = mid + 1; //初始化j,右边有序序列的初始索引
            int t = 0; //指向temp数组的当前索引
            //先把左右两边有序数据按照规则填充到temp数组,直到左右两边有一边处理完毕
            while (i <= mid && j <= right){
                if (arr[i] <= arr[j]){
                    temp[t] = arr[i];
                    t++;
                    i++;
                }else {
                    temp[t] = arr[j];
                    t++;
                    j++;
                }
            }
            //把剩余的一方依次填充到temp数组
            while (i <= mid){ //左边序列还有剩余的元素
                temp[t++] = arr[i++];
            }
            while (j <= right){ //右边序列还有剩余的元素
                temp[t++] = arr[j++];
            }
            //将temp数组的元素拷贝到arr
            //拷贝每次小序列
            t = 0;
            int tempLeft = left;
            while (tempLeft <= right){
                arr[tempLeft++] = temp[t++];
            }
            count++;
            System.out.println("第" + count + "次合并: arr[] = " + Arrays.toString(arr));
        }

        测试结果展示:

        分析:

{10, 6, 7, 1, 3, 4, 2,9}被拆分成了{10, 6}{7, 1}{3, 4}{2, 9}:

第一次合并:{6, 10}有序
第二次合并:{1, 7}有序
第三次合并: {1, 6, 7, 10}有序
第四次合并:{3, 4}有序
第五次合并:{2, 9}有序
第六次合并: { 2,3,4,9}有序
第七次合并:{1,2,3,4,6,7,9,10}有序

5.3 特点总结

  1. 归并的缺点在于需要O(N)的空间复杂度,归并排序的思考更多的是解决在磁盘中的外排序问题。

  2. 时间复杂度:O(N*logN)

  3. 空间复杂度:O(N)

  4. 稳定性:稳定

 5.4 海量数据的排序问题

        外部排序:排序过程需要在磁盘等外部存储进行的排序

        前提:内存只有 1G,需要排序的数据有 100G

        因为内存中因为无法把所有数据全部放下,所以需要外部排序,而归并排序是最常用的外部排序:

         1. 先把文件切分成 200 份,每个 512 M

         2. 分别对 512 M 排序,因为内存已经可以放的下,所以任意排序方式都可以

        3. 进行 2路归并,同时对 200 份有序文件做归并过程,最终结果就有序了

6.排序算法复杂度及稳定性分析

        详解总概图如下所示:

ps:本次的内容就到这里了,本文的相关内容吸取了博主【兴趣使然黄小黄 】的相关思想,如果大家感兴趣的话,可以去了解一下他,如果喜欢的话还请一键三连哦!!!

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

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

相关文章

初始数据库 - 了解数据库

centos 7 版本当中安装 mysql 安装与卸载中&#xff0c;⽤⼾全部切换成为root&#xff0c;⼀旦 安装&#xff0c;普通⽤⼾是可以使用的。 卸载不需要的环境 首先&#xff0c;在安装之前&#xff0c;要先确定你当前系统当中是否已经有了mysql 的环境&#xff0c;如果你不想卸…

2023全国职业院校技能大赛信息安全管理与评估正式赛(模块三CTF)

全国职业院校技能大赛高等职业教育组信息安全管理与评估 \任务书\ 模块三 网络安全渗透、理论技能与职业素养 极安云科专注技能竞赛&#xff0c;包含网络建设与运维和信息安全管理与评估两大赛项&#xff0c;及各大CTF&#xff0c;基于两大赛项提供全面的系统性培训&#xf…

用户案例:系统保留分区未分配驱动器号

“遇到了Win7保留分区无法改变驱动器号的问题。我尝试使用激活工具激活Win7时收到提示&#xff0c;系统保留分区未分配驱动器号。根据网上的建议&#xff0c;我打算在磁盘管理中给保留分区分配驱动号。然而&#xff0c;在电脑中我发现了两个未分配驱动器号的分区&#xff0c;一…

【PTA刷题+代码+详解】求二叉树度为1的结点个数(递归法)

文章目录 题目C代码详解 题目 在二叉树T中&#xff0c;其度为1的结点是指某结点只有左孩子或只有右孩子。利用递归方法求二叉树T的度为1的结点个数。 1&#xff09;如果TNULL&#xff0c;则是空树&#xff0c;度为1的结点个数为0&#xff0c;返回值为0&#xff1b; 2&#xff0…

GEE:重分类

作者:CSDN @ _养乐多_ 本文记录了在 Google Earth Engine(GEE)平台上对一副类别图像进行重分类的代码。并以 COPERNICUS/Landcover/100m/Proba-V-C3/Global 数据集中的土地利用数据为例。 结果如下图所示, 文章目录 一、核心函数二、示例代码三、代码链接一、核心函数 核…

保姆级:Windows Server 2012上安装.NET Framework 3.5

目录 一.问题所在无法在安装SQL server2008&#xff08;2012&#xff09; 1.无法安装一下功能 .NET Framework 3.5 二.解决措施 1、打开服务器管理器 2、添加角色和功能 3、选择安装功能 4、指定备用源路径 5、配置本地文件路径 一.问题所在无法在安装SQL server2008&…

【启扬方案】启扬储能管理平板助力储能电站实现智能且高效化运行

在储能领域&#xff0c;储能电站扮演着重要角色&#xff0c;储能电站技术的应用贯穿于电力系统发电、输电、配电、用电的各个环节。实现电力系统削峰填谷、可再生能源发电波动平滑与跟踪计划处理、高效系统调频&#xff0c;增加供电的可靠性。 但随着储能电⼒系统建设发展得越来…

单片机——通信协议(FPGA+c语言应用之iic篇)

一.I2C的功能特点 &#xff08;1&#xff09;功能包括&#xff1a; 1.只需要两条总线&#xff1b; 2.没有严格的波特率要求&#xff0c;例如使用RS232&#xff0c;主设备生成总线时钟&#xff1b; 3.所有组件之间都存在简单的主/从关系&#xff0c;连接到总线的每个设备均可通…

使用Microsoft Dynamics AX 2012 - 9. 核心设置和基本功能

企业的组织结构决定了ERP系统的设置。因此&#xff0c;在开始使用Dynamics AX之前&#xff0c;需要有一个实施项目来设置企业组织和其他核心参数。 实例是Dynamics AX的独立安装&#xff0c;它包含自己的数据库和应用程序。因此&#xff0c;在一个实例中修改配置设置和可编程对…

【Netty的线程模型】

Netty的线程模型 Netty的线程模型知识拓展单Reactor单线程模型单Reactor多线程模型主从Reactor模型 Netty的线程模型 Netty通过Reactor模型基于多路复用器接收并处理用户请求的&#xff0c;多路复用IO模型参考&#xff1a; 多路复用IO模型: 操作系统的IO模型有哪些&#xff1f…

探索SSL证书的应用场景,远不止网站,还有小程序、App Store等

说到SSL证书&#xff0c;我们都知道其是用于实现HTTPS加密保障数据安全的重要工具&#xff0c;在建设网站的时候经常会部署SSL证书。但实际上&#xff0c;SSL证书的应用场景远不止网站&#xff0c;它还被广泛地应用到小程序、App Store、抖音广告、邮件服务器以及各种物联网设备…

解决 Photoshop 中的“暂存盘已满”错误

问题&#xff1a;“暂存盘已满”错误 解决方案 注意: 建议在使用 Photoshop 时&#xff0c;操作系统硬盘上具有 50 GB 的可用空间。根据您正在处理的文件类型&#xff0c;可能需要额外的可用空间。 CtrlK打开首选项【暂存盘】-【D盘】优化 Photoshop 的使用空间 禁用自动恢复存…

【JVM从入门到实战】(四)类的生命周期

什么是类的生命周期 类的生命周期描述了一个类加载、连接、初始化、使用、卸载的整个过程 一个类完整的生命周期如下&#xff1a; 加载阶段 加载阶段第一步是类加载器根据类的全限定名通过不同的渠道以二进制流的方式获取字节码信息。 程序员可以使用Java代码拓展的不同的渠道…

C语言—每日选择题—Day45

第一题 1. 以下选项中&#xff0c;对基本类型相同的指针变量不能进行运算的运算符是&#xff08;&#xff09; A&#xff1a; B&#xff1a;- C&#xff1a; D&#xff1a; 答案及解析 A A&#xff1a;错误&#xff0c;指针不可以相加&#xff0c;因为指针相加可能发生越界&…

二百一十六、Flume——Flume拓扑结构之负载均衡和故障转移的开发案例(亲测,附截图)

一、目的 对于Flume的负载均衡和故障转移拓扑结构&#xff0c;进行一个开发测试 二、负载均衡和故障转移 &#xff08;一&#xff09;结构含义 Flume支持使用将多个sink逻辑上分到一个sink组 &#xff08;二&#xff09;结构特征 sink组配合不同的SinkProcessor可以实现负…

Etcd实战(二)-k8s集群中Etcd数据存储

1 介绍 k8s中所有对象的manifest都需要保存到某个地方&#xff0c;这样他们的manifest在api server重启和失败的时候才不会丢失&#xff0c;因此引入了etcd。在k8s中只有api server和etcd直接交互&#xff0c;其它组件都通过api server间接和etcd交互&#xff0c;这样做的好处…

智能优化算法应用:基于引力搜索算法3D无线传感器网络(WSN)覆盖优化 - 附代码

智能优化算法应用&#xff1a;基于引力搜索算法3D无线传感器网络(WSN)覆盖优化 - 附代码 文章目录 智能优化算法应用&#xff1a;基于引力搜索算法3D无线传感器网络(WSN)覆盖优化 - 附代码1.无线传感网络节点模型2.覆盖数学模型及分析3.引力搜索算法4.实验参数设定5.算法结果6.…

计网 - LVS 是如何直接基于 IP 层进行负载平衡调度

文章目录 模型LVS的工作机制初探LVS的负载均衡机制初探 模型 大致来说&#xff0c;可以这么理解&#xff08;只是帮助我们理解&#xff0c;实际上肯定会有点出入&#xff09;&#xff0c;对于我们的 PC 机来说&#xff0c;物理层可以看成网卡&#xff0c;数据链路层可以看成网卡…

AI日报:苹果为使用Mac的人工智能开发者推出开源工具

文章目录 总览主要介绍开发理念开发细节MLX功能用途 MLX可以用于商业软件吗&#xff1f; 总览 苹果正在为开发人员提供新的工具&#xff0c;用于在其硬件上训练和运行大型语言模型。 主要介绍 开发理念 苹果公司通过发布一系列新的开源人工智能工具&#xff0c;向开源人工智能…

QT QIFW Windows下制作安装包(一)

一、概述 1、QIFW是一款基于QT框架开发的跨平台安装框架。QIFW是QT Installer FrameWork的缩写&#xff0c;支持Windows、Linux和macos等多种平台。QIFW可以帮助开发者创建自己的安装程序&#xff0c;将它们打包到通用的安装包中&#xff0c;并提供可视化的界面进行安装。 2…