在一维数组中的考察中,最常见的就是找出数组中的重复数、只出现一次的数或者丢失(消失)数等等。
一般来说,首先想到的就是用哈希表(集合)来记录出现过的数,基本所有的题都可以用集合来做,而技巧性在于有时可以把原数组自身作为哈希表;
其次就是位运算,原理是相同的数做异或运算 ^ 会得到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 Trueelse: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 numelse:temp.add(num)return -1
用集合轻松解决,但是可以不使用额外的集合,而是将原数组本身当作集合,通过交换使得数组的值与索引(下标)一一对应,若出现两个值对应同一个索引,即为重复的元素。
class Solution:def findRepeatNumber(self, nums: [int]) -> int:i = 0while i < len(nums):if nums[i] == i: # 值与索引已经相同,跳过i += 1continueif 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 = 1while h & ret == 0: # 从右边开始找第一位为1的(两个数不同的位)h <<= 1a, b = 0, 0# 分别异或for n in nums:if n & h: # n 在这一位是 1,一个组a ^= nelse: # n 在这一位是 0,另一个组b ^= nreturn [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 = 0b = 0for i in range(len(nums)):b = (b ^ nums[i]) & ~aa = (a ^ nums[i]) & ~breturn 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 = 0for i in range(n):ans = ans ^ nums[i] ^ ians ^= (i + 1) # 正常的数组,包括 nreturn 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 - 1nums[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 - 1nums[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 = 1right = len(nums) - 1while left < right:mid = left + (right - left) // 2 cnt = 0 # 记录小于等于mid的元素个数for num in nums:if num <= mid:cnt += 1if cnt > mid:right = midelse:left = mid + 1return 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 = 0right = len(nums) - 1while left < right:mid = left + (right - left) // 2if mid % 2 == 1: # 只考虑偶数下标mid -= 1if nums[mid] == nums[mid + 1]: # 如果它和下一个数相同,说明还正常,单一元素在右边区间left = mid + 2else:right = midreturn 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 + 1for 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 + 1return n + 1
用集合可以做,但是题目要求时间复杂度为 O(n) 并且只使用常数级别额外空间。实际上,对于一个长度为 N 的数组,其中没有出现的最小正整数只能在 [1, N+1] 中。这是因为如果 [1, N] 都出现了,那么答案是 N+1,否则答案是 [1, N] 中没有出现的最小正整数。所以我们的思路就是:不考虑负数,将它们设置为 n + 1;对于在 [1, N] 中的数(此时之前的负数为 n + 1,不会被考虑),将其值对应下标的数变为负(作为已经出现过了的标志);最后找出第一个不是负数的,其对应下标就是没出现的,即为答案。若所有都出现过,答案则为 n + 1。