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

news/2024/5/19 23:06:38 标签: 算法, 笔记, 数组, 双指针, java, leetcode, 推荐算法

文章目录

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


前言


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

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

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

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

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

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

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

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

我们展示一下代码:

java">	/**
     * 方法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就是超过一半的元素。
java">	/**
     * 方法二:比较特殊的计数法
     *
     * @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)
在这里插入图片描述
在这里插入图片描述
这个题用Set集合解决比较好,Set集合不存储重复元素,这个是该集合的特性。题目说明其他元素都是出现两次,我们刚好可以利用这个操作,当要添加元素的key与集合中已存在的数出现重复的时候,我们就不进行操作,并且将这个key一起删掉,确保只存在一个数,这样遍历一边就可以知道答案了。【注意:确保集合有元素 】

java">	/**
     * 基于集合寻找
     *
     * @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的值就被交换了。

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

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

java">	/**
     * 基于位运算
     * 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)
在这里插入图片描述
在这里插入图片描述
那我们就来认识一下荷兰的国旗吧:
在这里插入图片描述
感兴趣的可以看看历史:荷兰和俄罗斯的国旗为什么高度相似?到底是谁抄袭谁? (baidu.com)

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

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

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

当然我们可以这样考虑:

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

漂亮的双指针代码如下:

java"> 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的时候就可以停止了。

我们画图表示一下:
在这里插入图片描述
这里面的重点在于index的位置为2的时候进行交换后为right–,index不做处理,当然这里考虑到了,index 为 1的情况,所以先不动,1 比较特殊,跳过去就没法处理了。

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

代码如下:

java"> 	 /**
     * 采用位运算的方式交换
     * @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++;
            }
        }
    }

总结

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


http://www.niftyadmin.cn/n/4972435.html

相关文章

JavaScript 基础知识回顾与复习---闭包

当我们说到闭包&#xff0c;在JavaScript中闭包是一个让人难以理解甚至说是一个近乎神话的概念。闭包往往也是面试必考的题目&#xff0c;如果能够掌握闭包对我们自己来说那也是一种极大的提升。在学习的过程中不要害怕闭包&#xff0c;闭包并不是一个新的语法或者模式&#xf…

C语言日常刷题 2

文章目录 题目答案与解析1、2、3、4、5、6、7、 题目 1、以下程序段的输出结果是&#xff08; &#xff09; #include<stdio.h> int main() { char s[] "\\123456\123456\t"; printf("%d\n", strlen(s)); return 0; }A: 12 B: 13 C: 16 D: 以上都…

React生命周期(新-旧)

文章目录 前言1、生命周期介绍2、钩子函数介绍 生命周期的三个阶段一、生命周期&#xff08;旧&#xff09;1.初始化阶段(挂载阶段)① constructor② componentWillMount③ render④ componentDidMount 2.更新阶段① shouldComponentUpdate② componentWillUpdate③ render④ c…

2.2 Dubbo的基本应用-本地存根 、本地伪装 、参数回调

本地存根 官网地址&#xff1a; http://dubbo.apache.org/zh/docs/v2.7/user/examples/local-stub/ 本地存根&#xff0c; 名字很抽象&#xff0c; 但实际上不难理解&#xff0c; 本地存根就是一 段逻辑&#xff0c; 这段逻辑是在服务消费端执行的 &#xff0c; 这段逻辑一 般都…

Failed to load local image resource/images/1.jpg无法加载本地图片资源

微信小程序开发无法加载本地图片 先放报错图片 绝对路径不行&#xff0c; <image src"../../images/1.jpg" mode"heightFix"></image>使用相对路径就可以了 <image src"../../images/1.jpg" mode"heightFix"><…

Linux——基础IO(2)及动静态库多种方式使用及制作

目录 0. 前言 1. 文件存储设备—磁盘 1.1 文件及存储介质 1.2 磁盘结构 1.3 磁盘存储结构 1.4 磁盘的抽象&#xff08;虚拟、逻辑&#xff09;结构 1.5 磁盘分区管理 2. 理解文件系统 2.1 Linux磁盘文件管理 2.2 文件inode属性及Data block数据追溯 2.3 inode编号及…

金字塔原理(演示的逻辑)

前言&#xff1a;如何做好PPT&#xff0c;在演讲中运用金字塔原理&#xff1f;今天学习最后一部分——演示的逻辑。 目录 在书面上呈现金字塔 常见方法 多级标题法 下划线法 数字编号法 行首缩进法 项目符号法 要点 在PPT演示文稿中呈现金字塔 基本原则 文字图表占…

openGauss学习笔记-51 openGauss 高级特性-列存储

文章目录 openGauss学习笔记-51 openGauss 高级特性-列存储51.1 语法格式51.2 参数说明51.3 示例 openGauss学习笔记-51 openGauss 高级特性-列存储 openGauss支持行列混合存储。行存储是指将表按行存储到硬盘分区上&#xff0c;列存储是指将表按列存储到硬盘分区上。 行、列…