【Super数据结构】先进先出/后进先出,队列和栈代码实现+应用场景

在这里插入图片描述

🏠关于此专栏:Super数据结构专栏将使用C/C++语言介绍顺序表、链表、栈、队列等数据结构,每篇博文会使用尽可能多的代码片段+图片的方式。
🚪归属专栏:Super数据结构
🎯每日努力一点点,技术累计看得见


老式的手电筒是装电池的,如下图所示。1号电池装入后,再装第2号电池。如果想从中取出电池,只有先取出第2号电池,才能取第1号电池。像这种后进入先出来的结构,在数据结构中被称为栈。
在这里插入图片描述

栈的概念和结构

栈是一种特殊的线性表,其只允许在固定的一端进行插入和删除元素操作。进行数据插入和删除操作的一端称为栈顶,另一端称为栈底。栈中的数据元素遵守后进先出LIFO(Last In First Out)的原则。

栈有两种常用的操作,从栈顶取数据以及将输入放入栈中。对于这两种操作,我们经常将它们称为出栈和压栈。

压栈:栈的插入操作叫做进栈/压栈/入栈,入数据在栈顶。
出栈:栈的删除操作叫做出栈。出数据也在栈顶。
在这里插入图片描述

栈的实现

栈的实现一般可以使用数组或者链表实现,相对而言数组的结构实现更优一些。因为数组在尾上插入、删除数据的代价比较小。下面使用数组实现栈结构。

在开始实现栈结构之前,我们要先定义一个栈结构。我们需要一个动态开辟的数组,用它存储栈内元素,一个变量记录当前栈顶的位置,还需要一个变量记录栈当前的容量。(这里的栈顶指针top指向栈顶元素的下一位置)

typedef int STDataType;

typedef struct Stack
{
	STDataType* a;
	int top;
	int capacity;
}Stack;

下面给出栈的各个接口↓↓↓

//初始化
void StackInit(Stack* s, int n);
//销毁
void StackDestory(Stack* s);
//入栈
void StackPush(Stack* s, STDataType x);
//出栈
void StackPop(Stack* s);
//获取栈顶元素
STDataType StackTop(Stack* s);
//判断栈是否为空
bool IsEmpty(Stack* s);
//获取栈已保存元素个数
int StackSize(Stack* s);

栈的初始化
首先给出栈的初始化操作,根据用户给出的初始容量n,为动态数组a开辟n个空间,并将capacity改为n。由于初始时没有保存任何元素,故将栈顶指针top指向0。

void StackInit(Stack* s, int n)
{
	assert(s);
	s->a = (STDataType*)malloc(sizeof(STDataType) * n);
	s->top = 0;
	s->capacity = n;
}

栈的销毁
栈在堆区开辟空间,栈不再使用时,需要将开辟的空间释放掉。再将top指针和容量置为0。

void StackDestory(Stack* s)
{
	assert(s);
	free(s->a);
	s->capacity = s->top = 0;
}

入栈操作
在入栈前,需要检查容量是否足够,如果容量不足,则扩容2倍空间。由于栈顶指针top指向的是栈顶元素的下一个位置,所以我们要先在top下标位置保存入栈元素,再将top指针加1。

在这里插入图片描述

void StackPush(Stack* s, STDataType x)
{
	assert(s);
	if (s->top == s->capacity)
	{
		s->a = (STDataType*)realloc(s->a, sizeof(STDataType) * s->capacity * 2);
		if (s->a == NULL)
		{
			perror("malloc error!\n");
			return 0;
		}
		s->capacity *= 2;
	}
	s->a[s->top++] = x;
}

出栈操作
如果栈中已经没有元素,此时应该禁止用户出栈。如果当前栈中有一个以上元素,我们只需要将栈顶指针top减一,即可完成出栈操作。

例如:有一个保存了4个元素的栈,初始时top指向栈顶元素的下一位置,如下图绿色箭头所示。当执行出栈操作后,top减1,top直向出栈前的栈顶元素。由于栈顶有效元素位置位于top指针以下,故此时红色top指针指向的数字4不是有效元素。
在这里插入图片描述

void StackPop(Stack* s)
{
	assert(s);
	assert(!IsEmpty(s));
	--s->top;
}

获取栈顶元素
由于栈顶指针top指向栈顶元素的下一个位置,我们在返回栈顶元素时,需要返回下标为top-1的元素。注意:在栈为空时,不允许用户获取栈顶元素。

STDataType StackTop(Stack* s)
{
	assert(s);
	assert(!IsEmpty(s));
	return s->a[s->top - 1];
}

判断栈是否为空 及 获取栈已存储元素数
由于栈顶指针top保存栈顶元素的下一个下标。因而,没有元素时它保存的值就是0,有元素时它就是栈中已存储的元素数。

bool IsEmpty(Stack* s)
{
	assert(s);
	return s->top == 0;
}

int StackSize(Stack* s)
{
	assert(s);
	return s->top;
}

队列

我们去奶茶店买奶茶时,需要按先来先点餐/取餐的方式。类似的场景包括医院叫号、银行叫号等等。生活中存在着大量的先到先服务的场景,我们将这种排队结构称之为队列。
在这里插入图片描述

队列的概念及结构

队列:只允许在一端进行插入数据操作,在另一端进行删除数据操作的特殊线性表,队列具有先进先出FIFO(First In First Out) 。

入队列:进行插入操作的一端称为队尾
出队列:进行删除操作的一端称为队头
在这里插入图片描述

队列的实现

队列也可以数组和链表的结构实现,使用链表的结构实现更优一些,因为如果使用数组的结构,出队列在数组头上出数据,效率会比较低。

在开始实现之前,我们一样定义一个队列的存储结构。由于这里使用单链表结构实现队列,所以需要先创建链表结点结构QNode。队列需要保存头和尾,因而在Queue结构中需要存储头尾结点。

typedef int QDataType;

typedef struct QueueNode
{
	QDataType val;
	struct QueueNode* next;
}QNode;

typedef struct Queue
{
	QNode* head;
	QNode* tail;
}Queue;

接下来一起看看队列的各个接口↓↓↓

void QueueInit(Queue* q);

void QueueDestory(Queue* q);

void QueuePush(Queue* q, QDataType x);

void QueuePop(Queue* q);

QDataType QueueFront(Queue* q);

QDataType QueueBack(Queue* q);

int QueueSize(Queue* q);

bool IsEmpty(Queue* q);

队列初始化
队列初始时没有任何元素,我们应该让它的头尾指针均指向空。
在这里插入图片描述

void QueueInit(Queue* q)
{
	assert(q);
	q->head = q->tail = NULL;
}

队列的销毁
头结点指向第一个元素的结点,我通过用next指针保存头结点的下一个结点,释放完头指针指向的结点后,再让它等于next指针,next指针再指向头指针的下一个结点…(以此类推)。当头指针为空时,整个队列就销毁完成了。注意,尾指针还不是空,还需要将它置空。

void QueueDestory(Queue* q)
{
	assert(q);
	while (q->head != NULL)
	{
		QNode* next = q->head->next;
		free(q->head);
		q->head = next;
	}
	q->tail = NULL;
}

入队操作
在队列中新增元素,只能在队尾添加。当队列还没有任何元素时,我们需要创建一个结点保存新元素,并将头尾指针指向它;如果队列中已经有元素时,我们只需要将尾结点的next域改为新结点,并将尾指针指向新添加的结点即可。
在这里插入图片描述

void QueuePush(Queue* q, QDataType x)
{
	assert(q);
	QNode* newnode = (QNode*)malloc(sizeof(QNode));
	newnode->val = x;
	newnode->next = NULL;
	if (q->head == NULL)
	{
		q->head = q->tail = newnode;
	}
	else
	{
		q->tail->next = newnode;
		q->tail = newnode;
	}
}

出队操作
如果队列中没有任何元素,则不允许用户出队。如果此时队列中仅有一个元素,则将它出队后,需要将头尾指针置空;如果队列中不止一个元素,则要先保存头指针的下一个结点,在将头结点删除后,将头指针改为新的头结点。
在这里插入图片描述

void QueuePop(Queue* q)
{
	assert(q);
	assert(!IsEmpty(q));
	if (q->head == q->tail)
	{
		free(q->head);
		q->head = q->tail = NULL;
	}
	else
	{
		QNode* newHead = q->head->next;
		free(q->head);
		q->head = newHead;
	}
}

获取头部元素
头指针指向的就是第一个结点的地址,只要返回它指向的结点的val域即可。

QDataType QueueFront(Queue* q)
{
	assert(q);
	assert(!IsEmpty(q));
	return q->head->val;
}

获取尾部元素
尾指针指向的就是最后一个结点的地址,只要返回它指向的结点的val域即可。

QDataType QueueBack(Queue* q)
{
	assert(q);
	assert(!IsEmpty(q));
	return q->tail->val;
}

获取队列元素数量
使用一个新指针保存头指针保存的结点地址。通过不断向后移动,每次移动就记录一次长度,当走到空时,就将整个队列走完了。此时返回记录的长度即可。

int QueueSize(Queue* q)
{
	assert(q);
	int size = 0;
	QNode* cur = q->head;
	while (cur)
	{
		size++;
		cur = cur->next;
	}
	return size;
}

判断队列是否为空
如果头结点指针指向空,就说明整个队列为空。

bool IsEmpty(Queue* q)
{
	assert(q);
	return q->head == NULL;
}

环形队列

上面实现的队列使用的是链表。假如我们使用的是数组存储队列那会是什么效果呢?

如果我们用一个长度为长度为8的数组存储队列元素。使用front存储队列开始的下标,rear指向队尾元素的下一个下标。如果元素入队列,则Queue[rear]=新元素,在++rear。如果元素出队列则让front++即可。
在这里插入图片描述
但如果不断入队不断出队,最终rear和front都会超出数组下标范围。此时我们通过在将rear++和front操作改为(rear+1)%len及(front+1)%len[其中len是数组长度],就可以让rear或front回到数组最前面。这样就可实现数组空间重复利用。
在这里插入图片描述

front==rear时我们已经定义为NULL,队列中没有元素。如果我们不断插入元素,则在插入8个元素后,rear和front重合,这时候就引出一个问题:rear==front到底是表示空,还是表示满呢?
在这里插入图片描述
为了解决这个问题,我们可以选择在front前面的一个元素不保存数据,使用(rear + 1) % len == front来作为队列已经满的依据,使用rear == front作为队列为空的依据。

这时计算队列的长度可以使用(rear - front + len) % len来计算。对于循环队列,这里只做介绍,不进行代码演示了。

综合巩固

学完上面的内容,下面我们来牛刀小试以下:

( ఠൠఠ )ノtest1:有效的括号

这里使用C语言时,需要将我们上述实现的栈结构全部包含进来。我了展示方便,这里给出的代码是C++语言。如果进入的是左括号,则直接保存到栈中;如果是右括号,则与栈中的括号匹配,不符合则返回false,如果匹配成功,则将栈顶弹出(因为这对括号已经能配对了)。直到s字符串走到结束时,这时候需要查看栈中是否还有括号,如果还有,说明这个字符串中的左括号多余右括号,故返回false;如果没有括号了,则说明所有括号都配对成功。

class Solution {
public:
    bool isValid(string s) {
        stack<char>stk;

        for(auto e : s)
        {
            if(!stk.empty() &&(
                (e == ')' && stk.top() == '(') ||
                (e == ']' && stk.top() == '[') ||
                (e == '}' && stk.top() == '{')
            ))
            {
                stk.pop();
            }
            else if(e == '(' || e == '[' || e == '{')
            {
                stk.push(e);
            }
            else
            {
                return false;
            }
        }

        return stk.empty();
    }
};

( ఠൠఠ )ノtest2:用队列实现栈

所有入栈的内容都保存在q1队列中,当需要获取栈顶元素时,则获取q1的队尾元素;当需要弹出栈顶元素,先将q1元素入到q2,直到只剩一个元素,这个元素用做返回,在返回前,将q2的内容再导会q1中。q2这里的作用仅仅是在q1需要做出栈时,作为q1队尾之前的各个元素保存的临时队列。

class MyStack {
public:
    MyStack() {

    }
    
    void push(int x) {
        q1.push(x);
    }
    
    int pop() {
        while(q1.size() > 1)
        {
            q2.push(q1.front());
            q1.pop();
        }
        int ret = q1.front();
        q1.pop();
        while(!q2.empty())
        {
            q1.push(q2.front());
            q2.pop();
        }
        return ret;
    }
    
    int top() {
        return q1.back();
    }
    
    bool empty() {
        return q1.empty();
    }
    queue<int>q1;//用于保存数据
    queue<int>q2;//用于临时交换
};

/**
 * Your MyStack object will be instantiated and called as such:
 * MyStack* obj = new MyStack();
 * obj->push(x);
 * int param_2 = obj->pop();
 * int param_3 = obj->top();
 * bool param_4 = obj->empty();
 */

( ఠൠఠ )ノtest3:用栈实现队列

使用一个输出栈和一个输入栈。当入队是元素均入到输入栈中;当输出栈为空时,将输入栈的元素全部入到输出栈,这样可以使得本来位于输入栈栈底的元素跑到输出栈的栈顶,这么操作就可以实现先进先出。

class MyQueue {
public:
    MyQueue() {

    }
    
    void push(int x) {
        pushstack.push(x);
    }
    
    int pop() {
        if(popstack.empty())
        {
            while(!pushstack.empty())
            {
                popstack.push(pushstack.top());
                pushstack.pop();
            }
        }
        int ret = popstack.top();
        popstack.pop();
        return ret;
    }
    
    int peek() {
        if(popstack.empty())
        {
            while(!pushstack.empty())
            {
                popstack.push(pushstack.top());
                pushstack.pop();
            }
        }
        return popstack.top();
    }
    
    bool empty() {
        return popstack.empty() && pushstack.empty();
    }
    stack<int>popstack;
    stack<int>pushstack;
};

/**
 * Your MyQueue object will be instantiated and called as such:
 * MyQueue* obj = new MyQueue();
 * obj->push(x);
 * int param_2 = obj->pop();
 * int param_3 = obj->peek();
 * bool param_4 = obj->empty();
 */

( ఠൠఠ )ノtest4:设计循环队列

这道题是对上面介绍的循环队列的实现。涉及下标的问题,多使用模运算符,实现下标从数组尾到头和头到尾的转换。

class MyCircularQueue {
public:
    MyCircularQueue(int k) {
        len = k + 1;
        a = (int*)malloc(sizeof(int) * len);
        front = rear = 0;
    }
    
    bool enQueue(int value) {
        if((rear + 1) % len == front) return false;
        a[rear] = value;
        rear = (rear + 1) % len;
        return true;
    }
    
    bool deQueue() {
        if(rear == front) return false;
        front = (front + 1) % len;
        return true;
    }
    
    int Front() {
        if(isEmpty()) return -1;
        return a[front];
    }
    
    int Rear() {
        if(isEmpty()) return -1;
        return a[(rear - 1 + len) % len];
    }
    
    bool isEmpty() {
        return front == rear;
    }
    
    bool isFull() {
        return (rear + 1) % len == front;
    }
    int* a;
    int front;
    int rear;
    int len;
};

/**
 * Your MyCircularQueue object will be instantiated and called as such:
 * MyCircularQueue* obj = new MyCircularQueue(k);
 * bool param_1 = obj->enQueue(value);
 * bool param_2 = obj->deQueue();
 * int param_3 = obj->Front();
 * int param_4 = obj->Rear();
 * bool param_5 = obj->isEmpty();
 * bool param_6 = obj->isFull();
 */

文章结语:这篇文章对时间复杂度、空间复杂度、数据结构与算法概念进行了简要的介绍。
🎈欢迎进入Super数据结构专栏,查看更多文章。
如果上述内容有任何问题,欢迎在下方留言区指正b( ̄▽ ̄)d

  • 28
    点赞
  • 24
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值