常用的二叉樹遍歷主要分爲深度優先遍歷(dfs)和廣度優先遍歷(bfs),其中dfs又有前序、中序、後序遍歷之分。然而不管你用迭代還是遞歸的方法實現,它們的空間複雜度都爲O(N)。而本文介紹的Morris算法,只需O(1)的空間複雜度,本質上是使用時間換取空間的一種方法。
假設我們要遍歷如下二叉樹:
如果採用中序遍歷,那麼遍歷順序應該爲1 2 3 4 5 6 7 8 9 10,我們定義一個cur表示當前節點,首先我們需要最遍歷的是節點1,但是當前cur在根節點6,所以需要向左子樹遍歷。但是這會產生一個問題,就是如果我們不想使用太多的空間,而是直接使用cur=cur->left記錄當前節點,那麼在我們向左子樹遍歷的時候,原來的節點6就會丟失,在遍歷完左子樹之後將無法返回節點6。這裏的解決方法就是,找到6的前驅節點,這裏爲5,將5的右節點指向6,這樣在遍歷6節點的左子樹時,6節點也將會被記錄,最後通過節點5重新遍歷到節點6。
同樣的,在我們遍歷4 2節點的左子樹時,我們也將其前驅節點3 1的右子樹指向它們,這樣在遍歷完它們的左子樹之後就能回到當前節點。如下圖:
回到當前節點之後只需將其前驅節點右子樹記錄的當前節點重新置爲NULL即可。
Morris遍歷算法的步驟如下:
1, 根據當前節點,找到其前驅節點,如果前序節點的右孩子是空,那麼把前序節點的右孩子指向當前節點,然後進入當前節點的左孩子。
2, 如果當前節點的左孩子爲空,打印當前節點,然後進入右孩子。
3,如果當前節點的前序節點其右孩子指向了它本身,那麼把前序節點的右孩子設置爲空,打印當前節點,然後進入右孩子。
c++代碼如下(中序遍歷):
struct TreeNode {
int val;
TreeNode *left;
TreeNode *right;
TreeNode(int x) : val(x), left(NULL), right(NULL) {}
};
vector<int> inorderTraversal(TreeNode* root) {
vector<int>v;
TreeNode* cur = root;
while (cur) {
if (cur->left == NULL) {// 左子樹爲空時,直接比較,然後進入右子樹
v.push_back(cur->val);
cur = cur->right;
}
else {// 進入左子樹
/* 找cur的前驅結點,找到後分兩種情況
/* 1、cur的左子結點沒有右子結點,那cur的左子結點就是前驅
/* 2、cur的左子結點有右子結點,就一路向右下,走到底就是cur的前驅*/
TreeNode* preceesor = cur->left;
while (preceesor->right && preceesor->right != cur) {
preceesor = preceesor->right;
}
// 前驅已經指向自己了,說明以及遍歷過左子樹了,進入右子樹
if (preceesor->right == cur) {
v.push_back(cur->val);
preceesor->right = NULL; // 斷開連接,恢復原樹
cur = cur->right;
}
else { // 前驅還沒有指向自己,說明左邊還沒有遍歷,將前驅的右指針指向自己,後進入左子樹
preceesor->right = cur;
cur = cur->left;
}
}
}
return v;
}
知道了Morris的中序遍歷之後,它的前序後續版本也就很簡單了。
如果是前序遍歷,還是先找當前節點的前驅節點,然後再將前驅節點的右子樹指向當前節點的後繼節點即可。
如果是後續遍歷,我們還是可以按照前序遍歷的方法。我們知道,後序遍歷順序爲左右中,前序遍歷順序爲中左右,我們可以將前序遍歷的左右交換一下,按照中右左的順序對樹進行遍歷,之後再進行反轉操作,即可得到左右中的後序遍歷順序。我們只是增加了一個反轉操作,時間複雜度仍然爲O(N)。