带头结点的单链表

文章介绍了带头结点的单链表的创建、有序插入、翻转和删除操作。头结点包含指向链表首尾的指针,简化了操作。插入时按顺序找到合适位置;翻转链表需注意避免形成循环;删除操作涉及不同情况的处理,如删除首节点、尾节点或中间节点。
摘要由CSDN通过智能技术生成

       头节点

        前面把不带头结点的单链表的基本功能都搞得差不多了,那么我们就发现一个很尴尬的事情,那就是我们通过对链表的了解之后,我们发现对于链表的创建其实是一个需要认真分析的事情,我们在创建链表的时候要去考虑表是不是为空啊,头指针要注意不能随便动啊,等等,那么如果一个链表带上了头结点之后我们就可以统一这些操作了

        我们先来了解一些头结点这个小玩意,首先顾名思义,头结点他本身就是一个结点,我们在对于普通结点来说是搞一个结构体啊,然后结构体包括了指针域和数据域,数据域存放下一个结点的地址。那么头结点呢?头结点可就不是这样了,当然我们还是要搞个结构体,那么结构体里面有哪些属性呢?其实头结点的功能很多,但是最最基本的我们往里面放一个指针,一个永远指向第一个结点的指针!

typedef struct HeadNode
{
    node* first;//永远指向链表第一个结点
    node* last;//永远指向链表最后一个结点
}head;

        除了头结点,我们还知道头指针这个东西,在没有头结点之前,头指针永远指向第一个结点,那么我们都说了,头结点也是结点一个特殊的结点,自然头指针就永远指向头节点了,在之前我们要遍历链表,就得知道第一个结点在哪里,明显啊,结点都是存储下一个结点的地址,那第一个结点的地址自然就没有人有啊,我们之前就是通过头指针来记住他,这样我们在插入的时候特别是头插的时候,头指针就得动来动去,最后还要返回头指针,平常搞搞可能还不太麻烦,可是设计到一些其他的复杂一些的操作就很不方便了。那么有了头结点之后呢,我们只需把头节点当成参数,只需要他一个结点,就可以找到整个链表了。

        链表的有序创建(插入)

        为什么我们管这个也叫插入呢,因为我们有了头节点之后,我们就可以利用头节点先去创建一个空链表,什么意思呢?按照我们上面头节点的结构体来看,这个时候我们的头节点里面有两个指针域,一个永远指向头,一个永远指向尾,创建一个空链表就是让这两个指针同时指向空就ok了,在这里我们返回了这个头节点,那么从现在开始,我们所有的操作都是基于头节点来搞。

head* creat_TSlist(void)
{
    head* l =(head*) malloc(sizeof(*l));
    l->first = NULL;
    l->last = NULL;
    return l;
}

        我们就先搞个正序插入吧,大家一听正序,这不就是swap函数麻,直接搞,但是我们其实会发现,swap是事先准备好了两个数然后开始比较,可是我们的链表中的结点是one by one的,所以要考虑特殊情况,而且,由于比较之后得交换你还得记住他们得地址,和前一个得地址,(所以在链表这一块来说,我们把逻辑理清楚,代码才写的明白,而理清逻辑最好用得方法就是把图给他画清楚了)而且链表怎么交换位置,这可不是顺序表,我们修改链表得顺序的时候是通过修改他们前面or后面的结点所储存的地址来实现的,在这里我们就明确了一个东西,我们必须先找到要插入的地方,然后再来插入。

        怎么找插入地点

        怎么去找那个地方呢?新来一个结点,我要把他放进去,那么我就拿这个新来的结点的数据域去和链表里面所有的节点去比较,当找到一个比我大的数据的时候,我就停止,并且返回这个节点,为了方便,我们设置一个pk指针,这个指针一开始指向链表的第一个节点,也就是pk = l->first,这样可以通过遍历比较出来,然后再返回pk就ok了。

while(pk)
    {
        if(pk->data > cur->data)
        {
            break;
        }
        pr = pk;
        pk = pk->next;
    }

        如何插入

        找到了之后如何插入呢?我们现在来分情况讨论,新来的节点定义为cur

1、假设第一个节点就比新来的大,那么返回pk,这个时候我们这个cur是不是就应该放在链表的首个,也就是说现在头节点里面存的东西要变一下了, l->first = cur;然后再让cur指向pk:cur->next = pk,这样就ok了。

2、假设链表都遍历完了,我还是没有找到比cur大的节点,那意思就是cur是这个链表里面最大的一个了,此时我们返回pk,接着让头节点里面的last变一下, l->last = cur;然后让pk指向cur:pk->next = cur

3、假设我们链表还没有遍历完,在中间找到了,怎么办呢?

 if (pk == NULL)
    {
        pr->next = cur;//l->last->next = cur;
        l->last = cur;
    }
    else
    {
        if (pk == l->first)
        {
            l->first = cur;
            cur->next = pk;
        }
        else
        {
            pr->next = cur;
            cur->next = pk;
        }
        
    }

这就是这三种情况了,所以pk这个东西是很重要的

        翻转链表

        翻转链表啥意思呢,就是把一个12345的链表输出成54321,怎么搞呢,还得是画图,逻辑是什么呢?其实就是把大家的指针域里面的东西全部拿出来换一换,原来头节点里面指向1的现在要指向5,原来1里面放了2的地址,2里面放了1的地址现在就反过来,但是我们千万千万要注意一个点,就是单链表最后一个节点的指针域是存放的NULL,所以我们不能单纯的原来的第一个指向空

 同样的我们来分情况讨论

1、当链表为空的时候,也就是l->first = NULL,我们直接返回空

if(l->first == NULL)
    {
        return l;
    }

2、当链表里面有多个节点的时候(大于1),我们就要慢慢来了,假设1234吧,第一步,按照我们上面说的,先让第一个指向空1->next = NULL;然后2->next = 1;依此类推,为了实现这个步骤呢,我们就先设置两个指针,n1,n2来指向他们俩

 node* n1;
 node* n2;
n1 = l->first;
 n2 = n1->next;
n1->next = NULL;//这一步非常关键兄弟们
    /*
        很明显我们做完1之后发现n1和n2形成了双向循环,原因就是没有把n1指向n2断开啊
        (所以就会先输出n3->n2->n1->n2->n1这样一直循环下去),
        所以我们只需要在循环前直接让n1指向空就完事了
        (如果你是在循环中断开的话,那么每次都会断开,那就会少一个数字了)
    */
n2->next = n1;
        n1 = n2;
       

所以要搞三个指针

 node* n1;
    node* n2;
    node* n3;
    
    n1 = l->first;
    
    n2 = n1->next;
    n3 = n2->next;
    n1->next = NULL;//这一步非常关键兄弟们
    /*
        很明显我们做完1之后发现n1和n2形成了双向循环,原因就是没有把n1指向n2断开啊
        (所以就会先输出n3->n2->n1->n2->n1这样一直循环下去),
        所以我们只需要在循环前直接让n1指向空就完事了
        (如果你是在循环中断开的话,那么每次都会断开,那就会少一个数字了)
    */
    while(1)
    {
        
        n2->next = n1;
        n1 = n2;
        n2 = n3;
        n3 = n3->next;
    }

这个时候问题来了,我们要一直往后面迭代啊,但是我们现在2存的是1的地址了啊,也就说2已经和3没有任何关系了,那我怎么往后面走啊,所以我们直接设置一个n3去记住后面那个节点,我们发现走到最后两个数的时候,也就是n2指向5的时候,这个时候n3已经是NULL了,但是我们没有把4的地址存到5的指针域里面去,所以还得循环,可是循环就要走 n3 = n3->next;这一步,明显就是对空指针的引用了,已经错了,但其实我们发现,当n3指向NULL的时候,我们是不是已经走到最后一步了,也就是说n2已经不需要往后面移动了,那么n3的历史意义就结束了,所以无所谓n3了,那么我们只需要在每次存地址之前判断一下n3是不是空,如果是空我们就不需要动n3了

 if(n3 == NULL)
        {
            n2->next = n1;
            n1 = n2;
            n2 = n3;
            l->first = n1;
            return l;
        }

3、当链表里面只有一个节点的时候,这个时候,这是什么情况呢?就是n1->next == NULL,也就是n2指向空的时候,那直接打印就行了,一个数字你翻转个鬼

        基于上面三种情况,完整函数:

head* Convers(head*l)
{
    node* n1;
    node* n2;
    node* n3;
    if(l->first == NULL)
    {
        return l;
    }
    n1 = l->first;
    if(n1->next == NULL)
    {
        return l;
    }
    n2 = n1->next;
    n3 = n2->next;
    l->last = n1;//虽然说啊翻转用不上尾,但是如果先翻转后删除就得把尾搞好!其实本来也应该把尾把搞好,不然first和last都指向翻转后的第一个了
    n1->next = NULL;//这一步非常关键兄弟们
    /*
        很明显我们做完1之后发现n1和n2形成了双向循环,原因就是没有把n1指向n2断开啊
        (所以就会先输出n3->n2->n1->n2->n1这样一直循环下去),
        所以我们只需要在循环前直接让n1指向空就完事了
        (如果你是在循环中断开的话,那么每次都会断开,那就会少一个数字了)
    */
    while(1)
    {
        if(n3 == NULL)
        {
            n2->next = n1;
            n1 = n2;
            n2 = n3;
            l->first = n1;
            return l;
        }
        n2->next = n1;
        n1 = n2;
        n2 = n3;
        n3 = n3->next;
    }

 前面说过了我们这是带头节点的所以最后一定要记得把头节点里面的first指过去,因为走到最后的时候,n1是指向5的,所以我们把n1赋给first就行了 
        l->first = n1;
       记得是在最后一步执行这个,因为n1是在不断变化的,

         删除单链表中某个值

同样的我们分情况讨论(怎么找到那个数就不说了)

1、链表为空,直接返回

2、要删除的在第一个,且链表只有一个节点

if(l->first == l->last )//要删除的数在第一个,且链表只有1个节点
            {
                l->first = NULL;
                l->last = NULL;
                free(px);
                px = NULL;
            }

3、要删除的在第一个,但是链表不止有一个节点

 else//要删除的数字在第一个,但是链表中不止一个节点
            {
                l->first = px->next;
                px->next = NULL;
                free(px);
                px = NULL;
            }

4、要删除的在最后一个

else if(px == l->last)
        {
            pr->next = NULL;
            l->last = pr;
            free(px);
            px = NULL;
        }

5、要删除的在中间

else
        {
            pr->next = px->next;
            px->next =NULL;
            free(px);
            px = NULL;
        }

怎么断开怎么连接,想必都是非常的清楚了,我们重点来讲一下为什么他可以做到删除所有的,我们如果想要删除一个接着去找下一个数值怎么做呢?首先我们知道删除一个怎么搞,就是先去遍历链表把他找出来,然后去看他属于上面5种情况的哪一种,好这是一次。这样就完成了一次删除,明显的,如果链表中还有,我们迭代一次就ok了。就是在外面在加个大的while(px),只要px不为null我就接着找,然后接着删除,但其实如果这样的话我们会把前面找过的节点再找一遍(因为你遍历每次都要把px = l->first,所以px每次都是从头开始遍历),这样就效率不高,那么我们是不是可以在找到px的时候记录一下pf =px->next,然后在删除px之后,我们再px = pf,这样下次就会是从px的下一个开始找,这样就不需要从头开始遍历了。

        基于上面所说的,完整的函数

void delete_all_x(head *l,ElemType x)
{
    node* pr,*pf ;
    node* px = l->first;
    while (px)
    {
        while (px)
        {
            if(px->data == x)
            {
                break;
            }
            pr = px;
            px = px->next;
        }
        
        if(px == NULL)//单链表为空
        {
            return ;
        }
        pf = px->next;
        if(px == l->first)
        {
            if(l->first == l->last )//要删除的数在第一个,且链表只有1个节点
            {
                l->first = NULL;
                l->last = NULL;
                free(px);
                px = NULL;
            }
            else//要删除的数字在第一个,但是链表中不止一个节点
            {
                l->first = px->next;
                px->next = NULL;
                free(px);
                px = NULL;
            }
        }
        else if(px == l->last)
        {
            pr->next = NULL;
            l->last = pr;
            free(px);
            px = NULL;
        }
        else
        {
            pr->next = px->next;
            px->next =NULL;
            free(px);
            px = NULL;
        }
        px = pf;
    }
}

         以上就是带头节点的单链表,那么单链表在这里也就结束了,完整的代码我也还是放在gitcode上面吧,有需要的可以去看看。

HPAlways / 带头节点的单链表的各种操作 · GitCodeGitCode——开源代码托管平台,独立第三方开源社区,Git/Github/Gitlabhttps://gitcode.net/HPAlways/TSlist

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值