数据结构与算法 - 排序算法

一、概述

1. 基于比较的排序算法

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

2. 非比较排序算法

非比较排序算法 时间复杂度 空间复杂度 稳定性
计数排序 O(n+k) O(n+k) 稳定
桶排序 O(n+k) O(n+k) 稳定
基数排序 O(d*(n+k)) O(n+k) 稳定

其中,n是数组长度,k是桶长度,d是基数位数。

3. 稳定 vs 不稳定

4. 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是用归并 + 二分插入的混合排序算法
  • 值得注意的是从Java 8 开始支持Arrays.parallelSort并行排序
  • 根据最新的提交记录来看JDK 21可能会引入基数排序等优化

二、外部排序

1. 冒泡排序

要点:

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

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

第二轮冒泡

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

优化手段:每次循环时,若能确定更合适的右边界,则可以减少冒泡轮数(x记录最后一次交换的位置)

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

递归实现

java 复制代码
package com.itheima.algorithms.sort;

import java.util.Arrays;

public class BubbleSort {

    private static void bubble(int[] a) {
        helper(a, a.length - 1);
    }

    private static void helper(int[] a, int j) {
        if(j == 0) {
            return;
        }

        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;
                x = i;
            }
        }
        helper(a, x);
    }

    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));
    }
}

非递归实现

java 复制代码
package com.itheima.algorithms.sort;

import java.util.Arrays;

public class BubbleSort {

    private static void bubble(int[] a) {
        int j = a.length - 1;
        while(true) {
            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;
                    x = i;
                }
            }
            j = x;  // 更新右边界
            if(j == 0) {
                // 没有发生交换,已经有序
                break;
            }
        }
    }

    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. 选择排序

要点:

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

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

非递归实现

java 复制代码
package com.itheima.algorithms.sort;

import java.util.Arrays;

public class SelectionSort {

    public static void sort(int[] a) {
        // 1. 选择轮数 a.length - 1
        // 2. 交换的索引位置 初始a.length - 1,每次递减
        for(int right = a.length - 1; right >= 0; right--) {
            int max = right;
            for(int i = 0; i < right; i++) {
                if(a[i] > a[max]) {
                    max = i;
                }
            }
            if(max != right) {
                int t = a[max];
                a[max] = a[right];
                a[right] = 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. 堆排序

要点:

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

建堆

交换,下潜调整

代码实现

java 复制代码
package com.itheima.algorithms.sort;

import java.util.Arrays;

public class HeapSort {
    public static void sort(int[] a) {
        // 建堆
        heapify(a, a.length);
        // 交换,下潜调整
        for (int right = a.length - 1; right > 0; right--) {
            swap(a, 0, right);
            down(a, 0, right);
        }
    }

    // 建堆 O(n)
    private static void heapify(int[] array, int size) {
        // 如何找到最后这个非叶子节点  size / 2 - 1
        for (int i = size / 2 - 1; i >= 0; i--) {
            down(array, i, size);
        }
    }

    // 下潜
    // leetcode 上数组排序题目用堆排序求解,非递归实现比递归实现大约快 6ms
    // 将 parent 索引处的元素下潜: 与两个孩子较大者交换, 直至没孩子或孩子没它大
    private 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 位置的元素,插入到已排序区域

例如,

递归实现

java 复制代码
package com.itheima.algorithms.sort;

import org.checkerframework.checker.units.qual.A;

import java.util.Arrays;

public class InsertionSort {
    private static void insertion(int[] a, int low, int high) {
        if(low > high) {
            return;
        }
        int i = low - 1;
        int t = a[low];
        while(i >= 0 && a[i] > t) {  // 没有找到插入位置
            a[i + 1] = a[i];  // 空出插入位置
            i--;
        }

        if(i + 1 != low) {
            a[i + 1] = t;
        }
        insertion(a, low + 1, high);
    }

    public static void main(String[] args) {
        int[] a = {9, 3, 7, 6, 2, 5, 8, 1, 4};
        System.out.println(Arrays.toString(a));
        insertion(a, 1, a.length - 1);
        System.out.println(Arrays.toString(a));
    }
}

非递归实现

java 复制代码
package com.itheima.algorithms.sort;

import java.util.Arrays;

public class InsertionSort {

    public static void sort(int[] a) {
        for(int low = 1; low < a.length; low++) {
            // 将low位置的元素插入到[0 .. low - 1]的已排序区域
            int t = a[low];
            int i = low - 1;

            while(i >= 0 && t < a[i]) {
                a[i + 1] = a[i];
                i--;
            }
            if(i != low - 1) {
                a[i + 1] = t;
            }
        }
    }

    public static void main(String[] args) {
        int[] a = {9, 3, 7, 6, 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初始时通常取数组长度的一半,逐渐减小为之前的一半

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

代码

java 复制代码
package com.itheima.algorithms.sort;

import java.util.Arrays;

public class MergeSortTopDown {

    /**
     * 合并有序数组
     * @param a1
     */
    public static void sort(int[] a1) {
        int[] a2 = new int[a1.length];
        split(a1, 0, a1.length - 1, a2);
    }

    /**
     * 划分数据
     * @param a1
     * @param left
     * @param right
     */
    private static void split(int[] a1, int left, int right, int[] a2) {
        // 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);
    }

    /**
     *
     * @param a1 原始数组
     * @param i 第一个有序范围
     * @param iEnd
     * @param j 第二个有序范围
     * @param jEnd
     * @param a2 临时数组
     */
    public static void merge(int[] a1, int i, int iEnd, int j, int jEnd, int[] a2) {
        int k = i;
        while(i <= iEnd && j <= jEnd) {
            if(a1[i] < a1[j]) {
                a2[k] = a1[i];
                i++;
            } else {
                a2[k] = a1[j];
                j++;
            }
            k++;
        }
        // 第二个有序数组的剩余元素
        if(i > iEnd) {
            System.arraycopy(a1, j, a2, k, jEnd -j + 1);
        }
        // 第一个有序数组的剩余元素
        if(j > jEnd) {
            System.arraycopy(a1, i, a2, k, iEnd - i + 1);
        }
    }

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

6. 归并排序

6.1 递归实现(自上而下)

要点:

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

代码

java 复制代码
package com.itheima.algorithms.sort;

import java.util.Arrays;

public class MergeSort {

    /**
     * 合并有序数组
     * @param a1
     */
    public static void sort(int[] a1) {
        int[] a2 = new int[a1.length];
        split(a1, 0, a1.length - 1, a2);
    }

    /**
     * 划分数据
     * @param a1
     * @param left
     * @param right
     */
    private static void split(int[] a1, int left, int right, int[] a2) {
        // 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);
    }

    /**
     *
     * @param a1 原始数组
     * @param i 第一个有序范围
     * @param iEnd
     * @param j 第二个有序范围
     * @param jEnd
     * @param a2 临时数组
     */
    public static void merge(int[] a1, int i, int iEnd, int j, int jEnd, int[] a2) {
        int k = i;
        while(i <= iEnd && j <= jEnd) {
            if(a1[i] < a1[j]) {
                a2[k] = a1[i];
                i++;
            } else {
                a2[k] = a1[j];
                j++;
            }
            k++;
        }
        // 第二个有序数组的剩余元素
        if(i > iEnd) {
            System.arraycopy(a1, j, a2, k, jEnd -j + 1);
        }
        // 第一个有序数组的剩余元素
        if(j > jEnd) {
            System.arraycopy(a1, i, a2, k, iEnd - i + 1);
        }
    }

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

6.2 时间复杂度

两个长度为m和n的链表合并,时间复杂度是m + n

归并,时间复杂度:f(n) = 2f(n / 2) + n, f(1) = c,等价解 f(n) = nlog_2(n) + cn

6.3 非递归排序

java 复制代码
package com.itheima.algorithms.sort;

import java.util.Arrays;

public class MergeSortBottomUp {
    /**
     *
     * @param a1 原始数组
     * @param i 第一个有序范围
     * @param iEnd
     * @param j 第二个有序范围
     * @param jEnd
     * @param a2 临时数组
     */
    public static void merge(int[] a1, int i, int iEnd, int j, int jEnd, int[] a2) {
        int k = i;
        while(i <= iEnd && j <= jEnd) {
            if(a1[i] < a1[j]) {
                a2[k] = a1[i];
                i++;
            } else {
                a2[k] = a1[j];
                j++;
            }
            k++;
        }
        // 第二个有序数组的剩余元素
        if(i > iEnd) {
            System.arraycopy(a1, j, a2, k, jEnd -j + 1);
        }
        // 第一个有序数组的剩余元素
        if(j > jEnd) {
            System.arraycopy(a1, i, a2, k, iEnd - i + 1);
        }
    }

    public static void sort(int[] a1) {
        int n= a1.length;
        int[] a2 = new int[n];
        // width代表有序区间的宽度,取值依次为1、2、4、...
        for(int width = 1; width < n; width *= 2) {
            // [left, right]分别代表待合并区间的左右边界
            for (int left = 0; left < n ; left += 2 * width) {
                int right = Math.min(left + 2 * width - 1, n - 1);
                // System.out.printf("width %d [%d, %d] %n", width, left, right);
                int mid = Math.min(left + width - 1, n - 1);
                merge(a1, left, mid, mid + 1, right, a2);
            }
            // 从a2复制到a1
            System.arraycopy(a2, 0, a1,0, n);
        }
    }

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

7. 归并 + 插入

基本思路:

  1. 选择阈值:设定一个阈值(如,32等),当子数组的大小小于该阈值时,使用插入排序进行排序;否则,使用归并排序。

  2. 实现过程:

  • 在归并排序的分解阶段,将数组分割为较小的子数组
  • 当子数组的大小达到设定的阈值时,停止递归,使用插入排序对该子数组进行排序
  • 在较大的子数组上继续使用归并排序
  1. 优势:
  • 小规模数据使用插入排序速度更快
  • 归并排序保证了整体的O(n log n)性能,但通过在小规模时切换到插入排序可以减少常数因子,提升性能。
java 复制代码
package com.itheima.algorithms.sort;

import java.util.Arrays;

public class MergeInsertionSort {

    /**
     * 插入排序
     * @param a
     * @param left
     * @param right
     */
    public static void insertionSort(int[] a, int left, int right) {
        for(int low = left + 1; low <= right; low++) {
            // 将low位置的元素插入到[0 .. low - 1]的已排序区域
            int t = a[low];
            int i = low - 1;

            while(i >= left && t < a[i]) {
                a[i + 1] = a[i];
                i--;
            }
            if(i != low - 1) {
                a[i + 1] = t;
            }
        }
    }


    /**
     * 归并排序
     * @param a1
     */
    public static void sort(int[] a1) {
        int[] a2 = new int[a1.length];
        split(a1, 0, a1.length - 1, a2);
    }

    /**
     * 划分数据
     * @param a1
     * @param left
     * @param right
     */
    private static void split(int[] a1, int left, int right, int[] a2) {
        // 2. 治
        // 当子数组的大小小于阈值时,使用插入排序,否则使用归并排序
        if(right - left <= 32) {
            // 插入排序
            insertionSort(a1, 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);
    }

    /**
     * 合并有序数组
     * @param a1 原始数组
     * @param i 第一个有序范围
     * @param iEnd
     * @param j 第二个有序范围
     * @param jEnd
     * @param a2 临时数组
     */
    public static void merge(int[] a1, int i, int iEnd, int j, int jEnd, int[] a2) {
        int k = i;
        while(i <= iEnd && j <= jEnd) {
            if(a1[i] < a1[j]) {
                a2[k] = a1[i];
                i++;
            } else {
                a2[k] = a1[j];
                j++;
            }
            k++;
        }
        // 第二个有序数组的剩余元素
        if(i > iEnd) {
            System.arraycopy(a1, j, a2, k, jEnd -j + 1);
        }
        // 第一个有序数组的剩余元素
        if(j > jEnd) {
            System.arraycopy(a1, i, a2, k, iEnd - i + 1);
        }
    }

    public static void main(String[] args) {
        int[] a = {9, 3, 7, 6, 2, 5, 8, 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和j都从左边出发向右查找,i找到比基准点4大的5,j找到比基准点小的2,停下来交换

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

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

代码:

java 复制代码
package com.itheima.algorithms.sort;

import java.util.Arrays;

/**
 * 单边循环快排(lomuto洛穆托分区方案)
 * 核心思想:每轮找到一个基准点元素,把比他小的放在它左边,比它大的放在它右边,这称为分区
 * 1. 选择最右边的元素作为基准点元素
 * 2. j指针负责找到比基准点小的元素,一旦找到则将j位置的元素与i位置的元素交换
 * 3. i指针指向大于基准点元素的左边界,也是每次交换的目标索引
 * 4. 最后基准点与i位置的元素交换,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) {
            // 进行分区并获取基准元素的最终位置
            int pivotIndex = partition(a, left, right);
            // 递归排序基准元素左边的子数组
            quick(a, left, pivotIndex - 1);
            // 递归排序基准元素右边的子数组
            quick(a, pivotIndex + 1, right);
        }
    }

    /**
     * 分区
     * @param a
     * @param left
     * @param right
     */
    private static int partition(int[] a, int left, int right) {
        // 1. 选择最右边的元素作为基准点
        int pivot = a[right];
        // 2. 初始化i指针
        int i = left;
        for(int j = left; j < right; j++) {
            // 3. j 找到比基准元素小的了,交换i和j位置的元素
            if(a[j] < pivot) {
                if(i != j) {
                    swap(a, 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 = {2, 3, 1, 7, 6, 4, 5};
        System.out.println(Arrays.toString(a));
        sort(a);
        System.out.println(Arrays.toString(a));
    }
}

双边循环要点:

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

例:

选择最左侧的4作为基准点,i找到比基准点大的5停下来,j找到比基准点小的1停下来(包含等于),二者交互

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

i == j,则退出循环,基准点与i位置的元素交互

代码

java 复制代码
package com.itheima.algorithms.sort;

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);
    }

    /**
     *
     * @param a
     * @param left
     * @param right
     * @return
     */
    private static int partition(int[] a, int left, int right) {
        // i找比基准点大的,j找比基准点小的
        int i = left, j = right;
        int pivot = a[left];
        while(i < j) {
            // 先右侧扫描
            while(i < j && a[j] > pivot) {
                j--;
            }
            // 后左侧扫描(顺序不能调换)
            while(i < j && pivot >= a[i]) {
                i++;
            }
            // 一旦找到,二者进行交换
            swap(a, i, j);
        }
        // 最后基准点与i交换,i即为基准点最终索引
        swap(a, left, 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, 3, 1, 7, 6, 4, 5};
        System.out.println(Arrays.toString(a));
        sort(a);
        System.out.println(Arrays.toString(a));
    }
}

8.1 随机基准点

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

改进代码

java 复制代码
        // 随机基准点
        int idx = ThreadLocalRandom.current().nextInt(right - left + 1) + left;
        // 将left与idx位置的元素交换
        swap(a, idx, left);

8.2 处理重复值

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

改进代码

java 复制代码
package com.itheima.algorithms.sort;

import java.util.Arrays;
import java.util.concurrent.ThreadLocalRandom;

public class QuickSortHandleDuplicate {


    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);
    }

    /**
     * 循环内
     *     i从left + 1开始,从左向右找大的或相等的
     *     j从right开始,从右向左找小的或相等的
     *     交换, i++ j--
     * 循环外,j和基准点交换,j即为分区位置
     * @param a
     * @param left
     * @param right
     * @return
     */
    private static int partition(int[] a, int left, int right) {
        // 随机基准点
        int idx = ThreadLocalRandom.current().nextInt(right - left + 1) + left;
        // 将left与idx位置的元素交换
        swap(a, idx, left);
        // 从left + 1开始
        int i = left + 1, j = right;
        int pivot = a[left];
        while(i <= j) {
            // i从左到右找大的或相等的
            while(i <= j && a[i] < pivot) {
                i++;
            }
            // j从右到左找小的或者相等的
            while(i <= j && a[j] > pivot) {
                j--;
            }

            if(i <= j) {
                swap(a, i, j);;
                i++;
                j--;
            }
        }
        // 最后基准点与i交换,i即为基准点最终索引
        swap(a, j, left);
        return j;
    }

    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));
    }
}

8.3 快速排序 + 插入排序

java 复制代码
package com.itheima.algorithms.sort;

import java.util.Arrays;
import java.util.concurrent.ThreadLocalRandom;

public class QuickSortHandleDuplicate {

    /**
     * 插入排序
     * @param a
     * @param left
     * @param right
     */
    public static void insertionSort(int[] a, int left, int right) {
        for(int low = left + 1; low <= right; low++) {
            // 将low位置的元素插入到[0 .. low - 1]的已排序区域
            int t = a[low];
            int i = low - 1;

            while(i >= left && t < a[i]) {
                a[i + 1] = a[i];
                i--;
            }
            if(i != low - 1) {
                a[i + 1] = t;
            }
        }
    }

    /**
     * 快速排序
     * @param a
     */
    public static void sort(int[] a) {
        quick(a, 0, a.length - 1);
    }

    private static void quick(int[] a, int left, int right) {
        if (right - left <= 32) {
            insertionSort(a, left, right);
            return;
        }
        int p = partition(a, left, right);
        quick(a, left, p - 1);
        quick(a, p + 1, right);
    }

    /**
     * 循环内
     *     i从left + 1开始,从左向右找大的或相等的
     *     j从right开始,从右向左找小的或相等的
     *     交换, i++ j--
     * 循环外,j和基准点交换,j即为分区位置
     * @param a
     * @param left
     * @param right
     * @return
     */
    private static int partition(int[] a, int left, int right) {
        // 随机基准点
        int idx = ThreadLocalRandom.current().nextInt(right - left + 1) + left;
        // 将left与idx位置的元素交换
        swap(a, idx, left);
        // 从left + 1开始
        int i = left + 1, j = right;
        int pivot = a[left];
        while(i <= j) {
            // i从左到右找大的或相等的
            while(i <= j && a[i] < pivot) {
                i++;
            }
            // j从右到左找小的或者相等的
            while(i <= j && a[j] > pivot) {
                j--;
            }

            if(i <= j) {
                swap(a, i, j);;
                i++;
                j--;
            }
        }
        // 最后基准点与i交换,i即为基准点最终索引
        swap(a, j, left);
        return j;
    }

    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));
    }
}

性能对比

9. 计数排序

要点

    1. 找到最大值,创建一个大小为最大值+1的count数组
    1. count数组的索引对应原始数组的元素,用来统计该元素的出现次数
    1. 遍历count数组,根据count数组的索引(即原始数组的元素)以及出现次数,生成排序后的内容
  • count数组的索引是:已排序好的
  • 前提:最大值不能太大
java 复制代码
package com.itheima.algorithms.sort;

import java.util.Arrays;

/**
 * 计数排序
 */
public class CountingSort {

    /*
        要点
        1. 让原始数组的最小值映射到 count[0] 最大值映射到 count 最右侧
        2. 原始数组元素 - 最小值 = count 索引
        3. count 索引 + 最小值 = 原始数组元素
     */

    public static void main(String[] args) {
        int[] a = {5, 1, 1, 3, 0, -1};
        System.out.println(Arrays.toString(a));
        sort(a);
        System.out.println(Arrays.toString(a));
    }


    // 2N + K         n*log(n)
    /*
        {5, 1, 1, 3, 0, -1}  原始数组 a

        [1   1   2   0   1   0   1 ] count
         0   1   2   3   4   5   6
         -1  0   1       3       5
     */

    private static void sort(int[] a) {
        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];
            }
        }
        int[] count = new int[max - min + 1];

        // v原始数组元素 - 最小值 = count索引
        for (int v : a) {
            count[v - min]++;
        }

        int k = 0;
        for (int i = 0; i < count.length; i++) {
            // i + min代表原始数组元素,count[i]出现次数
            while(count[i] > 0) {
                a[k++] = i + min;
                count[i]--;
            }
        }
    }
}

性能对比

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

java 复制代码
    public static void sort(byte[] a) {
        int[] count = new int[256];
        for (int i : a) {
            // 记录每个字节值的出现次数
            // i & 0xFF是为了将byte转换为int类型,这样可以有效地处理负值
            // 因为字节的值范围是-128到127,而count数组的索引应该在0-255之间
            count[i & 0xFF]++;
        }
        // 反向填充排序后的值
        int k = a.length - 1;
        for(int i = 128 + 256; k >= 0; ) {
            // 找到一个计数大于0的字节值
            while (count[--i & 0xFF] == 0);
            int v = i & 0xFF;  // 当前处理的字节值
            int c = count[i & 0xFF];  // 获取当前字节值的计数
            for(int j = 0; j < c; j++) {
                a[k] = (byte) v;  // 将字节值填充到结果数组的后面
                k--;
            }
        }
    }

稳定计数排序

java 复制代码
    /**
     * 稳定版计数排序
     * @param a
     */
    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[] count = new int[max - min + 1];
        for (int i : a) {
            count[i - min]++;
        }
        
        // 累加计数数组 如果count[i] = 5,则表示有5个元素会放在排序数组中i的位置之前
        for (int i = 1; i < count.length; i++) {
            count[i] = count[i] + count[i - 1];
        }

        int[] b = new int[a.length];
        for(int i = a.length - 1; i >= 0; i--) {
            // 找到当前元素在计数数组中的索引
            int j = a[i] - min;
            // 减少计数,表示该元素已经放置过了
            count[j]--;
            // 将元素放入结果数组b的正确位置
            b[count[j]] = a[i];
        }
        System.arraycopy(b, 0, a, 0, a.length);
    }

10. 桶排序

初步实现

java 复制代码
package com.itheima.algorithms.sort;

import com.itheima.datastructure.Array.DynamicArray;

import java.util.Arrays;

public class BucketSort {
    public static void main(String[] args) {
        int[] ages = {20, 18, 28, 66, 25, 31, 67, 30};  // 假设年龄范围1~99
        System.out.println(Arrays.toString(ages));
        sort(ages);
        System.out.println(Arrays.toString(ages));
    }

    /**
     * 桶排序
     * @param ages
     */
    private static void sort(int[] ages) {
        // 1. 准备桶
        DynamicArray[] buckets = new DynamicArray[10];
        for (int i = 0; i < buckets.length; i++) {
            buckets[i] = new DynamicArray();
        }
        // 2. 放入年龄数据
        for (int age : ages) {
            buckets[age / 10].addLast(age);
        }
        int k = 0;
        for (DynamicArray bucket : buckets) {
            // 3. 对桶内元素进行排序
            int[] array = bucket.stream().toArray();
            InsertionSort.sort(array);
            // 4. 把每个桶排序好的内容,依次放回原始数组
            for (int i : array) {
                ages[k++] = i;
            }
        }
    }
}

通用

java 复制代码
package com.itheima.algorithms.sort;

import com.itheima.datastructure.Array.DynamicArray;

import java.util.Arrays;

public class BucketSortGeneric {

    public static void main(String[] args) {
        int[] ages = {9, 0, 5, 1, 4, 7, 2, 8};  // 假设年龄范围1~99
        System.out.println(Arrays.toString(ages));
        sort(ages, 20);
        System.out.println(Arrays.toString(ages));
    }

    /**
     * 桶排序通用版 - 参考计数排序
     * @param ages
     * @param range
     */
    private static void sort(int[] ages, int range) {
        // 0. 找最大最小值
        int max = ages[0];
        int min = ages[0];
        for (int i = 1; i < ages.length; i++) {
            if (ages[i] > max) {
                max = ages[i];
            }
            if (ages[i] < min) {
                min = ages[i];
            }
        }

        // 1. 准备桶 c
        // (max - min) / range + 1 减少桶的个数
        DynamicArray[] buckets = new DynamicArray[(max - min) / range + 1];
        for (int i = 0; i < buckets.length; i++) {
            buckets[i] = new DynamicArray();
        }
        // 2. 放入年龄数据
        for (int age : ages) {
            buckets[(age - min) / range].addLast(age);
        }
        int k = 0;
        for (DynamicArray bucket : buckets) {
            // 3. 对桶内元素进行排序
            int[] array = bucket.stream().toArray();
            InsertionSort.sort(array);
            // 4. 把每个桶排序好的内容,依次放回原始数组
            for (int i : array) {
                ages[k++] = i;
            }
        }
    }
}

11. 基数排序

基数排序(Radix Sort)的核心思路是按位排序,即将数字按位(从最低位到最高位或从最高位到最低位)逐位排序。基数排序通常适用于整数和字符串排序。以下是基数排序的基本步骤和思路:

  1. 确定最大位数
  • 首先,基数排序需要知道待排序的最大数字的位数,这可以通过遍历整个数组来完成。例如,对于数组中的最大数max,基数排序只需要处理max的位数。
  1. 按位排序
  • 基数排序通过多次(位数个数)使用一个稳定的排序算法(通常是计数排序)来对数字进行排序,以下是逐步的过程:
  • 从最低位开始:首先对每个数字的最低位进行排序,得到的结果是基于该位的排序
  • 接着是次低位:然后对于每个数字的次低位进行排序,虽然这个排序会受到前一次排序结果的影响,但依然是基于该新的位进行的
  • 重复直到最高位:继续这个过程,直到处理到最高位

例如

代码:

java 复制代码
package com.itheima.algorithms.sort;

import java.util.ArrayList;

public class RadixSort {

    /**
     * 基数排序
     *
     * @param a 待排序字符串数组
     */
    public static void radixSort(String[] a) {
        // 1. 找出最长字符串的长度
        int maxLength = 0;
        for (String s : a) {
            if (s.length() > maxLength) {
                maxLength = s.length();
            }
        }

        // 2. 准备桶
        ArrayList<String>[] buckets = new ArrayList[128];
        for (int i = 0; i < buckets.length; i++) {
            buckets[i] = new ArrayList<>();
        }

        // 3. 从字符串的低位到高位,进行多轮按位桶排序
        for (int i = maxLength - 1; i >= 0; i--) {
            // 根据某一个位的值放入到对应桶
            for (String s : a) {
                int num = Integer.parseInt(s);
                if(s.length() < maxLength) {
                    // 处理字符串长度不等的情况
                    s = String.format("%0" + maxLength + "d", num);
                }
                buckets[s.charAt(i) - '0'].add(String.valueOf(num));
            }
            int k = 0;
            for (ArrayList<String> bucket : buckets) {
                // 4. 重新取出排好序的字符串,依次放回原始数组
                for (String s : bucket) {
                    a[k++] = s;
                }
                bucket.clear();
            }
        }
    }

    public static void main(String[] args) {
        /*String[] phoneNumbers = new String[10];
        phoneNumbers[0] = "13812345678";
        phoneNumbers[1] = "13912345678";
        phoneNumbers[2] = "13612345678";
        phoneNumbers[3] = "13712345678";
        phoneNumbers[4] = "13512345678";
        phoneNumbers[5] = "13412345678";
        phoneNumbers[6] = "15012345678";
        phoneNumbers[7] = "15112345678";
        phoneNumbers[8] = "15212345678";
        phoneNumbers[9] = "15712345678";*/

        String[] phoneNumbers = new String[10];
        phoneNumbers[0] = "138";
        phoneNumbers[1] = "139";
        phoneNumbers[2] = "1068";
        phoneNumbers[3] = "137";
        phoneNumbers[4] = "135";
        phoneNumbers[5] = "14";
        phoneNumbers[6] = "150";
        phoneNumbers[7] = "151";
        phoneNumbers[8] = "162";
        phoneNumbers[9] = "57";
        RadixSort.radixSort(phoneNumbers);
        for (String phoneNumber : phoneNumbers) {
            System.out.print(phoneNumber + " ");
        }
    }
}

三、习题

1. 数组的相对排序

给你两个数组,arr1arr2arr2 中的元素各不相同,arr2 中的每个元素都出现在 arr1 中。

arr1 中的元素进行排序,使 arr1 中项的相对顺序和 arr2 中的相对顺序相同。未在 arr2 中出现过的元素需要按照升序放在 arr1 的末尾。

示例 1:

复制代码
输入:arr1 = [2,3,1,3,2,4,6,7,9,2,19], arr2 = [2,1,4,3,9,6]
输出:[2,2,2,1,4,3,3,9,6,7,19]

示例 2:

复制代码
输入:arr1 = [28,6,22,8,44,17], arr2 = [22,28,8,6]
输出:[22,28,8,6,17,44]

提示:

  • 1 <= arr1.length, arr2.length <= 1000
  • 0 <= arr1[i], arr2[i] <= 1000
  • arr2 中的元素 arr2[i] 各不相同
  • arr2 中的每个元素 arr2[i] 都出现在 arr1

解法一:计数排序

java 复制代码
class Solution {
    public int[] relativeSortArray(int[] arr1, int[] arr2) {
        // 1. 记录每个元素出现的次数
        int[] count = new int[1001];
        for (int i : arr1) {
            count[i]++;
        }
        int[] result = new int[arr1.length];
        int k = 0;
        // 2. 如果元素有在arr2出现,添加到result中
        for (int i : arr2) {
            while (count[i] > 0) {
                result[k++] = i;
                count[i]--;
            }
        }
        // 3. 处理不在arr2中的元素
        for (int i = 0; i < count.length; i++) {
            while (count[i] > 0) {
                result[k++] = i;
                count[i]--;
            }
        }
        return result;
    }
}

2. 按出现频率排序

给你一个整数数组 nums ,请你将数组按照每个值的频率 升序 排序。如果有多个值的频率相同,请你按照数值本身将它们 降序 排序。

请你返回排序后的数组。

示例 1:

复制代码
输入:nums = [1,1,2,2,2,3]
输出:[3,1,1,2,2,2]
解释:'3' 频率为 1,'1' 频率为 2,'2' 频率为 3 。

示例 2:

复制代码
输入:nums = [2,3,1,3,2]
输出:[1,3,3,2,2]
解释:'2' 和 '3' 频率都为 2 ,所以它们之间按照数值本身降序排序。

示例 3:

复制代码
输入:nums = [-1,1,-6,4,5,-6,1,4,1]
输出:[5,-1,4,4,-6,-6,1,1,1]

提示:

  • 1 <= nums.length <= 100
  • -100 <= nums[i] <= 100

解法一:计数排序。执行耗时8ms

java 复制代码
class Solution {
    public int[] frequencySort(int[] nums) {
        // 1. 记录每个元素出现的次数
        int[] count = new int[201];
        for (int num : nums) {
            // 加100是为了处理num为负数的情况
            count[num + 100]++;
        }

        return Arrays.stream(nums).boxed().sorted((a, b) -> {
            int fa = count[a + 100];
            int fb = count[b + 100];
            if(fa == fb) {
                // 如果频率相同,按照数值本身将它们降序排序
                return Integer.compare(b, a);
            } else {
                // 按照每个值的频率升序排序
                return fa - fb;
            }
        }).mapToInt(Integer::intValue).toArray();
    }
}

解法二:计数排序 + HashMap。执行耗时6ms

  • 使用Map代替固定大小数组
  • 减少计算频率的次数
  • 使用list和Comparator
java 复制代码
import java.util.*;  

class Solution {  
    public int[] frequencySort(int[] nums) {  
        // 1. 记录每个元素出现的次数  
        Map<Integer, Integer> count = new HashMap<>();  
        
        for (int num : nums) {  
            count.put(num, count.getOrDefault(num, 0) + 1);  
        }  

        // 2. 使用 List 存储元素及其频率,方便排序  
        List<Map.Entry<Integer, Integer>> entryList = new ArrayList<>(count.entrySet());  
        
        // 3. 按照频率升序,然后数值降序排序  
        entryList.sort((a, b) -> {  
            if (a.getValue().equals(b.getValue())) {  
                return Integer.compare(b.getKey(), a.getKey()); // 按数值降序  
            }  
            return Integer.compare(a.getValue(), b.getValue()); // 按频率升序  
        });  

        // 4. 构造结果数组  
        int index = 0;  
        for (Map.Entry<Integer, Integer> entry : entryList) {  
            int frequency = entry.getValue();  
            for (int i = 0; i < frequency; i++) {  
                nums[index++] = entry.getKey();  
            }  
        }  

        return nums;  
    }  
}

3. 最大间距

给定一个无序的数组 nums,返回 数组在排序之后,相邻元素之间最大的差值 。如果数组元素个数小于 2,则返回 0

您必须编写一个在「线性时间」内运行并使用「线性额外空间」的算法。(桶排序或基数排序)

示例 1:

复制代码
输入: nums = [3,6,9,1]
输出: 3
解释: 排序后的数组是 [1,3,6,9],其中相邻元素 (3,6) 和 (6,9) 之间都存在最大差值 3。

示例 2:

复制代码
输入: nums = [10]
输出: 0
解释: 数组元素个数小于 2,因此返回 0。

提示:

  • 1 <= nums.length <= 10^5
  • 0 <= nums[i] <= 10^9

解法一:桶排序。超出内存限制

java 复制代码
class Solution {
    public int maximumGap(int[] nums) {
        int n = nums.length;
        if (n < 2) {
            return 0;
        }

        sort(nums, 1);

        int ret = 0;
        for (int i = 1; i < n; i++) {
            ret = Math.max(ret, nums[i] - nums[i - 1]);
        }
        return ret;
    }

    public static void sort(int[] nums, int range) {
        int max = nums[0];
        int min = nums[0];
        for (int i = 1; i < nums.length; i++) {
            if (nums[i] > max) {
                max = nums[i];
            }
            if (nums[i] < min) {
                min = nums[i];
            }
        }

        ArrayList[] buckets = new ArrayList[(max - min) / range + 1];
        for (int i = 0; i < buckets.length; i++) {
            buckets[i] = new ArrayList();
        }
        for (int num : nums) {
            buckets[(num - min) / range].addLast(num);
        }

        int k = 0;
        for (ArrayList<Integer> bucket : buckets) {
            // 创建一个新的int数组
            int[] array = new int[bucket.size()];

            // 将ArrayList的元素复制到int数组
            for (int i = 0; i < bucket.size(); i++) {
                array[i] = bucket.get(i);
            }

            // 执行插入排序
            insertionSort(array, 0, array.length - 1);

            // 将排序后的结果写回nums数组
            for (int a : array) {
                nums[k++] = a;
            }
        }
    }

    public static void insertionSort(int[] nums, int left, int right) {
        for (int low = left + 1; low <= right; low++) {
            int t = nums[low];
            int i = low - 1;

            while (i >= left && t < nums[i]) {
                nums[i + 1] = nums[i];
                i--;
            }
            if (i != low - 1) {
                nums[i + 1] = t;
            }
        }
    }
}

解法二:桶排序 - 合理化桶的数量。执行耗时52ms

java 复制代码
class Solution {
    public int maximumGap(int[] nums) {
        int n = nums.length;
        if (n < 2) {
            return 0;
        }

        sort(nums);

        int ret = 0;
        for (int i = 1; i < n; i++) {
            ret = Math.max(ret, nums[i] - nums[i - 1]);
        }
        return ret;
    }

    public static void sort(int[] nums) {
        int max = nums[0];
        int min = nums[0];
        for (int i = 1; i < nums.length; i++) {
            if (nums[i] > max) {
                max = nums[i];
            }
            if (nums[i] < min) {
                min = nums[i];
            }
        }

        // ArrayList[] buckets = new ArrayList[(max - min) / range + 1];
        int range = Math.max((max - min) / (nums.length - 1), 1);
        ArrayList[] buckets = new ArrayList[(max - min) / range + 1];
        for (int i = 0; i < buckets.length; i++) {
            buckets[i] = new ArrayList();
        }
        for (int num : nums) {
            buckets[(num - min) / range].addLast(num);
        }

        int k = 0;
        for (ArrayList<Integer> bucket : buckets) {
            // 创建一个新的int数组
            int[] array = new int[bucket.size()];

            // 将ArrayList的元素复制到int数组
            for (int i = 0; i < bucket.size(); i++) {
                array[i] = bucket.get(i);
            }

            // 执行插入排序
            insertionSort(array, 0, array.length - 1);

            // 将排序后的结果写回nums数组
            for (int a : array) {
                nums[k++] = a;
            }
        }
    }

    public static void insertionSort(int[] nums, int left, int right) {
        for (int low = left + 1; low <= right; low++) {
            int t = nums[low];
            int i = low - 1;

            while (i >= left && t < nums[i]) {
                nums[i + 1] = nums[i];
                i--;
            }
            if (i != low - 1) {
                nums[i + 1] = t;
            }
        }
    }
}

解法三:桶排序 - 只保留桶内最大最小值。执行耗时13ms

java 复制代码
class Solution {

    static class Pair {
        int max = 0;
        int min = 1000_000_000;

        public void add(int v) {
            max = Math.max(max, v);
            min = Math.min(min, v);
        }

        @Override
        public String toString() {
            return "[" + min + "," + max + "]";
        }
    }

    public int maximumGap(int[] nums) {
        // 1. 处理特殊情况
        if (nums.length < 2) {
            return 0;
        }
        // 2. 桶排序
        // 桶个数 (max - min) / range + 1 期望桶个数 nums.length + 1
        // range = (max - min) / nums.length
        int max = nums[0];
        int min = nums[0];
        for (int i = 1; i < nums.length; i++) {
            if (nums[i] > max) {
                max = nums[i];
            }
            if (nums[i] < min) {
                min = nums[i];
            }
        }
        if (max == min) {
            return 0;
        }

        int range = Math.max(1, (max - min) / nums.length);
        int size = (max - min) / range + 1;
        Pair[] buckets = new Pair[size];

        // 2. 放入数据
        for (int num : nums) {
            int idx = (num - min) / range;
            if (buckets[idx] == null) {
                buckets[idx] = new Pair();
            }
            buckets[idx].add(num);
        }
        // 3. 寻找最大差值
        int r = 0;
        int lastMax = buckets[0].max;
        for (int i = 1; i < buckets.length; i++) {
            Pair pair = buckets[i];
            if (pair != null) {
                r = Math.max(r, pair.min - lastMax);
                lastMax = pair.max;
            }
        }
        return r;
    }
}

解法四:基数排序。执行耗时68ms

java 复制代码
    public int maximumGap(int[] nums) {
        // 0. 数组长度小于2,直接返回0
        if(nums.length < 2) {
            return 0;
        }
        // 1. 计算数组的最大值
        int max = nums[0];
        for (int num : nums) {
            max = Math.max(num, max);
        }

        // 2. 准备10个桶
        ArrayList<Integer>[] buckets = new ArrayList[10];
        for (int i = 0; i < buckets.length; i++) {
            buckets[i] = new ArrayList<>();
        }

        // 3. 没超过最大值
        long exp = 1;
        while(max >= exp) {
            for (int num : nums) {
                buckets[(num / (int) exp % 10)].add(num);
            }
            int k = 0;
            for (ArrayList<Integer> bucket : buckets) {
                for (Integer i : bucket) {
                    nums[k++] = i;
                }
                bucket.clear();
            }
            exp *= 10;
        }

        // 4. 求最大间距
        int r = 0;
        for (int i = 1; i < nums.length; i++) {
            r = Math.max(r, nums[i] - nums[i - 1]);
        }
        return r;
    }

4. 排序数组

给你一个整数数组 nums,请你将该数组升序排列。

示例 1:

复制代码
输入:nums = [5,2,3,1]
输出:[1,2,3,5]

示例 2:

复制代码
输入:nums = [5,1,1,2,0,0]
输出:[0,0,1,1,2,5]

提示:

  • 1 <= nums.length <= 5 * 10^4
  • -5 * 10^4 <= nums[i] <= 5 * 10^4

解法一:单边快排(随机轴值)。执行耗时1918ms

java 复制代码
class Solution {
    public static int[] sortArray(int[] nums) {
        quickSort(nums, 0, nums.length - 1);
        return nums;
    }

    private static void quickSort(int[] nums, int left, int right) {
        if (left < right) {
            int pivot = partition(nums, left, right);
            quickSort(nums, left, pivot - 1);
            quickSort(nums, pivot + 1, right);
        }
    }

    private static int partition(int[] nums, int left, int right) {
        int idx = ThreadLocalRandom.current().nextInt(right - left + 1) + left;
        swap(nums, idx, right);
        int pivot = nums[right];
        int i = left;
        for (int j = left; j < right; j++) {
            if (nums[j] < pivot) {
                if (i != j) {
                    swap(nums, i, j);
                }
                i++;
            }
        }
        swap(nums, i, right);
        return i;
    }

    public static void swap(int[] a, int i, int j) {
        int t = a[i];
        a[i] = a[j];
        a[j] = t;
    }
}

解法二:双边快排(随机)。执行耗时1643ms

java 复制代码
class Solution {
    public static int[] sortArray(int[] nums) {
        quickSort(nums, 0, nums.length - 1);
        return nums;
    }

    private static void quickSort(int[] nums, int left, int right) {
        if (left < right) {
            int pivot = partition(nums, left, right);
            quickSort(nums, left, pivot - 1);
            quickSort(nums, pivot + 1, right);
        }
    }

    private static int partition(int[] nums, int left, int right) {
        int idx = ThreadLocalRandom.current().nextInt(right - left + 1) + left;
        swap(nums, idx, left);
        int pivot = nums[left];
        int i = left, j = right;
        while (i < j) {
            while (i < j && nums[j] > pivot) {
                j--;
            }
            while (i < j && nums[i] <= pivot) {
                i++;
            }
            swap(nums, i, j);
        }
        swap(nums, left, i);
        return i;
    }

    public static void swap(int[] a, int i, int j) {
        int t = a[i];
        a[i] = a[j];
        a[j] = t;
    }
}

解法三:快排(随机 + 重复)。执行耗时26ms

java 复制代码
class Solution {
    public static int[] sortArray(int[] nums) {
        quickSort(nums, 0, nums.length - 1);
        return nums;
    }

    private static void quickSort(int[] nums, int left, int right) {
        if (left < right) {
            int pivot = partition(nums, left, right);
            quickSort(nums, left, pivot - 1);
            quickSort(nums, pivot + 1, right);
        }
    }

    private static int partition(int[] nums, int left, int right) {
        int idx = ThreadLocalRandom.current().nextInt(right - left + 1) + left;
        swap(nums, idx, left);
        int pivot = nums[left];
        int i = left + 1, j = right;
        while (i <= j) {
            while (i <= j && nums[i] < pivot) {
                i++;
            }
            while (i <= j && nums[j] > pivot) {
                j--;
            }
            if (i <= j) {
                swap(nums, i, j);
                i++;
                j--;
            }
        }
        swap(nums, j, left);
        return j;
    }

    public static void swap(int[] a, int i, int j) {
        int t = a[i];
        a[i] = a[j];
        a[j] = t;
    }
}

解法四:快排(随机 + 重复 + 插入)。执行耗时18ms

java 复制代码
class Solution {
    public static int[] sortArray(int[] nums) {
        quickSort(nums, 0, nums.length - 1);
        return nums;
    }

    public static void insertionSort(int[] nums, int left, int right) {
        for (int low = left + 1; low <= right; low++) {
            int t = nums[low];
            int i = low - 1;

            while (i >= left && t < nums[i]) {
                nums[i + 1] = nums[i];
                i--;
            }
            if (i != low - 1) {
                nums[i + 1] = t;
            }
        }
    }

    private static void quickSort(int[] nums, int left, int right) {
        if (right - left <= 32) {
            insertionSort(nums, left, right);
            return;
        }
        int pivot = partition(nums, left, right);
        quickSort(nums, left, pivot - 1);
        quickSort(nums, pivot + 1, right);
    }

    private static int partition(int[] nums, int left, int right) {
        int idx = ThreadLocalRandom.current().nextInt(right - left + 1) + left;
        swap(nums, idx, left);
        int pivot = nums[left];
        int i = left + 1, j = right;
        while (i <= j) {
            while (i <= j && nums[i] < pivot) {
                i++;
            }
            while (i <= j && nums[j] > pivot) {
                j--;
            }
            if (i <= j) {
                swap(nums, i, j);
                i++;
                j--;
            }
        }
        swap(nums, j, left);
        return j;
    }

    public static void swap(int[] a, int i, int j) {
        int t = a[i];
        a[i] = a[j];
        a[j] = t;
    }
}

解法五:计数排序。执行耗时3ms

java 复制代码
class Solution {
    public static int[] sortArray(int[] nums) {
        int max = nums[0];
        int min = nums[0];
        for (int i = 1; i < nums.length; i++) {
            if (nums[i] > max) {
                max = nums[i];
            }
            if (nums[i] < min) {
                min = nums[i];
            }
        }
        int[] count = new int[max - min + 1];

        for (int num : nums) {
            count[num - min]++;
        }

        int k = 0;
        for (int i = 0; i < count.length; i++) {
            while (count[i] > 0) {
                nums[k++] = i + min;
                count[i]--;
            }
        }
        return nums;
    }
}

5. 排序链表

给你链表的头结点 head ,请将其按 升序 排列并返回 排序后的链表

示例 1:

复制代码
输入:head = [4,2,1,3]
输出:[1,2,3,4]

示例 2:

复制代码
输入:head = [-1,5,3,4,0]
输出:[-1,0,3,4,5]

示例 3:

复制代码
输入:head = []
输出:[]

提示:

  • 链表中节点的数目在范围 [0, 5 * 10^4]
  • -10^5 <= Node.val <= 10^5

进阶: 你可以在 O(n log n) 时间复杂度和常数级空间复杂度下,对链表进行排序吗?

解法一:把链表中的值放入数组当中进行排序,再根据排序后的值创建新的链表返回

java 复制代码
/**
 * Definition for singly-linked list.
 * public class ListNode {
 * int val;
 * ListNode next;
 * ListNode() {}
 * ListNode(int val) { this.val = val; }
 * ListNode(int val, ListNode next) { this.val = val; this.next = next; }
 * }
 */
class Solution {
    public ListNode sortList(ListNode head) {
        if (head == null || head.next == null) {
            return head;
        }

        List<Integer> values = new ArrayList<>();
        ListNode current = head;

        while (current != null) {
            values.add(current.val);
            current = current.next;
        }
        Collections.sort(values);

        ListNode newHead = new ListNode(values.get(0));
        ListNode temp = newHead;

        for (int i = 1; i < values.size(); i++) {
            temp.next = new ListNode(values.get(i));
            temp = temp.next;
        }

        return newHead;
    }
}

解法二:快慢指针 + 合并有序链表(归并)

java 复制代码
/**
 * Definition for singly-linked list.
 * public class ListNode {
 * int val;
 * ListNode next;
 * ListNode() {}
 * ListNode(int val) { this.val = val; }
 * ListNode(int val, ListNode next) { this.val = val; this.next = next; }
 * }
 */
class Solution {
    public ListNode sortList(ListNode head) {
        if (head == null || head.next == null) {
            return head;
        }

        ListNode mid = findMiddle(head);
        ListNode secondHalf = mid.next;
        mid.next = null;

        ListNode sortedFirstHalf = sortList(head);
        ListNode sortedSecondHalf = sortList(secondHalf);

        return merge(sortedFirstHalf, sortedSecondHalf);

    }

    private ListNode merge(ListNode l1, ListNode l2) {
        ListNode dummy = new ListNode(0);
        ListNode current = dummy;

        while (l1 != null && l2 != null) {
            if (l1.val < l2.val) {
                current.next = l1;
                l1 = l1.next;
            } else {
                current.next = l2;
                l2 = l2.next;
            }
            current = current.next;
        }
        current.next = l1 != null ? l1 : l2;

        return dummy.next;
    }

    private ListNode findMiddle(ListNode head) {
        ListNode slow = head;
        ListNode fast = head.next;
        while (fast != null && fast.next != null) {
            slow = slow.next;
            fast = fast.next.next;
        }
        return slow;
    }
}

6. 数组的相对顺序

给你两个数组,arr1arr2arr2 中的元素各不相同,arr2 中的每个元素都出现在 arr1 中。

arr1 中的元素进行排序,使 arr1 中项的相对顺序和 arr2 中的相对顺序相同。未在 arr2 中出现过的元素需要按照升序放在 arr1 的末尾。

示例 1:

复制代码
输入:arr1 = [2,3,1,3,2,4,6,7,9,2,19], arr2 = [2,1,4,3,9,6]
输出:[2,2,2,1,4,3,3,9,6,7,19]

示例 2:

复制代码
输入:arr1 = [28,6,22,8,44,17], arr2 = [22,28,8,6]
输出:[22,28,8,6,17,44]

提示:

  • 1 <= arr1.length, arr2.length <= 1000
  • 0 <= arr1[i], arr2[i] <= 1000
  • arr2 中的元素 arr2[i] 各不相同
  • arr2 中的每个元素 arr2[i] 都出现在 arr1

解法一:

java 复制代码
class Solution {
    public int[] relativeSortArray(int[] arr1, int[] arr2) {
        // 1. 记录每个元素出现的次数
        int[] count = new int[1001];
        for (int i : arr1) {
            count[i]++;
        }
        int[] result = new int[arr1.length];
        int k = 0;
        // 2. 如果元素有在arr2出现,添加到result中
        for (int i : arr2) {
            while (count[i] > 0) {
                result[k++] = i;
                count[i]--;
            }
        }
        // 3. 处理不在arr2中的元素
        for (int i = 0; i < count.length; i++) {
            while (count[i] > 0) {
                result[k++] = i;
                count[i]--;
            }
        }
        return result;
    }
}

7. 按照频率将数组升序排序

给你一个整数数组 nums ,请你将数组按照每个值的频率 升序 排序。如果有多个值的频率相同,请你按照数值本身将它们 降序 排序。

请你返回排序后的数组。

示例 1:

复制代码
输入:nums = [1,1,2,2,2,3]
输出:[3,1,1,2,2,2]
解释:'3' 频率为 1,'1' 频率为 2,'2' 频率为 3 。

示例 2:

复制代码
输入:nums = [2,3,1,3,2]
输出:[1,3,3,2,2]
解释:'2' 和 '3' 频率都为 2 ,所以它们之间按照数值本身降序排序。

示例 3:

复制代码
输入:nums = [-1,1,-6,4,5,-6,1,4,1]
输出:[5,-1,4,4,-6,-6,1,1,1]

提示:

  • 1 <= nums.length <= 100
  • -100 <= nums[i] <= 100

解法一:计数排序

java 复制代码
class Solution {
    public int[] frequencySort(int[] nums) {
        // 1. 记录每个元素出现的次数
        int[] count = new int[201];
        for (int num : nums) {
            // 加100是为了处理num为负数的情况
            count[num + 100]++;
        }

        return Arrays.stream(nums).boxed().sorted((a, b) -> {
            int fa = count[a + 100];
            int fb = count[b + 100];
            if(fa == fb) {
                // 如果频率相同,按照数值本身将它们降序排序
                return Integer.compare(b, a);
            } else {
                // 按照每个值的频率升序排序
                return fa - fb;
            }
        }).mapToInt(Integer::intValue).toArray();
    }
}

优化:

java 复制代码
import java.util.*;  

class Solution {  
    public int[] frequencySort(int[] nums) {  
        // 1. 记录每个元素出现的次数  
        Map<Integer, Integer> count = new HashMap<>();  
        
        for (int num : nums) {  
            count.put(num, count.getOrDefault(num, 0) + 1);  
        }  

        // 2. 使用 List 存储元素及其频率,方便排序  
        List<Map.Entry<Integer, Integer>> entryList = new ArrayList<>(count.entrySet());  
        
        // 3. 按照频率升序,然后数值降序排序  
        entryList.sort((a, b) -> {  
            if (a.getValue().equals(b.getValue())) {  
                return Integer.compare(b.getKey(), a.getKey()); // 按数值降序  
            }  
            return Integer.compare(a.getValue(), b.getValue()); // 按频率升序  
        });  

        // 4. 构造结果数组  
        int index = 0;  
        for (Map.Entry<Integer, Integer> entry : entryList) {  
            int frequency = entry.getValue();  
            for (int i = 0; i < frequency; i++) {  
                nums[index++] = entry.getKey();  
            }  
        }  

        return nums;  
    }  
}

8. 计算右侧小于当前元素的个数

给你一个整数数组 nums,按要求返回一个新数组 counts。数组 counts 有该性质: counts[i] 的值是 nums[i] 右侧小于 nums[i] 的元素的数量。

示例 1:

复制代码
输入:nums = [5,2,6,1]
输出:[2,1,1,0] 
解释:
5 的右侧有 2 个更小的元素 (2 和 1)
2 的右侧仅有 1 个更小的元素 (1)
6 的右侧有 1 个更小的元素 (1)
1 的右侧有 0 个更小的元素

示例 2:

复制代码
输入:nums = [-1]
输出:[0]

示例 3:

复制代码
输入:nums = [-1,-1]
输出:[0,0]

提示:

  • 1 <= nums.length <= 10^5
  • -10^4 <= nums[i] <= 10^4

解法一:暴力解。超出时间限制

java 复制代码
class Solution {
    public List<Integer> countSmaller(int[] nums) {
        List<Integer> counts = new ArrayList<>();
        int n = nums.length;
        for (int i = 0; i < n; i++) {
            int count = 0;
            for (int j = n - 1; j >= i; j--) {
                if (nums[j] < nums[i]) {
                    count++;
                }
            }
            counts.add(count);
        }
        return counts;
    }
}

解法二:

java 复制代码
class Solution {
    public List<Integer> countSmaller(int[] nums) {
        int n = nums.length;
        List<Integer> res = new ArrayList<>(n);

        int[] index = new int[n];
        int[] helper = new int[n];
        int[] count = new int[n];

        for (int i = 0; i < n; i++) {
            index[i] = i;
        }

        merge(nums, index, helper, count, 0, nums.length - 1);

        for (int i = 0; i < n; i++) {
            res.add(i, count[i]);
        }
        return res;
    }

    private void merge(int[] nums, int[] index, int[] helper, int[] count, int left, int right) {
        if (left == right || left > right)
            return;
        int mid = (left + right) >> 1;

        if (left < mid) {
            merge(nums, index, helper, count, left, mid);
        }

        if (mid + 1 < right) {
            merge(nums, index, helper, count, mid + 1, right);
        }

        int i = left, j = mid + 1;
        int hi = left;
        while (i <= mid && j <= right) {
            if (nums[index[i]] <= nums[index[j]]) {
                helper[hi++] = index[j++];
            } else {
                count[index[i]] += right - j + 1;
                helper[hi++] = index[i++];
            }
        }
        while (i <= mid) {
            helper[hi++] = index[i++];
        }

        while (j <= right) {
            helper[hi++] = index[j++];
        }

        for (int k = left; k <= right; k++) {
            index[k] = helper[k];
        }
    }
}

9. 前k个高频元素

给你一个整数数组 nums 和一个整数 k ,请你返回其中出现频率前 k 高的元素。你可以按 任意顺序 返回答案。

示例 1:

复制代码
输入: nums = [1,1,1,2,2,3], k = 2
输出: [1,2]

示例 2:

复制代码
输入: nums = [1], k = 1
输出: [1]

提示:

  • 1 <= nums.length <= 10^5
  • k 的取值范围是 [1, 数组中不相同的元素的个数]
  • 题目数据保证答案唯一,换句话说,数组中前 k 个高频元素的集合是唯一的

进阶: 你所设计算法的时间复杂度 必须 优于 O(n log n) ,其中 n是数组大小。

解法一:HashMap + 小根堆

(图片来自. - 力扣(LeetCode)

java 复制代码
public class Solution {
    public int[] topKFrequent(int[] nums, int k) {
        HashMap<Integer, Integer> count = new HashMap<>();

        // 1. 使用HashMap统计每个元素出现的次数
        for (int num : nums) {
            count.put(num, count.getOrDefault(num, 0) + 1);
        }

        // 2. 使用一个小根堆来维护前k个高频元素
        PriorityQueue<Map.Entry<Integer, Integer>> minHeap = new PriorityQueue<>(k,
                (a, b) -> a.getValue() - b.getValue());
        for (Map.Entry<Integer, Integer> entry : count.entrySet()) {
            minHeap.offer(entry);
            // 如果堆的大小超过k,移除频率最低的元素,最后剩下的k个元素即为所求
            if (minHeap.size() > k) {
                minHeap.poll();
            }
        }

        // 3. 获取结果
        int i = 0;
        int[] result = new int[k];
        while (!minHeap.isEmpty()) {
            result[i++] = minHeap.poll().getKey();
        }
        return result;
    }
}

解法二:HashMap + 桶排序

java 复制代码
class Solution {
    public int[] topKFrequent(int[] nums, int k) {
        HashMap<Integer, Integer> count = new HashMap<>();
        List<Integer> res = new ArrayList<>();

        // 1. 使用HashMap统计每个元素出现的次数
        for (int num : nums) {
            count.put(num, count.getOrDefault(num, 0) + 1);
        }

        // 2. 将频率作为数组下标,对于出现频率不同的数字集合,存入对应的数组下标
        List<Integer>[] list = new List[nums.length + 1];
        for (Integer key : count.keySet()) {
            int i = count.get(key);
            if (list[i] == null) {
                list[i] = new ArrayList<>();
            }
            list[i].add(key);
        }

        // 3. 倒序遍历数组获取出现频率从大到小的排列
        for (int i = list.length - 1; i >= 0 && res.size() < k; i--) {
            if (list[i] == null) {
                continue;
            }
            res.addAll(list[i]);
        }
        return res.stream().mapToInt(Integer::intValue).toArray();
    }
}

10. 颜色分类

给定一个包含红色、白色和蓝色、共 n个元素的数组 nums ,**原地**对它们进行排序,使得相同颜色的元素相邻,并按照红色、白色、蓝色顺序排列。

我们使用整数 012 分别表示红色、白色和蓝色。

必须在不使用库内置的 sort 函数的情况下解决这个问题。

示例 1:

复制代码
输入:nums = [2,0,2,1,1,0]
输出:[0,0,1,1,2,2]

示例 2:

复制代码
输入:nums = [2,0,1]
输出:[0,1,2]

提示:

  • n == nums.length
  • 1 <= n <= 300
  • nums[i]012

进阶:

  • 你能想出一个仅使用常数空间的一趟扫描算法吗?

解法一:维护三个指针,分别指向当前分析的元素、红色的末尾边界和蓝色的起始边界。

java 复制代码
class Solution {
    public void sortColors(int[] nums) {
        int zeroIndex = 0;
        int twoIndex = nums.length - 1;
        int current = 0;

        while (current <= twoIndex) {
            if (nums[current] == 0) {
                // 红色
                swap(nums, current, zeroIndex);
                zeroIndex++;
                current++;
            } else if (nums[current] == 2) {
                // 蓝色
                swap(nums, current, twoIndex);
                twoIndex--;
            } else {
                // 白色
                current++;
            }
        }
    }

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

11. 数组中的第k个最大元素

给定整数数组 nums 和整数 k,请返回数组中第 k 个最大的元素。

请注意,你需要找的是数组排序后的第 k 个最大的元素,而不是第 k 个不同的元素。

你必须设计并实现时间复杂度为 O(n) 的算法解决此问题。

示例 1:

复制代码
输入: [3,2,1,5,6,4], k = 2
输出: 5

示例 2:

复制代码
输入: [3,2,3,1,2,4,5,5,6], k = 4
输出: 4

提示:

  • 1 <= k <= nums.length <= 10^5
  • -10^4 <= nums[i] <= 10^4

解法一:自定义堆小根堆。执行耗时21ms

java 复制代码
class Solution {
    static class MinHeap {
        int[] array;
        int size;

        public MinHeap(int capacity) {
            array = new int[capacity];
        }

        public int peek() {
            return array[0];
        }

        public boolean offer(int offered) {
            if (size == array.length) {
                return false;
            }
            up(offered);
            size++;
            return true;
        }

        public void replace(int replaced) {
            array[0] = replaced;
            down(0);
        }

        private void up(int offered) {
            int child = size;
            while (child > 0) {
                int parent = (child - 1) >> 1;
                if (offered < array[parent]) {
                    array[child] = array[parent];
                } else {
                    break;
                }
                child = parent;
            }
            array[child] = offered;
        }

        private void down(int parent) {
            int left = (parent << 1) + 1;
            int right = left + 1;
            int min = parent;
            if (left < size && array[left] < array[min]) {
                min = left;
            }
            if (right < size && array[right] < array[min]) {
                min = right;
            }
            if (min != parent) {
                swap(min, parent);
                down(min);
            }
        }

        // 交换两个索引处的元素
        private void swap(int i, int j) {
            int t = array[i];
            array[i] = array[j];
            array[j] = t;
        }
    }

    public int findKthLargest(int[] nums, int k) {
        MinHeap heap = new MinHeap(k);
        // 先将k个元素入堆
        for (int i = 0; i < k; i++) {
            heap.offer(nums[i]);
        }
        // 将剩余的元素与堆顶元素比较,如果比堆顶元素大则替换。比较完成后,堆顶元素即为第k大的元素(小根堆)
        for (int i = k; i < nums.length; i++) {
            if (nums[i] > heap.peek()) {
                heap.replace(nums[i]);
            }
        }

        return heap.peek();
    }
}

解法二:堆排序(优先队列)。执行耗时63ms

java 复制代码
class Solution {
    public int findKthLargest(int[] nums, int k) {
        PriorityQueue<Integer> minHeap = new PriorityQueue<>();
        for (int num : nums) {
            minHeap.offer(num);
            if(minHeap.size() > k) {
                minHeap.poll();
            }
        }
        return minHeap.peek();
    }
}

解法三:快排(随机 + 处理重复 + 插入)。执行耗时28ms

java 复制代码
class Solution {

    public int findKthLargest(int[] nums, int k) {
        quickSort(nums, 0, nums.length - 1);
        return nums[nums.length - k];
    }

    private static void quickSort(int[] nums, int left, int right) {
        if (right - left <= 32) {
            insertionSort(nums, left, right);
            return;
        }
        int pivot = partition(nums, left, right);
        ;
        quickSort(nums, left, pivot - 1);
        quickSort(nums, pivot + 1, right);
    }

    private static int partition(int[] nums, int left, int right) {
        int idx = ThreadLocalRandom.current().nextInt(right - left + 1) + left;
        swap(nums, idx, left);
        int i = left + 1, j = right;
        int pivot = nums[left];

        while (i <= j) {
            while (i <= j && nums[i] < pivot) {
                i++;
            }
            while (i <= j && nums[j] > pivot) {
                j--;
            }
            if (i <= j) {
                swap(nums, i, j);
                i++;
                j--;
            }
        }
        swap(nums, j, left);
        return j;
    }

    private static void insertionSort(int[] nums, int left, int right) {
        for (int low = left + 1; low <= right; low++) {
            int t = nums[low];
            int i = low - 1;
            while (i >= left && t < nums[i]) {
                nums[i + 1] = nums[i];
                i--;
            }
            if (i != low - 1) {
                nums[i + 1] = t;
            }
        }
    }

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

12. 翻转对

给定一个数组 nums ,如果 i < jnums[i] > 2*nums[j] 我们就将 (i, j) 称作一个重要翻转对

你需要返回给定数组中的重要翻转对的数量。

示例 1:

复制代码
输入: [1,3,2,3,1]
输出: 2

示例 2:

复制代码
输入: [2,4,3,5,1]
输出: 3

注意:

  1. 给定数组的长度不会超过50000
  2. 输入数组中的所有数字都在32位整数的表示范围内。

解法一:归并排序

java 复制代码
class Solution {
    public int reversePairs(int[] nums) {
        if (nums == null || nums.length < 2) {
            return 0;
        }
        return mergeSort(nums, 0, nums.length - 1);
    }

    private int mergeSort(int[] nums, int left, int right) {
        if (left >= right) {
            return 0;
        }
        int mid = left + (right - left) / 2;

        // 统计左半部分和右半部分的翻转对
        int count = mergeSort(nums, left, mid) + mergeSort(nums, mid + 1, right);
        // 统计跨越两部分的翻转对数量
        count += countImportantPairs(nums, left, mid, right);
        // 合并两个部分
        merge(nums, left, mid, right);

        return count;
    }

    private void merge(int[] nums, int left, int mid, int right) {
        int[] temp = new int[right - left + 1];
        int i = left, j = mid + 1, k = 0;

        while (i <= mid && j <= right) {
            if (nums[i] <= nums[j]) {
                temp[k++] = nums[i++];
            } else {
                temp[k++] = nums[j++];
            }
        }

        while (i <= mid) {
            temp[k++] = nums[i++];
        }

        while (j <= right) {
            temp[k++] = nums[j++];
        }
        System.arraycopy(temp, 0, nums, left, temp.length);
    }

    private int countImportantPairs(int[] nums, int left, int mid, int right) {
        int count = 0;
        int j = mid + 1;

        // 使用双指针法,j 代表右边数组的指针
        for (int i = left; i <= mid; i++) {
            // 转换为long处理超出Integer.MAX_VALUE -> 2147483647的情况
            while (j <= right && (long) nums[i] > 2 * (long) nums[j]) {
                j++;
            }
            // 当前 j 的位置表示 (mid + 1) 到 j - 1 都和 nums[i] 满足条件
            count += (j - (mid + 1));
        }
        return count;
    }
}

13. 通过删除字母匹配到字典里最长单词

给你一个字符串 s 和一个字符串数组 dictionary ,找出并返回 dictionary 中最长的字符串,该字符串可以通过删除 s 中的某些字符得到。

如果答案不止一个,返回长度最长且字母序最小的字符串。如果答案不存在,则返回空字符串。

示例 1:

复制代码
输入:s = "abpcplea", dictionary = ["ale","apple","monkey","plea"]
输出:"apple"

示例 2:

复制代码
输入:s = "abpcplea", dictionary = ["a","b","c"]
输出:"a"

提示:

  • 1 <= s.length <= 1000
  • 1 <= dictionary.length <= 1000
  • 1 <= dictionary[i].length <= 1000
  • sdictionary[i] 仅由小写英文字母组成

解法一:

  1. 检查子序列:通过遍历字典中的每个字符串,检查它是否是s中的一个子序列

  2. 比较长度和字典序:对于所有符合条件的字符串,选择最长的一个。如果有多个长度相同的字符串,则选择字典序较小的那个

java 复制代码
    public String findLongestWord(String s, List<String> dictionary) {
        String result = "";

        for (String word : dictionary) {
            if(isSubsequence(word, s)) {
                // 比较长度和字典序
                if(word.length() > result.length() || (word.length() == result.length() && word.compareTo(result) < 0))  {
                    result = word;
                }
            }
        }

        return result;
    }

    /**
     * 检查字典中的每个字符串是否为s的一个子序列
     * @param word
     * @param s
     * @return
     */
    private boolean isSubsequence(String word, String s) {
        int wIndex = 0, sIndex = 0;
        while(wIndex < word.length() && sIndex < s.length()) {
            if(word.charAt(wIndex) == s.charAt(sIndex)) {
                wIndex++;
            }
            sIndex++;
        }

        return wIndex == word.length();
    }

14. 有序数组的平方

给你一个按 非递减顺序 排序的整数数组 nums,返回 每个数字的平方 组成的新数组,要求也按 非递减顺序 排序。

示例 1:

复制代码
输入:nums = [-4,-1,0,3,10]
输出:[0,1,9,16,100]
解释:平方后,数组变为 [16,1,0,9,100]
排序后,数组变为 [0,1,9,16,100]

示例 2:

复制代码
输入:nums = [-7,-3,2,3,11]
输出:[4,9,9,49,121]

提示:

  • 1 <= nums.length <= 10^4
  • -10^4 <= nums[i] <= 10^4
  • nums 已按 非递减顺序 排序

进阶:

  • 请你设计时间复杂度为 O(n) 的算法解决本问题

解法一:双指针法。大的元素添加到结果数组末尾

java 复制代码
class Solution {
    public int[] sortedSquares(int[] nums) {
        int n = nums.length;
        int[] result = new int[n];
        // 双指针法
        int left = 0, right = n - 1;
        int position = n - 1;

        while (left <= right) {
            int leftSquare = nums[left] * nums[left];
            int rightSquare = nums[right] * nums[right];

            // 大的添加到结果数组
            if (leftSquare > rightSquare) {
                result[position--] = leftSquare;
                left++;
            } else {
                result[position--] = rightSquare;
                right--;
            }
        }
        return result;
    }
}
相关推荐
迷途之人不知返28 分钟前
数据结构之,栈与队列
数据结构
MATLAB代码顾问1 小时前
多种时间序列预测算法的MATLAB实现
开发语言·算法·matlab
高山上有一只小老虎2 小时前
字符串字符匹配
java·算法
愚润求学3 小时前
【动态规划】专题完结,题单汇总
算法·leetcode·动态规划
MOONICK3 小时前
数据结构——哈希表
数据结构·哈希算法·散列表
林太白3 小时前
跟着TRAE SOLO学习两大搜索
前端·算法
ghie90903 小时前
图像去雾算法详解与MATLAB实现
开发语言·算法·matlab
云泽8083 小时前
从三路快排到内省排序:探索工业级排序算法的演进
算法·排序算法
weixin_468466854 小时前
遗传算法求解TSP旅行商问题python代码实战
python·算法·算法优化·遗传算法·旅行商问题·智能优化·np问题
FMRbpm4 小时前
链表5--------删除
数据结构·c++·算法·链表·新手入门