栈实现队列算法解析

我现在得用C语言写一个用栈实现队列的算法题。首先,我得先回忆一下队列和栈的特性。队列是先进先出(FIFO)的,而栈是后进先出(LIFO)的。所以,用栈来模拟队列的话,可能需要两个栈来配合,一个用来处理入队操作,另一个处理出队操作。

那具体怎么实现呢?比如,假设有两个栈,一个输入栈s1,一个输出栈s2。当有元素入队的时候,就直接压入s1。当需要出队的时候,如果s2为空,就把s1里的所有元素依次弹出并压入s2,这样s2的栈顶元素就是队列的第一个元素,弹出即可。如果s2不为空,就直接弹出s2的栈顶。这样的话,就能保证FIFO的顺序了。

那在C语言里,怎么实现这两个栈呢?首先,得定义栈的结构,可能需要动态数组或者固定大小的数组。考虑到题目可能没有说明队列的最大容量,可能需要动态扩容。不过,可能题目希望用固定大小的结构,或者假设容量足够?这时候可能需要询问或者假设。这里可能需要用两个动态增长的栈结构。

然后,具体的操作:入队(push)、出队(pop)、查看队首元素(peek)、判断队列是否为空(empty)这些函数需要实现。

那具体步骤:

  1. 定义两个栈的结构,每个栈有一个数组指针,记录栈顶的位置,以及可能的最大容量。

  2. 初始化这两个栈,比如初始时栈顶为-1,或者0,根据实现方式。

  3. 入队操作:直接将元素压入s1。

  4. 出队操作:如果s2为空,将s1的所有元素弹出并压入s2,然后弹出s2的栈顶。如果s2不为空,直接弹出。

  5. 查看队首元素:类似出队操作,但只是查看而不弹出。这时候同样需要将s1的元素倒入s2后,查看s2的栈顶。

  6. 判断队列是否为空:当两个栈都为空时,队列为空。

那在C语言中,如何具体实现栈的结构呢?比如,每个栈可以用结构体包含元素数组、栈顶索引、容量。或者,因为题目可能需要更简化的结构,比如用两个数组和两个指针来表示栈?

比如:

typedef struct {

int *data;

int top;

int capacity;

} Stack;

然后创建两个Stack变量,s1和s2。不过,在C语言中,动态调整栈的容量可能需要realloc,这会增加复杂度。或者题目中的队列操作可能在有限的容量下进行,这时候可以预设一个足够大的数组。比如,假设每个栈的最大容量足够大,或者题目中的测试用例不会超过某个范围。如果题目没有说明,可能需要处理扩容的情况,但这样代码会复杂一些。

例如,假设每次入栈的时候,如果栈已满,则扩容为原来的两倍。这可能需要在push函数里处理。

但这样的话,每个栈的操作都要有对应的push和pop函数,同时处理可能的扩容问题。对于队列的实现来说,这可能会比较繁琐。

或者,可能题目希望用两个固定大小的栈,这时用户需要保证队列的操作不会超过栈的总容量?比如,每个栈的大小足够处理所有可能的元素,这样在代码中可以不做动态扩容处理,简化代码。

这里可能需要做一个权衡。或者,题目中的测试用例不会有超过一定数量的操作,所以在实现的时候可以预设一个较大的容量。

比如,假设每个栈的初始容量为100,当需要扩容时,比如当压入元素时栈已满,就扩容到两倍大小。这样,栈的结构体需要包含动态数组、当前容量、栈顶索引。

那具体实现步骤:

首先,定义栈的结构体和基本操作函数:

typedef struct {

int *data;

int top;

int capacity;

} Stack;

// 创建栈

Stack* createStack() {

Stack* s = (Stack*)malloc(sizeof(Stack));

s->capacity = 4; // 初始容量

s->data = (int*)malloc(sizeof(int) * s->capacity);

s->top = -1; // 空栈时top是-1

return s;

}

// 压栈

void pushStack(Stack* s, int val) {

if (s->top == s->capacity -1) {

// 需要扩容

s->capacity *= 2;

s->data = (int*)realloc(s->data, sizeof(int)*s->capacity);

}

s->data[++s->top] = val;

}

// 判断栈是否为空

int isStackEmpty(Stack* s) {

return s->top == -1;

}

// 弹栈

int popStack(Stack* s) {

if (isStackEmpty(s)) {

return -1; // 错误处理,可能需要其他方式

}

return s->data[s->top–];

}

// 查看栈顶元素

int peekStack(Stack* s) {

if (isStackEmpty(s)) {

return -1;

}

return s->data[s->top];

}

然后,队列的结构体包含两个栈:

typedef struct {

Stack* s1; // 输入栈

Stack* s2; // 输出栈

} MyQueue;

创建队列的函数:

MyQueue* myQueueCreate() {

MyQueue* q = (MyQueue*)malloc(sizeof(MyQueue));

q->s1 = createStack();

q->s2 = createStack();

return q;

}

入队操作:

void myQueuePush(MyQueue* obj, int x) {

pushStack(obj->s1, x);

}

出队操作:

int myQueuePop(MyQueue* obj) {

// 如果s2为空,将s1的元素全部倒入s2

if (isStackEmpty(obj->s2)) {

while (!isStackEmpty(obj->s1)) {

int val = popStack(obj->s1);

pushStack(obj->s2, val);

}

}

return popStack(obj->s2);

}

查看队首元素:

int myQueuePeek(MyQueue* obj) {

if (isStackEmpty(obj->s2)) {

while (!isStackEmpty(obj->s1)) {

int val = popStack(obj->s1);

pushStack(obj->s2, val);

}

}

return peekStack(obj->s2);

}

判断队列是否为空:

bool myQueueEmpty(MyQueue* obj) {

return isStackEmpty(obj->s1) && isStackEmpty(obj->s2);

}

释放队列:

void myQueueFree(MyQueue* obj) {

free(obj->s1->data);

free(obj->s1);

free(obj->s2->data);

free(obj->s2);

free(obj);

}

但这里可能需要注意,当栈为空时,弹出或者查看栈顶元素时的错误处理。比如,在题目中,是否保证pop或者peek操作在队列不为空时调用?如果是的话,那在代码中可以假设这些操作在合法的情况下调用,否则需要处理错误。比如,题目可能要求当队列为空时调用pop或者peek时返回-1或者其他特定值,或者在代码中做相应的处理。

另外,在C语言中,题目可能需要实现特定的结构,比如函数名和参数必须符合题目要求。例如,题目可能给出函数原型,比如:

typedef struct {

} MyQueue;

MyQueue* myQueueCreate();

void myQueuePush(MyQueue* obj, int x);

int myQueuePop(MyQueue* obj);

int myQueuePeek(MyQueue* obj);

bool myQueueEmpty(MyQueue* obj);

void myQueueFree(MyQueue* obj);

这个时候,前面的结构体需要按照这个来定义。例如,MyQueue结构体内部包含两个栈s1和s2。

所以上面的代码结构可能需要调整,比如将Stack结构体的定义包含在MyQueue内部,或者作为内部结构。不过,这在C语言中可能需要提前声明或者放在头文件中,但在算法题中,可能需要将所有结构体定义在同一个文件中。

综上,最终的代码应该包括:

  • MyQueue结构体,包含两个Stack指针。

  • 各个队列操作函数,内部调用栈的操作函数。

  • 确保在pop或peek时,当s2为空时将s1中的元素全部倒入s2。

  • 处理栈的动态扩容问题,但可能在题目测试用例中,栈的大小不会超出,所以动态扩容的代码是否正确可能影响结果。

另外,需要考虑内存管理的问题,比如在free队列的时候,需要先释放两个栈的data数组,再释放栈结构体本身,再释放队列结构体。

现在,测试一下这个过程是否正确。例如,假设依次入队1、2、3,然后出队应该得到1。此时,s1中有元素吗?第一次入队1,push到s1。入队2,push到s1。入队3,push到s1。当调用pop时,s2为空,所以将s1中的元素依次弹出并压入s2。此时s2中的元素是3,2,1?或者原s1是1,2,3,当弹出并压入s2时,顺序会反转。例如,s1的弹出顺序是3,2,1,压入s2的顺序是3→2→1,此时s2中的元素是1在最上面?或者是不是这样?

哦,原来栈的push顺序是每次压到栈顶。比如,s1中的元素是1、2、3,其中栈顶是3。当弹出时,弹出顺序是3、2、1。然后将这些元素依次压入s2,所以s2的元素顺序是3、2、1?或者,每次弹出的是3,然后压入s2,那么s2中的元素是3,然后是2,然后是1。此时s2的栈顶是1。对吗?这样的话,当从s2弹出时,得到的是1,也就是队列的队首元素。是的,这样是对的。

例如,入队顺序是1→2→3,队列应该是1、2、3。用两个栈模拟的话,当需要出队时,将s1中的元素全部倒入s2,此时s2中的元素是3、2、1,栈顶是1。出队时弹出1,正确。之后如果继续出队,s2中还有2、3,依次弹出2,然后3。如果此时再次入队4,则压入s1。之后如果出队的话,先弹出s2中的3,然后s2为空,再把s1中的4倒入s2,弹出4。这样的顺序是正确的。

那这个逻辑是对的。所以,在代码中,当s2为空时,将s1的元素全部弹出并压入s2,这样s2的栈顶元素就是队列的头部。

那现在,代码部分的问题可能出现在栈的实现是否正确,比如栈的top是否初始化为-1,或者是否正确处理了扩容。

例如,假设栈的初始容量是4,当压入多个元素时,是否自动扩容?比如,当s1中有n个元素,n超过初始容量时,会扩容到8,然后继续存储。这样在多次入队时不会出现栈溢出的问题。

另外,在队列的free操作中,是否正确释放了栈的data数组?例如,在myQueueFree函数中,先释放s1的data数组,再释放s1结构体,同样处理s2,最后释放队列结构体。

可能的错误点:

  1. 在栈的pop函数中,没有检查栈是否为空,导致访问非法内存。比如,如果栈是空的,调用pop会导致错误。但根据题目要求,可能假设所有操作都是有效的。比如,当队列为空时不会调用pop或者peek。否则,代码需要处理错误情况,返回特定值或者断言。

例如,在myQueuePop函数中,如果队列为空,应该怎么处理?题目可能要求在这种情况下返回-1或者进行其他处理。但一般情况下,题目可能保证在非空队列时才会调用这些操作。所以代码可能不需要处理错误情况。或者,在队列为空时调用pop或peek会导致未定义行为。

在本题中,是否需要处理这种情况呢?可能题目中的测试用例会保证合法操作,所以可以忽略错误处理。但为了代码的鲁棒性,可能需要添加判断,比如在myQueuePop中,如果队列为空,返回-1或者报错。此时,需要调用myQueueEmpty来判断队列是否为空,如果是,返回错误。

例如,修改myQueuePop函数:

int myQueuePop(MyQueue* obj) {

if (myQueueEmpty(obj)) {

return -1; // 或者处理错误

}

// 原来的代码

}

同理,myQueuePeek函数也需要处理这种情况。但原题可能假设这些操作在队列非空时调用,因此可能不需要处理。例如,LeetCode中的类似题目通常会在函数说明中注明,当队列为空时调用pop或peek操作返回什么值。比如,LeetCode 232题用栈实现队列,题目中的peek和pop操作保证队列非空时调用。所以代码可能不需要处理错误情况。

现在,假设按照上述代码实现,可以正确模拟队列的操作。那这个代码是否适用于所有情况?

例如,连续入队和出队交替的情况。比如,入队1,出队1,此时s2弹出后为空。然后入队2,入队3,出队2,这时候,出队时s2为空,需要将s1中的2、3倒入s2,得到顺序是3、2,弹出栈顶2?不对啊。哦,原队列是先进先出,这时候入队1,出队1是正确的。然后入队2、3,队列中的顺序是2→3。出队操作应该返回2。此时,当执行出队时,s2为空,所以将s1中的元素倒入s2。s1中的元素是2、3,弹出顺序是3→2,压入s2后顺序是2→3,所以栈顶是3?那此时弹出的是3,这显然错误。这说明前面的逻辑有问题?

哦,这里发现了大问题!这时候,队列的顺序应该是2→3,出队应该得到2。但按照上面的逻辑,当s1中有2、3(假设入队顺序是push到s1),此时将s1中的元素倒入s2时,顺序会被反转。比如,s1中的元素是2、3,栈顶是3。弹出s1的顺序是3,然后是2,压入s2的顺序是3→2,此时s2中的元素是3在栈底,2在栈顶。此时,弹出s2的栈顶元素是2,这样正确。那刚才的错误分析是不对的。

比如,原s1是push顺序是2,3。栈s1中的元素是[2,3],栈顶是3。当弹出所有元素时,顺序是3,2,依次压入s2。此时s2中的元素是3、2,栈顶是2。所以弹出时得到的是2,正确。哦,没问题。那前面的例子是正确的。

那这个逻辑是对的。因为s1中的元素顺序是先进后出,当被倒入s2后,顺序反转,所以在s2中的顺序是原队列顺序的反转的反转,即原队列顺序。比如,队列的入队顺序是1、2、3,此时在s1中的顺序是1、2、3,栈顶是3。倒入s2后,s2中的顺序是3、2、1,栈顶是1。弹出1,正确。之后如果继续入队4,则压入s1。此时s1中是4,s2中是3、2。再出队时,s2不为空,弹出2。正确。如果此时再出队,s2弹出3。此时s2为空,s1中是4。此时再出队的话,将s1中的4倒入s2,弹出4。所以整个过程是正确的。

所以,这个逻辑是正确的。那代码的实现没有问题。

那现在,这样的代码在C语言中是否可行?

例如,当创建栈的时候,初始容量为4,如果入队的元素超过4个,会自动扩容。这样,在多次入队操作时,不会有栈溢出的问题。

综上,这个方案是正确的,可以正确用两个栈模拟队列的操作。


要使用栈实现队列,可以利用两个栈,一个作为输入栈(s1),另一个作为输出栈(s2)。输入栈负责处理入队操作,而输出栈负责处理出队和查看队首元素操作。当输出栈为空时,将输入栈的所有元素依次弹出并压入输出栈,以反转元素顺序,确保队列的先进先出特性。

方法思路

  1. 入队操作:直接将元素压入输入栈(s1)。
  2. 出队操作:如果输出栈(s2)为空,将输入栈的所有元素依次弹出并压入输出栈,然后弹出输出栈的栈顶元素。如果输出栈不为空,直接弹出栈顶元素。
  3. 查看队首元素:与出队操作类似,但仅查看栈顶元素而不弹出。
  4. 判断队列是否为空:当两个栈都为空时,队列为空。

解决代码

#include 
#include 

typedef struct {
    int *data;
    int top;
    int capacity;
} Stack;

Stack* createStack() {
    Stack* stack = (Stack*)malloc(sizeof(Stack));
    stack->capacity = 4;
    stack->data = (int*)malloc(sizeof(int) * stack->capacity);
    stack->top = -1;
    return stack;
}

void pushStack(Stack* stack, int x) {
    if (stack->top == stack->capacity - 1) {
        stack->capacity *= 2;
        stack->data = (int*)realloc(stack->data, sizeof(int) * stack->capacity);
    }
    stack->data[++stack->top] = x;
}

int popStack(Stack* stack) {
    return stack->data[stack->top--];
}

int peekStack(Stack* stack) {
    return stack->data[stack->top];
}

bool isStackEmpty(Stack* stack) {
    return stack->top == -1;
}

typedef struct {
    Stack* s1;
    Stack* s2;
} MyQueue;

MyQueue* myQueueCreate() {
    MyQueue* queue = (MyQueue*)malloc(sizeof(MyQueue));
    queue->s1 = createStack();
    queue->s2 = createStack();
    return queue;
}

void myQueuePush(MyQueue* obj, int x) {
    pushStack(obj->s1, x);
}

int myQueuePop(MyQueue* obj) {
    if (isStackEmpty(obj->s2)) {
        while (!isStackEmpty(obj->s1)) {
            int val = popStack(obj->s1);
            pushStack(obj->s2, val);
        }
    }
    return popStack(obj->s2);
}

int myQueuePeek(MyQueue* obj) {
    if (isStackEmpty(obj->s2)) {
        while (!isStackEmpty(obj->s1)) {
            int val = popStack(obj->s1);
            pushStack(obj->s2, val);
        }
    }
    return peekStack(obj->s2);
}

bool myQueueEmpty(MyQueue* obj) {
    return isStackEmpty(obj->s1) && isStackEmpty(obj->s2);
}

void myQueueFree(MyQueue* obj) {
    free(obj->s1->data);
    free(obj->s1);
    free(obj->s2->data);
    free(obj->s2);
    free(obj);
}

代码解释

  1. 栈结构定义:使用动态数组实现栈,支持自动扩容。
  2. 队列结构定义:包含两个栈,分别用于输入和输出。
  3. 入队操作:将元素压入输入栈,时间复杂度为O(1)。
  4. 出队操作:若输出栈为空,将输入栈元素依次压入输出栈后弹出栈顶元素,分摊时间复杂度为O(1)。
  5. 查看队首元素:类似出队操作,但仅查看栈顶元素。
  6. 判断队列是否为空:当两个栈均为空时队列为空,时间复杂度O(1)。
  7. 内存管理:释放栈和队列时,确保释放动态分配的内存,避免内存泄漏。

你可能感兴趣的:(我的博客,算法)