文章目录
前言
栈和队列是两种十分常见且十分重要的数据结构,学到了这里那么恭喜你,你已经具备初步编写算法的能力了。深入理解栈和队列能有效提高我们的编程思维,他们虽然有自己的名字,但本质上都是线性表,只不过是带限制的线性表
一、栈(LIFO)
想象一个管子,我们往里面丢硬币,这个管子只有一个开口,也就是说当你存硬币到一定程度想拿出来用的话,最后放进去的那个是最先拿出来的,这种只能后进先出(last In First Out)的线性表我们称他为栈,简称LIFO。我们把进入元素的那一端叫做栈顶,压在底下的叫做栈底,插入元素叫入栈(压栈),删除元素叫出栈(弹栈)
(一)顺序栈
顺序栈顾名思义就是用顺序表实现的栈
存储结构
typedef int SElemType;
typedef struct{
SElemType *base, *top;
int stacksize;
}SqStack;
操作
1. 顺序栈的初始化
初始化S.top=S.base,top指向的是栈顶元素上的第一个空位
Status InitStack(SqStack &S){
S.base = new SElemType[MAXSIZE];
if(!S.base) exit(OVERFLOW);
S.top = S.base;
S.stacksize = MAXSIZE;
return OK;
}
2. 判断顺序栈是否为空
Status StackEmpty(SqStack S){
if(S.top == S.base) return TRUE;
return FALSE:
}
3. 获取顺序栈长度
int StackLength(SqStack S){
return S.top - S.base;
}
4. 清空顺序栈
Status ClearStack(SqStack S){
if(S.base) S.top = S.base;
return Ok;
}
5. 销毁顺序栈
Status DestoryStack(SqStack &S){
if(S.base){
delete S.base;
S.stacksize = 0;
S.top = S.base = NULL:
}
return OK;
}
6. 顺序栈的入栈
对于一个顺序栈,想要入栈,首先要考虑栈是否满了,如果没满,由于top指向的是栈顶元素上的第一个空位置,所以直接将e存入top指向的空间,随后将top+1,这两句写成一句示例如下
Status Push(SqStack &S, SElemType e){
if(S.top - S.base == S.MAXSIZE) return ERROR:
*S.top++ = e;
return OK;
}
7. 顺序栈的出战
出栈时要考虑是否栈空,非空就先将top向下移一个位置使top指向要弹出的元素,随后将该元素用e带回
Status Pop(SqStack &S, SElemType e){
if(S.top == S.base) return ERROR;
e = *--S.top;
return OK;
}
8. 获取栈顶元素
Status GetTop(SqStack S, SElemType e){
if(S.top == S.base) return ERROR;
e = *S.top;
return OK;
}
(二)链栈
对于链栈,基于链表的栈
存储结构
typedef struct StackNode{
SElemType data;
StackNode *next
}*LinkStack;
操作
链栈的大多数操作和顺序栈一样,这里除要讲讲一些特别的
1. 链栈的初始化
链栈的指针指向第一个元素,只要我们在入栈时保证链表使头插法,就可以完美实现链栈,这给我们带来了极大的便利
Status InitStack(LinkStack &S){
S = NULL;
return OK;
}
2. 判断链栈是否为空
Status StackEmpty(LinkStack &S){
if(S == NULL) return TRUE;
return FALSE;
}
3. 入栈
Status Push(LinkStack &S, SElemType e){
p = new StakcNode;
p -> data = e;
p -> next = s;
s = p;
return OK;
}
4. 出栈
Status Pop(LinkStack &S, SElemType e){
if(S == NULL) return ERROR;
e = s -> data;
p = new StackNode;
p = s;
s = s -> next;
delete p;
return OK;
}
二、队列(FIFO)
想象你在肯德基买早餐,你看着前面的人一个个拿到了帕尼尼,因为他们比你早到,这种先进先出(First In First Out)的数据结构就叫做队列。(最后你也拿到了帕尼尼,因为到你出队了)
(一)顺序队列
任何顺序存储结构我时常面临溢出的风险,考虑到队列出队和入队不是一个方向,如果队尾到了数组末端,但队头又出去了一些元素,这个时候就会出现队列的假溢出,是个人都会把数据存到前面空出的位置,但是这引入了一个新问题,如何判断队满和队空,目前市面上主流的方法主要是设置一个flag来表示是空还是满,另一种是浪费一个存储空间,我们不存满数组,这样也有计算的余地,后面我们主要使用第二种方法
存储结构
一个顺序队列,我们需要一个base指针来表示存放数据的数组位置,然后两个游标分别表示队列的头和尾
typedef int QElemType
# define MAXSIZE100;
typedef struct{
QElemType *Base;
int front, rear;
}SqQueue
操作
1. 初始化循环队列
Status InitQueue(SqQueue &Q){
Q.base = new QElemType[MAXSIZE];
if(!Q.base) exit(OVERFLOW);
Q.front = Q.rear = 0;
return OK;
}
2. 获取循环队列长度
由于我们的线性队列是循环队列,因而不能直接用rear减去front,要对他们的差加上数组长度再取模,这样得到的才是队列的长度
int QueueLength(SqQueue Q){
return (Q.front - Q.rear + MAXSIZE) % MAXSIZE;
}
3. 获取队头元素
QElemType GetHead(SqQueue Q){
if(Q.front == Q.rear) return ERROR;
return Q.base[Q.front];
}
4. 入队
因为我们留空了一个数组元素,所以(Q.rear + 1) % MAXSIZE == Q.front)就变成了判断对满的条件。在更新尾指针时,要注意位置溢出,所以要去一个模
Status EnQueue(SqQueue &Q, QElemType e){
if((Q.rear + 1) % MAXSIZE == Q.front) return ERROR;
Q.base[Q.rear] = e;
Q.rear = (Q.rear + 1) % MAXSIZE;
return OK;
}
5. 出队
(Q.rear == Q.front)是判断队空的条件
Status DeQueue(SqQueue &Q, QElemType e){
if(Q.rear == Q.front) return ERROR;
e = Q.base[Q.front];
Q.front = (Q.front + 1) % MAXSIZE;
return OK;
}
6. 清空循环队列
Status ClearQueue(SqQueue &Q){
Q.front = Q.rear = 0;
return OK;
}
7. 判断队列是否为空
Status QueueEmpty(SqQueue Q){
if(Q.front == Q.rear) return TRUE;
return FALSE:
}
(二)链队
存储结构
链队的存储结构有些特殊,节点一个结构体,指针又要一个结构体,区别于链栈是因为链栈只需要一个指针只要保证头插法就好,而链队需要两个指针
typedef struct QNode{
QElemType data;
QNode *next;
}*QueuePtr;
typedef struct{
QueuePtr front, rear;
}LinkQueue
操作
1. 链队的初始化
Status InitQueue(LinkQueue &Q){
Q.front = Q.rear = new QNode;
if(!Q.front) exit(OVERFLOW);
Q.front -> next = NULL;
return OK;
}
2. 清空链队
清空链队我们不仅需要把两个游标归零,还需要把中间各个节点占用的内存释放掉,所以通过声明两个指针p,q逐个删除节点
Status ClearQueue(LinkQueue &Q){
QueuePtr p, q;
Q.rear = Q.front;
p = Q,front -> next;
Q.front -> next = NULL;
while(p){
q = p;
p = p -> next;
delete q;
}
return OK;
}
3. 销毁链队
Status DestoryQueue(LinkQueue &Q){
while(Q.front){
Q.rear = Q.front -> next;
delete Q.front;
Q.front = Q.rear;
}
return Ok
}
4. 判断队空
Status QueueEmpty(LinkQueue Q){
if(Q.front == Q.rear) return TRUE;
return FALSE;
}
5. 取队头元素
front指向的是队头元素前面的头结点,也就是说front指向的是一个数据域为空的节点,他的下一个节点才是即将要出队的元素
Status GetHead(LinkQueue Q, QElemType e){
if(Q.front == Q.rear) return ERROR;
QueuePtr p = Q.front -> next;
e = p -> data;
return OK;
}
6. 入队
Q.rear是为节点的地址,通过他我们将该节点的next指针设置为s,同时我们更新Q.rear为s
Status EnQueue(LinkQueeu &Q, QElemType e){
p = new QNode;
if(!p) exit(OVERFLOW);
p -> data = e;
Q.rear -> next = s;
Q.rear = s;
return OK;
}
7. 出队
链队出队时,首先获取即将出队的元素地址,然后将元素的数据用e带回,接着更新Q.front的next直向出队元素p的下一个元素,最后判断出队后是否队空,如果队空了,就需要更新Q.rear为Q.front,因为此时尾游标指向了一个不存在的节点
Status DeQueeu(LinkQueue &Q, QElemType e){
if(Q.front == Q.rear) return ERROR;
QueuePtr p;
p = Q.front -> next;
e = p -> data;
Q.front -> next = p -> next;
if(Q.rear == p) Q.rear = Q.front;
delete p;
return OK;
}