目录
文章目录
1.树的定义、实现与基本操作
1.1 有关二叉树的重要定义
- 路径 - 长度 — 祖先 - 子孙
- 深度:从根结点到该结点的路径长度
- 树的高度=最深节点的深度+1
- 结点的层数=深度 (根结点结点层数为0,深度为0)
- 叶节点、内部节点(/分支节点)
- (定义有争议) 满二叉树:结点若是分支结点,定有两个非空子结点
- (定义无争议) 完全二叉树:从根节点到每一层从左到右填充。即:二叉树的高度为d,除第 d-1层外,其它各层都是满的,第 d-1 层所有的结点都连续集中在最左边
※满二叉树定理:
- 对非空满二叉树,其叶结点数=分支结点数+1
- 引理:对非空二叉树,其空子树的数目=结点数目+1
1.2 二叉树的ADT和两种实现方式
ADT:
template<typename E> class BinNode {
public:
virtual ~BinNode() {}
virtual E& element() = 0;
virtual void setElement(const E&) = 0;
virtual BinNode* left() const = 0;
virtual void setLeft(const E&) = 0;
virtual BinNode* right() const = 0;
virtual void setRight(const E&) = 0;
virtual bool isLeaf() = 0;
1.使用指针实现二叉树
- 包含一个数据区和两个指向子节点的指针
- 很大比例的空间被结构性开销占据
template<typename Key,typename E>
class BSTNode:public BinNode<E> {
private:
Key k; //为实现二叉检索树:存储关键码
E it;
BinNode *rc;
BinNode *lc;
public:
BSTNode() {lc=rc=NULL;}
BSTNode(Key K,E e,BSTNode *l=NULL,BSTNode *r=NULL) {
k=K; it=2; lc=l; rc=r;
}
E& element() {
return it;
}
void setElement(const E& e) {
it=e;
}
inline BinNode* left() const {
return lc;
}
void setLeft(BinNode<E> *b) {
lc=(BTSNode*)b;
}
BinNode* right() const {
return rc;
}
void setRight(BinNode<E> *b) {
rc=(BTSNode*)rc;
}
bool isLeaf() {
return (lc==NULL)&&(rc==NULL);
}
};
※ 分支结点和叶结点是否使用相同的类来定义十分重要:为节省存储空间
方法: 给BinNode定义一个基类VarBinNode,再由该基类的子类来具体区分是分支结点IntlNode还是叶结点LeafNode
即:
使用复合设计模式
- 使用一个虚基类和两个独立的结点类
- 每个子类中对其实现可按照自己的需求进行
1.基类VarBinNode
class VarBinNode {
public:
virtual ~VarBinNode() {}
virtual bool isLeaf() = 0;
};
2.叶结点LeafNode
template<typename E>
class LeafNode:public VarBinNode {
private:
E leafElmt;
public:
LeafNode(E leafE) {
leafElmt=leafE;
}
bool isLeaf() { return true; }
E value() { return leafElmt; }
};
3.分支结点IntlNode
template<typename E>
class IntlNode:public VarBinNode {
private:
//不知道左/右子结点是叶子还是中间节点
//只能先定义基类结点的指针,最后再强制类型转换
VarBinNode* left;
VarBinNode* right;
E inElmt;
public:
IntlNode(const E inE,VarBinNode* l,VarBinNode* r) {
inElmt=inE;
left=l;
right=r;
}
bool isLeaf() { return false; }
VarBinNode* leftchild() { return left; }
VarBinNode* rightchild() { return right; }
void traverse(VarBinNode *root) { //找到root树下的所有叶结点
if(root==NULL) return;
if(root->isLeaf()) {
cout<<"Leaf:"<<((LeafNode *)root)->value() << endl;
}
traverse(((IntlNode*)root)->leftchild());
traverse(((IntlNode*)root)->rightchild());
}
};
※:另一种traverse写法: 在基类中定义traverse函数,子类自己具有traverse函数各自的实现方式(书p105)
1.2 使用数组实现完全二叉树
- 简单紧凑地实现完全二叉树
- 亲属结点的下标公式:(位置从数组的0号位开始)
- Parent: (r-1)/2,当r≠0时 ——>(则:若有n个结点,叶结点的下标为(n-1)/2-1~n)
- Leftchild: 2r+1(<n)
- Rightchild: 2r+2(<n)
- Leftsibling: r-1,r为偶数时
- Rightsibling: r+1,r为奇数且r+1<n时
1.3 二叉树的遍历
- 枚举:对每个结点都进行一次访问并将其列出
- 遍历:按一定顺序访问二叉树的结点
1. 前序遍历
先访问结点,再访问子结点
递归实现:
template<typename E>
void preorder(BinNode<E> *root) {
if(root==NULL) {
return;
}
visit(root); //对当前访问的结点进行的操作
preorder(root->left());
preorder(root->right());
}
栈实现:
在这里插入代码片
2.中序遍历
先访问左子结点(以及整棵左子树),再访问该节点,最后访问右子结点(以及整棵右子树)
※二叉检索树使用的遍历方法
递归实现:
template<typename E>
void inorder(BinNode<E>* root) {
if(root==NULL) return;
inorder(root->left());
visit(root); //与前序遍历visit位置不同
midorder(root->right());
}
3.后序遍历
先访问子结点,在访问该结点
应用如:释放树中所有结点所占用的空间
递归实现:
template<typename E>
void postorder(BinNode<E>* root) {
if(root==NULL) return;
inorder(root->left());
postorder(root->right());
visit(root); //三种遍历都是通过visit所在位置不同实现的
}
栈实现:
在这里插入代码片
4.层序遍历
队列实现:
#include<queue>
using namespace std;
template<typename E>
void levelorder(BinNode<E>* root) {
queue q;
BinNode<E>* b;
q.push(root);
while(!q.empty()) {
b=q.pop();
visit(b);
if(b->left()!=NULl) q.push(b->left());
if(b->right()!=NULL) q.push(b->right());
}
}
2.二叉检索树BST
- 又称“二叉查找树”、“二叉排序树”
- 使记录的插入和检索都能很快完成(继承字典的结构,时间复杂度:log(n))
- 从根结点开始,在BST中检索K值:十分有效->仅需检索两棵子树之一; 全过程直到K被找到或者遇到叶子结点(若仍没发现K,则K不在此BST树中)
2.1 BST的性质
- 对其中任意一个结点,其左子树中任意一个结点的值都小于该结点,右子树中任意一个结点的值都大于等于该结点
- 按中序遍历打印各节点,结果由小到大排列(由性质1容易得出)
- 将关键码和值存在树的结点中
2.1 使用字典实现BST
3. 堆(heap)与优先队列
- 堆:又名存储池
3.1 堆的性质
- 堆是一棵完全二叉树(往往拿数组实现)
- 堆中存储的数据局部有序:结点存储的值与其子结点存储的值间有某些联系
- 最大堆:任意一个结点存储的值都大于等于其任意一个子结点存储的值
- 根节点存储了该数中的最大值
- 实现堆排序
- 最小堆:小于等于
- 实现置换选择算法
- 最大堆:任意一个结点存储的值都大于等于其任意一个子结点存储的值
- 兄弟结点间没有必然联系(※凭此区分BST和堆),堆只实现了局部排序
3.2 最大堆的实现
- insert::首先将要插入的结点置于堆的末尾位置n,再与其父节点比较,若大于父节点就与父节点交换位置,直到小于等于父节点,则达到正确位置
- void siftdown(int pos):从当前位置pos向下层进行重排列(非叶子结点的所有节点向下交换的重排列)
关键:认为root下的左右子树都已经是堆,选择子树中根节点大的那个交换位置,然后递归到最后(?这个有问题,没解释清楚)
1. 将当前位置和值最大的子结点比较,若是最大子节点大于当前位置值则交换位置
2. 重复步骤1,直到当前位置结点在树中找到了恰当位置
※:siftup:从当前位置pos向上层进行重排列(非根节点到最后一个叶子节点向上交换的重排列) - removefirst:移去根节点后,令最后一个叶子节点做根节点,若此时堆中不止一个结点,就利用siftdown(0)将根放入正确的位置,实现重排序
- remove:
#include<assert.h>
#include<iostream>
#define Assert(a,b) assert((a)&&(b))
using namespace std;
template<typename E> class heap {
private:
E* Heap;
int maxsize;
int n;
void siftup(int pos) {
while(!isTop(pos)) {
int j=parent(pos);
if(Heap[pos]>Heap[j]) return;
swapE(pos,j);
pos=j;
}
}
void siftdown(int pos) {
while(!isLeaf(pos)) {
int j=leftChild(pos);
int rc=rightChild(pos);
if(rc<n && Heap[j]<Heap[rc]) j=rc;
if(Heap[pos]>Heap[j]) return;
swapE(pos,j);
pos=j;
}
}
void swapE(int p1,int p2) {
E temp=Heap[p1];
Heap[p1]=Heap[p2];
Heap[p2]=temp;
}
public:
heap(E* h,int num,int max) {
Heap=h; n=num; maxsize=max;buildHeap();
}
int size() const {
return n;
}
bool isLeaf(int pos) const {
return (pos>=n/2)&&(pos<n);
}
int leftChild(int pos) const{
return 2*pos+1;
}
int rightChild(int pos) const{
return 2*pos+2;
}
int parent(int pos) const {
return (pos-1)/2;
}
void buildHeap() {
// for(int i=1; i<n; i++) {
// siftup(i);}
for(int i=n/2-1; i>=0; i--) siftdown(i); //从第一个非叶节点开始
}
void insert(const E& it) {
Assert(n<maxsize,"Heap is full");
int curr=n++;
Heap[curr]=it;
while((curr!=0)&&(Heap[curr]>Heap[parent(curr)])) {
swapE(curr,parent(curr));
}
}
E removefirst() {
Assert(n>0,"Heap is empty");
E it=Heap[0];
Heap[0]=Heap[--n];
if(n!=0) siftdown(0);
return it;
}
//与parent交换到合适的位置之后,siftdown
E remove(int pos) {
Assert(n>0,"Heap is empty");
E it=Heap[pos];
if(pos==n-1) n--;
else {
Heap[pos]=Heap[--n];
while((pos!=0)&&(Heap(pos)>Heap(parent(pos)))) {
swapE(pos,parent(pos));
pos=parent(pos);
}
if(n!=0) siftdown(pos);
}
return it;
}
void printHeap() const {
for(int i=0;i<n;i++) {
cout<<Heap[i]<<" ";
}
cout<<endl;
}
void heapsort() const {
heap mh(Heap,n,maxsize);
for(int i=0;i<n;i++) {
Rational tmp=mh.removefirst();
cout<<tmp<<" ";
}
cout<<endl;
}
};
4. Huffman编码树
- 固定长度编码方法:ASCII码
变长编码:Huffman编码 - Huffman树:满二叉树
- 具有最小外部路径权重的二叉树:加权路径之和最小(权重大的叶结点深度小)
4.1 Huffman编码树的建立过程
1.创建n个初始Huffman树(只包含单一的、记录了对应字母的叶结点) -> 放入一个森林中
2.将n棵树按权重由小到大排成一列
3.取出前两棵树,将其标记为同一结点的叶子,从而获得一颗权重是这两棵树权重之和的新树
4.按照该新树的权重,将其插回序列中
5.重复2~4,直到序列中只剩下一个元素
(exp:P120 例5.8)
4.1 Huffman编码
- 0对应左节点,1对应右节点
- 信息反编码
- 前缀特性