二叉樹的遍歷與樹的轉換
一、 二叉樹的遍歷:
在程序設計基礎第三單元中有這么個關於案情分析的邏輯問題:
某地刑偵大隊對涉及6個嫌疑人的一樁疑案進行分析:
- A、B至少有1人作案
- A、E、F 3人中至少有2人參與作案
- A、D不可能是同案犯
- B、C或同時作案,或與本案無關
- C、D中有且僅有1 人作案
- 如果D沒有參與作案,則E也不可能參與作案
試分析出作案人員是誰?
這個問題的邏輯我們已經都明白了,但是怎么將其轉換成代碼寫入我們的程序呢?這就需要我們了解二叉樹是如何遍歷的。
- 1. 什么是二叉樹的遍歷
對二叉樹的遍歷,即對二叉樹的每個結點都訪問,且只訪問一次。遍歷的目的是運算。有了遍歷我們就能夠訪問每一個結點,對其數據進行計算和處理。
- 2. 二叉樹遍歷的方法
(1)先序遍歷
先序遍歷:根節點、左子樹、右子樹。
先序遍歷的序列是:A B D E C。
(2)中序遍歷
中序遍歷:左子樹、根節點、右子樹。
中序遍歷的序列是:D B E A C。
(3)后序遍歷
后序遍歷:左子樹、右子樹、根節點。
后序遍歷的序列是:D E B C A。
如本單元開始講述的罪犯問題,解題思路是
(1) 將案情寫成邏輯表達式
將案情描述寫成邏輯表達式,從第1條到第6條依次用CC1,CC2,C3,CC4,CC5,CC6來表示。用A,B,C,D,E,F分別來表示6個人的變量,其中6個變量的值也只能是0或者1。則可以推導出
CC1:A和B至少有一人作案。因此有CC1=(A||B);
CC2:A和D不可能是同案犯。因此有CC2=!(A&&D);
CC3:A,E,F中至少有兩人涉嫌作案,分析有3種可能,第1種,A和E作案,寫成(A&&E);第二種,A和F作案,寫成(A&&F);第三種,E和F作案,寫成(E&&F)。這三種可能性是或的關系,因此有CC3=(A&&E)||(A&&F)||(E&&F);
CC4:B和C或同時作案,或都與本案無關,分析有2種可能,第1種兩人同時作案寫成(B&&C),第2種兩人都與本案無關寫成(!B&&!C),兩者為或的關系,因此有CC4= (B&&C)||( !B&&!C)
CC5:C和D中有且僅有一人作案,寫成CC5=(C&&!D)||(D&&!C);
CC6:如果D沒有參與作案,則E也不可能參與作案。分析得出CC6=D||(!D&&!E);
(2) 破案綜合判斷條件
將案情分析的6條歸納成一個破案綜合判斷條件CC.
CC=CC1&&CC2&&CC3&&CC4&&CC5&&CC6 當CC為1時,說明6條的每一條都滿足情況,就可以結案了。
(3) 遍歷
用A,B,C,D,E,F分別來表示6個人的變量,0表示不是作案人,1表示是作案人,其中6個變量的值也只能是作案與沒作案兩種可能,所以可以采用二叉樹的遍歷解決此問題。
參考代碼:
/* 遍歷二叉樹解決罪犯問題 */ #include "stdio.h" void main() { int CC1,CC2,CC3,CC4,CC5,CC6,CC;/*定義7個變量分別表示6句話及總結果*/ int A,B,C,D,E,F; /*定義六個人的情況變量*/ for(A=0;A<=1;A++) /*A的兩種可能*/ for(B=0;B<=1;B++) /*B的兩種可能*/ for(C=0;C<=1;C++) /*C的兩種可能*/ for(D=0;D<=1;D++) /*D的兩種可能*/ for(E=0;E<=1;E++) /*E的兩種可能*/ for(F=0;F<=1;F++) /*F的兩種可能*/ { CC1=(A||B); /*第1句話的邏輯表達式*/ CC2=!(A&&D); /*第2句話的邏輯表達式*/ CC3=(A&&E)||(A&&F)||(E&&F); /*第3句話的邏輯表達式*/ CC4= (B&&C)||(!B&&!C); /*第4句話的邏輯表達式*/ CC5=(C&&!D)||(D&&!C); /*第5句話的邏輯表達式*/ CC6=D||(!D&&!E); /*第6句話的邏輯表達式*/ if((CC=CC1&&CC2&&CC3&&CC4&&CC5&&CC6)==1 ) /*判斷條件都成立*/ { /*輸出判斷結果*/ A==0?printf("A不是罪犯\n"):printf("A是犯罪\n"); B==0?printf("B不是罪犯\n"):printf("B是犯罪\n"); C==0?printf("C不是罪犯\n"):printf("C是犯罪\n"); D==0?printf("D不是罪犯\n"):printf("D是犯罪\n"); E==0?printf("E不是罪犯\n"):printf("E是犯罪\n"); F==0?printf("F不是罪犯\n"):printf("F是犯罪\n"); } } }
一、 樹、森林與二叉樹的轉換:
我們前面已經講過了樹的定義和存儲結構,對於樹來說,在滿足樹的條件下可以是任意形狀,一個結點可以有任意多個孩子,顯然對樹的處理要復雜的多,去研究關於樹的性質和算法,真的不容易。有沒有簡單的辦法解決對樹處理的難題呢?
我們前邊也講了二叉樹,盡管它也是樹,但由於每個結點最多只能有左孩子和右孩子,面對的變化就少很多了。因此很多性質和算法都被研究了出來。如果所有的樹都像二叉樹一樣方便就好了。
在講樹的存儲結構時,我們提到了樹的孩子兄弟法可以將一棵樹用二叉鏈表進行存儲,所以借助二叉鏈表,樹和二叉樹可以相互進行轉換。從物理結構來看,它們的二叉鏈表也是相同的,只是解釋不太一樣而已。因此,只要我們設定一定的規則,用二叉樹來表示樹,甚至表示森林都是可以的,森林與二叉樹也可以相互進行轉換。
- 1. 樹轉換為二叉樹
將樹轉換為二叉樹的步驟如下:
(1)加線。在所有 兄弟結點之間加一條連線。
(2)去線。對樹中每個結點,只保留它與第一個孩子結點的連線,刪除它與其他孩子結點之間的連線。
(3)層次調整。以樹的根結點為軸心,為整棵樹順時針旋轉一定的角度,使之結構層次分明。注意第一個孩子是二叉樹結點的左孩子,兄弟轉換過來的孩子是結點的右孩子。
如下圖所示,一棵樹經過三個步驟轉換為一棵二叉樹。初學者容易犯的錯誤是在層次 調整時,弄錯了左右孩子的關系。比如圖中F、G本都是樹的結點B的孩子,是結點E的兄弟,因此轉換后,F就是二叉樹結點E的右孩子,G是二叉樹結點F的右孩子。
- 1. 二叉樹轉換為樹
二叉樹轉換為樹是樹轉換成二叉樹的逆過程,也就是反過來做而已。步驟如下:
(1)加線。若某結點的左孩子結點存在,則將這個左孩子的右孩子結點、右孩子的右孩子結點、右孩子的右孩子的右孩子結點······,反正就是左孩子的n個右孩子結點都作為此節點的孩子。將該結點與這些右孩子結點用線連接起來。
(2)去線。刪除原二叉樹中所有結點與其右孩子結點的連線。
(3)層次調整。使之結構層次分明。
- 1. 二叉樹轉換為森林
判斷一棵二叉樹能夠轉換成一棵樹還是森林,標准跟簡單,那就是只要看這棵二叉樹的根結點有沒有葉子,有就是森林,沒有就是一棵樹。那么如果是轉換成森林,步驟如下:
(1)從根結點開始,若右孩子存在,則把與右孩子結點的連線刪除,再查看分離后的二叉樹,若右孩子存在,則連線刪除······,直到所有右孩子連線都刪除為止,得到分離的二叉樹。
(2)再將每棵樹分離后的二叉樹轉換為樹即可。
一、
赫夫曼(Huffman)樹,又稱最優樹,是一類帶權路徑長度最短的樹,有着廣泛的應用。本節先討論最優二叉樹。
- 1. 最優二叉樹
首先給出路徑和路徑長度的概念。從樹中一個結點到另一個結點之間的分支構成了這兩個結點之間的路徑。路徑上分支的數目稱作路徑長度。樹的路徑長度是從樹根到每一個結點的路徑長度之和。完全二叉樹就是這種路徑長度最短的二叉樹。
若將上述概念推廣到一半情況,考慮帶權的結點。結點的帶權路徑路徑長度為該結點到樹根之間的路徑長度與結點上的權的乘積。樹的帶權路徑長度為樹中所有葉子結點的帶權路徑長度之和,通常記作WPL=。
假設有n個權值{},試構造一棵有n個葉子結點的二叉樹,每個葉子結點帶權為,則其中帶權路徑長度WPL最小的二叉樹稱作最優二叉樹或赫夫曼樹。
例如,下圖中的3棵二叉樹,都有4個葉子結點a、b、c、d,分別帶權7、5、2、4,它們的帶權路徑長度分別為
(a) WPL=7×2+5×2+2×2+4×2=36
(b) WPL=7×3+5×3+2×1+4×2=46
(c) WPL=7×1+5×2+2×3+4×3=35
其中以(c)樹的為最小。可以驗證,它恰為赫夫曼樹,即帶權路徑長度在所有帶權為7、5、2、4的四個葉子結點的的二叉樹中居最小。
在解某判定問題時,利用赫夫曼樹可以得到最佳判定算法。例如,要編制一個將百分之轉換為五級分制的程序。顯然很簡單,只要利用條件語句便可完成。如:
if(a<60)b=”bad”;
else if(a<70) b=”pass”;
else if(a<80) b=”general”;
else if(a<90) b=”good”;
else b=”excellent”;
這個判定過程可以圖(a)的判定樹表示。如果上述程序需反復使用,而且每次的輸入量很大,則考慮上述程序的質量問題,即操作所需要的時間。因為實際生活中學生的成績在5個等級上分布是不均勻的。假設其分布規律如下表所示:
則80%以上的數據需要進行3次或3次以上的比較才能得出結果。假定以5,15,40,30和10為權構造一棵5個葉子結點的赫夫曼樹如圖(b)所示的判定過程,它可以使大部分數據經過比較少的比較次數得出結果。但由於每個判定框都有兩次比較,將這兩次比較分開,我們能得到如圖(c)所示的判定樹,按此判定樹可以寫出相應的程序。假設現有10000個輸入數據,若按(a)的判定過程進行操作,則總共需要進行31500次比較;而按(c)的判定過程進行操作,則總共僅需進行22000次比較。這樣就大大提高程序的效率。
那么如何構造赫夫曼樹呢?赫夫曼最早給出一個帶有規律的算法,俗稱赫夫曼算法。現敘述如下:
(1) n個權值{},將每個結點看成無左右子樹的一棵樹
(2) 選擇權值最小的兩個構造一棵新的二叉樹,根結點記為這兩個權值之和
(3) 刪除這兩個樹,用新得到的二叉樹加入進去
(4) 重復(2)和(3),直到只剩一棵樹為止。這棵樹便是赫夫曼樹。
例如下圖所示的赫夫曼樹構造過程。其中,根結點上標注的數字是所賦的權。
- 1. 赫夫曼編碼
電報作為遠距離的通信手段,即將需傳送的文字轉換成由二進制的字符組成的字符串。例如,假設需傳送電文為’A B A C C D A’,它只有4種字符,只需要兩個字符的串便可分辨。假設A、B、C、D的編碼分別為:00、01、10、11,則上述7個字符的電文便為’00010010101100’,總長14位,對方接收時,可按二位一分進行譯碼。
當然,在傳送電文時,希望總長盡可能短。如果對每個字符設計長度不等的編碼,且讓電文中出現次數較多的字符采用盡可能短的編碼,則傳送電文的總長便可減少。如果設計A、B、C、D的編碼分別為0、00、1、01,則上述7個字符的電文可轉換成總長度為9的字符串’000011010’。但是這樣的電文無法翻譯,例如傳送過去的前4個字符子串’0000’就可有多種譯法,或是’AAAA’或是’ABA’,也可以是’BB’等。因此,若要設計長度不等的編碼,則必須是任一個字符的編碼都不是另一個字符編碼的前綴,這種編碼稱做前綴編碼。
可以利用二叉樹來設計二進制的前綴編碼。假設有一顆如圖所示的二叉樹,其4個葉子結點分別表示為A、B、C、D這四個字符,且約定左分支表示’0’,右分支表示字符’1’,則可以從根結點到葉子結點的路徑上分支字符組成的字符串作為該葉子結點字符的編碼。可以證明,如此得到的必為二進制前綴編碼。如圖所得,A、B、C、D的二進制前綴編碼分別為0、10、110和111。
又如何得到使電文總長最短的二進制前綴編碼呢?假設每種字符在電文中出現的次數為,其編碼長度為,電文中只有n種字符,則電文總長為。對應到二叉樹上,若置為葉子結點的權,恰為從根結點到葉子的路徑長度。由此可見,設計電文總長最短的二進制前綴編碼即以n種字符出現的頻率作權,設計一棵赫夫曼樹的問題,由此得到的二進制前綴編碼 ,便稱為赫夫曼編碼。
這里進行一下劇透,轉到決策樹的樹結構看一下:
import numpy as np import pandas as pd import matplotlib.pyplot as plt import matplotlib as mpl from sklearn import tree #決策樹 from sklearn.tree import DecisionTreeClassifier #分類樹 from sklearn.model_selection import train_test_split#測試集和訓練集 ## 設置屬性防止中文亂碼 mpl.rcParams['font.sans-serif'] = [u'SimHei'] mpl.rcParams['axes.unicode_minus'] = False from sklearn.datasets import load_iris iris = load_iris() x = iris.data y = iris.target x_train, x_test, y_train, y_test = train_test_split(x, y, train_size=0.8, random_state=14) from sklearn.preprocessing import MinMaxScaler #數據歸一化 ss = MinMaxScaler () x_train = ss.fit_transform(x_train) x_test = ss.transform(x_test) # entropy 香農熵 model = DecisionTreeClassifier(criterion='entropy',random_state=0, min_samples_split=10) #模型訓練 model.fit(x_train, y_train) y_test_hat = model.predict(x_test) # 方式二:直接使用pydotplus插件生成pdf文件 from sklearn import tree import pydotplus dot_data = tree.export_graphviz(model, out_file=None) graph = pydotplus.graph_from_dot_data(dot_data) # graph.write_pdf("iris2.pdf") graph.write_png("decision.png")
這不很像我們的二叉樹嘛,所以二叉樹一定要好好學習.