题干
给定一个大小为 n
的数组 nums
,返回其中的多数元素。多数元素是指在数组中出现次数大于⌊n/2⌋
的元素。
你可以假设数组是非空的,并且给定的数组总是存在多数元素。
示例 1:
输入:nums = [3,2,3] 输出:3
示例 2:
输入:nums = [2,2,1,1,1,2,2] 输出:2
提示:
n == nums.length
1 <= n <= 5 * 104
-109 <= nums[i] <= 109
进阶:尝试设计时间复杂度为 O(n)、空间复杂度为 O(1) 的算法解决此问题。
题解
初学者看到进阶后面这句话的时候会慌,为什么我就是写不出O(n)的算法?但其实这道题O(n)的算法是一个专用找多数元素的算法,比较像学校里学的针对某种特殊题目的所谓“结论技巧”;对于有基础的大佬来说多掌握一点总是没错的,但是初学者切忌舍本逐末,因此在这里会给出很多经典算法写成的题解,也许复杂度比不上O(n),但是泛用性肯定更好。
首先省略暴力解法(先遍历一遍数组找出不重复的元素,再针对每一个元素再遍历一遍数组判断是不是多数元素)。
解法一:排序
为什么会和排序扯上关系?这需要一些思考成本。那么还是一个实际样例,当你在看比赛的时候,很多时候会有显示胜率的时候,这个时候可能会有类似进度条的这个一个东西,我们是怎么判断到底是左边胜率高还是右边胜率高?
我们脑子一般的想法是目测一个中点,如果中点是黑的,那说明黑方胜率>50%,反之就是白方胜率>50%,因此我们可以先得出一个基本正确的结论,如果一个数组是有序的,且一个数是多数元素,那么数组中间位置上的那个数就是我们要求的众数(题目已说明众数一定存在)。
当然上面我们为了简单易懂,只举了两种元素时候的情况,当元素种类大于2时也应该是正确的,读者可以自行证明。
如果上述没看懂,换个说法,往一个横条上填色,已知有一种颜色的量>50%,那么就算你从头或者从尾开始涂,不管怎么涂都会把中点包括在里面。
接下来我们来缝缝补补一下这个结论,题目要求的是“出现次数大于⌊n/2⌋
”,也就是说3个元素至少出现2次,4个元素至少出现3次,因此我们需要去判断的位置是nums[n/2]。那么代码已经呼之欲出了,直接调用C++自带的sort即可。
class Solution {
public:
int majorityElement(vector<int>& nums) {
int n=nums.size()/2;
sort(nums.begin(),nums.end());
return nums[n];
}
};
题解二:分治
请掌握这种算法思想,很有用。
分治是什么意思呢?说白了就是大事化小。比如有时候一个年级要找出这次考试谁是年级第一,当然你可以把所有人成绩做一个排序,然后找第一;另一个想法是先找出每一个班的第一名,再对这几个班级第一排序找年级第一。分治可以提高算法效率,具体为什么可以在这里不做赘述。
回到这道题,根据分治的观点,一个数组的众数,只会出现在前半部分的众数和后半部分众数这两个数之间,而前半部分的众数只会出现在前半部分的前半部分和前半部分的后半部分这两个数之间,如果其中某个部分只有一个数,那这个数就是这个部分的众数,经过这么分析下来,算法已经呼之欲出了,就是递归。
先上代码(力扣官方题解):
class Solution {
int count_in_range(vector<int>& nums, int target, int lo, int hi) {
int count = 0;
for (int i = lo; i <= hi; ++i)
if (nums[i] == target)
++count;
return count;
}
int majority_element_rec(vector<int>& nums, int lo, int hi) {
if (lo == hi)
return nums[lo];
int mid = (lo + hi) / 2;
int left_majority = majority_element_rec(nums, lo, mid);
int right_majority = majority_element_rec(nums, mid + 1, hi);
if (count_in_range(nums, left_majority, lo, hi) > (hi - lo + 1) / 2)
return left_majority;
if (count_in_range(nums, right_majority, lo, hi) > (hi - lo + 1) / 2)
return right_majority;
return -1;
}
public:
int majorityElement(vector<int>& nums) {
return majority_element_rec(nums, 0, nums.size() - 1);
}
};
一点一点来解释:
int count_in_range(vector<int>& nums, int target, int lo, int hi) {
int count = 0;
for (int i = lo; i <= hi; ++i)
if (nums[i] == target)
++count;
return count;
}
//这个函数用来确定从lo到hi这个范围内target出现了几次
int majority_element_rec(vector<int>& nums, int lo, int hi) {
if (lo == hi)
return nums[lo];
//如果这个范围只有一个元素,那这个元素就是众数
int mid = (lo + hi) / 2;
int left_majority = majority_element_rec(nums, lo, mid);
//递归找前半部分的众数
int right_majority = majority_element_rec(nums, mid + 1, hi);
//递归找后半部分的众数
if (count_in_range(nums, left_majority, lo, hi) > (hi - lo + 1) / 2)
return left_majority;
//如果前半部分的众数出现次数大于n/2,那就是整个部分的众数
if (count_in_range(nums, right_majority, lo, hi) > (hi - lo + 1) / 2)
return right_majority;
//如果后半部分的众数出现次数大于n/2,那就是整个部分的众数
return -1;
//如果都没有超过n/2,那就没有众数
}
为什么要return -1?题目不是说众数一定存在吗?没错,但是整个部分众数存在不代表每个部分都有众数存在,比如数组[2,4,3,3,3](众数为3),分割成[2,4](没有众数)和[3,3,3](众数为3)。
题解三:哈希
哈希表是一个很有用的东西,我们用map来实现,每一个键值对表示元素和元素的出现次数,最后遍历整个哈希表,出现次数最多的元素就是众数,代码如下(力扣官方题解):
class Solution {
public:
int majorityElement(vector<int>& nums) {
unordered_map<int, int> counts;
int majority = 0, cnt = 0;
//基于范围的for循环从C++11标准引入,低于该标准会报错
//这个for循环,每次num会等于nums[0]、nums[1]、nums[2]···
for (int num: nums) {
++counts[num];
if (counts[num] > cnt) {
majority = num;
cnt = counts[num];
}
}
return majority;
}
};
题解四:Boyer-Moore 投票算法
千呼万唤始出来,终于来到了最精彩、最巧妙的算法,B-M算法的证明有很多论文,大家可以自行查阅,在这里按照力扣用户ID:鹤滨写的算法思想(说的特别直观)来和大家讲。
还是我最擅长的讲故事时间,汉末三国,魏蜀吴三国并立,现在他们要攻占一座空城,为了决一死战,三国约定,每次只能一个人攻城,如果看到城里是自己人,那就和他一起守城,如果守城的不是自己人,那就发起自爆式袭击,和敌方的任意一个人同归于尽,最后这个城里剩下的是哪一方的兵,这个城就归谁。
背景交代到这里,现在我们考虑一个事实,如果最后城里剩下的是曹操的兵,说明了什么?这说明曹操的兵是最多的。这个结论可以推广到四国并立、五国并立甚至百国并立······,也就是说,最后这个城是谁占领的,那么众数就是谁。但请注意,这里的众数仅代表人数最多,不代表超过n/2,但本题题目说明了多数元素一定存在,因此这个众数就是要求的多数元素。
接下来我们来看看战役回放。
时间0:魏兵进城,占领城池。
时间1:吴兵进城,和魏兵同归于尽,但城还是魏国的(因为自爆了,没人去把城门上的旗子换掉)
时间3:吴兵进城,城是魏国的,但是城里没有守军,因此城变成吴国的,守城变成1人。
时间4:魏兵进城,守城变成0人。
时间5:魏兵进城,城变成魏国的,守城变成1个人。
攻城结束,城是魏国的,因此多数元素是魏国。
故事到此结束,我们来看代码(官方题解):
class Solution {
public:
int majorityElement(vector<int>& nums) {
int candidate = -1;//城的归属,初始没人占领
int count = 0;//城里守军人数,初始没人
for (int num : nums) {
//城是己方占领,守军人数+1
if (num == candidate)
++count;
//城不是己方占领,和对方一个兵自爆,--count
//如果城里没有人,但是城不是己方的,那么城的所有权改变,且守军人数为1
else if (--count < 0) {
candidate = num;
count = 1;
}
}
return candidate;
}
};
这个算法还是挺有意思的,可以用来获取众数。