LeetCode 169题要求找出数组中出现次数超过⌊n/2⌋的元素(n为数组长度)。这个元素被称为多数元素(Majority Element)。举个例子,对于数组[3,2,3],长度为3,⌊3/2⌋=1,所以出现次数超过1次的元素就是3。
这个问题的难点在于如何在不同的约束条件下找到最优解。我们先从最直观的解法开始,逐步优化到最佳方案。在实际面试中,面试官通常会期待候选人能够展示这种从基础到优化的思考过程。
最直接的思路是遍历数组,对每个元素统计它在数组中出现的次数。如果某个元素的计数超过⌊n/2⌋,就返回该元素。
python复制def majorityElement(nums):
majority_count = len(nums)//2
for num in nums:
count = sum(1 for elem in nums if elem == num)
if count > majority_count:
return num
这个解法的时间复杂度是O(n²),因为对于每个元素(n个),都要遍历整个数组(n次)进行计数。空间复杂度是O(1),因为没有使用额外的存储空间。
注意:虽然这个解法能够通过LeetCode的测试用例,但在实际应用中,当数组很大时,这种解法会非常低效。它主要作为理解问题的起点。
为了优化时间复杂度,我们可以使用哈希表(字典)来记录每个元素出现的次数。这样只需要遍历数组两次:第一次统计每个元素的出现次数,第二次检查哪个元素的计数超过了⌊n/2⌋。
python复制def majorityElement(nums):
counts = {}
for num in nums:
counts[num] = counts.get(num, 0) + 1
majority_count = len(nums) // 2
for num, count in counts.items():
if count > majority_count:
return num
这个解法的时间复杂度是O(n),因为我们只需要遍历数组两次。空间复杂度是O(n),因为最坏情况下需要存储所有不同的元素及其计数。
在实际应用中,这种解法已经比暴力解法高效很多,特别是当数组很大时。但是否可以进一步优化空间复杂度呢?
我们可以优化为只遍历一次数组,在遍历过程中就检查是否有元素的计数已经超过了阈值:
python复制def majorityElement(nums):
counts = {}
majority_count = len(nums) // 2
for num in nums:
counts[num] = counts.get(num, 0) + 1
if counts[num] > majority_count:
return num
这种优化虽然减少了遍历次数,但时间复杂度和空间复杂度的大O表示法并没有变化,只是实际运行时间可能略有改善。
由于多数元素出现的次数超过⌊n/2⌋,所以排序后数组中间位置的元素一定是多数元素。例如:
python复制def majorityElement(nums):
nums.sort()
return nums[len(nums)//2]
这个解法的时间复杂度取决于排序算法,通常是O(n log n)。空间复杂度取决于排序的实现:
虽然这个解法的时间复杂度比哈希表解法差,但它的代码极其简洁,在实际应用中,当n不是特别大时,这种解法可能更受欢迎。
提示:在Python中,sorted()函数使用的是Timsort算法,空间复杂度是O(n)。而list.sort()方法是原地排序,空间复杂度是O(1)。
Boyer-Moore投票算法是解决多数元素问题的最优解法,它可以在O(n)时间和O(1)空间内解决问题。算法的核心思想是通过消除不同的元素对来找到多数元素。
算法步骤:
python复制def majorityElement(nums):
count = 0
candidate = None
for num in nums:
if count == 0:
candidate = num
count += (1 if num == candidate else -1)
return candidate
为什么这个算法有效?因为多数元素的数量超过其他所有元素数量的总和。在最坏情况下,多数元素会与其他所有元素一一抵消,但最终仍然会有剩余的多数元素。
考虑数组[2,2,1,1,1,2,2]:
这个算法的优势在于:
在实际面试中,如果能直接给出这个解法并解释清楚原理,会给面试官留下很好的印象。
| 解法 | 时间复杂度 | 空间复杂度 | 适用场景 |
|---|---|---|---|
| 暴力解法 | O(n²) | O(1) | 仅用于理解问题,实际不推荐 |
| 哈希表 | O(n) | O(n) | 通用解法,容易理解和实现 |
| 排序 | O(n log n) | O(1)或O(n) | 代码简洁,适合中等规模数据 |
| Boyer-Moore | O(n) | O(1) | 最优解,适合大规模数据 |
无论使用哪种解法,都需要考虑一些边界条件:
在实际工程中,如果没有题目保证,我们需要添加额外的检查逻辑。
这是一个类似的变种问题,可以使用改进的Boyer-Moore算法解决。我们需要维护两个候选元素和计数器。
python复制def majorityElement(nums):
if not nums:
return []
# 初始化两个候选和计数器
cand1, cand2, count1, count2 = None, None, 0, 0
# 第一遍找出两个候选
for num in nums:
if num == cand1:
count1 += 1
elif num == cand2:
count2 += 1
elif count1 == 0:
cand1, count1 = num, 1
elif count2 == 0:
cand2, count2 = num, 1
else:
count1 -= 1
count2 -= 1
# 第二遍验证这两个候选是否真的满足条件
result = []
for cand in [cand1, cand2]:
if nums.count(cand) > len(nums)//3:
result.append(cand)
return result
在大数据场景下,数组可能分布在多台机器上。我们可以使用MapReduce框架:
这种方法结合了Boyer-Moore算法的思想,适合处理超大规模数据。
对于无法全部存储在内存中的流式数据,Boyer-Moore算法特别适用,因为它只需要O(1)的空间,可以实时维护候选元素和计数器。
多数元素算法在实际中有广泛的应用:
理解这些算法不仅能帮助解决LeetCode问题,更能为实际工程问题提供解决方案。我在处理大规模日志分析时,就曾使用Boyer-Moore算法的变种来快速识别高频错误模式,显著提高了问题排查效率。