您的位置:首页 > 理论基础 > 数据结构算法

【从今天开始好好学数据结构04】程序员你心中就没点“树”吗?

2019-11-20 08:07 537 查看

目录

前面我们讲的都是线性表结构,栈、队列等等。今天我们讲一种非线性表结构,树。树这种数据结构比线性表的数据结构要复杂得多,内容也比较多,首先我们先从树(Tree)开始讲起。
@

树(Tree)

树型结构是一种非线性结构,它的数据元素之间呈现分支、分层的特点。

1.树的定义

树(Tree)是由n(n≥0)个结点构成的有限集合T,当n=0时T称为空树;否则,在任一非空树T中:
(1)有且仅有一个特定的结点,它没有前驱结点,称其为根(Root)结点;
(2)剩下的结点可分为m(m≥0)个互不相交的子集T1,T2,…,Tm,其中每个子集本身又是一棵树,并称其为根的子树(Subtree)。

注意:树的定义具有递归性,即“树中还有树”。树的递归定义揭示出了树的固有特性

2.什么是树结构

什么是“树”?再好的定义,都没有图解来的直观。所以我在图中画了几棵“树”。你来看看,这些“树”都有什么特征?

你有没有发现,“树”这种数据结构真的很像我们现实生活中的“树”

3.为什么使用树结构

在有序数组中,可以快速找到特定的值,但是想在有序数组中插入一个新的数据项,就必须首先找出新数据项插入的位置,然后将比新数据项大的数据项向后移动一位,来给新的数据项腾出空间,删除同理,这样移动很费时。显而易见,如果要做很多的插入和删除操作和删除操作,就不该选用有序数组。另一方面,链表中可以快速添加和删除某个数据项,但是在链表中查找数据项可不容易,必须从头开始访问链表的每一个数据项,直到找到该数据项为止,这个过程很慢。 树这种数据结构,既能像链表那样快速的插入和删除,又能想有序数组那样快速查找

4.树的常用术语

结点——包含一个数据元素和若干指向其子树的分支
度——结点拥有的子树个数
树的度——该树中结点的最大度数
叶子——度为零的结点
分支结点(非终端结点)——度不为零的结点
孩子和双亲——结点的子树的根称为该结点的孩子,相应地,该结点称为孩子的双亲
兄弟——同一个双亲的孩子
祖先和子孙——从根到该结点所经分支上的所有结点。相应地,以某一结点为根的子树中的任一结点称为该结点的子孙。
结点的层次——结点的层次从根开始定义,根结点的层次为1,其孩子结点的层次为2,……
堂兄弟——双亲在同一层的结点
树的深度——树中结点的最大层次
有序树和无序树——如果将树中每个结点的各子树看成是从左到右有次序的(即位置不能互换),则称该树为有序树;否则称为无序树。
森林——m(m≥0)棵互不相交的树的有限集合


到这里,树就讲的差不多了,接下来讲讲二叉树(Binary Tree)

二叉树(Binary Tree)

树结构多种多样,不过我们最常用还是二叉树,我们平时最常用的树就是二叉树。二叉树的每个节点最多有两个子节点,分别是左子节点和右子节点。二叉树中,有两种比较特殊的树,分别是满二叉树和完全二叉树。满二叉树又是完全二叉树的一种特殊情况。

1.二叉树的定义和特点

二叉树的定义:
二叉树(Binary Tree)是n(n≥0)个结点的有限集合BT,它或者是空集,或者由一个根结点和两棵分别称为左子树和右子树的互不相交的二叉树组成 。
————————————
二叉树的特点:
每个结点至多有二棵子树(即不存在度大于2的结点);二叉树的子树有左、右之分,且其次序不能任意颠倒。

2.几种特殊形式的二叉树

1、满二叉树:
定义:深度为k且有2k-1个结点的二叉树,称为满二叉树。
特点:每一层上的结点数都是最大结点数
2、完全二叉树:
定义:
深度为k,有n个结点的二叉树当且仅当其每一个结点都与深度为k的满二叉树中编号从1至n的结点一一对应时,称为完全二叉树
特点:
特点一 : 叶子结点只可能在层次最大的两层上出现;
特点二 : 对任一结点,若其右分支下子孙的最大层次为l,则其左分支下子孙的最大层次必为l 或l+1

建议看图对应文字综合理解


代码创建二叉树

首先,创建一个节点Node类

package demo5;
/*
* 节(结)点类
*/
public class Node {
//节点的权
int value;
//左儿子(左节点)
Node leftNode;
//右儿子(右节点)
Node rightNode;
//构造函数,初始化的时候就给二叉树赋上权值
public Node(int value) {
this.value=value;
}

//设置左儿子(左节点)
public void setLeftNode(Node leftNode) {
this.leftNode = leftNode;
}
//设置右儿子(右节点)
public void setRightNode(Node rightNode) {
this.rightNode = rightNode;
}

接着创建一个二叉树BinaryTree 类

package demo5;
/*
* 二叉树Class
*/
public class BinaryTree {
//根节点root
Node root;

//设置根节点
public void setRoot(Node root) {
this.root = root;
}

//获取根节点
public Node getRoot() {
return root;
}
}

最后创建TestBinaryTree 类(该类主要是main方法用来测试)来创建一个二叉树

package demo5;
public class TestBinaryTree {

public static void main(String[] args) {
//创建一颗树
BinaryTree binTree = new BinaryTree();
//创建一个根节点
Node root = new Node(1);
//把根节点赋给树
binTree.setRoot(root);
//创建一个左节点
Node rootL = new Node(2);
//把新创建的节点设置为根节点的子节点
root.setLeftNode(rootL);
//创建一个右节点
Node rootR = new Node(3);
//把新创建的节点设置为根节点的子节点
root.setRightNode(rootR);
//为第二层的左节点创建两个子节点
rootL.setLeftNode(new Node(4));
rootL.setRightNode(new Node(5));
//为第二层的右节点创建两个子节点
rootR.setLeftNode(new Node(6));
rootR.setRightNode(new Node(7));
}

}

下面将会讲的遍历、查找节点、删除节点都将围绕这三个类开展

不难看出创建好的二叉树如下(画的不好,还望各位见谅):

3.二叉树的两种存储方式

二叉树既可以用链式存储,也可以用数组顺序存储。数组顺序存储的方式比较适合完全二叉树,其他类型的二叉树用数组存储会比较浪费存储空间,所以链式存储更合适。

我们先来看比较简单、直观的链式存储法

接着是基于数组的顺序存储法(该例子是一棵完全二叉树)

上面例子是一棵完全二叉树,所以仅仅“浪费”了一个下标为0的存储位置。如果是非完全二叉树,则会浪费比较多的数组存储空间,如下。


还记得堆和堆排序吗,堆其实就是一种完全二叉树,最常用的存储方式就是数组。

4.二叉树的遍历

前面我讲了二叉树的基本定义和存储方法,现在我们来看二叉树中非常重要的操作,二叉树的遍历。这也是非常常见的面试题。

经典遍历的方法有三种,前序遍历中序遍历后序遍历

前序遍历是指,对于树中的任意节点来说,先打印这个节点,然后再打印它的左子树,最后打印它的右子树。

中序遍历是指,对于树中的任意节点来说,先打印它的左子树,然后再打印它本身,最后打印它的右子树。

后序遍历是指,对于树中的任意节点来说,先打印它的左子树,然后再打印它的右子树,最后打印这个节点本身。


我想,睿智的你已经想到了二叉树的前、中、后序遍历就是一个递归的过程。比如,前序遍历,其实就是先打印根节点,然后再递归地打印左子树,最后递归地打印右子树。

在之前创建好的二叉树代码之上,我们来使用这三种方法遍历一下~

依旧是在Node节点类上添加方法:可以看出遍历方法都是用的递归思想

package demo5;
/*
* 节(结)点类
*/
public class Node {
//===================================开始 遍历========================================
//前序遍历
public void frontShow() {
//先遍历当前节点的内容
System.out.println(value);
//左节点
if(leftNode!=null) {
leftNode.frontShow();
}
//右节点
if(rightNode!=null) {
rightNode.frontShow();
}
}

//中序遍历
public void midShow() {
//左子节点
if(leftNode!=null) {
leftNode.midShow();
}
//当前节点
System.out.println(value);
//右子节点
if(rightNode!=null) {
rightNode.midShow();
}
}

//后序遍历
public void afterShow() {
//左子节点
if(leftNode!=null) {
leftNode.afterShow();
}
//右子节点
if(rightNode!=null) {
rightNode.afterShow();
}
//当前节点
System.out.println(value);
}

}

然后依旧是在二叉树BinaryTree 类上添加方法,并且添加的方法调用Node类中的遍历方法

package demo5;
/*
* 二叉树Class
*/
public class BinaryTree {

public void frontShow() {
if(root!=null) {
//调用节点类Node中的前序遍历frontShow()方法
root.frontShow();
}
}

public void midShow() {
if(root!=null) {
//调用节点类Node中的中序遍历midShow()方法
root.midShow();
}
}

public void afterShow() {
if(root!=null) {
//调用节点类Node中的后序遍历afterShow()方法
root.afterShow();
}
}

}

依旧是在TestBinaryTree类中测试

package demo5;

public class TestBinaryTree {

public static void main(String[] args) {
//前序遍历树
binTree.frontShow();
System.out.println("===============");
//中序遍历
binTree.midShow();
System.out.println("===============");
//后序遍历
binTree.afterShow();
System.out.println("===============");
//前序查找
Node result = binTree.frontSearch(5);
System.out.println(result);

}

如果递归理解的不是很透,我可以分享一个学习的小方法:我建议各位可以这样断点调试,一步一步调,思维跟上,仔细推敲每一步的运行相信我,你会重新认识到递归!(像下面这样贴个图再一步一步断点思维更加清晰)

贴一下我断点对递归的分析,希望对你有一定的帮助~

二叉树遍历的递归实现思路自然、简单,易于理解,但执行效率较低。为了提高程序的执行效率,可以显式的设置栈,写出相应的非递归遍历算法。非递归的遍历算法可以根据递归算法的执行过程写出。至于代码可以尝试去写一写,这也是一种提升!具体的非递归算法主要流程图贴在下面了:

二叉树遍历算法分析:

二叉树遍历算法中的基本操作是访问根结点,不论按哪种次序遍历,都要访问所有的结点,对含n个结点的二叉树,其时间复杂度均为O(n)。所需辅助空间为遍历过程中所需的栈空间,最多等于二叉树的深度k乘以每个结点所需空间数,最坏情况下树的深度为结点的个数n,因此,其空间复杂度也为O(n)。

5.二叉树中节点的查找与删除

刚才讲到二叉树的三种金典遍历放法,那么节点的查找同样是可以效仿的,分别叫做前序查找、中序查找以及后序查找,下面代码只以前序查找为例,三者查找方法思路类似~

至于删除节点,有三种情况:

1、如果删除的是根节点,那么二叉树就完全被删了
2、如果删除的是双亲节点,那么该双亲节点以及他下面的所有子节点所构成的子树将被删除
3、如果删除的是叶子节点,那么就直接删除该叶子节点

那么,我把完整的三个类给贴出来(包含创建、遍历、查找、删除)

依旧是Node节点类

package demo5;
/*
* 节(结)点类
*/
public class Node {
//节点的权
int value;
//左儿子
Node leftNode;
//右儿子
Node rightNode;
//构造函数,初始化的时候就给二叉树赋上权值
public Node(int value) {
this.value=value;
}

//设置左儿子
public void setLeftNode(Node leftNode) {
this.leftNode = leftNode;
}
//设置右儿子
public void setRightNode(Node rightNode) {
this.rightNode = rightNode;
}

//前序遍历
public void frontShow() {
//先遍历当前节点的内容
System.out.println(value);
//左节点
if(leftNode!=null) {
leftNode.frontShow();
}
//右节点
if(rightNode!=null) {
rightNode.frontShow();
}
}

//中序遍历
public void midShow() {
//左子节点
if(leftNode!=null) {
leftNode.midShow();
}
//当前节点
System.out.println(value);
//右子节点
if(rightNode!=null) {
rightNode.midShow();
}
}

//后序遍历
public void afterShow() {
//左子节点
if(leftNode!=null) {
leftNode.afterShow();
}
//右子节点
if(rightNode!=null) {
rightNode.afterShow();
}
//当前节点
System.out.println(value);
}

//前序查找
public Node frontSearch(int i) {
Node target=null;
//对比当前节点的值
if(this.value==i) {
return this;
//当前节点的值不是要查找的节点
}else {
//查找左儿子
if(leftNode!=null) {
//有可能可以查到,也可以查不到,查不到的话,target还是一个null
target = leftNode.frontSearch(i);
}
//如果不为空,说明在左儿子中已经找到
if(target!=null) {
return target;
}
//查找右儿子
if(rightNode!=null) {
target=rightNode.frontSearch(i);
}
}
return target;
}

//删除一个子树
public void delete(int i) {
Node parent = this;
//判断左儿子
if(parent.leftNode!=null&&parent.leftNode.value==i) {
parent.leftNode=null;
return;
}
//判断右儿子
if(parent.rightNode!=null&&parent.rightNode.value==i) {
parent.rightNode=null;
return;
}

//递归检查并删除左儿子
parent=leftNode;
if(parent!=null) {
parent.delete(i);
}

//递归检查并删除右儿子
parent=rightNode;
if(parent!=null) {
parent.delete(i);
}
}

}

依旧是BinaryTree 二叉树类

package demo5;
/*
* 二叉树Class
*/
public class BinaryTree {
//根节点root
Node root;

//设置根节点
public void setRoot(Node root) {
this.root = root;
}

//获取根节点
public Node getRoot() {
return root;
}

public void frontShow() {
if(root!=null) {
//调用节点类Node中的前序遍历frontShow()方法
root.frontShow();
}
}

public void midShow() {
if(root!=null) {
//调用节点类Node中的中序遍历midShow()方法
root.midShow();
}
}

public void afterShow() {
if(root!=null) {
//调用节点类Node中的后序遍历afterShow()方法
root.afterShow();
}
}
//查找节点i
public Node frontSearch(int i) {
return root.frontSearch(i);
}
//删除节点i
public void delete(int i) {
if(root.value==i) {
root=null;
}else {
root.delete(i);
}
}

}

依旧是TestBinaryTree测试类

package demo5;

public class TestBinaryTree {

public static void main(String[] args) {
//创建一颗树
BinaryTree binTree = new BinaryTree();
//创建一个根节点
Node root = new Node(1);
//把根节点赋给树
binTree.setRoot(root);
//创建一个左节点
Node rootL = new Node(2);
//把新创建的节点设置为根节点的子节点
root.setLeftNode(rootL);
//创建一个右节点
Node rootR = new Node(3);
//把新创建的节点设置为根节点的子节点
root.setRightNode(rootR);
//为第二层的左节点创建两个子节点
rootL.setLeftNode(new Node(4));
rootL.setRightNode(new Node(5));
//为第二层的右节点创建两个子节点
rootR.setLeftNode(new Node(6));
rootR.setRightNode(new Node(7));
//前序遍历树
binTree.frontShow();
System.out.println("===============");
//中序遍历
binTree.midShow();
System.out.println("===============");
//后序遍历
binTree.afterShow();
System.out.println("===============");
//前序查找
Node result = binTree.frontSearch(5);
System.out.println(result);

System.out.println("===============");
//删除一个子树
binTree.delete(4);
binTree.frontShow();

}

}

到这里,总结一下,我们学了一种非线性表数据结构,树。关于树,有几个比较常用的概念你需要掌握,那就是:根节点、叶子节点、父节点、子节点、兄弟节点,还有节点的高度、深度、层数,以及树的高度等。我们平时最常用的树就是二叉树。二叉树的每个节点最多有两个子节点,分别是左子节点和右子节点。二叉树中,有两种比较特殊的树,分别是满二叉树和完全二叉树。满二叉树又是完全二叉树的一种特殊情况。二叉树既可以用链式存储,也可以用数组顺序存储。数组顺序存储的方式比较适合完全二叉树,其他类型的二叉树用数组存储会比较浪费存储空间。除此之外,二叉树里非常重要的操作就是前、中、后序遍历操作,遍历的时间复杂度是O(n),你需要理解并能用递归代码来实现。

如果本文章对你有帮助,哪怕是一点点,请点个赞呗,谢谢~

欢迎各位关注我的公众号,一起探讨技术,向往技术,追求技术...说好了来了就是盆友喔...

内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: