数据结构与算法---3.栈和队列

数据结构与算法之栈和队列

一. 栈

1. 定义:只允许在一端进行插入和删除操作的线性表。
2. 特点:先进后出
3. 栈的基本操作
InitStack(&S):初始化一个空栈S。
StackEmpty(S):判断一个栈是否为空,若为空栈则返回true。
Push(&S, x):进栈,若栈S未满,则将x加入并使其为新栈顶。
Pop(&S, &x):出栈,若栈非空,则返回栈顶元素,并用x返回。
GetTop(S,&x):读栈顶元素,若栈非空则用x返回栈顶元素
ClearStack(&S):销毁栈,并释放S占用的内存空间。

二. 栈的顺序存储结构

1.顺序栈:采用顺序存储的栈

#define MaxSize 50
typedef struct
{
	ElemType data[MaxSize];
	int top;
}SqStack;

栈空条件:S.top == -1
栈长: S.top+1
栈满条件: S.top == MaxSize-1

2. 基本操作
初始化

void InitStack(SqStack &S)
{
	s.top == -1;
}

判断栈空

bool tackEmpty(SqStack s)
{
	if(s.top == -1)
		return true;
	else
		return false;
}

进栈

bool Push(SqStack &S, ElemType x)
{
	if(S.top == MaxSize-1)
		return false;
	S.data[++S.top] = x;
	return true ;
}

出栈

bool Pop(SqStack &S, Elemtype &x)
{
	if(S.top == -1)
		return false;
	x = S.data[S.top—];
	return true;
}

读取栈顶元素

bool GetTop(SqStack S, Elemtype &x)
{
	if(S.top == -1)
		return false;
	x = S.data[S.top];
	return true;
}

3.共享栈
共享栈:将两个栈底设置在共享空间的两端,栈顶向空间中间延伸。(一个数组两端都有一个标记最开始分别在数组两端,随着入栈出栈两标记向中间靠拢。即把两个栈的栈底向外,栈顶向内)

判空:0号栈 top == -1
1号栈 top == MaxSize
栈满: top1-top0 == 1

优点:采取事件复杂度仍为O(1),但空间利用率更高。

三. 栈的链式存储结构

1.链栈:采用链式存储的栈

typedef struct Linknode
{
	ElemType data;
	struct Linknode *next;
}*LiStack;

*通常所有操作都在表头进行。如果操作在表尾进行显然会麻烦很多,比如当出栈时则需要遍历一遍。
问题.设链表不带头结点且所有操作均在表头进行,则下列最不适合作为链栈的是( )。
A.只有表头结点指针,没有表尾指针的双向循环链表
B.只有表尾结点指针,没有表头指针的双向循环链表
C.只有表头结点指针,没有表尾指针的单向循环链表
D.只有表尾结点指针,没有表头指针的单向循环链表

答案: C
通常栈的插入和删除在表头进行。对于选项C,插入和删除一个结点后,仍需将其变为循环单链表,因此需要找到其尾结点,时间复杂度为o(n)。

若不做题干中的限制,则栈顶可取表头(带头结点的链表)或第二个结点(不带头结点的链表),
找指针的位置取头结点(带头结点的链表)或表头(不带头结点的链表)。

四. 队列

1. 定义:只允许在表的一端进行插入,表的另一端进行删除操作的线性表(先进先出)。
2. 队列的基本操作
InitQueue(&Q): 初始化队列,构造一个空队列Q。
QueueEmpty(Q):判断队列为空,若队列Q为空则返回true。
EnQueue(&Q, x):入队,若队列未满,将x加入使之成为新的队尾。
DeQueue(&Q, &x):出队,若队列非空,则删除对头元素,并用x返回。
GetHead(Q, &x):读队头元素,若队列Q非空则用x返回队头元素。
ClearQueue(&Q):销毁队列,并释放队列Q占用的内存空间。

五. 队列的顺序存储

1.顺序队:采用顺序存储的队列。
2.注意: front指向队首元素;rear指向队尾的下一个位置。(入队时队尾rear后移)
初始时front == rear == 0

#define MaxSize 50
typedef struct
{
	ElemType data[MaxSize];
	int front, rear;
}SqQueue;

判空条件:Q.front == Q.rear
队长:Q.rear-Q.front
队满条件:Q.rear == MaxSize (但是存在假溢出问题)

3.循环队列:把存储队列的顺序队列在逻辑上视为一个环。(通过取余 %MaxSise)
front指针移动: Q.front = (Q.front+1)%MaxSize
rear指针移动:Q.rear = (Q.rear + 1)%MaxSize
队列长度:(Q.rear+MaxSize-Q.front)%MaxSize (如果rear循环到了front的前面则直接减为负所以要加一个MaxSize)

问题:对空队满条件一样,如何解决?
队空条件: Q.front == Q.rear
队满条件:Q.front == Q.rear
方1:牺牲一个存储单元(常用)
队空条件: Q.front == Q.rear
队满条件:Q.front == (Q.rear+1)%MaxSize
方2:增加一个变量表示元素的个数Q.size
队空条件: Q.size == 0
队满条件:Q.size == MaxSize
方3:增加tag标识
tag = 1 (入队为1,出队为0)
队空条件:Q.front == Q.rear&&tag == 0
队满条件:Q.front == Q.rear&&tag == 1

4.循环队列的基本操作
初始化:

void InitQueue(SqQueue &Q)
{
	Q.rear = Q.front = 0;
}

判断队空

bool isEmpty(SqQueue Q)
{
	if(Q.rear == 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;
	Q.rear = (Q.rear + 1)%MaxSize;
	return true;
} 

六. 队列的链式存储结构

1.链队:采用链式存储的队列

typedef struct
{
	ElemType data;
	struct LinkNode *next;
}LinkNode;

typedef struct
{
	LinkNode *front, *rear;
}LinkQueue;

2.链队的基本操作

初始化:

void InitQueue(LinkQueue &Q)
{
	Q.front = (LinkNode*)malloc(sizeof(LinkNode));
	Q.rear = Q.front;
	Q.front->next = NULL;
}

判对空:

void isEmpty(LinkQueue Q)
{
	if(Q.front == Q.rear)
		return true;
	else
		return false;
}

入队(与链表的尾插法类似,而且还有头结点,front一直在头结点位置,rear则一直指向最后一个元素):

void EnQueue(LinkQueue &Q, ElemType x)
{
	LinkNode *s = (LinkNide *)malloc(sizeof(LinkNode));
	s->data = x;
	s->next = NULL;
	Q.rear->next = 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;
	Q.front->next = p->next;
	if(Q.rear == p)
		Q.rear = Q.front;
	free(p);	//这里是释放指针p所指的地址空间
	return true;
}

七.关于栈的几个问题:

1.连续输入、输出时栈和队列的输入输出关系?
栈的输入和输出是逆序的
队列的输入输出是同序的

2.在输入升序时,不连续的输入输出会使栈的输出有什么特点?
例如进栈为1,2,3,4,5……n
因为不连续,所以输出的可能有很多种(例如输入为1,2,3.则输出可能为1,2,3;3,2,1;2,1,3等)

特点:出栈序列中每一个元素后面比它小的元素组成的是一个递减序列。

解释: 简单的说就是在较大的元素入栈前没有进行出栈则该元素一定排在较大元素的后面。

详解:不连续输出即有两种情况,一种是输入一个就输出一个,显然此时是逆序即全为降序。第二种存在连续输出,例如3,4,5连续输出则把3,4,5看作整体输出后逆序则成5,4,3显然符合规则,如果我们
把3,4,5这样连续输出的看作一个整体记为x,x的内部是符合上述特点的。从全局来看在入栈时x前面的
元素都比x中任何一个元素小,而且又不是和x一起连续出栈的(即2进行了出栈,不然2应该纳入x中)。
也就是说x前面的元素是先出栈的,所以出栈后x中的所有元素都比出栈后x前面的元素大(出栈前在x
前面的元素,出栈后也在x前面),显然也符合上述特点。同理出栈前在x后面的元素讨论与x前的元素类似。

3.求合法出栈序列个数?
例如:入栈为1,2,3……k……n。求最后一个出栈的元素是k的合法序列个数。

思路:最后一个出栈为k,由2可知:入栈时在k前面的元素一定要在k入栈前全部出栈,而入栈时k后面的元素进行出栈入栈,最后才是k出栈,即把元素分为三部分,入栈时排在k前面的,k,入栈时排在k后面的。求出前一部分的所有情况f(k-1),求出第三部分的所有情况f(n-k),把f(k-1)*f(n-k)便是最后一个出栈为k的合法序列个数。

所以如果给出一个入栈序列求所有合法出栈序列个数则是最后一个出栈元素从1到n然后相加即
f(n)=f(0)*f(n-1)+f(1)*f(n-2)+……f(n-2)*f(1)+f(n-1)*f(0)
分别求出f(0)一直到f(n)
f(0)=f(1)=1
f(2)=f(0)*f(1)+f(1)*f(0)=2
f(3)=f(0)*f(2)+f(1)*f(1)+f(2)*f(0)=5
……
由递归可求f(n)=C(2n,n)/(n+1) 其中C(2n,n)是C2n取n
*此种算法并没有把可能入栈时出现重复元素的情况包含进去,所以只适用于不重复的乱序入栈求合法出栈的序列个数

问题1.是否一定要强调为升序?
eg:如果入栈序列为1.2.3.4.5
1.对于栈来说强调升序!
当连续出栈时便是逆序,与入栈序列无关。
当使用非连续出栈时如果入栈序列为升序,显然如上讨论。
当使用非连续出栈时如果入栈序列不确定,则不满足上述讨论。

理由如下:就局部连续来看如果不是升序则可能在该元素后面有大有小且个数、也由很多可能若由如下情况:4,8,6,3,9则逆序后为9,3,6,8,4显然既有升序又有降序。

所以综上便可得出结论:如果入栈序列是升序则有如下结论不管是连续出栈还是非连续出栈
出栈后组成的序列每一个元素后面比该元素小的元素重新组成的序列是降序的。

问题2.可行与不可行之间排列数的关系?
2.不可行的排列数就是所有元素的排列数即把n个元素放在n个不同的位置共有多少种排列方法即n*(n-1)……321=n!种,

八. 双端队列

1.双端队列:允许两端都可以进行入队以及出队操作的队列。(无论哪一端先出的元素在前,后厨的元素在序列后,即两端的元素后出的在后)
2.特点:把双端队列的某一端的入队出队受限则是栈,把双端队列的一端的输入受限一端的输出受限则是队列。
3.特殊双端队列
输出受限的双端队列:四个输入输出屏蔽一个输出,即两端都可以输入而只有一端可以输出。
输入受限的双端队列:四个输入输出屏蔽一个输入,即两端都可以输出而只有一端可以输入。

九. 关于栈与队列的几个注意事项。

*栈是在栈顶进行入栈和出栈操作的

*可以插入的一端叫队尾,可以删除的一端叫队头

*入队时一般是尾指针rear后移即入队操作在队尾进行,rear一般指的是队尾元素的后一位。

*队列除了上面说的一种front指向第一个元素,rear指向最后一个的下一位外,还有一种是front指向第一个入队元素的前一位而front则指向最后一位元素,即第一个入队元素的位置是front和rear的下一位然后rear后移至插入元素所在位置。
两种方法使用不同导致后面操作的实现也会不同。比如循环队列便用的rear指向队尾元素的下一位,而链队则用的是front指向头结点即队头元素的前一位。顺序队列和链队用两种任何一种都可以,但无论如何都要空一个位置。
区别便是先存取元素再移动指针还是先移动指针再存取。若是顺序队列则无论哪一种都很简单无非就是下标的问题,对于链队来说则若是rear指向头结点显然使用方法与链表的头插法类似,rear一直指向头结点;若rear一直指向尾结点那就与链表的尾插法类似,现插入后移动指针。

*几个概念:表头、表尾、队头、队尾、栈顶、栈底,一定要分清,前两个是针对一般的线性表,当中两个显然是队列,最后两个是栈。同时它们都是指线性表的第一个元素和最后一个元素。

***对于数据结构,要灵活变通,究竟是先移动指针在存取还是先存取在移动,以及要不要或者能不能用头结点等等都是根据具体问题具体分析,但是关于线性表、链表、顺序表、栈、队列的概念和特点一定要牢记!这些概念是必须参考的。(即上面说的都是具体实现可灵活变通,而概念是性质特点所在必须牢记)

十. 栈和队列的应用

1. 栈——括号匹配
问题:给出一连串的括号,问括号序列是否合法?eg:[]{(}
算法:
1>初始化一个空栈,顺序读入括号
2>若为右括号,则与栈顶元素进行匹配
匹配,则弹出栈顶元素
不匹配,则该序列不合法
3>若为左括号,则压栈
4>若全部的元素遍历完成,栈中非空则序列不合法

2. 栈——表达式求值
几种表达式:前缀表达式(+ab),中缀表达式(a+b),后缀表达式(ab+)
eg:[(a+b)c]-[e-f]
中缀表达式转换成前缀表达式:-
+abc-ef
中缀表达式转换成后缀表达式:ab+c*ef–
关键在于优先级要分清

问题:表达式求值
中缀转换成后缀表达式算法:
数字直接加入后缀表达式
运算符时
1>若为( ,入栈;
2>若为),则依次把栈中的运算符加入后缀表达式,直到出现(,并从栈中删除(;
3>若为±*/
栈空,入栈
栈顶元素为(,入栈
高于栈顶元素优先级,入栈
否则,依次弹出栈顶运算符,直到一个优先级比它低的运算符或(为止;
4>遍历完成,若栈非空依次弹出所有元素

3. 递归算法转换成非递归算法时往往要借助栈来进行
递归:若在一个函数、过程或数据结构的定义中有应用了它自身,则称它为递归定义的,简称递归(关键在于能否把一个复杂问题转换成相同性质的更小规模的问题)
eg:斐波那契数列:f(n)=f(n-1)+f(n-2) n>1; f(1)=1;f(0)=0;
递归产生的问题:
在递归调用过程中,系统为每一层的返回点、局部变量、传入实数等开辟了递归工作栈来进行数据存储,递归次数过多容易造成栈溢出。
通常情况下递归的效率并不高。

十一. 特殊矩阵的压缩存储

1.基本概念
压缩存储:指多个值相同的元素只分配一个存储空间,对零元素不分配存储空间。
特殊矩阵:指具有许多相同矩阵元素或零元素,并且这些相同矩阵元素或零元素的分布有一定规律性的矩阵。
特殊矩阵的压缩存储:找出特殊矩阵中值相同的矩阵元素的分布规律,把那些呈现规律性分布、值相同的多个矩阵元素压缩存储在一个存储空间上。

2.对称矩阵
对称矩阵:aij=aji,即沿主对角线对折重合位置的元素值相同(在压缩过程中只用存储下三角区域和主对角线)
数组下标为k=i(i-1)/2+j-1 i>=j; k=j(j-1)/2+i-1 i<j

3.类三角矩阵
类三角矩阵:上三角区域或者下三角区域元素值相同(不含主对角线)
存放数组B[n(n+1)/2+1] (元素值相同的放在数组的最后一位)
类下三角矩阵:数组下标为k=i(i-1)/2+j-1 i>=j; k=n(n+1)/2 i<j
类上三角矩阵:数组下标为k=i(2n-i+2)/2+j-1 i<=j; k=n(n+1)/2 i>j

4.三对角矩阵
三对角矩阵:若对一个n阶方阵中的任意元素aij,当|i-j|>1,有aij=0(1<=i,j<=n),则称为三对角矩阵(即除了主对角线以及平行与主对角线左右的两条线上的元素外其他元素都为0)
数组下标 :k=3*(i-1)-1+j-i+1=2i+j-3
已知k求ij? i=[(k+1)/3+1] j=k-2i+3

5.稀疏矩阵
稀疏矩阵:矩阵元素个数S相对于矩阵中非0元素的个数t来说非常多,即s>>t的矩阵称为稀疏矩阵。
稀疏矩阵使用三元组进行压缩存储后失去了随机存储的特性。(因为压缩过程中要保持对应元素的行号和列号,只有把行号、列号确定了才能操作对应的元素所以不可以像数组那样直接通过下标找到对应元素)

6.数组
数组:是由n个相同类型的数据元素构成的有限序列,每个数据元素称为一个数组元素,每个元素受n个线性关系的约束,每个元素在n个线性关系中的序号称为该元素的下标,并称为数组为n维数组。(数组是线性表的推广,此数组不是数组类型)
数组特点
数组具有维度(一维数组、二维数组);
数组一旦被定义,其维度和维界不可变,数组除初始化和销毁外,只有存取元素和修改元素的操作。
存储结构:采用顺序存储。
二维数组:二维数组可看作以一维数组为数组元素的数组
二维数组的存储:按行优先存储,按列优先存储。

猜你喜欢

转载自blog.csdn.net/weixin_45884870/article/details/103557523
今日推荐