数据结构之线性表——(二、链式存储结构-单链表)
链式存储结构以及基本运算的实现
背景:由于线性表的存储特点是用物理上的相邻实现逻辑上的相邻,他要求用连续的存储单元顺序存储线性表中的各个元素,所以,对线性表中的元素进行插入、删除时要移动元素,很影响运算的效率。而在链式存储结构中,存储单元的地址可连续,也可不连续,这就意味着这些数据元素可以存放在内存中未被占用的任何位置。它不要求逻辑上相邻的两个数据元素物理上也相邻,其逻辑关系是通过“链”建立起数据元素之间的关系,因此对线性表的插入、删除操作时不需要移动数据,提高了效率。
一、单链表
单链表的表示
链表是通过一组任意的存储单元来存储线性表中的数据元素的,每个数据元素(ai),除了存放自身的数据外,还需要存放其后继ai+1所在的存储单元的地址,这两部分信息组成一个“结点”,存放数据元素信息的数据域为data,存放其后继地址的称为指针域next,所以n个数据元素的线性表通过每个结点的指针域连成一条“链”,故称为链表。其结点的结构如下:
注:在这里我们称为单链表,单链表是因为其每个结点中只有一个指向后继的指针,所以称为单链表。
1、单链表的定义
单链表是由一个个结点构成的,使用单链表首先要定义一个结点,也可称为结构体,其定义如下:
typedef struct node
{
DataType data;
struct node *next;
}Lnode,*LinkList;
/*Lnode是结构体别名,用其定义结点如:Lnode n;
但是用 *LinkList定义的是指针。 */
定义头指针变量
LinkList H; //linkList是指向Lnode类型结点的指针类型
在上图中,顺序存储结构的地址是连续的,而且其数据域中的数据也是按照逻辑结构自上而下的。在链式存储结构中,每个结点(node)不仅存储了数据本身(A),还存放了下一个结点的地址(0x5),但头指针中没有存放数据,它存放的是第一个结点(A)的地址。在顺序存储结构里我们将A称为B的直接前驱,B称为A的直接后继,由于是单向链表,在这个链式存储中只有前驱到后继的指针,前驱和后继的关系由指针来链接,也就是说,如果我们要查找后面的元素必须要从头查找。
作为线性表的一种存储结构,我们关心的是结点之间的逻辑结构,而对每个结点之间的实际地址不感兴趣。
单链表分为带头和不带头两种
对于线性表来说,有头有尾,链表也同样,我们把链表中第一个结点存储的位置叫做头指针,通常用“头指针”来标识一个单链表。在上图带头结点单链表示意图中,第一个结点的地址放在了指针变量H中,链表的最后一个结点指针为空(NULL)。
在下图中,假设p是指向线性表第i个元素的指针,则结点ai的数据域用p->data来表示,即p->data的值是一个数据元素,p->data = ai;结点ai的指针域用p->next表示,即p->next是一个指针,(p->next)->data = ai+1。
2、单链表的基本操作
(1)初始化创建单链表
在每次将新节点插入到单链表的尾部时,我们需要加入一个指针 r来始终指向单链表中的为节点。
步骤:
1、初始化,头指针H=NULL,尾指针r=NULL
2、线性表中元素的顺序依次读入数据元素,如果不是结束标志,申请结点。
3、将新节点插入到r所指结点的后面,然后r指向新节点。(第一个结点不同)
LinkList Creat_LinkList()
{
LinkList L = NULL; //头指针L置空
Lnode *s,*r=NULL;
char x,flag='0';
printf("请输入数据,输入0结束\n");
scanf("%d",&x);
while(x!=flag)
{
s = (Lnode *)malloc(sizeof(Lnode)); //申请一个新结点
s->data = x; //将输入的x放在结点的数据域中
if(L==NULL) L = s; //如果头指针为空,将结点s的指针放在头指针中
else r->next = s; //若不为空放在指针r 中
r = s; //r始终指向最后一个结点
printf("请输入数据,输入0结束\n");
scanf("%c",&x);
}
if(r!=NULL)
r->next = NULL; //对于非空表,最后结点的指针域放空指针
return L;
}
(2)按序号查找结点
1、从链表的第一个数据元素结点起,判断当前结点是否是第i个结点。
2、若是第i个结点,则返回,该结点的指针,否则继续下一个,直至结束为止。
3、若没有第i个结点时返回为空。
//按序号查找结点
Lnode *Get_LinkList(LinkList L,int i)
{
Lnode *p =L; //把第一个结点的指针放在p里面
int j=1;
if(i==1) return p;
if(p==NULL) { printf("表为空");return NULL; }
while((p->next!=NULL) && j<i) //判断当前结点是否是第i个结点
{ p = p->next; ++j;}
if(j==i) return p; //如果是第i个结点,则返回该节点的指针
else return NULL;
}
(3)单链表的插入
设p指向单链表中某结点,s指向待插入的值为x的新节点,将结点s插入到结点p的后面,插入的示意图如下:
- s->next = p->next;
- p->next = s; 先挂链再改链,顺序不能交换
1、查找第i-1个结点,如果找到继续,否则结束
2、申请、填装新节点。
3、将新节点插入,结束。
Lnode * Insert_LinkList(LinkList L,int i,char x)
{
Lnode *p,*s; //定义p s 两个指针
if(i==1) //如果插入的位置是第一个呢
{ s = (Lnode *)malloc(sizeof(Lnode));
s->data = x;
s->next = L; //把老的第一个的地址放进新第一个里面
L = s; return L;//把新第一个的地址放在头指针里面
}
p = Get_LinkList(L,i-1);
if( p==NULL) //第i个结点前面位置不存在不能插入
{ printf("插入位置前面没有元素!\n");
return 0;
}
else //在插入位置前面一个元素的地址放在新结点中
{
s = (Lnode *)malloc(sizeof(Lnode));//创建新节点来存放插入的元素
s->data = x; //将用户要插入的元素放入数据域中
s->next = p->next; //p->next是i-1的地址,s放在i位置的前面,接在i-1位置后面
p->next = s;
return L;
}
}
在插入函数中,如果我们在插入在第i个位置,那就查找第i-1个位置,如果i-1个位置不存在就无法插入,当查找到i-1个位置时,将i-1个结点中存储的第i个位置的地址放在新申请结点s的指针域中,再将s的地址放在i-1个位置的指针域中,这样s结点就插入到位置i中,i接在s的后面。
(4)删除结点
设p指向单链表中某结点,现在要根据用户输入的位置i 删除结点p。
要删除结点p,首先要找到p的前驱结点q,然后完成指针的操作。
- q-next = p->next;
- free(q);
1、查找到第 i-1个结点,如果找到继续,否则结束
2、若存在第i个结点,继续下一步,否则返回没找到
3、删除这个结点
//按位置删除结点
LinkList Del_LinkList(LinkList L,int i)
{ LinkList p;Lnode *s =L;
p = Get_LinkList(L,i-1);
if(i==1)
{
L = s->next;
return L;
}
else if(p->next == NULL){ printf("第%d个结点不存在\n",i); return L; }
else{
s = p->next;
p->next = s->next;
free(s);
return L;
}
}
- 在单链表中插入、删除一个结点,必须知道其前驱结点
- 单链表不具有按序号随机访问的特点,只能从头指针开始一个又一个顺序进行
实现单链表基本的创建、插入、删除等操作:
#include "stdio.h"
#include "malloc.h"
#include "stdlib.h"
typedef struct node
{ char data;
struct node *next;
}Lnode,*LinkList;
//单链表的创建
LinkList Creat_LinkList()
{ LinkList L = NULL;
Lnode *s,*r=NULL;
char flag='0';
char x;
printf("请输入,输入0结束\n");
fflush(stdin);
scanf("%c",&x);
while(x!=flag)
{ s = (Lnode *)malloc(sizeof(Lnode));
s->data =x;
if(L==NULL) L=s;
else r->next = s;
r = s;
scanf("%c",&x);
}
if(r!=NULL)
r->next = NULL;
return L;
}
void Disp_LinkList(LinkList L)
{ LinkList p = L;
while(p!=NULL)
{ printf("%c ",p->data);p=p->next; }
}
//按序号查找结点
Lnode *Get_LinkList(LinkList L,int i)
{
Lnode *p =L; //把第一个结点的指针放在p里面
int j=1;
if(i==1) return p;
if(p==NULL) { printf("表为空");return NULL; }
while((p->next!=NULL) && j<i) //判断当前结点是否是第i个结点
{ p = p->next; ++j;}
if(j==i) return p; //如果是第i个结点,则返回该节点的指针
else return NULL;
}
Lnode * Insert_LinkList(LinkList L,int i,char x)
{
Lnode *p,*s; //定义p s 两个指针
if(i==1) //如果插入的位置是第一个呢
{ s = (Lnode *)malloc(sizeof(Lnode));
s->data = x;
s->next = L; //把老的第一个的地址放进新第一个里面
L = s; return L;//把新第一个的地址放在头指针里面
}
p = Get_LinkList(L,i-1);
if( p==NULL) //第i个结点前面位置不存在不能插入
{ printf("插入位置前面没有元素!\n");
return 0;
}
else //在插入位置前面一个元素的地址放在新结点中
{
s = (Lnode *)malloc(sizeof(Lnode));//创建新节点来存放插入的元素
s->data = x; //将用户要插入的元素放入数据域中
s->next = p->next; //p->next是i-1的地址,s放在i位置的前面,接在i-1位置后面
p->next = s;
return L;
}
}
//按位置删除结点
LinkList Del_LinkList(LinkList L,int i)
{ LinkList p;Lnode *s =L;
p = Get_LinkList(L,i-1);
if(i==1)
{
L = s->next;
return L;
}
else if(p->next == NULL){ printf("第%d个结点不存在\n",i); return L; }
else{
s = p->next;
p->next = s->next;
free(s);
return L;
}
}
void main()
{
LinkList L; int n;char x;
L = Creat_LinkList();
printf("你创建的单链表为:\n");
Disp_LinkList(L);
printf("\n输入你要插入的位置:\n");
scanf("%d",&n);fflush(stdin);
printf("输入你要插入的元素:\n");
scanf("%c",&x);
L = Insert_LinkList(L,n,x);
printf("你插入后的单链表为:\n");
Disp_LinkList(L);
printf("\n输入你要删除的位置:\n");
scanf("%d",&n);
L = Del_LinkList(L,n);
printf("你删除后的单链表为:\n");
Disp_LinkList(L);
}
代码运行图:
单链表的简单运用:使用单链表制作电子通讯录(单链表)
需求:
- 输入联系人信息,包含姓名、电话号码两部分。
- 实现联系人信息的插入:输入要插入的位置和新的联系人信息,将此联系人插入的链表的相应位置。
- 实现联系人信息的删除:输入要删除的联系人姓名,将此姓名的联系人删除。
- 实现按联系人的姓名查找信息
#include "stdio.h"
#include "stdlib.h"
#include "string.h"
struct numbook //定义一个结构体
{ char name[10]; //定义一个数组来存放姓名
char phone[15]; //定义一个数组来存放电话号码
struct numbook * next; //定义一个指针
};
//判断用户输入的号码是否合法
int IsLegal(const char *str) //用常量输入,只能使用无法修改
{
while(*str!='\0') //!='\0'.就是运行到字符串结尾时结束。
{
if(!(*str>='0' && *str<='9')) //用户输入的号码不在0到9之间返回0
return 0;
str++;
}
return 1;
}
//用户信息导入
numbook * input()
{
numbook *p,*q=NULL;
numbook * L = NULL;
char name[10],phone[15];
while( printf("请输入姓名和电话号码,输入“ok”结束!\n"),scanf("%s",&name),strcmp(name,"ok")!=0)
{
p = (struct numbook *)malloc(sizeof(numbook));
strcpy(p->name,name); //将用户输入的姓名赋值到单链表中的p-name中
while(scanf("%s",&phone),IsLegal(phone)==0)
{ printf("电话号码无效,重新输入。\n");} //判断电话号码是否合法
strcpy(p->phone,phone);
if( L == NULL) { L = p; }
else { q->next = p; }
q = p;
}
if(q!=NULL) q->next =NULL; return L;
}
//输出电话簿
void display(numbook * L)
{ while(L!=NULL)
{
printf("%s - %s \n",L->name,L->phone);
L = L->next;
}
}
//在电话簿中根据姓名查找联系人信息
numbook * find(numbook *L,char *na)
{
while(L!=NULL)
{ if(strcmp(L->name,na)==0) //如果姓名和电话簿中的相等返回L
return L;
else
L = L->next; //否则接续下一个结点
}
return NULL;
}
//删除联系人
numbook * DeleteByName(numbook *L,const char *na)
{ numbook *p,*q; //定义两个指针,让q和p同时前进,便于操作指针
if(L==NULL){ printf("空表无法再删除!\n"); //当L为空表时
return L;
}
if(strcmp(L->name,na)==0) //当删除的是第一个结点时
{
p = L; L = L->next;
free(p);printf("删除成功!\n"); return L;
}
p = L; //将电话簿的头指针放在p中,p指向了第一个元素
q = p->next; //将第一个结点存放的指针放在q中,q指向了第二个结点
while(q!=NULL && strcmp(q->name,na)!=0)
{
p = q;
q = q->next;
}
if(q->next==NULL){
free(q);
p->next = NULL;printf("删除成功!\n");return L; }
//删除的元素为最后一个,q结点的指针是空的,直接释放q再将q前面的p指针置空
else {
p->next = q->next; free(q);printf("删除成功!\n");return L; }
return L; //将q中存放的地址放到q前面位置的p中,再释放q
}
//插入联系人信息
numbook * InsertByName(numbook *L,const char *na,numbook *e)
{
numbook *p,*q; //同样定义两个指针,让q和p同时前进,便于操作指针
if(L==NULL) { L = e; e->next = NULL; return L; }
//如果头指针为空,代表输入的为第一个结点,就把它地址放在头指针里
//在把这个结点的指针域置空,代表后面没有结点了,也就是它为第一个结点
if(strcmp(L->name,na)==0) //当用户输入的姓名是第一个联系人的姓名时
{ e->next = L; L = e; return L; }
//将这第一个人的地址放在新插入结点的指针域中,再将它的指针放在头指针中
p = L; q = p->next;
while(q!= NULL && strcmp(q->name,na)!=0)
{
p = q; q = q->next;
}
if(q == NULL)
{ p->next = e;
e->next = NULL;
return L;
}
else {
p->next = e; e->next = q; return L;
}
}
void main(){
char na[10];int i=0;
numbook * L,*p,*e;
L = input();
display(L);
while(i<10){
fflush(stdin);
while(printf("请输入你要操作的功能(1.查询联系人。2.插入联系人。3.删除联系人)\n"),scanf("%d",&i),i>3)
{printf("输入错误,重新输入");}
switch(i){
case 1: printf("请输入要查找的姓名:\n");
fflush(stdin); //清除键盘缓冲区
scanf("%s",&na);
p = find(L,na);
if(p==NULL) printf("查无此人\n");
else printf("这个人的信息为:姓名“%s”,电话“%s”。\n",p->name,p->phone);
break;
case 2: printf("请输入要插入的信息(姓名 电话)\n");
e = (struct numbook *)malloc(sizeof(struct numbook));
scanf("%s%s",e->name,e->phone);
printf("请输入要插入位置的姓名:\n");
scanf("%s",na);
L = InsertByName(L,na,e);
display(L);
break;
case 3: printf("请输入要删除联系人的姓名:\n");
scanf("%s",&na);
p = find(L,na);
if(p==NULL) printf("查无此人\n");
L = DeleteByName(L,na);
display(L);
break;
}
i=NULL;
}
}
运行结果: