【数据结构基础/接口函数的编写】链表的基础知识和接口函数全编写

目录

Singly-Linked List

顺序表的缺陷

链表的优缺点

优点

缺点

链表的实现

定义结构体

链表的打印 SListPrint

链表的尾插 SListPushBack

头插 SListPushFront

尾删 SListPopback

头删 SListPopFront

查找 SListFind

插入 SListInsert

删除 SListErase

销毁 SListDestroy

总结

单链表 Singly-Linked List

链表是一种常见的基础数据结构,结构体指针在这里得到了充分的利用。链表可以动态的进行存储分配,也就是说,链表是一个功能极为强大的数组,他可以在节点中定义多种数据类型,还可以根据需要随意增添,删除,插入节点。链表都有一个头指针,一般以head来表示,存放的是一个地址。链表中的节点分为两类,头结点和一般节点,头结点是没有数据域的。链表中每个节点都分为两部分,一个数据域,一个是指针域。说到这里你应该就明白了,链表就如同车链子一样,head指向第一个元素:第一个元素又指向第二个元素;……,直到最后一个元素,该元素不再指向其它元素,它称为“表尾”,它的地址部分放一个“NULL”(表示“空地址”),链表到此结束。

作为有强大功能的链表,对他的操作当然有许多,比如:链表的创建,修改,删除,插入,输出,排序,反序,清空链表的元素,求链表的长度等等。

顺序表的缺陷

  1. realloc是有空间损耗的,在realloc运行的时候,首先进行判断,空间不够就会异地扩容,如果空间足够就会原地扩容。增容是要付出代价的。

  2. 为避免频繁扩容,空间满了一般会扩二倍,可能就会导致一定的空间浪费。

  3. 顺序表要求顺序从开始位置连续存储,我们在头部或者中间位置插入删除数据,就需要挪动数据,效率不高

但顺序表支持随机访问,有些算法,需要结构支持随机访问。比如,二分查找,比如优化的快排。

针对顺序表的缺陷我们就设计出来链表

链表的优缺点

优点

  1. 按需申请空间,不用了就释放空间,会合理的利用空间。

  2. 头部中间插入删除数据,不需要挪动数据,从而不存在空间浪费。

缺点

  1. 每存放一个数据,都要存一个指针去链接后面的数据节点。

  2. 不支持数据的随机访问(如同顺序表里直接用a[i] 去访问第i个数据)

单链表的缺陷还是很多的,单纯单链表的增删查找的意义不大。

  • 很多OJ题目考查的都是单链表

  • 单链表更多的是做更复杂的数据结构的子结构,哈希桶、临接表

链表存储数据还是要看双向链表。

链表的实现

定义结构体

 typedef int SLTDataType;
 typedef struct SListNode
 {
     SLTDataType data;
     struct SListNode *next;
 } SLTNode;

链表的打印 SListPrint

关键就是

  cur = cur->next

因为这个链表里面的两个数据分别是

 typedef struct SListNode
 {
     SLTDataType data;
     struct SListNode *next;
 }SLTNode;

cur被赋给了cur的next,实际上这个next里存放的就是下一个结构体的地址。

 void SListPrint(SLTNode *phead)
 {
     SLTNode *cur = phead;
     while(cur!=NULL)
    {
         printf("%d->", cur->data);
         cur = cur->next;
    }
 }

我们一般学习链表分为逻辑图和物理图,在我们的想象里,一般是phead去指向链表的第一个结构体,然后第一个结构体指向第二个结构体,以此类推,但是物理图并非如此,每个都是单独存放。

链表的尾插 SListPushBack

要尾插,首先就要找到这个尾巴。需要找到最后一个节点。我们需要找到这个原来的尾,去指向新的NewNode。

如何找到呢?

尾的标志就是下一个节点为空

     SLTNode *tail = phead;
     while(tail->next !=NULL)
    {
         tail = tail->next;
    }

直到找到了尾节点,再找到新节点

     SLTNode *newnode = (SLTNode *)malloc(sizeof(SLTNode));
     newnode->next = NULL;
     newnode-> data = x;
     tail->next = newnode;

最后一步是通过修改尾的指向,指向新的结构体newnode内存块。

但是发现运行不起来,程序会崩掉,这是因为如果我们直接去判断

     while (tail->next!= NULL)

会造成对NULL的解引用,从而导致了程序崩溃。

有一个问题,形参phead的改变没有影响实参plist。

我们是把plist的值拷贝了一份给phead,因为二者都是一级指针,如果想要改变plist实参的数值,你需要传递plist的地址,然后用二级指针接收。类似下面:

 &plist = pphead
 //改变*pphead 就改变了plist的值

所以我们可以把新空间块放在前面扩建,然后再去判断传过来的地址是不是空,如果是空的话,证明我们的链表整个为空。

头插 SListPushFront

我们知道pphead形参是plist实参的地址,如果我们头插,就是把*pphead(plist的地址) 放到新组建的newnode组块的next里,然后在把newnode的地址传给 解引用后的pphead。

这样就可以做到plist指向newnode,而newnode->又指向原来plist指向的值。这个时候即便去验证*pphead为空的情况,可以发现并没有影响,所以不用单独列出考虑。

 void SListPushFront(SLTNode **pphead, SLTDataType x)
 {
     SLTNode *newnode = BuyListNode(x);
 ​
     newnode->next = *pphead;
     *pphead = newnode;
 }

尾删 SListPopback

尾删首先要做到的就是找到尾巴,这个很简单,我们只需要使用

     SLTNode *tail = *pphead;
     while(tail->next)
    {
         prev = tail;
         tail = tail->next;
    }

就可以找到尾,随后很多同学就直接用

     free(tail);
     tail = NULL;

free掉tail就结束函数了,这是不正确的。因为tail之前的一个块是没有置空的,这样就会导致野指针。从而程序出错。正确做法应该是记录一个指针变量prev,每次记录tail之前的块,然后最后找到尾后置空prev->next即可。

 void SLitstPopBack(SLTNode **pphead)
 {
     if(*pphead ==NULL)
    {
         return;
    }
     //assert(*pphead!=NULL)//比较粗暴
     if(((*pphead)->next)==NULL)
    {
         free((*pphead)->next);
         *pphead = NULL;
    }
     SLTNode *prev = NULL;
     SLTNode *tail = *pphead;
     while(tail->next)
    {
         prev = tail;
         tail = tail->next;
    }
     free(tail);
     tail = NULL;
     prev->next = NULL;
 }

头删 SListPopFront

头删相对于尾删来说简单一些,只需要一个tmp变量存储中间值,然后free掉*pphead即可,不过要注意plist传过来为空指针的情况

 void SLitstPopFront(SLTNode **pphead)
 {
    if (*pphead == NULL)
    {
        return;
    }
 ​
    SLTNode *tmp = (*pphead)->next;
    free(*pphead);
    *pphead = tmp;
 }

查找 SListFind

查找本身不会改变plist的值,所以无需传递二级指针。

 SLTNode* SListFind(SLTNode *phead, SLTDataType x)
 {
     SLTNode *pos = phead;
     while(pos)
    {
         if(pos->data == x)
        {
             return pos;
        }
         pos = pos->next;
    }
     return NULL;
 }

查找本身很简单,如何实现多次查找呢( 也就是如果想查找的值在链表内多次出现的话)

     SLTNode* pos = SListFind(plist,2);
     int i = 1;
     while(pos)
    {
         printf("第%d个pos节点: %p->%d\n", i++, pos, pos->data);
         pos = SListFind(pos->next, 2);
    }

查找的同时也可以进行修改,很简单,此处就不贴代码了。

插入 SListInsert

插入接口函数需要先设计好Find函数,先查找到具体的值的位置pos,然后插入到pos位置之前。

但是由于单向链表是无法直接找到pos前一个位置的,所以我们需要再定义一个posPrev,然后找到posPrev,随后让posPrev指向newnode,newnode再指向pos。

但是我们需要注意posPrev在pos之前,如果pos就是plist指向的结构体的地址的话,程序会崩溃。在这个条件的时候,变成头插了,所以直接复用头插即可。

 void SListInsert(SLTNode **pphead, SLTNode *pos, SLTDataType x)
 {
     SLTNode *newnode = BuyListNode(x);
     //找到pos前一个位置
     if (pos == *pphead)
    {
         SListPushFront(pphead, x);
    }
     else
    {
         SLTNode *posPrev = *pphead;
         while (posPrev->next != pos)
        {
             posPrev = posPrev->next;
        }
         posPrev->next = newnode;
         newnode->next = pos;
    }
 }

我们发现,在pos前面插入是非常麻烦的,这是因为我们需要确定pos之前的位置,也就是posPrev的值,但是posPrev的值又需要考虑区分为:pos就是*pphead时(posPrev没法取值),posPrev可以取值,两种情况,非常麻烦,所以我们可以考虑在pos的后面插入。

 void SListInsertAfter(SLTNode *pos, SLTDataType x)
 {
     SLTNode *newnode = BuyListNode(x);
     newnode->next = pos->next;
     pos->next = newnode;
 }

删除 SListErase

删除相对于插入是非常类似的。我们需要判断pos的位置来确定是否头删,如果是头删直接复用头删函数,或者很简单的贴两行代码如下:

 if(*pphead == pos)
 {
 *pphead = pos->next;
 free(pos);
 }

随后需要进行的就是正常位置的删除,也就是中间删除。只需要用posPrev去找到pos的前一个位置,然后改变指向即可。

         SLTNode *posPrev = *pphead;
         while (posPrev->next != pos)
        {
             posPrev = posPrev->next;
        }
         posPrev->next = pos->next;
         free(pos);

至此我们可以考虑一下如果是尾删的话会不会需要再单独列出来呢?

尾删时,pos->next = NULL; 把空指针传过去给posPrev->next完全没有问题,所以不需要单独列出来。实际上,我们在写单链表的时候,只需要考虑头部情况即可。

一个问题:在free后不是紧接着要跟一个置空吗?

 free(pos);
 pos = NULL;

但是在接口函数里,pos只是形参,是实参的一份临时拷贝,改变pos不会影响实参的值,所以可以不加。但是为了好的习惯,建议在free后都加置空。同样的,观看整体的函数,可以发现需要讨论情况,比较繁琐。

 void SListErase(SLTNode **pphead, SLTNode *pos)
 {
     assert(pphead);
     //头删
     if (pos == *pphead)
    {
         *pphead = pos->next;
         free(pos);
    }
     else
    {
         SLTNode *posPrev = *pphead;
         while (posPrev->next != pos)
        {
             posPrev = posPrev->next;
        }
         posPrev->next = pos->next;
         free(pos);
    }
 }

所以我们可以使用SListEraseAfter函数,删除pos位置的后一个接口即可。

销毁 SListDestroy

销毁实际上就是把每个内存块都进行free,所以有多种做法。

比如我们可以复用尾删函数进行销毁,如下:

 void SListDestroy(SLTNode **pphead)
 {
     assert(pphead);
     while (pphead)
    {
         SLitstPopBack(pphead);
    }
 }

同样的,我们也可以直接进行销毁,如下:

 void SListDestroy(SLTNode **pphead)
 {
     assert(pphead);
     SLTNode *cur = pphead;
     while(cur)
    {
         SLTNode *next = cur->next;
         free(cur);
         cur = next;
    }
     *pphead = NULL;
 }

总结

单向链表是比较简单的,不外乎主要的增删查改功能,而在对单向链表的考察中,也一般是增删查改的变形,我们需要做的就是记住链表的特点,并利用其特点和优点去实现简单链表和完成相应的题目。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值