1. 程式人生 > >第K大/Top K及其簡單實現

第K大/Top K及其簡單實現

見網上第K大多數只給思路,沒給實現,我就來填坑了。

update 2017-09-23 有同學反饋說面試遇到這個題,博文給了助攻,哈預料之中。

Top K 和第K大基本等價,以下我們以第K大為例且假設第K大一定存在,Top K 可以在第k大基礎上稍微改動獲得。
本文介紹6種方法,只考慮實現功能,不做異常判斷,面試的話快排和最小堆的方法比較不錯,測試提交的話可以去Leetcode,或者直接拿最下面的資料生成程式碼去對拍跑。

快排的思想 近似O(n)

呼叫降序快排的partition函式,設區間為[low,high],返回index,則index左邊都是大於data[index]的。
1. 若index及index左邊數字有k個則data[index]就是第k大,index及其左邊元素為Top K元素
2. 左邊數字大於k個則繼續在[low,index]裡找
3. 左邊數字小於k個則去右邊[index+1,high]找 k - 左邊數字個數

#include <cstdio>
#include <iostream>
using namespace std;
const int maxn = 1e5 + 5;
//改為 data[high] >= key 和 data[low] <= key 則為第k小
int part(int *data, int low, int high) {
    int key = data[low];
    while (low < high) {
        while (low < high && data[high] <= key) high--;
        data[low] = data[high];
        while
(low < high && data[low] >= key) low++; data[high] = data[low] ; } data[low] = key; return low; } int k_th(int *data, int k, int low, int high) { int pos = part(data, low, high); int cnt = pos - low + 1; //[low,pos]元素個數 if (cnt == k) return data[pos]; else
if (cnt < k) return k_th(data, k - cnt, pos + 1, high); else return k_th(data, k, low, pos); } int k_th(int *data, int n, int k) { if(k < 1 || k > n) return -1; return k_th(data, k, 0, n - 1); //閉區間 //遍歷data[0,k)即可獲得top K,但不能保證有序 } int main() { // int data[] = {1, 5, 6, 7, 3, 2, 10, 9, 0, 231, 3214, 61}; // int n = sizeof(data) / sizeof(int); // int k = 2; // cout << k_th(data, n, k) << endl; // freopen("in.txt","r",stdin); // freopen("out.txt","w",stdout); int n, k, data[maxn]; std::ios::sync_with_stdio(false); while (cin >> n >> k) { for (int i = 0; i < n; ++i) { cin >> data[i]; } cout << k_th(data, n, k) << endl; } return 0; }

小根堆 O(nlogk)

維護一個k個元素的小根堆,保持堆裡元素為最大的K個且堆頂為第k大(堆裡最小的),掃一遍資料,若堆裡個數小於k則插入,否則看新的數和堆頂數大小關係:
1. 若新來的數小於等於堆頂,即新元素比Top K裡最小的還小,則新來的數顯然不可能是前k大
2. 若新來的數大於堆頂,則刪掉堆頂,將新數字放到堆裡且調整堆來保持堆的屬性

由於實現堆程式碼量較多,我們可以用C++的優先佇列、set等代替手工堆偷跑,當然這裡也提供了手動實現版。

#include <cstdio>
#include <vector>
#include <queue>
#include <iostream>
using namespace std;
const int maxn = 1e5 + 5;
//維持一個k大小的最小堆,根據新元素和堆頂大小決定要不要加入堆且刪堆頂
// O(nlogk)
int biggest_k_th(int *data, int n, int k) {
    priority_queue<int, vector<int>, greater<int> >q;   //小根堆
    while (!q.empty()) q.pop();

    for (int i = 0; i < n; ++i) {
        if (q.size() < k) {
            q.push(data[i]);
        } else if (data[i] > q.top()) {
            q.pop();
            q.push(data[i]);
        }
    }
    //取k次q.top()且pop()k次即為有序的前K大
    return q.top();
}

int smallest_k_th(int *data, int n, int k) {
    priority_queue<int>q;   //大根堆
    while (!q.empty()) q.pop();

    for (int i = 0; i < n; ++i) {
        if (q.size() < k) {
            q.push(data[i]);
        } else if (data[i] < q.top()) {
            q.pop();
            q.push(data[i]);
        }
    }
    return q.top();
}

int main() {
    // freopen("in.txt","r",stdin);
    // freopen("out.txt","w",stdout);
    std::ios::sync_with_stdio(false);
    int n, k, data[maxn];
    while (cin >> n >> k) {
        for (int i = 0; i < n; ++i) {
            cin >> data[i];
        }
        cout << biggest_k_th(data, n, k) << endl;
    }
    return 0;
}

手動實現版

#include <iostream>
#include <cstdio>
#include <cstring>
using namespace std;
const int maxn = 1e5 + 5;
const int maxK = 1e5 + 5;

int heapCnt = 0;
int heap[maxK];

void adjust(int *heap, int begin, int end) {    //[begin,end)
    int cur = begin;
    int son = 2 * cur + 1;
    while (son < end) {
        if (son + 1 < end && heap[son] > heap[son + 1]) son++;
        if (heap[cur] < heap[son]) return;
        swap(heap[son], heap[cur]);
        cur = son;
        son = 2 * cur + 1;
    }
}

void buildHeap(int *heap, int k) {  //[heap,heap+k) 開區間
    for (int i = k / 2; i >= 0;  --i) {
        adjust(heap, i, k);
    }
}

int k_th(int *data, int n, int k) {
    heapCnt = 0;
    for (int i = 0; i < n; ++i) {
        if (heapCnt < k) {
            heap[heapCnt++] = data[i];
            if (heapCnt == k) {
                buildHeap(heap, k); //data[0,k)共k個
            }
        } else {
            if (data[i] > heap[0]) {
                heap[0] = data[i];
                adjust(heap, 0, heapCnt);
            }
        }
    }
    return heap[0];
}

int main() {
    // freopen("in.txt", "r", stdin);
    // freopen("out.txt", "w", stdout);
    int n, k, data[maxn];
    std::ios::sync_with_stdio(false);
    while (cin >> n >> k) {
        for (int i = 0; i < n; ++i) {
            cin >> data[i];
        }
        cout << k_th(data, n, k) << endl;
    }
    return 0;
}

計數排序 O(n)

按照計數排序思想給資料的值計數,再從資料的最大值往最小值遍歷,則總次數大於等於k的那個數為第k大,見程式碼一目瞭然。
優點:速度快且不用庫也程式碼量少,妥妥的O(n)
缺點:只適用於數值不大的情況,當然你用hashmap這類庫計數的話就沒這問題了。

#include <iostream>
#include <cstdio>
#include <cstring>
using namespace std;
const int maxn = 1e5 + 5;
const int maxVal = 1e5 + 5; //O(n) 適用於資料值不大的情況

int k_th(int *data, int n, int k) {
    int mmin = data[0], mmax = data[0];
    int times[maxVal];
    memset(times,0,sizeof(times));

    for (int i = 0; i < n; ++i) {
        mmin = min(mmin, data[i]);
        mmax = max(mmax, data[i]);
        times[data[i]]++;
    }

    int cnt = 0;
    for (int i = mmax; i >= mmin; --i) {
        cnt += times[i];
        if (cnt >= k) { // >= 是因為第k大的數可能有若干個,找第一個
            return i;
        }
        //反過來遍歷則為第k小
        //每次輸出times[i]次i,注意下邊界就出了有序前k大
    }
    return -1;
}

int main() {
    // freopen("in.txt","r",stdin);
    // freopen("out.txt","w",stdout);
    int n, k, data[maxn];
    std::ios::sync_with_stdio(false);
    while (cin >> n >> k) {
        for (int i = 0; i < n; ++i) {
            cin >> data[i];
        }
        cout<< k_th(data, n, k) <<endl;
    }
    return 0;
}

二分 O(nlogn)

假設第K大的數字是val,那麼val肯定在一個數字區間裡,我們叫 [l,r] ,我們就二分這個區間和val。
最開始l=所有數的最小值,r=最大值,假設當前值是mid,如果所有資料中大於等於mid的數字至少k個,說明當前數值可能是答案(若mid存在的情況則將區間調為[mid,r],mid不存在的話就改為[mid+1,r]),否則mid偏大,在[l,mid-1]裡查詢;二分不會的可見這篇文章。
二分本身是需要有序的,但我們二分的是答案值,int數字本身就有排序效果。

#include <iostream>
#include <cstdio>
#include <cstring>
using namespace std;
const int maxn = 1e5 + 5;
const int maxVal = 1e5 + 5;

bool ok(int *data, int n, int k, int mid) {
    int cnt = 0;
    for (int i = 0; i < n; ++i) {
        if (data[i] >= mid) cnt++;
    }
    return cnt >= k;
}
int k_th(int *data, int n, int k) {
    int mmin = data[0], mmax = data[0];
    bool vis[maxVal];
    memset(vis, false, sizeof(vis));

    for (int i = 0; i < n; ++i) {
        mmin = min(mmin, data[i]);
        mmax = max(mmax, data[i]);
        vis[data[i]] = true;
    }

    int l = mmin, r = mmax;
    while (l < r) {
        int mid = (l + r + 1) / 2;
        if (ok(data, n, k, mid)) {
            if (!vis[mid]) l = mid + 1;
            else l = mid;
        } else {
            r = mid - 1;
        }
    }
    return l;
}

int main() {
    // freopen("in.txt", "r", stdin);
    // freopen("out.txt", "w", stdout);
    int n, k, data[maxn];
    std::ios::sync_with_stdio(false);
    while (cin >> n >> k) {
        for (int i = 0; i < n; ++i) {
            cin >> data[i];
        }
        cout << k_th(data, n, k) << endl;
    }
    return 0;
}

暴力式選擇/氣泡排序 O(kn)

特慢做法:排序k個,每次遍歷n個元素,O(k*n)

#include <iostream>
#include <cstdio>
#include <cstring>
using namespace std;
const int maxn = 1e5 + 5;

int k_th(int *data, int n, int k) {
    for (int i = 0; i < k; ++i) {
        for (int j = 0; j < n - i - 1; ++j) {
            if (data[j] > data[j + 1]) {
                swap(data[j], data[j + 1]);
            }
        }
    }
    return data[n-k];
}

int main() {
    // freopen("in.txt", "r", stdin);
    // freopen("out.txt", "w", stdout);
    int n, k, data[maxn];
    std::ios::sync_with_stdio(false);
    while (cin >> n >> k) {
        for (int i = 0; i < n; ++i) {
            cin >> data[i];
        }
        cout << k_th(data, n, k) << endl;
    }
    return 0;
}

真暴力排序O(nlogn)

排完取 data[k],這麼暴力就不說了。

資料生成程式碼

生成10組資料,每組一個n(範圍:[a_n,b_n]),然後n個數 [a,b]。

#include <cstdio>
#include <cmath>
#include <cstdlib>
using namespace std;
int rand_ab(int a, int b) { //[a,b]
    return a + rand() % (b + 1 - a);
}
void make(){    
    int a_n = 10000, b_n = 100000;
    int a = 1, b = 10000;
    for (int i = 0; i < 10; ++i) {
        int n = rand_ab(a_n, b_n);
        printf("%d ", n);
        int a_k = 1, b_k = n;
        printf("%d\n", rand_ab(a_k,b_k));
        printf("%d", rand_ab(a, b));
        for (int i = 1; i < n; ++i) {
            printf(" %d", rand_ab(a, b));
        }
        printf("\n");
    }
}
int main() {
    // freopen("out.txt","w",stdout);
    make();
    return 0;
}