zoukankan      html  css  js  c++  java
  • 寻找最小的k个数

    题目描述

    输入n个整数,输出其中最小的k个。

    分析与解法

    解法一

    要求一个序列中最小的k个数,按照惯有的思维方式,则是先对这个序列从小到大排序,然后输出前面的最小的k个数。

    至于选取什么的排序方法,我想你可能会第一时间想到快速排序(我们知道,快速排序平均所费时间为n*logn),然后再遍历序列中前k个元素输出即可。因此,总的时间复杂度:O(n * log n)+O(k)=O(n * log n)

    解法二

    咱们再进一步想想,题目没有要求最小的k个数有序,也没要求最后n-k个数有序。既然如此,就没有必要对所有元素进行排序。这时,咱们想到了用选择或交换排序,即:

    1、遍历n个数,把最先遍历到的k个数存入到大小为k的数组中,假设它们即是最小的k个数;
    2、对这k个数,利用选择或交换排序找到这k个元素中的最大值kmax(找最大值需要遍历这k个数,时间复杂度为O(k));
    3、继续遍历剩余n-k个数。假设每一次遍历到的新的元素的值为x,把x与kmax比较:如果x < kmax ,用x替换kmax,并回到第二步重新找出k个元素的数组中最大元素kmax‘;如果x >= kmax,则继续遍历不更新数组。

    每次遍历,更新或不更新数组的所用的时间为O(k)O(0)。故整趟下来,时间复杂度为n*O(k)=O(n*k)

    void select_sortN(vector<int> &vec, int n)
    {
        int size = vec.size();
        for(int i = 0; i < n; ++i)
        {
            int k = i;
            for(int j = i; j < size; ++j)
            {
                if(vec[k] > vec[j])
                {
                    k = j;
                }
            }
            swap(vec[k], vec[i]);
        }
    }

    解法三

    更好的办法是维护容量为k的最大堆,原理跟解法二的方法相似:

    • 1、用容量为k的最大堆存储最先遍历到的k个数,同样假设它们即是最小的k个数;
    • 2、堆中元素是有序的,令k1<k2<...<kmax(kmax设为最大堆中的最大元素)
    • 3、遍历剩余n-k个数。假设每一次遍历到的新的元素的值为x,把x与堆顶元素kmax比较:如果x < kmax,用x替换kmax,然后更新堆(用时logk);否则不更新堆。

    这样下来,总的时间复杂度:O(k+(n-k)*logk)=O(n*logk)。此方法得益于堆中进行查找和更新的时间复杂度均为:O(logk)(若使用解法二:在数组中找出最大元素,时间复杂度:O(k))

    void heap_sortN(vector<int> &vec, vector<int> &res, int n)
    {
        for(int i = 0; i != n; ++i)
        {
            make_heap(vec.begin(), vec.end(), [](int i, int j){return i > j;});
            res.push_back(*vec.begin());
            vec.erase(vec.begin());
        }
    }

    解法四

    在《数据结构与算法分析--c语言描述》一书,第7章第7.7.6节中,阐述了一种在平均情况下,时间复杂度为O(N)的快速选择算法。如下述文字:

    • 选取S中一个元素作为枢纽元v,将集合S-{v}分割成S1和S2,就像快速排序那样
      • 如果k <= |S1|,那么第k个最小元素必然在S1中。在这种情况下,返回QuickSelect(S1, k)。
      • 如果k = 1 + |S1|,那么枢纽元素就是第k个最小元素,即找到,直接返回它。
      • 否则,这第k个最小元素就在S2中,即S2中的第(k - |S1| - 1)个最小元素,我们递归调用并返回QuickSelect(S2, k - |S1| - 1)。

    此算法的平均运行时间为O(n)。

    下面代码实现了quickselect,前n个数就保存在原数组中.

    void select_minK(vector<int> &vec, int start, int end, int n)
    {
        int temp = vec[start];
        int i = start, j = end;
        bool flag = true;
        while(i != j)
        {
            if(flag)
            {
                if(vec[j] >= temp)
                {
                    --j;
                }
                else
                {
                    vec[i] = vec[j];
                    flag = false;
                    continue;
                }
            }
    
    
            if(!flag)
            {
                if(vec[i] <= temp)
                {
                    ++i;
                }
                else
                {
                    vec[j] = vec[i];
                    flag = true;
                    continue;
                }
            }
        }
        vec[i] = temp;
    
        if(i - start == n - 1)
        {
            return ;
        }
        else if(i - start < n - 1)
        {
            return select_minK(vec, i + 1, end, n - (i - start) - 1);
        }
        else
        {
            return select_minK(vec, start, i - 1, n);
        }
    }

    这个快速选择SELECT算法,类似快速排序的划分方法。N个数存储在数组S中,再从数组中选取“中位数的中位数”作为枢纽元X,把数组划分为Sa和Sb俩部分,Sa<=X<=Sb,如果要查找的k个元素小于Sa的元素个数,则返回Sa中较小的k个元素,否则返回Sa中所有元素+Sb中小的k-|Sa|个元素,这种解法在平均情况下能做到O(n)的复杂度。

    更进一步,《算法导论》第9章第9.3节介绍了一个最坏情况下亦为O(n)时间的SELECT算法,有兴趣的读者可以参看。

    举一反三

    1、谷歌面试题:输入是两个整数数组,他们任意两个数的和又可以组成一个数组,求这个和中前k个数怎么做?

    分析:

     “假设两个整数数组为A和B,各有N个元素,任意两个数的和组成的数组C有N^2个元素。
       那么可以把这些和看成N个有序数列:
              A[1]+B[1] <= A[1]+B[2] <= A[1]+B[3] <=…
              A[2]+B[1] <= A[2]+B[2] <= A[2]+B[3] <=…
              …
             A[N]+B[1] <= A[N]+B[2] <= A[N]+B[3] <=…
        问题转变成,在这N^2个有序数列里,找到前k小的元素”
    

    2、有两个序列A和B,A=(a1,a2,...,ak),B=(b1,b2,...,bk),A和B都按升序排列。对于1<=i,j<=k,求k个最小的(ai+bj)。要求算法尽量高效。

    3、给定一个数列a1,a2,a3,...,an和m个三元组表示的查询,对于每个查询(i,j,k),输出ai,ai+1,...,aj的升序排列中第k个数。

    可以用类似于快排的思路去求解,代码如下:

    int select_num(vector<int> &vec, int start, int end, int n)
    {
        int temp = vec[start];
        int i = start, j = end;
        bool flag = true;
        while(i != j)
        {
            if(flag)
            {
                if(vec[j] >= temp)
                {
                    --j;
                }
                else
                {
                    vec[i] = vec[j];
                    flag = false;
                    continue;
                }
            }
    
    
            if(!flag)
            {
                if(vec[i] <= temp)
                {
                    ++i;
                }
                else
                {
                    vec[j] = vec[i];
                    flag = true;
                    continue;
                }
            }
        }
        vec[i] = temp;
    
        if(i - start == n - 1)
        {
            return temp;
        }
        else if(i - start < n - 1)
        {
            return select_num(vec, i + 1, end, n - (i - start) - 1);
        }
        else
        {
            return select_num(vec, start, i - 1, n);
        }
    }

    这里将start和end简化为坐标进行处理, 道理不变

  • 相关阅读:
    手把手教你利用create-nuxt-app脚手架创建NuxtJS应用
    初识NuxtJS
    webpack打包Vue应用程序流程
    用选择器代替表格列的筛选功能
    Element-UI
    Spectral Bounds for Sparse PCA: Exact and Greedy Algorithms[贪婪算法选特征]
    Sparse Principal Component Analysis via Rotation and Truncation
    Generalized Power Method for Sparse Principal Component Analysis
    Sparse Principal Component Analysis via Regularized Low Rank Matrix Approximation(Adjusted Variance)
    Truncated Power Method for Sparse Eigenvalue Problems
  • 原文地址:https://www.cnblogs.com/ddddddwwwxx/p/5539423.html
Copyright © 2011-2022 走看看