11.树结构
1.树的基本概念及实现
定义和基本术语
-
树是由一个集合以及在该集合上定义的一种关系构成的。
-
集合中的元素称为树的结点,所定义的关系称为父子关系。
-
父子关系在树的结点之间建立了一个层次结构。
-
在这种层次结构中有一个结点具有特殊的地位,这个结点称为该树的根结点,或简称为树根。
结点的层次和树的深度
-
结点的层次(level)从根开始定义,层次数为0的结点是根结点,其子树的根的层次数为1…
-
树中结点的最大层次数称为树的深度(Depth)或高度。树中结点也有高度,其高度是以该结点为根的树的高度
结点的度与树的度
-
结点拥有的子树的数目称为结点的度( Degree )
-
度为0的结点称为叶子(leaf)结点。度不为0的结点称为非终端结点或分支结点。除根之外的分支结点也称为内部结点
-
性质11.1 树中的结点数等于树的边数加1 ,也等于所有结点的度数之和加1。
-
在树中结点总数与边的总数是相当的,基于这一事实,在对涉及树结构的算法复杂性进行分析时,可以用结点的数目作为规模的度量
路径
-
在树中k+1个结点通过k条边连接构成的序列{ ( v0,v1 ) ,( v1,v2) …( vk-1,vk) |k≥0} ,称为长度为k的路径(path)
-
树中任意两个结点之间都存在唯一的路径。这意味着树既是连通的,同时又不会出现环路。从根结点开始,存在到其他任意结点的一条唯一路径,根到某个结点路径的长度,恰好是该结点的层次数
有序树、m叉树、森林
-
如果将树中结点的各子树看成是从左至右是有次序的,则称该树为有序树;若不考虑子树的顺序则称为无序树。对于有序树,我们可以明确的定义每个结点的第一个孩子、第二个孩子等,直到最后一个孩子。若不特别指明,一般讨论的树都是有序树。
-
树中所有结点最大度数为m的有序树称为m叉树。
-
森林(forest) 是m(m≥0)棵互不相交的树的集合。对树中每个结点而言,其子树的集合即为森林。树和森林的概念相近。删去一棵树的根,就得到一个森林;反之,加上一个结点作树根,森林就变为一棵树。
import java.util.List;
public class TreeNode<E> {
public E key;
public TreeNode<E> parent;
public List<TreeNode<E>> children;
public TreeNode(E key, TreeNode<E> parent) {
this.key = key;
this.parent = parent;
}
public TreeNode(E key) {
this.key = key;
}
@Override
public String toString() {
return key+"" ;
}
}
import java.util.List;
public interface ITree<E> {
int getSize();//节点数
TreeNode<E> getRoot();//获取根节点
TreeNode<E> getParent(TreeNode<E> x);//获取x的父节点
TreeNode<E> getFirstChild(TreeNode<E> x);//获取第一个儿子
TreeNode<E> getNextSibling(TreeNode<E> x);//获取x的下一个兄弟
int getHeight(TreeNode<E> x);//子树高度
void insertChild(TreeNode<E> x, TreeNode<E> child);//插入子节点
void deleteChild(TreeNode<E> x, int i);//删除第i个子节点
List<List<TreeNode<E>>> levelOrder(TreeNode<E> x);//层次遍历
}
import java.util.*;
public class MyTree<E> implements ITree<E> {
public int size=0;
public TreeNode root;
public MyTree(TreeNode root) {
this.root = root;
size++;
}
public int getSize() {
return size;
}
public TreeNode<E> getRoot() {
return root;
}
public TreeNode<E> getParent(TreeNode<E> x) {
return x.parent;
}
public TreeNode<E> getFirstChild(TreeNode<E> x) {
List<TreeNode<E>> children = x.children;
if(children!=null){
return children.get(0);
}
return null;
}
public TreeNode<E> getNextSibling(TreeNode<E> x) {
List<TreeNode<E>> children = x.parent.children;
int i=children.indexOf(x);
try{
return children.get(i+1);
}catch(Exception e){
return null;
}
}
public int getHeight(TreeNode<E> x) {//用DFS深度搜索
if(x.children==null){
return 0;
}
int height = 0;
List<TreeNode<E>> children = x.children;
for (TreeNode<E> t : children) {
height=Math.max(height, getHeight(t));//不要忘记复制
}
return height + 1;
}
public int getHeight(){
return getHeight(root);
}
public void insertChild(TreeNode<E> x, TreeNode<E> child) {//x为父节点
if(x.children==null){
x.children=new ArrayList<TreeNode<E>>();
}
x.children.add(child);
child.parent=x;//不要忘记为新节点增加父亲链接
size++;
}
public void deleteChild(TreeNode<E> x, int i) {
if(x.children!=null){
List<TreeNode<E>> children=x.children;
children.remove(i);
size--;
}
}
public List<List<TreeNode<E>>> levelOrder(TreeNode<E> x) {
//使用队列,加入头元素,弹出队列的一个元素,就加入弹出元素的所有孩子
//而且要每行一个list
List<List<TreeNode<E>>> res=new ArrayList<List<TreeNode<E>>>();
Queue<TreeNode<E>> q=new LinkedList<TreeNode<E>>();
TreeNode<E> last=x;//上一行的最后一个
TreeNode<E> nlast=null;//当前要弹出的元素
q.add(x);
List<TreeNode<E>> tt=new ArrayList<TreeNode<E>>();
res.add(tt);
while(!q.isEmpty()){//注意这里是不为空,不能写null
TreeNode<E> peek=q.peek();//将要弹出的元素
if(peek.children!=null) {//不为空时就加入弹出元素的所有孩子
List<TreeNode<E>> children = peek.children;
for (int i = 0; i < children.size(); i++) {
q.add(children.get(i));
nlast = children.get(i);//更新当前要弹出的元素
}
}
tt.add(q.poll());//弹出队列的一个元素
if(peek==last&&!q.isEmpty()){//当前元素已经到最后一个元素
tt=new ArrayList<TreeNode<E>>();
res.add(tt);
last=nlast;
}
}
return res;
}
public List<List<TreeNode<E>>> levelOrder() {
return levelOrder(root);
}
}
2.二叉树的基本概念及实现
定义
-
每个结点的度均不超过2的有序树,称为叉树(binarytree)。
-
与树的递归定义类似,二叉树的递归定义如下:二叉树或者是一棵空树,或者是一棵由一个根结点和两棵互不相交的分别称为根的左子树和右子树的子树所组成的非空树
二叉树的性质
性质11.2 在二叉树的第i层上最多有2i个结点 根节点为0层
性质11.3 高度为h的二叉树至多有 2(h+1)-1个结点 根节点为h为0
性质11.4 对任何一棵二叉树T ,如果其终端结点数为n0,度为2的结点数为n2,则n0=n2+1
总节点数n=度数+1=1n1+2n2 +1 (性质11.1)
总节点数n=n1+n0+n2->n0=n2+1
性质11.5 有n个结点的完全二叉树的高度为⌊log2n⌋
性质11.3 含有n≥1个结点的二叉树的高度至多为n-1;高度至少为⌊log2n⌋
性质11.4 如果对一棵有n个结点的完全=叉树的结点进行编号,则对任一结点i(1≤i ≤n) ,有
(1) 如果i=1,则结点i是二义树的根,无双亲;如果i>1,则其双亲结点PARENT(i) 是结点⌊i/2⌋。
(2) 如果2i>n,则结点i无左孩子;否则其左孩子是结点2i
(3)如果2i+1>n,则结点i无右孩子;否则其右孩子是结点2i+1。
特殊二义树
定义满二叉树高度为k并且有2^(k+1)-1 个结点的=叉树。在满二叉树中,每层结点都达到最大数,即每层结点都是满的,因此称为满二叉树。
定义完全二叉树 若在一棵满二叉树中,在最下层从最右侧起去掉相邻的若干叶子结点,得到的二叉树即为完全二叉树
3.二叉查找树BST
定义
- 所谓二叉查找树( binary searchtree , BST)或者是一棵空树;或者是
具有以下性质的二叉树:
(1) 若它的左子树不空,则其左子树中所
有结点的值不大于根结点的值;
(2) 若它的右子树不空,则其右子树中所有结点的值不小于根结点的值;
(3) 它的左、右子树都是二叉查找树
- 结论:中序遍历一棵二叉查找树可以得到一个按关键字递增的有序序列
#include<iostream>
using namespace std;
typedef struct BiTNode /* 结点结构 */{
int data; /* 结点数据 */
struct BiTNode *lchild, *rchild; /* 左右孩子指针 */
}BiTNode,*BiTree;
BiTree SearchBST(BiTree T,int key){
/* 在根节点T二叉排序树中递归地查找某关键字等于key的数据元素,
若查找成功,则返回指向该数据元素结点的引用,否则返回null*/
if(!T||T->data==key){//查找结束
return T;
}else if(key<T->data){//在左子树中继续查找
return SearchBST(T->lchild,key);
}else{//在右子树中继续查找
return SearchBST(T->rchild,key);
}
}
/*递归查找二叉排序树T中是否存在key, */
bool SearchBST(BiTree T,int key,BiTree f,BiTree& p){
//T为当前处理的节点,key为要查找的值
//指针f指向T的双亲,其初始调用值为NULL
//p为最后找到的结果
if(!T){//查找不成功
p=f;//结果指针p指向父亲 即查找路径上访问的最后一个结点
return false;
}else if(T->data==key){//查找成功
p=T;//指针p指向该数据元素结点
return true;
}else if(key<T->data){//在左子树中继续查找
return SearchBST(T->lchild,key,T,p);
}else{//在右子树中继续查找
return SearchBST(T->rchild,key,T,p);
}
}
/*当二叉排序树T中不存在关键字等于key的数据元素时,*/
/*插入key并返回TRUE, 否则返回FALSE */
bool InsertBST(BiTree& T,int key){//注意T为引用
BiTree p;
if(!SearchBST(T,key,NULL,p)){//查找不成功
BiTree s=(BiTree)malloc(sizeof(BiTNode));
s->data=key;
s->lchild=s->rchild=NULL;//此句不是废话,默认不是NULL,是随机值,如果不赋值为NULL,无法判断是否为NULL
if(p==NULL){//被插结点s为新的根结点
T=s;
}else if(key<p->data){//被插结点s为左孩子
p->lchild=s;
}else{
p->rchild=s;//被插结点s为右孩子
}
return true;
}else{
return false;//树中已有关键字相同的结点,不再插入
}
}
bool Delete(BiTree& p) {
//从二叉排序树中删除结点p,并重接它的左或右子树
BiTree q, s;
if (!p->rchild) {//右子树空则只需重接它的左子树,次情况包括左右都为空
q = p;
p = p->lchild;
free(q);
} else if (!p->lchild) {//只需重接它的右子树
q = p;
p = p->rchild;
free(q);
} else {//左右子树均不空
//方案:右孩子不动,左孩子最大的覆盖p
q = p;//将q初始化为p,用于判断q是否移动
//q最后指向s的父亲
s = p->lchild;
//s初始化为p的左子树根 最后指向p左孩子最大的
while (s->rchild) {//转左,然后向右到尽头,使s指向被删结点的“前驱"
q = s;
s = s->rchild;
}
p->data = s->data;//s->data为p左孩子最大的覆盖p
//由于s可能会有左孩子,挂在父亲上
if (q != p) {//重接* q 的右子树
q->rchild = s->lchild;
} else {//重接* q 的右子树
q->lchild = s->lchild;
free(s);
}
return true;
}
}
bool DeleteBST(BiTree& T,int key){
//若二叉排序树T中存在关键字等千key的数据元素时,则删除该数据元素结点,并返回TRUE,否则返回FALSE
if(!T){
return false;
}else{
if(key==T->data){
return Delete(T);
}else if(key<T->data){
return DeleteBST(T->lchild,key);
} else{
return DeleteBST(T->rchild,key);
}
}
}
//求T的最小值
int MinBST(BiTree& T){
BiTree p=T;
while(p->lchild){
p=p->lchild;
}
return p->data;
}
//求T的最大值
int MaxBST(BiTree& T){
BiTree p=T;
while(p->rchild){
p=p->rchild;
}
return p->data;
}
int main(){
BiTree T=NULL;//初始化为NULL
InsertBST(T,62);
InsertBST(T,88);
InsertBST(T,58);
InsertBST(T,47);
InsertBST(T,35);
InsertBST(T,73);
InsertBST(T,51);
InsertBST(T,99);
InsertBST(T,37);
InsertBST(T,93);
DeleteBST(T,58);
cout<<MinBST(T)<<endl;
cout<<MaxBST(T)<<endl;
cout<<"end"<<endl;
}
4.平衡二叉树
定义
-
在二叉树中,任何一个结点v的平衡因子都定义为其左、右子树的高度差。注意,空树的高度定义为-1。
-
在二叉查找树T中,若所有结点的平衡因子的绝对值均不超过1 ,则称T为一棵AVL树。
-
在插入和删除时维护平衡,即可
新增操作
- 新节点记为N,从下到上第一个被破坏平衡的祖先记为p (至少是祖父,或者更根的),在同一路径上p的子节点记为q , q的子节点记为s
- 按pqs的位置关系分为左左型、右右型、左右型、右左型、两两对称
- 通过“旋转”来使树重新平衡,旋转不会破坏BST的性质,但能改变子树高度
-
先看中间,左左型和右右型,它们只需沿反方向旋转一次即可
-
左右型和右右型,先调整q和s ,转变为上述两种类型
-
念一下中序遍历顺序,找找感觉
#include <iostream>
#define LH +1//左高
#define EH 0//等高
#define RH -1//右高
using namespace std;
typedef struct BSTNode /* 结点结构 */{
int data; /* 结点数据 */
int bf;//结点的平衡因子
struct BSTNode *lchild, *rchild; /* 左右孩子指针 */
}BSTNode,*BSTree;
void L_Rotate(BSTree& p) {//左左型
//右边大,左旋
//对以p为根的二叉排序树作左旋处理,处理之后p指向新的树根结点,即旋转处理之前的右子树的根结点
BSTree rc=p->rchild;//re指向的p的右子树根结点
p->rchild=rc->lchild;//re的左子树挂接为p的右子树
rc->lchild=p;//p挂在最准根的左边
p=rc;//p指向新的根结点
}
void R_Rotate(BSTree& p){//右右型
//左边大,右旋
//对以p为根的二叉排序树作右旋处理,处理之后p指向新的树根结点,即旋转处理之前的左子树的根结点
BSTree lc=p->lchild;//lc指向的p的左子树根结点
p->lchild=lc->rchild;//lc的右子树挂接为p的左子树
lc->rchild=p;//p挂在最准根的右边
p=lc;//P指向新的根结点
}
void LeftBalance(BSTree& T){//左右型
//对以指针T所指结点为根的二叉树作左平衡旋转处理,结束时指针T指向新的根结点
BSTree lc=T->lchild;//lc指向T的左子树根结点
switch(lc->bf){//检查T的左子树的平衡度,并作相应平衡处理
case LH://新结点插人在T的左孩子的左子树上,要作单右旋处理
T->bf=lc->bf=EH;
R_Rotate(T);
break;
case RH://新结点插人在T的左孩子的右子树上,要作双旋处理
BSTree rd=lc->rchild;
switch(rd->bf){//修改T及其左孩子的平衡因子
case LH:
T->bf=RH;
lc->bf=EH;
break;
case EH:
T->bf=lc->bf=EH;
break;
case RH:
T->bf=EH;
lc->bf=LH;
break;
}
rd->bf=EH;
L_Rotate(T->lchild);//对T的左子树作左旋平衡处理
R_Rotate(T);//对T作右旋平衡处理
}
}
void RightBalance(BSTree& T){//右左型
//对以指针T所指结点为根的二叉树作右平衡旋转处理,结束时指针T指向新的根结点
BSTree rc=T->rchild;//lc指向T的右子树根结点
switch(rc->bf){//检查T的右子树的平衡度,并作相应平衡处理
case RH://新结点插人在T的右孩子的右子树上,要作单右旋处理
T->bf=rc->bf=EH;
L_Rotate(T);
break;
case LH://新结点插人在T的右孩子的左子树上,要作双旋处理
BSTree ld=rc->lchild;
switch(ld->bf){//修改T及其右孩子的平衡因子
case LH:
T->bf=EH;
rc->bf=RH;
break;
case EH:
T->bf=rc->bf=EH;
break;
case RH:
T->bf=RH;
rc->bf=EH;
break;
}
ld->bf=EH;
R_Rotate(T->rchild);//对T的左子树作右旋平衡处理
L_Rotate(T);//对T作左旋平衡处理
}
}
bool InsertAVL(BSTree& T,int key,bool& taller){
///若在平衡的二叉排序树T中不存在和key有相同关键字的结点,则插入一个数据元素为key的新结点
//并返回true,否则返回false,若因插人而使二叉排序树失去平衡,则作平衡旋转处理
//taller反映T长高与否
if(!T){//插人新结点,树“长高”,置taller为true
T=(BSTree)malloc(sizeof(BSTNode));
T->data=key;
T->lchild=T->rchild=NULL;
T->bf=EH;
taller= true;
}else{
if(key==T->data){//树中已存在和key有相同关键字的结点则不再插人
taller= false;
return false;
}else if(key<T->data){//应继续在T的左子树中进行搜索
if(!InsertAVL(T->lchild,key,taller)){//无论是否为true 都执行InsertAVL
return false;
}
//只要插入成功taller就为true
if(taller){//己插人到T的左子树中且左子树“长高”
switch (T->bf){//检查T的平衡度
case LH://原本左子树比右子树高,需要作左平衡处理
LeftBalance(T);
taller= false;
break;
case EH://原本左、右子树等高,现因左子树增高而使树增高
T->bf=LH;
taller= true;
break;
case RH://原本右子树比左子树高,现左、右子树等高
T->bf=EH;
taller= false;
break;
}
}
}else{//应继续在T的右子树中进行搜索
if(!InsertAVL(T->rchild,key,taller)){//未插入
return false;
}
if(taller){//已插人到T的右子树且右子树长高
switch (T->bf){//检查T的平衡度
case LH://原本左子树比右子树高,现左、右子树等高
T->bf=EH;
taller= false;
break;
case EH://原本左、右子树等高,现因右子树增高而使树增高
T->bf=RH;
taller= true;
break;
case RH://原本右子树比左子树高,需要作右平衡处理
RightBalance(T);
taller= false;
break;
}
}
}
}
return true;
}
/* 二叉树的中序遍历递归算法 */
void InOrderTraverse(BSTree T){
if (T == NULL)
return;
/* 中序遍历左子树 */
InOrderTraverse(T->lchild);
/* 显示结点数据,可以更改为其他对结点操作 */
cout<<T->data<<" ";
/* 最后中序遍历右子树 */
InOrderTraverse(T->rchild);
}
int main(){
int i;
int a[13] = { 1, 2, 1, 34, 65, 50, 56, 10, 9, 8,12,3,10 };
BSTree T = NULL;
bool taller;
for (i = 0; i <13; i++){
InsertAVL(T, a[i], taller);
InOrderTraverse(T);
cout<<endl;
}
}
5.红黑树
要求不那么严格的平衡树一红黑树
-
(1)节点都有颜色标记,且只能是红色或黑色。
(2)根是黑色。
(3)所有叶子都是黑色(叶子是NIL/nill节点,不保存实际的数据)。
(4)每个红色节点必须有两个黑色的子节点,也可以表述为从每个叶子到根的所有路径上不能有两个连续的红色节点。
(5)从任一节点到其每个叶子的所有简单路径都包含相同数目的黑色节点。 -
根黑,红黑,叶黑,红不连,同黑
红黑树不是严格的AVL树,平衡二叉树是严格限制层高的,来优化查找性能,但是在维护上消耗时间多
新增节点
-
新增节点规定为红色
-
如果,父节点为黑色,不会破坏规则
-
如果父节点为红色,破坏了红不连的规则,看uncle的颜色
- uncle为红, 那么必然祖父节点为黑色,将G、P、U反色,此时G这棵子树符合规则,应递归检查G (把G视为新节点)是否违反规则。
- 如果父节点为红色,破坏了红不连的规则,看uncle的颜色
- uncle为黑,那么必然祖父节点为黑色,根据G、P、N的位置关系分为四种情况。
- 左左型,PG反色,向右旋转;右右型为镜像
- 左右型,先沿P左旋,变成左左型;右左型为镜像
删除节点
-
节点为叶子或单支或观支
-
双支先转单支
-
节点(toDelete)被删,儿子(N)顶替
- A.节点为红,无需调整
- B.节点为黑,要调整,根据N与新兄弟(S)的颜色做不同调整
考虑N为黑且为左子(右子对称) :
1N是root,完结
2兄弟为红,转为兄弟为黑, case3
3-6兄弟为黑的情况又分为:
双子为黑父为黑
双子为黑,父为红
S内子为黑,父任意
S外子为黑,父任意
N为红色,染黑即可
6.二叉树补充
叶子节点的个数
第k层的节点数
是否完全二叉树
两颗二叉树是否相同,是否互为镜像
翻转二叉树
前序遍历
后续遍历
BST区间搜索
7.前缀树
-
Trie; 又称单词查找树;是一种树形结构;用于保存大量的字符串。它的优点是:利用字符串的公共前缀来节约存储空间
-
Trie 树主要是利用词的公共前缀缩小查词范围、通过状态间的映射关系避免了字符的遍历,从而达到高效检索的目的。
public class TrieNode {
int level;
TrieNode[] children = new TrieNode[26]; // 子节点信息
TrieNode parent; // 当前节点的父节点
public boolean isLast;
public int fre = 1;//出现频率
}
public class Trie {
TrieNode root;
public Trie() {
root = new TrieNode();
}
public void insert(String str) {
char[] chars = str.toCharArray();
TrieNode p = root;
//遍历单词的每个字符
for (int i = 0; i < chars.length; i++) {
char c = chars[i];
TrieNode child = p.children[c - 'a'];
if (child == null) {
TrieNode nnode = new TrieNode();
nnode.level = i;
p.children[c - 'a'] = nnode;
p = nnode;
} else {
p = child;
child.fre++;
}
}
p.isLast = true;
}
/**
* 深度遍历
*/
public void printAll() {
print("", root);
}
private void print(String prefix, TrieNode p) {
if (p.isLast && prefix.length() > 0) {
System.out.println(prefix + " " + p.fre);
}
for (int i = 0; i < 26; i++) {
if (p.children[i] != null) {
print(prefix + (char) ('a' + i), p.children[i]);
}
}
}
//搜索以prefix开头的,打印存在的后面的字符
public void search(String prefix) {
char[] chars = prefix.toCharArray();
TrieNode p = root;
for (int i = 0; i < chars.length; i++) {
char c = chars[i];
TrieNode child = p.children[c - 'a'];
if (child == null) {//结算
return;
} else {
p = child;
}
}
print("", p);
}
}
8.题解
题1:二叉树的最小深度
给定一个二叉树,找出其最小深度。
最小深度是从根节点到最近叶子节点的最短路径上的节点数量。
说明: 叶子节点是指没有子节点的节点。
示例:
给定二叉树 [3,9,20,null,null,15,7],
3
/ \
9 20
/ \
15 7
返回它的最小深度 2.
public int minDepth(TreeNode root) {
if(root==null){
return 0;
}
//如果root已经是叶子它调用min后left和right都为0,会返回1,代表一层
int left=minDepth(root.left);//左子树的深度
int right=minDepth(root.right);//右子树的深度
//左右子树有一个为空
//最后结果等于左子树深度或者右子树深度+1,其中的1是指根
if(left==0||right==0){
return left+right+1;
}
return 1+Math.min(left,right);
}
题2:求根到叶子节点数字之和
给定一个二叉树,它的每个结点都存放一个 0-9 的数字,每条从根到叶子节点的路径都代表一个数字。
例如,从根到叶子节点路径 1->2->3 代表数字 123。
计算从根到叶子节点生成的所有数字之和。
说明: 叶子节点是指没有子节点的节点。
示例 1:输入: [1,2,3]
1
/ \
2 3
输出: 25
解释:
从根到叶子节点路径 1->2 代表数字 12.
从根到叶子节点路径 1->3 代表数字 13.
因此,数字总和 = 12 + 13 = 25.
示例 2:输入: [4,9,0,5,1]
4
/ \
9 0
/ \
5 1
输出: 1026
解释:
从根到叶子节点路径 4->9->5 代表数字 495.
从根到叶子节点路径 4->9->1 代表数字 491.
从根到叶子节点路径 4->0 代表数字 40.
因此,数字总和 = 495 + 491 + 40 = 1026.
List<String> list;
public void dfs(String pre,TreeNode node){//递归node的所有从根到叶的字符串
String s=pre+node.val;//每层将自己的val拼接到s后,如果有左子树就复制一份给下一层调用
//如果有右子树就复制一份给下一层调用
//当没有左右子树的时候就是最深的层,负责结算到list,并return结束这条之路
if(node.left==null&&node.right==null){//当前节点为叶子节点,就结算
list.add(s);
return;
}
if(node.left!=null){
dfs(s,node.left);
}
if(node.right!=null){
dfs(s,node.right);
}
}
public int sumNumbers(TreeNode root) {
if(root==null){
return 0;
}
list=new ArrayList<String>();
dfs("",root);
int sum=0;
for (int i = 0; i < list.size(); i++) {
sum+=Integer.parseInt(list.get(i));
}
return sum;
}
int res=0;
//root为当前节点,cur为前面积累的数,也就是root的数,前面的数
//比如root.val为5,前面积累的数位49
//将积累的数乘10,再加上此节点的数,就位495、
void fun(TreeNode root,int cur){
if(root==null){
return ;
}
//如果为叶子节点就结算,先将前面积累的数*10+val,得到最后结果加入总和
if(root.left==null&&root.right==null){
res+=(cur*10+root.val);
return;
}
fun(root.left,cur*10+root.val);
//如果有左子树就传递给下一层调用
//最深的层结算
fun(root.right,cur*10+root.val);
}
public int sumNumbers2(TreeNode root) {
fun(root,0);
return res;
}
题3:检查平衡性
实现一个函数,检查二叉树是否平衡。在这个问题中,平衡树的定义如下:任意一个节点,其两棵子树的高度差不超过 1。
示例 1:
给定二叉树 [3,9,20,null,null,15,7]
3
/ \
9 20
/ \
15 7
返回 true 。
示例 2:
给定二叉树 [1,2,2,3,3,null,null,4,4]
1
/ \
2 2
/ \
3 3
/ \
4 4
public int check(TreeNode root){//此函数返回root的高度
if(root == null) return 0;
//深度等于左右子树高的深度+1
int longest = Math.max(check(root.left),check(root.right))+1;
return longest;
}
public boolean isBalanced(TreeNode root) {
if(root == null||root.left==null&&root.right==null){
return true;
}
//判断root是否平衡,即判断左右子树高度只差是否大于1
boolean b=Math.abs(check(root.left)-check(root.right))<=1;
//根节点平衡,左右子树也平衡,整个树才平衡
//将b放在判断最前面可以节省时间。不符合时直接返回false
return b&&isBalanced(root.left)&&isBalanced(root.right);
}
题4:将有序数组转换为二叉搜索树
给一个排序数组(从小到大),将其转换为一棵高度最小的二叉搜索树。
样例 1:
输入:[]
输出:{}
解释:二叉搜索树为空
样例 2:
输入:[1,2,3,4,5,6,7]
输出: {4,2,6,1,3,5,7}
解释:
拥有最小高度的二叉搜索树
4
/ \
2 6
/ \ / \
1 3 5 7
要创建一棵高度最小的树,就必须让左右子树的节点数量尽量接近,也就是说,我们要让数组中间的值成为根节点,这么一来,数组左边一半就成为左子树,右边一半成为右子树。
然后,我们继续以类似方式构造整棵树。数组每一区段的中间元素成为子树的根节点,左半部分成为左子树,右半部分成为右子树。
一种实现方式是使用简单的root.insertNode(int v)方法,从根节点开始,以递归方式将值v 插入树中。这么做的确能构造最小高度的树,但不太高效。每次插入操作都要遍历整棵树,用时为O(N log N)。
另一种做法是以递归方式运用createMinimalBST 方法,从而删去部分多余的遍历操作。这个方法会传入数组的一个区段,并返回最小树的根节点。
该算法简述如下。
(1) 将数组中间位置的元素插入树中。
(2) 将数组左半边元素插入左子树。
(3) 将数组右半边元素插入右子树。
(4) 递归处理。
public TreeNode getBST(int[] a,int start,int end){
if(start>end){
return null;
}
if(start==end){
return new TreeNode(a[start]);
}
int mid=start+((end-start)>>1);//选取start到end中间的元素
TreeNode res=new TreeNode(a[mid]);//作为根节点
res.left=getBST(a,start,mid-1);//左子树递归
res.right=getBST(a,mid+1,end);//右子树递归
return res;
}
public TreeNode sortedArrayToBST(int[] nums) {
return getBST(nums,0,nums.length-1);
}
题5:特定深度节点链表
给定一棵二叉树,设计一个算法,创建含有某一深度上所有节点的链表(比如,若一棵树的深度为 D,则会创建出 D 个链表)。返回一个包含所有深度的链表的数组。
示例:
输入:[1,2,3,4,5,null,7,8]
1
/ \
2 3
/ \ \
4 5 7
/
8
输出:[[1],[2,3],[4,5,7],[8]]
我们可以将前序遍历算法稍作修改,将level + 1 传入下一个递归调用。下面是使用深度优先搜索的实现代码。
void createLevelLinkedList(TreeNode root, ArrayList<LinkedList<TreeNode>> lists, int level) {
if (root == null) return; // 基础情况
LinkedList<TreeNode> list = null;
if (lists.size() < level+1) { //list的size小于第level+1层(level层),list中没有level层,创建新level层
//list0保存0层,list1保存1层
list = new LinkedList<TreeNode>();
/* 每一层都按顺序遍历。如果我们第一次访问第i层,那么一定已经访问了第0至i-1层,
因此可以放心地将层数加入到尾部 */
lists.add(list);
}else{//有的话直接获取
list = lists.get(level);
}
list.add(root);//将root的值加入level层
createLevelLinkedList(root.left, lists, level + 1);
createLevelLinkedList(root.right, lists, level + 1);
}
ArrayList<LinkedList<TreeNode>> createLevelLinkedList(TreeNode root) {
ArrayList<LinkedList<TreeNode>> lists = new ArrayList<LinkedList<TreeNode>>();
createLevelLinkedList(root, lists, 0);//初始状态遍历0层
return lists;
}
public ListNode[] listOfDepth(TreeNode tree) {
ArrayList<LinkedList<TreeNode>> lists=createLevelLinkedList(tree);
int n=lists.size();
ListNode[] res=new ListNode[n];
//下面为转换结果
for (int i = 0; i < n; i++) {
LinkedList<TreeNode> leval = lists.get(i);//原结果的第i层
res[i]=new ListNode(leval.get(0).val);//最准结果第i层
ListNode p=res[i];//结果第i层第一个元素
for (int j = 1; j < leval.size(); j++) {
ListNode temp=new ListNode(leval.get(j).val);//先创建
p.next=temp;//再赋值给next
p=p.next;//移动next
}
//节点创建过程类似桶排序,listnode数组为桶,每个桶通过next成一条串
//1.创建res[i]=new
//2.p=res[i]
//3.循环 创建节点->赋值给next->移动next
}
return res;
}
另一种做法是对广度优先搜索稍加修改,即从根节点开始迭代,然后第2 层,第3 层,以此类推。处于第i 层时,则表明我们已访问过第i - 1 层的所有节点,也就是说,要得到i 层的节点,只需直接查看i - 1 层节点的所有子节点即可。
ArrayList<LinkedList<TreeNode>> createLevelLinkedList3(TreeNode root) {
ArrayList<LinkedList<TreeNode>> result = new ArrayList<LinkedList<TreeNode>>();
/* 访问根节点 */
LinkedList<TreeNode> current = new LinkedList<TreeNode>();
if (root != null) {
current.add(root);
}
while (current.size()>0){//当前要处理的层
result.add(current); //加入前一层
LinkedList<TreeNode> parents = current; //将当前层备份到parents
current = new LinkedList<TreeNode>();//重新清空
for (TreeNode parent : parents) {//处理当前层的左右孩子,添加进current
/* 访问子节点 */
if (parent.left!=null) {
current.add(parent.left);
}
if (parent.right!=null) {
current.add(parent.right);
}
}
}
return result;
}
题6:合法二叉搜索树
实现一个函数,检查一棵二叉树是否为二叉搜索树。
示例 1:
输入:
2
/ \
1 3
输出: true
示例 2:
输入:
5
/ \
1 4
/ \
3 6
输出: false
解释: 输入为: [5,1,4,null,null,3,6]。
根节点的值为 5 ,但是其右子节点值为 4 。
解法1:中序遍历
看到此题,首先想到的可能是中序遍历,即将所有元素复制到数组中,然后检查该数组是否有序。这种解法会占用一点儿额外的内存,但大部分情况下都奏效。
唯一的问题在于,它无法正确处理树中的重复值。例如,该算法无法区分下面这两棵树(其中一棵是无效的),因为两者的中序遍历结果相同。
不过,要是假定这棵树不得包含重复值,那么这种做法还是行之有效的。
解法2:最小与最大法
第二种解法利用的是二叉搜索树的定义。一棵什么样的树才可称为二叉搜索树?我们知道这棵树必须满足以下条件:对于每个节点,left.data <= current.data < right.data,但是这样还不够。试看下面这棵小树。
尽管每个节点都比左子节点大,比右子节点小,但这显然不是一棵二叉搜索树,其中25 的位置不对。
更准确地说,成为二叉搜索树的条件是:所有左边的节点必须小于或等于当前节点,而当前节点必须小于所有右边的节点。
public boolean isValidBST(TreeNode root) {
if (root == null) return true;
TreeNode maxLeft = root.left, minRight = root.right;
// 找寻左子树中的最右(数值最大)节点
while (maxLeft != null && maxLeft.right != null)
maxLeft = maxLeft.right;
// 找寻右子树中的最左(数值最小)节点
while (minRight != null && minRight.left != null)
minRight = minRight.left;
// 当前层是否合法,
boolean ret = (maxLeft == null || maxLeft.val < root.val) && (minRight == null || root.val < minRight.val);
// 进入左子树和右子树并判断是否合法
return ret && isValidBST(root.left) && isValidBST(root.right);
}
题7:后继者
设计一个算法,找出二叉搜索树中指定节点的“下一个”节点(也即中序后继)。
如果指定节点没有对应的“下一个”节点,则返回null。
示例 1:
输入: root = [2,1,3], p = 1
2
/ \
1 3
输出: 2
示例 2:
输入: root = [5,3,6,2,4,null,null,1], p = 6
5
/ \
3 6
/ \
2 4
/
1
输出: null
-
如果 p 大于当前节点的值,说明后继节点一定在 RightTree
-
如果 p 等于当前节点的值,说明后继节点一定在 RightTree
-
如果 p 小于当前节点的值,说明后继节点一定在 LeftTree 或自己就是
递归调用 LeftTree,如果是空的,说明当前节点就是答案
如果不是空的,则说明在 LeftTree 已经找到合适的答案,直接返回即可
public TreeNode inorderSuccessor(TreeNode root, TreeNode p) {
if(root==null||p==null){
return null;
}
if(p.val>=root.val){
return inorderSuccessor(root.right,p);
}else {
TreeNode left=inorderSuccessor(root.left,p);
if(left!=null){
return left;
}else{
return root;
}
}
}
如果每个节点有parent
TreeNode inorderSucc(TreeNode n) {
if (n == null) return null;
//如果有右子树n的后继就为右子树最左边的元素,比它大的元素中最小的
if(n.right != null) {
return leftMostChild(n.right);
} else {//没有右子树,就要往上找
TreeNode q = n;//q指向当前元素
TreeNode x = q.parent;//x指向父亲
// 向上移动,直至当前节点位于左子树时停止,x指向的节点位位于父亲左子树上
//如果一直都没有为左子树的节点,x为null。最终返回null
while (x != null && x.left != q) {
q = x;
x = x.parent;
}
return x;
}
}
TreeNode leftMostChild(TreeNode n){
if (n==null) {
return null;
}
while (n.left!= null) {
n = n.left;
}
return n;
}
中序遍历非递归
//中序遍历
void InOrderWithoutRecursion1(BiTree root){
//空树
if (root == NULL)
return;
//树非空
BiTree p = root;
stack<BiTree> s;
while (!s.empty() || p){//p不为空或者栈不为空,都可以继续处理
while (p){//沿左支线一撸到底,全部入栈
s.push(p);
p = p->lchild;
}
//处理栈顶
if (!s.empty()){
p = s.top();//上面while结束p为null,重新指向栈顶
cout << p->data<<endl;//当前子树左子树已处理,处理根,访问p
s.pop();
//进入右子树,开始新的一轮左子树遍历(这是递归的自我实现)
p = p->rchild;//有可能为空,为空,只消费栈中内容,不为空,就要向栈中生产若干内容
}
}
}
//中序遍历
void InOrderWithoutRecursion2(BiTree root){
//空树
if (root == NULL)
return;
//树非空
BiTree p = root;
stack<BiTree> s;
while (!s.empty() || p){
if (p){
s.push(p);
p = p->lchild;
}
else{
p = s.top();
s.pop();
cout <<p->data<<endl;
p = p->rchild;
}
}
}
public TreeNode inorderSuccessor(TreeNode root, TreeNode p) {
if(root==null){
return null;
}
TreeNode t=root;
Stack<TreeNode> s=new Stack<TreeNode>();
boolean isFond=false;
while(!s.empty()||t!=null){
while(t!=null){
s.push(t);
t=t.left;
}
if(!s.empty()){
t=s.peek();
if(p.val==t.val){//如果找到匹配元素就isFond为true
isFond=true;
}else if(isFond){//下一次弹出的元素就为结果
return t;
}
s.pop();
t=t.right;
}
}
return null;
}
题8:首个共同祖先
设计并实现一个算法,找出二叉树中某两个节点的第一个共同祖先。不得将其他的节点存储在另外的数据结构中。注意:这不一定是二叉搜索树。
例如,给定如下二叉树: root = [3,5,1,6,2,0,8,null,null,7,4]
3
/ \
5 1
/ \ / \
6 2 0 8
/ \
7 4
示例 1:
输入: root = [3,5,1,6,2,0,8,null,null,7,4], p = 5, q = 1
输入: 3
解释: 节点 5 和节点 1 的最近公共祖先是节点 3。
示例 2:
输入: root = [3,5,1,6,2,0,8,null,null,7,4], p = 5, q = 4
输出: 5
解释: 节点 5 和节点 4 的最近公共祖先是节点 5。因为根据定义最近公共祖先节点可以为节点本身。
说明:
所有节点的值都是唯一的。
p、q 为不同节点且均存在于给定的二叉树中。
顺着一条p 和q 都在同一边的链子查找,也就是说,若p 和q 都在某节点的左边,就到左子树中查找共同祖先,若都在右边,则在右子树中查找共同祖先。要是p 和q不在同一边,那就表示已经找到第一个共同祖先。
public static TreeNode lowestCommonAncestor(TreeNode root, TreeNode p, TreeNode q) {
if (!covers(root, p) || !covers(root, q)) { // Error check - one node is not in tree
return null;
}
return ancestorHelper(root, p, q);
}
public static TreeNode ancestorHelper(TreeNode root, TreeNode p, TreeNode q) {
if (root == null || root == p || root == q) {
return root;
}
boolean pIsOnLeft = covers(root.left, p);
boolean qIsOnLeft = covers(root.left, q);
if (pIsOnLeft != qIsOnLeft) { // Nodes are on different side
return root;
}
TreeNode childSide = pIsOnLeft ? root.left : root.right;
return ancestorHelper(childSide, p, q);
}
public static boolean covers(TreeNode root, TreeNode p) {
if (root == null) return false;
if (root == p) return true;
return covers(root.left, p) || covers(root.right, p);
}
题9:检查子树
检查子树。你有两棵非常大的二叉树:T1,有几万个节点;T2,有几万个节点。设计一个算法,判断 T2 是否为 T1 的子树。
如果 T1 有这么一个节点 n,其子树与 T2 一模一样,则 T2 为 T1 的子树,也就是说,从节点 n 处把树砍断,得到的树与 T2 完全相同。
示例1:
输入:t1 = [1, 2, 3], t2 = [2]
输出:true
示例2:
输入:t1 = [1, null, 2, 4], t2 = [3, 2]
输出:false
提示:
树的节点数目范围为[0, 20000]。
public static boolean checkSubTree(TreeNode t1, TreeNode t2) {
StringBuilder string1 = new StringBuilder();
StringBuilder string2 = new StringBuilder();
getOrderString(t1, string1);
getOrderString(t2, string2);
return string1.indexOf(string2.toString()) != -1;
}
public static void getOrderString(TreeNode node, StringBuilder sb) {
if (node == null) {
sb.append("X"); // Add null indicator
return;
}
sb.append(node.val); // Add root
getOrderString(node.left, sb); // Add left
getOrderString(node.right, sb); // Add right
}
题10:求和路径
给定一棵二叉树,其中每个节点都含有一个整数数值(该值或正或负)。设计一个算法,打印节点数值总和等于某个给定值的所有路径的数量。注意,路径不一定非得从二叉树的根节点或叶节点开始或结束,但是其方向必须向下(只能从父节点指向子节点方向)。
示例:
给定如下二叉树,以及目标和 sum = 22,
5
/ \
4 8
/ / \
11 13 4
/ \ / \
7 2 5 1
返回:
3
解释:和为 22 的路径有:[5,4,11,2], [5,8,4,5], [4,11,7]
提示:
节点总数 <= 10000
public int pathSum(TreeNode root, int sum) {
// key是前缀和, value是大小为key的前缀和出现的次数
Map<Integer, Integer> prefixSumCount = new HashMap<>();
// 前缀和为0的一条路径
prefixSumCount.put(0, 1);
// 前缀和的递归回溯思路
return recursionPathSum(root, prefixSumCount, sum, 0);
}
private int recursionPathSum(TreeNode node, Map<Integer, Integer> prefixSumCount, int target, int currSum) {
// 1.递归终止条件
if (node == null) {
return 0;
}
// 2.本层要做的事情
int res = 0;
// 当前路径上的和
currSum += node.val;
//---核心代码
// 看看root到当前节点这条路上是否存在节点前缀和加target为currSum的路径
// 当前节点->root节点反推,有且仅有一条路径,如果此前有和为currSum-target,而当前的和又为currSum,两者的差就肯定为target了
// currSum-target相当于找路径的起点,起点的sum+target=currSum,当前点到起点的距离就是target
res += prefixSumCount.getOrDefault(currSum - target, 0);
// 更新路径上当前节点前缀和的个数
prefixSumCount.put(currSum, prefixSumCount.getOrDefault(currSum, 0) + 1);
//---核心代码
// 3.进入下一层
res += recursionPathSum(node.left, prefixSumCount, target, currSum);
res += recursionPathSum(node.right, prefixSumCount, target, currSum);
// 4.回到本层,恢复状态,去除当前节点的前缀和数量
prefixSumCount.put(currSum, prefixSumCount.get(currSum) - 1);
return res;
}