数据结构-前端-二叉树

143 阅读4分钟

概述

在了解二叉树之前我们先了解一下 在计算机科学中的概念和作用,首先树是一种非线性的数据结构,以分层的方式存储数据,被用来存储具有层级关系的数据,比如文件系统中的文件,有序列表等。

关于树的一些基本概念

以下是一个树的局部图: image.png

  • 根节点:一棵树最上面的节点称为根节点,对应上图中23那个点。
  • 父节点:如果一个节点下面链接多个节点,那么可以称该节点是其下面节点的父节点,其下的节点称之为子节点,如上图中的13是7、9、15的父节点,而7、9、15是13的子节点。
  • 叶子节点:如果一个节点没有任何子节点,那么它就是叶子节点,例如上图中的42、7、9、15。
  • 树的遍历:以某种特定顺序访问树中所有的节点称为树的遍历。
  • 层次:见上图中第几层的标注,我们通常以此来定义树的深度

二叉树

当我们对树的概念有了一些了解以后,可以基于此来认识一下二叉树,二叉树和正常的树一样,只不过我们对它做了一些限制,比如二叉树的每个节点的子节点不允许超过两个,并且通过这个限制,我们可以写出高效的程序在树中插入、查找和删除数据。

二叉树的一些特点

  1. 每个节点最多拥有两个子节点
  2. 二叉树中节点处于左侧位置的子节点叫左节点,右侧的叫右节点
  3. 同一组子节点中,左节点数据一定要小于右节点数据

实现二叉树

基于以上认识,我们尝试通过javascript实现一个二叉树,由于树是由节点组成,所以我们要先定义一个对象TreeNode。 要实现TreeNode,我们先了解一个他的特点,首先节点中会保存一个数据,其次它可能存在两个属性指向它的左右子节点,最后可以有一个方法可以获取到当前节点的值,基于此我们开始实现。

class TreeNode {
  constructor(data, left, right) {
    // 存放节点数据
    this.data = data;
    // 存放左节点
    this.left = left;
    // 存放右节点
    this.right = right;
  }
  // 获取节点数据
  show() {
    return this.data;
  }
}

进入正题,我们基于节点类开始实现一个二叉树

class BST {
  constructor() {
    this.root = null;
  }
  // 插入节点
  insert(data) {
    const n = new Node(data, null, null);
    if(this.root === null) {
      this.root = n;
    } else {
      let current = this.root;
      let parent;
      while(true) {
        parent = current;
        if(data < current.data) {
          current = current.left;
          if(current === null) {
            parent.left = n;
            break;
          }
        } else {
          current = current.right;
          if(current === null) {
            parent.right = n;
            break;
          }
        }
      }
    }
  }
}

BST先要有一个 insert() 方法,用来向树中加入新节点。首先我们要创建一个TreeNode对象,将数据传入该对象保存。

其次检查 BST 是否有根节点,如果没有,那么这是棵新树,该节点就是根节点,这个方法 到此也就完成了,否则进入下一步。

如果待插入节点不是根节点,那么就需要准备遍历 BST,找到插入的适当位置。该过程类似于遍历链表。用一个变量存储当前节点,一层层地遍历 BST

进入 BST 以后,下一步就要决定将节点放在哪个地方。找到正确的插入点时,会跳出循环。查找正确插入点的算法如下。

  1. 设根节点为当前节点。
  2. 如果待插入节点保存的数据小于当前节点,则设新的当前节点为原节点的左节点;反之,执行第 4 步。
  3. 如果当前节点的左节点为 null,就将新的节点插入这个位置,退出循环;反之,继续执行下一次循环。
  4. 设新的当前节点为原节点的右节点。
  5. 如果当前节点的右节点为 null,就将新的节点插入这个位置,退出循环;反之,继续执行下一次循环。

以上一个简单的二叉树就实现了,但是如果我们想要通过遍历去查找或者展示二叉树中的节点数据,我们应该怎么做呢。为此我们要继续给BST添加遍历功能,本文将介绍二叉树常用的三种遍历方式。

先序遍历

先访问根节点,然后以同样方式访问左子树和右子树,下图虚线即为先序访问顺序 image.png

中序遍历

先访问左子树,再访问根节点,最后以同样方式访问右子树,下图虚线即为中序访问顺序 image.png

后序遍历

先访问左子树,然后以同样方式访问右子树,最后访问根节点 image.png 基于此,我们继续完善BST

class BST {
  ...
  // 中序遍历, 按照节点上的键值,以升序访问 BST 上的所有节点
  inOrder(node) {
    if(node) {
      this.inOrder(node.left);
      console.log(node.show() + ' ');
      this.inOrder(node.right)
    }
  }
  // 先序遍历, 先访问根节点,然后以同样方式访问左子树和右子树
  preOrder(node) {
    if(node) {
      console.log(node.show() + ' ');
      this.preOrder(node.left);
      this.preOrder(node.right)
    }
  }
  // 后序遍历, 先访问叶子节点,从左子树到右子树,再到根节点
  lastOrder(node) {
    if(node) {
      this.lastOrder(node.left);
      this.lastOrder(node.right);
      console.log(node.show() + ' ');
    }
  }
}

我们对于二叉树的功能需求可能还远不止于此,所以,我们继续完善BST

class BST {
   ...
  // 获取最大节点
  getMax(node) {
    let current = node || this.root;
    while(current.right) {
      current = current.right;
    }
    return current;
  }
  // 获取最小节点
  getMin(node) {
    let current = node || this.root;
    while(current.left) {
      current = current.left;
    }
    return current;
  }
  // 查找给定值
  find(data) {
    let current = this.root;
    while(current) {
      if(current.data > data) {
        current = current.left;
      } else if(current.data < data) {
        current = current.right;
      } else {
        return current
      }
    }
    return null
  }
  // remove 删除
  remove(data) {
    const root = this.removeNode(this.root, data);
    console.log(root)
  }
  // removeNode 删除节点
  removeNode(node, data) {
    if(node == null) return null;
    if(data == node.data) {
      if(!node.left && !node.right) {
        return null;
      }
      if(!node.left) {
        return node.right;
      }
      if(!node.right) {
        return node.left;
      }
      let tempNode = this.getMin(node.right);
      node.data = tempNode.data;
      node.right = this.removeNode(node.right, tempNode.data);
      return node;
    } else if(data < node.data) {
      node.left = this.removeNode(node.left, data);
      return node;
    } else {
      node.right = this.removeNode(node.right, data);
      return node;
    }
  }
}

ok到此,一个基本的二叉树以及常用方法都提供了,需要注意的是在删除方法中,逻辑会比较复杂,我们在这里特别解释一下。

为了管理删除操作的复杂度,我们使用递归操作,同时定义两个方法:remove() 和 removeNode()。

BST 中删除节点的第一步是判断当前节点是否包含待删除的数据,如果包含,则删除该 节点;如果不包含,则比较当前节点上的数据和待删除的数据。如果待删除数据小于当前 节点上的数据,则移至当前节点的左子节点继续比较;如果删除数据大于当前节点上的数 据,则移至当前节点的右子节点继续比较。

如果待删除节点是叶子节点(没有子节点的节点),那么只需要将从父节点指向它的链接 指向 null。

如果待删除节点只包含一个子节点,那么原本指向它的节点久得做些调整,使其指向它的 子节点。

最后,如果待删除节点包含两个子节点,正确的做法有两种:要么查找待删除节点左子树 上的最大值,要么查找其右子树上的最小值。这里我们选择后一种方式(为了保证左节点一定小于右节点,我们需要寻找一个数作为中间点,而左子树中的最大值和右子树中的最小值恰恰就满足一要求)。

总结

本文是在作者阅读相关书籍后做的一个小总结,目的是为了帮助自己更好的理解二叉树,同时也希望能帮助其他有需要的小伙伴。