阿翰 剑指offer 之 Day 23 数学(简单

博客详细介绍了如何寻找数组中出现次数超过一半的数字,包括排序、哈希表、随机化、分治法和摩尔投票法五种解法,并对每种方法的时间和空间复杂度进行了分析。此外,还讲解了构建乘积数组的算法,通过实例代码展示了实现过程。
摘要由CSDN通过智能技术生成

目录

数学

1 数组中出现次数超过一半的数字

1. 数组排序

1.1 函数排序

1.2 快排 

​2. 哈希表统计次数

3. 随机化

3.2 简化代码

4. 分治法

5. 摩尔(Boyer-Moore)投票法

 2 构建乘积数组


数学

1 数组中出现次数超过一半的数字

剑指 Offer 39. 数组中出现次数超过一半的数字https://leetcode-cn.com/problems/shu-zu-zhong-chu-xian-ci-shu-chao-guo-yi-ban-de-shu-zi-lcof/

本题五种解法:

排序、hashmap统计次数、随机化分治法以及摩尔投票法: 核心理念为 票数正负抵消 。此方法时间和空间复杂度分别为 O(N) 和 O(1) ,为本题的最佳解法。

1. 数组排序

 数组排序法: 将数组 nums 排序,数组中点的元素 一定为众数。

1.1 函数排序

public int majorityElement(int[] nums) { 
        return Arrays.stream(nums).sorted().toArray()[nums.length / 2];
    }


//或者

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

Arrays.sort比利用stream流再sorted,然后再toArray()更快 

复杂度分析

  • 时间复杂度:O(nlogn)。将数组排序的时间复杂度为 O(nlogn)。

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

1.2 快排 

用了一下快排结果。。。。 

class Solution {
    public int majorityElement(int[] nums) {
        qiuckSort(nums,0, nums.length-1);
        return nums[nums.length / 2];
    }
    public void qiuckSort(int[] nums, int low, int high){
        if(low > high) return;
        int l = low,r = high;
        int temp = nums[l];
        while(l<r){
            while (l<r && temp <= nums[r]) r--;
            nums[l] = nums[r];
            while (l<r && temp >= nums[l]) l++;
            nums[r] = nums[l];
        }
        nums[l] = temp;
        qiuckSort(nums, low, l-1);
        qiuckSort(nums, l+1, high);
    }
}

2. 哈希表统计次数

遍历数组 nums ,用 HashMap 统计各数字的数量,即可找出 众数 。此方法时间和空间复杂度均为 O(N)。

3. 随机化

思路

因为超过 一半以上 的数组下标被众数占据了,这样随机挑选一个下标对应的元素并验证,有很大的概率能找到众数。

算法

由于一个给定的下标对应的数字很有可能是众数,随机挑选一个下标,检查它是否是众数,如果是就返回,否则继续随机挑选。

class Solution {
    private int randRange(Random rand, int min, int max) {
        return rand.nextInt(max - min) + min;
    }

    private int countOccurences(int[] nums, int num) {
        int count = 0;
        for (int i = 0; i < nums.length; i++) {
            if (nums[i] == num) {
                count++;
            }
        }
        return count;
    }

    public int majorityElement(int[] nums) {
        Random rand = new Random();

        int majorityCount = nums.length / 2;

        while (true) {
            int candidate = nums[randRange(rand, 0, nums.length)];
            if (countOccurences(nums, candidate) > majorityCount) {
                return candidate;
            }
        }
    }
} 

3.2 简化代码

    public int majorityElement(int[] nums) {
        while(true){
            int candidate = nums[new Random().nextInt(nums.length)];
            int count = 0;
            for (int i = 0; i < nums.length; i++) {
                if(nums[i] == candidate){
                    count++;
                }
            }
            if(count > nums.length / 2){
                return candidate;
            }
        }
    }

官方题解的写的为了易读性,较为繁琐,改完舒服多了~

4. 分治法

如果数 a 是数组 nums 的众数,如果将 nums 分成两部分,那么 a 必定是至少一部分的众数。

使用反证法来证明这个结论。假设 a 既不是左半部分的众数,也不是右半部分的众数,那么 a 出现的次数少于 l / 2 + r / 2 次,其中 l 和 r 分别是左半部分和右半部分的长度。由于 l / 2 + r / 2 <= (l + r) / 2,说明 a 也不是数组 nums 的众数,因此出现了矛盾。所以这个结论是正确的。

这样以来,就可以使用分治法解决这个问题:将数组分成左右两部分,分别求出左半部分的众数 a1 以及右半部分的众数 a2,随后在 a1 和 a2 中选出正确的众数。

算法

使用经典的分治算法递归求解,直到所有的子问题都是长度为 1 的数组。长度为 1 的子数组中唯一的数显然是众数,直接返回即可。如果回溯后某区间的长度大于 1,必须将左右子区间的值合并

  • 如果它们的众数相同,那么显然这一段区间的众数是它们相同的值。
  • 否则,需要比较两个众数在整个区间内出现的次数来决定该区间的众数。
class Solution {
    private int countInRange(int[] nums, int num, int low, int high) {
        int count = 0;
        for (int i = low; i <= high; i++) {
            if (nums[i] == num) {
                count++;
            }
        }
        return count;
    }

    private int majorityElementRec(int[] nums, int low, int high) {
        if (low == high) {
            return nums[low];
        }
        int mid = (high - low) / 2 + low;
        int left = majorityElementRec(nums, low, mid);
        int right = majorityElementRec(nums, mid + 1, high);

        if (left == right) {
            return left;
        }

        int leftCount = countInRange(nums, left, low, high);
        int rightCount = countInRange(nums, right, low, high);

        return leftCount > rightCount ? left : right;
    }

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

 

5. 摩尔(Boyer-Moore)投票

class Solution {
    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;
    }
} 

复杂度分析

时间复杂度:O(n)O(n)。Boyer-Moore 算法只对数组进行了一次遍历。

空间复杂度:O(1)O(1)。Boyer-Moore 算法只需要常数级别的额外空间

拓展: 由于题目说明 给定的数组总是存在多数元素 ,因此本题不用考虑 数组不存在众数 的情况。若考虑,需要加入一个 “验证环节” ,遍历数组 nums 统计 x 的数量。

若 x 的数量超过数组长度一半,则返回 x ;
否则,返回未找到众数;时间和空间复杂度不变,仍为 O(N) 和 O(1) 。

class Solution {
    public int majorityElement(int[] nums) {
        int x = 0, votes = 0, count = 0;
        for(int num : nums){
            if(votes == 0) x = num;
            votes += num == x ? 1 : -1;
        }
        // 验证 x 是否为众数
        for(int num : nums)
            if(num == x) count++;
        return count > nums.length / 2 ? x : 0; // 当无众数时返回 0
    }
}

 2 构建乘积数组

剑指 Offer 66. 构建乘积数组https://leetcode-cn.com/problems/gou-jian-cheng-ji-shu-zu-lcof/

class Solution {
    public int[] constructArr(int[] a) {
        int len = a.length;
        if(len == 0) return new int[0];
        int[] b = new int[a.length];
        b[0] = 1;
        int t = 1 ;
        for(int i = 1; i < len; i++) {
            b[i] = b[i - 1] * a[i - 1];
        }
//        下三角
//        System.out.println(Arrays.toString(b));
        for (int i = a.length - 1; i > 0; i--) {
            t *= a[i];
            b[i-1] *= t;
        }
        return b;
    }
}

 

我也想说我是个傻逼,做题都不知道拆解一下!

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值