0%

数据结构的二叉查找树之非递归遍历

对于二叉查找树,前面有博文介绍:关于二叉树的三种遍历方式介绍,参见前面链接。二叉树的遍历关键在于:出的去要回得来。以中序遍历为例,先遍历左子树,再访问根节点,最后遍历右子树,当遍历完左子树,即发现访问到左子树的叶子节点了,下一步便是访问该叶子节点的根节点,再右子树。接下来还需要一层一层的回到根节点。这就给了我们提示,如何在非递归的情况下,遍历二叉树:保存回去的路或搭建回去的桥。这里先讨论第一种,提供栈实现迭代的方法,即直接导用STL 中的 stack 容器保存回去的路实现。需要注意的是压栈的顺序将直接影响到树的下一层压栈的顺序,因为栈这一数据结构每次只能提取栈顶的元素。

代码其余部分下载:http://download.csdn.net/detail/yeswenqian/6977693

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
一、先序遍历(根 -- 左子树 -- 右子树)

方法一:其实现思想为:

1. 访问节点 N ,并打印数据,然后将该节点入栈;

2. 判断该节点的左孩子是否为空,不为空则将该节点置为节点 N ,若为空,则取出栈顶节点并出栈操作,然后将该节点的右孩子置为节点 N ,循环 1-2
3. 直到节点 N 为空或是栈中元素为空,则遍历完成。
/*

* 6 先序遍历: 6 2 1 4 3 8 10

* / \

* 2 8 中序遍历: 1 2 3 4 6 8 10

* / \ \

* 1 4 10 后序遍历: 1 3 4 2 10 8 6

* /

* 3
*/
/*先序遍历*/
void printPreorder(BinaryTree *pBinaryTree)
{
if (NULL == pBinaryTree)
return ;

stack<BinaryTree*> S; //#include<stack> STL
BinaryTree *pS = pBinaryTree;

while ((pS != NULL) || !S.empty())
{
while (pS)
{
cout << pS->value << endl;
S.push(pS); //事实上每个节点都有入栈过程
pS = pS->left_child;
}

if (!S.empty())
{
pS = S.top();
S.pop();
pS = pS->right_child;
}

}
}
方法二:

1. 将根节点压栈;

2. 取出位于栈顶的节点 N 并打印数据,然后出栈操作;

3. 如果节点 N 有右孩子,则将右孩子节点压栈,随后如果节点 N 有左孩子,则将左孩子节点压栈;

4. 最后在栈不为空的情况下,循环 2-4
/*先序遍历*/
void printPreorder(BinaryTree *pBinaryTree)
{
if (NULL == pBinaryTree)
return;

stack<BinaryTree*> S;
BinaryTree *pS = pBinaryTree;

S.push(pS);

while (!S.empty())
{
pS = S.top();
cout << pS->value << endl;
S.pop();

if (pS->right_child) //栈先进后出:先压右子树
S.push(pS->right_child);

if (pS->left_child)
S.push(pS->left_child);

}
}
二、中序遍历(左子树 -- 根 -- 右子树)

方法一:实现思想:

1. 将节点 N 压栈,如果该节点存在左孩子则将其置为 N,重复操作;(此时栈中均为左孩子节点(栈底根节点))

2. 如果 N 节点不存在左孩子,则取出栈顶元素并出栈操作,如果N节点有右孩子则将其置为N,并压栈处理;

3. 循环1-2,直至N为空或栈为空。
/*中序遍历*/
void printInorder(BinaryTree *pBinaryTree)
{
if (NULL == pBinaryTree)
return;

stack<BinaryTree*> S;
BinaryTree *pS = pBinaryTree;

while ((pS != NULL) || !S.empty())
{
while (pS)
{
S.push(pS);
pS = pS->left_child;
}

pS = S.top();
cout << pS->value << endl;

if (!S.empty())
{
S.pop();
pS = pS->right_child;
}

}
}
方法二:
中序遍历的特点是根节点(父节点)是在中间段访问,就是先访问左孩子节点,那么压栈顺序便是右孩子先入栈然后父节点,最后就是左节点。但必须保证每次入栈的顺序都是一样的(右-根-左),上一级的孩子节点,到了下一层就成了父节点,那么上一层压入的节点又得弹出来,然后等到其右孩子节点入栈后,再次入栈,然后压入其左孩子节点,如此循环,期间父节点(这里叶子节点也看作是特殊的父节点,因为其也存在访问其左右孩子的过程)必须有两次入栈过程,所以设定标记记录节点入栈的次数情况。

/*中序遍历*/
void printInorder(BinaryTree *pBinaryTree)
{
if (NULL == pBinaryTree)
return;

stack<pair<BinaryTree*, bool>> S;
BinaryTree *pS;
bool isFirst;
S.push(make_pair(pBinaryTree, true));

while (!S.empty())
{
pS = S.top().first;
isFirst = S.top().second;
S.pop();

if (isFirst)
{
if (pS->right_child)
S.push(make_pair(pS->right_child, true));

S.push(make_pair(pS, false));

if (pS->left_child)
S.push(make_pair(pS->left_child, true));
}
else
{
cout << pS->value << endl;
}
}

}
三、后序遍历(左子树 -- 右子树 -- 根)
后序遍历和其余两种遍历方式有所不同,因为根节点是最后访问的。

/*

* 6
* / \
* 2 8
* / \ \ 后序遍历: 1 3 4 2 10 8 6
* 1 4 10
* /
* 3
*/
方法一:

先访问左子树,这就需要先把节点的左孩子入栈(循环),直到节点没有左孩子(栈中元素6,2,1),然后同样方式访问该节点的右孩子(右子树),如果没有右孩子时可取栈顶元素并出栈(栈中元素6,2),然后取栈顶元素2,同样方式访问节点2的右孩子,但是前面取栈顶元素2的时候却不能将节点2出栈,因为该节点的右子树还未访问。同样是栈顶元素那什么时候可以出栈呢?节点没有右孩子的时候么,如果这样的话那么节点2什么时候出栈勒。事实是当该节点两次出现(不是第一次)在栈顶的时候,因为鉴于后序遍历的特点(左-右-根),这样访问父节点的前提是该父节点的左孩子和右孩子均已访问完毕,访问完左孩子节点后(左孩子及节点出栈),父节点出现在栈顶,再转向访问右孩子,访问完毕后(右孩子节点出栈),这样父节点再一次出现在栈顶,这时便可以访问了,访问完后,该父节点也就可以出栈了。
所以我们就需要一个变量来记录某元素出现在栈顶的次数是否是第一次,当不是第一次出现时,表明该节点的左右孩子均已访问完毕(叶子节点虽然没有孩子,但也存在一个去访问其 “孩子” 的过程),这里遍历的是整型数据,不能像本专栏中第一篇博文介绍的那样判断唯一性方法来处理,这里定义一个结构体,将节点指针和记录该节点出现栈顶次数是否为一次的bool型变量绑在一起,可以在访问节点的时候进行判断。

typedef struct _BTnode
{
struct _BinaryTree *pTnode;
bool isFirst;
}BTnode;

void printPostorder(BinaryTree *pBinaryTree)
{
if (NULL == pBinaryTree)
return;

stack<BTnode*> S;
BinaryTree *pS = pBinaryTree;
BTnode *pTemp;

while ((pS != NULL) || !S.empty())
{
while (pS)
{
BTnode *pBTnode = (BTnode*)malloc(sizeof(BTnode));
pBTnode->pTnode = pS;
pBTnode->isFirst = true;
S.push(pBTnode);
pS = pS->left_child;
}

if (!S.empty())
{
pTemp = S.top();
if (pTemp->isFirst == true)
{
pTemp->isFirst = false;
pS = pTemp->pTnode->right_child;
}
else //非第一次
{
cout << pTemp->pTnode->value << endl;
S.pop();
}
}
}

}
方法二:
既然后序遍历需要添加一个变量,那么可以借助 STL 中的 pair 对组来进行绑定。pair 将两个值视作一个单元,我们可以设置第一个值为节点指针,第二个置为 bool 型变量,利用 make_pair() 函数来构建对组。在方法一中说到,后序遍历需保证节点的孩子均已访问完毕,再访问该节点。根据栈的特点,我们可以把遍历树的过程看做是一个将树节点压栈的过程,遍历顺序就等于压栈顺序以及何时出栈。将父节点二度入栈,第二次入栈修改标记,表明该节点的孩子节点访问完毕(压栈完毕)。
具体结合程序理解

void printPostorder(BinaryTree *pBinaryTree)
{
if (NULL == pBinaryTree)
return;

stack<pair<BinaryTree*, bool>> S;
BinaryTree *pS;
bool isFirst;
S.push(make_pair(pBinaryTree, true));

while (!S.empty())
{
pS = S.top().first;
isFirst = S.top().second;
S.pop();

if (isFirst) //第二次入栈
{
S.push(make_pair(pS, false)); //随后将其孩子节点入栈
if (pS->right_child)
S.push(make_pair(pS->right_child, true));
if (pS->left_child)
S.push(make_pair(pS->left_child, true));
}
else
{
cout << pS->value << endl;
}
}

}
方法三:

后序遍历要保证父节点是最后被访问的,且最先被访问的是左孩子。比较棘手的地方是对于根(父)节点的访问,前面两种方法是采用标记的策略,我们也可以把访问根(父)节点的情况列出来,即在什么条件下可以对其进行访问了。以下面这可树为例:

1). 当该节点没有孩子节点的时候,可以直接访问,如节点1,节点102). 当该节点左右孩子节点,或只存在左孩子或右孩子等皆被访问完后,可以访问该节点;其余情况则可以按次序依次将右左孩子节点入栈,然后每次取栈顶元素,循环进行判断,直至栈为空。

/*

* 6
* / \
* 2 8
* / \ \
* 1 4 10 后序遍历: 1 3 4 2 10 8 6
* /
* 3
*/
先将节点6压栈,存在孩子节点,继而将节点8,2压栈(栈:6,8,2),取栈顶元素2,继续重复,将节点4,1压栈(栈:6,8,2,4,1),取栈顶1,为第一种情况,无孩子节点,则访问(出栈,栈:6,8,2,4),再取栈顶元素4,重复上述过程,没有左孩子或右孩子的情况当做已访问,或不理会。节点有无孩子节点容易判别,留下的问题就是怎么知道节点的孩子节点均已被访问,弹出一个节点之后,位于栈顶的总是父节点,这样也继续访问其左孩子,陷入死循环。我们发现按上述过程,如栈顶节点与刚出栈(表示已访问)的节点存在父子关系,则表明该栈顶节点的孩子节点均已访问完毕,可以访问该栈顶节点。这样我们就需要额外一个节点指针来指向刚出栈的节点,与栈顶节点进行 “亲子鉴定”。
具体参见程序:

/*后序遍历*/
void printPostorder(BinaryTree *pBinaryTree)
{
if (NULL == pBinaryTree)
return;

stack<BinaryTree*> S;
BinaryTree *pS;
BinaryTree *pre = NULL;

S.push(pBinaryTree);

while (!S.empty())
{
pS = S.top();
if ((NULL == pS->left_child) && (NULL == pS->right_child)
|| ((pre == pS->left_child) || (pre == pS->right_child)))
{
cout << pS->value << endl;
S.pop();
pre = pS;
}
else
{
if (pS->right_child)
S.push(pS->right_child);
if (pS->left_child)
S.push(pS->left_child);
}
}

}

这里额外贴出一篇文章,在没有栈和递归的情况下遍历二叉树:Inorder Tree Traversal without recursion and without stack!

这里额外贴出一篇文章,在没有栈和递归的情况下遍历二叉树:Inorder Tree Traversal without recursion and without stack!