目录
1. 栈
1.1 栈的定义
栈(Stack)是只允许在一端进行插入和删除操作的线性表。首先栈是一种线性表,但限定这种线性表只能在某一端进行插入和删除操作。
栈顶(Top):线性表允许进行插入和删除的那一端。
栈底(Bottom):固定的,线性表不允许进行插入和删除的那一端。
空栈:不含任何元素的空表。
假设某个栈 S=(,
,
,
,
),因为只能从单侧往线性表里存放数据,也就是入栈。所以
为栈底元素,
为栈顶元素。由于栈只能在栈顶进行插入和删除操作,进栈的次序依次为
,
,
,
,
,而出栈的次序为
,
,
,
,
。由此可见,栈的操作特性可以明显地概括为后进先出(Last In First Out,LIFO)。
拓展:n个不同的元素进栈,出栈元素不同排列的个数为
。这个公式称为卡特兰数。
1.2 栈的基本操作
InitStack(&S):初始化一个空栈S。
StackEmpty(S):判断一个栈是否为空,若栈为空则返回true,否则返回false。
Push(&S,x):进栈,若栈S未满,则将x加入使之成为新栈顶。
Pop(&S,&x):出栈,若栈S非空,则弹出栈顶元素,并用x返回。
GetTop(S,&x):读栈顶元素,若栈S非空,则用x返回栈顶元素。
DestroyStack(&S):销毁栈,并释放栈S占用的存储空间。
1.3 栈的顺序存储结构
栈是一种操作受限(操作受限的意思是栈只能从一端进行操作)的线性表,类似于线性表。
1.3.1 顺序栈
采用顺序存储的栈称为顺序栈。它利用一组地址连续的存储单元存放自栈底到栈顶的数据元素,同时附设一个指针(Top)指示当前栈顶元素的位置。
#define Maxsize 50 //定义栈中元素的最大个数
typedef struct
{
Elemtype data[Maxsize]; //存放栈中元素
int top; //栈顶指针
}SqStack;
栈顶指针:S.top,初始时设置S.top=-1;
栈顶元素:S.data[S.top]。
进栈操作:栈不满时,栈顶指针先加1,再送值到栈顶元素。
出栈操作:栈非空时,先取栈顶元素值,再将栈顶指针减1。
栈空条件:S.top==-1;
栈满条件:S.top==MaxSize-1;
栈长:S.top+1;
1.3.2 顺序栈的基本运算
栈操作示意图如下图所示,图a是空栈,图c是A、B、C、D、E共5个元素依次入栈后的结果,图d是C、D、E相继出栈后的,top指针指向新的栈顶。
//初始化栈
void InitStack(SqStack &S)
{
S.top=-1; //初始化栈顶指针
}
//判栈空
bool StackEmpty(SqStack S)
{
if(S.top==-1) //栈空
return true;
else //不空
return fasle;
}
//进栈
bool Push(SqStack &S,ElemType x)
{
if(S.top==MaxSize-1) //栈满,报错 ,因为栈使用静态数组ElemType data[MaxSize]来定义的,静态数据在使用的时候就必须规定其最大的上限
return false;
S.data[++S.top]=x; //前置++ 指针先加1 然后把所要进栈的数据x放到指针+1后的数据位上
return true;
}
S.data[++S.top]=x;等价于
S.top=S.top+1; //栈顶指针+1 ,使得栈顶指针top=0,
S.Data[S.top]=x; //把数据x放到栈顶指针top所指的数据域空间上。
//出栈
bool Pop(SqStack &S,ElemType &x)
{
if(S.top==-1) //栈空,报错
return false;
x=S.data[S.top--]; //先出栈,然后指针再后置-- 也就是指针减一
return true;
}
x=S.data[S.top--]等价于
x=S.data[S.top]; //栈顶指针指的数据先出栈
S.top=S.top-1; //数据出栈后,栈顶指针--;
//读栈顶元素
bool GetTop(SqStack S,ElemType &x)
{
if(S.top==-1) //栈空,报错
return false;
x=S.data[S.top]; //x记载栈顶元素
return true;
}
注意:
这里top指向的是栈顶元素,所以进栈操作为S.data[++S.top]=x,出栈操作为x=S.data[S.top--]。
若栈顶指针初始化为S.top=0,即top指向栈顶元素的下一个位置,则入栈操作变为S.data[S.top++]=x;(因为top指向栈顶元素的下一个位置,所以指针的上面应该还有一个位置,这个位置可以用来存放入栈的元素,指针可以后置++,指向新入栈的数据地址即可)
出栈操作变为x=data[--S.top]。(因为top指向栈顶元素的下一个位置,top指针指向的位置上面还有一个位置,所以应该前置--,S.top=-1,指针指向栈顶元素,然后再让栈顶元素出栈,再将出栈元素赋值给x)
相应的栈空、栈满条件也会发生变化。
1.3.3 共享栈
利用栈底位置相对不变的特性,可以让两个顺序栈共享一个一维数组空间,将两个栈的栈底分别设置在共享空间的两端,两个栈顶向共享空间的中间延伸。
当两个栈的栈顶指针都指向栈顶元素时,top0=-1时0号栈为空,top1=MaxSize时1号栈为空;仅当两个栈顶指针相邻(top1-top0=1)时,判断为栈满。
当0号栈进栈时,top0先加1再赋值,1号栈进栈时top1先减1再赋值;出栈时则刚好相反。
共享栈是为了更有效地利用存储空间,两个栈的空间相互协调,只有在整个存储空间被占满时才会发生上溢。其存取数据的时间复杂度均为O(1)。
1.4 栈的链式存储
采用链式存储的栈称为链栈,链栈的优点是便于多个栈共享存储空间和提高其效率,且不存在栈满上溢的情况。通常采用单链表实现,并规定所有操作都是在单链表的表头进行的。这里规定链栈没有头结点,Lhead指向栈顶元素。
typedef struct Linknode
{
ElemType data; //数据域
struct Linknode *next; //指针域
}*LiStack; //栈类型定义
采用链式存储,便于结点的插入和删除。链栈的操作和链表类似,入栈和出栈的操作都在链表的表头进行。
注意:对于带头结点和不带头结点的链栈,具体的实现是有差别的。
1.5 栈相关应用
1. 元素a,b,c,d,e依次进入初始为空的栈中,若元素进栈后可停留、可出栈,直到所有元素都出栈,则在所有可能的出栈序列中,以元素d开头的序列个数是多少?
首先,元素a,b,c,d,e 依次进栈,那么进栈顺序是确定的,而且要求计算以元素d开头的序列个数,那么出栈的首个元素一定是d,也就是说a b c d 进栈之后,d会进行出栈操作。此时存储内存中a b c 的顺序是固定的,所以不管d出栈之后,e是否进栈,a b c 的出栈顺序一定是确定的,也就是c b a。
就是因为在d出栈后,e进不进栈的选择造成了出栈序列的多样性。事实上,出栈序列可以这样认为:d_c_b_a_,e的顺序不定,在任意一个“_”上均有可能。所以以元素d开头的序列个数一共有4个。
2. 设栈的初始状态为空,当字符序列 “n1_” 作为栈的输入时,输出长度为3,且可用做C语言标识符的序列有()个。
这个题目整体来说比较简单,但是注意隐藏的限制条件:C语言标识符。
C语言标识符规定只能以英文字母或下划线开头,而不能是数字开头。所以由 n 1 _ 三个字符组合成的标识符有nl_,n_l,_ln 和 _nl 四种。
第一种:n进栈,n出栈,l进栈,l出栈,_进栈,_出栈。
第二种:n进栈,n出栈,l进栈,_进栈,_出栈,l出栈。
第三种:n进栈,l进栈,_进栈,_出栈,l出栈,n出栈。
第四种:也就是_nl 是不可能实现的。 所以最终可用做C语言标识符的序列有3个。
3. 经过以下栈的操作后,变量x的值为?
InitStack(st);Push(st,a);Push(st,b);Pop(st,x);Top(st,x);
InitStack(st)的意思是初始化栈;
Push(st,a)的意思是元素a进栈;
Push(st,b)的意思是元素b进栈; 此时栈中有元素a b;
Pop(st,x)的意思是栈顶元素出栈; 也就是元素b出栈,此时x表示元素b;
Top(st,x)的意思是获取栈顶元素的值; 当元素b出栈以后,栈顶元素就变为了a,所以此时获取的栈顶元素x就是a;
4. 向一个栈顶指针为top的链栈(不带头结点)中插入一个x结点,则执行 x->next=top;top=x指令
链栈采用不带头结点的单链表表示,进栈操作在首部插入一个结点x(即x->next=top),插入完后需要将top指向该插入的结点x。
5. 有5个元素,其入栈次序为A, B, C, D, E,在各种可能的出栈次序中,第一个出栈元素为C且第二个出栈元素为D的出栈序列有哪几个?
因为入栈次序是A, B, C, D, E,所以出栈次序实际上是确定好的。题目规定第一个出栈元素是C并且第二个出栈元素是D。则意味着C D进出栈的次序是确定好的。即 A B C 进栈,然后 C 出栈;D 进栈,D 出栈。此时出栈的前两个元素就是C D;
此时栈中还有A B元素,接下来面临的选择有E入栈或者B出栈。
所以可能有三种: CDEBA:CD出栈后紧接着E入栈,E出栈,B出栈,A出栈。
CDBEA:CD出栈之后,不着急E入栈,而是B先出栈,然后E入栈,E出栈,A在出栈。
CDBAE:CD出栈之后,考虑E最后入栈,BA先出栈。
6. 若元素的进栈序列为A, B, C, D, E,运用栈操作,能否得到出栈序列B,C,A,E,D和D,B,A,C,E?为什么?
首先看B,C,A,E,D:A B入栈,B出栈,C入栈,C出栈,A出栈,D E入栈,E出栈,D出栈。
D,B,A,C,E:A B C D入栈,D出栈,接下来无论如何也不可能是B出栈,所以D,B,A,C,E的出栈序列是无法通过栈操作得到的。
7. 假设以 I 和 O 分别表示入栈和出栈操作。栈的初态和终态均为空,入栈和出栈的操作序列可表示为仅由 I 和 O 组成的序列,可以操作的序列称为合法序列,否则称为非法序列。
(1)下面所示的序列中哪个是合法的?
A. IOIIOIOO B. IOOIOIIO C. IIIOIOIO D. IIIOOIOO
(2)通过对(1)的分析,写出一个算法,判定所给的操作序列是否合法。若合法,返回true,否则返回false(假定被判定的操作序列已存入一维数组中)。
(1) A D合法,B C是不合法的。首先B起始时入栈一次,因为题目说明了栈的初态和终态均为空,也就是说栈中是没有其他元素的。B入栈一次是不可能连续两次进行出栈的。
C 入栈四次,而出栈操作只进行了一次,显然是不合法的。
(2) 算法思想:事实上,不管是入栈还是出栈
//判断字符数组A中的输入输出序列是否合法。如是,返回true,否则返回fasle int Judge(char A[]) { int i = 0; int j = 0; int k = 0; //i用来表示数组的下标,j和k分别表示输入输出的个数 while (A[i] != '\0')// \0是结束标志,只要进栈的字符不到结束标志,就一直在循环中进行进栈出栈操作 { switch (A[i]) { case 'I': j++; //进栈的情况 j++ break; case 'O': k++; //出栈的情况,k++ if (k > j) //一定要保证出栈的次数小于进栈,否则是非法的 { printf("序列非法\n"); exit(0); } i++; //数组中元素++ } if (j != k) { printf("序列非法\n"); return false; } else { printf("序列合法\n"); return true; } } }
8. 设单链表的表头指针为L,结点结构由data和next两个域构成,其中data域为字符型。试设计算法判断该链表的全部n个字符是否中心对称。例如xyx、xyyx都是中心对称。
算法思想:可以充分的利用栈来判断链表中的数据是否中心对称。让链表的前一半元素依次进栈。这时,在处理链表的后一半元素时,当访问到链表的一个元素后,立刻从已经存入栈中的前一半元素中进行出栈操作,每次出栈一个元素,将该元素和访问到的元素进行对比,如果相同,则将链表的下一个元素与栈中在弹出的元素进行比较,直至链表到尾。这时若栈是空栈,则得出链表中心对称的结论;否则,当链表中的一个元素与栈中弹出的元素不等时,结论就是链表非中心对称,结束算法的执行。
//L是带头结点的n个元素单链表,该算法判断链表是否为中心对称
int dc(LinkList L, int n)
{
int i;
char s[n / 2]; //定义字符栈,长度为单链表的一半
p = L->next; //p是链表的工作指针,指向待处理的当前元素
for (i = 0; i < n / 2; i++) //链表的前一半元素进栈
{
s[i] = p->data; //通过循环将链表的数据依次放到链表的数据域
p = p->next; //处理完上一个数据,指针指向下一个待处理的数据
}
i--; //因为离开上面循环的条件是i=n/2,为了保证是数组中最后一个元素和后一半的首元素进行对比,所以让i--,恢复到最后的i值
if (n % 2 == 1) //表示整个链表的长度是奇数,则必定存在中心结点,也就是xyx中心对称形式,y是肯定无法和其他元素进行对比的
p = p->next; //让工作指针指向下一个元素,跳过这个中心结点
while (p != NULL&&s[i] == p->data)//两个条件进入while循环
//1. 工作指针不能指向空指针,如果指向,就表示该数组的最后一个元素已经比较完了
//2. 让前一半元素的最后一个和后一半元素的首元素进行比较,如果相等,i--;前一半元素进行后退,p=p->next,工作指针依次向后一半元素方向移动
//之所以用p->data是因为上面的for循环,当跳出循环的上一秒,i=n/2-1,s[i] = p->data;也就是把上一半数组的最后一个元素赋值完
//p = p->next该操作会使得 工作指针指向后一半元素的首个元素。所以可以直接拿 s[i] == p->data 进行比较。
{
i--;
p = p->next;
}
if (i == -1) //栈为空栈
{
return 1; //链表中心对称
}
else
return 0; //链表不中心对称
}
2. 队列
2.1 队列的定义
队列(Queue)简称队,也是一种操作受限的线性表,和栈一样 ,也是只允许在表的一端进行插入,在表的另一端进行删除。向队列中插入元素称为入队或进队。从队列中删除元素称为出队或离队。队列和我们日常生活中的排队是一致的,最早排队的也是最早离队的。队列的操作特性是先进先出(First In First Out,FIFO)
队头(Front):允许删除的一端,又称为队首。
队尾(Rear):允许插入的一端。
空队列:不含任何元素的空表。
2.2 队列的基本操作
InitQueue(&Q):初始化队列,构造一个空队列Q。
QueueEmpty(Q):判队列空,若队列为空返回true,否则返回false。
EnQueue(&Q,x):入队,若队列Q未满,将x加入,使之成为新的队尾。
DeQueue(&Q,&x):出队,若队列Q非空,删除对头元素,并用x返回。
GetHead(Q,&x):读对头元素,若队列Q非空,则将队头元素赋值给x。
注意:栈和队列是操作受限的线性表,并不是任何对线性表的基本操作都可以在栈和队列上进行的。例如,不能随意的读取栈和队列的中间元素。
2.3 队列的顺序存储
队列的顺序实现是指分配一块连续的存储单元存放队列中的元素,并且附设两个指针:队头指针front指向队头元素,队尾指针rear指向队尾元素的下一个位置。
#define MaxSize 50 //定义队列中元素的最大个数
typedef struct
{
ElemType data[MaxSize]; //存放队列元素
int front,rear; //队头指针和队尾指针
}SqQueue;
初始条件(队空条件):Q.front==Q.rear==0 队头指针等于队尾指针指向0。
进队操作:队不满时,先送值到队尾元素,再将队尾指针加1。
出队操作:队非空时,先取队头元素值,再将队头指针加1,指向队头的下一个元素。
如图(a):Q.front==Q.rear==0,图a所示为队列的初始状态,该条件可以作为队列判空的条件。
图(b):表示abcde 5个元素依次入队。
图(c):表示a元素出队。
图(d):表示在 图(c) 的基础上出队3次,这个时候Q.rear=MaxSize,但这并不能作为队列的判满条件,因为很明显此时队列还有空位。如果此时进行入队操作,那么会出现名义上的 “上溢出” 现象,但这种溢出并不是真正的溢出,在data数组中仍然存在可以存放元素的位置。
2.4 循环队列
将顺序队列臆造成一个环状的空间,也就是把存储队列元素的表从逻辑上视为一个环,称为循环队列。
初始时:Q.front=Q.rear=0;
队首指针进1:Q.front=(Q.front+1)%MaxSize。
队尾指针进1:Q.rear=(Q.rear+1)%MaxSize。
队列长度:(Q.rear-Q.front+MaxSize)%MaxSize。
如下图(b)(c):入队出队时,指针都按顺时针方向进1。
首先,如图(d1)所示,Q.rear=Q.front是无法判断循环队列是队满还是队空的。那么是如何判断队空还是队满呢?
区分队空还是队满的三种处理方式:
①:牺牲一个单元来区分队空还是队满,也就是入队的时候,所有的队列单元不要全部用满,少用一个队列单元(这是比较普遍的做法)。
队头指针在队尾指针的下一个位置作为队满的标志。如上图(d2)所示:
队满条件:(Q.rear+1)%MaxSize=Q.front。
队空条件:Q.front==Q.rear。
队列中元素的个数:(Q.rear-Q.front+MaxSize)%MaxSize。
②:类型中增设表示元素个数的数据成员。也就是在结构体中增加一个表示元素个数的成员变量。这样,表达队空、队满就有专门的成员变量去表示。队空的条件为:Q.size==0;队满的条件为:Q.size==MaxSize。
③:类型中增设tag数据成员,以区分是队满还是队空。
tag等于0时,若因删除导致Q.front==Q.rear,则为队空;
tag等于1时,若因插入导致Q.front==Q.rear,则为队满。
2.4.1 循环队列的操作
//初始化
void InitQueue(SqQueue &Q)
{
Q.rear=Q.front=0; //初始化队首、队尾指针
}
//判队空
bool isEmpty(SqQueue Q)
{
if(Q.rear==Q.front)
return true; //队空条件
else
return false;
}
//判队满
bool isEmpty(SqQueue Q)
{
if((Q.rear+1)%MaxSize==Q.front)
return true; //队空条件
else
return false;
}
//入队
bool EnQueue(SqQueue &Q,ElemType x)
{
if((Q.rear+1)%MaxSize==Q.front) //先判断队列是否满了,满了的情况下肯定是不能入队了
{
return false; //队满了报错
}
Q.data[Q.rear]=x; //把要入队的元素x给到队尾指针指向的数据
Q.rear=(Q.rear+1)%MaxSize; //该操作实现队尾指针加1
return true;
}
//出队
bool DeQueue(SqQueue &Q,ElemType &x)
{
if(Q.front==Q.rear) //判断队是否空,队空肯定是没有元素可以进行出队操作了
return false;
x=Q.data[Q.front]; //把队首指针指向的元素赋值给x,用于返回
Q.front=(Q.front+1)%MaxSize; //队首指针加1后移
return true;
}
2.5 队列的链式存储
队列的链式表示称为链队列,实际上是一个同时带有队头指针和队尾指针的单链表。头指针指向队头结点,尾指针指向队尾结点,队尾结点就是单链表的最后一个结点。
typedef struct LinkNode //链式队列结点
{
ElemType data; //数据域
struct LinkNode *next; //指针域
}LinkNode;
typedef struct //链式队列
{
LinkNode *front,*rear; //队列的队头和队尾指针
}LinkQueue;
当Q.front==NULL且Q.rear==NULL时,链式队列为空。
出队时,首先判断队是否为空,若不空,则取出队头元素,将其从链表中摘除,并让Q.front指向下一个结点(若该结点为最后一个结点,则置Q.front和Q.rear都为NULL)。入队时,建立一个新结点,将新结点插入到链表的尾部,并让Q.rear指向这个新插入的结点(若原队列为空队,则令Q.front也指向该结点)。
注:不带头结点的链式队列在操作上往往比较麻烦,因此通常将链式队列设计成一个头结点的单链表。
2.5.1 链式队列的基本操作
// 初始化
void InitQueue(LinkQueue &Q)
{
Q.front=Q.rear=(LinkNode*)malloc(sizeof(LinkNode)); //建立头结点
//动态开辟一个头结点,将头指针和尾指针全部指向这个头结点
Q.front->next=NULL; //初始化为空 将头结点的指针域指向为空,表示头结点既是队列的第一个结点,也是队列的最后一个结点
}
//判断队空
bool IsEmpty(LinkQueue Q)
{
if(Q.front==Q.rear) //队头指针和队尾指针指向同一个结点,表示队列为空
return true;
else //否则表示队列非空
return false;
}
//入队
void EnQueue(LinkQueue &Q,ElemType x)
{
//想要使新元素入队,首先先要动态开辟一个结点,保证有空间存储新元素的数据
LinkNode *s=(LinkNode *)malloc(sizeof(LinkNode)); //指针S指向这个新开辟的结点
s->data=x; //把数据x存放到指针s指向的数据域中
s->next=NULL; //创建新结点,因为队列中新插入的结点一定是放在队列的队尾,所以新创建结点的指针域一定指向的是NULL 空指针
Q.rear->next=s; //因为rear指针是指向当前的队尾结点,插入结点以后,要让队尾指针指针域指向新插入的结点s
Q.rear=s; //最后要让队尾指针指向新插入的结点
}
//出队
bool DeQueue(LinkQueue &Q,ElemType &x)
{
if(Q.front==Q.rear)
return false; //空队,没有元素可以实现出队操作
LinkNode *p=Q.front->next; //定义指针p指向队首这次要删除的这个结点的后一个结点
x=p->data; //把指针p数据域的元素给到x 用以返回
Q.front->next=p->next; //因为要执行出队操作,删除一个结点,要让队首结点的指针域指向下一个结点的指针域 (如下图所示)
if(Q.rear==p) //该队列只有这一个结点,删除该结点后,队尾指针指向了指针p
Q.rear=Q.front; //删除这个结点以后,队列再次变为空队列,Q.rear=Q.front表示队列为空
free(p); //释放掉动态开辟的空间
return true;
}
2.6 双端队列
双端队列是指允许两端都可以进行入队和出队操作的队列,其元素的逻辑结构仍是线性结构。将队列的两端分别称为前端和后端,两端都可以入队和出队。
在双端队列进队时,前端进的元素排列在队列中后端进的元素前面,后端进的元素排列在队列中前端进的元素的后面。在双端队列出队时,无论是前端还是后端出队,先出的元素排列在后出的元素的前面。
输出受限的双端队列:允许在一端进行插入和删除,但是在另一端只允许插入的双端队列称为输出受限的双端队列。
输入受限的双端队列:允许在一端进行插入和删除,但是在另一端只允许删除的双端队列称为输入受限的双端队列。
2.7 该部分相关练习
1. 循环队列存储在数组A[0……n]中,入队时的操作为 rear=(rear+1)mod(n+1);
因为数组下标范围是0~n,因此数组容量为 n+1。循环队列中元素入队的操作是rear=(rear+1)MOD MaxSize,题中MaxSize=n+1。因此入队操作应为rear=(rear+1)MOD (n+1)
2. 已知循环队列的存储空间为数组A[21],front 指向队头元素的前一个位置,rear 指向队尾元素,假设当前front 和 rear 的值分别为8 和 3,则该队列的长度为 16;
队列的长度为(rear-front+MaxSize)%MaxSize=(3-8+21)%21=16
3. 若数组A[0……5] 来实现循环队列,且当前rear 和 front的值分别为1 和 5,当从队列中删除一个元素,再加入两个元素后,rear 和 front 的值分别为 3和0;
循环队列中,每删除一个元素,队首指针front=(front+1)%6;每插入一个元素,队尾指针 rear=(rear+1)%6。
删除一个元素后,front=(5+1)%6=0
加入第一个元素后,rear=(1+1)%6=2; 加入第二个元素后,rear=(2+1)%6=3;
4. 假设一个循环队列 Q[MaxSize]的队头指针为front,队尾指针为rear,队列中的最大容量为MaxSize,此外,该队列再没有其他数据成员,则判断该队列的列满条件是Q.front==(Q.rear+1)%MaxSize;
不能添加任何其他数据成员,只能采用牺牲一个存储单元的方法来区分是队空还是队满,约定 “队列头指针在队尾指针的下一位置作为队满的标志”
5. 最适合用作链队的链表是:带队首指针和队尾指针的非循环单链表
6. 最不适合用作链式队列的链表是:只带队首指针的非循环单链表
7. 用链式存储方式的队列进行删除操作时需要:头尾指针可能都要修改
一般情况下,队列用链式存储时,删除元素从表头删除,通常仅需修改头指针;
但是如果队列中仅有一个元素,这个时候进行删除操作时,尾指针也需要被修改,当仅有一个元素时,删除后队列为空,需修改尾指针为 rear = front;
8. 在一个链队列中,假设队头指针为front,队尾指针为rear,x 所指向的元素需要入队,则需要执行的操作为:rear->next=x,x->next=NULL,rear=x
队列中入队操作是需要在队尾执行的,所以首先让队尾指针的指针域指向这个指针x,新元素入队以后,新元素就会作为队列的队尾元素,所以紧接着需要设置队尾元素的指针域指向NULL空指针,然后设置队尾指针指向这个结点。
9. 已知循环队列存储在一维数组A[0……n-1]中,且队列非空时front和rear分别指向队头元素和队尾元素。若初始时队列为空,且要求第一个进入队列的元素存储在A[0]处,则初始时front和rear的值分别为 0,n-1;
首先循环队列存储在一维数组A[0……n-1]中,所以MaxSize=n;第一个进入队列的元素存储在A[0]处,此时front 和rear 的值都为0;入队时需要执行(rear+1)%n=n-1(也就是1对n取余的结果是n-1),由于只是执行入队操作,而不执行出队操作,所以队首指针front 的值还是0;
10. 设有如下图所示的火车车轨,入口到出口之间有"条轨道,列车的行进方向均为从左至右,列车可驶入任意一条轨道。现有编号为1~9的9列列车,驶入的次序依次是 8, 4, 2, 5, 3, 9, 1,6,7。若期望驶出的次序依次为1 ~ 9,则 n 至少是()。
这道题的意思是:我有n 条轨道,驶入的次序是4,2,5,3,9,1,6,7,想要使得输出的次序为1,2,3,4,5,6,7,8,9,问最少要几条通道(入栈和出栈的问题)
首先,因为是 1-9 顺序出栈的,所以每条轨道上的列车号只能是依次递增的,否则是无法满足要求的;
3. 栈和队列的应用
3.1 栈在括号匹配中的应用
假设表达式中允许出现两种括号:圆括号和方括号,圆括号和方括号的嵌套结构可以是任意的。(我们在VS 环境下编写代码时,可以很明显的发现,不管是大括号还是中括号都是成双成对的出现的,如果有哪个括号是单独出现的,那么编译环境就会报错)
细细分析:
1. 计算机接收第一个括号 “[” 后,计算机期待与之匹配的第八个括号 “]” 出现。
2. 在计算机获得第二个括号 “( ”,此时第一个括号 “[” 暂时放在一边,计算机急迫期待与之匹配的第七个括号 “ )” 出现。
3. 获得了第3个括号 “[” ,此时第2个括号“( ” 暂时放在一边,而急迫期待与之匹配的第4个括号 “]” 出现。第3个括号的期待得到满足,消解之后,第 2 个括号的期待匹配又成为当前最急迫的任务。
4. 以此类推,该处理过程和栈的思想吻合。(之所以这么说,是因为 ( ( ( ) ) ) , 最后出现的左括号(第三个左括号)是最先被匹配的(第四个就是右括号),因为第四个出现的右括号就会首先和第三个出现的左括号进行匹配,这个和栈先入后出(就是说第一个进栈的左括号是最后一个和第六个右括号进行匹配的)的特性是相同的; 当我们遇到左括号时,就把他压入栈中,当出现右括号时,就会首先和栈顶的左括号进行匹配,等同于出栈操作)
算法的思想:
1. 初始设置一个空栈,顺序读入括号。
2. 若是右括号,则使置于栈顶的最急迫期待得以消解,或者是不合法的情况(括号序列不匹配,退出程序)。
3. 若是左括号,则作为一个新的更急迫的期待压入栈中,自然而然的使原有的栈中的所有未消解的期待的急迫性降了一级。算法结束时,栈为空,否则括号序列不匹配。
#define MaxSize 10 //定义栈中元素的最大个数
typedef struct
{
char data[MaxSize]; //静态数组存放栈中元素
int top; //栈顶指针
}SqStack;
bool bracketCheck(char str[], int length)
{
SqStack S;
InitStack(S); //初始化一个栈
for(int i=0;i<length;i++)
{
if(str[i]=='('||str[i]=='['||str[i]=='{')
{
Push(S,str[i]); //扫描到左括号,入栈
}
else
{
if(StackEmpty(S)) //扫描到右括号,且当前栈空
return false; //匹配失败
char topElem;
Pop(S,topElem); //栈顶元素出栈
if(str[i]==')'&&topElem!='(')
return false;
if(str[i]==']'&&topElem!='[')
return false;
if(str[i]=='}'&&topElem!='{')
return false;
}
}
retrun StackEmpty(S); //检索完全部括号后,栈空说明匹配成功
}
//初始化栈
void InitStack(SqStack &S)
//判断栈是否为空
bool StackEmpty(SqStack S)
//新元素入栈
bool Push(SqStack &S,char x)
//栈顶元素出栈,用x返回
bool Pop(SqStack &S,char &x)
3.2 栈在表达式求值中的应用
表达式求值是程序设计语言编译中一个最基本的问题。中缀表达式不仅依赖运算符的优先级,而且还要处理括号。后缀表达式的运算符在操作数后面,在后缀表达式中已考虑了运算符的优先级,没有括号,只有操作数和运算符。
什么是前缀表达式?什么是中缀表达式?什么是后缀表达式?
当运算符在两个操作数的前面时,称作前缀表达式。例如+ab;
当运算符在两个操作数的中间时,称作中缀表达式。例如 a+b;
当运算符在两个操作数的后面时,称作后缀表达式。例如ab+;注意:ab的顺序是不能颠倒的。
后缀表达式适用于基于栈的编程语言(stack-oriented programming language),如:Forth、PostScript
中缀表达式A+B*(C-D)-E / F 所对应的后缀表达式为 ABCD-*+EF / -
中缀转后缀的手算方法:
① 确定中缀表达式中各个运算符的运算顺序
② 选择下一个运算符,按照 [左操作数 右操作数 运算符] 的方式组合成一个新的操作数
③ 如果还有运算符没被处理,就继续 ②
((15
(7-(1+1)))
3)-(2+(1+1)) 中缀表达式
15 7 1 1+-
3
2 1 1++- 后缀表达式
通过后缀表示计算表达式值的过程为:顺序扫描表达式的每一项,然后根据它的类型做如下操作:
若该项是操作数,则将其压入栈中;若该项是操作符 <op>,则连续从栈中退出两个操作数Y和X,形成运算指令 X<op>Y ,并将运算结果重新压入栈中。当表达式的所有项都扫描并处理完后,栈顶存放的就是最后的计算结果。
例如,后缀表达式 ABCD-*+EF / - 求值的过程:
- 置空栈,栈中内容为空;
- 扫描到A,A属于操作数,操作数进栈,栈中元素为A;
- 扫描到B,B属于操作数,操作数进栈,栈中元素为A,B;
- 扫描到C,C属于操作数,操作数进栈,栈中元素为A,B,C;
- 扫描到D,D属于操作数,操作数进栈,栈中元素为A,B,C,D;
- 扫描到 - ,- 属于操作符,D、C退栈,计算 C-D,结果
进栈,栈中元素为A,B,
;
- 扫描到 * ,* 属于操作符,
、B 退栈,计算 B*
,结果
进栈,栈中元素为A,
;
- 扫描到 + ,+ 属于操作符,
、A退栈,计算A+
,结果
进栈,栈中元素为
;
- 扫描到 E,E 属于操作数,操作数进栈,栈中元素为
、E;
- 扫描到 F,F 属于操作数,操作数进栈,栈中元素为
、E、F;
- 扫描到 / ,/ 属于操作符,F、E退栈,计算E/F,结果
进栈,栈中元素
、
;
- 扫描到 - ,- 属于操作符,
、
退栈,计算
-
,结果
进栈,栈中元素为
;
总结如下:通过这里的学习,我们应该明白了计算机计算表达式时,实际上还是栈的运用;
用栈实现后缀表达式的计算:
① 从左往右扫描下一个元素,直到处理完所有元素
② 若扫描到操作数则压入栈,并回到①;否则执行③
③ 若扫描到运算符,则弹出两个栈顶元素,执行相应运算,运算结果压回栈顶,回到①
注意:先出栈的是右操作符;
中缀表达式转后缀表达式(机算):
初始化一个栈,用于保存暂时还不能确定运算顺序的运算符。
从左到右处理各个元素,直到末尾。可能遇到三种情况:
① 遇到操作符。直接加入后缀表达式。
② 遇到界限符。遇到 “(” 直接入栈;遇到 “)” 则依次弹出栈内运算符并加入后缀表达式,直到弹出 “(” 为止。注意: “(” 不加入后缀表达式。
③ 遇到运算符。依次弹出栈中优先级高于或等于当前运算符的所有运算符,并加入后缀表达式,若碰到 “(” 或栈空则停止。之后再把当前运算符入栈。
中缀表达式的计算(用栈实现)(中缀转后缀 + 后缀表达式求值 两个算法的结合 ):
初始化两个栈,操作数栈和运算符栈
若扫描到操作数,压入操作数栈
若扫描到运算符或界限符,则按照 “中缀转后缀” 相同的逻辑压入运算符栈(期间也会弹出运算符,每当弹出一个运算符时,就需要再弹出两个操作数栈的栈顶元素并执行相应运算,运算结果再压回操作数栈)
栈在表达式求值中的应用主要分为三方面:
3.3 栈在递归中的应用
递归是一种重要的程序设计方法。若在一个函数、过程或数据结构的定义中又应用了它自身,则这个函数、过程或数据结构称为递归。
递归通常把一个大型的复杂问题层层转化为一个与原问题相似的规模较小的问题来求解,递归策略只需少量的代码就可以描述出解题过程所需要的多次重复计算,大大减少了程序的代码量。(缺点是递归的效率一般不是很高,原因是递归调用过程中包含了很多重复的计算)
//斐波那契数列的实现
int Fib(int n) //斐波那契数列的实现
{
if(n==0)
return 0; //边界条件
else if(n==1)
return 1; //边界条件
else
return Fib(n-1)+Fib(n-2); //递归表达式
}
递归的精髓在于能否将原始问题转换为属性相同但规模较小的问题。
在递归调用过程中,系统为每一层的返回点、局部变量、传入实参等开辟了递归工作栈来进行数据存储,递归次数过多容易造成栈溢出。
以下是斐波那契数列的 n=5 时的递归调用执行过程:
显然,在递归调用过程中,Fib(3)被计算了2次,Fib(2)被计算了3次。Fib(1)被调用了5次,Fib(0)被调用了3次。所以,递归的效率低下,但是比较明显的优点是代码简单,容易理解。
3.4 队列在层次遍历中的应用
在信息处理中有一大类的问题需要逐层或逐行处理。这类问题的解决方法往往是在处理当前层或者当前行时就对下一层或者下一行做预处理,把处理顺序安排好,等到当前层或者当前行处理完毕,就可以处理下一层或者下一行。使用队列是为了保存下一步的处理顺序。
层次遍历二叉树的过程:
①:根结点入队;
②:若队空(所有结点都以处理完毕),则结束遍历;否则重复 ③ 操作;
③:队列中第一个结点出队,并访问之。若其有左孩子,则将左孩子入队;若其有右孩子,则将右孩子入队,返回 ②;
层次遍历二叉树的过程:
- A入,队内A,队外无;
- A出,BC入,队内BC,队外A;
- B出,D入,队内CD,队外AB;
- C出,EF入,队内DEF,队外ABC;
- D出,G入,队内EFG,队外ABCD;
- E出,HI入,队内FGHI,队外ABCDE;
- F出,队内GHI,队外ABCDEF;
- GHI出,队外ABCDEFGHI;
3.5 队列在计算机系统中的应用
队列在计算机系统中应用主要有以下两个方面:第一个方面是解决主机与外部设备之间速度不匹配的问题,第二个方面是解决由多用户引起的资源竞争问题。
第一方面(主机和打印机之间速度不匹配):主机输出数据给打印机打印,输出数据的速度比打印数据的速度要快的多,由于速度不匹配,若直接把输出的数据送给打印机打印显示是不行的。解决的方法是设置一个打印数据缓冲区,主机把要打印输出的数据依次写入这个缓冲区,写满后就暂停输出,转去做其他的事情。打印机就从缓冲区中按照先进先出的原则依次取出数据并打印,打印完后再向主机发出请求。主机接到请求后再向缓冲区写入打印数据。打印数据缓冲区中所存储的数据就是一个队列。
第二方面(CPU 中央处理器,包含运算器和控制器)资源的竞争就是一个典型的例子:在一个带有多终端的计算机系统上,有多个用户需要 CPU 各自运行自己的程序,他们分别通过各自的终端向操作系统提出占用CPU的请求。操作系统通常按照每个请求在时间上的先后顺序,把它们排成一个队列,每次把 CPU 分配给队首请求的用户使用。当相应的程序运行结束或用完规定的时间间隔后,令其出队,再把 CPU 分配给新的队首请求的用户使用。
3.6 相关练习
1. 对于一个问题的递归算法求解和其相对应的非递归算法求解,非递归算法通常效率高一些;
2. 将中缀表达式 a+b-a*((c+d)/e-f)+g 转换为等价的后缀表达式 ab + acd + e / f - * - g + 时,用栈来存放暂时还不能确定运算次序的操作符。栈初始时为空,转换过程中同时保存在栈中的操作符的最大个数为 5;
用栈实现中缀表达式转后缀表达式:
初始化一个栈,用于保存暂时还不能确定运算顺序的运算符。
从左到右处理各个元素,直到末尾,可能遇到三种情况:
① 遇到操作数。直接加入后缀表达式。
② 遇到界限符。遇到 “(” 直接入栈:遇到 “)” 则依次弹出栈内运算符并加入后缀表达式,直到弹出 “(” 为止。注意: “(” 不加入后缀表达式。
③ 遇到运算符。依次弹出栈中优先级高于或等于当前运算符的所有运算符,并加入后缀表达式;
若遇到 “(” 或栈空则停止。之后再把当前运算符入栈。
3. 假设栈初始为空,将中缀表达式 a / b+(c * d - e * f)/ g 转换为等价的后缀表达式的过程中,当扫描到 f 时,栈中的元素依次是 + ( - *
这道题和上一道题思想是一样的;
4. 假设一个算数表达式中包含圆括号、方括号和花括号 3 种类型的括号,编写一个算法来判别表达式中的括号是否配对,以字符 “\0” 作为算术表达式的结束符。
算法思想:
首先扫描每个字符,遇到花、中、圆的左括号时进栈,遇到花、中、圆的右括号时检查栈顶元素是否为相应的左括号,若是,退栈,否则配对成功。栈若不空也为错误。
bool BracketsCheck(char *str)
{
InitStack(S); //初始化栈
int i = 0;
while (str[i] != '\0') //只要不等于结束标志,则进入循环
{
switch (str[i])
{
//左括号入栈
case '(':
Push(S, '('); //Push入栈
break;
case '[':
Push(S, '['); //Push入栈
break;
case '{':
Push(S, '{'); //Push入栈
break;
//遇到右括号,检查栈顶
case ')':
Pop(S, e);
if (e != '(')
return false;
break;
case ']':
Pop(S, e);
if (e != '[')
return false;
break;
case '}':
Pop(S, e);
if (e != '{')
return false;
break;
default:
break;
}
i++;
}
if (!IsEmpty(S))
{
printf("括号不匹配\n");
return fasle;
}
else
{
printf("括号匹配\n");
return true;
}
}
5. 某汽车轮渡口,过江渡船每次能载 10 辆车过江。过江车辆分为客车类和货车类,上渡船有如下规定:同类车先到先上船;客车先于货车上船,且每上 4 辆客车,才允许放上 1 辆货车;若等待客车不足 4 辆,则以货车代替;若无货车等待,允许客车都上船。设计一个算法模拟渡口管理。
算法思想:
假设数组 q 的最大下标为 10 ,恰好是每次载渡的最大量。假设客车的队列为 q1 ,货车的队列为 q2 。若 q1 充足,则每取 4 个 q1 元素后再取一个 q2 元素,直到 q 的长度为 10 。若 q1 不充足,则直接用 q2 补齐。
Queue q; //过江渡船载渡队列
Queue q1; //客车队列
Queue q2; //货车队列
void manager()
{
int i = 0, j = 0; //j表示渡船上的总车辆数 i表示客车数
while (j < 10) //渡船上总车辆不少于10个时
{
if (!QueueEmpty(q1) && i < 4) //如果客车非空,并且未上满4辆车
{
DeQueue(q1, x); //从客车队列出队
EnQueue(q, x); //客车上渡船
i++; //客车数加1
j++; //渡船上总车辆数加1
}
else if (i == 4 & !QueueEmpty(q2)) //如果客车数已满4,并且货车队列非空
{
DeQueue(q2, x); //从货车队列出队
EnQueue(q, x); //货车上渡船
j++; //渡船上总车辆数++
i = 0; //因为是上满4个客车,上一个货车,那么上完一个货车以后,就要从新进入下一个循环,将客车数清0
}
else //其他情况(客车队列空或者货车队列空)
{
while (j < 10 && i < 4 && !QueueEmpty(q2)) //客车队列空 也就是客车上不满4辆 这个时候需要货车来代替
{
DeQueue(q2, x); //从货车队列出队
EnQueue(q, x); //货车上渡船
i++; //i用来计数,当i大于4时,退出该循环
j++; //渡船上总车辆数加1
}
i = 0;
}
if (QueueEmpty(q1) && QueueEmpty(q2))
j = 11; //若货车和客车加起来不足10辆
}
}
4. 数组和特殊矩阵
矩阵在计算机图形学、工程计算中占有举足轻重的地位。在数据结构中考虑的是如何用最小的内存空间来存储同样的一组数据。数据结构研究的内容是如何将矩阵更有效的存储在内存中,并且能方便的提取矩阵中的元素。数据结构中不研究矩阵及其运算。
4.1 数组的定义
数组是由 n (n1)个相同类型的数据元素构成的有限序列,每个数据元素称为一个数组元素,每个元素在 n 个线性关系中的序号称为该元素的下标。下标的取值范围称为数组的维界。
数组和线性表的关系:
数组是线性表的推广。一维数组可视为一个线性表;二维数组可视为其元素也是定长线性表的线性表。数组一旦被定义,其维数和维界就不再改变。因此,除结构的初始化和销毁外,数组只会有存取元素和修改元素的操作。
4.2 数组的存储结构
大多数计算机语言都提供了数组数据类型,逻辑意义上的数组可采用计算机语言中的数组数据类型进行存储,一个数组的所有元素在内存中占用一段连续的存储空间。
以一维数组 A[0……n-1] 为例,其存储结构关系式为
LOC() =LOC(
) + i * L(0
i
n)--- 其中,L 是每个数组元素所占的存储单元。
也就是说第一个元素存储到内存中以后,因为是连续存储的;所以下一个会存储在第一个元素乘以存储单元大小的位置上,以后的每一个元素依次类推;
对于多维数组,有两种映射方法:按行优先和按列优先。以二维数组为例:
按行优先存储的基本思想是:先行后列,先存储行号较小的元素,行号相等先存储列号较小的元素。 设二维数组的行下标与列下标的范围分别是 [0,] 与 [0,
],则存储结构关系式为
LOC(a i,j)= LOC(a 0,0)+[i×(h,+1)+ j]×L
按列优先存储时,得出存储结构关系式为:
LOC(a i,j)=LOC(a 0,0)+[j×(h+1)+i]×L
4.3 特殊矩阵的压缩存储
压缩矩阵:指为多个值相同的元素只分配一个存储空间,对零元素不分配存储空间。其目的是节省存储空间。
特殊矩阵:指具有许多相同矩阵元素或零元素,并且这些相同的矩阵元素或零元素的分布有一定规律性的矩阵。常见的特殊矩阵有对称矩阵、上(下)三角矩阵、对角矩阵等。
特殊矩阵的压缩存储方法:找出特殊矩阵中值相同的矩阵元素的分布规律,把那些呈现规律性分布的、值相同的多个矩阵元素压缩存储到一个存储空间中。
4.4 稀疏矩阵
矩阵中非零元素的个数为 t ,相对矩阵元素的个数 s 来说非常少,也可以说 s 远远大于 t 的矩阵称为稀疏矩阵。
若采用常规的方法来存储稀疏矩阵,相当于浪费空间,并且稀疏矩阵中零元素的分布是没有规律的;因此,将非零元素及其相应的行和列构成一个三元组(行标,列标,值),按照某种规律来存储三元组。
4.5 相关练习
1. 对 n 阶对称矩阵压缩存储时,需要表长为 n(n+1)/2 的顺序表。
首先是 n 阶对称矩阵,所以进行压缩存储时,只需要存储上三角部分或者下三角部分(含对角线元素),所以假设是存储下三角矩阵,第一行一个元素,第二个两个元素,第三行三个元素,依次构成等差数列;1+2+3+……n= n(n+1)/2 ;
2. 一个 nxn 的对称矩阵A,将其下三角部分按行存放在一维数组B中,而 A【0】【0】存放于B【0】中,则第 i+1 行的对角元素A【i】【i】存放于B中的 (i+3)i/2 处。
首先 A【0】【0】存放于B【0】中,那么意味着告诉了我们二维数组的起始位置是0 0,一维数组的起始位置也是0,又因为是对称矩阵,只考虑下三角矩阵或者上三角矩阵,那么二维数组第0行,存放一个元素;第一行存放两个元素;第 i 行存放 i+1 个元素;那么总共就构成一个等差数列,首行一个元素,末行 i+1 个元素,总共 i+1 行,总共就是 (i+1)(i+2)/2 个元素,因为存放到一维数组 B[] 中是从下角标 0 开始存放的,所以最终是应该是存放到 (i+1)(i+2)/2-1 处,化简得到最终答案。
3. 在一个二维数组 A 中,假设每个数组元素的长度为 3 个存储单元,行下标 i 为 0~8,列下标 j 为 0~9 ,从首地址 SA 开始连续存放。在这种情况下,元素 A[8][5] 的起始地址为 SA+255
首先题目告诉我们,是一个二维数组,每个数组元素的长度为 3 个存储单元,每一行10个元素, A [8] [5] 存放在第9行的第六个位置上,所以最终应该是8*10+5=85,85*3=255,所以最终元素A[8][5] 的起始地址为 SA+255 。
4. 将三对角矩阵 A[1...100] [1...100] 按行优先存入一维数组 B[1...298] 中, A 中元素 A[66] [65] 在数组 B 中的位置 k 为 195 。
三对角矩阵也就是只有斜对角上有 3 列元素,其余位置上都是 0 ;三对角存放通过画图可以发现,只有第一行和最后一行是两个元素,其余行都是三个元素;同样的只有第一列和最后一列是两个元素,其余列都是三个元素;这样也就是说第 66 行前面有 65 行, 64 行是三个元素,一行是两个元素;所以 66 行前总共有 64*3+2=194 个元素;矩阵斜对角线上的对应的坐标是 (66,66);所以65就是第66行的第一个元素;此时也就可以确定 A[66][65] 在数组 B 中的位置为 64*3+2+1=195;
5. 若将 n 阶上三角矩阵力按列优先级压缩存放在一维数组B[l...n(n+1) /2 + 1]中,则存放到B [k]中的非零元素 aij ( 1i ,j
n )的下标 i、j 与 k 的对应关系是 j (j-1) / 2+ i;
首先注意到是按照列优先级压缩存放的,上三角矩阵,第一列一个元素,第二个两个元素,第三列三个元素…… 元素 aij 前面有 j-1 列,第 j-1 列有 j-1 个元素,所以等差数列 (1+j-1)(j-1)/2;又因为是按照列优先级来排列的,所以第 i 行需要在 j(j-1) / 2+i ;
6. 二维数组 A 按照行优先方式存储,每个元素占用 1 个存储单元。若元素 A[0][0] 的存储地址是 100,A[3][3] 的存储地址是 220,则元素 A[5][5] 的存储地址是 300 ;
根据题目中 A[0][0] 和 A[3][3] 的存储地址可以知道 A[3][3] 是220-100+1=121个元素,假设每一行存储 n 个元素,A[3][3] 也就是第121个元素:121=3*n+4 (加4是因为存储在数组中是从下角标0开始的),解得 n=39,也就是说一行存放 39 个元素,A[5][5] 存储的地址就是 39*5+6-1=300