第 2 章 线性表
线性表的定义和基本操作
线性表的定义
线性表是具有相同数据类型的 n 个数据元素的有限序列。
相关概念:表头元素、表尾元素、直接前驱、直接后继
:warning
- 线性表是一种逻辑结构,表示元素之间一对一的相邻关系。顺序表和链表是指存储结构, 两者属于不同层面的概念,因此不要将其混淆。
线性表基本操作
Initiist(&L)
: 初始化表。构造一个空的线性表Length(L)
: 求表长。 返回线性表工的长度,即中数据元素的个数LocateElem(L,e)
: 按值查找操作。在表 t 中查找具有给定关键字值的元素GetElem(I,主)
:按位查找操作。获取表工中第 i 个位置的元素的值。ListInsert(&L,i,e)
:插入操作。在表工中的第个位置上插入指定元素 e。ListDelete(&L,i,&e)
:删除操作。删除表工中第 i 个位置的元素,并用 e 返回删除元素的值。PrintList(L)
:输出操作。按前后顺序输出线性表 的所有元素值。Empty(L)
:判空操作。若工为空表,则返回 true,否则返回 false。DestroyList(&L)
:销毁操作。销毁线性表,并释放线性表工所占用的内存空间。
线性表的顺序表示
顺序表的定义
**定义:**线性表的顺序存储又称顺序表。它是用一组地址连续的存储单元依次存储线性表中的数据元 素,从而使得逻辑上相邻的两个元素在物理位置上也相邻。
**特点:**逻辑顺序与其在储的物理顺相同
线性表中元素的位序是从 1 开始的,而数组中元素的下标是从 0 开始的。
数据结构描述
静态分配
#define MaxSize 50 // 定义线性表的最大长度
typeof struct{
ElemType data[MaxSize]; // 顺序表的元素
int length // 顺序表的当前长度
} SqList; // 顺序表的类型定义
动态分配
#define InitSize 50 // 表的初始长度
typeof struct{
ElemType *data; // 指示动态分配数组的指针
int MaxSize, length // 顺序表的当前长度
} SqList; // 顺序表的类型定义
// C 的初始动态分配语句为
L.data = (ElemType*)malloc(sizeof(ElemType)*InitSize);
// C ++的初始动态分配语句为
L.data = new ElemType(InitSize);
优点: 1、支持随机访问,可在 O(1)时间内找到制定元素 2、存储密度高,每个节点只存储数据元素
缺点: 1、插入删除需要移动大量元素,插入操作平均需要移动 n/2 个元素,删除操作平均需要移动(n- 1)/2 个元素。 2、顺序存储分配需要一段连续的存储空间,不够灵活
顺序表上的基本操作的实现
- 顺序表的初始化 静态分配
// 声明一个顺序表
void InitList(SqList &L){
L.Length = 0;
}
动态分配
void InitList(SqList &L){
L.data = (ElemType *)malloc(MaxSize * sizeof(ElemType));
L.length = 0;
L.MaxSize = InitSize;
}
- 插入操作
bool ListInsert(SqList &L, int i ,ElemType e){
if(i<1 || i > L.length+1) return false; // 判断i(位序)的范围是否有效
if(L.length >= MaxSize) return false; // 当前存储空间已满,不能插入
for( int j=L.length; j>=i; j--){
L.data[j] = L.data[j -1]; // 将第i 个元素及之后的元素后移
}
L.data[i - 1 ] = e; // 在位置主处放入e
L.length++;
return true;
}
最好情况:在表尾插入,时间复杂度 O(1)。 最坏情况:在表头插入,时间复杂度 O(n)。 平均情况:O(n)
- 删除操作
bool ListDelete(SqList &L, int i ,ElemType &e){
if(i<1 || i > L.length+1) return false; // 判断i(位序)的范围是否有效
e = L.data[i-1];
if(L.length >= MaxSize) return false; // 当前存储空间已满,不能插入
for( int j=i; j<L.length; j++){
L.data[j-1] = L.data[j]; // 将第i 个元素及之后的元素前移
}
L.length--;
return true;
}
最好情况:在表尾删除,时间复杂度 O(1)。 最坏情况:在表头删除,时间复杂度 O(n)。 平均情况:O(n)
- 按值查找(顺序查找)
int LocationElem(SqList L ,ElemType e){
int i;
for(int i = 0; i < L.length ; i++){
if(L.data[i] ==e){
return i+1;
}
}
return 0;
}
最好情况:在表头,时间复杂度 O(1)。 最坏情况:在表尾,时间复杂度 O(n)。 平均情况:O(n)
线性表的链式表示
单链表的定义
线性表的链式存储又称单链表,它是指通过一组任意的存储单元来存储线性表中的数据元素。为了建立数据元素之间的线性关系,对每个链表结点,除存放元素自身的信息之外,还需要存放一个指向其后继的指针。
typeof struct LNode{
ElemType data; // 数据域
struct LNode *next; // 指针域
} LNode,*LinkList
头指针 L(或 head 等)来标识一个单链表,指出链表的起始地址,头指针为 NULI 时表示一个空表。
为了操作上的方便,在单链表第 一个数据结点之前附加 一个结点,称为头结点。头结点的数据域可以不设任何信息,但也可以记录表长等信息。单链表带头结点时, 头指针 L 指向头结点
头结点和头指针的关系:不管带不带头结点,头指针都始终指向链表的第 一个结点,而头结 点是带头结点的链表中的第 一个结点,结点内通常不存储信息。
基本操作的实现
- 单链表初始化
// 带头节点
bool InitList(LinkList &L){
L = (LNode*)malloc(sizeof (LNode));
L ->next = NULL;
return true;
}
// 不带头节点
bool InitList(LinkList &L){
L = null;
return true;
}
- 求表长操作
// 不带头节点
int Length(LinkList L ){
int Len = 0;
LNode *p = L;
while(p->next != NULL){
p = p->next;
Len++;
}
return Len
}
- 按序号查找节点
LNode *GetElem(LinkList L ,int i){
LNode *p = L;
int j=0;
while(p !=NULL&&j<i){
p = p->next;
j++;
}
return p;
}
- 按值查找表节点
LNode *LocationElem(LinkList L, ElemType e){
LNode *p = L;
while(p != NULL&&p->data != e){
p = p->next;
}
return p;
}
- 插入结点操作
分前插法和后插法
// 找到第i-1的节点,再其后插入
bool ListInsert(LinkList &L , int i , ElemType e){
LNode *p = L;
int j=0;
while( p!= NULL&& j<i-1){
p = p->next;
j++;
}
if( p ==NULL) return false;
LNode *s = (LNode*)malloc(sizeof(LNode));
// 后插法
s->data = e;
s->next = p->next;
p->next = s;
// 前插法 交换思想
s->next = p->next;
p->next = s;
temp = p->data;
p->data = s->data;
s->data = temp;
return true;
}
- 删除结点 删除结点操作是将单链表的第 i 个结点删除。先检查删除位置的合法性,然后查找表中第 1- 1 个结点,即被删结点的前驱,再删除第之个结点。
bool ListDelete(LinkList &L, int i, ElemType &e){
LNode *p = L;
int j=0;
while( p!= NULL&& j<i-1){
p = p->next;
j++;
}
if( p ==NULL) return false;
LNode *q = p->next;
e = q->data;
p->next = q->next;
free(q);
return true;
}
- 采用头插法建立单链表
LinkList List_HeadInsert(LinkList &L){
LNode *s;
int x;
L = (LNode*)maclloc(sizeof(LNode));
L-next = NULL;
scanf("%d",&x);
while(x!=9999){
s = (LNode*)malloc(sizeof(LNode));
s->data = x;
s->next = L->next;
L->next = s;
scanf("%d", &x);
}
return L ;
}
- 采用尾插法建立单链表
需增加尾指针 r ,使其始终指向当前链表的尾结点
LinkList List_TailInsert(LinkList &L){
int x;
L = (LNode*)malloc(sizeof(LNode));
LNode *s , *r = L;
scanf("%d",&x);
while(x!=9999){
s = (LNode*)malloc(sizeof(LNode));
s->data = x;
r->next = s;
r = s;
scanf("%d",&x);
}
r->next = NULL;
return L
}
双链表
双链表结点中有两个指针prior
和next
,分别指向其直接前驱和直接后继,
结点类型定义
typedef struct DNode{
ElemType data;
Struct DNode *prior ,*next;
}DNode , *DNodeList;
- 双链表的插入操作 p s c 在 p 和 c 中插入 s
s-next = p->next ;
p->next-prior = s;
p->next = s;
s-prior = p
- 双链表的删除操作
删除结点*p 的后继结点 *q
p->next = q->next ;
q->next->prior = p
free(q);
循环链表
- 循环单链表 表尾结点*r 的 next 域指向 L,故表中没有指针域为 NULL 的结点,因此,循环单链表的判空条件不是头结点的指针是否为空,而是它是否等于头指针 L。 设的是尾指针工, r- >next 即为头指针,对在表头或表尾插入元素都只需要 O(1)的时间复杂度。
- 循环双链表 当循环双链表为空表时,其头结点的 prior 域和 next 域都等于 L。
静态链表
静态链表是用数组来描述线性表的链式存储结构,结点也有数据域 data 和指针域 next, 与前面所讲的链表中的指针不同的是,这里的指针是结点在数组中的相对地址 (数组下标),又 称游标 。静态链表也要预先分配一块连续的内存空间。
结构类型
#define MaxSize 50;
typedef struct{
ElemType data;
int next;
} SLinkList[MaxSize]
静态链表以 next==- 1 作为其结束的标志
顺序表和链表的比较
- 存取方式
- 逻辑结构和物理结构
- 查找、插入和删除操作
- 空间分配
如何选取存储结构
- 基于存储考虑
- 基于运算考虑
- 基于环境的考虑