LeetCode面试经典150题 - 1. 数组、字符串题解记录(持续更新中)

LeetCode面试经典150题 - 1. 数组、字符串题解记录(持续更新中)

面试经典 150 题 - 学习计划 - 力扣(LeetCode)全球极客挚爱的技术成长平台

  1. 88. 合并两个有序数组 - 力扣(LeetCode)

    方法一:直接合并sort,注意:使用了切片赋值 nums1[:],表示对整个 nums1 列表的所有元素进行替换。这样做不会改变 nums1 这个对象的引用,而是在原有对象内更新数据,从而外部对这个对象的引用也会看到更新后的内容。

    class Solution:
        def merge(self, nums1: List[int], m: int, nums2: List[int], n: int) -> None:
            """
            Do not return anything, modify nums1 in-place instead.
            """
            # 使用了切片赋值 nums1[:],表示对整个 nums1 列表的所有元素进行替换。这样做不会改变 nums1 这个对象的引用,而是在原有对象内更新数据,从而外部对这个对象的引用也会看到更新后的内容。
            nums1[:] = nums1[:m] + nums2[:]
            nums1.sort()
    
    

    方法二:倒序双指针原地修改nums1

    class Solution:
        def merge(self, nums1: List[int], m: int, nums2: List[int], n: int) -> None:
            """
            Do not return anything, modify nums1 in-place instead.
            """
            if n == 0:
                return
            # 倒序双指针原地修改nums1
            p1 = m - 1
            p2 = n - 1
            for i in range(m + n - 1, -1, -1):
                if p1 >= 0 and nums1[p1] >= nums2[p2]:
                    nums1[i] = nums1[p1]
                    p1 -= 1
                elif p2 >= 0:
                    nums1[i] = nums2[p2]
                    p2 -= 1
    
  2. 27. 移除元素 - 力扣(LeetCode)

    快慢双指针

    class Solution:
        def removeElement(self, nums: List[int], val: int) -> int:
            ori_len = len(nums)
            fp = 0 # fast p
            sp = 0 # slow p
            for fp in range(ori_len):
                if nums[fp] != val:
                    nums[sp] = nums[fp]
                    sp += 1
            return sp
    
  3. 26. 删除有序数组中的重复项 - 力扣(LeetCode)​

    快慢双指针

    class Solution:
        def removeDuplicates(self, nums: List[int]) -> int:
            sp = 1
            fp = 1
            while fp < len(nums):
                if nums[fp] == nums[fp-1]:
                    fp += 1
                else:
                    nums[sp] = nums[fp]
                    sp += 1
                    fp += 1
    
            return sp
    
  4. 80. 删除有序数组中的重复项 II - 力扣(LeetCode)

    快慢双指针:

    class Solution:
        def removeDuplicates(self, nums: List[int]) -> int:
            count = 1
            sp = 1 # slow p
            for fp in range(1, len(nums)):
                if nums[fp] == nums[fp-1]:
                    count += 1
                    if count <= 2:
                        nums[sp] = nums[fp]
                        sp += 1
                else:
                    count = 1 # 注意这里是count赋值为1,因为当前fp处元素已经计入长度
                    nums[sp] = nums[fp]
                    sp += 1
            return sp
    

    更简洁的写法:看成栈,更新sp处nums相当于入栈

    class Solution:
        def removeDuplicates(self, nums: List[int]) -> int:
            sp = 2 # slow p从索引2开始
            for fp in range(2, len(nums)):
                if nums[fp] != nums[sp-2]: # 注意这里是fp与sp-2相比较,因为sp相当于当前栈长度,sp处相当于栈顶,fp需要与栈顶的前面第二个入栈元素比较是否相等即可实现跳过出现次数超过两次的元素
                    nums[sp] = nums[fp]
                    sp += 1
            return min(sp, len(nums))
    
  5. 169. 多数元素 - 力扣(LeetCode)

    注意题中暗示数组中仅有一个数满足题意

    class Solution:
        def majorityElement(self, nums: List[int]) -> int:
            nums.sort()
            maxCount = len(nums) // 2
            for fp in range(maxCount, len(nums)):
                if nums[fp] == nums[fp-maxCount]:
                    return nums[fp]
    

    简化方法:根据数学关系来,多数元素在排序后的数组中,必定出现在中间位置(即索引 n/2 处),因为它的出现次数超过了一半

    class Solution:
        def majorityElement(self, nums: List[int]) -> int:
            nums.sort()
            maxCount = len(nums) // 2
            # 多数元素在排序后的数组中必定出现在中间位置(即索引 n/2 处),因为它的出现次数超过了一半
            return nums[maxCount]
    
  6. 189. 轮转数组 - 力扣(LeetCode)

    叠加nums,然后在其中取正确的切片即可:

    class Solution:
        def rotate(self, nums: List[int], k: int) -> None:
            """
            Do not return anything, modify nums in-place instead.
            """
            k = k % len(nums) # 防止超出长度
            nums[:] = (nums + nums)[len(nums) - k : 2 * len(nums) - k] # 注意索引!开头的是len(nums) - k
    

    **原地做法**​ (空间O(1),翻转三次!整体翻转 -> 前k个元素翻转 -> 后n-k个元素翻转):

    # 注:请勿使用切片,会产生额外空间
    class Solution:
        def rotate(self, nums: List[int], k: int) -> None:
            def reverse(i: int, j: int) -> None:
                while i < j:
                    nums[i], nums[j] = nums[j], nums[i]
                    i += 1
                    j -= 1
    
            n = len(nums)
            k %= n  # 轮转 k 次等于轮转 k % n 次
            reverse(0, n - 1)
            reverse(0, k - 1)
            reverse(k, n - 1)
    
  7. 121. 买卖股票的最佳时机 - 力扣(LeetCode)​

    方法一:

    收益一变负,就重置,重新开始买卖

    class Solution:
        def maxProfit(self, prices: List[int]) -> int:
            # 只能买卖一次
            maxProfit = 0
            profit = 0 # 当前profit
            for i in range(1, len(prices)):
                profit += (prices[i] - prices[i-1])
                if profit < 0: # 如果当前profit更新变负,说明要重置profit
                    profit = 0
                elif profit > maxProfit: # 更新最大profit
                    maxProfit = profit
            return maxProfit
    

    方法二:

    记录当前买入最低价,然后不断更新最高收益

    class Solution:
        def maxProfit(self, prices: List[int]) -> int:
            # 只能买卖一次
            minCost = float('inf') # 当前遇到的最低买入价格
            maxProfit = 0
            for i in range(len(prices)):
                minCost = min(minCost, prices[i])
                maxProfit = max(maxProfit, prices[i] - minCost)
            return maxProfit
    

    动规dp做法:

    一定要记住dp数组的定义在这种题目中应该如何设置!

    class Solution:
        def maxProfit(self, prices: List[int]) -> int:
            # dp0:表示当天 **持有股票** 时的最大收益(最优方案)。
            # dp1:表示当天 **不持有股票** 时的最大收益(最优方案)。
            # 初始化第0天的,以开始递推
            dp0, dp1 = -prices[0], 0
    
            for i in range(1, len(prices)): # 第i天,要从索引1天开始
                old_dp0 = dp0
                # 今天持有:要么昨天已经在持有,要么之前从来没买过(手上现金额没使用过,还是0)今天刚买入
                dp0 = max(old_dp0, -prices[i])
                # 今天不持有:要么昨天也不持有,要么昨天持有今天刚卖出
                dp1 = max(dp1, old_dp0 + prices[i])
    
            return max(dp0, dp1)
    
  8. 122. 买卖股票的最佳时机 II - 力扣(LeetCode)

    个人比较喜欢这么做,dp做法不如这样简单

    class Solution:
        def maxProfit(self, prices: List[int]) -> int:
            profit = 0
            for i in range(1, len(prices)):
                if prices[i] > prices[i-1]:
                    profit += (prices[i] - prices[i-1])
            return profit
    
  9. 55. 跳跃游戏 - 力扣(LeetCode)

    逐步更新右边界即可

    class Solution:
        def canJump(self, nums):
            rightBound = 0
            leftP = 0 # 扩展的指针,必须在边界内
            while leftP <= rightBound and leftP < len(nums): # 注意终止条件是到达右边界!
                new_rightBound = nums[leftP] + leftP
                if new_rightBound > rightBound: # 逐步拓展右边界
                    rightBound = new_rightBound
                if rightBound >= len(nums) - 1:
                    return True
                leftP += 1
            return False
    
  10. 45. 跳跃游戏 II - 力扣(LeetCode)

    可以用层序遍历来理解: 将问题视作分层跳转,其实相当于每一层跳转的起始点范围对应的索引范围为[leftP, curRightBound]​。

    在当前层更新NextRightBound​,相当于将下一层入队列!

    然后minJump​统计的是层数。

    class Solution:
        def jump(self, nums: List[int]) -> int:
            # 一定要考虑好边界!因为笔试都是acm格式,样例非常少且看不到提交后问题出在哪里!!!
            if len(nums) == 1: return 0
    
            minJump = 0 # 最小跳跃次数
            curRightBound = 0 # 当前拓展层级的最右边界
            NextRightBound = 0
            leftP = 0 # 当前指针位置
            while leftP <= curRightBound and leftP < len(nums):
                minJump += 1 # 进行跳转
                for i in range(leftP, curRightBound + 1): # 当前拓展层级的跳转起始点的索引范围
                    NextRightBound = max(NextRightBound, nums[i] + i)
                    if NextRightBound >= len(nums) - 1:
                        return minJump
                # 更新
                curRightBound = NextRightBound
                leftP = i
            return False
    
  11. 274. H 指数 - 力扣(LeetCode)

    先降序排序,然后h从大到小遍历寻找是否有合适的h;

    h的初始值一定是length,因为可以先假定当前至少发了length篇文章,且至少有length篇文章的被引次数大于等于length。

    if citations[h-1] >= h​是关键判断!

    class Solution:
        def hIndex(self, citations: List[int]) -> int:
            # if len(citations) == 1: return min(1, citations[0])
    
            length = len(citations)
            citations.sort(key=lambda x: -x) # 降序排列
            h = length # 初始化h
            while h >= 0:
                if citations[h-1] >= h:
                    return h
                else:
                    h -= 1
            return h
    

    时间上更优秀的做法:

    新建并维护一个数组 counter 用来记录当前引用次数的论文有几篇,然后再倒序统计大于k的论文数量

    class Solution:
        def hIndex(self, citations: List[int]) -> int:
            n = len(citations)
            cnt = [0] * (n + 1)
            for c in citations:
                cnt[min(c, n)] += 1  # 引用次数 > n,等价于引用次数为 n
            s = 0
            for i in range(n, -1, -1):  # i=0 的时候,s>=i 一定成立
                s += cnt[i]
                if s >= i:  # 说明有至少 i 篇论文的引用次数至少为 i
                    return i
    
  12. 380. O(1) 时间插入、删除和获取随机元素 - 力扣(LeetCode)

    利用python集合库函数

    注意该方法中:

    insert​ 和 remove​ 操作由于使用了 set​(基于哈希表)实现,通常能达到平均 O ( 1 ) O(1) O(1) 的时间复杂度;

    但是 getRandom​ 方法里,调用了 list(self.set)​ 将集合转换成列表,这一步需要 O ( n ) O(n) O(n) 的时间,其中 n n n 是集合的大小。

    因此,这样的实现不符合要求的所有操作都必须是 O ( 1 ) O(1) O(1) 时间复杂度。

    如果希望实现O(1) 的随机抽取,则需要使用支持下标访问的序列(例如列表)或者在插入时同时维护列表和集合的数据结构。

    import random
    class RandomizedSet:
    
        def __init__(self):
            self.set = set()
    
        def insert(self, val: int) -> bool:
            if val in self.set:
                return False
            self.set.add(val)
            return True
    
        def remove(self, val: int) -> bool:
            if val not in self.set:
                return False
            self.set.remove(val)
            return True
    
        def getRandom(self) -> int:
            return random.choice(list(self.set))
    
    
    # Your RandomizedSet object will be instantiated and called as such:
    # obj = RandomizedSet()
    # param_1 = obj.insert(val)
    # param_2 = obj.remove(val)
    # param_3 = obj.getRandom()
    

    正确题解(来自力扣官方题解):

    变长数组 + 哈希表

    这道题要求实现一个类,满足插入、删除和获取随机元素操作的平均时间复杂度为 O(1)。

    变长数组可以在 O(1) 的时间内完成获取随机元素操作,但是由于无法在 O(1) 的时间内判断元素是否存在,因此不能在 O(1) 的时间内完成插入和删除操作。

    哈希表可以在 O(1) 的时间内完成插入和删除操作,但是由于无法根据下标定位到特定元素,因此不能在 O(1) 的时间内完成获取随机元素操作。

    为了满足插入、删除和获取随机元素操作的时间复杂度都是 O(1),需要将变长数组和哈希表结合,变长数组中存储元素,哈希表中存储每个元素在变长数组中的下标。

    插入操作时, 首先判断 val 是否在哈希表中,如果已经存在则返回 false,如果不存在则插入 val,操作如下:

    • 在变长数组的末尾添加 val;
    • 在添加 val 之前的变长数组长度为 val 所在下标 index,将 val 和下标 index 存入哈希表;
    • 返回 true。

    删除操作时, 首先判断 val 是否在哈希表中,如果不存在则返回 false,如果存在则删除 val,操作如下:

    • 从哈希表中获得 val 的下标 index;

    • 将变长数组的最后一个元素 last 移动到下标 index 处,在哈希表中将 last 的下标更新为 index;

    • 在变长数组中删除最后一个元素,在哈希表中删除 val;

    • 返回 true。

    删除操作的重点在于将变长数组的最后一个元素移动到待删除元素的下标处,然后删除变长数组的最后一个元素。该操作的时间复杂度是 O(1),且可以保证在删除操作之后变长数组中的所有元素的下标都连续,方便插入操作和获取随机元素操作。

    获取随机元素操作时,由于变长数组中的所有元素的下标都连续,因此随机选取一个下标,返回变长数组中该下标处的元素。

    import random
    class RandomizedSet:
        def __init__(self):
            self.nums = []
            self.indices = {}
    
        def insert(self, val: int) -> bool:
            if val in self.indices:
                return False
            self.indices[val] = len(self.nums)
            self.nums.append(val)
            return True
    
        def remove(self, val: int) -> bool:
            if val not in self.indices:
                return False
            id = self.indices[val]
            self.nums[id] = self.nums[-1]
            self.indices[self.nums[id]] = id
            self.nums.pop()
            del self.indices[val]
            return True
    
        def getRandom(self) -> int:
            return random.choice(self.nums)
    
  13. 238. 除自身以外数组的乘积 - 力扣(LeetCode)

你可能感兴趣的:(Mophead的小白刷题笔记,leetcode,python,面试经典150题)