第二章 线性表
一. 线性表的定义和基本操作
1. 线性表(Linear List)的定义
线性表是具有相同数据类型的n个数据元素组成的有限序列,n为表长,n=0时为空表。
若用L命名线性表,则一般表示为:L = (a1,a2,...,ai,...,an)
几个概念:
①位序:ai是线性表中的“第i个”
②a1是表头元素,an是表尾元素
③除第一个元素外,每个元素有且仅有一个直接前驱;除最后一个元素外,每个元素有且仅有一个直接后继
2. 线性表的基本操作
①对数据的操作——创销,增删改查
②什么时候需要传入引用“&”:对参数的修改需要“带回来”
二. 顺序表的定义
1. 顺序表的定义
顺序表(Sequence List)——用顺序存储的方式实现线性表顺序存储
把逻辑上相邻的元素存储在物理位置上也相邻的存储单元中。
C语言中sizeof()的用法。
顺序表的特点:
①随机访问,在O(1)时间内找到第i个元素
②存储密度高m,每个结点只存储数据元素
③拓展容量不方便(即使动态分配,也有时间复杂度高的问题)
④插入、删除数据元素不方便,需要移动大量元素
2. 静态分配顺序表
/*静态分配
*/
#define MaxSize 10
typedef struct {
ElemType data[MaxSize];
int length;
}SqList;//Sq:sequence 顺序,结构
void InitList(SqList& L) {
for (int i = 0; i < MaxSize; i++){
L.data[i] = 0;//不初始化可能有遗留的“脏数据”
}
L.length = 0;
}
C语言中声明顺序表示长度和内容都需要初始化,所有元素都设置为默认初始值
Q:“数组”存满了怎么办?
A:长度不可调(静态分配)
3. 动态分配顺序表
/*动态分配
*/
#define InitSize 10
typedef struct {
int* data;
int MaxSize;
int length;
}SqList;
void InitList(SqList& L) {
L.data = (int*)malloc(InitSize * sizeof(int));
L.length = 0;
L.MaxSize = InitSize;
}
void IncreaseSize(SqList& L, int len) {
int* p = L.data;
L.data = (int*)malloc((L.MaxSize + len) * sizeof(int));
for (int i = 0; i < L.length; i++) {
L.data[i] = p[i];//复制数据到新区域
}
L.MaxSize += len;
free(p);
}
顺序表存满时,可再用malloc动态拓展顺序表的最大容量(需要强制类型转换,把malloc指针转换为原有顺序表中数据类型,如此在访问元素时方不出错)。需要把数据元素复制到新的存储区域,并用free函数释放原区域
#define定义函数,定义常量
三. 顺序表的插入删除
1. 插入listInsert(&L,i,e)
注意位序和数组下标(索引index)的关系,并从后面的元素依次移动
注意健壮性(可以用if判断),给出反馈,能处理异常情况
/*顺序表的插入
*/
bool ListInsert(SqList& L, int i, int e) {
if (i<1 || i>L.length) return false;//判断i的范围是否合法
if (L.length >= Maxsize) return false;//判断是否已满
for (int j = L.length; j >= i; j--) {
L.data[j] = L.data[j - 1];
}
L.data[i - 1] = e;
L.length++;
return true;
}
2. 插入操作的时间复杂度
最坏情况:新元素插入到表头,将原有的n个元素向后移,i = 1,循环n次,最坏时间复杂度O(n)
平均:
其中p为插入到每个位置的可能性,p=1/n+1.
3. 删除listDelete(&L,i,&e)
注意e为引用类型参数,因为要修改
注意先移动前面的元素
/*顺序表的删除
*/
bool ListDelete(SqList& L, int i, int& e) {
if (i < 1 || L.length < i) return false;
e = L.data[i - 1];
for (int j = i; j < L.length; j++) {
L.data[j - 1] = L.data[j];
}
L.length--;
return true;
}
4. 删除操作的时间复杂度
最坏情况:删除表头元素,循环(n-1)次,最坏时间复杂度O(n)
平均情况:
四. 顺序表的查找
1. 按位查找getElem(L,i)
/*顺序表的按位查找
*/
ElemType GetElem(SqList L, int i) {
return L.data[i - 1];
}
2. 按值查找locateElem(L,e)
遍历返回位序
/*顺序表的按值查找
*/
ElemType LocateElem(SqList L, ElemType e) {
for (int i = 0; i < L.length; i++) {
if (L.data[i] == e) {
return i + 1;//i+1是位序
}
return 0;
}
}
3. 按值查找时间复杂度
最坏情况:最坏时间复杂度O(n)
平均情况:
五. 单链表的定义
1. 什么是单链表
每个结点除了存放数据元素外,还要存储指向下一个结点的指针。
优点:不要求大片连续空间,改变容量方便。
缺点:不可随机存取,要耗费一定空间存放指针,无法逆向检索。
逻辑结构:线性表
2. 单链表的代码实现
typedef struct LNode {
ElemType data;//数据域
struct LNode* next;//指针域
}LNode,*LinkList;
1.声明不带头结点的单链表
/*声明不带头结点的单链表*/
bool InitList(LinkList& L) {
L = NULL;//防止脏数据
return true;
}
//判空
bool Empty(LinkList L) {
return(L == NULL);
}
2.带头结点的单链表(头结点不存储数据)
/*带头结点的单链表*/
bool InitList(LinkList& L) {
L = (LNode*)malloc(sizeof(LNode));
if (L == NULL) return false;
L->next = NULL;//头结点后暂时没有其他结点
return true;
}
//判空
bool Empty(LinkList L){
return (L->next == NULL);
}
六. 单链表的插入与删除
1. 按位序插入(带头结点)
/*按位序插入(带头结点)*/
bool ListInsert(LinkList& L, int i, ElemType e) {
if (i < 1)return false;
LNode* p;
int j = 0;
p = L;
while (p != NULL && j < i - 1) {//找到第(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连接到p后面
return true;
}
所有的增删操作均可以画图分析
2. 按位序插入(不带头结点)
/*按位序插入(不带头结点)*/
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;
int j = 1;
p = L;
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;
return true;
}
3. 指定结点的后插操作
/*指定结点的后插操作*/
//在给定结点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;
s->next = p->next;
p->next = s;
return true;
}
可见此部分代码与按位序插入部分的代码有所重合,所以可以调用此部分的函数来替代(封装的思想)
4. 指定结点的前插操作
/*给定结点的前插操作*/
bool InsertPriorNode(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->data = p->data;
p->data = e;
return true;
5. 按位序删除(带头结点)
/*按位序删除(带头结点)*/
bool ListDelete(LinkList& L, int i, ElemType e) {
if (i < 1)return false;
LNode* p;
int j = 0;
p = L;
//找到需要修改的结点的前一个结点,修改指针指向
while (p != NULL && j < i - 1) {//找到第(i-1)个结点
p = p->next;
j++;
}
if (p == NULL)return false;
if (p->next == NULL)return false;//指定位序处为空
LNode* q = p->next;
e = q -> data;
p->next = q->next;
free(q);
return true;
}
6. 指定结点的删除
/*指定结点的删除*/
bool DeleteNode(LNode* p) {
if (p == NULL)return false;
LNode* q = p ->next;
p->data = p->next->data;
p->next = q->next;
free(q);
return true;
}
七. 单链表的查找
1. 按位查找
/*按位查找*/
LNode* GetElem(LinkList L, int i) {
if (i < 0)return NULL;
LNode* p;
int j = 0;
p = L;//头结点相当于第0个结点
while (p != NULL && j < i){
p = p->next;
j++;
}
return p;
}
又在封装后可以替换掉前面查找位序对应的代码
2. 按值查找
/*按值查找*/
LNode* LocateElem(LinkList L,ElemType e){
LNode* p = L->next;//从第1个结点开始
while (p != NULL && p->data != e) {
p = p->next;
}
return p;//遍历完了没找到会返回NULL
}
3.求表长
/*求表长*/
int Length(LinkList L) {
int len = 0;
LNode* p = L;
while (p->next != NULL) {
p = p->next;
len++;
}
return len;
}
八. 单链表的建立
1.尾插法建立单链表
①初始化单链表
②变量length统计长度
③while循环插入元素到尾部
/*尾插法*/
LinkList List_TailInsert(LinkList& L) {
int x;
L = (LinkList)malloc(sizeof(LNode));
L->next = NULL;
LNode* s, * r = L;
scanf("%d", &x);
while (x != 9999) {//停止输入所需要输入x的值,自己定义的
s = (LNode*)malloc(sizeof(LNode));
s->data = x;
r->next = s;
r = s;
scanf("%d", &x);
}
r->next = NULL;
return L;
}
2. 头插法建立单链表(逆置)
①初始化单链表
②while循环插入元素到头部
可发现只需要在尾插法代码上稍作修改
/*头插法*/
LinkList List_HeadInsert(LinkList& L) {
LNode* s;
int x;
L = (LinkList)malloc(sizeof(LNode));//LinkList和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;
}
3. 给出一个LinkList L,用头插法逆置
/*逆置*/
LinkList List_Reverse(LinkList L) {
Lr = (LinkList)malloc(sizeof(LNode));
Lr->next = NULL;
LNode* s;
LNode* p = L->next;
while (p != NULL) {
s = (LNode*)malloc(sizeof(LNode));
s->data = p->data;
s->next = Lr->next;
Lr->next = s;
p = p->next;
}
return Lr;
}
九. 双链表
1. 双链表与单链表的区别
①单链表:无法逆向检索
②双链表:可进可退,存储密度更低
2. 单链表的初始化
/*初始化双链表*/
typedef struct DNode {
ElemType data;
struct DNode* prior, * next;
}DNode,*DLinkList;
bool InitDLinkList(DLinkList &L){
//分配头结点(DNode*和DLinkList是等价的,只是强调的东西不同)
L = (DNode*)malloc(sizeof(DNode);
if (L == NULL) return false;
L->prior = NULL;
L->next = NULL;
return true;
}
3. 判空(与单链表相同)
//判空
bool Empty(DLinkList L) {
return(L ->next == NULL);
}
4. 双链表的后插法
/*双链表的后插法*/
//在p结点后面插入s结点
bool InsertNextNode(DNode* p, DNode* s) {
s->next = p->next;
p->next->prior = s;
s->prior = p;
p->next = s;
}
//更具有健壮性的代码(针对在尾指针处插入)
bool InsertNextNode(DNode* p, DNode* s) {
if(p == NULL || s == NULL)return false;
s->next = p->next;
if (p->next != NULL) p->next->prior = s;//p有后继
s->prior = p;
p->next = s;
return true;
}
5. 双链表的删除和销毁
/*双链表的删除*/
bool DeleteNextNode(DNode* p) {//删除p的后继
if (p == NULL)return false;
DNode* q = p->next;//找到下一个结点
if (q == NULL)return false;//p没有后继
p->next = q->next;
if (q->next != NULL)q->next->prior = p;
free(q);
return true;
}
//双链表的销毁
void DestroyList(DLinkList& L) {
while (L->next != NULL) DeleteNextNode(L);
free(L);
L == NULL;
}
6. 双链表的遍历
①后向遍历 ②前向遍历(跳过头结点)
//后向遍历
while (p != NULL){
p = p->next;
}
//向前遍历
while (p->prior != NULL) {
p = p->prior;
}
十. 循环链表
1. 循环单链表(可循环遍历各个结点)
①初始化
分配头结点后,把头结点next指向表头。
L->next=L;
②判断某个结点是否为表尾
/*判断某个结点是否为表位*/
bool IsTail(LinkList L, LNode* p) {
return(p->next == L);
}
2.循环双链表
表头prior指向表尾,表尾next指向表头。
①初始化
分配头结点后
L->next=L;
L->prior=L;
②判空与循环单链表类似
③插入时不用判断后继是否为空
④删除
p->next=q->next;
q->next->prior=p;
free(q);
十一. 静态链表(逻辑相邻,可以物理不相邻)
1. 静态链表的定义(用数组实现的链表)
分配一整片连续的内存空间,各个结点集中安置。
每个数据元素4B,每个游标4B(每个结点一共8B)
设起始的地址为addr,则的地址为(addr+8*2)
2. 静态链表的初始化
/*静态链表的初始化*/
#define MaxSize 10
struct Node{
ElemType data;
int next;//下一个元素的数组下标
};
void testSLinkList() {
struct Node a[MaxSize];
}
把a[0]的next设为-1,把其他结点的next设为特殊值,如-2。
3. 查找
从头结点出发挨个往后遍历结点。O(n)
4. 插入位序为i的结点
①找到一个空结点(判空操作),存入数据元素
②从头结点出发找到位序为(i-1)的结点
③修改新的结点的next(游标)
④修改(i-1)号结点的next(游标)
5. 删除某个结点
①从头结点出发找到要删除的结点的前驱结点
②修改前驱结点的游标
③被删除的结点的next改为-2
6. 优缺点
①优点:增删不需要移动大量元素
②缺点:不能随机存取,只能从头结点开始往后查,容量固定。
适用于不支持指针的低级语言;数据元素表容量固定不变的场景(如操作系统的文件分配表FAT)。
十二. 顺序表和链表的比较
1. 逻辑结构上都是线性表
2. 存储结构
顺序存储:①优点:支持随机存取,存储密度高
②缺点:大片连续空间分配不方便,改变容量不方便
链式存储:①优点:离散的小空间分配方便,改容量方便
②缺点:不可随机存取,存储密度低
3. 基本操作
①表长难以预估,经常增删——链表
②表长可以预估,经常查——顺序表
4. 开放问题答题框架
①逻辑结构-->②存储结构-->③基本操作重点