栈的实现
栈的结构并不复杂,但是问题来了,我们应该怎样实现栈了,到底是用顺序表还是链表了,哪个更好了?
顺序表:入栈更加方便,直接在size的位置插入即可,出栈也方便,直接出对应的下标就行
但如果你用单链表就没不是这样了,首先你每次入栈的时候都必须要找到尾节点;出栈的时候出的是尾节点,但此时你还必须记录尾节点的前一个节点,因为如果你要继续出栈的话,就要把栈顶的元素删除,此时你为了保证链表的结构,必须让尾节点的前一个节点指向空
下面来看看栈的代码实现,我分别用顺序表和单链表各实现了一下,大家可以比较一下优劣。
顺序表实现栈
头文件以及测试部分代码
stack.h
#define _CRT_SECURE_NO_WARNINGS 1
#include<stdio.h>
#include<assert.h>
#include<string.h>
#include<stdlib.h>
#include<stdbool.h>
//顺序表实现栈
typedef int SDataType;
typedef struct Stack
{
SDataType* a;
int sz;
int cap;
}Stack;
//将顺序表初始化
void Stackinit(Stack* cc);
//栈的插入
void StackPush(Stack* cc, SDataType x);
//栈的删除
void StackPop(Stack* cc);
//栈顶取出数据
SDataType Stacktake(Stack* cc);
//栈里面元素的打印
void StackPrint(Stack* cc);
//记录栈里面元素的个数
int StackSize(Stack* cc);
//判断是否为空
bool StackEmpty(Stack* cc);
//栈的销毁
void StackDestory(Stack* cc);
test.c
#define _CRT_SECURE_NO_WARNINGS 1
#include"stack.h"
//顺序表实现栈
void test()
{
Stack c;
Stackinit(&c);
StackPush(&c, 1);
StackPush(&c, 2);
StackPush(&c, 3);
StackPush(&c, 4);
StackPush(&c, 5);
printf("%d\n", Stacktake(&c));
printf("%d\n", Stacktake(&c));
StackPrint(&c);
StackPop(&c);
StackPop(&c);
StackPop(&c);
//StackPop(&c);
StackPrint(&c);
printf("%d\n", StackSize(&c));
printf("%d\n", StackEmpty(&c));
}
int main()
{
test();
return 0;
}
各个接口的实现
1.初始化
void Stackinit(Stack* cc)
{
assert(cc);
cc->cap = 4;
SDataType* new = (SDataType*)malloc(sizeof(SDataType)*cc->cap);
if (new == NULL)
{
printf("malloc fail\n");
exit(-1);
}
else
{
cc->a = new;
}
cc->sz = 0;
}
2.栈的插入
void StackPush(Stack* cc, SDataType x)
{
assert(cc);
if (cc->sz == cc->cap)
{
cc->cap *= 2;
cc->a = BuySList(cc);
cc->a[cc->sz] = x;
cc->sz++;
}
else
{
cc->a[cc->sz] = x;
cc->sz++;
}
}
3.栈的删除
void StackPop(Stack* cc)
{
assert(cc);
assert(!StackEmpty(cc));
//任何要删除的地方都要这个断言,因为如果顺序表为空就不能删除了
cc->sz--;
}
4.栈顶取出数据
SDataType Stacktake(Stack* cc)
{
assert(cc);
assert(!StackEmpty(cc));
return cc->a[cc->sz - 1];
}
5.栈里面元素的打印
void StackPrint(Stack* cc)
{
assert(cc);
int i = 0;
for (i = 0; i < cc->sz; i++)
{
printf("%d ", cc->a[i]);
}
printf("\n");
}
6.记录栈中元素个数
int StackSize(Stack* cc)
{
assert(cc);
return cc->sz;
}
7.判空
bool StackEmpty(Stack* cc)
{
return cc->sz == 0;
}
8.栈的销毁
void StackDestory(Stack* cc)
{
assert(cc);
//因为顺序表空间是连续的,所以可以这样释放
free(cc->a);
cc->a = NULL;
cc->sz = cc->cap = 0;
}
单链表实现
头文件以及测试部分代码
stack.h
#include<string.h>
#include<stdlib.h>
#include<stdbool.h>
//单链表实现栈
typedef int stackDataType;
typedef struct stack
{
struct stack* next;
stackDataType data;
}stack;
//栈的插入
void stackPush(stack** cc,stackDataType x);
//栈里面元素的打印
void stackPrint(stack* cc);
//栈里面删除栈顶元素
void stackPop(stack** cc);
//取出栈顶的数据
stackDataType stacktake(stack** cc);
//判断栈是否为空
bool stackEmpty(stack* cc);
//记录栈中元素的个数
int stackSize(stack* cc);
//销毁
void stackDestory(stack** cc);
test.c
#define _CRT_SECURE_NO_WARNINGS 1
#include"stack.h"
void test()
{
stack* c=NULL;
stackPush(&c,1);
stackPush(&c,2);
stackPush(&c,3);
stackPop(&c);
printf("%d\n", stacktake(&c));
stackPrint(c);
printf("%d\n", stackSize(c));
stackDestory(&c);
stackPrint(&c);
}
int main()
{
test();
return 0;
}
各个接口的实现
1.栈的插入
void stackPush(stack** cc,stackDataType x)
{
assert(cc);
//一个节点也没有的情况
if ((*cc) == NULL)
{
(*cc) = BuyNode(x);
(*cc)->next = NULL;
}
//有节点的情况,注意由于栈是后进先出的,所以头插比较方便,拿出来时
//的时间复杂度为O(1),如果尾插复杂度为O(n)
else
{
stack* newnode = BuyNode(x);
newnode->next = (*cc);
(*cc) = newnode;
}
}
2.栈里面元素的打印
void stackPrint(stack* cc)
{
while (cc)
{
printf("%d ", cc->data);
cc = cc->next;
}
printf("\n");
}
3.删除栈顶的节点
void stackPop(stack** cc)
{
assert(cc);
assert(*cc);
stack* tailpre= *cc;
while (tailpre->next->next)
{
tailpre = tailpre->next;
}
free(tailpre->next);
tailpre->next = NULL;
}
4.取出栈顶的数据
stackDataType stacktake(stack** cc)
{
assert(cc);
assert(*cc);
return (*cc)->data;
}
5.判空
bool stackEmpty(stack* cc)
{
return cc == NULL;
}
6.记录栈中元素个数
int stackSize(stack* cc)
{
int n = 0;
while (cc)
{
n++;
cc = cc->next;
}
return n;
}
7.销毁
void stackDestory(stack** cc)
{
assert(cc);
while (*cc)
{
struct stack* next = (*cc)->next;
free(*cc);
*cc = next;
}
}
队列
队列的结构也并不复杂,和栈很像,栈是后进先出,而队列是先进先出,那么问题又来了,队列的实现使用顺序表好还是链表好了?
单链表:插入的话,每次要在尾节点后插入,需要遍历找尾,但是如果出队列的话,只要出头节点就可以了,然后将头节点的下一个节点变为头就可以了。
顺序表:插入的话很简单,每次只要在size为下标的位置插入即可,但如果出队列的话,只能出下标为0的位置,此时出了之后还需要把后面的元素往前移,也并不简单
我们发现其实这两者并没有哪个更优的说法,并不像实现栈的时候有明显的差距,那有没有其他方法可以提高效率了?
肯定是有的:我们发现单链表效率地的原因是它每次都要找尾,那我们能不能不找尾了,我用一个指针来记录尾的位置不就行了吗,前提是这个指针要一直都在,所以我们一开始就将它定义为结构体成员,这种方法叫做结构体包裹。
下面同样,我用两中方法都实现了一下,大家可以对比一下优劣。
单链表(没有用结构体包裹)来实现
头文件以及测试部分
Queue.h
#define _CRT_SECURE_NO_WARNINGS 1
#include<stdio.h>
#include<assert.h>
#include<stdbool.h>
#include<stdlib.h>
typedef int QueueDataType;
typedef struct Queue
{
struct Queue* next;
QueueDataType x;
}Queue;
//因为我是用单链表实现的队列,插入就相当于初始化了,所以没必要去初始化
void QueuePush(Queue** cc,QueueDataType x);
//取出数据,因为队列是先进先出,所以应该取队列顶的数据,也就是头节点
QueueDataType Queuetake(Queue** cc);
//删除,删除取出来的数据,也就是队列头部的数据,也就是头节点
void QueuePop(Queue** cc);
//判断是否为空
bool QueueEmpty(Queue* cc);
//记录队列的节点个数
int QueueSize(Queue* cc);
//打印队列中的节点
void QueuePrint(Queue* cc);
test.c
#define _CRT_SECURE_NO_WARNINGS 1
#include"Queue.h"
void test1()
{
Queue *c=NULL;
QueuePush(&c, 1);
QueuePush(&c, 2);
QueuePush(&c, 3);
QueuePush(&c, 4);
printf("%d\n", Queuetake(&c));
QueuePop(&c);
printf("%d\n", Queuetake(&c));
QueuePop(&c);
QueuePop(&c);
QueuePop(&c);
QueuePop(&c);
QueuePrint(c);
}
int main()
{
*test1();
return 0;
}
各个接口的实现
1.队列的插入
//插入
void QueuePush(Queue** cc, QueueDataType x)
{
assert(cc);
//当没有节点的情况
if (*cc == NULL)
{
Queue* newnode = BuyNode(x);
newnode->next = NULL;
*cc = newnode;
}
//当有节点的情况
else
{
Queue* newnode = BuyNode(x);
//找尾
Queue* tail = (*cc);
while (tail->next)
{
tail = tail->next;
}
tail->next = newnode;
newnode->next = NULL;
//其实这一步写了等于没写,因为你下一次调用这个接口的时候,你tail又被赋值为*cc了
tail = newnode;
}
}
2.出队列
//取出数据,因为队列是先进先出,所以应该取栈顶的数据,也就是头节点
QueueDataType Queuetake(Queue** cc)
{
assert(cc);
assert(*cc); //这句话和52行起到的是相同的效果
assert(!QueueEmpty(*cc));
return (*cc)->x;
}
3.删除
//删除,删除取出来的数据,也就是队列头部的数据,也就是头节点
void QueuePop(Queue** cc)
{
assert(cc);
assert(!QueueEmpty(*cc)); //这句话和下一行起到的是相同的效果
assert(*cc);
Queue* newnode = (*cc)->next;
(*cc) = newnode;
}
4.判空
//判断是否为空
bool QueueEmpty(Queue* cc)
{
return cc == NULL;
}
5.记录节点个数
//记录队列的节点个数
int QueueSize(Queue* cc)
{
int n = 0;
while (cc)
{
n++;
cc = cc->next;
}
return n;
}
6.打印队列中的节点
void QueuePrint(Queue* cc)
{
while (cc)
{
printf("%d ", cc->x);
cc = cc->next;
}
printf("\n");
}
7.销毁
//销毁
void QueueDestory(Queue** cc)
{
assert(cc);
while (*cc)
{
Queue* next = (*cc)->next;
free(*cc);
*cc = next;
}
}
单链表实现(结构体包裹的方式)
头文件以及测试部分
Queue.h
#define _CRT_SECURE_NO_WARNINGS 1
#include<stdio.h>
#include<assert.h>
#include<stdbool.h>
#include<stdlib.h>
typedef int QueueDataType;
typedef int SListDataType;
typedef struct SList
{
struct SList* next;
SListDataType x;
}SList;
typedef struct Queue
{
SList* head;
SList* tail;
}Queue;
//这种结构体包裹的写法是要初始化的,因为你队列里面包含了两个指针
void Queueinit(Queue* cc);
void QueuePush(Queue** cc, QueueDataType x);
//取出数据,因为队列是先进先出,所以应该取队列顶的数据,也就是头节点
QueueDataType Queuetake(Queue** cc);
//删除,删除取出来的数据,也就是队列头部的数据,也就是头节点
void QueuePop(Queue** cc);
//判断是否为空
bool QueueEmpty(Queue* cc);
//记录队列的节点个数
int QueueSize(Queue* cc);
//打印队列中的节点
void QueuePrint(Queue* cc);
//销毁
void QueueDestory(Queue* cc);
test.c
#define _CRT_SECURE_NO_WARNINGS 1
#include"Queue.h"
void test2()
{
Queue c;
Queueinit(&c);
QueuePush(&c, 1);
QueuePush(&c, 2);
QueuePush(&c, 3);
QueuePush(&c, 4);
printf("%d\n", Queuetake(&c));
QueuePop(&c);
printf("%d\n", Queuetake(&c));
QueuePop(&c);
QueuePop(&c);
QueuePop(&c);
//QueuePop(&c);
QueuePrint(&c);
}
int main()
{
test2();
return 0;
}
各个接口的实现
1.初始化
//这种结构体包裹的写法是要初始化的,因为你队列里面包含了两个指针
void Queueinit(Queue* cc)
{
assert(cc);
cc->head = cc->tail = NULL;
}
2.入队列
void QueuePush(Queue* cc, QueueDataType x)
{
assert(cc);
//没有节点的情况
if (cc->tail == NULL)
{
SList* newnode = BuyNode(x);
newnode->next = NULL;
cc->head = cc->tail = newnode;
}
//有节点的情况
else
{
//这就是结构体包裹这种写法的好处,这样你就不需要找尾节点了
//因为我一开始定义队列的时候,就定义了一个指向尾的指针,这样
//的想法可以大大提高效率
SList* newnode = BuyNode(x);
cc->tail->next = newnode;
newnode->next = NULL;
cc->tail = newnode;
}
}
3.判空
bool QueueEmpty(Queue* cc)
{
assert(cc);
return cc->head == NULL;
}
4.出队列
QueueDataType Queuetake(Queue* cc)
{
assert(cc);
assert(!QueueEmpty(cc));
return cc->head->x;
}
5.删除队列头部节点
void QueuePop(Queue* cc)
{
assert(cc);
assert(!QueueEmpty(cc));
SList* newhead = cc->head->next;
cc->head = newhead;
}
6.记录队列节点个数
int QueueSize(Queue* cc)
{
assert(cc);
int n = 0;
while (cc->tail)
{
n++;
cc->tail = cc->tail->next;
}
return n;
}
7.打印队列元素
void QueuePrint(Queue* cc)
{
assert(cc);
SList* cur = cc->head;
while (cur)
{
printf("%d ", cur->x);
cur=cur->next;
}
printf("\n");
}
8.销毁
void QueueDestory(Queue* cc)
{
assert(cc);
while (cc->head)
{
SList* next = cc->head->next;
free(cc->head);
cc->head = next;
}
cc->tail = NULL;
}