文章目录
2.1 线性表的定义与基本操作
2.1.1 线性表的定义
线性表是具有相同
数据类型的n (n≥0) 个数据元素
的有限序列
,其中n为表长
,当n=0时线性表是一个空表
。若用L命名线性表,则其一般表示为
2.1.2 线性表的基本操作
线性表的基本操作:
2.1.3 小结
2.2 线性表的顺序表示
2.2.1 顺序表的定义
线性表的顺序存储又称顺序表。它是用一种地址连续的存储单元存储数据元素,使得逻辑上相邻的数据元素物理上也相邻。
(用存储位置的相邻来体现数据元素之间的逻辑关系)
注意:线性表中元素的位序是从1开始的,而数组中元素的下标是从0开始的。
1. 静态分配
#define MaxSize 10 //定义最大长度
typedef struct{
ElemType data[MaxSize]; //用数组存放数据元素
int length; //顺序表的当前长度
}SqList; //顺序表的类型定义
//初始化一个顺序表
//避免内存中会遗留“脏数据”
void InitList(SqList &L){
for(int i=0;i<MaxSize;i++)
L.data[i] = 0;
L.length = 0;
}
int main()
{
SqList L; //声明一个顺序表
InitList(L); //初始化顺序表
return 0;
}
2. 动态分配
#include <stdio.h>
#include <stdlib.h> //malloc、free函数的头文件
#define InitSize 10 //表长度的初始定义
typedef struct{
ElemType *data; //指示动态分配数组的指针
int MaxSize; //数组的最大容量
int length; //顺序表的当前长度
}SqList; //顺序表的类型定义
void InitList(SqList &L){
L.data = (ElemType *)malloc(sizeof(ElemType)*InitSize);
L.length = 0;
L.MaxSize = InitSize;
}
//增加动态数组的长度
void IncreaseSize(SqList &L,int len){
ElemType *p = L.data;
L.data = (ElemType *)malloc(sizeof(ElemType)*(MaxSize+len)); //重新分配了15个存储单元
for(int i=0;i<L.length;i++)
L.data[i] = p[i]; //将数据复制到新区域(时间开销大)
L.MaxSize = L.MaxSize+len;
free(p);
}
int main()
{
SqList L; //声明一个顺序表
InitList(L); //初始化顺序表
//.....随便插入几个元素
IncreaseSize(L,5);
return 0;
}
3. 顺序表的特点
4. 小结
2.2.2 顺序表上基本操作的实现
1. 插入
ListInsert(&L,i,e):插入操作。在表L中的 第i个(i是位序)
位置上 插入指定元素e。
1 ≤ i ≤ L.length+1。
(插入、删除代码建立在顺序表的“静态分配”实现方式之上, “动态分配”也雷同。)
//在位序i插入元素e
bool ListInsert(SqList &L,int i,int e) {
if (i<1 || i>L.length + 1) return false;
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 += 1;
return true;
}
2. 插入操作的时间复杂度分析
问题规模n= L.length (表长)
3. 删除
ListDelete(&L,i,&e):删除操作。删除表L中 第i个(i是位序)
位置 的元素,并用e返回删除元素的值。
1 ≤ i ≤ L.length。
//删除位序i的元素,并用e带回
bool ListDelete(SqList& L, int i, int& e) {
if (i<1 || i>L.length) 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. 删除操作的时间复杂度分析
2.3 线性表的链式表示
2.3.1 单链表的定义
1. 什么是单链表
2. 单链表中结点类型的描述以及单链表的表示
单链表中结点类型的描述:
typedef struct LNode {
int data;
struct LNode* next;
}LNode,*LinkList;
单链表的表示:
通常用头指针标识一个单链表
LNode * L; //声明一个指向单链表第一个结点的指针
或 LinkList L; //声明一个指向单链表第一个结点的指针(代码可读性更强)
3.不带头结点的单链表
L= Null 是空表。
#include <stdio.h>
typedef struct LNode {
int data;
struct LNode* next;
}LNode,*LinkList;
//初始化一个空的单链表
bool InitList(LinkList &L) {
L = NULL; //防止脏数据
return true;
}
//判断单链表是否为空
bool IsEmpty(LinkList L) {
if (L == NULL) return true;
else return false;
}
void test() {
LinkList L; ////声明一个指向单链表的指针
InitList(L); //初始化一个空表
//......后续代码......
}
4. 带头结点的单链表
L->next == NULL是空表。
#include <stdio.h>
#include <stdlib.h>
typedef struct LNode {
int data;
struct LNode* next;
}LNode,*LinkList;
//初始化一个空的单链表
bool InitList(LinkList &L) {
L = (LNode *)malloc(sizeof(LNode)); //分配一个头结点
if (L == NULL) //malloc分配失败
return false;
L->next = NULL;
return true;
}
//判断单链表是否为空
bool IsEmpty(LinkList L) {
if (L->next == NULL) return true;
else return false;
}
void test() {
LinkList L; ////声明一个指向单链表的指针
InitList(L); //初始化一个空表
//......后续代码......
}
5. 小结
2.3.2 单链表上基本操作的实现
一、单链表的插入删除
1. 按位序插入
插入结点操作将值为x的新结点插入到单链表的第i个位置上。先检查插入位置的合法性,然后找到待插入位置的前驱结点,即第i-1个结点,在它后面插入结点。
(1) 带头结点
头结点可以看作“第0个”结点(这样写代码便于理解),j 初始设0
。
//在第i个位置插入元素e
bool ListInsert(LinkList &L,int i,int e) {
if (i < 1) //i值不合法
return false;
int j = 0; //当前p指向的结点位置
LNode* p; //指针p指向当前扫描到的结点
p = L;
while (j < i - 1 && p != NULL) { //循环找到第i-1个结点
p = p->next;
j++;
}
if (p == NULL) //i值不合法
return false;
LNode* s = (LNode*)malloc(sizeof(LNode));
s->data = e;
s->next = p->next;
p->next = s;
return true; //插入成功
}
最好时间复杂度:O(1)
最坏时间复杂度:O(n)
平均时间复杂度:O(n)
(2) 不带头结点
不带头结点,那么 j 初始设1
。
//在第i个位置插入元素e,不带头结点
bool ListInsert(LinkList& L, int i, int e) {
if (i < 1) //i值不合法
return false;
if (i == 1) { //插入第一个结点的操作与其他结点的操作不同
LNode* s = (LNode*)malloc(sizeof(LNode));
s->data = e;
s->next = L;
L = s; //头指针指向新结点
return true;
}
LNode* p; //指针p指向当前扫描到的结点
p = L;
int j = 1; //当前p指向的结点位置
while (j < i - 1 && p != NULL) { //循环找到第i-1个结点
p = p->next;
j++;
}
if (p == NULL) //i值不合法
return false;
LNode* s = (LNode*)malloc(sizeof(LNode));
s->data = e;
s->next = p->next;
p->next = s;
return true; //插入成功
}
最好时间复杂度:O(1)
最坏时间复杂度:O(n)
平均时间复杂度:O(n)
2. 指定结点的后插操作
//在p结点之后插入元素e
bool InsertNextNode(LNode *p, int 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;
}
时间复杂度:O(1)
3. 指定结点的前插操作
想在指定结点前插的话,有两种方法;
法一:
循环查找p的前驱结点q,再对q后插。时间复杂度是O(n)。
时间复杂度:O(n)
法二:
在p结点后插入结点s,然后将p结点的数据域与s结点的数据域交换。
//在p结点之前插入结点s
bool InsertPriorNode(LNode* p, LNode* s) {
if (p == NULL || s == NULL)
return false;
s->next = p->next;
p->next = s; //s连到p之后
int temp = p->data; //交换数据域部分
p->data = s->data;
s->data = temp;
return true;
}
时间复杂度:O(1)
4. 按位序删除
(1) 带头结点的:
//删除第i个位置的元素,并用e返回
bool ListDelete(LinkList& L, int i, int& e) {
if (i < 1) return false;
LNode* p; //p指向当前扫描到的结点
p = L;
int j = 0; //当前p指向的是第几个结点
while (j < i - 1 && p != NULL) {
p = p->next;
j++;
}
if (p == NULL) //i值不合法
return false;
if (p->next == NULL) //第i-1个结点之后已无其他结点
return false;
LNode* q = p->next; //q指向被删除的结点
e = q->data; //用e返回元素的值
p->next = q->next;
free(q); //释放结点的存储空间
return true; //删除成功
}
最好时间复杂度:O(1)
最坏时间复杂度:O(n)
平均时间复杂度:O(n)
(2) 不带头结点的:
bool ListDelete(LinkList& L, int i, int& e) {
if (i < 1) return false;
LNode* p; //p指向当前扫描到的结点
p = L;
int j = 1; //当前p指向的是第几个结点
if (i == 1) {
LNode* s;
s = L;
L = s->next;
free(s);
}
//.......剩下的与上面 带头结点代码中while开始到结束 的一样。
}
最好时间复杂度:O(1)
最坏时间复杂度:O(n)
平均时间复杂度:O(n)
5. 指定结点的删除
删除结点p,需要修改其前驱结点的next指针。有两个方法。
法1:
传入头指针,循环寻找p的前驱结点。
时间复杂度:O(n)
法2:
将删除结点的数据域与后驱结点的数据域交换,然后直接删除后驱结点就好。
( 类似于指定结点前插的实现)
如果p是最后一个结点,不能 用方法二
,只能从表头开始依次寻找p的前驱,使用方法一。
这里可以发现单链表的局限性:无法逆向检索,有时候不太方便。
//删除指定结点p
bool DeleteNode(LNode* p) {
if (p == NULL) return false;
LNode* q = p->next;
p->data = q->data;
p->next = q->next;
free(q);
return true;
}
时间复杂度:O(1)
6. 小结
二、单链表的查找
1. 按位查找
时间复杂度:O(n)
2. 按值查找
//按值查找,找到数据域==e的结点
LNode* LocateElem(LinkList L, int e) {
LNode* p = L ->next;
//从第1个结点开始查找数据域为e的结点
while (p != NULL && p->data != e)
p = p->next;
return p; //找到后返回该结 点指针,否则返回NULL
}
时间复杂度:O(n)
3. 求表的长度
求表长操作就是计算单链表中数据结点(不含头结点)的个数。设置一个计数器len。
(1)带头结点的:
//求表的长度
int Length(LinkList L) {
int len = 0; //统计表长
LNode* p = L;
while (p ->next != NULL) {
p = p->next;
len++;
}
return len;
}
(2)不带头结点的:
只需要把len初始值改为1即可。
(1) (2) 时间复杂度都是:O(n)
4. 小结
三、单链表的建立
如果给你很多个数据元素(ElemType),要把它们存到一个单链表里边,怎么搞?
Step 1:初始化一个单链表
Step 2:每次取一个数据元素,插入到表尾/表头
这里只写带头结点的。
1. 尾插法
//尾插法建立单链表
LinkList List_Taillnsert(LinkList& L) {
int x;
LNode *s,*r = L; //r是表尾指针
L = (LNode*)malloc(sizeof(LNode)); //头结点
scanf_s("%d", &x); //输入结点的值
while (x != 9999) {
s = (LNode*)malloc(sizeof(LNode));
s->data = x;
r->next = s;
r = s; //r指向新的表尾结点;永远保持r指向最后一个结点
scanf_s("%d", &x);
}
r->next = NULL; //尾结点指针置空
return L;
}
时间复杂度: O(n)
2. 头插法
//头插法
LinkList Head_Insert(LinkList &L) {
int x;
LNode* s;
L = (LNode*)malloc(sizeof(LNode)); //头结点
L->next = NULL; //初始为空链表
scanf_s("%d",&x);
while (x != 9999) {
s = (LNode*)malloc(sizeof(LNode));
s->data = x;
s->next = L->next;
L->next = s;
scanf_s("%d",&x);
}
return L;
}
时间复杂度: O(n)
3. 小结
2.3.3 双链表
以下代码都是基于带头结点的
单链表结点中只有一个指向其后继的指针,使得单链表只能从头结点依次顺序地向后遍历。要访问某个结点的前驱结点(插入、删除操作时),只能从头开始遍历,访问后继结点的时间复杂度为O(1),访问前驱结点的时间复杂度为O(n)。
为了克服单链表的上述缺点,引入了双链表,
双链表结点中有两个指针 prior和next,分别指向其前驱结点和后驱结点。如下图:
1. 双链表的初始化
//双链表
typedef struct DNode {
int data;
struct DNode* prior;
struct DNode* next;
}DNode,*DLinkList;
//双链表的初始化
bool InitList(DLinkList &L) {
L = (DNode*)malloc(sizeof(DNode)); //头结点
if (L == NULL) return false;
L->prior = NULL; //头结点的prior永远指向NULL
L->next = NULL; //头结点目前后面没有结点
return true;
}
//判断双链表是否为空
bool Empty(DLinkList L) {
if (L->next == NULL) return true;
else return false;
}
2. 双链表的插入
如果p是最后一个结点,就要加上一句 if(p->next != NULL) //如果p结点有后继结点。
注意:1 2步必须在4之前。
//双链表的插入,在p结点之后插入s结点
bool InsertNextDNode(DNode *p,DNode *s) {
if (p == NULL || s == NULL) return false;
s->next = p->next; // 1
if(p->next != NULL) //如果p结点有后继结点
p->next->prior = s; // 2
s->prior = p; // 3
p->next = s; // 4
return true;
}
3. 双链表的删除
如果p是最后一个结点,就要加上一句 if(q->next != NULL) //q结点不是最后一个结点
//双链表的删除,删除p结点的后继节点
bool DeleteNextDNode(DNode* p) {
if (p == NULL) return false;
DNode* q = p->next; //要删除的结点q
if (q == NULL) return false; //若p没有后继
p->next = q->next;
if(q->next != NULL) //q结点不是最后一个结点
q->next->prior = p;
free(q); //释放结点空间
return true;
}
4. 双链表的遍历
5. 小结
2.3.4 循环链表
1. 循环单链表
循环单链表的初始化:
//初始化循环单链表
bool InitList(LinkList& L) {
L = (LNode*)malloc(sizeof(LNode)); //分配一个头结点
if (L == NULL) return false;
L->next = L;
return true;
}
//判断循环单链表是否为空
bool Empty(LinkList L) {
if (L->next == L) return true;
else return true;
}
2. 循环双链表
循环双链表的初始化:
//初始化循环双链表
bool InitDLinkList(DLinkList &L) {
L = (DNode*)malloc(sizeof(DNode)); //分配一个头结点
if (L == NULL) return false;
L->prior = L;
L->next = L;
return true;
}
//判断循环双链表是否为空
bool Empty(DLinkList L) {
if (L->next == L) return true;
else return false;
}
循环双链表的插入:
循环双链表的删除:
3. 小结
2.3.5 静态链表
1. 什么是静态链表
其实就是数组,只是每个位置存储的是结点类型的元素,指针域放的是下一个元素的下标。
2. 如何定义一个静态链表
3. 简述基本操作的实现
4. 小结
2.3.6 顺序表和链表的比较
1. 逻辑结构
2. 物理结构
3. 基本操作
4. 顺序表与链表的适用场景
5. 知识回顾&&重要考点