1.链式存储结构定义:
结点在存储器的位置上是任意的,即逻辑相邻的数据元素在物理上不一定相邻。
2.单链表、双链表、循环链表:
单链表:结点只有一个指针域链表
双链表:结点有两个指针域的链表
循环链表:首尾相连的链表
3.头指针、头结点、首元结点:
头指针:指向链表中第一个结点的指针
头结点:在链表的首元结点之前附加的一个结点
首元结点:是指链表中存储第一个元素的结点
4.引入头结点的好处:
1.由于第一个结点的位置被存放在头结点的指针域中,所以在链表的第一个位置上的操作和其他位置上的操作一致,无需进行特殊处理。
2.无论链表是否为空,其头指针都指向头结点的非空指针,因此空表和非空表的处理也得到了统一。
4. 链表的特点
1.结点在存储器中的位置是任意的,逻辑上相邻的数据元素物理上不一定相邻
2.访问时是通过头指针进入链表,并通过每个结点的指针域依次向后 扫描其余结点,所以寻找第一个结点和最后一个结点所花时间并不相同。
附:顺序表->随机存取 链表->顺序存取
存储学生、学号、姓名、成绩的单链表结点类型定义如下:
typedef struct student{
char num[8]; //数据域
char name[8]; //数据域
int score; //数据域
struct student*next; //指针域
}LNode,*LinkList
5.单链表的基本操作:
5.1单链表的初始化
Status InitLis_L(LinkList &L)
{
L=(LinkList)malloc(sizeof(LNode));//使用malloc动态分配一个新节点
L->next=NULL;
return OK;
}
5.2判断单链表是否为空
Status ListEmpty(LinkList &L)
{
if(L->next) //非空
return 0;
else
return 1;
}
5.3单链表的销毁
//单链表的销毁:链表销毁后是不存在
Status DestoryList(LinkList &L)
{
Lnode *p;
while(L)
{
p=L;
L=L->next;
free(p);
}
}
5.4单链表的清空
清空后的单链表仍然存在,但是链表中已没有元素,成为空链表(头指针和头结点仍然还在)
/*
依次释放所有的结点,并将头结点的指针域置空
*/
Status ClearList(LinkList &L)
{
LNode *p;
LNode *q;
p=L->next;//此时p的位置在首元结点
while(p)
{
q=p->next;
free(p);
p=q;
}
L->next=NULL;//将L链表中的头结点置空
return OK;
}
5.5单链表的表长
//算法思想:从首元结点开始,依次计数所有的结点
Status ListLength_L(LinkList L)
{
LinkList p;
p=L->next;
int i=0;
while(p)
{
i++;
p=p->next;
}
return i;
}
5.6单链表的取值
取单链表中第i个元素的值
/*
算法思想:
1.从第一个结点(L->next)出发顺链扫描,用指针p指向当前扫描到的结点,p的初始值是p=L->next;
2.j做计数器,累计当前扫描过的结点数,j的初值为1;
3.当p指向扫描到下一结点时,计数器j就加1;
4.当j==i时,p所指的结点就是要找的第i个结点;
*/
Status GetElem(LinkList L,int i,ElemType &e)
{
LNode *p;
p=L->next; //初始化p,此时p指向首元结点
int j=1; //初始化计数器为1
while(p&&j<i) //顺链域向后扫描,直到p为空或者p指向第i个元素
{
p=p->next;
j++;
}
if(j>i||!p) return ERROR; //i值不合法,i>n或者i<=0
e=p->data; //取第i个结点的数据域
return OK;
}
5.7单链表的按值查找
根据指定数据获取该数据所在的位置(地址)
/*
1.从第一个结点开始和e相比较
2.如果找到一个其值与e相等的数据元素,则返回其在链表中的位置或者地址
3.如果查遍整个链表都没有找到其值和e相等的元素,则返回0或者“NULL”
*/
LNode *LocateElem_L(LinkList L,ElemType e)
{
LNode *p;
p=L->next;
while(p&&p->data!=e)
{
p=p->next;
}
return p;
}
5.8单链表的插入结点
在第i个结点前插入值为e的新节点
/*
算法步骤:
1.首先找到i-1个位置的结点,并记为p;
2.生成一个新的数据域为e的新节点s;
3.插入新结点:s->next=p->next p->next=s;
*/
Status ListInsert_L(LinkList &L,int i,ElemType e)
{
LNode *p=L->next;
LNode *s;
int j=1;
while(p&&j<i-1) //查找第i-1个结点,p指向该节点
{
p=p->next;
j++;
}
if(!p||j>i-1) return ERROR;
s=(LinkList)malloc(sizeof(LNode));//生成新节点s;
s->data=e;
s->next=p->next;//将结点*s的指针域指向第i个结点
p->next=s; //将结点*p的指针域指向结点*s
}
5.9单链表的删除第i个结点
/*
算法思想:
1.首先要找到删除结点的前驱结点,将其前驱结点的位置设为P,保存要删除结点的值
2.让p->next指向第i+1的结点
*/
Status ListDelete_L(LinkList &L,int i,ElemType &e)
{
LNode *p=L->next;
int j=1;
while(p&&j<i-1) //查找第i-1个元素
{
p=p->next;
j++;
}
if(!p||j>i-1) return ERROR; //查找位置不合法
LNode *q=p->next;//临时保存被删结点的地址
p->next=p->next->next;//改变删除结点前驱结点的指针域
e=q->data;
free(q); //释放删除结点的空间
return OK;
}
6. 单链表的时间效率分析
当学完单链表的插入和删除操作后,和顺序表做了一个简单的比较,发现顺序表是随机存取,但是它插入和删除元素的时候需要大量的移动元素,所以它的时间复杂度为O(n),但是链表虽然插入和删除操作只需修改前后指针即可,不需要移动,但需要按照顺序先查找元素所在的前一个位置结点,然后再执行删除和插入的操作,其算法时间复杂度也是O(n),那为何说链表的效率高呢?
答:
①因为O(n)的内涵不同,一个是读的效率O(n),一个是写的效率O(n);数组擅长读取而链表擅长写入。在写入场景中,数组链表的复杂度是定位写入复杂度之和,都是O(n),但写入比定位的O(n)慢很多,所以两个表面看起来一样的O(n)的实际时间还是差很多。
② 当我们要知道插入和删除的位置时,链式存储的优越性就表现出来了,假如我们要在a10与a11之间插入10个元素,那么顺序存储每插入一个元素后面的元素就要移动一次位置,每次都是O(n)。而链式存储,只需要第一次时找到要插入的那个位置,后面的就只是赋值移动指针而已,时间复杂度为O(1)。
因此,可以得出一个结论:对于插入或者删除操作越频繁的操作,单链表的效率优势就越是明显。
7. 单链表的建立
7.1.单链表的建立——头插法
元素插在链表的头部,也叫前插法
/*
算法思想:
1.从一个空表开始,重复的读入数据
2.生成新的结点,将读入数据存放到新的结点的数据域中
3.从最后一个结点开始,依次将各结点插入到链表的前端
*/
void CreateList(LinkList &L,int n)
{
L=(LinkList)malloc(sizeof(LNode));
L->next=NULL;
for(int i=0;i<n;i++)
{
LNode *p=(LinkList)malloc(sizeof(LNode));
scanf("%d",p->data); //输入元素值赋给新结点*p的数据域
p->next=L->next; //将新结点*p插入到头结点之后
L->next=p;
}
附:采用头插法建立单链表,读入数据顺序和生成的链表中元素的顺序是相反的。每个结点插入时间是O(1),则总的时间复杂度是O(n)。
7.1.单链表的建立——尾插法
元素插在链表的尾部,也叫后插法,同前插法一样,每次申请一个新结点,读入相应的数据元素值。不同的是,为了使新结点能够插入到表尾,需要增加一个尾指针r来指向链表的尾结点。
/*
算法步骤:
1.创建只有头结点的空链表;
2.尾指针r初始化,指向头结点
3.根据创建链表包括元素的个数n,循环n次执行以下操作:
①生成一个新结点*P,
②输入元素值赋给新结点*p的数据域
③新结点*p插入到尾结点*r之后
④尾指针r指向新的尾结点*p
*/
void CreateList_R(LinkList &L,int n)
{
L=(LinkList)malloc(sizeof(LNode));
L->next=NULL;
LNode *r=L;
for(int i=0;i<n;i++)
{
LNode *p=(LinkList)malloc(sizeof(LNode));
scanf("%d",p->data);
p->next=NULL;
r->next=p;//将新结点*p插入到尾结点*r之后
r=p; //r指向新的尾结点*p
}
}
8. 循环链表
循环链表和单链表的区别在于,表中最后一个结点的指针不是NULL,而是改为指向头结点,从而整个链表形成一个环。循环链表可以从表中任意一个结点开始遍历整个链表,使得操作效率更高。
8.1.如何将带尾指针的循环链表合并
分析操作:
①p存表头结点
②tb表头连接到ta表尾
③释放tb表头结点
④修改指针
void Connect(LinkList Ta,LinkList Tb)
{
LNode *p=Ta->next;//创建一个新的结点存Ta表的头结点
Ta->next=Tb->next->next;//让Tb的首元结点接在Ta表的尾结点的后面
free(Tb->next);//将Tb的头结点释放掉
Tb->next=p;//让Tb的尾结点和Ta的头结点相连
return Tb;
}
时间复杂度是O(1)
9. 双向链表
9. 1引入双向链表的原因
单链表查找某结点的后继结点比较容易,时间复杂度是O(1),但是查找某结点的前驱结点要从表头开始出发,时间复杂度是O(n)。而双向链表可以克服单链表这一缺点。
双向链表:在单链表的每一个结点里面再增加一个指向其直接前驱的指针域prior,这样链表就形成了有两个方向不同的链,故称为双向链。
9. 2循环双向链表
双向链表的查询操作和单链表基本一致,但是删除和插入有了很大的变化,不仅要修改后继指针的位置,还需要修改前驱指针的位置。
头结点的前驱指针指向链表的最后一个结点。
最后一个结点的后继指针指向头结点。
9. 3双向链表的插入算法
①在带头结点的双向链表L中的第i个元素之前插入元素e
Status ListInsert_DuL(DuLinkList &L,int i,ElemType &e)
{
DuLNode *p=(DuLinkList)malloc(sizeof(DuLNode));
if(!(p=GetElem_DuL(L,i))) return ERROR;
DuLNode *s=(DuLinkList)malloc(sizeof(DuLNode));
s->data=e;
s->prior=p->prior;
p->prior->next=s;
p->prior=s;
s->next=p;
return OK;
}
②在带头结点的双向链表L中的第i个元素之后插入元素e
s->next=p->next;
p->next->prior=s;
s->prior=p;
p->next=s;
以上代码顺序并不是唯一,但是也不是任意的。
9. 4双向链表的删除操作
删除带头结点的双向链表L中的第i个元素、
//双向链表的删除
Status ListDelete_DUL(DuLinkList &L,int i,ElemType &e)
{
DuLNode *p=(DuLinkList)malloc(sizeof(DuLNode));
if(!(p=GetElem_DuL(L,i))) return ERROR;
e=p->data;
p->prior->next=p->next;//修改被删结点的前驱节点的后继指针
p->next->prior=p->prior;//修改被删结点的后继节点的前驱指针
free(p);
return OK;
}