一、引言
这道题做出来还是比较简单的,方法非常多,甚至于 C++ 最高票答案贴出来了 6 种方法供读者慢慢研究。不过这篇博客不对 C++ 最高票答案进行详细分析,而是对最高票答案 (Java)进行分析,因为确实是一个非常烧脑的一个解题方法。
话不多说,直接看题吧:
Given an array of size n, find the majority element. The majority element is the element that appears more than ⌊ n/2 ⌋ times.
You may assume that the array is non-empty and the majority element always exist in the array.
题目信息不多,简单翻译下:
给定一个长度为 n 的数组,请找出其中的主要元素。主要元素是一种在数组中至少出现了 ⌊ n/2 ⌋ 次的元素。
你可以假定数组非空并且一定存在主要元素。
这里需要解释下 ⌊ n/2 ⌋
:这并非中括号,而是向下取整的意思,也就是说如果 n = 5,那么这个式子等于 2,因为 5 / 2 = 2.5,2.5 向下取整则为 2。
那么说白了,这道题就是要找到给定数组中出现次数大于 ⌊ n/2 ⌋
的元素嘛。
二、unordered_map 先行:元素出现次数的统计
只要涉及到元素的出现次数的统计,那么映射是肯定需要的了,又考虑到这里不需要映射内元素的排序,这里选择使用 std::unordered_map。
那么,现在让我们整理下解题的思路:
首先,我们需要遍历整个数组,拿到所有元素的出现次数的映射“数字 - 出现次数”
然后,我们遍历这个映射关系,找到其中出现次数最大的即可(出现次数最大的必然是大于 n / 2 向下取整的)
根据这个思路,我写出了第一个版本的代码:
// my solution 1 , runtime = 19 ms
class Solution1 {
public:
int majorityElement(vector<int>& nums) {
unordered_map<int, int> appear_count;
int max = 0, result = 0;
for (auto i : nums) ++appear_count[i];
for (auto item : appear_count)
if (max < item.second) {
max = item.second;
result = item.first;
}
return result;
}
};
代码逻辑非常简单,就是使用 max 记录最大出现次数,result 记录需要返回的值,appear_count 记录出现次数。
但是这份代码并没有用到或者说并没有完美的用到 ⌊ n/2 ⌋
这个条件,甚至于说,我们都不需要将整个数组全部遍历,我们只要找到了一个元素重复了 ⌊ n/2 ⌋
次,我们就可以直接返回这个元素了,都不需要再进行处理了。
那么,为了更好的使用到 ⌊ n/2 ⌋
这个条件,我写出了第二个版本的代码:
// my solution 2 , runtime = 22 ms
class Solution3 {
public:
int majorityElement(vector<int>& nums) {
unordered_map<int, int> appear_count;
for (auto i : nums)
if (++appear_count[i] > nums.size() / 2)
return i;
}
};
这份代码只有短短 4 行,非常简练:
首先,我还是定义了一个 unordered_map 用来统计出现次数
然后,我试图遍历整个数组,每次遍历都递增对应元素的计数值;当该元素的计数值超过了 nums.size() / 2 了,就返回当前的这个元素值(这里值得注意的是,C++ 里面的 int 类型的值相除,默认就是地板除法也就是去尾法,相当于就是向下取整,所以这里正好契合题意)
这一个方法不仅思路简单,就连代码也是非常优雅,就我自己的观点而言,这是我最喜欢的解答方案。
三、无语凝噎:最高票答案在搞什么鬼
然而,做出来了自认为的最优雅的答案并没有让我觉得满足,于是我点开了最高票答案。
然后我就懵了 T_T
这里我将其代码“翻译”成了 C++:
// most posts answer , runtime = 19 ms
class Solution4 {
public:
int majorityElement(vector<int>& nums) {
int majority = nums[0], count = 1;
for (int i = 1; i < nums.size(); ++i) {
if (count == 0) {
count++;
majority = nums[i];
} else if (majority == nums[i]) {
count++;
} else count--;
}
return majority;
}
};
这份代码在搞什么鬼???
完全看不懂啊 :(
不过别急,让我们拿着一个 case 慢慢地看这个方法,或许就能看出来什么端倪了:
Input: [1, 1, 2, 3, 1]
让我们输入这个数组,然后跟着它的代码走一遍看看:
进入循环前的各变量初始值:
i 初始值 | majority 初始值 | count 初始值 |
---|---|---|
1 | nums[0] | 1 |
循环过程中的各变量值变化:
当前循环次数 | i 值 | num[i] 值 | majority 值 | count 值 |
---|---|---|---|---|
第 1 次 | 1 | 1 | nums[1] | 2 |
第 2 次 | 2 | 2 | nums[1] | 1 |
第 3 次 | 3 | 3 | nums[1] | 0 |
第 4 次 | 4 | 1 | nums[4] | 1 |
最后返回了 majority = nums[4] = 1
。接下来让我们详细分析下这个过程,看看作者的逻辑到底是什么样的:
首先,我们关注一下作者的初始值:作者声明了两个变量,顾名思义,majority 必然是最后要返回的主要元素的值,count 为计数值,至于是什么计数,我们还要参考后面的代码逻辑才能定义
然后,作者以 i = 1 为初始值开始了 nums 数组的遍历:这里为什么要以 i = 1 开始遍历呢?很简单,因为作者已经处理了第 1 个元素了,并且将 majority 和 count 都相对于第 1 个元素进行了初始化(一方面也是处理当前数组只有一个元素的情况);之后,作者进行了三次判断,首先判断 count 是否为 0,然后判断 majority 是否等于当前的 nums[i],最后的情况(也就是
count !=0 && majority != nums[i]
)的时候只进行 count 的递减。那么这里作者究竟是什么意思呢?我们通过观察多次遍历的过程,最终发现 count 其实就是作者用来模拟⌊ n/2 ⌋
这个条件的工具。为什么这么说呢?只要一个元素的出现次数大于了⌊ n/2 ⌋
,那么我们进行只要与此元素值相同的值进行 count ++操作,只要与此元素不同的值进行 count – 操作,最后的 count 结果必然大于 0。根据这个思路,作者循环遍历得到了 majority 的值最后,作者放心的返回得到的 majority 的值,因为有对于第一个元素的初始化,所以当数组中只有一个元素的时候,也不会出错
如果还不懂上面的这个逻辑的话,我这里再详细进行解释下:
在一个数组中,一个出现了 至少
⌊ n/2 ⌋
次的元素有什么特征呢?如果我们对 majority (该数组的主要元素)进行这样的操作:
1.当
majority = nums[i]
则count++
2.当majority != nums[i]
则count--
当我们遍历完整个数组,必然出现 count > 0 对不对?
换句话说,当出现 count <= 0,当前的元素则必然不是 majority(该数组的主要元素)。当我们遇到了 count = 0 的时候怎么办?我们记录当前的
nums[i]
为 majority 再进行上述的计算即可。
相信根据我上述的两个表格和相信解释,大家应该就能了解作者的思路了。
可以说,非常非常奇妙!
但是,非常非常难以理解!
所以说,这样的代码其实是不可取的 T_T
不过呢,我们需要有这种能够读懂他人代码逻辑的能力,尽管他的代码再如何如何不友好,这是一种能力,一种每个程序员都应该掌握的能力。
四、总结
这道题,做出来也许只花了不到 15 分钟,但是为了看懂最高票答案的逻辑,我居然也差不多花了相等量的时间。
不过这一切都是值得的。
阅读代码也是一种能力,一种直接与代码的思维直接对话的能力。
其实我相信每个程序员都会有所感触:
真正写代码的时间并不多,多的是去理解他人的代码。这些代码或简洁或复杂,或注释良好或逻辑潦草,我们能做的并非是督促作者如何如何,我们需要的是提高自己的阅读能力,才能立于更加主动的位置。
认识到阅读代码与编写代码同样重要,那么这道题也算是很有收获了 ^_^