(面试经典刷题)挑战一周刷完150道-Python版本-第3天(40个题)-I(前10个题)

一、长度最小的子数组

给定一个含有 n 个正整数的数组和一个正整数 target 。

找出该数组中满足其总和大于等于 target 的长度最小的 连续子数组 [numsl, numsl+1, …, numsr-1, numsr] ,并返回其长度。如果不存在符合条件的子数组,返回 0 。
(面试经典刷题)挑战一周刷完150道-Python版本-第3天(40个题)-I(前10个题)_第1张图片
可以是暴力解法
也可以是滑动窗口。可以降低复杂度。

class Solution:
    def minSubArrayLen(self, target: int, nums: List[int]) -> int:
        if nums is None or len(nums)==0:
            return 0
        lenf=len(nums)+1
        total =0
        i=j=0
        while (j<len(nums)):
            total=total+nums[j]
            j+=1
            while(total>=target):
                lenf=min(lenf,j-i)
                total=total-nums[i]
                i+=1
        if lenf==len(nums)+1:
            return 0
        else:
            return lenf

这段代码通过滑动窗口的方法,不断调整子数组的左右边界,从而找到满足条件的最小子数组的长度。这是一个高效的解决方案,其时间复杂度为 O(N),其中 N 是输入数组的长度。

二、无重复字符的最长子串

这段代码通过滑动窗口的方法,不断调整子数组的左右边界,从而找到满足条件的最小子数组的长度。这是一个高效的解决方案,其时间复杂度为 O(N),其中 N 是输入数组的长度。
(面试经典刷题)挑战一周刷完150道-Python版本-第3天(40个题)-I(前10个题)_第2张图片

暴力法比较费资源。
所以可以用滑动窗口+哈希表

class Solution:
    def lengthOfLongestSubstring(self, s: str) -> int:
        dic, res, i={},0,-1
        for j in range(len(s)):
            if s[j] in dic:
                i =max(dic[s[j]],i)
            dic[s[j]] =j
            res = max(res,j-i)
        return res

这段代码利用了滑动窗口的思想,不断调整起始位置 i 和结束位置 j,以寻找最长的不重复子串。它的时间复杂度为 O(N),其中 N 是输入字符串的长度,因为每个字符只会被处理一次。

三、串联所有单词的子串

给定一个字符串 s 和一个字符串数组 words。 words 中所有字符串 长度相同。

s 中的 串联子串 是指一个包含 words 中所有字符串以任意顺序排列连接起来的子串。

例如,如果 words = [“ab”,“cd”,“ef”], 那么 “abcdef”, “abefcd”,“cdabef”, “cdefab”,“efabcd”, 和 “efcdab” 都是串联子串。 “acdbef” 不是串联子串,因为他不是任何 words 排列的连接。
返回所有串联子串在 s 中的开始索引。你可以以 任意顺序 返回答案。
(面试经典刷题)挑战一周刷完150道-Python版本-第3天(40个题)-I(前10个题)_第3张图片
滑动窗口+哈希表

class Solution:
    def findSubstring(self, s: str, words: List[str]) -> List[int]:
        res = []
        m, n, ls = len(words), len(words[0]), len(s)
        for i in range(n):
            if i + m * n > ls:
                break
            differ = Counter()
            for j in range(m):
                word = s[i + j * n: i + (j + 1) * n]
                differ[word] += 1
            for word in words:
                differ[word] -= 1
                if differ[word] == 0:
                    del differ[word]
            for start in range(i, ls - m * n + 1, n):
                if start != i:
                    word = s[start + (m - 1) * n: start + m * n]
                    differ[word] += 1
                    if differ[word] == 0:
                        del differ[word]
                    word = s[start - n: start]
                    differ[word] -= 1
                    if differ[word] == 0:
                        del differ[word]
                if len(differ) == 0:
                    res.append(start)
        return res


这段代码是用来在字符串 s 中寻找包含给定单词列表 words 中所有单词的子串的起始位置列表。这是一个基于滑动窗口和计数器的算法。

下面是代码的逻辑解读:

  1. 创建一个空列表 res,用于存储结果,即包含所有给定单词的子串的起始位置。

  2. 获取单词列表 words 中单词的长度 n 以及 s 的总长度 ls

  3. 进入一个循环,迭代从 0 到 n - 1,这个循环的目的是为了在每个可能的起始位置开始查找子串。

  4. 在循环中,首先检查是否还有足够的字符用于查找子串,即 i + m * n <= ls,其中 m 是单词列表中单词的数量。

  5. 创建一个空的计数器 differ,用于记录当前窗口中每个单词的出现次数。

  6. 在嵌套循环中,对每个单词进行处理:

    • 使用切片 s[i + j * n: i + (j + 1) * n] 获取当前位置的单词。
    • 将单词添加到计数器 differ 中,并增加其出现次数。
  7. 接下来,对 words 中的单词进行迭代,逐个减少计数器 differ 中对应单词的出现次数。

  8. 接下来,进入另一个循环,从当前位置 i 开始,每次移动 n 个字符,这是为了滑动窗口。在每次迭代中:

    • 如果不是第一次迭代,就要更新计数器 differ,即添加新单词并删除旧单词,以维护窗口中各单词的计数。
    • 如果 differ 中没有剩余单词(即所有单词的计数都为零),说明找到了一个包含所有单词的子串,将当前起始位置 start 添加到结果列表 res 中。
  9. 最后,返回结果列表 res,其中包含了所有包含给定单词的子串的起始位置。

这段代码使用了滑动窗口的思想,通过逐步调整窗口的起始位置和计数器的状态,来寻找包含给定单词的子串。它的时间复杂度取决于输入数据的大小,通常在合理范围内。

四、最小覆盖子串

给你一个字符串 s 、一个字符串 t 。返回 s 中涵盖 t 所有字符的最小子串。如果 s 中不存在涵盖 t 所有字符的子串,则返回空字符串 “” 。
注意:

对于 t 中重复字符,我们寻找的子字符串中该字符数量必须不少于 t 中该字符数量。
如果 s 中存在这样的子串,我们保证它是唯一的答案。

(面试经典刷题)挑战一周刷完150道-Python版本-第3天(40个题)-I(前10个题)_第4张图片
哈希表+滑动窗口
这段代码实现了一个用于在字符串 s 中找到包含字符串 t 中所有字符的最小窗口的函数 minWindow。函数返回满足条件的最小窗口字符串,如果没有这样的窗口,则返回空字符串。

以下是代码的逻辑解读:

  1. 创建一个名为 need 的 defaultdict,用于存储字符串 t 中每个字符的出现次数。这个字典将用于跟踪我们需要在窗口中找到的字符以及它们的数量。

  2. 计算 needCnt,表示还需要找到多少个 t 中的字符。

  3. 初始化变量 i 为0,用于表示窗口的左边界。

  4. 初始化变量 res 为一个元组 (0, float('inf')),用于记录满足条件的最小窗口的左右边界。

  5. 开始遍历字符串 s,使用 enumerate 函数同时获取字符和它们的索引。

  6. 对于每个字符 c,检查它是否在 t 中需要的字符之一。如果是,减少 needCnt 的计数,并将 need[c] 减1。

  7. needCnt 变为0时,表示窗口中包含了所有 t 中的字符,接下来要缩小窗口以找到最小窗口。

  8. 进入一个循环,不断增加左边界 i,以排除多余的元素,直到不能再排除为止。

  9. 在步骤2中提到的循环中,如果找到了一个满足条件的窗口,记录下其左右边界。

  10. 接下来,增加左边界 i,寻找新的满足条件的窗口。将左边界对应的字符的计数加1,同时增加 needCnt

  11. 最后,如果 res 的右边界超过了字符串 s 的长度,则返回空字符串,表示没有找到满足条件的窗口。

  12. 否则,返回满足条件的最小窗口子字符串,使用 s[res[0]:res[1]+1] 来提取这个子字符串。

这段代码使用了滑动窗口的思想,通过不断调整窗口的左右边界,找到包含 t 中所有字符的最小窗口。时间复杂度取决于字符串 st 的长度,通常在合理范围内。

五、汇总区间

给定一个 无重复元素 的 有序 整数数组 nums 。

返回 恰好覆盖数组中所有数字 的 最小有序 区间范围列表 。也就是说,nums 的每个元素都恰好被某个区间范围所覆盖,并且不存在属于某个范围但不属于 nums 的数字 x 。

列表中的每个区间范围 [a,b] 应该按如下格式输出:

“a->b” ,如果 a != b
“a” ,如果 a == b
(面试经典刷题)挑战一周刷完150道-Python版本-第3天(40个题)-I(前10个题)_第5张图片
双指针

class Solution:
    def summaryRanges(self, nums: List[int]) -> List[str]:
        def f(i: int, j: int) -> str:
            return str(nums[i]) if i == j else f'{nums[i]}->{nums[j]}'

        i = 0
        n = len(nums)
        ans = []
        while i < n:
            j = i
            while j + 1 < n and nums[j + 1] == nums[j] + 1:
                j += 1
            ans.append(f(i, j))
            i = j + 1
        return ans


这段代码实现了一个函数 summaryRanges,该函数接受一个整数列表 nums 作为输入,并返回一个包含连续区间的字符串列表。

以下是代码的逻辑解读:

  1. 定义了一个内部函数 f,该函数接受两个整数参数 ij,用于表示一个连续区间的起始和结束。函数 f 返回一个表示区间的字符串,如果区间只包含一个元素,则返回这个元素,否则返回一个区间范围字符串。

  2. 初始化变量 i 为0,用于表示当前扫描的起始索引。

  3. 获取输入列表 nums 的长度 n

  4. 创建一个空列表 ans,用于存储连续区间的结果。

  5. 进入一个 while 循环,循环条件是 i < n,表示只要没有遍历完整个列表就继续循环。

  6. 在循环中,初始化变量 ji,然后开始查找连续区间的结束索引。通过不断比较 nums[j]nums[j + 1] 是否连续,来增加 j 的值。

  7. 一旦找到了不连续的元素,表示一个连续区间的结束,将该区间的起始索引 i 和结束索引 j 传递给内部函数 f,得到区间的字符串表示,然后将这个字符串添加到结果列表 ans 中。

  8. 更新 ij + 1,以准备查找下一个连续区间。

  9. 循环继续,直到遍历完整个列表。

  10. 最后,返回包含所有连续区间的字符串列表 ans

这段代码的主要思路是遍历输入列表 nums,通过比较相邻元素是否连续来确定连续区间,然后将每个连续区间表示为一个字符串,并将这些字符串添加到结果列表中。这样,函数就能返回包含所有连续区间的字符串列表。

六、合并区间

以数组 intervals 表示若干个区间的集合,其中单个区间为 intervals[i] = [starti, endi] 。请你合并所有重叠的区间,并返回 一个不重叠的区间数组,该数组需恰好覆盖输入中的所有区间 。
(面试经典刷题)挑战一周刷完150道-Python版本-第3天(40个题)-I(前10个题)_第6张图片
可以使用排序。

class Solution:
    def merge(self, intervals: List[List[int]]) -> List[List[int]]:
        intervals.sort(key=lambda x: x[0])

        merged = []
        for interval in intervals:
            # 如果列表为空,或者当前区间与上一区间不重合,直接添加
            if not merged or merged[-1][1] < interval[0]:
                merged.append(interval)
            else:
                # 否则的话,我们就可以与上一区间进行合并
                merged[-1][1] = max(merged[-1][1], interval[1])

        return merged


这段代码实现了合并重叠区间的功能。给定一个包含多个区间的列表 intervals,代码会将所有重叠的区间合并成一个或多个新的区间。

以下是代码的逻辑解读:

  1. 首先,使用 intervals.sort(key=lambda x: x[0]) 对输入的区间列表进行排序,排序的关键字是每个区间的起始值,这样可以将区间按照起始值的升序排列,方便后续的合并操作。

  2. 创建一个空列表 merged,用于存储合并后的区间。

  3. 进入一个循环,遍历排序后的区间列表 intervals

  4. 对于每个区间 interval,首先检查 merged 是否为空,或者当前区间与 merged 中的最后一个区间不重叠。如果满足条件,直接将当前区间添加到 merged 中。

  5. 如果当前区间与 merged 中的最后一个区间重叠,那么就将它们合并。合并的方法是将 merged 中的最后一个区间的结束值更新为当前区间的结束值和原结束值中的较大值。

  6. 循环继续,处理下一个区间,以此类推,直到遍历完所有区间。

  7. 最后,返回合并后的区间列表 merged,其中包含了所有重叠区间的合并结果。

这段代码的核心思想是通过排序和逐个比较区间,将重叠的区间合并成一个或多个新的区间。合并的过程是在合适的时机更新合并后的区间的结束值,以确保得到合并后的结果。这是一个常见的区间合并问题的解决方法。

七、插入区间

给你一个 无重叠的 ,按照区间起始端点排序的区间列表。

在列表中插入一个新的区间,你需要确保列表中的区间仍然有序且不重叠(如果有必要的话,可以合并区间)。
(面试经典刷题)挑战一周刷完150道-Python版本-第3天(40个题)-I(前10个题)_第7张图片
方法一:模拟。
方法二:二分查找处理。

class Solution:
    def insert(self, intervals: List[List[int]], newInterval: List[int]) -> List[List[int]]:
        if intervals == []: return [newInterval]
        if newInterval[1] < intervals[ 0][ 0]: return [newInterval] + intervals
        if newInterval[0] > intervals[-1][-1]: return intervals + [newInterval]        
        lst1, lst2 = zip(*intervals)
        l1, l2 = bisect_left(lst2, newInterval[0]), bisect_right(lst1, newInterval[1])
        intervals[l1 : l2] = [[min(newInterval[0], intervals[l1][0]), max(newInterval[1], intervals[l2 - 1][1])]]
        return intervals

这段代码实现了将新的区间 newInterval 插入到已排序的区间列表 intervals 中,并合并重叠的区间。这种操作通常用于处理区间合并问题。

以下是代码的逻辑解读:

  1. 如果输入的 intervals 列表为空,直接返回包含 newInterval 的列表 [newInterval],因为没有其他区间可合并。

  2. 如果 newInterval 的结束值小于 intervals 中的第一个区间的起始值,说明 newInterval 在所有区间之前,直接将 newInterval 插入到 intervals 的开头,然后返回合并后的结果。

  3. 如果 newInterval 的起始值大于 intervals 中的最后一个区间的结束值,说明 newInterval 在所有区间之后,直接将 newInterval 插入到 intervals 的末尾,然后返回合并后的结果。

  4. 在其他情况下,需要对 intervals 进行更复杂的合并操作。首先,通过列表解析和内置函数 zip,将 intervals 列表中的起始值和结束值分别提取到两个分开的列表 lst1lst2 中。

  5. 使用二分查找方法 bisect_left 找到 newInterval 的起始值在 lst2 中的插入位置,并将结果存储在变量 l1 中。这个位置表示了要插入 newInterval 的位置。

  6. 使用二分查找方法 bisect_right 找到 newInterval 的结束值在 lst1 中的插入位置,并将结果存储在变量 l2 中。这个位置也表示了要插入 newInterval 的位置。

  7. 然后,通过切片操作 intervals[l1: l2] 获取要合并的区间子列表,这些区间需要合并成一个。

  8. 使用列表切片赋值,将合并后的区间 [min(newInterval[0], intervals[l1][0]), max(newInterval[1], intervals[l2 - 1][1])] 替换掉 intervals 中的对应区间。

  9. 返回合并后的 intervals 列表作为结果。

这段代码的关键思路是通过二分查找找到要插入的位置,并合并重叠的区间。这是一个高效的方法,时间复杂度为 O(log N),其中 N 是 intervals 列表的长度。

八、用最少数量的箭引爆气球

有一些球形气球贴在一堵用 XY 平面表示的墙面上。墙面上的气球记录在整数数组 points ,其中points[i] = [xstart, xend] 表示水平直径在 xstart 和 xend之间的气球。你不知道气球的确切 y 坐标。

一支弓箭可以沿着 x 轴从不同点 完全垂直 地射出。在坐标 x 处射出一支箭,若有一个气球的直径的开始和结束坐标为 xstart,xend, 且满足 xstart ≤ x ≤ xend,则该气球会被 引爆 。可以射出的弓箭的数量 没有限制 。 弓箭一旦被射出之后,可以无限地前进。

给你一个数组 points ,返回引爆所有气球所必须射出的 最小 弓箭数 。

(面试经典刷题)挑战一周刷完150道-Python版本-第3天(40个题)-I(前10个题)_第8张图片
方法一:排序+贪心

class Solution:
    def findMinArrowShots(self, points: List[List[int]]) -> int:
        if not points:
            return 0
        
        points.sort(key=lambda balloon: balloon[1])
        pos = points[0][1]
        ans = 1
        for balloon in points:
            if balloon[0] > pos:
                pos = balloon[1]
                ans += 1
        
        return ans


这段代码实现了解决气球射击问题(Minimum Number of Arrows to Burst Balloons)的算法。问题的描述是,在平面上有一些气球,每个气球用一个区间 [start, end] 表示,你可以射出一支箭,该箭会刺穿所有与它相交的气球。求最少需要多少支箭才能刺穿所有气球。

以下是代码的逻辑解读:

  1. 首先,检查 points 是否为空,如果为空则返回0,因为没有气球需要射击。

  2. 对气球的区间列表 points 进行排序,排序的依据是气球的结束坐标 balloon[1],这是为了优先射击最早结束的气球。

  3. 初始化变量 pos 为第一个气球的结束坐标 points[0][1],并初始化变量 ans 为1,表示至少需要一支箭来刺穿第一个气球。

  4. 进入一个循环,遍历排序后的气球列表 points

  5. 对于每个气球,首先检查气球的起始坐标 balloon[0] 是否大于当前箭的位置 pos。如果大于 pos,说明这个气球不能被当前的箭刺穿,需要发射一支新的箭。此时,将 pos 更新为当前气球的结束坐标 balloon[1],并将 ans 加1。

  6. 循环继续,继续检查下一个气球。

  7. 最后返回 ans,即所需的最少箭的数量,这个数量是可以刺穿所有气球的最小值。

这段代码的核心思想是通过贪心算法,按照气球的结束坐标进行排序,并不断地更新箭的位置,以确保尽量多地刺穿气球。这是一个高效的解决方案,时间复杂度为 O(NlogN),其中 N 是气球的数量。

九、有效的数独

请你判断一个 9 x 9 的数独是否有效。只需要 根据以下规则 ,验证已经填入的数字是否有效即可。

数字 1-9 在每一行只能出现一次。
数字 1-9 在每一列只能出现一次。
数字 1-9 在每一个以粗实线分隔的 3x3 宫内只能出现一次。(请参考示例图)

注意:

一个有效的数独(部分已被填充)不一定是可解的。
只需要根据以上规则,验证已经填入的数字是否有效即可。
空白格用 ‘.’ 表示。
(面试经典刷题)挑战一周刷完150道-Python版本-第3天(40个题)-I(前10个题)_第9张图片
哈希

class Solution:
    def isValidSudoku(self, board: List[List[str]]) -> bool:

        row = [[0] * 9 for _ in range(9)]
        col = [[0] * 9 for _ in range(9)]
        block = [[0] * 9 for _ in range(9)]

        for i in range(9):
            for j in range(9):
                if board[i][j] != '.':
                    num = int(board[i][j]) - 1
                    b = (i // 3) * 3 + j // 3
                    if row[i][num] or col[j][num] or block[b][num]:
                        return False
                    row[i][num] = col[j][num] = block[b][num] = 1
        return True


这段代码是用于验证数独是否有效的解答代码。数独是一个9x9的二维网格,其中填有数字 1 到 9,要求每行、每列和每个3x3的小九宫格内都不能有重复的数字。如果数独规则被满足,返回True;否则,返回False。

以下是代码的逻辑解读以及相关的专业知识:

  1. 代码使用三个二维数组 rowcolblock 来分别记录每一行、每一列和每一个3x3小九宫格内数字的出现情况。这些数组的维度都是9x9,用于记录数字1到9是否已经在对应的行、列或小九宫格内出现。

  2. 使用两个嵌套的循环遍历整个数独网格,外层循环迭代行,内层循环迭代列。

  3. 对于每个遍历到的格子,首先检查它是否是空格(用’.'表示)。如果是空格,直接跳过,因为空格不需要验证。

  4. 如果格子不是空格,将其内容转换成整数,并减去1,得到数字的索引,因为数组索引是从0开始的。

  5. 然后,计算当前格子属于哪个小九宫格(用b表示)。这是通过 (i // 3) * 3 + j // 3 计算得到的,其中 ij 是当前格子的行和列索引。这个计算将行索引和列索引分别除以3,然后分别乘以3,以确定当前格子在哪个小九宫格内。

  6. 接下来,检查当前数字在对应的行、列和小九宫格是否已经出现过。如果在任何一个地方已经出现过,就返回False,因为数独规则被违反了。

  7. 如果当前数字在行、列和小九宫格都没有出现过,就将对应的标记数组 rowcolblock 中对应的位置标记为1,表示该数字已经出现过。

  8. 最后,循环结束后,如果没有发现任何违反数独规则的情况,就返回True,表示数独是有效的。

这段代码实现了一个高效的数独验证算法,时间复杂度为O(1),因为数独网格的大小是固定的。它利用了三个二维数组来记录数字的出现情况,通过遍历一次数独网格就能够判断是否满足数独规则。

十、H指数

给你一个整数数组 citations ,其中 citations[i] 表示研究者的第 i 篇论文被引用的次数。计算并返回该研究者的 h 指数。

根据维基百科上 h 指数的定义:h 代表“高引用次数” ,一名科研人员的 h 指数 是指他(她)至少发表了 h 篇论文,并且每篇论文 至少 被引用 h 次。如果 h 有多种可能的值,h 指数 是其中最大的那个。

让我们用一个示例来解释:

假设一个研究者的论文引用次数分别是 [3, 0, 6, 1, 5]。我们可以首先将引用次数降序排列:[6, 5, 3, 1, 0]。然后我们从高到低逐个查看引用次数,找到最大的 h,使得前 h 个数都不小于 h。在这个例子中,前3个数都不小于3,但第4个数是1,小于4,因此 H 指数为3。

所以,H 指数就是一个能够衡量研究者学术贡献和影响力的指标,反映了其发表论文的质量和影响程度。一个较高的 H 指数通常表示一个更有影响力的研究者。在编程中,你需要根据研究者的论文引用次数数组,计算并返回其 H 指数。
(面试经典刷题)挑战一周刷完150道-Python版本-第3天(40个题)-I(前10个题)_第10张图片

二分法思路


class Solution:
    def hIndex(self, nums: List[int]) -> int:
        # x篇>=x,则一定x-1篇>=x-1
        # x篇>=x不符合,则一定x+1篇>=x+1不符合
        n=len(nums)
        l,r=1, n # 篇数取值范围
        def ck(x):
            if sum(c>=x for c in nums)>=x:
                return False
            else:
                return True
        while l<=r:
            mid=l+(r-l)//2
            if ck(mid):
                r=mid-1
            else:
                l=mid+1
        return l-1

这段代码实现了计算研究者的 H 指数(h-index)的函数。H 指数是一个用于衡量研究者学术影响力的指标,表示一个研究者至少有多少篇论文被引用了至少 h 次。

以下是代码的逻辑解读:

  1. 首先,获取输入列表 nums 的长度,即研究者的论文总数。

  2. 初始化两个变量 lr,分别表示篇数取值范围的左边界和右边界。初始时,左边界 l 为1,右边界 r 为论文总数 n

  3. 定义一个辅助函数 ck(x),用于检查是否存在至少 x 篇论文被引用了至少 x 次。如果存在,返回 False,否则返回 True。这个函数的目的是帮助确定 x 是否符合 H 指数的定义。

  4. 进入一个循环,循环条件是 l 小于等于 r

  5. 在循环内,计算中间值 mid,使用二分查找的思想。mid 表示当前尝试的 H 指数。

  6. 调用 ck(mid) 检查是否存在至少 mid 篇论文被引用了至少 mid 次。如果符合条件,说明 mid 可能是一个有效的 H 指数,所以将右边界 r 缩小到 mid - 1

  7. 如果不符合条件,说明 mid 不可能是 H 指数,因此将左边界 l 增加到 mid + 1

  8. 循环结束后,返回 l - 1,因为 l 是第一个不满足条件的值,减1得到符合条件的最大 H 指数。

总的来说,这段代码采用了二分查找的方法来确定 H 指数的值。通过不断调整 lr,并使用 ck 函数进行验证,最终找到满足 H 指数定义的最大值。这是一个高效的算法,时间复杂度为 O(NlogN),其中 N 是论文总数。

你可能感兴趣的:(面试,python,算法)