目录
链式存储与链表:用一组物理位置任意的存储单元来存放线性表的数据元素。这组存储单元既可以是连续的,也可以是不连续的,甚至是零散分布在内存中的任意位置上的。链表中元素的逻辑次序和物理次序不一定相同。
一. 一些术语
结点:数据元素的存储映像,由两部分组成:数据域+指针域;
头指针H:记录第一个元素的地址;链表由n个结点和指针链组成,它是线性表的链式存储映像,称为线性表的链式存储结构。
单链表由头指针唯一确定,因此单链表可以用头指针的名字来命名。
单链表,双链表,循环链表:结点只有一个指针域的链表,称为单链表或线性链表;结点有两个指针域的链表,称为双链表;首尾相接的链表称为循环链表;
头指针,头结点和首元结点:头指针:是指向链表中第一个结点的指针;首元结点:是指链表中存储第一个数据元素a1的结点;头结点:是在链表的首元结点之前附设的一个结点;(因此,链表的存储结构可以带头结点,也可以不带头结点)
例:26个英文字母的链式存储结构
讨论1:如何表示空表?
无头结点时,头指针为空时表示空表;有头结点时,当头结点的指针域为空时表示空表;
讨论2:在链表中设置头结点有什么好处?
1.便于首元结点的处理:首元结点的地址保存在头结点的指针域中,所以在链表的第一个位置上的操作和其它位置一致,无须进行特殊处理;
2.便于空表和非空表的统一处理:无论链表是否为空,头指针都是指向头结点的非空指针,因此空表和非空表的处理也就统一了;
讨论3:头结点的数据域装的是什么?
头结点的数据域可以为空,也可存放线性表长度等附加信息,但此结点不能计入链表长度值。
讨论4:链表(链式存储结构)的特点?
(1)结点在存储器中的位置是任意的,即逻辑上相邻的数据元素在物理上不一定相邻。
(2)访问时只能通过头指针进入链表,并通过每个结点的指针域依次向后顺序扫描其余结点,所以寻找第一个结点和最后一个结点所花费的时间不等(顺序存取);
顺序表->随机存取;链表->顺序存取;
二. 单链表的定义与表示
单链表由表头唯一确定,因此单链表可以用头指针的名字来命名,若头指针是L,则把此链表称为表L。
typedef struct Lnode //声明结点类型和指向结点的指针类型
{
ElemType data; //结点的数据域
struct Lnode *next; //结点的指针域,指向定义的结构体,嵌套定义
}Lnode,*LinkList; //LinkList为指向结构体Lnode的指针类型
注意:在C++中,结构体的定义需要使用struct
关键字,struct Lnode
表示定义了一个名为Lnode
的结构体。而typedef
关键字可以用来为数据类型创建别名,将struct Lnode
与一个新的名称关联起来,使得使用该结构体更加方便。通过这段代码,我们可以使用Lnode作为结构体类型的名称(Lnode就相当于struct Lnode了),以及LinkList作为指向Lnode结构体的指针类型的名称。这里Lnode和LinkList都是类型。通过使用typedef关键字定义结构体别名,可以简化代码的书写,并提高代码的可读性和可维护性。
定义链表L:LinkList L(这里定义了一个头指针L指向了单链表);Lnode *L(一般不用,也是定义指针指向头结点);
定义结点指针p:Lnode *p(指针加*,Lnode这里意思是指向Lnode类型的指针)或LinkList p(LinkList本身就是指针型,不用加*);
例如,定义一个学生信息管理系统。可以用以下定义:
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;
typedef struct Lnode
{
ElemType data;
struct Lnode *next;
}Lnode,*LinkList;
三. 单链表基本操作的实现
(1)单链表的初始化
这里要求初始化一个带头结点的单链表,算法步骤:(1)生成新结点作为头结点,用头指针L指向头结点;(2)将头结点的指针域悬空;
Status InitList_L(LinkList &L){
L = new Lnode; //或C语言的写法:L = (LinkList)malloc(sizeof(Lnode));
L->next = NULL; //如果结构体引用的成员为指针就用->,成员为一般变量就用.
return OK;
}
L = new Lnode
创建了一个新的Lnode节点,并将其地址赋值给L。这样L就指向了链表的头节点。然后,L->next = NULL;
这行代码将链表的头节点的next指针指向NULL,表示链表目前为空,没有其他节点。最后,函数返回OK,表示初始化成功。
(2)判断链表是否为空
int ListEmpty(LinkList L){ //若L为空表,则返回1,否则返回0;
if(L->next) //非空
return 0;
else
return 1;
}
(3)销毁链表
算法思路:从头指针开始,依次释放所有结点
(1)p=L,这里头指针L里面放的是L的头结点的地址,这时p,L都指向头结点;(2)L=L->next,将L往后移一个结点,让它指向下一个结点;(3)delete p(C语言中是free(p)),删除上一个结点;(4)执行(1),这时p也指向下一个结点,依次循环往复;(5)循环条件:当p,L指向最后一个结点时,执行L=L->next,这时候L为NULL,循环结束。
Status DestroyList_L(LinkList &L){ //销毁单链表
Lnode *p; //或LinkList p;
while(L){
p=L; //从头结点开始
L=L->next; //删除之前先把下一个结点找到
delete p; //C语言中用free(p);
}
return OK;
}
(4)清空链表(注意和销毁链表的区别)
链表仍存在,但链表中无元素,成为空链表(头指针和头结点仍然在)。
算法思路:依次释放所有结点,并将头结点指针域设置为空。
Status ClearList(LinkList &L){
Lnode *p,*q; //或LinkList p,q;
p=L->next; //指向首元结点;
while(p){
q=p->next; //q指向p当前结点的下一个结点,先保存地址
delete p; //销毁p
p=q; //p移动到下一个结点
}
L->next=NULL; //把头结点的指针域置空
//注意这里L始终指向头结点
return OK;
}
(5)求单链表的表长
算法思路:从首元结点开始,依次遍历所有结点。
int ListLength_L(LinkList &L){ //返回L中数据元素个数
LinkList p;
p=L->next; //p指向第一个结点
i=0;
while(p){ //遍历单链表,统计结点数
i++;
p=p->next;
}
}
(6)取值-取单链表第i个元素的内容
算法思路:从链表的头指针出发,顺着链域next逐个结点往下搜索,直至搜索到第i个结点为止。因此,链表不是随机存取结构。
步骤:(1)从第1个结点(L->next)顺链扫描,用指针p指向当前扫描到的结点,p初值p = L->next;(2)j做计数器,累计当前扫描过的结点数,j初值为1;(3)当p指向扫描到的下一结点时,计数器j加1;(4)当j==i时,p所指的结点就是要找的第i个结点;
Status GetElem_L(LinkList L,int i,ElemType &e){
//获取链表中的第i个元素的内容,通过变量e返回
p=L->next; //此时p指向首元结点
j=1; //初始化
while(p&&j<i){ //向后扫描,直到p指向第i个元素或者p为空
p=p->next;
++j;
}
if(!p||j>i) return ERROR; //当i取0,-1...或者p=NULL(此时i大于链表长度),报错
e=p->data; //把第i个结点的数据元素存到e中
return OK;
}
注意&&表示逻辑与,||表示逻辑或。
(7)按值查找-根据指定数据获取该数据位置序号
算法步骤:(1)从第一个结点起,依次和e相比较;(2)如果找到一个其值与e相等的数据元素,则返回其在链表中的位置或地址;(3)如果查遍整个链表都没有找到其值和e相等的元素,则返回0或NULL;
//在线性表L中查找值为e的元素的序号
int LocateElem_L(LinkList L,Elemtype e){
p=L->next; //指向首元结点
j=1;
while(p&&p->data!=e){ //当p走到最后或找到e时,退出循环
p=p->next; //移动到下一个结点
j++; //j加1
}
if(p) return j;
else return 0;
}
(8)插入-在第i个结点前插入新结点e
算法步骤:(1)首先找到a_(i-1)的地址p;(2)生成一个数据域为e的新结点s;(3)插入新结点:第一步,新结点的指针域指向结点a_i;第二步,结点a_(i-1)的指针域指向新结点;(注意此两步不能交换)
Status ListInsert_L(LinkList &L,int i,ElemType e){
p=L;
j=0; //初始化
while(p&&j<i-1){
p=p->next;
++j;
}
if(!p||j>i-1)return ERROR; //i取0,负值或超过表长,非法
s=new LNode; //建立新的s结点
s->data=e; //把e放入s的数据域中
s->next=p->next; //将结点s插入L中,此时p指向第i-1个结点,这一步表示s的指针域存入第i个结点
p->next=s; //将第i-1个结点的指针域更改为s(这里s是s的地址)
return OK;
}
(9)删除结点
算法步骤:(1)首先找到a_(i-1)的存储位置p,保存要删除的a_i的值;(2)令p -> next指向a_(i+1);(3)释放结点a的空间。
Status ListDelete_L(LinkList &L,int i,ElemType &e){ //将线性表第i个元素删除
p=L;
j=0; //初始化
while(p->next&&j<i-1){
p=p->next;
++j; //寻找第i个结点,并令p指向其前驱(第i-1个结点)
}
if(!(p->next)||j>i-1)return ERROR; //i取0,负值或超过表长,删除位置不合理
q=p->next; //此时q指向第i个结点
p->next=q->next; //p的指针域指向第i+1个结点
e=q->data; //保存删除节点的数据域
delete q; //释放删除节点的空间
return OK;
}
单链表的查找,插入,删除操作时间复杂度分析:查找的时间复杂度为O(n),插入和删除操作本身的时间复杂度为O(1)。
(10)单链表的建立-头插法
头插法:指元素插入在链表头部,也叫前插法。倒位序,从最后一个元素开始插入。
算法步骤:(1)从一个空表开始,重复读入数据;(2)生成新结点,将读入数据存放到新结点的数据域中;(3)从最后一个结点开始,依次将各结点倒序插入到链表的前端;
void CreateList_H(LinkList &L,int n){
L=new LNode;
L->next=NULL; //先建立一个带头结点的单链表
for(i=n,i>0,i--){
p=New LNode; //生成新结点
cin>>p->data; //输入数据
p->next=L->next; //将新结点与后续已有结点相连
L->next=p; //头结点与新插入的结点相连
}
}
(11)单链表的建立-尾插法
尾插法:指元素插入在链表尾部,也叫尾插法。正位序,从第一个元素开始插入。
算法步骤:(1)从一个空表L开始,将新结点逐个插入到链表的尾部,尾指针r指向链表的头结点;(2)初始时,r同L均指向头结点。每读入一个数据元素则申请一个新结点,将新结点插入到尾结点后,r指向新结点;
void CreateList_R(LinkList &L,int n){
L=new LNode;
L->next=NULL; //先建立一个带头结点的单链表
r=L; //尾指针先指向头结点
for(i=0,i<n,i++){
p=New LNode; //生成新结点
cin>>p->data; //输入数据
p->next=NULL; //新结点是最后一个结点,所以指针域为NULL
r->next=p; //此时r指向上一个结点,它的指针域指向新结点p
r=p; //尾指针+1,指向新的最后一个结点
}
}