栈
栈是允许在同一端进行插入和删除操作的特殊线性表。
允许进行插入和删除操作的一端称为栈顶(top),另一端为栈底(bottom);栈底固定,而栈顶浮动;
插入一般称为进栈/入栈(PUSH),删除则称为出栈/弹栈(POP);
具有后进先出(Last In, First Out,LIFO)的特性。
应用场景:
- 函数调用的执行过程(调用栈)
- 浏览器中的后退与前进
- 括号匹配
- 撤销与反撤销
基于链表的实现
1.栈的初始化
stackTop
栈顶指针:链表的表头即为栈顶
size
记录栈的大小
链表的头插是入栈操作
初始化栈的大小为0
栈顶指针初始化为 NULL
typedef struct Stack {
struct Node* stackTop;
int size;
}Stack;
Stack* createStack() {
Stack* myStack = (Stack*)malloc(sizeof(Stack));
myStack->size = 0;
myStack->stackTop = NULL;
return myStack;
}
2.链表的初始化
typedef struct Node {
int data;
struct Node* next;
}Node;
3.入栈
void push(Stack* myStack, int data) {
Node* newNode = (Node*)malloc(sizeof(Node));
newNode->data = data;
newNode->next = myStack->stackTop;//新节点的next指针指向栈顶
myStack->stackTop = newNode;//栈顶更新为新节点
myStack->size++;//计数
}
4.出栈
先入后出
void pop(Stack* myStack) {
if (myStack->size == 0) {
printf("栈为空");
}
else {
Node* nextNode = myStack->stackTop->next;
free(myStack->stackTop);
myStack->stackTop = nextNode;
myStack->size--;
}
}
5.获取栈顶元素
int top(Stack* myStack) {
if (myStack->size == 0) {
return myStack->size;
}
return myStack->stackTop->data;
}
6.判断栈是否为空
int empty(Stack* myStack) {
if (myStack->size == 0) {
return 0;
}
return 1;
}
int main() {
Stack* myStack = createStack();
push(myStack, 1);
push(myStack, 2);
push(myStack, 3);
while (empty(myStack)) {
printf("%d ", top(myStack));
pop(myStack);
}
//3 2 1
return 0;
}
基于数组的实现
1.栈的初始化
#define MAX 20
typedef struct Stack {
int* data;
int stackTop;
}Stack;
Stack* creatStack() {
Stack* myStack = (Stack*)malloc(sizeof(Stack));
myStack->data = (int*)malloc(sizeof(int) * MAX);//初始化一个大容量
myStack->stackTop = -1;
return myStack;
}
2.入栈
先递增 stackTop
指针,然后将数据放入数组中栈顶指示的位置。
void push(Stack* myStack, int data) {
myStack->data[++myStack->stackTop] = data;
}
3.出栈
减少栈顶指针: 将栈顶指针的值减少1,表示栈顶向下移动一个位置。
void pop(Stack* myStack) {
if (myStack->stackTop == -1) {
printf("空栈\n");
}
else {
myStack->stackTop--;
}
}
4.获取栈顶元素
int top(Stack* myStack){
if (myStack->stackTop == -1) {
printf("空栈\n");
return myStack->stackTop;
}
else {
return myStack->data[myStack->stackTop];
}
}
5.判断栈是否为空
int empty(Stack* myStack) {
if (myStack->stackTop == -1) {
return 0;
}
else {
return 1;
}
}
int main() {
Stack* myStack = creatStack();
push(myStack, 1);
push(myStack, 2);
push(myStack, 3);
while (empty(myStack)) {
printf("%d ", top(myStack));
pop(myStack);
}
return 0;
}
总结
链栈和 数组栈的进栈push和出栈pop操作时间复杂度均为O(1)。
栈的应用
20.有效的括号
给定一个只包括
'('
,')'
,'{'
,'}'
,'['
,']'
的字符串s
,判断字符串是否有效。有效字符串需满足:
- 左括号必须用相同类型的右括号闭合。
- 左括号必须以正确的顺序闭合。
- 每个右括号都有一个对应的相同类型的左括号。
不匹配的场景包括:
1.左边多了;
2.右边多了;
3.不匹配。
遇到左括号,将右括号入栈
遇到右括号,弹栈匹配比对
bool isValid(char* s) {
// 检查字符串长度是否为偶数,如果是奇数则返回 false
if(strlen(s) % 2 != 0){
return false;
}
char* t = (char*)malloc(sizeof(char) * (strlen(s) + 1));
int i = 0;
int top = 0; // 栈顶指针
for(i = 0; i < strlen(s); i++) {
// 遇到左括号,将相应的右括号入栈
if(s[i]=='(') {
t[top++] = ')';
} else if(s[i]=='{') {
t[top++] = '}';
} else if(s[i]=='[') {
t[top++] = ']';
} else if(s[i]==')' || s[i]=='}' || s[i]==']') {
// 遇到右括号,检查栈是否为空或者栈顶元素是否匹配
if(top == 0 || t[--top] != s[i]) {
return false;
}
}
}
// 检查栈是否为空,不为空说明有未闭合的左括号
if(top != 0) {
return false;
}
return true;
}
1047. 删除字符串中的所有相邻重复项
给出由小写字母组成的字符串
S
,重复项删除操作会选择两个相邻且相同的字母,并删除它们。在 S 上反复执行重复项删除操作,直到无法继续删除。
在完成所有重复项删除操作后返回最终的字符串。答案保证唯一。
用字符串模拟栈
char * removeDuplicates(char * s){
//求出字符串长度
int strLength = strlen(s);
//开辟栈空间。栈空间长度应为字符串长度+1(为了存放字符串结束标志'\0')
char* stack = (char*)malloc(sizeof(char) * strLength + 1);
int stackTop = 0;
int index = 0;
//遍历整个字符串
while(index < strLength) {
//取出当前index对应字母,之后index+1
char letter = s[index++];
//若栈中有元素,且栈顶字母等于当前字母(两字母相邻)。将栈顶元素弹出
if(stackTop > 0 && letter == stack[stackTop - 1])
stackTop--;
//否则将字母入栈
else
stack[stackTop++] = letter;
}
//存放字符串结束标志'\0'
stack[stackTop] = '\0';
//返回栈本身作为字符串
return stack;
}
队列
队列是一种只允许在表的前端(front)进行删除操作,而在表的后端(rear)进行插入操作的数据结构。
进行插入操作的端称为队尾,进行删除操作的端称为队头。
先进先出(First In, First Out,FIFO)的数据结构。
长度有限,队空不可删除,队满不可插入
应用场景:
- 任务调度
- 打印队列
- 缓冲区管理
基于链表的实现
1.队的初始化
要知道队首队尾元素,需要指向队列头部和尾部的节点指针 frontNode
和 tailNode
typedef struct Node {
int data;
struct Node* next;
}Node;
typedef struct Queue {
struct Node* frontNode;
struct Node* tailNode;
int size;
}Queue;
Queue* createQueue() {
Queue* myQueue = (Queue*)malloc(sizeof(Queue));
myQueue->frontNode = myQueue->tailNode = NULL;
myQueue->size = 0;
return myQueue;
}
2.入队
如果队列为空,则令头、尾节点都指向该节点
如果队列不为空,将新节点追加到队列尾部,并更新尾部节点指针
void push(Queue* myQueue,int data) {
Node* newNode = createNode(data);
if (myQueue->size == 0) {
myQueue->frontNode = myQueue->tailNode = newNode;
} else {
myQueue->tailNode->next = newNode;
myQueue->tailNode = newNode;
}
myQueue->size++;
}
3.出队
void pop(Queue* myQueue) {
if (myQueue->size == 0) {
printf("队列为空");
system("pause");
return;
} else {
Node* newNode = myQueue->frontNode->next;// 指向队首的下一个节点
free(myQueue->frontNode);// 释放队列的头节点的内存
myQueue->frontNode = newNode;//队列的头节点指针指向下一个节点
myQueue->size--;
}
}
4.获取队首元素
int front(Queue* myQueue){
if (myQueue->size == 0) {
printf("队列为空");
system("pause");
return 0;
}
return myQueue->frontNode->data;
}
5.判断队是否为空
int empty(Queue* myQueue) {
if (myQueue->size == 0) {
return 0;
} else{
return 1;
}
}
int main() {
Queue* myQueue = createQueue();
push(myQueue, 1);
push(myQueue, 2);
push(myQueue, 3);
while (empty(myQueue)) {
printf("%d ", front(myQueue));
pop(myQueue);
}
return 0;
}
基于数组的实现
在数组中删除首元素的时间复杂度为 O(n) ,导致出队操作效率较低。
我们可以采用以下方法来避免这个问题:
使用一个变量
front
指向队首元素的索引,并维护一个变量size
用于记录队列长度。定义
rear = front + size
,这个公式计算出的rear
指向队尾元素之后的下一个位置。
因此,数组中包含元素的有效区间为 [front, rear - 1]
- 入队操作:将输入元素赋值给
rear
索引处,并将size
增加 1 。 - 出队操作:只需将
front
增加 1 ,并将size
减少 1 。
可以看到,入队和出队操作都只需进行一次操作,时间复杂度均为 O(1)
typedef struct {
int* nums; // 用于存储队列元素的数组
int front; // 队首指针,指向队首元素
int queSize; // 尾指针,指向队尾 + 1
int queCapacity; // 队列容量
} Queue;
1.初始化
Queue* createQueue(int capacity) {
Queue* queue = (Queue*)malloc(sizeof(Queue));
// 初始化数组
queue->queCapacity = capacity;
queue->nums = (int*)malloc(sizeof(int) * queue->queCapacity);
queue->front = queue->queSize = 0;
return queue;
}
2.获取队列的容量
int capacity(Queue* queue) {
return queue->queCapacity;
}
3.获取队列的长度
方法一:
int size(Queue* queue) {
return queue->queSize;
}
方法二:
此时front指向队首,rear指向队尾
rear > front : rear - front + 1
不做讨论
rear < front : rear- front + 1 + maxsize
总结为:
(rear - front + 1 + maxsize) % maxsize
4.判断队列是否为空
方法一:
bool empty(Queue* queue) {
return queue->queSize == 0;
}
方法二:
当队列为空时,有front = rear
5.判断队列是否为满
在循环队列中,我们使用取余操作来实现队尾指针 rear 在越过数组尾部时回到数组的开头,从而形成环形队列。这样可以充分利用数组空间,避免出现队列已满但实际上还有空间的情况。
当队列为空时,有front = rear
当所有队列空间全占满时,也有front = rear
为了区别这两种情况
方法一:
int isQueueFull(Queue* queue){
if (queue->queSize == queue->queCapacity) {
return 1;
}
return 0;
方法二:
牺牲一个存储单元
一种常见的做法是将队列的实际容量减一,即队列的最大容量为 capacity - 1
,这样可以通过 (rear + 1) % capacity == front 来判断队列是否已满。
front
指向队首元素,rear
指向队尾元素下一项
6.访问队首元素
int peek(Queue* queue) {
assert(size(queue) != 0);
return queue->nums[queue->front];//队列的大小不为零,返回队首元素
}
7.入队
在不断进行入队和出队的过程中,front
和 rear
都在向右移动,当它们到达数组尾部时就无法继续移动了。为了解决此问题,我们可以将数组视为首尾相接的“环形数组”。
对于环形数组,我们需要让 front
或 rear
在越过数组尾部时,直接回到数组头部继续遍历。这种周期性规律可以通过“取余操作”来实现
void push(Queue* queue, int num) {
if (size(queue) == capacity(queue)) {
printf("队列已满\r\n");
return;
}
// 计算队尾指针,指向队尾索引 + 1
// 通过取余操作实现 rear 超过数组的长度时,回到数组的开头。
int rear = (queue->front + queue->queSize) % queue->queCapacity;
// 将 num 添加至队尾
queue->nums[rear] = num;
queue->queSize++;
}
8.出队
int pop(Queue* queue) {
if (empty(queue)) {
printf("队列为空\n");
return -1;
}
int num = peek(queue); // 保存出队元素
queue->front = (queue->front + 1) % queue->queCapacity;
queue->queSize--;
return num; // 返回出队的元素
}
参考:
感谢您的阅读