【数据结构】第4章 树与二叉树

第4章 树与二叉树

  • 1 树、森林
    • 1.1 树的定义
    • 1.2 基本术语
    • 1.3 树的性质
    • 1.4 树的存储结构
      • 1.4.1 双亲表示法【顺序存储】
      • 1.4.2 孩子表示法【链式存储】
      • 1.4.3 孩子兄弟表示法(二叉树表示法)【链式存储】
      • 1.4.4 举例
    • 1.5 树和森林的遍历
      • 1.5.1 树的遍历
        • (1) 先根遍历
        • (2) 后根遍历
        • (3)层次遍历
      • 1.5.2 森林的遍历
        • (1)先序遍历森林
        • (2)中序遍历森林
  • 2 二叉树【逻辑结构】
    • 2.1 二叉树的定义
    • 2.2 特殊二叉树
    • 2.3 二叉树的性质
    • 2.4 二叉树的存储结构
      • 2.4.1 顺序存储结构
      • 2.4.2 链式存储结构★
    • 2.5 二叉树的遍历
      • 2.5.0 二叉树遍历特点
      • 2.5.1 先序遍历PreOrder(NLR)
      • 2.5.2 中序遍历InOrder(LNR)
      • 2.5.3 后序遍历PostOrder(LRN)
      • 2.5.4 遍历的非递归算法
        • (1)先序遍历
        • (2)中序遍历
        • (3)后序遍历
        • (4)层次遍历
      • 2.5.5 由遍历序列构造二叉树
    • 2.6 线索二叉树【物理结构】
      • 2.6.1 线索二叉树的存储结构
      • 2.6.2 先序线索二叉树
        • (1)先序线索二叉树的构造
        • (2)先序线索二叉树的遍历
      • 2.6.3 中序线索二叉树
        • (1)中序线索二叉树的构造
        • (2)中序线索二叉树的遍历
      • 2.6.4 后序线索二叉树
        • (1)后序线索二叉树的构造
        • (2)后序线索二叉树的遍历
  • 3 树、森林与二叉树的转换
    • 3.1 树转换为二叉树
    • 3.2 森林转换为二叉树
    • 3.3 二叉树转换为森林
  • 4 树(森林)的应用——并查集
  • 5 树与二叉树的应用
    • 5.1 二叉排序树(二叉查找树BST)
      • 5.1.1 二叉排序树的定义
      • 5.1.2 二叉排序树的特点
      • 5.1.3 二叉排序树的基操
        • (1)查找
          • (1.1)非递归
          • (1.2)递归
        • (2)插入
        • (3)构造
        • (4)删除
    • 5.2 平衡二叉树(平衡树AVL)
      • 5.2.1 平衡二叉树的定义
      • 5.2.2 平衡二叉树的基操
        • (1)插入
          • (1.1)LL平衡旋转(右单旋转)
          • (1.2)RR平衡旋转(左单旋转)
          • (1.3)LR平衡旋转(先左后右双旋转)
          • (1.4)RL平衡旋转(先右后左双旋转)
        • (2)查找
    • 5.3 哈夫曼树和哈夫曼编码
      • 5.3.1 哈夫曼树的定义
      • 5.3.2 哈夫曼树的构造算法
      • 5.3.3 哈夫曼树的特点
      • 5.3.4 哈夫曼编码
  • 6 其他
  • 7 参考

1 树、森林

1.1 树的定义

  • 树是递归定义的
  • 树是一种逻辑结构,也是一种分层结构
  • 树的特点:
    (1)树的根结点没有前驱,除根结点外的所有结点有且只有一个前驱
    (2)树种所有结点可以有零个或多个后继
  • n n n个结点的树中有 n − 1 n-1 n1条边
  • 树可为空

1.2 基本术语

【数据结构】第4章 树与二叉树_第1张图片

  • 祖先←——→子孙
    双亲←——→孩子←——→兄弟←——→堂兄弟
  • 结点的度       :树中一个结点的孩子个数
    树的度        :树中结点的最大度数
  • 分支结点(非终端结点):度>0的结点
    叶子结点(终端结点) :度=0的结点
  • 结点的层次      :从树根开始定义
    结点的深度      :从根结点开始自顶向下逐层累加
    结点的高度      :从叶结点开始自底向上逐层累加
    树的高度(深度)   :树中结点的最大层数
    树的宽度       :树中含结点数最多的那一层的结点个数
  • 有序树        :树中结点的各子树从左到右是有次序的,不能互换
    无序树
  • (两个结点之间的)路径:两个结点之间所经过的结点序列
    结点的路径长度    :路径上所经过的边的个数
    树中的路径从上向下
    树的路径长度     :从树根到每个结点的路径长度的总和
    根到每个结点的路径长度的最大值 = 树的高度 - 1
  • 森林         : m ( m ≥ 0 ) m(m≥0) mm0棵互不相交的树的集合

1.3 树的性质

  • 树中的结点数=分支数 + 1
    分支数 = 树中各结点的度之和

假 设 树 中 度 为 i ( i = 0 , 1 , ⋅ ⋅ ⋅ , m ) 的 结 点 数 为 n i 假设树中度为i(i=0,1,···,m)的结点数为n_i ii=0,1,,mni
(1) 总 结 点 数 = n 0 + n 1 + n 2 + ⋅ ⋅ ⋅ + n m 总结点数=n_0+n_1+n_2+···+n_m =n0+n1+n2++nm
(2) 总 分 支 数 = 0 × n 0 + 1 × n 1 + 2 × n 2 + ⋅ ⋅ ⋅ + m × n m 总分支数=0×n_0+1×n_1+2×n_2+···+m×n_m =0×n0+1×n1+2×n2++m×nm
(3) 总 结 点 数 = 总 分 支 数 + 1 总结点数=总分支数+1 =+1

  • 度为 m m m的树中第 i i i层上至多有 m i − 1 m^{i-1} mi1个结点( i ≥ 1 i≥1 i1
  • 高度为 h h h m m m叉树至多有 n = m h − 1 m − 1 n=\frac{m^h-1}{m-1} n=m1mh1个结点
    m h − 1 + m h − 2 + m h − 3 + ⋅ ⋅ ⋅ + m − 1 m^{h-1}+m^{h-2}+m^{h-3}+···+m-1 mh1+mh2+mh3++m1推导,此时高度为 n n n个结点组成的树中的最小高度(等比数列求和)
  • 具有 n n n个结点的 m m m叉树的最小高度为 h = ⌈ l o g m ( n ( m − 1 ) + 1 ) ⌉ h=\lceil{log}_m(n(m-1)+1)\rceil h=logm(n(m1)+1)
    此时,除了最大层,其余层的结点的双亲结点的度数应该都为 m m m,其实就是高度为 h h h m m m叉树至多含有结点树的逆推导
  • m m m叉树中:
  • 结点 i i i的第 k k k个子女结点的编号 j = ( i − 1 ) × m + k + 1 j=(i-1)×m+k+1 j=(i1)×m+k+1 1 ≤ i ≤ m 1≤i≤m 1im
    i i i个结点之前,有1 ∼ i − 1 \sim i-1 i1 i − 1 i-1 i1个结点,每一个结点都有 m m m个孩子,所以到结点 i i i的第1个子女结点时,编号已经到了 ( i − 1 ) × m + 1 (i-1)×m+1 (i1)×m+1 + 1 +1 +1是因为要算上根结点)
  • 反之, i = ⌊ [ j − ( k + 1 ) ] / m + 1 ⌋ i=\lfloor[j-(k+1)]/m+1\rfloor i=[j(k+1)]/m+1 1 < i ≤ m 11<im
  • 结点 i i i有右兄弟的条件: i ≤ ⌊ ( i + m − 2 ) / m ⌋ × m i≤\lfloor(i+m-2)/m\rfloor×m i(i+m2)/m×m,右兄弟的编号为 i + 1 i+1 i+1
    当结点 i i i不是其双亲的第 m m m个子女( ( i − 1 ) × m + m + 1 (i-1)×m+m+1 (i1)×m+m+1)时才有右兄弟
    或者,对于任一双亲结点 j j j,其第 m m m个子女结点的编号是 j m + 1 jm+1 jm+1,此时结点 i i i有右兄弟的条件: ( i − 1 ) % m ! = 0 (i-1)\%m!=0 (i1)%m!=0
    j m + 1 = ( j − 1 ) m + m + 1 jm+1=(j-1)m+m+1 jm+1=(j1)m+m+1
    j m = i − 1 jm=i-1 jm=i1 m m m的整数倍,此时结点 i i i是其双亲结点 j j j的第 m m m个子女结点

1.4 树的存储结构

1.4.1 双亲表示法【顺序存储】

  • 顺序存储
  • 思想:每个结点(根结点除外)只有唯一双亲
  • 优点:便于寻找双亲
  • 缺点:求结点的孩子时需要遍历整个结构
    【数据结构】第4章 树与二叉树_第2张图片
#define MAX_TREE_SIZE 100			//树中最多结点数

/*树的结点定义*/
typedef struct {
	ElemType data;					//数据元素值
	int parent;						//双亲位置域
}PTNode;

/*树的类型定义*/
typedef struct {
	PTNode nodes[MAX_TREE_SIZE];	//双亲表示
	int n;							//结点数
}PTree;

1.4.2 孩子表示法【链式存储】

  • 一维数组顺序存储每个结点信息【同双亲表示法】
  • 单链表存储每个结点的孩子结点,每个链表结点存储孩子结点在数组中的序号和指向其兄弟的指针
  • n n n个结点有 n n n个孩子链表
  • 优点:便于寻找孩子
  • 缺点:寻找双亲需要遍历 n n n个结点中孩子链表指针域所指向的 n n n个孩子链表
    【数据结构】第4章 树与二叉树_第3张图片
/*孩子链表结点定义*/
typedef struct ChildNode {
	int nodeID;						//孩子在数组中的序号
	struct ChildNode* next;			//指向兄弟
}ChildNode, * ChildList;

/*树的结点定义*/
typedef struct {
	ElemType data;					//数据元素值
	ChildList child;				//孩子单链表
}CTNode;

/*树的类型定义*/
typedef struct {
	CTNode nodes[MAX_TREE_SIZE];	//孩子表示
	int n;							//结点数
}CTree;

1.4.3 孩子兄弟表示法(二叉树表示法)【链式存储】

  • 以二叉链表作为树的存储结构
  • 结点内容:
    (1)结点值
    (2)指向结点第一个孩子结点的指针
    (3)指向结点下一个兄弟结点的指针
  • 优点:
    (1)便于实现树与二叉树的转换
    (2)便于查找孩子
  • 缺点:查找双亲结点麻烦
  • 改进:为每个结点增设一个 p a r e n t parent parent域指向其双亲结点
typedef struct CSNode {
	ElemType data;								//数据域
	struct CSNode* firstchild, * nextsibling;	//第一个孩子和右兄弟指针
}CSNode, * CSTree;

1.4.4 举例

【数据结构】第4章 树与二叉树_第4张图片
【数据结构】第4章 树与二叉树_第5张图片

1.5 树和森林的遍历

森林 二叉树
先根遍历 先序遍历 先序遍历
后根遍历 中序遍历 中序遍历

1.5.1 树的遍历

(1) 先根遍历

  • 规则:根结点——→子树
  • 遍历序列与相应二叉树的先序序列相同

(2) 后根遍历

  • 规则:子树——→根结点
  • 遍历序列与相应二叉树的中序序列相同

(3)层次遍历

  • 同二叉树的层次遍历

1.5.2 森林的遍历

(1)先序遍历森林

  • 规则
    1° 访问森林中第一棵树的根结点;
    2° 先序遍历第一棵树中根结点的子树森林;
    3° 先序遍历除去第一棵树之后剩余的树构成的森林。
  • 遍历序列与相应二叉树的先序序列相同

(2)中序遍历森林

  • 规则
    1° 中序遍历第一棵树中根结点的子树森林;
    2° 访问森林中第一棵树的根结点;
    3° 中序遍历除去第一棵树之后剩余的树构成的森林。
  • 遍历序列与相应二叉树的中序序列相同

2 二叉树【逻辑结构】

2.1 二叉树的定义

  • 二叉树是递归定义的
  • 二叉树是有序树
  • 二叉树的每个结点至多只有2棵子树
  • 二叉树与度为2的有序树的区别:
    (1)度为2的树至少有3个结点,而二叉树可为空;
    (2)度为2的有序树的孩子的左右次序是相对于另一孩子而言的,而二叉树的结点次序是确定的。

2.2 特殊二叉树

  • 满二叉树:高度为 h h h,且含有 2 h − 1 2^h-1 2h1个结点的二叉树

对满二叉树按层序编号,对于编号为 i i i的结点:
(1)若有双亲,则其双亲为 ⌊ i / 2 ⌋ \lfloor i/2 \rfloor i/2
(2)若有左孩子,则左孩子为 2 i 2i 2i
(3)若有右孩子,则右孩子为 2 i + 1 2i+1 2i+1

  • 完全二叉树:当且仅当其每个结点都与高度为 h h h的满二叉树中编号为 1 ∼ n 1\sim n 1n的结点一一对应时,高度为 h h h、有 n n n个结点的二叉树

(1)若 i ≤ ⌊ n / 2 ⌋ i≤\lfloor n/2 \rfloor in/2,则结点 i i i为分支结点,否则为叶子结点
(2)叶子结点只可能出现在层次最大的两层
(3)若有度为1的结点,则只可能有一个,且该结点只有左孩子而无右孩子
(4)按层序编号后,一旦出现某结点(编号为 i i i)为叶子结点或只有左孩子则编号大于 i i i的结点均为叶子结点
(5)若 n n n为偶数,则编号最大的分支结点(编号为 n / 2 n/2 n/2)只有左孩子,没有右孩子,其余分支结点左、右孩子都有。
   若 n n n为奇数,则每个分支结点都有左孩子和右孩子

  • 二叉排序树:左子树上的所有结点的关键字均小于根结点的关键字
          右子树上的所有结点的关键字均大于根结点的关键字
          左、右子树又各是一棵二叉排序树
  • 平衡二叉树:树上任一结点的左子树和右子树的深度之差不超过1

2.3 二叉树的性质

假 设 树 中 度 为 i ( i = 0 , 1 , ⋅ ⋅ ⋅ , 2 ) 的 结 点 数 为 n i 假设树中度为i(i=0,1,···,2)的结点数为n_i ii=0,1,,2ni,总结点数为 n n n

  • n 0 = n 2 + 1 n_0=n_2+1 n0=n2+1
    总 结 点 数 = n 0 + n 1 + n 2 总结点数=n_0+n_1+n_2 =n0+n1+n2
    总 分 支 数 = 0 × n 0 + 1 × n 1 + 2 × n 2 总分支数=0×n_0+1×n_1+2×n_2 =0×n0+1×n1+2×n2
    总 结 点 数 = 总 分 支 数 + 1 总结点数=总分支数+1 =+1
  • 非空二叉树上第 k k k层上至多有 2 k − 1 2^{k-1} 2k1个结点( k ≥ 1 k≥1 k1
  • 高度为h的二叉树至多有 2 h − 1 2^h-1 2h1个结点
  • 完全二叉树按层序排序,则:
    (1)当 i > 1 i>1 i>1时,结点 i i i的双亲的编号为 ⌊ i / 2 ⌋ \lfloor i/2 \rfloor i/2
       i i i为偶数时,其双亲的编号为 i / 2 i/2 i/2,结点 i i i是双亲的左孩子
       i i i为奇数时,其双亲的编号为 ( i − 1 ) / 2 (i-1)/2 (i1)/2,结点 i i i是双亲的右孩子
    (2)当 2 i ≤ n 2i≤n 2in时,结点 i i i的左孩子编号为 2 i 2i 2i,否则无左孩子
       当 2 i + 1 ≤ n 2i+1≤n 2i+1n时,结点 i i i的右孩子编号为 2 i + 1 2i+1 2i+1,否则无右孩子
    (3)结点 i i i所在层次(深度)为 ⌊ l o g 2 i ⌋ \lfloor {log}_2i \rfloor log2i
  • 具有 n n n个(n>0)结点的完全二叉树的高度为 ⌈ l o g 2 ( n + 1 ) ⌉ \lceil {log}_2(n+1)\rceil log2(n+1) ⌊ l o g 2 n ⌋ + 1 \lfloor {log}_2n\rfloor+1 log2n+1
    2 h − 1 − 1 < n ≤ 2 h − 1 2^{h-1}-12h11<n2h1 2 h − 1 ≤ n < 2 h 2^{h-1}≤n<2^h 2h1n<2h
  • 完全二叉树中, n 1 = 1 或 0 n_1=1或0 n1=10,可根据奇偶性判断 n 1 n_1 n1的值

2.4 二叉树的存储结构

2.4.1 顺序存储结构

  • 适合存储完全二叉树和满二叉树
  • 最好从数组下标1开始存储树中结点

2.4.2 链式存储结构★

  • 二叉链表的常用结点结构
    二叉链表的结点结构
  • 含有 n n n个结点的二叉链表中,含有 n + 1 n+1 n+1个空链域
    n n n个结点共有 2 n 2n 2n个指针域,用了 n − 1 n-1 n1个,还剩 2 n − ( n − 1 ) 2n-(n-1) 2n(n1)个空链域
typedef struct BitNode {
	ElemType data;							//数据域
	struct  BitNode* lchild, * rchild;		//左、右孩子指针
}BitNode,*BiTree;

2.5 二叉树的遍历

  • 二叉树的遍历:按某条搜索路ing访问树中每个结点,使得每个结点均被访问一次,而且仅被访问一次。
  • 序:根结点在何时被访问
  • NLR、LNR、LRN:按照先遍历左子树后遍历右子树的原则进行二叉树遍历
根结点 左子树 右子树
N L R

【数据结构】第4章 树与二叉树_第6张图片

2.5.0 二叉树遍历特点

  • 中序遍历中, n n n m m m前的条件: n n n m m m的左方
  • 三种遍历序列中,所有叶子结点的先后顺序相同
    因为访问左右子树的先后顺序不变
  • 先序序列和后序序列相反的条件:任意结点只有一个孩子,即树的高度 = 结点个数
    不可能存在一个结点同时拥有左右孩子
    先序序列和后序序列相同的条件:只有一个根结点
  • 先序序列和中序序列相同的条件:所有非叶结点只有右子树

2.5.1 先序遍历PreOrder(NLR)

void PreOrder(BiTree T) {
	if (!T)
		return;								//如果二叉树为空,则什么也不做
	visit(T);								//访问根结点
	PreOrder(T->lchild);					//访问左子树
	PreOrder(T->rchild);					//访问右子树
}

2.5.2 中序遍历InOrder(LNR)

void InOrder(BiTree T) {
	if (!T)
		return;								//如果二叉树为空,则什么也不做
	PreOrder(T->lchild);					//访问左子树
	visit(T);								//访问根结点
	PreOrder(T->rchild);					//访问右子树
}

2.5.3 后序遍历PostOrder(LRN)

void PostOrder(BiTree T) {
	if (!T)
		return;								//如果二叉树为空,则什么也不做
	PreOrder(T->lchild);					//访问左子树
	PreOrder(T->rchild);					//访问右子树
	visit(T);								//访问根结点
}

2.5.4 遍历的非递归算法

(1)先序遍历

借助栈

void PreOrder(BiTree T) {
	InitStack(S);							//初始化栈
	BitNode* p = T;							//设置遍历指针p
	if (!T)
		return;								//如果二叉树为空,则什么也不做
	while (p || !IsEmpty(S)) {				//栈不空或p不空时循环
		if (p) {
			visit(p);						//栈顶元素出栈,访问出栈结点
			/*一路向左*/
			Push(S, p);						//当前结点入栈
			p = p->lchild;					//左子树
		}
		else {
			/*左孩子空,出栈*/
			Pop(S, p);						//转向出栈结点的右子树
			p = p->rchild;					//右子树
		}
	}
}

(2)中序遍历

借助栈

void InOrder(BiTree T) {
	InitStack(S);							//初始化栈
	BitNode* p = T;							//设置遍历指针p
	if (!T)
		return;								//如果二叉树为空,则什么也不做
	while (p || !IsEmpty(S)) {				//栈不空或p不空时循环
		if (p) {
			/*一路向左*/
			Push(S, p);						//当前结点入栈
			p = p->lchild;					//左子树
		}
		else {
			/*左孩子空,出栈*/
			Pop(S, p);						//转向出栈结点的右子树
			visit(p);						//栈顶元素出栈,访问出栈结点
			p = p->rchild;					//右子树
		}
	}
}

(3)后序遍历

借助栈
1° 沿着根的左孩子,依次入栈,直到左孩子为空;
2° 读栈顶元素:
  若其右孩子不空且未被访问过,将右子树转执行1°
  否则,转执行3°
3° 栈顶元素出栈并访问

void PostOrder(BiTree T) {
	InitStack(S);								//初始化栈
	BitNode* p = T;								//设置遍历指针p
	BitNode* r = NULL;							//辅助指针r,指向最近访问过的结点;或者在结点中增加一个标志域,记录是否已被访问
	if (!T)
		return;									//如果二叉树为空,则什么也不做
	while (p || !IsEmpty(S)) {					//栈不空或p不空时循环
		if (p) {
			/*一路向左*/
			Push(S, p);							//当前结点入栈
			p = p->lchild;						//左子树
		}
		else {
			GetTop(S, p);						//读栈顶结点
			if (p->rchild && p->rchild != r) {	//若栈顶结点i的右子树存在,且未被访问过
				p = p->rchild;					//转向i的右孩子j
				push(S, p);						//将结点j压入栈
				p = p->lchild;					//转向结点j的左子树
			}
			else {								//否则,弹出结点并访问
				Pop(S, p);						//此时栈顶结点i的左右子树都已分析完,将结点i出栈
				visit(p);						//访问结点i
				r = p;							//记录被访问的结点i
				p = NULL;						//结点访问完后,重置p指针
			}
		}
	}
}

(4)层次遍历

借助队列

void LevelOrder(BiTree T) {
	InitQueue(Q);								//初始化辅助队列
	BitNode* p;									//设置遍历指针p
	if (!T)
		return;									//如果二叉树为空,则什么也不做
	EnQueue(Q, T);								//将根结点入队
	while (!IsEmpty(Q)) {						//队列不空则循环
		DeQueue(Q, p);							//队头结点出队
		visit(p);								//访问出队结点
		if (p->lchild)
			EnQueue(Q, p->lchild);				//左子树不空,则左子树树根结点入队
		if (p->rchild)
			EnQueue(Q, p->rchild);				//右子树不空,则右子树树根结点入队
	}
}

可用于求二叉树的高度的【非递归算法】
层次遍历+队列+聪明的小脑瓜
1° 设置变量 l e v e l level level记录当前结点所在层数
2° 设置变量 l a s t last last指向当前层的最右结点(即保存队尾rear)
3° 每次层次遍历时与 l a s t last last指针比较,若两者相等,则 l e v e l + 1 level+1 level+1,并让 l a s t last last指向下一层的最右结点,直到遍历完成
----------------------------------------------------------------------------------------------------------------
求二叉树的高度的【递归算法】
1° 求左子树高度 l l e v e l llevel llevel
2° 求右子树高度 r l e v e l rlevel rlevel
m a x { l l e v e l , r l e v e l } + 1 max\{llevel,rlevel\}+1 max{llevel,rlevel}+1
+ 1 +1 +1是因为要算上根结点的高度

可用于判定完全二叉树
层次遍历+队列+聪明的小脑瓜
1° 将所有结点入队,包括空结点
2° 若遇到空结点入队,则出队查看其后是否有非空结点,若有则不是完全二叉树

2.5.5 由遍历序列构造二叉树

(这个部分蛮有意思的,当时学的时候很喜欢做这部分的题,嘻嘻)
【注意】以下提到的根结点是相对于当前分析的树(子树)而言的

  • 先 序 + 中 序 → 唯 一 二 叉 树 先序+中序→唯一二叉树 +  前者相当于入栈次序,后者相当于出栈次序
    先序——确定根结点值(序列第1个)
    中序——确定根结点位置
  • 后 序 + 中 序 → 唯 一 二 叉 树 后序+中序→唯一二叉树 +
    先序——确定根结点值(序列最后1个)
    中序——确定根结点位置
  • 层 序 + 中 序 → 唯 一 二 叉 树 层序+中序→唯一二叉树 +
    层序——确定根结点值(序列第1个)
    中序——确定根结点位置
  • 先 序 + 后 序 → 不 确 定 先序+后序→不确定 +

2.6 线索二叉树【物理结构】

  1. 引入线索二叉树的目的:
    加快查找结点的前驱或后继的速度

  2. 线索二叉树特点:

  • 若无左子树,令 l c h i l d lchild lchild指向其前驱结点;
  • 若无右子树,令 r c h i l d rchild rchild指向其后继结点;
  • 增加两个标识域,区分指针域指向的是孩子还是前驱(后继)
    线索链表结点图示
    l t a g = { 0 , l c h i l d 域指示结点的左孩子 1 , l c h i l d 域指示结点的前驱 ltag=\begin{cases} 0&\text{,$lchild$域指示结点的左孩子}\\ 1&\text{,$lchild$域指示结点的前驱} \end{cases} ltag={01lchild域指示结点的左孩子lchild域指示结点的前驱
    r t a g = { 0 , r c h i l d 域指示结点的右孩子 1 , r c h i l d 域指示结点的后继 rtag=\begin{cases} 0&\text{,$rchild$域指示结点的右孩子}\\ 1&\text{,$rchild$域指示结点的后继} \end{cases} rtag={01rchild域指示结点的右孩子rchild域指示结点的后继
  1. n n n个结点的线索二叉树上含有的线索数为 n − 1 n-1 n1
    n n n个结点共有链域指针 2 n 2n 2n
    除根结点外,每个结点都被一个指针指向(即树中有多少条边),共 n − 1 n-1 n1个结点
    剩余的链域建立线索,共 2 n − ( n − 1 ) 2n-(n-1) 2n(n1)个线索

2.6.1 线索二叉树的存储结构

/*线索链表*/
typedef struct ThreadNode {
	ElemType data;							//数据域
	struct  ThreadNode* lchild, * rchild;	//左、右线索
	int ltag, rtag;							//左、右线索标志
}ThreadNode,* ThreadTree;

2.6.2 先序线索二叉树

p指向当前正在访问的结点
pre指向刚刚访问过的结点,即pre指向p的前驱

  • 检查p的左指针是否为空,若为空就指向它的前驱pre;
  • 检查pre的右指针是否为空,若为空就指向它的后继p。

(1)先序线索二叉树的构造

/*p指向当前正在访问的结点*/
/*pre指向刚刚访问过的结点,即pre指向p的前驱*/
void PreTread(ThreadTree& p, ThreadTree& pre) {
	if (!p)
		return;									//当前访问结点为空,无操作

	if (!p->lchild) {							//左子树为空
		/*建立前驱线索*/
		p->lchild = pre;
		p->ltag = 1;
	}
	if (!pre && !pre->rchild) {					//当前正在访问结点的前驱结点非空,且前驱结点没有右子树
		/*建立前驱结点的后继线索*/
		pre->rchild = p;
		pre->rtag = 1;
	}
	pre = p;									//标记当前结点成为刚刚访问过的结点

	PreTread(p->lchild, pre);					//递归,线索化左子树
	PreTread(p->rchild, pre);					//递归,线索化右子树
}
void CreatePreThread(ThreadTree T) {
	ThreadTree pre = NULL;

	if (!T)
		return;									//空二叉树,无操作

	PreTread(T, pre);							//线索化二叉树

	/*处理遍历的最后一个结点*/
	pre->rchild = NULL;
	pre->rtag = 1;
}

(2)先序线索二叉树的遍历

/*求先序线索二叉树中结点p在先序序列下的后继*/
ThreadNode* NextNode(ThreadNode* p) {
	if (p->ltag == 0)
		return p->lchild;						//ltag==0,直接返回左孩子

	return p->rchild;							//rtag==0,此时左孩子空,返回右孩子
												//rtag==1,此时左孩子空,右孩子空,返回后继结点
}

/*不含头结点的先序线索二叉树的先序遍历*/
void Preorder(ThreadNode* T) {
	for (ThreadNode* p = T; p; p = NextNode(p))
		visit(p);
}

2.6.3 中序线索二叉树

(1)中序线索二叉树的构造

p指向当前正在访问的结点
pre指向刚刚访问过的结点,即pre指向p的前驱

  • 检查p的左指针是否为空,若为空就指向它的前驱pre;
  • 检查pre的右指针是否为空,若为空就指向它的后继p。
/*中序遍历对二叉树线索化*/
void InTread(ThreadTree &p, ThreadTree &pre) {
	if (!p)
		return;									//当前访问结点为空,无操作

	InTread(p->lchild, pre);					//递归,线索化左子树
	if (!p->lchild) {							//左子树为空
		/*建立前驱线索*/
		p->lchild = pre;
		p->ltag = 1;
	}
	if (!pre && !pre->rchild) {					//当前正在访问结点的前驱结点非空,且前驱结点没有右子树
		/*建立前驱结点的后继线索*/
		pre->rchild = p;
		pre->rtag = 1;
	}
	pre = p;									//标记当前结点成为刚刚访问过的结点
	InTread(p->rchild, pre);					//递归,线索化右子树
}
/*中序遍历建立中序线索二叉树*/
void CreateInThread(ThreadTree T) {
	ThreadTree pre = NULL;
	if (!T)
		return;									//空二叉树,无操作

	InThread(T, pre);							//线索化二叉树

	/*处理遍历的最后一个结点*/
	pre->rchild = NULL;
	pre->rtag = 1;
}

可增加头结点 H e a d Head Head

/*类似于双向线索链表*/
Head -> lchild = 根结点;
Head -> rchild = 最后一个结点;
第一个结点 -> lchild = Head;
最后一个结点 -> rchild = Head;

(2)中序线索二叉树的遍历

/*求中序线索二叉树中中序序列下的第一个结点*/
ThreadNode* Firstnode(ThreadNode* p) {
	while (p->ltag == 0)
		p = p->lchild;							//最左下结点(不一定是叶结点)
	return p;
}

/*求中序线索二叉树中结点p在中序序列下的后继*/
ThreadNode* NextNode(ThreadNode* p) {
	if (p->rtag == 0)
		return Firstnode(p->rchild);
	else
		return p->rchild;						//rtag==1,直接返回后继线索
}

/*不含头结点的中序线索二叉树的中序遍历*/
void Inorder(ThreadNode* T) {
	for (ThreadNode* p = Firstnode(T); p; p = NextNode(p))
		visit(p);
}
/*求中序线索二叉树中中序序列下的最后一个结点*/
ThreadNode* Finalnode(ThreadNode* p) {
	while (p->rtag == 0)
		p = p->rchild;							//最右下结点(不一定是叶结点)
	return p;
}
/*求中序线索二叉树中结点p在中序序列下的前驱*/
ThreadNode* FrontNode(ThreadNode* p) {
	if (p->ltag == 0)
		return Firstnode(p->lchild);
	else
		return p->lchild;						//ltag==1,直接返回前驱线索
}

2.6.4 后序线索二叉树

p指向当前正在访问的结点
pre指向刚刚访问过的结点,即pre指向p的前驱

  • 检查p的左指针是否为空,若为空就指向它的前驱pre;
  • 检查pre的右指针是否为空,若为空就指向它的后继p。

(1)后序线索二叉树的构造

/*p指向当前正在访问的结点*/
/*pre指向刚刚访问过的结点,即pre指向p的前驱*/
void PostTread(ThreadTree& p, ThreadTree& pre) {
	if (!p)
		return;									//当前访问结点为空,无操作

	PostTread(p->lchild, pre);					//递归,线索化左子树
	PoatTread(p->rchild, pre);					//递归,线索化右子树

	if (!p->lchild) {							//左子树为空
		/*建立前驱线索*/
		p->lchild = pre;
		p->ltag = 1;
	}
	if (!pre && !pre->rchild) {					//当前正在访问结点的前驱结点非空,且前驱结点没有右子树
		/*建立前驱结点的后继线索*/
		pre->rchild = p;
		pre->rtag = 1;
	}
	pre = p;									//标记当前结点成为刚刚访问过的结点
}
void CreatePostThread(ThreadTree T) {
	ThreadTree pre = NULL;
	if (!T)
		return;									//空二叉树,无操作

	PostThread(T, pre);							//线索化二叉树

	/*处理遍历的最后一个结点*/
	pre->rchild = NULL;
	pre->rtag = 1;
}

(2)后序线索二叉树的遍历

注意:需要栈的支持
因为结点 x x x的右孩子不一定为空

为了解决求后序后继的问题,需要采用带标志域的三叉链表
(1)双亲   *parent
(2)左孩子  *lchild
(3)右孩子  *rchild

  • 若结点 x x x是二叉树的根,则其后继为空;
  • 若结点 x x x是其双亲的右孩子,或是其双亲的左孩子且其双亲没有右子树,则其后继即为双亲;
  • 若结点 x x x是其双亲的左孩子,且其双亲有右子树,则其后继为双亲的右子树按后序遍历列出的第一个结点。

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

3.1 树转换为二叉树

  • 规则:左孩子右兄弟
    由于根结点无兄弟,所以转换后的二叉树无右子树
  • 画法:
    1° 在兄弟结点之间加一连线;
    2° 对每个结点,只保留它与第一个孩子的连线,而与其他孩子的连线全部抹掉;
    3° 以树根为轴心,顺时针旋转45度。
  • 对应二叉树中无右孩子的结点个数 = 分支结点数 + 1

3.2 森林转换为二叉树

  • 规则
    1° 先将森林中的每棵树转换为二叉树;
    2° 将第一棵树的树根当作根结点;
    3° 依次将每一棵树当作其前一棵树的右子树。
  • 画法
    1° 将森林中的每棵树转换成相应的二叉树;
    2° 每棵树的根也可视为兄弟关系,在每棵树的根之间加一根连线;
    3° 以第一棵树的树根为轴心,顺时针旋转45度。

3.3 二叉树转换为森林

  • 规则
    1° 若二叉树非空,则二叉树的根及其左子树作为第一棵树的二叉树形式,故将根的右链断开;
    2° 二叉树根的右子树又可视为一个由除第一棵树外的森林转换后的二叉树,同1°的转换方法;
    3° 直到最后只剩一棵没有右子树的二叉树为止;
    4° 将每棵二叉树依次转换成树,得到森林。
  • 转换是唯一

4 树(森林)的应用——并查集

Union(S, Root1, Root2);				//将集合S中的子集Root2并入子集Root1
									//要求:Root1与Root2不相交
Find(S, x);							//查找集合S中单元素x所在的子集,并返回该子集的名字
Initial(S);							//将集合S中的每个元素都初始化为只有一个单元素的子集
void Union(int S[], int Root1, int Root2) {
	/*要求Root1和Root2是不同的,且表示子集合的名字*/
	S[Root2] = Root1;				//将根Root2连接到另一根Root1上面
}
int Find(int S[], int x) {
	while (S[x] >= 0)				//循环寻找x的根
		x = S[x];
	return x;						//根的S[]小于0
}
void Initial(int S[]) {				//S为并查集
	for (int i = 0; i < size; i++)
		S[i] = -1;					//每个自成单元素集合
}

5 树与二叉树的应用

5.1 二叉排序树(二叉查找树BST)

  • 动态树表

5.1.1 二叉排序树的定义

左 子 树 结 点 值 < 根 结 点 值 < 右 子 树 结 点 值 左子树结点值<根结点值<右子树结点值 <<

  1. 若左子树非空,则左子树上所有结点的值均小于根结点的值
  2. 若右子树非空,则右子树上所有结点的值均大于根结点的值
  3. 左、右子树也分别是一棵二叉排序树

5.1.2 二叉排序树的特点

  • 在一棵二叉排序树中删除又插入这个结点
    (1)若这个结点为叶结点,则插入结点后的二叉排序树与删除之前的相同
    (2)若这个结点非叶结点,则插入节点后的二叉排序树与删除之前的不完全相同
  • 最佳二叉排序树——高度最小的二叉排序树,构造方法
    1° 对各关键字按值从小到大排序;
    2° 仿照折半查找的判定树的构造方法构造二叉排序树

5.1.3 二叉排序树的基操

(1)查找

(1.1)非递归
BSTNode* BST_Search(BiTree T, ElemType key) {
	while (T != NULL && key != T->data) {
		/*若树空或等于根节结点值,则结束循环*/
		if (key < T->data)
			T = T->lchild;			//小于,则在左子树上查找
		else
			T = T->rchild;			//大于,则在右子树上查找
	}
}

查找效率主要取决于树的高度。

  • 平衡二叉树 ——  O ( l o g 2 n ) O({log}_2n) O(log2n)
  • 单支树   ——  O ( n ) O(n) O(n)
  • 平均查找长度 A S L T = 层 数 × 该 层 结 点 数 总 结 点 数 {ASL}_T=\frac{层数×该层结点数}{总结点数} ASLT=×

二叉排序树、二叉查找的关系

  • 两者查找的平均时间性能差不多
  • 二分查找的判定树唯一,二叉排序树的查找不唯一
  • 关于插入、删除
    (1)二叉排序树无需移动结点,平均执行时间 O ( l o g 2 n ) O({log}_2n) O(log2n)
    (2)二分查找的对象是有序顺序表,代价为 O ( n ) O(n) O(n)
  • 关于有序表的查找
    (1)有序表是静态查找表——存储结构:顺序表;二分查找
    (2)有序表是动态查找表——逻辑结构:二叉排序树
(1.2)递归
BSTNode* BST_Search(BiTree T, ElemType key) {
	if (!T)
		return NULL;
	if (key == T->data)
		return T;
	if (key < T->data)
		return BST_Search(T->lchild, key);
	if (key > T->data)
		return BST_Search(T->rchild, key);
}

(2)插入

int BST_Insert(BiTree& T, KeyType k) {
	if (T == NULL) {
		/*原树为空,新插入的记录为根结点*/
		T = (BiTree)malloc(sizeof(BSTNode));
		T->key = k;
		T->lchild = NULL;
		T->rchild = NULL;
		return 1;							//返回1,插入成功
	}
	else if (k == T->key)
		return 0;							//树中存在相同关键字的结点,插入失败
	else if (k < T->key)
		return BST_Insert(T->lchild, k);	//插入到T的左子树
	else
		return BST_Insert(T->rchild, k);	//插入到T的右子树
}

(3)构造

void Creat_BST(BITree& T, KeyType str[], int n) {
	T = NULL;						//初始化时T为空树
	int i = 0;
	while (i < n) {
		/*依次将每个关键字插入到二叉排序树中*/
		BST_Insert(T, str[i]);
		i++;
	}
}

(4)删除

  1. 若被删除结点 z z z是叶结点,则直接删除;
  2. 若结点 z z z只有一棵左子树或右子树,则让 z z z的子树成为 z z z父结点的子树,替代 z z z的位置
  3. 若结点 z z z有左、右两棵子树,则令 z z z的直接后继(或直接前驱)替代 z z z,然后从二叉排序树中删去这个直接后继(或直接前驱),将问题转换为第1、2两种情况。

在二叉树中删除并插入某结点,得到的二叉树不一定和原来的相同

5.2 平衡二叉树(平衡树AVL)

5.2.1 平衡二叉树的定义

  • 任意结点的左、右子树高度差(平衡因子)的绝对值不超过1
  • 平衡因子取值: − 1 、 0 、 1 -1、0、1 101

5.2.2 平衡二叉树的基操

(1)插入

  • 每当插入一个结点时,需检查操作是否会导致二叉树不平衡。
  • 若导致了不平衡,先找到插入路径上离插入结点最近的平衡因子的绝对值大于1的结点 A A A,再对以 A A A为根的子树进行结点位置调整。
(1.1)LL平衡旋转(右单旋转)
  • 结点 A A A的左孩子( L L L)的左子树( L L L)上插入了新结点
  1. A A A的左孩子 B B B向右上旋转代替 A A A成为根结点
  2. A A A结点向右下旋转成为 B B B的右子树的根结点
  3. B B B的原右子树作为 A A A结点的左子树
    【数据结构】第4章 树与二叉树_第7张图片
(1.2)RR平衡旋转(左单旋转)
  • 结点 A A A的右孩子( R R R)的右子树( R R R)上插入了新结点
  1. A A A的右孩子 B B B向左上旋转代替 A A A成为根结点
  2. A A A结点向左下旋转成为 B B B的左子树的根结点
  3. B B B的原左子树作为 A A A结点的右子树
    【数据结构】第4章 树与二叉树_第8张图片
(1.3)LR平衡旋转(先左后右双旋转)
  • 结点 A A A的左孩子( L L L)的右子树( R R R)上插入了新结点
  1. A A A的左孩子 B B B的右子树的根结点 C C C向左上旋转提升到 B B B结点的位置
  2. 再把该 C C C结点向右上提升到 A A A结点的位置
    【数据结构】第4章 树与二叉树_第9张图片
(1.4)RL平衡旋转(先右后左双旋转)
  • 结点 A A A的右孩子( R R R)的左子树( L L L)上插入了新结点
  1. A A A的右孩子 B B B的左子树的根结点 C C C向右上旋转提升到 B B B结点的位置
  2. 再把该 C C C结点向左上提升到 A A A结点的位置
    【数据结构】第4章 树与二叉树_第10张图片

(2)查找

  • 与二叉排序树相同
  • 在查找过程中,与给定值进行比较的关键字个数不超过树的深度
  • 含结点个数最少的平衡二叉树
    【数据结构】第4章 树与二叉树_第11张图片

假设以 n h n_h nh表示深度为 h h h的平衡树中含有的最少结点数
递推公式: n 0 = 0 , n 1 = 1 , n 2 = 2 , n h = n h − 1 + n h − 2 + 1 n_0=0,n_1=1,n_2=2,n_h=n_{h-1}+n_{h-2}+1 n0=0n1=1n2=2nh=nh1+nh2+1
含有 n n n个结点的平衡二叉树的最大深度为 O ( l o g 2 n ) O({log}_2n) O(log2n),平均查找长度为 O ( l o g 2 n ) O({log}_2n) O(log2n)

5.3 哈夫曼树和哈夫曼编码

5.3.1 哈夫曼树的定义

  • 结点的       :树中结点被赋予的表示某种意义的数值
  • 结点的带权路径长度  :从树的根到任意结点的路径长度(经过的边数) × × ×该结点上的权值
  • 树的带权路径长度   :树中所有叶结点的带权路径长度之和,记为 W P L = ∑ i = 1 n w i l i WPL=\sum_{i=1}^nw_il_i WPL=i=1nwili

w i w_i wi :第 i i i个叶结点所带的权值
l i l_i li  :该叶结点到根结点的路径长度

  • 哈夫曼树(最优二叉树):在含有 n n n个带权叶结点的二叉树中,其中带权路径长度 W P L WPL WPL最小的二叉树

5.3.2 哈夫曼树的构造算法

给定 n n n个权值为 w 1 w_1 w1 w 2 w_2 w2 ⋅ ⋅ ⋅ ··· w n w_n wn的结点

将这 n n n个结点分别作为 n n n棵仅含一个结点的二叉树,构成森林 F F F
构造一个新结点,从 F F F中选取两棵根结点权值最小的树作为新结点的左、右子树,并且将新结点的权值置为左、右子树上根结点的权值之和;
F F F中删除刚才选出的两棵树,同时将新得到的树加入 F F F中;
重复步骤2°和3°,直到 F F F中只剩下一棵树为止。

5.3.3 哈夫曼树的特点

  • 每个初始结点最终都成为叶结点,且权值越小的结点到根结点的路径长度越大
  • 构造过程中共新建了 n − 1 n-1 n1个结点(双分支结点)。因此哈夫曼树的结点总数为 2 n − 1 2n-1 2n1
    n n n个叶结点 + + + n − 1 n-1 n1个双分支结点
  • 每次构造都选择2棵树作为新结点的孩子,因此哈夫曼树中不存在度为 1 1 1的结点

5.3.4 哈夫曼编码

  • 固定长度编码:对每个字符用相等长度的二进制位表示的编码方式
  • 可变长度编码:允许对不同字符用不等长的二进制位表示的编码方式
  • 可变长度编码的特点:
    (1)对频率高的字符赋以短编码;
    (2)对频率较高的字符赋以较长编码;
    (3)可使字符的平均编码长度减短,起到压缩数据的效果。
  • 前缀编码:没有一个编码是另一个编码的前缀的编码
  • 哈夫曼树——→哈夫曼编码
    1° 首先,将每个出现的字符当作一个独立的结点,其权值位它出现的频度(次数);
    2° 其次,构造出对应的哈夫曼树,所有字符结点都出现在叶结点中;
    3° 字符的编码——从根至该字符的路径上边标记的序列
    (1)边标记为“0”:转向左孩子
    (2)边标记为“1”:转向右孩子
    没有明确规定0、1对应左、右子树
    左、右孩子结点的顺序是任意的
    哈夫曼树并不唯一,但 W P L WPL WPL必然相同且最优

6 其他

  • 大根堆:根结点的关键字值既 ≥ ≥ 左子女的关键字值,又 ≥ ≥ 右子女的关键字值

7 参考

王道

你可能感兴趣的:(数据结构)