一、定义:(递归的方法)
树(Tree)是n(n≥0)个结点的有限集。n=0时称为空树。在任意一棵非空树中:(1)有且仅有一个特定的称为根(Root)的结点;(2)当n>1时,其余结点可分为m(m>0)个互不相交的有限集T、T2、……、Tm,其中每一个集合本身又是一棵树,并且称为根的子树(SubTree)。
两点注意:1.n>0时,根节点是唯一的;
2.m>0时,子树的个数没有限制,但彼此之间互不相交
1、结构特点:一对多
2、结点的度:结点拥有的子树个数称为结点的度。
树的度:树内各结点的度的最大值。
3、结点间的关系:结点的子树的根称为该结点的孩子;该结点称为孩子的双亲。
4、结点的层次:从根开始定义,根为第一层。树中结点的最大层次称为树的深度或者高度。
如果将树中结点的各子树看成从左至右是有次序的,不能互换的,则称该树是有序树。
5、森林:m(m>=0)棵互不相交的树的集合。对树中每个结点而言,其子树的集合即为森林。
二、树的存储结构(待更新。。。)
三、二叉树的定义
引入:1.折半查找算法
2.对于在某个阶段都是两种结果的情形,比如开和关、0和1、真和假、上和下、对和错、正与反等,都适合用二叉树来建模。
1、定义
二叉树(Binary Tree)是n(n=0)个结点的有限集合,该集合或者为空集(称为空二叉树)。或者由一个根结点和两棵互不相交的、分别称为根结点的左子树和右子树的二叉树组成。
2、二叉树特点
- 每个结点最多2棵子树,不存在度大于2的结点;
- 左子树和右子树是有次序的,不能任意颠倒;
- 需要区分左子树还是右子树。
二叉树有5种形态:
- 空二叉树
- 只有一个根节点
- 根节点只有左子树
- 根节点只有右子树
- 根节点既有左子树又有右子树
3、特殊二叉树
3.1、斜树
分为左斜树和右斜树。结点的个数与二叉树的深度相同。
3.2、满二叉树
定义:
1.如果所有的分支结点都存在左子树和右子树;2.所有叶子结点都在同一层上。
特点:
(1)叶子结点只能在最下一层;
(2)非叶子结点的度是2;
(3)在同样深度的二叉树中,满二叉树的结点数最多,叶子结点数最多。
3.3、完全二叉树
定义:
对一棵具有n个结点的二叉树按层序编号,如果编号为i (1<i<n)的结点与同样深度的满二叉树中编号为i的结点在二叉树中位置完全相同,则这棵二叉树称为完全二叉树,如图所示。
满二叉树一定是一棵完全二叉树;反之不一定成立。
特点:
(1)叶子结点只能出现在最下两层。
(2)最下层的叶子一定集中在左部连续位置。
(3)倒数二层,若有叶子结点,一定都在右部连续位置。
(4)如果结点度为1,则该结点只有左孩子,即不存在只有右子树的情况。
(5)同样结点数的二叉树,完全二叉树的深度最小。
4、二叉树的性质
性质1:
在二叉树的第i层上至多有2**(i-1)个结点(i≥1)。
第一层是根结点,只有一个,所以2**(1-1)=2**0=1。
第二层有两个,2**(2-1)=2**1=2。
第三层有四个,2**(3-1)=2**2=4。
。。。
性质2:
深度为k的二叉树至多有2**k-1个结点(k≥1)。
性质3:
对任何一棵二叉树T,如果其终端结点数为n0,度为2的结点数为n2,则n0=n2+1。终端结点数就是叶子结点数。
性质4:
具有n个结点的完全二叉树的深度为[log2(n)]+1([x]表示不大于x的最大整数)。
性质5:
如果对一棵有n个结点的完全二叉树(其深度为[Log2(n)]+1)的结点按层序编号(从第1层到第[Log2(n)]+1层,每层从左到右),对任一结点i (1≤i≤n)有:
1.如果i=1,则结点i是二叉树的根,无双亲;如果i>1,则其双亲是结点[i/2]。
2.如果2i>n,则结点i无左孩子(结点i为叶子结点);否则其左孩子是结点2i。
3.如果2i+1>n,则结点i无右孩子;否则其右孩子是结点2i+1。
5、二叉树的存储结构
5.1、二叉树的顺序存储结构
二叉树的顺序存储结构就是用一维数组存储二叉树中的结点,并且结点的存储位置,也就是数组的下标要能体现结点之间的逻辑关系,比如双亲与孩子的关系,左右兄弟的关系等。
当然对于一般的二叉树,尽管层序编号不能反映逻辑关系,但是可以将其按完全二叉树编号,只不过,把不存在的结点设置为“^”而已。 ^表示空档不存在。
顺序存储结构一般只用于完全二叉树。
5.2、二叉链表
既然顺序存储适用性不强,我们就要考虑链式存储结构。
二叉树每个结点最多有两个孩子,所以为它设计一个数据域和两个指针域是比较自然的想法,我们称这样的链表叫做二叉链表。
6、二叉树的遍历(定义是递归的方式,因此遍历也可以采用递归)
遍历是二叉树最重要的一门学问。
二叉树的遍历(traversing binary tree)是指从根结点出发,按照某种次序依次访问二叉树中所有结点,使得每个结点被访问一次且仅被访问一次。
树的结点之间不存在唯一的前驱和后继关系。
对于计算机来说,它只有循环、判断等方式来处理;也就是说,它只会处理线性序列。遍历方法都是在把树中的结点变成某种意义的线性序列。
6.1、前序遍历
次序:根节点->左子树->右子树
function preOrder(root){
let result = [];
if(root==null){
return
}
// 前序遍历位置
result.push(root.val)
preOrder(root.left)
preOrder(root.right)
}
序列化(打平到一个字符串):
let sep=","
let NULL="#"
// 主函数,序列化为字符串
function serialize(root){
let sb = new StringBuilder()//StringBuilder可以用于高效拼接字符串
help(root,sb)
return sb.toString();
}
//辅助函数,将二叉树存入StringBuilder
function help(root,sb){
if(root==null){
sb.append(NULL).append(sep)
return
}
// 前序遍历的位置
sb.append(root.val).append(sep)
help(root.left,sb)
help(root.right,sb)
}
6.2、中序遍历
次序:左子树->根节点->右子树
function preOrder(root){
let result = [];
if(root==null){
return
}
preOrder(root.left)
// 中序遍历位置
result.push(root.val)
preOrder(root.right)
}
6.3、后序遍历
次序:左子树->右子树->根节点
function preOrder(root){
let result = [];
if(root==null){
return
}
preOrder(root.left)
preOrder(root.right)
// 后序遍历位置
result.push(root.val)
}
6.4、层序遍历
次序:从上至下,从左到右
第一种:一层一层输出
function traverse(root){
if(root==null){
return []
}
let result=[]
//初始化队列,将root入队
let list = new Array();
list.push(root)
while(list.length!==0){
let len=list.length
// 层序遍历的位置
let temp=[]
for(let i=0;i<len;i++){
let node=list.shift()
temp.push(node.val)
if(node.left!=null){
list.push(node.left)
}
if(node.right!=null){
list.push(node.right)
}
}
result.push(temp)
}
return result
}
第二种:标准的二叉树层级遍历框架
function traverse(root){
if(root==null){
return
}
let result=[]
//初始化队列,将root入队
let list = new Array();
list.push(root)
while(list.length!==0){
// 层序遍历的位置
let node=list.shift()
result.push(node.val)
if(node.left!=null){
list.push(node.left)
}
if(node.right!=null){
list.push(node.right)
}
}
return result
}
第三种:带空指针的层序遍历(以便于后期反序列化)
function serialize(root){
if(root==null){
return [null]
}
let result=[]
//初始化队列,将root入队
let list = new Array();
list.push(root)
while(list.length!==0){
// 层序遍历的位置
let node=list.shift()
if(node==null){
result.push(null)
continue
}
result.push(node.val)
list.push(node.left)
list.push(node.right)
}
return result
}
6.5、反序列化(由序列推出二叉树结构)
原理: 先确定根节点root,然后遵循遍历规则,递归生成左右子树。
三种遍历都是从根结点开始,前序遍历是先打印再递归左和右。
已知前序和中序,可以唯一确定一棵二叉树;
已知后序和中序,可以唯一确定一棵二叉树。
注意:已知前序和后序,是不能确定一棵二叉树的。
情况一:如何通过二叉树的前序遍历结果还原一棵二叉树?
注意:一般语境下,单单前序遍历结果是不能还原二叉树结构的,因为缺少空指针的信息。但是,如果前序遍历列表包含空指针的信息,就可以还原。
let sep=","
let NULL="#"
// 主函数,将字符串反序列化为二叉树结构
function deserialize(data){
// 将字符串转化为列表
let nodes=new Array()
for(let s of data.split(sep)){ //以逗号分隔转化成列表
nodes.push(s)
}
return help(nodes)
}
// 辅助函数,通过nodes列表构造二叉树
function help(nodes){
if(nodes.length==0){
return null
}
// 前序遍历位置
// 列表最左侧就是根节点
let first=nodes.shift()
if(first==NULL){
return null
}
let root=new TreeNode(parseInt(first))
root.left=help(nodes)
root.right=help(nodes)
return root
}
情况二:如何通过二叉树的后序遍历结果还原一棵二叉树? 难点
**deserialize方法原理:**先确定根节点root,然后遵循遍历规则,递归计算生成左右子树。
核心思想:后序遍历结果中,root的值是列表中最后一个元素。我们应该从后往前取出列表元素,先用最后一个元素构造出root,再递归调用生成root的左右子树。从后往前的意思是:先用最后一个元素构造出root,然后先构造出 root.right子树,最后构造出 root.left子树。
let sep=","
let NULL="#"
// 主函数,将字符串反序列化为二叉树结构
function deserialize(data){
// 将字符串转化为列表
let nodes=new Array()
for(let s of data.split(sep)){
nodes.push(s)
}
return help(nodes)
}
// 辅助函数,通过nodes列表构造二叉树
function help(nodes){
if(nodes.length==0){
return null
}
// 后序遍历位置
// 列表最右侧就是根节点
let last=nodes.pop()
if(last==NULL){
return null
}
let root=new TreeNode(parseInt(last))
// 先构造出右子树,后构造出左子树
root.right=help(nodes)
root.left=help(nodes)
return root
}
情况三:如何通过二叉树的层序遍历结果还原一棵二叉树?
用队列进行层级遍历,同时用索引 i 记录对应子节点的位置:
function deserialize(result){
if(result==[null]){
return null
}
// 第一个元素就是root的值
let first=result.shift()
let root=new TreeNode(first)
//初始化队列,记录父节点
let list = new Array();
list.push(root)
for(let i=0;i<result.length;){
//队列中存的都是“父”节点
let parent=list.shift()
//左侧结点的值
let left=result[i++]
if(left==null){
parent.left=null
}else{
parent.left=new TreeNode(left)
list.push(parent.left)
}
//右侧结点的值
let right=result[i++]
if(right!=null){
parent.right=new TreeNode(right)
list.push(parent.right)
}else{
parent.right=null
}
}
return root
}
二叉树的相关知识暂时更新到这。
点击这里🔍几种特殊的二叉树查看一些特殊二叉树的相关知识。