Abel's tech blogAbel's tech blog
  • 基础知识
  • 面向对象
  • IO流
  • String
  • 异常处理机制
  • 多线程
  • 反射机制
  • JVM相关知识
  • 数据库基础
  • 数据库进阶
  • 复杂SQL语句
  • Redis
  • Spring-IOC
  • Spring-AOP
  • Spring-Test
  • SpringBoot
  • SpringMVC
  • MyBatis
  • 基于MyBatis的分页查询
  • SpringSecurity
  • 微服务概念
  • Nacos
  • Dubbo
  • Seata
  • Sentinel
  • SpringGateway网关
  • ELK
  • Quartz
  • 消息队列
  • 数据结构
  • 算法
  • TCP/IP
  • 交换机
  • 路由器
  • Docker
  • Kubernetes
  • Linux
  • 各类工具

    • 菜鸟工具
    • 菜鸟教程
    • IDEA下载
    • 数据结构和算法可视化网站
    • jwt解析
    • maven仓库
  • 开发文档

    • Java 8 API 文档
    • Java 17 API 文档
    • MyBatis 3 中文
    • MyBatis-spring 中文
    • Spring Framework 5 API DOC
    • Spring Framework 6 API DOC
    • SpringBoot 2.7.6 API DOC
    • SpringBoot 3 API DOC
    • Hypertext Transfer Protocol -- HTTP/1.0
  • 配置文件下载

    • 阿里云Maven仓库配置
    • Nginx反向代理配置模板
    • JavaScript组件库
  • JDK 8 Windows x86 64-bit
  • JDK 17 Windows x86 64-bit
  • Maven
  • IntelliJ IDEA 各版本
  • Git
  • 基础知识
  • 面向对象
  • IO流
  • String
  • 异常处理机制
  • 多线程
  • 反射机制
  • JVM相关知识
  • 数据库基础
  • 数据库进阶
  • 复杂SQL语句
  • Redis
  • Spring-IOC
  • Spring-AOP
  • Spring-Test
  • SpringBoot
  • SpringMVC
  • MyBatis
  • 基于MyBatis的分页查询
  • SpringSecurity
  • 微服务概念
  • Nacos
  • Dubbo
  • Seata
  • Sentinel
  • SpringGateway网关
  • ELK
  • Quartz
  • 消息队列
  • 数据结构
  • 算法
  • TCP/IP
  • 交换机
  • 路由器
  • Docker
  • Kubernetes
  • Linux
  • 各类工具

    • 菜鸟工具
    • 菜鸟教程
    • IDEA下载
    • 数据结构和算法可视化网站
    • jwt解析
    • maven仓库
  • 开发文档

    • Java 8 API 文档
    • Java 17 API 文档
    • MyBatis 3 中文
    • MyBatis-spring 中文
    • Spring Framework 5 API DOC
    • Spring Framework 6 API DOC
    • SpringBoot 2.7.6 API DOC
    • SpringBoot 3 API DOC
    • Hypertext Transfer Protocol -- HTTP/1.0
  • 配置文件下载

    • 阿里云Maven仓库配置
    • Nginx反向代理配置模板
    • JavaScript组件库
  • JDK 8 Windows x86 64-bit
  • JDK 17 Windows x86 64-bit
  • Maven
  • IntelliJ IDEA 各版本
  • Git

二分查找算法

定义

二分查找算法也称折半查找(Binary Search),它是一种效率较高的查找方法。但是,折半查找要求线性表必须采用顺序存储结构,而且表中元素按关键字有序排列

  • 前提要求:
    1. 线性表必须采用顺序存储结构 -- 数组满足
    2. 要求元素实现了排序

线性表

线性表是最基本、最简单、也是最常用的一种数据结构。一个线性表是n个具有相同特性的数据元素的有限序列。 线性表中数据元素之间的关系是一对一的关系,即除了第一个和最后一个数据元素之外,其它数据元素都是首尾相接的(注意,这句话只适用大部分线性表,而不是全部。比如,循环链表逻辑层次上也是一种线性表(存储层次上属于链式存储,但是把最后一个数据元素的尾指针指向了首位结点)

顺序存储结构

顺序存储结构是存储结构类型中的一种,该结构是把逻辑上相邻的结点存储在物理位置上相邻的存储单元中,顺序存储结构的主要优点是节省存储空间。结点之间的逻辑关系由存储单元的邻接关系来体现。

查找过程

首先,假设表中元素是按升序排列,将表中间位置记录的关键字与查找关键字比较
如果两者相等,则查找成功;
否则利用中间位置记录将表分成前、后两个子表,
如果中间位置记录的关键字大于查找关键字,则进一步查找前一子表,
否则进一步查找后一子表。
重复以上过程,直到找到满足条件的记录,使查找成功,或直到子表不存在为止,此时查找不成功。

二分查找

排序算法

冒泡排序

原理机制

相邻元素两两比较,大的(小的)往后排,一轮比较结束后,最大值(最小值)出现在最大下标处,会比较多轮

冒泡排序

代码实现

class BubbleSort {
    public static void main(String[] args) {
        int[] ary = {23, 12, 7, 0, 67, 9, 11};
        
        for (int i = 0; i < ary.length - 1; i++) {  //i表示轮数
            for (int j = 0; j < ary.length - 1 - i; j++) {  //j表示遍历元素的下标
                if (ary[j] > ary[j + 1]) {
                    int tmp = ary[j];
                    ary[j] = ary[j + 1];
                    ary[j + 1] = tmp;
                }
            }
        }
        System.out.println(Arrays.toString(ary));
    }
}

选择排序

定义

选择排序(Selection sort)是一种简单直观的排序算法。它的工作原理是:第一次从待排序的数据元素中选出最小(或最大)的一个元素,存放在序列的起始位置,然后再从剩余的未排序元素中寻找到最小(大)元素,然后放到已排序的序列的末尾。以此类推,直到全部待排序的数据元素的个数为零。选择排序是不稳定的排序方法。

排序规则

  • 排序规则解读:从第一位开始,为每一位求应该保存的最小值,除了最后一位 选择排序

代码实现

class SelectionSort {
    public static void main(String[] args) {
        for (int i = 0; i < arr.length - 1; i++) {
            for (int j = i + 1; j < arr.length; j++) {
                if (arr[i] > arr[j]) {
                    int temp = arr[i];
                    arr[i] = arr[j];
                    arr[j] = temp;
                }
                System.out.println(Arrays.toString(arr));
            }
            System.out.println();
        }
    }
}

稳定性

提示

选择排序是给每个位置选择当前元素最小的,比如给第一个位置选择最小的,在剩余元素里面给第二个元素选择第二小的,依次类推,直到第n-1个元素,第n个元素不用选择了,因为只剩下它一个最大的元素了。那么,在一趟选择,如果一个元素比当前元素小,而该小的元素又出现在一个和当前元素相等的元素后面,那么交换后稳定性就被破坏了。举个例子,序列5 8 5 2 9,我们知道第一遍选择第1个元素5会和2交换,那么原序列中两个5的相对前后顺序就被破坏了,所以选择排序是一个不稳定的排序算法。

稳定性: 排序后相同元素的相对前后位置是否发生改变;若改变,则该算法是不稳定的;若不改变,则该算法是稳定的.

冒泡排序是稳定的排序算法.

插入排序

定义

插入排序,一般也被称为直接插入排序。对于少量元素的排序,它是一个有效的算法 [1] 。插入排序是一种最简单的排序方法,它的基本思想是将一个记录插入到已经排好序的有序表中,从而一个新的、记录数增1的有序表。

思想

将序列分为排好序部分与未排序部分,对未排序部分的元素进行遍历,将遍历到的元素插入到排好序部分适当的位置去,则排好序部分元素数量增1,直到未排序部分的元素数量为0,则所有元素插入完成,即排序完成.

插入排序

代码实现

class InsertionSort {
    public static void main(String[] args) {
        for (int i = 0; i < arr.length; i++) {
            int j = i;
            while (j > 0) {
                if (arr[j] < arr[j - 1]) {
                    int tmp = arr[j];
                    arr[j] = arr[j - 1];
                    arr[j - 1] = tmp;
                    j--;
                    System.out.println(Arrays.toString(arr));
                } else {
                    break;
                }
            }
            System.out.println();
        }
    }
}


三种算法排序效率

数组容量相同情况下对比

  • 数组容量为50000

    • 冒泡排序:3693毫秒
    • 选择排序:1106毫秒
    • 插入排序:366毫秒
  • 数组容量为100000

    • 冒泡排序:14995毫秒
    • 选择排序:4138毫秒
    • 插入排序:1470毫秒
  • 数组容量为200000

    • 冒泡排序:60749毫秒
    • 选择排序:16143毫秒
    • 插入排序:5823毫秒

希尔排序

概念

希尔排序(Shell sort)的名称源自于它的发明者Donald Shell。

它通过相距一定间隔的元素来工作,各轮对比所有的距离随着算法的进行而减小,走到只比较相邻元素的最后一轮为止,所以,它也叫做缩减增量排序(Diminishing increment sort)。

从某种程度来看,它是插入排序的升级版。

原理

  • 假设有这样一个数组:int[] arr = {8,1,4,9,0,3,5,2,7,6}
  • 默认的增量是length/2,本例中数组长度是10,则默认的增量是5,那么,第一个元素将于第6个元素进行对比,其他个元素以此类推
    • 希尔排序并不要求增量的大小,除以2的做法是惯例
  • 以升序为例,如果左侧的“同组”数据较小,则需要换位,结果将是这样的{3,1,2,7,0,8,5,4,9,6}(较小的在左侧,较大的在右侧,整个数组依然是无序的)
  • 第1轮对比完成后,接下来将增量在原基础的基础上除以2,所以新的增量是5/2=2,即第1个和第3个、第5个、第7个、第9个成组对比,以此类推

对着两组元素进行插入排序(代码实现时,每一次的换位都是插入排序,所以,前一次的换位也是通过插入排序实现的),使得这两个部分各自是有序的:{0,1,2,4,3,6,5,7,9,8}

  • 接下来这一轮的增量是前次增量除以2,即2/2=1,即全部参与排序
    • 对于局部有序的数组,插入裴谞的效率很高,即使全部排序,也非常快
  • 当增量大于0室进行每一轮循环,由于上一轮的增量已经是1,而1/2=0,所以终止循环,排序结束。

特殊情况

如果根据增量确定需要对比的关系后,各组对应关系的数量不统一怎么办?

  • 通常是数组右侧的若干个元素
  • 希尔排序的“排序”过程一般是插入排序,它会先跳过增量值对应数量的元素,然后向右移动,并每次对比“当前下标-增量值”对应的下标元素。

实现步骤分析

  • 在增量值大于0之前进行多轮循环

      class ShellSort {
        public static void main(String[] args) {
          // 创建需要排序的数组对象
          int[] arr = {8, 1, 4, 9, 0, 3, 5, 2, 7, 6};
            
          // 多轮循环
          // 初始条件:增量(gap)值为数组长度除以2
          // 循环条件:增量>0
          // 条件自变:增量除以2
          for (int gap = arr.length / 2; gap > 0; gap /= 2) {
          }
        }
      }
    
  • 从与增量值大小相等的下标位置开始,向右循环

    class ShellSort {
      public static void main(String[] args) {
        // 创建需要排序的数组对象
        int[] arr = {8, 1, 4, 9, 0, 3, 5, 2, 7, 6};
            
        // 多轮循环
        for (int gap = arr.length / 2; gap > 0; gap /= 2) {
          // 从与增量值大小相等的下标位置开始,向右循环
          for(int i = gap; i < arr.length; i++) {
          }
        }
      }
    }
    
  • 由于循环后,左侧的“同组”元素可能有多个,人需要使用新的循环来处理

    class ShellSort {
      public static void main(String[] args) {
        // 创建需要排序的数组对象
        int[] arr = {8, 1, 4, 9, 0, 3, 5, 2, 7, 6};
            
        // 多轮循环
        for (int gap = arr.length / 2; gap > 0; gap /= 2) {
          // 准备判断大小及必要的换位,初始下标与增量值相等
          for(int i = gap; i < arr.length; i++) {
            // 接下来的循环表示向左找“同组”的元素尝试对比及必要的换位
            // j:左侧“同组”元素的下标,即被对比元素的下标,因为可能有多个,所以需要循环
            for(int j = i - gap; j >= 0; j++) {
                // 内部代码待定
            }
          }
        }
      }
    }
    
  • 处理对比

    class ShellSort {
      public static void main(String[] args) {
        // 在以上的第3个循环内部
        // 判断需对比的两个元素的大小,进行必要的换位
        // 注意:使用的下标分别是 j 和 j+gap,后者是因为 j 的值变化时需要跟随调整
        if (arr[j] > arr[j + gap]) {
          // 执行换位,此处省略换位代码
        } else {
          // 注意:此处必须添加break语句
          // 基于插入排序的特点,一但左侧元素更小,那么,更左侧的元素肯定也是更小的
          // 则,没有必要继续循环下去
          break;
        }
      }
    }
    

代码实现

public class ShellSort {
    public static void main(String[] args) {
        int[] arr = {8, 1, 4, 9, 0, 3, 5, 2, 7, 6};
        
        System.out.println(Arrays.toString(arr));
        System.out.println();
        
        for (int gap = arr.length / 2; gap > 0; gap /= 2) {
            for (int i = gap; i < arr.length; i++) {
                for (int j = i - gap; j >= 0; j -= gap) {
                    if (arr[j] > arr[j + gap]) {
                        int temp = arr[j];
                        arr[j] = arr[j + gap];
                        arr[j + gap] = temp;
                        // 输出数组,观察每次交换后的数组
                        System.out.println(Arrays.toString(arr));
                    } else {
                        break;
                    }
                }
            }
            System.out.println();
        }
    }
}

归并排序

概念

  • 归并排序是由现代计算机之父——冯·诺依曼提出的算法
  • 归并排序的执行效率甚至高于希尔排序

原理

  • 归并排序的核心思想是将2个未排序的“小”数组的所有元素有序的填充到一个新的数组中去,排序的同时形成“合并”。

    • 先将两个数组的第一个元素进行对比,将较小的数字(升序排列)填充到新数组中。
    • 将取走数据的数组的下一位,与未被取走数据的数组的那一位对比。 img_1.png
    • 继续相同的操作即可······
    • 直到将最后一个放进去
  • 以上演示了两个很小的数组的合并过程,事实上,在实际应用中执行排序时,需要排序的原数组只有一个,并且长度通常超过2,那么就需要先将大数组进行反复拆分,得到如同以上演示的小数组(实际执行时,会拆至数组长度为1),再进行排序合并操作,这是一种经典的分治(divide and conquer)策略。

    分治策略

    分治策略是一种非常经典的思想,它将问题分(divide)成一些小问题,然后递归求解,而治(conquer)的阶段会将分的阶段得到的问题解合并在一起。

    • 递归:表现在方法的内部,调用当前方法自身
      • 例如在a()方法的内部调用a()方法
      • 仅当内部调用的a()方法运行结束,才会继续运行外部a()方法剩余的代码

实现步骤分析

  • 代码要解决的核心问题是:
    • 如何将原数组反复拆分,得到诺干个小数组;
    • 如何将两两的小数组合并成较大的数组,直到与原数组相同长度的数组。
  • 分析:将数组一分为二

    • 中间点:上级数组第1个下标+(上级数组最大下标-上级数组第1个下标)/2
    • 左侧:上级数组第1个下标 ~ 中间点
    • 右侧:中间点+1 ~ 上级数组最大下标
    class Demo{
      /**
       * 这是一个自定义的方法,方法的生命可以参考main()方法
       * @param arr 原数组
       * @param start 需要被拆分的区间的起始元素下标
       * @param end 需要被拆分的区间的结束元素下标
       */
      public static void mergeSort(int[] arr, int start, int end){
        // 求数组需要被拆分的区间的中间点
        int mid = start + (end - start) / 2;
        // 创建两个新的数组,因为每次拆分都执行这个分发,所以得到的左右新数组待定
        int[] leftArr = ???;
        int[] rightArr = ???;
      }
    }
    
  • 递归拆分数组

    class Demo{
      // 注意:方法的返回值将由void改为int[]
      public static void mergeSort(int[] arr, int start, int end){
        // 求数组需要被炒粉的区间的中间点
        int mid = start + (end - start) / 2;
        // 递归:使用当前数组作为参数,继续调用当前方法,也就是按照当前代码继续拆分数组
        int[] leftArr = mergeSort(arr, start, end);
        int[] rightArr = mergeSort(arr, mid + 1, end);
        // 假设原数组长度为8,调用当前方法时,传入的参数应该为:start=0;end=7
        // mergeSort()第1次执行时,mid = 0+(7-0)/2,结果为3
        // 为leftArr赋值时发生递归,传入的start=0,mid(end)=3
        // 第1次递归时,mid=0+(3-0)/2,结果为1
        // 为leftArr赋值时再次发生递归,传入的start=0,mid(end)=1
        // 再次递归时,mid=0+(1-0)/2,结果为0
        // 为leftArr复制时再次发生递归,传入的start=0,mid(end)=0
        // 则leftArr不应该继续拆分
        // 为rightArr赋值时同理
      }
    }
    
  • 为了防止递归“死循环”

    class Demo{
      public static int[] mergeSort(int[] arr, int start, int end) {
        // 为防止递归出现“死循环”,当end与start相同时(发生在尝试对长度为1的数组再拆分),不必再执行
        if (end == start) {
          // 使用return返回当前片段数组
          return new int[]{arr[start]};
        }
        // 求数组长度的中间值,用于将一个数组拆分为两个
        int mid = start + (end - start) / 2;
        // 递归:使用当前数据作为参数,继续调用当前方法
        int[] leftArr = mergeSort(arr, start, mid);
        int[] rightArr = mergeSort(arr, mid + 1, end);
      }
    }
    
  • 将两个小数组合并为有序的大数组

    • 假设leftArr和rightArr的长度都是1

      class Demo{
        public static int[] mergeSort(int[] arr, int start, int end) {
          // 暂时忽略前序的“拆分数组”的代码,假设leftArr和rightArr已经可用
      
          // 创建新的数组,用于存放合并后的有序结果
            int[] newArr = new int[leftArr.length+rightArr.length];
          
            // --- 假设leftArr和rightArr的长度都是1 ---
            // 将较小的数字放在新数组下标为0的位置,将较大的数字放在新数组下标为1的位置
            if (leftArr[0] < rightArr[0]){
                newArr[0] = leftArr[0];
                newArr[1] = rightArr[0];
            }else {
                newArr[0] = rightArr[0];
                newArr[1] = leftArr[0];
            }
        }
      }
      
    • 处理leftArr和rightArr的长度都大于1的情况,并使用后置自增

      class Demo{
        public static int[] mergeSort(int[] arr, int start, int end) {
          // 暂时忽略前序的“拆分数组”的代码,假设leftArr和rightArr已经可用
      
          // 创建新的数组,用于存放合并后的有序结果
            int[] newArr = new int[leftArr.length+rightArr.length];
          
            // 声明变量,l,r,n分表表示leftArr,rightArr,newArr的下标变量
          int l = 0, r = 0, n = 0;
          // 当leftArr和rightArr都没有遍历结束之前,一直循环
          while (l < leftArr.length && r < rightArr.length) {
              // 对比两个数组中的元素
              if (leftArr[l] <= rightArr[r]) {
                  // 将较小的数字存入到新数组中,并自增较小的数组和新数组的下标变量
                  newArr[n++] = leftArr[l++];
              } else {
                  newArr[n++] = rightArr[r++];
              }
          }
        }
      }
      
      • 使用“三元表达式”替换if语句
        class Demo{
          public static int[] mergeSort(int[] arr, int start, int end) {
            // 暂时忽略前序的“拆分数组”的代码,假设leftArr和rightArr已经可用
        
            // 创建新的数组,用于存放合并后的有序结果
            int[] newArr = new int[leftArr.length+rightArr.length];
          
            // 声明变量,l,r,n分表表示leftArr,rightArr,newArr的下标变量
            int l = 0, r = 0, n = 0;
            // 当leftArr和rightArr都没有遍历结束之前,一直循环
            while (l < leftArr.length && r < rightArr.length) {
              // 将较小的数字存入到新数组中,并自增较小的数组和新数组的下标变量
              newArr[n++] = leftArr[l] <= rightArr[r] ? leftArr[l++] : rightArr[r++];
            }
          }
        }
        
    • 合并时,有可能左侧或者右侧其中一侧提前全部合并到新数组,但另一个还有剩余,则将剩余所有数据直接填充到新数组中即可

      class Demo{
        public static int[] mergeSort(int[] arr, int start, int end) {
          // 假设此前的循环已经执行结束
      
          // 如果左侧还有剩余,可直接一次全部填充到新数组中
          while (l<leftArr.length){
              newArr[n++] = leftArr[l++];
          }
      
          // 如果右侧还有剩余,可直接一次全部填充到新数组中
          while (r< rightArr.length){
              newArr[n++] = rightArr[r++];
          }
          
          // 返回新数组
          return newArr;
        }
      }
      

代码实现

public class MergeSort {
    public static void main(String[] args) {
        int[] arr = {8, 1, 4, 9, 0, 3, 5, 2, 7, 6};
        System.out.println(Arrays.toString(arr));
        System.out.println();
        
        // 调用mergeSort()方法进行排序,并输出最后结果数组
        System.out.println(Arrays.toString(mergeSort(arr, 0, arr.length - 1)));
    }
    
    
    /**
     * 这是一个已定义的方法,方法的声明可以参考main()方法
     *
     * @param array 原数组
     * @param start 需要被拆分的区间的起始元素下标
     * @param end   需要被拆分的区间的结束元素下标
     * @return 合并的数组
     */
    public static int[] mergeSort(int[] array, int start, int end) {
        // 为防止递归出现“死循环”,当end与start相同时(发生在对长度为1的数组再拆分),不必再执行
        if (end == start) {
            return new int[]{array[start]};
        }
        
        // 求数组需要被拆分的区间中中间点
        int mid = start + (end - start) / 2;
        // 创建两个新的数组,分别表示拆分之后的左侧区域和右侧区域
        int[] leftArray = mergeSort(array, start, mid);
        int[] rightArray = mergeSort(array, mid + 1, end);
        
        // 创建新的数组,用于存放合并后的有序结果
        int[] newArray = new int[leftArray.length + rightArray.length];
        
        // 声明变量,l,r,n分别表示leftArray,rightArray,newArray的下标变量
        int l = 0, r = 0, n = 0;
        // 当leftArray,rightArray都没有遍历结束之前,一直循环
        while (l < leftArray.length && r < rightArray.length) {
            // 将较小的数字存入到新数组中,并自增较小的数组和新数组的下标变量
            newArray[n++] = leftArray[l] <= rightArray[r] ? leftArray[l++] : rightArray[r++];
        }
        
        // 如果左侧还有剩余,可直接一次全部填充到新数组中
        while (l < leftArray.length) {
            newArray[n++] = leftArray[l++];
        }
        
        // 如果右侧还有剩余,可直接一次全部填充到新数组中
        while (r < rightArray.length) {
            newArray[n++] = rightArray[r++];
        }
        
        // 返回新数组
        return newArray;
    }
}

快速排序

概念

快速排序是目前经典内部排序算法中平均性能最优的算法,通常情况下,它的排序效率明显高于前面五种排序算法。

原理

  • 先挑选数组中的某个元素,它将作为所有元素排列大小的分界值
    • 作为分界值的数组元素称之为:枢纽元(pivot),也可称之为:主元
  • 需要将比枢纽元小的元素放在其左侧位置,将比枢纽元大的元素放在其右侧位置
    • 并不关心起左侧区域或右侧区域内的各元素是否有序

以一个数组为例:

  • 可以使用最右侧的6最为枢纽元,如需升序排列,则需要先达到以下效果:

提示

为了便于理解,本例选取数组最右侧的元素作为枢纽元,但是并不代表这种选取方式是最优的。

  • 将原数组根据枢纽元划分开来的过程称之为:分区(Partition)
    • 第一次分区过程大概是这样的:先确定枢纽元
      • 需要再脑海中奖数组的最终目标想象成由下图的几个区域组成

        注意:

        在分区结束前,枢纽元位置的元素不会发生换位操作

      • 为了便于描述,以下将“<=枢纽元”的区域称之为“左侧分区”,将“>枢纽元”的区域称之为“右侧分区”
    • 使用第一个数字与枢纽元比较大小:
    • 由于5小于6,所以,5在左侧分期,并且不用换位
    • 再使用数组的第二个元素与枢纽元进行对比

      注意:

      尽管第二位的1比第一位的5跟小,但不需要换位,后续会使用递归再次处理

    • 在使用数组的第三个元素与枢纽元进行对比
    • 由于9大于6,所以,9在右侧区域

      注意:

      不用换位,只需要标记位置即可,此时枢纽元元素仍在数组的最右侧

    • 在使用数组的第四个元素与枢纽元进行对比
    • 由于3小于6,所以,3在左侧的分区

      注意:

      由于各分区内的元素必须是连续的,所以,此次需要换位

    • 后续以此类推,全部完成后,将枢纽元与右侧分区的第一个元素换位
    • 得到第一次分区的最终结果
    • 接下来,对左右两个分区继续执行相同的操作
    • 最终得到有序的数组(始终对同一个数组的片段进行处理,未拆分,则无需合并)

代码实现

public class QuickSort1 {
    public static void main(String[] args) {
        int[] array = {8, 1, 4, 9, 0, 3, 5, 2, 7, 6};
        System.out.println(Arrays.toString(array));
        System.out.println();
        
        quickSort(array, 0, array.length-1);
    
        System.out.println(Arrays.toString(array));
        
    }
    
    /**
     * 这是一个自定义的方法
     * 注意:快速排序本质上没有拆出新数组,也不需要合并,所以该方法不需要返回值
     * @param array 原数组
     * @param start 需要被拆分的区间的起始元素下标
     * @param end 需要被拆分的区间的结束元素下标
     */
    public static void quickSort(int[] array, int start, int end) {
        int pivot = array[end];
        
        // 标记左侧区域的位置,在需遍历的数组片段的左侧
        int x = start - 1;
        
        // 遍历数组片段
        for (int i = start; i < end; i++) {
            // 判断当前元素是否小于或等于枢纽元
            if (array[i] <= pivot) {
                // 判断当前元素的左侧是否已经存在大于枢纽元的元素
                if (i - x > 1) {
                    int tmp = array[i];
                    array[i] = array[x + 1];
                    array[x + 1] = tmp;
                    x++;
                } else {
                    x = i;
                }
            }
        }
        
        // 交换枢纽元与第一个大于枢纽元的元素的位置,即:将枢纽元放在左右区域的中间
        if (pivot < array[x + 1]) {
            array[end] = array[x + 1];
            array[x + 1] = pivot;
        }
        
        // 如果左侧区域仍有多个元素,则递归
        // 因为x是左侧区域的最右侧元素下标
        // 若比需要处理的数组片段的最左侧元素下标大,则表示左侧区域存在超过一个元素
        if (x > start) {
            quickSort(array, start, x);
        }
        // 如果右侧区域仍有多个元素,则递归
        if (end - x - 1 > 1) {
            quickSort(array, x + 2, end);
        }
    }
}

排序算法的选取

  • 在一般情况下,可能比较关注算法所表现出来的运算效率,评估值主要包括:
    • 判断次数
    • 交换次数
    • 耗时
  • 可以随机生成50000个随机数构成的数组,并观察以上各指标。

多种排序算法的运算效率

  • 50000个随机数的数组排序测试结果参考:

注意

  • 一般不建议使用元素特别多的数组,因为有些算法的性能非常差;
    • 例如,使用冒泡排序来处理长度为50万的数组,整个排序过程可能需要7分钟以上(仅供参考)
  • 由于随机数值大小、初始数组内元素的大小顺序、机器的性能等多方面原因,测试结果仅供参考;
    • 例如,在快速排序时,数组元素的顺序直接影响排序效率,另外,插入排序、希尔排序、归并排序也是这样
  • 归并排序没有发生实质的交换,而是将原数组的元素有序的填充到新数组,只能使用“填充次数”替代“交换次数”作为参考。
  • 另外,还必须关注算法的稳定性
  • 算法的稳定性:排序的本质是“交换位置”,在数组中,可能存在多个值完全相同的元素,如果排序前这些元素的位置和排序后一样,则算法是稳定的,如果发生了位置变化,则算法是不稳定的。
    • 即使归并排序中没有实质的交换位置,最终结果与原数组对比,也可视为发生了位置的交换。
    • 尽管相同值的元素从表面上看是相同的,但是,只要本质上不是同一个,在处理时就应该视为不同的数据
      • 例如多本相同的数、同款同型号手机等。。。
  • 总的来说,在选取排序算法时,首先要考虑是否关注稳定性,如果需要算法是稳定的,只能在归并排序,插入排序和冒泡排序中选择,并且,有序归并排序的效率最高,应该优先选取归并排序,当然,如果不需要算法是稳定的,快速排序则是最优先考虑的算法。