二叉樹的遍歷是一個非常基礎又重要的內容。遍歷就是訪問二叉樹中的每一個節點,並且每個節點只訪問一次。二叉樹的遍歷分爲前序遍歷、中序遍歷、後序遍歷和層級遍歷。
前序遍歷
前序遍歷的順序是:根節點 -> 左子節點 -> 右子節點。
上圖二叉樹的前序遍歷結果是: ABCDEFGH
遞歸實現
按照前序遍歷的順序,可以非常快寫出其遞歸實現
void PreOrderTraverseRec (Node* root) {
if (root != NULL) {
printf("%d ", root->val);
PreOrderTraverseRec (root->left);
PreOrderTraverseRec (root->right);
}
}
非遞歸實現
前序遍歷的非遞歸實現可以使用棧模擬。
版本1
其實在前序遍歷的內部實現,每次遍歷一個節點後,接下來先遍歷這個節點的左子節點,對這個左子節點將其看成根節點,又遞歸遍歷其左子節點,直到訪問到葉節點。將葉節點彈棧,得到其父節點。因爲已經訪問過父節點,以及父節點的左子節點,所以接下來應該按照上述方法遞歸訪問父節點的右子樹。
void PreOrderTraverseNonRec (Node* root) {
if (root == NULL) {
printf("empty tree!\n");
return;
} else {
//method 1
stack<Node*> s;
Node* p = root;
while ( p != NULL || !s.empty()) {
while (p != NULL) {
printf("%d ", p->val);
s.push (p);
p = p->left;
}
if (!s.empty()) {
p = s.top ();
s.pop ();
p = p->right;
}
}
}
}
版本2
還有另外一種更加簡潔的用棧模擬的方法。前序遍歷的順序是遍歷根節點 -> 左子節點 -> 右子節點。根據棧的後進先出的特點,可以按照根節點 -> 右節點 -> 左子節點的順序將節點壓入棧中。這樣每次壓棧之前訪問根節點,在彈棧先訪問左子節點,再到右子節點。
void PreOrderTraverseNonRec (Node* root) {
stack<Node*> s;
Node* cur = NULL;
s.push (root);
while (!s.empty()) {
cur = s.top ();
printf("%d ", cur->val);
s.pop (cur);
if (cur->right)
s.push (cur->right);
if (cur->left)
s.push (cur->left);
}
}
}
中序遍歷
中序遍歷的順序是: 根節點的左子節點 -> 根節點 -> 根節點的右子節點
遞歸實現
void InOrderTraverseRec (Node* root) {
if (root != NULL) {
InOrderTraverseRec (root->left);
printf("%d ", root->val);
InOrderTraverseRec (root->right);
}
}
非遞歸實現
中序遍歷的非遞歸實現與前序遍歷非遞歸實現的版本1很相似,只不過輸出根節點的先後順序不同。
void InOrderTraverseNonRec (Node* root) {
if (root == NULL) {
printf("empty tree!\n");
return;
} else {
stack<Node*> s;
Node* p = root;
while (p != NULL || !s.empty()) {
while (p != NULL) {
s.push (p);
p = p->left;
}
if (!s.empty()) {
p = s.top ();
s.pop ();
printf("%d ", p->val);
p = p->right;
}
}
}
}
後序遍歷
後續遍歷的順序爲: 左子節點 -> 右子節點 -> 根節點
遞歸實現
void PostOrderTraverseRec (Node* root) {
if (root != NULL) {
PostOrderTraverseRec (root->left);
PostOrderTraverseRec (root->right);
printf("%d ", root->val);
}
}
非遞歸實現
版本1
由於後續遍歷中需要先訪問左、右子節點,再來訪問根節點,因此非遞歸實現應該爲:訪問根節點,再向左不斷訪問左子節點,直到訪問到葉子節點,這時不能將這個葉子節點彈棧,因爲其右子節點還沒有被訪問。所以,應該按照上述方法訪問完葉節點的右子樹,再來訪問葉節點。爲了區別出兩次訪問順序,可以使用一個變量記錄根節點是否已經被訪問過。
void PostOrderTraverseNonRec2 (Node* root)
{
if (root == NULL) return;
stack <Node*> s;
Node *p = root;
while (p != NULL || !s.empty()) {
while (p != NULL) {
p->isFirstVisited = true;
s.push(p);
p = p->left;
}
while ( !s.empty()) {
p = s.top();
if (p->isFirstVisited) {
p->isFirstVisited = false;
p = p->right;
} else {
printf("%d ", p->val);
s.pop();
p = NULL;
}
}
}
}
版本2
第一種方法需要額外的空間存儲節點是否被訪問過,浪費空間。可以直接使用一個指針記錄當前訪問的節點的前一個被訪問的節點,如果前一個被訪問的節點正好是當前節點的右子節點,則說明右子節點已經被訪問過,直接輸出當前節點即可。
void PostOrderTraverseNonRec (Node* root) {
stack<Node*> s;
Node *cur = root, *pre = NULL;
while (cur != NULL || !s.empty()) {
while (cur != NULL) {
s.push (cur);
cur = cur->left;
}
if (!s.empty()) {
cur = s.top ();
if (cur->right == NULL || cur->right == pre) {
printf("%d ", cur->val);
pre = cur;
s.pop ();
cur == NULL;
} else {
cur = cur->right;
}
}
}
}
層級遍歷
層級遍歷就是按照層次的順序,從上往下一層一層遍歷二叉樹。
遞歸實現
爲了能夠逐層遍歷二叉樹,必須知道二叉樹的高度,然後才能逐層遍歷。而在每一層中,先遍歷根節點的左子節點,再遍歷右子節點。
int GetHeight (Node* root) {
if(root == NULL)
return 0;
else
return max(GetHeight(root->left) + 1, GetHeight(root->right) + 1);
}
void PrintLevel (Node* root, int level) {
if (root == NULL)
return;
if (level == 1) {
printf("%d ", root->val);
return;
}
return ( PrintLevel(root->left, level-1) + PrintLevel( root->right, level-1));
}
void LevelTraverseRec (Node* root) {
int height = GetHeight (root);
for (int i = 1; i <= height; ++i)
{
PrintLevel (root, i);
}
}
非遞歸實現
層級遍歷可以使用隊列來模擬:每次訪問根節點R,再分別訪問根節點的左右子節點L、R,再繼續訪問L、R的左右子節點。這就符合隊列的先進先出的特定。
void LevelTraverseNonRec (Node* root) {
if (root == NULL) return;
Node* cur = root;
queue<Node*> q;
q.push (root);
while (!q.empty()) {
cur = q.front ();
printf("%d ", cur->val);
q.pop ();
if (cur->left)
p.push (p->left);
if (cur->right)
p.push (p->right);
}
}