Leetcode - Kth Largest Element in an Array

這道題目是用排序做不出來的。很沒含量。
然后看了下 quick selection 算法。打算明天自己重寫下。
待補充。

今天和女朋友算是大吵了架。然后各種事吧。

繼續(xù)把。

這道題目。我目前用了三種做法。正好復習了下快速排序,堆排序這些比起基礎(chǔ)排序稍微更復雜些的算法。然后有了更多的認識。

最簡單的做法。先排序。然后直接找。

/* quick sort and get it
    public int findKthLargest(int[] nums, int k) {
        if (nums == null || nums.length == 0)
            return -1;
        Arrays.sort(nums);
        return nums[nums.length - k];
    }
    */

這種做法沒有任何技術(shù)含量。不解釋了。

第二種做法。 快速選擇, quick select
為什么會有這么一種做法。
快排的時候,我們會使用一個函數(shù),partition,找到這段數(shù)組的區(qū)分點,然后再次使用遞歸分別對兩段子sub array 使用快速排序。
但是對于查找。如果我們確定我們要找的數(shù)字,在一段中,我們根本不在乎另外一段。所以根本不需要對他進行排序。直接對我們確定的那一段進行排序就行了。
這就是快速選擇的基本思想。
然后算法導論里面講的更復雜。因為現(xiàn)在我的解法,是有最壞結(jié)果的。也就是快速排序最壞結(jié)果的情況,對于快速選擇,同樣也是最壞的。算法導論里面,講的貌似就是怎么把最壞情況的復雜度,也降到線性。在這里我就不做研究了。就像我對快速排序的做法,也是沿襲了普林斯頓算法的習慣,一開始就 shuffle sort 一下。
但是。。。
這個快速選擇的復雜度是線性的,而shuffle sort是nlogn. 所以快速選擇里不能使用這個思想。然后網(wǎng)上的建議,就是找他們的中位數(shù),然后以他作為 Pivot。
應(yīng)該是可以的。但是我還是按照老方法做的。
代碼如下:

    /** quick select and get it */
    public int findKthLargest(int[] nums, int k) {
        if (nums == null || nums.length == 0)
            return -1;
        
        return quickSelect(nums, nums.length - k + 1, 0, nums.length - 1);
    }
    
    private int quickSelect(int[] nums, int k, int begin, int end) {
        if (begin > end)
            return -1;
        else if (begin == end)
            return nums[begin];
        int j = partition(nums, begin, end);
        if (k < (j - begin + 1))
            return quickSelect(nums, k, begin, j - 1);
        else if (k > (j - begin + 1))
            return quickSelect(nums, k - (j - begin + 1), j + 1, end);
        else
            return nums[j];
    }
    
    private int partition(int[] nums, int begin, int end) {
        int v = nums[begin];
        int i = begin;
        int j = end + 1;
        while (i <= j) {
            while (nums[++i] <= v) {
                if (i >= end)
                    break;
            }
            while (nums[--j] > v) {
                if (j <= 0)
                    break;
            }
            if (i >= j)  // take care, here must be >=
                break;
            int temp = nums[i];
            nums[i] = nums[j];
            nums[j] = temp;
        }
        nums[begin] = nums[j];
        nums[j] = v;
        return j;
    }

然后快排的注意點。
if (i >= j)
這里必須是 >= !!!!!!
否則會有潛在的bug
什么時候會出現(xiàn) i >= j
快排中,i 指的元素都是 > v
j 指的元素都是 <= v
所以大多數(shù)情況下,當i在某個元素停下。此時這個元素 >= v
然后j開始往后退,碰到這個元素的時候,不會停下,繼續(xù)往后退一格。
然后停下。
所以一般最后的結(jié)局都是 i > j 。那么我們?yōu)槭裁葱枰?i >= j呢
似乎 i 不會有機會 = j
當一個子數(shù)列,所有的元素都小于v 時
i = end
然后j開始跑。 --j, 此時 j = end
然后發(fā)現(xiàn)是 <= v 的,于是立刻停下。
此時 i == j
然后如果沒有 i >= j 中的等于。
循環(huán)不會結(jié)束,繼續(xù)執(zhí)行。
然后 ++i 會造成數(shù)組越界。
所以 >= 是必須的!

第三種做法。堆排序。
為什么快排的時間消耗的多。因為,很多東西我不需要去排。
所以,堆排序,類似于一種在動態(tài)的過程中不斷進行排序,貪婪思想的排序算法,最適合這種查找。
我需要找到k大,那么,我只要把堆的頭移出去k次。第k次,一定是第k大的。
所以之后的那些排序時間我就不用花了。而且根本不在乎最好最壞復雜度。
下面稍微說一下堆排序。
我之前做了總結(jié),但是因為事情太多,沒有放上來。
堆排序是神級排序。
他的復雜度一直是 nlogn, 最壞情況下也是如此。
快排呢,最壞情況時 o n^2. 也就是,當數(shù)組已經(jīng)逆序排好了的時候。復雜度會很糟糕。
所以這一點,堆排序完爆快排。
歸并排序呢,最好最壞也都是 o nlogn. 但是,歸并排序不是in place,需要額外申請內(nèi)存。而堆排序是 in place 的。
你看我的代碼里好像有復制數(shù)組的地方。
因為數(shù)組的index都是從0開始的,如果這樣使用堆排序,代碼寫起來會煩一些。如果直接從1開始,會方便很多。
兒子一定是 2 * i, 2 × i + 1
父親一定是 i / 2
所以我這個相當于簡化版堆排序。但是從0開始的,完全可以寫出來。
所以內(nèi)存使用方面,堆排序又完爆歸并排序,merge sort.
PS: 其實歸并排序可以做成 in place 的,這是我老師告訴我的。。太夸張了。但是做成in place 之后,代碼首先會復雜很多,其次會有很多邏輯判斷,所以最終導致速度不如以前了,也許節(jié)約了內(nèi)存,但是已經(jīng)失去了 merge sort 本身的意義。
然后還有個驚天消息。 oracle java 的 Arrays.sort() 這個方法其實是錯誤的。
也就是說,這個官方API代碼是有bug的。但是為什么我們平時使用完全沒問題呢?
因為這個bug幾乎不可能發(fā)生。老師說,需要構(gòu)造一個一百多萬位,特殊構(gòu)造的數(shù)組,才可以暴露這個bug。

繼續(xù)。
那么,堆排序在各個方面,領(lǐng)先于,快排,歸并排序。我們很少用呢?
因為 cache performance.
想象一下啊。堆排序,經(jīng)常是 i 與 i * 2這樣一個層級進行比較。如果 i 很大,兩者之間的差距是很大的。
而且每次swim,會將許多距離很遠的數(shù)字進行比較。很可能有些數(shù)字并沒有放進cache中。所以CPU得特地再把那些數(shù)字從內(nèi)存中取出來放進cache
所以,堆排序的cache performace 很糟糕。
像快排,都是元素和一個 pivot value 在比,不需要和遠處的元素比。cache performance 目測不會差。
歸并排序,是兩個數(shù)組的相近位置進行比較,感覺也不會差。
所以,老師說,堆排序這方面的缺陷影響太多,所以退出了主流排序的舞臺。
對于cache performace 這個論斷,我其實并沒有真正理解,只是自己感覺了下對的。
有機會要去看下CSAPP的cache部分。

差不多就說完了。

忘記上代碼了:

/** heap sort and get it */
    private int N = 0;
    public int findKthLargest(int[] nums, int k) {
        if (nums == null || nums.length == 0)
            return -1;
        N = nums.length;
        int[] aux = new int[N + 1];
        for (int i = 1; i < N + 1; i++)
            aux[i] = nums[i - 1];
        for (int i = N / 2; i >= 1; i--)
            swim(aux, i);
        for (int i = 1; i < k; i++)
            bottom(aux);
        return bottom(aux);
    }
    
    private void swim(int[] aux, int index) {
        int i = index * 2;
        while (i <= N) {
            if (i + 1 <= N && aux[i + 1] > aux[i])
                i = i + 1;
            if (aux[index] < aux[i]) {
                int temp = aux[index];
                aux[index] = aux[i];
                aux[i] = temp;
                index = i;
                i = 2 * i;
            }
            else
                break;
        }
    }
    
    private int bottom(int[] aux) {
        int ret = aux[1];
        aux[1] = aux[N];
        aux[N] = ret;
        N--;
        int i = 1;
        int j = 2 * i;
        while (j <= N) {
            if (j + 1 <= N && aux[j + 1] > aux[j])
                j = j + 1;
            if (aux[i] < aux[j]) {
                int temp = aux[i];
                aux[i] = aux[j];
                aux[j] = temp;
                i = j;
                j = 2 * i;
            }
            else
                break;
        }
        
        return ret;
    }

**
總結(jié): 快速選擇, 堆排序
**

Anyway, Good luck,Richardo!

My code:

public class Solution {
    public int findKthLargest(int[] nums, int k) {
        if (nums == null || nums.length == 0 || k <= 0) {
            return 0;
        }
        shuffle(nums);
        int begin = 0;
        int end = nums.length - 1;
        while (begin <= end) {
            int pivot = begin;
            int i = begin + 1;
            int j = end;
            while (i <= j) {
                while (i <= end && nums[i] < nums[pivot]) {
                    i++;
                }
                while (j > pivot && nums[j] > nums[pivot]) {
                    j--;
                }
                if (i > j) {
                    break;
                }
                else {
                    int temp = nums[i];
                    nums[i] = nums[j];
                    nums[j]= temp;
                    i++;
                    j--;
                }
            }
            
            int temp = nums[pivot];
            nums[pivot] = nums[j];
            nums[j] = temp;
            if (nums.length - j == k) {
                return nums[j];
            }
            else if (nums.length - j > k) {
                begin = j + 1;
            }
            else {
                end = j - 1;
            }
        }
        
        return -1;
    }
    
    private void shuffle(int[] nums) {
        Random r = new Random();
        for (int i = 1; i < nums.length; i++) {
            int ri = r.nextInt(i + 1);
            int temp = nums[ri];
            nums[ri] = nums[i];
            nums[i] = temp;
        }
    }
}

reference:
https://discuss.leetcode.com/topic/14597/solution-explained

我一開始自己寫了一個 quick select,但是并沒有加入shuffle
所以速度很慢。
后來看了提示,可以加 shuffle,復雜度是O(n)
然后速度一下子提升了上去。
還有一種復雜的算法教你如果選擇pivot導致最壞情況下,復雜度仍然是O(n),這里就不看了。

這道題目還可以用最小堆。
尤其當輸入是流時,quick select 并不能很好的發(fā)揮作用,而Min-heap
的時間復雜度是 nlogk,空間復雜度是 k
可以很好的解決流的問題。

Anyway, Good luck, Richardo! -- 09/04/2016

My code:

public class Solution {
    public int findKthLargest(int[] nums, int k) {
        PriorityQueue<Integer> pq = new PriorityQueue<Integer>();
        for (int i = 0; i < nums.length; i++) {
            if (pq.size() < k) {
                pq.offer(nums[i]);
            }
            else {
                if (pq.peek() >= nums[i]) {
                    continue;
                }
                else {
                    pq.poll();
                    pq.offer(nums[i]);
                }
            }
        }
        
        return pq.peek();
    }
}

time complexity: O(n log k)
K-Min Heap

Quick Select:

import java.util.Random;

public class Solution {
    public int findKthLargest(int[] nums, int k) {
        shuffle(nums);
        return quickSelect(nums, nums.length - k);
    }
    
    private int quickSelect(int[] nums, int k) { // k is index
        int lo = 0;
        int hi = nums.length - 1;
        while (lo <= hi) {
            int j = partition(nums, lo, hi);
            if (j == k) {
                return nums[j];
            }
            else if (j < k) {
                lo = j + 1;
            }
            else {
                hi = j - 1;
            }
        }
        
        return -1;
    }
    
    private int partition(int[] nums, int lo, int hi) {
        int pivot = lo;
        lo += 1;
        while (lo <= hi) {
            while (lo < nums.length && nums[lo] < nums[pivot]) {
                lo++;
            }
            while (hi >= 0 && nums[hi] > nums[pivot]) {
                hi--;
            }
            if (lo >= hi) {
                break;
            }
            int temp = nums[lo];
            nums[lo] = nums[hi];
            nums[hi] = temp;
            lo++;
            hi--;
        }
        int temp = nums[pivot];
        nums[pivot] = nums[hi];
        nums[hi] = temp;
        return hi;
    }
    
    private void shuffle(int[] nums) {
        Random r = new Random();
        for (int i = 1; i < nums.length; i++) {
            int index = r.nextInt(i + 1);
            int temp = nums[index];
            nums[index] = nums[i];
            nums[i] = temp;
        }
    }
    
    public static void main(String[] args) {
        Solution test = new Solution();
        int[] nums = new int[]{3,2,1,5,6,4};
        int ret = test.findKthLargest(nums, 2);
        System.out.println(ret);
    }
}

先轉(zhuǎn)換成找最小的k (index), 然后再 quick select.

Anyway, Good luck, Richardo! -- 09/24/2016

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • 概述 排序有內(nèi)部排序和外部排序,內(nèi)部排序是數(shù)據(jù)記錄在內(nèi)存中進行排序,而外部排序是因排序的數(shù)據(jù)很大,一次不能容納全部...
    蟻前閱讀 5,297評論 0 52
  • 概述:排序有內(nèi)部排序和外部排序,內(nèi)部排序是數(shù)據(jù)記錄在內(nèi)存中進行排序,而外部排序是因排序的數(shù)據(jù)很大,一次不能容納全部...
    每天刷兩次牙閱讀 3,818評論 0 15
  • 1.插入排序—直接插入排序(Straight Insertion Sort) 基本思想: 將一個記錄插入到已排序好...
    依依玖玥閱讀 1,347評論 0 2
  • 背景 一年多以前我在知乎上答了有關(guān)LeetCode的問題, 分享了一些自己做題目的經(jīng)驗。 張土汪:刷leetcod...
    土汪閱讀 12,890評論 0 33
  • 概述排序有內(nèi)部排序和外部排序,內(nèi)部排序是數(shù)據(jù)記錄在內(nèi)存中進行排序,而外部排序是因排序的數(shù)據(jù)很大,一次不能容納全部的...
    Luc_閱讀 2,370評論 0 35

友情鏈接更多精彩內(nèi)容