2.4 线性表的链式存储结构
线性表链式存储结构的特点是: 用 一 组任意的存储单元存储线性表的数据元素(这组存储单元可以是连续的,也可以是不连续的)。
其中存储数据元素信息的域称为数据域;存储直接后继存储位置的域称为指针域。指针域中存储的信息称作指针或链。n个结点链结成 一个链表,即为线性表的链式存储结构。
首元结点、头结点、头指针三者的区分:
- 首元结点是指链表中存储第一个数据元素a1 的结点。
- 头结点是在首元结点之前附设的一个结点,其指针域指向首元结点。
- 头指针是指向链表中第一个结点的指针。若链表设有头结点,则头指针所指结点为线性表的头结点;若链表不设头结点,则头指针所指结点为该线性表的首元结点。
链表增加头结点的主要作用:
- 便于首元结点的处理:头结点后,插入和删除数据元素的算法统一了,不再需要判断是否在第一个元素之前插入或删除第一个元素;
- 便于空表和非空表的统一处理:不论链表是否为空,其头指针是指向头结点的非空指针,链表的头指针不变,因此空表和非空表的处理也就统一了。
typedef struct Lnode {
ElemType data;//结点的数据域
struct Lnode* next;//结点的指针域
}Lnode, * LinkList;//LinkList为指向结构体LNode的指针类型
① 为了提高程序的可读性,在此对同一结构体指针类型起了两个名称,LinkList与LNode* ,两者本质上是等价的。通常习惯上用LinkList定义单链表,强调定义的是某个单链表的头指针;用LNode *定义指向单链表中任意结点的指针变量
② 注意区分指针变量和结点变量两个不同的概念,若定义 LinkList p或LNode *p,则p为指向某结点的指针变量,表示该结点的地址;而 *p 为对应的结点变量,表示该结点的名称。
2.5 单链表基本操作的实现
单链表的初始化
① 生成新结点作为头结点,用头指针L指向头结点。
② 头结点的指针域置空。
// 初始化单链表
Status InitList(LinkList& L) {
//① 生成新结点作为头结点,用头指针L指向头结点。
L = (Lnode*)malloc(sizeof(Lnode));//或者 L = new LNode;
if (!L) exit(OVERFLOW);//空间分配失败
//② 头结点的指针域置空。
L->next = NULL;
return OK;
}
单链表的基础操作
//判断单链表是否为空
Status ListEmpty(LinkList L) {
if (!L->next) return 1;//为空
return 0;
}
//单链表的销毁
Status DestroyList(LinkList& L) {
Lnode* p;
while (L) {
p = L;//p用于循环删除结点
L = L->next;//每循环一次让L指向下一元素
free(p);//释放p指向的结点
}
printf("单链表已被销毁!\n");
return OK;
}
//清空单链表
Status ClearList(LinkList& L) {
Lnode* p, * q;
p = L->next;//指针指向首元节点
while (p) {
q = p->next;//q指向要删除元素的下一节点
delete p;
p = q;
}
L->next = NULL;//头结点指针域置空
printf("单链表已清空!\n");
return OK;
}
//求单链表表长
Status ListLength(LinkList L) {
if (!L->next) return 0;//空表
Lnode* p;
p = L;
int count = 0;//计数器
while (p) {
p = p->next;//遍历单链表,统计节点数
++count;
}
return count - 1;//减去头结点
}
//单链表元素的遍历
Status ListPrint(LinkList L) {
Lnode* p;
p = L->next;
if (!p) return ERROR;//空链表
while (p) {
printf("%d\t", p->data);
p = p->next;
}
printf("\n");
return OK;
}
单链表的查找
按位查找:
- ①用指针p指向首元结点,用j做计数器初值赋为1。
- ② 从首元结点开始依次顺着链域next向下访问,只要指向当前结点的指针p 不为空( NULL),并且没有到达序号为i的结点,则循环执行以下操作:p指向下一个结点;计数器j相应加1。
- ③退出循环时,如果指针p为空,或者计数器j大于i,说明指定的序号i值不合法(i大于表长n或i小于等于0),取值失败返回ERROR;否则取值成功,此时j=i时,p所指的结点就是要找的第i个结点,用参数e保存当前结点的数据域,返回OK。
按值查找返回地址:
- ① 用指针p指向首元结点。
- ②从首元结点开始依次顺着链域next向下查找,只要指向当前结点的指针p不为空,并且p所指结点的数据域不等于给定值e,则循环执行以下操作:p指向下一个结点。
- ③返回p。若查找成功,p此时即为结点的地址值,若查找失败,p的值即为NULL
//按位查找,找第i个元素
Status GetElement(LinkList L, int i, ElemType& e) {
Lnode* p;
p = L->next;//p指向首元节点
int j = 1;//表示第一个元素
if (!p || i < j) return ERROR;//查找的位置不存在
while (p && i > j) {
p = p->next;
++j;
}
e = p->data;
return OK;
}
//按值查找,返回结点地址
Lnode* LocatElem(LinkList L, ElemType e) {
Lnode* p;
p = L->next;//初始化,p指向首元节点
while (p && p->data != e) {//顺链域向后扫描,直到p为空或p所指结点的数据域等于e
p = p->next;//p指向下一节点
}
return p;//查找成功返回e的结点p,查找失败返回NULL;
}
//按值查找,返回位序
int LocatElemN(LinkList L, ElemType e) {
Lnode* p;
p = L->next;
int i = 1;//用于返回位序
while (p && p->data != e) {
++i;
p = p->next;
}
if (p) return i;//只有结点不为空才找到
return 0;
}
单链表的插入
①查找结点ai -1并由指针p指向该结点。
② 生成一个新结点*s
③ 将新结点的数据域置为e
④ 将新结点的指针域指向结点ai
⑤ 将结点*p的指针域指向新结点
//单链表的插入
Status ListInsert(LinkList& L, int i, ElemType e) {
Lnode* p;
Lnode* s = new Lnode;
p = L; int j = 0;
if (!p || (i - 1 < j)) return ERROR;//插入位置不合法,i>n+1或者i<1;
while (p && i - 1 > j) {
p = p->next; j++;// 查找第i-1个结点,p指向该结点
}
s->data = e; s->next = p->next; p->next = s;//将新结点连接起来
return OK;
}
单链表的删除
① 查找结点ai-1并由指针p指向该结点。
② 临时保存待删除结点ai的地址在q中,以备释放。
③将结点*p的指针域指向ai的直接后继结点。
④释放结点ai的空间。
//单链表的删除
Status ListDelete(LinkList& L, int i, ElemType& e) {
Lnode* p, * q;
p = L; int j = 0;
if (!(p->next) || j > i - 1) return ERROR;//删除位置不合理
while ((p->next) && j < i - 1) {
p = p->next; j++;//查找第i-1个结点,p指向该结点
}
q = p->next;//临时保存被删除结点
e = q->data;//保存删除结点的元素值
p->next = q->next;//改变删除结点前驱结点的指针域
free(q);//释放删除结点的空间.
return OK;
}
单链表的头插法和尾插法
前插法
- ① 创建一个只有头结点的空链表。 ② 根据待创建链表包括的元素个数n,循环n次执行以下操作:生成一个新结点* p;输入元素值赋给新结点*p的数据域;将新结点 *p插人到头结点之后。
尾插法
- ① 创建一个只有头结点的空链表。
- ② 尾指针r初始化,指向头结点。
- ③ 根据创建链表包括的元素个数n,循环n次执行以下操作:生成一个新结点* p;输入元素值赋给新结点* p的数据域;将新结点* p插入到尾结点* r之后;尾指针r指向新的尾结点* p。
//单链表的头插法插入多个元素
void CreateList_H(LinkList& L, int n) {
Lnode* p;
if (L) {//确保已初始化,创建了头结点
for (int i = 0; i < n; i++) {
p = new Lnode;
printf("请输入需要插入的第%d个值:", i + 1);
scanf("%d", &p->data);//输入元素赋值给新结点*p的数据域
p->next = L->next; L->next = p;//将新结点*p插入到头结点之后
}
}
}
//单链表的尾插法(仅适用于空表后插)
void CreateList_R(LinkList L, int n) {
Lnode* p, * r;//新结点声明和创建尾指针
r = L;//尾指针指向头结点
if (L) {//确保已初始化,创建了头结点
for (int i = 0; i < n; i++) {
p = new Lnode;
printf("请输入需要插入的第%d个值:", i + 1);
scanf("%d", &p->data);//输入元素赋值给新结点*p的数据域
p->next = NULL;
r->next = p;
r = p;
}
}
}
循环链表
循环链表:是一种头尾相连的链表,表中的最后一个结点的指针域指向头结点,整个链表形成一个环。
优点:从表中的任一结点出发均可找到表中其他节点。
注意:循环链表没有NULL指针,因此终止条件是判断它们是否等于头指针。
| 头指针表示单循环链表 | 尾指针表示单循环链表 | |
|---|---|---|
| 找a1的时间复杂度 | O(1) | O(1) |
| 找an的时间复杂度 | O(n) | O(1) |
LinkList Connect(LinkList Ta, LinkList Tb){
//假设Ta、Tb都是非空的单循环链表
p=Ta->next;//p存表头结点
Ta->next=Tb->next->next ;//②Tb表头连结Ta表尾
delete Tb->next;//③释放Tbfue'b>next);
Tb->next=p;//④修改指针
return Tb;//时间复杂度是O(1)。
}
双向链表
//双向链表的存储结构
typedef struct DuLNode{
ElemType data;//数据域
struct DuLNode *prior,*next;//直接前驱和直接后继
} DuLNode, * DuLinkList;
双向链表的插入
status ListInsert_DuL ( DuLinkList &L,int i,ElemType e){
//在带头结点的双向链表工中第i个位置之前插人元素e
if(!(p=GetElem_DuL(L,i))) return ERROR;
s=new DuLNode; s->data=e;
s->prior=p->prior; p->prior->next=s;
s->next=p; p->prior=s;
return OK;
}
双向链表的删除
status ListDelete_DuL ( DuLinkList &L,int i,ElemType &e){ //删除带头结点的双向链表工中的第i个元素
if(!(p=GetElem_DuL(L,i))) return ERROR;
e = p->data;
p->prior->next=p->next; //修改被删结点的前驱结点的后继指针
p->next->prior=p->prior;//修改被删结点的后继结点的前驱指针
delete p;//释放被删结点的空间
return OK;
}
2.6 单链表、循环链表、双向链表的时间效率比较
2.7 顺序表和链表的优缺点
| 优点(顺序表) | 缺点(顺序表) |
|---|---|
| 存储密度大,无须为顺序表中元素之间的逻辑关系而增加额外存储空间 | 在插入、删除某一元素时,需要移动大量元素 |
| 可以随机存取表中任一元素 | 造成存储空间的“碎片”,浪费存储空间 |
| 优点(链表) | 缺点(链表) |
|---|---|
| 插入和删除不需要移动数据元素 | 存储密度小,指针需要额外占用存储空间 |
| 结点空间可以动态申请和释放 | 元素需要顺序存取 |
存储密度 = 节点数据本身占用的空间 / 结点占用的空间总量
2.8 顺序表和链表的比较
2.9 线性表的应用
已知两个有序集合A和B,数据元素按值非递减有序排列,现要求一个新的集合C= A∪B,使集合C中的数据元素仍按值非递减有序排列。
Status ListUnion(SqList La, SqList Lb, SqList& Lc) {
//① 获取三个顺序表的长度
int La_len = La.length;
int Lb_len = Lb.length;
int Lc_len = Lc.length;
//② 将La中的数据进行遍历
for (int i = 0; i < La_len; i++) {
//③ 插入数据La到Lc时,先判断此时Lc中是否还有表空间
if (Lc_len == Lc.MaxSize) CreateList(Lc, 10);
//④ 将La中的每一个数据存入到Lc中
ListInsert(Lc, i + 1, La.data[i]);
}
//⑤ 遍历Lb中的每一个数据元素与Lc中的元素进行比较
for (int j = 0; j < Lb_len; j++) {
//⑥ Lb中的数据插入到Lc之前,需要拿Lb中的数据与Lc中每一个元素进行比对吗,不相同则插入
if (LocatElem(Lc, Lb.data[j]) == 0) {//根据按位查找,没找到相同元素则返回0
//⑦ 插入Lb中的元素时,先判断此时Lc中是否还有表空间
if (Lc.length == Lc.MaxSize) CreateList(Lc, 10);
//⑧ 调用插入函数,每次都插入到Lc中的最后一个位置
ListInsert(Lc, Lc.length+1, Lb.data[j]);
}
}
//⑨ 将Lc中的数据进行排序,调用冒泡排序函数
ListSort(Lc);
return OK;
}