Estructura de datos de TreeMap y análisis de código fuente Siga al caballo oscuro

Después de la entrevista con TP-LINK, siento que he descuidado mi comprensión de TreeMap. Aquí aprenderé el conocimiento de TreeMap. El video original es el siguiente:
java tutorial advanced-TreeMap estructura de datos y análisis de código fuente

1. Características de TreeMap

  • concepto:

    TreeMap es una colección de dos columnas, una subclase de Map. La capa inferior consiste en una estructura de árbol rojo-negro.

  • Características:

    • Las claves de los elementos no se pueden repetir
    • Los elementos se ordenan por tamaño.

Los ejemplos son los siguientes:
Los elementos no se pueden repetir y la repetición se sobrescribirá.

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);
    }
}

resultado de salida:

{
    
    1=ghi}

El orden en que se extraen las llaves no tiene nada que ver con el orden en que se colocan y se ordenarán de forma predeterminada de llaves pequeñas a llaves grandes.

@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);
    }

resultado de salida:

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

2. La estructura de datos de TreeMap

La capa inferior de TreeMap se compone de un árbol rojo-negro, y el árbol rojo-negro es un árbol de búsqueda binaria especial.

Estructuras de árbol comunes:
inserte la descripción de la imagen aquí

2.1 Árbol de búsqueda binario

Un árbol que cumple con las siguientes características se denomina árbol de búsqueda binaria

2.1.1 Definición de árbol de búsqueda binario

  • Características:

1. Si el subárbol izquierdo no está vacío, los valores de todos los nodos del subárbol izquierdo son menores que el valor de su nodo raíz
2. Si el subárbol derecho no está vacío, los valores de todos los nodos del subárbol derecho son mayores que el valor de su nodo raíz
3. Los subárboles izquierdo y derecho también son árboles de clasificación binaria
4. No hay nodos iguales;

  • en conclusión:

    Un árbol de búsqueda binaria es un árbol binario en el que los valores de cada nodo se ordenan según su tamaño.El árbol de búsqueda binaria es conveniente para buscar los valores de los nodos.

  • imagen:
    inserte la descripción de la imagen aquí

2.1.2 Operación de búsqueda del árbol de búsqueda binaria

  • Como encontrar:

Comenzando desde el nodo raíz, si los datos a encontrar son iguales al valor del nodo, entonces regrese.
Si los datos que se van a encontrar son menores que el valor del nodo, busque recursivamente en el subárbol izquierdo;
si los datos que se van a encontrar son mayores que el valor del nodo, entonces busque recursivamente en el subárbol derecho.

  • imagen:
    inserte la descripción de la imagen aquí

2.2 Árbol binario equilibrado

2.2.1 Definición de árbol binario equilibrado

Para evitar el fenómeno de los "engarces", reducir la altura del árbol y mejorar nuestra eficiencia de búsqueda, existe otra estructura de árbol: "árbol binario equilibrado"

El valor absoluto de la diferencia de altura entre sus subárboles izquierdo y derecho no supera 1, y los subárboles izquierdo y derecho son un árbol binario equilibrado.
inserte la descripción de la imagen aquí

2.2.2 Rotación del árbol binario balanceado

  • concepto:

En el proceso de construcción de un árbol binario balanceado, cuando haya un nuevo nodo para insertar, verifique si el balance del árbol está roto debido a la inserción, si es así, necesita hacer una rotación para cambiar la estructura del árbol. .

  • Dos métodos de rotación:

    • Mano izquierda:

      La rotación a la izquierda es tirar de la rama derecha del nodo hacia la izquierda, el nodo secundario derecho se convierte en el nodo principal y transferir el nodo secundario izquierdo redundante después de la promoción al nodo secundario derecho del nodo degradado;
      inserte la descripción de la imagen aquí

    • Rotación derecha:

      Tire de la rama izquierda del nodo hacia la derecha, el nodo secundario izquierdo se convierte en el nodo principal y transfiera el nodo secundario derecho redundante después de la promoción al nodo secundario izquierdo del nodo degradado
      inserte la descripción de la imagen aquí

  • Cuatro situaciones de desequilibrio:

    • En el caso de izquierda y derecha, es necesario utilizar 10 como nodo de referencia para la rotación a la derecha
      inserte la descripción de la imagen aquí

    • En el caso de izquierda y derecha, primero gire a la izquierda con 7 como nodo de referencia y luego gire a la derecha con 11 como nodo de referencia
      inserte la descripción de la imagen aquí

    • En el caso de derecha e izquierda, primero use 15 como nodo de referencia para realizar la rotación a la derecha y luego use 11 como nodo de referencia para realizar la rotación a la izquierda
      inserte la descripción de la imagen aquí

    • En el caso de la derecha y la derecha, gire a la izquierda con 11 nodos sin referencia
      inserte la descripción de la imagen aquí

2.3 Árbol rojo-negro

2.3.1 Definición de árbol rojo-negro

  • Descripción general:

    Un árbol rojo-negro es un árbol de búsqueda binaria autoequilibrado.

    Cada nodo del árbol rojo-negro tiene un bit de almacenamiento que indica el color del nodo, que puede ser rojo o negro.

    El árbol rojo-negro no está muy equilibrado, y su equilibrio se logra a través de las "características del árbol rojo-negro".

  • Características del árbol rojo-negro:

    1. Cada nodo es rojo o negro;
    2. El nodo raíz debe ser negro;
    3. Cada nodo de hoja es negro (los nodos de hoja son Nil)
    4. Si un nodo es rojo, sus nodos secundarios deben ser negros (no se pueden conectar dos nodos rojos)
    5. Para cada nodo, la ruta simple desde el nodo hasta todos sus nodos hoja descendientes contiene el mismo número de nodos negros;
    6. La diferencia de profundidad entre los subárboles izquierdo y derecho en el árbol rojo-negro no puede exceder el doble del valor corto (por ejemplo, la profundidad del subárbol izquierdo es 2 y la profundidad del subárbol derecho es 4, lo cual es inconsistente)
  • imagen:
    inserte la descripción de la imagen aquí

2. Análisis del código fuente de TreeMap

2.1 análisis del método 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);
    }

resultado de salida:

def

Veamos el código fuente del método get()

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

Entre ellos, ¿qué es Entry<K,V>? También podría seguir mirando el código fuente.

//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表示结点的颜色
    
    //下面方法省略…………
}

El método getEntry() específico es el siguiente: Se puede ver que
La clave en TreeMap no puede ser nula

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;
    }

Después de pasar el comparador, consulte a través del comparador

//传入比较器的情况下
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 Análisis del método 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;
    }

Echemos un vistazo a los detalles del método de comparación aquí.
Si la clave put es nula, se lanzará una excepción de puntero nulo, o si la clave no implementa el comparador comparador o la interfaz Comparable, también se generará una excepción de puntero nulo. arrojado

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

Luego mira la segunda mitad del método 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. Colección personalizada de TreeMap

Use un árbol binario para implementar una colección TreeMap y escriba métodos clave como put(), get() y 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;
    }
}


  • clase de prueba
@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);
    }

resultado de la operación:

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

seguir probando obtener

@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));
    }

resultado de la operación:

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

Finalmente, pruebe el método de eliminar eliminar

@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);
    }

resultado de la operación:

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

Supongo que te gusta

Origin blog.csdn.net/sinat_38316216/article/details/131348202
Recomendado
Clasificación