热门标签 | HotTags
当前位置:  开发笔记 > 编程语言 > 正文

java以图判树_Java关于数据结构的实现:树

树有以下特点:每个节点有零个或多个子节点;没有父节点的节点称为根节点;每一个非根节点有且只有一个父节点;除了根节点外

树有以下特点:

每个节点有零个或多个子节点;

没有父节点的节点称为根节点;

每一个非根节点有且只有一个父节点;

除了根节点外,每个子节点可以分为多个不相交的子树;

每个节点有零个或多个子节点;

没有父节点的节点称为根节点;

每一个非根节点有且只有一个父节点;

除了根节点外,每个子节点可以分为多个不相交的子树;

与树相关的概念

节点的度:一个节点含有的子树的个数称为该节点的度;

树的度:一棵树中,最大的节点的度称为树的度;

叶节点或终端节点:度为零的节点;

非终端节点或分支节点:度不为零的节点;

父亲节点或父节点:若一个节点含有子节点,则这个节点称为其子节点的父节点;

孩子节点或子节点:一个节点含有的子树的根节点称为该节点的子节点;

兄弟节点:具有相同父节点的节点互称为兄弟节点;

节点的层次:从根开始定义起,根为第1层,根的子节点为第2层,以此类推;

深度:对于任意节点n, n的深度为从根到n的唯一路径长,根的深度为0;

高度:对于任意节点n, n的高度为从n到一片树叶的最长路径长,所有树叶的高度为0;

堂兄弟节点:父节点在同一层的节点互为堂兄弟;

节点的祖先:从根到该节点所经分支上的所有节点;

子孙:以某节点为根的子树中任一节点都称为该节点的子孙。

森林:由m(m>=0)棵互不相交的树的集合称为森林;

节点的度:一个节点含有的子树的个数称为该节点的度;

树的度:一棵树中,最大的节点的度称为树的度;

叶节点或终端节点:度为零的节点;

非终端节点或分支节点:度不为零的节点;

父亲节点或父节点:若一个节点含有子节点,则这个节点称为其子节点的父节点;

孩子节点或子节点:一个节点含有的子树的根节点称为该节点的子节点;

兄弟节点:具有相同父节点的节点互称为兄弟节点;

节点的层次:从根开始定义起,根为第1层,根的子节点为第2层,以此类推;

深度:对于任意节点n, n的深度为从根到n的唯一路径长,根的深度为0;

高度:对于任意节点n, n的高度为从n到一片树叶的最长路径长,所有树叶的高度为0;

堂兄弟节点:父节点在同一层的节点互为堂兄弟;

节点的祖先:从根到该节点所经分支上的所有节点;

子孙:以某节点为根的子树中任一节点都称为该节点的子孙。

森林:由m(m>=0)棵互不相交的树的集合称为森林;

注:参照亲戚关系,这些概念很好理解,家族谱也是一种树结构。:grinning:

树的分类

无序树:树中任意节点的子节点之间没有顺序关系,这种树称为无序树,也称为自由树;

有序树:树中任意节点的子节点之间有顺序关系,这种树称为有序树;

无序树:树中任意节点的子节点之间没有顺序关系,这种树称为无序树,也称为自由树;

有序树:树中任意节点的子节点之间有顺序关系,这种树称为有序树;

其中有序树又分为:

二叉树:每个节点最多含有两个子树的树称为二叉树;

完全二叉树:对于一颗二叉树,假设其深度为d(d>1)。除了第d层外,其它各层的节点数目均已达最大值,且第d层所有节点从左向右连续地紧密排列,这样的二叉树被称为完全二叉树;

满二叉树:所有叶节点都在最底层的完全二叉树;

AVL树:当且仅当任何节点的两棵子树的高度差不大于1的二叉树;

二叉查找树:树中的每个节点,它的左子树中所有项小于X中的项,它的右子树中的所有项大于X中的项。

霍夫曼树:带权路径最短的二叉树称为哈夫曼树或最优二叉树;

B树:一种对读写操作进行优化的自平衡的二叉查找树,能够保持数据有序,拥有多余两个子树。

二叉树:每个节点最多含有两个子树的树称为二叉树;

完全二叉树:对于一颗二叉树,假设其深度为d(d>1)。除了第d层外,其它各层的节点数目均已达最大值,且第d层所有节点从左向右连续地紧密排列,这样的二叉树被称为完全二叉树;

满二叉树:所有叶节点都在最底层的完全二叉树;

AVL树:当且仅当任何节点的两棵子树的高度差不大于1的二叉树;

二叉查找树:树中的每个节点,它的左子树中所有项小于X中的项,它的右子树中的所有项大于X中的项。

霍夫曼树:带权路径最短的二叉树称为哈夫曼树或最优二叉树;

B树:一种对读写操作进行优化的自平衡的二叉查找树,能够保持数据有序,拥有多余两个子树。

1.1 二叉查找树

二叉查找树是一种有序二叉树,它的查找、插入的时间复杂度为O(logN)

二叉查找是是一种有序二叉树.

主要特点

若任意节点的左子树不空,则左子树上所有节点的值均小于它的根节点的值。

若任意节点的右子树不空,则右子树上所有节点的值均大于它的根节点的值。

任意节点的左右子树叶为二叉查找树。

没有键值相等的节点。

若任意节点的左子树不空,则左子树上所有节点的值均小于它的根节点的值。

若任意节点的右子树不空,则右子树上所有节点的值均大于它的根节点的值。

任意节点的左右子树叶为二叉查找树。

没有键值相等的节点。

就是说二叉查找树上的节点是排好序的&#xff1a;左子树 <根节点 <右子树。

性能分析

在最坏的情况下&#xff0c;即当先后插入的关键字有序时&#xff0c;构造成的二叉查找树蜕变为单支树&#xff0c;输的深度为n&#xff0c;其平均查找长度为(n&#43;1)/2。

在最好的情况下&#xff0c;二叉查找树的形态和折半查找的判定树相同&#xff0c;其时间复杂度为O(log2(N))。

在最坏的情况下&#xff0c;即当先后插入的关键字有序时&#xff0c;构造成的二叉查找树蜕变为单支树&#xff0c;输的深度为n&#xff0c;其平均查找长度为(n&#43;1)/2。

在最好的情况下&#xff0c;二叉查找树的形态和折半查找的判定树相同&#xff0c;其时间复杂度为O(log2(N))。

我们来看看二叉查找树上的相关操作。

b481c1f92be93a77ae1f3d7d6f57d511.png

构造

当我们用一组数值构造一棵二叉查找树时&#xff0c;就相当于对这组数值进行了排序&#xff0c;在最坏情况下&#xff0c;即该组数值是从小到达排好序的&#xff0c;构造出来的二叉树的所有节点都 没有左子树&#xff0c;这种情况下的时间复杂度为O(N2)。(N的平方)

另外&#xff0c;树排序的问题使得CPU Cache性能较差&#xff0c;特别是当节点是动态内存分配时。而堆排序的CPU Cache性能较好。另一方面&#xff0c;树排序是最优的增量排序(incremental sorting)算法&#xff0c; 保持一个数值序列的有序性。

查找

由于二叉查找树具有左子树 <根节点 <右子树的特点&#xff0c;因此在二叉查找树b中查找x的过程如下&#xff1a;

若b是空树&#xff0c;则查找失败&#xff0c;否则&#xff1a;

若x等于根节点的值&#xff0c;则查找成功&#xff0c;否则&#xff1a;

若x小于b根节点的值&#xff0c;则查找左子树&#xff0c;否则

若x大于b根节点的值&#xff0c;则查找右子树。

若b是空树&#xff0c;则查找失败&#xff0c;否则&#xff1a;

若x等于根节点的值&#xff0c;则查找成功&#xff0c;否则&#xff1a;

若x小于b根节点的值&#xff0c;则查找左子树&#xff0c;否则

若x大于b根节点的值&#xff0c;则查找右子树。

整个流程是一个递归的过程。

插入

插入的过程也是查找的过程&#xff0c;在二叉查找树中插入节点x的过程如下&#xff1a;

若b是空树&#xff0c;则x作为根节点插入&#xff0c;否则&#xff1a;

若x的值等于根节点的值&#xff0c;则直接返回&#xff0c;否则&#xff1a;

若x的值小于根节点的值&#xff0c;则将x插入当该根节点的左子树中&#xff0c;否则

将x插入该根节点的右子树中。

若b是空树&#xff0c;则x作为根节点插入&#xff0c;否则&#xff1a;

若x的值等于根节点的值&#xff0c;则直接返回&#xff0c;否则&#xff1a;

若x的值小于根节点的值&#xff0c;则将x插入当该根节点的左子树中&#xff0c;否则

将x插入该根节点的右子树中。

这也是一个递归的过程&#xff0c;这里我们要关注两点&#xff1a;

插入的过程也是查找的过程

二叉查找树不允许有相同值的节点

插入的过程也是查找的过程

二叉查找树不允许有相同值的节点

删除

在二叉查找树上删除一个节点&#xff0c;分为三种情况&#xff1a;

若删除的是叶子节点&#xff0c;则不会破坏树的结构&#xff0c;只需要修改其双亲节点的指针即可。

若删除的节点只有一个孩子节点&#xff0c;则用它的孩子节点代替它的位置即可&#xff0c;如此也不会破坏红黑树的结构。

若删除的节点有两个孩子节点&#xff0c;这种情况复杂一下&#xff0c;我们通常会找到要删除节点X的左子树里的最大元素或者右子树里的最小元素&#xff0c;然后用M替换掉X&#xff0c;再删除节点&#xff0c;因为此时M最多只会有一个节点(如果左子树最大元素则没有右子节点&#xff0c;若是右子树最小元素则没有左子节点)&#xff0c;若M没有孩子节点&#xff0c;直接进入情况1处理&#xff0c;若M只有一个孩子&#xff0c;则直接进入情况2处理。

若删除的是叶子节点&#xff0c;则不会破坏树的结构&#xff0c;只需要修改其双亲节点的指针即可。

若删除的节点只有一个孩子节点&#xff0c;则用它的孩子节点代替它的位置即可&#xff0c;如此也不会破坏红黑树的结构。

若删除的节点有两个孩子节点&#xff0c;这种情况复杂一下&#xff0c;我们通常会找到要删除节点X的左子树里的最大元素或者右子树里的最小元素&#xff0c;然后用M替换掉X&#xff0c;再删除节点&#xff0c;因为此时M最多只会有一个节点(如果左子树最大元素则没有右子节点&#xff0c;若是右子树最小元素则没有左子节点)&#xff0c;若M没有孩子节点&#xff0c;直接进入情况1处理&#xff0c;若M只有一个孩子&#xff0c;则直接进入情况2处理。

另外&#xff0c;如果删除的次数不多&#xff0c;可以采用 懒惰删除 的方式&#xff0c;即当一个元素删除时&#xff0c;它仍然留在树中&#xff0c;只是被比较为已删除&#xff0c;这种方式在有重复项是特别有用&#xff0c; 另外如果删除的元素又重新插入&#xff0c;这种方式可以避免新单元的创建开销。

1.2 AVL树

AVL树是带有平衡条件的二叉查找树。

主要特点

AVL树中的任何阶段的两棵子树高度最大差别为1。

AVL树中的任何阶段的两棵子树高度最大差别为1。

AVL树还有个平衡因子的概念&#xff0c;平衡因子 &#61; 左子树高度 - 右子树高度&#xff0c;因此平衡因子为-1&#xff0c;0&#xff0c;1的为平衡二叉树&#xff0c;其他的都是不平衡的。

另外&#xff0c;把一棵不平衡的二叉查找树变成一棵平衡二叉树&#xff0c;我们称之为 AVL旋转。

我们来看看不同情况下AVL旋转如何进行。

左左情况&#xff1a;右旋

右右情况&#xff1a;左旋

左右情况&#xff1a;先左旋&#xff0c;再右旋

右左情况&#xff1a;先右旋&#xff0c;再左旋

左左情况&#xff1a;右旋

右右情况&#xff1a;左旋

左右情况&#xff1a;先左旋&#xff0c;再右旋

右左情况&#xff1a;先右旋&#xff0c;再左旋

注&#xff1a;所谓左左指的是左边的左子树多了一个&#xff0c;其他的依此类推。

具体操作如下所示&#xff0c;我们可以看到左左情况和右右情况只需要单旋转就可以完成&#xff0c;左右情况与右左情况需要先把它们变成左左情况与右右情况 再进行旋转&#xff0c;因此这两种情况需要双旋转才能完成。

3d8e219c11d032d0ae0ea2ad75fc4b3a.png

性能分析

查找、插入与删除在平均和最坏的情况下的时间复杂度为O(logN)。

AVL树也是二叉查找树的一种&#xff0c;它的很多操作都可以向我们上面描述的二叉查找树的操作那样进行。删除操作有点例外&#xff0c;我们在进行删除操作

时可以把要删除的节点向下旋转形成一个叶子节点&#xff0c;然后直接删除这个叶子节点&#xff0c;因为旋转成叶子节点期间&#xff0c;做多有logN个节点被旋转&#xff0c;每次 AVL旋转花费的事件固定&#xff0c;所以删除操作的时间复杂度是O(logN)。

1.3 红黑树

红黑树是平衡二叉树的变种&#xff0c;它的操作的时间复杂度是O(logN).

红黑树是一种具有着色性质的树&#xff0c;具有以下特点&#xff1a;

每个节点被着成红色或者黑色。

根是黑色的。

叶子节点都是黑色的&#xff0c;叶子节点指的是NULL节点&#xff0c;有些红黑树图中可能没有标记出来。

如果一个节点是红色的&#xff0c;那么他额子节点必须是黑色的&#xff0c;也就是不会存在两个红色节点毗邻。

从一个节点到一个叶子节点(NULL节点)的每一条路径必须包含相同数目的黑色节点。

每个节点被着成红色或者黑色。

根是黑色的。

叶子节点都是黑色的&#xff0c;叶子节点指的是NULL节点&#xff0c;有些红黑树图中可能没有标记出来。

如果一个节点是红色的&#xff0c;那么他额子节点必须是黑色的&#xff0c;也就是不会存在两个红色节点毗邻。

从一个节点到一个叶子节点(NULL节点)的每一条路径必须包含相同数目的黑色节点。

红黑树也是一种二叉查找树&#xff0c;查找操作与二叉查找树相同&#xff0c;插入与删除操作有所不同。

1.4 B树

B树是一种自平衡的树&#xff0c;能够保持数据有序&#xff0c;B树为系统大块数据的读写操作做了优化&#xff0c;通常用在数据库与文件系统的实现上。

我们前面讲解了二叉查找树、AVL树&#xff0c;红黑树&#xff0c;这三种都是典型的二叉查找树结构&#xff0c;其查找的事件复杂度O(logN)与树的深度有关&#xff0c;考虑这么一种情况&#xff0c;如果有 大量的数据&#xff0c;而节点存储的数据有限&#xff0c;这种情况下&#xff0c;我们只能去扩充树的深度&#xff0c;就会导致查找效率低下。

怎么解决这种问题&#xff0c;一个简单的想法就是&#xff1a;二叉变多叉。

这里我们想象一下常见的文件系统&#xff0c;它也是一种树结构&#xff0c;在查找文件时&#xff0c;树的深度就决定了查找的效率。因此B树就是为了减少数的深度从而提高查找效率的一种 数据结构。

主要特点

一个阶为M的B树具有以下特点&#xff1a;

注&#xff1a;M阶指的是M叉查找树&#xff0c;例如M &#61; 2&#xff0c;则为二叉查找树。

数据项存储在树叶上。

非叶节点存储直到M-1个关键字以指示搜索方向&#xff1a;关键字代表子树i&#43;1中最小的关键字。

树的根或者是一片树叶&#xff0c;或者其儿子数都在2和M之间。

除根外&#xff0c;所有非树叶节点的儿子树在M/2与M之间。

所有的树叶都在相同的深度上拥有的数据项都在L/2与L之间。

数据项存储在树叶上。

非叶节点存储直到M-1个关键字以指示搜索方向&#xff1a;关键字代表子树i&#43;1中最小的关键字。

树的根或者是一片树叶&#xff0c;或者其儿子数都在2和M之间。

除根外&#xff0c;所有非树叶节点的儿子树在M/2与M之间。

所有的树叶都在相同的深度上拥有的数据项都在L/2与L之间。

性能分析

B树在查找、插入以及删除等操作中&#xff0c;时间复杂度为O(logN)。

二、树的操作与源码实现

在文章 01Java关于数据结构的实现&#xff1a;表、栈与队列 中我们 讨论了ArrayList与LinkedList的实现&#xff0c;它们的瓶颈在于查找效率低下。因而Java集合设计了Set与Map接口&#xff0c;它们在插入、删除与查找等基本操作都有良好的表现。

2.1 TreeMap/TreeSet实现原理

TreeSet实际上是基于TreeMap的NavigableSet的实现&#xff0c;它在功能上完全依赖于TreeMap&#xff0c;TreeMap是一个基于红黑树实现的Map&#xff0c;它在存储时对元素进行排序。

因此只要理解了TreeMap实现即可&#xff0c;TreeSet在功能上完全依赖于TreeMap。

TreeMap具有以下特点&#xff1a;

TreeMap是一个有序的key-value集合&#xff0c;基于红黑树实现。

没有实现同步

TreeMap是一个有序的key-value集合&#xff0c;基于红黑树实现。

没有实现同步

TreeMap实现以下接口&#xff1a;

NavigableMap&#xff1a;支持一系列导航方法&#xff0c;比如返回有序的key集合。

Cloneable&#xff1a;可以被克隆。

Serializable&#xff1a;支持序列化。

NavigableMap&#xff1a;支持一系列导航方法&#xff0c;比如返回有序的key集合。

Cloneable&#xff1a;可以被克隆。

Serializable&#xff1a;支持序列化。

成员变量

382b3f6753d05b9dc7abe9671a124d0d.png

构造方法

19077a0bc32212295b1626433b327e4f.png

内部类

TreeMap里面定义了静态内部类TreeMapEntry来描述节点信息。

7190818c92f1f5de3a7f3d00fd23441a.png

aac03e6a5efef2114d7e9ac8a277b152.png

操作方法

在正式介绍TreeMap里的增、删、改、查操作之前&#xff0c;我们先来看看TreeMop里关于节点染色&#xff0c;树的旋转等操作的实现&#xff0c;它们是TreeMap实现的基础。

节点染色

3e09da814a49a9fb5cdac097929bdbd9.png

在介绍染色规则之前&#xff0c;我们先来回顾一下红黑树的特点&#xff1a;

每个节点被着成红色或者黑色。

根是黑色的。

叶子节点都是黑色的&#xff0c;叶子节点指的是NULL节点&#xff0c;有些红黑树图中可能没有标记出来。

如果一个节点是红色的&#xff0c;那么他额子节点必须是黑色的&#xff0c;也就是不会存在两个红色节点毗邻。

从一个节点到一个叶子节点(NULL节点)的每一条路径必须包含相同数目的黑色节点。

每个节点被着成红色或者黑色。

根是黑色的。

叶子节点都是黑色的&#xff0c;叶子节点指的是NULL节点&#xff0c;有些红黑树图中可能没有标记出来。

如果一个节点是红色的&#xff0c;那么他额子节点必须是黑色的&#xff0c;也就是不会存在两个红色节点毗邻。

从一个节点到一个叶子节点(NULL节点)的每一条路径必须包含相同数目的黑色节点。

关于节点染色&#xff0c;我们有多种情况需要考虑。

首先说明

若新节点位于树的根上&#xff0c;没有父节点&#xff0c;直接将其染成黑色即可。这个在代码中无需操作&#xff0c;因为节点默认就是黑色的。

若新节点的父节点是黑色&#xff0c;这个时候树依然满足红黑树的性质&#xff0c;并不需要额外的处理。

若新节点位于树的根上&#xff0c;没有父节点&#xff0c;直接将其染成黑色即可。这个在代码中无需操作&#xff0c;因为节点默认就是黑色的。

若新节点的父节点是黑色&#xff0c;这个时候树依然满足红黑树的性质&#xff0c;并不需要额外的处理。

以上两种情况无需额外的处理&#xff0c;我们再来考虑需要处理的情况。

情况1&#xff1a;如果新节点N的父节点P是红色&#xff0c;且其叔父节点U也为红色&#xff0c;我们可以将父节点P与叔父节点U染成黑色&#xff0c;祖父节点G染成红色。

情况2&#xff1a;如果新节点N的父节点P是红色&#xff0c;且其叔父节点U为黑色或者没有叔父节点&#xff0c;P为其父节点P的右子节点&#xff0c;P为为其父节点G的左子节点。这种情况我们针对祖父节点G做一次右旋。并将P染成黑色&#xff0c;染成红色

情况3&#xff1a;如果新节点N的父节点P是红色&#xff0c;且其叔父节点U为黑色或者没有叔父节点&#xff0c;P为其父节点P的右子节点&#xff0c;P为为其父节点G的左子节点。这种情况下我们对做一次左旋调换&#xff0c;调换P与N的位置&#xff0c;这样情况3变成了 情况2&#xff0c;剩下的按照情况2处理即可。

情况1&#xff1a;如果新节点N的父节点P是红色&#xff0c;且其叔父节点U也为红色&#xff0c;我们可以将父节点P与叔父节点U染成黑色&#xff0c;祖父节点G染成红色。

情况2&#xff1a;如果新节点N的父节点P是红色&#xff0c;且其叔父节点U为黑色或者没有叔父节点&#xff0c;P为其父节点P的右子节点&#xff0c;P为为其父节点G的左子节点。这种情况我们针对祖父节点G做一次右旋。并将P染成黑色&#xff0c;染成红色

情况3&#xff1a;如果新节点N的父节点P是红色&#xff0c;且其叔父节点U为黑色或者没有叔父节点&#xff0c;P为其父节点P的右子节点&#xff0c;P为为其父节点G的左子节点。这种情况下我们对做一次左旋调换&#xff0c;调换P与N的位置&#xff0c;这样情况3变成了 情况2&#xff0c;剩下的按照情况2处理即可。

我们来看看具体的源码实现。

public class TreeMap extends AbstractMap implements NavigableMap, Cloneable, java.io.Serializable{ //染色 private void fixAfterInsertion(TreeMapEntry x) { x.color &#61; RED; while (x !&#61; null && x !&#61; root && x.parent.color &#61;&#61; RED) { //新节点N(即x)在其祖父节点的左子树上&#xff0c;叔父节点在左子树上。 if (parentOf(x) &#61;&#61; leftOf(parentOf(parentOf(x)))) { TreeMapEntry y &#61; rightOf(parentOf(parentOf(x))); //情况1&#xff1a;如果新节点N的父节点P是红色&#xff0c;且其叔父节点U也为红色&#xff0c;我们可以将父节点P与叔父节点U染成黑色&#xff0c;祖父节点G染成红色。 if (colorOf(y) &#61;&#61; RED) { setColor(parentOf(x), BLACK); setColor(y, BLACK); setColor(parentOf(parentOf(x)), RED); x &#61; parentOf(parentOf(x)); } //情况2&#xff1a;如果新节点N的父节点P是红色&#xff0c;且其叔父节点U为黑色或者没有叔父节点&#xff0c;P为其父节点P的右子节点&#xff0c;P为为其父节点G的左 //子节点。这种情况我们针对祖父节点G做一次右旋。并将P染成黑色&#xff0c;染成红色 else { //情况3&#xff1a;x为其父节点的右子节点&#xff0c;先对其父节点进行左旋&#xff0c;调换两者的位置 if (x &#61;&#61; rightOf(parentOf(x))) { x &#61; parentOf(x); rotateLeft(x); } setColor(parentOf(x), BLACK); setColor(parentOf(parentOf(x)), RED); rotateRight(parentOf(parentOf(x))); } } //情况1&#xff1a;新节点N(即x)在其祖父节点的右子树上&#xff0c;叔父节点在左子树上&#xff0c;这种情况和在右子节点的情况相似&#xff0c;知识旋转方向相反罢了。 else { TreeMapEntry y &#61; leftOf(parentOf(parentOf(x))); //如果新节点N的父节点P是红色&#xff0c;且其叔父节点U也为红色&#xff0c;我们可以将父节点P与叔父节点U染成黑色&#xff0c;祖父节点G染成红色。 if (colorOf(y) &#61;&#61; RED) { setColor(parentOf(x), BLACK); setColor(y, BLACK); setColor(parentOf(parentOf(x)), RED); x &#61; parentOf(parentOf(x)); } //情况2&#xff1a;如果新节点N的父节点P是红色&#xff0c;且其叔父节点U为黑色或者没有叔父节点&#xff0c;P为其父节点P的右子节点&#xff0c;P为为其父节点G的左 //子节点。这种情况我们针对祖父节点G做一次右旋。并将P染成黑色&#xff0c;染成红色 else { //情况3&#xff1a;x为其父节点的左子节点&#xff0c;先对其父节点进行右旋&#xff0c;调换两者位置。 if (x &#61;&#61; leftOf(parentOf(x))) { x &#61; parentOf(x); rotateRight(x); } setColor(parentOf(x), BLACK); setColor(parentOf(parentOf(x)), RED); rotateLeft(parentOf(parentOf(x))); } } } root.color &#61; BLACK; }}

节点旋转

5a18745541990a046a2982c99569baef.png

左旋

之前在网上看到一组关于左旋、右旋的动态图&#xff0c;很形象&#xff0c;这里也贴出来。

6e82a0cda6dab752ef3002d04908005a.gif

找到要旋转节点p的右节点r&#xff0c;然后将r的左子节点赋值给p的右子节点&#xff0c;如果r的左子节点为空&#xff0c;则直接将r节点设置为P的父节点。

将原来p的父节点设置成r的父节点&#xff0c;如果原来p的父节点为空&#xff0c;则直接接r设置成根节点&#xff0c;如果根节点不空且其做子节点为p&#xff0c;则将r设置为新的左子节点&#xff0c;如果根节点不空且其右子节点为p&#xff0c;则将r设置为新的右子节点&#xff0c;

再将r的左子节点设为p&#xff0c;p的父节点设置为r&#xff0c;左旋完成。

找到要旋转节点p的右节点r&#xff0c;然后将r的左子节点赋值给p的右子节点&#xff0c;如果r的左子节点为空&#xff0c;则直接将r节点设置为P的父节点。

将原来p的父节点设置成r的父节点&#xff0c;如果原来p的父节点为空&#xff0c;则直接接r设置成根节点&#xff0c;如果根节点不空且其做子节点为p&#xff0c;则将r设置为新的左子节点&#xff0c;如果根节点不空且其右子节点为p&#xff0c;则将r设置为新的右子节点&#xff0c;

再将r的左子节点设为p&#xff0c;p的父节点设置为r&#xff0c;左旋完成。

右旋

523495ddccf07984fafd39ce529b8d32.gif

找到要旋转节点p的左子节点l&#xff0c;然后将l的右子节点赋值给p的左子节点&#xff0c;如果l的右子节点为空&#xff0c;则直接将l节点设置为P的父节点。

将原来p的父节点设置成l的父节点&#xff0c;如果原来p的父节点为空&#xff0c;则直接接l设置成根节点&#xff0c;如果根节点不空且其做子节点为p&#xff0c;则将l设置为新的左子节点&#xff0c;如果根节点不空且其右子节点为p&#xff0c;则将l设置为新的右子节点&#xff0c;

再将l的右子节点设为p&#xff0c;p的父节点设置为l&#xff0c;右旋完成。

找到要旋转节点p的左子节点l&#xff0c;然后将l的右子节点赋值给p的左子节点&#xff0c;如果l的右子节点为空&#xff0c;则直接将l节点设置为P的父节点。

将原来p的父节点设置成l的父节点&#xff0c;如果原来p的父节点为空&#xff0c;则直接接l设置成根节点&#xff0c;如果根节点不空且其做子节点为p&#xff0c;则将l设置为新的左子节点&#xff0c;如果根节点不空且其右子节点为p&#xff0c;则将l设置为新的右子节点&#xff0c;

再将l的右子节点设为p&#xff0c;p的父节点设置为l&#xff0c;右旋完成。

我们来看看具体的源码实现。

8bb6a360d2c4ca88500cc404e4858a02.png

44e71ecfc98af3fb3698533d338bd909.png

put

6a6d1c773a2dc801ebfd5d00e06b1827.png

320bf1d7a75c1ddfa2f9c021aa089f01.png

fbd3f6b0353e64218791c89acd342d12.png

插入操作采用了二叉排序树的查找算法&#xff0c;整个流程如下&#xff1a;

如果当前TreeMap没有根节点&#xff0c;将当前节点作为根节点插入&#xff0c;否则&#xff0c;

根据提供的比较器(如果没有提供则使用默认的比较器)进行查找比较&#xff0c;查找该节点的插入位置&#xff0c;即它的父节点的位置。

查找到父节点后&#xff0c;根据比较结果插入到对应位置&#xff0c;并进行染色处理。

如果当前TreeMap没有根节点&#xff0c;将当前节点作为根节点插入&#xff0c;否则&#xff0c;

根据提供的比较器(如果没有提供则使用默认的比较器)进行查找比较&#xff0c;查找该节点的插入位置&#xff0c;即它的父节点的位置。

查找到父节点后&#xff0c;根据比较结果插入到对应位置&#xff0c;并进行染色处理。

remove

前面我们讲了插入操作&#xff0c;删除操作要比插入操作复杂一下&#xff0c;我们先来描述一下删除操作的大概流程&#xff1a;

将红黑树当成一棵二叉查找树&#xff0c;进行节点删除。

通过旋转和着色&#xff0c;使其重新变成一棵复合规则的红黑树。

将红黑树当成一棵二叉查找树&#xff0c;进行节点删除。

通过旋转和着色&#xff0c;使其重新变成一棵复合规则的红黑树。

二叉查找树时怎么做删除的。前面我们已经说过&#xff0c;在二叉查找树上删除一个节点&#xff0c;分为三种情况&#xff1a;

0af8cc85d58fe10de3ee72364f4f0570.png

若删除的是叶子节点&#xff0c;则不会破坏树的结构&#xff0c;只需要修改其双亲节点的指针即可。

若删除的节点只有一个孩子节点&#xff0c;则用它的孩子节点代替它的位置即可&#xff0c;如此也不会破坏红黑树的结构。

若删除的节点有两个孩子节点&#xff0c;这种情况复杂一下&#xff0c;我们通常会找到要删除节点X的左子树里的最大元素或者右子树里的最小元素&#xff0c;然后用M替换掉X&#xff0c;再删除节点&#xff0c;因为此时M最多只会有一个节点(如果左子树最大元素则没有右子节点&#xff0c;若是右子树最小元素则没有左子节点)&#xff0c;若M没有孩子节点&#xff0c;直接进入情况1处理&#xff0c;若M只有一个孩子&#xff0c;则直接进入情况2处理。

若删除的是叶子节点&#xff0c;则不会破坏树的结构&#xff0c;只需要修改其双亲节点的指针即可。

若删除的节点只有一个孩子节点&#xff0c;则用它的孩子节点代替它的位置即可&#xff0c;如此也不会破坏红黑树的结构。

若删除的节点有两个孩子节点&#xff0c;这种情况复杂一下&#xff0c;我们通常会找到要删除节点X的左子树里的最大元素或者右子树里的最小元素&#xff0c;然后用M替换掉X&#xff0c;再删除节点&#xff0c;因为此时M最多只会有一个节点(如果左子树最大元素则没有右子节点&#xff0c;若是右子树最小元素则没有左子节点)&#xff0c;若M没有孩子节点&#xff0c;直接进入情况1处理&#xff0c;若M只有一个孩子&#xff0c;则直接进入情况2处理。

注&#xff1a;这里的替换指的是值拷贝&#xff0c;值拷贝并不会破坏红黑树的性质。

这样三种情况都可以当做第一种或者第二种情况处理。

在删除节点时&#xff0c;我们有两个问题需要注意&#xff1a;

如果删除的额是红色节点&#xff0c;不会违反红黑树的规则。

如果删除的是黑色节点&#xff0c;那么这个路径上就少了一个黑色节点&#xff0c;则违反了红黑树规则。

如果删除的额是红色节点&#xff0c;不会违反红黑树的规则。

如果删除的是黑色节点&#xff0c;那么这个路径上就少了一个黑色节点&#xff0c;则违反了红黑树规则。

这样我们可以得知只有在插入的节点是黑色的时候才需要我们进行处理&#xff0c;具体说来&#xff1a;

情况1&#xff1a;若删除节点N的兄弟节点B是红色&#xff0c;这种情况下&#xff0c;先对父节点P进行左旋操作&#xff0c;结合对换P与B的颜色。此时左子树仍然少了一个黑色节点&#xff0c;此时进入情况3.

情况2&#xff1a;若删除节点N的父亲节点P&#xff0c;兄弟节点B以及B的儿子节点都是黑色&#xff0c;则将B染成红色&#xff0c;这样P到叶子节点的所有路径都包含了相同的黑色节点&#xff0c;但是P的父节点G到叶子节点的路径却少了 一个黑色节点。这个时候我们要重新按照这套规则对P节点再进行一次平衡处理。

情况3&#xff1a;若删除节点N的父亲节点P是红色&#xff0c;兄弟节点B是黑色&#xff0c;则交换P与B颜色&#xff0c;这样在B所在路径上增加了一个黑色节点&#xff0c;弥补了已经删除的&#xff0c;树重新达到平衡。

情况4&#xff1a; 若删除节点N的兄弟节点B是黑涩&#xff0c;B的左孩子节点BL是红色&#xff0c;B的右孩子节点BR是黑色&#xff0c;P为任意颜色。则减缓B与BL的颜色&#xff0c;右旋节点B。此时N所在路径并没有增加黑色节点&#xff0c;没有达到平衡&#xff0c;进入情况5.

情况5&#xff1a;若删除节点N的兄弟节点B是黑色&#xff0c;B的右孩子节点BR是红色&#xff0c;B的左孩子节点BL为任意颜色&#xff0c;P为任意颜色。则BR染成黑色&#xff0c;P染成黑色&#xff0c;B染成原来P的颜色&#xff1b;左旋节点&#xff0c;这样 N路径上增加了一个黑色节点&#xff0c;B路径上少了一个黑色节点B&#xff0c;又增加了一个黑色节点BR&#xff0c;刚好达到平衡。

情况1&#xff1a;若删除节点N的兄弟节点B是红色&#xff0c;这种情况下&#xff0c;先对父节点P进行左旋操作&#xff0c;结合对换P与B的颜色。此时左子树仍然少了一个黑色节点&#xff0c;此时进入情况3.

情况2&#xff1a;若删除节点N的父亲节点P&#xff0c;兄弟节点B以及B的儿子节点都是黑色&#xff0c;则将B染成红色&#xff0c;这样P到叶子节点的所有路径都包含了相同的黑色节点&#xff0c;但是P的父节点G到叶子节点的路径却少了 一个黑色节点。这个时候我们要重新按照这套规则对P节点再进行一次平衡处理。

情况3&#xff1a;若删除节点N的父亲节点P是红色&#xff0c;兄弟节点B是黑色&#xff0c;则交换P与B颜色&#xff0c;这样在B所在路径上增加了一个黑色节点&#xff0c;弥补了已经删除的&#xff0c;树重新达到平衡。

情况4&#xff1a; 若删除节点N的兄弟节点B是黑涩&#xff0c;B的左孩子节点BL是红色&#xff0c;B的右孩子节点BR是黑色&#xff0c;P为任意颜色。则减缓B与BL的颜色&#xff0c;右旋节点B。此时N所在路径并没有增加黑色节点&#xff0c;没有达到平衡&#xff0c;进入情况5.

情况5&#xff1a;若删除节点N的兄弟节点B是黑色&#xff0c;B的右孩子节点BR是红色&#xff0c;B的左孩子节点BL为任意颜色&#xff0c;P为任意颜色。则BR染成黑色&#xff0c;P染成黑色&#xff0c;B染成原来P的颜色&#xff1b;左旋节点&#xff0c;这样 N路径上增加了一个黑色节点&#xff0c;B路径上少了一个黑色节点B&#xff0c;又增加了一个黑色节点BR&#xff0c;刚好达到平衡。

以上的流程看起来比较复杂&#xff0c;本质上来说就是我们删除了一个黑色节点&#xff0c;破坏了当前路径黑色节点的个数&#xff0c;解决的方法要么为这条路径再添加一个黑色节点&#xff0c;要么将其他路径的黑色节点都去掉一个。

public class TreeMap extends AbstractMap implements NavigableMap, Cloneable, java.io.Serializable{ public V remove(Object key) { TreeMapEntry p &#61; getEntry(key); if (p &#61;&#61; null) return null; V oldValue &#61; p.value; deleteEntry(p); return oldValue; } private void deleteEntry(TreeMapEntry p) { //操作记录自增 modCount&#43;&#43;; //集合大小自减 size--; ///如果要删除的节点p的左右子节点都不为空&#xff0c;则查找其替代节点并进行节点替换 if (p.left !&#61; null && p.right !&#61; null) { //查找其替代节点&#xff0c;替代节点为左子树的最大元素或者右子树的最小元素 TreeMapEntry s &#61; successor(p); p.key &#61; s.key; p.value &#61; s.value; p &#61; s; } // p has 2 children //查找替代节点的孩子节点&#xff0c;replacement指的是我们图中说来的N节点&#xff0c;p指的是图中的 TreeMapEntry replacement &#61; (p.left !&#61; null ? p.left : p.right); //删除p&#xff0c;并重新建立replacement节点的连接 if (replacement !&#61; null) { replacement.parent &#61; p.parent; if (p.parent &#61;&#61; null) root &#61; replacement; else if (p &#61;&#61; p.parent.left) p.parent.left &#61; replacement; else p.parent.right &#61; replacement; // Null out links so they are OK to use by fixAfterDeletion. p.left &#61; p.right &#61; p.parent &#61; null; //如果删除的黑色节点&#xff0c;则需要重新平衡树 if (p.color &#61;&#61; BLACK) fixAfterDeletion(replacement); } else if (p.parent &#61;&#61; null) { // return if we are the only node. root &#61; null; } else { // No children. Use self as phantom replacement and unlink. if (p.color &#61;&#61; BLACK) fixAfterDeletion(p); if (p.parent !&#61; null) { if (p &#61;&#61; p.parent.left) p.parent.left &#61; null; else if (p &#61;&#61; p.parent.right) p.parent.right &#61; null; p.parent &#61; null; } } } //查找其替代节点&#xff0c;替代节点为左子树的最大元素或者右子树的最小元素 static TreeMapEntry successor(TreeMapEntry t) { if (t &#61;&#61; null) return null; //查找右子树的最小元素&#xff0c;即最左孩子 else if (t.right !&#61; null) { TreeMapEntry p &#61; t.right; while (p.left !&#61; null) p &#61; p.left; return p; } //查找左子树的最大元素&#xff0c;即最右孩子 else { TreeMapEntry p &#61; t.parent; TreeMapEntry ch &#61; t; while (p !&#61; null && ch &#61;&#61; p.right) { ch &#61; p; p &#61; p.parent; } return p; } } static TreeMapEntry predecessor(TreeMapEntry t) { if (t &#61;&#61; null) return null; else if (t.left !&#61; null) { TreeMapEntry p &#61; t.left; while (p.right !&#61; null) p &#61; p.right; return p; } else { TreeMapEntry p &#61; t.parent; TreeMapEntry ch &#61; t; while (p !&#61; null && ch &#61;&#61; p.left) { ch &#61; p; p &#61; p.parent;s } return p; } }}

我们再来看看deleteEntry()方法的实现流程&#xff1a;

如果要删除的节点p的左右子节点都不为空&#xff0c;则查找其替代节点并进行节点替换。

查找替代节点的孩子节点&#xff0c;replacement指的是我们图中说来的N节点&#xff0c;p指的是图中的M&#xff0c;如果p是黑色节点&#xff0c;则删除p后需要重新进行树的平衡处理。

如果要删除的节点p的左右子节点都不为空&#xff0c;则查找其替代节点并进行节点替换。

查找替代节点的孩子节点&#xff0c;replacement指的是我们图中说来的N节点&#xff0c;p指的是图中的M&#xff0c;如果p是黑色节点&#xff0c;则删除p后需要重新进行树的平衡处理。

Get

eb54bdb9d9e6c05fc1eec48d198c54dc.png

TreeMap的查找流程和二叉查找树的查找流程是一样的&#xff0c;这里是从根节点开始查找&#xff0c;根据比较结果决定是下一步是从左子树开始查找&#xff0c;还是从右子树开始查找。

15201480058



推荐阅读
  • 本文介绍了iOS数据库Sqlite的SQL语句分类和常见约束关键字。SQL语句分为DDL、DML和DQL三种类型,其中DDL语句用于定义、删除和修改数据表,关键字包括create、drop和alter。常见约束关键字包括if not exists、if exists、primary key、autoincrement、not null和default。此外,还介绍了常见的数据库数据类型,包括integer、text和real。 ... [详细]
  • 阿,里,云,物,联网,net,core,客户端,czgl,aliiotclient, ... [详细]
  • Spring特性实现接口多类的动态调用详解
    本文详细介绍了如何使用Spring特性实现接口多类的动态调用。通过对Spring IoC容器的基础类BeanFactory和ApplicationContext的介绍,以及getBeansOfType方法的应用,解决了在实际工作中遇到的接口及多个实现类的问题。同时,文章还提到了SPI使用的不便之处,并介绍了借助ApplicationContext实现需求的方法。阅读本文,你将了解到Spring特性的实现原理和实际应用方式。 ... [详细]
  • 本文详细介绍了Java中vector的使用方法和相关知识,包括vector类的功能、构造方法和使用注意事项。通过使用vector类,可以方便地实现动态数组的功能,并且可以随意插入不同类型的对象,进行查找、插入和删除操作。这篇文章对于需要频繁进行查找、插入和删除操作的情况下,使用vector类是一个很好的选择。 ... [详细]
  • Oracle优化新常态的五大禁止及其性能隐患
    本文介绍了Oracle优化新常态中的五大禁止措施,包括禁止外键、禁止视图、禁止触发器、禁止存储过程和禁止JOB,并分析了这些禁止措施可能带来的性能隐患。文章还讨论了这些禁止措施在C/S架构和B/S架构中的不同应用情况,并提出了解决方案。 ... [详细]
  • 本文讨论了一个关于cuowu类的问题,作者在使用cuowu类时遇到了错误提示和使用AdjustmentListener的问题。文章提供了16个解决方案,并给出了两个可能导致错误的原因。 ... [详细]
  • 使用在线工具jsonschema2pojo根据json生成java对象
    本文介绍了使用在线工具jsonschema2pojo根据json生成java对象的方法。通过该工具,用户只需将json字符串复制到输入框中,即可自动将其转换成java对象。该工具还能解析列表式的json数据,并将嵌套在内层的对象也解析出来。本文以请求github的api为例,展示了使用该工具的步骤和效果。 ... [详细]
  • 图解redis的持久化存储机制RDB和AOF的原理和优缺点
    本文通过图解的方式介绍了redis的持久化存储机制RDB和AOF的原理和优缺点。RDB是将redis内存中的数据保存为快照文件,恢复速度较快但不支持拉链式快照。AOF是将操作日志保存到磁盘,实时存储数据但恢复速度较慢。文章详细分析了两种机制的优缺点,帮助读者更好地理解redis的持久化存储策略。 ... [详细]
  • 基于事件驱动的并发编程及其消息通信机制的同步与异步、阻塞与非阻塞、IO模型的分类
    本文介绍了基于事件驱动的并发编程中的消息通信机制,包括同步和异步的概念及其区别,阻塞和非阻塞的状态,以及IO模型的分类。同步阻塞IO、同步非阻塞IO、异步阻塞IO和异步非阻塞IO等不同的IO模型被详细解释。这些概念和模型对于理解并发编程中的消息通信和IO操作具有重要意义。 ... [详细]
  • Tomcat/Jetty为何选择扩展线程池而不是使用JDK原生线程池?
    本文探讨了Tomcat和Jetty选择扩展线程池而不是使用JDK原生线程池的原因。通过比较IO密集型任务和CPU密集型任务的特点,解释了为何Tomcat和Jetty需要扩展线程池来提高并发度和任务处理速度。同时,介绍了JDK原生线程池的工作流程。 ... [详细]
  • ALTERTABLE通过更改、添加、除去列和约束,或者通过启用或禁用约束和触发器来更改表的定义。语法ALTERTABLEtable{[ALTERCOLUMNcolu ... [详细]
  • Java学习笔记之面向对象编程(OOP)
    本文介绍了Java学习笔记中的面向对象编程(OOP)内容,包括OOP的三大特性(封装、继承、多态)和五大原则(单一职责原则、开放封闭原则、里式替换原则、依赖倒置原则)。通过学习OOP,可以提高代码复用性、拓展性和安全性。 ... [详细]
  • Java在运行已编译完成的类时,是通过java虚拟机来装载和执行的,java虚拟机通过操作系统命令JAVA_HOMEbinjava–option来启 ... [详细]
  • 本文讨论了在openwrt-17.01版本中,mt7628设备上初始化启动时eth0的mac地址总是随机生成的问题。每次随机生成的eth0的mac地址都会写到/sys/class/net/eth0/address目录下,而openwrt-17.01原版的SDK会根据随机生成的eth0的mac地址再生成eth0.1、eth0.2等,生成后的mac地址会保存在/etc/config/network下。 ... [详细]
  • Python SQLAlchemy库的使用方法详解
    本文详细介绍了Python中使用SQLAlchemy库的方法。首先对SQLAlchemy进行了简介,包括其定义、适用的数据库类型等。然后讨论了SQLAlchemy提供的两种主要使用模式,即SQL表达式语言和ORM。针对不同的需求,给出了选择哪种模式的建议。最后,介绍了连接数据库的方法,包括创建SQLAlchemy引擎和执行SQL语句的接口。 ... [详细]
author-avatar
fan9210729
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有