目录
一、树的基础概念
树是一种非线性的数据结构,它是由n(n>=0)个有限结点组成一个具有层次关系的集合。把它叫做树是因为它看
起来像一棵倒挂的树,也就是说它是根朝上,而叶朝下的。它具有以下的特点:
- 从根结点出发分成的M个子集,彼此之间不能相交。
- 有一个特殊的节点,称为根节点,根节点没有前驱节点
二、树的常用术语
术语 | 含义 |
---|---|
节点的度 | 一个节点含有的子树的个数;如上图:A的为6 |
树的度 | 一棵树中,最大的节点的度;如上图:树的度为6 |
叶子节点或终端节点 | 度为0的节点;如上图:B、C、H、I…等节点为叶节点 |
双亲节点或父节点 | 若一个节点含有子节点,则这个节点称为其子节点的父节点; 如上图:A是B的父节点 |
孩子节点或子节点 | 一个节点含有的子树的根节点称为该节点的子节点; 如上图:B是A的孩子节点 |
根结点 | 一棵树中,没有双亲结点的结点;如上图:A |
节点的层次 | 从根开始定义起,根为第1层,根的子节点为第2层 |
树的高度或深度 | 树中节点的最大层次; 如上图:树的高度为4 |
非终端节点或分支节点 | 度不为0的节点 |
兄弟节点 | 具有相同父节点的节点互称为兄弟节点 |
堂兄弟节点 | 双亲在同一层的节点互为堂兄弟;如上图:H、I互为兄弟节点 |
节点的祖先 | 从根到该节点所经分支上的所有节点;如上图:A是所有节点的祖先 |
子孙 | 以某节点为根的子树中任一节点都称为该节点的子孙。如上图:所有节点都是A的子孙 |
森林 | 由m(m>=0)棵互不相交的树的集合称为森林 |
三、二叉树(重点)
3.1 二叉树的定义
一棵二叉树是结点的一个有限集合,该集合或者为空,或者是由一个根节点加上两棵别称为左子树和右子树的二叉
树组成。
二叉树的特点:
- 每个结点最多有两棵子树,即二叉树不存在度大于 2 的结点。
- 二叉树的子树有左右之分,其子树的次序不能颠倒,因此二叉树是有序树。
3.2 特殊的二叉树
- 满二叉树: 一个二叉树,如果每一个层的结点数都达到最大值,则这个二叉树就是满二叉树。也就是说,如果
一个二叉树的层数为K,且结点总数是2**k** - 1,则它就是满二叉树。 - 完全二叉树: 完全二叉树是效率很高的数据结构,完全二叉树是由满二叉树而引出来的。对于深度为K的,有n
个结点的二叉树,当且仅当其每一个结点都与深度为K的满二叉树中编号从1至n的结点一一对应时称之为完全
二叉树。 要注意的是满二叉树是一种特殊的完全二叉树
- 二分查找树(BST):结点的值之间满足:左子树的节点值 < 根节点值 < 右子树的节点值
- 平衡二叉树:该树中任意一个结点的左右子树高度差<=1
3.3 二叉树的常用性质
-
若规定根节点的层数为1,则一棵非空二叉树的第i层上最多有2i-1(i>0)个结点
-
若规定只有根节点的二叉树的深度为1,则深度为K的二叉树的最大结点数是2k - 1(k>=0)
-
对任何一棵二叉树, 如果其叶结点个数为 n0, 度为2的非叶结点个数为 n2,则有n0=n2+1
推导:由于任意二叉树满足节点个数n和边长x,x = n - 1 的性质
在二叉树中只有三种节点n0,n1,n2
n0 + n1 + n2 = n //0个子树的结点 + 1个子树的结点 + 2个子树的结点
n1 + 2n2 = n-1 //边长的关系,有两个子树的一定有两条边
最终n0 = n2 + 1
-
具有n个结点的完全二叉树的深度k为 log 2 ( n − 1 ) \log_2({n - 1}) log2(n−1) 上取整
-
对于具有n个结点的完全二叉树,如果按照从上至下从左至右的顺序对所有节点从0开始编号,则对于序号为i的结点有:
-
若i>0,双亲序号:(i-1)/2;i=0,i为根节点编号,无双亲节点
-
若2i+1<n,左孩子序号:2i+1,否则无左孩子
-
若2i+2<n,右孩子序号:2i+2,否则无右孩子
-
3.4 二叉树的遍历
遍历:按照一定的顺序访问这个集合的所有元素,做到不重复,不遗漏。
3.4.1 深度优先遍历
先序遍历
可以理解为 根 左 右
先访问根节点
递归访问左子树
递归访问右子树
利用栈这个结构演示先序遍历,后面的遍历都是差不多的
根左右
- A入栈第一次访问并打印
- B入栈第一次访问并打印
- D入栈第一次访问并打印
- D没有左子树和右子树,访问了三次,已访问完毕,弹出。回到B
- 访问B的左子树,将E入栈第一次访问并打印
- E没有左子树和右子树,访问了三次,已访问完毕,弹出。回到B
- B第三次访问完毕,弹出,回到A
- 访问A的左子树,D入栈第一次访问并打印
- C访问完毕,弹出返回A
- A弹出
- 最终:ABDEC
中序遍历
可以理解为 左 根 右 利用栈就是第一次访问压入栈,第二次访问的时候打印,第三次访问完毕的时候弹出
- 递归访问左子树
- 访问根节点
- 递归访问右子树
后序遍历
可以理解为 左 根 右 利用栈就是第一次访问压入栈,第三次访问的时候打印并弹出
递归访问左子树
递归访问右子树
最后访问根节点
3.4.2 广度优先遍历
层序遍历
按照编号顺序依次访问,可以借助队列来实现
借助队列实现的步骤
- A入队
- A的左右子树不为空,BC入队,A出队
- B的左右子树不为空,DE入队,B出队
- C的右子树不为空,F入队,C出队
- D的左右子树为空,D出队
- E的左子树不为空,G入队,E出队
- F的左右子树为空,F出队
- G的右子树不为空,H入队,G出队
- H的左右子树为空,H出队
特点:
每当遍历一层结点结束,队列中存储了下一层要遍历的结点;当整个队列为空,二叉树的层序遍历就结束了。
3.4.3 举例
特点:
- 前序遍历的第一个节点一定是根节点
- 中序遍历左子树的在根节点的左侧,左子树在根节点的右侧
- 后序遍历的结果集–转置reverse(ACFBEGHD)–根右左
3.5 二叉树遍历的实现
//二叉树的结点
class Node<E>{
E val;
Node<E> left;
Node<E> right;
public Node(E val){
this.val = val;
}
}
public class BinaryTree_<E> {
Node<Character> root;
//建立一颗测试二叉树
public void build(){
Node<Character> node = new Node<>('A');
Node<Character> node1 = new Node<>('B');
Node<Character> node2 = new Node<>('C');
Node<Character> node3 = new Node<>('D');
Node<Character> node4 = new Node<>('E');
Node<Character> node5 = new Node<>('F');
Node<Character> node6 = new Node<>('G');
Node<Character> node7 = new Node<>('H');
node.left = node1;
node.right = node2;
node1.left = node3;
node1.right = node4;
node4.left = node6;
node6.right = node7;
node2.right = node5;
root = node;
}
/**
* 先序遍历
* @param root
*/
public void preorderTraversal(Node root){
if (root == null){
return;
}
//先打印根节点
System.out.println(root.val);
//递归访问左子树
preorderTraversal(root.left);
// 递归访问右子树
preorderTraversal(root.right);
}
/**
* 中序遍历
* @param root
*/
public void inorderTraversal(Node root){
if (root == null){
return;
}
preorderTraversal(root.left);
System.out.println(root.val);
preorderTraversal(root.right);
}
/**
* 后序遍历
* @param root
*/
public void postorderTraversal(Node root){
if (root == null){
return;
}
preorderTraversal(root.left);
preorderTraversal(root.right);
System.out.println(root.val);;
}
/**
* 求节点个数
* @param root
* @return
*/
//理解语义:这个函数是帮我们求节点的个数,我们只能知道根节点,下面的结点不知道,那就让子函数帮我们完成。
//最终就是左子树的结点个数子函数完成,左子树的节点个数子函数完成,最后加上根节点就搞定了
public int nodeCount(Node root){
if (root == null){
return 0;
}
//左子树结点 + 右子树结点 + 根节点
return 1 + nodeCount(root.left) + nodeCount(root.right);
}
/**
* 求叶子节点的个数
* @param root
* @return
*/
//理解语义:这个函数是帮我们求叶子节点的个数。
//当这棵树是有左子树和右子树的时候,我们只知道根节点,那么求左子树,右子树叶子节点交给子函数,最后只要把左子树叶子结点 + 右子树叶子节点就可以
public int leafNode(Node root){
//数为空,叶子节点为0
if (root == null){
return 0;
}
//只有一个节点即根节点,叶子节点为1
if (root.left == null && root.right == null){
return 1;
}
return leafNode(root.left) + leafNode(root.right);
}
/**
*第k层节点的个数
* @param root
* @param k
* @return
*/
//理解语义:这个函数是求第k层结点的个数
//当树有左子树和右子树的时候,我们只知道根节点。假设我们求得是第三层的节点个数。可以拆封成求左右子树第二层的节点个数 + 求左右子树的左右子树的第一层结点,就形成了递归
public int nodeByK(Node root, int k){
//当数为空或者k < 0,结点个数为0
if (root == null || k < 0){
return 0;
}
//k是第一层,只有一个结点的时候
if (k == 1){
return 1;
}
return nodeByK(root.left,k - 1) + nodeByK(root.right, k - 1);
}
/**
* 查找节点
* @param root
* @return
*/
//这个比较简单,现在左子树递归找符合条件的结点,找到了直接返回。没找到在递归去访问右子树找符合条件的结点,找到了返回。
public boolean find(Node root, int val){
//树为空
if (root == null){
return false;
}
//根节点就是要找的结点
if (root.val == val){
return true;
}
return find(root.left,val) || find(root.right,val);
}
/**
* 借助队列,实现二叉树的层序遍历
* @param root
*/
public void levelOrder(Node<E> root) {
//双端队列,存储的类型是Node
Deque<Node<E>> queue = new LinkedList<>();
queue.offer(root);
// 循环的终止条件就是队列为空
while (!queue.isEmpty()) {
// 取出当前层的节点个数,每当进行下一次遍历的时候,队列中就存储了该层的所有元素
int n = queue.size();
for (int i = 0; i < n; i++) {
//让结点出队并打印
Node<E> node = queue.poll();
System.out.print(node.val + " ");
if (node.left != null) {
queue.offer(node.left);
}
if (node.right != null) {
queue.offer(node.right);
}
}
}
//循环结束,代表一层遍历完毕
}
/**
* 使用层序遍历统计一颗二叉树的结点个数
* @param root
* @return
*/
public int getNodesNonRecursion(Node root){
if (root == null){
return 0;
}
int size = 0;
Deque<Node> queue = new LinkedList<>();
queue.offer(root);
while (!queue.isEmpty()){
Node cur = queue.poll();
//计数
size ++;
if (cur.left != null){
queue.offer(cur.left);
}
if (cur.right != null){
queue.offer(cur.right);
}
}
return size;
}
}