3.2 线性表的定义
线性表:零个或多个数据元素的有限序列
用数学语言来进行定义:若将线性表记为(a1,...,ai-1,ai,ai+1,...,an),则表中ai-1领先于ai,ai领先于ai+1,称ai-1是ai的直接前驱元素,ai+1是ai的直接后继元素。当i=1,2,...,n-1时,ai有且仅有一个直接后继,当i=2,3,...,n时,ai有且仅有一个直接前驱。
所以,线性表元素的个数n(n≥0)定义为线性表的长度,当n=0时,称为空表。
在较复杂的线性表中,一个数据元素可以由若干个数据项组成
3.3 线性表的抽象数据类型
线性表的抽象数据类型定义:
ADT 线性表(List)
Data
线性表的数据对象集合为{a1,a2,...,an},每个元素的类型均为DataType。其中,除第一个元素a1外,每一个元素有且仅有一个直接前驱元素,除了最后一个元素an外,每一个元素有且仅有一个直接后继元素。数据元素之间的关系是一对一的关系。
Operation
InitList (*L); // 初始化操作,建立一个空的线性表
ListEmpty (L); // 若线性表为空,返回true,否则返回false
ClearList (*L); // 将线性表清空。
GetElem (L,i,*e); // 将线性表L中的第i个位置元素值返回给e
LocateElem (L,e); // 在线性表L中查找与给定值e相等的元素,如果查找成功,返回该元素在表中序号表示成功;否则,返回0表示失败。
ListInsert (*L,i,e); // 在线性表L中的第i个位置插入新元素e
ListDelete (*L,i,e); // 删除线性表L中第i个位置元素,并用e返回其值
ListLength (*L,i,e); // 返回线性表L的元素个数
endADT
3.4 线性表的顺序存储结构
3.4.1 顺序存储定义
顺序存储定义:线性表的顺序存储结构,指的是用一段地址连续的存储单元依次存储线性表的数据元素。
3.4.2 顺序存储方式
可以用C语言中的一维数组来实现顺序存储结构。
线性表顺序存储的结构代码:
#define MAXSIZE 20 // 存储空间初始分配量
typedef int ElemType; // ElemType类型根据实际情况而定,这里假设为int类型
typedef struct
{
ElemType data[MAXSIZE]; // 数组存储数据元素,最大值为MAXSIZE
int length; // 线性表当前长度
}SqList
描述顺序存储结构需要以下三个属性:
1. 存储空间的起始位置:数组data,它的存储位置就是存储空间的存储位置。
2. 线性表的最大存储容量:数组长度MaxSize。
3. 线性表的当前长度:length。
3.4.3 数据长度与线性表长度区别
在任何时刻,线性表长度小于数组长度
3.4.4 地址计算方法
地址定义:存储器中的每个存储单元都有自己的编号,这个编号称为地址。
地址计算公式:
LOC(ai+1) = LOC(ai)+c // LOC表示获得存储位置的函数
LOC(ai) = LOC(ai)+(i-1)*c
3.5 顺序存储结构的插入与删除
3.5.2 插入操作
插入算法的思路:
1. 如果插入位置不合理,抛出异常
2. 如果线性表长度大于等于数组长度,则抛出异常或动态增加容量
3. 从最后一个元素开始向前遍历到第i个位置,分别将它们都向后移动一个位置
4. 将要插入元素填入位置i处
5. 表长+1
实现代码如下:
// 初始条件:顺序线性表L已存在,1≤i≤ListLength(L)
// 操作结果:在L中第i个位置之前插入新的数据元素e,L的长度加1
Status ListInsert(SqList *L,int i,ElemType e)
{
int k;
if(L->length == MAXSIZE) // 顺序线性表已经满
return ERROR;
if(i < 1 || i > L -> length+1) // 当i不在范围内时
return ERROR;
if(i < = L -> length) // 若插入数据不在表尾
{
for(k = L -> length-1; k>=i-1; k--) // 将要插入位置后数据元素向后移动一位
L->data[k+1] = L->data[k];
}
L->data[i-1] = e; // 将新元素插入
L->length++;
return ok;
}
3.5.3 删除操作
删除算法的思路:
1. 如果删除位置不合理,抛出异常
2. 取出删除元素
3. 从删除元素位置开始遍历到最后一个元素位置,分别将它们都向前移动一个位置
4. 表长减一
实现代码如下:
// 初始条件:顺序线性表L已存在,1≤i≤ListLength(L)
// 操作结果:删除L的第i个数据元素,并用e返回其值,L的长度减1
Status ListDelete(SqList *L,int i,ElemType e)
{
int k;
if(L->length == 0) // 线性表为空
return ERROR;
if(i < 1 || i > L -> length) // 删除位置不正确
return ERROR;
*e = L->data[i-1];
if(i < L -> length) // 如果删除不是最后位置
{
for(k = i; k< L -> length; k++) // 将删除位置后继元素前移
L->data[k-1] = L->data[k];
}
L->length--;
return ok;
}
3.5.4 线性表顺序存储结构的优缺点
优点:
1. 无需为表示表中元素之间的逻辑关系而增加额外的存储空间
2. 可以快速地存取表中任一位置的元素
缺点:
1. 插入和删除操作需要移动大量元素
2. 当线性表长度变化较大时,难以确定存储空间的容量
3. 造成存储空间的“碎片”
3.6 线性表的链式存储结构
3.6.2 线性表链式存储结构定义
为了表示每个数据元素ai与其直接后继数据元素ai+1之间的逻辑关系,对数据元素ai来说,除了存储其本身的信息之外,还需存储一个指示其直接后继的信息(即直接后继的存储位置)。
数据域:存储数据元素信息的域叫做数据域。
指针域:存储直接后继元素位置的域叫做指针域。
指针或链:指针域中存储的信息叫做指针或链。
结点:数据域和指针域组成数据元素ai的存储映像叫做结点
线性表的链式存储结构:n个结点(ai的存储映像)链接成一个链表,即为线性表(a1,a2,...,an)的链式存储结构。
单链表:此链表的每个结点中只包括一个指针域,所以叫做单链表。
头指针:链表中第一个结点的存储位置叫做头指针
线性链表的最后一个结点指针为空(通常用NULL或“^”表示)。
头结点:单链表的第一个结点前附设一个结点,称为头结点。
3.6.3 头指针与头结点的异同
头指针:
1. 头指针是指链表指向第一个结点的指针,若链表有头结点,则是指向头节点的指针。
2. 头指针具有标识作用,所以常用头指针冠以链表的名字。
3. 无论链表是否为空,头指针均不为空。头指针是链表的必要元素
头结点:
1. 头节点是为了操作的统一和方便而设立的,放在第一元素的结点之前,其数据域一般无意义(也可存放链表的长度)。
2. 有了头节点,对在第一元素结点前插入结点和删除第一结点,其操作与其他结点的操作就统一了。
3. 头节点不一定是链表必须要素。
3.6.4 线性表链式存储结构代码描述
若线性表为空表,则头结点的指针域为“空”。
单链表中,我们在C语言中可用结构指针来描述:
// 线性表的单链表存储结构
typedef struct Node
{
ElemType data;
struct Nodec *next;
} Node;
typedef struct Node *LinkList; // 定义LinkList
从以上结构定义中,我们知道:
结点由存放数据元素的数据域和存放后继结点地址的指针域组成。
3.7 单链表的读取
获得链表第i个数据的算法思路:
1. 声明一个指针P指向链表第一个结点,初始化j从1开始;
2. 当j小于i时,就遍历链表,让p的指针向后移动,不断指向下一结点,j累加1;
3. 若到链表末尾p为空,则说明第i个结点不存在;
4. 否则查找成功,返回结点p的数据。
实现代码算法如下:
// 初始条件:顺序线性表L已存在,1≤i≤ListLength(L)
// 操作结果:用e返回L中第i个数据元素的值
Status GetElem(LinkList L,int i,ElemType *e)
{
int j;
LinkList p; // 声明一指针p*
p = L->next; // 让p指向链表L的第一个结点
j = 1; // j为计数器
while(p && j<i) // p不为空且计数器j还没有等于i时,循环继续
{
p = p->next; // 让p指向下一个结点
++j;
}
if(!p || j>i)
return ERROR; // 第i个节点不存在
*e = p->data; // 取第i个结点的数据
return ok;
}
由于单链表的结构中没有定义表长,所以不能实现知道要循环多少次,因此不方便用for来控制循环。其主要核心思想是“工作指针后移” 。
3.8 单链表的插入与删除
3.8.1 单链表的插入
单链表第i个数据插入结点的算法思路:
1. 声明一指针p指向链表头结点,初始化j从1开始;
2. 当j<i时,就遍历链表,让p的指针向后移动,不断指向下一结点,j累加1;
3. 若到链表末尾p为空,则说明第i个结点不存在;
4. 否则查找成功,在系统中生成一个空结点s;
5. 将数据元素e赋值给s->data;
6. 单链表的插入标准语句s->next=p->next; p->next=s;
7. 返回成功
实现代码算法如下:
// 初始条件:顺序线性表L已存在,1≤i≤ListLength(L)
// 操作结果:在L中第i个结点位置之前插入新的数据元素e,L的长度加1
Status ListInsert(LinkList *L,int i,ElemType e)
{
int j;
LinkList p,s;
p = *L;
j = 1;
while(p && j<i) // 寻找第i-1个结点
{
p = p->next;
++j;
}
if(!p || j>i)
return ERROR; // 第i个结点不存在
s = (LinkList)malloc(sizeof(Node)); // 生成新节点(C标准函数)
s->data = e;
s->next = p->next; // 将p的后继结点赋值给s的后继
p->next = s; // 将s赋值给p的后继
return ok;
}
C语言malloc函数的作用:生成一个新的结点,类型和Node是一样的,实质是在内存中找了一块空地,用来存放数据e的s结点。
3.8.2 单链表的删除
单链表第i个数据删除结点的算法思路:
1. 声明一指针p指向链表头指针,初始化j从1开始;
2. 当j<i时,就遍历链表,让p的指针向后移动,不断指向下一个结点,j累加1;
3. 若到链表末尾p成空,则说明第i个节点不存在;
4. 否则查找成功,将欲删除的结点p->next赋值给q;
5. 单链表的标准删除语句p->next=q->next;
6. 将q节点中的数据赋值给e,作为返回;
7. 释放q结点;
8. 返回成功;
实现代码算法如下:
/ 初始条件:顺序线性表L已存在,1≤i≤ListLength(L)
// 操作结果:删除L的第i个结点,并用e返回其值,L的长度减1
Status ListDelete(LinkList *L,int i,ElemType *e)
{
int j;
LinkList p,q;
p = *L;
j = 1;
while(p->next && j<i) // 遍历寻找第i-1个结点
{
p = p->next;
++j;
}
if(!(p->next) || j>i)
return ERROR; // 第i个结点不存在
q = p->next;
p->next = q->next; // 将q的后继赋值给p的后继
*e = q->data; // 将q节点中的数据给e
free(q); // 让系统回收此结点,释放内存
return ok;
}
C语言free函数的作用:让系统回收一个Node结点,释放内存
总结:对于插入或删除数据越频繁的操作,单链表的效率优势就越是明显。
3.9 单链表的整表创建
单链表整表创建的算法思路:
1. 声明一指针p和计数器变量i;
2. 初始化一空链表L;
3. 让L的头结点的指针指向NULL,即建立一个带头节点的单链表;
4. 循环 :
4.1 生成一新结点赋值给p;
4.2 随机生成一数字赋值给p的数据域p->data;
4.3 将p插入到头结点与前一新结点之间。
实现算法代码如下:
// 随机产生n个元素的值,建立带表头结点的单链线性表L(头插法)
void CreateListTail(LinkList *L, int n)
{
LinkList p,r;
int i;
srand(time(0)); // 初始化随机数种子
*L = (LinkList)malloc(sizeof(Node)); // 为整个线性表
r = *L;
for(i = 0; i<n; i++)
{
p = (Node *)malloc(sizeof(Node)); // 生成新结点
p->data = rand()%100+1; // 随机生成100以内的数字
r = p;
}
r->next = Null; // 表示当前链表结束
}
以上程序,L指整个单链表,r是指向尾结点的变量,r会随着循环不断地变化结点,L则是随着循环增长为一个多结点的链表。
3.10 单链表的整表删除
单链表整表删除的算法思路:
1. 声明一结点p和q;
2. 将第一个结点赋值给p;
3. 循环:
3.1 将下一结点赋值给q;
3.2 释放p;
3.3 将q赋值给p;
实现代码算法如下:
// 初始条件:顺序线性表L已存在,操作结果:将L重置为空表
Status ClearList(LinkList *L)
{
LinkList p,q;
p = (*L)->next; // p指向第一个结点
while(p) // 没到表尾
{
q = p->next;
free(p);
p = q;
}
(*L) ->next=NULL; // 头结点指针域为空
return ok;
}
3.11 单链表结构与顺序存储结构优缺点
存储分配方式:
1. 顺序存储结构用一段连续的存储单元依次存储线性表的数据元素。
2. 单链表采用链式存储结构,用一组任意的存储单元存放线性表的元素。
时间性能:
1. 查找:
1.1 顺序存储结构O(1)
1.2 单链表O(n)
2. 插入和删除:
2.1 顺序存储结构需要平均移动表长一般的元素,时间为O(n)
2.2 单链表在线出某位置的指针后,插入和删除时间仅为O(1)
3. 空间性能:
3.1 顺序存储结构需要预分配存储空间,分大了浪费,分小了易发生上溢
3.2 单链表不需要分配存储空间,只要有就可以分配,元素个数不受限制
结论:
若线性表需要频繁查找,很少进行插入和删除操作,采用顺序存储结构。
若需要频繁插入和删除,采用单链表结构。
当线性表中的元素个数变化较 大或者不知道多大,采用单链表结构,这样可以无需考虑存储空间的大小问题。
如果事先知道线性表的大致长度,用顺序存储结构效率会高很多
3.12 静态链表
静态链表:用数组描述的链表叫做静态链表,有的也叫做游标实现法。一为了方便插入数据,我们一般会把数组建立的大一些,防止溢出。
3.12.3 静态链表优缺点
优点:在插入和删除操作时,只需要修改游标,不需要移动元素,从而改进了在顺序存储结构中的插入和删除操作需要移动大量元素的缺点。
缺点:
1. 没有解决连续存储分配带来的表长难以确定的问题
2. 失去了顺序存储结构随机存取的特性
3.13 循环链表
循环链表:将单链表中终端结点的指针端由空指针改为指向头结点,就使整个单链表形成一个环,这种头尾相接的单链表称为单循环链表,简称循环链表。
3.14 双向链表
双向链表:双向链表是在单链表的每个结点中,再设置一个指向其前驱结点的指针域
在双向链表中的结点都有两个指针域,一个指向直接后继,一个指向直接前驱