1. 程式人生 > >演算法分析與設計 第二週

演算法分析與設計 第二週

演算法分析與設計

尋找第K大數

題目描述

這裡寫圖片描述

選題原因

本週學習了分治演算法,在學習中出現了例題尋找第K大數字,以往的做法通常是維護一個長度為k的陣列,儲存最大的k個數,掃描所有的值,不斷地加入陣列。而新的分治演算法則有著極低的複雜度。恰巧本題是中等難度,因此選擇使用兩種解法分別解題並做比較。

維護長度為K陣列

解題思路

維持長度為k的陣列,以及兩個變數min minpos ,記錄當前陣列中最小的數字及最小數所在的位置。當有數字大過最小數,則替換最小數,並且掃描整個陣列,更新最小數。

解題過程

    int findKthLargest(vector
<int>
& nums, int k) { int temp[k]; //初始化陣列 for (int i = 0; i < k; i++) { temp[i] = -1; } int min = -1; int minindex = 0; vector<int>::iterator it = nums.begin(); //遍歷陣列 for ( ; it != nums.end(); it++) { //如果有數字大於最小數,加入陣列
if (*it > min) { temp[minindex] = *it; min = *it; //更新陣列中的最小數 for (int i = 0; i < k; i++) { if (temp[i] < min) { min = temp[i]; minindex = i; } } } } return
temp[minindex]; }

分治演算法

解題思路及過程

原始演算法

每次挑選一箇中間數x(首先選擇k - 1),並維護三個陣列,left right v,若數字大於x,則放入right;若數字小於x,則放入left;否則放入v。再根據情況遞迴。遞迴規則如下:

這裡寫圖片描述

圖中是找第k小數規則,只需要將他稍微顛倒一下即可。演算法如下:

    int findKthLargest(vector<int>& nums, int k) {
        int comp = nums[k - 1];
        vector<int>::iterator it = nums.begin();
        vector<int> left;
        vector<int> right;
        vector<int> v;
        //為所有數字分組
        for ( ; it != nums.end(); it++) {
            if (*it > comp) {
                right.push_back(*it);
            } else if (*it < comp) {
                left.push_back(*it);
            } else {
                v.push_back(*it);
            }
        }

        //選擇需要遞迴的陣列
        if (k <= right.size()) {
            //在右邊
            return findKthLargest(right, k);
        } else if (k > right.size() && k <= right.size() + v.size()) {
            //已選中
            return v.front();
        } else {
            //在左邊
            return findKthLargest(left, k - right.size() - v.size());
        }
    }

優化取中間值

結果發現超出了記憶體限制,演算法需要改進,首先修改中間數x的取值,當長度小於20時,取nums的中間數,為了保險,選擇兩個數取平均值(如果只取一個數可能取到最小數,因此取中間兩個數,作為調節)

        int max = nums.size();           //總長
        int comp = 0;
        if (max > 20) {
            comp = (nums[max/2] + nums[max/2 + 1]) / 2;
        } else {
            comp = nums[max/2];
        }

這裡寫圖片描述

通過測試,不妨修改一個簡單的引數,當長度小於5時,就用心的方法取中間值x,再次測試。

這裡寫圖片描述

發現演算法反而變差了,那我們不妨試著當長度大於10時則取值,再次測試。

這裡寫圖片描述

發現演算法與20時差不多。

優化記憶體佔用

之前演算法失敗的原因是佔用太多記憶體,雖然在此基礎上使空間佔用稍小了一些,但實際上還有別的方法可以優化更多。
觀察後發現,實際上,每次分類之後我們只會用到一個分組,則另外一個分組實際上是不需要放入那麼多數字的。實際上,vector是很佔用空間的。
我們發現,當 右邊個數 >= k時,則只會用到右邊,此時就不必向左邊陣列插入元素;同理,當leftnum >= max - k,則只需要用到左邊。
因此,我們使用兩個變數標記useleft useright,每次插入前判斷。

    int findKthLargest(vector<int>& nums, int k) {
        int max = nums.size();           //總長
        int comp = 0;
        if (max > 20) {
            comp = (nums[max/2] + nums[max/2 + 1]) / 2;
        } else {
            comp = nums[max/2];
        }
        vector<int>::iterator it = nums.begin();
        bool useleft = false;           //結果只需要left,不需要向right和v新增
        bool useright = false;          //結果只需要right,不需要向left和v新增
        int leftnum = 0;                //實際left應該有的數量,下同
        int rightnum = 0;
        int vnum = 0;
        vector<int> left;
        vector<int> right;
        vector<int> v;
        //為所有數字分組
        for ( ; it != nums.end(); it++) {
            if (*it > comp) {
                //當只用到左邊的時候,則不需要實際插入
                if (!useleft) {
                    right.push_back(*it);
                }
                //需要計數,之後用到
                rightnum++;
                //判斷此時是否只用得到
                if (rightnum >= k) {
                    useright = true;
                }
            } else if (*it < comp) {
                if (!useright) {
                    left.push_back(*it);
                }
                leftnum++;
                if(leftnum >= max - k) {
                    useleft = true;
                }
            } else {
                v.push_back(*it);
                vnum++;
            }
        }

        //選擇需要遞迴的陣列
        if (k <= rightnum) {
            //在右邊
            return findKthLargest(right, k);
        } else if (k > rightnum && k <= rightnum + vnum) {
            //已選中
            return v.front();
        } else {
            //在左邊
            return findKthLargest(left, k - rightnum - vnum);
        }
    }

這裡寫圖片描述

發現雖然速度上沒有什麼實際的變化,但是對於空間的損耗的確少了許多。

原始碼

class Solution {
public:
    int findKthLargest(vector<int>& nums, int k) {
        int max = nums.size();           //總長
        int comp = 0;
        if (max > 20) {
            comp = (nums[max/2] + nums[max/2 + 1]) / 2;
        } else {
            comp = nums[max/2];
        }
        vector<int>::iterator it = nums.begin();
        bool useleft = false;           //結果只需要left,不需要向right和v新增
        bool useright = false;          //結果只需要right,不需要向left和v新增
        int leftnum = 0;                //實際left應該有的數量,下同
        int rightnum = 0;
        int vnum = 0;
        vector<int> left;
        vector<int> right;
        vector<int> v;
        //為所有數字分組
        for ( ; it != nums.end(); it++) {
            if (*it > comp) {
                //當只用到左邊的時候,則不需要實際插入
                if (!useleft) {
                    right.push_back(*it);
                }
                //需要計數,之後用到
                rightnum++;
                //判斷此時是否只用得到
                if (rightnum >= k) {
                    useright = true;
                }
            } else if (*it < comp) {
                if (!useright) {
                    left.push_back(*it);
                }
                leftnum++;
                if(leftnum >= max - k) {
                    useleft = true;
                }
            } else {
                v.push_back(*it);
                vnum++;
            }
        }

        //選擇需要遞迴的陣列
        if (k <= rightnum) {
            //在右邊
            return findKthLargest(right, k);
        } else if (k > rightnum && k <= rightnum + vnum) {
            //已選中
            return v.front();
        } else {
            //在左邊
            return findKthLargest(left, k - rightnum - vnum);
        }
    }
};