js数据结构-二叉树(二叉搜索树)

前言

可能有一部分人没有读过我上一篇写的二叉堆,因此这里把二叉树的基本概念复制过来了,若是读过的人能够忽略前面针对二叉树基本概念的介绍,另外若是对链表数据结构不清楚的最好先看一下本人以前写的js数据结构-链表node

二叉树

二叉树(Binary Tree)是一种树形结构,它的特色是每一个节点最多只有两个分支节点,一棵二叉树一般由根节点,分支节点,叶子节点组成。而每一个分支节点也经常被称做为一棵子树。react

图片描述

  • 根节点:二叉树最顶层的节点
  • 分支节点:除了根节点之外且拥有叶子节点
  • 叶子节点:除了自身,没有其余子节点

经常使用术语
在二叉树中,咱们经常还会用父节点和子节点来描述,好比图中2为6和3的父节点,反之6和3是2子节点算法

二叉树的三个性质

  1. 在二叉树的第i层上,至多有2^i-1个节点小程序

    • i=1时,只有一个根节点,2^(i-1) = 2^0 = 1
  2. 深度为k的二叉树至多有2^k-1个节点segmentfault

    • i=2时,2^k-1 = 2^2 - 1 = 3个节点
  3. 对任何一棵二叉树T,若是总结点数为n0,度为2(子树数目为2)的节点数为n2,则n0=n2+1

树和二叉树的三个主要差异

  • 树的节点个数至少为1,而二叉树的节点个数能够为0
  • 树中节点的最大度数(节点数量)没有限制,而二叉树的节点的最大度数为2
  • 树的节点没有左右之分,而二叉树的节点有左右之分

二叉树分类

二叉树分为彻底二叉树(complete binary tree)和满二叉树(full binary tree)数组

  • 满二叉树:一棵深度为k且有2^k - 1个节点的二叉树称为满二叉树
  • 彻底二叉树:彻底二叉树是指最后一层左边是满的,右边可能满也可能不满,而后其他层都是满的二叉树称为彻底二叉树(满二叉树也是一种彻底二叉树)

图片描述

二叉搜索树

二叉搜索树知足如下的几个性质:数据结构

  • 若任意节点的左子树不空,则左子树上全部节点的值均小于它的根节点的值;
  • 若任意节点的右子树不空,则右子树上全部节点的值均大于它的根节点的值;
  • 任意节点的左、右子树也须要知足左边小右边大的性质

咱们来举个例子来深刻理解如下post

一组数据:12,4,18,1,8,16,20
由下图能够看出,左边的图知足了二叉树的性质,它的每一个左子节点都小于父节点,右子节点大于其父节点,同时左子树的节点都小于根节点,右子树的节点都大于根节点this

图片描述

二叉搜索树主要的几个操做:spa

  • 查找(search)
  • 插入(insert)
  • 遍历(transverse)

二叉树搜索树的链式存储结构

经过下图,能够知道二叉搜索树的节点一般包含4个域,数据元素,分别指向其左,右节点的指针和一个指向父节点的指针所构成,通常把这种存储结构称为三叉链表。
图片描述

用代码初始化一个二叉搜索树的结点:

  • 一个指向父亲节点的指针 parent
  • 一个指向左节点的指针 left
  • 一个指向右节点的指针 right
  • 一个数据元素,里面能够是一个key和value
class BinaryTreeNode {
        constructor(key, value){
            this.parent = null;
            this.left = null;
            this.right = null;
            this.key = key;
            this.value = value;
        }
    }

接着咱们再用代码去初始化一个二叉搜索树

  • 在二叉搜索树中咱们会维护一个root指针,这个就至关于链表中的head指针,在没有任何节点插入的时候它指向空,在有节点插入之后它指向根节点。
class BinarySearchTree {
        constructor() {
            this.root = null;
        }
    }

建立节点

static createNode(key, value) {
        return new BinarySearchTree(key, value);
    }

插入操做

看下面这张图,13是咱们要插入的节点,它插入的具体步骤:

  1. 跟根节点12作比较,比12大,因此咱们肯定了,这个节点是往右子树插入的
  2. 而根节点的右边已经有节点,那么跟这个节点18作比较,结果小于18因此往18的左节点找位置
  3. 而18的左节点也已经有节点了,因此继续跟这个节点作比较,结果小于16
  4. 恰好16的左节点是空的(left=null),因此13这个节点就插入到了16的左节点

图片描述

经过上面的描述,咱们来看看代码是怎么写的

  • 定义两个指针,分别是p和tail,最初都指向root,p是用来指向要插入的位置的父节点的指针,而tail是用来查找插入位置的,因此最后它会指向null,用上图举个例子,p最后指向了6这个节点,而tail最后指向了null(tail为null则说明已经找到了要插入的位置)
  • 循环,tail根据咱们上面分析的一步一步往下找位置插入,若是比当前节点小就往左找,大则往右找,一直到tail找到一个空位置也就是null
  • 若是当前的root为null,则说明当前结构中并无节点,因此插入的第一个节点直接为跟节点,即this.root = node
  • 将插入后的节点的parent指针指向父节点
insert(node){
        let p = this.root;
        let tail = this.root;
        
        // 循环遍历,去找到对应的位置
        while(tail) {
            p = tail;
            // 要插入的节点key比当前节点小
            if (node.key < tail.key){
                tail = tail.left;
            }
            // 要插入的节点key比当前节点大
            else {
                tail = tail.right;
            }
        }
        
        // 没有根节点,则直接做为根节点插入
        if(!p) {
            this.root = node;
            return;
        }
        
        // p是最后一个节点,也就是咱们要插入的位置的父节点
        // 比父节点大则往右边插入
        if(p.key < node.key){
            p.right = node;
        }
        // 比父节点小则往左边插入
        else {
            p.left = node;
        }
        
        // 指向父节点
        node.parent = p;

    }

查找

查找就很简单了,其实和插入差多,都是去别叫左右节点的大小,而后往下找

  • 若是root = null, 则二叉树中没有任何节点,直接return,或者报个错什么的。
  • 循环查找
search(key) {
        let p = this.root;
        if(!p) {
            return;
        }
        
        while(p && p.key !== key){
            if(p.key<key){
                p = p.right;
            }else{
                p = p.left;
            }
        }
        
        return p;
    }

遍历

  • 中序遍历(inorder):先遍历左节点,再遍历本身,最后遍历右节点,输出的恰好是有序的列表
  • 前序遍历(preorder):先本身,再遍历左节点,最后遍历右节点
  • 后序遍历(postorder):先左节点,再右节点,最后本身

最经常使用的通常是中序遍历,由于中序遍历能够获得一个已经排好序的列表,这也是为何会用二叉搜索树排序的缘由

根据上面对中序遍历的解释,那么代码就变的很简单,就是一个递归的过程,递归中止的条件就是节点为null

  • 先遍历左节点-->yield* this._transverse(node.left)
  • 遍历本身 --> yield* node
  • 遍历左节点 --> yield* this._transverse(node.right)
transverse() {
        return this._transverse(this.root);
    }
    
    *_transverse(node){
        if(!node){
            return;
        }
        yield* this._transverse(node.left);
        yield node;
        yield* this._transverse(node.right)
    }

图片描述
看上面这张图,咱们简化的来看一下,先访问左节点4,再本身12,而后右节点18,这样输出的就恰好是一个4,12,18

补充:这个地方用了generater,因此返回的一个迭代器。能够经过下面这种方式获得一个有序的数组,这里的前提就当是已经有插入的节点了

const tree = new BinaryTree();
   //...中间省略插入过程
    
   // 这样就返回了一个有序的数组 
   var arr = [...tree.transverse()].map(item=>item.key);

完整代码

class BinaryTreeNode {
  constructor(key, value) {
    // 指向父节点
    this.p = null;

    // 左节点
    this.left = null;

    // 右节点
    this.right = null;

    // 键
    this.key = key;

    // 值
    this.value = value;
  }
}

class BinaryTree {
  constructor() {
    this.root = null;
  }

  static createNode(key, value) {
    return new BinaryTreeNode(key, value);
  }

  search(key) {
    let p = this.root;
    if (!p) {
      return;
    }

    while (p && p.key !== key) {
      if (p.key < key) {
        p = p.right;
      } else {
        p = p.left;
      }
    }

    return p;
  }

  insert(node) {
    // 尾指针的父节点指针
    let p = this.root;

    // 尾指针
    let tail = this.root;

    while (tail) {
      p = tail;
      if (node.key < tail.key) {
        tail = tail.left;
      } else {
        tail = tail.right;
      }
    }

    if (!p) {
      this.root = node;
      return;
    }

    // 插入
    if (p.key < node.key) {
      p.right = node;
    } else {
      p.left = node;
    }

    node.p = p;
  }

  transverse() {
    return this.__transverse(this.root);
  }

  *__transverse(node) {
    if (!node) {
      return;
    }
    yield* this.__transverse(node.left);
    yield node;
    yield* this.__transverse(node.right);
  }
}

总结

二叉查找树就讲完了哈,其实这个和链表很像的,仍是操做那么几个指针,既然叫查找树了,它主要仍是用来左一些搜索,还有就是排序了,另外补充一下,二叉查找树里找最大值和最小值也很方即是不是,若是你大体读懂了的话。
这篇文章我写的感受有点乱诶,由于总感受哪里介绍的不到位,让一些基础差的人会看不懂,若是有不懂或者文章哪里写错了,欢迎评论留言哈

后续

后续写什么呢,这个问题我也在想,排序算法,react第三方的一些模拟实现?,作个小程序组件库?仍是别的,容我再想几个小时,由于能够,有建议的朋友们也能够留言说一下哈。最后最后,最重要的请给个赞,请粉一个呢,谢谢啦

相关文章
相关标签/搜索