求数组最小的k个数

本文介绍两种寻找数组中最小k个数的高效算法:一种利用partition函数实现O(n)时间复杂度,另一种利用最大堆实现O(nlogk)时间复杂度。详细解析了各自的实现原理、时间复杂度分析及优缺点。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

写两种聪明的解法(更推荐第二种,原因见优缺点)。计数或者排序这些朴素的解法就不说了。

1. 利用partition函数的O(n)解法

实现

  1. 定义partition函数,partition函数必须满足:传入一个数组和一个范围,返回一个index,在index左边的所有数字小于等于array[index],在index右边的所有数字大于等于array[index](这里和快速排序中partition函数的要求不一样,在快速排序中如果使用Hoare Scheme或其变体形式的快速排序[注:即递归排序的两部分是相连的,而非被index隔开],仅仅要求被index分割的左半边数组小于等于右半边数组)
  2. 不断地循环进行partition,直到index == n - 1,此时array[0...index]就是最小的n个数
private static int[] firstN(int[] a, int k) {
    int[] aCopy = Arrays.copyOf(a, a.length);

    int left = 0, right = aCopy.length - 1;
    int index = partition(aCopy, left, right);
    while (index != k - 1) {
        if (index < k - 1) {
            left = index + 1;
            index = partition(aCopy, left, right);
        } else {
            right = index - 1;
            index = partition(aCopy, left, right);
        }
    }

    int[] result = new int[k];
    System.arraycopy(aCopy, 0, result, 0, k);
    return result;
}

private static int partition(int[] a, int left, int right) {
    swap(a, right, left + new Random(0).nextInt(right - left + 1));
    int pivotValue = a[right];

    int divideIndex = left;
    // array[left...divideIndex - 1] < pivotValue
    for (int i = left; i < right; i++) {
        if (a[i] < pivotValue) {
            swap(a, i, divideIndex);
            divideIndex++;
        }
    }
    swap(a, right, divideIndex);

    // array[left...divideIndex - 1] < pivotValue && array[divideIndex] == pivotValue
    return divideIndex;
}

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

时间复杂度分析

平均时间复杂度:假设数组长度为n,partition所用时间为pn,那么由于每次都大约把原来数组划分为两半,平均所用时间 T(n) = p * (n + n / 2 + n / 4 + …) = O(n)

优缺点

  • 优点
  • 缺点
    1. partition会改变数组,因此需要拷贝原来的数组
    2. 当数组很大以至于无法全部读取到内存中时不能使用
    3. 有Stack Overflow的风险 (勘误:此条错误)
    4. 最坏情况下会退化到O(n^2)的时间复杂度,这和快排一样

2. 利用最大堆的O(nlogk)解法

最大堆的定义:

  1. 最大堆是一颗完全二叉树
  2. 最大堆的根的值大于等于左右孩子的值
  3. 最大堆左右子树也是最大堆

最大堆可以被放进一个数组。如果把根放在index为0的位置,那么heap[i]的左右孩子分别是heap[i * 2 + 1]heap[i * 2 + 2],父亲为heap[(i - 1) / 2],最后一片叶子为heap[heap.length - 1]

实现

  1. 建立一个最大堆
  2. 对于数组中每一个元素,如果该元素大于等于最大堆的根,那么这个元素肯定不是最小的k个数之一,直接丢弃;如果该元素小于最大堆的根,那么把最大堆的根替换为该元素,并调整最大堆以使其符合定义
  3. 迭代完数组所有的元素后,堆中所存的就是最小的k个元素(可以通过数学归纳法/循环不变量来证明)
private static int[] firstN(int[] a, int k) {
    int[] heap = new int[k];
    System.arraycopy(a, 0, heap, 0, k);

    // build heap
    for (int i = (k - 1) / 2; i >= 0; i--)
        adjustHeap(heap, i);

    for (int i = k; i < a.length; i++) {
        if (a[i] < heap[0]) {
            heap[0] = a[i];
            adjustHeap(heap, 0);
        }
    }

    return heap;
}

private static void adjustHeap(int[] heap, int root) {
    int max = root;
    int left = root * 2 + 1, right = root * 2 + 2;

    if (left < heap.length && heap[left] > heap[max]) {
        max = left;
        swap(heap, root, left);
    }

    if (right < heap.length && heap[right] > heap[max]) {
        max = right;
        swap(heap, root, right);
    }

    if (max != root) {
        adjustHeap(heap, max);
    }
}

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

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

时间复杂度分析

调整堆的时间复杂度为O(logk),在整个过程中调整了n次堆,所以时间复杂度为O(nlogk),平均和最坏情况下都是这样的

优缺点

  • 优点
    1. 稳定可靠,最坏情况下也有O(nlogk)的速度
    2. 可以排序很大的数组
    3. 实现简单,不容易出bug
  • 缺点
    1. 平均比第一种方式稍微慢一点
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值