JavaScript を使用して二分探索木を実装する方法

JavaScript を使用して二分探索木を実装する方法

コンピュータ サイエンスで最も一般的に使用され、議論されているデータ構造の 1 つは、二分探索木です。これは、非線形挿入アルゴリズムで導入される最初のデータ構造であることが多いです。バイナリ検索ツリーは二重リンクリストに似ており、各ノードにはいくつかのデータと他のノードへの 2 つのポインタが含まれていますが、これらのノードが相互に関係する方法が異なります。バイナリ検索ツリー ノードのポインターは通常、現在の値に関連付けられたサブツリーを示すために「左」と「右」と呼ばれます。このノードの簡単な JavaScript 実装は次のとおりです。

var ノード = {
  値: 125,
  左: null、
  右: null
};

名前が示すように、バイナリ検索ツリーは階層的なツリー構造に編成されます。最初の項目がルート ノードになり、追加の各値はそのルートの祖先としてツリーに追加されます。ただし、バイナリ検索ツリーのノードの値は一意であり、含まれる値に従って順序付けられます。ノードの左側のサブツリーの値は常にノードの値よりも小さく、右側のサブツリーの値は常にノードの値よりも大きくなります。この方法により、バイナリ検索ツリーで値を見つけることは非常に簡単になります。探している値が処理中のノードより小さい場合は、左に進みます。値が大きい場合は、右に進みます。重複があると関係が壊れてしまうため、バイナリ検索ツリーには重複があってはなりません。次の図は単純な二分探索木を表しています。

上の図は、ルート値が 8 である二分探索木を表しています。値 3 が追加されると、3 は 8 より小さいため、ルートの左の子になります。値 1 が追加されると、1 は 8 より小さい (つまり左) ため、1 は 3 より小さい (再び左) ため、3 の左の子になります。値 10 が追加されると、10 は 8 より大きいため、ルートの右の子になります。このプロセスは、値 6、4、7、14、および 13 で継続されます。この二分探索木の深さは 3 です。つまり、ルートから最も遠いノードは 3 つのノードです。

バイナリ検索ツリーは自然にソートされた順序で表示されるため、各ステップですぐに可能性を排除できるため、データをすばやく見つけるのに使用できます。検索する必要があるノードの数を制限することで、検索をより高速に実行できます。上記のツリーで値 6 を見つけたいとします。ルートから始めて、6 は 8 より小さいと判断し、ルートの左の子に進みます。 6 は 3 より大きいので、正しいノードに移動します。正しい値が見つかります。したがって、値を見つけるには、9 つ​​のノードではなく 3 つのノードにアクセスするだけで済みます。

JavaScript で二分探索木を実装するには、まず基本インターフェースを定義します。

関数 BinarySearchTree() {
  this._root = null;
}

バイナリサーチツリー.プロトタイプ = {

  //コンストラクタを復元する
  コンストラクタ: BinarySearchTree、

  追加: 関数 (値) {
  },

  含む: 関数(値){
  },

  削除: 関数(値){
  },

  サイズ: 関数(){
  },

  toArray: 関数(){
  },

  toString: 関数(){
  }

};

基本的な接続は他のデータ構造と似ており、値を追加および削除するメソッドがあります。また、JavaScript に役立つ便利なメソッド、size()、toArray()、toString() も追加しました。

バイナリ検索ツリーの使用を開始するには、contains() メソッドから始めるのがよいでしょう。 contains() メソッドは値を引数として受け入れ、その値がツリー内に存在する場合は true を返し、存在しない場合は false を返します。このメソッドは、基本的なバイナリ検索アルゴリズムに従って、値が存在するかどうかを判断します。

バイナリサーチツリー.プロトタイプ = {

  //さらにコード

  含む: 関数(値){
    var found = false、
      現在の = this._root

    //検索するノードがあることを確認する
    while(!found && current){

      //値が現在のノードより小さい場合は左へ進む
      (値<現在の値)の場合{
        現在の = 現在の.left;

      //値が現在のノードより大きい場合は右に進みます
      } それ以外の場合 (値 > 現在の値){
        現在の = 現在の.right;

      //値が等しい、見つかりました!
      } それ以外 {
        見つかりました = true;
      }
    }

    //ノードが見つかった場合のみ続行します
    戻り値が見つかりました。
  },

  //さらにコード

};

検索はツリーのルートから始まります。データが追加されていない場合は、おそらくルート権限がないため、確認する必要があります。ツリーのトラバースは、前述した単純なアルゴリズムに従います。つまり、探している値が現在のノードより小さい場合は左に移動し、大きい場合は右に移動します。値が見つかるまで (この場合 found は true に設定されます)、またはその方向にノードがなくなるまで (この場合値はツリー内にありません)、現在のポインターは毎回上書きされます。

contains() で使用されるメソッドは、ツリーに新しい値を挿入するためにも使用できます。主な違いは、ツリー内の値を検索するのではなく、新しい値を配置する場所を検索することです。

バイナリサーチツリー.プロトタイプ = {

  //さらにコード

  追加: 関数(値){
    //新しいアイテムオブジェクトを作成し、データを配置します
    var ノード = {
        値: 値、
        左: null、
        右: null
      },

      //構造を走査するために使用される
      現在;

    //特別なケース: ツリーにはまだアイテムがありません
    if (this._root === null){
      this._root = ノード;
    } それ以外 {
      現在の = this._root;

      while(true){

        //新しい値がこのノードの値より小さい場合は左に進みます
        (値<現在の値)の場合{

          // 残っていない場合は、新しいノードはそこに属します
          (current.left === null)の場合{
            現在の左ノード;
            壊す;
          } それ以外 {
            現在の = 現在の.left;
          }

        //新しい値がこのノードの値より大きい場合は右に進みます
        } それ以外の場合 (値 > 現在の値){

          // 権利がない場合、新しいノードはそこに属します
          (current.right === null)の場合{
            現在のノードの現在の位置。
            壊す;
          } それ以外 {
            現在の = 現在の.right;
          }   

        //新しい値が現在の値と等しい場合は無視します
        } それ以外 {
          壊す;
        }
      }
    }
  },

  //さらにコード

};

バイナリ検索ツリーに値を追加する場合、ルートが存在しないという特殊なケースがあります。この場合、ルートを新しい値に設定するだけで簡単に作業が完了します。その他のケースでは、基本的なアルゴリズムは contains() で使用されるものとまったく同じです。新しいノードは、現在の値より小さい場合は左に移動し、大きい場合は右に移動します。主な違いは、それ以上進むことができない場合、ここに新しい値が入るという点です。したがって、左に移動する必要があるが左ノードがない場合、新しい値は左ノードになります (右ノードと同じ)。重複がないため、同じ値を持つノードが見つかった場合は操作が停止します。

size() メソッドに進む前に、ツリーのトラバーサルについて詳しく説明したいと思います。二分探索木のサイズを計算するには、木内のすべてのノードを訪問する必要があります。二分探索木には通常、さまざまな種類のトラバーサル方法がありますが、最も一般的なのは順序付きトラバーサルです。左のサブツリー、ノード自体、右のサブツリーの順に処理して、各ノードで順序どおりにトラバーサルを実行します。二分探索木はこのように左から右にソートされるため、結果としてノードは正しいソート順序で処理されます。ノードのトラバーサルの順序は、size() メソッドにとっては実際には重要ではありませんが、toArray() メソッドにとっては重要です。どちらのメソッドもトラバーサルを実行する必要があるため、普遍的に使用できる traverse() メソッドを追加することにしました。

バイナリサーチツリー.プロトタイプ = {

  //さらにコード

  トラバース: 関数(プロセス){

    //ヘルパー関数
    関数 inOrder(ノード){
      if (ノード){

        //左のサブツリーを走査する
        (node.left !== null)の場合{
          ノードの左端に順序を入れます。
        }      

        //このノードのプロセスメソッドを呼び出す
        プロセスを呼び出す(これ、ノード);

        //右のサブツリーをトラバースする
        (node.right !== null)の場合{
          ノードの右端に順序を入れます。
        }
      }
    }

    //ルートから開始
    inOrder(this._root);
  },

  //さらにコード

};

このメソッドは、ツリー内の各ノードで実行される関数である 1 つの引数 process を受け入れます。このメソッドは、ツリーを再帰的に走査するための inOrder() と呼ばれるヘルパー関数を定義します。現在のノードが存在する場合にのみ再帰が左右に移動することに注意してください (null を複数回処理することを避けるため)。次に、traverse() メソッドはルート ノードから順にトラバースし、process() 関数は各ノードを処理します。このメソッドを使用して、size()、toArray()、toString() を実装できます。

バイナリサーチツリー.プロトタイプ = {

  //さらにコード

  サイズ: 関数(){
    var 長さ = 0;

    this.traverse(function(node){
      長さ++;
    });

    長さを返します。
  },

  toArray: 関数(){
    var 結果 = [];

    this.traverse(function(node){
      結果をプッシュします(ノードの値)。
    });

    結果を返します。
  },

  toString: 関数(){
    this.toArray().toString() を返します。
  },

  //さらにコード

};

size() と toArray() はどちらも traverse() メソッドを呼び出し、各ノードで実行する関数を渡します。 size() の場合、関数は単にサイズ変数を増分しますが、toArray() は関数を使用してノードの値を配列に追加します。 toString() メソッドは、toArray() を呼び出す前に返された配列を文字列に変換して返します。

ノードを削除するときは、それがルート ノードであるかどうかを判断する必要があります。ルート ノードは他のノードと同様に扱われますが、最後にルート ノードを別の値に設定する必要があるという重要な例外があります。簡単にするために、これは JavaScript コード内の特別なケースとして扱われます。

ノードを削除する最初のステップは、ノードが存在するかどうかを判断することです。

バイナリサーチツリー.プロトタイプ = {

  //ここにさらにコード

  削除: 関数(値){

    var found = false、
      親 = null、
      現在の = this._root、
      子の数、
      交換、
      置き換え親;

    //検索するノードがあることを確認する
    while(!found && current){

      //値が現在のノードより小さい場合は左へ進む
      (値<現在の値)の場合{
        親 = 現在;
        現在の = 現在の.left;

      //値が現在のノードより大きい場合は右に進みます
      } それ以外の場合 (値 > 現在の値){
        親 = 現在;
        現在の = 現在の.right;

      //値が等しい、見つかりました!
      } それ以外 {
        見つかりました = true;
      }
    }

    //ノードが見つかった場合のみ続行します
    (見つかった場合){
      //続く
    }

  },
  //ここにさらにコード

};

remove() メソッドの最初の部分は、バイナリ検索を使用して削除するノードを見つけ、値が現在のノードより小さい場合は左に移動し、値が現在のノードより大きい場合は右に移動することです。最終的にはノードを親から削除する必要があるため、トラバース時に親ノードを追跡します。 found が true に等しい場合、current の値は削除されるノードになります。

ノードを削除するときに注意すべき条件が 3 つあります。

  1. リーフノード
  2. 子ノードが1つだけあるノード
  3. 2つの子を持つノード

バイナリ検索ツリーからリーフノード以外のものを削除すると、ツリーを適切にソートするために値を移動する必要があります。最初の 2 つは実装が比較的簡単で、リーフ ノードを削除し、子ノードを 1 つ持つノードを削除してその子ノードに置き換えるだけです。最後のケースは、後でアクセスするのが少し複雑になります。

ノードを削除する方法を理解する前に、ノードに存在する子ノードの数を知る必要があります。それがわかったら、ノードがルート ノードであるかどうかを判断する必要があります。その結果、かなり単純な決定ツリーが得られます。

バイナリサーチツリー.プロトタイプ = {

  //ここにさらにコード

  削除: 関数(値){

    var found = false、
      親 = null、
      現在の = this._root、
      子の数、
      交換、
      置き換え親;

    //ノードを見つける(スペースの関係で削除)

    //ノードが見つかった場合のみ続行します
    (見つかった場合){

      //子供の数を計算する
      子カウント = (current.left !== null ? 1 : 0) + 
             (current.right !== null ? 1 : 0);

      //特別なケース: 値はルートにあります
      (現在の値 === this._root) の場合{
        スイッチ(子の数){

          //子要素はなし、ルート要素のみ削除
          ケース0:
            this._root = null;
            壊す;

          // 子が 1 つある場合、そのうちの 1 つをルートとして使用します
          ケース1:
            this._root = (current.right === null ? 
                   現在の左:現在の右);
            壊す;

          // 子供が2人、仕事はほとんどない
          ケース2:

            //やるべきこと

          //デフォルトなし

        }    

      //非ルート値
      } それ以外 {

        スイッチ (childCount) {

          //子はないので、親から削除するだけです
          ケース0:
            //現在の値がその 
            //親の左ポインタをnullにする
            (現在の値 < 親の値) の場合{
              親.left = null;

            //現在の値がその
            //親の右ポインタをnullにする
            } それ以外 {
              親.right = null;
            }
            壊す;

          // 子が 1 つある場合は、親に再割り当てするだけです
          ケース1:
            //現在の値がその 
            //親の左ポインタをリセットする
            (現在の値 < 親の値) の場合{
              parent.left = (current.left === null ? 
                      現在の右:現在の左);

            //現在の値がその 
            //親の右ポインタをリセットする
            } それ以外 {
              parent.right = (current.left === null ? 
                      現在の右:現在の左);
            }
            壊す;  

          // 2 つの子、少し複雑
          ケース2:

            //やるべきこと     

          //デフォルトなし

        }

      }

    }

  },

  //ここにさらにコード

};

ルートノードを扱う場合は、それを上書きするだけの簡単なプロセスです。ルート以外のノードの場合、削除するノードの値に応じて、親の対応するポインタを設定する必要があります。削除された値が親より小さい場合、左のポインタを null (子を持たないノードの場合) または削除されたノードの左のポインタにリセットする必要があります。削除された値が親より大きい場合、右のポインタを null または削除されたノードの右のポインタにリセットする必要があります。

前述したように、2 つの子を持つノードを削除するのが最も複雑な操作です。次の図は、カテキュー検索ツリーの表現です。

ルートは 8 で、左の子は 3 です。3 を削除するとどうなるでしょうか? 2 つの可能性があります: 1 (3 の左の子、順序どおりの先行子と呼ばれる) または 4 (右のサブツリーの左端の子、順序どおりの後続子と呼ばれる) はどちらも 3 を置き換えることができます。

これら 2 つのオプションのいずれかが適切です。順序どおりの先行値、つまり削除する値の前の値を見つけるには、削除するノードの左のサブツリーをチェックして、右端の子を選択します。順序どおりの後続値、つまり削除する値の直後の値を見つけるには、プロセスを逆にして、左端の右のサブツリーをチェックします。これらの各操作を完了するには、ツリーをもう一度通過する必要があります。

バイナリサーチツリー.プロトタイプ = {

  //ここにさらにコード

  削除: 関数(値){

    var found = false、
      親 = null、
      現在の = this._root、
      子の数、
      交換、
      置き換え親;

    //ノードを見つける(スペースの関係で削除)

    //ノードが見つかった場合のみ続行します
    (見つかった場合){

      //子供の数を計算する
      子カウント = (current.left !== null ? 1 : 0) + 
             (current.right !== null ? 1 : 0);

      //特別なケース: 値はルートにあります
      (現在の値 === this._root) の場合{
        スイッチ(子の数){

          //スペース節約のため他のケースは削除

          // 子供が2人、仕事はほとんどない
          ケース2:

            //新しいルートは古いルートの左の子になります
            //... 多分
            置き換え = this._root.left;

            // 最も右の葉ノードを見つけます 
            //本当の新しいルート
            (replacement.right !== null){
              置き換え親 = 置き換え;
              置き換え = 置き換え.right;
            }

            //左の最初のノードではない
            (置換親!== null)の場合{

              //新しいルートを 
              //前の位置
              置き換え親.right = 置き換え.left;

              //新しいルートに古いものをすべて渡す 
              //ルートの子
              replacement.right = this._root.right;
              replacement.left = this._root.left;
            } それ以外 {

              //子を割り当てるだけ
              replacement.right = this._root.right;
            }

            //正式に新しいルートを割り当てる
            this._root = 置換;

          //デフォルトなし

        }    

      //非ルート値
      } それ以外 {

        スイッチ (childCount) {

          //スペース節約のため他のケースは削除

          // 2 つの子、少し複雑
          ケース2:

            //新しいトラバーサルのためにポインタをリセットする
            置き換え = current.left;
            置換親 = 現在の;

            //一番右のノードを見つける
            while(replacement.right !== null){
              置き換え親 = 置き換え;
              置き換え = 置き換え.right;
            }

            置き換え親.right = 置き換え.left;

            // 置換に子を割り当てる
            置き換え後の右辺を現在の右辺とする。
            置き換え後の左辺を現在の左辺とする。

            // 置換物を適切な場所に配置する
            (現在の値 < 親の値) の場合{
              親.left = 置き換え;
            } それ以外 {
              親.right = 置き換え;
            }     

          //デフォルトなし

        }

      }

    }

  },

  //ここにさらにコード

};

ルート ノードと 2 つの子を持つ非ルート ノードのコードはほぼ同じです。この実装では、常に左のサブツリーを調べて右端の子を見つけることで、順序どおりの先行項目を見つけます。トラバーサルは、while ループ内の replacement 変数と replacementParent 変数を使用して実行されます。置換内のノードは最終的に現在のノードを置き換えるノードとなるため、その親の右ポインタを置換の左ポインタに設定することで、現在の位置から削除されます。ルート ノードの場合、replacement がルート ノードの直接の子である場合、replacementParent は null になるため、replacement の右ポインターはルートの右ポインターに設定されます。最後のステップは、置換ノードを正しい場所に割り当てることです。ルート ノードの場合、置換は新しいルートに設定され、ルート以外のノードの場合、置換は元の親の適切な位置に割り当てられます。

注意: 常にノードをその順序どおりの先行ノードに置き換えると、ほとんどの値がツリーの片側にある不均衡なツリーが発生する可能性があります。不均衡なツリーは検索の効率が低いことを意味するため、実際のシナリオでは懸念事項となるはずです。バイナリ検索ツリーの実装では、ツリーが適切にバランスを保つように、順序どおりの前任者を使用するか、順序どおりの後任者を使用するかを決定する必要があります (多くの場合、自己バランス型バイナリ検索ツリーと呼ばれます)。

要約する

JavaScript を使用してバイナリ サーチ ツリーを実装する方法に関するこの記事はこれで終わりです。JavaScript バイナリ サーチ ツリーに関するより関連性の高いコンテンツについては、123WORDPRESS.COM の以前の記事を検索するか、以下の関連記事を引き続き参照してください。今後とも 123WORDPRESS.COM をよろしくお願いいたします。

以下もご興味があるかもしれません:
  • JavaScript データ構造における二分探索木の実装方法
  • Javascriptは、小さな配列から大きな配列へのバイナリ検索ツリーの変換を実装します。
  • JavaScript アルゴリズムの二分探索木の例コード
  • JavaScript で二分探索木を実装する
  • JavaScript 初心者のための二分探索木アルゴリズムのチュートリアル

<<:  .Htaccess を使用して、Web サイトへの悪意のある IP 攻撃を防ぎ、指定されたドメイン名へのアクセスを禁止し、マシン クローラーを禁止し、ホットリンクを禁止します。

>>:  MySQL の厄介な Aborted 警告をケーススタディで分析する

推薦する

古い Vue プロジェクトに Vite サポートを追加する方法

1. はじめに会社のプロジェクトを引き継いで2年になります。今では毎回プロジェクトを起動するのに1分...

MySQL水平および垂直テーブル変換操作の実装方法

この記事では、例を使用して、MySQL の水平テーブルと垂直テーブル間の変換操作を実装する方法を説明...

HTML マルチヘッダーテーブルコード

1. マルチヘッダーテーブルコードコードをコピーコードは次のとおりです。 <!DOCTYPE ...

nginx高可用性クラスタの実装プロセス

この記事は主に、nginx 高可用性クラスタの実装プロセスを紹介します。この記事のサンプルコードは非...

proxy_pass を設定した後に Nginx が 404 を返す問題を解決する

目次1. proxy_pass を設定した後に Nginx が 404 を返す問題のトラブルシューテ...

dockerfile における ENTRYPOINT と CMD の組み合わせと違い

前回の記事【dockerコンテナのためのdockerfileを詳しく解説】では、dockerfile...

border-image を使用してテキストバブルの境界線を実装する方法のサンプルコード

開発中に、非常に単純なテキストバブル効果に遭遇しました。これは、おおよそ次のようになります。 うーん...

MySQL sql_mode の使用に関する詳細な説明

目次序文sql_mode の説明最も重要なオプションすべてのオプション要約する序文前回の記事「MyS...

W3C チュートリアル (3): W3C HTML アクティビティ

HTML は、World Wide Web 上で公開するために使用されるハイブリッド言語です。 XH...

Ubuntu 20.04は静的IPアドレスを設定します(異なるバージョンを含む)

Ubuntu 20.04はnetplanを通じてネットワークを管理するため、以前のバージョンとは少...

React Fiberの仕組みの詳細な説明

目次React Fiberとは何ですか?なぜReact Fiberなのか? React Fiberは...

JavaScriptは行削除機能を備えたテーブルを動的に生成します

この記事の例では、テーブルを動的に生成したり行を削除したりするためのJavaScriptの具体的なコ...

MySQLデータベースが予期せずクラッシュし、テーブルデータファイルが破損して起動できなくなる問題を解決します。

問題: MySQL データベースが予期せずクラッシュしたため、データベースを起動できませんでした。エ...

MySQLデータベースインデックスの左端一致原則

目次1. 共同インデックスの説明2. ac はインデックスを使用できますか? 3. 考える4. 最左...

DockerはRedis5.0をビルドし、データをマウントします

目次1. 永続データの簡単なマウント2. DockerFileでイメージをビルドし、設定ファイルを指...