查找二进制堆的最后一个元素

时间:2009-02-01 02:42:39

标签: algorithm data-structures binary-tree binary-heap

引用Wikipedia

  

使用a是完全可以接受的   传统的二叉树数据结构   实现二进制堆。有   找到相邻的问题   在最后一级的元素   添加元素时的二进制堆   可以解决   算法 ...

关于这种算法如何运作的任何想法?

我无法找到有关此问题的任何信息,因为大多数二进制堆是使用数组实现的。

任何帮助表示感谢。


最近,我注册了一个OpenID帐户,无法编辑我的初始帖子或评论答案。这就是我通过这个答案回应的原因。对不起。


引用米奇小麦:

  

@Yse:你的问题是“我怎么找到   二进制堆的最后一个元素“?

是的,确实如此。 或者更确切地说,我的问题是:“如何找到非基于数组的二进制堆的最后一个元素?”。

引用Suppressingfire:

  

是否存在某些背景信息   问这个问题? (即,是吗?   你正在尝试的一些具体问题   解决?)

如上所述,我想知道“找到非基于数组的二进制堆的最后一个元素”的好方法,这是插入和删除节点所必需的。

引用罗伊:

  

对我来说,这似乎是最容易理解的   只需使用普通的二叉树   结构(使用pRoot和节点   定义为[data,pLeftChild,   pRightChild])并另外添加两个   指针(pInsertionNode和   pLastNode)。 pInsertionNode和   pLastNode将在更新期间更新   插入和删除子程序   在数据时保持它们是最新的   在结构内变化。这个   给O(1)访问两个插入   结构的点和最后一个节点。

是的,这应该有效。如果我没有弄错,找到插入节点和最后一个节点,当它们的位置由于删除/插入而变为另一个子树时,可能会有点棘手。但我会试一试。

引用Zach Scrivena:

  

如何执行深度优先   搜索...

是的,这将是一个很好的方法。我也会尝试一下。

我仍然想知道,如果有办法“计算”最后一个节点和插入点的位置。具有N个节点的二进制堆的高度可以通过获取大于N的最小二次幂的log(基数2)来计算。也许可以计算最深级别上的节点数。然后可能确定如何遍历堆以到达插入点或节点以进行删除。

6 个答案:

答案 0 :(得分:17)

基本上,引用的语句指的是解决在堆中插入和删除数据元素的位置的问题。为了保持二进制堆的“形状属性”,必须始终从左到右填充堆的最低级别,不留空节点。要保持二进制堆的平均O(1)插入和删除时间,您必须能够确定下一次插入的位置以及最低级别上用于删除根节点的最后一个节点的位置,两者都是在不断的时间。

对于存储在数组中的二进制堆(具有隐含的,压缩的数据结构,如Wikipedia条目中所述),这很容易。只需在数组末尾插入最新的数据成员,然后将其“冒泡”到位(遵循堆规则)。或者将数组中的最后一个元素替换为“bubbling down”以进行删除。对于数组存储中的堆,堆中元素的数量是一个隐式指针,指向下一个数据元素的插入位置以及在哪里找到用于删除的最后一个元素。

对于存储在树结构中的二进制堆,此信息不是那么明显,但由于它是完整的二叉树,因此可以对其进行计算。例如,在具有4个元素的完整二叉树中,插入点将始终是根节点的左子节点的右子节点。用于删除的节点将始终是根节点的左子节点的左子节点。对于任何给定的任意树大小,树将始终具有特定的形状,具有明确定义的插入和删除点。因为树是具有任何给定大小的特定结构的“完整二叉树”,所以很有可能在O(1)时间内计算插入/删除的位置。然而,问题是,即使你知道它在结构上的位置,你也不知道节点在内存中的位置。因此,您必须遍历树以到达给定节点,这是一个O(log n)进程,使所有插入和删除最少为O(log n),从而打破了通常所需的O(1)行为。任何搜索(“深度优先”或其他一些)也将至少为O(log n),因为所记录的遍历问题通常是O(n),因为半排序堆的随机性。

技巧是通过增加数据结构(“线程化”树,如同),能够在恒定时间内计算引用这些插入/删除点在维基百科文章中提及)或使用其他指针。

在我看来最容易理解的实现,具有低内存和额外的编码开销,只是使用普通的简单二叉树结构(使用pRoot和Node定义为[data,pParent,pLeftChild,pRightChild] ])并添加两个额外的指针(pInsert和pLastNode)。在插入和删除子例程期间,pInsert和pLastNode都会更新,以便在结构中的数据发生更改时保持当前状态。此实现使O(1)访问结构的插入点和最后一个节点,并且应该允许在插入和删除中保留整体O(1)行为。实现的成本是插入/删除子程序中的两个额外指针和一些小额外代码(也就是最小)。

编辑:为O(1)插入添加了伪代码()

这是插入子程序的伪代码,平均为O(1):

define Node = [T data, *pParent, *pLeft, *pRight]

void insert(T data)
{
    do_insertion( data );   // do insertion, update count of data items in tree

    # assume: pInsert points node location of the tree that where insertion just took place
    #   (aka, either shuffle only data during the insertion or keep pInsert updated during the bubble process)

    int N = this->CountOfDataItems + 1;     # note: CountOfDataItems will always be > 0 (and pRoot != null) after an insertion

    p = new Node( <null>, null, null, null);        // new empty node for the next insertion

    # update pInsert (three cases to handle)
    if ( int(log2(N)) == log2(N) )
        {# #1 - N is an exact power of two
        # O(log2(N))
        # tree is currently a full complete binary tree ("perfect")
        # ... must start a new lower level
        # traverse from pRoot down tree thru each pLeft until empty pLeft is found for insertion
        pInsert = pRoot;
        while (pInsert->pLeft != null) { pInsert = pInsert->pLeft; }    # log2(N) iterations
        p->pParent = pInsert;
        pInsert->pLeft = p;
        }
    else if ( isEven(N) )
        {# #2 - N is even (and NOT a power of 2)
        # O(1)
        p->pParent = pInsert->pParent;
        pInsert->pParent->pRight = p;
        }
    else 
        {# #3 - N is odd
        # O(1)
        p->pParent = pInsert->pParent->pParent->pRight;
        pInsert->pParent->pParent->pRight->pLeft = p;
        }
    pInsert = p;

    // update pLastNode
    // ... [similar process]
}

因此,插入(T)平均为O(1):在所有情况下都精确为O(1),除非当树为O(log N)时树必须增加一级,这发生在每个log N插入(假设没有删除)。添加另一个指针(pLeftmostLeaf)可以使所有情况下的insert()O(1)并避免交替插入的可能的病理情况&amp;删除完整的完整二叉树。 (添加pLeftmost是一个练习[很容易]。)

答案 1 :(得分:7)

我第一次参与堆栈溢出。

是的,Zach Scrivena的上述回答(上帝,我不知道如何正确地引用其他人,对不起)是对的。如果给出节点数,我想要添加的是简化方法。

基本理念是:

假设此完整二叉树中的节点数为N,请执行&#34; N%2&#34;计算并将结果推入堆栈。继续计算,直到N == 1.然后弹出结果。结果为1表示正确,0表示向左。序列是从根到目标位置的路线。

示例:

树现在有10个节点,我想在位置11插入另一个节点。如何路由它?

11 % 2 = 1  --> right    (the quotient is 5, and push right into stack)
 5 % 2 = 1  --> right    (the quotient is 2, and push right into stack)
 2 % 2 = 0  --> left     (the quotient is 1, and push left into stack. End)

然后弹出堆栈:left - &gt;对 - &gt;对。这是从根开始的路径。

答案 2 :(得分:6)

您可以使用二进制堆大小的二进制表示来查找O(log N)中最后一个节点的位置。可以存储和递增大小,这将花费O(1)时间。这背后的基本概念是二叉树的结构。

假设我们的堆大小为7. 7的二进制表示是&#34; 111&#34;。现在,记住要始终省略第一位。所以,现在我们留下了&#34; 11&#34;。从左到右阅读。该位是&#39; 1&#39;,因此,转到根节点的右子节点。然后左边的字符串是&#34; 1&#34;,第一位是&#39; 1&#39;。因此,再次转到您所在的当前节点的右侧子节点。由于您不再需要处理位,这表示您已到达最后一个节点。因此,该过程的原始工作是将堆的大小转换为位。省略第一位。根据最左边的位,如果它是&#39; 1,则转到当前节点的右子节点,如果是&#39; 0,则转到当前节点的左子节点。

当您始终到二叉树的最后时,此操作始终需要O(log N)时间。这是查找最后一个节点的简单而准确的过程。

您在第一次阅读时可能无法理解。尝试在纸上使用这种方法来获得二元堆的不同值,我确定你会得到它背后的直觉。我确信这些知识足以解决您的问题,如果您想要更多数字解释,可以参考我的blog

希望我的回答对你有帮助,如果有,请告诉我......! ☺

答案 3 :(得分:1)

如何执行depth-first search,在正确的孩子面前访问左孩子,以确定树的高度。此后,您遇到的较短深度的第一片叶子或有失踪孩子的父母将指示您应该在“冒泡”之前放置新节点的位置。


上面的深度优先搜索(DFS)方法并不假设您知道树中的节点总数。如果此信息可用,那么我们可以通过使用完整二叉树的属性快速“放大”到所需位置:

N 为树中的节点总数, H 为树的高度。

N H )的某些值为(1,0),(2,1),(3,1),(4,2), ......,(7,2),(8,3)。 与这两者相关的通式是 H = ceil [log2( N +1)] - 1。 现在,仅给出 N ,我们希望以最少的步数从根到达新节点的位置,即没有任何“回溯”。 我们首先计算高度 H = ceil的完美二叉树中节点 M 的总数[log2( N +1)] - 1, M = 2 ^( H +1) - 1.

如果 N == M ,那么我们的树完美,新节点应添加到新级别。这意味着我们可以简单地执行DFS(左前),直到我们点击第一个叶子;新节点成为此叶子的左子节点。故事结束。

但是,如果 N &lt; M ,然后我们树的最后一层仍有空位,新节点应添加到最左边的空位。 已经位于树的最后一级的节点数量只是( N - 2 ^ H + 1)。 这意味着新节点在最后一级从左侧获取点 X =( N - 2 ^ H + 2)。

现在,要从根部到达那里,你需要在每个级别进行正确的转弯(L vs R),这样你才能在最后一级找到 X 点。在实践中,您将通过在每个级别进行一些计算来确定转弯。但是,我认为下表显示了大图和相关模式而没有陷入算术(你可能会认为这是一种arithmetic coding的统一分布形式):

0 0 0 0 0 X 0 0 <--- represents the last level in our tree, X marks the spot!
          ^
L L L L R R R R <--- at level 0, proceed to the R child
L L R R L L R R <--- at level 1, proceed to the L child
L R L R L R L R <--- at level 2, proceed to the R child 
          ^                      (which is the position of the new node)
          this column tells us
          if we should proceed to the L or R child at each level

编辑:假设我们知道树中节点的总数,在最短的步骤中添加了关于如何到达新节点的描述。

答案 4 :(得分:0)

解决方案,以防你没有父母的参考! 要找到下一个节点的正确位置,您需要处理3个案例

  • case(1)树级别完成Log2(N)
  • case(2)树节点数是偶数
  • case(3)树节点计数为奇数

插入:

void Insert(Node root,Node n)
{
Node parent = findRequiredParentToInsertNewNode (root);
if(parent.left == null)
parent.left = n;
else
parent.right = n;
}

找到节点的父节点以便插入

void findRequiredParentToInsertNewNode(Node root){

Node last = findLastNode(root);

 //Case 1 
 if(2*Math.Pow(levelNumber) == NodeCount){
     while(root.left != null)
      root=root.left;
  return root;
 }
 //Case 2
 else if(Even(N)){
   Node n =findParentOfLastNode(root ,findParentOfLastNode(root ,last));
 return n.right;
 }
//Case 3
 else if(Odd(N)){
  Node n =findParentOfLastNode(root ,last);
 return n;
 }

}

要查找最后一个节点,您需要执行BFS(广度优先搜索)并获取队列中的最后一个元素

 Node findLastNode(Node root)
 {
     if (root.left == nil)
         return root

     Queue q = new Queue();

     q.enqueue(root);
     Node n = null;

     while(!q.isEmpty()){
      n = q.dequeue();
     if ( n.left != null )
         q.enqueue(n.left);
     if ( n.right != null )
         q.enqueue(n.right);
        }
   return n;
}

查找最后一个节点的父节点,以便在删除情况下替换为root时将节点设置为null

 Node findParentOfLastNode(Node root ,Node lastNode)
{
    if(root == null)
        return root;

    if( root.left == lastNode || root.right == lastNode )
        return root;

    Node n1= findParentOfLastNode(root.left,lastNode);
    Node n2= findParentOfLastNode(root.left,lastNode);

    return n1 != null ? n1 : n2;
}

答案 5 :(得分:0)

我知道这是一个旧帖子,但我正在寻找同一个问题的答案。但我无法负担o(log n)解决方案,因为我必须在几秒钟内找到最后一个节点数千次。我确实有一个O(log n)算法,但我的程序是爬行的,因为它执行此操作的次数。经过深思熟虑,我终于找到了解决这个问题的方法。不确定这是否有趣的事情。

此解决方案是O(1)用于搜索。对于插入,它肯定小于O(log n),虽然我不能说它是O(1)。

只是想补充一点,如果有兴趣,我也可以提供我的解决方案。 解决方案是将二进制堆中的节点添加到队列中。每个队列节点都有前后指针。我们继续从左到右将节点添加到此队列的末尾,直到我们到达二进制堆中的最后一个节点。此时,二进制堆中的最后一个节点将位于队列的后面。 每次我们需要找到最后一个节点时,我们从后面出列,而倒数第二个现在成为树中的最后一个节点。 当我们想要插入时,我们从后面向后搜索我们可以插入的第一个节点并将其放在那里。它不完全是O(1),但会大大缩短运行时间。