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 赤黒木の定義
-
概要:
赤黒ツリーは、自己平衡型二分探索ツリーです。
赤黒ツリーの各ノードには、ノードの色 (赤または黒) を示す記憶ビットがあります。
赤黒の木はバランスが高いわけではなく、そのバランスは「赤黒の木の特性」によって実現されます。
-
赤黒木の特徴:
- 各ノードは赤または黒のいずれかです。
- ルート ノードは黒でなければなりません。
- 各葉ノードは黒です(葉ノードはNil)
- ノードが赤の場合、その子ノードは黒である必要があります (2 つの赤いノードを接続することはできません)。
- 各ノードについて、そのノードからそのすべての子孫リーフ ノードへの単純なパスには、同じ数の黒いノードが含まれます。
- 赤黒ツリーの左右のサブツリー間の深さの差は 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}