力扣448 找到所有数组中消失的数字

博客围绕力扣448题“找到所有数组中消失的数字”展开。先给出题干和示例,接着解析解题方法,一是用哈希表(额外空间),使用Python集合,介绍不同删除元素方法;二是原地修改,利用列表索引代替“对照序列”,通过标记元素找出消失数字,最后进行小结。

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

力扣448 找到所有数组中消失的数字

题干

给定一个范围在 1 ≤ a[i] ≤ n ( n = 数组大小 ) 的 整型数组,数组中的元素一些出现了两次,另一些只出现一次。

找到所有在 [1, n] 范围之间没有出现在数组中的数字。

您能在不使用额外空间且时间复杂度为O(n)的情况下完成这个任务吗? 你可以假定返回的数组不算在额外空间内。

示例

输入:
[4,3,2,7,8,2,3,1]

输出: [5,6]

解析

说一下本人怎么理解本题的。

首先给定数组长度为n ,正常来说是可以满足 数字1-n各出现一次的,但是因为数组nums中有些元素出现了2次,导致一些元素“消失”了,即没有位置让它出现了。这就是题干中“消失的数字”的由来。

然后,本人想到用哈希的方法来解题,从 1 - n 组成的“对照序列”中删除那些nums中出现过的元素,剩下的即为答案。由于哈希表每次查询和删除的开销为O(1),所以一套下来的时间复杂度可以保证为O(N)

这里,我提出一种说法,叫做“对照序列”,即为当给定数组nums不出现重复元素时,数字 1-n 组成的序列。

1、哈希表(额外空间)

本题的哈希表使用Python集合即可。

class Solution:
    def findDisappearedNumbers(self, nums: List[int]) -> List[int]:
        # 获取n的值
        n = len(nums)
        # 通过集合 创建“对照序列”
        ns = set(range(1, n+1))
        # 遍历nums,从对照序列中删除出现过的元素
        for i in nums:
            ns.discard(i)
        # 对照序列中剩下的即为未出现过的“消失的数字”
        return ns

值得注意的是,删除集合元素的方法有好几个:
1、set.pop() 随机从集合中删除一个元素,显然不适用于本题

2、set.remove(x) 从集合中删除指定元素,若不存在(没找到)会报错。由于nums中部分元素出现2次,当第二次遍历到相同元素时,如果使用remove删除集合元素,那么就会报错,因此也不适用该方法。

3、set.discard(x) 从集合中删除指定元素,若不存在不会报错。非常适合本题,理由2中已述。

2、原地修改

本题的题干中有要求,能否不使用额外空间解题。而解法1是开辟长度为N的集合,因此不算是最优解。

其实,看了力扣官方的题解或是民间大神的题解,都可以理解这道题,关键是他们的题解都是上来直接讲正确的做法是怎么做,却没有说如何想到这种做法的,本人认为比起记住正确的做法,培养自己如何解题才是更重要的。

好了,回到本题。题解1中使用到的额外空间是“对照序列”,所以如果我们想不使用额外空间,那么就必须不使用“对照序列”。但是,对于解本题来说,“对照序列”又是必不可少的。这就陷入一个很尴尬的境地了。

抛开上述考量,之前遇到的题中,很多都会要求“不使用额外空间”,这些题往往都是提示“直接在原地修改”,那么本题是不是也可以直接在原地修改呢?或者说,我们必不可少的“对照序列”,是不是也可以在原数组中找到呢?

我觉得这里就是解题的关键,我们要是能意识到,在给定数组nums中,隐藏了我们需要的“对照序列”,那么就可以不使用额外空间解题。

说到这里应该很多人就反应过来了,我们需要的“对照序列”,完全可以通过列表的索引来代替。其实我们可以把列表这种数据结构看成上下两部分:
1、上部分:元素值序列
2、下部分:索引序列
这里列表的下半部分,正是天然的“对照序列”。

好,现在我们有了“对照序列”,那么如何从对照序列中”删除“出现过的元素呢?这里我在参考了力扣理解后想到了这样的方法:
遍历nums,把出现过的元素作为索引值进行索引,然后标记索引到的元素。
这样一来,当再次遍历nums时,凡是被“标记”过的元素,其索引值都出现过的元素,那么那么未“标记”的元素,其索引值就是未出现过的元素了。

这么说很难理解,配上例子会很好理解:

输入:
nums = [1, 1, 3]

遍历nums
1、num = 1 找到nums[0],标记nums[0]的值,这里可以把nums[0]变为负数,这里“变负数”不是取反,而是变为元素值绝对值的负数。
此时nums = [-1, 1, 3]
2、继续遍历nums,num = 1 , 再次标记nums[0]
此时nums = [-1, 1, 3]
3、继续遍历nums,num = 3,标记nums[2],把nums[2]变为负数
此时nums = [-1, 1, -3]

这时我们再看nums列表上部分和下部分的对应关系:
[ -1, 1, -3]
[ 0 ,1, 2]
被标记过的元素都为负数,其对应的索引都是出现过的元素,未被标记过的元素其索引都是未出现的数字。此时未标记元素为1,索引为1,故为出现的元素为1+1 =2

class Solution:
    def findDisappearedNumbers(self, nums: List[int]) -> List[int]:
        # 遍历数组,标记出现过的元素
        for num in nums:
            nums[abs(num)-1] = -abs(nums[abs(num)-1])
		# 题干提到,返回的数组不算额外空间
        results = list()
        # 再次遍历,查看哪些元素被标记过,哪些未标记过
        for i in range(len(nums)):
            if nums[i] > 0:
                results.append(i+1)
        return results

小结

本题题解2的关键是:
1、如何在原地找到解题必不可少的“对照序列”(将列表看作上下两部分)
2、如何在新的“对照序列”中“删除”出现过的元素(标记即视为删除)

本人悟出原地的对照序列时,采用的标记是将元素值统一改为-1,但是这样会出问题,因为改为-1的元素,就不能用来标记了,所以导致漏解,注意标记的正确姿势是将元素值改为原值绝对值的负数。

另外,值得吐槽的是,方法2更难想,用到的空间更少,但时间开销大于方法1。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值