数据结构与算法A 第五章二叉树
二叉树的概念
二叉树的定义和基本术语
二叉树:二叉树由结点的有限集合构成这个有限集合或者为空集 (empty)或者为由一个根结点 (root) 及两棵互不相交、分别称作这个根的左子树 (left subtree) 和右子树 (right subtree) 的二叉树组成的集合
注意:每个结点至多两棵子树,且有左右之分,不能随意交换
二叉树的五种基本形态:二叉树可以为空,结点的左、右子树也可空
- 空
- 独根
- 空左
- 空右
- 左右都不空
根:引出二叉树结点的起始结点
父结点:任何非根结点的前驱结点
子结点:左子结点(左孩子,左子女)。右子结点(右孩子,右子女)
兄弟结点:具有相同的父结点
叶结点:没有子结点的结点
度:一个结点子树数目
内部结点(分支结点):除叶结点外非终端结点
边:父结点k与子结点k’之间的有向连线<k,k’>
路径:从结点k0到结点ks存在一些边相连
满二叉树、完全二叉树、扩充二叉树
满二叉树:一棵二叉树的任何结点,或是树叶,或者左右结点均非空(没有度为1的情况)
完全二叉树:一棵二叉树最多只有最下面的两层结点度数可以<2,并且最下面一层的结点都集中在该层最左边的连续位置上(其叶结点只可能在层次最大的两层出现)
完全二叉树中由根结点到各个节点的路径长度总和在具有同样结点个数的二叉树中达到了最小
扩充二叉树:在二叉树中出现空子树的位置增加空树叶(度数为1的分支下面增加空树叶,度为0的分支下增加两个空树叶)
扩充二叉树是满二叉树,新增空树叶(外部结点)的个数等于原二叉树的结点个数(内部结点)+1
扩充二叉树外部路径长度 E 和内部路径长度 I 满足:E = I + 2n (n 是内部结点个数)
二叉树的主要性质
在二叉树中,第i层上最多有2^i哥结点(i >= 0)
深度为k的二叉树至多有2^(k+1) - 1个结点
任何一个二叉树,若其终端结点数为n0(度为0的结点),度为2的结点数为n2,则n0 = n2 + 1
满二叉树定理:非空满二叉树树叶数目等于其分支结点数+1
满二叉树定理推论:一个非空二叉树的空子树数目等于其节点数+1
n个结点的完全二叉树的高度为[log2(n+1)],深度为[log2(n+1)]-1,其中高度定义为二叉树中层数最大的叶结点层数+1
对于具有n个结点的完全二叉树,有
- 如果i=0.结点i是二叉树的根结点;若i>0则其父结点编号为[(i-1)/2]
- 当2i+1<=n-1时,结点i的左子结点是2i+1,否则i没有左子结点;当2i+2<=n-1时,结点i的右子结点是2i+2,否则i没有右子结点;
- i为偶数且0<i<n时,结点i的左兄弟结点是结点i-1,否则没有左兄弟结点;i为奇数且i+1<n时,结点i的右兄弟结点是结点i+1,否则没有左兄弟结点
二叉树的周游
二叉树的ADT
- 对二叉树的操作和运算主要在访问二叉树的结点信息上
1 | // 二叉树结点的ADT |
1 | template <class T> class BinaryTree { |
二叉树的周游(遍历):按照一定顺序依次访问(操作)树中的所有结点
二叉树是一种非线性结构,每个结点可以有一个以上的直接后继,因袭需要把二叉树中的结点转化为顺序结构才能周游整个二叉树。周游二叉树的过程实际上就是把二叉树的结点放入一个线性序列的过程,即对二叉树进行线性化
线性化的方法:
- 深度优先周游
- 广度优先周游
深度优先周游二叉树
深度优先周游算法的思想:尽可能地沿分支结点向深度方向进行周游。先沿着分支结点向左下降,当左子树为空的时候,返回分支结点再转向右结点
二叉树的基本单元组成:根结点,左子树,右子树。用LtR分别表示周游左子树,访问当前结点,周游右子树
深度优先周游二叉树的分类:
- 前序遍历 (tLR次序, preorder traversal):
- 访问根结点
- 按前序周游左子树
- 按前序周游右子树
- 中序遍历 (LtR次序, inorder traversal),也称对称序
- 按中序周游左子树
- 访问根节点
- 按中序周游右子树
- 后序遍历 (LRt次序, postorder traversal)
- 按后序周游左子树
- 按后序周游右子树
- 访问根节点
- 前序遍历 (tLR次序, preorder traversal):
- 二叉树的前序(中序、后序)序列:前序(中序、后序)周游二叉树得到二叉树结点的一个有序序列
递归算法实现二叉树的深度优先周游
1 | template<class T> |
非递归算法实现二叉树的深度优先周游
关键:设置一个栈,记录尚待遍历的结点,以备后续访问
非递归前序遍历二叉树的思想:
- 遇到一个结点,访问之,并将其非空右子结点压栈;
- 下降去前序遍历其左子树;
- 按前序方式遍历完左子树后,从栈顶弹出一个结点,访问之,并继续按前序方式遍历其右子树
- 最开始压入一个空指针作为监视哨,当这个空指针被弹出来,周游结束
1 | // 非递归前序遍历二叉树 |
- 非递归中序遍历二叉树思想:
- 遇到一个结点,将其压栈后,遍历其左子树
- 遍历完左子树后,从栈顶弹出该结点并访问之
- 然后下降到其右子树再去遍历其右子树
1 | template<class T> void |
- 非递归后序遍历二叉树思想:
- 遇到一个结点,将其压栈,遍历其左子树
- 遍历结束后,尚不能马上访问处于栈顶的该结点,须向右下降去遍历其右子树
- 遍历完右子树后方可从访问栈顶元素并弹出4
- 栈中的元素增加一个特征位,以区别栈顶弹出结点时是从其左子树(仍需遍历右子树),还是从右子树返回。L 表示进入左子树并从其返回。R 表示进入右子树并从其返回
1 | enum Tags{Left,Right}; // 定义枚举类型标志位 |
深度优先遍历二叉树时间代价 : 遍历具有 n 个结点的二叉树 需 O(n)时间
深度优先遍历二叉树空间代价 : 遍历过程中栈的最大容量,与树的高度成正比
- 最坏情况:高度为 n,所需空间复杂度为O(n)
- 最好情况:O(1)
- 平均情况:O(log n)
广度优先周游二叉树
过程:从二叉树的第0层(根结点)开始,自上至下逐层遍历;在同一层中,按照从左到右的顺序对结点逐一访问
需要使用队列作为辅助的存储结构。在周游开始的时候,首先将根结点放入队列中;然后每次从队列中取出队头元素进行处理,每处理一个结点时,按从左至右的顺序把它所有子结点放入队列
1 | void BinaryTree<T>::LevelOrder(BinaryTreeNode<T>* root) |
广度优先遍历二叉树时间代价为 O(n)
广度优先遍历二叉树空间代价与树的最大宽度相关
- 最佳 O(1)
- 最坏 O(n)
二叉树的存储结构
二叉树的链式存储结构(用指针实现)
二叉树是非线性结构,二叉树的各结点随机地存储在内存空间中,结点之间的逻辑关系用指针来链接
对于每一个节点,除了存储自身的信息外,还要设置两个指向左右子结点的指针。结点包含三个域:数据域和左、右指针域
二叉链表:有两个指针域的结点结构
1 | // BinaryTreeNode类中增加两个私有数据成员实现二叉链表 |
- 三叉链表:有三个指针域的结点结构,提供“向上”访问的能力,容易查找某个结点的父结点
- 递归框架寻找父结点——注意返回
1 | template<class T> |
- 非递归框架找父结点
1 | BinaryTreeNode<T>* BinaryTree<T>::Parent(BinaryTreeNode<T> *current) |
- 穿线二叉树:将空指针穿起来
- 中序线索化二叉树:递归实现
1 | template <class T> |
穿线二叉树空间代价分析
- 总空间 (2p + d)n
- 结构性开销 2pn
链式存储特点
- 优点:运算方便,通过指针可直接访问相关结点
- 缺点:根据满二叉树定理:一半的指针是空的。空指针 n+1,存储密度低。结构性开销较大
完全二叉树的顺序存储结构
二叉树的顺序存储:按照一定次序,用一组地址连续的存储单元存储二叉树上的各个结点
完全二叉树的层次序列足以反映其结构。完全二叉树的顺序存储,存储结构上是线性的,但依然完整反映了二叉树的逻辑结构
按广度优先周游二叉树形成顺序存储,自上至下,自左至右的对所有结点进行编码,根据结点的存储地址可计算其左、右子结点、父结点、兄弟结点的存储地址
顺序结构数组的下标仍能满足完全二叉树的性质
二叉搜索树
二叉搜索树(BST)的属性:
- 或为一棵空树
- 或任何一个检索码为 K 的结点满足
- 其左子树(若非空)任一结点的检索码均小于 K
- 其右子树(若非空)任一结点的检索码均大于或等于 K
- 其左、右子树分别为二叉搜索树
二叉搜索树的特点:中序遍历是正序的(由小到大的排列)
二叉搜索树的检索
BST的检索:从根结点开始,在二叉搜索树中查找检索码为 K 的结点。若根结点检索码为 K,则检索成功并结束
- 若 K 小于根结点的值,则只需检索左子树
- 若 K 大于根结点的值,只检索右子树
- 如此,一直持续到 K 被找到或遇上叶结点仍没发现 K ,则检索失败
检索代价:只需检索两个子树之一:每比较1次,待检子树的高度降1,代价为O(h) 树高 h
二叉搜索树的插入
- BST的插入:需要保持BST的性质和性能(中序遍历有序)
- 从根结点开始,若根结点为空,则将 K 结点作为根插入,操作结束
- 若 K 小于根结点的值,将其插入左子树
- 若 K 大于根结点的值,将其插入右子树
- 直到叶结点,确定插入位置,把待插入节点作为一个新叶结点插入到二叉树搜索树中
1 | // 二叉搜索树的结点插入算法 |
- 时间复杂度与根到插入位置的路径长度相关,O(h)
二叉搜索树的删除
BST上的删除:删除一个结点后仍能保持BST性质,且树高变化较小
改进的BTS删除算法思想:
- 若结点pointer没有左子树,则用pointer右子树的根代替被删除节点pointer
- 若结点有左子树,则在左子树中找到按中序周游的最后一个节点temppointer(即左子树最大结点)并将其从二叉树中删去。由于temppointer没有右子树,只需用左子树代替temppointer,然后用temppointer代替删除的结点pointer
1 | template <class T> |
- 树高h,时间代价O(h)
堆与优先队列
- 如果在一组对象中查找最大值或最小值,堆能提供较高的效率
堆的定义及其实现
堆:一个关键码序列{K0, K1, …, Kn-1} 若具有下述特性:
- Ki ≤ K2i+1 / Ki ≥ K2i+1
- Ki ≤ K2i+2 / Ki ≥ K2i+2
最小值堆(min-heap):任一结点的值小于或等于其子结点的值,根结点存储着树中所有结点的最小值,是特殊的完全二叉树
堆序只是局部有序,只满足内部结点的值不大于其左右子结点,但是一个结点与兄弟结点之间没有必然联系
堆是不唯一的
建堆
时间复杂度O(n)
将一棵完全二叉树调整成堆,假设根的左、右子树都已是堆,且根为T,思路:
- T 的值小于或等于其两个子结点的值,此时为堆,无须调整
- T 的值大于两个子结点至少一个的值,此时 T 应与两个子结点中较小者交换,若 T 仍大于其新的子结点,需 将 T 继续往下 “筛”(siftdown)
- 直至到某一个层使 T 不再大于其子结点,或已为叶结点
筛选法建堆:
- 将 n 个关键码组织到一维数组中
- 以叶结点为根的子树都已满足堆的特性,亦即,当 i ≥ [n/2] 时,以关键码 Ki 为根的子树已为堆
- 最后一个分支结点 i = [n/2] - 1 开始,采用 siftdown 从右向左、自底向上将以各个分支结点为根的子树调整成堆,直到树根为止,调整方式如“思路”
堆的ADT
1 | template <class T> |
- 对最小堆用筛选法 SiftDown 调整,时间复杂度O(logn)
1 | template <class T> |
- 对最小堆用筛选法 SiftUp 向上调整
1 | template<class T> |
- 建最小堆
1 | template<class T> |
堆的插入算法
- 思路:
- 插在堆的最后
- 与其父结点比较,若不满足堆的性质则往上“拉” (SiftUp)
- 逐步向上,直到与其父结点满足堆的性质
1 | template <class T> |
- 时间复杂度O(logn)
堆的删除算法
思路:
- 删除某个位置的元素后将最末端的结点填入这个位置
- 末端元素需要与被删除位置的子结点比较交换,直到过滤到该结点的正确位置位置(siftdown)
最小堆的删除操作
1 | template<class T> |
- 删除根结点
1 | template <class T> T MinHeap<T>:: RemoveRoot() |
- 堆删除算法的时间代价O(logn)
优先队列
根据需要释放具有最小/大值的对象
改变已存储于优先队列中对象的优先权
Huffman树及其应用
Huffman树
一个具有 n 个外部结点的扩充二叉树,每个外部结点 di 有一个与之对应的权 wi,这个扩充二叉树的带权外部路径长度总和最短。
权越大的叶结点离根越近
建立Huffman树思路:
- 按照“权”(诸如,频率) 将字符组成有序序列
- 取走当前序列的前两个字符(“权”最小),将其标记为Huffman树的叶结点,并将这两个叶结点作为一个(新生成)分支结点的两个子结点,该分支结点的权为两叶结点的权之和
- 将所得子树的“权”放回序列中适当位置,保持“权”的有序性
- 重复上述步骤直至序列中只剩一个元素,则Huffman树建立完毕
Huffman编码
编码:采用一组无歧义的规则将字符集中每个字符编码为唯一可标识的代码串
- 定长编码:所有字符的编码长度均相同,编码一个具有 n 个字符的字符集最少需要 log2n 位
- 变长编码:根据字符出现频率编码,高频编码较短,低频字符编码较长
待编码的字符集D = {d0,…,dn-1} ,D中各个字符的出现频率为W = {w0,…,wn-1} ,对字符集D进行二进制编码,使得通信编码总长度最短Huffman编码过程如下:
- 用d0,…,dn-1作为外部结点构造具有最小带权外部路径长度的扩充二叉树
- 把每个结点引向其左子结点的边标0,引向其右子结点的边标1
3。 从根结点到每个叶结点路径上的编号连接起来就是这个外部节点所代表字符的编码
Huffman编码的空间效率:字符的平均编码长度
- 各字符的编码长度ci 乘以其出现概率 pi ,即: c0p0 + c1p1 + … + cn-1pn-1
- fi 表示第i个字符的出现频率,fT 表示所有字符出现的总次数(c0f0 + c1f1 + … + cn-1fn-1) / fT
第五章 二叉树 OJ作业
1:Huffman编码树
1 | /*描述 |
2:Pre-Post-erous!
1 | /*描述 |
3:由中根序列和后根序列重建二叉树
1 | /*描述 |