2.2线性表的链式存储结构
**链式存储结构:**结点在存储器中的位置是任意的,即逻辑上相邻的数据元素在物理上不一定相邻。
线性表的链式表示又称为非顺序映像或链式映像
这组存储单元既可以是连续的,也可以是不连续的,甚至是零散分布在内存中的任意位置上的。
-
如何表示空表?
-
无头结点时,头指针为空时表示空表。
-
有头结点时,当头结点的指针域为空时表示空表。
-
-
头结点的数据域内装的是什么?
头结点的数据域可以为空,也可以存放线性表长度等附加信息,但此结点不能计入链表长度值。
1.单链表
1.1单链表的定义和表示
例如,存储学生学号、姓名、成绩的单链表结点类型定义如下:
typedef struct student {
char num[8];
char name[8];
int score;
struct student* next;
}Node,*LinkList;
为了统一链表的操作,通常这样定义:
typedef struct {
char num[8];
char name[8];
int score;
}ElemType;
typedef struct Lnode {
ElemType data;
struct Lnode* next;
}Lnode,*LinkList;
变量定义:
LinkList L;
LNode *p,*s;
重要操作:
p=L;//p指向头结点
s=L->next;//s指向首元结点
p=p->next;//p指向下一结点
1.2单链表的初始化(带头结点)
即构造一个空表。
【算法步骤】:
- 生成新结点作头结点,用头指针L指向头结点。
- 将头结点的指针域置空。
bool InitList_L(LinkList& L) {
L = new Lnode;//或L=(LinkList)malloc(sizeof(Lnode));
L->next = NULL;
return true;
}
1.3判断链表是否为空
空表:链表中无元素,称为空链表(头指针和头结点仍然在)
**【算法思路】**判断头结点指针域是否为空
1.4单链表的销毁
链表销毁后不存在
**【算法思路】**从头指针开始,依次释放所有结点
结束条件:L==NULL 循环条件:L!=NULL
bool DestoryList(LinkList& L) {
Lnode* p;
while (L) {
p = L;
L = L->next;
delete p;
}
return true;
}
1.5清空链表
链表仍存在,但链表中无元素,成为空链表(头指针和头结点仍然在)
**【算法思路】**依次释放所有结点,并将头结点指针域设置为空
结束条件:p==NULL 循环条件:p!=NULL
bool ClearList(LinkList& L) {
Lnode* p, * q;
p = L->next;
while (p) { //没到表尾
q = p->next;
delete p;
p = q;
}
L->next = NULL;//头结点指针域为空
return true;
}
1.6求单链表的表长
**【算法思路】**从首元结点开始,依次计数所有结点
int ListLength(LinkList L) {
Lnode* p;
p = L->next; //p指向第一个结点
int i = 0;
while (p) {
i++;
p = p->next;
}
return i;
}
1.7单链表取值
取单链表中第i个元素的内容
【算法思路】 从链表的头指针出发,顺着链域next逐个结点往下搜索,直至搜索到第i个结点为止。因此链表不是随机存取结构。
【算法步骤】:
- 从第1个结点(L->next)顺链扫描,用指针p指向当前扫描到的结点,p初值为p=L->next。
- j做计数器,累计当前扫描过的结点数,j初值为1。
- 当p指向扫描到的下一结点时,计数器j加1。
- 当j==i时,p所指的结点就是要找的第i个结点。
bool GetElem(LinkList L, int i, int& e) {//获取线性表中的某个数据元素的内容,通过变量e返回
Lnode* p;
p = L->next;
int j = 1;
while (j < i && p) {//向后扫描,直到p指向第i个元素或p为空
p = p->next;
++j;
}
if (!p || j > i) return false;
e = p->data;
return true;
}
1.8单链表的按值查找
按值查找——根据指定数据获取该数据所在的位置(该数据的地址)。
【算法步骤】:
- 从第一个结点起,依次和e相比较。
- 如果找到一个其值与e相等的数据元素,则返回其在链表中的“位置”或地址。
- 如果查遍整个链表都没有找到其值和e相等的元素,则返回0或者“NULL‘’。
Lnode* LocateElem(LinkList L, int e) {
//在线性表L中查找值为e的数据元素
//找到,则返回L中值为e的数据元素的地址,查找失败返回NULL
Lnode* p;
p = L->next;
while (p && p->data != e) {
p = p->next;
}
return p;
}
按值查找——根据指定数据获取该数据位置序号(是第几个数据元素)
1.9单链表的插入
在第i个结点前插入值为e的新结点。
【算法步骤】:
-
首先找到i-1的存储位置p;
-
生成一个数据域为e的新结点s。
-
插入新结点:①新结点的指针域指向结点i
②节点i-1的指针域指向新结点
①s->next (i结点的地址) =p->next;
②p->next=s;
//在L中第i个元素之前插入数据元素e
bool ListInsert(LinkList& L, int e, int i) {
Lnode* p;
p = L;
int j = 0;
while (p && j < i - 1) {//寻找第i-1个结点,p指向i-1结点
p = p->next;
++j;
}
if (!p || j > i - 1) return false;//i大于表长+1或者小于1,插入位置非法
Lnode* s = new Lnode;//生成新结点s,将结点s的数据域置为e
s->data = e;
s->next = p->next;
p->next = s;
return true;
}
1.10单链表的删除
删除第i个结点
【算法步骤】:
-
首先找到i-1的存储位置p,保存要删除的i的值。
-
令p->next指向i+1。
p->next = p->next->next
-
释放结点i的空间。
bool ListDelete(LinkList& L, int i, int& e) {
Lnode* p;
p = L;
int j = 0;
while (p->next && j < i - 1) {//寻找第i个结点,并令p指向其前驱
p = p->next;
++j;
}
if (!(p->next) || j > i - 1) return false;//删除位置不合理
Lnode* q;
q = p->next;//临时保存被删结点的地址以备释放
p->next = q->next;//改变删除结点前驱结点的指针域
e = q->data;
delete q;
return true;
}
1.11单链表的查找、插入、删除算法时间效率分析
- 查找:
- 因线性链表只能顺序存取,即在查找时要从头指针找起,查找的时间复杂度为O(n)。
- 插入和删除:
- 因线性链表不需要移动元素,只要修改指针,一般情况下时间复杂度为O(1)。
- 但是,如果要在单链表中进行前插或删除操作,由于要从头查找前驱结点,所耗时间复杂度为O(n)。
1.12单链表的建立
建立单链表:头插法——元素插入在链表头部,也叫前插法。
【算法步骤】:
- 从一个空表开始,重复读入数据;
- 生成新结点,将读入数据存放到新结点的数据域中;
- 从最后一个结点开始,依次将各节点插入到链表的前端。
例如,建立链表L(a,b,c,d,e)
void CreateList_H(LinkList& L, int n) {
L = new Lnode;
L->next = NULL;//先建立一个带头结点的单链表
for (int i = n; i > 0; --i) {
Lnode* p = new Lnode;
scanf("%d", & p->data);//输入元素值
p->next = L->next;//插入到表头
L->next = p;
}
}
算法的时间复杂度O(n)
建立单链表:尾插法——元素插入在链表尾部,也叫后插法。
【算法步骤】:
- 从一个空表L开始,将新结点逐个插入到链表的尾部,尾指针r指向链表的尾结点。
- 初始时,r同L均指向头结点。每读入一个数据元素则申请一个新结点,将新结点插入到尾结点后,r指向新结点。
void CreateList_F(LinkList& L, int n) {
L = new Lnode;
L->next = NULL;
Lnode* r;
r = L;//尾指针r指向头结点
for (int i; i < n; i++) {
Lnode* p;
scanf("%d", &p->data);
p->next = NULL;
r->next = p;
r = p;//r指向新的尾结点
}
}
2.循环链表
循环链表:是一种头尾相接的链表(即:表中最后一个结点的指针域指向头结点,整个链表形成一个环)。
优点:从表中的任一结点出发均可找到表中其他结点。
: 由于循环链表中没有NULL指针,故涉及遍历操作时,其终止条件就不再像非循环链表那样判断p或P->next是否为空,而是判断它们是否等于头指针。
循环条件:
p!=NULL ---> p!=L
p->next!=NULL ---> p->next!=L
单链表 单循环链表
表的操作常常是在表的首尾位置上进行。(用尾指针操作更方便)
带尾指针循环链表的合并(将Tb合并在Ta之后)
- p存表头结点 p=Ta->next
- Tb表头连接到Ta表尾 Ta->next=Tb->next->next
- 释放Tb表头结点 delete Tb->next
- 修改指针 Tb->next=p
LinkList Connect(LinkList Ta, LinkList Tb) {
Lnode* p;//假设Ta、Tb都是非空的单循环链表
p = Ta->next;//①p存表头结点
Ta->next = Tb->next->next;//②Tb表头连接Ta表尾
delete Tb->next;//③释放Tb表头结点
Tb->next = p;//④修改指针
return Tb;
}
3.双向链表
双向链表:在单链表的每个结点里再增加一个指向其直接前驱的指针域prior,这样链表中就形成了有两个方向不同的链,故称为双向链表。
双向链表的结构可定义如下:
typedef struct DuLNode {
int data;
struct DuLNode* prior, * next;
}DuLNode,*DuLinkList;
双向循环链表:
- 让头结点的前驱指针指向链表的最后一个结点。
- 让最后一个结点的后继指针指向头结点。
双向链表结构的对称性(设指针p指向某一结点):
p->prior->next = p = p->next->prior
在双向链表中有些操作(如:ListLength、GetElem等),因仅涉及一个方向的指针,故它们的算法与线性链表的相同。但在插入、删除时,则需同时修改两个方向上的指针,两者的操作的时间复杂度均为O(n)。
3.1双向链表的插入
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-61aHZJTH-1689762984853)(D:\学习\学习笔记\Typora-img\image-20230719160350565.png)]
1. s->prior=p->prior;
2. p->prior->next=s;
3. s->next=p;
4. p->prior=s;
bool ListInsert_Dul(DuLinkList& L, int i, int e) {
//在带头结点的双向循环链表L中第i个位置之前插入元素e
DuLNode* p = new DuLNode;
if (!(p = GetElemP_Du(L, i)))return false;
DuLNode* s = new DuLNode;
s->data = e;
s->prior = p->prior;
p->prior->next = s;
s->next = p;
p->prior = s;
return true;
}
3.2双向链表的删除
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-CAADak1q-1689762984853)(D:\学习\学习笔记\Typora-img\image-20230719162511270.png)]
1. p->prior->next=p->next;
2. p->next->prior=p->prior;
bool ListDelete_Dul(DuLinkList& L, int i, int& e) {
DuLNode* p = new DuLNode;
if (!(p = GetElemP_Dul(L, i))) return false;
DuLNode* e;
e = p->data;
p->prior->next = p->next;
p->next->prior = p->prior;
delete(p);
return true;
}
4.单链表、循环链表和双向链表的时间效率比较
查找表头节点 | 查找表尾结点 | 查找结点*p的前驱结点 | |
---|---|---|---|
带头结点的单链表L | L->next时间复杂度O(1) | 从L->next依次向后遍历时间复杂度O(n) | 通过p->next无法找到其前驱 |
带头结点仅设头指针L的循环单链表 | L->next时间复杂度O(1) | 从L->next依次向后遍历时间复杂度O(n) | 通过p->next可以找到其前驱时间复杂度O(n) |
带头结点仅设尾指针R的循环单链表 | R->next时间复杂度O(1) | R时间复杂度O(1) | 通过p->next可以找到其前驱时间复杂度O(n) |
带头结点的双向循环链表L | L->next时间复杂度O(1) | L->prior时间复杂度O(1) | p->prior时间复杂度O(1) |
2.3顺序表和链表的比较
链式存储结构的优点:
- 结点空间可以动态申请和释放;
- 数据元素的逻辑次序靠结点的指针来指示,插入和删除时不需要移动数据元素。
链式存储结构的缺点:
- 存储密度小,每个结点的指针域需额外占用存储空间。当每个结点的数据域所占字节不多时,指针域所占存储空间的比重显得很大。(存储密度是指结点数据本身所占的存储量和整个结点结构中所占的存储量之比。)
- 链式存储结构是非随机存取结构,对任一结点的操作都要从头指针依指针链查找到该节点,这增加了算法的复杂度。
顺序表适用情况:
- 表长变化不大,且能事先确定变化的范围。
- 很少进行插入或删除操作,经常按元素位置序号访问数据元素。
链表适用情况:
- 长度变化较大。
- 频繁进行插入或删除操作。