我们的目标:
1、了解线性结构的特点 掌握顺序表的定义、查找、插入和删除。
2、掌握链表的定义、创建、查找、插入和删除。
3、能够从时间和空间复杂度的角度比较两种存储结构的不同特点及其适用场合。(持续更新)
目录
前言
今天我们主要学习线性表的重要基本操作以及了解链表的使用方法。
一、线性表的链式表示和实现
1.1 链式存储结构
结点在存储器中的位置是任意的,即逻辑上相邻的数据元素在物理上不一定相邻。
因此,线性表的链式表示又称为非顺序映像或链式映像。
它是如何实现的——通过指针实现。
例1 单链表的存储映像
例2 画出26 个英文字母表的链式存储结构
逻辑结构:( a, b, … ,y, z)
链式存储结构:
可以看到,各结点由两个域组成:
数据域:存储元素数值数据。
指针域:存储直接后继结点的存储位置。
1.2 与链式存储有关的术语
1、结点:数据元素的存储映像。由数据域和指针域两部分组成。
2、链表:n 个结点由指针链组成一个链表。它是线性表的链式存储映像,称为线性表的链式存储结构。
3、单链表、双链表、循环链表:
结点只有一个指针域的链表,称为单链表或线性链表。
有两个指针域的链表,称为双链表。
首尾相接的链表称为循环链表。
循环链表示意图:
4、头指针、头结点和首元结点 :
头指针是指向链表中第一个结点的指针。
首元结点是指链表中存储第一个数据元素a1的结点。
头结点是在链表的首元结点之前附设的一个结点。
数据域内只放空表标志和表长等信息。
上例链表的逻辑结构示意图有以下两种形式:
区别:① 无头结点 ② 有头结点
接下来我们一起讨论一下:
讨论1. 那么应该如何表示空表?
答:有头结点时,当头结点的指针域为空时表示空表。
讨论2. 在链表中设置头结点有什么好处?
⒈ 便于首元结点的处理
首元结点的地址保存在头结点的指针域中,所以在链表的第一个位置上的操作和其它位置一致,无须进行特殊处理。
⒉ 便于空表和非空表的统一处理
无论链表是否为空,头指针都是指向头结点的非空指针,因此空表和非空表的处理也就统一了。
讨论3. 头结点的数据域内装的是什么?
头结点的数据域可以为空,也可存放线性表长度等附加信息,但此结点不能计入链表长度值。
这里是头结点的数据域
1.3 链表(链式存储结构)的特点
1. 结点在存储器中的位置是任意的,即逻辑上相邻的数据元素在物理上不一定相邻。
2. 访问时只能通过头指针进入链表,并通过每个结点的指针域向后扫描其余结点,所以寻找第一个结点和最后一个结点所花费的时间不等。
因此,这种存取元素的方法被称为顺序存取法。
1.4 链表的优缺点
优点
时间:插入、删除等操作不必移动数据,只需修改链接指针,修改效率较高。
空间:数据元素的个数可以自由扩充。
缺点
时间:存取效率不高,必须采用顺序存取,即存取数据元素时,只能按链表的顺序进行访问(顺藤摸瓜)。
空间:存储密度小。
1.5 单链表的定义和实现
非空表
空表
单链表是由表头唯一确定,因此单链表可以用头指针的名字来命名;若头指针名是L,则把链表称为表L。
代码实现:
typedef struct Lnode
{
ElemType data; //数据域
struct LNode *next; //指针域
}LNode,*LinkList; // *LinkList为Lnode类型的指针
注意:区分指针变量和结点变量两个不同的概念。
指针变量p:表示结点地址———> LNode *p
结点变量*p:表示一个结点
若p->data=ai, 则p->next->data=ai+1
1.6 几个简单基本操作的算法实现
回顾一下:
初始化(构造一个空表 )
Status InitList_L(LinkList &L){
L=new LNode;
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;
}
下面进行解题——求表长
方法:“数”结点
指针p依次指向各个结点,从第一个元素开始“数”,一直“数”到最后一个结点。
代码实现:
p=L->next;
i=0;
while(p){i++;p=p->next;}
最后整体代码实现:
int ListLength_L(LinkList L){
//返回L中数据元素个数
LinkList p;
p=L->next; //p指向第一个结点
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;
}
按照上一篇的文章:https://mp.csdn.net/mp_blog/creation/editor/129893007
接着讲到了线性表重要基本操作——取值
二、线性表重要基本操作
2.5 取值
根据位置i获取相应位置数据元素的内容。
那么顺序表里如何找到第i个元素?
答:链表的查找要从链表的头指针出发,顺着链域next逐个结点往下搜索,直至搜索到第i个结点为止。因此,链表不是随机存取结构。
举个例子,分别取出表中i=3和i=15的元素。
算法步骤
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<i){ //向后扫描,直到p指向第i个元素或p为空
p=p->next; ++j;
}
if(!p || j>i)return ERROR; //第i个元素不存在
e=p->data; //取第i个元素
return OK;
}//GetElem_L
2.6 查找
根据指定数据获取数据所在的位置。
算法步骤
1. 从第一个结点起,依次和e相比较;
2. 如果找到一个其值与e相等的数据元素,则返回其在链表中的“位置”或地址;
3. 如果查遍整个链表都没有找到其值和e相等的元素,则返回0 或“NULL”。
代码实现:
//在线性表L中查找值为e的数据元素
LNode *LocateELem_L (LinkList L,Elemtype e) {
//返回L中值为e的数据元素的地址,查找失败返回NULL
p=L->next;
while(p &&p->data!=e)
p=p->next;
return p;
}
另一种方法:
//在线性表L中查找值为e的数据元素
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;
}
2.7 插入
将值为x的新结点插入到表的第i个结点的位置上,即插入到ai-1与ai之间。
插入过程:s->next=p->next; p->next=s;
注意:步骤一和步骤二不能互换!如果先走步骤二会导致ai-1与ai之间过不去。就好比s->next=p->next;是链接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&&j<i−1){p=p->next;++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
2.8 删除
将表的第i个结点删去。
算法步骤:
1. 找到ai-1存储位置p ;
2. 临时保存结点ai的地址在q中,以备释放;
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<i-1){ //寻找第i个结点,并令p指向其前驱
p=p->next; ++j;
}
if(!(p->next)||j>i-1) return ERROR; //删除位置不合理
q=p->next; //临时保存被删结点的地址以备释放
p->next=q->next; //改变删除结点前驱结点的指针域
e=q->data; //保存删除结点的数据域
delete q; //释放删除结点的空间
return OK;
}//ListDelete_L
三、链表
3.1 链表的运算时间效率分析
1. 查找: 因线性链表只能顺序存取,即在查找时要从头指针找起,查找的时间复杂度为 O(n)。
2. 插入和删除: 因线性链表不需要移动元素,只要修改指针,一般情况下时间复杂度为 O(1)。
注意:如果要在单链表中进行前插或删除操作,由于要从头查找前驱结点,所耗时间复杂度为 O(n) 。
3.2单链表的建立(前插法)
从一个空表开始,重复读入数据:
生成新结点,将读入数据存放到新结点的数据域中,再将该新结点插入到链表的前端。
代码实现:
void CreateList_F(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; //插入到表头
}
}//CreateList_F
3.3 单链表的建立(尾插法)
从一个空表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<n;++i){
p=new LNode; //生成新结点
cin>>p->data; //输入元素值
p->next=NULL; r->next=p; //插入到表尾
r=p; //r指向新的尾结点
}
}//CreateList_L
四、总结
以上就是今天要讲的内容,链表的方法还没有介绍完,我会持续更新。这章我们主要把线性表的重要基本操作讲解完了,希望大家多多理解,有什么问题还要向大佬们多多指教!我会继续努力的!