J'ai codé récemment un tas de différentes implémentations d'arbre de recherche binaire (AVL, splay, treap) et je suis curieux de savoir s'il existe un moyen particulièrement "bon" d'écrire un itérateur pour parcourir ces structures. La solution que j'ai utilisée en ce moment est d'avoir chaque nœud dans les pointeurs du magasin BST vers les éléments suivants et précédents dans l'arborescence, ce qui réduit l'itération à une itération de liste liée standard. Cependant, je ne suis pas vraiment satisfait de cette réponse. Il augmente l'utilisation de l'espace de chaque nœud de deux pointeurs (suivant et précédent), et dans un certain sens, c'est juste de la triche.
Je connais un moyen de construire un itérateur d'arbre de recherche binaire qui utilise O(h) espace de stockage auxiliaire (où h est la hauteur de l'arbre) en utilisant une pile pour garder une trace de la frontière des nœuds à explorer plus tard, mais j'ai résisté au codage en raison de l'utilisation de la mémoire. J'espérais qu'il existe un moyen de construire un itérateur qui utilise uniquement un espace constant.
Ma question est la suivante: existe-t-il un moyen de concevoir un itérateur sur un arbre de recherche binaire avec les propriétés suivantes?
next()
et hasNext()
s'exécutent en O(1) heure).Pour le rendre plus facile, c'est bien si vous supposez que la structure de l'arbre ne change pas de forme pendant l'itération (c'est-à-dire sans insertions, suppressions ou rotations), mais ce serait vraiment cool s'il y avait une solution qui pourrait effectivement gérer cela.
L'itérateur le plus simple possible stocke la dernière clé vue, puis à l'itération suivante, recherche dans l'arborescence la borne supérieure la moins haute pour cette clé. L'itération est O (log n). Cela a l'avantage d'être très simple. Si les clés sont petites, les itérateurs sont également petits. bien sûr, il a l'inconvénient d'être une manière relativement lente d'itérer à travers l'arbre. Cela ne fonctionnera pas non plus pour les séquences non uniques.
Certaines arborescences utilisent exactement l'implémentation que vous utilisez déjà, car il est important pour leur utilisation spécifique que l'analyse soit très rapide. Si le nombre de clés dans chaque nœud est important, la pénalité de stockage des pointeurs frères n'est pas trop lourde. La plupart des arbres B utilisent cette méthode.
de nombreuses implémentations d'arborescence de recherche conservent un pointeur parent sur chaque nœud pour simplifier les autres opérations. Si vous avez cela, vous pouvez utiliser un simple pointeur vers le dernier nœud vu comme état de votre itérateur. à chaque itération, vous recherchez l'enfant suivant dans le dernier parent du nœud vu. s'il n'y a plus de frères et sœurs, alors vous montez d'un niveau de plus.
Si aucune de ces techniques ne vous convient, vous pouvez utiliser une pile de nœuds, stockée dans l'itérateur. Cela sert la même fonction que la pile d'appels de fonction lors de l'itération normale dans l'arborescence de recherche, mais au lieu de parcourir les frères et sœurs et de revenir sur les enfants, vous poussez les enfants sur la pile et renvoyez chaque frère successif.
Comme TokenMacGuy l'a mentionné, vous pouvez utiliser une pile stockée dans l'itérateur. Voici une implémentation rapide testée de cela en Java:
/**
* An iterator that iterates through a tree using in-order tree traversal
* allowing a sorted sequence.
*
*/
public class Iterator {
private Stack<Node> stack = new Stack<>();
private Node current;
private Iterator(Node argRoot) {
current = argRoot;
}
public Node next() {
while (current != null) {
stack.Push(current);
current = current.left;
}
current = stack.pop();
Node node = current;
current = current.right;
return node;
}
public boolean hasNext() {
return (!stack.isEmpty() || current != null);
}
public static Iterator iterator(Node root) {
return new Iterator(root);
}
}
Une autre variante consisterait à parcourir l'arbre au moment de la construction et à enregistrer le parcours dans une liste. Vous pouvez ensuite utiliser l'itérateur de liste.
Ok, je sais que c'est vieux, mais on m'a demandé cela dans un entretien avec Microsoft il y a quelque temps et j'ai décidé de travailler un peu dessus. J'ai testé cela et cela fonctionne très bien.
template <typename E>
class BSTIterator
{
BSTNode<E> * m_curNode;
std::stack<BSTNode<E>*> m_recurseIter;
public:
BSTIterator( BSTNode<E> * binTree )
{
BSTNode<E>* root = binTree;
while(root != NULL)
{
m_recurseIter.Push(root);
root = root->GetLeft();
}
if(m_recurseIter.size() > 0)
{
m_curNode = m_recurseIter.top();
m_recurseIter.pop();
}
else
m_curNode = NULL;
}
BSTNode<E> & operator*() { return *m_curNode; }
bool operator==(const BSTIterator<E>& other)
{
return m_curNode == other.m_curNode;
}
bool operator!=(const BSTIterator<E>& other)
{
return !(*this == other);
}
BSTIterator<E> & operator++()
{
if(m_curNode->GetRight())
{
m_recurseIter.Push(m_curNode->GetRight());
if(m_curNode->GetRight()->GetLeft())
m_recurseIter.Push(m_curNode->GetRight()->GetLeft());
}
if( m_recurseIter.size() == 0)
{
m_curNode = NULL;
return *this;
}
m_curNode = m_recurseIter.top();
m_recurseIter.pop();
return *this;
}
BSTIterator<E> operator++ ( int )
{
BSTIterator<E> cpy = *this;
if(m_curNode->GetRight())
{
m_recurseIter.Push(m_curNode->GetRight());
if(m_curNode->GetRight()->GetLeft())
m_recurseIter.Push(m_curNode->GetRight()->GetLeft());
}
if( m_recurseIter.size() == 0)
{
m_curNode = NULL;
return *this;
}
m_curNode = m_recurseIter.top();
m_recurseIter.pop();
return cpy;
}
};
Traversée de l'arbre , de Wikipedia:
Tous les exemples d'implémentations nécessitent un espace de pile d'appels proportionnel à la hauteur de l'arborescence. Dans un arbre mal équilibré, cela peut être assez considérable.
Nous pouvons supprimer l'exigence de pile en conservant des pointeurs parents dans chaque nœud, ou en enfilant l'arborescence. Dans le cas de l'utilisation de threads, cela permettra une traversée en ordre considérablement améliorée, bien que la récupération du nœud parent requis pour la traversée en précommande et en post-commande soit plus lente qu'un simple algorithme basé sur la pile.
Dans l'article, il existe un pseudocode pour l'itération avec l'état O(1), qui peut être facilement adapté à un itérateur.
Qu'en est-il de l'utilisation d'une première technique de recherche approfondie? L'objet itérateur doit simplement avoir une pile des nœuds déjà visités.
Utilisez O(1) espace, ce qui signifie que nous n'utiliserons pas la pile O(h)).
Pour commencer:
hasNext ()? current.val <= endNode.val pour vérifier si l'arborescence est entièrement traversée.
Trouver min via l'extrême gauche: nous pouvons toujours rechercher l'extrême gauche pour trouver la prochaine valeur minimale.
Une fois le min le plus à gauche vérifié (nommez-le current
). La minute suivante sera de 2 cas: si current.right! = Null, nous pouvons continuer à rechercher l'enfant le plus à gauche de current.right, comme la minute suivante. Ou, nous devons regarder en arrière pour le parent. Utilisez l'arborescence de recherche binaire pour trouver le nœud parent du courant.
Remarque: lorsque vous effectuez une recherche binaire pour parent, assurez-vous qu'il satisfait parent.left = current.
Parce que: si parent.right == current, ce parent doit avoir été visité auparavant. Dans l'arbre de recherche binaire, nous savons que parent.val <parent.right.val. Nous devons ignorer ce cas spécial, car il conduit à une boucle ifinite.
public class BSTIterator {
public TreeNode root;
public TreeNode current;
public TreeNode endNode;
//@param root: The root of binary tree.
public BSTIterator(TreeNode root) {
if (root == null) {
return;
}
this.root = root;
this.current = root;
this.endNode = root;
while (endNode != null && endNode.right != null) {
endNode = endNode.right;
}
while (current != null && current.left != null) {
current = current.left;
}
}
//@return: True if there has next node, or false
public boolean hasNext() {
return current != null && current.val <= endNode.val;
}
//@return: return next node
public TreeNode next() {
TreeNode rst = current;
//current node has right child
if (current.right != null) {
current = current.right;
while (current.left != null) {
current = current.left;
}
} else {//Current node does not have right child.
current = findParent();
}
return rst;
}
//Find current's parent, where parent.left == current.
public TreeNode findParent(){
TreeNode node = root;
TreeNode parent = null;
int val = current.val;
if (val == endNode.val) {
return null;
}
while (node != null) {
if (val < node.val) {
parent = node;
node = node.left;
} else if (val > node.val) {
node = node.right;
} else {//node.val == current.val
break;
}
}
return parent;
}
}
Par définition, il n'est pas possible pour next () et hasNext () de s'exécuter dans O(1) time. Lorsque vous regardez un nœud particulier dans un BST, vous n'avez aucune idée de la la hauteur et la structure des autres nœuds sont, donc vous ne pouvez pas simplement "sauter" au bon nœud suivant.
Cependant, la complexité de l'espace peut être réduite à O(1) (à l'exception de la mémoire pour le BST lui-même). Voici la façon dont je le ferais en C:
struct node{
int value;
struct node *left, *right, *parent;
int visited;
};
struct node* iter_next(struct node* node){
struct node* rightResult = NULL;
if(node==NULL)
return NULL;
while(node->left && !(node->left->visited))
node = node->left;
if(!(node->visited))
return node;
//move right
rightResult = iter_next(node->right);
if(rightResult)
return rightResult;
while(node && node->visited)
node = node->parent;
return node;
}
L'astuce consiste à avoir à la fois un lien parent et un indicateur visité pour chaque nœud. À mon avis, nous pouvons affirmer qu'il ne s'agit pas d'une utilisation d'espace supplémentaire, mais simplement d'une partie de la structure du nœud. Et évidemment, iter_next () doit être appelé sans que l'état de l'arborescence change (bien sûr), mais aussi que les drapeaux "visités" ne changent pas de valeurs.
Voici la fonction de testeur qui appelle iter_next () et imprime la valeur à chaque fois pour cet arbre:
27
/ \
20 62
/ \ / \
15 25 40 71
\ /
16 21
int main(){
//right root subtree
struct node node40 = {40, NULL, NULL, NULL, 0};
struct node node71 = {71, NULL, NULL, NULL, 0};
struct node node62 = {62, &node40, &node71, NULL, 0};
//left root subtree
struct node node16 = {16, NULL, NULL, NULL, 0};
struct node node21 = {21, NULL, NULL, NULL, 0};
struct node node15 = {15, NULL, &node16, NULL, 0};
struct node node25 = {25, &node21, NULL, NULL, 0};
struct node node20 = {20, &node15, &node25, NULL, 0};
//root
struct node node27 = {27, &node20, &node62, NULL, 0};
//set parents
node16.parent = &node15;
node21.parent = &node25;
node15.parent = &node20;
node25.parent = &node20;
node20.parent = &node27;
node40.parent = &node62;
node71.parent = &node62;
node62.parent = &node27;
struct node *iter_node = &node27;
while((iter_node = iter_next(iter_node)) != NULL){
printf("%d ", iter_node->value);
iter_node->visited = 1;
}
printf("\n");
return 1;
}
Qui affichera les valeurs dans l'ordre trié:
15 16 20 21 25 27 40 62 71
Si vous utilisez la pile, vous n'obtenez que "Utilisation de mémoire supplémentaire O (h), h est la hauteur de l'arborescence". Cependant, si vous souhaitez utiliser uniquement O(1) mémoire supplémentaire, vous devez enregistrer l'analyse ci-dessous: - Si le nœud actuel a le bon enfant: trouvez min de la sous-arborescence droite - Il le nœud actuel n'a pas de bon enfant, vous devez le rechercher à partir de la racine et continuer à mettre à jour son ancêtre le plus bas, qui est son nœud suivant le plus bas
public class Solution {
//@param root: The root of binary tree.
TreeNode current;
TreeNode root;
TreeNode rightMost;
public Solution(TreeNode root) {
if(root==null) return;
this.root = root;
current = findMin(root);
rightMost = findMax(root);
}
//@return: True if there has next node, or false
public boolean hasNext() {
if(current!=null && rightMost!=null && current.val<=rightMost.val) return true;
else return false;
}
//O(1) memory.
public TreeNode next() {
//1. if current has right child: find min of right sub tree
TreeNode tep = current;
current = updateNext();
return tep;
}
public TreeNode updateNext(){
if(!hasNext()) return null;
if(current.right!=null) return findMin(current.right);
//2. current has no right child
//if cur < root , go left; otherwise, go right
int curVal = current.val;
TreeNode post = null;
TreeNode tepRoot = root;
while(tepRoot!=null){
if(curVal<tepRoot.val){
post = tepRoot;
tepRoot = tepRoot.left;
}else if(curVal>tepRoot.val){
tepRoot = tepRoot.right;
}else {
current = post;
break;
}
}
return post;
}
public TreeNode findMin(TreeNode node){
while(node.left!=null){
node = node.left;
}
return node;
}
public TreeNode findMax(TreeNode node){
while(node.right!=null){
node = node.right;
}
return node;
}
}