寻找第k大的数(转)

1、排序解决法

如果是一个有序数组,那么寻找第k的大数则相当简单了,且效率为1。数组排序算法中相对较优的算法为快速排序,效率为N*lgN,将数组从到到小排列,第k大的数则为array[k-1]。

快排的思想为,从数组中取任意一个值key,将大于key的值放在key右边,小于key的值放在key左边。key的左边和右边则都是有序的了,然后递归key左边的子数组和key右边的子数组,直到每个子数组长度为1,此时,整个数组均有序了。

public static int partition(int[] array, int left, int right) {
    int k = array[left];
    int i = left;
    int j = right;
    while (j > i) {
        while (array[j] < k && j > i) {
            j--;
        }
        if (j > i) {
            array[i] = array[j];
            i++;
        }
        while (array[i] > k && j > i) {
            i++;
        }
        if (j > i) {
            array[j] = array[i];
            j--;
        }
    }
    array[i] = k;
    return i;
}

public static void quickSort(int[] array, int left, int right) {
    if (left >= right) {
        return;
    }
    int i = partition(array, left, right);
    quickSort(array, left, i - 1);
    quickSort(array, i + 1, right);
}

本文中快排略有差异,是按从大到小顺序排列。
快排的partition算法有两种写法,具体可查看快速排序及主定理。此解法效率为N*lgN

2、类快排解法

由于只要求找出第k大的数,没必要将数组中所有值都排序。

快排中的partition算法,返回key在数组中的位置,如果key的位置正好等于k-1,那么问题则得到解决,如果key的位置不等于k-1,可使用递归查找对应子数组。直到key的位置等于k-1,则找对问题的解。

public static int findK(int[] array, int left, int right, int k) {
    int i = partition(array, left, right);
    if (i == k - 1) {
        return array[k - 1];
    } else if (i > k - 1) {
        return findK(array, left, i - 1, k);
    } else if (i < k - 1) {
        return findK(array, i + 1, right, k);
    }
    return 0;
}

此解法的效率值为N*lgK,由于K是常数,所以此解法效率值为N,优于排序解法

3、最小堆解法

最小堆是一种特殊的数组结构,它实质是一个完全二叉树,且树中子节点的值均大于父节点的值,详见 堆排序及优先队列

考虑到只需要找到第k大的数,构造一个大小为k的最小堆,堆中根节点为最小值。如果数组中最大的几个数均在堆中,那么堆中根节点的值就是问题的解。

构造最小堆

void minheapfix(int a[], int i, int n)
{
    int temp = a[i];
    int j = 2 * i + 1;
    while (j < n)
    {
        if (j + 1 < n&&a[j + 1] < a[j])
        {
            j = j + 1;
        }
        if (a[j] > temp)
            break;
        a[i] = a[j];
        i = j;
        j = i * 2 + 1;
    }
    a[i] = temp;
}

void buildheap(int a[], int n)
{
    for (int i = n / 2 - 1; i >= 0; i--)
    {
        minheapfix(a, i, n);
    }
}

最小堆已构造完成,将数组中剩余的值与根节点相比,大于根节点的值则将根节点的值与之交换,同时维护最小堆的特性,遍历结束,则根结点即为问题的解。

int findK1(int a[], int l, int r, int k)
{
    int n = r + 1;
    buildheap(a, k);
    for (int i = k; i < n; ++i)
    {
        if (a[i] > a[0])
        {
            swap(a[i], a[0]);
            minheapfix(a, 0, k);
        }
    }
    return a[0];
}

 4、BFPRT算法

面说了快排法能够使得其时间复杂度到达期望为O(n),那能不能使得时间复杂度严格为O(n)呢?可以的,这里就引出了BFPRT算法。

首先我们可以来看一下BFPRT算法的步骤:

  1. 分组(5个一组)
  2. 小组内排序(对于每一组的排序的时间复杂度是O(1))
  3. 把每个组的中位数拿出来组成一个新的数组
  4. 使用BFPRT递归算出这个中位数组的中位数
  5. 用这个当做划分值进行划分
  6. 划分成两边以后递归调用直到找到第k个数

原理:
通过这样选到的划分数一定在0.3N-0.7N之间,如图所示

每次选取到的划分数一定都比红色的数要大,比蓝色的数要小

这样时间复杂度的迭代公式就是:

通过推导不难算出其时间复杂度严格为O(n)

#include <iostream>
#include <algorithm>

using namespace std;

int InsertSort(int array[], int left, int right);
int GetPivotIndex(int array[], int left, int right);
int Partition(int array[], int left, int right, int pivot_index);
int BFPRT(int array[], int left, int right, int k);

int main()
{
    int k = 8; // 1 <= k <= array.size
    int array[20] = { 11,9,10,1,13,8,15,0,16,2,17,5,14,3,6,18,12,7,19,4 };

    cout << "原数组:";
    for (int i = 0; i < 20; i++)
        cout << array[i] << " ";
    cout << endl;

    // 因为是以 k 为划分,所以还可以求出第 k 小值
    cout << "" << k << " 小值为:" << array[BFPRT(array, 0, 19, k)] << endl;

    cout << "变换后的数组:";
    for (int i = 0; i < 20; i++)
        cout << array[i] << " ";
    cout << endl;

    return 0;
}

/**
 * 对数组 array[left, right] 进行插入排序,并返回 [left, right]
 * 的中位数。
 */
int InsertSort(int array[], int left, int right)
{
    int temp;
    int j;

    for (int i = left + 1; i <= right; i++)
    {
        temp = array[i];
        j = i - 1;

        while (j >= left && array[j] > temp)
        {
            array[j + 1] = array[j];
            j--;
        }

        array[j + 1] = temp;
    }

    return ((right - left) >> 1) + left;
}

/**
 * 数组 array[left, right] 每五个元素作为一组,并计算每组的中位数,
 * 最后返回这些中位数的中位数下标(即主元下标)。
 *
 * @attention 末尾返回语句最后一个参数多加一个 1 的作用其实就是向上取整的意思,
 * 这样可以始终保持 k 大于 0。
 */
int GetPivotIndex(int array[], int left, int right)
{
    if (right - left < 5)
        return InsertSort(array, left, right);

    int sub_right = left - 1;

    // 每五个作为一组,求出中位数,并把这些中位数全部依次移动到数组左边
    for (int i = left; i + 4 <= right; i += 5)
    {
        int index = InsertSort(array, i, i + 4);
        swap(array[++sub_right], array[index]);
    }

    // 利用 BFPRT 得到这些中位数的中位数下标(即主元下标)
    return BFPRT(array, left, sub_right, ((sub_right - left + 1) >> 1) + 1);
}

/**
 * 利用主元下标 pivot_index 进行对数组 array[left, right] 划分,并返回
 * 划分后的分界线下标。
 */
int Partition(int array[], int left, int right, int pivot_index)
{
    swap(array[pivot_index], array[right]); // 把主元放置于末尾

    int partition_index = left; // 跟踪划分的分界线
    for (int i = left; i < right; i++)
    {
        if (array[i] < array[right])
        {
            swap(array[partition_index++], array[i]); // 比主元小的都放在左侧
        }
    }

    swap(array[partition_index], array[right]); // 最后把主元换回来

    return partition_index;
}

/**
 * 返回数组 array[left, right] 的第 k 小数的下标
 */
int BFPRT(int array[], int left, int right, int k)
{
    int pivot_index = GetPivotIndex(array, left, right); // 得到中位数的中位数下标(即主元下标)
    int partition_index = Partition(array, left, right, pivot_index); // 进行划分,返回划分边界
    int num = partition_index - left + 1;

    if (num == k)
        return partition_index;
    else if (num > k)
        return BFPRT(array, left, partition_index - 1, k);
    else
        return BFPRT(array, partition_index + 1, right, k - num);
}
原数组:11 9 10 1 13 8 15 0 16 2 17 5 14 3 6 18 12 7 19 48 小值为:7
变换后的数组:4 0 1 3 2 5 6 7 8 9 10 12 13 14 17 15 16 11 18 19
转自:https://www.jianshu.com/p/33ee33ce8699
原文地址:https://www.cnblogs.com/zl1991/p/13025336.html