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

根据”老朽“多年在中国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迪斯尼。

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值