1. 程式人生 > >演算法分析與設計第五次作業(leetcode 中 Majority Element 題解)

演算法分析與設計第五次作業(leetcode 中 Majority Element 題解)

心得體會

這個題目有兩個版本Majority Element,和Majority Element II,解題的方法比較巧妙,有點想不到的感覺,並且證明過程也很有趣,所以就記錄下來(具體詳情見正文題解)。

題解正文

題目描述在這裡插入圖片描述在這裡插入圖片描述

問題分析

題目要求majority number,也就是出現次數最多的數,第一個題目求佔比超過1/2的數,第二題要求佔比超過1/3的那些數,其實這種題目可以拓展為求佔比超過1/k的那些數字。下面會從簡單版本I的求解過渡到版本II,最後得出1/n眾數的求解。

解題思路

下面的思路中,nums表示輸入的數字陣列

  1. 最簡單的方法當然是對於每一個數字都用一個num記錄數值,一個count記錄出現次數,最後篩選count大於nums.size()/k的那些數字。這樣做的話時間複雜度和空間複雜度都是線性的,達不到題目要求的O(1)空間複雜度。
  2. 所以換一個思路,我們把考慮把數字進行配對,比如求出1/2眾數,因為佔比超過1/2的數字最多有一個,所以我們可以將1/2眾數和其它數字兩兩配對,如果存在1/2眾數,那麼配對到最後一定還會有1/2眾數剩餘,因為1/2眾數佔比超過1/2,也就比其它數字個數總和還多。配對完成之後,還剩下那些沒法配對的數字,這些數字就是1/2眾數了。所以換一個思路,我們把考慮把數字進行配對,比如求出1/2眾數,因為佔比超過1/2的數字最多有一個,所以我們可以將1/2眾數和其它數字兩兩配對,如果存在1/2眾數,那麼配對到最後一定還會有1/2眾數剩餘,因為1/2眾數佔比超過1/2,也就比其它數字個數總和還多。配對完成之後,還剩下那些沒法配對的數字,這些數字就是1/2眾數了。 通過上面的分析我們將這個問題轉化成了一個配對問題,但是具體怎麼配對呢:可以用一個num記錄數值,count記錄對應數值已經出現的次數(初值為0),然後對nums陣列遍歷,如果遇到和當前num記錄數值相同的數,就將count遞增,這樣做的含義是加入配對佇列,等待其它的數字與之配對相消
    ;反之和num值不同則需要將count遞減(要求count>0),這樣做對應的含義是將兩個不同類別的數字配對相消;如果count值為零就將num值替換為遍歷的當前數字nums[i] 並遞增count。這樣做到最後nums陣列中數字只有兩個結果:a.通過配對消除;b.加入待配對佇列(也就是num&count表示的單個數字組成的佇列),根據上面的分析,被存在num中的只能是1/2眾數,少數數字和部分1/2眾數被配對消除。 問題解決,但是細節部分還有一些問題:
    • 加入到配對佇列的不一定就是1/2眾數,也可能是1/2眾數之外的數字,但是這不影響演算法的正確性: 首先我們確認,加入到配對佇列的數字只有兩種,1/2眾數和其它數字。 如果加入配對佇列
      的是1/2眾數那麼就是上面分析的情況,不會有問題。 如果加入到配對佇列的不是1/2眾數,那麼後續遍歷到的與之不同的數字也有兩種可能,一種可能是1/2眾數,另一種可能是其它數字,如果是1/2眾數,那麼和上面分析的情況是一樣的,即1/2眾數 vs 其它數字配對相消;如果不是1/2眾數,那麼就是其它數字 vs 其它數字配對相消,這樣對於找出1/2眾數更加有利,因為其他數字正在以比1/2眾數更快的速度被配對消去,又因為其他數字本來就比1/2眾數少,那麼自然更快被消去。 所以無論如何,一個1/2眾數至少消去一個其他數字,最後留在等待配對佇列中的只能是1/2眾數(因為其它的數都被消去了)。
    • 萬一1/2眾數根本不存在,這個可以在上述演算法做完之後,遍歷一遍nums陣列求出num數值出現次數,超過n/2則存在,並且就是num,反正不存在1/2眾數,因為1/2眾數必然滿足前面分析到的情況。
  3. 擴充套件到求1/k眾數,思路和求1/2眾數一樣,只不過上次是每組兩個數字配對,這次是每組k個數字的配對。 具體做法:將眾數與非眾數劃分為k個一組(其中眾數每一個只出現一次,其它數字用非眾數填充),就能夠在第nums.size()/k次消去之前將所有非眾數全部去掉,而餘下的數字都是1/k眾數。 為什麼是這樣,可以如下證明:假設有x(0<=x<1)個1/k眾數,首先建立一個大小nums.size()的容器N,我們將N均分為k個小容器,其中x個小容器用來放1/k眾數,每一個小容器放入不同的1/k眾數,這樣這x個小容器必定被裝滿而且每個1/k眾數還有剩餘;與此同時,均分剩餘的非1/k眾數填充剩下的容器,一定裝不滿(因為所有數字加起來剛好填滿所有小容器,現在有一些1/k眾數還在外面,所以剩餘的k-x個小容器一定裝不滿)。然後我們每次從每個容器中去掉一個數字,對應的實際含義是將k個不同類別的數字配對相消,這樣在第nums.size()/k次消去之前所有的小容器都將清空,留下的x個容器中全都是1/k眾數,證畢。 最後說一下配對怎麼做(其實和前面1/2眾數配對差不多):申請兩個大小為k-1的陣列num[k-1]={}和count[k-1]={}用來記錄眾數值和個數,然後遍歷nums,如果在num陣列中有某個數字與當前的nums[i]相等,相應的count[i]遞增,這樣做的含義是將數字加入配對佇列,等待其它的數字與之配對相消;反之如果在num陣列中不存在這樣的數字,則需要將所有count[i]遞減(要求count陣列中所有count>0),這樣做對應的含義是將k個不同類別的數字配對相消;如果count陣列中存在某個count[i]值為零,就將對應的num[i]值替換為遍歷的當前數字nums[i] 並遞增count。這樣做以後所有可能的1/k眾數都在num陣列中了,我們只需再遍歷一次nums陣列求出num[i]對應count[i],如果count[i]>nums.size()/k那麼num[i]就是1/k眾數。 如果你還想要問“加入到配對佇列的不一定就是1/k眾數,也可能是1/k眾數之外的數字怎麼辦”這樣的問題,那我的回答和前面2.1說到的一樣,不會影響結果,因為這樣的話非眾數將以更快的速度被消去,更利於求出答案。

演算法步驟

因為1/2眾數求解、1/3眾數求解都可以歸入1/k眾數求解,下面只給出1/k眾數求解的步驟: 下面的nums陣列為輸入的帶查詢陣列,num,count都是大小為k的陣列,用於儲存k-1個可能的眾數。

  1. 遍歷nums陣列,對於每個nums[i]
    1. 遍歷num陣列,對於每個num[j] 判斷nums[i]是否等於num[j],如果是,遞增count[j],並跳出當層迴圈(break),並在出迴圈後跳過外層迴圈餘下程式碼(continue);
    2. 遍歷count陣列,對於每個count[j] 是否存在某個count[j]等於0,如果是,將num[j]設定為當前遍歷到的數字nums[i],並設定count[j]為1,然後跳出當層迴圈(break),出迴圈之後跳過外層迴圈餘下程式碼;
    3. 遍歷count陣列將所有count[j]遞減;
  2. count陣列全置為零;
  3. 遍歷nums陣列,對於每個nums[i]. 遍歷nums陣列,對於每個nums[i]
    1. 遍歷遍歷num陣列,對於每個num[j] 如果nums[i]和num[j]相等就將count[j]遞增;
  4. 遍歷count陣列,對每個count[i] 如果count[j]>nums.size()/k就輸出num[i]作為結果之一;

演算法複雜度分析

時間複雜度:只需要一次遍歷nums陣列求出k個可能的1/k眾數(這其中每次迴圈都要遍歷兩個大小為k的陣列num和count,判斷num[i]是否等於當前數字,count是否等於0);再一次遍歷求出k個數字對應的個數count(這其中每次迴圈都要遍歷num陣列判斷num[i]是否等於當前數字);最後一次遍歷num陣列判斷數字是否確實是眾數;所以總的空間複雜度為O(2kn)+O(kn)+O(k)=O(kn)。如果k是常數級別,複雜度可寫為O(n)。 空間複雜度:一共使用兩個大小為k的陣列num和count,空間複雜度為O(k),如果k為常數級別,可寫為O(1)。

程式碼實現&結果分析

  • 1/2眾數求解:
    class Solution {
    public:
        int majorityElement(vector<int>& nums) {
            int num = 0;
            int count = 0;
            for (int i = 0; i < nums.size(); ++i) {
                if ( num == nums[i] ) {
                    count++;
                } else if ( count == 0 ) {
                    num = nums[i];
                    count++;
                } else {
                    count--;
                }
            }
            return num;
        }
    };
    
    提交結果: 在這裡插入圖片描述
  • 1/3眾數求解:
    class Solution {
    public:
        vector<int> majorityElement(vector<int>& nums) {
            int num1 = 0, num2 = 0;
            int count1 = 0, count2 = 0;
            for (int i = 0; i < nums.size(); ++i) {
                if ( num1 == nums[i] ) {
                    count1++;
                } else if ( num2 == nums[i] ) {
                    count2++;
                } else if ( count1 == 0 ) {
                    num1 = nums[i];
                    count1++;
                } else if ( count2 == 0 ) {
                    num2 = nums[i];
                    count2++;
                } else {
                    count1--;
                    count2--;
                }
            }
            count1 = 0;
            count2 = 0;
            for (int i = 0; i < nums.size(); ++i)
            {
                if ( nums[i] == num1 ) count1++;
                else if ( nums[i] == num2 ) count2++;
            }
            vector<int> res;
            if ( count1 > nums.size()/3 ) res.push_back(num1);
            if ( count2 > nums.size()/3 ) res.push_back(num2);
            return res;
        }
    };
    
    提交結果: 在這裡插入圖片描述 beat 98%+,上述解法基本上就是最優解法。