Оптимально знайдіть найменший елемент kth у двійковому дереві пошуку


112

Мені потрібно знайти найменший елемент kth у дереві двійкового пошуку, не використовуючи статичну / глобальну змінну. Як цього ефективно досягти? Моє рішення, яке я маю на увазі, - це операція в O (n), найгірший випадок, оскільки я планую здійснити внутрішній обхід всього дерева. Але в глибині душі я відчуваю, що тут не використовую властивість BST. Чи правильне моє рішення, чи є кращий?


7
Чи збалансоване дерево?
kennytm

Це не. Але якби це було збалансовано, чи існує оптимальний спосіб?
брагбой

1
Якщо ви здійснили пошук за "Статистика замовлень", ви знайдете те, що вам потрібно.
RAL

Я вважаю, що більшість відповідей нижче, а правильні - це обман у тому, що вони використовують якусь глобальну змінну (будь то посилання на ціле число чи змінна, яка зменшується і повертається). Якщо абсолютно нічого з цього не дозволено, я б використовував рекурсію без передачі посилань.
Henley Chiu

Відповіді:


170

Ось лише окреслення ідеї:

У BST ліве піддерево вузла Tмістить лише елементи, менші за значення, що зберігаються в T. Якщо kменша кількість елементів у лівому піддереві, то kнайменший елемент повинен належати лівому піддереву. В іншому випадку, якщо kбільший розмір, то kнайменший елемент знаходиться у правій піддерев’ї.

Ми можемо збільшити BST, щоб кожен вузол в ньому зберігав кількість елементів у його лівому піддереві (припустимо, що ліве піддерево даного вузла включає цей вузол). За допомогою цієї інформації можна легко перетинати дерево, багаторазово запитуючи кількість елементів у лівому піддереві, щоб вирішити, чи потрібно повторюватись у ліве або праве піддерево.

Тепер, припустимо, ми знаходимося у вузлі T:

  1. Якщо k == num_elements (ліве піддерево T) , то відповідь, яку ми шукаємо, - це значення у вузлі T.
  2. Якщо k> num_elements (ліве піддерево T) , то, очевидно, ми можемо ігнорувати ліве піддерево, оскільки ці елементи також будуть меншими, ніж kнайменше. Отже, ми зводимо проблему до пошуку k - num_elements(left subtree of T)найменшого елемента правого піддерева.
  3. Якщо k <num_elements (ліве піддерево T) , то kнайменше найменше знаходиться десь у лівому піддереві, тому ми зменшуємо задачу на пошук kнайменшого елемента в лівому піддереві.

Аналіз складності:

Для цього потрібен O(depth of node)час, який є O(log n)в гіршому випадку на збалансованому BST або O(log n)в середньому для випадкового BST.

BST вимагає O(n)зберігання, а O(n)для зберігання інформації про кількість елементів потрібно інший . На всі операції BST потрібен O(depth of node)час, і потрібен O(depth of node)додатковий час для збереження інформації про "кількість елементів" для вставки, видалення або обертання вузлів. Отже, зберігання інформації про кількість елементів у лівому піддереві зберігає складність простору та часу BST.


59
Щоб знайти N-й найменший предмет, потрібно лише зберегти розмір лівого під-дерева. Ви б використовували розмір правого піддерева, якщо ви також хотіли б знайти N-й найбільший елемент. Насправді, ви можете зробити це менш дорогим: збережіть загальний розмір дерева в корені та розмір лівого під-дерева. Коли вам потрібно розмір правого піддерева, ви можете відняти розмір лівого від загального розміру.
Джеррі Труну

37
Таке доповнене BST називається «деревом статистики порядку».
Даніель

10
@Ivlad: на кроці 2: Я думаю, що "k - num_elements" має бути "k - num_elements -1", оскільки ви також повинні включати кореневий елемент.
розум

1
@understack - ні, якщо ви вважаєте, що корінь є частиною піддерева.
IVlad

16
Якщо дерево не містить поля, що містить "кількість елементів у його лівому та правому піддереві", тоді метод в кінцевому підсумку буде BigO (n), оскільки вам потрібно буде пройти правою чи лівою піддеревою на кожному вузлі, щоб обчислити k індекс поточного вузла.
Роберт С. Барнс

68

Більш простим рішенням було б зробити обхідний простір та відстежувати елемент, який зараз друкується (без його друку). Коли ми досягнемо 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
+1: ідея в правильному напрямку, але, можливо, потрібно буде затягнути деякі вільні кінці; дивіться stackoverflow.com/a/23069077/278326
Arun

1
Мені подобається це рішення, оскільки BST вже замовлений, обходу має бути достатньо.
Мерлін

3
Якщо n наближається до загальної кількості вузлів у цьому дереві, ваш алгоритм потребує часу для завершення роботи O (n), що погано для обраної відповіді-O (log n)
Spark8006

13
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


11

Більш простим рішенням було б зробити обхідний прохід і відстежувати елемент, який в даний час друкується лічильником k. Коли ми досягнемо k, надрукуємо елемент. Час виконання - O (n). Пам'ятайте, тип повернення функції не може бути недійсним, він повинен повертати оновлене значення 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-го найменшого елемента.
Vineeth Chitteti

10

// додати версію 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();
        }
    }
}

Мені подобається це рішення та відповідне рекурсивне. Чесно кажучи, більшість відповідей на це питання занадто заплутані / складні для читання.
Henley Chiu

Я люблю це рішення! Ясно і чудово!
Ругал

Це рішення - обхід дерева "в порядку" і зменшення лічильника після відвідування вузла, щоб згодом зупинитися, коли лічильник отримає рівний нулю. Найгірший випадок - це порядок O (n). Не найоптимальніше порівняння з рекурсивними рішеннями @ IVlad, найгірший випадок яких займає O (log n)
Хорхе П.


4

Враховуючи просто звичайне двійкове дерево пошуку, все, що ви можете зробити, - це починати з найменшого та просуватися вгору, щоб знайти потрібний вузол.

Якщо ви збираєтеся робити це дуже часто, ви можете додати атрибут до кожного вузла, який вказує, скільки вузлів знаходиться в його лівому під-дереві. Використовуючи це, ви можете спустити дерево безпосередньо до правильного вузла.


4

Рекурсивна на замовлення Прогулянка з лічильником

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):

  1. if( counter == k )тест необхідний у трьох місцях: (а) після лівого піддерева, (б) після кореня та (в) після правого піддірева. Це потрібно для того, щоб kth елемент був виявлений для всіх локацій , тобто незалежно від піддерева, в якому він знаходиться.

  2. if( result == -1 )тест, необхідний для забезпечення друку лише результату , інакше друкуються всі елементи, починаючи з найменшого до кореня.


Часова складність цього рішення полягає в тому O(k + d), де dзнаходиться максимальна глибина дерева. Тому він використовує глобальну змінну, counterале це питання незаконно.
Валентин Шергін

Привіт Арун, поясни, будь ласка, прикладом. Я не розумію цього особливо вашої першої точки.
Andy897

3

Для не збалансованого дерева пошуку потрібно O (n) .

Для збалансованого дерева пошуку потрібно O (k + log n) в гіршому випадку, але просто O (k) в амортизованому розумінні.

Маючи та керуючи додатковим цілим числом для кожного вузла: розмір під-дерева надає часовій складності O (log n) . Таке збалансоване дерево пошуку зазвичай називають RankTree.

Взагалі, є рішення (засновані не на дереві).

З повагою


1

Це добре працює: status: це масив, який містить, чи знайдений елемент. k: елемент kth, який слід знайти. 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;
}

1

Хоча це, безумовно, не оптимальне рішення проблеми, але це інше потенційне рішення, яке, на мою думку, деяким людям може бути цікавим:

/**
 * 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 );
}

1

підпис:

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;
}

1

Ну ось мої 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;
 }

0

Це те, що я хоч і працює. Він буде працювати в 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

3
Я не думаю, що це рішення спрацює. Що робити, якщо найменший Kth знаходиться у правій під дереві вузла дерева?
Аніл Вишний

0

Добре, ми можемо просто скористатись тим, щоб перейти і натиснути відвідуваний елемент на стек. поп k кількість разів, щоб отримати відповідь.

ми також можемо зупинитися після k елементів


1
це не оптимальне рішення
брагбой

0

Рішення для повної 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;
}

0

Ядро Linux має чудову розширену структуру даних червоно-чорного дерева, яка підтримує операції на основі рангових операцій в O (log n) в linux / lib / rbtree.c.

Дуже неочищений порт Java також можна знайти на веб- сторінці http://code.google.com/p/refolding/source/browse/trunk/core/src/main/java/it/unibo/refolding/alg/RbTree.java , разом з RbRoot.java та RbNode.java. N-й елемент можна отримати, викликавши RbNode.nth (вузол RbNode, int n), передаючи корінь дерева.


0

Ось стисла версія в 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 (журнал N) , щоб знайти в найменший вузол, а потім виведення (к) , щоб пройти до к-го вузла, так що це O (до + увійти п).


як щодо версії java?
Henley Chiu


0

Не вдалося знайти кращого алгоритму .. тому вирішив написати його :) Виправте мене, якщо це неправильно.

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));
}

}


0

Ось код Java,

max (Корінь вузла, int k) - щоб знайти kth найбільшим

min (Корінь вузла, int k) - знайти kth Найменший

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);
}

0

це теж працювало б. просто викличте функцію з maxNode в дереві

def k_largest (self, вузол, k): якщо k <0: повернути None,
якщо k == 0: повернути вузол, інше: k - = 1 повернути self.k_largest (self.predecessor (вузол), k)


0

Я думаю, що це краще, ніж прийнята відповідь, оскільки не потрібно змінювати оригінальний вузол дерева, щоб зберігати кількість його дочірніх вузлів.

Нам просто потрібно скористатися переходом порядку, щоб підрахувати найменший вузол зліва направо, перестати шукати, коли кількість дорівнює К.

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);
}

0

Найкращий підхід вже є. Але я хотів би додати простий код для цього

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 );
}}

0

Використання допоміжного класу 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;
    }
}
}

0

Час складності рішення Python: O (n) Складність простору: O (1)

Ідея полягає у використанні Inverz Traversal Morris

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  )

-1

Я написав акуратну функцію для обчислення найменшого елемента kth. Я використовую порядок обходу і зупиняється, коли він досягає найменшого елемента 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);  }}

Немає метрик щодо того, чому це оптимально. І у великих, і в малих випадках
Woot4Moo

-1
int RecPrintKSmallest(Node_ptr head,int k){
  if(head!=NULL){
    k=RecPrintKSmallest(head->left,k);
    if(k>0){
      printf("%c ",head->Node_key.key);
      k--;
    }
    k=RecPrintKSmallest(head->right,k);
  }
  return k;
}

2
Будь ласка, завжди додайте код із деяким рівнем опису щодо того, що він робить, і як це допомагає вирішити проблему.
Рен

-1
public TreeNode findKthElement(TreeNode root, int k){
    if((k==numberElement(root.left)+1)){
        return root;
    }
    else if(k>numberElement(root.left)+1){
        findKthElement(root.right,k-numberElement(root.left)-1);
    }
    else{
        findKthElement(root.left, k);
    }
}

public int numberElement(TreeNode node){
    if(node==null){
        return 0;
    }
    else{
        return numberElement(node.left) + numberElement(node.right) + 1;
    }
}

-1
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;

      }
  }

}

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.