C语言基础数据结构——栈和队列

目录

1.栈

1.1栈的选型

1.2 实现代码

2.队列

2.1整体思路

2.2初始化和销毁

2.3出入队列

2.4取队列元素

2.5判断队列是否为空

2.6返回队列中元素个数 

2.7 Test 


1.栈

       栈:一种特殊的线性表,其只允许在固定的一端进行插入和删除元素操作。 进行数据插入和删除操作的一端 称为栈顶,另一端称为栈底。 栈中的数据元素遵守后进先出 LIFO Last In First Out )的原则。 就像给枪械弹夹装子弹一样
                             
                                                      (图片源于网络)
压栈:栈的插入操作叫做进栈 / 压栈 / 入栈, 入数据在栈顶 。(压入子弹)
出栈:栈的删除操作叫做出栈。 出数据也在栈顶 。(射出子弹)
Tips:

          数据结构中的栈和堆只是与内存中的栈和堆名字相同,

实际上并没有太多联系。

                                                                         

1.1栈的选型
栈的实现一般可以使用 数组或者链表实现 ,相对而言数组的结构实现更优一些。因为数组在尾上插入数据的 代价比较小。
只有一头去变化,数组使用更快。

1.2 实现代码

初始化如下

typedef int STDataType;
typedef struct Stack {
	STDataType* arr;
	int capacity;
	int top;
}ST;
top作为栈顶
对top理解如下:
入栈的顺序就是1 2 3 4。

stackpush是唯一的放入数据的操作

top等于零时每插入一个元素就top++,指向栈顶元素的下一个

top指向栈顶元素的话必须让top=-1,如果还让top=0,那top=0时就不能区分此时是否有元素。

其余接口的实现(几乎等同于顺序表):

void STInit(ST* pst) {
	assert(pst);
	pst->top = 0;//初始状态可以是0也可以是-1
	pst->capacity = 0;
	pst->arr = NULL;
}

void STPush(ST* pst,STDataType x) {
	assert(pst);
	if (pst->top == pst->capacity) {
		int newcapacity = pst->capacity == 0 ? 4 : 2 * pst->capacity;
		STDataType* tmp = (STDataType*)realloc(pst->arr, sizeof(STDataType) * newcapacity);
		if (!tmp) {
			perror("realloc failed!");
			exit(1);
		}
		pst->arr = tmp;
		pst->capacity = newcapacity;
	}
	pst->arr[pst->top++] = x;
}

void STPop(ST* pst) {
	assert(pst);
	assert(!STEmpty(pst));
	pst->top--;
}

STDataType STTop(ST* pst) {
	assert(pst);
	assert(!STEmpty(pst));
	return pst->arr[pst->top - 1];
}

//删除或者取栈顶数据时都需要判断栈是否为空
bool STEmpty(ST* pst) {
	assert(pst);
	return pst->top == 0;
}

int STSize(ST* pst) {
	assert(pst);
	return pst->top;
}

void STDestroy(ST* pst) {
	assert(pst);
	free(pst->arr);
	pst->arr = NULL;
	pst->capacity = 0;
	pst->top = 0;
}

 应用时,注意函数声明的顺序

栈的出入顺序不一定是固定的

答案是C

链式栈(单链表)建议将头作为栈顶,因为栈尾不方便进行删除操作

2.队列

  队列:

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

    恰好与栈不同。

2.1整体思路

     用链表实现(单链表就足够了)更为合适,因为当数据的出和入不在同一边,若使用数组,势必有一段需要O(n)的复杂度来整体移动数组。

                  

       对于涉及链表尾部的操作,为了不遍历链表,我们加入一个ptail变量。

       因为涉及改变phead(队头)和ptail(队尾)的值,所以我们首先考虑使用phead和ptail的二级指针来维护头尾指针

struct Queue{
    QNode** pphead;
    QNode** pptail; 
};

单纯的链表不需要结构体来包含整个链表,但是队列需要结构体。我们可以类比的来看,顺序表中间有很多个值需要配合使用,所以不仅每个元素是结构体,整个顺序表也是个结构体。队列需要同时有头指针和尾指针(实现尾差和头删的功能),单独放两个有关队列的指针不合适,所以将两个指针放在一起构成一个新的结构体

(链表相对于队列,类似数组相对于顺序表)

    同时,将phead和ptail放进一个结构体时,上文所讲的二级指针问题也不存在了,可以直接通过结构体指针去更改phead和ptail

左为队列,右为链表 

(目前为止的队列和单链表的对比,本质依然是新瓶装旧酒)

 以上的设计在函数QueueSize(求队列中元素个数)中复杂度依然是O(n),但是如果在初始化和定义队列时加上size这个变量(加数据++,减数据--),整体复杂度又变回了O(1)。

        因此,数据结构的设计是灵活多变的,只是有一些传统,在传统的结构上我们应该根据自己的需要来增加、修改(包括是否需要size,是否需要哨兵位,是否需要头尾两个指针)

typedef int QDataType;
typedef struct QueueNode {
	QDataType val;
	struct QueueNode* next;
}QNode;

//为了便于维护,我们将链表进行设计而得到队列
typedef struct Queue {
	QNode* phead;
	QNode* ptail;
	int size;
}Queue;

再按照之前链表的实现方式,实现队列的各个接口。

2.2初始化和销毁

有了整体思路,目前学习的数据结构都应该先实现初始化和销毁接口。

void QueueInit(Queue* pq) {
	assert(pq);
	pq->phead = NULL;
	pq->ptail = NULL;
	pq->size = 0;
}

void QueueDestroy(Queue* pq) {
	assert(pq);
	QNode* cur = pq->phead;
	while (cur) {
		QNode* next = cur->next;
		free(cur);
		cur = next;
	}
	pq->phead = NULL;
	pq->ptail = NULL;
	pq->size = 0;
}
2.3出入队列

只有“入队列”一个函数需要malloc,所以我们不再封装BuyNewNode函数:

void QueuePush(Queue* pq, QDataType x) {
	assert(pq);
	QNode* newnode = (QNode*)malloc(sizeof(QNode));
	if (newnode == NULL) {
		perror("malloc fail!");
	}

	newnode->val = x;
	newnode->next = NULL;

	if (pq->phead == NULL) {
		pq->phead = pq->ptail = newnode;
	}
	else {
		pq->ptail->next = newnode;
		pq->ptail = pq->ptail->next;
	}
	pq->size++;
}

  每次进入队列,ptail就向后走,所以出队列的时候就应该出phead位置的Node 

但是以上代码有问题如下:

1.size数值没有变化

2.红笔圈出的部分可能是 NULL ptr

3.如果只有一个节点的话,ptail即将变成野指针

因此,对于出队列,需要分成:多个节点,一个节点,零节点三种情况进行讨论

void QueuePop(Queue* pq) {
	assert(pq);
	//0个节点,使用此类暴力检查或者带if语句的温柔检查
	assert(pq->phead && pq->ptail);
	//一个节点,单独
	if (pq->phead->next == NULL) {
		free(pq->phead);
		pq->phead = pq->ptail = NULL;
	}
	else {
		QNode* next = pq->phead->next;
		free(pq->phead);
		pq->phead = next;
	}
	pq->size--;
}
2.4取队列元素

虽然从理论上讲,队列只会从phead处取出数据,但是在具体运用中(如银行人工窗口的取号系统),我们要能够从头或者尾取出数据。

                                                  

实现如下: 

QDataType QueueFront(Queue* pq) {
	assert(pq);
	//必须使用暴力检查,因为有返回值
	assert(pq->phead);

	return pq->phead->val;
}

QDataType QueueBack(Queue* pq) {
	assert(pq);
	//必须使用暴力检查,因为有返回值
	assert(pq->ptail);

	return pq->ptail->val;
}

不同于没有返回值的出入队列函数,这两个函数都有返回值,若不适用暴力检查   来报错,就会返回一个不正确的值,埋下bug的种子。

Tips:

有读者问为什么后面的这些函数没使用perror检查是否成功,原因如下:

1.perror适用于偏系统的调用失败(如 malloc realloc)

2.这些调用如果真的失败,那电脑很可能已经出了大问题(那么小的空间都开辟不出来!!)

                                                    

2.5判断队列是否为空
bool QueueEmpty(Queue* pq) {
	assert(pq);
	return pq->size == 0;
}

很巧妙的写法,如果siez为0就返回1,表示确实为空。

2.6返回队列中元素个数 
int QueueSize(Queue* pq)
{
	assert(pq);

	return pq->size;
}

体现了size变量的好处。

2.7 Test 

不管是队列还是栈,检验方法都不同于之前的链表和顺序表,需要一个print函数来打印,队列和栈都是边使用边出,遍历一遍之后数据就全部出去了。

先检查入队列和取头功能

 最正确的测试方法:

数据用一个出一个。 

上一篇:爬虫逆向实战(36)-某建设监管平台(RSA,魔改)


下一篇:SpringWEB组件及运行流程