当前位置: 首页 > news >正文

十大常见排序算法详解(附Java代码实现和代码解析)

文章目录

  • 十大排序算法
    • ⛅前言
    • 🌱1、排序概述
    • 🌴2、排序的实现
      • 🌵2.1 插入排序
        • 🐳2.1.1 直接插入排序
          • 算法介绍
          • 算法实现
        • 🐳2.1.2 希尔排序
          • 算法介绍
          • 算法实现
      • 🌵2.2 选择排序
        • 🐳2.2.1 选择排序
          • 算法介绍
          • 算法实现
        • 🐳2.2.2 堆排序
          • 算法介绍
          • 算法实现
      • 🌵2.3 交换排序
        • 🐳2.3.1 冒泡排序
          • 算法介绍
          • 算法实现
        • 🐳2.3.2 快速排序
          • 算法介绍
          • 算法实现
      • 🌵2.4 归并排序
          • 算法介绍
          • 算法实现
      • 🌵2.5 非比较排序
        • 🐳2.5.1 计数排序
          • 算法介绍
          • 算法实现
        • 🐳.5.2 桶排序
          • 算法介绍
          • 算法实现
        • 🐳2.5.3 基数排序
          • 算法介绍
          • 算法实现
    • 🌳总结

十大排序算法

⛅前言

  大家好👋,我是知识汲取者😄,今天要给大家带来一篇有关排序的文章,相信大家一定在工作中或生活中接触过不少有关排序的问题吧,比如:生活中,我们在课间做体操时的排队(根据身高排序)、考试的排名(根据分数排序)、报道后老师点名(根据序号排序)……工作中,我们需要对某个表按照id进行排序、或者按照姓名的首字母进行排序,这些都是很常见的,当然这些都是可以直接手动调用一个函数就能一键完成,并不需要我们去具体实现,但是大家难道就不好奇为什么我点一下就能直接实现这个排序功能呢?它是怎么实现的呢?假如让我来制定一个排序规则,我又该如何去实现它呢?如果您有这样或那样的疑惑的话,我相信您一定能有所收获的😉

  本文适合人群:想系统了解十大基础排序算法、还没有接触或对与排序算法不熟练的读者。本文将详细介绍十大经典排序算法:①直接插入排序、②希尔排序、③直接选择排序、④堆排序、⑤冒泡排序、⑥快速排序、⑦归并排序、⑧计数排序、⑨桶排序、⑩基数排序。通过本文,您不仅能学到十大排序的思想、具体实现,同时还能了解它们各自的特点,比如:应用场景、排序算法的事件复杂度等。让我们一起冲冲冲(●ˇ∀ˇ●)(备注:本文所有代码实现均以升序为例)

PS:文中部分图片摘自菜鸟教程,如有侵权还请及时告知,笔者将立即删除

相关推荐📢:

  • 知识汲取者的主页:欢迎🥰参观,希望能对你有所帮助
  • 算法与数据结构专栏:包含博主所有对算法的学习笔记
  • 算法和数据结构导学:很适合算法初学者
  • 算法题解专栏:包含博主部分算法题解
  • 算法刷题站点:LeetCode | 牛客
  • 代码仓库:Gitee | Github

在这里插入图片描述

🌱1、排序概述

  • 什么是排序

    排序是计算机内经常进行的一种操作,其目的是将一组“无序”的记录序列按照某种规则调整为“有序”的记录序列

  • 排序的目的是什么?

    1. 方便查找数据
    2. 有利于观察数据的规律性

    ……

  • 什么是排序算法

    所谓排序算法,就是能够解决排序问题的算法,而算法(Algorithm)是指解题方案的准确而完整的描述,是一系列解决问题的清晰指令,算法代表着用系统的方法描述解决问题的策略机制

  • 什么是排序算法的稳定性

    排序算法的稳定性是指,一组数据经过排序后,具有相同值的两个数在排序后,仍然保持相同的次序。

    比如在 [ a , b , c , d , e , f ] [a,b,c,d,e,f] [a,b,c,d,e,f] 这组数中 c c c e e e具有相同的值,此时 c c c e e e的前面;经过排序后变为 [ a , b , e , d , c , f ] [a,b,e,d,c,f] [a,b,e,d,c,f] ,此时 c c c e e e的后面,它们的次序发生了改变,则称该排序算法具有不稳定性;同理,如果排序后变为 [ a , b , c , e , d , f ] [a,b,c,e,d,f] [a,b,c,e,d,f] ,此时此时 c c c仍然在 e e e的前面,则称该排序算法具有稳定性。

  • 排序算法的分类

    image-20221014170410285

    备注

    • 内部排序:被排序的数据都是存储在内存1中,且排序过程也是发生在内存中的

      适用于规模不是很大的数据

    • 外部排序:排需的数据一部分存储在内存中,一部分存储在外存2中,排序过程需要涉及内存外数据的交换

      适用于规模很大、不能一次性装入内存中的数据

    • 比较排序:比较排序算法是通过比较元素之间的大小关系来确定它们的顺序。

    • 非比较排序:非比较排序算法是一类不基于元素之间比较的排序算法。

🌴2、排序的实现

对于排序的实现,这里我统一都采用对数组进行一个增序排序(从小到大进行排序),也就是数组的元素随着索引的递增而递增,而对于排序的目标数组直接采用 arr[10]=[5, 3, 9, 1, 7, 2, 8, 4, 6, 0]

    public static void main(String[] args) {// 准备待排序的数组int[] arr = {5, 3, 9, 1, 7, 2, 8, 4, 6, 0};// 进行排序// 输出排序后的数组System.out.println(Arrays.toString(arr));}

🌵2.1 插入排序

每次将一个待排序的记录,按其大小插入已经排好序的子列表的适当位置,直到记录插入完成,也就说明排序已完成

  • 插入排序的特点
    • 稳定性:插入排序是一种稳定的排序算法,即相等元素的相对顺序在排序前后保持不变。
    • 原地排序:插入排序是一种原地排序算法,不需要额外的辅助空间,只需使用少量的额外空间进行元素交换。
    • 时间复杂度:在平均情况和最坏情况下,插入排序的时间复杂度为O(n^2),其中n是待排序序列的长度。在最好情况下(序列已经部分有序),插入排序的时间复杂度可以达到O(n)。
    • 对小规模数据效果好:相比其他复杂度更高的排序算法,插入排序在处理小规模数据时的效果较好。当待排序序列的规模较小时,插入排序的性能优于其他排序算法。
🐳2.1.1 直接插入排序
算法介绍

直接插入排序(Straight Insertion Sort)是一种简单且常用的排序算法。它的基本思想是将待排序的元素依次插入已经排好序的部分,使得插入后仍然保持有序。

  • 实现步骤

    • Step1:遍历待排序数组。从第二个元素开始,依次将每个元素插入已排序序列中。
    • Step2:比较与插入操作。将当前待插入元素与已排序序列中的元素逐个比较并移动位置,直至找到合适的插入位置或遍历完已排序序列。
    • Step3:插入元素。将待插入元素插入到合适的位置,使得插入后的子序列仍然有序。
    • Step4:重复上述步骤。继续对下一个未排序元素进行插入操作,直至所有元素都被插入到已排序序列中。
  • 示意图

    image-20230929175235180

算法实现
    /*** 直接插入排序* @param arr 待排序的数组*/private static void directInsertionSort(int[] arr) {// 从第2个元素开始遍历for (int i = 1; i < arr.length; i++) {// 待插入的元素int key = arr[i];int j = i - 1;// 将当前元素与前面已经排序好的元素进行比较,直到没有发现比自己大的元素while (j >= 0 && arr[j] > key) {// 当发现后一个元素比前一个元素小时,用前一个元素覆盖后一个元素的值arr[j + 1] = arr[j];j--;}// 将待插入的元素放到对应的位置arr[j + 1] = key;}}

备注:如果想要降序排序,只需要修改为while (j >= 0 && arr[j] < key) 即可

🐳2.1.2 希尔排序
算法介绍

希尔排序(Shell Sort)也称缩小增量排序,是对直接插入排序的改进版本,它通过引入步长的概念,先将待排序序列分割成若干个子序列,对每个子序列进行直接插入排序,然后逐步缩小步长进行排序,最终使整个序列有序。

  • 实现步骤

    • Step1:确定步长序列。选择一个步长序列,一般可以使用希尔原始提出的序列(n/2, n/4, …, 1),也可以根据实际情况选择其他步长序列。
    • Step2:根据步长分组。根据选定的步长,将待排序序列分成若干个子序列。
    • Step3:对各个子序列进行直接插入排序。对每个子序列应用直接插入排序算法,将子序列转化为部分有序序列。
    • Step4:不断缩小步长并重复上述步骤。重复步骤2和步骤3,逐步减小步长,直至步长为1,完成最后一次排序。
  • 示意图

    image-20230930102027569

    image-20230930102529692

    image-20230930102847753

算法实现
    /*** 希尔排序* @param arr 待排序的数组*/private static void shellSort(int[] arr) {int n = arr.length;// 确定步长序列int gap = n / 2;while (gap > 0) {for (int i = gap; i < n; i++) {int temp = arr[i];int j = i;// 对每个子序列应用直接插入排序算法while (j >= gap && arr[j - gap] > temp) {arr[j] = arr[j - gap];j -= gap;}arr[j] = temp;}// 缩小步长gap /= 2;}}

备注:如果想要降序排序,只需要修改为while (j >= gap && arr[j - gap] < temp) 即可

🌵2.2 选择排序

每一趟从待排序的记录中选出最大或最小的记录,让后按顺序放在已排序的子列表的最后,直到所有记录排完

  • 选择排序的特点
    • 不稳定性:选择排序是一种不稳定的排序算法,即相等元素的相对顺序可能在排序过程中发生改变。
    • 原地排序:选择排序是一种原地排序算法,不需要额外的辅助空间,只需使用少量的额外空间进行元素交换。
    • 时间复杂度:选择排序的时间复杂度始终为O(n^2),其中n是待排序序列的长度。无论序列是否有序,每次都需要遍历剩余未排序部分来找到最小(或最大)元素。
    • 大规模数据效果差:由于选择排序每次都要从剩余未排序部分中选取最小(或最大)元素,并将其放入已排序部分的末尾,因此在处理大规模乱序数据时效率较低。
🐳2.2.1 选择排序
算法介绍

选择排序(Selection Sort)是一种简单直观的排序算法,它的基本思想是每次从未排序的部分中选取最小(或最大)的元素,放置在已排序部分的末尾,逐步构建有序序列。选择排序的时间复杂度为O(n^2),不稳定,但是由于其操作简单,对于小规模数据或部分有序的数据仍然具有一定的实用价值。

  • 实现步骤

    • Step1:遍历数组。从数组的第一个元素开始,依次向后遍历所有元素。
    • Step2:寻找最小值。在当前遍历到的元素及其后面的元素中,找到最小的元素。
    • Step3:交换位置。将最小值与当前遍历到的元素进行交换位置,即将最小值放到当前遍历的位置上。
    • Step4:重复上述步骤。重复步骤2和步骤3,直到遍历完整个数组

    每次遍历都寻找出剩余元素中的最小值,将最小值放到前面,思路很简单

  • 示意图

    image-20230930141614197

算法实现
    /*** 选择排序* * @param arr 待排序的数组*/private static void selectSort(int[] arr) {int n = arr.length;// 遍历数组for (int i = 0; i < n - 1; i++) {// 从前往后遍历,寻找最小值的索引int minIdx = i;for (int j = i + 1; j < n; j++) {if (arr[j] < arr[minIdx]) {minIdx = j;}}// 将最小值与当前元素交换位置swap(arr, i, minIdx);}}/*** 交换 arr 数组中索引为 i 和索引为 j 处的元素*/private static void swap(int[] arr, int i, int j) {int temp = arr[i];arr[i] = arr[j];arr[j] = temp;}

备注:如果想要降序排序,只需要修改为if (arr[j] > arr[minIdx]) 即可

🐳2.2.2 堆排序
算法介绍

堆排序(Heap Sort)是一种基于二叉堆(Binary Heap)的排序算法。它通过构建最大堆(Max Heap)或最小堆(Min Heap)来进行排序。

如果你对堆这个数据结构不太了解到话,这个排序可能一下很难看懂,关于堆的详解可以参考这篇文章:【数据结构篇】堆,把堆搞懂了,再来看这个堆排序可能会变得很简单

  • 实现步骤

    • Step1:构建最大堆。将待排序的序列构建成一个最大堆。
    • Step2:将堆顶元素与末尾元素交换。将堆顶元素(最大值)与末尾元素交换,并将交换后的末尾元素排除在堆之外。
    • Step3:重新调整堆顶元素位置。通过下沉操作,使交换后的新堆顶元素重新满足最大堆的性质。
    • Step4:重复步骤2和步骤3,直到堆中只剩下一个元素为止。
  • 示意图

    初始状态:[5, 3, 9, 1, 7, 2, 8, 4, 6, 11]

    构建最大堆:

    1. 定位最后一个非叶子节点,计算公式n/2-1
    2. 以最后一个非叶子节点为出发点,从后往前遍历
    3. 不断递归的下浮元素,确保父节点永远是比子节点大的
    4. 最终就可以得到一个最大堆

    下方是构建最大堆的示意图(红色部分是当前正在进行下浮的父节点):

    image-20230930211534837

    image-20230930211918172

    image-20230930212052026

    最核心的步骤就是构建最大堆,最大堆已经实现了,现在只需要按照下面的步骤即可得到一个升序数组

    1. 循环从 n-1 到 1,每次循环表示将当前未排序的部分中最大的元素放到已排序的部分的末尾。
    2. 在每一次循环中,i就会减一,也就是堆的最大索引会减一,防止末尾节点参与下浮比较,然后调用 swap 函数将堆的根节点与未排序部分的最后一个元素交换位置。经过交换后,堆的根节点变成了当前未排序部分中的最大值。
    3. 然后,调用 heapifyDown 函数对剩余的元素进行堆调整。heapifyDown 函数会将交换后的根节点不断下沉,直到满足堆的性质。
    4. 经过循环操作,每次将最大值放到已排序部分的末尾,直到所有元素都被放置到已排序部分,即完成了排序。

    这里我就不画图了,思路在这里,大家可以自行画一个草图

算法实现
    /*** 堆排序** @param arr 待排序的数组*/public static void heapSort(int[] arr) {int n = arr.length;// 构建最大堆(从最后一个非叶子节点开始)for (int i = n / 2 - 1; i >= 0; i--) {heapifyDown(arr, n, i);}System.out.println(Arrays.toString(arr));// 排序(从后往前遍历,不断将极大值放到堆的末尾)for (int i = n - 1; i > 0; i--) {// 将根节点与最后一个元素交换位置swap(arr, 0, i);// 对剩余的元素进行堆调整heapifyDown(arr, i, 0);}}/*** 下浮** @param arr* @param n 数组中的元素,arr.length* @param i 要下浮元素的索引*/private static void heapifyDown(int[] arr, int n, int i) {// 父节点索引int parent = i;// 左节点索引int left = 2 * i + 1;// 右节点索引int right = 2 * i + 2;// 如果左子节点存在且大于父节点,则更新最大值if (left < n && arr[left] > arr[parent]) {parent = left;}// 如果右子节点存在且大于父节点或左子节点,则更新最大值if (right < n && arr[right] > arr[parent]) {parent = right;}// 如果最大值不是父节点,则交换位置并递归调整子树if (parent != i) {swap(arr, i, parent);heapifyDown(arr, n, parent);}}/*** 交换 arr 数组中索引为 i 和索引为 j 处的元素*/private static void swap(int[] arr, int i, int j) {int temp = arr[i];arr[i] = arr[j];arr[j] = temp;}

备注:如果想要降序排序,需要改用最小堆,只需要修改if (right < n && arr[right] < arr[parent])if (right < n && arr[right] < arr[parent])两处即可

几个注意点

  1. 对于完全二叉树的最后一个非叶子节点,我们需要通过n/2-1进行计算得到,这个的由来也很简单,画一个草图就能理解了,关于数学证明推导可以参考文末的文章链接
  2. 这里需要理解堆的下浮操作,关于堆的下浮操作可以参考这篇文:【数据结构篇】堆

🌵2.3 交换排序

两两相互比较,当发现两个记录的次序相反时就进行交换,直到没有反序的记录为止

  • 交换排序的特点
    • 交换操作:交换排序通过比较相邻元素的大小,并根据需要进行交换操作来达到排序的目的。常见的交换排序算法有冒泡排序和快速排序。
    • 原地排序:交换排序通常是原地排序算法,不需要额外的辅助空间。
    • 时间复杂度:冒泡排序的平均和最坏时间复杂度为O(n^2),而快速排序的平均时间复杂度为O(nlogn)。快速排序的性能通常优于冒泡排序。
🐳2.3.1 冒泡排序
算法介绍

冒泡排序(Bubble Sort)是一种简单的排序算法。它重复地遍历待排序的元素列表,比较相邻两个元素的大小,并按照规定的顺序交换它们,直到整个列表排序完成。

  • 实现步骤

    • Step1:比较。从列表的第一个元素开始,依次比较每个元素和其相邻的下一个元素。
    • Step2:交换。如果当前元素大于相邻的下一个元素,则交换这两个元素的位置。
    • Step3:继续向后遍历,重复执行上述步骤,直到最后一个元素。
    • Step4:重复以上步骤,直到没有任何交换操作发生,即列表已排序完成。
  • 示意图

    第1轮:
    [3, 5, 1, 7, 2, 8, 4, 6, 9, 11]第2轮:
    [3, 1, 5, 2, 7, 4, 6, 8, 9, 11]第3轮:
    [1, 3, 2, 5, 4, 6, 7, 8, 9, 11]第4轮:
    [1, 2, 3, 4, 5, 6, 7, 8, 9, 11]第5轮:
    [1, 2, 3, 4, 5, 6, 7, 8, 9, 11]....第9轮:
    [1, 2, 3, 4, 5, 6, 7, 8, 9, 11]
    

    每一轮都会将一个最大值放到末尾,这一点类似于堆排序

    bubbleSort

算法实现
    /*** 冒泡排序** @param arr 待排序的数组*/public static void bubbleSort(int[] arr) {int n = arr.length;for (int i = 0; i < n - 1; i++) {for (int j = 0; j < n - i - 1; j++) {if (arr[j] > arr[j + 1]) {// 如果发现左侧元素比当前元素小,则进行交换swap(arr, j , j+1);}}}}

备注:如果想要降序排序,只需要修改为while (j >= gap && arr[j - gap] < temp) 即可

🐳2.3.2 快速排序
算法介绍

快速排序(Quicksort)是 C.R.A.Hoare 于1962年提出一种分区交换排序,采用分治策略。快速排序的基本思想:从序列中选取一个主元,并以该主元为基准,通过一趟排序将要排序的数据分割成独立的两部分,其中一部分的所有数据都比另外一部分的所有数据都要小,然后再按此方法对这两部分数据分别进行快速排序,整个排序过程可以递归进行,以此达到整个数据变成有序序列。

快速排序是目前已知的平均速度最快的一种排序方法,是对冒泡排序的一种改进。

  • 主要实现步骤

    • Step1选取主元。一般是选取数组的第0个元素或最后一个作为主元
    • Step2划分数组。遍历数组,根据主元将数组划分成两个子数组,如果是增序排序,则左侧的子数组恒大于右侧子数组;如果是降序排序,则左侧的子数组恒小于右侧的子数组
    • Step3递归Step1Step2递归进行

    快排是实现方式有很多种,可以选取固定主元,比如:每次都选取区间第一个元素作为主元,每次都选取区间最后一个元素作为主元,或者每次随机化选取主元。后面的代码实现,我都会讲解一下三种不同的实现

    温馨提示:可以通过随机化主元让快速排序的时间复杂度更加稳定,这在一些算法题中是比较有用的

  • 示意图

    方式一:两个指针都往后遍历,每次主元选择区间最右侧元素

    image-20231001123509797

    image-20231001123542638

    方式二:两个指针相向遍历,每次选取区间最右侧元素作为主元

    image-20231001134355535

    方式三:以方式一为基础,随机化主元

    图略……

算法实现

PS:我个人比较喜欢方式一,方式二可能更加简介,但是方式二的注意点比较多,比如边界问题、能否取等问题

  • 方式一:两个指针都往后遍历,每次主元选择区间最右侧元素

        /*** 快速排序** @param arr 待排序的数组*/private static void quickSort(int[] arr) {if (arr == null || arr.length == 0) {return;}quickSort(arr, 0, arr.length - 1);}/*** 快速排序** @param arr   待排序的数组* @param left  待划分区间的左边界* @param right 待划分区间的右边界*/private static void quickSort(int[] arr, int left, int right) {if (left >= right) {// 区间只剩一个元素,停止划分return;}// 将数组划分为两部分,获取划分点位置int pivotIndex = partition(arr, left, right);// 对划分的两部分进行递归快排quickSort(arr, left, pivotIndex - 1);quickSort(arr, pivotIndex + 1, right);}/*** 划分区间** @param arr   待排序的数组* @param left  待划分区间的左边界* @param right 待划分区间的右边界* @return*/private static int partition(int[] arr, int left, int right) {// 选择区间最右侧元素作为主元int pivot = arr[right];// 双指针从前往后遍历,划分区间(左侧区间 < 主元,右侧区间 >= 主元)int i = left - 1;for (int j = left; j < right; j++) {if (arr[j] < pivot) {// 如果当前元素比主元小就放到 i+1 的左侧swap(arr, ++i, j);}}// 将主元放到分界点,然后返回主元索引swap(arr, i + 1, right);return i + 1;}/*** 交换 arr 数组中索引为 i 和索引为 j 处的元素*/private static void swap(int[] arr, int i, int j) {int temp = arr[i];arr[i] = arr[j];arr[j] = temp;}
    
  • 方式二:两个指针相向遍历,每次选取区间最右侧元素作为主元

        /*** 快速排序** @param arr 待排序的数组*/private static void quickSort(int[] arr) {if (arr == null || arr.length == 0) {return;}quickSort(arr, 0, arr.length - 1);}/*** 快速排序** @param arr   待排序的数组* @param left  待划分区间的左边界* @param right 待划分区间的右边界*/private static void quickSort(int[] arr, int left, int right) {if (left >= right) {// 区间只剩一个元素,停止划分return;}// 将数组划分为两部分,获取划分点位置int demarcation = partition(arr, left, right);// 对划分的两部分进行递归快排quickSort(arr, left, demarcation-1);quickSort(arr, demarcation, right);}/*** 划分区间** @param arr   待排序的数组* @param left  待划分区间的左边界* @param right 待划分区间的右边界* @return*/private static int partition(int[] arr, int left, int right) {// 选择区间最右侧元素作为主元int pivot = arr[right];// 双指针相向遍历,划分区间(左侧区间 <= 主元,右侧区间 >= 主元)int i = left - 1;int j = right + 1;while (i < j) {// i从前往后遍历,寻找大于等于主元的元素while (arr[++i] < pivot) ;// j从后往前遍历,寻找小于主元的元素while (arr[--j] > pivot) ;if (i < j) {// 如果 i<j,说明区间中不止一个元素,需要进行交换swap(arr, i, j);}}return i;}/*** 交换 arr 数组中索引为 i 和索引为 j 处的元素*/private static void swap(int[] arr, int i, int j) {int temp = arr[i];arr[i] = arr[j];arr[j] = temp;}
    

    注意点

    1. 指针移动判断是否取等?

      while (arr[++i] < pivot) ;
      while (arr[--j] > pivot) ;
      

      看代码就知道,肯定是不能取等的,因为如果取等,可能会出现所有的数都相等,那么指针的移动就会出界,从而报索引越界异常。所以得到的区间肯定是 左侧区间 <= 主元,右侧区间 >= 主元

    2. 移动指针是否在判断之前?

      在1.中,我们就已经知道了左侧和右侧区间都可能等于主元,如果我们先判断后移动指针,此时做右区间各有一个等于主元的值,那么此时就陷入了死循环,为了避免这种情况,我们就采用先移动后判断

    3. 为什么返回的分界点 i,必须放在右侧区间?

      这个其实看我画的那个示意图就能够明白了,因为区间划分完成后 i 索引对应的元素可能是大于等于主元的,我们如果将 i 放到左侧区间,那么就不满足左侧区间中的元素全部小于等于主元,这就导致最终快排是无法成功的,可能会一直陷入死循环

以上3点是方式二的一些注意点,也解释了为什么要这样写

  • 方式三:随机化选取主元(方式二实现)

    随机化快速排序,能够在一定程度上让排序的时间更加稳定,不至于因为固定选取主元导致出现极端的情况。

    具体实现可以基于前面两种方式,只是将主元进行了随机化:我们可以通过随机选取一个索引,然后将随机的主元与最左侧元素进行交换,这样就又变成了固定主元了。

    以下是核心代码:

    		 // 随机选择主元元素,并将其交换到区间右侧int randomIndex = random.nextInt(right - left + 1) + left;swap(arr, randomIndex, right);int pivot = arr[right];
    

🌵2.4 归并排序

算法介绍

归并排序法(Merge Sort,以下简称MS)是分治法思想运用的一个典范。归并排序的基本思想:先将一个序列按照划分成一个个的有序子序列(每个子序列都只有一个元素),然后两两进行合并(前提是两个子列表都有序),形成一个新的有序列表,再重复进行两两合并,直到所有子列表合并成一个有序列表,也就说明排序完成了。通常采用二路归并排序实现,所谓的二路归并排序是指将相邻两个有序子序列合并为一个有序列表。

  • 归并排序的特点

    • 分治思想:归并排序采用分治思想,将待排序序列不断拆分成更小的子序列,然后将子序列排序并合并成一个有序序列。常见的归并排序算法有自顶向下的递归实现和自底向上的迭代实现。
    • 稳定性:归并排序是一种稳定的排序算法,相等元素的相对顺序在排序前后保持不变。
    • 非原地排序:归并排序需要额外的辅助空间来存储临时的合并结果,因此它不是原地排序算法。
    • 时间复杂度:归并排序的平均和最坏时间复杂度都为O(nlogn),其中n是待排序序列的长度。它的性能稳定且较为优秀。
  • 主要实现步骤

    • Step1划分。通常是使用递归将一个序列按照 n / 2 n/2 n/2划分成一个个的有序的子序列
    • Step2合并。将相邻的子序列两两合并成一个新的有序子序列
  • 常见归并排序的实现方式

    • 自顶向下归并排序(Top-down Merge Sort):

      • 也被称为递归归并排序。
      • 使用分治法的思想,将数组不断地分割成较小的子数组,直到每个子数组只包含一个元素。
      • 然后按照从底部到顶部的顺序,逐步对已经分割的子数组进行合并,形成有序的更大的子数组,最终得到完全排序的数组。
      • 递归实现方式,通过递归调用和合并操作来完成排序。

      自底向上归并排序(Bottom-up Merge Sort):

      • 直接使用迭代的方式,从单个元素开始,两两合并相邻的子数组。
      • 每一次合并都会形成一个更大的有序子数组,直到最后合并成一个完全排序的数组。
      • 不需要递归调用,而是通过循环迭代依次合并子数组。
      • 具体过程是先两个两个地合并相邻子数组,然后四个四个地合并,依次类推,直到整个数组排序完成。
    • 原地归并排序

      • 首先,将待排序数组分割为多个子数组,直到每个子数组只有一个元素。
      • 然后,从单个元素开始,两两合并相邻的子数组,并将结果原地存回到原始数组中。这里的关键是通过交换元素的位置,实现原地合并。
      • 重复上述合并操作,每次合并的子数组大小翻倍,直到整个数组完全有序。

    PS:我平常用的最多的是自顶向下实现的归并排序

  • 示意图

    • 自顶向下的递归实现:

      • 初始数组:5, 3, 9, 1, 7, 2, 8, 4, 6, 0
        • 递归分解成子数组:5, 3, 9, 1, 7 和 2, 8, 4, 6, 0
        • 继续分解子数组:5, 3 和 9, 1, 7,以及 2, 8 和 4, 6, 0
        • 继续分解子数组:5 和 3,以及 9 和 1, 7,和 2 和 8,以及 4 和 6,以及 0
      • 开始归并操作:
        • 归并 5 和 3:3, 5
        • 归并 9 和 1, 7:1, 7, 9
        • 归并 2 和 8:2, 8
        • 归并 4 和 6:4, 6
        • 归并 0:0
      • 继续归并:
        • 归并 3, 5 和 1, 7, 9:1, 3, 5, 7, 9
        • 归并 2, 8 和 4, 6:2, 4, 6, 8
      • 最终归并:
        • 归并 1, 3, 5, 7, 9 和 2, 4, 6, 8:1, 2, 3, 4, 5, 6, 7, 8, 9

      可以看到自顶向下是先分解后合并

    • 自底向上的迭代实现:

      • 初始数组:5, 3, 9, 1, 7, 2, 8, 4, 6, 0
      • 第一轮迭代:
        • 归并 [5] 和 [3]:3, 5
        • 归并 [9] 和 [1]:1, 9
        • 归并 [7] 和 [2]:2, 7
        • 归并 [8] 和 [4]:4, 8
        • 归并 [6] 和 [0]:0, 6
      • 第二轮迭代:
        • 归并 [3, 5] 和 [1, 9]:1, 3, 5, 9
        • 归并 [2, 7] 和 [4, 8]:2, 4, 7, 8
        • 归并 [0, 6]:0, 6
      • 第三轮迭代:
        • 归并 [1, 3, 5, 9] 和 [2, 4, 7, 8]:1, 2, 3, 4, 5, 7, 8, 9
        • 归并 [0, 6]:0, 6
      • 最终归并:
        • 归并 [1, 2, 3, 4, 5, 7, 8, 9] 和 [0, 6]:0, 1, 2, 3, 4, 5, 6, 7, 8, 9

      可以看到自底向上是两两合并,没有进行分界

    • 原地归并排序实现:

      • 第一次合并:
        • 归并 [5] 和 [3]:3, 5
        • 归并 [9] 和 [1]:1, 9
        • 归并 [7] 和 [2]:2, 7
        • 归并 [8] 和 [4]:4, 8
        • 归并 [6] 和 [0]:0, 6
      • 第二次合并:
        • 第二次合并:
        • 归并 [3, 5] 和 [1, 9]:1, 3, 5, 9
        • 归并 [2, 7] 和 [4, 8]:2, 4, 7, 8
        • 归并 [0, 6]:0, 6
      • 第三次合并:
        • 归并 [1, 3, 5, 9] 和 [2, 4, 7, 8]:1, 2, 3, 4, 5, 7, 8, 9
        • 归并 [0, 6]:0, 6
      • 最终合并:
        • 归并 [1, 2, 3, 4, 5, 7, 8, 9] 和 [0, 6]:0, 1, 2, 3, 4, 5, 6, 7, 8, 9

      原地和自底向上的过程类似,通过元素交换来实现排序,没有使用辅助数组

算法实现
  • 方式一:自顶向下的递归实现

        /*** 归并排序** @param arr 待排序的数组*/private static void mergeSort(int[] arr) {if (arr == null || arr.length == 0){return;}mergeSort(arr, 0, arr.length-1);}/*** 归并排序** @param arr 待排序的数组* @param left* @param right*/private static void mergeSort(int[] arr, int left, int right) {if (left >= right){// 区间只剩一个元素,无需分解return;}int mid = (left + right) / 2;// 对左半部分进行递归排序mergeSort(arr, left, mid);// 对右半部分进行递归排序mergeSort(arr, mid + 1, right);// 合并左右两部分merge(arr, left, mid, right);}/*** 合并** @param arr* @param left* @param mid* @param right*/public static void merge(int[] arr, int left, int mid, int right) {// 临时数组用于存储合并后的结果int[] temp = new int[right - left + 1];// 左区间起始索引,用于遍历区间int i = left;// 右区间起始位置,用于遍历右区间int j = mid + 1;// 临时数组的起始索引,用于遍历临时数组int k = 0;// 遍历左右子区间,进行合并while (i <= mid && j <= right) {// 将左右区间中的较小值放入临时数组中if (arr[i] <= arr[j]) {temp[k++] = arr[i++];} else {temp[k++] = arr[j++];}}// 将左区间剩余元素复制到临时数组while (i <= mid) {temp[k++] = arr[i++];}// 将右区间剩余元素复制到临时数组while (j <= right) {temp[k++] = arr[j++];}// 将临时数组中的元素复制回原数组for (int m = 0; m < k; m++) {arr[left + m] = temp[m];}}
    

    备注:关于数组的合并数组拷贝,可以替换成System.arraycopy(temp, 0, arr, left, k),注意不能使用Arrays.copyof()

  • 方式二:自底向上的迭代实现

        /*** 归并排序** @param arr 待排序的数组*/private static void mergeSort(int[] arr) {if (arr == null || arr.length == 0){return;}int n = arr.length;// 遍历不同大小的子数组进行两两合并for (int i = 1; i < n; i *= 2) {for (int j = 0; j < n - i; j += i * 2) {merge(arr, j, j + i - 1, Math.min(j + i * 2 - 1, n - 1));}}}/*** 合并** @param arr* @param left* @param mid* @param right*/public static void merge(int[] arr, int left, int mid, int right) {// 临时数组用于存储合并后的结果int[] temp = new int[right - left + 1];// 左区间起始索引,用于遍历区间int i = left;// 右区间起始位置,用于遍历右区间int j = mid + 1;// 临时数组的起始索引,用于遍历临时数组int k = 0;// 遍历左右子区间,进行合并while (i <= mid && j <= right) {// 将左右区间中的较小值放入临时数组中if (arr[i] <= arr[j]) {temp[k++] = arr[i++];} else {temp[k++] = arr[j++];}}// 将左区间剩余元素复制到临时数组while (i <= mid) {temp[k++] = arr[i++];}// 将右区间剩余元素复制到临时数组while (j <= right) {temp[k++] = arr[j++];}// 将临时数组中的元素复制回原数组for (int m = 0; m < k; m++) {arr[left + m] = temp[m];}}
    

    可以看到两者基本上是类似的,特别是合并代码基本上是一模一样的,只是对于区间的划分不同,迭代对于区间的划分可能没有递归那么好理解,迭代:

            for (int i = 1; i < n; i *= 2) {for (int j = 0; j < n - i; j += i * 2) {merge(arr, j, j + i - 1, Math.min(j + i * 2 - 1, n - 1));}}
    
    1. 第一个循环是控制步长,也就是区间的大小,区间都是两两合并,所以i*=2
    2. 第二个循环是遍历起始索引,一个区间的元素是i*2,那么下一个区间的起始元素就是j+=i*2
    3. 但是对于最后一个区间,元素的个数可能不足,所以需要进行一个防止越界的判断,也就是Math.min(j + i * 2 - 1, n - 1)
  • 方式三:原地归并排序

        /*** 归并排序** @param arr 待排序的数组*/private static void mergeSort(int[] arr) {if (arr == null || arr.length == 0){return;}mergeSort(arr, 0, arr.length-1);}/*** 归并排序** @param arr 待排序的数组* @param left* @param right*/private static void mergeSort(int[] arr, int left, int right) {if (left >= right){// 区间只剩一个元素,无需分解return;}int mid = (left + right) / 2;// 对左半部分进行递归排序mergeSort(arr, left, mid);// 对右半部分进行递归排序mergeSort(arr, mid + 1, right);// 合并左右两部分merge(arr, left, mid, right);}/*** 合并** @param arr* @param left* @param mid* @param right*/public static void merge(int[] arr, int left, int mid, int right) {// 左区间起始索引,用于遍历区间int i = left;// 右区间起始位置,用于遍历右区间int j = mid + 1;// 遍历左右子区间,进行合并while (i <= mid && j <= right) {if (arr[i] > arr[j]) {// 将右半部分的元素移动到左半部分的末尾int temp = arr[j];for (int k = j; k > i; k--) {arr[k] = arr[k - 1];}arr[i] = temp;// 更新指针和中间位置mid++;j++;}i++;}}
    

🌵2.5 非比较排序

非比较排序是不需要将记录进行两两比较的

  • 非比较排序的特点
    • 不依赖比较操作:非比较排序算法不基于元素之间的比较操作,而是利用其他技巧实现排序。常见的非比较排序算法有计数排序、桶排序和基数排序。
    • 时间复杂度:非比较排序算法通常具有较好的时间复杂度。计数排序、桶排序和基数排序的平均和最坏时间复杂度都为O(n+k),其中k是数据的取值范围。
    • 适用性局限:非比较排序算法通常对输入数据有一定的限制,比如要求数据是整数或字符串,并且取值范围不过大。在满足条件的情况下,非比较排序算法可以获得更好的性能。
    • 稳定性:非比较排序算法中的计数排序和基数排序是稳定的,而桶排序在某些实现方式下可以是稳定的。
🐳2.5.1 计数排序
算法介绍

计数排序(Counting Sort)是一种线性时间复杂度的排序算法,适用于一定范围内的整数排序。它不基于比较,而是通过统计每个元素出现的次数,然后根据统计信息将元素排列在正确的位置上。

  • 实现步骤

    • Step1计算max、min、range。找出待排序数组中的最大值max和最小值min,确定统计数组的大小range。
    • Step2构造count数组。①记录每个元素出现的次数。遍历待排序数组,统计每个元素的出现次数,并将统计结果保存到count数组中。②求前缀和。计算每个元素在排序结果中的位置,即前缀和,使得count[i]表示小于等于元素i的元素个数。
    • Step3构造temp数组。创建一个与待排序数组大小相同的辅助数组temp,用于存储排序结果。
    • Step4映射元素。从后向前遍历待排序数组,根据count数组确定每个元素在排序结果中的位置,并将元素放入temp数组中。
    • Step5拷贝。将temp数组复制回原始数组,完成排序。
  • 示意图

    img

    image-20231001195111358

算法实现
    /*** 计数排序** @param arr 待排序的数组*/public static void countingSort(int[] arr) {int n = arr.length;// 获取数组中的最大值和最小值int max = Arrays.stream(arr).max().getAsInt();int min = Arrays.stream(arr).min().getAsInt();// 计算出数据的范围int range = max - min + 1;// 计数数组,用于记录每一个元素出现的次数int[] count = new int[range];// 结果数组,用于临时存储排序的结果int[] temp = new int[n];// 统计每个元素出现的次数for (int num : arr) {// num-min的目的是为了缩小范围,范围从0开始,避免浪费空间count[num - min]++;}// 计算前缀和for (int i = 1; i < range; i++) {count[i] += count[i - 1];}// 根据count数组确定每个元素在排序结果中的位置for (int i = n - 1; i >= 0; i--) {// 通过索引定位到对应的元素,让后将对应的元素映射到对应的位置int index = count[arr[i] - min] - 1;temp[index] = arr[i];// 计数器数组对应索引的元素已经被用过一次了,计数-1count[arr[i] - min]--;}// 将临时数组复制回原始数组System.arraycopy(temp, 0, arr, 0, n);}

count数组是核心

  1. count的索引-1 , arr[i] - min-1,对应的 arr 元素的索引,减一的是因为索引是从0开始的
  2. count的值count[index]对应的是当前元素前还有多少个元素
🐳.5.2 桶排序
算法介绍

桶排序(Bucket Sort)是一种线性时间复杂度的排序算法,它通过将待排序元素分布到不同的桶中,并对每个桶内的元素进行排序,最后按照桶的顺序将各个桶中的元素依次合并得到有序结果。

  • 实现步骤

    • Step1确定桶的数量和范围。根据待排序数据的特点,确定需要的桶的数量。一般来说,桶的数量可以根据数据量的大小进行设置,范围可以根据数据的最大值和最小值来计算。
    • Step2将待排序数组元素分配到不同的桶中。遍历待排序数组,根据元素的大小将其放入相应的桶中。
    • Step3对每个桶内的元素进行排序。可以使用其他排序算法(如插入排序、快速排序等)对每个非空桶进行排序,或者递归地使用桶排序对每个非空桶进行排序。
    • Step4合并桶内的元素。将每个非空桶中的元素按照顺序依次合并到一个临时数组中。
  • 示意图

    image-20231001201836047

算法实现
  • 方式一:基于Collections.sort()实现的桶排序

    这种方式严格意义来说可能属于比较排序,因为对于桶中的元素是直接使用了其它的比较排序算法,比如:插入排序、快速排序等等方式,通过将桶中的元素排序完之后再进行合并

         /*** 桶排序** @param arr 待排序的数组*/public static void bucketSort(int[] arr) {int n = arr.length;if (n == 0) {return;}// 计算最小值和最大值int min = arr[0];int max = arr[0];for (int num : arr) {if (num < min) {min = num;} else if (num > max) {max = num;}}// 计算桶的数量 = (最大元素-最小元素) / 桶的大小 + 1int bucketCount = (max - min) / n + 1;// 创建桶并将元素分配到桶中ArrayList<ArrayList<Integer>> buckets = new ArrayList<>(bucketCount);for (int i = 0; i < bucketCount; i++) {buckets.add(new ArrayList<>());}for (int num : arr) {int bucketIndex = (num - min) / n;buckets.get(bucketIndex).add(num);}// 对每个非空桶进行排序,然后合并结果int index = 0;for (ArrayList<Integer> bucket : buckets) {if (bucket.isEmpty()) {continue;}// 对桶中的元素进行排序(可以使用其他排序算法对桶内元素进行排序)Collections.sort(bucket);for (int num : bucket) {arr[index++] = num;}}}
    
  • 方式二:基于递归实现的桶排序(自顶向上3的递归)

    这种方式算是严格意义上的桶排序了,完全没有进行元素之间的比较,而是直接通过不断将元素划分成不同的桶,最终划分成一个元素之后,所有的元素都按照一定顺序分布在不同的桶中,最后再进行合并即可完成排序

    备注:其实之前的计数排序本质也是桶排序,它是自底向上4的桶排序

        /*** 桶排序** @param arr 待排序的数组*/public static void bucketSort(int[] arr){bucketSort(arr, arr.length);}/*** 桶排序* @param arr 待排序的数组* @param bucketCount 桶的数量*/public static void bucketSort(int[] arr, int bucketCount) {if (arr == null || arr.length == 0) {return;}// 查找最小值和最大值(这种方式比 stream 流性能更好)int min = Integer.MAX_VALUE;int max = Integer.MIN_VALUE;for (int num : arr) {min = Math.min(min, num);max = Math.max(max, num);}// 计算桶的大小和范围int bucketSize = (max - min) / bucketCount + 1;// 创建桶列表List<List<Integer>> buckets = new ArrayList<>();for (int i = 0; i < bucketCount; i++) {buckets.add(new ArrayList<>());}// 分配元素到不同的桶中for (int num : arr) {int index = (num - min) / bucketSize;buckets.get(index).add(num);}// 递归地对每个非空桶进行排序for (List<Integer> bucket : buckets) {if (!bucket.isEmpty()) {int[] bucketArr = bucket.stream().mapToInt(Integer::intValue).toArray();bucketSort(bucketArr, bucketCount);// 将排序后的结果放回桶中for (int i = 0; i < bucketArr.length; i++) {bucket.set(i, bucketArr[i]);}}}// 合并桶的结果得到最终的有序数组int index = 0;for (List<Integer> bucket : buckets) {for (int num : bucket) {arr[index++] = num;}}}
    
🐳2.5.3 基数排序
算法介绍

基数排序(Radix Sort)是一种非比较性的排序算法,它根据元素的位数进行排序。基数排序将待排序的元素按照个位、十位、百位等位数进行分组,并依次对每个位数进行稳定的排序操作,最终得到有序的结果。

  • 实现步骤

    • Step1:确定待排序数组中的最大值,并确定其位数。这将用于确定排序的轮数。
    • Step2:创建 10 个桶,每个桶对应一个数字(0-9)。
    • Step3:从最低有效位(个位)开始,按照当前位的值将所有元素分配到相应的桶中。
    • Step4:将各个桶中的元素按照顺序合并为一个新的数组。
    • Step5:重复步骤 3 和步骤 4,直到遍历完最高有效位(最高位)。
  • 示意图

    image-20231002002914010

算法实现
    /*** 基数排序** @param arr 待排序的数组*/public static void radixSort(int[] arr) {if (arr == null || arr.length == 0){return;}// 获取数组中的最大值int max = Integer.MIN_VALUE;for (int num : arr) {max = Math.max(max, num);}// 确定最大值的位数int digits = (int) Math.log10(max) + 1;// 创建 10 个桶List<List<Integer>> buckets = new ArrayList<>();for (int i = 0; i < 10; i++) {buckets.add(new ArrayList<>());}// 进行基数排序for (int i = 0; i < digits; i++) {// 分配到桶中for (int num : arr) {// 计算num的第(i+1)位上的数值,并将该数存入对应的桶中int digit = (num / (int) Math.pow(10, i)) % 10;buckets.get(digit).add(num);}// 合并桶中的元素int index = 0;for (List<Integer> bucket : buckets) {for (int num : bucket) {arr[index++] = num;}bucket.clear();}}}

注意:进行桶合并时,一定要按照索引顺序从前往后遍历获取元素,因为通过一位的合并,最小的先入桶,也就是桶中的元素索引越小,值越小

备注:如果想要降序排序,只需要将合并桶元素的代码修改为:

            for (int j = buckets.size() - 1; j >= 0; j--) {List<Integer> bucket = buckets.get(j);for (int k = bucket.size() - 1; k >= 0; k--) {arr[index++] = bucket.get(k);}bucket.clear();}

🌳总结

image-20221013211454011

备注

  • n:数据规模,也就是参与排序的元素个数
  • k:"桶"的个数
  • In-place:占用常数内存,不占用额外内存
  • Out-place:占用额外内存

img

其中最为常见的排序算法有:快速排序归并排序

以上是对十大常见排序算法的一个基础讲解,关于十大排序算法的水还是很深的,其中有很多的扩展和变种,要想完全吃透十大常见的排序算法还需要进一步学习,感兴趣的可以直接到 LeetCode 上搜索对应的排序算法题进行专项练习,一次来提高自己对这十大排序算法的熟练度,以及更加深入的理解


参考文章

  • 数据结构(Java语言版,雷军环、吴名星编著)
  • 十大经典排序算法
  • 十大经典排序,你全都会了吗?(附源码、动图、万字详解)
  • 十大经典排序算法(动图演示)
  • 堆排序(完全二叉树)最后一个非叶子节点的序号是n/2-1的原因 - 为了得到而努力 - 博客园 (cnblogs.com)
  • 【算法总结】快速排序及边界问题分析_快速排序边界分析_Ethan-Code的博客-CSDN博客
  • 三分钟搞懂桶排序 - 知乎 (zhihu.com)

在此致谢(^_^ゞ


  1. 内存也称主存,是计算机的内部存储器,是CPU与外存沟通的桥梁 ↩︎

  2. 外存也称辅存,是计算机的外部存储器,常见的U盘、移动硬盘都是外存 ↩︎

  3. 自顶向下:自顶向下的方法是从原始问题开始,通过将问题分解为更小的子问题,并递归地解决这些子问题,最终得到整体的解。在这种方法中,我们先处理原始问题,然后逐步地将问题分解为更小的子问题,并通过递归调用解决这些子问题。自顶向下的方法通常使用递归的方式来实现 ↩︎

  4. 自底向上:自底向上的方法是从解决最基本、最小规模的子问题开始,逐步合并得到整体的解。在这种方法中,我们先处理较小规模的子问题,然后将子问题的解合并起来,直到达到原始问题的解。自底向上的方法通常使用迭代或者循环的方式来实现 ↩︎

相关文章:

十大常见排序算法详解(附Java代码实现和代码解析)

文章目录 十大排序算法⛅前言&#x1f331;1、排序概述&#x1f334;2、排序的实现&#x1f335;2.1 插入排序&#x1f433;2.1.1 直接插入排序算法介绍算法实现 &#x1f433;2.1.2 希尔排序算法介绍算法实现 &#x1f335;2.2 选择排序&#x1f433;2.2.1 选择排序算法介绍算…...

在Ubuntu上通过Portainer部署微服务项目

这篇文章主要记录自己在ubuntu上部署自己的微服务应用的过程&#xff0c;文章中使用了docker、docker-compose和portainer&#xff0c;在部署过程中遇到了不少问题&#xff0c;因为博主也是初学docker-compose&#xff0c;通过这次部署实战确实有所收获&#xff0c;在这篇文章一…...

软件测试基础学习

注意&#xff1a; 各位同学们&#xff0c;今年本人求职目前遇到的情况大体是这样了&#xff0c;开发太卷&#xff0c;学历高的话优势非常的大&#xff0c;公司会根据实际情况考虑是否值得培养&#xff08;哪怕技术差一点&#xff09;&#xff1b;学历稍微低一些但是技术熟练的…...

移动手机截图,读取图片尺寸

这个代码的设计初衷是为了解决图片处理过程中的一些痛点。想象一下&#xff0c;我们都曾遇到过这样的情况&#xff1a;相机拍摄出来的照片、网络下载的图片&#xff0c;尺寸五花八门&#xff0c;大小不一。而我们又渴望将它们整理成一套拥有统一尺寸的图片&#xff0c;让它们更…...

服务器应用程序不可用的原因是什么引起的

服务器应用程序不可用的原因是什么引起的 服务器应用程序不可用的原因是什么引起的?其实服务器应用程序不可用可能是由多种原因引起的。主要包括软件故障、网络问题、硬件故障、安全问题、配置错误、容量不足、数据库问题等&#xff0c;具体详细服务器应用程序不可用的原因如下…...

使用SPY++查看窗口信息去排查客户端UI软件问题

目录 1、使用SPY++查看窗口的信息 2、使用SPY++查看某些软件UI窗口用什么UI组件实现的...

Flink CDC MySQL同步MySQL错误记录

1、启动 Flink SQL [appuserwhtpjfscpt01 flink-1.17.1]$ bin/sql-client.sh2、新建源表 问题1&#xff1a;Encountered “(” 处理方法&#xff1a;去掉int(11)&#xff0c;改为int Flink SQL> CREATE TABLE t_user ( > uid int(11) NOT NULL AUTO_INCREMENT COMME…...

深入了解 Linux 中的 AWK 命令:文本处理的瑞士军刀

简介 在Linux和Unix操作系统中&#xff0c;文本处理是一个常见的任务。AWK命令是一个强大的文本处理工具&#xff0c;专门进行文本截取和分析&#xff0c;它允许你在文本文件中查找、过滤、处理和格式化数据。本文将深入介绍Linux中的AWK命令&#xff0c;让你了解其基本用法和…...

【RuoYi项目分析】网关的AuthFilter完成“认证”,注意是认证而不是权限

文章目录 1. 功能介绍2. AuthFilter的配置3. AuthFilter实现分析4. 资料参考 过滤器的功能是检验经过网关的每一个请求&#xff0c;检查 token 中的信息是否有效。 注意是“认证检查”&#xff0c;而不是“权限” 1. 功能介绍 1、在用户完成登录后&#xff0c;程序会把用户相关…...

excel将文件夹下面的表格文件指定名称的sheet批量导出到指定文件中,并按照文件名保存在新文件的不同sheet中

excel将文件夹下面的表格文件指定名称的sheet批量导出到指定文件中&#xff0c;并按照文件名保存在新文件的不同sheet中 import pandas as pd import ositems os.listdir("./") sheetname"" for item in items:if item.__contains__(xls):dfpd.read_exc…...

IIS管理器无法打开。启动后,在任务栏中有,但是窗口不见了

找到IIS管理器启动程序的所在位置 并在cmd命令行中调用 inetmgr.exe /reset 进行重启 先查看IIS管理器属性&#xff0c;找到其位置 管理员模式打开cmd命令行&#xff0c;并切换到上面的文件夹下运行Inetmgr.exe /reset 运行完成后可以重新看到IIS窗口 原因&#xff1a;由于某…...

使用VBA实现快速模糊查询数据

实例需求&#xff1a;基础数据保存在Database工作表中&#xff0c;如下图所示。 基础数据有37个字段&#xff0c;上图仅展示部分字段内容&#xff0c;下图中黄色字段为需要提取的数据字段。 在Search工作表B1单元格输入查询关键字Title和Genre字段中搜索关键字&#xff0c;包…...

spring boot flowable多人前加签

1、前加签插件 package com.xxx.flowable.cmd;import com.xxx.auth.security.user.SecurityUser; import com.xxx.commons.ApplicationContextHolder; import com.google.common.collect.Lists; import org.apache.commons.collections.CollectionUtils; import org.apache.co…...

结构体运算符重载

1.降序 struct Point{int x, y;//重载比较符bool operator < (const Point &a) const{return x > a.x;//当前元素大时&#xff0c;是降序} };2.升序 struct Point{int x, y;//重载比较符 // bool operator < (const Point &a) const{ // return x…...

幽默直观的文档作者注释

注释是程序中非常重要的一部分&#xff0c;在不同的编程语言中&#xff0c;注释的风格和语言描述会有所不同。以下是一些常用的注释风格和语言描述&#xff1a; 直观注释&#xff1a;这种注释使用简洁、明了的语言&#xff0c;帮助读者快速地理解代码。例如&#xff0c;代码中…...

前端开发网站推荐

每个人都会遇见那么一个人&#xff0c;永远无法忘却&#xff0c;也永远不能拥有。 以下是一些可以用来查找和比较前端框架的推荐网站&#xff1a; JavaScript框架比较&#xff1a; 这些网站提供了对不同JavaScript框架和库的详细比较和评估。 JavaScripting: 提供了大量的JavaS…...

【C语言】通讯录管理系统(保姆级教程+内含源码)

C系列文章目录 目录 C系列文章目录 前言 一&#xff0c;模块化编程 二&#xff0c;系统框架构建 1.成员信息的创建 2.菜单实现 3.系统功能声明 三、系统功能实现 1.初始化通讯录 2.增加联系人 3.显示所有联系人 4.根据姓名查找位置 5.删除指定联系人 6.查找指定联…...

python自动解析301、302重定向链接

嗨喽~大家好呀&#xff0c;这里是魔王呐 ❤ ~! python更多源码/资料/解答/教程等 点击此处跳转文末名片免费获取 使用模块requests 方式代码如下&#xff1a; import requests url_string"http://******" r requests.head(url_string, streamTrue) print r.h…...

【未解决问题】opencv 交叉编译 ffmpeg选项始终为NO

opencv 打不开视频的原因 在交叉编译时候&#xff0c;发现在 pc 端能用 opencv 打开的视频&#xff0c;但是在 rv1126 上打不开。在网上查了很久&#xff0c;原因可能是 ffmpeg 造成的。 解决opencv源代码编译找不到ffmpeg-CSDN博客 交叉编译 ffmpeg 尝试了一天还是第二个博客…...

Python实用技术二:数据分析和可视化(2)

目录 一&#xff0c;多维数组库numpy 1&#xff0c;操作函数&#xff1a;​ 2&#xff0c;numpy数组元素增删 1&#xff09;添加数组元素 2&#xff09;numpy删除数组元素 3&#xff09;在numpy数组中查找元素 4&#xff09;numpy数组的数学运算 3&#xff0c;numpy数…...

内存分配函数malloc kmalloc vmalloc

内存分配函数malloc kmalloc vmalloc malloc实现步骤: 1)请求大小调整:首先,malloc 需要调整用户请求的大小,以适应内部数据结构(例如,可能需要存储额外的元数据)。通常,这包括对齐调整,确保分配的内存地址满足特定硬件要求(如对齐到8字节或16字节边界)。 2)空闲…...

【根据当天日期输出明天的日期(需对闰年做判定)。】2022-5-15

缘由根据当天日期输出明天的日期(需对闰年做判定)。日期类型结构体如下&#xff1a; struct data{ int year; int month; int day;};-编程语言-CSDN问答 struct mdata{ int year; int month; int day; }mdata; int 天数(int year, int month) {switch (month){case 1: case 3:…...

【力扣数据库知识手册笔记】索引

索引 索引的优缺点 优点1. 通过创建唯一性索引&#xff0c;可以保证数据库表中每一行数据的唯一性。2. 可以加快数据的检索速度&#xff08;创建索引的主要原因&#xff09;。3. 可以加速表和表之间的连接&#xff0c;实现数据的参考完整性。4. 可以在查询过程中&#xff0c;…...

Android15默认授权浮窗权限

我们经常有那种需求&#xff0c;客户需要定制的apk集成在ROM中&#xff0c;并且默认授予其【显示在其他应用的上层】权限&#xff0c;也就是我们常说的浮窗权限&#xff0c;那么我们就可以通过以下方法在wms、ams等系统服务的systemReady()方法中调用即可实现预置应用默认授权浮…...

用docker来安装部署freeswitch记录

今天刚才测试一个callcenter的项目&#xff0c;所以尝试安装freeswitch 1、使用轩辕镜像 - 中国开发者首选的专业 Docker 镜像加速服务平台 编辑下面/etc/docker/daemon.json文件为 {"registry-mirrors": ["https://docker.xuanyuan.me"] }同时可以进入轩…...

Maven 概述、安装、配置、仓库、私服详解

目录 1、Maven 概述 1.1 Maven 的定义 1.2 Maven 解决的问题 1.3 Maven 的核心特性与优势 2、Maven 安装 2.1 下载 Maven 2.2 安装配置 Maven 2.3 测试安装 2.4 修改 Maven 本地仓库的默认路径 3、Maven 配置 3.1 配置本地仓库 3.2 配置 JDK 3.3 IDEA 配置本地 Ma…...

蓝桥杯 冶炼金属

原题目链接 &#x1f527; 冶炼金属转换率推测题解 &#x1f4dc; 原题描述 小蓝有一个神奇的炉子用于将普通金属 O O O 冶炼成为一种特殊金属 X X X。这个炉子有一个属性叫转换率 V V V&#xff0c;是一个正整数&#xff0c;表示每 V V V 个普通金属 O O O 可以冶炼出 …...

Spring是如何解决Bean的循环依赖:三级缓存机制

1、什么是 Bean 的循环依赖 在 Spring框架中,Bean 的循环依赖是指多个 Bean 之间‌互相持有对方引用‌,形成闭环依赖关系的现象。 多个 Bean 的依赖关系构成环形链路,例如: 双向依赖:Bean A 依赖 Bean B,同时 Bean B 也依赖 Bean A(A↔B)。链条循环: Bean A → Bean…...

智能AI电话机器人系统的识别能力现状与发展水平

一、引言 随着人工智能技术的飞速发展&#xff0c;AI电话机器人系统已经从简单的自动应答工具演变为具备复杂交互能力的智能助手。这类系统结合了语音识别、自然语言处理、情感计算和机器学习等多项前沿技术&#xff0c;在客户服务、营销推广、信息查询等领域发挥着越来越重要…...

前端中slice和splic的区别

1. slice slice 用于从数组中提取一部分元素&#xff0c;返回一个新的数组。 特点&#xff1a; 不修改原数组&#xff1a;slice 不会改变原数组&#xff0c;而是返回一个新的数组。提取数组的部分&#xff1a;slice 会根据指定的开始索引和结束索引提取数组的一部分。不包含…...