数据结构(8)数之二叉搜索树

208 阅读13分钟

二叉搜索树的概念

  • 二叉搜索树(BST,Binary Search Tree),也称二叉排序树或二叉查找树

  • 二叉搜索树是一颗二叉树,可以为空;如果不为空,满足以下性质:

    • 非空左子树的所有键值小于其根结点的键值。
    • 非空右子树的所有键值大于其根结点的键值。
    • 左、右子树本身也都是二叉搜索树
    • 二叉搜索树的特点就是相对较小的值总是保存在左结点上,相对较大的值总是保存在右结点上

image.png

二叉搜索树的操作

  • 二叉搜索树常见的操作

    • insert(key):向树中插入一个新的键。
    • search(key):在树中查找一个键,如果结点存在,则返回true;如果不存在,则返回false
    • preOrderTraverse:通过先序遍历方式遍历所有结点。
    • inOrderTraverse:通过中序遍历方式遍历所有结点。
    • postOrderTraverse:通过后序遍历方式遍历所有结点。
    • min:返回树中最小的值/键。
    • max:返回树中最大的值/键。
    • remove(key):从树中移除某个键。

二叉搜索树的封装

二叉搜索树节点和二叉搜索树的结构

image.png

//二叉搜索树节点的封装
        class BinarySearchTreeNode{
            constructor(data){
                this.data=data;//存储数据
                this.left=null;//左节点的引用
                this.right=null;//右节点的引用
            }
        }

对于BinarySearchTree来说,只需要保存根结点即可,因为其他结点都可以通过根结点找到。

//封装一个BinarySearchTree的类
        class BinarySearchTree{
            constructor() {
                //保存根的属性
                this.root = null;
            }
        }

向树中插入数据的方法

代码思路:需要通过两个部分来完成这个功能

先创建insert方法,用于插入根节点

  • 首先根据传入的数据创建对应的二叉搜索树节点

  • 其次向树中插入数据需要分成两种情况:

    • 第一次插入直接修改根节点即可,之前根节点指向空改成指向新节点
    • 不是第一次插入需要与树中的节点进行比较调用insertNode方法决定插入的位置

再创建一个用于内部使用的方法insertNode(),用于插入非根结点

  • 比较树中的节点与新结点的值大小
    • 如果树中节点的值比新节点的值小就判断树中节点的右子树是否为空,为空时插入,不为空时继续往右比较

    • 如果树中节点的值比新节点的值大就判断树中节点的左子树是否为空,为空时插入,不为空时继续往左比较

遍历二叉搜索树

树的遍历,针对所有的二叉树都是适用的,不仅仅是二叉搜索树

树的遍历:

  • 遍历一棵树是指访问树的每个节点(访问到节点可以对每个节点进行某些操作也可以是简单的打印)
  • 但是树和线性结构不太一样,线性结构通常按照从前到后的顺序遍历
  • 树的遍历常见的有三种方式: 先序遍历、中序遍历、后续遍历。(还有程序遍历,使用较少,可以使用队列来完成)

先序遍历

遍历过程为:

  • 1.访问根结点
  • 2.先序遍历其左子树
  • 3.先序遍历其右子树

根节点 -> 左节点 -> 右节点

image.png

前序遍历任何树进行就是从根节点开始,一直往左。

image.png

先序遍历代码思路

代码解析:

  • 遍历树最好的办法就是递归,因为每个节点都可能有自己的子节点,所以递归调用是最好的方式
  • 在先序遍历中在经过节点的时候先将该节点打印出来
  • 然后遍历节点的左子树,再然后遍历节点的右子树
 preOrderTraverse(){
                // 调用preOrderTranversalNode遍历节点的方法
                this.preOrderTraversalNode(this.root);
            }
            // preOrderTraversalNode方法去实现递归操作
            preOrderTraversalNode(node){
                //当前查找到的节点位空时就访问了树中所有的节点,不为空就继续先序遍历
                //node接收传入的节点,handler是处理节点的操作函数,此处就是打印节点
                if (node != null) {
                    // 先传入的是根节点{left:node,data:11,right:node}就不为空
                    // 传入的节点不为空执行if语句,就进行前序遍历,先遍历左子树,再遍历右子树
                    //1.先遍历根节点,此处访问根节点时打印根节点的数据
                    console.log(node.data);
                    // 2.前序遍历左子树,递归遍历左子树
                    this.preOrderTraversalNode(node.left);
                    // 3.前序遍历右子树,递归遍历右子树
                    this.preOrderTraversalNode(node.right);
                }
            }

03615D3A2C6FC479178CBB561868D011.png

中序遍历

遍历过程为:

  • 1.中序遍历其左子树;
  • 2.访问根结点;
  • 3.中序遍历其右子树。

左节点 -> 根节点 -> 右节点

image.png

拿一面镜子,在树底从左向右移动,取得照到的所有节点。

image.png

//中序遍历所有节点,中序遍历是左根右,二叉搜索树中序遍历的结果是从小到的
            inOrderTraversal(){
                this.inOrderTraversalNode(this.root);
            }
            inOrderTraversalNode(node){
                if(node!=null){
                    //1.节点不为空,中序遍历先遍历左子树
                    this.inOrderTraversalNode(node.left);
                    //2.再遍历根节点,此处访问根节点时打印根节点的数据
                    console.log(node.data);
                    //3.最后遍历右子树
                    this.inOrderTraversalNode(node.right);
                }
            }

函数自调用运行顺序同上和先序遍历相同。

后序遍历

遍历过程为:

  • 1.后序遍历其左子树;
  • 2.后序遍历其右子树;
  • 3.访问根结点。

左节点 -> 右节点 -> 根节点

image.png

一个接一个扯掉最左边的叶节点

image.png

//后序遍历,遍历顺序是左右根
            postOrderTraversal() {
                this.postOrderTraversalNode(this.root)
            }
            postOrderTraversalNode(node){
                // 遍历顺序是左右根
                if(node!=null){
                    //先访问左子树
                    this.postOrderTraversalNode(node.left);
                    //再访问右子树
                    this.postOrderTraversalNode(node.right);
                    //最后访问根节点,打印其数据
                    console.log(node.data);
                }
            }

得到树中的最大值和最小值

// 找树中的最大值
            getMax() {
                // 树中的最大值只可能在树的右子树中,从根节点的右子树中开始一直往右查找直到子树的右边为空
                let current = this.root;//current变量用于保存当前查找的节点
                while (current.right) {
                    //当前查找的节点current.right=null时,while判断条件null做布尔判断为false
                    current = current.right;
                }
                //最后while循环结束current就是最大的节点,返回其数据
                return current.data;
            }
            // 找树中的最小值
            getMin() {
                // 树中的最小值只可能在树的左子树中,从根节点的左子树中开始一直往左查找直到子树的左边为空
                let current = this.root;//current变量用于保存当前查找的节点
                while (current.left) {
                    //当前查找的节点current.left=null时,while判断条件null做布尔判断为false
                    current = current.left;
                }
                //最后while循环结束current就是最大的节点,返回其数据
                return current.data;
            }
  • 代码解析:

    • 代码依次向左找到最左边的结点就是最小值,
    • 代码依次向右找到最右边的结点就是最大值.

搜索特定的值

//查找元素是否存在于树中
            search(element) {
                //从根节点开始查找,element大于当前查找到的节点就继续往右查找,小于就继续向左查找,当等于时就返回true,直到当前查找的节点为null时就结束查找,表示树中没有该元素返回false
                if (this.root == null) {
                    return false;
                } else {
                    //根节点不为空,从根节点开始查找,current保存当前查找到的节点
                    let current = this.root;
                    while (current) {
                        //while循环结束条件时查找完了树中所有的节点,则current就为空,做布尔判断为false结束while循环
                        if (element > current.data) {
                            //查找的元素大于当前节点的数据,则向右继续找
                            current = current.right;
                        } else if (element < current.data) {
                            //查找的元素小于当前节点的数据,则向左继续找
                            current = current.left;
                        } else {
                            //查找的元素等于于当前节点的数据,就返回true
                            return true;
                        }
                    }
                    //当while循环表示查找了树中所有的元素都没有返回false
                    return false;
                }
            }

代码解析:

  • 使用循环实现

    • node == null,循环结束也就是后面不再有节点的,访问了整棵树都没有该节点
    • 找到对应的element, 也就是node.data ==element的时候直接返回true
    • 如果node.data> element, 那么说明传入的值更小, 需要向左查找继续循环.
    • 如果node.data < element 那么说明传入的值更大, 需要向右查找继续循环.

二叉搜索树的删除

删除节点的思路

删除节点要从查找要删的节点开始, 当找到节点后, 需要考虑三大种情况:

  • 1.该节点是叶结点(没有子节点),然后需要分成没有子节点的根节点以及不是根节点的叶子节点两种情况
  • 2.该节点有一个子节点,也需要分为只有一个子节点的根节点,以及只有一个子节点的非根节点
  • 3.该节点有两个子节点,也需要分为根节点和非根节点两种情况

image.png

//定义临时保存的变量
let current = this.root;
//查找到要删除节点需要变量来保存要删除节点的父节点,以及变量来判断要删除的节点是父节点的左子节点还是右子节点
let father;//保存当前查找到的节点的父节点
let isLeftChild;//用于判断当前查找到的节点是父节点的左子节点还是右子节点
//查找要删除的节点
while (element != current.data) {
/*当前查找到的节点current的数据等于需要删除的数据时就停止查找,
element=current.data时element!=current.data
做布尔判断为false结束while循环,current就是要删除的节点*/
if (element < current.data) {
//element小于当前查找节点就继续向当前查找节点的左子树查找
father = current;//每次查找需要将当前查找节点保存下来,再继续查找当前节点的子节点
current = current.left;
isLeftChild = true;//true左子树查找表示是左子节点
 } else {
 //右子树查找
father = current;
 current = current.right;
isLeftChild = false;//false表示是右子节点
 }
  }
//while循环完毕current就是要删除的节点

第1种大情况

if (deletenode.left == null && deletenode.right == null) {
                        //第1种情况没有子节点,删除叶子节点,节点左右都为空
                        if (deletenode == this.root) {
                            //删除的是根节点,根节点左右子树都为空,树中只有一个根节点,将根节点置空即可
                            this.root = null;
                        } else {
                            //删除的不是根节点,如果要删除的节点是其父节点的右子节点就将父节点的右侧置空,要删除的节点是其父节点的左子节点就将要删除的节点的父节点左侧置空
                            if (isLeftChild) {
                                //true执行if语句,表示要删除的节点是其父节点的左子节点
                                father.left = null;
                            } else {
                                //false执行else语句,表示要删除的节点是其父节点的右子节点
                                father.right = null;
                            }
                        }
                    }

983B6A8897A5A2E6BAEFE7CBE9BBCFD4.png

第2种大情况

if (deletenode.left != null && deletenode.right == null) {
                        //第2种情况中只有一个左子节点
                        if (deletenode == this.root) {
                            //要删除的节点是根节点,就让要删除的节点的左子节点成为根节点
                            this.root = deletenode.left;
                        } else {
                            //不是根节点就需要将要删除的节点的左子节点连接到要删除的节点的父节点
                            if (isLeftChild) {
                                //true表示要删除的节点是父节点的左节点,就将要删除的节点的左子节点连接到要删除的节点是父节点的左侧
                                father.left = deletenode.left;
                            } else {
                                //flase表示要删除的节点是父节点的右节点,就将要删除的节点的左子节点连接到要删除的节点是父节点的右侧
                                father.right = deletenode.left;
                            }
                        }
                    }else if (deletenode.left == null && deletenode.right != null) {
                        //第2种情况中只有一个右子节点
                        if (deletenode == this.root) {
                            //要删除的节点是根节点,就让要删除的节点的右子节点成为根节点
                            this.root = deletenode.rightt;
                        } else {
                            //不是根节点就需要将要删除的节点的右子节点连接到要删除的节点的父节点
                            if (isLeftChild) {
                                //true表示要删除的节点是父节点的左节点,就将要删除的节点的右子节点连接到要删除的节点是父节点的左侧
                                father.left = deletenode.right;
                            } else {
                                //flase表示要删除的节点是父节点的右节点,就将要删除的节点的右子节点连接到要删除的节点是父节点的右侧
                                father.right = deletenode.right;
                            }
                        }
                    }

40E3857ECD0CEC9067220A3735BA6A04.png

第3种大情况

第3种情况节点有两个子节点,而子节点可能还有子节点,就需要在树中先找到可以接替要删除的节点将其连接到树中接替其他节点,新的节点需要满足是要删除节点的左子树中值最大的,又比要删除节点的右子树所有节点值小,所以新节点是要删除节点左子树中的最大值或者要删除节点右子树中的最小值,这两个值最接近原来要删除节点的值。

前驱&后继

  • 而在二叉搜索树中, 这两个特别的节点, 有两个特比的名字.
  • 比要删除的节点小一点点的节点, 称为要删除节点的前驱,一定是要删除节点左子树的最大值,就在要删除节点的左子树中向右一直查找直到为空
  • 比要删除的节点大一点点的节点, 称为要删除节点的后继,一定是要删除节点右子树的最小值,就在要删除节点的右子树中向左一直查找直到为空

此处以后继为例,找到后继节点后,后继节点的左侧不可能有节点,但是后继节点的右子树可能会有子节点以及子节点的子节点。

后继节点右侧有节点时需要先将后继节点右侧的节点连接到后继节点的父节点的左侧,再将后继节点的右侧指向要删除节点的右侧,然后后继节点的左侧连接到要删除节点的左侧,最后根据要删除节点是其父节点的左子节点或者右子节点来确定后继节点连接到要删除节点的父节点的左侧或者右侧。此时就需要考虑到后继节点是否是要删除的节点的右子节点这种特殊情况,这种情况就不需要先将后继节点右侧的节点连接到后继节点的父节点的左侧,再将后继节点的右侧指向要删除节点的右侧,直接将后继节点的左侧连接到要删除节点的左侧,然后根据要删除节点是其父节点的左子节点或者右子节点来确定后继节点连接到要删除节点的父节点的左侧或者右侧。。

//找要删除节点的后继节点方法:后继节点是右子树的最小值
            gethoujiNode(node) {
                //node接收传入的要删除的节点,从要删除的节点右子树开始找,在右子树中往左寻找直到当前寻找的节点的左侧为空才是右子树中最小的值就找到了后继节点
                let current = node.right;//current变量保存当前查找的节点,从要删除的节点的右侧开始查找
                let father = null;//用于保存当前查找的节点的父节点
                while (current.left) {
                    //当current.left的值为null时做布尔判断为false,结束while循环就表示current当前找到的节点就是后继节点
                    father = current;//保存当前查找的节点的父节点
                    current = current.left;//继续向左查找
                }
                //后继节点的左侧不可能有节点只能为空,因为后继节点是右子树中的最小值,但后继节点可能存在右子树
                //当后继节点是要删除节点的右节点时,直接将后继节点的左侧连接到要删除节点的左侧,然后根据要删除节点是其父节点的左子节点或者右子节点来确定后继节点连接到要删除节点的父节点的左侧或者右侧。
                if (current != node.right) {
                    //当后继节点不是要删除节点的右节点,需要将右侧的节点连接到后继节点的父节点的左侧,然后将后继节点的右侧指向要删除节点的右侧
                    father.left = current.right;//将后继节点右侧的节点连接到后继节点的父节点的左侧
                    current.right=node.right;//将后继节点的右侧指向要删除节点的右侧
                }
                //返回后继节点current
                return current;
            }

E8BEFA4508D16E90122114141A961445.png

777CF297A187A8286EA3ED9B875BC86B.png

let houjinode = this.gethoujiNode(deletenode);
               if (deletenode == this.root) {
                                     //要删除的节点是根节点,先让根节点换成后继节点,再将后继节点的左侧连接到要删除节点的左侧
                            this.root = houjinode;
                            houjinode.left = deletenode.left;//将后继节点的左侧连接到要删除节点的左侧
                        } else {
                            //要删除的节点不是根节点
                            if (isLeftChild) {
                                //true表示要删除的节点是其父节点的左子节点,将父节点的左侧连接到后继节点,然后将后继节点的左侧连接到要删除节点的左侧
                                father.left = houjinode;
                                houjinode.left = deletenode.left;//将后继节点的左侧连接到要删除节点的左侧
                            } else {
                                //false表示要删除的节点是其父节点的左子节点,将父节点的右侧连接到后继节点,然后将后继节点的左侧连接到要删除节点的左侧
                                father.right = houjinode;
                                houjinode.left = deletenode.left;//将后继节点的左侧连接到要删除节点的左侧
                            }
                        }

关于前中后遍历的笔试题

image.png

image.png

image.png

image.png

image.png