目录
前言:
数据结构有:顺序表、链表、栈、队列、二叉树等等,栈和队列的实现和功能对与顺序表、链表来说有很大不同,而且要完成它们之间的相互实现,今天就让我们来看看它们两个的特殊之处。
栈
栈的概念及结构:
栈:一种特殊的线性表,其只允许在固定的一端进行插入和删除元素操作。进行数据插入和删除操作的一端称为栈顶,另一端称为栈底。栈中的数据元素遵守后进先出LIFO(Last In First Out)的原则。
压栈:栈的插入操作叫做进栈/压栈/入栈,入数据在栈顶。
出栈:栈的删除操作叫做出栈。出数据也在栈顶
栈里的操作有以下这些:
- 初始化栈
- 入栈
- 出栈
- 获取栈顶元素
- 获取栈中有效元素个数
- 检测栈是否为空,如果为空返回非零结果,如果不为空返回0
- 销毁栈
栈的实现
栈一般可以使用链表或数组实现,但是根据栈的后进先出,栈并不需要头部的插入或删除,所以相对而言数组的结构实现更优一些。
我们要实现一个由数组实现的栈,首先我们要创建应该结构体,里面应该有一个数组、一个整形类型_top便与我们得到栈顶元素和一个整形类型_capacity来与我们的数组的大小做对比以此来判断是否要扩容。所以结构体的代码如下:
typedef int STDataType;//便于我们以后要修改栈的类型
typedef struct Stack
{
STDataType* _a;
int _top; // 栈顶
int _capacity; // 容量
}Stack;
1、初始化栈
我们要改变栈,那我们就要传栈的地址。
// 初始化栈
void StackInit(Stack* ps)
{
assert(ps);
ps->_a = NULL;
ps->_capacity = 0;
ps->_top = 0;
}
注意:ps->_top = 0 ,_top的位置是栈顶元素的下一个,如果要取当前元素的时候要_top要--。
2、入栈
在我们要入栈的时候,首先我们要判断一下栈是否满了,如果满了,我们就要扩容,反之,就直接入栈。( 当ps->top == ps->_capacity的时候就说明我们要扩容了)
// 入栈
void StackPush(Stack* ps, STDataType data)
{
assert(ps);
if (ps->_capacity == ps->_top)
{
//取扩容之后的栈的容量的大小
int capacity = ps->_capacity == 0 ? 4 : ps->_capacity * 2;
//创建一个新的、大的数组
STDataType* new = realloc(ps->_a, sizeof(STDataType) * capacity);
if (new == NULL)
{
perror("StackPush()::realloc faile");
return;
}
ps->_a = new;
ps->_capacity = capacity;
}
ps->_a[ps->_top] = data;
ps->_top++;
}
3、出栈
当我们要出栈的时候,直接ps->_top -- 就可以了,不过要确保栈里面还有元素。
// 出栈
void StackPop(Stack* ps)
{
assert(ps);
assert(ps->_top > 0);
ps->_top--;
}
4、获取栈顶元素
正如我上面所述,我这的ps->_top是栈顶元素的下一个,我们要ps->_top - 1才能得到栈顶元素。
// 获取栈顶元素
STDataType StackTop(Stack* ps)
{
assert(ps);
return ps->_a[ps->_top - 1];
}
5、获取栈中有效元素个数
这里设_top是0的好处就它可以也代表有效元素个数。
STDataType StackTop(Stack* ps)
{
assert(ps);
return ps->_a[ps->_top - 1];
}
6、判空
检测栈是否为空,如果为空返回非零结果,如果不为空返回0 。
// 检测栈是否为空,如果为空返回非零结果,如果不为空返回0
int StackEmpty(Stack* ps)
{
assert(ps);
return ps->_top == 0;
}
7、销毁栈
// 销毁栈
void StackDestroy(Stack* ps)
{
assert(ps);
free(ps->_a);
ps->_a = NULL;
ps->_capacity = ps->_top = 0;
}
综上,我们的栈就构建好了,现在看看栈的实现效果:
int main()
{
Stack s;//创建一个栈
StackInit(&s);//初始化栈
StackPush(&s, 1);//进栈
StackPush(&s, 2);
StackPush(&s, 3);
StackPush(&s, 4);
StackPush(&s, 5);
while (!StackEmpty(&s))//当栈为空的时候就退出
{
STDataType top = StackTop(&s);//得到栈顶的元素
StackPop(&s);//出栈
printf("%d ", top);//打印栈顶的元素
}
StackDestroy(&s);//销毁栈
return 0;
}
运行结果:
队列
队列的概念及结构:
队列:只允许在一端进行插入数据操作,在另一端进行删除数据操作的特殊线性表,队列具有先进先出FIFO(First in First Out)
入队列:进行插入操作的一端称为队尾。
出队列:进行删除操作的一端称为队头。
队列里的操作有以下这些:
- 初始化队列
- 队尾入队列
- 队头出队列
- 获取队列头部元素
- 获取队列队尾元素
- 获取队列中有效元素个数
- 检测队列是否为空,如果为空返回非零结果,如果非空返回0
- 销毁队列
队列的实现
队列也可以数组和链表的结构实现,使用链表的结构实现更优一些,因为如果使用数组的结构,出队列在数组头上出数据,效率会比较低。
我们要实现一个由链表实现的队列,首先我们要创建应该两个结构体,一个结构体来存放单个节点的数据,另一个结构体来存放头尾指向的结构体,便于我们以后的入队列和出队列,存放单个节点数据的结构体跟常规的单链表结构体基本一样,就是多了一个整形类型_size来计算队列里的元素个数。所以结构体的代码如下:
typedef int QDataType;//便于我们以后修改队列存放的类型
//单个节点的结构
typedef struct QListNode
{
struct QListNode* _next;
QDataType _data;
_size;
}QNode;
// 队列的结构
typedef struct Queue
{
QNode* _front;//头节点
QNode* _rear;//尾节点
}Queue;
1、初始化队列
和栈一样,我们要改变队列,那我们就要传队列的地址。
void QueueInit(Queue* q)
{
assert(q);
q->_front = NULL;//初始化头节点
q->_rear = NULL;//初始化尾节点
q->_size = 0;
}
2、队尾入队列
队尾入队列的时候要分下面两种情况;
头节点和尾节点都是空指针的时候:malloc出一个QNode节点,然后让头尾节点都指向它。
头节点和尾节点不是空指针的时候:头节点保持不动,malloc出一个QNode节点,让尾节点的next指向它,然后改变尾节点。
// 队尾入队列
void QueuePush(Queue* q, QDataType data)
{
assert(q);
QNode* newnode = (QNode*)malloc(sizeof(QNode));
if (newnode == NULL)
{
perror("BuyNode()::malloc faile");
return NULL;
}
newnode->_data = data;
newnode->_next = NULL;
if (q->_front == NULL)
{
q->_front = q->_rear = newnode;
}
else
{
q->_rear->_next = newnode;
q->_rear = newnode;
}
q->_size++;
}
3、队头出队列
当我们要队头出队列的时候,最开始确保队列里面有节点,然后判断一下队列是否只有一个节点的情况,不然可能会漏free尾节点,会使它变为野指针。如果有多个节点,用一个临时变量来存储头节点,然后让头节点向后移一位,以及free(临时变量);如果只有一个节点直接free(头节点或尾节点)(因为此时它们的地址是一样的),最后置空头尾节点。
// 队头出队列
void QueuePop(Queue* q)
{
assert(q);
assert(!QueueEmpty(q));
if (q->_front->_next == NULL)//队列里只有一个节点的时候
{
free(q->_front);
q->_front = q->_rear = NULL;
}
else//队列里有多个节点的时候
{
QNode* a = q->_front;
q->_front = q->_front->_next;
free(a);
a = NULL;
}
}
4、获取队列头部元素
这里就很简单了,我们可以直接从头节点获得队列的头部元素。(记得确保队列里面有节点)
// 获取队列头部元素
QDataType QueueFront(Queue* q)
{
assert(q);
assert(!QueueEmpty(q));
return q->_front->_data;
}
5、获取队列队尾元素
同上。我们可以直接从尾节点获得队列的尾部元素。(记得确保队列里面有节点)
// 获取队列队尾元素
QDataType QueueBack(Queue* q)
{
assert(q);
assert(!QueueEmpty(q));
return q->_rear->_data;
}
6、获取队列中有效元素个数
由于Qnode结构体里面有_size成员,所以我们可以很方便的得到队列中有效元素个数。
QDataType QueueBack(Queue* q)
{
assert(q);
assert(!QueueEmpty(q));
return q->ptail->_size;
}
7.判空
当头尾节点都为空指针的时候或它们其中一个为空指针的时候队列都为空。
// 检测队列是否为空,如果为空返回非零结果,如果非空返回0
int QueueEmpty(Queue* q)
{
assert(q);
return q->_front == NULL;
}
8、销毁队列
我们可以创建一个临时变量指向头节点,然后通过循环遍历一遍来free每个节点。
void QueueDestroy(Queue* q)
{
assert(q);
QNode* cur = q->_front;
while (cur)
{
QNode* next = cur->_next;
free(cur);
cur = NULL;
cur = next;
}
q->_front = q->_rear = NULL;
q->_size = 0;
}
综上,我们的队列就构建好了,现在看看队列的实现效果:
int main()
{
Queue q; //创建一个队列
QueueInit(&q);//初始化队列
QueuePush(&q, 1);//队尾入元素
QueuePush(&q, 2);
QueuePush(&q, 3);
QueuePush(&q, 4);
QueuePush(&q, 5);
QueuePush(&q, 4);
while (!QueueEmpty(&q))
{
int ret = QueueFront(&q);//获取队列的头部元素
QueuePop(&q);//队头出队列
printf("%d ", ret);
}
QueueDestroy(&q);//销毁队列
return 0;
}
运行结果:
用队列实现栈
代码思路:
当我们要模仿栈的后入先出,我们可以创建两个队列。
- 当我们要模拟最重要操作之一入栈的时候,最开始两个队列都是空的,元素随便入一个队列;但运行一段时间后,模拟入栈,要入的元素放在里面有元素的队列里,我们要确保一个队列有数据,一个队列没有数据,以便于模拟出栈的操作。
- 如果要模拟获取栈顶元素的时候,可以从有数据的队列里取队尾的数据,根据此方法,其他的栈的操作也可以一一写出来。
代码实现
1、创建两个队列
在上面我们已经完成了队列的实现,用的时候直接将其拷贝下来就可以使用。(这里我为了简略代码,不然会显的十分臃肿)
//创建两个队列
typedef struct
{
Queue q1;
Queue q2;
}
Mystack;
2、将两个队列初始化
我们不能直接创建队列,要malloc一个变量来存储,不然我们创建队列的是临时变量,出了作用域就没用了。
//初始化队列q1和q2
Mystack* mystackcreate()
{
Mystack*pst=(Mystack*)malloc(sizeof(Mystack));
//初始化队列q1和q2
QueueInit(&(pst->q1));
QueueInit(&(pst->q2));
return pst;
}
3、模拟入栈
如上面代码思路所述:最开始两个队列都是空的,元素随便入一个队列。但运行一段时间后,模拟入栈,要入的元素放在里面有元素的队列里,我们要确保一个队列有数据,一个队列没有数据。
void mystackPush(Mystack*obj,int x)
{
if(!QueueEmpty(&(obj->q1)))//如果队列q1不是空的那就入队列
{
QueuePush(&(obj->q1),x);
}
else//如果队列q1是空的,入队列q2
{
QueuePush(&(obj->q2),x);
}
}
4、模拟出栈
当我们要模拟出栈的时候,因为我们两个队列一个为空empty,一个里面有数据noempty,那我们可以让一直noempty队头出队列直到还剩一个数据的时候就可以模拟出栈,让empty接收noempty队头出队列的数据。
int mystackPop(Mystack* obj)
{
//假设法,就可以不用明确的知道哪个是空的队列,哪一个是有数据的队列
Queue*empty=&(obj->q1);
Queue*nonEmpty =&(obj->q2);
if(!QueueEmpty(&(obj->q1)))
{
nonEmpty=&(obj->q1);
empty =&(obj->q2);
}
//不为空前size-1导走,删出最后一个就是栈顶数据
while(Queuesize(nonEmpty)>1)
{
QueuePush(empty,QueueFront(nonEmpty));
QueuePop(nonEmpty);
}
int top = QueueFront(nonEmpty);QueuePop(nonEmpty);
return top;
}
5、模拟获取栈顶的元素
当q1不为空的时候就取出队尾的元素,反之,取出q2队尾的元素。
int mystackTop(Mystack* obj)
{
if(!QueueEmpty(&(obj->q1)))
{
return QueueBack(&(obj->q1));
}
else
{
return queueBack(&(obj->q2));
}
}
6、判空
当两个队列都为空的时候,模拟栈就为空。
bool myStackEmpty(MyStack* obj)
{
return QueueEmpty(&(obj->q1)) && QueueEmpty(&(obj->q2);
}
7、销毁模拟栈
如果我们直接free(obj),这样可以吗?
void myStackFree(MyStack* obj)
{
free(obj);
}
答案是不行的。下面的是我们创建模拟栈的内存占用图。
当我们free(obj)的时候,q1和q2是被我们free掉了,可是q1和q2开辟的空间没有被free,这就会形成内存泄漏。
所以我们要先销毁队列,再销毁模拟栈。
void myStackFree(MyStack* obj)
{
QueueDistory(&(obj->q1));
QueueDistory(&(obj->q2));
free(obj);
}
最终,用队列模拟栈的实现完成。
用栈实现栈队列
代码思路
用栈实现队列有些不同,我们可以创建两个栈,一个pushst用来入数据,另一个popst用来出数据。
- 当我们要取模拟队列的队头数据的时候,可以先将pushst里的数据全部导入popst里面,然后取popst栈顶的数据。
- 当我们要入数据的时候直接入到pushst里就可以了;
- 当我们要出数据的时候就出popst里的数据,当popst里没有数据可以出的时候再将pushst里的数据导入到popst里,如果出数据的时候,连pushst里都没有数据了,那就说明此时模拟队列已经为空了。
代码实现
1、创建两个栈
这个与用队列实现栈差不多。
//创建两个栈
typedef struct
{
ST pushst;
ST popst;
}
MyQueue;
2、将两个栈初始化
//初始化两个栈
MyQueue* myQueuecreate()
{
MyQueue*obj=(MyQueue*)malloc(sizeof(Myqueue));
STInit(&(obj->pushst));
STInit(&(obj->popst));
return obj;
}
3、模拟队尾入数据
我们直接将数据入到pushst栈里即可。
//入数据
void myQueuePush(MyQueue*obj,int x)
{
STPush(&(obj->pushst),x);
}
4、模拟获取队列头部元素
当栈popst为空的时候,就要把push里的数据全部导入到popst里,便于得到栈底的元素。
int myQueuePeek(MyQueue*obj)
{
if(STEmpty(&(obj->popst)))
{
// 导数据
while(!sTEmpty(&(obj->pushst)))
{
int top =sTTop(&(obj->pushst));
STPush(&(obj->popst),top);
STPop(&(obj->pushst));
}
}
return sTTop(&(obj->popst));
}
5、模拟队头出队列
根据栈的后入先出的特性,在栈popst里栈顶的元素就是模拟队列里的队头的数据,我们StackPop(&popst)即可。
void myQueuePop(MyQueue* obj)
{
assert(myQueueEmpty(obj));
TPop(&(obj->popst));
}
6、判空
当栈pushst和栈popst都为空的时候,模拟队列就为空。
bool myQueueEmpty(MyQueue*obj)
{
return StackEmpty(&(obj->popst))&& StackEmpty(&(obj->pushst));
}
7、销毁模拟队列
与销毁模拟栈一样,不能直接free(obj),我们要先销毁栈,再销毁模拟队列。
void myQueueFree(MyQueue* obj)
{
StackDestroy(&(obj->popst));
StackDestroy(&(obj->pushst));
free(obj);
}
以上就是详解栈和队列的实现以及它们的相互实现的全部内容,博客创作不易,点个赞呗❤
(完)