前言:

本题解整理了一位大佬在leetcode中的代码的方法,该博文致力于让所有人都能够能够看懂该方法。为此,本题解将从统计数字出现次数的解题方式开始讲起,再推导出逐位统计的解题方式,期望以循序渐进的方式得出最终代码的思想。

相关知识关键字:

二进制、位运算、真值表、逻辑表达式、状态机

题目:

剑指offer 56 II. 数组中数字出现的次数 II

在一个数组 nums 中除一个数字只出现一次之外,其他数字都出现了三次。请找出那个只出现一次的数字。

示例 1:

输入:nums = [3,4,3,3]

输出:4

示例 2:

输入:nums = [9,1,7,9,7,9,7]

输出:1

题解:

对于数组nums,其只有一个数字出现了一次,其余数字均出现了三次,一种直观的想法是直接采用一个map统计各个字符出现的次数,最后再遍历map中的各个键值对,直到找到只出现了一次的数字。其代码如下

    public int singleNumber(int[] nums) {
        //统计各个数字出现的次数,键为数字,值为出现的次数
        Map<Integer,Integer> map =new HashMap<Integer,Integer>();
        for(int i:nums){
            if(!map.containsKey(i)){
                map.put(i,1);
                continue;
            }
            map.put(i,map.get(i)+1);
        }
        //遍历map中的键值对,查看值出现次数为1的键,即为答案
        int result = 0;
        for(Map.Entry<Integer,Integer> entry:map.entrySet()){
            if(entry.getValue()==1){
                result = entry.getKey();
                break;
            }
        }
        return result;
    }

对于该解题方法,其空间复杂度为O(n),时间复杂度为O(n),这显然不会是该题的最优解。

在得出逐位运算的解题方式之前,我们需要研究下该数组中的数字用二进制的方式进行表示的特点。

以题干给出的示例1为例,nums=[3,4,3,3],将数组中各个数字采用二进制的方式写出,

3 = (0011)2

4 = (0100)2

3 = (0011)2

3 = (0011)2

通过对数组中各个数的二进制表示形式逐位进行观察,我们可以发现,当数组中只出现一次的那个数字(用k表示)在二进制的对应位为0时,该对应位为1在数组各个数字中出现的总次数应当为3^n ,当k的对应位为1时,该对应位为1在数组各个数字中出现的总次数应当为 3^n + 1,为此,我们可以统计数字中的各个位中1出现的次数,当为3^n 次时,只出现一次的数字的对应位应当为0,当为3^n + 1次时,只出现一次的数字的对应位应当为1。由此,我们可以得到如下代码:

    public int singleNumber(int[] nums) {
        if(nums==null||nums.length==0) return 0;
        int result = 0;
        for(int i = 0;i<32;i++){
            //统计该位1的出现次数情况
            int count = 0;
            int index = 1<<i;
            for(int j:nums){
                //该位与操作后的结果不为0,则表示该位为1的情况出现了
                if((index&j)!=0){
                    count++;
                }
            }
            //该位上出现1的次数mod3后为1,表示出现一次的数字该位为1
            if(count%3==1){
                result|=index;
            }
        }
        return result;
    }

对于该解题方法,其时间复杂度为O(n),空间复杂度为O(1)。在某种程度上,这是最优解了。但是,该题解仍有改进的空间(其时间复杂度的常系数为32)。

有了对数组中数字的各二进制位进行逐一统计分析出现次数的相关基础后,我们便可以推导出那个击败100%的答案的解法了。回顾上面的解题方法的分析部分,其需要我们对数字的二进制位逐位进行统计,对于int数据类型,我们需要遍历32次数组(int占4字节),以便统计出各个二进制位出现的次数。那我们有没有办法只遍历一次数组便得出答案呢?当然有,我们可以一次分析32bit的int的各个位在数组的各个数字中出现的次数。在分析上面的代码我们可以发现,实际上,我们只需要记录对应位出现的次数为0、1、2次的情况,当对应位出现次数为3的时候,我们便可以将该位出现的次数置为0,重新开始进行计数。由于int型中的各个二进制位出现的次数为3进制的,为此我们需要两个位来记录各个位出现的次数,由此我们需要引入两个变量a,b来统计对应位出现的次数。由ab两个变量组合起来来记录各个二进制位出现为1的情况。变量a表示高位的情况,变量b表示低位的情况,而在遍历数组运算完成之后,遍历b的值便是答案。

变量ab组合的各个二进制位组合的形式有如下三种,考虑进新引入的变量c的各二进制位的情况,我们可以得到如下真值表:

由以上真值表,我们便可得出变量a,b的逻辑表达式,其表示如下

a = a’(!b’)(!c)+(!a’)b’c

b = (!a’)b’(!c)+(!a’)(!b’)c = (!a’)[b’(!c)+(!b’)c] = (!a’)[b’^c]

由此,我们可以得到如下代码

    public int singleNumber(int[] nums) {
        //a对应位为1表示出现2次的记录,b对应位表示出现1次或0次的记录,ab共同组成该位出现的次数
        int a = 0,b =0;
        for(int i:nums){
            int temp = a;
            a = (~a&b&i)|(a&~b&~i);
            b = ~temp&(b^i);
        }
        return b;
    }

实际上,我们还能对a的逻辑表达式进行简化,先得到b的逻辑表达式,之后用b代替b’作为输入,由此可以简化a为

a = (!a’)(!b)c+a’(!b)(!c) = (!b)[(!a’)c+a’(!c)] = (!b)[a’^c]

由此,我们可以得到如下代码

    public int singleNumber(int[] nums) {
        //a为对应位的1出现2次的记录,b为对应位出现1次的记录,ab共同组成该位出现的次数
        int a = 0,b =0;
        for(int i:nums){
            b = ~a&(b^i);
            a = ~b&(a^i);
        }
        return b;
    }

至此,我们得到了最终的代码。


这个是本人的公众号,致力于写出绝大部分人都能读懂的技术文章。欢迎相互交流,我们博采众长,共同进步。

K:剑指offer-56 题解 谁说数字电路的知识不能用到算法中?从次数统计到数字电路公式推导,一文包你全懂的更多相关文章

  1. 剑指 Offer 56 - II. 数组中数字出现的次数 II + 位运算

    剑指 Offer 56 - II. 数组中数字出现的次数 II Offer_56_2 题目详情 解题思路 java代码 package com.walegarrett.offer; /** * @Au ...

  2. 剑指 Offer 56 - I. 数组中数字出现的次数 + 分组异或

    剑指 Offer 56 - I. 数组中数字出现的次数 Offer_56_1 题目描述 解题思路 java代码 /** * 方法一:数位方法 */ class Offer_56_1_2 { publi ...

  3. 《剑指offer》题解

    有段时间准备找工作,囫囵吞枣地做了<剑指offer>提供的编程习题,下面是题解收集. 当初没写目录真是个坏习惯(-_-)||,自己写的东西都要到处找. 提交的源码可以在此repo中找到:h ...

  4. 【剑指Offer面试编程题】题目1349:数字在排序数组中出现的次数--九度OJ

    题目描述: 统计一个数字在排序数组中出现的次数. 输入: 每个测试案例包括两行: 第一行有1个整数n,表示数组的大小.1<=n <= 10^6. 第二行有n个整数,表示数组元素,每个元素均 ...

  5. 剑指 Offer 30. 包含min函数的栈 + 双栈实现求解栈中的最小值

    剑指 Offer 30. 包含min函数的栈 Offer_30 题目描述: 题解分析: 题目其实考察的是栈的知识,本题的目的是使用两个栈来求解最小值. 第二个栈主要用来维护第一个栈中的最小值,所以它里 ...

  6. 剑指Offer的学习笔记(C#篇)-- 数字在排序数组中出现的次数

    题目描述 统计一个数字在排序数组中出现的次数. 一 . 题目分析 该题目并不是难题,但该题目考察目的是正确的选择合适的查找方法.题目中有一个关键词是:排序数组,也就是说,该数组已经排好了,我一开始直接 ...

  7. 剑指offer——56在排序数组中查找数字

    题目描述 统计一个数字在排序数组中出现的次数.   题解: 使用二分法找到数k然后向前找到第一个k,向后找到最后一个k,即可知道有几个k了 但一旦n个数都是k时,这个方法跟从头遍历没区别,都是O(N) ...

  8. 剑指offer 56.删除有序链表中的重复结点

    56. 删除有序链表中的重复结点 题目描述 在一个排序的链表中,存在重复的结点,请删除该链表中重复的结点,重复的结点不保留,返回链表头指针. 例如,链表1->2->3->3-> ...

  9. 【位运算】剑指offer 56. 数组中数字出现的次数

    这是一系列位运算的题目,本文将由浅入深,先从最简单的问题开始: 问题1: 一个数组中只有一个数字出现过1次,其余数字都出现过两次,请找到那个只出现1次的数字.要求时间复杂度是 \(O(n)\),空间复 ...

随机推荐

  1. Memcached笔记——(三)Memcached使用总结

    为了将N个前端数据同步,通过Memcached完成数据打通,但带来了一些新问题: 使用iBatis整合了Memcached,iBatis针对每台server生成了唯一标识,导致同一份数据sql会产生不 ...

  2. 正则表达式入门-python代码

    题记 本文介绍了Python对于正则表达式的支持,包括正则表达式基础以及Python正则表达式标准库的完整介绍及使用示例. 正则表达式在很多的应用中都有使用到,特别是在网络爬虫中格式化html后取出自 ...

  3. swap和shm的区别

    在使用docker的过程中,发现其有很多内存相关的命令,对其中的swap(交换内存)和shm(共享内存)尤其费解.于是查阅了一些资料,弄明白了二者的基本区别. swap 是一个文件,是使用硬盘空间的一 ...

  4. dubbo与trivial超时机制的深入思考

    说在前面 trivial是根据之前设计的RPC框架而来的(还在增进当中),其中较为不同的一个点为,在客户端去掉了业务线程池,因为既然都要等待,不必要再加一层. 进入正题 有在网上看到这样的信息,“之前 ...

  5. 带你学习Javascript中的函数进阶(一)

    1. 函数的定义和调用 1.1 函数的定义方式 函数声明方式function关键字(命名函数) 函数表达式(匿名函数) new Function() var fn = new Function('参数 ...

  6. Ubuntu 18.04 国内的 apt 源

    一.Ubuntu 18.04 国内的 apt 源 1. 阿里源 deb http://mirrors.aliyun.com/ubuntu/ bionic main restricted univers ...

  7. 压力测试(六)-阿里云Linux服务器压测接口实战

    1.SpringBoot 接口打包,并用jar包方式部署 简介:用jar包方式在控制台进行启动 打包 mvn package && java -jar target/gs-spring ...

  8. 前端H5,点击选择图片控件,图片直接在页面上展示~

    <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8&quo ...

  9. 并发工具类的使用 CountDownLatch,CyclicBarrier,Semaphore,Exchanger

    1.CountDownLatch 允许一个或多个线程等待直到在其他线程中执行的一组操作完成的同步辅助. A CountDownLatch用给定的计数初始化. await方法阻塞,直到由于countDo ...

  10. java CRC16 算法

    代码摘自:https://www.cnblogs.com/lujiannt/p/9246256.html 1.CRC16算法 public class CRC16Util { /** * 计算CRC1 ...