树 ---> 二叉树:左孩子,右兄弟原则:
- 每个节点的左指针指向她的第一个孩子,右指针指向她在树中相邻的右兄弟。
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())
ListpreOrderLeft = preOrder.subList(1, index + 1);
ListpreOrderRight = preOrder.subList(index + 1, preOrder.size()); // 在中序序列中,切割出对应的左右子树的,先序序列,用来递归
// left子树的中序: [0 ~ index)
// right子树的中序: [index + 1 ~ inOrder.size())
ListinOrderLeft = inOrder.subList(0, index);
ListinOrderRight = 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; public class MyBSTree4.自平衡的二叉搜索树{ private Node root; private int size; // add // remove // contains 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; } 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; } public boolean contains(T value) { // TODO return false; } // -------------------------------------------------------------------- // ------------------为了练习代码--------------------------------------- // 树遍历: 深度遍历, 广度遍历 // 还原一棵树 public MyLinkedList leOrder() { // 创建一个队列 MyLinkedQueue queue = new MyLinkedQueue<>(); // 创建一个存储遍历结果的线性表 MyLinkedList 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; } public MyLinkedList preOrder() { // KEY:获得BST的先序遍历的核心逻辑 // 用来实现先序遍历的栈stack // 用链表list保存先序遍历结果 // 根结点入栈,但不算访问 // 当栈不空,出栈一个节点,把他的 右 左 孩子(如果不为null)入栈,循环遍历 // 出栈并访问一个结点,加到list后面 // 栈空,退出循环,返回list。 // 用来实现先序遍历的栈 MyArrayStack stack = new MyArrayStack<>(); // 保存先序遍历结果的链表 MyLinkedList 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; } public MyLinkedList inOrder() { // KEY:中序序列, // 用栈stack实现,两层递归 // 从根结点到 root的左子树最左下结点路径依次入栈 // 弹栈并访问(加入list) // 让当前结点的右子树成为下一轮循环的node。 // 用来实现中序遍历的栈 MyArrayStack stack = new MyArrayStack<>(); // 保存中序遍历结果的链表 MyLinkedList 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; } public MyLinkedList postOrder() { // KEY:获得BST的后序遍历的核心逻辑 // 用来实现后序遍历的栈stack // 用链表list保存后序遍历结果 // 根结点入栈,但不算访问 // 当栈不空,出栈一个节点,把他的 左 右 孩子(如果不为null)入栈,循环遍历 // 出栈并访问,的结点,加到list前面(头插法) // 栈空,退出循环,返回list。 // 用来实现后序遍历的栈 MyArrayStack stack = new MyArrayStack<>(); // 保存后序遍历结果的链表 MyLinkedList 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; } public MyLinkedList preOrderRecursive() { MyLinkedList list = new MyLinkedList<>(); preOrderRecursiveHelper(list, root); return list; } // 递归遍历某个结点 private void preOrderRecursiveHelper(MyLinkedList list, Node node) { if (node == null) { return; } list.add(node.value); // 递归遍历左子树 preOrderRecursiveHelper(list, node.left); // 递归遍历右子树 preOrderRecursiveHelper(list, node.right); } public MyLinkedList postOrderRecursive() { MyLinkedList list = new MyLinkedList<>(); postOrderRecursiveHelper(list, root); return list; } // 递归遍历某个结点 private void postOrderRecursiveHelper(MyLinkedList list, Node node) { if (node == null) { return; } // 递归遍历左子树 postOrderRecursiveHelper(list, node.left); // 递归遍历右子树 postOrderRecursiveHelper(list, node.right); list.add(node.value); } public MyLinkedList inOrderRecursive() { MyLinkedList list = new MyLinkedList<>(); inOrderRecursiveHelper(list, root); return list; } // 递归遍历某个结点 private void inOrderRecursiveHelper(MyLinkedList list, Node node) { if (node == null) { return; } // 递归遍历左子树 inOrderRecursiveHelper(list, node.left); list.add(node.value); // 递归遍历右子树 inOrderRecursiveHelper(list, node.right); } public void buildTreeByInAndPreOrder(List inOrder, List preOrder) { root = buildTreeByInAndPreOrderHelper(inOrder, preOrder); size = inOrder.size(); } private Node buildTreeByInAndPreOrderHelper(List inOrder, List 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 preOrderLeft = preOrder.subList(1, index + 1); List preOrderRight = preOrder.subList(index + 1, preOrder.size()); // 在中序序列中,切割出对应的左右子树的,先序序列,用来递归 // left子树的中序: [0 ~ index) // right子树的中序: [index + 1 ~ inOrder.size()) List inOrderLeft = inOrder.subList(0, index); List 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; } public void buildTreeByInAndPostOrder(List inOrder, List postOrder) { root = buildTreeByInAndPostOrderHelper(inOrder, postOrder); size = inOrder.size(); } private Node buildTreeByInAndPostOrderHelper(List inOrder, List 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 postOrderLeft = postOrder.subList(0, index); List postOrderRight = postOrder.subList(index, postOrder.size() - 1); // 在中序序列中,切割出对应的左右子树的,先序序列,用来递归 // left子树的中序: [0 ~ index) // right子树的中序: [index + 1 ~ inOrder.size()) List inOrderLeft = inOrder.subList(0, index); List 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; } } }
5.红黑树树本身适合存储大批量的数据, 但是我们不希望稀疏树产生, 稀疏树的性能太差
自平衡的二叉搜索树, 存在意义, 是为了解决普通二叉搜索树, 在不断添加和删除的过程中变成稀疏树导致效率降低的问题的
自平衡的二叉搜索树 ----> 平衡性能是很好的
由于平衡性能太好, 所以导致这个树高度非常严格: 有可能会新加一个结点, 导致很上层的结构变得不平衡, 所以导致变换操作(旋转)变成非局部性操作, 代码实现过于复杂
// 红黑树是一个二叉搜索树 / 红黑树是一个自平衡的二叉搜索树
// 红黑树中每一个结点都有颜色: 红色, 黑色
// 红黑树中根结点是黑色的, 叶子结点也是黑色的(nil)
// 红黑树中, 在父子关系上没有连续的红色结点
// 红黑树, 要保证黑高平衡(叶子结点,到根结点的简单路径上, 经过相同数目的黑色结点)注意:在实际工程中, 用到的都是红黑树, 而非自平衡的二叉搜索树: 原因自平衡的二叉搜索树有可能旋转是个非局部性操作, 代码实现比较复杂, 红黑树虽然平衡性能, 不如自平衡的二叉搜索树, 但是操作是局部性操作.
// Java什么地方用到了红黑树? 为什么要在这个地方用到红黑树?
// 红黑树的有什么好处?
相比较线性表: 好处是树比较适合大批量的数据存储
和普通的树比较: 二叉, 结点用链表实现好定义
和二叉树比较: 可以根据大小查找/添加/删除, 效率更高
和二叉搜索树比较: 红黑树不可能变成稀疏树
和自平衡的二叉搜索树比较: 虽然平衡性能没有自平衡的二叉搜索树好, 但是操作是局部性操作
// 红黑树是怎么保证没有连续的红色结点? 红黑树是怎么保证黑高平衡?
通过旋转, 以及向上分裂和向下分裂
旋转: 左旋, 右旋,先左旋再右旋, 先右旋转再左旋
旋转过程中, 如果颜色不同, 发生颜色交换
向上分裂: 左右孩子由红变黑, 自己由黑变红
向下分裂: 自己由红变黑, 左右孩子由黑变红



