数据结构《学习笔记》——二叉树

387 阅读4分钟

这是我参与8月更文挑战的第8天,活动详情查看:8月更文挑战

一、树的介绍

  • 树:n(n >= 0)个节点构成的有限集合
  • 对于任一棵非空树(n > 0),具备以下性质:
    • 树中有一个称为**"根"**的特殊节点
    • 其余节点可分为m(m > 0)个互不相交的有限集T1、T2、...、Tm,其中每个集合本身又是一棵树,称为原来树的**"子树"**

树的性质是很多的...

  • 节点的度:树的节点个数
  • 叶节点:度为0的节点(也称为叶子节点)
  • 节点的层次:规定根节点在1层,其它任一节点的层数是其父节点的层数加1

二、二叉树

二叉树的特性

  • 一个二叉树第i层的最大节点数为:2^(i-1), i>=1

    比如第一层(根节点)为1个;第二层最多为2节点

  • 深度为k的二叉树有最大节点总数为:2^k-1, k>=1

  • 对任何非空二叉树T,若n0表示叶节点(度为0)的个数,n2表示度为2的非叶节点个数,那么两者满足关系n0 = n2 + 1

a. 完全二叉树

  • 除二叉树最后一层外,其他各层的节点数都达到最大个数

  • 且最后一层从左向右达到叶节点连续存在,只缺右侧若干节点

  • 下面的图从左到右看,E有右子节点但是D没有,所以不是完全二叉树。如果给D加一个右子节点,那么它就是完全二叉树

完全二叉树.png

b. 完美二叉树(满二叉树)

  • 在二叉树中,除了最下一层的叶节点外,每层节点都有2个子节点,就构成了满二叉树

满二叉树.png

二叉树的存储方式可以是数组也可以是链表,但是我们一般用链表。因为使用数组的是否如果该二叉树不是满二叉树,会造成很多空间的浪费。

三、二叉搜索树

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

二叉搜索树初始化

  • 首先,二叉搜索树有一个根节点,初始化让这个根节点指向空;this.root = null;

  • 其次,二叉搜索树的每一个节点包含3个元素,左子节点、右子节点和键值;初始状态下指针指向空

    function Node() {
        this.key = key;
        this.left = null;
        this.right = null;
    }
    

四、二叉搜索树常见方法

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

1. insert方法

  1. 根据key创建节点

  2. 判断根节点是否有值;如果根节点为null时直接插入,否则进行下一步操作

    BinarySearchTree.prototype.insert = function (key) {
        var newNode = new Node(key);
        if (this.root == null) {
            this.root = newNode;
        } else {
            this.insertNode(this.root, newNode);
        }
    }
    
  3. 在搜索树进行插入操作的时候,如果插入的值大于根节点,那么就要把该节点插到根节点的右子节点;如果此时根节点已经有右子节点了,那就应该继续让这个要插入的节点的值和该右子节点进行比较插入;这个过程其实就是在套娃,一直到这个左或者右节点为空的时候就停止,所以这里用递归来实现,用另一个函数来进行对节点的插入。

    BinarySearchTree.prototype.insertNode = function (node, newNode) {
        if (newNode.key < node.key) {
            if (node.left == null) {
                node.left = newNode;
            } else {
                this.insertNode(node.left, newNode);
            }
        } else {
            if (node.right == null) {
                node.right = newNode;
            } else {
                this.insertNode(node.right, newNode);
            }
        }
    }
    

2. 遍历二叉搜索树

由于树的结构是比较特殊的,在遍历节点的时候我们往往都是采用递归的方法来实现。

a. 先序遍历

  • 访问根节点
  • 先先序遍历其左子树,再先序遍历右子树
BinarySearchTree.prototype.preOrderTraversalNode = function (node, handler) {
    if (node != null) {
        handler(node.key);
        this.preOrderTraversalNode(node.left, handler);
        // 3. 处理经过节点的右子节点
        this.preOrderTraversalNode(node.right, handler);
    }
}
//--------
var resultString = ''
tree.preOrderTraversal(function (key) {
    resultString += key + ' ';
})

这里的handler是一个处理结点的回调函数,便于我们看遍历的结果;在先序遍历中,我们要先遍历所有的左子节点,再处理经过节点的右子节点;比如下面这张图:

这里用的递归思路还是有一点复杂的,是一个个函数的嵌套,遍历结束之后一个个跳出来的,需要花一点时间来理解。

遍历7的左节点到5的时候,先遍历3,3是叶节点,继续遍历6(此时的node是5),遍历完5的左右节点之后,就继续遍历7的右子节点。

先序遍历.png

b. 中序遍历

  • 中序遍历其左子树
  • 访问根节点
  • 中序遍历右子树
if (node != null) {
    this.inOrderTraversalNode(node.left, handler);
    handler(node.key);
    this.inOrderTraversalNode(node.right, handler);
}

c. 后序遍历

  • 后序遍历其左子树,再后序遍历其右子树
  • 访问根节点
if (node != null) {
    this.inOrderTraversalNode(node.left, handler);
    this.inOrderTraversalNode(node.right, handler);
    handler(node.key);
}

理解了先序遍历之后,中序遍历和后序遍历都是用递归的方式实现,不同之处在于何时来处理节点。

3. 最大值&最小值

最大值和最小值在数中是很容易找的。最左边的子节点(左下)就是最小值;最右边的子节点(右下)就是最大值。这里演示查找最大值。

  • 获取根节点
  • 依次向右查找,直到节点为null
BinarySearchTree.prototype.max = function () {
    var node = this.root;
    var key = null;
    while (node != null) {
        key = node.key
        node = node.right
    }
    return node.key
}

在查找的时候,一直都找右节点,直到这个右节点的值为空的时候返回这个节点的键值。

4. 搜索特定的值

这个方法可以通过递归的方法实现,也可以通过循环来进行搜索

  • 递归;将查找值和当前节点的值进行对比,如果较小就往左子树找,如果较大就往右子树上查找,直到node.key == key时,返回true,如果经过这些步骤之后还是找不到的话就返回false。

    BinarySearchTree.prototype.searchNode = function (node, key) {
        if (node == null) {
            return false;
        }
        if (node.key > key) {
            return this.searchNode(node.left, key);
        } else if (node.key < key) {
            return this.searchNode(node.right, key);
        } else {
            return true;
        }
        return false;
    }
    
  • 循环 先获取根节点,再循环搜索key

    循环的话会比较好理解一点,就是键值比较,决定往左子树还是右子树遍历,直到找到目标值。

    var node = this.root;
    while (node != null) {
        if (node.key > key) {
            node = node.left;
        } else if (node.key < key) {
            node = node.right;
        } else {
            return true;
        }
    }
    return false;
    

删除操作是最复杂的,那就放在下一篇文章吧!今天学不下去了aaa...codewhy老师真的讲的好棒!