数据结构–二叉树
KEY:(不敢相信没有堆…)
- 二叉树的定义及其主要特征 ☑️
- 二叉树的顺序存储结构和链式存储结构实现
- 二叉树的遍历及应用
- 二叉排序(查找、检索)树 (BST)
- 平衡的二叉检索树- AVL 树
- 哈夫曼(Huffman)树和哈夫曼编码
参考教材
电子工业出版社 数据结构算法与分析 c++版 第三版 Clifford A.Shaffer
部分参考了清华大学严版教材c语言版
一、基本概念
1.1 定义
二叉树由结点的有限集合组成,这个集合或者为空, 或者由一个根结点及两棵不相交的,分别称作这个根的 左子树和右子树的二叉树组成。
1.2 特点
- 每个结点至多有二棵子树
- 二叉树的子树有左、右之分,且其次序不能任意颠倒
1.3 树相关术语
- 从一个结点到它的两个子结点都有边(edge)相连,这个结 点称为它的子结点的父结点(parent)。
- 路径的长度:边的数量
- 结点M的深度(depth)就是从根结点到M的路径的 长度。
- 树的高度(height)等于最深的结点的深度+1。任何深度为d的结点的层数(level)都为d。根结点深度为0,层数也为0。(严版根结点深度和层次为1 )
- n 没有非空子树的结点称为叶结点(leaf)或终端结点。至少有一个非空子树的结点称为分支结点或 内部结点(internal node)。
- 结点的度:树结点的子结点数。 n 叶节点的度等于0,内部节点的度>0.
1.4 二叉树的相关术语
1.4.1 满二叉树
1.4.1.1 定义
如果一棵二叉树的任何结点,要么是叶子节点,要么是刚还有两个非空子节点的分支节点。
1.4.1.2 性质
1. 非空满二叉树叶子结点数等于其分支节点数+1
首先二叉树的结点数n=叶结点数l+分支结点数b
因为每个分支结点,恰好有两个子结点,故一棵满二叉树有2*b条边,换一个角度看,每个结点都刚好有一条边连接其父结点,故有n-1条边
n
−
1
=
2
∗
b
=
l
+
b
−
1
⇒
l
=
b
+
1
n-1=2*b=l+b-1\\ \Rightarrow l=b+1
n−1=2∗b=l+b−1⇒l=b+1
2. 一棵非空二叉树 空子树的数目等于其结点数目加1
由性质1可知空子树,每个叶子结点有两棵空子树,所以,空子树的数目既叶子结点数*2
1.4.2 完全二叉树
1.4.2.1 定义
若一棵二叉树最多只有最下面的两层结点度数可以小于2,并且最下面一层的结点都集中在该 层最左边的若干位置上,则称此二叉树为完全二 叉树。
形状要求:自根结点起每一层从左至右地填充。一棵高度为d的完全二叉除了d-1层外,每一层都是满的。底层叶结点集中在左边的若干位置 上。
1.4.2.2 性质
- 具有 n 个结点的完全二叉树的高度为
h e i g h t = ⌊ l o g 2 n ⌋ + 1 height=\lfloor log_2n\rfloor +1 height=⌊log2n⌋+1
-
若对含 n 个结点的完全二叉树从上到下且从左至右进 行 1 至 n 的编号,则对完全二叉树中任意一个编号为 i 的结点:
- 若 i=1,则该结点是二叉树的根,无双亲, 否则,编号为 ⌊ i / 2 ⌋ \lfloor i/2 \rfloor ⌊i/2⌋的结点为其双亲结点;
-
若 2i>n,则该结点无左孩子, 否则,编号为 2i 的结点为其左孩子结点;
- 若 2i+1>n,则该结点无右孩子结点, 否则,编号为2i+1 的结点为其右孩子结点。
这个性质写出来只是为了方便编程把。。。。。
1.4 3 二叉树的性质
-
二叉树的第i层(根为第0层)最多有2i个结点
-
高度为k(深度为k-1。只有一个根结点的二叉 树的高度为1,深度为0)的二叉树至多有 2 k − 1 2^k-1 2k−1个 结点
-
任何一棵二叉树,度为0的结点比度为2的结点多 一个。
推导过程
假设二叉树度为0的结点有$ n_0 , 度 为 1 的 结 点 有 ,度为1的结点有 ,度为1的结点有n_1 度 为 2 的 有 度为2的有 度为2的有n_2 , 总 数 量 为 n , 则 ,总数量为n,则 ,总数量为n,则n=n_0+n_1+n_2$
则边数为: n 1 + 2 ∗ n 2 \\ n1 + 2*n2 n1+2∗n2
又 : 每个结点都与其父节点有唯一的一条边 除了根节点
所以 :边数又为: n-1
⇒ 2 ∗ n 2 = n 0 + n 2 − 1 ⇒ n 2 + 1 = n 0 \Rightarrow 2*n_2=n_0+n_2-1\\ \Rightarrow n_2+1=n_0 ⇒2∗n2=n0+n2−1⇒n2+1=n0
二、二叉树的实现
2.1 二叉树结点ADT
template <typename E>
//注意这只是二叉树的结点的ADT
class BinNode
{
public:
BinNode() {}
virtual ~BinNode() {}
virtual E &val()=0;//返回元素的值
virtual void setVal(const E&)=0;//设置该结点的元素值
virtual BinNode * left()const=0;//返回该节点左子树的指针
virtual BinNode * reight()const =0;
virtual void setLeft(BinNode *)=0;//设置其左子树的值
virtual void setRight(BinNode *)=0;
virtual bool isLeaf()=0;//判断本结点是否为叶子结点
};
2.2 遍历二叉树
2.2.0 总论
-
遍历:系统地访问二叉树中的结点。每个结点都 正好被访问到一次。
-
方法:
-
前序遍历(preorder traversal):DLR
-
中序遍历(inorder traversal):(LDR)
-
后序遍历(postorder traversal):(LRD)
-
层次遍历:从上到下,从左至右
层次遍历算法思想
- 首先创建一个队列;若二叉树非空, 将根放入队列; 2. 从队列取出一个元素并访问,如果该 元素有左孩子就将它放入队列,如果 该元素有右孩子就将它放入队列; 3. 若队列非空,继续第2步,直至队列为 空,则二叉树的层次遍历过程结束。
-
2.2.1 习题
一、给树写遍历
- 先序: - + a *b-c d / e f
- 中序:a + b * c - d e / f
- 后序:a b c d - * + e f . -
- 层次:- + / a * e f b - c d
二、给定中序+任意遍历可以确定还原唯一的一棵树
-
中序+先序:
先序:abcdefg 中序cbdaegf
先序是根左右,中序是左根右.
根据先序所以a是根节点,将中序遍历分为
第0层:a
第一层:左:cbd 右:egf
然后是b
b左结点为 c 右为d
同理e把e左子树分为空 右子树分为gf
然后是f 把f右子树分为左子树g
复杂一点的:已知一棵二叉树的先序序列:ABDGJEHCFIKL;中序序列:DJGBEHACKILF。画出二叉树的形态。
-
中序+后序
已知某二叉树的后序遍历序列是dabec,中序遍历序列 是debac
后序是左右根 所以根节点是c 所以中序遍历被分为
deba <- c-> /
然后是e d<-e->ba
然后是b b->a
-
中序+层次
层次: A B C D E F G
中序: B D A E G C F
第一轮:
第第二轮:
第三轮:最后结果:
2.2.2 应用
一、统计二叉树中叶子结点的个数(先序遍历)
算法思想:
二叉树的叶子结点数=左子树的叶子结点数+右子树的叶子结点数.所以只需要递归的调用函数,不断分解其根节点,左右子树.当遍历到空子树/叶子节点,子问题结束.
算法伪代码
template <typename E>
int BTNode<E>::leafNum(BinNode<E> *root)
{
if(root==NULL) return 0;
if(root->isLeaf()) return 1;
else return leafNum(root->left())+leafNum(root->right());
}
二、 求二叉树的高度(后序遍历)
算法思想
从二叉树高度的定义可知,二叉树的 高度应为其左、右子树高度的最大值加 1。 由此,需先分别求得左、右子树的高度, 算法中“访问结点”的操作为:求得左、 右子树高度的最大值,然后加 1 。最小子问题为当前节点为空
算法实现
int Depth(BinNode *T)
{
int dep=0;
if(!T) return 0;
else
{
int depthLeft=Depth(T->left());
int depthRight=Depth(T->right());
dep = 1+(depthLeft>depthRight?depthLeft:depthRight);
}
return dep;
}
三、 复制二叉树(后序遍历)
虽然老师上课没怎么说这里,但是为什么要用后序遍历呢?因为一般来说都是二叉链表实现二叉树,所以一般都需要先 生成好左右孩子,然后再建这个结点.
2.3 二叉树的存储结构
2.3.1顺序存储结构
一般来说 顺序存储结构只用于完全二叉树.因为在最差情况下,普通的二叉树,如果为单支树,需要 2 k − 1 2^k-1 2k−1大小的一维数组却只有k个结点.当然如果不在意这个也是可以存储的, 非完全二叉树在置空值而转换为完全二叉树存储
例如下图,存储结果是 CEDJFX//K/G/I/L
关于结点的计算公式:
公式中r表示结点的索引, n表示二叉树结点总数。
- Parent® = $ \lfloor (r-1)/2\rfloor $ ,当r!=0时。
- Leftchild® =2r +1,当2r+1<n时。
- Rightchild® = 2r+ 2 ,当2r+2 < n 时。
- Leftsibling® =r-1,当r为偶数且0<=r<=n-1。
- Rightsibling® =r+1,当r为奇数且r+1<n。
2.3.2链式存储结构实现
这个比较常用,一般有两种,一种是含有两个指针域的结点结构(左、右孩子) ,另一种在此基础上还包括了指针。
以第一种为例,因为在含有n个结点的二叉链表中,除了根节点外都有其父节点,所以有n-1条边,故有n+1个空链域 。这些空链域常用来存储其他有用的信息,这也就可以得到一种新的存储结构--线索链表。 不过回到正题,二叉链表的定义和实现(简易版,无关键字)
#include "BTNode.h"
#include <iostream>
using namespace std;
template <typename E>
BTNode<E>::BTNode()
{
lc=rc=NULL;
//ctor
}
template <typename E>
BTNode<E>::BTNode(E it,BTNode *left,BTNode *right)
{
lc=left;
rc=right;
item=it;
//ctor
}
template <typename E>
BTNode<E>::~BTNode()
{
//dtor
}
template <typename E>
E& BTNode<E>:: val()//返回元素的值
{
return item;
}
template <typename E>
void BTNode<E>::setVal(const E& it)//设置该结点的元素值
{
item=it;
}
template <typename E>
BTNode<E>* BTNode<E>::left()const
{
return left;
}
template <typename E>
BTNode<E>* BTNode<E>::right()const
{
return right;
}
template <typename E>
void BTNode<E>::setLeft( BinNode<E>* b)
{
lc=(BTNode*)b;
}
template <typename E>
void BTNode<E>::setRight( BinNode<E>* b)
{
rc=(BTNode*)b;
}
template <typename E>
bool BTNode<E>::isLeaf()
{
return (lc==NULL)&&(rc==NULL);
}
三、二叉检索(排序、查找)树
3.1 基本概念
Ⅰ、定义
二叉检索树或者为空, 或者是满足下列条件 的非空二叉树:
- 若它的左子树非空, 则左子树上所有结点的值均小于根结点的值;
- 它的右子树非空, 则右子树上所有结点的值均大于 或等于根结点的值;
- 左右子树本身又各是一棵二叉检索树。
Ⅱ、性质
按照中序遍历将各结点打印出来,将得到按 照由小到大的排列。
Ⅲ、作用
二叉检索树,树如其名,很方便检索呀!二叉检索树的效率就在于只需检索二个子树之一。
- 从根结点开始,在二叉检索树中检索值 K。如果根结点储 存的值为K,则检索结束。
- n 如果K小于根结点的值,则只需检索左子树 n
- 如果K大于根结点的值,就只检索右子树
- 这个过程一直持续到K被找到或者我们遇上了一个树叶。
- 如果遇上树叶仍没有发现K,那么K就不在该二叉检索树 中。
3.2 定义
ADT:
数据对象:一组可比较大小的数据
数据关系:
BST树或者是一棵空树,或者是具有BST 特 性的二叉树基本操作
- 构建空树
- 插入一个新数据
- 删除树中的一个数据
- 查找数据
- 输出树中所有数据
首先结点的类型在前面已经写过,也即是BTNode(简易版,无关键字)
#ifndef BST_H
#define BST_H
#include <BTNode.h>
template <typename E>
class BST
{
public:
BST();
virtual ~BST();
void clear();
void insert();
E remoeve(const E&);
E removeRoot();
E find(const E&) const;
int size();
void print()const;
private:
int nodeCount;
BTNode<E> *root;
//辅助的内部私有函数
void clearhelp(BTNode<E>);
BTNode<E>* inserthelp(BTNode<E> *,const E &);
BTNode<E>* deletemin(BTNode<E>*);
BTNode<E>* getmin(BTNode<E>*);
BTNode<E>* removehelp(BTNode<E>*, const E&);
//假定没有重复元素,若有那只好删除高度低的了
E findhelp(BTNode<E>*, const E&) const;
void printhelp(BTNode<E>*, int) const;
};
#endif // BST_H
是不是惊了!!定义都有这么多!!
3.3 某些函数的具体实现
3.3.1 查找
问题描述
对于给定的一棵BST和待查元素值e,若BST中存在某个结点的元素值 等于e,那么返回查找成功; 否则返回查找不成功。
算法思想
-
递归
若BST树为空 查找失败
否则:
- 若给定值小于根节点的关键字,则继续在根节点的左子树上查找
- 若给定值大于根节点的关键字,则继续在根节点的左子树上查找
- 若给定值等于根节点的关键字,则查找成功。
然而怎么表示你找到了这个结点呢?书上对结点的存储其实有关键字和值,也就是每个结点不仅有本身的值比如abcde,还有一个可以标识它的关键字用于排序。因而可以传一个可改变的地址,然后更改改地址的值表示你找到的那个结点的值是什么。后面的代码也一律默认存在关键字和值。
-
非递归
- 定义一个指向BST树结点的临时指针变量temp
- 从根节点开始,将其值赋值给temp
- 若temp不等于空指针,则取temp的值与待查找定值比较
- 若给定值等于temp所指结点的关键字,则查找成功,结束查找;
- 若给定值小于temp所指结点的关键字,则将temp的左孩子指针的 值赋值给temp;
- 若给定值大于temp所指结点的关键字,则将temp的右孩子指针的值赋值给temp;
- 继续执行c,直至temp为空或进入d,若temp为空则查找失败。
算法描述
递归
template <typename Elem, typename Key>
Elem BST<Elem,Key> :: findhelp( BTNode<Elem,Key>* subroot,const Key& k) const
{
if (subroot == NULL) return NULL; // Empty tree
if (k < subroot->key())
return findhelp(subroot->left(), k); // Check left
else if (k > subroot->key())
return findhelp(subroot->right(), k);// Check right
else return subroot->val(); // Found it }
}
非递归(很明显不递归更省空间)
template <typename Elem, typename Key>
bool BST<Elem,Key>::findhelp(const Key& k)const
{
BTNode<Elem,Key>*temp=root;
while(temp)
{
if(temp->key()==k) return true;
if(temp->key()<k) temp=temp->right();
else if(temp->key()>k) temp=temp->left();//不要掉了这个else,这是老师ppt里重大bug,找了我好久啊
}
return false;
}
算法分析
时间复杂度 平均情况为 O ( l o g n ) O(log_n) O(logn) 最差情况为 O(n)
空间复杂度 递归最差O(n) 非递归O(1)
3.3.2 插入
插入需要注意的是插入后不能改变BST的特性。插入的基本想法是要作为叶子结点插入最合适。
算法思想
-
递归
若(当前)BST树为空,直接插入该新建结点,并返回新结点的地址。
否则:
- 若给定值小于(当前)根节点关键字,则继续在左子树上进行插入(左子树的根节点作为下次递归根节点),将返回值设置为当前根结点的左孩子
- 若给定值大于(当前)根节点关键字,则继续在右子树上进行插入(右子树的根节点作为下次递归根节点),将返回值设置为当前根结点的右孩子
- 返回当前树根节点
-
非递归
-
新建一个结点r 数据域存储新的元素值
-
若BST为空,将当前根节点设置为r。插入结束,返回成功。
-
定义两个临时树结点指针变量,cur和nxt。把bst树的根节点的值赋给cur。
-
若cur非空 将cur的值赋给nxt
- 若新元素值小于cur所指关键字,则将cur的左孩子结点赋给cur
- 若新元素值大于cur所指关键字,则将cur的右孩子结点赋给cur
-
重复执行第三步,直至cur为空 结束查找
-
若当前元素值小于nxt的关键字,则将r作为nxt的左孩子,否则将r作为nxt的右孩子
-
结束插入,返回成功。
-
算法描述
-
递归
template <typename Elem,typename Key> BTNode<Elem,Key> *BST<Elem,Key>::inserthelp(BTNode<Elem,Key>* subRoot,const Elem &e,const Key& k)const { if(!subRoot) return new BTNode<Elem,Key>(e,k,NULL,NULL); if(k<(subRoot->key())) subRoot->setLeft(inserthelp(subRoot->left(),e,k)); else subRoot->setRight(inserthelp(subRoot->right(),e,k)); return subRoot; }
-
非递归
template <typename Elem, typename Key> bool BST<Elem,Key>::inserthelp(const Elem &e,const Key& k) { if(!root) {root=new BTNode<Elem,Key>(e,k,NULL,NULL);return true;} BTNode<Elem,Key> *cur,*nxt; cur=root; while(cur) { nxt=cur; if(k<cur->key()) cur=cur->left(); else cur=cur->right(); } if(k<nxt->key()) nxt->setLeft(new BTNode<Elem,Key>(e,k,NULL,NULL)); else nxt->setRight(new BTNode<Elem,Key>(e,k,NULL,NULL)); return true; }
算法分析
- BST的插入操作,插入结点都是作为一个叶子结点插入到BST中。
- 算法的步骤分为查找和插(接)入。
- 接入过程不需要移动结点,也不会整体改动树,所以时间开销为常数。
- BST的插入时间复杂度等于查找时间复杂度。
- 如果二叉树是平衡的,则有n个结点的二叉树 的高度约为logn,但是,如果二叉树完全不平衡(如成一个链表的形状),则其高度可以 达到n。
3.3.3 删除最小结点
根据bst树的特点,最小值的结点一定是最左的结点。因此我们只需要遍历bst树的左子树,找到一个结点左子树为空,则该结点为最小结点。但是要注意删除后要保持bst特性。要考虑的只有两种情况,
第一种是删除的结点不仅没有左孩子,右孩子也没有。 此时直接把这个结点删除即可(即设置7的左孩子为空)
第二种是待删除结点有右孩子的情况,那么此时就需要把右孩子代替待删除结点的位置。比如下图删除最小的24需要把32变为37的左孩子
算法思想
-
递归
若BST的当前根结点的左孩子为空,则该结点为最小值结点, 返回最小值结点右孩子结点;
否则:
-
继续在左子树上进行删除bst树最小值的操作,并将返回值设置为根节点的左孩子
-
返回当前根节点。
-
-
非递归
- 初始化两个临时指针cur和nxt
- 用临时指针cur初始化指向根节点。
- 如果根节点为NULL 返回失败,结束。
- nxt指向cur的左孩子。
- 当nxt非空,
- 如果nxt的左孩子为空,表示nxt为最小结点,cur的左孩子设置为nxt的右孩子。返回成功。
- 否则 把nxt赋给cur,nxt为nxt的左孩子。
- cur设置为cur的右孩子。
算法描述
递归:
template<typename Elem,typename Key>
BTNode<Elem,Key>* BST<Elem,Key>::deletemin(BTNode<Elem,Key>*subroot,BTNode<Elem,Key>* &dele)
{
if(!subroot->left())
{
dele=subroot;
return subroot->right();
}
else
{
subroot->setLeft(deletemin(subroot->left(),dele));
return subroot;
}
}
//这是一个私有函数 调用之前要判断嗷,入下:
Elem removemin()
{
if(!root)
{
cout<<"empty tree"<<endl;
return -1;
}
else
{
BTNode<Elem,Key> *temp=new BTNode<Elem,Key>();
root= deletemin(root,temp);
nodeCount--;
Elem a=temp->val();
delete temp;
return a;
}
}
非递归
bool BST<Elem,Key>::deletemin()
{
if(!root) return false;
BTNode<Elem,Key> *cur,*nxt;
cur=root;
nxt=cur->left();
while(nxt)
{
if(!(nxt->left()))
{
cur->setLeft(nxt->right());
delete nxt;
return true;
}
else cur=nxt;
nxt=nxt->left();
}
root=root->right();
return true;
}
算法分析
-
BST的最小值结点,从根结点的左孩子开始, 第一个左孩子为空的结点。
-
算法的步骤分为查找和接入。
-
BST删除最小值的时间复杂度等于查找时间 复杂度。
-
如果二叉树是平衡的,则有n个结点的二叉树 的高度约为logn,但是,如果二叉树完全不平 衡(如成一个链表的形状),则其高度可以 达到n。
3.3.4 删除
在BST树中找到保存R值的结点
删除该结点。
删除算法保持树的BST特性,如果待删除的结点为度为2的结点,那么需要把右子树中最小的作为根节点。
算法思想
-
递归
若BST树为空,返回空。
否则
- 若给定值R小于根结点的关键字,则继续在左子树上进行删除;将返回值 (结点指针)设置为根结点的左孩子;
- 若给定值R大于根结点的关键字,则继续在右子树上进行删除;将返回值 (结点指针)设置为根结点的右孩子;
- 若给定值R等于根结点的关键字,则分三种情况处理:
- 如果R结点没有子结点,根结点设置为空;(即,让R的父结点指向R 结点的指针改为NULL);并设置被删除结点的地址为R结点的地址。(这个是用于直到我删除了哪个结点)
- 如果R结点只有一个子结点,根结点设置为子结点;(即,让R的父 结点指向R结点的指针改为R的子结点)并设置被删除结点的地址为R结点 的地址。
- 如果R结点有两个子结点,找到R结点右子树中的最小值结点(deletmin),交换R 结点的值和R结点的右子树中的最小值;并设置被删除结点的地址为R结 点右子树的最小值地址。
-
非递归
首先在BST树中查找要删除的结点(find),看是否在BST树中,若不在则不做任何操作;
否则,假设要删除的结点为cur,结点cur的父结点为parent结点。下面分两 种情况讨论:
- 若删除的是根结点,则parent为空
- 若cur为叶结点,则设置根结点为空,并删除p结点;
- 若p为度为1的结点,则设置根结点为p的非空子结点,并删除p结点;
- 若p为度为2的结点,则首先找到p结点右子树中的最小值结点ps,用 该最小值取代p结点中的值,并删除ps结点。
- 若pp不为空,分三种情况讨论:
- 若p为叶结点,则设置pp指向p结点的指针为空,并删除p结点;
- 若p为度为1的结点,则设置pp指向p结点的指针直接指向p的非空子 结点,并删除p结点;
- 若p为度为2的结点,则首先找到p结点右子树中的最小值结点ps,用该最小值取代p结点中的值,并删除ps结点。(deletemin)
- 若删除的是根结点,则parent为空
算法实现
累了累了,先写一个,以后有空再写了
template<typename Elem,typename Key>
BTNode<Elem,Key>* BST<Elem,Key>::removehelp(BTNode<Elem,Key>*subroot, const Key&k )
{
if(subroot==NULL) return NULL;
else
{
if(k<(subroot->key())) subroot->setLeft(removehelp(subroot->left(),k));
else if(k>subroot->key()) subroot->setRight(removehelp(subroot->right(),k));
else
{
if(subroot->left()==NULL)
{
subroot=subroot->right();
}//01 00
else if(subroot->right()==NULL)
{
subroot=subroot->left();
}
//10
else
{
BTNode<Elem,Key> *temp=new BTNode<Elem,Key>();
subroot->setRight(deletemin(subroot->right(),temp));
subroot->setVal(temp->val());
subroot->setKey(temp->key());
delete temp;
}
}
return subroot;
}
}
算法分析
- BST的删除算法的步骤分为查找和删除处理。
- BST删除的时间复杂度取决于被删除结点的 深(高)度。
- 如果二叉树是平衡的,则有n个结点的二叉树 的高度约为logn,但是,如果二叉树完全不平 衡(如成一个链表的形状),则其高度可以 达到n。
四、堆树
貌似,不考代码的样子,简单复习一下吧,知道一下堆的siftdown和建树。
4.1 基本概念
堆树或者是一棵空树,或者是具有下列性质的 一棵完全二叉树(堆的局部有序特性)
- 每一个结点存储的值都小于或等于其子结点存储的值 称为 最小值堆
- 任意一个结点的值都大于或等于其任意一个子 结点存储的值 称为 最大值堆
4.2 特点
- 堆是一棵完全二叉树
- 堆树的每个结点保存集合中的一个数据
- 集合中的数据具有可比较大小的关键码
- 堆中结点存储的数据之间满足局部有序性
- 同一个数据集合,可以存在多个不同形态的堆
4.3 ADT
最大值堆(MaxHeap)ADT 设计
数据对象:一组可比较大小的数据
数据关系:堆或者是一棵空树,或者是具有局部有序性(任意一 个结 点的值都大于或等于其任意一个子结点存储的值) 的完全二叉树
基本操作:构建空树 插入一个新数据 删除树中的最大值 构建一个最大值堆
4.4 基本操作(最大值堆)
4.4.1 查找
堆的查找算法的算法分析
1)若查找最大值堆的最大值,只需访问 堆树的根结点,物理上访问数组的首地址 的元素,时间复杂度为O(1)。
2)若查找其他值,则必须用二叉树的遍 历算法进行查找,堆结构特征并不能加快 查找的性能,时间复杂度为O(n)。
4.4.2 插入
要插入一个新值
它应该成为树的一个叶结点是最合适,新建一个结点,存储新值; 然后把新结点接(插)入堆树中 调整新树,维持堆的性质
算法思想
- 在堆的末尾位置新增一个叶结点,存放新元素值val。
- 基于数组实现堆,只要数组有剩余空间,则在数组 中存储当前堆元素的后继空间,存放新元素val
- 当然,val很可能不在正确的位置,需要将其与父结点进 行比较,以使它移到正确的位置,保持整棵树的堆性质。
- 如果val的值小于或等于其父结点的值,则它已经处 于正确的位置,插入完成
- 如果val值已经位于堆的根结点位置(即数组的首地 址位置),则它已经处于正确的位置,插入完成
- 如果val的值大于其父结点的值,则两个元素交换位 置(值),一直到其到达正确的位置
算法分析
-
堆的插入操作,插入结点都是作为一个叶子 结点插入到堆中。
-
算法的步骤分为插(接)入和交换值。
-
接入过程不需要移动结点,也不会整体改动 树,所以时间开销为常数。
-
堆的插入时间复杂度取决于交换的次数。
-
堆是完全二叉树,则有n个结点的堆的高度为 logn。插入新元素时,最佳情况时不用交换, 最差情况交换logn,平均情况为logn。
4.4.3 构建
- 逐个插入
- 交换法建堆
下拉(siftdown)操作的步骤:
假设根的左、右子树都已经是堆,并且根的元素为R。在这种情 况下,有两种可能:
- R的值大于或等于其两个子结点,此时堆结构已经完成;
- R的值小于某一个或全部两个子结点的值,此时R应与两个 子结点中值较大的一个交换,若R仍然小于其新子结点的一个 或两个。在这种情况下,只需要简单地继续这种将“R拉下来” 的过程,直至到达某一层使它大于它的子结点,或者它成为叶结点。
交换法构建最大值堆的方法:
- 从数组的较大序号结点向较小序号结点顺序访问
- 对于每一项调用siftdown 操作
- 不必访问叶结点
void maxheap<Elem,Comp>::siftdown(int pos)
{
while (!isLeaf(pos))
{
int j = leftchild(pos);
int rc = rightchild(pos);
if ((rc<n) && Comp::lt(Heap[j],Heap[rc]))
j = rc;
if (!Comp::lt(Heap[pos], Heap[j]))
return;
swap(Heap, pos, j);
pos = j;
}
}
//i从0开始
//完全二叉树 编号 2(i+1)>n,则该结点无左孩子
//从大号王小号
void heap::buildheap()
{
for (int i=n/2-1; i>=0; i--)
siftdown(i);
}
复杂度
初始化堆:O(n)
4.4.4 删除某个位置的值
- 确定该位置是否合法
- 交换堆该结点的值与堆的最后一个结点的值,堆的元素个数减一。
- 这个结点位置新的值,可能比父结点的值大,要向上交换,直到其小于或等于其父结点的值,或达到根结点位置
- 然后这个值(新的位置)很可能比它的另一个子树的结点小,要 执行下拉(shiftdown)操作
template <class Elem, class Comp>
bool maxheap<Elem, Comp>::remove(int pos,
Elem & it) {
if ((pos < 0) || (pos >= n)) return false;
swap(Heap, pos, --n);
while ((pos != 0) && (Comp::gt(Heap[pos],
Heap[parent(pos)])))
swap(Heap, pos, parent(pos));
siftdown(pos);
it = Heap[n];
return true;
}
总结
堆的插入删除平均和最差时间代价都是 O ( l o g n ) O(log_n) O(logn)
五、平衡二叉树(AVL树)
AVL树是最先发明的自平衡二叉查找树,也被称为高度平衡树。相比于"二叉查找树",它的特点是:AVL树中任何节点的两个子树的高度最大差别为1。
AVL树的查找、插入和删除在平均和最坏情况下都是O(logn)。
如果在AVL树中插入或删除节点后,使得高度之差大于1。此时,AVL树的平衡状态就被破坏,它就不再是一棵二叉树;为了让它重新维持在一个平衡状态,就需要对其进行旋转处理。学AVL树,重点的地方也就是它的旋转算法;以下参考
typedef int Type;
typedef struct AVLTreeNode{
Type key; // 关键字(键值)是用来对AVL树的节点进行排序的。
int height; //高度
struct AVLTreeNode *left; // 左孩子
struct AVLTreeNode *right; // 右孩子
}Node, *AVLTree;
总览
LL型
LeftLeft,也称为"左左"。插入或删除一个节点后,根节点的左子树的左子树还有非空子节点,导致"根的左子树的高度"比"根的右子树的高度"大2,导致AVL树失去了平衡。
这次失衡结点是16,11是其左孩子,9为其失衡结点的左孩子的左孩子,所以是LL型,以失衡结点的左孩子为旋转中心进行一次右旋转即可。将k1变成根节点,k2变成k1的右子树,“k1的右子树"变成"k2的左子树”。
LR型
LeftRight,也称为"左右"。插入或删除一个节点后,根节点的左子树的右子树还有非空子节点,导致"根的左子树的高度"比"根的右子树的高度"大2,导致AVL树失去了平衡。
如:最开始插入数据16,3,7后的结构如上图所示,结点16失去了平衡,3为16的左孩子,7为失衡结点的左孩子的右孩子,所以为LR型。
接下来通过两次旋转操作复衡,先通过以3为旋转中心,进行左旋转,结果如图所示,然后再以7为旋转中心进行右旋转,旋转后恢复平衡了。
RR型
称为"右右"。插入或删除一个节点后,根节点的右子树的右子树还有非空子节点,导致"根的右子树的高度"比"根的左子树的高度"大2,导致AVL树失去了平衡。
RL型
称为"右左"。插入或删除一个节点后,根节点的右子树的左子树还有非空子节点,导致"根的右子树的高度"比"根的左子树的高度"大2,导致AVL树失去了平衡。
总结 LL/RR都只需要进行一次右旋/左旋,但是LR/RL需要进行两次旋转,以LR为例,抓着失衡结点的下一个结点左旋,然后抓着失衡节点根节点右旋。
6.2 特点
- 最优二叉树也称为哈夫曼编码树
- 最优二叉树是一棵满二叉树,没有度为1的结点
- 最优二叉树的叶子结点,每个结点保存集合中的一个数据,即一个权值
- 最优二叉树是带权路径长度最短的树,权值较大的结点离根结点较近
6.3 ADT
数据对象:一组可比较大小的数据(频率值)
数据关系:哈夫曼树是一棵满二叉树,数据集中的数据存储在叶结点中
基本操作:构建哈夫曼树 获取结点的路径(根结点到某个叶结点的路径) 解码 编码
删除一个节点后,根节点的右子树的左子树还有非空子节点,导致"根的右子树的高度"比"根的左子树的高度"大2,导致AVL树失去了平衡。
[外链图片转存中…(img-C2b1hiDw-1603279150816)]
总结 LL/RR都只需要进行一次右旋/左旋,但是LR/RL需要进行两次旋转,以LR为例,抓着失衡结点的下一个结点左旋,然后抓着失衡节点根节点右旋。
六、huffman编码树
6.1 简介
哈夫曼树,又称最优树,是一类带权路径长度最短的树。用于数据压缩。可以利用字母出现频率来编码,经常出现的字母的编码较短,这样的处理既能节省磁盘空间,又能提高运算速度。
从树中一个结点到另一个结点之间的分支构成了两结点之间的路径,路径上的分支个数称为路径长度。二叉树的路径长度是指由根结点到所有叶子结点的路径长度之和。如果二叉树中的叶子结点都有一定的权值,则可将这一概念拓展:设二叉树具有n个带权值的叶子结点,则从根结点到每一个叶子结点的路径长度与该叶子结点权值的乘积之和称为二叉树路径长度,记为: W P L = ∑ k = 1 n w k l k WPL=\sum_{k=1}^{n}w_kl_k WPL=k=1∑nwklk
哈夫曼算法:
(1)根据给定n个权值{w1,w2,…,wn}构成n棵二叉树的集合F={T1,T2,…,Tn};其中,每棵二叉树Ti(1<=i<=n)只有一个带权值wi的根结点,其左、右子树均为空。
(2)在F中选取两棵根结点权值最小的二叉树作为左、右子树来构造一棵新的二叉树,且置新的二叉树根结点权值为其左右子树根结点的权值之和。
(3)在F中删除这两棵树,同时将生成新的二叉树加入到F中。
(4)重复(2)(3),直到F中只剩下一棵二叉树加入到F中。
6.2 特点
- 最优二叉树也称为哈夫曼编码树
- 最优二叉树是一棵满二叉树,没有度为1的结点
- 最优二叉树的叶子结点,每个结点保存集合中的一个数据,即一个权值
- 最优二叉树是带权路径长度最短的树,权值较大的结点离根结点较近
6.3 ADT
数据对象:一组可比较大小的数据(频率值)
数据关系:哈夫曼树是一棵满二叉树,数据集中的数据存储在叶结点中
基本操作:构建哈夫曼树 获取结点的路径(根结点到某个叶结点的路径) 解码 编码
//结点类 又分为两类结点,一个是叶子节点LeafNode一个是中间节点IntNode,由huffman特性可以知道他只存在这两种结点,具体实现就不写了。就知道一下这几个函数作用就行
template <typename E>
class HuffNode
{
//Node abstract base class public:
HuffNode();
virtual int weight() = 0;
virtual bool isLeaf() = 0;
virtual HuffNode* left() const = 0;
virtual void setLeft(HuffNode*) = 0;
virtual HuffNode* right() const = 0;
virtual void setRight(HuffNode*) = 0;
};
6.4 实现huffman树
6.4.1 Huffman树构建算法思想
贪心算法
- 根据给定的n 个权值 { w 1 , w 2 , …, w n },
构造n 棵二叉树的集合F = {T1,T2, … , Tn}, 其中每棵二叉树中均只含一个带权值为 wi 的根结点, 其左、右子树为空树;(也就是节点类的LeafNode) - 在F中选取其根结点的权值为最小的两棵二叉树, 分别作为左、右子树构造一棵新的二叉树,并置这棵新的 二叉树根结点的权值为其左、右子树根结点的权值之和;
- 从 F 中删去这两棵树,同时加入刚生成的新树;重复 (2)和 (3)两步,直至F 中只含一棵树为止。
6.4.2 算法描述
由于每次都需要选出F中权值最小的两棵二叉树,故可以用最小值堆来存储这个二叉树集合
- 建立最小值堆,类型为huffman树,存储n个二叉树结点
- 若当前堆内元素个数大于1,重复执行3-4
- 用两个个临时哈夫曼树类型变量temp1,temp2分别存储最小值堆里最小的两个结点,将他们从堆里移除。还用一个临时变量temp3存储新建的哈夫曼树结点,其左右节点分别为temp1,temp2
- 在堆里插入temp3
HuffTree* buildHuff(HuffTree<E>** TreeArray, int count)
{
heap<HuffTree*,minTreeComp>* forest = new heap<HuffTree<E>*, minTreeComp>(TreeArray, count, count);
HuffTree<char> *temp1, *temp2, *temp3 = NULL;
while (forest->size() > 1)
{
temp1 = forest->removefirst(); // Pull first two trees
temp2 = forest->removefirst(); // off the list
temp3 = new HuffTree<E>(temp1, temp2);
forest->insert(temp3); // Put the new tree back on list
delete temp1; // Must delete the remnants
delete temp2; // of the trees we created
}
return temp3;
}
6.4.3 算法分析
哈夫曼树 的构建算法基于贪心算法设计。
算法的步骤分为查找权值最小的两个子树,合并子树。
构建树的过程时间复杂度取决于查找。
如果用堆来实现查找最小值,初始时构建最小值堆的开销为O(n),每次的查找开销为2logn。
构建整个树的时间复杂度为O(nlogn) 。
七、线索二叉树
首先,回顾一下遍历算法
先序:ABCDEFGHK
中序:BDCAHGKFE
后序:DCBHKGFEA
- 基本操作是访问结点,对于n个结点的二叉树而言 ,无论是那种遍历,时间复杂度均为O(n);
- 空间复杂度
- 二叉树结点的存储O(n);
- 递归调用时需要使用堆栈,任何时刻同时存在的活跃调 用数目跟树的深度有关,极差情况下为O(n)。
- 有些应用需要多次遍历,如何降低时空开销?
如何适应多次遍历的应用需求?
- 思路1: 在结点中增加前驱和后继指针,速度快,有额外的空间开销。
- 思路2: 利用现有的二叉树结点中的n+1个空指针。
7.1 基本概念
指向该线性序列中的“前驱”和 “后继” 的指针,称作==“线索”==
与其相应的二叉树,称作 “线索二叉树”
包含 “线索”的存储 结构,称作“线索链表”
需要注意的是 这个前驱/后继的意思是指某种次序遍历所得到的序列中的前驱后继
如:下图是一棵中序线索树和中序线索链表,虚线表示线索。因为a是最最左的,f是最右的,因此a前驱为NULL,f后驱为NULL
易知中序遍历结果为a+b*c-d-e/f
7.2 对线索链表中结点的约定
在二叉链表的结点中增加两个标志域, 并作如下规定:
-
若该结点的左子树不空,则Lchild域的指针指向其左子树,且左标志域的值为“指针 Link”;
-
否则,Lchild域的指针指向其“前驱”,且左标志的值为“线索 Thread” 。
-
若该结点的右子树不空, 则rchild域的指针指向其右子树, 且右标志域的值为 “指针 Link”;
-
否则,rchild域的指针指向其“后继”, 且右标志的值为“线索 Thread”。
如此定义的二叉树的存储结构称作“线索链表”。
7.3 实际操作
在中序遍历过程中修改结点的 左、右指针域,以保存当前访问结 点的“前驱”和“后继”信息。
遍历过程中,附设指针pre,并始终保持 指针pre指向当前访问的、指针p所指 结点的前驱。
补充题目
( 👍 )1、存在这样的二叉树,对它采用任何次序的遍历,结果相同。
( 👍 )2、中序遍历一棵二叉排序树的结点,可得到排好序的结点序列。
( 👍 )3、对于任意非空二叉树,要设计其后序遍历的非递归算法而不使用堆栈结构,最适合的方法 是对该二叉树采用三叉链表。
( 👎 )4、在哈夫曼编码中,当两个字符出现的频率相同时,其编码也相同,对于这种情况应做特殊 处理。
若以 { 4 , 5 , 6 , 7 , 8 } 作为权值构造哈夫曼树,则该树的带权路径长度为( C )。
A. 67
B. 68
C. 69
D. 70
解析: 画树过程见Huffman,带权路径长为 W P L = ∑ k = 1 n w k l k WPL=\sum_{k=1}^{n}w_kl_k WPL=∑k=1nwklk
故 W P L = 3 ∗ 4 + 3 ∗ 5 + 2 ∗ 8 + 2 ∗ 6 + 2 ∗ 7 = 69 WPL=3*4+3*5+2*8+2*6+2*7=69 WPL=3∗4+3∗5+2∗8+2∗6+2∗7=69