python3实现最滑动窗口最大值算法题——使用胜者树

题目

给定一个数组 nums,有一个大小为 k 的滑动窗口从数组的最左侧移动到数组的最右侧。你只可以看到在滑动窗口内的 k 个数字。滑动窗口每次只向右移动一位。

结果返回滑动窗口中的最大值。

示例:输入: nums = [1,3,-1,-3,5,3,6,7], 和 k = 3
输出: [3,3,5,5,6,7]
解释:

滑动窗口的位置     最大值


[1 3 -1] -3 5 3 6 7      3
1 [3 -1 -3] 5 3 6 7      3
1 3 [-1 -3 5] 3 6 7      5
1 3 -1 [-3 5 3] 6 7      5
1 3 -1 -3 [5 3 6] 7      6
1 3 -1 -3 5 [3 6 7]      7

题目来源

提示:你可以假设k 总是有效的,在输入数组不为空的情况下,1 ≤ k ≤ 输入数组的大小。(但是在数组可以为空,这个时候k会等于0)(LeetCode都是骗人的233)

除了暴力遍历方法,可以通过存储滑动窗口的数据结构入手,使用或者双向队列存储滑动窗口内的值。这里选择堆(双向队列貌似更简单更快)。

"每次只找最大值"启发了我使用胜者树来解决问题。

胜者树与败者树

胜者树和败者树都是完全二叉树,是树形选择排序的一种变型。每个叶子结点相当于一个选手,每个中间结点相当于一场比赛,每一层相当于一轮比赛。

不同的是,胜者树的中间结点记录的是胜者的标号;而败者树的中间结点记录的败者的标号。

胜者树与败者树可以在log(n)的时间内找到最值。任何一个叶子结点的值改变后,利用中间结点的信息,还是能够快速地找到最值。在k路归并排序中经常用到。

详细知识

思路

使用胜者树有两个关键点:第一是如何初始化,第二是当滑动窗口移动时,胜者树如何重构。

初始化

  • 首先确定树的实现方法。由于是完全二叉树,可以使用数组(列表)模拟树型结构。list[1]为根节点,对于任意节点list[i]list[i/2]为父节点,(python的整数除为//而不是/) list[i/2*2]list[i/2*2+1]为左右子节点

  • 其次确定树的节点个数(数组的大小)。对于k个要比较的数据,创建一个2k-1大小的完全二叉树即可。(注意各个节点对应的数组下标)

    胜者树_1

重构

  • 首先确定新加入的“选手”在树中叶子节点的位置。当滑动窗口移动时,每次替换的叶子节点位置也依次变化。

  • 根据胜者树逐层向上比较,最终找到最大值。

实现步骤

  1. 用数组nums的前k个元素构造胜者树并初始化
  2. 逐渐向后遍历nums的每个元素,将其加入胜者树进行比较,找到当前滑动窗口的最大值并记录。

python3实现

def maxSlidingWindow(nums, k: int):
    result=[]
    if not nums:
        return result
    elif k==1:
        return nums
    # 确定胜者树的大小
    size=2*k
    tail=size-1
    # 初始化胜者树
    tree=[None]*size
    for i in range (k):
        tree[i+k]=nums[i]
    p=tail
    while(p!=1):
        if tree[p]>tree[p-1]:
            tree[p//2]=tree[p]
        else:
            tree[p//2]=tree[p-1]            
        p-=2
    result.append(tree[1])
    # 重建胜者树
    extra=0
    for index in range(k,len(nums)):                                                                                                       
        number=nums[index]
        # p为替换的"选手"的位置
        # 由于滑动窗口向前滑动,每次替换的叶子节点的位置不一样,因此重新计算p
        # extra用来决定替换位置,具体替换规律可通过画图观察得到
        p=tail-k+1+extra%k
        tree[p]=number     
        while(p!=1):
            parent=p//2
            l_child=parent*2
            r_child=l_child+1
            if tree[l_child]>tree[r_child]:
                tree[parent]=tree[l_child]
            else:
                tree[parent]=tree[r_child]
            p=parent
        result.append(tree[1])
        extra+=1
    return result

num=[1,3,-1,-3,5,3,6,7]
print(maxSlidingWindow(num,3))

心得体会

本题借用了胜者树败者树的思路,但是跟“外部排序”略有不同,只能使用胜者树,不能使用败者树,因为排序的时候是找到最大(最小)值后就将根节点的值弹出不再使用(即每次重构根节点的值都会更新),而本题最大(最小)值可能会重复使用(即每次重构根节点的值不一定会更新)。败者树只存储了败者,重构时一定会更新根节点,所以会出现问题。

另外当滑动窗口移动时,重构时每次替换的叶子节点位置也依次变化,如果没注意这点,也会导致bug。

posted @ 2020-02-18 17:56  墨莲玦  阅读(1226)  评论(0编辑  收藏  举报