数据结构之树
树的定义
- 树(Tree)是n个节点的有限集。n=0时称为空树,在任意一棵非空树中:
- 有且仅有一个特定的称为**根(Root)**的节点;
- 当n > 1时,其余结点可分为m(m > 0)个互不相交的有限集T1,T2,…Tm,其中每个集合又是一棵树,并且称为根的子树(SubTree)。
- 结点拥有的子树数称为结点的度(Degree)。度为0的结点为叶结点,其他结点为内部结点。结点的子树的根称为结点的孩子,该结点称为孩子的双亲,同一个双亲的孩子互为兄弟。
- 结点的层次从根开始,每往下延伸子树一次,就增加一层。
- 森林(Forest) 是m棵互不相交的树的集合。
树的存储结构
双亲表示法
以一组连续空间存储树的结点,在每个结点中,附设一个指示器指示其双亲节点在数组中的位置:
data | parent |
---|
结构定义:
#define MAX_TREE_SIZE 100
typedef int TElemType;
typedef struct PTnode
{
TElemType data;
int parent;
}PTnode;
typedef struct name
{
PTnode nodes[MAX_TREE_SIZE];
int r,n;//根的位置和结点数目
}PTree;
孩子表示法
把每个结点的孩子结点排列起来,以单链表作存储结构,则n个结点有n个孩子链表,如果是叶子结点则此单链表为空,然后n个头指针又组成一个线性表,采用顺序存储结构,放入一个一维数组中。
两个结点结构,一个孩子链表的孩子结点:
child | next |
---|
另外一个是表头数组的表头结点:
data | firstchild |
---|
结构代码:
#define MAX_TREE_SIZE 100
typedef struct CTnode
{
int child;
struct CTNode *next;
}*ChildPtr;
typedef struct
{
TElemType data;
ChildPtr firstchild;
}CTBox;
typedef struct
{
CTBox nodes[MAX_TREE_SIZE];
int r,n;////根的位置和结点数目
};
孩子兄弟表示法
设置两个指针,分别指向节点的第一个孩子和此结点的右兄弟。
data | fisrtchild | rightsib |
---|
结构代码:
#define MAX_TREE_SIZE 100
typedef struct CSnode
{
TElemType data;
struct CSnode *firstchild,*rightsib;
}CSnode,*CSTree;
这种做法最大的好处就是把树变成了二叉树结构,其实前面这些东西知道就行,下面的二叉树才是重中之重。
二叉树的定义
二叉树(Binary Tree)是n个结点的有限集合,该集合为空,或由一个根节点和两棵互不相交的、分别称为根结点的左子树和右子树的二叉树组成。
二叉树的分类与性质
分类
有一些具有特殊性质的二叉树:
- 斜树:所有结点只有左子树或只有右子树。(其实我们发现斜树就是线性表)
- 满二叉树:所有分支结点都存在左子树和右子树,并且所有叶子都在同一层上。
- 完全二叉树:对一棵具有n个结点的二叉树按层序和从左到右进行编号,编号为i的结点与同样深度的满二叉树中编号为i的结点的位置相同。
性质
- 性质1:在二叉树的第i层上至多有 2 i − 1 2^{i-1} 2i−1个结点。
- 性质2:深度为k的二叉树之多有 2 k − 1 2^{k} - 1 2k−1个结点。
- 性质3:对任何一棵二叉树T,如果其终端结点数为 n 0 n_0 n0,度为2的结点数为 n 2 n_2 n2,则 n 0 = n 2 + 1 n_0 = n_2 + 1 n0=n2+1
- 性质4☆:具有n个结点的完全二叉树的深度为 ⌊ l o g 2 n ⌋ + 1 \lfloor log_2 n \rfloor + 1 ⌊log2n⌋+1
- 性质5:对于一棵有n个结点的完全二叉树的结点按层序编号,对任意结点i有:
- 如果i=1,则结点为根,无双亲;如果i>1,则其双亲是结点 ⌊ i / 2 ⌋ \lfloor i / 2\rfloor ⌊i/2⌋
- 如果2i > n,则结点i无左孩子,否则其左孩子是结点2i
- 如果2i+1>n,则结点i无右孩子,否则其右孩子是结点2i+1
二叉树的存储结构
对于二叉树而言,顺序存储的适用性不强,会出现很多存储空间的浪费,所以只考虑链式存储结构,设计一个数据域和两个指针域,这种链表也称为二叉链表:
lchild | data | rchild |
---|
结构代码:
#define MAX_TREE_SIZE 100
typedef struct BiTNode
{
TElemType data;
struct BiTNode *rchild,*lchild;
}BiTNode,*BiTree;
二叉树的遍历☆
二叉树的遍历非常重要,非常多的题目都在二叉树的遍历基础上,这一部分必须要身体力行,用递归和迭代都给他好好遍历一下。
二叉树的遍历是指从根节点出发,按照某种次序依次访问二叉树中的所有结点,使得每个结点被访问一次且仅被访问一次。
既然要遍历二叉树,就必须要先建立二叉树:
struct TreeNode {
char data;
TreeNode *lchild;
TreeNode *rchild;
TreeNode(int x) : data(x), lchild(NULL), rchild(NULL) {}
};
TreeNode* CreateTree(string s, int &pos)
{
if(s[pos] == '#' || pos >= s.length())
return NULL;
TreeNode *T = new TreeNode(s[pos++]);
T->lchild = CreateTree(s, pos);
pos++;
T->rchild = CreateTree(s, pos);
return T;
}
PS:之后的代码我会用c++写,c实在不熟练(
前序遍历
Rule:若二叉树为空,则空操作返回,否则先访问根节点,然后前序遍历左子树,再前序遍历右子树。
Code:
void PreOrderTraverse(TreeNode *root)
{
if(root == NULL) return;
cout << root -> data << endl;
PreOrderTraverse(root -> lchild);
PreOrderTraverse(root -> rchild);
return;
}
中序遍历
Rule:若二叉树为空,则空操作返回,否则先访问根节点的左子树,然后访问根节点,再访问右子树。
void InOrderTraverse(TreeNode *root)
{
if(root == NULL) return;
PreOrderTraverse(root -> lchild);
cout << root -> data << endl;
PreOrderTraverse(root -> rchild);
return;
}
后序遍历
Rule:若二叉树为空,则空操作返回,否则按从左到右先叶子结点的方式遍历左右子树,最后访问根节点
void PostOrderTraverse(TreeNode *root)
{
if(root == NULL) return;
PreOrderTraverse(root -> lchild);
PreOrderTraverse(root -> rchild);
cout << root -> data << endl;
return;
}
PS:迭代的遍历方式过于简单,之前刷leetcode的时候也写过一些迭代的方法,如果有兴趣可以看一下。传送门
层序遍历
Rule:若二叉树为空,则空操作返回,否则从树的第一层,也就是根结点开始访问,从上而下逐层遍历,在同一层中,按从左到右的顺序对结点逐个访问
层序遍历的问题用队列比较好解决:
void SeqOrderTraverse(TreeNode *root)
{
if(root == NULL) return;
queue<TreeNode*> s;
s.push(root);
while(!s.empty())
{
TreeNode *T = s.front();
cout << T -> data << endl;
s.pop();
if(T->lchild) s.push(T->lchild);
if(T->rchild) s.push(T->rchild);
}
return ;
}
以上遍历的主程序代码:
int main()
{
string s = "AB#D##C##";
TreeNode *Mytree;
int pos = 0;
Mytree = CreateTree(s, pos);
PreOrderTraverse(Mytree);
InOrderTraverse(Mytree);
PostOrderTraverse(Mytree);
SeqOrderTraverse(Mytree);
return 0;
}
线索二叉树
我们可以发现,在普通的二叉树中,有很多空指针,浪费了很多空间,所以设计出线索二叉树来充分利用这些空间,在线索二叉树中,存在指向前驱和后继的指针,这些指针称为线索。
结构实现:
lchild | ltag | data | rtag | rchild |
---|
其中:
- ltag为0时指向该结点的左孩子,为1时指向该结点的前驱。
- rtag为0时指向该结点的右孩子,为1时指向该结点的后继。
结构代码:
typedef enum {Link,Thread} PointerTag;
struct ThreadTreeNode {
char data;
TreeNode *lchild;
TreeNode *rchild;
PointerTag Ltag;
PointerTag Rtag;
TreeNode(int x) : data(x), lchild(NULL), rchild(NULL),Ltag(0),Rtag(0) {}
};
因为前驱和后继的信息只有在遍历二叉树的时候才能得到,所以我们要考一次遍历来对这种二叉树进行线索化,具体做法是设置一个全局变量,始终指向前驱结点,通过中序遍历实现线索化,Code:
ThreadTreeNode *Pre;
void InThreading(ThreadTreeNode *root)
{
if(root == NULL) return;
InThreading(root -> lchild);
if(!(root -> lchild))
{
root -> Ltag = Thread;
root -> lchild = Pre;
}
if(!(Pre -> rchild))
{
Pre -> Rtag = Thread;
Pre -> rchild = root;
}
Pre = root;
InThreading(root -> rchild);
return;
}
赫夫曼树
赫夫曼树实在太常用了,所谓赫夫曼树就是对于每个结点都带权的情况,带权路径长度WPL最小的二叉树,在编码等方面非常普遍的使用这种数据结构,这里就再回忆一遍赫夫曼树的构造过程吧:
- 根据给定的n个权值{ w 1 , w 2 , w 3 , . . . , w n w_1,w_2,w_3,...,w_n w1,w2,w3,...,wn} 构成二叉树的集合F={ T 1 , T 2 , T 3 , . . . , T n T_1,T_2,T_3,...,T_n T1,T2,T3,...,Tn },其中每棵二叉树的左右子树均为空。
- 在F中选取两棵根结点的权值最小的树作为左右子树构造一棵新的二叉树,其权值为左右子树根结点权值之和。
- 在F中删除这两棵树,并把新的二叉树加入F。
- 重复2-3,直到F中仅含一棵树,这棵树就是赫夫曼树。
二叉查找树
二叉查找树定义:
- 若左子树不空,则左子树上所有结点的值均小于它的根结点的值;
- 若右子树不空,则右子树上所有结点的值均大于或等于它的根结点的值;
- 左、右子树也分别为二叉排序树;
二叉树的插入和删除:
二叉查找树的插入过程:
- 若当前的二叉查找树为空,则插入的元素为根节点;
- 若插入的元素值小于根节点值,则将元素插入到左子树中;
- 若插入的元素值不小于根节点值,则将元素插入到右子树中。
二叉查找树的删除,分三种情况进行处理:
- p为叶子节点,直接删除该节点,再修改其父节点的指针(注意分是根节点和不是根节点),如图a;
- p为单支节点(即只有左子树或右子树)。让p的子树与p的父亲节点相连,删除p即可(注意分是根节点和不是根节点;
- 和右子树均不空。找到p的后继y,因为y一定没有左子树,所以可以删除y,并让y的父亲节点成为y的右子树的父亲节点,并用y的值代替p的值;或者方法二是找到p的前驱x,x一定没有右子树,所以可以删除x,并让x的父亲节点成为y的左子树的父亲节点。
平衡二叉树
在上面二叉查找树的定义中,我们可以观察到,二叉查找树有可能会发展成链表,极大增加了查找的时间复杂度,所以科学家又提出了平衡二叉树(AVL)的思想:在AVL中任何节点的两个儿子子树的高度最大差别为1,所以它也被称为高度平衡树,n个结点的AVL树最大深度约1.44log2n。查找、插入和删除在平均和最坏情况下都是O(logn)。增加和删除可能需要通过一次或多次树旋转来重新平衡这个树。具体的插入和旋转过程可以看浙大老师的这个视频,讲得很好。
红黑树
红黑树也是一种二叉查找树,性质如下:
- 性质1:每个节点要么是黑色,要么是红色。
- 性质2:根节点是黑色。
- 性质3:每个叶子节点是黑色。
- 性质4:每个红色结点的两个子结点一定都是黑色。
- 性质5:任意一结点到每个叶子结点的路径都包含数量相同的黑结点。
文字在视频面前是薄弱的,就不赘述浪费时间了,这个视频讲的非常好(没错我我就是优质视频的搬运工2333)