数据结构基础----二分搜索树

原文:https://loubobooo.com/2018/11/04/%E5%88%9D%E5%AD%A6%E6%95%B0%E6%8D%AE%E7%BB%93%E6%9E%84-%E4%BA%8C%E5%88%86%E6%90%9C%E7%B4%A2%E6%A0%91/

前言

之前我们一直专注在线性数据结构上,在这一章要开始学习计算机领域应用及其广泛的–树结构。

树的概括

下图的数据存储方式便是一种树结构。

和线性数据结构不同的是,线性的数据结构是把所有的数据排成一排,而树结构更像是自然界中树的枝杈,不断延伸,不断扩展,其本身也是一种天然的组织结构,类似于电脑中文件夹这种目录结构。

将数据使用树结构存储后,出奇的高效,其中包括但不限于二分搜索树,平衡二叉树,AVL,红黑树,堆,并查集,线段树,Trie(字典树,前缀树)

二分搜索树的基础

在了解二分搜索树之前,我们首先明确二叉树的概念,首先它和链表一样,同样属于一种动态数据结构。

可以看粗,二叉树中每个元素存在于节点内,和链表不同的是,除了要存放元素e,还要有指向其他节点的leftright两个变量(引用)。

  • 对于树结构来说,二叉树也是最常用的一种数据结构,同时二分搜索树满足二叉树的性质(从本质上讲,二分搜索树也是一棵二叉树)
  • 对于一棵二叉树来说,它只有一个根节点,并且这个根节点是唯一的(如下图中28便是这棵二叉树的根节点)
  • 对于每一个节点来说,它最多只能有两个子节点,指向左和右的两个节点(也可以称之为左孩子和右孩子)
  • 二叉树中,一个孩子都没有的节点,通常称之为叶子节点

在这里,不是所有二叉树都像如下图示的二叉树这样规整(每个节点都有两个孩子,叶子节点也在最底层)

叶子节点:这个节点没有任何孩子节点,这样的节点称之为叶子节点

  • 二叉树有一个非常重要的性质,是具有天然的递归结构(对于每一个节点,它的左孩子同时也是一棵二分搜索树的根节点)。

如下图示,这是一棵满的二叉树。即对于每一个节点来说,除了叶子节点外都有两个孩子节点。

如下图所示,只有左孩子,或者只有右孩子,或者左右孩子都为空,甚至退化成链表等等,这些都满足二叉树的定义。

下面可以来看下什么是二分搜索树。二分搜索树除了满足二叉树的定义(只有一个根节点,有左右孩子节点或者左右孩子节点为空……),还有自己独特的性质,即每个节点的值都要大于其左子树下所有节点的值,同时每个节点的值小于其右子树下所有节点的值

如下图示顾名思义,根节点28大于左子树中最大的值22,并且根节点28小于右子树最小的值29

注:二分搜索树当然有可能不是一棵满的二叉树

为了要能达到这样的性质,就必须让我们存储的元素具有可比较性,那么我们用二分搜索树来存储数据的话,然后来查找一个数据53就会非常简单。由于41是根节点,所以根据二分搜索树的性质53这个节点就一定在41的右子树。这样一来,41的左子树所存储的这些数据就会被忽略,这就大大地加快了查询速度

二分搜索树中添加元素

首先在最开始的时候,在二分搜索树里一个节点都没有

  • 现在添加一个新的元素41,显然这个节点会成为根节点
  • 再来一个新元素28,从根节点出发,判断28比根节点41小,根据二分搜索树的定义、因此添加到41的左子树中
  • 利用二分搜索树的定义,每次添加一个新的元素,从根节点开始,如果小于根节点就插到根节点的左子树,反之插入到根节点的右子树
  • 这个过程以此类推……
    注:我们此时的二分搜索树不包含重复元素

如果想包含重复元素的话,只需定义:左子树小于等于节点;或者右子树大于等于节点

过程如图示:

  • 代码如下:
     
    private class Node{
        public E e;
        public Node left, right;
    
        public Node(E e){
            this.e = e;
            left = null;
            right = null;
        }
    }
    private Node add(Node node, E e){
        if(node == null){
            // 将null的节点连接起来
            size++;
            return new Node(e);
        }
        // 这里用递归,不断调用左孩子节点,然后比较插入的元素和节点e
        if(e.compareTo(node.e) < 0){
            node.left = add(node.left, e);
        }else if(e.compareTo(node.e) > 0){ //e.compareTo(node.e) > 0
            node.right = add(node.right, e);
        }
        // 对相等的情况不做判断了
        return node;
    }
    

二分搜索树的查询

对于查询元素操作来说,只需要看一下节点node所存的元素就好了,不必牵扯添加一个元素之后如何挂接到整个二分搜索树中,因此是相对简单的。

  • 代码如下
     
    // 看二分搜索树中是否包含元素e
    public boolean contains(E e){
        return contains(root, e);
    }
    
    // 看以node为根的二分搜索树中是否包含元素e,递归算法
    private boolean contains(Node node, E e){
        if(node == null){
            return false;
        }
        if(e.compareTo(node.e) == 0){
            return true;
        }else if(e.compareTo(node.e) < 0){
            return contains(node.left, e);
        }else{ // e.compareTo(node.e) > 0
            return contains(node.right, e);
        }
    }
    

二分搜索树的前中后序遍历

前序遍历

  • 对于一个数据结构的遍历,本质很简单,就是把这个数据结构中所存储的所有元素都访问一遍。对应到二分搜索树便是访问一遍所有节点。
  • 对于线性数据(无论数组还是链表),从头到尾做一层循环就解决了,但是对于树结构来说却不是。要访问整个二叉树所有节点,需要考虑左子树所有节点,还要考虑右子树所有节点。

访问完根节点先访问了左子树再访问了右子树,这就完成了二叉树的遍历操作,而这种遍历操作也称之为前序遍历

  • 用代码来实现:
     
    // 二分搜索树的前序遍历
    public void preOrder(){
        preOrder(root);
    }
    
    // 前序遍历以node为根的二分搜索树,递归算法
    private void preOrder(Node node){
        if(node == null){
            return;
        }
        preOrder(node.left);
        preOrder(node.right);
    }
    

中序遍历

通常而言,对于二分搜索来说。前序遍历是最自然的一种遍历方式,同时呢也是最常用的一种遍历方式。
如果我们改变节点的访问顺序,使之 先访问该节点的左子树,再访问该节点,最后访问该节点的右子树,这样的遍历操作称之为中序遍历。

  • 用代码实现
     
    //中序遍历以node为根的二分搜索树,递归算法
    public void inOrder(){
        inOrder(root);
    }
    
    private void inOrder(Node node){
        if(node == null){
            return;
        }
        inOrder(node.left);
        inOrder(node.right);
    }
    

后序遍历

同理,先访问该节点的左子树,再访问该节点的右子树,最后访问这个节点,这样的遍历操作便是后序遍历。

  • 用代码实现
     
    // 二分搜索树的后序遍历
    public void postOrder(){
        postOrder(root);
    }
    
    // 后序遍历以node为根的二分搜索树,递归算法
    private void postOrder(Node node){
        if(node == null){
            return;
        }
        postOrder(node.left);
        postOrder(node.right);
    }
    

二分搜索树的层序(广度优先)遍历

上述二分搜索树的前中后序遍历,本质上都是深度优先的遍历。于此相对应的是广度优先遍历(即层序遍历)。

广度优先遍历

对于二分搜索树来说,每一个节点对应右一个深度值,通常会把根节点叫做深度为0相应的节点。层序遍历就是,先遍历第0层的节点(28),再遍历第1层的节点(16,30),最后遍历第2层的节点(13,22,29,42)。

如下图示:

  • 用代码实现
     
    // 二分搜索树的层序遍历
    public void levelOrer(){
        Queue<Node> queue = new LinkedList<>();
        queue.add(root);
        while(!queue.isEmpty()){
            // 当前遍历的节点 等于 队列中的元素出队之后的那个元素
            Node cur = queue.remove();
            System.out.println(cur.e);
            if(cur.left != null){
                queue.add(cur.left);
            }
            if(cur.right != null){
                queue.add(cur.right);
            }
        }
    }
    

意义

  • 更快的找到问题的解
  • 常用于算法设计中-最短路径
  • 图中的深度优先遍历和广度优先遍历

二分搜索树删除节点

删除最大值和最小值

对于二分搜索树来说,删除一个节点相对是比较复杂的操作。因此分解删除操作,从最简单的,删除二分搜索树的最小值和最大值开始。

  • 首先,要想删除二分搜索树的最小值和最大值,就需要先找到二分搜索树的最小值和最大值
  • 对于二分搜索树来说,其一个节点的左子树中所有节点都小于该节点
  • 因此删除最小值一定是从根节点开始,不停的向左走,直到向左走再也走不动为止,那个值一定是最小值
  • 删除最大值同理

用代码实现:

 
// 寻找二分搜索树的最大元素
public E maximum(){
    if(size == 0){
        throw new IllegalArgumentException("BST is empty!");
    }
    return minimum(root).e;
}

// 返回以node为根的二分搜索树的最大值所在的节点
private Node maximum(Node node){
    if(node.right == null){
        return node;
    }
    return minimum(node.right);
}

// 从二分搜索树中删除最小值所在节点,返回最小值
public E removeMin(){
    E ret = minimum();
    root = removeMin(root);
    return ret;
}

// 删除以node为根的二分搜索树中的最小节点
// 返回删除节点后新的二分搜索树的根
private Node removeMin(Node node){
    // 现在最小节点就是node本身,然后接上node.right
    if(node.left == null){
        Node rightNode = node.right;
        node.right = null;
        size--;
        return rightNode;
    }
    node.left = removeMin(node.left);
    return node;
}

// 从二分搜索树中删除最大值所在节点,返回最大值
public E removeMax(){
    E ret = maximum();
    root = removeMax(root);
    return ret;
}

// 删除以node为根的二分搜索树中的最大节点
// 返回删除节点后新的二分搜索树的根
private Node removeMax(Node node){
    // 现在最小节点就是node本身,然后接上node.right
    if(node.right == null){
        Node leftNode = node.left;
        node.left = null;
        size--;
        return leftNode;
    }
    node.right = removeMax(node.right);
    return node;
}

删除任意节点

在二分搜索树中删除任意节点,会出现以下3种情况:

  • 删除只有左孩子的节点

    要删除的节点元素58这种情况,与删除最大值的逻辑相同,并在删除完成之后,让左孩子所在的二叉树挂接到原来父亲节点

过程如下图所示:

  • 用代码实现:

     
    // 待删除节点右子树为空的情况
    if(node.right == null){
        Node leftNode = node.left;
        node.left = null;
        size--;
        return leftNode;
    }
    
  • 删除只有右孩子的节点

    要删除的节点元素58这种情况,与删除最大值的逻辑相同,并在删除完成之后,让左孩子所在的二叉树挂接到原来父亲节点

过程如下图所示:

  • 用代码实现:

     
    // 待删除节点左子树为空的情况
    if(node.left == null){
        Node rightNode = node.right;
        node.right = null;
        size--;
        return rightNode;
    }
    
  • 删除左右都有孩子的节点d

    1. 找到d节点右子树中最小的值,即s=min(d->right)
    2. 删除s节点,即delMin(d->right)
    3. 连接上之前d的左子树,即s->right。连接上之前d的右子树,即s->left
    4. 删除d,使s成为新的子树的根

注:->表示指向
过程如下图所示:

  • 用代码实现:

     
    // 待删除节点左右子树均不为空的情况
    // 找到比待删除节点大的最小节点,即待删除节点右子树的最小节点
    // 用这个节点顶替待删除节点的位置,successor --> 后继
    Node successor = minimum(node.right);
    successor.right = removeMin(node.right);
    successor.left = node.left;
    
    node.left = node.right = null;
    return successor;
    
  • 完整代码实现

     
    // 从二分搜索树中删除元素为e的节点
    public void remove(E e){
        root = remove(root, e);
    }
    
    // 删除以node为根的二分搜索树中值为e的节点,递归算法
    // 返回删除节点后新的二分搜索树的根
    private Node remove(Node node, E e){
        if(node == null){
            return null;
        }
        if(e.compareTo(node.e) < 0){
            node.left = remove(node.left, e);
            return node;
        }else if(e.compareTo(node.e) > 0){
            node.right = remove(node.right, e);
            return node;
        }else{ // e == node.e
    
            // 待删除节点左子树为空的情况
            if(node.left == null){
                Node rightNode = node.right;
                node.right = null;
                size--;
                return rightNode;
            }
            // 待删除节点右子树为空的情况
            if(node.right == null){
                Node leftNode = node.left;
                node.left = null;
                size--;
                return leftNode;
            }
            // 待删除节点左右子树均不为空的情况
            // 找到比待删除节点大的最小节点,即待删除节点右子树的最小节点
            // 用这个节点顶替待删除节点的位置,successor --> 后继
            Node successor = minimum(node.right);
            successor.right = removeMin(node.right);
            successor.left = node.left;
    
            node.left = node.right = null;
            return successor;
        }
    }
发布了106 篇原创文章 · 获赞 139 · 访问量 40万+

猜你喜欢

转载自blog.csdn.net/mingyuli/article/details/86264162
今日推荐