数据结构与算法基础(二)b线性表----链表
一、链表
1.1 链式存储结构
物理位置任意的 存储单元 来存放线性表的数据元素
存储单元可连续 可零散
逻辑次序 物理次序 不一定相同
1.2 概念
1.2.1结点
数据元素的存储映像,数据域、指针域两部分组成
1.2.2链表
n个结点由指针链组成一个链表
他是线性表的链式存储映像,称为线性表的链式存储结构
1.2.3 单链表
结点只有一个指针域的链表(线性链表)
1.2.4 双链表
结点有两个指针域的链表
1.2.5 循环链表
首尾相接的链表
1.2.6头指针 头结点 首元结点
头指针:指向链表中第一个结点的指针
首元结点:存储第一个数据元素的结点
头结点: 首元结点前 附加的一个结点
1.3 链式表的表示
1.3.1 空表的表示
无头结点时,头指针为空时
有头结点时,头结点的指针域为空
1.3.2 头结点的好处
便于处理首元结点
便于空表和非空表的统一处理:无论链表是否非空,头指针都是指向头结点的非空指针,因此空表和非空表的处理也就统一了
1.3.3 头结点 的数据域中装什么?
可为空,可存放线性表的长度等附加信息
1.4 链表的特点
- 结点在存储器中的位置是任意的,逻辑和物理不一定相邻
- 访问时只能通过头指针进入链表,顺着每个节点的指针域依次向后扫描其余结点,寻找第一个结点和最后一个结点所花费的时间不等
- 链表—顺序存取法
- 顺序表----随机存取
二、单链表
## 2.1 定义
单链表 由表头唯一确定,因此单链表可以用头指针的名字命名, 头指针名为L,则链表称为L
2.2 单链表的存储结构
数据域,指针域
嵌套的定义,指针又指向一个包含两个元素的结构体
Lnode表示结点的结构体 Lnode a; a.data a.next
定义链表L: LinkList L;
定义结点指针p: LNode*p <----->LinkList p;
2.3 举例
2.4 单链表的基本操作
2.4.1 单链表的初始化
构造一个空表
- 开辟一块儿内存,生成新结点 作为头结点,用头指针指向头结点
- 头结点的指针域置空
算法描述:
Status InitiList_L(LinkList &L){
L=new LNode;//new一个结点,new的结果是指向结点的指针,赋值给L
//或 L=(LinkList)malloc(sizeof(Lnode));给Lnode的长度分配一块空间,转换成LinkList的地址
L->next=Null;//L的下一个域 置空
return OK;
}
2.4.2 判断链表是否为空
思路:头结点的指针域是否为空
int ListEmpty(LinkList L){//传入一个链表L,判断若是空表返回1
if(L->next) //头指针有数据域和指针域,判断指针域的情况;非空
return 0;
else
return 1;
}
2.4.3 单链表的销毁:链表销毁后不存在
思路:从头结点开始,依次释放每一个结点
L是这个链表 需要一个额外的指针p
指针p指向头指针L-------p中存储L的地址
p=L 将L赋值给p
L指向下一个结点----下一个节点的地址赋值给L:L=L->next;
现在就可把p指向的结点删除 free(p);(c) delete p; (c++)
什么时候结束,当L指向空
结束条件:L==NULL
循环条件:L!=NULL 或 L
Status DestroyList_L(LinkList &L){//销毁单链表L
Lnode *p;//或LinkList P;//当前想要销毁的结点
while(L){//当前结点不为空
p=L;
L=L->next;
delete p;
}
return OK;
}
2.4.4 清空链表
链表仍存在,单链表中无元素,成为空链表(头指针和头结点热然存在)
思路: 依次释放所有结点,并将头结点的指针域设置为空
L 头结点
a1 首元结点
L->next L指向首元结点的指针域,并赋值给p
p指向首元结点
(回忆上一个算法要连同头结点一起删除,p=L从第一个开始
从第二个开始p=L->next; )
想要删除p但是就无法定位a2,则q指向a2的指针域,就可以删除p了;
p=q; q赋值给p,q再指向下一个指针域
Status ClearList(LinkList &L){//将L重置为空表
Lnode *p,*q; //或LinkList p,q;p用来存放要删除的结点,q存放下一个结点
p=L->next;
while(p){ //没到表尾
q=p->next;
delete p;
p=q;
}
L->next=Null; //头结点指针域为空
return OK;
}
2.4.5 求单链表的表长
思路:从首元结点开始依次计数所有的结点
基本操作:p=p->next
int ListLength_L(LinkList L){//返回L中数据元素的个数
LinkList p; //或LNode *p 指向单链表中一个结点的指针变量
p=L->next; //L指向头结点的指针域 p指向第一个结点
i=0;
while(p){ //遍历单链表,统计结点数
i++;
p=p->next;
}
return i;
}
回顾
2.4.6 取值 取单链表中第i个元素的内容
思考:顺序表中如何找到第i个元素? 找到elem[i-1]
-从链表的头指针出发,顺着链域next逐个结点向下搜索,知道搜索到第i个结点为止,链表不是随机存取结构
-算法:
Status GetElem_L(LinkList L,int i,ElemType &e){//从链表L中获取第i个元素的值,由变量e返回 ; &e引用型变量
p=L->next; j=1; //初始化 指针指向首元结点,计数器在第一个结点
while(p&&j<i){//向后扫描,直到p指向第i个元素或p为空
p=p->next;++j;
}
if(!p||j>i) return ERROR;//取第i个元素不存在
e=p->data;//取第i个元素
return OK;
}
2.4.7 按值查找
–根据指定数据获取该数据所在的位置(地址)
Lnode *LocateElem_L(LinkList L,Elemtype e){
//在线性表L中查找值为e的数据元素
//找到,则返回L中值为e的数据元素的地址,查找失败则返回NULL
p=L->next;//p指向首元结点
while(p&&p->data!=e)
p=p->next;//执行次数最多,最好只用1次,做多n次,所以O(n)
return p;
}
–按值查找,找到后返回该数据的位置序号
//在线性表L中查找值为e的数据元素的位置序号
int LocateElem_L(LinlList L,Elemtype e){//返回L中值为e的数据元素的位置序号,查找失败返回0
p=->next;j=1;
while(p&&p->data!=e)//指针不为空且还没有找到
{p=p->next;j++;}
if(p) return j;//
else return 0;
}
2.4.8 插入
不可先2后1,ai的地址会丢失
Status ListInsert_L(LinkList &L,int i,ElemType e){
//寻找i-1个结点,p指向i-1结点
p=L;j=0;
while(p&&j<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;
p->next=s;
}
return OK;
2.4.9 删除
Status ListDelete_L(LinkList &L,int i, ELemtype &e){//链表L,要删除的位置i,删除的结点可通过e保存
p=L;j=0;
while(p->next&&j<i-1){p=p->next;++j;}//找到要删除结点i的前驱i-1,指针变量p指向i-1
if(!(p->next)||j>i-1) return ERROR;
q=p->next;//p为i-1个结点,q为第i个结点,临时保存被删结点i
p->next=q->next;//修改被删结点前驱结点的指针域
e=q->data;//保存第i个结点的数据
delete q;
return OK;
}
插入和删除的时间复杂度:
因为线性链表不需要移动元素,只要修改指针,一般情况下时间复杂度为O(1)
但由于要从头查找前驱结点,所消耗的时间复杂度为O(n)
2.4.10单链表的建立
- 头插法:元素插在链表的头部
void CreateList_H(LinkList &L,int n){//插入n次,循环n次
L=new LNode;//生成头结点
L->next=NULL;//头结点的指针域置空
for(i=n;i>0;i--){
p=new LNode;//生成新结点p=(LNode)*malloc(sizeof(LNode));
cin>>p->data;//输入元素值,放在data域 scanf(&p->data)
p->next=L->next;// 原来的首元结点L->next 的地址赋值给新结点的地址域。即新结点插到表头
L->next=p; //将新结点接在链表头结点
}
}
头插法的时间复杂度为O(n)
- 尾插法: 元素插在链表尾部,后插法
void createList_R(LinkList &L,int n){//引用型
L=new LNode;//开辟一块新内存为头结点, L头指针
L->next=NULL;//给头结点的next域赋值为空
r=L;//此时尾指针也指向头节点,将头指针赋值给尾指针
for(i=0;i<n;++i){//插入n个结点,执行n次
p=new LNode;//生成新结点
cin>>p->data;//输入元素值
p->next=NULL;//置空
r->next=p;// 将新结点赋值给尾结点;即插入到表尾
r=p; //改变尾指针,尾指针指向新结点
}
}
算法的时间复杂度为O(n)
三、循环链表
3.1 定义
一种头尾相接的链表
表中最后一个节点的指针域指向头结点,整个链表形成一个环
优点:从表中任意出发均可以找到表中其他结点
3.2 结束条件的判断
3.3 循环链表的合并
上述操作分别对应:
- p=Ta->next;
- Ta->next=Tb->next->next;// Tb->next是b1的地址
- delete Tb->next;
- Tb->next=p;
LinkList Connect(LinkList Ta,LinkList Tb){//假设 ta tb都是非空的单循环链表
p=Ta->next;//1.p存头结点
Ta->next=Tb->next->next;//2. tb表头连结ta表尾
delete Tb->next;//3. 释放tb的表头结点
Tb->next=p;//4.修改指针
return Tb;
}
时间复杂度:
每个语句都执行一次,共四次,O(1)
四、双向链表
4.1 双向链表的结构
4.2 双向循环链表
4.2.1 双向链表的插入
1. x的前驱要存a的地址;a的地址存在b的prior域里
2. a的next域要存x的地址;a的next是p的前驱的后继
3. x的next域里要存b的地址
4. b的前驱要存x的地址
Void LinkInsert_DuL(DuLinkList&L,int i,ElemType e){//在带头结点的双向循环链表L中的第i个位置之前插入元素e
if(!(p=GetElemP_DuL(L,i))) return ERROR;
s=new DuLNode;
s->date=e;
s->prior=p->prior;//1
p->prior->next=s;//2
s->next=p;//3
p->prior=s;//4
return OK;
}//ListInsert_DuL
4.2.2 双向链表的删除
1. a的next域要存c的地址; c的地址存在b的next域里
2. c的prior域里要存a的地址;a的地址存在b的prior域里
Void LinkDelete_DuL(DuLinkList&L,int i,ElemType &e){//删除带头结点的双向循环链表L中的第i个元素并用e返回
if(!(p=GetElemP_DuL(L,i))) return ERROR;
e=p->data;
p->prior->next=p->next;//1
p->next->prior=p->prior;//2
free(p);//释放
return OK;
}//ListDelete_DuL
4.3 单链表、循环链表和双向链表的
五、 顺序表和链表的比较
六、应用举例
6.1线性表的应用
6.1.1 线性表的合并:求并集
- 算法步骤: 依次取出Lb中的元素,在La中查找该元素,如果找不到,则将其插在La的最后
Void union(List &La,List Lb){
La_len=ListLength(La);
Lb_len=ListLength(Lb);
for(i=1;i<=Lb_len;i++){
GetElem(Lb,i,e);
if(!LocateElem(La,e)) ListInsert(&La,++La_len,e)
}
}
}//ListDelete_DuL
算法的时间复杂度是: O(ListLength(La)*ListLength(Lb))
6.1.2 有序表的合并
算法步骤:
1创建一个空表Lc
2依次从La Lb中摘取元素值较小的结点插在Lc的最后,直至其中一个变为空表为止
3继续将剩余表的剩余结点插在Lc表的最后
用顺序表
Void MergeList_Sq(SqList LA,SqList LB,SqList &LC){
pa=LA.elem;
pb=LB.elem;// 两个指针的初值分别指向两个表中的第一个元素
LC.length=LA.length+LB.length;//新表长度为两个表的长度之和
LC.elem=new Elemtype[LC.length];//为新表分配一个数组空间
pc=LC.elem;//指针pc指向新表的第一个元素
pa_last=LA.elem+LA.length-1;//指针pa_last指向LA表的最后一个元素
pb_last=LB.elem+LB.length-1;//指针pb_last指向LB表的最后一个元素
while(pa<=pa_last&&pb<=pb_last){//两个表都非空
if(*pa<=*pb) *pc++=*pa++; //依次摘取两表中较小的结点
else *pc++=*pb++;
}
while(pa<pa_last) *pc++=*pa++; // LB表已达到表尾,将LA中剩余元素加入LC
while(pb<=pb_last) *pc++=*pb++;//LA表已经达到表尾,将LB中剩余元素加入LC
}//MergeList_Sq
算法的时间和空间复杂度都是:O(ListLength(La)+ListLength(Lb))
用链表实现有序表的合并
Void MergeList_L(LinkList &La,LinkList &Lb,LinkList &Lc){//Lc是合并后的链表
pa=La->next;
pb=Lb->next;
pc=Lc=La;//用La的头结点作为Lc的头结点
while(pa&&pb){
if(pa->data<=pb->data){//比较pa pb指针data域的值
pc->next=pa;
pc=pa;
pa=pa->next;}
else{pc->next=pb; pc=pb;pb=pb->next;}
}
pc->next=pa?pa:pb;//插入剩余段,pa非空,则加入pa否则pb
delete Lb;//释放Lb的头结点
}
}//ListDelete_DuL
时间复杂度:最坏的情况 pa pb都要访问一次
O(ListLength(La)+ListLength(Lb))
空间复杂度:就在原来链表的基础上O(1)
6.2 案例分析
6.2.1 一元多项式的运算 加减乘运算
多项式相加的步骤:
void CreatePolyn(Polynumial &p,int n){//建立表示多项式的有序链表p,输入m项的系数和指数
p->new PNode;//建立一个带头结点的单链表
p->next=NULL;
for(i=1;i<=n;++i){//依次输入n个非零项
s=new PNode;//生成新结点
cin>>s->coef>>s->expn;//输入系数和指数
pre=p;//pre保存q的前驱,初值为头结点
q=P->next;//q初始化。指向首元结点
while(q&&q->expn<s->expn){//找到第一个大于输入指数的项*q
pre=q;
q=q->next;
}
s->next=q;//将输入项s插刀q和其前驱结点pre之间
pre->next=s;
}
}
6.2.2图书管理系统
若用顺序表:数组的每一个元素都是一个复合类型,包含有三个部分
若用链式结构:每个数据元素的data域都是由三个部分构成的结构类型
根据实际:通过序号查找:顺序;经常插入删除运算,链式
struct Book{
char id[20];
char name[50];
int price;
}
//顺序表
typedef struct{
Book *elem;
int length;
}SqList;
//链表
typedef struct LNode{
Book data;
struct LNode *next;
}LNode,*LinkList;