排序算法Java实现

文章目录

  • 排序算法
    • 概述
        • 比较排序算法
        • 非比较排序算法
        • 稳定 vs 不稳定
        • Java 中的排序
    • 外部排序
        • 1) 冒泡排序
        • 2) 选择排序
        • 3) 堆排序
        • 4) 插入排序
        • 5) 希尔排序
        • 6) 归并排序
            • 递归实现
            • 时间复杂度
            • 非递归实现
        • 7) 归并+插入
        • 8) 快速排序
            • 随机基准点
            • 处理重复值
        • 9) 计数排序
        • 10) 桶排序
        • 11) 基数排序

排序算法

概述

比较排序算法
算法最好最坏平均空间稳定思想注意事项
冒泡O(n)O( n 2 n^2 n2)O( n 2 n^2 n2)O(1)Y比较最好情况需要额外判断
选择O( n 2 n^2 n2)O( n 2 n^2 n2)O( n 2 n^2 n2)O(1)N比较交换次数一般少于冒泡
O( n l o g n nlogn nlogn)O( n l o g n nlogn nlogn)O( n l o g n nlogn nlogn)O(1)N选择堆排序的辅助性较强,理解前先理解堆的数据结构
插入O(n)O( n 2 n^2 n2)O( n 2 n^2 n2)O(1)Y比较插入排序对于近乎有序的数据处理速度比较快,复杂度有所下降,可以提前结束
希尔O(nlogn)O( n 2 n^2 n2)O( n l o g n nlogn nlogn)O(1)N插入gap序列的构造有多种方式,不同方式处理的数据复杂度可能不同
归并O( n l o g n nlogn nlogn)O( n l o g n nlogn nlogn)O( n l o g n nlogn nlogn)O(n)Y分治需要额外的O(n)的存储空间
快速O( n l o g n nlogn nlogn)O( n 2 n^2 n2)O( n l o g n nlogn nlogn)O(logn)N分治快排可能存在最坏情况,需要把枢轴值选取得尽量随机化来缓解最坏情况下的时间复杂度
非比较排序算法
非比较排序算法时间复杂度空间复杂度稳定性
计数排序O(n+k)O(n+k)稳定
桶排序O(n+k)O(n+k)稳定
基数排序O(d*(n+k))O(n+k)稳定

其中

  • n 是数组长度
  • k 是桶长度
  • d 是基数位数
稳定 vs 不稳定

stability_playing_cards.svg

Java 中的排序

Arrays.sort

JDK 7~13 中的排序实现

排序目标条件采用算法
int[] long[] float[] double[]size < 47混合插入排序 (pair)
size < 286双基准点快排
有序度低双基准点快排
有序度高归并排序
byte[]size <= 29插入排序
size > 29计数排序
char[] short[]size < 47插入排序
size < 286双基准点快排
有序度低双基准点快排
有序度高归并排序
size > 3200计数排序
Object[]-Djava.util.Arrays.useLegacyMergeSort=true传统归并排序
TimSort

JDK 14~20 中的排序实现

排序目标条件采用算法
int[] long[] float[] double[]size < 44 并位于最左侧插入排序
size < 65 并不是最左侧混合插入排序 (pin)
有序度低双基准点快排
递归次数超过 384堆排序
对于整个数组或非最左侧 size > 4096,有序度高归并排序
byte[]size <= 64插入排序
size > 64计数排序
char[] short[]size < 44插入排序
再大双基准点快排
递归次数超过 384计数排序
size > 1750计数排序
Object[]-Djava.util.Arrays.useLegacyMergeSort=true传统归并排序
TimSort
  • 其中 TimSort 是用归并+二分插入排序的混合排序算法
  • 值得注意的是从 JDK 8 开始支持 Arrays.parallelSort 并行排序
  • 根据最新的提交记录来看 JDK 21 可能会引入基数排序等优化

外部排序

1) 冒泡排序

要点

  • 每轮冒泡不断地比较相邻的两个元素,如果它们是逆序的,则交换它们的位置
  • 下一轮冒泡,可以调整未排序的右边界,减少不必要比较

以数组 3、2、1 的冒泡排序为例,第一轮冒泡

image-20230504153631958

第二轮冒泡

image-20230504154044402

未排序区域内就剩一个元素,结束

image-20230504154213239

优化手段:每次循环时,若能确定更合适的右边界,则可以减少冒泡轮数

以数组 3、2、1、4、5 为例,第一轮结束后记录的 x,即为右边界

x之后的元素,在此轮排序中没有交换位置,他们是有序的,不需要进行下一轮排序

image-20230504161136854

非递归版代码

采用优化手段后的冒泡排序才有可能时间复杂度达到O(n)

import java.util.Arrays;public class BubbleSort {// 冒泡排序方法private static void bubble(int[] a) {// j 表示未排序部分的边界int j = a.length - 1;// swapped 标志位,用于判断在一轮遍历中是否发生了交换boolean swapped;do {swapped = false;// x 用于记录最后一次交换的位置int x = 0;// 遍历未排序部分的数组元素for (int i = 0; i < j; i++) {// 如果当前元素大于下一个元素,则交换它们if (a[i] > a[i + 1]) {int t = a[i];a[i] = a[i + 1];a[i + 1] = t;// 设置标志位为 true,表示发生了交换swapped = true;// 更新最后一次交换的位置x = i;}}// 更新未排序部分的边界为最后一次交换的位置j = x;} while (j!= 0 && swapped);}public static void main(String[] args) {// 定义待排序的数组int[] a = {6, 5, 4, 3, 2, 1};// 输出排序前的数组System.out.println(Arrays.toString(a));// 调用冒泡排序方法bubble(a);// 输出排序后的数组System.out.println(Arrays.toString(a));}
}
2) 选择排序

要点

  • 每一轮选择,找出最大(最小)的元素,并把它交换到合适的位置

以下面的数组选择最大值为例

image-20230507112728513

非递归实现

public class SelectionSort {// 选择排序方法public static void sort(int[] a) {// 1. 选择轮数 a.length - 1// 2. 交换的索引位置(right) 初始 a.length - 1, 每次递减for (int right = a.length - 1; right > 0; right--) {// 假设当前最大元素的索引为 rightint max = right;// 遍历未排序部分,找到最大元素的索引for (int i = 0; i < right; i++) {if (a[i] > a[max]) {max = i;}}// 如果最大元素不在当前假设位置,进行交换if (max!= right) {swap(a, max, right);}}}// 交换数组中两个索引位置的元素private static void swap(int[] a, int i, int j) {int t = a[i];a[i] = a[j];a[j] = t;}public static void main(String[] args) {int[] a = {6, 5, 4, 3, 2, 1};// 输出初始数组System.out.println(Arrays.toString(a));// 调用排序方法sort(a);// 输出排序后的数组System.out.println(Arrays.toString(a));}
}
3) 堆排序

要点:

  • 建立大顶堆
  • 每次将堆顶元素(最大值)交换到末尾,调整堆顶元素,让它重新符合大顶堆特性

建堆

image-20230508080820117

交换,下潜调整

image-20230508080912944

image-20230508080959301

image-20230508081052055

image-20230508081220301

image-20230508081315265

代码

public class HeapSort {// 堆排序方法public static void sort(int[] a) {// 建堆操作heapify(a, a.length);// 从最后一个元素开始,每次将堆顶元素与当前未排序部分的最后一个元素交换,然后对堆顶元素进行下潜操作,以保持堆的性质for (int right = a.length - 1; right > 0; right--) {//每次将堆顶元素和最后一个元素交换后,改变right的值,相当于将交换后的最大的值排除在堆之外,其余部分参与下潜swap(a, 0, right);down(a, 0, right);}}// 建堆,时间复杂度为 O(n)private static void heapify(int[] array, int size) {// 从最后一个非叶子节点开始,对每个非叶子节点进行下潜操作,构建堆for (int i = size / 2 - 1; i >= 0; i--) {down(array, i, size);}}// 下潜操作// 在 LeetCode 上数组排序题目用堆排序求解时,非递归实现比递归实现大约快 6msprivate static void down(int[] array, int parent, int size) {while (true) {int left = parent * 2 + 1;int right = left + 1;int max = parent;// 如果左子节点存在且大于当前最大元素,更新最大元素索引为左子节点索引if (left < size && array[left] > array[max]) {max = left;}// 如果右子节点存在且大于当前最大元素,更新最大元素索引为右子节点索引if (right < size && array[right] > array[max]) {max = right;}// 如果最大元素就是当前父节点,说明没有找到更大的子节点,退出循环if (max == parent) {break;}// 交换最大子节点和父节点的元素swap(array, max, parent);// 更新父节点索引为最大子节点索引,继续下潜parent = max;}}// 交换数组中两个索引位置的元素private static void swap(int[] a, int i, int j) {int t = a[i];a[i] = a[j];a[j] = t;}public static void main(String[] args) {int[] a = {2, 3, 1, 7, 6, 4, 5};// 输出初始数组System.out.println(Arrays.toString(a));// 调用堆排序方法sort(a);// 输出排序后的数组System.out.println(Arrays.toString(a));}
}
4) 插入排序

要点

  • 将数组分为两部分 [0 … low-1] [low … a.length-1]
    • 左边 [0 … low-1] 是已排序部分
    • 右边 [low … a.length-1] 是未排序部分
  • 每次从未排序区域取出 low 位置的元素, 插入到已排序区域

image-20230513150750673

image-20230513150907333

代码

public class InsertionSort {public static void sort(int[] a) {// low 从 1 开始,因为单个元素的数组本身就是已排序的for (int low = 1; low < a.length; low++) {// 将 low 位置的元素暂存起来,准备插入到已排序区域int t = a[low];// i 初始为已排序区域的最后一个位置,也就是 low - 1int i = low - 1; // 当 i 大于等于 0 且暂存的元素小于当前已排序区域的元素时,继续寻找插入位置while (i >= 0 && t < a[i]) { // 将当前已排序区域的元素向后移动一位,为暂存元素空出插入位置a[i + 1] = a[i]; // i 递减,继续在已排序区域中向前比较i--;}// 找到插入位置,如果 i 不等于 low - 1,说明元素进行了移动if (i!= low - 1) {// 将暂存的元素插入到正确位置a[i + 1] = t;}}}public static void main(String[] args) {int[] a = {9, 3, 7, 2, 5, 8, 1, 4};// 输出初始数组System.out.println(Arrays.toString(a));// 调用插入排序方法sort(a);// 输出排序后的数组System.out.println(Arrays.toString(a));}
}
5) 希尔排序

本质上是优化的插入排序

要点

  • 简单的说,就是分组实现插入,每组元素间隙称为 gap
  • 每轮排序后 gap 逐渐变小,直至 gap 为 1 完成排序
  • 对插入排序的优化,让元素更快速地交换到最终位置

下图演示了 gap = 4,gap = 2,gap = 1 的三轮排序前后比较

代码

两层循环,外层循环控制间隙(步长gap),内层循环采取由前到后的方式,每次遍历到一个元素,就和它同一组的前面的元素进行比较并在同一组的元素中进行插入操作,和它同一组的前面的元素已经被排好序,所以每一组遍历到最后一个元素时都是被排好序的。

public class ShellSort {// 希尔排序方法public static void sort(int[] a) {// 外层循环控制步长(gap)逐渐减小for (int gap = a.length >> 1; gap > 0; gap = gap >> 1) {// 内层循环对每个子序列进行插入排序for (int low = gap; low < a.length; low++) {// 将 low 位置的元素暂存起来,准备插入到已排序区域int t = a[low];// i 初始为已排序区域的最后一个位置,也就是 low - gapint i = low - gap; // 当 i 大于等于 0 且暂存的元素小于当前已排序区域的元素时,继续寻找插入位置while (i >= 0 && t < a[i]) { // 将当前已排序区域的元素向后移动 gap 位,为暂存元素空出插入位置a[i + gap] = a[i]; // i 递减 gap,继续在已排序区域中向前比较i -= gap;}// 找到插入位置,如果 i 不等于 low - gap,说明元素进行了移动if (i!= low - gap) {// 将暂存的元素插入到正确位置a[i + gap] = t;}}            }}public static void main(String[] args) {int[] a = {9, 3, 7, 2, 5, 8, 1, 4};// 输出初始数组System.out.println(Arrays.toString(a));// 调用希尔排序方法sort(a);// 输出排序后的数组System.out.println(Arrays.toString(a));}
}
6) 归并排序
递归实现

要点

  • 分 - 每次从中间切一刀,处理的数据少一半
  • 治 - 当数据仅剩一个时可以认为有序
  • 合 - 两个有序的结果,可以进行合并排序(参见数组练习 E01. 合并有序数组)

image-20230513143854887

代码

自顶至下的方式,通过递的方法,先将数组分成两份,再逐步分散成又两份,直到一份中只有一个元素停止分散,这是认为每一份是有序的。然后通过归的方法,将每对的两份组合在一起,再复制回原来的数组中(此处的组合要求其实是进行一个合并有序数组的工作,需要一个中间数组来储存结果)

public class MergeSortTopDown {/*a1 原始数组i~iEnd 第一个有序范围的起始和结束索引j~jEnd 第二个有序范围的起始和结束索引a2 临时数组*/public static void merge(int[] a1, int i, int iEnd, int j, int jEnd, int[] a2) {// 合并两个有序范围到临时数组 a2int k = i;while (i <= iEnd && j <= jEnd) {// 比较两个范围的当前元素,将较小的元素放入 a2if (a1[i] < a1[j]) {a2[k] = a1[i];i++;} else {a2[k] = a1[j];j++;}k++;}// 如果第一个范围已遍历完,将第二个范围剩余元素复制到 a2if (i > iEnd) {System.arraycopy(a1, j, a2, k, jEnd - j + 1);}// 如果第二个范围已遍历完,将第一个范围剩余元素复制到 a2if (j > jEnd) {System.arraycopy(a1, i, a2, k, iEnd - i + 1);}}public static void sort(int[] a1) {// 进行归并排序,使用临时数组 a2int[] a2 = new int[a1.length];split(a1, 0, a1.length - 1, a2);}private static void split(int[] a1, int left, int right, int[] a2) {// 获取当前要处理的子数组int[] array = Arrays.copyOfRange(a1, left, right + 1);// System.out.println(Arrays.toString(array));// 2. 治(当子数组只有一个元素时,直接返回)if (left == right) {return;}// 1. 分(将子数组分成两部分)int m = (left + right) >>> 1;split(a1, left, m, a2);                 // 递归处理左半部分split(a1, m + 1, right, a2);       // 递归处理右半部分// 3. 合(合并两个有序的子数组)merge(a1, left, m, m + 1, right, a2);// 将合并后的结果复制回原始数组System.arraycopy(a2, left, a1, left, right - left + 1);}public static void main(String[] args) {int[] a = {9, 3, 7, 2, 8, 5, 1, 4};// 输出初始数组System.out.println(Arrays.toString(a));// 调用归并排序方法sort(a);// 输出排序后的数组System.out.println(Arrays.toString(a));}
}
时间复杂度
  • 两个长度为 m 和 n 的链表合并,时间复杂度是 m + n

  • 归并,时间复杂度: f ( n ) = 2 f ( n / 2 ) + n , f ( 1 ) = c f(n) = 2f(n/2) + n, f(1)=c f(n)=2f(n/2)+n,f(1)=c,等价解 f ( n ) = n l o g 2 n + c n f(n) = nlog_2{n} + cn f(n)=nlog2n+cn

                 8/     \4       4/ \     / \2   2   2   2||   ||  ||  ||11   11  11  11    f(8) = 2f(4) + 8
    f(4) = 2f(2) + 4
    f(2) = 2f(1) + 2
    f(1) = 1f(8) = 8 + 24
    f(4) = 4 + 8
    f(2) = 2 + 2
    f(1) = 1
    
    • 当 n = 16 时,结果 80
    • 当 n = 64 时,结果 448
  • 若逐一合并,时间复杂度: f ( n ) = ∑ n = 0 n − 1 n + 1 f(n)=\sum\limits_{n=0}^{n-1}n+1 f(n)=n=0n1n+1,等价解 f ( n ) = 1 2 ( n 2 + n ) f(n)=\frac{1}{2}(n^2+n) f(n)=21(n2+n)

    1|0 => 1
    1|1 => 2
    1|2 => 3
    1|3 => 4
    1|4 => 5
    1|5 => 6
    1|6 => 7
    1|7 => 836
    
    • 当 n = 16 时,结果 136
    • 当 n = 64 时,结果 2080
非递归实现

自下至上的方式,从子数组大小为 1 开始逐步递增,每次翻倍,直到子数组大小达到或超过原始数组长度。将子数组大小为1的子数组两两合并后形成子数组大小为2的数组。此时都是有序的,之后子数组大小加倍后再合并,直到合并成一个完整的数组。

public class MergeSortBottomUp {/*a1 原始数组i~iEnd 第一个有序范围j~jEnd 第二个有序范围a2 临时数组*/public static void merge(int[] a1, int i, int iEnd, int j, int jEnd, int[] a2) {// 合并两个有序范围到临时数组 a2。// k 是临时数组 a2 的索引,初始值为 i。int k = i;// 当两个范围都还有元素时,进行比较并将较小的元素放入临时数组 a2。while (i <= iEnd && j <= jEnd) {if (a1[i] < a1[j]) {a2[k] = a1[i];i++;} else {a2[k] = a1[j];j++;}k++;}// 如果第一个范围已遍历完,将第二个范围剩余元素复制到临时数组 a2。if (i > iEnd) {System.arraycopy(a1, j, a2, k, jEnd - j + 1);}// 如果第二个范围已遍历完,将第一个范围剩余元素复制到临时数组 a2。if (j > jEnd) {System.arraycopy(a1, i, a2, k, iEnd - i + 1);}}public static void sort(int[] a1) {int n = a1.length;// 创建一个与原始数组 a1 长度相同的临时数组 a2。int[] a2 = new int[n];// 从子数组大小为 1 开始逐步递增,每次翻倍,直到子数组大小达到或超过原始数组长度。for (int width = 1; width < n; width *= 2) {// 遍历原始数组 a1,以当前子数组大小为步长进行合并操作。for (int i = 0; i < n; i += 2 * width) {// 计算第一个子数组的结束索引,确保不超过原始数组长度且不超出当前合并范围。int m = Integer.min(i + width - 1, n - 1);// 计算第二个子数组的结束索引,确保不超过原始数组长度且不超出当前合并范围。int j = Integer.min(i + 2 * width - 1, n - 1);System.out.println(i + " " + m + " " + j);// 合并两个子数组,将结果放入临时数组 a2。merge(a1, i, m, m + 1, j, a2);}// 将临时数组 a2 的内容复制回原始数组 a1,完成一轮合并。System.arraycopy(a2, 0, a1, 0, n);}}public static void main(String[] args) {int[] a = {9, 3, 7, 2, 8, 5, 1, 4};// 输出初始数组。System.out.println(Arrays.toString(a));// 调用排序方法对数组 a 进行排序。sort(a);// 输出排序后的数组。System.out.println(Arrays.toString(a));}
}
7) 归并+插入
  • 小数据量且有序度高时,插入排序效果高
  • 大数据量用归并效果好
  • 可以结合二者

设计思路,普通的归并排序是当每一组的元素为1时,才认为该组元素有序,我们结合插入排序的适合小数据量的有点,认为当每一组的数据量小于32时,用插入排序的方式使得该组元素达成有序的目的,再使用归并排序达到并的目的。

public class MergeInsertionSort {// 插入排序方法,对指定范围内的数组进行插入排序public static void insertion(int[] a, int left, int right) {// low 从 left + 1 开始,因为单个元素的数组本身就是已排序的for (int low = left + 1; low <= right; low++) {// 将 low 位置的元素暂存起来,准备插入到已排序区域int t = a[low];// i 初始为已排序区域的最后一个位置,也就是 low - 1int i = low - 1;// 当 i 大于等于 left 且暂存的元素小于当前已排序区域的元素时,继续寻找插入位置while (i >= left && t < a[i]) {// 将当前已排序区域的元素向后移动一位,为暂存元素空出插入位置a[i + 1] = a[i];// i 递减,继续在已排序区域中向前比较i--;}// 找到插入位置,如果 i 不等于 low - 1,说明元素进行了移动if (i!= low - 1) {// 将暂存的元素插入到正确位置a[i + 1] = t;}}}/*a1 原始数组i~iEnd 第一个有序范围j~jEnd 第二个有序范围a2 临时数组*/public static void merge(int[] a1, int i, int iEnd, int j, int jEnd, int[] a2) {// 合并两个有序范围到临时数组 a2int k = i;while (i <= iEnd && j <= jEnd) {// 比较两个范围的当前元素,将较小的元素放入 a2if (a1[i] < a1[j]) {a2[k] = a1[i];i++;} else {a2[k] = a1[j];j++;}k++;}// 如果第一个范围已遍历完,将第二个范围剩余元素复制到 a2if (i > iEnd) {System.arraycopy(a1, j, a2, k, jEnd - j + 1);}// 如果第二个范围已遍历完,将第一个范围剩余元素复制到 a2if (j > jEnd) {System.arraycopy(a1, i, a2, k, iEnd - i + 1);}}public static void sort(int[] a1) {// 创建临时数组 a2int[] a2 = new int[a1.length];// 调用分割方法进行排序split(a1, 0, a1.length - 1, a2);}private static void split(int[] a1, int left, int right, int[] a2) {
//        int[] array = Arrays.copyOfRange(a1, left, right + 1);
//        System.out.println(Arrays.toString(array));// 2. 治(当子数组只有一个元素时,直接返回)if (right == left) {return;}// 如果子数组长度小于等于 32,使用插入排序if (right - left <= 32) {insertion(a1, left, right);System.out.println("insert..." + left + " " + right + " " + Arrays.toString(a1));return;}// 1. 分(将子数组分成两部分)int m = (left + right) >>> 1;split(a1, left, m, a2);                 // 递归处理左半部分split(a1, m + 1, right, a2);       // 递归处理右半部分System.out.println(left + " " + right + " " + Arrays.toString(a1));// 3. 合(合并两个有序的子数组)merge(a1, left, m, m + 1, right, a2);// 将合并后的结果复制回原始数组System.arraycopy(a2, left, a1, left, right - left + 1);}public static void main(String[] args) {int[] a = {9, 3, 7, 2, 8, 5, 1, 4};// 输出初始数组System.out.println(Arrays.toString(a));// 调用排序方法sort(a);// 输出排序后的数组System.out.println(Arrays.toString(a));}
}
8) 快速排序

单边排序和双边排序我都采取了以右边界为基准点的方式,便于记忆

单边循环(lomuto分区)要点

  • 选择最右侧元素作为基准点
  • j 找比基准点小的,i 找比基准点大的,一旦找到,二者进行交换
    • 交换时机:j 找到小的,且与 i 不相等
    • i 找到 >= 基准点元素后,不应自增(此处在代码位置有详细讲解)
  • 最后基准点与 i 交换,i 即为基准点最终索引(i指向比基准点大的元素)

例:

i 和 j 都从左边出发向右查找,i 找到比基准点4大的5,j找到比基准点小的2,停下来交换

image-20230513145045085

i 找到了比基准点大的5,j 找到比基准点小的3,停下来交换

image-20230513145259217

j 到达right 处结束,right 与 i 交换,一轮分区结束

image-20230513145454772

代码

  1. i和j同时向前遍历,直到找到一个比基准点大的值,i不动,j向后遍历直到找到一个比基准点小的值,i和j同时++。此时i++后i处的值到j处的范围内的值肯定比基准点的值大,因为此处已经被j遍历过,一种情况是此处的值比基准点大且未被交换过,一种情况是i和j相差为1,此处的值是刚刚交换过来的i处原来的比基准点大的值。
  2. 当j小于基准点,i的值会有两种情况
    1. i == j此时说明i处的值没有出现过比基准点大的情况,i++
    2. i!= j 此时说明出现过i的值比基准点大的情况,交换后i++,此时i++后i处的值到j处的范围内的值肯定比基准点的值大,因为此处已经被j遍历过,一种情况是此处的值比基准点大且未被交换过,一种情况是i和j相差为1,此处的值是刚刚交换过来的i处原来的比基准点大的值。
  3. 最后一步当i处的值和基准点的值进行交换时,有两种情况
    1. 从来没有发生过i和j处的值的交换,说明基准点左侧的值都比基准点小,此时i指向基准点,交换没有影响
    2. 发生过i和j处的交换,此时i处指向的值到j指向的值的范围内一定是比基准点大的值,i处是从左到右第一个比基准点大的值。
public class QuickSortLomuto {// 快速排序入口方法public static void sort(int[] a) {// 调用快速排序的递归方法,对整个数组进行排序quick(a, 0, a.length - 1);}private static void quick(int[] a, int left, int right) {// 如果左边界大于等于右边界,说明子数组只有一个元素或为空,直接返回if (left >= right) {return;}// 进行分区操作,返回基准点元素的索引int p = partition(a, left, right); // 对基准点左边的子数组进行快速排序quick(a, left, p - 1);// 对基准点右边的子数组进行快速排序quick(a, p + 1, right);}private static int partition(int[] a, int left, int right) {// 选取最右边的元素作为基准点元素值int pv = a[right]; // i 表示小于基准点元素值的区间的右边界(即现状态从左到右第一个比基准点大的元素)int i = left;// j 用于遍历数组int j = left;while (j < right) {// 如果当前元素小于基准点元素值if (a[j] < pv) { // 如果 i 不等于 j,说明找到了小于基准点的元素且不在正确位置,进行交换if (i!= j) {swap(a, i, j);}// 扩大小于基准点元素值的区间i++;}// 继续遍历下一个元素j++;}// 将基准点元素放到正确的位置(i 所指位置)swap(a, i, right);// 返回基准点元素的索引return i;}private static void swap(int[] a, int i, int j) {// 交换数组中两个位置的元素int t = a[i];a[i] = a[j];a[j] = t;}public static void main(String[] args) {int[] a = {5, 3, 7, 2, 9, 8, 1, 4};// 输出初始数组System.out.println(Arrays.toString(a));// 调用快速排序方法sort(a);// 输出排序后的数组System.out.println(Arrays.toString(a));}
}

双边循环要点

  • 选择最左侧元素作为基准点
  • j 找比基准点小的,i 找比基准点大的,一旦找到,二者进行交换
    • i 从左向右
    • j 从右向左
  • 最后基准点与 i 交换,i 即为基准点最终索引

例:

i 找到比基准点大的5停下来,j 找到比基准点小的1停下来(包含等于),二者交换

image-20230513145918612

i 找到8,j 找到3,二者交换,i 找到7,j 找到2,二者交换

image-20230513150158220

i == j,退出循环,基准点与 i 交换

image-20230513150351115

代码

i和j循环的顺序不能随便改动,因为我们是要取最右侧的元素为基准点,最后一步是用i处的值和基准点的值进行交换,所以我们要优先让i去找比基准点大的值。假设这样一种场景,i和j交换后,i处的值比基准点小,j处的值比基准点大,i和j相邻,如果j优先—,那么i处的值将比基准点小,却需要和基准点交换。

import java.util.Arrays;public class QuickSortHoare {// 快速排序的入口方法,对给定数组进行排序public static void sort(int[] a) {// 调用快速排序的递归方法,传入数组的起始索引和结束索引quick(a, 0, a.length - 1);}// 快速排序的递归方法private static void quick(int[] a, int left, int right) {// 如果左边界大于等于右边界,说明子数组只有一个元素或为空,直接返回if (left >= right) {return;}// 对当前子数组进行分区操作,得到基准点的索引int p = partition(a, left, right);// 对基准点左边的子数组进行快速排序quick(a, left, p - 1);// 对基准点右边的子数组进行快速排序quick(a, p + 1, right);}// 分区方法,将数组分为两部分,左边部分小于基准点,右边部分大于基准点private static int partition(int[] a, int left, int right) {// i 表示左指针,初始指向左边界int i = left;// j 表示右指针,初始指向右边界int j = right;// 选取右边界的元素作为基准点元素值int pv = a[right];while (i < j) {// 从左往右找第一个大于基准点的元素(注意,如果找不到比基准点大的元素,i < j也不会成立,不会发生swap)while (i < j && a[i] <= pv) {i++;}// 从右往左找第一个小于基准点的元素(这里的=是避免一直在基准点处循环,基准点的值就是j处的值)while (i < j && a[j] >= pv) {j--;}// 如果找到了满足条件的 i 和 j,交换它们所指的元素if (i < j) {swap(a, i, j);}}// 将基准点元素放到正确的位置(i 所指位置)swap(a, right, i);// 返回基准点元素的索引return i;}// 交换数组中两个位置的元素private static void swap(int[] a, int i, int j) {int t = a[i];a[i] = a[j];a[j] = t;}public static void main(String[] args) {int[] a = {9,7,5,8,4};// 输出初始数组System.out.println(Arrays.toString(a));// 调用快速排序方法sort(a);// 输出排序后的数组System.out.println(Arrays.toString(a));}}
随机基准点

使用随机数作为基准点,避免万一最大值或最小值作为基准点导致的分区不均衡

image-20230513152038090

改进代码

int idx = ThreadLocalRandom.current().nextInt(right - left + 1) + left;//基准点随机
swap(a, idx, right);//交换随机基准点和数组最后一个值,将其设为基准点
package cn.itcast.demo;import java.util.Arrays;
import java.util.concurrent.ThreadLocalRandom;public class QuickSortHoare {// 快速排序的入口方法,对给定数组进行排序public static void sort(int[] a) {// 调用快速排序的递归方法,传入数组的起始索引和结束索引quick(a, 0, a.length - 1);}// 快速排序的递归方法private static void quick(int[] a, int left, int right) {// 如果左边界大于等于右边界,说明子数组只有一个元素或为空,直接返回if (left >= right) {return;}// 对当前子数组进行分区操作,得到基准点的索引int p = partition(a, left, right);// 对基准点左边的子数组进行快速排序quick(a, left, p - 1);// 对基准点右边的子数组进行快速排序quick(a, p + 1, right);}// 分区方法,将数组分为两部分,左边部分小于基准点,右边部分大于基准点private static int partition(int[] a, int left, int right) {int idx = ThreadLocalRandom.current().nextInt(right - left + 1) + left;//基准点随机swap(a, idx, right);//交换随机基准点和数组最后一个值,将其设为基准点// i 表示左指针,初始指向左边界int i = left;// j 表示右指针,初始指向右边界int j = right;// 选取右边界的元素作为基准点元素值int pv = a[right];while (i < j) {// 从左往右找第一个大于基准点的元素// 从右往左找第一个小于基准点的元素while (i < j && a[j] >= pv) {j--;}while (i < j && a[i] <= pv) {i++;}// 如果找到了满足条件的 i 和 j,交换它们所指的元素if (i < j) {swap(a, i, j);}}// 将基准点元素放到正确的位置(i 所指位置)swap(a, right, i);// 返回基准点元素的索引return i;}// 交换数组中两个位置的元素private static void swap(int[] a, int i, int j) {int t = a[i];a[i] = a[j];a[j] = t;}public static void main(String[] args) {int[] a = {9,7,5,8,4};// 输出初始数组System.out.println(Arrays.toString(a));// 调用快速排序方法sort(a);// 输出排序后的数组System.out.println(Arrays.toString(a));}}
处理重复值

如果重复值较多,则原来算法中的分区效果也不好,如下图中左侧所示,需要想办法改为右侧的分区效果

image-20230513151851103

改进代码

package cn.itcast.demo;import java.util.Arrays;public class QuickSortHoare {// 快速排序的入口方法,对给定数组进行排序public static void sort(int[] a) {// 调用快速排序的递归方法,传入数组的起始索引和结束索引quick(a, 0, a.length - 1);}// 快速排序的递归方法private static void quick(int[] a, int left, int right) {// 如果左边界大于等于右边界,说明子数组只有一个元素或为空,直接返回if (left >= right) {return;}// 对当前子数组进行分区操作,得到基准点的索引int p = partition(a, left, right);// 对基准点左边的子数组进行快速排序quick(a, left, p - 1);// 对基准点右边的子数组进行快速排序quick(a, p + 1, right);}// 分区方法,将数组分为两部分,左边部分小于基准点,右边部分大于基准点private static int partition(int[] a, int left, int right) {// i 表示左指针,初始指向左边界int i = left;// j 表示右指针,初始指向右边界int j = right - 1;// 选取右边界的元素作为基准点元素值int pv = a[right];while (i <= j) {// 从左往右找第一个大于等于基准点的元素(注意,如果找不到比基准点大的元素,i < j也不会成立,不会发生swap)while (i <= j && a[i] < pv) {i++;}// 从右往左找第一个小于等于基准点的元素(这里的=是避免一直在基准点处循环,基准点的值就是j处的值)while (i <= j && a[j] > pv) {j--;}// 如果找到了满足条件的 i 和 j,交换它们所指的元素if (i <= j) {swap(a, i, j);i++;j--;//这里如果不加i++和j--的话,遇到所有值一样的情况,会循环进行交换}}// 将基准点元素放到正确的位置(i 所指位置)swap(a, right, i);// 返回基准点元素的索引return i;}// 交换数组中两个位置的元素private static void swap(int[] a, int i, int j) {int t = a[i];a[i] = a[j];a[j] = t;}public static void main(String[] args) {int[] a = {2,1,3,2,4};// 输出初始数组System.out.println(Arrays.toString(a));// 调用快速排序方法sort(a);// 输出排序后的数组System.out.println(Arrays.toString(a));}
}
  • 核心思想是

    • 改进前,i 只找大于的,j 会找小于等于的。一个不找等于、一个找等于,势必导致等于的值分布不平衡
    • 改进后,二者都会找等于的交换,等于的值会平衡分布在基准点两边
  • 细节:

    • 因为一开始 i 就可能等于 j,因此外层循环需要加等于条件保证至少进入一次,让 j 能减到正确位置
    • 内层 while 循环中 i <= j 的 = 也不能去掉,因为 i == j 时也要做一次与基准点的判断,好让 i 及 j 正确
    • i == j 时,也要做一次 i++ 和 j-- 使下次循环二者不等才能退出
    • 因为最后退出循环时 i 会大于 j,因此最终与基准点交换的是 i
  • 内层两个 while 循环的先后顺序不再重要

9) 计数排序

方法1(简化后的计数排序)

记录好每个元素的个数,通过寻找min值和max的值确定计数数组的大小的偏移量,参考哈希表的思路,然后遍历计数数组从小到大输出达到排序的目的。

public static void sort(int[] a) {// 找到数组中的最小值和最大值int min = a[0];int max = a[0];for (int i : a) {if (i > max) {max = i;} else if (i < min) {min = i;}}// 创建计数数组,大小为最大值减去最小值加一int[] counting = new int[max - min + 1];// 遍历原数组,统计每个元素出现的次数for (int i : a) {counting[i - min]++;}// 将计数数组中的元素按顺序填充回原数组int k = 0;for (int i = 0; i < counting.length; i++) {while (counting[i] > 0) {a[k] = i + min;counting[i]--;k++;}}}

针对 byte [],因为数据范围已知,省去了求最大、最小值的过程,java 中对 char[]、short[]、byte[] 的排序都可能采用 counting 排序

// 针对 byte 类型数组的计数排序,省去求最大最小值过程(因为 byte 范围已知)public static void sort(byte[] a) {// 创建计数数组,大小为 256(byte 的取值范围是 -128 到 127)int[] counting = new int[256];for (int i : a) {// 确保索引在 0 到 255 范围内counting[i & 0xFF]++;}int k = a.length - 1;// 从较大索引开始遍历计数数组for (int i = 128 + 256; k >= 0;) {// 找到第一个非零计数的索引while (counting[--i & 0xFF] == 0);int v = i & 0xFF;int c = counting[i & 0xFF];// 将对应数量的元素填充回原数组for (int j = 0; j < c; j++) {a[k] = (byte) v;k--;}}}

稳定计数排序

实现了稳定性,调整了计数数组,在原来的计数数组的基础上由前到后相加,是的每个位置上存储的是小于等于|当前索引对应的元素|的元素个数,之后从原数组末尾开始遍历,每遍历一个元素,根据对应的计数数组的元素的值放入temp数组中,再将对应的计数数组元素-1,下一次遍历到这个元素时就会放在上一次遍历的元素的前边,达到稳定性

 // 稳定计数排序(适用于一般整数数组)public static void sort2(int[] a) {// 找到数组中的最小值和最大值int min = a[0];int max = a[0];for (int i : a) {if (i > max) {max = i;} else if (i < min) {min = i;}}// 创建计数数组,大小为最大值减去最小值加一int[] counting = new int[max - min + 1];// 遍历原数组,统计每个元素出现的次数for (int i : a) {counting[i - min]++;}// 调整计数数组,使得每个位置存储小于等于当前索引的元素总数for (int i = 1; i < counting.length; i++) {counting[i] = counting[i] + counting[i - 1];}// 创建临时数组用于存储排序后的结果int[] b = new int[a.length];// 从原数组末尾开始遍历,保证稳定性for (int i = a.length - 1; i >= 0; i--) {int j = a[i] - min;counting[j]--;// 将元素放入临时数组的正确位置b[counting[j]] = a[i];}// 将临时数组的内容复制回原数组System.arraycopy(b, 0, a, 0, a.length);}
}
10) 桶排序

将数组中的元素分为若干桶(组),每个桶是一个范围的数组,桶之间有顺序关系,将桶中的元素排好序之后再按照桶的顺序进行合并

初步实现

import java.util.ArrayList;
import java.util.Arrays;class InsertionSort {// 插入排序方法,用于对给定的整数数组进行排序public static void sort(int[] a) {int n = a.length;for (int i = 1; i < n; i++) {int key = a[i];int j = i - 1;// 将当前元素与前面已排序的部分进行比较和插入while (j >= 0 && a[j] > key) {a[j + 1] = a[j];j--;}a[j + 1] = key;}}
}public class BucketSort {public static void main(String[] args) {int[] ages = {20, 18, 66, 25, 67, 30};// 输出初始数组System.out.println(Arrays.toString(ages));// 调用排序方法sort(ages);// 输出排序后的数组System.out.println(Arrays.toString(ages));}public static void sort(int[] a) {// 使用 ArrayList 数组作为桶,每个桶代表一个特定范围的元素集合ArrayList<Integer>[] buckets = new ArrayList[10];for (int i = 0; i < buckets.length; i++) {// 初始化每个桶为一个新的 ArrayListbuckets[i] = new ArrayList<>();}for (int v : a) {// 根据元素的值除以 10 确定桶的索引,将元素添加到对应的桶中ArrayList<Integer> bucket = buckets[v / 10];bucket.add(v);}int k = 0;for (ArrayList<Integer> bucket : buckets) {// 将桶中的元素转换为整数数组int[] array = bucket.stream().mapToInt(Integer::intValue).toArray();// 对桶中的元素进行插入排序InsertionSort.sort(array);for (int v : array) {// 将排序后的桶中的元素放回原数组a[k++] = v;}}}
}

优化:

上面的方法将桶的范围设置为固定的,如过所有元素都在一个桶中,那么桶排序就没有意义,桶的个数和自定义每个桶的范围大小和元素的范围有关

import java.util.ArrayList;
import java.util.Arrays;class InsertionSort {// 插入排序方法,用于对给定的整数数组进行排序public static void sort(int[] a) {int n = a.length;// 从第二个元素开始遍历数组for (int i = 1; i < n; i++) {int key = a[i];int j = i - 1;// 将当前元素与前面已排序的部分进行比较和插入while (j >= 0 && a[j] > key) {a[j + 1] = a[j];j--;}a[j + 1] = key;}}
}public class BucketSortGeneric {public static void main(String[] args) {int[] ages = {20, 10, 28, 66, 25, 31, 67, 30, 70};// 输出初始数组System.out.println("初始数组:" + Arrays.toString(ages));// 调用排序方法,传入数组和范围参数sort(ages, 20);// 输出排序后的数组System.out.println("排序后的数组:" + Arrays.toString(ages));}public static void sort(int[] a, int range) {int max = a[0];int min = a[0];// 找到数组中的最大值和最小值for (int i = 1; i < a.length; i++) {if (a[i] > max) {max = a[i];}if (a[i] < min) {min = a[i];}}// 1. 准备桶// 根据范围计算桶的数量,并创建桶列表数组ArrayList<Integer>[] buckets = new ArrayList[(max - min) / range + 1];for (int i = 0; i < buckets.length; i++) {buckets[i] = new ArrayList<>();}// 2. 放入年龄数据// 将每个年龄值分配到对应的桶中for (int age : a) {buckets[(age - min) / range].add(age);}int k = 0;for (ArrayList<Integer> bucket : buckets) {// 3. 排序桶内元素// 获取桶中的元素列表,并转换为整数数组进行插入排序int[] array = bucket.stream().mapToInt(Integer::intValue).toArray();InsertionSort.sort(array);System.out.println("桶内排序后的数组:" + Arrays.toString(array));// 4. 把每个桶排序好的内容,依次放入原始数组// 将排序后的桶中的元素放回原数组for (int v : array) {a[k++] = v;}}}
}
11) 基数排序

对每一位进行桶排序,适合数据量较大的情况,此时采用桶排序或者计数排序需要的计数数组或者桶数组过大

import java.util.ArrayList;
import java.util.Arrays;public class RadixSort {// 基数排序方法,对字符串数组进行排序public static void radixSort(String[] a, int length) {// 创建 128 个桶,因为 ASCII 码值范围是 0-127ArrayList<String>[] buckets = new ArrayList[128];for (int i = 0; i < buckets.length; i++) {// 初始化每个桶为一个新的 ArrayListbuckets[i] = new ArrayList<>();}// 从字符串的最后一位开始,逐位进行分配和收集for (int i = length - 1; i >= 0; i--) {// 将每个字符串分配到对应的桶中for (String s : a) {// 根据字符串当前位的字符的 ASCII 值确定桶的索引buckets[s.charAt(i)].add(s);}int k = 0;// 遍历每个桶,将桶中的字符串收集回原数组for (ArrayList<String> bucket : buckets) {for (String s : bucket) {a[k++] = s;}// 清空桶,为下一轮分配做准备bucket.clear();}}}public static void main(String[] args) {String[] phoneNumbers = new String[10];phoneNumbers[0] = "138";phoneNumbers[1] = "139";phoneNumbers[2] = "136";phoneNumbers[3] = "137";phoneNumbers[4] = "135";phoneNumbers[5] = "134";phoneNumbers[6] = "150";phoneNumbers[7] = "151";phoneNumbers[8] = "152";phoneNumbers[9] = "157";// 调用基数排序方法,传入字符串数组和字符串长度(这里是 3,因为电话号码示例只取了前三位)RadixSort.radixSort(phoneNumbers, 3);// 输出排序后的字符串数组for (String phoneNumber : phoneNumbers) {System.out.println(phoneNumber);}}
}

基数排序是稳定排序,因此先排个位、再排十位,十位的排序不会打乱个位取值相等的元素顺序

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

如若内容造成侵权/违法违规/事实不符,请联系一条长河网进行投诉反馈,一经查实,立即删除!

相关文章

Redmi Note 7 Pro(violet)免授权9008文件分享及刷机教程

获取文件 关注微信公众号 heStudio Community回复 violet_9008 获取下载链接。 刷机教程 下载搞机助手&#xff08;可以从上方文件中获取&#xff09;并安装。手机按音量减键和电源键进入 Fastboot 模式&#xff0c; 打开搞机助手&#xff0c;点击进入 9008 模式 等待手机…

功能强大的项目管理平台通常融合多种方法论,系统化解决项目管理难点

难、质量管理难等难点&#xff0c;使用科学的方法论配合专业的项目管理工具&#xff0c;能够更快更好管理项目&#xff0c;提高项目成功率。 那么项目管理的不同阶段分别会用到哪些关键方法论呢&#xff1f; 例如&#xff1a;启动阶段&#xff0c;会用到SMART目标原则制定合理且…

C# winforms 使用菜单和右键菜单

初级代码游戏的专栏介绍与文章目录-CSDN博客 我的github&#xff1a;codetoys&#xff0c;所有代码都将会位于ctfc库中。已经放入库中我会指出在库中的位置。 这些代码大部分以Linux为目标但部分代码是纯C的&#xff0c;可以在任何平台上使用。 源码指引&#xff1a;github源…

20240924替换电脑微信消息提示音

一.准备工作 1.先关闭电脑微信,退出进程 2.打开资源管理器,找到微信的安装位置,进入微信软件的资源目录,找到"WeChatResource.dll"文件 3.将"WeChatResource.dll"文件复制2份,其中一份复制到桌面(用作等下修改),另一份任意保存起来(用作保存原始文件,防止出…

如何使用MacPorts安装tesseract来进行简单的OCR识别

希望文章能给到你启发和灵感&#xff5e; 如果觉得文章对你有帮助的话&#xff0c;点赞 关注 收藏 支持一下博主吧&#xff5e; 阅读指南 开篇说明一、基础环境说明1.1 硬件环境1.2 软件环境 二、下载MacPorts三、如何使用macPorts安装Tesseract四、 配置并使用Tesseract五、最…

excel 时间戳与日期转换

使用 函数&#xff1a; TEXT((C1/100028800)/8640025569,"yyyy/mm/dd HH:MM:ss.000") 因为咱们的时间戳是从 1970-1-1 08:00:00 开始计算的&#xff0c;所以需要对咱们的列处理&#xff1a; 28800 是代表 1970年1月1号的8点&#xff0c; 8个小时是28800秒&#xff…

python爬虫:从12306网站获取火车站信息

代码逻辑 初始化 (init 方法)&#xff1a; 设置请求头信息。设置车站版本号。 同步车站信息 (synchronization 方法)&#xff1a; 发送GET请求获取车站信息。返回服务器响应的文本。 提取信息 (extract 方法)&#xff1a; 从服务器响应中提取车站信息字符串。去掉字符串末尾的…

OpenCV图像分割(1)图像分割函数grabCut()的使用

操作系统&#xff1a;ubuntu22.04 OpenCV版本&#xff1a;OpenCV4.9 IDE:Visual Studio Code 编程语言&#xff1a;C11 算法描述 运行 GrabCut 算法。 该函数实现了 GrabCut 图像分割算法 OpenCV 中的 grabCut() 函数是一种用于图像分割的技术&#xff0c;它可以帮助用户从图…

stable diffusion这个插件牛,高清【图片换脸】,高清【视频换脸】 一键完成!

前言 最近发现一个很不错的sdwebui的插件&#xff0c;不仅能完成图片换脸&#xff0c;还能进行视频换脸&#xff0c;而且效果比之前的 faceid和reactor要好很多&#xff0c;更像更高清&#xff0c;哈哈&#xff0c;废话不多说&#xff0c;直接上干货~插件是 easyPhoto&#xff…

今日早报 每日精选15条新闻简报 每天一分钟 知晓天下事 9月24日,星期二

每天一分钟&#xff0c;知晓天下事&#xff01; 2024年9月24日 星期二 农历八月廿二 1、 外卖新规征求意见&#xff1a;规范外卖满减、起送费等机制&#xff0c;剑指餐饮浪费。 2、 发改委&#xff1a;预计全年将实现200万辆低排放标准乘用车退出。 3、 商务部&#xff1a;中…

高通平台Android源码下载

1&#xff09;、打开&#xff1a;Android releases | CodeLinaro Wiki&#xff0c;选择相应的硬件版本Android系统 2&#xff09;、repo 源码 repo init --depth1 -u https://git.codelinaro.org/clo/la/platform/manifest.git -b release -m LA.UM.8.6.2.c31-03300-89xx.0.xm…

智算中心动环监控:构建高效、安全的数字基础设施@卓振思众

在当今快速发展的数字经济时代&#xff0c;智算中心作为人工智能和大数据技术的核心支撑设施&#xff0c;正日益成为各行业实现智能化转型的重要基石。为了确保这些高性能计算环境的安全与稳定&#xff0c;卓振思众动环监控应运而生&#xff0c;成为智算中心管理的重要组成部分…

论文复现| Free-Form Image Inpainting with Gated Convolution

论文地址具有上下文注意的生成图像修复 论文代码:GitHub 01配置环境 根据原文代码中read me中要求&#xff0c;进行环境配置以及包的安装。 Run 安装python3。 安装tensorflow(在1.3.0,1.4.0,1.5.0,1.6.0,1.7.0版本上进行了测试)。 安装tensorflow工具包neuralgym(运行pi…

【零基础入门AI:83%的文本推荐系统都在用的算法 TF-IDF】

什么是推荐系统&#xff1f; 在如今这个信息爆炸的时代&#xff0c;推荐系统是根据用户的信息或者行为&#xff0c;向用户推荐用户可能会感兴趣的内容。其中基于文本的推荐系统&#xff0c;比如搜索引擎&#xff0c;头条、微信这类资讯类应用的搜索功能&#xff0c;就是在一个…

图表示学习中的Transformer:Graphormer的突破

人工智能咨询培训老师叶梓 转载标明出处 在自然语言处理和计算机视觉等领域&#xff0c;Transformer架构已经成为主导选择。然而&#xff0c;在图级别的预测任务中&#xff0c;它的表现并不如主流的图神经网络&#xff08;GNN&#xff09;变体。这一现象引发了一个思考&#x…

轻松重置 MySQL 8.0 Root 密码的简便方法!

在Windows环境下安装MySQL数据后&#xff0c;如果忘记了 MySQL 8.0 的 root 密码&#xff0c;不必担心&#xff01;通过 --skip-grant-tables 和 named-pipe 模式登录后&#xff0c;只需几步简单的 SQL 命令即可重置密码&#xff1a;刷新权限表、修改密码、再刷新权限&#xff…

SpringBoot | Maven快速上手

文章目录 一、Maven1.1 Maven 简介&#xff1a;1.2 Maven 的核心功能&#xff1a;1.2.1 项目构建&#xff1a;1.2.2 依赖管理&#xff1a; 1.3 Maven 仓库&#xff1a;1.3.1 本地仓库&#xff1a;1.3.2 中央仓库&#xff1a;1.3.3 私服&#xff1a; 二、第一个 SpringBoot 程序…

数据处理与统计分析篇-day09-数据透视表与日期时间处理

一. 数据透视表 概述 数据透视表&#xff08;Pivot Table&#xff09;是一种交互式的表&#xff0c;可以进行某些计算&#xff0c;如求和与计数等。 所进行的计算与数据跟数据透视表中的排列有关。之所以称为数据透视表&#xff0c;是因为可以动态地改变它们的版面布置&#…

智慧水利采砂船在线监控平台:构建高效、智能的河道采砂监管体系

随着科技的不断发展&#xff0c;水利行业的智慧化转型也日益受到重视。智慧水利采砂船在线监控平台便是这一转型的重要成果之一。该平台主要服务于水政执法人员&#xff0c;针对取得河道采砂许可证的采砂公司及采砂船&#xff0c;实施在线自动监控&#xff0c;旨在提高监管效率…

OSError: [Errno 16] Device or resource busy: ‘.nfs*‘报错解决办法

目录 1 项目场景&问题描述&#xff1a;2 原因分析&#xff1a;2.1 问题背景&#xff1a; 3 解决方案&#xff1a;3.1 创建存放临时文件的目录3.2 使用该目录3.2.1 设置环境变量 TMPDIR3.2.2 运行时设置&#xff08;推荐&#xff09;3.2.3 代码中设置 4 总结 1 项目场景&…