1.逻辑结构定义
树 ---> 二叉树:左孩子,右兄弟原则:
- 每个节点的左指针指向她的第一个孩子,右指针指向她在树中相邻的右兄弟。
2.存储结构实现
二叉树存储结构:
- 数组存储,浪费空间
- // 前提
如果我们对一个完全二叉树按照层级进行编号(根节点编号1) , 这个完全二叉树上的所有结点, 都满足一个特点
父结点编号 * 2 = 该结点的left结点编号
父结点编号 * 2 + 1= 该结点的right结点编号
这样即使给定我们任意一个x位置, 我们都能很快找到它的父结点位置(x/2)以及子结点位置(2x, 2x+1)
- // 前提
- 链式存储
-
private int value; private Node leftChild; private Node rightChild;
-
3.二叉树的操作
二叉树的遍历:
- 广度遍历(层序遍历):用队列实现
-
广度优先遍历:广度优先遍历是连通图的一种遍历策略,因为它的思想是从一个顶点V0开始,辐射状地优先遍历其周围较广的区域故得名。
根据广度优先遍历的特点我们利用Java数据结构队列Queue来实现。
-
出队即访问,访问则让所有孩子入队
(1)、节点1进队,节点1出队,访问节点1
(2)、节点1的孩子节点2进队,节点3进队。
(3)、节点2出队,访问节点2,节点2的孩子节点4进队,节点5进队;
(4)、节点3出队,访问节点3,节点3的孩子节点6进队,节点7进队;
(5)、节点4出队,访问节点4,节点4没有孩子节点。
(6)、节点5出队,访问节点5,节点5没有孩子节点。
(7)、节点6出队,访问节点6,节点6没有孩子节点。
(8)、节点7出队,访问节点7,节点7没有孩子节点,结束。
-
树一开始是为了存储大量数据时,高度增加为对数,但是要对一棵二叉树层序遍历,不如一开始就用线性表存储。
-
- 深度优先遍历:用栈实现
-
深度优先遍历是图论中的经典算法。其利用了深度优先搜索算法可以产生目标图的相应拓扑排序表,采用拓扑排序表可以解决很多相关的图论问题,如最大路径问题等等。
根据深度优先遍历的特点我们利用Java集合类的栈Stack先进后出的特点来实现。我用二叉树来进行深度优先搜索。
-
出栈即访问,访问则让所有孩子入栈
(1)、首先节点 1 进栈,节点1在栈顶;
(2)、然后节点1出栈,访问节点1,节点1的孩子节点3进栈,节点2进栈;
(3)、节点2在栈顶,然后节点2出栈,访问节点2
(4)、节点2的孩子节点5进栈,节点4进栈
(5)、节点4在栈顶,节点4出栈,访问节点4,
(6)、节点4左右孩子为空,然后节点5在栈顶,节点5出栈,访问节点5;
(7)、节点5左右孩子为空,然后节点3在站顶,节点3出栈,访问节点3;
(8)、节点3的孩子节点7进栈,节点6进栈
(9)、节点6在栈顶,节点6出栈,访问节点6;
(10)、节点6的孩子为空,这个时候节点7在栈顶,节点7出栈,访问节点7
(11)、节点7的左右孩子为空,此时栈为空,遍历结束。
-
KEY:获得BST的先序遍历的核心逻辑
用来实现先序遍历的栈stack
用链表list保存先序遍历结果
根结点入栈,但不算访问
当栈不空,出栈一个节点,把他的 右 左 孩子(如果不为null)入栈,循环遍历
出栈并访问一个结点,加到list后面
栈空,退出循环,返回list。KEY:中序序列,
用栈stack实现,两层递归
从根结点到 root的左子树最左下结点路径依次入栈
弹栈并访问(加入list)
让当前结点的右子树成为下一轮循环的node。
KEY:获得BST的后序遍历的核心逻辑
用来实现后序遍历的栈stack
用链表list保存后序遍历结果
根结点入栈,但不算访问
当栈不空,出栈一个节点,把他的 左 右 孩子(如果不为null)入栈,循环遍历
出栈并访问,的结点,加到list前面(头插法)
栈空,退出循环,返回list。// 递归出口
if (inOrder.size() == 0) {
return null;
}// 先序序列的第一个结点,就是根结点
T value = preOrder.get(0);
// 找到根节点在中序序列的位置
int index = inOrder.indexOf(value);
// 构建根节点
Node root = new Node(value);
// 在先序序列中,切割出左右子树
// left子树的先序: [1 ~ index + 1)
// right子树的先序: [index + 1 ~ preOrder.size())
List<T> preOrderLeft = preOrder.subList(1, index + 1);
List<T> preOrderRight = preOrder.subList(index + 1, preOrder.size());// 在中序序列中,切割出对应的左右子树的,先序序列,用来递归
// left子树的中序: [0 ~ index)
// right子树的中序: [index + 1 ~ inOrder.size())
List<T> inOrderLeft = inOrder.subList(0, index);
List<T> inOrderRight = inOrder.subList(index + 1, inOrder.size());// 根据left子树的中序和先序, 递归构建left子树, 返回left子树的根结点
root.left = buildTreeByInAndPreOrderHelper(inOrderLeft, preOrderLeft);
// 根据right子树的中序和先序, 递归构建right子树, 返回right子树的根结点
root.right = buildTreeByInAndPreOrderHelper(inOrderRight, preOrderRight);return root;
package com._28_datastructure._04_tree.bstree;
import com._28_datastructure._01_linkedlist._01mylinkedlist.MyLinkedList;
import com._28_datastructure._02_stack.MyArrayStack;
import com._28_datastructure._03_queue.MyLinkedQueue;
import java.io.OutputStream;
import java.util.List;
/**
* 手写实现二叉搜索树(不一定平衡)
* 操作:add
* remove
* contains
* <p>
* 限定:T 是 comparable 的子类
* 区别:不能存null,因为不能比大小
*
* @author zxcsjf
* @since 2022/07/09 10:58
*/
public class MyBSTree<T extends Comparable> {
private Node root;
private int size;
// add
// remove
// contains
/**
* BST 的添加方法
*
* @param value 要添加的内容
* @return 成功,返回true
* @author zxcsjf
* @since 2022/07/09 11:05:08
*/
public boolean add(T value) {
// 不允许存null
if (value == null) {
throw new IllegalArgumentException("BST不能存null");
}
// 树为空,直接存为root
if (isEmpty()) {
root = new Node(value);
size++;
return true;
}
// 树不为空,从根结点遍历,找到要添加的位置
Node mid = root; // 定义一个遍历结点
Node midF = null; // 保存mid的父节点
int com = 0; // 保存比较结果
// 循环遍历
while (mid != null) {
com = mid.value.compareTo(value);
// 保存mid的父节点
midF = mid;
// 如果com > 0, 说明mid.value > value
if (com > 0) {
// 往左子树查找
mid = mid.left;
} else if (com < 0) {
// 往右子树查找
mid = mid.right;
} else {
// mid 存储内容相同,不能存
return false;
// 3种解决办法:
// 1.拉链法:value存链表;
// 2.Node里面再加一个计数器
// 3.修正的BST:让重复的10,放在原来的10的左子树的最右下位置
}
}
if (com > 0) {
midF.left = new Node(value);
} else {
midF.right = new Node(value);
}
size++;
return true;
}
/**
* 删除分为三种情况:
* 1.删除叶子:直接删
* 2.删除单分支:让非空子树代替他
* 3.删除双分支:先替换,再删除
* 3.a.替换:左子树的最大值 或者 柚子树的最小值,拿来替换
* 3.b.删除:左子树的最大值 或者 右子树的最小值
*
* @param value 要删除的结点值
* @return
* @author zxcsjf
* @since 2022/07/09 11:33:21
*/
public boolean remove(T value) {
if (value == null) {
throw new IllegalArgumentException("不能删除null!");
}
if (isEmpty()) {
throw new RuntimeException("树为空!不能删除!");
}
// TODO:总体思路:
// 先查找,再删除
// 定义一个mid指向要删除的节点
Node mid = root;
// midF指向要删除的节点的父节点
Node midF = null;
// 1.循环查找到要 删除的节点mid 和 父节点midF
while (mid != null) {
int com = mid.value.compareTo(value);
// 当前结点的value比要找的大,
if (com > 0) {
midF = mid;// 保存父节点
mid = mid.left; // 就去左子树中找
} else if (com < 0) {
// 当前结点的value比要找的小,
midF = mid;
mid = midF.right;
} else {
// com = 0, 找到要删除的节点mid
// 这里不处理,留到后面统一处理,
break;
}
}
// 2.跳出循环时,有两种可能
// mid == null,没找到
// mid是要删除的结点,midF是它的父节点(midF也有可能为null)
if (mid == null) {
return false;
}
// 3.如果mid为双分支节点
if (mid.left != null && mid.right != null) {
// 先查找,再替换
// 让mid的左子树的最右下结点替换到mid
Node max = mid.left; // max 就是mid左子树的最大值
Node maxF = mid; // maxF 保存用于替换的节点 的父节点
while (max.right != null) {
maxF = max;
max = max.right;
}
// 退出循环说明找到max结点,替换
mid.value = max.value;
// 再删除这个儿子的原地位,转化为处理单分支情况
mid = max;
midF = maxF;
}
// 4.统一处理叶子、单分支的情况
// ch是mid的 非空子树或者 null
Node ch = mid.left != null ? mid.left : mid.right;
// 如果没有跑双分支的情况,且要删的的就是单分支根结点
if (midF == null) {
root = ch;
size--; // 别忘了size-- !!!!!
return true;
}
// 子树上移
// 如果要删除的结点是midF的右孩子
if (mid == midF.right) {
midF.right = ch;
} else {
// 如果要删除的结点是midF的左孩子
midF.left = ch;
}
size--;
return true;
}
/**
* BST 查找方法
*
* @param null
* @return
* @author zxcsjf
* @since 2022/07/11 14:32:25
*/
public boolean contains(T value) {
// TODO
return false;
}
// --------------------------------------------------------------------
// ------------------为了练习代码---------------------------------------
// 树遍历: 深度遍历, 广度遍历
// 还原一棵树
/**
* BST的层序遍历levelOrder
*
* @param
* @return void
* @author zxcsjf
* @since 2022/07/11 9:56:06
*/
public MyLinkedList leOrder() {
// 创建一个队列
MyLinkedQueue<Node> queue = new MyLinkedQueue<>();
// 创建一个存储遍历结果的线性表
MyLinkedList<T> list = new MyLinkedList<>();
//1, 把根结点入队列
queue.offer(root);
// 2, 循环: 队列不空
while (!queue.isEmpty()) {
// 出队列一个结点
Node poll = queue.poll();
// 访问这个结点
list.add(poll.value);
// 把出队列结点的左右孩子入队列
if (poll.left != null) {
queue.offer(poll.left);
}
if (poll.right != null) {
queue.offer(poll.right);
}
}
return list;
}
/**
* 获得BST的先序遍历序列
*
* @param
* @return MyLinkedList
* @author zxcsjf
* @since 2022/07/11 10:11:07
*/
public MyLinkedList preOrder() {
// KEY:获得BST的先序遍历的核心逻辑
// 用来实现先序遍历的栈stack
// 用链表list保存先序遍历结果
// 根结点入栈,但不算访问
// 当栈不空,出栈一个节点,把他的 右 左 孩子(如果不为null)入栈,循环遍历
// 出栈并访问一个结点,加到list后面
// 栈空,退出循环,返回list。
// 用来实现先序遍历的栈
MyArrayStack<Node> stack = new MyArrayStack<>();
// 保存先序遍历结果的链表
MyLinkedList<T> list = new MyLinkedList<>();
// 根结点入栈,但不算访问
stack.push(root);
// 当栈不空,出栈一个节点,把他的 右 左 孩子入栈,循环遍历
while (!stack.isEmpty()) {
// 出栈并访问,的结点,加到list后面
Node node = stack.pop();
list.add(node.value);
if (node.right != null) {
stack.push(node.right);
}
if (node.left != null) {
stack.push(node.left);
}
}
// 栈空,退出循环,返回list
return list;
}
/**
* BST的中序遍历
*
* @param Node
* @return
* @author zxcsjf
* @since 2022/07/11 10:11:07
*/
public MyLinkedList inOrder() {
// KEY:中序序列,
// 用栈stack实现,两层递归
// 从根结点到 root的左子树最左下结点路径依次入栈
// 弹栈并访问(加入list)
// 让当前结点的右子树成为下一轮循环的node。
// 用来实现中序遍历的栈
MyArrayStack<Node> stack = new MyArrayStack<>();
// 保存中序遍历结果的链表
MyLinkedList<T> list = new MyLinkedList<>();
// 标记结点
Node node = root;
// stack.push(node); 这里不能先让root压栈,第一个压栈的地方是在小循环里面
// 当栈不空,或者node不为null,循环遍历
while (!stack.isEmpty() || node != null) {
while (node != null) {
stack.push(node);
node = node.left;
}
node = stack.pop();
list.add(node.value);
node = node.right;
}
// 栈空,退出循环
return list;
}
/**
* BST的后序遍历
*
* @param
* @return
* @author zxcsjf
* @since 2022/07/11 10:11:07
*/
public MyLinkedList postOrder() {
// KEY:获得BST的后序遍历的核心逻辑
// 用来实现后序遍历的栈stack
// 用链表list保存后序遍历结果
// 根结点入栈,但不算访问
// 当栈不空,出栈一个节点,把他的 左 右 孩子(如果不为null)入栈,循环遍历
// 出栈并访问,的结点,加到list前面(头插法)
// 栈空,退出循环,返回list。
// 用来实现后序遍历的栈
MyArrayStack<Node> stack = new MyArrayStack<>();
// 保存后序遍历结果的链表
MyLinkedList<T> list = new MyLinkedList<>();
// 根结点先入栈,但不算访问
stack.push(root);
// 当栈不空,出栈一个节点,把他的左右孩子入栈,循环遍历
while (!stack.isEmpty()) {
// 出栈的结点,视为被访问,头插到root前面
Node node = stack.pop();
list.add(0, node.value);
if (node.left != null) {
stack.push(node.left);
}
if (node.right != null) {
stack.push(node.right);
}
}
// 栈空,退出循环
return list;
}
/**
* 递归得到先序序列
*
* @param null
* @return
* @author zxcsjf
* @since 2022/07/11 19:58:58
*/
public MyLinkedList preOrderRecursive() {
MyLinkedList<T> list = new MyLinkedList<>();
preOrderRecursiveHelper(list, root);
return list;
}
// 递归遍历某个结点
private void preOrderRecursiveHelper(MyLinkedList<T> list, Node node) {
if (node == null) {
return;
}
list.add(node.value);
// 递归遍历左子树
preOrderRecursiveHelper(list, node.left);
// 递归遍历右子树
preOrderRecursiveHelper(list, node.right);
}
/**
* 递归得到后序序列
*
* @param null
* @return list
* @author zxcsjf
* @since 2022/07/11 19:58:33
*/
public MyLinkedList postOrderRecursive() {
MyLinkedList<T> list = new MyLinkedList<>();
postOrderRecursiveHelper(list, root);
return list;
}
// 递归遍历某个结点
private void postOrderRecursiveHelper(MyLinkedList<T> list, Node node) {
if (node == null) {
return;
}
// 递归遍历左子树
postOrderRecursiveHelper(list, node.left);
// 递归遍历右子树
postOrderRecursiveHelper(list, node.right);
list.add(node.value);
}
/**
* 递归得到中序序列
*
* @param null
* @return
* @author zxcsjf
* @since 2022/07/11 19:58:38
*/
public MyLinkedList inOrderRecursive() {
MyLinkedList<T> list = new MyLinkedList<>();
inOrderRecursiveHelper(list, root);
return list;
}
// 递归遍历某个结点
private void inOrderRecursiveHelper(MyLinkedList<T> list, Node node) {
if (node == null) {
return;
}
// 递归遍历左子树
inOrderRecursiveHelper(list, node.left);
list.add(node.value);
// 递归遍历右子树
inOrderRecursiveHelper(list, node.right);
}
/*------------------------建树(还原一棵树)---------------------------*/
/**
* 递归实现
* 由先序序列和中序序列还原一棵树
*
* @param inOrder 中序序列
* @param preOrder 先序序列
* @return node 树的根结点
* @author zxcsjf
* @since 2022/07/11 20:07:53
*/
public void buildTreeByInAndPreOrder(List<T> inOrder, List<T> preOrder) {
root = buildTreeByInAndPreOrderHelper(inOrder, preOrder);
size = inOrder.size();
}
private Node buildTreeByInAndPreOrderHelper(List<T> inOrder, List<T> preOrder) {
// 递归出口
if (inOrder.size() == 0) {
return null;
}
// 先序序列的第一个结点,就是根结点
T value = preOrder.get(0);
// 找到根节点在中序序列的位置
int index = inOrder.indexOf(value);
// 构建根节点
Node root = new Node(value);
// 在先序序列中,切割出左右子树
// left子树的先序: [1 ~ index + 1)
// right子树的先序: [index + 1 ~ preOrder.size())
List<T> preOrderLeft = preOrder.subList(1, index + 1);
List<T> preOrderRight = preOrder.subList(index + 1, preOrder.size());
// 在中序序列中,切割出对应的左右子树的,先序序列,用来递归
// left子树的中序: [0 ~ index)
// right子树的中序: [index + 1 ~ inOrder.size())
List<T> inOrderLeft = inOrder.subList(0, index);
List<T> inOrderRight = inOrder.subList(index + 1, inOrder.size());
// 根据left子树的中序和先序, 递归构建left子树, 返回left子树的根结点
root.left = buildTreeByInAndPreOrderHelper(inOrderLeft, preOrderLeft);
// 根据right子树的中序和先序, 递归构建right子树, 返回right子树的根结点
root.right = buildTreeByInAndPreOrderHelper(inOrderRight, preOrderRight);
return root;
}
/**
* 递归实现
* 中序 + 后序 还原一棵树
* @param inOrder 中序序列
* @param postOrder 后序序列
* @return
* @author zxcsjf
* @since 2022/07/11 20:41:00
*/
public void buildTreeByInAndPostOrder(List<T> inOrder, List<T> postOrder) {
root = buildTreeByInAndPostOrderHelper(inOrder, postOrder);
size = inOrder.size();
}
private Node buildTreeByInAndPostOrderHelper(List<T> inOrder, List<T> postOrder) {
// 递归出口
if (inOrder.size() == 0) {
return null;
}
// 找到根结点内容: 后序中最后一个位置
T value = postOrder.get(postOrder.size() - 1);
// 构建根结点
Node root = new Node(value);
// 在中序中, 根据根的内容, 确定根的下标位置
int index = inOrder.indexOf(value);
// 在后序序列中,切割出左右子树
// left子树的后序: [0 ~ index)
// right子树的后序: [index ~ postOrder.size() - 1)
List<T> postOrderLeft = postOrder.subList(0, index);
List<T> postOrderRight = postOrder.subList(index, postOrder.size() - 1);
// 在中序序列中,切割出对应的左右子树的,先序序列,用来递归
// left子树的中序: [0 ~ index)
// right子树的中序: [index + 1 ~ inOrder.size())
List<T> inOrderLeft = inOrder.subList(0, index);
List<T> inOrderRight = inOrder.subList(index + 1, inOrder.size());
// 根据left子树的中序和后序, 递归构建left子树, 返回left子树的根结点
root.left = buildTreeByInAndPostOrderHelper(inOrderLeft, postOrderLeft);
// 根据right子树的中序和后序, 递归构建right子树, 返回right子树的根结点
root.right = buildTreeByInAndPostOrderHelper(inOrderRight, postOrderRight);
return root;
}
private boolean isEmpty() {
return root == null;
}
public Node getRoot() {
return root;
}
// 树的结点
class Node {
T value; // 值域
Node left; // 左孩子
Node right; // 右孩子
public Node(T value) {
this.value = value;
}
}
}
4.自平衡的二叉搜索树
树本身适合存储大批量的数据, 但是我们不希望稀疏树产生, 稀疏树的性能太差
自平衡的二叉搜索树, 存在意义, 是为了解决普通二叉搜索树, 在不断添加和删除的过程中变成稀疏树导致效率降低的问题的
自平衡的二叉搜索树 ----> 平衡性能是很好的
由于平衡性能太好, 所以导致这个树高度非常严格: 有可能会新加一个结点, 导致很上层的结构变得不平衡, 所以导致变换操作(旋转)变成非局部性操作, 代码实现过于复杂
5.红黑树
// 红黑树是一个二叉搜索树 / 红黑树是一个自平衡的二叉搜索树
// 红黑树中每一个结点都有颜色: 红色, 黑色
// 红黑树中根结点是黑色的, 叶子结点也是黑色的(nil)
// 红黑树中, 在父子关系上没有连续的红色结点
// 红黑树, 要保证黑高平衡(叶子结点,到根结点的简单路径上, 经过相同数目的黑色结点)注意:在实际工程中, 用到的都是红黑树, 而非自平衡的二叉搜索树: 原因自平衡的二叉搜索树有可能旋转是个非局部性操作, 代码实现比较复杂, 红黑树虽然平衡性能, 不如自平衡的二叉搜索树, 但是操作是局部性操作.
// Java什么地方用到了红黑树? 为什么要在这个地方用到红黑树?
// 红黑树的有什么好处?
相比较线性表: 好处是树比较适合大批量的数据存储
和普通的树比较: 二叉, 结点用链表实现好定义
和二叉树比较: 可以根据大小查找/添加/删除, 效率更高
和二叉搜索树比较: 红黑树不可能变成稀疏树
和自平衡的二叉搜索树比较: 虽然平衡性能没有自平衡的二叉搜索树好, 但是操作是局部性操作
// 红黑树是怎么保证没有连续的红色结点? 红黑树是怎么保证黑高平衡?
通过旋转, 以及向上分裂和向下分裂
旋转: 左旋, 右旋,先左旋再右旋, 先右旋转再左旋
旋转过程中, 如果颜色不同, 发生颜色交换
向上分裂: 左右孩子由红变黑, 自己由黑变红
向下分裂: 自己由红变黑, 左右孩子由黑变红