常见的数据结构---队列、树与堆的深入剖析
目录
一、队列
二、树
三、堆
在现代计算机科学与工程领域,队列、树和堆是三种极其重要的基础数据结构,它们各自具有独特的特点和应用。在日常开发中,合理选择和使用这些数据结构可以显著提高程序的效率和可维护性。它们不仅奠定了算法设计的理论基础,还在系统开发、数据处理和实际应用中扮演着不可或缺的角色。
一、队列
队列(Queue)是一种特殊的线性数据结构,它遵循先进先出(First In First Out, FIFO)的原则。这意味着最早进入队列的元素将是最先被移除的元素。在队列中,插入操作通常发生在队尾(rear),而删除操作则发生在队头(front)。这种特性使得队列非常适合用来模拟现实生活中的排队现象,如银行或超市收银台前的顾客排队。
1. 队列的定义与特点
定义:队列是一种特殊的线性表,只允许在一端(称为队尾)插入数据,在另一端(称为队头)删除数据。
特点:
先进先出:队列中最早加入的元素最先被移除。
单向操作:数据只能从队尾插入,从队头移除。
受限性:与栈类似,操作位置有限,不支持随机访问。
2. 队列的基本操作
-
入队(Enqueue)在队尾插入一个元素。
-
出队(Dequeue)从队头移除一个元素。
-
获取队头元素(Front)查看队头元素,但不移除。
-
检查是否为空(IsEmpty)判断队列中是否还有数据。
-
检查是否已满(IsFull)(对于静态实现的队列)判断队列是否达到容量限制。
3. 队列的分类
3.1 普通队列
最简单的队列形式。
操作规则:
入队:在队尾插入数据。
出队:从队头移除数据。
#include <stdio.h>
#include <stdlib.h>
#define MAX_SIZE 100
typedef struct {
int data[MAX_SIZE];
int front; // 指向队头
int rear; // 指向队尾
} Queue;
// 初始化队列
void initQueue(Queue *q) {
q->front = 0;
q->rear = 0;
}
// 判断队列是否为空
int isEmpty(Queue *q) {
return q->front == q->rear;
}
// 判断队列是否已满
int isFull(Queue *q) {
return q->rear == MAX_SIZE;
}
// 入队操作
void enqueue(Queue *q, int value) {
if (isFull(q)) {
printf("Queue is full\n");
return;
}
q->data[q->rear++] = value;
}
// 出队操作
int dequeue(Queue *q) {
if (isEmpty(q)) {
printf("Queue is empty\n");
return -1;
}
return q->data[q->front++];
}
int main() {
Queue q;
initQueue(&q);
enqueue(&q, 10);
enqueue(&q, 20);
printf("Dequeued: %d\n", dequeue(&q));
printf("Dequeued: %d\n", dequeue(&q));
return 0;
}
3.2 循环队列
为了解决普通队列中由于数据出队导致的空间浪费问题,采用环状的存储方式。
特点:通过逻辑上的首尾相连使队列能高效利用存储空间。
实现:利用取模运算控制队列的首尾相连。
#include <stdio.h>
#include <stdlib.h>
#define MAX_SIZE 5
typedef struct {
int data[MAX_SIZE];
int front; // 指向队头
int rear; // 指向队尾
} CircularQueue;
// 初始化循环队列
void initCircularQueue(CircularQueue *q) {
q->front = q->rear = 0;
}
// 判断循环队列是否为空
int isCircularQueueEmpty(CircularQueue *q) {
return q->front == q->rear;
}
// 判断循环队列是否已满
int isCircularQueueFull(CircularQueue *q) {
return (q->rear + 1) % MAX_SIZE == q->front;
}
// 入队操作
void circularEnqueue(CircularQueue *q, int value) {
if (isCircularQueueFull(q)) {
printf("Circular Queue is full\n");
return;
}
q->data[q->rear] = value;
q->rear = (q->rear + 1) % MAX_SIZE;
}
// 出队操作
int circularDequeue(CircularQueue *q) {
if (isCircularQueueEmpty(q)) {
printf("Circular Queue is empty\n");
return -1;
}
int value = q->data[q->front];
q->front = (q->front + 1) % MAX_SIZE;
return value;
}
int main() {
CircularQueue q;
initCircularQueue(&q);
circularEnqueue(&q, 10);
circularEnqueue(&q, 20);
printf("Dequeued: %d\n", circularDequeue(&q));
printf("Dequeued: %d\n", circularDequeue(&q));
return 0;
}
3.3 双端队列(Deque, Double-Ended Queue)
支持在队列的两端进行入队和出队操作。
分类:
输入受限双端队列:只能从一端入队。
输出受限双端队列:只能从一端出队。
#include <stdio.h>
#include <stdlib.h>
#define MAX_SIZE 100
typedef struct {
int data[MAX_SIZE];
int front; // 指向队头
int rear; // 指向队尾
} Deque;
// 初始化双端队列
void initDeque(Deque *dq) {
dq->front = dq->rear = 0;
}
// 判断是否为空
int isDequeEmpty(Deque *dq) {
return dq->front == dq->rear;
}
// 判断是否已满
int isDequeFull(Deque *dq) {
return (dq->rear + 1) % MAX_SIZE == dq->front;
}
// 从队头插入
void enqueueFront(Deque *dq, int value) {
if (isDequeFull(dq)) {
printf("Deque is full\n");
return;
}
dq->front = (dq->front - 1 + MAX_SIZE) % MAX_SIZE;
dq->data[dq->front] = value;
}
// 从队尾插入
void enqueueRear(Deque *dq, int value) {
if (isDequeFull(dq)) {
printf("Deque is full\n");
return;
}
dq->data[dq->rear] = value;
dq->rear = (dq->rear + 1) % MAX_SIZE;
}
// 从队头删除
int dequeueFront(Deque *dq) {
if (isDequeEmpty(dq)) {
printf("Deque is empty\n");
return -1;
}
int value = dq->data[dq->front];
dq->front = (dq->front + 1) % MAX_SIZE;
return value;
}
// 从队尾删除
int dequeueRear(Deque *dq) {
if (isDequeEmpty(dq)) {
printf("Deque is empty\n");
return -1;
}
dq->rear = (dq->rear - 1 + MAX_SIZE) % MAX_SIZE;
return dq->data[dq->rear];
}
int main() {
Deque dq;
initDeque(&dq);
enqueueRear(&dq, 10);
enqueueRear(&dq, 20);
enqueueFront(&dq, 5);
printf("Dequeued from front: %d\n", dequeueFront(&dq));
printf("Dequeued from rear: %d\n", dequeueRear(&dq));
return 0;
}
3.4 优先队列(Priority Queue)
队列中的每个元素附加一个优先级,根据优先级决定元素的出队顺序。
实现方式:常用堆(Heap)结构实现。
#include <stdio.h>
#include <stdlib.h>
#define MAX_SIZE 100
typedef struct {
int data[MAX_SIZE];
int size; // 当前优先队列大小
} PriorityQueue;
// 初始化优先队列
void initPriorityQueue(PriorityQueue *pq) {
pq->size = 0;
}
// 插入元素到优先队列
void enqueuePriorityQueue(PriorityQueue *pq, int value) {
if (pq->size >= MAX_SIZE) {
printf("Priority Queue is full\n");
return;
}
pq->data[pq->size++] = value;
int i = pq->size - 1;
// 上浮操作
while (i > 0 && pq->data[i] < pq->data[(i - 1) / 2]) {
int temp = pq->data[i];
pq->data[i] = pq->data[(i - 1) / 2];
pq->data[(i - 1) / 2] = temp;
i = (i - 1) / 2;
}
}
// 移除优先队列中的最小元素
int dequeuePriorityQueue(PriorityQueue *pq) {
if (pq->size == 0) {
printf("Priority Queue is empty\n");
return -1;
}
int min = pq->data[0];
pq->data[0] = pq->data[--pq->size];
int i = 0;
// 下沉操作
while (2 * i + 1 < pq->size) {
int smallest = 2 * i + 1;
if (smallest + 1 < pq->size && pq->data[smallest + 1] < pq->data[smallest]) {
smallest++;
}
if (pq->data[i] <= pq->data[smallest]) break;
int temp = pq->data[i];
pq->data[i] = pq->data[smallest];
pq->data[smallest] = temp;
i = smallest;
}
return min;
}
int main() {
PriorityQueue pq;
initPriorityQueue(&pq);
enqueuePriorityQueue(&pq, 15);
enqueuePriorityQueue(&pq, 10);
enqueuePriorityQueue(&pq, 5);
printf("Dequeued: %d\n", dequeuePriorityQueue(&pq));
printf("Dequeued: %d\n", dequeuePriorityQueue(&pq));
return 0;
}
4. 队列的实现方式
4.1 顺序队列(基于数组)
优点:实现简单。
缺点:如果不使用循环队列,出队会导致存储空间浪费。数组大小固定,可能导致空间浪费或溢出。
#include <stdio.h>
#define MAX_SIZE 100
typedef struct {
int data[MAX_SIZE];
int front;
int rear;
} Queue;
void initQueue(Queue *q) {
q->front = q->rear = 0;
}
int isEmpty(Queue *q) {
return q->front == q->rear;
}
int isFull(Queue *q) {
return (q->rear + 1) % MAX_SIZE == q->front;
}
void enqueue(Queue *q, int value) {
if (isFull(q)) {
printf("Queue is full\n");
return;
}
q->data[q->rear] = value;
q->rear = (q->rear + 1) % MAX_SIZE;
}
int dequeue(Queue *q) {
if (isEmpty(q)) {
printf("Queue is empty\n");
return -1;
}
int value = q->data[q->front];
q->front = (q->front + 1) % MAX_SIZE;
return value;
}
int main() {
Queue q;
initQueue(&q);
enqueue(&q, 10);
enqueue(&q, 20);
printf("Dequeued: %d\n", dequeue(&q));
printf("Dequeued: %d\n", dequeue(&q));
return 0;
}
4.2 链式队列(基于链表)
优点:动态分配存储空间,无需提前固定大小。
缺点:需要额外的指针开销。
#include <stdio.h>
#include <stdlib.h>
typedef struct Node {
int data;
struct Node *next;
} Node;
typedef struct {
Node *front;
Node *rear;
} Queue;
void initQueue(Queue *q) {
q->front = q->rear = NULL;
}
int isEmpty(Queue *q) {
return q->front == NULL;
}
void enqueue(Queue *q, int value) {
Node *newNode = (Node *)malloc(sizeof(Node));
newNode->data = value;
newNode->next = NULL;
if (q->rear) {
q->rear->next = newNode;
} else {
q->front = newNode;
}
q->rear = newNode;
}
int dequeue(Queue *q) {
if (isEmpty(q)) {
printf("Queue is empty\n");
return -1;
}
Node *temp = q->front;
int value = temp->data;
q->front = temp->next;
if (q->front == NULL) {
q->rear = NULL;
}
free(temp);
return value;
}
int main() {
Queue q;
initQueue(&q);
enqueue(&q, 10);
enqueue(&q, 20);
printf("Dequeued: %d\n", dequeue(&q));
printf("Dequeued: %d\n", dequeue(&q));
return 0;
}
4.3 循环队列
原理:利用取模运算实现首尾相连。
操作公式:
入队位置:(rear+1)%capacity
出队位置:(front+1)%capacity
优点:充分利用存储空间。
缺点:实现稍微复杂。
#include <stdio.h>
#include <stdlib.h>
#define MAX_SIZE 5
typedef struct {
int data[MAX_SIZE];
int front; // 指向队头
int rear; // 指向队尾
} CircularQueue;
// 初始化循环队列
void initCircularQueue(CircularQueue *q) {
q->front = q->rear = 0;
}
// 判断循环队列是否为空
int isCircularQueueEmpty(CircularQueue *q) {
return q->front == q->rear;
}
// 判断循环队列是否已满
int isCircularQueueFull(CircularQueue *q) {
return (q->rear + 1) % MAX_SIZE == q->front;
}
// 入队操作
void circularEnqueue(CircularQueue *q, int value) {
if (isCircularQueueFull(q)) {
printf("Circular Queue is full\n");
return;
}
q->data[q->rear] = value;
q->rear = (q->rear + 1) % MAX_SIZE;
}
// 出队操作
int circularDequeue(CircularQueue *q) {
if (isCircularQueueEmpty(q)) {
printf("Circular Queue is empty\n");
return -1;
}
int value = q->data[q->front];
q->front = (q->front + 1) % MAX_SIZE;
return value;
}
int main() {
CircularQueue q;
initCircularQueue(&q);
circularEnqueue(&q, 10);
circularEnqueue(&q, 20);
printf("Dequeued: %d\n", circularDequeue(&q));
printf("Dequeued: %d\n", circularDequeue(&q));
return 0;
}
5. 队列的典型应用场景
-
广度优先搜索(BFS):在图算法中,使用队列存储当前层次的节点。
-
任务调度:在操作系统中,用队列管理任务执行顺序,如打印任务、进程调度等。
-
数据缓冲:在网络通信中,队列用于暂存数据以待后续处理。
-
生产者-消费者模型:队列作为中间缓冲区,连接生产者与消费者。
-
消息队列:在分布式系统中,用于异步通信和任务分发。
6. 队列的优势与局限性
优势
简单易用:先进先出的操作模式清晰且自然。
操作高效:逻辑简单,易于实现和使用。插入和删除操作的时间复杂度为 O(1)O(1)O(1)。
应用广泛:从算法设计到工程实践,队列都有重要作用。
局限性
操作受限:只能在两端进行操作,不支持随机访问。
空间利用问题:普通队列在频繁出队时可能造成空间浪费。
普通队列效率低:出队后空间不可复用。
7、小结
队列作为一种基础数据结构,因其先进先出的特性,在数据的有序处理、任务调度、广度搜索等场景中发挥了重要作用。
通过对队列的分类、操作及实现方式的深入理解,开发者能够灵活运用队列解决各种复杂问题,并优化程序性能。
顺序队列适用于固定大小的简单队列场景,但会导致存储空间浪费。链式队列通过动态分配解决空间问题,适合大小不确定的队列,但需额外的内存指针。循环队列通逻辑上的首尾相连实现存储空间的高效利用,是一种改进的顺序队列方式。
二、树
树是一种 非线性数据结构,以分层的方式存储数据。它由一个根节点(Root)和多个子节点(Child)组成,每个节点可能有多个子节点,但只有一个父节点(Parent)。树特别适用于分层组织和快速查找的场景。
1. 树的定义与特点
定义
树是由节点和边组成的结构,具有以下特点:
树中只有一个根节点。除根节点外的每个节点有且仅有一个父节点。节点之间通过边连接形成一个无环的层次结构。
树的特点
递归定义:树是一个根节点和若干子树的集合,每棵子树本质上也是一棵树。
无环性:从根节点到任意节点仅有一条路径。
层次结构:树具有明显的分层结构,数据之间存在父子关系。
节点关系:父节点:直接指向当前节点的节点。子节点:被当前节点指向的节点。兄弟节点:具有同一个父节点的节点。叶子节点:没有子节点的节点。根节点:没有父节点的节点。
2. 树的基本概念
根节点(Root):没有父节点的节点。
父节点(Parent):直接指向其他节点的节点。
子节点(Child):被某个节点直接指向的节点。
叶子节点(Leaf):没有子节点的节点。
度(Degree):节点的子节点个数称为该节点的度,树的最大度称为树的度。
层次(Level):树中节点的层次从根节点开始,根节点为第 1 层,其子节点为第 2 层,以此类推。
高度(Height):节点高度:从当前节点到叶子节点的最长路径上的边数。树高度:根节点的高度。
深度(Depth):节点的深度是从根节点到当前节点的路径长度。
路径(Path):从一个节点到另一个节点经过的所有节点和边。
森林(Forest):由多棵互不相交的树组成的集合。
3. 树的分类
按节点连接关系分类
普通树:节点可以有任意数量的子节点。
// 树的节点定义
typedef struct TreeNode {
int value;
struct TreeNode **children; // 子节点指针数组
int childCount; // 子节点数
} TreeNode;
二叉树:每个节点最多有两个子节点,分别是左子节点和右子节点。
特殊的二叉树
// 定义二叉树节点
typedef struct TreeNode {
int value;
struct TreeNode* left;
struct TreeNode* right;
} TreeNode;
搜索二叉树(BST):左子树的所有节点小于根节点,右子树的所有节点大于根节点。
// 插入
TreeNode* insertBST(TreeNode* root, int value) {
if (!root) return createTreeNode(value);
if (value < root->value)
root->left = insertBST(root->left, value);
else if (value > root->value)
root->right = insertBST(root->right, value);
return root;
}
// 搜索
TreeNode* searchBST(TreeNode* root, int value) {
if (!root || root->value == value) return root;
if (value < root->value)
return searchBST(root->left, value);
return searchBST(root->right, value);
}
平衡二叉树:左右子树高度差不超过 1。
满二叉树:所有层的节点数都达到最大值。
完全二叉树:所有层均满,最后一层的节点从左到右连续排列。
多叉树:每个节点可以有多个子节点,例如 N 叉树。
Trie 树:一种特殊的多叉树,用于快速存储和检索字符串。
红黑树、AVL树:自平衡二叉搜索树,用于保证操作的效率。
#include <stdio.h>
#include <stdlib.h>
// AVL 树节点定义
typedef struct AVLNode {
int value;
int height;
struct AVLNode* left;
struct AVLNode* right;
} AVLNode;
// 获取节点高度
int getHeight(AVLNode* node) {
return node ? node->height : 0;
}
// 计算平衡因子
int getBalanceFactor(AVLNode* node) {
return getHeight(node->left) - getHeight(node->right);
}
// 创建新节点
AVLNode* createAVLNode(int value) {
AVLNode* node = (AVLNode*)malloc(sizeof(AVLNode));
node->value = value;
node->height = 1;
node->left = NULL;
node->right = NULL;
return node;
}
// 右旋
AVLNode* rightRotate(AVLNode* y) {
AVLNode* x = y->left;
AVLNode* T = x->right;
x->right = y;
y->left = T;
y->height = 1 + (getHeight(y->left) > getHeight(y->right) ? getHeight(y->left) : getHeight(y->right));
x->height = 1 + (getHeight(x->left) > getHeight(x->right) ? getHeight(x->left) : getHeight(x->right));
return x;
}
// 左旋
AVLNode* leftRotate(AVLNode* x) {
AVLNode* y = x->right;
AVLNode* T = y->left;
y->left = x;
x->right = T;
x->height = 1 + (getHeight(x->left) > getHeight(x->right) ? getHeight(x->left) : getHeight(x->right));
y->height = 1 + (getHeight(y->left) > getHeight(y->right) ? getHeight(y->left) : getHeight(y->right));
return y;
}
// 插入节点
AVLNode* insertAVL(AVLNode* root, int value) {
if (!root) return createAVLNode(value);
if (value < root->value)
root->left = insertAVL(root->left, value);
else if (value > root->value)
root->right = insertAVL(root->right, value);
else
return root;
root->height = 1 + (getHeight(root->left) > getHeight(root->right) ? getHeight(root->left) : getHeight(root->right));
int balance = getBalanceFactor(root);
// 左左
if (balance > 1 && value < root->left->value)
return rightRotate(root);
// 右右
if (balance < -1 && value > root->right->value)
return leftRotate(root);
// 左右
if (balance > 1 && value > root->left->value) {
root->left = leftRotate(root->left);
return rightRotate(root);
}
// 右左
if (balance < -1 && value < root->right->value) {
root->right = rightRotate(root->right);
return leftRotate(root);
}
return root;
}
3.2 按数据组织方式分类
堆(Heap):完全二叉树,满足堆序性质。
并查集树:用于解决连通性问题,支持合并和查找操作。
4. 树的基本操作
遍历:深度优先遍历(DFS):前序遍历:根 → 左 → 右。中序遍历:左 → 根 → 右。后序遍历:左 → 右 → 根。广度优先遍历(BFS):层次遍历:按层次从上到下,从左到右依次访问节点。
插入:根据树的性质插入节点,例如在二叉搜索树中插入时需保持排序规则。
删除:移除指定节点后需调整树结构以保持性质,例如删除二叉搜索树节点时需替换为合适的继承节点。
查找:按树的规则查找特定节点,如二叉搜索树的查找效率为 O(logn)O(\log n)O(logn)。
高度计算:递归或迭代计算节点的高度。
5. 树的存储方式
链式存储:
每个节点通过指针指向其子节点和父节点。常见于动态树结构,如链表表示的二叉树。
typedef struct TreeNode {
int value;
struct TreeNode *left;
struct TreeNode *right;
} TreeNode;
顺序存储:使用数组存储完全二叉树。左子节点的索引为 2i+1,右子节点的索引为 2i+2。
#include <stdio.h>
#include <stdlib.h>
#define MAX_SIZE 100 // 最大节点数
typedef struct CompleteBinaryTree {
int data[MAX_SIZE]; // 数组存储完全二叉树的节点值
int size; // 当前节点数
} CompleteBinaryTree;
// 初始化完全二叉树
void initTree(CompleteBinaryTree* tree) {
tree->size = 0;
}
// 插入节点
void insertNode(CompleteBinaryTree* tree, int value) {
if (tree->size >= MAX_SIZE) {
printf("Tree is full. Cannot insert more nodes.\n");
return;
}
tree->data[tree->size] = value;
tree->size++;
}
// 获取左子节点索引
int getLeftChildIndex(int index) {
return 2 * index + 1;
}
// 获取右子节点索引
int getRightChildIndex(int index) {
return 2 * index + 2;
}
// 打印树的层序遍历
void printTree(CompleteBinaryTree* tree) {
printf("Tree: ");
for (int i = 0; i < tree->size; i++) {
printf("%d ", tree->data[i]);
}
printf("\n");
}
// 打印节点的左右子节点
void printChildren(CompleteBinaryTree* tree, int index) {
if (index >= tree->size) {
printf("Invalid index\n");
return;
}
printf("Node %d: ", tree->data[index]);
int leftIndex = getLeftChildIndex(index);
int rightIndex = getRightChildIndex(index);
if (leftIndex < tree->size)
printf("Left Child = %d ", tree->data[leftIndex]);
else
printf("Left Child = NULL ");
if (rightIndex < tree->size)
printf("Right Child = %d ", tree->data[rightIndex]);
else
printf("Right Child = NULL ");
printf("\n");
}
int main() {
CompleteBinaryTree tree;
initTree(&tree);
// 插入节点
insertNode(&tree, 1);
insertNode(&tree, 2);
insertNode(&tree, 3);
insertNode(&tree, 4);
insertNode(&tree, 5);
insertNode(&tree, 6);
// 打印树
printTree(&tree);
// 打印某些节点的子节点
printChildren(&tree, 0); // 根节点
printChildren(&tree, 1); // 第二层左节点
printChildren(&tree, 2); // 第二层右节点
return 0;
}
6. 树的应用场景
- 二叉树:二叉搜索树(BST):高效查找、插入、删除。堆:优先队列的实现。
- Trie 树:快速字符串匹配、前缀查询。
- 红黑树、AVL 树:数据库索引、动态集合操作。
- 决策树:机器学习中的分类和回归算法。
- 表达式树:表达式求值。
- 文件系统:文件和目录的层次管理。
7. 树的优势与局限性
优势
- 自然表示分层结构,适合数据的分级存储。
- 支持高效的查找、插入和删除操作。
- 可扩展性强,灵活适应不同场景。
局限性
- 链式存储需要额外的指针开销。
- 树结构的实现较为复杂,维护特性(如平衡)需付出额外代价。
- 不适合随机访问,性能劣于数组。
8. 小结
树作为一种基础数据结构,在理论研究和实际工程中都具有重要地位,广泛应用于文件系统、数据库索引、路由表等场景。通过不同种类的树(如二叉树、AVL树、红黑树等),我们可以根据需求选择适合的结构来优化数据操作效率。掌握树的基本知识和实现方法,是深入学习算法与数据结构的重要一步。
二叉树及其变种:解决高效数据存储与操作问题。Trie 树:适合字符串处理场景。红黑树、AVL 树:优化动态数据的平衡性。掌握树的基本原理、特性及其适用场景,是深入理解算法与系统设计的关键。
三、堆
1、堆的定义与特性
堆(Heap)是一种特殊的基于树的抽象数据类型(特殊的完全二叉树),堆通常用于实现优先队列,在这种结构中,最高优先级的元素总是位于根节点。
堆满足以下特性:对于任意给定的节点i
,如果P
是i
的父节点,那么P
的键值(或元素)要么大于等于(最大堆, max-heap),要么小于等于(最小堆, min-heap)i
的键值。
大顶堆(Max Heap):任意节点的值都不小于其子节点的值,堆顶为最大值。
小顶堆(Min Heap):任意节点的值都不大于其子节点的值,堆顶为最小值。
完全二叉树 (Complete Binary Tree): 堆通常以完全二叉树的形式实现,这意味着除了最底层外,所有层都是满的,并且最底层的节点尽可能靠左排列。
存储结构:通常使用数组来存储堆,逻辑上的父子节点关系可以通过索引计算:
父节点索引:i
左子节点索引:2i + 1
右子节点索引:2i + 2
若需要访问父节点:(i - 1) / 2
2、堆的基本操作
-
插入(Insert):在堆尾添加新元素。将新元素“上浮”(Bubble Up),以保持堆的性质。
-
删除堆顶元素(Remove Top/Pop):将堆顶元素替换为最后一个元素。删除最后一个元素。将新堆顶“下沉”(Bubble Down),以保持堆的性质。
-
堆化(Heapify):将无序数组调整为堆结构。从最后一个非叶节点开始,对每个节点执行下沉操作。
-
堆排序(Heap Sort):利用堆性质进行排序。大顶堆适用于升序排序,小顶堆适用于降序排序。
- 获取最大/最小元素 (Get-Max/Get-Min): 对于最大堆或最小堆,根节点就是具有最大或最小键值的元素。
3、应用场景
优先队列 (Priority Queue): 堆是最常用的优先队列实现方式之一。在优先队列中,每次取出的都是具有最高优先级的元素。
堆排序 (HeapSort): 一种高效的排序算法,时间复杂度为O(n log n)。
选择问题 (Selection Problem): 如寻找数组中的第k大或第k小元素。
图算法: 例如Dijkstra算法和Prim算法,其中需要频繁访问或更新最小权重的边。
内存管理: 某些操作系统使用堆来管理动态内存分配。
4、代码实现
1. 最大堆(大顶堆)
#include <stdio.h>
#include <stdlib.h>
#define MAX_SIZE 100
typedef struct {
int data[MAX_SIZE];
int size;
} MaxHeap;
// 初始化堆
void initHeap(MaxHeap* heap) {
heap->size = 0;
}
// 上浮操作
void bubbleUp(MaxHeap* heap, int index) {
while (index > 0) {
int parent = (index - 1) / 2;
if (heap->data[index] > heap->data[parent]) {
// 交换
int temp = heap->data[index];
heap->data[index] = heap->data[parent];
heap->data[parent] = temp;
index = parent;
} else {
break;
}
}
}
// 插入元素
void insert(MaxHeap* heap, int value) {
if (heap->size >= MAX_SIZE) {
printf("Heap is full!\n");
return;
}
heap->data[heap->size] = value;
bubbleUp(heap, heap->size);
heap->size++;
}
// 下沉操作
void bubbleDown(MaxHeap* heap, int index) {
while (index * 2 + 1 < heap->size) {
int leftChild = index * 2 + 1;
int rightChild = index * 2 + 2;
int largerChild = leftChild;
if (rightChild < heap->size && heap->data[rightChild] > heap->data[leftChild]) {
largerChild = rightChild;
}
if (heap->data[index] < heap->data[largerChild]) {
// 交换
int temp = heap->data[index];
heap->data[index] = heap->data[largerChild];
heap->data[largerChild] = temp;
index = largerChild;
} else {
break;
}
}
}
// 删除堆顶元素
int removeTop(MaxHeap* heap) {
if (heap->size == 0) {
printf("Heap is empty!\n");
return -1;
}
int top = heap->data[0];
heap->data[0] = heap->data[--heap->size];
bubbleDown(heap, 0);
return top;
}
// 打印堆
void printHeap(MaxHeap* heap) {
for (int i = 0; i < heap->size; i++) {
printf("%d ", heap->data[i]);
}
printf("\n");
}
int main() {
MaxHeap heap;
initHeap(&heap);
insert(&heap, 10);
insert(&heap, 20);
insert(&heap, 15);
insert(&heap, 30);
insert(&heap, 25);
printf("Heap elements: ");
printHeap(&heap);
printf("Removed top: %d\n", removeTop(&heap));
printf("Heap after removal: ");
printHeap(&heap);
return 0;
}
2. 堆排序
void heapSort(int arr[], int n) {
// 构建大顶堆
for (int i = n / 2 - 1; i >= 0; i--) {
bubbleDown(arr, n, i);
}
// 逐步将堆顶移到数组末尾,缩小堆的范围
for (int i = n - 1; i > 0; i--) {
int temp = arr[0];
arr[0] = arr[i];
arr[i] = temp;
bubbleDown(arr, i, 0); // 对新的堆顶进行下沉
}
}
5、小结
堆是一种功能强大的数据结构,适用于优先级处理和高效排序。它利用了完全二叉树的特性,结合数组实现,简化了存储和计算,同时保持了良好的时间复杂度,是许多算法和系统开发中的核心工具。
总结来说,队列、树和堆都是非常重要的基础数据结构,各自在不同的场景中发挥着不可替代的作用。队列适合处理按顺序排队的任务,树则为数据的组织和快速检索提供了有效的解决方案,而堆则常用于需要优先级排序的场合。通过深入理解它们的实现与应用,开发者可以根据实际需求选择最适合的结构,提高程序的性能与可靠性。掌握这些数据结构,不仅能提升代码的效率,还能为进一步学习复杂算法打下坚实的基础。