数据结构复习——线性表
尚未完成awa
所有代码均来自《2023年数据结构考研复习指导》
线性表
线性表的基本操作
//非正确写法
InitList(&L);//初始化表
Length(L);//求表长
LocateElem(L,e);//按值查找操作
GetElem(L,i);//按位查找
ListInsert(&L,i,e);//插入操作
ListDelete(&L,i,&e);//删除操作
PrintList(L);//输出操作
Empty(L);//判空操作
DestroyList(L);//销毁操作
顺序表
顺序表的定义
1.静态分配
//静态分配
#define MaxSize 50 //定义线性表的最大长度
typedef struct{
ElemType data[MaxSize]; //顺序表的元素
int length; //顺序表的当前长度
}SqList; //顺序表的类型定义
2.动态分配
//动态分配
#define InitSize 100 //表长度的初始定义
typedef struct{
ElemType *data; //指示动态分配数组的指针
int MaxSize,length; //数组的最大容量和当前个数
}SeqList; //动态分配数组顺序表的类型定义
C的动态初始分配语句
L.data = (ElemType*)malloc(sizeof(ElemType) * InitSize);
C++的动态初始分配语句
L.data = new ElemType[InitSize];
顺序表上基本操作的实现(使用静态分配)
/*************************************************/
//定义的是静态分配链表
#define SqListMaxSize 50 //定义线性表的最大长度
typedef struct {
int data[SqListMaxSize]; //顺序表的元素
//ElemType data[MaxSize];
int length; //顺序表的当前长度
}SqList; //顺序表的类型定义
//初始化
void InitList(SqList& L) {
memset(L.data, 0, sizeof(L));
L.length = 0;
return ;
}
//线性表长度
int Length(SqList& L) {
return L.length;
}
//判空
bool Empty(SqList& L) {
return Length(L) == 0;
}
//插入操作
bool ListInsert(SqList& L, int i, int e) {
if (i<1 || i>L.length + 1) //判断i的范围是否有效
return false;
if (L.length >= SqListMaxSize) //当前存储空间已满,不能插入
return false;
for (int j = L.length; j >= 1; j--) //将第i个元素及之后的元素后移
L.data[j] = L.data[j - 1];
L.data[i - 1] = e; //在位置i处放入e
L.length++; //线性表长度加1
return true;
}
//删除操作
bool ListDelete(SqList& L, int i, int& e) {
if (i<1 || i>L.length) //判断i的范围是否有效
return false;
e = L.data[i - 1]; //将被删除的元素赋值给e
for (int j = 1; j < L.length; j++) //将第i个位置后的元素前移
L.data[j - 1] = L.data[j];
L.length--; //线性表长度减1
return true;
}
//按值查找
int LocateElem(SqList L, int e) {
int i;
for (i = 0; i < L.length; i++)
if (L.data[i] == e) //下表为i的元素值等于e,返回其位序i+1
return i + 1; //退出循环,说明查找失败
return 0;
}
//位序查找
int GetElem(SqList L, int i) {
return L.data[i];
}
//打印
void PrintList(SqList L) {
}
//销毁
void DestroyList(SqList& L) {
//使用的静态链表,由系统自动回收
}
链表
单链表
单链表的定义
1.表结点的定义
typedef struct LNode {
ElemType data; //数据域
struct LNode* next; //指针域
}LNode,*LinkList;
单链表是非随机存取的存储结构,即不能直接找到表中某个特定的结点。
在查找某个特定的结点时,需要从表头开始遍历。
2.分类
1)不带头结点
2)带头结点
引入头结点带来两个优点:
① 由于第一个数据节点的位置被存放在头结点的指针域中,因此在链表的第一个位置上的操作和在表的其他位置的操作一致,无需进行特殊处理
② 无论链表是否为空,其头指针都指向头结点的非空指针(空表中头结点的指针域为NULL),因此空表和非空表的处理也得到了统一
单链表上基本操作的实现
1.采用头插法建立单链表
LinkList List_HeadInsert(LinkList& L) {
//头插法建立单链表
LNode* s;
int x;
L = (LinkList)malloc(sizeof(LNode)); //头结点
L->next = NULL;
scanf("%d", &x);
while (x != 9999) { //此处9999为结束标志,实际使用中根据情况自行设置
s = (LNode*)malloc(sizeof(LNode)); //新结点
s->data = x;
s->next = L->next;
L->next = s;
scanf("%d", &x);
}
return L;
}
设所建立的单链表表长为 n n n,则头插法建立单链表的时间复杂度为 O ( n ) O(n) O(n)
2.采用尾插法建立单链表
LinkList List_TailInsert(LinkList& L) {
//尾插法建立单链表
int x;
L = (LinkList)malloc(sizeof(LNode));
LNode* s, * r = L; //r为表尾指针
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;
}
设所建立的单链表表长为 n n n,则尾插法建立单链表的时间复杂度为 O ( n ) O(n) O(n)
3.按序号查找结点值
LNode* GetElem(LinkList L, int i) {
//按序号查找结点值
int j = 1;
LNode* p = L->next;
if (i == 0)
return L;
if (i < 1)
return NULL;
while (p && j < i) {
p = p->next;
j++;
}
return p; //若i大于表长,则返回NULL
}
按序号查找结点值的时间复杂度为 O ( n ) O(n) O(n)
4.按值查找表结点
LNode* LocateElem(LinkList L, ElemType e) {
//按值查找表结点
LNode* p = L->next;
while (p != NULL && p->data != e)
p = p->next;
return p;
}
按值查找结点值的时间复杂度为 O ( n ) O(n) O(n)
5.插入结点操作
插入节点操作是将值为
x
x
x的新结点插入到单链表的第
i
i
i个位置上。
应先检查插入位置的合法性,然后找到待插入位置的前驱结点,即第
i
−
1
i-1
i−1个结点,再在其后插入新结点。
算法首先调用按序号查找算法,查找第
i
i
i个结点。
//s是指向新结点的指针
//p是指向第i-1个结点的指针
p = GetElem(L, i - 1);
s->next = p->next;
p->next = s;
本算法主要的时间开销在于查找第i-1个元素,时间复杂度为
O
(
n
)
O(n)
O(n)
若在给定的结点后面插入新结点,则时间复杂度为
O
(
1
)
O(1)
O(1)
6.对某一节点进行前插操作
前插操作:在某结点的前面插入新结点
后插操作:在某结点的后面插入新结点
对结点的前插操作均可转化为后插操作,前提是从单链表的头结点开始遍历,找到其前驱结点,时间复杂度为 O ( n ) O(n) O(n)
此外,可以采用另一种方式:
//待插入结点为*s,欲将*s插入到*p的前面
//仍然将*s插入到*p后面,然后将p->data与s->data交换
s->next = p->next;
p->next = s;
temp = p->data;
p->data = s->data;
s->data = temp;
该算法,既满足了逻辑关系,又能使得时间复杂度为 O ( 1 ) O(1) O(1)
7.删除结点操作
删除节点操作是将单链表的第
i
i
i个结点删除
应先检查删除位置的合法性,后查找表中第 i − 1 i-1 i−1个结点,即被删除结点的前驱结点,再将其删除
//结点*p为找到的被删除结点的前驱结点
//仅需修改*p的指针域,将*p的指针域next指向*q的下一个结点
p = GetElem(L, i - 1);
q = p->next;
p->next = q->next;
free(q);
该算法的主要时间耗费在查找操作上,时间复杂度为 O ( n ) O(n) O(n)
还有另一种实现方式
p = GetElem(L, i - 1);
q = p->next;
p->data = p->next->data;
p->next = q->next;
free(q);
个人感觉没啥用
8.求表长操作
设置一个计数器,从表头开始遍历,每访问一个结点,计数器值+1,直至访问到空结点。
算法时间度为
O
(
n
)
O(n)
O(n)
双链表
双链表的定义
1.为什么要使用双链表
单链表只有后继指针,限制了单链表的遍历操作只能从头结点进行
在获取特定结点的前驱结点时,只能从头遍历
为解决单链表的该缺点,双链表设置了前驱指针和后继指针。
2.双链表表结点的定义
typedef struct DNode {
ElemType data; //数据域
struct DNode* prior, * next; //前驱指针和后继指针
}DNode, * DLinkList;
双链表上基本操作的实现
1.头插法建立双链表
DLinkList DList_HeadInsert(DLinkList& L) {
//头插法建立双链表
DNode* s;
int x;
L = (DLinkList)malloc(sizeof(DNode)); //头结点
L->next = NULL;
L->prior = NULL;
scanf("%d", &x);
while (x != 9999) { //此处9999为结束标志,实际使用中根据情况自行设置
s = (DNode*)malloc(sizeof(DNode)); //新结点
s->data = x;
s->next = L->next;
s->next->prior = s;
L->next = s;
s->prior = L;
scanf("%d", &x);
}
return L;
}
2.尾插法建立双链表
DLinkList DList_TailInsert(DLinkList& L) {
//尾插法建立双链表
int x;
L = (DLinkList)malloc(sizeof(DNode));
DNode* s, * r = L; //r为表尾指针
scanf("%d", &x);
while (x != 9999) {
s = (DNode*)malloc(sizeof(DNode));
s->data = x;
r->next = s;
s->prior = r;
//->next = NULL;
//由于最后有尾结点指针置空,这步可以省略
r = s;
scanf("%d", &x);
}
r->next = NULL; //尾结点指针置空
return L;
}
3.按序号查找结点值
DNode* GetElem(DLinkList L, int i) {
//按序号查找结点值
int j = 1;
DNode* p = L->next;
if (i == 0)
return L;
if (i < 1)
return NULL;
while (p && j < i) {
p = p->next;
j++;
}
return p; //若i大于表长,则返回NULL
}
按序号查找结点值的时间复杂度为 O ( n ) O(n) O(n)
4.按值查找表结点
DNode* LocateElem(DLinkList L, ElemType e) {
//按值查找表结点
DNode* p = L->next;
while (p != NULL && p->data != e)
p = p->next;
return p;
}
按值查找表结点的时间复杂度为 O ( n ) O(n) O(n)
5.双链表的插入操作(后插)
双链表的插入操作是指在双链表中p所指的结点之后插入结点*s
s->next = p->next; //1
p->next->prior = s; //2
s->prior = p; //3
p->next = s; //4
//1,2两步必须在4之前
6.双链表的删除操作
双链表的删除操作是指删除双链表中结点p的后继结点q
p->next = q->next;
q->next->prior = p;
free(q);
循环链表
循环单链表
1.循环单链表
循环单链表和单链表的区别在于:
单链表的尾结点的后继指针是NULL
循环单链表的尾结点的指针改为指向头结点,从而使整个链表形成一个环
2.循环单链表和单链表在操作上的差异
[1]判空
单链表的判空实现为
bool IsEmpty(LinkList L) {
return (L->next == NULL);
}
循环单链表的判空实现为
bool IsEmpty(LinkList L) {
return (L->next == L);
}
[2]插入、删除
循环单链表的插入、删除操作与单链表的几乎一样
对于循环单链表需要注意的是,如果插入、删除的位置在表尾,则要注意操作后仍要使得该单链表具有循环的性质,即表尾结点的next域指向表头
[3]遍历
单链表只能从表头结点开始遍历整个链表
而循环单链表可以从任意一个结点开始遍历整个链表
此外,建议在使用循环单链表的时候,不仅设置头指针,最好还设置一个尾指针,以方便对表尾处进行操作
循环双链表
1.循环双链表
循环双链表和双链表的区别在于:
双链表的尾结点的后继指针是NULL,双链表的头结点的前驱指针是NULL
循环双链表的尾结点的指针改为指向头结点,循环双链表的头结点的指针改为指向尾结点,从而使整个链表形成一个环
2.循环双链表和双链表在操作上的差异
[1]判空
循环双链表的判空实现为
bool IsEmpty(DLinkList L) {
return (L->next == L && L->prior == L);
}
静态链表
静态链表的定义
1.静态链表是如何实现的
静态链表借助数组来描述线性表的链式存储结构,结点也有数据域data和指针域next,与之前所涉及的指针的概念相区分的是,静态链表的指针值得是结点的相对地址(即为数组下表)。
静态链表需要预先分配一块连续的内存空间
2.静态链表结构类型
#define MaxSize 50 //静态链表的最大长度
typedef struct {
ElemType data;
int next; //下一个元素的数组下标
}SLinkList[MaxSize];
静态链表上的基本操作
1.静态链表的尾结点
静态链表以
SLinkList A[MaxSize];
A[k]->next == -1;
作为结束的标志
2.静态链表的插入、删除
静态链表的插入、删除操作与动态链表的相同,只需要修改指针,不需要移动元素。
静态链表并没有单链表使用起来方便,因此更适合于在不支持指针的高级语言中实现。
顺序表和链表的比较
1.存取(读写)方式
顺序表可以顺序存取,也可以随机存取,链表只能从表头顺序存取元素。
例如在第i个位置上执行存或取得操作,顺序表仅需一次访问,而链表则需要从表头开始依次访问i次。
2.逻辑结构和物理结构
采用顺序存储时,逻辑上相邻的元素,对应的物理存储位置也相邻
而采用链式存储时,逻辑上相邻的元素,物理存储位置不一定相邻,对应的逻辑关系是通过指针链接来表示的
3.查找、插入和删除操作
对于按值查找,顺序表无序时,两者的复杂度均为
O
(
n
)
O(n)
O(n)
顺序表有序时,可采用折半查找,此时的时间复杂度为
l
o
g
2
n
log_2{n}
log2n
对于按序号查找,顺序表支持随机访问,时间复杂度为
O
(
1
)
O(1)
O(1),而链表的平均时间复杂度为
O
(
n
)
O(n)
O(n)。
顺序表的插入、删除操作,平均需要移动半个表长的元素
链表的插入、删除操作,只需要膝盖相关结点的指针域即可。
由于链表的每个结点都带有指针域,故存储密度不够大。
4.空间分配
顺序存储在静态存储分配情形下,一旦存储空间装满就不能扩充,若再加入新元素,则会出现内存溢出,因此需要预先分配足够大的存储空间。
动态存储分配可以对存储空间进行扩充,但是需要移动大量元素,导致操作效率降低,而分配的成功与否则与内存中的情况相关。
链式存储的结点空间只在需要时申请分配,只要内存有空间就可以分配,操作灵活、高效。
选择存储结构
1.基于存储的考虑
难以估计线性表的长度或存储规模时,不宜采用顺序表
链表不用实现估计存储规模,但是链表的存储密度较低
2.基于运算的考虑
在顺序表中按序号访问
a
i
a_i
ai的时间复杂度为
O
(
1
)
O(1)
O(1),而链表中按序号访问的时间复杂度为
O
(
n
)
O(n)
O(n)。若是经常按序号访问数据元素,那么顺序表优于链表。
在顺序表中进行插入、删除操作时,平均移动表中一半的元素,当数据元素信息量较大时且表较长时,会很耗时;在链表中进行插入、删除操作时,主要耗时在寻找插入、删除的位置。若是经常进行插入、删除操作,那么链表优于顺序表。
难免有错误之处,请大佬们在评论区指出!