M.Hiroi's Home Page

Algorithms with Python

赤黒木 (red-black tree) [4]

[ PrevPage | Python | NextPage ]

はじめに

前回は 2-3 木をベースにした Left-Leaning Red Black Tree (LLRB 木) を作成しました。LLRB 木の場合、赤節は黒節の左の子にしかなれません。今回は黒節の右の子にも赤節を許す赤黒木を作ってみましょう。

●データの挿入

データ挿入の基本的な考え方は簡単です。通常の赤黒木のようにデータを挿入しますが、左右の子がともに赤節になったならば、その節を分割して親節で木のバランスを修正します。プログラムの作成は簡単で、赤黒木 [1] のプログラムを少し修正するだけです。左部分木のバランスを修正する関数 balance_insert_left() は次のようになります。

リスト : データの挿入

# 4node の分割
def split(node):
    node.color = RED
    node.left.color = BLACK
    node.right.color = BLACK

# 左部分木の修正
def balance_insert_left(node, flag):
    if flag: return node, flag
    if node.color == BLACK:
        if node.right.color == RED:
            # 4 node なので分割
            split(node)
        else:
            if node.left.right.color == RED:
                node.left = rotate_left(node.left)
            if node.left.left.color == RED:
                # 赤が 2 つ続く
                node = rotate_right(node)
                # 4 node になるので分割
                split(node)
            else:
                # 赤一つならば終了
                flag = True
    return node, flag

# データの挿入
def insert(node, x):
    if node is null: return Node(x), False
    if x < node.data:
        node.left, flag = insert(node.left, x)
        return balance_insert_left(node, flag)
    elif x > node.data:
        node.right, flag = insert(node.right, x)
        return balance_insert_right(node, flag)
    return node, True

balance_insert_left() でバランスを修正する場合、node.left は赤節になります。最初に、右の子 (node.right) が赤節かチェックします。そうであれば、左右の子が赤になるので、split() で節を分割して node の親節でバランスを修正します。

右の子が黒節ならば、node.left の子に赤節がないかチェックします。この処理は 赤黒木 [1] のプログラムと同じですが、左右の子が赤節になった場合は split() で node を分割する処理を追加します。赤節が node.left の一つだけならば、木のバランスを修正する必要はありません。

右部分木のバランスを修正する関数 balance_lnsert_right() は、子のチェックと回転操作が左右対称になるだけなので説明は割愛いたします。詳細は プログラムリスト をお読みください。

●補足

ところで、データの挿入処理は次のようにプログラムすることもできます。

リスト : データの挿入 (2-3 木版)

# 簡単な実装方法
def insert1(node, x):
    if node is null: return Node(x)
    # 木をたどる
    if x < node.data:
        node.left = insert1(node.left, x)
    elif x > node.data:
        node.right = insert1(node.right, x)
    # バランスの修正
    # 左の子をチェック
    if node.left.color == RED:
        if node.left.right.color == RED:
            node.left = rotate_left(node.left)
        if node.left.left.color == RED:
            node = rotate_right(node)
    # 右の子をチェック
    if node.right.color == RED:
        if node.right.left.color == RED:
            node.right = rotate_right(node.right)
        if node.right.right.color == RED:
            node = rotate_left(node)
    # 4 node の分割
    if node.left.color == RED and node.right.color == RED:
        split(node)
    return node

左右の子で赤節が 2 つ続いているかチェックし、そうであれば木を回転してバランスを修正します。そのあとで左右の子がともに赤節ならば split() で分割します。また、参考 URL によると、木をたどるときに 4 node の分割処理を行うと、通常の赤黒木の挿入処理になります。

リスト : データの挿入 (2-3-4 木版)

# 簡単な実装方法
def insert1(node, x):
    if node is null: return Node(x)
    # 4 node の分割
    if node.left.color == RED and node.right.color == RED:
        split(node)
    # 木をたどる
    if x < node.data:
        node.left = insert1(node.left, x)
    elif x > node.data:
        node.right = insert1(node.right, x)
    # バランスの修正
    # 左の子をチェック
    if node.left.color == RED:
        if node.left.right.color == RED:
            node.left = rotate_left(node.left)
        if node.left.left.color == RED:
            node = rotate_right(node)
    # 右の子をチェック
    if node.right.color == RED:
        if node.right.left.color == RED:
            node.right = rotate_right(node.right)
        if node.right.right.color == RED:
            node = rotate_left(node)
    return node

このように、とても簡単にプログラムできるのですが、実行速度はどちらのプログラムも LLRB 木より少し遅くなるようです。興味のある方は試してみてください。

●データ挿入のテスト

それでは、ここでデータ挿入のテストを行ってみましょう。テストプログラムは次のようになります。

リスト : データ挿入の簡単なテスト

# 木の表示
def print_node(node, n):
    color = ('B', 'R')
    if node is not null:
        print_node(node.left, n + 1)
        print('    ' * n, color[node.color], node.data)
        print_node(node.right, n + 1)

# 赤黒木の条件を満たしているか
def check_rb23tree(node):
    if node is not null:
        if node.color == RED:
            if node.left.color == RED or node.right.color == RED:
                raise 'rb23 error1'
        else:
            if node.left.color == RED and node.right.color == RED:
                raise 'rb23 error2'
        a = check_rb23tree(node.left)
        b = check_rb23tree(node.right)
        if a != b: raise 'rb23 error3'
        if node.color == BLACK: a += 1
        return a
    return 0

# test
if __name__ == '__main__':
    import random, time
    root = make_null()
    buff = range(8)
    print('insert test')
    for x in buff:
        print('----- insert', x)
        root = insert1(root, x)
        root.color = BLACK
        print_node(root, 0)
        check_rb23tree(root)

print_node() は赤黒木を表示する関数です。黒節は B を、赤節は R をつけて表しています。終端は None ではなく null なので、node と null を比較していることに注意してください。

関数 check_rb23tree() は赤黒木の条件をチェックする関数です。node が赤節の場合、その子が赤節であればエラーを送出します。node が黒節の場合、左右の子がともに赤節の場合もエラーを送出します。check_rb23tree() の返り値は黒高さです。左右の部分木の黒高さを求め、それが等しくない場合はエラーを送出します。

テストは 0 から 7 までの整数値を順番に赤黒木に挿入します。実行結果は次のようになります。

insert test
----- insert 0
 B 0
----- insert 1
 B 0
     R 1
----- insert 2
     B 0
 B 1
     B 2
----- insert 3
     B 0
 B 1
     B 2
         R 3
----- insert 4
     B 0
 B 1
         B 2
     R 3
         B 4
----- insert 5
     B 0
 B 1
         B 2
     R 3
         B 4
             R 5
----- insert 6
         B 0
     B 1
         B 2
 B 3
         B 4
     B 5
         B 6
----- insert 7
         B 0
     B 1
         B 2
 B 3
         B 4
     B 5
         B 6
             R 7

どの経路でも黒高さが同じになっていることがすぐにわかると思います。また、赤節は黒節の左右のどちらかにしか存在しません。これが 2-3 木をベースにした赤黒木の特徴です。

今度は木の高さを比較してみましょう。テストプログラムは次のようになります。

リスト : データ挿入の簡単なテスト (2)

import node
import avlnode
import rbnode
import rb23node
import llrb23node
import random

# 木の高さを求める
def get_height(node, term = None):
    if node is not term:
        a = get_height(node.left)
        b = get_height(node.right)
        return max(a, b) + 1
    return 0

for n in [1000, 2000, 4000, 8000, 16000, 32000]:
        a = None
        b = rbnode.make_null()
        c = rb23node.make_null()
        d = llrb23node.make_null()
        buff = list(range(n))
        random.shuffle(buff)
        for x in buff:
            a = avlnode.insert(a, x)
            b, _ = rbnode.insert(b, x)
            b.color = rbnode.BLACK
            c, _ = rb23node.insert(c, x)
            c.color = rb23node.BLACK
            d, _ = llrb23node.insert1(d, x)
            d.color = llrb23node.BLACK
        print(n, get_height(a), get_height(b, rbnode.null), get_height(c, rb23node.null), get_height(d, llrb23node.null))

それでは、実行結果を示します。

      表 : 木の高さ

  N   : AVL  RB  RB23 LLRB
---------------------------
 1000 : 12   12   12   13
 2000 : 13   14   14   16
 4000 : 14   15   15   16
 8000 : 16   16   16   18
16000 : 16   17   17   19
32000 : 18   18   19   21

RB は赤黒木 (2-3-4 木) で、RB23 は今回作成した赤黒木 (2-3 木) です。木の高さは AVL 木 < RB 木 < RB23 木 < LLRB 木になりました。

次はソート済みデータで試してみましょう。結果は次のようになりました。

      表 : 木の高さ
  
 (a) 昇順

  N   : AVL  RB  RB23 LLRB
---------------------------
 1000 : 10   17   15   10
 2000 : 11   19   16   11
 4000 : 12   21   17   12
 8000 : 13   23   18   13
16000 : 14   25   19   14
32000 : 15   27   20   15

 (b) 逆順

  N   : AVL  RB  RB23 LLRB
---------------------------
 1000 : 10   17   15   15
 2000 : 11   19   16   16
 4000 : 12   21   17   17
 8000 : 13   23   18   18
16000 : 14   25   19   19
32000 : 15   27   20   20

ソート済みデータの場合、木の高さは RB 木よりも RB23 木の方が低くなりました。これらの結果を見ると、2-3 木をベースにした赤黒木でも十分な性能が得られるように思います。

●データ削除のテスト

データの削除は 赤黒木 [2] のプログラムをそのまま使用することができます。それでは、ここでデータ削除のテストを行ってみましょう。テストプログラムは次のようになります。

リスト : データ削除の簡単なテスト

# test
if __name__ == '__main__':
    import random, time
    root = make_null()
    buff = range(8)
    print('insert test')
    for x in buff:
        root = insert(root, x)
        root.color = BLACK
        check_rb23tree(root)
    print('delete test')
    print_node(root, 0)
    for x in buff:
        print('----- delete ', x)
        root, _ = delete(root, x)
        root.color = BLACK
        print_node(root, 0)
        check_rb23tree(root)

実行結果を示します。

         B 0
     B 1
         B 2
 B 3
         B 4
     B 5
         B 6
             R 7
delete test
----- delete 0
     B 1
         R 2
 B 3
         B 4
     R 5
         B 6
             R 7
----- delete 1
     B 2
 B 3
         B 4
     R 5
         B 6
             R 7
----- delete 2
     B 3
         R 4
 B 5
     B 6
         R 7
----- delete 3
     B 4
 B 5
     B 6
         R 7
----- delete 4
     B 5
 B 6
     B 7
----- delete 5
 B 6
     R 7
----- delete 6
 B 7
----- delete 7

このように、データを削除しても赤黒木のバランスが大きく崩れることはありません。

●RB23tree クラスの作成

最後に赤黒木を表すクラスを作成します。次のリストを見てください。

#
# rb23tree.py : 赤黒木 (2-3 木をベース)
#
#               Copyright (C) 2009-2022 Makoto Hiroi
#
import rb23node

# 赤黒木
class RB23tree:
    def __init__(self):
        self.root = rb23node.make_null()

    # 探索
    def search(self, x):
        return rb23node.search(self.root, x)

    # 挿入
    def insert(self, x):
        self.root, _ = rb23node.insert(self.root, x)
        self.root.color = rb23node.BLACK

    # 削除
    def delete(self, x):
        self.root, _ = rb23node.delete(self.root, x)
        self.root.color = rb23node.BLACK

    # 巡回
    def traverse(self):
        for x in rb23node.traverse(self.root):
            yield x

    # 表示
    def __str__(self):
        if self.root is rb23node.null: return 'RB23tree()'
        buff = 'RB23tree('
        for x in rb23node.traverse(self.root):
            buff += '%s, ' % x
        buff = buff.rstrip(',  ')
        buff += ')'
        return buff

# テスト
if __name__ == '__main__':
    import random
    tree = RB23tree()
    data = [random.randint(0, 100) for x in range(10)]
    print(data)
    print(tree)
    for x in data: tree.insert(x)
    print(tree)
    for x in data:
        print('search', x, tree.search(x))
        print('delete', x)
        tree.delete(x)
        print('search', x, tree.search(x))
        print(tree)

クラス名は RB23tree としました。RB23tree のメソッドはモジュール rb23node の操作関数を呼び出すだけです。メソッド __init__() で root を初期化するとき、モジュール rb23node の関数 make_null() を呼び出して、終端オブジェクトを設定することに注意してください。あとは、とくに難しいところはないでしょう。

それでは、テストの実行結果を示します。

[21, 64, 51, 35, 94, 17, 89, 35, 99, 98]
RB23tree()
RB23tree(17, 21, 35, 51, 64, 89, 94, 98, 99)
search 21 True
delete 21
search 21 False
RB23tree(17, 35, 51, 64, 89, 94, 98, 99)
search 64 True
delete 64
search 64 False
RB23tree(17, 35, 51, 89, 94, 98, 99)
search 51 True
delete 51
search 51 False
RB23tree(17, 35, 89, 94, 98, 99)
search 35 True
delete 35
search 35 False
RB23tree(17, 89, 94, 98, 99)
search 94 True
delete 94
search 94 False
RB23tree(17, 89, 98, 99)
search 17 True
delete 17
search 17 False
RB23tree(89, 98, 99)
search 89 True
delete 89
search 89 False
RB23tree(98, 99)
search 35 False
delete 35
search 35 False
RB23tree(98, 99)
search 99 True
delete 99
search 99 False
RB23tree(98)
search 98 True
delete 98
search 98 False
RB23tree()

●RB23tree の評価

最後に、赤黒木 (2-3-4木)、LLRB 木、赤黒木 (2-3 木) の性能を比較してみましょう。次のリストを見てください。

リスト : 赤黒木 (2-3-4 木)、LLRB 木、赤黒木 (2-3 木) のテスト

from llrb23tree import *
from rbtree import *
from rb23tree import *
import time, random

def insert_test(tree, buff):
    s = time.time()
    for x in buff:
        tree.insert(x)
    e = time.time()
    return e - s

def search_test(tree, buff):
    s = time.time()
    for x in buff:
        tree.search(x)
    e = time.time()
    return e - s

def delete_test(tree, buff):
    s = time.time()
    for x in buff:
        tree.delete(x)
    e = time.time()
    return e - s

for x in [10000, 20000, 40000, 80000, 160000]:
    buff = [random.randint(0, 100000) for _ in range(x)]
    # buff.sort()
    print('{:6d}'.format(x), end='')
    for tree in [RBtree, LLRB23tree, RB23tree]:
        a = tree()
        print(': {:.3f} {:.3f} {:.3f}'.format(insert_test(a, buff), search_test(a, buff), delete_test(a, buff)), end=' ')
    print()

データは乱数で生成します。そして、insert_test() で木にデータを挿入する時間、search_test() でデータを探索する時間、delete_test() でデータを削除する時間を計測します。結果は次のようになりました。

                    表 : 実行結果 (単位 : 秒)

      :       RB tree       :      LLRB tree      :      RB23 tree
 個数 : 挿入   探索   削除  : 挿入   探索   削除  : 挿入   探索   削除
------------------------------------------------------------------------
 10000: 0.054  0.018  0.048 : 0.063  0.020  0.056 : 0.054  0.019  0.047
 20000: 0.134  0.042  0.100 : 0.129  0.041  0.109 : 0.110  0.042  0.102
 40000: 0.259  0.097  0.235 : 0.299  0.101  0.251 : 0.239  0.098  0.233
 80000: 0.568  0.223  0.504 : 0.653  0.228  0.515 : 0.526  0.224  0.500
160000: 1.131  0.486  0.997 : 1.384  0.502  1.052 : 1.068  0.498  0.999

実行環境 : Python 3.8.10, Ubuntu 20.04 LTS (WSL1), Intel Core i5-6200U 2.30GHz

通常の赤黒木と同様に、2-3 木をベースにした赤黒木も高速ですね。

次はソート済みのデータで試してみましょう。実行結果は次のようになりました。

            表 : ソート済みデータの実行結果 (単位 : 秒)

  (a) 昇順

      :       RB tree       :      LLRB tree      :      RB23 tree
 個数 : 挿入   探索   削除  : 挿入   探索   削除  : 挿入   探索   削除
------------------------------------------------------------------------
 10000: 0.091  0.017  0.045 : 0.062  0.016  0.056 : 0.066  0.017  0.040
 20000: 0.193  0.044  0.085 : 0.129  0.035  0.121 : 0.148  0.039  0.086
 40000: 0.365  0.079  0.184 : 0.287  0.074  0.256 : 0.301  0.084  0.319
 80000: 0.797  0.177  0.406 : 0.670  0.228  0.648 : 0.803  0.189  0.399
160000: 1.539  0.342  0.783 : 1.117  0.333  1.143 : 1.259  0.360  0.807

  (b) 逆順

      :       RB tree       :      LLRB tree      :      RB23 tree
 個数 : 挿入   探索   削除  : 挿入   探索   削除  : 挿入   探索   削除
------------------------------------------------------------------------
 10000: 0.078  0.017  0.044 : 0.073  0.020  0.043 : 0.064  0.017  0.047
 20000: 0.171  0.036  0.098 : 0.157  0.038  0.092 : 0.132  0.036  0.087
 40000: 0.333  0.082  0.186 : 0.345  0.080  0.191 : 0.278  0.086  0.188
 80000: 0.708  0.165  0.419 : 0.718  0.170  0.401 : 0.565  0.165  0.396
160000: 1.383  0.351  0.774 : 1.413  0.362  0.813 : 1.116  0.349  0.802

実行環境 : Python 3.8.10, Ubuntu 20.04 LTS (WSL1), Intel Core i5-6200U 2.30GHz

ソート済みデータの場合、データの挿入は RB 木よりも RB23 木の方が速くなりました。これらの結果を見ると、2-3 木をベースにした赤黒木でも十分な性能が得られることがわかります。

なお、これらの結果は M.Hiroi のコーディング、実行したマシン、プログラミング言語などの環境に大きく依存しています。また、これらの環境だけではなく、データの種類によっても実行時間はかなり左右されます。興味のある方は、いろいろなデータをご自分の環境で試してみてください。

●参考 URL

  1. Robert Sedgewick, "Left-Leaning Red Black Trees" (PDF)

●プログラムリスト

#
# rb23node.py : 赤黒木用操作関数 (2-3 木をベース)
#
#               Copyright (C) 2009-2022 Makoto Hiroi
#

# 終端
null = None

# 色
BLACK = 0
RED = 1

# 節
class Node:
    def __init__(self, x, color = RED):
        self.data = x
        self.color = color
        self.left = null
        self.right = null

# 終端の設定
def make_null():
    global null
    if null is None:
        null = Node(None, BLACK)
        null.left = None
        null.right = None
    return null

# 右回転
def rotate_right(node):
    lnode = node.left
    node.left = lnode.right
    lnode.right = node
    lnode.color = node.color
    node.color = RED
    return lnode

# 左回転
def rotate_left(node):
    rnode = node.right
    node.right = rnode.left
    rnode.left = node
    rnode.color = node.color
    node.color = RED
    return rnode

#
# データの探索
#
def search(node, x):
    while node is not null:
        if x == node.data: return True
        elif x < node.data:
            node = node.left
        else:
            node = node.right
    return False

#
# データの挿入
#

# 4node の分割
def split(node):
    node.color = RED
    node.left.color = BLACK
    node.right.color = BLACK

# 左部分木の修正
def balance_insert_left(node, flag):
    if flag: return node, flag
    if node.color == BLACK:
        if node.right.color == RED:
            # 4 node なので分割
            split(node)
        else:
            if node.left.right.color == RED:
                node.left = rotate_left(node.left)
            if node.left.left.color == RED:
                # 赤が 2 つ続く
                node = rotate_right(node)
                # 4 node になるので分割
                split(node)
            else:
                # 赤一つならば終了
                flag = True
    return node, flag

# 右部分木の修正
def balance_insert_right(node, flag):
    if flag: return node, flag
    if node.color == BLACK:
        if node.left.color == RED:
            # 4 node なので分割
            split(node)
        else:
            if node.right.left.color == RED:
                node.right = rotate_right(node.right)
            if node.right.right.color == RED:
                # 赤が 2 つ続く
                node = rotate_left(node)
                # 4node になるので分割
                split(node)
            else:
                # 赤一つならば終了
                flag = True
    return node, flag

# データの挿入
def insert(node, x):
    if node is null: return Node(x), False
    if x < node.data:
        node.left, flag = insert(node.left, x)
        return balance_insert_left(node, flag)
    elif x > node.data:
        node.right, flag = insert(node.right, x)
        return balance_insert_right(node, flag)
    return node, True

# 簡易バージョン (遅い)
def insert1(node, x):
    if node is null: return Node(x)
    if x < node.data:
        node.left = insert1(node.left, x)
    elif x > node.data:
        node.right = insert1(node.right, x)
    # バランスの修正
    if node.left.color == RED:
        if node.left.right.color == RED:
            node.left = rotate_left(node.left)
        if node.left.left.color == RED:
            node = rotate_right(node)
    if node.right.color == RED:
        if node.right.left.color == RED:
            node.right = rotate_right(node.right)
        if node.right.right.color == RED:
            node = rotate_left(node)
    if node.left.color == RED and node.right.color == RED:
        split(node)
    return node

#
# データの削除
#

# 右部分木の修正
def balance_right(node, flag):
    if flag: return node, flag
    if node.left.left.color == BLACK and node.left.right.color == BLACK:
        if node.left.color == BLACK:
            node.left.color = RED
            if node.color == BLACK: return node, False
            node.color = BLACK
        else:
            node = rotate_right(node)
            node.right, _ = balance_right(node.right, False)
    else:
        if node.left.right.color == RED:
            node.left = rotate_left(node.left)
        node = rotate_right(node)
        node.right.color = BLACK
        node.left.color = BLACK
    return node, True

# 左部分木の修正
def balance_left(node, flag):
    if flag: return node, flag
    if node.right.left.color == BLACK and node.right.right.color == BLACK:
        if node.right.color == BLACK:
            node.right.color = RED
            if node.color == BLACK: return node, False
            node.color = BLACK
        else:
            node = rotate_left(node)
            node.left, _ = balance_left(node.left, False)
    else:
        if node.right.left.color == RED:
            node.right = rotate_right(node.right)
        node = rotate_left(node)
        node.left.color = BLACK
        node.right.color = BLACK
    return node, True

# 最小値を探す
def search_min(node):
    while node.left is not null: node = node.left
    return node.data

# 削除
def delete(node, x):
    if node is null: return node, True
    if x == node.data:
        if node.left is null and node.right is null:
            return null, node.color == RED
        elif node.right is null:
            node.left.color = BLACK
            return node.left, True
        elif node.left is null:
            node.right.color = BLACK
            return node.right, True
        else:
            node.data = search_min(node.right)
            node.right, flag = delete(node.right, node.data)
            return balance_right(node, flag)
    elif x < node.data:
        node.left, flag = delete(node.left, x)
        return balance_left(node, flag)
    else:
        node.right, flag = delete(node.right, x)
        return balance_right(node, flag)

#
# 巡回
#
def traverse(node):
    if node is not null:
        for x in traverse(node.left):
            yield x
        yield node.data
        for x in traverse(node.right):
            yield x

# 木の表示
def print_node(node, n):
    color = ('B', 'R')
    if node is not null:
        print_node(node.left, n + 1)
        print('    ' * n, color[node.color], node.data)
        print_node(node.right, n + 1)

# 赤黒木の条件を満たしているか
def check_rb23tree(node):
    if node is not null:
        if node.color == RED:
            if node.left.color == RED or node.right.color == RED:
                raise 'rb23 error1'
        else:
            if node.left.color == RED and node.right.color == RED:
                raise 'rb23 error2'
        a = check_rb23tree(node.left)
        b = check_rb23tree(node.right)
        if a != b: raise 'rb23 error3'
        if node.color == BLACK: a += 1
        return a
    return 0

# test
if __name__ == '__main__':
    import random, time
    root = make_null()
    buff = list(range(1000))
    random.shuffle(buff)
    print('insert test')
    for x in buff:
        root = insert(root, x)
        root.color = BLACK
        check_rb23tree(root)
    print('search test')
    random.shuffle(buff)
    for x in buff:
        if not search(root, x):
            raise 'search error'
    print('delete test')
    for x in buff:
        root, _ = delete(root, x)
        root.color = BLACK
        check_rb23tree(root)

初版 2009 年 1 月 10 日
改訂 2022 年 9 月 10 日

Copyright (C) 2009-2022 Makoto Hiroi
All rights reserved.

[ PrevPage | Python | NextPage ]