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

一、概述

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,即为右边界

递归实现

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

非递归实现

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

要点:

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

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

非递归实现

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. 堆排序

要点:

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

建堆

交换,下潜调整

代码实现

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 位置的元素,插入到已排序区域

例如,

递归实现

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

非递归实现

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的三轮排序前后对比

代码

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 递归实现(自上而下)

要点:

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

代码

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 非递归排序

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. 实现过程:

  • 在归并排序的分解阶段,将数组分割为较小的子数组
  • 当子数组的大小达到设定的阈值时,停止递归,使用插入排序对该子数组进行排序
  • 在较大的子数组上继续使用归并排序

3. 优势:

  • 小规模数据使用插入排序速度更快
  • 归并排序保证了整体的O(n log n)性能,但通过在小规模时切换到插入排序可以减少常数因子,提升性能。
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交换,一轮分区结束

代码:

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位置的元素交互

代码

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 随机基准点

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

改进代码

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

8.2 处理重复值

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

改进代码

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 快速排序 + 插入排序

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数组
  • 2. count数组的索引对应原始数组的元素,用来统计该元素的出现次数
  • 3. 遍历count数组,根据count数组的索引(即原始数组的元素)以及出现次数,生成排序后的内容
  • count数组的索引是:已排序好的
  • 前提:最大值不能太大
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排序。

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

稳定计数排序

    /**
     * 稳定版计数排序
     * @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. 桶排序

初步实现

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

通用

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的位数。

2. 按位排序

  • 基数排序通过多次(位数个数)使用一个稳定的排序算法(通常是计数排序)来对数字进行排序,以下是逐步的过程:
  • 从最低位开始:首先对每个数字的最低位进行排序,得到的结果是基于该位的排序
  • 接着是次低位:然后对于每个数字的次低位进行排序,虽然这个排序会受到前一次排序结果的影响,但依然是基于该新的位进行的
  • 重复直到最高位:继续这个过程,直到处理到最高位

例如

代码:

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. 数组的相对排序

给你两个数组,arr1 和 arr2arr2 中的元素各不相同,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 中

解法一:计数排序

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

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
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

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

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

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

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

    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

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

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

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

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

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) 时间复杂度和常数级空间复杂度下,对链表进行排序吗?

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

/**
 * 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;
    }
}

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

/**
 * 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. 数组的相对顺序

给你两个数组,arr1 和 arr2arr2 中的元素各不相同,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 中

解法一:

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

解法一:计数排序

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

优化:

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

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

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

解法二:

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)

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 + 桶排序

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 ,原地 对它们进行排序,使得相同颜色的元素相邻,并按照红色、白色、蓝色顺序排列。

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

必须在不使用库内置的 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] 为 01 或 2

进阶:

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

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

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

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

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

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 < j 且 nums[i] > 2*nums[j] 我们就将 (i, j) 称作一个重要翻转对

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

示例 1:

输入: [1,3,2,3,1]
输出: 2

示例 2:

输入: [2,4,3,5,1]
输出: 3

注意:

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

解法一:归并排序

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
  • s 和 dictionary[i] 仅由小写英文字母组成

解法一:

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

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

    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) 的算法解决本问题

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

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

  • 12
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值