寻找第K小的数

题目描述:
N个都不相同的整数中挑选出第K小的数。


O(NlogN)快速排序实现

最直接的办法就是对这N个数从小到大进行排序,之后直接挑选出第K个数就可以了。
实现代码

#include <iostream>

//对arr数组中的[beg,end)进行快速排序
void qsort(int *arr, int beg, int end)
{
    if (beg==end)//如果只有一个节点则不需要进行排序
    {
        return ;
    }

    int x = arr[beg];//枢纽选择arr[beg]
    int i = beg, j=end-1;
    while ( i < j )
    {
        //一次划分
        while(i<j && arr[j]>=x)
        {
            --j;
        }
        if(arr[j]<x)
        {
            arr[i] = arr[j];
            ++i;
        }

        while(i<j && arr[i]<x)
        {
            ++i;
        }
        if( arr[i]>=x )
        {
            arr[j] = arr[i];
            --j;
        }

    }
    arr[i] = x;
    qsort(arr, beg, i);//递归进行快速排序
    qsort(arr, i+1, end);
}

int main()
{
    int *arr;
    int N,K;

    freopen("in.txt","r",stdin);

    while( std::cin >> N >> K)
    {
        arr = new int[N];
        for ( int i=0; i<N; ++i)
            std::cin >> arr[i];

        qsort(arr,0,N);

        std::cout << arr[K-1] << std::endl;

        delete []arr;
    }

    fclose(stdin);

    return 0;
}


O(N*logK)大顶堆实现

大顶堆的定义:每一个根节点的值均大于等于左右子节点的值

实现说明:
根据大顶堆的定义,我们可以建立一个有K个数据的大顶堆来保存数据。这K个数据是当前所访问的数据中的最小的K个数,接着继续访问未访问的数据,如果该数据大于大顶堆中的最大值,则继续访问下一个数据。否则删除大顶堆中的最大值,同时将该数插入大顶堆中。最后遍历完数据之后,大顶堆的最大值便是我们所求的第K小的成绩。

复杂度分析:
建立一个大小为K的大顶堆需要KlogK时间(具体如何计算见大顶堆的复杂度分析),对剩余的N-K个数据插入到大顶堆中需要(N-K)logK时间,所以复杂度就是O(N*logK)

代码

#include <iostream>

void swap(int &a, int &b)
{
    int tmp = a;
    a = b;
    b = tmp;
}

//按照大顶堆规则调整
void adjust(int *heap, int pos, int S)
{
    if ( pos*2+1 <= S)//存在右子树
    {
        if ( heap[pos]>=heap[pos*2] && heap[pos]>=heap[pos*2+1])
        {
            ;//当前子树符合小顶堆要求,不需要调整
        }
        else if ( heap[pos]<heap[pos*2] && heap[pos*2+1]<heap[pos*2])
        {

            swap(heap[pos], heap[pos*2]);//交换数据
            adjust(heap, pos*2, S);
        }
        else
        {
            swap(heap[pos], heap[pos*2+1]);//交换数据
            adjust(heap, pos*2+1, S);
        }
    } 
    else if ( pos*2==S && heap[pos]<heap[pos*2])//仅存在左子树
    {
        swap(heap[pos], heap[pos*2]);
    }
    else//调整到叶子节点了,不需要继续调整
    {
        ;
    }
}

//大顶堆实现
int find_Kth(int *arr, int N, int K)
{
    int *heap = new int[K+1];

    for (int i=1; i<=K; ++i)
    {
        heap[i] = arr[i-1];
    }

    //调整heap,使其变为一个小顶堆结构
    for(int i=K/2; i>=1; --i)
    {
        adjust(heap, i, K);
    }

    for(int i=K; i<N; ++i)
    {
        if ( arr[i] < heap[1] )//当前访问的数据小于大顶堆的最大值
        {
            heap[1] = arr[i];//删掉大顶堆最大值并将新数据插入到heap[1]这个
            adjust(heap, 1, K);//调整当前结构使其符合大顶堆
        }
    }

    int ans = heap[1];

    delete []heap;

    return ans;
}


int main()
{
    freopen("in.txt", "r", stdin);

    int *arr;
    int N,K;
    while ( std::cin >> N >> K )
    {
        arr = new int[N];
        for(int i=0; i<N; ++i)
            std::cin >> arr[i];

        std::cout << find_Kth(arr, N, K) << std::endl;

        delete []arr; 
    }

    return 0;
}

O(N)快速排序优化实现

O(NlogN)的实现算法中我们利用了快速排序算法。不过我们发现,针对该问题,我们可以通过优化快速排序算法从而使得解决问题的时间复杂度接近于O(N)

具体实现描述

快速排序算法的每一次划分,产生了三个数据集合
1. 枢纽元素x
2. 小于枢纽元素的子集[beg, i)
3. 大于枢纽元素的子集[i+1, end)
三个数据集的元素个数都是确定的,所以我们可以只在某一个数据集中查找我们需要的数据。因此就可以减少另外一部书数据的查找。

复杂度证明
该方法解决该问题的递归式为 T(N)=T(N/2)+N ,通过解该递归式我们就可以求出该算法的时间复杂度了。

T(N)=T(N/2)+N
T(N/2)=T(N/4)+N/2

T(2)=T(1)+1

由此不断的回代可得: T(N)=N+N/2+N/4++4+2+1 。这是一个等比公式,利用等比公式前N项和的计算方法,我们可以得到:

T(N)=a1(1qN)1q=N(1(1/2)logN)1(1/2)=2N

算法的时间复杂度是默认 N 或者说是趋于很大的一个数。

具体操作
使用快速排序,从数组中随机找出一个元素X,把数组分成比X小和比X大的两部分,假设比X小的部分元素个数为B,则:

(1)如果B >= K,则递归从比X小的部分找第K小的元素
(2)如果 B < K,则递归从比X大的部分找第(K-B)小的元素

代码

#include <iostream>

//
int find_Kth(int *arr, int beg, int end, int K)
{
    if (beg==end)//如果只有一个节点则不需要进行排序
    {
        return arr[beg];
    }

    int x = arr[beg];//枢纽选择arr[beg]
    int i = beg, j=end-1;
    while ( i < j )
    {
        //一次划分
        while(i<j && arr[j]>=x)
        {
            --j;
        }
        if(arr[j]<x)
        {
            arr[i] = arr[j];
            ++i;
        }


        while(i<j && arr[i]<x)
        {
            ++i;
        }
        if( arr[i]>=x )
        {
            arr[j] = arr[i];
            --j;
        }

    }
    arr[i] = x;
    if ( i-beg+1 > K )
    {
        return find_Kth(arr, beg, i, K);
    }
    else if( i-beg+1 == K)
    {
        return arr[i];
    }
    else
    {
        return find_Kth(arr, i+1, end, K-(i-beg+1) );
    }

}

int main()
{
    int *arr;
    int N, K;

    freopen("in.txt","r",stdin);

    while( std::cin >> N >> K)
    {
        arr = new int[N];
        for ( int i=0; i<N; ++i)
            std::cin >> arr[i];

        std::cout << find_Kth(arr, 0, N, K) << std::endl;

        delete []arr;
    }

    fclose(stdin);

    return 0;
}

O(N) Hash表实现

我这里使用的hash实现有点取巧了,或者说是该方法有局限性。

具体实现描述
由于每个人的成绩都不同,且成绩是[0, MAX]中的某个整数,所以我们可以直接开辟一个大小为MAX的标记数组tag,标记该成绩值是否出现。
标记完成之后我们就再从头到尾访问该标记数组,当访问到第K个标记值是true的时候,该标记所在的位置就是我们要找的第K小的数。这里的MAX我们可以遍历数组找出最大值。

代码

#include <iostream>
#include <vector>
#include <cstring>

int find_Kth(int *arr, int N, int K)
{
    int max = arr[0];
    for (int i=0; i<N; ++i)//寻找最大的元素
    {
        if ( arr[i]>max )
        {
            max = arr[i];
        }
    }

    bool *tag = new bool[max+1];

    memset(tag, 0x00, sizeof(bool)*(max+1) );//初始化为false
    for(int i=0; i<N; ++i)
    {
        //如果存在则标记为true,题目默认每个数值只出现一次
        tag[ arr[i] ] = true;
    }

    for (int j=0,i=0; i<=max; ++i)
    {
        if ( tag[i] )//如果存在i,则j=j+1
        {
            ++j;
            if ( j==K )//j是第K个数
            {
                delete []tag;
                return i;
            }
        }
    }
}

int main()
{
    freopen("in.txt", "r", stdin);


    int N, K;
    int *arr;

    while( std::cin >> N >> K )
    {
        arr = new int[N];
        for(int i=0; i<N; ++i)
            std::cin >> arr[i];

        std::cout << find_Kth(arr, N, K) << std::endl;

        delete []arr;
    }

    return 0;
}
  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值