【数据结构】链表

一、链表初识

   什么是链表呢?链表是一种数据结构,就像数组,连续地存放一组数据,结构体,连续存放不同类型的一组数据,所以数据结构就是一种数据存放的思想,链表也一样。

   那我们为什么要用链表呢?举个例子,比如有一个数组,数组中有若干元素,如果我们要在数组中间增加或删除一个元素,那么我们就要挪动该元素以后的所有数组中的元素,这样的话内存的开销和运算量就会大大增加,这样的话就会十分不灵活。如果是链表的话,对数据的增删的难度就会大大降低。我们看一下为什么:

首先我们看一下链表的结构:
在这里插入图片描述

   链表其实就向一串珠子,把一串珠子串在一起就是把一组数据串在一起,在链表中我们把每个珠子叫做节点,每个节点就是一个结构体,这个结构体包含两项,一项是存放数据的常量,另一项是指向下一个节点的指针,如上所示。



当我们增加数据的时候,我们只需要修改增加处节点的指针指向,让它指向增加的元素,然后再把增加元素的指针指向它的下一个节点,再将原来两个节点的指针的节点断开,就完成了数据的增加,如下图所示:
在这里插入图片描述

当我们删除数据的时候,我们只需要把要删除的节点之前的节点指针指向它的下一个的下一个元素,再把要删除节点的指针断开,就可以完成数据的删除,见下图:

在这里插入图片描述


   通过以上介绍,我们可以看出链表对于增加和删除元素根本不涉及后方元素的移动,所以就没有较大的内存开销,这样操作起来更灵活,更方便,效率更高。这便是链表的优点。


二、链表与数组的区别与实现

   上面我们说到,数组可以存放一组连续的数据,它是这样实现的:

#include<stdio.h>

int main()
{
        int i = 0;
        int array[] = {1,2,3};
        for(i = 0;i<sizeof(array)/sizeof(array[0]);i++){
                printf("%d ",array[i]);
        }
        putchar('\n');

        return 0;
} 

那如何用链表存储这三个数据呢?简单定义如下:

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

int main()
{
        struct Test t1 = {1,NULL};
        struct Test t2 = {2,NULL};
        struct Test t3 = {3,NULL};
        
        return 0;
}

下面将这三个结构体串在一起,即把 t 1 t1 t1 结构体的指针 n e x t next next 指向 t 2 t2 t2 结构体,把 t 2 t2 t2 结构体的指针 n e x t next next 指向 t 3 t3 t3 结构体:

		t1.next = &t2;
        t2.next = &t3;

最终实现链表的打印:( . . . 运算符的优先级高于 − > -> > 运算符的优先级)

		printf("use t1 to print three nums\n");
        printf("%d %d %d\n",t1.data,t1.next->data,t1.next->next->data);

使用数组和链表的方式打印出来的结果是一样的:

在这里插入图片描述



那么这两者在内存中实质上有什么区别呢?我们用一幅图来看看:
在这里插入图片描述很容易看出,数组存储数据的连续性,而链表在内存中都是随机存储的,而且对于链表来说,拿到一个链表的头,就可以遍历整个链表。


三、链表静态添加和动态遍历

   说到数据的添加,对于数组,由于数组的大小固定,肯定是无计可施,唯一可行的办法就是重新申请一个适合大小的数数组,这当然就要产生很大的内存开销。而对于链表而言,就略胜一筹了,只需要再定义一个结构体,再将原本链表尾部指针指向这个结构体就可以了。接着以上的代码,我们再添加一个 t 4 t4 t4 节点来保存 4 4 4 这个数据:

	struct Test t4 = {4,NULL};
	t3.next = &t4;

接着打印:

	printf("%d %d %d %d\n",t1.data,t1.next->data,t1.next->next->data,t1.next->next->next->data); 

运行结果:
在这里插入图片描述

以上就是链表数据的增加,当时这样写显然很麻烦,并不能体现其高效,方便的特性,下面我们写一种算法来动态遍历链表:

#include<stdio.h>

void printLink(struct Test *head)
{
        struct Test *point;
        point = head;
        while(point != NULL){
                printf("%d ",point->data);
                point = point->next;
        }
        putchar('\n');
}
int main()
{
        struct Test t1 = {1,NULL};
        struct Test t2 = {2,NULL};
        struct Test t3 = {3,NULL};
        struct Test t4 = {4,NULL};

        t1.next = &t2;
        t2.next = &t3;
        t3.next = &t4;

        printf("use t1 to print three nums\n");
        // printf("%d %d %d %d\n",t1.data,t1.next->data,t1.next->next->data,t1.next->next->next->data); 
        printLink(&t1);

        return 0;
}

在这里插入图片描述

以上就实现了链表的动态遍历。(这部分所有代码和函数都接本节内容以上全部代码)


四、统计链表节点个数及链表查找

统计链表节点个数函数:

int getLinkTotalNodeNum(struct Test *head)
{
        struct Test *point;
        point = head;
        int cnt = 0;
        while(point != NULL){
                cnt++;
                point = point->next;
        }
        return cnt;
}

链表查找函数:

int searchLink(struct Test *head, int data)
{
        struct Test *point;
        point = head;
        while(point != NULL){
                if(point ->data == data){
                        return 1;
                }
                point = point->next;
        }
        return 0;
}

应用例程:

int main()
{
        int ret;
        struct Test t1 = {1,NULL};
        struct Test t2 = {2,NULL};
        struct Test t3 = {3,NULL};
        struct Test t4 = {4,NULL};
        struct Test t5 = {5,NULL};
        struct Test t6 = {6,NULL};

        t1.next = &t2;
        t2.next = &t3;
        t3.next = &t4;
        t4.next = &t5;
        t5.next = &t6;

        printf("use t1 to print three nums\n");
        // printf("%d %d %d %d\n",t1.data,t1.next->data,t1.next->next->data,t1.next->next->next->data); 
        printLink(&t1);
        printf("%d",getLinkTotalNodeNum(&t1));
        putchar('\n');
        ret = searchLink(&t1, 1);
        if(ret == 1){
                printf("exist 1\n");
        }el
        {
                printf("no exist 1\n");
        }
        ret = searchLink(&t1, 8);
        if(ret == 1){
                printf("exist 8\n");
        }else
        {
                printf("no exist 8\n");
        }

        return 0;
}

运行结果:
在这里插入图片描述
(这部分所有代码和函数都接本节内容以上全部代码)


五、链表从指定节点后方插入新节点

在这里插入图片描述

看上图,若要在一串链表中插入一个新的数据我们可以分为三步走:

  1. 找到要插入数据的节点 2 2 2
  2. 将要插入的节点 5 5 5 指向 3 3 3
  3. 将前节点 2 2 2 指向新节点 5 5 5

注意:第二步和第三步不能互换,因为如果互换,可能会使节点 3 3 3 的地址丢失。

编程实现如下:

int insertFromBehind(struct Test *head, int data, struct Test *new)
{
        struct Test *point = head;
        while(point != NULL){
                if(point->data == data){
                        new->next = point->next;
                        point->next = new;
                        return 1;
                }
                point = point->next;
        }
        return 0;
}
struct Test
{
        int data;
        struct Test *next;
};
int main()
{
        int ret;
        struct Test t1 = {1,NULL};
        struct Test t2 = {2,NULL};
        struct Test t3 = {3,NULL};
        struct Test t4 = {4,NULL};
        struct Test t5 = {5,NULL};
        struct Test t6 = {6,NULL};

        t1.next = &t2;
        t2.next = &t3;
        t3.next = &t4;
        t4.next = &t5;
        t5.next = &t6;

        struct Test new = {100,NULL};

        printf("use t1 to print three nums\n"); 
        printLink(&t1);
        puts("after insert behind:");
        insertFromBehind(&t1, 3, &new);
        printLink(&t1);
        
        return 0;
}

(这部分所有代码和函数都接本节内容以上全部代码)
运行结果:
在这里插入图片描述



六、链表从指定节点前方插入新节点

   上面提到再后方插入新节点,同时也可以再前方插入新节点,但是由于在前方插入可能涉及到头节点的改变,所以这就需要涉及到两种情况:

  • 在原来第一个节点前插入:(头节点要发生变化)

在这里插入图片描述
这种情况插入过程可以分为以下步骤:

  1. 找到头节点。
  2. 将新节点 101 101 101 指向原来的头节点 1 1 1
  3. 将头节点改为 101 101 101
  • 不在原来第一个节点前插入:(头节点不发生变化)
    在这里插入图片描述
    这种情况插入过程分为以下步骤:
  1. 找到被插入节点 101 101 101
  2. 将新节点 103 103 103 指向被插入节点的后一个节点 1 1 1
  3. 将被插入节点 101 101 101 指向新节点 103 103 103

编程实现:

struct Test* insertFromForward(struct Test *head, int data, struct Test *new)
{
        struct Test *point = head;
        if(point->data == data){
                new->next = head;
                return new;
        }
        while(point->next != NULL){
                if(point->next->data == data){
                        new->next = point->next;
                        point->next = new;
                        printf("insert ok\n");
                        return head;
                }
                point = point->next;
        }
        printf("no this data: %d\n",data);
        return head;
}
int main()
{
        int ret;
        struct Test t1 = {1,NULL};
        struct Test t2 = {2,NULL};
        struct Test t3 = {3,NULL};
        struct Test t4 = {4,NULL};
        struct Test t5 = {5,NULL};
        struct Test t6 = {6,NULL};

        t1.next = &t2;
        t2.next = &t3;
        t3.next = &t4;
        t4.next = &t5;
        t5.next = &t6;

        struct Test *head = &t1;

        struct Test new = {100,NULL};
        struct Test new1 = {101,NULL};
        struct Test new2 = {103,NULL};

		printf("use t1 to print three nums\n");
        printLink(head);
        puts("after insert behind:");
        insertFromBehind(head, 6, &new);
        printLink(head);
        puts("after insert behind:");
        head = insertFromForward(head, 1, &new1);
        printLink(head);
        puts("after insert behind:");
        head = insertFromForward(head, 2, &new2);
        printLink(head);

		return 0;
}

(这部分所有代码和函数都接本节内容以上全部代码)
运行结果:

在这里插入图片描述



七、链表删除指定节点

   有增加必有删除,那么对于删除,由于可能涉及到链表头节点的变化,我们同样需要考虑两种情况:

  • 删除第一个节点:(改变头节点)

在这里插入图片描述
这种情况删除可以分为以下步骤:

  1. 找到头节点。
  2. 将头节点改为头节点的下一个节点。

  • 删除第一个节点以外的其他节点
    在这里插入图片描述
  1. 找到要删除的节点 2 2 2
  2. 将被删除节点的前一个节点 1 1 1 指针指向要删除节点 2 2 2 的下一个节点 3 3 3

编程实现:

struct Test* deleteNode(struct Test *head, int data)
{
        struct Test *point = head;
        if(point->data == data){
                head = head->next;
                return head;
        }
        while(point->next != NULL){
                if(point->next->data == data){
                        point->next = point->next->next;
                        return head;
                }
                point = point->next;
        }
        return head;
}

int main()
{
        int ret;
        struct Test t1 = {1,NULL};
        struct Test t2 = {2,NULL};
        struct Test t3 = {3,NULL};
        struct Test t4 = {4,NULL};
        struct Test t5 = {5,NULL};
        struct Test t6 = {6,NULL};

        t1.next = &t2;
        t2.next = &t3;
        t3.next = &t4;
        t4.next = &t5;
        t5.next = &t6;

        struct Test *head = &t1;

        struct Test new = {100,NULL};
        struct Test new1 = {101,NULL};
        struct Test new2 = {103,NULL};

        printf("use t1 to print three nums\n");
         printLink(head);
        puts("after delete:");
        head = deleteNode(head, 6);
        printLink(head);

		return 0;
}

(这部分所有代码和函数都接本节内容以上全部代码)
运行结果:

在这里插入图片描述

注意:$free$ 只能释放被 $malloc$ 开辟的空间,静态创建的链表不能释放。



八、更改链表节点(类似于查找节点,不细分析)

函数代码:

int changeLink(struct Test *head, int data, int new)
{       
        struct Test *point;
        point = head;
        while(point != NULL){
                 if(point->data == data){
                        point->data = new;
                        return 1;
                 }
                 point = point->next;
         }
         return 0;
}



九、动态创建链表之头插法

  之前我们都是静态创建链表,现在我们需要学会动态地去创建链表,而且这种方法在以后最为常用,首先介绍动态创建链表:
在这里插入图片描述
通过这幅图我们可以总结以下头插法创建链表的过程:

  1. 如果头节点为空,则直接把待插入节点作为头节点。
  2. 将新节点 2 2 2 指向原头节点 1 1 1
  3. 将新节点 2 2 2 作为头节点。
  4. 将新节点 3 3 3 指向 头节点 2 2 2
  5. 将新节点 3 3 3 作为头节点。

编程实现:

这部分为头插法创建节点的代码:

struct Test* insertFromHead(struct Test *head, struct Test *new)
{
                if(head == NULL ){
                        head = new;
                }else
                {
                        new->next = head;
                        head = new;
                }
        return head;
}

这部分为头插法创建链表的代码:

struct Test* createLink(struct Test *head)
{
        struct Test *new;
        while(1){
                new = (struct Test*)malloc(sizeof(struct Test));
                printf("enter your new Node:\n");
                scanf("%d",&(new->data));
                if(new->data == 0){
                        printf("0 quit!\n");
                        free(new);
                        return head;
                }
                head = insertFromHead(head, new);
        }
}
int main()
{
        struct Test *head = NULL;
        printLink(head);
        head = createLink(head);
        printLink(head);
        
        return 0;
}

(这部分所有代码和函数都接本节内容以上全部代码)
运行结果:

在这里插入图片描述


十、动态创建链表之尾插法

  尾插法作为一种动态创建链表的方式,也很常用,我们这里和头插法一样,去分析一下它的实现过程:
在这里插入图片描述
通过这副图我们可以分析以下尾插法创建链表的过程:

  1. 如果头节点为空,则直接将新节点作为头节点。
  2. 不断遍历链表,直到找到尾节点 1 1 1
  3. 将尾节点 1 1 1 的指针指向新节点。

编程实现:

这部分为尾插法创建节点的代码:

struct Test* insertBehind(struct Test *head, struct Test *new)
{
        struct Test *p = head;
        if(p == NULL){
                head = new;
                return head;
        }else
        {
                while(p->next != NULL){
                        p = p->next;
                }
                p->next = new;
                new->next = NULL;
        }
        return head;
}

这部分为尾插法创建链表的代码:

struct Test* createLink2(struct Test *head)
{
        struct Test *new;
        while(1){
                new = (struct Test*)malloc(sizeof(struct Test));
                printf("enter your new Node:\n");
                scanf("%d",&(new->data));
                if(new->data == 0){
                        printf("0 quit!\n");
                        free(new);
                        return head;
                }
                head = insertBehind(head, new);
        }
}
int main()
{
        struct Test *head = NULL;
        printLink(head);
        head = createLink2(head);
        printLink(head);
        
		return 0;
}

(这部分所有代码和函数都接本节内容以上全部代码)
运行结果:
在这里插入图片描述

一辈子很短,努力的做好两件事就好;第一件事是热爱生活,好好的去爱身边的人;第二件事是努力学习,在工作中取得不一样的成绩,实现自己的价值,而不是仅仅为了赚钱;

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

IT阳晨。

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值