连这些都不知道都不敢说自己会链表

本文深入浅出地介绍了链表的概念,通过比喻帮助读者理解链表的本质——内存管理方式。作者从数据结构的角度出发,阐述了链表的节点定义、初始化、释放及如何创建链表。通过代码示例展示了链表的头结点定义、尾插法和头插法增加结点的过程,同时提到了其他链表操作如删除、反转等作为进阶话题。文章适合初学者了解链表的基本概念和操作。
摘要由CSDN通过智能技术生成

全网最详细最容易懂的链表

我一上大二,学校就给我塞来一本数据结构。c语言学得还是一脸懵,一接触数据结构人直接傻了,老师一上来全是概念理论。确实是这样,教数据结构的老师都会默认学生们学c学得不错,一上来就是,这个这样那个怎样,说完就叫你去理解一份链表代码然后就是模仿,根本不会说一种数据结构的核心在哪里,一种数据结构怎么去形成,写出一种数据结构的逻辑思维在哪里,怎么着手去完成一份数据结构代码。

链表的趣味之路

到底什么是链表。书上的说法:用一组任意的存储单元存储线性表的数据元素(这组存储单元可以是连续的,也可以是不连续的)。因此,为了表示每个数据元素ai与其直接后续数据元素ai+1之间的逻辑关系,对数据元素ai来说,除了存储其本身的信息之外,还需存储一个指示其直接后续的信息(即直接后续的存储的位置)。这两部分信息组成数据元素ai的存储映像,称为结点(node)。它包括两个域:其中存储数据元素信息的域称为数据域;存储直接后续存储位置的域称为指针域。指针域存储的信息称为指针或链。n个结点链结成一个链表。

似乎看懂了,但还是一点模糊。看懂就是知道有那么几个节点,然后连起来。我老师也是这样说,还在黑板上画了一个图,,也做了一个火车车厢的例子,确实懂了,不就是几个块连起来嘛。怎么把几个结点连起来是不是好像也有点知道,但是好像又不知道,知道是用指针连起来,我老师也跟我说用某个结点用指针指向另一个结点,确实明白了,不就好像我拿着一个棍子指针你嘛。完了。这就是链表了。链表核心就这样讲完了。道理我们都懂,看也看会了,听也听会了,但是代码怎么去表示链表呢?

其实不是,链表的本质根本不是连接,而是某种或者某个数据在内存的管理方式。

我一开始学c时,老师就叫我用某种数据类型(比如int)定义一个变量。数据类型存在的意义就是申请内存,一个int 32位,一个long long 64位等等,背得很溜,这不就是内存大小吗?有一个变量我们就可以把数据存在这个变量里面,注意这是一个变量,一个变量能干嘛,一个是不是只能存一个且一种数据,但是如果我要存储多个数据呢?怎么办?这时候很多人可以想到数组。OK。我main函数内定义一个数组,定多大呢?在main函数里面定义变量申请的内存是栈里面的还是堆里面的呢?函数里面定义是不是就是栈里的,那一个栈里面最大的内存有多大呢?其实不知,但有一个标准栈叫做系统栈,系统栈最大内存为8MB,也就是200万个整型数据,大于200万则爆栈,但是要是我想定义一千万个呢,栈里面放不下一千万个内存,但堆里面可以啊,在内存保证下,可以不断地向堆里面申请内存。一个数组,内存不断申请,这是什么,这不就是一个顺序表吗?如果你说数组是顺序表,我还有驳回的余地,为什么,如果一个数组一出生就被决定了大小,就像你一出生就被规定只能有一米高,什么感受,这明显失去了数据存储的意义。数据存储的意义是什么?比如一本英语字典,我要把这本字典全部单词解释存到计算机内存中,我是不是有一个单词就存一个,而不是一下说明只能存多少个。

注意啊,我不是在讲顺序表。上面涉及到顺序表只是因为顺序表和链表只有一个差别,就是顺序表申请的内存都是连续的,什么是连续。连续和不连续的区别就好像我家一片田,这一片都是我家的,这是连续;而不连续就好像我家在城南有一块田,城北有一块田,地球上我家这也有一块田,那也有一块田。连续的好说,连续的只要知道这片田有多大,从哪开始就知道哪里是我家的田。但是不连续呢?我站在我家城南这块田上,怎么才能知道我在城北还有一块田,那就得在城南的这块田里面插个牌子,这个牌子就写着我家城北的田在哪里,也就是我城北的地址,这样我站在城南这块田上就能顺着地址跑到城北的那块田那里了,然后城北这块田上也写着下块田的地址,我又可以顺着地址跑到下块田,当跑到某个田上面没有写地址说明这块田是我家的最后一块田了。好,总结一下,我是从城南这块田开始顺着地址跑的,是不是只要知道城南这块田在哪我就可以顺着往下跑,但是我怎么知道城南这块田在哪里呢,我就是不知道,当时我一来就跟国家土地资源局要一份说明,表明城南这块地是我家的,所以只要城南的说明就可以。

写了好多字,我也不知道写了什么?如果看了,就当看着玩的,那就直接来代码操作。

最简单的链表形成之路

哦哦哦,到代码操作了。打开编辑器写完stdio头文件写完main函数,接下来我该干什么,我要干什么来着。哦哦哦,我要写链表,那链表是什么来自,忘了。

来,记住下面创建链表的步骤:结点定义->结点初始化->结点释放->定义头结点->尾插法增加结点 (附头插法,中间插法)

结点定义:为什么要定义结点,我就想问,结点是什么东西,如果没有数据结构,在c语言里面有结点这个东西吗?结点不就是上面说的一块田吗,不就是一块内存吗,只不过这块田上还有一块牌子,这个牌子上写着地址吗,c里面用什么数据类型定义变量存储地址,不就是指针类型吗?所以结点不就是一块田加一块牌子嘛?c语言里面居然没有结点这个东西,但c里面有一个办法可以构造一个不存在的东西,那就是结构体,所以这第一步涉及c语言中的结构体(知识点:结构体构造、指针类型存的是地址、数据类型规定一块内存大小)。

struct Node{
    int data;//田的大小,内存的大小
    struct Node* next;//next就是一个牌子,里面存的是地址,为什么要用struct Node*,struct Node是结点这个数据类型的名称,因为存的这个地址的空间大小也是结点的大小,所以要用结点来定义;
}//为方便给结构体换个名,以后代码用名称Node
typedef struct Node{
    int data;
    struct Node* next;
} Node;

结点初始化:定义结点只是定义,就好像单方面在八种基本数据类型中加一个结点数据类型。这种数据类型用来定义一个变量时,一般都会在某个函数里面定义,也就是在某块栈里面定义一个变量,上面说过,栈里面的容量是有限的,所以要在堆里面申请内存。记住每个结点都要初始化。

Node *n = (Node *) malloc(sizeof(Node));//malloc函数里面有向堆里面申请内存的功能,其参数是申请内存的大小,其成功会返回一个无类型指针指向分配的地址,这个指针就是存着这个段内存。可以这样理解:首先Node *n为定义一个Node结点数据类型的变量n,这个变量存的是地址,malloc(sizeof(Node))返回这个指针因为是无类型指针,所以要(Node *) malloc(sizeof(Node)) 强类型转换才能用。(malloc函数需要头文件stdlib.h)
int val = 1;
n->data = val;//给结点赋值val,存入真实数据
n->next = NULL;//给田上的牌子留白,就是说明下这是最后一块田

结点释放:因为上面申请了内存,不用时就要释放,这是江湖规则,如果不受规则就会间接影响程序

free(n);

**定义头结点:**这步就是上面说的更国土局要一个说明,标明城南那块地是我家的。定义完头节点就算是一个有结点链表了。如果要定义一个空链表,就是直接Node *head = NULL;就可以

Node *head = (Node *) malloc(sizeof(Node));
head->data = 1;
head->next = NULL;

链表完整代码:

#include<stdio.h>
#include<stdlib.h>
typedef struct Node{
    int data;
    struct Node *next;
} Node;

int main() {
    Node *head = (Node *) malloc(sizeof(Node));
    head->data = 1;
    head->next = NULL;
    return 0;
}

尾插法增加结点

#include<stdio.h>
#include<stdlib.h>
typedef struct Node{
    int data;
    struct Node *next;
} Node;

int main() {
    Node *head = (Node *) malloc(sizeof(Node));
    head->data = 1;
    head->next = NULL;
    
	//创建第一个新结点
    int val = 2;
	Node *t = (Node*) malloc(sizeof(Node));
    t->data = val;
    t->next = NULL;
    //把这个新结点插在链表的最后面
    head->next = t;
    
    //创建第二个新结点
    val = 3;
	Node *t = (Node*) malloc(sizeof(Node));
    t->data = val;
    t->next = NULL;
    //把这个新结点插在链表的最后面
    head->next->next = t;
    
    
    //创建第三个新结点
    val = 4;
	Node *t = (Node*) malloc(sizeof(Node));
    t->data = val;
    t->next = NULL;
    //把这个新结点插在链表的最后面
    head->next->next->next = t;
    return 0;
}

头插法增加结点:

#include<stdio.h>
#include<stdlib.h>
typedef struct Node{
    int data;
    struct Node *next;
} Node;

int main() {
    Node *head = (Node *) malloc(sizeof(Node));
    head->data = 1;
    head->next = NULL;
    
	//创建第一个新结点
    int val = 2;
	Node *t = (Node*) malloc(sizeof(Node));
    t->data = val;
    t->next = NULL;
    //把这个新结点插在链表的最后面
    t->next = head;
    t = head;//把新结点设为头结点
    
    //创建第二个新结点
    int val = 3;
	Node *t = (Node*) malloc(sizeof(Node));
    t->data = val;
    t->next = NULL;
    //把这个新结点插在链表的最后面
    t->next = head;
    t = head;//把新结点设为头结点
    
    
    //创建第三个新结点
    int val = 2;
	Node *t = (Node*) malloc(sizeof(Node));
    t->data = val;
    t->next = NULL;
    //把这个新结点插在链表的最后面
    t->next = head;
    t = head;//把新结点设为头结点
}

某位置插入结点:此处引用头插的代码,尾插也可以,只是为了得到一个多结点的链表

#include<stdio.h>
#include<stdlib.h>
typedef struct Node{
    int data;
    struct Node *next;
} Node;

int main() {
    Node *head = (Node *) malloc(sizeof(Node));
    head->data = 1;
    head->next = NULL;
    
	//创建第一个新结点
    int val = 2;
	Node *t = (Node*) malloc(sizeof(Node));
    t->data = val;
    t->next = NULL;
    //把这个新结点插在链表的最后面
    t->next = head;
    t = head;//把新结点设为头结点
    
    //创建第二个新结点
    int val = 3;
	Node *t = (Node*) malloc(sizeof(Node));
    t->data = val;
    t->next = NULL;
    //把这个新结点插在链表的最后面
    t->next = head;
    t = head;//把新结点设为头结点
    
    
    //创建第三个新结点
    int val = 4;
	Node *t = (Node*) malloc(sizeof(Node));
    t->data = val;
    t->next = NULL;
    //把这个新结点插在链表的最后面
    t->next = head;
    t = head;//把新结点设为头结点
    
    
    //创建一个新结点
    int val = 6;
	Node *t = (Node*) malloc(sizeof(Node));
    t->data = val;
    t->next = NULL;
    
    int idx = 2;//插入位置
    //先跑到要插入位置的前一个结点
    Node *p = head;//用一个指针来跑
    idx--;
    while (idx--) {
		p = p->next;
    }
    //然后插入
    t-next = p->next;
    p->next = n;
}

上面已经完成一个链表的形成,下面将是加强版,也就是经过函数包装和增加管理结点。

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


typedef struct Node {
    int val;
    struct Node *next;
} Node;

typedef struct List {
    Node *head;
    int len;
} List;

//初始化结点
Node *initNode(int data) {
    Node *n = (Node *) malloc(sizeof(Node));
    n->val = data;
    n->next = NULL;
    return n;
}
//释放结点
void freeNode(Node *n) {
    if (!n) return ;
    free(n);
    return ;
}

//初始化链表
List *initList() {
    List *l = (List *) malloc(sizeof(List));
    l->head = initNode(1);
    l->len = 0;
    return l;
}

//释放结点
void freelist(List *l) {
    if (!l) return ;
    Node *p = l->head;
    while (p) {
        Node *k = p;
        p = p->next;
        free(k);
    }
    free(l);
    return ;
}

//插入
int insertNode(List *l, int idx, Node *n) {
    if (!l) return 0;
    if (idx < 0 || idx > l->len + 1) return 0;
    Node *p = (l->head);
    while (idx--) {
        p = p->next;
    }

    n->next = p->next;
    p->next = n;
    l->len++;
    return 1;
}
int insertList(List *l, int idx, int val) {
    Node *n = initNode(val);
    if (insertNode(l, idx, n) == 0) {
        freeNode(n);
        return 0;
    }
    return 1;
}

int main() {
    srand(time(0));
    List *l = initList();
    int cnt = 10;
    while (cnt--) {
        int val = rand() % 100;
        int i = insertList(l, 0, val);

    }
    
    Node *p = l->head;
    while (p) {
        printf("%d : %p\n", p->val, p);
        p = p->next;
    }
    return 0;
}

对于删除结点、反转链表、循环链表、双向链表、环形链表等等其他算法操作在此就不进行详解,只要了解了链表的本质其他算法其实都是概念理论问题

总结

链表:多块内存 -> 一个块内存管理下一块内存

在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

扑天鹰

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

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

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

打赏作者

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

抵扣说明:

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

余额充值