0.PTA得分截图
1.本周学习总结
1.1 总结树及串内容
一.串:
-
串的定义:
串是由零个或多个字符组成的有限序列。(eg:s="a1a2...an",串名为s,串长为n,a1a2...an为串值,空串n=0).
-
串的存储结构:
顺序存储:
顺序串是用一组地址连续的存储单元存储串值的字符序列。可用一个数组来表示。
- 定长顺序存储的结构体定义:
#define MaxSize 100 //MaxSize常量表示字符串最大长度。
typedef struct
{ char data[MaxSize];//data域存储字符串,
int length;//length域存储字符串长度,
} SqString;
- 动态存储的结构体定义:
typedef struct {
char *ch; // 若是非空串,则按串长分配存储区,
// 否则ch为NULL
int length; // 串长度
} HString;
链式存储:
链串的组织形式与一般的链表类似。链串中的一个结点可以存储多个字符。通常将链串中每个结点所存储的字符个数称为结点大小。
- 链串的节点类型定义:
typedef struct snode
{ char data;
struct snode *next;
} LiString;
- 链串的结构体定义:
ypedef struct Chunk{
char ch[MAXSIZE];
struct Chunk *next;
}Chunk;
typedef struct{
Chunk *head,*tail; //串的头指针和尾指针
int curlen; //串的当前长度
}LString;
-
串的基本操作:
StrAssign(&s,cstr):将字符串常量cstr赋给串s,即生成其值等于cstr的串s。
StrCopy(&s,t):串复制。将串t赋给串s。
StrEqual(s,t):判串相等。若两个串s与t相等则返回真;否则返回假。
StrLength(s):求串长。返回串s中字符个数。
Concat(s,t):串连接:返回由两个串s和t连接在一起形成的新串。
SubStr(s,i,j):求子串。返回串s中从第i(1≤i≤n)个字符开始的、由连续j个字符组成的子串。
InsStr(s1,i,s2):插入。将串s2插入到串s1的第i(1≤i≤n+1)个字符中,即将s2的第一个字符作为s1的第i个字符,并返回产生的新串。
DelStr(s,i,j):删除。从串s中删去从第i(1≤i≤n)个字符开始的长度为j的子串,并返回产生的新串。
RepStr(s,i,j,t):替换。在串s中,将第i(1≤i≤n)个字符开始的j个字符构成的子串用串t替换,并返回产生的新串。
DispStr(s):串输出。输出串s的所有元素值。
-
C++中的字符串:
是一个类-(string)
函数:size(),length(),empty(),substr(),find(),compare(),append()等
#include<string>
//包含这个类
string s;
//字符串声明
getline(cin,s);
//获取一行字符串。
cin>>s;
//获取空格之前字符
-
串的模式匹配算法
- 算法作用:
确定主串中所含子串第一次出现的位置(定位),即如何实现Index(s,t)函数
主串s称为目标串,把子串t称为模式串,因此定位也称作模式匹配。
Index(s,t)函数:匹配成功,返回位置;不成功,返回-1
-
一.BF算法(简单匹配算法):
- BF算法的基本思路:
1.从目标串s=“s0s1…sn-1”的第一个字符开始和模式串t=“t0t1…tm-1”中的第一个字符比较
2.若相等,则继续逐个比较后续字符;
3.否则从目标串s的第二个字符开始重新与模式串t的第一个字符进行比较。
4.依次类推,若从模式串s的第i个字符开始,每个字符依次和目标串t中的对应字符相等,则匹配成功,该算法返回i;否则,匹配失败,函数返回-1。
-
例如:设目标串s=“aaaaab”,模式串t=“aaab”。
-
BF算法的具体代码:
int index(SqString s,SqString t)
{ int i=0, j=0;
while (i<s.length && j<t.length)
{ if (s.data[i]==t.data[j]) //继续匹配下一个字符
{ i++; //主串和子串依次匹配下一个字符
j++;
}
else //主串、子串指针回溯重新开始下一次匹配
{ i=i-j+1; //主串从下一个位置开始匹配
j=0; //子串从头开始匹配
}
}
if (j>=t.length)
return(i-t.length); //返回匹配的第一个字符的下标
else
return(-1); //模式匹配不成功
}
-
二.KMP算法(特点:速度快):
-
KMP算法相对于BF算法的改进之处:
1.主串不需回溯i指针
2.将模式串向右“滑动”尽可能远的一段距离。 -
KMP算法用next数组保存部分匹配信息:
用next[j]来表示失配时下一次模式串匹配的位置t[next[j]];
下一次匹配时目标串的匹配位置为失配的位置。 -
如何寻找next[j]?
模式串t存在某个k(0<k<j),使得以下成立:
“t0t1…tk -1” = “ tj-ktj-k+1…tj-1 ”
开头的k个字符 t[j]前面的k个字符
next[j]=k; -
next[j]的规则:
-
例子:
t | a | a | a | b |
---|---|---|---|---|
j | 0 | 1 | 2 | 3 |
next[j] | -1 | 0 | 1 | 2 |
-
若模式串很长,无法肉眼识别?
1.已知next[j]=k,tj=tk,
t0t1….......tk-1tk=tj-ktj-k+1…........tj-1tj
则next[j+1]=k+1
2.已知next[j]=k,tj≠tk。
t0t1….......tk-1tk≠tj-ktj-k+1…........tj-1tj
则k=next[k] -
KMP算法的具体代码:
int KMPIndex(SqString s,SqString t)
{ int next[MaxSize],i=0,j=0;
GetNext(t,next);
while (i<s.length && j<t.length)
{ if (j==-1 || s.data[i]==t.data[j])
{ i++;
j++; //i,j各增1
}
else j=next[j]; //i不变,j后退
}
if (j>=t.length)
return(i-t.length); //匹配模式串首字符下标
else
return(-1); //返回不匹配标志
}
-
KMP算法的优点:不需回溯。对于从外设读入的庞大文件很有效可以边读入边匹配,无须回头重读。
-
next数组改进:nextval数组
nextval数组的取值规则
将next[j]修正为nextval[j]。
t.data[j]?=t.data[next[j])
1.若不等,则 nextval[j]=next[j];
2.若相等nextval[j]=nextval[k]。
例子:
- 由模式串t求出nextval值:
void GetNextval(SqString t,int nextval[])
{ int j=0,k=-1;
nextval[0]=-1;
while (j<t.length)
{ if (k==-1 || t.data[j]==t.data[k])
{ j++;k++;
if (t.data[j]!=t.data[k])
nextval[j]=k;
else
nextval[j]=nextval[k];
}
else
k=nextval[k];
}
}
- 修改后的KMP算法:
int KMPIndex1(SqString s,SqString t)
{ int nextval[MaxSize],i=0,j=0;
GetNextval(t,nextval);
while (i<s.length && j<t.length)
{ if (j==-1 || s.data[i]==t.data[j])
{ i++;
j++;
}
else
j=nextval[j];
}
if (j>=t.length)
return(i-t.length);
else
return(-1);
}
二.树:
-
树的定义:
树型结构为非线性结构。
现实世界中描述层次结构的关系:
操作系统的文件系统、Internet中的DNS(域名系统)、人类的族谱等
计算机领域中:编译器语法结构、数据库系统、分析算法:人工智能、数据挖掘算法。
-
树的基本术语:
根——即根结点(没有前驱)
叶子——即终端结点(没有后继)
森林——指m棵不相交的树的集合(例如删除A后的子树个数)
有序树——结点各子树从左至右有序,不能互换(左为第一)
无序树——结点各子树可互换位置。
双亲——即上层的那个结点(直接前驱)
孩子——即下层结点的子树的根(直接后继)
兄弟——同一双亲下的同层结点(孩子之间互称兄弟)
祖先——即从根到该结点所经分支的所有结点
子孙——即该结点下层子树中的任一结点
结点——即树的数据元素
结点的度——结点挂接的子树数,分支数目
结点的层次——从根到该结点的层数(根结点算第一层)
终端结点——即度为0的结点,即叶子
分支结点——即度不为0的结点(也称为内部结点)
树的深度(或高度)——指所有结点中最大的层数
-
树的性质:
性质1 树中的结点数等于所有结点的度数之和加1。
性质2 度为m的树中第i层上至多有m的i-1次方个结点(i≥1)。
-
树的存储结构:
1、双亲存储结构:
- 结构体定义:
typedef struct
{ ElemType data; //结点的值
int parent; //指向双亲的位置
} PTree[MaxSize];
- 缺点:
找父亲容易,找孩子不容易
2、孩子链存储结构:
- 结构体定义:
typedef struct node
{ ElemType data; //结点的值
struct node *sons[MaxSons]; //指向孩子结点
} TSonNode;
- 缺点:
1.空指针太多
2.找父亲不容易
3、孩子兄弟链存储结构:
孩子兄弟链存储结构为每个结点设计3个域:一个数据元素域,第一个孩子结点指针域,一个兄弟结点指针域。
- 节点类型:
typedef struct tnode
{ ElemType data; //结点的值
struct tnode *son; //指向孩子结点
struct tnode *brother; //指向兄弟
} TSBNode;
-
缺点:
每个结点固定只有两个指针域,类似二叉树
找父亲不容易。 -
在一棵树T中最常用的操作是查找某个结点的祖先结点,采用哪种存储结构最合适?
双亲存储结构。
-
如最常用的操作是查找某个结点的所有兄弟,采用哪种存储结构最合适?
孩子链存储结构或者孩子兄弟链存储结构。
-
树的遍历:
定义:树的遍历运算是指按某种方式访问树中的每一个结点且每一个结点只被访问一次。
- 主要的遍历方法:
先根遍历:
若树不空,则先访问根结点,然后依次先根遍历各棵子树。
后根遍历:
若树不空,则先依次后根遍历各棵子树,然后访问根结点。
层次遍历:
若树不空,则自上而下、自左至右访问树中每个结点。
注:先根和后根遍历算法都是递归的。
-
森林:
n(n>0)个互不相交的树的集合称为森林。只要把树的根结点删去就成了森林。
反之,只要给n棵独立的树加上一个结点,并把这n棵树作为该结点的子树,则森林就变成了一颗树。
-
一、二叉树:
-
二叉树的定义:
是n(n>=0)个结点的有限集合,它或为空树(n=0),或由一个根结点和至多两棵称为根的左子树和右子树的互不相交的二叉树组成。
-
二叉树中不存在度大于2的结点,并且二叉树的子树有左子树和右子树之分。
-
二叉树的五种基本形态:
-
两种特殊的二叉树:
-
满二叉树:
定义:在一棵二叉树中,如果所有分支结点都有双分结点,并且叶结点都集中在二叉树的最下一层。则此树为满二叉树。
满二叉树有2的0次方+2的1次方+2的平方+...+2的h-1次方=2的h次方-1 个结点。
-
完全二叉树:
定义:深度为k 的,有n个结点的二叉树,当且仅当其每一个结点都与深度为k 的满二叉树中编号从1至n的结点一一对应,没有单独右分支结点。
完全二叉树实际上是对应的满二叉树删除叶结点层最右边若干个结点得到的。
-
完全二叉树性质(含n为结点):
1.n1=0或者n1=1。n1可由n的奇偶性确定:
2.如果有度为1节点,只可能有一个,且该结点只有左孩子无右孩子。
3.除树根结点外,若一个结点的编号为i,则它的双亲结点的编号为i/2。
4.若编号为i的结点有左孩子结点,则左孩子结点的编号为2i;若编号为i的结点有右孩子结点,则右孩子结点的编号为2i+1。
若i≤n/2,则编号为i的结点为分支结点,否则为叶结点。
n=3 ->n/2=1,编号为1的是分支结点;编号为2、3的是叶结点
n=4 ->n/2=2,编号为1、2的是分支结点;编号为3、4的是叶结点
-
满二叉树和完全二叉树的区别:
满二叉树是叶子一个也不少的树,而完全二叉树虽然前n-1层是满的,但最底层却允许在右边缺少连续若干个结点。满二叉树是完全二叉树的一个特例。 -
二叉树的性质:
性质1 :非空二叉树上叶节点数等于双分支节点数加1。
n=n0+n1+n2
n=总分支数+1
总的分支数=n1+2n2
n=n1+2n2+1=n0+n1+n2
n0=n2+1
性质2:在二叉树的第 i 层上至多有 2的i-1次方 个结点(i≥1)。
性质3 高度为h的二叉树至多有2的h-1次方个节点(h≥1)。
深度为 k 的二叉树上的结点数至多为:
2的0次方+2的1次方+····+2的k-1次方 = 2的k次方-1 。
性质4: 具有n个结点的完全二叉树的深度必为 log2n的下限 + 1
-
二叉树的存储结构:
-
顺序存储(对结点编号用数组存储):
- 完全二叉树的顺序存储结构:
顺序存储结构(不用下标为0的元素)
结构体定义:
typedef ElemType SqBTree[MaxSize];
SqBTree bt="ABCDEF#####";
- 非完全二叉树的顺序存储结构:
结构体定义:
typedef ElemType SqBTree[MaxSize];
SqBTree bt="#ABD#C#E######F";
-
二叉树顺序存储结构缺点:
1.对于完全二叉树来说,其顺序存储是十分合适
2.在最坏的情况下,一个深度为k且只有k个结点的单支树(树中不存在度为2的结点)却需要2k-1的一维数组。空间利用率太低!
3.数组,查找、插入删除不方便。 -
链式存储:
-
节点类型定义:
typedef struct node
{ ElemType data;
struct node *lchild, *rchild;
} BTNode;
typedef BTNode *BTree;
- 链式存储结构体定义:
typedef struct TNode *Position;
typedef Position BinTree; /* 二叉树类型 */
struct TNode{ /* 树结点定义 */
ElementType Data; /* 结点数据 */
BinTree Left; /* 指向左子树 */
BinTree Right; /* 指向右子树 */
};
- 链式存储演示:
在二叉链表中:
n个结点,指针域个数为2n个;
分支数为n-1个;
空指针域为n+1个。
-
二叉树的基本运算:
void CreateBTree(BTree &bt,string str);//创建二叉树
void PreOrder( BTree bt );//先序遍历二叉树
void InOrder(BTree bt);//中序遍历二叉树
void PostOrder(BTree bt);//后序遍历二叉树
void LevelOrder(BTree bt);//层次遍历二叉树
-
销毁二叉树:
void DestroyBTree(BTree bt)
{
if (bt != NULL)
{
DestroyBTree(bt->lchild);
DestroyBTree(bt->rchild);
delete bt;
}
- 二叉树创建方法:二叉树的顺序存储结构转成二叉链
void CreateBTree2(BTree &bt,string str, int i)
{ int len;
//BTree bt;
len = str.size();
bt = new TNode;
if (i > len || i < 1)
{
bt=NULL;return;
}
if (str[i] == '#')
{
bt=NULL; return;
}
bt->data = str[i];
CreateBTree2(bt->lchild,str, 2 * i);
CreateBTree2(bt->rchild,str, 2 * i + 1);
}
- 先序遍历递归建树:
BTree CreatTree(string str, int &i)
{
BTree bt;
if (i > len - 1) return NULL;
if (str[i] == '#') return NULL;
bt = new BTNode;
bt->data = str[i];
bt->lchild = CreatTree(str, ++i);
bt->rchild = CreatTree(str, ++i);
return bt;
}
- 层次法建树 :
思路:
取字符串第一个数据,创建根结点,入队列
while(队列不空)
{ 出队一个节点T
取一个字符str[i]
if(str[i]=='0') T->lchild=NULL
else 生成T的左孩子,值str[i],并把左孩子入队列。
取一个字符str[i]
if(str[i]=='0') T->rchild=NULL
else 生成T的右孩子,值str[i],并把右孩子入队列。
}
具体代码:
void CreateBTree(BTree& BT, string str)
{
BTree T; int i = 0;
queue<BTree> Q;//队列
if (str[0] != '0') { /*分配根结点单元,并将结点地址入队*/
BT = new BTNode;
BT->data = str[0];
BT->lchild = BT->rchild = NULL;
Q.push(BT);
}
else BT = NULL; /* 若第1个数据就是0,返回空树 */
while (!Q.empty())
{
T = Q.front();/*从队列中取出一结点地址*/
Q.pop();
i++;
if (str[i] == '0') T->lchild = NULL;
else
{ /*生成左孩子结点;新结点入队*/
T->lchild = new BTNode;
T->lchild->data = str[i];
T->lchild->lchild = T->lchild->rchild = NULL;
Q.push(T->lchild);
}
i++; /* 读入T的右孩子 */
if (str[i] == '0') T->rchild = NULL;
else
{ /*生成右孩子结点;新结点入队*/
T->rchild = new BTNode;;
T->rchild->data = str[i];
T->rchild->lchild = T->rchild->rchild = NULL;
Q.push(T->rchild);
}
} /* 结束while */
}
- 括号法字符串构造二叉树:
具体代码:
void CreateBTNode(BTNode*& b,char* str)
{ //由str 二叉链b
BTNode* St[MaxSize],* p;
int top = -1, k , j = 0;
char ch;
b = NULL; //建立的二叉链初始时为空
ch = str[j];
while (ch != '\0') //str未扫描完时循环
{
switch (ch)
{
case '(': top++; St[top] = p; k = 1; break; //可能有左孩子结点,进栈
case ')': top--; break;
case ',': k = 2; break; //后面为右孩子结点
default: //遇到结点值
p = (BTNode*)malloc(sizeof(BTNode));
p->data = ch; p->lchild = p->rchild = NULL;
if (b == NULL) //p为二叉树的根结点
b = p;
else //已建立二叉树根结点
{
switch (k)
{
case 1: St[top]->lchild = p; break;
case 2: St[top]->rchild = p; break;
}
}
}
j++; ch = str[j]; //继续扫描str
}
}
-
二叉树的遍历:
- 二叉树遍历定义:
二叉树的遍历是指按照一定次序访问树中所有节点,并且每个节点仅被访问一次的过程。它是最基本的运算,是二叉树中所有其他运算的基础。
先序遍历:ABCDEFGHK
中序遍历:BDCAEHGKF
后序遍历:DCBHKGFEA
-
先序遍历:
-
先序遍历二叉树的过程是:
访问根节点;
先序遍历左子树;
先序遍历右子树.
1.递归调用从根节点开始,最后遍历完所有节点,在根节点结束。
2.结点访问2遍 -
先序遍历的递归算法:
void PreOrder(BTree bt)
{ if (bt!=NULL)
{ printf("%c ",bt->data); //访问根结点
PreOrder(bt->lchild);
PreOrder(bt->rchild);
}
}
- 先序遍历非递归算法:
若二叉树bt不空,则入栈根节点bt。
while(栈不空)
{ 出栈栈顶,访问根节点。
if(bt有右孩子) 入栈bt->rchild。
if(bt有左孩子) 入栈bt->lchild。
}
-
中序遍历:
中序遍历二叉树的过程是:
中序遍历左子树;
访问根节点;
中序遍历右子树。
- 中序遍历的递归算法:
void InOrder(BTree bt)
{ if (bt!=NULL)
{ InOrder(bt->lchild);
printf("%c ",bt->data); //访问根结点
InOrder(bt->rchild);
}
}
- 中序遍历的非递归算法:
if (树不空)
{
入栈根节点bt,p=bt
while (栈不空)
{
while (p有左孩子)
{ p=p->lchild;
将p进栈;
}
if (栈不空)
{ 出栈p并访问之;
p=p->rchild;
}
}
}
-
后序遍历:
-
后序遍历二叉树的过程是:
后序遍历左子树;
后序遍历右子树;
访问根节点。
最后一个结点是根节点 -
后序遍历的递归算法:
void PostOrder(BTree bt)
{ if (bt!=NULL)
{ PostOrder(bt->lchild);
PostOrder(bt->rchild);
printf("%c ",bt->data); //访问根结点
}
}
- 后序遍历的非递归算法:
p=b;
do
{ while (结点p有左孩子)
{ 将结点p进栈;
p=p->lchild;
}
//此时栈顶结点(尚未访问)没有左孩子或左子树已遍历过
while (栈不空且结点p是栈顶结点)
{ 取栈顶结点p;
if (结点p的右子树已访问)
{ 访问结点p;
退栈;
}
else p=p->rchild; //转向处理其右子树
}
} while (栈不空);
-
层次遍历:
在进行层次遍历时,对某一层的节点访问完后,再按照它们的访问次序对各个节点的左、右孩子顺序访问。
- 层次遍历二叉树的过程是:
初始化队列,先将根节点进队。
while(队列不空)
{ 队列中出列一个节点*p,访问它;
若它有左孩子节点,将左孩子节点进队;
若它有右孩子,将右孩子进队。
}
可以用队列存储,当找到一个叶子结点时,在队列中通过双亲结点的位置输出根结点到该叶子结点的逆路径。
typedef struct node
{ ElemType data;
struct node *lchild, *rchild;
int parent;
} BTNode;
typedef BTNode *BTree;
typedef struct
{ BTNode * data[MaxSize]; //存放队列元素
int front,rear; //队头指针和队尾指针
} QuType; //顺序队类型
- 层次遍历求逆路径具体代码:
void AllPath2(BTree b)
{
int k;
BTree p;
QuType* qu; //定义非非环形队列指针
InitQueue(qu); //初始化队列
b->parent = -1; //创建根结点对应的队列元素
enQueue(qu,b); //根结点进队
while (!QueueEmpty(qu)) //队不空循环
{
deQueue(qu,p); //出队
if (p->lchild == NULL && p->rchild == NULL)
{
k = qu->front; //输出结点p到根结点的路径逆序列
while (qu->data[k]->parent != -1)
{
printf("%c->",qu->data[k]->data);
k = qu->data[k]->parent;
}
printf("%c\n",qu->data[k]->data);
}
if (p->lchild != NULL) //结点p有左孩子
{
p->lchild->parent = qu->front; //其双亲位置为qu->front
enQueue(qu,p->lchild); //结点p的左孩子进队
}
if (p->rchild != NULL) //结点p有右孩子
{
p->rchild->parent = qu->front;
enQueue(qu,p->rchild); //结点p的右孩子进队
}
}
}
-
递归小结:
1.递归形参设计参数,可以保存递归每一层值,但是无法返回主调函数。
2.设计递归函数返回值,可以得到每层递归值,同时利用该值设计递归提前结束条件。
3.务必记住,递归是一级级调用,又务必一级级返回!!! -
二叉树的应用:
1.设计一个算法Level()求二叉树b中值为x的结点的层次
int GetLevel2(BTree bt, char x, int h)
{
int l=0;
if (bt == NULL) return 0;
if (bt->data== x)
{
return h;
}
l=GetLevel2(bt->lchild, x, h+1);
if (l == 0) //找不到继续遍历右子树
{
l=GetLevel2(bt->rchild, x, h);
}
else return l;//尾递归。找到可以返回,不继续遍历
}
2.二叉树采用二叉链存储结构,设计算法输出值为x节点所有祖先。
函数类型为bool,利用函数返回值确定是否找到节点。利用函数调用返回值为true选择祖先节点。
bool ancestor(BTree bt, char x)
{
if (bt == NULL) return false;
else if (bt->lchild != NULL && bt->lchild->data == x ||
bt->rchild != NULL && bt->rchild->data == x)
{
cout << bt->data << " "; //找到孩子为x结点
return true;
}
else if (ancestor(bt->lchild, x) || ancestor(bt->rchild, x))
{
cout << bt->data << " ";//输出祖先
return true;
}
else return false;
}
3.假设二叉树采用二叉链存储结构,设计一个算法求二叉树b中第k层的结点个数。
思路:设计算法为Lnodenum(b,h,k,&n),h表示b所指的结点层次,n是引用型参数,用于保存第k层的结点个数。初始调用时,b为根结点指针,h为1,n赋值为0,
即调用方式是:n=0;Lnodenum(b,1,k,n)。
先序遍历:
void Lnodenum(BTNode *b,int h,int k,int &n)
{ if (b==NULL) //空树直接返回
return;
else //处理非空树
{ if (h==k) n++; //当前访问的结点在第k层时,n增1
else if (h<k) //若当前结点层次小于k,递归处理左、右子树
{ Lnodenum(b->lchild,h+1,k,n);
Lnodenum(b->rchild,h+1,k,n);
}
}
}
4.判断两棵二叉树是否相似
bool Like(BTNode *b1,BTNode *b2)
//b1和b2两棵二叉树相似时返回true,否则返回false
{ bool like1,like2;
if (b1==NULL && b2==NULL)
return true;
else if (b1==NULL || b2==NULL)
return false;
else
{ like1=Like(b1->lchild,b2->lchild);
like2=Like(b1->rchild,b2->rchild);
return (like1 && like2); //返回like1和like2的与运算结果
}
}
5.用二叉树表示算术表达式
6.中缀表达式转二叉表达式树,
1.树中叶子结点均为操作数,分支结点均为运算符。
2.运算符栈
3.存放树根栈
while(遍历表达式)
{
若为操作数,生成树结点,入树根栈
若为运算符:
若优先级>栈顶运算符,则入运算符栈
若小于,出栈,树根栈弹出2个结点建树,新生成树根入树根栈
若相等,则出栈栈顶运算符
}
-
二叉树与树、森林之间的转换
1、森林、树转换为二叉树:
做法:1.相邻兄弟节点加一水平连线
2.除了左孩子和叶子节点,删掉连线
3.水平连线以左边节点轴心旋转45度
-
一颗树转换为二叉树:
-
多颗树转换为一颗二叉树:
2、二叉树还原为一颗树:
做法:
1.任一节点k,搜索所有右孩子
2.删掉右孩子连线
3.若节点k父亲为k0,则k0和所有k右孩子连线
3、二叉树还原为多颗树
-
二叉树的构造:
同一棵二叉树(假设每个结点值唯一)具有唯一先序序列、中序序列和后序序列。但不同的二叉树可能具有相同的先序序列、中序序列或后序序列。(序列中不包括空节点)
仅由先序、中序或后序序列中的一种,无法唯一构造出该二叉树。
-
二叉树几种构造方法:
顺序存储结构转二叉链
先序遍历建二叉树
层次遍历法建二叉树--队列
括号法建二叉树--栈
先序+中序序列造二叉树
中序+后序序列造二叉树 -
先序和中序序列构造二叉树:
例子:
具体代码:
BTRee CreateBT1(char* pre, char* in, int n)
{
BTNode* s; char* p; int k;
if (n <= 0) return NULL;
s = new BTNode;
s->data = *pre; //创建根节点
for (p = in; p < in + n; p++) //在中序中找为*ppos的位置k
if (*p == *pre)
break;
k = p - in;
s->lchild = CreateBT1(pre + 1, in, k);//构造左子树
s->rchild = CreateBT1(pre + k + 1, p + 1, n - k - 1);//右子树
return s;
}
- 后序和中序序列构造二叉树:
例子:
具体代码:
BTRee CreateBT2(char *post,char *in,int n)
{ BTNode *s; char *p; int k;
if (n<=0) return NULL;
s=new BTNode;//创建节点
s->data=*(post+n-1); //构造根节点。
for (p=in;p<in+n;p++)//在中序中找为*ppos的位置k
if (*p==*(post+n-1))
break;
k=p-in;
s->lchild=CreateBT2(post,in,k); //构造左子树
s->rchild=CreateBT2(post+k,p+1,n-k-1);//构造右子树
return s;
}
-
线索二叉树:
-
定义:
二叉链存储结构时,每个节点有两个指针域,总共有2n个指针域。
有效指针域:n-1(根节点没指针指向)
空指针域:n+1
利用这些空链域,指向该线性序列中的“前驱”和“后继”的指针,称作线索。 -
线索二叉树性质:
1)若结点有左子树,则lchild指向其左孩子;否则, lchild指向其直接前驱(即线索);
2)若结点有右子树,则rchild指向其右孩子;否则, rchild指向其直接后继(即线索) 。
为了表示有无左右孩子,增加两个标志域:
左孩子域 | 左孩子标志域 | 节点数据 | 右孩子标志域 | 右孩子域 |
---|---|---|---|---|
lchild | LTag | data | RTag | rchild |
解释:
LTag :若 LTag=0, lchild域指向左孩子; 若 LTag=1, lchild域指向其前驱。
RTag :若 RTag=0, rchild域指向右孩子; 若 RTag=1, rchild域指向其后继。
- 线索树结点类型定义:
typedef struct node
{ ElemType data; //结点数据域
int ltag,rtag; //增加的线索标记
struct node *lchild; //左孩子或线索指针
struct node *rchild; //右孩子或线索指针
} TBTNode; //线索树结点类型定义
- 线索化二叉树方法:
1.先序线索二叉树:
例子:
2.中序线索二叉树:
例子:
3.后序线索二叉树:
例子:
- 遍历线索化二叉树:
中序线索二叉树可以找到对应树每个节点的前驱和后继节点。先序和后序线索二叉树无法做到。
优点:中序遍历算法既没有递归也没有用栈,所有节点只需遍历一次,空间效率得到提高。
结点的后继:(前继同理)
结点有右孩子,则为右子树最左孩子节点
结点无右孩子,则为后继线索指针指向节点
遍历线索化二叉树步骤:
1.找中序遍历的第一个结点(左子树上处于“最左下”(没有左子树)的结点)。
2.找中序线索化链表中结点的后继:
若无右子树,则为后继线索所指结点;否则为对其右子树进行中序遍历时访问的第一个结点。
具体代码:
void ThInOrder(TBTNode *tb)
{ TBTNode *p=tb->lchild; //p指向根结点
while (p!=tb)//tb头结点
{
while (p->rtag==0) p=p->lchild; //找开始结点
printf("%c",p->data); //访问开始结点
while (p->rtag==1 && p->rchild!=tb)
{ p=p->rchild;
printf("%c",p->data);
}
p=p->rchild;
}
}
- 中序线索二叉树创建:
具体代码:
TBTNode* pre; //全局变量
TBTNode* CreatThread(TBTNode* b) //中序线索化二叉树
{
TBTNode* root;
root = (TBTNode*)malloc(sizeof(TBTNode)); //创建头结点
root->ltag = 0; root->rtag = 1; root->rchild = b;
if (b == NULL) root->lchild = root; //空二叉树
else
{
root->lchild = b;
pre = root; //pre是*p的前驱结点,供加线索用
Thread(b); //中序遍历线索化二叉树
pre->rchild = root; //最后处理,加入指向头结点的线索
pre->rtag = 1;
root->rchild = pre; //头结点右线索化
}
return root;
}
void Thread(TBTNode*& p) //对二叉树b进行中序线索化
{
if (p != NULL)
{
Thread(p->lchild); //左子树线索化
if (p->lchild == NULL) //前驱线索化
{
p->lchild = pre; p->ltag = 1;
} //建立当前结点的前驱线索
else p->ltag = 0;
if (pre->rchild == NULL) //后继线索化
{
pre->rchild = p; pre->rtag = 1;
} //建立前驱结点的后继线索
else pre->rtag = 0;
pre = p;
Thread(p->rchild); //递归调用右子树线索化
}
}
注:CreatThread(b)算法:对以二叉链存储的二叉树b进行中序线索化,并返回线索化后头结点的指针root。
Thread(p)算法:对以*p为根结点的二叉树子树的中序线索化。
-
哈曼夫树:
-
哈曼夫树的定义:设二叉树具有n个带权值的叶子节点,那么从根节点到各个叶子节点的路径长度与相应节点权值的乘积的和,叫做二叉树的带权路径长度(WPL)。具有最小带权路径长度的二叉树称为哈夫曼树(最优树)。
-
构造哈夫曼树的原则:
权值越大的叶结点越靠近根结点。
权值越小的叶结点越远离根结点。
-
构造哈夫曼树的过程:
(1)根据给定的n个权值{w1,w2,……wn},构造n棵只有根结点的二叉树。F={T1,T2,…,Tn}。
(2)在F中选取根结点的权值最小和次小的两棵二叉树作为左、右子树构造一棵新的二叉树,这棵新的二叉树根结点的权值为其左、右子树根结点权值之和。
(3)在集合F中删除作为左、右子树的两棵二叉树,并将新建立的二叉树加入到集合F中。
(4)重复(2)、(3)两步,当F中只剩下一棵二叉树时,这棵二叉树便是所要建立的哈夫曼树。
-
哈夫曼树的特点:
已知n0个叶子节点,求哈夫曼树总节点数n。
1.没有单分支节点,n1 = 0
因为每次两棵树合并。
2.n和n0关系?
n = n0+n1+n2 = n0+n2 = 2n0-1。
3.关于WPL
WPL=叶子结点的weight*h的和
WPL=分支节点权重值和
-
哈曼夫树结构体定义:
顺序结构:
typedef struct
{ char data; //节点值
float weight; //权重
int parent; //双亲节点
int lchild; //左孩子节点
int rchild; //右孩子节点
} HTNode;
-
初始化哈曼夫树:
typedef struct
{
int data;
int parent;
int lchild;
int rchild;
}HTNode,*HuffmanTree;
void CreateHTree(HuffmanTree &ht, int n)
{
int len;
len = 2 * n - 1;
ht = new HTNode[len];
}
-
用ht[]数组构造哈夫曼树:
算法思路:
1.初始化哈夫曼数组ht,包含n个叶子结点,2n-1个总节点
* 所有2n-1个节点的parent、lchild和rchild域置为初值-1。
* 输入n个叶子节点有data和weight域值
2.构造非叶子节点ht[i](存放在ht[n]~ht[2n-2]中)
* 从ht[0] ~ht[i-1]中找出根节点(即其parent域为-1)最小的两个节点ht[lnode]和ht[rnode]
* ht[lnode]和ht[rnode]的双亲节点置为ht[i],并且ht[i].weight= ht[lnode].weight+ht[rnode].weight。
3.如此这样直到所有2n-1个非叶子节点处理完毕。
具体代码:
void CreateHT(HTNode ht[],int n)
{ int i,j,k,lnode,rnode; float min1,min2;
//此处补充叶子节点相关设置
for (i=0;i<2*n-1;i++) //所有节点的相关域置初值-1
ht[i].parent=ht[i].lchild=ht[i].rchild=-1;
for (i=n;i<2*n-1;i++) //构造哈夫曼树
{ min1=min2=32767; lnode=rnode=-1;
for (k=0;k<=i-1;k++)
if (ht[k].parent==-1) //未构造二叉树的节点中查找
{ if (ht[k].weight<min1)
{ min2=min1;rnode=lnode;
min1=ht[k].weight;lnode=k; }
else if (ht[k].weight<min2)
{ min2=ht[k].weight;rnode=k; }
} //if
ht[lnode].parent=i;ht[rnode].parent=i;
ht[i].weight=ht[lnode].weight+ht[rnode].weight;
ht[i].lchild=lnode;ht[i].rchild=rnode;
}
}
-
改进哈曼夫树(用堆区建):
-
建堆实现哈夫曼树:
n个叶子结点建一个小根堆。(顺序结构)
利用堆每次调整,找2个最小。
利用树结构实现找最小值,时间复杂度O(nlogn) -
对同一组权值{w1 ,w2 , …… , wn}:哈夫曼树不唯一,但是WPL唯一。
-
哈夫曼编码:
在远程通讯中,要将待传字符转换成由二进制的字符串。
编码目标:总编码长度最短.
让待传字符串中出现次数较多的字符采用尽可能短的编码,则转换的二进制字符串便可以减少。
-
关键:要设计长度不等的编码,则必须使任一字符的编码都不是另一个字符的编码的前缀。符合这种编码要求的编码方式称为前缀编码。
-
构造哈夫曼树来实现前缀编码:
哈夫曼编码:根节点到叶子节点经过路径组成的0,1序列.
规定:左分支用“0”表示;右分支用“1”表示。前缀编码
结构体类型定义:
typedef struct
{ char cd[N]; //存放当前节点的哈夫曼码
int start; //哈夫曼码在cd中的起始位置
} HCode;
哈夫曼编码具体代码:
void CreateHCode(HTNode ht[],HCode hcd[],int n)
{ int i,f,c; HCode hc;
for (i=0;i<n;i++) //根据哈夫曼树求哈夫曼编码
{ hc.start=n;c=i; f=ht[i].parent;
while (f!=-1) //循环直到无双亲节点即到达树根节点
{ if (ht[f].lchild==c) //当前节点是左孩子节点
hc.cd[hc.start--]='0';
else //当前节点是双亲节点的右孩子节点
hc.cd[hc.start--]='1';
c=f;f=ht[f].parent; //再对双亲节点进行同样的操作
}
hc.start++; //start指向哈夫曼编码最开始字符
hcd[i]=hc;
}
}
-
并查集:
:用来查找一个元素所属的集合及合并2个元素各自专属的集合等运算。
-
应用:亲戚关系、朋友圈应用。
在并查集中,每个分离集合对应的一棵树,称为分离集合树。整个并查集也就是一棵分离集合森林。 -
集合查找:在一棵高度较低的树中查找根结点的编号,所花的时间较少。同一个集合标志就是根(parent)是一样。
-
集合合并:两棵分离集合树A和B,高度分别为hA和hB,则若hA>hB,应将B树作为A树的子树;否则,将A树作为B树的子树。
总之,总是高度较小的分离集合树作为子树。得到的新的分离集合树C的高度hC =MAX{hA,hB+1}。 -
并查集支持以下三种操作:
Union (Root1, Root2) //合并操作
Find (x) //查找操作
UFSets (s) //构造函数
对于并查集来说,每个集合用一棵树表示。 -
并查集顺序存储结构体定义:
typedef struct node
{ int data; //结点对应人的编号
int rank; //结点秩:子树的高度,合并用
int parent; //结点对应双亲下标
} UFSTree; //并查集树的结点类型
- 初始化并查集树:
void MAKE_SET(UFSTree t[],int n) //初始化并查集树
{ int i;
for (i=1;i<=n;i++)
{ t[i].data=i; //数据为该人的编号
t[i].rank=0; //秩初始化为0
t[i].parent=i; //双亲初始化指向自已
}
}
- 查找一个元素所属的集合
int FIND_SET(UFSTree t[],int x) //在x所在子树中查找集合编号
{ if (x!=t[x].parent) //双亲不是自已
return(FIND_SET(t,t[x].parent)); //递归在双亲中找x
else
return(x); //双亲是自已,返回x
}
- 两个元素各自所属的集合的合并
void UNION(UFSTree t[],int x,int y) //将x和y所在的子树合并
{ x=FIND_SET(t,x); //查找x所在分离集合树的编号
y=FIND_SET(t,y); //查找y所在分离集合树的编号
if (t[x].rank>t[y].rank) //y结点的秩小于x结点的秩
t[y].parent=x; //将y连到x结点上,x作为y的双亲结点
else //y结点的秩大于等于x结点的秩
{ t[x].parent=y; //将x连到y结点上,y作为x的双亲结点
if (t[x].rank==t[y].rank) //x和y结点的秩相同
t[y].rank++; //y结点的秩增1
}
}
对于n个人,合并算法的时间复杂度=查找时间复杂度
O(log2n)。树最高高度不会超过O(log2n)
1.2.谈谈你对树的认识及学习体会。
树是图的一种特殊形式,在人们的生活中运用广泛,比如文件查找和家谱关系。树是由根结点和若干颗子树构成的。
树是由一个集合以及在该集合上定义的一种关系构成的。集合中的元素称为树的结点,所定义的关系称为父子关系。
经过一段时间的学习,我发现树可以解决的问题还有很多,可以使用到日常的一些活动中,结构也非常有趣,就是理解起来有点困难。
2.阅读代码
2.1 题目及解题代码(来自“力扣”):
- 题目:
- 解题代码:
/**
* Definition for a binary tree node.
* struct TreeNode {
* int val;
* TreeNode *left;
* TreeNode *right;
* TreeNode(int x) : val(x), left(NULL), right(NULL) {}
* };
*/
class Solution {
public:
double sum = 0;
int sumOfLeftLeaves(TreeNode* root) {
dfs(root,2);
return sum;
}
void dfs(TreeNode *p, int flag) {
if (p == NULL)
return;
if (p->left == NULL && p->right == NULL && flag == 0) {
sum += p->val;
return;
}
if (p->left != NULL)
dfs(p->left, 0);
if (p->right != NULL)
dfs(p->right, 1);
}
};
2.1.1 该题的设计思路
时间复杂度:O(n)
空间复杂度:最坏情况O(n)。
2.1.2 该题的伪代码
public:
初始化和sum
int sumOfLeftLeaves(TreeNode* root) {
dfs(root, 2);//从根出发进行遍历
return sum;
}
void dfs(TreeNode* p, int flag) {
if 二叉树为空
return;
if (左右子树为空且flag==0。为左叶子结点) {
sum += p->val;//sum根据节点的值增加
return;
}
if 节点的左子树不为空
dfs(p->left, 0);//递归向它的左孩子节点
if 节点的右子树不为空
dfs(p->right, 1);//递归向节点的右节点
}
};
2.1.3 运行结果
2.1.4分析该题目解题优势及难点。
优势:
巧妙的用flag的不同来代表树的左右结点,
用递归处理树的遍历
难点:
树的左叶子节点的判断。
用递归不同值查找左叶子节点
2.2 题目及解题代码(来源:力扣(LeetCode))
-
题目:
-
解题代码:
int rob(TreeNode root) {
int[] res = dp(root);
return Math.max(res[0], res[1]);
}
/* 返回一个大小为 2 的数组 arr
arr[0] 表示不抢 root 的话,得到的最大钱数
arr[1] 表示抢 root 的话,得到的最大钱数 */
int[] dp(TreeNode root) {
if (root == null)
return new int[]{0, 0};
int[] left = dp(root.left);
int[] right = dp(root.right);
// 抢,下家就不能抢了
int rob = root.val + left[0] + right[0];
// 不抢,下家可抢可不抢,取决于收益大小
int not_rob = Math.max(left[0], left[1])
+ Math.max(right[0], right[1]);
return new int[]{not_rob, rob};
}
2.2.1 该题的设计思路
有两个选择:
不偷当前节点,两个儿子节点都要给出最多的钱。
偷当前节点,则不能偷其两个儿子节点。
其中下标0表示不偷,下标1表示偷。那么对于以上情况能得到最多的钱就应该表示为:
不偷当前节点所能获得的最大收益 = 左儿子所能获得的最大收益 + 右儿子所能获得的最大收益
偷当前节点所能获得的最大收益 = 偷当前节点的钱 + 不偷左儿子所获得的钱 +不偷右儿子所获得的钱
情况一:都不被抢;
情况二:第一间房子被抢最后一间不抢;
情况三:最后一间房子被抢第一间不抢。
时间复杂度:O(N)
空间复杂度:O(N)
2.2.2 该题的伪代码
int rob(TreeNode root) {
计算小偷可以获得的收益(偷or不偷)
return Math.max(res[0], res[1]);//返回小偷可以得到的最大钱数
}
/* 返回一个大小为 2 的数组 arr
arr[0] 表示不抢 root 的话,得到的最大钱数
arr[1] 表示抢 root 的话,得到的最大钱数 */
int[] dp(TreeNode root) {
if 为空二叉树
return new int[] {0, 0};
int[] left = dp(root.left);//计算当前节点左孩子小偷抢或不抢可得的收益并递归遍历
int[] right = dp(root.right);//计算当前节点右孩子小偷抢或不抢可得的收益并递归遍历
// 抢,下家就不能抢了
int rob = 这家抢的金额+不抢左孩子获得的收益+ 抢右孩子获得的收益
// 不抢,下家可抢可不抢,取决于收益大小
int not_rob = 左孩子可以获得的最大收益+ 右孩子可以获得的最大收益
return new int[] {not_rob, rob};
}
2.2.3 运行结果
2.2.4分析该题目解题优势及难点。
优势:
题目相当于树形结构的动态规划,这个做法比较节省空间,空间复杂度只有递归函数堆栈所需的空间,不需要备忘录的额外空间。
难点:
这个题有点像层次遍历,但这些房子不是一排,而是围成了一个圈,还是要用数组来做。做法容易失误,题目说如果两个直接相连的房子在同一天晚上被打劫,房屋将自动报警,如果用层次遍历有的房子不在同一层,不满足要求。
2.3 题目及解题代码(来源:力扣(LeetCode))
-
题目:
-
解题代码:
/**
* Definition for a binary tree node.
* struct TreeNode {
* int val;
* TreeNode *left;
* TreeNode *right;
* TreeNode(int x) : val(x), left(NULL), right(NULL) {}
* };
*/
public class Solution {
public TreeNode constructMaximumBinaryTree(int[] nums) {
return construct(nums, 0, nums.length);
}
public TreeNode construct(int[] nums, int l, int r) {
if (l == r)
return null;
int max_i = max(nums, l, r);
TreeNode root = new TreeNode(nums[max_i]);
root.left = construct(nums, l, max_i);
root.right = construct(nums, max_i + 1, r);
return root;
}
public int max(int[] nums, int l, int r) {
int max_i = l;
for (int i = l; i < r; i++) {
if (nums[max_i] < nums[i])
max_i = i;
}
return max_i;
}
}
2.3.1 该题的设计思路
首先调用 construct(nums, 0, n),其中 nnn 是数组 numsnumsnums 的长度。
在索引范围 (l:r−1)(l:r-1)(l:r−1) 内找到最大值的索引,将 nums[max_i]nums[max_i]nums[max_i] 作为根节点。
调用 construct(nums, l, max_i) 创建根节点的左孩子。递归执行此操作,创建根节点的整个左子树。
类似的,调用 construct(nums, max_i + 1, r) 创建根节点的右子树。
方法 construct(nums, 0, n) 返回最大二叉树的根节点。
时间复杂度:O(n^2)。方法 construct 一共被调用n 次。每次递归寻找根节点时,需要遍历当前索引范围内所有元素找出最大值。一般情况下,每次遍历的复杂度为 O(logn),总复杂度为 O(nlogn)。最坏的情况下,数组 numsnumsnums 有序,总的复杂度为 O(n^2)
空间复杂度:O(n)。递归调用深度为 n。平均情况下,长度为 n 的数组递归调用深度为 O(logn)。
2.3.2 该题的伪代码
public class Solution {
public TreeNode constructMaximumBinaryTree(int[] nums) {
返回头节点
}
public TreeNode construct(int[] nums, int l, int r) {
if 为空树
return null;
int max_i根节点 = 数组中的最大数
初始化二叉树
root.left = construct(nums, l, max_i);//取最大根节点左边的最大节点递归构造当前节点的最大左子树
root.right = construct(nums, max_i + 1, r);//取最大根节点右边的最大节点递归构造当前节点的最大右子树
return root;
}
public int max(int[] nums, int l, int r) {
最大数初始化为左节点
for i = l to i = r-1; i++
{
if 比较当前值和后面的值
选出最大值
}
返回最大值
}
}
2.3.3 运行结果
2.3.4分析该题目解题优势及难点。
优势:
取出数组的最大值,并把数组根据最大值分成左右两个数组,然后进行递归赋值。
遍历一次数组并随时将新的节点插入树的相应位置,如果当前遍历的值大于树的根节点,则将当前节点作为新的根节点,将原来的树当作新根节点的左子树,否则的话,将其递归的交给根节点的右子树处理(因为从左向右遍历,新的节点一定在右边)。
还是用递归,分别构建左右子树。然后对时间空间的利用率比较优化。
难点:
这个题做法比较多样,还有另一个单调栈的方法,比这个要麻烦一点。
2.4 题目及解题代码(来源:力扣(LeetCode))
-
题目:
-
解题代码:
class Solution {
public List<Integer> largestValues(TreeNode root) {
if(root == null){
return new LinkedList<Integer>();
}
//创建一个队列,用于存放当前节点相邻的节点
Queue<TreeNode> queue = new LinkedList<>();
//初始化,将根节点放入队列当中
queue.add(root);
LinkedList<Integer> result = new LinkedList();
while(!queue.isEmpty()){
//计算当前节点相邻的节点数量
int size = queue.size();
int min = Integer.MIN_VALUE;
//遍历当前节点相邻节点
for(int i = 0; i<size; i++){
TreeNode temp = queue.poll();
min = Math.max(min,temp.val);
if(temp.left != null){
queue.add(temp.left);
}
if(temp.right != null){
queue.add(temp.right);
}
}
result.add(min);
}
return result;
}
}
2.4.1 该题的设计思路
这道题用层次遍历:
一行一行的取出数据并比较大小。用一个队列保存当前节点的左右孩子,先将根节点入列,然后每pop一个元素出来,就执行两个操作:
访问该节点;若该元素左右孩子不为空,便将其顺序入列
此过程不断进行直至队列为空。
本题只需在以上在便利的过程中找到每层的最大节点即可。
时间复杂度:O(n)
空间复杂度:O(n)
2.4.2 该题的伪代码
class Solution {
public List<Integer> largestValues(TreeNode root) {
if 二叉树为空
初始化二叉树
创建一个队列queue,用于存放当前节点相邻的节点(可能为孩子节点)
//初始化队列,并push根节点
初始化新队列result用来存放每行的最大元素
while 队列不为空 {
//计算当前节点相邻的节点数量
int size = queue.size();
int min = Integer.MIN_VALUE;
//遍历当前节点相邻节点
for i = 0 to i = size-1; i++
{
TreeNode temp = 队头pop元素
min = 当前层次最大元素
if 当前节点左子树不空
左子树入队queue
end if
if 当前节点右子树不空
右子树入队queue
end if
}
每层最大元素入队result
}
return result;
}
}
2.4.3 运行结果
2.4.4分析该题目解题优势及难点。
优势:
运用了广度优先遍历的知识,借助队列进行层次遍历
难点:
广度优先遍历的基本思路为:每次将根节点的相邻节点放入一个容器(队列)中,遍历到目前为止容器中所有节点,
并在遍历的过程中再将每个节点的所有相邻节点放入容器中,重复此过程直到队列为空。
要不断依次比较最大值。