数据结构-二叉树

树结构

  • 树(Tree): n(n>=0)个节点构成的有限集合。
    • 当 n = 0时,称为空树
  • 对于任一课非空树(n>0),它具备以下性质:
    • 树中有一个成为“根(root)”的特殊节点,用 r标识
    • 其余节点分为m(m>0)个互不相交的有限集T1,T2,T3....Tm, 其中每个集合本身又是一颗树,称为原来树的“子树(SubTree)”

树的术语

  1. 节点的度: 节点的子树个数(比如二叉树一个节点只有两个分支,度为2. 最下面的节点,没有分支了,度为0)。
  2. 树的度: 树的所有节点中最大的度数 (如果某个节点有4个分支,其他节点都没这么多,那么树的度为4)。
  3. 叶节点: 度为0的节点
  4. 父节点:有子树的节点
  5. 子节点:若A节点是B节点的父节点,则称B节点是A节点的子节点
  6. 兄弟节点: 具有同一父节点的节点彼此称为兄弟节点
  7. 路径和路径长度: 从节点n1到nk的路径称为一个节点的序列n1,n2,n3....nk, ni是ni+1的父节点。 路径说包含 的个数为路径的长度。(从跟节点沿着分支一直走到底,没有其他分支了。这个就是路径。路径中每个节点连接的分支就是边 )
  8. 节点的层次: 规定根节点在1层, 其他任一节点的层数是其父节点的层数加1
  9. 树的深度: 所有节点的最大层级是这个树的深度。

二叉树

二叉树(binary tree)是指树中节点的度不大于2的有序树,它是一种最简单且最重要的树。二叉树的递归定义为:二叉树是一棵空树,或者是一棵由一个根节点和两棵互不相交的,分别称作根的左子树和右子树组成的非空树;左子树和右子树又同样都是二叉树 [2]

特殊类型

  • 满二叉树:如果一棵二叉树只有度为0的结点和度为2的结点,并且度为0的结点在同一层上,则这棵二叉树为满二叉树 。
  • 完全二叉树:深度为k,有n个结点的二叉树当且仅当其每一个结点都与深度为k的满二叉树中编号从1到n的结点一一对应时,称为完全二叉树

任何一棵树都可以转换为二叉树


image.png

二叉树的存储

  • 数组(2n-1找到左边的节点, 2n+2找右边的节点)
image.png
  • 链表(使用链表更合适)


    image.png

二叉搜索树

二叉搜索树(Binary Search Tree),(又:二叉查找树,二叉排序树)

  • 二叉搜索树是一颗二叉树,可以为空
  • 如果不为空,满足以下性质:
    • 非空左子树的所有键值小于根节点的键值
    • 非空右子树的所有键值大于根节点的键值
    • 左、右子树本身也是二叉搜索树

特点

  • 二叉搜索树的特点是 较小的值 总是保存在左节点上, 较大的值总是保存在右节点上

树的遍历

树的遍历是树的一种重要的运算。所谓遍历是指对树中所有结点的系统的访问,即依次对树中每个结点访问一次且仅访问一次。树的3种最重要的遍历方式分别称为前序遍历、中序遍历和后序遍历。 (先访问父节点(从根节点开始)就叫前序, 中间访问父节点叫做中序, 最后访问父节点叫做后序)

js 实现代码

   // 创建BinarySearchTree
    function BinarySerachTree() {
        // 创建节点构造函数
        function Node(key) {
            this.key = key
            this.left = null
            this.right = null
        }

        // 保存根的属性
        this.root = null

        // 二叉搜索树相关的操作方法
        // 向树中插入数据
        BinarySerachTree.prototype.insert = function (key) {
            // 1.根据key创建对应的node
            var newNode = new Node(key)

            // 2.判断根节点是否有值,没有直接赋值
            if (this.root === null) {
                this.root = newNode
            } else {
                this.insertNode(this.root, newNode)
            }
        }

        BinarySerachTree.prototype.insertNode = function (node, newNode) {
            // 1.准备向左子树插入数据 插入的值比当前节点的值小,往左节点插入
            if (newNode.key < node.key) { 
                if (node.left === null) { // 1.1.node的左子树上没有内容
                    node.left = newNode
                } else { 
                    // 1.2.node的左子树上已经有了内容 递归调用,直到插入到正确的位置
                    this.insertNode(node.left, newNode)
                }
            } else { // 2.准备向右子树插入数据
                if (node.right === null) { // 2.1.node的右子树上没有内容
                    node.right = newNode
                } else { // 2.2.node的右子树上有内容
                    this.insertNode(node.right, newNode)
                }
            }
        }

        // 获取最大值和最小值
        // 最小值,最左边的节点值
        BinarySerachTree.prototype.min = function () {
            var node = this.root
            while (node.left !== null) {
                node = node.left
            }
            return node.key
        }
        // 最大值 最右边的节点值
        BinarySerachTree.prototype.max = function () {
            var node = this.root
            while (node.right !== null) {
                node = node.right
            }
            return node.key
        }

        // 搜搜特定的值
        /*
        BinarySerachTree.prototype.search = function (key) {
            return this.searchNode(this.root, key)
        }

        BinarySerachTree.prototype.searchNode = function (node, key) {
            // 1.如果传入的node为null那么, 那么就退出递归
            if (node === null) {
                return false
            }

            // 2.判断node节点的值和传入的key大小
            if (node.key > key) { // 2.1.传入的key较小, 向左边继续查找
                return this.searchNode(node.left, key)
            } else if (node.key < key) { // 2.2.传入的key较大, 向右边继续查找
                return this.searchNode(node.right, key)
            } else { // 2.3.相同, 说明找到了key
                return true
            }
        }
        */
        //    查找值 存不存在
        BinarySerachTree.prototype.search = function (key) {
            var node = this.root
            while (node !== null) {
                if (node.key > key) {
                    node = node.left
                } else if (node.key < key) {
                    node = node.right
                } else {
                    // 找到 返回 true
                    return true
                }
            }
            // 其他情况 返回 false
            return false
        }

        BinarySerachTree.prototype.removeNode = function (node, key) {
            // 1.如果传入的node为null, 直接退出递归.
            if (node === null) return null

            // 2.判断key和对应node.key的大小
            if (node.key > key) {
                node.left = this.removeNode(node.left, key)

            }
        }

        // 删除结点
        BinarySerachTree.prototype.remove = function (key) {
            // 1.定义临时保存的变量
            var current = this.root
            // 当前节点的父节点   初始根节点无父节点
            var parent = null
            // 标识 判断是否是 左节点
            var isLeftChild = true

            // 2.开始查找节点
            while (current.key !== key) {
                // 再查找子节点前,先把当前节点保存到parent中。 就是之后节点的父节点
                parent = current
                // 如果 需要删除的值小于 当前节点值
                if (key < current.key) {
                    // 设置 标识 isLeftChild 为true  
                    // 并 把当前节点设置为 左节点
                    isLeftChild = true
                    current = current.left
                } else {
                    isLeftChild = false
                    current = current.right
                }

                // 如果发现current已经指向null, 那么说明没有找到要删除的数据
                if (current === null) return false
            }

            //  到这里没有 return  说名找到了节点
            // 3.删除的结点是叶结点 该 节点 没有 子节点 最简单
            if (current.left === null && current.right === null) {
                // 如果当前节点就是根节点,并且没有子节点 。直接把根节点 置为 null 即可
                if (current == this.root) {
                    this.root == null
                } else if (isLeftChild) {
                    // 如果 这是 左节点 ,那把当前节点的父节点 的 left置为null
                    parent.left = null
                } else {
                    parent.right = null
                }
            }

            // 4.删除有一个子节点的节点 (上面已经排除了 左右节点同时 不存在的情况)
            // 只有 判断 一个节点不存在的情况,另一个节点肯定存在
            else if (current.right === null) {
                // 如果当前节点是根节点,右节点不存在。 那么直接把左节点赋值给 根节点
                // 根节点就被删除了。 原来根节点的左节点作为 现在的根节点
                if (current == this.root) {
                    this.root = current.left
                } else if (isLeftChild) {
                    // 如果需要删除的节点 是左节点,并且不存在右子节点的情况,
                    // 把删除节点的左节点赋值为  父节点的左节点即可
                    parent.left = current.left
                } else {
                    parent.right = current.left
                }
            } else if (current.left === null) {
                if (current == this.root) {
                    this.root = current.right
                } else if (isLeftChild) {
                    parent.left = current.right
                } else {
                    parent.right = current.right
                }
            }

            // 5.删除有两个节点的节点
            // 存在两个节点的情况  比较复杂。 要么采用前驱找到左边的最大节点(都小于右边的节点值),要么才有后继找到右边的最小节点(都大于左边的节点值)
            // 替换掉需要删除的节点
            else {
                // 1.获取后继节点 (这里用后继方式)
                // getSuccessor获取的后继节点 以及处理好了右子节点
                var successor = this.getSuccessor(current)

                // 2.判断是否是根节点
                // 如果需要删除的是根节点,那么直接把右边找到的最小节点赋值给root即可
                if (current == this.root) {
                    this.root = successor
                } else if (isLeftChild) {
                    // 如果需要删除的节点是左节点
                    // 把删除节点的父节点 的left 改为 后继节点
                    parent.left = successor
                } else {
                    parent.right = successor
                }

                // 上面只是 把后继节点接上 了父节点
                // 还需要把删除节点的左子节点  拼接到 后继节点上的左节点上
                // 3.将删除节点的左子树赋值给successor
                successor.left = current.left
            }

            return true
        }

        // 前驱,是小于x.key的最大关键字的结点。(获取需要删除节点,左边节点里面最大的节点值| 左边节点,一直往右)
        // 后继,是大于x.key的最小关键字的结点 (获取需要删除节点,右边节点里面的最小的节点值 | 右边节点一直往左)
        // 找后继的方法 
        BinarySerachTree.prototype.getSuccessor = function (delNode) {
            // 1.使用变量保存临时的节点
            // 保存 后继节点的父节点
            var successorParent = delNode
            // 保存找到的后继 默认为删除节点
            var successor = delNode
            // 要从右子树开始找
            var current = delNode.right 

            // 2.寻找节点
            // current 为 null 的情况下 就找到了
            // 一直往昨早
            while (current != null) {
                successorParent = successor
                successor = current
                current = current.left
            }

            // 1. 如果后继节点 不是 删除节点的右节点(即 删除的节点不是 刚好是只有一个右节点)
            //  需要把删除节点的 由节点 赋值到 后继节点的右节点
            // 2. 后继节点 虽然没有左节点了。但是存在 右节点的情况。  
            // 需要把 后继节点的右节点 赋值给 后继节点父节点的左节点
            // 然后再把 删除节点的右节点赋值给后继节点的 右节点
            // 返回 getSuccessor 之后  会把后继节点移动到 删除节点的位置
            if (successor != delNode.right) {
                successorParent.left = successorParent.right
                successor.right = delNode.right
            }
        }

        // 遍历方法
        // 先序遍历 (先执行handler函数)
        BinarySerachTree.prototype.preOrderTraversal = function (handler) {
            this.preOrderTranversalNode(this.root, handler)
        }

        BinarySerachTree.prototype.preOrderTranversalNode = function (node, handler) {
            if (node !== null) {
                // 先执行 handler函数
                handler(node.key)
                // 一直递归调用查找左节点,直到没有左节点后,返回上一层,再执行右节点
                // 右节点里面又递归,找左节点。。。。
                this.preOrderTranversalNode(node.left, handler)
                this.preOrderTranversalNode(node.right, handler)
            }
        }

        // 中序遍历(再找到最左边的节点后再执行handler函数)
        BinarySerachTree.prototype.inOrderTraversal = function (handler) {
            this.inOrderTraversalNode(this.root, handler)
        }

        BinarySerachTree.prototype.inOrderTraversalNode = function (node, handler) {
            if (node !== null) {
                this.inOrderTraversalNode(node.left, handler)
                handler(node.key)
                this.inOrderTraversalNode(node.right, handler)
            }
        }

        // 后续遍历
        BinarySerachTree.prototype.postOrderTraversal = function (handler) {
            this.postOrderTraversalNode(this.root, handler)
        }

        BinarySerachTree.prototype.postOrderTraversalNode = function (node, handler) {
            if (node !== null) {
                this.postOrderTraversalNode(node.left, handler)
                this.postOrderTraversalNode(node.right, handler)
                handler(node.key)
            }
        }
    }

    // 测试代码
    var bst = new BinarySerachTree()

    // 插入数据
    bst.insert(11)
    bst.insert(7)
    bst.insert(15)
    bst.insert(5)
    bst.insert(3)
    bst.insert(9)
    bst.insert(8)
    bst.insert(10)
    bst.insert(13)
    bst.insert(12)
    bst.insert(14)
    bst.insert(20)
    bst.insert(18)
    bst.insert(25)

    bst.insert(6)

//    // 测试前序遍历结果
//    var resultString = ""
//    bst.preOrderTraversal(function (key) {
//        resultString += key + " "
//    })
//    alert(resultString) // 11 7 5 3 6 9 8 10 15 13 12 14 20 18 25
//
//    // 测试中序遍历结果
//    resultString = ""
//    bst.inOrderTraversal(function (key) {
//        resultString += key + " "
//    })
//    alert(resultString) // 3 5 6 7 8 9 10 11 12 13 14 15 18 20 25
//
//    // 测试后续遍历结果
//    resultString = ""
//    bst.postOrderTraversal(function (key) {
//        resultString += key + " "
//    })
//    alert(resultString) // 3 6 5 8 10 9 7 12 14 13 18 25 20 15 11
//
//    // 获取最值
//    alert(bst.min()) // 3
//    alert(bst.max()) // 25
//
//    // 查找特定的值
//    alert(bst.search(10)) // true
//    alert(bst.search(21)) // false
    // 查找数据
    alert(bst.remove(10))
    alert(bst.remove(20))
    alert(bst.remove(21))

缺点:
不平衡二叉树: 如果按顺序插入节点,比如 9,8,7,6,5,4,3,1. 这样二叉树就类似于链表了。如果输入序列已经排序,则时间复杂度为O(N)

平衡二叉树/红黑树就是为了将查找的时间复杂度保证在O(logN)范围内

你可能感兴趣的:(数据结构-二叉树)