題外話
最近有些網友來信問我博客怎么不更新了,是不是不刷題了,真是慚愧啊,題還是在刷的,不過刷題的頻率沒以前高了,看完《算法導論》后感覺網上很多討論的題目其實在導論中都已經有非常好的算法以及數學證明,只是照搬的話好像意義也不是很大,希望找到些有代表性的題目在更新,另外希望能接着前面的《窮舉遞歸和回溯算法終結篇》一系列如動態規划、貪心算法類的終結篇,在梳理自己知識結構的同時也能夠幫助讀者們更系統的學習算法思想。好了話不多說,進入正題。
問題描述
給定一個數組A[n], 定義數組的主元素 ( Majority Element) 為數組中出現次數超過 n/2 的元素。設計一個高效的算法來尋找數組的主元素。題目來源在這里 。
解法一
最容易想到的方法就是便利數組進行元素計數,然后返回元素個數大於 n/2 的元素,這種方法需要 O(n) 的時間復雜度 和 O(n) 空間復雜度,不算是一個好方法。
解法二
在解法一的基礎上考慮消去 O(n) 的空間復雜度,如果元素出現次數超過 n/2,那么假設數組已經排序的話,那么中位數就是我們要找的數。進一步的我們除了中位數,我們不需要其他的數排好序。問題進一步轉化為求數組的中位數,推廣版本就是在O(n)的時間內尋找第 i 大的數,這在算法導論上有詳細的論述,網上資料也很多。基本來說,就是利用快排的 partition 對數組進行划分,分為 [..., pivot, ...] 三個部分,假設划分后 pivot 是第 m 個元素,如果 m == i, 則pivot 即為第 i 大元素;反之對如果 pivot 的位置在大於 i (m > i),則對 left 部分進行遞歸尋找第 i 大元素;反之對 right 部分進行遞歸尋找第 (i - m) 大元素。代碼如下, 然而這種算法在大數組的情況下回超時。
1 #include <iostream> 2 #include <string> 3 #include <vector> 4 #include <set> 5 #include <unordered_set> 6 #include <map> 7 #include <unordered_map> 8 #include <queue> 9 #include <stack> 10 #include <algorithm> 11 #include <functional> 12 #include <utility> 13 #include <cstdio> 14 #include <cstdlib> 15 using namespace std; 16 17 /* 18 * Return a position k, such that all the elements in [left, k) 19 * are smaller than or equal to num[k] && all the elements in 20 * (k, right] are larger than num[k] 21 * 22 * Note that here is the randomize version of partition, every time 23 * we choose a random number as the pivot. 24 */ 25 int RandomPartition(vector<int> &num, int left, int right) 26 { 27 // random partition 28 int rnd = rand() % (right - left + 1) + left; 29 swap(num[rnd], num[right]); 30 31 // pivot 32 int x = num[right]; 33 int i = left - 1; 34 for (int j = left; j < right; ++j) 35 { 36 if (num[j] <= x) 37 { 38 swap(num[j], num[i + 1]); 39 i += 1; 40 } 41 } 42 swap(num[right], num[i+1]); 43 return i + 1; 44 } 45 46 /* 47 * Return the i-th ordered element in num[left, right] but without 48 * sorting the array. 49 */ 50 51 int RandomSelect(vector<int> &num, int left, int right, int i) 52 { 53 if (right - left + 1 < i || left > right) return -1; 54 55 // partition the num[], return the pivot position m 56 int m = RandomPartition(num, left, right); 57 58 // k is the number of element in num[left, m] 59 int k = m - left + 1; 60 61 if (k == i) 62 { 63 return num[m]; 64 } 65 else if (k > i) 66 { 67 // find the i-th ordered element in num[left, m-1] 68 return RandomSelect(num, left, m - 1, i); 69 } 70 else 71 { 72 // find the (i-k)-th ordered element in num[m+1, right] 73 return RandomSelect(num, m + 1, right, i - k); 74 } 75 } 76 77 /* 78 * return the median of num[] 79 */ 80 int majorityElement(vector<int> &num) 81 { 82 int n = num.size(); 83 int mid = n / 2; 84 return RandomSelect(num, 0, n-1, mid); 85 } 86 87 int main() 88 { 89 int a[] = {3, 2, 2}; 90 int b[] = {3, 2, 2, 2}; 91 92 vector<int> v1(a, a + 3); 93 vector<int> v2(b, b + 4); 94 95 cout << majorityElement(v1) << endl; 96 cout << majorityElement(v2) << endl; 97 98 return 0; 99 }
解法三
這種方法的思想是把 majority element 看成是 1,而把其他的元素看成是 -1。算法首先取第一個元素 x 作為 majority element,並計 mark = 1;而后遍歷所有的元素,如果元素和 x 相等, 則 mark ++;否則如果不等, 則 mark--, 如果 mark == 0, 則重置 mark = 1, 並且更新 x 為當前元素。 由於majority element 的數量大於一半,所以最后剩下的必然是majority element. AC code 如下.
1 #include <iostream> 2 #include <string> 3 #include <vector> 4 #include <set> 5 #include <unordered_set> 6 #include <map> 7 #include <unordered_map> 8 #include <queue> 9 #include <stack> 10 #include <algorithm> 11 #include <functional> 12 #include <utility> 13 #include <cstdio> 14 #include <cstdlib> 15 using namespace std; 16 17 int majorityElement(vector<int>& num) 18 { 19 int n = num.size(); 20 if (n < 0) return -1; 21 if (n == 1) return num[0]; 22 23 int x = num[0]; 24 int mark = 1; 25 for (int i = 1; i < n; ++i) 26 { 27 if (mark == 0) 28 { 29 mark = 1; 30 x = num[i]; 31 } 32 else if (num[i] == x) 33 { 34 mark++; 35 } 36 else if (num[i] != x) 37 { 38 mark--; 39 } 40 } 41 return x; 42 } 43 44 int main() 45 { 46 int a[] = {3, 2, 2}; 47 int b[] = {3, 2, 2, 2}; 48 49 vector<int> v1(a, a + 3); 50 vector<int> v2(b, b + 4); 51 52 cout << majorityElement(v1) << endl; 53 cout << majorityElement(v2) << endl; 54 55 return 0; 56 }
參考文獻
[1] 《算法導論》第二版,第九章 《中位數和順序統計學》.
[2] http://people.cis.ksu.edu/~subbu/Papers/Majority%20Element.pdf
