链式存储结构:
结点在存储器中的位置是任意的,即逻辑上相邻的数据元素在物理上不一定相邻,线性表的链式表示又称为非顺序映像或链式映像。
数组与链表
数组是需要一块连续的内存空间来存储数据,对内存的要求非常高
链表并不需要一块连续的内存空间,只要内存空间充足,即使内存空间存在碎片,只要碎片
的大小足够存储一个链表节点的数据,该碎片的空间都有可能被分配,链表通过指针将一组零散的空间串联起来使用。
把串联在链表上的每一个内存块称为链表的结点
链表各结点由两个域组成:
数据域:存储元素数值数据
指针域:存储直接后继结点的存储位置
单链表、双链表、循环链表、静态链表:
结点只有一个指针域的链表,称为单链表或线性链表
有两个指针域的链表,称为双链表,通常头指针只设置一个,除非实际情况需要
首尾相接的链表称为循环链表
用数组描述的链表称为静态链表
头指针、头结点和首元结点
头指针是指向链表中第一个结点的指针,是链表的名字
首元结点是指链表中存储第一个数据元素a1的结点
头结点是在链表的首元结点之前附设的一个结点
头节点和头指针的区分:
不管带不带头节点,头指针都始终指向链表的第一个结点,而头结点是带头结点的链表中的第一个结点,结点内通常不存储信息。
引入头节点后,可以带来两个优点:
1.由于第一个数据结点的位置被存放在头节点的指针域中,因此在链表的第一个位置上操作和在表的其他位置上的操作一致,无须进行特殊处理。
2.无论链表是否为空,其头指针都是指向头结点的非空指针(空表中头结点的指针域为空),因此空表和非空表的处理也就得到了统一。
如何表示空表?
有头结点时,当头结点的指针域为空时表示空表 无头结点时,头指针为空时表示空表。
为什么在链表中设置头结点?
1、防止空链表无法操作与表示。
2、在第一个元素结点前插入结点(或删除第一个结点),使其操作与对其它结点一致。
3、头结点的存在使得空链表与非空链表的处理操作一致。
4、对单链表的多数操作应明确相应结点以及该结点的前驱。
链表(链式存储结构)的特点
(1)结点在存储器中的位置是任意的,即逻辑上相邻的数据元素在物理上不一定相邻
(2)访问时只能通过头指针进入链表,并通过每个结点的指针域向后扫描其余结点,所以寻找第一个结点和最后一个结点所花费的时间不等
优点
• 数据元素的个数可以自由扩充
• 插入、删除等操作不必移动数据,只需修改链接指针,修改效率较高
缺点
• 存储密度小
• 存取效率不高,必须采用顺序存取,即存取数据元素时,只能按链表的顺序进行访问
单链表是由表头唯一确定,因此单链表可以用头指针的名字来命名
若头指针名是L,则把链表称为表L
存储结构定义
typedef struct LNode{ //定义单链表结点类型
ElemType data; //数据域
struct LNode *next; //指针域
}LNode,*LinkList; // *LinkList为Lnode类型的指针
注意区分指针变量和结点变量两个不同的概念
指针变量p:表示结点地址
结点变量*p:表示一个结点
LinkList与LNode* ,两者本质上是等价的。
通常用LinkList定义单链表,强调的是某个单链表的头指针;
用LNode*定义指向单链表中任意结点的指针变量。
若定义LinkList L,则L为单链表的头指针,即表示单链表L。
若定义LNode *p, 则p为指向单链表中某个结点的指针,用 *p代表该结点。
初始化(构造一个空表 )
(1)生成新结点作头结点,用头指针L指向头结点。
(2)头结点的指针域置空。
Status InitList_L(LinkList &L)
{
L=new LNode; // L = (LNode * ) malloc (sizeof(LNode));
if (L == NULL) //内存不足分配失败
return false;
L->next=NULL;
return OK;
}
销毁
从头指针开始,依次释放所有结点
Status DestroyList_L(LinkList &L)
{
LinkList p;
while(L)
{
p=L;
L=L->next;
delete p;
}
return OK;
}
清空与销毁的区别
清空:链表仍存在,但链表中无元素,成为空链表(头指针和头结点仍然在)。
销毁:依次释放所有结点,并将头结点指针域设置为空
Status ClearList(LinkList & L)
{
// 将L重置为空表
LinkList p,q;
p=L->next; //p指向第一个结点
while(p){
q=p->next;
delete p;
p=q;
}
L->next=NULL; //头结点指针域为空
return OK;
}
求表长
int ListLength_L(LinkList L){
//返回L中数据元素个数
LinkList p;
p=L->next; //p指向第一个结点 L本身为头节点指针,L->next 指向首元点。
i=0;
while(p){ //遍历单链表,统计结点数
i++;
p=p->next; }
return i;
}
判断表是否为空
int ListEmpty(LinkList L){
//若L为空表,则返回1,否则返回0
if(L->next) //非空
return 0;
else
return 1;
}
获取元素
根据位置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个结点。
//获取线性表L中的某个位置数据元素的内容
Status GetElem_L(LinkList L,int i,ElemType &e){
p=L->next;
j=1; //初始化
while(p&&j p=p->next;
++j;
}
if(!p || j>i)
return ERROR; //第i个元素不存在
e=p->data; //取第i个元素
return OK;
}//GetElem_L
按序号查找操作的时间复杂度为O(n)
查找元素
1、从第一个结点起,依次和e相比较。
2、如果找到一个其值与e相等的数据元素,则返回其在链表中的“位置”或地址;
3、如果查遍整个链表都没有找到其值和e相等的元素,则返回0或“NULL”
//在线性表L中查找值为e的数据元素 1、查找数据返地址 2、查找数据返序号
LNode *LocateELem_L (LinkList L,Elemtype e) {
//返回L中值为e的数据元素的地址,查找失败返回NULL
p=L->next;
while(p &&p->data!=e)
p=p->next;
return p;
}
int LocateELem_L (LinkList L,Elemtype e) {
//返回L中值为e的数据元素的位置序号,查找失败返回0
p=L->next; j=1;
while(p &&p->data!=e)
{p=p->next; j++;}
if(p) return j;
else return 0;
}
插入
将值为x的新结点插入到表的第i个结点的位置上,即插入到ai-1与ai之间
(1)找到ai-1存储位置p
(2)生成一个新结点*s
(3)将新结点*s的数据域置为x
(4)新结点*s的指针域指向结点ai
(5)令结点*p的指针域指向新结点*s
//在L中第i个元素之前插入数据元素e
Status ListInsert_L(LinkList &L,int i,ElemType e){
p=L;j=0;
while(p&&jnext;++j;} //寻找第i−1个结点
if(!p||j>i−1)return ERROR; //i大于表长 + 1或者小于1
s=new LNode; //生成新结点s
s->data=e; //将结点s的数据域置e
s->next=p->next; //将结点s插入L中
p->next=s;
return OK;
}//ListInsert_L
删除
将表的第i个结点删去
步骤:
(1)找到ai-1存储位置p
(2)临时保存结点ai的地址在r中,以备释放
(3)令p->next指向ai的直接后继结点
(4)将ai的值保留在e中
(5)释放ai的空间
//将线性表L中第i个数据元素删除
Status ListDelete_L(LinkList &L,int i,ElemType &e){
p=L;j=0;
while(p->next &&j
}
if(!(p->next)||j>i-1) return ERROR; //删除位置不合理
r=p->next; //临时保存被删结点的地址以备释放
p->next=r->next; //改变删除结点前驱结点的指针域
e=r->data; //保存删除结点的数据域
delete r; //释放删除结点的空间
return OK;
}//ListDelete_L
链表运算时间效率分析
查找: 因线性链表只能顺序存取,即在查找时要从头指针找起,查找的时间复杂度为 O(n)。
插入和删除: 因线性链表不需要移动元素,只要修改指针,一般情况下时间复杂度为 O(1)。
如果要在单链表中进行前插或删除操作,由于要从头查找前驱结点,所耗时间复杂度为 O(n)。
头插法
从一个空表开始,重复读入数据:
1、生成新结点
2、将读入数据存放到新结点的数据域中
3、将该新结点插入到链表的前端
void CreateList_F(LinkList &L,int n){
L=new LNode;
L->next=NULL; //先建立一个带头结点的单链表
for(i=n;i>0;--i){
p=new LNode; //生成新结点p=(LNode*)malloc(sizeof(LNode));
cin>>p->data; //输入元素值 scanf(&p-> data);
p->next=L->next;L->next=p; //插入到表头
}
}//CreateList_F
采用头插法建立单链表时,读入数据的顺序与生成的链表中的元素的顺序是相反的。每个结点插入的时间为O(1),设单链表长为n,则总时间复杂度为O(n)。
尾插法
从一个空表L开始,将新结点逐个插入到链表的尾部,尾指针r指向链表的尾结点。
初始时,r同L均指向头结点。每读入一个数据元素则申请一个新结点,将新结点插入到尾结点后,r指向新结点。
void CreateList_L(LinkList &L,int n){
//正位序输入n个元素的值,建立带表头结点的单链表L
L=new LNode;
L->next=NULL;
r=L; //尾指针r指向头结点
for(i=0;i
cin>>p->data; //输入元素值
p->next=NULL; r->next=p; //插入到表尾
r=p; //r指向新的尾结点
}
}//CreateList_L
采用尾插法,生成的链表中结点的次序和输入数据的顺序不一致。
附设了一个指向表尾结点的指针,时间复杂度和头插法相同。
循环链表是另一种形式的链式存储结构。它的特点是表中最后一个结点的指针域指向头结点,整个链表形成一个环。
开始结点:rear->next->next
终端结点:rear
用尾指针rear表示的单循环链表。对开始结点a1和终端结点an查找时间都是O(1)。而表的操作常常是在表的首尾位置上进行,因此,实用中多采用尾指针表示单循环链表。
尾指针判空:rear==rear->next;
头指针判空:L->next=L
循环单链表的插入、删除算法与单链表的几乎一样,所不同的是若操作是在表尾进行,则执行的操作不同,以让单链表继续保持循环的性质。当然,正是因为循环单链表是一个“环”,因此在任何一个位置上插入和删除操作都是等价的,无须判断是否是表尾。
循环链表的合并
LinkList Connect(LinkList &Ta,LinkList &Tb)
{//假设Ta、Tb都是非空的单循环链表
p=Ta->next; //p存表头结点
Ta->next=Tb->next->next; //Tb表头连接Ta表尾
delete Tb->next; //释放Tb表头结点
Tb->next=p; //修改指针
return Tb;
}
双向链表也叫双链表,是链表的一种,它的每个数据结点中都有两个指针,分别指向直接后继和直接前驱。所以,从双向链表中的任意一个结点开始,都可以很方便地访问它的前驱结点和后继结点。一般我们都构造双向循环链表。
typedef struct DuLNode{ //定义双链表结点类型
ElemType data; //数据域
struct DuLNode *prior; //前驱指针
struct DuLNode *next; //后继指针
}DuLNode, *DuLinkList
空双向循环链表:L->next=L
双向循环链表:p->next->prior = p->prior->next = p
带头双向循环链表
带头:存在一个哨兵位的头节点,该节点是个无效节点,不存储任何有效信息,但使用它可
以方便我们头尾插和头尾删时不用判断头节点指向NULL的情况。
双向:每个结构体有两个指针,分别指向前一个结构体和后一个结构体。
循环:最后一个结构体的指针不再指向NULL,而是指向第一个结构体(单向)第一个结构体的前指针指向最后一个结构体,最后一个结构体的后指针指向第一个结构体(双向)
双向链表的插入
核心代码:
1. s->prior=p->prior;
2. p->prior->next=s;
3. s->next=p;
4. p->prior=s;
双向链表的插入:
Status ListInsert_DuL(DuLinkList &L,int i,ElemType e){
if(!(p=GetElemP_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;
}
双向链表的删除
核心代码:
1. p->prior->next=p->next;
2. p->next->prior=p->prior;
Status ListDelete_DuL(DuLinkList &L,int i,ElemType &e){
if(!(p=GetElemP_DuL(L,i))) return ERROR;
e=p->data;
p->prior->next=p->next;
p->next->prior=p->prior;
delete p;
return OK;
}
静态链表,也是线性存储结构的一种,它兼顾了顺序表和链表的优点于一身,既能快速访问元素,又能快速增加和删除元素。可以看做是顺序表和链表的升级版。
使用静态链表存储数据,数据全部存储在数组中(和顺序表一样),但存储位置是随机的,数据之间“一对一”的逻辑关系通过一个整形变量维持(称为“游标”,和指针功能类似),和链表类似。
静态链表借助数组来描述线性表的链式存储结构,结点也有数据域data和指针域next,这里的指针是结点的相对地址(数组下标),又称游标。和顺序表一样,静态链表也要预先分配一块连续的内存空间。
通常,静态链表会将第一个数据元素放到数组下标为1的位置(a[1])中。
静态链表的定义
数据域:用于存储数据元素的值;
游标:其实就是数组下标,表示直接后继元素所在数组中的位置;
因此,静态链表中节点的构成用 C 语言实现为:
typedef struct {
int data; //数据域
int cur; //游标
}component;
静态链表以next==-1作为其结束的标志。静态链表的插入、删除操作与动态链表的相同,只需要修改指针,而不需要移动元素。