二叉树

来源:互联网 发布:淘宝转化率1.39 编辑:程序博客网 时间:2024/06/06 02:24
二叉树(Binary Tree)是每个节点最多有两个子树的树结构,常被用于实现二叉查找树和二叉堆。通常子树被称作“左子树”(left subtree)和“右子树”(right subtree)。本文主要介绍了二叉树的类型与表示、遍历、复制以及线索二叉树的相关内容。

二叉树的类型

  1. 完全二叉树(Complete Binary Tree):在一棵二叉树中,除最后一层外,若其馀层都是满的,并且最后一层或者是满的,或者是在右边缺少连续若干节点,则此二叉树为完全二叉树。具有n个节点的完全二叉树的深度为log2n+1,深度为k的完全二叉树,至少有2k-1个节点,至多有2k-1个节点.
  2. 满二叉树(Full Binary Tree): 一棵深度为k,且有2kk-1个节点的二叉树。
  3. 二叉查找树(Binary Search Tree),也称二叉搜索树、有序二叉树(ordered binary tree),排序二叉树(sorted binary tree),是指一棵空树或者具有下列性质的二叉树:
    1. 若任意节点的左子树不空,则左子树上所有结点的值均小于它的根结点的值;
    2. 任意节点的右子树不空,则右子树上所有结点的值均大于它的根结点的值;
    3. 任意节点的左、右子树也分别为二叉查找树;
    4. 没有键值相等的节点(no duplicate nodes)。

    二叉查找树相比于其他数据结构的优势在于查找、插入的时间复杂度为O(log n)。二叉查找树是基础性数据结构,可用于构建更为抽象的数据结构,如集合、multiset、关联数组等。

  4. 平衡二叉树(AVL树): 平衡二叉树是一棵空树或它的左右两个子树的高度差的绝对值不超过1,并且左右两个子树都是一棵平衡二叉树;查找、插入和删除在平均和最坏情况下都是O(log n),增加和删除可能需要通过一次或多次树旋转来重新平衡这个树。构造与调整方法 平衡二叉树的常用算法有红黑树、AVL、Treap、伸展树、SBT等。

二叉树的表示

存储方式

  • 连续存储:二叉树可以用数组或线性表来存储,而且如果这是满二叉树,这种方法不会浪费空间。用这种紧凑排列,如果一个结点的索引为i,它的子结点能在索引2i+1和2i+2找到,并且它的父节点(如果有)能在索引\lfloor(i-1)/2\rfloor找到(假设根节点的索引为0)。这种方法更有利于紧凑存储和更好的访问的局部性,特别是在前序遍历中。然而,它需要连续的存储空间,这样在存储高度为h的n个结点组成的一般普通树时将会浪费很多空间。一种最极坏的情况下如果深度为h的二叉树每个节点只有右孩子需要占用2的h次幂减1,而实际却只有h个结点,空间的浪费太大,这是顺序存储结构的一大缺点。
  • 链式存储:在使用记录或内存地址指针的编程语言中,二叉树通常用树结点结构来存储。有时也包含指向唯一的父节点的指针。如果一个结点的子结点个数小于2,一些子结点指针可能为空值,或者为特殊的哨兵结点。 使用链表能避免顺序储存浪费空间的问题,算法和结构相对简单,但使用二叉链表,由于缺乏父链的指引,在找回父节点时需要重新扫描树得知父节点的节点地址。
    struct TreeNode {    int val;    TreeNode *left;    TreeNode *right;    TreeNode(int x) : val(x), left(NULL), right(NULL) {}};

二叉树的枚举

n个节点的二叉树的种类为Catalan数.

二叉树的遍历

二叉树的先序遍历(Preorder Traversal)、中序遍历(Inorder Traversal)、后序遍历(Postorder Traversal)主要有一下三种方式:

  • 可以通过很容易使用递归来实现,而不借助任何辅助数据结构。
  • 主要通过栈或者队列来迭代实现二叉树的遍历
  • 二叉树的Morris遍历可不使用用栈,在常量空间O(1)、线性时间O(n)内实现二叉树的前中后序遍历,且遍历后不破坏二叉树的形状(中间过程允许改变其形状);详细介绍可参考 Morris遍历(Morris Traversal)。

中序遍历(Inorder Traversal)

下面左边代码显示的是用Morris方法中序遍历二叉树,而如下右边代码所示二叉树的中序遍历常规方法通常需要用栈来辅助。

vector inorderTraversal(TreeNode *root) {    vector order;    for(TreeNode *now=root,*tmp;now;) {        if(now->left==NULL){            order.push_back(now->val);            now=now->right;        }else{            tmp=now->left;            while(tmp->right!=NULL&&tmp->right!=now)                tmp=tmp->right;            if(tmp->right) {                order.push_back(now->val);                tmp->right=NULL;                now=now->right;            }else{                tmp->right=now;                now=now->left;            }        }    }    return order;}
vector inorderTraversal(TreeNode *root) {vector order;stack<TreeNode*> S;for(TreeNode *now=root;now;) {for(;now->left;now=now->left)S.push(now);do {order.push_back(now->val);if(now->right){now=now->right;break;}if(S.empty())return order;now=S.top();S.pop();} while(true);}return order;}

先序遍历(Preorder Traversal)

下面左边代码显示的是用Morris方法先序遍历二叉树,而如下右边代码所示二叉树的先序遍历常规方法通常需要用栈来辅助。

vector preorderTraversal(TreeNode *root) {    vector order;    for(TreeNode *now=root,*tmp;now;) {        if(now->left==NULL){            order.push_back(now->val);            now=now->right;        }else{            tmp=now->left            while(tmp->right!=NULL&&tmp->right!=now)                tmp=tmp->right;            if(tmp->right) {                tmp->right=NULL;                now=now->right;            }else{                order.push_back(now->val);                tmp->right=now;                now=now->left;            }        }    }    return order;}
vector preorderTraversal(TreeNode *root) {vector order;stack<TreeNode*> S;for(TreeNode *now=root;now;) {order.push_back(now->val);if(now->right)S.push(now->right);if(now->left){now=now->left;continue;}if(S.empty())return order;now=S.top(),S.pop();}return order;}

后序遍历(Postorder Traversal)

二叉树的后序遍历比先序和中序遍历要复杂一点,可以通过队列来辅助实现。

vector<int> postorderTraversal(TreeNode *root) {deque<TreeNode*> dq;deque<int> order;TreeNode* now;if(!root)return vector<int>();dq.push_back(root);while(!dq.empty()) {now=dq.front();dq.pop_front();order.push_front(now->val);if(now->left)dq.push_front(now->left);if(now->right)dq.push_front(now->right);}return vector<int>(order.begin(),order.end());}

层次遍历(Level Order Traversal)

二叉树的层次遍历也需要用队列来辅助。

vector<vector<int> > levelOrder(TreeNode *root) {deque<TreeNode*> Q;vector<vector<int> > res;if(!root)return res;vector<int> *V=new vector<int>();for(TreeNode* rightest=root;true;root=Q.front(),Q.pop_front()) {V->push_back(root->val);if(root->left)Q.push_back(root->left);if(root->right)Q.push_back(root->right);if(rightest==root) {res.push_back(*V);V=new vector<int>();rightest=rightest->right;if(rightest==NULL) {if(Q.empty())return res;else rightest=Q.back();}}}return res;}

深度优先遍历(Depth First Search)

和图的深度优先搜索不同的是,不需记住访问过的每一个结点,因为树中不会有环。先序、中序和后序遍历都是深度优先遍历的特例。

广度优先遍历(Breadth First Search)

二叉树的广度优先遍历又称按层次遍历。算法借助队列实现。

线索二叉树(Threaded Binary Tree)

可以发现二叉树的表示中有部分结点为NULL,A.J. Perlis和C. Thornton提出了线索树来利用这些空间来帮助我们遍历二叉树。线索二叉树中所有应该为空的右孩子指针指向该节点在中序遍历中的后继,所有应该为空的左孩子指针指向该节点的中序遍历的前驱。因此线索二叉树能线性地中序遍历二叉树,且可以方便的找到一个节点的父节点,这比显式地使用父亲节点指针或者栈效率更高。这在栈空间有限,或者无法使用存储父节点的栈时很有作用(对于通过深度优先搜索来查找父节点而言)。线索二叉树
考虑这样的例子:一个节点k有一个右孩子r,那么r的左指针可能是指向一个孩子节点,或是一个指回k的线索。如果r有左孩子,这个左孩子同样也应该有一个左孩子或是指回k的线索。对于所有的左孩子同理。因此沿着这些从r发出的左指针,我们最终会找到一个指回k的线索。这种特性是对称的:当q是p的左孩子时,我们可以沿着q的右孩子找到一个指回p的线索。
那么在有N个节点的二叉树中需要利用N+1个空指针添加线索,因为在N个节点的二叉树中,每个节点有2个指针,所以一共有2N个指针,除了根节点以外每一个节点都有一个指针从它的父节点指向它,所以一共使用了N-1个指针。所以剩下2N-(N-1)=N+1个空指针。
为了区分线索与树枝,需要在二叉树的表示中添加两个bit的标识位LTag和RTag来进行区分。同时在为线索二叉树添加结点时需要做一点特殊处理。

0 0
原创粉丝点击