目录
第一章、绪论
1.1_数据结构的基本概念
知识总览
数据元素、数据项
- 数据元素是数据的基本单位,通常作为一个整体考虑
- 一个数据元素可由若干个数据项组成,数据项是构成数据元素的不可分割的最小单位
数据结构、数据对象
- 数据结构是相互之间存在一种或多种特定关系的数据元素的集合
- 数据对象是具有相同性质的数据元素的集合,是数据的一个子集
数据结构的三要素
-
逻辑结构 :集合 :各个元素同属于一个集合,别无其它关系; 线性结构 :数据元素之间是一对一的关系,除了第一个元素,每一个元素都有唯一前趋,除了最后一个元素,每一个元素都有唯一后驱;树形结构 :数据元素之间是一对多的关系;图结构 :数据元素之间是多对多的关系。
-
物理结构(存储结构) :
-
数据的运算 :运算的定义是针对逻辑结构的,运算的实现是针对存储结构的。
数据类型、抽象数据类型
- 数据类型是一个值的集合和定义在这个集合上的一组操作的总称。
- 抽象数据类型(ADT)是抽象数据组织及与之相关的操作。也就是在数据结构三要素中只关心逻辑结构和数据的运算,而不关心物理结构(存储结构)
1.2.1_算法的基本概念
什么是算法
- 程序 = 数据结构 + 算法
- 数据结构是要处理的信息
- 算法是处理信息的步骤
算法的特性
- 算法特性是算法必须具备的特性
- 1.有穷性 :一个算法总在有穷步之后结束,且每一步会在有穷时间内完成。
- 注 :算法必须是有穷的,而程序可以是无穷的。
- 2.确定性 :对于相同的输入只能得到相同的输出。
- 3.可行性 :算法中描述的操作都可以通过已经实现的基本运算执行有限次实现。
- 4.输入 :有零个或多个输入。
- 5.输出 :有一个或多个输出。
1.2.2_算法的时间复杂度
- 一般情况下我们只看一个算法的最坏时间复杂度和平均时间复杂度
- 如何计算 :找到一个基本操作(最深层循环)
1.2.3_算法的空间复杂度
- 无论问题规模怎么变,算法运行所需的内存空间都是固定的常量 O ( 1 ) O(1) O(1),算法原地工作
- 开一个与问题规模n有关的一维数组
函数递归调用带来的内存开销
- 空间复杂度 与 递归调用的深度 有关
第二章、线性表
2.1_线性表的定义和基本操作
- 线性表是具有相同数据类型(每个数据元素所占空间一样大)的n个数据元素的有限 序列(序 :有次序)。
- 注意 :数据元素的位序从1开始,数组下标从0开始。用数组实现线性表时需要审题。
线性表的基本操作
- 参数的引用“&”是C++中的,可以把对参数的修改结果“带回来”
2.2.1_顺序表的定义
- 顺序表 :用顺序存储(逻辑上相邻的元素,物理上也相邻)的方式实现线性表
顺序表的实现-静态分配
#define MaxSize 10 // 定义最大长度
typedef struct{
ElemType data[MaxSize]; // 用静态的“数组”存放数据元素
int length; // 顺序表的当前长度
}SqList;
#include <stdio.h>
#define MaxSize 10
typedef struct{
int data[MaxSize];
int length;
}SqList;
void InitList(SqList &L)
{
// 可以省略,但可能由于遍历时用到MaxSize有脏数据,要用length遍历
for (int i = 0; i < MaxSize; i ++ )
L.data[i] = 0;
L.length = 0; // 不可省略,顺序表初始长度为0
}
int main()
{
SqList L; // 声明一个顺序表
InitList(L); // 初始化顺序表
return 0;
}
顺序表的实现-动态分配
- 这样就可以让顺序表的容量可变
- 虽然动态分配可以使顺序表的大小可以灵活改变,但是时间开销还是比较大的(复制元素)
- 注意malloc和free是一对函数
- free函数会把p这个指针所指向的这一整片的存储空间给释放掉,归还给系统,然后由于p是局部于这个函数的变量,函数结束后,存储p这个变量的存储空间会被系统自动回收
#define InitSize 10 // 顺序表的初始长度
typedef struct
{
ElemType *data; // 指示动态分配数组的指针,这个指针指向顺序表中第一个数据元素
int MaxSize; // 顺序表的最大容量
int length; // 顺序表的当前长度
} SeqList; // 顺序表的类型定义(动态分配方式)
// 动态申请和释放空间
// 在C语言中的函数分别是malloc和free函数
// malloc函数是申请一整片连续的内存空间,且会return一个指向这一整片存储空间开始地址的指针,需要强制转型为你定义的数据元素类型的指针
// L.data = (ElemType *)malloc(sizeof(ElemType) * InitSize);
// malloc和free包含在<stdlib.h>头文件中
// 在C++语言中分别是new和delete这两个关键字
#include <stdlib.h>
#define InitSize 10
typedef struct
{
int *data;
int MaxSize;
int length;
} SeqList;
void InitList(SeqList &L)
{
L.data = (int *)malloc(sizeof(int) * InitSize);
L.MaxSize = InitSize;
L.length = 0;
}
void IncreaseSize(SeqList &L, int len)
{
int *p = L.data;
L.data = (int *)malloc(sizeof(int) * (L.MaxSize + len));
for (int i = 0; i < L.length; i ++ )
L.data[i] = p[i];
L.MaxSize = L.MaxSize + len;
free(p);
}
int main()
{
SeqList L;
InitList(L);
// ...往顺序表中随意随便插入几个元素
IncreaseSize(L, 5);
return 0;
}
顺序表的特点
- 随机访问,即可以在 O ( 1 ) O(1) O(1)时间内找到第i个元素(不论是静态分配还是动态分配代码都是 d a t a [ i − 1 ] data[i - 1] data[i−1]
- 存储密度高,每个节点只存储数据元素(链表还要存指针)
- 拓展容量不方便(即便采用动态分配的方式实现,拓展长度的时间复杂度也比较高)
- 插入、删除操作不方便,需要移动大量元素
2.2.2.1_顺序表的插入删除
顺序表的插入
- L i s t I n s e r t ListInsert ListInsert(&L, i, e) :插入操作,在表L中的第i个位置(位序)插入指定元素i
- 本节代码建立在顺序表的“静态分配”实现方式之上,“动态分配”也雷同。
- 时间复杂度的平均情况 : p = 1 / ( n + 1 ) p=1/(n+1) p=1/(n+1);i=1,循环n次,i=2,循环n-1次…;平均循环次数 = n p + ( n − 1 ) ∗ p + . . . + 1. p = n ∗ ( n + 1 ) / 2 ∗ 1 / ( n + 1 ) = n / 2 =np+(n-1)*p+...+1.p=n*(n+1)/2*1/(n+1)=n/2 =np+(n−1)∗p+...+1.p=n∗(n+1)/2∗1/(n+1)=n/2
#define MaxSize 10
typedef struct
{
int data[MaxSize];
int length;
} SqList;
bool ListInsert(SqList &L, int i, int e)
{
if (i < 1 || i > L.length + 1) // 判断i的范围是否有效
return false;
if (L.length == MaxSize) // 当前存储空间已满,不能插入
return false;
for (int j = L.length; j >= i; j -- ) // 将第i个元素及之后的元素后移
L.data[j] = L.data[j - 1];
L.data[i - 1] = e; // 在位置i处放e
L.length ++ ; // 长度加1
return true; // 反馈
}
int main()
{
SqList L;
InitList(L);
// ...插入一些元素
ListInsert(L, 5, 5);
return 0;
}
顺序表的删除
- L i s t D e l e t e ListDelete ListDelete(&L, i, &e) :删除操作,删除表L中第i个位置的元素,并用e返回删除元素的值
- 因为要返回e,所以这要有一个引用操作,因此,在这个函数中操作的变量e,在内存中其实对应的是同一份数据
- 在删除操作中是先移动前面的元素再移动后面的元素,而在插入操作中要把元素往后移时,先把后面的元素往后移,然后再移前面的元素
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 = i; j < L.length; j ++ ) // 将第i个位置后的元素前移
L.data[j - 1] = L.data[j];
L.length -- ; // 线性表长度减一
return true;
}
int main()
{
SqList L;
InitList(L);
// ...插入一些元素
int e = -1; // 用变量e把删除的元素“带回来”
if (ListDelete(L, 3, e))
printf("已删除第3个元素,删除元素值为=%d\n", e);
else
printf("位序i不合法,删除失败");
}
2.2.2.2_顺序表的查找
顺序表的按位查找
- G e t E l e m GetElem GetElem(L, i) :按位查找操作。获取表L中第i个位置的元素的值
- O ( 1 ) O(1) O(1)
// 静态分配实现顺序表,动态分配实现的顺序表也是如此
ElemType GetElem(SqList L, int i)
{
// 判断i合法性
return L.data[i - 1];
}
顺序表的按值查找
- L o c a t e E l e m LocateElem LocateElem(L, e) :按值查找操作,在表L中查找具有给定关键字值的元素
#define InitSize 10
typedef struct
{
ElemType *data;
int MaxSize;
int length;
} SeqList;
ElemType LocateElem(SeqList L, ElemType e)
{
for (int i = 0; i < L.length; i ++ )
if (L.data[i] == e)
return i + 1; // 返回位序
return 0;
}
结构类型的比较
- 在C语言中,结构类型的比较不能直接用“==”,需要依次对比各个分量来判断两个结构体是否相等;如果C++,则可以用重载 “= =“。
- 但是,《数据结构》考研初试中,手写代码可以直接用“= =”,无论ElemType是基本数据类型还是结构类型。但是有的学校考《C语言程序设计》,那么也许语言就要严格一些。最好还是看一下相关的历年真题。
2.3.1_单链表的定义
用代码定义一个单链表
struct LNode // 定义单链表节点类型
{
ElemType data; // 每个节点存放一个数据元素
struct LNode *next; // 指针指向下一个节点
};
struct LNode *p = (struct LNode *)malloc(sizeof(struct LNode)); // 增加一个新的节点 :在内存中申请一个节点需要的空间,并用指针p指向这个节点
- 这样写每次都要有 s t r u c t struct struct有些麻烦,所以教材中使用了 t y p e d e f typedef typedef关键字(C语言),可以把数据类型重命名
- t y p e d e f < 数 据 类 型 > < 别 名 > typedef<数据类型><别名> typedef<数据类型><别名>
- 这样就可以变成 :
struct LNode // 定义单链表节点类型
{
ElemType data; // 每个节点存放一个数据元素
struct LNode *next; // 指针指向下一个节点
};
typedef struct LNode LNode;
LNode *p = (LNode *)malloc(sizeof(LNode));
- 教材中还有一种更简洁的方式
typedef struct LNode // 定义单链表节点类型
{
ElemType data;
struct LNode *next;
} LNode, *LinkList;
// LinkList - 单链表
// 上面这种写法等价于 :
struct LNode
{
ElemType data;
struct LNode *next;
}
typedef struct LNode LNode;
typedef struct LNode *LinkList;
- 要表示一个单链表时,只需声明一个头指针L,指向单链表的第一个结点
// 声明一个指向单链表第一个结点的指针
LNode *L;
// 等价于
LinkList L; // 代码可读性更强,详见下面例子中,
// GetElem函数的LNode *和LinkList虽然两者时等价的
//但在这个函数中它最终要返回的是第i个结点,所以把返回值的类型定义为LNode *,
// 其实它LNode *就是想强调返回的是一个结点,而LinkList想强调这是一个单链表
typedef struct LNode
{
ElemType data;
struct LNode *next;
}LNode, *LinkList;
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 != NULL && j < i)
{
p = p -> next;
j ++ ;
}
return p;
}
- 强调这是一个单链表 - 使用LinkList
- 强调这是一个结点 - 使用LNode *
不带头结点的单链表
typedef struct LNode
{
ElemType data;
struct LNode *next;
}LNode, *LinkList;
bool InitList(LinkList &L) // 注意传入引用
{
L = NULL; // 空表,暂时还没有任何结点 防止脏数据!!!
return true;
}
void test()
{
LinkList L; // 声明一个指向单链表的指针 注意,此处并没有创建一个结点!!!
// 初始化一个空表
InitList(L);
}
// (不带头结点)
bool Empty(LinkList L)
{
if (L == NULL)
return true;
else
return false;
}
// 或者
// (不带头结点)
bool Empty(LinkList L)
{
return (L == NULL);
}
带头结点的单链表
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;
}
void test()
{
LinkList L; // 声明一个指向单链表的指针
InitList(L); // 初始化一个空表
}
// 判断单链表是否为空(带头节点)
bool Empty(LinkList L)
{
if (L -> next == NULL)
return true;
else
return false;
}
- 如果不带头结点 :头指针所指向的下一个结点就是实际用于存放数据的结点;而如果带头结点的话 :头指针所指向的这个结点把它称为头结点,这个头结点是不存放实际数据元素的,只有这个头结点之后的下一个结点才用于存放数据
2.3.2.1_单链表的插入删除
按位序插入(带头结点)
- L i s t I n s e r t ListInsert ListInsert(&L, i, e) :插入操作,在表L中的第i个位置上插入指定元素e
- i是位序
- 找到第i-1个结点,将新结点插入其后
- 头结点可以看成第0个结点,所以当i=1时,也适用分析逻辑
// 在第i个位置插入元素e(带头结点)
bool ListInsert(LinkList &L, int i, ElemType e)
{
if (i < 1)
return false;
LNode *p = L; // 指针p指向当前扫描到的结点
int j = 0; // 当前p指向的是第几个结点
while (p != NULL && j < i - 1) // 循环找到第i-1个结点
{
j ++ ;
p = p -> next;
}
if (p == NULL) // i值不合法
return false;
LNode *s = (LNode *)malloc(sizeof(LNode));
s -> data = e;
s -> next = p -> next;
p -> next = s;
return true;
}
按位序插入(不带头结点)
- L i s t I n s e r t ListInsert ListInsert(&L, i, e) :插入操作,在表L中的第i个位置上插入指定元素e
- 找到第i-1个结点,将新结点插入其后
- 不存在“第0个”结点,因此i=1时需要特殊处理
- 不带头结点,则插入、删除第1个元素时,需要更改头指针L
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 = L;
int j = 1; // 注意这里是1!!!不带头结点
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;
}
- 不带头结点写代码更不方便,因此之后代码默认使用带头结点,但考试中两种情况都可能考察
指定结点的后插操作
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;
}
指定结点的前插操作
// 前插操作 :在p结点之前插入元素e
// O(1)
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;
}
// 王道书版本
bool InsertPriorNode(LNode *p, LNode *s)
{
if (p == NULL || s == NULL)
return false;
s -> next = p -> next;
p -> next = s;
ElemType temp = p -> data; // 交换数据域部分
p -> data = s -> data;
s -> data = temp;
return true;
}
按位序删除(带头结点)
- L i s t D e l e t e ListDelete 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 = L;
int j = 0;
while (p != NULL && j < i - 1)
{
p = p -> next;
j ++ ;
}
if (p == NULL)
return false;
if (p -> next == NULL) // 第i-1个结点之后已无结点
return false;
LNode *q = p -> next;
e = q -> data;
p -> next = q -> next;
free(q);
return true;
}
指定结点的删除
- 删除结点p,需要修改其前趋结点的next指针
- 方法1 :传入头指针,循环寻找p的前趋结点
- 方法2 :偷天换日(类似于结点前插的实现)
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;
}
- 但是!以上有BUG!如果p是最后一个结点,只能从表头开始依次寻找p的前趋,时间复杂度O(N),否则 p − > n e x t − > d a t a p -> next -> data p−>next−>data会出错;不过可能大概只扣一分
2.3.2.2_单链表的查找
- 本节只探讨“带头结点”的情况
按位查找
- G e t E l e m GetElem GetElem(L, i)
LNode * GetElem(LinkList L, int i)
{
if (i < 0)
return false;
LNode *p = L;
int j = 0;
while (p != NULL && j < i)
{
p = p -> next;
j ++ ;
}
return p;
}
- 如果i=0,头结点
- 如果i大于长度,返回NULL
按值查找
LNode * LocateElem(LinkList L, ElemType e)
{
LNode *p = L -> next;
while (p != NULL && p -> data != e)
p = p -> next;
return p;
}
求表的长度
int Length(LinkList L)
{
LNode *p = L;
int len = 0;
while (p != NULL)
{
p = p -> next;
len ++ ;
}
return len;
}
2.3.2.3_单链表的建立
尾插法建立单链表
- 设置变量length记录链表长度,用ListInsert,O(n^2);设置一个表尾指针,只要每次对r指针进行后插操作InsertNextNode,然后把表尾指针往后移
// O(n)
LinkList List_TailInsert(LinkList &L)
{
int x;
L = (LinkList)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;
}
头插法建立单链表
- 对头结点的后插操作
2.3.3_双链表
双链表的初始化(带头结点)
typedef struct DNode
{
ElemType data;
struct DNode *next, *prior;
}DNode, *DLinkList;
// 初始化双链表
bool InitDLinkList(DLinkList &L)
{
L = (DLinkList)malloc(sizeof(DNode));
if (L == NULL)
return false;
L -> next = NULL; // 头结点之后暂时还没有结点
L -> prior = NULL; // 头结点的prior指针永远指向NULL
return true;
}
void testDLinkList()
{
DLinkList L;
InitDLinkList(L);
}
bool Empty(DLinkList L)
{
if (L -> next == NULL)
return true;
else
return false;
}
双链表的插入
// 在p结点之后插入s结点(后插)
bool InsertNextNode(DNode *p, DNode *s)
{
if (p == NULL || s == NULL)
return false;
s -> next = p -> next;
if (p -> next != NULL) // 如果p结点有后继结点
p -> next -> prior = s;
s -> prior = p;
p -> next = s;
return p;
}
- 以上是前插法,但由于是双链表,如果要前插,只要找到前一个用后插就可以
双链表的删除
// 删除p结点的后继结点
bool DeleteNextNode(DNode *p)
{
if (p == NULL)
return false;
DNode *q = p -> next;
if (q == NULL)
return false;
p -> next = q -> next;
if (q -> next != NULL)
q -> next -> prior = p;
free(q);
return true;
}
void DestroyList(DLinkList &L)
{
while (L -> next != NULL)
DeleteNextDNode(L);
free(L); // 释放头结点
L = NULL; // 头结点指向NULL
}
双链表的遍历
2.3.4_循环链表
循环双链表
typedef struct LNode // 定义单链表结点类型
{
ElemType data;
struct LNode *next;
}LNode, *LinkList;
// 初始化一个循环单链表
bool InitList(LinkList &L)
{
L = (LinkList)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
return false;
}
// 判断结点p是否为循环单链表的表尾结点
bool isTail(LinkList L, LNode *p)
{
if (p -> next == L)
return true;
else
return false;
}
- 单链表:从一个结点出发只能找到后续的结点
- 循环单链表:从一个结点出发可以找到其它任何一个结点
循环双链表
- 双链表:表头结点的prior指向NULL;表尾结点的next指向NULL
- 循环双链表:表头结点的prior指向表尾结点;表尾结点的next指向表头结点
循环双链表的初始化
typedef struct DNode
{
ElemType data;
struct DNode *prior, *next;
}DNode, *DLinkList;
bool InitDLinkList(DLinkList &L)
{
L = (LinkList)malloc(sizeof(LNode));
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;
}
bool isTail(LinkList L, DNode *p)
{
if (p -> next == L)
return true;
else
return false;
}
循环双链表的插入
- 其实是双链表插入的缩减版
bool InsertNextDNode(LNode *p, LNode *s)
{
s -> next = p -> next;
p -> next -> prior = s;
s -> prior = p;
p -> next = s;
}
循环双链表的删除
- 同上,
p -> next = q -> next;
q -> next -> prior = p;
free(q);
2.3.5_静态链表
- 单链表:各个结点在内存中星罗棋布,散落天涯
- 静态链表:分配一整片连续的空间,各个结点集中安置
- 静态链表就是用数组的方式实现的链表
- 优点:增、删操作不需要大量移动元素
- 缺点:不能随机存取,只能从头结点开始依次往后查找;容量固定不可变
- 适用场景:不支持指针的低级语言;数据元素固定不变的场景(如操作系统的文件分配表FAT)
用代码定义一个静态链表
#define MaxSize 10;
struct Node
{
ElemType data;
int next;
};
void testSLinkList()
{
struct Node a[MaxSize]; // 数组a作为静态链表
}
简述基本操作的实现
- 初始化静态链表:把a[0]的next设为-1;把其它结点的next设为一个特殊值用来表示结点空闲,如-2
- 查找:从头结点出发挨个往后遍历结点 O(N)
- 插入位序为i的结点:1.找到一个空的结点,存入数据元素;2.从头结点出发找到位序为i-1的结点;3.修改新结点的next;4.修改i-1号结点的next
- 插入位序为i的结点时第一步找到一个空的结点有一个问题,那就是可能出现脏数据,所以在初始化的时候,可以让next为某个特殊的值,比如-2
- 删除结点:1.从头结点出发找到前趋结点;2.修改前趋结点的游标;3.被删除结点next设为-2
2.3.6_顺序表vs链表