概述

基于比较的排序算法,常见的有以下几种

算法

最好

最坏

平均

空间

稳定性

思想

注意事项

冒泡排序

O(n)

O(n^2)

O(n^2)

O(1)


比较

最好情况需要额外判断

选择排序

O(n^2)

O(n^2)

O(n^2)

O(1)


比较

顺序选择元素,交换次数较多,不适合大规模数据

堆排序

O(nlogn)

O(nlogn)

O(nlogn)

O(1)


选择

需要使用到数据结构中的堆

插入排序

O(n)

O(n^2)

O(n^2)

O(1)


比较

对基本有序的数据处理速度较快

希尔排序

O(nlogn)

O(n^2)

O(nlogn)

O(1)


插入

分组间隔gap的构造方式有多种,不同方式处理的数据复杂度可能不同

归并排序

O(nlogn)

O(nlogn)

O(nlogn)

O(n)


归并

需要额外的空间

快速排序

O(nlogn)

O(n^2)

O(nlogn)

O(logn)


分治

可能存在最坏情况,需要选取合适的基准点(可以使用随机数)

一、冒泡排序

冒泡排序(Bubble Sort)是一种简单的排序算法。它重复地遍历要排序的数组,一次比较两个相邻的元素,并且如果它们的顺序错误就交换它们。这个过程持续进行直到没有再需要交换的元素,即数组已经排序完成。

int[] num = new int[]{2,1,4,3,5,1};


1、基础版本
  1. bubbleSort1 是一个公共方法,它接受一个整型数组 num 作为参数,用于对该数组进行冒泡排序。
  2. 外层循环 for(int i = num.length - 1; i > 0; i--) 控制排序的次数,一共进行数组长度-1次排序,每次循环都会将最大的元素放在数组的最后。
  3. 内层循环 for(int j = 0; j < i; j++) 用于比较相邻的元素并进行交换。如果当前元素 num[j] 大于下一个元素 num[j + 1] ,则交换它们的位置。
  4. 在交换过程中,使用一个临时变量 temp 来暂存 num[j] 的值,然后将 num[j + 1] 的值赋给 num[j] ,最后将 temp 的值赋给 num[j + 1] ,完成两个元素的交换。

通过不断重复执行内层循环,每次都会将当前最大的元素放在数组的末尾,直到完成所有的排序操作。

public void bubbleSort1(int[] num) {
        // 进行数组长度-1次排序
        for(int i = num.length - 1; i > 0; i--) {
            // 进行每一趟排序后,最大的一个元素都会被放在数组后端,所以后面的元素可以不用参与排序
            for(int j = 0; j < i; j++) {
                if(num[j] > num[j + 1]) {
                    int temp = num[j];
                    num[j] = num[j+1];
                    num[j+1] = temp;
                }
            }
        }
    }
2、改进版本

这段代码是另一种优化过的冒泡排序算法,称为"冒泡排序的改进版"。下面是对代码的解释:

  1. bubbleSort2 是一个公共方法,它接受一个整型数组 num 作为参数,用于对该数组进行冒泡排序。
  2. 外层循环 for(int i = num.length - 1; i > 0; i--) 控制排序的次数,每次循环都会将最大的元素放在数组的最后。
  3. 在内层循环中,使用一个布尔型变量 flag 来标记前一趟排序是否有元素交换。初始时,将 flag 设为 false
  4. 内层循环 for(int j = 0; j < i; j++) 用于比较相邻的元素并进行交换。如果当前元素 num[j] 大于下一个元素 num[j + 1] ,则交换它们的位置,并将 flag 设为 true ,表示有元素交换。
  5. 在每一趟排序结束后,通过判断 flag 的值,如果 flag 仍为 false ,则说明前一趟排序没有进行任何元素交换,即数组已经有序,可以提前退出排序。

通过这种优化方式,当待排序数组已经有序时,可以避免不必要的比较和交换操作,提高了排序的效率。

public void bubbleSort2(int[] num) {
        // 进行数组长度-1次排序
        for(int i = num.length - 1; i > 0; i--) {
            boolean flag = false;// 标记前一趟排序是否交换元素
            // 进行每一趟排序后,最大的一个元素都会被放在数组后端,所以后面的元素可以不用参与排序
            for(int j = 0; j < i; j++) {
                if(num[j] > num[j + 1]) {
                    int temp = num[j];
                    num[j] = num[j+1];
                    num[j+1] = temp;
                    flag = true;
                }
            }
            if(!flag)break;// 如果前一趟排序没有交换过元素,说明数组已经有序,退出排序
        }
    }
3、递归版本

这段代码是使用递归实现的冒泡排序算法。下面是对代码的解释:

  1. bubbleSort3 是一个公共方法,它接受一个整型数组 num 作为参数,用于对该数组进行冒泡排序。
  2. bubbleSort3 方法内部调用了一个私有方法 bubble ,该方法用于实现递归排序。
  3. bubble 方法接受两个参数,一个是待排序的数组 num ,另一个是当前需要排序的元素的索引 j 。
  4. 在 bubble 方法中,首先进行一个判断,如果 j 等于0,即只剩下一个元素,直接返回,递归结束。
  5. 在内层循环 for(int i = 0; i < j; i++) 中,进行相邻元素的比较和交换操作,与普通的冒泡排序类似。
  6. 在每一趟排序结束后,通过递归调用 bubble 方法,对前 j 个元素进行排序,即 bubble(num, j-1) 。

通过递归调用 bubble 方法,每次都对较小的范围进行排序,直到只剩下一个元素,实现了冒泡排序。

public void bubbleSort3(int[] num) {
        bubble(num, num.length - 1);
    }

    private void bubble(int[] num, int j) {
        // 只有一个元素时,直接递归结束
        if(j == 0)return;
        // i < j 防止数组越界
        for(int i = 0; i < j; i++) {
            if(num[i] > num[i+1]) {
                int temp = num[i];
                num[i] = num[i+1];
                num[i+1] = temp;
            }
        }
        // 递归,对前j个元素排序
        bubble(num, j-1);
    }
4、递归改进版

这段代码是基于之前的递归冒泡排序进行了一些改进。下面是对代码的解释:

  1. bubbleSort3 是一个公共方法,它接受一个整型数组 num 作为参数,用于对该数组进行冒泡排序。
  2. bubbleSort3 方法内部调用了一个私有方法 bubble ,该方法用于实现递归排序。
  3. bubble 方法接受两个参数,一个是待排序的数组 num ,另一个是当前需要排序的元素的索引 j 。
  4. 在 bubble 方法中,首先进行一个判断,如果 j 等于0,即只剩下一个元素,直接返回,递归结束。
  5. 在内层循环 for(int i = 0; i < j; i++) 中,进行相邻元素的比较和交换操作,与普通的冒泡排序类似。
  6. 在每一趟排序结束后,通过记录无序区间的右边界 x ,即最后一次交换的位置,来减少下一次排序的范围。
  7. 在递归调用 bubble 方法时,将右边界 x 作为参数传入,以对前 x 个元素进行排序。

通过记录无序区间的右边界,可以减少排序的范围,提高算法的效率。

public void bubbleSort3(int[] num) {
        bubble(num, num.length - 1);
    }

    private void bubble(int[] num, int j) {
        // 只有一个元素时,直接递归结束
        if(j == 0)return;
        int x = 0; // 记录无序区间的右边界
        // i < j 防止数组越界
        for(int i = 0; i < j; i++) {
            if(num[i] > num[i+1]) {
                int temp = num[i];
                num[i] = num[i+1];
                num[i+1] = temp;
                x = i; // 更新右边界
            }
        }
        // 递归,对前x个元素排序
        bubble(num, x);
    }
5、总结

总结起来,以上几种冒泡排序算法的时间复杂度都为O(n^2),空间复杂度都为O(1)或O(n)(递归版需要消耗栈空间,栈的深度为n-1,因此空间复杂度为O(n))。其中,改进版的冒泡排序算法在某些特定情况下可能会比普通的冒泡排序算法效率稍高一些,而递归冒泡排序算法虽然具有递归调用的特点,但总体的时间复杂度与普通冒泡排序算法相似。

二、插入排序

插入排序(Insertion Sort)是一种简单直观的排序算法。它的工作原理是通过构建有序序列,对于未排序数据,在已排序序列中从后向前扫描,找到相应位置并插入。

具体步骤如下:

  1. 从第一个元素开始,该元素可以认为已经被排序。
  2. 取出下一个元素,在已经排序的元素序列中从后向前扫描。
  3. 如果该元素(已排序)大于新元素,将该元素移到下一位置。
  4. 重复步骤3,直到找到已排序的元素小于或等于新元素的位置。
  5. 将新元素插入到该位置后。
  6. 重复步骤2~5,直到所有元素都排序完成。

插入排序的时间复杂度为O(n^2),其中n为待排序元素的个数。虽然插入排序的时间复杂度较高,但它在小规模数据或基本有序的数据集上表现良好。

int[] num = new int[]{100,2,1,4,3,5,-11};


1、普通版本

这段代码实现了插入排序算法。下面是对代码的解释:

  1. 首先,通过传入一个整型数组arr作为参数,表示待排序的数组。
  2. 在for循环中,从第二个元素开始遍历数组(索引为1),因为第一个元素可以认为已经被排序。
  3. 定义两个变量:insertVal表示待插入的数,insertIndex表示待插入数的前一个索引。
  4. 在while循环中,首先判断insertIndex是否大于等于0,以确保不会数组越界;同时判断insertVal是否小于arr[insertIndex],如果满足条件,则将arr[insertIndex]向后移动一位(即arr[insertIndex+1] = arr[insertIndex]),同时将insertIndex减1。
  5. 重复执行步骤4,直到insertIndex不满足条件,或者insertVal大于等于arr[insertIndex]。
  6. 如果insertIndex+1不等于i(即待插入的数有变动),则将insertVal赋值给arr[insertIndex+1]。
  7. 最后,打印每一次插入排序后的数组。

这段代码实现了插入排序的逻辑,通过不断将待插入的数与已排序的数进行比较,并将较大的数向后移动,最终将待插入的数插入到正确的位置上,实现了数组的排序。

public void insertSort1(int [] arr) {
        for(int i = 1; i < arr.length; i++) {
            //定义待插入的数
            int insertVal = arr[i];//从第二个数开始
            int insertIndex = i-1;
            //确保不会数组越界insertIndex>=0
            while(insertIndex >= 0 && insertVal < arr[insertIndex]) {
                arr[insertIndex+1] = arr[insertIndex];
                insertIndex--;
            }
            if(insertIndex+1 != i) {//没有此条件一样
                arr[insertIndex+1] = insertVal;
            }
            System.out.println("第"+i+"次插入排序"+Arrays.toString(arr));
        }
    }
2、递归版本

这段代码同样实现了插入排序算法。下面是对代码的解释:

  1. 首先,定义了一个公共方法 insertSort ,接受一个整型数组 num 作为参数,表示待排序的数组。
  2. 在 insertSort 方法中,调用了一个私有方法 insert ,并传入参数1,表示从第二个元素开始插入排序。
  3. 在 insert 方法中,首先判断 low 是否等于 num.length ,如果相等,说明数组已经有序,递归结束。
  4. 然后,记录要插入的元素,即 insertVal = num[low] 。
  5. 接下来,从 low-1 开始向前遍历,找到要插入的位置。使用变量 i 表示有序区间的右边界。
  6. 在 while 循环中,判断 i 是否大于等于0,并且 num[i] 是否大于 insertVal ,如果满足条件,则将 num[i] 向后移动一位(即 num[i+1] = num[i] ),同时将 i 减1。
  7. 重复执行步骤6,直到 i 不满足条件,或者 num[i] 小于等于 insertVal 。
  8. 找到了第一个比要插入元素小的元素,将要插入的元素插入到该元素后面(即 num[i+1] = insertVal )。
  9. 最后,递归调用 insert 方法,对 low+1 到 num.length-1 的元素进行排序。
  10. 通过递归的方式,不断地将待插入的数与已排序的数进行比较,并将较大的数向后移动,最终将待插入的数插入到正确的位置上,实现了数组的排序。
public void insertSort(int[] num) {
        // 第一个元素默认有序,从第二个元素开始插入
        insert(num, 1);
    }

    private void insert(int[] num, int low) {
        // 当low == num.length时,说明数组已经有序,递归结束
        if(low == num.length)return;
        // 记录要插入的元素
        int insertVal = num[low];
        // 从low-1开始向前遍历,找到要插入的位置
        // i 是有序区间的右边界
        int i = low - 1;
        // i >= 0 防止数组越界,一直向前遍历,直到找到要插入的位置
        while(i >= 0 && num[i] > insertVal) {
            // 空出要插入的位置
            num[i+1] = num[i];
            // 继续向前遍历
            i--;
        }
        // 找到了第一个比要插入元素小的元素,将要插入的元素插入到该元素后面
        num[i+1] = insertVal;
        // 递归,对low+1到num.length-1的元素排序
        insert(num, low + 1);
    }

另一种写法

函数 insert1 接受两个参数: num 是待排序的整数数组, low 是当前需要插入的元素的索引。

首先,函数会检查 low 是否等于数组的长度,如果是,则表示已经完成了整个排序过程,直接返回。否则,函数会继续执行排序操作。

在排序的过程中,函数使用一个循环来比较当前元素和它前面的元素的大小关系。如果当前元素小于前面的元素,则交换它们的位置,直到当前元素不再小于前面的元素或者已经到达数组的起始位置。

然后,函数通过递归调用 insert1 来处理下一个需要插入的元素,即 low + 1 。

整个过程会不断重复,直到所有的元素都被插入到正确的位置,完成排序。

public void insert1(int[] num, int low) {
        if(low == num.length)return;
        int i = low - 1;
        while(i >= 0 && num[i] > num[i+1]) {
            int temp = num[i];
            num[i] = num[i+1];
            num[i+1] = temp;
            i--;
        }
        insert1(num, low + 1);
    }

三、选择排序

选择排序是一种简单直观的排序算法,其基本思想是在未排序的序列中选择最小(或最大)的元素,然后将其放到已排序的序列的末尾。重复这个过程直到所有元素都被排序。

选择排序的时间复杂度为O(n^2),因此对于大规模数据的排序效率较低,但是对于小规模数据排序效率还是比较高的。

以下是Java代码实现:

private void selectionSort(int[] arr){
        // 进行数组长度-1次选择
        for(int i = 0; i < arr.length - 1; i++){
            int minIndex = i; // 记录未排序区间最小的元素的下标,假设i下标对应元素最小
            // 在后面的元素中看能否找到更小的
            for(int j = i + 1; j < arr.length; j++){
                if(arr[j] < arr[minIndex]){
                    minIndex = j;
                }
            }
            // 如果最小的就是i对应的元素,就不需要交换了,可以减少操作次数
            if(i == minIndex)continue;
            // 交换:最小的元素和未排序区间的左端点交换
            int temp = arr[i];
            arr[i] = arr[minIndex];
            arr[minIndex] = temp;
        }
    }

在这个实现中,我们首先获取数组的长度n,然后使用两个嵌套循环来遍历数组。外部循环从0开始,到n-1结束,表示需要将前n-1个元素排序。内部循环从外部循环的下一个位置开始,到n结束,表示在未排序的元素中选择最小的元素。

在内部循环中,我们使用一个minIndex变量来记录当前找到的最小元素的下标。如果我们找到了比当前最小元素更小的元素,我们就更新minIndex的值。一旦内部循环结束,我们就知道了未排序元素中的最小值,我们将它与未排序序列的第一个元素交换,将它放到已排序序列的末尾。最终,当外部循环结束时,整个数组就被排序了。

四、堆排序

堆排序是一种基于二叉堆数据结构的排序算法。它的基本思想是将待排序的元素构建成一个二叉堆,然后依次将堆顶元素与堆尾元素交换,再重新调整堆,重复这个过程直到整个序列有序。

下面是使用Java描述堆排序的代码:

private void heapSort(int[] arr){
        int n = arr.length;
        // 建堆
        for(int i = n/2 - 1; i >= 0; i--){
            heapify(arr, n, i);
        }
        // 将堆顶元素与堆尾元素交换,并且重新调整
        for(int i = n - 1; i > 0; i--){
            int temp = arr[0];
            arr[0] = arr[i];
            arr[i] = temp;
            heapify(arr, i, 0);
        }
    }
    // 调整
    private void heapify(int[] arr, int n, int i){
        int largest = i; // 初始化最大元素为根节点
        int left = i*2 + 1;
        int right = left + 1;
        if(left < n && arr[left] > arr[largest]){
            largest = left;
        }
        if(right < n && arr[right] > arr[largest]){
            largest = right;
        }
        // 如果最大元素不是根节点,就需要交换
        if(i != largest){
            int temp = arr[i];
            arr[i] = arr[largest];
            arr[largest] = temp;
            // 交换后可能影响了堆结构,需要重新调整
            heapify(arr, n, largest);
        }
    }

这段代码首先构建一个最大堆,然后通过不断交换堆顶元素和堆尾元素的方式,将最大的元素逐步移到数组的末尾。在每次交换后,需要重新调整堆,以保持堆的性质。最终,得到的数组就是有序的。

五、希尔排序

希尔排序(Shell Sort)是一种改进的插入排序算法,也称为缩小增量排序。它通过将待排序的元素按照一定的间隔进行分组,然后对每组进行插入排序。随着排序的进行,间隔逐渐减小,直到间隔为1时,完成最后一次插入排序,从而使整个序列变得有序。

以下是希尔排序的Java实现代码:

private void shellSort(int[] arr){
        int gap = arr.length >> 2; // 初始间隔为数组长度的一半
        while(gap > 0){
            for(int i = gap; i < arr.length; 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; // 间隔缩小一半
        }
    }

希尔排序的时间复杂度取决于间隔序列的选择,最坏情况下为O(n^2),平均情况下为O(n log n)。它相对于简单插入排序来说,具有较好的性能。

六、归并排序

归并排序是一种经典的排序算法,它通过将待排序的数组分成两个子数组分别对子数组进行递归排序,然后将两个有序的子数组合并成一个有序的数组。该算法的核心思想是分治法,即将一个大问题分解成若干个小问题,然后逐个解决小问题,最后将小问题的解合并成大问题的解。

下面是使用Java语言描述归并排序算法的示例代码:

1、自顶至下的递归实现
/**
     * 归并排序
     * @param arr 待排序数组
     */
    private void mergeSort(int[] arr){
        // 特殊情况判断
        if(arr == null || arr.length < 2)return;
        // 临时数组
        int[] temp = new int[arr.length];
        // 调用归并排序
        mergeSort(arr, 0, arr.length - 1, temp);
    }

    /**
     * 归并排序算法
     * @param arr 待排序数组
     * @param left 左边界
     * @param right 右边界
     * @param temp 临时数组
     */
    private void mergeSort(int[] arr, int left, int right, int[] temp){
        // 当left == right时,说明数组只有一个元素,递归结束
        if(left < right){
            // 计算中间索引
            int mid = (left + right) >>> 1;
            // 对左边数组进行归并排序
            mergeSort(arr, left, mid, temp);
            // 对右边数组进行归并排序
            mergeSort(arr, mid + 1, right, temp);
            // 合并两个有序数组
            merge(arr, left, mid, right, temp);
        }
    }

    /**
     * 合并两个有序数组
     * @param arr 待排序数组
     * @param left 左边界
     * @param mid 中间索引
     * @param right 右边界
     * @param temp 临时数组
     */
    private void merge(int[] arr, int left, int mid, int right, int[] temp){
        // i 是左边数组的左边界,j 是右边数组的左边界,t 是临时数组的左边界
        int i = left, j = mid + 1, t = 0;
        // 合并两个有序数组
        while(i <= mid && j <= right){
            if(arr[i] <= arr[j]){
                temp[t++] = arr[i++];
            }else {
                temp[t++] = arr[j++];
            }
        }
        // 将左边数组剩余元素放入临时数组
        while(i <= mid){
            temp[t++] = arr[i++];
        }
        // 将右边数组剩余元素放入临时数组
        while(j <= right){
            temp[t++] = arr[j++];
        }
        // 将临时数组元素拷贝到原数组,注意是从left开始
        for(i = 0; i < t; i++){
            arr[left + i] = temp[i];
        }
    }
2、自底向上的迭代实现

从底部开始,先将相邻的两个元素归并,然后是四个元素,依次类推,直到整个数组有序。

/**
     * 归并排序迭代实现
     * @param arr 待排序数组
     */
    private void mergeSort1(int[] arr){
        int n = arr.length;
        // 临时数组
        int[] temp = new int[n];
        // width 是每次归并的数组长度, 1, 2, 4, 8, ...
        for(int width = 1; width < n; width *= 2){
            // left 是每次归并的左边界, 0, 2, 4, 6, ...
            for(int left = 0; left < n; left += 2 * width){
                // right 是每次归并的右边界, 1, 3, 5, 7, ...
                int right = Math.min(left + 2 * width - 1, n - 1);
                // mid 是每次归并的中间索引, 0, 1, 2, 3, ...
                int mid = Math.min(left + width - 1, n - 1);
                // 合并两个有序数组
                merge(arr, left, mid, right, temp);
            }
            // 将临时数组元素拷贝到原数组,merge函数中已经拷贝,所以下面一行不要写
            //System.arraycopy(temp, 0, arr, 0, n);
        }
    }
    /**
     * 合并两个有序数组
     * @param arr 待排序数组
     * @param left 左边界
     * @param mid 中间索引
     * @param right 右边界
     * @param temp 临时数组
     */
    private void merge(int[] arr, int left, int mid, int right, int[] temp){
        // i 是左边数组的左边界,j 是右边数组的左边界,t 是临时数组的左边界
        int i = left, j = mid + 1, t = 0;
        // 合并两个有序数组
        while(i <= mid && j <= right){
            if(arr[i] <= arr[j]){
                temp[t++] = arr[i++];
            }else {
                temp[t++] = arr[j++];
            }
        }
        // 将左边数组剩余元素放入临时数组
        while(i <= mid){
            temp[t++] = arr[i++];
        }
        // 将右边数组剩余元素放入临时数组
        while(j <= right){
            temp[t++] = arr[j++];
        }
        // 将临时数组元素拷贝到原数组,注意是从left开始
        for(i = 0; i < t; i++){
            arr[left + i] = temp[i];
        }
    }
3、优化的归并排序

当归并排序中左侧子数组的最大值小于等于右侧子数组的最小值时,可以直接返回,无需进行合并操作。以下是优化后的归并排序的Java代码示例:

public class MergeSort {
    public static void mergeSort(int[] arr) {
        if (arr == null || arr.length <= 1) {
            return;
        }
        int[] temp = new int[arr.length];
        mergeSort(arr, 0, arr.length - 1, temp);
    }
private static void mergeSort(int[] arr, int left, int right, int[] temp) {
    if (left >= right) {
        return;
    }
    int mid = left + (right - left) / 2;
    mergeSort(arr, left, mid, temp);
    mergeSort(arr, mid + 1, right, temp);
    if (arr[mid] <= arr[mid + 1]) {
        return; // 左侧子数组的最大值小于等于右侧子数组的最小值,无需合并
    }
    merge(arr, left, mid, right, temp);
}

private static void merge(int[] arr, int left, int mid, int right, int[] temp) {
    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++];
    }
    System.arraycopy(temp, 0, arr, left, k);
}

public static void main(String[] args) {
    int[] arr = {5, 2, 9, 1, 7, 6, 3};
    mergeSort(arr);
    System.out.println(Arrays.toString(arr));
}
}

这段代码中的 mergeSort 方法是归并排序的入口,通过调用 mergeSort 方法来对整个数组进行排序。其中, mergeSort 方法使用了递归的方式将数组不断分割为更小的子数组,直到子数组的长度为1或0时停止递归。在归并的过程中,使用了一个临时数组 temp 来辅助合并操作。在合并的过程中,如果左侧子数组的最大值小于等于右侧子数组的最小值,就直接返回,无需进行合并操作,从而实现了优化。

4、多线程归并排序

将大数组划分成多个子数组,分别在不同的线程中进行归并排序,然后再将各个有序子数组合并成一个有序数组。

以下是一个简单的多线程归并排序的Java代码示例:

import java.util.Arrays;

public class MultiThreadedMergeSort {
    private int[] array;
    private int[] tempArray;
public void sort(int[] array) {
    this.array = array;
    this.tempArray = new int[array.length];
    mergeSort(0, array.length - 1);
}

private void mergeSort(int low, int high) {
    if (low < high) {
        int mid = (low + high) / 2;

        // 创建两个线程分别处理左半部分和右半部分的归并排序
        Thread leftThread = new Thread(() -> mergeSort(low, mid));
        Thread rightThread = new Thread(() -> mergeSort(mid + 1, high));

        leftThread.start();
        rightThread.start();

        try {
            leftThread.join();
            rightThread.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        merge(low, mid, high);
    }
}

private void merge(int low, int mid, int high) {
    for (int i = low; i <= high; i++) {
        tempArray[i] = array[i];
    }

    int i = low;
    int j = mid + 1;
    int k = low;

    while (i <= mid && j <= high) {
        if (tempArray[i] <= tempArray[j]) {
            array[k] = tempArray[i];
            i++;
        } else {
            array[k] = tempArray[j];
            j++;
        }
        k++;
    }

    while (i <= mid) {
        array[k] = tempArray[i];
        i++;
        k++;
    }
}

public static void main(String[] args) {
    int[] array = {9, 5, 7, 2, 6, 1, 8, 3, 4};
    MultiThreadedMergeSort mergeSort = new MultiThreadedMergeSort();
    mergeSort.sort(array);
    System.out.println(Arrays.toString(array));
}

这个代码示例使用多线程归并排序来对一个整数数组进行排序。它将数组划分为多个子数组,并使用不同的线程对每个子数组进行归并排序。然后,它将各个有序子数组合并成一个有序数组。在 main 方法中,我们创建一个示例数组并调用 sort 方法进行排序,然后打印排序后的结果。

这些是归并排序的一些常见实现方式,每种方式都有不同的优势和适用场景。

七、快速排序

快速排序(Quicksort)是一种常用的排序算法,它的基本思想是通过分治的策略将一个大问题分解为多个小问题,然后逐步解决这些小问题,最终得到排序结果。

快速排序的基本步骤如下

  1. 选择一个基准元素(pivot),通常可以选择待排序数组的第一个元素。
  2. 将数组分为两部分,使得左边的元素都小于等于基准元素,右边的元素都大于等于基准元素。这个过程称为分区(partition)
  3. 对左右两个子数组分别递归地进行快速排序。
  4. 合并左右两个子数组,得到最终的排序结果。

以下是使用Java描述快速排序算法的一种实现方式:

public int[] sortArray(int[] nums) {
        quickSort(nums, 0, nums.length - 1);
        return nums;
    }
    private void quickSort(int[] nums, int low, int high){
        if(low < high){
            int pv = partition(nums, low, high);
            // 递归
            quickSort(nums, low, pv -1);
            quickSort(nums, pv+1, high);
        }
    }
    // 分区
    private int partition(int[] nums, int left, int right){
    // 获得随机基准点
    // 产生[left,right]内的随机数
        int idx = ThreadLocalRandom.current().nextInt(right - left + 1) + left;
        // 把随机基准点与第一个交换
        swap(nums, idx, left);
        int low = left + 1;
        int high = right;
        // 确定基准点
        int pv = nums[left];
        while(low <= high){
        // 从右向左遍历,找到第一个比基准元素小或相等的元素
            while(low <= high && nums[high] > pv){
                high--;
            }
            // 从左向右遍历,找到第一个比基准元素大或相等的元素
            while(low <= high && nums[low] < pv){
                low++;
            }
           // 交换 
            if(low <= high){
                swap(nums, low, high);
                low++;
                high--;
            }
        }
         // 将基准元素放到正确的位置
        swap(nums, left, high);
        return high;
    }
    private void swap(int[] arr, int i, int j){
        int temp = arr[i];
        arr[i] = arr[j];
        arr[j] = temp;
    }

在上述实现中,我们选择待排序数组的第一个元素作为基准元素,并使用两个指针 i 和 j 分别从数组的两端向中间移动,找到需要交换的元素,然后将它们交换位置。最后,将基准元素放到最终位置,完成一次分区操作。通过递归地对左右子数组进行快速排序,最终得到完整的排序结果。

需要注意的是,快速排序的性能高度依赖于基准元素的选择,不同的基准选择策略可能会导致不同的性能表现。此外,快速排序是一种原地排序算法,不需要额外的空间。

八、计数排序

计数排序(Counting Sort)是一种线性时间复杂度的排序算法,适用于一定范围内的正整数排序。它的基本思想是通过统计每个元素出现的次数,然后根据元素的大小顺序重新排列。

以下是使用Java实现计数排序的示例代码:

private void countSort(int[] arr){
        // 求数组中的最大、最小值
        int min = arr[0], max = arr[0];
        for(int i = 1; i < arr.length; i++){
            if(arr[i] < min) min = arr[i];
            if(arr[i] > max) max = arr[i];
        }
        // 使得最小值对应下标0位置
        int[] count = new int[max - min + 1];
        for(int val : arr){
            count[val - min]++;
        }
        int k = 0;
        for(int i = 0; i < count.length; i++){
            while(count[i] > 0){
                arr[k++] = i + min;
                count[i]--;
            }
        }
    }

以上代码实现了计数排序算法。首先,通过遍历数组找到最大值、最小值,然后创建一个计数数组,长度为最大值减最小值加一。接下来,统计每个元素出现的次数,将次数保存在计数数组中。最后,根据计数数组重新排列原数组,将元素按照从小到大的顺序放回原数组中。

九、桶排序算法

桶排序是一种线性排序算法,它的基本思想是将待排序的数据分到有限数量的桶中,然后对每个桶中的数据进行排序,最后将所有桶中的数据按照顺序依次排列起来。桶排序的时间复杂度为O(n),但是它的空间复杂度较高,需要额外的空间来存储桶。

下面是使用Java实现桶排序的示例代码:

/**
     * 桶排序算法
     * @param arr 待排序数组
     * @param size 每个桶的大小
     */
    private void bucketSort(int[] arr, int size){
        if(arr == null || arr.length == 0)return;
        // 求数组中的最大、最小值
        int min = arr[0], max = arr[0];
        for(int i = 1; i < arr.length; i++){
            if(arr[i] < min) min = arr[i];
            if(arr[i] > max) max = arr[i];
        }
        // 计算桶的数量
        int count = (max - min) / size + 1;
        ArrayList<ArrayList<Integer>> buckets = new ArrayList<>(count);
        // 初始化每个桶
        for(int i = 0; i < count; i++){
            buckets.add(new ArrayList<>());
        }
        // 将数据分配到桶中
        for (int value : arr) {
            int idx = (value - min) / size;
            buckets.get(idx).add(value);
        }
        // 对每个桶进行排序
        for(int i = 0; i < count; i++){
            Collections.sort(buckets.get(i));
        }
        // 将桶中的数据依次取出,得到排序后的数组
        int k = 0;
        for(int i = 0; i < count; i++){
            for(int j = 0; j < buckets.get(i).size(); j++){
                arr[k++] = buckets.get(i).get(j);
            }
        }
    }

在这个示例代码中,我们首先找到了待排序数组中的最大值和最小值,然后计算出了需要的桶的数量。接着,我们创建了桶的列表,并将待排序数组中的元素分配到对应的桶中。然后,我们对每个桶中的数据进行排序,最后将桶中的数据依次取出,得到排序后的数组。桶的大小size可以根据具体情况进行调整。

十、基数排序

基数排序是一种非比较排序算法,它根据元素的每个位上的值进行排序。它的基本思想是将待排序的元素按照低位到高位的顺序依次进行排序,最终得到有序序列。

以下是使用Java实现基数排序的示例代码:

import java.util.Arrays;

public class RadixSort {
// 获取数组中最大值
public static int getMax(int[] arr) {
    int max = arr[0];
    for (int i = 1; i < arr.length; i++) {
        if (arr[i] > max) {
            max = arr[i];
        }
    }
    return max;
}

// 基数排序
public static void radixSort(int[] arr) {
    int max = getMax(arr); // 获取数组中的最大值

    // 对每个位进行排序
    for (int exp = 1; max / exp > 0; exp *= 10) {
        countingSort(arr, exp);
    }
}

// 计数排序
public static void countingSort(int[] arr, int exp) {
    int n = arr.length;
    int[] output = new int[n];
    int[] count = new int[10]; // 0-9
    // Arrays.fill(count, 0);

    // 统计每个位上的元素个数
    for (int i = 0; i < n; i++) {
        count[(arr[i] / exp) % 10]++;
    }

    // 累加count数组,得到每个元素的正确位置
    for (int i = 1; i < 10; i++) {
        count[i] += count[i - 1];
    }

    // 将元素放入output数组中
    for (int i = n - 1; i >= 0; i--) {
        output[count[(arr[i] / exp) % 10] - 1] = arr[i];
        count[(arr[i] / exp) % 10]--;
    }

    // 将output数组复制回原数组
    for (int i = 0; i < n; i++) {
        arr[i] = output[i];
    }
}

// 测试
public static void main(String[] args) {
    int[] arr = { 170, 45, 75, 90, 802, 24, 2, 66 };
    System.out.println("原始数组:" + Arrays.toString(arr));
    radixSort(arr);
    System.out.println("排序后数组:" + Arrays.toString(arr));
}

以上代码中, getMax 函数用于获取数组中的最大值, radixSort 函数是基数排序的主要实现, countingSort 函数是基数排序中的计数排序部分。在 main 函数中,我们定义了一个待排序的数组,然后调用 radixSort 函数进行排序,并输出排序后的结果。