7.4 7.5 二叉树的基本操作与遍历

来源:互联网 发布:公司淘宝客服规则大全 编辑:程序博客网 时间:2024/05/28 18:45

二叉树类定义

这里的二叉树使用了典型的二叉链实现方法:分为左右两个孩子链。这里和之前的链表实现一样,使用了类内定义的结点结构体BinaryTreeNode。考虑到插入和删除对于位置的要求比较复杂,因此在此处的二叉树实现中并没有包含(在二叉搜索树中,插入和删除是一个重点过程);同时考虑到这是一个原型二叉树,因此也没有使用泛型,对于树的结点的值的类型默认定义为字符串类型。事实上,对于建立二叉树的构造函数,通过字符串类型和一个转换函数,不难将其实现为通用的树。
这里与前面的一些数据结构实现不同的地方在于,定义了一些私有的辅助函数,用来实现公有方法中的递归算法。这样对于共有接口无需特殊指定递归初值,用起来更自然一些。
另外,这里也大量使用了STL。运用成熟的标准库能够大量减少代码量,并且内置的标准库的安全性、边界容错性都是可以保证的。实际上在学习数据结构时,只要学过了的东西,标准库中对应的都是可以直接拿来使用的。

/*** @brief 二叉树类。为了简化定义,数据类型为字符串类型。*/class BinaryTree{public:    /**    * @brief 二叉树结点定义    */    struct BinaryTreeNode{        BinaryTreeNode * left;  //左孩子        BinaryTreeNode * right; //右孩子        std::string value;     //值        BinaryTreeNode(std::string v = ""){            value = v;            left = right = nullptr;        }    };private:    BinaryTreeNode * _root;     //二叉树根节点    /**    * @brief 销毁二叉树    * @param BinaryTreeNode * root 需要销毁的二叉树树根    */    void destory(BinaryTreeNode * root);    /**    * @brief 转化为字符串的辅助递归函数    * @param BinaryTreeNode * root 需要访问的结点    * @param std::string & s 返回的字符串    */    void to_string_sub(BinaryTreeNode * root, std::string & s);    /**    * @brief 求树的深度的辅助递归子函数    * @param BinaryTreeNode * root 子树树根    * @return int 当前以root为根的子树的高度    */    const BinaryTreeNode * find_sub(BinaryTreeNode * root, const std::string & v);    int depth_sub(BinaryTreeNode * root);    /**    * @brief 求树的宽度的辅助递归子函数    * @param BinaryTreeNode * root 子树树根    * @param int level 当前root所处的层次    * @param std::vector<int> & level_width 每一层的宽度数组    */    void width_sub(BinaryTreeNode * root, int level, std::vector<int> & level_width);    /**    * @brief 中序遍历递归辅助函数    * @param BinaryTreeNode * root 子树树根    * @param std::string & str 输出字符串    */    void inorder_traversal_sub(BinaryTreeNode * root, std::string & str);public:    /**    * @brief 构造函数。根据括号表示法的字符串序列创建一棵新的二叉树。    * @param const std::string & str 需要被创建的用括号表示法表示的二叉树    */    BinaryTree(const std::string & str);    /**    * @brief 构造函数。创造一棵空的二叉树。    */    BinaryTree();    /**    * @brief 析构函数    */    ~BinaryTree();    /**    * @brief 根据当前的二叉树生成字符串。    * @return std::string 返回当前二叉树的括号表示法字符串    */    std::string to_string();    /**    * @brief 查找指定值的结点    * @param std::string & v 需要查找的值    * @return const BinaryTreeNode * 返回的找到的结点指针。未找到则返回nullptr    */    const BinaryTreeNode * find(const std::string & v);    /**    * @brief 得到树的深度(高度)    * @return const int 树高    */    const int depth();    /**    * @brief 得到树的宽度    * @return const int 树的宽度    */    const int width();    /**    * @brief 返回二叉树的中序遍历序列    * @return std::string 二叉树的中序遍历序列    */    std::string inorder_traversal();    /**    * @brief 返回二叉树的后序遍历序列    * @return std::string 二叉树的后序遍历序列    */    std::string postorder_traversal();    /**    * @brief 返回二叉树的先序遍历序列    * @return std::string 二叉树的先序遍历序列    */    std::string preorder_traversal();    /**    * @brief 返回二叉树的层次遍历序列    * @return std::string 二叉树的层次遍历序列    */    std::string level_traversal();};

基本操作的实现

构造函数

实现根据二叉树的括号表示建立二叉树还是比较复杂的。首先我们可能会考虑用递归的方法实现这个函数。我们以二叉树A(B(C,D),E(,F(G)))来为例。很明显从左至右A为树根,而紧接着A后面的括号和最右边的一个括号构成了A的两个孩子。事实上,很难在O(1)时间内分别求出A结点的左孩子和右孩子,也就是意味着不能将大问题化解为两个规模稍小的子问题。因此就不太容易借助递归的方法生成一棵二叉树。
那么不用递归的话,对于这种具有层次的数据结构,必然需要借助栈来实现。这样对于括号表示的二叉树,我们有如下四种情况需要考虑:
(a)读取到普通字符。我们将普通的字符加入当前结点的字符串current_string。同时需要判断当前字符的下一个字符是否为要求的特殊字符()、、,。如果是,就要创建结点,并把栈顶的左右孩子分别连接起来。
(b)读取到(,表示层次更深入了一层。这时候就需要将当前结点current进栈。同时把标志位flag设置为左孩子(二叉树的括号表示是从左到右表示的)。
(c)读取到),表示以栈顶元素为根节点的子树已经处理完毕了。这时候只需要将其弹出即可,恢复current为栈顶元素。
(d)读取到,,表示需要从左子树处理右子树,设置标志位flag为右孩子即可。
实际上,在(a)过程中增加一个条件判断是否应该新建节点,比较麻烦;但是省去了碰到标志字符新建节点时,左右子树判断的麻烦:那样的实现在栈中需要同时保存左右子树信息(类似于手工模拟递归)。
另外在程序的健壮性方面,增加了一系列断言语句判断输入的字符串是否合法。还有一点要说明的是,这里先将树根建立并放入了栈,这样能够保证第一次引用st.top()不会出现栈空的情况,也有利于配对括号的判断。

BinaryTree::BinaryTree(const std::string & str){    if (str.empty()){            //空串代表空树        _root = nullptr;        return;    }    stack<BinaryTreeNode *> st; //使用栈作为辅助                                //左右孩子常量定义。    const int left_child = 0;    const int right_child = 1;    BinaryTreeNode * current;   //表示当前创建的结点指针    std::string current_string; //当前结点的字符串值    int flag = left_child;    int i;    for (i = 0; i < str.length() && str[i] != '('; i++){        current_string.push_back(str[i]);    }    current = new BinaryTreeNode(current_string);    current_string.clear();    for (; i < str.length(); i++){        if (str[i] == '('){            flag = left_child;            st.push(current);        } else if (str[i] == ')'){            assert(!st.empty());    //检查括号配对情况            current = st.top();            st.pop();        } else if (str[i] == ','){            flag = right_child;        } else{            current_string.push_back(str[i]);            //判断下一个字符是否为当前小节的结束符            if (i < str.length() && (str[i + 1] == '(' || str[i + 1] == ')' || str[i + 1] == ',')){                current = new BinaryTreeNode(current_string);                if (flag == left_child){                    st.top()->left = current;                } else{                    st.top()->right = current;                }                current_string.clear();            }        }    }    assert(st.empty()); //检查括号配对情况    _root = current;}

生成括号表示法字串

对于已经建立的二叉树,要生成括号表示法,用递归很容易解决。上面提到了使用to_string_sub作为递归函数,字符串类型的引用参数s来存放转化后的结果。

void BinaryTree::to_string_sub(BinaryTreeNode * root, std::string & s){    s.append(root->value);    if (!root->left && !root->right)        return;    s.push_back('(');    if (root->left)        to_string_sub(root->left, s);    if (root->right){        s.push_back(',');        to_string_sub(root->right, s);    }    s.push_back(')');    return;}std::string BinaryTree::to_string(){    std::string result;    this->to_string_sub(this->_root, result);    return result;}

查找指定值的结点

同上面一样,依然是一个简洁的递归模型。首先判断当前结点的值是否满足,不满足则依次寻找左子树和右子树是否存在满足的结点。

const BinaryTree::BinaryTreeNode * BinaryTree::find(const std::string & v){    return this->find_sub(_root, v);}const BinaryTree::BinaryTreeNode * BinaryTree::find_sub(BinaryTreeNode * root, const std::string & v){    if (!root)        return nullptr;    if (root->value == v)        return root;    const BinaryTreeNode * leftnode = this->find_sub(root->left, v);    return leftnode?leftnode:this->find_sub(root->right, v);}

求树高

在之前的递归练习题中已经说明了求解思路了:利用二叉树的递归定义特点写出公式h(x)=max{h(L(x)),h(R(x))}+1。其中L(x)R(x)分别表示x的左孩子结点和右孩子结点。

int BinaryTree::depth_sub(BinaryTreeNode * root){    if (!root)        return 0;    return std::max(depth_sub(root->left), depth_sub(root->right)) + 1;}const int BinaryTree::depth(){    return this->depth_sub(_root);}

求树的宽度

求树的宽度,这里依然使用递归的思想去做。在递归时每扫描一个结点,就把当前结点所处的层次的结点个数加一。最后对所有层次的结点数取一个最大值就可以了。max_element就是返回在一个迭代器指定的区间内的最大值,返回的也是迭代器,需要使用*操作符得到具体的值。

void BinaryTree::width_sub(BinaryTreeNode * root, int level, vector<int> & level_width){    if (!root)        return;    if (level < level_width.size()){   //判断是否达到新的一层        level_width[level]++;    } else{        level_width.push_back(1);    }    //分别处理左右子树    this->width_sub(root->left, level + 1, level_width);    this->width_sub(root->right, level + 1, level_width);    return;}const int BinaryTree::width(){    vector<int> levels;    this->width_sub(_root, 0, levels);    return *max_element(levels.begin(), levels.end());  //返回最大值即为树的宽度}

树的销毁

销毁树和前面的操作没有太大区别,就是需要先保留根指针,通过根寻找左右子树,销毁左右子树以后才能销毁根。

void BinaryTree::destory(BinaryTreeNode * root){    if (!root)        return;    destory(root->left);    destory(root->right);    delete root;    return;}BinaryTree::~BinaryTree(){    destory(_root);}

树的遍历

因为树是非线性结构,因此其遍历也不想线性表的遍历那么简单。总的来说,分为广度遍历(又叫层次遍历)和深度遍历。而对于二叉树深度遍历,又可以分为先序遍历,中序遍历和后序遍历。
对于广度遍历来说,在前面的队列中也介绍过,需要借助队列来进行辅助操作。

std::string BinaryTree::level_traversal(){    std::string result;    std::queue<BinaryTreeNode *> q; //使用队列作为层次遍历的辅助容器    q.push(_root);    while(!q.empty()){        result.append(q.front()->value);        result.push_back(' ');        if(q.front()->left)            q.push(q.front()->left);        if(q.front()->right)            q.push(q.front()->right);        q.pop();    }    return result;}

对于深度遍历,先序序列就是“根左右”,中序序列就是“左根右”,后序序列就是“左右根”。
下面分别给出中序遍历:

void BinaryTree::inorder_traversal_sub(BinaryTreeNode * root, std::string & str){    if(!root)        return;    this->inorder_traversal_sub(root->left, str);    str.push_back(' ');    str.append(root->value);    str.push_back(' ');    this->inorder_traversal_sub(root->right, str);    return;}std::string BinaryTree::inorder_traversal(){    std::string result;    this->inorder_traversal_sub(_root, result);    return result;}

其他的两种遍历方法较为类似。
对于遍历的非递归化,依然是要借助栈来辅助。以中序遍历为例,其访问顺序是先左子树,再访问树根,最后访问右子树。因此,栈中存放的都是未访问的结点。在整个栈不空的条件下,首先需要进入左子树的最里层,访问最左边的叶子结点后,将其弹出,然后开始处理其右子树。

std::string BinaryTree::inorder_traversal(){    std::string result;    std::stack<BinaryTreeNode *> st;    BinaryTreeNode * p = _root;    while(!st.empty() || p != nullptr){        //先寻找最左结点(可能包含右子树)        while(p != nullptr){            st.push(p);            p = p->left;        }        if(!st.empty()){            //弹出并访问最左边结点            p = st.top();            st.pop();            result.append(p->value);            result.push_back(' ');            //将其右子树进栈,准备下一轮处理。            //对于其右子树的处理,依然从最左边开始            //遵循“左根右”的顺序。            p = p->right;        }    }    return result;}

测试代码

int main(){    string s = "Von.Neuuman(Turing(,Floyd(Donald.E.Knuth)),Dijkstra(Prim,Yann Lecun(,Andrew Y. Ng)))";    string dest = "Dijkstra";    //cin >> s;    BinaryTree bt(s);    //output the tree    cout << "(1)BinaryTree: " << bt.to_string() << endl;    auto hnode = bt.find(dest);    cout << "(2)the lchild and rchild node of \"Dijkstra\" node: " << hnode->left->value << "," << hnode->right->value << endl;    cout << "(3)the depth of BinaryTree: " << bt.depth() << endl;    cout << "(4)the width of BinaryTree: " << bt.width() << endl;    cout << "(5)inorder traversal: " << bt.inorder_traversal() << endl;    cout << "(6)level traversal: " << bt.level_traversal() << endl;    cout << "(7)Destroy BinaryTree.";    return 0;}

输出

(1)BinaryTree: Von.Neuuman(Turing(,Floyd(Donald.E.Knuth)),Dijkstra(Prim,Yann Lecun(,Andrew Y. Ng)))(2)the lchild and rchild node of "Dijkstra" node: Prim,Yann Lecun(3)the depth of BinaryTree: 4(4)the width of BinaryTree: 3(5)inorder traversal:  Turing  Donald.E.Knuth  Floyd  Von.Neuuman  Prim  Dijkstra  Yann Lecun  Andrew Y. Ng(6)level traversal: Von.Neuuman Turing Dijkstra Floyd Prim Yann Lecun Donald.E.Knuth Andrew Y. Ng(7)Destroy BinaryTree.
原创粉丝点击