树
- 树的定义:树是一种数据结构,它是由n(n>=1)个有限结点组成的一个具有层次关系的集合。把它叫做“树”是因为它看起来像一棵倒挂的树,也就是说它是根朝上,而叶朝下的。它具有以下的特点:每个结点有零个或多个子结点;没有父结点的结点称为根结点;每一个非根结点有且只有一个父结点;除了根结点外,每个子结点可以分为多个不相交的子树;
- 树的大小:节点的数量;
- 树中一个节点的深度:该节点到根节点的路径上的链接数;
- 树的高度:所有节点中的最大深度;
- 根节点的深度:0,所以只含根节点的树的高度为0。
二叉搜索树
二分查找定理:在N个键的有序数组中进行二分查找最多需要(lgN+1)次比较(无论是否成功)。查找失败时,至少需要lgN次,成功最多需要(lgN+1)次。
二叉树的介绍
-
二叉搜索树的特点:左子树的节点值比父亲节点小,而右子树的节点值比父节点大。
-
满二叉树:除最后一层无任何子节点外,每一层上的所有结点都有两个子结点的二叉树。
-
完全二叉树:n = n0 + n1 + n2; n0 = n2 + 1;n1 = 0 or 1。
-
完全二叉树的高度:N个节点,高度为
$\lfloor lgN \rfloor$
-
二叉树复杂度:所有操作在最坏情况下所需的时间都和树的高度成正比。但在N个点的二叉树中,查找和插入的运行时间增长数量级最坏为N,平均约为1.09lgN。
二叉树的遍历
先序遍历(DFS)
遍历顺序规则为: 根左右;两种实现:递归 or 栈
中序遍历(DFS的变种)
遍历顺序规则为: 左根右
后序遍历(DFS的变种)
遍历顺序规则为: 左右根
层次遍历(BFS)
遍历顺序规则为: 从左到右按层;借助队列实现
Show me the code
:
import java.util.Comparator;
import java.util.LinkedList;
class BinarySearchTree<T> {
private class BSTNode<U> {
private U value;
private BSTNode<U> parent;
private BSTNode<U> left;
private BSTNode<U> right;
public BSTNode(U value, BSTNode<U> parent, BSTNode<U> left, BSTNode<U> right) {
this.value = value;
this.parent = parent;
this.left = left;
this.right = right;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (!(o instanceof BSTNode)) return false;
BSTNode<?> bstNode = (BSTNode<?>) o;
if (!value.equals(bstNode.value)) return false;
if (parent != null ? !parent.equals(bstNode.parent) : bstNode.parent != null) return false;
if (left != null ? !left.equals(bstNode.left) : bstNode.left != null) return false;
if (right != null ? !right.equals(bstNode.right) : bstNode.right != null) return false;
return true;
}
@Override
public int hashCode() {
return value.hashCode();
}
}
private BSTNode<T> root;
private int size = 0;
private Comparator<T> cmp ;
public BinarySearchTree(Comparator cmp) {
this.cmp = cmp;
}
// 查找
public BSTNode<T> get(T value) {
return get(value,root);
}
private BSTNode<T> get(T value, BSTNode<T> root) {
if (root == null)
return null;
int compare = cmp.compare(value, root.value);
if (compare == 0)
return root;
else if (compare < 0)
return get(value, root.left);
else
return get(value, root.right);
}
private void put(T value, BSTNode<T> root) {
BSTNode<T> temp = root;
BSTNode<T> tempParent = temp;
while (temp != null) {
tempParent = temp;
int compare = cmp.compare(value, temp.value);
if (compare < 0)
temp = temp.left;
else if (compare > 0)
temp = temp.right;
else {
System.out.println("Nodes already exist, duplicate insertion is prohibited, doing nothing!");
return;
}
}
assert tempParent != null;
if (cmp.compare(value,tempParent.value) < 0)
tempParent.left = new BSTNode<>(value,tempParent,null,null);
else
tempParent.right = new BSTNode<>(value,tempParent,null,null);
size++;
}
// 插入
public void put(T value) {
if (root == null) {
root = new BSTNode<T>(value,null,null,null);
size++;
}else{
put(value, root);
}
}
private void transplant(BSTNode<T> nodeToBeDelete, BSTNode<T> childNode) {
if (nodeToBeDelete.parent.left == nodeToBeDelete)
nodeToBeDelete.parent.left = childNode;
else
nodeToBeDelete.parent.right = childNode;
if (childNode != null)
childNode.parent = nodeToBeDelete.parent;
}
/*
删除一个节点:
1. 被删除节点没有子树的情况,直接删除,并修改对应父节点的指针为空。
2. 对于只有一个子树的情况,考虑将其子树作为其父节点的子树,根据被删除的节点确定:
如果被删除的节点是其父节点的左子树,则其子树作为左子树连到其父节点;
如果被删除的节点是其父节点的右子树,则其子树作为右子树连到其父节点。
3. 有两个子树的情况,可以考虑两种方法:
用被删除节点A的右子树的最左节点或者A的左子树的最右节点作为替代A的节点,并修改相应的最左或最右节点的父节点的指针。
*/
public void delete(T value) {
BSTNode<T> nodeToBeDeleted = get(value);
if (nodeToBeDeleted == null){
System.out.println("The point to be deleted does not exist!");
return;
}
if (nodeToBeDeleted.left == null && nodeToBeDeleted.right == null) // 叶子节点
transplant(nodeToBeDeleted,null);
else if (nodeToBeDeleted.left == null) // 左子树为空
transplant(nodeToBeDeleted, nodeToBeDeleted.right);
else if (nodeToBeDeleted.right == null) // 右子树为空
transplant(nodeToBeDeleted, nodeToBeDeleted.left);
else { // 左右子树都不为空:右子树最左点替换
BSTNode<T> minModeOfRight = getMinNode(nodeToBeDeleted.right);
if (minModeOfRight == nodeToBeDeleted.right) { // 是删除节点的直接右孩子
transplant(nodeToBeDeleted,minModeOfRight);
minModeOfRight.left = nodeToBeDeleted.left;
nodeToBeDeleted.left.parent = minModeOfRight;
} else { // 不是删除节点的直接右孩子
transplant(minModeOfRight,minModeOfRight.right);
transplant(nodeToBeDeleted,minModeOfRight);
minModeOfRight.left = nodeToBeDeleted.left;
nodeToBeDeleted.left.parent = minModeOfRight;
minModeOfRight.right = nodeToBeDeleted.right;
nodeToBeDeleted.right.parent = minModeOfRight;
}
}
size--;
}
private BSTNode<T> getMaxNode(BSTNode<T> root) {
BSTNode<T> max = root;
while (max.right != null) {
max = max.right;
}
return max;
}
public T getMax() {
return getMaxNode(root).value;
}
private BSTNode<T> getMinNode(BSTNode<T> root) {
BSTNode<T> min = root;
while (min.left != null) {
min = min.left;
}
return min;
}
public T getMin() {
return getMinNode(root).value;
}
// 先序遍历(DFS) -- 递归实现
public void preOrderTraversal() {
preOrderTraversal(root);
}
private void preOrderTraversal(BSTNode<T> root) {
if (root == null)
return;
System.out.print(root.value + " ");
preOrderTraversal(root.left);
preOrderTraversal(root.right);
}
// 中序遍历
public void inOrderTraversal() {
inOrderTraversal(root);
}
private void inOrderTraversal(BSTNode<T> root) {
if (root == null)
return;
inOrderTraversal(root.left);
System.out.print(root.value + " ");
inOrderTraversal(root.right);
}
// 后序遍历
public void postOrderTraversal() {
postOrderTraversal(root);
}
private void postOrderTraversal(BSTNode<T> root) {
if (root == null)
return;
postOrderTraversal(root.left);
postOrderTraversal(root.right);
System.out.print(root.value + " ");
}
// 先序遍历(DFS) -- 栈实现
public void preOrderTraversalStack() {
preOrderTraversalStack(root);
}
private void preOrderTraversalStack(BSTNode<T> root) {
if (root == null)
return;
LinkedList<BSTNode<T>> stack = new LinkedList<>();
BSTNode<T> top = root;
while (!stack.isEmpty() || top != null) {
if (top != null) {
System.out.print(top.value + " "); // 每次入栈输出
stack.push(top);
top = top.left; // 继续往左子树深入
} else { // top == null
top = stack.pop().right; // 出栈往右子树深入
}
}
}
// 层次遍历
public void levelOrderTraversal() {
levelOrderTraversal(root);
}
// 层次遍历(BFS) -- 队列
private void levelOrderTraversal(BSTNode<T> root) {
if (root == null)
return;
LinkedList<BSTNode<T>> queue = new LinkedList<>();
queue.add(root);
BSTNode<T> head;
while (!queue.isEmpty()){
head = queue.remove();
System.out.print(head.value + " ");
if (head.left != null)
queue.add(head.left);
if (head.right != null)
queue.add(head.right);
}
}
public int getSize() {
return size;
}
}
class TestBST {
public static void main(String[] args) {
BinarySearchTree<Integer> bst = new BinarySearchTree<>((Comparator<Integer>) (o1, o2) -> {
if(o1 < o2)
return -1;
else if (o1 > o2)
return 1;
else
return 0;
});
int[] arrayInt = {5,1,9,7,8,4,0,2,3,6};
for (int i : arrayInt) {
bst.put(i);
}
// bst.delete(1);
System.out.println("size: " + bst.getSize() + " Max: " + bst.getMax() + " Min: " + bst.getMin());
System.out.print("PreOrder: "); bst.preOrderTraversal(); // 5 1 0 4 2 3 9 7 6 8
System.out.print("\n" + "PreOrder: "); bst.preOrderTraversalStack(); // 5 1 0 4 2 3 9 7 6 8
System.out.print("\n" + "InOrder: "); bst.inOrderTraversal(); // 0 1 2 3 4 5 6 7 8 9
System.out.print("\n" + "PostOrder: "); bst.postOrderTraversal(); // 0 3 2 4 1 6 8 7 9 5
System.out.print("\n" + "LevelOrder: "); bst.levelOrderTraversal(); // 5 1 9 0 4 7 2 6 8 3
}
}
平衡二叉搜索树(AVL)
-
AVL树的引入:平衡二叉树在二叉排序树上引入的,在二叉树中,如果插入的节点接近有序,那么二叉树就会退化为链表大大降低了查找效率,为了使二叉树无论什么情况下最大限度的接近满二叉树,从而保证它的查找效率,因此引入平衡二叉树。
-
AVL定义:满足二叉搜索树的特性基础上,它的左子树和右子树的高度之差的绝对值不超过1,并且左子树和右子树也是一个平衡二叉树。
-
平衡因子:左子树高度减去右子树的高度的值或者右子树高度减去左子树高度的值。显然 -1 <=bf <= 1,下面分析假设右减左。
-
AVL树的实现:如何保证二叉树在任何情况下都能最大限度接近满二叉树,从而保证它的查找效率呢?那就是旋转,当平衡因子的绝对值大于1的时候,我们就需要对其进行旋转。
参考:
https://blog.csdn.net/jyy305/article/details/70949010
https://mp.weixin.qq.com/s/dYP5-fM22BgM3viWg4V44A
在插入的过程中,会出现以下四种情况破坏AVL树的特性,我们可以采取如下相应的旋转。
- 左-左型:做右旋;
- 右-右型:做左旋;
- 左-右型:先做左旋转成左-左型,再做右旋;
- 右-左型:先做右旋转成右-右型,再做左旋。
Show me the code
:
// 定义节点
class AvlNode {
int data;
AvlNode lchild;//左孩子
AvlNode rchild;//右孩子
int height;//记录节点的高度
}
//在这里定义各种操作
public class AVLTree{
//计算节点的高度
static int height(AvlNode T) {
if (T == null) {
return -1;
}else{
return T.height;
}
}
//左左型,右旋操作,k2代表bf为-2的根节点,即最大不平衡树的根节点(下同)
static AvlNode R_Rotate(AvlNode K2) {
AvlNode K1;
//进行旋转
K1 = K2.lchild;
K2.lchild = K1.rchild;
K1.rchild = K2;
//重新计算节点的高度
K2.height = Math.max(height(K2.lchild), height(K2.rchild)) + 1;
K1.height = Math.max(height(K1.lchild), height(K1.rchild)) + 1;
return K1;
}
//右右型,左旋操作k2代表bf为2的根节点,即最大不平衡树的根节点
static AvlNode L_Rotate(AvlNode K2) {
AvlNode K1;
K1 = K2.rchild;
K2.rchild = K1.lchild;
K1.lchild = K2;
//重新计算高度
K2.height = Math.max(height(K2.lchild), height(K2.rchild)) + 1;
K1.height = Math.max(height(K1.lchild), height(K1.rchild)) + 1;
return K1;
}
//左-右型,进行左旋,再右旋,k3代表bf为-2的根节点,即最大不平衡树的根节点
static AvlNode L_R_Rotate(AvlNode K3) {
//先对其孩子进行左旋
K3.lchild =L_Rotate(K3.lchild);
//再进行右旋
return R_Rotate(K3);
}
//右-左型,先进行右旋,再左旋,k3代表bf为2的根节点,即最大不平衡树的根节点
static AvlNode R_L_Rotate(AvlNode K3) {
//先对孩子进行右旋
K3.rchild = R_Rotate(K3.rchild);
//再进行左旋
return L_Rotate(K3);
}
//插入数值操作
static AvlNode insert(int data, AvlNode T) {
if (T == null) {
T = new AvlNode();
T.data = data;
T.lchild = T.rchild = null;
} else if(data < T.data) {
//向左孩子递归插入
T.lchild = insert(data, T.lchild);
//进行调整操作
//如果左孩子的高度比右孩子大2
if (height(T.lchild) - height(T.rchild) == 2) {
//左-左型
if (data < T.lchild.data) {
T = R_Rotate(T);
} else {
//左-右型
T = R_L_Rotate(T);
}
}
} else if (data > T.data) {
T.rchild = insert(data, T.rchild);
//进行调整
//右孩子比左孩子高度大2
if(height(T.rchild) - height(T.lchild) == 2)
//右-右型
if (data > T.rchild.data) {
T = L_Rotate(T);
} else {
T = L_R_Rotate(T);
}
}
//否则,这个节点已经在树上存在了,我们什么也不做
//重新计算T的高度
T.height = Math.max(height(T.lchild), height(T.rchild)) + 1;
return T;
}
}
红黑二叉搜索树
AVL的问题
虽然平衡树解决了二叉查找树退化为近似链表的缺点,能够把查找时间控制在 O(logn),不过却不是最佳的,因为平衡树要求每个节点的左子树和右子树的高度差至多等于1,这个要求实在是太严了,导致每次进行插入、删除节点的时候,几乎都会破坏平衡树的第二个规则,进而都需要通过不断的左旋和右旋来进行调整。尤其是在动态插入的场景中,平衡树需要频繁着进行调整,这会使它的性能大打折扣,为了解决这个问题,于是有了红黑树。
红黑树定义
红黑树的一种定义方式是含有红黑链接并满足下列条件的二叉搜索树:
- 红链接均为左链接;
- 没有任何一个节点同时与两条红链接相连;
- 该树完美黑色平衡,即任意空链接到根节点的路径上的黑链接数量相同。
每个节点内维护着一个颜色变量,这里红色节点指其父节点指向它的链接的颜色为红色,即红链接指向它;黑色节点指其父节点指向它的链接的颜色是黑色,即黑链接指向它。
红黑树的优点
红黑树在插入、删除等操作,不会像AVL那样频繁着破坏红黑树的规则,所以不需要频繁调整,在插入、删除方面红黑树性能远远优于AVL,这也是我们为什么大多数情况下使用红黑树的原因。单单在查找方面的效率的话,AVL的确比红黑树快。所以,我们也可以说,红黑树是一种不大严格的平衡树,也可以说是一个折中的方案。
红黑树的特性
- 一颗大小为N的红黑树的高度不会超过2lgN:因为黑链接完美平衡,红链接和黑链接交叉链接,所以高度不会超过2lgN。
- 查找和插入的最坏情况下的运行时间增长数量级为2lgN,平均约为1.001lgN。
Show me the code
:
public class RedBlackBST<T extends Comparable<T>> {
private static final boolean RED = true;
private static final boolean BLACK = false;
private static class Node<U> {
private U value;
private Node<U> parent;
private Node<U> left;
private Node<U> right;
// 由其父节点指向它的链接的颜色, 也说红色/黑色节点,这里红色/黑色节点指其父节点指向它的链接的颜色为红色/黑色
private boolean color; // true:RED false:BLACK
public Node(U value, Node<U> parent, Node<U> left, Node<U> right, boolean color) {
this.value = value;
this.parent = parent;
this.left = left;
this.right = right;
this.color = color;
}
}
private Node<T> root;
private int size;
public RedBlackBST() {}
public int getSize() {
return size;
}
public T getRoot() {
return root.value;
}
private boolean isRed(Node<T> node) {
if (node == null)
return false;
return node.color == RED;
}
public boolean isRed(T value) {
Node<T> node = get(value,root);
return isRed(node);
}
public Node<T> get(T value) {
return get(value,root);
}
private Node<T> get(T value, Node<T> root) {
if (root == null) {
System.out.println("The point you are looking for does not exist!");
return null;
}
if (value.compareTo(root.value) < 0)
return get(value, root.left);
else if (value.compareTo(root.value) > 0)
return get(value,root.right);
else
return root;
}
// 左旋:当红链接向右指时(即小指大),左旋变成向左指(即大指小);当出现连续的红链接时,下层左旋
private Node<T> rotateLeft(Node<T> p) {
Node<T> x = p.right;
p.right = x.left;
x.left = p;
x.color = p.color;
p.color = RED;
x.parent = p.parent;
p.parent = x;
return x;
}
// 右旋:当红链接向左指时(即大指小),右旋变成向右指(即小指大);当出现连续的红链接时,上层右旋
private Node<T> rotateRight(Node<T> p) {
Node<T> x = p.left;
p.left = x.right;
x.right = p;
x.color = p.color;
p.color = RED;
x.parent = p.parent;
p.parent = x;
return x;
}
// 颜色变换:即将两个子节点链接颜色->BLACK,父节点链接颜色->RED
private void filpColors(Node<T> p) {
p.color = RED;
p.left.color = BLACK;
p.right.color = BLACK;
}
/* 插入:红黑二叉树规定插入的链接为红链接,然后分情况进行旋转调整,大体可分为以下几种情况
case1: 向树底部的2-节点插入
case1_1: 左插入,父节点直接成为3-节点,无需调整;
case1_2: 右插入,父节点成为一个错误的3-节点,存在向右的红链接,需左旋父节点;
case2: 向树底部的3-节点插入
case2_1: 右插入,得到一个2-3平衡树,只需要进行颜色变换,即将两个子节点链接颜色->BLACK,父节点链接颜色->RED;
case2_2: 左插入,形成两条连续同向的红链接,右旋上层的红链接(父节点)到case2_1,执行case2_1后续操作;
case2_3: 中插入,形成两条连续不同向的红链接,左旋下层的红链接(父节点的左子节点)到case2_2,执行case2_2后续操作。
当然上面 5 种情况可以归纳成 4 种情况,其中case1_1无需调整,不考虑,那么就是 3 种需要调整的情况:
case1: 如果父节点p的右子节点是红色并且左子节点是黑色,进行左旋(p);
case2: 如果父节点p的左子节点是红色并且左子节点的左子节点也是红色,进行右旋(p);
case3: 如果父节点p的左子节点是红色并且右子节点也是红色,进行颜色变换;
end: 最后要设置根节点为黑色节点,每次根节点由红变黑则树的黑链接高度+1。
*/
public void put(T value) {
// 插入,不允许重复,如果已存在会return,每次插入都可能会更新根节点
if (root == null) {
root = new Node<T>(value, null,null,null, BLACK);
size++;
} else {
root = put(root, value, root);
size++;
root.color = BLACK;
}
}
private Node<T> put(Node<T> p, T value, Node<T> parent) {
if (p == null) {
return new Node<T>(value,parent,null,null, RED);
}
int compare = value.compareTo(p.value);
if (compare < 0) {
p.left = put(p.left,value,p);
}else if (compare > 0){
p.right = put(p.right,value,p);
} else {
System.out.println("Nodes already exist, duplicate insertion is prohibited, doing nothing!");
size--;
}
if (isRed(p.right) && !isRed(p.left))
p = rotateLeft(p);
if (isRed(p.left) && isRed(p.left.left))
p = rotateRight(p);
if (isRed(p.left) && isRed(p.right))
filpColors(p);
return p;
}
// 先序遍历(DFS) -- 递归实现
public void preOrderTraversal() {
preOrderTraversal(root);
}
private void preOrderTraversal(Node<T> root) {
if (root == null)
return;
System.out.print(root.value + " ");
preOrderTraversal(root.left);
preOrderTraversal(root.right);
}
}
class TestRedBlackBST {
public static void main(String[] args) {
RedBlackBST<Integer> redBlackBST = new RedBlackBST<>();
int[] arrayInt = {5,1,9,7,8};
for (int i : arrayInt) {
redBlackBST.put(i);
// System.out.println(redBlackBST.getRoot());
}
System.out.println("Size: " + redBlackBST.getSize());
System.out.print("PreOrder: "); redBlackBST.preOrderTraversal(); // 8 5 1 7 9
}
}
总结
时间复杂度 | 最坏情况 | 平均情况 | 最坏情况 | 平均情况 |
---|---|---|---|---|
操作 | 查找 | 查找 | 插入 | 插入 |
顺序查询(无序链表) | N | N/2 | N | N |
二分查找(有序数组) | lgN | lgN | N | N/2 |
二叉树查找(BST) | N | 1.39lgN | N | 1.39lgN |
2-3树查找(红黑树) | 2lgN | 1.001lgN | 2lgN | 1.001lgN |