以最优方式在二叉搜索树中找到第 k 个最小元素

Posted

技术标签:

【中文标题】以最优方式在二叉搜索树中找到第 k 个最小元素【英文标题】:Find kth smallest element in a binary search tree in Optimum way 【发布时间】:2011-01-20 17:06:20 【问题描述】:

我需要在不使用任何静态/全局变量的情况下找到二叉搜索树中的第 k 个最小元素。如何有效地实现它? 我想到的解决方案是在 O(n) 中进行操作,这是最坏的情况,因为我打算对整个树进行中序遍历。但在内心深处,我觉得我没有在这里使用 BST 属性。我的假设解决方案是正确的还是有更好的解决方案?

【问题讨论】:

树平衡了吗? 不是。但如果是平衡的,有没有最优的方法? 如果您在“订单统计”上进行搜索,您会找到您需要的。 我感觉下面的大部分答案都是正确的,因为他们正在使用某种全局变量(无论是对整数的引用,还是递减并返回的变量) )。如果绝对不允许这些,我会使用递归而不传入任何引用。 【参考方案1】:

这里只是一个想法的大纲:

在 BST 中,节点 T 的左子树只包含小于存储在 T 中的值的元素。如果k 小于左子树中的元素个数,则kth 最小的元素必须属于左子树。否则,如果k 较大,则kth 最小的元素在右子树中。

我们可以扩充 BST,让其中的每个节点都存储其左子树中的元素数量(假设给定节点的左子树包括该节点)。有了这条信息,就很容易遍历树,反复询问左子树的元素个数,决定递归到左子树还是右子树。

现在,假设我们在节点 T:

    如果 k == num_elements(T 的左子树),那么我们要寻找的答案就是节点 T 中的值。 如果k > num_elements(left subtree of T),那么显然我们可以忽略左子树,因为这些元素也会小于kth 最小的。因此,我们将问题简化为找到右子树的k - num_elements(left subtree of T) 最小元素。 如果k ,那么kth 最小的元素在左子树中的某个位置,所以我们将问题简化为找到kth 最小元素左子树。

复杂性分析:

这需要 O(depth of node) 时间,在平衡 BST 的最坏情况下是 O(log n),对于随机 BST 来说平均是 O(log n)

一个 BST 需要 O(n) 存储,并且需要另一个 O(n) 来存储有关元素数量的信息。所有 BST 操作都需要O(depth of node) 时间,并且需要O(depth of node) 额外时间来维护用于插入、删除或旋转节点的“元素数量”信息。因此,在左子树中存储元素个数的信息可以保持 BST 的空间和时间复杂度。

【讨论】:

要找到第N个最小的项,只需要存储左子树的大小即可。如果您还希望能够找到第 N 个最大的项目,则可以使用右子树的大小。实际上,您可以降低成本:将树的总大小存储在根中,以及左子树的大小。当需要调整右子树的大小时,可以从总大小中减去左子树的大小。 这种增强的 BST 被称为“订单统计树”。 @Ivlad:在第 2 步中:我认为“k - num_elements”应该是“k - num_elements -1”,因为您也必须包含根元素。 @understack - 如果您假设根是子树的一部分,则不会。 如果树不包含包含“其左右子树中的元素数”的字段,那么该方法最终将是 BigO(n),因为您需要向右走或每个节点的左子树,以计算当前节点的第k个索引。【参考方案2】:

一个更简单的解决方案是进行中序遍历并跟踪当前要打印的元素(不打印它)。当我们到达 k 时,打印元素并跳过其余的树遍历。

void findK(Node* p, int* k) 
  if(!p || k < 0) return;
  findK(p->left, k);
  --k;
  if(k == 0)  
    print p->data;
    return;  
   
  findK(p->right, k); 

【讨论】:

+1:思路是对的,但有些松散的地方可能需要收紧;见***.com/a/23069077/278326 我喜欢这个解决方案,因为 BST 已经排序,一次遍历就足够了。 如果 n 接近这棵树中的节点总数,你的算法将需要 O(n) 时间才能完成,这对所选答案不利-O(log n)【参考方案3】:
public int ReturnKthSmallestElement1(int k)
    
        Node node = Root;

        int count = k;

        int sizeOfLeftSubtree = 0;

        while(node != null)
        

            sizeOfLeftSubtree = node.SizeOfLeftSubtree();

            if (sizeOfLeftSubtree + 1 == count)
                return node.Value;
            else if (sizeOfLeftSubtree < count)
            
                node = node.Right;
                count -= sizeOfLeftSubtree+1;
            
            else
            
                node = node.Left;
            
        

        return -1;
    

这是我基于上述算法在 C# 中的实现,只是想我会发布它,以便人们更好地理解它对我有用

谢谢你,IVlad

【讨论】:

【参考方案4】:

//添加一个没有递归的java版本

public static <T> void find(TreeNode<T> node, int num)
    Stack<TreeNode<T>> stack = new Stack<TreeNode<T>>();

    TreeNode<T> current = node;
    int tmp = num;

    while(stack.size() > 0 || current!=null)
        if(current!= null)
            stack.add(current);
            current = current.getLeft();
        else
            current = stack.pop();
            tmp--;

            if(tmp == 0)
                System.out.println(current.getValue());
                return;
            

            current = current.getRight();
        
    

【讨论】:

我喜欢这个解决方案和相应的递归解决方案。老实说,这个问题的大多数答案都太令人困惑/太复杂了,无法阅读。 我喜欢这个解决方案!清晰而伟大! 此解决方案是“按顺序”遍历树并在访问节点后减少计数器,稍后在计数器为零时停止。最坏的情况是 O(n) 阶。与@IVlad 的递归解决方案相比,它不是最优化的,最坏的情况是 O(log n)【参考方案5】:

更简单的解决方案是进行中序遍历并使用计数器 k 跟踪当前要打印的元素。当我们到达 k 时,打印元素。运行时间为 O(n)。请记住函数返回类型不能为 void,它必须在每次递归调用后返回其更新后的 k 值。一个更好的解决方案是在每个节点上使用一个经过排序的位置值的增强型 BST。

public static int kthSmallest (Node pivot, int k)
    if(pivot == null )
        return k;   
    k = kthSmallest(pivot.left, k);
    k--;
    if(k == 0)
        System.out.println(pivot.value);
    
    k = kthSmallest(pivot.right, k);
    return k;

【讨论】:

我猜你的解决方案在空间复杂度方面比增强的 BST 更好。 即使找到了第 k 个最小的元素,搜索也不会停止。【参考方案6】:

您可以使用迭代中序遍历: http://en.wikipedia.org/wiki/Tree_traversal#Iterative_Traversal 从堆栈中弹出一个节点后,只需检查第 k 个元素。

【讨论】:

【参考方案7】:

给定一个简单的二叉搜索树,你所能做的就是从最小的开始,向上遍历找到正确的节点。

如果您要经常这样做,您可以为每个节点添加一个属性,表示其左子树中有多少个节点。使用它,您可以将树直接下降到正确的节点。

【讨论】:

【参考方案8】:

带计数器的递归有序遍历

Time Complexity: O( N ), N is the number of nodes
Space Complexity: O( 1 ), excluding the function call stack

这个想法类似于@prasadvk 解决方案,但它有一些缺点(见下面的注释),所以我将其作为单独的答案发布。

// Private Helper Macro
#define testAndReturn( k, counter, result )                         \
    do  if( (counter == k) && (result == -1) )                    \
        result = pn->key_;                                          \
        return;                                                     \
      while( 0 )

// Private Helper Function
static void findKthSmallest(
    BstNode const * pn, int const k, int & counter, int & result ) 

    if( ! pn ) return;

    findKthSmallest( pn->left_, k, counter, result );
    testAndReturn( k, counter, result );

    counter += 1;
    testAndReturn( k, counter, result );

    findKthSmallest( pn->right_, k, counter, result );
    testAndReturn( k, counter, result );


// Public API function
void findKthSmallest( Bst const * pt, int const k ) 
    int counter = 0;
    int result = -1;        // -1 := not found
    findKthSmallest( pt->root_, k, counter, result );
    printf("%d-th element: element = %d\n", k, result );

注意事项(以及与@prasadvk 解决方案的区别):

    if( counter == k ) 需要在 三个 处进行测试:(a) 在左子树之后,(b) 在根之后,和 (c) 在右子树之后。这是为了确保为所有位置检测到第 k 个元素,即不管它位于哪个子树。

    需要if( result == -1 ) 测试以确保只打印结果元素,否则打印从第 k 个最小元素到根的所有元素。

【讨论】:

此解决方案的时间复杂度为O(k + d),其中d 是树的最大深度。因此它使用了一个全局变量counter,但这对于这个问题是非法的。 嗨阿伦,你能举个例子解释一下吗?我不明白这一点,尤其是你的第一点。【参考方案9】:

对于平衡搜索树,需要O(n)

对于平衡搜索树,在最坏的情况下需要O(k + log n),而O(k)在摊销感。

拥有和管理每个节点的额外整数:子树的大小给出了O(log n)的时间复杂度。 这种平衡的搜索树通常称为RankTree。

一般来说,有解决方案(不是基于树)。

问候。

【讨论】:

【参考方案10】:

这很好用: status : 是保存是否找到元素的数组。 k :是要找到的第 k 个元素。 count :跟踪树遍历期间遍历的节点数。

int kth(struct tree* node, int* status, int k, int count)

    if (!node) return count;
    count = kth(node->lft, status, k, count);  
    if( status[1] ) return status[0];
    if (count == k)  
        status[0] = node->val;
        status[1] = 1;
        return status[0];
    
    count = kth(node->rgt, status, k, count+1);
    if( status[1] ) return status[0];
    return count;

【讨论】:

【参考方案11】:

虽然这绝对不是问题的最佳解决方案,但它是我认为有些人可能会感兴趣的另一个潜在解决方案:

/**
 * Treat the bst as a sorted list in descending order and find the element 
 * in position k.
 *
 * Time complexity BigO ( n^2 )
 *
 * 2n + sum( 1 * n/2 + 2 * n/4 + ... ( 2^n-1) * n/n ) = 
 * 2n + sigma a=1 to n ( (2^(a-1)) * n / 2^a ) = 2n + n(n-1)/4
 *
 * @param t The root of the binary search tree.
 * @param k The position of the element to find.
 * @return The value of the element at position k.
 */
public static int kElement2( Node t, int k ) 
    int treeSize = sizeOfTree( t );

    return kElement2( t, k, treeSize, 0 ).intValue();


/**
 * Find the value at position k in the bst by doing an in-order traversal 
 * of the tree and mapping the ascending order index to the descending order 
 * index.
 *
 *
 * @param t Root of the bst to search in.
 * @param k Index of the element being searched for.
 * @param treeSize Size of the entire bst.
 * @param count The number of node already visited.
 * @return Either the value of the kth node, or Double.POSITIVE_INFINITY if 
 *         not found in this sub-tree.
 */
private static Double kElement2( Node t, int k, int treeSize, int count ) 
    // Double.POSITIVE_INFINITY is a marker value indicating that the kth 
    // element wasn't found in this sub-tree.
    if ( t == null )
        return Double.POSITIVE_INFINITY;

    Double kea = kElement2( t.getLeftSon(), k, treeSize, count );

    if ( kea != Double.POSITIVE_INFINITY )
        return kea;

    // The index of the current node.
    count += 1 + sizeOfTree( t.getLeftSon() );

    // Given any index from the ascending in order traversal of the bst, 
    // treeSize + 1 - index gives the
    // corresponding index in the descending order list.
    if ( ( treeSize + 1 - count ) == k )
        return (double)t.getNumber();

    return kElement2( t.getRightSon(), k, treeSize, count );

【讨论】:

【参考方案12】:

签名:

Node * find(Node* tree, int *n, int k);

调用为:

*n = 0;
kthNode = find(root, n, k);

定义:

Node * find ( Node * tree, int *n, int k)

   Node *temp = NULL;

   if (tree->left && *n<k)
      temp = find(tree->left, n, k);

   *n++;

   if(*n==k)
      temp = root;

   if (tree->right && *n<k)
      temp = find(tree->right, n, k);

   return temp;

【讨论】:

【参考方案13】:

这是我的 2 美分...

int numBSTnodes(const Node* pNode)
     if(pNode == NULL) return 0;
     return (numBSTnodes(pNode->left)+numBSTnodes(pNode->right)+1);



//This function will find Kth smallest element
Node* findKthSmallestBSTelement(Node* root, int k)
     Node* pTrav = root;
     while(k > 0)
         int numNodes = numBSTnodes(pTrav->left);
         if(numNodes >= k)
              pTrav = pTrav->left;
         
         else
              //subtract left tree nodes and root count from 'k'
              k -= (numBSTnodes(pTrav->left) + 1);
              if(k == 0) return pTrav;
              pTrav = pTrav->right;
        

        return NULL;
 

【讨论】:

【参考方案14】:

这就是我的想法,它确实有效。它将运行在 o(log n)

public static int FindkThSmallestElemet(Node root, int k)
    
        int count = 0;
        Node current = root;

        while (current != null)
        
            count++;
            current = current.left;
        
        current = root;

        while (current != null)
        
            if (count == k)
                return current.data;
            else
            
                current = current.left;
                count--;
            
        

        return -1;


     // end of function FindkThSmallestElemet

【讨论】:

我认为这个解决方案行不通。如果第 K 个最小的在树节点的右子树中怎么办?【参考方案15】:

好吧,我们可以简单地使用 in order 遍历并将访问的元素压入堆栈。 弹出k次,得到答案。

我们也可以在 k 个元素之后停止

【讨论】:

这不是最佳解决方案【参考方案16】:

完整 BST 案例的解决方案:-

Node kSmallest(Node root, int k) 
  int i = root.size(); // 2^height - 1, single node is height = 1;
  Node result = root;
  while (i - 1 > k) 
    i = (i-1)/2;  // size of left subtree
    if (k < i) 
      result = result.left;
     else 
      result = result.right;
      k -= i;
      
  
  return i-1==k ? result: null;

【讨论】:

【参考方案17】:

Linux 内核具有出色的增强型红黑树数据结构,支持 linux/lib/rbtree.c 中 O(log n) 中基于等级的操作。

还可以在http://code.google.com/p/refolding/source/browse/trunk/core/src/main/java/it/unibo/refolding/alg/RbTree.java 找到一个非常粗略的 Java 端口,以及 RbRoot.java 和 RbNode.java。第n个元素可以通过调用RbNode.nth(RbNode node, int n)获取,传入树的根节点。

【讨论】:

【参考方案18】:

这是 C# 中的一个简洁版本,它返回第 k 个最小元素,但需要将 k 作为 ref 参数传入(与 @prasadvk 的方法相同) :

Node FindSmall(Node root, ref int k)

    if (root == null || k < 1)
        return null;

    Node node = FindSmall(root.LeftChild, ref k);
    if (node != null)
        return node;

    if (--k == 0)
        return node ?? root;
    return FindSmall(root.RightChild, ref k);

找到最小的节点是O(log n),然后遍历到第k个节点是O(k),所以是O(k + log n)。

【讨论】:

java版本怎么样?【参考方案19】:

http://www.geeksforgeeks.org/archives/10379

这是这个问题的确切答案:-

1.在 O(n) 时间内使用中序遍历 2.在k+log n次使用增广树

【讨论】:

【参考方案20】:

我找不到更好的算法..所以决定写一个:) 如果这是错误的,请纠正我。

class KthLargestBST
protected static int findKthSmallest(BSTNode root,int k)//user calls this function
    int [] result=findKthSmallest(root,k,0);//I call another function inside
    return result[1];

private static int[] findKthSmallest(BSTNode root,int k,int count)//returns result[]2 array containing count in rval[0] and desired element in rval[1] position.
    if(root==null)
        int[]  i=new int[2];
        i[0]=-1;
        i[1]=-1;
        return i;
    else
        int rval[]=new int[2];
        int temp[]=new int[2];
        rval=findKthSmallest(root.leftChild,k,count);
        if(rval[0]!=-1)
            count=rval[0];
        
        count++;
        if(count==k)
            rval[1]=root.data;
        
        temp=findKthSmallest(root.rightChild,k,(count));
        if(temp[0]!=-1)
            count=temp[0];
        
        if(temp[1]!=-1)
            rval[1]=temp[1];
        
        rval[0]=count;
        return rval;
    

public static void main(String args[])
    BinarySearchTree bst=new BinarySearchTree();
    bst.insert(6);
    bst.insert(8);
    bst.insert(7);
    bst.insert(4);
    bst.insert(3);
    bst.insert(4);
    bst.insert(1);
    bst.insert(12);
    bst.insert(18);
    bst.insert(15);
    bst.insert(16);
    bst.inOrderTraversal();
    System.out.println();
    System.out.println(findKthSmallest(bst.root,11));

【讨论】:

【参考方案21】:

这里是java代码,

ma​​x(Node root, int k) - 找到第 k 个最大的

min(Node root, int k) - 找到第 k 个最小的

static int count(Node root)
    if(root == null)
        return 0;
    else
        return count(root.left) + count(root.right) +1;

static int max(Node root, int k) 
    if(root == null)
        return -1;
    int right= count(root.right);

    if(k == right+1)
        return root.data;
    else if(right < k)
        return max(root.left, k-right-1);
    else return max(root.right, k);


static int min(Node root, int k) 
    if (root==null)
        return -1;

    int left= count(root.left);
    if(k == left+1)
        return root.data;
    else if (left < k)
        return min(root.right, k-left-1);
    else
        return min(root.left, k);

【讨论】:

【参考方案22】:

这也可以。只需在树中调用带有 maxNode 的函数

def k_largest(self, node, k): 如果 k 如果 k == 0: 返回节点 别的: k -=1 return self.k_largest(self.predecessor(node), k)

【讨论】:

【参考方案23】:

我认为这比接受的答案更好,因为它不需要修改原始树节点来存储它的子节点的数量。

我们只需要使用中序遍历从左到右计数最小的节点,一旦计数等于K就停止搜索。

private static int count = 0;
public static void printKthSmallestNode(Node node, int k)
    if(node == null)
        return;
    

    if( node.getLeftNode() != null )
        printKthSmallestNode(node.getLeftNode(), k);
    

    count ++ ;
    if(count <= k )
        System.out.println(node.getValue() + ", count=" + count + ", k=" + k);

    if(count < k  && node.getRightNode() != null)
        printKthSmallestNode(node.getRightNode(), k);

【讨论】:

【参考方案24】:

最好的方法已经存在。但我想为此添加一个简单的代码

int kthsmallest(treenode *q,int k)
int n = size(q->left) + 1;
if(n==k)
    return q->val;

if(n > k)
    return kthsmallest(q->left,k);

if(n < k)
    return kthsmallest(q->right,k - n);

int size(treenode *q)
if(q==NULL)
    return 0;

else
    return ( size(q->left) + size(q->right) + 1 );

【讨论】:

【参考方案25】:

使用辅助Result类来跟踪是否找到节点和当前k。

public class KthSmallestElementWithAux 

public int kthsmallest(TreeNode a, int k) 
    TreeNode ans = kthsmallestRec(a, k).node;
    if (ans != null) 
        return ans.val;
     else 
        return -1;
    


private Result kthsmallestRec(TreeNode a, int k) 
    //Leaf node, do nothing and return
    if (a == null) 
        return new Result(k, null);
    

    //Search left first
    Result leftSearch = kthsmallestRec(a.left, k);

    //We are done, no need to check right.
    if (leftSearch.node != null) 
        return leftSearch;
    

    //Consider number of nodes found to the left
    k = leftSearch.k;

    //Check if current root is the solution before going right
    k--;
    if (k == 0) 
        return new Result(k - 1, a);
    

    //Check right
    Result rightBalanced = kthsmallestRec(a.right, k);

    //Consider all nodes found to the right
    k = rightBalanced.k;

    if (rightBalanced.node != null) 
        return rightBalanced;
    

    //No node found, recursion will continue at the higher level
    return new Result(k, null);



private class Result 
    private final int k;
    private final TreeNode node;

    Result(int max, TreeNode node) 
        this.k = max;
        this.node = node;
    


【讨论】:

【参考方案26】:

Python 解决方案 时间复杂度:O(n) 空间复杂度:O(1)

想法是使用Morris Inorder Traversal

class Solution(object):
def inorderTraversal(self, current , k ):
    while(current is not None):    #This Means we have reached Right Most Node i.e end of LDR traversal

        if(current.left is not None):  #If Left Exists traverse Left First
            pre = current.left   #Goal is to find the node which will be just before the current node i.e predecessor of current node, let's say current is D in LDR goal is to find L here
            while(pre.right is not None and pre.right != current ): #Find predecesor here
                pre = pre.right
            if(pre.right is None):  #In this case predecessor is found , now link this predecessor to current so that there is a path and current is not lost
                pre.right = current
                current = current.left
            else:                   #This means we have traverse all nodes left to current so in LDR traversal of L is done
                k -= 1
                if(k == 0):
                    return current.val
                pre.right = None       #Remove the link tree restored to original here 
                current = current.right
        else:               #In LDR  LD traversal is done move to R 
            k -= 1
            if(k == 0):
                return current.val
            current = current.right

    return 0

def kthSmallest(self, root, k):
    return self.inorderTraversal( root , k  )

【讨论】:

【参考方案27】:
public int kthSmallest(TreeNode root, int k) 
     
    LinkedList<TreeNode> stack = new LinkedList<TreeNode>();

    while (true) 
      while (root != null) 
        stack.push(root);
        root = root.left;
      
      root = stack.pop();
      k = k - 1;
      if (k == 0) return root.val;
      root = root.right;
    

     

【讨论】:

【参考方案28】:

步骤如下:

1.向每个节点添加一个字段,指示其根的树的大小。这支持平均 O(logN) 时间的操作。

2.为了节省空间,一个字段指示其根节点的大小就足够了。我们不需要同时保存左子树和右子树的大小。

3.进行中序遍历直到LeftTree == K, LeftTree = Size(T->Left) + 1

4.示例代码如下:

int Size(SearchTree T)

    if(T == NULL) return 0;
    return T->Size;

Position KthSmallest(SearchTree T, int K)

    if(T == NULL) return NULL;

    int LeftTree;
    LeftTree = Size(T->Left) + 1;

    if(LeftTree == K) return T;

    if(LeftTree > K) 
        T = KthSmallest(T->Left, K); 
    else if(LeftTree < K) 
        T = KthSmallest(T->Right, K - LeftTree);
       

    return T;

5.同理,我们也可以得到KthLargest函数。

【讨论】:

【参考方案29】:

我写了一个简洁的函数来计算第 k 个最小的元素。我使用有序遍历并在它到达第 k 个最小元素时停止。

void btree::kthSmallest(node* temp, int& k)
if( temp!= NULL)   
 kthSmallest(temp->left,k);       
 if(k >0)
 
     if(k==1)
    
      cout<<temp->value<<endl;
      return;
    

    k--;
 

 kthSmallest(temp->right,k);  

【讨论】:

没有提供关于为什么这是最佳的指标。大案小案【参考方案30】:
public static Node kth(Node n, int k)
    Stack<Node> s=new Stack<Node>();
    int countPopped=0;
    while(!s.isEmpty()||n!=null)
      if(n!=null)
        s.push(n);
        n=n.left;
      else
        node=s.pop();
        countPopped++;
        if(countPopped==k)
            return node;
        
        node=node.right;

      
  

【讨论】:

以上是关于以最优方式在二叉搜索树中找到第 k 个最小元素的主要内容,如果未能解决你的问题,请参考以下文章

230. 二叉搜索树中第K小的元素

二叉树中第 K小的元素

230. 二叉搜索树中第K小的元素

230 Kth Smallest Element in a BST 二叉搜索树中第K小的元素

LeetCode——230. 二叉搜索树中第K小的元素(Java)

LeetCode——230. 二叉搜索树中第K小的元素