TreeMap 原理实现及常用方法

news2024/12/23 9:30:18
  • TreeMap概述

  • 红黑树回顾

  • TreeMap构造

  • put方法

  • get 方法

  • remove方法

  • 遍历

  • 总结

一. TreeMap概述

  • TreeMap存储K-V键值对,通过红黑树(R-B tree)实现;

  • TreeMap继承了NavigableMap接口,NavigableMap接口继承了SortedMap接口,可支持一系列的导航定位以及导航操作的方法,当然只是提供了接口,需要TreeMap自己去实现;

  • TreeMap实现了Cloneable接口,可被克隆,实现了Serializable接口,可序列化;

  • TreeMap因为是通过红黑树实现,红黑树结构天然支持排序,默认情况下通过Key值的自然顺序进行排序;

二. 红黑树回顾

因为TreeMap的存储结构是红黑树,我们回顾一下红黑树的特点以及基本操作,红黑树的原理可参考关于红黑树(R-B tree)原理:

https://www.cnblogs.com/LiaHon/p/11203229.html

下图为典型的红黑树:

红黑树规则特点:

  • 节点分为红色或者黑色;

  • 根节点必为黑色;

  • 叶子节点都为黑色,且为null;

  • 连接红色节点的两个子节点都为黑色(红黑树不会出现相邻的红色节点);

  • 从任意节点出发,到其每个叶子节点的路径中包含相同数量的黑色节点;

  • 新加入到红黑树的节点为红色节点;

红黑树自平衡基本操作:

  • 变色:在不违反上述红黑树规则特点情况下,将红黑树某个node节点颜色由红变黑,或者由黑变红;

  • 左旋:逆时针旋转两个节点,让一个节点被其右子节点取代,而该节点成为右子节点的左子节点

  • 右旋:顺时针旋转两个节点,让一个节点被其左子节点取代,而该节点成为左子节点的右子节点

三. TreeMap构造

我们先看一下TreeMap中主要的成员变量

/**
 * 我们前面提到TreeMap是可以自动排序的,默认情况下comparator为null,这个时候按照key的自然顺序进行排
 * 序,然而并不是所有情况下都可以直接使用key的自然顺序,有时候我们想让Map的自动排序按照我们自己的规则,
 * 这个时候你就需要传递Comparator的实现类
 */
private final Comparator<? super K> comparator;

/**
 * TreeMap的存储结构既然是红黑树,那么必然会有唯一的根节点。
 */
private transient Entry<K,V> root;

/**
 * Map中key-val对的数量,也即是红黑树中节点Entry的数量
 */
private transient int size = 0;

/**
 * 红黑树结构的调整次数
 */
private transient int modCount = 0;

上面的主要成员变量根节点root是Entry类的实体,我们来看一下Entry类的源码

static final class Entry<K,V> implements Map.Entry<K,V> {
    //key,val是存储的原始数据
    K key;
    V value;
    //定义了节点的左孩子
    Entry<K,V> left;
    //定义了节点的右孩子
    Entry<K,V> right;
    //通过该节点可以反过来往上找到自己的父亲
    Entry<K,V> parent;
    //默认情况下为黑色节点,可调整
    boolean color = BLACK;

    /**
     * 构造器
     */
    Entry(K key, V value, Entry<K,V> parent) {
        this.key = key;
        this.value = value;
        this.parent = parent;
    }

    /**
     * 获取节点的key值
     */
    public K getKey() {return key;}

    /**
     * 获取节点的value值
     */
    public V getValue() {return value;}

    /**
     * 用新值替换当前值,并返回当前值
     */
    public V setValue(V value) {
        V oldValue = this.value;
        this.value = value;
        return oldValue;
    }

    public boolean equals(Object o) {
        if (!(o instanceof Map.Entry))
            return false;
        Map.Entry<?,?> e = (Map.Entry<?,?>)o;
        return valEquals(key,e.getKey()) && valEquals(value,e.getValue());
    }

    public int hashCode() {
        int keyHash = (key==null ? 0 : key.hashCode());
        int valueHash = (value==null ? 0 : value.hashCode());
        return keyHash ^ valueHash;
    }

    public String toString() {
        return key + "=" + value;
    }
}

Entry静态内部类实现了Map的内部接口Entry,提供了红黑树存储结构的java实现,通过left属性可以建立左子树,通过right属性可以建立右子树,通过parent可以往上找到父节点。

大体的实现结构图如下:

TreeMap构造函数:

//默认构造函数,按照key的自然顺序排列
public TreeMap() {comparator = null;}
//传递Comparator具体实现,按照该实现规则进行排序
public TreeMap(Comparator<? super K> comparator) {this.comparator = comparator;}
//传递一个map实体构建TreeMap,按照默认规则排序
public TreeMap(Map<? extends K, ? extends V> m) {
    comparator = null;
    putAll(m);
}
//传递一个map实体构建TreeMap,按照传递的map的排序规则进行排序
public TreeMap(SortedMap<K, ? extends V> m) {
    comparator = m.comparator();
    try {
        buildFromSorted(m.size(), m.entrySet().iterator(), null, null);
    } catch (java.io.IOException cannotHappen) {
    } catch (ClassNotFoundException cannotHappen) {
    }
}

四. put方法

put方法为Map的核心方法,TreeMap的put方法大概流程如下:

我们来分析一下源码

public V put(K key, V value) {
    Entry<K,V> t = root;
    /**
     * 如果根节点都为null,还没建立起来红黑树,我们先new Entry并赋值给root把红黑树建立起来,这个时候红
     * 黑树中已经有一个节点了,同时修改操作+1。
     */
    if (t == null) {
        compare(key, key); 
        root = new Entry<>(key, value, null);
        size = 1;
        modCount++;
        return null;
    }
    /**
     * 如果节点不为null,定义一个cmp,这个变量用来进行二分查找时的比较;定义parent,是new Entry时必须
     * 要的参数
     */
    int cmp;
    Entry<K,V> parent;
    // cpr表示有无自己定义的排序规则,分两种情况遍历执行
    Comparator<? super K> cpr = comparator;
    if (cpr != null) {
        /**
         * 从root节点开始遍历,通过二分查找逐步向下找
         * 第一次循环:从根节点开始,这个时候parent就是根节点,然后通过自定义的排序算法
         * cpr.compare(key, t.key)比较传入的key和根节点的key值,如果传入的key<root.key,那么
         * 继续在root的左子树中找,从root的左孩子节点(root.left)开始:如果传入的key>root.key,
         * 那么继续在root的右子树中找,从root的右孩子节点(root.right)开始;如果恰好key==root.key,
         * 那么直接根据root节点的value值即可。
         * 后面的循环规则一样,当遍历到的当前节点作为起始节点,逐步往下找
         *
         * 需要注意的是:这里并没有对key是否为null进行判断,建议自己的实现Comparator时应该要考虑在内
         */
        do {
            parent = t;
            cmp = cpr.compare(key, t.key);
            if (cmp < 0)
                t = t.left;
            else if (cmp > 0)
                t = t.right;
            else
                return t.setValue(value);
        } while (t != null);
    }
    else {
        //从这里看出,当默认排序时,key值是不能为null的
        if (key == null)
            throw new NullPointerException();
        @SuppressWarnings("unchecked")
        Comparable<? super K> k = (Comparable<? super K>) key;
        //这里的实现逻辑和上面一样,都是通过二分查找,就不再多说了
        do {
            parent = t;
            cmp = k.compareTo(t.key);
            if (cmp < 0)
                t = t.left;
            else if (cmp > 0)
                t = t.right;
            else
                return t.setValue(value);
        } while (t != null);
    }
    /**
     * 能执行到这里,说明前面并没有找到相同的key,节点已经遍历到最后了,我们只需要new一个Entry放到
     * parent下面即可,但放到左子节点上还是右子节点上,就需要按照红黑树的规则来。
     */
    Entry<K,V> e = new Entry<>(key, value, parent);
    if (cmp < 0)
        parent.left = e;
    else
        parent.right = e;
    /**
     * 节点加进去了,并不算完,我们在前面红黑树原理章节提到过,一般情况下加入节点都会对红黑树的结构造成
     * 破坏,我们需要通过一些操作来进行自动平衡处置,如【变色】【左旋】【右旋】
     */
    fixAfterInsertion(e);
    size++;
    modCount++;
    return null;
}

put方法源码中通过fixAfterInsertion(e)方法来进行自平衡处理,我们回顾一下插入时自平衡调整的逻辑

接下来我们看一看这个方法

private void fixAfterInsertion(Entry<K,V> x) {
    //新插入的节点为红色节点
    x.color = RED;
    //我们知道父节点为黑色时,并不需要进行树结构调整,只有当父节点为红色时,才需要调整
    while (x != null && x != root && x.parent.color == RED) {
        //如果父节点是左节点,对应上表中情况1和情况2
        if (parentOf(x) == leftOf(parentOf(parentOf(x)))) {
            Entry<K,V> y = rightOf(parentOf(parentOf(x)));
            //如果叔父节点为红色,对应于“父节点和叔父节点都为红色”,此时通过变色即可实现平衡
            //此时父节点和叔父节点都设置为黑色,祖父节点设置为红色
            if (colorOf(y) == RED) {
                setColor(parentOf(x), BLACK);
                setColor(y, BLACK);
                setColor(parentOf(parentOf(x)), RED);
                x = parentOf(parentOf(x));
            } else {
                //如果插入节点是黑色,插入的是右子节点,通过【左右节点旋转】(这里先进行父节点左旋)
                if (x == rightOf(parentOf(x))) {
                    x = parentOf(x);
                    rotateLeft(x);
                }
                //设置父节点和祖父节点颜色
                setColor(parentOf(x), BLACK);
                setColor(parentOf(parentOf(x)), RED);
                //进行祖父节点右旋(这里【变色】和【旋转】并没有严格的先后顺序,达成目的就行)
                rotateRight(parentOf(parentOf(x)));
            }
        } else {
            //父节点是右节点的情况
            Entry<K,V> y = leftOf(parentOf(parentOf(x)));
            //对应于“父节点和叔父节点都为红色”,此时通过变色即可实现平衡
            if (colorOf(y) == RED) {
                setColor(parentOf(x), BLACK);
                setColor(y, BLACK);
                setColor(parentOf(parentOf(x)), RED);
                x = parentOf(parentOf(x));
            } else {
                //如果插入节点是黑色,插入的是左子节点,通过【右左节点旋转】(这里先进行父节点右旋)
                if (x == leftOf(parentOf(x))) {
                    x = parentOf(x);
                    rotateRight(x);
                }
                setColor(parentOf(x), BLACK);
                setColor(parentOf(parentOf(x)), RED);
                //进行祖父节点左旋(这里【变色】和【旋转】并没有严格的先后顺序,达成目的就行)
                rotateLeft(parentOf(parentOf(x)));
            }
        }
    }
    //根节点必须为黑色
    root.color = BLACK;
}

源码中通过 rotateLeft 进行【左旋】,通过 rotateRight 进行【右旋】。都非常类似,我们就看一下【左旋】的代码,【左旋】规则如下:“逆时针旋转两个节点,让一个节点被其右子节点取代,而该节点成为右子节点的左子节点”。

private void rotateLeft(Entry<K,V> p) {
    if (p != null) {
        /**
         * 断开当前节点p与其右子节点的关联,重新将节点p的右子节点的地址指向节点p的右子节点的左子节点
         * 这个时候节点r没有父节点
         */
        Entry<K,V> r = p.right;
        p.right = r.left;
        //将节点p作为节点r的父节点
        if (r.left != null)
            r.left.parent = p;
        //将节点p的父节点和r的父节点指向同一处
        r.parent = p.parent;
        //p的父节点为null,则将节点r设置为root
        if (p.parent == null)
            root = r;
        //如果节点p是左子节点,则将该左子节点替换为节点r
        else if (p.parent.left == p)
            p.parent.left = r;
        //如果节点p为右子节点,则将该右子节点替换为节点r
        else
            p.parent.right = r;
        //重新建立p与r的关系
        r.left = p;
        p.parent = r;
    }
}

就算是看了上面的注释还是并不清晰,看下图你就懂了

五. get 方法

get方法是通过二分查找的思想,我们看一下源码

public V get(Object key) {
    Entry<K,V> p = getEntry(key);
    return (p==null ? null : p.value);
}
/**
 * 从root节点开始遍历,通过二分查找逐步向下找
 * 第一次循环:从根节点开始,这个时候parent就是根节点,然后通过k.compareTo(p.key)比较传入的key和
 * 根节点的key值;
 * 如果传入的key<root.key, 那么继续在root的左子树中找,从root的左孩子节点(root.left)开始;
 * 如果传入的key>root.key, 那么继续在root的右子树中找,从root的右孩子节点(root.right)开始;
 * 如果恰好key==root.key,那么直接根据root节点的value值即可。
 * 后面的循环规则一样,当遍历到的当前节点作为起始节点,逐步往下找
 */
//默认排序情况下的查找
final Entry<K,V> getEntry(Object key) {
    
    if (comparator != null)
        return getEntryUsingComparator(key);
    if (key == null)
        throw new NullPointerException();
    @SuppressWarnings("unchecked")
    Comparable<? super K> k = (Comparable<? super K>) key;
    Entry<K,V> p = root;
    while (p != null) {
        int cmp = k.compareTo(p.key);
        if (cmp < 0)
            p = p.left;
        else if (cmp > 0)
            p = p.right;
        else
            return p;
    }
    return null;
}
/**
 * 从root节点开始遍历,通过二分查找逐步向下找
 * 第一次循环:从根节点开始,这个时候parent就是根节点,然后通过自定义的排序算法
 * cpr.compare(key, t.key)比较传入的key和根节点的key值,如果传入的key<root.key,那么
 * 继续在root的左子树中找,从root的左孩子节点(root.left)开始:如果传入的key>root.key,
 * 那么继续在root的右子树中找,从root的右孩子节点(root.right)开始;如果恰好key==root.key,
 * 那么直接根据root节点的value值即可。
 * 后面的循环规则一样,当遍历到的当前节点作为起始节点,逐步往下找
 */
//自定义排序规则下的查找
final Entry<K,V> getEntryUsingComparator(Object key) {
    @SuppressWarnings("unchecked")
    K k = (K) key;
    Comparator<? super K> cpr = comparator;
    if (cpr != null) {
        Entry<K,V> p = root;
        while (p != null) {
            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;
}

六. remove方法

remove方法可以分为两个步骤,先是找到这个节点,直接调用了上面介绍的getEntry(Object key),这个步骤我们就不说了,直接说第二个步骤,找到后的删除操作。往期:一百期面试题汇总

public V remove(Object key) {
    Entry<K,V> p = getEntry(key);
    if (p == null)
        return null;

    V oldValue = p.value;
    deleteEntry(p);
    return oldValue;
}

通过deleteEntry(p)进行删除操作,删除操作的原理我们在前面已经讲过

  • 删除的是根节点,则直接将根节点置为null;

  • 待删除节点的左右子节点都为null,删除时将该节点置为null;

  • 待删除节点的左右子节点有一个有值,则用有值的节点替换该节点即可;

  • 待删除节点的左右子节点都不为null,则找前驱或者后继,将前驱或者后继的值复制到该节点中,然后删除前驱或者后继(前驱:左子树中值最大的节点,后继:右子树中值最小的节点);

private void deleteEntry(Entry<K,V> p) {
    modCount++;
    size--;
 //当左右子节点都不为null时,通过successor(p)遍历红黑树找到前驱或者后继
    if (p.left != null && p.right != null) {
        Entry<K,V> s = successor(p);
        //将前驱或者后继的key和value复制到当前节点p中,然后删除节点s(通过将节点p引用指向s)
        p.key = s.key;
        p.value = s.value;
        p = s;
    } 
    Entry<K,V> replacement = (p.left != null ? p.left : p.right);
    /**
     * 至少有一个子节点不为null,直接用这个有值的节点替换掉当前节点,给replacement的parent属性赋值,给
     * parent节点的left属性和right属性赋值,同时要记住叶子节点必须为null,然后用fixAfterDeletion方法
     * 进行自平衡处理
     */
    if (replacement != null) {
        //将待删除节点的子节点挂到待删除节点的父节点上。
        replacement.parent = p.parent;
        if (p.parent == null)
            root = replacement;
        else if (p == p.parent.left)
            p.parent.left  = replacement;
        else
            p.parent.right = replacement;
        p.left = p.right = p.parent = null;
        /**
         * p如果是红色节点的话,那么其子节点replacement必然为红色的,并不影响红黑树的结构
         * 但如果p为黑色节点的话,那么其父节点以及子节点都可能是红色的,那么很明显可能会存在红色相连的情
         * 况,因此需要进行自平衡的调整
         */
        if (p.color == BLACK)
            fixAfterDeletion(replacement);
    } else if (p.parent == null) {//这种情况就不用多说了吧
        root = null;
    } else { 
        /**
         * 如果p节点为黑色,那么p节点删除后,就可能违背每个节点到其叶子节点路径上黑色节点数量一致的规则,
         * 因此需要进行自平衡的调整
         */ 
        if (p.color == BLACK)
            fixAfterDeletion(p);
        if (p.parent != null) {
            if (p == p.parent.left)
                p.parent.left = null;
            else if (p == p.parent.right)
                p.parent.right = null;
            p.parent = null;
        }
    }
}

操作的操作其实很简单,场景也不多,我们看一下删除后的自平衡操作方法fixAfterDeletion

private void fixAfterDeletion(Entry<K,V> x) {
    /**
     * 当x不是root节点且颜色为黑色时
     */
    while (x != root && colorOf(x) == BLACK) {
        /**
         * 首先分为两种情况,当前节点x是左节点或者当前节点x是右节点,这两种情况下面都是四种场景,这里通过
         * 代码分析一下x为左节点的情况,右节点可参考左节点理解,因为它们非常类似
         */
        if (x == leftOf(parentOf(x))) {
            Entry<K,V> sib = rightOf(parentOf(x));

            /**
             * 场景1:当x是左黑色节点,兄弟节点sib是红色节点
             * 兄弟节点由红转黑,父节点由黑转红,按父节点左旋,
             * 左旋后树的结构变化了,这时重新赋值sib,这个时候sib指向了x的兄弟节点
             */
            if (colorOf(sib) == RED) {
                setColor(sib, BLACK);
                setColor(parentOf(x), RED);
                rotateLeft(parentOf(x));
                sib = rightOf(parentOf(x));
            }

            /**
             * 场景2:节点x、x的兄弟节点sib、sib的左子节点和右子节点都为黑色时,需要将该节点sib由黑变
             * 红,同时将x指向当前x的父节点
             */
            if (colorOf(leftOf(sib))  == BLACK &&
                colorOf(rightOf(sib)) == BLACK) {
                setColor(sib, RED);
                x = parentOf(x);
            } else {
                /**
                 * 场景3:节点x、x的兄弟节点sib、sib的右子节点都为黑色,sib的左子节点为红色时,
                 * 需要将sib左子节点设置为黑色,sib节点设置为红色,同时按sib右旋,再将sib指向x的
                 * 兄弟节点
                 */
                if (colorOf(rightOf(sib)) == BLACK) {
                    setColor(leftOf(sib), BLACK);
                    setColor(sib, RED);
                    rotateRight(sib);
                    sib = rightOf(parentOf(x));
                }
                /**
                 * 场景4:节点x、x的兄弟节点sib都为黑色,而sib的左右子节点都为红色或者右子节点为红色、
                 * 左子节点为黑色,此时需要将sib节点的颜色设置成和x的父节点p相同的颜色,
                 * 设置x的父节点为黑色,设置sib右子节点为黑色,左旋x的父节点p,然后将x赋值为root
                 */
                setColor(sib, colorOf(parentOf(x)));
                setColor(parentOf(x), BLACK);
                setColor(rightOf(sib), BLACK);
                rotateLeft(parentOf(x));
                x = root;
            }
        } else {//x是右节点的情况
            Entry<K,V> sib = leftOf(parentOf(x));

            if (colorOf(sib) == RED) {
                setColor(sib, BLACK);
                setColor(parentOf(x), RED);
                rotateRight(parentOf(x));
                sib = leftOf(parentOf(x));
            }

            if (colorOf(rightOf(sib)) == BLACK &&
                colorOf(leftOf(sib)) == BLACK) {
                setColor(sib, RED);
                x = parentOf(x);
            } else {
                if (colorOf(leftOf(sib)) == BLACK) {
                    setColor(rightOf(sib), BLACK);
                    setColor(sib, RED);
                    rotateLeft(sib);
                    sib = leftOf(parentOf(x));
                }
                setColor(sib, colorOf(parentOf(x)));
                setColor(parentOf(x), BLACK);
                setColor(leftOf(sib), BLACK);
                rotateRight(parentOf(x));
                x = root;
            }
        }
    }

    setColor(x, BLACK);
}

当待操作节点为左节点时,上面描述了四种场景,而且场景之间可以相互转换,如deleteEntry后进入了场景1,经过场景1的一些列操作后,红黑树的结构并没有调整完成,而是进入了场景2,场景2执行完成后跳出循环,将待操作节点设置为黑色,完成。

我们下面用图来说明一下四种场景帮助理解,当然大家最好自己手动画一下。往期:一百期面试题汇总

场景1:

当x是左黑色节点,兄弟节点sib是红色节点,需要兄弟节点由红转黑,父节点由黑转红,按父节点左旋,左旋后树的结构变化了,这时重新赋值sib,这个时候sib指向了x的兄弟节点。

但经过这一系列操作后,并没有结束,而是可能到了场景2,或者场景3和4

场景2:

节点x、x的兄弟节点sib、sib的左子节点和右子节点都为黑色时,需要将该节点sib由黑变红,同时将x指向当前x的父节点

经过场景2的一系列操作后,循环就结束了,我们跳出循环,将节点x设置为黑色,自平衡调整完成。

场景3:

节点x、x的兄弟节点sib、sib的右子节点都为黑色,sib的左子节点为红色时,需要将sib左子节点设置为黑色,sib节点设置为红色,同时按sib右旋,再将sib指向x的兄弟节点

并没有完,场景3的一系列操作后,会进入到场景4

场景4:

节点x、x的兄弟节点sib都为黑色,而sib的左右子节点都为红色或者右子节点为红色、左子节点为黑色,此时需要将sib节点的颜色设置成和x的父节点p相同的颜色,设置x的父节点颜色为黑色,设置sib右孩子的颜色为黑色,左旋x的父节点p,然后将x赋值为root

四种场景讲完了,删除后的自平衡操作不太好理解,代码层面的已经弄明白了,但如果让我自己去实现的话,还是差了一些,还需要再研究。

七. 遍历

遍历比较简单,TreeMap的遍历可以使用map.values(), map.keySet(),map.entrySet(),map.forEach(),这里不再多说。

八. 总结

本文详细介绍了TreeMap的基本特点,并对其底层数据结构红黑树进行了回顾,同时讲述了其自动排序的原理,并从源码的角度结合红黑树图形对put方法、get方法、remove方法进行了讲解,最后简单提了一下遍历操作

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/164493.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

蓝桥杯STM32G431RBT6学习——LED

蓝桥杯STM32G431RBT6学习——LED 前言 LED为每年必考考点&#xff0c;也是入门的基础&#xff0c;国信长天的开发板LED硬件如下&#xff1a; 经典的锁存器控制&#xff0c;因为LED所用引脚与LCD重叠&#xff0c;因此通过锁存器进行控制其状态。当74HC573的LE引脚&#xff08…

C语言综合练习5:快译通下

1 词典文件介绍 前面建立的词典&#xff0c;只有两个单词&#xff0c;现在我们要建立一个上万个单词的词典&#xff0c;所有单词及其翻译都在一个名为dict.txt的文件&#xff08;词典文件&#xff09;中 每个单词有两行&#xff0c;其中一行是单词原文&#xff0c;下一行是对…

Redis中的事务和乐观锁实现

redis事务相关命令&#xff1a; 开启事务&#xff1a;multi 关闭事务&#xff1a;discard 提交事务&#xff1a;exec 正常执行事务情况&#xff1a; 127.0.0.1:6379> multi OK 127.0.0.1:6379> set name zhangsan QUEUED 127.0.0.1:6379> set age 20 QUEUED 127.0.0.1…

AJAX这一篇就够啦~

AJAX这一篇就够啦~AJAX1、AJAX概述1.1 AJAX简介1.2 XML简介1.3 AJAX的特点2、HTTP相关2.1 HTTP概述2.2 请求报文2.3 响应报文2.4 常见的响应状态码2.5 不同类型的请求及其作用2.6 一般http请求 与 ajax请求3、原生AJAX的使用3.1 准备工作3.2 核心对象3.3 GET请求3.4 POST请求3.…

新岁序开,和Jina AI共同码梦! (奖品攻略大揭秘)

Jina AI 北京、深圳、柏林、湾区的小伙伴给您拜年啦&#xff01; Jina AI 开源社区致力于促进 多模态 AI 技术 的应用落地以及传播&#xff0c;一直以来&#xff0c;我们都为拥有这样一个全球化、多元化和高速发展的社区而感到自豪和感激&#xff01;正值新年之际&#xff0c;我…

从C和C++内存管理来谈谈JVM的垃圾回收算法设计-下

从C和C内存管理来谈谈JVM的垃圾回收算法设计-下引言基本概念对象GC ROOTS垃圾回收常见算法标记清除优缺点引用计数优缺点部分标记清除算法优缺点复制算法优缺点多空间复制算法标记整理(标记压缩)优缺点分代设计HotSpot具体实现跨代引用并发可行性经典垃圾回收器Serial新生代垃圾…

Binding常用辅助属性、多重绑定、优先级绑定

Binding常用辅助属性、多重绑定、优先级绑定 Binding常用辅助属性 StringFormat <Window.Resources><sys:Int32 x:Key"myInt">200</sys:Int32><sys:Single x:Key"mySingle">100.123456</sys:Single> </Window.Resourc…

Linux 中断控制器(五):中断号映射

中断号分为硬件中断号(HW ID)和软件中断号(IRQ number)。 这里有两个中断控制器,处理完毕进入 CPU。外设和中断控制器连接在一起,外设给中断控制器的是硬件中断号,如果中断控制器有级联,那么硬件中断号在不同的中断控制器中可能会重复。但是到了 CPU 以后,我们需要对不…

C语言:分支语句和循环语句

往期文章 C语言&#xff1a;初识C语言 目录往期文章前言1. 什么是语句2. 分支语句&#xff08;选择结构&#xff09;2.1 if语句2.2 switch语句3. 循环语句3.1 while循环3.2 for循环3.3 do while 循环3.4 goto语句后记前言 趁热打铁啊。写完该系列第一篇博客我就来劲了&#x…

TicTacToe: 基于时序差分TD(0)算法的agent实现以及完整python实现框架

目录 1. 前言 2. TD(0) 3. 实现要点解读 3.1 Class Env 3.2 Class State 3.3 Class Agent 3.3.1 class TD0Agent(Agent) 3.3.2 class MinimaxAgent 3.3.3 class RandomAgent(Agent) 3.3.4 class HumanPlayer(Agent) 3.4 棋盘和玩家的表示 4. Utility Function 4.1…

Redis02之使用java代码操作Redis

目录 1、可视化管理工具redis-desktop-manager安装与配置 ​编辑 2、Java访问redis 注1&#xff1a;不需要记得API的方法&#xff0c;只需要查redis命令 3、web3.0设置 1、可视化管理工具redis-desktop-manager安装与配置 1.1 双击redis-desktop-manager-0.8.8.384.exe即可…

【LeetCode】2293. 极大极小游戏

2293. 极大极小游戏 题目描述 给你一个下标从 0 开始的整数数组 nums &#xff0c;其长度是 2 的幂。 对 nums 执行下述算法&#xff1a; 设 n 等于 nums 的长度&#xff0c;如果 n 1 &#xff0c;终止 算法过程。否则&#xff0c;创建 一个新的整数数组 newNums &#xf…

自动驾驶轨迹生成-贝塞尔(Bézier)曲线

引言 最近刚看完贝塞尔曲线&#xff0c;工作就遇到了相应的需求&#xff0c;所以写一下过程。主要讲的是自动驾驶中&#xff0c;车换道时用到贝塞尔曲线&#xff0c;当然其他的很多领域也会有&#xff0c;例如图形学等。 在车遇到障碍物或者是前车速度较慢的时候&#xff0c;…

【编译原理核心知识点总结】第三章、正则文法、NFA、DFA

阅读规范&#xff1a; 本文以重点为主&#xff0c;零碎知识点/相对不够重要的为辅助阅读内容&#xff0c;以脚注形式给出&#xff0c;点击脚注即可快速跳转。助解题目通常是为了帮助理解给出的题目&#xff0c;考试不考&#xff0c;若已理解可直接跳过。文中提到的课本是陈火旺…

SpringBoot 并发编程学习历程(绝对的干货)

如果一个项目总用单线程来跑&#xff0c;难免会遇到一些性能问题&#xff0c;所以再开发中&#xff0c;我们应该尽量适量的使用多线程&#xff08;在保证线程安全的情况下&#xff09;。 本教程大概目录&#xff1a; 1.模拟单线程情节 2.用Callable实现 并发编程 3.用DeferedR…

高并发系统设计 --基于bitmap的用户签到

业务需求分析 一般像微博&#xff0c;各种社交软件&#xff0c;游戏等APP&#xff0c;都会有一个签到功能&#xff0c;连续签到多少天&#xff0c;送什么东西&#xff0c;比如&#xff1a; 签到1天送10积分&#xff0c;连续签到2天送20积分&#xff0c;3天送30积分&#xff0…

Qt之QDrag的使用(含源码+注释)

一、效果示例图 提示&#xff1a;主控件&#xff08;CDragTest界面&#xff0c;UI中中包含CWidget界面&#xff09;&#xff1b;子控件&#xff08;CWidget界面&#xff0c;在CDragTest界面添加&#xff09; 提示&#xff1a;源码中拖拽数据设置的文本不同&#xff0c;是博主准…

【ONE·C || 分支循环】

总言 C语言&#xff1a;分支循环。 文章目录总言1、分支语句1.1、if语句1.1.1、基本格式1.1.2、逻辑真假与悬空else1.1.3、练习1.2、switch语句1.2.1、基本格式&#xff1a;break、case、default1.2.2、练习&#xff1a;switch语句嵌套2、循环语句2.1、while循环2.1.1、基本格式…

uniapp实现界面可任意拖动小图标

uniapp实现界面可任意拖动小图标一、问题&#xff1a;二、解决步骤2.1 根据uni-app官方提供的案例&#xff0c;创建组件2.2 在需要的界面引入组件使用额外注意一、问题&#xff1a; 例如购物车小图标可任意拖动 二、解决步骤 2.1 根据uni-app官方提供的案例&#xff0c;创建…

Kubernetes教程(二)---集群网络之 Flannel 核心原理

来自&#xff1a;指月 https://www.lixueduan.com 原文&#xff1a;https://www.lixueduan.com/posts/kubernetes/02-cluster-network/ 本文主要记录了 Kubernetes 集群网络方案之 Flannel 核心原理详解&#xff0c;包括其隧道方案中的两种&#xff1a;UDP 实现和 VXLAN 实现…