文章目录
- 栈
- 栈的定义
- 站的抽象数据类型
- 两栈共享空间
- 栈的作用
- 递归的定义
- 栈运算
- 队列
- 循环队列
- 队列链式存储结构及实现
栈
栈的定义
栈(Stack)是计算机科学中的一种抽象数据类型,它是一个只能在一端进行插入和删除操作的线性数据结构。栈按照后进先出(LIFO)的原则存储数据,即最后放入的元素最先被取出。类比物理世界中的堆叠物品,每次加入的物品都被放在上面,取出时也只能从上面取出,最后放入的物品最先被取出。
栈有两个基本的操作:
- push:将元素压入栈顶。
- pop:将栈顶元素弹出。
除此之外,栈还常常涉及到以下几个概念:
- 栈顶(Top):表示栈顶元素所在位置的下标或指针。
- 空栈(Empty):表示栈内没有任何元素。
- 满栈(Full):表示栈已经无法再容纳新的元素。
- 栈的大小(Size):表示栈可以容纳的元素的最大数量。
栈的应用非常广泛,例如表达式求值、函数调用、回溯算法等都与栈密切相关。
站的抽象数据类型
**抽象数据类型(ADT)**是一种数学模型,它定义了数据的逻辑行为和操作。具体来说,ADT 描述了数据对象的属性和可对其执行的操作,并不考虑这些操作的具体实现方式。
栈(Stack)是一种常见的 ADT,其特点是后进先出(Last In First Out,LIFO),即最后加入的元素最先被取出。栈只支持两个基本操作:压入(Push)和弹出(Pop)。在栈中插入一个元素时,该元素会被放置在栈顶;在弹出一个元素时,栈顶的元素将被移除,并返回该元素的值。
根据这些基本操作,可以衍生出其他一些操作,例如查看栈顶元素(Top)、判断**栈是否为空(IsEmpty)**等。这些操作都可以通过压入和弹出操作来实现。
栈可以用于许多应用场景,例如函数调用时的内存分配、括号匹配、表达式求值等。在实际编程中,栈通常使用数组或链表来实现。
Push(item): 将一个元素item压入栈顶。
Pop(): 弹出栈顶元素,并将其返回。
Top(): 返回栈顶的元素,但不弹出该元素。
IsEmpty(): 判断栈是否为空,如果为空返回True,否则返回False。
Size(): 返回栈中元素的个数。
在实际编程中,栈通常使用数组或链表来实现。这里介绍一种用数组实现栈的方法,叫做顺序存储结构。
顺序存储结构是指将栈中的元素保存在连续的内存空间中,可以通过数组来实现。具体来说,我们需要定义一个数组、一个栈顶指针、以及一些相关的操作。
#define MAXSIZE 100
typedef struct {
int data[MAXSIZE];
int top; // 栈顶指针
} Stack;
void init(Stack *s) {
s->top = -1;
}
int is_empty(Stack *s) {
return s->top == -1;
}
int is_full(Stack *s) {
return s->top == MAXSIZE - 1;
}
void push(Stack *s, int x) {
if (is_full(s)) {
printf("Stack Overflow!\n");
return;
}
s->top++;
s->data[s->top] = x;
}
int pop(Stack *s) {
if (is_empty(s)) {
printf("Stack Underflow!\n");
return -1;
}
int x = s->data[s->top];
s->top--;
return x;
}
int top(Stack *s) {
if (is_empty(s)) {
printf("Stack Empty!\n");
return -1;
}
return s->data[s->top];
}
int size(Stack *s) {
return s->top + 1;
}
两栈共享空间
两栈共享空间是指在一个数组中实现两个栈的数据结构。这种实现方法可以节省空间,因为两个栈共用一个数组。
具体实现方法是将数组分成两个部分,**一个部分为第一个栈的存储区域,另一个部分为第二个栈的存储区域。**两个栈的起始位置分别从数组的两端开始向中间靠拢。当两个栈的栈顶指针相遇时,表示两个栈都满了。
需要注意的是,在进行入栈和出栈操作时,需要判断哪个栈的栈顶指针需要移动。具体判断方法是,如果要对第一个栈进行入栈操作,则将元素插入到第一个栈的栈顶,并将第一个栈的栈顶指针加1;如果要对第二个栈进行入栈操作,则将元素插入到第二个栈的栈顶,并将第二个栈的栈顶指针减1。出栈操作也是类似的。
以下是一个简单的示例代码,实现了两个栈在同一个数组内共享空间的功能:
#include <stdio.h>
#include <stdlib.h>
#define MAX_SIZE 100
typedef struct {
int array[MAX_SIZE];
int top1;
int top2;
} TwoStacks;
void initStacks(TwoStacks *stacks) {
stacks->top1 = -1;
stacks->top2 = MAX_SIZE;
}
void push1(TwoStacks *stacks, int item) {
if (stacks->top1 < stacks->top2 - 1) {
stacks->top1++;
stacks->array[stacks->top1] = item;
} else {
printf("Stack Overflow\n");
exit(1);
}
}
void push2(TwoStacks *stacks, int item) {
if (stacks->top1 < stacks->top2 - 1) {
stacks->top2--;
stacks->array[stacks->top2] = item;
} else {
printf("Stack Overflow\n");
exit(1);
}
}
int pop1(TwoStacks *stacks) {
if (stacks->top1 >= 0) {
int item = stacks->array[stacks->top1];
stacks->top1--;
return item;
} else {
printf("Stack Underflow\n");
exit(1);
}
}
int pop2(TwoStacks *stacks) {
if (stacks->top2 < MAX_SIZE) {
int item = stacks->array[stacks->top2];
stacks->top2++;
return item;
} else {
printf("Stack Underflow\n");
exit(1);
}
}
int main() {
TwoStacks stacks;
initStacks(&stacks);
push1(&stacks, 1);
push2(&stacks, 2);
push1(&stacks, 3);
push2(&stacks, 4);
push1(&stacks, 5);
printf("%d\n", pop1(&stacks)); // Output: 5
printf("%d\n", pop2(&stacks)); // Output: 4
printf("%d\n", pop1(&stacks)); // Output: 3
printf("%d\n", pop2(&stacks)); // Output: 2
printf("%d\n", pop1(&stacks)); // Output: 1
return 0;
}
栈的作用
栈还可以用于解决一些特定的编程问题,例如表达式求值、递归算法、回溯算法等等。在这些情况下,栈可以帮助我们存储和跟踪程序运行时的状态信息,以便于算法的正确执行。
这里是用栈做的小的运用
#include <stdio.h>
#include <stdlib.h>
#define MAX_STACK_SIZE 100
// 定义栈结构体
typedef struct {
int data[MAX_STACK_SIZE];
int top;
} Stack;
// 初始化栈
void init_stack(Stack *s) {
s->top = -1;
}
// 判断栈是否为空
int is_empty(Stack *s) {
return s->top == -1;
}
// 判断栈是否已满
int is_full(Stack *s) {
return s->top == MAX_STACK_SIZE - 1;
}
// 入栈操作
void push(Stack *s, int value) {
if (is_full(s)) {
printf("Stack is full.\n");
exit(1);
}
s->data[++(s->top)] = value;
}
// 出栈操作
int pop(Stack *s) {
if (is_empty(s)) {
printf("Stack is empty.\n");
exit(1);
}
return s->data[(s->top)--];
}
// 递归函数
int factorial(int n) {
Stack s;
init_stack(&s);
int result = 1;
push(&s, n); // 入栈
while (!is_empty(&s)) {
int x = pop(&s); // 出栈
if (x == 1) {
break;
}
result *= x;
push(&s, x - 1); // 入栈
}
return result;
}
// 示例程序
int main() {
int n = 5;
printf("%d! = %d\n", n, factorial(n));
return 0;
}
递归的定义
递归是一种在函数定义中使用自身的技术。在递归中,一个函数调用自身,直到达到某个终止条件为止。通常情况下,递归用于解决可以被分割成相同模式的子问题的问题,这些问题可以通过不断地调用函数来得到解决。
递归函数通常包括两部分:基本情况和递归情况。基本情况是指函数能够直接处理的情况,不需要再次调用自身来解决。递归情况则是指函数需要将问题分解成更小的子问题,并通过调用自身来解决它们。
递归的优点是可以使代码更简洁、易读、易于理解,但同时也可能会导致复杂度高、栈溢出等问题。因此,在使用递归时需要谨慎考虑其使用场景和实现方式。
总之学会递归就要学会递归
逆波兰表示法
波兰表示法
栈运算
使用栈可以很方便地实现算术表达式的求值。具体步骤如下:
-
创建两个栈:操作数栈和运算符栈。
-
从左到右扫描表达式中的每个字符。
-
如果当前字符是数字,则将其压入操作数栈。
-
如果当前字符是运算符,则判断运算符栈是否为空,如果不为空,则比较该运算符与运算符栈顶的运算符的优先级,如果该运算符的优先级小于或等于运算符栈顶的运算符,则弹出运算符栈顶的运算符和操作数栈中的两个元素,进行相应的运算,并将结果压入操作数栈。否则将该运算符压入运算符栈。
-
如果当前字符是左括号,则将其压入运算符栈。
-
如果当前字符是右括号,则循环弹出运算符栈顶的运算符和操作数栈中的两个元素,进行相应的运算,并将结果压入操作数栈,直到遇到左括号为止。
-
循环执行步骤 3-6,直到扫描完整个表达式。
-
循环弹出运算符栈顶的运算符和操作数栈中的两个元素,进行相应的运算,并将结果压入操作数栈,直到运算符栈为空。
-
操作数栈中最后剩下的元素即为表达式的计算结果。
这个方法可以实现任意长度的算术表达式的求值,包括含有加减乘除、括号等多种运算符的复杂表达式。
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <ctype.h> // 需要使用 isdigit 函数
#define MAX_STACK_SIZE 100 // 定义栈的最大容量
int stack[MAX_STACK_SIZE]; // 定义操作数栈
char opstack[MAX_STACK_SIZE]; // 定义运算符栈
int top = -1; // 操作数栈顶指针
int optop = -1; // 运算符栈顶指针
int pop() { // 弹出操作数栈顶元素
if (top == -1) {
printf("Stack underflow!\n");
exit(1);
}
return stack[top--];
}
char oppop() { // 弹出运算符栈顶元素
if (optop == -1) {
printf("Stack underflow!\n");
exit(1);
}
return opstack[optop--];
}
void push(int value) { // 将元素压入操作数栈
if (top >= MAX_STACK_SIZE - 1) {
printf("Stack overflow!\n");
exit(1);
}
stack[++top] = value;
}
void oppush(char value) { // 将元素压入运算符栈
if (optop >= MAX_STACK_SIZE - 1) {
printf("Stack overflow!\n");
exit(1);
}
opstack[++optop] = value;
}
int priority(char op) { // 定义运算符优先级
if (op == '*' || op == '/') {
return 2;
} else if (op == '+' || op == '-') {
return 1;
} else {
return 0;
}
}
int evaluate(char *expression) { // 表达式求值函数
int i, len, operand1, operand2;
char c, op;
len = strlen(expression);
for (i = 0; i < len; i++) {
c = expression[i];
if (isdigit(c)) { // 如果是数字字符,将其转换为整数并压入操作数栈
push(c - '0');
} else if (c == '(') { // 如果是左括号,将其压入运算符栈
oppush(c);
} else if (c == ')') { // 如果是右括号,则弹出运算符栈顶的运算符和操作数栈中的两个元素,进行相应的运算,并将结果压入操作数栈,直到遇到左括号为止
while (opstack[optop] != '(') {
operand2 = pop();
operand1 = pop();
op = oppop();
switch (op) {
case '+': push(operand1 + operand2); break;
case '-': push(operand1 - operand2); break;
case '*': push(operand1 * operand2); break;
case '/':
if (operand2 == 0) { // 处理除数为0的情况
printf("Division by zero!\n");
exit(1);
}
push(operand1 / operand2);
break;
}
}
oppop(); // 弹出左括号
} else if (c == '+' || c == '-' || c == '*' || c == '/') { // 如果是运算符,则比较该运算符与运算符栈顶的运算符的优先级,如果该运算符的优先级小于或等于运算符栈顶的运算符,则弹出运算符栈顶的运算符和操作数栈中的两个元素,进行相应的运算,并将结果压入操作数栈。否则将该运算符压入运算符栈。
while (optop >= 0 && priority(c) <= priority(opstack[optop]))
{
operand2 = pop();
operand1 = pop();
op = oppop();
switch (op) {
case '+': push(operand1 + operand2); break;
case '-': push(operand1 - operand2); break;
case '*': push(operand1 * operand2); break;
case '/':
if (operand2 == 0) { // 处理除数为0的情况
printf("Division by zero!\n");
exit(1);
}
push(operand1 / operand2);
break;
}
}
oppush(c);
} else if (c == ' ') { // 如果是空格,则忽略
continue;
} else { // 如果不是数字、括号或运算符,则输入的表达式有误
printf("Invalid expression!\n");
exit(1);
}
}
while (optop >= 0) { // 处理剩余的运算符
operand2 = pop();
operand1 = pop();
op = oppop();
switch (op) {
case '+': push(operand1 + operand2); break;
case '-': push(operand1 - operand2); break;
case '*': push(operand1 * operand2); break;
case '/':
if (operand2 == 0) { // 处理除数为0的情况
printf("Division by zero!\n");
exit(1);
}
push(operand1 / operand2);
break;
}
}
return pop(); // 返回操作数栈顶元素,即为表达式求值结果
}
int main() {
char expression[100];
printf("Please enter an arithmetic expression:\n");
fgets(expression, 100, stdin);
printf("The result is: %d\n", evaluate(expression));
return 0;
}
队列
队列是一种常见的数据结构,它按照先进先出(FIFO)的原则来存储和访问元素。队列通常用于在多个处理单元之间分配任务或缓冲数据。
在队列中,新元素被添加到队列的尾部,而从队列中删除元素时则从队列的头部开始进行。这意味着最先添加的元素会最先被取出,而最后添加的元素则会最后被取出。这种方式确保了队列中的元素按照添加顺序进行处理。
队列的基本操作包括入队(将元素添加到队列尾部)和出队(从队列头部删除元素)。此外,队列还可以支持其他操作,如查看队列头部元素、检查队列是否为空等。
队列的实现
#include <stdio.h>
#include <stdlib.h>
#define MAX_QUEUE_SIZE 100
// 定义一个队列结构体
struct queue {
int items[MAX_QUEUE_SIZE];
int front, rear;
};
// 初始化队列
struct queue* create_queue() {
struct queue* q = (struct queue*)malloc(sizeof(struct queue));
q->front = -1;
q->rear = -1;
return q;
}
// 检查队列是否为空
int is_empty(struct queue* q) {
if (q->rear == -1)
return 1;
else
return 0;
}
// 检查队列是否已满
int is_full(struct queue* q) {
if (q->front == 0 && q->rear == MAX_QUEUE_SIZE - 1)
return 1;
else if (q->front == q->rear + 1)
return 1;
else
return 0;
}
// 向队列尾部插入元素
void enqueue(struct queue* q, int value) {
if (is_full(q)) {
printf("Queue is full.\n");
} else {
if (q->front == -1) {
q->front = 0;
}
q->rear = (q->rear + 1) % MAX_QUEUE_SIZE;
q->items[q->rear] = value;
printf("Enqueued item: %d\n", value);
}
}
// 从队列头部删除元素并返回该元素
int dequeue(struct queue* q) {
int item;
if (is_empty(q)) {
printf("Queue is empty.\n");
item = -1;
} else {
item = q->items[q->front];
if (q->front == q->rear) {
q->front = -1;
q->rear = -1;
} else {
q->front = (q->front + 1) % MAX_QUEUE_SIZE;
}
}
return item;
}
// 显示队列中所有元素
void display(struct queue* q) {
int i;
if (is_empty(q)) {
printf("Queue is empty.\n");
} else {
printf("Queue contains the following items:\n");
for (i = q->front; i <= q->rear; i++) {
printf("%d ", q->items[i]);
}
printf("\n");
}
}
// 主函数用于演示队列操作
int main() {
struct queue* q = create_queue();
// 向队列添加元素
enqueue(q, 1);
enqueue(q, 2);
enqueue(q, 3);
enqueue(q, 4);
// 显示队列中的元素
display(q);
// 从队列删除元素
int removed_item = dequeue(q);
printf("Removed item: %d\n", removed_item);
// 显示更新后的队列
display(q);
return 0;
}
循环队列
循环队列是一种使用固定大小的环形缓冲区来实现队列的数据结构。在循环队列中,队列的尾部连接到了队列的头部,使得元素可以按照先进先出(FIFO)的顺序被访问。
在循环队列中,需要维护两个指针:一个指向队列的头部(front),另一个指向队列的尾部(rear)。当向队列中插入元素时,将元素添加到尾部指针所指向的位置,并将尾部指针后移。同样地,当从队列中删除元素时,将元素从头部指针所指向的位置删除,并将头部指针后移。由于循环队列是环形的,当指针到达缓冲区的尾部时,它会返回缓冲区的开头。
循环队列相对于普通队列的好处在于,它可以有效地利用内存空间并避免因为队列的头部或尾部始终在数组的末尾而导致的空间浪费。此外,在循环队列中,我们可以更高效地判断队列是否已满或为空。
#include <stdio.h>
#include <stdlib.h>
#define MAX_SIZE 5 // 定义最大队列长度为5
typedef struct {
int front; // 队头指针
int rear; // 队尾指针
int data[MAX_SIZE]; // 队列数组
} Queue;
/**
* 初始化队列
* @param q 待初始化的队列
*/
void init(Queue *q) {
q->front = q->rear = 0;
}
/**
* 判断队列是否为空
* @param q 待判断的队列
* @return 如果队列为空,则返回1;否则返回0。
*/
int is_empty(Queue *q) {
return q->front == q->rear;
}
/**
* 判断队列是否已满
* @param q 待判断的队列
* @return 如果队列已满,则返回1;否则返回0。
*/
int is_full(Queue *q) {
return (q->rear + 1) % MAX_SIZE == q->front;
}
/**
* 入队操作
* @param q 队列指针
* @param x 入队元素
* @return 成功入队则返回1,否则返回0。
*/
int enqueue(Queue *q, int x) {
if (is_full(q)) { // 如果队列已满,则无法入队
printf("队列已满,无法入队。\n");
return 0;
} else {
q->data[q->rear] = x; // 将元素插入队尾
q->rear = (q->rear + 1) % MAX_SIZE; // 队尾指针加1,注意取模
return 1;
}
}
/**
* 出队操作
* @param q 队列指针
* @param x 出队元素的存储地址
* @return 成功出队则返回1,否则返回0。
*/
int dequeue(Queue *q, int *x) {
if (is_empty(q)) { // 如果队列为空,则无法出队
printf("队列为空,无法出队。\n");
return 0;
} else {
*x = q->data[q->front]; // 取出队头元素
q->front = (q->front + 1) % MAX_SIZE; // 队头指针加1,注意取模
return 1;
}
}
int main() {
Queue q; // 定义队列
init(&q); // 初始化队列
enqueue(&q, 1); // 入队元素1
enqueue(&q, 2); // 入队元素2
enqueue(&q, 3); // 入队元素3
enqueue(&q, 4); // 入队元素4
enqueue(&q, 5); // 入队元素5,此时队列已满,5入不了队列
enqueue(&q, 6); // 入队元素6,此时无法入队
int x; // 定义变量x,用于存储出队元素
dequeue(&q, &x); // 出队操作,取出队头元素1
printf("出队元素:%d\n", x);
dequeue(&q, &x); // 出队操作,取出队头元素2
printf("出队元素:%d\n", x);
enqueue(&q, 6); // 入队新元素6
dequeue(&q, &x); // 出队操作,取出队头元素3
printf("出队元素:%d\n", x);
dequeue(&q, &x); // 出队操作,取出队头元素4
printf("出队元素:%d\n", x);
dequeue(&q, &x); // 出队操作,取出队头元素6
printf("出队元素:%d\n", x);
dequeue(&q, &x); //
dequeue(&q, &x); // 出队操作,此时队列为空,无法出队
printf("出队元素:%d\n", x);
return 0;
}
//程序有瑕疵,这里仅是抛砖作用。
队列链式存储结构及实现
首先,定义一个结构体来表示链表中的节点,包含数据域和指针域:
typedef struct ListNode {
int data;
struct ListNode* next;
} ListNode;
其中 data 表示节点中存储的数据,next 是指向下一个节点的指针。
接着,定义一个结构体来表示队列,包含头指针和尾指针:
typedef struct Queue {
ListNode* front; // 队头指针
ListNode* rear; // 队尾指针
} Queue;
其中 front 指向队头节点,rear 指向队尾节点。
创建空队列时,可以将 front 和 rear 初始化为 NULL。
入队操作时,需要新建一个节点,并将其插入到队尾:
void enQueue(Queue* q, int value) {
ListNode* node = (ListNode*)malloc(sizeof(ListNode));
node->data = value;
node->next = NULL;
if (q->rear == NULL) { // 队列为空时
q->front = q->rear = node;
} else {
q->rear->next = node;
q->rear = node;
}
}
出队操作时,需要删除队头节点,并更新队头指针:
int deQueue(Queue* q) {
if (q->front == NULL) { // 队列为空时
return -1;
}
ListNode* node = q->front;
int value = node->data;
if (q->front == q->rear) { // 队列只有一个节点时
q->front = q->rear = NULL;
} else {
q->front = q->front->next;
}
free(node);
return value;
}
小例子
#include <stdio.h>
#include <stdlib.h>
// 链表节点结构体
typedef struct ListNode {
int data;
struct ListNode* next;
} ListNode;
// 队列结构体
typedef struct Queue {
ListNode* front; // 队头指针
ListNode* rear; // 队尾指针
} Queue;
// 初始化队列
void initQueue(Queue* q) {
q->front = NULL;
q->rear = NULL;
}
// 入队操作
void enQueue(Queue* q, int value) {
ListNode* node = (ListNode*)malloc(sizeof(ListNode));
node->data = value;
node->next = NULL;
if (q->rear == NULL) { // 队列为空时
q->front = q->rear = node;
} else {
q->rear->next = node;
q->rear = node;
}
}
// 出队操作
int deQueue(Queue* q) {
if (q->front == NULL) { // 队列为空时
return -1;
}
ListNode* node = q->front;
int value = node->data;
if (q->front == q->rear) { // 队列只有一个节点时
q->front = q->rear = NULL;
} else {
q->front = q->front->next;
}
free(node);
return value;
}
int main() {
Queue q;
initQueue(&q);
// 入队操作
enQueue(&q, 1);
enQueue(&q, 2);
enQueue(&q, 3);
// 出队操作
int value1 = deQueue(&q);
int value2 = deQueue(&q);
int value3 = deQueue(&q);
printf("%d %d %d\n", value1, value2, value3); // 输出:1 2 3
return 0;
}