数据结构-2.6.单链表的插入和删除


一.按位序插入(带头结点):

1.思路:

在第i个位置上插入一个元素,就需要找到第i-1个结点,用malloc函数申请一个新的结点,把要插入的元素放入该结点:

再把第i-1个结点的指针修改:

特例:把指定元素插入在第一个位置,此时就体现出带头结点的单链表的好处->头结点可视为"第0个"节点

2.代码演示:

#include<stdio.h>
#include<stdlib.h> 
​
​
//定义单链表结点类型
typedef struct LNode  
{
    int data; //每个结点存放一个数据元素
    struct LNode *next; //指针指向下一个结点      
}LNode,*LinkList;
​
​
//初始化一个单链表(带头结点)
bool InitList(LinkList &L)
{
    L = (LNode *)malloc( sizeof(LNode) ); //分配一个头结点
    if(L==NULL) //代表内存不足,分配失败-->意味着带头结点的单链表无法创建 
    {
        return false;
    }
    else
    {
        L -> next = NULL; //头结点之后暂时还没有节点,所以指向NULL
        return true; 
    } 
} 
​
​
//判断单链表是否为空(带头结点)
bool Empty(LinkList L)
{
    if(L->next==NULL) //头结点之后如果指向NULL,代表没有数据 
    {
        return true;
    }
    else
    {
        return false;
    }
} 
​
​
/*在第i个位置插入元素e(带头结点)
->插入元素之前已经初始化了一个空表,有头结点,此时L指向头结点(有数据元素和指向下一个结点的指针) */
bool ListInsert(LinkList &L,int i,int e)
{
    if(i<1) //i表示位序,必须是正整数,所以要进行有界判断 
    {
        return false;
    }
    //走到这儿说明位序i合法
    LNode *p; //指针p指向当前扫描到的结点 
    int j=0; //当前p指向的是第几个结点:j为0代表头结点 
    p=L; //L指向头结点,头结点是第0个结点(不存数据)
    while(p!=NULL && j<i-1) //循环找到第i-1个结点 
    {
        /*以i为2为例(和下述图例无关,这个就是用来解释循环),此时p!=NULL,j为0,i-1为1,符合循环条件
          此时p为p->next就指向了下一个结点。j++后为1,不符合循环条件,跳出循环,
          此时p就是i为2的后一个位置,就把i为2空出来了,就可以插入元素了*/
        p = p->next;
        j++;
    } 
    if(p==NULL) //说明i值不合法 
    {
        return false;
    }
    //申请一个新的结点空间 
    LNode *s = (LNode *)malloc( sizeof(LNode) );
    //把参数e存入这个新的结点空间 
    s->data = e;
    //把s指向的结点next指针让他等于p结点next指针指向的位置 
    s->next = p->next;
    //将结点s连到p之后
    p->next = s; 
    return true; //插入成功 
}
 
​
int main()
{
    //声明一个指向单链表的指针
    LinkList L;
    //初始化一个空表
    InitList(L); 
    return 0;
} 
例一:插在表头

i为1时不走第一个if语句和while循环,且此时p指向头结点,不为空,也不走第二个if语句(i值不合法即为0或者负数时会

没有头结点:比如i为0时头结点就是-1,不合法,头结点应该在0位置上,i为负数时同理,因此i值不合法时没有头结点也就使得头结点为NULL)

申请一块新的结点,存入要存的元素,

p->next此时是a1,所以要把p结点(p指向头结点)next指针指向的位置赋值给s指向的结点next指针

i为1时可达到最好的时间复杂度(插在表头),因为不走while循环

注:必须先执行s->next = p->next;,再执行p->next = s;,如果颠倒了,会使得s最终指向自己:

例二:插在表中

i为3时(也代表插入的数据最后在第三个位置上),i-1为2,会走while循环,会循环两次,第一次循环后p指向a1,第二次循环后p指向a2,所以p->next指向a3:

例三:插在表尾

i为5时(也代表插入的数据最后在第五个位置上),i-1为4,会走while循环,会循环4次,第一次循环后p指向a1,第二次循环后p指向a2,第三次循环后p指向a3,第四次循环后p指向a4,所以p->next指向NULL:

这个函数第一部分就是越界判断,第二部分用来循环找到第i-1个结点,第三部分用来插入元素

i为5时可达到最坏的时间复杂度(插在表尾),因为走while循环的次数最多

例四:直接插在比表尾还靠后的位置->显然做不到,因为最多只能先插在表尾,再插比表尾还靠后的位置

i为6时,i-1为5,共走5次while循环,第4次循环后p指向a4,此时p不为NULL,且j为4,i-1为5,符合循环条件,

第5次循环时p就指向了NULL,j++为5,不符合循环条件,跳出循环,此时会走第二个if语句,之后return false结束函数。

while循环结束后p为NULL就代表第i-1个结点不存在->就无法插入第i个结点

3.总结:


二.按位序插入(不带头结点):

1.思路:

注:由于不存在头结点即不存在"第0个"结点,因此插在第一个位置即i为1时需要特殊处理。

2.代码演示:

#include<stdio.h>
#include<stdlib.h> 
​
​
//定义单链表结点类型
typedef struct LNode  
{
    int data; //每个结点存放一个数据元素
    struct LNode *next; //指针指向下一个结点      
}LNode,*LinkList;
​
​
//初始化一个单链表(带头结点)
bool InitList(LinkList &L)
{
    L = (LNode *)malloc( sizeof(LNode) ); //分配一个头结点
    if(L==NULL) //代表内存不足,分配失败-->意味着带头结点的单链表无法创建 
    {
        return false;
    }
    else
    {
        L -> next = NULL; //头结点之后暂时还没有节点,所以指向NULL
        return true; 
    } 
} 
​
​
//判断单链表是否为空(带头结点)
bool Empty(LinkList L)
{
    if(L->next==NULL) //头结点之后如果指向NULL,代表没有数据 
    {
        return true;
    }
    else
    {
        return false;
    }
} 
​
​
/*在第i个位置插入元素e(不带头结点) */
bool ListInsert(LinkList &L,int i,int e)
{
    if(i<1) //i表示位序,必须是正整数,所以要进行有界判断 
    {
        return false;
    }
    //走到这儿说明位序i合法
    if( i==1 ) //插入第一个结点的操作与其他结点的操作不同 
    {
        //申请一个新的结点空间 
        LNode *s = (LNode *)malloc( sizeof(LNode) );
        //把参数e存入这个新的结点空间 
        s->data = e;
        //让
        s->next = L;
        //让头指针指向新结点 
        L = s; 
        return true; //表示插入成功 
    }
    LNode *p; //指针p指向当前扫描到的结点 
    int j=1; //当前p指向的是第几个结点:j为1代表第一个结点(不是头结点) 
    p=L; //p指向第一个结点(注:不是头结点) 
    while(p!=NULL && j<i-1) //循环找到第i-1个结点 
    {
        /*以i为2为例,此时p!=NULL,j为0,i-1为2,符合循环条件
          此时p为p->next就指向了下一个结点。j++后为1,不符合循环条件,跳出循环,
          此时p就是i为2的后一个位置,就把i为2空出来了,就可以插入元素了*/
        p = p->next;
        j++;
    } 
    if(p==NULL) //说明i值不合法 
    {
        return false;
    }
    //申请一个新的结点空间 
    LNode *s = (LNode *)malloc( sizeof(LNode) );
    //把参数e存入这个新的结点空间 
    s->data = e;
    //把s指向的结点next指针让他等于p结点next指针指向的位置 
    s->next = p->next;
    //将结点s连到p之后
    p->next = s; 
    return true; //插入成功 
}
 
​
int main()
{
    //声明一个指向单链表的指针
    LinkList L;
    //初始化一个空表
    InitList(L); 
    return 0;
} 
例一:插在表头:

i为1时,走第二个if语句,先利用malloc函数申请一个新的结点,再在这个新的结点中存入要插入的元素:

L一开始指向a1,但结果要求L指向s,

所以需要先把L赋值给s->next即把a1和s里面指向下一个元素的指针相连(新结点的next指针指向L所指向的结点),

最后需要修改头指针L即让L指向新的结点(把s连接到p之后即p=s)。

例二:插在表头之外的位置即i>1


三.指定结点的后插操作:

1.代码演示:

#include<stdio.h>
#include<stdlib.h> 
​
​
//定义单链表结点类型
typedef struct LNode  
{
    int data; //每个结点存放一个数据元素
    struct LNode *next; //指针指向下一个结点      
}LNode,*LinkList;
​
​
//初始化一个单链表(带头结点)
bool InitList(LinkList &L)
{
    L = (LNode *)malloc( sizeof(LNode) ); //分配一个头结点
    if(L==NULL) //代表内存不足,分配失败-->意味着带头结点的单链表无法创建 
    {
        return false;
    }
    else
    {
        L -> next = NULL; //头结点之后暂时还没有节点,所以指向NULL
        return true; 
    } 
} 
​
​
//判断单链表是否为空(带头结点)
bool Empty(LinkList L)
{
    if(L->next==NULL) //头结点之后如果指向NULL,代表没有数据 
    {
        return true;
    }
    else
    {
        return false;
    }
} 
​
​
//后插操作:在p结点之后插入元素e
/*注:单链表的链表指针只能往后寻找,所以给定p,p之后的结点就都可以找出来, 
      p之前的结点就无法找出来,也就无法往前插,只能后插 */
bool InsertNextNode(LNode *p,int e)
{
    if(p==NULL)
    {
        return false;
    }
    //申请一个新的结点空间(注:如果此时内存已满,就无法申请出新的结点空间,导致s为NULL) 
    LNode *s = (LNode *)malloc( sizeof(LNode) );
    if(s==NULL) //内存分配失败 
    {
        return false;
    }
    //用结点s保存数据元素e 
    s->data = e;
    //此时p指向下一个元素(p->next)即y,最终需求s指向y,那么就需要s->next = p->next
    s->next = p->next;
    //将结点s连接到p之后 
    p->next = s; 
    return true;
} 
 
​
int main()
{
    //声明一个指向单链表的指针
    LinkList L;
    //初始化一个空表
    InitList(L); 
    return 0;
} 

2.图解:

申请一个新的结点空间(注:如果此时内存已满,就无法申请出新的结点空间,导致s为NULL),把要插入的元素存入这个新申请的结点空间:

此时p指向下一个元素(p->next)即y,最终需求s指向y,那么就需要s->next = p->next,再把s连接到p之后即p->next = s:

该操作没有循环,显然时间复杂度是O(1)。

3.插入操作的简化:

在第i-1个结点后进行了后插数据元素(最后插入的元素就在第i个位置),因此可简化为:封装的好处


四.指定结点的前插操作:

1.实现方式一:传入头指针

单链表的链表指针只能往后寻找,因此只能后插,不能前插

->想要前插的话就必须引入头指针

该方式时间复杂度为O(n),因为有循环,较麻烦。

2.实现方式二:既然结点不能动,那就动数据元素

以下图为例,先申请一个新的结点,再把这个结点作为p结点的后继结点(就是p结点后面插一个新结点):

关键:既然结点不能动,那就动数据元素

把p结点的数据元素x复制到s结点中,再把要插入的元素放入e,

注:p结点和s结点这些只是名字,真正决定结点的是数据元素和存放的指针,

此时新申请的这块结点s(内部数据元素为x)就相当于p结点,存放e元素的结点就在p结点之前了,实现了前插操作

该方式时间复杂度为O(1),因为没有循环,较高效。

第二个版本:


五.按位序删除(带头结点):

1.思路:

删除表L中第i个位置的元素,要用到free函数,

再找到第i-1个结点,

然后修改第i-1个结点的指针,使其指向第i+1个结点。

2.图解:

3.代码实现:

#include<stdio.h>
#include<stdlib.h> 
​
​
//定义单链表结点类型
typedef struct LNode  
{
    int data; //每个结点存放一个数据元素
    struct LNode *next; //指针指向下一个结点      
}LNode,*LinkList;
​
​
//初始化一个单链表(带头结点)
bool InitList(LinkList &L)
{
    L = (LNode *)malloc( sizeof(LNode) ); //分配一个头结点
    if(L==NULL) //代表内存不足,分配失败-->意味着带头结点的单链表无法创建 
    {
        return false;
    }
    else
    {
        L -> next = NULL; //头结点之后暂时还没有节点,所以指向NULL
        return true; 
    } 
} 
​
​
//判断单链表是否为空(带头结点)
bool Empty(LinkList L)
{
    if(L->next==NULL) //头结点之后如果指向NULL,代表没有数据 
    {
        return true;
    }
    else
    {
        return false;
    }
} 
​
​
/*按位序删除(带头结点)
 ->插入元素之前已经初始化了一个空表,有头结点,此时L指向头结点(有数据元素和指向下一个结点的指针)*/
bool ListDelete(LinkList &L,int i,int &e)
{
    if(i<1)
    {
        return false; 
    }
    LNode *p; //指针p指向当前扫描到的结点
    int j=0; //当前p指向的是第几个结点:j为0代表头结点
    p=L; //L指向头结点,头结点是第0个结点(不存数据)
    while(p!=NULL && j<i-1) //循环找到第i-1个结点 
    {
        p = p->next;
        j++;
    }
    //循环结束后p为第i-1个结点 
    if(p==NULL) //p为NULL,代表第i-1个结点不存在,也就说明i值不合法 
    {
        return false;
    }
    if(p->next == NULL) //代表第i-1个结点之后已无其他结点 
    {
        return false;
    } 
    //令q指向被删除的结点 
    LNode *q = p->next;
    //用e返回被删除的元素的值 
    e = q->data;
    //将*q结点从链中"断开" 
    p->next = q->next;
    //释放结点的存储空间 
    free(q);
    //删除成功 
    return true;
} 
 
​
int main()
{
    //声明一个指向单链表的指针
    LinkList L;
    //初始化一个空表
    InitList(L); 
    return 0;
} 
图解:

i为4代表要删除第4个元素并返回,经过while循环p指向第三个元素,p->next指向第四个元素:

第二个if语句和第三个if语句明显不会走,之后把要删除的元素即p->next赋值给q,再把q的值即q->data返回给e:

注:变量e即此次要删除的结点的值最终要返回给ListDelete函数调用处,所以需要&(参数e为引用类型)

根据需求,要把第i+1个元素即q->next与第i-1个元素连接,也就是将*q结点从链中"断开",最后把删除的q释放:

时间复杂度:最好的时间复杂度就是i为1时(即删除第一个结点),不需要走while循环


六.指定结点的删除:

1.思路:

注:单链表的链表指针只能往后寻找,因此要删除的结点的前一个结点的指针就无法利用遍历找到,就需要别的方法:

2.实现方式一:传入头指针

3.实现方式二:偷天换日(类似于结点前插的实现)

p->next为q。题目需求是删除结点p,因此最终把一开始的p里的数据元素x删除即可,根据图里的一系列操作后,最终

数据元素x被删除,也就相当于没有一开始的p了。(p,q之类的就是个名字,真正要操作的是数据元素和内部的指针)

时间复杂度为O(1),因为没有循环。

4.极限情况:删除单链表里的最后一个结点

当执行p->data = p->next->data就会出错,因为p->next为q,而q里没有值即NULL(空指针),p->next->data就出错

-->所以上述代码有bug,即无法删除单链表里的最后一个结点

因此,如果要删除的结点是最后一个结点,

那么只能从表头开始依次寻找要删除的结点(最后一个结点)的前驱(使用传入头指针的方法),

时间复杂度为O(n)。


七.单链表的局限性:

无法逆向检索,有时候不太方便,甚至极限情况无法实现。


八.总结:


评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值