第3章:表、栈和队列(Lists, Stacks, and Queues)
一、导读
1.1 本章学习目标
第3章介绍三种最基本的线性数据结构:表(List)、栈(Stack)和队列(Queue)。它们是构建更复杂数据结构的基石,也是实际编程中使用最频繁的数据结构。本章将深入讲解它们的抽象定义、不同实现方式(数组和链表),以及各种操作的复杂度分析。
本章的核心学习目标包括:
- 理解表的抽象概念:表是一种支持在任意位置进行插入和删除的线性结构,它的数组实现和链表实现各有什么优缺点?
- 掌握链表的各种变体:单链表、双链表、循环链表的结构特点和适用场景分别是什么?
- 深入理解栈的LIFO特性:栈的"后进先出"原则如何应用于函数调用、表达式求值和括号匹配?
- 深入理解队列的FIFO特性:队列的"先进先出"原则如何应用于广度优先搜索和任务调度?
- 学会用数组和链表分别实现栈和队列:两种实现方式在性能上有什么差异?
- 了解游标实现:如何用数组模拟链表,避免指针操作的开销?
1.2 为什么第三章如此重要
表、栈和队列是数据结构世界的"三块基石"。几乎所有后续的数据结构——树、图、散列表、优先队列——都可以看作是这三种基本结构的扩展或组合。
更重要的是,这三种数据结构在实际编程中的应用无处不在:
- 栈:浏览器的后退按钮、编辑器的撤销功能、编译器的语法分析、函数调用管理
- 队列:操作系统的进程调度、打印队列、BFS搜索、消息队列
- 链表:操作系统的内存管理、LRU缓存、音乐播放列表
掌握它们的实现细节和性能特点,是成为合格程序员的基本功。
1.3 核心问题引导
在阅读本章时,请带着以下问题思考:
数组和链表在插入、删除、访问操作上的时间复杂度各是多少?为什么?
为什么单链表不适合按位置随机访问?如何改进?
如何用两个栈实现一个队列?如何用两个队列实现一个栈?
栈在函数调用中扮演什么角色?递归调用时栈发生了什么?
如何用栈将中缀表达式转换为后缀表达式?如何计算后缀表达式的值?
循环队列如何解决"假溢出"问题?
二、核心概念详解
3.1 表(List)
表是最基本的线性数据结构,它包含零个或多个元素 a₁, a₂, ..., aₙ。表的长度是元素的个数,空表长度为0。除第一个元素外,每个元素有唯一的前驱;除最后一个元素外,每个元素有唯一的后继。
3.1.1 表的数组实现
数组实现使用一块连续的内存空间存储表的元素。
#define MAX_SIZE 1000
typedef struct ArrayList {
int data[MAX_SIZE];
int size;
} ArrayList;
void init(ArrayList *L) {
L->size = 0;
}
int get(ArrayList *L, int pos) {
if (pos < 0 || pos >= L->size) return -1;
return L->data[pos];
}
// 在位置pos插入元素 - O(n)
void insert(ArrayList *L, int pos, int val) {
if (L->size >= MAX_SIZE) return;
if (pos < 0 || pos > L->size) return;
for (int i = L->size; i > pos; i--)
L->data[i] = L->data[i - 1];
L->data[pos] = val;
L->size++;
}
// 删除位置pos的元素 - O(n)
void delete(ArrayList *L, int pos) {
if (pos < 0 || pos >= L->size) return;
for (int i = pos; i < L->size - 1; i++)
L->data[i] = L->data[i + 1];
L->size--;
}
// 按位置访问 - O(1)
// 这是数组实现的最大优势
int access(ArrayList *L, int pos) {
return L->data[pos];
}数组实现的优缺点:
- 优点:随机访问 O(1),内存连续利用率高
- 缺点:插入和删除需要移动大量元素 O(n),容量固定(除非动态扩容)
3.1.2 表的链表实现
链表使用节点(Node)存储元素,每个节点包含数据和指向下一个节点的指针。
typedef struct Node {
int data;
struct Node *next;
} Node;
typedef struct LinkedList {
Node *head;
int size;
} LinkedList;
void init(LinkedList *L) {
L->head = NULL;
L->size = 0;
}
// 在头部插入 - O(1)
void insertFront(LinkedList *L, int val) {
Node *newNode = (Node *)malloc(sizeof(Node));
newNode->data = val;
newNode->next = L->head;
L->head = newNode;
L->size++;
}
// 在位置pos插入 - O(n)(需要遍历到pos-1)
void insert(LinkedList *L, int pos, int val) {
Node *newNode = (Node *)malloc(sizeof(Node));
newNode->data = val;
if (pos == 0) {
newNode->next = L->head;
L->head = newNode;
} else {
Node *curr = L->head;
for (int i = 0; i < pos - 1; i++)
curr = curr->next;
newNode->next = curr->next;
curr->next = newNode;
}
L->size++;
}
// 删除值为val的第一个节点 - O(n)
void delete(LinkedList *L, int val) {
if (L->head == NULL) return;
if (L->head->data == val) {
Node *tmp = L->head;
L->head = L->head->next;
free(tmp);
L->size--;
return;
}
Node *curr = L->head;
while (curr->next != NULL && curr->next->data != val)
curr = curr->next;
if (curr->next != NULL) {
Node *tmp = curr->next;
curr->next = tmp->next;
free(tmp);
L->size--;
}
}
// 按位置访问 - O(n)
int get(LinkedList *L, int pos) {
Node *curr = L->head;
for (int i = 0; i < pos; i++) {
if (curr == NULL) return -1;
curr = curr->next;
}
return curr ? curr->data : -1;
}
// 释放整个链表
void destroy(LinkedList *L) {
Node *curr = L->head;
while (curr != NULL) {
Node *tmp = curr;
curr = curr->next;
free(tmp);
}
L->head = NULL;
L->size = 0;
}链表实现的优缺点:
- 优点:插入和删除(已知位置时)只需修改指针 O(1),动态大小
- 缺点:随机访问 O(n),每个节点额外存储指针,缓存不友好
3.1.3 双链表和循环链表
// 双链表节点
typedef struct DNode {
int data;
struct DNode *prev;
struct DNode *next;
} DNode;
// 双链表支持向前和向后遍历
// 删除操作更方便:已知节点指针时,删除为O(1)
void deleteNode(DNode **head, DNode *node) {
if (*head == node) *head = node->next;
if (node->prev) node->prev->next = node->next;
if (node->next) node->next->prev = node->prev;
free(node);
}
// 循环链表:最后一个节点的next指向头节点
// 适用于需要循环遍历的场景(如轮转调度)
typedef struct CircList {
Node *head;
} CircList;
void initCirc(CircList *L) {
L->head = NULL;
}
void insertCirc(CircList *L, int val) {
Node *newNode = (Node *)malloc(sizeof(Node));
newNode->data = val;
if (L->head == NULL) {
newNode->next = newNode;
L->head = newNode;
} else {
Node *tail = L->head;
while (tail->next != L->head)
tail = tail->next;
tail->next = newNode;
newNode->next = L->head;
}
}3.2 栈(Stack)
栈是只允许在一端(称为"栈顶")进行插入和删除操作的线性表。栈的核心特征是后进先出(LIFO, Last In First Out)。
栈的两个核心操作:
- push:在栈顶插入元素
- pop:从栈顶删除并返回元素
// 栈的链表实现
typedef struct StackNode {
int data;
struct StackNode *next;
} StackNode;
typedef struct Stack {
StackNode *top;
int size;
} Stack;
void initStack(Stack *S) {
S->top = NULL;
S->size = 0;
}
int isEmpty(Stack *S) {
return S->top == NULL;
}
// push - O(1)
void push(Stack *S, int val) {
StackNode *newNode = (StackNode *)malloc(sizeof(StackNode));
newNode->data = val;
newNode->next = S->top;
S->top = newNode;
S->size++;
}
// pop - O(1)
int pop(Stack *S) {
if (isEmpty(S)) {
fprintf(stderr, "Stack underflow\n");
exit(1);
}
int val = S->top->data;
StackNode *tmp = S->top;
S->top = S->top->next;
free(tmp);
S->size--;
return val;
}
// peek - O(1)
int peek(Stack *S) {
if (isEmpty(S)) {
fprintf(stderr, "Stack is empty\n");
exit(1);
}
return S->top->data;
}3.2.1 栈的应用:括号匹配
// 检查括号是否匹配
int isBalanced(const char *expr) {
Stack S;
initStack(&S);
for (int i = 0; expr[i] != '\0'; i++) {
char c = expr[i];
if (c == '(' || c == '[' || c == '{') {
push(&S, c);
} else if (c == ')' || c == ']' || c == '}') {
if (isEmpty(&S)) return 0;
char top = pop(&S);
if ((c == ')' && top != '(') ||
(c == ']' && top != '[') ||
(c == '}' && top != '{'))
return 0;
}
}
return isEmpty(&S);
}
// isBalanced("({[]})") → 1(匹配)
// isBalanced("({[})") → 0(不匹配)3.2.2 栈的应用:后缀表达式求值
// 计算后缀表达式(逆波兰表达式)
int evalPostfix(const char *expr) {
Stack S;
initStack(&S);
for (int i = 0; expr[i] != '\0'; i++) {
char c = expr[i];
if (c >= '0' && c <= '9') {
push(&S, c - '0');
} else if (c == '+' || c == '-' || c == '*' || c == '/') {
int b = pop(&S);
int a = pop(&S);
switch (c) {
case '+': push(&S, a + b); break;
case '-': push(&S, a - b); break;
case '*': push(&S, a * b); break;
case '/': push(&S, a / b); break;
}
}
}
return pop(&S);
}
// evalPostfix("23+4*") → (2+3)*4 = 203.3 队列(Queue)
队列是只允许在一端(队尾)插入、另一端(队头)删除的线性表。队列的核心特征是先进先出(FIFO, First In First Out)。
队列的两个核心操作:
- enqueue:在队尾插入元素
- dequeue:从队头删除并返回元素
// 队列的链表实现
typedef struct QueueNode {
int data;
struct QueueNode *next;
} QueueNode;
typedef struct Queue {
QueueNode *front;
QueueNode *rear;
int size;
} Queue;
void initQueue(Queue *Q) {
Q->front = Q->rear = NULL;
Q->size = 0;
}
int isQueueEmpty(Queue *Q) {
return Q->front == NULL;
}
// enqueue - O(1)
void enqueue(Queue *Q, int val) {
QueueNode *newNode = (QueueNode *)malloc(sizeof(QueueNode));
newNode->data = val;
newNode->next = NULL;
if (Q->rear == NULL) {
Q->front = Q->rear = newNode;
} else {
Q->rear->next = newNode;
Q->rear = newNode;
}
Q->size++;
}
// dequeue - O(1)
int dequeue(Queue *Q) {
if (isQueueEmpty(Q)) {
fprintf(stderr, "Queue underflow\n");
exit(1);
}
int val = Q->front->data;
QueueNode *tmp = Q->front;
Q->front = Q->front->next;
if (Q->front == NULL) Q->rear = NULL;
free(tmp);
Q->size--;
return val;
}3.3.1 循环队列的数组实现
循环队列使用固定大小的数组,通过取模运算实现"循环"效果,解决了普通数组队列的"假溢出"问题。
#define QUEUE_CAPACITY 100
typedef struct CircQueue {
int data[QUEUE_CAPACITY];
int front;
int rear;
int size;
} CircQueue;
void initCQ(CircQueue *Q) {
Q->front = 0;
Q->rear = -1;
Q->size = 0;
}
int isCQEmpty(CircQueue *Q) {
return Q->size == 0;
}
int isCQFull(CircQueue *Q) {
return Q->size == QUEUE_CAPACITY;
}
void enqueueCQ(CircQueue *Q, int val) {
if (isCQFull(Q)) return;
Q->rear = (Q->rear + 1) % QUEUE_CAPACITY;
Q->data[Q->rear] = val;
Q->size++;
}
int dequeueCQ(CircQueue *Q) {
if (isCQEmpty(Q)) return -1;
int val = Q->data[Q->front];
Q->front = (Q->front + 1) % QUEUE_CAPACITY;
Q->size--;
return val;
}3.4 用栈实现队列
// 用两个栈实现队列
typedef struct StackQueue {
Stack inStack;
Stack outStack;
} StackQueue;
void initSQ(StackQueue *Q) {
initStack(&Q->inStack);
initStack(&Q->outStack);
}
// enqueue: 直接压入inStack - 摊还O(1)
void enqueueSQ(StackQueue *Q, int val) {
push(&Q->inStack, val);
}
// dequeue: 从outStack弹出 - 摊还O(1)
int dequeueSQ(StackQueue *Q) {
if (isEmpty(&Q->outStack)) {
while (!isEmpty(&Q->inStack))
push(&Q->outStack, pop(&Q->inStack));
}
if (isEmpty(&Q->outStack)) {
fprintf(stderr, "Queue is empty\n");
exit(1);
}
return pop(&Q->outStack);
}
// 摊还分析:每个元素最多被push两次、pop两次
// 因此每次操作的摊还代价是O(1)三、实战案例
3.1 用链表实现多项式加法
typedef struct PolyNode {
int coeff; // 系数
int exp; // 指数
struct PolyNode *next;
} PolyNode;
// 创建多项式节点
PolyNode *createPolyNode(int coeff, int exp) {
PolyNode *node = (PolyNode *)malloc(sizeof(PolyNode));
node->coeff = coeff;
node->exp = exp;
node->next = NULL;
return node;
}
// 多项式加法
PolyNode *addPoly(PolyNode *p1, PolyNode *p2) {
PolyNode dummy;
PolyNode *curr = &dummy;
dummy.next = NULL;
while (p1 != NULL && p2 != NULL) {
PolyNode *newNode;
if (p1->exp > p2->exp) {
newNode = createPolyNode(p1->coeff, p1->exp);
p1 = p1->next;
} else if (p1->exp < p2->exp) {
newNode = createPolyNode(p2->coeff, p2->exp);
p2 = p2->next;
} else {
int sum = p1->coeff + p2->coeff;
if (sum != 0) {
newNode = createPolyNode(sum, p1->exp);
} else {
p1 = p1->next;
p2 = p2->next;
continue;
}
p1 = p1->next;
p2 = p2->next;
}
curr->next = newNode;
curr = newNode;
}
while (p1 != NULL) {
curr->next = createPolyNode(p1->coeff, p1->exp);
curr = curr->next;
p1 = p1->next;
}
while (p2 != NULL) {
curr->next = createPolyNode(p2->coeff, p2->exp);
curr = curr->next;
p2 = p2->next;
}
return dummy.next;
}
// 例如:(3x² + 2x + 1) + (x² + 5) = 4x² + 2x + 6
// 时间复杂度:O(m + n),m和n分别是两个多项式的项数3.2 中缀表达式转后缀表达式
int precedence(char op) {
if (op == '+' || op == '-') return 1;
if (op == '*' || op == '/') return 2;
if (op == '^') return 3;
return 0;
}
void infixToPostfix(const char *infix, char *postfix) {
Stack S;
initStack(&S);
int j = 0;
for (int i = 0; infix[i] != '\0'; i++) {
char c = infix[i];
if (c >= 'a' && c <= 'z') {
postfix[j++] = c;
} else if (c == '(') {
push(&S, c);
} else if (c == ')') {
while (!isEmpty(&S) && peek(&S) != '(')
postfix[j++] = (char)pop(&S);
if (!isEmpty(&S)) pop(&S);
} else {
while (!isEmpty(&S) && precedence((char)peek(&S)) >= precedence(c))
postfix[j++] = (char)pop(&S);
push(&S, c);
}
}
while (!isEmpty(&S))
postfix[j++] = (char)pop(&S);
postfix[j] = '\0';
}
// infixToPostfix("a+b*c") → "abc*+"
// infixToPostfix("(a+b)*c") → "ab+c*"四、本章小结
本章系统介绍了三种基本的线性数据结构:
表是最基本的线性结构。数组实现支持 O(1) 随机访问但插入删除 O(n);链表实现插入删除 O(1)(已知位置时)但随机访问 O(n)。选择哪种实现取决于应用场景的操作频率。
栈是LIFO结构,只允许在栈顶进行插入和删除。所有操作都是 O(1)。栈在函数调用管理、表达式求值、括号匹配、回溯算法中有广泛应用。
队列是FIFO结构,在队尾插入、队头删除。使用链表或循环数组实现时,所有操作都是 O(1)。队列在BFS、任务调度、缓冲区管理中有广泛应用。
游标实现用数组模拟链表,适用于不支持指针的语言或需要减少指针开销的场景。
栈和队列可以互相模拟:两个栈可以实现队列(摊还 O(1)),两个队列可以实现栈。
五、思考与练习
思考题
如果你需要频繁在表的中间位置插入元素,但很少随机访问,应该选择数组还是链表?为什么?
用两个队列实现一个栈,写出 push 和 pop 的代码,并分析时间复杂度。
为什么双链表在已知节点指针时可以实现 O(1) 删除,而单链表不行?
编程练习
实现一个支持 getMin() 操作的栈,getMin() 返回栈中最小元素,所有操作均为 O(1)。
用链表实现一个多项式乘法程序。
编写程序检查一个字符串中的括号是否匹配(包括 ()、[]、{})。