💭

C言語でTreeMapを実装する。

2024/04/02に公開

はじめに

C 言語でのツリーマップ(TreeMap)の設計と実装について詳しく説明します。コードは細かく説明しています。

TreeMap とは

TreeMap とは、キーバリュー形式のデータ構造です。追加、検索、削除の計算量が O(log(n)) という特徴を持ちます。

TreeMap は二分探索木を使って実装されます。二分探索木は、各ノードにおいて、親ノードよりも小さい値を持つノードは左に、大きい値を持つノードは右に配置される木構造です。こうすることで、ルートノードから任意のノードまでの探索が O(log(n)) で行えます。

仕組み

TreeMap は、与えられたキーに対して二分木を構築し、高速なアクセスを実現します。二分木の各ノードは、左の子ノードよりも小さく、右の子ノードよりも大きい値を持ちます。二分木の高さが log(n) であるため、追加、検索、削除の計算量が O(log(n)) になります。

データ構造

TreeMap のデータ構造は以下です。

typedef struct Node Node;
struct Node {
  char *key;
  char *value;
  Node *left;
  Node *right;
};

typedef struct TreeMap TreeMap;
struct TreeMap {
  Node *root;
};

Node は二分木のノードを表します。root には二分木の根が入ります。

初期化

TreeMap の初期化は以下のように実装します。

TreeMap *newTreeMap() {
  TreeMap *treeMap = malloc(sizeof(TreeMap));
  treeMap->root = NULL;
  return treeMap;
}

root は NULL で初期化します。

追加

TreeMap にキーと値を追加する関数を以下に示します。

Node **findNode(Node **cur, const char *key) {
  while (*cur) {
    if (strcmp(key, (*cur)->key) > 0) {
      cur = &((*cur)->right);
    } else if (strcmp(key, (*cur)->key) < 0) {
      cur = &((*cur)->left);
    } else {
      break;
    }
  }
  return cur;
}

static Node *newNode(const char *key, const char *value) {
  Node *node = malloc(sizeof(Node));
  node->key = strdup(key);
  node->value = strdup(value);
  node->left = NULL;
  node->right = NULL;
  return node;
}

void insertToTreeMap(TreeMap *treemap, const char *key, const char *value) {
  Node **node = findNode(&(treemap->root), key);
  if (*node != NULL) {
    free((*node)->value);
    (*node)->value = strdup(value);
    return;
  }
  *node = newNode(key, value);
}

追加はポインタのポインタを使って再帰を使わずに実装します。ポインタのポインタはC言語の最難関の一つですが、理解すると非常に強力なテクニックです。

検索

TreeMap からキーに対応する値を取得する関数を以下に示します。

Node *searchNode(Node *cur, const char *key) {
  while (cur) {
    if (strcmp(key, cur->key) > 0) {
      cur = cur->right;
    } else if (strcmp(key, cur->key) < 0) {
      cur = cur->left;
    } else {
      break;
    }
  }
  return cur;
}

char *getValueFromTreeMap(const TreeMap *treemap, const char *key) {
  Node *node = searchNode(treemap->root, key);
  if (node == NULL) return NULL;
  return node->value;
}

検索は二分木を辿りながらキーを比較していき、一致したら値を返します。

使ってみる

TreeMap を使ってみます。

int main() {
  TreeMap *treeMap = newTreeMap();

  insertToTreeMap(treeMap, "key2", "value2");
  insertToTreeMap(treeMap, "key1", "value1");
  insertToTreeMap(treeMap, "key3", "value3");

  char *s = getValueFromTreeMap(treeMap, "key1");
  printf("%s\n", s); // value1
  s = getValueFromTreeMap(treeMap, "key2");
  printf("%s\n", s); // value2
  s = getValueFromTreeMap(treeMap, "key3");
  printf("%s\n", s); // value3
  return 0;
}

TreeMap にキーと値を追加し、検索して値を取得します。普通に使うことができます。

最悪ケース

この実装では、最悪ケースで二分木がリストになるため、計算量が O(n) になります。最悪ケースは、キーを追加する順番が昇順または降順の場合で、以下のような入力が最悪ケースになります。

insertToTreeMap(treeMap, "key1", "value1");
insertToTreeMap(treeMap, "key2", "value2");
insertToTreeMap(treeMap, "key3", "value3");

この場合、二分木がリストになり、計算量が O(n) になります。この問題を解決するためには、平衡二分木を使う必要があります。

メモリの解放

メモリ解放を実装します。

static void freeComponents(Node *node) {
  if (node == NULL) return;
  free(node->key);
  free(node->value);
}

static void freeNodeAndComponents(Node *node) {
  if (node == NULL) return;
  freeComponents(node);
  free(node);
}

static void freeNode(Node *node) {
  if (node == NULL) return;
  freeNode(node->left);
  freeNode(node->right);
  freeNodeAndComponents(node);
}

void freeTreeMap(TreeMap *treemap) {
  freeNode(treemap->root);
  free(treemap);
}

再帰関数を利用しています。

要素の削除

要素の削除を実装します。

static Node **findMinNode(Node **cur) {
  while ((*cur)->left) cur = &((*cur)->left);
  return cur;
}

int removeFromTreeMap(TreeMap *treemap, const char *key) {
  Node **node = findNode(&(treemap->root), key);
  if (*node == NULL) return -1;

  Node *tmp = *node;
  if ((*node)->left == NULL) {
    *node = (*node)->right;
    freeNodeAndComponents(tmp);
    return 0;
  }

  if ((*node)->right == NULL) {
    *node = (*node)->left;
    freeNodeAndComponents(tmp);
    return 0;
  }

  Node **min = findMinNode(&((*node)->right));
  freeComponents(*node);
  (*node)->key = strdup((*min)->key);
  (*node)->value = strdup((*min)->value);

  Node *tmp2 = *min;
  *min = (*min)->right;
  freeNodeAndComponents(tmp2);
  return 0;
}

要素の削除は、削除するノードが子を持たない場合、または片方の子を持つ場合、削除するノードを削除するだけです。削除するノードが両方の子を持つ場合、右の子の最小ノードを削除するノードに移動させ、右の子の最小ノードを削除します。慎重にメモリを解放する必要があります。

まとめ

C 言語で TreeMap を実装する方法について説明しました。TreeMap はキーと値を追加、検索、削除する際に O(log(n)) の計算量で高速に処理できます。ただ、今回の単純な実装では最悪ケースで O(n) になります。発展課題として、平衡二分木を使うことで最悪ケースでも O(log(n)) にすることができます。平衡二分木の実装は別で説明します。

コードは以下にあります。

https://github.com/derbuihan/c_treemap

Discussion