本文介绍的相关树结构主要有:二分搜索树、AVL树、2-3树、红黑树、二叉堆、线段树、字典树、并查集
知识点基于慕课网liuyubobobo讲师的视频
1. 相关概念
- 满二叉树:除最后一层无任何子节点外,每一层上的所有结点都有两个子结点的二叉树。
- 完全二叉树:对于深度为k的,有n个结点的二叉树,当其层序编号(按层来编号)与深度k的满二叉树的层序编号相同时,称为完全二叉树。所有的叶子节点都出现在最下面两层。其节点数的取值范围为:2^(k-1)-1 < n <= 2^k-1
- 平衡二叉树:它是一棵空树或它的左右两个子树的高度差的绝对值不超过1,并且左右两个子树都是一棵平衡二叉树
- 二叉树的性质(参考资料):
- 在第i层上最多有2^(i-1)个节点
- 深度为k的二叉树最多一共有2^k-1个节点
- 设度为0(即叶子节点)的节点数为n0,度为2的节点数为n2,则有n0 = n2 + 1
- 具有n个节点的完全二叉树的深度为 ⌊㏒2(n)⌋ ,即log 2为底n的对数再向下取整
2. 二分搜索树
- 二分搜索树(Binary Search Tree)的各个节点的值:大于其左子树的所有节点的值,小于大于其右子树的所有节点的值
- 二分搜索树的子树也是二分搜索树
- 存储的数据要有可比性
- 中序遍历的结果则是按小到大排序
- 对于二分搜索树来说,添加、查找、删除的是与它的最大深度k有关的,就是对于这些操作最多也只需要遍历k次。假如是满二叉树,n = 2^k - 1,k = ㏒2(n+1),时间复杂度为O(㏒2(n)),也就是O(㏒n),因为以多少为底都是无关紧要的
- 假如添加元素的顺序不恰当,就会使树退化成链表,而AVL树可以解决这个问题
3. AVL树
- AVL树本质上是具有平衡功能的二分搜索树
- 平衡因子:左右子树的高度差
- 只要某节点的平衡因子大于1,而该树就出现了不平衡现象,就要通过左旋或右旋来解决,共有四种情况:LL、RR、LR、RL
假如AVL树的节点结构如下所示:
private class Node {
public K key;
public V value;
// 以该节点作为根的树的高度
public int height;
public Node left, right;
public Node(K key, V value) {
this.key = key;
this.value = value;
left = null;
right = null;
height = 1;
}
}
// 根节点
private Node root;
// 树大小
private int size;
先定义一些辅助方法,如下所示:
// 查找以node为根的树的最小节点
private Node searchMinNode(Node node) {
if (node.left == null) {
return node;
} else {
return searchMinNode(node.left);
}
}
// 获取平衡因子,可根据正负来判断左右子树谁高谁低
private int getBalanceFactor(Node node) {
if (node != null) {
return getHeight(node.left) - getHeight(node.right);
}
return 0;
}
// 获取以node为根的树的高度
private int getHeight(Node node) {
if (node != null) {
return node.height;
}
return 0;
}
// 对节点y进行向右旋转操作,返回旋转后新的根节点x
// y x
// / \ / \
// x T4 向右旋转 (y) z y
// / \ - - - - - - - -> / \ / \
// z T3 T1 T2 T3 T4
// / \
// T1 T2
private Node rightRotate(Node y) {
Node x = y.left;
Node T3 = x.right;
x.right = y;
y.left = T3;
y.height = 1 + Math.max(y.left.height, y.right.height);
x.height = 1 + Math.max(x.left.height, x.right.height);
return x;
}
// 对节点y进行向左旋转操作,返回旋转后新的根节点x
// y x
// / \ / \
// T1 x 向左旋转 (y) y z
// / \ - - - - - - - -> / \ / \
// T2 z T1 T2 T3 T4
// / \
// T3 T4
private Node leftRotate(Node y) {
Node x = y.right;
Node T2 = x.left;
x.left = y;
y.right = T2;
y.height = Math.max(getHeight(y.left), getHeight(y.right)) + 1;
x.height = Math.max(getHeight(x.left), getHeight(x.right)) + 1;
return x;
}
3.1 添加操作
添加了某节点后,不平衡现象的只会发生在该节点的父辈和祖父辈上(从该节点到根节点的这条路径上所经过的节点),就要对这些节点进行平衡因子的判断并调整平衡。删除元素时同理
public void add(K key, V value) {
root = addNode(root, key, value);
}
private Node addNode(Node node, K key, V value) {
if (node == null) {
size++;
return new Node(key, value);
} else {
if (key.compareTo(node.key) < 0) {
node.left = addNode(node.left, key, value);
} else if (key.compareTo(node.key) > 0) {
node.right = addNode(node.right, key, value);
} else {
node.value = value;
}
node.height = 1 + Math.max(getHeight(node.left), getHeight(node.right));
int balanceFactor = getBalanceFactor(node);
// LL
if (balanceFactor > 1 && getBalanceFactor(node.left) >= 0) {
return rightRotate(node);
}
// RR
if (balanceFactor < -1 && getBalanceFactor(node.right) <= 0) {
return leftRotate(node);
}
// LR
if (balanceFactor > 1 && getBalanceFactor(node.left) < 0) {
node.left = leftRotate(node.left);
return rightRotate(node);
}
// RL
if (balanceFactor < -1 && getBalanceFactor(node.right) > 0) {
node.right = rightRotate(node.right);
return leftRotate(node);
}
return node;
}
}
3.2 删除操作
public void remove(K key) {
root = removeNode(root, key);
}
private Node removeNode(Node node, K e) {
// 没有找到该元素
if (node == null) {
return null;
}
Node retNode;
if (e.compareTo(node.key) < 0) {
node.left = removeNode(node.left, e);
retNode = node;
} else if (e.compareTo(node.key) > 0) {
node.right = removeNode(node.right, e);
retNode = node;
} else {
// 待删节点的左子树为空,或者待删节点是叶子节点
if (node.left == null) {
Node rightNode = node.right;
node.right = null;
size--;
retNode = rightNode;
} else if (node.right == null) {
// 待删节点的右子树为空
Node leftNode = node.left;
node.left = null;
size--;
retNode = leftNode;
} else {
// 待删节点的左右子树均不为空
// 1. 先找出比待删节点稍大的节点,称为后继者
Node successor = searchMinNode(node.right);
// 2.1. 先删除后继者原来位置的那个节点
// 2.2. 再把待删节点替换为后继者
successor.right = removeNode(node.right, successor.key);
successor.left = node.left;
node.right = node.left = null;
retNode = successor;
}
}
// 当删除的节点是叶子节点时,retNode将会是null,此时直接返回null
if (retNode == null) {
return null;
}
retNode.height = 1 + Math.max(getHeight(retNode.left), getHeight(retNode.right));
int balanceFactor = getBalanceFactor(retNode);
// LL
if (balanceFactor > 1 && getBalanceFactor(retNode.left) >= 0) {
return rightRotate(retNode);
}
// RR
if (balanceFactor < -1 && getBalanceFactor(retNode.right) <= 0) {
return leftRotate(retNode);
}
// LR
if (balanceFactor > 1 && getBalanceFactor(retNode.left) < 0) {
retNode.left = leftRotate(retNode.left);
return rightRotate(retNode);
}
// RL
if (balanceFactor < -1 && getBalanceFactor(retNode.right) > 0) {
retNode.right = rightRotate(retNode.right);
return leftRotate(retNode);
}
return retNode;
}
4. 2-3树
-
满足二分搜索树的基本性质
-
每个节点可以存放一个元素或者两个元素,每个节点有2个或者3个孩子
-
2-3树是一种绝对平衡的树,从根节点到叶子节点的节点数量是相同的
-
2-3树在添加元素的同时也要满足绝对平衡
例子1:根节点为12,往根节点添加元素6
例子2:紧接例子1,再添加元素2,此时融合后的根节点暂时存了3个元素,于是拆分
例子3:当融合后的元素个数是3且是叶子节点时,则先拆分再向父节点融合。如下图往第一棵树添加元素4
例子4:当向父节点融合后,父节点存了3个元素,则父节点再拆分。如下图往第一棵树添加元素4
5. 红黑树
红黑树与2-3树相似,它们节点对应关系如下所示
于是红黑树有以下性质:
- 根节点是黑色的
- 每一个**叶子节点(这里的叶子节点指的是最后的空节点)**是黑色的
- 如果一个节点是红色的,那么它的孩子节点是黑色的
- 黑色节点的右孩子一定是黑色的
- 从任意一个节点到叶子节点,经过的黑色节点是一样的,即黑平衡
5.1 添加操作
最复杂的添加有三种情况
-
如下图所示,当初始状态如①所示,当添加的元素比红色节点的大时,顺序则是①②③④⑤
-
若添加的元素比红色节点的小时,则是直接从①跳到③
-
若添加到元素比黑色节点的大时,则是直接从①跳到④
public class RedBlackTree<K extends Comparable<K>, V> {
private static final boolean RED = true;
private static final boolean BLACK = false;
private class Node {
public K key;
public V value;
public int height;
public boolean color;
public Node left, right;
public Node(K key, V value) {
this.key = key;
this.value = value;
left = null;
right = null;
color = RED;
}
}
private Node root;
private int size;
public boolean isRed(Node node) {
if (node == null) {
return BLACK;
}
return node.color;
}
public void add(K key, V value) {
root = addNode(root, key, value);
root.color = BLACK;
}
/**
* 添加元素时,新元素的初始状态是红色,然后再根据情况改变颜色
*/
private Node addNode(Node node, K key, V value) {
if (node == null) {
size++;
return new Node(key, value);
}
if (key.compareTo(node.key) < 0) {
node.left = addNode(node.left, key, value);
} else if (key.compareTo(node.key) > 0) {
node.right = addNode(node.right, key, value);
} else {
node.value = value;
}
if (isRed(node.right) && !isRed(node.left)) {
node = leftRotate(node);
}
if (isRed(node.left) && isRed(node.left.left)) {
node = rightRotate(node);
}
if (isRed(node.left) && isRed(node.right)) {
flipColor(node);
}
return node;
}
// node x
// / \ / \
// T1 x 左旋转 node T3
// / \ - - - - - - - -> / \
// T2 T3 T1 T2
private Node leftRotate(Node node) {
Node x = node.right;
node.right = x.left;
x.left = node;
x.color = node.color;
node.color = RED;
return x;
}
// node x
// / \ / \
// x T3 右旋转 T1 node
// / \ - - - - - - - -> / \
// T1 T2 T2 T3
private Node rightRotate(Node node) {
Node x = node.left;
node.left = x.right;
x.right = node;
x.color = node.color;
node.color = RED;
return x;
}
// 翻转颜色
private void flipColor(Node node) {
node.color = RED;
node.left.color = BLACK;
node.right.color = BLACK;
}
}
5.2 与AVL树比较
- 当查询的频率高时,AVL树性能好
- 红黑树的最大高度可以是2㏒n(一直沿着红色节点走),并不满足平衡二叉树的定义,只是黑平衡
- 添加和删除操作比AVL树好,综合性能更好
6. 二叉堆
- 二叉堆是一颗完全二叉树
- 堆中的某节点的值不大于其父节点的值,即根节点的值就是最大值,这种堆称为最大堆。反过来定义则是最小堆。注意,节点的值的大小与其所在层数是没有关系的
- 当使用数组存储二叉堆时,根节点放在索引0位置,若某节点的索引为 i ,则它的左子树和右子树的索引分别为 i*2+1 以及 i*2+2,它的父节点索引为 ⌊(i - 1)/2⌋
- 基于这种堆可以十分简单地实现优先队列
- Java提供的优先队列PriorityQueue是最小堆
假如堆是基于动态数组实现的,自定义一些辅助方法:
// 获取父节点索引
private int getParent(int index) {
if (index != 0) {
return (index - 1) / 2;
} else {
throw new IllegalArgumentException("Root doesn't have parent.");
}
}
// 获取左子树索引
private int getLeftChild(int index) {
return index * 2 + 1;
}
// 获取右子树索引
private int getRightChild(int index) {
return index * 2 + 2;
}
6.1 添加操作
public void add(E e) {
// 往数组末尾添加元素
array.addLast(e);
// 上浮操作,使其满足堆的性质:堆中的某节点的值不大于其父节点的值
siftUp(getSize() - 1);
}
// 对某个索引的值进行上浮操作
private void siftUp(int index) {
while (index > 0 && array.get(getParent(index)).compareTo(array.get(index)) < 0) {
// 交换数组中的两个索引的值
array.exchange(index, getParent(index));
index = getParent(index);
}
}
6.2 删除操作
// 取出最大元素,并从堆中删除
public E extractMax() {
E result = peekMax();
// 交换第一个元素(根节点)和末尾元素
array.exchange(0, getSize() - 1);
// 删除末尾元素
array.removeLast();
// 对第一个元素进行下沉操作
siftDown(0);
return result;
}
public E peekMax() {
if (!isEmpty()) {
return array.get(0);
} else {
throw new IllegalArgumentException("This is an empty heap.");
}
}
// 对某个索引的值进行下沉操作
private void siftDown(int index) {
while (getLeftChild(index) < getSize()) {
int max = getLeftChild(index);
// 如果当前节点有右孩子 并且 右孩子大于左孩子
if (max + 1 < getSize()
&& array.get(max + 1).compareTo(array.get(max)) > 0) {
max = max + 1;
}
// 此时max索引的值是左右孩子当中的最大值
if (array.get(index).compareTo(array.get(max)) < 0) {
array.exchange(index, max);
index = max;
} else {
break;
}
}
}
7. 线段树(区间树)
- 二叉树的每个节点存储的都是某个区间范围的信息,叶子节点可以只存储一个元素的信息,也可以存储某个更小区间的信息
- 假如总区间有n个元素,那么使用数组存储线段树时,好的情况需要2*n的空间;坏情况需要4*n的空间,即当n不是2的次幂时,就会造成某些节点的左子树存了一个元素的信息,右子树却存了两个元素的信息,这种情况就需要4*n的空间
简单实现的代码如下所示:
// 融合器
public interface Merger<E> {
E merger(E a, E b);
}
// 线段树
public class SegmentTree<E> {
// 存放原始数据
private E[] data;
// 真正的线段树
private E[] tree;
private Merger<E> merger;
public SegmentTree(E[] arr, Merger<E> merger) {
data = (E[]) new Object[arr.length];
for (int i = 0; i < data.length; i++) {
data[i] = arr[i];
}
this.merger = merger;
tree = (E[]) new Object[4 * arr.length];
buildSegmentTree(0, 0, data.length - 1);
}
public int getSize() {
return data.length;
}
public E get(int index) {
if (index < 0 || index >= data.length) {
throw new IllegalArgumentException("Illegal parameter 'index'.");
}
return data[index];
}
public void set(int index, E e) {
if (index < 0 || index >= data.length) {
throw new IllegalArgumentException("Illegal parameter 'index'.");
} else {
data[index] = e;
update(0, 0, data.length - 1, index, e);
}
}
private void update(int treeIndex, int l, int r, int index, E e) {
if (l == r) {
tree[treeIndex] = e;
return;
} else {
int mid = l + (r - l) / 2;
int leftChildIndex = getLeftChild(treeIndex);
int rightChildIndex = getRightChild(treeIndex);
if (index >= mid + 1) {
update(rightChildIndex, mid + 1, r, index, e);
} else {
update(leftChildIndex, l, mid, index, e);
}
tree[treeIndex] = merger.merger(tree[leftChildIndex], tree[rightChildIndex]);
}
}
/**
* 查询区间为[queryLeft, queryRight]的信息
*/
public E query(int queryLeft, int queryRight) {
if (queryLeft > queryRight || queryLeft < 0 || queryLeft >= data.length
|| queryRight >= data.length) {
throw new IllegalArgumentException("Illegal parameter 'left' or 'right'.");
} else {
return queryFromChild(0, 0, data.length - 1, queryLeft, queryRight);
}
}
private E queryFromChild(int index, int l, int r, int queryLeft, int queryRight) {
if (l == queryLeft && r == queryRight) {
return tree[index];
} else {
int leftChildIndex = getLeftChild(index);
int rightChildIndex = getRightChild(index);
// 这种写法是避免l+r的结果会越界
int mid = l + (r - l) / 2;
if (mid < queryLeft) {
return queryFromChild(rightChildIndex, mid + 1, r, queryLeft, queryRight);
} else if (mid >= queryRight) {
return queryFromChild(leftChildIndex, l, mid, queryLeft, queryRight);
} else {
E leftResult = queryFromChild(leftChildIndex, l, mid, queryLeft, mid);
E rightResult = queryFromChild(rightChildIndex, mid + 1, r, mid + 1, queryRight);
return merger.merger(leftResult, rightResult);
}
}
}
/**
* 创建线段树,在tree[index]处,存放的是区间为data[l,r]的信息
*/
private void buildSegmentTree(int index, int l, int r) {
if (l == r) {
tree[index] = data[l];
return;
} else {
int leftChildIndex = getLeftChild(index);
int rightChildIndex = getRightChild(index);
// 这种写法是避免l+r的结果会越界
int mid = l + (r - l) / 2;
buildSegmentTree(leftChildIndex, l, mid);
buildSegmentTree(rightChildIndex, mid + 1, r);
// 根据融合器来操作区间元素,如求和,求最大值等
// 不能求均值,这里的代码逻辑不符合数学上的逻辑
tree[index] = merger.merger(tree[leftChildIndex], tree[rightChildIndex]);
}
}
private int getLeftChild(int index) {
return 2 * index + 1;
}
private int getRightChild(int index) {
return 2 * index + 2;
}
}
8. 字典树
字典树常用于处理字符串,并不是一个二叉树,如下图所示的字典树中,就存储了"cat"、"dog"、"pan"和"panda"
简单实现的代码如下所示:
public class Trie {
private Node root;
private int size;
private class Node {
/**
* 判断从根到该节点能否组成一个单词
*/
public boolean isWord;
public TreeMap<Character, Node> branch;
public Node(boolean isWord) {
this.isWord = isWord;
branch = new TreeMap<>();
}
public Node() {
this(false);
}
}
public Trie() {
root = new Node();
size = 0;
}
public int getSize() {
return size;
}
/**
* 向字典树中添加一个单词
*/
public void add(String word) {
Node cur = root;
for (int i = 0; i < word.length(); i++) {
char c = word.charAt(i);
if (cur.branch.get(c) == null) {
cur.branch.put(c, new Node());
}
cur = cur.branch.get(c);
}
// 以前不存在该单词,则size++
if (!cur.isWord) {
cur.isWord = true;
size++;
}
}
/**
* 判断字典树中是否存在某单词
*/
public boolean contains(String word) {
Node cur = root;
for (int i = 0; i < word.length(); i++) {
cur = cur.branch.get(word.charAt(i));
if (cur == null) {
return false;
}
}
return cur.isWord;
}
/**
* 查询是否存在以特定字符串为前缀的单词
*/
public boolean containsPrefix(String prefix) {
Node cur = root;
for (int i = 0; i < prefix.length(); i++) {
cur = cur.branch.get(prefix.charAt(i));
if (cur == null) {
return false;
}
}
return true;
}
}
9. 并查集
有这么一类问题,假如A和B是朋友,B和C是朋友,那么A和C也是朋友,即“朋友”关系具有传递性。那么假如有一大群人,如何快速判断任意两个人是不是朋友关系呢?这时就需要并查集数据结构。而且判断一张图是否存在环也需要用上并查集。
并查集与一般树结构不同的地方在于:子节点指向父节点
9.1 实现思路
- 初始化时,让每个元素的父节点都指向自己
- 连接某两个元素A和B时,可以先递归找到A和B的根节点,然后让A的根节点指向B的根节点
- 对于任意两个元素,只需判断它们的根节点是否相同,就能得出这两个节点是否有连接关系
9.2 实现代码
public class UnionFind {
/**
* 记录各个元素的父节点
* 1.若parent[i] = j, 则说明i的父节点是j
* 2.若parent[i] = i, 则说明此时i是根节点
*/
private int[] parent;
public UnionFind(int capacity) {
parent = new int[capacity];
// 初始时,每个节点都是一颗独立的树
for (int i = 0; i < capacity; i++) {
parent[i] = i;
}
}
public int getCapacity() {
return parent.length;
}
public boolean isConnected(int e1, int e2) {
validateElement(e1);
validateElement(e2);
return findRoot(e1) == findRoot(e2);
}
public void union(int e1, int e2) {
validateElement(e1);
validateElement(e2);
int root1 = findRoot(e1);
int root2 = findRoot(e2);
parent[root2] = root1;
}
private int findRoot(int e) {
if (parent[e] == e) {
return e;
}
// 路径压缩
parent[e] = findRoot(parent[e]);
return parent[e];
}
private void validateElement(int e) {
if (e < 0 || e >= parent.length) {
throw new IllegalArgumentException("Out of bound!");
}
}
}