树(tree)
首先可以想象,现实中的树是由树根、茎干、树枝、树叶组成的,树的营养是由树根出发、通过茎干与树枝不断传递,最终到达树叶的。
在数据结构中,树则是用来概括这种传递关系的一种数据结构。
定义
结点(node):树枝分叉处、树叶、树根的抽象
根节点(root):树根的抽象,对一棵树来说最多存在一个根节点
叶子结点(leaf):树叶的抽象,且叶子结点不再延申出新的结点
边(edge):把茎干和树枝的统一抽象,且一条只用来链接两个结点(一个端点一个)(特指二叉树)
树中的结点不能被边连接成环。
在数据结构中,一般把根结点置于最上方,然后向下延伸出若干条边到达子结点(child)(从而向下形成子树(subtree)),而子结点又向下延伸出边并连接一些结点…直至到达叶子结点,看起来就像是把现实中的树颠倒过来的样子。
常用性质
二叉树
二叉树的递归定义:
一是递归边界,二是递归式。
二叉树中任何一个结点的左子树既可以是一棵空树,也可以是一棵有左子树和右子树的二叉树;结点的右子树也既可以是一棵空树,又可以是一棵有左子树和右子树的二叉树,这样直到递归边界,递归定义结束。
注意:二叉树与度为2的树的区别。对树来说不区分左右顺序的,因此度为2的树只能说明树中每个结点的子结点个数不超过2。而二叉树虽然也满足每个结点的子结点个数不超过2,但它的左右子树是严格区分的,不能随意交换左子树和右子树的位置,是最主要的区别。
特殊的二叉树
几个概念:层次、孩子结点、父亲结点、兄弟结点、祖先结点、子孙结点
二叉树的存储结构与基本操作
struct node {
typename data; //数据域
node* lchild; //指向左子树根结点的指针
node* rchild; //指向右子树根结点的指针
};
由于二叉树建树前根节点不存在,因此其地址一般设为NULL:
node* root = NULL;
如果需要新建结点(例如往二叉树中插入结点的时候),就可以使用下面的函数:
node* newNode(int v) {
node* Node = new node; //申请一个node型变量的地址空间
Node->data = v; //结点权值为v
Node->lchild = Node->rchild = NULL; //初始状态下没有左右孩子
return Node; //返回新建结点的地址
}
二叉树常用操作
二叉树结点的查找、修改
查找操作是指在给定数据域的条件下,在二叉树中找到所有数据域为给定数据域的结点,并将它们的数据域修改为给定的数据域。
递归式:指对当前结点的左子树和右子树分别递归
递归边界:当前结点为空时到达死胡同
void search(node* root, int x, int newdata) {
if(root == NULL) {
return; //空树,死胡同(递归边界)
}
if(root->data == x) { //找到数据域为x的结点,把它修改成newdata
root->data = newdata;
}
search(root->lchild, x, newdata); //往左子树搜索x(递归式)
search(root->rchild, x, newdata); //往右子树搜索x(递归式)
}
二叉树结点的插入
二叉树结点的插入位置就是数据域在二叉树中查找失败的位置。由于这个位置是确定的,因此在递归查找的过程中一定是只根据二叉树的性质来选择左子树或右子树中的一棵子树进行递归,且最后到达空树(死胡同)的地方就是查找失败的地方,也就是结点需要插入的地方。
//insert函数将在二叉树中插入一个数据域为x的新结点
//注意根结点指针root要使用引用,否则插入不会成功
void insert(node* &root, int x) {
if(root == NULL) { //空树,说明查找失败,也即插入位置(递归边界)
root = newNode(x);
return;
}
if(由二叉树的性质,x应该插在左子树) {
insert(root->lchild, x); //往左子树搜索(递归式)
} else {
insert(root->rchild, y); //往右子树搜索(递归式)
}
}
根结点指针root使用了引用&,这么做是,在insert函数中新建了结点,并把新结点的地址赋给了当层的root。不使用引用就不能把新结点接导二叉树上面。(改变root本身,前面search函数知识修改指针root指向的内容)
判断是否加引用:如果函数中需要新建结点,即对二叉树的结构做出修改,就需要加引用;如果知识修改当前已有结点的内容,或仅仅是遍历树,就不用加引用。
注意:在新建结点之后,务必令新结点的左右指针域为NULL,表示这个新结点暂时没有左右子树。
二叉树的创建
二叉树的创建其实就是二叉树结点的插入过程。
把需要插入的数据存储在数组中,然后再将它们使用insert函数一个个插入二叉树中,并最终返回根结点的指针root。熟悉后可以直接在建立二叉树的过程中边输入数据边插入。
node* Create(int data[], int n) {
node* root = NULL; //新建空根结点root
for(int i = 0; i < n; i++) {
insert(root, data[i]); //将data[0]~data[n-1]插入二叉树中
}
return root; //返回根结点
}
二叉树存储结构
注意:递归边界为root == NULL而不是 * root == NULL
原因是如果子树是空树,那么root一定是NULL,表示这个结点不存在。而*root的含义是获取地址root指向的空间的内容,但这无法说明地址root是否为空,也即无法确定是否存在这个结点,因此 * root == NULL的写法是错误的。这个区别也是结点地址为NULL和结点内容为NULL的区别(也相当于结点不存在与结点存在但是没有内容的区别)
完全二叉树的存储结构
对完全二叉树当中的任何一个结点(设编号为x),其左孩子的编号一定是2x,而右孩子的编号一定是2x+1。
完全二叉树可以通过建立一个大小为2的k次方的数组来存放所有结点的信息,其中k为完全二叉树的最大高度,且1号位存放的必须是根结点。
数组中元素存放的顺序恰好位该完全二叉树的层序遍历序列,而判断某个结点是否为叶结点的标志为:该结点(记下标为root)的左子结点的编号root * 2大于结点总个数n;判断某个结点是否为空结点的标志:该结点下标root大于结点总个数n。