vlambda博客
学习文章列表

c语言 | 单链表的实现

今天分享的是单链表。准确的说,单链表不算是C语言中的内容,而是属于数据结构的内容,因为它没有新的知识点,只是利用了结构体和指针等的知识。但是它在C语言中应用还是很广泛的,在RTOS中,也是非常多的地方使用到了链表。今天暂时说一下单链表的实现和简单应用,下一节当中再介绍双链表。

首先,要对单链表有个概念。单链表其实是对数组的扩展,数组是为了存储很多个数据而产生的,但是它有两个缺陷,第一个缺陷就是数组里面所有的元素都是同样的类型,为了解决这个问题,产生了结构体。另一个缺陷就是数组的大小在初始后就决定了,并且在后面都不允许改变,为了解决这个问题,于是产生了链表。链表可以认为是长度可变的数组,灵活性比较大。链表是由一个个节点构成,每个节点之间用指针的方式连接起来,有一个头指针用来找到链表中的第一个节点,然后根据指针就可以找到每一个节点。

说明:在本次实验中,使用的是vscode编辑器,编译环境是gcc,不建议使用VC6.0,因为VC6.0使用的c语言标准太老了,很多语法都不支持,并且,VC6.0使用体验极差,没有代码高亮功能等等。所以,推荐使用vscode编辑器,也可以使用windows自带的编译器,打开cmd终端,使用gcc命令编译.c文件,生成.exe可执行文件后执行即可。不过,需要自己先去搭建编译环境,这个可以自行百度。

一、创建节点

struct  node
{

    int data;
    struct  node * pNext;
};

struct  node* creat_node(int data)
{
    struct  node*p =(struct  node*)malloc(sizeof(struct  node));
    if(p==NULL)
    {
        printf("创建节点失败\n");
        return NULL;
    }
    bzero(p,sizeof(struct  node));
    p->pNext=NULL;
    p->data=data;
    return p;
}

先定义一个结构体类型,里面有两个成员变量,第一个是要存放的数据,第二个是指向下一个节点的指针。

二、插入节点

创建好节点之后,这个节点还是孤立的节点,并没有连起来,所以我们可以采用插入节点的方法来扩展,同时将节点连起来。节点的插入可以是从头部插入,也可以是从尾部插入,一般不会从中间插入,没有意义,因为插入主要是扩展链表的,没有理由从中间插入。

1、尾部插入

void insert_tail(struct  node*pH,struct  node*new)
{
    struct  node*p=pH;
    while(p->pNext!=NULL)
    {
        p=p->pNext;
    }
    p->pNext=new;
    pH->data+=1;
}

尾部插入其实并不难,只要将原来最后的节点连上新插入的节点即可。

2、头部插入

void insert_head(struct  node*pH,struct  node*new)
{
    struct  node*p;
    p=pH;
    new->pNext=p->pNext;
    p->pNext=new;
    pH->data+=1;
}

头部插入就多一个步骤,首先应该将新节点连上原来的第一个节点,然后让原来的头指针指向新的节点。这个顺序是不能颠倒的。如果你先将头指针与新节点相连,那么原来的第一个节点就被丢掉了,你的新节点就没办法去连接原来的第一个节点了。

三、遍历

通过前面插入节点,已经可以形成一条链表了。那么,如果我想访问这个链表里面每个节点的数据,那么该怎么办呢?这个时候就需要遍历。

void traverse(struct  node*pH)  //遍历
{
    struct  node*p;
    p=pH;
    printf("开始遍历\n");
    while(p->pNext!=NULL)
    {

        printf("%d\n",p->pNext->data);
        p=p->pNext;

    }
    printf("遍历结束\n");
}

遍历就是使用while循环来判断是否找到最后一个节点,在找到最后一个节点之前,依次取出每个节点里的数据,每次循环后,指针p都指向下一个节点。这里有一个小问题就是,如果你取出的是当前指针指向的那个节点的数据,那么当p指向最后一个节点的时候,就会退出while循环,导致丢失了最后一个数据。为了解决这个问题,有两种方法,第一种方法是退出while循环之后再单独处理一下最后这个节点的数据,这种方法可行,但是影响程序的优美,让人觉得是一种补丁,另一种方法就是我总是取当前指针的下一个节点的数据,这样在倒数第二个节点的时候,就已经把最后一个节点的数据取出来了。但是这样又会有一个新问题,就是第一个数据不是取不出了吗?不用担心,我们通常让第一个节点成为“头节点”,不是用户的有效节点,这个头节点可以用来存放这个链表中一共有几个数据,所以,这个头节点还是挺有用的,但是是非必要的。

四、删除节点

前面有插入节点,自然就应该有删除节点。删除节点是会比较麻烦一点,而且根据用户不同的需求,也是不一样的,这里我们只考虑删除指定的数据的节点,可以只删除一个,也可以删除所有。比如希望把链表中值为5的节点全部删除等等。

void delete_node(struct  node*pH,int data,int option)   //删除指定数据的节点
{
    struct  node*p=pH;
    struct  node*pBack=p;
    int flag=0;
    if(p->pNext==NULL)
    {
        printf("没有可以删除的有效节点");
    }
    else
    {
        pBack=pBack->pNext;
        switch (option)
        {
        case SINGLE_DELETE:
        while(p->pNext!=NULL)
        {


            while (p->pNext->data==data)  //找到第一个数据
            {
                flag=1;
                 if(p->pNext->pNext==NULL)  //该节点是尾节点
                 {
                     p->pNext=NULL;  

                 }
                 else
                 {
                    p->pNext=p->pNext->pNext;    //重新连线

                 } 
                free(pBack);            
                pH->data-=1;
                break;

            }
            p=pBack;
            if(pBack->pNext!=NULL)
            {
                  pBack=pBack->pNext;
            }
            else
            {
                if(flag==0)
                {
                    printf("不存在 这样的节点\n");
                }
            }

        }
            break;
        case MULTIPLE_DELETE:
        while (p->pNext!=NULL )
        {

           while (p->pNext->data==data)
           {
               flag=1;
               if(p->pNext->pNext==NULL)  //该节点是尾节点
             {
                 p->pNext=NULL;
                 pH->data-=1;
                   break;  
             }
            else
             {
                 p->pNext=p->pNext->pNext;    //重新连线
             }  
              free(pBack);  
            pBack=p->pNext;
              pH->data-=1;
           }

            p=pBack;
            if(pBack->pNext!=NULL)
            {
                pBack=pBack->pNext;   
            }
            else
            {
                if(flag==0)
                {
                    printf("不存在这样的节点\n");
                }
            }

        }           
            break;

        default:
            break;
        }

    }

}

这个函数花费我的时间是最多的,从构思到编写,然后调试。

这里简要说一下我的思路吧。在这个函数当中,有三个参数,第一个是头指针,用来定位链表,第二个是用户待删除的数据,第三个是选项,用来选择是删除一个还是删除所有。

先来考虑删除一个的情况,同样是使用while循环来判断是否到了最后一个节点,如果不是的话,再判断当前指针的下一个节点的数据是否满足要求,如果满足要求,就改变它们的“连线”,这一部分主要是指针的操作,还得考虑下一个节点是不是尾节点,因为普通节点和尾节点的连线方式是不一样的。找到之后就退出循环。而删除所有就是找到之后不退出循环,继续寻找下一个。另外,还需要注意的是删除节点之后最好释放内存,避免内存泄漏。

简单的理解就是先遍历,然后找数据,找到就改变“连线”。

五、逆序

链表的逆序就是将链表中的数据颠倒一下。

void reverse_linkedlist(struct node *pH)
{

    struct node *p=pH->pNext;
    struct node *pBack=p;
    if ((NULL ==p) || (NULL == p->pNext))
        return;
    while (p->pNext!=NULL)
    {

         pBack=p->pNext;   //先保存好p后面的值
       if(p==pH->pNext)    //第一个节点
       {
           p->pNext=NULL;
       }
       else
       {
           p->pNext=pH->pNext;  //连上前一个
           pH->pNext=p;
       }

        p=pBack;       
    }
    p->pNext=pH->pNext; 
    pH->pNext=p;

}

这个可能不是那么好理解。

大致的思路就是,指针p先指向头节点的后一个节点,然后进入while循环遍历。如果是第一个节点,就让它指向NULL,因为第一个节点最终会成为最后一个节点。然后p向移动一个节点,让那个节点指向头指针所连的那个节点,再让头指针指向p。如此循环,

画了一个草图如下

其实,在整个过程当中,头指针总是指向p前一个节点,比如p当前指向了2节点,那么头指针就指向1节点,本来1节点是指向2节点的,但是第一步操作让1节点指向了NULL。然后让2节点指向1节点,头指针再指向p所在位置,也就是2节点,之后p再往后移动一格,重复这样的动作,就能把前面n-1个节点倒序连接起来,但是同样存在一个问题就是当p指向最后一个节点的时候,退出while循环,导致最后一个节点没有连上倒数第二个节点,头指针也没有连上最后一个节点,这就需要自己再添加最后两句代码了。

 

最后我们可以看一下最终的效果

我们先是创建了5个有效节点,排序为1,2,3,4,5。然后遍历一次,可以正常显示,再把数据为1的节点删除,之后再逆序,最终遍历后输出结果,没有问题。再测试其他的情况,也都没有问题,说明我们的代码实现了预定目标。

 

总结:其实要写出这样的一个代码框架都是不难的,关键在于调试,没有人可以一遍就写出完美的、符合要求的代码,代码都是调出来的,俗话说三分写,七分调。只要有思路,耐心的去调试,逐渐就能写出符合要求的代码。另外,代码的规范性,可读性也是要注意的。