leetcode 剑指 Offer 39. 数组中出现次数超过一半的数字 & 169. 多数元素(2020.3.13) & 面试题 17.10. 主要元素(投票算法)

【题目】剑指 Offer 39. 数组中出现次数超过一半的数字 & 169. 多数元素 & 面试题39. 数组中出现次数超过一半的数字

169. 多数元素)& 面试题39. 数组中出现次数超过一半的数字一定存在众数, 面试题 17.10. 主要元素不一定存在众数

给定一个大小为 n 的数组,找到其中的多数元素。多数元素是指在数组中出现次数大于 ⌊ n/2 ⌋ 的元素。
你可以假设数组是非空的,并且给定的数组总是存在多数元素。
示例 1:

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

示例 2:

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

【解题思路1】排序

出现次数最多的元素大于n/2次的就是众数,暴力法会超时,所以可以先排序,然后下标是n/2的元素一定是众数,n为奇数或者偶数都可以。
在这里插入图片描述

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

时间复杂度:O(nlogn)。将数组排序的时间复杂度为 O(nlogn)。
空间复杂度:O(logn)。如果使用语言自带的排序算法,需要使用 O(logn) 的栈空间。如果自己编写堆排序,则只需要使用 O(1) 的额外空间。

【解题思路2】map

class Solution {
    public int majorityElement(int[] nums) {
        Map<Integer, Integer> map = new HashMap<>();
        int n = nums.length / 2;
        for(int num : nums) {
            map.put(num, map.getOrDefault(num, 0) + 1);
            if(map.get(num) > n) {
                return num;
            }
        }
        return 0;
    }
}

时间复杂度:O(n),其中 n 是数组 nums 的长度。遍历数组 nums 一次,对于 nums 中的每一个元素,将其插入哈希表都只需要常数时间。如果在遍历时没有维护最大值,在遍历结束后还需要对哈希表进行遍历,因为哈希表中占用的空间为 O(n),因此总时间复杂度为 O(n)O(n)。
空间复杂度:O(n)。哈希表最多包含 n - ⌊n/2⌋ 个键值对,所以占用的空间为 O(n)。这是因为任意一个长度为 n 的数组最多只能包含 n 个不同的值,但题中保证 nums 一定有一个众数,会占用(最少)⌊n/2⌋ + 1 个数字。因此最多有 n - (⌊n/2⌋ + 1) 个不同的其他数字,所以最多有n - ⌊n/2⌋ 个不同的元素。

【解题思路3】分治-递归

如果元素a是整个数组的众数,那么将数组一分为二,a也必定至少是其中一部分的众数,所以可以将数组分成左右两部分,分别求出左半部分的众数 a1 以及右半部分的众数 a2,随后在 a1 和 a2 中选出正确的众数。
分治递归求解,直到所有的子问题都是长度为 1 的数组。长度为 1 的子数组中唯一的数显然是众数,直接返回即可。如果回溯后某区间的长度大于 1,我们必须将左右子区间的值合并。如果它们的众数相同,那么显然这一段区间的众数是它们相同的值。否则,我们需要比较两个众数在整个区间内出现的次数来决定该区间的众数。

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

    public int majority(int[] nums, int left, int right) {
        // 如果区间里只有一个元素,那它就是众数
        if(left == right) {
            return nums[left];
        }

        // 分别递归找出左右半边的众数
        int mid = left + (right - left) / 2;
        int leftNum = majority(nums, left, mid);
        int rightNum = majority(nums, mid + 1, right);

        // 左右半边众数一致
        if(leftNum == rightNum) {
            return leftNum;
        }

        // 左右半边众数不一致,数一下哪个多
        int leftNumCount = count(nums, leftNum, left, right);
        int rightNumCount = count(nums, rightNum, left, right);
        return leftNumCount > rightNumCount ? leftNum : rightNum;
    }

    // 两半边数组众数不一样,所以需要数一下哪边多
    public int count(int[] nums, int num, int left, int right) {
        int count = 0;
        for(int i = left; i <= right; i++) {
            if(nums[i] == num) {
                count++;
            }
        }
        return count;
    }
}

时间复杂度:O(nlogn)。函数 majority() 会求解 2 个长度为 n/2 的子问题,并做两遍长度为 n 的线性扫描。因此,分治算法的时间复杂度可以表示为:T(n) = 2T(n/2) + 2n
根据 主定理,本题满足第二种情况,所以时间复杂度可以表示为:
T ( n ) = Θ ( n l o g b a log ⁡ n ) = Θ ( n l o g 2 2 log ⁡ n ) = Θ ( n log ⁡ n ) \begin{aligned} T(n) &= \Theta(n^{log_{b}a}\log n) \\ &= \Theta(n^{log_{2}2}\log n) \\ &= \Theta(n \log n) \\ \end{aligned} T(n)=Θ(nlogbalogn)=Θ(nlog22logn)=Θ(nlogn)
空间复杂度:O(logn)。尽管分治算法没有直接分配额外的数组空间,但在递归的过程中使用了额外的栈空间。算法每次将数组从中间分成两部分,所以数组长度变为 1 之前需要进行 O(logn) 次递归,即空间复杂度为 O(logn)。

【解题思路4】Boyer-Moore 摩尔投票算法

这个思路比较有趣,但限定一定存在众数的情况
把众数记为 +1,把其他数记为 -1,将它们全部加起来,显然和大于 0,从结果本身可以看出众数比其他数多。

  1. 维护一个候选众数 candidate 和它出现的次数 count。初始时 candidate 可以为任意值,count 为 0;
  2. 遍历数组 nums 中的所有元素,对于每个元素 x,在判断 x 之前,如果 count 的值为 0,我们先将 x 的值赋予candidate,随后我们判断 x:
    (1)如果 x 与 candidate 相等,那么计数器 count 的值增加 1;
    (2)如果 x 与 candidate 不等,那么计数器 count 的值减少 1。
  3. 在遍历完成后,candidate 即为整个数组的众数。
[7, 7, 5, 7, 5, 1 | 5, 7 | 5, 5, 7, 7 | 7, 7, 7, 7]

每一步遍历时 candidate 和 count 的值:

nums:      [7, 7, 5, 7, 5, 1 | 5, 7 | 5, 5, 7, 7 | 7, 7, 7, 7]
candidate:  7  7  7  7  7  7   5  5   5  5  5  5   7  7  7  7
count:      1  2  1  2  1  0   1  0   1  2  1  0   1  2  3  4
class Solution {
    public int majorityElement(int[] nums) {
        int count = 1;
        int candidate = nums[0];
        for (int i = 1; i < nums.length; i++) {
            if (candidate == nums[i]){
                count++;
            }else {
                count--;
                if (count == 0) {  //若count为0,更换候选人
                    candidate = nums[i];
                    count++;
                }
            }
        }
        return candidate;
    }
}

时间复杂度:O(n)。Boyer-Moore 算法只对数组进行了一次遍历。
空间复杂度:O(1)。Boyer-Moore 算法只需要常数级别的额外空间。

【解题思路5】位运算

  • 由于主要元素是数组中多一半的数,那么这个主要元素的每位二进制也是数组每个元素二进制数中多一半的数
  • 统计每位数字的第i位二进制,假如第i位为1比较多,那么将ans的第i位置为1,否则为0
class Solution {
    public int majorityElement(int[] nums) {
        int ans = 0;
        int n = nums.length;
        //统计每位数字的第i位二进制
        for(int i = 0; i < 32; i++){
            int cnt = 0;
            for(int j = 0; j < n; j++){
                //如果第i位为1
                if((nums[j] >> i & 1) == 1) cnt++;
            }
            //如果所有数字的二进制数中,第i位1比0多
            if(cnt > n / 2) ans ^= (1 << i);
        }
        int C = 0;
        for(int i = 0; i < n; i++) {
            if(nums[i] == ans) C++;
        }
        if(C <= n / 2) ans = -1;
        return ans;
    }
}

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

【题目】 面试题 17.10. 主要元素

数组中占比超过一半的元素称之为主要元素。给定一个整数数组,找到它的主要元素。若没有,返回-1。

示例 1:

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

示例 2:

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

示例 3:

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

说明:
你有办法在时间复杂度为 O(N),空间复杂度为 O(1) 内完成吗?

已标记关键词 清除标记
©️2020 CSDN 皮肤主题: 编程工作室 设计师:CSDN官方博客 返回首页