树和树的算法
一、树
1.1 树的概念
树(英语:tree)是一种抽象数据类型(ADT)或是实作这种抽象数据类型的数据结构,用来模拟具有树状结构性质的数据集合。它是由n(n>=1)个有限节点组成一个具有层次关系的集合。把它叫做“树”是因为它看起来像一棵倒挂的树,也就是说它是根朝上,而叶朝下的。它具有以下的特点:
- 每个节点有零个或多个子节点;
- 没有父节点的节点称为根节点;
- 每一个非根节点有且只有一个父节点;
- 除了根节点外,每个子节点可以分为多个不相交的子树;
1.2 树的术语
- 节点的度:一个节点含有的子树的个数称为该节点的度;
- 树的度:一棵树中,最大的节点的度称为树的度;
- 叶节点或终端节点:度为零的节点;
- 父亲节点或父节点:若一个节点含有子节点,则这个节点称为其子节点的父节点;
- 孩子节点或子节点:一个节点含有的子树的根节点称为该节点的子节点;
- 兄弟节点:具有相同父节点的节点互称为兄弟节点;
- 节点的层次:从根开始定义起,根为第1层,根的子节点为第2层,以此类推;
- 树的高度或深度:树中节点的最大层次;
- 堂兄弟节点:父节点在同一层的节点互为堂兄弟;
- 节点的祖先:从根到该节点所经分支上的所有节点;
- 子孙:以某节点为根的子树中任一节点都称为该节点的子孙。
- 森林:由m(m>=0)棵互不相交的树的集合称为森林;
1.3 树的种类
1.3.1
无序树:树中任意节点的子节点之间没有顺序关系,这种树称为无序树,也称为自由树;
1.3.2 有序树
树中任意节点的子节点之间有顺序关系,这种树称为有序树;
二叉树:每个节点最多含有两个子树的树称为二叉树;
- 完全二叉树:对于一颗二叉树,假设其深度为d(d>1)。除了第d层外,其它各层的节点数目均已达最大值,且第d层所有节点从左向右连续地紧密排列,这样的二叉树被称为完全二叉树,其中满二叉树的定义是所有叶节点都在最底层的完全二叉树;
- 平衡二叉树(AVL树):当且仅当任何节点的两棵子树的高度差不大于1的二叉树;
- 排序二叉树(二叉查找树(英语:Binary Search Tree),也称二叉搜索树、有序二叉树)
霍夫曼树(用于信息编码):带权路径最短的二叉树称为哈夫曼树或最优二叉树;
B树:一种对读写操作进行优化的自平衡的二叉查找树,能够保持数据有序,拥有多余两个子树。
1.4 树的存储与表示
顺序存储:将数据结构存储在固定的数组中,然在遍历速度上有一定的优势,但因所占空间比较大,是非主流二叉树。二叉树通常以链式存储。
1.5 常见的一些树的应用场景
- xml,html等,那么编写这些东西的解析器的时候,不可避免用到树
- 路由协议就是使用了树的算法
- mysql数据库索引
- 文件系统的目录结构
- 所以很多经典的AI算法其实都是树搜索,此外机器学习中的decision tree也是树结构
二、二叉树
2.1 二叉树的基本概念
二叉树是每个节点最多有两个子树的树结构。通常子树被称作“左子树”(left subtree)和“右子树”(right subtree)
2.2 二叉树的性质(特性)
性质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 时为根,除外)
(1)完全二叉树——若设二叉树的高度为h,除第 h 层外,其它各层 (1~h-1) 的结点数都达到最大个数,第h层有叶子结点,并且叶子结点都是从左到右依次排布,这就是完全二叉树。
(2)满二叉树——除了叶结点外每一个结点都有左右子叶且叶子结点都处在最底层的二叉树。
2.3 二叉树的节点表示以及树的创建
2.3.1 Python 建树
通过使用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)
2.3.2 Java的建树
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)。那么树的两种重要的遍历模式是深度优先遍历和广度优先遍历,深度优先一般用递归,广度优先一般用队列。一般情况下能用递归实现的算法大部分也能用堆栈来实现(掌握先序、中序、后序的非递归方式)。
3.1 深度优先遍历
对于一颗二叉树,深度优先搜索(Depth First Search)是沿着树的深度遍历树的节点,尽可能深的搜索树的分支。
那么深度遍历有重要的三种方法。这三种方式常被用于访问树的节点,它们之间的不同在于访问每个节点的次序不同。这三种遍历分别叫做先序遍历(preorder),中序遍历(inorder)和后序遍历(postorder)。我们来给出它们的详细定义,然后举例看看它们的应用。
递归实现先序、中序、后序非常强大的地方是每个都会访问同一个节点三次,所以三个遍历方式只是调换一下函数执行顺序。
无论是否是递归方式都用到了栈(函数栈也是栈):因为树的结构是从上到下访问,如果要返回去访问另一处的节点,那么必须要有栈来“记忆”。
3.1.1 先序遍历
在先序遍历中,我们先访问根节点,然后递归使用先序遍历访问左子树,再递归使用先序遍历访问右子树
根节点->左子树->右子树
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);
}
}
}
}
3.1.2 中序遍历
在中序遍历中,我们递归使用中序遍历访问左子树,然后访问根节点,最后再递归使用中序遍历访问右子树
左子树->根节点->右子树
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;
}
}
}
}
3.1.3 后序遍历
在后序遍历中,我们先递归使用后序遍历访问左子树和右子树,最后访问根节点
左子树->右子树->根节点
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 + " ");
}
}
}
思考:哪两种遍历方式能够唯一的确定一颗树???
3.2 广度优先遍历(层次遍历)
通过一个队列的方法来实现
从树的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)
3.3 Morris 遍历
二叉树的遍历一般额外空间复杂度为O(logn),根据高度来的(节点回到自身需要保存到栈中),要回到上一个很难(通过栈解决)。
一种时间复杂度O(n),额外空间复杂度O(1)的二叉树的遍历方式,N为二叉树的节点个数。
Morris 遍历规则:
- 来到当前节点,记为cur,如果cur无左孩子,cur向右移动cur = cur.right
- 如果cur有左孩子:找到左子树上最右节点,记为mostright,①如果mostright的right指针指向空,让其指向cur,然后cur向左移动cur = cur.left ②如果mostright指向cur,让其指向空,cur向右移动。
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();
}
怎么实现逆序打印?
采用链表逆序的方法,打印完再调整回来,这样就没有引入额外空间复杂度
四、树的题目
4.1 如何画出一棵树
先序 + 中序
思想:
- 先序取第一位即是根,然后根据这个元素找到中序的左子树和右子树
- 先判断左子树,先序除了第一位后是连续的一块左子树的元素和连续的一块右子树元素,去先序连续一块左子树的第一位,再到中序去分割新的左子树和右子树
- 通过重复2,可以画出一个树
中序+后序也可以
4.2 二叉树中找到一个节点的后继节点
题目:现有一种新的二叉树节点类型如下
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);
}
}
先驱节点:节点有左子树,那么左子树的右节点一定是它的前驱。如果没有左子树,往上找,如果一个节点是父节点的右孩子,那么这个父节点就是前驱节点
4.3 二叉树的序列化与反序列化
序列化:
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);
}
}
同理可以学习中序、后序,层次化的序列化和反序列化
4.4 判断二叉树是否是平衡二叉树
平衡二叉树:一个树的任一节点的左子树和右子树的高度差不超过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);
}
}
4.5 如何判断一棵树是二叉搜索树
二叉搜索树:任何一个节点,左子树都比它小,右子树都比它大。
解题思路:二叉树的中序遍历节点是依次升序的就是搜索二叉树。用非递归版本的中序遍历中与前一个值进行比较:一旦产生前一个节点比后一个节点要大,说明不是二叉搜索树。
通常搜索二叉树是不出现重复节点的,一般重复的节点的信息都是压到一个节点内的(如前缀树)。
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);
}
}
4.6 怎么判断一棵树是否是完全二叉树
判断方式:二叉树按层遍历
判断依据:
- 一个节点有右孩子但是没有左孩子 ,一定不是完全二叉树
- 如果一个节点不是左右孩子都全,在1的条件下,后面遇到的所有节点都必须是叶节点,否则就不是完全二叉树
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;
}
}
补充知识:使用二叉树实现堆比数组的节省了扩容代价
4.7 已知一棵完全二叉树,求节点的个数
题目要求:时间复杂度低于O(n),n为这棵树的节点个数
时间复杂度低于O(n),说明无法采用广度优先遍历的方式获取
解题思路:
- 先遍历左子树的左边界,记录层数(完全二叉树性质,这个就是树的层数),时间复杂度为O(logn)
- 遍历右子树的左边界,是不是到了最后一层,如果到达最后一层那么左子树就是满二叉树,如果不是,那么左子树可能满可能不满。
- 如果右子树的左边界不是到最后一层(右子树少一层:右子树节点总数=1<<(h-level-1)),那么节点总数等于 1<<(h-level-1)+左树递归求总数
补充知识点:如果一棵树是一棵满二叉树,高度是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)平方