Java数据结构(十)——冒泡排序、快速排序

文章目录

  • 冒泡排序
    • 算法介绍
    • 代码实现
    • 优化策略
    • 复杂度和稳定性
  • 快速排序
    • 算法介绍
    • 优化策略
    • 非递归实现
    • 代码演示
    • 复杂度和稳定性

冒泡排序

算法介绍

冒泡排序是一种简单的排序算法。它重复地遍历要排序的数列,一次比较两个元素,如果它们的顺序错误就交换。遍历数列的工作是重复地进行直到没有再需要交换,也就是说该数列已经排序完成。这个算法的名字由来是因为越小的元素会经由交换慢慢“浮”到数列的顶端,就像冒泡一样。

冒泡排序算法的步骤如下(排升序):

  1. 比较相邻的元素。如果第一个比第二个大,就交换它们两个;
  2. 对每一对相邻元素做同样的工作,从开始第一对到结尾的最后一对。这步做完后,最后的元素会是最大的数;
  3. 针对所有的元素重复以上的步骤,除了最后每轮筛选出来的最大的值;
  4. 持续每次对越来越少的元素重复上面的步骤,直到没有任何一对数字需要比较。

【图示】

在这里插入图片描述


代码实现

    public void bubbleSort(int[] array) {int len = array.length;while(len > 0) {len--;for(int i = 0; i < len; i++) {if(array[i] > array[i+1]) {int tmp = array[i];array[i] = array[i+1];array[i+1] = tmp;}}}}

优化策略

上代码存在一个问题可以优化,这个问题是:当某次遍历交换完后序列有序,排序不会停止,而是继续走完所有的趟数。对此可以优化,使得在某趟遍历没有任何交换的情况下(已经有序),不再开始下一趟,直接跳出排序,代码如下:

    public void bubbleSort() {int len = array.length;boolean flag = true;while(len > 0 && flag) {len--;flag = false;for(int i = 0; i < len; i++) {if(array[i] > array[i+1]) {flag = true;int tmp = array[i];array[i] = array[i+1];array[i+1] = tmp;}}}}

优化代码中通过设置了一个boolean类型的变量,保证了当序列有序,能够及时结束排序。


复杂度和稳定性

时间复杂度O(N^2)

优化后的冒泡排序在第一次遍历且没有交换发生时就会停止,因此某些时候时间复杂度可以从O(N^2)降低到O(N)。虽然优化后的冒泡排序在最好情况下有显著改进,但在最坏情况和平均情况下的时间复杂度并没有改变,仍为O(N^2)

空间复杂度O(1)

稳定性:稳定,通过控制交换的条件(相等时不交换)可以达到稳定。


快速排序

算法介绍

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

快速排序使用 分治法策略 来把一个序列分为较小和较大的两个子序列,然后递归地排序两个子序列。

(排升序)将序列划分为两个序列需要确定一个基准值,当划分结束后,基准值左边元素均小于该基准值,右边元素均大于该基准值。 实现这种划分有三种方式,分别是:挖坑法Hoare法前后指针法

【挖坑法】

首先确定一个基准值,通常会选择序列最左边的元素,然后将基准值存放在key变量中(通常将存放基准值的变量命名为key),此时将基准值所在位置视为“坑”。需要其他元素来“填坑”,由于基准值是序列的最左边的元素,所以我们要从序列的最右端开始,向前寻找第一个比基准值小的元素作为填坑元素填到坑位,之后,坑位更新到填坑元素的位置,由于填坑元素是从右边开始寻找的,所以我们要从左边开始寻找第一个比基准值大的元素作为填坑元素填坑,更新坑位到填坑元素的位置,继续第二轮,接着上次右边的位置向左寻找第一个比基准值小的元素填坑……以此类推,当向右寻找和向左寻找的两个位置重合,结束循环,重合位置就是基准值应所在的位置,此时基准值左边的所有元素都小于基准值,基准值右边的所有元素都大于基准值,即确定了基准值的最终位置。

图示:

在这里插入图片描述

关键代码实现

        int key = array[0];//存放基准值int pivot = 0;//坑位(下标值)int left = 0;//左“指针”(下标值)int right = array.length - 1;//右“指针”(下标值)while(left < right) {while(left < right && array[right] >= key) {right--;}array[pivot] = array[right];pivot = right;while(left < right && array[left] <= key) {left++;}array[pivot] = array[left];pivot = left;}array[pivot] = key;
  • 注意问题
    1. 左右“指针”寻找填坑元素时,要注意判断left < right,避免寻找过程中超出寻找范围
    2. 当 right 向前(left 向后)寻找比 key 值小(大)的元素时,对于遇到与 key 值相同值得处理,应该不是目标值,要跳过继续寻找。

【Hoare法】

首先选定一个基准值(通常最左边),创建两个“指针”,分别指向最左边和最右边,右指针从右向左移动时,它会寻找第一个小于基准值的元素;左指针开始从左向右移动,寻找第一个大于基准值的元素,找到后,进行交换;重复寻找并交换的过程,直到左右“指针”相遇;最后,将基准值与相遇的位置的元素交换。

在这里插入图片描述

关键代码实现

        int key = array[0];int left = 0;int right = array.length - 1;while(left < right) {while(left < right && array[right] >= key) {right--;}while(left < right && array[left] <= key) {left++;}swap(array, left, right);}swap(array, 0, left);
  • 注意问题
    1. 先动右指针,再动左指针
    2. 指针寻找时找到与基准值一样的值,跳过继续寻找,即取等号

【前后指针法】

选定最左边元素为基准值,创建两个指针,这里命名为prevcur,初始分别指向基准值位置和基准值下一个位置,cur负责向后寻找比基准值小的元素,找到了就让prev++然后交换两个指针指向的位置的元素,然后继续寻找交换,直到cur指针越界,最后,将prev指向的位置的元素与基准值元素交换。

在这里插入图片描述

关键代码实现

        int key = array[0];int cur = 1;int prev = 0;while(cur < array.length) {if(array[cur] < key && array[++prev] != array[cur]) {swap(array, cur, prev);}cur++;}swap(array, 0, prev);

演示三种划分方法使用的元素序列是一致的,但最终划分的结果可能不同:

  • 挖坑法:[15, 29, 29, 34, 87, 45, 63, 56, 78, 71]
  • Hoare法:[29, 15, 29, 34, 87, 45, 63, 56, 78, 71]
  • 前后指针法:[15, 29, 29, 34, 87, 45, 63, 56, 78, 71]

但即使划分的结果不同,最终都能满足基准值的位置是正确的,且其左边元素均比它小,右边的元素均比它大


三种划分方式的使用频率和重要程度即介绍的顺序:挖坑法 > Hoare法 > 前后指针法。

例如,有些快速排序的选择题会考察第n趟的结果,题目一般都是以挖坑法划分方式的,但如果利用挖坑法得到的结果不在选项中,则依次按照Hoare法和前后指针法尝试。


了解了划分方法后实际上只解决了快速排序的一步,每次经过划分后,都会有一个元素的最终位置确定,并且将序列被分为两个子序列,快速排序要做的是继续对左右子序列进行相同的划分,直到序列有序。可以以递归的方式实现:

【图示】

以挖坑法演示:

在这里插入图片描述


完整实现

    public void quickSort(int[] array, int begin, int end) {if(begin >= end) {return;}int key = array[begin];int left = begin;int right = end;int pivot = begin;while(left < right) {while(left < right && array[right] >= key) {right--;}array[pivot] = array[right];pivot = right;while(left < right && array[left] <= key) {left++;}array[pivot] = array[left];pivot = left;}array[pivot] = key;//递归地划分quickSort(array, begin, pivot - 1);quickSort(array, pivot + 1, end);}

我们假设每次划分后确定的基准值元素的最终位置都在序列的中间位置,于是通过下面的简单图可以理解快速排序的时间复杂度:

在这里插入图片描述

快速排序的平均时间复杂度是O(N*log2N),但这建立在 每次划分后确定的基准值元素的最终位置都在序列的中间位置 的前提下,如果出现极端情况每次每次划分后确定的基准值元素的最终位置都在序列的两侧(即序列有序),此时就会出现快速排序的最坏时间复杂度:O(N^2)的情况。


优化策略

【三数取中】

当序列有序时,快速排序的时间复杂度会达到O(N^2)级别,这是因为有序序列每次拿到的基准值都是序列的最值元素(最大值或最小值),这时候指针就得遍历整个序列。

为此,可以通过三数取中的方式来避免每次确定基准值时选到最值元素,三数取中的原理是:取出序列中位置最左、最右和中间的三个元素,比较它们的大小,选出第二大(即中间大)的元素,然后将它换到基准值位置(通常是最左边),这样取基准值时,就不会出现选到最值的极端情况了

以下是三数取中以及三数取中优化后的快速排序:

    private void swap(int[] array, int a, int b) {int tmp = array[a];array[a] = array[b];array[b] = tmp;}//三数取中private int getMidIndex(int[] array, int left, int right) {int mid = left + (right - left >> 1);if(array[left] > array[right]) {if(array[left] > array[mid]) {return array[mid] > array[right] ? mid : right;}else {return left;}}else {if(array[right] > array[mid]) {return array[mid] > array[left] ? mid : left;}else {return right;}}}//三数取中后的快速排序public void quickSort(int[] array, int begin, int end) {if(begin >= end) {return;}//取得中间大的元素的下标int midIndex = getMidIndex(array, begin, end);//交换,避免出现极端情况swap(array, begin, midIndex);int key = array[begin];int left = begin;int right = end;int pivot = begin;while(left < right) {while(left < right && array[right] >= key) {right--;}array[pivot] = array[right];pivot = right;while(left < right && array[left] <= key) {left++;}array[pivot] = array[left];pivot = left;}array[pivot] = key;//递归地划分quickSort(begin, pivot - 1);quickSort(pivot + 1, end);}

【小区间优化】

对快速排序进行了三数取中优化后,实际上还能继续优化。

小区间优化针对当待排序的数组的数据量较少时,采用其他排序方法(通常是直接插入排序)对小数组排序。因为对大量小数组进行递归调用和分区操作会产生较大的开销。

小区间优化需要确定一个阈值,这个阈值决定了小数组的长度到达多少时进行直接插入排序,阈值的确定与数据量有很大的关系,不过常见的阈值有8,10,15

小区间优化后的快速排序(采用10为阈值,仅作为代码演示):

    private void swap(int[] array, int a, int b) {int tmp = array[a];array[a] = array[b];array[b] = tmp;}private int getMidIndex(int left, int right) {int mid = left + (right - left >> 1);if(array[left] > array[right]) {if(array[left] > array[mid]) {return array[mid] > array[right] ? mid : right;}else {return left;}}else {if(array[right] > array[mid]) {return array[mid] > array[left] ? mid : left;}else {return right;}}}//小区间优化的直接插入排序代码public void insertSort(int[] array, int begin, int end) {for(int i = begin + 1; i <= end; i++) {int tmp = array[i];int pos = i - 1;for(int j = i - 1; j >= begin; j--) {if(array[j] > tmp) {array[j+1] = array[j];pos--;}else {break;}}array[pos+1] = tmp;}}public void quickSort(int begin, int end) {if(begin >= end) {return;}int midIndex = getMidIndex(begin, end);swap(array, begin, midIndex);int key = array[begin];int left = begin;int right = end;int pivot = begin;while(left < right) {while(left < right && array[right] >= key) {right--;}array[pivot] = array[right];pivot = right;while(left < right && array[left] <= key) {left++;}array[pivot] = array[left];pivot = left;}array[pivot] = key;//小区间优化if(pivot - 1 - begin <= 10) {insertSort(array, begin, pivot - 1);}else {quickSort(begin, pivot - 1);}if(end - pivot - 1 <= 10) {insertSort(array, pivot + 1, end);}else {quickSort(pivot + 1, end);}}

非递归实现

非递归实现需要用到,栈用来存储序列的左右边界下标,需要注意的是:

  • 基于栈的“先入后出”特性,为了能先划分左子序列,我们必须先将右子序列的两个边界下标入栈,再将左子序列的两个边界下标入栈,以确保出栈时先拿到的是左子序列的边界下标
  • 向栈中存放某个序列的两个边界下标时,也要注意栈的特性,比如,接下来的示例代码中,我们选择先将右边界下标入栈,再将左边界下标入栈
    public void quickSortNoR(int[] array) {//创建一个栈,并初始化栈的元素Stack<Integer> stack = new Stack<>();stack.push(array.length - 1);stack.push(0);//快速排序的核心代码while(!stack.isEmpty()) {//出栈,注意出栈元素的含义,要结合入栈顺序int begin = stack.pop();int end = stack.pop();int key = array[begin];int left = begin;int right = end;int pivot = begin;while(left < right) {while(left < right && array[right] >= key) {right--;}array[pivot] = array[right];pivot = right;while(left < right && array[left] <= key) {left++;}array[pivot] = array[left];pivot = left;}array[pivot] = key;//入栈,入栈规则:先入右子序列的两个边界,再入左子序列的两个边界if(end - pivot - 1 > 1) {stack.push(end);stack.push(pivot + 1);}if(pivot - 1 - begin > 1) {stack.push(pivot - 1);stack.push(begin);}}}

演示代码并没有采用三数取中和小区间优化,读者可以自行尝试,也比较简单,理解了非递归代码后在合适的位置插入优化代码即可。


代码演示

方便起见,将代码再次展示:

递归实现(三数取中和小区间优化)

    public void quickSort(int begin, int end) {if(begin >= end) {return;}int midIndex = getMidIndex(begin, end);swap(array, begin, midIndex);int key = array[begin];int left = begin;int right = end;int pivot = begin;while(left < right) {while(left < right && array[right] >= key) {right--;}array[pivot] = array[right];pivot = right;while(left < right && array[left] <= key) {left++;}array[pivot] = array[left];pivot = left;}array[pivot] = key;//小区间优化if(pivot - 1 - begin <= 10) {insertSort(array, begin, pivot - 1);}else {quickSort(begin, pivot - 1);}if(end - pivot - 1 <= 10) {insertSort(array, pivot + 1, end);}else {quickSort(pivot + 1, end);}}private void swap(int[] array, int a, int b) {int tmp = array[a];array[a] = array[b];array[b] = tmp;}private int getMidIndex(int left, int right) {int mid = left + (right - left >> 1);if(array[left] > array[right]) {if(array[left] > array[mid]) {return array[mid] > array[right] ? mid : right;}else {return left;}}else {if(array[right] > array[mid]) {return array[mid] > array[left] ? mid : left;}else {return right;}}}//小区间优化的直接插入排序代码public void insertSort(int[] array, int begin, int end) {for(int i = begin + 1; i <= end; i++) {int tmp = array[i];int pos = i - 1;for(int j = i - 1; j >= begin; j--) {if(array[j] > tmp) {array[j+1] = array[j];pos--;}else {break;}}array[pos+1] = tmp;}}

非递归实现

    public void quickSortNoR(int[] array) {Stack<Integer> stack = new Stack<>();stack.push(array.length - 1);stack.push(0);while(!stack.isEmpty()) {int begin = stack.pop();int end = stack.pop();int key = array[begin];int left = begin;int right = end;int pivot = begin;while(left < right) {while(left < right && array[right] >= key) {right--;}array[pivot] = array[right];pivot = right;while(left < right && array[left] <= key) {left++;}array[pivot] = array[left];pivot = left;}array[pivot] = key;if(end - pivot - 1 > 1) {stack.push(end);stack.push(pivot + 1);}if(pivot - 1 - begin > 1) {stack.push(pivot - 1);stack.push(begin);}}}

复杂度和稳定性

时间复杂度O(N*log2N)

尽管快速排序的最坏时间复杂度为:O(N^2),但快速排序的时间复杂度为O(N*log2N)是约定俗成的,我们可以通过例如三数取中来避免最坏情况的发生。

空间复杂度O(log2N)~O(N)

  • O(log2N)主要是递归调用栈所占据的空间。在平均情况下,递归深度接近O(log2N)
  • O(N):最坏空间复杂度,当数组已经接近有序或完全逆序时,递归深度达到n,因此递归调用栈占据的空间也达到n。

稳定性:不稳定


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

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

多线程篇(其它容器- CopyOnWriteArrayList)(持续更新迭代)

一、CopyOnWriteArrayList&#xff08;一&#xff09; 1. 简介 并发包中的并发List只有CopyOnWriteArrayList。 CopyOnWriteArrayList是一个线程安全的ArrayList&#xff0c;对其进行的修改操作都是在底层的一个复制的数 组&#xff08;快照&#xff09;上进行的&#xff0…

redis 基本数据类型—string类型

一、介绍 Redis 中的字符串&#xff0c;直接就是按照二进制数据的方式存储的&#xff0c;不会做任何的编码转换。 Redis对于 string 类型&#xff0c;限制了大小最大是512M 二、命令 SET 将 string 类型的 value 设置到 key 中。如果 key 之前存在&#xff0c;则覆盖&#…

Jwt、Filter、Interceptor

目录 JWT(Json Web Token) jwt令牌 组成 应用场景 生成令牌 解析令牌 登录实例 Filter过滤器 Filter Filter登录校验 Interceptor拦截器 Interceptor 拦截路径 执行流程 登录实例 JWT(Json Web Token) jwt令牌 定义了一种简洁的、自包含的格式&#xff0c;…

二、(JS)JS中常见的键盘事件

一、常见的键盘事件 onkeydown 某个键盘按键被按下onkeypress 某个键盘按键被按下onkeyup 某个键盘按键被松开 二、事件的执行顺序 onkeydown、onkeypress、onkeyup down 事件先发生&#xff1b;press 发生在文本被输入&#xff1b;up …

【大模型理论篇】大模型周边自然语言处理技术(NLP)原理分析及数学推导(Word2Vec、TextCNN、Gated TextCNN、FastText)

1. 背景介绍 进入到大模型时代&#xff0c;似乎宣告了与过去自然语言处理技术的结束&#xff0c;但其实这两者并不矛盾。大模型时代&#xff0c;原有的自然语言处理技术&#xff0c;依然可以在大模型的诸多场景中应用&#xff0c;特别是对数据的预处理阶段。本篇主要关注TextCN…

使用Python生成多种不同类型的Excel图表

目录 一、使用工具 二、生成Excel图表的基本步骤 三、使用Python创建Excel图表 柱形图饼图折线图条形图散点图面积图组合图瀑布图树形图箱线图旭日图漏斗图直方图不使用工作表数据生成图表 四、总结 Excel图表是数据可视化的重要工具&#xff0c;它通过直观的方式将数字信…

PCIe进阶之TL:First/Last DW Byte Enables Rules Traffic Class Field

1 First/Last DW Byte Enables Rules & Attributes Field 1.1 First/Last DW Byte Enables Rules Byte Enable 包含在 Memory、I/O 和 Configuration Request 中。本文定义了相应的规则。Byte Enable 位于 header 的 byte 7 。对于 TH 字段值为 1 的 Memory Read Request…

【STM32】esp8266通过MQTT连接服务器|订阅发布

1. MQTT协议 该协议为应用层协议&#xff0c;传输层使用的是tcp,MQTT的订阅和发布&#xff0c;就相当于在抖音中你关注了某个领域的博主&#xff08;订阅&#xff09;&#xff0c;如果有其他人发了作品就会推给你&#xff08;发布&#xff09;&#xff0c;默认已经安装好了 简…

哈希表、算法

哈希表 hash&#xff1a; 在编程和数据结构中&#xff0c;"hash" 通常指的是哈希函数&#xff0c;它是一种算法&#xff0c;用于将数据&#xff08;通常是字符 串&#xff09;映射到一个固定大小的数字&#xff08;哈希值&#xff09;。哈希函数在哈希表中尤为重要…

探索图论中的关键算法(Java 实现)

“日出东海落西山 愁也一天 喜也一天 遇事不钻牛角尖” 文章目录 前言文章有误敬请斧正 不胜感恩&#xff01;||Day031. 最短路径算法Dijkstra算法Java 实现&#xff1a; Bellman-Ford算法Java 实现&#xff1a; 2. 最小生成树算法Prim算法Java 实现&#xff1a; Kruskal算法Ja…

C++速通LeetCode简单第9题-二叉树的最大深度

深度优先算法递归&#xff1a; /*** Definition for a binary tree node.* struct TreeNode {* int val;* TreeNode *left;* TreeNode *right;* TreeNode() : val(0), left(nullptr), right(nullptr) {}* TreeNode(int x) : val(x), left(nullptr), right…

Conmi的正确答案——MySQL的层级递归查询(递归公共表表达式,CTE)

数据库&#xff1a;oceanbase-ce 递归sql主体&#xff1a; WITH RECURSIVE country_area_tree AS (-- 非递归部分&#xff0c;初始化查询SELECT id, area_name, parent_id, 0 AS levelFROM country_areaWHERE id 589004044419077UNION ALL-- 递归部分&#xff0c;找到子节点S…

聚类_K均值

import numpy as np import matplotlib.pyplot as plt from sklearn.datasets import make_blobs1.数据预处理 #创建基于高斯分布的样本点, x是点的坐标&#xff0c;y是所属聚类值 x, y make_blobs(n_samples100, centers6, random_state100, cluster_std0.6) # 设置图形尺寸…

2. 变量和指令(omron 机器自动化控制器)——1

机器自动化控制器——第二章 变量和指令 1 2-1 变量一览表MC通用变量轴变量▶ 轴组变量 运动控制指令的输入变量输入变量的有效范围▶ 枚举体一览表 运动控制指令的输出变量运动控制指令的输入输出变量 2-1 变量一览表 MC功能模块使用的变量分为两类。 一类是监视轴等的状态及…

电脑提示丢失mfc140u.dll的详细解决方案,mfc140u.dll文件是什么

遇到电脑显示“缺少 mfc140u.dll 文件”的错误其实是比较常见的。这种提示通常表示某个应用程序在尝试运行时未能找到它所需的关键 DLL 文件&#xff0c;导致无法正常启动。不过&#xff0c;别担心&#xff0c;本文将一步步引导你通过几种不同的方法来解决这个问题&#xff0c;…

VSCode拉取远程项目

天行健&#xff0c;君子以自强不息&#xff1b;地势坤&#xff0c;君子以厚德载物。 每个人都有惰性&#xff0c;但不断学习是好好生活的根本&#xff0c;共勉&#xff01; 文章均为学习整理笔记&#xff0c;分享记录为主&#xff0c;如有错误请指正&#xff0c;共同学习进步。…

el-input设置后缀显示单位并阻止滚轮微调

项目中收集form表单信息时&#xff0c;有时会需要在el-input后面显示单位&#xff0c;效果如图&#xff1a; 当然&#xff0c;我们可以直接在输入框后面加上单位&#xff0c;但直接给输入框上加单位不管是视图上还是用户体验上看起来都要好一点 element-plus / element-ui给我…

[基于 Vue CLI 5 + Vue 3 + Ant Design Vue 4 搭建项目] 01 安装 nodejs 环境

文章目录 下载安装测试 这里让我们去看看如何安装一下 nodejs 的环境 下载 通过官网进行下载安装包 官网 https://nodejs.org/zh-cn点击 下载 Node.js (LTS) 开始下载 安装 下载完成之后&#xff0c;双击进行安装 开始进行安装了 这样&#xff0c;node.js 就安装好了 测试 …

unity3d入门教程三

unity3d入门教程三 8.1游戏脚本8.2脚本的使用8.3认识脚本组件8.4帧率9.1游戏脚本9.2获取节点和组件9.3MonoBehaviour9.4父节点与子节点9.5组件的属性9.6脚本的单步调试 8.1游戏脚本 通过程序控制对象属性&#xff08;如运动&#xff0c;修改transform的位置属性&#xff09; …

SpringCloudAlibaba:Seata

1. 面试题 2.1 你简历上写用微服务boot/cloud做过项目&#xff0c;你不可能只有一个数据库吧&#xff1f;谈谈多个数据库之间如何处理分布式事务&#xff1f; 2.2 阿里巴巴的Seata-AT模式如何做到对业务的无侵入&#xff1f; 在一阶段&#xff0c;Seata 会拦截“业务 SQL”&a…