树及其外部存储

术语

图片描述


  1.     树最顶端的节点称为“根”,一棵树只有一个根
  2. 父节点
        每一个节点(除了根)都刚好有一条边向上链接到另一个节点,上面这个节点就称为下面节点的“父节点”
  3. 子节点
        每一个节点均可能有一条或者多条边向下链接到其它节点,下面的这些节点就称为它的“子节点”
  4. 叶节点
        没有子节点的节点称为“叶子节点”或者简称为“叶节点”
  5. 子树
        每一个节点能够做为“子树”的根,它和它全部的子节点构成了这棵树的子树
  6. 路径
        设想顺着链接节点的边从一个节点走到另外一个节点,所通过节点的顺序排列就称为“路径”

二叉树

    若是一棵树中每一个节点最多只能有两个子节点,这样的树就称为“二叉树”,二叉树每一个节点的两个子节点称为“左子节点”和“右子节点”。java

    若是咱们给二叉树加一个额外的条件,就能够获得一种被称做二叉搜索树(binary search tree)的特殊二叉树。二叉搜索树要求:每一个节点都不比它左子树的任意元素小,并且不比它的右子树的任意元素大。(若是咱们假设树中没有重复的元素,那么上述要求能够写成:每一个节点比它左子树的任意节点大,并且比它右子树的任意节点小)node

平衡二叉树

    平衡二叉树(Balanced Binary Tree)具备如下性质:它是一棵空树或它的左右两个子树的高度差的绝对值不超过1,而且左右两个子树都是一棵平衡二叉树。git

二叉搜索树的查找过程

    二叉搜索树能够方便的实现搜索算法。在搜索元素x的时候,咱们能够将x和根节点比较:程序员

  1. 若是x等于根节点,那么找到x,中止搜索 (终止条件)
  2. 若是x小于根节点,那么搜索左子树
  3. 若是x大于根节点,那么搜索右子树

    当二叉搜索树平衡时达到最高搜索效率,时间复杂度为O(logN);当二叉搜索树单调插入数据时,搜索效率最低,此时二叉搜索树至关于链表,时间复杂度为O(N)github

    二叉搜索树的查找代码以下(仅考虑数据不重复的状况):算法

public Node find(int key) {
    Node current = root;
    while (current.data != key) {
        if (key < current.data) {
            current = current.left;
        } else {
            current = current.right;
        }
        if (current == null) {
            return null;
        }
    }
    return current;
}

二叉搜索树插入过程

    二叉搜索树的插入相对简单,二叉查找树的插入过程以下:数据库

  1. 若当前的二叉搜索树为空,则插入的元素为根节点
  2. 若插入的元素值小于根节点值,则将元素插入到左子树中
  3. 若插入的元素值不小于根节点值,则将元素插入到右子树中

    二叉搜索树的插入代码以下(仅考虑数据不重复的状况):数组

public void insert(int key, double data) {
    Node newNode = new Node();
    newNode.key = key;
    newNode.data = data;
    if (root == null) {
        root = newNode;
    } else {
        Node current = root;
        Node parent;
        while (true) {
            parent = current;
            if (key < current.key) {
                current = current.left;
                if (current == null) {
                    parent.left = newNode;
                    return;
                }
            } else {
                current = current.right;
                if (current == null) {
                    parent.right = newNode;
                    return;
                }
            }
        }
    }
}

二叉搜索树删除过程

    二叉搜索树删除过程也分为三种状况:数据结构

  1. 待删除节点是叶节点,此时只要删除该节点,并修改其父节点的指针指向null便可
  2. 待删除节点只有一个子节点,此时只要将父节点的指针指向该节点的子树便可
  3. 待删除节点有两个子节点,此时须要找到该节点的后继节点,用后继节点来代替它

如何查找后继节点

    一个节点的后继节点即全部比该节点大的节点集合中最小的那个节点。为此能够查找该节点的右子树的最左节点便可,如图:
图片描述数据结构和算法

    查找后继节点代码以下:

private Node getSuccessor(Node delNode) {
    Node successorParent = delNode;
    Node successor = delNode;
    Node current = delNode.right;
    while (current != null) {
        successorParent = successor;
        successor = current;
        current = current.left;
    }
    // 节点移位,参照/docs/二叉搜索树后继节点
    if (successor != delNode.right) {
        successorParent.left = successor.right;
        successor.right = delNode.right;
    }
    return successor;
}

    待删除节点有两个子节点的删除过程如图:
图片描述

    删除节点的代码以下:

public void delete(int key) {
    Node current = root;
    Node parent = root;
    boolean isLeftChild = true;
    while (current.data != key) {
        parent = current;
        if (key < current.data) {
            isLeftChild = true;
            current = current.left;
        } else {
            isLeftChild = false;
            current = current.right;
        }
        if (current == null) {
            // 未找到待删除的节点
            return;
        }
    }

    // 没有子节点
    if (current.left == null && current.right == null) {
        if (current == root) {
            root = null;
        } else if (isLeftChild) {
            parent.left = null;
        } else {
            parent.right = null;
        }
    } else if (current.right == null) {
        // 只有左节点
        if (current == root) {
            root = current.left;
        } else if (isLeftChild) {
            parent.left = current.left;
        } else {
            parent.right = current.left;
        }
    } else if (current.left == null) {
        if (current == root) {
            root = current.right;
        } else if (isLeftChild) {
            parent.left = current.right;
        } else {
            parent.right = current.right;
        }
    } else {
        // 两个节点
        Node successor = getSuccessor(current);
        if (current == root) {
            root = successor;
        } else if (isLeftChild) {
            parent.left = successor;
        } else {
            parent.right = successor;
        }
        successor.left = current.left;
    }
}

红黑树

    红黑树(英语:Red–black tree)是平衡二叉搜索树的一种实现方式,是在计算机科学中用到的一种数据结构,典型的用途是实现关联数组。

    红黑树必须知足如下的规则:

  1. 每个节点不是红色就是黑色
  2. 根老是黑色的
  3. 若是节点是红色的,则它的子节点必须是黑色的(反之倒不必定必须为真)
  4. 该树是完美黑色平衡的,即任意空连接到根结点的路径上的黑连接数量相同
  5. 若是一个黑色节点下面有一个红色节点和一个黑色节点,那么红色节点只能是左节点

旋转

    旋转又分为左旋和右旋。一般左旋操做用于将一个向右倾斜的红色连接旋转为向左连接。

    左旋如图所示:
图片描述

代码以下:

private Node rotateLeft(Node node) {
    Node x = node.right;
    node.right = x.left;
    x.left = node;
    x.color = x.left.color;
    x.left.color = RED;
    return x;
}

    右旋如图所示:
图片描述

代码以下:

private Node rotateRight(Node node) {
    Node x = node.left;
    node.left = x.right;
    x.right = node;
    x.color = x.right.color;
    x.right.color = RED;
    return x;
}

颜色变换

    在插入数据过程当中,遇到一个黑色节点下面带有两个红色的子节点就要进行颜色变换。颜色变换规则以下:两个红色子节点变为黑色,黑色父节点一般变为红色,若是父节点是根节点的话,则父节点继续保持为黑色。

代码以下:

private void flipColors(Node node) {
    node.color = !node.color;
    node.left.color = !node.left.color;
    node.right.color = !node.right.color;
}

红黑树的插入过程

    红黑树在插入时,跟二叉搜索树的插入规则是一致的,惟一不一样的是,红黑树要保持自身的平衡,而这能够经过旋转和颜色变换作到。切记,红黑树在旋转和颜色变换的过程当中,必须遵照红黑树的几条规则。

代码以下:

public void insert(int key) {
    root = insert(root, key);
    // 根节点只能是黑色
    root.color = BLACK;
}

private Node insert(Node node, int key) {
    if (node == null) {
        return new Node(key, RED);
    }

    if (key < node.key) {
        node.left = insert(node.left, key);
    } else if (key > node.key) {
        node.right = insert(node.right, key);
    } else {
        node.key = key;
    }

    // 若是一个黑色节点下面的两个节点一个黑色,一个红色,则红色节点只能是左节点
    if (isRed(node.right) && !isRed(node.left)) {
        node = rotateLeft(node);
    }

    // 红色节点下面不能有红色节点
    if (isRed(node.left) && isRed(node.left.left)) {
        node = rotateRight(node);
    }

    // 当一个黑色节点下有两个红色节点,则要进行颜色变换
    if (isRed(node.left) && isRed(node.right)) {
        flipColors(node);
    }
    return node;
}

红黑树的查找和删除过程

    红黑树的查找跟二叉搜索树的查找过程是彻底一致的
    红黑树的删除过程过于复杂,以至于不少程序员用不一样的方法去规避它,其中一种方法是:为已删除的节点作标记而不实际删除它。这里不作进一步的讨论。

    红黑树的详细实现能够参考:红黑树完整代码Java实现

2-3-4树

    2-3-4树是一种多叉树,名字中的二、3和4的含义是指一个节点可能含有的子节点的个数。2-3-4树性质以下:

  1. 任一节点只能是 2 度节点、3 度节点或 4 度节点,不存在元素数为 0 的节点(2度节点和3度节点是指该节点有2个或者3个子节点)
  2. 全部叶子节点都拥有相同的深度(depth)
  3. 元素始终保持排序顺序

    2-3-4树结构图以下:
图片描述

2-3-4树的组织

    为了方便起见,用从0到2的数字给数据项编号,用0到3给子节点链编号。节点中的数据项按照关键字升序排列,习惯上从左到右升序。还加上如下几点:

  1. 根是child0的子树的全部子节点的关键字值小于key0
  2. 根是child1的子树的全部子节点的关键字值大于key0而且小于key1
  3. 根是child2的子树的全部子节点的关键字值大于key1而且小于key2
  4. 根是child3的子树的全部子节点的关键字值大于key2

    如图:
图片描述

节点分裂

    2-3-4树依靠节点分裂来保持自身的平衡性。2-3-4树分裂的规则是自顶向下的,若是根节点或者待插入的节点中数据项已满,就要进行分裂,分裂规则以下:

  1. 建立一个空节点,它是要分裂节点的兄弟,在要分裂节点的右边
  2. 待分裂节点右边的数据项移到右边节点中,左边的数据项保留在原有节点中,中间的数据项上升到父节点中

    如图:
图片描述

2-3树

    2-3树也是一种多叉树,与2-3-4树相似,如今在不少应用程序中还在应用,一些用于2-3树的技术会在B-树中应用。

    2-3树比2-3-4树少一个数据项和一个子节点。节点能够保存1个或者2个数据项,能够有0个、1个、2个或者3个子节点。其它方面,父节点和子节点的关键字值的排列顺序和2-3-4树是同样的。

节点分裂

    2-3树节点分裂和2-4树节点分裂有很大的不一样。2-3树节点分裂是自底向上的(即若插入数据时根节点数据项已满,不进行分裂,只有待插入的节点数据项满时才进行分裂),并且2-3树节点分裂必须用到新数据项。
图片描述

树的外部存储

磁盘布局

    计算机中的机械磁盘是由磁头和圆盘组成,每一个圆盘上划分为多个磁道,每一个磁道又划分为多个扇区。

    磁盘的结构图以下:
图片描述

磁盘读写原理

    系统将文件存储到磁盘上时,按柱面、磁头、扇区的方式进行,即最早是第1磁道的第一磁头(也就是第1盘面的第1磁道)下的全部扇区,而后,是同一柱面的下一磁头,……,一个柱面存储满后就推动到下一个柱面,直到把文件内容所有写入磁盘。

    系统也以相同的顺序读出数据。读出数据时经过告诉磁盘控制器要读出扇区所在的柱面号、磁头号和扇区号(物理地址的三个组成部分)进行(目前可能是经过LBA线性寻址的方式定位)。

磁盘预读

    因为存储介质的特性,磁盘自己存取就比主存慢不少,再加上机械运动耗费(磁盘旋转和磁头移动),磁盘的存取速度每每是主存的几百分之一,所以为了提升效率,要尽可能减小磁盘I/O。为了达到这个目的,磁盘每每不是严格按需读取,而是每次都会预读,即便只须要一个字节,磁盘也会从这个位置开始,顺序向后读取必定长度的数据放入内存。

    预读的长度通常为页(page)的整倍数。页是计算机管理存储器的逻辑块,硬件及操做系统每每将主存和磁盘存储区分割为连续的大小相等的块,每一个存储块称为一页(在许多操做系统中,页得大小一般为4k),主存和磁盘以页为单位交换数据。当程序要读取的数据不在主存中时,会触发一个缺页异常,此时系统会向磁盘发出读盘信号,磁盘会找到数据的起始位置并向后连续读取一页或几页载入内存中,而后异常返回,程序继续运行(详情请参考页面置换算法以及虚拟内存)。

扩展

  • 每一个扇区的弧长是同样的吗?

    目前大多数教程中给出的图片都是老式的机械磁盘的组成。在老式机械磁盘中,每一个磁道的扇区弧长是不同的。越靠内的磁道密度越大,存储的数据也就越多;越靠外的磁道密度越小,存储的数据也就越少。因此,虽然内外磁道的扇区弧长不同,因为密度的缘由,每一个扇区存储的数据量仍然是同样的,都是512B。在新式磁盘中,内外磁道的扇区密度都是相同的,因此新式磁盘每一个扇区的弧长都是同样的。

B-树和B+树

    2-3树和2-3-4树是B树的一种特例,B树的操做与2-3树和2-3-4树大体相同,此处不在过多介绍。

B树为什么适于外部存储

    前面已经简单介绍过,磁盘控制器每次预读几个文件块的内容,因此对于磁盘读写来讲,当须要的数据都在一个文件块中时,磁盘读写次数最少,此时效率是最高的。而B树设计将每一个节点的数据项恰好填满一个文件块.

    假设这样一种极端状况,若是每一个文件块中只有一条记录是咱们须要的。那么当咱们获取第二条记录时又要从新从磁盘加载新的文件块。此时因为磁盘读取次数增多,致使程序的性能大大降低。

B+树

    B+树是B-树的变形。B+树与B树的区别在于:

  1. B+树非叶子节点只保存索引,数据所有保存在叶子节点上
  2. B+树的全部的叶子节点组成了一张链表,便于数据遍历
  3. 对于有M个数据项的B+树,最多只会有M的子节点

如图:
图片描述

MySQL存储引擎对B+树的优化

    基于上文对2-3-4树和2-3树的讨论,传统的B+树也是按照50%的分裂方式,这样节点分裂后,新的节点中只有原来一半的数据量,不但浪费了空间,还形成节点的增多,从而加剧磁盘IO的次数。在目前绝大部分关系型数据库中,都针对B+树索引的递增/递减插入进行了优化,新的分裂策略在插入新数据时,不移动原有页面的任何记录,只是将新插入的记录写到新页面之中,这样原有页面的利用率仍然是100%。

    因此对于MySQL数据库来讲,使用自增主键插入数据时就会造成一个紧凑的索引结构,近似顺序填满。因为每次插入时也不须要移动已有数据,所以效率很高,也不会增长不少开销在维护索引上。

如图:
图片描述

参考资料

相关文章
相关标签/搜索