树
本次整理树的部分大概包括:二叉树、二叉搜索树、AVL树、红黑树、B树、T树。
树的定义:
树是n(n>=0)个结点的有限集合。树有空树和非空树:空树(n=0),不包含任何结点;非空树(n>0),有且仅由一个根节点,根节点没有双亲,其余结点可以有多个子结点(孩子),其中每一个子结点又是一棵树(称为这棵树的子树)。没有孩子的结点称为叶结点。
第一部分:二叉树
1、定义:
也是结点的一个有限集合,该集合或为空,或是由一个根节点加上两棵分别称之为左子树和右子树的互不相交的二叉树组成[递归定义]。
2、性质
- 若层次从1开始,则二叉树的第 i 层最多有2^i -1个结点
- 高度为k的二叉树,最多有2^(k) -1个结点
- 对于任意一棵二叉树,若叶子结点的个数为n0,度为2的结点个数为n2,则有n0=n2+1
– n=n0+n1+n2
– n=2*n2+n1+1
满二叉树:每一层都是最满子树个数。
完全二叉树:设二叉树高为h,则共有h+1层,除第h层外,其余层都达到最大个数,第h层从右向左缺少若干个连续的结点。
3、二叉树的存储
一般来讲,二叉树有以下几种存储方案:
- 链式存储
- 静态表示
顺序存储:就是采用数组进行存储,从根节点开始,从左至右一次存储在一个存储单元中,若该结点为空,则不进行存储(但该下标位置元素置为特殊标记)。
因为一个结点与其子结点总是具有这样的关系,如一个结点若存储在下标i位置处,则其左右孩子就存储在2^i+1, 2^i+2的位置处(若有左右孩子的话)。
因此,二叉树的顺序存储看起来是这样的:
顺序存储方便随机访问与删除结点,但是随之有一个比较重要的问题就是,存储空间造成很大的浪费,比如这样:
可以看到,这就造成了空间的很大程度上的浪费。
链式存储
链式存储自然就是将二叉树的结点做成一个结构体,进行存储:
//二叉树
typedef char Elemtype;
typedef struct BtNode
{
Elemtype data;
struct BtNode* leftchild;
struct BtNode* rightchild;
}BtNode, & BinaryTree;
二叉树的创建(简单方式创建):
BtNode* Buynode()
{
BtNode* s=(BtNode*)malloc(sizeof(BtNode));
if(s==NULL) exit(1);
return s;
}
BtNode* CBtree()
{
BtNode* p=Buynode();
BtNode* tmp=p;
Elemtype elem;
cin>>elem;
while(elem1='#')
{
BtNode* s=NULL;
s=BuyNode();
s->data=elem;
s->leftchild=nullptr;
s->rightchild=nullptr;
p->next=s;
p=s;
}
return tmp->next;
}
利用先序和中序创建二叉树:
试想一下,一颗二叉树他们的先序遍历和中序遍历就隐藏着二叉树的信息。
在先序序列中,每次的第一个节点总是当前树的根节点,在中序遍历序列中,找到这个根节点,他左边的肯定是其左子树的结点的中序遍历,他右边的肯定是其右子树的中序遍历。
int FindIS(const char*is,int n,Elemtype key)
{
for(int i=0;i<n;++i)
{
if(is[i]==key)
return i;
}
return -1;
}
//ps是先序序列,is是中序序列
BtNode* CreatPI(const char* ps,const char* is,int n)
{
BtNode* s=NULL;//作为根节点返回
if (n >= 1)
{
s = Buynode();
s->data = ps[0];
int pos = FindIs(is, n, ps[0]);
if (pos == -1) exit(1);
s->leftchild = CreatPI(ps + 1, is, pos);
s->rightchild = CreatPI(ps + 1 + pos, is + pos + 1, n - pos - 1);
}
return s;
}
BtNode* CreateBTreePI(const char*ps,const char*is,int n)
{
if(ps==NULL || is==NULL || n<=0) return NULL;
return CreatPI(ps,is,n);
}
利用中序和后序创建二叉树:
同先序与中序创建二叉树同理,在后序序列中,每次的最后一个节点总是当前树的根节点,在中序遍历序列中,找到这个根节点,他左边的肯定是其左子树的结点的中序遍历,他右边的肯定是其右子树的中序遍历。以此类推…
BtNode* CreateIL(const char*is,const char*ls,int n)
{
BtNode* s=Buynode();
if (n >= 1)
{
s = Buynode();
s->data = ls[n - 1];
int pos = FindIs(is, n, ls[n - 1]);
if (pos == -1) exit(1);
s->leftchild = CreatIL(is, ls, pos);
s->rightchild = CreatIL(is + pos + 1, ls + pos, n - pos - 1);
}
return s;
}
BtNode* CreateIL(const char*is,const char*ls,int n)
{
if(is==NULL || ls==NULL || n<=0) return NULL;
return CreatIL(is,ls,n);
}
二叉树的遍历:
因为二叉树总是递归定义的,因此,递归遍历也是最简单的做法。
//先序遍历(递归)
void PreOrder(BtNode* root)
{
if(root==NULL) return;
cout<<root->data;
InOrder(root->leftchild);
InOrder(root->rightchild);
}
//中序遍历(递归)
void InOrder(BtNode* root)
{
if(root==NULL) return;
InOrder(root->leftchild);
cout<<root->data;
InOrder(root->rightchild);
}
//后序遍历(递归)
void PostOrder(BtNode* root)
{
if(root==NULL) return;
InOrder(root->leftchild);
InOrder(root->rightchild);
cout<<root->data;
}
//非递归先序遍历
void NonePreOrder(BtNode* root)
{
if(root==NULL) return;
stack<BtNode*> sta;
sta.push(root);
while(!sta.empty())
{
root=sta.top();
sta.pop();
cout<<root->data;
if(root->leftchild!=NULL)
{
sta.push(root->leftchild);
}
if(root->rightchild!=NULL)
{
sta.push(root->rightchild);
}
}
cout<<endl;
}
//非递归中序遍历
void NoneInOrder(BtNode* root)
{
if(root==NULL) return;
stack<BtNode*> sta;
while(!sta.empty() || root!=NULL)
{
while(root!=NULL)
{
sta.push(root);
roo=root->leftchild;
}
root=sta.top();
sta.pop();
cout<<root->data;
root=root->rightchild;
}
cout<<endl;
}
//非递归后序
void NonePostOrder(BtNode* root)
{
if(root==NULL) return;
stack<BtNode*> sta;
BtNode* tag=NULL;
while(!sta.empty() || root!=NULL)
{
while(root!=NULL)
{
sta.push(root);
root=root->leftchild;
}
root=sta.top();
sta.pop();
if(root->right!=NULL || root->rightchild==tag)//左右都已经访问了
{
cout<<root->data;
tag=root;
root=NULL;
}
else
{
sta.push(root);
root=root->rightchild;
}
}
cout<<endl;
}
上面的非递归遍历的方式很容易理解,不过,可以有另一种方式来遍历:
以后序遍历为例:
若入栈一次,就将这个节点的标记次数加一,当一个结点的标记次数为1时,说明它入栈一次,看看其左孩子空否,不空入栈;当标记次数为2是=时,证明其左子树已经遍历完成,观察其右子树;当标记次数为3时,说明其左右子树都已经遍历完成,访问此节点,完成后序遍历。
总结一下,就是利用出栈次数为3时打印后序遍历序列;利用出栈次数为2时打印中序;利用出栈次数为1时打印先序。
代码实现:
struct stkNode
{
BtNode* pnode;
int pos;
stkNode(BtNode* p, int c = 0) :pnode(p), pos(c) {}
};
//利用出栈次数为3时打印,后序
void stkNicePostOrder(BtNode* ptr)
{
if (ptr == NULL) return;
stack<stkNode> st;
st.push(stkNode(ptr));
while (!st.empty())
{
stkNode node = st.top(); st.pop();
if (++node.pos == 3)
{
cout << node.pnode->data;
}
else
{
st.push(node);
if (node.pos == 1 && node.pnode->leftchild != nullptr)
{
st.push(stkNode(node.pnode->leftchild));
}
else if (node.pos == 2 && node.pnode->rightchild != nullptr)
{
st.push(stkNode(node.pnode->rightchild));
}
}
}
cout << endl;
}
//利用出栈次数为2时打印,中序
void stkNiceInOrder(BtNode* ptr)
{
if (ptr == NULL) return;
stack<stkNode> st;
st.push(stkNode(ptr));
while (!st.empty())
{
stkNode node = st.top(); st.pop();
if (++node.pos == 2)
{
cout << node.pnode->data;
if (node.pnode->rightchild != nullptr)
{
st.push(stkNode(node.pnode->rightchild));
}
}
else
{
st.push(node);
if (node.pos == 1 && node.pnode->leftchild != nullptr)
{
st.push(stkNode(node.pnode->leftchild));
}
}
}
cout << endl;
}
层次遍历:从根节点开始,从上到下,从左到右一次遍历。因为是按照像顺序存储的那样的关系进行遍历,故用队列即可:
//层次遍历
void LevelOrder(BtNode* ptr)
{
if (ptr == nullptr) return;
queue<BtNode*> qu;
qu.push(ptr);
while (!qu.empty())
{
ptr = qu.front(); qu.pop();
cout << ptr->data;
if (ptr->leftchild != NULL)
{
qu.push(ptr->leftchild);
}
if (ptr->rightchild != NULL)
{
qu.push(ptr->rightchild);
}
}
}
如果是需要进行Z自行遍历呢?(按照图示方式遍历)
思想:用两个栈解决。
void ZLevelOrder(BtNode* ptr)
{
if (ptr == nullptr) return;
stack<BtNode*> ast, bst;
ast.push(ptr);
while (!ast.empty() || !bst.empty())
{
while (!ast.empty())
{
ptr = ast.top(); ast.pop();
cout << ptr->data;
if (ptr->leftchild != NULL) { bst.push(ptr->leftchild); }
if (ptr->rightchild != NULL) { bst.push(ptr->rightchild); }
}
while (!bst.empty())
{
ptr = bst.top(); bst.pop();
cout << ptr->data;
if (ptr->rightchild != nullptr) { ast.push(ptr->rightchild); }
if (ptr->leftchild != nullptr) { ast.push(ptr->leftchild); }
}
}
cout <<endl;
}
凭借二叉树的递归定义,还有很多计算二叉树性质的算法:例如计算二叉树节点个数、计算二叉树深度、二叉树的查询、查找双亲结点
//计算二叉树节点个数
int count(BtNode* root)
{
if(root==NULL)return 0;
return count(root->leftchild)+ count(root->rightchild)+1;
}
//计算二叉树深度
int Depth(BtNode* root)
{
if(root==NULL) return 0;
return max(Depth(root->leftchild),Depth(root->rightchild))+1;
}
//二叉树的查询val
BtNode* Find(BtNode* ptr, Elemtype val)
{
if (ptr == NULL || ptr->data == val) return ptr;
else
{
BtNode* p = Find(ptr->leftchild, val);
if (p == NULL)
{
p = Find(ptr->rightchild, val);
return p;
}
}
}
//查找双亲结点
BtNode* FindParent(BtNode* ptr, BtNode* child)
{
if (ptr == nullptr || child == ptr)return NULL;
else
{
BtNode* p = FindParent(ptr->leftchild, child);
if (p == NULL)
{
p = FindParent(ptr->rightchild, child);
return p;
}
}
}
上面提到了完全二叉树和满二叉树的概念,那么如何判断一棵树是不是完全二叉树或者满二叉树呢?
我们知道,如果是满二叉树,则每层都会达到其最大节点个数,因此,可以用两个队列来记录当前层的节点和下一层节点,当两层结点个数满足2倍关系时,则就是满二叉树。
bool IsFullBtTree(BtNode* root)
{
bool tag=true;
if(root==NULL) return tag;
queue<BtNode*> a,b;
a.push(root);
int s=1;
while(!a.empty() || !b.empty())
{
if(s!=a.size())
{
tag=false;
return tag;
}
while(!a.empty())
{
root=a.front();
a.pop();
if(root->leftchild) b.push(root->leftchild);
if(root->rightchild) b.push(root->rightchild);
}
s+=s;
if(s!=b.size())
{
tag=false;
return tag;
}
while(!b.empty())
{
root=b.front();
b.pop();
if(root->leftchild) a.push(root->leftchild);
if(root->leftchild) a.push(root->rightchild);
}
s+=s;
}
return tag;
}
那么完全二叉树呢,关键在于最后一层的节点个数是否是从右至左缺少连续个结点:其实就是将每个节点的左右孩子都入队列,一旦有空节点就进行判断。如果是一直连续的空节点则也是完全二叉树;一旦出现空节点后,又出现了有效结点,则不是完全二叉树。
bool IsCompleteBtTree(BtNode* root)
{
if(ptr==nullptr) return false;
queue<BtNode*> q;
q.push(root);
while(!q.empty())
{
root=q.front();
q.pop();
if(root==NULL) break;//一旦有空节点证明中间有空节点,直接退出
q.push(ptr->leftchild);
q.push(ptr->rightchild);
}
while(!q.empty())
{
if(q.front())
{
return false;
}
q.pop();
}
return true;
}
下一节:二叉排序树...