原地哈希总结【Python】

一般情况下,我们使用哈希表解决的问题是「统计待查找的每项出现的次数」,实现方法是用Python中「字典」保存。需要借助额外的 O ( n ) O(n) O(n)空间来保存这个字典。

但是在一些题目中,明确要求「在不使用额外空间」的条件下实现,那么此时只能通过原地修改数组,来反映出原数组每项出现的次数。这就是「原地哈希」问题。

所谓原地哈希,就是建立「数组中待查找的项」和「对应下标」的映射关系,然后把所有项都放到应该放的位置。这就可以通过查看「恢复后数组中的项」和「其下标」是否满足该映射关系,来间接得到每个项出现的次数。


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

给定一个范围在 1 ≤ a[i] ≤ n ( n = 数组大小 ) 的 整型数组,数组中的元素一些出现了两次,另一些只出现一次。
找到所有在 [1, n] 范围之间没有出现在数组中的数字。
您能在不使用额外空间且时间复杂度为O(n)的情况下完成这个任务吗? 你可以假定返回的数组不算在额外空间内。

示例:
输入:
[4,3,2,7,8,2,3,1]
输出:
[5,6]

思路
如果不用原地哈希,那么很简单。就是额外开辟个空间保存数组中每项出现的次数,然后遍历[1,n],查看每项是否在哈希表中即可。但是不符合「不适用额外空间」的要求。

那么如何使用原地哈希呢?关键在于如何建立「项」和「下标」的映射关系。

建立映射:由于数组中每项范围都在1 ≤ a[i] ≤ n内,如果数组a刚好是[1,n]中的所有数,那么就刚好依次排满每个位置。所以我们可以将数a[i]放到下标为a[i]-1的位置,如把1放到下标为0的位置,完成对数组的恢复。
那么我们如何将数组进行恢复呢?我们可以对数组进行一次遍历,对于遍历到的数 x = n u m s [ i ] x=nums[i] x=nums[i],我们知道 x x x应当出现在数组中的 x − 1 x - 1 x1的位置,因此当 x x x n u m s [ x − 1 ] nums[x-1] nums[x1]不相等,就交换 x x x n u m s [ x − 1 ] nums[x-1] nums[x1],这样 x x x 就出现在了正确的位置。

统计次数:在交换完毕后,所有出现在数组中的数都会被放到下标为x-1的地方,我们遍历数组,判断当前「项」和「下标」是否满足映射关系,如果满足,说明该数出现了一次。如果不满足,说明:

  1. 该位置的数出现了不止一次
  2. 该下标对应的本应出现的数,在数组中没有出现。

a[1]=3,说明3出现了至少两次。 并且2没有出现在原数组中。

可以看到,原地哈希的条件是我们要查找的数都有一个确定的范围,一般来说都是在[1,n]内,这样映射比较好建立。原地哈希的时间复杂度是 O ( n ) O(n) O(n),空间复杂度是 O ( 1 ) O(1) O(1)

    def findDisappearedNumbers(self, nums):
        """
        找到所有在 [1, n] 范围之间没有出现在数组中的数字。
        :type nums: List[int]
        :rtype: List[int]
        """
        n = len(nums)
        if n <= 1:
            return []

        for i in range(n):
            while nums[i] != nums[nums[i] - 1]:
                nums[nums[i]-1], nums[i] = nums[i], nums[nums[i]-1]
        res = []
        for i in range(n):
            if nums[i] != i + 1:
                res.append(i+1)
        return res

442. 数组中重复的数据

给定一个整数数组 a,其中1 ≤ a[i] ≤ n (n为数组长度), 其中有些元素出现两次而其他元素出现一次。
找到所有出现两次的元素。
你可以不用到任何额外空间并在O(n)时间复杂度内解决这个问题吗?

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

同上面的方法,此时求的是出现两次的元素,由于题目中还说数组中每个数只会出现两次或一次,那么对于恢复后的数组,不满足映射关系的项,该位置的数就是出现两次的数。所以最后返回该位置的数即可。

    def findDuplicates(self, nums):
        """
        因为规定了数组中每个数的范围是不超过数组长度n的正整数,所以可以原地哈希
        通过「交换」将值换到下标处。最后遍历数组,和下标没有相差1的数就是重复的
        """
        n = len(nums)
        if n <= 1:
            return []

        for i in range(n):
            while nums[i] != nums[nums[i]-1]:
                nums[nums[i]-1], nums[i] = nums[i], nums[nums[i]-1]

        res = []
        for i in range(n):
            if nums[i] != i + 1:
                res.append(nums[i])
        return res

41. 缺失的第一个正数

给你一个未排序的整数数组,请你找出其中没有出现的最小的正整数。

示例 1:
输入: [1,2,0]
输出: 3

示例 2:
输入: [3,4,-1,1]
输出: 2

思路:
对于一个长度为 n n n的整数数组,我们要知道其中没有出现的最小正整数的范围是[1, n+1]。这是因为如果[1,n]在数组中都出现了,那么答案就是n+1,如果[1,n]有数没出现,那么答案就是该数。所以我们要查找的数就在[1,n+1]内,目标就是遍历[1,n+1],查看是否出现在原数组中。

对于原地哈希方法,由于数组长度是n,最后n+1这个元素不需要我们查找,因为当前面n个元素都查找到时,就可以自动返回这个元素。
所以我们就将「原数组中在[1,n]内」的数放到合适的位置,然后遍历数组,遍历到第一个不符合映射关系的位置,那么返回该位置对应的数即可。

    def firstMissingPositive(self, nums):
        """
        原地哈希,因为我们知道最小正整数范围是[1,n+1],而n+1是前面n个元素都找不到的情况下返回的,所以不用哈希。
        所以我们可以对原数组的下标和该位置的数建立一个哈希映射,就是位置i存放值为i+1的数。
        那么我们遍历原数组,如果nums[i]在[1,n]内,就将nums[i]放到合适的位置,即下标为nums[i]-1的地方。这里用交换操作
        """
        n = len(nums)

        for i in range(n):
            # 当 这个数在[1,n]内,且这个数和将要交换去的位置的数不相等,就交换
            while 1 <= nums[i] <= n and nums[i] != nums[nums[i] - 1]:
                nums[nums[i] - 1], nums[i] = nums[i], nums[nums[i] - 1]  # 交换顺序不能变!!!
                # 因为这种置换语句是先计算等式右边的所有值,然后从左至右依次赋值给等式左边

        for i in range(n):
            if nums[i] != i + 1:
                return i + 1

        return n + 1

总结一下,原地哈希题目的基本步骤如下:

  1. 判断条件:原地哈希的「条件」是我们要统计查找的数在一定的范围内,一般来说都是在[1,n]内。
  2. 恢复数组:然后我们就可以通过「置换」将数组中符合这个范围的数放到合适的位置,恢复数组。
  3. 遍历查找:最后按顺序遍历恢复后的数组,「查看项和下标是否满足映射关系」,来决定我们要查找的数出现的次数。
  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值