训练营第六天| 哈希表理论基础 242.有效的字母异位词 349. 两个数组的交集 202. 快乐数 1. 两数之和 | Sundri

哈希表理论基础

常见的三种哈希结构

当我们想使用哈希法来解决问题的时候,我们一般会选择如下三种数据结构。

  • 数组
  • set (集合)
  • map(映射)

242.有效的字母异位词(哈希数组)

# 解题思路:

1.创建一个长度为26的数组 arr,所有index对应的值都初始化为0

2.遍历字符串s,找到每个字母在数组里面的相对位置,数量每次增加1: arr [ord(s[i]) - ord('a')] += 1

3.遍历字符串t,找到每个字母在数组里面的相对位置,数量每次减少1, arr [ord(t[i]) - ord('a')] -= 1

4. 遍历字符串,如果找到一个非0的值,就说明s和t不是异位词,否则返回true

# 复杂度分析:

T: O(N) N是字符串长度

S: O(S) S=26,相当于字符集的大小,可以看成是O1)

def isAnagram(self, s: str, t: str) -> bool:
        if len(s) != len(t): return False
        bucket = [0] * 26
        for i in range(len(s)):
            bucket[ord(s[i]) - ord('a')] += 1
            bucket[ord(t[i]) - ord('a')] -= 1
        for i in range(len(bucket)):
            if bucket[i] != 0:
                return False 
        return True

349. 两个数组的交集

# 面试备注

2022年11月linkedin面试题,面试官给我的是两个已经升序排列的数组,让我求交集;

follow up问题是:如果数组里面有重复的元素,如何能够做到去重

# 方法一:哈希表法 

class Solution:
    def intersection(self, nums1: List[int], nums2: List[int]) -> List[int]:
        d = collections.defaultdict()
        res = []
        for num in nums1:
            if num not in d:
                d[num] = 1
        for num in nums2:
            if num in d and d[num] != 0:
                res.append(num)
                d[num] -= 1
        return res

# 时间空间复杂度:

T:max(m,n) 时间复杂度是两个数组里面取最大值

S:max(m,n) 空间复杂度是把其中一个数组放入哈希表的值,也是两个数组的size中取最大值

# 方法二:集合 set

# 解题思路

如果使用哈希集合存储元素,则可以在 O(1) 的时间内判断一个元素是否在集合中,从而降低时间复杂度。

首先使用两个集合分别存储两个数组中的元素,然后遍历较的集合,判断其中的每个元素是否在另一个集合中,如果元素也在另一个集合中,则将该元素添加到返回值。该方法的时间复杂度可以降低到 O(m+n)。

def intersection(self, nums1: List[int], nums2: List[int]) -> List[int]:
        set1 = set(nums1)
        set2 = set(nums2)
    
        if len(set1) > len(set2):
            return [num for num in set1 if num in set2]
        return [num for num in set2 if num in set1]

# 为什么是遍历较小的集合?

因为这样能够让遍历的时间复杂度尽可能的降低。遍历较小的集合(O(m)),查询较大的集合(O(1)),时间复杂度就是较小的集合。如果反过来,时间复杂度就会取决于较大的集合。所以用集合做这个题目的时候,一定要去思考:两个数组的大小关系是怎么样的。

# 方法三:排序+双指针(最应该掌握的方法)

# 解题思路

如果两个数组是有序的,则可以使用双指针的方法得到两个数组的交集。

首先对两个数组进行排序,然后使用两个指针遍历两个数组。可以预见的是加入答案的数组的元素一定是递增的,为了保证加入元素的唯一性,我们需要额外记录变量 pre 表示上一次加入答案数组的元素(在python里面,pre 可以用 res[-1]来进行表示,回想下 time schedule的题目例子)。

初始时,两个指针分别指向两个数组的头部。每次比较两个指针指向的两个数组中的数字,如果两个数字不相等,则将指向较小数字的指针右移一位,如果两个数字相等,且该数字不等于 pre ,将该数字添加到答案并更新 pre 变量,同时将两个指针都右移一位。当至少有一个指针超出数组范围时,遍历结束。

# 复杂度分析

时间复杂度:

O(mlogm+nlogn),其中 m 和 n 分别是两个数组的长度。对两个数组排序的时间复杂度分别是 O(mlogm) 和 O(nlogn),双指针寻找交集元素的时间复杂度是O(m+n),因此总时间复杂度是 O(mlogm+nlogn)

空间复杂度:

O(logm+logn),其中 m 和 n 分别是两个数组的长度。空间复杂度主要取决于排序使用的额外空间。(如果题目已经完成排序,空间复杂度就是 O(1),不算输出数组的大小)

# 如果排序的两个方法是并列的,时间复杂度应该是相加,还是两者取较大值?

在leetcode给的官方题解里面,因为两个sort是平行的关系,所以总的时间复杂度应该是两个sort的时间复杂度之和。可以思考成线程的一个关系,程序要先执行这个条件,再执行下一个条件,所以时间一定是相加,而不是取两个sort时间里面的较大值!一定要更正这个思维误区!总而言之:平行关系的时间复杂度,在数学上是相加的关系!

def intersection(self, nums1: List[int], nums2: List[int]) -> List[int]:
        nums1.sort()
        nums2.sort()
        idx1, idx2 = 0,0
        res = []
        while idx1 < len(nums1) and idx2 < len(nums2):
            if nums1[idx1] < nums2[idx2]:
                if not res or res[-1] != nums1[idx1]:
                    res.append(nums1[idx1])
                    idx1 += 1
            elif nums1[idx1] > nums2[idx2]:
                if not res or res[-1] != nums1[idx2]:
                    res.append(nums2[idx2])
                    idx2 += 1
            else:
                res.append(nums1[idx1])
                idx1 += 1
                idx2 += 1
        return res

202. 快乐数(** 易错题,双指针做法好秀!)

# 方法一:集合

class Solution:
    def isHappy(self, n: int) -> bool:
        seen = set()
        while n != 1:
            number = self.calculate_sum(n)
            if number in seen:
                return False
            if number == 1:
                return True 
            if number not in seen and number != 1:
                seen.add(number)
            n = number
        return True # 最后还是要return true,因为要考虑到 n = 1 的情况! 
        
    
    def calculate_sum(self, n):
        cur_sum = 0
        while n:
            cur_sum += (n % 10) * (n % 10)
            n //= 10
        return cur_sum

# 易错点:

1. 这个题我做了很多次都做错,主要原因是在于,这个n有两个功能:

一、需要一个while循环把n遍历到0为止,这样可以把n所有位置上面的digit的平方相加,计算出新一层所需要的n。 -- 这个部分就是 getNext() 函数所实现的功能

二、需要把新的这个n重新放到一个while循环去遍历,并且每次都对这个n进行判断:

a)n == 1?如果相等,直接返回 true

b) n != 1 但是 n 已经出现过? 说明已经陷入了死循环,直接返回 False

c)n != 1 且 n 没有出现过:把 n 加入到 seen 的集合当中,继续重复遍历

class Solution:
    def isHappy(self, n: int) -> bool:
        seen = set()
        while n != 1:
            number = self.getNext(n)
            if number in seen:
                return False
            if number == 1:
                return True 
            if number not in seen and number != 1:
                seen.add(number)
            n = number
        return True # 最后还是要return true,因为要考虑到 n = 1 的情况! 
        
    
    def getNext(self, n):
        cur_sum = 0
        while n:
            cur_sum += (n % 10) * (n % 10)
            n //= 10
        return cur_sum

# 方法二:双指针(卧槽,被秀到了...)

通过反复调用 getNext(n) 得到的链是一个隐式的链表。隐式意味着我们没有实际的链表节点和指针,但数据仍然形成链表结构。起始数字是链表的头 “节点”,链中的所有其他数字都是节点。next 指针是通过调用 getNext(n) 函数获得。

意识到我们实际有个链表,那么这个问题就可以转换为检测一个链表是否有环。因此我们在这里可以使用弗洛伊德循环查找算法。这个算法是两个奔跑选手,一个跑的快,一个跑得慢。在龟兔赛跑的寓言中,跑的慢的称为 “乌龟”,跑得快的称为 “兔子”。

不管乌龟和兔子在循环中从哪里开始,它们最终都会相遇。这是因为兔子每走一步就向乌龟靠近一个节点(在它们的移动方向上)。

# 实现的关键点:

1.slow指针从 n 开始,fast指针从 getNext(n) 开始

2. slow指针每次走一步, fast指针每次走两步 (getNext(getNext(n)))

def isHappy(self, n):
        def hasNext(n):
            cur_sum = 0
            while n:
                cur_sum += (n % 10) * (n % 10)
                n //= 10
            return cur_sum
        
        slow, fast = n, hasNext(n)
        while slow != 1 and fast !=1 and slow != fast:
            # print("slow",slow,"fast",fast)
            slow = hasNext(slow) # slow 指针走一步
            fast = hasNext(hasNext(fast)) # fast指针走两步
            if slow == fast:
                return False
        return True

# 时间复杂度:

O(logn)。该分析建立在对前一种方法的分析的基础上,但是这次我们需要跟踪两个指针而不是一个指针来分析,以及在它们相遇前需要绕着这个循环走多少次。
如果没有循环,那么快跑者将先到达 1,慢跑者将到达链表中的一半。我们知道最坏的情况下,成本是 O(logn)。
一旦两个指针都在循环中,在每个循环中,快跑者将离慢跑者更近一步。一旦快跑者落后慢跑者一步,他们就会在下一步相遇。假设循环中有 k 个数字。如果他们的起点是相隔 k-1 的位置(这是他们可以开始的最远的距离),那么快跑者需要 k-1步才能到达慢跑者,这对于我们的目的来说也是不变的。因此,主操作仍然在计算起始 n 的下一个值,即O(logn)。


# 空间复杂度:

O(1),对于这种方法,我们不需要哈希集来检测循环。指针需要常数的额外空间。


1. 两数之和

def twoSum(self, nums: List[int], target: int) -> List[int]:
        d = collections.defaultdict()
        res = []
        for idx, num in enumerate(nums):
            if target - num in d:
                res.append(idx)
                res.append(d[target-num])
                return res
            else:
                d[num] = idx

你可能感兴趣的:(数据结构)