Leetcode刷题:剑指offer【面试题03 数组中重复的数字】

本文探讨了在数组中查找重复数字的多种算法,包括排序、哈希表、原地哈希和二分查找。重点介绍了原地哈希和二分查找的实现细节和时间复杂度分析。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

【面试题03】数组中重复的数字

难度: 简单
要求: 找出数组中重复的数字
限制: 2 <= n <= 100000

在一个长度为 n 的数组 nums 里的所有数字都在 0~n-1 的范围内。数组中某些数字是重复的,但不知道有几个数字重复了,也不知道每个数字重复了几次。请找出数组中任意一个重复的数字。例如,如果输入长度为 7 的数组 {2, 3, 1, 0, 2, 5, 3},那么对应的输出是重复的数字 2 或者 3。

Leetcode题目对应位置: 面试题03:数组中重复的数字

思路一:排序

先对数组 nums 进行排序,然后从前向后遍历数组,看相邻元素中是否有相同的,相同即为我们要找的重复目标,直接 return。排序的目的就是将所有相同的数字挨到一起,这样就只需要和自己的下一位比较就能获取到重复值。

时间复杂度:O(nlogn),排序一个长度为 n 的数组需要 O(nlogn) 的时间。
空间复杂度:O(1)

特点:

  • 1)此方法改变了原列表,要注意题设条件。
  • 2)时间复杂度较高,不太适合题设关注时间复杂度的情况
# python
class Solution:
    def findRepeatNumber(self, nums) -> int:
        nums.sort()
        n = len(nums)
        for i in range(n):
            if nums[i + 1] == nums[i]:
                return nums[i]
        return -1

在这里插入图片描述
细节问题:

1)不要将 len(nums) 写在 for 循环里,避免每次循环都要计算数组长度
2)关于 python 自带的 sort 函数的实现机制、采用了什么排序算法参考以下文章:
python sort函数内部实现原理

思路二:哈希表

创建一个哈希表,以列表元素作为键值。若当前值不在哈希表内,则将哈希表对应位置置1,否则就是找到了重复值,直接return。

时间复杂度:O(n),最坏情况下需要遍历整个数组。
空间复杂度:O(n),需要用辅助空间创建哈希表。

第一种: 使用一个 List 做哈希表,因为题目说了元素范围不会超过 0~n-1,所以初始化一个长度为 n 的列表是完全能包含所有元素在内的。然后使用元素本身作为键值(key),让 key 对应的 value 具有辨识度即可。意思就是不论是将 value 作为一个状态置 1,还是作为一个计数器 +1 都可以。

比较推荐作为一个状态位,若 key 对应的 value 为 0 说明还没有碰到过这个数,将 value 置 1,否则说明已经重复了,直接 return。

class Solution:
    def findRepeatNumber(self, nums) -> int:
        n = len(nums)
        table = [0] * n
        for i in range(n):
            if table[nums[i]] == 0:
                table[nums[i]] = 1       # 作为状态位置1
            else:
                return nums[i]
        return -1

class Solution:
    def findRepeatNumber(self, nums) -> int:
        c = [0] * len(nums)
        for i in range(len(nums)):
            c[nums[i]] += 1        # 作为计数器
            if c[nums[i]] > 1:
                return nums[i]
        return -1

在这里插入图片描述
第二种: 直接用 python 的字典做哈希表,因为字典的 key 本身就会被散列到一个地址上,所以也能实现 O(1) 的查找速度。

# python
class Solution:
    def findRepeatNumber(self, nums: List[int]) -> int:
        repeatDict = {}
        for num in nums:
            if num not in repeatDict:
                repeatDict[num] = 1
            else:
                return num

在这里插入图片描述
特点:

  • 1)哈希表方法需要额外的辅助空间,是一种以空间换时间的思路
  • 2)不需要修改原数组

思路三:原地哈希 / 鸽巢原理 ⭐

由于题目中给了条件说列表元素值不会超过 列表长度 - 1,所以可以用列表的位置 i i i 来存放元素 i i i。假设这个数组中没有重复的数字,那么数组排序之后数字 i i i 就会出现在下标为 i i i 的位置。由于实际上有重复数字,那么数组排序之后有些位置可能没有数字,而有些位置可能存在多个数字。那么只要进行原地排序,一旦发现某个位置 i i i 有第二个数值要存进来,就说明该数字是重复的。具体实现如下:

1)如果位置 i i i 的元素值 m m m 刚好等于 i i i,则不做处理继续扫描下一个元素;
2)若位置 i i i 的元素值 m m m 不等于 i i i,则将位置 i i i 上的元素与位置 m m m 上的元素做交换,使得位置 m m m 的索引和元素都等于 m m m
3)当要将位置 i i i 上的元素 m m m 放到正确位置 m m m 上时,若发现位置 m m m 上的元素就是 m m m,则说明已经找到了重复的元素,直接 return。

时间复杂度:O(n)
空间复杂度:O(1),不需要额外的辅助空间。

class Solution:
    def findRepeatNumber(self, nums: List[int]) -> int:
        n = len(nums)
        for i in range(n):
            while nums[i] != i:        # 这里必须是while
                if nums[nums[i]] == nums[i]:
                    return nums[i]
                nums[nums[i]], nums[i] = nums[i], nums[nums[i]]

细节问题:

1)注意上面 for 循环内部第一层必须是 while,不能写成 if,否则会漏判,比如测试用力 [1, 2, 4, 3, 2] 用 if 会漏掉 2。这是因为在顺序遍历数组 nums 时,每遇到一个数组元素 nums[i] 是将该元素值正确归位到数组的 num[i] 位置,即将 nums[i] 归位,而不是将数值 i 归位到位置 i,所以用 if 代替 while 就会出现漏判。

2)语句 nums[nums[i]], nums[i] = nums[i], nums[nums[i]] 实际上就是做两个数的互换。在 python 中 a, b = b, a 就相当于 t1 = a, t2 = b, a = t2, b = t1,这是因为 python 和 C 不同,在C语言中,系统是为每个变量分配内存空间。而在python中,是为每个值分配内存空间。具体参考这篇文章:Python心法:a,b=b,a原理

3)语句 nums[nums[i]], nums[i] = nums[i], nums[nums[i]] 不可以写反成 nums[i], nums[nums[i]] = nums[nums[i]], nums[i],因为 nums[i] 如果先做了改变,到了 nums[nums[i]] 这个索引已经不对了,指向了其他位置,会形成死循环。

在这里插入图片描述
特点:

  • 1)将数组本身作为哈希表,利用了题设中数组元素不会超过 0~n-1 这个条件
  • 2)不需要额外的存储空间,但会修改原数组

借用剑指 offer 原书上给的例子帮助理解:

以数组 {2, 3, 1, 0, 2, 5, 3} 为例。数组的第 0 个数字是 2,与其下标(0)不等,故将它与下标为 2 的数字(1)进行交换,得到交换后的数组 {1, 3, 2, 0, 2, 5, 3}。此时数组第 0 个数字是 1,与其下标(0)仍不等,故再次将它与下标为 1 的数字(3)进行交换,得到交换后的数组 {3, 1, 2, 0, 2, 5, 3}。仍然不等,再次交换第 0 个数字和第 3 个数字,得到 {0, 1, 2, 3, 2, 5, 3}。此时第 0 个数字是 0,已经正确归位,扫描下一个数字。在接下来的几个数字中,第 1、2、3 个数字的下标和数值都相等,所以不做处理。继续扫描到第 4 个数字是 2,由于其下标和数值不等,所以准备交换第 4 个数字和第 2 个数字,此时发现第 2 个数字已经是 2,也就是数字 2 在下标为 2 和下标为 4 的两个位置都出现了,因此找到了重复的数字 2。

思路四:二分查找 ⭐

假如题目再添加额外要求: 1)不使用额外的存储空间,2)不允许修改原数组,那么前三种方法都无法满足要求,此时考虑一种以时间换空间的方法,即二分查找。

我发现剑指 offer 给出的例子无法找到类似 [0, 1, 2, 0, 4, 5, 6, 7, 8, 9] 这样用例的正确答案,主要原因是第一次二分后,0~4 这个范围在测试数组中有 5 个数,此时算法不能确定这 5 个数是无重复的情况(0, 1, 2, 3, 4 )还是有重复的情况(0, 1, 2, 0, 4),最终找到的答案是不对的。

官方代码: 03_02_DuplicationInArrayNoEdit/FindDuplicationNoEdit.cpp

我在官方代码中添加了一个测试用例 test11,发现无法通过测试:

void test11()
{
    int numbers[] = { 0, 1, 2, 0, 4, 5, 6, 7, 8, 9 };
    int duplications[] = { 0 };
    test("test11", numbers, sizeof(numbers) / sizeof(int), duplications, sizeof(duplications) / sizeof(int));
}

所以我重写了一个代码逻辑:

由于第一次二分可能无法确定重复的数字出现再哪一段里,所以可以考虑逐渐缩小段的长度,同时增加段的个数来寻找。在确定了重复数字所在段的情况下,再利用二分法在该段寻找,就不会掉进上面用例的坑里。需要注意的是,这里不是对数组 nums 分段,而是对数据范围 0 ~ n-1 分段。

1)第一次将 0 ~ n-1 分为 2 段 [low_1, high_1][low_2, high_2]。依次对这两段数据统计在数组 nums 中,该数据范围中的数字出现的次数,寻找是否存在 数字个数 > high - low + 1 的情况,若存在说明重复数字就在这段里,直接 return low 和 high,后面的段不需要再统计。
2)若两段都统计过后,没有找到数字个数大于 high - low + 1 的情况,则细化段的长度。此时将 0 ~ n-1 分为 3 段。依次对这三段数据做与第一步相同的操作。
3)最坏情况下,需要将 0 ~ n-1 分为 n 段,即每个数字自成一段,此时实际上就是对每个数字单独统计在数组 nums 中出现的次数。

时间复杂度:O(n^3)
空间复杂度:O(1)

class Solution:
    def findRepeatNumber(self, nums) -> int:
        sec_num = len(nums)
        # 确定重复数字所在段[low, high]
        for i in range(2, sec_num + 1):
            low, high = self.findSec(nums, i)
            if low != -1:   # 找到了重复数字所在段
                break
        # 二分法查找重复数字
        while low < high:
            mid = int(high - low - 1 + low)
            c = self.countRange(nums, low, mid)
            if c > (mid - low + 1):
                high = mid
            else:
                low = mid + 1
        return low

    def findSec(self, nums, sec):
        length = int(len(nums) / sec)
        for i in range(sec):
            low = i * length
            high = i * length + length - 1
            if i == sec - 1:
                high = len(nums) - 1
            c = self.countRange(nums, low, high)
            if c > (high - low + 1):
                return low, high
        return -1, -1

    def countRange(self, nums, low, high):
        count = 0
        for i in nums:
            if i >= low and i <= high:
                count += 1
        return count

注意:由于这个思路是在加了约束条件的情况下写出来的,所以放在leetcode下运行会报超出时间限制,我是在 pycharm 里进行测试的。

给出几个测试用例:

nums = [0, 1, 2, 0, 4, 5, 6, 7, 8, 9]
nums = [1, 1, 1]
nums = [0, 1, 2, 4, 4]

二分法这个解决方案是一种时间换空间的方法,写出来的时间复杂度也是非常高了… 目前我也没有想到更好的解决方案,如果有大佬想到了更好的思路,希望多多指教呀~ 🙆‍♀️


参考资料:
[1] LeetCode 面试题03:数组中重复的数字
[2] python sort函数内部实现原理
[3] Python心法:a,b=b,a原理
[4] 剑指 offer 第二版

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

不吃饭就会放大招

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值