链表I(实现基本操作单链表)

顺序存储 VS 链式存储

数组—顺序存储

数组作为一个顺序储存方式的数据结构,可是有大作为的,它的灵活使用为我们的程序设计带来了大量的便利。
由于是顺序存储的,所以在查找和使用的过程中,运用指针和数字的加减即可定位一个数据,非常方便快捷而且简单易学。
但是,插入和删除的繁重却让我们很是头疼,我们来看:

在这里插入图片描述
我们很容易可以看出,要想在数组中插入或者删除某个数据,就得移动后面的所有数据,工作量很是庞大!!(想想都很头疼)。

链表—链式存储

那么有没有一种存储方式可以很快的插入和删除呢?我们的链式存储就引进来啦!!想象一下,如果我们可以把所有的数据拿一根线穿起来,就像下图一样:

在这里插入图片描述
当我们要在两个数据中间插入的时候,把这跟线剪断,然后放一个新的数据进去,再把线连起来,我们就可以完美的得到一串新的数据了,是不是很棒!!

在这里插入图片描述

由此我们可以像下图一样定义一个新的数据结构,在DATA里我们存放数据,在NEXT里我们单向穿起两边的数据,这样我们就可以实现把数据链式存储起来啦!

单链表

今天我们先来学习最简单的单链表,即只能单向指向的链表。
首先,我们按照上面的思路,定义链表的结构体。

定义结构体

其中,typedef关键字来定义自己习惯的数据类型名称;struct指的是结构体,亦被直接称为“结构”,需要用相关的不同类型的数据来描述一个数据对象;struct后面的linkNode指的是结构名。

//定义结构体
typedef struct LinkNode  //自定义一个结构,里头包含val(数据)和next(指针)
{
    int val;        //数据域,类型可以自定义
    struct LinkNode *next;  //指针域
} LinkNode;  //类型名称,名称也可根据喜好自定义

我们可以看到,在自定义的这个叫LinkNode的结构体里,有两个空间可以分别存放int型的数据val和一个用来指向下一个数据的struct LinkNode *类型的next指针。最后你可以把这个类型定义为Linknode,也可以不定义,但是千万别忘记分号’;’!!!

链表初始化

在单链表中,每个节点只包含一个指向链表下一个节点的指针。记住,链表最后一个节点要定义为NULL,来提示链表后不再有其他节点。

我们的思路是这样的:建立一个头节点来指向下面整一串的数据,这样如果要在绳的头端做文章(比如插入或者删除)就可以让头节点指向的数据变动啦!(要不然头节点一个人可是操作不了的哦,因为next指针是单向指向的,是不可以回头的)。

//链表初始化
LinkNode *CreateHeadNode()
{
    LinkNode *head = (LinkNode*)malloc(sizeof(LinkNode));  //建立一个头节点
    head->next = NULL;   //让头节点的下一个节点为空,即头节点指向NULL
    return head;   //返回头节点,头节点创建完毕
}

这里简单提一下malloc()函数,用于动态内存申请,其函数原型为void *malloc(unsigned int size),因为链表在内存上不是连续的空间(这也就是为什么不能用指针+数字访问内存的一个原因),所以要用动态内存申请来开辟一个小空间来存放自己想存放的东西。

插入

首先我们可以定义一个新的节点来存放要插入的那个数,这样插入的时候就看上去清晰一些然后操作也会简单一点。

定义新节点

//定义新节点
LinkNode *CreateNewNode()
{
    int data;
    scanf("%d", &data);  //插入一个数
    LinkNode *cur = (LinkNode *)malloc(sizeof(LinkNode));  //定义一个节点来存放它
    cur->val = data;    //存放这个值
    cur->next = NULL;
    return cur;
}

我们有两者方式插入,分别是头插法和尾插法

头插法

顾名思义在头端插入,当然我们在有头节点的情况下是每次都在头节点的下一个节点插入。头插法的核心,就是让新节点的下一个节点等于head的下一个节点,再让head的下一个节点等于新节点完成插入,更直白点说,就是让新节点指向head的下一个,head指回新节点。
代码如下:

//头插法
LinkNode *InsertHeadList(LinkNode *head)
{
    int n;
    printf("Please input the number:\n");  //输入要插入多少个数字
    scanf("%d", &n);
    printf("Please input the data:\n");  //提示一下底下将要输入插入的数字
    while(n--)
    {
        LinkNode *cur = CreateNewNode();  //调用刚刚我们定义好的新节点函数
        /*头插法核心*/
        cur->next = head->next;    
        head->next = cur;
    }
    return head;
}

我们看一下图片将会更加清晰的认识到头插法,因为每一个新节点会插入在上一个节点前,所以最后输出的时候会是倒着输出的,并且我们需要考虑是否需要头节点。
在这里插入图片描述

尾插法

同样,尾插法即在尾端插入,新插入的节点指向NULL。尾插法的核心,就是用把链表先遍历到最后一个节点,再让新节点插在最后一个节点的后面。代码如下:

//尾插法
LinkNode *InsertTailList(LinkNode *head)
{
    int n;
    printf("Please input the number:\n");   //输入要插入多少个数字
    scanf("%d", &n);
    LinkNode *cur = head;
    while(cur->next)
        cur = cur->next;   //遍历到链表的尾部插入
    printf("Please input the data:\n");  //提示一下底下将要输入插入的数字
    while(n--)
    {
        LinkNode *insert = CreateNewNode();  //调用刚刚我们定义好的新节点函数
        /*尾插法核心*/
        cur->next = insert;
        insert->next = NULL;
        cur = insert;
    }
    return head;
}

下图我们也可以清晰的看到,和头插法不同的是尾插法可以正着输出,而且不用考虑是否有头节点,但是需要遍历到尾部,会消耗一定时间。
在这里插入图片描述

指定位置删除

你以为,链表遍历到那个节点就可以非常爽快的直接删除,还十分心情舒畅的想着“就这?”,真的是这样吗?NoNoNo,你确实可以直接free这个节点,但是它的前后该怎么连接呢?所以,我们得开辟一个空间(假设是pre)(当然,如果你的头节点没有存放数据,那么pre也可以直接指向head),让它指向整个链表然后跟着遍历链表的指针(假设是cur)后头跑,当cur指向的数据是我们要删除的数据,那么pre就登场了,让pre指向cur的下一个指针,就可以开辟出一条绕过cur的新线了,此时再free掉cur,就可以无后顾之忧的删掉这个数据啦!

//删除
LinkNode *DelectNode(LinkNode *head)
{
    int del;
    printf("Please input the del val:\n");
    scanf("%d", &del);  //输入待删除的数据
    LinkNode *pre = head;  //因为我建立的投节点没有存放数据,所以开辟的新空间就可以是head
    while(pre->next)
    {
        if(pre->next->val==del)  //这里我只让pre指针遍历了,意思和上面描述的是一样的
        {
            LinkNode *cur= pre->next;
            pre->next = pre->next->next;  //相当于让pre指向cur的下一个
            free(cur);
        }
        else pre = pre->next;
    }
    return head;
}

下面的图很清晰的表达了删除的意思:
在这里插入图片描述

反转链表

我们以前学习过的数组可以用两个指针分别从头和尾同时向中间缩进,然后交换指向的值进行反转。但是链表不可以往回走,所以效仿数组是行不通了。
我们可以使用迭代法,先开辟一个新的节点(假设是newhead)让其为NULL,然后用一个节点(假设是cur)遍历数组,让每一个节点都往回指像newhead,再让newhead和cur都往后继续走,就可以一个接一个完成反转啦!最后如果一开始我们有头节点的话,可以在最后让头节点指向newhead(这就是为什么一开始我没有让head=NULL然后让它跑的原因),如果没有头节点就直接返回newheaad即可。
在这里插入图片描述
代码如下:

//反转链表
LinkNode *ReserveList(LinkNode *head)
{
    LinkNode *newhead = NULL;
    LinkNode *cur = head->next;
    while(cur)
    {
        LinkNode *next = cur->next;  //保存一下xur的下一个节点,到时候cur转向了过后就找不回来了
        cur->next = newhead;
        newhead = cur;
        cur = next;   //如果不保存就不能往后遍历了对吧
    }
    head->next = newhead;  //头节点现在是无用的,把它利用起来让头节点指向新节点
    return head;
}

链表排序

排序我们也有两种方法,初级版的只交换值,升级版的交换整个节点

交换值

就和数组的排序一样,非常简单,就不细说了,直接上代码:

LinkNode *SortList(LinkNode*head)
{
    LinkNode *p=head,*r;
    int t;
    while(p->next)  //一层循环固定待交换的值
    {
        r = p->next;  //相当于j=i+1
        while(r->next)  //二层循环遍历链表
        {
            if(p->next->val>r->next->val)
            {
                t = p->next->val;
                p->next->val = r->next->val;
                r->next->val = t;
            }
            else  //一定要加else!!!因为交换过后r很可能已经在NULL的位置了,此时r后已经无节点了会报错
                r = r->next;
        }
        p = p->next;
    }
    return head;
}
    

交换节点

顾名思义是将两个节点交换而不是简单的交换值。我们可以让要交换的两个值的各自的前一个节点进行操作。代码如下:

//排序
LinkNode *SortList(LinkNode *head)
{
    LinkNode *p = head;
    LinkNode *q;
    while(p->next)
    {
        q = p->next;
        while(q->next)
        {
            if(p->next->val>q->next->val)  //交换的是p的下一个节点和q的下一个节点
            {
                LinkNode *next1 = p->next;  //便于分清是哪个位置
                LinkNode *next2 = q->next->next;
                p->next = q->next;
                p->next->next = next1; //如果让p->next->next=q会有些看不明白,新存放一个next1便于理解
                q->next = next2;
            }
            else 
            	q = q->next;  //一定要加else!!!因为交换过后r很可能已经在NULL的位置了,此时r后已经无节点了会报错
        }
        p = p->next;
    }
    return head;
}

合并两个升序链表

既然我们都实现了合并,不如再加大点难度,让两个升序的链表合并,使合并过后依旧是一个升序链表。这里我们可以开辟一个新节点start,用来当作新链表的头节点,然后遍历两个链表,哪个节点更小就让那个节点连在新链表后面,然后那个链表的指针往后移继续遍历。如果一个遍历完了另一个还没完,那就很简单了,直接把没完的接在新链表后头就行了嘛!
在这里插入图片描述
代码如下:

//合并两个有序链表
LinkNode *Semerge(LinkNode *head1,LinkNode *head2)
{
    LinkNode *start=(LinkNode*)malloc(sizeof(LinkNode));
    LinkNode *cur = start;  //用于遍历新链表
    start->next = NULL;
    head1 = head1->next;
    head2 = head2->next;
    while(head1&&head2)
    {
        if(head1->val<head2->val)  //哪个小,哪个就连在新链表后面,并且那个链表往后继续遍历继续比较
        {
            cur->next=head1;
            head1 = head1->next;
        }
        else
        {
            cur->next = head2;
            head2 = head2->next;
        }
        cur = cur->next;
    }
    if(!head1)  //一个为空,新链表后面直接连接顶一个链表
    {
        cur->next = head2;
    } 
    else 
    {
        cur->next = head1;
    }
    return start;
}

判断链表是否为空

代码已经很浅显易懂啦!

//判断链表是否为空
void IsEmptyList(LinkNode *head)
{
    if(!head)   //!head和head==NULL是一个意思
        printf("Yes Empty!\n");  
    else
        printf("No Empty!\n");
}

打印链表

和你想的一样,就是全部遍历一遍然后一个一个输出。

//打印
void PrintList(LinkNode *head)
{
    LinkNode *cur = head->next;
    while(cur->next)
    {
        printf("%d->", cur->val); //这里为了美观,加入了->
        cur = cur->next;
    }
    printf("%d\n", cur->val);
}

完整代码

最后附上总代码:
总代码的注释比较少,所以如果有不懂的一定要回上去看看详细讲解呀!

#include<stdio.h>
#include<stdlib.h>
#include<malloc.h>

//定义结构体
typedef struct LinkNode
{
    int val;
    struct LinkNode *next;
} LinkNode;

//定义头节点
LinkNode *CreateHeadNode()
{
    LinkNode *head = (LinkNode*)malloc(sizeof(LinkNode));
    head->next = NULL;
    return head;
}

//定义新节点
LinkNode *CreateNewNode()
{
    int data;
    scanf("%d", &data);
    LinkNode *cur = (LinkNode *)malloc(sizeof(LinkNode));
    cur->val = data;
    cur->next = NULL;
    return cur;
}

//头插法
LinkNode *InsertHeadList(LinkNode *head)
{
    int n;
    printf("Please input the number:\n");
    scanf("%d", &n);
    printf("Please input the data:\n");
    while(n--)
    {
        LinkNode *cur = CreateNewNode();
        cur->next = head->next;
        head->next = cur;
    }
    return head;
}

//尾插法
LinkNode *InsertTailList(LinkNode *head)
{
    int n;
    printf("Please input the number:\n");
    scanf("%d", &n);
    LinkNode *cur = head;
    while(cur->next)
        cur = cur->next;
    printf("Please input the data:\n");
    while(n--)
    {
        LinkNode *insert = CreateNewNode();
        cur->next = insert;
        insert->next = NULL;
        cur = insert;
    }
    return head;
}

//删除
LinkNode *DelectNode(LinkNode *head)
{
    int del;
    printf("Please input the del val:\n");
    scanf("%d", &del);
    LinkNode *pre = head;
    while(pre->next)
    {
        if(pre->next->val==del)
        {
            LinkNode *cur= pre->next;
            pre->next = pre->next->next;
            free(cur);
        }
        else pre = pre->next;
    }
    return head;
}

//反转
LinkNode *ReserveList(LinkNode *head)
{
    LinkNode *newhead = NULL;
    LinkNode *cur = head->next;
    while(cur)
    {
        LinkNode *next = cur->next;
        cur->next = newhead;
        newhead = cur;
        cur = next;
    }
    head->next = newhead;
    return head;
}

//排序
LinkNode *SortList(LinkNode *head)
{
    LinkNode *p = head;
    LinkNode *q;
    while(p->next)
    {
        q = p->next;
        while(q->next)
        {
            if(p->next->val>q->next->val)
            {
                LinkNode *next1 = p->next;  //便于分清是哪个位置
                LinkNode *next2 = q->next->next;
                p->next = q->next;
                p->next->next = next1;
                q->next = next2;
            }
            else q = q->next;
        }
        p = p->next;
    }
    return head;
}

//合并
LinkNode *Semerge(LinkNode *head1,LinkNode *head2)
{
    LinkNode *start=(LinkNode*)malloc(sizeof(LinkNode));
    LinkNode *cur = start;
    start->next = NULL;
    head1 = head1->next;
    head2 = head2->next;
    while(head1&&head2)
    {
        if(head1->val<head2->val)
        {
            cur->next=head1;
            head1 = head1->next;
        }
        else
        {
            cur->next = head2;
            head2 = head2->next;
        }
        cur = cur->next;
    }
    if(!head1)
    {
        cur->next = head2;
    } 
    else 
    {
        cur->next = head1;
    }
    return start;
}

//判断链表是否为空
void IsEmptyList(LinkNode *head)
{
    if(!head)
        printf("Yes Empty!\n");
    else
        printf("No Empty!\n");
}

//打印
void PrintList(LinkNode *head)
{
    LinkNode *cur = head->next;
    while(cur->next)
    {
        printf("%d->", cur->val);
        cur = cur->next;
    }
    printf("%d\n", cur->val);
}

int main(void)
{
    LinkNode *head1 = CreateHeadNode();
    LinkNode *head2 = CreateHeadNode();
    LinkNode *newhead = CreateHeadNode();
    printf("Input the first list:\n");
    head1 = InsertHeadList(head1);
    PrintList(head1);
    printf("\n");
    printf("Input the second list:\n");
    head2 = InsertTailList(head2);
    PrintList(head2);
    printf("\n");
    printf("Delect\n");
    head1 = DelectNode(head1);
    PrintList(head1);
    printf("\n");
    printf("Is empty?\n");
    IsEmptyList(head1);
    printf("\n");
    printf("Reserve\n");
    head1 = ReserveList(head1);
    PrintList(head1);
    printf("\n");
    printf("Sort1!\n");
    head1 = SortList(head1);
    PrintList(head1);
    printf("\n");
    printf("Sort2!\n");
    head2 = SortList(head2);
    PrintList(head2);
    printf("\n");
    printf("Semerge!\n");
    newhead=Semerge(head1, head2);
    PrintList(newhead);
    printf("\n");
    system("pause");
    return 0;
}

总结

学习方法

我当时学链表课看了两遍书翻了三遍,确实是很难呀(也可能是我脑子笨哈哈哈哈)。但是我总结下来几个非常有效的学习链表的方法:

  • 一定要画图!画图!画图!!!重要的事情多说几遍,画图真的可以很好的让我们看懂到底是怎么操作链表的,然后自己写题的时候思路也会更加清晰。
  • 要多刷几遍基础题,链表的基础题真的不是很多,但是一定要熟练掌握,后期的很多操作都是在基础题上延申的,量不在多而在精嘛。
  • 一遍两遍学不会不要着急,逐字逐句的看书看博客,一遍一遍加深印象和概念。
  • 学链表前可以简单学习一下结构体,对理解有帮助。

注意事项

还有几个注意事项:

  • 函数到底返回什么,要不要返回,都要想清楚。
  • 关于头指针到底要不要新存放值,对接下来的调用和返回都有很大影响,所以一定一定要想清楚自己定义的是什么,自己要用的是什么!!!!
  • 多看看别人定义的指针名和函数名(我就是看的少了到现在定义函数名都很菜嗐),尽量不要用拼音定义。

最后

  • 申明一下,以上图片分别摘自https://mp.weixin.qq.com/s/Igkt5pDi_icZ52uNxUXipQ和https://blog.csdn.net/xiaoxingxing1744/article/details/82753969
  • 关于节点还是结点,我所学习的书上是节这里我就都写节点了。
  • 如有错误或者建议和意见,敬请在评论区评论!!我一定会及时回复并改正的!
  • 接下来我会继续更新链表的相关知识,敬请期待吧~
  • 创作不易,点个⭐再走吧~
  • 3
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值