第二章 线性表
线性表的定义和特点
由n(n≥0)个数据特性相同的元素构成的有限序列,称为线性表。
线性表中元素的个数n(n≥0)定义为线性表的长度,当n=0时称之为空表。
对于非空的线性表或线性结构,其特点是:
- 存在唯一的一个被称作”第一个“的数据元素
- 存在唯一的一个被称作”最后一个“的数据元素
- 除第一个元素之外,结构中的每个数据元素均只有一个前驱;
- 除最后一个元素之外,结构中的每个数据元素均只有一个后继。
线性表的类型定义
线性表的抽象数据类型定义:
线性表的顺序表示和实现
线性表的顺序表示
线性表的顺序表示指的是用一组地址连续的存储单元依次存储线性表的数据元素,这种表示也称作线性表的顺寻存储结构或顺序映像。通常,称这种存储结构的线性表为顺序表(Sequential List)。特点是,逻辑上相邻的数据元素,其物理位置也是相邻的。
线性表的顺序存储是一种随机存取的存储结构。
定义顺序表
typedef struct {
int* data;
int length;
}SqList;
顺序表中基本操作的实现
- 初始化
顺序表的初始化操作就是构造一个空的顺序表
bool InitList(SqList& L) {
L.data = new int[MAXSIZE];
if (!L.data)
{
exit(-2);
return false;
}
L.length = 0;
return true;
}
- 取值
取值操作是根据指定的位置序号i,获取顺序表中第i个数据元素的值。
由于顺序存储结构具有随机存取的特点,可以直接通过数组下标定位得到,data[i-1]单元
存储第i个数据元素。
bool GetElem(SqList L, int i, int& e) {
if (i < 1 || i > L.length)
{
return false;
}
e = L.data[i - 1];
return true;
}
- 查找
查找操作是根据指定的元素值e,查找顺序表中第1个值与e相等的元素。
若查找成功,则返回该元素在表中的位置序号;若查找失败,则返回0。
int LocateElem(SqList L, int e) {
for (int i = 0; i < L.length; i++)
{
if (L.data[i] == e)
{
return i+1;
}
}
return 0;
}
- 插入
线性表的插入操作是指在表的第i个位置插入一个新的数据元素e,使长度为n的线性表:
(a1,…,ai−1,ai,…,an)
变成长度为n + 1的线性表:
(a1,…,ai−1,e,ai,…,an)
数据元素ai−1和ai之间的逻辑关系发生了变化。在线性表的顺序存储结构中,由于逻辑上相邻的数据元素在物理位置上也是相邻的,因此,除非i=n+1,否则必须移动元素才能反映这个逻辑关系的变化。
例如,图2.5所示为一个线性表在插入前后数据元素在存储空间中的位置变化。为了在线性表的第5个位置上插入一个值为25的数据元素,则需将第5个至第8个数据元素依次向后移动一个位置。
一般情况下,在第i(1≤i≤n)个位置插入一个元素时,需从最后一个元素即第n个元素开始,依次向后移动一个位置,直至第i个元素(共n−i + 1个元素)。
bool ListInsert(SqList& L, int index, int e) {
if (index < 1 || index > L.length+1)
{
return false;
}
if (L.length == MAXSIZE)
{
return false;
}
for (int i = L.length-1; i >= index-1; i--)
{
L.data[i+1] = L.data[i];
}
L.data[index - 1] = e;
L.length++;
return true;
}
- 删除
线性表的删除操作是指将表的第i个元素删去,将长度
为n的线性表:
(a1,…,ai−1,ai,ai+1,…,an)
变成长度为n−1的线性表:
(a1,…,ai−1,ai+1,…,an)
数据元素ai−1、ai和ai+1之间的逻辑关系发生了变化,为了在存储结构上反映这个变化,同样需要移动元素。如图2.6所示,为了删除第4个数据元素,必须将第5个至第8个元素都依次向前移动一个位置。
一般情况下,删除第i(1≤i≤n)个元素时需将第i + 1个至第n个元素(共n−i个元素)依次
向前移动一个位置(i = n 时无须移动)。
bool ListDelete(SqList& L, int index) {
if (index < 1 || index > L.length)
{
return false;
}
for (int i = index; i <= L.length-1; i++)
{
L.data[i - 1] = L.data[i];
}
L.length--;
return true;
}
线性表的链式表示和实现
单链表的定义和表示
线性表链式存储结构的特点是:用一组任意的存储单元存储线性表的数据元素(这组存储单元可以是连续的,也可以是不连续的)。因此,为了表示每个数据元素ai与其直接后继数据元素ai+1之间的逻辑关系,对数据元素ai来说,除了存储其本身的信息之外,还需存储一个指示其直接后继的信息(直接后继的存储位置)。这两部分信息组成数据元素ai的存储映像,称为节点(node)。它包括两个域:其中存储数据元素信息的域称为数据域;存储直接后继存储位置的域称为指针域。指针域中存储的信息称作指针或链。n个节点[ai(1≤i≤n)的存储映像]链接成一个链表,即为线性表:
(a1, a2,…, an)
的链式存储结构。又由于此链表的每个节点中只包含一个指针域,故又称线性链表或单链表。
由上述可见,单链表可由头指针唯一确定。在C语言中可用“结构指针”来描述:
typedef struct LNode {
int data;
struct LNode* next;
}LNode,*LinkList;
链表增加头节点的作用如下。
- 便于首元节点的处理
- 便于空表和非空表的统一处理
单链表基本操作的实现
- 初始化
单链表的初始化操作就是构造一个如图2.10(b)所示的空表。
bool InitList(LinkList& L) {
L = new LNode;
L->next = NULL;
return true;
}
- 取值
和顺序表不同,链表中逻辑相邻的节点并没有存储在物理相邻的单元中,这样,根据给定的节点位置序号i,在链表中获取该节点的值不能像顺序表那样随机访问,而只能从链表的首元节点出发,顺着链域next逐个节点向下访问。
bool GetElem(LinkList L, int i, int& e) {
LNode *p = L->next;
int j = 1;
while (p && j < i)
{
p = p->next;
j++;
}
if (!p || j > i)
{
return false;
}
e = p->data;
return true;
}
- 查找
链表中按值查找的过程和顺序表类似,从链表的首元节点出发,依次将节点值和给定值e进行比较,返回查找结果。
int LocateElem(LinkList L, int e) {
LNode* p = L->next;
int j = 0;
while (p && p->data !=e)
{
p = p->next;
j++;
}
return j+1;
}
- 插入
假设要在单链表的两个数据元素a和b之间插入一个数据元素x,已知p为其单链表存储结构中指向节点a的指针,如图2.11(a)所示。
bool ListInsert(LinkList& L, int i, int e) {
LNode* p = L;
int j = 0;
while (p && (j < i-1))
{
p = p->next;
j++;
}
LNode *s = new LNode;
s->data = e;
s->next = p->next;
p->next = s;
return true;
}
- 删除
要删除单链表中指定位置的元素,同插入元素一样,首先应该找到该位置的前驱节点。如图2.13所示,在单链表中删除元素b时,应该首先找到其前驱节点a。为了在单链表中实现元素a、b和c之间逻辑关系的变化,仅需修改节点a中的指针域即可。假设p为指向节点a的指针,则修改指针的语句为:
bool ListDelete(LinkList& L, int i) {
LNode* p = L;
int j = 0;
while ((p->next) && (j < i-1))
{
p = p->next;
j++;
}
if (!(p->next) || (j > i-1))
{
return false;
}
LNode* q = p->next;
p->next = q->next;
delete q;
return true;
}
【算法分析】
类似于插入算法,删除算法时间复杂度亦为O(n)。
- 前插法
前插法是通过将新节点逐个插入链表的头部(头节点之后)来创建链表,每次申请一个新节点,读入相应的数据元素值,然后将新节点插入到头节点之后。
void CreateList_H(LinkList& Llist, int n) {
LNode* L = new LNode;
L->next = NULL;
int num;
for (int i = 0; i < n; i++)
{
LNode* p = new LNode;
scanf("%d", &num);
p->data = num;
p->next = L->next;
L->next = p;
}
Llist = L;
}
- 后插法
后插法是通过将新节点逐个插入链表的尾部来创建链表。同前插法一样,每次申请一个新节点,读入相应的数据元素值。不同的是,为了使新节点能够插入表尾,需要增加一个尾指针r指向链表的尾节点。
void CreateList_R(LinkList& List, int n) {
LNode* L = new LNode;
L->next = NULL;
LNode* r = L;
int num;
for (int i = 0; i < n; i++)
{
LNode* p = new LNode;
scanf("%d", &num);
p->data = num;
p->next = NULL;
r->next = p;
r = p;
}
List = L;
}
循环链表
双向链表
以上讨论的链式存储结构的节点中只有一个指示直接后继的指针域,由此,从某个节点出发只能顺指针向后寻查其他节点。若要寻查节点的直接前驱,则必须从表头指针出发。换句话说,在单链表中,查找直接后继的执行时间为O(1),而查找直接前驱的执行时间为O(n)。为克服单链表这种单向性的缺点,可利用双向链表(Double Linked List)。
顾名思义,在双向链表的节点中有两个指针域,一个指向直接后继,另一个指向直接前驱,节点结构如图2.19(a)所示,在C语言中可描述如下:
typedef struct DuLNode
{
ElemType data; //数据域
struct DuLNode *prior; //指向直接前驱
struct DuLNode *next; //指向直接后继
}DuLNode,*DuLinkList;
- 双向链表的插入
bool ListInsert_DuL(DuLinkList &L,int i,ElemType e)
{//在带头节点的双向链表L中第i个位置之前插入元素e
if(!(p=GetElem_DuL(L,i))) //在L中确定第i个元素的位置指针p
return false; //p为NULL时,第i个元素不存在
s=new DuLNode; //生成新节点*s
s->data=e; //将节点*s数据域置为e
s->prior=p->prior; //将节点*s插入L中,此步对应图2.20①
p->prior->next=s; //对应图2.20②
s->next=p; //对应图2.20③
p->prior=s; //对应图2.20④
return true;
}
- 双向链表的删除
bool ListDelete_DuL(DuLinkList &L,int i)
{//删除带头节点的双向链表L中的第i个元素
if(!(p=GetElem_DuL(L,i))) //在L中确定第i个元素的位置指针p
return false; //p为NULL时,第i个元素不存在
p->prior->next=p->next; //修改被删节点的前驱节点的后继指针,对应图2.21①
p->next->prior=p->prior; //修改被删节点的后继节点的前驱指针,对应图2.21②
delete p; //释放被删节点的空间
return true;
}
顺序表和链表的比较
空间性能的比较
- 存储空间的分配
顺序表的存储空间必须预先分配,元素个数有一定限制,易造成存储空间浪费或空间溢出现象;而链表不需要为其预先分配空间,只要内存空间允许,链表中的元素个数就没有限制。
基于此,当线性表的长度变化较大,难以预估存储规模时,宜采用链表作为存储结构。
- 存储密度的大小
存储密度是指数据元素本身所占用的存储量和整个节点结构所占用的存储量之比。
顺序表的存储密度为1,而链表的存储密度小于1。
基于此,当线性表的长度变化不大,易于事先确定其大小时,为了节约存储空间,宜采用顺序表作为存储结构。
时间性能的比较
- 存取元素的效率
若线性表的主要操作是和元素位置紧密相关的一类取值操作,很少做插入或删除时,宜采用顺序表作为存储结构。 - 插入和删除操作的效率
对于频繁进行插入或删除操作的线性表,宜采用链表作为存储结构。
小结