一、非线性结构
1.非线性结构
由之前的学习,我们知道,线性结构是一个数据元素的集合,每个结点最多只有一个前驱结点和一个后继结点。而非线性结构则是没有这个限制,其特征就是每个结合中的结点都可能有多个前驱和多个后继。
通常我们将最多只有一个前驱结点,但可能有多个后继结点的非线性结构称为树;而前驱结点和后继结点个数不限的非线性结构则称为图。
二、树
1.树的定义
树与线性表一样,也是由n个结点组成的有限集合,并且是个有层次关系的集合。称其为树则是因为其结构图看起来像一棵都过来的树。
![3673a0853150572d380db73789923ba94ef.jpg](https://oscimg.oschina.net/oscnet/3673a0853150572d380db73789923ba94ef.jpg)
当树中的结点数n=0时,称这棵树为空数;一棵非空数具有如下特征:
-
有且仅有一个根结点(root),root结点没有前驱结点,仅能有后继结点。
-
当树中的结点数n>1时,除根结点外,其余结点可以分为m(m>0)个互不相交的有限集合T1,T2...Tm;其中每个集合都是一棵树。这些集合T1,T2...Tm称为这棵树的子树。
如上图所示:结点2,3,4都是原树的一棵子树,即T1={2};T2={3,6,7};T3={4}
树结构在生活中的常见应用有:文件系统目录,单位公司的组织架构。
2.树的相关概念
-
树的结点:树中的每一个元素都称作树的一个结点;下图的A,B,C,D,E,F,G,H,I,J都是一个结点。
-
结点的度:一个结点含有子树的个数,称为该结点的度;下图中A结点的度为3;B,C,D结点的度FE分别为3,1,2;E,F,G,H,I,J结点的度为0。
-
叶子结点/终端结点:度为0的结点称为叶子(leaf)结点;下图中E,F,G,H,I,J为叶子结点。
-
非终端结点/分支结点:度不为0的结点;下图中A,B,C,D为分支结点,而除root结点外的分支结点也成为内部结点。
-
树的度:一棵树中所有结点的度中值最大的度称为树的结点;下图中树的度为3。
-
子结点:树中一个结点的直接后继结点,称为该结点的子结点(child);下图中A结点的子结点为B,C,D;B结点的子结点为E,F,G;C的子结点为H;D的子结点为I,J;E,F,G,H,I,J没有子结点。
-
父结点:树中结点的直接前驱结点,称为该结点的父结点。下图中B,C,D结点的父结点是A;E,F,G的父结点是B;H结点的父结点是C;I,J节点的父结点是D;根结点永远没有父结点。
-
兄弟结点:父结点相同的结点互称为兄弟结点。下图中B,C,D之间为兄弟结点;E,F,G为兄弟结点;I,J之间为兄弟节点;H没有兄弟节点。
-
堂兄弟结点:父结点是兄弟结点的结点互称为堂兄弟结点。下图中B,C,D的子结点之间都是堂兄弟结点。
-
子孙结点:以某结点为根结点的树中任意结点(不包括根结点自身)都是该结点的子孙结点。下图中A结点的子孙结点是B,C,D,E,F,G,H,I,J。
-
祖先结点:从根结点到该结点所经路径上的所有结点,都是其祖先结点。下图中E的祖先结点有A,B。
![7db0db4a2e4c4d2d79db517f0c5d9f9156e.jpg](https://oscimg.oschina.net/oscnet/7db0db4a2e4c4d2d79db517f0c5d9f9156e.jpg)
-
结点的层次:从树的根结点开始,根结点为第一层,根 的子结点为第二层,以此类推。如下图所示。
-
树的高度/深度:树中结点的最大层次,即为树的高度/深度。下图所示树的深度为4。
-
森林:m(m>=0)棵互不相交的树的集合,称为森林。
![c2e2dd404bec89d18ea0149923c6a0159a1.jpg](https://oscimg.oschina.net/oscnet/c2e2dd404bec89d18ea0149923c6a0159a1.jpg)
3.树的分类
树的常见分类有:有序树,无序树,m叉树(二叉树)等。
-
有序树:若树中的结点的各个子树看作是从左往右有次序的(即各子树的位置不能改变),则成为有序树。一般树中讨论的都是有序树。
-
无序树:树中各结点的子树顺序无关紧要,则称其为无序树。
-
m叉树:树中结点度最大为m的有序树,我们称其为m叉树。
由于二叉树最常用,因此一般只需要学二叉树就可以了。
二、二叉树
1.二叉树的定义
一棵有序树若其任意结点的度均不大于2,我们称其为二叉树;二叉树要么是一个棵空树,要么是一棵由根结点和两个互不相交的根的左子树和右子树组成的非空树,且其所有子树也满足上述要求。即:
![125d9b12f0398c7ae0074646f388a7b161d.jpg](https://oscimg.oschina.net/oscnet/125d9b12f0398c7ae0074646f388a7b161d.jpg)
-
二叉树中任意结点的子结点个数都不能超过2,并且子结点有左右之分,即子结点位置不能随意调换,如上图从C,E的位置不能互换。
-
一个结点的子结点里,位于左边的称为左孩子,右边的称为右孩子,其左右孩子构成的子树称为左右子树。
![636075bf759a1cd09e3abfaa5edc4b92af2.jpg](https://oscimg.oschina.net/oscnet/636075bf759a1cd09e3abfaa5edc4b92af2.jpg)
2.满二叉树和完全二叉树
满二叉树:一棵高度为n的且包含有2^n-1个结点的二叉树,称之为满二叉树。在满二叉树中,其每一层的结点数都达到其所能容纳的最大个数,即每一层上的结点都是满的,因而称为满二叉树。
完全二叉树:若一棵满二叉树中,从高度最大的层次的最右侧起,去掉若干个叶子结点,其构成的二叉树就称之为完全二叉树。
![6b12a242d90e31d4643b06255e58380c58d.jpg](https://oscimg.oschina.net/oscnet/6b12a242d90e31d4643b06255e58380c58d.jpg)
由以上定义可知:一棵满二叉树必然是一棵完全二叉树,反之则不成立。
3.二叉树的性质
由前面二叉树的定义及特点可以得到一些二叉树的性质或规律:
-
二叉树的每一层最多可以有2^(k-1)个结点(k为层数)。
-
一棵高度为k的二叉树,最多可以拥有2^k-1个结点。
-
对于任意一棵二叉树,若其有叶子结点的个数为n,度为2的结点数为m,则恒有n=m+1。
-
有n个结点的二叉树,其高度至多为(可看作一个线性表)n,至少为[log2n]+1(即完全二叉树);对数值向下取整。
![2770824b7dd24ddd896b131d3aba8afeac9.jpg](https://oscimg.oschina.net/oscnet/2770824b7dd24ddd896b131d3aba8afeac9.jpg)
4.二叉树的存储结构
与线性结构相似,二叉树也分为顺序存储和链式存储。
二叉树的顺序存储:对于满二叉树和完全二叉树而言,顺序存储是可以将数据元素从上至下,从左至右逐层逐个放到一个数组当中(将第i个结点存放到数组的i位置中),则可得到:
![563d3572148c8f12bface451ac0c0ce0677.jpg](https://oscimg.oschina.net/oscnet/563d3572148c8f12bface451ac0c0ce0677.jpg)
则有结点i的左右孩子分别是结点2i,2i+1;并且对于完全二叉树及满二叉树来说,数组的空间没有浪费,也能通过计算极快的得到子结点的位置,结点之间的关系也能通过公式确定。但对于一般的二叉树而言,却并不能尽善尽美,若也如完全二叉树一样,结点编号即是结点在数组中的位置,则必须使用‘虚结点’来占位,以保证父子结点之间满足:i为父结点则左右孩子必是2i和2i+1的关系:
![e3b110916c74b185288503414a816424fcf.jpg](https://oscimg.oschina.net/oscnet/e3b110916c74b185288503414a816424fcf.jpg)
如上图所示,数组中索引4,6,7,8,9,10,11,12,13都必须是‘虚结点’,这就造成了数组空间的浪费。但要是直接按顺序对二叉树进行存储,虽然空间利用率得到改善,但父子结点之间的关系将不复存在,即单纯得到一个数组,结点关系都丢失了,更不可行。
为了解决上述问题,通常二叉树采用另一种存储方式:链式存储。
![072a9fb70eda1a063c8e9dce299dc97c523.jpg](https://oscimg.oschina.net/oscnet/072a9fb70eda1a063c8e9dce299dc97c523.jpg)
二叉树的链式存储结构将二叉树的每个结点设计成至少三个区域:数据存储域(data),左孩子域(left-child)和右孩子域(right-child),其中data存储数据元素,left-child存放左孩子结点地址,right-child存放右孩子结点地址,由此可得到一个二叉链表:
若要支持逆向查询,可在增加一个父结点的指针域,则有:
三、二叉树的实现
1.二叉树的遍历
遍历是指按照一定次序访问集合中的所有结点,且每个结点刚好都只访问了一次。前面学过的线性表的遍历比较简单,只需按数组下标挨个访问或者根据链表头结点的指针挨个访问即可。但二叉树的遍历则不同,二叉的遍历方式多种多样,主要有深度优先和层次遍历(用的少),我们主要讨论常用的几种;
若将二叉树分为左子树,根,右子树三个部分来看待,规定遍历时的顺序是先左子树再右子树(二叉树中的所有子树都满足)。那么就可得三种遍历方式:
-
先序遍历(DLR):根→左子树→右子树。
-
中序遍历(LDR):左子树→根→右子树。
-
后序遍历(LRD):左子树→右子树→根。
示例:求下图的三种遍历结果
![ca531f35d2cc17c211ab908b1edbeee6ea9.jpg](https://oscimg.oschina.net/oscnet/ca531f35d2cc17c211ab908b1edbeee6ea9.jpg)
DLR结果:A,B,C,D,E,F;
LDR结果:C,B,D,A,E,F;
LRD结果:C,D,B,F,E,A;
遍历技巧:先将树树分为三个部分(已上图先序遍历为例):根A部分,左子树B部分,右子树E部分。则有[A],[B,C,D],[E,F];接着再对B子树和E子树分别进行先序遍历可得到:[A],[[B],[C],[D]],[[E],[F]]。同理,进行中序遍历时有[B,C,D],[A],[E,F];再对B子树和E子树分别进行中序遍历可得到:[[C],[B],[D]],[A],[[E],[F]]。后序遍历:[[C],[D],[B]],[[F],[E]],[A]。
因为二叉树本身是一个递归定义的结构,所以这三种遍历都可通过递归来实现。
常见面试题,根据中序遍历和后序(先序)遍历,推出先序遍历的结果。例子:某二叉树的后序遍历是5,4,3,7,6,2,1;中序是4,5,1,3,2,6,7;先序是什么?(1,4,5,2,3,6,7)
一个小结论,由中序和后序,可推出先序;中序和先序可推出后序;但先序和后序推不出中序。
2.二叉树的Java实现
二叉树中结点的存储结构以链式存储为例:
/**
* 二叉树结点的实现,以链式存储为例
* Created by bzhang on 2019/2/28.
*/
public class TreeNode {
private Object data; //存储数据元素
private TreeNode left; //左孩子
private TreeNode right; //右孩子
private TreeNode parent; //父结点
//构造方法
public TreeNode() {
}
//构造方法
public TreeNode(Object data, TreeNode left, TreeNode right, TreeNode parent) {
this.data = data;
this.left = left;
this.right = right;
this.parent = parent;
}
public Object getData() {
return data;
}
public void setData(Object data) {
this.data = data;
}
public TreeNode getLeft() {
return left;
}
public void setLeft(TreeNode left) {
this.left = left;
}
public TreeNode getRight() {
return right;
}
public void setRight(TreeNode right) {
this.right = right;
}
public TreeNode getParent() {
return parent;
}
public void setParent(TreeNode parent) {
this.parent = parent;
}
}
二叉树功能接口结构:
/**
* 二叉树功能接口
* Created by bzhang on 2019/2/28.
*/
public interface BTree {
//二叉树中的结点数
public int size();
//判读是否是空树
public boolean isEmpty();
//树的高度
public int getHeight();
//获取根结点
public TreeNode getRoot();
//根据索引查找结点
public TreeNode find(int key);
//中序遍历
public void inOrder();
//先序遍历
public void preOrder();
//后序遍历
public void postOrder();
//根据传入的结点后序遍历
public void postOrder(TreeNode root);
//中序遍历(不使用递归)
public void inOrderByStack();
//先序遍历(不使用递归)
public void preOrderByStack();
//后序遍历(不使用递归)
public void postOrderByStack();
//层次遍历
public void levelOrder();
}
链式实现二叉树:
/**
* 链式存储实现二叉树功能
* Created by bzhang on 2019/2/28.
*/
public class LinkedBTree implements BTree{
//跟结点
private TreeNode root;
public void setRoot(TreeNode root) {
this.root = root;
}
//传入root结点,构造一个二叉树
public LinkedBTree(TreeNode root) {
this.root = root;
}
@Override
public int size() {
return size(root);
}
//计算树中的节点个数
private int size(TreeNode root) {
if (root==null){
return 0;
}else {
int right = size(root.getRight());
int left = size(root.getLeft());
return right+left+1;
}
}
@Override
public boolean isEmpty() {
return root==null;
}
@Override
public int getHeight() {
return getHeight(root);
}
//递归计算二叉树高度
private int getHeight(TreeNode root) {
if (root==null){
return 0;
}else {
int r = getHeight(root.getRight());
int l = getHeight(root.getLeft());
return r>l?(r+1):(l+1);
}
}
@Override
public TreeNode getRoot() {
return root;
}
@Override
public TreeNode find(Object key) {
return find(key,this.root);
}
//递归查询二叉树中是否存在key
public TreeNode find(Object key,TreeNode root){
if (root==null){
return null;
}else if (key.equals(root.getData())){
return root;
}else {
TreeNode left = find(key, root.getLeft());
TreeNode right = find(key, root.getRight());
if (left!=null)
return left;
else if (right!=null)
return right;
else
return null;
}
}
@Override
public void inOrder() {
checkedNotNull(root);
System.out.print("递归实现中序遍历:");
inOrderRecursive(root);
System.out.println("");
}
//递归实现中序遍历
private void inOrderRecursive(TreeNode root) {
if (root.getLeft()!=null){
inOrderRecursive(root.getLeft());
}
System.out.print(root.getData());
if (root.getRight()!=null)
inOrderRecursive(root.getRight());
}
@Override
public void preOrder() {
checkedNotNull(root);
System.out.print("递归实现前序遍历:");
preOrderRecursive(root);
System.out.println("");
}
//递归实现前序遍历
private void preOrderRecursive(TreeNode root) {
System.out.print(root.getData());
if (root.getLeft()!=null){
preOrderRecursive(root.getLeft());
}
if (root.getRight()!=null)
preOrderRecursive(root.getRight());
}
//判断是否为空树
private void checkedNotNull(TreeNode root) {
if (root==null){
throw new RuntimeException("空树不能进行遍历");
}
}
@Override
public void postOrder() {
checkedNotNull(root);
System.out.print("递归实现后序遍历:");
postOrderRecursive(root);
System.out.println("");
}
//递归实现后序遍历
private void postOrderRecursive(TreeNode root) {
if (root.getLeft()!=null)
postOrderRecursive(root.getLeft());
if (root.getRight()!=null)
postOrderRecursive(root.getRight());
System.out.print(root.getData());
}
@Override
public void postOrder(TreeNode node) {
checkedNotNull(node);
postOrderRecursive(node);
}
@Override
public void inOrderByStack() {
checkedNotNull(root);
System.out.print("中序遍历(非递归):");
Deque<TreeNode> stack = new LinkedList(); //栈用于存取二叉树中的结点,先进后出的特性,中序遍历时可让根先进,左孩子后进
TreeNode temp = root;
while (temp!=null||!stack.isEmpty()){
while (temp!=null){ //循环将根及左孩子放入栈中
stack.push(temp);
temp = temp.getLeft();
}
if (!stack.isEmpty()){
temp = stack.pop();
System.out.print(temp.getData());
temp = temp.getRight();
}
}
System.out.println("");
}
@Override
public void preOrderByStack() {
checkedNotNull(root);
System.out.print("前序遍历(非递归):");
Deque<TreeNode> stack = new LinkedList();
TreeNode temp = root;
while (temp!=null||!stack.isEmpty()){
while (temp!=null){
System.out.print(temp.getData());
stack.push(temp);
temp = temp.getLeft();
}
if (!stack.isEmpty()){
temp = stack.pop();
temp = temp.getRight();
}
}
System.out.println("");
}
@Override
public void postOrderByStack() {
checkedNotNull(root);
System.out.print("后序遍历(非递归):");
Deque<TreeNode> stack = new LinkedList();
TreeNode temp = root;
TreeNode lastNode = root;
while (temp!=null||!stack.isEmpty()){
while (temp!=null){
stack.push(temp);
temp = temp.getLeft();
}
if (!stack.isEmpty()){
temp = stack.peek();
if (temp.getRight()!=null&&temp.getRight()!=lastNode){
temp = temp.getRight();
}else {
lastNode = stack.pop();
System.out.print(lastNode.getData());
temp = null;
}
}
}
System.out.println("");
}
@Override
public void levelOrder() {
checkedNotNull(root);
System.out.print("层次遍历:");
Queue<TreeNode> queue = new LinkedList<TreeNode>();
TreeNode temp = root;
queue.offer(temp);
while (queue.size()!=0){
int l = queue.size();
for (int i = 0;i<l;i++){
temp = queue.poll();
if (temp.getLeft()!=null)
queue.offer(temp.getLeft());
if (temp.getRight()!=null)
queue.offer(temp.getRight());
System.out.print(temp.getData());
}
}
}
}
测试代码:
/**
* 测试类,以下面这棵树为例
* A
* / \
* B C
* / / \
* D F G
* 前序:A,B,D,C,F,G
* 中序:D,B,A,F,C,G
* 后序:D,B,F,G,C,A
* Created by bzhang on 2019/2/28.
*/
public class Test {
public static void main(String[] args) {
TreeNode node6 = new TreeNode("G",null,null);
TreeNode node5 = new TreeNode("F",null,null);
TreeNode node4 = new TreeNode("D",null,null);
TreeNode node3 = new TreeNode("C",node5,node6);
TreeNode node2 = new TreeNode("B",node4,null);
TreeNode node1 = new TreeNode("A",node2,node3);
BTree bTree = new LinkedBTree(node1);
System.out.println("是否空树?"+bTree.isEmpty());
System.out.println("树中有多少个结点:"+bTree.size());
System.out.println("树的高度:"+bTree.getHeight());
System.out.println(bTree.find("D"));
bTree.inOrder();
bTree.preOrder();
bTree.postOrder();
bTree.inOrderByStack();
bTree.preOrderByStack();
bTree.postOrderByStack();
bTree.levelOrder();
}
}