大厂算法面试:使用移动窗口查找两个不重叠且元素和等于给定值的子数组

Posted tyler_download

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了大厂算法面试:使用移动窗口查找两个不重叠且元素和等于给定值的子数组相关的知识,希望对你有一定的参考价值。

根据”老朽“多年在中国IT业浸淫的经验,我发现无论大厂还是小厂,其算法面试说难也不难。难在于算法面试的模式都是在给定网站上做算法题,90分钟做三道。我自认个人水平在平均线以上,但通过多次尝试发现,要在90分钟内完成给定算法题非常困难,这还是在我有过多年算法训练的基础上得出的结论,特别是这些题目往往有一些很不好想到的corner case,使得你的代码很难快速通过所有测试用例,我们今天要研究的题目就属于有些特定情况不好处理的例子。此外“不难”在于,很多公司的面试算法题其特色与整个行业类似,那就是缺乏原创,中国公司90%以上的面试算法题全部来自Leetcode,因此刷完后者,甚至把后者那五百多道题”背“下来,你基本上能搞定,国内仿造hackerrank的牛X网,其题目就是这个特点。

因此通过研究足够样本量的算法题,掌握其思路,甚至记住其题型就能大大增加我们面试成功的几率。我们看看这次题目:
给定一个所有元素都是正整数的数组,同时给定一个值target,要求从数组中找到两个不重叠的子数组,使得各自数组的元素和都等于给定数值target,并且要求两个数组元素个数之和最小,例如给定数组为[1 , 2, 1, 1, 1],同时给定目标值3,此时它有三个子数组分别为[1,2], [2,1],[1,1,1],他们的元素和都等于3,但是由于前两个数组有重叠,因此满足条件的两个子数组为[1,2],[1,1,1],或者是[2,1],[1,1,1]。

如果是白板面试,也就是你跟面试官面对面,那么拿到题目后不要立刻着手,而是要跟他澄清一些疑问,例如你可以问:1,如果数组为空,或者数组内没有满足条件的子数组,那应该返回什么值,面试官可能回答返回0或者空;2,数组最大长度是多少,对方可能回答一百万个元素。

现在我们看看问题的处理。解决这个问题有三个要点,1,找到所有满足条件的子数组,2,从这些数组中找到不重叠数组的组合,3,从步骤2中找到元素数量之和最小的两个数组。首先我们看第1点如何完成。策略如下,我们使用一种叫滑动窗口的办法,所谓窗口其实就是两个标记:start, end,它分别对应窗口的起始和结束位置,例如start = 0, end = 2,那么这个窗口所包含的元素就是[1,2,1],窗口左边和右边都可以滑动,例如start向右滑动一个单位变成1,那么对应的子数组元素就是[2,1],如果右边end向右滑动一个单位变成3,那么窗口对应元素就是[1,2,1,1],窗口还能整体滑动,例如start和end都能向右滑动1个单位,那么对应窗口元素就是[2,1,1].

使用滑动窗口我们能方便的找到元素和等于给定值的子数组。注意到数组只包含正整数,因此如果保持start不变,end向右边移动,那么窗口内部的元素和就会变大,如果保持end不变,那么窗口内元素和就会减小。所以我们首先让start = 0, end = -1,此时窗口内不包含任何元素,于是窗口元素和可以认为是0.接下来我们让end向右移动一个单位,也就是end=0,此时窗口包含1个元素,也就是头元素2,此时窗口元素和小于给定值,因此end继续向右移动一个单位,此时窗口内元素和为3,这次我们找到了满足条件的子数组。

让end继续向右移动一个单位,此时窗口内元素为[1,2,1],元素和为4大于给定值,于是我们让start向左挪动一个单位,得到子数组[2,1],此时我们又找到了满足条件的子数组。如此类推,我们从数组最左端出发,如果窗口内元素和小于给定指定值,那么就向右移动end,如果大于给定值,那么就像左移动一个单位,当窗口挪出数组,也就是end的值大于数组最后一个元素的下标时,查找结束,当前能找到所有满足元素和等于特定值的所有子数组。

第二步就是找到不重叠而且两个数组长度之和最小的子数组。这就是cornner case,也是不好调试通过的地方。要找到长度和最小的两个子数组,我们需要做到,首先记录下当前找到的,位于start左边的长度最小的满足条件的数组。首先使用对应sub_array记录当前找到的满足条件的子数组,使用subarray_index作为遍历队列的标记。首先它的值为0,如果sub_array[subarray_index]对应的子数组不跟当前窗口重叠,也就是给定子数组的末尾元素其下标小于start,那么我们就能增加subarray_index的值以遍历下一个元素,在这个遍历的过程中,我们记录下长度最小的子数组,使用shortest_array_index进行标记。

当start向右移动时,我们就查看subarray_index能否向右移动,如果start向右移动后,subarray_index指向的子数组不与当前窗口重叠,那么subarray_index就可以向右移动,然后记录下长度最小的子数组。当移动窗口找到一个满足条件的子数组时,算法查看当前找到的子数组长度与shortest_array_index指向的子数组长度之和是否变小,如果变小了那么就记录下这两个子数组,需要注意的是这两个数组不会发送重合,因为我们确保subarray_index指向数组不跟滑动窗口重合,而shortest_array_index指向数组要不跟subarray_index指向数组一样,要不就在该数组左边,因此shortest_array_index指向子数组绝对不会跟当前滑动窗口指向的子数组重合。

通过上面步骤,只要滑动窗口移动出了数组,那么步骤2找到的两个子数组就能满足条件,我们看看具体实现代码:

subarray_list = []

def over_lapped(array1, array2):
    if array1[0] <= array2[0] and array2[0] <= array1[1]:
        return True
    return False

def find_all_subarray(array, target):
    start = 0
    end = -1
    window_sum = 0
    shortest_array_index = -1
    shortest_array_len = 0
    subarray_index = 0
    total_length = float('inf')  #当前满足条件两个子数组的长度
    pair = []
    while end < len(array):
        if window_sum == target: #将满足条件的子数组放入队列
            subarray_list.append((start, end))
            current_length = end - start + 1
            #记录当前满足条件的两个不重叠数组长度之和的最小值
            if shortest_array_index != -1 and shortest_array_len + current_length < total_length:
                total_length = shortest_array_len + current_length
                pair = [shortest_array_index, len(subarray_list) - 1]

            if shortest_array_index == -1:
                shortest_array_index = 0
                shortest_array_len = end - start + 1


        if window_sum <= target: #当前窗口内的元素和小于给定值,窗口右边向右扩展一个元素
            end += 1
            if end < len(array):
                window_sum += array[end]

        if window_sum > target: #当前窗口内元素和大于给定值,窗口左边右移去除最左边元素
            window_sum -= array[start]
            start += 1
            #记录位于窗口左边的满足条件的最短子数组
            while subarray_index < len(subarray_list):
                (sub_array_start, sub_array_end) = subarray_list[subarray_index]
                if sub_array_end < start :
                    #满足条件的子数组位于当前窗口的左边
                    if (sub_array_end - sub_array_start + 1) < shortest_array_len:
                        shortest_array_index = subarray_index
                        #记录位于当前窗口左边,满足条件且长度最小的子数组
                        shortest_array_len = sub_array_end - sub_array_start + 1
                    subarray_index += 1
                else:
                    break

    if len(pair) < 1:
        return None

    return (subarray_list[pair[0]], subarray_list[pair[1]])

array = [ 1, 1, 1, 2,1 , 3, 1, 2, 2, ]
pairs = find_all_subarray(array, 3)


if pairs is not None:
    print(f"shortest sub arrays are pairs[0] and pairs[1]")

上面代码运行后,所得结果为:
shortest sub arrays are (2, 3) and (5, 5)
也就是说第一个子数组起始为2,结尾为3,对应子数组就是[1,2],第二个子数组起始下标为5,结束下标为5,因此对应数组为[3],这两个数组满足条件,而且不难看出他们的长度之和最小,为了确保算法正确性,我们再次修改array,在其末尾加上一个元素3变成:

array = [ 1, 1, 1, 2,1 , 3, 1, 2, 2, 3]
pairs = find_all_subarray(array, 3)

代码运行后所得结果为:

shortest sub arrays are (5, 5) and (9, 9)

由此可以看出算法正确性得以保证,由于算法只需要使用滑动窗口对数组进行一次变量,因此时间复杂度为O(n),同时我们需要使用一个队列来存放满足条件的子数组,因此空间复杂度为O(n),这道题的难点在于获得两个不重叠的子数组,我花费了大量的时间在调试这一点上,如果面试机考中出现这道题,而且我在事先没有见过它的话,那么在调试步骤2时一定会让我挂掉。

更多干货请点击这里:http://m.study.163.com/provider/7600199/index.htm?share=2&shareId=7600199,更多有趣技术视频请在B站搜索Coding迪斯尼。

以上是关于大厂算法面试:使用移动窗口查找两个不重叠且元素和等于给定值的子数组的主要内容,如果未能解决你的问题,请参考以下文章

大厂算法面试之leetcode精讲3.动态规划

搞定大厂算法面试之leetcode精讲4.贪心

大厂算法面试之leetcode精讲9.位运算

破解大厂算法面试最难动态规划题:将数组分割成元素和相等的两部分

大厂算法面试之leetcode精讲6.深度优先&广度优先

flink流计算随笔