单链表的基本操作

       线性表有两种存储方式,一种是顺序表,在顺序表的基本操作中,有说过,顺序表是存放在一片连续的内存空间中,以物理位置的相邻来表示逻辑位置的相邻。当数据量过多时,对内存空间的利用率就比较低。

        而线性表的另外一种表示就是链表,它没有占据一整片连续的内存,而是分散在一个个零散的区域中,以指针的形式将各个节点连接起来,以表示线性表逻辑上的相邻顺序。这样做可以提高对内存空间的利用率。

        链表的类型有很多,根据有无头结点,有无环,双向还是单向一共可以组合为8种类型,接下来要说的是单向不带环,不带头结点的链表的相关操作。

1. 单链表的存储结构

        单链表有由一个个的结点构成,通过指针将各节点连接起来,就构成了一条完整的链表。在链表结点中,不仅包含基本的数据,还有就是因为是单链表,所以只需要指向下一个节点的指针即可。存储结构如下:

typedef char LinkListType;//结点所存储数据的数据类型
//定义链表结点结构体
//每个链表节点包含一个数据元素data和指向下一个节点的指针next
typedef struct LinkListNode                                                                                                                                            
{
    LinkListType data;//结点的数据域
    struct LinkListNode* next;//指向下个结点的指针
}LinkListNode//将链表节点结构体重新命名

2. 链表的定义及初始化

       各节点利用指针连接起来形成的链即为链表。只要已知前一个结点的内容,即可根据前一个结点的next域找到下一个节点。所以,只要知道头结点的指针即地址,就可以顺着各节点的next域找到之后的各个结点。因此,可以用头结点的指针来表示该链表。

       定义一个单链表就是定义一个头指针,如

LinkListNode* head;//定义指向头结点的头指针head

       初始化单链表

       初始时,单链表中并没有结点,所以头指针的指向为空。可以简单的写为:

head = NULL;

       不过,为提高代码的可维护性,将链表的初始化封装为函数。因为要改变头指针的指向,即改变指针变量的内容,由于形参只是实参的一份临时拷贝,所以,再传参时,要传头指针的地址,即二级指针。

//初始化单链表,这里的phead指的是头指针的地址
void LinkListInit(LinkListNode** phead)
{
    if(phead == NULL)
    {
        //非法输入,对空指针解引用程序会异常终止
        return;
    }
    *phead = NULL;//将头指针*phead的指向置为空
    return;
}

3. 在单链表中尾插元素

        在链表中尾插元素,不考虑非法输入,首先要申请新结点,使新节点的数据域为所给的数据,指针域统一赋为NULL,在具体使用时再对其指针域进行修改。如果新节点申请成功,返回新节点的地址,如果失败,返回NULL。为提高代码的可维护性,将新节点的申请封装为函数:

//创建新节点,value是新节点的数据域
LinkListNode* CreateNode(LinkListType value)
{
    LinkListNode* new_node = (LinkListNode*)malloc(sizeof(LinkListNode));//动态申请节点
    new_node->data = value;//使新节点的数据域为value
    new_node->next = NULL;//指针域置为空
    return new_node;//如果申请成功,返回新节点的地址,如果失败,返回NULL                                                                                                
}

        根据链表初始状态的不同,需要考虑以下几种情形:

(1)如果插入之前,链表为空,此时,插入之后,直接使头指针指向新插入的结点即可

(2)若链表不为空,则首先要遍历找到最后一个元素,再将新节点插入最后一个节点的后面

        因为对于情形(1),需要改变头指针的指向,所以,函数传参时,传的是头指针的地址,即二级指针。针对上述两种情况,编写代码如下:

//在单链表尾部插入一个新节点,节点的数据为value                                                                                                                         
void LinkListPushBack(LinkListNode** phead,LinkListType value)
{
    if(phead == NULL)
    {
        //非法输入,对空指针解引用程序会异常终止,所以这里需要判断一下
        return;
    }

    if(*phead == NULL)
    {
        //空链表,需改变头指针的指向
        *phead = CreateNode(value);
        return;
    }
    
    //链表非空
    LinkListNode* cur = *phead;//定义cur为当前节点的指针,初始为头节点的指针即头指针    
    while(cur->next != NULL)//遍历寻找最后一个节点
    {
        cur = cur->next;
    }
    //此时,cur为最后一个节点,所以新节点只需插入在cur之后即可
    cur->next = CreateNode(value);
    return;
}

        该算法的时间复杂度为O(n),空间复杂度为O(1)。

4. 在单链表中尾删结点

        因为该链表中的节点都是用malloc动态申请出来的,所以,要对接点进行删除,就要人为的用free去释放。这里将节点的销毁也封装为函数,原因有二:

(1)与节点的申请函数对应,可以提高代码的可读性

(2)也是比较重要的一点,如果该结点的数据也是malloc动态申请出来的,那在释放节点之前就要先释放节点的数据域,在释放该结点,将其封装为函数,可以在销毁函数中增加一条语句,便可以做到一改全改。这样可以增强代码的可维护性。

//销毁单链表中的一个节点node
//这里,封装为函数,可以提高代码的维护性
void LinkListNodeDestory(LinkListNode* node)
{
    free(node);//free释放节点                                                                                                                                           
}

        根据尾删前链表的状态不同,可分以下几种情形:

(1)如果链表为空,则尾删失败

(2)如果链表中只有一个结点,则尾删后,链表的头指针要指向空

(3)如果链表中不只有一个节点,则就要遍历找到倒数第二个节点,将其next域置为NULL,并将最后一个节点删除

        因为在(2)中要改变头指针的指向,所以函数传参时要传递头指针的地址,即二级指针。针对以上几种情形,有以下两种实现方法:

        方法1:不需要定义节点变量:

//在单链表尾部删除一个节点(不定义新结点)
void LinkListPopBack1(LinkListNode** phead)
{
    if(phead == NULL)
    {
        //非法输入
        return;
    }
    if(*phead == NULL)
    {
        //空链表
        return;
    }

    if((*phead)->next == NULL)
    {
        //只有一个节点,删除后需要改变头指针的指向
        LinkListNodeDestory(*phead);//销毁头节点*phead
        *phead = NULL;//再使头指针置为空
        return;
    }
    
    LinkListNode* cur = *phead;//定义当前节点指针cur
    while(cur->next->next != NULL)//遍历查找倒数第二个节点
    {
        cur = cur->next;
    }
     //此时,cur为倒数第二个节点
    LinkListNode* to_delete = cur->next;//保存要删除的结点地址                                                                                                            
    cur->next = NULL;//将倒数第二个节点的next域置为空
    LinkListNodeDestory(to_delete);//销毁最后一个节点to_delete
    return;
}

        方法2:需定义新节点变量

//定义新节点来尾删
void LinkListPopBack2(LinkListNode** phead)
{
    if(phead == NULL)
    {
        //非法输入
        return;
    }
    if(*phead == NULL)
    {
        //空链表
        return;
    }
    if((*phead)->next == NULL)
    {
        //链表中只有一个节点,此时,需要改变头指针的指向
        LinkListNodeDestory(*phead);//销毁头节点*phead
        *phead = NULL;
        return;
    }

    LinkListNode* cur = (*phead)->next;//定义当前节点查找最后一个节点
    LinkListNode* pre = *phead;//定义当前节点的前一个节点
    while(cur->next != NULL)
    {
        pre = cur;//当前节点不是最后一个节点,就使前一个节点先指向当前节点
        cur = cur->next;//当前节点在后移
    }
    //当前节点cur是最后一个节点
    pre->next = NULL;//使倒数第二个节点的next域置为空
    LinkListNodeDestory(cur);//销毁最后一个节点
    return;
}

        这两种方法的时间复杂度和空间复杂度均为O(n)和O(1)。

5. 在单链表中头插节点

        根据链表的状态不同,可以分为以下两种情况讨论:

(1)初始时,链表为空,此时,只需使头指针指向新节点即可

(2)初始时,链表不为空,此时,需要先使新节点的next域指向原来的头结点,再使头指针指向新节点即可。

        因为(1)可以包含在(2)中一起讨论,所以不需要在分情况讨论。头插结点要改变头指针的指向,所以,函数的参数要传头指针的地址,即二级指针。函数的代码如下:

//头插节点,新节点的数据域为value
void LinkListPushFront(LinkListNode** phead,LinkListType value)
{
    if(phead == NULL)
    {
        //非法输入
        return;
    }
    
    LinkListNode* new_node = CreateNode(value);//创建新节点
    new_node->next = *phead;//使新节点的next域指向原来的头节点,如果链表初始为空,即指向NULL
    *phead = new_node;//再使头指针指向新节点                                                                                                                            
    return;
}

        该算法的时间复杂度和空间复杂度均为O(1)。

6. 在单链表中头删结点

        根据初始时链表的状态不同,分为以下一种情形:

(1)链表为空,删除失败

(2)链表只有一个结点,先将头结点释放,在将头结点置为空

(3)链表中不只有一个节点,将头结点的地址保存起来,使头指针指向第二个节点,在根据保存的头结点的地址删除头结点。

        同样,(2)(3)可以合并讨论。头删结点要改变头指针的指向,所以,函数的参数要传头指针的地址,即二级指针。函数的代码如下:

//头删节点
void LinkListPopFront(LinkListNode** phead)
{
    if(phead == NULL)
    {   
        //非法输入
        return;
    }   
    if(*phead == NULL)
    {   
        //空链表
        return;
    }   

    LinkListNode* tmp = (*phead)->next;//保存第二个节点的地址,如果链表中只有一个节点,则此时tmp即为NULL
    LinkListNodeDestory(*phead);//销毁第一个节点
    *phead = tmp;//使头指针指向第二个节点
}

          该算法的时间复杂度和空间复杂度均为O(1)。

7. 查找指定元素所在的地址

        首先,如果链表为空,则肯定是找不到指定元素的。如果链表不为空,指定的元素可能在该链表中,也可能不在该链表中。这两种情况都要从头遍历链表。如果在,返回指定元素所在的地址,如果不在返回NULL。因为在该算法中,并没有改变头指针的指向,所以函数传参时只需传头指针即以及指针即可。

//查找指定元素to_find所在的地址,如果没找到,返回NULL
LinkListNode* LinkListFind(LinkListNode* head, LinkListType to_find)
{
    if(head == NULL)
    {
        //链表为空,找不到指定元素
        return NULL;//直接返回NULL即可
    }                                                                                                                                                                   
    //设置cur为当前指针,初始指向头节点
    LinkListNode* cur = head;
    //遍历链表寻找要查找的元素
    while(cur != NULL)
    {
        if(cur->data == to_find)
        {   
            return cur;//当找到要查找的元素,返回该元素的地址
        }
        cur = cur->next;//指针后移,继续往后遍历
    }
    return NULL;//链表遍历完,还没有找到,说明要查找的元素不再该链表中,返回NULL
}

        该算法的时间复杂度和空间复杂度分别为O(n)和O(1)。

8. 在任意位置之后插入节点

        因为要在指定位置之后插入节点,说明链表中必须至少有一个节点,所以可分为以下情形:

(1)链表为空,插入失败

(2)插入的位置在或不在链表中。此时需要遍历链表来使插入的位置与链表中各节点的地址进行对比,如果相等,则在该结点之后直接插入即可。若遍历完链表还没找到指定的位置,说明该位置不在该链表中,则插入失败。

        因为在此过程中,并没有改变头指针的指向,所以函数传参时只需传入头指针即一级指针即可。

//在单链表任意位置pos之后插入节点value
void LinkListInsertAfter(LinkListNode* head,LinkListNode* pos,LinkListType value)
{
    if(pos == NULL)
    {    
        //非法输入
        return;
    }    

    if(head == NULL)
    {    
        //空链表,插入失败
        return;
    }    

    //定义cur为当前指针,初始指向头节点
    LinkListNode* cur = head;
    //遍历链表,判断插入的位置pos是否在链表中
    while(cur != NULL)
    {    
        if(cur == pos)//如果插入的位置在链表中
        {    
            LinkListNode* node = CreateNode(value);//动态申请新节点
            node->next = pos->next;//使新节点的next指向pos的下个节点
            pos->next = node;//再使pos的next域指向新节点
            return;
        }    
        cur = cur->next;//如果没找到插入的位置就一直往后遍历
    }    
    return;//遍历完整个链表都没找到要插入的位置,说明该位置不在该链表中,则插入失败
}

        该算法是时间复杂度和空间复杂度分别为O(n)和O(1)。

9. 在任意位置之前插入节点

        因为要在指定位置之前插入元素,说明链表中必须要有节点。所以,可分为以下情形:

(1)链表为空,插入失败

(2)如果插入的位置为头结点,即在第一个结点前插入节点,此时相当于头插,需要改变头指针的指向

(3)如果在链表的其他位置之前插入节点,此时需要找到该位置的前一个节点,在进行插入

(4)如果插入的位置不在链表中,则插入失败

        因为(2)中需要改变头指针的地址,所以函数传参时传的是头指针的地址即二级指针。代码如下:

//在单链表任意位置pos之前插入节点value
void LinkListInsert(LinkListNode** phead,LinkListNode* pos,LinkListType value)
{
    if(phead == NULL || pos == NULL)
    {
        //非法输入
        return;
    }
    if(*phead == NULL)
    {
        //空链表,插入失败
        return;
    }
    //如果在第一个节点前插入新节点
    if(pos == *phead)
    {
        //直接调用头插函数
        LinkListPushFront(phead,value);
        return;
    }

    //定义cur为当前位置指针来表示指定位置的上一个节点,初始为头指针
    LinkListNode* cur = *phead;
    //遍历链表,判断插入的位置是否在链表中
    while(cur != NULL)
    {
        if(cur->next == pos)//如果插入的位置在链表中
        {
            break;//跳出循环
        }
        cur = cur->next;//还未找到要插入的位置,一直往后遍历
    }
    if(cur == NULL)//当遍历完整个链表还没找到指定的位置,则插入失败
    {
        return;
    }
    LinkListInsertAfter(*phead,cur,value);//此时cur为指定位置的上一个节点,在指定位置之前插入,即在cur之后插入节点
    return;
}

        该算法的时间复杂度和空间复杂度分别为O(n)和O(1)。

10. 删除任意位置处的节点

        根据初始时链表的状态和位置的不同,可分为以下几种情形:

(1)链表为空时,删除失败

(2)当删除的位置为第一个结点时,此时需改变头指针的指向,相当于头删

(3)当删除的位置在链表中其他节点时,不需要改变头指针的指向

(4)当删除的位置不在链表中时,删除失败。

        代码如下:

//删除任意位置处的节点
void LinkListErase(LinkListNode** phead, LinkListNode* pos)
{
    if(phead == NULL || pos == NULL)
    {   
        //非法输入
        return;
    }
    if(*phead == NULL)
    {   
        //链表为空,删除失败
        return;
    }
    if(pos == *phead)
    {
        //删除位置为第一个节点
        *phead = pos->next;//使头指针指向第二个节点
        LinkListNodeDestory(pos);//删除头节点
        return;
    }
    
    //定义cur为当前指针变量来表示指定位置的上一个节点,初始为头节点
    LinkListNode* cur = *phead;
    while(cur != NULL)
    {
        if(cur->next == pos)//如果删除的位置在链表中
        {
            break;
        }
        cur = cur->next;//还没找到要是删除的位置,就继续往后遍历
    }
    if(cur == NULL)//如果遍历完整个链表还没找到指定位置,说明指点位置不再链表中
    {
        return;
    }
//删除的位置在链表中,cur表示指定位置的上一个节点
    cur->next = pos->next;//使cur指向指定位置的下一个节点
    LinkListNodeDestory(pos);//删除指定位置出的节点
    return;
}

        该算法的时间复杂度和空间复杂度分别为O(n)和O(1)。

11. 删除指定值的结点(如果有多个指定值,只删除一个)

        根据指定的值删除节点,可分为以下几种情形

(1)链表为空,删除失败

(2)要删除的结点为头结点,此时需要修改头指针的指向

(3)要删除的结点为后面的结点,此时,需要遍历寻找要删除的结点

(4)链表中没有要删除的值,此时,需要遍历完整个链表才知道。

        代码如下:

//删除指定元素to_delete的节点,如果有多个,只删除一个
void LinkListRemove(LinkListNode** phead,LinkListType to_delete)
{
    if(phead == NULL)
    {    
        //非法输入
        return;
    }    
    if(*phead == NULL)
    {    
        //空链表
        return;
    }    
    if((*phead)->data == to_delete)
    {    
        //删除头节点,此时,需要改变头指针的指向
        LinkListNode* tmp = (*phead)->next;//保存要删除节点的下一个节点的地址
        LinkListNodeDestory(*phead);//删除要删除的节点
        *phead = tmp;//使头指针指向下个节点
        return;
    }    

    //定义cur为当前节点的地址,初始为头指针
    LinkListNode* cur = *phead;
    while(cur->next != NULL)//遍历查找要删除的节点
    {    
        if(cur->next->data == to_delete)//如果找到了
        {    
            LinkListNode* tmp = cur->next->next;//保存要删除节点的下一个节点的地址
            LinkListNodeDestory(cur->next);//删除要删除的节点
            cur->next = tmp;//使cur的next指向下个节点
            return;
        }    
        cur = cur->next;//没找到,继续往后遍历
    }    
    return;//当遍历完链表还没找到,说明链表中不存在该节点
}

        在此例中,还可以通过上述已有的函数来实现。

(1)先通过LinkListFind函数找到要删除结点所在的位置

(2)再根据找到的位置利用LinkListErase函数将该结点删除。

12. 删除指定值的结点(如果有多个指定值,则全部删除)

        可以通过上述已有的函数来实现。

(1)先通过LinkListFind函数找到要删除结点所在的位置

(2)再根据找到的位置利用LinkListErase函数将该结点删除

(3)如果有多个,就一直循环(1)(2)两过程,直到找不到要删除的结点,即删除成功

        代码如下:

//删除指定元素的所有节点
void LinkListRemoveAll(LinkListNode** phead,LinkListType to_delete)
{
    if(phead == NULL)
    {
        //非法输入
        return;
    }
    //找到删除节点的位置(一次只能找一个)
    LinkListNode* pos = LinkListFind(*phead,to_delete);
    while(pos != NULL)//如果链表中还有要删除的节点                                                                                                                      
    {
        LinkListErase(phead,pos);//根据节点的位置将其删除
        pos = LinkListFind(*phead,to_delete);//继续查找指定元素的下一个节点所在位置
    }
    return;
}

13. 链表判空

        如果链表的头指针指向NULL,即链表为空,返回1

        如果链表的头指针指向NULL,即链表非空,返回0

        代码如下:

//链表判空,如果为空,返回1,否则返回0
int LinkListEmpty(LinkListNode* head)                                                                                                                                   
{
    if(head == NULL)
    {
        //链表为空
        return 1;
    }
    return 0;
}

14. 统计链表中的元素个数

(1)当链表的头指针指向NULL时,链表为空

(2)否则,定义当前节点的指针变量cur和计数器变量count,依次往后遍历,移动一次,count加1,直到cur指向NULL

        代码如下:

//统计链表中的节点个数
size_t LinkListSize(LinkListNode* head)
{
    //头指针指向NULL,链表为空
    if(head == NULL)
    {
        return 0;
    }

    size_t count = 0;//定义计数器变量用于记录节点的个数,初始为0                                                                                                        
    LinkListNode* cur = head;//定义当前指针变量,初始指向头节点
    while(cur != NULL)//当cur不为空时
    {
        count++;//节点个数加1
        cur = cur->next;//cur后移
    }
    return count;
}

        另外,上述代码中的链表为空语句可以包含在后面的语句中,所以可以省略不写。

15. 取链表中最后一个节点的地址

 (1)链表为空时,最后一个节点的地址即为NULL

(2)链表不为空时,要遍历找到最后一个节点,而最后一个节点的判断依据是它的next域为NULL

        代码如下:

//取链表最后一个节点的地址                                                                                                                                              
LinkList* LinkListBack(LinkListNode* head)
{
    if(head == NULL)
    {
        //链表为空
        return NULL;
    }
    LinkList* cur = head;//定义cur为当前指针变量,初始指向头节点
    while(cur->next != NULL)//当为找到最后一个节点时,指针一直往后移,直到找到为止
    {
        cur = cur->next;
    }
    //此时,cur为最后一个节点的地址
    return cur;
}

16. 销毁链表

        可分为以下几种情况:

(1)链表为空,不用再销毁

(2)链表非空,从头结点开始遍历,一个一个进行销毁

        代码如下:

//销毁链表
void LinkListDestory(LinkListNode** phead)
{
    if(phead == NULL)
    {
        //非法输入
        return;
    }
    if(*phead == NULL)
    {
        //链表为空
        return;
    }
    //定义cur为当前指针变量,初始指向头节点
    LinkListNode* cur = *phead;
    while(cur != NULL)//如果cur不为NULL,就要对其进行销毁
    {
        LinkListNode* to_delete = cur;//保存要销毁的节点地址
        cur = to_delete->next;//使cur指向要销毁节点的下一个节点
        DestoryNode(to_delete);//销毁节点
    }
    *phead = NULL;//全部销毁结束后,使头指针置为NULL
    return;
}


评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值