前言:
顺序表需要连续的空间且插入和删除操作需要移动大量元素,根据这些顺序表的缺点,我们引入了链表。
链式存储线性表时,不需要使用地址连续的存储单元,即不要求逻辑上相邻的元素在物理位置上也相邻
它通过“链”建立元素之间的逻辑关系,因此插入和删除操作不需要移动元素,而只需修改指针,但也会失去顺序表可随机存取的优点。
2.3.1 单链表的定义
线性表的链式存储又称单链表,它是指通过一组任意的存储单元来存储线性表中的数据元素。为了建立数据元素之间的线性关系,对每个链表结点,除存放元素自身的信息之外,还需要存放一个指向其后继的指针
你可以理解成链表就是把顺序表全部“切”成一段一段,再利用指针将这些“碎片”连接起来,这些“碎片”就是结点
typedef struct LNode{//定义单链表节点类型
ElemType data;//数据域
struct LNode *next;//指针域
}LNode,*LinkList;//两者是一样的,都是别名
单链表解决了顺序表需要大量连续存储空间的缺点,但引入了指针域增加了一定空间上的开销
单链表的元素离散地分布在存储空间中,因此是非随机存取的存储结构,查找特定结点时,需要从头开始遍历,依次查找
在创建链表时,可以选择带头结点和不带头结点这两种形式
通常用*头指针L(或head等)*来标识一个单链表,指出链表的起始地址,头指针为NULL,表示空表。
在单链表第一个数据结点之前附加一个结点,这就是头结点,头结点可以不带任何的信息,也可以记录表长信息,引入头结点是为了操作上的方便
单链表带头结点时,头指针L指向头结点,不带头结点时,头指针L指向第一个数据结点,表尾指针域为NULL,如下图所示
引入头结点后,可以带来两个优点:
-
由于第一个数据结点的位置被存放在头结点的指针域中,因此在链表的第一个位置上的操作和在表的其他位置上的操作一致,无须进行特殊处理。
-
无论链表是否为空,其头指针都是指向头结点的非空指针(空表中头结点的指针域为空),因此空表和非空表的处理也就得到了统一。
2.3.2 单链表上基本操作的实现
带头结点单链表的操作代码书写较为方便,如无特殊说明,本节均默认链表带头结点。
1.单链表的初始化
带头结点和不带头结点的单链表的初始化操作是不同的。
带头结点的单链表初始化时,需要创建一个头结点,并让头指针指向头结点,头结点的next域初始化为NULL。
bool InitList(LinkList &L){//带头结点的单链表的初始化
L=(LNode*)malloc(sizeof(LNode));//之前定义了类型LNode,创建头结点
L->next=NULL;//头结点之后暂时还没有元素结点
return true;
}
不带头结点的单链表初始化时,只需将头指针L初始化为NULL。
bool InitList(LinkList &L){//不带头结点的单链表的初始化
L=NULL;
return true;
}
注意:
- 设p为指向链表结点的结构体指针,则*p表示结点本身,因此可用p->data或(*p).data访问*p这个结点的数据域,个人推荐前者,二者完全等价。
- 成员运算符(.)左边是一个普通的结构体变量,而指向运算符(->)左边是一个结构体指针。
- 通过(*p).next可以得到指向下一个结点的指针,直接用p->next->data,表示下一个结点的指针
2.求表长操作
就是记录数据结点的个数,主要的思想是遍历,从第一个结点开始访问下一个结点,设置一个变量(如len),len初始化为0,每访问一次,len+1
int Length(Linklist L){//因为要计算长度,故返回int整形len
int len=0;
LNode *p=L;
while(p->next!=NULL)
//如果下一个结点不为空,则继续
p=p->next;//访问下一个结点
len++;//注意顺序,访问后再++
//不要颠倒顺序!
}
return len;
}
求表长的时间复杂度是O(n),注意单链表的长度是不包含头结点的
3.按序号查找结点
思想:主要的思想是遍历,从单链表的第一个结点开始,沿着next域从前往后依次搜索,直到找到第i个结点为止,则返回该结点的指针;
若i小于单链表的表长,则返回NULL
LNode *GetElem(LinkList L,int i){
//LNode强调返回的是一个结点,形参Linklist强调这是个单链表
LNode *p=L; // 指针p指向当前扫描到的结点
int j=0; // 记录当前结点的位序,头结点是第0个结点
while(p!=NULL&&j<i){ // 循环找到第i个结点
p=p->next;
j++;
}
return p; // 返回第i个结点的指针或NULL
}
按序号查找操作的时间复杂度为O(n)
4.按值查找表结点
主要的思想还是遍历,从单链表的第一个结点开始,从前往后依次比较表中各结点的data
若某结点的data域等于给定值e,则返回该结点的指针
若整个单链表中没有这样的结点,则返回NULL。
LNode*LocateElem(LinkList L,ElemType e){
LNode *p=L->next;
while(p!=NULL&&p—>data!=e)
//代码3的意思是在遍历完之前找到data=e的结点
p=p->next;
return p;
//找到后返回该结点指针,否则返回NULL
}
按值查找操作的时间复杂度为O(n)=
5.插入操作
插入结点操作将值为x的新结点插入到单链表的第i个位置,插入之前要检查值和位置是否合法
首先查找第i-1个结点,假设第i-1个结点为*p,然后令新结点 s的指针域指向 *p的后继,再令结点 *p的指针域指向新插入的结点 *s
bool ListInsert(LinkList &L,int i,Elemtype e){
LNode *p=L;//指针p指向当前扫描到的结点
int j=0;
//记录当前结点的位序,头结点是第0个结点//循环找到第i—1个结点
p=p->next; j++; {
if(p==NULL) //i值不合法
return false;
LNode *s=(LNode*)malloc(sizeof(LNode)); //建立新的结点
s->data=e;//赋值
s->next=p->next;//关键
p->next=s;//关键
return true;
}
代码中的s->next=p->next和p->next=s顺序不能颠倒!,否则,先执行p->next=s后,指向其原后继的指针就不存在了,再执行s->next=p->next时,相当于执行了s->next=s,显然不对
需注意的是,当链表不带头结点时,若是,则要做特殊处理,将头指针L指向新的首结点。当链表带头结点时,插入位置i为1时不用做特殊处理。
本算法的时间开销在于查找第i-1个元素,时间复杂度为O(n),如果在指定结点后面插入新结点,则时间复杂度O(1)
6.删除结点的操作
思想:删除结点的操作是将单链表的第i个结点删除。先检查删除位置的合法性,然后查找表中第i-1个结点,就是被删结点的前驱,再删除第i个结点
bool ListDelete(LinkList &L,int i,ElemType &e){
LNode *p=L;//指针p指向当前扫描到的结点
int j=0;//记录当前结点的位序,头结点是第0个结点
while(p!=NULL&&j<i-1){//循环找到第i—1个结点
p=p->next;
j++;
}
if(p==NULLIIp->next==NULL)//i值不合法
return false;
LNode *q=p->next;//令q指向被删除结点
e=q->data;//用e返回元素的值
p->next=q->next;//将*q结点从链中“断开”
free(q);
//释放结点的存储空间
return true;
}
同插入算法一样,该算法的主要时间也耗费在查找操作上,时间复杂度为O(n)。
当链表不带头结点时,需要判断被删结点是否为首结点,若是,则要做特殊处理,将头指针L指向新的首结点
当链表带头结点时,删除首结点和删除其他结点的操作是相同的