树和二叉树一篇就搞定!

二叉树铺垫——树

前面几篇文章咱们主要介绍的线性表,栈,队列,串,等等,都是一对一的线性结构,而今天咱们所讲解的 “树” 则是一种典型的非线性结构,非线性结构的特色就是,任意一个结点的直接前驱,若是存在,则必定是惟一的,直接后继若是存在,则能够有多个,也能够理解为一对多的关系,下面咱们就先来认识一下树ios

树的概念

下图咱们平常生活中所见到的树,能够看到,从主树干出发,向上衍生出不少枝干,而每一根枝干,又衍生出一些枝丫,就这样组成了咱们在地面上能够看到的树的结构,但对于每个小枝丫来说,归根结底,仍是来自于主树干的层层衍生造成的。算法

咱们每每须要在计算机中解决这样一些实际问题 例如:数组

  • 用于保存和处理树状的数据,例如家谱,组织机构图
  • 进行查找,以及一些大规模的数据索引方面
  • 高效的对数据排序

先不提一些复杂的功能,就例如对于一些有树状层级结构的数据进行建模,解决实际问题,咱们就能够利用 “树” 这种结构来进行表示,为了更符合咱们的习惯,咱们通常把 “树” 倒过来看,咱们就能够将其概括为下面这样的结构,这也就是咱们数据结构中的 “ 树”微信

树中的常见术语

  • 结点:包含数据项以及指向其余结点的分支,例如上图中圆 A 中,既包含数据项 A 又指向 B 和 C 两个分支数据结构

  • 特别的,由于 A 没有前驱,且有且只有一个,因此称其为根结点函数

  • 子树:由根结点以及根结点的全部后代导出的子图称为树的子树post

    • 例以下面两个图均为上面树中的子树

  • 结点的度:结点拥有子树的数目,简单的就是直接看有多少个分支,例如上图 A 的度为2,B的度为1学习

  • 叶结点:也叫做终端结点,即没有后继的结点,例如 E F G H I
  • 分支结点:也叫做非终端结点,除叶结点以外的均可以这么叫
  • 孩子结点:也叫做儿子结点,即一个结点的直接后继结点,例如 B 和 C 都是 A 的孩子结点
  • 双亲结点:也叫做父结点,一个结点的直接前驱,例如 A 是 B 和 C 的双亲结点
  • 兄弟结点:同一双亲的孩子结点互称为兄弟结点 例如 B 和 C 互为兄弟spa

  • 堂兄弟:双亲互为兄弟结点的结点,例如 D 和 E 互为堂兄弟
  • 祖先结点:从根结点到达一个结点的路径上的全部结点,A B D 结点均为 H 结点的祖先结点设计

  • 子孙结点:以某个结点为根的子树中的任意一个结点都称为该结点的子孙结点,例如 C 的子孙结点有 E F I
  • 结点的层次:设根结点层次为1,其他结点为其双亲结点层次加1,例如,A 层次为1,B C 层次为 2

  • 树的高度:也叫做树的深度,即树中结点的最大层次

  • 有序/无序树:树中结点子树是否从左到右为有序,有序则为有序树,无序则为无序树

可能你们也看到了,上面我举的例子,分支所有都在两个之内,这就是咱们今天所重点介绍的一种树—— “二叉树”

二叉树

在计算机科学中,二叉树(英语:Binary tree)是每一个结点最多只有两个分支(即不存在分支度大于2的结点)的树结构。一般分支被称做“左子树”或“右子树”。二叉树的分支具备左右次序,不能随意颠倒。 ——维基百科

根据定义须要特别强调的:

  • 是每一个结点最多只有两个分支,不是表明只能有两个分支,而是最多,没有或者只要一个都是能够的
  • 左子树和右子树必须有明确的次序,即便只有一颗也要说明,具体是左子树仍是右子树

几种特殊的二叉树

(一) 满二叉树

一般状况下,咱们见到的树都是有高有低的,层次不齐的,若是一颗二叉树中,任意一层的结点个数都达到了最大值,这样的树称为满二叉树,一颗高度为 k 的二叉树具备 2k - 1 次个结点

(二) 彻底二叉树

彻底二叉树是效率很高的数据结构,彻底二叉树是由满二叉树而引出来的。对于深度为K的,有n个结点的二叉树,当且仅当其每个结点都与深度为K的满二叉树中编号从1至n的结点一一对应时称之为彻底二叉树

如何快速判断是否是彻底二叉树:

  • 若是一棵二叉树只有最下面两层结点的度能够小于2,而且最下面一层的结点都集中在该层最左边的连续位置上,此树能够成为彻底二叉树
  • 看着树的示意图,心中默默按照满二叉树的结构逐层顺序编号,若是编号出现了空挡,就说明不是彻底二叉树

(三) 正则二叉树

正则二叉树也称做严格二叉树,若是一颗二叉树的任意结点,要么是叶结点,要么就恰有两颗非空子树,即除了度数为0的叶结点外,全部分支结点的度都为2

二叉树的性质

  • 性质1:一个非空的二叉树的第 i 层上最多有 2i-1 (i $\geq$ 0) 个结点
  • 性质2:深度为 k 的二叉树至多有 2k-1 个结点
  • 性质3:任何一颗二叉树中,若叶节点个数为n0 ,度为2的节点个数为 n2 则 n0 = n2 + 1
  • 性质4:具备 n 个结点的彻底二叉树的深度为 [log2n]+1 注:[n] 表示不大于 n 的最大整数
  • 性质5:若是对一颗有 n 个结点的彻底二叉树按层次自上而下(每层从左到右)对结点从1 到 n 进行编号,则对任意一个结点 i (i $\leq$ i $\leq$ n)
    • 若 i = 1 ,则结点 i 为根,无双亲,若 i > 1,则双亲的编号是 [i/2]
    • 若 2i $\leq$ n ,则 i 的左孩子的编号为 2i,不然无左孩子
    • 若 2i + 1 $\leq$ n, 则 i 的右孩子的编号为 2i + 1,不然无右孩子

二叉树的顺序存储结构

(一) 彻底二叉树中

对于树这种一对多的的关系,使用顺序存储结构确实不是很合理,可是也是能够实现的

对于一个彻底二叉树来讲,将树上编号为 i 的结点存储在一维数组中下标为 i 的份量中,以下图所示

(二) 普通二叉树中

若是对于普通二叉树,则须要现将一些空结点补充,使其成为彻底二叉树,新增的空结点设置为 ^ 以下图所示

(三) 较为极端的状况中

如在深度为 k 右斜树中,这种状况下,它只有 k 个结点,可是根据前面的性质,咱们能够知道,却须要分配 2k-1 个存储单元,这显然对存储空间是极大的浪费,因此看起来只有彻底二叉树的状况下,顺序存储方式比较实用

二叉树的链式存储结构(重点)

顺序结构显然不是很适合使用,因此在实际中,咱们会选择链式存储结构,链式存储结构中,除了须要存储自己的元素,还须要设置指针,用来反映结点间的逻辑关系,二叉树中,每一个结点最多有两个孩子,因此咱们设置两个指针域,分别指向该结点的左孩子和右孩子,这种结构称为二叉链表结点(重点讲解这一种)

二叉树中经常进行 的一个操做是寻找结点的双亲,每一个结点还能够增长一个指向双亲的指针域,这种结构称为三叉链表节点

利用二叉链表结点就能够构成二叉链表,以下图所示:

树和二叉链表的代码表示

(一) 树的抽象数据类型

#ifndef _BINARYTREE_H_
#define _BINARYTREE_H_ 
#include<iostream>
using namespace std;

template<class T>
class binaryTree {
public:
    // 清空 
    virtual void clear()=0;
    // 判空,表空返回true,非空返回false                    
    virtual bool empty()const=0;
    //二叉树的高度 
    virtual int height() const=0;
    //二叉树的结点总数 
    virtual int size()const=0;
    //前序遍历 
    virtual void preOrderTraverse() const=0;
    //中序遍历 
    virtual void inOrderTraverse() const=0;
    //后序遍历 
    virtual void postOrderTraverse() const=0;
    //层次遍历 
    virtual void levelOrderTraverse() const=0;
    virtual ~binaryTree(){};
};

#endif

(二) 二叉链表的表示

注意:咱们之因此将Node类型及其指针设置为私有成员是由于,利于数据的封装和隐藏,关于此概念,这一篇不过多说明了,咱们仍是重点关注算法的实现

#ifndef _BINARYLINKLIST_H_
#define _BINARYLINKLIST_H_
#include "binaryTree.h"
#include<iostream>
using namespace std;

//elemType为顺序表存储的元素类型
template <class elemType>                   
class BinaryLinkList: public binaryTree<elemType>
{ 
private:
    //二叉链表节点 
    struct Node {
        // 指向左右孩子的指针 
        Node *left, *right;
        // 结点的数据域 
        elemType data;
        //无参构造函数 
        Node() : left(NULL), right(NULL) {}
        Node(elemType value, Node *l = NULL, Node *r = NULL) {
            data = value;
            left = 1;
            right = r;
        }
        ~Node() {}
    };
    
    //指向二叉树的根节点 
    Node *root;
    //清空
    void clear(Node *t) const;
    //二叉树的结点总数
    int size(Node *t) const;
    //二叉树的高度
    int height(Node *t) const;
    //二叉树的叶结点个数
    int leafNum(Node *t) const;
    //递归前序遍历
    void preOrder(Node *t) const;
    //递归中序遍历
    void inOrder(Node *t) const;
    //递归后序遍历
    void postOrder(Node *t) const;
    
public:
    //构造空二叉树 
    BinaryLinkList() : root(NULL) {}
    ~BinaryLinkList() {clear();}
    //判空 
    bool empty() const{ return root == NULL; }
    //清空 
    void clear() {
        if (root)
            clear(root);
        root = NULL;
    }
    //求结点总数
    int size() const { return size(root); }
    //求二叉树的高度
    int height() const { return heigth(root); }
    //二叉树叶节点的个数
    int leafNum() const { return leafNum(root); }
    //前序遍历
    void preOrderTraverse() const { if(root) preOrder(root); }
    //中序遍历
    void inOrderTraverse() const { if(root) inOrder(root); }
    //后序遍历
    void postOrderTraverse() const {if(root) postOrder(root); }
    //层次遍历
    void levelOrderTeaverse() const;
    //非递归前序遍历
    void preOrderWithStack() const;
    //非递归中序遍历
    void inOrderWithStack() const;
    //非递归后序遍历
    void postOrderWithStack() const;
};

#endif

二叉树的遍历

(一) 深度优先遍历

概念:沿着二叉树的深度遍历二叉树的节点,尽量深的访问二叉树的分支,主要分为:前序遍历,中序遍历,后序遍历,三种

  • 先序遍历
    • 先访问根节点,而后前序遍历左子树,最后前序遍历左子树 (根 - 左 - 右)
  • 中序遍历
    • 先中序遍历左子树,再访问根节点,最后中序遍历右子树 (左 - 根 - 右)
  • 后序遍历
    • 前后序遍历左子树,后序遍历右子树,最后访问根节点 (左 - 右 - 根)

举个例子就清楚了:

以上图为例,三种遍历方式的执行顺序为:

  • 前序遍历:A - B - C - E - F
  • 中序遍历:B - A - E - C - F
  • 后序遍历:B - E - F - C - A

咱们以中序为例:先中序遍历左子树,再访问根节点,最后中序遍历右子树 (左 - 根 - 右)这是什么意思呢?

中序遍历,就是把每一个点都当作头结点,而后每次都执行中序遍历,也就是(左 - 根 - 右),等左边空了,就返回访问当前结点的父节点,也就是中,记录后,再访问右

例如:从根结点 A 出发,先访问左孩子 B ,左边没有了,返回到 A ,访问 A 右边 C ,对其再进行中序遍历, 即先访问 E 而后返回 C 再 访问 F 即:B - A - E - C - F

首先咱们先使用递归的方法来实现这三种遍历方式,采用递归,给个人感受就是极其容易理解,并且写代码很简洁,想要快速实现这种算法,简直不要太快

(1) 前序遍历-递归

template <class elemType>
void BinaryLinkList<elemType>:: preOrder(Node *t) const {
    if (t) {
        cout << t -> data << ' ';
        preOrder(t -> left);
        preOrder(t -> right);
    }
}

(2) 中序遍历-递归

template <class elemType>
void BinaryLinkList<elemType>:: inOrder(Node *t) const {
    if (t) {
        preOrder(t -> left);
        cout << t -> data << ' ';
        preOrder(t -> right);
    }
}

(3) 后序遍历-递归

template <class elemType>
void BinaryLinkList<elemType>:: postOrder(Node *t) const {
    if (t) {
        preOrder(t -> left);
        preOrder(t -> right);
        cout << t -> data << ' ';       
    }
}

提示:你们可能会注意到,在前面的定义中咱们定义了这样三个方法,而且其都是公有的

//前序遍历
void preOrderTraverse() const { if(root) preOrder(root); }
//中序遍历
void inOrderTraverse() const { if(root) inOrder(root); }
//后序遍历
void postOrderTraverse() const {if(root) postOrder(root); }

这是由于,前面递归的这三种方法,都须要一个Node类型的指针做为参数,而指向二叉树的根节点root又是私有的,这就致使咱们没有办法使用 BinaryLinklist类的对象来调用它,因此咱们须要写一个公共的接口函数,也就是咱们上面这三个

虽然递归的方式简单易懂,可是递归消耗的空间和时间都比较多,因此咱们能够设计出另外一些算法来实现上面的三种遍历,那就是利用栈的思想

(1) 前序遍历-栈

template <class elemType>
void BinaryLinkList<elemType>::preOrderWithStack() const {
    //STL中的栈 
    stack<Node* > s;
    //工做指针,初始化指向根结点 
    Node *p = root;
    //栈非空或者p非空 
    while (!s.empty() || p) {
        if (p) {
            //访问当前节点 
            cout << p -> data << ' ';
            //指针压入栈 
            s.push();
            //工做指针指向左子树 
            p = p -> left;
        } else {
            //获取栈顶元素 
            p = s.top();
            //退栈 
            s.pop();
            //工做指针指向右子树 
            p = p -> right; 
        }
    } 
}

(2) 中序遍历-栈

template <class elemType>
void BinaryLinkList<elemType>::inOrderWithStack() const {
    //STL中的栈 
    stack<Node* > s;
    //工做指针,初始化指向根结点 
    Node *p = root;
    //栈非空或者p非空 
    while (!s.empty() || p) {
        if (p) {
            //指针压入栈 
            s.push();
            //工做指针指向左子树 
            p = p -> left;
        } else {
            //获取栈顶元素 
            p = s.top();
            //退栈 
            s.pop();
            //访问当前节点 
            cout << p -> data << ' ';
            //工做指针指向右子树 
            p = p -> right; 
        }
    } 
}

(3) 后序遍历-栈

后序遍历略微特殊,在其中设置了Left 和 Right 两个标记,用来区分栈顶弹出的结点是从栈顶结点的左子树返回的仍是右子树返回的

template <class elemType>
void BinaryLinkList<elemType>::postOrderWithStack() const {
    //定义标记 
    enum ChildType {Left,Right};
    //栈中元素类型
    struct StackElem {
        Node *pointer;
        ChildType flag;
    }; 
    StackElem elem;
    //STL中的栈 
    stack<StackElem> S;
    //工做指针,初始化指向根结点 
    Node *p = root;
    while (!S.empty() || p) {
        while (p != NULL) {
            elem.pointer = p;
            elem.flag = Left;
            S.push(elem);
            p = p -> left;
        }
        elem = S.top();
        S.pop();
        p = elem.pointer;   
        //已经遍历完左子树 
        if (elem.flag == Left) {
            elem.flag = Right;
            S.push(elem);
            p = p -> right;
            //已经遍历完右子树 
        } else { 
            cout << p -> data << ' ';
            p = NULL;
        }
    }
}

用栈的方式来实现这三种遍历,确实没有递归方式容易理解,学习这部分时能够对照一个简单的图,来思考,能够帮助你更好认识代码,能够参考上面个人举例图

(二) 广度优先遍历

广度优先遍历,又叫作宽度优先遍历,或者层序遍历,思想就是,从根节点开始访问,从上而下逐层遍历,在同一层中,按照从左到右的顺序对结点逐个访问

咱们可使用队列的思想来完成这样一种遍历方式

  • 初始化队列,根节点入队
  • 队列非空,循环执行下面三步,不然结束
  • 出队一个节点,同时访问该节点
  • 若该节点左子树非空,则将其左子树入队
  • 若该节点右子树非空,则将其右子树入队
template <class elemType>
void BinaryLinkList<elemType>::levelOrderTeaverse() const {
    queue<Node* > que;
    Node *p = root;
    if (p) que.push(p);
    while (!que.empty()) {
        //取队首元素 
        p = que.front();
        //出队 
        que.pop();
        //访问当前节点 
        cout << p -> data << ' ';
        //左子树入队
        if (p -> left != NULL)
            que.push(p -> left);
        //右子树入队 
        if (p -> right != NULL)
            que.push(p -> rigth);
    }
}

二叉树的其余常见运算

(一) 求节点总数

template <class elemType>
int BinaryLinkList<elemType>::size(Node *t) const {
    if (t == NULL)
        return 0;
    return 1 + size(t -> left) + size(t -> right);
}

(二) 求二叉树高度

template <class elemType>
int BinaryLinkList<elemType>::height(Node *t) const {
    if (t == NULL) 
        return 0;
    else {
        int lh = height(t -> left);
        int rh = height(t -> right);
        return 1 + ((lh > rh) ? lh : rh);
    }
}

(三) 求叶结点个数

template <class elemType>
int BinaryLinkList<elemType>::leafNum(Node *t) const {
    if (t == NULL)
        return 0;
    else if ((t -> left == NULL) && (t -> right == NULL))
        return 1;
    else
        return leafNum(t -> left) + leafNum(t -> right);
}

(四) 清空

template <class elemType>
void BinaryLinkList<elemType>::clear(Node *t) {
    if (t -> left)
        clear(t -> left);
    if (t -> right)
        clear(t -> right); 
    delete t;
}

结尾:

若是文章中有什么不足,或者错误的地方,欢迎你们留言分享想法,感谢朋友们的支持!

若是能帮到你的话,那就来关注我吧!若是您更喜欢微信文章的阅读方式,能够关注个人公众号

在这里的咱们素不相识,却都在为了本身的梦而努力 ❤

一个坚持推送原创开发技术文章的公众号:理想二旬不止

相关文章
相关标签/搜索