(四)树 911

本文详细介绍了树和二叉树的概念,包括它们的定义、性质、存储结构以及遍历算法。特别讨论了二叉排序树的查找、插入和删除操作,并阐述了哈夫曼树的生成和编码。此外,还探讨了平衡二叉树(AVL树)的调整策略和大顶堆、小顶堆的构建与操作。
摘要由CSDN通过智能技术生成
  1. 熟悉树的概念和树的各种表示、二叉树的定义、性质、存储结构和生成算法。
  2. 熟悉一般树的存储结构、树和森林之间的相互转换及树与森林遍历。
  3. 熟练掌握二叉树的遍历运算。
  4. 理解二叉排序树的定义、查找、插入、删除和生成算法。
  5. 熟练掌握哈夫曼树的定义和生成过程、哈夫曼编码。
  6. 理解平衡二叉树的建树、查找、插入和删除。
  7. 理解大顶堆、小顶堆。

1. 树、二叉树

树的概念

书P104
树(Tree):n(n≥0)个结点构成的有限集合。
n=0,空树。

对于任一棵非空树:
(1)有根(Root)结点;
(2)其余结点可分为m(m>0)个互不相交的有限集,其中每个集合本身又是一棵树,称为原来树的子树。

子树是不相交的。
除根结点外,每个结点有且仅有一个父结点。
一棵N个结点的树有N-1条边。

树的基本术语

书P104
结点的度:子树的个数。
树的度:所有结点中最大的度数。
叶结点:度为0的结点。
父结点:具有子树的结点是其子树的根结点的父结点。
子结点:也称孩子结点。
兄弟结点:具有同一父结点的各结点彼此是兄弟结点。
祖先结点:沿树根到某一结点路径上的所有结点。
子孙结点:子树中的所有结点。
结点的层次:根结点在1层。
树的深度:树中所有结点中的最大层次。
路径和路径长度:路径所包含的边的个数为路径的长度。

树的表示

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

二叉树的定义

书P106
二叉树T:一个有穷的结点集合。这个集合可以为空。
若不为空,则它是由根结点和称为其左子树和右子树的两个不相交的二叉树组成。
二叉树的子树有左右顺序之分。
斜二叉树:只有左子树或只有右子树。结构最差,深度达到N。
完美二叉树(满二叉树):所有分支结点都存在左子树和右子树。,并且所有叶结点都在同一层上。
完全二叉树:叶结点只能出现在最下层和次下层,且最下层的叶结点集中在树的左部。
完美二叉树必定是完全二叉树,有N个结点的完全二叉树深度为O(logN)。

二叉树的性质

书P107
一个二叉树第i层的最大结点数为:2i-1,i≥1。
深度为k的二叉树有最大结点总数为:2k-1,k≥1。
对任何非空二叉树T,若n0表示叶结点的个数,n2表示度为2的非叶结点个数,则n0=n2+1。
具有n个结点的完全二叉树的深度k为:[log2n](向下取整)+1。

二叉树的存储结构

顺序存储结构

书P108
一般用于完全二叉树,其他二叉树采用存储结构会造成空间浪费。
完全二叉树的存储结构:存储空间利用率高,结点的父子关系计算简单,不易实现增加、删除操作。
完全二叉树:按从上至下、从左到右顺序存储n个结点的完全二叉树的结点父子关系。
在N个结点的完全二叉树中,对于下标为i的结点:
[i/2]≥1时,[i/2]是其父结点;[i/2]=0时,表明是根结点,无父结点(向下取整)
2i≤N时,2i是其左孩子;否则无左孩子
2i+1≤N时,2i+1是其右孩子;否则无右孩子

链式存储结构

书P108

typedef struct TNode *Position;
typedef Position BinTree; /* 二叉树类型 */
struct TNode{ /* 树结点定义 */
	ElementType Data; /* 结点数据 */
	BinTree Left;     /* 指向左子树 */
	BinTree Right;    /* 指向右子树 */
};

二叉树的创建

书P119
创建二叉树要先确定结点的输入顺序,常用先序创建和层序创建。
typedef int ElementType; /* 假设结点数据是整数 /
#define NoInfo 0 /
用0表示没有结点 */

BinTree CreatBinTree()
{
	ElementType Data;
	BinTree BT, T;
	Queue Q = CreatQueue(); /* 创建空队列 */
	
	/* 建立第1个结点,即根结点 */
	scanf("%d", &Data);
	if( Data != NoInfo ){
		/* 分配根结点单元,并将结点地址入队 */
		BT = (BinTree)malloc(sizeof(struct TNode));
		BT->Data = Data;
		BT->Left = BT->Right = NULL;
		AddQ( Q, BT );
	}
	else return NULL; /* 若第1个数据就是0,返回空树 */

	while( !IsEmpty(Q) ){
		T = DeleteQ(Q); /* 从队列中取出一结点地址*/
		scanf("%d", &Data); /* 读入T的左孩子 */
		if( Data == NoInfo ) T->Left = NULL;
		else {  /* 分配新结点,作为出队结点左孩子;新结点入队 */
			T->Left = (BinTree)malloc(sizeof(struct TNode));
			T->Left->Data = Data;
			T->Left->Left = T->Left->Right = NULL;
			AddQ( Q, T->Left );
		}
		scanf("%d", &Data); /* 读入T的右孩子 */
		if( Data == NoInfo ) T->Right = NULL;
		else {  /* 分配新结点,作为出队结点右孩子;新结点入队 */
			T->Right = (BinTree)malloc(sizeof(struct TNode));
			T->Right->Data = Data;
			T->Right->Left = T->Right->Right = NULL;
			AddQ( Q, T->Right );
		}
	} /* 结束while */
	return BT;
}

2. 树的存储结构、树和森林之间的相互转换及树与森林遍历

树的存储结构

双亲表示法
孩子表示法
孩子兄弟表示法

树和森林之间的相互转换

树、森林与二叉树的相互转换

树与森林遍历

树和森林的遍历

3. 二叉树的遍历运算

中序遍历

书P111

void InorderTraversal( BinTree BT )
{
    if( BT ) {
        InorderTraversal( BT->Left );
		/* 此处假设对BT结点的访问就是打印数据 */
        printf("%d ", BT->Data); /* 假设数据为整型 */
        InorderTraversal( BT->Right );
    }
}

先序遍历

书P112

void PreorderTraversal( BinTree BT )
{
    if( BT ) {
        printf("%d ", BT->Data );
        PreorderTraversal( BT->Left );
        PreorderTraversal( BT->Right );
    }
}

后序遍历

书P113

void PostorderTraversal( BinTree BT )
{
    if( BT ) {
        PostorderTraversal( BT->Left );
        PostorderTraversal( BT->Right );
        printf("%d ", BT->Data);
    }
}

非递归遍历(中序)

书P113
非递归算法实现的基本思路:使用堆栈

void InorderTraversal( BinTree BT )
{
	BinTree T;
	Stack S = CreateStack(); /* 创建空堆栈S,元素类型为BinTree */

	T = BT; /* 从根结点出发 */
	while( T || !IsEmpty(S) ){
		while( T ){ /* 一直向左并将沿途结点压入堆栈 */
			Push(S, T); 
			T = T->Left; 
		}
		T = Pop(S); /* 结点弹出堆栈 */
        printf("%d ", T->Data); /*(访问)打印结点 */
		T = T->Right; /* 转向右子树 */
	}
}

层序遍历

书P115
二叉树遍历的核心问题:二维结构的线性化
需要一个存储结构保存暂时不访问的结点
存储结构:堆栈、队列

void LevelorderTraversal ( BinTree BT )
{ 
	Queue Q; 
	BinTree T;

	if ( !BT ) return; /* 若是空树则直接返回 */
	
	Q = CreatQueue(); /* 创建空队列Q */
	AddQ( Q, BT );
	while ( !IsEmpty(Q) ) {
		T = DeleteQ( Q );
		printf("%d ", T->Data); /* 访问取出队列的结点 */
		if ( T->Left )   AddQ( Q, T->Left );
		if ( T->Right )  AddQ( Q, T->Right );
	}
}

4. 二叉排序树的定义、查找、插入、删除和生成算法

定义

书P125
二叉搜索树(BTS):也称二叉排序树或二叉查找树。
可以为空,若不为空则:
(1)非空左子树的所有键值小于其根结点的键值。
(2)非空右子树的所有键值大于其根结点的键值。
(3)左、右子树都是二叉搜索树。

查找

查找的过程

书P126
树为空,返回NULL。
树非空,根结点关键字和X进行比较:
(1)若X小于根结点键值,只需在左子树中继续搜索;
(2)若X大于根结点键值,只需在右子树中继续搜索;
(3)若X等于根结点键值,搜索完成,返回指向此结点的指针。

递归查找函数

书P127

Position Find( BinTree BST, ElementType X )
{
	if( !BST ) return NULL; /* 查找失败 */
	
	if( X > BST->Data ) 
		return Find( BST->Right, X ); /* 在右子树中递归查找 */
	else if( X < BST->Data ) 
		return Find( BST->Left, X );  /* 在左子树中递归查找 */
	else /* X == BST->Data */
		return BST;  /* 在当前结点查找成功,返回当前结点的地址 */
}
迭代查找函数

书P127
查找的效率取决于树的高度。
非递归函数的执行效率高,故一般使用迭代函数。

Position Find( BinTree BST, ElementType X )
{
    while( BST ) {
        if( X > BST->Data )
			BST = BST->Right;    /* 向右子树中移动,继续查找 */
        else if( X < BST->Data )
			BST = BST->Left;     /* 向左子树中移动,继续查找 */
        else /* X == BST->Data */
            break; /* 在当前结点查找成功,跳出循环 */
    }
    return BST; /* 返回找到的结点地址,或是NULL */
}
查找最大和最小元素

书P127
最大元素一定是在树的最右分枝的端结点上
最小元素一定是在树的最左分枝的端结点上

Position FindMax( BinTree BST )
{
    if( BST ) 
        while( BST->Right )
            BST = BST->Right; /* 沿右分支一直向下,直到最右端点 */
    return BST;
}

Position FindMin( BinTree BST )
{ /* 最小元素在最左端点 */
    if( !BST ) return NULL; /* 空的二叉搜索树,返回NULL */
    else if( !BST->Left ) return BST; /* 找到最左端点并返回 */
    else return FindMin( BST->Left ); /* 沿左分支递归查找 */
}

插入

书P128

BinTree Insert( BinTree BST, ElementType X )
{
    if( !BST ){ /* 若原树为空,生成并返回一个结点的二叉搜索树 */
        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  if( X > BST->Data )
            BST->Right = Insert( BST->Right, X ); /*递归插入右子树*/
        /* else X已经存在,什么都不做 */
	}
    return BST;
}

删除

书P130
删除需要考虑三种情况:
删除叶结点:直接删除,并修改父结点指针为NULL。
删除只有一个孩子的结点:将父指针指向要删除结点的孩子结点。
删除有左、右两棵子树的结点:用另一结点(右子树的最小元素或左子树的最大元素)代替被删除结点。

BinTree Delete( BinTree BST, ElementType X ) 
{ 
    Position Tmp; 

    if( !BST ) 
        printf("要删除的元素未找到"); 
    else {
        if( X < BST->Data ) 
            BST->Left = Delete( BST->Left, X );   /* 从左子树递归删除 */
        else if( X > BST->Data ) 
			BST->Right = Delete( BST->Right, X ); /* 从右子树递归删除 */
		else { /* BST就是要删除的结点 */
			/* 如果被删除结点有左右两个子结点 */ 
			if( BST->Left && BST->Right ) {
				/* 从右子树中找最小的元素填充删除结点 */
				Tmp = FindMin( BST->Right );
				BST->Data = Tmp->Data;
				/* 从右子树中删除最小元素 */
				BST->Right = Delete( BST->Right, BST->Data );
			}
			else { /* 被删除结点有一个或无子结点 */
				Tmp = BST; 
                if( !BST->Left )       /* 只有右孩子或无子结点 */
					BST = BST->Right; 
                else                   /* 只有左孩子 */
					BST = BST->Left;
				free( Tmp );
			}
		}
	}
    return BST;
}

生成

5. 哈夫曼树

定义

书P152
带权路径长度(WPL):从根结点到该结点之间的路径长度与该结点上所带权值的乘积。
最优二叉树或哈夫曼树:WPL最小的二叉树。

生成过程

书P153
每次把权值最小的两棵二叉树合并。
调整最小堆:O(N)。
2(N-1)+1个删除:O(NlogN)。
N-1个插入:O(NlogN)。
整体时间复杂度:O(NlogN)。

typedef struct HTNode *HuffmanTree; /* 哈夫曼树类型 */
struct HTNode{ /* 哈夫曼树结点定义 */
	int Weight;         /* 结点权值 */
	HuffmanTree Left;   /* 指向左子树 */
	HuffmanTree Right;  /* 指向右子树 */
};

HuffmanTree Huffman( MinHeap H )
{ /* 这里最小堆的元素类型为HuffmanTree */
  /* 假设H->Size个权值已经存在H->Data[]->Weight里 */
	int i, N;
	HuffmanTree T;

    BuildHeap(H); /* 将H->Data[]按权值Weight调整为最小堆 */
	N = H->Size;
    for ( i=1; i<N; i++ ) { /* 做H->Size-1次合并 */
        T = (HuffmanTree)malloc(sizeof(struct HTNode)); /* 建立一个新的根结点 */
        T->Left = DeleteMin(H);  /* 从最小堆中删除一个结点,作为新T的左子结点 */
        T->Right = DeleteMin(H); /* 从最小堆中删除一个结点,作为新T的右子结点 */
        T->Weight = T->Left->Weight+T->Right->Weight; /* 计算新权值 */
        Insert( H, T ); /* 将新T插入最小堆 */
	}	
	return DeleteMin(H); /* 最小堆中最后一个元素即是指向哈夫曼树根结点的指针 */
}

哈夫曼编码

书P156
哈夫曼编码也成为前缀编码,任何字符的编码都不是另一字符编码的前缀,可以无二义地解码。
左分支记0,右分支记1。
字符只在叶结点上,没有度为1的结点。
哈夫曼编码不是唯一的。

6. 平衡二叉树

书P134
平衡二叉树(AVL树):空树,或任一结点的左、右子树高度差的绝对值不超过1。
给定结点数为n的AVL树的最大高度为O(log2n)。

建树

单旋调整(RR旋转)

书P135
麻烦结点在发现者右子树的右边,因而叫RR插入,需要RR旋转(右单旋)。

单旋调整(LL旋转)

书P136
麻烦结点在发现者左子树的左边,因而叫LL插入,需要LL旋转(左单旋)。

AVLTree SingleLeftRotation ( AVLTree A )
{ /* 注意:A必须有一个左子结点B */
  /* 将A与B做如图4.35所示的左单旋,更新A与B的高度,返回新的根结点B */     

	AVLTree B = A->Left;
	A->Left = B->Right;
	B->Right = A;
	A->Height = Max( GetHeight(A->Left), GetHeight(A->Right) ) + 1;
    B->Height = Max( GetHeight(B->Left), A->Height ) + 1;
 
	return B;
}
双旋调整(LR旋转)

书P137
麻烦结点在发现者左子树的右边,因而叫LR插入,需要LR旋转(左-右双旋)。

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

书P137
麻烦结点在发现者右子树的左边,因而叫RL插入,需要RL旋转(右-左双旋)。

查找

插入

书P139

typedef struct AVLNode *Position;
typedef Position AVLTree; /* AVL树类型 */
typedef struct AVLNode{
	ElementType Data; /* 结点数据 */
	AVLTree Left;     /* 指向左子树 */
	AVLTree Right;    /* 指向右子树 */
	int Height;       /* 树高 */
};

int Max ( int a, int b )
{
	return a > b ? a : b;
}

AVLTree Insert( AVLTree T, ElementType X )
{ /* 将X插入AVL树T中,并且返回调整后的AVL树 */
	if ( !T ) { /* 若插入空树,则新建包含一个结点的树 */
		T = (AVLTree)malloc(sizeof(struct AVLNode));
		T->Data = X;
		T->Height = 0;
		T->Left = T->Right = NULL;
	} /* if (插入空树) 结束 */

	else if ( X < T->Data ) {
		/* 插入T的左子树 */
		T->Left = Insert( T->Left, X);
		/* 如果需要左旋 */
        if ( GetHeight(T->Left)-GetHeight(T->Right) == 2 )
            if ( X < T->Left->Data ) 
               T = SingleLeftRotation(T);      /* 左单旋 */
            else 
               T = DoubleLeftRightRotation(T); /* 左-右双旋 */
	} /* else if (插入左子树) 结束 */
    
	else if ( X > T->Data ) {
		/* 插入T的右子树 */
		T->Right = Insert( T->Right, X );
		/* 如果需要右旋 */
        if ( GetHeight(T->Left)-GetHeight(T->Right) == -2 )
            if ( X > T->Right->Data ) 
               T = SingleRightRotation(T);     /* 右单旋 */
            else 
               T = DoubleRightLeftRotation(T); /* 右-左双旋 */
	} /* else if (插入右子树) 结束 */

    /* else X == T->Data,无须插入 */

	/* 别忘了更新树高 */
	T->Height = Max( GetHeight(T->Left), GetHeight(T->Right) ) + 1;
	
	return T;
}

删除

7. 大顶堆、小顶堆

书P142
优先队列:特殊的队列,取出元素的顺序是按照元素的优先权(关键字)大小,而不是元素进入队列的先后顺序。
堆,通常被称作优先队列。
在这里插入图片描述
堆常用二叉树表示,一般是完全二叉树。

堆的结构特性

结构性:用数组表示的完全二叉树。
有序性:任一结点的关键字是其子树所有结点的最大值(或最小值)。

大顶堆(最大堆)

创建

书P144

#define MAXDATA 1000  /* 该值应根据具体情况定义为大于堆中所有可能元素的值 */

MaxHeap CreateHeap( int MaxSize )
{ /* 创建容量为MaxSize的空的最大堆 */

	MaxHeap H = (MaxHeap)malloc(sizeof(struct HNode));
	H->Data = (ElementType *)malloc((MaxSize+1)*sizeof(ElementType));
	H->Size = 0;
	H->Capacity = MaxSize;
	H->Data[0] = MAXDATA; /* 定义"哨兵"为大于堆中所有可能元素的值*/

	return H;
}
插入

书P145
从新增的最后一个结点的父结点开始,用要插入的元素向下过滤上层结点。
T(N)=O(logN)

bool IsFull( MaxHeap H )
{
	return (H->Size == H->Capacity);
}

bool Insert( MaxHeap H, ElementType X )
{ /* 将元素X插入最大堆H,其中H->Data[0]已经定义为哨兵 */
    int i;
 
	if ( IsFull(H) ) { 
        printf("最大堆已满");
        return false;
    }
	i = ++H->Size; /* i指向插入后堆中的最后一个元素的位置 */
	for ( ; H->Data[i/2] < X; i/=2 )
        H->Data[i] = H->Data[i/2]; /* 上滤X */
	H->Data[i] = X; /* 将X插入 */
	return true;
}
删除

书P146
从根结点开始,用最大堆中的最后一个元素向上过滤下层结点。
T(N)=O(logN)

#define ERROR -1 /* 错误标识应根据具体情况定义为堆中不可能出现的元素值 */

bool IsEmpty( MaxHeap H )
{
	return (H->Size == 0);
}

ElementType DeleteMax( MaxHeap H )
{ /* 从最大堆H中取出键值为最大的元素,并删除一个结点 */
    int Parent, Child;
    ElementType MaxItem, X;

    if ( IsEmpty(H) ) {
        printf("最大堆已为空");
        return ERROR;
    }

    MaxItem = H->Data[1]; /* 取出根结点存放的最大值 */
    /* 用最大堆中最后一个元素从根结点开始向上过滤下层结点 */
    X = H->Data[H->Size--]; /* 注意当前堆的规模要减小 */
	for( Parent=1; Parent*2<=H->Size; Parent=Child ) {
		Child = Parent * 2;
        if( (Child!=H->Size) && (H->Data[Child]<H->Data[Child+1]) )
            Child++;  /* Child指向左右子结点的较大者 */
        if( X >= H->Data[Child] ) break; /* 找到了合适位置 */
		else  /* 下滤X */
			H->Data[Parent] = H->Data[Child];
    }
    H->Data[Parent] = X;

    return MaxItem;
} 
建立

书P148
将已经存在的N个元素按最大堆的要求存放在一个一维数组中。
方法1:将N个元素一个个相继插入到一个初始为空的堆中去,O(NlogN)。
方法2:按顺序插入,完全二叉树的结构特性,调整为满足最大堆的有序特性。线性时间复杂度,T(n)=O(n)。

void PercDown( MaxHeap H, int p )
{ /* 下滤:将H中以H->Data[p]为根的子堆调整为最大堆 */
    int Parent, Child;
    ElementType X;

    X = H->Data[p]; /* 取出根结点存放的值 */
	for( Parent=p; Parent*2<=H->Size; Parent=Child ) {
		Child = Parent * 2;
        if( (Child!=H->Size) && (H->Data[Child]<H->Data[Child+1]) )
            Child++;  /* Child指向左右子结点的较大者 */
        if( X >= H->Data[Child] ) break; /* 找到了合适位置 */
		else  /* 下滤X */
			H->Data[Parent] = H->Data[Child];
    }
    H->Data[Parent] = X;
}

void BuildHeap( MaxHeap H )
{ /* 调整H->Data[]中的元素,使满足最大堆的有序性  */
  /* 这里假设所有H->Size个元素已经存在H->Data[]中 */

	int i;

	/* 从最后一个结点的父节点开始,到根结点1 */
	for( i = H->Size/2; i>0; i-- )
		PercDown( H, i );
}

小顶堆(最小堆)

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值