位运算(异或运算) :数组中数字出现的次数

Posted 炫云云

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了位运算(异或运算) :数组中数字出现的次数相关的知识,希望对你有一定的参考价值。

数组中数字出现的次数

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

示例 1:

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

示例 2:

输入:nums = [1,2,10,4,1,4,3,3]
输出:[2,10] 或 [10,2]

位运算(异或运算)

设整型数组 nums 中出现一次的数字为 x ,出现两次的数字为 a, a, b, b, … ,即:

n u m s = [ a , a , b , b , . . . , x ] nums = [a, a, b, b, ..., x] nums=[a,a,b,b,...,x]
异或运算有个重要的性质,两个相同数字异或为 0 ,任何数字与0异或结果是其本身。即对于任意整数 a 有 a ⊕ a = 0 a \\oplus a = 0 aa=0 。因此,若将 nums 中所有数字执行异或运算,留下的结果则为 出现一次的数字 x x x ,即:
   a ⊕ a ⊕ b ⊕ b ⊕ . . . ⊕ x =    0 ⊕ 0 ⊕ . . . ⊕ x =    x \\begin{aligned} & \\ \\ a \\oplus a \\oplus b \\oplus b \\oplus ... \\oplus x \\\\ = & \\ \\ 0 \\oplus 0 \\oplus ... \\oplus x \\\\ = & \\ \\ x \\end{aligned} ==  aabb...x  00...x  x

异或运算满足交换律 a ⊕ b = b ⊕ a a \\oplus b = b \\oplus a ab=ba ,即以上运算结果与 nums 的元素顺序无关。代码如下:

def singleNumber( nums):
    z = 0
    for num in nums:  # 1. 遍历 nums 执行异或运算
        z ^= num
        print(z)      
    return z;         # 2. 返回出现一次的数字 x

nums = [3,3,4,4,1]
end = singleNumber( nums)
3
0
4
0
1

Picture1.png

本题难点: 数组 nums 有 两个 只出现一次的数字,因此无法通过异或直接得到这两个数字。

设两个只出现一次的数字为 x , y x , y x,y ,由于 x ≠ y x \\ne y x=y ,则 x x x y y y 二进制至少有一位不同(即分别为 0 和 1 ),根据此位可以将 nums 拆分为分别包含 x 和 y 的两个子数组。

易知两子数组都满足 「除一个数字之外,其他数字都出现了两次」。因此,仿照以上简化问题的思路,分别对 两子数组 遍历执行异或操作,即可得到两个只出现一次的数字 x , y x , y x,y

算法流程:

  1. 遍历 nums执行异或:

设整型数组 n u m s = [ a , a , b , b , . . . , x , y ] nums = [a, a, b, b, ..., x, y] nums=[a,a,b,b,...,x,y] ,初始化n=0,任何数字与0异或结果是其本身。 对 nums 中所有数字执行异或,得到的结果为 x ⊕ y x \\oplus y xy ,即:
n =    n ⊕ a ⊕ a ⊕ b ⊕ b ⊕ . . . ⊕ x ⊕ y =    0 ⊕ 0 ⊕ . . . ⊕ x ⊕ y =    x ⊕ y \\begin{aligned} n = & \\ \\ n \\oplus a \\oplus a \\oplus b \\oplus b \\oplus ... \\oplus x \\oplus y \\\\ = & \\ \\ 0 \\oplus 0 \\oplus ... \\oplus x \\oplus y \\\\ = & \\ \\ x \\oplus y \\end{aligned} n===  naabb...xy  00...xy  xy

  1. 循环左移计算 m :

根据异或运算定义,若整数 x ⊕ y x \\oplus y xy 某二进制位为 1 ,则 x x x y y y 的此二进制位一定不同。换言之,找到 x ⊕ y x \\oplus y xy为 1 的二进制位,即可将数组 nums 拆分为上述的两个子数组。根据与运算特点,可知对于任意整数 a 有:

a & 0001 = 1 a \\& 0001 = 1 a&0001=1 ,则 a a a 的第一位为 1 ;

a & 0010 = 1 a \\& 0010 = 1 a&0010=1 ,则 a a a 的第二位为 1 ;

以此类推……

因此,初始化一个辅助变量 m = 1 m = 1 m=1 ,通过与运算从右向左循环判断,可 获取整数 x ⊕ y x \\oplus y xy 首位 是 1的位置 ,记录于 m m m 中,

举个例子:n = 10 ^ 2 = 1010 ^ 0010 = 1000,第四位为1.如果(n & m)的结果等于0说明n的最低为是0

代码如下:

while n & m == 0: # m 循环左移一位,直到 n & m != 0   
    m <<= 1 
  1. 拆分 nums 为两个子数组

  2. 分别遍历两个子数组执行异或:

    通过遍历判断 nums中各数字和 m 做与运算的结果,可将数组拆分为两个子数组,并分别对两个子数组遍历求异或,则可得到两个只出现一次的数字,代码如下:

for num in nums:
    if num & m: x ^= num  # 若 num & m != 0 , 划分至子数组 1 ,执行遍历异或
    else: y ^= num        # 若 num & m == 0 , 划分至子数组 2 ,执行遍历异或
return x, y               # 遍历异或完毕,返回只出现一次的数字 x 和 y

  1. 返回值:

返回只出现一次的数字 x, y 即可。

设 nums = [3, 3, 4, 4, 1, 6],以上计算流程如下图所示。

Picture2.png

class Solution:
    def singleNumbers(self, nums: List[int]) -> List[int]:
        x, y, n, m = 0, 0, 0, 1
        for num in nums:         # 1. 遍历异或
            n ^= num 
        while n & m == 0:        # 2. 循环左移,计算 m
            m <<= 1       
        for num in nums:         # 3. 遍历 nums 分组
            if num & m: x ^= num # 4. 当 num & m != 0
            else: y ^= num       # 4. 当 num & m == 0
        return x, y              # 5. 返回出现一次的数字

复杂度分析:

时间复杂度 O(N) : 线性遍历 nums 使用 O(N) 时间,遍历 x ⊕ y x \\oplus y xy 二进制位使用 O ( 32 ) = O ( 1 ) O(32) = O(1) O(32)=O(1)时间。

空间复杂度 O(1): 辅助变量 a , b , x , y 使用常数大小额外空间。

判定字符是否唯一

实现一个算法,确定一个字符串 s 的所有字符是否全都不同。

示例 1:

输入: s = "leetcode"
输出: false 

示例 2:

输入: s = "abc"
输出: true

如果你不使用额外的数据结构,会很加分。

位运算(异或运算)

这里就是设置一个值,mask。然后利用每个字符串的唯一性质(unicode),就将1向左移动unicode的值。得到一个二进制的数值(如100000)如果与的答案为0,就说明没有出现过,将这个位置的1或给mask,不同的字符就代表着不同的位置,这个mask就会在不同的位置变为1。然后循环。

class Solution(object):
    def isUnique(self, astr):
        """
        :type astr: str
        :rtype: bool
        """
        mask = 0    #假设这个变量占26个bit ,那么我们可以把它看成000...00(26个0),
                    #这26个bit对应着26个字符,对于一个字符c,检查对应下标的bit值即可判断是否重复。
        for i in astr:
            move = ord(i) - ord('a')  
            if mask & (1<<move) != 0:  #使用左移运算符1 << move 则可以得到对应下标为1,其余下标为0的数,如字符char = 'c',则得到的数为000...00100
                                    # 将这个数跟mark做与运算,由于这个数只有一个位为1,其他位为0,那么与运算的结果中,其他位肯定是0,
                                    # 而对应的下标位是否0则取决于之前这个字符有没有出现过,若出现过则被标记为1,那么与运算的结果就不为0
                return False
            else:            
                mask |= (1<<move)    #若之前没有出现过,则对应位的与运算的结果也是0,那么整个结果也为0。
                                    #对于没有出现过的字符,我们用或运算mask |= (1<<move) 将对应下标位的值置为1。
        return True
class Solution(object):
    def isUnique(self, astr):
        """
        :type astr: str
        :rtype: bool
        """
        from collections import Counter
        dic=Counter(astr)
        return len(dic.values())==0 or max(dic.values())<2

参考

力扣(LeetCode) (leetcode-cn.com)]

《画解剑指 Offer 》

https://leetcode-cn.com/problems/search-insert-position/solution/te-bie-hao-yong-de-er-fen-cha-fa-fa-mo-ban-python-/

以上是关于位运算(异或运算) :数组中数字出现的次数的主要内容,如果未能解决你的问题,请参考以下文章

剑指 Offer 56 - I. 数组中数字出现的次数(位运算(异或与),Java)

剑指Offer56 - I. 数组中数字出现的次数(位运算)

二进制和位运算中的异或

leetcode中等剑指 Offer 56 数组中数字出现的次数

leetcode中等剑指 Offer 56 数组中数字出现的次数

剑指56-2 数组中数字出现的次数