数据结构----树

169 阅读9分钟

一、什么是树?

1.1 定义

是一种数据结构,是一种非顺序数据结构。

树(tree)是包含 n(n≥0)个节点,当 n=0 时,称为空树,非空树中(n-1)条边的有穷集

非空树中

  1. 每个元素称为节点(node)
  2. 有一个特定的节点被称为根节点或树根(root)
  3. 除根节点之外的其余数据元素被分为m(m>=0)个互不相交的集合T1、T2、…、Tn,其中每一个集合 本身也是一棵树,被称作原树的子树。

1.2 树的相关术语

image.png

image.png

  1. 节点的度:一个节点含有的子树的个数称为该节点的度;例如图中节点A的度为2,节点H的度为1
  2. 树的度:一棵树中,最大的节点度称为树的度;例如图中最大的节点B的度为3,树的度为3
  3. 叶节点终端节点:度为零的节点(也称为叶子节点);例如图中的K,J,F,L,O,P
  4. 非终端节点分支节点:度不为零的节点;
  5. 父亲节点父节点:若一个节点含有子节点,则这个节点称为其子节点的父节点;图中A节点就是B 和 C的父节点
  6. 孩子节点子节点:一个节点含有的子树的根节点称为该节点的子节点;图中节点G和节点H为节点C的子节点
  7. 兄弟节点:具有相同父节点的节点互称为兄弟节点;节点B和节点C就是兄弟节点
  8. 节点的层次:从根开始定义起,根为第1层,根的子节点为第2层,以此类推;
  9. 深度:对于任意节点n,n的深度为从根到n的唯一路径长,根的深度为0;
  10. 高度:对于任意节点n,n的高度为从n到一片树叶的最长路径长,所有树叶的高度为0;
  11. 堂兄弟节点:父节点在同一层的节点互为堂兄弟;
  12. 节点的祖先:从根到该节点所经分支上的所有节点;
  13. 子孙:以某节点为根的子树中任一节点都称为该节点的子孙。
  14. 森林:由m(m>=0)棵互不相交的树的集合称为森林;

二、二叉树和二叉树搜索

2.1 二叉树定义

二叉树中的节点最多只能有两个子节点:一个是左侧子节点,另一个是右侧子节点。

2.2 二叉树五种基本形态

  • 空二叉树——如下图1
  • 只有一个根节点的二叉树——如下图2
  • 只有左子树——如下图(c)
  • 只有右子树——如下图(d)
  • 完全二叉树——如下图(e)

image.png

2.3 二叉搜索树

二叉搜索树是二叉树的一种,允许你在左侧节点存储(比父节点)小的值,在右侧节点存储(比父节点)大的值。 下图就是二叉搜索树 image.png

2.4 创建二叉搜索

组织结构:

image.png

2.4.1 创建二叉树类

由定义我们可以知晓,二叉树有左侧子节点和右侧子节点

定义节点

// 定义节点
export class Node{
    constructor(key){
        this.key = key // 节点值
        this.left = null;//左侧子节点引用
        this.right  = null;//右侧子节点引用
    }
}

创建二叉搜索类 在这我们需要引入Compare和defaultCompare方法

export const Compare = {
    LESS_THAN: -1,
    BIGGER_THAN: 1,
    EQUALS: 0
};

export function defaultCompare(a, b) {
    if (a === b) {
        return Compare.EQUALS;
    }
    return a < b ? Compare.LESS_THAN : Compare.BIGGER_THAN;
}

声明BinarySearchTree类的基本结构

import {defaultCompare} from './utils'
export default class BinarySearchTree{
    constructor(compareFn = defaultCompare){
        this.compareFn = compareFn;//用来比较节点的值
        this.root = null;//Node类型的根节点
    }() 返回树中最小的值/键
    - max() 返回树中最大的值/键
    - remove()
}

tree中常用的方法:

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

我们先了解这几个方法的含义,稍后我们将实现这些方法

2.4.2 insert 插入

向树中插入一个新键

  1. 验证插入操作是否是特殊情况
insert(key) {
        // 判断插入的节点是否是第一节点
        if (this.root == null) {
            // 是,创建一个Node类的实例并将他赋值给root属性来将root指向这个新节点
            this.root = new Node(key)
        }else{
            // 将节点添加到根节点以外的位置,需要借助辅助方法
            this.insertNode(this.root,key)
        }
    }
  1. 将节点添加到根节点以外的位置,即insertNode方法

    1. 如果树为空,需要找到插入新节点的位置。因此,参数为树的根节点和要插入的节点

    2. 如果新节点的键小于当前节点的键,那么需要检验当前节点的左侧子节点。注意:由于键可能是复杂的对象而不是数,我们使用传入二叉搜索构造函数的compareFn函数比较值。

      ⅰ. 若没有左侧子节点,就在那里插入新的节点

      ⅱ. 有左侧子节点,则需要通过递归调用insertNode方法,继续找到书的下一层,则下次比较的节点将会是当前节点的左侧子节点

    3. 如果新节点的键大于当前节点的键

      ⅰ. 若没有右侧子节点,则直接插入新节点

      ⅱ. 如果右侧有子节点,则需要递归insertNode方法,继续找到书的下一层,则下次比较的节点将会是当前节点的右侧子节点

insertNode代码

insertNode(node, key) {
        //新节点的键小于当前节点的键,则检查左侧子节点
        if (this.compareFn(key, node.key) === Compare.LESS_THAN) {
            if (node.left == null) {
                //如果没有左侧子节点,就插入新节点
                node.left = new Node(key)
            } else {
                // 如果有左侧子节点,就递归调用insertNode方法,继续找到树的下一层
                this.insertNode(node.left, key)
            }
        } else { //新节点的键大于当前节点的键
            if (node.right == null) {
                node.right = new Node(key)
            } else {
                this.insertNode(node.right, key)
            }
        }
    }

测试代码

const tree = new BinarySearchTree();
tree.insert(11);
// 插入元素
tree.insert(7);
tree.insert(15);
tree.insert(5);
tree.insert(3);
tree.insert(9);
tree.insert(8);
tree.insert(10);
tree.insert(13);
tree.insert(12);
tree.insert(14);
tree.insert(20);
tree.insert(18);
tree.insert(25);

此时树结构为

image.png

若想插入数据

tree.insert(6);

则此时的执行过程为

image.png

上述我们就能够在树种插入元素。大家可以自己体验一下。

2.4.3 实现树的遍历

遍历一颗树是指访问树的每个节点对它们进行某种操作的过程。 通过方法介绍我们也可得知,遍历树有三种方法。我们一起来看下这三种方法的区别吧

  • 中序遍历:左子树---> 根节点 ---> 右子树 左根右
  • 前序遍历:根节点---> 左子树 ---> 右子树 根左右
  • 后序遍历:左子树---> 右子树 ---> 根节点 左右根

2.4.3.1 中序遍历

中序遍历是一种以上行顺序访问BST所有节点的遍历方式,也就是以从最小到最大的顺序访问所有节点。

规则:左子树---> 根节点 ---> 右子树 左根右

先了解上面的规则,我们就能快速的写出中序遍历的实现

inOrderTraverse(callback){
        //接收一个节点和对应的回调函数作为参数
        this.inOrderTraverseNode(this.root,callback)
    }
    inOrderTraverseNode(node,callback){
        // 检查以参数形式传入的节点是否为null,这是停止递归继续执行的判断条件
        if(node != null){
            //调用相同的函数来访问左侧子节点
            this.inOrderTraverseNode(node.left,callback)
            callback(node.key) //对根节点进行操作
            this.inOrderTraverseNode(node.right,callback) //// 再访问右侧子节点
        }
    }

测试代码

const printNode = (value) => console.log(value); 
tree.inOrderTraverse(printNode);

输出:每个数会输出在不同行上 3 5 6 7 8 9 10 11 12 13 14 15 18 20 25

下图绘制了inOrderTraverse方法的访问路径

image.png

2.4.3.2 先序遍历

先序遍历是以优先于后代节点的顺序访问每个节点的。先序遍历的一种应用是打印一个结构化的文档。

规则:根节点---> 左子树 ---> 右子树 根左右

preOrderTraverse(callback){
        this.preOrderTraverseNode(this.root,callback)
    }
    preOrderTraverseNode(node,callback){
        // 检查以参数形式传入的节点是否为null,这是停止递归继续执行的判断条件
        if(node != null){
            callback(node.key) //对根节点进行操作
            //调用相同的函数来访问左侧子节点
            this.preOrderTraverseNode(node.left,callback)
            this.preOrderTraverseNode(node.right,callback) //// 再访问右侧子节点
        }
    }

测试代码

const printNode = (value) => console.log(value);
tree.preOrderTraverse(printNode);

输出:每个数会输出在不同行上 11 7 5 3 6 9 8 10 15 13 12 14 20 18 2

下图绘制了preOrderTraverse方法的访问路径

image.png

2.4.3.3 后序遍历

先序遍历是以优先于后代节点的顺序访问每个节点的。先序遍历的一种应用是打印一个结构化的文档。

规则:左子树 ---> 右子树 ---> 根节点 左右根

postOrderTraverse(callback){
        this.postOrderTraverseNode(this.root,callback)
    }
    postOrderTraverseNode(node,callback){
        // 检查以参数形式传入的节点是否为null,这是停止递归继续执行的判断条件
        if(node != null){
            //调用相同的函数来访问左侧子节点
            this.postOrderTraverseNode(node.left,callback)
            this.postOrderTraverseNode(node.right,callback) //// 再访问右侧子节点
            callback(node.key) //对根节点进行操作
        }
    }

测试代码

const printNode = (value) => console.log(value); 
tree.postOrderTraverse(printNode);

输出:每个数会输出在不同行上 3 6 5 8 10 9 7 12 14 13 18 25 20 15 11

下图绘制了postOrderTraverse方法的访问路径

image.png

2.4.4 搜索树中的位置

在树中,我们常需要进行的搜索类型

  • 搜索最大值
  • 搜索最小值
  • 搜索任意值

image.png

2.4.4.1 搜索最小值

从图中可以看出,最小值为:树最后一层最左侧的节点,即为这颗树的最小值

min(){
        return this.minNode(this.root)
    }
    minNode(node){
        let currentNode = node
        //遍历树的左边,直到找到树的最下层(最左端)
        while(currentNode!= null && currentNode.left!=null){
            currentNode = currentNode.left
        }
        return currentNode
    }
    

2.4.4.2 搜索最大值

从图中可以看出,最大值为:树最后一层最右侧的节点,即为这颗树的最大值

max(){
        return this.maxNode(this.root)
    }
    maxNode(node){
        let currentNode = node
        //遍历树的右边,直到找到树的最下层(最左端)
        while(currentNode!= null && currentNode.right!=null){
            currentNode = currentNode.right
        }
        return currentNode
    }

2.4.4.3 搜索任何值

实现search方法,在树中查找一个键。如果节点存在,则返回true;如果不存在,则返回false

  1. 判断传入的node是否合法(null或者undefined),如果不合法,则说明没有找到,直接返回false
  2. 如果找到的键比当前节点小,则从左侧子节点继续搜索
  3. 如果找到的键比当前节点大,则从右侧子节点继续搜索
  4. 否则找到的键与当前节点相等,返回true来表示找到这个键

我们根据上述的规则来写代码就方便多了

search(key){
        return this.searchNode(this.root,key)
    }
    searchNode(node,key){
        //检验传入的node是否合法
        if(node == null){
            return false
        }
        //要找的键比当前的节点小,则继续在左侧的子树上搜索
        if(this.compareFn(key, node.key) === Compare.LESS_THAN){
            return this.searchNode(node.left,key)
        }
        //要找的键比当前的节点大,从右侧子节点开始继续搜索
        else if(this.compareFn(key, node.key) === Compare.BIGGER_THAN){
            return this.searchNode(node.right,key)
        }
        //要找的键和当前节点相等
        else{
            return true 
        }
    }
    

2.4.5 移除一个节点

remove方法的实现。从树中移除一个节点,我们会想到三种情况

  1. 移除一个叶节点
image.png 2. 移除有一个左侧或者右侧子节点的节点 image.png 3. 移除有两个子节点的节点

image.png

解释:

  • 移除节点15,找到它右侧子树中最小的节点18
  • 用右侧子树中最小节点的键去更新这个节点
  • 上述操作,会是树中有两个拥有相同键的节点,要继续把右侧子树中的最小节点移除
  • 向父节点返回更新后的节点的引用

综上所述,我们慢慢写出移除的代码

remove(key) {
        return this.removeNode(this.root, key);
    }
    removeNode(node, key) {
        //如果正在检测的节点为null,说明键不存在于树中
        if (node == null) {
            return false
        }

        //如果要找的键比当前节点的值小,就沿树的左边找下一个节点
        if (this.compareFn(key, node.key) === Compare.LESS_THAN) {
            node.left = this.removeNode(node.left, key)
            return node
        }
        // 如果要找的键比当前节点的值大,就沿树的右边找下一个节点
        else if (this.compareFn(key, node.key) === Compare.BIGGER_THAN) {
            node.right = this.removeNode(node.right, key)
            return node
        }
        // 找到的要删除的键
        else{
            // 第一种情况:移除一个叶节点
            if(node.left == null && node.right == null){
                node = null
                return node
            }

            // 第二种情况,只有右侧子节点
            if(node.left == null){
                node = node.right //把对它的引用改为对它右侧子节点的引用
                return node
            }
            // 只有左侧子节点
            else if(node.right == null){
                node = node.left //把对它的引用改为对它左侧子节点的引用
                return node
            }

            //第三种情况:移除有两个字节点的节点
            //1 找到要移除的节点后,需要找到它右边子树中最小的节点
            const aux = this.minNode(node.right)
            //2. 用它右侧子树中最小节点的键去更新这个节点的值
            node.key = aux.key
            //3. 继续把右侧子树中的最小节点移除
            node.right = this.removeNode(node.right,aux.key)
             // 向它的父节点返回更新后节点的引用
            return node
        }
    }

三、二叉树相关算法题

可查看链接: www.yuque.com/u12177228/g…

欢迎点赞收藏