一、队列(Queue)详解
1. 队列基本概念
定义:
队列是一种**先进先出(FIFO)**的线性数据结构,就像现实生活中的排队一样。
图示:
text
入队方向 → → → → → → →
┌───┬───┬───┬───┬───┐
队列: │ A │ B │ C │ D │ E │
└───┴───┴───┴───┴───┘
出队方向 ← ← ← ← ← ← ←
↑ ↑
front rear
队头 队尾
2. 队列的组成要素
-
队头(front):允许删除的一端
-
队尾(rear):允许插入的一端
-
出队(dequeue):从队头删除元素
-
入队(enqueue):在队尾插入元素
3. 顺序循环队列
3.1 为什么需要循环队列?
普通顺序队列的问题:
text
初始状态:front=0, rear=0
入队A,B,C:front=0, rear=3
出队A,B:front=2, rear=3
┌───┬───┬───┬───┐
│ │ │ C │ │
└───┴───┴───┴───┘
↑ ↑
front rear
虽然前面有空间,但rear已到末尾,无法再入队→"假溢出"
3.2 循环队列的解决方案
通过取模运算实现循环利用空间:
c
// 关键运算:
rear = (rear + 1) % MAX_SIZE;
front = (front + 1) % MAX_SIZE;
3.3 循环队列的三种状态判断方法
方法一:牺牲一个存储单元(最常用)
c
typedef struct {
int data[MAX_SIZE];
int front; // 指向队头元素
int rear; // 指向队尾元素的下一个位置
} CircularQueue;
// 队空条件:front == rear
// 队满条件:(rear + 1) % MAX_SIZE == front
方法二:增加一个计数器
c
typedef struct {
int data[MAX_SIZE];
int front;
int rear;
int count; // 记录元素个数
} CircularQueue;
// 队空条件:count == 0
// 队满条件:count == MAX_SIZE
方法三:增加标志位
c
typedef struct {
int data[MAX_SIZE];
int front;
int rear;
int flag; // 0:空, 1:非空, 2:满
} CircularQueue;
3.4 完整实现代码(牺牲一个存储单元法)
c
#include <stdio.h>
#include <stdlib.h>
#include <stdbool.h>
#define MAX_SIZE 5
typedef struct {
int data[MAX_SIZE];
int front; // 队头索引
int rear; // 队尾索引(指向下一个入队位置)
} CircularQueue;
// 1. 初始化队列
void InitCircularQueue(CircularQueue *q) {
q->front = 0;
q->rear = 0;
printf("初始化队列,front=%d, rear=%d\n", q->front, q->rear);
}
// 2. 判断队列是否为空
bool IsEmpty(CircularQueue *q) {
return q->front == q->rear;
}
// 3. 判断队列是否已满
bool IsFull(CircularQueue *q) {
return (q->rear + 1) % MAX_SIZE == q->front;
}
// 4. 入队操作
bool Enqueue(CircularQueue *q, int value) {
if (IsFull(q)) {
printf("队列已满,无法入队 %d\n", value);
return false;
}
q->data[q->rear] = value;
q->rear = (q->rear + 1) % MAX_SIZE;
printf("入队: %d, front=%d, rear=%d\n", value, q->front, q->rear);
return true;
}
// 5. 出队操作
bool Dequeue(CircularQueue *q, int *value) {
if (IsEmpty(q)) {
printf("队列为空,无法出队\n");
return false;
}
*value = q->data[q->front];
q->front = (q->front + 1) % MAX_SIZE;
printf("出队: %d, front=%d, rear=%d\n", *value, q->front, q->rear);
return true;
}
// 6. 获取队头元素(不出队)
bool GetFront(CircularQueue *q, int *value) {
if (IsEmpty(q)) {
printf("队列为空,无队头元素\n");
return false;
}
*value = q->data[q->front];
return true;
}
// 7. 获取队列元素个数
int GetSize(CircularQueue *q) {
return (q->rear - q->front + MAX_SIZE) % MAX_SIZE;
}
// 8. 打印队列状态
void PrintQueue(CircularQueue *q) {
printf("队列状态: [");
if (IsEmpty(q)) {
printf("空");
} else {
int i = q->front;
while (i != q->rear) {
printf("%d", q->data[i]);
i = (i + 1) % MAX_SIZE;
if (i != q->rear) printf(", ");
}
}
printf("], front=%d, rear=%d, size=%d\n",
q->front, q->rear, GetSize(q));
}
// 9. 测试函数
void TestCircularQueue() {
printf("===== 测试循环队列 =====\n");
CircularQueue q;
InitCircularQueue(&q);
printf("\n1. 入队测试:\n");
for (int i = 1; i <= 6; i++) {
Enqueue(&q, i * 10);
PrintQueue(&q);
}
printf("\n2. 出队测试:\n");
int value;
for (int i = 0; i < 3; i++) {
Dequeue(&q, &value);
PrintQueue(&q);
}
printf("\n3. 再次入队测试(验证循环):\n");
for (int i = 7; i <= 9; i++) {
Enqueue(&q, i * 10);
PrintQueue(&q);
}
printf("\n4. 查看队头元素:\n");
if (GetFront(&q, &value)) {
printf("队头元素: %d\n", value);
}
printf("\n5. 获取队列大小:\n");
printf("当前队列大小: %d\n", GetSize(&q));
}
int main() {
TestCircularQueue();
return 0;
}
3.5 循环队列状态图示
text
初始状态:front=0, rear=0
┌───┬───┬───┬───┬───┐
│ │ │ │ │ │
└───┴───┴───┴───┴───┘
↑
front/rear
入队10,20后:front=0, rear=2
┌───┬───┬───┬───┬───┐
│10 │20 │ │ │ │
└───┴───┴───┴───┴───┘
↑ ↑
front rear
出队10后:front=1, rear=2
┌───┬───┬───┬───┬───┐
│ │20 │ │ │ │
└───┴───┴───┴───┴───┘
↑ ↑
front rear
入队30,40,50后:front=1, rear=0(循环)
┌───┬───┬───┬───┬───┐
│50 │20 │30 │40 │ │
└───┴───┴───┴───┴───┘
↑
front=1, rear=0
↓
循环利用空间
4. 链式队列
4.1 结构定义
c
// 队列节点
typedef struct QueueNode {
int data;
struct QueueNode *next;
} QueueNode;
// 链式队列(带front和rear指针)
typedef struct {
QueueNode *front; // 队头指针
QueueNode *rear; // 队尾指针
int size; // 队列大小
} LinkedQueue;
4.2 完整实现代码
c
#include <stdio.h>
#include <stdlib.h>
#include <stdbool.h>
// 1. 节点定义
typedef struct QueueNode {
int data;
struct QueueNode *next;
} QueueNode;
// 2. 链式队列定义
typedef struct {
QueueNode *front;
QueueNode *rear;
int size;
} LinkedQueue;
// 3. 初始化队列
void InitLinkedQueue(LinkedQueue *q) {
q->front = NULL;
q->rear = NULL;
q->size = 0;
printf("初始化链式队列\n");
}
// 4. 创建新节点
QueueNode* CreateQueueNode(int value) {
QueueNode *node = (QueueNode*)malloc(sizeof(QueueNode));
if (node == NULL) {
printf("内存分配失败\n");
return NULL;
}
node->data = value;
node->next = NULL;
return node;
}
// 5. 判断队列是否为空
bool IsLinkedQueueEmpty(LinkedQueue *q) {
return q->front == NULL;
}
// 6. 入队操作
bool EnqueueLinked(LinkedQueue *q, int value) {
QueueNode *newNode = CreateQueueNode(value);
if (newNode == NULL) return false;
if (IsLinkedQueueEmpty(q)) {
// 队列为空时,front和rear都指向新节点
q->front = newNode;
q->rear = newNode;
} else {
// 队列不为空,将新节点加到队尾
q->rear->next = newNode;
q->rear = newNode;
}
q->size++;
printf("入队: %d, 队列大小: %d\n", value, q->size);
return true;
}
// 7. 出队操作
bool DequeueLinked(LinkedQueue *q, int *value) {
if (IsLinkedQueueEmpty(q)) {
printf("队列为空,无法出队\n");
return false;
}
QueueNode *temp = q->front;
*value = temp->data;
q->front = q->front->next;
// 如果出队后队列为空,需要重置rear
if (q->front == NULL) {
q->rear = NULL;
}
free(temp);
q->size--;
printf("出队: %d, 队列大小: %d\n", *value, q->size);
return true;
}
// 8. 获取队头元素
bool GetFrontLinked(LinkedQueue *q, int *value) {
if (IsLinkedQueueEmpty(q)) {
printf("队列为空,无队头元素\n");
return false;
}
*value = q->front->data;
return true;
}
// 9. 打印队列
void PrintLinkedQueue(LinkedQueue *q) {
if (IsLinkedQueueEmpty(q)) {
printf("队列为空\n");
return;
}
printf("队列元素(队头→队尾): ");
QueueNode *current = q->front;
while (current != NULL) {
printf("%d", current->data);
if (current->next != NULL) printf(" → ");
current = current->next;
}
printf("\n队列大小: %d\n", q->size);
}
// 10. 销毁队列
void DestroyLinkedQueue(LinkedQueue *q) {
int value;
printf("销毁队列,依次出队: ");
while (!IsLinkedQueueEmpty(q)) {
DequeueLinked(q, &value);
printf("%d ", value);
}
printf("\n队列已销毁\n");
}
// 11. 测试函数
void TestLinkedQueue() {
printf("===== 测试链式队列 =====\n");
LinkedQueue q;
InitLinkedQueue(&q);
printf("\n1. 入队测试:\n");
for (int i = 1; i <= 5; i++) {
EnqueueLinked(&q, i * 10);
}
PrintLinkedQueue(&q);
printf("\n2. 出队测试:\n");
int value;
for (int i = 0; i < 3; i++) {
DequeueLinked(&q, &value);
PrintLinkedQueue(&q);
}
printf("\n3. 再次入队测试:\n");
for (int i = 6; i <= 8; i++) {
EnqueueLinked(&q, i * 10);
}
PrintLinkedQueue(&q);
printf("\n4. 查看队头元素:\n");
if (GetFrontLinked(&q, &value)) {
printf("队头元素: %d\n", value);
}
printf("\n5. 销毁队列:\n");
DestroyLinkedQueue(&q);
}
4.3 链式队列内存图示
text
初始状态:
front → NULL
rear → NULL
size = 0
入队10后:
front → [10|NULL] ← rear
size = 1
入队20后:
front → [10|∙] → [20|NULL] ← rear
size = 2
入队30后:
front → [10|∙] → [20|∙] → [30|NULL] ← rear
size = 3
出队10后:
front → [20|∙] → [30|NULL] ← rear
size = 2
5. 顺序队列 vs 链式队列对比
| 特性 | 顺序循环队列 | 链式队列 |
|---|---|---|
| 存储结构 | 数组 | 链表 |
| 容量限制 | 固定大小 | 动态扩展 |
| 内存使用 | 连续内存 | 离散内存 |
| 空间效率 | 存储密度高 | 有指针开销 |
| 时间效率 | O(1) 操作 | O(1) 操作 |
| 适用场景 | 队列大小可预估 | 队列大小变化大 |
| 优点 | 实现简单,访问快 | 灵活,无大小限制 |
| 缺点 | 容量固定,可能溢出 | 内存碎片,指针开销 |
二、树形结构(Tree)详解
1. 树的基本概念
1.1 树的定义
树是一种非线性数据结构 ,描述数据间一对多的层次关系。
1.2 树的图示
text
A (根节点,第1层)
/ \
B C (分支节点,第2层)
/ \ \
D E F (叶子节点,第3层)
/ \
G H (叶子节点,第4层)
1.3 树的相关术语
-
节点(Node):树中的每个元素
-
根节点(Root):没有父节点的节点(如A)
-
叶子节点(Leaf):没有子节点的节点(如D,E,F,G,H)
-
分支节点/内部节点:有子节点的节点(如A,B,C)
-
父节点(Parent):节点的直接上级
-
子节点(Child):节点的直接下级
-
兄弟节点(Sibling):同一父节点的子节点
-
祖先节点(Ancestor):从根到该节点路径上的所有节点
-
后代节点(Descendant):以该节点为根的子树中的所有节点
-
度(Degree):节点的子节点个数
-
节点B的度 = 2(有D,E两个子节点)
-
叶子节点的度 = 0
-
-
树的度:树中所有节点的最大度数
-
路径(Path):从节点A到节点B所经过的节点序列
-
路径长度:路径上的边数
-
层(Level):根节点为第1层,向下依次递增
-
深度(Depth):节点到根节点的路径长度(根节点深度为0或1)
-
高度(Height):节点到最远叶子节点的路径长度(叶子节点高度为0或1)
-
树的高度:根节点的高度
-
森林(Forest):多棵互不相交的树的集合
2. 二叉树(Binary Tree)
2.1 二叉树的定义
每个节点最多有两个子节点(左孩子和右孩子)的树结构。
2.2 二叉树的五种基本形态
-
空二叉树
-
只有一个根节点
-
根节点只有左子树
-
根节点只有右子树
-
根节点有左右子树
2.3 特殊二叉树
2.3.1 满二叉树(Full Binary Tree)
-
所有叶子节点都在同一层
-
每个分支节点都有两个子节点
-
深度为k的满二叉树有2^k-1个节点
text
A
/ \
B C
/ \ / \
D E F G
2.3.2 完全二叉树(Complete Binary Tree)
-
除了最后一层,其他层都是满的
-
最后一层的叶子节点都集中在左侧
-
按层序编号,编号与满二叉树一一对应
text
A
/ \
B C
/ \ /
D E F
2.3.3 斜树(Skewed Tree)
- 所有节点都只有左子节点或只有右子节点
左斜树: 右斜树:
text
A A
/ \
B B
/ \
C C
2.4 二叉树的性质
-
在二叉树的第i层上最多有2^(i-1)个节点(i≥1)
-
深度为k的二叉树最多有2^k-1个节点(k≥1)
-
对于任意二叉树,叶子节点数n₀与度为2的节点数n₂满足:n₀ = n₂ + 1
-
具有n个节点的完全二叉树深度为⌊log₂n⌋ + 1
-
对完全二叉树从上到下、从左到右编号(1开始):
-
节点i的父节点编号为⌊i/2⌋
-
节点i的左孩子编号为2i(如果2i≤n)
-
节点i的右孩子编号为2i+1(如果2i+1≤n)
-
3. 二叉树的遍历
3.1 遍历的重要性
遍历是二叉树各种操作的基础:查找、插入、删除、统计等。
3.2 深度优先遍历(DFS)
3.2.1 前序遍历(Preorder Traversal)
访问顺序:根节点 → 左子树 → 右子树
3.2.2 中序遍历(Inorder Traversal)
访问顺序:左子树 → 根节点 → 右子树
3.2.3 后序遍历(Postorder Traversal)
访问顺序:左子树 → 右子树 → 根节点
3.3 广度优先遍历(层序遍历)
访问顺序:从上到下、从左到右逐层访问
3.4 遍历图示与示例
示例二叉树:
text
1
/ \
2 3
/ \ \
4 5 6
/ \
7 8
遍历结果:
-
前序遍历:1 2 4 7 8 5 3 6
-
访问顺序:根→左→右
-
记忆:根左右
-
-
中序遍历:7 4 8 2 5 1 3 6
-
访问顺序:左→根→右
-
记忆:左根右
-
-
后序遍历:7 8 4 5 2 6 3 1
-
访问顺序:左→右→根
-
记忆:左右根
-
-
层序遍历:1 2 3 4 5 6 7 8
- 访问顺序:逐层从左到右
三、队列与树的综合应用
1. 使用队列实现栈的功能
c
// 使用两个队列实现栈(后进先出)
typedef struct {
LinkedQueue queue1;
LinkedQueue queue2;
} StackUsingQueues;
// 入栈操作:总是进入非空的队列
void PushUsingQueues(StackUsingQueues *s, int value) {
if (!IsLinkedQueueEmpty(&s->queue1)) {
EnqueueLinked(&s->queue1, value);
} else {
EnqueueLinked(&s->queue2, value);
}
}
// 出栈操作:将非空队列的前n-1个元素转移到另一个队列,然后出队最后一个元素
int PopUsingQueues(StackUsingQueues *s) {
LinkedQueue *nonEmpty, *empty;
int value;
if (!IsLinkedQueueEmpty(&s->queue1)) {
nonEmpty = &s->queue1;
empty = &s->queue2;
} else {
nonEmpty = &s->queue2;
empty = &s->queue1;
}
// 将前n-1个元素转移到空队列
while (nonEmpty->size > 1) {
DequeueLinked(nonEmpty, &value);
EnqueueLinked(empty, value);
}
// 出队最后一个元素(栈顶)
DequeueLinked(nonEmpty, &value);
return value;
}
2. 使用栈实现队列的功能
c
// 使用两个栈实现队列(先进先出)
typedef struct {
Stack stack1; // 用于入队
Stack stack2; // 用于出队
} QueueUsingStacks;
// 入队操作:直接压入stack1
void EnqueueUsingStacks(QueueUsingStacks *q, int value) {
Push(&q->stack1, value);
}
// 出队操作:如果stack2为空,将stack1的所有元素弹出并压入stack2
int DequeueUsingStacks(QueueUsingStacks *q) {
if (IsStackEmpty(&q->stack2)) {
while (!IsStackEmpty(&q->stack1)) {
int value;
Pop(&q->stack1, &value);
Push(&q->stack2, value);
}
}
int value;
Pop(&q->stack2, &value);
return value;
}
四、总结
队列总结:
-
特点:先进先出(FIFO),队尾入队,队头出队
-
实现方式:
-
顺序循环队列:数组实现,需要处理"假溢出"
-
链式队列:链表实现,动态扩展,无大小限制
-
-
应用场景:任务调度、消息队列、广度优先搜索等
树总结:
-
特点:非线性结构,一对多关系,层次结构
-
二叉树特殊形式:
-
满二叉树:所有层都满
-
完全二叉树:除最后一层外都满,最后一层左侧连续
-
-
遍历方式:
-
深度优先:前序、中序、后序(递归或栈实现)
-
广度优先:层序(队列实现)
-
-
应用场景:文件系统、数据库索引、表达式求值、哈夫曼编码等