1.树
1.1什么是树
树是一种非常重要的非线性的数据结构,它在计算机科学和软件工程中有广泛的应用。树是由n个有限节点组成的集合,形状非常像一颗倒挂的树。有一个特殊的节点称为根节点,根节点没有前驱节点,其余节点分为若干个互不相交的子集,这些子集本身就是一棵树,称为根的子树。
注意:树中,子树之间不能有交集,否则就不是树
1.2树涉及的术语
节点:树中的一个元素
根节点:树的最顶层节点,没有前驱(父节点),如上图A就是根节点
父节点:一个节点的上层节点,如上图B的父节点是A,J的父节点是F
子节点:一个节点的下层节点,如上图E的子节点是I,B的子节点是E,F
子孙节点:以某节点为根的子树中任何一个节点都称为该节点的子孙,如上图所有节点都是A的子孙
兄弟节点:具有相同父节点的节点,如上图E,F是一对兄弟节点
堂兄弟节点:双亲在同一层的节点的节点,如上图F,G是一对堂兄弟节点
叶子节点:没有子节点的节点(或度为0的节点),如上图的I,J,K,H都是叶子节点
度:一个节点的子节点的个数,如上图A的度是3,E的度是1,K的度是0
深度:根节点到某个节点的路径长度
高度:从某个节点到最远节点的路径长度
路径:从一个节点到另一个节点的序列
树的高度或深度:树中节点最大的高度,如上图该树的高度为4
1.3树的表现形式
树的表现形式有很多,如双亲表示法,孩子表示法,双亲孩子表示法,孩子兄弟表示法等,最常用的就是孩子兄弟表示法
Class TreeNode{
int value;
//第一个孩子引用
TreeNode firstChild;
//下一个兄弟引用
TreeNode nextBrother;
}
1.4树的应用
1.文件系统使用树结构来组织文件和目录。每个目录是一个节点,文件时叶子节点
2.数据库索引,B树和B+树常用于数据库索引,以提高数据检索的效率
3.路由算法,在计算机网络中。树结构用于网络拓扑,帮助设计路由算法
4.决策树,在机器学习中,决策树用来分类和回归任务
2.二叉树
2.1什么是二叉树
二叉树是一种特殊的树,其中每个节点最多有2个子节点,这两个子节点通常称为左子节点和右子节点,二叉树是节点的一个有限的集合。
对于任意二叉树都是有以下几种情况符合而成的:
2.2二叉树的性质
1.若规定根节点的层数为1,则一颗非空二叉树的第i层最多有2^i-1(i>0)个节点
2.若规定只有根节点的深度为1,则深度为k的二叉树的最大节点数是(2^k)-1(k>=0)
3.对于任意一颗二叉树,如果其叶子节点个数为n0,度为2的非叶子节点个数为n2,则有n0=n2+1,对于任何一颗二叉树(非空),叶子节点的个数永远比度为2的节点个数多1
证明:
4.具有n个节点的完全二叉树的深度k=log2(n+1)向上取整
5.对于具有n个节点的完全二叉树,如果按照从上到下从左到右的顺序对所有节点从0开始编号,则对于序号i的节点有:
若i>0,父节点:(i-1)/2
若i=0,i为根节点,无父节点
若2i+1<n,左孩子序号:2i+1,否则无左孩子
若2i+2<n,右孩子序号:2i+2,否则无右孩子
2.3特殊的二叉树
1.满二叉树
一棵树二叉树,如果每层的节点数都达到最大值,则这颗二叉树就是满二叉树。也就是说,如果一颗二叉树的层数为K,且节点个数为(2^K)-1,则他就是一颗满二叉树
2.完全二叉树:
完全二叉树是效率非常高的数据结构,完全二叉树出最后一层外,每一层都被填满,并且所有节点都尽可能地向左对齐
3.平衡二叉树
任何节点的左右子树高度差不超过1,例如AVL树
4.二叉搜索树(BST)
对于每个节点,其左子树所有节点的值小于该节点的值,右子树所有节点的值大于该节点的值
2.4二叉树的存储结构
二叉树的存储结构分为:顺序存储和类似于链表的链式存储
二叉树的链式存储时通过一个一个节点引用起来的,具体如下:
//孩子表示法
class TreeNode{
int val;
//左孩子引用
TreeNode left;
//右孩子引用
TreeNode right;
}
//孩子双亲表示法
class TreeNode{
int val;
//左孩子引用
TreeNode left;
//右孩子引用
TreeNode right;
//当前节点的父节点
TreeNode parent;
}
我们主要以孩子表示法来构建二叉树
3.二叉树的基本操作
假定创建一棵二叉树
public class BinaryTree{
public static class TreeNode{
TreeNode left;
TreeNode right;
int value;
TreeNode(int value){
this.value=value;
}
}
private TreeNode root;
public void createBinaryTree(){
TreeNode node1=new TreeNode(1);
TreeNode node2=new TreeNode(2);
TreeNode node3=new TreeNode(3);
TreeNode node4=new TreeNode(4);
TreeNode node5=new TreeNode(5);
TreeNode node6=new TreeNode(6);
root=node1;
node1.left=node2;
node2.left=node3;
node1.right=node4;
node4.left=node5;
node4.right=node6;
}
}
该树的形状如下:
3.1二叉树的创建
二叉树的创建通常是给定数组,然会对数组进行遍历而创建的,二叉树的创建通常可以通过递归或者迭代的方式来实现。
代码实现:
static class TreeNode{
TreeNode left;
TreeNode right;
Character val;
public TreeNode(Character val){
this.val=val;
}
}
public static int i=0;
public static TreeNode createBinaryTree(String str){
if(str==null||str.length()==0){
return null;
}
TreeNode root=null;
if(str.charAt(i)!='#'){
root=new TreeNode(str.charAt(i));
i++;
root.left=createBinaryTree(str);
root.right=createBinaryTree(str);
}else{
i++;
}
return root;
}
3.2二叉树的遍历
遍历就是沿着某条搜索路线,依次对树中每个节点均作一次访问,遍历是二叉树最重要的操作之一,是二叉树进行其他运算的基础
1.二叉树的前序遍历(先序遍历):首先访问根节点,再访问根的左子树,最后访问根的右子树
2.二叉树的中序遍历:首先访问根的左子树,再访问根节点,最后访问根的右子树
3.二叉树的后序遍历:首先访问根的左子树,再访问根的右子树,最后访问根节点
前序遍历打印的第一个节点一定是根节点,后序遍历打印的第一个节点一定是根节点,但是中序遍历打印的中间位置的节点不一定是根节点,中序遍历中根节点左侧打印的节点为左子树的节点,右侧打印的节点为右子树的节点
前序遍历代码实现:
public void preorderTraversal(TreeNode root){
if(root==null){
return;
}
System.out.print(root.value+" ");
preorderTraversal(root.left);
preorderTraversal(root.right);
}
中序遍历代码实现:
public void inorderTraversal(TreeNode root){
if(root==null){
return;
}
inorderTraversal(root.left);
System.out.print(root.value+" ");
inorderTraversal(root.right);
}
后序遍历代码实现:
public void postorderTraversal(TreeNode root){
if(root==null){
return;
}
postorderTraversal(root.left);
postorderTraversal(root.right);
System.out.print(root.value+" ");
}
3.3获取树中节点的个数
采用子问题的思想,获取树中节点的个数,可以分解为根节点+左子树节点个数+右子树节点个数
代码编写:
public int getLeavesCount(TreeNode root){
if(root==null){
return 0;
}
return getLeavesCount(root.left)+getLeavesCount(root.right)+1;
}
3.4获取叶子节点的个数
叶子节点的特点是没有子节点,也就是left和right孩子都为空,叶子节点的计算可以通过遍历思路来实现,也可以通过子问题思想将叶子节点个数转化为左子树叶子节点个数+右子树叶子节点个数
遍历思路代码编写:
private static int leafSize=0;
public int getLeafSize(TreeNode root){
if(root==null){
return 0;
}
if(root.left==null&&root.right==null){
leafSize++;
}
getLeafSize(root.left);
getLeafSize(root.right);
return leafSize;
}
子问题思想代码实现:
public int getLeafSize2(TreeNode root){
if(root==null){
return 0;
}
if(root.left==null&&root.right==null){
return 1;
}
return getLeafSize2(root.left)+getLeafSize2(root.right);
}
3.5获取第K层节点的个数
当根节点为空时,自然第k曾就不存在了,同样当k<=0时,k层的节点个数也就无意义了,自然返回0。通过创建队列queue用于存放每层的节点,每次循环都要将上一次queue中的元素全部删除,这样while循环每完成一次,就代表开始下一层了,即queue存放了新的一层的元素,当循环次数等于k-1时,queue中的元素个数就是第k层的节点数。while的第一次循环,queue中只有一个元素,将该元素出队列,观察该元素的左右孩子是否为空,如果不为空,将其入队列,这样queue中存放了第二层的节点。while的第二次循环,将queue中的元素依次出队列,并分别判断出队列的元素的左右孩子是否为空,不为空,则进行入队列操作,for循环结束,queue就已经存放了第三层的元素,依次类推
public int getKLevelNodeCount(TreeNode root, int k) {
if (root == null || k <= 0) {
return 0;
}
if (k == 1) {
return 1;
}
Queue<TreeNode> queue = new LinkedList<>();
queue.offer(root);
int level = 1;
int count = 0;
while (!queue.isEmpty()) {
int size = queue.size();
if (level == k) {
count = size;
break;
}
level++;
for (int i = 0; i < size; i++) {
TreeNode node = queue.poll();
if (node.left != null) {
queue.offer(node.left);
}
if (node.right != null) {
queue.offer(node.right);
}
}
}
return count;
}
3.6获取二叉树的高度
树的高度可以采用子问题的思想:求根节点左子树,右子树高度的较大值+1
代码编写:
public int getTreeHeight(TreeNode root){
if(root==null){
return 0;
}
int leftHeight=getTreeHeight(root.left)+1;
int rightHeight=getTreeHeight(root.right)+1;
return leftHeight>rightHeight?leftHeight:rightHeight;
}
3.7检测值为value的元素是否存在
思路:遍历二叉树,看是否存在value元素
代码编写:
public boolean contains(TreeNode root,int value){
if(root==null){
return false;
}
if(root.value==value){
return true;
}
return contains(root.left,value)|| contains(root.right,value);
}
3.8层序遍历
层序遍历:即从上到下从左到右依次打印树中的元素(每一层从左到右打印,从第一层开始到最后一层)
public void levelOrderTraversal(TreeNode root){
if(root==null){
return;
}
System.out.print(root.value+" ");
Queue<TreeNode> queue=new LinkedList<>();
queue.offer(root);
while(!queue.isEmpty()){
TreeNode node=queue.poll();
if(node.left!=null){
System.out.print(node.left.value+" ");
queue.offer(node.left);
}
if (node.right!=null){
System.out.print(node.right.value+" ");
queue.offer(node.right);
}
}
}
3.9判断一棵树是不是完全二叉树
完全二叉树的特点是,从上到下、从左到右,每一层的节点都被填满,直到最后一层。并且最后一层的节点都尽可能地集中在左侧。
思路:
在遍历过程中,一旦遇到第一个空节点,标记这个状态。从这个空节点开始,如果再遇到非空节点,则说明该树不是完全二叉树,因为完全二叉树的最后层的节点应该尽可能地集中在左侧。如果所有节点都被访问,且没有在遇到第一个空节点后出现非空节点,则该树是完全二叉树
代码实现:
public boolean isCompleteTree(TreeNode root) {
if (root == null) return true;
Queue<TreeNode> queue = new LinkedList<>();
queue.offer(root);
boolean end = false; // 用来标记是否遇到了第一个null节点
while (!queue.isEmpty()) {
TreeNode node = queue.poll();
if (node == null) {
end = true; // 标记遇到了第一个null节点
} else {
if (end) return false; // 如果已经标记了end,说明之前已经出现了null节点,此时不应该再出现非null节点
queue.offer(node.left);
queue.offer(node.right);
}
}
return true;
}
二叉树涉及的知识和内容还有很多,再次我们不仅一一进行列举说明。