使用树的求职面试问题,要保存哪些数据? 在哪里搜索?如何搜索?伪代码

问题描述

我正在解决以下求职面试问题并解决了大部分问题,但在最后一个要求上失败了。

问:构建一个支持以下功能的数据结构:

Init - 初始化空 DS。 O(1) 时间复杂度。

SetPositiveInDay(d,x) - 添加到 DS,在第 d 天正好 x 新人感染了 covid-19。 O(log n) 时间复杂度。

WorseBefore(d) - 从插入 DS 且小于的天数开始,返回最后一个新感染人数多于 d 的天数。 O(log n) 时间复杂度。

例如:

Init()
SetPositiveInDay(1,10)
SetPositiveInDay(2,20)
SetPositiveInDay(3,15)
SetPositiveInDay(5,17)
SetPositiveInDay(23,180)
SetPositiveInDay(8,13)
SetPositiveInDay(13,18)
WorstBefore(13) // Returns day #2
SetPositiveInDay(10,19)
WorstBefore(13) // Returns day #10

重要说明:您不能假设天数将按订单输入,也不能假设天数之间不会有“间隙”。 (有些日子可能不会保存在 DS 中,而之后的那些日子可能会保存)。


我做了什么?

我使用了 AVL 树(我也可以使用 2-3 棵树)。 对于我拥有的每个节点:

Sick - 当天新增感染人数。

maxLeftSick - 左子的最大感染人数。

maxRightSick - 右子的最大感染人数。

当插入一个新节点时,我确保不会丢失轮换数据,对于从新节点到我所做的根节点的每个单个节点:

enter image description here

enter image description here

但我没有成功实施 WorseBefore(d)

解决方法

在哪里搜索?

首先需要在按天排序的树中找到node对应的节点d。让x = Sick(node)。这可以在 O(log n) 内完成。

如果 maxLeftSick(node) > x,解必须node 的左子树中。在那里搜索解决方案并返回答案。这可以在 O(log n) 中完成 - 见下文。

否则,从node开始向上朝根遍历树,直到找到满足该属性的第一个节点nextPredecessor(这需要O(log n) ):

  • nextPredecessor 小于 node
  • 或者
    1. Sick(nextPredecessor) > x
    2. maxLeftSick(nextPredecessor) > x

如果不存在这样的节点,我们放弃。在第 1 种情况下,只需返回 nextPredecessor,因为这是最佳解决方案。

在情况 2 中,我们知道解必须nextPredecessor 的左子树中,因此在那里搜索并返回答案。同样,这需要 O(log n) - 见下文。


请注意,无需在 nextPredecessor 子树中搜索,因为该子树中唯一小于 node 的节点将是node 本身,我们已经排除了。

另请注意,没有必要比 nextPredecessor 更向上遍历树,因为这些节点甚至更小,我们正在寻找满足所有约束的最大节点。 >


如何搜索?

好的,那么我们如何在子树中搜索解决方案呢?使用 qx 信息,在以 maxLeftSick 为根的子树中找到比感染数 maxRightSick 更糟糕的最大一天很简单:

  1. 如果 q 有一个右孩子和 maxRightSick(q) > x,则在 q 的右子树中搜索。
  2. 如果 q 没有右孩子和 Sick(q) > x,则返回 Day(q)
  3. 如果 q 有一个左孩子和 maxLeftSick(q) > x,则在 q 的左子树中搜索。
  4. 否则子树 q 中没有解。

我们有效地使用 maxLeftSickmaxRightSick 修剪搜索树以仅包含“更差”的节点,并且在修剪后的树中我们得到最正确的节点,即具有最大日期的节点.

很容易看出该算法在 O(log n) 中运行,其中 n 是节点总数,因为步数受树的高度限制。

伪代码

这是伪代码(假设 maxLeftSickmaxRightSick 如果不存在相应的子节点,则返回 -1):


// Returns the largest day smaller than d such that its 
// infection number is larger than the infection number on day d.
// Returns -1 if no such day exists.
int WorstBefore(int d) {
    node = find(d);
    
    // try to find the solution in the left subtree
    if (maxLeftSick(node) > Sick(node)) {
        return FindLastWorseThan(node -> left,Sick(node));
    }
    // move up towards root until we find the first node
    // that is smaller than `node` and such that
    // Sick(nextPredecessor) > Sick(node) or 
    // maxLeftSick(nextPredecessor) > Sick(node).
    nextPredecessor = findNextPredecessor(node);
    if (nextPredecessor == null) return -1;

    // Case 1
    if (Sick(nextPredecessor) > Sick(node)) return nextPredecessor;
    
    // Case 2: maxLeftSick(nextPredecessor) > Sick(node)
    return FindLastWorseThan(nextPredecessor -> left,Sick(node));
}

// Finds the latest day within the given subtree with root "node" where
// the infection number is larger than x. Runs in O(log(size(q)).
int FindLastWorseThan(Node q,int x) {
    if ((q -> right) = null and Sick(q) > x) return Day(q);
    if (maxRightSick(q) > x) return FindLastWorseThan(q -> right,x);
    if (maxLeftSick(q) > x) return FindLastWorseThan(q -> left,x);
    return -1;
}
,

首先,您选择的数据结构在我看来不错。您没有明确提及它,但我假设您在 AVL 树中使用的“键”是日期编号,即树的有序遍历将按时间顺序列出节点。

我只是建议一个表面的改变:在节点本身中存储 sick 的最大值,这样你就不会存储两个相似的信息(maxLeftSickmaxRightSick)在一个节点实例中,但将这两个信息移动到子节点,以便您的 node.maxLeftSick 实际上存储在 node.left.maxSick 中,类似地 node.maxRightSick 存储在 node.right.maxSick 中。当那个孩子不存在时,这当然不会完成,但我们也不需要这些信息。在您的结构中,当 maxLeftSick 未定义时,left 将为 0。在我提议的结构中,你不会有那个值——0 会很自然地跟随着没有 left 孩子的事实。在我的提议中,根节点将在 maxSick 中包含一个信息,而您的信息中不存在该信息,该信息将是您的 root.maxLeftSickroot.maxRightSick 的总和。这些信息不会真正被使用,但它只是为了使整个树的结构保持一致。

因此,您只需存储一个 maxSick,它会将当前节点的 sick 值也考虑在该最大值中。您在轮换期间所做的处理需要相应地改变,但不会变得更复杂。

我假设您的 AVL 树是单线程的,即您不跟踪父指针。因此,创建一个 find 方法,它将返回要找到的节点的路径。例如,在 Python 语法中,它可能如下所示:

def find(self,day):
    node = self.root
    path = []  # an array of nodes
    while node:
        path.append(node)
        if node.day == day:  # bingo
            return path
        if day < node.day:
            node = node.left
        else:
            node = node.right

那么 worstBefore 方法可能如下所示:

def worstBefore(self,day):
    path = self.find(day)
    if not path:
        return  # day not found
    # get number of sick people on that day:
    sick = path[-1].sick
    # look for recent day with greater number of sick
    while path:
        node = path.pop()  # walk upward,starting with found node
        if node.day < day and node.sick > sick:
            return node.day
        if node.left and node.left.maxSick > sick:
            # we will find the result in this subtree
            node = node.left
            while True:
                if node.right and node.right.maxSick > sick:
                    node = node.right
                elif node.sick > sick:  # bingo
                    return node.day
                else:
                    node = node.left

因此,当您需要沿着该路径在树中向上回溯时,将使用 find 方法返回的路径来获取节点的父节点。

如果沿着这条路径找到一个左子节点,其 maxSick 更大,那么你就知道目标节点必须在那个子树中。然后以受控的方式沿着该子树走下去,当它仍然有 maxSick 大时选择正确的孩子。否则检查当前节点的 sick 值,如果该值更大,则返回该值。否则向左走,然后重复。

虽然没有这样的左子树,但沿着路径向上走。如果该父对象匹配,则返回它(确保验证日期编号)。继续检查具有较大 maxSick 的左子树。

这在 O(logn) 中运行,因为您首先将向上走零步或更多步,然后向下走零步或更多步(在左子树中)。

您可以看到您的示例场景在 repl.it 上运行。在那里我专注于这个问题,并没有实施轮换。