只出现一次的数字

934 阅读1分钟

「这是我参与2022首次更文挑战的第37天,活动详情查看:2022首次更文挑战

前言

笔者除了大学时期选修过《算法设计与分析》和《数据结构》还是浑浑噩噩度过的(当时觉得和编程没多大关系),其他时间对算法接触也比较少,但是随着开发时间变长对一些底层代码/处理机制有所接触越发觉得算法的重要性,所以决定开始系统的学习(主要是刷力扣上的题目)和整理,也希望还没开始学习的人尽早开始。

系列文章收录《算法》专栏中。

力扣题目链接

问题描述

给你一个整数数组 nums ,除某个元素仅出现 一次 外,其余每个元素都恰出现 三次 。请你找出并返回那个只出现了一次的元素。

 

示例 1:

输入:nums = [2,2,3,2]
输出:3

示例 2:

输入:nums = [0,1,0,1,0,1,100]
输出:100

 

提示:

  • 1 <= nums.length <= 3 * 104
  • -231 <= nums[i] <= 231 - 1
  • nums 中,除某个元素仅出现 一次 外,其余每个元素都恰出现 三次  

进阶:你的算法应该具有线性时间复杂度。 你可以不使用额外空间来实现吗?

剖析

方法一

这道题,第一反应就是遍历一遍,把数组元素全部放进map中,key为元素,value为元素出现次数。我们可以,在遍历的过程中把已经知道出现三次的从map中移除,这样最后省去遍历map同时又节约了空间。

空间复杂度为O(n):情况最差的情况下需要最后才能从map中移除非目标key。所以为(⌈n/3⌉+1)*2。但是这个方法需要put和remove map,需要计算hash、存在扩容等,走一趟会比较慢。

时间复杂度为O(n):遍历一遍数组存放map需要n次。代码如下:

public static int getOnlyOnceNum(int[] nums) {
    Map<Integer, Integer> numCountMap = new HashMap<>();
    for (int i = 0; i < nums.length; i++) {
        Integer numCount = numCountMap.get(nums[i]);
        if (numCount == null) {
            numCountMap.put(nums[i], 1);
        } else if (numCount == 2) {
            numCountMap.remove(nums[i]);
        } else {
            numCountMap.put(nums[i], ++numCount);
        }
    }

    for (Integer key : numCountMap.keySet()) {
        return key;
    }
    return 0;
}

方法二

那么有没有办法降低空间又走一趟比较快的方式呢?如果把数字代入到二进制世界,可以利用题目的约束条件“除某个元素仅出现 一次 外,其余每个元素都恰出现 三次”,对所有数字的对应二进制的每一位进行统计,因为只有一个元素出现1次,所以对应位如果整除3不等于0的话就能确定只出现1次元素的所有1的位置,只出现一次的元素的所有1都能确定位置,那么这个数就能确定了。代码如下:

public static int getOnlyOnceNum1(int[] nums) {
    int oneNum = 0;
    for (int i = 0; i < 32; ++i) {
        int everyBitCount = 0;
        for (int num : nums) {
            everyBitCount += (num >> i & 1);
        }
        if (everyBitCount % 3 != 0) {
            oneNum |= (1 << i);
        }
    }
    return oneNum;
}

时间复杂度O(n):32*n

空间复杂度O(1)

可以发现方法二的时间复杂度还不如方法一,但是位运算比map的操作速度会快很多,空间复杂度明显比方法一低,所以时间空间的综合效率方法二比1好。