编程之美2:寻找最大的K个数

根据楼楼参加笔试或者面试的经验而言,寻找最大的K个数这个问题,被问到已经不只两三次了,所以楼楼决定认认真真地把这个问题写一下,解法思想参照《编程之美》一书。

题目简介

有很多无序的数,我们姑且假定他们各不相等,怎么选出其中最大的K个数呢?

相关知识点

排序

题目解答

解法一:直接排序

这个解法是第一反应,假设有N个数,我们使用一个N个长度的数组将其存储下来,并且使用排序算法将其从大到小依次排列。排序完成后,输出前K个数。如果N不小,但是也不大,比如几千什么的,可以采用快速排序或者堆排序来完成。
代码

#include <iostream>  

using namespace std;  

int findMaxN(int *pArray, int len);


int comp(const void*a , const void*b)
{
    return *(int *)b - *(int *)a;
}
int main()  
{  
    int a[] = {9, 8, 7, 6, 5, 4, 3, 11, 12, 13, 1, 28};
    int K = 5;
    int len = sizeof(a) / sizeof(int);
    //利用快速排序法进行排序
    qsort(a, len, sizeof(int), comp);

    for (int i = 0; i < K; i++)
    {
        cout << a[i] << " ";
    }
    system("pause");
}  

复杂度分析
堆排序或者快速排序平均的复杂度为 O(NlogN)
延伸:qsort()用法

qsort(void*base, size_t num, size_t width, int(__cdecl*compare)(const void*,const void*))

第一个参数:待排序数组首地址
第二个参数:数组中待排序元素数量
第三个参数: 各元素的占用空间大小
第四个参数:指向函数的指针,用于确定排序的顺序。
以下为compare函数原型

compare( (void *) & elem1, (void *) & elem2 );
Compare 函数的返回值描述
小于 0elem1将被排在elem2前面
等于0elem1 等于 elem2
大于0elem1将被排在elem2后面

解法二:部分排序法

简单分析一下,我们就能发现解法一的一个明显不足之处,那就是我们将所有的元素都进行了排序,而题目要求只是寻找最大的K个数,也就是说我们只要将最大的K个数排好序就好了,没必要将剩下的N-K个数也进行排序。

在这里,我们可以使用快速排序来完成这个部分排序的功能。在快速排序中,每一轮都需要选定一个pivot,每一轮排序完成后,比pivot大的数都排在它前(后)面,而比pivot小的数都排在它的后(前)面。假设前面的序列为Sa,后面的序列为Sb,Sa的长度为n.
n>K 时,我们直接输出Sa的前K个元素就好了;
n=K 时,我们直接输出Sa这个序列;
n<K 时,我们就需要从Sb中找出 Kn 个元素和Sa一起输出就好了。

代码

#include <iostream>  

using namespace std;  

int kBig(int *pArray, int low, int high, int K);
int partion(int *pArray, int low, int high);

int main()  
{  
    int a[] = {1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17, 20};
    for (int i = 0; i <= kBig(a, 0, sizeof(a)/sizeof(int), 2); i++)
    {
        cout << a[i] << " ";
    }
    system("pause");
}  

//对前K大的数进行排序,并返回第K大数的下标
int kBig(int *pArray, int low, int high, int K)
{
    int index, n;
    if (low < high)
    {
        //对数组进行划分,并返回划分的位置
        index = partion(pArray, low, high);
        n = index - low + 1;     //Sa的个数
        if (n == K)     //如果恰好是K个的话,那么返回下标就可以了
        {
            return index;
        }
        if (n < K)     //如果Sa的个数不够的话,那么再从Sb中找K-n个
        {
            return kBig(pArray, index + 1, high, K - n);
        }
        if (n > K)     //如果Sa的个数大于K的话,那么就从Sa里面返回K个
        {
            return kBig(pArray, low, index, K);
        }
    }
}

//快速排序的划分函数并返回pivot的坐标
int partion(int *pArray, int low, int high)
{
    int i = low; int j = low;
    int pivot = pArray[low];
    for (; i < high, j < high;j++)
    {
        if (pArray[j] > pivot)
        {
            i++;
            swap(pArray[i], pArray[j]);
        }
    }
    swap(pArray[i], pArray[low]);
    return i;
}

复杂度分析
很显然,相对解法一而言,解法二的复杂度为 O(NlogK)

解法三:堆排序法

就楼楼的面试经验来看,如果这个问题你能答到堆排序算法的话,这时候面试官就基本满意了。这是他们想问的点,因为在他们问题里会不断地强调这个N是如何之大,内存受到限制之类的。比如如果N都是几百万的话,那用这么大的数组来存储,这就是非常不明智地做法了。用堆就可以完美解决存储问题。

#include <iostream>  

using namespace std;  

void buildMinHeap(int *pArray, int K);
void adjustHeap(int *pArray, int rootIndex, int heapSize);

int main()  
{  
    int a[] = {9, 8, 7, 6, 5, 4, 3, 11, 12, 13, 1, 28};
    int K = 5 ;

    //建一个K个元素大小的最小堆
    buildMinHeap(a, K);

    //从第K个元素开始扫描,看有没有比根节点更大的节点,若有则替换,并更新堆;若没有比根节点大则扫描下一个元素,直到数组结束
    for (int i = K; i < sizeof(a) / sizeof(int); i++)
    {
        if (a[i] > a[0])
        {
            swap(a[i], a[0]);
            adjustHeap(a, 0, K);
        }
    }

    //打印出前K大的数,没有排序。
    for (int i = 0; i < K; i++)
    {
        cout << a[i] << " ";
    }
    system("pause");
}  

//建一个K个元素大小的最小堆
void buildMinHeap(int *pArray, int K)
{
    for (int i = (K - 2) / 2; i >= 0; i--)
    {
        adjustHeap (pArray, i, K);
    }
}

void adjustHeap (int *pArray, int rootIndex, int heapSize)
{
    int minIndex = rootIndex;

    //左孩子节点
    int leftIndex = 2 * rootIndex + 1;

    //右孩子节点
    int rightIndex = 2 * (rootIndex + 1);

    //如果左孩子比根节点和右孩子节点小的话,则左孩子和根节点进行交换
    if ((leftIndex < heapSize) && (rightIndex < heapSize) && (pArray[leftIndex] < pArray[rightIndex]) && (pArray[leftIndex] < pArray[rootIndex]))
    {
        minIndex = leftIndex;
    }
    if ((leftIndex < heapSize) && (rightIndex >= heapSize) && (pArray[leftIndex] < pArray[rootIndex]))
    {
        minIndex = leftIndex;
    }


    if ((rightIndex < heapSize) && (pArray[rightIndex] < pArray[leftIndex]) && (pArray[rightIndex] < pArray[rootIndex]))
    {
        minIndex = rightIndex;
    }

    if (minIndex != rootIndex) 
    {
        //如果左孩子或者右孩子比根节点小的话,那么就交换,并且重新调整以minIndex为根节点的子树
        swap(pArray[rootIndex], pArray[minIndex]);
        adjustHeap(pArray, minIndex, heapSize);
    }
}

复杂度分析 O(NlogK) 可以看到堆排序这种做法并没有怎么提高时间复杂度,但是却极大的降低了对空间的存储要求,只需要维护一个K大小的堆。虽然上面程序同样是用了一个N大小的数组来存整个数据,但只是为了演示方便。事实上,很可能所有的数据包都是放在文件里面的,只需要扫描比较再更新堆就好了。

解法四:计数排序法

分析一下上面的三种解法,时间复杂度都不是线性的,那我们会问存不存在一种线性的解法?事实上是存在的,但存在的这种解法存在限制。解法思想如下:
如果所有N个数都是正整数,且他们的取值范围不大,我们知道最大的数是MAXN。那么我们可以申请一个数组count[MAXN]来记录每个数出现的次数。然后我们就可以找出最大的K个数。
看代码:

#include <iostream>  

using namespace std;  

int findMaxN(int *pArray, int len);

int main()  
{  
    int a[] = {9, 8, 7, 6, 5, 4, 3, 11, 12, 13, 1, 28};
    int K = 5;
    int MAXN = findMaxN(a, sizeof(a) / sizeof(int));
    //申请一个count数组,记录每一个数出现的次数
    int *count = new int[MAXN + 1]();
    for (int i = 0; i < sizeof(a) / sizeof(int); i++)
    {
        count[a[i]]++;
    }
    int index = MAXN;
    int sumCount = 0;
    for (;index >= 0; index--)
    {
        sumCount += count[index];
        if (sumCount == K)
        {
            break;
        }
    }

    //打印出最大的K个数
    for (int i = MAXN; i >= index; i--)
    {
        if (0 != count[i])
        {
            cout << i << " ";
        }
    }
    system("pause");
}  

//找出一个数组中最大的值
int findMaxN(int *pArray, int len)
{
    int MAXN = pArray[0];
    for (int i = 1; i < len; i++)
    {
        if (pArray[i] > MAXN)
        {
            MAXN = pArray[i];
        }
    }
    return MAXN;
}

复杂度分析 O(N)

同学们,有啥建议或者想法请给我留言哦~~~

  • 1
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值