栈和队列的初阶知识与基础OJ练习

  本文仅使用了两种常见的实现方法——动态数组栈、链队列,如果要看更多的队列和栈的实现方法,可以看我的下面这篇文章中有关队列和栈的部分:图的五种接口的实现,
  本文的有关队列和栈的实现均已上传码云,可以来关注呀~:我的Gitee star一下再走啊


文章目录

  • 一、栈
    • 1. 存储结构和接口
    • ️2.初始化栈
    • ️3. 入栈
    • 4.判断栈是否为空
    • 5.出栈
    • 6.返回栈顶元素
    • 7.判断栈的大小
    • 8.销毁队列
  • 二、队列
    • 1.存储结构与接口
    • 2.初始化链式队列
    • 3. 判断队列是否为空
    • 4.入队列
    • 5.出队列
    • 6.队列元素个数
    • 7.返回队列首元素和尾元素
    • 8.销毁队列
  • 三、OJ练习
    • 1.leetcode 20. 有效的括号
      • 题目描述
      • 思路
      • 代码
    • 2.leetcode 225.用队列实现栈
      • 题目描述
      • 思路
      • 代码
    • 3.leetcode 232.用栈实现队列
      • ️题目描述
      • ️思路
      • 代码
    • ️4.leetcode 622.设计循环队列
      • 题目描述
      • ✨思路
        • 思路1:数组实现循环队列
        • 代码
        • 思路2:单链表实现循环队列
        • 代码

一、栈

  栈是一种后进先出的结构。(LIFO)
  对比栈的两种实现:

数组栈:以头做栈底,尾做栈顶,尾插尾删效率很高,空间浪费也无所谓。(优势比较大)

链式栈:如果以为做栈顶,那么尾插尾删效率比较低,需要实现双向链表(否则每次都要找到表尾);如果用头做栈顶,头插头删,效率就很高,虽然节约了空间,但是优势不明显了(入栈出栈的效率相较数组栈没有区别)。

  因此我们选择实现一个动态数组栈

1. 存储结构和接口

  我们用一个动态数组来存储栈中的元素,top表示指向当前栈顶的下一个元素的下标(当栈为空的时候top的值是0),capacity表示当前栈的大小。

typedef int StackDataType;
typedef struct {
	int top;
	StackDataType* arr;
	int capacity;
}Stack;
void StackInit(Stack* ps);
void StackPush(Stack* ps, StackDataType e);
void StackPop(Stack* ps);
StackDataType StackTop(Stack* ps);
int StackSize(Stack* ps);
bool StackEmpty(Stack* ps);
void Stackdestroy(Stack* ps);

️2.初始化栈

  初始化栈就是把栈的top值置成0,capacity置成0,arr置成NULL。

void StackInit(Stack* ps)
{
	assert(ps);
	ps->capacity = 0;
	ps->arr = NULL;
	ps->top = 0;
	//top指0意味着指向栈顶数据的下一个位置
	//ps->top = -1;
	//top指-1意味着它指向栈顶数据
}

️3. 入栈

  由于是数组栈,我们每次入栈前可以查询栈的空间是否足够,并且用realloc扩容,然后在数组的top位置插入e,然后top++。

void checkcapacity(Stack* ps)
{
	if (ps->capacity == ps->top)
	{
		int newcapacity = (ps->arr == NULL ? 4 : 2 * ps->capacity);
		StackDataType* tmp = (StackDataType*)realloc(ps->arr, newcapacity*sizeof(StackDataType));
		if (tmp == NULL)
		{
			printf("realloc fault\n");
			exit(-1);
		}
		ps->arr = tmp;
		ps->capacity = newcapacity;
	}
}
void StackPush(Stack* ps, StackDataType e)
{
	assert(ps);
	checkcapacity(ps);
	ps->arr[ps->top] = e;
	ps->top++;
}

4.判断栈是否为空

  如果top==0,则栈为空,否则栈不为空。

bool StackEmpty(Stack* ps)
{
	assert(ps);
	return ps->top == 0;
	//return ps->top == -1;
}

5.出栈

  首先判断栈是否为空,然后top–。

void StackPop(Stack* ps)
{
	assert(ps && StackEmpty(ps) != true);
	ps->top--;
}

6.返回栈顶元素

  首先检查一下栈是否为空,然后返回数组下标为top的元素。

StackDataType StackTop(Stack* ps)
{
	assert(ps && StackEmpty(ps) != true);
	return ps->arr[ps->top - 1];
}

7.判断栈的大小

  返回top的值即可。

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

8.销毁队列

  free掉申请来的arr,然后把arr置成NULL,top和capacity都置成0。

void Stackdestroy(Stack* ps)
{
	assert(ps);
	ps->top = 0;
	ps->capacity = 0;
	free(ps->arr);
	ps->arr = NULL;
}

二、队列

  队列是一种先进先出的结构。(FIFO)
  实现这种结构有很多方法,我们下面用C语言实现不带头的链式队列。

1.存储结构与接口

  既然是链式队列,那么思路就是去仿照单链表,定义一个head指针和一个tail指针,入队列用头插,出队列用尾删,具体来说,我们的队列的结构和要实现的接口如下:

typedef int QDataType;
typedef struct QueueNode {
	QDataType data;
	struct QueueNode* next;
}QNode;
typedef struct {
	QNode* head;
	QNode* tail;
}Queue;
//不带哨兵位的链队列
void QueueInit(Queue* pq);
bool QueueEmpty(Queue* pq);
void QueuePush(Queue* pq, QDataType e);
void QueuePop(Queue* pq);
QDataType Queuefront(Queue* pq);
QDataType Queueback(Queue* pq);
int QueueSize(Queue* pq);
void Queuedestroy(Queue* pq);

2.初始化链式队列

  对于不带头结点的链式队列,初始化要完成的任务就是把head和tail都置空,表明当前队列为空,并且方便后续插入。

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

3. 判断队列是否为空

bool QueueEmpty(Queue* pq)
{
	assert(pq);
	return pq->head == NULL;
}

4.入队列

  与单链表的尾插类似,同样要特别注意单链表为空的情况。

void QueuePush(Queue* pq, QDataType e)
{
	assert(pq);
	QNode* newnode = (QNode*)malloc(sizeof(QNode));
	if (newnode == NULL)
	{
		printf("malloc fault\n");
		exit(-1);
	}
	newnode->data = e;
	newnode->next = NULL;
	if (QueueEmpty(pq) == true)
	{
		pq->head = pq->tail = newnode;
	}
	else
	{
		pq->tail->next = newnode;
		pq->tail = newnode;
	}
}

5.出队列

  出队列类似单链表的头删,要注意的是删除队列最后一个元素时要同时把tail和head都置空,并且注意如果队列空了就不要再删除了。

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

6.队列元素个数

  这里有两种实现方式,一个可以在队列的结构体中加入一个无符号整形变量size用来表示队列大小,每次入队列size++,出队列size–;另一种则是我们下面的实现方式,遍历一遍队列,用计数器返回队列长度。

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

7.返回队列首元素和尾元素

  这个简单,返回head和tail所指的元素就行了,注意检查队列是否为空。

QDataType Queuefront(Queue* pq)
{
	assert(pq);
	assert(!QueueEmpty(pq));
	return pq->head->data;
}
QDataType Queueback(Queue* pq)
{
	assert(pq);
	assert(!QueueEmpty(pq));
	return pq->tail->data;
}

8.销毁队列

  仿照单链表的销毁即可。

void Queuedestroy(Queue* pq)
{
	assert(pq);
	assert(!QueueEmpty(pq));
	QNode* cur = pq->head;
	while (cur)
	{
		QNode* next = cur->next;
		free(cur);
		cur = next;
	}
	pq->head = pq->tail = NULL;
}

三、OJ练习

1.leetcode 20. 有效的括号

题目描述

栈和队列的初阶知识与基础OJ练习_第1张图片

思路

这种括号匹配问题我们可以用栈来实现。
把我们有关栈的代码全部贴上(如果用cpp可以用STL库里头的栈)。
创建一个栈,
遇到左括号 入栈;
遇到右括号 出栈匹配;
最后看看栈是否为空;
如果一上来就遇到右括号 也就是说一上来就想出栈 栈为空;
那就return false
记得在每次return之前都要销毁我们的栈。

代码

bool isValid(char * s)
{
    //思路:遇到左括号 入栈
    //遇到右括号 出栈匹配
    //最后看看栈是否为空
    //如果一上来就遇到右括号 也就是说一上来就想出栈 栈为空
    //那就return false
    Stack st;
    StackInit(&st);
    while(*s)
    {
        if(*s == '(' || *s == '[' || *s == '{')
        {
            StackPush(&st, *s);
            s++;
        }
        if(*s == '}' || *s == ')' || *s == ']')
        {
            if(StackEmpty(&st)==true)
            {
                Stackdestroy(&st);
                return false;
            }
            StackDataType x = StackTop(&st);
            StackPop(&st);
            if((*s == '}' && x != '{') || (*s == ']' && x != '[') || (*s == ')' && x != '('))
            {
                Stackdestroy(&st);
                return false;
            }
            s++;
        }
    }
    if (StackEmpty(&st) == true)
    {
        Stackdestroy(&st);
        return true;
    }
    else
    {
        Stackdestroy(&st);
        return false;
    }
}

2.leetcode 225.用队列实现栈

题目描述

栈和队列的初阶知识与基础OJ练习_第2张图片

思路

  用两个队列来实现栈;

  1. 如果要入栈,就选择一个空队列入队列;
  2. 如果要出栈,就把非空队列里头除最后一个元素元素都入到另一个空队列里头,并且每入一个元素就弹出原非空队列元素,然后让最后一个元素出队列,结束出队列过程;
  3. 如果要返回栈顶元素,就返回非空队列的队尾元素就可以;
  4. 判断哪个队列为空可以用QueueEmpty函数,首先假设一个队列为空,另一个队列非空,然后判断一下这个队列是否为空,如果为空说明假设正确不需要调换,如果不为空说明假设错误,让另一个队列为非空队列;
  5. 判断栈是否为空就看两个队列是否都为空。
  6. 如果要销毁栈,就销毁两个队列,然后再free掉栈的指针就行。

代码

//思路:用两个队列来实现栈,当入栈的时候,就让元素加入一个队列;
//要出栈的时候,就把队列中除最后一个元素外都加入另一个队列中,在原本的队列中移除最后一个元素
//要查看栈顶元素就先把非空队列中除最后一个元素都加入到另一个队列中 然后返回最后一个元素 
//然后把这个元素也加入另一个队列里头 使原队列为空

typedef struct {
    Queue q1;
    Queue q2;
} MyStack;


MyStack* myStackCreate() {
    MyStack* st = (MyStack*)malloc(sizeof(MyStack));
    QueueInit(&st->q1);
    QueueInit(&st->q2);
    return st;
}

void myStackPush(MyStack* obj, int x) {
    Queue* emptyQ = &obj->q1;
    Queue* inemptyQ = &obj->q2;
    if(!QueueEmpty(&obj->q1))
    {
        emptyQ = &obj->q2;
        inemptyQ = &obj->q1;
    }
    QueuePush(inemptyQ, x);
}

int myStackPop(MyStack* obj) {
    Queue* emptyQ = &obj->q1;
    Queue* inemptyQ = &obj->q2;
    if(!QueueEmpty(&obj->q1))
    {
        emptyQ = &obj->q2;
        inemptyQ = &obj->q1;
    }
    while (QueueSize(inemptyQ)>1)
    {
        QueuePush(emptyQ, Queuefront(inemptyQ));
        QueuePop(inemptyQ);
    }
    int ret = Queuefront(inemptyQ);
    QueuePop(inemptyQ);
    return ret;
}

int myStackTop(MyStack* obj) {
    if (QueueEmpty(&obj->q1) == true)
    {
        return Queueback(&obj->q2);
    }
    else
    {
        return Queueback(&obj->q1);
    }
}

bool myStackEmpty(MyStack* obj) {
    return QueueEmpty(&obj->q1) == true && QueueEmpty(&obj->q2) == true;
}

void myStackFree(MyStack* obj) {
    Queuedestroy(&obj->q1);
    Queuedestroy(&obj->q2);
    free(obj);
}

3.leetcode 232.用栈实现队列

️题目描述

栈和队列的初阶知识与基础OJ练习_第3张图片

️思路

  用两个栈来实现队列,一个栈负责入队列操作,记为pushST,另一个栈负责出队列操作,记为popST。

  1. 入队列则入pushST栈。
  2. 出队列则先判断popST栈是否为空,如果为空,就把popST的元素全部都入到pushST栈中,然后弹出popST中的栈顶元素并返回。
  3. 返回队列开头元素,就在出队列操作中把弹出栈顶元素改为返回栈顶元素就行。
  4. 判断队列是否为空,就看两个栈是否为空。
  5. 销毁队列就销毁两个栈,然后free掉队列的指针。

代码

typedef struct {
    Stack pushST;
    Stack popST;
} MyQueue;


MyQueue* myQueueCreate() {
    MyQueue* q = (MyQueue*)malloc(sizeof(MyQueue));
    StackInit(&q->pushST);
    StackInit(&q->popST);
    return q;
}

void myQueuePush(MyQueue* obj, int x) {
    StackPush(&obj->pushST, x);
}

int myQueuePop(MyQueue* obj) {
    if (StackEmpty(&obj->popST))
    {
        while (!StackEmpty(&obj->pushST))
        {
            StackPush(&obj->popST, StackTop(&obj->pushST));
            StackPop(&obj->pushST);
        }
    }
    int ret = StackTop(&obj->popST);
    StackPop(&obj->popST);
    return ret;
}

int myQueuePeek(MyQueue* obj) {
    if (StackEmpty(&obj->popST))
    {
        while (!StackEmpty(&obj->pushST))
        {
            StackPush(&obj->popST, StackTop(&obj->pushST));
            StackPop(&obj->pushST);
        }
    }
    int ret = StackTop(&obj->popST);
    return ret;
}

bool myQueueEmpty(MyQueue* obj) {
    return StackEmpty(&obj->pushST) && StackEmpty(&obj->popST);
}

void myQueueFree(MyQueue* obj) {
    Stackdestroy(&obj->pushST);
    Stackdestroy(&obj->popST);
    free(obj);
}

️4.leetcode 622.设计循环队列

题目描述

栈和队列的初阶知识与基础OJ练习_第4张图片

✨思路

  循环队列的关键在于空出一个位置,以此来区分空队列和满队列,不然当满队列的时候由于是循环队列,队头和队尾指针会重合。然而初始化的时候,front和rear是重合的,这就无法区分队列为空和队列为满的情况。

思路1:数组实现循环队列

  数组实现循环队列的关键在于用%来使超过数组大小范围的front和rear值回到数组内。

  1. 如果队列k个元素 申请k+1个元素大小的空间
  2. 队列空 front == rear
  3. 队列满 (rear+1)%(k+1) == front
  4. 元素e入队列就让rear所指的元素变成e,然后rear++,然后rear%(k+1)回到数组范围内。
  5. 出队列就front++,然后front%(k+1)回到数组范围内。
  6. 返回队列首元素就返回数组下标为front的元素。
  7. 返回队列尾元素就返回下标如下的数组的元素(想到要用%后就画个图凑一下就知道了):
    i = ( o b j − > r e a r + o b j − > s i z e ) % ( o b j − > s i z e + 1 ) i = (obj->rear + obj->size)\%(obj->size + 1) i=(obj>rear+obj>size)%(obj>size+1)
    或者专门处理一下rear等于0的时候,他的前继是第k个元素。
  8. 销毁循环队列就free数组然后free循环队列的指针。

代码

typedef struct {
    int* a;
    int size;
    int front;
    int rear;
} MyCircularQueue;


MyCircularQueue* myCircularQueueCreate(int k) {
    MyCircularQueue* cq = (MyCircularQueue*)malloc(sizeof(MyCircularQueue));
    cq->a = (int*)malloc((k+1)*sizeof(int));
    cq->size = k;
    cq->front = cq->rear = 0;
    return cq;
}
bool myCircularQueueIsFull(MyCircularQueue* obj);
bool myCircularQueueIsEmpty(MyCircularQueue* obj);

bool myCircularQueueEnQueue(MyCircularQueue* obj, int value) {
    if(myCircularQueueIsFull(obj))
    {
        return false;
    }
    obj->a[obj->rear] = value;
    obj->rear++;
    obj->rear %= (obj->size + 1);
    return true;
}

bool myCircularQueueDeQueue(MyCircularQueue* obj) {
    if(myCircularQueueIsEmpty(obj))
    {
        return false;
    }
    obj->front++;
    obj->front %= (obj->size + 1);
    return true;
}

int myCircularQueueFront(MyCircularQueue* obj) {
    if (myCircularQueueIsEmpty(obj))
    {
        return -1;
    }
    return obj->a[obj->front];
}

int myCircularQueueRear(MyCircularQueue* obj) {
    if (myCircularQueueIsEmpty(obj))
    {
        return -1;
    }
    //rear指的不是尾巴元素 是尾巴元素的后一个元素 需要处理一下
    int i = (obj->rear + obj->size)%(obj->size + 1);
    return obj->a[i];
    //或者这样处理
    //if (obj->rear == 0)
    //{
    //    return obj->a[obj->size];
    //}
    //return obj->a[obj->rear - 1];
}

bool myCircularQueueIsEmpty(MyCircularQueue* obj) {
    return obj->rear == obj->front;
}

bool myCircularQueueIsFull(MyCircularQueue* obj) {
    return (obj->rear + 1)%(obj->size + 1) == obj->front;
}

void myCircularQueueFree(MyCircularQueue* obj) {
    free(obj->a);
    free(obj);
}

思路2:单链表实现循环队列

  1. 创建循环队列时,如果队列有K个元素的空间就申请k+1个元素的空间,用head记住这个空间的地址以便后续销毁,然后把这些节点的next值都连上,最后一个结点的next值指向第一个结点,然后定义一个头指针和尾指针和当前队列大小size。
  2. 判断队列是否为空,就看front和rear是否相等。
  3. 判断队列是否满,就看rear的next是否等于front。
  4. 入队列判断一下队列是否为满,如果不满就rear->data = e, 然后rear = rear->next。
  5. 出队列判断一下队列是否为空,如果不空就front=front->next
  6. 返回队首元素就判断一下队列是否为空,不空就返回front->data;
  7. 返回队尾元素就判断一下队列是否为空,不空就返回rear->data;
  8. 销毁队列就free掉head,然后free掉指向队列的指针。

代码

typedef struct Node{
    int data;
    struct Node* next;
}Node;

typedef struct {
    Node* front;
    Node* rear;
    Node* head;
    int size;
} MyCircularQueue;


MyCircularQueue* myCircularQueueCreate(int k) {
    MyCircularQueue* cq = (MyCircularQueue*)malloc(sizeof(MyCircularQueue));
    cq->size = k;
    cq->front = (Node*)malloc((k+1)*sizeof(Node));
    Node* tmp = cq->front;
    for (int i = 1; i <= k; i++)
    {
        tmp->next = tmp + 1;
        tmp = tmp->next;
    }
    tmp->next = cq->front;
    cq->rear = cq->front;
    cq->head = cq->front;
    return cq;
}

bool myCircularQueueIsEmpty(MyCircularQueue* obj);

bool myCircularQueueIsFull(MyCircularQueue* obj);

bool myCircularQueueEnQueue(MyCircularQueue* obj, int value) {
    if (myCircularQueueIsFull(obj))
    {
        return false;
    }
    obj->rear->data = value;
    obj->rear = obj->rear->next;
    return true;
}

bool myCircularQueueDeQueue(MyCircularQueue* obj) {
    if (myCircularQueueIsEmpty(obj))
    {
        return false;
    }
    obj->front = obj->front->next;
    return true;
}

int myCircularQueueFront(MyCircularQueue* obj) {
    if (myCircularQueueIsEmpty(obj))
    {
        return -1;
    }
    return obj->front->data;
}

int myCircularQueueRear(MyCircularQueue* obj) {
    if (myCircularQueueIsEmpty(obj))
    {
        return -1;
    }
    Node* cur = obj->front;
    while (cur->next != obj->rear)
    {
        cur = cur->next;
    }
    return cur->data;
}

bool myCircularQueueIsEmpty(MyCircularQueue* obj) {
    return obj->front == obj->rear;
}

bool myCircularQueueIsFull(MyCircularQueue* obj) {
    return obj->rear->next == obj->front;
}

void myCircularQueueFree(MyCircularQueue* obj) {
    free(obj->head);
    free(obj);
}

  对比两种思路可以发现,单链表实现虽然结构和创建复杂了一些,但是的入队列操作、出队列操作、判断队列是否为空、判断队列是否满、返回队首元素、返回队尾元素都比数组实现要简单很多。

你可能感兴趣的:(初级数据结构,LeetCode刷题,队列,栈,数据结构,leetcode)