标语:学习就是个不断搬砖和不断复习的过程,少数强者可以创新,多数人逐渐将其遗忘。
小编带大家学习数据结构中的二叉树,我们这里的实现主要是用C语言去实现的,当然也有C++的语法,用基础的语言有助于我们更好理解数据结构。
首先我们要知道数据结构中二叉树是个非线性结构,我们了解过数组,顺序表,链表等线性结构,对于计算机而言,只用线性结构存储数据是远远不够的,我们还需要非线性结构来保存,今天所说二叉树就是最简单的一种非线性结构。
学习二叉树我们想,为什么叫做二叉树呢?类比树,我们这个结构是不是有树根树杈呢?二叉是不是因为每个树干有两个树杈呢?基于此我们来看这个数据结构的代码研究。
- 首先一个树的树根不能少,树根又是由树节点构成的,我们就能很快看到这个树根的构成;
typedef struct BinTreeNode
{
DataType data;
struct BinTreeNode* LeftChild;
struct BinTreeNode* RightChild;
}BinTreeNode;//创建节点
typedef struct Bintree
{
BinTreeNode* root;
}BinTree;
- 当这个树雏形出来了后,我们需要做的就是完善这个二叉树,我们需要将这个树初始化和进行一系列的操作;
我们先来看二叉树的创建 :
我们可以细分成两种方法,第一种是有返回值的,返回的是这个二叉树的根节点指针,还有一种方法是没有返回值的,代表创建了这个二叉树,两种创建方法都很常用。
我们规定输入“#”代表空,我们创建一定是以递归的方式进行创建的。来看第一种无返回值的创建
void BinTreeCreat_1(BinTreeNode** t)//记住我们传的是树根的地址,这个**你应该知道了奥
{
DataType item;
scanf("%c", &item);
if (item == '#')
{
*t = NULL;
}
else
{
*t = (BinTreeNode*)malloc(sizeof(BinTreeNode));
assert(*t != NULL);
(*t)->data = item;
BinTreeCreat_1(&((*t)->LeftChild));
BinTreeCreat_1(&((*t)->RightChild));
}
}
再来看有返回值的创建
BinTreeNode* BinTreeCreate_2()
{
DataType item;
scanf("%c", &item);
if (item == '#')
{
return NULL;
}
else
{
BinTreeNode* p = (BinTreeNode*)malloc(sizeof(BinTreeNode));
assert(p != NULL);
p->data = item;
p->LeftChild = BinTreeCreate_2();
p->RightChild = BinTreeCreate_2();
return p;
}
}
接下来我们就来看这个二叉树的创建和初始化,上面给出了两种创建的方法,随便选一个就可以。
创建:
void BinTreeCreate(BinTree* t)
{
(*t).root = BinTreeCreate_2();
}
当然我们创建的方法很多,在这就不一一赘述,比如说先用个数组来保存这个你输入的值,然后用这个数组来创建二叉树,原理还是一样的,多加了个参数罢了。
3. 接下来我们就进入一个常见的环节:二叉树的遍历
我们对二叉树前中后序遍历耳熟能详,我们来一起研究一下这个遍历的递归和非递归形式。
首先是先序遍历:规则就是每次优先遍历根节点
我们来看代码
void PreOrder(BinTree *t)
{
_PreOrder(t->root);
}
void _PreOrder(BinTreeNode *t)
{
if (t != NULL)
{
printf("%c ", t->data);
_PreOrder(t->LeftChild);
_PreOrder(t->RightChild);
}
}
再来看前序遍历的非递归形式,这时候就要引入我们的栈结构了,当然我们可以用C语言去做个栈结构出来,但是没有必要,我们C++中已经有封装好的栈,所以我们只需要调动就可以,为什么要用这个栈呢?因为我们要将路径记录下来**,这个循环代替递归的条件就是用栈顶循环起来的,取栈顶相当于是退回父节点**
stack <BinTreeNode*> s;
在非递归中就用到了个很精妙的算法,就是循环来代替递归。需要注意的就是这个顺序问题,因为稍有不慎就顺序混乱。我们再来分析,前序遍历,优先遍历根然后左右,但是我们在压栈的时候会出现一个情况就是先压栈的后出栈,所以顺序我们要注意,还有就是删除栈顶元素的位置要放对。来看代码
void PreOrder(BinTree * t)
{
_PreOrderNoR(t->root);
}
void _PreOrderNoR(BinTreeNode *t)
{
BinTreeNode* p;
if (t != NULL)
{
s.push(t);
while (!s.empty())
{
p = s.top();
s.pop();
printf("%c ", p->data);
if (p->RightChild != NULL)//为啥是这个顺序呢?后进栈的在栈顶
{
s.push(p->RightChild);
}
if (p->LeftChild != NULL)
{
s.push(p->LeftChild);
}
}
}
}
我们来看中序遍历
递归形式
这个和前面递归形式一样,就是将这个顺序颠倒
void InOrder(BinTree *t)
{
_InOrder(t->root);
}
void _InOrder(BinTreeNode *t)
{
if (t != NULL)
{
_InOrder(t->LeftChild);
printf("%c ", t->data);
_InOrder(t->RightChild);
}
}
非递归形式
在中序遍历的时候,非递归的过程就显得容易一点,我们首先是将这个二叉树从根节点到最左边节点都压入栈中,然后打印出当前节点的值并且取栈顶退回上个节点,之后到上个节点判断这个父节点有没有右子树,如果有就将到新的分支中。
来看代码
void _InOrderNoR(BinTreeNode *t)
{
BinTreeNode* p;
stack<BinTreeNode*>dp;
do
{
while (t != NULL)
{
dp.push(t);
t = t->LeftChild;
}
p = dp.top();
dp.pop();
printf("%c ", p->data);
if (p->RightChild != NULL)
t = p->RightChild;
}while(!dp.empty()||t!=NULL)
}
void InOrderNoR(BinTree *t)
{
_InOrderNoR(t->root);
}
二叉树的后序遍历
首选还是看递归遍历,这个操作无非就是颠倒一下递归函数位置,来看代码
void PostOrder(BinTree *t)
{
_PostOrder(t->root);
}
void _PostOrder(BinTreeNode *t)
{
if (t != NULL)
{
_PostOrder(t->LeftChild);
_PostOrder(t->RightChild);
printf("%c ", t->data);
}
}
谈到二叉树的后续遍历,可以说是非递归遍历中最难写也是最难理解的代码了,我们有了对前两个非递归遍历的理解,后续遍历我们压栈的时候根节点一定是压在最下层的,怎么实现呢?我们先找到最左边的节点,在寻找过程中将遍历的节点压入栈中,然后我们依次退节点看有没有右树,最后再取栈顶。这时候有个问题就是返回上个节点如果是从左子树返回会判断是否有有右子树,那么从右子树返回是不是该到根节点了呢?这两个返回怎么区分从哪返回的呢?这时候就要加个指针作为标志位,这个标志位记录是从哪边返回来的。
来看代码
void _PostOrderNoR(BinTreeNode *t)
{
if (t != NULL)
{
BinTreeNode*p, *prev = NULL;
stack<BinTreeNode*> dq;
do
{
while (t != NULL)
{
dq.push(t);
t = t->LeftChild;
}
p = dq.top;
if (p->RightChild == NULL || p->RightChild == prev)
{
dq.pop();
printf("%c ", p->data);
prev = p;
}
else {
t = p->RightChild;
}
} while (!dq.empty());
}
}
void PostOrderNoR(BinTree *t)
{
_PostOrderNoR(t->root);
}
void PostOrder(BinTree *t)
{
_PostOrder(t->root);
}
这就是数据结构中二叉树的前中后续非递归遍历和递归遍历的写法,递归简化思维,非递归简化计算,看我们怎么去用这些代码实现我们想要的结果。
小编心得:学习这件事情,一辈子不能抛弃,一旦你抛弃了学习,就意味着你选择了平庸。