一、二叉树的基本概念

从逻辑结构角度来看,前面说的链表、栈、队列都是线性结构;而今天要了解的“二叉树”属于树形结构。

1.1 多叉树的基本概念,以上图中“多叉树”为例说明

  节点:多叉树中的每一个点都叫节点;其中最上面的那个节点叫“根节点”;

  父节点:节点1是节点2/3/4/5/6的父节点,然后节点2/3/4/5/6是节点1的子节点;节点2/3/4/5/6又是互为兄弟节点,因为它们有父节点为同一个节点;

  空树:一个没有任何节点的树叫空树;一棵树可以只有一个节点,也就是只有根节点;

  子树:子节点及子节点的后台节点形成的一个节点集合叫子树;对于只有两个子节点的节点,其左边的子节点叫左子树,右边的叫右子树;

  叶子节点(leaf):子树为0的节点;其他子树不为0的节点叫“非叶子节点”;

  层数(level):根节点在第1层,根节点的子节点在第2层,以此类推(有些说法也从第0层开始结算);

  节点的深度(depth):从根节点到当前节点的唯一路径上的节点总数;

  节点的高度(height):从当前节点到最远叶子节点的路径上的节点总数;

  树的深度:所有节点深度中的最大值;

  树的高度:所以节点高度中的最大值;数的深度等于数的高度

  有序树:树中任意节点的子节点之间有顺序关系;

  无序树:树中任意节点的子节点之间没有顺序关系,也叫“自由树”;

  森林:由n(n >= 0) 颗不相交的树组成的集合;

1.2 二叉树的特点

  • 每个节点的度最大为2(最多拥有2颗子树);
  • 左子树和右子树是有顺序的。即使某节点只有一颗子树,也要区分左右子树;
  • 非空二叉树的第i层,最多有2^(i-1)个节点(i >= 1)
  • 在高度为h的二叉树上最多有2^h - 1个节点(h >= 1);
  • 对于任何一颗非空二叉树,如果叶子节点个数为n0, 度为2的节点个数为n2, 则有n0 = n2 + 1。(这个好理解,假如这个二叉树除了第一级节点有2个子节点,后面的节点都是只有一个子节点,则不管这颗二叉树如何往下延伸,永远度为2的节点个数是1个,叶子节点为2个;然后如果在这个二叉树的中间节点,每加一个节点,相当于度为2的节点加一个,叶子节点也加一个,则度为2的节点和叶子节点的增加是同步同数量的,所以对于二叉树,叶子节点个数 = 度为2的节点个数 + 1 公式是永远成立的)
  • 假设度为1的节点个数为n1, 那么二叉树的节点总数 n = n0 + n1 + n2。则二叉树的边数T = n1 + 2 * n2 = n -1 = n0 + n1 + n2 -1  --> n2 + 1 = n0

1.3 真二叉树/满二叉树/完全二叉树

  • 真二叉树:所有节点的度都要么为2,要么为0;
  • 满二叉树:所有节点的度都要么为2,要么为0,且所有的叶子节点都在最后一层;
    • 假设满二叉树的高度为h (h>=1),那么第i层的节点数量:2^(i-1), 叶子节点数量:2(^h-1), 总节点数量n = 2^h -1 = 2^0 + 2^1 + 2^2 + ... + 2^(h-1)
    • 等比数列公式:a1=1, 公比q=2, 则an = a1*q^(n-1)=2^(n-1), 前n项之和Sn = a1+a2+...+an = 2^0 + 2^1 + 2^2 + ... + 2^(n-1) = a1(1-q^n)/(1-q)=2^n-1
    • 在同样高度的二叉树中,满二叉树的叶子节点数量最多、总节点数量最多;
    • 满二叉树一定是真二叉树,真二叉树不一定是满二叉树;
  • 完全二叉树:叶子节点只会出现在最后两层,且最后一层的叶子节点都靠左对齐;
    • 完全二叉树从根节点到倒数第二层是一颗满二叉树;满二叉一定是完全二叉树,完全二叉树不一定是满二叉树;
    • 度为1的节点只有左子树;度为1的节点要么是1个,要么是0个;
    • 假设完全二叉树的高度为h(h>=1), 那么至少有2^(h-1) 个节点(2^0 + 2^1 + 2^2 + ... + 2^(h-2) + 1), 最多有2^h - 1个节点(2^0 + 2^1 + 2^2 + ...+ 2^(h-1), 满二叉树);总节点数量为n, 则有2^(h-1) <= n < 2^h  -->  h-1 <= log(2)(n) < h --> h = floor(log(2)(n)) + 1  (floor是向下取整,ceiling是向上取整)
    • 一颗有n个节点的完全二叉树(n > 0),从上到下,从左到右对节点从1开始进行编号,对任意第i个节点
      • 如果i = 1, 它是根节点
      • 如果i > 1, 它的父节点编号为floor(i/2)
      • 如果2i <= n, 它的左子节点编号为2i
      • 如果2i > n, 它无左子节点
      • 如果2i + 1 <= n, 它的右子节点编号为2i + 1
      • 如果2i + 1 > n , 它无右子节点  

   

面试题:如果一颗完全二叉树有768个节点,求叶子节点的个数?

分析:假设叶子节点个数为n0,度为1的节点个数为n1,度为2的节点个数为n2。

  则总节点个数n = n0  +  n1 +  n2,而且n0 = n2 + 1 ;

  则n = 2n0 + n1 -1

  根据完全二叉树的定义我们知道,n1要么为0,要么为1:

  当n1为1时, n = 2n0,  n必然为偶数。叶子节点个数n0 = n / 2,非叶子节点个数 n1 + n2 = n / 2 ;

  当n1为0,n = 2n0 - 1,n必然为奇数。叶子节点个数n0 = (n + 1) / 2, 非叶子节点个数 n1 + n2 = (n - 1) / 2

  因此可以判断出来当这个完全二叉树有768个节点时,它的叶子节点个数为:384

二、二叉查找树

二叉查找树是一种特殊的二叉树,较小的值保存在左节点中,较大的值保存在右节点中。这一特性使得查找的效率很高,对于数值型和非数值型的数据,如单词和字符串,都是如此。

2.1 二叉查找树的插入逻辑

  2.1.1 设根节点为当前节点

  2.1.2 如果待插入节点保存的数据小于当前节点,则设新的当前节点为原节点的左节点;反之,执行第2.1.4步

  2.1.3 如果当前节点的左节点为null, 就将新的节点插入这个位置,退出循环;反之,继续执行下一次循环

  2.1.4 设新的当前节点为原节点的右节点

  2.1.5 如果当前节点的右节点为null, 就将新的节点插入这个位置,退出循环;反之,继续执行下一次循环

//插入元素
function insertBST(element){
var node = new Node(element, null, null); //根节点判断
if (root == null){
root = node;
}
else{ //非根节点
var current = root;
while(true){
if (element < current.element){ //往左节点方向放
if (current.left == null){
current.left = node;
break;
}
current = current.left;
}
else if (element > current.element){ //往右节点方向放
if (current.right == null){
current.right = node;
break;
}
current = current.right;
}
else { //相等,替换
current.element = element;
return;
}
}
}
size++;
}

2.2 二叉查找树的遍历,遍历有三种方式:中序、前序、后序

  中序指以升序的方式遍历所有节点;前序是指先访问根节点,再以同样的方式访问左子树和右子树;后序指的是先访问叶子节点,再从左子树到右子树,最后到根节点。

先看个效果图

遍历走势分析图:

遍历代码:

//二叉树中序遍历:以升序方式访问二叉树中所有节点
function inOrder(){
return inOrderByNode(root);
}
function inOrderByNode(node){
if (node){
var str = "";
str += inOrderByNode(node.left);
str += node.element + ", ";
str += inOrderByNode(node.right);
return str;
}
return "";
} //前序遍历:先访问根节点,再访问左子树和右子树
function preOrder(){
return preOrderByNode(root);
}
function preOrderByNode(node){
if (node){
var str = '';
str += node.element + ", "; //先访问根节点
str += preOrderByNode(node.left); //再访问左子树
str += preOrderByNode(node.right); //再访问右子树
return str;
}
return "";
} //后序遍历:先访问叶子节点,再左子树,再右子树,再到根节点
function postOrder(){
return postOrderByNode(root);
}
function postOrderByNode(node){
if (node){
var str = "";
str += postOrderByNode(node.left);
str += postOrderByNode(node.right);
str += node.element + ", ";
return str;
}
return "";
}

四则运算的表达式可以分为三种:

  • 前缀表达式(prefix  expression),又称波兰表达式
  • 中缀表达式(infix  expression)
  • 后缀表达式(postfix  expression),又称为逆波兰式表达式

如果将表达式的操作作为叶子节点,运算符作为父节点(假设只有四则运算),这些节点刚好可以组成一颗二叉树。

比如表达式:A / B + C * D - E  ,如果对这颗二叉树进行遍历

  • 前序遍历:- + / A B * C D E ,刚好就是前缀表达式(波兰表达式)
  • 中序遍历:A / B + C * D - E,刚好就是中缀表达式
  • 后序遍历:A B / C D * + E -,刚好就是后缀表达式(逆波兰表达式)

2.3 查找二叉查找树的最大值、最小值、是否存在某个值

最大值:因为较大的值都是在右子树上,则最大值一定是在右子树的最后一个节点上;

最小值:较小的值都是在左子树上,则最小值一定在左子树的最后一个节点上;

是否存在某个值,则是遍历查找

//查找最小值:因为较小的值都在左边,所以最小值一定是左子树的最后一个节点
function getMin(){
var minNode = getMinNode(root);
if (minNode) {
return minNode.element;
}
return null;
}
//查找最小节点
function getMinNode(node){
var current = node;
while(current){
if (current.left == null){
return current;
}
current = current.left;
}
return null;
} //查找最大值:因为较大的值都在右边,所以最大值一定是在右子树的最后一个节点
function getMax(){
var maxNode = getMaxNode(root);
if (maxNode){
return maxNode.element;
}
return null;
}
//查找最大节点
function getMaxNode(node){
var current = node;
while(current){
if (current.right == null){
return current;
}
current = current.right;
}
return null;
} //查找指定值,是否存在这个元素
function isExist(element){
var current = root;
while(current){
if (element < current.element){ //左子树寻找
current = current.left;
}
else if (element > current.element){ //右子树寻找
current = current.right;
}
else{ //存在
return true;
}
}
return false;
}

2.4 删除二叉查找树中的指定元素

从二叉查找树上删除节点的操作最复杂,其复杂程度取决于删除哪个节点。如果删除没有子节点 的节点,那么非常简单。如果节点只有一个子节点,不管是左子节点还是右子节点,就变 得稍微有点复杂了。删除包含两个子节点的节点最复杂。

//删除元素
function remove(element){
root = removeNode(root, element);
}
function removeNode(node, element){
if (node == null) {
return null;
} if (node.element == element){
size--;
//node没有左子树
if (node.left == null){
return node.right;
}
else if (node.right == null){ //node没有右子树
return node.left;
}
/**
* node有左子树和右子树,这个时候要找出最接近node节点值的节点
* 1、如果找出比node节点的element稍大的节点,则从node右节点的最小节点
* 2、如果找出比node节点的element稍小的节点,则从node左节点的最大节点
*/
//第一种方式,找出比node的element稍微大点的节点
var minNode = getMinNode(node.right);
node.element = minNode.element;
node.right = removeNode(node.right, minNode.element); // //第二种方式, 找出比node的element稍微小点的节点
// var maxNode = getMaxNode(node.left);
// node.element = maxNode.element;
// node.left = removeNode(node.left, maxNode.element); return node;
}
else if(element < node.element){ //往左子树方向继续找
node.left = removeNode(node.left, element);
return node;
}
else{
//往右子树方向继续找
node.right = removeNode(node.right, element);
return node;
}
}

完整demo见:https://github.com/xiaotanit/Tan_DataStruct

JS数据结构第五篇 --- 二叉树和二叉查找树的更多相关文章

  1. JS数据结构第六篇 --- 二叉树力扣练习题

    1.第226题:翻转二叉树 递归+迭代两种实现方式: /** 反转二叉树 * Definition for a binary tree node. * function TreeNode(val) { ...

  2. JS数据结构第三篇---双向链表和循环链表之约瑟夫问题

    一.双向链表 在上文<JS数据结构第二篇---链表>中描述的是单向链表.单向链表是指每个节点都存有指向下一个节点的地址,双向链表则是在单向链表的基础上,给每个节点增加一个指向上一个节点的地 ...

  3. JS数据结构第四篇 --- 栈

    一.什么是数据结构栈 在数据结构中有一个栈结构,在内存空间中也有一个栈空间,这两个”栈“是两个不同的概念.这篇我们说的是数据结构中的栈.栈是一种特殊的线性表,特殊性在哪?就是只能在栈顶进行操作,往栈顶 ...

  4. 数据结构和算法 – 9.二叉树和二叉查找树

      9.1.树的定义   9.2.二叉树 人们把每个节点最多拥有不超过两个子节点的树定义为二叉树.由于限制子节点的数量为 2,人们可以为插入数据.删除数据.以及在二叉树中查找数据编写有效的程序了. 在 ...

  5. JS原生第五篇 (帅哥)

    1.1 节点 1. 节点        网页是有很多的节点组成的  . 元素节点   指的是 :  标签     li  span 文本节点      属性节点 父子兄弟    父    parent ...

  6. 【Python五篇慢慢弹】数据结构看python

    数据结构看python 作者:白宁超 2016年10月9日14:04:47 摘要:继<快速上手学python>一文之后,笔者又将python官方文档认真学习下.官方给出的pythondoc ...

  7. 二叉树和二叉查找树--数据结构与算法JavaScript描述(10)

    二叉树和二叉查找树 概念 树是一种非线性的数据结构,以分层的方式存储数据. 树被用来存储具有层级关系的数据,比如文件系统的文件: 树还被用来存储有序列表. 一棵树最上面的节点称为根节点. 如果一个节点 ...

  8. 常见基本数据结构——树,二叉树,二叉查找树,AVL树

    常见数据结构——树 处理大量的数据时,链表的线性时间太慢了,不宜使用.在树的数据结构中,其大部分的运行时间平均为O(logN).并且通过对树结构的修改,我们能够保证它的最坏情形下上述的时间界. 树的定 ...

  9. 找出 int 数组的平衡点 & 二叉树 / 平衡二叉树 / 满二叉树 / 完全二叉树 / 二叉查找树

    找出 int 数组的平衡点 左右两边和相等, 若存在返回平衡点的值(可能由多个); 若不存在返回 -1; ``java int [] arr = {2,3,4,2,4}; ```js const ar ...

随机推荐

  1. 基于C#的机器学习--微基准测试和激活功能

    本章我们将学习以下内容: l  什么是微基准测试 l  如何将它应用到代码中 l  什么是激活函数 l  如何绘制和基准测试激活函数 每个开发人员都需要有一个好的基准测试工具.质量基准无处不在;你们每 ...

  2. STM32F0_HAL库驱动描述——HAL驱动程序概述

    HAL库文件结构: HAL驱动文件: 外设驱动API文件和头文件:包含了常见主要的通用API,其中ppp表示外设名称,如adc.usart.gpio.irda等: stm32f0xx_hal_ppp. ...

  3. 数据库系统概念:SQL的数据类型与模式、授权

    public class DataBase { public static void main() { } } /* 4.5 SQL的数据类型与模式 4.5.1 SQL的日期与时间类型 SQL标准支持 ...

  4. 洛谷 P1101-题解

    这道题可以用深搜(回溯)来写,相信大部分人都是这么想的,但是有些人可能在一些地方饶了半天,所以这里就贴一下我的思路,个人觉得自己的很好懂,除了tx和ty那里,但是tx和ty的那种用法对于输出路径的题目 ...

  5. Android 异常 UncaughtException detected: java.lang.RuntimeException: Parcelable encountered IOExcepti

    异常信息: UncaughtException detected: java.lang.RuntimeException: Parcelable encountered IOException wri ...

  6. C#编程之JSON序列化与反序列化

    1.在C#管理NuGet程序包中添加Json.NET 2.C#将对象序列化成JSON字符串 模型类1 /// <summary> /// JSON字符串模型.是否出错 /// </s ...

  7. python初步编写用户登录

     python初步编写用户登录 python编写用户登录 用python写一个脚本,使得这个脚本在执行后,可以产生如下的效果: 1.用户的账号为:root 密码为:westos 2.用户账号和密码均输 ...

  8. [算法]LeetCode 1.两数之和

    LeetCode 1.两数之和(python) 1.朴素解法 最朴素的两个for循环大法: class Solution: def twoSum(self, nums: List[int], targ ...

  9. Java初中级程序员面试题宝典

    Java基础部分 &与&&区别? &和&&都是逻辑运算符,都是判断两边同时真则为真,否则为假:但是&&当第一个条件不成之后,后面的条件都 ...

  10. thinkphp 插件

    1.切换到项目根目录,使用composer require 5ini99/think-addons:dev-master命令安装thinkphp插件 如果是root用户或是管理员执行的话会有提示 等一 ...