目录
你们好,各位未来的高级程序员们,你们好,今天我来为大家讲解一下栈和队列,相信大家对这部分的内容是很期待的。
一.栈:
1.介绍:
它是一种特殊的线性表,其只允许在固定的一端进行插入和删除操作。进行数据的插入和删除操作的一端被称作是栈顶,而另一端被称作是叫栈底。栈中的元素遵循先进后出的原则。(如下图所示),
(画画能力不是很好,请见谅)。除此之外,再给大家介绍两个有关栈方面知识的编程标准术语,
压栈:栈的输入操作,(入数据在栈顶);出栈:栈的输出(删除)操作,(出数据也在栈顶)。
2.栈的实现
(我们的家在构造栈的时候,要注意实现栈要以数组来实现栈,相信这里就有很多小伙伴就会问到为什么,我们之所以使用数组来实现栈,是因为数组相较于链表而言对于元素的访问很便利和快捷,而链表无法像数组一样这么快就能访问到元素,而是需要一次次地去遍历链表,这样很费时间,虽然说时间上的差距并不是很大,但是如果使用数组的话访问到的元素会更加方便,因此,基于多种因素的考虑,最终决定在这里使用数组来实现栈)。
(1).首先我们需要定义一个栈(定义一个结构体):
// 支持动态增长的栈
typedef int STDataType;
typedef struct Stack
{
STDataType* _a;
int _top;//标识栈顶元素
int _capacity;
}Stack;
我们在这里之所以不在结构体里设置一个数组,是因为我们最好在这里设置一个动态数组,这样的话方便我们日后数组的空间不够的话我们可以对数组进行扩充。否则,数组空间是固定的,存放的元素是固定的,那么这个栈的实用性就不强。
对了,是不是还有一些小伙伴对这里为什么要将它们放在结构体里面有一些不清楚,这是为了我们日后在进行函数传参时更加方便,为什么这么说呢,你每次传参不是都要将 _a(动态数组),_top(栈顶元素的位置),_capacity(容量)这三个变量传过去吗,大家不妨看一想,是传一个参数方便还是传三个参数方便呢?毋庸置疑,肯定是一个,而且更加准确。
(2).栈的初始化:
这是一个不错的习惯,建议大家都养成。
// 初始化栈
void StackInit(Stack* ps)
{
assert(ps);
ps->_a = NULL;
ps->_capacity = 0;
ps->_top = 0;//top指向的是栈顶元素的下一个位置
}
对了,说到这里,再来给大家将一个问题,就是top这个指针(top作为下标来访问数组中的元素)它到底指向的是栈顶元素的位置,还是栈顶元素的下一个位置,其实这个问题还是要看你自己的意愿,他具体指向哪里取决于你想让它指向哪里,具体解释如下列所示:
a.top指向栈顶元素的下一个位置:在初始化top的时候将top初始化为0(也就是ps->_top = 0;)当top等于0的时候,这个位置此时是没有值的,也就是此时这个位置是空的,我们可以向这个位置放元素,比如说向这个位置放值为1,写出的代码如下所示:
ps->_a[ps->top]=1;
ps->top++;
这两步代码是我们每次在进行存放元素时都必须要经历的两步,此时我们再次看栈,此刻栈中存放的元素就是1,那么1这个元素就是栈顶元素,top所指向的位置就是栈顶元素的下一个位置,由此我们不难想象,在每一次存放元素之后,top指向的位置永远都是栈顶元素的下一个位置。
b.top指向栈顶元素的当前位置:
如上图所示,当top初始化为-1时,top指向的位置不在数组内,top代表的位置就是数组起点的第一个位置,此时要想向数组中存放元素1,那么就得执行一下两行代码:
ps->top++;
ps->_a[ps->top];
这两步代码是我们每次在进行存放元素时都必须要经历的两步,此时我们再次看栈,此刻栈中存放的元素就是1,那么1这个元素就是栈顶元素,top所指向的位置就是栈顶元素的当前位置,由此我们不难想象,在每一次存放元素之后,top指向的位置永远都是栈顶元素的当前位置。
这两种初始化方式其实都可以,如何使用看情况,在这里,我使用的是ps->top==0这种初始化方式。
(3).栈的销毁:
// 销毁栈
void StackDestroy(Stack* ps)
{
assert(ps);
free(ps->_a);
ps->_a = NULL;
ps->_capacity = 0;
ps->_top = 0;
}
在这个销毁这里,我们首先要知道要将数组中的每一个元素都要销毁,因此,要访问到数组中的每一个元素,并逐一去销毁。
(4).入栈:
入栈其实就是向数组中去存放元素,在实现这一步骤的时候,我们需要注意的是在每次存放元素的时候需要先判断看一下数组中的空间够不够,如果不够就需要去申请新的数组空间,基于这种原因,我们每次在申请空间的时候都建议使用realloc动态内存开辟函数去将原来的那快的空间进行扩张操作,如果ps->_a指向NULL时;这种情况的话,realooc函数的功能就相当于时开创一块动态空间,因此大家在这里可以直接使用realloc函数。
// 入栈
void StackPush(Stack* ps, STDataType data)
{
assert(ps);
if (ps->_capacity == ps->_top)
{
int _newcapacity = ps->_capacity == 0 ? 4 : ps->_capacity * 2;
STDataType* tmp =(STDataType*)realloc(ps->_a,sizeof(STDataType) * _newcapacity);
if (tmp == NULL)
{
perror("realloc fail");
return;
}
ps->_a = tmp;//realloc函数将创建好后的数组空间的新地址传给了tmp指针,因此_a指针需要指向tmp指针指向的数组地址。
ps->_capacity = _newcapacity;//_capacity为数组中的容量,扩充数组,容量当然也随之扩大。
}
ps->_a[ps->_top] = data;//给数组当前位置赋值
ps->_top++;//top指向的是栈顶元素的下一个位置,因此要++
}
(5).出栈:
由于栈是后进先出的结构,因此,出栈也就是删除栈中的最后一个元素,那这样的话,我们让它存在在数组中,但是我们不故意去的打印它,不就好了吗,所以,在这里,我们只需要将_top指向的位置往前面移动一个元素的距离就可以了,也就是_top--。
// 出栈
void StackPop(Stack* ps)
{
assert(ps);
assert(ps->_top > 0);//要想进行出栈操作,就必须要保证栈中有元素,也就是_top必须大于1,因此,这里最好加上一个assert断言。
ps->_top--;
}
(6).获取栈顶元素:
由于栈是后进先出的结构,因此,栈顶元素就是最后一个进入的元素,也就是说,获取栈顶元素就是获得最后那个进入栈中的元素,因此,我们只需要将其直接返回就可以了。
// 获取栈顶元素
STDataType StackTop(Stack* ps)
{
assert(ps);
assert(ps->_top > 0);//要想进行获取栈顶元素的操作,就必须要保证栈中有元素,也就是_top必须大于1,因此,这里最好加上一个assert断言。
return ps->_a[ps->_top-1];
}
(7).获取栈中有效元素个数:
通过上述讲解,我们知道了_top指向的是栈顶元素的下一个位置,大家可以通过我们自己的聪明的大脑想一想,_top的值是不是就是栈中有效的元素个数,没错,是的,因为_top他指向的是栈顶元素的下一个位置,并且数组中的第一个元素的下标是由0开始的,如果说到这里大家还是不太了解的话,请看下面的这幅图,大家也可以自己尝试的去画一画图,画完之后应该就理解了。
// 获取栈中有效元素个数
int StackSize(Stack* ps)
{
assert(ps);
//在这里我们大家不需要去判断栈中是否含有元素,因为如果栈中没有元素的话,那么,返回0就行了。因为它这里是需要返回栈中的元素个数。
return ps->_top;
}
(8).检测栈是否为空:
检测栈是否为空,换句话说,也就是判断一下栈中是否含有有效元素就行,如果有,则说明部位空,否则,为空。
// 检测栈是否为空,如果为空返回非零结果,如果不为空返回0
int StackEmpty(Stack* ps)
{
assert(ps);
if (ps->_top == 0)
{
return 1;
}
else
{
return 0;
}
}
OK,同志们,到了这里,我们的栈也就结束了,接下来我们来讲解一下队列。
二.队列:
1.介绍:
它也是一种特殊的线性表,他只允许在一端进行插入数据操作,在另一端进行删除数据的操作,因此,队列具有先进先出的特点,在我们平时的使用过程中,它比较偏向于去做一些公平的程序
(画画能力不是很好,请见谅)。除此之外,再给大家介绍两个有关队列方面知识的编程标准术语,
出队列:进行删除操作的一端被称为对头;入队列:进行插入操作的一端被称为对尾。
2.队列的实现
在这里,我们使用单链表来实现队列结构(讲到这里,相信应该有一些小伙伴会在这里产生疑问,我来给大家解释一下吧,之所以使用单链表,其实一大部分原因都是基于队列的特点:先进先出,在这里如果我们使用数组去实现的话,那么在解决出队列的问题上就会非常的麻烦,要么是让头指针++,让其指向下一个位置,但是这样不好控制队列中的元素个数,要么让后面的所有元素统一往前面移动一位,将第一个元素覆盖掉,这样的话,还要将尾指针向前面移动一位,是的时间大打折扣,但是如果使用单链表的话在这里执行这些步骤的话会更加方便,因此,基于多种因素的考虑,最终决定在这里使用单链表来实现)。
对了,这里还需要注意一个地方,由于我们定义的是单链表,而且入队和出队它们大都是在对头和队尾进行操作,因此,我们需要两个指针将它们分别放在队头(头指针)和队尾(尾指针),这样方便我们继续后面进行的一系列操作。
(1).队列的定义(定义两个结构体):
typedef int QDataType;
// 链式结构:表示队列
typedef struct QListNode // }
{ // }
struct QListNode* _next; // } 由于我们在这里使用的是单链表去实现队列,因此我们
QDataType _data; // } 定义一个节点结构体。
}QNode; // }
// 队列的结构
typedef struct Queue // }
{ // }
QNode* _front; // } 这里为什么要定义一个结构体的原因在栈中有讲到。
QNode* _rear; // }
int _size; // }
}Queue; // }
(2).初始化队列:
// 初始化队列
void QueueInit(Queue* q)
{
q->_rear = NULL;//指针一般初始化为NULL
q->_front = NULL;
q->_size = 0;//_size为单链表中的有效元素个数,初始化为0
}
说到这里,大家肯定都或多或少有一定的疑问,就是为什么不给节点初始化呢?这是因为节点此时还没有被申请出来,也就是说,现在还没有节点,因此,不初始化。
(3).队尾入队列:
队尾入队列实际上就是向单链表中压入数据,每次在押入数据时都要使用malloc函数去向内存中申请一块空间作为节点并且存放数据。
// 队尾入队列
void QueuePush(Queue* q, QDataType data)
{
assert(q);
QNode* newQNode = (QNode*)malloc(sizeof(QNode));
if (newQNode == NULL)
{
perror("malloc fail");
return 1;
}
newQNode->_next = NULL;//对节点结构体中的数据进行初始化操作
newQNode->_data = data;//将data这个数据存入节点
// -----------------------------------------------------------------------------------------------
if (q->_rear == NULL)
{
q->_front = q->_rear = newQNode;
}
else
{
q->_rear->_next= newQNode;
q->_rear = newQNode;
}
// -----------------------------------------------------------------------------------------------
//以上我专门使用虚线画出来的这部分是在判断尾指针是不是指向的是NULL,因为指向NULL和 不指向NULL是两种情况,要分情况考虑,如果你在这里不判断尾指针是不是指向的是NULL, 那么我们在执行下面的新的指向操作时(也就是将新的节点插入到单链表中),这一步要进行 对_rear指针的解引用操作,如果尾指针在这里指向的是NULL,那么则无法对其进行解引用操 作,否则,系统会报错。
q->_size++;//多插入一个节点,有效元素数量++
}
(4).队头出队列:
此操作实际上就是单链表的头删操作,也会是说,就是将单链表的第一块空间将其释放就好了,头指针指向第二块空间就可以了。
// 队头出队列
void QueuePop(Queue* q)
{
assert(q);
assert(q->_size!=0);
// ----------------------------------------------------------------------------------------------------------------------
if (q->_front->_next == NULL) // }
{ // }
free(q->_front); // } 这里我们必须要分析一下单链表中现在有
q->_front = q->_rear = NULL; // } 几个节点,如果此时的单链表中只有一个
} // } 节点,那么此时对这个节点进行释放操作的
else // } 话,头指针和尾指针就会成为空指针,就要
{ // } 让它们两个都指向NULL,如果单链表中此
QNode* cur = q->_front->_next; // } 时有多个节点的话,那么尾指针不动,只是
free(q->_front); // } 头指针++,向后面移动一位,就可以了,不
q->_front = cur; // } 同情况下的操作方法时不一样的,因此需要
} // } 判断一下。
// ----------------------------------------------------------------------------------------------------------------------
q->_size--;//少一个元素,_size--
}
(5).获取队列头部元素:
这一步操作实际上就是得到单链表中的第一个元素,因此,我们在这里需要找到单链表中第一个节点,然后就可以得到其中的元素了(注意,这里只是得到第一个节点的元素,并不是删除)。
// 获取队列头部元素
QDataType QueueFront(Queue* q)
{
assert(q);
assert(q->_front);
return q->_front->_data;//得到了第一个节点的元素,直接将它返回就好了。
}
(6).获取队列队尾元素 :
这一步操作实际上就是得到单链表中的最后一个元素,因此,我们在这里需要找到单链表中最后一个节点,然后就可以得到其中的元素了(注意,这里只是得到最后一个节点的元素,并不是删除)。
// 获取队列队尾元素
QDataType QueueBack(Queue* q)
{
assert(q);
assert(q->_rear);
return q->_rear->_data;//得到了最后一个节点的元素,直接将它返回就好了。
}
(7).获取队列中有效元素个数:
这一步实际上就是获得单链表中的有效元素个数。
// 获取队列中有效元素个数
int QueueSize(Queue* q)
{
assert(q);
return q->_size;//由上面的结构体可知,size中的数据就是队列中有效元素的个数,因此,直接返回就可以了。
}
(8).判断队列是否为空:
判断队列是否为空,就是判断队列中是否含有元素,也就是头指针和尾指针是否同时都指向NULL。
// 检测队列是否为空,如果为空返回非零结果,如果非空返回0
int QueueEmpty(Queue* q)
{
assert(q);
return q->_front == NULL && q->_rear == NULL;//若符合条件,为1,返回1,否则,为0,返回0。
}