数据结构之二叉树

Posted Zhi Zhao

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了数据结构之二叉树相关的知识,希望对你有一定的参考价值。

一、二叉树的概念及性质

1.基本概念

二叉树的每个结点最多只有两颗子树,而且它的子树有左右之分,其次序不能任意颠倒。二叉树可以是空集,根可以有空的左子树或右子树,或者左右子树皆为空。

树的常用术语

结点的度:一个结点的子树个数称为此结点的度;

结点的层次:从根结点开始,根结点的层次为1,根的直接后继的层次为2,以此类推;

叶子结点:度为0的结点,即无后继的结点,也称为终端结点;

树的度:树的所有结点中最大的度数;

树的高度(深度):树中所有结点的层次的最大值。

2.性质

(1)在二叉树的第 i 层上最多有2^(i-1)个结点(i>=1)。

(2)深度为 k 的二叉树最多有(2^k)-1个结点。

(3)对任意一颗二叉树,叶子结点数总是等于度为2的结点数加1。

(4)具有 n 个结点的完全二叉树的深度为 [ log2(n) ]+1。

二、二叉树的存储结构

1.顺序存储结构

2.链式存储结构

C语言描述二叉树的链式存储结构如下:

typedef char ElementType;
typedef struct Node
{
	ElementType data;
	struct Node *left;
	struct Node *right;
}*BTree;

三、二叉树的遍历

1.二叉树遍历的递归算法

1)先序遍历:对二叉树中任意一个结点的访问是在其左、右子树遍历之前进行的。遍历过程为:①访问根结点;②先序遍历其左子树;③先序遍历其右子树。

算法描述

// 先序遍历(递归)
void preOrder(BTree root)
{
	if (root != NULL)
	{
		printf("二叉树先序遍历后的输出序列:\\n");
		printf("%c ",root->data);   // 访问根结点

		preOrder(root->left);     // 遍历左子树
		preOrder(root->right);    // 遍历右子树
	}
	printf("\\n");
}

2)中序遍历:对二叉树中任一结点的访问是在遍历其左子树后进行的,访问此结点后,再遍历其右子树。遍历过程为:①中序遍历其左子树;②访问根结点;③中序遍历其右子树。

算法描述

// 中序遍历(递归)
void inOrder(BTree root)
{
	if (root != NULL)
	{
		inOrder(root->left);          // 遍历左子树
		printf("二叉树中序遍历后的输出序列:\\n");
		printf("%c ", root->data);    // 访问根结点
		inOrder(root->right);         // 遍历右子树
	}
	printf("\\n");
}

3)后序遍历:对二叉树中任一结点的访问是在其左、右子树遍历之后进行的。遍历过程为:①后序遍历其左子树;②后序遍历其右子树;③访问根结点。

// 后序遍历(递归)
void postOrder(BTree root)
{
	if (root != NULL)
	{
		postOrder(root->left);              //遍历左子树
		postOrder(root->left);              //遍历右子树
		printf("二叉树后序遍历后的输出序列:\\n");
		printf("%c ", root->data);          // 访问根结点
	}
	printf("\\n");
}

 2.二叉树遍历的非递归算法

1)先序遍历的非递归算法:①访问当前结点并压栈;②然后遍历它的左子树;③当左子树遍历结束后,从栈顶弹出这个结点,接着遍历该结点的右子树。

// 先序遍历(非递归)
void PreOrder(BTree root)
{
	BTree p = root;
    SeqStack *S=createStack(int capacity);
	
	while (p != NULL || !empty(S))
	{
		if (p != NULL)
		{
			printf("%c ", p->data);          // 访问根结点
			push(S, p);     // 进栈
			p = p->left;
		}
		else
		{
			pop(S,&p);   // 出栈
			p = p->right;
		}
	}
}

2)中序遍历的非递归算法:①遇到一个结点,就把它压栈,并去遍历它的左子树;②当左子树遍历结束后,从栈顶弹出这个结点并访问它;③然后按其右指针再去中序遍历该结点的右子树。

// 中序遍历(非递归)
void InOrder(BTree root)
{
	SeqStack *S=createStack(int capacity);
	BTree p = root;

	while (p != NULL || !empty(S))
	{
		while (p != NULL)    // 走左子树,直到左子树为空
		{
			push(S,p);   // 进栈
			p = p->left;
		}
		if (!empty(S))
		{
			pop(S, &p);   // 出栈
			printf("%c ", p->data);          // 访问根结点
			p = p->right;
		}
	}
}

3)后序遍历的非递归算法:①从当前结点开始,进栈并走左子树,直到左子树为空;②取栈顶结点;③如果栈顶结点的右子树为空,或者栈顶结点的右孩子为刚访问过的结点,则退栈并访问,然后将当前结点指针置为空;否则,走右子树。

// 后序遍历(非递归)
void PostOrder(BTree root)
{
	SeqStack *S=createStack(int capacity);
	BTree p,pre;
	p = root;
	pre = NULL;
	while (p != NULL || !empty(S))
	{
		while (p != NULL)          // 走左子树,直到左子树为空
		{
			push(S, p);      // 进栈
			p = p->left;
		}
		p=top(S);   // 取栈顶结点
		if (p->right == NULL || p->right == pre)
		{
			printf("%c ",p->data);     // 访问根结点
			pre = p;      // pre为下一次处理结点的前驱
            pop(S, &p);   // 出栈
			p = NULL;
		}
		else
		{
			p = p->right;   // 走右子树
		}
	}
}

3.二叉树的层序遍历 

层序遍历:从上到下、从左到右访问二叉树的结点,又称为广度优先遍历。遍历从根结点开始,首先将根结点入队,然后开始执行循环:结点出队、访问该结点、其左右孩子入队。

// 层序遍历
void LayerOrder(BTree root)
{
	BTree p;
	LinkQueue *Q=createQueue();  // 创建一个链队列

	Push(Q, p);  // 入队
	while (Q->front != Q->rear)
	{
		Pop(Q, &p);  // 出队
		printf("%c ", p->data);     // 访问根结点
		if (p->left != NULL)
			Push(Q, p->left);
		if (p->right != NULL)
			Push(Q, p->right);
	}
}

四、二叉树遍历算法的应用

1.创建二叉树

由先序序列和中序序列创建二叉树:①根据先序序列的第一个元素确定根结点;②在中序序列中找到该元素,确定根结点的左、右子树的中序序列;③在先序序列中确定左、右子树

的先序序列;④由左子树的先序序列与中序序列建立左子树,由右子树的先序序列与中序序列建立右子树。

int pre[Max], in[Max]; // 先序序列、中序序列
// 先序序列和中序序列创建二叉树
// lb1,ub1分别是先序序列的下界和上界
// lb2,ub2分别是中序序列的下界和上界
BTree CreateBT(int lb1, int ub1, int lb2, int ub2)
{
	BTree root = (BTree)malloc(sizeof(BTree));
	root->data = pre[lb1];
	root->left = NULL;
	root->right = NULL;

	int p = lb2;
	while (in[p] != pre[lb1])  // 在中序序列中查找根结点
		p++;
	int L = p - lb2;  // L为左子树结点的个数
	if (p != lb2)    // 创建左子树
		root->left = CreateBT(lb1 + 1, lb1 + L, lb2, p - 1);
	if (p != ub2)      // 创建右子树
		root->right = CreateBT(lb1 + L + 1, ub1, p + 1, ub2);
	return root;
}

2.二叉树遍历算法的应用

// 输出二叉树中的叶子结点:先序遍历
void leafNode(BTree root)
{
	if (root != NULL)
	{
		if (root->left == NULL&&root->right == NULL)  // 如果结点没有左孩子和右孩子,则为叶子结点
			printf("%c ",root->data);   // 输出叶子结点
		leafNode(root->left);
		leafNode(root->right);
	}
}

// 求二叉树的高度:后序遍历
int depth(BTree root)
{
	int max, hl, hr;
	if (root != NULL)
	{
		hl = depth(root->left);  // 求左子树的高度
		hr = depth(root->right);  // 求右子树的高度
		max = hl > hr ? hl : hr;  // 求左右子树中的最大值
		return max + 1;          // 返回树的高度
	}
	else
		return 0;         // 如果是空树,返回0
}

五、特殊二叉树

1.满二叉树

满二叉树是一颗深度为k且有(2^k)-1个结点的二叉树。

特点:(1)每一层上的结点数都达到了最大值;(2)不存在度为1的结点,每个分支结点均有两颗高度相同的子树,且叶子结点都在最下面一层。

2.完全二叉树

如果二叉树只有最下面两层上的结点的度可以小于2,并且最下面一层上的结点都集中在该层最左边的若干位置上,则此二叉树称为完全二叉树。

特点:(1)在满二叉树的最下面一层上,从最右边开始连续删去若干结点后得到的二叉树仍然是一颗完全二叉树;(2)在完全二叉树中,若某个结点没有左孩子,则它一定没有右孩子,即该结点必是叶子结点;(3)完全二叉树中除最下面一层外,各层都充满了结点。

六、二叉搜索树

一颗二叉树,可以为空;如果不为空,满足以下性质:1)非空左子树的所有键值小于其根结点的键值;2)非空右子树的所有键值大于其根结点的键值;3)左、右子树都是二叉搜索树。

// 递归方式实现二叉搜索的插入
BinTree Insert( BinTree BST, ElementType X )
{
    if(BST==NULL)
    {
        BST=(BinTree)malloc(sizeof(struct TNode));
        BST->Data=X;
        BST->Left=BST->Right=NULL;
    }
    else
    {
        if(X<BST->Data)
            BST->Left=Insert(BST->Left,X);
        else
            BST->Right=Insert(BST->Right,X);
    }
    return BST;
}

// 非递归方式实现二叉搜索树的一般查找
Position Find( BinTree BST, ElementType X )
{
    BinTree P=BST;
    while((P!=NULL)&&(X!=P->Data))
    {
        if(X<P->Data)
            P=P->Left;
        else
            P=P->Right;
    }
    return P;
}

// 查找最小元素
Position FindMin( BinTree BST )
{
    if(BST==NULL)
        return NULL;
    BinTree P=BST;
    while(P->Left!=NULL)
    {
        P=P->Left;
    }
    return P;
}

// 查找最大元素
Position FindMax( BinTree BST )
{
    if(BST==NULL)
        return NULL;
    BinTree P=BST;
    while(P->Right!=NULL)
    {
        P=P->Right;
    }
    return P;
}


// 二叉搜索树的删除
BinTree Delete( BinTree BST, ElementType X )
{
    Position temp;
    if(BST==NULL)
        printf("Not Found\\n");
    else if(X<BST->Data)
        BST->Left=Delete(BST->Left,X);
    else if(X>BST->Data)
        BST->Right=Delete(BST->Right,X);
    else 
    {
        if(BST->Left&&BST->Right)
        {
            temp=FindMin(BST->Right);
            BST->Data=temp->Data;
            BST->Right=Delete(BST->Right,BST->Data);
        }
        else
        {
            temp=BST;
            if(!BST->Left)
                BST=BST->Right;
             else if(!BST->Right)
                 BST=BST->Left;
            free(temp);
        }
    }
    return BST;
}

七、平衡二叉树

平衡二叉树是由 Adelson-Velskii 和 Landis于1962年提出,又称为 AVL树。AVL树可以为空;如果不为空,则要满足任一结点的左右子树高度差的绝对值不超过1。

建立一颗平衡二叉树,并输出根结点。

输入输出规则:输入第一行是二叉树的结点数,第二行是每个结点的键值,输出一行是根结点的键值。

Sample Input 1:

5
88 70 61 96 120

Sample Output 1:

70

Sample Input 2:

7
88 70 61 96 120 90 65

Sample Output 2:

88

#include<stdio.h>
#include<stdlib.h>

typedef int ElementType;
typedef struct AVLNode
{
	ElementType data;
	struct AVLNode *left;
	struct AVLNode *right;
	int Height;
}AVLnode,*AVLTree;

// 求二叉树的高度:后序遍历
int depth(AVLTree root)
{
	int max, hl, hr;
	if (root != NULL)
	{
		hl = depth(root->left);  // 求左子树的高度
		hr = depth(root->right);  // 求右子树的高度
		max = hl > hr ? hl : hr;  // 求左右子树中的最大值
		return max + 1;          // 返回树的高度
	}
	else
		return 0;         // 如果是空树,返回0
}

// 左单旋
/* 注意:A必须有一个左子结点B
 将A与B做左单旋,更新A与B的高度,返回新的根结点B  */
AVLTree SingleLeftRotation(AVLTree A)
{
	AVLTree B = A->left;
	A->left = B->right;
	B->right = A;
	A->Height = depth(A);
	B->Height = depth(B);

	return B;
}

// 右单旋
/*注意:A必须有一个右子结点B
将A与B做右单旋,更新A与B的高度,返回新的根结点B*/
AVLTree SingleRightRotation(AVLTree A)
{
	AVLTree B = A->right;
	A->right = B->left;
	B->left = A;
	A->Height = depth(A);
	B->Height = depth(B);

	return B;
}
// 左-右双旋
/* 注意:A必须有一个左子结点B,且B必须有一个右子结点C
将A、B与C做两次单旋,返回新的根结点C */
AVLTree DoubleLeftRightRotation(AVLTree A)
{
	A->left = SingleRightRotation(A->left);  // 将B与C做右单旋,C被返回
	return SingleLeftRotation(A);      // 将A与C做左单旋,C被返回
}

//  右-左双旋
/* 注意:A必须有一个右子结点B,且B必须有一个左子结点C
将A、B与C做两次单旋,返回新的根结点C */
AVLTree DoubleRightLeftRotation(AVLTree A)
{
	A->right = SingleLeftRotation(A->right);  // 将B与C做左单旋,C被返回
	return SingleRightRotation(A);      // 将A与C做右单旋,C被返回
}

// 将x插入AVL树T中,并且返回调整后的AVL树
AVLTree Insert(AVLTree T,ElementType x)
{
	if (T == NULL)      /* 若插入空树,则新建包含一个结点的树 */
	{
		T = (AVLTree)malloc(sizeof(AVLnode));
		T->data = x;
		T->Height = 0;
		T->left = T->right = NULL;
	}
	else if (x < T->data)
	{
		T->left = Insert(T->left, x);    /* 插入T的左子树 */
		if (depth(T->left) - depth(T->right) == 2)        /* 如果需要左旋 */
			if (x < T->left->data)
				T = SingleLeftRotation(T);   /* 左单旋 */
			else
				T = DoubleLeftRightRotation(T);  /* 左-右双旋 */
	}
	else if (x>T->data)      
	{
		T->right = Insert(T->right, x);   /* 插入T的右子树 */
		if (depth(T->left) - depth(T->right) == -2)   /* 如果需要右旋 */
			if (x>T->right->data)
				T = SingleRightRotation(T);   /* 右单旋 */
			else
				T = DoubleRightLeftRotation(T);  /* 右-左双旋 */
	}
	/* 更新树高 */
	T->Height = depth(T);

	return T;
}

// 访问根结点
void visit_root(AVLTree root)
{
	if (root != NULL)
	{
		printf("%d ", root->data);   // 访问根结点
	}
}

int main()
{
	int i,N;
	int Elem;
	AVLTree T = NULL;

	printf("请输入要插入的元素个数:");
	scanf_s("%d",&N);
	for (i = 0;i < N;i++)
	{
		scanf_s("%d",&Elem);
		T = Insert(T,Elem);
	}
	visit_root(T);
	return 0;
}

八、哈夫曼树及哈夫曼编码

1.哈夫曼树

带权路径长度(WPL):设二叉树有n个叶子结点,每个叶子结点带有权值Wi,从根结点到每个叶子结点的长度为Li,则每个叶子结点的带权路径长度之和为:

WPL最小的二叉树称为最优二叉树或者哈夫曼树。

哈夫曼树的存储结构,用C语言描述哈夫曼树如下:

#define N 200   // 叶子结点数
typedef struct
{
	int weight;  // 结点的权值
	int parent;  // 双亲的下标
	int left;    // 左孩子结点序号
	int right;  // 右孩子结点序号
}*HTree;

哈夫曼树的特点:1)对于有n个叶子结点的哈夫曼树,结点总数为2n-1个;2)哈夫曼树的任意非叶子结点的左右子树交换后仍是哈夫曼树;3)对同一组权值,存在不同构的两棵哈夫曼树。

哈夫曼树的构造:1)初始化,1~n号单元的权值、双亲、左孩子、右孩子的值分别为叶子结点的权值、0、0、0;2)创建树,循环n-1次,通过n-1次的选择、删除与合并来创建哈夫曼树。选择就是从当前森林中选择双亲的下标为0且权值最小的两个根结点s1和s2;删除是指将根结点s1和s2的双亲的下标改为非0;合并就是将根结点s1和s2的权值和作为一个新结点的权值依次存入到数组的第n+1之后的单元中,同时记录这个新结点的左、右孩子的序号s1和s2。

// 构造哈夫曼树,数组w存放n个叶子结点的权值
HTree CreateHuffmanTree(HTree ht, int w[], int n)
{
	/* 1-n号单元存放叶子结点,赋初值 */
	for (int i = 1;i <= n;i++)
		ht[i] = { w[i-1],0,0,0 };
	/* 创建非叶子结点 */
	int m = 2 * n - 1;
	int s1, s2;
	for (int i = n ;i < m;i++)
	{
		/* 在ht[1]-ht[i]的范围内找parent为0,且weight最小的结点s1和s2 */
		findMin(ht,i,&s1);
		ht[s1].parent = i+1;
		findMin(ht, i, &s2);
		ht[s2].parent = i+1;

		ht[i + 1].weight = ht[s1].weight + ht[s2].weight;
		ht[i + 1].parent = 0;
		ht[i + 1].left = s1;
		ht[i + 1].right = s2;
	}
	return ht;
}

2.哈夫曼编码

对一棵具有n个叶子结点的哈夫曼树,若对树中的左分支标0,右分支标1,则从根结点到每个叶子结点的通路上,各分支的值就构成了一个二进制串,该二进制串就称为哈夫曼编码。哈夫曼编码是最优前缀编码方案。

void CreateHuffmanCode(HTree ht, int n, char *hc[])
{
	int c, p;   // 分别表示孩子结点和双亲结点的下标
	char *cd = (char *)malloc(n*sizeof(char));
	cd[n - 1] = '\\0';  // 从右向左逐位存放编码,首先存放编码结束符
	/* 求n个叶子结点对应的哈夫曼编码 */
	for (int i = 1;i <= n;i++)
	{
		int start = n - 1;  // 初始化编码起始指针
		/* 从叶子结点开始向上倒推 */
		c = i;
		p = ht[c].parent;
		while (p != 0)
		{
			start--;
			if (ht[p].left == c)   // 左分支标0
				cd[start] = '0';
			else                   // 右分支标1
				cd[start] = '1';
			c = p;
			p = ht[c].parent;   // 向上倒推
		}
		hc[i] = (char* )malloc((n-start)*sizeof(char));
		strcpy(hc[i], &cd[start]);
	}
	free(cd);
}

上述代码主要是实现算法的功能,提供思路,需要完整的代码可私聊我。

同时,若代码有误,欢迎大家指出,一起交流学习!

 

以上是关于数据结构之二叉树的主要内容,如果未能解决你的问题,请参考以下文章

数据结构之二叉树

Day2 基础数据结构之二叉树(未完成)

《数据结构》复习之二叉树

SDUT 3341 数据结构实验之二叉树二:遍历二叉树

SDUT 3341 数据结构实验之二叉树二:遍历二叉树

SDUT 3343 数据结构实验之二叉树四:还原二叉树