✅简介:与大家一起加油,希望文章能够帮助各位!!!!
保持学习、保持热爱、认真分享、一起进步!!!
前言
一、动态数组的弊端
二、单链表
1.链表的优势
2.什么是链表
3.如何定义一个链表
3.1底层代码逻辑
3.2链表元素的插入、删除操作
4.如何让我们在读入数据时降低时间开销
总结
上篇文章写了顺序表(数组),虽然我们现在可以创造出来动态增加的数组,但是动态数组还是有一系列的问题:
缺点一:我们的数组需要连续的整片的空间,而我们计算机中没有这么大的空间。
如下图:图中我们可以清晰的看到当第三次申请空间的时候,计算机明明有这么多空间但是却申请失败,因为数组需要连续不间断的空间来存放数据。
第二个缺点就是当我们扩容的时候,我们需要遍历之前的数据进行拷贝,浪费时间。当数据规模不大的时候,可以忽略。但是当数据达到一定程度后就会出现拷贝整个数组可能需要较长的时间,对性能会产生一定的影响。
动态性:链表是一种动态数据结构,可以在运行时灵活地插入、删除和修改元素,而不需要预先指定容量。这意味着链表可以根据需要动态地增长或缩小,提供更好的灵活性。
内存管理:链表的节点可以在内存中非连续地分布,每个节点可以存储任意数量的数据。这种内存分配方式可以更有效地利用内存空间,并且可以避免由于连续内存分配导致的碎片化问题。
链表是一种常见的线性数据结构,它由一系列的节点组成,每个节点包含两部分:数据和指向下一个节点的指针。每个节点可以在内存中不连续地分布,通过指针将它们串联在一起。下面呢,进入正题!!!
首先呢,我们先看这张图!这是带头节点,带头节点的链表是在链表头部添加了一个额外的节点,该节点不存储任何数据,只是作为链表的起始位置。带头节点的链表中,头节点的下一个节点才是实际存储数据的第一个节点。
不带头节点的主要区别就是在于第一个的问题,不带头结点首先定义一个同类型的指针,指向第一个存放元素的地址值,直接从存储数据的第一个节点开始。这里呢,因为不带头节点的代码更麻烦一点,所以我们主要以不带头节点的链表进行讲解!
首先,定义一个结构体(链表)
typedef struct _abc {
struct _abc* next;//指向下一个
int value;
} link;
现在,我们拥有了一个个的节点,该怎样去把他们去串在一起呢?
看完下面的代码,如果你学过数据结构的话不难看出这段代码的时间复杂度O(),怎样去降低这个时间的耗费呢?自己可以先停下来思考一下,后面会提到呦!!!
link* head = NULL; //定义头指针
int num;
while (true){
printf("请输入链表中的值:");
link* l = (link*)malloc(sizeof(link));
scanf("%d", &num);
if (num == -1) //如果num==-1结束循环
break;
l->value = num;
l->next = NULL;
link* k = head;
if (k) {//如果k=NULL进入
while (k->next) {
k = k->next;
}
k->next = l; //循环结束,链表走到了最后一个位置,我们需要把新增加的元素加上去
} else {
head = l;//如果head==NULL的话(第一次读入数据),则让head指向l的地址
}
}
遍历链表!!! 千万要记得malloc得到的空间一定要还回去,每次循环就是把指向地址的指针后移,读出数据。
for (link* p = head; p; p = p->next) {
printf("%d\t", p->value);
}
printf("\n");
//释放资源
list* li = p->head;
while (li != NULL) {
list* temp = li;
li = li->next;
free(temp);
}
数组要插入一个元素:首先我们需要把这个元素插入位置后面所有的元素后移一位,然后再增加进去。而链表就不需要这么麻烦:
- 创建一个新的节点,并将要插入的元素存储在该节点中。
- 将新节点的指针指向原本插入位置上的节点,即将新节点连接到链表中。
- 将插入位置前一个节点的指针指向新节点,即将前一个节点与新节点连接起来。
如下图:
void inBehind(arr* m, int locat, int value) {
if (locat < 0)
printf("输入有误!\n");
if (locat == 0) {
list* l = (list*)malloc(sizeof(list));
l->value = value;
l->next = m->head;
m->head = l;
} else {
list* p = m->head;
for (int i = 0; i < locat - 1; p = p->next, i++); //因为函数头指向了第一个list所以这里需要-1
list* l = (list*)malloc(sizeof(list));
l->value = value;
l->next = p->next;
p->next = l;
}
}
数组删除一个元素通常需要将被删除元素后面的所有元素向前移动一位,以填补被删除元素的位置。这样,就完成了元素在数组中的删除操作。相对于插入操作,数组的删除操作时间复杂度通常也是O(n),因为需要移动后续元素。
链表删除一个元素的步骤如下:
- 找到要删除的节点的前一个节点(如果是双向链表,还需找到后一个节点)。
- 将要删除的节点从链表中断开,即调整前一个节点(和后一个节点)的指针连接关系。
这样,就完成了元素在链表中的删除操作。相对于数组,链表的删除操作只需要修改节点之间的指针,时间复杂度通常是O(1)。
void del(arr* m,int locat){
list* p=m->head;
if(locat<1)
printf("输入有误!\n");
if(locat==1){
list* a=p->next;
free(p);
m->head=a;
}else{
list* a=p;//让a指向p的后面
list* b=p->next;//b指向p的前面
for(int i=0;inext;
p=p->next;
if(i==locat-2)
continue;
a=a->next;
}
//循环后,p指向了要删除的地址
a->next=b;
free(p); //释放资源
}
}
如果是插入或者删除数据在第一的元素的话需要我们单独讨论,而封装成函数的话,这时候我们需要改变head指针的指向,如果我们传入的只是head的话,是改变不了的head指针的指向的。在函数中修改指针指向的内容,不会对指针本身产生影响。如果你想修改指针本身的值(如指向不同的地址),你需要传递指针的指针或使用返回值将修改后的指针返回给调用函数。
避免重复遍历:在每次插入节点时,可以记录链表的尾部节点,避免每次都从头部开始遍历链表找到尾部节点。这样可以减少遍历的次数,提高效率。
typedef struct _list {
struct _list* next;
int value;
} list;
typedef struct {
list* head;//链表头部
list* behind;//链表尾部
} arr;
上面定义一个struct结构体,一个指向链表头部,一个指向链表尾部。这样每次写入数据可以减少遍历的次数。时间复杂度为O(n)。
arr a;
arr* p = &a;
p->behind = NULL;
p->head = NULL;
printf("请输入添加的数据:");
while (true) {
int num;
scanf("%d", &num);
if (num == -1) break;
list* q = (list*)malloc(sizeof(list));
q->value = num;
q->next = NULL;
if (p->head == NULL) {
p->head = q; //刚开始没有数据,让链表的头和尾的地址都指向q
p->behind = q;
} else {
p->behind->next = q;
p->behind = q;
}
}
这篇文章主要讲述了单链表的一些操作,代码里面重要的地方也有注释的。
以后也会持续更新!!!
成功不是将来才有的,而是从决定去做的那一刻起,持续累积而成。
以上均是个人的理解,如果有不对的地方请各位大佬帮忙斧正!