栈
栈种的数据会一个压着一个往上叠。先放上去的,就会被压在下面。因此是种“先入后出(FILO,Fist In Last Out)”的结构。
数组还是链表?
那么对栈的实现应该是用数组还是链表呢?
数组是双方向的,索引既可以++也可以--。链表是单方向的,索引只能够++。为了实现栈,那么就需要在插入时++,在读取时--,因此用数组去实现栈更为合适。但数组的内存往往是固定的,但栈不一定固定,因此还需要动态内存开辟realloc。
实际上,数组和链表都可以实现栈。这里只以数组为例。
栈的实现
栈是用数组来实现的,因此需要有个数组的头指针a。栈是后入先出,也就是说对栈的操作都是在栈头进行的,因此需要索引top记录栈这个数组的尾部(也就是栈的头部)。进行动态内存开辟需要对栈的空间进行判断,因此需要一个数记录栈的容量。
由于不能确定这个栈是用于存放int还是char等类型,因此需要通过typedef来对栈的数据类型进行宏定义。
栈需要完成的功能有:栈初始化、头插、头删、读取栈头数据、栈的销毁等。
typedef int STDataType;
typedef struct Stack
{
STDataType* a;
int top;
int capacity;
}ST;
void STInit(ST* pst);//栈初始化
void STPush(ST* pst, STDataType x);//头插
void STPop(ST* pst);//头删
STDataType STTop(ST* pst);//读取栈头数据
bool STEmpty(ST* pst);//检测栈是否为空
int STSize(ST* pst);//获取栈的尺寸
void STDestroy(ST* pst);//栈的销毁
现在逐一实现:
栈的初始化
栈是一个数组,那么在main中会先有个ST* stack;
来定义一个结构体。对栈的初始化就是对stack这个结构体中数据的初始化,其中stack.a才是实际的栈。stack是一个指针,指向所定义的栈。
void STInit(ST* pst)
{
assert(pst);//确保你在main中定义了ST* stack
pst->a=NULL;//栈内无空间
pst->top=0;//栈头为0
pst->capacity=0;//栈内空间为0
}
这里的top实际是栈中数据的数量,因此栈头的指针应该为top-1。栈头top-1会作为栈的数组a的索引,a[top-1]。
栈的头插
既然要插入数据,那么就要考虑的一个问题是,空间够不够用。因此在这个函数中,需要判断并扩容。那么如何判断?
我们有一个栈头索引top,还有栈的内存capacity,很明显,当top=capacity时,就说明栈满了需要扩容。
数组扩容使用的是realloc,来确保扩容的空间具有连续性。
void STPush(ST* pst,STDataType x)//main中定义的指向栈的指针,要插入的数据
{
assert(pst);
if(pst->top == pst->capacity)
{
int newCapacity = (pst->capacity==0)? 4:pst->capacity*2;
//使用三目操作符,如果空间为空,则扩容4个,如果不是,则扩容2倍
STDataType* tmp=(STDataType*)realloc(pst->a,newCapacity*sizeof(STDataType));
if(tmp==NULL)
{perror("realloc fail");return;}
pst->a=tmp;//将新空间传给栈
pst->capacity=newCapacity;
}
pst->a[pat->top]=x;//将数据插到栈头
pst->top++;//栈头索引++
}
栈的头删
栈的头删很简单,直接将top--就行,这样新插入数据时,会将原来的数覆盖。
但存在个问题,如果栈本身是空的,也就是top=0,那么再删除数据,就会造成越界。因此需要一个函数bool STEmpty(ST* pst)
来判断栈是否为空。
bool STEmpty(ST* pst)
{
assert(pst);
return pst->top==0;//如果为空,则返回true
}
void STPop(ST* pst)
{
assert(pst);
assert(!STEmpty(pst))//为空则直接断言
pst->top--;
}
读取栈头数据/栈中数据个数
就是直接读取a[top-1]和top。同样需要判断栈是否为空,毕竟空的不能读取。
STDataType STTop(ST* pst)
{
assert(pst);
assert(!STEmpty(pst));
return pst->a[pst->top-1];//注意这里的-1
}
int STSize(ST* pst)
{
assert(pst);
return pst->top;
}
栈的摧毁
这里注意不能直接free(stack),因为stack只是指向a的指针,真正的栈是stack.a。
void STDestroy(ST* pst)
{
assert(pst);
free(pst->a);
pst->a=NULL;
pst->top=pst->capacity=0;
}
队列
栈是竖着的,有底面,因此不能直接拿下面的,属于先入后出。
队列是横着的,通透的管道,排着队进排着队出,属于先入先出(FIFO,First In First Out)。
数组还是链表?
不同于栈只需要操作栈头,队列是需要操作头+尾。对如果用数组,那么删除head的数据时,就需要后面数据逐一前移,这样效率很低。
既然是链表,那么就需要对头和尾都有相应指针记录。
同样,队列需要能完成的任务有:队列初始化、读取队头/队尾数据、头删、尾插、队列销毁。
typedef int QDataType;
typedef struct QueueNode
{
struct QueueNode* next;
QDataType data;
}QNode;//定义链表,队列中每个数据都是在这里对应。
typedef struct Queue
{
QNode* phead;//单独记录队头,用于读取
QNode* ptail;//单独记录队尾,用于尾插
}Queue;
void QueueInit(Queue* pq);//队列初始化
void QueuePush(Queue* pq, QDataType x);//队列尾插
void QueuePop(Queue* pq);//队列头删
QDataType QueueFront(Queue* pq);//读取队头数据
QDataType QueueBack(Queue* pq);//读取队尾数据
int QueueSize(Queue* pq);//获取队列长度
bool QueueEmpty(Queue* pq);//判断队列是否为空
void QueueDestroy(Queue* pq);//队列销毁
队列的实现
队列初始化
在main函数中,使用Queue que;
来定义一个队列。que中已经具有对phead、ptail的初始化。
void QueueInit(Queue* pq)
{
assert(pq);
pq->phead=NULL;
pq->ptail=NULL;
}
队列尾插
栈开拓空间使用realloc来确保空间连续。队列使用的链表使用malloc就可以。
但是需要注意的一点是,如果是插入的第一个数据,那么phead和ptail都为NULL,必然不能只让ptail->next=newnode,同样需要处理下phead
void Queue(Queue* pq,QDataType x)
{
assert(pq);
QNode* newnode=(QNode*)malloc(sizeof(QNode));//创建队尾新节点
if(newnode==NULL)
{perror("malloc fail");return;}
newnode->data=x;//数据传给新节点
newnode->next=NULL;
if(pq->ptail==NULL)//如果队列为空
{
assert(pq->phead==NULL);
pq->phead = pq->ptail = newnode;
}
else
{
pq->ptail->next=newnode;//ptail->next指向新节点
pq->ptail->newnode;//将newnode设置为队尾
}
}
头删
删除就会面对一种情况就是,空队列是无法继续头删的。因此需要函数判断队列是否为空。
bool QueueEmpty(Queue* pq)
{
assert(pq);
return pq->phead==NULL;
}
然后面对的问题就是,如果只剩下一个结点,删除后,phead和ptail都需要=NULL。因此需要一个节点/多个节点分类讨论。
void QueuePop(Queue* pq)
{
assert(pq);
assert(!QueueEmpty(pq));
if(pq->head->next==NULL)//即只剩下一个结点
{
free(pq->head);
pq->phead=pq->ptail=NULL;
}
else//剩下多个结点
{
QNode* next = pq->phead->next;
free(pq->phead);
pq->phead=next;
}
}
获取队列长度
获取队列长度有两种方式,一种是在Queue结构体中添加一个size,即
typedef struct Queue
{
QNode* phead;//单独记录队头,用于读取
QNode* ptail;//单独记录队尾,用于尾插
int size;//用于记录队列长度
}Queue;
然后在其它函数,如头删、尾插时操作下size。但由于size用的并不多,所以也可以不定义size,而是通过遍历一次队列来获取size,如
int QueueSize(Queue* pq)
{
int cnt=0;
QNode* cur=pq->phead;
while(cur)
{
cur=cur->next;
cnt++;
}
return cnt;
}
据读取
读取队头/队尾数据时,要考虑到空队列无法读取。
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;
}
队列的销毁
队列的销毁要用到链表的销毁。不能像栈一样直接free。队列是链表,因此需要对每个元素free
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;
}