第二章线性表
2.1线性表的定义和基本操作
1,定义(逻辑上是线性的)
线性表是具有相同数据类型的n(n20)个数据元素的有限序列,其中n为表长,当n=0时线性表是一个空表。
1,值得注意的特性
数据元素同类型、有限、有序
2,重要术语
a;是线性表中的“第i个”元素线性表中的位序
a1是表头元素;a是表尾元素。
除第一个元素外,每个元素有且仅有一个直接前驱;除最后一个元素外,每个元素有且仅
有一个直接后继
2,基本操作
1,创销、增删改查(所有数据结构适用的)
InitList(&L):初始化表。构造一个空的线性表L,分配内存空间。
DestroyList(&L):销毁操作。销毁线性表,并释放线性表L所占用的内存空间。
Listlnsert(&L,i,e):插入操作。在表L中的第i个位置上插入指定元素e。
ListDelete(&L,i,&e):删除操作。删除表L中第i个位置的元素,并用e返回删除元素的值。
LocateElem(L,e):按值查找操作。在表L中查找具有给定关键字值的元素。
GetElem(L,i):按位查找操作。获取表L中第i个位置的元素的值。
2,其他常用操作:
Length(L):求表长。返回线性表L的长度,即L中数据元素的个数
PrintList(L):输出操作。按前后顺序输出线性表L的所有元素值。
Empty(L):判空操作。若L为空表,则返回true,否则返回false。
2.2线性表的顺序表示
2.2.1顺序表的定义
顺序表——用顺序存储的方式实现线性表顺序存储。把逻辑上相邻的元素存储在物理位置上也相邻的存储单元中,元素之间的关系由存储单元的邻接关系来体现。
1,存储结构
顺序存储——逻辑上相邻的数据元素物理上也相邻。
2,实现方式
1,静态分配
使用“静态数组”实现
大小一旦确定就无法改变
2,动态分配
使用“动态数组”实现。
L.data = (ElemType *) malloc (sizeof(ElemType) * size);
顺序表存满时,可再用malloc动态拓展顺序表的最大容量.
需要将数据元素复制到新的存储区域,并用free函数释放原区域.
3,特点
①随机访:问能在O(1)时间内找到第i个元素
②存储密度高,每个节点只存储数据元素
③拓展容量不方便(即便采用动态分配的方式实现,拓展长度的时间复杂度也比较高)
④插入、删除操作不方便,需要移动大量元素
2.2.2顺序表的插入删除
1,插入
1,代码实现
Listinsert(&L,i,e)
将元素e插入到L的第i个位置。
插入位置之后的元素都要后移
2,时间复杂度分析
最好O(1)、最坏O(n)、平均O(n)
2,删除
1,实现
ListDelete(&L,i,&e)。
将L的第i个元素删除,并用e返回。
删除位置之后的元素都要前移。
2,时间复杂度
最好O(1)、最坏O(n)、平均O(n)
3,代码要点
代码中注意位序i和数组下标的区别
算法要有健壮性,注意判断i的合法性
跨考同学注意:移动元素时,从靠前的元素开始?还是从表尾元素开始?
分析代码,理解为什么有的参数需要加“&”引用
如果不加“&”,则被调用函数中处理的是参数数据的复制品
2.2.3顺序表的查找
1,按位查找
1,代码实现
GetElem(L,i)
获取表L中第i个位置的元素的值
用数组下标即可得到第i个元素L.data[i-1]。
2,时间复杂度
O(1)
由于顾序表的各个数据元素在内存中连续存放,因此可以根据起始地址和数据元素大小立即找到第i个元素——“随机存取”特性
2,按值查找
1,代码实现
LocateElem(L,e)
在顺序表L中查找第一个元素值等于e的元素,并返回位序
从第一个元素开始依次往后检索。
2,时间复杂度
最好O(1):目标元素在第一个位置
最坏O(n):目标元素在最后一个位置
平均O(n):目标元素在每个位置的概率相同
2.3线性表的链式表示
2.3.1单链表的定义
1,概念
用链式存储”(存储结构)实现了“线性结构”(逻辑结构)
一个结点存储一个数据元素
各结点间的先后关系用一个指针表示
优点:不要求大片连续空间,改变容量方便
缺点:不可随机存取,要耗费一定空间存放指针
2,用代码定义一个单链表
typedef struct LNode{
ElemType data;
struct LNode *next;
}LNode, *LinkList;
3,两种实现
1,不带头结点
空表判断:L==NULL。写代码不方便。
2,带头结点
空表判断:L->next==NULL。写代码更方便
4,补充
typedef关键字的用法
"LinkList"等价于"LNode*"前者强调这是链表,后者强调这是结点合适的地方使用合适的名字,代码可读性更高
2.3.2单链表的插入删除
1,插入
1,按位序插入
找到第i-1个结点,将新结点插入其后
//在第i个位置插入元素e,带头节点
bool ListInsert(LinkList &L, int i, ElemType e)
{
if(i<1)
return false;
LNode *p; //指针P指向当前扫描到的节点
int j = 0;//当前P指向的是第几个节点
p = L; //L指向头节点,头节点是第0个节点,不存储数据
while(p!=NULL && j<i-1)//循环找到第i-1个节点
{
p=p->next;
j++;
}
if(p==NULL)
return false;//i值不合法
LNode *s = (LNode *)malloc(sizeof(Lnode));
s->data=e;
s->next==p->next;
p->next=s;
return true;
}
//在第i个位置插入元素e,不带头节点
//不带头结点当i=1需要特殊处理
bool ListInsert(LinkList &L, int i, ElemType e)
{
if(i<1)
return false;
if(i==1)
{
LNode *s = (LNode *)malloc(sizeof(LNode));
s->data=e;
s->next=L;
L=S; //头指针指向新节点
return true;
}
LNode *p; //指针P指向当前扫描到的节点
int j = 1;//当前P指向的是第几个节点,在带头节点时这里是0.
p = L; //L指向头节点,头节点是第0个节点,不存储数据
while(p!=NULL && j<i-1)//循环找到第i-1个节点
{
p=p->next;
j++;
}
if(p==NULL)
return false;//i值不合法
LNode *s = (LNode *)malloc(sizeof(Lnode));
s->data=e;
s->next==p->next;
p->next=s;
return true;
}
2,指定节点的后插操作
//后插操作:在p节点后插入元素e
bool InsertNextNode(LNode *p, ElemType e)
{
if(p == NULL)
return false;
LNode *s = (LNode *)malloc(sizeof(LNode));
if(s == NULL)//内存分配失败
{
return false;
}
s->data = e;
p->next = s;
s->next = NULL;
return true;
}
3,指定节点的前插操作
//前插操作:在p节点前面插入元素e
bool InsertBeforeNode(LNode *p, ElemType e)
{
if(p == NULL)
return false;
LNode *s = (LNode *)malloc(sizeof(LNode));
if(s == NULL)
return false;
s->next = p->next;
p->next = s; //新节点s连接到p后面
s->data = p->data;//将p中的元素复制到s中
p->data = e; //将p的元素用e覆盖
return true;
}
2,删除
1,按位序删除
ListDelete(&L,i,&e):删除操作。删除表L中第i个位置的元素,并用e返回删除元素的值。
找到第i-1个结点,将其指针指向第i+1个结点,并释放第i个结点
bool ListDelete(LinkList &L,int i, ElemType &e)
{
if(i < 1)
return false;
LNode *p;
int j = 0;
while(p!=NULL && j < i - 1)
{
p++;
j++;
}
if(p==NULL) //i值不合法,过大
return false;
if(p->next == NULL)//第i-1个节点后没有节点,无法删除,i值不合法
return false;
LNode *d = p->next; //要删除的节点
p->next = d->next;//将要删除的节点从链表中断开
e = d->data;//用e返回要删除的元素的值
free(d);
}
2,指定节点删除
删除结点p,需要修改其前驱结点的next 指针
方法1:传入头指针,循环寻找p的前驱结点
方法2:偷天换日(类似于结点前插的实现)
bool DeleteNode(LNode *p)
{
if(p == NULL)
return false;
LNode *s = p->next; //将节点s指向要删除节点p的后继节点
p->data = s->data;//将节点p和其后继节点交换数据
p->next = s->next;//将节点p指向其后继节点的下一个节点
free(s);//释放p的后继节点
}
有坑:指定结点是最后一个结点时,需要特殊处理
2.3.3单链表的查找
1,按位查找
GetElem(L,i):按位查找操作。获取表L中第i个位置的元素的值。
LNode *GetElem(LinkList L, int i)
{
if(i < 0)
return NULL;
LNode *p = L;//指向当前扫描到的节点
int j = 0; //当前p指向的是第几个节点
while(p != NULL && j < i)//循环找到第i个节点
{
p++;
j++;
}
return p;
}
2,按值查找
LocateElem(L,e):按值查找操作。在表L中查找具有给定关键字值的元素。
LNode *LocateElem(LNode *L, ElemType e)
{
LNode *p = L;//当前扫描到的节点
while(p != NULL)
{
if(p->data == e)
break;
p= p->next;
}
return p;
}
3,求单链表长度
int Length(LinkList L)
{
int len = 0;
LNode *p = L;
while(p != NULL)
{
len++;
p = p-> next;
}
return len;
}
4,Key
三种基本操作的时间复杂度都是O(n)
如何写循环扫描各个结点的代码逻辑
注意边界条件的处理
2.3.4单链表的建立
step1:初始化一个单链表
Step2:每次娶一个数据元素,插入到表尾/表头
1,尾插法
对链表的尾节点执行后插操作
//定义单链表的节点类型
typedef struct LNode{
ElemType data;
struct LNode *next;
}LNode. *LinkList;
//初始化一个带头结点的单链表
bool InitList(LinkList &L)
{
L = (LNode *)malloc(sizeof(LNode));//分配一个头节点
if(L == NULL)
return false;
L-Next = NULL; //头节点之后暂时还没有节点
return true;
}
//尾插法建立单链表
LinkList List_TailInsert(LinkList &L)
{
int x;
L = (LNode *)malloc(sizeof(LNode));
L = NULL;
LNode *s;
LNode *r=L; //r为表尾指针
scanf("%d", &x);
while(x!=999)//结束条件
{
s = (LNode *)malloc(sizeof(LNode)); //为新节点分配空间
s->data = x;//给新节点赋值
r->next = s;//把新节点插入到链表尾上,后插
r = s; //把r始终指向表尾节点
scanf("%d", &x);
}
r->next = NULL; //尾结点指针置空
return L;
}
2,头插法
对链表的头节点执行后插操作。
LinkList List_HeadInsert(LinkList &L)
{
L = (LNode *)malloc(sizeof(LNode));
L = NULL;
LNode *r = L;
int x;
scnaf("%d", &x);
while(x != 999)
{
r = (LNode *)malloc(sizeof(Lnode));
r->data = x;
r->next = L->next;
L->next = r;
scanf("%d", &x);
}
r->next = NULL;
return L;
}
2.3.5双链表
1,初始化(带头节点)
头结点的prior、next都指向NULL
//双链表的定义
typedef struct DNode{
ElemType data;
struct DNode *prior, *next;
}DNode, *DLinklist;
//初始化双链表
bool InitDLinkList(DLinklist &L)
{
L = (DNode *)malloc(sizeof(DNode));
if(L == NULL)
return false;
L->next = NULL;
L->prior = NULL;
return true;
}
2,插入(后插)
注意新插入结点、前驱结点、后继结点的指针修改边界情况;
新插入结点在最后一个位置,需特殊处理
//在p节点之后插入节点s
bool InsertNextDNode(DNode *p, DNode *s){
if(p == NULL || s = NULL)
return false;
s->next = p->next;
if(p->next != NULL)//如果P有后继节点
p->next->prior = s;
p->next =s;
s->prior = p;
}
3,删除(后删)
注意删除结点的前驱结点、后继结点的指针修改边界情况;
如果被删除结点是最后一个数据结点,需特殊处理
//删除P节点的后继节点
bool DeleteNextDNode(DNode *p)
{
if(p == NULL)
return false;
DNode *q = p->next;//p的后继节点
if(q == NULL)
return false;
p->next = q->next;
if(q->next != NULL)
q->next->prior = p;
free(q);
}
//删除整个链表
void DestoryList(DLinklist &L)
{
while(L->next != NULL)
{
DeleteNextDnode(p);
}
free(L);
L = NULL;
}
4,遍历
从一个给定结点开始,后向遍历、前向遍历的实现(循环的终止条件)
链表不具备随机存取特性,查找操作只能通过顺序遍历实现
2.3.6循环链表
1,循环单链表
循环单链表:表尾节点的next指针指向头节点
//初始化一个循环单链表
bool InitList(LinkList &L)
{
L = (LNode *)malloc(sizeof(LNode));
if(L == NULL)
return false;
L->next = L;//头结点的next指向头结点自身
return true;
}
//判断循环单链表是否为空
bool Empty(LinkList L)
{
if(L->next == L)
return true;
else
retrun false;
}
//判断节点p是否为循环单链表的尾节点
bool isTail(LinkList L, LNode *p)
{
if(p->next == L)
return true;
else
return false;
}
2,循环双链表
表头结点的prior指向表尾结点;
表尾结点的 next指向头结点
//定义双链表
typedef struct DLnode{
ElemType data;
struct DLnode *next, *prior;
}DLnode, *DLinkList;
//初始化空的循环双链表
bool InitDLinkList(DLinkList &L)
{
L = (DLnode *)malloc(sizeof(DLnode));
if(L == NULL)
return false;
L->next = L;
L->prior = L;
return true;
}
//判断循环双链表是否为空
bool Empty(DLinkList L)
{
if(L->next == L)
return true;
else
return false;
}
//判断节点p是否为循环双链表的表尾节点
bool IsTail(DLinkList L, DLnode *p)
{
if(p->next == L)
return true;
else
return false;
}
//在循环双链表的p节点后插入节点s
bool InsertNextDLinkList(DLnode *p, DLnode *s)
{
s->next=p->next;
s->prior = p;
p->next->prior = s;
p->next = s;
}
3,代码问题
1,如何判空
2,如何判断结点p是否是表尾/表头结点(后向/前向遍历的实现核心)
3,如何在表头、表中、表尾插入/删除一个结点(插入、删除操作的不易错思路)
2.3.7静态链表表
1,什么是静态链表
静态链表:分配一整片连续的内存空间,各个结点集中安置。用数组的方式实现的链表
优点:增、删操作不需要大量移动元素
缺点:不能随机存取,只能从头结点开始依次往后查找;容量固定不可变
适用场景:①不支持指针的低级语言;②数据元素数量固定不变的场景(如操作系统的文件分配表FAT)
2,如何定义静态链表
#define MaxSize 10//静态链表的最大长度
struct Node{
ElemType data;
int next;//下一个元素的数组下标
};
3,简述基本操作的实现
初始化静态链表:把a[0]的next设为-1
把其他结点的next 设为一个特殊值用来表示结点空闲,如-2。
查找:从头结点出发挨个往后遍历结点
插入位序为i的结点:
①找到一个空的结点,存入数据元素
②从头结点出发找到位序为i-1的结点
③修改新结点的 next
④修改i-1号结点的next
删除某个结点:
①从头结点出发找到前驱结点
②修改前驱结点的游标
③被删除结点next设为-2
2.3.8顺序表和链表的比较
1,逻辑结构
都属于线性表,都是线性结构
一对一
2,存储结构(物理结构)
顺序表
优点:支持随机存取、存储密度高
缺点:大片连续空间分配不方便,改变容量不方便
链表
优点::离散的小空间分配方便,改变容量方便
缺点:不可随机存取,存储密度低
3,数据的运算/基本操作
1,创
- 顺序表
需要预分配大片连续空间。若分配空间过小,则之后不方便拓展容量;
若分配空间过大,则浪费内存资源;
静态分配:静态数组(容量不可改变)
动态分配:动态数组(malloc、free)容量可改变
- 链表
只需分配一个头结点(也可以不要头结点,只声明一个头指针),之后方便拓展。
2,销毁
- 顺序表
修改Length = 0
静态分配:静态数组(系统自动回收空间)
动态分配:动态数组(malloc、free)需要手动free
- 链表
依次删除各个节点,free
3,增/删
- 顺序表
插入/删除元素要将后续元素都后移/前移
时间复杂度O(n),时间开销主要来自移动元素
若数据元素很大,则移动的时间代价很高
- 链表
插入/删除元素只需修改指针即可
时间复杂度O(n),时间开销主要来自查找目标元素
查找元素的时间代价更低
4,查找
- 顺序表
按位查找:0(1)按值查找:0(n)
若表内元素有序,可在O(log2n)时间内找到
- 链表
按位查找:O(n)
按值查找:O(n)
4,选择
表长难以预估、经常要增加/删除元素——链表
表长可预估、查询(搜索)操作较多——顺序表