链式存储的优缺点:
优点:
1、动态分配内存:
链式存储不需要在数据插入之前分配固定大小的数组或内存块,因此它更适合存储动态变化的数据
2、高效的插入和删除操作:
在链表中插入或删除元素只需要调整相邻节点的指针,不需要移动大量数据,因此时间复杂度通常为O(1)(在已知位置时)或O(n)(在未知位置需要遍历链表时)。
3、节省内存:
链表中的每个节点只需要存储数据和指向下一个节点的指针,没有额外的空间浪费(如数组中的空闲空间)。
4、灵活性
链表可以以多种方式实现,如单向链表、双向链表、循环链表等,可以根据具体需求选择合适的结构。
缺点:
1、访问速度慢:
链表不支持像数组那样的随机访问,访问某个元素需要从头节点开始遍历,时间复杂度为O(n)。
2、额外的空间开销:
每个节点除了存储数据以外,还需要存储指针,增加了内存的开销。
3、内存碎片化:
链表中的节点可能在内存的不同位置分配内存,可能导致内存碎片化,影响性能。
4、管理复杂:
链表的管理(如内存分配和释放)相对复杂,容易出现内存泄露或指针错误等问题。
单向链表
单向链表(Singly Linked List)是一种链式存储结构的数据结构,它包含一系列节点(Node),每个节点都包含两个部分:数据域(存储节点的数据)和指针域(存储指向下一个节点的引用或指针)。单向链表的特点是链表中的节点只能单向遍历,即只能从链表的头部开始,依次访问每个节点,直到链表的尾部。
在我的链表中,我在定义时就给它一个规定:头节点不存数据,数据从头节点的下一个节点开始存储。
#ifndef _LINKLIST_H
#define _LINKLIST_H#include <stdio.h>
#include <stdlib.h>#define OUT(A) {printf("%c\n",A);}
typedef char Type; //为了方便修改存储数据的类型//约定头节点不存数据
typedef struct node{Type data; //节点中用于存储值struct node *next; //下一个节点的地址
}list; //声明了节点结构体类型//创建
list *create_link();
//判空
int null_link(list *l);
//求长度
int length_link(list *l);
//首插
void head_insert_link(list *l,Type data);
//尾插
void tail_insert_link(list *l,Type data);
//查找
list *search_link(list *l,Type data);
//更新
void updata_link(list *l,Type oldData,Type newData);
//删除
void delete_link(list *l,Type data);
//遍历
void traverse_link(list *l);
//初始化
void init_link(list *l);
//回收
void free_link(list **l);#endif
还是跟顺序表一样,使用多文件封装的形式来存储,在.h文件中定义结构体,结构体包括存数据的变量data和指向下一个结构体的指针next;在这里定义结构体的时候node不能省略,因为在结构体里面需要定义一个指向这个结构体类型的指针,因此需要用到这个结构体类型来定义指针。
#include "linklist.h"//创建
list *create_link()
{list *l = (list *)malloc(sizeof(list));if(NULL == l){perror("create malloc");return NULL;}l->next = NULL;return l;
}
//判空
int null_link(list *l)
{if(NULL == l){puts("listlink is NULL");return -1;}return l->next == NULL?0:1;
}
//求长度
int length_link(list *l)
{if(NULL == l){puts("listlink is NULL");return -1;}int n = 0;while(l->next){n++;l = l->next;}return n;
}
//尾插
void tail_insert_link(list *l,Type data)
{if(NULL == l){puts("listlink is NULL");return;}//找表尾while(l->next) //l->next != NULL{l = l->next;}//创建新节点list *p=(list *)malloc(sizeof(list));if(NULL == l){perror("tail_insert malloc");}p->data = data; p->next = NULL;//把节点连接到链表中l->next = p;
}
//首插
void head_insert_link(list *l,Type data)
{if(NULL == l){puts("listlink is NULL");return;}list *p = (list *)malloc(sizeof(list));if(NULL == l){perror("head-insert malloc");return;}p->data = data;p->next = l->next;l->next = p;}
//查找
list *search_link(list *l,Type data)
{if(NULL == l){puts("listlink is NULL");return NULL;}while(l->next){/*if(l->next->data == data){return l->data;l = l->next;}*/l = l->next;if(data == l->data)return l;}return NULL;
}
//更新
void updata_link(list *l,Type oldData,Type newData)
{if(NULL == l){puts("listlink is NULL");return;}while(l->next){l = l->next;if(oldData == l->data){l->data = newData;}}
}
//删除
void delete_link(list *l,Type data)
{if(NULL == l){puts("listlink is NULL");return;}
#if 1while(l->next){if(l->next->data == data){list *p = l->next;l->next = p->next;free(p);p = NULL;//continue; //用else不用continue}else l = l->next;}
#elif 0list *p = l;while(l->next){if(data != l->data)p = l;l = l->next;if(data == l->data){list *q = l;l = l->next;p->next = l;free(q);break;}}/*if(data == l->data) //注释break,加上最后的if为全删{p->next = NULL;free(l);}*/
#endif
}
//遍历
void traverse_link(list *l)
{
#if 0printf("head->");while(l->next){printf("%c->",l->next->data);l = l->next;}puts("NULL");
#elif 1list *p = l->next;while(p){printf("%c ",p->data);p = p->next;}puts("");
#endif
}
//初始化
void init_link(list *l)
{if(NULL == l){puts("listlink is NULL");return;}
#if 1while(l->next){list *p = l->next;l->next = p->next;free(p);}
#elselist *p = l->next;while(p){list *n = p;p = p->next;free(n);}l->next = NULL;
#endif
}
//回收
void free_link(list **l)
{if(NULL == *l){puts("listlink is NULL");return;}while((*l)->next){list *p = (*l)->next;(*l) = p->next;free(p);}*l = NULL;
}
创建:list *create_link()
创建之后我们需要拿到头节点的地址,因此函数返回值为结构体指针类型,同样是在堆区开辟空间,因为约定了头节点不存数据,因此这里的data不用管,但是指向下一个节点的指针需要把它赋值为NULL,防止野指针出现。
判空:int null_link(list *l)
因为链表中有指向下一个节点的指针,但是要是链表为空,那么就只有一个头节点,头节点里面的指针指向的是NULL,因此我们可以通过判断头节点里面的指针指向的下一个节点是否为空来判断链表是否为空;空函数返回0,非空返回1。
求长度:int length_link(list *l)
单向链表的节点是通过指针连接在一起的,因此求长度只需要从头节点开始遍历到最后一个节点,遍历时使用一个变量计数,最后函数返回这个计数变量即可;循环条件为l->next,当循环到最后一个节点时,下一个为NULL,就会结束循环;在循环里每次让指针L往后移动一个节点。
尾插:void tail_insert_link(list *l,Type data)
尾插也就是在链表最后插入一个新的节点,首先我们传参传的是链表的头节点,因此需要先遍历找到链表的最后一个节点,然后用一个结构体指针来接收新创建节点的地址,这里也需要判断一下节点创建是否成功,创建成功之后让节点的data等于我们要添加的值,然后让新节点里面的指针指向NULL,最后还需要更改插入新节点之前链表最后一个节点中指针的指向,让这个指针指向我们新加入的节点。
头部插入(首插):void head_insert_link(list *l,Type data)
头插相比尾插要复杂一点,但是这里我们就不需要遍历链表了,因为我们拿到的参数就是头节点,首先也是需要创建新节点,然后给节点里的data赋值,然后让新节点里的指针next指向头节点的下一个,再让头节点的next指向我们新插入的节点;在这里头插函数中最后两条语句的顺序能更改,更改之后我们就无法正确将新节点的next指向头节点的下一个节点了。
查找:list *search_link(list *l,Type data)
查找只需要循环遍历链表,然后将需要查找的数据与节点中的data比较即可,只要找到这个data,就将该data所在节点的地址返回就OK啦。
更新:void updata_link(list *l,Type oldData,Type newData)
更新也就是将链表中某一个值修改为我们想改的值,传入的参数就有三个,表,原数据和更新的数据,也是通过循环遍历的方式来寻找我们需要更改的数据,找到之后直接修改即可,在代码中我的更新是将所有的要修改的那个原数据都改变,例如原来链表中有3个a,我要将a修改为b,那么就会将这三个a都修改为b,如果只想修改找到的第一个a的话就只需要在l->data = newData;语句后面加一个break,直接结束循环即可。
删除:void delete_link(list *l,Type data)
删除和插入是类似的操作,只不过一个是增加节点,另一个是删除节点,首先是将要删除的数据当做参数传入函数,然后通过遍历的方式来查找需要删除的节点,找到节点之后用一个指针来指向这个节点,然后就是断链,将与它相连的前面一条链断开;在这里使用一个指针来指向这个节点是因为要把这个节点的空间释放掉,我们在找到这个节点之后就可以让它的前一个节点的next去指向要删除节点的下一个节点,再把要删除的节点空间释放掉,最后再让指针p指向NULL即可。在这里删除有删一个和删全部的区别,在代码中如果只想删除找到的第一个对应的字母,那就在p=NULL后语句之后加一个break结束循环即可。
遍历:void traverse_link(list *l)
链表的遍历也就是通过循环来让L从头节点往后移动一直到最后一个节点,然后将每一个节点的data输出即可。
初始化:void init_link(list *l)
链表的初始化我们需要把除了头结点以外的节点都释放掉,同样还是使用循环遍历的方式,初始化跟删除是一样的操作,但是初始化是将所有节点都删除,跟删除差不多的我就不多说啦。
回收:void free_link(list **l)
链表的回收就是先将链表初始化然后再把头节点也释放,最后再让指向头节点的指针指向空即可。
测试:(主函数)
#include "linklist.h"
int main(int agrc,char *agrv[])
{list *p = create_link(); tail_insert_link(p,'a');tail_insert_link(p,'b');tail_insert_link(p,'b');tail_insert_link(p,'f');printf("尾插后链表内容:");traverse_link(p);head_insert_link(p,'b');head_insert_link(p,'c');head_insert_link(p,'d');printf("头插后链表内容:");traverse_link(p);printf("此时链表长度为:%d\n",length_link(p));list *q = search_link(p,'d');OUT(q->data);q->data = 'a';printf("将查找到的d改为a后:");traverse_link(p);updata_link(p,'a','b');printf("将第一个a更新为b:");traverse_link(p);delete_link(p,'b');printf("删除第一个b后:");traverse_link(p);init_link(p);printf("此时链表长度为:%d\n",length_link(p));free_link(&p);printf("回收后p的指向:%p\n",p);return 0;
}