红黑树是一种近似平衡的二叉查找树
分析之前,先把树的知识回顾一遍。文中实现了二叉查找树,在此基础上参考JDK1.8 TreeMap源码模拟红黑树的实现
二叉树
在链表中,插入、删除速度很快,但查找速度较慢。
在数组中,查找速度很快,但插入删除速度很慢。
二叉树,本质上,是对链表和数组的一个折中
- 二叉树是每个结点最多有两个子树的树结构。
- 它有五种基本形态:二叉树可以是空集;根可以有空的左子树或右子树;或者左、右子树皆为空。
性质
- 性质1:二叉树第i层上的结点数目最多为2i-1(i>=1)
- 性质2:深度为k的二叉树至多有2k-1个结点(k>=1)
- 性质3:包含n个结点的二叉树的高度至少为(log2n)+1
- 性质4:在任意一棵二叉树中,若终端结点的个数为n0,度为2的结点数为n2,则n0=n2+1
满二叉树
高度为h,并且由2^h-1个结点组成的二叉树,称为满二叉树
完全二叉树
- 叶子结点只能出现在最下层和次下层
- 且最下层的叶子结点集中在树的左部
如果一个完全二叉树的结点总数为768个,求叶子结点的个数。
- 二叉树性质:n0=n2+1
- 768=n0+n1+n2
- 完全二叉树度为1的结点个数要么为0,要么为1
- n0=n2+1=384
规律:如果一棵完全二叉树的结点总数为n,那么叶子结点等于(n+1)/2
平衡二叉树(AVL树)
- 它是一棵空树或它的左右两个子树的高度差的绝对值不超过1
- 左右两个子树都是一棵平衡二叉树
这个方案很好的解决了二叉查找树退化成链表的问题,
二叉查找树
- 若任意结点的左子树不空,则左子树上所有结点的值均小于它的根结点的值。
- 任意结点的右子树不空,则右子树上所有结点的值均大于它的根结点的值。
- 任意结点的左、右子树也分别为二叉查找树。
- 没有键值相等的结点。
实现
节点类:
class Node<E> {
Node<E> left = null;
Node<E> right = null;
E data = null;
public Node(E data) {
this.data = data;
}
}
二叉查找树:
方法:插入 查找 遍历 删除
class BinSearchTree<E> implements Comparator<E> {
public Node<E> root = null;
public List<Node<E>> list = null;
public void insert(E value) {
if (root == null) {
root = new Node<E>(value);
return;
}
Node<E> curNode = root;
Node<E> parNode = root;
boolean isLeft = true;
while (curNode != null) {
parNode = curNode;
if (compare(value, curNode.data) == 1) {
isLeft = false;
curNode = curNode.right;
} else if (compare(value, curNode.data) == -1) {
isLeft = true;
curNode = curNode.left;
} else {
System.out.println("Insert error for repeating elements!");
return;
}
}
Node<E> node = new Node<E>(value);
if (isLeft)
parNode.left = node;
else
parNode.right = node;
}
public void find(E value) {
Node<E> curNode = root;
while (curNode != null) {
if (compare(value, curNode.data) == 1)
curNode = curNode.right;
else if (compare(value, curNode.data) == -1) {
curNode = curNode.left;
} else {
System.out.println(value + " found");
return;
}
;
}
System.out.println(value + " Not found");
}
public void preOrder(Node<Object> root) {
if (root == null)
return;
System.out.print(root.data + " ");
preOrder(root.left);
preOrder(root.right);
}
@Override
public int compare(E o1, E o2) {
if ((Integer) o1 > (Integer) o2)
return 1;
else if ((Integer) o1 == (Integer) o2)
return 0;
else
return -1;
}
}
删除
successor方法,寻找后继者
static <K,V> TreeMap.Entry<K,V> successor(Entry<K,V> t) {
if (t == null)
return null;
//从右子树找到最小的节点
else if (t.right != null) {
Entry<K,V> p = t.right;
while (p.left != null)
p = p.left;
return p;
} else {
//右子树为空 找到第一个左父节点
Entry<K,V> p = t.parent;
Entry<K,V> ch = t;
while (p != null && ch == p.right) {
ch = p;
p = p.parent;
}
return p;
}
}
待删除的节点为叶子节点,可直接删除
待删除节点只有一个孩子节点
待删除节点既有左孩子,又有右孩子,需要寻找后继节点
后继节点就是比要删除的节点的关键值要大的节点集合中的最小值。
最大堆
实现
// 根据归纳法 如果两个子树为最大堆 根节点小 则交换 然后向下处理
class MaxHeap<E> implements Comparator<E> {
public List<E> heap;
public void swap(int i, int j) {
if (i != j) {
E temp = heap.get(i);
heap.set(i, heap.get(j));
heap.set(j, temp);
}
}
// build heap
public void buildMaxHeap(List<E> input) {
heap = input;
for (int i = heap.size() / 2 - 1; i >= 0; --i) {
maxHeapify(i);
}
}
// 向下调整堆
public void maxHeapify(int i) {
// 只要有左孩子
while (2 * i + 1 < heap.size()) {
// 默认当前最大
int max = i;
// 取与左孩子之间大者
if (compare(heap.get(2 * i + 1), heap.get(i)) > 0)
max = 2 * i + 1;
// 取与右孩子之间大者
if (2 * i + 2 < heap.size() && compare(heap.get(2 * i + 2), heap.get(max)) > 0)
max = 2 * i + 2;
// 根节点最大
if (max == i)
break;
else {
swap(i, max);
// 处理当前节点的子节点
i = max;
}
}
}
// 插入: 在末尾添加元素 向上调整堆
public void insert(E value) {
heap.add(value);
heapUp(heap.size() - 1);
}
// 向上调整堆
public void heapUp(int i) {
// 如果不为根节点
while (i > 0) {
// 默认当前最大
int max = i;
// 取与父节点之间大者
if (compare(heap.get(i / 2), heap.get(i)) > 0)
max = i / 2;
// 如果当前节点为大
if (max == i) {
swap(i, i / 2);
// 向上处理父节点
i /= 2;
} else
break;
}
}
// 删除元素 将末尾元素移至删除元素的位置 它的值较小 故向下调整 移除末尾元素
public void delete(int index) {
heap.set(index, heap.get(heap.size() - 1));
maxHeapify(index);
heap.remove(heap.size() - 1);
}
// 查处元素
public void find(E value){
int i = 0;
while(i < heap.size()){
if(compare(heap.get(i),value) > 0){
i = 2 * i +1;
}
else if(compare(heap.get(i),value) < 0){
i = 2 * i + 2;
}
else {
System.out.println(value + " found");
return;
}
}
System.out.println(value + " not found");
}
// 堆排序: 依次删除根节点
public void sort(List<E> array) {
buildMaxHeap(array);
while (heap.size() > 1) {
System.out.print(heap.get(0) + " ");
delete(0);
}
}
public void out() {
for (E i : heap) {
System.out.print(i + " ");
}
System.out.println();
}
@Override
public int compare(E o1, E o2) {
if ((Integer) o1 > (Integer) o2)
return 1;
else if ((Integer) o1 < (Integer) o2)
return -1;
else
return 0;
}
}
红黑树
- 红黑树是一种近似平衡的二叉查找树
- 二叉查找树的一般操作的执行时间为O(lgn)
- 若退化成了一棵具有n个结点的线性链后,最坏情况运行时间为O(n)
- 红黑树的查找、插入、删除的时间复杂度最坏为O(log n)
特性
- 每个节点或者是黑色,或者是红色。
- 根节点是黑色。
- 每个叶子节点是黑色。 [注意:这里叶子节点,是值为空的叶子节点]
- 如果一个节点是红色的,则它的子节点必须是黑色的。
- 从一个节点到该节点的子孙节点的所有路径上包含相同数目的黑节点。
性质:
- 一棵含有n个节点的红黑树的高度至多为2log(n+1)
- 从根到叶子的最长的可能路径不多于最短的可能路径的两倍长
当在对红黑树进行插入和删除等操作时可能会破坏红黑树的性质
调整
需要通过调整使得查找树重新满足红黑树的条件。
1.颜色调整
2.结构调整
左旋:
左旋的过程是将x的右子树绕x逆时针旋转,使得x的右子树成为x的父亲
使右子树的左孩子成为x的右孩子
右旋:
Tips:左旋和右旋之后 仍是二叉搜索树
左旋把右孩子移上去
右旋把左孩子移上去
实现
class RedBlackTree<E> implements Comparator<E> {
private static final boolean BLACK = true;
private static final boolean RED = false;
public Node<E> root = null;
private static int size = 0;
插入
在二叉搜索树的基础上进行调整
public void insert(E value) {
if (root == null) {
root = new Node<E>(value, null);
return;
}
Node<E> curNode = root;
Node<E> parNode = root;
boolean isLeft = true;
while (curNode != null) {
// parNode指向上次循环后的节点
parNode = curNode;
if (compare(value, curNode.data) == 1) {
isLeft = false;
curNode = curNode.right;
} else if (compare(value, curNode.data) == -1) {
isLeft = true;
curNode = curNode.left;
} else {
System.out.println("Insert error for repeating elements " + value);
return;
}
}
// 循环结束后 parNode指向要插入的节点 curNode指向null 即待插入的位置
// 新增节点作为子节点 放入待插入的位置
Node<E> node = new Node<E>(value, parNode);
if (isLeft)
parNode.left = node;
else
parNode.right = node;
// 对这棵树进行调整、平衡
fixAfterInsertion(node);
size++;
}
红黑树的核心在于调整算法fixAfterInsertion
调整规则
首先插入节点一定为红色
- 被插入的节点是根节点,直接把此节点涂为黑色。
- 被插入的节点的父节点是黑色,不处理。
被插入的节点的父节点是红色。一定存在祖父节点。
3.1父节点是左孩子,叔节点为红
3.2父节点是左孩子,叔节点为黑,当前为右
3.3父节点是左孩子,叔节点为黑,当前为左
3.1
父、叔节点置黑,违背特性5,故祖父置红。
递归处理祖父节点,若祖父为根节点,置黑返回。
3.2
对父节点进行左旋,此时父左叔黑,当前为左,变成3
3.3
父节点置黑,祖父节点置红,对祖父节点右旋
父节点为右孩子同理。
总结:
- 父左 叔红 —-父叔置黑 祖父置红 当前为祖
- 父左 叔黑 子右—-对父左旋,父节点变成左子节点,变成3,执行3
- 父左 叔黑 子左—-父置黑 祖置红 对父右旋
fixAfterInsertion函数
public void fixAfterInsertion(Node<E> node) {
// 新增节点为红色
node.color = RED;
// 为空或 为根节点 或父节点为黑 满足红黑树性质 返回
while (node != null && node != root && node.parent.color == RED) {
// 如果父节点为左孩子
if (parentOf(node) == leftOf(parentOf(parentOf(node)))) {
// 获取叔节点
Node<E> y = rightOf(parentOf(parentOf(node)));
// 若父节点为红 叔节点为红
if (colorOf(y) == RED) {
// 父节点 叔节点设为黑
setColor(parentOf(node), BLACK);
setColor(y, BLACK);
// 父节点的父节点设为红
setColor(parentOf(parentOf(node)), RED);
// 递归处理node的父节点的父节点
node = parentOf(parentOf(node));
}
// 若父节点为红 叔节点为黑
else {
// 如果node为右子树 对父节点进行左旋
if (node == rightOf(parentOf(node))) {
// 将node的父节点作为node
node = parentOf(node);
rotateLeft(node);
}
// 父节点设为黑
setColor(parentOf(node), BLACK);
// 父节点的父节点设为黑
setColor(parentOf(parentOf(node)), RED);
// 对父节点的父节点右旋
rotateRight(parentOf(parentOf(node)));
}
}
// 如果父节点为右孩子
else {
// 获取叔节点
Node<E> y = leftOf(parentOf(parentOf(node)));
// 若父节点为红 叔节点为红
if (colorOf(y) == RED) {
// 父节点 叔节点设为黑
setColor(parentOf(node), BLACK);
setColor(y, BLACK);
// 父节点的父节点设为红
setColor(parentOf(parentOf(node)), RED);
// 递归处理node的父节点的父节点
node = parentOf(parentOf(node));
}
// 若父节点为红 叔节点为黑
else {
// 如果node为左子树 右旋
if (node == leftOf(parentOf(node))) {
// 将node的父节点作为node
node = parentOf(node);
rotateRight(node);
}
// 父节点设为黑
setColor(parentOf(node), BLACK);
// 父节点的父节点设为黑
setColor(parentOf(parentOf(node)), RED);
// 对父节点的父节点左旋
rotateLeft(parentOf(parentOf(node)));
}
}
}
// 根节点设为黑
root.color = BLACK;
}
remove()
private void deleteEntry(Entry<K,V> p) {
modCount++;
size--;
// If strictly internal, copy successor's element to p and then make p
// point to successor.
if (p.left != null && p.right != null) {
Entry<K,V> s = successor(p);
p.key = s.key;
p.value = s.value;
p = s;
} // p has 2 children
// Start fixup at replacement node, if it exists.
Entry<K,V> replacement = (p.left != null ? p.left : p.right);
if (replacement != null) {
// Link replacement to parent
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;
// Null out links so they are OK to use by fixAfterDeletion.
p.left = p.right = p.parent = null;
// Fix replacement
if (p.color == BLACK)
fixAfterDeletion(replacement);
} else if (p.parent == null) { // return if we are the only node.
root = null;
} else { // No children. Use self as phantom replacement and unlink.
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;
}
}
}
TreeMap
extends AbstractMap<K,V>
implements NavigableMap<K,V>, Cloneable, java.io.Serializable
NavigableMap意味着它支持一系列的导航方法,具备针对给定搜索目标返回最接近匹配项的导航方法 。
//比较器,因为TreeMap是有序的,通过comparator接口我们可以对TreeMap的内部排序进行精密的控制
private final Comparator<? super K> comparator;
//TreeMap红-黑节点,为TreeMap的内部类
private transient Entry<K,V> root = null;
//容器大小
private transient int size = 0;
//TreeMap修改次数
private transient int modCount = 0;
//红黑树的节点颜色--红色
private static final boolean RED = false;
//红黑树的节点颜色--黑色
private static final boolean BLACK = true;
左旋:
private void rotateLeft(Entry<K,V> p) {
if (p != null) {
//获取P的右子节点
Entry<K,V> r = p.right;
//将R的左子树设置为P的右子树
p.right = r.left;
//若R的左子树不为空,则将P设置为R左子树的父亲
if (r.left != null)
r.left.parent = p;
//将P的父亲设置R的父亲
r.parent = p.parent;
//如果P的父亲为空,则将R设置为跟节点
if (p.parent == null)
root = r;
//如果P为其父节点(G)的左子树,则将R设置为P父节点(G)左子树
else if (p.parent.left == p)
p.parent.left = r;
//否则R设置为P的父节点(G)的右子树
else
p.parent.right = r;
//将P设置为R的左子树
r.left = p;
//将R设置为P的父节点
p.parent = r;
}
}
插入
步骤:
- 构建排序二叉树
- 平衡二叉树
规则:
- 插入新节点总是红色节点
- 如果插入节点的父节点是黑色, 能维持性质
- 如果插入节点的父节点是红色, 破坏了性质. 通过重新着色或旋转, 来维持性质
1.为跟节点
新插入的节点N没有父节点,当做根节点插入,设置为黑色
2.父节点为黑色
父节点为黑色,直接插入,设置为红色
3.父节点P和P的兄弟节点U都为红色