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

12 篇文章 0 订阅
9 篇文章 0 订阅

【题目】剑指 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);
        int count = 0;
        int more = nums[nums.length/2];
        for (int num : nums) {
            count += num == more ? 1 : 0;
        }
        return count > nums.length / 2 ? more : -1;
    }
}

时间复杂度: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) {
        int more =  majority(nums, 0, nums.length - 1);
        int count = 0;
        for (int num : nums) {
            count += num == more ? 1 : 0;
        }
        return count > nums.length / 2 ? more : -1;
    }

    public int majority(int[] nums, int left, int right) {
        if (left == right) {
            return nums[left];
        }

        int mid = left + (right - left) / 2;
        int leftMore = majority(nums, left, mid);
        int rightMore = majority(nums, mid + 1, right);

        if (leftMore == rightMore) {
            return leftMore;
        }

        int leftCount = count(nums, left, mid, leftMore);
        int rightCount = count(nums, mid + 1, right, rightMore);
        return leftCount > rightCount ? leftMore : rightMore;
    }

    public int count(int[] nums, int left, int right, int more) {
        int count = 0;
        for (int i = left; i <= right; i++) {
            if (nums[i] == more) {
                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 candidate = 0;
        int count = 0;
        for (int num : nums) {
            if (count == 0) {
                candidate = num;
            }
            if (num == candidate) {
                count++;
            } else {
                count--;
            }
        }
        count = 0;
        int n = nums.length;
        for (int num : nums) {
            if (num == candidate) {
                count++;
            }
        }
        return count > n / 2 ? candidate : -1;
    }
}

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

【解题思路5】位运算

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

时间复杂度: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) 内完成吗?

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值