二叉樹基本操作

2021-06-27 11:03:43 字數 4732 閱讀 1654

一.二叉樹的定義

二.二叉樹的建立

定義一棵無資料的二叉樹:

6 int left[size];

7 int right[size];

為了操作簡便,我們定義一棵不需要儲存資料的二叉樹,只要能儲存節點之間的邏輯關係就行,所以用兩個陣列來表示。

left[i],第i個節點的左子節點的序號

right[i],第i個節點的右子節點的序號

若left[i] == -1表示第i個節點沒有左子節點

若right[i] == -1表示第i個節點沒有右子節點

先建立一棵二叉樹。

1.建立根節點

2.若果要,建立左子樹

3.若果要,建立右子樹

8 int createtree()

9 16 left[index] = createtree();

17 right[index] = createtree();

18 return index;

19 }

輸入-1表示該節點為空,返回-1。

三.二叉樹的遍歷操作

先序遍歷:

1.若根節點(當前節點)不為空,訪問根結點,否則返回。

2.若左子樹不為空,訪問左子樹

3.若右子樹不為空,訪問右子樹

43 void prior(int root)

44 49 printf("%d\n", root);

50 prior(left[root]);

51 prior(right[root]);

52 }

上面描述了遞迴的先序遍歷的**,邏輯異常明顯。下面寫乙個非遞迴的先序遍歷**。

先序遍歷的遞迴**相較於中序與後續來說是較為簡單的,只要牢記訪問的順序是先根節點再左子樹最後是右子樹,左右子樹的訪問也是相同的。

我們可以看著遞迴**來寫出其對應的非遞迴**,遞迴的好處就在於,我們不需要自己去維護乙個棧。

仔細分析上面的**,這個我們看不見的函式呼叫棧為我們儲存了哪些東西,從而實現了我們想要的先序遍歷,答案當然是引數以及函式自己宣告的區域性變數啦

那麼在這個地方,就是root這個變數,所以每個節點都被儲存了一次,因為每個節點都可以成為root;然而在先序遍歷中,先輸出了root節點,這個root節點就沒有必要再儲存了,只要先對其右子樹壓棧,然後再對其左子樹壓棧,就可以保證訪問順序了;那麼在我們自己的實現中是否可以直接儲存。所以我們知道了壓棧發生在函式呼叫的時候,出棧當然發生在函式返回之時,函式做了什麼?輸出;好了可以開始寫**了:

20 void prior_s(int root)

21 26 stacknodes;

27 nodes.push(root);

28 while(!nodes.empty())

29

37 if(left[root] != -1)

38

41 }

42 }

上面這個非遞迴**其實更類似下面這個遞迴的**:

43 void prior(int root)

44 50 if(right[root] != -1)

51

54 }

仔細看他們之間的對應關係,函式呼叫就是壓棧,先訪問的後壓棧,後訪問的先壓棧。

中序遍歷:

1.若左子樹不為空,訪問左子樹

2.訪問根節點

3.若又子樹不為空,訪問又子樹

遞迴版本的**:

138 void middle(int root)

139

144 middle(left[root]);

145 printf("%d\n", root);

146 middle(right[root]);

147 }

中序遍歷的實現就沒有先序遍歷的實現看起來那麼的直觀了,因為在中序遍歷中涉及到,乙個節點被訪問兩次的情況,這使得我們無法寫出對乙個節點乙個一致的操作。

對第一次訪問到乙個節點時,我們要先看看他有沒有左子節點,若果有的話,就把當前節點壓棧,然後訪問左子節點;

對於乙個曾經被壓棧的節點,當它的左子樹已經被訪問完了,此時又訪問到該節點,這時我們應該直接輸出他,然後再訪問它的右子節點;

為了區別這兩種操作,最簡單的辦法是在每個節點中增加乙個訪問標識,用於表示節點是第幾次被訪問,好的,下面我們給出乙個不新增標識的**:

108 void middle_s(int root)                               

109

114 stacknodes;

115 nodes.push(root);

116 int no = root;

117 while(!nodes.empty() || no != -1)

118

124 no = nodes.top();

125 nodes.pop();

126 printf("%d ", no);

127 no = right[no];

128 }

129 }

看看上述**是如何巧妙地避免了二次訪問問題的,以及在避免了二次訪問問題之後是否又引入了其他的問題。

每當訪問乙個結點時,就判斷這個節點是否有左子節點,然後一直向下訪問直到乙個節點的左子節點為空,然後輸出這個節點,將no指向當前節點的右子節點,然後迴圈,如果這個節點存在右子節點則進入while迴圈,如果不存在,則不會進入while迴圈,而接下來就是輸出與出棧操作。本演算法的巧妙之處就在這裡,通過no這個變數有效地避免了二次訪問問題。

後序遍歷:

1.若左子樹不為空,訪問左子樹

2.訪問當前節點

3.若右子樹不為空,訪問右子樹

209 void back(int root)

210

215 back(left[root]);

216 back(right[root]);

217 printf("%d\n",root);

218 }

後序遍歷同中序遍歷一樣,也有令人惱火的二次訪問問題,通過前面的分析,大家也能看出這個二次訪問問題是什麼,我們這裡先貼出**,再來看看是如何躲避二次訪問的,準確的說,中序遍歷是採用了躲避二次訪問的方法(沒有判斷語句),而下面要給出的後序遍歷的實現是通過判斷來決定對當前節點的操作的,只不過沒有通過給每乙個節點增加乙個訪問標識那樣大的開銷:

172 void back_s(int root)

173

178 stacknodes;

179 nodes.push(root);

180 int lastoutput = root;

181 while(!nodes.empty())

182

190 else if((left[root] == lastoutput) || (right[root] == lastoutput))

191

196 else

197

202 if(left[root] != -1)

203

206 }

207 }

208 }

因為訪問順序與壓棧順序相反,所以上面**的壓棧順序是先壓右子節點再壓左子節點。

第184行的判斷語句是為了判斷當前節點是否是葉子節點,若是則直接輸出,並出棧。

注意在每乙個輸出操作的地方都有乙個

lastoutput = root;
語句,該語句用於記錄當前訪問的節點,並且在第190行的判斷中用到。

第190行的判斷語句用於判斷當前節點是否是第二次訪問,如果是第一次訪問,那麼它的左右孩子一定都沒有被訪問,則判斷失效。但如果是第二次訪問,則lastoutput一定是它的孩子節點,即上乙個被訪問的節點(還記得後序遍歷的順序嗎?)。

ok了。

可能敘述不清,博主會在後期盡量抽時間改進,有可能會新增點內容,必進現在只有遍歷的內容,另外,給每個節點增加訪問標識用於處理中序與後序遍歷的情況沒有列出

二叉樹基本操作

tree.h ifndef tree h define tree h include typedef int element 定義二叉樹 typedef struct nodetreenode void preorder treenode root 遞迴前序遍歷 void inorder treen...

二叉樹基本操作

include include define maxsize 100 typedef char elemtype typedef struct node btnode void createbtnode btnode b,char str 由str串建立二叉鏈 j ch str j btnode f...

二叉樹基本操作

include include include using namespace std template struct binode template class bitree bitree datatype a,intn bitree void preorder void inorder void...