二叉树(BinaryTree)

二叉树是n(n>=0)个结点的有限集合, 该集合或者为空集(称为空二叉树),或者由一个根结点和两棵互不相交的、分别称为根结点的左子树和右子树组成。

一颗二叉树

二叉树特点

由二叉树定义以及图示分析得出二叉树有以下特点:

  1. 每个结点最多有两颗子树,所以二叉树中不存在度大于2的结点。
  2. 左子树和右子树是有顺序的,次序不能任意颠倒。
  3. 即使树中某结点只有一棵子树,也要区分它是左子树还是右子树。

二叉树性质

  1. 在二叉树的第i层上最多有2i-1 个节点 。(i>=1)
  2. 二叉树中如果深度为k,那么最多有2k-1个节点。(k>=1)
  3. n0=n2+1 n0表示度数为0的节点数,n2表示度数为2的节点数。
  4. 在完全二叉树中,具有n个节点的完全二叉树的深度为[log2n]+1,其中[log2n]是向下取整。
  5. 若对含 n 个结点的完全二叉树从上到下且从左至右进行 1 至 n 的编号,则对完全二叉树中任意一个编号为 i 的结点有如下特性:
(1) 若 i=1,则该结点是二叉树的根,无双亲, 否则,编号为 [i/2] 的结点为其双亲结点;
(2) 若 2i>n,则该结点无左孩子, 否则,编号为 2i 的结点为其左孩子结点;
(3) 若 2i+1>n,则该结点无右孩子结点, 否则,编号为2i+1 的结点为其右孩子结点。

斜树

斜树:所有的结点都只有左子树的二叉树叫左斜树。所有结点都是只有右子树的二叉树叫右斜树。这两者统称为斜树。

左斜树

右斜树

满二叉树

在一棵二叉树中。如果所有分支结点都存在左子树和右子树,并且所有叶子都在同一层上,这样的二叉树称为满二叉树。 满二叉树的特点有:

  1. 叶子只能出现在最下一层。出现在其它层就不可能达成平衡。
  2. 非叶子结点的度一定是2。
  3. 在同样深度的二叉树中,满二叉树的结点个数最多,叶子数最多。

满二叉树

完全二叉树

对一颗具有n个结点的二叉树按层编号,如果编号为i(1<=i<=n)的结点与同样深度的满二叉树中编号为i的结点在二叉树中位置完全相同,则这棵二叉树称为完全二叉树。 满二叉树

特点: 1)叶子结点只能出现在最下层和次下层。 2)最下层的叶子结点集中在树的左部。 3)倒数第二层若存在叶子结点,一定在右部连续位置。 4)如果结点度为1,则该结点只有左孩子,即没有右子树。 5)同样结点数目的二叉树,完全二叉树深度最小。 注:满二叉树一定是完全二叉树,但反过来不一定成立。

二叉树的存储结构

顺序存储

二叉树的顺序存储结构就是使用一维数组存储二叉树中的结点,并且结点的存储位置,就是数组的下标索引。

完全二叉树

上图所示的一棵完全二叉树采用顺序存储方式,如下图表示:

顺序存储2

由图上图可以看出,当二叉树为完全二叉树时,结点数刚好填满数组。 那么当二叉树不为完全二叉树时,采用顺序存储形式如何呢?

顺序存储3

其中浅色结点表示结点不存在。那么上图所示的二叉树的顺序存储结构如下图所示:

顺序存储4

其中,∧表示数组中此位置没有存储结点。 此时可以发现,顺序存储结构中已经出现了空间浪费的情况。 那么对于右斜树极端情况对应的顺序存储结构如下图所示:

顺序存储5

由上图可以看出,对于这种右斜树极端情况,采用顺序存储的方式是十分浪费空间的。 因此,顺序存储一般适用于完全二叉树。

二叉链表

既然顺序存储不能满足二叉树的存储需求,那么考虑采用链式存储。 由二叉树定义可知,二叉树的每个结点最多有两个孩子。 因此,可以将结点数据结构定义为一个数据和两个指针域。 表示方式如下图所示:

二叉链表存储1

定义节点代码


class Node<E>{

    E data;
    Node<E> rchild;
    Node<E> lchild;

}

则完全二叉树可以采用下图表示。

完全二叉树

二叉链表存储2

上图中采用一种链表结构存储二叉树,这种链表称为二叉链表。

二叉树遍历

二叉树的遍历是指从二叉树的根结点出发, 按照某种次序依次访问二叉树中的所有结点, 使得每个结点被访问一次,且仅被访问一次。 二叉树的访问次序可以分为四种:

前序遍历: 根->左子树->右子树
中序遍历: 左子树->根->右子树
后序遍历; 左子树->右子树->根
层序遍历
前序遍历

通俗的说就是从二叉树的根结点出发, 当第一次到达结点时就输出结点数据, 按照先向左在向右的方向访问。

完全二叉树

上图所示二叉树访问如下:

  1. 从根结点出发,则第一次到达结点A,故输出A;
  2. 继续向左访问,第一次访问结点B,故输出B;
  3. 按照同样规则,输出D,输出H;
  4. 当到达叶子结点H,返回到D,此时已经是第二次到达D,故不在输出D,进而向D右子树访问,D右子树不为空,则访问至I,第一次到达I,则输出I;
  5. I为叶子结点,则返回到D,D左右子树已经访问完毕,则返回到B,进而到B右子树,第一次到达E,故输出E;
  6. 向E左子树,故输出J;
  7. 按照同样的访问规则,继续输出C、F、G;

则上图所示二叉树的前序遍历输出为:

ABDHIEJCFG
中序遍历

就是从二叉树的根结点出发,当第二次到达结点时就输出结点数据,按照先向左在向右的方向访问。

上图所示二叉树中序访问如下:

  1. 从根结点出发,则第一次到达结点A,不输出A,继续向左访问,第一次访问结点B,不输出B;继续到达D,H;
  2. 到达H,H左子树为空,则返回到H,此时第二次访问H,故输出H;
  3. H右子树为空,则返回至D,此时第二次到达D,故输出D;
  4. 由D返回至B,第二次到达B,故输出B;
  5. 按照同样规则继续访问,输出J、E、A、F、C、G;

则上图所示二叉树的中序遍历输出为:

HDIBJEAFCG
后序遍历

就是从二叉树的根结点出发,当第三次到达结点时就输出结点数据,按照先向左在向右的方向访问。

上图所示二叉树后序访问如下:

  1. 从根结点出发,则第一次到达结点A,不输出A,继续向左访问,第一次访问结点B,不输出B;继续到达D,H;
  2. 到达H,H左子树为空,则返回到H,此时第二次访问H,不输出H;
  3. H右子树为空,则返回至H,此时第三次到达H,故输出H;
  4. 由H返回至D,第二次到达D,不输出D;
  5. 继续访问至I,I左右子树均为空,故第三次访问I时,输出I;
  6. 返回至D,此时第三次到达D,故输出D;
  7. 按照同样规则继续访问,输出J、E、B、F、G、C,A;

则上图所示二叉树的后序遍历输出为:

HIDJEBFGCA
层次遍历

层次遍历就是按照树的层次自上而下的遍历二叉树。 针对上图所示二叉树的层次遍历结果为:

ABCDEFGHIJ

虽然二叉树的遍历过程看似繁琐,但是由于二叉树是一种递归定义的结构, 故采用递归方式遍历二叉树的代码十分简单。 递归实现代码如下:

package datastructure.tree;

import lombok.Getter;
import lombok.Setter;

import java.util.Queue;
import java.util.concurrent.LinkedBlockingDeque;

/**
 * 二叉树
 * 
 * <p>
 * 定义:二叉树是n(n>=0)个结点的有限集合,该集合或者为空集(称为空二叉树),或者由一个根结点和两棵互不相交的、分别称为根结点的左子树和右子树组成
 * </p>
 * <p>
 * 二叉树特点: 由二叉树定义以及图示分析得出二叉树有以下特点: 1)每个结点最多有两颗子树,所以二叉树中不存在度大于2的结点。
 * 2)左子树和右子树是有顺序的,次序不能任意颠倒。 3)即使树中某结点只有一棵子树,也要区分它是左子树还是右子树。
 * </p>
 * <p>
 * 二叉树性质: 1)在二叉树的第i层上最多有2i-1 个节点 。(i>=1) 2)二叉树中如果深度为k,那么最多有2k-1个节点。(k>=1)
 * 3)n0=n2+1 n0表示度数为0的节点数,n2表示度数为2的节点数。
 * 4)在完全二叉树中,具有n个节点的完全二叉树的深度为[log2n]+1,其中[log2n]是向下取整。 5)若对含 n
 * 个结点的完全二叉树从上到下且从左至右进行 1 至 n 的编号,则对完全二叉树中任意一个编号为 i 的结点有如下特性:
 * </p>
 * <p>
 * (1) 若 i=1,则该结点是二叉树的根,无双亲, 否则,编号为 [i/2] 的结点为其双亲结点; (2) 若 2i>n,则该结点无左孩子, 否则,编号为
 * 2i 的结点为其左孩子结点; (3) 若 2i+1>n,则该结点无右孩子结点, 否则,编号为2i+1 的结点为其右孩子结点。 。
 * </p>
 * 
 * @author liuyi27
 *
 */
@Setter
@Getter
public class BinaryTree {

    private Object data;
    private BinaryTree lchild, rchild;

    /**
     * 前序遍历递归算法
     * <p>
     * 前序遍历通俗的说就是从二叉树的根结点出发,当第一次到达结点时就输出结点数据,按照先向左再向右的方向访问。
     * </p>
     */
    public void preOrderTraverse(BinaryTree tree) {
        if (tree == null) {
            return;
        }

        System.out.print(tree.getData());
        preOrderTraverse(tree.getLchild());
        preOrderTraverse(tree.getRchild());
    }

    /**
     * 中序遍历递归算法
     * <p>
     * 中序遍历就是从二叉树的根结点出发,当第二次到达结点时就输出结点数据,按照先向左再向右的方向访问
     * </p>
     */
    public void inOrderTraverse(BinaryTree tree) {
        if (tree == null) {
            return;
        }

        inOrderTraverse(tree.getLchild());
        System.out.print(tree.getData());
        inOrderTraverse(tree.getRchild());
    }

    /**
     * 后序遍历递归算法
     * <p>
     * 后序遍历就是从二叉树的根结点出发,当第三次到达结点时就输出结点数据,按照先向左再向右的方向访问
     * </p>
     */
    public void postOrderTraverse(BinaryTree tree) {
        if (tree == null) {
            return;
        }

        postOrderTraverse(tree.getLchild());
        postOrderTraverse(tree.getRchild());
        System.out.print(tree.getData());
    }

    /**
     * 前序建立二叉树
     */
    public static void preCreate() {

    }

    /**
     * 层序遍历
     * <p>
     * 层次遍历就是按照树的层次自上而下的遍历二叉树
     * </p>
     * 
     * @param node
     * @param queue
     */
    public void LayerOrder(BinaryTree node, Queue<BinaryTree> queue) {

        if (node != null && queue.isEmpty()) {
            // 将当前节点放入队列首指针所指位置
            queue.add(node);
            System.out.print(queue.poll().getData());
        } else {
            System.out.print(node.getData());
        }

        if (node.lchild != null) {
            queue.add(node.lchild);
        }

        if (node.rchild != null) {
            queue.add(node.rchild);
        }

        BinaryTree nextNode = queue.poll();
        if (nextNode != null) {
            this.LayerOrder(nextNode, queue);
        }

    }

    public static void main(String[] args) {

        BinaryTree biTree2L3L = new BinaryTree();
        biTree2L3L.setData("D");

        BinaryTree biTree2L = new BinaryTree();
        biTree2L.setData("B");
        biTree2L.setLchild(biTree2L3L);

        BinaryTree biTree2R = new BinaryTree();
        biTree2R.setData("C");

        BinaryTree biTree = new BinaryTree();
        biTree.setData("A");
        biTree.setLchild(biTree2L);
        biTree.setRchild(biTree2R);

        Queue<BinaryTree> queue = new LinkedBlockingDeque<BinaryTree>();
        biTree.LayerOrder(biTree, queue);
        System.out.println("");
        biTree.preOrderTraverse(biTree);
        System.out.println("");
        biTree.inOrderTraverse(biTree);
        System.out.println("");
        biTree.postOrderTraverse(biTree);


    }
}

面试中关于二叉树结构的常见问题

  • 求二叉树的高度
  • 在二叉树中查找给定节点的祖先节点

  • 求二叉树的最低公共祖先LCA

性质:如果两条链有公共祖先,那么公共祖先往上的结点都重合.因为如果x=x',那么x->next=x'->next必然成立.

可能性一:若是二叉搜索树.

1如果x,y小于root,则在左边找

2如果x,y大于root,则在右边找

3如果x,y在root之间,则root就是LCA

可能性二:不是二叉搜索树,甚至不是二叉树,但是,每个一节点都有parent指针

那么解法有2:

1:空间换时间:从x,y到root的链表可以保存在栈中,找出最后一个相同结点即可.

2.不用空间换时间,多重扫描法,x,y到root两条链路可能一长一短,相差为n个结点,那么长链表先前移n步,然后,二者同步前移,找到第一个相同结点即可.(树不含环,这种办法有效.)

可能性三:这是一棵只有left和right的平凡二叉树.

那么需要辅助空间,空间换时间法,先调用16.中的GetNodePath()获得两条从root->x和root->y的链表路径.然后比较两条链表,找到最后一个相同的结点即可.


//若是二叉搜索树,返回x和y的公共最低祖先LCA

node* LowestCommonAncestor1(node* root,node* x,node* y)
{
    if(!root || !x || !y) return NULL;
    if(x->value < root->value && y->value < root->value)
        return LowestCommonAncestor1(root->left,x,y);
    else if(x->value > root->value && y->value > root->value)
        return LowestCommonAncestor1(root->right,x,y);
    else 
        return root;
}
//若不是搜索二叉树,但是,每个结点都有父结点,空间换时间法,否则需要重重复复地扫描路径
node* LowestCommonAncestor2(node* x,node* y)
{
    stack<node*> st1,st2;
    while(x)
    {
        st1.push(x);
        x=x->p;
    }
    while(y)
    {
        st2.push(y);
        y=y->p;
    }
    node* pLCA=NULL;
    while(!st1.empty() && !st2.empty() && st1.top()==st2.top())
    {
        pLCA=st1.top();
        st1.pop();
        st2.pop();
    }    
    return pLCA;
}
//不用空间换时间法
int GetListLength(node* x)
{
    int Count=0;
    while(x)
    {
        ++Count;
        x=x->p;
    }
    return Count;
}
int Myabs(int val)
{
    return val > 0 ? val : -val;
}
node* LowestCommonAncestor3(node* x,node* y)
{
    int LengthX=GetListLength(x);
    int LengthY=GetListLength(y);
    node* pLong=x,*pShort=y;
    if(LengthX < LengthY)
    {
        pLong=y;
        pShort=x;
    }
    for(int i=0;i<Myabs(LengthX-LengthY);++i)
        pLong=pLong->p;
    while( pLong && pShort && pLong !=pShort)
    {
        pLong=pLong->p;
        pShort=pShort->p;
    }
    if(pLong == pShort)
        return pLong;
    return NULL;
}
//既不是二叉搜索树,也不没有parent指针,只是一棵平凡的二叉树
bool GetNodePath(node* root, node* pNode, list<node*>& path);
node* LowestCommonAncestor4(node* root,node* x,node* y)
{
    list<node*> path1;
    list<node*> path2;
    GetNodePath(root,x,path1);
    GetNodePath(root,y,path2);
    node* pLCA=NULL;
    list<node*>::const_iterator it1=path1.begin();
    list<node*>::const_iterator it2=path2.begin();
    while(it1 != path1.end() && it2 != path2.end() && *it1 == * it2)
    {
        pLCA=*it1;
        ++it1;
        ++it2;
    }    
    return pLCA;

}
  • 已知前序遍历序列和中序遍历序列,确定一棵二叉树。

  • 已知后序遍历序列和中序遍历序列,确定一棵二叉树。

  • 在二叉树中找出和为某一值的所有路径

要求所有路径,路径即root到某一结点的结点之集合.这是一个深度优先原则的搜索.我们很容易想到先序遍历.

为了跟踪路径和,我们需要一个额外的辅助栈来跟踪递归调用栈的操作过程.

在进入到下一个调用FindPath(left)和FindPath(right)时,递归栈会将root压入栈,因此我们也模仿进栈.当FindPath(left)和FindPath(right)返回,FindPath(root)运行周期到之后, 局部函数变量root会被析造,root会从递归栈中弹出,因此,我们也从辅助栈中弹出root,只需要在中间加上判断条件,将满足条件的结果输出即可.改成迭代版也很简单.

void FindPath(node* root,int expectedSum,vector<int>& Path,int currentSum);//先序遍历改装版
void FindPath(node* root,int expectedSum)
{
    int currentSum=0;
    vector<int> Path;
    FindPath(root,expectedSum,Path,currentSum);
}
void FindPath(node* root,int expectedSum,vector<int>& Path,int currentSum)//先序遍历改装版
{
    if(!root)
        return ;
    //访问根结点,同时将root->value加入辅助栈
    currentSum += root->value;
    Path.push_back(root->value);
    if(root->left==NULL && root->right ==NULL && currentSum == expectedSum)
    {
        for(vector<int>::size_type i=0; i < Path.size(); ++i)
            cout<<Path[i]<<' ';
        cout<<endl;
    }
    FindPath(root->left,expectedSum,Path,currentSum);
    FindPath(root->right,expectedSum,Path,currentSum);
    //递归栈中,此时返回时,会将父结点销毁,因为,局部函数生命周期已经到了.
    //所以辅助栈也需要和递归栈同步,将栈顶元素弹栈,同时当前路径减去栈顶元素
    Path.pop_back();
    currentSum-=root->value;
}
  • 编写一个程序,把一个有序整数数组放到二叉树中

这道题做法非常多,单纯是这么要求比较奇怪,因此,我选择广度优先插入, 利用队列实现,其实插成一个链表,或者随便插入不知道可不可以?没有其它要求真不知道怎么弄.

  • 判断整数序列是不是二叉搜索树的后序遍历结果

典型的递归思维,后序遍历,根在最后,因此用根将二叉搜索树可以分成左右子树,再递归处理左右子树即可.

  • 求二叉树的镜像

画出一个特例,我们发现只需要交换每个结点的左右指针(注意:不是数值)即可.因此在先序遍历的时候换指针即可,没有顺序要求.

  • 一棵排序二叉树(即二叉搜索树BST),令 f=(最大值+最小值)/2,设计一个算法,找出距离f值最近、大于f值的结点。复杂度应尽可能低。

BST中最大值是最右边的值,最小值是最左边的值,这样就容易求出f,再求f的父指针或者右指针都可以?(我是这么认为的).这里求父指针.

  • 把二叉搜索树转变成排序的双向链表

其实就是中序遍历的迭代版本,只是将中间的访问结点代码换成了调整指针.这种办法返回的是链表的最后一个指针LastVist,因为是双链表,这也可以接受.

  • 打印二叉树中的所有路径.

和前边的思路类似,用辅助栈记录递归栈的运行过程,先序遍历(深搜)的过程中,遇到叶子结点(!left && !right的结点)就输出辅助栈的内容.

  • 求二叉树中从根到某一结点的一条路径.

思路一如既往还是利用辅助栈来追踪递归调用栈的运行过程,只是过程有所区别,将结点入栈,如果在结点的左边能找到一条路径,那么不需要和递归栈同步(即弹栈),直接返回true,如果左边没找到这样的一条路径,再到右边找,如果找到了,返回true.如果左右都找不到存在一条这样的路径,则说明在这个结点上不可能存在这样的路径,需要在辅助栈中弹出这个结点.再遍历其它结点.实质上也是先序遍历的改装版.

  • 判断B子树是否是A子树的子结构

  • 利用先序和中序结果重建二叉树

  • 求二叉树中叶子结点的个数

  • 求二叉树中节点的最大距离。

如果我们把二叉树看成一个图,父子节点之间的连线是双向的,我们定义距离为两个节点之间边的个数。(来自编程之美)

特点:相距最远的两个节点,一定是两个叶子节点,或者一个结点到它的根节点。(为什么?)因为如果当前结点不是叶子结点,即它还有子结点,那么它的子结点到另一个端点的距离肯定可以达到更远。如果是根结点,那么根结点只可能有一条支路。

  • 打印二叉树中某层的结点。

  • 判断两棵二叉树是否相等