栈和队列详解

栈和队列详解_第1张图片

栈和队列详解

    • 栈的概念
    • 栈的实现
      • 栈的定义
      • 初始化栈
      • 入栈
      • 出栈
      • 获取栈顶元素
      • 获取栈中有效元素个数
      • 销毁栈
      • 测试
  • 队列
    • 队列的概念
    • 队列的实现
      • 定义结点和对列
      • 初始化队列
      • 销毁队列
      • 队尾入队列
      • 队头出队列
      • 获取队列头部元素
      • 获取队列尾部元素
      • 获取队列中有效数据个数
      • 判断队列不为空

栈的概念

栈是一种特殊的线性表,它增加了一个限制,只允许在固定的一端进行插入和删除元素操作。进行数据插入和删除操作的一端称为栈顶,另一端称为栈底。栈中的数据元素遵守后进先出的原则。
举例,假设我们依次将1,2,3,4压入栈中,压栈就是栈的插入操作,出栈就是删除操作
栈和队列详解_第2张图片

栈和队列详解_第3张图片

栈和队列详解_第4张图片
当我们再全部出栈是,就从原来的1,2,3,4变成了4,3,2,1.
这就是栈的先进后出的原则。

有个问题进栈的是1,2,3,4后进先出一定是4,3,2,1吗?
其实不一定也可以1,2,3,4,比如我进一个出一个,还有其他很多种可能,自己代进去试一下就知道。但3,1,2,4肯定不行,试一下就知道。

栈的实现

既然栈是一种特殊的线性表,那意味着用单链表和顺序表都可以实现,但那种更好呢?

单链表虽然不适合用尾插和尾删,但是我们可以把单链表的头当作栈顶,因为单链表适合头插和头删。

虽然数组有下标随机访问的优势,但是根据栈的后进先出的规则,这里只需要访问栈顶元素
所以其实两种都还是适合的,下面我就只用顺序表这种方法来实现。

栈和队列详解_第5张图片

栈的定义

typedef int STDataType;
typedef struct Stack
{
	STDataType* a;
	//栈顶
	int top;
	int capacity;
}Stack;

跟顺序表一样

初始化栈

void STInit(Stack* pst)
{
	assert(pst);
	pst->a = NULL;
	//top应该是多少?
	//如果是0的话,top指向栈顶元素的下一个
	//如果是-1的话,top指向栈顶元素
	pst->top = 0;
	pst->capacity = 0;
}

1 .top的话两种情况的都可以,只是略有差异,对后面写代码会有一定的影响,这里就用0。
2.一开始为什么容量是0?
后面入栈的时候直接扩容,当realloc的那块空间为0的话,它就会发挥malloc的作用。

入栈

void STPush(Stack* pst, STDataType x)
{
	assert(pst);
	//判断增容
	//这里也就能看出top初始化不同的值,所带来的差异
	if (pst->top == pst->capacity)
	{
		int newcapacity = pst->capacity == 0?4 :pst->capacity * 2;
		STDataType* tmp = (STDataType*)realloc(pst->a, sizeof(STDataType) * newcapacity);
		if (tmp == NULL)
		{
			perror("realloc fail");
			return;
		}
		pst->a = tmp;
		pst->capacity = newcapacity;
	}
	pst->a[pst->top] = x;
	pst->top++;
}

每次将数据放在栈顶的位置,再将栈顶往后推。

出栈

void STPop(Stack* pst)
{
	assert(pst);
	assert(pst->top);
	pst->top--;
}

出栈只需要把栈顶往回退一格。

注意要对top进行断言,当top为0的时候就不能删了。

获取栈顶元素

STDataType STTop(Stack* pst)
{
	assert(pst);
	assert(pst->top!=0);
	return pst->a[pst->top-1];
}

只需要返回栈顶的前一个位置

获取栈中有效元素个数

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

销毁栈

void STDestroy(Stack* pst)
{
	assert(pst);
	//error错误
	//free(pst);
	free(pst->a);
	pst->top = 0;
	pst->capacity = 0;
}

注意,pst不是malloc或者realloc出来的,所以不能用free。
写成这样的话,还需要增加对顺序表的理解,顺序表是用结构体里的一个成员指针去维护的,pst只是这个结构体的地址。

测试

void test1()
{
	Stack st;
	//记得要初始化
	STInit(&st);
	STPush(&st, 4);
    STPush(&st, 3);
	STPush(&st, 2);
	STPush(&st, 1);
	//获取栈顶元素只能一个一个获取
	while (st.top != 0)
	{
		STPop(&st);
		printf("%d ", STTop(&st));
	}
	//记得销毁栈,避免内存泄漏
	STDestroy(&st);
}

队列

队列的概念

队列同样是线性表的一种特殊结构,它与栈不同的是,队列要满足先进先出的特性。
进行插入操作的一端称为队尾 ,出队列进行删除操作的一端称为队头
这就跟我们排队的顺序是一样的,先排队的先出去。
栈和队列详解_第6张图片

请问进队列是1,2,3,4,出队列一定是1,2,3,4吗?
答案是一定的。 这个大家可以试一下。

队列的实现

请问队列用单链表实现还是用数组实现好?
1. 这里其实用数组已经很不方便了,数组要在队头删除数据需要挪动数组。

2. 单链表虽然不适合尾插,但是记录尾结点的时候,每次入数据就不用在重新找尾了。
下面就用单链表来实现队列。

定义结点和对列

首先单链表需要定义一个结点

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

因为我们要记录尾方便尾插,在加上我们本来就要有单链表头节点的指针来维护,为了方便 ,我们就再定义
一个结构体。

typedef struct Queue
{
	QNode* phead;
	QNode* ptail;
	//记录有效数据的个数,后面如果要求,直接返回
	int size;
}Queue;

有个有意思的问题,能不能把链表的尾指针也放到定义结点中?
这个问题大家可以想一下。

初始化队列

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 = pq->ptail = NULL;
	pq->size = 0;
}

还是要注意,只有动态开辟的空间才能用free;

队尾入队列

void QueuePush(Queue* pq, QDataType x)
{
	assert(pq);

	QNode* newnode = (QNode*)malloc(sizeof(QNode));
	if (newnode == NULL)
	{
		perror("malloc fail\n");
		return;
	}
	newnode->data = x;
	newnode->next = NULL;

	if (pq->ptail == NULL)
	{
	//也可以不断,以防万一
		assert(pq->phead == NULL);
		pq->phead = pq->ptail = newnode;
		pq->tail->next=NULL;
	}
	else
	{
		pq->ptail->next = newnode;
		pq->ptail = newnode;
		pq->tail->next=NULL;
	}


	pq->size++;
}

1. 尾插的时候要分类
链表为空链表的时候,这个时候要把新开辟的结点的地址赋值给tail;
链表为非空链表的时候,这个时候需要把新开辟 的结点再尾部链接
当然如果你创建一个哨兵卫就只需要考虑链接 ,不需要 分类了。

2. 之前写单链表的时候还是要用二级指针,这次为什么不用?
其实仔细分析还是一样,改变的只是结构体的成员而非指向结构体的指针。

队头出队列

void QueuePop(Queue* pq)
{
	assert(pq);
	assert(!QueueEmpty(pq));

	// 1、一个节点
	// 2、多个节点
	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--;
}

之前写单链表的时候还是挺简洁不用分类的,为什么这个时候要分类呢?
如果链表只有一个结点的时候,那tail也就等于phead,这时tail不置空就会变成一个野指针!!!

获取队列头部元素

QDataType QueueFront(Queue* pq)
{
	assert(pq);
	//写成这样增加可读性。
	assert(!QueueEmpty(pq));

	return pq->phead->data;
}

获取队列尾部元素

QDataType QueueBack(Queue* pq)
{
	assert(pq);
	assert(!QueueEmpty(pq));

	return pq->ptail->data;
}

获取队列中有效数据个数

int QueueSize(Queue* pq)
{
	assert(pq);

	return pq->size;
}

判断队列不为空

bool QueueEmpty(Queue* pq)
{
	assert(pq);
	return pq->size == 0;
}

总的来说栈和对列的实现只要链表和顺序表这块学的好,写起来还是相对轻松的。顺便还可以复习一下链表和顺序表的知识。

你可能感兴趣的:(数据结构,网络,数据结构,c语言)