Showing posts with label binary search tree. Show all posts
Showing posts with label binary search tree. Show all posts

Friday, February 22, 2013

Common Lisp: Բալանսավորված բինար ծառեր

Գրառումներից մեկում ես ներկայացրեցի բինար որոնման ծառի իրականացումը որպես չփոփոխվող տվյալների կառուցվածք։ Այս անգամ էլ բալանսավորված բինար ծառերը ներկայացնելու համար ընտրել եմ այդ եղանակը, բայց որպես ցուցադրման համար ընտրել եմ Common Lisp լեզուն (Scheme լեզվի փոխարեն)։
ՈՒզում եմ հատկապես շեշտել, որ այս գրառման նպատակը բալանսավորված ծառերի հետ կապված ալգորիթմների ցուցադրությունը չէ։ Միակ նպատակը, որ ես դրել եմ իմ առաջ, դա Common Lisp լեզվով հայալեզու նյութերի ստեղծումն ու տարածումն է։ Նաև նկատելի է, որ բոլոր ներկայացված ալգորիթմներն արդյունավետությամբ չեն առանձնանում։
* * *
Սահմանումներ։ Common Lisp լեզվով ներկայացված բինար ծառի հանգույցն ունի երեք անդամ, որոնցից առաջինն ատոմ է և հանդիսանում է հանգույցում գրված արժեքը։ Երկրորդ ու երրորդ տարրերը ցուցակներ են, որոնք ներկայացնում են հանգույցի համապատասխանաբար ձախ և աջ ենթածառերը։ node-p պրեդիկատը ստուգում է, որ տրված ցուցակը ծառի հանգույց է․
(defun node-p (tree)
  (and (= 3 (list-length tree))
       (atom (car tree))
       (listp (cadr tree))
       (listp (caddr tree))))
Ծառի տերևը նույնպես հանգույց է, որի ենթածառերի փոխարեն nil արժեքն է։ leaf-p պրեդիկատը ստուգում է, որ տրված ցուցակը ծառի տերև է․
(defun leaf-p (tree)
  (and (node-p tree)
       (null (cadr tree))
       (null (caddr tree))))
Ծառի բարձրությունը նրա արմատից մինչև տերևներն ընկած ճանապարհներից ամենաերկարի երկարությունն է։ Այդ արժեքը կարելի է հաշվել tree-height ռեկուրսիվ ֆունկցիայով։
(defun tree-height (tree)
  (if tree
      (1+ (max (tree-height (cadr tree))
          (tree-height (caddr tree))))
      0))
Մոտավորապես նույն եղանակով կարելի է հաշվել ծառի բոլոր տարրերի քանակը.
(defun count-nodes (tree)
  (if tree
      (+ 1 (count-nodes (cadr tree))
           (count-nodes (caddr tree)))
      0))
Հանգույցի ձախ (L) ու աջ (R) ենթածառերի բարձրությունների տարբերությունը` h(L)-h(R), կանվանենք տվյալ հանգույցի բանլանսավորվածության գործակից.
(defun balance-factor (tree)
  (- (tree-height (cadr tree))
     (tree-height (caddr tree))))
Այս գրառման համատեքստում կասենք, որ բինար որոնման ծառը բալանսավորված է, երբ նրա յուրաքանչյուր հանգույցի աջ ու ձախ ենթածառերի բարձրությունները տարբերվում են ամենաշատը մեկով՝ բալանսավորվածության գործակիցը -1, 0 կամ 1 է (տես. AVL ծառեր)։ balanced-p պրեդիկատը ռեկուրսիվ եղանակով ստուգում է ամբողջ ծառի բալանսավորվածությունը․
(defun balanced-p (tree)
  (if (leaf-p tree)
      T
      (and (balanced-p (cadr tree)) 
    (balanced-p (caddr tree))
    (member (balance-factor tree) '(-1 0 1)))))
* * *
Գործողություններ։ Շատ հետաքրքիր գործողություն է բալանսավորված ծառում նոր արժեքի ավելացումը։ Դրա համար պետք է նախ որոնել և ավելացնել տրված արժեքը ծառում այնպես, ինչպես դա արվում է սովորական բինար որոնման ծառում։ Հետևյալ add-value ֆունկցիան բնար որոնման ծառում ավելացնում է նոր տերև՝ տրված արժեքով։
(defun add-value (tree val)
  (if tree
      (destructuring-bind (d l r)
          tree
        (cond ((< val d) (list d (add-value l val) r))
       ((> val d) (list d l (add-value r val)))
       (t tree)))
      (list val nil nil)))
Այնուհետև հետ գնալ որոնման ճանապարհով և ամեն մի հանգույցում ստուգել բալանսավորվածությունը։ Եթե այն խախտված է, ապա՝ վերականգնել։ Հայտնի է, որ հանգույցում կարող է հանդիպել բալանսավորվածության խախտման չորս դեպքերից որևէ մեկը (իրականում դեպքերը երկուսն են, իսկ մյուս երկուսը պարզապես սիմետրիկ տարբերակներ են)։ Տվյալների կառուցվածքներին նվիրված համարյա բոլոր գրքերում որտեղ պատմվում է AVL ծառերի մասին, այս չորս դեպքերը մանրամասնորեն նկարագրված են։ Ես պարզապես օրինակներով ցույց կտամ, թե ինչպես է ոչ բալանսավորված ծառը ձևափոխվում բալանսավորվածի։

Ստորև ներկայացված են ծառի A հանգույցում բալանսավորվածության խախտման չորս դեպքերը և այն գործողությունները, որոնց կատարումից հետո ծառը վերածվում է բալանսավորվածի։
G A F B E C D A գագաթը պտտել դեպի աջ։ G A F B E C D
G A F C E B D Պտտել B գագաթը դեպի ձախ, ապա A գագաթը՝ դեպի աջ։ G A F C E B D
G C F B E A D A գագաթը պտտել դեպի ձախ։ G C F B E A D
G B F C E A D Պտտել B գագաթը դեպի աջ, ապա A գագաթը՝ դեպի ձախ։ G B F C E A D
Դեպի աջ ու ձախ պտույտների գործողությունները ծրագրավորված են համապատասխանաբար rotate-right և rotate-left ֆունկցիաներով։
(defun rotate-right (tree)
  (destructuring-bind ((h l r) (lh ll lr)) 
      (list tree (cadr tree))
    (declare (ignore l))
    (list lh ll (list h lr r))))
(defun rotate-left (tree)
  (destructuring-bind ((h l r) (rh rl rr))
      (list tree (caddr tree))
    (declare (ignore r))
    (list rh (list h l rl) rr)))
Այս երկու ֆունկցիաների համադրմամբ պատրաստել եմ ևս երկու օգնական ֆունկցիաներ։ rotate-right-left ֆունկցիան դեպի աջ պտույտ է կատարում տրված ծառի աջ ենթածառում, ապա դեպի ձախ պտույտ է կատարում ծառի արմատում։ Իսկ rotate-left-right ֆունկցիան դեպի ձախ պտույտ է կատարում տրված ծառի ձախ ենթածառում, ապա դեպի աջ պտույտ է կատարում ծառի արմատում։
(defun rotate-right-left (tree)
  (destructuring-bind (h l r)
      tree
    (rotate-left (list h l (rotate-right r)))))
(defun rotate-left-right (tree)
  (destructuring-bind (h l r)
      tree
    (rotate-right (list h (rotate-left l) r))))
Հիմա պետք է ծրագրավորել մի ֆունկցիա, անվանենք այն add-value-to-avl, որը տրված ծառում կավելացնի տրված արժեքը և միաժամանակ կշտկի խախտված բալանսավորվածության դեպքերը։ Ձևափոխեմ քիչ վերևում բերված add-value ֆունկցիան այնպես, որ այն իր ձախ ու աջ ենթածառերում արժեքի ռեկուրսիվ ավելացման ժամանակ օգտագործի add-value-to-avl ֆունկցիան։ Թող այս նոր ֆունկցիան ստանա add-value-to-bst անունը։
(defun add-value-to-bst (tree val)
  (if tree
      (destructuring-bind (d l r)
          tree
        (cond ((< val d)
               (list d (add-value-to-avl l val) r))
              ((> val d)
               (list d l (add-value-to-avl r val)))
              (t tree)))
      (list val nil nil)))
Հիմնական add-value-to-avl ֆունկցիան տրված tree ծառում ավելացնում է տրված val արժեքը, ը նոր ստեղծված ծառը կապում է nw լեքսիկական փոփոխականին։ Այնուհետև հաշվում է nw ծառի բալանսավորվածության գործակիցը՝ bl։ Բալանսավորվածության խախտման չորս դեպքերը ստուգվում են cond կառուցվածքում, որի առաձին չորս ճյուղերի պայմանները ճշտորեն համընկնում են վերը բերված սխեմաների A գագաթում բալանսավորվածության խախտման դեպքերին։
(defun add-value-to-avl (tree val)
  (let* ((nw (add-value-to-bst tree val))
         (bl (balance-factor nw))
         (dl (caadr nw))
         (dr (caaddr nw)))
    (cond ((and (> bl 1) (< val dl))
            (rotate-right nw))
          ((and (> bl 1) (> val dl))
            (rotate-left-right nw))
          ((and (< bl -1) (< val dr))
            (rotate-right-left nw))
          ((and (< bl -1) (> val dr))
            (rotate-left nw))
          (t nw))))
Եվ վերջում մի մակրոս, որը հնարավորություն է տալիս մեկ տողով կառուցել AVL-ծառ՝ տրված արժեքներով։
(defmacro build-avl-tree (&body elems)
  (let ((result (gensym)))
    `(let ((,result '()))
       (dolist (e ',elems)
        (setf ,result (add-value-to-avl ,result e)))
       ,result)))
Առայժմ այսքանը Common Lisp լեզվով բալանսավորված բինար որոնման ծառերի իրականացման մասին։
* * *
Օգտագործված գրականություն։
  1. Guy Steele, Common LISP. The Language. Second Edition.
  2. Robert Sedgewick, Algorithms in Java, Parts 1-4 (3rd Edition).
  3. Robert Sedgewick, Kevin Wayne, Algorithms (4th Edition).
  4. Donald Knukth, Art of Computer Programming, Volume 3: Sorting and Searching (2nd Edition).
  5. Niklaus Wirth, Algorithms and Data Structures.
  6. Alfred Aho, Jeffrey Ullman, John Hopcroft, Data Structures and Algorithms.

Wednesday, January 30, 2013

Scheme: Չփոփոխվող բինար ծառերի մասին

Շարունակելով իմ նախորդ գրառման բինար որոնման ծառերի թեման, ուզում եմ նույն այդ օրինակով ցույց տալ, թե ինչպես կարելի է ծրագրեր գրել օգտագործելով միայն չփոփոխվող (immutable) տվյալների կառուցվածքներ։ Այս անգամ բինար որոնման ծառերի վարքը ծրագրավորել եմ Scheme լեզվով (այն Lisp ընտանիքի թերևս ամենահայտնի ներկայացուցիչն է)։ Ծառը ներկայացված է ցուցակի տեսքով, որի առաջին տարրը արմատի արժեքն է, երկրորդը՝ ձախ ենթածառն է, իսկ երրորդը՝ աջ ենթածառը։ Տերևներն իրենց աջ ու ձախ ենթածառերի փոխարեն պարունակում են #f արժեքը։ Օրինակ, 23 արժեքով տերևը ներկայանում է (23 #f #f) ցուցակով։ Իսկ ստորև բերված նկարում պատկերված ծառը․
        10
       /  \
      6    20
     / \    \
    4   8    28
            /  \
          23    30
            \
             26
ցուցակային ներկայացմամբ կունենա ահա այսպիսի տեսք․
(10 (6 (4 #f #f) (8 #f #f)) (20 #f (28 (23 #f (26 #f #f)) (30 #f #f))))
(Իհարկե կարելի է տերևները ներկայացնել կա՛մ ատոմներով, կա՛մ մեկ տարր պարունակող ցուցակներով։ Բայց ես ընտրել եմ այս ներկայացումը։)

Նախ ծառում արժեք ավելացնելու օրինակով ցույց տամ փոփոխվող (mutable) և չփոփոխվող (immutable) ծրագրավորելու տարբերությունները։ Ենթադրենք C լեզվով որպես բինար որոնման ծառի հանգույց սահմանված է _node ստրուկտուրան, իր _new_node կոնստրուկտորով.
typedef struct _node {
  int data;
  struct _node* left;
  struct _node* right;
} node;
node* _new_node( int value, node* l, node* r )
{
  node* result = (node*)malloc(sizeof(node));
  result->data = value;
  result->left = l;
  result->right = r;
  return result;
}
Այս տիպի հանգույցներով ծառում արժեք ավելացնելու համար սահմանված է _add_value ռեկուրսիվ ֆունկցիան, որն արգումենտում ստանում է ծառի արմատի ցուցիչի ցուցիչը և ավելացվող արժեքը:
void _add_value( node** tree, int value )
{
  if( *tree == NULL )
    *tree = _new_node( value, NULL, NULL );
  if( value < (*tree)->data )
    _add_value( &((*tree)->left), value );
  if( value > (*tree)->data )
    _add_value( &((*tree)->right), value );
}
Այս ֆունկցիայի համար ծառը փոփոխվող օբյեկտ է։ Այն դեպքում, երբ *tree ցուցիչի արժեքը NULL է, ստեղծվում է նոր հանգույց, և այդ նոր հանգույցի ցուցչիը վերագրվում է *tree ցուցիչին։ Քանի որ tree փոփոխականը ֆունկցիայի արգումենտում հայտարարված է struct _node** tree տեսքով, ապա փոփոխությունը կատարվում է հենց ծառի մեջ։

Չփոփոխվող տվյալներով տարբերակում նոր ստեղծված հանգույցն ավելացվում է ոչ թե ծառի մեջ, այլ տրոհվում է ծառը, ավելացվում է հանգույցը պետք եղած տեղում, ապա ստեղծվում է նոր ծառ։
node* _add_value_i( node* tree, int value )
{
  // եթե ծառը դատարկ է, ապա ստեղծել ու վերադարձնել նոր հանգույց
  if( tree == NULL )
    return _new_node( value, NULL, NULL );

  // տրոհել ծառը արմատի արժեքի, ձախ ու աջ ենթածառերի
  int d = tree->data;
  node* l = tree->left;
  node* r = tree->right;
  // ազատել հանգույցի զբաղեցրած հիշողությունը
  free(tree);
  
  // եթե տրված արժեքը փոքր է արմատի արժեքից, ապա 
  // ապա այն ավելացնել ձախ ենթածառում
  if( value < d )
    l = _add_value_i( l, value );
  // եթե տրված արժեքը մեծ է արմատի արժեքից, ապա 
  // ապա այն ավելացնել աջ ենթածառում
  else if( value > d )
    r = _add_value_i( r, value );
  // ստեղծել նոր արմատ ու վերադարձնել նրա ցուցիչը
  return _new_node( d, l, r );
}
(Պարզ է, որ եթե տրված արժեքը հավասար է արմատի արժեքին, ապա հանգույցը քանդելու կարիք չկա, այլ պետք է այն նույնությամբ վերադարձնել։)

Ծառում արժեք ավելացնող add-value պրոցեդուրայի սահմանումը Scheme լեզվով բերված է ստորև։ Այն համարյա նույնությամբ կրկնում է C լեզվով գրված տարբերակը։
(define add-value
  (lambda (tree val)
    (if tree
        (let ([d (car tree)] [l (cadr tree)] [r (caddr tree)])
          (cond
             [(< val d) (list d (add-value l val) r)]
             [(> val d) (list d l (add-value r val))]
             [else (list d l r)]))
        (list val #f #f))))
Քանի որ Scheme լեզվում ներդրված է աղբի հավաքման մեխանիզմը, այս պրոցեդուրայում կարիք չկա C լեզվի free() ֆունկցիային համարժեք գործողություններ կատարելու։
Ֆունկցիաների սահմանման համար Scheme լեզվում նախաատեսված է նաև (define (function-mane arguments) ...) գրելաձևը։ Բայց ինձ ավելի է դուր գալիս lambda արտահայտությամբ սահմանված (define function-name (lambda (arguments) ...) տարբերակը։ Հաջորդ բոլոր ֆունկցիաները սահմանելիս նույնպես օգտագործել եմ այս վերջին գրելաձևը։
Նաև սահմանեմ մի պրոցեդուրա, որը ծառի մեջ ավելացնում է ոչ միայն մեկ տրված արժեքը, այլ արժեքների ցուցակը։ Սա հնարավորություն կտա հեշտ ու ակնառու եղանակով կառուցել մեծ ծառեր։
(define add-to-tree
  (lambda (tree values)
    (if (empty? values)
        tree
        (add-to-tree (add-value tree (car values)) 
                     (cdr values)))))
Օրինակ, վերևի նկարում պատկերված ծառը կարելի է սահմանել (և սահմանածում համոզվելու համար արտածել) հետևյալ արտահայտություններով.
(define *tr* (add-to-tree #f '(10 6 20 4 8 28 23 30 26)))
(displayln *tr*)
Ծառից որևէ տրված արժեքը պարունակող հանգույցը հեռացնելու համար նույնպես կիրառված է նույն մոտեցումը՝ տրոհվում է ծառը, ապա հետ է հավաքվում, բայց արդեն առանց հեռացվող արժեքի։ Այն դեպքում, երբ ծառը դատարկ չէ, արմատը տրոհվում է d, l և r բաղադրիչների։ Երբ որոնվող արժեքը հավասար է d-ին, դիտարկվում են հետևյալ դեպքերը.
  1. Եթե \(r=\varnothing \;\wedge\; l=\varnothing\), ապա վերադարձվում է #f։
  2. Եթե \(r=\varnothing \;\vee\; l=\varnothing\), ապա վերադարձվում է համապատասխանաբար ձախ կամ աջ ենթածառը։
  3. Եթե \(r\ne\varnothing \;\wedge\; l\ne\varnothing\), ապա հեռացվում է աջ ենթածառի ամենափոքր արժեքը պարունակող հանգույցը, իսկ նրա արժեքը գրվում է արմատի արժեքի փոխարեն։
(define remove-value
  (lambda (tree val)
    (if tree
      (let ([d (car tree)] [l (cadr tree)] [r (caddr tree)])
        (cond
          [(< val d)
           (list d (remove-value l val) r)]
          [(> val d)
           (list d l (remove-value r val))]
          [else
           (cond
             [(and (not l) (not r)) #f]
             [(and l (not r)) l]
             [(and (not l) r) r]
             [else
              (let* ([h (minimum-value r)]
                     [t (remove-value r h)])
                (list h l t))])]))
      #f)))
minimum-value պրոցեդուրան, որ օգտագործված է remove-value պրոցեդուրայում, վերադարձնում է իրեն տրված ծառի ամենափոքր արժեքը, կամ, այլ կերպ ասած, ծառի ամենաձախ հանգույցի արժեքը։
(define minimum-value 
  (lambda (tree)
    (let ([l (cadr tree)])
      (if (not l)
          (car tree)
          (minimum-value l)))))

* * *
ՈՒ, պարզապես հետաքրքրության համար սահմանեմ նաև ծառի հետ կատարվող մի քանի գործողություններ։ Առաջինը թող լինի մի պրոցեդուրա, որ պարզում է տրված արժեքի առկայությունը ծառում։
(define contains-value 
  (lambda (tree val)
    (if tree
        (let ([d (car tree)])
          (cond 
            [(= val d) #t]
            [(< val d) (contains-value (cadr tree) val)]
            [(> val d) (contains-value (caddr tree) val)]))
        #f)))
Իսկ inorder-traverse պրոցեդուրան կատարում է ծառի ձախ-արմատ-աջ անցում՝ արմատի արժեքի վրա կիրառելով տրված պրոցեդուրան։
(define inorder-traverse 
  (lambda (tree func)
    (when tree
      (inorder-traverse (cadr tree) func)
      (func (car tree))
      (inorder-traverse (caddr tree) func))))
inorder-modify պրոցեդուրան տրված ծառից կառուցում ու վերադարձնում է մի նոր ծառ, որի հանգույցների արժեքները ձևափոխվել են ըստ տրված գործողության։
(define inorder-modify
  (lambda (tree func)
    (if tree
        (let* ([l (inorder-modify (cadr tree) func)]
               [d (func (car tree))]
               [r (inorder-modify (caddr tree) func)])
          (list d l r))
        #f)))
Այս պրոցեդուրայի աշխատանքի արդյունքը ցուցադրելու համար, օրինակ, տրված ծառից կառուցենք և արտածենք մի նոր ծառ, որի հանգույցների \(x\) արքժեքները փոխարինված են \((x\; \sqrt{x})\) տեսքի զույգերով։
(define *tr* (add-to-tree #f '(10 6 20 4 8 28 23 30 26)))
(displayln *tr*)
(displayln (inorder-modify *tr* (lambda (e) (list e (sqrt e)))))
Ահա նաև արդյունքը.
(10 (6 (4 #f #f) (8 #f #f)) (20 #f (28 (23 #f (26 #f #f)) (30 #f #f))))
((10 3.1622776601683795) ((6 2.449489742783178) ((4 2) #f #f) ((8 2.8284271247461903) #f #f)) ((20 4.47213595499958) #f ((28 5.291502622129181) ((23 4.795831523312719) #f ((26 5.0990195135927845) #f #f)) ((30 5.477225575051661) #f #f))))

Thursday, January 24, 2013

C++11: Բինար որոնման ծառեր

Այս գրառման մեջ ես ներկայացնում եմ բինար որոնման ծառի (binary search tree, BST) դասի ծրագրավորումը C++11 լեզվով։ Բինար որոնման ծառերն առանձնանում են նրանով, ամեն մի հանգույցի պարունակած արժեքը ավելի մեծ է քան նրա ձախ ենթածառի արժեքները և ավելի փոքր է, քան նրա աջ ենթածառի արժեքները։

Քանի որ բինար ծառի ամեն մի հանգույցը կարող է ունենալ առավելագույնը երկու ենթածառ, հանգույցը ներկայացնող շաբլոնային դասը կարելի է ներկայացնել հետևյալ տեսքով.
template<typename T>
class Node {
public:
  T data;
  Node* cleft;
  Node* cright;

public:
  Node( const T& val )
    : data(val), cleft(nullptr), cright(nullptr)
  {}
};
data դաշտը նախատեսված է հանգույցի տվյալների համար, cleft դաշտը ձախ ենթածառի ցուցիչն է, cright դաշտը՝ աջ ենթածառինը։ Հարմարության համար սահմանված է նաև կոնստրուկտոր, որը տրված արժեքը վերագրում է data փոփոխականին, իսկ աջ ու ձախ ենթածառերիո ցուցիչներին վերագրում է զրոյական ցուցիչի nullptr հաստատունը։

Ծառի մոդելը նույնպես ծրագրավորված է շաբլոնային դասի տեսքով։ Որպես ինտերֆեյսային մեթոդներ նախատեսել եմ հետևյալները.
  • Կոնստրուկտորներ, որոնցից մեկը ստեղծում է դատարկ ծառ, իսկ մյուսը ծառն արժեքավորում է արժեքավորող ցուցակով (initializer list) trva] արժեքներով։
  • Add մեթոդը տրված արժեքն ավելացնում է ծառի մեջ՝ ըստ այն պայմանի, որ ամեն մի հանգույցի ձախ ենթածառի բոլոր արժեքները պիտի լինեն ավելի փոքր, իսկ աջ ենթածառինն՝ ավելի մեծ, քան դիտարկվող հանգույցինն է։
  • Contains մեթոդը դրական պատասխան է տալիս, եթե ծառը պարունակում է տրված արժեքը։
  • Remove մեթոդը ծառից հեռացնում է տրված արժեքով հանգույցը՝ պահպանելով բինար ծառի վերը նշված հատկությունը։
  • Preorder, Inorder և Postorder մեթոդները անցնում են ծառի բոլոր հանգույցներով՝ կիրառելով համապատասխանաբար գագաթ-ձախ-աջ, ձախ-գագաթ-աջ և ձախ-աջ-գագաթ եղանակները։ Այս մեթոդներն արգումենտում ստանում են ֆունկցիա, որը կիրառվում է բոլոր հանգույցների data դաշտի նկատմամբ։
Բինար ծառը ներկայացված է իր արմատը ցույց տվող root ցուցիչով։ Դատարկ ծառի դեպքում այս ցուցիչն ունի nullptr արժեքը։ (Node դասը կարող է հայտարարվել նաև Tree դասի ներսում։)
template<typename T>
class Tree {
protected:
  Node<T>* root;
Կոնստրուկտորներից առաջինը արմատի ցուցիչին վերագրում է զրոյական արժեք։
public:
  Tree()
    : root(nullptr)
  {}
Երկրորդ կոնստրուկտորը արգումենտում սպասում է արժեքավորող ցուցակ և այդ ցուցակի տարրերը հերթականությամբ ավելացնում է ծառի մեջ։
  Tree( std::initializer_list<T> els )
    : Tree()
  {
    for( auto e : els )
      Add( e );
  }
Add, Contains, Remove, Preorder, Inorder, Postorder արտաքին (public) մեթոդներն իրականում թաղանթներ (wrapper) են addValue, search, traverse և removeNode ներքին (private) մեթոդների համար։
  // ծառում ավելացնում է val նոր արժեքը
  void Add( const T& val )
  { addValue( val, root ); }

  // ստուգում է val արժեքի առկայությունը ծառում
  bool Contains( const T& val )
  { return nullptr != search( val, root ); }
  
  // ծառից հեռացնում է val արժեքը
  void Remove( const T& val )
  { removeNode( val, root ); }

  // ծառի հանգույցներն անցնում է գագաթ-ձախ-աջ եղանակով և 
  // հանգույցի արժեքի նկատմամբ կիրառում է operation ֆունկցիան
  void Preorder( std::function<void(T)> operation )
  { traverse( root, T_Preorder, operation ); }
  
  // ծառի հանգույցներն անցնում է ձախ-գագաթ-աջ եղանակով և 
  void Inorder( std::function<void(T)> operation )
  { traverse( root, T_Inorder, operation ); }
  
  // ծառի հանգույցներն անցնում է ձախ-աջ-գագաթ եղանակով և 
  void Postorder( std::function<void(T)> operation )
  { traverse( root, T_Postorder, operation ); }
Վերջին երեք մեթոդներում T_Preorder, T_Inorder և T_Postorder իդենտիֆիկատորները սահմանված են հետևյալ կերպ.
protected:
  enum Order { T_Preorder, T_Inorder, T_Postorder };
Հիմա տեսնենք, թե իրականում ինչպես են կատարվում բինար ծառերին յուրահատուկ գործողությունները։ Եվ, քանի որ ծառն ինքնին ռեկուրսիվ կառուցվածք է, բոլոր գործողություններն իրականացված են ռեկուրսիվ տեսքով (չնայած ոչինչ չէր խանգարում դրանք իրականացնել առանց ռեկուրսիայի)։

Սկսեմ արժեքի ավելացման գործողությունից։ addValue մեթոդը ստանում է ավելացվող արժեքը և այն ծառի արմատի հղումը, որի մեջ պետք է ավելացնել նոր հանգույց՝ տրված արժեքով։ Դիտարկվում են երեք դեպքեր. i. եթե տրված արմատը դատարկ է, ապա ստեղծվում է նոր հանգույց և կապվում է այդ արմատին, ii. եթե տրված արժեքը փոքր է տրված արմատի արժեքից, ապա արժեքն ավելացվում է ձախ ենթածառում, iii. եթե տրված արժեքը մեծ է արմատի արժեքից, ապա այն ավելացվում է աջ ենթածառում։ Քանի որ ծառը կրկնվող արժեքներ չի կարող պարունակել, տրված արժեքի ու տրված արմատի արժեքների հավասարության դեպքը պարզապես չի դիտարկվում։
  void addValue( const T& val, Node<T>*& tree )
  {
    if( tree == nullptr )           // i
      tree = new Node<T>(val);
    else if( val < tree->data )     // ii
      addValue( val, tree->cleft );
    else if( val > tree->data )     // iii
      addValue( val, tree->cright );
  }
Հաջորդ պարզ գործողությունը ծառում տրված արժեքը պարունակող հանգույցի որոնումն է։ search մեթոդը ստանում է որոնվող արժեքը և այն արմատը, որից պետք է սկսել որոնումը և վերադարձնում է կա՛մ nullptr, եթե հանգույցի չի գտնվել, կա՛մ գտնված հանգույցի ցուցիչը։ Այս մեթոդի վերնագրի տարօրինակ գրառումը նույնպես C++11 ստանդարտի նորամությություններից է։ decltype(root) արտահայտությունն ասում է, որ մեթոդի վերադարձրած արժեքն ու նրա երկրորդ tree արգումենտը պետք է ունենան նոււյն տիպը, ինչ տիպ տրված է ծառի root արմատին։ Այս մեթոդում առանձնացված է այն դեպքը երբ տրված արմատը զրոյական է. վերդարձվում է nullptr։ i. Եթե տրված արժեքը հավասար է արմատի արժեքին, ապա արմատը ներկայացնող հանգույցը հենց որոնվող հանգույցն է։ ii. եթե val-ը փոքր է արմատի արժեքից, ապա որոնումը շարունակել ձախ ենթածառում, iii. հակառակ դեպքում որոնումը շարունակել աջ ենթածառում։
  decltype(root) search( const T& val, decltype(root) tree )
  {
    if( tree == nullptr )
      return nullptr;
    if( val == tree->data )
      return tree;
    if( val < tree->data )
      return search( val, tree->cleft );
    return search( val, tree->cright );
  }
Բնականաբար decltype(root) արտահայտության օգտագործումն ամենևին էլ պարտադիր չէր այս պարագայում։ Ես այն օգտագործել եմ միայն ցուցադրման նպատակով։ Կարելի է պարզապես բացահայտ գրել Node<T>*, որը հենց հանգույցի ցուցիչի տիպն է։
Համեմատաբար ավելի բարդ է ծառից տրված արժեքը պարունակող հանգույցը հեռացնելու գործողությունը։ Այս գործողությունը պետք է կատարել այնպես, որ բինար որոնման ծառը շարունակի պահպանել իր հատկությունները։ removeNode մեթոդը նույնպես ունի ռեկուրսիվ կառուցվածք, որտեղ նորից առանձնացված է տրված արմատի դատարկ լինելու դեպքը։ (Ավելի ճիշտ ասած, սա որոնող-հեռացնող մեթոդ է։) Այն դեպքում, երբ որոնումը կանգ է առել տրված արժեքը պարունակող հանգույցի վրա, դիտարկվում են քայլերի չորս տարբերակներ.
  • Երբ հանգույցը ենթածառեր չունի՝ տերև է: Այս դեպքում ազատվում է հանգույցի զբաղեցրած հիշողությունը և նրա հասցեն զրոյացվում է։
  • Երբ հանգույցն ունի միայն ձախ ենթածառ։ Այս դեպքում հանգույցը հեռացվում է և նրա հասցեին վերագրվում է ձախ ենթածառի հասցեն։
  • Համանման գործողություններ են կատարվում նաև այն դեպքում, երբ հանգույցն ունի միայն աջ ենթածառ։
  • Երբ առկա են հանգույցի և՛ ձախ, և՛ աջ ենթածառերը։ Այս դեպքում, որպեսզի պահպանվի բինար որոնման ծառի հատկությունները, հեռացվող հանգույցը պետք է փոխարինել կա՛մ ձախ ենթածառի ամենամեծ արժեքով, կա՛մ աջ ենթածառի ամենափոքր արժեքով։
  void removeNode( const T& val, decltype(root)& tree )
  {
    // ծառը դատարկ է
    if( tree == nullptr )
      return;
    // հեռացնել ձախ ենթածառից
    if( val < tree->data )
      removeNode( val, tree->cleft );
    // հեռացնել աջ ենթածառից
    else if( val > tree->data )
      removeNode( val, tree->cright );
    else {
      bool L(tree->cleft != nullptr);
      bool R(tree->cright != nullptr);
      if( !L && !R ) {
        // տերև է
        delete tree;
        tree = nullptr;
      }
      else if( L && !R ) {
        // միայն ձախ ենթածառն է
        auto temp(tree);
        tree = tree->cleft;
        delete temp;
      }
      else if( !L && R ) {
        // միայն աջ ենթածառն է
        auto temp(tree);
        tree = tree->cright;
        delete temp;
      }
      else /* if( L && R ) */ {
        // առկա են ձախ և աջ ենթածառերը
        auto temp(tree->cright);
        // որոնել աջ ենթածառի ամենաձախ հանգույցը
        while( temp->cleft != nullptr )
          temp = temp->cleft;
        // նրա արժեքը արտագրել "հեռացվող" հանգույցում
        tree->data = temp->data;
        // բայց հեռացնել աջ ենթածառի ամենաձախ հանգույցը
        removeNode( temp->data, tree->cright );
      }
    }
  }
Ծառի բոլոր հանգույցներով անցնելու այն երեք ստարատեգիաները, որոնք արտահայտված են Preorder, Inorder և Postorder արտաքին մեթոդներով, օգտագործում են միակ traverse ներքին մեթոդը։ Այստեղ ord արգումենտի արժեքով որոշվում է թե երբ պետք է գործողությունը կիրառել գագաթի արժեքի նկատմամբ։
  void traverse( decltype(root) tree, Order ord, std::function<void(T)> operation )
  {
    // ծառը դատարկ է
    if( tree == nullptr )
      return;
    // գագաթ-ձախ-աջ
    if( T_Preorder == ord )
      operation(tree->data);
    // անցում ձախ ենթածառով
    traverse(tree->cleft, ord, operation);
    // ձախ-գագաթ-աջ
    if( T_Inorder == ord )
      operation(tree->data);
    // անցում աջ ենթածառով
    traverse(tree->cright, ord, operation);
    // ձախ-աջ-գագաթ
    if( T_Postorder == ord )
      operation(tree->data);
  }
};

* * *
Ծառը ներկայացնող դասն արդեն պատրաստ է։ Այժմ կառուցենք ստորև բերված նկարում պատկերված բինար որոնման ծառը.
Տրված արժեքներով ծառ կառուցելու համար օգտագործենք արժեքավորող ցուցակը։
Tree<int> tr { 10, 6, 20, 4, 8, 16, 22, 7, 13, 17, 15 };
Ծառի պարունակությունը preorder, inorder և postorder անցումներով արտածելու համար սահմապենք printer ֆունկցիան և այն փոխանցենք Preorder, Inorder և Postorder արտքին մեթոդներին։
auto printer = [](int e) { printf("%d ", e); };

printf(" Preorder: ");
tr.Preorder( printer );
printf("\n");
  
printf("  Inorder: ");
tr.Inorder( printer );
printf("\n");
  
printf("Postorder: ");
tr.Postorder( printer );
printf("\n");
Կատարումից հետո կստանանք.
 Preorder: 10 6 4 8 7 20 16 13 15 17 22 
  Inorder: 4 6 7 8 10 13 15 16 17 20 22 
Postorder: 4 7 8 6 15 13 17 16 22 20 10 
Եթե հեռացնենք 13 արժեքը և ծառի պարունակությունն արտածենք ձախ-գագաթ-աջ կարգով, այնուհետև հեռանցնենք 10 արժեքը ու նորից արտածենք գագաթ-ձախ-աջ կարգով.
tr.Remove(13);
printf("  Inorder: ");
tr.Inorder( printer );
printf("\n");

printf(" Preorder: ");
tr.Preorder( printer );
printf("\n");
ապա կստանանք.
  Inorder: 4 6 7 8 10 15 16 17 20 22 
 Preorder: 15 6 4 8 7 20 16 17 22 
Այժմ, ենթադրենք պահանջվում է ծառի հանգույցները հավաքել std::vector<int> օբյեկտի մեջ, օրինակ, postorder կարգով։ Սահմանենք vec վեկտորը և մի լյամբդա արտահայտությունը, որը "բռնում" (capture) vec օբյեկտը որպես հղում և նրա մեջ է ավելացնում իր արգումենտում տրված արժեքը։
std::vector<int> vec;
tr.Postorder( [&vec](int e) { vec.push_back(e); });
Իսկ, օրինակ, ծառի արժեքների թվաբանական միջինը կարելի է հաշվել հետևյալ կերպ.
int summ(0), count(0);
tr.Inorder( [&summ,&count](int e) { summ += e; ++count; });
printf( "Sum = %f\n", 1.0 * summ / count );