【栈与队列经典OJ】

个人专栏:《数据结构-初阶》《经典OJ题目》《C语言》

欢迎大佬交流!

注:点击下面标题即可做题!

1、有效的括号

一、分析:

如果第一次遇到这种题,我觉得不太好想;

首先我们要遍历一遍字符串s;

先尝试双指针法看能否解决,定义一个左指针寻找左括号,一个右指针寻找右括号;

当左指针遇到左括号时开始让右指针从当前位置出发寻找对应的右括号;

如果找到了,就让右指针从左指针的下一个位置出发,同时利用 bool 数组标记右指针对应的位置;

时间复杂度为O(N^2) 级别(两个指针都要遍历一遍字符串)

来模拟一下示例五:

首先创建足够大的 bool 数组,全都置为false ;

初始状态下左指针指向 ( ,接着右指针从当前位置向后找对应的右括号发现在下标为2的位置;

接着用 bool 数组标记左右指针的位置为 true ;让左指针走到下一个位置;

此时结果已经错误了,因为下标为1的位置对应的是 [ 左括号,与其对应的应该是 ] ,而非 ( !

显然,双指针法不行了

尝试通过标记匹配位置来避免重复使用字符,但仍然无法解决嵌套匹配问题

我们在模拟示例五的过程中发现一个关键就是每个右括号都必须找到距离最近的左括号进行匹配

"距离最近" 就是 题眼, 这不就正好符合栈后进先出的特征;

每次入栈时,距离最近的元素就是栈顶元素!我们可以直接访问到栈顶元素!

因此我们的思路就是:遍历一遍字符串,如果是左括号入栈,如果是右括号就进行匹配;

匹配成功就让栈顶元素出栈,否则就直接返回 false

二、代码

下面我们来实现代码:

首先创建栈 st ,接着如果是左括号就直接入栈,如果是右括号就要进行匹配;

匹配之前要判断栈内元素是否为0,如果为0,直接返回 false;

接着继续进行判断吗,如果匹配则将栈顶元素弹出;

如果不匹配直接返回 false , 在返回 false 之前要进行销毁,否则就造成了内存泄露!;

当循环结束后,因为要进行销毁,此时先暂存栈内是否为空的结果;

接着就行销毁,最终返回暂存的结果即可

cpp 复制代码
typedef int STDataType;

//定义栈结构体
typedef struct stack
{
	STDataType* a; //动态数组
	int top;       //栈顶
	int capacity;  //容量
}Stack;

//初始化
void StackInit(Stack* ps)
{
	assert(ps);
	ps->a = NULL;
	ps->top = ps->capacity = 0;
}

//入栈
void StackPush(Stack* ps, STDataType x)
{
	assert(ps);
	//判断空间够不够
	if (ps->top == ps->capacity)
	{
		//申请空间
		int newcapacity = ps->capacity == 0 ? 4 : 2 * ps->capacity;
		STDataType* newa = (STDataType*)realloc(ps->a, sizeof(STDataType) * newcapacity);
		if (newa == NULL)
		{
			printf("realloc failed!\n");
			return;
		}
		ps->a = newa;
		ps->capacity = newcapacity;
	}
	ps->a[ps->top] = x;
	ps->top++;
}

//出栈
void StackPop(Stack* ps)
{
	assert(ps);
	assert(ps->top > 0);
	ps->top--;
}

//获取栈顶元素
STDataType StackTop(Stack* ps)
{
	assert(ps);
    assert(ps->top > 0);

	return ps->a[ps->top - 1];
}

//获取栈中有效元素个数
int StackSize(Stack* ps)
{
	assert(ps);
	return ps->top;
}

//检测栈是否为空
bool StackEmpty(Stack* ps)
{
	assert(ps);
	return ps->top == 0;
}

//销毁栈
void StackDestroy(Stack* ps)
{
	free(ps->a);
	ps->a = NULL;
	ps->capacity = ps->top = 0;
}

//初始化
void StackInit(Stack* ps);

//入栈
void StackPush(Stack* ps, STDataType x);

//出栈
void StackPop(Stack* ps);

//获取栈顶元素
STDataType StackTop(Stack* ps);

//获取栈中有效元素个数
int StackSize(Stack* ps);

//检测栈是否为空
bool StackEmpty(Stack* ps);

//销毁栈
void StackDestroy(Stack* ps);

bool isValid(char* s)
{
    //创建栈
    Stack st;
    StackInit(&st);

    for(int i = 0;s[i];i++)
    {
        char ch = s[i];
        if(ch == '(' || ch == '[' || ch == '{')
        {
            StackPush(&st,ch);
        }
        else
        {
            //右括号
            if(StackSize(&st) == 0) return false;
            //进行匹配
            char top = StackTop(&st);
            if((s[i] == ')' && top == '(') || (s[i] == ']' && top == '[')
               || s[i] == '}' && top == '{')
            {
                //匹配成功
                StackPop(&st);
            }
            else 
            {
                StackDestroy(&st);
                return false;
            }
        }
    }
    //判断栈内是否为空
    bool ret = StackEmpty(&st);

    StackDestroy(&st);

    return ret; 
}

2、用队列实现栈

这道题就是让我们用两个队列去实现一个栈;

队列是先进先出,而栈是后进先出;

我们通过模拟一个例子来找到关键步骤!

一、模拟 + 分析:

初始状态下,我们有两个队列q1,q2;

假设我们首先将1, 2,3push到 q1 中;

现在我们想要弹出栈顶元素,怎样弹出?

在q1中,无法pop,此时只能借助 q2 进行删除;

即:将前 N - 1 个数据拷贝到 q2 中;同时 将 q1 中的N个数据全部删除;

此时要不要再拷贝回去呢?

显然不用拷贝回去,即使拷贝回去之后再进行pop,push和不拷贝回去是一样的操作;

现在我们再进行push 4, 5

有两种方法,一是push到有元素的队列中,二是push到无元素的队列中;

a、push到有元素队列

此时如图所示

我们接着进行pop,通过空元素的 q2 即可完成!

同时将第N个数据 pop 掉,将 q2 置为空

逻辑很清晰连贯

b、push到无元素队列

此时如图所示

如果想进行 pop 操作的话,首先要确定栈顶元素是谁;

对于这种情况,无法确定栈顶元素是谁,因此无法进行 pop ;

排除这种情况!

c、总结:

不能出现两个队列均有元素的情况;入数据要往非空的队列中push;出数据要借助空队列pop,同时将最后一个数据也 pop

二、代码:

0、初始化

首先将之前实现好的队列项目粘贴进去;

接着创建两个队列

cpp 复制代码
typedef struct
{
    //两个队列
    Queue q1;
    Queue q2;
}MyStack

1、myStackCreate

如果我们直接创建一个结构体Mystack st,接着 return &st;

这样是否可行?

当然不行,因为创建出来的 st 是局部变量,出函数之后就被销毁了,返回的是错误的地址!

因此我们采用 malloc 的方式,直接开辟一块空间,用一个结构体指针 pst 来接收;

同时初始化两个指针,最终返回 pst;

cpp 复制代码
MyStack* myStackCreate()
{
    MyStack* pst = (MyStack*)malloc(sizeof(MyStack));

    QueueInit(&pst->q1);
    QueueInit(&pst->q2);

    return pst;
}

2、myStackPush

想要 push ,就要先知道哪个是非空队列;

我们直接采用假设法,因为后续还需要找到非空队列;

假设法就是先假设 q1 就是空队列;q2 为非空队列;

然后进行 if 判断,如果判断错误就 逆置假设!

最后调用队列中的 push 函数即可;

cpp 复制代码
void myStackPush(MyStack* obj, int x)
{
    //push到有元素的队列中
    Queue* empty = &obj->q1,*nonempty = &obj->q2;
    if(QueueEmpty(&obj->q2))
    {
        empty = &obj->q2;
        nonempty = &obj->q1;
    }
    QueuePush(nonempty,x);
}

3、myStackPop

先用假设法找到非空队列,接着将 size - 1 个数据挪动到 空队列 中;

并将最后一个数据删除;

在删除之前先暂存结果,删除之后return 即可

cpp 复制代码
int myStackPop(MyStack* obj)
{
    //将有元素的队列的前 size - 1 个数据导入空队列中,同时 pop 最后一个元素
    Queue* empty = &obj->q1,*nonempty = &obj->q2;
    if(QueueEmpty(&obj->q2))
    {
        empty = &obj->q2;
        nonempty = &obj->q1;
    }
    while(QueueSize(nonempty) > 1)
    {
        QueuePush(empty,QueueFront(nonempty));
        QueuePop(nonempty);
    }

    int ret = QueueFront(nonempty);
    QueuePop(nonempty);

    return ret;
}

4、myStackTop

找到非空队列之后直接返回队尾元素

cpp 复制代码
int myStackTop(MyStack* obj)
{
    Queue* empty = &obj->q1,*nonempty = &obj->q2;
    if(QueueEmpty(&obj->q2))
    {
        empty = &obj->q2;
        nonempty = &obj->q1;
    }
    return QueueBack(nonempty);
}

5、myStackEmpty

当两个队列均为空即表明栈为空

cpp 复制代码
bool myStackEmpty(MyStack* obj)
{
    return (QueueEmpty(&obj->q1) && QueueEmpty(&obj->q2));   
}

6、myStackFree

先释放两个队列申请的空间;然后再释放 obj ;

最终无需将 obj 置为空,因为想要修改一级指针就要传入二级指针!

调用完 myStackFree函数之后要显式置空!

cpp 复制代码
void myStackFree(MyStack* obj)
{
    QueueDestroy(&obj->q1);    
    QueueDestroy(&obj->q2);    

    free(obj);
    //obj = NULL;
}

3、用栈实现队列

和上道题的分析方法一样,我们先模拟,找到关键步骤;

一、分析 + 模拟

初始状态下假设我们先入队1, 2 ;如下图所示

接着我们想要再继续 push 是 push 到 st1 还是 st2呢?

由上道题经验可得,push 到有元素的栈中;

继续往里面 push 3, 4;

此时想要获取队头元素该怎么办?

借助 st2 将数据导回来,接着 return 栈顶元素;

此时我们发现,导过来之后不就变成了队列吗?

那么此时就不需要导回去了;

在这种情况下,pop也就是调用栈的pop即可;

不过,想要继续 push 呢?显然不能直接 push 到 st2;

是不是还需要先导回去再进行 push ?

并不是!

我们直接 push 到 st1 中!

为什么能这样做?

此时 st2 已经等价于 队列 ,而如果我们还是 push 到 st2 中,势必会打乱顺序!

此时我们便 push 到 st1 中,只需在 pop 时要注意先 pop st2中的元素,但 st2 中的为空时,再将 st1 直接导入 st2 中!继续 pop st2!

我们以push 5 为例

总结:

st1用来push,st2用来pop,当 st2 为空并且进行 pop 时,要先将 st1 中的数据导到 st2中!

二、代码实现

0、初始化

首先将已经实现好的栈项目粘贴进去

接着创建两个栈

cpp 复制代码
typedef struct
{
    Stack st1;
    Stack st2;
} MyQueue;

1、myQueueCreate

分析:

根据上题经验,我们需要 malloc 一块空间,防止出函数后指针变成野指针了;

接着调用 栈 的初始化函数对创建出来的两个栈进行初始化

cpp 复制代码
MyQueue* myQueueCreate()
{
    MyQueue* pq = (MyQueue*) malloc (sizeof(MyQueue));

    StackInit(&pq->st1);
    StackInit(&pq->st2);    

    return pq;
}

2、myQueuePush

分析:

直接 push 到 st1 即可

cpp 复制代码
void myQueuePush(MyQueue* obj, int x)
{
    StackPush(&obj->st1,x);
}

3、myQueuePop

分析:

首先确保两个栈均不为空!

pop前先检查 st2 是否为空,如果为空就将 st1 中的数据导入到 st2 中,再进行 pop

否则就直接 在 st2 中调用 pop 即可

cpp 复制代码
int myQueuePop(MyQueue* obj)
{
    //先判断 st2 是否为空
    if(StackEmpty(&obj->st2))
    {
        //为空就导数据
        while(!StackEmpty(&obj->st1))
        {
            Stackpush(&obj->st2,StackTop(&obj->st1));
            StackPop(&obj->st1);
        }
    }
    //此时 st2 中已经存在数据
    int top = StackTop(&obj->st2);

    StackPop(&obj->st2);

    return top;
}

4、myQueuePeek

分析:

要求返回队头元素;

想一想,如果我们的 st2 不为空,那么队头元素就在 st2 的栈顶!

如果 st2 为空,那就需要将 st1 导数据到 st2 中,接着返回 st2 的栈顶;

感觉跟 myQueuePop 操作有点相似;那么能否复用一下 Pop 呢?

当然可以,不过要注意最终要将Pop的返回值,也就是 st2 中的栈顶元素 push 到 st2 中

保证数据的正确性

cpp 复制代码
int myQueuePeek(MyQueue* obj)
{
    int top = myQueuePop(obj);
    Stackpush(&obj->st2,top);

    return top;
}

5、myQueueEmpty

分析:

和上题思路基本一致,直接给出代码

cpp 复制代码
bool myQueueEmpty(MyQueue* obj)
{
    Stack* st1 = &obj->st1;    
    Stack* st2 = &obj->st2;

    return (StackEmpty(st1) && StackEmpty(st2));    
}

6、myQueueFree

分析:

同样和上题思路基本一致,直接给出代码

cpp 复制代码
void myQueueFree(MyQueue* obj)
{
    StackDestroy(&obj->st1);
    StackDestroy(&obj->st2);    

    free(obj);
    
    //obj = NULL;
}

4、设计循环队列

一、分析

读完题目之后能发现就是让我们实现一个数据结构 - 循环队列

与普通队列不同的就是 队尾 的下一个元素就是 队头

感觉和循环链表有点像,那么能不能用链表来实现呢?

那能不能用数组来实现呢?

a、用数组来实现

既然要完成循环队列,那就需要头 尾 指针(此处指针就是下标来标记位置)以及 队列的额定空间 k;

初始状态下我们假设 k = 4,ptail 指向下标为0 的位置;如下图所示

现在我们 push 1,2,3

此时再进行 push 4

tail 就指向了 下标为 k 的位置,为了使队列循环起来,我们将 tail % k 即可;

此时 tail 就指向了 phead

此时我们发现当队列为满的条件时 head = tail

接下来我们进行四次 pop 操作;每次 pop 都会将 head向后挪动一位;

如果走到下标为 k 的位置,也要将 head % k

此时我们发现队列为空的条件是 tail = head

那我们到底该怎样进行判断呢?

当 tail = head ,到底是空还是满?

该怎样解决呢?

我们有两种解决办法

1、增加 size 变量

直接在定义结构体时增加 size 这个变量,用来标记队列元素个数

只需判断 size 大小就能知道到底是空还是满

2、多开一个空间

多开一个空间同样也能解决问题

我们通过图示来观察

1> 判满

可以发现判满的条件是 tail + 1 = head;

是这个吗?不要忘了下标的越界处理

正确结果应该是 (tail + 1) % (k + 1) = head ;

为什么 head 不需要取模呢?

因为每次要push时 head 都是在一个固定位置,只有 tail 会 ++,而 head不会;

而只有 pop 时 head 才会++;

2> 判空

显然,判空条件是 tail = head

b、用链表来实现

二、代码实现

0、初始化

分析:定义动态数组,头尾指针,以及额定空间 k

cpp 复制代码
typedef struct
{
    int* a;
    int head;
    int tail;
    int k;
} MyCircularQueue;

1、myCircularQueueCreate

分析:

如果只创建变量,出函数之后便会被销毁,因此我们开辟一块空间;

接着为动态数组开辟 k + 1 个大小的空间,同时对循环队列的变量进行初始化!

cpp 复制代码
MyCircularQueue* myCircularQueueCreate(int k)
{
    MyCircularQueue* pq = (MyCircularQueue*)malloc(sizeof(MyCircularQueue));

    pq->a = (int*)malloc(sizeof(int) * (k + 1));
    pq->head = pq->tail = 0;
    pq->k = k;

    return pq;
}

2、myCircularQueueEnQueue

分析:

首先判断队列是否满了,如果满了直接返回 false;

接着插入数据即可,下标正好是 tail 指向的位置;

最后更新 tail ,要注意避免回绕!

cpp 复制代码
bool myCircularQueueEnQueue(MyCircularQueue* obj, int value)
{
    if(myCircularQueueIsFull(obj)) return false;

    //入数据
    obj->a[obj->tail] = value;
    obj->tail++;
    
    //避免回绕
    obj->tail %= obj->k + 1;    
}

3、myCircularQueueDeQueue

分析:

首先判断是否为空,为空就返回 - 1;

不为空直接将 head++ 即可,注意避免回绕

cpp 复制代码
bool myCircularQueueDeQueue(MyCircularQueue* obj)
{
    if(myCircularQueueIsEmpty(obj)) return false;

    obj->head = (obj->head + 1) % (obj->k + 1);
    return true;
}

4、myCircularQueueFront

分析:

首先判断是否为空,为空返回 -1;

队头元素就在下标为 head 对应的位置上,因此直接输出 a[head] ;

cpp 复制代码
int myCircularQueueFront(MyCircularQueue* obj)
{
    if(myCircularQueueIsEmpty(obj)) return -1;
    return obj->a[obj->head];    
}

5、myCircularQueueRear

分析:

首先判断是否为空,为空直接返回 -1;

接着取队尾元素,队尾元素下标为 tail - 1;

处理当 tail = 0 特殊情况

a、利用 三目操作符

return tail == 0 ? a[k] : a[tail - 1];

b、利用取模运算

return a[(tail - 1 + k + 1) % (k + 1) ] ;

本质就是加上 x 再 % x,巧妙解决了回绕问题!

cpp 复制代码
int myCircularQueueRear(MyCircularQueue* obj)
{
    if(myCircularQueueIsEmpty(obj)) return -1;
    
    //return obj->tail == 0 ? obj->a[obj->k] : obj->a[obj->tail - 1];

    return obj->a[(obj->tail - 1 + obj-> k + 1) % (obj->k + 1)];
}

6、myCircularQueueIsEmpty

分析:

判空条件就是 tail = head;

cpp 复制代码
bool myCircularQueueIsEmpty(MyCircularQueue* obj)
{
    return obj->tail == obj->head;
}

7、myCircularQueueIsFull

分析:

判满条件就是 (tail + 1) % ( k + 1) = head;

cpp 复制代码
bool myCircularQueueIsFull(MyCircularQueue* obj)
{
    return (obj->tail + 1) % (obj->k + 1) == obj->head;     
}

8、myCircularQueueFree

分析:

先释放动态数组的内存,接着释放 obj

cpp 复制代码
void myCircularQueueFree(MyCircularQueue* obj)
{
    free(obj->a);
    free(obj);
    //obj = NULL;    
}

三、注意事项

最后在提交时应将 myCircularQueueIsEmptymyCircularQueueIsFull 这两个函数放到 myCircularQueueCreate 的后面或者前面;

避免影响我们后续函数的调用

如有不足之处欢迎指出!

相关推荐
夏日听雨眠1 小时前
数据结构(哈希函数)
数据结构·算法·哈希算法
诙_2 小时前
C++数据结构--B树,B+树,B*树
数据结构·b树
星恒随风2 小时前
C语言链表详解:从单链表到双向链表
c语言·开发语言·链表
bnmoel2 小时前
数据结构深度剖析顺序表:结构、扩容与增删查改全解析
c语言·数据结构·算法·顺序表
Liangwei Lin2 小时前
LeetCode 45. 跳跃游戏 II
数据结构·算法·leetcode
枕星而眠2 小时前
一篇吃透 C++ 核心基础:初始化、引用、指针、内联、重载、右值引用
开发语言·数据结构·c++·后端·visual studio
Season4502 小时前
C/C++的类型转换
c语言·开发语言·c++
平行侠3 小时前
026FFT快速乘法 - 从信号处理到大数计算的革命
数据结构·算法·信号处理
C雨后彩虹3 小时前
猴子爬山问题
java·数据结构·算法·华为·面试