数据结构(7)二叉树的介绍以及代码实现

树形结构:结点间具有层次关系,每一层的一个结点能且只能和上一层的一个结点相关,但同时可以和下一层的多个结点相关,即“一对多”关系。常见树形结构有树、堆。

关于树的定义

树是递归定义的:由N(n>=0)个结点构成的集合。当n=0时,称为空树;当n>1时,树有:

  • 有一个特殊的结点,称为根结点,根节点没有前驱结点
  • 除根结点外,其余结点被分成M(M>0)个互不相交的集合T1、T2、……、Tm,其中每一个集合Ti(1<= i <= m)又是一棵结构与树类似的子树。每棵子树的根结点有且只有一个前驱,可以有0个或多个后继

树的相关概念(主要理解)

结点:结点包括一个数据元素及若干指向其他子树的分支(指针(索引))
结点的度:结点所拥有子树的个数称为该结点的度
树的度:树中所有结点的度的最大值称为该树的度
叶子结点(终端节点):度为0的结点称为叶子结点
分支结点(非终端节点):度不为0的结点称为分支结点,一棵树中除叶节点外的所有节点都是分支结点
祖先结点:从根节点到该结点所经分支上的所有节点
子孙结点:以某节点为根节点的子树中所有节点
双亲结点(前驱结点):一个结点称之为其后继结点的双亲节点。
孩子结点(后继结点):树中一个节点的子树的根节点称为该结点的孩子结点
兄弟结点:具有同一双亲的结点之间互称为为兄弟结点
结点层次:从根节点到树中某节点所经路径上的分支数称为该结点的层次,根结点为第一层,其孩子节点为第二层,以此类推。
树的深度(高度):树中所有节点层次的最大值称为该树的深度
有序树和无序树:树中各结点从左到右是有次序的,即若交换了某结点各子树的相对位置则构成了不同的树,那么称之为有序树,反之则为无序树。
森林:m棵树的集合(m大于等于0)。在自然界中树和森林是两个不同的概念,但在数据结构中,它们之间的差别很小。删去一棵非空树的根节点,树就变成森林;反之若增加一个节点,让森林中每一棵树的根节点都变成他的子女,森林就变成一棵树

图解概念
在这里插入图片描述

树结构的实现方式
在计算机中存储树时,要求既要存储结点的数据元素信息,又要存储结点之间的逻辑关系信息。所以一共有四种表示方法。

1. 双亲表示法

对于上图中的树,双亲表示法有两种实现方式:

  1. 一维数组实现
    用一维数组存储树中的各个结点,其中数组元素是个记录,包含data和parent两个字段,分别表示结点的数据值和其父节点在数组中的下标。
    在这里插入图片描述
  2. 用指针表示出每个结点的双亲节点

双亲表示法的优缺点

  • 优点:寻找一个节点的双亲结点操作实现很方便
  • 缺点:寻找一个节点的孩子结点很不方便

2. 孩子表示法

孩子表示法的实现方式:用指针表示出每个结点的子结点,但是有个问题:怎么知道每个结点会有几个孩子结点呢?答案是看树的度,如下图中,树的度为3,则应该设计3个指针指向孩子结点。

数据结构:

typedef int DataType;
struct Node
{
	struct Node* _pChild1;  
	struct Node* _pChild2;  
	struct Node* _pChild3;  
	DataType _data;  //数据域
};

孩子表示法的优缺点

  • 优点:寻找一个节点的孩子结点比较方便
  • 缺点:寻找一个节点的双亲结点很不方便

3. 双亲孩子表示法

双亲孩子表示法:用指针既表示出每个结点的父节点,也表示出每个结点的子结点,即:双亲表示法+孩子表示法

typedef int DataType;
struct Node
{
	struct Node* _pParent;  //指向双亲节点
	struct Node* _pChild1;  
	struct Node* _pChild2;  
	struct Node* _pChild3;  
	DataType _data;  //数据域
};

4. 孩子兄弟表示法

孩子兄弟表示法:即表示出每个结点的第一个孩子结点,也表示出每个结点的下一个兄弟结点。孩子兄弟链表存储结构是一种二叉链表,链表中的每一个结点包含两个指针,分别指向对应结点的第一个子结点和下一个兄弟结点。

typedef int DataType;
struct Node
{
	struct Node* _pChild1;  
	struct Node* _pNextBrother;  
	DataType _data;  //数据域
};

在这里插入图片描述

二叉树

二叉树是一种特殊又重要的树。二叉树的特点:

  • 每个结点最多有两棵子树,即二叉树不存在度大于2的结点
  • 二叉树的子树有左右之分,其子树的次序不能颠倒
  • 二叉树即使只有一颗子树也要明确指出该子树是左子树还是右子树。

满二叉树&完全二叉树

  • 满二叉树: 在一棵二叉树中,如果所有分支结点都存在左子树和右子树,并且所有叶子节点都在同一层上

  • 完全二叉树:从根起,自上而下,自左而右,给满二叉树的每个结点从1到n连续编号,如果每个结点都与深度为k的满二叉树中编号从1至n的结点一一对应,则称为完全二叉树,满二叉树是完全二叉树。

二叉树的性质

  1. 二叉树上叶子结点数等于度为2的节点数加1.

证明:
设在一棵二叉树中,n为所有节点总数,n0为叶子结点个数,n1为度为1的节点个数,n2为度为2的结点个数。

则 n = n0 + n1 + n2 (1)
再看二叉树中的分支数:除根节点外,所有结点都有一个分支进入,设b为分支数,
则 n = b + 1
又因为分支是由度为1或2的结点分出的,因此:
b = n1 + 2 x n2
于是: n = n1 + 2 x n2 + 1 (2)
由(1)(2)得: n0 = n2 +1

  1. 二叉树上第i层上至少有2^(i-1)个结点。(i>=1,根结点的层数为1)

证明:数学归纳法

  1. 若规定只有根节点的二叉树的深度为1,则深度为K的二叉树的最大结点数是 2^k - 1 (k>=0)

  2. 对于完全二叉树(n≥1,n为节点数),如果按照从上至下从左至右的顺序对所有节点从1开始编号,则对于序号为i的结点有:

    1. 若编号为i的结点有左孩子,则左孩子结点编号为2i;若编号为i的结点有右孩子,则右孩子结点编号为2i+1
    2. 除树的根节点外,若一个结点的编号为i,则双亲编号为:i/2。
    3. 如果 2 * i ≤ n,则编号为i的结点为分支结点;若2 * i > n,i为叶子结点。
    4. 若n为奇数,则每个分支结点都既有左孩子又有右孩子;若n为偶数,则编号最大的分支结点只有左孩子,没有右孩子。

二叉树的存储结构

与线性表一样,二叉树也有顺序存储结构和链式存储结构。

  1. 顺序存储结构
typedef DataType SqBinTree[MaxSize];   //其中DataType为二叉树结点的数据类型;MAxSize为顺序表的最大长度。

用数组来存储一颗二叉树其实是不合适的,但是如果这个人二叉树是一个完全二叉树,则数组存储是很常见的,存储过程:首先,把根结点定为1,然后按照层次上从上到下、每层从左到右的顺序对每一结点进行编号。对于一般二叉树,也可以用数组存储,但是首先,得在二叉树上补上若干虚拟结点使其成为完全二叉树后,再按照上面的方法进行编号。下表中是个二叉树顺序存储结构的示例:

data在这里插入图片描述

顺序存储结构的优缺点

  • 优点:存储完全二叉树,简单省空间 ;
  • 缺点:存储一般二叉树尤其单支树,存储空间利用不高 。
  1. 二叉链存储结构

链表中每个结点包含两个指针,分别对应this结点的左孩子和右孩子:

struct BinTreeNode
{
	struct BinTreeNode* _pLeft;
	struct BinTreeNode* _pRight;
	DataType _data;   
};

这种存储结构的优点是访问结点的孩子很方便

  1. 三叉链存储结构
    链表中每个结点包含三个指针,分别对应this结点的左孩子、右孩子和父节点。
struct BinTreeNode
{
	struct BinTreeNode* _pParent;
	struct BinTreeNode* _pLeft;
	struct BinTreeNode* _pRight;
	DataType _data;   
};

1. 二叉树的实现(c++)

以下采用二叉链存储结构。

  1. 建立二叉链
    用ch扫描采用括号表示法表示二叉树的字符串str。分为以下几种情况:
    1)若 ch == ‘(’ ,则将前面刚创建的结点作为双亲节点进栈,并置k=1,表示其后创建的节点将作为这个节点的左孩子节点。
    2)若 ch == ‘(’,表示栈中结点的左右孩子结点处理完毕,退栈。
    3)若 ch == ‘,’,k=2,表示其后创建的节点为右孩子结点
    4)若 ch 为一个字母,创建一个节点,并根据k值建立它与栈中结点之间的联系,当k=1表示这个节点作为栈中栈顶结点的左孩子结点;当k=2表示这个节点作为栈中栈顶结点的右孩子结点;如此循环直至str处理完毕。算法中使用一个栈st保存双亲节点,top为其栈顶指针;k指定其后处理的结点是双亲节点的左孩子结点还是右孩子。
#define MaxSize 20
typedef char DataType;

typedef struct tnode
{
	DataType data;
	struct tnode* pchild1;
	struct tnode* pchild2;
}BTNode;

//建立二叉链
void CreateBTree(BTNode *&bt, char *str)
{
	BTNode *st[MaxSize], *p = NULL;
	int top = -1, k, j = 0;
	char ch;
	bt = NULL;//建立二叉树初始化为空
	ch = str[j];
	while ('\0' != ch)   //str未扫描完时循环
	{
		switch (ch)
		{
		case '(':top++; st[top] = p; k = 1; break;//左孩子节点
		case ')':top--; break;
		case ',':k = 2; break;  //右孩子结点
		default:
			p = (BTNode*)malloc(sizeof(BTNode));
			p->data = ch;
			p->pchild1 = p->pchild2 = NULL;
			if (bt == NULL)
				bt = p;
			else
			{
				switch (k)
				{
				case 1:st[top]->pchild1 = p; break;
				case 2:st[top]->pchild2 = p; break;
				
				}
			}
		}
		j++;
		ch = str[j];
	}
}
  1. 求二叉树高度(递归求法)
int BTHeight(BTNode *bt)
{
	int left, right;
	if (NULL == bt)
	{
		return 0;
	}
	else
	{
		left = BTHeight(bt->pchild1);
		right = BTHeight(bt->pchild2);
		return (left > right) ? (left + 1) : (right + 1);
	}
}
  1. 求二叉树结点个数(递归算法)
int NodeCount(BTNode *bt)
{
	int left, right;
	if (NULL == bt)
	{
		return 0;
	}
	else
	{
		left = NodeCount(bt->pchild1);
		right = NodeCount(bt->pchild2);
		return left + right + 1;
	}
}
  1. 求二叉树叶子结点个数
int LeafCount(BTNode *bt)
{
	int left, right;
	if (NULL == bt)
		return 0;
	else if (NULL == bt->pchild1 && NULL == bt->pchild2)
		return 1;
	else
	{
		left = LeafCount(bt->pchild1);
		right = LeafCount(bt->pchild2);
		return left + right;
	}
}
  1. 以括号表示法输出二叉树
void DisBTree(BTNode *bt)
{
	if (NULL != bt)
	{
		cout << bt->data;
		if (NULL != bt->pchild1 || NULL != bt->pchild2)
		{
			cout << '(';
			DisBTree(bt->pchild1);
			if (NULL != bt->pchild2) cout << ",";
			DisBTree(bt->pchild2);
			cout << ")";
		}
	}
	
}
  1. 二叉树的遍历

所谓二叉树的遍历,是指按照一定次序访问树中的所有节点,使得每个结点恰好被访问一 次。二叉树常用的遍历有:先序遍历(先根遍历)、中序遍历、后序遍历和层次遍历。不同遍历方式区别在于访问根节点的顺序。假设我有一个二叉树

1. 先序遍历

先序遍历规则:访问根结点;------> 先序遍历左子树;------> 先序遍历右子树。

void PreOrder(BTNode *bt)
{
	if (NULL != bt)
	{
		cout << bt->data;
		PreOrder(bt->childleft);
		PreOrder(bt->childright);
	}
}

2. 中序遍历

中序遍历规则:1)中序遍历左子树;2)访问根节点;3)中序遍历右子树。

void InOrder(BTNode *bt)
{
	if (NULL != bt)
	{
		InOrder(bt->childleft);
		cout << bt->data;
		InOrder(bt->childright);
	}
}

3. 后序遍历

后序遍历规则:1)后序遍历左子树;2)后序遍历右子树;3)访问根节点。

void PostOrder(BTNode *bt)
{
	if (NULL != bt)
	{
		PostOrder(bt->childleft);
		PostOrder(bt->childright);
		cout << bt->data;
	}
}

4. 层次遍历

层次遍历规则:在进行层次访问的时候,对某一层的结点访问完,再按照他们的访问次序对各个结点的左孩子、右孩子顺序访问。

层次访问需要一个环形队列qu来实现:先将根结点进队,在队不空时循环;从队列中出列一个节点*p,访问它;若它有左孩子节点,将左孩子节点进队;若它有右孩子结点,将右孩子结点进队。如此操作直到队空为止。

void LevelOrder(BTNode *bt)
{
	BTNode *p;
	BTNode *qu[MaxSize];//定义环形队列
	int front, rear;//对头与队尾指针
	front = rear = -1;//队列为空
	rear++;
	qu[rear] = bt;   //根节点进入队列
	while (front != rear){         //队列不为空	
		front = (front + 1) % MaxSize;
		p = qu[front];//队头出队列
		printf("%c ", p->data);//访问结点
		if (NULL != p->leftchild)//有左孩子时将其入队
		{
			rear = (rear + 1) % MaxSize;
			qu[rear] = p->leftchild;
		}
		if (NULL != p->rightchild)//有右孩子时将其入队
		{
			rear = (rear + 1) % MaxSize;
			qu[rear] = p->rightchild;
		}
	}
}

值得注意的是

  • 对于不同的二叉树,先序遍历序列可能相同
  • 对于不同的二叉树,中序遍历序列可能相同
  • 对于不同的二叉树,先序遍历序列可能相同
  • 对于不同的二叉树,先序遍历序列和后序遍历可能都相同
  • 由先序遍历和中序遍历序列能够唯一确定一棵二叉树
  • 由后序遍历和中序遍历序列能够唯一确定一棵二叉树

2. 二叉树的实现(java)

/**
 * @author Emma
 * @create 2020 - 03 - 21 - 9:01
 * 结点代码
 */
public class TreeNode {
    public int value;//结点的权
    public TreeNode left;
    public TreeNode right;
    
    //构造方法
    public TreeNode(int value){
        this.value = value;
    }
    
    //设置左节点
    public void setLeft(TreeNode left) {
        this.left = left;
    }

    //设置右节点
    public void setRight(TreeNode right) {
        this.right = right;
    }

    //前序遍历:根-左-右  (遍历需要递归)
    public void frontShow() {

        System.out.println(value);
        if(left != null)
            left.frontShow();

        if(right != null)
            right.frontShow();
    }

    //中序遍历  左-根-右
    public void midShow() {
        if(left != null)
            left.midShow();

        System.out.println(value);


        if(right != null)
            right.midShow();

    }

    //后序遍历  左-右-根
    public void afterShow() {

        if(left != null)
            left.afterShow();

        if(right != null)
            right.afterShow();

        System.out.println(value);
    }

    //前序查找
    public TreeNode frontSearch(int i) {
        TreeNode temp = null;

        if(this.value == i) {
           // 判断根结点是否找到了,找到了就返回
            return this;
        }else{
            //
            if(left != null){
                temp = left.frontSearch(i);
                //判断左边是否找到了,找到了就返回
                if(temp != null){
                    return temp;
                }
            }
            if(right != null){
                temp = right.frontSearch(i);
            }
        }
        return temp;
    }
    

    //删除一个节点
    public void delete(int i) {
        TreeNode parent = this;

        //判断左节点是否符合
        if(parent.left != null && parent.left.value == i){
            parent.left = null;
            return;
        }
        //判断右节点是否符合
        if(parent.right != null && parent.right.value == i){
            parent.right = null;
            return;
        }

        //如果左右结点都不是,将左节点赋值为parent,然后找左节点的左节点,以及左节点的右节点
        parent = left;
        if(parent != null){//左节点不为空才进入,为空就去考虑右节点
            parent.delete(i);
        }
        parent = right;
        if(parent != null){//右节点不为空才进入,为空就去返回null
            parent.delete(i);
        }


    }
}


/**
 * @author Emma
 * @create 2020 - 03 - 21 - 8:57
 * 二叉树代码
 */
public class BinaryTree {
    TreeNode root;//根结点

    public void setRoot(TreeNode root) {
        this.root = root;
    }
   

    public TreeNode getRoot() {
        return root;
    }

    //前序遍历
    public void frontShow() {
        if(root != null)
            root.frontShow();
    }

    //中序遍历
    public void midShow() {
        if(root != null)
            root.midShow();
    }

    //后序遍历
    public void afterShow() {
        if(root != null)
            root.afterShow();
    }

    //前序查找
    public TreeNode frontSearch(int i) {
        return root.frontSearch(i);
    }

    //删除一个结点
    public void delete(int i) {
        if(root.value == i){
            root = null;
        }else{
            root.delete(i);
        }

    }
}


/**
 * @author Emma
 * @create 2020 - 03 - 21 - 11:19
 * 测试代码
 */
public class BinaryTreeTest {

    public static void main(String[] args) {

        BinaryTree binaryTree = new BinaryTree();
        TreeNode root = new TreeNode(1);//根结点
        binaryTree.setRoot(root);

        TreeNode rootleft = new TreeNode(2);//根结点的左节点
        root.setLeft(rootleft);

        TreeNode rootright = new TreeNode(3);//根结点的右节点
        root.setRight(rootright);

        TreeNode rootleftleft = new TreeNode(4);//根结点的左节点的左节点
        rootleft.setLeft(rootleftleft);

        TreeNode rootleftright = new TreeNode(5);//根结点的左节点的右节点
        rootleft.setRight(rootleftright);

        TreeNode rootrightleft = new TreeNode(6);//根结点的右节点的左节点
        rootright.setLeft(rootrightleft);

        TreeNode rootrightright = new TreeNode(7);//根结点的右节点的右节点
        rootright.setRight(rootrightright);


        
        //前序遍历
        binaryTree.frontShow();
        System.out.println("*************");
        

        //中序
        binaryTree.midShow();
        System.out.println("*************");


        
        //后序
        binaryTree.afterShow();
        System.out.println("*************");

        //查找,可以有三种方式(前序查找,中序查找,后序查找
        TreeNode node = binaryTree.frontSearch(5);
        System.out.println(node);
        System.out.println("*************");
        //删除节点,如果有子节点一起删掉
        binaryTree.delete(8);
        binaryTree.frontShow();
    }
}
  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值