「LeetCode」169.多数元素

197 阅读5分钟

「这是我参与2022首次更文挑战的第17天,活动详情查看:2022首次更文挑战」。

题目描述🌍

给定一个大小为 n 的数组,找到其中的多数元素。多数元素是指在数组中出现次数 大于 ⌊ n/2 ⌋ 的元素。

你可以假设数组是非空的,并且给定的数组总是存在多数元素

示例 1

输入:[3,2,3]
输出:3

示例 2

输入:[2,2,1,1,1,2,2]
输出:2

进阶:

  • 尝试设计时间复杂度为 O(n)O(n)、空间复杂度为 O(1)O(1) 的算法解决此问题。

计数法🎮

解题思路

统计数组中每一个元素出现的次数,从中找出次数 > ⌊ n/2 ⌋ 的元素。

代码

Java

class Solution {
    public int majorityElement(int[] nums) {
        Map<Integer, Long> map = Arrays.stream(nums).boxed().collect(Collectors.groupingBy(Function.identity(), Collectors.counting()));
        int count = nums.length >> 1;
        // Map 无法使用增强for循环; Map.Entry<>为Map提供一种遍历方式
        for (Map.Entry<Integer, Long> entry : map.entrySet()) {
            if (entry.getValue() > count)
                return entry.getKey();
        }
        return -1;
    }
}

以上统计个数调用 IntStream 库函数,如下是不调库的写法:

class Solution {
    public int majorityElement(int[] nums) {
        // count every element
        Map<Integer, Integer> map = new HashMap<>();
        for (int num : nums) {
            // Integer default-value: null(not 0)
            if (!map.containsKey(num)) {
                map.put(num, 1);
            } else {
                map.put(num, map.get(num) + 1);
            }
        }
        // find the majorityNum [which > n/2]
        int half = nums.length / 2;
        for (Map.Entry<Integer, Integer> entry : map.entrySet()) {
            if (entry.getValue() > half)
                return entry.getKey();
        }
        // unreachable: because the majority is existent absolutely
        return -1;
    }
}

C++

class Solution {
public:
    int majorityElement(vector<int> &nums) {
        unordered_map<int, int> counts;
        int majority = 0;
        int mediant = 0;
        for (int num: nums) {
            ++counts[num];
            // 边计数边维护临时的多数元素, 这样可避免最后再遍历一次该哈希映射
            // 当且仅当一定存在多数元素时才有效
            if (counts[num] > mediant) {
                majority = num;
                mediant = counts[num];
            }
        }
        return majority;
    }
};

时间复杂度:O(n)O(n)

空间复杂度:O(n)O(n)

排序取中间元素🔮

解题思路

排序后相同元素一定是相邻的,所以数组排序后中间的元素一定是“多数元素”。

代码

Java

class Solution {
    public int majorityElement(int[] nums) {
        Arrays.sort(nums);
        return nums[nums.length >> 1];
    }
}

C++

class Solution {
public:
    int majorityElement(vector<int> &nums) {
        sort(nums.begin(), nums.end());
        return nums[nums.size() / 2];
    }
};

时间复杂度:O(nlogn)O(n·logn)

空间复杂度:O(log n)O(log\space n)

对拼消耗法🚀

解题思路

直接从头到尾遍历一次数组,不同元素逐个相消,相同元素个数 +1,消除到最后一定会剩余 1\ge1 个相同的元素,即多数元素。

⭐注:题目明确说到给定的数组总是存在多数元素,否则最后获取的数需要校验是否大于 ⌊ n / 2 ⌋

代码

Java

class Solution {
    public int majorityElement(int[] nums) {
        int count = 1;
        int num = nums[0];
        int length = nums.length;
        for (int i = 1; i < length; i++) {
            if (nums[i] == num) {
                count++;
            } else {
                count--;
                if (count < 1) {
                    num = nums[i + 1];
                }
            }
        }
        return num;
    }
}

C++

class Solution {
public:
    int majorityElement(vector<int> &nums) {
        int count = 1;
        int num = nums[0];
        int length = nums.size();
        for (int i = 1; i < length; ++i) {
            if (nums[i] == num) {
                count++;
            } else {
                count--;
                if (count < 1) {
                    num = nums[i + 1];
                }
            }
        }
        return num;
    }
};

时间复杂度:O(n)O(n)

空间复杂度:O(1)O(1)

随机化🎴

解题思路

因为数组中超过 n2⌊\frac{n}{2}⌋ 的数都是『同一个』多数元素,所以在数组中随机取值,验证后很大概率就是多数元素。当然最坏情况下,就是一直找不到多数元素,然而平均情况下的时间是线性的。

代码

Java

class Solution {
    // 验证随机数是否为多数元素
    public boolean verifyRandomNumber(int randomNum, int[] nums) {
        int count = 0;
        int length = nums.length;
        for (int i = 0; i < length; i++) {
            if (randomNum == nums[i])
                count++;
        }
        return count > length / 2;
    }

    // 生成给定区间内的随机数
    public int generateByRange(int min, int max, int[] nums) {
        Random random = new Random();
        // defaultRandomRange: [0,value)
        return random.nextInt(max - min) + min;
    }
	
    // 随机化
    public int majorityElement(int[] nums) {
        while (true) {
            int randomNum = nums[generateByRange(0, nums.length, nums)];
            if (verifyRandomNumber(randomNum, nums))
                return randomNum;
        }
    }
}

C++

class Solution {
public:
    int majorityElement(vector<int> &nums) {
        while (true) {
            int candidate = nums[rand() % nums.size()];
            int count = 0;
            for (int num: nums)
                if (candidate == num)
                    count++;
            if (count > nums.size() / 2)
                return candidate;
        }
    }
};

时间复杂度:O(n)O(n),验证元素的时间。

空间复杂度:O(1)O(1)

分治法🥏

解题思路

如果 x 是数组的多数元素,一旦将数组划分为两个区间,x 至少是其中一个区间的众数。这样就可以使用分治法加以解决;分别从左/右半区间选出众数 x1x2,其中在整个区间中个数较多的元素即为多数元素。

⭐代码解释:若左右两个区间的“众数”值一致,则合并区间后的“多数元素”就是该数;若值不一致,则分别计算 leftElerightEle 这两个值在区间合并后的个数,返回个数较多的那个【个数一致则无所谓哪个】,显然该数就是合并区间的多数元素

代码

Java

class Solution {
    public int majorityElement(int[] nums) {
        return getMajorityEle(nums, 0, nums.length - 1);
    }

    // 分治法求多数元素
    public int getMajorityEle(int[] nums, int low, int high) {
        if (low == high) {
            return nums[low];
        }

        int middle = (low + high) / 2;
        int leftEle = getMajorityEle(nums, low, middle);
        int rightEle = getMajorityEle(nums, middle + 1, high);

        // 若左右区间的"众数"相同, 则直接返回
        if (leftEle == rightEle) {
            return leftEle;
        }

        // 左右区间的"众数"不同, 那么就返回合并区间后二者中个数较多的那个
        int leftCount = countInRange(nums, leftEle, low, high);
        int rightCount = countInRange(nums, rightEle, low, high);
        return leftCount > rightCount ? leftEle : rightEle;
    }

    // 统计指定范围内 target 的个数
    public int countInRange(int[] nums, int target, int low, int high) {
        int count = 0;
        for (int i = low; i <= high; i++) {
            if (nums[i] == target)
                count++;
        }
        return count;
    }
}

C++

class Solution {
public:
    int majorityElement(vector<int> &nums) {
        return getModalNumber(nums, 0, nums.size() - 1);
    }

    int getModalNumber(vector<int> &nums, int low, int high) {
        if (low == high)
            return nums[low];

        int middle = (low + high) / 2;
        int left_ele = getModalNumber(nums, low, middle);
        int right_ele = getModalNumber(nums, middle + 1, high);

        if (left_ele == right_ele) {
            return left_ele;
        }

        int left_count = getCount_in_range(nums, left_ele, low, high);
        int right_count = getCount_in_range(nums, right_ele, low, high);
        return left_count > right_count ? left_ele : right_ele;
    }

    int getCount_in_range(vector<int> &nums, int target, int low, int high) {
        int count = 0;
        for (int i = low; i <= high; ++i) {
            if (nums[i] == target)
                ++count;
        }
        return count;
    }
};

🎈分治算法时间复杂度求解公式:T(n)=2T(n2)+2nT(n)=2T(\frac{n}{2})+2n

时间复杂度:O(nlogn)O(n·logn)

空间复杂度:O(log n)O(log\space n),递归使用额外栈空间。

摩尔投票算法🍤

解题思路

如果将多数元素替换为 1,而非其余元素替换为 -1,那么将它们全部加起来一定会大于 0

借鉴上述思想,但我们事先不知道多数元素是哪个,所以我们让数组中任意不相同的两个元素相互抵消(也会存在非多数元素非多数元素抵消的情况),最后剩下的那些 (个) 元素一定是相同。至于候选元素是否为多数元素仍需验证(是否 > n/2),但题目明确说到给定数组一定存在多数元素,所以无需考虑校验问题。

为什么说不一定,举个反例:[1, 1, 1, 2, 2, 2, 3],返回 3,错误。

其实认真一看,「摩尔投票算法」的思想不就是方法三「对拼消耗法」吗?!

写成 2 种方法是因为我也是今天才知道摩尔投票算法这个新玩意呢!方法三是老师曾经教过的方法,自己稍微总结下就贴出来了。

代码

Java

class Solution {
    // Boyer-Moore 算法
    public int majorityElement(int[] nums) {
        int count = 0;
        Integer candidate = null;

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

        return candidate;
    }
}

C++

class Solution {
public:
    int majorityElement(vector<int> &nums) {
        int count = 0;
        int candidate;
        for (int num: nums) {
            if (count == 0) {
                candidate = num;
            }
            count += (num == candidate) ? 1 : -1;
        }
        return candidate;
    }
};

时间复杂度:O(n)O(n)

空间复杂度:O(1)O(1)

牛刀小试

⭐理解了摩尔投票算法后,不妨试试这道进阶题:229. 求众数 Ⅱ

最后🌅

该篇文章为 「LeetCode」 系列的 No.16 篇,在这个系列文章中:

  • 尽量给出多种解题思路
  • 提供题解的多语言代码实现
  • 记录该题涉及的知识点

👨‍💻争取做到 LeetCode 每日 1 题,所有的题解/代码均收录于 「LeetCode」 仓库,欢迎随时加入我们的刷题小分队!