天天看点

[剑指offer][JAVA][面试题56 - I][第260题][位运算][HashSet]

【问题描述】 [面试题56 - I] [数组中数字出现的次数]

一个整型数组 nums 里除两个数字之外,其他数字都出现了两次。请写程序找出这两个只出现一次的数字。要求时间复杂度是O(n),空间复杂度是O(1)。

 

示例 1:

输入:nums = [4,1,4,6]
输出:[1,6] 或 [6,1]


           

【解答思路】

1.位运算

【1,4,4,6】

  • 根据异或的性质,相同异或结果为 0,相异的异或结果为 1,所以将数组中的数字做异或运算,得到两个只出现一次数字的异或结果。

    1 ^ 4 ^ 4 ^ 6 = 7

    [剑指offer][JAVA][面试题56 - I][第260题][位运算][HashSet]
  • 由于两数字不同,异或结果不为 0,则其二进制中必然有一位是不相同的。

    方法一: 7 & i = 0 (出现0 证明可以作为分组依据 )

    [剑指offer][JAVA][面试题56 - I][第260题][位运算][HashSet]
    方法二:7 & -7 = 0001
    [剑指offer][JAVA][面试题56 - I][第260题][位运算][HashSet]
  • 可以先找到不相同的位,就可以将数组分成两组,再像之前那样做异或运算。
    [剑指offer][JAVA][面试题56 - I][第260题][位运算][HashSet]
class Solution {
    // 自己寻找最右边 1 的位置
    public int[] singleNumbers(int[] nums) {
        if (nums == null || nums.length < 2) return new int[0];
        int xor = 0; // 数组第一次异或的结果,即两个只出现一次的数的异或结果
        for (int num : nums) xor ^= num;
        // 寻找右边第一个 1,也就是最右边不相同的位
        int i = 1;
        while ((i & xor) == 0){
            i <<= 1;
        }
        int[] res = new int[2];
        for (int num : nums) {
            if ((i & num) == 0) res[0] ^= num;
            else res[1] ^= num;
        }
        return res;
    }


    // 位运算 xor & (-xor)
    public int[] singleNumbers(int[] nums) {
        if (nums == null || nums.length < 2) return new int[0];
        int xor = 0; // 表示两个只出现一次的数字的异或结果
        for (int num : nums) xor ^= num;

        xor &= -xor;
        int[] res = new int[2];
        for (int num : nums) {
            if ((xor & num) == 0) res[0] ^= num;
            else res[1] ^= num;
        }
        return res;
    }
}


           

2. HashSet

遍历数组,遇到的数如果 HashSet 中存在,就把这个数删除。如果不存在,就把它加入到 HashSet 中。最后 HashSet 中剩下的两个数就是我们要找的了。

时间复杂度:O(N) 空间复杂度:O(N)

public int[] singleNumber(int[] nums) {
    HashSet<Integer> set = new HashSet<>();
    for (int n : nums) {
        if (set.contains(n)) {
            set.remove(n);
        } else {
            set.add(n);
        }
    }
    int[] result = new int[2];
    int i = 0;
    for (int n : set) {
        result[i] = n;
        i++;
    }
    return result;
}

           

【总结】

1. 位运算

异或运算(^)

运算规则:0^0=0; 0^1=1; 1^0=1; 1^1=0;

即:参加运算的两个对象,如果两个相应位为“异”(值不同),则该位结果为1,否则为0。

5 ^ 1 = 0101 ^ 0001 = 0100 = 4

5 ^ 3 = 0101 ^ 0011 = 0110 = 6

用法

  1. 翻转指定位 对应位异或1

    X=10101110,使X低4位翻转,用X ^0000 1111 = 1010 0001即可得到。

  2. 两个数是否相等 ==0

    5 ^ 5 = 0101 ^ 0101 = 0000 = 0

与运算(&)

运算规则:0&0=0; 0&1=0; 1&0=0; 1&1=1;

即:两位同时为“1”,结果才为“1”,否则为0

5 & 1 = 0101 & 0001 = 0001 = 1

5 & 2 = 0101 & 0010 = 0000 = 0

用法

取指定位 对应位与1

设X=10101110,

取X的低4位,用 X & 0000 1111 = 00001110 即可得到;

或运算(|)

运算规则:0|0=0; 0|1=1; 1|0=1; 1|1=1;

即 :参加运算的两个对象只要有一个为1,其值为1

用法:

指定位置置1 对应位或1

将X=10100000的低4位置1 ,用X | 0000 1111 = 1010 1111即可得到

取反运算(~)

运算规则:~1=0; ~0=1;

即:对一个二进制数按位取反,即将0变1,1变0

带符号左移运算(<<)

若左移时舍弃的高位不包含1,则每左移一位,相当于该数乘以2(右边补0)

3 << 1 = 0011 <<1 = 0110 = 6

带符号右移运算(>>)

正数操作数每右移一位,相当于该数除以2

(左补0 or 补1得看被移数是正还是负)

5 >> 1 = 0101 >> 1 = 0010 = 2

5 >> 2 = 0101 >> 2 = 0001 = 1

-14 >> 2 = 11110010 >> 2 = 11111100 = -4

无符号右移运算(>>>)

5 >>> 1 = 0101 >>> 1 = 0010 = 2

-14 >>>2 =11111111 11111111 1111111111110010 >>>2 = 00111111 11111111 1111111111111100 = 1073741820

移位总结

  • 正数的左移与右移,负数的无符号右移,就是相应的补码移位所得,在高位补0即可。
  • 负数的右移,就是补码高位补1,然后按位取反加1即可。

2. 位运算 判相等换位异或^ 取位与&1 置位或|1

3. HashMap 或 HashSet常见用法

3.1 HashSet

(1)增加

public boolean add(E e);

(2)删除

public boolean remove(Object j);

(3)对比查找

public boolean contains(Object j);

(4)清空集合

public void clear();

(5)获取长度

public int size();

3.2 HashMap

(1) 插入键值对数据

public V put(K key, V value)

(2)根据键值获取键值对值数据

public V get(Object key)

(3)获取Map中键值对的个数

public int size()

(4)判断Map集合中是否包含键为key的键值对

public boolean containsKey(Object key)

(5)判断Map集合中是否包含值为value的键值对

boolean containsValue(Object value)

(6)判断Map集合中是否没有任何键值对

public boolean isEmpty()

(7)清空Map集合中所有的键值对

public void clear()

(8)根据键值删除Map中键值对

public V remove(Object key)