【算法31】寻找数组的主元素(Majority Element)
题外话
最近有些网友来信问我博客怎么不更新了,是不是不刷题了,真是惭愧啊,题还是在刷的,不过刷题的频率没以前高了,看完《算法导论》后感觉网上很多讨论的题目其实在导论中都已经有非常好的算法以及数学证明,只是照搬的话好像意义也不是很大,希望找到些有代表性的题目在更新,另外希望能接着前面的《穷举递归和回溯算法终结篇》一系列如动态规划、贪心算法类的终结篇,在梳理自己知识结构的同时也能够帮助读者们更系统的学习算法思想。好了话不多说,进入正题。
问题描述
给定一个数组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