数据结构与算法--二叉树、二叉查找树、红黑树、多路查找树、二叉堆

目录

树的概念

二叉树

满二叉树

完全二叉树

二叉树的存储

二叉查找树

查找

插入

遍历

时间复杂度

应用

红黑树

红黑树的特征

左旋(RotateLeft)

右旋(RotateRight)

颜色反转

红黑树构建过程

时间复杂度

应用 

多路查找树

B树

B+树

典型应用

二叉堆

二叉堆的存储原理

二叉堆的典型应用


树的概念

树(tree)是n(n≥0)个节点的有限集。

当n=0时,称为空树。在任意一个非空树中,有且仅有一个特定的称为根的节点。

当n>1时,其余节点可分为m(m>0)个互不相交的有限集,每一个集合本身又是一个树,并称为根的子树。

        一个标准的树结构: 

节点1是根节点(root),没有父节点,
节点5、6、7、8是树的末端,没有“孩子”,被称为叶子节点(leaf),
节点2、3、4、是树的中端,有父节点,有孩子,被称为中间节点或枝节点,
图中的虚线部分,是根节点1的其中一个子树,
树的最大层级数,被称为树的高度或深度,上图这个树的高度是4。

 树的分类如下:

 

数据结构和算法的可视化网站:https://www.cs.usfca.edu/~galles/visualization/Algorithms.html

二叉树

        二叉树是树的一种特殊形式,每个节点最多有两个孩子节点。 可能只有一个或者没有孩子节点。

满二叉树

        二叉树的所有非叶子节点都存在左右孩子,且所有叶子节点都在同一层级上,那么这个树就是满二叉树

完全二叉树

        对一个有n个节点的二叉树,按层级顺序编号,则所有节点的编号为从1到n。如果这个树所有节点和同样深度的满二叉树的编号为从1到n的节点位置相同,则这个二叉树为完全二叉树

        满二叉树要求所有分支都是满的;完全二叉树只需保证最后一个节点之前的节点都齐全即可。

二叉树的存储

        二叉树属于逻辑结构,可以使用链表和数组进行存储。 

  • 链式存储

         二叉树的每一个节点包含3部分,存储数据的data变量,left指针与right指针

  • 数组存储

        使用数组存储时,会按照层级顺序把二叉树的节点放到数组中对应的位置上。如果某一个节点的左孩子或右孩子空缺,则数组的相应位置也空出来。

寻址方式:
        一个父节点的下标是n,那么它的左孩子节点下标就是2×n+1、右孩子节点下标就是2*(n+1)
        对于一个稀疏的二叉树来说,用数组表示法非常浪费空间,所以二叉树一般用链表存储实现。(二叉堆除外)

二叉查找树

二叉查找树(二叉排序树)在二叉树的基础上增加了以下几个条件:

        如果左子树不为空,左子树上所有节点的值均小于根节点的值

        如果右子树不为空,右子树上所有节点的值均大于根节点的值

        左、右子树均为二叉查找树

查找

    例如查找值为4的节点,步骤如下:

  1. 访问根节点6,发现4<6。 
  2. 访问节点6的左孩子节点3,发现4>3
  3. 访问节点3的右孩子节点4,发现4=4,这正是要查找的节点

        对于一个节点分布相对均衡的二叉查找树来说,如果节点总数是n,那么搜索节点的时间复杂度就 是O(logn),和树的深度是一样的。这种方式正是二分查找思想。

插入

   例如插入新元素5,步骤如下:

  1. 访问根节点6,发现5<6
  2. 访问节点6的左孩子节点3,发现5>3
  3. 访问节点3的右孩子节点4,发现5>4
  4. 5最终会插入到节点4的右孩子位置

遍历

        二叉树,是典型的非线性数据结构,遍历时需要把非线性关联的节点转化成一个线性的序列,以不同的方式来遍历,遍历出的序列顺序也不同。 

  • 深度优先遍历

输出顺序:        

        前序遍历:根节点、左子树、右子树

        中序遍历:左子树、根节点、右子树

        后序遍历:左子树、右子树、根节点

class TreeNode{
    int data;

    TreeNode leftChild;
    TreeNode rightChild;

    public TreeNode(int data) {
        this.data = data;
    }
}

public class BinaryTree {

    TreeNode root;

    public void insertNode(int data){
        root = insert(root, data);
    }

    private TreeNode insert(TreeNode node, int data){

        if (node == null){
            return new TreeNode(data);
        }

        if (data < node.data){
            node.leftChild = insert(node.leftChild, data);
        } else if (data > node.data){
            node.rightChild = insert(node.rightChild,data);
        } else {
            node.data = data;
        }

        return node;
    }

    /** 前序遍历 */
    public void preOrderTraveral(TreeNode node){
        if (node == null){
            return;
        }
        System.out.println(node.data);
        preOrderTraveral(node.leftChild);
        preOrderTraveral(node.rightChild);
    }

    /** 中序遍历 */
    public void inOrderTraveral(TreeNode node){
        if (node == null){
            return;
        }
        inOrderTraveral(node.leftChild);
        System.out.println(node.data);
        inOrderTraveral(node.rightChild);
    }

    /** 后续遍历 */
    public void postOrderTraveral(TreeNode node){
        if (node == null){
            return;
        }
        postOrderTraveral(node.leftChild);
        postOrderTraveral(node.rightChild);
        System.out.println(node.data);
    }

}
  • 广度优先遍历

        也叫层序遍历,就是二叉树从根节点到叶子节点的层次关系,一层一层横向遍历各个节点。

二叉树同一层次的节点之间是没有直接关联的,利用队列可以实现
1、根节点1进入队列

 2、节点1出队,输出节点1,并得到节点1的左孩子节点2、右孩子节点3。让节点2和节点3入队

 3、节点2出队,输出节点2,并得到节点2的左孩子节点4、右孩子节点5。让节点4和节点5入队

 4、节点3出队,输出节点3,并得到节点3的右孩子节点6。让节点6入队

 5、节点4出队,输出节点4,由于节点4没有孩子节点,所以没有新节点入队

 6、节点5出队,输出节点5,由于节点5同样没有孩子节点,所以没有新节点入队

 7、节点6出队,输出节点6,节点6没有孩子节点,没有新节点入队

public void levelOrderTraversal(TreeNode root){
    Queue<TreeNode> queue = new LinkedList<TreeNode>();
    queue.offer(root);

    while (!queue.isEmpty()){
        TreeNode node = queue.poll();
        System.out.println(node.data);
        if (node.leftChild != null){
            queue.offer(node.leftChild);
        }
        if (node.rightChild != null){
            queue.offer(node.rightChild);
        }
    }
}

时间复杂度

        二叉查找树的插入和查找时间复杂度为:O(logn)
        极端情况下二叉查找树退化成链表,时间复杂度为O(n),所以需要平衡二叉查找树。

应用

        非线性数据:菜单,组织结构、家谱等等
        线性数据:二叉查找树
        二叉查找树是有序的,只需中序遍历,就可以在 O(n) 的时间复杂度内,输出有序序列。
        二叉查找树的性能非常稳定,扩容很方便(链表实现)

红黑树

        在某些特定的情况下,二叉查找树会退化成链表,导致查找效率降低,因此就需要对二叉树进行平衡,而红黑树便解决了这个问题       

红黑树的特征

除了二叉查找树(BST)的特征外,还有以下特征:

  • 每个节点要么是黑色,要么是红色
  • 根节点是黑色
  • 每个叶子节点都是黑色的空结点(NIL结点)(为了简单期间,一般会省略该节点)
  • 如果一个节点是红色的,则它的子节点必须是黑色的(父子不能同为红)
  • 从任一结点到其每个叶子的所有路径都包含相同数目的黑色结点(平衡的关键)
  • 新插入节点默认为红色,插入后需要校验红黑树是否符合规则,不符合则需要进行平衡

        在对红黑树进行添加或者删除操作时可能会破坏这些特点,所以红黑树采取了很多方式来维护这些特点,从而维持平衡。主要包括:左旋转、右旋转和颜色反转

左旋(RotateLeft)

        逆时针旋转红黑树的两个结点,使得父结点被自己的右孩子取代,而自己成为自己的左孩子 

上图所示过程如下:

  1. 以X为基点逆时针旋转
  2. X的父节点被x原来的右孩子Y取代
  3. c保持不变
  4. Y节点原来的左孩子c变成X的右孩子

右旋(RotateRight)

        顺时针旋转红黑树的两个结点,使得父结点被自己的左孩子取代,而自己成为自己的右孩子

上图所示过程如下:

  1. 以X为基点顺时针旋转
  2. X的父节点被x原来的左孩子Y取代
  3. b保持不变
  4. Y节点原来的右孩子c变成X的左孩子

颜色反转

        当前节点与父节点、叔叔节点同为红色,这种情况违反了红黑树的规则,需要将红色向祖辈上传,父节点和叔叔节点红色变为黑色,爷爷节点从黑色变为红色(爷爷节点必为黑色,因为此前是符合红黑树规则的)。这样每条叶子结点到根节点的黑色节点数量并未发生变化,因此都其他树结构不产生影响。 

红黑树插入有五种情况,每种情况对应着不同的调整方法:

1. 新结点(A)位于树根,没有父结点

        直接让新结点变色为黑色,规则2得到满足。同时,黑色的根结点使得每条路径上的黑色结点数目都增加了1,所以并没有打破 规则5

2. 新结点(B)的父结点是黑色

        新插入的红色结点B并没有打破红黑树的规则,所以不需要做任何调整 

3. 新结点(D)的父结点和叔叔结点都是红色

        两个红色结点B和D连续,违反了规则4。因此我们先让结点B变为黑色

         这样一来,结点B所在路径凭空多了一个黑色结点,打破了规则5。因此让结点A变为红色

         结点A和C又成为了连续的红色结点,我们再让结点C变为黑色

         经过上面的调整,这一局部重新符合了红黑树的规则

4. 新结点(D)的父结点是红色,叔叔结点是黑色或者没有叔叔,且新结点是父结点的右孩子,父结点(B)是祖父结点的左孩子
        我们以结点B为轴,做一次左旋转,使得新结点D成为父结点,原来的父结点B成为D的左孩子

        这样进入了情况5

5. 新结点(D)的父结点是红色,叔叔结点是黑色或者没有叔叔,且新结点是父结点的左孩子,父结 点(B)是祖父结点的左孩子

        我们以结点A为轴,做一次右旋转,使得结点B成为祖父结点,结点A成为结点B的右孩子

         接下来,我们让结点B变为黑色,结点A变为红色

        经过上面的调整,这一局部重新符合了红黑树的规则

上图所示过程如下:

        1. 新插入节点默认为红色,5<10,插入到左子节点,插入后左子树深度为2(叶子节点黑色+根节点黑色),右子树深度为也是2(叶子节点黑色+根节点黑色),满足红黑树规则。

        2. 新插入节点为红色,9<10,需要在左子树进行插入,再和5比较,大于5,放到5的右子树中,此时各个叶子节点到根节点的深度依然是2,但5和9两个节点都是红色,不满足规则第4条,需要进行左旋、右旋操作,使其符合规则。可以看出经过操作后,左右子树又维持了平衡。

上图所示过程如下:

        1. 插入节点3后,可以看到又不符合红黑树的规则了,而此时的情况,需要采用颜色反转的操作,就是把5、10两个节点变为黑色,5、10的父节点变为红色,但父节点9是根节点,不能为红色,于是再将9变为黑色,这样整个树的深度其实增加了1层。

        2. 继续插入6节点,对树深度没有影响。

        3. 插入7节点后,6、7节点都为红节点,不满足规则4,需要进行颜色反转调整,也就是7的父节点和叔叔节点变为黑色,爷爷节点5变为红色。

上图所示过程如下:

        1. 继续插入节点19,对树深度没有影响,红黑树的规则都满足,无需调整。

        2. 插入节点32后,又出现了不满足规则4的情况,此时节点32没有叔叔节点,如果颜色反转的话,左右子树的深度就出现不一致的情况,所以需要对爷爷节点进行左旋操作。

        3. 父节点取代爷爷节点的位置,父节点变为黑色,爷爷节点变为父节点的左子树变为红色。

上图所示过程如下:

        1. 插入节点24后,红黑树不满足规则4,需要调整。

        2. 此时父节点32和叔叔节点10都为红色,需要进行颜色反转,爷爷节点19变为红色,父节点、叔叔节点变为黑色,颜色反转树的深度不发生变化。

上图所示过程如下:

        1.插入节点17后,未破坏红黑树规则,不需要调整。

红黑树构建过程

public class RBTreeNode {

    private int key;
    private boolean isBlack;
    private RBTreeNode left;
    private RBTreeNode right;
    private RBTreeNode parent;

    public RBTreeNode(int key) {
        this.key = key;
        this.isBlack = false;
    }

    public int getKey() {
        return key;
    }

    public void setKey(int key) {
        this.key = key;
    }

    public boolean isBlack() {
        return isBlack;
    }

    public void setBlack(boolean black) {
        isBlack = black;
    }

    public RBTreeNode getLeft() {
        return left;
    }

    public void setLeft(RBTreeNode left) {
        this.left = left;
    }

    public RBTreeNode getRight() {
        return right;
    }

    public void setRight(RBTreeNode right) {
        this.right = right;
    }

    public RBTreeNode getParent() {
        return parent;
    }

    public void setParent(RBTreeNode parent) {
        this.parent = parent;
    }

    @Override
    public String toString() {
        return "RBTreeNode{" +
                "key=" + key +
                ", color=" + (isBlack == true ? "BLACK" : "RED") +
                '}';
    }
}
public class RedBlackTree {

    RBTreeNode root;

    public void list(RBTreeNode node){

        if (node == null){
            return;
        }

        if (node.getLeft() == null && node.getRight() == null){
            System.out.println(node);
            return;
        }
        System.out.println(node);
        list(node.getLeft());
        list(node.getRight());
    }

    public void insert(int key){
        RBTreeNode node = new RBTreeNode(key);

        if (root == null){
            node.setBlack(true);
            root = node;
            return;
        }

        RBTreeNode parent = root;
        RBTreeNode son = null;
        if (key <= parent.getKey()) {
            son = parent.getLeft();
        }else {
            son = parent.getRight();
        }

        while (son != null){
            parent = son;
            if (key <= parent.getKey()) {
                son = parent.getLeft();
            }else {
                son = parent.getRight();
            }
        }

        if (key <= parent.getKey()){
            parent.setLeft(node);
        }else {
            parent.setRight(node);
        }
        node.setParent(parent);

        //自平衡
        banlanceInsert(node);
    }

    private void banlanceInsert(RBTreeNode node){
        RBTreeNode father, grandFather;

        while ((father = node.getParent()) != null && father.isBlack() == false){
            grandFather = father.getParent();
            if (grandFather.getLeft() == father){
                RBTreeNode uncle = grandFather.getRight();
                if (uncle != null && uncle.isBlack() == false){
                    setBlack(father);
                    setBlack(uncle);
                    setRed(grandFather);
                    node = grandFather;
                    continue;
                }
                if (node == father.getRight()){
                    leftRotate(father);
                    RBTreeNode tmp = node;
                    node = father;
                    father = tmp;
                }
                setBlack(father);
                setRed(grandFather);
                rightRotate(grandFather);
            } else {
                RBTreeNode uncle = grandFather.getLeft();
                if (uncle != null && uncle.isBlack() == false){
                    setBlack(father);
                    setBlack(uncle);
                    setRed(grandFather);
                    continue;
                }

                if (node == father.getLeft()){
                    rightRotate(father);
                    RBTreeNode tmp = node;
                    node = father;
                    father = tmp;
                }
                setBlack(father);
                setRed(grandFather);
                leftRotate(grandFather);
            }
        }
        setBlack(root);
    }

    private void leftRotate(RBTreeNode node){
        RBTreeNode right = node.getRight();
        RBTreeNode parent = node.getParent();

        if (parent == null){
            root = right;
            right.setParent(null);
        } else {
            if (parent.getLeft() != null && parent.getLeft() == node){
                parent.setLeft(right);
            }else {
                parent.setRight(right);
            }
            right.setParent(parent);
        }
        node.setParent(right);
        node.setRight(right.getLeft());
        if (right.getLeft() != null){
            right.getLeft().setParent(node);
        }
        right.setLeft(node);
    }

    private void rightRotate(RBTreeNode node){
        RBTreeNode left = node.getLeft();
        RBTreeNode parent = node.getParent();

        if (parent == null){
            root = left;
            left.setParent(null);
        } else {
            if (parent.getLeft() != null && parent.getLeft() == node){
                parent.setLeft(left);
            }else {
                parent.setRight(left);
            }
            left.setParent(parent);
        }
        node.setParent(left);
        node.setLeft(left.getRight());
        if (left.getRight() != null){
            left.getRight().setParent(node);
        }
        left.setRight(node);
    }

    private void setBlack(RBTreeNode node){
        node.setBlack(true);
    }

    private void setRed(RBTreeNode node){
        node.setBlack(false);
    }

}

时间复杂度

时间复杂度:O(logn) 

应用 

        在JDK1.8中HashMap使用数组+链表+红黑树的数据结构。内部维护着一个数组table,该数组保存着每 个链表的表头结点或者树的根节点。HashMap存储数据的数组定义如下,里面存放的Node<K,V>实体:

transient Node<K, V>[] table;//序列化时不自动保存

/*** 桶的树化阈值:即 链表转成红黑树的阈值, * 在存储数据时,当链表长度 > 该值时,则将链表转换
成红黑树 */ 
static final int TREEIFY_THRESHOLD = 8;

多路查找树

        多路查找树(muitl-way search tree),每一个节点的孩子数可以多于两个,且每一个节点处可以存储多个元素。

B树

        B树(BalanceTree)是对二叉查找树的改进。它的设计思想是,将相关数据尽量集中在一起,以便一 次读取多个数据,减少硬盘操作次数。

一棵m阶的B 树 (m叉树)的特性如下:

  1. B树中所有节点的孩子节点数中的最大值称为B树的阶,记为M
  2. 树中的每个节点至多有M棵子树 ---即:如果定了M,则这个B树中任何节点的子节点数量都不能超 过M
  3. 若根节点不是终端节点,则至少有两棵子树 除根节点和叶节点外,所有点至少有m/2棵子树
  4. 所有的叶子结点都位于同一层

B+树

B+树是B-树的变体,也是一种多路搜索树,其定义基本与B树相同,它的自身特征是:

  1. 非叶子结点的子树指针与关键字个数相同
  2. 非叶子结点的子树指针P[i],指向关键字值属于[K[i], K[i+1])的子树
  3. 为所有叶子结点增加一个链指针,叶子结点之间互相连通
  4. 所有关键字都在叶子结点出现

典型应用

        MySQL索引B+Tree

        B树是为了磁盘或其它存储设备而设计的一种多叉(下面你会看到,相对于二叉,B树每个内结点有多个 分支,即多叉)平衡查找树。

        多叉平衡

        B树的高度一般都是在2-4这个高度,树的高度直接影响IO读写的次数。

        如果是三层树结构,支撑的数据可以达到20G;如果是四层树结构,支撑的数据可以达到几十T

B和B+的区别

        B树和B+树的最大区别在于非叶子节点是否存储数据的问题。

        B树是非叶子节点和叶子节点都会存储数据。

        B+树只有叶子节点才会存储数据,而且存储的数据都是在一行上,而且这些数据都是有指针指向的,也就是有顺序的。

二叉堆

        二叉堆本质上是一种完全二叉树,它分为两个类型

        1. 大顶堆(最大堆) :最大堆的任何一个父节点的值,都大于或等于它左、右孩子节点的值

         2. 小顶堆(最小堆):最小堆的任何一个父节点的值,都小于或等于它左、右孩子节点的值

        二叉堆的根节点叫作堆顶,最大堆和最小堆的特点决定了:最大堆的堆顶是整个堆中的最大元素;最小堆的堆顶是整个堆中的最小元素

二叉堆的存储原理

        完全二叉树比较适合用数组来存储。用数组来存储完全二叉树是非常节省存储空间的。因为我们不需要 存储左右子节点的指针,单纯地通过数组的下标,就可以找到一个节点的左右子节点和父节点。

        从图中我们可以看到,数组中下标为 i 的节点的左子节点,就是下标为 i∗2 的节点,右子节点就是下标为 i∗2+1 的节点,父节点就是下标为 i/2 取整的节点

二叉堆的典型应用

优先队列、利用堆求 Top K问题

        在一个包含 n 个数据的数组中,我们可以维护一个大小为 K 的小顶堆,顺序遍历数组,从数组中取出数据与堆顶元素比较。如果比堆顶元素大,我们就把堆顶元素删除,并且将这个元素插入到堆中;如果比堆顶元素小,则不做处理,继续遍历数组。这样等数组中的数据都遍历完之后,堆中的数据就是前 K 大数据了

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值