一言之善,贵于千金。
———葛洪
目录
1.头插法创建链表 图示 代码 2.尾插法创建链表 图示 代码
3.查找某一个数据是否在链表中,如果没有,返回false,否则返回该结点的位序 图示 代码
4.统计结点个数 图示 代码 5.删除第i个结点 图示 代码
6.在某个位置之前插入结点 图示 代码 7.修改第i个结点的数据域的值 8.打印链表
一、前言
学习了线性表的顺序表示,我们知道顺序表示可以随机存取元素,意味着逻辑上相邻的两个元素在物理位置上也相连,但是我们可以看到插入和删除元素需要移动大量元素,而且还需要事先分配好一定量的空间。那么有没有一种结构是不要求物理位置必须相邻,而且插入删除也很快速呢?这就是我们要说的链式结构。
二、链式表示介绍
链式存储结构意味着逻辑上相邻的元素在物理位置上不一定也相邻,在删除和插入元素时不需要移动大量元素,同时不支持随机存取,必须找到直接前驱和直接后继才能操作相应的元素。
对于单链表来说,每个元素需要存储自身的值之外,还需要存储下一个元素的地址(直接后继地址),这是与顺序表示的最大区别,这时我们不称做这种存储结构为元素,我们称这种结构为结点,即链表是由有限个结点组成的,和顺序表示的元素说法来进行区分。其中每个结点存储自身信息的区域成为数据域,存储直接后继地址的区域称为指针域。指针域中存储的信息称作指针或链。
由于此链表中的每个结点只包含一个指针域,故又称为线性链表或单链表,整个链表的存取必须从头指针开始进行,头指针指向链表中第一个结点。
我们可以看到,每个结点的指针域内存放的是下一个设施的地址,这些设施虽然逻辑上相连,但并没有在同个路上。 最后一个结点由于后面没有结点需要指向,所以指针域为NULL(空),即什么也不指向。
由上述可见,单链表可由头指针唯一确定,在C语言中可以用结构指针来描述。
三、线性表的单链表存储结构
typedef struct LNode{
int data;
struct LNode * next; //指向结构体类型的指针,表示下一个结点
}LNode,* LinkList;
/* 最后一句的等同于下面代码,中括号是我自己加上的,只是提示把他们看作是一个整体
typedef {struct LNode } LNode; // struct LNode 的别名是 LNode
typedef {struct LNode * } LinkList; // struct LNode * 的别名是 LinkList
*/
四、带头结点的单链表
什么是头结点? 如字面意思,头结点就是在第一个结点前面增加一个结点,位置在头指针和第一个结点之间。
详细信息可以看这篇博客
https://blog.csdn.net/xiaohaiguang/article/details/105657135
以下是书中说明:
假设L是LinkList型的变量,则L为单链表的头指针,它指向表中第一个结点。若L为“空”(L= NULL),则所表示的线性表为“空”表,其长度n为
“零”。有时,我们在单链表的第一个结点之前附设一个结点,称之为头结点。头结点的数据域可以不存储任何信息,也可存储如线性表
的长度等类的附加信息,头结点的指针域存储指向第一个结点的指针(即第一个元素结点的存储位置)。
五、单链表的常用操作(带头结点)
结构体、头文件、宏定义
#include<cstdio>
#include<cstdlib>
#include<malloc.h>
#include<cstring>
#include<conio.h>
//函数结果状态代码
#define TRUE 1
#define FALSE 0
#define OK 1
#define ERROR 0
typedef int Status; //int 的别名,其值是函数结果状态代码
typedef struct LNode{
int data;
struct LNode * next; //指向结构体类型的指针,表示下一个结点
}LNode,* LinkList;
1.头插法创建链表
图示
所谓头插法就是插入的元素顺序和实际在链表中的顺序相反,比如 插入 1,2 输出就是 2,1
代码
Status create_headLinked_List(LinkList & L,int n)
//逆序输入n个元素的值,建立带表头结点单链线性表L
{
LinkList head = (LinkList)malloc(sizeof(LNode));//头结点的存储地址
if(NULL == head) return ERROR; //分配空间失败
L = head; //头指针L存储头结点P的地址,即头指针指向头结点
L->data = 0; //用头结点统计元素个数,刚开始为0个
L->next = NULL; //此时链表为空
for(int i = 0;i<n;i++)
{
LinkList List_Node = (LinkList)malloc(sizeof(LNode));
scanf("%d",&List_Node->data);
L->data++; //每插入一个元素,头结点统计链表元素个数加一
List_Node->next = L->next; //头插法,即链表中的元素顺序与输入相反
L->next = List_Node;
}
return OK;
}
2.尾插法创建链表
图示
代码
Status tail_create_heahLinked_List(LinkList & L,int n)
{
LinkList head = (LinkList)malloc(sizeof(LNode));//头结点的存储地址
if(NULL == head) return ERROR; //分配空间失败
L = head; //头指针L存储头结点P的地址,即头指针指向头结点
L->data = 0; //用头结点统计元素个数,刚开始为0个
L->next = NULL; //此时链表为空
LinkList temp = L;
for(int i = 0;i<n;i++)
{
LinkList List_Node = (LinkList)malloc(sizeof(LNode));
scanf("%d",&List_Node->data);
L->data++;
List_Node->next = NULL; //每插入一个元素,头结点统计链表元素个数加一
temp->next = List_Node; //尾插法,即链表中的元素顺序与输入相同
temp = temp->next;
}
return OK;
}
3.查找某一个数据是否在链表中,如果没有,返回false,否则返回该结点的位序。
图示
代码
Status getLNodeIndex(LinkList L,int number)
{
int i=1;
LinkList temp =L->next;
while(temp)
{
if(number == temp->data) return i;
temp = temp->next;
i++;
}
return FALSE;
}
4.统计结点个数
图示
我们直接利用头结点存储结点的个数,或者遍历统计,这里就不再演示这种方法。
代码
Status getLength(LinkList L)
{
return L->data;
}
5.删除第i个结点
图示
代码
Status deleteLnode(LinkList L ,int i)
{
LinkList temp = L;
int j=1;
while(temp->next && j<i ) //结束循环时,temp指向待删除结点的前一个结点
{
temp = temp->next;
j++;
}
if(NULL == temp->next || j > i) return ERROR;//循环结束,若最终待删除结点为空,或i为0或负数时,说明位置不合理,直接返回
LinkList p = temp->next; //p是待删除结点
temp->next = p->next; //将p的下一个结点地址赋值给p的前一个结点
free(p);
L->data--;
return OK;
}
6.在某个位置之前插入结点
图示
代码
Status insertLnode(LinkList L ,int i,int value)
{
LinkList temp = L;
int j=1;
LinkList newLnode = (LinkList)malloc(sizeof(LNode));
while(j<i)
{
temp = temp->next;
j++;
}
if( j > i || i>L->data + 1 ) return ERROR;//允许在最后一个元素之后插入元素
newLnode->data = value;
newLnode->next = temp->next;
temp->next = newLnode;
L->data++;
return OK;
}
7.修改第i个结点的数据域的值
Status updateLnode(LinkList L,int i,int value)
{
LinkList temp = L;
int j=1;
while(temp->next && j<i )
{
temp = temp->next;
j++;
}
if(NULL == temp->next || j > i) return ERROR;
//循环结束,若最终待修改结点为空,或i为0或负数时,说明位置不合理,直接返回
temp->next->data = value;
return OK;
}
8.打印链表
void headlinked_List_Traverse(LinkList L)
{
printf("该链表的元素个数为:%d \n",L->data);
LinkList p = L->next;
while(p)
{
printf(" %d ",p->data);
p = p->next;
}
}
六、完整代码
#include<cstdio>
#include<cstdlib>
#include<malloc.h>
#include<cstring>
#include<conio.h>
//函数结果状态代码
#define TRUE 1
#define FALSE 0
#define OK 1
#define ERROR 0
typedef int Status; //int 的别名,其值是函数结果状态代码
typedef struct LNode{
int data;
struct LNode * next; //指向结构体类型的指针,表示下一个结点
}LNode,* LinkList;
/* 最后一句的等同于下面代码,中括号是我自己加上的,只是提示把他们看作是一个整体
typedef {struct LNode } LNode; // struct LNode 的别名是 LNode
typedef {struct LNode * } LinkList; // struct LNode * 的别名是 LinkList
*/
//1.有头结点
Status create_headLinked_List(LinkList & L,int n)
//逆序输入n个元素的值,建立带表头结点单链线性表L
{
LinkList head = (LinkList)malloc(sizeof(LNode));//头结点的存储地址
if(NULL == head) return ERROR; //分配空间失败
L = head; //头指针L存储头结点P的地址,即头指针指向头结点
L->data = 0; //用头结点统计元素个数,刚开始为0个
L->next = NULL; //此时链表为空
for(int i = 0;i<n;i++)
{
LinkList List_Node = (LinkList)malloc(sizeof(LNode));
scanf("%d",&List_Node->data);
L->data++; //每插入一个元素,头结点统计链表元素个数加一
List_Node->next = L->next; //头插法,即链表中的元素顺序与输入相反
L->next = List_Node;
}
return OK;
}
//2.尾插法创建带头结点的链表
Status tail_create_heahLinked_List(LinkList & L,int n)
{
LinkList head = (LinkList)malloc(sizeof(LNode));//头结点的存储地址
if(NULL == head) return ERROR; //分配空间失败
L = head; //头指针L存储头结点P的地址,即头指针指向头结点
L->data = 0; //用头结点统计元素个数,刚开始为0个
L->next = NULL; //此时链表为空
LinkList temp = L;
for(int i = 0;i<n;i++)
{
LinkList List_Node = (LinkList)malloc(sizeof(LNode));
scanf("%d",&List_Node->data);
L->data++;
List_Node->next = NULL; //每插入一个元素,头结点统计链表元素个数加一
temp->next = List_Node; //尾插法,即链表中的元素顺序与输入相同
temp = temp->next;
}
return OK;
}
//3.查找某一个数据是否在链表中,如果没有,返回false,否则返回该结点的位序
Status getLNodeIndex(LinkList L,int number)
{
int i=1;
LinkList temp =L->next;
while(temp)
{
if(number == temp->data) return i;
temp = temp->next;
i++;
}
return FALSE;
}
//4.获取链表长度
Status getLength(LinkList L)
{
return L->data;
}
//5.删除某个位置节点 (从1开始)
Status deleteLnode(LinkList L ,int i)
{
LinkList temp = L;
int j=1;
while(temp->next && j<i )
{
temp = temp->next;
j++;
}
if(NULL == temp->next || j > i) return ERROR;//循环结束,若最终待删除结点为空,或i为0或负数时,说明位置不合理,直接返回
LinkList p = temp->next; //p是待删除结点
temp->next = p->next; //将p的下一个结点地址赋值给p的前一个结点
free(p);
L->data--;
return OK;
}
//6.在第i 个位置之前插入新结点
Status insertLnode(LinkList L ,int i,int value)
{
LinkList temp = L;
int j=1;
LinkList newLnode = (LinkList)malloc(sizeof(LNode));
while(j<i)
{
temp = temp->next;
j++;
}
if( j > i || i>L->data + 1 ) return ERROR;//允许在最后一个元素之后插入元素
newLnode->data = value;
newLnode->next = temp->next;
temp->next = newLnode;
L->data++;
return OK;
}
//7.输出带头结点链表中的元素
void headlinked_List_Traverse(LinkList L)
{
printf("该链表的元素个数为:%d \n",L->data);
LinkList p = L->next;
while(p)
{
printf(" %d ",p->data);
p = p->next;
}
}
//8.修改第i个结点的值
Status updateLnode(LinkList L,int i,int value)
{
LinkList temp = L;
int j=1;
while(temp->next && j<i )
{
temp = temp->next;
j++;
}
if(NULL == temp->next || j > i) return ERROR;//循环结束,若最终待修改结点为空,或i为0或负数时,说明位置不合理,直接返回
temp->next->data = value; //结点
return OK;
}
//销毁链表,包括头结点的所有结点全部释放
Status destroy_headlinked_list(LinkList L)
{
L->data = 0;
LinkList p;
while(L)
{
p = L->next;
free(L);
L = p;
}
return OK;
}
/*清空带头结点链表
1.先保留链表的头结点
2.然后把头结点后面的所有的都销毁
3.最后把头结点里指向首元结点的指针设为空 */
Status clear_headlinked_list(LinkList L)
{
L->data = 0;
LinkList p,q;
p = L->next;
while(L)
{
q = p->next;
free(p);
p = q;
}
L->next = NULL;
return OK;
}
int main(void)
{
LinkList L;//头指针L
int n = 0;
printf("请输入所要创建带头结点链表的元素个数:");
scanf("%d",&n);
if( tail_create_heahLinked_List( L, n) )
headlinked_List_Traverse(L);
int number=0;
printf("\n请输入查找的元素:");
scanf("%d",&number);
if(getLNodeIndex(L,number))
printf("%d",getLNodeIndex(L, number));
else printf("未找到! ");
printf("\n该链表的长度为:%d",getLength(L));
int i=1,value;
printf("\n请输入删除结点的位置:");
scanf("%d",&i);
deleteLnode( L ,i);
headlinked_List_Traverse(L);
printf("\n该链表的长度为:%d",getLength(L));
printf("\n请输入插入结点的位置和数据:");
scanf("%d %d",&i,&value);
insertLnode( L ,i, value);
printf("\n该链表的长度为\n:%d",getLength(L));
headlinked_List_Traverse(L);
printf("\n请输入待修改结点的位置和数据:");
scanf("%d %d",&i,&value);
updateLnode(L,i, value);
headlinked_List_Traverse(L);
if(destroy_headlinked_list(L))
printf("\n销毁带头结点链表L成功!\n");
return 0;
}
七、总结
1.链表的优缺点
优点:插入结点和删除结点效率高。不要求逻辑相邻物理位置必须相邻,位置可以分散。扩展性好。
缺点:不支持随机存取,要访问某一个结点,需要找到前驱结点。占用内存大,需要保存后继信息。
2.数组的优缺点
优点:支持随机存取,查找某个位置元素快速,占用内存小。
缺点:插入和删除元素需要移动大量元素,扩展性差。
3.链表操作注意事项
1.循环注意判断临界点的值。
2.在进行插入操作时,注意顺序。
3.利用好头结点。
4.注意是否改变头指针的值,要明确什么时候使用头指针引用(二级指针可以代替)