樹(英語:tree)是一種抽象數據類型(ADT)或是實做這種抽象數據類型的數據結構,用來模擬具備樹狀結構性質的數據集合。它是由n(n>=1)個有限節點組成一個具備層次關係的集合。把它叫作「樹」是由於它看起來像一棵倒掛的樹,也就是說它是根朝上,而葉朝下的。它具備如下的特色:html
無序樹:樹中任意節點的子節點之間沒有順序關係,這種樹稱爲無序樹,也稱爲自由樹;java
樹中任意節點的子節點之間有順序關係,這種樹稱爲有序樹;
二叉樹:每一個節點最多含有兩個子樹的樹稱爲二叉樹;node
霍夫曼樹(用於信息編碼):帶權路徑最短的二叉樹稱爲哈夫曼樹或最優二叉樹;
B樹:一種對讀寫操做進行優化的自平衡的二叉查找樹,可以保持數據有序,擁有多餘兩個子樹。python
順序存儲:將數據結構存儲在固定的數組中,然在遍歷速度上有必定的優點,但因所佔空間比較大,是非主流二叉樹。二叉樹一般以鏈式存儲。mysql
二叉樹是每一個節點最多有兩個子樹的樹結構。一般子樹被稱做「左子樹」(left subtree)和「右子樹」(right subtree)算法
性質1: 在二叉樹的第i層上至多有2^(i-1)個結點(i>0)
性質2: 深度爲k的二叉樹至多有2^k - 1個結點(k>0)
性質3: 對於任意一棵二叉樹,若是其葉結點數爲N0,而度數爲2的結點總數爲N2,則N0=N2+1;
性質4:具備n個結點的徹底二叉樹的深度必爲 log2(n+1)
性質5:對徹底二叉樹,若從上至下、從左至右編號,則編號爲i 的結點,其左孩子編號必爲2i,其右孩子編號必爲2i+1;其雙親的編號必爲i/2(i=1 時爲根,除外)sql
(1)徹底二叉樹——若設二叉樹的高度爲h,除第 h 層外,其它各層 (1~h-1) 的結點數都達到最大個數,第h層有葉子結點,而且葉子結點都是從左到右依次排布,這就是徹底二叉樹。數據庫
(2)滿二叉樹——除了葉結點外每個結點都有左右子葉且葉子結點都處在最底層的二叉樹。數組
經過使用Node類中定義三個屬性,分別爲elem自己的值,還有lchild左孩子和rchild右孩子數據結構
class Node(object): """節點類""" def __init__(self, elem=-1, lchild=None, rchild=None): self.elem = elem self.lchild = lchild self.rchild = rchild 樹的建立,建立一個樹的類,並給一個root根節點,一開始爲空,隨後添加節點 class Tree(object): """樹類""" def __init__(self, root=None): self.root = root def add(self, elem): """爲樹添加節點""" node = Node(elem) #若是樹是空的,則對根節點賦值 if self.root == None: self.root = node else: queue = [] queue.append(self.root) #對已有的節點進行層次遍歷 while queue: #彈出隊列的第一個元素 cur = queue.pop(0) if cur.lchild == None: cur.lchild = node return elif cur.rchild == None: cur.rchild = node return else: #若是左右子樹都不爲空,加入隊列繼續判斷 queue.append(cur.lchild) queue.append(cur.rchild)
Node節點類:
class Node{ public int value; public Node lChild; public Node rChild; public Node(int value){ this.value = value; } }
Tree類:
class Tree{ public Node root; //根節點初始化 public Tree(Node node){ root = node; } //樹中經過廣度優先遍歷的方式尋找空位置加新節點 public void add(int value){ Node temp = new Node(value); if(root==null){ root = temp; } Queue<Node> queue = new LinkedList<Node>(); queue.add(root); while(!queue.isEmpty()) { Node curNode = queue.poll(); if (curNode.lChild == null) { curNode.lChild = temp; return; } else if (curNode.rChild == null) { curNode.rChild = temp; return; } else { queue.add(curNode.lChild); queue.add(curNode.rChild); } } } }
樹的遍歷是樹的一種重要的運算。所謂遍歷是指對樹中全部結點的信息的訪問,即依次對樹中每一個結點訪問一次且僅訪問一次,咱們把這種對全部節點的訪問稱爲遍歷(traversal)。那麼樹的兩種重要的遍歷模式是深度優先遍歷和廣度優先遍歷,深度優先通常用遞歸,廣度優先通常用隊列。通常狀況下能用遞歸實現的算法大部分也能用堆棧來實現(掌握先序、中序、後序的非遞歸方式)。
對於一顆二叉樹,深度優先搜索(Depth First Search)是沿着樹的深度遍歷樹的節點,儘量深的搜索樹的分支。
那麼深度遍歷有重要的三種方法。這三種方式常被用於訪問樹的節點,它們之間的不一樣在於訪問每一個節點的次序不一樣。這三種遍歷分別叫作先序遍歷(preorder),中序遍歷(inorder)和後序遍歷(postorder)。咱們來給出它們的詳細定義,而後舉例看看它們的應用。
遞歸實現先序、中序、後序很是強大的地方是每一個都會訪問同一個節點三次,因此三個遍歷方式只是調換一下函數執行順序。
不管是不是遞歸方式都用到了棧(函數棧也是棧):由於樹的結構是從上到下訪問,若是要返回去訪問另外一處的節點,那麼必需要有棧來「記憶」。
在先序遍歷中,咱們先訪問根節點,而後遞歸使用先序遍歷訪問左子樹,再遞歸使用先序遍歷訪問右子樹
根節點->左子樹->右子樹
Python代碼實現:
def preorder(self, root): """遞歸實現先序遍歷""" if root == None: return print root.elem self.preorder(root.lchild) self.preorder(root.rchild)
Java代碼實現(遞歸方式):
public class PreOrder { private void preOrder(Node node){ if(node == null){ return; } System.out.println(node.value); preOrder(node.lChild); preOrder(node.rChild); } public static void main(String[] args){ PreOrder sort = new PreOrder(); Tree tree = new Tree(new Node(0)); tree.add(1); tree.add(2); tree.add(3); tree.add(4); sort.preOrder(tree.root); } }
Java 代碼實現(非遞歸方式):
public void preOrderUnRecur(Node head){ System.out.print("preOrder:"); if(head!=null){ //利用棧來實現 Stack<Node> stack = new Stack<Node>(); stack.push(head); while(!stack.isEmpty()){ Node node = stack.pop(); System.out.print(node.value + " "); //先壓進右孩子,利用先進後出原則 if(node.rChild!=null){ stack.push(node.rChild); } if(node.lChild!=null){ stack.push(node.lChild); } } } }
在中序遍歷中,咱們遞歸使用中序遍歷訪問左子樹,而後訪問根節點,最後再遞歸使用中序遍歷訪問右子樹
左子樹->根節點->右子樹
Python代碼實現:
def inorder(self, root): """遞歸實現中序遍歷""" if root == None: return self.inorder(root.lchild) print root.elem self.inorder(root.rchild)
Java代碼實現(遞歸方式):
public class InOrder { public void inOrder(Node node){ if(node==null){ return; } inOrder(node.lChild); System.out.println(node.value); inOrder(node.rChild); } public static void main(String[] args){ Tree tree = new Tree(new Node(0)); tree.add(1); tree.add(2); tree.add(3); tree.add(4); InOrder sort = new InOrder(); sort.inOrder(tree.root); } }
Java實現(非遞歸方式):
public void inOrderUnRecur(Node head){ System.out.print("InOrder:"); if(head!=null){ Stack<Node> stack = new Stack<>(); while(!stack.isEmpty() || head!=null){ if(head != null){ stack.push(head); head = head.lChild; }else{ head = stack.pop(); System.out.print(head.value + " "); head = head.rChild; } } } }
在後序遍歷中,咱們先遞歸使用後序遍歷訪問左子樹和右子樹,最後訪問根節點
左子樹->右子樹->根節點
Python代碼實現:
def postorder(self, root): """遞歸實現後續遍歷""" if root == None: return self.postorder(root.lchild) self.postorder(root.rchild) print root.elem
Java代碼實現(遞歸方式):
public class PostOrder { public void postOrder(Node node){ if(node==null){ return; } postOrder(node.lChild); postOrder(node.rChild); System.out.println(node.value); } public static void main(String[] args) { Tree tree = new Tree(new Node(0)); tree.add(1); tree.add(2); tree.add(3); tree.add(4); PostOrder sort = new PostOrder(); sort.postOrder(tree.root); } }
Java代碼實現(非遞歸方式:採用輔助空間方式,把先序(中右左)存儲到輔助棧,而後根據先進後出打印出結果就是後序遍歷結果(左右中)):
public void postOrderUnRecur(Node head){ System.out.print("postOrder:"); if(head!=null){ Stack<Node> stack1 = new Stack<Node>(); Stack<Node> stack2 = new Stack<Node>(); stack1.push(head); while(!stack1.isEmpty()){ head = stack1.pop(); stack2.push(head); //與先序的不一樣:先序打印,後序存儲起來 if(head.lChild!=null){ stack1.push(head.lChild); } if(head.rChild!=null){ stack1.push(head.rChild); } } //利用棧先進後出原則輸出後序遍歷結果 while(!stack2.isEmpty()){ head = stack2.pop(); System.out.print(head.value + " "); } } }
思考:哪兩種遍歷方式可以惟一的肯定一顆樹???
經過一個隊列的方法來實現
從樹的root開始,從上到下從從左到右遍歷整個樹的節點
def breadth_travel(self, root): """利用隊列實現樹的層次遍歷""" if root == None: return queue = [] queue.append(root) while queue: node = queue.pop(0) print node.elem, if node.lchild != None: queue.append(node.lchild) if node.rchild != None: queue.append(node.rchild)
二叉樹的遍歷通常額外空間複雜度爲O(logn),根據高度來的(節點回到自身須要保存到棧中),要回到上一個很難(經過棧解決)。
一種時間複雜度O(n),額外空間複雜度O(1)的二叉樹的遍歷方式,N爲二叉樹的節點個數。
Morris 遍歷規則:
public static void morrisIn(Node head){ if(head == null){ return; } Node cur = head; Node mostRight = null; while(cur!=null){ mostRight = cur.left; if(mostRight!=null){ //有左孩子,找到左子樹的最右節點 while(mostRight.right!=null && mostRight.right!=cur){ mostRight = mostRight.right; } if(mostRight.right == null){ mostRight.right = cur; cur = cur.left; continue; }else{ mostRight.right = null; } } System.out.print(cur.value + " ");//要往右節點走了,就是中序遍歷 cur = cur.right; } }
若是一個節點有左子樹,morris能回到節點兩次。若是沒有左子樹,只到節點一次。
morris改先序遍歷
public static void morrisPre(Node head){ if(head == null){ return; } Node cur = head; Node mostRight = null; while(cur!=null){ mostRight = cur.left; if(mostRight!=null){ while(mostRight.right!=null && mostRight.right!=cur){ mostRight = mostRight.right; } if(mostRight.right == null){ mostRight.right = cur; System.out.print(cur.value + " ") cur = cur.left; continue; }else{ mostRight.right = null; } }else{ System.out.print(cur.value + " "); } cur = cur.right; } System.out.println(); }
後序遍歷是第三次回到節點時候打印的,可是morris沒有回到節點第三次的。
怎麼作?
先去關注能回到節點兩次的節點,逆序打印它左子樹的右邊界。退出函數時單獨打印整棵樹的右邊界
public static void morrisPos(Node head){ if(head == null){ return; } Node cur1 = head; Node cur2 = head; while(cur1 !=null) { cur2 = cur1.left; if(cur2!=null){ while(cur2.right!=null && cur2.right!=cur1){ cur2 = cur2.right; } if(cur2.right==null){ cur2.right = cur1; cur1 = cur1.left; continue; }else{ cur2.right = null; printEdge(cur1.left); } } cur1 = cur1.right; } printEdge(head); System.out.println(); }
怎麼實現逆序打印?
採用鏈表逆序的方法,打印完再調整回來,這樣就沒有引入額外空間複雜度
先序 + 中序
思想:
中序+後序也能夠
題目:現有一種新的二叉樹節點類型以下
public class Node{ public int value; public Node left; public Node right; public Node parent; public Node(int value){ this.value = value; } }
這個結構只比普通二叉樹節點結構多了一個指向父節點的parent指針。假設一棵Node類型的節點組成的二叉樹,樹中每一個節點的parent指針都正確地指向父節點,頭節點的parent指向Null,只給一個在二叉樹中的某個節點Node,請實現返回node的後繼節點的函數。在二叉樹的中序遍歷的序列中,node的下一個節點叫做node的後繼節點。
解決思路:若是一個節點有右子樹,那麼右子樹的左邊界(整個樹最左下角)節點必定是它的後繼節點;若是沒有右子樹,經過這個節點的父指針parent指向父節點,若是發現這個節點是父節點的右孩子,就繼續往上,一直到某個節點是它父節點的左孩子,那麼這個最初節點的後繼就是這個父節點。
Java 代碼建立特殊的節點類:
public class FatherPointNode { public int value; public FatherPointNode lChild; public FatherPointNode rChild; public FatherPointNode parent; public FatherPointNode(int value){ this.value = value; } }
Java 代碼建立特殊的樹類:
public class FatherPointTree { public FatherPointNode root; //根節點初始化 public FatherPointTree(FatherPointNode node){ root = node; } //樹中經過廣度優先遍歷的方式尋找空位置加新節點 public void add(int value){ FatherPointNode temp = new FatherPointNode(value); if(root==null){ root = temp; } Queue<FatherPointNode> queue = new LinkedList<FatherPointNode>(); queue.add(root); while(!queue.isEmpty()) { FatherPointNode curNode = queue.poll(); if (curNode.lChild == null) { curNode.lChild = temp; temp.parent = curNode; //與原來的樹不一樣地方:添加父節點 return; } else if (curNode.rChild == null) { curNode.rChild = temp; temp.parent = curNode; return; } else { queue.add(curNode.lChild); queue.add(curNode.rChild); } } } }
Java 代碼找後繼節點:
public class SuccessorNode { public FatherPointNode successorNode(FatherPointNode node){ if(node==null){ return null; } if(node.rChild!=null){ return getLeftMost(node); //找右子樹的左邊界節點 }else{ while(node.parent!=null && node.parent.lChild!=node){ node = node.parent; } return node.parent; } } public FatherPointNode getLeftMost(FatherPointNode node){ if(node!=null){ while(node.lChild!=null){ node = node.lChild; } return node; } return null; } public static void main(String[] args) { FatherPointTree tree = new FatherPointTree(new FatherPointNode(0)); tree.add(1); tree.add(2); tree.add(3); tree.add(4); SuccessorNode sn = new SuccessorNode(); FatherPointNode result = sn.successorNode(tree.root.lChild.rChild);//節點4,後序節點應該是爲0; System.out.println(tree.root.lChild.rChild.value + " 後續節點:" + result.value); result = sn.successorNode(tree.root.lChild);//節點3,後序節點應該是爲1; System.out.println(tree.root.lChild.value + " 後續節點:" + result.value); } }
先驅節點:節點有左子樹,那麼左子樹的右節點必定是它的前驅。若是沒有左子樹,往上找,若是一個節點是父節點的右孩子,那麼這個父節點就是前驅節點
序列化:
eg:
1
2 3
4 5 6 7
先先序遍歷變成字符串:1_2_4_#_#_5_#_#_3_6_#_#_7_#_#_
用「#」來佔住位置,用_能夠區分節點,不然124,都在一塊兒沒法區分了
Java代碼實現:
public class SerialTree { //經過先序遍歷改編成序列化,原來打印處改成添加到字符串 public static String serialTree(Node curNode){ if(curNode==null){ return "#_"; //子節點爲null用#佔住 } String res = ""; res += curNode.value+"_"; res += serialTree(curNode.lChild); res += serialTree(curNode.rChild); return res; } public static void main(String[] args) { Tree tree = new Tree(new Node(0)); tree.add(1); tree.add(2); tree.add(3); tree.add(4); String result = serialTree(tree.root); System.out.println(result); } }
序列化+反序列化完整代碼:
import java.util.LinkedList; import java.util.Queue; public class SerialTree { public static String serialTree(Node curNode){ if(curNode==null){ return "#_"; } String res = ""; res += curNode.value+"_"; res += serialTree(curNode.lChild); res += serialTree(curNode.rChild); return res; } //解析字符串,將節點信息存入到隊列中 public static Node reconByPreString(String preString){ String[] value = preString.split("_"); Queue<String> queue = new LinkedList<String>(); for (int i = 0; i < value.length; i++) { queue.offer(value[i]); } return reconPreOrder(queue); } //根據隊列的信息遞歸生成節點 public static Node reconPreOrder(Queue<String> queue){ String value = queue.poll(); if(value.equals("#")){ return null; } Node head = new Node(Integer.valueOf(value)); head.lChild = reconPreOrder(queue); head.rChild = reconPreOrder(queue); return head; } //採用先序遍歷打印來驗證反序列化結果是否正確 public static void preOrder(Node node){ if(node == null){ return; } System.out.print(node.value + " "); preOrder(node.lChild); preOrder(node.rChild); } public static void main(String[] args) { Tree tree = new Tree(new Node(0)); tree.add(1); tree.add(2); tree.add(3); tree.add(4); String result = serialTree(tree.root); System.out.println(result); Node head = reconByPreString(result); System.out.println("驗證反序列化樹(先序遍歷結果):"); preOrder(head); } }
同理能夠學習中序、後序,層次化的序列化和反序列化
平衡二叉樹:一個樹的任一節點的左子樹和右子樹的高度差不超過1。
套路:遞歸函數
有什麼特色?到達一個節點三次!
第一次來到這個節點,左子樹轉一圈完回到這個節點,右子樹轉一圈完回到這個節點
解題思路:以每一個節點爲頭的子樹判斷是否平衡,若是都平衡那麼這個樹就是平衡的。
對於每一個節點的判斷:
所以遞歸函數須要返回兩個信息(經過一個對象返回,成員變量爲 ①是否平衡 ②高度)
Java 代碼實現:
//建立返回數據類:攜帶是否平衡信息和高度信息 class ReturnData{ public boolean isB; public int high; public ReturnData(boolean isB, int high){ this.isB = isB; this.high = high; } } public class IsBalanceTree { public static ReturnData processData(Node head){ if(head==null){ return new ReturnData(true, 0); } ReturnData leftData = processData(head.lChild); if(!leftData.isB){ return new ReturnData(false,0); } ReturnData rightData = processData(head.rChild); if(!rightData.isB){ return new ReturnData(false,0); } if(Math.abs(leftData.high-rightData.high)>1){ return new ReturnData(false,0); } return new ReturnData(true,Math.max(leftData.high,rightData.high)+1); } public static boolean isBalance(Node head){ return processData(head).isB; } public static void main(String[] args) { Tree tree = new Tree(new Node(0)); tree.add(1); tree.add(2); tree.add(3); tree.add(4); Boolean result = isBalance(tree.root); System.out.println("是不是平衡樹?:" + result); } }
二叉搜索樹:任何一個節點,左子樹都比它小,右子樹都比它大。
解題思路:二叉樹的中序遍歷節點是依次升序的就是搜索二叉樹。用非遞歸版本的中序遍歷中與前一個值進行比較:一旦產生前一個節點比後一個節點要大,說明不是二叉搜索樹。
一般搜索二叉樹是不出現重複節點的,通常重複的節點的信息都是壓到一個節點內的(如前綴樹)。
Java代碼實現:
import java.util.Stack; public class IsBST { public static boolean isBST(Node head){ if(head==null){ return false; } Stack<Node> stack = new Stack<>(); int value = Integer.MIN_VALUE; while(!stack.isEmpty() || head!=null){ if(head!=null){ //注意判斷條件不要寫成了head.lChild!=null stack.push(head); head = head.lChild; }else{ head = stack.pop(); if(head.value<value) { return false; } value = head.value; head = head.rChild; } } return true; } public static void main(String[] args) { Tree tree1 = new Tree(new Node(0)); //建立一個非二叉搜索樹 tree1.add(1); tree1.add(2); tree1.add(3); tree1.add(4); Tree tree2 = new Tree(new Node(7)); //建立一個二叉搜索樹 tree2.add(4); tree2.add(8); tree2.add(3); tree2.add(5); Boolean result = isBST(tree1.root); System.out.println("tree1 is BST?:" + result); result = isBST(tree2.root); System.out.println("tree2 is BST?:" + result); } }
判斷方式:二叉樹按層遍歷
判斷依據:
Java 代碼實現:
import java.util.LinkedList; import java.util.Queue; public class IsCBT { public static boolean isCBT(Node head){ if(head==null){ return false; } Queue<Node> queue = new LinkedList<Node>(); queue.offer(head); Node lChild = null; Node rChild = null; boolean leaf = false; while(!queue.isEmpty()){ head = queue.poll(); lChild = head.lChild; rChild = head.rChild; //判斷第一種狀況:右孩子不爲null,左孩子爲null if((leaf && (lChild!=null && rChild!=null)) || (lChild==null && rChild!=null)){ return false; } if(lChild!=null){ queue.offer(lChild); }else{ leaf = true; //出現狀況:左孩子不爲Null,右孩子爲Null 或者 左右孩子都爲Null,以後爲葉節點。 } } return true; } }
補充知識:使用二叉樹實現堆比數組的節省了擴容代價
題目要求:時間複雜度低於O(n),n爲這棵樹的節點個數
時間複雜度低於O(n),說明沒法採用廣度優先遍歷的方式獲取
解題思路:
補充知識點:若是一棵樹是一棵滿二叉樹,高度是l,那麼節點個數是2^l -1
Java 代碼實現:
public class TreeNodeNum { public static int treeNodeNum(Node head){ if(head==null){ return 0; } return bs(head,1, mostLeftLevel(head,1)); } //h:樹的深度, level:當前層數 public static int bs(Node node, int level, int h){ //若是level==h,說明當前節點是葉節點,節點個數爲1 if(level == h){ return 1; } if(mostLeftLevel(node.rChild,level + 1) == h){ System.out.println("左子樹滿"); return (1<<(h-level)) + bs(node.rChild,level+1, h); }else{ System.out.println("左子樹不必定滿"); return (1 << (h-level-1)) + bs(node.lChild, level+1, h); } } public static int mostLeftLevel(Node node,int level){ while(node!=null){ level++; node = node.lChild; } return level-1; } public static void main(String[] args) { Tree tree = new Tree(new Node(0)); tree.add(1); tree.add(2); tree.add(3); tree.add(4); int result = treeNodeNum(tree.root); System.out.println("徹底二叉樹的節點數目:" + result); } }
結果:算法的時間複雜度 O(logn)平方