zoukankan      html  css  js  c++  java
  • LeetCode 76,一题教会你面试算法时的思考套路

    本文始发于个人公众号:TechFlow,原创不易,求个关注


    今天是LeetCode专题的第45篇文章,我们一起来看看LeetCode的76题,最小窗口子串Minimum Window Substring。

    这题的官方难度是Hard,通过了也是34.2%,4202人点赞,299人反对。从通过率以及点赞比来看,这题的质量很高,稍稍有些偏难,所以小伙伴们请做好准备,这是一道有点挑战的问题。

    题意和样例

    我们一起来看下题意,这题的题意很短,给定两个字符串S和T。要求设计一个复杂度为的算法,在S串当中找到一个子串,能够包含T串当中的所有字符。要求返回合法且长度最小的窗口的内容。

    注意:

    • 如果不存在这样的窗口,返回“”。
    • 如果窗口存在,题目保证有且只有一个。

    样例:

    Input: S = "ADOBECODEBANC", T = "ABC"
    Output: "BANC"
    

    分析

    我们来分析一下这个问题,从题意当中大家应该都能感受到它的难度。因为上来题目当中就限定了我们使用的算法的复杂度必须是,然而我们遍历字符串的复杂度就已经是了,也就是说我们不能引入额外的计算开销,否则一定不满足题目的要求。

    可能有些同学会想到传说中在时间内判断字符串匹配的KMP算法,如果你不知道这个算法也没有关系,因为这个算法并不适用。因为我们要找的不是完全相等的子串的位置,而是找的是字符构成一样的子串,所以并不能通过引入字符串匹配算法来解决。没有学过KMP算法的同学可以松一口气了,这题当中并不会引入新的算法。

    解题的套路

    一般来说当我们面临一个算法问题的时候,我们常常的思考过程主要有两种。一种是适配,说白了就是把可能可以用上的算法往问题上套。根据题意先感觉一下,大概会用到什么样的算法,然后详细地推导适配的过程,看看是不是真的适用或者是有什么坑,或者是会出现什么新的问题。如果一切OK,能够推理得通,那么这个算法就是解。第二种方法是建模,也就是说从题意入手,对题意进行深入的分析,对问题进行建模和抽象,找到问题的核心,从而推导出用什么样的算法可以解决。

    举个很简单的例子,一般来说我们的动态规划算法都是适配。都是我们先感觉或者是猜测出可以使用动态规划,然后再去找状态和转移,最后建立状态转移方程。而一些搜索问题一般是建模,我们先对问题进行分析,然后找出需要搜索的解的存在空间,然后设计算法去搜索和剪枝,最后找到答案。

    据说一些顶级高手这两种方法是一起使用的,所以才可以那么快速地找到解。当然我不是顶级高手,所以这个也只是我的猜测。这个思考过程非常有用,特别是当我们面试的时候,遇到一个从未见过的问题,如果你什么套路也没有,头脑一片空白或者是苦思冥想不得要领是很常见的事情。当你有了套路之后,你就可以试着慢慢找到答案了。

    回到这道题本身,我们刚才已经试过了,拿字符串匹配的算法网上套是不行的。在视野里似乎也没有其他的算法可以套用,所以我们换一种思路,试试看建模。

    首先我们可以肯定一点,我们需要在遍历的时候找到答案,这样才可以保证算法的复杂度是。我们的目标是寻找子串,也就是说我们遍历的过程应该对应一个子串,并且我们有方法可以快速判断这个子串是否合法。这样我们才可以做到遍历的同时判断答案的可行性。进而可以想到这是一个区间维护的问题,区间维护我们经常使用的方法就是two pointers。所以我们可以试试two pointers能否适用。

    实际上这道题的正解就是two pointers。

    题解

    我们维护了一个区间,我们需要判断区间里的字符构成,这个很容易想到可以使用dict,维护每一个字符出现的次数。在这个题目当中,我们只需要考虑覆盖的情况,也就是说字符多了并不会构成非法。所以我们可以维护一个dict,每次读入一个字符更新它,当dict当中的字符满足要求的时候,为了使得区间长度尽量短,我们可以试着移动区间的左侧,尽量缩短区间的长度。

    从区间维护的角度来说,我们每次移动区间右侧一个单位,只有当区间内已经满足题意的时候才会移动左侧。通过移动左侧弹出元素来获取能够满足题意的最佳区间。

    我们来看下主要的流程代码:

    # 存储区间内的字符
    segement = {}
    for i in range(n):
        segement[s[i]] += 1
        # 当满足条件的时候移动区间左侧
        while l <= i and satisified(segment):
            # 更新最佳答案
            if i - l + 1 < ans_len:
                ans_len = i - l + 1
                beg, end = l, i + 1
            # 弹出元素
      segement[s[l]] -= 1
            l += 1
    

    到这里还有一个小问题,就是怎么样判断这个segment是否合法呢?我们可以用一个数字matched来记录目前已经匹配上的字符的数量。当某个字符在segment当中出现的次数和T中的次数相等的时候,matched加一。当matched的数量和T中字符种类的数量相等的时候,就可以认为已经合法了。

    我们把所有的逻辑串起来,就可以通过这题了。

    class Solution:
        def minWindow(self, s: str, t: str) -> str:
            from collections import Counter, defaultdict
            # 通过Counter直接获取T当中的字符构成
            counter = Counter(t)
            n, m = len(s), len(counter)
            l, beg, end = 0, 0, 0
            cur = defaultdict(int)
            matched = 0
            flag = False
            # 记录合法的字符串的长度
            ans_len = 0x3f3f3f3f
            
            for i in range(n):
                if s[i] not in counter:
                    continue
                    
                cur[s[i]] += 1
                # 当数量匹配上的时候,matched+1
                if cur[s[i]] == counter[s[i]]:
                    matched += 1
                    
                # 如果已经找到了合法的区间,尝试缩短区间的长度
                while l <= i and matched == m:
                    if i - l + 1 < ans_len:
                        flag = True
                        beg, end = l, i+1
                        ans_len = i - l + 1
                        
                    # 弹出左侧元素
                    c = s[l]
                    if c in counter:
                        cur[c] -= 1
                        if cur[c] < counter[c]:
                            matched -= 1
                            
                    l += 1
    
            
            return "" if not flag else s[beg: end]
    

    总结

    到这里,这道题就算是解决了。很多同学可能会觉得疑惑,为什么我们用到了两重循环,但是它依然还是的算法呢?

    这个是two pointers算法的常见问题,也是老生常谈的话题了。我们在分析复杂度的时候,不能只简单地看用到了几层循环,而是要抓住计算的核心。比如在这个问题当中,我们内部的while循环针对的变量是l,l这个变量对于i整体是递增的。也就是说无论外面这个循环执行多少次,里面的这个while循环一共最多累加只能执行n次。那么,当然这是一个的算法。

    这题总体来说有些难度,特别是一开始的时候可能会觉得没有头绪无从下手。这个时候有一个清晰的头脑以及靠谱的思考链非常重要,希望大家都能学到这个其中思维的过程,这样以后才可以应付更多的算法问题。

    如果喜欢本文,可以的话,请点个关注,给我一点鼓励,也方便获取更多文章。

    本文使用 mdnice 排版

  • 相关阅读:
    机器学习中 margin loss 、hinge loss 和 ramp loss 的区别
    ML 论文中 用到的 temperature
    对一系列 pickle load 进行解包,只保留最后几个
    Linux 常用命令速览
    Numpy 的 dtype 和 astype 的区别
    远程配置 tensorflow 环境
    pytorch 的一些坑
    Conda 配置虚拟 pytorch 环境 和 Tensorflow 环境
    Ubuntu 远程离线配置 pytorch 运行环境
    3.Vue起步
  • 原文地址:https://www.cnblogs.com/techflow/p/13082977.html
Copyright © 2011-2022 走看看