文章目录
单链表的基本知识
定义
线性表的链式存储又称为单链表,它是指通过一组任意的存储单元来存储线性表中的数据元素。为了建立数据元素之间的线性关系,对每个链表结点,除了存放元素自身的信息外,还需要存放一个指向其后继的指针。单链表的结构如下图所示:
- 存放数据的变量称为数据域,通常记为data
- 存放后继结点位置的指针称为指针域,通常记为next
单链表的结点类型描述如下:
//定义单链表的结点类型
typedef struct LNode{
ElemType data;//数据域
struct LNode *next;//指针域
}LNode,*LinkList;
头指针与头结点
通常使用一个头指针来唯一标识一个单链表,如单链表L,当头指针为NULL时,表示一个空表。此外,为了操作方便,通常会在单链表的第一个结点之前附加一个结点,称为头结点。头结点的数据域可以不存放任何数据,也可以存放一些额外信息如表长等等。头结点的指针域指向线性表的第一个元素结点,如下图所示:
头指针与头结点的区别
- 头指针始终指向链表的第一个结点
- 头结点是带头结点的链表的第一个结点
判断空表的条件区别
- 带头结点判断空表条件是:L->next=NULL
- 不带头结点只含头指针判断空表的条件是:L==NULL
引入头结点的优点
- 由于第一个数据结点的位置被存放在头结点的指针域中,所以在链表的第一个位置上的操作和在表的其它位置上的操作一致,不需要进行额外的特殊处理;反之,不带头结点只含头指针的情况下需要对一个位置上的操作进行特殊处理
- 不管链表是否为空,其头指针都是指向头结点的非空指针(空表中头结点的指针域为空),因此空表和非空表也得到了统一
单链表的基本操作
结点类型的定义
//定义单链表的结点类型
typedef struct LNode{
int data;//数据域
struct LNode *next;//指针域
}LNode,*LinkList;
初始化单链表
带头结点的单链表
//初始化单链表(带头结点)
bool InitList(LinkList &L){
L=(LNode*)malloc(sizeof(LNode));//分配一个头结点的内存空间
if(L==NULL)return false;//内存不足,分配失败
L->next=NULL;//链表中暂时没有元素
return true;
}
不带头结点的单链表
//初始化单链表(不带头结点)
bool InitList(LinkList &L){
L=NULL;//链表中暂时没有元素
return true;
}
创建单链表
头插法——逆向建表
使用“头插法”建表,读入数据的顺序与链表中元素的顺序相反
//头插法建表:读入数据的顺序与链表中元素的顺序相反
LinkList List_HeadInsert(LinkList &L){
LNode *s;
int x;
//L=(LinkList)malloc(sizeof(LNode));//创建头结点
//L->next=NULL;//初始化为空链表
scanf("%d",&x);
while(x!=-1){
s=(LNode*)malloc(sizeof(LNode));//开辟一个新的内存空间存放新的结点
s->data=x;
s->next=L->next;//设置新结点的指针域等于原头结点指向的第一个结点
L->next=s;//头指针重新指向链表的第一个结点
scanf("%d",&x);
}
return L;
}
尾插法——正向建表
使用“尾插法”建表,读入数据的顺序与链表中元素的顺序相同
//尾插法建表:读入数据的顺序与链表中元素的顺序一致
LinkList List_TailInsert(LinkList &L){
int x;//存放用户输入的数字
L=(LinkList)malloc(sizeof(LNode));//建立头结点,初始化空表
LNode *s,*r=L;//r为表尾指针
scanf("%d",&x);
while(x!=-1){
s=(LNode*)malloc(sizeof(LNode));//在内存中开辟出一个新结点的空间
s->data=x;
r->next=s;
r=s;//r指向新的表尾结点
scanf("%d",&x);
}
r->next=NULL;//尾指针的指针域置空
return L;
}
插入元素
按位序插入(带头结点)
//按照位序插入:在表L中的第i个位置插入指定元素e
bool InsertList1(LinkList &L,int i,int e){
if(i<1)return false;
LNode *p;//游标指针
int j=0;//表示p指向的是第几个结点
p=L;//L指向头结点
while(p!=NULL&&j<i-1){
p=p->next;
j++;
}
if(p==NULL)return false;//i值不合法
LNode *s=(LNode*)malloc(sizeof(LNode));//开辟一个新的内存空间给新结点存储待插入值
s->data=e;
s->next=p->next;
p->next=s;
return true;
}
按位序插入(不带头结点)
不带头结点,即不存在“第0个”结点,所以当要插入在第一个位置,即i=1的时候要进行特殊处理,与带头结点的操作不相同,注意对比。
//按照位序插入(不带头结点):在表L中的第i个位置插入指定元素e
bool InsertList2(LinkList &L,int i,int e){
if(i<1)return false;
//插入到第一个位置时与插入到其它结点的操作不相同
if(i==1){
LNode *s=(LNode*)malloc(sizeof(LNode));
s->data=e;
s->next=L;
L=s;
return true;
}
LNode *p;//游标指针
int j=1;//表示p指向的是第几个结点
p=L;//L指向头结点
while(p!=NULL&&j<i-1){
p=p->next;
j++;
}
if(p==NULL)return false;//i值不合法
LNode *s=(LNode*)malloc(sizeof(LNode));//开辟一个新的内存空间给新结点存储待插入值
s->data=e;
s->next=p->next;
p->next=s;
return true;
}
按指定结点前插
//指定结点的前插操作
bool InsertPriorNode(LNode *p,int e){
if(p==NULL)return false;
LNode *s=(LNode*)malloc(sizeof(LNode));
if(s==NULL)return false;//内存不够,分配空间失败
s->next=p->next;
p->next=s;
//交换p、s中的值达到变相的前插操作,即把新结点s放到了p的前面
int temp=p->data;
p->data=s->data;
s->data=temp;
p->data=e;
return true;
}
按指定结点后插
//指定结点的后插
bool ListInsert(LinkList &L,int i,int e){
if(i<1)return false;
LNode *p;//指针p指向当前扫描到的结点
int j=0;//当前p指向的是第几个结点
p=L;
//先找到第i-1个结点
while(p!=null&&j<i-1){
p=p->next;
j++;
}
if(p==NULL){
return false;
}
LNode *s=(LNode*)malloc(sizeof(LNode));
s->data=e;
s->next=p->next;
p->next=s;
return true;
}
删除元素
按位序删除(带头结点)
//按位序删除(带头结点)
bool ListDelete(LinkList &L,int i,int &e){
if(i<1)return false;
LNode *p;//指针p指向当前扫描到的结点
int j=0;
p=L;
while(p!=NULL&&j<i-1){
p=p->next;
j++;
}
if(p==NULL)return false;
if(p->next==NULL)return false;//第i-1个结点之后已经没有其它结点了
LNode *q=p->next;//令q指向被删除的结点
e=q->data;//用e返回被删除的元素值
p->next=q->next;//断链
free(q);//释放被删除结点原来占用的内存空间
return true;
}
按位序删除(不带头结点)
不带头结点,当要删除第一个元素,即i=1时需要进行特殊处理
if(i==1){
LNode *q=L;
e=q->data;
L=q->next;
free(q);
return true;
}
指定结点的删除
//指定结点的删除
bool DeleteNode(LNode *p){
if(p==NULL)
return false;
LNode *q=p->next;
p->data=p->next->data;//和后继结点交换数据域
p->next=q->next;//将*q结点从链中"断开"
free(q);
return true;
}
查找元素
按位查找
//按位查找(带头结点)
LNode * GetItem(LinkList L,int i){
if(i<0)
return NULL;
LNode *p;//游标指针
int j=0;//p指向的是第几个结点
p=L;
//循环找到第i个结点
while(p!=NULL&&j<i){
p=p->next;
j++;
}
return p;
}
按值查找
//按值查找
LNode * LocateItem(LinkList L,int e){
LNode *p=L->next;
//从第一个结点开始寻找数据域为e的结点
while(p!=NULL&&p->data!=e){
p=p->next;
}
return p;//找到后返回该点的指针,否则返回NULL
}
求表的长度
表长指的是元素结点的个数,不包含头结点
//求表的长度
int GetLength(LinkList L){
int len=0;
LNode *p=L;
while(p->next!=NULL){
p=p->next;
len++;
}
return len;
}
总结
头指针与头结点
- 通常使用头指针来标识一个单链表,如单链表L,当头指针为NULL时,此时为空表
- 在单链表第一个元素结点前面附加的结点称为头结点,其为了后续各项操作方便而设置,头结点的指针域指向单链表的第一个元素结点
带头结点与不带头结点
- 在题目中一定要看清楚是否带头结点,当处理第一个元素时
- 带头结点和不带头结点的处理操作是不同的
头插法与尾插法
- 头插法建表,读入数据的顺序与链表中元素的顺序是相反的,链表逆置就是用头插法进行操作
- 尾插法建表,读入数据的顺序与链表中元素的顺序是相同的,一般使用该方法建表
修改指针域与数据域的代码顺序不可颠倒
如上图所示,绿色箭头处代码和黄色箭头处的代码不能颠倒顺序,试想一下 ,如果颠倒顺序,先执行p->next=s; 即p指向的下一个结点是s,再执行s->next=p->next; 此时就变成了s的指针域存放s,这样导致s的指针域指向了其本身,显然是错误的。后续一些代码中,读者可以自行体会代码顺序的不同导致操作结果的不同,这一细节一定要重视!
END.