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

news/2024/11/28 10:46:18/


目录

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 区间总有一个区间还有剩余的元素未拷贝进tmpwhile (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.ppmy.cn/news/9342.html

相关文章

TiDB分布式数据库架构介绍

简介 TiDB 是 PingCAP 公司自主设计、研发的开源分布式关系型数据库&#xff0c;是一款同时支持在线事务处理与在线分析处理 (Hybrid Transactional and Analytical Processing, HTAP) 的融合型分布式数据库产品&#xff0c;具备水平扩容或者缩容、金融级高可用、实时 HTAP、云…

剑指offer----C语言版----第八天

目录 1. 矩阵中的路径 1.1 题目描述 1.2 基础知识 1.3 思路分析 1.4 小试牛刀 1. 矩阵中的路径 原题链接&#xff1a; 剑指 Offer 12. 矩阵中的路径 - 力扣&#xff08;LeetCode&#xff09;https://leetcode.cn/problems/ju-zhen-zhong-de-lu-jing-lcof/submissions/ 1.1 题…

HTTP慢速攻击详解

今天继续给大家介绍渗透测试相关知识&#xff0c;本文主要内容是HTTP慢速攻击详解。 免责声明&#xff1a; 本文所介绍的内容仅做学习交流使用&#xff0c;严禁利用文中技术进行非法行为&#xff0c;否则造成一切严重后果自负&#xff01; 再次强调&#xff1a;严禁对未授权设备…

SpringSecurity+JWT快速入门

文章目录1 简介1.1 认证和授权1.2 spring security的核心过滤器链1.3 入门案例认证流程1.4 自定义SpringSecurity认证、授权、校验1.4.1 登录1.4.2 校验2 快速入门2.1 pom引入依赖2.2 创建domain包2.2.1 新建AjaxResult2.2.2 新建User类2.3 创建common包2.3.1 创建HttpStatus类…

[NOIP 2003] 栈(三种方法:DP、数论、搜索)

[NOIP2003 普及组] 栈 题目背景 栈是计算机中经典的数据结构&#xff0c;简单的说&#xff0c;栈就是限制在一端进行插入删除操作的线性表。 栈有两种最重要的操作&#xff0c;即 pop&#xff08;从栈顶弹出一个元素&#xff09;和 push&#xff08;将一个元素进栈&#xff…

codeforces 1770B. Koxia and Permutation

B. Koxia and Permutation Reve has two integers n and k. Let p be a permutation† of length n. Let c be an array of length n−k1 such that cimax(pi,…,pik−1)min(pi,…,pik−1). Let the cost of the permutation p be the maximum element of c. Koxia wants you…

LaoCat带你认识容器与镜像(二【二章】)

二章三小节&#xff0c;欧力给~ 本章内容 Docker仓库相关。 本文实操全部基于Ubuntu 20.04 理解Docker仓库&#xff0c;那就需要读者有一定的代码仓库的知识理论&#xff0c;像我们平时工作中经常会将代码存放于代码仓库中一样Docker仓库的作用也是将"代码&#xff08;镜…

RabbitMQ 单机安装-CentOS

RabbitMQ 单机安装-CentOS 官网查看RabbitMQ和对应的Erlang版本 进入 RabbitMQ 官网 &#xff0c;点击 顶上的 Get Started 点击Download Installation 点击左侧的Erlang Versions 查看对应版本 根据自己需要安装的RabbitMQ版本&#xff0c;找到需要Erlang的版本。 下…