王道章节内容
知识框架
考纲内容
- 栈与队列的基本概念;
- 栈与队列的顺序存储结构;
- 栈与队列的链式存储结构;
- 多维数组的存储;
- 特殊矩阵的压缩存储;
- 栈、队列和数组的应用。
引入
后缀表达式
后缀表达式转中缀表达式
6 2 / 3 - 4 2 * + =
“逐一扫描”,首先是 6,存入;(6);NULL
然后是 2,存入;(6 2);NULL
再者是 /,符合条件做运算输出 6 / 2,更新为3;(6 2 /)——(3);6 / 2
接着是 3,存入;(3 3);6 / 2
下来是 -,符合条件做运算输出 - 3,更新为 0;(3 3 -)——(0);6 / 2 - 3
然后是 4,存入;(0 4);6 / 2 - 3
再者是 2,存入;(0 4 2);6 / 2 - 3
接着是 *,根据“顺序存入,倒序输出”,顶上做运算更新为 8;(0 8);6 / 2 - 3 ~ 4 * 2
下来是 +,符合条件做运算输出 + 4 * 2,更新为 8;(8);6 / 2 - 3 + 4 * 2
最后是 =,符合条件输出 = 8,更新为NULL。(NULL);6 / 2 - 3 + 4 * 2 = 8
栈
把以上过程抽象出来,就是我们粗糙的“栈”的出入操作。
定义
栈(stack)
栈是只允许在一端进行插入或删除操作的线性表;
允许插入和删除的一端叫栈顶(top),另一端叫栈底(bottom),不含数据元素的叫空栈;
栈又称为后进先出(Last In First Out)的线性表,简称 LIFO 结构。
插入数据:入栈(Push);
删除数据:出栈(Pop)。
抽象数据类型描述(ADT)
- Stack CreateStack( int MaxSize ): 生成空堆栈,其最大长度为MaxSize;
- int IsFull( Stack S, int MaxSize ):判断堆栈S是否已满;
- void Push( Stack S, ElementType item ):将元素item压入堆栈;
- int IsEmpty ( Stack S ):判断堆栈S是否为空;
- ElementType Pop( Stack S ):删除并返回栈顶元素;
顺序栈
定义
顺序栈
采用顺序存储的栈称为顺序栈,它利用一组地址连续的存储单元存放自栈底到栈顶的数据元素,同时附设一个指针( top )指示当前栈顶元素的位置。
结构代码实现
陈越实现:
#define MaxSize <储存数据元素的最大个数>
typedef struct SNode *Stack;
struct SNode{
ElementType Data[MaxSize];
int Top;
};
《大话》实现(与王道类似):
/* 顺序栈结构 */
typedef struct
{
SElemType data[MAXSIZE];
int top; /* 用于栈顶指针 */
}SqStack;
名词解释:
typedef struct SNode *Stack
typedef
用于为现有的类型定义一个新的别名;
struct SNode *Stack;
定义了一个名为Stack
的类型,它是struct SNode
类型的指针;通过这种方式,你可以使用
Stack
类型来声明指向SNode
结构体的指针。
struct SNode { ElementType Data[MaxSize]; int Top; }
struct SNode
定义了一个结构体类型,用于表示栈的数据结构;
ElementType Data[MaxSize];
定义了一个数组Data
,用于存储栈中的元素。ElementType
表示栈中元素的数据类型。例如,如果栈中存储的是整数,则ElementType
可能被定义为int;
int Top;
定义了一个整型变量Top
,用于表示栈顶元素的位置索引。当栈为空时,Top
的值通常为-1
。
操作及实现
初始化
《大话》实现:
/* 构造一个空栈S */
Status InitStack(SqStack *S)
{
/* S.data=(SElemType *)malloc(MAXSIZE*sizeof(SElemType)); */
S->top=-1;
return OK;
}
王道实现:
void InitStack(SqStack &S){
S.top = -1; //初始化栈顶指针
}
名词解释:
使用引用
&S
:
- 当你使用引用
&S
时,函数参数S
实际上是一个指向SqStack
对象的引用。这意味着当你在函数内部修改S
时,实际上是在修改原始的SqStack
对象;- 使用引用可以避免复制整个
SqStack
对象,这对于较大的结构体来说可以节省内存和提高效率;- 引用确保你在函数内部所做的修改会反映到调用函数的上下文中。
使用指针
*S
:
- 如果你使用指针
*S
,那么S
将是一个指向SqStack
的指针。这意味着你需要通过指针来访问和修改SqStack
的成员;- 如果你使用指针,那么你需要使用间接访问运算符
->
而不是成员访问运算符.
来访问SqStack
的成员;- 使用指针可以让你传递
SqStack
的地址,但在函数内部,你需要确保指针不是NULL
并且正确地使用指针。
举个例子:
使用引用传递:
int main() {
SqStack stack;
InitStack(stack); // 使用引用传递
printf("Top index: %d\n", stack.top); // 输出应为 -1
return 0;
}
使用指针传递:
int main() {
SqStack stack;
InitStack(&stack); // 使用指针传递
printf("Top index: %d\n", stack.top); // 输出应为 -1
return 0;
}
判空
《大话》实现:
/* 若栈S为空栈,则返回TRUE,否则返回FALSE */
Status StackEmpty(SqStack S)
{
if (S.top==-1)
return TRUE;
else
return FALSE;
}
王道实现:
bool StackEmpty(SqStack S){
if(S.top == -1 ) //栈空
return true;
else
return false;
}
求栈长
《大话》实现:
/* 返回S的元素个数,即栈的长度 */
int StackLength(SqStack S)
{
return S.top+1;
}
入栈(Push)
陈越实现:
void Push( Stack PtrS, ElementType item )
{
if ( PtrS->Top == MaxSize-1 ) {
printf(“堆栈满”); return;
}else {
PtrS->Data[++(PtrS->Top)] = item;
return;
}
}
名词解释:
void Push(Stack PtrS, ElementType item)
void
表示函数不返回任何值。Push
是函数名。(Stack PtrS, ElementType item)
表示函数接收两个参数:一个Stack
类型的指针PtrS
和一个ElementType
类型的变量item
。
《大话》实现:
/* 插入元素e为新的栈顶元素 */
Status Push(SqStack *S,SElemType e)
{
if(S->top == MAXSIZE -1) /* 栈满 */
{
return ERROR;
}
S->top++; /* 栈顶指针增加一 */
S->data[S->top]=e; /* 将新插入元素赋值给栈顶空间 */
return OK;
}
王道实现:
bool Push(SqStack &S, ElemType x){
if(S.top == Maxsize - 1)
return false;
S.data[++S.top]=x; //指针先+1,后入栈
return true;
}
名词解释:
bool Push(SqStack &S, ElemType x)
其作用是将一个元素
x
压入基于数组实现的栈SqStack
中。如果栈已满,函数返回false
,表示压栈操作失败。否则,它将栈顶指针加一并将元素存入栈中,然后返回true
表示成功。由于函数返回一个布尔值,因此返回类型为bool
。
出栈(Pop)
陈越实现:
ElementType Pop( Stack PtrS )
{
if ( PtrS->Top == -1 ) {
printf(“堆栈空”);
return ERROR; /* ERROR是ElementType的特殊值,标志错误
*/
} else
return ( PtrS->Data[(PtrS->Top)--] );
}
《大话》实现:
/* 若栈不空,则删除S的栈顶元素,用e返回其值,并返回OK;否则返回ERROR */
Status Pop(SqStack *S,SElemType *e)
{
if(S->top==-1)
return ERROR;
*e=S->data[S->top]; /* 将要删除的栈顶元素赋值给e */
S->top--; /* 栈顶指针减一 */
return OK;
王道实现:
bool Pop(SqStack &S, ElemType &x){
if(S.top == - 1)
return false;
x=S.data[S.top--]; //指针先-1,后入栈
return true;
}
读栈
读栈顶
《大话》实现:
/* 从栈底到栈顶依次对栈中每个元素显示 */
Status StackTraverse(SqStack S)
{
int i;
i=0;
while(i<=S.top)
{
visit(S.data[i++]);
}
printf("\n");
return OK;
}
共享栈
引子
定义
结构代码实现
陈越实现:
#define MaxSize <存储数据元素的最大个数>
struct DStack {
ElementType Data[MaxSize];
int Top1; /* 堆栈1的栈顶指针 */
int Top2; /* 堆栈2的栈顶指针 */
} S;
《大话》实现:
/* 两栈共享空间结构 */
typedef struct
{
SElemType data[MAXSIZE];
int top1; /* 栈1栈顶指针 */
int top2; /* 栈2栈顶指针 */
}SqDoubleStack;
操作及实现
初始化
《大话》实现:
/* 构造一个空栈S */
Status InitStack(SqDoubleStack *S)
{
S->top1=-1;
S->top2=MAXSIZE;
return OK;
}
判栈空
《大话》实现:
/* 若栈S为空栈,则返回TRUE,否则返回FALSE */
Status StackEmpty(SqDoubleStack S)
{
if (S.top1==-1 && S.top2==MAXSIZE)
return TRUE;
else
return FALSE;
}
求栈长
《大话》实现:
/* 返回S的元素个数,即栈的长度 */
int StackLength(SqDoubleStack S)
{
return (S.top1+1)+(MAXSIZE-S.top2);
}
入栈
《大话》实现:
/* 插入元素e为新的栈顶元素 */
Status Push(SqDoubleStack *S,SElemType e,int stackNumber)
{
if (S->top1+1==S->top2) /* 栈已满,不能再push新元素了 */
return ERROR;
if (stackNumber==1) /* 栈1有元素进栈 */
S->data[++S->top1]=e; /* 若是栈1则先top1+1后给数组元素赋值。 */
else if (stackNumber==2) /* 栈2有元素进栈 */
S->data[--S->top2]=e; /* 若是栈2则先top2-1后给数组元素赋值。 */
return OK;
}
出栈
《大话》实现:
/* 若栈不空,则删除S的栈顶元素,用e返回其值,并返回OK;否则返回ERROR */
Status Pop(SqDoubleStack *S,SElemType *e,int stackNumber)
{
if (stackNumber==1)
{
if (S->top1==-1)
return ERROR; /* 说明栈1已经是空栈,溢出 */
*e=S->data[S->top1--]; /* 将栈1的栈顶元素出栈 */
}
else if (stackNumber==2)
{
if (S->top2==MAXSIZE)
return ERROR; /* 说明栈2已经是空栈,溢出 */
*e=S->data[S->top2++]; /* 将栈2的栈顶元素出栈 */
}
return OK;
}
读栈
《大话》实现:
Status StackTraverse(SqDoubleStack S)
{
int i;
i=0;
while(i<=S.top1)
{
visit(S.data[i++]);
}
i=S.top2;
while(i<MAXSIZE)
{
visit(S.data[i++]);
}
printf("\n");
return OK;
}
链栈
定义
栈需要有栈顶指针,而单链表又可以有头指针,这启发我们可以将两者合二为一。
此时头结点失去其原有意义。至于为什么不取其他位置的指针,很简单,因为如果涉及插入和删除操作,不方便且有丢失数据逻辑关系的可能。
结构代码实现
陈越实现:
typedef struct SNode *Stack;
struct SNode{
ElementType Data;
struct SNode *Next;
} ;
《大话》实现:
/* 链栈结构 */
typedef struct StackNode
{
SElemType data;
struct StackNode *next;
}StackNode,*LinkStackPtr;
typedef struct
{
LinkStackPtr top;
int count;
}LinkStack;
名词解释:
StackNode,*LinkStackPtr;
StackNode
是struct StackNode
的类型别名。*LinkStackPtr;
定义了LinkStackPtr
类型,它是一个指向StackNode
类型的指针。这意味着LinkStackPtr
可以用于声明指向栈节点的指针。
王道实现:
typedef struct Linknode{
ElemType data; //数据域
struct Linknode *next; 指针域
}LiStack; 栈类型定义
操作及实现
初始化
《大话》实现:
/* 构造一个空栈S */
Status InitStack(LinkStack *S)
{
S->top = (LinkStackPtr)malloc(sizeof(StackNode));
if(!S->top)
return ERROR;
S->top=NULL;
S->count=0;
return OK;
}
p = (int *)malloc(sizeof(int))
- 分配足够的内存来存储一个整数;
- 这里的星号 * 在 malloc 函数调用的结果后面,用于类型转换;
- (int *) 是类型转换操作,将 malloc 返回的 void * 类型转换为 int * 类型,使得 p 可以安全地指向这块内存。
为什么这里不用加 * ?
S->top
是一个指向StackNode
的指针变量;LinkStackPtr
已经是一个指向StackNode
类型的指针类型;LinkStackPtr
相当于struct StackNode *。
判栈空
陈越实现:
int IsEmpty(Stack S)
{ /*判断堆栈S是否为空,若为空函数返回整数1,否
则返回0 */
return ( S->Next == NULL );
}
《大话》实现:
/* 若栈S为空栈,则返回TRUE,否则返回FALSE */
Status StackEmpty(LinkStack S)
{
if (S.count==0)
return TRUE;
else
return FALSE;
}
求栈长
《大话》实现:
/* 返回S的元素个数,即栈的长度 */
int StackLength(LinkStack S)
{
return S.count;
}
进栈
陈越实现:
void Push( ElementType item, Stack S)
{ /* 将元素item压入堆栈S */
struct SNode *TmpCell;
TmpCell=(struct SNode *)malloc(sizeof(struct SNode));
TmpCell->Element = item;
TmpCell->Next = S->Next;
S->Next = TmpCell;
}
《大话》实现:
/* 插入元素e为新的栈顶元素 */
Status Push(LinkStack *S,SElemType e)
{
LinkStackPtr s=(LinkStackPtr)malloc(sizeof(StackNode));
s->data=e;
s->next=S->top; /* 把当前的栈顶元素赋值给新结点的直接后继,见图中① */
S->top=s; /* 将新的结点s赋值给栈顶指针,见图中② */
S->count++;
return OK;
}
出栈
陈越实现:
ElementType Pop(Stack S)
{ /* 删除并返回堆栈S的栈顶元素 */
struct SNode *FirstCell;
ElementType TopElem;
if( IsEmpty( S ) ) {
printf(“堆栈空”); return NULL;
} else {
FirstCell = S->Next;
S->Next = FirstCell->Next;
TopElem = FirstCell ->Element;
free(FirstCell);
return TopElem;
}
}
《大话》实现:
/* 若栈不空,则删除S的栈顶元素,用e返回其值,并返回OK;否则返回ERROR */
Status Pop(LinkStack *S,SElemType *e)
{
LinkStackPtr p;
if(StackEmpty(*S))
return ERROR;
*e=S->top->data;
p=S->top; /* 将栈顶结点赋值给p,见图中③ */
S->top=S->top->next; /* 使得栈顶指针下移一位,指向后一结点,见图中④ */
free(p); /* 释放结点p */
S->count--;
return OK;
}
读栈
《大话》实现:
Status StackTraverse(LinkStack S)
{
LinkStackPtr p;
p=S.top;
while(p)
{
visit(p->data);
p=p->next;
}
printf("\n");
return OK;
}
队列
定义
队列是只允许在一端进行插入操作,而在另一端进行删除操作的线性表;
允许插入的一头叫队尾,允许删除的一头叫队尾;
队列是一种先进先出(First In First Out)的线性表,简称 FIFO 结构。
抽象数据类型描述
- Queue CreatQueue( int MaxSize ):生成长度为MaxSize的空队列;
- int IsFullQ( Queue Q, int MaxSize ):判断队列Q是否已满;
- void AddQ( Queue Q, ElementType item ): 将数据元素item插入队列Q中;
- int IsEmptyQ( Queue Q ): 判断队列Q是否为空;
- ElementType DeleteQ( Queue Q ):将队头数据元素从队列中删除并返回。
顺序存储结构
定义
为什么要引入 front 和 rear?
让我们想一下,当执行入列操作时是在队尾,,直接插入,不需要移动其他任何元素,时间复杂度低;但是,在执行出列操作时,按以往案例,似乎需要删除头结点,同时所有节点向前移动,时间复杂度较高。
怎么办呢?
不妨想想,假设对头不一定要在头结点位置,同时为了方便解决当只有一个数据元素时对头队尾重合的问题(难以判断是空还是只有一个),我们在出队时选择直接“原地”消失队头,同时引入 front 指针指向队头元素,rear 指针指向队尾的下一个位置。
这样当然还会有新的问题,比如但我们进行入列和出列,使得前面几个位置空出,后面几个位置占满,此时 rear就会超出数组之外,指向非法位置,但此时空间并没有全部利用,存在“伪溢出”。
如何解决这个问题呢,很简单,头尾相接。
循环队列
定义
循环队列
队列中的头尾相接的顺序存储结构(有点拗口哈哈);
即把出存储队列元素的表从逻辑上视为一个环;
队列空时,front = rear ;队列满时,选择空出一个元素空间,使得( rear + 1 )% QueueSize == front 。
补充:
通用的计算队列长度的公式:
( rear - front + QueueSize )% QueueSize
结构代码实现
《大话》实现:
/* 循环队列的顺序存储结构 */
typedef struct
{
QElemType data[MAXSIZE];
int front; /* 头指针 */
int rear; /* 尾指针,若队列不空,指向队列尾元素的下一个位置 */
}SqQueue;
Status visit(QElemType c)
{
printf("%d ",c);
return OK;
}
操作及实现
初始化
《大话》实现:
/* 初始化一个空队列Q */
Status InitQueue(SqQueue *Q)
{
Q->front=0;
Q->rear=0;
return OK;
}
王道实现:
void IniQueue(SqQueue &Q){
Q.rear = Q.front = 0; //初始化队头、队尾指针
}
判队空
《大话》实现:
/* 若队列Q为空队列,则返回TRUE,否则返回FALSE */
Status QueueEmpty(SqQueue Q)
{
if(Q.front==Q.rear) /* 队列空的标志 */
return TRUE;
else
return FALSE;
}
王道实现:
bool isEmpty(SqQueue Q){
if (Q.rear == Q.front)
return true;
else
return false;
}
入队
《大话》实现:
/* 若队列未满,则插入元素e为Q新的队尾元素 */
Status EnQueue(SqQueue *Q,QElemType e)
{
if ((Q->rear+1)%MAXSIZE == Q->front) /* 队列满的判断 */
return ERROR;
Q->data[Q->rear]=e; /* 将元素e赋值给队尾 */
Q->rear=(Q->rear+1)%MAXSIZE;/* rear指针向后移一位置, */
/* 若到最后则转到数组头部 */
return OK;
}
王道实现:
void EnQueue(LinkQueue &Q,ElemType x){
LinkNode *s = (LinkNode *)malloc(sizeof(LinkNode));
s->data = x;
s->next = NULL;
Q.rear->next = s; //插入队尾
Q.rear = s; //修改队尾指针
}
Q.rear->next = s;
目的:将新节点
s
插入到队列的末尾。作用:设置当前队尾节点
Q.rear
的next
指针指向新节点s
。
Q.rear = s;
目的:更新队尾指针
Q.rear
,使其指向新加入的节点s
。作用:将
Q.rear
设置为新节点s
,确保Q.rear
指向队列的最后一个节点。
出队
《大话》实现:
/* 若队列不空,则删除Q中队头元素,用e返回其值 */
Status DeQueue(SqQueue *Q,QElemType *e)
{
if (Q->front == Q->rear) /* 队列空的判断 */
return ERROR;
*e=Q->data[Q->front]; /* 将队头元素赋值给e */
Q->front=(Q->front+1)%MAXSIZE; /* front指针向后移一位置, */
/* 若到最后则转到数组头部 */
return OK;
}
王道实现:
bool DeQueue(LinkQueue &Q,ElemType &x){
if(Q.front == Q.rear)
return false;
LinkNode *q = Q.front->next; //创建一个临时指针 q,用于指向删除后的队首节点。
x = p->data;
Q.front->next = p->next;
if(Q.rear == p)
Q.rear = Q.front; //若原队列只有一个结点,删除后变空
free(p);
return true;
}
读队
《大话》实现:
/* 从队头到队尾依次对队列Q中每个元素输出 */
Status QueueTraverse(LinkQueue Q)
{
QueuePtr p;
p=Q.front->next;
while(p)
{
visit(p->data);
p=p->next;
}
printf("\n");
return OK;
}
链式存储结构
链队列,即使用尾进头出的单链表,插入和删除分别在链表的两头进行。
结构代码实现
陈越实现:
struct Node{
ElementType Data;
struct Node *Next;
};
struct QNode{ /* 链队列结构 */
struct Node *rear; /* 指向队尾结点 */
struct Node *front; /* 指向队头结点 */
};
typedef struct QNode *Queue;
Queue PtrQ;
《大话》实现:
typedef struct QNode /* 结点结构 */
{
QElemType data;
struct QNode *next;
}QNode,*QueuePtr;
typedef struct /* 队列的链表结构 */
{
QueuePtr front,rear; /* 队头、队尾指针 */
}LinkQueue;
操作及实现
初始化
/* 构造一个空队列Q */
Status InitQueue(LinkQueue *Q)
{
Q->front=Q->rear=(QueuePtr)malloc(sizeof(QNode));
if(!Q->front)
exit(OVERFLOW);
Q->front->next=NULL;
return OK;
}
判队空
/* 若Q为空队列,则返回TRUE,否则返回FALSE */
Status QueueEmpty(LinkQueue Q)
{
if(Q.front==Q.rear)
return TRUE;
else
return FALSE;
}
求队长
/* 求队列的长度 */
int QueueLength(LinkQueue Q)
{
int i=0;
QueuePtr p;
p=Q.front;
while(Q.rear!=p)
{
i++;
p=p->next;
}
return i;
}
入队
/* 插入元素e为Q的新的队尾元素 */
Status EnQueue(LinkQueue *Q,QElemType e)
{
QueuePtr s=(QueuePtr)malloc(sizeof(QNode));
if(!s) /* 存储分配失败 */
exit(OVERFLOW);
s->data=e;
s->next=NULL;
Q->rear->next=s; /* 把拥有元素e的新结点s赋值给原队尾结点的后继,见图中① */
Q->rear=s; /* 把当前的s设置为队尾结点,rear指向s,见图中② */
return OK;
}
出队
陈越实现:
//不带头结点的链式队列出队操作的一个示例
ElementType DeleteQ ( Queue PtrQ )
{ struct Node *FrontCell;
ElementType FrontElem;
if ( PtrQ->front == NULL) {
printf(“队列空”); return ERROR;
}
FrontCell = PtrQ->front;
if ( PtrQ->front == PtrQ->rear) /* 若队列只有一个元素 */
PtrQ->front = PtrQ->rear = NULL; /* 删除后队列置为空 */
else
PtrQ->front = PtrQ->front->Next;
FrontElem = FrontCell->Data;
free( FrontCell ); /* 释放被删除结点空间 */
return FrontElem;
}
《大话》实现:
/* 若队列不空,删除Q的队头元素,用e返回其值,并返回OK,否则返回ERROR */
Status DeQueue(LinkQueue *Q,QElemType *e)
{
QueuePtr p;
if(Q->front==Q->rear)
return ERROR;
p=Q->front->next; /* 将欲删除的队头结点暂存给p,见图中① */
*e=p->data; /* 将欲删除的队头结点的值赋值给e */
Q->front->next=p->next;/* 将原队头结点的后继p->next赋值给头结点后继,见图中② */
if(Q->rear==p) /* 若队头就是队尾,则删除后将rear指向头结点,见图中③ */
Q->rear=Q->front;
free(p);
return OK;
}
读队
/* 从队头到队尾依次对队列Q中每个元素输出 */
Status QueueTraverse(LinkQueue Q)
{
QueuePtr p;
p=Q.front->next;
while(p)
{
visit(p->data);
p=p->next;
}
printf("\n");
return OK;
}
双端队列
参考
王道《数据结构》;
程杰《大话数据结构》;
陈越《数据结构》。