版權申明:本文爲博主窗戶(Colin Cai)原創,歡迎轉帖。如要轉貼,必須註明原文網址 http://www.cnblogs.com/Colin-Cai/p/12664044.html 做者:窗戶 QQ/微信:6679072 E-mail:6679072@qq.com
所謂衆數,源於這樣的一個題目:一個長度爲len的數組,其中有個數出現的次數大於len/2,如何找出這個數。html
基於排序算法
排序是第一感受,就是把這個數組排序一下,再遍歷一遍獲得結果。sql
C語言來寫基本以下:數組
int find(int a, int len) { sort(a, len); return traverse(a, len); }
排序有時間複雜度爲O(nlogn)的算法,好比快速排序、歸併排序、堆排序,而遍歷一遍排序後的數組獲得結果是時間線性複雜度,也就是O(n)。因此整個算法時間複雜度是O(nlogn)。微信
尋找更好的算法函數
上面的算法實在太簡單,不少時候咱們第一感就能夠出來的東西未必靠譜。spa
咱們是否是能夠找到一種線性時間級別的算法,也就是Θ(n)時間級別的算法?Θ是上下界一致的符號。其實咱們很容易證實,不存在比線性時間級別低的算法,也就是時間o(n),小o不一樣於大O,是指低階無窮大。證實大體以下:code
若是一個算法能夠以o(n)的時間複雜度解決上述問題。由於是比n更低階的無窮大,那麼必定存在一個長度爲N的數組,在完成這個算法以後,數組內被檢測的元素小於N/2。假設算法運算的結果爲a,而後,咱們把這個數組在運算該算法時沒有檢測的元素所有替換爲成同一個不是算法所得結果a的數b。而後新的數組,再經過算法去運算,由於沒有檢測的數不會影響其算法結果,結果天然仍是a,可實際上,數組超過N/2次出現的數是b。從而致使矛盾,因此針對該問題的o(n)時間算法不存在。htm
咱們如今能夠開始想點更加深刻點的東西。blog
咱們首先會發現,若是一個數組中有兩個不一樣的數,將數組去掉這兩個數,獲得一個新數組,那麼這個新數組依然和老數組同樣存在相同的衆數。這一條很容易證實:
假設數組爲a,長度爲len,衆數爲x,出現的次數爲t,固然知足t>len/2。假設其中有兩個數y和z,y≠z。去掉這兩個數,剩下的數組長度爲len-2。若是這兩個數都不等於衆數x,也就是x≠y且x≠z,那麼x在新的數組中出現的次數依然是t,t>len/2>(len-2)/2,因此t依然是新的數組裏的衆數。而若是這兩個數中存在x,那麼天然只有一個x,則剩下的數組中x出現的次數是t-1,t-1>len/2-1=(len-2)/2,因此x依然是新數組的衆數。
有了上述的思路,咱們會去想,如何找到這一對對的不一樣的數呢。
咱們能夠記錄數字num和其重複的次數times,遍歷一遍數組,按照如下流程圖來。
num/times一直記錄着數字和其重複次數,times加1和減1都是隨着數組新來的數是否和num相同來決定,減1的狀況其實就取決於上面證實的那個命題,找到一對不相同的數字,去掉這兩個,剩下的數組的衆數不變。
關於在於證實最後的結果是所求的衆數。若是後面的結果不是衆數,那麼衆數每出現一次,就得與一個不是衆數的數一塊兒「抵消」,因此數組中不是衆數的數的數量不會少於衆數的數量,然而這不是現實。因而上述算法成立,它有着線性時間複雜度O(n),常數空間複雜度O(1)。
C語言代碼基本以下:
int find(int *a, int len) {
int i, num = 0, times = 0;for(i=0;i<len;i++) { if(times > 0) { if(num == a[i]) times++; else times--; } else { num = a[i]; times = 1; } } return num; }
若是用Scheme編寫,程序能夠簡潔以下:
(define (find s) (car (fold-right (lambda (n r) (if (zero? (cdr r)) (cons n 1) (cons (car r) ((if (eq? n (car r)) + -) (cdr r) 1)))) '(() . 0) s)))
升級以後的問題
上面的衆數是出現次數大於數組長度的1/2的,若是將這裏的1/2改爲1/3,要找出來怎麼作呢?
例如,數組是[1, 1, 2, 3, 4],那麼要找出的衆數爲1。
再昇華一下,若是是1/m,這裏的m是一個參數,該怎麼找出來呢?這個問題要去以前那個問題要複雜一些,另外咱們要意識到,問題升級以後,衆數是有可能不止一個的,好比[1, 1, 2, 2, 3]長度爲5,1和2都大於5/3。最多有m-1個衆數。
思路
若是依然是排序以後再遍歷,依然是有效的,但是時間複雜度仍是O(nlogn)級別,咱們仍是期待有着線性時間複雜度的算法。
對於第一個問題,成立的前提是去掉數組裏兩個不同的數,衆數依然不變。那麼對於升級以後的問題,是否是依然有相似的結果。不一樣於以前,咱們如今來看在衆數從1/2以上變成1/m以上,咱們來看去掉長度爲len的數組a裏m個互不相同的數,會發生什麼。證實過程以下:
一樣,咱們假設a裏有一個衆數x,x出現的次數爲t,看看去掉m個不同的數以後x仍是不是衆數。去掉m個數以後,新的數組長度爲len-m。x是衆數,因此x的出現次數t > len/m,若是去掉的m個數中沒有x,則x在剩餘的數組中的出現次數依然是t,t > len/m > (len-m)/m,因此這種狀況下x仍是衆數;若是去掉的m個數中存在x,由於m個數互不相同,因此其中x只有一個,因此x在剩餘的數組中的出現次數是t-1,t > len/m,從而t-1 > len/m-1 = (len-m)/m,因此x在剩餘的數組裏依然是衆數。以上對於數組中全部的衆數都成立。同理可證,對於數組中不是衆數的數,剩餘的數組中依然不是衆數,實際上,把上面全部的>替換爲≤便可。
有了上面的理解,咱們能夠仿照以前的算法,只是這裏改爲了長度最多爲n-1的鏈表。好比對於數組[1, 2, 1, 3],衆數1超過數組長度4的1/3,過程以下
初始時,空鏈表[]
檢索第一個元素1,發現鏈表中沒有記錄num=1的表元,鏈表的長度沒有達到2,因此插入到鏈表,獲得[(num=1,times=1)]
檢索第二個元素2,發現鏈表中沒有記錄num=2的表元,鏈表的長度沒有達到2,插入到鏈表,獲得[(num=1,times=1), (num=2,times=1)]
檢索第三個元素1,發現鏈表中已經存在num=1的表元,則把該表元times加1,獲得[(num=1,times=2), (num=2,times=1)]
檢索第四個元素3,發現鏈表中沒有num=3的表元,鏈表長度已經達到最大,等於2,因而執行消去,也就是每一個表元的times減1,並把減爲0的表元移出鏈表,獲得[(num=1,times=1)]
以上就是過程,最終獲得衆數爲1。
以上過程最終獲得的鏈表的確包含了全部的衆數,這一點很容易證實,由於任何一個衆數的times都不可能被徹底抵消掉。可是,以上過程實際並不保證最後獲得的鏈表裏全都是衆數,好比[1,1,2,3,4]最終獲得[(num=1,times=1), (num=4,times=1)],但4並非衆數。
因此咱們須要獲得這個鏈表以後,再遍歷一遍數組,將重複次數記載於鏈表之中。
Python下使用map/reduce高階函數來取代過程式下的循環,上述的算法也須要以下這麼多的代碼。
from functools import reduce def find(a, m): def find_index(arr, test): for i in range(len(arr)): if test(arr[i]): return i return -1 def check(r, n): index = find_index(r, lambda x : x[0]==n) if index >= 0: r[index][1] += 1 return r if len(r) < m-1: return r+[[n,1]] return reduce(lambda arr,x : arr if x[1]==1 else arr+[[x[0],x[1]-1]], r, []) def count(r, n): index = find_index(r, lambda x : x[0]==n) if index < 0: return r r[index][1] += 1 return r return reduce(lambda r,x : r+[x[0]] if x[1]>len(a)//m else r, \ reduce(count, a, \ list(map(lambda x : [x[0],0], reduce(check, a, [])))), [])
若是用C語言編寫代碼會更多一些,不過能夠不用鏈表,改用固定長度的數組效率會高不少,times=0的狀況表明着元素不被佔用。此處就不實現了,交給有興趣的讀者本身來實現吧。