栈
一个全新的结构——栈。它的本质我认为就是一个存储结构。它的存储方式是总结起来就是四个字,“先进后出”。如果你不好理解何为“先进后出”,我们可以想像成我们往一个箱子里面塞入几乎等同于箱口大小的不同颜色木板,先塞入红木板,在塞入黄木板,再塞入蓝木板。这个时候你想取出红木板,你就必须先把蓝木板取出,再取出黄木板,最后才能取出红木板。因为你没办法越过这两个挡在上面的木板去取最下面的红木板。栈就是这么一种情况,你要把接近出口的数据移除出去才能取下面的数据。
就像这样子,下面是封口的,无法从下面出去。了解完它的结构含义,我们再来实现一下对这个结构进行插入移除等等操作。
定义
先想清楚,我们是要用数组还是链表去实现这个结构。我们来分析一下使用数组和使用链表都有哪些优缺点。
链表:先看看我们如果使用链表进行实现栈的插入和移出数据的操作,我们把头相像成栈底,插入还好说,也挺方便的。但是再进行移出操作的时候,你要删除的是尾部的数据,也就是尾插的时候,你得每次都去找尾结点,释放一次数据尾结点就找不到了,因为是使用单链表,你无法回溯上一个结点,所以每次都要遍历一遍,时间复杂度就会很高。
数组:而数组就没有这个烦恼,不管是插入还是移出都很容易实现,所以我使用了数组去进行实现栈。当然链表也行。
typedef int STDataType;
typedef struct Stack
{
STDataType* a;
int top; // 栈顶
int capacity; // 容量
}Stack;
这个typedef是为了如果以后需要存储其他类型数据可以直接在这更改数据类型,十分方便。
结构体中的定义也很好理解,先定义一个数组a,再定义一个栈顶方便操作,再设置一个容量,方便扩容,判定大小。
初始化
初始化就很简单了。
void StackInit(Stack* ps)
{
assert(ps);
ps->a = NULL;
ps->top = 0;
ps->capacity = 0;
}
这个没什么好说的,就正常的初始化。
入栈
思考一下我们入栈的流程。你要入栈,你首先得看一下这个栈是不是已经满了,满了你想进去的话是不是得扩容。所以我们首先先判断一下是否满,满就扩,扩完再入栈。没满就直接入栈。
入栈就也很简单了,正常数组的入数据就行了。
void StackPush(Stack* ps, STDataType data)
{
if (ps->top == ps->capacity)
{
int newcapacity = ps->capacity == 0 ? 4 : ps->capacity * 2;
STDataType* tmp = (STDataType*)realloc(ps->a, newcapacity * sizeof(STDataType));
if (tmp == NULL)
{
perror("realloc fail");
return;
}
ps->a = tmp;
ps->capacity = newcapacity;
}
ps->a[ps->top] = data;
ps->top++;
}
这里的扩容我采用一个三目操作符,先判定是不是满或者空,你可以用两个if去进行判断,但是有一个很巧妙的办法,可以用一个if就可以判断是否满或者空。就是我们的栈顶的下标是否等于我们的栈的容量。因为这两个数字相等的时候,要么就是一开始刚初始化的时候,要么就是已经满了。因为第一次的话,容量是为0的,这个时候走第一个,赋值为4。其中new是这个函数中使用的容量,另一个是整个程序的容量。扩容多少就之前的两倍就行,多少倍影响不大,只是2倍合理一点。后面就用realloc进行扩容。扩容几个就用new*sizeof就行,因为new就是我们要扩容的个数。乘以我们个体的大小,就是总共的大小了。 后面就把数据赋予栈顶,然后top++,就行了。
出栈
void StackPop(Stack* ps)
{
assert(ps);
assert(!(StackEmpty(ps)));
ps->top--;
}
bool StackEmpty(Stack* ps)
{
assert(ps);
return ps->top == 0;
}
首先你要出栈,你栈里面肯定不是空的,这里写一个判空的函数。这里看我的栈顶的下标是否为0即可。然后你要出栈更简单,直接栈顶下标--,访问不到就行了。
获取栈顶元素
STDataType StackTop(Stack* ps)
{
assert(ps);
assert(!(StackEmpty(ps)));
return ps->a[ps->top-1];
}
同样的操作道理,判空。这里需要注意的是,我的top始终都是栈顶的下一个下标,这个时候访问栈顶就直接top-1就行。这也是数组实现比链表实现方便的一个地方。至于我为什么要把top设置为栈顶的下一个元素,是因为这样更加方便判空,当然这个看你自己。
获取栈的大小
int StackSize(Stack* ps)
{
assert(ps);
return ps->top;
}
这个很简单,同时这个也是为什么我要把top设置成真正的栈顶的下一个位置。这个时候获取大小,直接把top返回去就行了。
销毁栈
void StackDestroy(Stack* ps)
{
assert(ps);
free(ps->a);
ps->top = 0;
ps->capacity = 0;
}
正常的销毁,先释放空间,再把top和容量设成0就行了。
队列
定义
队列和栈都是一种储存结构,它们的区别是栈是先进后出,而队列是“先进先出”。这个也好理解,可以想象成我们在饭堂排队。先排队的先打饭,后来的后面打饭。
栈用数组实现方便一点,那么队列呢。同样我们来分析一下:
数组:想象一下如果我们用数组来实现,进行出队列的时候,是不是会感觉非常的变扭。首先我们入队列肯定得从下标0开始吧,依次开始入数据,然后出数据的时候,注意这个时候就开始跟栈不一样了。我们出队列,是先进先出,也就是说我们得从下标为0的开始出数据,然后依次出,这个时候出完数据,这些空间你觉得还好重复利用吗?我认为是不好继续利用的,这样会让你的队列变得十分奇怪。
链表:而链表就不会有这种烦恼,出就直接出了,反正空间不是连续的,我可以随时用随时释放。这样的空间利用率会高很多,其次这里也没有栈的时候的烦恼,实现栈的时候是尾删,而实现队列的时候是头删,方便太多了。所以这里我采用的是用链表实现队列。
typedef int QDataType;
// 链式结构:表示队列
typedef struct QListNode
{
struct QListNode* next;
QDataType data;
}QNode;
// 队列的结构
typedef struct Queue
{
QNode* head;
QNode* tail;
int size;
}Queue;
注意它们两的关系,链表只是实现队列的一个手段。不宜将它们放在同一个结构体当中,不好实现。注意到我们队列是整体的结构,而链表是我们队列中一个个个体的结构。所以在队列的结构体中我们定义队头和队尾的时候,类型是链表的结构体名称。然后同样定义一个size方便一点。
初始化
老样子先初始化。
void QueueInit(Queue* q)
{
assert(q);
q->head = NULL;
q->tail = NULL;
q->size = 0;
}
一样的,指针判空,大小赋0。
入队列
跟栈不同,我们入队列不用考虑扩容这个问题,我们入一个数据就malloc一个空间就行。然后把数据赋给新空间,也就是data,再把next置空,因为我们是尾插,后面没有数据的。又因为我们是尾插,你得确定我们插入之前有没有数据,如果有的话,你需要把它们链接起来。然后记得把tail的位置改变。最后注意size++。
void QueuePush(Queue* q, QDataType data)
{
assert(q);
QNode* newnode = (QNode*)malloc(sizeof(QNode));
if (newnode == NULL)
{
perror("malloc fail\n");
return;
}
newnode->data = data;
newnode->next = NULL;
if (q->tail == NULL)
{
assert(q->head == NULL);
q->head = q->tail = newnode;
}
else
{
q->tail->next = newnode;
q->tail = newnode;
}
q->size++;
}
出队列
这里的出队列也就是头删的思想。
void QueuePop(Queue* q)
{
assert(q);
assert(!QueueEmpty(q));
//一个
//多个
if (q->head->next == NULL)
{
free(q->head);
q->head = q->tail = NULL;
}
else
{
QNode* cur = q->head;
q->head = q->head->next;
free(cur);
}
q->size--;
}
bool QueueEmpty(Queue* q)
{
assert(q);
return q->head == NULL && q->tail == NULL;
}
这里判空就是你头尾指针都指向空时,这个队列就是空的。注意size要--。
获取头元素
这个更加简单,头元素嘛,就是head指向的数据。
QDataType QueueFront(Queue* q)
{
assert(q);
assert(!QueueEmpty(q));
return q->head->data;
}
获取队尾元素
同理啊,就是tail指向的数据。
QDataType QueueBack(Queue* q)
{
assert(q);
assert(!QueueEmpty(q));
return q->tail->data;
}
获取有效元素个数
这里直接访问size的大小即可,之前定义size也是为了方便获取元素个数。
int QueueSize(Queue* q)
{
assert(q);
return q->size;
}
销毁队列
这个是链表进行销毁得一步步的进行,一个个空间进行释放,为防止释放空间时,找不到指针,我们再定义一个指针去进行释放,最后置空的置空,赋0的赋0。
void QueueDestroy(Queue* q)
{
assert(q);
QNode* cur = q->head;
while (cur)
{
QNode* next = cur->next;
free(cur);
cur = next;
}
q->head = NULL;
q->tail = NULL;
q->size = 0;
}
好了,栈和队列的实现就是这些了,如果你还有不清楚,或者我讲的不好的地方欢迎指出。