链表专题

链表专题

计算机世界里,数据的存储分为两大类:顺序存储和链式存储。其中顺序存储最典型的就是数组,而链式存储最典型的就是链表。
链表是C语言中非常重要同时也是较难的一个知识点,后面讲栈和队列的时候都要用到链表的知识,所以一定要掌握。
一、数组的缺陷
第一,数组是连续的,所以在内存中需要一整块连续的空间。如果存储的数据很大,而内存中如果没有连续的、空闲的这么大的内存空间,内存分配就会失败,这样数组定义也就失败了。内存中一般也很难有那么大、整块、空闲的区域。
第二,虽然数组的存取速度很快,但是数组插入和删除元素的效率很低。因为数组是连续的,所以如果要插入一个元素,那么插入点后所有的元素都要往后移;而如果要删除一个元素,所删除元素后面所有的元素都要往前移。假如数组有1万个元素,要在第一个元素后面插入数据,那么后面9999个元素都要往后移;而如果删除第一个元素,后面9999个元素都要往前移,这就导致了数组插入和删除元素效率极低。
第三,用数组存放数据时,必须事先定义固定的长度,如果事先难以确定数组的长度,就必须将数组定义得足够大,这样显然会浪费很多内存。
二、链表的优点
第一,链表不需要连续地分配内存空间,每个元素之间不是连续而是分开的,所以不需要很大的一整块内存空间。而且这样也能够最大限度地使用内存中的零散存储单元。
第二,使用链表,插入和删除元素的效率很高。链表中各元素之间通过指针进行连接,各元素之间可以任意删除不需要的元素或插入新的元素,只需要更改一下删除点或插入点处指针变量的指向即可。比如要删除第二个元素,那么只要使第一个元素指向第三个元素就可以了;又比如要在第二个元素和第三个元素之间插入一个新元素M,那么只要使第二个元素指向M,然后M指向第三个元素即可。删除和插入元素都不需要移动其他元素。
第三,链表可以根据需要开辟内存空间,不需要像数组那样预先指定所要分配的内存空间的长度,而是现用现分配。
三、链表的缺点
由于链表中每一个元素都多包含一个地址,或者说是多包含了一个存放着地址的指针变量,所以相对而言会占用比较多的内存。此外,由于链表中各个元素在内存中不是连续存放的,所以要找到其中的某一个元素,必须先找到该元素的上一个元素,然后根据上一个元素提供的下一个元素的地址才能找到该元素。所以如果不提供“头指针”,那么整个链表都无法访问。如果元素很多,每个元素都要从第一个元素开始查找,所以效率就会很低。

链表相关术语
链表中每一个元素称为“结点”,每个结点都包括两个部分,一部分是“用户需要的实际数据”,另一部分是“下一个结点的地址”。
链表有一个“头指针”变量,它存放一个地址,该地址指向“头结点”,头结点中不存放数据,只存放第一个元素的地址。链表从第一个元素开始存放数据,第一个元素称为“首结点”,首结点中又存放第二个元素的地址... ...,直到最后一个元素。最后一个元素不再指向其他元素,称为“尾结点”。尾结点的地址部分存放一个“NULL”,表示“空地址”,链表到此结束。NULL在链表程序中往往作为链表结束的判断标志
综上所述,链表相关术语如下:
  头指针:存放头结点地址的指针变量。
  头结点:头结点是首结点前面的那个结点;头结点的数据类型和首结点的类型是一样的;头结点并不存放不效数据;设置头结点的目的是为了方便对链表进行操作。
  首结点:存放第一个有效数据的结点。
  尾结点:存放最后一个有效数据的结点。
  确定数组需要两个参数,一个是数组名,一个是数组的长度。
  确定链表只需要一个参数:头指针,头指针只是一个指针变量,里面存放着一个地址,千万不要同链表的结点混为一谈。
  链表主要分为四种:单向链表、双向链表、单向循环链表、双向循环链表。
  如果链表最后指向的不是NULL,而是头结点或首结点,那么整个链表就如同一个环,这样的链表称为循环链表。

#include <stdio.h>
#include <stdlib.h> //使用malloc和exit必须包含此头文件

struct NODE
{
    int data;
    struct NODE *next;
};

struct NODE *CreateLink(void);         //创建链表
void OutputLink(struct NODE *head);    //输出链表

int main(void)
{
    struct NODE *head;                 //定义头结点
    head = CreateLink();
    OutputLink(head);

    getchar();
    return 0;
}

//创建链表
struct NODE *CreateLink(void)
{
    int len = 0, i;                     //存放链表有效结点的个数。
    int val;                            //临时存放用户输入的结点的值

    struct NODE *head = malloc(sizeof * head);   //注释1
    struct NODE *move = head;                         //注释2
    move->next = NULL;                                   //注释3
    if (NULL == head)
    {
        printf("分配内存失败,程序终止!\n");
        exit(-1);
    }
    printf("请输入要生成的链表结点个数:len = ");
    scanf("%d", &len);
    for (i=0; i<len; i++)
    {
        struct NODE *fresh = malloc(sizeof * fresh); //循环一次创建一个新结点
        if (NULL == fresh)
        {
            printf("分配内存失败,程序终止!\n");
            exit(-1);
        }
        printf("请输入第%d个结点的值: ", i+1);
        scanf("%d", &val);
        fresh->data = val;     
        move->next = fresh;       //注释4
        fresh->next = NULL;       //注释5
        move = fresh;                //注释6
    }
    return head;
}

//输出链表
void OutputLink(struct NODE *head)
{
    struct NODE *move = head; //永远不要试图移动头指针,要用另外一个指针代替它移动
    while (move->next != NULL)   //注释7
    {
        printf("%d -> ", move->next->data);
        move = move->next;
        if (NULL == move->next)
            printf("NULL");
    }
    printf("\n");

    /*
        也可以这样写:
        move = move->next;  (或 move = head->next)
        while (move != NULL)
        {
            printf("%d -> ", move->data);
            move = move->next;
        }
    */
}

程序总结:
    链表是通过结构体实现的,但是程序中不管是头指针、头结点、首结点还是其他任何结点,其实定义的都是结构体指针。整个链表都是通过指针实现的,对链表的操作其实就是指针操作。
   注释1:定义头指针,用于存放头结点的地址。它是一个指向 struct NODE 类型变量的指针变量,要将它与结构体变量区分开,它只是存储了结构体变量的地址,是一个指针变量,而不是一个结构体变量。在定义指针的同时动态分配头结点内存空间,并使head指向该内存空间的首地址,即 head 存放了头结点的地址。
    注释2:定义一个指向 struct NODE 类型变量的指针变量 move。它用于将所有结点连接起来。它要从头结点开始依次指向所有结点。刚开始将它初始化为指向头结点,目的是用它将头结点和首结点连起来。然后依次使它指向每一个结点,从而实现将所有结点依次连接起来的目的。
    注释3:头结点指向初始化,即此时头结点和其他结点之间还没有指向关系。
    注释4:头结点中的指针成员存放首结点的地址(首地址,即首结点中第一个成员的地址),此时头结点和首结点就连接起来了,且此时 move->next 就是 head->next中也存放 首结点的首地址。本句一定要注意:刚开始的时候 move 是头指针,它指向的是头结点。 move->next 表示的是引用头结点中的 next 成员。现在将 fresh 的地址赋给 move->next ,则指向首结点的是头结点,而不是头指针。头指针和首结点之间还有一个没有存放任何数据除了首结点地址的头结点。从全局来看, move->next = fresh 的功能是从头结点开始,将链表中的所有结点连接起来。每循环一次就将新建的结点和上一个结点连起来,而头指针始终指向首结点。
    注释5:首结点指向初始化---无指向,这点很重要,不可省略。该语句的目的并不仅仅在于给每一个结点初始化,最主要在于使链表最后一个结点指向NULL,用以表示链表结束。
    注释6:指针变量move的指向开始发生变化,由指向头结点变为指向首结点,为连接首结点和第二个结点作准备。
    注释7:move->next 中最初存放着首结点的首地址,通过循环就可以使之依次指向链表中的每一个结点。
    exit(0)表示正常退出, exit(-1)表示非正常退出。

链表是通过结构体实现的,但是在程序中不管是头指针、头结点、首结点还是其他任何结点,其实定义的都是结构体指针。整个链表都是通过指针实现的。对链表的操作其实就是指针操作。包括后面要学到的栈和队列,它们的本质都是操作指针:栈中是一个指针不停地移动,队列中是两个指针不停地移动。

 

 

 

 

 

 

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值