1. 程式人生 > >面試--演算法--Top K

面試--演算法--Top K

Top K問題是面試時手寫程式碼的常考題,某些場景下的解法與堆排和快排的關係緊密,所以把它放在堆排後面講。

下面先來還原一下Top K考試常見的套路。

你正緊張地坐在小隔間裡,聽著越來越近的腳步聲,內心忐忑,猶如兔脫。

推門聲呷然而起,你扭頭一看,身體已不由自主起立,打量著眼前來人,心裡一陣竊喜:還好,面善。

面試官點頭致意,你配合坐下,滿心滿眼一片赤誠,恨不得把公司的茶水阿姨都誇一遍來表明你面試的誠意。

面試官拿著你的簡歷目不斜視,有意無意間想起了經年彼時自己面試的緊張場景,自己走過的彎路不能讓他再重複走,於是決定先暖暖場活躍活躍氣氛,也多少祭奠一下那夕陽晚鐘,那迎風少年,那狗日的青春。

面試官說今天的天氣真好,你說藍天白雲不多見,哈哈哈哈。

你說貴公司的辦公環境真心不錯,面試官看看四周表面剋制內心放浪——那是自…不對,不能這麼說,年輕人面前還是不要喜形於色。

面試官說還好了,哈哈哈哈。

真的,環境很不錯,哈哈哈哈。你說著,手心大汗淋漓,嗓子裡幹得冒煙。

面試官眯著眼看了看你,說公司大了人就多,人多了資料就多了,現在有一組千萬級別的數,你能不能幫我找出最大的5個?儘量少用空間和時間。

你聽完風中凌亂一臉懵逼,電光火石之間抖一抖眼皮,一陣狂喜,還好看過丑旦的這篇筆記。

Offer,穩了。

嘿嘿,以上扯的這個淡,希望能加深你對Top K問題的印象^_^。

言歸正傳,筆者見過關於Top K問題最全的分類總結是在

這裡(包括海量資料的處理),個人將這些題分成了兩類:一類是容易寫程式碼實現的;另一類側重考察思路的。毫無疑問,後一種比較簡單,你只要記住它的應用場景、解決思路,並能在面試的過程中將它順利地表達出來,便能以不變應萬變。前一種,需要手寫程式碼,就必須要掌握一定的技巧,常見的解法有兩種,就是前面說過的堆排和快排的變形。

本文主要來看看方便用程式碼解決的問題。

堆排解法

用堆排來解決Top K的思路很直接。

前面已經說過,堆排利用的大(小)頂堆所有子節點元素都比父節點小(大)的性質來實現的,這裡故技重施:既然一個大頂堆的頂是最大的元素,那我們要找最小的K個元素,是不是可以先建立一個包含K個元素的堆,然後遍歷集合,如果集合的元素比堆頂元素小(說明它目前應該在K個最小之列),那就用該元素來替換堆頂元素,同時維護該堆的性質,那在遍歷結束的時候,堆中包含的K個元素是不是就是我們要找的最小的K個元素?

實現:
在堆排的基礎上,稍作了修改,buildHeap和heapify函式都是一樣的實現,不難理解。

速記口訣:最小的K個用最大堆,最大的K個用最小堆。

public class TopK {

    public static void main(String[] args) {
        // TODO Auto-generated method stub
        int[] a = { 1, 17, 3, 4, 5, 6, 7, 16, 9, 10, 11, 12, 13, 14, 15, 8 };
        int[] b = topK(a, 4);
        for (int i = 0; i < b.length; i++) {
            System.out.print(b[i] + ", ");
        }
    }

    public static void heapify(int[] array, int index, int length) {
        int left = index * 2 + 1;
        int right = index * 2 + 2;
        int largest = index;
        if (left < length && array[left] > array[index]) {
            largest = left;
        }
        if (right < length && array[right] > array[largest]) {
            largest = right;
        }
        if (index != largest) {
            swap(array, largest, index);
            heapify(array, largest, length);
        }
    }

    public static void swap(int[] array, int a, int b) {
        int temp = array[a];
        array[a] = array[b];
        array[b] = temp;
    }

    public static void buildHeap(int[] array) {
        int length = array.length;
        for (int i = length / 2 - 1; i >= 0; i--) {
            heapify(array, i, length);
        }
    }

    public static void setTop(int[] array, int top) {
        array[0] = top;
        heapify(array, 0, array.length);
    }

    public static int[] topK(int[] array, int k) {
        int[] top = new int[k];
        for (int i = 0; i < k; i++) {
            top[i] = array[i];
        }
        //先建堆,然後依次比較剩餘元素與堆頂元素的大小,比堆頂小的, 說明它應該在堆中出現,則用它來替換掉堆頂元素,然後沉降。
        buildHeap(top);
        for (int j = k; j < array.length; j++) {
            int temp = top[0];
            if (array[j] < temp) {
                setTop(top, array[j]);
            }
        }
        return top;
    }
}

時間複雜度
n*logK

速記:堆排的時間複雜度是n*logn,這裡相當於只對前Top K個元素建堆排序,想法不一定對,但一定有助於記憶。

適用場景
實現的過程中,我們先用前K個數建立了一個堆,然後遍歷陣列來維護這個堆。這種做法帶來了三個好處:(1)不會改變資料的輸入順序(按順序讀的);(2)不會佔用太多的記憶體空間(事實上,一次只讀入一個數,記憶體只要求能容納前K個數即可);(3)由於(2),決定了它特別適合處理海量資料。

這三點,也決定了它最優的適用場景。

快排解法

用快排的思想來解Top K問題,必然要運用到”分治”。

與快排相比,兩者唯一的不同是在對”分治”結果的使用上。我們知道,分治函式會返回一個position,在position左邊的數都比第position個數小,在position右邊的數都比第position大。我們不妨不斷呼叫分治函式,直到它輸出的position = K-1,此時position前面的K個數(0到K-1)就是要找的前K個數。

實現:
“分治”還是原來的那個分治,關鍵是getTopK的邏輯,務必要結合註釋理解透徹,自動動手寫寫。

public class TopK {

    public static void main(String[] args) {
        // TODO Auto-generated method stub
        int[] array = { 9, 3, 1, 10, 5, 7, 6, 2, 8, 0 };
        getTopK(array, 4);
        for (int i = 0; i < array.length; i++) {
            System.out.print(array[i] + ", ");
        }
    }

    // 分治
    public static int partition(int[] array, int low, int high) {
        if (array != null && low < high) {
            int flag = array[low];
            while (low < high) {
                while (low < high && array[high] >= flag) {
                    high--;
                }
                array[low] = array[high];
                while (low < high && array[low] <= flag) {
                    low++;
                }
                array[high] = array[low];
            }
            array[low] = flag;
            return low;
        }
        return 0;
    }

    public static void getTopK(int[] array, int k) {
        if (array != null && array.length > 0) {
            int low = 0;
            int high = array.length - 1;
            int index = partition(array, low, high);
            //不斷調整分治的位置,直到position = k-1
            while (index != k - 1) {
                //大了,往前調整
                if (index > k - 1) {
                    high = index - 1;
                    index = partition(array, low, high);
                }
                //小了,往後調整
                if (index < k - 1) {
                    low = index + 1;
                    index = partition(array, low, high);
                }
            }
        }
    }
}

時間複雜度
n

速記:記住就行,基於partition函式的時間複雜度比較難證明,從來沒考過。

適用場景
對照著堆排的解法來看,partition函式會不斷地交換元素的位置,所以它肯定會改變資料輸入的順序;既然要交換元素的位置,那麼所有元素必須要讀到記憶體空間中,所以它會佔用比較大的空間,至少能容納整個陣列;資料越多,佔用的空間必然越大,海量資料處理起來相對吃力。

但是,它的時間複雜度很低,意味著資料量不大時,效率極高。

好了,兩種解法寫完了,趕緊實現一下吧。