C语言实现链表、堆栈和队列

线性结构

所谓线性结构,用偏数学的观点来看,其内部数据的关系构成一个有序集合,且这个有序集合中不存在自反性。

如果将这种数学表达实例化,可以表述为在这个集合中,所有的元素之间能够与整数的连续子集建立一一对应的关系,这种情况下即可称之为线性数据结构。

例如,数组是一种典型的线性结构,对于一个长度为 n n n的数组,其下标便和 [ 0 , n − 1 ] [0,n-1] [0,n1]构成了一一映射。树则显然不是一种线性结构,就树结构本身而言,对于父节点 S S S来说,其左节点 L L L和右节点 R R R并不能构成统一的偏序关系,除非在树这个结构上添加一个类似最大堆的预先约定。

按照这种观点,线性结构的顺序关系至少有两种方式可以实现,其一是数组实现,通过下标与整数的子集形成一一对应;其二则是链表实现,通过指向后一个节点的指针来规定节点间的偏序关系。就C语言的特性来说,其固有的数组必须声明长度,然而指针却十分宽松,也就是说对于一个新的数据结构,我们可以从数组和指针两个思路出发。前者即为静态数组,后者则为动态链表,二者结合在一起就是动态数组。

链表

单向链表

所谓链表就是每个节点除了值之外附带一个链接的表,一般这个连接指向下一个节点,从而形成一个有序队列。
在这里插入图片描述

如图所示,其中next指向下一个节点的指针,value为当前节点的值,最后一个节点的next值为NULL。在这个链表中,只有第一个节点的指针是暴露给我们的入口,其他节点的指针和值都需要通过第一个节点来查找。

这样的一个节点过指针实现为

//ADT.c
typedef struct NODE{
    struct NODE *next;
    int value;
}Node;

有了第一个节点,我们就可以找到第二个,依次类推,如果想找到第n个节点,也只需循环n次即可

//打印第n个节点
void printNodeN(Node *preNode,int n){
    for (int i = 0; i < n+1; i++)
        preNode = preNode->next;
    printf("the %dth node is",n,preNode->value);   
}

不过目前我们只有一个节点,如果希望建立一个新的链表,则需要在现有链表中添加一个元素。由于链表的最后一个节点不指向任何节点,所以我们可以将其设为NULL,这样以来,只需对现有链表进行循环,nextNULL时,将新节点接入即可。

void addNode(Node *preNode, int val){
    while (preNode->next != NULL)
        preNode = preNode->next;
    //建立新节点,开辟新的内存空间
    Node *newNode = (Node*)malloc(sizeof(Node));
    newNode->value = val;
    newNode->next = preNode->next;  //此值为NULL
    preNode->next = newNode;
}

然后测试一下链表

//打印所有节点
void printNode(Node *preNode){
    int i = 0;
    while (preNode->next!=NULL){
        printf("the %dth node:%d\n",i,preNode->value);
        preNode = preNode->next;
        i++;
    }
    printf("the %dth node:%d\n",i,preNode->value);
}
int main(){
    Node *firstNode;
    firstNode->value=0;
    firstNode->next=NULL;
    
    for (int i = 1; i < 6; i++)
        addNode(firstNode,i);//添加节点
    printNode(firstNode);
    return 0;
}

测试

PS E:\Code\AlgC> gcc .\ADT.c
PS E:\Code\AlgC> .\a.exe    
the 0th node:0
the 1th node:1
the 2th node:2
the 3th node:3
the 4th node:4

有序链表

这里的有序指的是链表的位置按照其value的大小进行排序,对于有序链表来说,新插入的节点未必会在原有链表的末尾,所以我们需要实现在原有链表中插入新值的功能。首先,我们可以改造一下此前的addNode函数。

void addNode(Node *preNode, int val, int n){
    int i = 0;
    while (preNode->next != NULL && ++i<n)//先比较,再循环
        preNode = preNode->next;
    
    //建立新节点,开辟新的内存空间
    Node *newNode = (Node*)malloc(sizeof(Node));
    newNode->value = val;
    newNode->next = preNode->next;  //newNode指向preNode后一个元素
    preNode->next = newNode;        //preNode指向newNode
}

这个函数有一个十分显著的问题,即当n为0时,会将newNode插入到1的位置。所以,我们最好在程序的结尾再加一个判断,当传入的序号为0时,交换第0个和第1个节点的值,即在函数开始加上一个判断。

    if (n==0){
        int temp = val;
        val = preNode->value;
        preNode->value = temp;
    }

若是在有序列表中插入值,则循环过程中的循环条件变为while (preNode->next!=NULL && preNode->value<val),判断条件变为if(preNode->value>val)

void addLargerNode(Node *preNode, int val){
    if (preNode->value>val){
        int temp = val;
        val = preNode->value;
        preNode->value = temp;
    }
    
    while (preNode->next!=NULL && preNode->value<val)//先比较,再循环
        preNode = preNode->next;
    
    //建立新节点,开辟新的内存空间
    Node *newNode = (Node*)malloc(sizeof(Node));
    newNode->value = val;
    newNode->next = preNode->next;  //newNode指向preNode后一个元素
    preNode->next = newNode;        //preNode指向newNode
}

循环链表

单链表只能从头部进行访问,如果不小心在其他位置进入,则只能遍历此后的节点,而对此前的节点无能为力。若想解决这个问题,只需在节点中加入一个指向前面的指针即可。然而,由于这会改变现有节点的结构,从而影响后面许多其他数据结构的实现,所以从略。

如果不希望增加节点所占用的内存,则可以使得链表的末尾节点指针指向初始节点。只不过这样一来,就无法用空指针来判断循环终止条件,而是需要保存起始节点,判断当前节点的指针是否指向起始节点来进行循环终止判定。

所以,添加节点函数、打印节点函数和主函数改为

void addCircle(Node *preNode, int val){
    Node *firstNode = preNode;
    //当指针指向初始节点时,说明此节点为尾节点
    while (preNode->next != firstNode)
        preNode = preNode->next;
    Node *newNode = (Node*)malloc(sizeof(Node));
    newNode->value = val;
    newNode->next = preNode->next;
    preNode->next = newNode;
}
//打印所有节点
void printNode(Node *preNode){
    Node* firstNode = preNode;
    int i = 0;
    while (preNode->next!=firstNode){
        printf("the %dth node:%d\n",i,preNode->value);
        preNode = preNode->next;
        i++;
    }
    printf("the %dth node:%d\n",i,preNode->value);
}
int main(){
    Node *firstNode;
    firstNode->value=0;
    firstNode->next=firstNode; //此时只有一个指向自身的节点
    
    for (int i = 1; i < 5; i++)
        addCircle(firstNode,i);//添加节点
    printNode(firstNode);
    pop(firstNode);
    return 0;
}

测试结果为

PS E:\Code\AlgC> gcc .\ADT.c
PS E:\Code\AlgC> .\a.exe    
the 0th node:0
the 1th node:1
the 2th node:2
the 3th node:3
the 4th node:4

循环链表在使用的时候显得更加灵活,例如,若将尾节点设为入口节点,那么对于堆栈还有队列的实现来说是大有帮助的。此时,如果希望增加节点,则只需在入口节点后面增加即可。如果希望实现栈式弹出,则只需删除最后一个节点;而若希望实现队列式删除,则只需将指针移动到首节点,然后删除即可。

堆栈

数组实现

栈与数组的区别在于,后者是静态的,即有确定多个元素个数。栈和队列则为动态数组,前者实现一种先入后出的策略,后者则是先入先出。下图即栈的一种直观描述。

在这里插入图片描述

所以,除了数组的索引功能之外,栈还要求实现压入和弹出的功能,以实现数组的动态变化。对此,我们只需在数组之外,额外给出一个指针指向当前数组的末尾即可。

#include<stdio.h>
#define MAXSTACK 100

//无脑栈的实现
typedef struct Stack
{
    int data[MAXSTACK]; //栈最大长度
    int length;         //栈长度
}Stack;

void popStack(Stack *s){
    if(s->length==0)
        printf("overflow");     //栈已空
    else
        s->length--;
}

void pushStack(Stack *s,int data){
    if(s->length==MAXSTACK)
        printf("overflow");     //栈已满
    else{
        s->data[s->length]=data;
        s->length++;
    }
}

//初始化栈,输入为数组和数组长度
Stack initStack(int *arr, int n){
    Stack s;
    s.length = 0;
    for (int i = 0; i < n; i++)
        pushStack(&s,arr[i]);

    return s;
}

int main(){
    Stack s;
    int data[10] = {1,2,3,4,5,6,7,8,9,0};
    s = initStack(data,10);
        
    printf("%d\n",s.length);
    for (int i = 0; i < 10; i++){
        printf("s.data[%d]=%d\n",i,s.data[i]);
    }
    
    return 0;    
}

其输出结果为

E:\Code\AlgC>gcc test.c
E:\Code\AlgC>a
10
s.data[0]=1
s.data[1]=2
...//雷同省略
s.data[9]=0

实验性的面向对象

上述方式虽然能够实现压栈和弹出的操作,但对于熟悉面向对象编程的人来说显然是难以忍受的,即我们并没有为栈的方法进行良好的封装。这种封装在此前可能是无法想象的,好在gcc编译器已经足够现代,让我们可以在结构体中实现类似于成员方法的功能。

//包含成员函数的栈实现
#include<stdio.h>

typedef struct stack{
    int datas[10];
    int length;
    int (*push)(int data);      //成员方法push的函数指针
    int (*pop)();               //成员方法pop的函数指针
}stack;

//push创建函数,输出为push函数
int (*createPush(int *x, int *length))(int){
    int push(int y){
        x[*length]=y;
        *length=*length+1;
        return 1;
    }
    return *push;
}

//pop创建函数,输出为pop函数
int (*createPop(int *x, int *length))(int){
    int pop(int y){
        *length=*length-1;
        return 1;
    }
    return *pop;
}

stack Stack(int *arr, int length){
    stack s;
    s.length = 0;
    s.push = createPush(s.datas,&(s.length));
    for (int i = 0; i < length; i++)
        s.push(arr[i]);
    return s;
}

int main(){
    stack s;
    s.length = 0;
    s.push = createPush(s.datas,&(s.length));

    int arr[5] = {1,2,3,4,5};
    for (int i = 0; i < 5; i++)
        s.push(arr[i]);
    
    printf("pushed successfully\n");
    for (int i = 0; i < s.length; i++)
        printf("a[%d]=%d\n",i,s.datas[i]);
    
    s.pop = createPop(s.datas,&(s.length));
    s.pop();
    s.pop();


    printf("poped successfully\n");
    for (int i = 0; i < s.length; i++)
        printf("a[%d]=%d\n",i,s.datas[i]);
    
}

测试

E:\Code\AlgC>a
pushed successfully
a[0]=1
a[1]=2
a[2]=3
a[3]=4
a[4]=5
poped successfully
a[0]=1
a[1]=2
a[2]=3

当然,类似于int (*push)(int data);这种写法,看上去就十分危险,让人不寒而栗。在结构体中定义指针,使用时稍有不慎就会报错,在C语言中并不推荐这么做。而且就性能来说,也额外增添了内存的开销,是十分不划算的。

链式堆栈

通过数组实现栈,最终得到的不过是个伪栈,因为我们还是可以对数组进行访问。相比之下,单向链表只暴露给我们一个初始值,和栈只暴露给我们顶部的值十分相似,区别只是在于栈的暴露给我们的是最后压入的值而已。

所以,栈节点的指针需要指向前一个节点,在此前,我们曾经写过单向链表,其指针为next指向下一个节点,方便起见,我们仍用这个词表示下面的节点。

typedef struct NODE{
    struct NODE *next;
    int value;
}Node;

相应的pushpop以及打印栈的函数为

//删除栈顶
void pop(Node *top){
    if (top->next!=NULL)
        top->next = top->next->next;
    else
        printf("empty stack");//此时栈已空
}

//传入栈顶指针和新增值
void push(Node *top, int val){
    Node *new;
    new = (Node*)malloc(sizeof(Node));

    new->value = val;
    if (top->next==NULL)     //此时栈为空
        new->next = NULL;   //新增节点为栈底,故指向空处
    else
        new->next = top->next;  //新节点指向原来栈顶指向的节点
    top->next = new;        //栈顶指向新节点
}
//打印链式栈
void printStack(Node *top){
    int i = 0;
    while(top->next!=NULL){
        top = top->next;
        printf("%d\n",top->value);
    }
}

测试一下,主函数为

int main(){
    Node *topNode;      //声明栈顶
    topNode->next=NULL; //栈顶指向空处,此时为空栈

    //初始化节点
    for (int i = 1; i < 6; i++)
        push(topNode,i);
    printStack(topNode);

    pop(topNode);
    printf("popped\n");
    printStack(topNode);
    return 0;
}

输出的值为

PS E:\Code\AlgC> gcc .\ADT.c
PS E:\Code\AlgC> .\a.exe    
5
4
3
2
1
popped
4
3
2
1

队列

栈式队列

队列和堆栈的区别在于,后者是采取先入后出的操作原则,前者则是先入先出。就其结构来分析,即栈在删除元素时,只能从栈顶开始删除,而队列则从列首开始,是一种先入先出的模式。

如果希望通过链表的形式实现队列,则至少有两个不同的思路,即前向列表和后向列表。前向列表将初始节点暴露给我们,我们在插入新的值时需要遍历所有节点,但是在删除时比较方便,只需对初始节点执行类似pop的操作即可;后向链表则相当于堆栈,将栈顶暴露给我们,我们在插入新节点时只需push即可,但是删除节点时需要遍历到队首。

由于此前已经实现了链表和堆栈的代码,所以下面将只给出链式队列有关的函数和主函数。

对于"栈"式队列,我们只需加入一个删除队首的操作即可。

typedef struct NODE{
    struct NODE *next;
    int value;
}Node;
void push(Node *top, int val);
void printStack(Node *top);

//删除队首
void delFirst(Node *top){
    while (top->next->next!=NULL)
        top = top->next;
    free(top->next->next);
    top->next = NULL;
}

int main(){
    Node *topNode;
    topNode->next=NULL;

    
    for (int i = 1; i < 5; i++)
        push(topNode,i);
    printStack(topNode);

    delFirst(topNode);
    printf("popped\n");
    printStack(topNode);
    return 0;
}

输出结果为

PS E:\Code\AlgC> gcc .\ADT.c
PS E:\Code\AlgC> .\a.exe    
4
3
2
1
popped
4
3
2

即完成了从栈到队列的更改。

链式队列

对于"链"式队列,其修改方式大同小异,只需在单项列表实现函数中添加一个删除首节点的函数即可。甚至我们并不需要再写一个新的函数,只需把pop直接拿过来用即可。

当然,二者之间还有一个细小的差别,此前我们再定义堆栈的时候,默认栈顶节点的值为空,所以再建立pop函数的时候并没有关注其value的变化。那么在建立队列删除操作时,需要补上这一条

//删除队首
void pop(Node *top){
    if (top->next!=NULL){
        top->value = top->next->value;
        top->next = top->next->next;
    }
    else
        printf("empty stack");
}
int main(){
    Node *firstNode;
    firstNode->value=0;
    firstNode->next=NULL;
    
    for (int i = 1; i < 5; i++)
        addNode(firstNode,i);//添加节点
    printNode(firstNode);
    pop(firstNode);
    printf("popped\n");
    printNode(firstNode);
    return 0;
}

结果为

PS E:\Code\AlgC> gcc .\ADT.c
PS E:\Code\AlgC> .\a.exe    
the 0th node:0
the 1th node:1
the 2th node:2
the 3th node:3
the 4th node:4
popped
the 0th node:1
the 1th node:2
the 2th node:3
the 3th node:4
  • 9
    点赞
  • 25
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

微小冷

请我喝杯咖啡

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值