30-寻找最小的k个数

问题描述

有n个整数,找出其中最小的k个数,时间复杂度尽可能低

解决这道问题有很多种方法,最常见的解法笔者认为有两大类,第一类是全排序,第二类是部分排序。

  1. 全排序中,复杂度较低的,对于数量比较大的数据集而言,可以考虑采用快速排序和堆排序,复杂度O(nlogn)。对于数量比较小的数据集而言,可以考虑其他排序方式,比如插入排序。
  2. 对于部分排序,题目没有要求最小的k个数有序,也没有要求最后的(n-k)个数有序,因此也就没有必要对所有的元素进行排序。因此,部分排序的基本思想是:

1)从n个序列中找到前k个数,找到这k个数中的最大值max;
2)将max与剩余的(n-k)个数比较,若剩余的(n-k)个数中,有比它小的,>则将这个数与max换位;
重复执行以上两个步骤,直到剩余(n-k)个数里没有比max值更小的数为止。

因此,部分排序相对于全排序而言,又快捷了一层,但是选择什么样的方式进行部分排序又是一个优化的点。最容易想到的是在数组中完成这个算法功能,那么它的时间复杂度会是 O(klogk+(n-k)logk)=O(nlogk)

当然,部分排序也可以采用其他的排序方案,有时候,选择合适的数据结构也可以显著提升算法效率 :通过改变数据的存储结构,利用堆的特点,用堆代替数组,在相同的部分排序思想下,采用堆排序的时间复杂度优于其他排序,因此,笔者重点介绍这个方法。

方法1–部分排序,构建容量为k的大顶堆

算法描述

1)遍历给定整数的前k个数,建立一个容量为k的最大堆(根结点数值大于子结>点),假设这k个数即为给定n个数里最小的k个数,遍历和建堆耗时O(k),此时k个数里最大的数在根结点。
2)遍历剩余的(n-k)个数,将遍历到的数xi与堆顶元素比较,如果小于堆顶元素,则用xi替换堆顶元素,更新堆,这里的时间复杂度为O(logk);如果大于或等于堆顶元素,则继续往下遍历。

时间复杂度分析

最坏的情况,如果每次遍历剩余的(n-k)个数时,都需要更新堆,则时间复杂度为
建堆费用+更新堆费用=O(klogk+(n-k)logk)=O(nlogk)

算法实现
/***********************
Author:tmw
date:2017-11-16
update:2019-3-22
************************/
#include <stdio.h>
#include <stdlib.h>

#define swap(a,b,t) (t=a,a=b,b=t)

/**
* 寻找前k个数
* 方法2:维护一个大小为k的大顶堆
**/
//将无序序列转变成大顶堆序列
//规定无序元素从数组的1号地址开始填
void heap_sort_one(int* array, int father_index, int array_len)
{
    int child_index=0;
    int temp=0;

    for(child_index=father_index*2; child_index<=array_len; child_index=child_index*2)
    {
        /**这里的child+1是因为后面有个array[child_index+1] 防止溢出**/
        if(child_index+1<=array_len && array[child_index]<array[child_index+1])
            child_index++;

        if(child_index<=array_len && array[father_index]<array[child_index])
        {
            swap(array[father_index],array[child_index],temp);
            father_index = child_index; //更新后继续更新孩子结点的左右子树
        }
    }
}

/**寻找最小的K个数 --- 维护一个K个元素的大顶堆
时间复杂度:建堆费用+更新堆费用=O(klogk+(n-k)logk)=O(nlogk)
**/
void heap_sort_topK(int* array, int array_len, int k)
{
    int i;
    int temp=0;

    /**建立一个k的大顶堆**/
    for(i=k/2; i>=1; i--) /**建堆:从下往上**/
        heap_sort_one(array,i,k); 

    /** 原堆排序代码
    for(i=array_len; i>0; i--)
    {
        swap(array[1],array[i],temp);
        heap_sort_one(array,1,i-1);
    }
    **/

    /**遍历集合setB={n-k,...,n}元素:
    1)若存在元素b[i]<堆顶元素heapTOP,则swap(b[i],heapTop),重新维护当前前k个元素的大顶堆
    2)若b[i]>堆顶元素heapTOP,则继续遍历setB中的元素
    **/
    for(i=array_len-k; i<=array_len; i++)
    {
        if(array[i]<array[1])
        {
            swap(array[i],array[1],temp);
            heap_sort_one(array,1,k); /**更新堆:从上往下**/
        }
    }
    printf("\n");
    for(i=1; i<=k; i++)
        printf("%d ",array[i]);
}

算法测试及测试结果
int main()
{
    int i=0;
    int array1[11]={0,0,4,3,9,10,1,-1,2,100,8};
    for(i=1; i<11; i++)
        printf("%d ",array1[i]);
    printf("\n序列中最小的4个数为:");
    heap_sort_topK(array1,10,4);

    printf("\n\n");

    int array2[10] ={0,50,10,90,30,70,40,80,60,20};
    for(i=1; i<10; i++)
        printf("%d ",array2[i]);
    printf("\n序列中最小的4个数为:");
    heap_sort_topK(array2,9,4);

    return 0;
}

在这里插入图片描述


比起方法1,下面这种方法笔者比较喜欢,它能把平均复杂度降到线性

方法2–线性选择算法

算法描述

联想快速排序的思想,可以考虑这样一种快速选择算法fast_select(S,k)

算法开始:选取一个中轴元素pivot(可参考快速排序里的三数取中方法),将集合S分割成左右两边(左:Sl;右Sr),左边元素都比它小(或相等),右边元素都比它大;
1)如果k小于左边元素的个数,那么第k个最小的元素一定在中轴元素左半边,对左半边做递归:fast_select(Sl,k);
2)如果k等于左边元素个数加1,即中轴元素就是第k个最小元素,程序返回这个元素
3)如果k大于左边元素个数,那么第k个最小的元素一定在中轴元素右半边,对右半边做递归:fast_select(Sr,k-|Sl|-1);

关于程序的返回
如果要查找的k的个数小于Sl的个数,则直接返回Sl中较小的k个元素;
如果要查找的k的个数大于Sl的个数,则返回Sl中所有元素和Sr中较小的k-|Sr|个元素。

线性选择算法时间复杂度

线性选择算法在平均情况下能做到 O(n) 的时间复杂度

算法实现
/***********************
Author:tmw
date:2017-11-17
************************/
#include <stdio.h>
#include <stdlib.h>

#define swap(x,y,t) (t=x,x=y,y=t)

//三数取中
int median3(int *array , int low , int high)
{
    int mid = (low+high)/2;
    int temp;
    if(array[high]<array[mid])
        swap(array[high],array[mid],temp);
    if(array[high]<array[low])
        swap(array[high],array[low],temp);
    //以上:保证array[high]为三者中最大
    if(array[low]<array[mid])
        swap(array[low],array[mid],temp);
    //以上:保证array[low]为三者中的中间值
    return array[low];
}

int fast_select( int* array , int low , int high , int k )
{
    //三数取中,中枢元素存在array[low]中
    array[low] = median3(array,low,high);
    int pivot = array[low];//pivot对中枢元素的值做备份
    int i = low;
    int j = high;
    while( i < j )
    {
        while( i < j && array[j] >= pivot )
            j--;
        array[i] = array[j];
        while( i < j && array[i] <= pivot)
            i++;
        array[j] = array[i];
    }
    array[i]=pivot;//此时,pivot的左边都比它小,右边都比它大

    /**线性选择主算法**/
    if( k <= i )//如果k小于左边元素的个数,那么第k个最小的元素一定在中轴元素左半边,对左半边做递归
        return fast_select(array,low,i-1,k);
    if( k > i + 1)//如果k大于左边元素个数,那么第k个最小的元素一定在中轴元素右半边,对右半边做递归
        return fast_select(array,i+1,high,k);
    else
        return array[i];
}
算法测试及测试结果
int main()
{
    printf("测试代码\n");
    int k = 4;
    int i;

    int a2[7] = {1,8,23,6,90,5,8};
    printf("原串:\t    ");
    for(i=0;i<7;i++)
        printf("%d ",a2[i]);
    fast_select(a2,0,6,k);
    printf("\n最小的%d个数:",k);
    for(i=0;i<k;i++)
        printf("%d ",a2[i]);
        
    printf("\n");
    
    int a4[9] = {100,56,23,7,17,0,23,65,2};
    printf("\n\n原串:\t    ");
    for(i=0;i<9;i++)
        printf("%d ",a4[i]);
    fast_select(a4,0,8,k);
    printf("\n最小的%d个数:",k);
    for(i=0;i<k;i++)
        printf("%d ",a4[i]);
    return 0;
}

测试结果


梦想还是要有的,万一实现了呢~~~ヾ(◍°∇°◍)ノ゙

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值