【Java 数据结构】常见排序算法(下)

news2024/11/13 9:32:53


目录

1、上期回顾

2、冒泡排序

3、快速排序

3.1 理解快速排序的二叉树结构

3.2 Hoare 法

3.3 三数取中

3.4 小区间优化

3.5 挖坑法

3.6 前后指针法

3.7 注意事项

4、归并排序


1、上期回顾

上期我们主要介绍了排序的基本认识,以及四个排序,分别是直接插入排序,希尔排序,选择排序,堆排序,从这些排序中,了解了算法的实现,以及复杂度,和排序稳定性的相关知识,本期我们将继续讲解剩下的排序内容。注:后续所说的复杂度 log,都是以2为底,特殊的会标注出来。


2、冒泡排序

这个排序肯定是见多不怪了,我记得我在学校学习C语言的阶段,第一个接触的排序就是冒泡排序,它本身也是个很简单的排序,这里就直接上代码了:

public void bubbleSort(int[] array) {
    // 外循环控制比较的趟数
    for (int i = 0; i < array.length - 1; i++) {
        boolean flag = true;
        // 内循环控制需要比较的元素个数
        for (int j = 0; j < array.length - 1 - i; j++) {
            if (array[j] > array[j + 1]) {
                swap(array, j, j + 1);
                flag = false;
            }
        }
        if (flag) {
            return;
        }
    }
}

不过这里我们需要注意,此处采用的 flag 是对该排序做的一种优化,如果当进入内循环之后,发现没有发生交换,则表示此时的数据已经有序了,不需要接着去比较了。

  • 时间复杂度分析:这个排序就很简单了,O(n^2)
  • 空间复杂度分析:O(1)
  • 稳定性:稳定

3、快速排序

这个排序算是我们比较重要的一个排序了,快速排序是Hoare在1962年提出的一种二叉树结构的交换排序法,如果你还没有接触过二叉树相关的知识,可以转至博主的二叉树文章中学习学习。

3.1 理解快速排序的二叉树结构

如何理解快速排序的二叉树结构呢?可以这样来想象一下:

你面前有一组数字,令第一个数作为基准值,你需要将这个基准值放到某个位置上,满足基准值的左边都比这个基准值小,右边都比基准值大,因此由基准值为界限又被划为了左右两个区间,这两个区间再次重复上述的操作。这样一来就可以看作一棵二叉树!

而如何确定基准值的位置,这就是我们快速排序要实现的算法!本期我们一共会讲解三种版本,方便大家学习快速排序。下面我们就用一张图,来描述下上述我们所说的理论部分。

这里先不关心博主画图用的是哪种版本的方法,主要来验证下我们上面所说的,每个区间所找的基准值,最终放到固定位置之后,基准值的左边比它小,基准值的右边比它大。 最终我们来从左往右看上图,其实就排序成功了。当然光看图可能了解的不是很通透,那么下面,我们结合着这张图,来实现快速排序的三种算法。

为了实参传递的统一性,我们快速排序的形参统一为以下格式,调用被封装的 quick 方法:

public void quickSort(int[] array) {
    quick(array, 0, array.length - 1);
}

3.2 Hoare 法

我上面画的那幅图就是 Hoare 法,主要逻辑是,令区间第一个数为基准值,定义两个变量,分别表示区间左边起始位置下标,和右边起始位置下标(区间最后一个数的下标),先从右边开始找比基准值小的,再去左边找比基准值大的,找到之后,交换这两个值,当这两个下标相遇了,就把基准值与其中一个下标交换即可,这样就能达到,基准值的左边都比它小,右边都比它大。

代码如下:

private void quick(int[] array, int left, int right) {
    if (left >= right) {
        return;
    }
    int pivot = partitionHoare(array, left, right);
    quick(array, left, pivot - 1); //左区间
    quick(array, pivot + 1, right); //右区间
}
private int partitionHoare(int[] array, int left, int right) {
    int pivot = array[left]; //将该区间第一个数作为基准值
    int begin = left;
    int end = right;
    while (begin < end) {
        // 右边找比基准小的数的下标, 为什么要取 >= 呢?
        while (begin < end && array[end] >= pivot) {
            end--;
        }
        // 左边找比基准大的数的下标
        while (begin < end && array[begin] <= pivot) {
            begin++;
        }
        // 交换
        swap(array, begin, end);
    }
    swap(array, left, begin);
    return begin; //返回基准值当前所处的下标, 左边都比它小, 右边都比它大
}

单看 quick 方法,有点像二叉树的前序遍历,确实也是这样的,前面我们也说过,快排是一种二叉树的结构,结合着代码再去看那幅图,是不是理解的更通透了呢?

这里有两个问题,我们来看 partitionHoare 方法实现里面:

1. 为什么要从右边开始找?

2. 为什么要取等于号,直接 > 或 < 不可以吗?

3. 外面的循环不是限制了 begin < end 吗?为什么里面的 while 还要限制呢?

  1. 如果左边作为基准值的话,只能从右边开始找,如果从左边开始找,当 begin 和 end 相遇之后的值,要比基准值大,因为 begin 和 end 交换后,end 位置的值已经比基准值要大了
  2. 如果不取等于号,可能会造成死循环,你设想下不取等于号时,区间里第一个元素和最后一个元素相同的情况下。
  3. 如果这组数本来就是升序的呢?右边 end 找不到比基准值小的数,如果基准就是最小的数呢?内部 whild 不限制的话 end 是不是会自减成为负数?导致下标不合法了?

上面这三点或许是小伙伴们有疑问的地方,这里给大家伙解释一下,那么再来思考个问题,什么情况下快速排序的效率最低呢?

当数组有序的情况下,快速排序的时间效率最差!试想一下,如果每次找的基准值都是最小的话,划分区间的时候,是不是就成了一棵没有左树的二叉树了啊?类似于一种链表的结构了,见下图:

为了解决这种极端情况下,快速排序划分不均匀的问题,于是便有了三数取中的算法,这算是对快速排序的一种优化,三数取中到底是啥,请接着往后看:

3.3 三数取中

三数取中,是针对快速排序极端情况下,为了均匀的分割区间而采用的一种优化,其步骤是,取该区间的第一个值,最后一个值,以及该区间中间位置的值,求出这三个值中第二大的数,与第一个值交换,这样一来,只要基准值不是最小的,就一定程度上能使区间分割的更均匀。

简单来说,就是有三个数的下标,让你求出第二大的值的下标嘛,这个还是比较简单的,我就直接来放代码了:

private int findMidValOfIndex(int[] array, int left, int right) {
    int mid = (left + right) >> 1;
    if (array[left] < array[right]) {
        if (array[left] < array[mid]) {
            // 走到这里面, left位置一定是最小的值
            // 我们这里只需要判断 mid 和 right 哪个位置小即可
            if (array[mid] < array[right]) {
                //mid是第二大的值
                return mid;
            } else {
                return right;
            }
        } else {
            // 走到这里, 则left位置小于等于right位置, 并大于mid位置, 则left是第二大的值
            return left;
        }
    } else { // 走这个else表示left位置大于等于right位置
        if (array[left] > array[mid]) {
            // 走到这里表示 left 位置一定是最大的值,
            // 我们只需要判断 mid 和 right 位置哪个大即可
            if (array[mid] > array[right]) {
                return mid;
            } else  {
                return right;
            }
        } else {
            //走到这表示 left位置大于right位置, 并小于等于mid位置, 则left是第二大的值
            return left;
        }
    }
}

这样的话,在我们 quick 方法中,求到了第二大值下标后,与 left 位置交换下即可:

private void quick(int[] array, int left, int right) {
    if (left >= right) {
        return;
    }
    // 三数取中
    int mid = findMidValOfIndex(array, left, right);
    swap(array, left, mid);
    int pivot = partitionHoare(array, left, right);
    quick(array, left, pivot - 1);
    quick(array, pivot + 1, right);
}

那这样一来,我们上面的效率最低的例子是不是就可以得到改善了?但是这样优化还是不够,因为当我们数据量够大的时候,二叉树的层数就越高,而越往后,区间被分割的越小,里面的数据也就越接近有序,但是越接近有序了,还会往下分割,这样会造成大量的压栈操作,递归本身就是在压栈的过程嘛,为了减少这样的情况,又有一个优化办法:小区间优化。

3.4 小区间优化

数据量大的时候,分割到区间越小,则表示数据越接近有序了,前面我们认识了一个数据越接近有序效率越快的排序,那就是直接插入排序,所以我们可以进行小区间优化,那么简单来说,就是当区间的数据个数小于某个值的时候,采用直接插入排序算法。

private void quick(int[] array, int left, int right) {
    if (left >= right) {
        return;
    }
    // 小区间优化 -> 如果待排序的数据小于15个,我们直接使用直接插入排序
    if ((right - left + 1) < 15) {
        insertSort(array);
        return;
    }
    // 三数取中
    int mid = findMidValOfIndex(array, left, right);
    swap(array, left, mid);
    int pivot = partitionHoare(array, left, right);
    quick(array, left, pivot - 1);
    quick(array, pivot + 1, right);
}
  • 时间复杂度分析:在我们有了三数取中优化的情况下,可以达到 O(n*logn),如果没有三数取中,极端最坏的情况下,能达到 O(n^2),但是我们通常说的快排都是优化过的,也就是 O(n*logn)。
  • 空间复杂度分析:每次递归都会压栈,随之开辟空间,那么快排类似于二叉树的前序遍历,左子树遍历完了,再有右子树,也就是会压栈,也会出栈,那么最大压栈多少呢?显然是树的高度,即 O(logn)。
  • 稳定性:不稳定
  • 快速排序整体的综合性能和使用场景都是比较好的

到这,快速排序基本上就实现完成了,但是还有两个版本,我们接着往后看。

3.5 挖坑法

这个方法很简单,主要思路就是,一样有两个引用,begin 和 end,end 从右找比基准小的,begin从左找比基准大的, 当 end 找到比基准小的,直接覆盖掉 begin 的位置,接着 begin 找到了比基准大的接着去覆盖 end 位置,相遇后,将基准值覆盖掉 begin 和 end 任意一个位置即可。直接看代码:

private int partitionPivot(int[] array, int left, int right) {
    int pivot = array[left];
    int begin = left;
    int end = right;
    while (begin < end) {
        while (begin < end && array[end] >= pivot) {
            end--;
        }
        array[begin] = array[end];
        while (begin < end && array[begin] <= pivot) {
            begin++;
        }
        array[end] = array[begin];
    }
    array[begin] = pivot;
    return begin;
}

我们平时所见到的快速排序,大多数都是挖坑法居多。

3.6 前后指针法

这个算法用的很少很少,思路就是,定义一个 cur 和 prev,cur 起始位置为 left+1,只要 cur 不大于 right,就往前走,找到比基准小的值就停下来,与 ++prev 位置的值进行交换,这样一来,比基准小的值跑到前面来了,cur 走完了之后,再把基准值与 prev 位置的值交换,也就满足了基准值左边比它小,右边比它大。

private int partitionPointer(int[] array, int left, int right) {
    int prev = left;
    int cur = left + 1;
    while (cur <= right) {
        // && 后面的避免自己跟自己交换
        if (array[cur] < array[left] && ++prev != cur) {
            swap(array, prev, cur);
        }
        cur++;
    }
    swap(array, left, prev);
    return prev;
}

3.7 注意事项

这三种方法,每种方法第一次分割后的结果可能都不相同,所以未来如果碰到类似让你求快排第一次分割后结果的序列,优先考虑挖坑法,再Hoare法,最后考虑前后指针法。

但是博主还是希望看到这篇文章的小伙伴,能把这三种版本都掌握,不怕学的多,就怕你不学。


4、归并排序

这个排序如何去想象呢,就类似于,你拿到一组数的时候,从中间砍一刀,变成了两个区间,接着把这两个区间分别再砍一刀,变成了四个区间,一直重复下去,当区间的元素个数砍成了一个的时候,那么这个区间就有序了,接着我们开始进行二路归并,也就是说把两个有序区间,合并成一个有序区间,这样一来,是不是整体就有序了?我们看图:

归并排序也需要对递归有一定的学习,按照上图来看,我们肯定是要先递归到每个区间只有一个元素的时候才能进行归并的,归并的逻辑就是,将两个有序序列合并成一个有序序列嘛,这还是蛮简单的,我们来看代码:

public void mergeSort(int[] array) {
    mergeSortChild(array, 0, array.length - 1);
}
private void mergeSortChild(int[] array, int left, int right) {
    if (left >= right) {
        return;
    }

    int mid = (left + right) >> 1;
    mergeSortChild(array, left, mid);
    mergeSortChild(array, mid + 1, right);
    merge(array, left, mid, right);
}
private void merge(int[] array, int left, int mid, int right) {
    // 准备归并 -> 将传过来的两个有序区间, 合并成一个有序区间
    int begin1 = left;
    int end1 = mid;
    int begin2 = mid + 1;
    int end2 = right;
    int[] tmp = new int[right - left + 1];
    int k = 0;
    while (begin1 <= end1 && begin2 <= end2) {
        if (array[begin1] < array[begin2]) {
            tmp[k++] = array[begin1++];
        } else {
            tmp[k++] = array[begin2++];
        }
    }
    // 跳出循环之后, begin1 和 begin2 区间总有一个区间还有剩余的元素未拷贝进tmp
    while (begin1 <= end1) {
        tmp[k++] = array[begin1++];
    }
    while (begin2 <= end2) {
        tmp[k++] = array[begin2++];
    }
    // 将tmp的数据拷回array中
    for (int i = 0; i < k; i++) {
        array[i + left] = tmp[i];
    }
}
  • 时间复杂度分析:O(n*logn)
  • 空间复杂度分析:最多会开辟数组长度个空间即 O(n)
  • 稳定性:稳定
  • 堆排序主要用于外排序

下期预告:【Java 数据结构】二叉搜索树

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

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

相关文章

【Nginx】静态资源部署(上)

文章目录Nginx静态资源概述Nginx静态资源的配置指令listen指令server_name指令匹配执行顺序location指令设置请求资源的目录root / aliasindex指令error_page指令静态资源优化配置语法Nginx静态资源压缩Gzip模块配置指令Gzip压缩功能的实例配置Gzip和sendfile共存问题gzip_stat…

116.(leaflet之家)leaflet空间判断-点与多边形的空间关系

听老人家说:多看美女会长寿 地图之家总目录(订阅之前建议先查看该博客) 文章末尾处提供保证可运行完整代码包,运行如有问题,可“私信”博主。 效果如下所示: 下面献上完整代码,代码重要位置会做相应解释 <!DOCTYPE html> <html>

电脑怎么录屏?这三个简单的电脑录屏方法,不要错过

在现在的社会&#xff0c;电脑已经是我们日常工作中不可或缺的一部分。我们通常都要在电脑进行各种各样的操作&#xff0c;所以在向其他人展示电脑操作的时候&#xff0c;我们通常都会把整个操作过程录屏下来。那电脑怎么录屏&#xff0c;今天小编就给大家分享三个简单的电脑录…

分布式数据库中间件Sharding-JDBC介绍

前文中介绍了分布式数据库中间件Mycat的一些特性&#xff0c;作为对比本文简要介绍Sharding-JDBC的一些特性以及分片的实现原理&#xff0c;进行对比分析以了解。 1、ShardingSphere介绍 ShardingSphere是一套开源的分布式数据库中间件解决方案&#xff0c;目前由Sharding-JDB…

15个有用的JavaScript技巧

今天这篇文章&#xff0c;是我从网络上整理的一些常见的 JavaScript Tips。我在我的项目中使用了所有这些实用技巧&#xff0c;今天我想把它们分享给你&#xff0c;希望也能够帮助到你。 1.数字分隔符 为了提高数字的可读性&#xff0c;可以使用下划线作为分隔符。 const la…

pytorch学习记录

pytorch学习记录0.引言1.numpy 简单入门1.1.常用基础1.2.numpy其他学习2.pytorch 简单入门2.1.Pytorch的基本组成元素2.2.Pytorch构建神经网络2.3.进阶部分2.4.其他参考资料0.引言 一步一步积累吧。 一篇不错的博客 1.numpy 简单入门 1.1.常用基础 numpy中文手册 numpy官方…

【李宏毅】HW12

HW12一、作业描述1、Policy Gradient2、Actor-Critic二、实验1、simple2、medium3、strong三、代码一、作业描述 在这个HW中&#xff0c;你可以自己实现一些深度强化学习方法&#xff1a; 1、策略梯度Policy Gradient 2、Actor-Critic 这个HW的环境是OpenAI gym的月球着陆器。…

ubuntu | 重装ubuntu(VMware虚拟机环境)

前言 最近又把双系统的ubuntu搞崩了&#xff0c;在grub界面进不去&#xff0c;估计找不到启动项&#xff0c;被迫重装。 这次觉得还是windows系统好使&#xff0c;就用vmware虚拟机了。 一、安装vmware虚拟机 参考我之前的博客。 二、安装ubuntu 下载ubuntu18.04的iso文件…

15.2 浏览器中的进程

浏览器中的进程 start 上一篇文章&#xff0c;学习到了如何区分进程和线程。在这里再复习一下&#xff0c;进程类似于一个工厂&#xff0c;线程类似于工厂的工人&#xff0c;一个工厂可以有一个或多个工人。 1. 浏览器的进程 一个浏览器中有很多的进程&#xff0c;我以谷歌…

2023年的摸鱼小技巧:Python自动生成请假条【思路详解】

嗨害大家好鸭&#xff01;我是小熊猫~ 这不2023到了&#xff0c;新的一年&#xff0c;新的摸鱼 在办公室里的小透明来实现用Python来批量生成请假条&#xff0c; 这波啊&#xff0c;这波是智取&#xff01;&#xff01;&#xff01; Python资料电子书:点击此处跳转文末名片获…

Excel函数公式大全—SUMIF/SUMIFS函数

EXCEL系列文章目录 Excel系列文章是本人亲身经历职场之后萌发的想法&#xff0c;为什么Excel覆盖如此之广&#xff0c;几乎每个公司、学校、家庭都在使用&#xff0c;但是它深藏的宝藏功能却很少被人使用&#xff0c;PQ、BI这些功能同样适用于数据分析&#xff1b;并且在一些需…

内部成员之五:内部类

文章目录一、内部类分类&#xff1a;成员内部类vs局部内部类成员内部类实例化成员内部类每日一考一、内部类 1、Java中允许将类A声明在类B中&#xff0c;则类A就是内部类&#xff0c;类B为外部类。 分类&#xff1a;成员内部类vs局部内部类 成员内部类&#xff08;静态、非静…

【ElasticSearch7.X】学习笔记(三)

【ElasticSearch7.X】学习笔记五、集群部署5.1、相关概念5.1.1、集群 Cluster3.1.2、节点 Node5.2、下载安装5.3、 启动5.4、 测试六、进阶6.1、核心概念6.1.1、索引&#xff08;Index&#xff09;6.1.2、类型&#xff08;Type&#xff09;6.1.3、文档&#xff08;Document&…

Handler的学习

一、Handler到底有什么用呢&#xff1f; 首先看以下案例&#xff1a; public class MainActivity extends AppCompatActivity { Button btn;Overrideprotected void onCreate(Bundle savedInstanceState) {super.onCreate(savedInstanceState);setContentView(R.layout.activi…

vue这些原理你都知道吗?(面试版)

前言 在之前面试的时候我自己也经常会遇到一些vue原理的问题, 我也总结了下自己的经常的用到的,方便自己学习,今天也给大家分享出来, 欢迎大家一起学习交流, 有更好的方法欢迎评论区指出, 后序我也将持续整理总结~ 描述 Vue 与 React 区别 说明概念: vue:是一套用于构建用户…

虚拟化技术学习笔记3

1、KVM虚拟机管理工具部署 学习目标&#xff1a; 能够为KVM虚拟机管理工具部署准备环境 能够部署KVM虚拟机管理工具 1、KVM系统需求&#xff1a; 1&#xff09;Host system requirements: 1核心、2G内存、6G硬盘 2&#xff09;KVM hypervisor requirements&#xff1a; l…

U3D客户端框架(资源管理篇)之主资源加载器模块设计实现

一、主资源加载器模块设计实现 作用 主资源加载器是为面向用户而设计的上层应用层类&#xff0c;用户使用主资源加载器作为文件加载器加载文件&#xff0c; 加载器可以通过Assetbundle方式加载&#xff0c;Http方式加载资源。 UML类图设计 前置模块 主资源加载器需要引用到一…

Qt扫盲-QHash理论总结

QHash理论总结一、概述二、使用1. 添加 元素2. 获取元素3. 遍历元素4. 删除元素5. qHash()的散列函数6.算法复杂性一、概述 QHash是Qt的通用容器类之一。它和QMap一样是用来存储(键&#xff0c;值)对的工具&#xff0c;并提供快速查找与键相关联的值的功能。 QHash提供了与QMa…

密码学的一些常识01

序 作为一个小白&#xff0c;对称加密&#xff0c;非对称加密&#xff0c;数字签名&#xff0c;数字证书&#xff0c;CA&#xff0c;diff-helman&#xff0c;DES,AED,RSA……这些都不会。所以&#xff0c;百度启动&#xff0c;先初步了解。 实际应用 我是个小白……0基础的&…

sklearn中精确率、召回率及F1值得micro,macro及weighted算法

为什么要用精确率和召回率 有这样一个训练集&#xff0c;1000个人参加了结直肠癌CRC的检测&#xff0c;实际有0.5%的人得了CRC&#xff08;即5个人&#xff09;。用神经网络算法得到检测这样一个训练集能达到99%的准确率。从数值上判断该算法是不错的&#xff0c;因为只有1%的误…