线性表
概述
- 线性表:零个或多个具有相同特性数据元素的有限序列。
- 在一个复杂的线性表中,数据元素可以由若干个数据项组成。
- 特点:
(1)均匀性: 同一线性表的各数据元素必定具有相同的数据类型和长度
(2)有序性: 各数据元素都有自己的位置且数据元素之间的相对位置是线性的
分类
- 按存储结构划分:
- 顺序表: 按索引值从小到大存放在一片相邻的连续区域,结构紧凑,存储密度为1。
- 链表: 单链表,双链表、循环链表。
- 按操作划分:
- 线性表: 不限制操作,先进后出、后进先出。
- 栈: 限制在同一端操作。
- 对列: 限制在两端操作,先进先出,后进后出。
抽象数据类型
1. 以上所提及的运算,是逻辑结构上定义的运算,只要给出这些运算的功能是“做什么”,至于“如何做”等实现细节,只有待确定了存储结构之后才考虑的。
2. 复杂的个性化操作,是将简单的基本操作组合而成的。
顺序存储结构
- 定义: 用一段地址连续的存储单元依次存储线性表的数据元素。
#define MAXSIZE 20 /* 存储空间初始分配量 */
typedef int ElemType; /* ElemType 类型根据实际情况而定,这里假设为int */
typedef struct
{
ElemType data[MAXSIZE]; /* 数组存储数据元素,最大值为MAXSIZE */
int length; /* 线性表的当前长度 */
}SqList;
地址计算
假设每个数据元素占用的是c个存储单元,那么线性表中的第i+1个元素的存储位置满足下列关系:(LOC表示获得存储位置的函数)
L O C ( a i + 1 ) = L O C ( a i ) + c LOC(a_{i+1}) = LOC(a_i) + c LOC(ai+1)=LOC(ai)+c
因此对于第i个数据元素ai的存储位置可以有a1推算得出:
L O C ( a i ) = L O C ( a i ) + ( i − 1 ) ∗ c LOC(a_{i}) = LOC(a_i) + (i - 1) * c LOC(ai)=LOC(ai)+(i−1)∗c
获取元素
#define OK 1
#define ERROR 0
#define TRUE 1
#define FALSE 0
typedef int Status;
/* 初始条件:顺序线性表L已存在,1<=i<=ListLength(L) */
/*操作结果:用e返回L中第i个数据元素的值*/
Status GetElem(SqList L, int i, ElemType *e)
{
if(L.length==0 || i < 1 || i > L.length)
return ERROR;
*e = L.data[i-1];
return OK;
}
插入操作
/* 初始条件:顺序线性表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) /* 插入范围出错 */
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;
}
删除操作
- 基本思路:
- 如果删除位置不合理,抛出异常
- 取出删除数据
- 从删除数据位置开始遍历到最后一个元素的位置,分别将它们都向前移动一个位置。
- 表长减一
/* 初始条件:顺序线性表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;
}
优缺点
链式存储结构
- 结点:
数据域
+指针域
- 数据域: 存储数据元素信息的域
- 指针域: 存储直接后继位置的域
- 单链表: 链表中的每个结点中只包含一个
指针域
- 头指针: 链表中第一个结点的存储位置
- 头节点: 设立在第一个结点前的一个结点,头指针的数据域可以不存储任何信息,也可以存储线性表的长度等附加信息
/* 线性表的单链表存储结构 */
typedef struct Node
{
ElemType data;
struct Node * next;
}Node;
typedef struct Node * LinkList; /* 定义LinkList */
获取元素
- 算法思路:
- 声明一个指针
p
指向链表的第一个节点,初始化j
从1
开始。- 当
j<i
时,就遍历链表,让p
的指针向后移动,不断指向下一个结点,j
累加1
;- 若到链表末尾
p
为空,说明第i
个元素不存在- 否则查表成功,返回节点
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;
*e = p->data;
return OK;
}
插入操作
- 算法思路:
- 声明一指针
p
指向链表头结点,初始化j
从1
开始;- 当
j<i
时,就遍历链表,让指针p
向后移动,不断指向下一个结点,j
累加1
;- 若到链表末尾即
p
为空,则说明第i
个节点不存在, 否则查找成功;- 在系统中生成一个空节点
s
,将数据e
赋值给s->data
- 单链表的插入标准语句
s->next=p->next;
p->next=s;
- 返回成功。
/* 初始条件:顺序线性表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) /目标:使p指向第i-1个结点/
{
p = p->next;
++j;
}
if(!p || j > i)
return ERROR; /* 第i个结点不存在 */
/*j>i是为了防止误输入为i<1的整数*/
s = (LinkList)malloc(sizeof(Node)); /* 生成新结点 */
s->data = e;
s->next = p->next; /* 将p的后继结点赋值给s的后继 */
p->next = s; /* 将s赋值为p的后继 */
return OK;
}
删除操作
- 算法思想:
- 声明一指针
p
指向链表的头结点,初始化j
从1
开始- 当
j<i
时,就遍历链表,让p
的指向向后移动,不断指向下一个结点,j
累加1
- 若到链表末尾即
p
为空,则说明第i
个结点不存在- 否则查找成功,将欲删除的节点
p->next
赋值给q
- 单链表的删除标准语句
p->next=q->next
- 将
q
结点中的数据赋值给e
,作为返回- 释放
q
结点- 返回成功
/* 初始条件:顺序线性表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) /* while结束后p会指向第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;
free(q);
return OK;
}
整表创建
- 算法思路:
- 声明一指针
p
和计数器变量i
;- 初始化空链表
L
;- 让
L
的头结点的指针域指向NULL
,即建立一个带头结点的单链表;- 循环:
(1)生成一新结点赋值给p
;
(2)随机生成一数字赋值给p
的数据域p->data
;
(3)将p
插入到头结点与前一新结点之间。
/* 随机产生n个元素的值,建立带表头结点的单链线性表L(头插法) */
void CreateListHead(LinkList *L, int n)
{
LinkList p;
int i;
srand(time(0));
p = *L;
*L = (LinkList)malloc(sizeof(Node));
(*L)->next = NULL;
for(i = 0; i < n; i++)
{
p = (LinkList)malloc(sizeof(Node)); /* 生成新结点 */
p->data = rand() % 100 + 1; /* 随机生成100以内的数字 */
p->next = (*L)->next;
(*L)->next = 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;
r->next = p;
r = p;
}
r->next = NULL;
}
循环链表
- 将单链表的终端结点的指针域由
NULL
改为头结点
,即可形成循环链表- 循环链表的循环的判断条件:
p->next != L
,即p->next
不等于头结点,则循环未结束
- 循环链表中不用头指针,而是用指向终端结点的
尾指针
来表示循环链表。
(1) 尾指针rear
指向终端结点,则查找终端结点是O(1)
(2)rear->next->next
指向开始结点,则查找开始结点也是O(1)
- 合并循环链表
p = rearA->next; /* 保存A表的头结点 */
rearA->next = rearB->next->next; /* 将本是指向B表的第一个结点(不是头结点) */
/* 赋值给rearA->next */
q=rearB->next;
rearB->next = p; /* 将原A表的头结点赋值给rearB->next */
free(q);
双向链表
- 在单链表的基础上,再设置一个指向其
前驱结点
的指针域
/* 线性表的双向链表结构 */
typedef struct DulNode
{
ElemType data;
struct DuLNode * prior; /* 直接前驱指针 */
struct DuLNode * next; /* 直接后继指针 */
}DulNode, * DuLinkList;
插入操作
s->prior = p; /* 把p赋值给s的前驱,如图1*/
s->next = p->next; /* 把p->next赋值给s的后继 如图2*/
p->next->prior = s; /* 把s赋值给p->next的前驱,如图3 */
p->next = s; /* 把s赋值给p的后继 */
删除操作
p->prior->next = p->next; /* 把p->next赋值给p->prior的后继,如图中1 */
p->next->prior = p->prior; /* 把p->prior赋值给p->next的前驱,如图中2 */
free(p); /* 释放结点 */
静态链表
/* 线性表的静态链表存储结构 */
/* 假设链表的最大长度是1000 */
#define MAXSIZE 1000
typedef struct
{
ElemType data; /* 游标(Cursor),为0时表示无指向 */
int cur;
}Component,StaticLinkList[MAXSIZE];
- 用数组表示链表,元素是由两数据域组成:
data
和cur
(1)data
:存放数据元素
(2)cur
:相当于单链表的next
指针,存放该元素的后继元素在数组中的下标
- 未被使用的数组元素称为备用链表
- 数组第一个元素的
cur
存放备用链表的第一个结点的下标- 数组最后一个元素的
cur
则存放第一个有数值的元素的下标,相当于单链表中头结点作用。
初始化
/* 将一维数组space中各分量链成一备用链表 */
/* space[0].cur为头指针,"0"表示空指针 */
Status initList(StaticLinkList space)
{
int i;
for(i = 0; i < MAXSIZE - 1; i++)
space[i].cur = i + 1;
space[MAXSIZE-1].cur = 0; /*目前静态链表为空,最后一个元素的cur为0*/
return OK;
}
插入操作
/* 若备用空间链表非空,则返回分配的结点下标,否则返回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 = MAX_SIZE - 1; /* 注意k首先是最后一个元素的下标 */
if(i < 1 || i > ListLength(L) + 1)
return ERROR;
j = Malloc_SSL(L); /* 获得空闲分量的下标 */
if(j)
{
L[j].data = e; /* 将数据赋值给此分量的data */
for(l = 1; l <= i - 1; l++) /* 找到第i个元素之前的位置 */
k = L[k].cur;
L[j].cur = L[k].cur; /* 把第i个元素之前的cur赋值给新元素的cur */
L[k].cur = j; /* 把新元素的下标赋值给第i个元素之前元素的cur */
return OK;
}
return ERROR;
}
问题: 该插入操作当数组中除第一个元素和最后一个元素外均插满时,再插入元素会产生错误。
删除操作
/* 删除在L中第i个数据元素e */
Status LinkDelete(StaticLinkList L, int i)
{
int j, k;
if(i < 1 || i > ListLength(L))
return ERROR;
k = MAX_SIZE - 1;
for(j = 1; j <= i - 1; j++)
k = L[k].cur; /* 找到第i-1个结点在数组中的下标 */
j = L[k].cur; /*找到第i个结点在数组中的下标*/
L[k].cur = L[j].cur;
Free_SSL(L, j);
return OK;
}
/* 将下标为k的空闲结点收回到备用链表 */
void Free_SSL(StaticLinkList space, int k)
{
space[k].cur = space[0].cur; /*把第一个元素cur值赋给要删除的分量cur*/
space[0].cur = k; /* 把要删除的分量下标赋值给第一个元素的cur */
}
优缺点
广义表
定义
n
个元素a1,a2,...,ai,...,an
的有限序列,其中ai
或者是原子项,或者是一个广义表,通常记作LS=(a1,a2,...,ai,...an)。
- 线性表 vs 广义表:
数据类型 | 区别 |
---|---|
线性表 | 元素仅限于原子项 |
广义表 | 元素容许具有其自身结构 |
原子是作为结构上不可分割的成分,它可以是数或是一个结构
举例
- A = () :A为一个空表,长度为0
- B = (e) :表B只有一个原子e,B的长度为1
- C = (a,(b,c,d)):表C的长度为2,2个元素分别为原子a和子表(b,c,d)
- D = (A, B, C):表D的长度为3,三个元素都是广义表,将上述的5. A、B、C带入,则D=((), (e),(b,c,d))。
- E = (E):递归的表,长度为2,E相当于一个无限的广义表E=(a,(a,(a,(a,…))))。
广义表的元素可以是子表,而子表的元素还可以是子表
- 区分广义表()和(())
区别 | () | (()) |
---|---|---|
是否为空表 | 空表 | 非空表 |
长度 | 0 | 1 |
表示
- 由于广义表难以用顺序存储结构表示,因此通常采用链式存储结构
- 表结点:
- 原子结点
- 头尾链表存储表示:
typedef enum {ATOM,LIST } ElemTag; //ATOM==0:表示原子,LIST==1:表示子表
typedef struct GLNode {
ElemTag tag; //公共部分,用以区分原子部分和表结点
union { //原子部分和表结点的联合部分
AtomType atom; //atom是原子结点的值域,AtomType由用户定义
struct { struct GLNode *hp, *tp;} ptr;
// ptr是表结点的指针域,ptr.hp 和ptr.tp分别指向表头和表尾
};
} *Glist; //广义表类型