二叉树
有下面两个特殊的类型
满二叉树,定义为除了叶子结点外,所有结点都有 2 个子结点。
完全二叉树,定义为除了最后一层以外,其他层的结点个数都达到最大,并且最后一层的叶子结点都靠左排列。
二叉树的遍历
若采用递归方法,会自动利用栈保存调用时的中间变量或返回地址等信息。
若采用非递归方法,则需要自定义一或多个栈保存信息。
对链式存储的二叉树还可以通过线索化遍历,不需要用栈。
前序遍历、中序遍历、后序遍历、层级遍历
前序遍历
基本思想
若为空,结束遍历操作。否则:
1)访问根节点
2)前序遍历根的左子树
3)前序遍历根的右子树
前序遍历的递归算法
void BinaryTree :: PreOrder(BinTreeNode * current)
{
if(current ! = NULL){
cout << current->data; //输出此结点内容
Preorder(current->leftChild); //遍历左子树
Preorder(current->rightChild); //遍历右子树
}
}
利用栈的先序遍历的非递归算法
1、访问过的左子树结点压入栈中直到叶子结点
2、后存储的先处理
3、pop出栈进入右子树
eg:
ABD先后入栈
popD.r(NULL)
popB.r(!=NULL,p->data=E入,E.l=NULL)
popE.r(NULL)
popA.r(!=NULL,p->data=C入,C.l!=NULL,p->data=F入,F.l=NULL)
popC.r(!=NULL,P->data=G,G入,G.l=NULL)
popF.r(NULL) Empty_stack(NULL)
顺序:ABDECFG
void BinaryTree :: PreOrder(BinTreeNode *p= root)
//带默认参数值 root
{
SeqStack S; //顺序栈,不同的是栈内存储的元素是指针
while(p||! S.Empty_Stack( ))
if(p){
cout << p->data << endl;
S.Push _Stack(p); //预留p指针在栈中
p = p->leftChild;
}
else {
S.Pop_Stack(p);
p = p->rightChild;
} //左子树为空,进右子树
}
中序遍历的基本思想
若为空,结束遍历操作。否则:
1)中序遍历根的左子树
2)访问根节点
3)中序遍历根的右子树
中序遍历的递归算法
void BinaryTree :: InOrder(BinTreeNode *current)
{
if(current ! = NULL){
InOrder(current->leftChild);
cout << current->data;
InOrder(current->rightChild);
}
}
利用栈的中序遍历的非递归算法
void BinaryTree:: InOrder(BinTreeNode *p= root)//带默认参数值 root
{
SeqStack S; //顺序栈,不同的是栈内存储的元素是指针
while(p||! S.Empty_Stack( ))
if(p){
S.Push _Stack(p); //预留p指针在栈中
p = p->leftChild;
}
else {
S.Pop_Stack(p);
cout << p->data << endl;
p = p->rightChild;
} //左子树为空,进右子树
}
后序遍历的基本思想
若为空,结束遍历操作。否则:
1)后序遍历根的左子树
2)后序遍历根的右子树
3)访问根节点
后序遍历的递归算法
void BinaryTree :: PostOrder(BinTreeNode * current)
{
if(current ! = NULL){
PostOrder(current->leftChild);
PostOrder(current->rightChild);
cout << current->data;
}
}
利用栈的后序遍历的非递归算法
深度优先搜索(DFS) 模式,深度优先搜索改成 迭代循环 写法通常需借助 栈 来手动控制(力扣145)
void BinaryTree :: PostOrder(BinTreeNode *p= root)//带默认参数值root
{
SeqStack s1; //s1栈存放结点指针
SeqStack s2; //s2栈存放标志flag
int flag;
while(p ||! s1.Empty_Stack( ))
{
if(p){
flag=0;
s1.Push_Stack(p); //当前p指针第一次进栈
s2.Push_Stack(flag); //标志flag进栈
p = p->leftChild;
}
else {
s1.Pop_Stack(p); //p指针出栈
s2.Pop_Stack(flag); //标志flag出栈
if(flag==0) {
flag=1;
s1.Push_Stack(p); //当前p指针第二次进栈
s2.Push_Stack(flag); //标志flag进栈
p = p->rightChild;
} //左子树为空,进右子树
else { //flag为1说明是第二次出栈,访问结点
cout << p->data << endl;
p=NULL; //把p赋空,不然会继续三次进栈重复遍历
}
}
}
}
要用层级遍历的方式周游一棵树,首先访问树的根,然后依次向下层处理,按照从左到右的顺序访问每层的结点。层级遍历运用了广度优先的策略 。
二叉树遍历过程中,每个结点都被访问了一次,时间复杂度是 O(n)。接着,真正执行增加和删除操作的时间复杂度是 O(1)。
树数据的查找操作和链表一样,都需要遍历每一个数据去判断,所以时间复杂度是 O(n)。
二叉链表存储的二叉树
结点值类型为字符型
设建立时的输入序列为AB0D00CE00F00,按前序遍历次序输入,0是空结点。
BinTreeNode * BinaryTree ::CreateBinTree()
{ //以加入结点的先序序列输入,构造二叉链表
char ch;
BinTreeNode *T
cin>>ch;
if(ch=='0')
T=NULL; //读入0时,将相应结点置空
else {
T=new BinTreeNode; //生成结点空间
T->data=ch;
T->leftchild =CreateBinTree();
//构造二叉树的左子树
T->rightchild =CreateBinTree();
//构造二叉树的右子树
}
return T;
}
线索二叉树
二叉树中各结点在不同的次序下的前驱、后继差异较大。如何实现这一问题的快速求解?
1)遍历 通过指定次序遍历发现结点的前驱与后继(费时)
2)在结点中增设前驱和后继指针 (费空间)
3)利用二叉链表中的空指针域,改为其前驱和后继。但由于不容易区分,在结点中引入区分标志。
ltag=0:指示该结点的left。
ltag=1:指示该结点的前驱。
rtag=0:指示该结点的right。
rtag=1:指示该结点的后继。
中序线索化实现
1、二叉树线索化
遍历一棵二叉树,同时检查左右指针是否为空,若为空,修改为前驱或后继的线索。设pre始终指向刚访问的结点,若root指向当前结点,则pre指向它的前驱。
void ThreadTree::InThread(TheadNode * root) //递归中序线索化二叉树
{
static TheadNode * pre=NULL;
if(root! =NULL)
{ InThread(root->leftchild, pre); //中序线索化左子树
if(root->leftchild==NULL)
{
//建立前驱线索
root->ltag=1;
root->leftchild=pre;
}
if((pre)&&(pre->rightchild==NULL)
{ //建立后继线索
pre->rtag=1; pre->rightchild=root;
}
pre=root;
InThread(root->rightchild, pre); //中序线索化右子树
}
}
2、中序线索二叉树的中序遍历
对链式存储的二叉树遍历不使用栈:
先找到中序遍历到的第一个节点,然后找此结点的后继,再找后继的后继,以此类推。
二叉查找树
二叉查找树(二叉搜索树):
1)在二叉查找树中的任意一个结点,其左子树中的每个结点的值,都小于这个结点的值。右子树中每个结点的值,都大于这个结点的值。
2)在二叉查找树中,尽可能规避两个结点数值相等的情况。
3)对二叉查找树进行中序遍历,就可以输出一个从小到大的有序数据队列。
可以把二叉查找树当作是二分查找思想的树形结构实现
二叉查找树的查找操作
1)首先判断根结点是否等于要查找的数据,如果是就返回。
2)如果根结点大于要查找的数据,就在左子树中递归执行查找动作,直到叶子结点。
3)如果根结点小于要查找的数据,就在右子树中递归执行查找动作,直到叶子结点。
这样的“二分查找”所消耗的时间复杂度就可以降低为 O(logn)。
二叉查找树的插入操作
从根结点开始,如果要插入的数据比根结点的数据大,且根结点的右子结点不为空,则在根结点的右子树中继续尝试执行插入操作。直到找到为空的子结点执行插入动作。
二叉查找树插入数据的时间复杂度是 O(logn)。这里的时间复杂度更多是消耗在了遍历数据去找到查找位置上,真正执行插入动作的时间复杂度仍然是 O(1)。
二叉查找树的删除操作
1)要删除的结点是某个叶子结点,则直接删除,将其父结点指针指向 null 即可。
2)要删除的结点只有一个子结点,只需要将其父结点指向的子结点的指针换成其子结点的指针即可。
3)要删除的结点有两个子结点,有两种可行的操作方式。
第一种,找到这个结点的左子树中最大的结点,替换要删除的结点。
第二种,找到这个结点的右子树中最小的结点,替换要删除的结点。
Trie树(字典树)
主要用来查找变长字符串的组合。
从概念上说,Trie树中每一层的结点代表正在搜索的字符串中某个特定位置的所有可能字符。比如,紧跟在根结点下方的结点代表字符串中位置1处的所有可能字符,下一层的结点代表字符串位置2处的所有可能字符,以此类推。因此,要查找一个字符串,从根结点开始,每下一层都包含待查找的字符串的下一个字符。这个过程使得搜索的时间只与待查找字符串的长度相关,而与待查找的字符串总数无关。
eg.输入一个字符串,判断它在已有的字符串集合中是否出现过?
(假设集合中没有某个字符串与另一个字符串拥有共同前缀且完全包含的特殊情况,例如 deep 和 dee)
如,已有字符串集合包含 6 个字符串分别为,cat, car, city, dog,door, deep。输入cat,输出 true;输入 home,输出 false。
暴力法:根据字符串匹配算法,设字串集中有n个字串,平均长度为m,新来的一个字符串,需要与每个字符串的每个字符进行匹配。时间复杂度O(mn),但其中也存在着很多无效匹配。
对字符建立一个树状结构:(将字串集合前缀合并),每个根结点到叶子结点的链条就是一个字符串
这个树结构也称作 Trie 树,或字典树。
1)根结点不包含字符;
2)除根结点外每一个结点都只包含一个字符;
3)从根结点到某一叶子结点,路径上经过的字符连接起来,即为集合中的某个字符串。
解决这个问题:
1)根据候选字符串集合,建立字典树。这需要使用数据插入的动作。
2)对于一个输入字符串,判断它能否在这个树结构中走到叶子结点。如果能,则出现过。
Java实现
二叉树的层序遍历 借助于队列来实现 借助队列的先进先出的特性
* 首先将根节点入队列 然后遍历队列。
* 首先将根节点打印出来,接着判断左节点是否为空 不为空则加入队列
public static void levelTraverse(Node root) {
if (root == null) {
return;
}
LinkedList<Node> queue = new LinkedList<Node>();
Node current = null;
queue.offer(root); // 根节点入队
while (!queue.isEmpty()) { // 只要队列中有元素,就可以一直执行,非常巧妙地利用了队列的特性
current = queue.poll(); // 出队队头元素
System.out.print("-->" + current.data);
// 左子树不为空,入队
if (current.leftChild != null)
queue.offer(current.leftChild);
// 右子树不为空,入队
if (current.rightChild != null)
queue.offer(current.rightChild);
}
}