zoukankan      html  css  js  c++  java
  • 寻找第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
  • 相关阅读:
    PostgreSQL数据损坏与checksum
    go get命令无响应解决方法
    vscode离线安装插件
    windows搭建Go语言环境
    centos7安装zabbix 5.0
    搭建人生重开模拟器
    博客园增加打赏
    记一次halo报错
    VM operation inconsistent with current state
    nextcloud安装onlyoffice
  • 原文地址:https://www.cnblogs.com/zl1991/p/13025336.html
Copyright © 2011-2022 走看看