二叉查找树 BST : https://blog.csdn.net/cj_286/article/details/90183298
二叉平衡树 AVL : https://blog.csdn.net/cj_286/article/details/90217072
红黑树 RBT : https://blog.csdn.net/cj_286/article/details/90245150
学习二叉树前需要先熟悉数组、链表、栈、队列等线性结构,还需要熟悉二叉树的先、中、后、层四种遍历。
JDK中用到的数据结构类
不可变数组String,动态数组StringBuilder
顺序表ArrayList,双链表LinkedList
队列Queue,栈Stack
哈希表HashMap,二叉树PriorityQueue
红黑树TreeMap
性质
二叉查找树(Binary Search Tree),(又:二叉搜索树,二叉排序树)它或者是一棵空树,或者是具有下列性质的二叉树:
1.若它的左子树不空,则左子树上所有结点的值均小于它的根结点的值;
2.若它的右子树不空,则右子树上所有结点的值均大于它的根结点的值;
3.它的左、右子树也分别为二叉排序树。
4.中序遍历序列为升序
遍历算法 (先序,中序,后序,层序)
1.先(根)序遍历的递归算法
⑴ 访问根结点;
⑵ 遍历左子树;
⑶ 遍历右子树。
2.中(根)序遍历的递归算法
⑴遍历左子树;
⑵访问根结点;
⑶遍历右子树。
3.后(根)序遍历得递归算法
⑴遍历左子树;
⑵遍历右子树;
⑶访问根结点。
4.层序遍历
⑴首先访问第一层的树根节点
⑵然后从左到右访问第二层上的节点
⑶接着是第三层的节点,以此类推
反转二叉树
如图所示
4 4
/ \ / \
2 7 --> 7 2
/ \ / \ / \ / \
1 3 6 9 9 6 3 1
使用先序/中序/后续/层序交换所有结点的左右子树
public class TreeNode {
int val;
public TreeNode left;
public TreeNode right;
public TreeNode(int val) {
this.val = val;
}
public TreeNode(int val, TreeNode left, TreeNode right) {
this.val = val;
this.left = left;
this.right = right;
}
public TreeNode() {
}
}
public class InvertBinaryTree {
/**
* Invert Binary Tree
* //先序遍历(根左右)
* 先序遍历先从二叉树的根开始,然后到左子树,再到右子树
* @param root
* @return
*/
public TreeNode invertTree1(TreeNode root){
if (root != null) {
TreeNode t = root.left;
root.left = root.right;
root.right = t;
invertTree1(root.left);
invertTree1(root.right);
return root;
}else {
return null;
}
}
/**
* Invert Binary Tree
* //后序遍历(左右根)
* 后序遍历先从左子树开始,然后到右子树,再到根
* @param root
* @return
*/
public TreeNode invertTree2(TreeNode root){
if (root != null) {
invertTree2(root.left);
invertTree2(root.right);
TreeNode t = root.left;
root.left = root.right;
root.right = t;
return root;
}else {
return null;
}
}
/**
* Invert Binary Tree
* //中序遍历(左根右)
* 中序遍历先从左子树开始,然后到根,再到右子树
* @param root
* @return
*/
public TreeNode invertTree3(TreeNode root){
if (root != null) {
invertTree3(root.left);
TreeNode t = root.left;
root.left = root.right;
root.right = t;
invertTree3(root.left);//root.left是原来的root.right
return root;
}else {
return null;
}
}
/**
* Invert Binary Tree
* 层序遍历 (一层一层遍历)
*
* @param root
* @return
*/
public TreeNode invertTree4(TreeNode root) {
if (root == null) {
return null;
}else{
Queue<TreeNode> queue = new LinkedList<>();
queue.offer(root);
while (!queue.isEmpty()){
TreeNode node = queue.poll();
TreeNode t = node.left;
node.left = node.right;
node.right = t;
if (node.left != null) {
queue.offer(node.left);
}
if (node.right != null) {
queue.offer(node.right);
}
}
return root;
}
}
}
插入
BST插入节点
首先定义一个Empty,用于存储键值对
/**
* 存储key,value
* @param <K>
* @param <V>
*/
public static class BSTEntry<K,V> implements Map.Entry<K,V> {
private K key;
private V value;
private BSTEntry<K,V> left;
private BSTEntry<K,V> right;
@Override
public K getKey() {
return key;
}
@Override
public V getValue() {
return value;
}
@Override
public V setValue(V value) {
this.value = value;
return value;
}
public BSTEntry(K key) {
this.key = key;
}
public BSTEntry(K key, V value) {
this.key = key;
this.value = value;
}
public BSTEntry(K key, V value, BSTEntry<K, V> left, BSTEntry<K, V> right) {
this.key = key;
this.value = value;
this.left = left;
this.right = right;
}
@Override
public String toString() {
return "AVLEntry{" +
"key=" + key +
", value=" + value +
'}';
}
}
比较关键字a和b的大小
public int compare(Object a, Object b) {
if (comparator != null) {
return comparator.compare((K)a,(K)b);//JDK中也是强转的
}else{
Comparable<K> t = (Comparable<K>) a;
return t.compareTo((K)b);
}
}
添加元素
public V put(K key,V value) {
if (root == null) {
root = new BSTEntry<K,V>(key,value);
size ++;
}else{
BSTEntry<K,V> p = root;
while (p != null) {
int cmp = compare(key,p.key);
if (cmp < 0) {
if (p.left == null) {
p.left = new BSTEntry<K,V>(key,value);
size ++;
break;
}else{
p = p.left;//再次循环比较
}
} else if (cmp > 0) {
if (p.right == null) {
p.right = new BSTEntry<K,V>(key,value);
size ++;
break;
}else{
p = p.right;
}
}else{
p.setValue(value);//替换旧值
break;
}
}
}
//不管是插入的是新值还是重复值,都返回插入的值,这个和JDK TreeMap不一样
return value;
}
遍历
BST迭代器
利用中序遍历实现顺序迭代
方案1:递归添加进线性集合,迭代线性集合
/**
* 递归实现iterator
* 空间复杂度是O(n)
*/
public class BSTIterator {
private Iterator<Integer> itr;
public BSTIterator(TreeNode root) {
//这个算法的空间复杂度是O(n)
ArrayList<Integer> list = new ArrayList<>();
inOrder(root,list);
itr = list.iterator();
}
/**
* 中序遍历将每个结点添加进集合
* @param p
* @param list
*/
private void inOrder(TreeNode p,ArrayList<Integer> list) {
if (p != null) {
inOrder(p.left,list);
list.add(p.val);
inOrder(p.right,list);
}
}
public boolean hasNext() {
return itr.hasNext();
}
public int next() {
return itr.next();
}
}
方案2:使用的是非递归算法,使左路径节点压栈
5
/ \
2 6
/ \ \
1 4 7
/
3
1.从根节点依次遍历左节点,将其压栈 (Stack:1,2,5) (Out:)
2.压栈到1时,如果1没有左子树,将其出栈 (Stack:2,5) (Out:1)
3.如果1也没有右子树,再次弹栈,2出栈(Stack:5) (Out:1,2)
4.再看2是否有右子树,有,将2右节点4所在的节点左子树压栈(Stack:3,4,5) (Out:1,2)
5.3没有左子树,3弹栈(Stack:4,5) (Out:1,2,3)
6.3没有右子树,4弹栈(Stack:5) (Out:1,2,3,4)
7.4.没有右子树,5弹栈(Stack:) (Out:1,2,3,4,5)
8.5有右子树,将5右节点6所在的节点左子树压栈(Stack:6) (Out:1,2,3,4,5)
9.6没有左子树,6弹栈(Stack:) (Out:1,2,3,4,5,6)
10.6有右子树,将6右节点7所在的节点左子树压栈(Stack:7) (Out:1,2,3,4,5,6)
11.7没有左子树,7弹栈(Stack:) (Out:1,2,3,4,5,6,7)
12.7没有右子树,结束,这时输出的结果正好时顺序的(Out:1,2,3,4,5,6,7)
/**
* 中序遍历实现迭代器
* 这里实现引入Stack,而非递归实现
* @param <K>
* @param <V>
*/
public class BSTIterator<K,V> implements Iterator<BSTEntry<K,V>> {
private Stack<BSTEntry<K,V>> stack;
public BSTIterator(BSTEntry<K, V> root) {
stack = new Stack<>();
addLeftPath(root);
}
private void addLeftPath(BSTEntry<K, V> p) {
while (p != null) {
stack.push(p);
p = p.left;
}
}
@Override
public boolean hasNext() {
return !stack.empty();
}
@Override
public BSTEntry<K, V> next() {
BSTEntry<K, V> entry = stack.pop();
addLeftPath(entry.right);
return entry;
}
}
查找
BST查找节点
查找原理
查找与插入类似,假设查找关键字key
1,若根结点的关键字值等于key,成功
2,若key小于根结点的关键字,递归查找左子树
3,若key大于根结点的关键字,递归查找右子树
4,若子树为空,查找不会成功
时间复杂度O(logN)
/**
* @param key
* @return
*/
private BSTEntry<K,V> getEntry(Object key) {
BSTEntry<K,V> p = root;//初始化指针p,指向根节点
while (p != null) {
int cmp = compare(key, p.key);//比较key与p.key的大小
if (cmp < 0) {
p = p.left;//key小于p.key,递归(循环)查找左子树
} else if (cmp > 0) {
p = p.right;//key大于p.key,递归(循环)查找右子树
} else {
return p;//key等于p.key,查找成功
}
}
return null;//查找失败
}
@Override
public V get(Object key) {
BSTEntry<K, V> entry = getEntry(key);
return entry != null ? entry.value : null;
}
查询是否包含key
public boolean containsKey(Object key) {
BSTEntry<K, V> entry = getEntry(key);
return entry != null;
}
查询是否包含value
/**
* 遍历所有结点查看比较value
* 这里直接使用的迭代器,一个一个比较(中序遍历)
* 时间复杂度O(N)
* @param value
* @return
*/
@Override
public boolean containsValue(Object value) {
Iterator<BSTEntry<K, V>> iterator = this.iterator();
while (iterator.hasNext()) {
if (iterator.next().equals(value)) {
return true;
}
}
return false;
}
寻找最小节点
/**
* 找到以p为根节点的最小结点(中序遍历的第一个结点)
* @param p
* @return
*/
private BSTEntry<K, V> getFirstEntry(BSTEntry<K, V> p) {
if(p == null)
return null;
while (p.left != null)//一直往左找,直到p没有左子树
p = p.left;
return p;//p是最左边且第一个没有左子树的结点
}
寻找最大节点
/**
* 找到以p为根节点的最大结点
* @param p
* @return
*/
private BSTEntry<K, V> getLastEntry(BSTEntry<K, V> p) {
if (p == null)
return null;
while (p.right != null)//一直往右找,直到p没有右子树
p = p.right;
return p;//p是最右边且第一个没有右子树的结点
}
删除
BST的删除
假设删除结点p,其父节点为f,分如下3中情况:
1,p是叶子结点,直接删除
5
/ \
2 6
/ \ \ 删除1-> 直接删除1
1 4 7
/
3
2,p只是左子树left(或右子树right),直接用p.left替换p
5 5
/ \ / \
2 6 2 6
/ \ \ 删除4-> 3直接替换4 / \ \
1 4 7 1 3 7
/
3
3,p既有左子树left,又有右子树right,找到右子树的最小结点rightMin(需要借助于getFirstEntry,或找到left的最大结点leftMax),用rightMin的值替换p的值,再根据以上两种情况删除rightMin
6 6 6
/ \ / \ / \
2 7 3 7 3 7
/ \ \ 找到2的后继节点3 / \ \ 已转化成情况1 / \ \
1 4 8 -> 1 4 8 -> 1 4 8
/ \ 将2替换成3 / \ 直接删除3 \
3 5 3 5 5
/**
* 以p为根结点删除key
* 递归查找删除
* @param p
* @param key
* @return
*/
private BSTEntry<K,V> deleteEntry(BSTEntry<K,V> p, Object key){
if(p == null)
return null;
int cmp = compare(key, p.key);
if (cmp == 0) {//找到删除的结点
if (p.left == null && p.right == null) {//情况1
p = null;
} else if (p.left == null && p.right != null) {//情况2
p = p.right;
} else if (p.left != null && p.right == null) {//情况2
p = p.left;
} else {//情况3
//为了达到平衡效果,随机执行以下两种情况
if ((size & 1) == 0) {//找右子树中最小的来替换
BSTEntry<K, V> rightMin = getFirstEntry(p.right);
p.key = rightMin.key;
p.value = rightMin.value;
BSTEntry<K, V> newRight = deleteEntry(p.right, p.key);
p.right = newRight;
}else {//找左子树中最大的来替代
BSTEntry<K, V> leftMax = getLastEntry(p.left);
p.key = leftMax.key;
p.value = leftMax.value;
BSTEntry<K, V> newLeft = deleteEntry(p.left, p.key);
p.left = newLeft;
}
}
} else if (cmp < 0) {//要删除的结点在左边
BSTEntry<K, V> newLeft = deleteEntry(p.left, key);//以左子树为根结点,递归重新找要删除的结点
p.left = newLeft;//实现真正的删除功能(如果p.left是要删除的结点,并且p.left结点没有子节点了,那么newLeft必定返回null,也就实现了删除功能)
} else /*cmp > 0*/{//要删除的结点在右边
BSTEntry<K, V> newRight = deleteEntry(p.right, key);//以右子树为根结点,递归重新找要删除的结点
p.right = newRight;//实现真正的删除功能(如果p.rigth是要删除的结点,并且p.right结点没有子节点了,那么newRight必定返回null,也就实现了删除功能)
}
return p;
}
/**
* 删除
* @param key
* @return
*/
@Override
public V remove(Object key) {
BSTEntry<K, V> entry = getEntry(key);//查找是否有该结点
if (entry == null) {
return null;
}
V oldValue = entry.getValue();//获取value
BSTEntry<K, V> newRoot = deleteEntry(root, key);//删除,并返回新的根节点(如果删除的是根节点,root就会变化)
root = newRoot;
return oldValue;
}
层序遍历
便于查看BST树结构
/**
* 层序遍历
*/
public void levelOrder() {
Queue<BSTEntry<K,V>> queue = new LinkedList<>();
queue.offer(root);
int preCount = 1;
int pCount = 0;
while (!queue.isEmpty()) {
preCount --;
BSTEntry<K,V> p = queue.poll();
System.out.print(p + " ");
if (p.left != null) {
queue.offer(p.left);
pCount ++;
}
if (p.right != null) {
queue.offer(p.right);
pCount ++;
}
if (preCount == 0) {
preCount = pCount;
pCount = 0;
System.out.println();
}
}
}
后续
寻找后续(前趋)节点(有parent指针)
情况1:t有右子树,和getFirstEntry完全一样
6
/ \
2 7
/ \ \ 寻找2的后继节点,也就是3
1 4 8
/ \
3 5
情况2:解法1,t没有右子树,向上回溯,找到第一个孩子是左子树孩子的父亲p
ch=5,p=4,ch是p的右孩子,将ch和p同时上移(ch=p,p=p.parent)
ch=4,p=2,ch是p的右孩子,将ch和p同时上移(ch=p,p=p.parent)
ch=2,p=6,ch是p的左孩子,则这时p(也就是6)是t(也就是5)的后继节点
情况2:解法2,t没有右子树,向上回溯,找到第一个关键字比孩子大的父亲p
ch=5,p=4,p比ch小,将ch和p同时上移(ch=p,p=p.parent)
ch=4,p=2,p比ch小,将ch和p同时上移(ch=p,p=p.parent)
ch=2,p=6,p比ch大,则这时p(也就是6)是t(也就是5)的后继节点
JDK TreeMap寻找后继节点的算法情况1和情况2的解法1
static <K,V> TreeMap.Entry<K,V> successor(Entry<K,V> t) {//寻找后继节点
if (t == null)
return null;
else if (t.right != null) {//情况1:有右子树
Entry<K,V> p = t.right;
while (p.left != null)//找到右子树的最小节点,即右子树最左边的节点
p = p.left;
return p;
} else {//情况2:没有右子树(解法1)
Entry<K,V> p = t.parent;
Entry<K,V> ch = t;
while (p != null && ch == p.right) {//找到第一个孩子是左孩子的父亲节点,或者根节点
ch = p;
p = p.parent;
}
return p;
}
}
JDK TreeMap寻找前趋节点的算法和后继节点是完全对称的
static <K,V> Entry<K,V> predecessor(Entry<K,V> t) {
if (t == null)
return null;
else if (t.left != null) {
Entry<K,V> p = t.left;
while (p.right != null)
p = p.right;
return p;
} else {
Entry<K,V> p = t.parent;
Entry<K,V> ch = t;
while (p != null && ch == p.left) {
ch = p;
p = p.parent;
}
return p;
}
}
寻找后续(前趋)节点(无parent指针)
情况1:p是最大节点,没有后继,返回null,或root为null,返回null
情况2:p有右子树,和getFirstEntry完全一样
情况3:p没有右子树,查找到节点p(同BST的查找),并将路径压栈,弹栈回溯的算法与有parent域的情况2类似
Stack实现:时间、空间复杂度小于O(logN),
过程是自底向上,它记录的是第一个遇到左孩子的父亲,则该左孩子的父亲就是t的后继节点 (情况3如下图所示)
从根节点开始寻找,一直找到t(5),将寻找路径上的节点全部压栈,然后再出栈
5(ch=5)出栈(poll),指针向上回溯(peek),这时指针指向4(p=4),4(p=4)的右孩子是5(ch=5)
4(ch=4)出栈(poll),指针向上回溯(peek),这时指针指向2(p=2),2(p=2)的右孩子是4(ch=4)
2(ch=2)出栈(poll),指针向上回溯(peek),这时指针指向6(p=6),6(p=6)的左孩子是2(ch=2),所以6就是我们所找的结果t(5)的后继节点(6)
While实现(临时变量代替栈):时间复杂度O(logN)、空间复杂度O(1),
过程是自顶向下,它记录的是最后一个遇到比t大的父亲,该节点就是t的后继节点 (情况3如下图所示)
public class InorderSuccessorInBST {
/**
* 根据root根结点,寻找p的后继结点
* @param root
* @param p
* @return
*/
public TreeNode inorderSuccessor(TreeNode root,TreeNode p) {
if(root == null) return null;//1.如果root为null,直接返回null
if(getLastEntry(root) == p) return null;//2.如果最大结点等于p结点,说明没有后继结点,直接返回null
if(p.right != null) return getFirstEntry(p.right);//3.如果p有右孩子,直接返回以右孩子为根节点的最小结点
//4.如果右孩子为空,可以有两种解法,一种是自顶向下,一种是自底向上
//自顶向下:(需要用到while循环)找到的是最后一个父亲,也就是后续结点
//自底向上:(需要用到栈Stack)找到的是第一个左孩子的父亲,
//以下使用的是自顶向下的解法
TreeNode parent = root;
TreeNode tmp = root;
while (parent != null) {
if(parent == p) break;
else if(p.val < parent.val){//小于说明在左孩子树上,说明parent有可能是p的后继节点,只要该parent是最后一个找到的(p.val < parent.val)的parent
tmp = parent;
parent = parent.left;
}else //大于说明在右孩子树上
parent = parent.right;
}
return tmp;
}
/**
* 找到以p为根节点的最大结点
* @param p
* @return
*/
private TreeNode getLastEntry(TreeNode p){
while (p.right != null)
p = p.right;
return p;
}
/**
* 找到以p为根节点的最小结点
* @param p
* @return
*/
private TreeNode getFirstEntry(TreeNode p){
while (p.left != null)
p = p.left;
return p;
}
}
JDK TreeMap中的删除就使用到了后继节点
public V remove(Object key) {
Entry<K,V> p = getEntry(key);
if (p == null)
return null;
V oldValue = p.value;
deleteEntry(p);
return oldValue;
}
private void deleteEntry(Entry<K,V> p) {
modCount++;
size--;
//(BST的删除)情况3:p有两个孩子,找到p的后继节点s,用s的key和value替换p的key和value,并让p指向s
if (p.left != null && p.right != null) {
Entry<K,V> s = successor(p);
p.key = s.key;
p.value = s.value;
p = s;//s可能是叶子节点,可能是只有右子树的节点
}
// 如果p是s替换过来的,那么p只能有右孩子
Entry<K,V> replacement = (p.left != null ? p.left : p.right);
//(BST的删除)情况2:p只有一个孩子,replacement要么是左孩子,要么是右孩子
if (replacement != null) {
// 这里需要将p的孩子节点replacement的parent和其父节点的left或right的索引重新赋值(因为p已被删除)
replacement.parent = p.parent;//直接用p孩子替换p,让孩子节点的parent指向新的parent,也就是p的parent
if (p.parent == null)
root = replacement;//如果p.parent是null,则p的孩子就是新的根节点
else if (p == p.parent.left)//p是左孩子
p.parent.left = replacement;// replacement重新指向p的父亲的左孩子
else //p是右孩子
p.parent.right = replacement;//replacement重新指向p的父亲的右孩子
p.left = p.right = p.parent = null;//释放指针p(删除)
// Fix replacement
if (p.color == BLACK)
fixAfterDeletion(replacement);
} else if (p.parent == null) { // return if we are the only node.
root = null;
} else { // (BST的删除)情况1:p是叶子节点,直接删除
if (p.color == BLACK)
fixAfterDeletion(p);
if (p.parent != null) {
if (p == p.parent.left)
p.parent.left = null;//释放掉p
else if (p == p.parent.right)
p.parent.right = null;//释放掉p
p.parent = null;//释放掉p
}
}
}
源码:
https://github.com/xiaojinwei/java-learning/blob/master/src/com/cj/learn/tree/bst/BSTMap.java
参考:
https://www.bilibili.com/video/av23890827
https://github.com/kosoraYintai/TreeMapSourceAnalysis