1. 队列
1.1 定义
是一种线性数据结构类型,可以用数组或链表等基础数据结构来实现。它遵循先进先出(FIFO,First In First Out)的原则。这意味着最先进入队列的元素会最先被移出。
1.2 基本概念
- 队列(Queue): 一种线性数据结构,遵循先进先出原则。
- 元素(Element): 队列中的单个数据项。
- 头(Front): 队列中最早添加的元素的位置,出队(dequeue)操作在此位置进行。
- 尾(Rear): 队列中最后添加的元素的位置,入队(enqueue)操作在此位置进行。
1.3 基本操作
- 入队(Enqueue): 将元素添加到队列的尾部。
- 出队(Dequeue): 移除并返回队列头部的元素。
- 检查队首元素(Peek/Front): 返回队列头部的元素,但不移除。
- 检查队列是否为空(IsEmpty): 判断队列是否为空。
- 检查队列是否已满(IsFull)(在固定大小的队列中): 判断队列是否已满。
1.4 实现方法
- 数组实现: 使用固定大小的数组实现队列,需处理数组的循环利用(环形队列)。
- 链表实现: 使用链表(单链表或双链表)实现队列,动态分配内存,无需预先指定大小。
1.5 应用场景
- 操作系统中的任务调度: 进程调度、打印任务管理。
- 广度优先搜索(BFS): 用于图和树的遍历。
- 缓冲区(Buffer): 数据流的处理,网络数据包的传输。
- 模拟现实世界的排队场景: 银行队列、超市结账等。
2. 链式队列——(一个指针指向头、一个指针指向尾)
2.1 makefile
OBJ:=a.out
OBJS+=main.c queue.c
CCl=gcc
$(OBJ):$(OBJS)
$(CC) $^ -o $@
.PHONY:
clean:
rm $(OBJ)
test:
valgrind --tool=memcheck --leak-check=full ./$(OBJ)
2.2 queue.h
#ifndef __QUEUE_H__
#define __QUEUE_H__
typedef int DataType;
typedef struct que_node
{
DataType data;
struct que_node *pnext;
}QueNode;
typedef struct que_list
{
QueNode *pfront;//指向队首
QueNode *prear;//指向队尾
int clen;
}QueList;
extern QueList *create_queue();
extern int push_queue(QueList *, DataType );
extern int pop_queue(QueList *, DataType *);
extern void queue_for_each(QueList *pque);
extern int get_queue_front(QueList *, DataType *);
extern void destroy_queue(QueList *);
extern void clear_queue(QueList *);
extern int is_empty_queue(QueList *pque);
#endif
2.3 queue.c
#include "queue.h"
#include <stdlib.h>
#include <stdio.h>
QueList *create_queue()
{
QueList *pque = malloc(sizeof(QueList));
if (NULL == pque)
{
perror("fail malloc");
return NULL;
}
pque->pfront = NULL;
pque->prear = NULL;
pque->clen = 0;
return pque;
}
int is_empty_queue(QueList *pque)
{
if (NULL == pque->pfront)
{
return 1;
}
return 0;
}
int push_queue(QueList *pque, DataType data)//入队——尾插
{
QueNode *pnode = malloc(sizeof(QueNode));
if (NULL == pnode)
{
perror("fail malloc");
return -1;
}
pnode->data = data;
pnode->pnext = NULL;
if (is_empty_queue(pque))//空队列
{
pque->pfront = pnode;
pque->prear = pnode;
}
else
{
pque->prear->pnext = pnode;
pque->prear = pnode;
}
pque->clen++;
return 0;
}
int pop_queue(QueList *pque, DataType *pdata)//出队——头删
{
if (is_empty_queue(pque))
{
return 1;
}
QueNode *pfree = pque->pfront;
pque->pfront = pfree->pnext;//prear没动,只用修改front的指向
if (pdata != NULL)//如果传入数据指针非空,把将要删除的数据传出函数
{
*pdata = pfree->data;
}
free(pfree);
pque->clen--;
if (NULL == pque->pfront)//如果队列删为空,将指向队尾的指针置为空,不为空,尾指针不变
{
pque->prear = NULL;
}
return 0;
}
void queue_for_each(QueList *pque)
{
QueNode *pnode = pque->pfront;
while (pnode != NULL)
{
printf("%d ", pnode->data);
pnode = pnode->pnext;
}
putchar('\n');
}
int get_queue_front(QueList *pque, DataType *pdata)//获得头指针数据
{
if (is_empty_queue(pque))
{
return -1;
}
*pdata = pque->pfront->data;
return 0;
}
void clear_queue(QueList *pque)//清空队列
{
while (!is_empty_queue(pque))
{
pop_queue(pque, NULL);
}
}
void destroy_queue(QueList *pque)//摧毁队列
{
clear_queue(pque);
free(pque);
}
2.4 main.c
#include <stdio.h>
#include "queue.h"
int main(int argc, const char *argv[])
{
int i = 0;
int ret = 0;
DataType push_data[] = {1, 2, 3 ,4, 5};
DataType data;//取出的队头元素
QueList *pque = create_queue();
if (NULL == pque)
{
return -1;
}
for (i = 0; i < sizeof(push_data)/sizeof(push_data[0]); i++)//入队
{
push_queue(pque, push_data[i]);
queue_for_each(pque);
}
#if 0
for (i = 0; i < sizeof(push_data)/sizeof(push_data[0]); i++)//出队
{
queue_for_each(pque);
pop_queue(pque, NULL);
}
#endif
#if 1
ret = get_queue_front(pque, &data);//获得队头元素
if (0 == ret)
{
printf("front data = %d\n", data);
}
destroy_queue(pque);
#endif
return 0;
}
3. 队列和栈结构的区别
3.1 数据结构特性
- 栈:栈是一种后进先出(LIFO,Last In First Out)的数据结构,最后压入栈的元素最先弹出。
- 队列:队列是一种先进先出(FIFO,First In First Out)的数据结构,最先进入队列的元素最先被移出。
3.2 操作
- 栈:栈支持的主要操作是压入(push)和弹出(pop)。元素只能从栈顶进行操作,不支持在中间插入或者访问非栈顶元素。
- 队列:队列支持的主要操作是入队(enqueue)和出队(dequeue)。元素只能从队列的头部删除(出队),从尾部添加(入队)。
3.3 访问元素方式
- 栈:只能访问栈顶元素,即最后压入栈的元素。
- 队列:可以访问队首和队尾的元素,但出队操作通常只能操作队首元素。
3.4 应用场景
- 栈:适合于需要后进先出访问顺序的场景,例如表达式求值、逆序输出、深度优先搜索的非递归实现等。
- 队列:适合于需要先进先出访问顺序的场景,例如广度优先搜索、缓冲区管理、任务调度等。
4. 树型结构——(一对多的非线性结构,由节点和边组成,具有层次的特点)
4.1 基本概念
- 节点(Node):树中的基本元素,每个节点包含一个数据元素及其子节点。
- 边(Edge):连接两个节点的路径。
- 根节点(Root):树的起始节点,没有父节点。
- 子节点(Child):某个节点的直接下属节点。
- 父节点(Parent):某个节点的直接上级节点。
- 叶节点(Leaf):没有子节点的节点。
- 内部节点(Internal Node):至少有一个子节点的节点。
- 子树(Subtree):节点及其所有后代节点构成的树。
- 深度(Depth):从根节点到某个节点的边数。
- 高度(Height):从某个节点到叶节点的最长路径的边数。
- 度(Degree):一个节点的子节点数量。
- 路径(Path):从一个节点到另一个节点经过的节点序列。
- 叶子节点(终端节点):只有前驱节点没有后继节点
- 非叶子节点(分支节点):根节点和叶子节点之间的节点
- 结点度:某节点后继节点的个数称
- 树的(广)度:树中各节点度的最大值 (整个树中某个节点的后继节点的最大值)
- 深度:从根节点到最底层节点的层数
4.2 树的分类
(1)二叉树(Binary Tree):每个节点最多有两个子节点(左子节点和右子节点)。
- 满二叉树(Full Binary Tree):所有节点要么是叶节点,要么有两个子节点。
- 完全二叉树(Complete Binary Tree):除了最后一层,其他层的节点都是满的,最后一层的节点从左到右连续排列。
- 平衡二叉树(Balanced Binary Tree):任何节点的两个子树的高度差至多为1。
- 搜索二叉树(Binary Search Tree,BST):左子节点的值小于父节点,右子节点的值大于父节点。
注意:1. 满二叉树第K层有2^(k-1)个节点
K层满二叉树总共有2^k-1个节点
2. 完全二叉树
在满二叉树的基础上
1. 增加节点:从左到又,从上到下依次增加
2. 减少节点:从下往上,从右往左依次减少
3. 满二叉树一定是完全二叉树
完全二叉树不一定是满二叉树
4. 已知完全二叉树的总节点个数,计算完全二叉树的叶子节点个数
解:通过注意1中满二叉树的规律,利用节点总个数和倒数第二层和倒数第一层的节点个数得出结果
(2)树堆(Heap)
- 最大堆(Max Heap):每个节点的值都大于等于其子节点的值。
- 最小堆(Min Heap):每个节点的值都小于等于其子节点的值。
(3)多路树(M-ary Tree):每个节点最多有 M 个子节点。
- B树(B-Tree):一种自平衡的多路查找树,用于数据库和文件系统。
- B+树(B+ Tree):B树的变种,所有值都在叶子节点,内部节点只存储索引。
(4)其他特殊树:
- 前序遍历(Pre-order Traversal):根节点 -> 左子树 -> 右子树
- 中序遍历(In-order Traversal):左子树 -> 根节点 -> 右子树
- 后序遍历(Post-order Traversal):左子树 -> 右子树 -> 根节点
- 层序遍历(Level-order Traversal):按层次从上到下、从左到右访问节点
4.3 树的应用场景
- 搜索和排序:二叉搜索树、AVL树、红黑树等用于快速搜索、插入和删除操作。
- 优先队列:堆用于实现优先队列,广泛应用于任务调度、图算法中的最短路径等。
- 数据库和文件系统:B树和B+树用于实现高效的数据库索引和文件系统管理。
- 字符串处理:字典树用于高效的前缀匹配和单词检索。
5. 二叉树的遍历
5.1 前序遍历 根 左 右
5.2 中序遍历 左 根 右
5.3 后序遍历 左 根 右
注意:前、中、后序遍历又叫深度优先
5.4 层序遍历 从上到下、从左至右,逐层向下遍历
注意:层序遍历又叫广度优先
5.5 二叉树的遍历特性
已知前序遍历序列和中序遍历序列,可以唯一确定一棵二叉树;
已知后序遍历序列和中序遍历序列,可以唯一确定一棵二叉树;
6. 二叉树实现代码——以递归为主
6.1 makefile
OBJ:=a.out
OBJS+=main.c tree.c queue.c
CCl=gcc
$(OBJ):$(OBJS)
$(CC) $^ -o $@
.PHONY:
clean:
rm $(OBJ)
test:
valgrind --tool=memcheck --leak-check=full ./$(OBJ)
6.2 tree.h
#ifndef __TREE_H__
#define __TREE_H__
typedef char TreeDataType;
typedef struct node
{
TreeDataType data;
struct node *pl;
struct node *pr;
}TreeNode;
extern TreeNode *create_bin_tree();
extern void pre_order(TreeNode *proot);
extern void mid_order(TreeNode *proot);
extern void pos_order(TreeNode *proot);
extern int get_tree_node_cnt(TreeNode *proot);
extern int get_tree_layer_cnt(TreeNode *proot);
extern void destroy_tree(TreeNode *proot);
extern void layer_order(TreeNode *proot);
#endif
6.3 tree.c
#include "tree.h"
#include <stdio.h>
#include <stdlib.h>
#include "queue.h"
char tree[] = {"ABE##F##DGH#I###C##"};
int idx = 0;
TreeNode *create_bin_tree()//二叉树的创建
{
TreeDataType data = tree[idx++];//tree数组后移的方式
if ('#' == data)//一个叶子节点遍历结束的标志
{
return NULL;
}
TreeNode *pnode = malloc(sizeof(TreeNode));
if (NULL == pnode)
{
perror("fail malloc");
return NULL;
}
pnode->data = data;
pnode->pl = create_bin_tree();//创建总子树
pnode->pr = create_bin_tree();//创建右子树
return pnode;
}
void pre_order(TreeNode *proot)//前序遍历 根 左 右
{
if (NULL == proot)//一次前序遍历结束的标志
{
return ;
}
printf("%c", proot->data);
pre_order(proot->pl);
pre_order(proot->pr);
}
void mid_order(TreeNode *proot)//中序遍历 左 根 右
{
if (NULL == proot)
{
return ;
}
mid_order(proot->pl);
printf("%c", proot->data);
mid_order(proot->pr);
}
void pos_order(TreeNode *proot)//后续遍历 左 右 根
{
if (NULL == proot)
{
return ;
}
pos_order(proot->pl);
pos_order(proot->pr);
printf("%c", proot->data);
}
int get_tree_node_cnt(TreeNode *proot)//获取二叉树节点个数
{
if (NULL == proot)
{
return 0;
}
return 1+get_tree_node_cnt(proot->pl)+get_tree_node_cnt(proot->pr);
}
int get_tree_layer_cnt(TreeNode *proot)//获取二叉树层数
{
if (NULL == proot)
{
return 0;
}
int cntl = get_tree_layer_cnt(proot->pl);
int cntr = get_tree_layer_cnt(proot->pr);
return cntl > cntr ? cntl+1 : cntr+1;
}
void destroy_tree(TreeNode *proot)//摧毁二叉树
{
if (NULL == proot)
{
return ;
}
destroy_tree(proot->pl);
destroy_tree(proot->pr);
free(proot);
}
void layer_order(TreeNode *proot)//层序遍历
{
DataType outdata;
QueList *pque = create_queue();
if (NULL == pque)
{
return ;
}
push_queue(pque, proot);
while (!is_empty_queue(pque))
{
pop_queue(pque, &outdata);
printf("%c", outdata->data);
if (outdata->pl != NULL)
{
push_queue(pque, outdata->pl);
}
if (outdata->pr != NULL)
{
push_queue(pque, outdata->pr);
}
}
destroy_queue(pque);
}
6.4 queue.h
#ifndef __QUEUE_H__
#define __QUEUE_H__
#include "tree.h"
typedef TreeNode* DataType;
typedef struct que_node
{
DataType data;
struct que_node *pnext;
}QueNode;
typedef struct que_list
{
QueNode *pfront;
QueNode *prear;
int clen;
}QueList;
extern QueList *create_queue();
extern int push_queue(QueList *, DataType );
extern int pop_queue(QueList *, DataType *);
extern int get_queue_front(QueList *, DataType *);
extern void destroy_queue(QueList *);
extern void clear_queue(QueList *);
extern int is_empty_queue(QueList *pque);
#endif
6.5 queue.c
#include "queue.h"
#include <stdlib.h>
#include <stdio.h>
QueList *create_queue()
{
QueList *pque = malloc(sizeof(QueList));
if (NULL == pque)
{
perror("fail malloc");
return NULL;
}
pque->pfront = NULL;
pque->prear = NULL;
pque->clen = 0;
return pque;
}
int is_empty_queue(QueList *pque)
{
if (NULL == pque->pfront)
{
return 1;
}
return 0;
}
int push_queue(QueList *pque, DataType data)
{
QueNode *pnode = malloc(sizeof(QueNode));
if (NULL == pnode)
{
perror("fail malloc");
return -1;
}
pnode->data = data;
pnode->pnext = NULL;
if (is_empty_queue(pque))
{
pque->pfront = pnode;
pque->prear = pnode;
}
else
{
pque->prear->pnext = pnode;
pque->prear = pnode;
}
pque->clen++;
return 0;
}
int pop_queue(QueList *pque, DataType *pdata)
{
if (is_empty_queue(pque))
{
return 1;
}
QueNode *pfree = pque->pfront;
pque->pfront = pfree->pnext;
if (pdata != NULL)
{
*pdata = pfree->data;
}
free(pfree);
pque->clen--;
if (NULL == pque->pfront)
{
pque->prear = NULL;
}
return 0;
}
int get_queue_front(QueList *pque, DataType *pdata)
{
if (is_empty_queue(pque))
{
return -1;
}
*pdata = pque->pfront->data;
return 0;
}
void destroy_queue(QueList *pque)
{
clear_queue(pque);
free(pque);
}
void clear_queue(QueList *pque)
{
while (!is_empty_queue(pque))
{
pop_queue(pque, NULL);
}
}
注意:链式队列实现二叉树层序遍历的方法
1. 创建二叉树
2. 将二叉树的根节点入队
3. 将根节点出队,打印根节点数据,并分别将根节点的左、右子树节点入队
4. 将根节点左节点出队,打印左节点数据,并将根节点左节点的左右节点入队
5. 循环上述操作直到遍历结束