這是leetcode上的3個題目,要求用非遞歸實現,其中以后序遍歷實現最難,既然遞歸實現的三種遍歷程序只需要改變輸入代碼順序,為什么循環不可以呢,帶着這種執拗的想法,我開始了這次研究
我依然是將遞歸用棧來實現,而不打算使用改變二叉樹結構的方法,那個我打算日后研究
首先以前序遍歷為例
遞歸實現是:
void preorderTraversal(TreeNode* root) { if (root == nullptr) return; cout << root->val; preorderTraversal(root->left); preorderTraversal(root->right); }
利用循環和棧來實現遞歸
我的思路是每次循環對應一次函數調用,每次函數調用的root加入棧中,思考后,寫出下面的程序
void preorderTraversal(TreeNode* root) { if (root == nullptr) return; stack<TreeNode*> sta; sta.push(root); while (!sta.empty()) { root = sta.top(); cout << root->val; if (root->left != nullptr) { sta.push(root->left); continue; } if (root->right != nullptr) { sta.push(root->right); continue; } sta.pop(); } }
但是經測試發現,這段程序有個巨大的漏洞,以至於無法結束循環,問題出現在pop某節點后,返回到父節點,會再次將該節點push到棧中,從而無限循環
對照遞歸的實現,腦補計算機函數進棧出棧的抽象圖,發現計算機的函數堆棧絕非是保存root的棧可以替代的,它還保存了代碼的執行位置,也就是棧指針和幀指針。
我們能否用變量保存上次循環執行的代碼位置呢,我們可以設置一些tag標記來模擬棧指針。但是,這里還有更好的實現方法。
我們可以保存上次循環結束時的root節點,其名lastRoot,
如果lastRoot = root->left,則說明root->left已經遍歷過一遍,不用再將其加入棧中,也就是下面的代碼段不用再執行
if (root->left != nullptr) { sta.push(root->left); continue; }
如果lastRoot = root->right,則說明root->left和root->right都已經遍歷過一遍,可以直接將root出棧,下面的代碼段不用執行
if (root->left != nullptr) { sta.push(root->left); continue; } if (root->right != nullptr) { sta.push(root->right); continue; }
理解之后,不難寫出代碼,前序,中序,后續遍歷的區別在於改變輸出當前節點代碼段的位置,同遞歸實現一樣,只是順序的區別,程序員就是這樣懶,妄想一招鮮吃遍天下
// 前序遍歷 vector<int> preorderTraversal(TreeNode* root) { vector<int> out; if (root == nullptr) return out; stack<TreeNode*> sta; sta.push(root); TreeNode* lastRoot = root; while (!sta.empty()) { root = sta.top(); if(lastRoot != root->right) { if (lastRoot != root->left) { out.push_back(root->val); if (root->left != nullptr) { sta.push(root->left); continue; } } if (root->right != nullptr) { sta.push(root->right); continue; } } lastRoot = root; sta.pop(); } return out; } // 中序遍歷 vector<int> inorderTraversal(TreeNode* root) { vector<int> out; if (root == nullptr) return out; stack<TreeNode*> sta; sta.push(root); TreeNode* lastRoot = root; while (!sta.empty()) { root = sta.top(); if (lastRoot != root->right) { if (lastRoot != root->left) { if (root->left != nullptr) { sta.push(root->left); continue; } } out.push_back(root->val); if (root->right != nullptr) { sta.push(root->right); continue; } } lastRoot = root; sta.pop(); } return out; } // 后序遍歷 vector<int> postorderTraversal(TreeNode* root) { vector<int> out; if (root == nullptr) return out; stack<TreeNode*> sta; sta.push(root); TreeNode* lastRoot = root; while (!sta.empty()) { root = sta.top(); if (lastRoot != root->right) { if (lastRoot != root->left) { if (root->left != nullptr) { sta.push(root->left); continue; } } if (root->right != nullptr) { sta.push(root->right); continue; } } out.push_back(root->val); lastRoot = root; sta.pop(); } return out; }
另外還有其他實現方法,比如前序遍歷,網上比較流行的方法是下面這種,這兩種寫法的思路是一樣的,不過下面的要更簡潔一些,雖然這種思路一開始我有些難以接受
不過還是要總結一下
1.選擇一個root節點
2.將其左節點依次加入棧中
3.輸出棧頂節點,彈出,然后將root設置為其右節點,重復1步驟
上面這些僅僅是程序的說明步驟,不能算是思路吧。不過要認真想一下的話,也許是跟人腦中遍歷樹的方式差不多,這里不做深入探討。覺得麻煩的話,可以直接背上面總結的步驟。
vector<int> preorderTraversal(TreeNode* root) { stack<TreeNode*> sta; vector<int> out; while (root || !sta.empty()) { while (root) { sta.push(root); out.push_back(root->val); root = root->left; } if (!sta.empty()) { root = sta.top(); sta.pop(); root = root->right; } } return out; } // 另一種寫法 vector<int> preorderTraversal(TreeNode* root) { stack<TreeNode*> sta; vector<int> out; while (root || !sta.empty()) { if(root) { sta.push(root); out.push_back(root->val); root = root->left; } else { root = sta.top(); sta.pop(); root = root->right; } } return out; }
另外一種實現思路,這次棧中僅僅保存右節點,因為是前序遍歷,左節點可以直接輸出,注意,這種方法僅能用於前序遍歷
vector<int> preorderTraversal(TreeNode* root) { vector<int> out; if (root == nullptr) return out; stack<TreeNode*> sta; sta.push(root); while (!sta.empty()) { out.push_back(root->val); if (root->right) sta.push(root->right); if (root->left) root = root->left; else { root = sta.top(); sta.pop(); } } return out; }
中序遍歷其他方法(同前序遍歷):
vector<int> inorderTraversal(TreeNode* root) { stack<TreeNode*> sta; vector<int> out; TreeNode* cur = root; while (cur != nullptr || !sta.empty()) { if (cur != nullptr) { sta.push(cur); cur = cur->left; } else { cur = sta.top(); sta.pop(); out.push_back(cur->val); cur = cur->right; } } return out; }
后序遍歷只能使用保存上次root節點的或tag標記的方法
后記:算法的研究深不見底,你總能從中發現新的東西,越是研究的深入,就越會發現我們平時習慣了的人類思維方式是多么神奇!