栈和队列初探

栈是允许在同一端进行插入和删除操作的特殊线性表。

允许进行插入和删除操作的一端称为栈顶(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. 每个右括号都有一个对应的相同类型的左括号。

不匹配的场景包括:

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.入队

在不断进行入队和出队的过程中,frontrear 都在向右移动,当它们到达数组尾部时就无法继续移动了。为了解决此问题,我们可以将数组视为首尾相接的“环形数组”。

对于环形数组,我们需要让 frontrear 在越过数组尾部时,直接回到数组头部继续遍历。这种周期性规律可以通过“取余操作”来实现
在这里插入图片描述

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;  // 返回出队的元素
}

参考:

1.Hello 算法 (hello-algo.com)

2.代码随想录 (programmercarl.com)


感谢您的阅读

  • 31
    点赞
  • 26
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值