算法之树(一,B-树原理详解)(Java版)-持续更新补充

简介: B树是用于在外存工作的平衡搜索树,MySQL中的索引主要是基于hash表或者B+树。 B树采取的方法就是,就充分的利用盘块的空间,在一个盘块中尽可能多的存储信息,或者在连续的盘块地址上存储尽可能多的信息

因为是复习,从基础开始一起复习。
如果冲着标题来的,可以直接跳到后半部分看B树的内容(~ ̄▽ ̄)~

支持云栖社区!同时俺也有自己的独立博客——白水东城,因为在社区博客里只能发发技术文章之类的,但在自己博客我会写一些最近随笔和读书笔记等等哈哈,也希望大家能支持一下 ( •̀ ω •́ )y
这里是我独立博客里这篇文章的链接:
算法之树(一,B-树原理详解)(Java版)-持续更新补充

一、二叉树

二叉树的基本操作

public class BinaryTreeNode {
    private int data;
    private BinaryTreeNode leftChild;
    private BinaryTreeNode rightChild;
    public int getData() {
        return data;
    }
    public void setData(int data) {
        this.data = data;
    }
    public BinaryTreeNode getLeftChild() {
        return leftChild;
    }
    public void setLeftChild(BinaryTreeNode leftChild) {
        this.leftChild = leftChild;
    }
    public BinaryTreeNode getRightChild() {
        return rightChild;
    }
    public void setRightChild(BinaryTreeNode rightChild) {
        this.rightChild = rightChild;
    }
    
}

public class BinaryTree {
    private BinaryTreeNode root;
    
    public BinaryTree() {
    }
    public BinaryTree(BinaryTreeNode root) {
        this.root = root;
    }
    public void setRoot(BinaryTreeNode root) {
        this.root = root;
    }
    public BinaryTreeNode getRoot() {
        return root;
    }
    
    public void clear(BinaryTreeNode node) {
        if(node != null) {
            clear(node.getLeftChild());
            clear(node.getRightChild());
            node = null;
        }
    }
    public void  clear() {
        clear(root);
    }
    public boolean isEmpty() {
        return root == null;
    }
    public int height() {
        return height(root);
    }
    public int height(BinaryTreeNode node) {
        if(node == null) {
            return 0;
        }else {
            int l = height(node.getLeftChild());
            int r = height(node.getRightChild());
            return l < r ? r + 1 : l + 1;
        }
    }
    public int size() {
        return size(root);
    }
    public int size(BinaryTreeNode node) {
        if(node == null) {
            return 0;
        }else {
            return 1 + size(node.getLeftChild()) + size(node.getRightChild());
        }
    }
    public BinaryTreeNode getParent(BinaryTreeNode node) {
        return (root == null || root == node) ? null : getParent(root, node);
    }
    public BinaryTreeNode getParent(BinaryTreeNode subTree, BinaryTreeNode node) {
        if(subTree == null) {
            return null;
        }
        if(subTree.getLeftChild() == node || subTree.getRightChild() == node) {
            return subTree;
        }
        BinaryTreeNode parent = null;
        if((parent = getParent(subTree.getLeftChild(),node)) != null) {
            return parent;
        }else {
            return getParent(subTree.getRightChild(), node);
        }
    }
    public BinaryTreeNode getLeftTree(BinaryTreeNode node) {
        return node.getLeftChild();
    }
    public BinaryTreeNode getRightTree(BinaryTreeNode node) {
        return node.getRightChild();
    }
    public void insertLeft(BinaryTreeNode parent, BinaryTreeNode newNode) {
        parent.setLeftChild(newNode);
    }
    public void insertRight(BinaryTreeNode parent, BinaryTreeNode newNode) {
        parent.setRightChild(newNode);
    }
    public void visited(BinaryTreeNode node) {
        
    }
    public void preOrder(BinaryTreeNode node) {
        if(node != null) {
            visited(node);
            preOrder(node.getLeftChild());
            preOrder(node.getRightChild());
        }
    }
    public void inOrder(BinaryTreeNode node) {
        if(node != null) {
           inOrder(node.getLeftChild());
           visited(node);
           inOrder(node.getRightChild());
        }
    }
    public void postOrder(BinaryTreeNode node) {
        if(node != null) {
            postOrder(node.getLeftChild());
            postOrder(node.getRightChild());
            visited(node);
        }
    }
}

二、二叉查找树

简单来说就是左子树结点值都小于根结点,右子树都大于根结点。

二叉查找树的操作


public class BinarySearchingTree {
    private BinaryTreeNode root;
    
    public BinarySearchingTree(BinaryTreeNode root) {
        this.root = root;
    }
    
    public BinaryTreeNode search(int data) {
        return search(root, data);
    }
    //二叉查找树的插入操作很简单,找到和待插入结点同样值的结点则不插入,
    //否则在树的末尾新建一个结点,不需要变动其他结点
    public void insert(int data) {
        if(root == null) {
            root = new BinaryTreeNode();
            root.setData(data);
        }else {
            searchAndInsert(null, root, data);
        }
    }
    //如果待删除的结点左右子树都不为空,
    //则选择右子树的最左结点或者左子树的最右结点来代替
    public void delete(int data) {
        if(root.getData() == data) {
            root = null;
            return;
        }
        //知道要删除那个结点的父结点很关键
        //但无法确定要删除的结点是其父结点的左还是右结点
        //需要特别判断一下 
        BinaryTreeNode parent = searchParent(data);
        if(parent == null) {
            return;
        }
        BinaryTreeNode node = search(parent, data);
        if(node.getLeftChild() == null && node.getRightChild() == null) {
            //叶子结点,直接删除
            if(parent.getLeftChild() != null && parent.getLeftChild().getData() == data) {
                parent.setLeftChild(null);
            }else {
                parent.setRightChild(null);
            }
        }else if(node.getLeftChild() != null && node.getRightChild() == null) {
            //左子树不为空
            if(parent.getLeftChild()!= null && parent.getLeftChild().getData() ==data) {
                parent.setLeftChild(node.getLeftChild());
            }else {
                parent.setRightChild(node.getRightChild());
            }
        }else if(node.getLeftChild() == null && node.getClass() != null) {
            //右子树不为空
            if(parent.getLeftChild() != null && parent.getLeftChild().getData() == data) {
                parent.setLeftChild(node.getRightChild());
            }else {
                parent.setRightChild(node.getRightChild());
            }
        }else {
            //左右子树都不为空
            //查找右子树最左子节点
            BinaryTreeNode deleteNode = node;
            BinaryTreeNode temp = node.getRightChild();
            //1. 右子树没有左孩子,直接上移
            if(temp.getLeftChild() == null) {
                temp.setLeftChild(deleteNode.getLeftChild());
            }else {
                while(temp.getLeftChild() != null) {
                    node = temp;
                    temp = temp.getLeftChild();
                }
                //2. 继承节点原右子树上移 
                node.setLeftChild(temp.getRightChild());
                //3. 继承节点就位
                temp.setLeftChild(deleteNode.getLeftChild());
                temp.setRightChild(deleteNode.getRightChild());
            }
            //4. 更新父节点为删除结点的原父节点
            if(parent.getLeftChild() != null && parent.getLeftChild().getData() == data) {
                parent.setLeftChild(temp);
            }else {
                parent.setRightChild(temp);
            }
        }
    }
    
    public BinaryTreeNode searchParent(int data) {
        return searchParent(null, root, data);
    }
    public BinaryTreeNode getRoot() {
        return root;
    }
    private BinaryTreeNode searchAndInsert(BinaryTreeNode parent, BinaryTreeNode node, int data) {
        if(node == null) {
            node = new BinaryTreeNode();
            node.setData(data);
            if(data > parent.getData()) {
                parent.setRightChild(node);
            }else {
                parent.setLeftChild(node);
            }
            return node;
        }else if(node.getData() == data) {
            return node;
        }else if(data > node.getData()) {
            return searchAndInsert(node, node.getRightChild(), data);
        }else {
            return searchAndInsert(node, node.getLeftChild(), data);
        }
    }
    private BinaryTreeNode search(BinaryTreeNode node, int data) {
        if(node == null) {
            return null;
        }else if(node.getData() == data) {
            return node;
        }else if(data > node.getData()) {
            return search(node.getRightChild(), data);
        }else {
            return search(node.getLeftChild(), data);
        }
    }
    private BinaryTreeNode searchParent(BinaryTreeNode parent, BinaryTreeNode node, int data) {
        if(node == null) {
            return null;
        }else if(node.getData() == data) {
            return parent;
        }else if(data > node.getData()) {
            return searchParent(node, node.getRightChild(), data);
        }else {
            return searchParent(node, node.getLeftChild(), data);
        }
    }
    
}

三、平衡二叉树

Balanced Binary Tree,又叫AVL树(由提出者名字缩写而来)。简单来说,在二叉查找树的基础上,要保持左右子树高度差不超过1,我们把左子树高度减去右子树高度叫做平衡因子(Balanced Factor,BF)
二叉查找树查找的时间复杂度最好是O(logn),最坏是O(n),而AVL最好最坏都是O(logn),插入和删除也是O(logn)。

代码暂时省略,以后回来补充

四、B-树、B+树

B-树

B减树和B树是一个意思,那个不是减号而是短横。这个不纠结,意思明白就行。
B树是用于在外存工作的平衡搜索树,MySQL中的索引主要是基于hash表或者B+树

为什么不用二叉查找树?

数据库索引为啥不用二叉查找树实现呢?
二叉查找树的时间复杂度为O(logn),从算法逻辑上讲查找速度和比较次数都是最小的。但是,现实要考虑 磁盘IO
数据库索引是存在磁盘上的,因为数据量很大的时候索引大小可能达到几个G。所以在利用索引查询的时候,不能把整个索引全部加载到内存,只有逐一加载每一个磁盘页,这里磁盘页对应着索引树的节点,如图(图片源于程序员小灰)。
此处输入图片的描述

当数据比较大,无法全部存入内存时,需要将部分数据存在外存中,在需要的时候读入内存,修改之后又写回外存。由于外存的速度与内存有几个数量级的差别,所以节省在外存上花的时间,对搜索树的性能提高时最有效的。
最常见的外存就是磁盘。磁盘是块设备,也就是说磁盘的读写单位是以块为单位,一般地块大小从0.5k到4k。即使你只读取一个字节,磁盘也是将包含该字节的所有数据读取到硬盘中。而在磁盘读取过程中,最占用时间的是磁盘的寻道,也就是磁头在盘片上找到需要读取的块所在位置的时间,而在盘片上顺序读取数据的所花的时间是占比比较小的。

要减少外存上花的时间,就可以从减少读盘次数以及减少寻道时间着手。B树采取的方法就是,就充分的利用盘块的空间,在一个盘块中尽可能多的存储信息,或者在连续的盘块地址上存储尽可能多的信息。在数据结构上的变化就是每个节点存储多个key信息以及包含多个子节点。

磁盘的IO次数由树的高度决定的,在最坏的情况下磁盘的IO数等于索引树的高度。而B-树是一棵平衡的m-路查找树,一个m-路查找树,高度为h,每一个节点最多容纳m-1个关键字,所以一棵m-路查找树总共可容纳m^k - 1个关键字。
与二叉查找树比较,当高度为h,能容纳2^h - 1个关键字,高度若为3,则二叉查找树只能容纳7个关键字;而对于200-路查找树可以容纳200^3 - 1个关键字!

再说回来,为了减少磁盘的IO次数,就需要把原来“瘦高”的树结构变得“矮胖”,而这个正满足B树的特征之一。
B树是多路平衡查找树,它的每一个节点最多包含k个孩子,k被称为B树的阶,k的大小取决于磁盘页的大小。就像刚才举例200-路查找树一样。

B树文件查找的具体过程

假设每一个磁盘页正好存放一个B树的节点,而子树的指针就是存放另一个磁盘页的地址。
那么查找操作就是:首先是根节点(从磁盘调出数据,进行第一次磁盘I/O,数据读入内存进行查找),内存中可以顺序也可以二分,如果找到要查找的那个数则OK,否则要确认指针的位置,也就是确认是那棵子树,然后递归下去。

此处输入图片的描述
图片来自: v_JULY_v的CSDN博客

B树的特征

  1. 根结点至少有两个子女。
  2. 每个中间节点都包含k-1个元素和k个孩子,其中 m/2 <= k <= m
  3. 每一个叶子节点都包含k-1个元素,其中 m/2 <= k <= m
  4. 所有的叶子结点都位于同一层。
  5. 每个节点中的元素从小到大排列,节点当中k-1个元素正好是k个孩子包含的元素的值域分划。

B树在查询中比较的次数不比二叉查找树少,但是因为B树把多个关键字放在了同一个节点中,这样减少了磁盘的IO次数,同时在内存中比较耗时几乎可以忽略,所以只要树高度足够低,IO次数足够少,就能调高查找性能。

B树的应用场景

B-树主要用于文件系统以及部分数据库索引,比如非关系型数据库MongoDB。

B树的代码实现

先mark在这,暂时不打算搞。

OK,下一篇接着搞树!总结了几个大牛的书和博客的内容,记下笔记,也希望能对你有帮助( ̄︶ ̄)↗ 

看到这里一定是真爱了,有啥疑惑可以留言噢~~
没有的话,再看看我的独立博客——白水东城也OK!哈哈
独立博客刚搞不久,支持云栖社区,也希望大家能支持下俺的博客= ̄ω ̄=

参考

  1. 《轻松学算法》赵烨
  2. 漫画:什么是B-树(程序员小灰)
  3. 从B树、B+树、B*树谈到R 树
  4. B树(Java)实现
目录
相关文章
|
1月前
|
存储 Java 关系型数据库
高效连接之道:Java连接池原理与最佳实践
在Java开发中,数据库连接是应用与数据交互的关键环节。频繁创建和关闭连接会消耗大量资源,导致性能瓶颈。为此,Java连接池技术通过复用连接,实现高效、稳定的数据库连接管理。本文通过案例分析,深入探讨Java连接池的原理与最佳实践,包括连接池的基本操作、配置和使用方法,以及在电商应用中的具体应用示例。
62 5
|
2月前
|
存储 算法 Java
Java HashSet:底层工作原理与实现机制
本文介绍了Java中HashSet的工作原理,包括其基于HashMap实现的底层机制。通过示例代码展示了HashSet如何添加元素,并解析了add方法的具体过程,包括计算hash值、处理碰撞及扩容机制。
|
28天前
|
存储 算法 Java
大厂面试高频:什么是自旋锁?Java 实现自旋锁的原理?
本文详解自旋锁的概念、优缺点、使用场景及Java实现。关注【mikechen的互联网架构】,10年+BAT架构经验倾囊相授。
大厂面试高频:什么是自旋锁?Java 实现自旋锁的原理?
|
28天前
|
Java
Java之CountDownLatch原理浅析
本文介绍了Java并发工具类`CountDownLatch`的使用方法、原理及其与`Thread.join()`的区别。`CountDownLatch`通过构造函数接收一个整数参数作为计数器,调用`countDown`方法减少计数,`await`方法会阻塞当前线程,直到计数为零。文章还详细解析了其内部机制,包括初始化、`countDown`和`await`方法的工作原理,并给出了一个游戏加载场景的示例代码。
Java之CountDownLatch原理浅析
|
1月前
|
Java 索引 容器
Java ArrayList扩容的原理
Java 的 `ArrayList` 是基于数组实现的动态集合。初始时,`ArrayList` 底层创建一个空数组 `elementData`,并设置 `size` 为 0。当首次添加元素时,会调用 `grow` 方法将数组扩容至默认容量 10。之后每次添加元素时,如果当前数组已满,则会再次调用 `grow` 方法进行扩容。扩容规则为:首次扩容至 10,后续扩容至原数组长度的 1.5 倍或根据实际需求扩容。例如,当需要一次性添加 100 个元素时,会直接扩容至 110 而不是 15。
Java ArrayList扩容的原理
|
29天前
|
算法
树的遍历算法有哪些?
不同的遍历算法适用于不同的应用场景。深度优先搜索常用于搜索、路径查找等问题;广度优先搜索则在图的最短路径、层次相关的问题中较为常用;而二叉搜索树的遍历在数据排序、查找等方面有重要应用。
27 2
|
1月前
|
存储 Java 关系型数据库
在Java开发中,数据库连接是应用与数据交互的关键环节。本文通过案例分析,深入探讨Java连接池的原理与最佳实践
在Java开发中,数据库连接是应用与数据交互的关键环节。本文通过案例分析,深入探讨Java连接池的原理与最佳实践,包括连接创建、分配、复用和释放等操作,并通过电商应用实例展示了如何选择合适的连接池库(如HikariCP)和配置参数,实现高效、稳定的数据库连接管理。
58 2
|
1月前
|
Java 数据格式 索引
使用 Java 字节码工具检查类文件完整性的原理是什么
Java字节码工具通过解析和分析类文件的字节码,检查其结构和内容是否符合Java虚拟机规范,确保类文件的完整性和合法性,防止恶意代码或损坏的类文件影响程序运行。
40 5
|
1月前
|
算法 Java 数据库连接
Java连接池技术,从基础概念出发,解析了连接池的工作原理及其重要性
本文详细介绍了Java连接池技术,从基础概念出发,解析了连接池的工作原理及其重要性。连接池通过复用数据库连接,显著提升了应用的性能和稳定性。文章还展示了使用HikariCP连接池的示例代码,帮助读者更好地理解和应用这一技术。
46 1
|
2月前
|
存储 安全 Java
深入理解Java中的FutureTask:用法和原理
【10月更文挑战第28天】`FutureTask` 是 Java 中 `java.util.concurrent` 包下的一个类,实现了 `RunnableFuture` 接口,支持异步计算和结果获取。它可以作为 `Runnable` 被线程执行,同时通过 `Future` 接口获取计算结果。`FutureTask` 可以基于 `Callable` 或 `Runnable` 创建,常用于多线程环境中执行耗时任务,避免阻塞主线程。任务结果可通过 `get` 方法获取,支持阻塞和非阻塞方式。内部使用 AQS 实现同步机制,确保线程安全。