二叉查找樹的遍歷方法有多種,遞歸實現,利用棧實現,線索樹實現,這幾種遍歷方法,其時間複雜度都爲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;
}
}
}
}
後序遍歷
後序遍歷這裏不再細述。
完整代碼見bstree.h