1. 引言

本文将探讨在Java中查找数组主元素的多种实现方法。主元素(Majority Element)是指在数组中出现次数超过 n/2(n为数组长度)的元素。我们将分析每种方法的代码实现、时间复杂度和空间复杂度,并给出适用场景建议。

2. 问题陈述

给定一个整数数组,我们需要判断是否存在主元素。主元素的定义是:该元素在数组中的出现次数必须严格超过数组长度的一半。例如,在数组 {2, 3, 2, 4, 2, 5, 2} 中,元素 2 出现了4次,而数组长度为7,4 > 7/2,因此 2 是主元素。

本文将使用以下示例数组作为测试用例:

int[] nums = {2, 3, 2, 4, 2, 5, 2};

3. 使用for循环

3.1. 实现

这是最直观的暴力解法:通过双重循环统计每个元素的出现次数。当发现某个元素出现次数超过 n/2 时立即返回。

int majorityThreshold = nums.length / 2;
Integer majorityElement = null;

for (int i = 0; i < nums.length; i++) {
    int count = 0;
    for (int j = 0; j < nums.length; j++) {
        if (nums[i] == nums[j]) {
            count++;
        }
    }
    if (count > majorityThreshold) {
        majorityElement = nums[i];
        break;
    }
}

assertEquals(2, majorityElement);

3.2. 复杂度分析

  • ⏱️ 时间复杂度O(n²)(嵌套循环导致)
  • 💾 空间复杂度O(1)(仅使用常数空间)

优点:实现简单,无需额外空间
缺点:性能差,不适用于大数组(当 n>10⁴ 时会明显卡顿)

4. 使用排序方法

4.1. 实现

利用排序后主元素必然占据中间位置的特性:排序后检查中间元素是否满足主元素条件。

Arrays.sort(nums);
int majorityThreshold = nums.length / 2;
int count = 0;
Integer majorityElement = null;
for (int i = 0; i < nums.length; i++) {
    if (nums[i] == nums[majorityThreshold]) {
        count++;
    }

    if (count > majorityThreshold) {
        majorityElement = nums[majorityThreshold];
        break;
    }
}

assertEquals(2, majorityElement);

4.2. 复杂度分析

  • ⏱️ 时间复杂度O(n log n)(主要来自排序)
  • 💾 空间复杂度O(1)O(n)(取决于排序算法实现)

优点:代码简洁,比暴力法高效
⚠️ 注意:会修改原数组顺序,且排序开销在超大数据集上仍不可忽视

5. 使用HashMap

5.1. 实现

通过HashMap统计元素频率,线性扫描后检查频率是否超过阈值。

Map<Integer, Integer> frequencyMap = new HashMap<>();
Integer majorityElement = null;

for (int num : nums) {
    frequencyMap.put(num, frequencyMap.getOrDefault(num, 0) + 1);
}

int majorityThreshold = nums.length / 2;
for (Map.Entry<Integer, Integer> entry : frequencyMap.entrySet()) {
    if (entry.getValue() > majorityThreshold) {
        majorityElement = entry.getKey();
        break;
    }
}

assertEquals(2, majorityElement);

5.2. 复杂度分析

  • ⏱️ 时间复杂度O(n)(两次线性遍历)
  • 💾 空间复杂度O(n)(最坏情况存储所有不同元素)

优点:时间效率高,适合大数据集
缺点:额外空间开销,内存敏感场景慎用

6. 使用Boyer-Moore投票算法

6.1. 实现

这是最优解法:通过候选者计数机制,在 O(n) 时间和 O(1) 空间内完成。

int majorityThreshold = nums.length / 2;
int candidate = nums[0];
int count = 1;
int majorityElement = -1;

for (int i = 1; i < nums.length; i++) {
    if (count == 0) {
        candidate = nums[i];
        count = 1;
    } else if (candidate == nums[i]) {
        count++;
    } else {
        count--;
    }
}

count = 0;
for (int num : nums) {
    if (num == candidate) {
        count++;
    }
}

majorityElement = count > majorityThreshold ? candidate : -1;
assertEquals(2, majorityElement);

算法执行过程分解

初始状态: [候选者(2), 计数(1)]
迭代1: [候选者(2), 计数(0), 元素(3)] // 3≠候选者,计数--
迭代2: [候选者(2), 计数(1), 元素(2)] // 2=候选者,计数++
迭代3: [候选者(2), 计数(0), 元素(4)] // 4≠候选者,计数--
迭代4: [候选者(2), 计数(1), 元素(2)] // 2=候选者,计数++
迭代5: [候选者(2), 计数(0), 元素(5)] // 5≠候选者,计数--
迭代6: [候选者(2), 计数(1), 元素(2)] // 2=候选者,计数++

6.2. 复杂度分析

  • ⏱️ 时间复杂度O(n)(两次线性遍历)
  • 💾 空间复杂度O(1)(仅用几个变量)

优点:时间空间双最优,适合超大数据集
⚠️ 注意:需要二次验证(因为算法只保证"可能的主元素")

7. 方法对比总结

方法 时间复杂度 空间复杂度 优缺点
for循环 O(n²) O(1) ✅ 实现简单
❌ 性能差,仅适合小数据集
排序 O(n log n) O(1)~O(n) ✅ 代码简洁
❌ 修改原数组,排序开销大
HashMap O(n) O(n) ✅ 时间效率高
❌ 空间开销大
Boyer-Moore算法 O(n) O(1) ✅ 时间空间双最优
⚠️ 需要二次验证,理解门槛稍高

8. 结论

本文分析了四种查找数组主元素的实现方案:

  1. 暴力法:仅适用于教学或极小数据集,实际开发中应避免
  2. 排序法:在允许修改原数组且数据量中等时可用
  3. HashMap法:内存充足时的首选方案,代码清晰易维护
  4. Boyer-Moore算法生产环境最优解,尤其适合内存敏感或超大数据场景

💡 开发建议:优先使用Boyer-Moore算法,除非有特殊约束(如不能修改原数组)。对于面试场景,掌握该算法能体现对经典算法的理解深度。

本文所有示例代码可在GitHub仓库中获取。


原始标题:Finding the Majority Element of an Array in Java