数据结构之栈,队列,树

目录

一.栈

1.栈的概念及结构

2.栈的实现

3.实现讲解

1.初始化栈

2.销毁栈

3.压栈

4.出栈

5.返回栈顶元素

6.返回栈内元素个数

7.判断栈内是否为空

二.队列

1.队列的概念及结构

2.队列的实现

3.实现讲解

1.初始化队列

2.销毁队列

3.单个成员入队列

4.单个成员出队列

5.判断队列是否为空

6.返回队列内元素个数

7.返回队列首个元素

8.返回队列尾部元素

三.树

1.树的概念概念及结构

2.树的相关概念

3.树的实现

4.实现讲解

1.初始化树

2.销毁树

3.单个成员插入树

4.单个成员移除树

5.判断树是否为空

6.返回树中元素个数

7.返回树的顶部元素


---------------------------------------------------------++本文开始++------------------------------------------------------

一.栈

1.栈的概念及结构

栈:一种特殊的线性表,其只允许在固定的一端进行插入和删除元素操作。进行数据插入和删除操作的一端 称为栈顶,另一端称为栈底。
栈中的数据元素遵守后进先出 LIFO(Last In First Out) 的原则。
压栈:栈的插入操作叫做进栈/压栈/入栈,入数据在栈顶
出栈:栈的删除操作叫做出栈。出数据也在栈顶
下面是栈的概念图:

2.栈的实现

对于栈的实现我们可以使用数组或者是链表来完成,这里我们用数组来模拟实现一个栈:

3.实现讲解

对于栈,我们可以采用顺序表的形式来编写,首先是来对Stack结构体的定义,我们定义容量,栈顶数,以及数据指针

cpp 复制代码
typedef int StackDataType;
typedef struct Stack
{
	StackDataType* data;
	int capacity;
	int top;
}Stack;

其次,我们想让栈具有以下几个功能,分别是:

1.初始化栈

2.销毁栈

3.压栈

4.出栈

5.返回栈顶元素

6.返回栈内元素个数

7.判断栈是否为空

下面我们分批次来完成这些函数功能:

1.初始化栈

我们先对栈来申请四个数据类型的空间,这里我们可以使用malloc函数,判断一下是否开辟成功,然后我们**把开辟出的空间的地址赋值给栈内的data,**将栈内的容量capacity赋值为开辟的数量4,将栈内的栈顶数置为0

cpp 复制代码
void StackInit(Stack* q)
{
	assert(q);
	StackDataType* tmp = (StackDataType*)malloc(sizeof(StackDataType) * 4);
	if (tmp == NULL)
	{
		perror("malloc failed");
		return;
	}
	q->data = tmp;
	q->capacity = 4;
	q->top = 0;
}

2.销毁栈

比较简单!我们直接释放掉栈内的data指针所指向的空间,将data置为空即可:

cpp 复制代码
void StackDestroy(Stack* q)
{
	free(q->data);
	q->data = NULL;
}

3.压栈

将数据压入栈内,这里我们首先先来对栈内的容量和栈顶数做一个判断:

如果栈顶数等于栈的容量数,那么就说明此时栈已经满了,我们需要对栈进行扩容,

这里我们**使用realloc函数,对栈内的data指针重新调整一块空间,**这里我们同样简单判断一下空间是否开辟成功,并将栈内的容量数+4,代表我们扩容成功,

随后我们将要进行压栈的数据,放入data中,这里top就发挥作用了,可以让我们直接访问栈顶对应的空间,赋值即可,随后把top++代表放入数据成功:

cpp 复制代码
void StackPush(Stack* q, StackDataType x)
{
	if (q->capacity == q->top)
	{
		StackDataType* tmp = (StackDataType*)realloc(q->data, sizeof(StackDataType) * (q->capacity + 4));
		if (tmp == NULL)
		{
			perror("realloc failed");
			return;
		}
		printf("extend successfully");
		q->data = tmp;
		q->capacity += 4;
	}
	q->data[q->top++] = x;
}

4.出栈

这里我们要使用另一个函数**,判断栈内是否为空,如果为空,那么就没有数据可供pop出去了**,我们结束这个函数,在自己撰写时,***请记住提前声明函数哦,***这样之后我们直接对栈内的top--即可:

cpp 复制代码
void StackPop(Stack* q)
{
	assert(q);
	assert(!StackEmpty(q));
	q->top--;
}

5.返回栈顶元素

操作之前先来判断一下栈内是否为空,如果为空那么取栈顶数top-1就是错误访问数组了,

如果不为空,我们才返回data中的第top-1的元素:

cpp 复制代码
STDataType StackTop(Stack* ps)
{
	assert(ps);
	assert(!StackEmpty(ps));
	return ps->a[ps->top-1];
}

6.返回栈内元素个数

这里不需要进别的操作,直接返回栈顶数top即可:

cpp 复制代码
int StackSize(Stack* ps)
{
	return ps->top;
}

7.判断栈内是否为空

返回一个判断栈顶数top是否为0的表达式即可:

cpp 复制代码
bool StackEmpty(Stack* ps)
{
	return ps->top == 0;
}

二.队列

1.队列的概念及结构

队列:只允许在一端进行插入数据操作,在另一端进行删除数据操作的特殊线性表,队列具有先进先出 FIFO(First In First Out) 的性质
入队列:进行插入操作的一端称为队尾
出队列:进行删除操作的一端称为队头

2.队列的实现

如果要实现一个队列,如果我们模仿栈使用一个顺序表,那么出队列的时候就很麻烦了!,因此我们在这里使用更为方便的链表实现队列的功能:

先来定义队列中每个成员节点的内容以及队列结构体中的首尾指针,哦对了!千万别忘记了添加一个int size成员,这关联到一个队列的重要功能!:

cpp 复制代码
typedef int QDatatype;

typedef struct  QueueNode
{
	struct QueueNode* next;
	QDatatype data;
}QNode;

typedef struct Queue
{
	QNode* head;
	QNode* tail;
	int size;
}Queue;

一个完整的队列应该具备以下几个功能:

1.初始化队列

2.销毁队列

3.单个成员入队列

4.单个成员出队列

5.判断队列是否为空

6.返回队列内元素个数

7.返回队列首个元素

8.返回队列尾部元素

3.实现讲解

1.初始化队列

这里我们传入一个Queue类型的指针,默认已经创建好了一个Queue结构体,我们只需使用指针把Queue内的tail和head头尾指针置为空,代表元素个数的size置为0,就完成了队列的初始化:

cpp 复制代码
void QueueInit(Queue* pq)
{
	assert(pq);
	pq->head = NULL;
	pq->tail = NULL;
	pq->size = 0;
}

2.销毁队列

销毁队列难度不大,我们定义一个QNode*类型的指针cur,让cur从头指针head开始遍历整个队列,再定义另一个指针prev用来备份cur,判断条件就设置为cur!=NULL,** 然后让cur依次指向下一个节点,每次**cur指向下一个节点之前都赋值给prev,再释放prev指向的空间,**然后我们再把Queue结构体中的head,tail头尾指针置为空,size赋值为0即可:

cpp 复制代码
void QueueDestroy(Queue* pq)
{
	assert(pq);
	QNode* cur = pq->head;
	QNode* prev = NULL;
	while (cur)
	{
		prev = cur;
		cur = cur->next;
		free(prev);
	}
	pq->head = NULL;
	pq->tail = NULL;
	pq->size = 0;
}

3.单个成员入队列

入队列,PushQueue,这里我们传入Queue*指针,以及要入队列的数据x,我们**使用malloc先开辟一块QNode的空间来存放我们的数据,完成一个节点的创建,将newnode的next置为NULL,data赋值x,**其次,将这一节点连接到队列当中去,这里其实类似于链表中的尾插,最后,别忘了让Queue中的size自加!

cpp 复制代码
void QueuePush(Queue* pq, QDatatype x)
{
	assert(pq);
	QNode* newnode = (QNode*)malloc(sizeof(QNode));
	if (newnode == NULL)
	{
		perror("malloc failed");
		return;
	}
	newnode->next = NULL;
	newnode->data = x;
	if (pq->tail == NULL)
	{
		pq->head = newnode;
		pq->tail = newnode;
	}
	else
	{
		pq->tail->next = newnode;
		pq->tail = newnode;
	}
	pq->size++;
}

4.单个成员出队列

单个成员出队列,我们只需将head指针备份一下,将head重新赋值为下一个节点的地址,再释放备份的地址的空间,完成了出队列的功能,这里需要注意的是,当队列中**只有一个成员时,出队列后head指向的是空,同时我们还需将tail改为空,**否则在访问队列尾部成员的时候会*对空指针进行非法解引用操作,*并且要将size自减一个,代码如下:

cpp 复制代码
void QueuePop(Queue* pq)
{
	assert(pq);
	assert(pq->head);
	QNode* del = pq->head;
	pq->head = pq->head->next;
	free(del);
	del = NULL;
	if (pq->head == NULL)
	{
		pq->tail = NULL;
	}
	pq->size--;
}

5.判断队列是否为空

这里比较简单,我们直接返回判断size是否为0的表达式即可,值得一提的是,在前面出队列的时候,我们可以先用这一函数来判断队列是否为空,代替断言:

cpp 复制代码
bool QueueEmpty(Queue* pq)
{
	assert(pq);
	return pq->size == 0;
}

6.返回队列内元素个数

这里我们返回size就可以了,同样比较简单:

cpp 复制代码
int QueueSize(Queue* pq)
{
	assert(pq);
	return pq->size;
}

7.返回队列首个元素

请注意,这里如果直接对head指针进行解引用,那么head可能为空,引发非法解引用的错误,因此,我们需要用到上面的函数,来判断队列是否为空,其次再返回头部成员:

cpp 复制代码
QDatatype QueueFront(Queue* pq)
{
	assert(pq);
	assert(!QueueEmpty(pq));
	return pq->head->data;
}

8.返回队列尾部元素

同样的道理,这里如果直接对tail指针进行解引用,那么tail可能为空,引发非法解引用的错误,因此,我们需要用到上面的函数,来判断队列是否为空,其次再返回尾部成员:

cpp 复制代码
QDatatype QueueBack(Queue* pq)
{
	assert(pq);
	assert(!QueueEmpty(pq));
	return pq->tail->data;
}

三.树

1.树的概念概念及结构

树型结构是一类重要的非线性数据结构。其中以树和二叉树最为常用,直观看来,树是以分支关系定义的层次结构。把它叫做"树"是因为它常看起来像一棵倒挂的树,也就是说它常是根朝上,而叶朝下的。

2.树的相关概念


节点的度一个节点含有的子树的个数称为该节点的度; 如上图:A的为6
叶节点或终端节点度为0的节点称为叶节点; 如上图:B、C、H、I...等节点为叶节点
非终端节点或分支节点:度不为0的节点; 如上图:D、E、F、G...等节点为分支节点
双亲节点或父节点若一个节点含有子节点,则这个节点称为其子节点的父节点; 如上图:A是B的父节点
孩子节点或子节点一个节点含有的子树的根节点称为该节点的子节点; 如上图:B是A的孩子节点
兄弟节点:具有相同父节点的节点互称为兄弟节点; 如上图:B、C是兄弟节点
树的度:一棵树中, 最大的节点的度称为树的度; 如上图:树的度为6
节点的层次:从根开始定义起,根为第1层,根的子节点为第2层,以此类推;
树的高度或深度:树中节点的最大层次; 如上图:树的高度为4
堂兄弟节点:双亲在同一层的节点互为堂兄弟;如上图:H、I互为兄弟节点
节点的祖先:从根到该节点所经分支上的所有节点;如上图:A是所有节点的祖先
子孙以某节点为根的子树中任一节点都称为该节点的子孙 。如上图:所有节点都是A的子孙
森林:由m(m>0)棵互不相交的树的集合称为森林;

3.树的实现

树的实现可以采用链表实现,也可以采用顺序表也就是数组实现,这里我们只介绍数组实现树的方法,首先是对树这个结构体的定义问题,跟我们的顺序表相同,我们定义储存数据类型的指针,用来记录数据存放个数的size***,以及后面用来放入数据判断是否需要扩容的capacity*** :

cpp 复制代码
typedef int HPDataType;
typedef struct Heap
{
	HPDataType* a;
	int size;
	int capacity;
}HP;

一个完整的树应该具备以下几个功能:

1.初始化树

2.销毁树

3.单个成员插入树

4.单个成员移除树

5.判断树是否为空

6.返回树中元素个数

7.返回树的顶部元素

4.实现讲解

1.初始化树

初始化一个树,我们只需先使用malloc开辟一个初始设定值的空间来存放数据,并且,将结构体内部的capacity设定为初始值,size置为0即可,下面是代码实现:

cpp 复制代码
void HeapInit(HP* php)
{
	assert(php);
	php->a = (HPDataType*)malloc(sizeof(HPDataType) * 4);
	if (php->a == NULL)
	{
		perror("malloc failed");
		return;
	}
	php->size = 0;
	php->capacity = 4;
}

2.销毁树

比较简单,我们直接释放掉结构体内存放数据的指针a所指向空间即可,如果有***必要的话可以将a置为NULL,防止野指针的产生,***然后我们将capacity和size都置为0即可:

cpp 复制代码
void HeapDestroy1(HP* php)//这里函数名后面加1是因为可能会与系统的函数冲突!
{
	free(php->a);
	php->a = NULL;
	free(php);	
	php = NULL;
}

3.单个成员插入树

这里首先要来判断树的容量是否足够大,也就是树中的capacity是否与size相等,如果相等那么就需要扩容,这里我们**使用realloc重新申请一块capacity+增长值的空间,**再重新赋给a,

同时capacity加上增长的值就完成了扩容,我们将要插入的数据放在最后,size++,到这里还没有结束,这也是树不同于前面链表与顺序表的地方,我们还需要对树进行调整

这里我选择的是建大堆,那么就是说父亲节点比孩子节点要大,我们插入一个AdjustUp调整树函数:

cpp 复制代码
void HeapPush(HP* php, HPDataType x)
{
	if (php->capacity == php->size)
	{
		HPDataType* tmp = (HPDataType*)realloc(php->a, sizeof(HPDataType) * (php->capacity + 4));
		if (tmp == NULL)
		{
			perror("realloc failed");
			return;
		}
		php->a = tmp;
		php->capacity += 4;
	}
	php->a[php->size++] = x;
	AdjustUp(php->a, php->size - 1);
}

下面是AdjustUp的实现:

对于这个函数,我们传入孩子节点以及要调整的数组的地址,定义父亲节点

这里需要用到一条规律:**++父亲节点=(孩子节点-1)/2;++**具体的推导过程可以参考别的讲解文章,不在此做过多介绍,将孩子节点的值与父亲节点比较,

**如果前者大于后者,那么就进行交换,将父亲节点作为孩子节点继续判断,直到前者小于等于后者,就结束比较,**外层我们使用一个while循环,条件设置为child>0,

为什么不是parent<0呢?**经过计算发现,parent不可能小于0,最小就是0,**因此最佳的解决方案是判断孩子节点的下坐标:

cpp 复制代码
void swap(HPDataType* p1, HPDataType* p2)
{
	HPDataType tmp = *p1;
	*p1 = *p2;
	*p2 = tmp;
}
void AdjustUp(HPDataType* a, int child)
{
	int parent = (child - 1) / 2;
	while (child>0)
	{
		if (a[child] > a[parent])
		{
			swap(&a[child], &a[parent]);
			child = parent;
			parent = (child - 1) / 2;
		}
		else
		{
			break;
		}
	}
}

4.单个成员移除树

首先判断树是否为空,如果不为空,就将数组的第一个值与最后一个值进行交换,因为这里Pop的是树的顶部元素,**大堆顶部元素是最大的,**我们进行交换后将size--,这样就访问间接删除了顶部元素,然后这里我们还需要向下建堆,AdjustDown函数:

cpp 复制代码
void HeapPop(HP* php)
{
	assert(php);
	assert(!HeapEmpty(php));
	swap(&php->a[0], &php->a[php->size - 1]);
	php->size--;	
	AdjustDown(php->a, php->size, 0);
}

跟AdjustUp函数同理,对于这个函数,我们传入父亲节点以及要调整的数组的地址,还需要数组元素个数,定义孩子节点:

这里需要用到一条规律:++孩子节点=父亲节点*2+1,++ 这是公式++父亲节点=(孩子节点-1)/2的逆推++,

这里我们需要选出孩子节点中的较大者来与父亲节点进行比较,如果前者大于后者,那么就进行交换,将孩子节点作为父亲节点继续判断,直到前者小于等于后者,就结束比较,外层我们使用一个while循环,条件设置为child<元素个数n:

cpp 复制代码
void AdjustDown(HPDataType* a, int n, int parent)
{
	int child = parent * 2 + 1;
	while (child < n)
	{
		//选出最大的一个孩子
		if (child + 1 < n && a[child] < a[child + 1])
		{
			child = child + 1;
		}
		if (a[parent] < a[child])
		{
			swap(&a[parent], &a[child]);
			parent = child;
			child = parent * 2 + 1;
		}
		else
		{
			break;
		}
	}
}

5.判断树是否为空

返回结构体中size是否为0的判断表达式即可:

cpp 复制代码
bool HeapEmpty(HP* php)
{
	assert(php);
	return php->size == 0;
}

6.返回树中元素个数

直接返回size的值即可:

cpp 复制代码
int HeapSize1(HP* php)
{
	assert(php);
	return php->size;
}

7.返回树的顶部元素

这里需要判断树是否为空,在返回数组的第一个元素,也就是树的顶部元素:

cpp 复制代码
HPDataType HeapTop(HP* php)
{
	assert(php);
	assert(!HeapEmpty(php));
	return php->a[0];
}

数据结构就更新到这里啦!觉得对你有帮助的话可以多多支持作者哦~

------------------------------------------------------本文结束------------------------------------------------------

相关推荐
cr.sheeper6 分钟前
CTFHUB-web进阶-php
开发语言·php
徐子童17 分钟前
二分查找算法专题
数据结构·算法
小王子102425 分钟前
数据结构与算法Python版 二叉查找树
数据结构·python·算法·二叉查找树
Clockwiseee30 分钟前
PHP之伪协议
android·开发语言·php
开心工作室_kaic41 分钟前
springboot498基于javaweb的宠物猫认养系统(论文+源码)_kaic
java·开发语言·数据库·美食
秋雨凉人心1 小时前
上传npm包加强
开发语言·前端·javascript·webpack·npm·node.js
凡人的AI工具箱1 小时前
每天40分玩转Django:Django文件上传
开发语言·数据库·后端·python·django
DoNow☼1 小时前
什么是数据结构
数据结构
cloud___fly1 小时前
Java线程池面试题
java·开发语言
计科土狗1 小时前
前缀和与差分
c++·算法