数据结构与算法系列文章目录
【数据结构与算法】data structures & algorithms 第一章:复杂度分析
【数据结构与算法】data structures & algorithms 第二章:基本概念
【数据结构与算法】data structures & algorithms 第三章:线性数据结构
【数据结构与算法】data structures & algorithms 第四章:树的数据结构
【数据结构与算法】data structures & algorithms 第五章:图的数据结构
【数据结构与算法】data structures & algorithms 第六章:各类常见的排序算法
【数据结构与算法】data structures & algorithms 第七章:散列表算法的初步运用
【数据结构与算法】data structures & algorithms 第八章:红黑树的理解与使用
一 、树与树的表示
1、数据管理的基本操作:查找
- 查找(Searching):根据某个给定关键字K,从集合R中找出关键字与K相同的记录;
- 静态查找:集合中记录是固定的;没有插入和删除操作,只有查找;
- 动态查找:集合中记录时动态变化的;除查找,还可能发生插入和删除
静态查找
struct LNode
{
ElementType Element[MaxSize];
int Length;
}
typedef struct LNode* List;
- 方法1:顺序查找
int sequentialSearch(List Tbl, ElementType k)
{
//在表Tbl[1]~Tbl[10]中查找关键字为k的数据元素
Tbl->Element[0] = k; //建立哨兵
for(int i = Tbl->Length; Tbl->Element[i] != k; i--);
return i; //查找成功返回所在单元下标;不成功返回0
}
顺序查找算法的时间复杂度为O(N)
- 方法2:二分查找(Binary Search)
假设n个数据元素的关键字满足有序、且是连续存放(数组),那么可以进行二分查找;
int binarySearch(List Tbl, ElementType k)
{
int left, right, mid;
int NoFound = -1;
left = 1;
right = Tbl->Length;
while (left <= right)
{
mid = (left + right) / 2;
if (k < Tbl->Element[mid])
{
right = mid -1;
}
else if (k > Tbl->Element[mid])
{
left = mid + 1;
}
else
{
return mid;
}
}
return NoFound;
}
二分查找算法具有对数的和时间复杂度O(logN)
-
11个元素的查找判定树
- 判定树上每个结点需要的查找次数刚好为该结点所在的层数
- 查找成功时查找次数不会超过判定树的深度
- n个结点的判定树的深度为**[log2n]+1**
- 平均搜索次数 A S L = ( 4 ∗ 4 + 4 ∗ 3 + 2 ∗ 2 + 1 ) / 11 = 3 ASL = (4*4+4*3+2*2+1)/11 = 3 ASL=(4∗4+4∗3+2∗2+1)/11=3
2、树的定义
- 树(tree):
n
(
n
≥
0
)
n (n \geq 0)
n(n≥0)个结点构成的有限集合
- 当n=0时,称为空树
- 非空树的性质:
- 树中有一个称为根(Root)的特殊结点,用r表示
- 其余结点可分为 m ( m > 0 ) m(m>0) m(m>0)个互不相交的有限集 T 1 , T 2 , ⋯ , T m T_1,T_2, \cdots ,T_m T1,T2,⋯,Tm,其中每个集合本身又是一棵树,称为原来树的子树
-
树的判断
- 子树是不相交的
- 除了根节点外,每个结点有且仅有一个父节点
- 一颗N个结点的树有N-1条边
-
树的基本术语
- 结点的度(Degree):结点的子树个数
- 树的度:树的所有结点中最大的度数
- 叶结点(Leaf):度为0的结点
- 父结点(Parent):有子树的结点,是其子树的根结点的父结点
- 子结点(Child):父结点的子树的根结点,是其子结点
- 兄弟结点(Sibling):具有同一父结点的各结点彼此是兄弟结点
- 路径和路径长度:从结点 n 1 n_1 n1到 n k n_k nk的路径为一个结点序列 n 1 , n 2 , ⋯ , n k n_1,n_2, \cdots ,n_k n1,n2,⋯,nk, n i n_i ni是 n i + 1 n_{i+1} ni+1的父结点;路径包含边的个数为路径的长度;
- 祖先结点(Ancestor):沿树根到某一结点路径上的所有结点都是这个结点的祖先结点
- 子孙结点(Descendant):某一结点的子树中的所有结点是这个结点的子孙结点;
- 结点的层次(Level):规定根结点在第一层,其它任一结点的层数是其父结点的层数加1;
- 树的深度(Depth):树中所有结点中的最大层次是这棵树的深度
-
树的表示法:儿子-兄弟表示法
3、二叉树
3.1二叉树定义
-
**二叉树T:**一个有穷的结点集合
- 这个集合可以为空,则为空树
- 若不为空,则它是由根结点和称为其**左子树TL和右子树TR**的两个不相交的二叉树组成
- 二叉树具有五种基本形态
- 空树
- 具有一个根节点的树
- 有左子树的树
- 有右子树的树
- 有左右子树的树
- 二叉树的子树有左右顺序之分
-
特殊二叉树
- 斜二叉树(Skewed Binary Tree)
- 满二叉树(Perfect Binary Tree)
每一层的结点数都达到最大值
-
Full Binary Tree
每个根结点要么没有子结点,要么有两个子结点
-
完全二叉树(Complete Binary Tree)
-
二叉树的重要性质
- 一个二叉树第i层的最大结点数为: 2 i − 1 , i ≥ 1 2^{i-1}, i \geq 1 2i−1,i≥1
- 深度为K的二叉树有最大结点总数为: 2 k − 1 , k ≥ 1 2^k-1, k \geq 1 2k−1,k≥1
- 对任何非空二叉树T,若 n 0 n_0 n0表示叶结点的个数、 n 2 n_2 n2是度为2的非叶结点个数,那么二者满足关系 n 0 = n 2 + 1 n_0 = n_2 + 1 n0=n2+1
-
二叉树的 抽象数据类型定义
- 类型名称:二叉树
- 对象数据集:一个有穷的结点集合;若不为空,则由根结点和其左、右二叉子树组成
- 操作集:
B
T
∈
B
i
n
T
r
e
e
,
I
t
e
m
∈
E
l
e
m
e
n
t
T
y
p
e
BT \in BinTree, Item \in ElementType
BT∈BinTree,Item∈ElementType,重要操作有:
- Boolean IsEmpty(Bin Tree BT):判别BT是否为空
- void Traversal(Bin Tree BT):遍历,按某顺序访问每个结点
- BinTree CreaBinTree():创建一个二叉树
- 常用的遍历方法:
- void PreOrderTraversal(BinTree BT):先序——根、左子树、右子树
- void InOrderTraversal(BinTree BT):中序——左子树、根、右子树
- void PostOrderTraversal(BinTree BT):后序——左子树、右子树、根
- void LevelOrderTraversal(BinTree BT):层次遍历——从上到下、从左到右
3.2二叉树的顺序存储结构
- 完全二叉树:按从上到下、从左到右顺序存储;n个结点的完全二叉树的结点父子关系:
- 非根结点(序号 i > 1 i > 1 i>1)的父结点的序号为 [ i / 2 ] [i/2] [i/2]
- 结点(序号为 i i i)的左子结点的序号为** 2 i 2i 2i**;若 2 i ≤ n 2i \leq n 2i≤n,否则没有左结点
- 结点(序号为 i i i)的右子结点的序号为 2 i + 1 2i+1 2i+1;若 2 i + 1 ≤ n 2i+1 \leq n 2i+1≤n,否则没有右结点
- 一般二叉树也可以采用这种结构,大但会造成空间浪费
3.3二叉树的链表存储结构
定义结构体:采用儿子-兄弟表示法
struct TreeNode
{
ElementType Data;
TreeNode* Left;
TreeNode* Right;
}
typedef TreeNode* BinTree;
4、二叉树的遍历
- 先序、中序和后序遍历过程:遍历过程中经过结点的路线一样,只是访问各结点的时机不同
4.1 先序遍历
-
遍历过程为:
- 访问根结点
- 先序遍历其左子树
- 先序遍历其右子树
-
先序遍历例子
- 先序遍历的递归程序
void PreOrderTraversal(BinTree BT)
{
if (BT)
{
cout << BT->Data;
PreOrderTraversal(BT->Left);
PreOrderTraversal(BT->Right);
}
}
4.2中序遍历
-
遍历过程:
- 中序遍历其左子树
- 访问根结点
- 中序遍历其右子树
-
中序遍历例子
- 中序遍历的递归程序
void InOrderTraversal(BinTree BT)
{
if (BT)
{
InOrderTraversal(BT->Left);
cout << BT->Data;
InOrderTraversal(BT->Right);
}
}
4.3后序遍历
-
遍历过程为:
- 后序遍历其左子树
- 后序遍历其右子树
- 访问根结点
-
后序遍历例子
- 后序遍历的递归程序
void PostOrderTraversal(BinTree BT){
if (BT) {
PostOrderTraversal(BT->Left);
PostOrderTraversal(BT->Right);
cout << BT->Data;
}
}
4.4二叉树的非递归遍历
- 以中序遍历非递归遍历算法为例
- 遇到一个结点,就把它压栈,并去遍历它的左子树
- 当左子树遍历结束后,从栈顶弹出这个结点并访问它
- 然后按其右指针再去中序遍历该结点的右子树
void InOrderTraversal(BinTree BT){
BinTree T = BT;
Stack S = creatStack();
while (T || !isEmpty(S)) {
//一直向左并将沿途结点压入堆栈
while (T) {
push(T, S);
T = T->Left;
}
if (!isEmpty(S)) {
T = pop(S); //结点弹出堆栈
cout << T->Data; //访问结点
T = T->Right; //转向右子树
} }}
- 先序遍历非递归遍历算法程序
- 遇到一个结点,访问它并将它压栈,同时去遍历它的左子树
- 当左子树遍历结束后,从栈顶弹出这个结点
- 然后按其右指针再去遍历该结点的右子树
void PreOrderTraversal(BinTree BT)
{
BinTree T = BT;
Stack S = creatStack();
while (T || !isEmpty(S))
{
while (T)
{
cout << T->Data;
push(T, S);
T = T->Left;
}
if (!isEmpty(S))
{
T = pop(S);
T = T->Right;
}
}
}
- 后序遍历非递归遍历算法程序:
- 遇到一个结点,把它压栈,然后去遍历它的左子树
- 当左子树遍历结束后,弹出一个结点
- 若该结点的右结点为NULL,或右结点最近已经访问过,则访问当前结点
- 若不满足上述条件,则将当前结点二次压栈,并遍历其右子树
void PostOrderTraversal(BinTree BT)
{
BinTree T = BT;
Stack S = creatStack();
BinTree T1 = NULL; //用来记录最近一次访问的结点
while (T || !isEmpty(S))
{
while (T)
{
push(T, S);
T = T->Left;
}
if (!isEmpty(S))
{
//遍历完该结点的左子树后
//弹出该结点
T = pop(S);
//判断该结点右子树是否为空
//或右子树结点是否已经访问过
//若满足上述条件,则访问该结点,并更新T1的最近访问结点
//应当注意的是当前结点应该置零
if (T->Right == NULL || T->Right == T1)
{
cout << T->Data;
T1 = T;
T = NULL;
}
//若不满足上述条件,则将该结点二次压栈,并开始访问右子树
else
{
push(T, S);
T = T->Right;
}
}
}
}
4.5、二叉树的层序遍历
-
二叉树遍历的核心问题:二维结构的线性化
- 从结点访问其左、右子结点
- 访问左子结点后,右子结点处理
- 需要一个存储结构保存暂时不访问的结点
- 存储结构:堆栈、队列
-
队列实现:遍历从根结点开始,首先将根结点入队,然后开始执行循环:结点出队、访问该结点、其左右子结点分别入队
-
**层序基本过程:**先根结点入队,然会:
- 从队列中取出一个元素
- 访问该元素所指结点
- 若将该元素所指结点的左右子结点非空,则将其左右子结点顺序入队
void LevelOrderTraversal(BinTree BT)
{
//创建空队列Q
Queue Q;
BinTree T;
if (!BT)
{
return;
}
addQ(BT, Q);
while (!isEmpty(Q))
{
T = deleteQ(Q);
cout << T->Data;
if (T->Left)
{
addQ(T->Left, Q);
}
if (T->Right)
{
addQ(T->Right, Q);
}
}
}
4.6二叉树遍历的应用
- 输出二叉树中的叶结点
- 在递归遍历算法中增加检测结点的左右子树是否都为空的条件
void PreOrderPrintLeaves(BinTree BT)
{
if (BT)
{
if (!BT->Left && !BT->Right)
{
cout << BT->Data;
}
PreOrderPrintLeaves(BT->Left);
PreOrderPrintLeaves(BT->Right);
}
}
- 求二叉树的高度
int PostOrderGetHeight(BinTree BT)
{
int HL, HR, MaxH;
if (BT)
{
HL = PostOrderGetHeight(BT->Left);
HR = PostOrderGetHeight(BT->Right);
MaxH = (HL > HR) ? HL : HR;
return (MaxH + 1);
}
else
{
return 0;
}
}
-
二元运算表达式树及其遍历
- 先序遍历得到前缀表达式、中序遍历得到中缀表达式、后序遍历得到后缀表达式
- 中缀表达式会受到运算符优先级的影响,可以通过添加括号处理
-
通过遍历序列确定唯一二叉树
- 中序遍历序列与任意一种遍历序列都可以确定唯一的二叉树
- 例如,先序和中序如下
5、树的同构
5.1题意理解
- 同构:一棵树可以通过若干次左右子树互换后,与第二棵树一致,则称这两棵树同构
- 输出格式:输入给出两棵二叉树的信息
- 先在一行中给出该树的结点数
- 第i行对应编号第i个结点,给出该结点中存储的字母、其左子结点的编号、右子结点的编号
- 如果子结点为空,则在相应位置上给出“-”
5.2求解思路
- 二叉树的表示:用结构数组表示二叉树(也称为静态链表)
#define MaxTree 10
#define ElementType char
#define Tree int
#define Null -1
struct TreeNode
{
ElementType Element;
Tree Left;
Tree Right;
};
- 程序框架搭建
- 读数据建立二叉树
- 二叉树同构判别
int main()
{
TreeNode T1[MaxTree];
TreeNode T2[MaxTree];
Tree R1 = BuildTree(T1);
Tree R2 = BuildTree(T2);
if (Isomorphic(R1, R2, T1, T2))
{
cout << "yes" << endl;
}
else
{
cout << "no" << endl;
}
return 0;
}
- 如何建立二叉树
Tree BuildTree(TreeNode& T[])
{
ElementType e;
Tree l;
Tree r;
Tree check[MaxTree];
int N;
Tree root;
cout << "输入二叉树的结点数: ";
cin >> N;
if (N)
{
for (int i = 0; i < N; i++)
{
check[i] = 0;
}
cout << "依次输入结点名,左子结点百年好,右子结点编号" << endl;
for (int i = 0; i < N; i++)
{
cin >> e;
cin >> l;
cin >> r;
if (l != "-")
{
T[i].Left = l;
check[T[i].Left] = 1;
}
else
{
T[i].Left = Null;
}
if (r != "-")
{
T[i].Right = r;
check[T[i].Right] = 1;
}
else
{
T[i].Right = Null;
}
}
for (int i = 0; i < N; i++)
{
if (!check[i])
{
break;
}
}
root = i;
}
return root;
}
- 如何判别两棵二叉树同构
int Isomorphic(Tree R1, Tree R2, TreeNode T1[], TreeNode T2[])
{
//空树都是同构的
if (R1 == Null && R2 == Null)
{
return 1;
}
//一颗为空树,另一颗为非空树的情况
if (((R1 == Null) && (R2 != Null)) || ((R1 != Null) && (R2 == Null)))
{
return 0;
}
//两棵树的根节点不一样
if (T1[R1].Element != T2[R2].Element)
{
return 0;
}
//都没有左子树的情况
if ((T1[R1].Left == Null) && (T2[R2].Left == Null))
{
//递归判断是否所有结点都没有左子树
return Isomorphic(T1[R1]>Right, T2[R2].Right);
}
//结点的左子结点不为空,且相同的情况
if (((T1[R1].Left != Null) && (T2[R2].Left != Null)) && ((T1[T1[R1].Left].Element) == (T2[T2[R2].Left])))
{
//分别对两棵树的左右子结点,重复上述操作,并将结果返回作逻辑与操作
return (Isomorphic(T1[R1].Left, T2[R2].Left) && Isomorphic(T1[R1].Right, T2[R2].Right));
}
//有一棵树的左子结点为空,或两棵树的左子结点不相同的情况
else
{
//上述操作都是判断两棵二叉树是否相同,这里是判断两棵树是否能够若干次左右子树的交换,得到两棵相同的二叉树
return (Isomorphic(T1[R1].Left, T2[R2].Right) && Isomorphic(T1[R1].Right, T2[R2].Left));
}
}
6、二叉搜索树
6.1定义
- 二叉树搜索树(BST, Binary Search Tree),也称为二叉排序树或二叉查找树
- 非空左子树的所有键值小于其根结点的键值
- 非空右子树的所有键值大于其根节点的键值
- 左、右子树都是二叉搜索树
- 二叉搜索树的特别函数
- BinTree find(ElementType X, BinTree BST):从二叉搜索树BST中查找元素X,返回其所在结点的地址
- BinTree findMin(BInTree BST):从二叉搜索树BST中查找并返回最小元素所在结点的地址
- BinTree findMax(BinTree BST):从二叉搜索树BST中查找并返回最大元素所在结点的地址
- BinTree insert(ElementType X, BinTree BST):向二叉搜索树中插入元素
- BinTree deleteB(ElementType X, BinTree BST):删除二叉搜索树中的元素
6.2二叉搜索树的查找操作
-
查找从根节点开始,如果树为空,返回NULL
-
若搜索树非空,则根节点键值和X进行比较:
- 若X小于根节点键值,只需在左子树中继续搜索
- 若X大于根节点键值,只需在右子树中继续搜索
- 若两者比较结果是相等,搜索完成,返回指向此结点的指针
-
递归函数
BinTree find(ElementType X, BinTree BST)
{
if ( !BST)
{
return NULL;
}
if (X > BST->Data)
{
return find(X, BST->Right);
}
else if (X < BST->Data)
{
return find(X, BST->Left);
}
else
{
return BST;
}
}
- 上述递归函数都是尾递归,由于非递归函数的执行效率高,可将尾递归函数改为迭代函数
BinTree iterFInd(ElementType X, BinTree BST)
{
while (BST)
{
if (X > BST->Data)
{
BST = BST->Right;
}
else if (X < BST->Data)
{
BST = BST->Left;
}
else
{
return BST;
}
}
return NULL;
}
查找的效率取决于树的高度
6.3查找最大和最小元素
最大元素一定是在树的最右分支的段结点上
最小元素一定是在树的最左分支的端结点上
- 查找最小元素的递归函数
BinTree findMin(BinTree BST)
{
if (!BST)
{
return NULL;
}
else if (!BST->Left)
{
return BST;
}
else
{
return findMin(BST->Left);
}
}
- 查找最大元素的迭代函数
BinTree findMax(BinTree BST)
{
if (BST)
{
while (BST->Right)
{
BST = BST->Right;
}
}
return BST;
}
6.4二叉搜索树的插入
- 关键是要找到元素应该插入的位置,可以采用与find函数类似的方法
BinTree insert(ElementType X, BinTree BST)
{
if (!BST)
{
BinTree BST1 = new TreeNode;
BST1->Left = BST1->Right = NULL;
}
else
{
if (X < BST->Data)
{
BST->Left = insert(X, BST->Left);
}
else if (X > BST->Data)
{
BST->Right = insert(X, BST->Right);
}
}
return BST;
}
6.5二叉搜索树的删除
- 删除操作要考虑三种情况:
- 删除的结点没有子树,是叶结点:直接删除,并再修改其父结点指针–置为NULL
- 删除的结点只有一个子树:将其父结点的指针指向要删除结点的子结点
- 删除的结点有两棵子树:用另一结点替代被删除结点,右子树的最小元素或者左子树的最大元素
BinTree deletetB(ElementType X, BinTree BST)
{
BinTree tmp;
if (!BST)
{
cout << "NULL" << endl;
}
//要删除的结点在根结点的左子树中
else if (X < BST->Data)
{
BST->Left = deleteB(X, BST->Left);
}
//要删除的结点在根节点的右子树中
else if (X > BST->Data)
{
BST->Right = deleteB(X, BST->Right);
}
//找到要删除的结点
else
{
//要删除的结点有两棵子树
if (BST->Left && BST->Right)
{
//使用右子树中的最小元素替代
tmp = findMin(BST->Right);
BST->Data = tmp->Data;
BST->Right = deleteB(BST->Data, BST->Right);
}
else
{
//要删除的结点无子树或者只有一棵子树
tmp = BST;
if (!BST->Left)
{
BST = BST->Right;
}
else if (!BST->Rigth)
{
BST = BST->Left;
}
}
}
return BST;
}
7、平衡二叉树
7.1什么是平衡二叉树
平衡二叉树(Balanced Binary Tree)(AVL树):空树,或者任一结点左、右子树高度差的绝对值不大于1,即 ∣ B F ( T ) ∣ ≤ 1 |BF(T)|\leq1 ∣BF(T)∣≤1
平衡因子(Balanced Factor,简称BF): B F ( T ) = h L − h R BF(T) = h_L-h_R BF(T)=hL−hR,其中hL和hR分别为T的左、右子树的高度
高度并不是树的最大层次数,而是该根结点的最大路径长度
设 n h n_h nh是高度为 h h h的平衡二叉树的最小结点数, F h F_h Fh为对应的斐波那契数列
- 给定结点数 n n n的AVL树的最大高度为 O ( l o g 2 n ) O(log_2n) O(log2n)
7.2平衡二叉树的调整
现有一棵平衡二叉树,插入一个新的元素,更新BF值后发现,有一结点的BF值大于1,称该节点为发现者,新插入的结点为麻烦结点
- RR插入:麻烦节点 位于 发现者的右子树的右子树中,则此时需要进行RR旋转(右单旋)
- LL插入:麻烦结点 位于 发现者的左子树的左子树中,则此时需要进行LL旋转(左单旋)
- LR插入:麻烦结点 位于 发现者的左子树的右子树中,则此时需要进行LR旋转(左右双旋)
- RL插入:麻烦结点 位于 发现者的右子树的左子树中,则此时需要进行RL旋转(右左双旋)
- 注意:有时候插入元素即可,不需要调整结构,也可能需要重新计算一些平衡因子
8、案例:是否为同一棵二叉搜索树
8.1案例介绍
-
题意理解
给定一个插入序列就可以唯一确定一棵二叉搜索树;然而,一棵给定的二叉搜索树却可以由多种不同的插入序列得到;
问题:对于输入的各种插入序列,需要判断它们是否能生成一样的二叉搜索树
样例:{4, 2}的4指树有4个结点,2指有两个序列要与原序列进行比较
-
求解思路
两个序列是否对应相同搜索树的判别-
1、分别建立两棵搜索树的判别方法
根据两个序列分别建树,再判别树是否一样 -
2、不建树的判别方法
-
3、建一棵树,再判别其它序列是否与该树一致
- Ⅰ.搜索树表示
- Ⅱ.建立搜索树T
- Ⅲ.判别一序列是否与搜索树T一致
-
8.2程序实现
- 搜索树表示
struct TreeNode
{
int v;
TreeNode* Left;
TreeNode* Right;
int flag;
};
typedef TreeNode* Tree;
-
程序框架搭建
对每组数据:- 读入N和L,N为树的结点数,L为需判断的树的个数
- 根据第一行序列建立树T
- 依据树T分别判别后面的T个序列是否能与T形成同一搜索树并输出结果
需要设计的主要函数:
- 读数据建立搜索树T
- 判别一序列是否与T构成一样的搜索树
int main()
{
//N表示树由N个结点组成
//L表示有L个序列要与原序列比较
int N, L;
Tree T;
cin >> N;
while(N)
{
cin >> L;
//根据第一行序列建搜索树
T = makeTree(N);
for (int i = 0; i < L; i++)
{
if (judge(T, N))
{
cout << "yes" << endl;
}
else
{
cout << "no" << endl;
}
//清除T中的标记flag
resetT(T);
}
freeTree(T);
cin >> N;
}
return 0;
}
- 建搜索树
Tree makeTree(int N)
{
Tree T;
int V;
cin >> V;
T = newNode(V);
for (int i = 0; i < N; i++)
{
cin >> V;
T = insert(T, V);
}
return T;
}
Tree newNode(int V)
{
Tree T = new TreeNode;
T->v = V;
T->Left = T->Right = NULL;
T->flag = 0;
return T;
}
Tree insert(Tree T, int V)
{
if (!T)
{
T = newNode(V);
}
else
{
if (V > T->v)
{
T->Right = insert(T->Right, V);
}
else
{
T->Left = insert(T->Left, V);
}
}
return T;
}
- 判别
方法:在树T中按顺序搜索序列中的每个数- 如果每次搜索所经过的结点在前面均出现过,则一致
- 否则(某次搜索中遇到前面未出现的结点),则不一致
int judge(Tree T, int N)
{
//ret为0代表目前还一致,1代表已经不一致
int V, ret = 0;
cin >> V;
//判断根节点是否一致
if (V != T->v)
{
ret = 1;
}
else
{
T->flag = 1;
}
//根结点已经访问过了,故i从1开始
for (int i = 1; i < N; i+=)
{
cin >> V;
//但发现序列中的某个数与T不一致时,必须把把序列后面的数都访问完
if ((!ret) && (!check(T, V)))
{
ret = 1;
}
}
if (ret)
{
return 0;
}
else
{
return 1;
}
}
int check(Tree T, int V)
{
if (T->flag)
{
if (V <= T->v)
{
return check(T->Left, V);
}
else if (V > T->v)
{
return check(T->Right, V);
}
}
else
{
if (V == T->v)
{
T->flag = 1;
return 1;
}
else
{
return 0;
}
}
}
- 清除T中各结点的flag标记
void resetT(Tree T)
{
if (T->Left)
{
resetT(T->Left);
}
if (T->Right)
{
resetT(T->Right);
}
T->flag = 0;
}
- 释放T的空间
void freeTree(Tree T)
{
if (T->Left)
{
freeTree(T->Left);
}
if (T->Right)
{
freeTree(T->Right);
}
delete T;
}
9、堆
9.1什么是堆
-
优先队列(Priority Queue):特殊的队列,取出元素的顺序是依照元素的**优先权(关键字)**大小,而不是元素进入队列的先后顺序
-
问题:如何组织优先队列
若采用数组或链表实现优先队列- 数组:
插入——元素总是插入尾部 ~ Θ ( 1 ) \Theta(1) Θ(1)
删除——查找最大(或最小)关键字 ~ Θ ( n ) \Theta(n) Θ(n)
从数组中删去需要移动的元素 ~ O ( n ) O(n) O(n) - 链表:
插入——元素总是插入链表的头部 ~ Θ ( 1 ) \Theta(1) Θ(1)
删除——查找最大(或最小)关键字 ~ Θ ( n ) \Theta(n) Θ(n)
删去结点 ~ Θ ( 1 ) \Theta(1) Θ(1) - 有序数组:
插入——找到合适的位置 ~ O ( n ) O(n) O(n)或 O ( l o g 2 n ) O(log_2n) O(log2n)
移动元素并插入 ~ O ( n ) O(n) O(n)
删除——删去最后一个元素 ~ Θ ( 1 ) \Theta(1) Θ(1) - 有序链表:
插入——找到合适的位置 ~ O ( n ) O(n) O(n)
插入元素 ~ Θ ( 1 ) \Theta(1) Θ(1)
删除——删除首元素或最后元素 ~ Θ ( 1 ) \Theta(1) Θ(1)
- 数组:
-
这里使用完全二叉树表示优先队列
- **结构性:**用数组表示的完全二叉树
- **有序性:**任一结点的关键字是其子树所有结点的最大值(或最小值)
- 最大堆(MaxHeap),也称大顶堆:根节点的关键字为两边子树所有结点的关键字的最大值
- 最小堆(MinHeap),也称小顶堆:根节点的关键字为两边子树所有结点的关键字的最小值
-
完全二叉树
如果二叉树的深度为k,则除第k层外其余所有层节点的度都为2,且叶子节点从左到右依次存在。也即是,将满二叉树的最后一层从左到右依次删除若干节点就得到完全二叉树。
满二叉树是一棵特殊的完全二叉树,但完全二叉树不一定是满二叉树。 -
堆的抽象数据类型描述
- 类型名称:最大堆
- 数据对象集:完全二叉树,每个结点的元素值不小于其子结点的元素值
- 操作集:最大堆
H
∈
M
a
x
H
e
a
p
H\in MaxHeap
H∈MaxHeap,元素
i
t
e
m
∈
E
l
e
m
e
n
t
T
y
p
e
item\in ElementType
item∈ElementType,主要操作有:
- MaxHeap create(int MaxSize):创建一个空的最大堆
- Boolean isFull(MaxHeap J):判断最大堆H是否已满
- insert(MaxHeap H, ElementType item):将元素item插入最大堆H
- Boolean isEmpty(MaxHeap H):判断最大堆H是否为空
- ElementType deleteMax(MaxHeap H):返回H中最大元素(高优先级)
9.2操作集
- 最大堆的定义
struct HeapStruct
{
ElementType* Elements;
int Size;
int Capacity;
};
typedef HeapStruct* MaxHeap;
- 最大堆的空创建
MaxHeap creat(int MaxSize)
{
MaxHeap H = new HeapStruct;
H->Elements = new ElementType[MaxSize + 1];
H->Size = 0;
H->Capacity = MaxSize;
H->Elements[0] = MaxData;
//定义“哨兵”为大于堆中所有可能元素的值
//同样的也可以设置MinData为小于堆中所有元素的值,适用于最小堆
return H;
}
- **插入:**将新增结点插入到其父结点到根结点的有序序列中
void insert(MaxHeap H, ElementType item)
{
int i;
if (isFull(H))
{
cout << "最大堆已满";
return;
}
i = ++H->Size;
for (; H->Elements[i / 2] < item; i /=2)
{
//父节点若比item小,则往下调;
//若一直到根结点,都没有比item大的结点
//哨兵作为最大值,会结束循环体
H->Elements[i] = H->Elements[i / 2];
}
H->Elements[i] = item;
}
上述操作比交换数据要快, T ( N ) = O ( l o g N ) T(N) = O(logN) T(N)=O(logN)
- 最大堆的删除:取出根结点(最大值)元素,同时删除堆的一个结点,重新调整完全二叉树, T ( N ) = O ( l o g N ) T(N) = O(log N) T(N)=O(logN)
ElementType deleteMax(MaxHeap& H)
{
int parent, child;
ElementType MaxItem, temp;
if (isEmpty(H))
{
cout << "最大堆已空" << endl;
return;
}
MaxItem = H->Elements[1];
//用最大堆中最后一个元素从根节点开始向上过滤下层结点
temp = H->Elements[H->Size--];
//条件用来判断父结点是否有左子结点
for (parent = 1; parent * 2 <= H->Size; parent = child)
{
child = parent * 2;
//在左、右子结点中选出值最大的那个
if ((child != H->Size) && (H->Elements[child] < H->Elements[child + 1]))
{
child++
}
if (temp >= H->Elements[child])
{
break;
}
else
{
H->Elements[parent] = H->Elements[child];
}
}
H->Elements[parent] = temp;
return MaxItem;
}
- 最大堆的建立
建立最大堆:将已经存在的N个元素按最大堆的要求存放在一个一维数组中- 方法1:通过插入操作,将N个元素一个个相继插入到一个初始为空的堆中去,其时间代价最大为** O ( N l o g N ) O(NlogN) O(NlogN)**
- 方法2:在线性时间复杂度下建立最大堆
- Ⅰ、将N个元素按输入顺序存入,先满足完全二叉树的结构特性
- Ⅱ、调整各结点位置,以满足最大堆的有序特性
- 找出最后一个有子结点的父结点,让该父结点的树变为最大堆;
- 相继地在数组上一个位置对应的父结点,让该结点的树变为最大堆;
- 一直循环,直到整个完全二叉树的根结点;
- **线性时间复杂度为 **$T(n)