本文讨论了一种方法,当输入二进制搜索树(BST)的相应前序遍历时,可以使用单调堆栈的概念来重建该树。
目录
前提条件
简介
二进制搜索树包含节点,每个节点最多有两个子节点。节点的排列使搜索操作消耗O(logn) 。在二叉树中,左子树中的元素的值小于根的值,右子树中的元素的值大于根的值;这个属性对任何子树和整个树本身都有效。
这篇文章试图解释当BST的前序遍历以增量的方式给出时,如何重建BST。讨论了该方法的一个版本,并对该方法进行了改进,以涵盖另一组不同的情况,修改代码以涵盖更新的方法。
第一版
考虑下面的二进制搜索树和相应的预排序遍历,如图所示
很明显,第一个元素是根,下一个元素是左边,如果它的值小于当前值,则是右边的孩子。
root->data = preorder[0], cur = root
for i in preorder array
if i > cur->data
then, cur->right = new bst node (i), cur = cur->right
else
cur->left = new bst node (i), cur = cur->left
下面的树是上述算法的结果:
很明显,这个算法产生的树不是BST!
第二版
我们将以某种方式回到值为5的节点,并将节点7附加到它的右边,类似的情况也发生在15上。一种方法是记住被附加的节点,只要它成为树的一部分。堆栈适合这种情况,因为对树的访问是以后进先出的方式进行的(最后被附加的节点5在10被访问之前被访问)。
所以算法是这样的:
如果堆栈中的peek元素大于预排序遍历中的当前元素,那么新的元素可以直接连接到peek的左边
,如下所示
if top(stack)->data > preorder[i]
top(stack)->left = new bst node (i)
但是,如果预排序的元素大于peek,那么元素就需要连接到右边的位置。对于BST中的一个给定的节点,左子树中的所有/任何元素都小于该节点本身,而右子树中的元素则更多。考虑到实际BST中的15,它不可能在节点10的左子树中。所以节点的右元素大于节点,小于节点的父元素。
继续弹出堆栈,直到堆栈的顶部小于预排序元素。将该元素作为最后一个弹出节点的右子,同时将新连接的节点推入堆栈。
一步一步的算法
- 输入BST的预排序遍历
- 用根的数据初始化一个BST,根的数据是输入数组的第一个元素
- 初始化一个可以容纳BST节点的堆栈,并推送上述元素
- 初始化一个索引变量,比如说
i1 - 从
preorder阵列中读取i'th值到data - 如果数据大于堆栈中的顶部元素,那么
- 从堆栈中弹出元素到例如
temp,直到堆栈不为空或直到data大于顶部元素 - 创建一个BST节点,把
data作为它的数据,并把它附加到temp's的右边,并把temp's的右边推到堆栈中。 - 如果第6步中的条件不成立,那么。
- 创建一个BST节点,其数据为
data,并将其附加到top's left,并将新创建的BST节点推到堆栈中。 - 递增
i,并从第5步开始重复,直到i成为无效。
C代码
#include <stdio.h>
#include <stdlib.h>
struct _bst {
int data;
struct _bst *left, *right;
};
typedef struct _bst bst_t;
struct _stack {
bst_t *data;
struct _stack *next;
};
typedef struct _stack stack_t;
stack_t* push(stack_t *s, bst_t *data) {
stack_t *t;
t = malloc(sizeof(*t));
t->next = s;
t->data = data;
return t;
}
stack_t* pop(stack_t *s, bst_t **data) {
stack_t *t = NULL;
if (s) {
t = s->next;
if (data) *data = s->data;
free(s);
}
else
if (data) *data = NULL;
return t;
}
bst_t* top(stack_t *s) {
return (s) ? s->data : NULL;
}
// it does not need to be function at all
// you may inline if you really want a seperate
// function for this;
#define isempty(s) ((s) == NULL)
bst_t* preorder_to_bst(int *preorder, unsigned int n) {
bst_t *root = NULL, *temp = NULL, *cur = NULL;
stack_t *s = NULL;
int i, data;
// step 2
root = malloc(sizeof(*root));
root->data = preorder[0], root->left = root->right = NULL;
// step 3
s = push(s, root);
// step 4-11
for (i = 1; i < n; ++i) {
// step 5
data = preorder[i];
// step 6
if (data > top(s)->data) {
// step 7
while (!isempty(s) && data > top(s)->data) {
s = pop(s, &temp);
}
// step 8
temp->right = malloc(sizeof(*temp->right));
temp->right->data = data, temp->right->left = temp->right->right = NULL;
s = push(s, temp->right);
}
else
// step 9
if (data < top(s)->data) {
// step 10
top(s)->left = malloc(sizeof(bst_t));
top(s)->left->data = data,
top(s)->left->right = top(s)->left->left = NULL;
s = push(s, top(s)->left);
}
}
return root;
}
// inorder traversal of the given BST is displayed
void bst_inorder(bst_t *root) {
if (root) {
bst_inorder(root->left);
printf("%d ", root->data);
bst_inorder(root->right);
}
}
int main() {
// let the below array be output of preorder traversal
// of a valid BST
int preorder[] = {20, 10, 5, 1, 7, 15, 30, 25, 32, 40};
unsigned int n = sizeof(preorder) / sizeof(preorder[0]);
// get root for the reconstructed BST
bst_t *root = preorder_to_bst(preorder, n);
// validate answer by displaying corresponding
// BST's inorder traversal
bst_inorder(root);
puts("");
return 0;
}
**Notes**:上面的代码试图解释这个概念,但一些基本的验证没有包括在代码中,如:
-
验证
malloc编辑的内存 -
使用后的内存释放
-
函数参数的NULL指针验证
问题
preorder_to_bst()的时间复杂性是多少?
(假设内存分配消耗的时间是恒定的)
O(n)
O(n^2)
O(n^3)
O(nlogn)
这就对了!
应用
-
将数据存储在一个数据结构中有助于根据处理的类型轻松地进行处理。比方说,系统需要在进程重启后访问树,在这种情况下,树的数据可能需要写到一个文件中。树的数据可能需要使用一个遍历机制按顺序写入。当数据需要再次以树形格式表示时,可以使用本文讨论的方法。
-
可能需要将树转移到其他进程,甚至通过网络。在这些情况下,数据会通过管道或套接字按顺序传输。用本文讨论的方法可以在接收端有效地重建树,因为它不使用递归。
复杂度
- 该算法的时间复杂度为O(n2)。
- 算法的空间复杂度是O(2n) - 如果输入数组中有
n个元素,那么n,堆栈可能会消耗大量的空间,另外n,树本身会消耗大量的空间。
我们鼓励读者改进代码,甚至可能改进算法本身。一个可能的、简单的代码改进是通过命令行参数、stdin、文件来输入数组,并提供选择任何一种方法的机会。为了验证你的改进,请从一个有效的BST中重建输入,并且不要互换数组元素。
通过OpenGenus的这篇文章,你一定有了从一个给定的预排序遍历中构建二进制搜索树(BST)的完整想法,好好享受吧。