###链式存储结构###
用任意物理位置存储,存储单元可能恰好连续,但逻辑顺序和物理顺序就不一定相同了。
在存储这一个元素的值还要存储下一个元素的地址。一个节点分成两个部分(数据域,指针域)
节点:数据元素的存储映像。数据域+指针域。
链表:n个结点由指针链连接在一起。它是线性表的链式存储映像。
※链表类型:
- 单链表:节点只有一个指针域的链表,称为单链表或线性链表。
- 双链表:结点有两个指针域的链表,一个用来存储上一结点地址,一个用存储下一结点的地址,还有一个数据域。
- 循环链表:首尾相接的链表(单循环,双循环)
头指针:是指向链表中第一个结点的指针(可带可不带)
首元结点:是指链表(线性表)中存储的第一个数据元素a1的结点
头结点:是在链表的寿元结点之前附设的一个结点
有几个问题需注意
- 空表:无头结点时,头指针为空时表示空表;有头结点时,当头结点的指针域为空表示空表
- 头结点的好处:首元结点地址在头结点指针域中,故处理链表的第一位置和其它位置一致,无需特殊处理;便于统一处理空表和非空表
- 头结点的数据域:头结点数据域可以为空,也可以存放表长等附加信息,但是此结点不可以计入链表长度值。
- 链表(链式存储结构的特点)
- 结点位置任意,逻辑与物理无关
- 访问时只能通过头指针进入链表,并通过每一个结点的指针域依次向后扫描其余节点,所以寻找第一个结点和最后一个时间不等、
###单链表的定义和表示###
单链表是由表头唯一确定,故单链表可以用头指针名字命名,若头指针名为L,则把链表称表L
单链表的存储结构(利用结构体实现)
typedef struct Lnode { //声明结点类型和指向结点的指针类型
ElemType data; //结点数据域
struct Lnode *next; //结点指针域(嵌套定义)
} Lnode, *LinkList; //LinkList为指向结构题Lnode的指针类型
typedef ...........Lnode,*LinkList 同时给这个结点结构体类型和指向这种结点结构题类型的指针起了新的名字
结点类型叫Lnode;指向结点指针类型叫LinkList;
下一次定义结点直接 Lnode L;
定义指向结点指针可以Lnode *p;也可以LinkList p;
例如:存储学生学号姓名成绩的单链表结点类型如下
typedef struct student {
char num[8]; //数据域
char name[8]; //数据域
int score; //数据域
struct student *next; //指针域
} Lnode, *LinkList;
为了统一操作,我们通常这样来定义:
typedef struct {
char num[8];
char name[8];
int score;
} ElemType; //这样与各种方法操作就统一了
###单链表的基本操作的实现###
算法1:单链表的初始化(构造一个空表)
步骤:生成新节点作为头结点,用头指针L指向头结点;将头结点的指针域置空;
Status InitList_L(LinkList &L) {
L = new LNode; //或者 L = (LinkList)malloc(sizeof(LNode)); LNode 是上面定义出来的结点类型
L -> next = NULL; //指针指向的结构的成员变量用->
return OK;
}
算法2:判断链表是否为空:链表为空,相当于头结点的指针域为空
bool ListEmpty(LinkList L) {
if (L -> next) return 0;//非空返回0
else return 1;
}
算法3:单链表的销毁,链表销毁后就不存在了
思路是从头指针开始,依次释放所有结点
p用来删除先前new出来的结点空间,L则是不断向前移动,直到移动到最后空
Status DestroyList_L(LinkList &L) {
Lnode *p;
while (L) {
p = L;
L = L->next; //这一步很重要!!!最后指向空时,p指向最后一个结点,刚好释放所有空间
delete p;
}
return OK;
}
算法4:清空链表
链表仍然存在,但是链表中没有元素,成为空链表了(头指针,头结点仍然存在)
第一步:
如果没有头结点:p = L; 此时p指向首元结点
如果有头结点:p = L->next;此时p通过L指向的头结点的指针域指向首元结点
第二步:
用指针变量q记录下一个结点地址,一边p删除后找不到下一节点
反复执行:p = q; q = q->next; 注意这两部不可以交换,以面地址丢失
结束条件:p == NULL; 循环条件:p != NULL;
Status ClearList(LinkList &L) { //将L重置为空表
Lnode *p, *q; //或者LinkList p,q;两个结点指针
p = L->next; //将p指向首元结点
while (p) { //p没有到表尾,p非空意味着还有结点还可以删,p空了就说明结点删完了
q = p->next; //q用来记录下一个结点地址,以面下一节点地址丢失
delete p; //安心删除p所指的空间
p = q; //p跟上q
}
L->next = NULL; //将头结点指向空,此时为空表
return OK; //返回成功
}
算法5:求单链表表长
从首元结点开始依次计数所有结点
int ListLength_L(LinkList L) { //返回L中数据元素个数
LinkList p = L->next; //p指向首元结点
int i = 0;
while (p) { //p非空结点就计数
i++;
p = p->next;
}
return i;
}
###单链表的进阶操作###
算法1:取值——取单链表当中第i个元素
从链表头指针出发,顺着链子出发,知道搜索到第i个节点位置,链表不是随机存取结构
Status GetElem_L(LinkList L, int i, ElemType &e) { //获取线性表L中的某个数据元素的内容,通过变量e返回
p = L->next; j = 1; //初始化,p指向首元结点
while (p&&j<i) { //向后扫描,直到p指向第i个元素p为空
p = p->next; ++j;
}
if (!p||j>i) return ERROR; //第i个元素不存在
e = p->data; //取第i个元素
return OK;
} //GetElem_L
算法2:
按值查找——根据指定数据获得数据所在位置(地址)
//在线性表中L中查找值为e的数据元素的位置序号
Lnode *LocateElem_L(LinkList L, ElemType e) {
//返回L中值为e的位置元素的位置序号,查找失败返回0
p = L->next; j = 1;
while (p&&p->data!=e) {
p=p->next; j++;
}
if (p) return j;
else return 0;
}
算法3:
-
首先找到的存储位置p
-
生成一个数据域为e的新节点s
-
插入新节点
-
新节点指针域指向ai
-
结点的指针域指向新节点
-
//在L第i个元素之前插入数据元素 e,只能在第i个元素之前,而不能在之后,这意味着,要在链表尾部加元素是不能通过该方法解决的。
Status ListInsert_L(LinkList &L, int i, ElemType e) {
p = L; j = 0; //从位置为0就可以插入了,也就是头结点,而非首元结点
while (p&&j<i-1) { //寻找第i-1个结点,p指向i-1结点
p=p->next; ++j;
}
if (!p||j>i-1) return ERROR; //i大于表长+1或者小于1,插入位置非法
s = new Lnode; s->data = e; //生成新节点s,将节点s的数据域置为e
s->next=p->next; //生成新节点s,将节点s的数据域置为e
p->next=s; //将节点s插入L中
return OK;
} //ListInsert_L
算法4:删除——删除第i个结点
-
首先找到的存储位置p,保存要删除ai的值
-
令p->指向
p->next=p->next->next //指向后面的后面
-
释放结点ai的空间
//将线性表L中的第i个元素删除
Status ListDelete_L(LinkList &L, int i, ElemType &e) {
p = L; j=0; //最开始指向头结点,也就是首元结点之前
while (p->next&&j<i-1) {
p=p->next; ++j;
} //寻找第i个结点就是从首元结点开始向后走i-1次,并令p指向要找结点的前驱
if (!(p->next)||j>i-1) return ERROR;
//删除位置不合法p指向结点的指针域是空表示指向表尾,j>i-1表示所找的前驱位置小于零;这两种情况一种是所找i结点小于等于就0,一种是找的i结点位置大于表尾元素位置,都是越界查找了,因此返回ERROR
q=p->next; //找到目标节点的前驱保存目标节点
p->next=q->next; //让前驱结点直接指向后继节点
e=q->data; //保存删除节点的数据域,也可以不用保存,万一删除结点是有用的,看情况操作
delete q; //释放删除节点的空间
return OK; //返回工作状态成功
} //ListDelete_L
-
分析前四个算法的时间效率:
-
查找算法:最差1次循环,最差n次循环,所以O(n)
-
插入和删除:链表插入不像列表不用移动元素,只要修改指针,一般情况下时间复杂度为O(n),其实就是查找到前驱结点的复杂度
-
如果在单链表中经行前插或者删除操作,由于要从头查找前驱结点时间复杂度为O(n)
-
###单链表进阶操作###
算法1:取值——取单链表当中第i个元素
从链表头指针出发,顺着链子出发,知道搜索到第i个节点位置,链表不是随机存取结构
Status GetElem_L(LinkList L, int i, ElemType &e) { //获取线性表L中的某个数据元素的内容,通过变量e返回
p = L->next; j = 1; //初始化,p指向首元结点
while (p&&j<i) { //向后扫描,直到p指向第i个元素p为空
p = p->next; ++j;
}
if (!p||j>i) return ERROR; //第i个元素不存在
e = p->data; //取第i个元素
return OK;
} //GetElem_L
算法2:
按值查找——根据指定数据获得数据所在位置(地址)
//在线性表中L中查找值为e的数据元素的位置序号
int LocateElem_L(LinkList L, ElemType e) {
//返回L中值为e的位置元素的位置序号,查找失败返回0
p = L->next; j = 1;
while (p&&p->data!=e) {
p=p->next; j++;
}
if (p) return j;
else return 0;
}
算法3:
-
首先找到的存储位置p
-
生成一个数据域为e的新节点s
-
插入新节点
-
新节点指针域指向
-
结点的指针域指向新节点
-
//在L第i个元素之前插入数据元素 e,只能在第i个元素之前,而不能在之后,这意味着,要在链表尾部加元素是不能通过该方法解决的。
Status ListInsert_L(LinkList &L, int i, ElemType e) {
p = L; j = 0; //从位置为0就可以插入了,也就是头结点,而非首元结点
while (p&&j<i-1) { //寻找第i-1个结点,p指向i-1结点
p=p->next; ++j;
}
if (!p||j>i-1) return ERROR; //i大于表长+1或者小于1,插入位置非法
s = new Lnode; s->data = e; //生成新节点s,将节点s的数据域置为e
s->next=p->next; //生成新节点s,将节点s的数据域置为e
p->next=s; //将节点s插入L中
return OK;
} //ListInsert_L
算法4:删除——删除第i个结点
-
首先找到的存储位置p,保存要删除ai的值
-
令p->指向 p->next=p->next->next //指向后面的后面
-
释放结点ai的空间
//将线性表L中的第i个元素删除
Status ListDelete_L(LinkList &L, int i, ElemType &e) {
p = L; j=0; //最开始指向头结点,也就是首元结点之前
while (p->next&&j<i-1) {
p=p->next; ++j;
} //寻找第i个结点就是从首元结点开始向后走i-1次,并令p指向要找结点的前驱
if (!(p->next)||j>i-1) return ERROR;
//删除位置不合法p指向结点的指针域是空表示指向表尾,j>i-1表示所找的前驱位置小于零;这两种情况一种是所找i结点小于等于就0,一种是找的i结点位置大于表尾元素位置,都是越界查找了,因此返回ERROR
q=p->next; //找到目标节点的前驱保存目标节点
p->next=q->next; //让前驱结点直接指向后继节点
e=q->data; //保存删除节点的数据域,也可以不用保存,万一删除结点是有用的,看情况操作
delete q; //释放删除节点的空间
return OK; //返回工作状态成功
} //ListDelete_L
-
分析前四个算法的时间效率:
-
查找算法:最差1次循环,最差n次循环,所以O(n)
-
插入和删除:链表插入不像列表不用移动元素,只要修改指针,一般情况下时间复杂度为O(n),其实就是查找到前驱结点的复杂度
-
如果在单链表中经行前插或者删除操作,由于要从头查找前驱结点时间复杂度为O(n)
-
###单链表的建立###
算法1:头插法——元素插入在链表的头部,也叫前插法
-
从一个空表开始,重复读入数据;
-
生成新节点,将读入数据存放在新节点数据域中
-
从最后一个结点开始,依次将各个结点插入到链表前端
例如:建立链表L(a, b, c, d, e)
1.创建新表 L= new LNode;
//或C语言中如下: L=(LinkList)malloc(sizeof(LNode)): 创建头结点,返回头结点指针,并将头结点的指针域置空 L->next=NULL;
2.插入元素(反复执行)
p = new LNode; p->data=an; //创建插入结点
p->next = L->next; //把新节点的指针域指向L头结点后面的链的第一个结点
L->next=p; //把头结点的指针指向新节点
void CreateList_H(LinkList &L, int n) {
L=new Lnode;
L->next=NULL; //先建立一个带头结点的单链表
for (i=n; i>0; i--) {
p= new LNode, //成新结点p=(LNode*)malloc(sizeof(LNode));
cin>>p=>data; //入元素值 scanf(&p->data);
p->next=L>next; //插入到表头
L->next=p;
}
} //CreateList_H
//时间复杂度是O(n)
算法2:尾插法——元素插入在链表尾部
1.从一个空表L开始,将新结点逐个插入到链表的尾部,尾指针r指向链表的尾结点。
2.初始时,r同L均指向头结点。每读入一个数据元素则申请一个新结点,将新节点插入尾结点后,r指向新节点。
//正位序输入n个元素的值,建立带头结点的单链表L
void CreateList_R(LinkList &L, int n){
L=new LNode; L->next=NULL;
r = L;//尾指针r指向头结点
for(i=0; i<n; i++) {
p= new LNode; cin>>p->data;//生成新结点,输入元素值
p->next=NULL;
r->next=p; //插入到表尾
r=p;//指向新的尾结点
}
} //CreatList_R
//时间复杂度O(n)
这样一来单链表部分就学习完了,理解记忆工作量都比较大,需要有一定的语言基础,内容相当重要,加油👊。