栈和队列基本理论及代码实现

前言

栈和队列是两种常见的数据结构,它们都是一种特殊的线性表,由于它们本身所具有的不同特点和操作特性,使得他们在算法设计和程序开发中非常有用,以下是对栈和队列的基本原理、代码实现的简单描述。

一、栈

1.栈的基本概念

  (stack)是一种特殊的线性表,仅限在一端进行操作(即只在栈顶进行插入和删除)。它的操作特点遵循后进先出(LIFO)的原则,即最后进入栈的元素最先被取出。类似于我们在箱子里进行收纳,最先被放进去的东西位于箱子的最底下,拿的时候需要将上面所有东西都拿出来,确保其在最顶层,才能将其拿出来。
  栈的主要操作包括压栈(push),即向栈顶添加元素;以及弹栈(pop),即移除栈顶的元素并返回其值。栈的存储结构可以是顺序存储(如数组)或链式存储(如链表),这两种储存方式也就意味着我们可以用两种方式来实现栈——数组和链表。

栈的基本原理的绘图实现

2.栈的实现

  栈可以通过数组(顺序存储)或链表(链式存储)来实现,其中用数组实现时,虽然较为简单直观,但会受限于数组的大小,会出现内存过剩或者内存不够的情况;而链表实现则较为灵活,因为它可以动态申请空间来分配内存,不易受固定大小的限制。

数组实现

  使用数组实现栈时,需要定义一个固定大小的数组,并使用一个指针(通常称为栈顶指针,我们一般将其命名为top)来指示栈顶元素的位置。在进行一系列栈的操作之前,不要忘记初始化栈顶指针top!

初始化

  在初始化时,我们通常令top=-1(因为数组元素是从0开始计数)来表示栈内还未存放元素,从而记录栈内存放元素的情况,也可以初始化top为0等其他值(这里笔者初始化top为-1,如果初始化为其他值,后续的代码处理会与笔者的有细微出入)

void initStack(Stack* stack) {
    stack->top = -1;
}
入栈

  压入操作(Push):将元素添加到数组的末尾,并将栈顶指针指向新添加的元素,通常称为入栈、压栈、进栈。在入栈之前,要先判断栈是否为满,为满时不能入栈,否则会导致栈溢出。

// 判断栈满
int isFull(Stack* stack) {
    if (stack->top == MAX_SIZE - 1) {  //如果top等于栈的大小-1就意味着栈满(栈存放元素是从top=0开始的)
        return 1;
    }
    return 0;
}
// 入栈
void push(Stack* stack, int value) {
    if (isFull(stack)) {
        printf("栈溢出\n");
        return;  //如果栈满,则提示栈满后跳出程序
    }
    stack->items[++stack->top] = value;
}
判断栈空及出栈

  弹出操作(Pop):将栈顶指针向下移动一个位置,并返回出栈的元素,表示从栈顶移除元素,通常称为出栈。在出栈前要先判断栈空,若栈为空则无法进行出栈操作。

//判断栈空
int isEmpty(Stack* stack) {
    if (stack->top == -1) {
        return 1;  //如果top等于-1,则栈为空
    }
    return 0;
}
//出栈
int pop(Stack* stack) {
    if (isEmpty(stack)) {
        printf("栈为空\n");
        return -1;
    }
    return stack->items[stack->top--];
}
获取栈顶元素

  在获取栈顶元素之前,也要判断栈是否为空即栈顶元素是否存在。

int peek(Stack* stack) {
    if (isEmpty(stack)) {
        printf("Stack is empty\n");
        return -1;
    }
    return stack->items[stack->top];
}
完整代码
#include <stdio.h>
#include <stdlib.h>
#define MAX_SIZE 100   //定义数组大小

// 定义栈结构
typedef struct {
    int items[MAX_SIZE];
    int top;
} Stack;

// 初始化栈
void initStack(Stack* stack) {
    stack->top = -1;
}

// 判断栈是否为空
int isEmpty(Stack* stack) {
    if (stack->top == -1) {
        return 1; 
    }
    return 0;
}

// 判断栈是否已满
int isFull(Stack* stack) {
    if (stack->top == MAX_SIZE - 1) { 
        return 1;
    }
    return 0;
}
// 入栈操作
void push(Stack* stack, int value) {
    if (isFull(stack)) {
        printf("Stack overflow\n");
        return;
    }
    stack->items[++stack->top] = value;
}

// 出栈操作
int pop(Stack* stack) {
    if (isEmpty(stack)) {
        printf("Stack underflow\n");
        return -1;
    }
    return stack->items[stack->top--];
}

// 获取栈顶元素
int peek(Stack* stack) {
    if (isEmpty(stack)) {
        printf("Stack is empty\n");
        return -1;
    }
    return stack->items[stack->top];
}

//主函数
int main() {
    Stack stack;
    initStack(&stack);

    push(&stack, 10);
    push(&stack, 20);
    push(&stack, 30);

    printf("Top element: %d\n", peek(&stack));

    printf("Popped element: %d\n", pop(&stack));
    printf("Popped element: %d\n", pop(&stack));
    printf("Popped element: %d\n", pop(&stack));

    return 0;
}

  这种实现方式较为简单直观,但可能会受到数组大小限制,因为数组在定义完后,所占有的空间是固定的,在用数组实现栈时,若申请空间过大会浪费内存,但若申请空间太小又会造成空间不够的问题,所以其灵活性较差。

链表实现

  使用链表实现栈时,每个结点必须包含一个数据域和指向下一个结点的指针域。这里会涉及到链表的增删和链表的遍历及打印,类比来说,入栈就是将新的结点插入链表的头部;出栈就类似于删去链表的头结点。笔者在这里使用了虚拟头结点,方便后续进行头插法和对链表的头结点进行删除。
在这里插入图片描述

初始化

  声明链表的头结点并对其内的数据和指针进行初始化。

//初始化栈
Node* InitStack() {
	Node* S = (Node *)malloc(sizeof(Node));
	S->data = 0;  //对栈内元素个数进行计数
	S->next = NULL;
	return S;
}
入栈

  压入操作(Push):创建一个新结点,将其链接到链表的头部,令虚拟头结点直接指向新节点,并更新栈顶指针指向新结点,类似于链表的头插法。用链表实现入栈时不用判断栈是否为满,因为可以为其申请内存开辟空间。

int push(Node* S, int val) {
	Node* node = (Node*)malloc(sizeof(Node)); //声明一个新结点并为其开辟空间
	node->data = val;
	node->next = S->next;
	S->next = node;
	S->data++;
}

判断栈空及出栈

  弹出操作(Pop):将栈顶结点从链表中移除,让虚拟头结点指向需要删除的结点的下一个结点,并更新栈顶指针指向下一个节点,类似于链表中删除头结点。

//判断栈空
int isEmpty(Node* S) {
	if (S->data == 0 || S->next == NULL) {
		return 1;
	}
	return 0;
}
//出栈(在这之前,要先判断栈是否为空。不为空才能出栈,为空则出栈失败)
int pop(Node* S) {
	if (isEmpty(S)) {
		return -1;
	} else {
		Node* node = S->next;
		int data = node->data;
		S->next = node->next;
		free(node);
		return data;
	}
}
获取栈顶元素

  在获取栈顶元素之前要判断栈是否为空,不为空就返回虚拟头结点的下一个结点内的数据域即可。

int peek(Node* S) {
	if (isEmpty(S)) {
		return -1;
	} else {
		return S->next->data;
	}
}
打印栈

  打印栈即遍历链表并打印。

void PrintStack(Node* S) {
	Node* node = S->next;
	while (node) {
		printf("%d->", node->data);
		node = node->next;
	}
	printf("NULL\n");
}
完整代码
//用链表实现栈
#include <stdio.h>
#include <stdlib.h>
typedef struct Node {
	int data;
	struct Node* next;
}Node;
//初始化栈
Node* InitStack() {
	Node* S = (Node*)malloc(sizeof(Node));
	S->data = 0; //对栈内元素个数进行计数
	S->next = NULL;
	return S;
}
//判断栈空
int isEmpty(Node* S) {
	if (S->data == 0 || S->next == NULL) {
		return 1;
	}
	return 0;
}
//获取栈顶元素
int peek(Node* S) {
	if (isEmpty(S)) {
		return -1;
	} else {
		return S->next->data;
	}
}
//出栈(在这之前,要先判断栈是否为空。不为空才能出栈,为空则出栈失败)
int pop(Node* S) {
	if (isEmpty(S)) {
		return -1;
	} else {
		Node* node = S->next;
		int data = node->data;
		S->next = node->next;
		free(node);
		return data;
	}
}
//入栈
int push(Node* S, int val) {
	Node* node = (Node*)malloc(sizeof(Node));
	node->data = val;
	node->next = S->next;
	S->next = node;
	S->data++;
}
//遍历栈
void PrintStack(Node* S) {
	Node* node = S->next;
	while (node) {
		printf("%d->", node->data);
		node = node->next;
	}
	printf("NULL\n");
}
//主函数
int main() {
	Node* S = InitStack();
	push(S, 1);   
	PrintStack(S);   //每进行一次入栈或出栈操作都打印一次,方便观察
	push(S, 2);
	PrintStack(S);
	push(S, 3);
	PrintStack(S);
	push(S, 4);
	PrintStack(S);
	int i = pop(S);
	printf("current elem = %d\n", i);
	PrintStack(S);
	return 0;
}

  链表实现不受固定大小的限制,较为灵活,其可以动态开辟空间来为入栈元素分配内存。

二、队列

1.队列的基本概念

  队列也是一种线性表,只允许在其一端(即队尾)进行入队操作,在另一端(即队头)进行出队操作。其操作特点遵循先进先出(FIFO)原则,即最先进入队列的元素最先被取出。类似于我们在餐厅排队买饭,先进入队伍排队的人可以先拿到饭离开,后面来的人要等前面的人都拿上饭离开,才能买饭离开。
  队列的主要操作包括入队(enqueue),即向队尾添加元素;以及出队(dequeue),即移除队头的元素并返回其值。队列的存储结构同样可以是顺序存储(如数组)或链式存储(如链表)。
在这里插入图片描述

2.队列的实现

数组实现

  与栈类似,也是使用一个固定大小的数组来保存队列中的元素,但不同的是,队列要使用两个指针,来分别标识队列的头部和尾部

初始化
void initQueue(Queue* queue) {
    queue->front = 0;
    queue->rear = -1;
}

入队

  入队操作(enqueue):将新元素添加到数组的尾部,并更新尾部指针。入队之前,要先判断对是否为满,为满则无法入队。

// 判断队列是否已满
int isFull(Queue* queue) {
    if (queue->rear == MAX_SIZE - 1) {
        return 1;
    }
    return 0;
}

// 入队操作
void enqueue(Queue* queue, int value) {
    if (isFull(queue)) {
        printf("队已满\n");
        return;
    }
    queue->items[++queue->rear] = value;
}
判断队空并出队

  出队操作(Dequeue):移除数组的头部元素,并更新头部指针。出队前要判断队是否为空,为空则无法出队。

// 判断队列是否为空
int isEmpty(Queue* queue) {
    if (queue->rear < queue->front) {
        return 1;
    }
    return 0;
}
// 出队操作
int dequeue(Queue* queue) {
    if (isEmpty(queue)) {
        printf("队为空\n");
        return -1;
    }
    return queue->items[queue->front++];
}
获取队首元素
int peek(Queue* queue) {
    if (isEmpty(queue)) {
        printf("\n");
        return -1;
    }
    return queue->items[queue->front];
}
完整代码
```c
#include <stdio.h>
#include <stdlib.h>

#define MAX_SIZE 100

// 定义队列结构
typedef struct {
    int items[MAX_SIZE];
    int front;
    int rear;
} Queue;

// 初始化队列
void initQueue(Queue* queue) {
    queue->front = 0;
    queue->rear = -1;
}

// 判断队列是否为空
int isEmpty(Queue* queue) {
    if (queue->rear < queue->front) {
        return 1;
    }
    return 0;
}

// 判断队列是否已满
int isFull(Queue* queue) {
    if (queue->rear == MAX_SIZE - 1) {
        return 1;
    }
    return 0;
}

// 入队操作
void enqueue(Queue* queue, int value) {
    if (isFull(queue)) {
        printf("队已满\n");
        return;
    }
    queue->items[++queue->rear] = value;
}

// 出队操作
int dequeue(Queue* queue) {
    if (isEmpty(queue)) {
        printf("队为空\n");
        return -1;
    }
    return queue->items[queue->front++];
}

// 获取队首元素
int peek(Queue* queue) {
    if (isEmpty(queue)) {
        printf("\n");
        return -1;
    }
    return queue->items[queue->front];
}

//主函数
int main() {
    Queue queue;
    initQueue(&queue);

    enqueue(&queue, 10);
    enqueue(&queue, 20);
    enqueue(&queue, 30);

    printf("Front element: %d\n", peek(&queue));

    printf("Dequeued element: %d\n", dequeue(&queue));
    printf("Dequeued element: %d\n", dequeue(&queue));
    printf("Dequeued element: %d\n", dequeue(&queue));

    return 0;
}
```c



#### 判断队空并出队
&emsp;&emsp;==出队操作==(dequeue):==先检查队列是否为空==,若为空则返回错误。否则,将队头节点的数据取出并保存,更新队头指针指向下一个节点,然后释放原队头节点的内存空间。

```c
//判断队空
int isEmpty(Node* Q) {
	if (Q->data == 0 || Q->next == NULL) {
		return 1;
	} else {
		return 0;
	}
}
//出队
int deQueue(Node* Q) {
	if (isEmpty(Q)) {
		return 0;
	} else {
		Node* q = Q->next;
		int val = q->data;
		Q->next = q->next;
		free(q);
		Q->data--;
		return val;
	}
}

  这种实现方式简单易懂,但和实现栈时一样,可能会受到数组大小限制。

链表实现

  链表实现队列时,通常使用两个指针:一个指向队列的头部,一个指向队列的尾部。每个链表结点都包含一个数据元素和一个指向下一个节点的指针。这样的设计使得入队和出队操作的时间复杂度都为O(1),因为无需移动整个队列。
在这里插入图片描述

初始化
Node* InitQueue() {
	Node* Q = (Node*)malloc(sizeof(Node));
	Q->data = 0;
	Q->next = NULL;
	return Q;
}
入队

  入队操作(enqueue):首先创建一个新结点,将数据存入其中,然后更新队尾指针指向新结点,并将新结点链接到原队尾节点的后面。

void enQueue(Node* Q, int val) {
	Node* node = (Node*)malloc(sizeof(Node));
	node->data = val;
	Node* q = Q;
	while (q->next != NULL) {
		q = q->next;
	}
	q->next = node;
	node->next = NULL;
	Q->data++;
}
打印队列

  打印队列即遍历并打印链表。

void PrintQueue(Node* Q) {
	Node* q = Q->next;
	while (q) {
		printf("%d->", q->data);
		q = q->next;
	}
	printf("NULL\n");
}
完整代码
#include <stdio.h>
#include <stdlib.h>
typedef struct Node {
	int data;
	struct Node* next;
}Node;
//初始化队列
Node* InitQueue() {
	Node* Q = (Node*)malloc(sizeof(Node));
	Q->data = 0;
	Q->next = NULL;
	return Q;
}
//入队
void enQueue(Node* Q, int val) {
	Node* node = (Node*)malloc(sizeof(Node));
	node->data = val;
	Node* q = Q;
	while (q->next != NULL) {
		q = q->next;
	}
	q->next = node;
	node->next = NULL;
	Q->data++;
}
//判断队空
int isEmpty(Node* Q) {
	if (Q->data == 0 || Q->next == NULL) {
		return 1;
	} else {
		return 0;
	}
}
//出队
int deQueue(Node* Q) {
	if (isEmpty(Q)) {
		return 0;
	} else {
		Node* q = Q->next;
		int val = q->data;
		Q->next = q->next;
		free(q);
		Q->data--;
		return val;
	}
}
//打印队列
void PrintQueue(Node* Q) {
	Node* q = Q->next;
	while (q) {
		printf("%d->", q->data);
		q = q->next;
	}
	printf("NULL\n");
}
//主函数
int main() {
	Node* Q = InitQueue();
	enQueue(Q, 1); 
	PrintQueue(Q);    //每进行一次入队或出队操作都打印一次,方便观察
	enQueue(Q, 2);
	PrintQueue(Q);
	enQueue(Q, 3);
	PrintQueue(Q);
	enQueue(Q, 4);
	PrintQueue(Q);
	int val1 = deQueue(Q);
	printf("出队元素为:%d\n", val1);
	PrintQueue(Q);
	int val2 = deQueue(Q);
	printf("出队元素为:%d\n", val2);
	PrintQueue(Q);
	return 0;
}

  这种实现方式在内存管理上比较灵活,不需要事先指定队列的最大容量,而且可以动态地分配和释放节点的内存空间。

三、总结

  本篇博客包含了栈和队列的基本原理、相关操作和其对应的分别用数组、链表的代码实现。
  栈和队列的应用十分广泛:栈可以用于实现递归算法,例如深度优先搜索(DFS);栈可以用于实现表达式计算,例如中缀表达式转后缀表达式;队列可以用于实现广度优先搜索(BFS);队列可以用于实现任务调度,例如多线程的任务队列;栈和队列都可以用于实现缓存结构,例如浏览器历史记录。 因此掌握栈和队列的解题思想十分重要,学习完理论知识后,一定要多加练习,确保自己熟练掌握栈和队列的思想,在学习完本篇博客后,可以去力扣上完成相关题目,如225.用队列实现栈,232.用栈实现队列,1700.无法吃午餐的学生数量,20.有效的括号,150.逆波兰表达式求值等。
  本篇博客到此结束,希望对你有所帮助~

  • 25
    点赞
  • 20
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值