栈、队列、树、哈希表
一、栈(Stack)
1.1 基本概念
定义 :限定仅在表尾 进行插入和删除操作的线性表
特性:
-
先进后出(FILO)
-
后进先出(LIFO)
核心概念:
-
栈顶(Top):允许操作的一端
-
栈底(Bottom):不允许操作的一端
-
入栈(Push):向栈顶添加元素
-
出栈(Pop):从栈顶移除元素
1.2 数据结构栈 vs 系统栈
| 对比项 | 数据结构栈 | 系统栈(调用栈) |
|---|---|---|
| 存储位置 | 堆空间 | 内存固定区域(约8M) |
| 内容 | 用户数据 | 函数调用、局部变量、参数 |
| 大小 | 可自定义 | 有限制 |
| 实现 | 数组/链表 | 硬件/OS支持 |
1.3 实现方式
顺序栈(数组实现)
#define MAXSIZE 100
typedef struct {
int data[MAXSIZE];
int top; // 栈顶指针
} SeqStack;
链栈(链表实现)
typedef struct StackNode {
int data;
struct StackNode *next;
} StackNode;
typedef struct {
StackNode *top; // 栈顶指针
int size; // 栈大小
} LinkStack;
1.4 基本操作
// 初始化
void InitStack(SeqStack *S) {
S->top = -1;
}
// 入栈
int Push(SeqStack *S, int x) {
if (S->top == MAXSIZE-1) return 0; // 栈满
S->data[++S->top] = x;
return 1;
}
// 出栈
int Pop(SeqStack *S, int *x) {
if (S->top == -1) return 0; // 栈空
*x = S->data[S->top--];
return 1;
}
1.5 应用场景
-
函数调用栈:保存调用现场
-
表达式求值:中缀转后缀
-
括号匹配:检查嵌套正确性
-
递归实现:转化为非递归
-
DFS算法:路径记录与回溯
二、队列(Queue)
2.1 基本概念
定义 :只允许在队尾 插入,在队头删除的线性表
特性:先进先出(FIFO)
核心概念:
-
队头(Front):允许删除的一端
-
队尾(Rear):允许插入的一端
2.2 实现方式
循环队列(顺序实现)
#define MAXSIZE 100
typedef struct {
int data[MAXSIZE];
int front; // 队头指针
int rear; // 队尾指针
} CircularQueue;
// 判空:front == rear
// 判满:(rear + 1) % MAXSIZE == front
链式队列
typedef struct QueueNode {
int data;
struct QueueNode *next;
} QueueNode;
typedef struct {
QueueNode *front; // 队头指针
QueueNode *rear; // 队尾指针
int size;
} LinkQueue;
2.3 基本操作
// 入队(循环队列)
int EnQueue(CircularQueue *Q, int x) {
if ((Q->rear + 1) % MAXSIZE == Q->front)
return 0; // 队满
Q->data[Q->rear] = x;
Q->rear = (Q->rear + 1) % MAXSIZE;
return 1;
}
// 出队
int DeQueue(CircularQueue *Q, int *x) {
if (Q->front == Q->rear)
return 0; // 队空
*x = Q->data[Q->front];
Q->front = (Q->front + 1) % MAXSIZE;
return 1;
}
2.4 应用场景
-
任务调度:CPU进程调度
-
缓冲区:打印机任务队列
-
BFS算法:图的广度优先搜索
-
消息队列:系统间通信
-
数据流处理:生产者-消费者模式
三、树(Tree)
3.1 基本概念
定义:n(n≥0)个节点的有限集合
-
n=0:空树
-
n>0:有且仅有一个根节点,其余分为m个互不相交的子树
重要术语:
-
节点的度:子树的个数
-
叶子节点:度为0的节点
-
树的度:树中最大的节点度数
-
节点的层次:根为第1层
-
树的高度:最大层次
3.2 二叉树(Binary Tree)
定义:每个节点最多有两个子树的树结构
特殊二叉树
-
满二叉树:
-
所有层都达到最大节点数
-
深度为k,节点数=2^k - 1
-
-
完全二叉树:
-
除最后一层外都是满的
-
最后一层节点集中在左边
-
二叉树性质
-
第i层最多有 2^(i-1) 个节点
-
深度为k的二叉树最多有 2^k - 1 个节点
-
叶子节点数 = 度为2的节点数 + 1
-
n个节点的完全二叉树深度 = ⌊log₂n⌋ + 1
3.3 遍历方式
深度优先遍历(DFS)
// 先序遍历:根->左->右
void PreOrder(TreeNode *root) {
if (root == NULL) return;
visit(root);
PreOrder(root->left);
PreOrder(root->right);
}
// 中序遍历:左->根->右
void InOrder(TreeNode *root) {
if (root == NULL) return;
InOrder(root->left);
visit(root);
InOrder(root->right);
}
// 后序遍历:左->右->根
void PostOrder(TreeNode *root) {
if (root == NULL) return;
PostOrder(root->left);
PostOrder(root->right);
visit(root);
}
广度优先遍历(层序遍历)
void LevelOrder(TreeNode *root) {
if (root == NULL) return;
TreeNode *queue[MAXSIZE];
int front = 0, rear = 0;
queue[rear++] = root;
while (front < rear) {
TreeNode *node = queue[front++];
visit(node);
if (node->left) queue[rear++] = node->left;
if (node->right) queue[rear++] = node->right;
}
}
3.4 存储结构
顺序存储(数组)
适用于完全二叉树
// 下标从1开始
// 节点i的父节点:i/2
// 左孩子:2*i,右孩子:2*i+1
链式存储(二叉链表)
typedef struct BiTNode {
int data;
struct BiTNode *lchild, *rchild;
} BiTNode, *BiTree;
3.5 树的应用
-
文件系统:目录结构
-
数据库索引:B树、B+树
-
决策树:机器学习
-
语法分析树:编译器
-
家谱图:层次关系
四、哈希表(Hash Table)
4.1 基本概念
定义:通过哈希函数将关键字映射到表中位置的数据结构
核心思想:存储位置 = f(key)
目标:实现O(1)的平均查找时间
4.2 哈希函数设计
| 方法 | 公式 | 特点 |
|---|---|---|
| 直接定址 | h(key) = a×key + b | 简单,无冲突 |
| 除留余数 | h(key) = key % p | 最常用 |
| 平方取中 | 取key²的中间几位 | 分布均匀 |
| 折叠法 | 分割相加 | 适合长关键字 |
4.3 冲突解决方法
开放定址法
// 线性探测:hᵢ(key) = (h(key) + i) % m
// 二次探测:hᵢ(key) = (h(key) ± i²) % m
// 双重散列:hᵢ(key) = (h₁(key) + i×h₂(key)) % m
链地址法(最常用)
typedef struct HashNode {
int key;
int value;
struct HashNode *next;
} HashNode;
typedef struct {
HashNode **table;
int size;
int count;
} HashMap;
4.4 性能指标
装载因子
α = 表中元素个数 / 哈希表长度
- 通常保持 α < 0.75
平均查找长度(ASL)
| 方法 | ASL成功(平均) | ASL失败(平均) |
|---|---|---|
| 线性探测 | (1+1/(1-α)²)/2 | (1+1/(1-α))/2 |
| 链地址法 | 1+α/2 | α+e⁻α |
4.5 基本操作实现
// 链地址法实现
HashMap* CreateHashMap(int size) {
HashMap *hm = (HashMap*)malloc(sizeof(HashMap));
hm->table = (HashNode**)calloc(size, sizeof(HashNode*));
hm->size = size;
hm->count = 0;
return hm;
}
int HashFunction(int key, int size) {
return abs(key) % size;
}
void HashMapPut(HashMap *hm, int key, int value) {
int index = HashFunction(key, hm->size);
// 查找是否已存在
HashNode *node = hm->table[index];
while (node) {
if (node->key == key) {
node->value = value; // 更新
return;
}
node = node->next;
}
// 新建节点,头插法
HashNode *newNode = (HashNode*)malloc(sizeof(HashNode));
newNode->key = key;
newNode->value = value;
newNode->next = hm->table[index];
hm->table[index] = newNode;
hm->count++;
}
int HashMapGet(HashMap *hm, int key) {
int index = HashFunction(key, hm->size);
HashNode *node = hm->table[index];
while (node) {
if (node->key == key)
return node->value;
node = node->next;
}
return -1; // 未找到
}
4.6 应用场景
-
快速查找:字典、缓存
-
数据去重:统计唯一值
-
数据库索引:加速查询
-
密码存储:存储哈希值
-
路由表:网络路由器
五、四种数据结构对比总结
| 特性 | 栈 | 队列 | 树 | 哈希表 |
|---|---|---|---|---|
| 访问方式 | 栈顶访问 | 队头访问 | 多种遍历 | 直接访问 |
| 插入位置 | 栈顶 | 队尾 | 指定位置 | 哈希位置 |
| 删除位置 | 栈顶 | 队头 | 指定位置 | 任意位置 |
| 时间复杂度 | O(1) | O(1) | 遍历O(n) | 平均O(1) |
| 空间效率 | 高 | 高 | 中等 | 较低 |
| 主要应用 | 函数调用 | 任务调度 | 层次关系 | 快速查找 |
六、学习建议与常见问题
6.1 栈的注意事项
-
栈溢出:递归深度过大或栈空间不足
-
括号匹配:注意不同括号的优先级
-
表达式求值:运算符优先级处理
6.2 队列的注意事项
-
循环队列判空/满:注意判断条件
-
假溢出:顺序队列的问题
-
队列大小:合理设置初始大小
6.3 树的注意事项
-
平衡性:树不平衡会影响性能
-
遍历顺序:不同遍历的应用场景
-
存储方式:根据需求选择合适结构
6.4 哈希表的注意事项
-
哈希函数设计:影响性能的关键
-
冲突处理:选择合适的解决方法
-
装载因子:及时扩容避免性能下降
-
内存管理:链地址法的节点释放
七、综合应用示例
7.1 计算器(栈应用)
int calculate(char* s) {
int stack[MAXSIZE], top = -1;
int num = 0, res = 0;
char sign = '+';
for (int i = 0; s[i]; i++) {
if (isdigit(s[i])) {
num = num * 10 + (s[i] - '0');
}
if ((!isdigit(s[i]) && s[i] != ' ') || i == strlen(s)-1) {
switch(sign) {
case '+': stack[++top] = num; break;
case '-': stack[++top] = -num; break;
case '*': stack[top] *= num; break;
case '/': stack[top] /= num; break;
}
sign = s[i];
num = 0;
}
}
while (top != -1) {
res += stack[top--];
}
return res;
}
7.2 LRU缓存(哈希表+双向链表)
// 实现O(1)的get和put操作
// 使用哈希表快速查找,双向链表维护访问顺序
这份笔记涵盖了栈、队列、树和哈希表的核心知识点。建议您:
-
理解每种数据结构的特点和适用场景
-
掌握基本操作的实现方法
-
多做练习题,特别是综合应用题目
-
在实际项目中尝试应用这些数据结构