解法一:排序法
设数组的长度为 n,众数为 x,且众数 x 出现的次数 count(x) > n/2
排序操作会将数组中的元素按照从小到大(或从大到小)的顺序排列。但无论元素如何排列,元素的数量并不会改变,也就是说众数的出现次数依旧大于数组长度的一半。
对于一个长度为 n 的数组,其索引范围是从 0 到 n - 1。当对数组进行排序后,中间位置的索引计算方式如下:
- 若
n为奇数,中间位置的索引是n / 2。例如,当n = 5时,中间位置的索引是5 / 2 = 2(索引从 0 开始)。 - 若
n为偶数,通常我们说的中间位置可以取n / 2 - 1或者n / 2,不过在这种情况下,由于众数出现的次数大于n / 2,不管取哪个位置,结果都是众数。例如,当n = 6时,n / 2 = 3,n / 2 - 1 = 2(索引从 0 开始)。
由于众数 x 出现的次数大于 n / 2,那么在排序后的数组中,无论众数 x 是大值还是小值,它都会在数组中占据超过一半的连续位置。这就保证了数组的中间位置一定被众数 x 占据。
举例说明
- 奇数长度数组
假设有数组 [1, 2, 2, 2, 3],数组长度 n = 5,众数是 2,出现了 3 次,大于 5 / 2 = 2。对数组排序后还是 [1, 2, 2, 2, 3],中间位置的索引是 5 / 2 = 2,对应的值是 2,也就是众数。
- 偶数长度数组
假设有数组 [1, 2, 2, 2, 2, 3],数组长度 n = 6,众数是 2,出现了 4 次,大于 6 / 2 = 3。对数组排序后还是 [1, 2, 2, 2, 2, 3],中间位置的索引可以取 6 / 2 - 1 = 2 或者 6 / 2 = 3,对应的值都是 2,即众数。
func majorityElement(nums []int) int {
sort.Slice(nums, func(i, j int) bool {
return nums[i] < nums[j]
})
return nums[len(nums)/2]
}
时间复杂度:O(nlogn)。将数组排序的时间复杂度为O(nlogn)。
空间复杂度:O(logn)。如果使用语言自带的排序算法,需要使用O(logn)的栈空间。如果自己编写堆排序,则只需要使用O(1)的额外空间。
解法二:摩尔投票法
题目下面说了进阶版,尝试设计时间复杂度为 O(n)、空间复杂度为 O(1) 的算法解决此问题。
设数组的长度为 n,众数为 x,且众数 x 出现的次数 > n/2
- 推论一:若记众数的票数为+1,非众数的票数为−1,则一定有所有数字的票数和>0
- 推论二:若数组的前a个数字的票数和=0,则 数组剩余(n−a)个数字的票数和一定仍>0,即后(n−a)个数字的众数仍为x。
根据以上推论,遍历并统计票数。当发生票数和=0时,剩余数组的众数一定不变,每次发生票数和=0都可以缩小剩余数组区间。当遍历完成时,最后一轮假设的数字即为众数。
核心理念为票数正负抵消
func majorityElement(nums []int) int {
res := 0
votes := 0
for _, v := range nums{
if votes == 0 { // 假设当前元素为候选众数
res = v
}
if res == v { // 当前元素等于候选众数,投票数加 1,否则减 1
votes++
} else {
votes--
}
}
return res
}