链表是由一系列结点组成的,每个节点包含两域,一个是数据域,用来保存用户数据,另外一个是指针域,保存下一个节点的地址且在内存中是非连续的存储模式。链表在指定位置进行插和删除时不需要移动元素,只需要修改指针。
一、单链表:
是一种常见的动态存储线性表的实现方式,分为单向,双向,单循环,双循环等基本结构:
1.结点:每个节点包含两部分:数据部分和指针部分,数据部分存储元素的值,指针部分指向下一个结点。
①首元结点:链表中第一个有效结点(数据域存储有效数据)。
②头结点(可以不保存任何数据):是链表中的附着在首元结点之前的节点数据域存储当前链表特性(长度),指向链表的第一个结点(这里我们可以理解为数组的首地址元素,但不能混淆),链表中的最后一个结点的指针部分为NULL,表示链表的结束。
N个节点(ai)链接成一个链表,即是线性表(a1,a2,⋯,an)的链式存储结构,因此链表的每个结点中只包含一个指针域,所以叫单链表。
2.头指针:对于线性表来说,我们把链表中第一个结点的存储位置叫做头指针。
①链表的入口点:头指针是链表的起点,它使得链表可以从第一个结点开始遍历。没有头指针,无法访问链表的内容或对链表进行操作。
②链表操作管理的核心作用:它使得访问,插入,删除和遍历链表中的结点成为可能,正确管理头指针是确保链表功能正常和避免出错的关键。
为了更加方便的对链表进行操作,会在单链表的额第一个结点前附设一个 结点,称为头结点,头结点的数据域可以不存储任何信息。最后一个结点必须指向空(NULL),否则最后一个结点的指针指向未知位置,变为野指针。
当然了,头指针和头结点区别还是很大的
3.链表的优势:
①链表具有很好的动态性(非顺序存储结构),单线联系。
②在进行插入和删除数据时,仅仅需要改变指针的指向,提升数据的修改效率。
③结点的个数可以无限增加(硬件允许),自由扩充。
4.链表的缺点:
①查找元素效率太低,需要遍历链表,从时间复杂度的角度来说,处理时间过长,不算是一个好的算法。
②降低了磁盘数据的存储密度,容易产生碎片(磁盘碎片是指文件在磁盘上的不连续存储,这会导致空闲空间的浪费和访问性能的下降),碎片会影响磁盘的存储效率和性能。
下面我们来看看具体的定义过程:
//结点结构定义
typedef struct LN
{
ElemType data;//数据域
struct LN *next;//指针域
} LNode;
//链表结构定义
typedef struct
{
LNode head;//头结点
int count;//链表中的有效长度
} LinkList;
在这里写几个式子给大家看看:
LNode *p,*q;
p=q;//表示p和q 指向q指向的结点
q=p->next;//表示p指向q
//next 是指针类型,存储下一个结点的地址
p = p->next;//表示将p移动到下一个结点
q->next = p; //表示q指向p
q->next = p->next;// p的下一个结点位置是q的下一个结点
初始化链表并构造结点:
/*
初始化链表
参数1:传入带初始化的链表指针
返回值:成功返回0
*/
int InitList(LinkList *L)
{
L->head.next = NULL;
L->count = 0;
return 0;
}
/*
构造结点
参数1:结点数据域的值
返回值:成功返回一个结点的指针,失败返回NULL
*/
LNode *NewNode(ElemType dat)
{
LNode *new;
new = (LNode *)malloc(1 * sizeof(LNode));
if(new == NULL)
return NULL;
else
{
new->data = dat;
new->next = NULL;
return new;
}
}
销毁-置空链表:
/*
销毁链表:逐个释放结点,遍历
参数1:待销毁的链表指针
返回值:成功返回0,失败返回-1
*/
int DestoryList(LinkList *L)
{
LNode *p,*q;//p指向释放位置,q指向下一个待释放的位置
p=L->head.next;
while(p != NULL)
{
q=p->next;
free(p);
p=q;
}
L->head.next = NULL;
L->count = 0;
}
在这里释放p的原因是:链表结点在使用时也就在堆区分配了内存,必须要显式释放这些内存,防止内存泄漏。释放p相当于同时释放了p的数据域和指针域。
获取链表的长度(可以用来判断链表是否为空):
/*
判断链表长度
参数1:链表指针
返回值:成功返回链表长度,失败返回-1
*/
int ListLength(Link *L)
{
LNode *p;
int i = 0;
p=L->head.next;//head.next代表头结点的指针域
while(p !=NULL)
{
p=p->next;
i++;
}
return L->count;//或者用return i来代替
}
打印链表:
/*
打印链表
参数:链表指针
*/
void ListPrint(Link *L)
{
LNode *p;
p=L->head.next;//head.next代表头结点的指针域
while(p != NULL)
{
printf("%d\n",p->data);
p=p->next;
}
}
初始化部分函数测试:
int main()
{
LinkList list;
LNode *node;
InitList(&list);
node=NewNode(55);
if(ListEmpty(&list) == 0)
printf("list链表为空\n");
else
printf("list长度为%d\n",ListLength(&list));
return 0;
}
取值操作(这里我们给出两种方法):
①通过位置查找该位置上的值,成功,通过形参返回
/*
取值操作:通过位置查找该位置上的值,成功,通过形参返回
参数1:链表指针
参数2:数据的位置
参数3:使用指针返回取到的数据
返回值:成功返回0,失败返回-1
*/
int GetElemList(LinkList *L,int i,ElemType *e)
{
LNode *p;
int j=1;
//初始化
p=L->head.next;//p指向首元结点
while(j<i && p)
{
p=p->next;
j++;
}
if(!p)//!表示取反操作,相当于取值失败
return -1;
*e=p->data;
return 0;
}
②通过传入一个值,判断该值是否在链表中,如果有,返回结点
/*
取值操作:通过传入一个值,判断该值是否在链表中,如果有,返回结点
参数1:链表指针
参数2:值
返回值:成功返回结点,失败返回NULL
*/
LNode *GetLocateElemList(Link *L,ElemType e)
{
LNode *p;
p=L->head.next;
while(p->data != e && p)
{
p=p->next;
}
return p;
}
插入操作:前插,中插,尾插
前插(从头部插入数据):
/*
从头部插入数据
参数1:链表指针
参数2:新结点数据域值
返回值:成功返回0,失败返回-1
*/
int ListHeadInsert(LinkList *L,ELemType e)
{
LNode *new;
new=NewNode(e);//创建孤立的结点
if(new == NULL)
return -1;//创建新的结点失败,返回
new->next = L->head.next;//new指向原来的首元结点
L->head.next = new;//将连表头结点指向new
L->count++;
return 0;
}
尾插(从尾部插入数据):
/*
从尾部插入插入数据
参数1:链表指针
参数2:新结点数据域值
返回值: 成功返回0,失败返回-1
*/
int ListTailInsert(LinkList *L,ElemType e)
{
LNode *p=L->head.next;//p指向首元结点
int length = L->count;//链表有效数据个数
LNode *new;
new=NewNode(e);
if(new == NULL)
return -1;
while(--length)//跳出时,p指向最后一个有效的数据
p=p->next;
p->next = new;
L->count++;
return 0;
}
中插(从中间插入数据):
/*
从尾部插入插入数据
参数1:链表指针
参数2:数据的位置
参数3:新结点数据域值
返回值: 成功返回0,失败返回-1
*/
int ListMidInsert(LinkList *L,int i,ElemType e)
{
LNode *p;
int j=;
LNode *new;
new=NewNode(e);//创建新结点
if(new == NULL)
return -1;
//初始化
p=L->head.next;//p指向首元结点
while(j<i && p)
{
p=p->next;
j++;
}
if(!p)//p == NULL;
return -1;//位置非法
new->next=p->next;
p->next=new;
L->count++;
return 0;
}
这篇博客就暂时写到这吧,因为Leo也只写到这,后续还会继续更新的,大家谅解一下。