第5-10章:线性结构,元素之间存在线性次序(线性表、数组与矩阵、栈、队列、
跳表和散列表第11-15章:层次结构(二叉树和树、优先队列、竞赛树、搜索树)
具有层次结构的数据一般不适合于用线性数据结构描述
术语大集合
树
对应一个层次结构
根(root)
:层次中最高层的元素
孩子(children)
:根的孩子是根的下一层元素,是树的子树的根
叶子(leaves)
:树中没有孩子的元素
高度(height)
或深度(depth)
:树的级数(或层数)
元素的度
:指其孩子的个数。如上图:Joe的度为3;Mary的度为2;Ann的度为0
树的度
:是其元素度的最大值。如上图那棵树的度即3
定义二叉树
二叉树和树的区别
二叉树 | 树 |
---|---|
可以为空 | 不能为空 |
每个元素都恰好有两棵子树(其中一个或两个可能为空) | 每个元素可有任意多个子树 |
在二叉树中每个元素的子树都是有序的,也就是说,可以用左、右子树来区别 | 子树间是无序的 |
特性①:包含n(n>0)
个元素的二叉树边数n-1
证明:二叉树中每个元素(除了根)有且只有一个父母,在孩子与其父母间有且只有一条边,因此,边数为n-1
特性②:若二叉树的高度为h(h≥0)
,则该二叉树最少有h个元素,最多有 2 h − 1 2^h-1 2h−1个元素。
特性③:包含n(n≥0)
个元素的二叉树的高度最大为n,最小为 l o g 2 ( n + 1 ) log_2(n+1) log2(n+1)
特性④:二叉树中度为0的元素数 = 度为2的元素数+1
满二叉树
当高度为h的二叉树恰好有 2 h − 1 2^h-1 2h−1个元素时,称其为满二叉树(full binary tree)。对高度为h的满二叉树中的元素按从第上到下,从左到右的顺序从1到 2 h − 1 2^h-1 2h−1进行编号。
完全二叉树
h层完全二叉树:
特性⑤:设完全二叉树中一元素的序号为i
,1≤i≤n
。则有以下关系成立:
i=1
时,该元素为二叉树的根。若i>1
,则该元素父母的编号为(i/2)
2i>n
时,该元素无左孩子。否则,其左孩子的编号为2i
2i+1>n
,该元素无右孩子。否则,其右孩子编号为2i+1
完全二叉树:按照从上到下同层从左到右对元素编号,将二叉树的元素按照编号存储在数据中相应位置
一个有n个元素的二叉树需要的存储空间:n+1到 2 n 2^n 2n
当缺少的元素数目比较少时,数组描述方法是有效的。
二叉树最常用的描述方法。
每个元素都存储在一个节点内,每个节点:
leftChild | element | rightChild |
---|
在n个结点的二叉链表中,有n+1个空指针域
//链表二叉树的节点结构
template<class T>
struct binaryTreeNode
{
T element;
binaryTreeNode<T> *leftChild;//指向左孩子节点的指针
binaryTreeNode<T> *rightChild;//指向右孩子节点的指针
//第一个构造函数——无参数
binaryTreeNode(){leftChild = rightChild = NULL;}
//第二个构造函数——有一个参数用来初始化element,而指针域被置为NULL
binaryTreeNode(const T& theElement)
{
element(theElement)
leftChild = rightChild = NULL;
}
//第三个构造函数——三个参数用来初始化三个域
binaryTreeNode(const T& the Element,
binaryTreeNode *theLeftChild,
binaryTreeNode *theRightChild)
{
element(theElement)
leftChild = theLeftChild;
rightChild = theRightChild;
}
};
都是基于11.6将提到的遍历
确定其高度指路11.8
确定其元素数目指路11.8
复制
在屏幕或纸上显示二叉树
确定两棵二叉树是否一样
删除整棵树
用递归的后序遍历,将访问根结点改为释放根结点空间即可
若为数学表达式树,计算该数学表达式指路11.6
若为数学表达式树,给出对应的带括号的表达式指路11.6
visit函数
template <class T>
void visit(binaryTreeNode<T> *x)
{//访问节点*x,仅输出element域
cout << x->element <<' ';
}
先访问一个节点,再访问该节点的左右子树(根、左、右)
template <class T>
void preOrder(binaryTreeNode<T> *t)
{//前序遍历二叉树*t
if(t != Null)
{
visit(t);//访问树根
preOrder(t->leftChild);//前序遍历左子树
preOrder(t->rightChild);//前序遍历右子树
}
}
- 每遇到一个节点,先访问该节点,并把该节点的非空右孩子入栈,然后遍历其左子树
- 左子树遍历完后,从栈中弹出节点(右子树的根),继续遍历。
- 为了算法的简洁,最开始推入一个空指针入栈作为监视哨;当这个空指针被弹出时,遍历结束
template<class T>
void preOrder(binaryTreeNode<T>*t)
{
arrayStack <binaryTreeNode<T>*> S(MaxLength);
S.push(NULL);
binaryTreeNode<T>*p=t;
while (p != NULL)
{
visit(p);
if (p->rightChild != NULL)
S.push(p->rightChild);
if(p->leftChild != NULL)
p = p->leftChild;
else
{
p = S.top();
S.pop();
}
}
}
先访问一个节点的左子树,然后访问该节点,最后访问右子树(左、根、右)
template <class T>
void inOrder(binaryTreeNode<T> *t)
{//中序遍历二叉树*t
if(t != Null)
{
inOrder(t->leftChild);//中序遍历左子树
visit(t);//访问树根
inOrder(t->rightChild);//中序遍历右子树
}
}
输出完全括号化的中缀表达式
对一棵数学表达式树分别进行中序、前序和后序遍历,结果便是表达式的中缀、前缀和后缀形式。中缀形式是我们通常的书写形式。
template <class T> void infix(binaryTreeNode<T> *t) {//输出中缀表达式 if(t != NULL) { cout << '('; infix(t->leftChild);//左操作数 cout << t->element;//操作符 infix(t->rightChild);//右操作数 cout << ')'; } }
- 每遇到一个节点就把它入栈,然后去遍历其左子树
- 遍历完左子树后,弹出栈顶节点并访问
- 按照其右链接指示的地址再去遍历该节点的右子树
template<class T>
void inOrder(binaryTreeNode<T>*t)
{
arrayStack <binaryTreeNode<T>*> S(MaxLength);
binaryTreeNode<T>*p=t;
do
{
while (p!=NULL)
{
S.push(p);
p=p->leftChild;
}
if(!S.empty())
{
p=S.top();
S.pop();
visit(p);
p=p->rightChild;
}
}while(p!=NULL||!S.empty())
}
先访问一个节点的左右子树,再访问该节点(左、右、根)
template <class T>
void postOrder(binaryTreeNode<T> *t)
{//后序遍历二叉树*t
if(t != Null)
{
postOrder(t->leftChild);//后序遍历左子树
postOrder(t->rightChild);//后序遍历右子树
visit(t);//访问树根
}
}
后序遍历的非递归实现主要思想:
- 每遇到一个节点就把它入栈,然后去遍历其左子树
- 遍历完左子树后,回到栈顶节点
- 按照其右链接指示的地址再去遍历该节点的右子树
- 遍历完右子树后,弹出栈顶节点并访问
给栈中的每个元素加一个标志位tag
- 用枚举类型表示,tag为left表示已进入该节点的左子树;tag为right表示已进入该节点的右子树
栈中的元素类型stackElement
pointer tag
//定义枚举类型:Tag
enum Tag{left,right};
//自定义新的类型,把二叉树节点和标记封装在一起
typedef struct
{
binaryTreeNode<T>* node;
Tag tag;
}TagNode;
//后序遍历
void postOrder(binaryTreeNode<T> *t)
{
if (t == NULL)
return;
arrayStack<TagNode> s;
TagNode tagnode;
binaryTreeNode<T>* p = t;
while (!s.empty() || p)
{
while (p)
{
tagnode.node = p;
//该节点的左子树被访问过
tagnode.tag = Tag::left;
s.push(tagnode);
p = p->leftchild;
}
tagnode = s.top();
s.pop();
//左子树被访问过,则还需进入右子树
if (tagnode.tag == Tag::left)
{
//置换标记
tagnode.tag = Tag::right;
//再次入栈
s.push(tagnode);
p = tagnode.node;
//进入右子树
p = p->rightchild;
}
else//右子树已被访问过,则可访问当前节点
{
tagnode.node->element;
//置空,再次出栈(这一步是理解的难点)
p = NULL;
}
}
}
从上往下逐层,同层从左到右的次序访问各元素
参考理解博客
借助队列来实现,核心思想:
每次出队一个元素,就将该元素的孩子节点加入队列中,直至队列中元素个数为0时,出队的顺序就是该二叉树的层次遍历结果
template <class T>
void levelOrder(binaryTreeNode<T> *t)
{//层次遍历二叉树*t
arrayQueue<binaryTreeNode<T>*> q;//链队列的应用
while (t != NULL)
{
visit(t);//访问t
//将t的孩子插入队列
if(t->leftChild != Null)
q.push(t->leftChild);
if(t->rightChild != Null)
q.push(t->rightChild);
//提取下一个要访问的节点
try {t = q.front();}
catch(queueEmpty) {return;}
q.pop();
}
}
BinaryTree
抽象数据类型 binaryTree
{
实例:
元素集合;如果不空,则被划分为根、左子树和右子树;
每个子树仍是一个二叉树;
操作:
empty():如果二叉树为空,则返回true,否则返回false
size():返回二叉树的节点/元素个数
preorder(visit):前序遍历二叉树,visit是访问函数
inOrder(visit):中序遍历二叉树
postOrder(visit):后序遍历二叉树
levelOrder(visit):层次遍历二叉树
}
template<class T>
class binaryTree
{
public:
virtual ~binaryTree(){}
//二叉树为空时返回true,否则返回false
virtual bool empty() const = 0;
//返回二叉树中元素的个数
virtual int size() const = 0;
//前序遍历二叉树
virtual void preOrder(void(*)(T*) = 0;
//中序遍历二叉树
virtual void inOrder(void(*)(T*) = 0;
//后序遍历二叉树
virtual void postOrder(void(*)(T*) = 0;
//层序遍历二叉树
virtual void levelOrder(void(*)(T*) = 0;
}
linkedBinaryTree
template<class T>
class linkedBinaryTree:public binaryTree<binaryTreeNode<E>>
{
public:
//基础操作(代码见上方)
linkedBinaryTree(){root = NULL; treeSize = 0;}//构造函数
~linkedBinaryTree(){}; //析构函数
bool empty() const {return treeSize == 0};
void visit(binaryTreeNode<T> *x);//遍历辅助
void preOrder(binaryTreeNode<T>*t);//前序遍历
void inOrder(binaryTreeNode<T>*t);//中序遍历
void postOrder(binaryTreeNode<T>*t);//后序遍历
void levelOrder(); //层次遍历
//补充操作
binaryTreeNode<T>* find(binaryTreeNode<T>*t, int k);//查找
void makeTree(int n); //建立树
int height(binaryTreeNode<T>*t);//计算二叉树的高度
int number(binaryTreeNode<T>*t);//计算二叉树节点数目
private:
binaryTreeNode<T> *root;//指向根的指针
int treeSize;//树的节点个数
};
借助于队列应用,流程同层次遍历
每次出队一个元素,且在出队时检查其是否为所找的元素,并将该元素的孩子节点加入队列中,直至队列中元素个数为0时,
template<class T>
binaryTreeNode<T>*linkedBinaryTree<T>::find(binaryTreeNode<T>*t, int k)
{
queue <binaryTreeNode<T>*>q;
while (t!=NULL)
{
if (t->element==k)
return t;
if (t->leftChild!=NULL)
q.push(t->leftChild);
if (t->rightChild!=NULL)
q.push(t->rightChild);
if (q.empty())
return NULL;
t=q.front();
q.pop();
}
}
根节点为1,编号为 i 的节点的左孩子节点为 a,右孩子节点为 b,-1 表示该位置没有节点。
template<class T>
void linkedBinaryTree<T>::makeTree(int n)
{
root = new binaryTreeNode<T>(1);
for(int i = 1; i <= n; i++)
{
binaryTreeNode<T>*p = find(root, i);
int a,b;
cin >> a >> b;
if (a != -1)
{
p->leftChild = new binaryTreeNode<T> (a);
}
if (b != -1)
{
p->rightChild = new binaryTreeNode<T> (b);
}
}
}
template<class T>
int linkedBinaryTree<T>::height(binaryTreeNode<T>*t)
{
if(t == NULL)
return 0;
int h1 = height(t->leftChild);
int h2 = height(t->rightChild);
if(h1 > h2)
return ++h1;
else
return ++h2;
}
template<class T>
int linkedBinaryTree<T>::number(binaryTreeNode<T>*t)
{
int x = 0;
if(t != NULL)
{
x = number(t->leftChild) + number(t->rightChild) + 1;
}
return x;
}
优秀原理解释博客
degradeFromParent(i)
——节点i与其父节点间的衰减量
if degradeFromParent(i) > 容忍值
,则不可能通过放置放大器来时信号的衰减不超过容忍值degradeToLeaf(i)
——从节点i到以i为根节点的子树的任一叶子的衰减量的最大值。
degradeToLeaf(i) = 0
degradeToLeaf(i) = max{degradeToLeaf(j) + degradeFromParent(j)
(j
是i
的孩子)下图中假定容忍值为3
树的二叉树描述
对于树 t 的每个节点x,x节点的
leftChild
指针指向x的第一个孩子
,x节点的rightChild
指针指向x的下一个兄弟
。
森林的二叉树表示
首先得到树林中每棵树(设有m棵树)的二叉树描述
然后,
第i棵作为第i-1棵树的右子树
并查集优质博客①
并查集优质博客②
初始时有n个元素,每个元素都属于一个独立的等价类
向R中添加新关系(a,b),执行combine(a,b)
classA = find(a); classB = find(b); if(classA != classB) unite(classA, classB);
查询(Find):查询两个元素是否在同一个集合中。
int find(int x) //查找x的教主
{
while(pre[x] != x) //如果x的上级不是自己(则说明找到的人不是教主)
x = pre[x]; //x继续找他的上级,直到找到教主为止
return x; //教主驾到~~~
}
合并(Union):把两个不相交的集合合并为一个集合。
//寻找x的代表元(即教主);
//寻找y的代表元(即教主);
//如果x和y不相等,则选一个人作为另一个人的上级,如此一来就完成了x和y的合并。
void union(int x,int y) //我想让虚竹和周芷若做朋友
{
int fx=find(x), fy=find(y);//虚竹的老大是玄慈,芷若MM的老大是灭绝
if(fx != fy) //玄慈和灭绝显然不是同一个人
pre[fx]=fy; //方丈只好委委屈屈地当了师太的手下啦
}
性能改进
提高最坏情况下的性能的方法
路径的缩短可以通过称为路径压缩(path compression)的过程实现。