算法通过村第三关-数组黄金笔记|数组难解

文章目录

  • 前言
    • 数组中出现超过一半的数字
    • 数组中只出现一次的数字
    • 颜色的分类问题(荷兰国旗问题)
      • 基于冒泡排序的双指针(快慢指针)
      • 基于快排的双指针(对撞指针)
  • 总结


前言


提示:苦不来自外在环境中的人、事、物,只是自内的妄想和执着。 --金惟纯

这里整理一些比较经典的题目,巩固一下数组的学习。

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

题目介绍参考:剑指 Offer 39. 数组中出现次数超过一半的数字 - 力扣(LeetCode)
算法通过村第三关-数组黄金笔记|数组难解_第1张图片
对于这种题目,如果没有思路的话,我们可以先把常见的数据结构和算法策略过一般,这里参考以前的文章巩固一下。算法通关村第一关-链表白银挑战笔记|公共子节点_师晓峰的博客-CSDN博客
算法通过村第三关-数组黄金笔记|数组难解_第2张图片
我们想一下,查找统计出所有出现的个数,大于一半就可以,这不就是一种想法,排序行不行呢,对数组进行排序,超过一半必定是中位数。

如果不了解中位数这个概念的:
聪明回答:

在数学中,中位数指的是一组数字排序后的中间值,即将一组数字从小到大排列,中间的那个数就是中位数。如果一组数字有奇数个,那么中位数就是排序后的中间数;如果一组数字有偶数个,那么中位数是中间两个数的平均值。中位数可以用来表示一组数据的中心趋势,较为稳定而不易受极端值的影响。

但是如果你不放心的话可以在遍历一遍数组,确定这个数组是否超过一半,所以第二种方法就出来了。这种方法的时间复杂度取决于排序算法的时间复杂度,最快的话O(nlogn),排序的代价比较高,我们试想一下还有别的方法吗

我们使用Hash行不行呢?我们先创建一个HashMap的key是元素的值,value是已经出现的次数,然后遍历数组来统计所有元素出现的次数,最后在遍历一边Hash,找到次数超过一半的数字,这不又一种方法出来了。

我们展示一下代码:

	/**
     * 方法1 基于Hash
     * @param array
     * @return
     */
    public static int moreThanHalfNum(int[] array) {
        // 参数校验
        if (array == null || array.length == 0) {
            return 0;
        }
        HashMap<Integer, Integer> res = new HashMap<>();
        int len = array.length;
        for (int i = 0; i < len; i++) {
            res.put(array[i], res.getOrDefault(array[i], 0) + 1);
            if (res.get(array[i]) > len / 2){
                return array[i];
            }
        }
        return 0;
    }

当然采用Hash的方法可以解决,但是这里最多给70,着并不是最优的结果,那么有没有巧妙的方法呢?

拓展:采用上面你的算法时间复杂度为O(n),但是这是用空间复杂度O(n)换来的,那么有没有空间复杂度为O(1)且时间负责度为O(n)的呢?

你听说过摩尔投票法则吗?它用来解决多数问题(中位数)可以说是一把利刃。

聪明的回答:

摩尔投票法(Moore’s Voting Algorithm)是一种用于在数组或列表中查找出现次数超过一半的元素的算法。该算法基于以下观察:如果一个元素出现次数超过一半,那么它在数列中出现的次数一定比其他所有元素出现次数之和还要多。

算法步骤如下:

  1. 初始化两个变量:候选元素(candidate)和计数器(count),候选元素用于保存当前被选中的元素,计数器用于统计候选元素的出现次数。
  2. 遍历整个数组或列表,对于每一个元素:
    • 如果计数器为0,将当前元素设为候选元素,将计数器设为1。
    • 否则,如果当前元素与候选元素相同,计数器加1;否则,计数器减1。
  3. 完成遍历后,最后留下的候选元素就是候选元素,但这并不代表它一定是超过一半的元素,只是候选元素的可能性更高。
  4. 最后,再次遍历整个数组或列表,统计候选元素的出现次数。如果它的出现次数确实超过一半,那么它就是超过一半的元素;否则,不存在超过一半的元素。

下面给出一个例子来解释摩尔投票法:
假设我们有一个数组:[2, 4, 5, 2, 2]。

  • 遍历第一个元素2,将其设为候选元素,计数器为1。
  • 遍历第二个元素4,与候选元素不同,计数器减1。
  • 遍历第三个元素5,与候选元素不同,计数器减1。
  • 遍历第四个元素2,与候选元素相同,计数器加1。
  • 遍历第五个元素2,与候选元素相同,计数器加1。
    此时,计数器为2,最后剩下的候选元素是2。
    再次遍历整个数组,统计元素2的出现次数,发现它出现了3次,大于数组长度的一半,所以2就是超过一半的元素。
	/**
     * 方法二:比较特殊的计数法
     *
     * @param array
     * @return
     */
    public static int moreThanHalfNum2(int[] array) {
        if (array == null || array.length == 0) {
            return 0;
        }
        int count = 0;
        Integer candidate = null;
        for (int num : array) {
            if (count == 0) {
                candidate = num;
            }
            count += (num == candidate) ? 1 : -1;
        }
        // check 记得在检查一边
        count = 0;
        int len = array.length;
        for (int num : array) {
            if (num == candidate) {
                count++;
            }
            if (count >= len / 2) {
                return candidate;
            }
        }
        return candidate;
    }

Q&A

Q : 这里问什么要在检查一边,可以不检查?会出现什么问题?

A :必须再检查一边,这里是确保candidate一定是超出一半的数,不检查投出的结果不一定正确[1,2,3],有结果,但是不符合要求。

数组中只出现一次的数字

参考题目介绍:136. 只出现一次的数字 - 力扣(LeetCode)
算法通过村第三关-数组黄金笔记|数组难解_第3张图片
算法通过村第三关-数组黄金笔记|数组难解_第4张图片
这个题用Set集合解决比较好,Set集合不存储重复元素,这个是该集合的特性。题目说明其他元素都是出现两次,我们刚好可以利用这个操作,当要添加元素的key与集合中已存在的数出现重复的时候,我们就不进行操作,并且将这个key一起删掉,确保只存在一个数,这样遍历一边就可以知道答案了。【注意:确保集合有元素 】

	/**
     * 基于集合寻找
     *
     * @param arr
     * @return
     */
    public static Integer findOneNum(int[] arr) {
        // 校验参数
        if (arr == null || arr.length == 0){
            return null;
        }
        // 特殊处理
        if (arr.length == 1) {
            return arr[0];
        }
        HashSet<Integer> res = new HashSet<>();
        for(int i = 0; i < arr.length; i++){
            if (!res.add(arr[i])){
                res.remove(arr[i]);
            }
        }
        // check 确保set集合存在元素
        if (res.size() == 0){
            return null;
        }
        return (Integer) res.toArray()[0];
    }

当然这个方法也不是最优解,算法就是这么奇妙,有时后令人讨厌,有时候让你欣喜。提示一下,可以想一想位运算来解决:你知道异或这个操作吗?

聪明回答:

计算机中的异或操作(XOR),也称为“排他性或”操作,是一种逻辑运算,用于比较两个值的不同之处。异或操作有以下几条规则:

  1. 同一个值与自身进行异或操作结果为0:A ⊕ A = 0
  2. 任意值与0进行异或操作结果不变:A ⊕ 0 = A
  3. 异或操作满足交换律:A ⊕ B = B ⊕ A
  4. 异或操作满足结合律:(A ⊕ B) ⊕ C = A ⊕ (B ⊕ C)
  5. 异或操作满足自反性:A ⊕ B ⊕ A = B

举例来说明:

  1. 与自身进行异或操作:7 ⊕ 7 = 0
  2. 与0进行异或操作:5 ⊕ 0 = 5
  3. 交换律:3 ⊕ 5 = 5 ⊕ 3
  4. 结合律:(2 ⊕ 4) ⊕ 6 = 2 ⊕ (4 ⊕ 6)
  5. 自反性:2 ⊕ 4 ⊕ 2 = 4

异或操作在计算机中有很多应用,其中一个常见的应用是用于交换两个变量的值。例如,假设有两个变量a和b,我们可以使用异或操作进行交换如下:

a = a ⊕ b;
b = a ⊕ b;
a = a ⊕ b;

在经过以上操作后,a和b的值就被交换了。

看到了吗?我们可以根据这个自反性质得到我们想要的答案,是不是非常简单。

遍历一边就能拿到想要的结果,代码展示:

	/**
     * 基于位运算
     * 0 ^ * = *
     * A ^ B ^ A = B
     * @param arr
     * @return
     */
    public static int findOneNum2(int[] arr) {
        int res = 0;
        for(int num : arr){
            res ^= num;
        }
        return res;
    }

颜色的分类问题(荷兰国旗问题)

参考题目介绍:75. 颜色分类 - 力扣(LeetCode)
算法通过村第三关-数组黄金笔记|数组难解_第5张图片
算法通过村第三关-数组黄金笔记|数组难解_第6张图片
那我们就来认识一下荷兰的国旗吧:
算法通过村第三关-数组黄金笔记|数组难解_第7张图片
感兴趣的可以看看历史:荷兰和俄罗斯的国旗为什么高度相似?到底是谁抄袭谁? (baidu.com)

这个问题很典型,双指针问题,当然可以采用多种方式的双指针解决,我们研究第一种与冒泡类似的,第二种与快排类似。

基于冒泡排序的双指针(快慢指针)

冒泡排序我们都知道,就是根据大小逐步和后面的比较,慢慢调整到整体有序的状态,这种方法是比较稳定的排序方法。

当然我们可以这样考虑:

  1. 第一次遍历,我们将数组中所有0交换到数组的头部
  2. 第二次遍历,只需要处理1和2。

漂亮的双指针代码如下:

 public static void sortColors(int[] nums) {
        // 定义快慢指针
        int n = nums.length;
        int left = 0;
        // 先处理 0 把0移到最左边
        for(int right = 0; right < n; right++) {
            if (nums[right] == 0){
                swap(nums,left,right);
                left++;
            }

        }
        // 接着处理1 把1移到次走遍
        for(int right = left; right < n; right++){
            if (nums[right] == 1){
                swap(nums,left,right);
                left++;
            }
        }

    }
	
	// 采用位运算的方式交换
    public static void swap(int[] nums, int left, int right) {
        nums[left] = nums[left] ^ nums[right];
        nums[right] = nums[left] ^ nums[right];
        nums[left] = nums[left] ^ nums[right];
    }

这里解决的话效果还是可以的,但是如果再进一步问你,可以一次遍历就解决吗?你就要考虑第二种方法了。

基于快排的双指针(对撞指针)

如果要求一次遍历就解决问题,我们要怎么想办法呢?隐约觉得需要使用三个指针才可以:

  • left指针,表示left左侧的元素都是0
  • right指针,便是right右侧的元素都是2
  • index指针,从头到尾遍历数组,根据nums[index]是0还是2决定与left交换还是和right交换

index的位置上的元素代表我们将要处理的数字。index为1,我们不需要做什么,直接+ 1,如果是0,放在左边,如果是2,放在右边,当index == right的时候就可以停止了。

我们画图表示一下:
算法通过村第三关-数组黄金笔记|数组难解_第8张图片
这里面的重点在于index的位置为2的时候进行交换后为right–,index不做处理,当然这里考虑到了,index 为 1的情况,所以先不动,1 比较特殊,跳过去就没法处理了。

那么问题来了,index 为0的时候执行交换的话index++,如果存在都会被交换到右边,这里只需要处理1和0的问题就可以了。

代码如下:

 	 /**
     * 采用位运算的方式交换
     * @param nums
     * @param left
     * @param right
     */
    public static void swap(int[] nums, int left, int right) {
        nums[left] = nums[left] ^ nums[right];
        nums[right] = nums[left] ^ nums[right];
        nums[left] = nums[left] ^ nums[right];
    }

    public static void sortColors(int[] nums) {
        // 定义快慢指针
        int left = 0 , index = 0,right = nums.length - 1;
        while(index <= right) {
            if (nums[index] == 2){
                swap(nums,index,right--);
            }else if (nums[index] == 0){
                swap(nums,index++,left++);
            }else {
                index++;
            }
        }
    }

总结

注意:双指针问题,边界和条件。

你可能感兴趣的:(算法集训营,算法,笔记,数组,双指针,java,leetcode,推荐算法)