题目1——括号匹配问题
题目来源. - 力扣(LeetCode)
思路——辅助栈法
括号匹配问题是一个经典的计算机科学问题,常用于检查一个字符串中的括号是否正确匹配。这包括各种括号,如小括号“()”,大括号“{}”,尖括号“<>”以及方括号“[]”。
这个问题可以通过使用栈(Stack)来解决,因为栈的后进先出(LIFO)特性使得我们可以很方便地处理嵌套结构。
以下是使用栈来解决括号匹配问题的基本思路:
初始化一个空栈:这个栈将用于存储尚未找到匹配项的左括号。
遍历输入字符串:对于字符串中的每个字符,我们执行以下操作:
- 如果遇到左括号(如'(', '{', '<', '['),我们将其压入栈中。
- 如果遇到右括号(如')', '}', '>', ']'),我们检查栈顶元素(如果存在)是否是对应的左括号。如果是,我们将栈顶元素弹出,表示找到了匹配项;如果不是或者栈为空,说明存在括号不匹配的情况,返回false。
检查栈是否为空:在遍历完整个字符串后,如果栈为空,说明所有括号都找到了匹配项,返回true;否则,栈中剩余的左括号没有找到匹配项,返回false。
这个算法的时间复杂度是O(n),其中n是输入字符串的长度,因为我们只需要遍历一次字符串。空间复杂度也是O(n),在最坏的情况下,栈中可能存储n/2个左括号(假设所有括号都是左括号)。
队列(Queue)通常用于处理具有先进先出(FIFO)特性的问题,不适合用于解决括号匹配问题,因为队列无法方便地处理嵌套结构。
代码实现
bool isValid(char* s)
{ST st;StackInit(&st);while(*s){if (*s == '(' || *s == '{' || *s == '['){StackPush(&st, *s);++s;}else{//遇到右括号了,但是栈里面没有数据//说明前面没有左括号,不匹配if (StackEmpty(&st)){StackDestroy(&st);return false;}STDataType top = StackTop(&st);StackPop(&st);if ((*s == '}' && top != '{')|| (*s == ']' && top != '[')|| (*s == ')' && top != '(')){StackDestroy(&st);return false;}else{s++;}}}//如果栈不是空,说有栈中还有左括号未出bool ret = StackEmpty(&st);StackDestroy(&st);return ret;
}
题目2——用队列实现栈
题目来源. - 力扣(LeetCode)
思路
这道题目是为初级读者准备的,题目涉及到栈和队列两种数据结构。
栈是一种后进先出的数据结构,元素从顶端入栈,然后从顶端出栈。
队列是一种先进先出的数据结构,元素从后端入队,然后从前端出队。
我们可以使用一个队列来实现这一想法
使用一个队列时,为了满足栈的特性,即最后入栈的元素最先出栈,同样需要满足队列前端的元素是最后入栈的元素。
入栈操作时,首先获得入栈前的元素个数 n,然后将元素入队到队列,再将队列中的前 n 个元素(即除了新入栈的元素之外的全部元素)依次出队并入队到队列,此时队列的前端的元素即为新入栈的元素,且队列的前端和后端分别对应栈顶和栈底。
由于每次入栈操作都确保队列的前端元素为栈顶元素,因此出栈操作和获得栈顶元素操作都可以简单实现。出栈操作只需要移除队列的前端元素并返回即可,获得栈顶元素操作只需要获得队列的前端元素并返回即可(不移除元素)。
由于队列用于存储栈内的元素,判断栈是否为空时,只需要判断队列是否为空即可。
代码实现
// 定义一个链表节点的结构体
typedef struct tagListNode { struct tagListNode* next; // 指向下一个节点的指针 int val; // 节点的值
} ListNode; // 定义一个栈的结构体
typedef struct { ListNode* top; // 栈顶元素,指向栈顶节点的指针
} MyStack; // 创建一个新的栈
MyStack* myStackCreate() { MyStack* stk = calloc(1, sizeof(MyStack)); // 使用calloc分配内存并初始化为0 return stk; // 返回新创建的栈
} // 向栈中压入一个元素
void myStackPush(MyStack* obj, int x) { ListNode* node = malloc(sizeof(ListNode)); // 为新节点分配内存 node->val = x; // 设置新节点的值 node->next = obj->top; // 新节点指向原来的栈顶 obj->top = node; // 更新栈顶为新的节点
} // 从栈中弹出并返回栈顶元素
int myStackPop(MyStack* obj) { ListNode* node = obj->top; // 获取栈顶节点 int val = node->val; // 获取栈顶节点的值 obj->top = node->next; // 更新栈顶为下一个节点 free(node); // 释放原来的栈顶节点 return val; // 返回弹出的值
} // 获取栈顶元素的值
int myStackTop(MyStack* obj) { return obj->top->val; // 直接返回栈顶节点的值
} // 判断栈是否为空
bool myStackEmpty(MyStack* obj) { return (obj->top == NULL); // 如果栈顶为NULL,则栈为空
} // 释放栈占用的内存
void myStackFree(MyStack* obj) { while (obj->top != NULL) { // 当栈不为空时 ListNode* node = obj->top; // 获取栈顶节点 obj->top = obj->top->next; // 更新栈顶为下一个节点 free(node); // 释放原来的栈顶节点 } free(obj); // 释放栈本身占用的内存
}
题目3——用栈实现队列
题目来源. - 力扣(LeetCode)
思路——双栈
使用栈来实现队列的原理主要基于两个栈的交互操作。栈(Stack)是一种后进先出(LIFO,Last In First Out)的数据结构,而队列(Queue)是一种先进先出(FIFO,First In First Out)的数据结构。因此,直接使用一个栈是无法实现队列的先进先出特性的。但是,通过两个栈的配合使用,我们可以模拟出队列的行为。
原理如下:
- 定义两个栈:我们定义两个栈,stack1 和 stack2。stack1 只用于接收入队操作(enqueue)的元素,而 stack2 只用于执行出队操作(dequeue)。
- 入队操作:当需要进行入队操作时,我们直接将元素压入 stack1。此时,stack1 中的元素顺序是后进先出的,但我们还不能直接从 stack1 中弹出元素,因为这样会破坏队列的先进先出特性。
- 出队操作:当需要进行出队操作时,我们首先检查 stack2 是否为空。如果 stack2 不为空,我们直接从 stack2 的栈顶弹出元素即可,因为 stack2 中的元素顺序已经是先进先出的了。但是,如果 stack2 为空,我们就需要将 stack1 中的所有元素逐个弹出并压入 stack2,这样 stack2 中的元素顺序就变成了先进先出。然后,我们再从 stack2 的栈顶弹出元素进行出队操作。
通过这种方式,我们可以保证队列的先进先出特性得以维持。虽然使用两个栈会增加一些操作的复杂性(特别是在 stack2 为空时需要将 stack1 中的元素全部转移到 stack2),但整体上这种方法是可以有效实现队列的。
需要注意的是,当队列为空时,stack1 和 stack2 都应该是空的。同时,为了优化性能,我们可以在入队时尽量避免不必要的元素转移操作。例如,当 stack2 不为空时,我们可以直接将新元素压入 stack2 而不是 stack1,这样可以减少后续出队时的元素转移次数。但是,这种方法需要更复杂的逻辑来控制两个栈的使用,因此在实际实现时需要根据具体需求进行权衡。
此时出队栈为空,所以要将入队栈的第一个进入的元素放入出队栈
此时出队栈不为空,执行出队操作
代码实现
typedef int STDataType;
typedef struct Stack
{
STDataType* a;
int top;
int capacity;
}ST;// 定义队列的结构体
typedef struct { ST pushst; // 入队栈 ST popst; // 出队栈
} MyQueue; // 初始化队列
MyQueue* myQueueCreate() { MyQueue* obj = (MyQueue*)malloc(sizeof(MyQueue)); if (obj == NULL) { perror("malloc fail"); return NULL; } STInit(&obj->pushst); // 初始化入队栈 STInit(&obj->popst); // 初始化出队栈 return obj;
} // 入队操作
void myQueuePush(MyQueue* obj, int x) { STPush(&obj->pushst, x); // 将元素推入入队栈
} // 查看队首元素
int myQueuePeek(MyQueue* obj) { if (STEmpty(&obj->popst)) { // 如果出队栈为空,则将入队栈中的元素全部倒入出队栈 while (!STEmpty(&obj->pushst)) { STPush(&obj->popst, STPop(&obj->pushst)); } } return STTop(&obj->popst); // 返回出队栈的栈顶元素
} // 出队操作
int myQueuePop(MyQueue* obj) { int front = myQueuePeek(obj); STPop(&obj->popst); // 弹出出队栈的栈顶元素 return front;
} // 检查队列是否为空
bool myQueueEmpty(MyQueue* obj) { return STEmpty(&obj->pushst) && STEmpty(&obj->popst);
} // 释放队列内存
void myQueueFree(MyQueue* obj) { STDestroy(&obj->pushst); STDestroy(&obj->popst); free(obj);
}
题目4——设计循环队列
题目来源. - 力扣(LeetCode)
介绍
在解决这个问题之前,我们必须明白,什么是循环队列?
循环队列是一种线性数据结构,其操作表现基于先进先出(FIFO)的原则,并通过使用固定大小的数组和两个指针来有效地管理队列的入队和出队操作。在循环队列中,当队尾指针到达数组的末尾时,它不会溢出,而是会循环回到数组的开头,从而形成一个循环。类似地,当队头指针到达数组的开头时,它也会循环到数组的末尾。
循环队列的主要特点是它有一个固定的容量,当队列满时,就不能再添加新元素;当队列空时,就不能再移除元素。通过维护两个指针(通常称为队头指针和队尾指针),我们可以实现循环队列的高效操作。队头指针指向队列中第一个元素的下一个位置(如果队列不为空),而队尾指针指向队列中最后一个元素的位置。
以下是循环队列的一些关键特性:
-
固定大小:循环队列使用一个固定大小的数组来存储元素。这意味着在队列创建时,就确定了其最大容量。
-
循环性:当队尾指针到达数组的末尾时,它会自动回到数组的开头,从而形成一个循环。同样,队头指针也会在到达数组开头时循环到末尾。
-
入队和出队操作:入队操作在队尾添加元素,队尾指针向前移动;出队操作从队头移除元素,队头指针向前移动。当队头指针或队尾指针到达数组边界时,它们会通过取模运算(
%
)回到数组的另一端,从而实现循环。 -
队列满和队列空:循环队列中,判断队列是否为空或满需要特殊的逻辑。通常,队列满的条件是
(rear + 1) % MAXSIZE == front
,队列空的条件是front == rear
。 -
空间利用率:由于循环队列使用固定大小的数组,因此其空间利用率较高。但是,如果队列的大小设置不当,可能会导致空间浪费或频繁的队列满的情况。
循环队列在多种场景中都非常有用,尤其是在需要频繁进行入队和出队操作的场景中。通过避免动态内存分配和释放的开销,循环队列可以实现高效的队列操作。
思路
我们如果用一个数组来实现这个环形队列的话,上面这三种状态就对应于以下三种状态:
可以看出,此时这个数组和环形完全扯不上关系,这其实很简单,我们只需注意判断两个地方:
1.当指针指向整个数组的后方的时候,让该指针重新指向数组的第一个元素。
2.当指针指向整个数组的前方的时候,让该指针直接指向数组最后一个有效元素的后面。
这样就使得该数组在逻辑上是“环形”的了。
代码实现
// 定义循环队列结构体
typedef struct { int* a; // 队列元素数组 int front; // 队头指针 int rear; // 队尾指针 int k; // 队列容量
} MyCircularQueue; // 创建循环队列
MyCircularQueue* myCircularQueueCreate(int k) { MyCircularQueue* obj = (MyCircularQueue*)malloc(sizeof(MyCircularQueue)); if (obj == NULL) { return NULL; // 内存分配失败时返回NULL } obj->front = obj->rear = 0; obj->a = (int*)malloc(sizeof(int) * (k + 1)); // 分配k+1个空间,用于处理队列满的情况 if (obj->a == NULL) { free(obj); // 如果数组分配失败,释放之前为obj分配的内存 return NULL; } obj->k = k; return obj;
} // 检查队列是否为空
bool myCircularQueueIsEmpty(MyCircularQueue* obj) { return obj->front == obj->rear;
} // 检查队列是否已满
bool myCircularQueueIsFull(MyCircularQueue* obj) { return (obj->rear + 1) % (obj->k + 1) == obj->front;
} // 执行入队操作
bool myCircularQueueEnqueue(MyCircularQueue* obj, int value) { if (myCircularQueueIsFull(obj)) { return false; // 队列已满,无法入队 } obj->a[obj->rear++] = value; obj->rear %=(obj->k + 1); // 更新队尾指针,实现循环 return true;
}
int myCircularQueueFront(MyCircularQueue* obj) { if (myCircularQueueIsEmpty(obj)) { return -1; // 如果队列为空,则返回-1表示没有元素可取 } else { return obj->a[obj->front]; // 返回队头元素的值 }
} int myCircularQueueRear(MyCircularQueue* obj) { if (myCircularQueueIsEmpty(obj)) { return -1; // 如果队列为空,则返回-1表示没有元素可取 } else { // 由于队尾指针指向的是下一个将要插入的位置,所以我们需要返回rear-1的元素 // 但如果rear为0,则需要通过取模操作来确保索引正确 return obj->a[(obj->rear - 1 + obj->k) % (obj->k + 1)]; }
} void myCircularQueueFree(MyCircularQueue* obj) { free(obj->a); // 释放队列元素数组的内存 free(obj); // 释放队列结构体的内存
}