TreeMap データ構造とソース コード分析 ダークホースを追跡する

TP-LINK へのインタビューを終えて、TreeMap についての理解を怠っていたように感じましたので、ここで TreeMap の知識を学びます。元のビデオは次のとおりです: Java チュートリアル Advanced-TreeMap データ構造とソース コード
分析

1. ツリーマップの特徴

  • コンセプト:

    TreeMap は 2 列のコレクションであり、Map のサブクラスです。最下層は赤黒のツリー構造で構成されます。

  • 特徴:

    • 要素キーを繰り返すことはできません
    • 要素はサイズ順に並べ替えられます

例は次のとおりです。
要素は繰り返すことができず、繰り返すと上書きされます。

public class Demo {
    
    
    @Test
    public void test() {
    
    
        // 创建对象
        TreeMap<Integer, String> map = new TreeMap<>();
        // 添加元素
        map.put(1, "abc");
        map.put(1, "def");
        map.put(1, "ghi");
        System.out.println(map);
    }
}

出力結果:

{
    
    1=ghi}

キーが取り出される順序は、キーが入力される順序とは関係がなく、デフォルトで小さいキーから大きいキーの順にソートされます。

@Test
    public void test() {
    
    
        // 创建对象
        TreeMap<Integer, String> map = new TreeMap<>();
        // 添加元素
        map.put(9, "abc");
        map.put(2, "def");
        map.put(1, "ghi");
        System.out.println(map);
    }

出力結果:

{
    
    1=ghi, 2=def, 9=abc}

2. TreeMapのデータ構造

TreeMap の最下層は赤黒ツリーで構成されており、赤黒ツリーは特殊な二分探索ツリーです。

一般的なツリー構造:
ここに画像の説明を挿入

2.1 二分探索木

以下の性質を満たす木を二分探索木と呼びます。

2.1.1 二分探索木の定義

  • 特徴:

1. 左のサブツリーが空でない場合、左のサブツリー上のすべてのノードの値はそのルート ノードの値より小さいです;
2. 右のサブツリーが空でない場合、そのルート ノードのすべてのノードの値は、右のサブツリーがルート ノードの値より大きい;
3. 左と右のサブツリーもバイナリ ソート ツリーである;
4. 等しいノードがない;

  • 結論は:

    二分探索木とは、各ノードの値を大きさに応じて並べた二分木で、ノードの値を探索するのに便利です。

  • 写真:
    ここに画像の説明を挿入

2.1.2 二分探索木の探索動作

  • 見つけ方:

ルートノードから開始して、検索するデータがノードの値と等しい場合に戻ります。
検索するデータがノードの値より小さい場合は、左側のサブツリーを再帰的に検索します。
検索するデータがノードの値より大きい場合は、右側のサブツリーを再帰的に検索します。

  • 写真:
    ここに画像の説明を挿入

2.2 バランスの取れた二分木

2.2.1 バランス二分木の定義

「ひだ」現象を回避し、ツリーの高さを低くし、検索効率を向上させるために、別のツリー構造があります。それが「バランス二分木」です。

左右のサブツリー間の高さの差の絶対値は 1 を超えず、左右のサブツリーは両方ともバランスの取れた二分木です。
ここに画像の説明を挿入

2.2.2 平衡二分木の回転

  • コンセプト:

バランスの取れた二分木を構築する過程で、新しいノードを挿入する場合、挿入によってツリーのバランスが崩れていないか確認し、崩れている場合は回転を行ってツリーの構造を変更する必要があります。 。

  • 2 つの回転方法:

    • 左手:

      左ローテーションは、ノードの右ブランチを左に引っ張り、右の子ノードが親ノードになり、昇格後の冗長な左の子ノードを降格されたノードの右の子ノードに転送します。
      ここに画像の説明を挿入

    • 右回転:

      ノードの左ブランチを右に引っ張り、左の子ノードが親ノードとなり、昇格後の冗長な右の子ノードを降格したノードの左の子ノードに転送します。
      ここに画像の説明を挿入

  • 4 つの不均衡状況:

    • 左右の場合、右回転の基準ノードとして10を使用する必要があります
      ここに画像の説明を挿入

    • 左右の場合は、まず7を基準ノードとして左回転し、次に11を基準ノードとして右回転します
      ここに画像の説明を挿入

    • 右と左の場合、最初に 15 を参照ノードとして使用して右回転を実行し、次に 11 を参照ノードとして使用して左回転を実行します
      ここに画像の説明を挿入

    • 左右の場合は未参照ノード11個で左回転
      ここに画像の説明を挿入

2.3 赤黒の木

2.3.1 赤黒木の定義

  • 概要:

    赤黒ツリーは、自己平衡型二分探索ツリーです。

    赤黒ツリーの各ノードには、ノードの色 (赤または黒) を示す記憶ビットがあります。

    赤黒の木はバランスが高いわけではなく、そのバランスは「赤黒の木の特性」によって実現されます。

  • 赤黒木の特徴:

    1. 各ノードは赤または黒のいずれかです。
    2. ルート ノードは黒でなければなりません。
    3. 各葉ノードは黒です(葉ノードはNil)
    4. ノードが赤の場合、その子ノードは黒である必要があります (2 つの赤いノードを接続することはできません)。
    5. 各ノードについて、そのノードからそのすべての子孫リーフ ノードへの単純なパスには、同じ数の黒いノードが含まれます。
    6. 赤黒ツリーの左右のサブツリー間の深さの差は short 値の 2 倍を超えることはできません (たとえば、左のサブツリーの深さは 2 で、右のサブツリーの深さは 4 であり、これは矛盾します)。
  • 写真:
    ここに画像の説明を挿入

2. TreeMapのソースコード解析

2.1 get() メソッドの分析

    @Test
    public void test() {
    
    
        // 创建对象
        TreeMap<Integer, String> map = new TreeMap<>();
        // 添加元素
        map.put(9, "abc");
        map.put(2, "def");
        map.put(1, "ghi");

        String s = map.get(2);
        System.out.println(s);
    }

出力結果:

def

get() メソッドのソースコードを見てみましょう。

public V get(Object key) {
    
    
	//调用方法根据键获取Entry对象
    Entry<K,V> p = getEntry(key);
    //判断对象如果是null返回null,如果不是null返回对象中的值
    return (p==null ? null : p.value);
}

このうち、Entry<K,V> とは何かについては、引き続きソース コードを見てください。

//Entry类型表示结点
static final class Entry<K,V> implements Map.Entry<K,V> {
    
    
    K key;					//key表示键
    V value;				//value表示值
    Entry<K,V> left;		//left表示左子结点的地址
    Entry<K,V> right;		//rigth表示右子结点的地址
    Entry<K,V> parent;		//parent表示父结点的地址
    boolean color = BLACK;  //color表示结点的颜色
    
    //下面方法省略…………
}

具体的な getEntry() メソッドは次のとおりです。
TreeMap のキーを null にすることはできません

final Entry<K,V> getEntry(Object key) {
    
    
        //判断有没有传入comparator
        if (comparator != null)
            //调用方法,使用比较器做查询
            return getEntryUsingComparator(key);
    	//判断传入的键是否为null
        if (key == null)
            //如果要查询的键是null则抛出空指针异常
            throw new NullPointerException();
        @SuppressWarnings("unchecked")
    	//把Object类型的键向下转型为Comparable
        Comparable<? super K> k = (Comparable<? super K>) key;
    	//先把二叉树的根结点赋值给p
        Entry<K,V> p = root;
    	//如果p不为null,一直循环比较
        while (p != null) {
    
    
            //调用Comparable的compareTo()方法进行比较
            int cmp = k.compareTo(p.key);
            //如果cmp小于0,表示要查找的键小于结点的数字
            if (cmp < 0)
                //把p左子结点赋值给p对象
                p = p.left;
            //如果cmp大于0,表示要查找的键大于结点的数字
            else if (cmp > 0)
                //把P右子结点赋值给p对象
                p = p.right;
            else
                //要查找的键等于结点的值,就把当前Entry对象直接返回
                return p;
        }
    	//已经找到叶子结点,没有找到要查找的数字返回null
        return null;
    }

コンパレータを渡した後、コンパレータを介してクエリを実行します

//传入比较器的情况下
final Entry<K,V> getEntryUsingComparator(Object key) {
    
    
        @SuppressWarnings("unchecked")
    		//把Object类型的Key向下转型为对应的键的类型
            K k = (K) key;
    	//给比较器对象起名字cpr
        Comparator<? super K> cpr = comparator;
        if (cpr != null) {
    
    
            //把二叉树的根结点赋值给P对象
            Entry<K,V> p = root;
            //循环用要查找的数字和结点中的数字进行比较
            while (p != null) {
    
    
                //调用比较器的compare()
                int cmp = cpr.compare(k, p.key);
                if (cmp < 0)
                    p = p.left;
                else if (cmp > 0)
                    p = p.right;
                else
                    return p;
            }
        }
        return null;
    }

2.2 put() メソッドの分析

public V put(K key, V value) {
    
    
    //获取根结点赋值给变量t
    Entry<K,V> t = root;
    //判断根结点是否为null
    if (t == null) {
    
    
        //对key进行非空和类型校验
        compare(key, key);
		//新建一个结点
        root = new Entry<>(key, value, null);
        //设置集合长度为1
        size = 1;
        //记录集合被修改的次数
        modCount++;
		//添加成功返回null
        return null;
    }

ここで、compare メソッドの詳細を見てみましょう。put
キーが null の場合、null ポインタ例外がスローされます。キーが comparator コンパレータまたは Comparable インターフェイスを実装していない場合、null ポインタ例外もスローされます。投げられた。

// 非空和类型校验
    final int compare(Object k1, Object k2) {
    
    
        return comparator==null ? ((Comparable<? super K>)k1).compareTo((K)k2)
            : comparator.compare((K)k1, (K)k2);
    }

次に、put() メソッドの後半を見てください。

   	//如果根结点不是null则执行下面代码
    int cmp;
    Entry<K,V> parent;
    
    //把比较器对象赋值给变量cpr
    Comparator<? super K> cpr = comparator;
    //判断比较器对象如果不是空则执行下面代码
    if (cpr != null) {
    
    
        do {
    
    
            //把当前结点赋值给变量parent
            parent = t;
            //比较当前结点的键和要存储的键的大小
            cmp = cpr.compare(key, t.key);
            //如果要存储的键小于当前结点,则继续和左边的结点进行比较
            if (cmp < 0)
                t = t.left;
            //如果要存储的键大于当前结点,则继续和右边的结点进行比较
            else if (cmp > 0)
                t = t.right;
            else
                //如果要存储的键等于当前结点的键,则调用setValue()方法设置新的值
                //并结束循环
                return t.setValue(value);
          //循环直到遍历到叶子结点结束为止
        } while (t != null);
    }
    //如果比较器对象是空则执行下面代码
    else {
    
    
        //如果要保存的键为空,抛出空指针异常
        if (key == null)
            throw new NullPointerException();
        @SuppressWarnings("unchecked")
        	//把键转型为Comparable类型
            Comparable<? super K> k = (Comparable<? super K>) key;
        do {
    
    
            //把当前结点赋值给变量parent
            parent = t;
            //比较要存储的键和当前结点的键
            cmp = k.compareTo(t.key);
            //如果要存储的键小于当前结点,则继续和左边的结点比较
            if (cmp < 0)
                t = t.left;
            //如果要存储的键大于当前结点,则继续和右边的结点比较
            else if (cmp > 0)
                t = t.right;
            else
                //如果要存储的键等于当前结点的键,则调用setValue()方法设置新的值
                //并结束循环
                return t.setValue(value);
           //循环直到遍历到叶子结点结束为止
        } while (t != null);
    }
    //遍历结束如果没有找到相同的键,则执行下面代码
    //创建新的结点对象,保存键值对,设置父结点
    Entry<K,V> e = new Entry<>(key, value, parent);
    //如果新的键小于父结点的键,则保存在左边
    if (cmp < 0)
        parent.left = e;
    else
        //如果新的键大于父结点的键,则保存在右边
        parent.right = e;
    //维持红黑树的平衡
    fixAfterInsertion(e);
    //集合长度加一
    size++;
    //集合修改次数加一
    modCount++;
    //返回被覆盖的值是null
    return null;
}

3. カスタム TreeMap コレクション

バイナリ ツリーを使用して TreeMap コレクションを実装し、put()、get()、remove() などの主要なメソッドを記述します。

package com.exercise;

import java.util.Comparator;

/**
 * 自定义一个TreeMap
 *
 * @author wty
 * @date 2023/6/23 11:31
 */
public class MyTreeMap<K, V> {
    
    
    // 自定义一个内部类
    private class Entry<K, V> {
    
    
        // 键
        K key;
        // 值
        V value;
        // 左子结点
        Entry<K, V> left;
        // 右子结点
        Entry<K, V> right;
        // 父结点
        Entry<K, V> parent;

        //有参构造器
        public Entry(K key, V value, Entry<K, V> left, Entry<K, V> right, Entry<K, V> parent) {
    
    
            this.key = key;
            this.value = value;
            this.left = left;
            this.right = right;
            this.parent = parent;
        }
    }

    // 定义一个比较器
    private final Comparator<? super K> comparator;

    // 无参构造给comparator赋值
    public MyTreeMap() {
    
    
        comparator = null;
    }


    // 有参构造给comparator赋值
    public MyTreeMap(Comparator<? super K> comparator) {
    
    
        this.comparator = comparator;
    }

    // 根结点
    private Entry<K, V> root;

    // 定义集合的长度
    private int size;

    /**
     * @param
     * @return int
     * @description //获取长度
     * @date 2023/6/23 19:09
     * @author wty
     **/
    public int size() {
    
    
        return size;
    }

    /**
     * @param
     * @return V
     * @description //根据键获取值
     * @param: k
     * @date 2023/6/23 19:10
     * @author wty
     **/
    public V get(K key) {
    
    
        Entry<K, V> entry = getEntry(key);
        return null == entry ? null : entry.value;
    }

    /**
     * @param
     * @return com.exercise.MyTreeMap<K, V>.Entry<K,V>
     * @description //根据键获取值(通用方法)
     * @param: key
     * @date 2023/6/23 19:11
     * @author wty
     **/
    private Entry<K, V> getEntry(Object key) {
    
    
        // 非空校验
        if (null == key) {
    
    
            throw new NullPointerException();
        }

        // 给跟结点起一个名字
        Entry<K, V> t = root;

        // 判断有没有传入比较器
        // 传入了比较器
        if (null != comparator) {
    
    
            // 向下转型
            K k = (K) key;

            // 循环
            while (null != t) {
    
    
                int cmp = comparator.compare(k, t.key);

                if (cmp < 0) {
    
    
                    t = t.left;
                } else if (cmp > 0) {
    
    
                    t = t.right;
                } else {
    
    
                    return t;
                }
            }
        } else {
    
    
            // 没有传入比较器
            Comparable<K> k = (Comparable<K>) key;

            while (null != t) {
    
    
                int cmp = k.compareTo(t.key);

                if (cmp > 0) {
    
    
                    t = t.right;
                } else if (cmp < 0) {
    
    
                    t = t.left;

                } else {
    
    
                    return t;
                }
            }
        }
        // 如果找不到,就返回null
        return null;
    }

    /**
     * @param
     * @return java.lang.String
     * @description //添加元素
     * @param: key
     * @param: value
     * @date 2023/6/23 19:19
     * @author wty
     **/
    public V put(K key, V value) {
    
    
        //给根结点赋值
        Entry<K, V> t = root;

        // 非空校验
        if (null == key) {
    
    
            throw new NullPointerException();
        }

        // 判断集合是否为空
        if (null == t) {
    
    
            // 创建一个新的结点
            Entry<K, V> entry = new Entry<>(key, value, null, null, null);

            // 给根结点赋值
            root = entry;

            // 集合长度+1
            size++;
            return null;
        }

        // 创建键值对,表示新增结点的父结点
        Entry<K, V> parent = t;
        int cmp = 0;

        // 判断是否有比较器
        // 有比较器
        if (null != comparator) {
    
    
            while (null != t) {
    
    
                parent = t;
                //判断键
                cmp = comparator.compare(key, t.key);

                if (cmp > 0) {
    
    
                    t = t.right;
                } else if (cmp < 0) {
    
    
                    t = t.left;

                } else {
    
    
                    // 用新的值替换旧的值,把旧的值替换掉
                    V v = t.value;
                    t.value = value;
                    return v;
                }
            }
        } else {
    
    
            // 没有比较器
            Comparable<? super K> k = (Comparable<? super K>) key;
            while (null != t) {
    
    
                parent = t;
                cmp = k.compareTo(t.key);
                if (cmp > 0) {
    
    
                    t = t.right;
                } else if (cmp < 0) {
    
    
                    t = t.left;

                } else {
    
    
                    // 用新的值替换旧的值,把旧的值替换掉
                    V v = t.value;
                    t.value = value;
                    return v;
                }
            }

        }
        // 要添加的键值对 键不重复
        Entry<K, V> entry = new Entry<>(key, value, null, null, parent);
        if (cmp > 0) {
    
    
            parent.right = entry;
        } else {
    
    
            parent.left = entry;
        }

        // 集合长度增加
        size++;
        return null;
    }

    /**
     * @param
     * @return V
     * @description //移除元素
     * @param: key
     * @date 2023/6/23 19:30
     * @author wty
     **/
    public V remove(K key) {
    
    
        Entry<K, V> entry = getEntry(key);

        if (null == entry) {
    
    
            return null;
        }

        // 删除操作


        // 1.删除中间结点
        // 1.1没有左子树,只有右子树
        if (entry.left == null && entry.right != null) {
    
    
            // 判断要删除的结点是父结点的右子结点
            if (entry.parent.right == entry) {
    
    
                entry.parent.right = entry.right;
            } else if (entry.parent.left == entry) {
    
    
                entry.parent.left = entry.right;
            } else {
    
    
                root = entry.right;
            }

            // 让被删除结点的子结点,指向父结点
            entry.right.parent = entry.parent;
        }
        // 1.2没有右子树,只有左子树
        else if (entry.right == null && entry.left != null) {
    
    
            // 判断要删除的结点是父结点的右子结点
            if (entry.parent.right == entry) {
    
    
                entry.parent.right = entry.left;
            } else if (entry.parent.left == entry) {
    
    
                entry.parent.left = entry.left;
            } else {
    
    
                root = entry.left;
            }

            // 让被删除结点的子结点,指向父结点
            entry.left.parent = entry.parent;
        }

        // 2.删除根结点 既有右子树,又有左子树
        else if (entry.right != null && entry.left != null) {
    
    

            //找到后继结点
            Entry<K, V> target = entry.right;
            // 用右子树的最左子结点去替换
            while (target.left != null) {
    
    
                target = target.left;
            }

            // 右子结点作为后继结点
            if (entry.right == target) {
    
    
                target.parent = entry.parent;

                if (entry == root) {
    
    
                    root = target;
                } else if (entry.parent.right == entry) {
    
    
                    entry.parent.right = target;
                } else if (entry.parent.left == entry) {
    
    
                    entry.parent.left = target;
                }

                // 被删除结点左子结点重新指向新的父结点
                entry.left.parent = target;
                target.left = entry.left;
            } else {
    
    
                // 右子树的最左子结点作为后继结点
                if (target.right == null) {
    
    
                    // 后继结点没有子结点
                    target.parent.left = null;

                } else {
    
    
                    // 后继结点有子结点
                    target.parent.left = target.right;
                    target.right = target.parent;
                }

                // 让后继结点替换掉被删除结点
                if (entry == root) {
    
    
                    root = target;
                } else if (entry.parent.right == entry) {
    
    
                    entry.parent.right = target;
                } else if (entry.parent.left == entry) {
    
    
                    entry.parent.left = target;
                }

                // 被删除结点左右子树需要指向后继结点
                entry.left.parent = target;
                entry.right.parent = target;
                target.left = entry.left;
                target.right = entry.right;
            }


        } else {
    
    
            // 3.删除叶子结点
            if (entry.parent.right == entry) {
    
    
                entry.parent.right = null;
            } else if (entry.parent.left == entry) {
    
    
                entry.parent.left = null;
            } else {
    
    
                root = null;
            }

        }

        // 给集合长度减少1
        size--;

        return entry.value;
    }


    /**
     * @param
     * @return java.lang.String
     * @description //打印树的结构
     * @date 2023/6/23 19:55
     * @author wty
     **/
    @Override
    public String toString() {
    
    
        // 非空检验
        if (null == root) {
    
    
            return "{}";
        }

        String s = "{";
        String s1 = methodToString(root);
        s = s + s1.substring(0, s1.length() - 2) + "}";
        return s;
    }

    /**
     * @param
     * @return java.lang.String
     * @description //打印树的结构(递归调用)
     * @param: entry
     * @date 2023/6/23 19:55
     * @author wty
     **/
    private String methodToString(Entry<K, V> entry) {
    
    
        String s = "";

        // 拼接左子树
        if (entry.left != null) {
    
    
            s += methodToString(entry.left);
        }

        // 拼接中间结点
        s += entry.key + "=" + entry.value + ", ";

        // 拼接右子树
        if (entry.right != null) {
    
    
            s += methodToString(entry.right);
        }

        return s;
    }
}


  • テストクラス
@Test
    public void test() {
    
    
        MyTreeMap<Integer, String> treeMap = new MyTreeMap<>();

        treeMap.put(5, "abc");
        treeMap.put(3, "def");
        treeMap.put(6, "ghi");
        treeMap.put(1, "jkl");
        treeMap.put(4, "mno");

        System.out.println(treeMap);
    }

操作結果:

{
    
    1=jkl, 3=def, 4=mno, 5=abc, 6=ghi}

テストを続行して取得

@Test
    public void test() {
    
    
        MyTreeMap<Integer, String> treeMap = new MyTreeMap<>();

        treeMap.put(5, "abc");
        treeMap.put(3, "def");
        treeMap.put(6, "ghi");
        treeMap.put(1, "jkl");
        treeMap.put(4, "mno");

        System.out.println(treeMap);
        System.out.println(treeMap.get(3));
    }

操作結果:

{
    
    1=jkl, 3=def, 4=mno, 5=abc, 6=ghi}
def

最後に、delete 削除メソッドをテストします。

@Test
    public void test() {
    
    
        MyTreeMap<Integer, String> treeMap = new MyTreeMap<>();

        treeMap.put(5, "abc");
        treeMap.put(3, "def");
        treeMap.put(6, "ghi");
        treeMap.put(1, "jkl");
        treeMap.put(4, "mno");

        System.out.println(treeMap);
        System.out.println(treeMap.get(3));

        treeMap.remove(4);
        System.out.println(treeMap);
    }

操作結果:

{
    
    1=jkl, 3=def, 4=mno, 5=abc, 6=ghi}
def
{
    
    1=jkl, 3=def, 5=abc, 6=ghi}

おすすめ

転載: blog.csdn.net/sinat_38316216/article/details/131348202