需求: 给你一个数列 (7, 3, 10, 12, 5, 1, 9),要求能够高效的完成对数据的查询和添加
解决方案——使用数组
数组未排序, 优点:直接在数组尾添加,速度快。 缺点:查找速度慢.
数组排序,优点:可以使用二分查找,查找速度快,缺点:为了保证数组有序,在添加新数据时,找到插入位置后,后面的数据需整体移动,速度慢。
使用链式存储-链表. 不管链表是否有序,查找速度都慢,添加数据速度比数组快,不需要数据整体移动。
在此基础上,我们引入了二叉排序树, 它能够高效的实现数组的添加和删除
二叉排序树介绍
二叉排序树:BST: (Binary Sort(Search) Tree), 对于二叉排序树的任何一个非叶子节点,要求左子节点的值比当前节点的值小,右子节点的值比当前节点的值大。
特别说明:如果有相同的值,可以将该节点放在左子节点或右子节点
比如针对前面的数据 (7, 3, 10, 12, 5, 1, 9) ,对应的二叉排序树以及添加一个元素2后二叉排序树为:
public class BinarySortTreeDemo {
public static void main(String[] args) {
int[] arr = {7, 3, 10, 12, 13, 5, 1, 9};
BinarySortTree binarySortTree = new BinarySortTree();
//循环将节点添加到二叉树
for (int i = 0; i < arr.length; i++) {
binarySortTree.add(new Node(arr[i]));
}
//中序遍历二叉树
System.out.println("中序遍历二叉树");
binarySortTree.midOrder();
}
}
/**
* 创建二叉排序树
*/
class BinarySortTree{
//创建根节点
private Node root;
//创建二叉排序树的添加方法
public void add(Node node){
if (root==null){
root = node;
}else {
root.add(node);
}
}
//创建二叉排序树的中序遍历方法
public void midOrder(){
if (root==null){
System.out.println("该树为空,无法进行中序遍历");
}else {
root.midOder();
}
}
}
/**
* 1.创建节点
*/
class Node{
int value;
Node left;
Node right;
public Node(int value){
this.value = value;
}
@Override
public String toString() {
return "Node{" +
"value=" + value +
'}';
}
//二叉排序树的生成
public void add(Node node){
if (node==null){//如果为空,直接返回
return;
}
//传入的节点的值小于当前节点
if (node.value<this.value){//插入到左子树
if (this.left==null){
this.left = node;
}else {
//递归调用添加的方法
this.left.add(node);
}
}else {
//传入节点的值大于当前节点
if (this.right==null){
this.right = node;
}else {
//右子树递归调用添加的方法
this.right.add(node);
}
}
}
public void midOder(){
if (this.left!=null){
this.left.midOder();
}
System.out.println(this);
if (this.right!=null){
this.right.midOder();
}
}
}
二叉排序树的删除情况比较复杂,有下面三种情况需要考虑
实现思路
public class BinarySortTreeDemo {
public static void main(String[] args) {
int[] arr = {7, 3, 10, 12, 5, 1, 9, 2};
BinarySortTree binarySortTree = new BinarySortTree();
//循环将节点添加到二叉树
for (int i = 0; i < arr.length; i++) {
binarySortTree.add(new Node(arr[i]));
}
//中序遍历二叉树
System.out.println("中序遍历二叉树");
binarySortTree.midOrder();
//测试删除叶子节点
binarySortTree.delNode(2);
binarySortTree.delNode(5);
binarySortTree.delNode(9);
binarySortTree.delNode(12);
//测试删除含有两个叶子节点的节点
binarySortTree.delNode(7);
binarySortTree.delNode(3);//原来3的位置放的是2
binarySortTree.delNode(10);//原来10的位置放的是9
binarySortTree.delNode(1);//原来10的位置放的是9
System.out.println("执行删除操作完毕");
binarySortTree.midOrder();
}
}
/**
* 创建二叉排序树
*/
class BinarySortTree{
//创建根节点
private Node root;
//创建二叉排序树的添加方法
public void add(Node node){
if (root==null){
root = node;
}else {
root.add(node);
}
}
//创建二叉排序树的中序遍历方法
public void midOrder(){
if (root==null){
System.out.println("该树为空,无法进行中序遍历");
}else {
root.midOder();
}
}
/**
* 查找要删除的节点
* @param value
* @return 如果有返回该节点,,如果没有返回null
*/
public Node search(int value){
if (root==null){
return null;
}else {
return root.search(value);
}
}
/**
* 查找要删除的父节点
* @param value
* @return 如果有返回该节点,如果没有返回null
*/
public Node searchParent(int value){
if (root==null){
return null;
}else {
return root.searchParent(value);
}
}
/**
* 删除对应的节点
* @param value
*/
public void delNode(int value){
if (root==null){
return;
}else {
//如果根节点不会空则进行节点的删除操作
//1. 查找要删除的节点targetNode
Node targetNode = search(value);
//如果没有找到要删除的节点
if (targetNode==null){
return;
}
//如果要查找的二叉树只有一个节点
if (root.left==null && root.right==null){
root = null;
return;
}
//2.去找到targetNode的父节点
Node parent = searchParent(value);
//a.如果要删除的节点都是叶子节点
if (targetNode.left==null && targetNode.right==null){
if (parent.left!=null && parent.left.value==value){//是左子节点
parent.left = null;
}else if(parent.right!=null && parent.right.value==value){//是右子节点
parent.right = null;
}
}else if(targetNode.left!=null && targetNode.right!=null){//b.删除有两个子树的节点
//在右树中找最小的
int minVal = delRightTreeMin(targetNode.right);
targetNode.value = minVal;
System.out.println("minVal = " + minVal);
}else {//c.删除只有一个子树的节点
//x如果要删除的节点有左子节点
if (targetNode.left!=null){
if (parent!=null){//***防止出现在删除最后两个节点时,首先删除根节点的情况
if (parent.left.value==value){//如果targetNode是parent的左子节点
parent.left = targetNode.left;
}else {//如果targetNode是parent的右子节点
parent.right = targetNode.left;
}
}else {
root = targetNode.left;
}
}else {//x如果要删除的节点有右子节点
if (parent!=null){//***防止出现在删除最后两个节点时,首先删除根节点的情况
if (parent.left.value==value){//如果targetNode是parent的左子节点
parent.left = targetNode.right;
}else {//如果targetNode是parent的右子节点
parent.right = targetNode.right;
}
}else {
root = targetNode.right;
}
}
}
}
}
/**
* 编写方法:
* 1.返回以node为根节点的二叉树的最小节点的值
* 2.删除以node为根节点的二叉树的最小节点
* @param node
* @return
*/
public int delRightTreeMin(Node node){
Node target = node;
// 循环的查找左子节点,就会找到最小值
while (target.left!=null){
target = target.left;
}
// 如果左子节点为空,说明找到了最小节点
// 删除最小节点
delNode(target.value);
return target.value;
}
}
/**
* 1.创建节点
*/
class Node{
int value;
Node left;
Node right;
public Node(int value){
this.value = value;
}
@Override
public String toString() {
return "Node{" +
"value=" + value +
'}';
}
//二叉排序树的生成
public void add(Node node){
if (node==null){//如果为空,直接返回
return;
}
//传入的节点的值小于当前节点
if (node.value<this.value){//插入到左子树
if (this.left==null){
this.left = node;
}else {
//递归调用添加的方法
this.left.add(node);
}
}else {
//传入节点的值大于当前节点
if (this.right==null){
this.right = node;
}else {
//右子树递归调用添加的方法
this.right.add(node);
}
}
}
public void midOder(){
if (this.left!=null){
this.left.midOder();
}
System.out.println(this);
if (this.right!=null){
this.right.midOder();
}
}
/**
* 查找要删除的节点
* @param value 要删除的节点的值
* @return 如果找到返回该节点,否则返回null
*/
public Node search(int value){
if (value<this.value){//如果小于父节点,则进入左子树
if (this.left==null){
return null;
}
return this.left.search(value);//左子树递归调用查找的方法
}else if(value>this.value){//如果大于父节点,则进行入右子树
if (this.right==null){
return null;
}
return this.right.search(value);//右子树递归调用查找方法
}else {//如果查找的值等于父节点,则直接返回该节点
return this;
}
}
/**
* 查找要删除的父节点
* @param value 要删除的节点的值
* @return 要删除的父节点, 否则返回null
*/
public Node searchParent(int value){
//如果当前节点就是要删除的父节点, 则直接返回
if ( (this.left!=null && this.left.value==value) || (this.right!=null && this.right.value==value) ){
return this;
}else {
// 如果需要找的值小于当前节点则递归调用左子树(判空)
if (this.left!=null && value<this.value){
return this.left.searchParent(value);
// 如果需要找到值大于当前节点则递归调用右子树(判空)
}else if (this.right!=null && value>this.value){
return this.right.searchParent(value);
}else {
return null;//没有找到父节点
}
}
}
}
看一个案例(说明二叉排序树可能的问题)
给你一个数列{1,2,3,4,5,6},要求创建一个二叉排序树(BST), 并分析问题所在
平衡二叉树也叫平衡二叉搜索树(Self-balancing binary search tree)又被称为AVL树, 可以保证查询效率较高。
具有以下特点:
举例: 下图1,2是AVL树,3不是. 因为左右两个子树高度差为2
// 返回左子树的高度
public int leftHeight() {
if (left == null) {
return 0;
}
return left.height();
}
// 返回右子树的高度
public int rightHeight() {
if (right == null) {
return 0;
}
return right.height();
}
// 返回以该结点为根结点的树的高度
public int height() {
return Math.max(left == null ? 0 : left.height(), right == null ? 0 : right.height()) + 1;
}
/**
* 相当于上面使用三目运算符实现的返回根节点的树的高度的方法
* @return
*/
/* public int height(){
int leftVal = 0;
int rightVal = 0;
//如果左树不为空,则递归调用该方法
if (left!=null){
leftVal=left.height();
}
//如果右树不为空,则递归调用该方法
if (right!=null){
rightVal = right.height();
}
// 返回时,需要将本层的高度加上
return Math.max(leftVal, rightVal)+1;
}*/
//左旋转方法
private void leftRotate() {
//创建新的结点,以当前根结点的值
Node newNode = new Node(value);
//把新的结点的左子树设置成当前结点的左子树
newNode.left = left;
//把新的结点的右子树设置成当前结点的右子树的左子树
newNode.right = right.left;
//把当前结点的值替换成右子结点的值
value = right.value;
//把当前结点的右子树设置成当前结点右子树的右子树
right = right.right;
//把当前结点的左子树(左子结点)设置成新的结点
left = newNode;
}
AVl右旋转思路图解
//右旋转方法
private void rightRotate() {
Node newNode = new Node(value);
newNode.right = right;
newNode.left = left.right;
value = left.value;
left = left.left;
right = newNode;
}
二叉树的操作效率较高,但是也存在问题, 请看下面的二叉树
二叉树需要加载到内存的,如果二叉树的节点少,没有什么问题,但是如果二叉树的节点很多(比如1亿), 就存在如下问题:
如果对二叉查找树/二叉排序树和平衡二叉树等概念理解不清楚, 建议看看下面
理解完全二叉树、平衡二叉树、二叉查找树
在二叉树中,每个节点有数据项,最多有两个子节点。如果允许每个节点可以有更多的数据项和更多的子节点,就是多叉树(multiway tree)
后面我们讲解的2-3树,2-3-4树就是多叉树,多叉树通过重新组织节点,减少树的高度,能对二叉树进行优化。
举例说明(下面2-3树就是一颗多叉树)
B树通过重新组织节点,降低树的高度,并且减少i/o读写次数来提升效率。
如下图B树通过重新组织节点, 降低了树的高度.
文件系统及数据库系统的设计者利用了磁盘预读原理,将一个节点的大小设为等于一个页(页得大小通常为4k),这样每个节点只需要一次I/O就可以完全载入
将树的度M设置为1024,在600亿个元素中最多只需要4次I/O操作就可以读取到想要的元素, B树(B+)广泛应用于文件存储系统以及数据库系统中
2-3树是最简单的B树结构, 具有如下特点:
插入规则:
下图是模拟2-3树创建的结构图
除了23树,还有234树等,概念和23树类似,也是一种B树。它除了每个节点所容纳的元素个数不同外, 创建方式和23树一样 如图
B树的介绍
前面已经介绍了2-3树和2-3-4树,他们就是B树(英语:B-tree 也写成B-树),这里我们再做一个说明,我们在学习Mysql时,经常听到说某种类型的索引是基于B树或者B+树的,如下图:
树的说明:
B+树是B树的变体,也是一种多路搜索树。
B* 树是B+树的变体,在B+树的非根和非叶子结点再增加指向兄弟的指针。(和B树,B+树区别在此)
B* 树定义了非叶子结点关键字个数至少为(2/3)*M(M为树的度),即块的最低使用率为2/3,而B+树的块的最低使用率为B+树的1/2。
从第1个特点我们可以看出,B*树分配新结点的概率比B+树要低,空间使用率更高