树(英语: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)平方