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

数据结构十大排序之(冒泡,快排,并归)

接上期:

数据结十大排序之(选排,希尔,插排,堆排)-CSDN博客

前言:

在计算机科学中,排序算法是最基础且最重要的算法之一。无论是大规模数据处理还是日常的小型程序开发,排序算法的效率直接影响到系统的性能表现。因此,了解和掌握常见的排序算法,对于每一个程序员来说,都具有重要的意义。

本文将对三种经典的排序算法——冒泡排序快速排序归并排序进行深入剖析。这三种算法在不同的应用场景下各有优势,分别代表了排序算法的不同发展阶段。从简单直观的冒泡排序到高效且复杂的快速排序,再到稳定且适用于大数据量的归并排序,我们将一一解析它们的工作原理、时间复杂度以及实际应用中的优缺点。通过本文的学习,读者不仅能够掌握这些排序算法的基本概念,还能更好地理解在特定问题中选择合适排序算法的重要性。

无论你是算法学习的初学者,还是在进行系统优化的开发者,相信本文都会为你提供有价值的参考。接下来,我们将逐一揭开这些经典排序算法的神秘面纱。

4 交换排序

基本思想:所谓交换,就是根据序列中两个记录键值的比较结果来对换这两个记录在序列中的位置,交换排序的特 点是:将键值较大的记录向序列的尾部移动,键值较小的记录向序列的前部移动。

4.1 冒泡排序

冒泡排序(Bubble Sort)是一种简单的排序算法,它通过重复地遍历排序的元素,比较相邻的元素并交换它们的位置,直到整个数组排序。冒泡排序的核心思想是通过元素之间的交换,将较大的元素“冒泡”到磁盘的终止,较小的元素“沉淀”到磁盘的迁移,最终达到排序的目的。

原理

冒泡排序的核心思想是:

  1. 从队列的开始位置,依次比较相邻的两个元素。
  2. 如果这两个元素的顺序不正确(例如,前一个元素大于后一个元素),就交换它们的位置。
  3. 每一个批次结束后,工厂的部分都会放置当前轮次中最大的元素。换句话说,更大的元素会“冒泡”到批次的终止。
  4. 每一个遍历减少比较的元素个数,因为遍历的元素已经是排序好了。
算法步骤
  1. 比较相邻的元素。如果第一个比第二个大,就交换他们两个。

  2. 对每一对相邻元素作同样的工作,从开始第一对到结尾的最后一对。这步做完后,最后的元素会是最大的数。

  3. 针对所有的元素重复以上的步骤,除了最后一个。

  4. 持续每次对越来越少的元素重复上面的步骤,直到没有任何一对数字需要比较。

private static void BubbleSort(int[] arr) {for (int i = 0; i < arr.length - 1; i++) {for (int j = 0; j < arr.length - 1 - i; j++) {if (arr[j] > arr[j + 1]) {swap(arr, j, j + 1);}}}
}
  • 外层循环( for (int i = 0; i < arr.length - 1; i++)):

    • 这个循环的作用是控制冒泡排序的遍历次数。每次遍历结束后,最大的元素就被“冒泡”到队列的最后面。因此,随着排序的进行,内层可以循环最少遍历一些元素。arr.length - 1次次遍历足够了,因为仓库最后一个元素自然会排到正确的位置。
  • 内层循环( for (int j = 0; j < arr.length - 1 - i; j++)):

    • 内层循环负责获取当前未排序部分的各个元素,进行比较和交换。每次内层循环将未排序部分的最大元素推到队列的最后(该部分会被外层循环控制,不再被内层循环控制)层循环处理)。
    • 注意:arr.length - 1 - i控制了内层循环的长度。随着外层循环的进行,内层循环每次遍历的范围逐渐减小,因为已排序的元素不再参与比较。
  • 比较与交换( if (arr[j] > arr[j + 1])):

    • 如果当前元素arr[j]大于其后面的元素arr[j + 1],则交换它们的位置。这样增加,新增的元素被“冒泡”到了队列的后面。

静态展示:

假设我们有一个仓库[5, 2, 9, 1, 5, 6],使用冒泡排序进行排序:

  1. 初始数组[5, 2, 9, 1, 5, 6]
  2. 第一轮(i = 0)
    • 比较52,交换位置:[2, 5, 9, 1, 5, 6]
    • 相比之下59不交换
    • 比较91,交换位置:[2, 5, 1, 9, 5, 6]
    • 比较95,交换位置:[2, 5, 1, 5, 9, 6]
    • 比较96,交换位置:[2, 5, 1, 5, 6, 9]
    • 第一次运输结束,最大的元素9已经搬运的搬运。
  3. 第二轮(i = 1)
    • 相比之下25不交换
    • 比较51,交换位置:[2, 1, 5, 5, 6, 9]
    • 相比之下55不交换
    • 相比之下56不交换
    • 第二次运输结束,最大的元素6已经搬运货物的支架。
  4. 第三轮(i = 2)
    • 比较21,交换位置:[1, 2, 5, 5, 6, 9]
    • 相比之下25不交换
    • 相比之下55不交换
    • 第三次旅程结束,最大的元素5已经搬运的搬运的。
  5. 第四轮(i = 3)
    • 相比之下21不交换
    • 由于没有进行任何交换,提前退出循环,排序结束。

最终数组为:[1, 2, 5, 5, 6, 9]

时间复杂度
  • 最坏情况时间复杂度:O(n²),当集群完全逆序时,冒泡排序需要进行n-1次轮巡,每轮遍历中需要进行n-1次比较和交换,因此总的时间复杂度为 O(n²)。

  • 最好情况时间复杂度:O(n),当阵列已经是集群的情况下,冒泡排序只需要进行一次遍历,不发生交换,算法会提前终止。因此,最好情况下的时间复杂度是 O(n)。

  • 平均情况时间复杂度:O(n²),大多数情况下,冒泡排序的时间复杂度为O(n²),因为每次遍历都需要比较和交换。

空间复杂度

因此冒泡排序是原地排序算法,它只需要升级的额外空间来交换元素,空间复杂度为O(1)

稳定性

冒泡排序是稳定的。在满足元素的情况下,冒泡排序不会改变它们的相对顺序。这是因为当两个元素一致时,冒泡排序不会进行交换操作。

优缺点

优点:

  • 实现简单:冒泡排序的实现非常简单,代码量少,容易理解。
  • 稳定排序:它是一个稳定的排序算法,相同元素的相对顺序不会改变。
  • 空间复杂度低:冒泡排序是原地排序算法,空间复杂度为O(1),不需要额外的内存空间。

缺点:

  • 效率低:冒泡排序的时间复杂度是O(n²),对于大规模数据,效率非常低。
  • 不适用于大规模数据排序:由于其时间复杂度较高,因此大规模数据时性能不如其他排序处理算法,如排序、归并排序等。
  • 改进空间有限:虽然可以通过设置标志位优化提前退出,但在实际应用中,它仍然比其他更高效的排序算法要慢。
适用场景
  • 小规模数据排序:由于冒泡排序的时间复杂度较高,它适用于数据量较小的排序任务。
  • 教学和演示:其算法简单易懂,冒泡排序常用于教学和排序算法的基本原理。
  • 已经部分小区的情况:如果阵列接近小​​区,冒泡排序会比其他复杂的排序算法(如快速排序、归并排序等)获得优势,因为它可以通过优化提前结束。

4.2 快排

快速排序(Quick Sort)是一种非常的排序算法,由计算机科学家高效托尼·霍尔(Tony Hoare)于 1960 年提出。它采用了分治法的思想,通过将一个大问题分解为几个小问题来解决。快速排序基本思想是:通过一个“基准”元素将待排序数据分为两个部分,使得一部分数据都比基准小,另一部分数据都比基准大,然后梯度地对这两部分分别进行排序。

原理

快速排序的核心思想是分治法,即通过选择一个“基准元素”将队列分成两部分:

  1. 选择基准元素:从仓库中选择一个元素作为基准元素(可以选择第一个元素、最后一个元素、中间元素或者随机选择)。
  2. 分区操作:通过一个排序趟将流量分成两个部分,左边部分的元素小于基准元素,右边部分的元素大于基准元素。基准元素末端位于其正确的位置。
  3. 梯度排序:梯度地对基准元素左边和右边的子队列进行快速排序。
算法步骤

4.2.1递归版

  1. 选择基准元素:从集群中选择一个基准元素,常见的策略有:
    • 选择第一个元素作为基准。
    • 选择最后一个元素作为基准。
    • 选择中间元素作为基准。
    • 随机选择一个元素作为基准。
  2. 操作:检索西藏,将比基准小元素移到左边,比基准大的元素移到右边,最终将基准元素移到正确的位置。
  3. 梯度排序:分别对左子队列和右子队列进行快速排序。

4.2.1.1 霍尔(Tony Hoare)法

private static void QuickSort(int[] arr) {
    int left = 0;
    int right = arr.length - 1;
    MyQuickSort(arr, left, right);
}

private static void MyQuickSort(int[] arr, int left, int right) {
    if (left < right) {
        int div = partition(arr, left, right);  // 获取分区后的基准元素位置
        MyQuickSort(arr, left, div - 1);   // 对基准元素左边的子数组进行排序
        MyQuickSort(arr, div + 1, right);  // 对基准元素右边的子数组进行排序
    }
}

private static int partition(int[] arr, int left, int right) {
    int i = left;
    int j = right;
    int pivot = arr[left];  // 选择基准元素为第一个元素
    while (i < j) {
        while (arr[j] >= pivot && i < j) {  // 向左找到比基准元素小的元素
            j--;
        }
        while (arr[i] <= pivot && i < j) {  // 向右找到比基准元素大的元素
            i++;
        }
        swap(arr, i, j);  // 交换
    }
    swap(arr, i, left);  // 将基准元素放到正确的位置
    return i;  // 返回基准元素的最终位置
}

private static void swap(int[] arr, int i, int j) {
    int temp = arr[i];
    arr[i] = arr[j];
    arr[j] = temp;
}

QuickSort方法 

作用:这是QuickSort的入口方法,初始化了两个指针leftright,并调用MyQuickSort方法来进行排序。left是数组的开始索引,right是数组的结束索引。

MyQuickSort方法

  • 分层终止条件if (left < right)检查了当前子队列的大小。如果left>= right,说明该部分队列只有一个元素或没有元素,结束递归。

  • 分区操作partition方法是返回基准元素的末端位置div,该位置右侧的元素都小于基准元素,右侧的元素都大于基准元素。然后分区地对左侧和右侧的子队列进行排序。

partition方法

  • 选择基准元素pivot被选择为队列的第一个元素(arr[left])。快速排序的一个特点是每次选择一个基准元素,通常有多种选择方式:第一个元素、最后一个元素、随机元素或者三个数取中法。这里选择了第一个元素作为基准元素。

  • 双指针法i从左边开始,j从右边开始,找到一个大于基准元素的arr[i]和小于基准元素的arr[j],并交换它们的位置,直到ij相遇。

  • 交换基准元素:当ij相遇时,交换基准元素和当前i的位置,将基准元素放到正确的位置。

  • 返回元素位置:返回值是i,即基准元素末端的位置。此时,基准元素左边的元素都比它小,右边的元素都比它大。

静态展示

假设我们有一个数组[10, 7, 8, 9, 1, 5],使用快速排序进行排序:

  1. 初始数组[10, 7, 8, 9, 1, 5]

  2. 第一轮分区操作

    • 作为5第一个基准。
    • 将小于5的元素放置在左边,大于5的元素放置在右边,最终[1, 7, 8, 9, 10, 5]元素5放置在正确的位置。
    • 基准要素5的位置是4,此时左子副本为[1],右子副本为[7, 8, 9, 10]
  3. 队列对左子队列[1]进行排序,由于只有一个元素,排序结束。

  4. 梯度对右子队列[7, 8, 9, 10]进行排序

    • 作为10第一个基准。
    • 分区操作后,重新安装[7, 8, 9, 10],基准元素10被放置在正确的位置。
    • 基准要素10的位置是3,此时左子副本为[7, 8, 9],右子副本为空。
  5. 层次对子吞吐量[7, 8, 9]进行排序

    • 作为9第一个基准。
    • 分区操作后,重新安装[7, 8, 9],基准元素9被放置在正确的位置。
    • 基准要素9的位置是2,此时左子副本为[7, 8],右子副本为空。
  6. 层次对子吞吐量[7, 8]进行排序

    • 作为8第一个基准。
    • 分区操作后,重新安装[7, 8],基准元素8被放置在正确的位置。
    • 基准要素8的位置是1,此时左子副本为[7],右子副本为空。

排序最终的后端为[1, 5, 7, 8, 9, 10]

4.2.1.2. 挖坑法

private static void QuickSort(int[] arr) {
    int left = 0;
    int right = arr.length - 1;
    MyQuickSort(arr, left, right);
}

private static void MyQuickSort(int[] arr, int left, int right) {
    if (left < right) {
        int div = partition(arr, left, right);  // 获取分区后的基准元素位置
        MyQuickSort(arr, left, div - 1);   // 对基准元素左边的子数组进行排序
        MyQuickSort(arr, div + 1, right);  // 对基准元素右边的子数组进行排序
    }
}

private static int partion(int[] arr, int left, int right) {
    int i = left;
    int j = right;
    int pivot = arr[left];  // 选择基准元素为第一个元素
    while (i < j) {
        // 向右扫描,找到小于基准元素的元素
        while (arr[j] >= pivot && i < j) {
            j--;
        }
        arr[i] = arr[j];  // 把小于基准元素的元素移动到左侧

        // 向左扫描,找到大于基准元素的元素
        while (arr[i] <= pivot && i < j) {
            i++;
        }
        arr[j] = arr[i];  // 把大于基准元素的元素移动到右侧
    }
    arr[i] = pivot;  // 将基准元素放置到正确的位置
    return i;  // 返回基准元素的位置
}

QuickSort方法 

作用:这是QuickSort的入口方法,初始化了两个指针leftright,并调用MyQuickSort方法来进行排序。left是数组的开始索引,right是数组的结束索引。

MyQuickSort方法

  • 分层终止条件if (left < right)检查了当前子队列的大小。如果left>= right,说明该部分队列只有一个元素或没有元素,结束递归。

  • 分区操作partition方法是返回基准元素的末端位置div,该位置右侧的元素都小于基准元素,右侧的元素都大于基准元素。然后分区地对左侧和右侧的子队列进行排序。

partition方法 

  • 作用partition 方法的目标是通过选择一个基准元素(pivot)并将其放置在正确的位置,来将数组分为两部分:一部分小于基准元素,另一部分大于基准元素。
  • 步骤
    1. 初始化:选择 arr[left] 作为基准元素 pivot
    2. 双指针扫描
      • 使用两个指针 ij,分别从数组两端向中间扫描。
      • 右扫描:指针 j 向左扫描,找到一个小于基准元素的元素。
      • 左扫描:指针 i 向右扫描,找到一个大于基准元素的元素。
      • 如果指针 i 小于指针 j,则交换 arr[i]arr[j],直到两个指针交错。
    3. 交换基准元素:当 ij 相遇时,将基准元素 pivot 放置在正确的位置,即 arr[i],这样基准元素左侧的所有元素都小于它,右侧的所有元素都大于它。
    4. 返回基准元素的位置:返回基准元素的位置 i,供递归调用进一步分区。
静态展示

初始数组:

[10, 7, 8, 9, 1, 5]

第一轮分区操作:

选择数组的第一个元素 5 作为基准元素(pivot)。

  • 步骤

    • 设置两个指针 iji 从左边开始,j 从右边开始。
    • 首先,j 指针向左移动,找到小于 5 的元素 1
    • 然后,i 指针向右移动,找到大于 5 的元素 10
    • 交换 ij 指针指向的元素,交换 101,得到数组:
      [1, 7, 8, 9, 10, 5]
      
    • 继续移动指针,重复上述步骤,直到 ij 相遇。
    • 最终,基准元素 5 被放置在正确的位置,即索引 4
  • 数组变更

    [1, 7, 8, 9, 10, 5]
    
  • 分区结果

    • 基准元素 5 放置在索引 4,此时数组分为两个部分:
      • 左子数组 [1]
      • 右子数组 [7, 8, 9, 10]

对左子数组 [1] 进行排序:

左子数组只有一个元素 [1],已经是有序的,因此不需要进一步排序。

对右子数组 [7, 8, 9, 10] 进行排序:

选择 10 作为基准元素。

  • 步骤

    • 设置 iji 从左边开始,j 从右边开始。
    • 首先,j 指针向左移动,找到小于 10 的元素 9
    • 然后,i 指针向右移动,找到大于 10 的元素(没有)。
    • 交换 ij 指针指向的元素,基准元素 10 被放置在正确位置,数组保持不变。
    • 结果:
      [7, 8, 9, 10]
      
  • 数组变更

    [7, 8, 9, 10]
    
  • 分区结果

    • 基准元素 10 放置在索引 3,此时数组分为两个部分:
      • 左子数组 [7, 8, 9]
      • 右子数组 [](为空)

对子数组 [7, 8, 9] 进行排序:

选择 9 作为基准元素。

  • 步骤

    • 设置 iji 从左边开始,j 从右边开始。
    • 首先,j 指针向左移动,找到小于 9 的元素 8
    • 然后,i 指针向右移动,找到大于 9 的元素(没有)。
    • 交换 ij 指针指向的元素,基准元素 9 被放置在正确位置,数组保持不变。
    • 结果:
      [7, 8, 9]
      
  • 数组变更

    [7, 8, 9]
    
  • 分区结果

    • 基准元素 9 放置在索引 2,此时数组分为两个部分:
      • 左子数组 [7, 8]
      • 右子数组 [](为空)

对子数组 [7, 8] 进行排序:

选择 8 作为基准元素。

  • 步骤

    • 设置 iji 从左边开始,j 从右边开始。
    • ij 都指向元素 78,没有交换的必要。
    • 基准元素 8 放置在正确位置,数组保持不变。
  • 数组变更

    [7, 8]
    
  • 分区结果

    • 基准元素 8 放置在索引 1,此时数组分为两个部分:
      • 左子数组 [7]
      • 右子数组 [](为空)

排序完成后的数组:

最终的有序数组是:

[1, 5, 7, 8, 9, 10]

4.2.1.3.左右指针法

private static void QuickSort(int[] arr) {
    int left = 0;
    int right = arr.length - 1;
    MyQuickSort(arr, left, right);
}

private static void MyQuickSort(int[] arr, int left, int right) {
    if (left < right) {
        int div = partition(arr, left, right);  // 获取分区后的基准元素位置
        MyQuickSort(arr, left, div - 1);   // 对基准元素左边的子数组进行排序
        MyQuickSort(arr, div + 1, right);  // 对基准元素右边的子数组进行排序
    }
}

private static int partion(int[] arr, int left, int right) {int prev = left;int cur = left + 1;while (cur <= right) {if(arr[cur] < arr[prev]) {swap(arr,prev,cur);}cur++;}swap(arr,prev,left);return prev;
}

private static void swap(int[] arr, int i, int j) {
    int temp = arr[i];
    arr[i] = arr[j];
    arr[j] = temp;
}

QuickSort方法 

作用:这是QuickSort的入口方法,初始化了两个指针leftright,并调用MyQuickSort方法来进行排序。left是数组的开始索引,right是数组的结束索引。

MyQuickSort方法

  • 分层终止条件if (left < right)检查了当前子队列的大小。如果left>= right,说明该部分队列只有一个元素或没有元素,结束递归。

  • 分区操作partition方法是返回基准元素的末端位置div,该位置右侧的元素都小于基准元素,右侧的元素都大于基准元素。然后分区地对左侧和右侧的子队列进行排序。

partition方法

  • partition 方法是快速排序中的核心部分,用来对数组进行分区,并返回基准元素的位置。
  • 这里选择了 arr[left] 作为基准元素(prev)。
  • prev 指向基准元素的位置,curprev + 1 开始,扫描数组:
    • 如果 arr[cur] 小于基准元素 arr[prev],则交换 arr[prev]arr[cur],使得比基准元素小的元素移到左边。
    • cur 继续向右扫描,直到扫描完整个数组。
  • 在扫描完成后,交换 arr[prev] 和基准元素位置 arr[left],将基准元素放置到正确的位置。
  • 最后,返回 prev,即基准元素的位置。

 静态展示

假设我们有数组 [10, 7, 8, 9, 1, 5],我们将逐步展示排序过程:

  1. 初始数组:[10, 7, 8, 9, 1, 5]

  2. 第一次分区:

    • 选择基准元素:pivot = 10

    • 扫描过程中,交换了元素,使得数组变为:[5, 7, 8, 9, 1, 10]

    • 基准元素 10 被放置在正确位置,基准位置返回 5

    • 左子数组:[5, 7, 8, 9, 1],右子数组:[]

  3. 左子数组排序 [5, 7, 8, 9, 1]

    选择基准元素:pivot = 5

    • 扫描过程中,交换了元素,使得数组变为:[1, 7, 8, 9, 5]

    • 基准元素 5 被放置在正确位置,基准位置返回 0

    • 左子数组:[1],右子数组:[7, 8, 9]

  4. 左子数组 [1] 排序:

    • 只有一个元素,排序结束。
  5. 右子数组 [7, 8, 9] 排序:

    选择基准元素:pivot = 7

    • 扫描过程中,交换了元素,使得数组变为:[7, 8, 9]

    • 基准元素 7 被放置在正确位置,基准位置返回 1

    • 左子数组:[],右子数组:[8, 9]

  6. 右子数组 [8, 9] 排序:

    • 选择基准元素:pivot = 8
    • 基准元素 8 被放置在正确位置,排序结束。
  7. 最终结果:

    • 排序完成后,数组为:[1, 5, 7, 8, 9, 10]

4.2.2 非递归版

public static void quickNor(int[] array) {int start = 0;int end = array.length-1;Deque<Integer> stack = new ArrayDeque<>();int pivot = partion(array,start,end);if(pivot > start + 1) {stack.push(start);stack.push(pivot-1);}if(pivot < end-1) {stack.push(pivot+1);stack.push(end);}while (!stack.isEmpty()) {end = stack.pop();start = stack.pop();pivot = partion(array,start,end);if(pivot > start + 1) {stack.push(start);stack.push(pivot-1);}if(pivot < end-1) {  stack.push(pivot+1);stack.push(end);}}
}
private static int partion(int[] arr, int left, int right) {int i = left;int j = right;int pivot = arr[left];while (i < j) {while (arr[j] >= pivot && i < j) {j--;}while (arr[i] <= pivot && i < j) {i++;}swap(arr,i,j);}swap(arr,i,left);return i;
}

private static void swap(int[] arr, int i, int j) {

        int temp = arr[i];

        arr[i] = arr[j];

        arr[j] = temp;

}

该代码包含两个主要部分:

  1. quickNor:快速排序的非递归实现。
  2. partition:分区操作,用于将数组分成两个子数组,并返回基准元素的最终位置。

quickNor 方法分析

public static void quickNor(int[] array) {int start = 0;int end = array.length - 1;Deque<Integer> stack = new ArrayDeque<>();int pivot = partion(array, start, end);if (pivot > start + 1) {stack.push(start);stack.push(pivot - 1);}if (pivot < end - 1) {stack.push(pivot + 1);stack.push(end);}while (!stack.isEmpty()) {end = stack.pop();start = stack.pop();pivot = partion(array, start, end);if (pivot > start + 1) {stack.push(start);stack.push(pivot - 1);}if (pivot < end - 1) {stack.push(pivot + 1);stack.push(end);}}
}

1. 初始化

int start = 0;
int end = array.length - 1;
Deque<Integer> stack = new ArrayDeque<>();
  • startend 代表数组的左右边界(即待排序区间)。
  • stack 是一个双端队列(Deque),用来存储子数组的左右边界,模拟递归调用栈。

2. 第一次分区操作

int pivot = partion(array, start, end);
  • 在非递归实现中,首先对整个数组进行一次分区操作,通过调用 partition 方法,获取基准元素的最终位置 pivot

3. 将左右子数组压入栈

if (pivot > start + 1) {stack.push(start);stack.push(pivot - 1);
}
if (pivot < end - 1) {stack.push(pivot + 1);stack.push(end);
}
  • 分区操作后,基准元素已经被放置在它的正确位置。接着,将基准元素左右两边的子数组的边界压入栈中,准备对它们进行排序。
    • 如果左边子数组(startpivot - 1)有多于一个元素,就将其压入栈中。
    • 如果右边子数组(pivot + 1end)有多于一个元素,也将其压入栈中。

4. 非递归排序过程

while (!stack.isEmpty()) {end = stack.pop();start = stack.pop();pivot = partion(array, start, end);if (pivot > start + 1) {stack.push(start);stack.push(pivot - 1);}if (pivot < end - 1) {stack.push(pivot + 1);stack.push(end);}
}
  • 使用 while 循环来模拟递归的过程。每次从栈中弹出一对 startend,即一个待排序子数组的左右边界。
  • 对该子数组执行分区操作,获得基准元素的最终位置 pivot
  • 如果基准元素左边有子数组(startpivot - 1),并且左边子数组长度大于 1,则将其左右边界压入栈中。
  • 如果基准元素右边有子数组(pivot + 1end),并且右边子数组长度大于 1,则将其左右边界压入栈中。
  • 继续循环,直到栈为空,排序完成。

partition 方法分析

private static int partion(int[] arr, int left, int right) {int i = left;int j = right;int pivot = arr[left]; // 选择基准元素为第一个元素while (i < j) {while (arr[j] >= pivot && i < j) {j--;}while (arr[i] <= pivot && i < j) {i++;}swap(arr, i, j);  // 交换}swap(arr, i, left);  // 将基准元素放置到正确的位置return i;
}

1. 基准元素选择

int pivot = arr[left];
  • 选择数组的第一个元素作为基准元素。

2. 双指针扫描

while (i < j) {while (arr[j] >= pivot && i < j) {j--;}while (arr[i] <= pivot && i < j) {i++;}swap(arr, i, j);
}
  • 使用两个指针 ij,分别从数组的两端向中间扫描。
  • i 向右移动,直到找到一个比基准元素大的元素;j 向左移动,直到找到一个比基准元素小的元素。
  • 然后交换 ij 指向的元素,继续扫描。

3. 基准元素放置

swap(arr, i, left);
  • 当两个指针相遇时,ij 就是基准元素应该放置的位置。
  • 将基准元素放置到 i(或 j)的位置,并返回 i(或 j)作为新的基准元素的位置。

swap 方法

private static void swap(int[] arr, int i, int j) {int temp = arr[i];arr[i] = arr[j];arr[j] = temp;
}
  • 交换数组中的两个元素。
优点:
  • 非递归:避免了递归调用栈的开销,适合于栈空间较小或者深度较大的情况下使用。
  • 高效:时间复杂度在平均情况下为 O(n log n),最坏情况下为 O(n^2),但通过改进基准元素的选择可以优化到 O(n log n)
  • 原地排序:快速排序是原地排序算法,空间复杂度低。
缺点:
  • 最坏时间复杂度:当基准元素选择不当时,快速排序可能会退化为 O(n^2),例如在已排序或逆序数组上使用时。
  • 不稳定:快速排序不是稳定的排序算法,可能会改变相等元素的相对顺序。
静态展示

假设我们有一个数组 [10, 7, 8, 9, 1, 5],使用快速排序的非递归版本进行排序。接下来,我们展示分区过程和栈的变化过程。

第一次分区操作:

  • 选择 10 为基准元素(pivot = arr[left])。
  • 执行 partition 方法,经过扫描和交换后,基准元素 10 被放置到正确位置。
  • 基准元素的位置 pivot = 5(索引 5)。

栈操作

  • 将左子数组(start = 0, end = 4)和右子数组(start = 6, end = 5)的边界压入栈中。
  • 栈内容:[0, 4, 6, 5]

2. 第二次分区操作(左子数组 [10, 7, 8, 9, 1]):

  • 选择 10 为基准元素,经过分区操作后,基准元素 10 已经被放置在索引 4。
  • 基准元素的位置 pivot = 4

栈操作

  • 对左子数组 [10, 7, 8, 9, 1] 继续分区(start = 0, end = 3)。
  • 对右子数组 [5],由于长度为 1,不需要处理。

栈内容[0, 3, 6, 5]

3. 第三次分区操作(子数组 [10, 7, 8, 9]):

  • 选择 10 为基准元素,经过分区操作后,基准元素 10 已经被放置在索引 3。
  • 基准元素的位置 pivot = 3

栈操作

  • 对左子数组 [10, 7, 8] 继续分区(start = 0, end = 2)。
  • 对右子数组 [9],由于长度为 1,不需要处理。

栈内容[0, 2, 6, 5]

4. 第四次分区操作(子数组 [10, 7, 8]):

  • 选择 10 为基准元素,经过分区操作后,基准元素 10 已经被放置在索引 2。
  • 基准元素的位置 pivot = 2

栈操作

  • 对左子数组 [10, 7] 继续分区(start = 0, end = 1)。
  • 对右子数组 [8],由于长度为 1,不需要处理。

栈内容[0, 1, 6, 5]

5. 第五次分区操作(子数组 [10, 7]):

  • 选择 10 为基准元素,经过分区操作后,基准元素 10 已经被放置在索引 1。
  • 基准元素的位置 pivot = 1

栈操作

  • 对左子数组 [10],由于长度为 1,不需要处理。
  • 对右子数组 [7],由于长度为 1,不需要处理。

栈内容[6, 5]

6. 结束

  • 当栈为空时,排序结束,最终的数组是:[1, 5, 7, 8, 9, 10]。
时间复杂度
  • 最坏时间复杂度O(n^2)

    • 在最坏的情况下(例如每次选择的所有pivot阵列中最大或最小的元素),快速排序的队列深度将为n,每次分区的时间复杂度为O(n),因此总体时间复杂度为O(n^2)
    • 这种情况通常发生在数据已经部分社区或完全逆序的情况下。
  • 平均时间复杂度O(n log n)

    • 在平均情况下(即每次选择的pivot能够均匀地进行磁盘分区),快速排序的队列深度大致为log n,每次分区操作的时间复杂度为O(n),总体时间复杂度为O(n log n)
  • 最好时间复杂度O(n log n)

    • 最好的情况发生在每次故障都将阵列均匀分割成两部分,层数的深度为log n,每次故障的时间复杂度为O(n),因此总时间复杂度为O(n log n)
空间复杂度
  • 快速排序的空间复杂度主要来自于递归栈。每次递归调用都需要一定的栈空间,而递归的最大深度是log n。因此,空间复杂度为O(log n)
  • 注意,快速排序是原地排序算法,不需要额外的存储数据,不用使用栈或分区。
稳定性

快速排序不稳定。在分区操作中,如果有可靠的元素,它们的相对顺序可能会被改变。比如,在分区过程中,如果有两个相邻的元素位于各自基准的两边,它们的顺序可能会发生变化,因此快速排序是一个不稳定的排序算法。

优缺点

优点:

  • 平均时间复杂度如下:在大多数情况下,快速排序的时间复杂度为O(n log n),性能优于其他O(n log n)算法,如归并排序和堆排序。
  • 原地排序:快速排序是原地排序算法,不需要额外的存储空间,空间复杂度低。
  • 强大:对于大多数数据集,快速排序表现良好,特别是在数据量增加的情况下。

缺点:

  • 不稳定排序:快速排序会改变元素的相对顺序。
  • 最坏时间复杂度分数:在最坏情况下,时间复杂度为O(n²),虽然可以通过随机选择基准元素或“三数取中”来减少最坏情况的发生,但它仍然有可能出现最坏的情况。
  • 网络深度增加:在某些极端情况下,网络深度可能达到n,导致栈溢出。
适用场景
  • 大数据排序:快速排序是处理大数据集合时非常的排序算法,尤其适用于内存中的排序。
  • 随机访问数据结构:对于阵列快速排序(顺序存储)这种随机访问的数据结构非常高效。
  • 需要高效排序的场景:当需要一个排序算法且不要求高效的稳定性时,快速排序通常是首选。

5.并归算法

归并排序(Merge Sort)是一种典型的分治法排序算法,由John von Neumann在1945年提出。归并排序将一个大问题分解成若干个小问题,逐步解决这些小问题,然后将它们合并成一个大问题的解。归并排序的核心思想是通过递归地分割数组,直到数组中每个部分只包含一个元素,然后将这些部分合并成有序的数组。

原理

归并排序的基本思想是:

  1. 分割:将数组分成两半,递归地对每个子数组进行排序。
  2. 合并:将排序好的子数组合并成一个有序数组。在合并的过程中,通过比较每个子数组的元素,依次将较小的元素加入到新的数组中。
算法步骤
  1. 分解:将待排序数组分成两半,递归地对每一半进行归并排序,直到子数组的长度为 1。
  2. 合并:对每一对已排序的子数组进行合并操作,合并成一个新的有序子数组。合并时,依次比较两个子数组的元素,选择较小的元素放入结果数组,直到一个子数组全部合并。
  3. 递归:对左子数组和右子数组递归执行上述过程,直到数组完全有序。

递归版

private static void mergeSort(int[] arr) {mergeSortTmp(arr,0,arr.length-1);
}

private static void mergeSortTmp(int[] arr, int left, int right) {
    if (left >= right) {
        return;
    }
    int mid = (right + left) / 2;
    mergeSortTmp(arr, left, mid);  // 排序左半部分
    mergeSortTmp(arr, mid + 1, right);  // 排序右半部分
    merge(arr, left, mid, right);  // 合并
}

private static void merge(int[] arr, int left, int mid, int right) {
    int[] tmp = new int[right - left + 1];  // 临时数组,用来存放合并后的数据
    int k = 0;  // 临时数组的索引
    int s1 = left;  // 左子数组的起始位置
    int s2 = mid + 1;  // 右子数组的起始位置

    // 合并两个有序子数组
    while (s1 <= mid && s2 <= right) {
        if (arr[s1] < arr[s2]) {
            tmp[k++] = arr[s1++];  // 将较小的元素放入临时数组
        } else {
            tmp[k++] = arr[s2++];  // 将较小的元素放入临时数组
        }
    }

    // 如果左子数组还有剩余元素,将其加入临时数组
    while (s1 <= mid) {
        tmp[k++] = arr[s1++];
    }

    // 如果右子数组还有剩余元素,将其加入临时数组
    while (s2 <= right) {
        tmp[k++] = arr[s2++];
    }

    // 将临时数组的元素复制回原数组
    for (int i = 0; i < k; i++) {
        arr[i + left] = tmp[i];
    }
}

1. mergeSort(int[] arr)

  • 作用mergeSort 是归并排序的入口方法,它接受一个整型数组 arr 作为参数。
  • 功能:调用 mergeSortTmp 方法来递归地对数组进行排序。
  • 注意arr.length - 1 是数组的右边界(即数组的最后一个索引),所以是递归排序的右边界。

2. mergeSortTmp(int[] arr, int left, int right)

  • 作用mergeSortTmp 是归并排序的递归方法,它负责递归地将数组分成两半进行排序,直到数组大小为1。
  • 步骤
    1. 递归终止条件:当 left >= right 时,表示当前数组只有一个元素,已经有序。此时不需要继续递归。
    2. 计算中间点mid = (right + left) / 2,计算当前子数组的中间索引。
    3. 递归排序
      • 先递归排序左半部分:mergeSortTmp(arr, left, mid)
      • 再递归排序右半部分:mergeSortTmp(arr, mid + 1, right)
    4. 合并操作:递归完成后,通过 merge 方法合并左右两个有序子数组。

3. merge(int[] arr, int left, int mid, int right)

  • 作用merge 方法负责将两个有序的子数组合并成一个有序的数组。
  • 步骤
    1. 创建临时数组tmp 用于存放合并后的数组数据,数组的大小为 right - left + 1,即合并的两个子数组的大小之和。
    2. 合并操作
      • 使用两个指针 s1s2 分别指向左右子数组的起始位置。
      • 比较两个子数组中的元素,将较小的元素放入临时数组 tmp 中。
      • 一旦某个子数组的元素全部合并到 tmp 中,直接将另一个子数组中剩余的元素加入 tmp
    3. 将合并后的数据复制回原数组
      • 将临时数组 tmp 中的所有元素复制回原数组 arr 的对应位置,从 left 开始,直到合并完成。
静态展示

假设我们有一个数组 [38, 27, 43, 3, 9, 82, 10],我们用归并排序来对其进行排序:

  1. 初始数组[38, 27, 43, 3, 9, 82, 10]

  2. 第一次分割

    • [38, 27, 43][3, 9, 82, 10]
  3. 继续分割

    • [38, 27, 43] -> [38][27, 43],然后将 [27, 43] 继续分割成 [27][43]
    • [3, 9, 82, 10] -> [3, 9][82, 10],然后将 [3, 9] 继续分割成 [3][9][82, 10] 继续分割成 [82][10]
  4. 开始合并

    • [27][43] 合并成 [27, 43]
    • [3][9] 合并成 [3, 9]
    • [82][10] 合并成 [10, 82]
  5. 合并结果

    • 合并 [38][27, 43][27, 38, 43]
    • 合并 [3, 9][10, 82][3, 9, 10, 82]
  6. 最终合并

    • 合并 [27, 38, 43][3, 9, 10, 82],最终得到有序数组 [3, 9, 10, 27, 38, 43, 82]
非递归版
public static void mergeSortNor(int[] array) {int gap = 1;while (gap < array.length) {for (int i = 0; i < array.length; i = i + gap * 2) {int left = i;int mid = left + gap - 1;if(mid >= array.length) {mid = array.length-1;}int right = mid + gap;if(right >= array.length) {right = array.length-1;}merge(array,left,mid,right);}gap *= 2;}
}

gap 变量的作用:

  • gap 是当前要合并的子数组的“间隔”或“块”的大小。初始值为 1,表示每次合并两个单独的元素(大小为1的子数组)。在每一轮合并操作后,gap 会翻倍,表示合并的范围逐渐增大,直到整个数组被完全合并。

外部 while 循环:

while (gap < array.length) {
    // 合并操作
    gap *= 2;
}

  • 作用:每次增加 gap 的大小,直到 gap 大于或等于数组的大小为止。
  • gap 每次翻倍,表示每一轮合并的子数组的大小逐渐增大。

内部 for 循环:

for (int i = 0; i < array.length; i = i + gap * 2) {
    int left = i;
    int mid = left + gap - 1;
    if(mid >= array.length) {
        mid = array.length - 1;
    }
    int right = mid + gap;
    if(right >= array.length) {
        right = array.length - 1;
    }
    merge(array, left, mid, right);
}

  • 作用:该循环用于每次处理 gap 大小的子数组,通过分段逐步将相邻的子数组合并。
  • i 是每次处理的子数组的左边界,left 是当前要合并的第一个子数组的左边界。
  • mid 是当前子数组的中间点,即第一个子数组的右边界。
  • right 是当前子数组的右边界,表示第二个子数组的右边界。right 可能会超出数组的边界,因此需要检查并调整为数组的最后一个索引。

merge(array, left, mid, right)

  • 作用:将两个已排序的子数组 [left, mid][mid+1, right] 合并为一个有序的数组。这个 merge 操作是归并排序的核心部分。

gap *= 2

  • 作用:在每次完成一次合并后,gap 的大小翻倍,意味着合并的范围变大。
时间复杂度
  • 最坏时间复杂度:O(n log n),在归并排序中,不管数据如何,分割过程和合并过程的时间复杂度始终是 O(n log n)。
  • 最好时间复杂度:O(n log n),即使是最好的情况,归并排序也需要执行 log n 级别的递归,并且每一层递归中会处理 n 个元素。
  • 平均时间复杂度:O(n log n),归并排序的平均时间复杂度也是 O(n log n)。
空间复杂度

归并排序需要额外的空间来存储合并后的数组,因此其空间复杂度是 O(n),其中 n 是待排序数组的长度。每次合并时,需要创建一个临时数组来存储排序后的数据。

稳定性

归并排序是稳定的。当两个元素相等时,归并排序会保持它们在原数组中的相对顺序。因为在合并两个子数组时,相等的元素会按先出现的顺序排列。

优缺点

优点:

  • 时间复杂度稳定:归并排序的时间复杂度是 O(n log n),无论输入数据的顺序如何,时间复杂度都一样。
  • 稳定排序:归并排序是一种稳定排序算法,相同的元素不会改变相对顺序。
  • 适用于大数据:由于其稳定的 O(n log n) 时间复杂度,归并排序特别适合大规模数据的排序,尤其是在外部排序(例如磁盘排序)中应用广泛。

缺点:

  • 空间复杂度较高:归并排序需要额外的空间,空间复杂度为 O(n),比一些原地排序算法(如快速排序)要高。
  • 实现较复杂:与简单的排序算法(如冒泡排序、选择排序)相比,归并排序的实现相对复杂,尤其是在处理合并操作时。
  • 适合链表而非数组:归并排序虽然适用于数组,但在链表结构中,由于链表操作的特性(不需要额外的空间来合并),它的性能表现更好。
适用场景
  • 大数据集排序:对于非常大的数据集,归并排序表现出色,尤其是在数据存储在磁盘等外部介质上时,归并排序能够进行外部排序。
  • 需要稳定排序:当排序需要保持相等元素的相对顺序时,归并排序是非常理想的选择。
  • 链表排序:归并排序非常适合链表排序,因为链表的合并操作不需要额外的空间,可以直接在链表中进行操作。

结语

排序算法是计算机科学中非常基础且重要的算法之一,广泛应用于各种数据处理任务中。通过对快速排序归并排序冒泡排序选择排序等经典排序算法的分析,我们可以看到,每种排序算法在时间复杂度、空间复杂度、稳定性以及适用场景上都有其特点和优势。

  • 冒泡排序选择排序虽然简单易懂,但在大规模数据排序时性能较差,适合小规模或部分有序的数据。
  • 快速排序通过分治法有效地将大规模数据分解为小问题,平均情况下表现非常优秀,虽然最坏情况的时间复杂度较高,但通过优化基准选择策略可以大大降低这种情况的发生几率。
  • 归并排序通过递归地将数据分割并合并,确保了稳定性和 O(n log n) 的时间复杂度,尤其在外部排序和链表排序中有广泛应用,尽管其空间复杂度较高。

在实际应用中,如何选择排序算法取决于数据的规模、初始状态、稳定性要求以及对空间复杂度的考虑。例如,快速排序通常是大数据排序的首选,而在需要稳定排序的情况下,归并排序则可能更为合适。

总之,了解各种排序算法的实现原理及其适用场景,不仅能帮助我们在不同的任务中做出合理的选择,还能为后续算法的学习和优化打下坚实的基础。希望通过这篇文章的讲解,读者能对排序算法有更深刻的理解,并能根据实际需求灵活选择合适的算法,以提高程序的效率和性能。

相关文章:

数据结构十大排序之(冒泡,快排,并归)

接上期&#xff1a; 数据结十大排序之&#xff08;选排&#xff0c;希尔&#xff0c;插排&#xff0c;堆排&#xff09;-CSDN博客 前言&#xff1a; 在计算机科学中&#xff0c;排序算法是最基础且最重要的算法之一。无论是大规模数据处理还是日常的小型程序开发&#xff0c;…...

MySql:基本查询

✨✨作者主页&#xff1a;嶔某✨✨ ✨✨所属专栏&#xff1a;MySql✨✨ 本文的代码中&#xff0c; [ ] 里面的都可以省略 在 MySQL 中&#xff0c;CRUD 是数据库操作的核心&#xff0c;代表以下四种基本操作&#xff1a; C&#xff08;Create&#xff09;&#xff1a;创建、插…...

28、基于springboot的房屋租赁系统

房屋是人类生活栖息的重要场所&#xff0c;随着城市中的流动人口的增多&#xff0c;人们对房屋租赁需求越来越高&#xff0c;为满足用户查询房屋、预约看房、房屋租赁的需求&#xff0c;特开发了本基于Spring Boot的房屋租赁系统。 本文重点阐述了房屋租赁系统的开发过程&…...

96 vSystem

vSystem系统 1 技术背景 网络虚拟化旨在构建出一套与网络底层物理拓扑相互独立的逻辑网络环境&#xff0c;提供给不同需求的用户使用。基于这种思想&#xff0c;诞生出了 VLAN 技术和 VPN 技术。近年来&#xff0c; 随着以 VMM&#xff08;Virtual Machine Monitor&#xff0c…...

[创业之路-197]:华为的发展路径启示

目录 前言&#xff1a; 一、由小公司走向大公司&#xff1a; 二、由农村包围城市&#xff1a; 三、由国内走向国际&#xff1a; 四、由代理商走向设备商&#xff0c;再到系统方案商&#xff0c;再到生态系统的搭建&#xff1a; 五、由随性到跟随&#xff0c;到赶超&#…...

两款Windows电脑便签,常用的电脑桌面便签小工具推荐

现在的职场环境中&#xff0c;效率高低会影响我们的去留以及晋升&#xff0c;而电脑便签无疑是提高效率的重要辅助工具。对于Windows电脑的用户来说&#xff0c;选择合适的电脑桌面便签小工具尤为重要。今天为大家推荐两款使用过且好用实用的Windows电脑便签&#xff0c;希望可…...

sql server索引优化语句

第一步 建一个测试表 --create table TestUsers --( -- Id int primary key identity(1,1), -- Username varchar(30) not null, -- Password varchar(10) not null, -- CreateDateTime datetime not null --)第二步 插入100w数据 大概1分钟执行时间 ----插入数据…...

从监控异常发现网络安全

前言 最近在前端异常监控系统中&#xff0c;发现一些异常信息&#xff0c;从中做了一些分析&#xff0c;得到一些体会&#xff0c;因此作文。 发现异常 某天早上打开监控系统发现&#xff0c;当天凌晨1点过测试环境有2个前端上报的异常&#xff0c;报错的原因都是由于没有获取…...

Android学习(七)-Kotlin编程语言-Lambda 编程

Lambda 编程 而 Kotlin 从第一个版本开始就支持了 Lambda 编程&#xff0c;并且 Kotlin 中的 Lambda 功能极为强大。Lambda 表达式使得代码更加简洁和易读。 2.6.1 集合的创建与遍历 集合的函数式 API 是入门 Lambda 编程的绝佳示例&#xff0c;但在开始之前&#xff0c;我们…...

中国人工智能学会技术白皮书

中国人工智能学会的技术白皮书具有多方面的重要作用&#xff0c;是极具权威性和价值的参考资料。 看看编委会和编写组的阵容&#xff0c;还是很让人觉得靠谱的 如何下载这份资料呢&#xff1f;下面跟着步骤来吧 步骤一&#xff1a;进入中国智能学会官网。百度搜索“中国智能学…...

【集合】Java 8 - Stream API 17种常用操作与案例详解

文章目录 Java8 Stream API 17种常用操作与案例详解1. collect()&#xff1a;将流中的元素收集到集合中2. filter()&#xff1a;根据条件过滤流中的元素3. map()&#xff1a;元素映射为另一个值4. forEach()&#xff1a;对流中的元素执行操作5. flatMap()&#xff1a;将流中的元…...

Spring(三)-SpringWeb-概述、特点、搭建、运行流程、组件、接受请求、获取请求数据、特殊处理、拦截器

文章目录 一、SpringWeb概述 二、SpringWeb特点 三、搭建SpringWeb&#xff08;在web项目中&#xff09; 1、导包 2、在web.xml文件中配置统一拦截分发器 DispatcherServlet 3、开启 SpringWEB 注解 4、处理器搭建 四、SpringWeb运行流程 五、SpringWeb组件 1、前端控…...

uni-app商品搜索页面

目录 一:功能概述 二:功能实现 一:功能概述 商品搜索页面,可以根据商品品牌,商品分类,商品价格等信息实现商品搜索和列表展示。 二:功能实现 1:商品搜索数据 <view class="search-map padding-main bg-base"> <view class…...

基于Spring Boot的远程教育网站

一、系统背景与意义 随着互联网技术的飞速发展和普及&#xff0c;远程教育已成为现代教育体系中的重要组成部分。它打破了时间和空间的限制&#xff0c;让学习者可以随时随地进行学习。基于Spring Boot的远程教育网站正是为了满足这一需求而设计的&#xff0c;它利用互联网技术…...

降低Mobx技术债问题-React前端数据流方案调研整理

我们现在主要是使用Mobx&#xff0c;但是Mobx的易于上手和灵活度也带来了很多预期以外的问题&#xff0c;随着项目的增长我们的代码技术债变得愈加沉重&#xff0c;不同的模块杂糅一起、单一store无限膨胀。 为此我们的调研是希望能找到一个更好的state配置、数据流的约定方案。…...

Linux通信System V:消息队列 信号量

Linux通信System V&#xff1a;消息队列 & 信号量 一、信号量概念二、信号量意义三、操作系统如何管理ipc资源&#xff08;2.36版本&#xff09;四、如何对信号量资源进行管理 一、信号量概念 信号量本质上就是计数器&#xff0c;用来保护共享资源。多个进程在进行通信时&a…...

STM32, GD32 cubemx CAN 低速率125kbps 报文丢失,解决了

用STM32 CUBEMX生成的GD32的 can程序&#xff0c;在500K波特率时可以正常使用&#xff0c;没有发现丢包&#xff0c;但速率降到250k和125k时&#xff0c;发送138帧数据&#xff0c;会丢失5个包。&#xff08;系统时钟168M&#xff0c;APB1的时钟42M&#xff09; 试了各种方法无…...

医疗服务品质提升:SSM 与 Vue 打造医院预约挂号系统方案

摘要 随着信息技术在管理上越来越深入而广泛的应用&#xff0c;管理信息系统的实施在技术上已逐步成熟。本文介绍了医院预约挂号系统的开发全过程。通过分析医院预约挂号系统管理的不足&#xff0c;创建了一个计算机管理医院预约挂号系统的方案。文章介绍了医院预约挂号系统的系…...

在UE5中调用ImGui图形界面库

ImGui是一个小巧灵活、简洁美观的图形界面库 首先我们直接参考Github https://github.com/SLSNe/Unreal5-ImGui 把项目下载下来后 打开项目目录或者引擎目录 项目根目录/Plugins/ImGui/ 或 UE5引擎根目录/Engine/Plugins/ 如果没有Plugins文件夹就新建一个 把项目放里面…...

汇聚点滴启迪思维(三)

switch存在的问题 缺少default语句 ‌switch语句可以包含一个可选的default语句&#xff0c;用于处理没有与任何case标签匹配的情况。如果没有default语句&#xff0c;并且没有与表达式匹配的case标签&#xff0c;程序将不会执行任何操作。 除了case switch包含的大括号中间…...

C#代码实现把中文录音文件(.mp3 .wav)转为文本文字内容

我们有一个中文录音文件.mp3格式或者是.wav格式&#xff0c;如果我们想要提取录音文件中的文字内容&#xff0c;我们可以采用以下方法&#xff0c;不需要使用Azure Speech API 密钥注册通过离线的方式实现。 1.首先我们先在NuGet中下载两个包 NAudio 2.2.1、Whisper.net 1.7.3…...

第18篇 :深入剖析systemverilog中 randomize 再谈失败案例(六)

今天,我们再谈一随机失败案例,希望再次同大家续探讨这块内容。 一 案例分析 我们先看一例子,代码如下: 上述代码中,共有5处使用 randomize 随机。它们的随机对象都是类 helloworld_test 中的 rand shortint unsigned counter ; 其中,counter 被赋予初始数值 66 。 …...

(耗时4天制作)详细介绍macOS系统 本博文含有全英版 (全文翻译稿)

(耗时4天制作)详细介绍macOS系统 本博文含有全英版-CSDN博客 全篇英文 Introduction to the macOS System I. Overview of macOS macOS is a proprietary operating system developed by Apple Inc., primarily used for Macintosh (Mac) computers. It is the first comme…...

React与Vue的区别(相同点和不同点)

前言 JavaScript是世界上最流行的语言之一&#xff0c;React和Vue是JS最流行的两个框架。但各有优缺点&#xff0c;本文将详细对比两大框架 一、框架背景 React React是由Facebook开发的用于构建用户界面的JavaScript库&#xff0c;Facebook对市场上JavaScript MVC框架都不太…...

flutter --no-color pub get 超时解决方法

新建Flutter项目后&#xff0c;运行报错&#xff0c;需要执行pub get 点击Run ‘flutter pub get’ … … … 卡着&#xff0c;不动了&#xff0c;提示超时 是因为墙的问题 解决方案&#xff1a; 添加以下环境变量 变量名: PUB_HOSTED_URL 变量值: https://pub.flutter-io.cn …...

MacPorts 中安装高/低版本软件方式,以 RabbitMQ 为例

查询信息 这里以 RabbitMQ 为例&#xff0c;通过搜索得到默认安装版本信息&#xff1a; port search rabbitmq-server结果 ~/Downloads> port search rabbitmq-server rabbitmq-server 3.11.15 (net)The RabbitMQ AMQP Server ~/Downloads>获取二进制文件 但当前官网…...

启动报错java.lang.NoClassDefFoundError: ch/qos/logback/core/status/WarnStatus

报错信息图片 日志&#xff1a; Exception in thread "Quartz Scheduler [scheduler]" java.lang.NoClassDefFoundError: ch/qos/logback/core/status/WarnStatus先说我自己遇到的问题&#xff0c;我们项目在web设置了自定义的log输出路径&#xff0c;多了一个 / 去…...

本科阶段最后一次竞赛Vlog——2024年智能车大赛智慧医疗组准备全过程——11上位机与小车交互

本科阶段最后一次竞赛Vlog——2024年智能车大赛智慧医疗组准备全过程——11上位机与小车交互 ​ 根据上一节的配置&#xff0c;目前已经建立了通讯环境&#xff0c;接下来给大家带来上位机与小车交互 这一章节里面也有图片大家去地瓜开发者社区看对应文章吧链接...

harbor离线安装 配置https 全程记录

1. 下载harbor最新版本 下载网址: 找最新的版本: https://github.com/goharbor/harbor/releases/download/v2.11.2/harbor-offline-installer-v2.11.2.tgz 这里我直接使用迅雷下载, 然后上传 1.1解压 sudo tar -xf harbor-offline-installer-v2.11.2.tgz -C /opt/ 2. 配置Harb…...

使用生存分析进行游戏时间测量

标题&#xff1a;Playtime Measurement with Survival Analysis 作者&#xff1a;Markus Viljanen, Antti Airola, Jukka Heikkonen, Tapio Pahikkala 译者&#xff1a;游戏数据科学 1 游戏中的游戏时间 1.1 为什么游戏时间很重要 游戏分析在理解玩家行为方面变得越来越重…...

Fiddler勾选https后google浏览器网页访问不可用

一、说明 最近电脑重新安装系统后&#xff0c;之前的所有工具都需要重新安装和配置&#xff0c;有个项目需要抓包https包查看一下请求的内容&#xff0c;通过Fiddler工具&#xff0c;但是开启后&#xff0c;发现https的无法抓取&#xff0c;同时google浏览器也不无法访问互联网…...

【信息系统项目管理师】高分论文:论信息系统项目的成本管理(社区网格化管理平台系统)

更多内容请见: 备考信息系统项目管理师-专栏介绍和目录 文章目录 论文一、规划成本管理二、估算成本三、制定预算四、控制成本论文 2022年6月,我作为项目经理负责了XX市社区网格化管理平台系统集成项目建设,该项目投资金额512.5万元,建设周期12个月。该项目由XX市综治办发…...

vscode配置markdown代码片段snippet不生效问题

markdown.json {"cpp code": {"prefix": "cpp","body": ["cpp","$1","",],"description": "cpp code"} }Ctrl Shift P 后输入settings ,然后选择open settings (json)&#xff…...

小脑萎缩与维生素补充:科学饮食,助力健康

小脑萎缩是一种神经影像学表现&#xff0c;常见于某些遗传症和神经系统变性类疾病&#xff0c;患者常表现出共济失调、语言功能障碍、眼球运动障碍以及肌肉功能障碍等症状。尽管小脑萎缩目前尚无完全治愈的方法&#xff0c;但通过合理的饮食调整和必要的维生素补充&#xff0c;…...

基于SSM+Vue的个性化旅游推荐系统

系统展示 用户前台界面 管理员后台界面 系统背景 随着社会经济的快速发展和人民生活水平的不断提高&#xff0c;旅游业逐渐成为我国国民经济的重要支柱产业。然而&#xff0c;在旅游市场日益繁荣的背景下&#xff0c;游客对于旅游产品和服务的需求逐渐呈现出多样化和个性化的趋…...

灰狼优化算法(GWO)与狼群算法(WPA)的全面比较

灰狼优化算法&#xff08;GWO&#xff09;与狼群算法&#xff08;WPA&#xff09;都是基于狼群行为的智能优化算法&#xff0c;但它们在多个方面存在显著的区别。以下是对这两种算法的全方面比较&#xff1a; 一、算法起源与灵感 1. 灰狼优化算法&#xff08;GWO&#xff09;&…...

探索未知,乐享惊喜 —— 盲盒APP开发,开启您的个性化惊喜之旅!

在这个瞬息万变的数字时代&#xff0c;我们总在寻找那些能触动心灵、带来无限可能的小确幸。为了满足您对未知的好奇与对惊喜的渴望&#xff0c;我们匠心打造了一款全新的盲盒APP&#xff0c;旨在为您的生活增添一抹不同寻常的色彩&#xff0c;让每一次打开都是一次全新的探索与…...

音视频学习(二十五):ts

TS&#xff08;MPEG-TS&#xff0c;MPEG Transport Stream&#xff09; 是一种广泛应用于流媒体传输和存储的容器格式。它最早由 MPEG&#xff08;Moving Picture Experts Group&#xff09;组织制定&#xff0c;用于视频和音频的压缩编码。在 HLS&#xff08;HTTP Live Stream…...

MVVM、MVC、MVP 的区别

MVVM&#xff08;Model-View-ViewModel&#xff09;、MVC&#xff08;Model-View-Controller&#xff09;和MVP&#xff08;Model-View-Presenter&#xff09;是三种常见的软件架构模式&#xff0c;它们在客户端应用开发中被广泛使用。每种模式都有其特定的设计理念和应用场景&…...

低延迟!实时处理!中软高科AI边缘服务器,解决边缘计算多样化需求!

根据相关统计&#xff0c;随着物联网的发展和5G技术的普及&#xff0c;到2025年&#xff0c;全球物联网设备连接数将达到1000亿&#xff0c;海量的计算数据使得传输到云端再处理的云计算方式显得更捉襟见肘。拥有低延迟、实时处理、可扩展性和更高安全性的边缘计算应运而生&…...

CSS|14 z-index

z-index z-index表示谁压盖着谁&#xff0c;数值大的会压盖住数值小的。只有定位的元素才有z-index值&#xff0c;只有设置了固定定位、相对定位、绝对定位了的元素&#xff0c;才会拥有z-indexz-index的值是没有单位的&#xff0c;值是一个正整数&#xff0c;默认的z-index值…...

Elasticsearch:使用 Open Crawler 和 semantic text 进行语义搜索

作者&#xff1a;来自 Elastic Jeff Vestal 了解如何使用开放爬虫与 semantic text 字段结合来轻松抓取网站并使其可进行语义搜索。 Elastic Open Crawler 演练 我们在这里要做什么&#xff1f; Elastic Open Crawler 是 Elastic 托管爬虫的后继者。 Semantic text 是 Elasti…...

【潜意识Java】深度解析黑马项目《苍穹外卖》与蓝桥杯算法的结合问题

目录 为什么要结合项目与算法&#xff1f; 1. 蓝桥杯与《苍穹外卖》项目的结合 实例&#xff1a;基于蓝桥杯算法思想的订单配送路径规划 问题描述&#xff1a; 代码实现&#xff1a;使用动态规划解决旅行商问题 代码解析&#xff1a; 为什么这个题目与蓝桥杯相关&#x…...

powershell基础(1)

powershell基础(1) 1. 安装PowerShell 首先&#xff0c;确保你的计算机上已经安装了PowerShell。对于Windows 10及更高版本&#xff0c;PowerShell通常是默认安装的。你也可以从微软官网下载并安装最新版本的PowerShell Core。 2. 打开PowerShell 在Windows搜索栏中输入“P…...

国标GB28181协议平台Liveweb:搭建建筑工地无线视频联网监控系统方案

随着科技高速发展&#xff0c;视频信号经过数字压缩&#xff0c;通过互联网宽带或者移动4G网络传递&#xff0c;可实现远程视频监控功能。将这一功能运用于施工现场安全管理&#xff0c;势必会大大提高管理效率&#xff0c;提升监管层次。而这些&#xff0c;通过Liveweb监控系统…...

踩准智能汽车+机器人两大风口,速腾聚创AI+机器人应用双线爆发

日前&#xff0c;RoboSense速腾聚创交出了一份亮眼的Q3财报。受到多重利好消息影响&#xff0c;其股价也应势连续大涨。截止12月9日发稿前&#xff0c;速腾聚创股价近一个月内累计涨幅已超88%。 财务数据方面&#xff0c;速腾聚创在今年前三季度实现总收入约11.3亿元&#xff0…...

本地maven项目打包部署到maven远程私库

目的&#xff1a;在自己的maven项目中&#xff0c;要把当前maven项目部署到maven私库&#xff0c;供其他人引入依赖使用。 首先要确保你当前能访问到你的私库&#xff0c;能拉私库的maven依赖即可。 maven部署命令&#xff1a; mvn deploy:deploy-file -Dmaven.test.skiptrue -…...

青少年编程与数学 02-004 Go语言Web编程 14课题、数据操作

青少年编程与数学 02-004 Go语言Web编程 14课题、数据操作 一、数据操作二、CRUD说明&#xff1a; 三、数据验证为什么需要数据验证&#xff1f;Go Web应用中的数据验证示例步骤 1: 定义订单结构体步骤 2: 实现验证逻辑步骤 3: 在HTTP处理函数中使用验证 四、数据格式化什么是数…...

Java 中 ConcurrentHashMap 和 HashMap 能存 null 吗?深挖原理和使用场景

前言 当你使用 HashMap 或 ConcurrentHashMap 时&#xff0c;可能会冒出一个经典问题&#xff1a;它们能存储 null 键或 null 值吗&#xff1f; 初学者可能觉得无所谓&#xff0c;试一下不就知道了&#xff0c;但在真实项目中&#xff0c;这个问题可能导致严重的 bug。今天我们…...

【JavaWeb后端学习笔记】Spring Task实现定时任务处理

Spring Task是Spring框架提供的任务调度工具&#xff0c;可以按照约定的时间自动执行某个代码逻辑。 主要的应用场景有&#xff1a;纪念日提醒&#xff0c;处理订单未支付&#xff0c;还款提醒等。 1、corn表达式 使用Spring Task首先需要了解corn表达式&#xff0c;通过cor…...