问题描述
我正在解决以下求职面试问题并解决了大部分问题,但在最后一个要求上失败了。
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
- 右子的最大感染人数。
当插入一个新节点时,我确保不会丢失轮换数据,对于从新节点到我所做的根节点的每个单个节点:
但我没有成功实施 WorseBefore(d)
。
解决方法
在哪里搜索?
首先需要在按天排序的树中找到node
对应的节点d
。让x = Sick(node)
。这可以在 O(log n) 内完成。
如果 maxLeftSick(node) > x
,解必须在 node
的左子树中。在那里搜索解决方案并返回答案。这可以在 O(log n) 中完成 - 见下文。
否则,从node
开始向上朝根遍历树,直到找到满足该属性的第一个节点nextPredecessor
(这需要O(log n) ):
-
nextPredecessor
小于node
, - 或者
-
Sick(nextPredecessor) > x
或 -
maxLeftSick(nextPredecessor) > x
。
-
如果不存在这样的节点,我们放弃。在第 1 种情况下,只需返回 nextPredecessor
,因为这是最佳解决方案。
在情况 2 中,我们知道解必须在 nextPredecessor
的左子树中,因此在那里搜索并返回答案。同样,这需要 O(log n) - 见下文。
请注意,无需在 nextPredecessor
的 右 子树中搜索,因为该子树中唯一小于 node
的节点将是node
本身,我们已经排除了。
另请注意,没有必要比 nextPredecessor
更向上遍历树,因为这些节点甚至更小,我们正在寻找满足所有约束的最大节点。 >
如何搜索?
好的,那么我们如何在子树中搜索解决方案呢?使用 q
和 x
信息,在以 maxLeftSick
为根的子树中找到比感染数 maxRightSick
更糟糕的最大一天很简单:
- 如果
q
有一个右孩子和maxRightSick(q) > x
,则在q
的右子树中搜索。 - 如果
q
没有右孩子和Sick(q) > x
,则返回Day(q)
。 - 如果
q
有一个左孩子和maxLeftSick(q) > x
,则在q
的左子树中搜索。 - 否则子树
q
中没有解。
我们有效地使用 maxLeftSick
和 maxRightSick
修剪搜索树以仅包含“更差”的节点,并且在修剪后的树中我们得到最正确的节点,即具有最大日期的节点.
很容易看出该算法在 O(log n)
中运行,其中 n
是节点总数,因为步数受树的高度限制。
伪代码
这是伪代码(假设 maxLeftSick
和 maxRightSick
如果不存在相应的子节点,则返回 -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
的最大值,这样你就不会存储两个相似的信息(maxLeftSick
和 maxRightSick
)在一个节点实例中,但将这两个信息移动到子节点,以便您的 node.maxLeftSick
实际上存储在 node.left.maxSick
中,类似地 node.maxRightSick
存储在 node.right.maxSick
中。当那个孩子不存在时,这当然不会完成,但我们也不需要这些信息。在您的结构中,当 maxLeftSick
未定义时,left
将为 0。在我提议的结构中,你不会有那个值——0 会很自然地跟随着没有 left
孩子的事实。在我的提议中,根节点将在 maxSick
中包含一个信息,而您的信息中不存在该信息,该信息将是您的 root.maxLeftSick
和 root.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 上运行。在那里我专注于这个问题,并没有实施轮换。