先序遍歷:按照根節點->左子樹->右子樹的順序訪問二叉樹html
【遞歸版】node
1 public class PreOrderRecur { 2 public static class Node { 3 public int value; 4 public Node left; 5 public Node right; 6 7 public Node(int data) { 8 this.value = data; 9 } 10 } 11 12 public static void preOrderRecur(Node head) { 13 if (head == null) { 14 return; 15 } 16 System.out.println(head.value + " "); 17 preOrderRecur(head.left); 18 preOrderRecur(head.right); 19 }
怎麼理解上述遞歸的先序遍歷?算法
先打印當前節點,而後打印整顆左子樹,而後再打印整顆右子樹。上圖二叉樹的打印順序爲:一、二、四、五、三、六、7數組
實際應該怎麼理解preOrderRecur()這個函數呢?其實來到每一個節點的順序並非打印順序。下面來套一下preOrderRecur這個函數:數據結構
來到1後,而後去它的左孩子,1→2,2→4,此時4遇到null,直接返回,返回後又回到4,而後又去4的右孩子,右孩子爲null,又回到4,整個4這顆樹遍歷完後,回到2,2去到它的右孩子,來到5,5的左孩子null,直接返回,又回到5,而後又回到5的右孩子,右孩子爲null,又回到5,依次類推。。。app
因此若是忽略第16行的打印行爲,遞歸函數來到每一個節點的順序是:一、二、四、四、四、二、五、五、五、二、一、三、六、六、六、三、七、七、七、三、1。函數
在這樣一個順序中,若是把打印放在第一次來到這個節點的時候,就是先序遍歷:一、二、四、五、三、六、7post
若是把打印放在第二次來到這個節點的時候,就是中序遍歷:四、二、五、一、六、三、7this
若是把打印放在第三次來到這個節點的時候,就是後序遍歷:四、五、二、六、七、三、1spa
因此preOrderRecur這個函數依次訪問節點時,每一個節點都會訪問3次,這個順序是不變的,只是你把打印時機放在哪,就被加工成先序、中序、後序三種遍歷方式。
因此重要的是理解遞歸函數這個結構的節點訪問順序,至因而哪一種遍歷,只是打印時機的問題。
【非遞歸版】
1 public static void preOrderUnRecur(Node head) { 2 System.out.println("pre-order: "); 3 if (head != null) { 4 Stack<Node> stack = new Stack<>(); 5 stack.add(head); 6 while (!stack.isEmpty()) { 7 //head變量被複用了,能夠將此head理解爲當前節點 8 head = stack.pop(); 9 System.out.println(head.value + " "); 10 //若是該節點的右孩子不爲空 11 if (head.right != null) { 12 //棧中壓右孩子 13 stack.push(head.right); 14 } 15 if (head.left != null) { 16 stack.push(head.left); 17 } 18 } 19 } 20 }
準備一個棧,先把頭節點1放進去,開始走第6行的while循環:棧不爲null,彈出棧頂1,並打印,1的右孩子3不爲空,3進棧,1的左孩子2不爲空,2進棧;繼續此while循環,將2節點彈出並打印,2有右孩子5,5進棧,2有左孩子4,4進棧;再來到while循環,4被彈出並打印,由於4無左、右孩子,沒有把任何東西放進棧裏;再次來到while循環,將5彈出並打印,5也沒有孩子,因此棧也不會進入新的節點。。。重複此過程。
當前節點被彈出的時候,先壓右,後壓左,彈出就是先彈左後彈右,因而當前節點爲頭,彈出的順序是先左後右,先序遍歷的順序就被模擬出來了。
爲何要使用棧而不用隊列?
由於二叉樹只有從上到下的路徑,沒有回去的路徑,因此就得想一個結構,讓它可以回去,棧就再合適不過了。好比棧中壓了一、二、4,咱們能夠依次彈出四、二、1,由於二叉樹的總體順序是從上到下的,而咱們在遍歷到某一位置的時候,咱們是但願能回去的,因此這個結構就必須是:遍歷的時候從上到下,回去的時候從下到上,因此要使用棧。若是使用隊列的話,遍歷的時候是從上到下的,隊列又只能讓你從上到下,因此就不能使用隊列。
中序遍歷:按照左子樹->根節點->右子樹的順序訪問
【遞歸版】
public static void inOrderRecur(Node head) { if (head == null) { return; } inOrderRecur(head.left); System.out.print(head.value + " "); inOrderRecur(head.right); }
【非遞歸版】
public static void inOrderUnRecur(Node head) { System.out.print("in-order: "); if (head != null) { Stack<Node> stack = new Stack<Node>(); while (!stack.isEmpty() || head != null) { if (head != null) { stack.push(head); head = head.left; } else { head = stack.pop(); System.out.print(head.value + " "); head = head.right; } } } System.out.println(); }
總體思路:只要是當前節點,就先把本身的左邊界(包括當前節點在內整條左側的邊)先壓到棧裏。當前節點不爲空時,當前節點壓入棧,當前節點往左;當前節點爲空時,從棧拿一個,讓它變成當前節點,打印,當前節點往右邊走。這個過程先壓一側左邊界並依次往外彈,彈到每一個節點再去遍歷右孩子的過程,就是模擬了左中右的過程。
如上圖,head依次指向一、二、4節點,一、二、4進棧,由於4無左孩子,此時head指向null。而後從棧中拿出一個節點4並打印,往4的右邊(右孩子)走,右邊又是空,接着從棧拿出2,當前節點往右走,即來到2的右孩子5,此時當前節點5不爲空,將5壓入棧;當前節點從5來到5的左孩子(null),5彈出。。。重複此過程,遍歷結果爲四、二、五、一、六、三、7
後序遍歷:按照左子樹->右子樹->根節點的順序訪問
【遞歸版】
public static void posOrderRecur(Node head) { if (head == null) { return; } posOrderRecur(head.left); posOrderRecur(head.right); System.out.print(head.value + " "); }
【非遞歸版】
實現思路:
若是要實現「中右左」的遍歷好很差實現?這很好實現,由於先序遍歷是「中左右」,這個過程是當前節點先壓右孩子,再壓左孩子。怎麼改出一個「中右左」的遍歷?當前節點先壓左孩子,再壓右孩子就變成「中右左」了。實現「中右左」後,打印的時候咱們能夠先不打印,而是存入一個輔助的棧裏,由於棧是一種順序的逆序,因此當你實現中右左後,放到棧裏後,把棧中的元素依次彈出就是左右中了,這就是後序遍歷。
public static void postOrderUnRecur(Node head) { System.out.println("post-order: "); if (head != null) { Stack<Node> s1 = new Stack<>(); Stack<Node> s2 = new Stack<>(); s1.push(head); while (!s1.isEmpty()) { head = s1.pop(); s2.push(head); if (head.left != null) { s1.push(head.left); } if (head.right != null) { s1.push(head.right); } } while (!s2.isEmpty()) { System.out.print(s2.pop().value + " "); } } }
二叉樹能夠用常見的三種遍歷結果來描述其構造,可是不夠直觀,尤爲是二叉樹中有重複值的時候,僅經過三種遍歷的結果來構造二叉樹的真實結構是難上加難。
那麼咱們如何設計一個更直觀的二叉樹描述呢?首先咱們來看下面圖中這個二叉樹結構,咱們來設計一個算法用來更直觀的描述樹的結構
public class PrintBinaryTree { public static class Node { public int value; public Node left; public Node right; public Node(int data) { this.value = data; } } public static void printTree(Node head) { System.out.println("Binary Tree:"); printInOrder(head, 0, "H", 17); System.out.println(); } /** * * @param head 頭結點 * @param height 樹的高度 * @param to 字符表示 H表明頭 V表明右結點,v表明左結點 * @param len */ public static void printInOrder(Node head, int height, String to, int len) { //保證結點空時退出遞歸 if (head == null) { return; } //先遞歸遍歷右結點,找到右結點就輸出加上符號V和固定空格的字符 printInOrder(head.right, height + 1, "v", len); //得到該結點對應的字符,VnumV,表示右結點 String val = to + head.value + to; //計算須要補多少位的空格 int lenM = val.length(); int lenL = (len - lenM) / 2; int lenR = len - lenM - lenL; val = getSpace(lenL) + val + getSpace(lenR); //輸出補位空格的字符 System.out.println(getSpace(height * len) + val); //遞歸遍歷左結點,若是不爲空則打印字符 ^num^ printInOrder(head.left, height + 1, "^", len); } public static String getSpace(int num) { String space = " "; StringBuffer buf = new StringBuffer(""); for (int i = 0; i < num; i++) { buf.append(space); } return buf.toString(); } public static void main(String[] args) { Node head = new Node(1); head.left = new Node(2); head.right = new Node(3); head.left.left = new Node(4); head.right.left = new Node(5); head.right.right = new Node(6); head.left.left.right = new Node(7); printTree(head); } }
這個函數能夠將你的二叉樹很是直觀以一棵樹的樣子打印出來,不過打印出來的結果須要將頭旋轉逆時針90度來看
上面打印結果中,H1H表示1是頭節點,v6v指的是,個人父節點是我左下方離我最近的,即節點3;^5^表示,個人父節點是我左上方離我最近的,即節點3。
【題目】
如今有一種新的二叉樹節點類型以下:
public class Node { public int value; public Node left; public Node right; public Node parent; public Node(int data) { this.value = data; } }
該結構比普通二叉樹節點結構多了一個指向父節點的parent指針。假設有一 棵Node類型的節點組成的二叉樹, 樹中每一個節點的parent指針都正確地指向本身的父節點,頭節點的parent指向null。只給一個在二叉樹中的某個節點 node, 請實現返回node的後繼節點的函數。
【分析】
首先要理解一個概念:在二叉樹的中序遍歷的序列中, node的下一個節點叫做node的後繼節點;在中序遍歷序列中,一個節點的前一個節點叫它的前驅節點。
這顆二叉樹中序遍歷的序列爲:四、二、五、一、六、三、7。2就是4的後繼節點,5是2的前驅節點。
有一種很笨的方法:無論給的是哪一個節點,經過parent指針都能找到頭節點,而後從頭節點開始,進行中序遍歷獲得一個序列,從序列中就能知道該節點的下一個節點。可是這樣作會遍歷整棵樹,複雜度較高。
咱們能不能不用遍歷整棵樹,就能找到一個節點的後繼節點呢?好比說給你3,它的後繼節點是7,能不能只走過2個節點就能找到7,而不用遍歷整棵樹,生成一個序列後才找到7?
總體思路以下:
一個節點X,若是X有右子樹,這個X的後繼節點必定是它右子樹最左的節點。好比2有右子樹,它的後繼節點就是5;再好比1,有右子樹,它的後繼節點就是1的右子樹最左的節點,即6。
一個節點X,若是X沒有右子樹,首先要考察X到底做爲哪個節點左子樹的最後一個節點。好比節點4沒有右子樹,就要考察到底哪一個節點,4做爲整顆左子樹的最後一個節點,2就是這樣的節點,2的左子樹的最後一個節點是4;若是給的是5,5也沒有右子樹,就要找究竟是哪一個節點,它的左子樹的最後一個節點是5,5是做爲1整顆左子樹的最後一個節點被打印的,因此1就是5的後繼節點;若是給的是7,哪一個節點的左子樹是以7結尾的,沒有,因此7的後繼不存在。因而這個問題就變成了:當X沒有右子樹,經過X的parent指針找到X的父節點,若是發現X不是父節點的左孩子,就繼續往上,直到某個節點是它父節點的左孩子中止,父節點就是原始節點的後繼。好比一開始X指向5,parent指向2,由於5不是2的左孩子,因此X往上指向2,parent指向1,2是1的左孩子,此過程中止,1就是5的後繼節點。
public class SuccessorNode { public static class Node { public int value; public Node left; public Node right; public Node parent; public Node(int data) { this.value = data; } } /** * 獲得某個節點的後繼節點 * @param node 不是指頭節點,而是二叉樹中的某一個節點 * @return */ public static Node getSuccessorNode(Node node) { if (node == null) { return node; } //若是當前節點的右孩子不爲空,說明當前節點有右子樹 if (node.right != null) { //直接找到右子樹最左的節點 return getLeftMost(node.right); } else { //當前節點沒有右子樹 Node parent = node.parent; //parent=null,或者當前節點等於本身parent的左孩子時,while中止 while (parent != null && parent.left != node) { node = parent; parent = node.parent; } return parent; } } /** * 找到這顆子樹最左的節點 * @param node 某一顆子樹的頭部 * @return */ public static Node getLeftMost(Node node) { if (node == null) { return node; } while (node.left != null) { node = node.left; } return node; } public static void main(String[] args) { Node head = new Node(6); head.parent = null; head.left = new Node(3); head.left.parent = head; head.left.left = new Node(1); head.left.left.parent = head.left; head.left.left.right = new Node(2); head.left.left.right.parent = head.left.left; head.left.right = new Node(4); head.left.right.parent = head.left; head.left.right.right = new Node(5); head.left.right.right.parent = head.left.right; head.right = new Node(9); head.right.parent = head; head.right.left = new Node(8); head.right.left.parent = head.right; head.right.left.left = new Node(7); head.right.left.left.parent = head.right.left; head.right.right = new Node(10); head.right.right.parent = head.right; Node test = head.left.left; System.out.println(test.value + " next: " + getSuccessorNode(test).value); test = head.left.left.right; System.out.println(test.value + " next: " + getSuccessorNode(test).value); test = head.left; System.out.println(test.value + " next: " + getSuccessorNode(test).value); test = head.left.right; System.out.println(test.value + " next: " + getSuccessorNode(test).value); test = head.left.right.right; System.out.println(test.value + " next: " + getSuccessorNode(test).value); test = head; System.out.println(test.value + " next: " + getSuccessorNode(test).value); test = head.right.left.left; System.out.println(test.value + " next: " + getSuccessorNode(test).value); test = head.right.left; System.out.println(test.value + " next: " + getSuccessorNode(test).value); test = head.right; System.out.println(test.value + " next: " + getSuccessorNode(test).value); test = head.right.right; // 10's next is null System.out.println(test.value + " next: " + getSuccessorNode(test)); } }
補充問題:如何找一個節點的前驅節點?
X若是有左子樹,左子樹最右的節點就是它的前驅節點;X若是沒有左子樹,就往上找,直到某個節點是它父節點的右孩子就中止。
【題目】
首先咱們介紹二叉樹先序序列化的方式,假設序列化的結果字符串爲str,初始時str等於空字符串。先序遍歷二叉樹,若是遇到空節點,就在str的末尾加上「#!」,「#」表示這個節點爲空,節點值不存在,固然你也能夠用其餘的特殊字符,「!」表示一個值的結束。若是遇到不爲空的節點,假設節點值爲3,就在str的末尾加上「3!」。如今請你實現樹的先序序列化。給定樹的根結點root,請返回二叉樹序列化後的字符
【序列化】
咱們知道所謂的二叉樹是一種由對象引用將多個結點關聯起來的抽象數據結構,是存在於內存中的,不能進行持久化,若是須要將一顆二叉樹的結構持久化保存,須要將其轉換爲字符串並保存到文件中,因而關鍵是創建一套規則,使得二叉樹能夠與字符串一一對應,根據一個二叉樹能夠惟一的獲得一個字符串,根據一個字符串也能夠惟一的還原出一棵二叉樹。
好比上面這棵樹,它存在於內存裏,怎麼給它序列化呢?即怎麼將這棵樹變成字符串?把這個字符串記錄在文本里面,下次建這棵樹的時候,只要經過這個字符串就能還原成原來的樹。
先序遍歷的序列化結果就是:1!2!4!#!#!5!#!#!3!6!#!#!7!#!#!
遍歷時對於爲null的結點在判斷條件中會直接跳過或者返回,可是在序列化時,對於爲null的結點也須要遍歷並將其對應爲」#!」,即在序列化時對於任何null或者非null的結點都須要遍歷和處理所以咱們只是對非空的結點進行遍歷,在遍歷每一個非空的結點時關注它的子節點是否爲null,若是爲null就爲字符串加上「#!」而且再也不繼續遍歷,若是不爲null就繼續遍歷。
public class SerializeTree { public static class Node { public int value; public Node left; public Node right; public Node(int data) { this.value = data; } } /** * 先序序列化 * @param head * @return */ public static String serialByPre(Node head) { if (head == null) { return "#!"; } //先把當前節點做爲整個字符串的頭 String res = head.value + "!"; //而後加上左樹造成的字符串 res += serialByPre(head.left); //再加上右樹造成的字符串 res += serialByPre(head.right); //就是整顆樹造成的結果 return res; } }
爲何要用到#呢?
好比這兩顆樹,它們的遍歷結果都是一、一、1。若是咱們不用#把位置佔住,就沒法區分原來的結構了。
爲何要用到!做爲一個值的結尾?
好比這兩棵樹,若是不用一個字符來佔結尾的話,上面兩棵樹遍歷結果都是123,就沒法區分是一、23仍是十二、3了,因此咱們須要在一個值的結尾加上!來標記,這樣1!23!和12!3!就能區分開了。
【反序列化】
所謂反序列化是根據一個字符串從新創建一棵二叉樹,反序列化是序列化的逆過程,對於一個字符串,首先按照分隔符!將其分割爲字符串數組,每一個字符串元素表明一個結點,若是使用的是先序遍歷的方式序列化的,就使用先序遍歷的方式將字符串反序列化爲二叉樹。
/** * 將先序遍歷序列化的字符串,反序列化爲二叉樹 * @param preStr * @return */ public static Node reconByPreString(String preStr) { String[] values = preStr.split("!"); Queue<String> queue = new LinkedList<>(); //將數組元素依次假如隊列中,這樣就能夠依次彈出了 for (int i = 0; i != values.length; i++) { /* 區分add()和offer(): 二者都是往隊列尾部插入元素,不一樣的是,當超出隊列界限的時候,add()方法是拋出異常讓你處理, 而offer()方法是直接返回false */ queue.offer(values[i]); } return reconPreOrder(queue); } public static Node reconPreOrder(Queue<String> queue) { String value = queue.poll(); if (value.equals("#")) { //若是value等於#,當前節點建出一個null return null; } //由於是採用先序序列化的,因此反序列化時也是先中再左再右 Node head = new Node(Integer.valueOf(value)); head.left = reconPreOrder(queue); head.right = reconPreOrder(queue); return head; }
中序遍歷和後序遍歷的序列化與反序列化與先序遍歷基本類似。下面補充一個層次遍歷的序列化與反序列化
這棵樹層次遍歷序列化的字符串爲:1!2!3!5!#!#!6!#!#!#!#!(注意,5和6節點下面還有一層null節點)
public class SerializeTree { public static class Node { public int value; public Node left; public Node right; public Node(int data) { this.value = data; } } /** * 按層次遍歷序列化 * @param head * @return */ public static String serialByLevel(Node head) { if (head == null) { return "#!"; } String res = head.value + "!"; Queue<Node> queue = new LinkedList<>(); queue.offer(head); while (!queue.isEmpty()) { head = queue.poll(); if (head.left != null) { res += head.left.value + "!"; queue.offer(head.left); } else { res += "#!"; } if (head.right != null) { res += head.right.value + "!"; queue.offer(head.right); } else { res += "#!"; } } return res; } /** * 按層次遍歷的方式反序列化 * * @param levelStr * @return */ public static Node reconByLevelString(String levelStr) { String[] values = levelStr.split("!"); int index = 0; Node head = generateNodeByString(values[index++]); Queue<Node> queue = new LinkedList<>(); if (head != null) { queue.offer(head); } Node node = null; while (!queue.isEmpty()) { node = queue.poll(); node.left = generateNodeByString(values[index++]); node.right = generateNodeByString(values[index++]); if (node.left != null) { queue.offer(node.left); } if (node.right != null) { queue.offer(node.right); } } return head; } public static Node generateNodeByString(String val) { if (val.equals("#")) { return null; } return new Node(Integer.valueOf(val)); } /** * 直觀打印二叉樹 * @param head */ public static void printTree(Node head) { System.out.println("Binary Tree:"); printInOrder(head, 0, "H", 17); System.out.println(); } public static void printInOrder(Node head, int height, String to, int len) { if (head == null) { return; } printInOrder(head.right, height + 1, "v", len); String val = to + head.value + to; int lenM = val.length(); int lenL = (len - lenM) / 2; int lenR = len - lenM - lenL; val = getSpace(lenL) + val + getSpace(lenR); System.out.println(getSpace(height * len) + val); printInOrder(head.left, height + 1, "^", len); } public static String getSpace(int num) { String space = " "; StringBuffer buf = new StringBuffer(""); for (int i = 0; i < num; i++) { buf.append(space); } return buf.toString(); } public static void main(String[] args) { Node head = null; head = new Node(1); head.left = new Node(2); head.right = new Node(3); head.left.left = new Node(4); head.right.right = new Node(5); printTree(head); String level = serialByLevel(head); System.out.println("serialize tree by level: " + level); head = reconByLevelString(level); System.out.print("reconstruct tree by level, "); printTree(head); } }
平衡二叉樹(Balanced Binary Tree)又被稱爲AVL樹(有別於AVL算法),且具備如下性質:它是一 棵空樹或它的左右兩個子樹的高度差的絕對值不超過1,而且左右兩個子樹都是一棵平衡二叉樹。
根據上述性質咱們能夠發現圖(a)是一棵平衡二叉樹,而圖(b)是一棵不平衡二叉樹。圖中結點的數值表明的就是當前結點的平衡因子。也驗證了上述性質,一棵平衡二叉樹的全部結點的平衡因子只多是-一、0、1三種。
判斷一棵樹是否是平衡樹,大的思路是:若是以每個點做爲頭節點的樹都是平衡樹,則整棵樹是平衡的。
假設遍歷到X,怎麼判斷X的整棵樹是否是平衡的?咱們要收集如下信息:(1)左樹是否平衡,若是不平衡,直接返回false;(2)右樹是否平衡,若是不平衡,返回false;(3)在左樹和右樹都平衡的狀況下,左樹的高度是多少?(4)在左樹和右樹都平衡的狀況下,右樹的高度是多少?而後就能夠判斷左樹和右樹的高度差是否超過1。
列出上述的可能性後,設計遞歸返回結構。左樹的過程應該給我一個返回值,返回值裏包含(1)和(3)的信息;右樹也要返回(2)和(4)的信息。因此遞歸函數的返回值應該包含兩個信息:這顆樹是否平衡,和這顆樹的高度是多少。
public class IsBalancedTree { public static class Node { public int value; public Node left; public Node right; public Node(int data) { this.value = data; } } /** * 設計遞歸函數的返回值 */ public static class ReturnData { public boolean isBalance; public int height; public ReturnData(boolean isBalance, int height) { this.isBalance = isBalance; this.height = height; } } /** * 遞歸函數 * @param head * @return */ public static ReturnData process(Node head) { if (head == null) { return new ReturnData(true, 0); } ReturnData leftData = process(head.left); if (!leftData.isBalance) { return new ReturnData(false, 0); } ReturnData rightData = process(head.right); if (!rightData.isBalance) { return new ReturnData(false, 0); } if (Math.abs(leftData.height - rightData.height) > 1) { return new ReturnData(false, 0); } return new ReturnData(true, Math.max(leftData.height, rightData.height) + 1); } /** * 判斷是不是平衡二叉樹 * @param head * @return */ public static boolean isBalanceTree(Node head) { return process(head).isBalance; } public static void main(String[] args) { Node head = new Node(1); head.left = new Node(2); head.right = new Node(3); head.left.left = new Node(4); head.left.left.right = new Node(5); System.out.println(isBalanceTree(head)); } }
二叉搜索樹(BST),也稱有序二叉樹,排序二叉樹,是指一棵空樹或者具備下列性質的二叉樹:
怎麼判斷一棵樹是否是搜索二叉樹?二叉樹中序遍歷的節點是依次升序的就是搜索二叉樹。
使用二叉樹遍歷的非遞歸版本更方便判斷,採用二叉樹的中序遍歷的非遞歸版本,在其中打印的位置用比較大小代替便可。
public class IsBSTTree { public static class Node { public int value; public Node left; public Node right; public Node(int data) { this.value = data; } } public static boolean isBSTTree(Node head) { if (head == null) { return true; } int preNode = Integer.MIN_VALUE; Stack<Node> stack = new Stack<>(); while (!stack.isEmpty() || head != null) { if (head != null) { stack.push(head); head = head.left; } else { head = stack.pop(); if (preNode > head.value) { return false; } preNode = head.value; head = head.right; } } return true; } public static void main(String[] args) { Node head = new Node(4); head.left = new Node(3); head.right = new Node(6); System.out.println(isBSTTree(head)); } }
判斷邏輯:二叉樹按層次遍歷。
(1)若是一個節點有右孩子,可是沒有左孩子,必定不是徹底二叉樹,直接返回false;
(2)若是一個節點不是左右孩子都全(有左無右、或者左右都沒),它後面遇到的全部節點都必須是葉子節點,不然必定不是徹底二叉樹。
若是遍歷完整顆二叉樹後,都不違法(1)和(2),那麼這棵樹就是徹底二叉樹。
遍歷到5以前的節點都不知足(1)和(2)的條件,5節點有左孩子,但無右孩子,由於5後面遇到的全部節點都是葉子節點,符合(2)條件,因此該樹是徹底二叉樹。
public class IsCBTTree { public static class Node { public int value; public Node left; public Node right; public Node(int data) { this.value = data; } } public static boolean isCBT(Node head) { if (head == null) { return true; } Queue<Node> queue = new LinkedList<>(); //若是出現狀況(2),leaf變爲true,此時後面遇到的節點必須是葉子節點 boolean leaf = false; Node l = null; Node r = null; queue.offer(head); while (!queue.isEmpty()) { head = queue.poll(); l = head.left; r = head.right; /* (leaf && (l != null || r != null)):開啓了葉子節點的階段,而且左孩子不等於空,或者右孩子不等於空,返回false 即,若是開啓了後序節點都是葉節點這一階段,拿到的每個節點的左右孩子都必須是null,不然返回false (l == null && r != null):狀況(1),若是一個節點左孩子爲空,右孩子不爲空,直接返回false */ if ((leaf && (l != null || r != null)) || (l == null && r != null)) { return false; } if (l != null) { queue.offer(l); } if (r != null) { queue.offer(r); } //若是左右孩子不全,開啓階段 if (l == null || r == null) { leaf = true; } } return true; } public static void main(String[] args) { Node head = new Node(1); head.left = new Node(2); head.right = new Node(3); head.left.left = new Node(4); System.out.println(isCBT(head)); } }
要求:時間複雜度低於O(N),N爲這棵樹的節點個數
【分析】
不能經過遍從來求得節點個數,由於這樣是嚴格的O(N)。
已知一個結論:若是一顆滿二叉樹的高度爲h,那麼它的節點個數是2h-1。
先遍歷整棵樹的左邊界,由於這是一顆徹底二叉樹,因此遍歷左邊界後,就能知道這棵樹的高度h,並記錄高度。若是這棵樹的節點是N,那麼遍歷左邊界的代價是O(logN)。
而後遍歷頭節點X的右子樹的左邊界,判斷右子樹的左邊界有沒有到最後一層。
狀況一:判斷右子樹的左邊界到最後一層,那麼X的左子樹就是滿的,左子樹的高度就是3,因此左子樹的節點數就是23-1,再加上頭節點:23-1+1=23
右子樹也是徹底二叉樹,能夠遞歸求右子樹的節點個數。
狀況二:右子樹的左邊界沒到最後一層,右子樹就是滿的,高度爲左樹滿的高度-1,此處右子樹的高度爲2,那麼右子樹的節點個數就是22-1,再加上頭節點:22-1+1。而左樹又是一顆徹底二叉樹,遞歸左樹。
因此每次拿到一個節點,就判斷它右樹的左邊界有沒有到最後一層,到了,左樹就是滿的;沒到,右樹是滿的。只不過是左樹滿和右樹滿的高度不同而已。
public class CompleteTreeNodeNumber { public static class Node { public int value; public Node left; public Node right; public Node(int data) { this.value = data; } } public static int nodeNum(Node head) { if (head == null) { return 0; } return bs(head,1,mostLeftLevel(head,1)); } /** * 遞歸過程 * @param node 當前節點 * @param level node在第幾層 * @param h 固定值,表示整棵樹的深度 * @return 以這個node節點爲頭的節點個數 */ public static int bs(Node node, int level, int h) { //若是level來到最後一層,node就是葉子節點,由於node在level上 if (level == h) { return 1; } //mostLeftLevel(node.right, level + 1)求這個node的右子樹的最左的深度 //若是node的右子樹的最左的深度和總體深度相等 if (mostLeftLevel(node.right, level + 1) == h) { //1<<(h-level):至關於2^(h-level),表示左樹的節點個數+當前節點以後的節點總數 //bs(node.right, level + 1, h):右樹也是一顆徹底二叉樹,遞歸求它的節點個數 return (1 << (h - level)) + bs(node.right, level + 1, h); } else { //右子樹的左邊界沒到樹的總深度 //(h-level-1):右樹的高度比左樹的高度少一個 return (1 << (h - level - 1)) + bs(node.left, level + 1, h); } } /** * 從node開始,node處在level層,求整棵樹最左的邊界到了哪一層 * @param node * @param level * @return */ public static int mostLeftLevel(Node node, int level) { while (node != null) { level++; node = node.left; } return level - 1; } }
複雜度分析:求的過程當中,每一層只遍歷一個節點,一共有O(logN)層。當遍歷到一個節點時,會遍歷它右子樹的左邊界,這又是一個O(logN),因此這個算法的複雜度是O((logN)2)。