在数组中找重复数、只出现一次的数或丢失数的题目(Leetcode题解-Python语言)

在一维数组中的考察中,最常见的就是找出数组中的重复数、只出现一次的数或者丢失(消失)数等等。

一般来说,首先想到的就是用哈希表集合)来记录出现过的数,基本所有的题都可以用集合来做,而技巧性在于有时可以把原数组自身作为哈希表
其次就是位运算,原理是相同的数做异或运算 ^ 会得到0,而一个数与0做异或会得到这个数本身
最后,在排好序或者对空间要求为O(1)但又不能修改原数组的情况下,二分查找也是一种方法。

136. 只出现一次的数字找出一个只出现一次的数字

class Solution:
    def singleNumber(self, nums: List[int]) -> int:
        ans = set()
        for num in nums:
            if num in ans:
                ans.remove(num)
            else:
                ans.add(num)
        return ans.pop()

虽然可以用集合解决,但是此题最优的做法是位运算,数组里面所有相同的数异或会得到0,而那个只出现一次的数再与0做异或,直接得到结果本身,代码如下:

class Solution:
    def singleNumber(self, nums: List[int]) -> int:
        ans = nums[0]
        for i in range(1, len(nums)):
            ans = ans ^ nums[i]
        return ans

217. 存在重复元素是否存在重复元素

class Solution:
    def containsDuplicate(self, nums: List[int]) -> bool:
        temp = set()
        for num in nums:
            if num in temp:
                return True
            else:
                temp.add(num)
        return False

剑指 Offer 03. 数组中重复的数字找出一个重复元素

class Solution:
    def findRepeatNumber(self, nums: List[int]) -> int:
        temp = set()
        for num in nums:
            if num in temp:
                return num
            else:
                temp.add(num)
        return -1

用集合轻松解决,但是可以不使用额外的集合,而是将原数组本身当作集合,通过交换使得数组的值与索引(下标)一一对应,若出现两个值对应同一个索引,即为重复的元素。

在这里插入图片描述

class Solution:
    def findRepeatNumber(self, nums: [int]) -> int:
        i = 0
        while i < len(nums):
            if nums[i] == i:  # 值与索引已经相同,跳过
                i += 1
                continue
            if nums[i] == nums[nums[i]]:   # 值与值所指向下标的值一样,说明是重复数
            	return nums[i]
            nums[nums[i]], nums[i] = nums[i], nums[nums[i]]
        return -1

注意: Python 中, a, b = c, d操作的原理是先暂存元组 (c,d) ,然后 “按左右顺序” 赋值给 a 和 b 。因此,若写为 nums[i], nums[nums[i]] = nums[nums[i]], nums[i],则 nums[i] 会先被赋值,之后 nums[nums[i]] 指向的元素则会出错。

260. 只出现一次的数字 III剑指 Offer 56 - I. 数组中数字出现的次数)(找出两个只出现一次的数字

回想到,对所有数字进行异或就可以得到结果,本题中其余数字也是出现两次,区别在于有两个数字只出现一次。在这里,我们会希望这两个数字分别出现在两组中,对这两组都进行异或,这样就能得到答案了。怎么做呢?线索在于,对所有数字进行异或后的结果,考虑其每一位取值的意义,如果为0,说明这两个数字的这一位相同,如果为1则不相同。

找到第一位为1的,说明这两个数在这一位上一个为1、一个为0。以此我们可以把数组分为两部分,这两个数各自存在于这两部分中,划分的依据就是这一位的取值。至于其他出现两次的数,在分组时相同的数一定在同一组,因此对这两组都进行全部异或,出现两次的数会抵消,最后剩下这两个数字。

class Solution:
    def singleNumber(self, nums: List[int]) -> List[int]:
        # 全部异或
        ret = reduce(lambda x, y: x ^ y, nums) # reduce(二元函数, 可迭代对象)
        h = 1
        while h & ret == 0:  # 从右边开始找第一位为1的(两个数不同的位)
            h <<= 1
        a, b = 0, 0
        # 分别异或
        for n in nums:
            if n & h:  # n 在这一位是 1,一个组
                a ^= n
            else:      # n 在这一位是 0,另一个组
                b ^= n
        return [a, b]

137. 只出现一次的数字 II剑指 Offer 56 - II. 数组中数字出现的次数 II)(剑指 Offer II 004. 只出现一次的数字 )(找出一个只出现一次的数字,然而其他数字都出现三次

class Solution:
    def singleNumber(self, nums: List[int]) -> int:
        counter = collections.Counter(nums)
        ans = [num for num, val in counter.items() if val == 1]
        return ans[0]

这一题用集合的话,还不能简单地出现过就 pop,没出现过就 push,因为重复数字是出现三次的,所以应该用 Counter 来解决,更加优化的思路是借鉴数字电路的:

class Solution:
    def singleNumber(self, nums: List[int]) -> int:
        a = 0
        b = 0
        for i in range(len(nums)):
            b = (b ^ nums[i]) & ~a
            a = (a ^ nums[i]) & ~b
        return b

思路是设置一个状态机,有 a、b 两个记录器:第一次碰到数字 x 时,记录器 b 记录下来,记录器 a 为 0;第二次碰到数字 x 时,记录器 a 记录下来,记录器 b 为 0;第三次碰到数字 x 时,两个记录器都为 0。这样遍历所有数字之后,出现三次的为 0,出现一次的就存放在记录器 b 中。

实现记录器 b 的方法:第一次碰到 x 记录,第二次碰到 x 变 0,实际上就是异或 b = b ^ x,但是第三次碰到 x 时 b 还是0,区别就只在于记录器 a 的值为 x ,而第一二次时 a 都为0,因此是 b = (b ^ x) & ~a ,对于记录器 a 同理。

268. 丢失的数字剑指 Offer 53 - II. 0~n-1中缺失的数字)(找出 0 - n 范围中没出现的那一个数

class Solution:
    def missingNumber(self, nums: List[int]) -> int:
        n = len(nums)
        ans = 0
        for i in range(n):
            ans = ans ^ nums[i] ^ i
        ans ^= (i + 1)  # 正常的数组,包括 n
        return ans

方法一:正常的数组求和减去缺失的数组求和,差值就是缺失的数;

方法二:正常的数组与缺失的数组做异或,相同的数会异或为0,剩下的就是缺失的数。

448. 找到所有数组中消失的数字找出多个 1 - n 范围中没出现的数

class Solution:
    def findDisappearedNumbers(self, nums: List[int]) -> List[int]:
        n = len(nums)
        for num in nums:
            x = (num - 1) % n  # 由于是表示下标,所以 num - 1
            nums[x] += n       # 加上 n 不会改变其对 n 取余数的结果
        ans = [i + 1 for i, num in enumerate(nums) if num <= n]  # 没有被加上 n 的下标就是数组里没有的
        return ans

由于是多个数没出现,所以不能简单地用异或解决,用集合固然可以做,但是更优化地是把原数组本身作为集合,利用值与索引之间的映射关系来找出目标数。此题中,我们把数组中出现了的值对应的下标都加上 n,则没有被加上 n 的下标就是数组里没有的。

442. 数组中重复的数据找出多个 1 - n 范围中重复出现的数

class Solution:
    def findDuplicates(self, nums: List[int]) -> List[int]:
        n = len(nums)
        for num in nums:
            x = (num - 1) % n  # 由于是表示下标,所以 num - 1
            nums[x] += n       # 加上 n 不会改变其对 n 取余数的结果
        ans = [i + 1 for i, num in enumerate(nums) if num > n * 2]  # 被加上2次 n 的下标就是数组里重复的数
        return ans

与上一题同理,利用值与索引的对应关系,找出在数组中出现两次的值(其对应下标的值被两次加上了 n)。

287. 寻找重复数找出唯一的重复出现的数

class Solution:
    def findDuplicate(self, nums: List[int]) -> int:
        left = 1
        right = len(nums) - 1
        while left < right:
            mid = left + (right - left) // 2 
            cnt = 0 # 记录小于等于mid的元素个数
            for num in nums:
                if num <= mid:
                    cnt += 1
            if cnt > mid:
                right = mid
            else:
                left = mid + 1
        return left

这题比较特别,规定了不能修改数组 nums 且只用常量级 O(1) 的额外空间。给定一个包含 n + 1 个整数的数组,其数字都在 1 到 n 之间,只有一个数字是重复的。因此,对于某个数字 x 来说,正常来说小于等于 x 的数字应该有 x 个,例如有1、2、3、4共4个数字小于等于4,如果大于4了,则说明1、2、3、4其中有一个数字重复了,所以右边界左移,反之左边界右移。此为二分数值型

540. 有序数组中的单一元素剑指 Offer II 070. 排序数组中只出现一次的数字)(找出有序数组的唯一不重复的数

class Solution:
    def singleNonDuplicate(self, nums: List[int]) -> int:
        left = 0
        right = len(nums) - 1
        while left < right:
            mid = left + (right - left) // 2
            if mid % 2 == 1:   # 只考虑偶数下标
                mid -= 1
            if nums[mid] == nums[mid + 1]:  # 如果它和下一个数相同,说明还正常,单一元素在右边区间
                left = mid + 2
            else:
                right = mid
        return nums[left]

这题用集合可以做到 O(n) 的时间,但是用二分可以做到 O(logn)。注意到,由于数组中只有一个不重复的数,所以总长度一定是奇数,而首尾下标都为偶数。又因为数组是有序的,所以重复数都是两两一起出现,且正常的情况都是(偶数索引,奇数索引),只有当出现那一个不重复的数(偶数索引),索引才会变成(奇数,偶数)。所以用二分索引法找到每个偶数下标,如果它和下一个数相同,则说明排序还是正常的,即单一元素在右边区间;否则,则说明排序已经不正常,单一元素在左边区间。

41. 缺失的第一个正数找出数组中没有出现的最小的正整数

class Solution:
    def firstMissingPositive(self, nums: List[int]) -> int:
        n = len(nums)
        for i in range(n):
            if nums[i] <= 0:
                nums[i] = n + 1
        
        for i in range(n):
            num = abs(nums[i])
            if num <= n:
                nums[num - 1] = -abs(nums[num - 1])
        
        for i in range(n):
            if nums[i] > 0:
                return i + 1
        
        return n + 1

用集合可以做,但是题目要求时间复杂度为 O(n) 并且只使用常数级别额外空间。实际上,对于一个长度为 N 的数组,其中没有出现的最小正整数只能在 [1, N+1] 中。这是因为如果 [1, N] 都出现了,那么答案是 N+1,否则答案是 [1, N] 中没有出现的最小正整数。所以我们的思路就是:不考虑负数,将它们设置为 n + 1;对于在 [1, N] 中的数(此时之前的负数为 n + 1,不会被考虑),将其值对应下标的数变为负(作为已经出现过了的标志);最后找出第一个不是负数的,其对应下标就是没出现的,即为答案。若所有都出现过,答案则为 n + 1。

在这里插入图片描述

  • 0
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值