大话数据结构学习笔记之(3) - 线性表、顺序存储、单链表及静态链表
定义
线性表(List): 零个或多个数据元素的有限序列
数学定义: 若将线性表记为 , 则表中 领先于 , 领先于 , 称 是 的直接前驱元素, 是 的直接后继元素。 当有 时, 仅有一个直接后继, 当 时, 有且仅有一个直接前驱
线性表的抽象数据类型
ADT 线性表(List)
Data 线性表的数据对象集合为 , 每个元素的类型均为
DataType
。 其中, 除第一个元素 外,每一个元素有且只有一个直接前驱元素,除了最后一个元素 外,每一个元素有且只有一个直接后继元素。数据元素之间的关系是一对一的关系Operation
InitList(*L); 初始化操作, 建立一个空的线性表 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, e, *e); 删除线性表
L
中第i
的位置元素, 并用e
返回其值 ListLength(L); 返回线性表
L
的元素个数endADT
线性表的顺序存储结构
即用一段地址连续的存储单元依次存储线性表的数据元素
#define MAXSIZE 20 // 存储空间初始分配量
typedef int ElemType; // ElemType 类型根据实际情况而定, 这是设为 int
typedef struct
{
ElemType data[MAXSIZE]; // 数组存储数据元素, 最大值为 MAXSIZE
int length; // 线性表当前长度
} SqList;
由上可知,顺序存储结构需要三个属性
- 存储空间的起始位置:数组
data
, 它的存储位置就是存储空间的存储位置 - 线性表的最大存储容量:数组长度
MaxSize
- 线性表的当前长度:
length
顺序存储结构的插入操作
插入算法的思路:
- 如果插入位置不合理,抛出异常
- 如果线性表长度大于等于数组长度,则抛出异常或动态增加容量
- 从最后一个元素开始向前遍历到第
i
个位置,分别将它们都向后移动一个位置 - 将要插入元素填入位置
i
处 - 表长加
1
// 操作结果:在 L 中第 i 个位置插入新的数据元素 e, L 的长度加 1
Status ListInsert(SqList *L, int i, ElemType e)
{
if(L->length == MAXSIZE) // 顺序线性表已满
return ERROR;
if(i < 1 || i > L->length + 1) // 当插入位置 i 不在范围内时
return ERROR;
if(i <= L->length) // 若插入位置不在表尾
{
for(int k = L->length - 1; k >= i - 1; k--) // 将要插入位置后数据元素向后移动一位
L->data[k + 1] = L->data[k];
}
L->data[i - 1] = e; // 插入新元素
L->length++;
return OK;
}
顺序存储结构的删除操作
删除算法的思路:
- 如果删除位置不合理,抛出异常
- 取出删除元素
- 从删除元素位置开始遍历到最后一个位置,分别将它们都向前移动一个位置
- 表长减
1
Status ListDelete(SqList *L, int i, ElemType *e)
{
if(L->length == 0)
return ERROR;
if(i < 1 || i > L->length)
return ERROR;
*e = L->data[i - 1];
if(i < L->length)
{
for(int j = i - 1; j < L->length - 1; j++)
L->data[j] = L->data[j + 1];
}
L->length--;
}
线性表顺序存储结构的优缺点
线性表的链式存储结构
定义
为了表示每个数据元素
与其直接后继数据元素
之间的逻辑关系,对数据元素
来说, 除了存储其本身的信息之外,还需存储一个指示其直接后继的信息(即直接后继的存储位置)。我们把存储数据元素信息的域成为数据域,把存储直接后继位置的域称为指针域。指针域中存储的信息称作指针或链。这两部分信息组成数据元素
的存储映像,称为结点(Node
)
n
个结点(
的存储映像)链结成一个链表,即为线性表(
)的链式存储结构, 因为此链表的每个节点中只包含一个指针域,所以叫单链表
链表中第一个结点的存储位置叫做头指针。单链表的第一个结点前附设一个结点,称为头结点
头指针与头结点的异同
单链表
无头结点的单链表
有头结点的单链表
单链表结构
结点由存放数据元素的数据域以及存放后继结点地址的指针域组成
typedef struct Node
{
ElemType data;
struct Node *next;
}Node;
typedef struct Node *LinkList;
单链表的插入
单链表第i
个数据插入节点的算法思路:
- 声明一个结点
p
指向链表第一个结点, 初始化j
从1
开始 - 当
j < i
时,就遍历链表, 让p
的指针向后移动, 不断指向下一结点,j
累加1
- 若到链表末尾
p
为空,则说明第i
个元素不存在 - 否则查找成功,在系统生成一个空结点
s
- 将数据元素
e
赋值给s->data
- 单链表的插入标准语句
s->next = p->next; p->next = s
- 返回成功
// 操作结果:在 L 中第 i 个位置之前插入新的数据元素 e, L 的长度加 1
Status ListInsert(LinkList *L, int i, ElemType e)
{
LinkList p, s;
p = *L;
int j = 1;
while(p && j < i) // 寻找第 i 个结点
{
p = p->next;
++j;
}
if(!p || j > i) // 第 i 个元素不存在
return ERROR;
s = (LinkList)malloc(sizeof(Node)); // 生成新结点
s->data = e;
s->next = p->next; // 将 p 的后继结点赋值给 s 的后继
p->next = s; // 将 s 赋值给 p 的后继
return OK;
}
单链表的删除
单链表第i
个数据删除结点的算法思路:
- 声明一个结点
p
指向链表第一个结点,初始化j
从1
开始 - 当
j < i
时,遍历链表,让p
的指针向后移动,不断指向下一个结点,j
累加1
- 若到链表末尾
p
为空,则说明第i
个元素不存在 - 否则查找成功,将与删除的结点
p->next
赋值给q
, 即p
为欲删除结点的上一结点,q
为欲删除结点 - 单链表的删除标准语句
p->next = q->next
- 将
q
结点中的数据赋值给e
,返回 - 释放
q
结点 - 返回成功
Status ListDelete(LinkList *L, int i, ElemType *e)
{
LinkList p, q;
p = (*L);
int j = 1;
while(p->next && j < i)
{
p = p->next;
++j;
}
if(!(p->next) || j > i) // 第 i 个元素不存在
return ERROR;
q = p->next;
p->next = q->next; // 将 q 的后继赋值给 p 的后继
*e = q->data; // 将 q 结点中的数据赋值给 *e
free(q); // 让系统回收此节点,释放内存
return OK;
}
单链表结构与顺序存储结构优缺点
静态链表
用数组描述的链表叫做静态链表,也叫作游标实现法,是为了给某些没有实现指针的编程语言设计的类似单链表
// 线性表的静态链表存储结构
#define MAXSIZE 1000
typedef struct
{
ElemType data;
int cur; // 游标,为 0 时表示无指向
}Component, StaticLinkList[MAXSIZE];
另外数组第一个和最后一个元素作为特殊元素处理,不存数据。我们通常把未被使用的数组元素称为备用链表。而数组第一个元素,即下标为0
的元素的cur
就存放备用链表的第一个结点的下标;而数组的最后一个元素的cur
则存放第一个有数值的元素的下标,相当于单链表中的头结点作用,当整个链表为空时,则为0
。下图所示相当于初始化的数组状态。
静态链表的插入
由于静态链表中操作的是数组,而不是动态链表中的结点申请和释放问题。故需实现申请结点和释放结点的功能,方法是将所有未被使用的及被删除的分量用游标链接成备用链表,插入时,从备用链表取得第一个结点作为待插入的新节点
// 若备用链表非空,则返回分配的结点下标,否则返回 0
int Malloc_SLL(StaticLinkList space)
{
int i = space[0].cur; // 获取备用链表的第一个结点,即存放在数组第一个元素中的 cur
if(space[0].cur)
space[0].cur = space[i].cur; // 备用链表第一个结点被申请使用,故后移一个
return i;
}
// 在 L 中第 i 个元素之前插入新的数据元素 e
Status ListInsert(StaticLinkList L, int i, ElemType e)
{
int j, k, l;
k = MAXSIZE - 1;
if(i < 1 || i > ListLength(L) + 1)
return ERROR;
j = Malloc_SLL(L);
if(j)
{
L[j].data = e;
for(l = 1; l < i; l++)
{
k = space[k].cur;
}
L[j].cur = L[k].cur;
L[k].cur = j;
return OK;
}
return ERROR;
}
静态链表的删除
// 将下标为 k 的空闲结点回收到备用链表
void Free_SSL(StaticLinkList L, int k)
{
L[k].cur = L[0].cur; // 把备用链表的第一个元素 cur 值赋给要删除的分量 cur
L[0].cur = k; // 把要删除的分量下标赋给第一个元素的 cur
}
// 删除在 L 中第 i 个数据元素 e
Status ListDelete(StaticLinkList L, int i)
{
int j, k;
k = MAXSIZE - 1;
if(i < 1 || i > ListLength(L))
return ERROR;
for(j = 1; j < i; j++)
k = L[k].cur;
j = L[k].cur;
L[k].cur = L[i].cur;
Free_SSL(L, j);
return OK;
}
静态链表优缺点
Code
结语
关于链表的面试题后续整理, fighting