二叉查找树的遍历方法有多种,递归实现,利用栈实现,线索树实现,这几种遍历方法,其时间复杂度都为O(n)
,而空间复杂度递归和栈为O(h)
,线索树需要额外的标识位来表明是线索还是节点指针,空间复杂度为O(n)
,当节点数量非常大时,树高h = log(n)
仍然较大,有没有其他的遍历算法,其空间效率更高呢?这就是Morris算法,其时间复杂度为O(n)
,空间复杂度做到了O(1)
。这是较其他几种遍历方法最不同的地方。
其算法的思想有点类似线索树,只不过线索树中的后继指针是一直存储在节点中,而morris算法中也有后继指针,但却是临时的,即遍历的过程中生成临时后继指针,节点遍历过后检测到其右子节点指针为后继指针删除该指针,恢复为空指针。
中序遍历
中序遍历算法描述如下:
1. 如果当前节点的左孩子为空,则输出当前节点并将其右孩子作为当前节点。
2. 如果当前节点的左孩子不为空,在当前节点的左子树中找到当前节点在中序遍历下的前驱节点。
a) 如果前驱节点的右孩子为空,将它的右孩子设置为当前节点。当前节点更新为当前节点的左孩子。
b) 如果前驱节点的右孩子为当前节点,将它的右孩子重新设为空(恢复树的形状)。输出当前节点。当前节点更新为当前节点的右孩子。
3. 重复以上1、2直到当前节点为空。
中序遍历Morris算法实现如下:
template<class T>
void BST<T>::morrisInorder() {
Node<T>* p = root;
Node<T>* tmp;
while (p != NULL) {
if (p->left == 0) { // 如果当前节点的左孩子为空,打印当前节点,然后进入右孩子
visit(p);
p = p->right;
} else {
tmp = p->left; // 根据当前节点,找到其前序节点(左子树最右节点),然后进入当前节点的左孩子。
while (tmp->right != 0 && tmp->right != p)
tmp = tmp->right;
if (tmp->right == NULL) { // 如果前序节点的右孩子是空,那么把前序节点的右孩子指向当前节点
tmp->right = p;
p = p->left;
} else { // 如果当前节点的前序节点其右孩子指向了它本身,那么把前序节点的右孩子设置为空,打印当前节点,然后进入右孩子。
visit(p);
tmp->right = 0;
p = p->right;
}
}
}
}
前序遍历
前序遍历算法描述如下:
1. 如果当前节点的左孩子为空,则输出当前节点并将其右孩子作为当前节点。
2. 如果当前节点的左孩子不为空,在当前节点的左子树中找到当前节点在中序遍历下的前驱节点。
a) 如果前驱节点的右孩子为空,将它的右孩子设置为当前节点。输出当前节点(在这里输出,这是与中序遍历唯一一点不同)。当前节点更新为当前节点的左孩子。
b) 如果前驱节点的右孩子为当前节点,将它的右孩子重新设为空。当前节点更新为当前节点的右孩子。
3. 重复以上1、2直到当前节点为空。
前序遍历Morris算法实现代码如下:
template<class T>
void BST<T>::morrisPreorder() {
Node<T>* p = root;
Node<T>* tmp;
while (p != NULL) {
if (p->left == NULL) {
visit(p);
p = p->right;
} else {
tmp = p->left;
while (tmp->right != NULL && tmp->right != p)
tmp = tmp->right;
if (tmp->right == NULL) {
visit(p);
tmp->right = p;
p = p->left;
} else {
tmp->right = NULL;
p = p->right;
}
}
}
}
后序遍历
后序遍历这里不再细述。