日本語

二分探索木(BST)の基礎を探求し、JavaScriptで効率的に実装する方法を学びます。このガイドはBSTの構造、操作、そして世界中の開発者向けの実用的な例を網羅しています。

二分探索木:JavaScriptによる包括的な実装ガイド

二分探索木(BST)は、コンピュータサイエンスにおける基本的なデータ構造であり、データの効率的な探索、ソート、および取得に広く使用されています。その階層構造により、多くの操作で対数時間計算量が実現され、大規模なデータセットを管理するための強力なツールとなります。このガイドでは、BSTの包括的な概要を提供し、世界中の開発者向けにJavaScriptでの実装を実演します。

二分探索木を理解する

二分探索木とは?

二分探索木は、各ノードが最大2つの子(左の子と右の子と呼ばれる)を持つ木構造ベースのデータ構造です。BSTの重要な特性は、任意のノードに対して以下の条件が成り立つことです:

この特性により、BST内の要素は常に順序付けられ、効率的な探索と取得が可能になります。

主要な概念

JavaScriptで二分探索木を実装する

Nodeクラスの定義

まず、BSTの各ノードを表す`Node`クラスを定義します。各ノードは、データを格納する`key`と、その子を指す`left`および`right`ポインタを含みます。


class Node {
  constructor(key) {
    this.key = key;
    this.left = null;
    this.right = null;
  }
}

BinarySearchTreeクラスの定義

次に、`BinarySearchTree`クラスを定義します。このクラスは、ルートノードと、木の挿入、探索、削除、巡回のためのメソッドを含みます。


class BinarySearchTree {
  constructor() {
    this.root = null;
  }

  // ここにメソッドが追加されます
}

挿入

`insert`メソッドは、与えられたキーを持つ新しいノードをBSTに追加します。挿入プロセスは、既存のノードとの相対的な位置に新しいノードを配置することで、BSTの特性を維持します。


insert(key) {
  const newNode = new Node(key);

  if (this.root === null) {
    this.root = newNode;
  } else {
    this.insertNode(this.root, newNode);
  }
}

insertNode(node, newNode) {
  if (newNode.key < node.key) {
    if (node.left === null) {
      node.left = newNode;
    } else {
      this.insertNode(node.left, newNode);
    }
  } else {
    if (node.right === null) {
      node.right = newNode;
    } else {
      this.insertNode(node.right, newNode);
    }
  }
}

例: BSTに値を挿入する


const bst = new BinarySearchTree();
bst.insert(11);
bst.insert(7);
bst.insert(15);
bst.insert(5);
bst.insert(3);
bst.insert(9);
bst.insert(8);
bst.insert(10);
bst.insert(13);
bst.insert(12);
bst.insert(14);
bst.insert(20);
bst.insert(18);
bst.insert(25);

探索

`search`メソッドは、与えられたキーを持つノードがBSTに存在するかどうかを確認します。キーを現在のノードのキーと比較し、それに応じて左または右の部分木に移動して木を巡回します。


search(key) {
  return this.searchNode(this.root, key);
}

searchNode(node, key) {
  if (node === null) {
    return false;
  }

  if (key < node.key) {
    return this.searchNode(node.left, key);
  } else if (key > node.key) {
    return this.searchNode(node.right, key);
  } else {
    return true;
  }
}

例: BSTで値を探索する


console.log(bst.search(9));  // 出力: true
console.log(bst.search(2));  // 出力: false

削除

`remove`メソッドは、与えられたキーを持つノードをBSTから削除します。これは、ノードを削除する際にBSTの特性を維持する必要があるため、最も複雑な操作です。考慮すべき3つのケースがあります:


remove(key) {
  this.root = this.removeNode(this.root, key);
}

removeNode(node, key) {
  if (node === null) {
    return null;
  }

  if (key < node.key) {
    node.left = this.removeNode(node.left, key);
    return node;
  } else if (key > node.key) {
    node.right = this.removeNode(node.right, key);
    return node;
  } else {
    // keyがnode.keyと等しい場合

    // ケース1 - リーフノード
    if (node.left === null && node.right === null) {
      node = null;
      return node;
    }

    // ケース2 - ノードに子が1つだけある場合
    if (node.left === null) {
      node = node.right;
      return node;
    } else if (node.right === null) {
      node = node.left;
      return node;
    }

    // ケース3 - ノードに子が2つある場合
    const aux = this.findMinNode(node.right);
    node.key = aux.key;
    node.right = this.removeNode(node.right, aux.key);
    return node;
  }
}

findMinNode(node) {
  let current = node;
  while (current != null && current.left != null) {
    current = current.left;
  }
  return current;
}

例: BSTから値を削除する


bst.remove(7);
console.log(bst.search(7)); // 出力: false

木の巡回

木の巡回は、特定の順序で木内の各ノードを訪れることです。いくつかの一般的な巡回方法があります:


inOrderTraverse(callback) {
  this.inOrderTraverseNode(this.root, callback);
}

inOrderTraverseNode(node, callback) {
  if (node !== null) {
    this.inOrderTraverseNode(node.left, callback);
    callback(node.key);
    this.inOrderTraverseNode(node.right, callback);
  }
}

preOrderTraverse(callback) {
  this.preOrderTraverseNode(this.root, callback);
}

preOrderTraverseNode(node, callback) {
  if (node !== null) {
    callback(node.key);
    this.preOrderTraverseNode(node.left, callback);
    this.preOrderTraverseNode(node.right, callback);
  }
}

postOrderTraverse(callback) {
  this.postOrderTraverseNode(this.root, callback);
}

postOrderTraverseNode(node, callback) {
  if (node !== null) {
    this.postOrderTraverseNode(node.left, callback);
    this.postOrderTraverseNode(node.right, callback);
    callback(node.key);
  }
}

例: BSTを巡回する


const printNode = (value) => console.log(value);

bst.inOrderTraverse(printNode);   // 出力: 3 5 8 9 10 11 12 13 14 15 18 20 25
bst.preOrderTraverse(printNode);  // 出力: 11 5 3 9 8 10 15 13 12 14 20 18 25
bst.postOrderTraverse(printNode); // 出力: 3 8 10 9 12 14 13 18 25 20 15 11

最小値と最大値

BSTの順序付けられた性質のおかげで、最小値と最大値を見つけるのは簡単です。


min() {
  return this.minNode(this.root);
}

minNode(node) {
  let current = node;
  while (current !== null && current.left !== null) {
    current = current.left;
  }
  return current;
}

max() {
  return this.maxNode(this.root);
}

maxNode(node) {
  let current = node;
  while (current !== null && current.right !== null) {
    current = current.right;
  }
  return current;
}

例: 最小値と最大値を見つける


console.log(bst.min().key); // 出力: 3
console.log(bst.max().key); // 出力: 25

二分探索木の応用例

二分探索木は、以下のようなさまざまなアプリケーションで使用されています:

パフォーマンスに関する考慮事項

BSTのパフォーマンスはその構造に依存します。最良のケースでは、平衡したBSTは挿入、検索、削除の操作で対数時間計算量を可能にします。しかし、最悪のケース(例:偏った木)では、時間計算量は線形時間にまで低下する可能性があります。

平衡木と非平衡木

平衡BSTとは、すべてのノードの左右の部分木の高さの差が最大で1である木のことです。AVL木や赤黒木などの自己平衡アルゴリズムは、木が平衡状態を保つことを保証し、一貫したパフォーマンスを提供します。サーバーへの負荷に基づいて、地域ごとに異なる最適化レベルが必要になる場合があります。平衡化は、グローバルな高使用状況下でパフォーマンスを維持するのに役立ちます。

時間計算量

BSTの高度な概念

自己平衡木

自己平衡木は、平衡を維持するために自動的に構造を調整するBSTです。これにより、木の高さが対数的に保たれ、すべての操作で一貫したパフォーマンスが提供されます。一般的な自己平衡木には、AVL木や赤黒木があります。

AVL木

AVL木は、任意のノードの左右の部分木の高さの差が最大で1であることを保証することで平衡を維持します。このバランスが崩れると、回転が実行されてバランスが回復されます。

赤黒木

赤黒木は、色のプロパティ(赤または黒)を使用して平衡を維持します。AVL木よりも複雑ですが、特定のシナリオではより良いパフォーマンスを提供します。

JavaScriptコード例:二分探索木の完全な実装


class Node {
  constructor(key) {
    this.key = key;
    this.left = null;
    this.right = null;
  }
}

class BinarySearchTree {
  constructor() {
    this.root = null;
  }

  insert(key) {
    const newNode = new Node(key);

    if (this.root === null) {
      this.root = newNode;
    } else {
      this.insertNode(this.root, newNode);
    }
  }

  insertNode(node, newNode) {
    if (newNode.key < node.key) {
      if (node.left === null) {
        node.left = newNode;
      } else {
        this.insertNode(node.left, newNode);
      }
    } else {
      if (node.right === null) {
        node.right = newNode;
      } else {
        this.insertNode(node.right, newNode);
      }
    }
  }

  search(key) {
    return this.searchNode(this.root, key);
  }

  searchNode(node, key) {
    if (node === null) {
      return false;
    }

    if (key < node.key) {
      return this.searchNode(node.left, key);
    } else if (key > node.key) {
      return this.searchNode(node.right, key);
    } else {
      return true;
    }
  }

  remove(key) {
    this.root = this.removeNode(this.root, key);
  }

  removeNode(node, key) {
    if (node === null) {
      return null;
    }

    if (key < node.key) {
      node.left = this.removeNode(node.left, key);
      return node;
    } else if (key > node.key) {
      node.right = this.removeNode(node.right, key);
      return node;
    } else {
      // keyがnode.keyと等しい場合

      // ケース1 - リーフノード
      if (node.left === null && node.right === null) {
        node = null;
        return node;
      }

      // ケース2 - ノードに子が1つだけある場合
      if (node.left === null) {
        node = node.right;
        return node;
      } else if (node.right === null) {
        node = node.left;
        return node;
      }

      // ケース3 - ノードに子が2つある場合
      const aux = this.findMinNode(node.right);
      node.key = aux.key;
      node.right = this.removeNode(node.right, aux.key);
      return node;
    }
  }

  findMinNode(node) {
    let current = node;
    while (current != null && current.left != null) {
      current = current.left;
    }
    return current;
  }

  min() {
    return this.minNode(this.root);
  }

  minNode(node) {
    let current = node;
    while (current !== null && current.left !== null) {
      current = current.left;
    }
    return current;
  }

  max() {
    return this.maxNode(this.root);
  }

  maxNode(node) {
    let current = node;
    while (current !== null && current.right !== null) {
      current = current.right;
    }
    return current;
  }

  inOrderTraverse(callback) {
    this.inOrderTraverseNode(this.root, callback);
  }

  inOrderTraverseNode(node, callback) {
    if (node !== null) {
      this.inOrderTraverseNode(node.left, callback);
      callback(node.key);
      this.inOrderTraverseNode(node.right, callback);
    }
  }

  preOrderTraverse(callback) {
    this.preOrderTraverseNode(this.root, callback);
  }

  preOrderTraverseNode(node, callback) {
    if (node !== null) {
      callback(node.key);
      this.preOrderTraverseNode(node.left, callback);
      this.preOrderTraverseNode(node.right, callback);
    }
  }

  postOrderTraverse(callback) {
    this.postOrderTraverseNode(this.root, callback);
  }

  postOrderTraverseNode(node, callback) {
    if (node !== null) {
      this.postOrderTraverseNode(node.left, callback);
      this.postOrderTraverseNode(node.right, callback);
      callback(node.key);
    }
  }
}

// 使用例
const bst = new BinarySearchTree();
bst.insert(11);
bst.insert(7);
bst.insert(15);
bst.insert(5);
bst.insert(3);
bst.insert(9);
bst.insert(8);
bst.insert(10);
bst.insert(13);
bst.insert(12);
bst.insert(14);
bst.insert(20);
bst.insert(18);
bst.insert(25);

const printNode = (value) => console.log(value);

console.log("中間順巡回:");
bst.inOrderTraverse(printNode);

console.log("先行順巡回:");
bst.preOrderTraverse(printNode);

console.log("後行順巡回:");
bst.postOrderTraverse(printNode);

console.log("最小値:", bst.min().key);
console.log("最大値:", bst.max().key);

console.log("9を探索:", bst.search(9));
console.log("2を探索:", bst.search(2));

bst.remove(7);
console.log("7を削除後に探索:", bst.search(7));

結論

二分探索木は、多くの応用を持つ強力で汎用性の高いデータ構造です。このガイドでは、BSTの構造、操作、およびJavaScriptでの実装を網羅した包括的な概要を提供しました。このガイドで説明した原則と技術を理解することで、世界中の開発者はソフトウェア開発におけるさまざまな問題を解決するためにBSTを効果的に活用できます。グローバルなデータベースの管理から検索アルゴリズムの最適化まで、BSTの知識はどのプログラマーにとっても貴重な資産です。

コンピュータサイエンスの旅を続ける中で、自己平衡木やそのさまざまな実装のような高度な概念を探求することは、あなたの理解と能力をさらに高めるでしょう。二分探索木を効果的に使用する技術を習得するために、さまざまなシナリオで練習と実験を続けてください。