树与二叉树(从数据结构的三要素出发)

文章目录

一、二叉树

逻辑结构

1. 基本定义

二叉树是一种常见的树形数据结构,其中每个节点最多有两个子节点,分别称为左子节点和右子节点。二叉树的逻辑结构可以通过以下几个方面来描述:

  • 节点(Node):二叉树中的每个元素称为节点。每个节点包含三个部分:一个数据元素、一个指向左子节点的链接和一个指向右子节点的链接。
  • 根节点(Root Node):二叉树的顶点节点,唯一没有父节点的节点。
  • 叶子节点(Leaf Node):没有子节点的节点。
  • 内部节点(Internal Node):至少有一个子节点的节点,且度 ≤ 2 \leq2 2
  • 子树(Subtree):由一个节点及其所有后代节点构成的树。每个节点都有左子树和右子树。
  • 空树(Empty Tree):不包含任何节点的树。

m m m叉树 v.s. 度为 m m m的树。在这里插入图片描述

2. 二叉树的类型

  • 普通二叉树(Binary Tree):每个节点最多有两个子节点,没有其他特殊性质要求。
  • 满二叉树(Full Binary Tree):所有非叶子节点都有两个子节点,并且所有叶子节点在同一层。高度为 h h h的满二叉树,含有 2 h − 1 2^h-1 2h1个 结点。
  • 完全二叉树(Complete Binary Tree):除了最后一层外,每层节点都是满的,并且最后一层的叶子节点尽可能左对齐。
  • 平衡二叉树(Balanced Binary Tree):左右子树的高度差不超过1。
  • 二叉排序树(Balanced Sorting Tree):左子树关键字 < 根结点关键字 < 右子树关键字。

3. 二叉树的性质

  • n 0 = 1 + n 2 n_0=1+n_2 n0=1+n2

n = n 0 + n 1 + n 2 n=n_0+n_1+n_2 n=n0+n1+n2(二叉树的性质)且 n = n 1 + 2 n 2 + 1 n=n_1+2n_2+1 n=n1+2n2+1(树的性质),联立可得 n 0 = 1 + n 2 n_0=1+n_2 n0=1+n2

  • 具有 n n n个结点的完全二叉树的高度 h h h ⌈ l o g 2 ( 1 + n ) ⌉ \lceil log_2(1+n) \rceil log2(1+n)⌉ ⌊ l o g 2 n + 1 ⌋ \lfloor log_2n+1 \rfloor log2n+1

高为 h h h的二叉树最多有 2 h − 1 2^h-1 2h1个结点(满二叉树)
高为 h h h的二叉树最少有 ( 2 h − 1 − 1 ) + 1 = 2 h − 1 (2^{h-1}-1)+1=2^{h-1} (2h11)+1=2h1个结点(高是 h − 1 h-1 h1的满二叉树再加上一个结点)
那么 2 h − 1 ≤ n ≤ 2 h − 1 2^{h-1}\leq n\leq 2^h-1 2h1n2h1,可推出 h − 1 ≤ l o g 2 n h-1 \leq log_2n h1log2n l o g 2 ( n + 1 ) ≤ h log_2(n+1)\leq h log2(n+1)h
故有, h = ⌊ l o g 2 n + 1 ⌋ h=\lfloor log_2n+1 \rfloor h=log2n+1 h = ⌈ l o g 2 ( 1 + n ) ⌉ h=\lceil log_2(1+n) \rceil h=log2(1+n)⌉

  • 对于完全二叉树,可以由结点数 n n n推出 n 0 、 n 1 、 n 2 n_0、n_1、n_2 n0n1n2,即 n 1 = 0 n_1=0 n1=0 1 1 1
    • 若完全二叉树有 2 k 2k 2k个结点,则必有 n 1 = 1 , n 0 = k , n 2 = k − 1 n_1=1,n_0=k,n_2=k-1 n1=1,n0=k,n2=k1

    因为 n = n 0 + n 1 + n 2 → 2 k = n 0 + n 1 + n 2 n=n_0+n_1+n_2 \rightarrow 2k=n_0+n_1+n_2 n=n0+n1+n22k=n0+n1+n2,又因为 n 0 = 1 + n 2 n_0=1+n_2 n0=1+n2,故有 2 k = 2 n 2 + n 1 + 1 2k=2n_2+n_1+1 2k=2n2+n1+1,所以 n 1 = 1 , n 2 = k − 1 , n 0 = k n_1=1,n_2=k-1,n_0=k n1=1,n2=k1,n0=k

    • 若完全二叉树有 2 k − 1 2k-1 2k1个结点,则必有 n 1 = 0 , n 0 = k , n 2 = k − 1 n_1=0,n_0=k,n_2=k-1 n1=0,n0=k,n2=k1

    因为 n = n 0 + n 1 + n 2 → 2 k − 1 = n 0 + n 1 + n 2 n=n_0+n_1+n_2 \rightarrow 2k-1=n_0+n_1+n_2 n=n0+n1+n22k1=n0+n1+n2,又因为 n 0 = 1 + n 2 n_0=1+n_2 n0=1+n2,故有 2 k − 1 = 2 n 2 + n 1 + 1 2k-1=2n_2+n_1+1 2k1=2n2+n1+1,所以 n 1 = 0 , n 2 = k − 1 , n 0 = k n_1=0,n_2=k-1,n_0=k n1=0,n2=k1,n0=k


物理结构

1. 顺序存储

特别适合完全二叉树。根节点存储在数组的第一个位置,对于任意节点在数组中的位置 i i i

  • i i i的左孩子节点的位置是 2 i 2i 2i
  • i i i的右孩子节点的位置是 2 i + 1 2i + 1 2i+1
  • i i i的双亲结点的位置是 ⌊ i / 2 ⌋ \lfloor i/2\rfloor i/2

在这里插入图片描述

若是普通的二叉树所需空间仍然是 2 h − 1 2^h-1 2h1。极大的浪费了存储空间,此时考虑用链式存储方式。

2. 链式存储

typedef struct BiTNode {
    int data;                           // 数据域
    struct BiTNode *lchild, *rchild;    // 指针域
} BiTNode, *BiTree;

在这里插入图片描述
注: n n n个结点的二叉链表共有 n + 1 n+1 n+1 个空链域。

数据的操作

1.先序遍历(根左右)

递归算法
void PreOrder(BiTree T)
{
	if(T)
	{
		visit(T);
		PreOrder(T->lchild);
		PreOrder(T->rchild);
	}
}
非递归算法
void PreOrder(BiTree T)
{
	Stack s, InitStack(s);     // 定义栈
	BiTNode *p = T;            // p是遍历指针
	while (p || !isEmpty(s))
	{
		if (p)                 // 一路向左
		{
			visit(p);
			Push(s, p);
			p = p -> lchild;
		}
		else                   // 左孩子是空,则转向访问右孩子
		{
			Pop(s, p)          // 已经访问过该结点,出栈
			p = p -> rchild;
		}
	}
}

2.中序遍历(左根右)

递归算法
void InOrder(BiTree T)
{
	if(T)
	{
		InOrder(T->lchild);
		visit(T);
		InOrder(T->rchild);
	}
}
非递归算法
void InOrder(BiTree T)
{
	Stack s, InitStack(s);		// 栈
	BiTNode *p = T;   			// 遍历指针
	while (p || !isEmpty(s))	// 若p是NULL则证明GetTop(s)是叶子结点
	{
		if (p)		// 一路向左
		{
			Push(s, p);		// 入栈的是根结点
			p = p -> lchild;
		}
		else
		{
			Pop(s, p), visit(p);	// 因为是一路向左,先弹出来的一定是左孩子,然后是根节点,最后是右孩子
			p = p -> rchild;		// 左走到头了,该向右转
		}
	}
}

3.后序遍历(根左右)

递归算法
void PostOrder(BiTree T)
{
	if(T)
	{
		PostOrder(T->lchild);
		PostOrder(T->rchild);
		visit(T);
	}
}
非递归算法
void PostOrder(BiTree T)
{
	Stack s, InitStack(s);
	BiTNode *p = T;
	BiTNode *r = NULL;
	
	while (p || !isEmpty(s))
	{
		if (p)			// 一路向左
		{
			Push(s, p);
			p = p -> lchild;
		}
		else 
		{
			GetTop(s, p);
			if (p -> rchild && p -> rchild != r) p = p -> rchild;   // 如果右子树存在且未被访问过
			else    	// 否则访问节点并出栈
			{
				Pop(s, p);
				visit(p);
				r = p;   	// 标记已访问节点
				p = NULL;	// 因为是叶子结点或整个子树已经被处理完,还需要继续访问上一个结点,故将p设置为NULL
			}
		}
	}
}

为什么要判断右子树是否访问过:

  • 后序遍历要求在访问一个节点之前先访问其左子树和右子树,因此在每次访问一个节点时必须确保其左右子树已经访问完毕。
  • 通过 r r r指针标记上次访问的节点,如果当前节点的右子树存在且没有被访问过,则先处理右子树;否则说明左右子树都处理完毕,可以访问当前节点。
  • 这种机制确保了每个节点在其左右子树都处理完后才被访问,符合后序遍历的定义。

4.层次遍历(自上而下,从左到右)

void InverLevel(BiTree T)
{
	Queue q, InitQueue(q);	// 队列
	BiTNode *p = T;			// 遍历指针
	EnQueue(q, p);
	
	while (!isEmpty(q))
	{
		DeQueue(q, p);
		visit(p);
		if (p -> lchild) EnQueue(q, p -> lchild);		// 若左孩子非空,则入队列
		if (p -> rchild) EnQueue(q, p -> rchild);		// 若右孩子非空,则入队列
	}
}

5. 由遍历序列构造二叉树

先序+中序

在这里插入图片描述

查找中序遍历中节点值的位置

int findPosition(int* inorder, int inStart, int inEnd, int data)
{
    for (int i = inStart; i <= inEnd; i++) 
        if (inorder[i] == data) 
        	return i;
    return -1;
}

递归构建二叉树

BiTree buildTree(int* preorder, int preStart, int preEnd, int* inorder, int inStart, int inEnd) {
    if (preStart > preEnd || inStart > inEnd)		// 越界
        return NULL;

    // 先序遍历的第一个元素是当前子树的根节点
    int rootData = preorder[preStart];
    BiTree root = (BiTree)malloc(sizeof(BiTNode));
    root->data = rootData;
    root->lchild = root->rchild = NULL;

    // 在中序遍历中找到根节点的位置
    int rootIndex = findPosition(inorder, inStart, inEnd, rootData);

    // 计算左子树的节点数量
    int leftTreeSize = rootIndex - inStart;

    // 递归构建左子树
    root->lchild = buildTree(preorder, preStart + 1, preStart + leftTreeSize, inorder, inStart, rootIndex - 1);

    // 递归构建右子树
    root->rchild = buildTree(preorder, preStart + leftTreeSize + 1, preEnd, inorder, rootIndex + 1, inEnd);

    return root;
}

构建二叉树的主函数

BiTree constructTree(int* preorder, int* inorder, int length) 
{
    return buildTree(preorder, 0, length - 1, inorder, 0, length - 1);
}
后序+中序

在这里插入图片描述
递归构建二叉树

BiTree buildTree(int* postorder, int postStart, int postEnd, int* inorder, int inStart, int inEnd) {
    if (postStart > postEnd || inStart > inEnd) {
        return NULL;
    }

    // 后序遍历的最后一个元素是当前子树的根节点
    int rootData = postorder[postEnd];
    BiTree root = (BiTree)malloc(sizeof(BiTNode));
    root->data = rootData;
    root->lchild = root->rchild = NULL;

    // 在中序遍历中找到根节点的位置
    int rootIndex = findPosition(inorder, inStart, inEnd, rootData);

    // 计算左子树的节点数量
    int leftTreeSize = rootIndex - inStart;

    // 递归构建左子树
    root->lchild = buildTree(postorder, postStart, postStart + leftTreeSize - 1, inorder, inStart, rootIndex - 1);

    // 递归构建右子树
    root->rchild = buildTree(postorder, postStart + leftTreeSize, postEnd - 1, inorder, rootIndex + 1, inEnd);

    return root;
}
层序+中序

在这里插入图片描述
从层序遍历中提取在当前中序区间内的节点

void extractSubLevelOrder(int* levelOrder, int* subLevelOrder, int levelSize, int* inorder, int inStart, int inEnd) 
{
    int subIndex = 0;
    for (int i = 0; i < levelSize; i++)
        for (int j = inStart; j <= inEnd; j++) 
            if (levelOrder[i] == inorder[j]) 
            {
                subLevelOrder[subIndex++] = levelOrder[i];
                break;
            }
}

递归构建二叉树

BiTree buildTree(int* levelOrder, int levelSize, int* inorder, int inStart, int inEnd) {
    if (inStart > inEnd) {
        return NULL;
    }

    // 层序遍历的第一个元素是当前子树的根节点
    int rootData = levelOrder[0];
    BiTree root = (BiTree)malloc(sizeof(BiTNode));
    root->data = rootData;
    root->lchild = root->rchild = NULL;

    // 在中序遍历中找到根节点的位置
    int rootIndex = findPosition(inorder, inStart, inEnd, rootData);

    // 构建左子树的层序遍历数组
    int leftSubLevelOrder[levelSize];
    extractSubLevelOrder(levelOrder, leftSubLevelOrder, levelSize, inorder, inStart, rootIndex - 1);

    // 构建右子树的层序遍历数组
    int rightSubLevelOrder[levelSize];
    extractSubLevelOrder(levelOrder, rightSubLevelOrder, levelSize, inorder, rootIndex + 1, inEnd);

    // 递归构建左子树
    root->lchild = buildTree(leftSubLevelOrder, rootIndex - inStart, inorder, inStart, rootIndex - 1);

    // 递归构建右子树
    root->rchild = buildTree(rightSubLevelOrder, inEnd - rootIndex, inorder, rootIndex + 1, inEnd);

    return root;
}

二、线索二叉树

逻辑结构

线索二叉树是在普通二叉树的基础上进行改进,目的是为了提高对二叉树的遍历效率。在普通二叉树中,节点的左右指针通常指向其左右子树,而在线索二叉树中,除了指向左右子树的指针外,还增加了指向某种遍历次序下该节点的前驱节点和后继节点的线索。

  • 在线索二叉树中,每个节点的左右指针可以被视为是线索化的。
  • 左指针:
    • 如果节点的左子树存在,则指向左子树。
    • 如果节点的左子树不存在,则指向其在中序遍历下的前驱节点(称为前驱线索)。
  • 右指针:
    • 如果节点的右子树存在,则指向右子树。
    • 如果节点的右子树不存在,则指向其在中序遍历下的后继节点(称为后继线索)。

其标志域的含义如下:
l t a g = { 0 , l c h i l d  域指示结点的左孩子  1 , l c h i l d  域指示结点的前驱  r t a g = { 0 , r c h i l d  域指示结点的右孩子  1 , r c h i l d  域指示结点的后继  \begin{aligned} & ltag = \begin{cases}0, & lchild\text { 域指示结点的左孩子 } \\ 1, & lchild\text { 域指示结点的前驱 }\end{cases} \\ & rtag = \begin{cases}0, & rchild\text { 域指示结点的右孩子 } \\ 1, & rchild\text { 域指示结点的后继 }\end{cases} \end{aligned} ltag={0,1,lchild 域指示结点的左孩子 lchild 域指示结点的前驱 rtag={0,1,rchild 域指示结点的右孩子 rchild 域指示结点的后继 

物理结构

采用链式存储方式:

typedef struct ThreadNode{
	int data;									// 数据域
	struct ThreadNode *lchild, *rchild;			// 左右孩子指针
	int ltag, rtag;								// 左右线索标志 
} ThreadNode, *ThreadTree;

在这里插入图片描述

数据的操作

1. 线索二叉树的构造

先序线索二叉树

在这里插入图片描述
访问结点

void visit(ThreadNode *q)
{
	if (q -> lchild == NULL)		// 建立前驱线索
	{
		q -> lchild = pre;
		q -> ltag = 1;
	}
	if (q -> rchild == NULL && pre)	// 建立后继线索
	{
		pre -> rchild = q;
		pre -> rtag = 1;
	}
	pre = q;
}

先序遍历

void PreThread(ThreadNode *T)
{
	if (T)
	{
		visit(T);
		if (T -> ltag == 0) 		// 若上一行处理前驱线索,若不判断是否是索引,就会使得程序陷入死循环中
			PreThread(T -> lchild);
		PreThread(T -> rchild);
	}
}

创建前序索引二叉树

void CreatePreThread(ThreadNode *T)
{
	pre = NULL;
	if (T)
	{
		PreThread(T);
		if (pre -> rchild == NULL) pre -> rtag = 1;		// 检查最后一个结点,将其后继设置成线索
	}
}
中序线索二叉树

在这里插入图片描述
访问结点

void visit(ThreadNode *q)
{
	if (q -> lchild == NULL)		// 建立前驱线索
	{
		q -> lchild = pre;
		q -> ltag = 1;
	}
	if (q -> rchild == NULL && pre)	// 建立后继线索
	{
		pre -> rchild = q;
		pre -> rtag = 1;
	}
	pre = q;
}

中序遍历

void InThread(ThreadNode *T)
{
	if (T)
	{
		InThread(T -> lchild);
		visit(T);
		InThread(T -> rchild);
	}
}

创建中序索引二叉树

void CreateInThread(ThreadNode *T)
{
	pre = NULL;
	if (T)
	{
		InThread(T);
		if (pre -> rchild == NULL) pre -> rtag = 1;		// 检查最后一个结点,将其后继设置成线索
	}
}
后序线索二叉树

在这里插入图片描述
访问结点

void visit(ThreadNode *q)
{
	if (q -> lchild == NULL)		// 建立前驱线索
	{
		q -> lchild = pre;
		q -> ltag = 1;
	}
	if (q -> rchild == NULL && pre)	// 建立后继线索
	{
		pre -> rchild = q;
		pre -> rtag = 1;
	}
	pre = q;
}

中序遍历

void PostThread(ThreadNode *T)
{
	if (T)
	{
		PostThread(T -> lchild);
		PostThread(T -> rchild);
		visit(T);
	}
}

创建中序索引二叉树

void CreatePostThread(ThreadNode *T)
{
	pre = NULL;
	if (T)
	{
		PostThread(T);
		if (pre -> rchild == NULL) pre -> rtag = 1;		// 检查最后一个结点,将其后继设置成线索
	}
}

2. 线索二叉树的遍历

先序线索二叉树找后继

在这里插入图片描述

ThreadNode * Nextnode(ThreadNode *p)
{
	if (p -> rtag) p = p ->rlchild;
	else 
	{
		if (p -> ltag) p = p -> rchild;
		else p = p -> lchild;
	}
	return p;
}

利用先序线索二叉树进行先序遍历

void PreOrder(ThreadNode *T)
{
	for (ThreadNode *p = T; p; p = NextNode(p))
		visit(p);
}
先序线索二叉树找前驱(改用三叉链表)

在这里插入图片描述

三叉链表

typedef struct ThreadNode {
	int data;										// 数据域
	struct ThreadNode *lchild, *rchild, *parent;	// 左右孩子、双亲指针
	int ltag, rtag;									// 左右线索域
}

寻找最右下结点

ThreadNode * Lastnode(ThreadNode *p)
{
	while (p -> rtag == 0) p = p -> rchild;
	return p;
}

找前驱

ThreadNode * Prenode(ThreadNode *p)
{
	if (p -> parent == NULL) return NULL;		// 根结点
	if (p -> ltag) p = p -> lchild;
	else 
	{
		ThreadNode *father = p -> parent;		// 找到p的双亲结点
		if (p == father -> lchild) p = father;
		else if (p == father -> rchild && father -> rtag) p = father;
		else Lastnode(father -> lchild);
	}
	return p;
}

利用先序线索二叉树进行逆先序遍历

void RevPreorder(ThreadNode *T)
{
	for (ThreadNode *p = LastNode(T); p; p = Prenode(p))
		visit(p);
}
中序线索二叉树找后继

在这里插入图片描述
寻找最左下结点

ThreadNode * Firstnode(ThreadNode *p)
{
	while (p -> ltag == 0) p = p -> lchild;
	return p;
}

找后继

ThreadNode * Nextnode(ThreadNode *p)
{
	if (p -> rtag) p = p -> rchild;
	else p = Firstnode(p -> rchild);

	return p;
}

利用中序线索二叉树进行中序遍历

void Inorder(ThreadNode *T)
{
	for (ThreadNode *p = Firstnode(T); p; p = Nextnode(p))
		visit(p);
}
中序二叉树找前驱

在这里插入图片描述

寻找最右下结点

ThreadNode * Lastnode(ThreadNode *p)
{
	while (p -> rtag == 0) p = p -> rchild;
	return p;
}

找前驱

ThreadNode * Prenode(ThreadNode *p)
{
	if (p -> ltag) p = p -> lchild;
	else p = LastNode(p -> lchild);
	
	return p;
}

利用中序线索二叉树进行逆中序遍历

void RevInorder(ThreadNode *T)
{
	for (ThreadNode *p = LastNode(T); p; p = Prenode(p))
		visit(p);
}
后序线索二叉树找后继(改用三叉链表)

在这里插入图片描述
三叉链表

typedef struct ThreadNode {
	int data;										// 数据域
	struct ThreadNode *lchild, *rchild, *parent;	// 左右孩子、双亲指针
	int ltag, rtag;									// 左右线索域
}

寻找最左下结点

ThreadNode * Firstnode(ThreadNode *p)
{
	while (p -> ltag == 0) p = p -> lchild;
	return p;
}

找后继

ThreadNode * Nextnode(ThreadNode *p)
{
	if (p -> parent == NULL) return NULL;		// 根结点
	if (p -> rtag) p = p -> rchild;
	else 
	{
		ThreadNdoe *father = p -> parent;
		if (father -> rchild == p) p = father;
		else if (father -> lchild == p && father -> rtag) p = father;
		else p = Firstnode(father -> rchild);
	}
	return p;
}

利用后序线索二叉树进行后序遍历

void Postorder(ThreadNode *T)
{
	for (ThreadNode *p = Firstnode(T); p; p = Nextnode(p))
		visit(p);
}
后序线索二叉树找前驱

在这里插入图片描述
找前驱

ThreadNode * Prenode(ThreadNode *p)
{
	if (p -> ltag) p = p -> lchild;
	else 
	{
		if (p -> rtag) p = p -> lchild;
		else p = p -> rchild;
	}
	return p;
}

利用后序线索二叉树进行逆后序遍历

void RevPostorder(ThreadNode *T)
{
	for (ThreadNode *p = T; p; p = Prenode(p))
		visit(p);
}

三、树

逻辑结构

树是 n ( n ≥ 0 ) n(n≥0) n(n0)个结点的有限集。当 n = 0 n=0 n=0时,称为空树。在任意一棵非空树中应满足:

  1. 有且仅有一个特定的称为根的结点。
  2. n > 1 n>1 n>1时,其余结点可分为 m ( m > 0 ) m(m>0) m(m>0)个互不相交的有限集 T 1 , T 2 , ⋯ , T m T_1,T_2,⋯,T_m T1,T2,,Tm,其中每个集
    合本身又是一棵树,并且称为根的子树。

显然,树的定义是递归的,即在树的定义中又用到了其自身,树是一种递归的数据结构。树
作为一种逻辑结构,同时也是一种分层结构,具有以下两个特点:

  1. 树的根结点没有前驱,除根结点外的所有结点有且只有一个前驱。
  2. 树中所有结点都可以有零个或多个后继。

在这里插入图片描述

物理结构

1.顺序存储——下标表示法

假设一个 m m m 叉树存储在一个一维数组 A 中,其中下标从 1 开始:

  1. 根节点:存储在 A[1]
  2. 节点的第 k k k个子节点
    • 如果节点 A[i] 的下标为 i,那么其第 k k k个子节点的下标为 m ∗ i + k − ( m − 1 ) m*i + k - (m - 1) mi+k(m1),其中 1 ≤ k ≤ m 1 \leq k \leq m 1km
  3. 节点的双亲节点
    • 如果节点 A[i] 的下标为 i,那么其双亲节点的下标为 ⌊ i + ( m − 1 ) m ⌋ \left\lfloor \frac{i + (m - 1)}{m} \right\rfloor mi+(m1)(根节点除外)。

在这里插入图片描述

优点:对于完全 m m m叉树能够快速找到一个结点的孩子和双亲。
缺点:对于一颗非完全 m m m叉树,可能会造成存储空间的浪费,必须要 m h − 1 m − 1 \frac{m^h-1}{m-1} m1mh1个存储空间。

2.顺序存储——双亲表示法

typedef struct {
	int data;		// 数据域
	int parent;		// 双亲域
} PTNode;

typedef struct {
	PTNode nodes[Maxsize];
	int n;					// 定义当前树的结点个数
}

在这里插入图片描述

优点:找双亲(父节点)很方便
缺点:找孩子不方便,只能从头到尾遍历整个数组

3.顺序存储+链式存储——孩子表示法

typedef struct CTNode {			// 定义孩子结点
	int child;					// 孩子结点在数组中的位置
	struct CTNode *next;		// 下一个孩子
} CTNode;

typedef struct {				// 定义根节点
	int data;
	CTNode *firstChild;			// 记录该结点的第一个孩子
} CTBox;

typedef struct {
	CTBox nodes[Maxsize];
	int n, r;					//定义当前结点的个数以及根节点的位置
} CTree;

在这里插入图片描述

优点:找孩子很方便
缺点:找双亲(父节点)不方便,只能遍历每个链表

4.链式存储——孩子兄弟表示法

typedef struct CSNode {
	int data;									// 数据域
	struct CSNode *firstchild, *nextsibling;	// 第一个孩子结点和右兄弟指针
} CSNode, *CSTree;

左孩子右兄弟
在这里插入图片描述

优点:对于树的前序遍历可以借助二叉树的前序遍历。且插入删除操作也可以借助二叉树的操作实现。
缺点:访问不直观,对于要进行层次遍历不如顺序存储方便。

数据的操作

1. 树的先根遍历

在这里插入图片描述
树的先根遍历与这颗树所对应的二叉树的先序遍历相同

void PreOrder(CSTree *T)
{
	if (T)
	{
		visit(T);
		PreOrder(T -> firstchild);
		PreOrder(T -> nextsibling);
	}
}

2. 树的后根遍历

在这里插入图片描述
树的后根遍历与这颗树所对应的二叉树的中序遍历相同

void PostOrder(CSTree *T)
{
	if (T)
	{
		PostOrder(T -> firstchild);
		PostOrder(T -> nextsibling);
		visit(T);
	}
}

3. 树的层序遍历——孩子表示法

在这里插入图片描述

定义数据结构

typedef struct CTNode {			// 定义孩子结点
	int child;					// 孩子结点在数组中的位置
	struct CTNode *next;		// 下一个孩子
} CTNode;

typedef struct {				// 定义根节点
	int data;
	CTNode *firstChild;			// 记录该结点的第一个孩子
} CTBox;

typedef struct {
	CTBox nodes[Maxsize];
	int n, r;					//定义当前结点的个数以及根节点的位置
} CTree;

在这里插入图片描述

层次遍历

void InverLevel(CTree T)
{
	Queue q, InitQueue(q);
	CTBox p = T.nodes[T.r];			// 根结点
	Enqueue(q, p);					// 根结点入队列
	
	while (!isEmpty(q))
	{
		Dequeue(q, p);
		visit(p);
		CTNode *c= p.firstchild;	// 访问根结点的孩子结点
		while (c) Enqueue(q, T.nodes[c.child]), c = c -> next;	// 将孩子结点全部入队列
	}
}

四、森林

逻辑结构

森林(Forest)是树的概念的延伸,是由 n ( n ≥ 0 ) n(n \geq 0) n(n0)棵互不相交的树的集合。当 n = 0 n = 0 n=0时,称为空森林。森林是一种递归的数据结构,可以将其定义为:

一个空森林:不包含任何树。
一个非空森林:由一棵树及其他不相交的若干棵树组成。

在这里插入图片描述

物理结构

顺序存储——双亲表示法

typedef struct {
	int data;		// 数据域
	int parent;		// 双亲域
} PFNode;

typedef struct {
	PFNode nodes[Maxsize];
	int n;					// 定义当前树的结点个数
}

在这里插入图片描述

优点:找双亲(父节点)很方便
缺点:找孩子不方便,只能从头到尾遍历整个数组

顺序存储+链式存储——孩子表示法

typedef struct CTNode {          // 定义孩子结点
    int child;                   // 孩子结点在数组中的位置
    struct CTNode *next;         // 下一个孩子
} CTNode;

typedef struct {                 // 定义树结点
    int data;
    CTNode *firstChild;          // 记录该结点的第一个孩子
} CTBox;

typedef struct {
    CTBox nodes[Maxsize];        // 所有结点的数组
    int n;                       // 当前结点的个数
    int roots[Maxsize];          // 记录根节点的位置的数组
    int rootCount;               // 根节点的数量
} FCTree;

在这里插入图片描述
优点:找孩子很方便
缺点:找双亲(父节点)不方便,只能遍历每个链表

链式存储——孩子兄弟表示法

typedef struct CSNode {
    int data;                          // 数据域
    struct CSNode *firstchild;         // 第一个孩子结点指针
    struct CSNode *nextsibling;        // 右兄弟结点指针
} CSNode, *CSTree;

typedef struct {
    CSTree roots[Maxsize];             // 根节点的数组
    int rootCount;                     // 根节点的数量
} FCSForest;

在这里插入图片描述
优点:对于森林的前序遍历可以借助二叉树的前序遍历。且插入删除操作也可以借助二叉树的操作实现。
缺点:访问不直观,对于要进行层次遍历不如顺序存储方便。

数据的操作

森林的先序遍历

在这里插入图片描述
森林的先序遍历与这颗树所对应的二叉树的先序遍历相同

void PreOrder(CSTree *T)
{
	if (T)
	{
		visit(T);
		PreOrder(T -> firstchild);
		PreOrder(T -> nextsibling);
	}
}

森林的中序遍历

在这里插入图片描述
森林的中序遍历与这颗树所对应的二叉树的中序遍历相同

void InOrder(CSTree *T)
{
	if (T)
	{
		InOrder(T -> firstchild);
		visit(T);
		InOrder(T -> nextsibling);
	}
}

五、树、森林与二叉树的转换

树转二叉树

在这里插入图片描述

森林转二叉树

在这里插入图片描述

二叉树转树

在这里插入图片描述

二叉树转森林

在这里插入图片描述


六、哈夫曼编码(应用)

带权路径长度

结点的权:有某种现实含义的数值(如:表示结点的重要性等)
结点的带权路径长度:从树的根到该结点的路径长度(经过的边数)与该结点上权值的乘积
树的带权路径长度:树中所有叶结点的带权路径长度之和(WPL, Weighted Path Length)
W P L = ∑ i = 1 n w i l i WPL=\sum_{i=1}^n w_il_i WPL=i=1nwili

在这里插入图片描述
例如这颗树的 W P L = 3 × ( 5 + 1 + 10 + 3 ) = 57 WPL=3\times(5+1+10+3)=57 WPL=3×(5+1+10+3)=57

哈夫曼树的定义

在这里插入图片描述

在含有 n n n个带权叶结点的二叉树中,其中带权路径长度(WPL)最小的二叉树称为哈夫曼树,也称最优二叉树

哈夫曼树的构造

给定 n n n个权值分别为 w 1 , w 2 , … , w n w_1, w_2,…, w_n w1,w2,,wn的结点,构造哈夫曼树的算法描述如下:
1)将这 n n n个结点分别作为n棵仅含一个结点的二叉树,构成森林F。
2)构造一个新结点,从 F F F中选取两棵根结点权值最小的树作为新结点的左、右子树,并且将新结点的权值置为左、右子树上根结点的权值之和。
3)从 F F F中删除刚才选出的两棵树,同时将新得到的树加入 F F F中。
4)重复步骤2)和3),直至 F F F中只剩下一棵树为止。

在这里插入图片描述
哈夫曼树的性质:

  1. 每个初始结点最终都成为叶结点,且权值越小的结点到根结点的路径长度越大
  2. 哈夫曼树的结点总数为 2 n − 1 2n − 1 2n1

n 0 = n , n 1 = 0 n_0=n,n_1=0 n0=n,n1=0,又因为 n 总 = n 0 + n 1 + n 2 n_{总}=n_0+n_1+n_2 n=n0+n1+n2 n 0 = 1 + n 2 n_0=1+n_2 n0=1+n2,联立可得 n 总 = 2 n 0 − 1 = 2 n − 1 n_{总}=2n_0-1=2n-1 n=2n01=2n1

  1. 哈夫曼树中不存在度为1的结点。
  2. 哈夫曼树并不唯一,但WPL必然相同且为最优

七、并查集

逻辑结构

并查集(Disjoint Set)是一种数据结构,用于管理一组互不相交的集合。它支持两种主要操作:查找(Find)和合并(Union)。

在这里插入图片描述

物理结构——顺序存储

因为并查集只需要查找双亲结点,则用双亲表示法能将并查集的优势最大化。

在这里插入图片描述

typedef struct {
	int data;			// 数据域
	int parent;			// 双亲位置域
} PTNode;

typedef struct {
	PTNode nodes[Maxsize];		// 双亲表示
	int n;						// 结点数
} PTree;

数据的操作

1.初始化

在这里插入图片描述

void Initial(int s[])
{
	for (int i = 0; i < size; i ++ ) s[i] = -1;
}

2.并(Union)

在这里插入图片描述

void Union(int s[], int Root1, int Root2)
{
	if (Root1 == Root2) return; 			// 若来自一个集合,不进行合并操作
	else s[Root2] = Root1; 					// 将Root2变成Root1的孩子
}

3.查(Find)

在这里插入图片描述

int find(int s[], int x)
{
	while (s[x] != -1) x = s[x];
	return x;
}

4.并查集的优化

分析时间复杂度
  • Find操作( h h h为树高): O ( h ) O(h) O(h)
    在这里插入图片描述

  • Union操作: O ( 1 ) O(1) O(1)

故并查集的时间复杂度主要是由树的高度决定的。

Union操作的优化(按秩合并)

优化思路:在每次Union操作构建树的时候,尽可能让树不长高。

  • 用根节点的绝对值表示树的结点总数
  • Union操作,让小树合并到大树

在这里插入图片描述

void Union(int s[], int Root1, int Root2)
{
	if (Root1 == Root1) return;
	if (s[Root2] > s[Root1])		// Root2的结点数比Root1少(绝对值)
	{
		s[Root1] += s[Root2];		// 累加结点数
		s[Root2] = s[Root1];		// 小树合并到大树
	}
	else
	{
		s[Root2] += s[Root1];		// 累加结点数
		s[Root1] = s[Root2];		// 小树合并到大树
	}
}

用该方法构造出来的树 h ≤ ⌊ l o g 2 n ⌋ + 1 h\leq\lfloor log_2n \rfloor+1 hlog2n+1。故Find操作的时间复杂度为 O ( l o g 2 n ) O(log_2n) O(log2n)

Find操作的优化(路径压缩)

优化思路:在每次Find操作的时候,将查找路径上所有结点都挂到根结点下。
在这里插入图片描述

int find(int s[], int x)
{
	if (s[x] != -1) s[x] = find(s[x]);			//将x的父亲置为x父亲的祖先节点,实现路径的压缩
	return s[x];
}

这样可使树的高度不超过 O ( α ( n ) ) O(\alpha(n)) O(α(n)) α ( n ) \alpha(n) α(n)是一个增长很缓慢的函数,对于常见的 n n n值,通常 α ( n ) ≤ 4 \alpha(n)\leq4 α(n)4,故树高 h ≤ 4 h\leq4 h4,Find操作的时间复杂度 O ( 1 ) O(1) O(1)

在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Nie同学

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值