• 算法之异或运算及其应用


    算法之异或运算及其应用

    基本介绍

    异或算法又可称为无进位加法

    1 ^ 1 = 0 ( 1 + 1 = 10 ,如果不进位的话,那结果就是0 )

    1 ^ 0 = 1 ( 1 + 0 = 1 )

    0 ^ 1 = 1 ( 0 + 1 = 1 )

    0 ^ 0 = 0 ( 0 + 0 = 0 )

    特性

    满足交换律和结合律,表明计算结果和异或顺序无关

    N ^ 0 = N N ^ N = 0

    应用 - 快速交换值

    1. 代码实现

    交换 a 与 b 的值

      private void swap(int a, int b) {
            a = a ^ b;
            b = a ^ b;
            a = a ^ b;
        }
    

    2. 好处

    按照上述方式进行值的交换,就无需开辟一个新的空间(不用创建一个变量来辅助进行值的交换)

    3. 原理说明

    需要使用到的知识为:

    异或运算结果和顺序无关

    N ^ 0 = N N ^ N = 0

    说明如下:

    譬如 a = 甲,b = 乙

    ① a = a ^ b => 此时 a = 甲 ^ 乙 , b = 乙

    ② b = a ^ b => 此时 a = 甲 ^ 乙 , b = 甲 ^ 乙 ^ 乙 = 甲 ^ 0 = 甲

    ③ a = a ^ b => 此时 a = 甲 ^ 乙 ^ 甲 = 甲 ^ 甲 ^ 乙 = 0 ^ 乙 = 乙 , b = 甲

    通过分析,我们可以得到,经过这三步后, a 和 b 的值确实交换了

    4. 使用前提

    使用这个看似很高逼格的交换前提是 a 和 b 不能指向同一空间 ( 它们的值可以相等,但不能指向同一空间 ), 不然 a 和 b 就都会变成 0 ,因为此时 a ^ b = a ^ a = 0

    你没办法确保传进来的两个值一定是不同的( 在排序算法及其应用2 - 冒泡排序中我们曾用过这个交换方法,是因为我们确定传进来的两个数肯定不是指向同一空间的 ),所以这种抖机灵的写法是不推荐的,日常最好还是按照我们一般的交换方式来写

    算法题

    1. 问题

    有一个数组[]

    ① 数组中只有一种数字出现奇数次,其他数字都出现偶数次

    ② 数组中有两种数字出现奇数次,其他数字都出现偶数次

    找到出现奇数次的数字

    要求: 时间复杂度为 O(n), 空间复杂度为 O(1)

    2. 思路

    对于第 ① 种情况,所求的奇数次的数字即为数组中每个元素进行异或后的结果

    证明如下:

    由异或运算的特性:运算顺序不影响异或结果,所以出现偶数次的数字经过异或后就会变成0,所以结果自然只剩下出现奇数次的数字

    举个栗子: 比如 arr = {1, 3, 9, 6, 6, 3, 3, 1, 3} [ 其中唯一出现奇数次的数字为 9 ]

    将所有元素异或后得到 1 ^ 3 ^ 9 ^ 6 ^ 6 ^ 3 ^ 3 ^ 1 ^ 3 = 1 ^ 1 ^ 3 ^ 3 ^ 3 ^ 3 ^ 6 ^ 6 ^ 9 = 0 ^ 9 = 9

    对于第 ② 种情况,会比第一种难一些,实现思路如下:

    首先将数组中每个元素进行异或,得到的结果就是两个奇数次的数字的异或,将这个结果赋值给 eor

    由于这两个数字不一样,所以 eor != 0,这两个数字的二进制肯定有一位是不同的

    假设它们的第 8 位不同,即得到的异或结果的第 8 位为 1,我们让 eor' = “第 8 位为 1,其他都为 0”,而数组中所有元素可以分为两组, 一组为 ‘第 8 位为 0’,另一组为 ‘第 8 位为 1’,而我们所要求的这两个数分别在这两组中( 不可能为同一个组,因为上面已经假设他们第 8 位是不同的,所以必然一个为 1,一个为 0 ) ,并且每一组中除了要求的两个数,其他数都是偶数次的

    我们将数组元素分别与 eor' 进行 & 运算,将第 8 位上等于 1 (或 0)的数字筛选出来,进行异或运算,这样就可以得到其中一个出现奇数次的数 a

    再将 eor 与 a 进行异或,即可得到另外一个出现奇数次的数 b

    听上去是不是有些抽象,那我们来举个栗子~

    比如 arr = {1, 3, 9, 6, 6, 3, 3, 1, 3, 7} [ 其中唯一出现奇数次的数字为 9 和 7 ]

    第一步,先将所有元素进行异或: eor = 1 ^ 3 ^ 9 ^ 6 ^ 6 ^ 3 ^ 3 ^ 1 ^ 3 ^ 7 = 1 ^ 1 ^ 3 ^ 3 ^ 3 ^ 3 ^ 6 ^ 6 ^ 9 ^ 7 = 0 ^ 9 ^ 7= 9 ^ 7 = 14

    9 的二进制 1 0 0 1

    7 的二进制 ^ 1 1 1

    ​ -----------------

    ​ 1 1 1 0 ( 化为十进制就是14 )

    第二步,我们看到 eor 的二进制结果中至少有一位不为 0,我们选出不为 0 的一位,譬如第 2 位, 那么 eor' = "第 2 位为1,其他都为 0" = 2

    第三步,将 eor' 分别与数组元素进行 & 运算,就可以将元素分为了两组

    1, 1 | 3, 3, 3, 3

    9 | 6, 6

    ​ | 7

    第 2 位为 0 第 2 位为 1

    我们取与 eor' 异或结果为 0 的那一组(你想取结果为 0 或结果为 1 的都可以),即 第 2 位为 0 的那一组,将这组的元素进行异或运算,得到结果 a = 1 ^ 1 ^ 9 = 0 ^ 9 = 9

    第四步,再将 a 与 eor 进行异或 ,即可得到结果 b = 9 ^ 14 = 7

    a 和 b 即是我们要求的值

    3. 代码

    问题 ① 的代码

      public int getOneNum(int[] arr) {
            int eor = 0;
            for (int i : arr) {
                eor ^= i;
            }
            return eor;
        }
    

    问题 ② 的代码

        public void getTwoNum(int[] arr) {
            int eor = 0;
            for (int i : arr) {
                eor ^= i;
            }
            // eor = 所求两个数的异或结果
            // eor 一定不为0, eor 必然有一位上是 1
            int rightOne = eor & (~eor + 1);    // 提取出最右位的1
            int onlyOne = 0;    // eor'
            for (int cur: arr){
                if ((cur & rightOne) == rightOne) { // 这里目的是为了分组, == 0 也是可以的
                    onlyOne ^= cur;
                }
            }
            System.out.println(onlyOne + " " + (eor ^ onlyOne));
        }
    

    说明:其中 int rightOne = eor & (~eor + 1); // 提取出最右位的1 用于得到某个数最右边位置上的1,是一种常规操作,要学会使用

    欢迎大家来我博客逛逛 mmimo技术小栈

  • 相关阅读:
    第二部分:并发工具类17->ReadWriteLock:如何快速实现一个完备的缓存
    第二部分:并发工具类16->Semaphore:如何快速实现一个限流器
    第二部分:并发工具类15->Lock和condition(下)
    一款类似B站的开源弹幕播放器,太酷了
    2021年基础知识点复习
    Autofac.Core.DependencyResolutionException: An exception was thrown while activating Castle.Proxies.MiniProgramAppServiceProxy.
    一张图解析FastAdmin中的弹出窗口的功能
    vue 关闭代码严格模式,轻松运行
    vue项目严格模式下的常见错误
    mysql下载安装包及安装步骤
  • 原文地址:https://www.cnblogs.com/mmimo/p/15388019.html
Copyright © 2020-2023  润新知