PS:本文章部分内容参考自王道考研数据结构笔记
数据:是对客观事物的符号表示,在计算机科学中是指所有能输入到计算机中并被计算机程序处理的符号的总称。
数据元素:数据的基本单位,一个数据元素可由若干数据项组成。
数据项:数据的不可分割的最小单位。
数据对象:性质相同的数据元素的集合,是数据的一个子集。
数据结构:指互相之间存在着一种或多种特定关系的数据元素的集合,包括逻辑结构,存储结构和对数据的运算。(数据元素都不是孤立存在的)。
抽象数据类型(ADT):指一个数学模型以及定义在该模型上的一组操作,只取决于它的一组逻辑特性,用一个三元组表示(D, S, P)。
数据类型:是程序设计语言中的一个概念,它是一个值的集合和操作的集合。
逻辑结构:是指数据之间关系的描述,与数据的存储结构无关。分为线性结构和非线性结构,通常分为四类结构:
存储结构:是指数据结构在计算机中的表示,又称为数据的物理结构。它包括数据元素的表示和关系的表示,通常由四种基本的存储方法实现:
算法和程序十分相似,但又有区别。程序不一定具有有穷性,程序中的指令必须是机器可执行的,而算法中的指令则无此限制。算法代表了对问题的解,而程序则是算法在计算机上的特定的实现。一个算法若用程序设计语言来描述,则它就是一个程序。
如何计算:
常用技巧:
加法规则: O ( f ( n ) ) + O ( g ( n ) ) = O ( m a x ( f ( n ) , g ( n ) ) ) O(f(n))+O(g(n))=O(max(f(n), g(n))) O(f(n))+O(g(n))=O(max(f(n),g(n)))
乘法规则: O ( f ( n ) ) × O ( g ( n ) ) = O ( f ( n ) × g ( n ) ) O(f(n))×O(g(n))=O(f(n)×g(n)) O(f(n))×O(g(n))=O(f(n)×g(n))
“常对幂指阶”
O ( 1 ) < O ( l o g 2 n ) < O ( n ) < O ( n l o g 2 n ) < O ( n 2 ) < O ( n 3 ) < O ( 2 n ) < O ( n ! ) < O ( n n ) O(1)
三种复杂度:
算法的性能问题只有在 n 很大时才会暴露出来
普通程序:
递归程序:
线性表:是具有相同数据类型的 n 个数据元素的有限序列。
特点:
存在惟一的第一个元素。
存在惟一的最后一个元素。
除第一个元素之外,每个元素均只有一个直接前驱。
除最后一个元素之外,每个元素均只有一个直接后继。
线性表的存储结构:
InitList(&L)
:初始化表。构造一个空的线性表 L,并分配内存空间。
DestroyList(&L)
:销毁表。并释放线性表 L 占用的内存空间。
ListInsert(&L, i, &e)
:插入操作。在表 L 的第 i 个位置插入指定元素 e 。
ListDelete(&L, i, &e)
:删除操作。删除表 L 中第 i 个位置的元素,并用 e 返回删除元素的值。
LocateElem(L, e)
:按值查找。在表 L 中查找具有给定元素值的元素。
GetElem(L, i)
:按位查找。获取表 L 中第 i 个位置的元素的值。
Length(L)
:求表长。返回线性表 L 的长度,即表中元素的个数。
PrintList(L)
:打印表。按顺序输出线性表 L 的所有元素值。
Empty(L)
:判断是否为空。若 线性表L 为空表,则返回 true,否则返回 false。
操作数据结构的思路:创销、增删改查
顺序表:用顺序存储的方式实现线性表。顺序存储,将逻辑上相邻的元素存储在相邻的物理位置上。
特点:
静态实现:
#define MaxSize 10 // 定义最大长度
typedef struct {
int data[MaxSize]; // 使用静态的数组存放数据元素
int length; // 顺序表的当前长度
}SqList;
// 初始化顺序表
void InitList(SqList &L) {
L.length = 0; // 顺序表初始长度为0
}
int main() {
SqList L; // 声明一个顺序表
InitList(L); // 初始化顺序表
return 0;
}
动态实现:
#define InitSize 10 // 顺序表的初始长度
typedef struct {
int *data; // 声明动态分配数组的指针
int MaxSize; // 顺序表的最大容量
int length; // 顺序表的当前长度
}SeqList;
// 初始化顺序表
void InitList(SqList &L) {
// 用malloc函数申请一片连续的存储空间
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 = L.MaxSize + len; // 顺序表最大长度增加len
free(p); // 释放原来的内存空间
}
int main() {
SeqList L; // 声明一个顺序表
InitList(L); // 初始化顺序表
...
IncreaseSize(L, 5);
return 0;
}
malloc()
函数的作用:会申请一片存储空间,并返回存储空间第一个位置的地址,也就是该位置的指针。
插入:
#define MaxSize 10 // 定义最大长度
typedef struct {
int data[MaxSize]; // 用静态的数组存放数据元素
int length; // 顺序表的当前长度
}SqList;
// 在顺序表i位置插入e
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, 3, 3);
return 0;
}
时间复杂度:
删除:
#define MaxSize 10
typedef struct {
int data[MaxSize];
int length;
} SqList;
// 删除顺序表i位置的数据并存入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;
if (ListDelete(L, 3, e))
printf("已删除第3个元素,删除元素值为%d\n", e);
else
printf("位序i不合法,删除失败\n");
return 0;
}
时间复杂度:
按位查找:
// 静态分配的按位查找
#define MaxSize 10
typedef struct {
ElemType data[MaxSize];
int length;
}SqList;
ElemType GetElem(SqList L, int i) {
return L.data[i-1];
}
// 动态分配的按位查找
#define InitSize 10
typedef struct {
ElemType *data;
int MaxSize;
int length;
}SeqList;
ElemType GetElem(SeqList L, int i) {
return L.data[i-1];
}
时间复杂度: O ( 1 ) O(1) O(1)
按值查找:
#define InitSize 10
typedef struct {
ElemType *data;
int MaxSize;
int length;
}SqList;
// 查找第一个元素值为e的元素,并返回其位序
int LocateElem(SqList L, ElemType e) {
for (int i = 0; i < L.length; i++)
if (L.data[i] == e)
return i+1; // 数组下标为i的元素值等于e,返回其位序i+1
return 0; // 没有查找到
}
在《数据结构》考研初试中,手写代码可以直接用“==”,无论 ElemType 是基本数据类型还是结构类型
时间复杂度:
不带头结点的单链表:
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){
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;
}
按位序插入(带头结点):
typedef struct LNode{
ElemType data;
struct LNode *next;
}LNode, *LinkList;
//在第i个位置插入元素e
bool ListInsert(LinkList &L, int i, ElemType e){
if(i<1)
return False;
LNode *p; //指针p指向当前扫描到的结点
int j=0; //当前p指向的是第几个结点
p = L; //循环找到第i-1个结点
while(p!=NULL && j<i-1){ //如果i>lengh,p最后会等于NULL
p = p->next;
j++;
}
//p值为NULL说明i值不合法
if (p==NULL)
return false;
//在第i-1个结点后插入新结点
LNode *s = (LNode *)malloc(sizeof(LNode));
s->data = e;
s->next = p->next;
p->next = s;
//将结点s连到p后
return true;
}
时间复杂度:
按位序插入(不带头结点):
typedef struct LNode{
ElemType data;
struct LNode *next;
}LNode, *LinkList;
//在第i个位置插入元素e
bool ListInsert(LinkList &L, int i, ElemType e){
//判断i的合法性
if(i<1)
return false;
//需要判断插入的位置是否是第1个
if(i==1){
LNode *s = (LNode *)malloc(size of(LNode));
s->data =e;
s->next =L;
L=s; //头指针指向新结点
return true;
}
//i>1的情况与带头结点一样,唯一区别是j的初始值为1
LNode *p; //指针p指向当前扫描到的结点
int j=1; //当前p指向的是第几个结点
p = L;
//循环找到第i-1个结点
while(p!=NULL && j<i-1){ //如果i>lengh,p最后会等于NULL
p = p->next;
j++;
}
//p值为NULL说明i值不合法
if (p==NULL)
return false;
//在第i-1个结点后插入新结点
LNode *s = (LNode *)malloc(sizeof(LNode));
s->data = e;
s->next = p->next;
p->next = s;
return true;
}
时间复杂度:
除非特别声明,否则之后的代码都默认为带头结点!
指定结点的后插操作:
typedef struct LNode{
ElemType data;
struct LNode *next;
}LNode, *LinkList;
// 在结点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;
}
// 按位序插入的函数中可以直接调用后插操作
bool ListInsert(LinkList &L, int i, ElemType e){
if(i<1)
return False;
LNode *p;
//指针p指向当前扫描到的结点
int j=0;
//当前p指向的是第几个结点
p = L;
//循环找到第i-1个结点
while(p!=NULL && j<i-1){
//如果i>lengh, p最后会等于NULL
p = p->next;
j++;
}
return InsertNextNode(p, e)
}
时间复杂度: O ( 1 ) O(1) O(1)
指定结点的前插操作:
如果传入头指针,就可以循环整个链表找到指定结点p的前驱结点q,再对q进行后插操作;
如果不传入头指针,可以在指定结点p后插入一个结点s,并交换两个结点所保存的数据,从而变相实现指定结点的前插操作。
typedef struct LNode{
ElemType data;
struct LNode *next;
}LNode, *LinkList;
// 在结点p前插入元素e
bool InsertPriorNode(LNode *p, ElemType e){
if(p==NULL)
return false;
LNode *s = (LNode *)malloc(sizeof(LNode));
// 内存不足分配失败
if(s==NULL)
return false;
// 将s插入结点p之后
s->next = p->next;
p->next = s;
// 交换两个结点中的数据
s->data = p->data;
p->data = e;
return true;
}
时间复杂度: O ( 1 ) O(1) O(1)
按位序删除:
typedef struct LNode{
ElemType data;
struct LNode *next;}LNode, *LinkList;
// 删除第i个结点并将其所保存的数据存入e
bool ListDelete(LinkList &L, int i, ElemType &e){
if(i<1)
return false;
LNode *p; //指针p指向当前扫描到的结点
int j=0; //当前p指向的是第几个结点
p = L;
//循环找到第i-1个结点
while(p!=NULL && j<i-1){
//如果i>lengh,p和p的后继结点会等于NULL
p = p->next;
j++;
}
if(p==NULL)
return false;
if(p->next == NULL)
return false;
//令q暂时保存被删除的结点
LNode *q = p->next;
e = q->data;
p->next = q->next;
free(q)
return true;
}
时间复杂度:
删除指定结点:
如果传入头指针,就可以循环整个链表找到指定结点p的前驱结点q,再对p进行删除操作;
如果不传入头指针,可以把指定结点p的后继结点q删除,并使结点p保存结点q存储的数据,从而变相实现删除指定结点的操作。但是如果指定结点p没有后继结点,这么做会报错。
// 删除指定结点p
bool DeleteNode(LNode *p){
if(p==NULL)
return false;
LNode *q = p->next; // 令q指向p的后继结点
// 如果p是最后一个结点,则q指向NULL,继续执行就会报错
p->data = q->data;
p->next = q->next;
free(q);
return true;
}
时间复杂度: O ( 1 ) O(1) O(1)
按位查找:
typedef struct LNode{
ElemType data;
struct LNode *next;
}LNode, *LinkList;
// 查找指定位序i的结点并返回
LNode * GetElem(LinkList L, int i){
if(i<0)
return NULL;
LNode *p;
int j=0;
p = L;
while(p!=NULL && j<i){
p = p->next;
j++;
}
return p;
}
// 封装后的插入操作,在第i个位置插入元素e,可以调用查询操作和后插操作
bool ListInsert(LinkList &L, int i, ElemType e){
if(i<1)
return False;
// 找到第i-1个元素
LNode *p = GetElem(L, i-1);
// 在p结点后插入元素e
return InsertNextNode(p, e)
}
时间复杂度:
按值查找:
// 查找数据域为e的结点指针,否则返回NULL
LNode * LocateElem(LinkList L, ElemType e){
LNode *P = L->next;
// 从第一个结点开始查找数据域为e的结点
while(p!=NULL && p->data != e){
p = p->next;
}
return p;
}
时间复杂度:
计算单链表长度:
// 计算单链表的长度
int Length(LinkList L){
int len=0; //统计表长
LNode *p = L;
while(p->next != NULL){
p = p->next;
len++;
}
return len;
}
时间复杂度: O ( n ) O(n) O(n)
尾插法建立单链表:
// 使用尾插法建立单链表L
LinkList List_TailInsert(LinkList &L){
int x; //设ElemType为整型int
L = (LinkList)malloc(sizeof(LNode)); //建立头结点(初始化空表)
LNode *s, *r = L; //r为表尾指针
scanf("%d", &x); //输入要插入的结点的值
while(x!=9999){ //输入9999表示结束
s = (LNode *)malloc(sizeof(LNode));
s->data = x;
r->next = s;
r = s; //r指针指向新的表尾结点
scanf("%d", &x);
}
r->next = NULL; //尾结点指针置空
return L;
}
时间复杂度: O ( n ) O(n) O(n)
头插法建立单链表:
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;
//将新结点插入表中,L为头指针
scanf("%d", &x);
}
return L;
}
头插法实现链表的逆置:
// 将链表L中的数据逆置并返回
LNode *Inverse(LNode *L){
LNode *p, *q;
p = L->next; //p指针指向第一个结点
L->next = NULL; //头结点置空
// 依次判断p结点中的数据并采用头插法插到L链表中
while (p != NULL){
q = p;
p = p->next;
q->next = L->next;
L->next = q;
}
return L;
}
具体解释详见【数据结构】单链表逆置:头插法图解
**双链表的定义:**双链表也是链表的一种。双链表的每个数据节点中都有两个指针,分别指向前驱节点和后继结点。
双链表的实现:
typedef struct DNode{ //定义双链表结点类型
ElemType data; //数据域
struct DNode *prior, *next; //前驱和后继指针
}DNode, *DLinklist;
双链表的初始化 (带头结点):
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->prior = NULL; //头结点的prior指针永远指向NULL
L->next = NULL; //头结点之后暂时还没有结点,置空
return true;
}
void testDLinkList(){
DLinklist L;
InitDLinkList(L);
...
}
// 判断双链表是否为空
bool Empty(DLinklist L){
if(L->next == NULL)
return true;
else
return false;
}
双链表的后插操作:
typedef struct DNode{
ElemType data;
struct DNode *prior, *next;
}DNode, *DLinklist;
// 将结点s插入到结点p之后
bool InsertNextDNode(DNode *p, DNode *s){
if(p==NULL || s==NULL)
return false;
s->next = p->next;
// 判断结点p之后是否有后继结点
if (p->next != NULL)
p->next->prior = s;
s->prior = p;
p->next = s;
return true;
}
双链表的前插操作、按位序插入操作都可以转换成后插操作
双链表的删除操作:
// 删除p结点的后继结点
bool DeletNextDNode(DNode *p){
if(p==NULL)
return false;
// 找到p的后继结点q
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;
}
// 销毁一个双链表
bool DestoryList(DLinklist &L){
// 循环释放各个数据结点
while(L->next != NULL){
DeletNextDNode(L);
free(L);
// 头指针置空
L=NULL;
}
}
双链表的遍历:
// 向后遍历
while(p!=NULL){
// 对结点p做相应处理
p = p->next;
}
// 向前遍历
while(p!=NULL){
// 对结点p做相应处理
p = p->prior;
}
// 跳过头结点的遍历
while(p->prior!=NULL){
//对结点p做相应处理
p = p->prior;
}
双链表不可随机存取,按位查找、按值查找操作都只能用遍历的方式实现。
**循环链表的定义:**循环链表是另一种形式的链式存储结构。它的特点是表中最后一个结点的指针域指向头结点,整个链表形成一个环。
循环单链表的实现:
typedef struct LNode{
ElemType data;
struct LNode *next;
}DNode, *Linklist;
// 初始化循环单链表
bool InitList(LinkList &L){
L = (LNode *)malloc(sizeof(LNode));
if(L==NULL)
return false;
// 最后一个结点的next指针指向头结点
L->next = L;
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;
}
循环双链表的实现:
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;
// 头结点的prior指针指向最后一个结点,最后一个结点的next指针指向头结点
L->prior = L;
L->next = L;
}
// 判断循环双链表是否为空
bool Empty(DLinklist L){
if(L->next == L)
return true;
else
return false;
}
// 判断结点p是否为循环双链表的表尾结点
bool isTail(DLinklist L, DNode *p){
if(p->next == L)
return true;
else
return false;
}
循环双链表的插入和删除操作:
// 将结点s插入到结点p之后
bool InsertNextDNode(DNode *p, DNode *s){
s->next = p->next;
//循环双链表不用担心p结点的下一个结点为空
p->next->prior = s;
s->prior = p;
p->next = s;
}
// 删除p结点的后继结点
bool DeletNextDNode(DNode *p){
// 找到p的后继结点q
DNode *q =p->next;
//循环双链表不用担心q结点的下一个结点为空
p->next = q->next;
q->next->prior=p;
free(q);
return true;
}
特点:
静态链表的定义:
#define MaxSize 10 //静态链表的最大长度
struct Node{ //静态链表结构类型的定义
ElemType data; //存储数据元素
int next; //下一个元素的数组下标
};
// 用数组定义多个连续存放的结点
void testSLinkList(){
struct Node a[MaxSize]; //数组a作为静态链表, 每一个数组元素的类型都是struct Node
...
}
也可以这么定义:
#define MaxSize 10 //静态链表的最大长度
typedef struct{ //静态链表结构类型的定义
ELemType data; //存储数据元素
int next; //下一个元素的数组下标
}SLinkList[MaxSize];
void testSLinkList(){
SLinkList a;
}
第一种是我们更加熟悉的写法,第二种写法则更加侧重于强调 a 是一个静态链表而非数组。
静态链表的注意点:
a[0]
的next
设为-1,并将空闲结点的next
设置为某个特殊值,比如-2。next
为 -1;④修改 i-1 号结点的next
为新结点的下标;逻辑结构:顺序表和链表都属于线性表,都是线性结构。
存储结构:
顺序表:顺序存储
链表:链式存储
基本操作 - 创建:
顺序表:需要预分配大片连续空间。若分配空间过小,则之后不方便拓展容量;若分配空间过大,则浪费内存资源。
malloc()
、free()
)。链表:只需要分配一个头结点或者只声明一个头指针。
基本操作 - 销毁:
顺序表:修改 Length
= 0
free()
。链表:依次删除各个结点 free()
。
基本操作 - 增/删:
基本操作 - 查找:
InitStack(&S)
:初始化栈。构造一个空栈 S,分配内存空间。DestroyStack(&S)
:销毁栈。销毁并释放栈 S 所占用的内存空间。Push(&S, x)
:进栈。若栈 S 未满,则将 x 加入使其成为新的栈顶元素。Pop(&S, &x)
:出栈。若栈 S 非空,则弹出(删除)栈顶元素,并用 x 返回。GetTop(S, &x)
:读取栈顶元素。若栈 S 非空,则用 x 返回栈顶元素。StackEmpty(S)
:判空。断一个栈 S 是否为空,若 S 为空,则返回 true,否则返回 false。顺序栈的定义:
#define MaxSize 10 //定义栈中元素的最大个数
typedef struct{
ElemType data[MaxSize]; //静态数组存放栈中元素
int top; //栈顶元素
}SqStack;
void testStack(){
SqStack S; //声明一个顺序栈(分配空间)
}
顺序栈的初始化:
#define MaxSize 10
typedef struct{
ElemType data[MaxSize];
int top;
}SqStack;
// 初始化栈
void InitStack(SqStack &S){
S.top = -1; //初始化栈顶指针
}
// 判断栈是否为空
bool StackEmpty(SqStack S){
if(S.top == -1)
return true;
else
return false;
}
入栈出栈:
// 新元素进栈
bool Push(SqStack &S, ElemType x){ // 判断栈是否已满
if(S.top == MaxSize - 1)
return false;
S.data[++S.top] = x;
return true;
}
// 出栈
bool Pop(SqStack &x, ElemType &x){ // 判断栈是否为空
if(S.top == -1)
return false;
x = S.data[S.top--];
return true;
}
读取栈顶元素:
// 读栈顶元素
bool GetTop(SqStack S, ElemType &x){
if(S.top == -1)
return false;
x = S.data[S.top];
return true;
}
共享栈(两个栈共享同一片空间):
#define MaxSize 10 //定义栈中元素的最大个数
typedef struct{
ElemType data[MaxSize]; //静态数组存放栈中元素
int top0; //0号栈栈顶指针
int top1; //1号栈栈顶指针
}ShStack;
// 初始化栈
void InitSqStack(ShStack &S){
S.top0 = -1;
S.top1 = MaxSize;
}
链栈的定义:
typedef struct Linknode{
ElemType data; //数据域
Linknode *next; //指针域
}Linknode,*LiStack;
void testStack(){
LiStack L; //声明一个链栈
}
链栈的初始化:
typedef struct Linknode{
ElemType data;
Linknode *next;
}Linknode,*LiStack;
// 初始化栈
bool InitStack(LiStack &L){
L = (Linknode *)malloc(sizeof(Linknode));
if(L == NULL)
return false;
L->next = NULL;
return true;
}
// 判断栈是否为空
bool isEmpty(LiStack &L){
if(L->next == NULL)
return true;
else
return false;
}
入栈出栈:
// 新元素入栈
bool pushStack(LiStack &L,ElemType x){
Linknode *s = (Linknode *)malloc(sizeof(Linknode));
if(s == NULL)
return false;
s->data = x;
// 头插法
s->next = L->next;
L->next = s;
return true;
}
// 出栈
bool popStack(LiStack &L, int &x){
// 栈空不能出栈
if(L->next == NULL)
return false;
Linknode *s = L->next;
x = s->data;
L->next = s->next;
free(s);
return true;
}
InitQueue(&Q)
:初始化队列。构造一个空队列 Q。DestroyQueue(&Q)
:销毁队列。销毁并释放队列 Q 所占用的内存空间。EnQueue(&Q, x)
:入队。若队列 Q 未满,将 x 加入,使之成为新的队尾。DeQueue(&Q, &x)
:出队。若队列 Q 非空,删除队头元素,并用 x 返回。GetHead(Q,&x)
:读队头元素。若队列 Q 非空,则将队头元素赋值给 x。QueueEmpty(Q)
:判空。若队列 Q 为空,则返回 true。顺序队列的定义:
#define MaxSize 10; //定义队列中元素的最大个数
typedef struct{
ElemType data[MaxSize]; //用静态数组存放队列元素
int front, rear; //队头指针和队尾指针
}SqQueue;
void test{
SqQueue Q; //声明一个队列
}
顺序队列的初始化:
#define MaxSize 10;
typedef struct{
ElemType data[MaxSize];
int front, rear;
}SqQueue;
// 初始化队列
void InitQueue(SqQueue &Q){
// 初始化时,队头、队尾指针指向0
// 队尾指针指向的是即将插入数据的数组下标
// 队头指针指向的是队头元素的数组下标
Q.rear = Q.front = 0;
}
// 判断队列是否为空
bool QueueEmpty(SqQueue Q){
if(Q.rear == Q.front)
return true;
else
return false;
}
入队出队(循环队列):
// 新元素入队
bool EnQueue(SqQueue &Q, ElemType x){
// 如果队列已满直接返回
if((Q.rear+1)%MaxSize == Q.front) //牺牲一个单元区分队空和队满
return false;
Q.data[Q.rear] = x;
Q.rear = (Q.rear+1)%MaxSize;
return true;
}
// 出队
bool DeQueue(SqQueue &Q, ElemType &x){
// 如果队列为空直接返回
if(Q.rear == Q.front)
return false;
x = Q.data[Q.front];
Q.front = (Q.front+1)%MaxSize;
return true;
}
获得队头元素:
// 获取队头元素并存入x
bool GetHead(SqQueue &Q, ElemType &x){
if(Q.rear == Q.front)
return false;
x = Q.data[Q.front];
return true;
}
注意:
循环队列不能使用Q.rear == Q.front
作为判空的条件,因为当队列已满时也符合该条件,会与判空发生冲突!
解决方法一:牺牲一个单元来区分队空和队满,即将(Q.rear+1)%MaxSize == Q.front
作为判断队列是否已满的条件。(主流方法)
解决方法二:设置 size 变量记录队列长度。
#define MaxSize 10;
typedef struct{
ElemType data[MaxSize];
int front, rear;
int size;
}SqQueue;
// 初始化队列
void InitQueue(SqQueue &Q){
Q.rear = Q.front = 0;
Q.size = 0;
}
// 判断队列是否为空
bool QueueEmpty(SqQueue 0){
if(Q.size == 0)
return true;
else
return false;
}
// 新元素入队
bool EnQueue(SqQueue &Q, ElemType x){
if(Q.size == MaxSize)
return false;
Q.size++;
Q.data[Q.rear] = x;
Q.rear = (Q.rear+1)%MaxSize;
return true;
}
// 出队
bool DeQueue(SqQueue &Q, ElemType &x){
if(Q.size == 0)
return false;
Q.size--;
x = Q.data[Q.front];
Q.front = (Q.front+1)%MaxSize;
return true;
}
解决方法三:设置 tag 变量记录队列最近的操作。(tag=0
:最近进行的是删除操作;tag=1
:最近进行的是插入操作)
#define MaxSize 10;
typedef struct{
ElemType data[MaxSize];
int front, rear;
int tag;
}SqQueue;
// 初始化队列
void InitQueue(SqQueue &Q){
Q.rear = Q.front = 0;
Q.tag = 0;
}
// 判断队列是否为空
bool QueueEmpty(SqQueue 0){
if(Q.front == Q.rear && Q.tag == 0)
return true;
else
return false;
}
// 新元素入队
bool EnQueue(SqQueue &Q, ElemType x){
if(Q.rear == Q.front && tag == 1)
return false;
Q.data[Q.rear] = x;
Q.rear = (Q.rear+1)%MaxSize;
Q.tag = 1;
return true;
}
// 出队
bool DeQueue(SqQueue &Q, ElemType &x){
if(Q.rear == Q.front && tag == 0)
return false;
x = Q.data[Q.front];
Q.front = (Q.front+1)%MaxSize;
Q.tag = 0;
return true;
}
链队列的定义:
// 链式队列结点
typedef struct LinkNode{
ElemType data;
struct LinkNode *next;
}
// 链式队列
typedef struct{
// 头指针和尾指针
LinkNode *front, *rear;
}LinkQueue;
链队列的初始化(带头结点):
typedef struct LinkNode{
ElemType data;
struct LinkNode *next;
}LinkNode;
typedef struct{
LinkNode *front, *rear;
}LinkQueue;
// 初始化队列
void InitQueue(LinkQueue &Q){
// 初始化时,front、rear都指向头结点
Q.front = Q.rear = (LinkNode *)malloc(sizeof(LinkNode));
Q.front -> next = NULL;
}
// 判断队列是否为空
bool IsEmpty(LinkQueue Q){
if(Q.front == Q.rear)
return true;
else
return false;
}
入队出队:
// 新元素入队
void EnQueue(LinkQueue &Q, ElemType x){
LinkNode *s = (LinkNode *)malloc(sizeof(LinkNode));
s->data = x;
s->next = NULL;
Q.rear->next = s;
Q.rear = s;
}
// 队头元素出队
bool DeQueue(LinkQueue &Q, ElemType &x){
if(Q.front == Q.rear)
return false;
LinkNode *p = Q.front->next;
x = p->data;
Q.front->next = p->next;
// 如果p是最后一个结点,则将队头指针也指向NULL
if(Q.rear == p)
Q.rear = Q.front;
free(p);
return true;
}
以上是带头结点的链队列,下面是不带头结点的操作:
typedef struct LinkNode{
ElemType data;
struct LinkNode *next;
}LinkNode;
typedef struct{
LinkNode *front, *rear;
}LinkQueue;
// 初始化队列
void InitQueue(LinkQueue &Q){
// 不带头结点的链队列初始化,头指针和尾指针都指向NULL
Q.front = NULL;
Q.rear = NULL;
}
// 判断队列是否为空
bool IsEmpty(LinkQueue Q){
if(Q.front == NULL)
return true;
else
return false;
}
// 新元素入队
void EnQueue(LinkQueue &Q, ElemType x){
LinkNode *s = (LinkNode *)malloc(sizeof(LinkNode));
s->data = x;
s->next = NULL;
// 第一个元素入队时需要特别处理
if(Q.front == NULL){
Q.front = s;
Q.rear = s;
}else{
Q.rear->next = s;
Q.rear = s;
}
}
//队头元素出队
bool DeQueue(LinkQueue &Q, ElemType &x){
if(Q.front == NULL)
return false;
LinkNode *s = Q.front;
x = s->data;
if(Q.front == Q.rear){
Q.front = Q.rear = NULL;
}else{
Q.front = Q.front->next;
}
free(s);
return true;
}
定义:
考点:判断输出序列的合法化
栈中合法的序列,双端队列中一定也合法ZS
栈 | 输入受限的双端队列 | 输出受限的双端队列 |
---|---|---|
14个合法 ( 1 n + 1 C 2 n n \frac{1}{n+1}C^{n}_{2n} n+11C2nn) | 只有 4213 和 4231 不合法 | 只有 4132 和 4231 不合法 |
#define MaxSize 10
typedef struct{
char data[MaxSize];
int top;
}SqStack;
void InitStack(SqStack &S);
bool StackEmpty(SqStack &S);
bool Push(SqStack &S, char x);
bool Pop(SqStack &S, char &x);
// 判断长度为length的字符串str中的括号是否匹配
bool bracketCheck(char str[], int length){
SqStack S;
InitStack(S);
// 遍历str
for(int i=0; i<length; i++){
// 扫描到左括号,入栈
if(str[i] == '(' || str[i] == '[' || str[i] == '{'){
Push(S, str[i]);
}else{
// 扫描到右括号且栈空直接返回
if(StackEmpty(S))
return false;
char topElem;
// 用topElem接收栈顶元素
Pop(S, topElem);
// 括号不匹配
if(str[i] == ')' && topElem != '(' )
return false;
if(str[i] == ']' && topElem != '[' )
return false;
if(str[i] == '}' && topElem != '{' )
return false; }
}
// 扫描完毕若栈空则说明字符串str中括号匹配
return StackEmpty(S);
}
例:将 ( ( 15 ÷ ( 7 − ( 1 + 1 ) ) ) × 3 ) − ( 2 + ( 1 + 1 ) ) ((15÷(7−(1+1)))×3)−(2+(1+1)) ((15÷(7−(1+1)))×3)−(2+(1+1)) 转换为后缀表达式?
答: 15 7 1 1 + − ÷ 3 × 2 1 1 + + − 15\ 7\ 1\ 1 + -\ ÷\ 3\ ×\ 2\ 1\ 1\ + +\ - 15 7 1 1+− ÷ 3 × 2 1 1 ++ −
中缀转后缀要遵循“左优先”原则:只要左边的运算符能先计算,就优先计算左边的。
例:计算 A B C D − × + E F ÷ − A\ B\ C\ D - × + E\ F\ ÷\ - A B C D−×+E F ÷ − ?
答: ( A + B × ( C − D ) ) − E ÷ F (A+B×(C-D))-E÷F (A+B×(C−D))−E÷F
弹出栈顶元素时,先出栈的是“右操作数”。
例:将 A + B ∗ ( C − D ) – E / F A + B * (C - D) – E / F A+B∗(C−D)–E/F 转为前缀表达式?
答: + A − ∗ B − C D / E F +\ A - *\ B - C\ D\ /\ E\ F + A−∗ B−C D / E F
中缀转前缀遵循“右优先”原则:只要右边的运算符能先计算,就优先算右边的。
前缀表达式的计算方法:
**中缀转后缀的机算方法:**初始化一个栈,用于保存暂时还不能确定运算顺序的运算符。从左到右处理各个元素,直到末尾。可能遇到三种情况:
#define MaxSize 40
typedef struct{
char data[MaxSize];
int top;
}SqStack;
typedef struct{
char data[MaxSize];
int front,rear;
}SqQueue;
void InitStack(SqStack &S);
bool StackEmpty(SqStack S);
bool Push(SqStack &S, char x);
bool Pop(SqStack &S, char &x);
void InitQueue(SqQueue &Q);
bool EnQueue(LQueue &Q, char x);
bool DeQueue(LQueue &Q, char &x);
bool QueueEmpty(SqQueue Q);
// 判断元素ch是否入栈
int JudgeEnStack(SqStack &S, char ch){
char tp = S.data[S->top];
// 如果ch是a~z则返回-1
if(ch >= 'a' && ch <= 'z')
return -1;
// 如果ch是+、-、*、/且栈顶元素优先级大于等于ch则返回0
else if(ch == '+' && (tp == '+' || tp == '-' || tp == '*' || tp == '/'))
return 0;
else if(ch == '-' && (tp == '+' || tp == '-' || tp == '*' || tp == '/'))
return 0;
else if(ch == '*' && (tp == '*' || tp == '/'))
return 0;
else if(ch == '/' && (tp == '*' || tp == '/'))
return 0;
// 如果ch是右括号则返回2
else if(ch == ')')
return 2;
// 其他情况ch入栈,返回1
else return 1;
}
// 中缀表达式转后缀表达式
int main(int argc, char const *argv[]) {
SqStack S;
SqQueue Q;
InitStack(S);
InitQueue(Q);
char ch;
printf("请输入表达式,以“#”结束:");
scanf("%c", &ch);
while (ch != '#'){
// 当栈为空时
if(StackEmpty(&S)){
// 如果输入的是数即a~z,直接入队
if(ch >= 'a' && ch <= 'z')
EnQueue(Q, ch);
// 如果输入的是运算符,直接入栈
else
Puch(S, ch);
}else{
// 当栈非空时,判断ch是否需要入栈
int n = JudgeEnStack(S, ch);
// 当输入是数字时直接入队
if(n == -1){
EnQueue(Q, ch);
}else if(n == 0){
// 当输入是运算符且运算符优先级不高于栈顶元素时
while (1){
// 取栈顶元素入队
char tp;
Pop(S, tp);
EnQueue(Q, tp);
// 再次判断是否需要入栈
n = JudgeEnStack(S, ch);
// 当栈头优先级低于输入运算符或者栈头为‘)’时,入栈并跳出循环
if(n != 0){
EnStack(S, ch);
break;
}
}
}else if(n == 2){
// 当出现‘)’时 将()中间的运算符全部出栈入队
while(1){
char tp;
Pop(S, tp);
if(tp == '(')
break;
else
EnQueue(Q, tp);
}
}else{
// 当运算符优先级高于栈顶元素或出现‘(’时直接入栈
Push(S, ch);
}
}
scanf("%c", &ch);
}
// 将最后栈中剩余的运算符出栈入队
while (!StackEmpty(S)){
char tp;
Pop(S, tp);
EnQueue(Q, tp);
}
// 输出队中元素
while (!QueueEmpety(Q)){
printf("%c ", DeQueue(Q));
}
return 0;
}
**中缀表达式的机算方法:**中缀转后缀 + 后缀表达式的求值(两个算法的结合)
函数调用的特点:最后被调用的函数最先执行结束(LIFO)。
函数调用时,需要用一个“函数调用栈” 存储:
递归调用时,函数调用栈可称为“递归工作栈” 。每进入一层递归,就将递归调用所需信息压入栈顶;每退出一层递归,就从栈顶弹出相应信息。
缺点:效率低,太多层递归可能会导致栈溢出;可能包含很多重复计算。
可以自定义栈将递归算法改造成非递归算法。
除非题目特别说明,否则数组下标默认从0开始。
一维数组的存储:各数组元素大小相同,且物理上连续存放。设起始地址为 LOC,则数组元素 a [ i ] a[i] a[i] 的存放地址 = LOC + i * sizeof(ElemType) (0≤i<10)
二维数组的存储:
**对称矩阵的压缩存储:**若 n 阶矩阵中任意一个元素 a i , j a_{i,j} ai,j都有 a i , j = a j , i a_{i,j} = a_{j,i} ai,j=aj,i 则该矩阵为对称矩阵。对于对称矩阵,只需存储主对角线+下三角区。若按照行优先原则将各元素存入一维数组中,即 a i , j a_{i,j} ai,j 存入到数组 B [ k ] B[k] B[k] 中,那么有 k = j ( j − 1 ) 2 + i − 1 k=\frac{j(j-1)}{2}+i-1 k=2j(j−1)+i−1 ,又 a i , j = a j , i a_{i,j}=a_{j,i} ai,j=aj,i 故有:
k = { i ( i − 1 ) 2 + j − 1 , i ≥ j j ( j − 1 ) 2 + i − 1 , i < j k=\left\{ \begin{array}{lr} \frac{i(i-1)}{2}+j-1, &i\geq j&\\ \frac{j(j-1)}{2}+i-1, &i< j&\\ \end{array} \right. k={2i(i−1)+j−1,2j(j−1)+i−1,i≥ji<j
三角矩阵的压缩存储:
下三角矩阵:除了主对角线和下三角区,其余的元素都相同。
上三角矩阵:除了主对角线和上三角区,其余的元素都相同。
压缩存储策略:按行优先原则将主对角线+下三角区存入一维数组中,并在最后一个位置存储常量。即 a i , j a_{i,j} ai,j 存入到数组 B [ k ] B[k] B[k] 中,那么数组 B [ k ] B[k] B[k] 共有 n ( n − 1 ) 2 + 1 \frac{n(n-1)}{2}+1 2n(n−1)+1 个元素。对于k,有:
k = { i ( i − 1 ) 2 + j − 1 , i ≥ j n ( n − 1 ) 2 , i < j k=\left\{ \begin{array}{lr} \frac{i(i-1)}{2}+j-1, &i\geq j&\\ \frac{n(n-1)}{2}, &i< j&\\ \end{array} \right. k={2i(i−1)+j−1,2n(n−1),i≥ji<j
StrAssign(&T, chars)
:赋值操作。把串 T 赋值为 chars。StrCopy(&T, S)
:复制操作。由串 S 复制得到串 T。StrEmpty(S)
:判空操作。若 S 为空串,则返回 TRUE,否则返回 FALSE。StrLength(S)
:求串长。返回串 S 中元素的个数。ClearString(&S)
:清空操作。将 S 清为空串。DestroyString(&S)
:销毁串。将串 S 销毁(回收存储空间)。Concat(&T, S1, S2)
:串联接。用 T 返回由 S1 和 S2 联接而成的新串 。SubString(&Sub, S, pos, len)
:求子串。用 Sub 返回串 S 的第 pos 个字符起长度为 len 的子串。Index(S, T)
:定位操作。若主串 S 中存在与串 T 值相同的子串,则返回它在主串 S 中第一次出现的位置;否则函数值为 0。StrCompare(S, T)
:比较操作。若 S>T,则返回值>0;若 S=T,则返回值=0;若 S串的顺序存储(静态数组):
// ch[0]废弃不用,声明int型变量length来存放串的长度
#define MAXLEN 255
typedef struct{
char ch[MAXLEN];
int length;
}SString;
// 串的初始化
bool InitString(SString &S){
S.length = 0;
return true;
}
// 求串的长度
int StrLength(SString S){
return S.length;
}
// 求主串由位序pos开始len长度的子串存入到串Sub中
bool SubString(SString &Sub, SString S, int pos, int len){
if(pos+len-1 > S.length)
return false;
for(int i=pos; i<pos+len; i++)
Sub.ch[i-pos+1] = S.ch[i];
Sub.length = len;
return true;
}
// 比较S与T的大小。若S>T,则返回值大于0;若S=T,则返回值等于0;若S
int StrCompare(SString S, SString T){
for(int i=1; i<=S.length && i<=T.length; i++){
if(S.ch[i]!=T.ch[i])
return S.ch[i]-T.ch[i]
}
// 扫描过的所有字符都相同,则长度长的串更大
return S.length-T.length;
}
// 定位串T在串S中的位置,若无法定位则返回0
int Index(SString S, SString T){
int i=1, n=StrLength(S), m=StrLength(T);
SString sub; //用于暂存数据
while(i<=n-m+1){
SubString(sub, S, i, m);
if(StrCompare(sub, T)!=0)
++i;
else
return i;
}
return 0;
}
void test{
SString S;
InitString(S);
...
}
串的顺序存储(动态数组):
#define MAXLEN 255
typedef struct{
char *ch;
int length;
}HString;
bool InitString(HString &S){
S.ch = (char *)malloc(MAXLEN * sizeof(char));
if(S.ch == NULL)
return false;
S.length = 0;
return true;
}
void test{
HString S;
InitString(S);
...
}
串的链式存储:
typedef struct StringNode{
char ch; //每个结点存1个字符
struct StringNode *next;
}StringNode, *String;
上述方式存储密度很低,可以使每个结点存储多个字符。每个结点被称为块,整个链表被称为块链结构。
typedef struct StringNode{
char ch[4]; //每个结点存多个字符
struct StringNode *next;
}StringNode, *String;
// 在主串S中找到与模式串T相同的子串并返回其位序,否则返回0
int Index(SString S, SString T){
int k=1;
int i=k, j=1;
while(i<=S.length && j<=T.length){
if(S.ch[i] == T.ch[j]){
++i; ++j;
}else{
k++; i=k; j=1;
}
}
if(j>T.length)
return k;
else
return 0;
}
时间复杂度:设模式串长度为m,主串长度为n
例:求模式串“abcabd”的 next 数组。
序号j | 1 | 2 | 3 | 4 | 5 | 6 |
---|---|---|---|---|---|---|
模式串 | a | b | c | a | b | d |
next[j] | 0 | 1 | 1 | 1 | 2 | 3 |
KPM 算法:当子串和模式串不匹配时,主串指针 i 不回溯,模式串指针 j=next[j]。KMP 算法的平均时间复杂度为: O ( n + m ) O(n+m) O(n+m)。
KPM 算法代码实现:
// 获取模式串T的next[]数组
void getNext(SString T, int next[]){
int i=1, j=0;
next[1]=0;
while(i<T.length){
if(j==0 || T.ch[1]==T.ch[j]){
++i; ++j;
next[i]=j;
}else
j=next[j];
}
}
// KPM算法,求主串S中模式串T的位序,没有则返回0
int Index_KPM(SString S, SString T){
int i=1, j=1;
int next[T.length+1];
getNext(T, next);
while(i<=S.length && j<=T.length){
if(j==0 || S.ch[i]==T.ch[j]){
++i; ++j;
}else
j=next[j];
}
if(j>T.length)
return i-T.length;
else
return 0;
}
int main() {
SString S={"ababcabcd", 9};
SString T={"bcd", 3};
printf("%d ", Index_KPM(S, T)); //输出9
}
void getNextval(SString T, int nextval[]){
int i=1,j=0;
nextval[1]=0;
while(i<T.length){
if(j==0 || T.ch[i]==T.ch[j]){
++i; ++j;
if(T.ch[i]!=T.ch[j])
nextval[i]=j;
else
nextval[i]=nextval[j];
}else
j=nextval[j];
}
}
度为m的树 | m叉树的区别 |
---|---|
任意结点的度≤m(最多m个孩子) | 任意结点的度≤m(最多m个孩子) |
至少有一个结点度=m(有m个孩子) | 允许所有结点的度都<m |
一定是非空树,至少有m+1个结点 | 可以是空树 |
二叉树是 n(n≥0)个结点的有限集合:
二叉树的特点:
二叉树的五种状态:
满二叉树:一棵高度为 h,且含有 2 h − 1 2^h - 1 2h−1 个结点的二叉树。
特点:
完全二叉树:当且仅当其每个结点都与高度为 h 的满二叉树中编号为1~n的结点一一对应时,称为完全二叉树。
特点:
二叉排序树:一棵二叉树或者是空二叉树,或者是具有如下性质的二叉树:左子树上所有结点的关键字均小于根结点的关键字;右子树上所有结点的关键字均大于根结点的关键字。
平衡二叉树:树上任一结点的左子树和右子树的深度之差不超过 1。
设非空二叉树中度为 0、1 和 2 的结点个数分别为 n 0 n_0 n0、 n 1 n_1 n1 和 n 2 n_2 n2,则 n 0 = n 2 + 1 n_0 = n_2 + 1 n0=n2+1。
推导过程:设树中结点总数为 n,则: n = n 0 + n 1 + n 2 n=n_0+n_1+n_2 n=n0+n1+n2 , n = n 1 + 2 n 2 + 1 n=n_1+2n_2+1 n=n1+2n2+1。
二叉树第 i 层至多有 2 i − 1 2^{i-1} 2i−1 个结点(i≥1)。
高度为 h 的二叉树至多有 2 h − 1 2^h − 1 2h−1 个结点(满二叉树)。
具有 n 个(n>0)结点的完全二叉树的高度 h 为 ⌈ l o g 2 ( n + 1 ) ⌉ \lceil log_2(n + 1)\rceil ⌈log2(n+1)⌉ 或 ⌊ l o g 2 n ⌋ + 1 \lfloor log_2n\rfloor + 1 ⌊log2n⌋+1。
对于完全二叉树,可以由总结点数 n 推出度为 0、1 和 2 的结点个数 n 0 n_0 n0、 n 1 n_1 n1 和 n 2 n_2 n2。
推导过程: n 1 n_1 n1 = 0 或 1, n 0 = n 2 + 1 n_0 = n_2 + 1 n0=n2+1,则 n 0 + n 2 n_0 + n_2 n0+n2 一定是奇数。
若完全二叉树有 2 k 2k 2k(偶数)个结点,则有 n 1 = 1 n_1=1 n1=1, n 0 = k n_0 = k n0=k, n 2 = k − 1 n_2 = k-1 n2=k−1;
若完全二叉树有 2 k − 1 2k-1 2k−1(奇数)个结点,则有 n 1 = 0 n_1=0 n1=0, n 0 = k n_0 = k n0=k, n 2 = k − 1 n_2 = k-1 n2=k−1;
二叉树的顺序存储:
顺序存储完全二叉树:定义一个长度为 MaxSize 的数组 t,按照从上至下、从左至右的顺序依次存储完全二叉树中的各个结点。让第一个位置空缺,保证数组中下标和结点编号一致。
#define MaxSize 100
struct TreeNode{
ElemType value; //结点中的数据元素
bool isEmpty; //结点是否为空
}
//初始化树T
bool initTree(TreeNode T[]){
for(int i=0; i<MaxSize; i++){
T[i].isEmpty=true;
}
return true;
}
void test(){
struct TreeNode T[MaxSize];
initTree(T);
}
这样可以使用二叉树的性质求一些问题:
但是如果不是完全二叉树,依然按层序将各节点顺序存储,那么将无法从结点编号反映出结点间的逻辑关系。可以将二叉树中的结点编号与完全二叉树对应起来存储,==不过这样会浪费很多内存空间。==因此,二叉树的顺序存储结构,只适合存储完全二叉树。
二叉树的链式存储:
typedef struct BiTNode{
ElemType data;
struct BiTNode *lchild, *rchild;
}BiTNode, *BiTree;
//初始化
bool initTree(BiTree &root){
root = (BiTree)malloc(sizeof(BiTNode));
if(root == NULL)
return false;
root->lchild = NULL;
root->rchild = NULL;
return true;
}
void test{
BiTree root = NULL;
initTree(root);
...
}
二叉树的链式存储可以非常方便的找到指定结点的左右孩子,但是找到指定结点的父结点却非常困难,只能从根节点开始遍历寻找。因此,如果寻找父结点的需求比较多,也可以加上父结点指针形成三叉链表。
遍历:按照某种次序把所有结点都访问一遍。
先序遍历(PreOrder)的操作过程:
中序遍历(InOrder)的操作过程:
若二叉树为空,则什么也不做;
若二叉树非空:
后序遍历(PostOrder)的操作过程:
代码实现:
typedef struct BiTNode{
ElemType data;
struct BiTNode *lchild, *rchild;
}BiTNode, *BiTree;
// 先序遍历
void PreOrder(BiTree T){
if(T!=NULL){
PreOrder(T->lchild);
PreOrder(T->rchild);
visit(T);
}
}
// 中序遍历
void InOrder(BiTree T){
if(T!=NULL){
InOrder(T->lchild);
visit(T);
InOrder(T->rchild);
}
}
// 后序遍历
void PostOrder(BiTree T){
if(T!=NULL){
PostOrder(T->lchild);
PostOrder(T->rchild);
visit(T);
}
}
// 应用:求树的深度
int treeDepth(BiTree T){
if(T == NULL){
return 0;
}else{
int l = treeDepth(T->lchild);
int r = treeDepth(T->rchild);
return l>r ? l+1 r+1;
}
}
算法思想:
代码实现:
// 二叉树结点
typedef struct BiTNode{
char data;
struct BiTNode *lchild, *rchild;
}BiTNode, *BiTree;
// 链式队列结点
typedef struct LinkNode{
BiTNode *data;
struct LinkNode *next;
}LinkNode;
typedef struct{
LinkNode *front, *rear;
}LinkQueue;
// 层序遍历
void LevelOrder(BiTree T){
LinkQueue Q;
InitQueue(Q);
BiTree p;
EnQueue(Q, T);
while(!IsEmpty(Q)){
DeQueue(Q, p);
visit(p);
if(p->lchild!=NULL)
EnQueue(Q, p->lchild);
if(p->rchild!=NULL)
EnQueue(Q, p->rchild);
}
}
一个前序遍历序列可能对应多种二叉树形态。同理,一个后序遍历序列、一个中序遍历序列、一个层序遍历序列也可能对应多种二叉树形态。即:若只给出一棵二叉树的 前/中/后/层序遍历序列 中的一种,不能唯一确定一棵二叉树。
由二叉树的遍历序列构造二叉树:
由 前序+中序遍历序列 构造二叉树:由前序遍历的遍历顺序(根节点、左子树、右子树)可推出根节点,由根节点在中序遍历序列中的位置即可推出左子树与右子树分别有哪些结点。
由 后序+中序遍历序列 构造二叉树:由后序遍历的遍历顺序(左子树、右子树、根节点)可推出根节点,由根节点在中序遍历序列中的位置即可推出左子树与右子树分别有哪些结点。
由 层序+中序遍历序列 构造二叉树:由层序遍历的遍历顺序(层级遍历)可推出根节点,由根节点在中序遍历序列中的位置即可推出左子树与右子树分别有哪些结点。
使用链式存储二叉树如何找到指定结点 p 在中序遍历序列中的前驱和后继?
答:从根节点出发,重新进行一次中序遍历,指针 q 记录当前访问的结点,指针 pre 记录上一个被访问的结点。当 q = = p q==p q==p 时,pre 为 q 的前驱结点;当 p r e = = p pre==p pre==p 时,q 为 p 的后继结点。
缺点:找前驱、后继很不方便;遍历操作必须从根结点开始。
n 个结点的二叉树,有 n+1 个空链域,可用来记录前驱、后继的信息。指向前驱、后继的指针被称为“线索”,形成的二叉树被称为线索二叉树。
线索二叉树的结点在原本二叉树的基础上,新增了左右线索标志 tag。当 tag == 0 时,表示指针指向孩子;当 tag == 1 时,表示指针是“线索”。
// 线索二叉树的结点
typedef struct ThreadNode{
ElemType data;
struct ThreadNode *lchild, *rchild;
int ltag, rtag; //左、右线索标志
}
中序线索二叉树的存储:
中序线索化:
typedef struct ThreadNode{
ElemType data;
struct ThreadNode *lchild, *rchild;
int ltag, rtag;
}ThreadNode, *ThreadTree;
// 中序遍历线索化二叉树
void InThread(ThreadTree &p, ThreadTree &pre) {
if (p != NULL) {
InThread(p->lchild, pre);
if (p->lchild == NULL) {
p->lchild = pre;
p->ltag = 1;
}
if (pre != NULL && pre->rchild == NULL) {
pre->rchild = p;
pre->rtag = 1;
}
pre = p;
InThread(p->rchild, pre);
}
}
// 创建线索二叉树T
void CreateInThread(ThreadTree T) {
ThreadNode *pre = NULL;
if (T != NULL) {
InThread(T, pre);
pre->rchild = NULL;
pre->rtag = 1;
}
}
先序线索化:
typedef struct ThreadNode{
ElemType data;
struct ThreadNode *lchild, *rchild;
int ltag, rtag;
}ThreadNode, *ThreadTree;
// 先序遍历线索化二叉树
void PreThread(ThreadTree &p, ThreadTree &pre){
if (p != NULL) {
if (p->lchild == NULL) {
p->lchild = pre;
p->ltag = 1;
}
if (pre != NULL && pre->rchild == NULL) {
pre->rchild = p;
pre->rtag = 1;
}
pre = p;
PreThread(p->lchild, pre);
PreThread(p->rchild, pre);
}
}
// 创建线索二叉树
void CreatePreThread(ThreadTree T){
ThreadNode *pre = NULL;
if (T != NULL) {
PreThread(T, pre);
pre->rchild = NULL;
pre->rtag = 1;
}
}
后序线索化:
typedef struct ThreadNode{
ElemType data;
struct ThreadNode *lchild, *rchild;
int ltag, rtag;
}ThreadNode, *ThreadTree;
// 后序遍历线索二叉树
void PostThread(ThreadTree &p, ThreadTree &pre){
PostThread(p->lchild, pre);
PostThread(p->rchild, pre);
if (p != NULL) {
if (p->lchild == NULL) {
p->lchild = pre;
p->ltag = 1;
}
if (pre != NULL && pre->rchild == NULL) {
pre->rchild = p;
pre->rtag = 1;
}
pre = p;
}
}
// 后序线索化二叉树T
void CreatePostThread(ThreadTree T){
ThreadNode *pre = NULL;
if (T != NULL) {
PostThread(T, pre);
pre->rchild = NULL;
pre->rtag = 1;
}
}
中序线索二叉树找到指定结点 * p 的中序后继 next:
p->rtag==1
,则next = p->rchild
;p->rtag==0
,则 next 为 p 的右子树中最左下结点。// 找到以p为根的子树中,第一个被中序遍历的结点
ThreadNode *FirstNode(ThreadNode *p){
// 循环找到最左下结点(不一定是叶结点)
while(p->ltag==0)
p=p->lchild;
return p;
}
// 在中序线索二叉树中找到结点p的后继结点
ThreadNode *NextNode(ThreadNode *p){
// 右子树中最左下的结点
if(p->rtag==0)
return FirstNode(p->rchild);
else
return p->rchild;
}
// 对中序线索二叉树进行中序循环(非递归方法实现)
void InOrder(ThreadNode *T){
for(ThreadNode *p=FirstNode(T); p!=NULL; p=NextNode(p)){
visit(p);
}
}
中序线索二叉树找到指定结点 * p 的中序前驱 pre:
p->ltag==1
,则pre = p->lchild
;p->ltag==0
,则 next 为 p 的左子树中最右下结点。// 找到以p为根的子树中,最后一个被中序遍历的结点
ThreadNode *LastNode(ThreadNode *p){
// 循环找到最右下结点(不一定是叶结点)
while(p->rtag==0)
p=p->rchild;
return p;
}
// 在中序线索二叉树中找到结点p的前驱结点
ThreadNode *PreNode(ThreadNode *p){
// 左子树中最右下的结点
if(p->ltag==0)
return LastNode(p->lchild);
else
return p->lchild;
}
// 对中序线索二叉树进行中序循环(非递归方法实现)
void RevOrder(ThreadNode *T){
for(ThreadNode *p=LastNode(T); p!=NULL; p=PreNode(p))
visit(p);
}
先序线索二叉树找到指定结点 * p 的先序后继 next:
p->rtag==1
,则next = p->rchild
;p->rtag==0
:
先序线索二叉树找到指定结点 * p 的先序前驱 pre:
后序线索二叉树找到指定结点 * p 的后序前驱 pre:
p->ltag==1
,则pre = p->lchild
;p->ltag==0
:
后序线索二叉树找到指定结点 * p 的后序后继 next:
**双亲表示法(顺序存储):**每个结点中保存指向双亲的“指针”。
#define MAX_TREE_SIZE 100
// 树的结点
typedef struct{
ElemType data;
int parent;
}PTNode;
// 树的类型
typedef struct{
PTNode nodes[MAX_TREE_SIZE];
int n; //结点数量
}PTree;
孩子表示法(顺序+链式存储):
#define MAX_TREE_SIZE 100
struct CTNode{
int child; //孩子结点在数组中的位置
struct CTNode *next; //下一个孩子
}
typedef struct{
ElemType data;
struct CTNode *firstChild; //第一个孩子
}CTBox;
typedef struct{
CTBox node[MAX_TREE_SIZE];
int n,r; //结点数和根的位置
}CTree;
**孩子兄弟表示法(链式存储):**用孩子兄弟表示法可以将树转换为二叉树的形式。
//孩子兄弟表示法结点
typedef struct CSNode{
ElemType data;
struct CSNode *firstchild, *nextsibling; //第一个孩子和右兄弟结点
}CSNode, *CSTree;
**森林和二叉树的转换:**森林是m(m≥0)棵互不相交的树的集合,可以将森林中的树看做一棵树中的兄弟结点,再使用孩子兄弟表示法将其转换为二叉树。
**树的先根遍历:**若树非空,先访问根结点, 再依次对每棵子树进行先根遍历。(深度优先遍历)
void PreOrder(TreeNode *R){
if(R!=NULL){
visit(R);
while(R还有下一个子树T)
PreOrder(T);
}
}
树的先根遍历序列与这棵树相应二叉树的先序序列相同。
**树的后根遍历:**若树非空,先依次对每棵子树进行后根遍历,最后再访问根结点。(深度优先遍历)
void PostOrder(TreeNode *R){
if(R!=NULL){
while(R还有下一个子树T)
PreOrder(T);
visit(R);
}
}
树的后根遍历序列与这棵树相应二叉树的中序序列相同。
树的层次遍历(用队列实现):
**森林的先序遍历:**若森林为非空,则按如下规则进行遍历:
森林的先序遍历效果等同于依次对各个树进行先根遍历,等同于对二叉树的先序遍历。
**森林的中序遍历:**若森林为非空,则按如下规则进行遍历:
森林的中序遍历效果等同于依次对各个树进行后根遍历,等同于对二叉树的中序遍历。
二叉排序树,又称二叉查找树(BST,Binary Search Tree)。一棵二叉树或者是空二叉树,或者是具有如下性质的二叉树:
即:左子树结点值 < 根结点值 < 右子树结点值。因此,对二叉排序树进行中序遍历,可以得到一个递增的有序序列。
二叉排序树的查找:
// 二叉排序树结点
typedef struct BSTNode{
int key;
struct BSTNode *lchild, *rchild;
}BSTNode, *BSTree;
// 在二叉排序树中查找值为key的结点(非递归)
BSTNode *BST_Search(BSTree T, int key){
while(T!=NULL && key!=T->key){
if(key < T->key) T=T->lchild;
else T=T->rchild;
}
return T;
}
// 在二叉排序树中查找值为key的结点(递归实现)
BSTNode *BST_Search(BSTree T, int key){
if(T==NULL || T->key==key)
return T;
else if(key < T->key)
return BSTSearch(T->lchild, key);
else if(key > T->key)
return BSTSearch(T->rchild, key);
}
// 在二叉排序树中插入关键字为k的新节点(递归实现)
int BST_Insert(BSTree &T, int k){
if(T==NULL){
T=(BSTree)malloc(sizeof(BSTNode));
T->key=k;
T->lchild = T->rchild = NULL;
return 1;
}else if(k==T->key){
return 0;
}else if(k<T->key){
return BST_Insert(T->lchild, k);
}else
return BST_Insert(T->rchild, k);
}
// 按照str[]中的关键字序列建立二叉排序树
void Creat_BST(BSTree &T, int str[], int n){
T=NULL;
int i=0;
while(i<n){
BST_Insert(T,str[i]);
i++;
}
}
**二叉排序树的删除:**先搜索找到目标结点:
**查找效率分析:**查找失败的平均查找长度 ASL(Average Search Length)
平衡二叉树(Balanced Binary Tree),简称平衡树(AVL树),树上任一结点的左子树和右子树的高度之差不超过 1。
结点的平衡因子 = 左子树高 - 右子树高
平衡二叉树的结点:
typedef struct AVLNode{
int key;
int balance; //平衡因子
struct AVLNode *lchild, *rchild;
}AVLNode, *AVLTree;
**调整最小不平衡子树:**在平衡二叉树的插入操作中,只要将最小不平衡子树调整平衡,则其他祖先结点都会恢复平衡。调整可分为以下四种情况:
**调整最小不平衡子树(LL):**由于在结点 A 的左孩子(L)的左子树(L)上插入了新结点,A的平衡因子由 1增至 2,导致以 A 为根的子树失去平衡,需要一次向右的旋转操作。将 A 的左孩子 B 向右上旋转代替 A 成为根结点,将 A 结点向右下旋转成为 B 的右子树的根结点,而 B 的原右子树则作为 A 结点的左子树。
**查找效率分析:**若树高为h,则最坏情况下,查找一个关键字最多需要对比 h 次,即查找操作的时间复杂度不可能超过 O(h)。
由于平衡二叉树上任一结点的左子树和右子树的高度之差不超过 1,假如以 n h n_h nh表示深度为 h 的平衡树中含有的最少结点数,则有 n 0 = 0 n_0 = 0 n0=0, n 1 = 1 n_1 = 1 n1=1, n 2 = 2 n_2 = 2 n2=2,并且有 n h = n h − 1 + n h − 2 + 1 n_h = n_{h−1} + n_{h−2} +1 nh=nh−1+nh−2+1 。
W P L = ∑ i = 1 n w i l i WPL=\sum_{i=1}^{n}w_il_i WPL=i=1∑nwili
哈夫曼树的定义:在含有 n 个带权叶结点的二叉树中,其中带权路径长度(WPL)最小的二叉树称为哈夫曼树,也称最优二叉树。
给定 n 个权值分别为 w 1 w_1 w1, w 2 w2 w2,…, w n w_n wn 的结点,构造哈夫曼树的算法描述如下:
构造哈夫曼树的注意事项:
图:图 G 由顶点集 V 和边集 E 组成,记为 G = ( V , E ) G = (V, E) G=(V,E),其中 V ( G ) V(G) V(G) 表示图 G 中顶点的有限非空集; E ( G ) E(G) E(G) 表示图 G 中顶点之间的关系(边)集合。若 V = { v 1 , v 2 , … , v n } V = \{v1, v2, … , vn\} V={v1,v2,…,vn},则用 ∣ V ∣ |V| ∣V∣ 表示图 G 中顶点的个数,也称图 G 的阶; E = { ( u , v ) ∣ u ∈ V , v ∈ V } E = \{(u, v) | u\in V, v\in V\} E={(u,v)∣u∈V,v∈V},用 ∣ E ∣ |E| ∣E∣ 表示图 G 中边的条数。
注意:线性表可以是空表,树可以是空树,但图不可以是空,即V一定是非空集。
无向图:若 E 是无向边(也称边)的有限集合时,则图 G 为无向图。边是顶点的无序对,记为 ( v , w ) (v, w) (v,w) 或$ (w, v)$,其中 v、w 是顶点。可以说顶点 w 和顶点 v 互为邻接点,边 ( v , w ) (v, w) (v,w) 依附于顶点 w 和 v;或者说边 ( v , w ) (v, w) (v,w) 和顶点 v、w 相关联。
有向图:若 E 是有向边(也称弧)的有限集合时,则图 G 为有向图。 弧是顶点的有序对,记为 < v , w >
简单图:① 不存在重复边; ② 不存在顶点到自身的边。
多重图:图 G 某两个结点之间的边数多于一条,又允许顶点通过同一条边和自己关联, 则 G 为多重图。
边的权:在一个图中,每条边都可以标上具有某种含义的数值,该数值称为该边的权值。
带权图:边上带有权值的图称为带权图,也称网。
带权路径长度:当图是带权图时,一条路径上所有边的权值之和,称为该路径的带权路径长度。
无向完全图:无向图中任意两个顶点之间都存在边。若无向图的顶点数 ∣ V ∣ = n |V|=n ∣V∣=n,则 ∣ E ∣ ∈ [ 0 , C n 2 ] |E|∈[0, \mathrm{C}_n^2] ∣E∣∈[0,Cn2] = [ 0 , n ( n – 1 ) / 2 ] [0, n(n–1)/2] [0,n(n–1)/2]。
有向完全图:有向图中任意两个顶点之间都存在方向相反的两条弧。若有向图的顶点数 ∣ V ∣ = n |V|=n ∣V∣=n,则 ∣ E ∣ ∈ [ 0 , 2 C n 2 ] = [ 0 , n ( n – 1 ) ] |E|∈[0, 2\mathrm{C}_n^2]=[0, n(n–1)] ∣E∣∈[0,2Cn2]=[0,n(n–1)]。
稀疏图、稠密图:边数很少的图称为稀疏图,反之称为稠密图。
邻接矩阵存储无向图、有向图:
#define MaxVertexNum 100 //顶点数目的最大值
typedef struct{
char Vex[MaxVertexNum]; //顶点表
int Edge[MaxVertexNum][MaxVertexNum]; //邻接矩阵,边表
int vexnum,arcnum; //图的顶点数和边数
}MGraph;
度:第 i 个结点的度 = 第 i 行(或第 i 列)的非零元素个数;
出度:第 i 行的非零元素个数;
入度:第 i 列的非零元素个数。
邻接矩阵法存储带权图:
#define MaxVertexNum 100 //顶点数目的最大值
#define INFINITY 2147483647; //表示“无穷”
typedef char VertexType; //顶点数据类型
typedef int EdgeType; //边数据类型
typedef struct{
VertexType Vex[MaxVertexNum]; //顶点表
EdgeType Edge[MaxVertexNum][MaxVertexNum]; //边的权值
int vexnum,arcnum; //图的当前顶点数和弧数
}MGraph;
性能分析:
**邻接矩阵的性质:**设图 G 的邻接矩阵为 A(矩阵元素为 0 或 1),则 A n A^n An 的元素 A n [ i ] [ j ] A^n[i][j] An[i][j] 等于由顶点 i 到顶点 j 的长度为 n 的路径的数目。
邻接表存储存储无向图、有向图:
#define MVNum 100 //最大顶点数
typedef struct ArcNode{ //边/弧
int adjvex; //邻接点的位置
struct ArcNode *next; //指向下一个表结点的指针
}ArcNode;
typedef struct VNode{
char data; //顶点信息
ArcNode *first; //第一条边/弧
}VNode, AdjList[MVNum]; //AdjList表示邻接表类型
typedef struct{
AdjList vertices; //头结点数组
int vexnum, arcnum; //当前的顶点数和边数
}ALGraph;
对比邻接矩阵:
邻接表 | 邻接矩阵 | |
---|---|---|
空间复杂度 | 无向图:$O( | V |
适用场景 | 存储稀疏图 | 存储稠密图 |
表示方式 | 不唯一 | 唯一 |
计算度、入度、出度 | 计算有向图的度、入度不方便,其余很方便 | 遍历对应行或列 |
找相邻的边 | 找有向图的入边不方便,其余很方便 | 遍历对应行或列 |
十字链表存储有向图:
#define MAX_VERTEX_NUM 20 //最大顶点数量
typedef struct ArcBox{ //弧结点
int tailvex, headvex; //弧尾,弧头顶点编号(一维数组下标)
struct ArcBox *hlink, *tlink; //弧头相同、弧尾相同的下一条弧的链域
InfoType info; //权值
}ArcBox;
typedef struct VexNode{ //顶点结点
VertexType data; //顶点数据域
ArcBox *firstin, *firstout; //该顶点的第一条入弧和第一条出弧
}VexNode;
typedef struct{ //有向图
VexNode xlist[MAX_VERTEX_NUM]; //存储顶点的一维数组
int vexnum, arcnum; //有向图的当前顶点数和弧数
}OLGraph;
邻接多重表存储无向图:
#define MAX_VERTEX_NUM 20 //最大顶点数量
struct EBox{ //边结点
int i,j; //该边依附的两个顶点的位置(一维数组下标)
EBox *ilink,*jlink; //分别指向依附这两个顶点的下一条边
InfoType info; //边的权值
};
struct VexBox{
VertexType data;
EBox *firstedge; //指向第一条依附该顶点的边
};
struct AMLGraph{
VexBox adjmulist[MAX_VERTEX_NUM];
int vexnum,edgenum; //无向图的当前顶点数和边数
};
四种存储方法比较:
邻接矩阵 | 邻接表 | 十字链表 | 邻接多重表 | |
---|---|---|---|---|
空间复杂度 | $O( | V | ^2)$ | 无向图:$O( |
找相邻边 | 遍历对应行或列 | 找有向图的入边必须遍历整个邻接表 | 很方便 | 很方便 |
删除顶点或边 | 删除边很方便,删除顶点需要大量移动数据 | 无向图中删除边或顶点都不方便 | 很方便 | 很方便 |
适用场景 | 存储稠密图 | 存储稀疏图 | 存储有向图 | 存储无向图 |
表示方式 | 唯一 | 不唯一 | 不唯一 | 不唯一 |
Adjacent(G, x, y)
:判断图 G 是否存在边 < x , y > Neighbors(G, x)
:列出图 G 中与结点 x 邻接的边。InsertVertex(G, x)
:在图 G 中插入顶点 x 。DeleteVertex(G, x)
:从图 G 中删除顶点 x。AddEdge(G, x, y)
:若无向边 ( x , y ) (x, y) (x,y) 或有向边 < x , y > RemoveEdge(G, x, y)
:若无向边 ( x , y ) (x, y) (x,y) 或有向边 < x , y > FirstNeighbor(G, x)
:求图 G 中顶点 x 的第一个邻接点,若有则返回顶点号。若 x 没有邻接点或图中不存在 x,则返回 -1。NextNeighbor(G, x, y)
:假设图 G 中顶点 y 是顶点 x 的一个邻接点,返回除 y 之外顶点 x 的下一个邻接点的顶点号,若 y 是 x 的最后一个邻接点,则返回 -1。Get_edge_value(G, x, y)
:获取图 G 中边 ( x , y ) (x, y) (x,y) 或 < x , y > Set_edge_value(G, x, y, v)
:设置图 G 中边 ( x , y ) (x, y) (x,y) 或 < x , y > ⼴度优先遍历(Breadth-First-Search, BFS)要点:
广度优先遍历用到的操作:
FirstNeighbor(G, x)
:求图 G 中顶点 x 的第⼀个邻接点,若有则返回顶点号;若 x 没有邻接点或图中不存在 x,则返回 -1。NextNeighbor(G, x, y)
:假设图 G 中顶点 y 是顶点 x 的⼀个邻接点,返回除 y 之外顶点 x 的下⼀个邻接点的顶点号,若 y 是 x 的最后⼀个邻接点,则返回 -1。广度优先遍历伪代码:
bool visited[MAX_VERTEX_NUM]; //访问标记数组
// 对图G进行广度优先遍历
void BFSTraverse(Graph G){
for(i=0; i<G.vexnum; ++i) //访问标记数组初始化
visited[i]=FALSE;
InitQueue(Q); //初始化辅助队列
for(i=0; i<G.vexnum; ++i) //从0号结点开始遍历,对每个连通分量进行一次广度优先遍历
if(!visited[i])
BFS(G,i);
}
//从顶点v开始广度优先遍历图G
void BFS(Graph G,int v){
visit(G,v); //访问图G的结点v
visited[v]=TREE; //标记v已被访问
EnQueue(Q,v); //顶点v入队列Q
while(!isEmpty(Q)){
DeQueue(Q,v); //队列头节点出队并将头结点的值赋给v
for(w=FirstNeighbor(G,v); w>=0; w=NextNeighbor(G,v,w)){
//检测v的所有邻结点
if(!visited[w]){
visit(w);
visited[w]=TREE;
EnQueue(Q,w);
}
}
}
}
复杂度分析:
⼴度优先⽣成树:⼴度优先⽣成树由⼴度优先遍历过程确定。由于邻接表的表示⽅式不唯⼀,因此基于邻接表的⼴度优先⽣成树也不唯⼀。
bool visited[MAX_VERTEX_NUM]; //访问标记数组
// 对图G进行深度优先算法
void DFSTraverse(Graph G){
for(v=0; v<G.vexnum; v++){ //初始化标记数组
visited[v]=FALSE;
}
for(v=0; v<G.vexnum; v++){
if(!visited[v])
DFS(G,v);
}
}
// 从顶点v出发深度优先遍历图G
void DFS(Graph G,int v){
visit(G,v);
visited[v]=TREE;
for(w=FirstNeighbor(G,v);w>=0;w=NextNeighbor(G,v)){
if(!visited[w])
DFS(G,v);
}
}
复杂度分析:
深度优先遍历序列:
同⼀个图的邻接矩阵表示⽅式唯⼀,因此深度优先遍历序列唯⼀,深度优先⽣成树也唯⼀;
同⼀个图的邻接表表示⽅式不唯⼀,因此深度优先遍历序列不唯⼀,深度优先⽣成树也不唯⼀。
Prim 算法(普⾥姆) | Kruskal 算法(克鲁斯卡尔) | |
---|---|---|
时间复杂度 | $O( | V |
适用场景 | 稠密图 | 稀疏图 |
⽆权图可以视为每条边的权值都为 1 的带权图。
使用 BFS算法求无权图的最短路径问题,需要使用三个数组:
d[]
数组用于记录顶点 u 到其他顶点的最短路径。path[]
数组用于记录最短路径从那个顶点过来。visited[]
数组用于记录是否被访问过。代码实现:
#define MAX_LENGTH 2147483647 //地图中最大距离,表示正无穷
// 求顶点u到其他顶点的最短路径
void BFS_MIN_Disrance(Graph G,int u){
for(i=0; i<G.vexnum; i++){
visited[i]=FALSE; //初始化访问标记数组
d[i]=MAX_LENGTH; //初始化路径长度
path[i]=-1; //初始化最短路径记录
}
InitQueue(Q); //初始化辅助队列
d[u]=0;
visites[u]=TREE;
EnQueue(Q,u);
while(!isEmpty[Q]){ //BFS算法主过程
DeQueue(Q,u); //队头元素出队并赋给u
for(w=FirstNeighbor(G,u);w>=0;w=NextNeighbor(G,u,w)){
if(!visited[w]){
d[w]=d[u]+1;
path[w]=u;
visited[w]=TREE;
EnQueue(Q,w); //顶点w入队
}
}
}
}
final[]
数组用于标记各顶点是否已找到最短路径。dist[]
数组用于记录各顶点到源顶点的最短路径长度。path[]
数组用于记录各顶点现在最短路径上的前驱。#define MAX_LENGTH = 2147483647;
// 求顶点u到其他顶点的最短路径
void BFS_MIN_Disrance(Graph G,int u){
for(int i=0; i<G.vexnum; i++){ //初始化数组
final[i]=FALSE;
dist[i]=G.edge[u][i];
if(G.edge[u][i]==MAX_LENGTH || G.edge[u][i] == 0)
path[i]=-1;
else
path[i]=u;
final[u]=TREE;
}
for(int i=0; i<G.vexnum; i++){
int MIN=MAX_LENGTH;
int v;
// 循环遍历所有结点,找到还没确定最短路径,且dist最⼩的顶点v
for(int j=0; j<G.vexnum; j++){
if(final[j]!=TREE && dist[j]<MIN){
MIN = dist[j];
v = j;
}
}
final[v]=TREE;
// 检查所有邻接⾃v的顶点路径长度是否最短
for(int j=0; j<G.vexnum; j++){
if(final[j]!=TREE && dist[j]>dist[v]+G.edge[v][j]){
dist[j] = dist[v]+G.edge[v][j];
path[j] = v;
}
}
}
}
Dijkstra算法能够很好的处理带权图的单源最短路径问题,但不适⽤于有负权值的带权图。
Floyd算法:求出每⼀对顶点之间的最短路径,使⽤动态规划思想,将问题的求解分为多个阶段。
Floyd算法使用到两个矩阵:
dist[][]
:目前各顶点间的最短路径。path[][]
:两个顶点之间的中转点。代码实现:
int dist[MaxVertexNum][MaxVertexNum];
int path[MaxVertexNum][MaxVertexNum];
void Floyd(MGraph G){
int i,j,k;
// 初始化部分
for(i=0;i<G.vexnum;i++){
for(j=0;j<G.vexnum;j++){
dist[i][j]=G.Edge[i][j];
path[i][j]=-1;
}
}
// 算法核心部分
for(k=0;k<G.vexnum;k++){
for(i=0;i<G.vexnum;i++){
for(j=0;j<G.vexnum;j++){
if(dist[i][j]>dist[i][k]+dist[k][j]){
dist[i][j]=dist[i][k]+dist[k][j];
path[i][j]=k;
}
}
}
}
}
Floyd算法可以⽤于负权值带权图,但是不能解决带有“负权回路”的图(有负权值的边组成回路),这种图有可能没有最短路径。
BFS算法 | Dijkstra算法 | Floyd算法 | |
---|---|---|---|
无权图 | ✔ | ✔ | ✔ |
带权图 | ✘ | ✔ | ✔ |
带负权值的图 | ✘ | ✘ | ✔ |
带负权回路的图 | ✘ | ✘ | ✘ |
时间复杂度 | $O( | V^2 | ) 或 或 或O( |
通常⽤于 | 求⽆权图的单源最短路径 | 求带权图的单源最短路径 | 求带权图中各顶点间的最短路径 |
用有向无环图描述表达式 ( ( a + b ) ∗ ( b ∗ ( c + d ) ) + ( c + d ) ∗ e ) ∗ ( ( c + d ) ∗ e ) ((a+b)*(b*(c+d))+(c+d)*e)*((c+d)*e) ((a+b)∗(b∗(c+d))+(c+d)∗e)∗((c+d)∗e)
拓扑排序:在图论中,由⼀个有向⽆环图的顶点组成的序列,当且仅当满⾜下列条件时,称为该图的⼀个拓扑排序:
或定义为:拓扑排序是对有向⽆环图的顶点的⼀种排序,它使得若存在⼀条从顶点 A 到顶点 B 的路径,则在排序中顶点 B 出现在顶点 A 的后⾯。每个 AOV ⽹都有⼀个或多个拓扑排序序列。
拓扑排序的实现:
例如,“番茄炒蛋工程”的其中一个拓扑排序为:准备厨具,买菜,洗番茄,切番茄,打鸡蛋,下锅炒,吃。
#define MaxVertexNum 100 //图中顶点数目最大值
typedef struct ArcNode{ //边表结点
int adjvex; //该弧所指向的顶点位置
struct ArcNode *nextarc; //指向下一条弧的指针
}ArcNode;
typedef struct VNode{ //顶点表结点
VertexType data; //顶点信息
ArcNode *firstarc; //指向第一条依附该顶点的弧的指针
}VNode,AdjList[MaxVertexNum];
typedef struct{
AdjList vertices; //邻接表
int vexnum,arcnum; //图的顶点数和弧数
}Graph; //Graph是以邻接表存储的图类型
// 对图G进行拓扑排序
bool TopologicalSort(Graph G){
InitStack(S); //初始化栈,存储入度为0的顶点
for(int i=0;i<g.vexnum;i++){
if(indegree[i]==0)
Push(S,i); //将所有入度为0的顶点进栈
}
int count=0; //计数,记录当前已经输出的顶点数
while(!IsEmpty(S)){ //栈不空,则存入
Pop(S,i); //栈顶元素出栈
print[count++]=i; //输出顶点i
for(p=G.vertices[i].firstarc;p;p=p=->nextarc){
//将所有i指向的顶点的入度减1,并将入度为0的顶点压入栈
v=p->adjvex;
if(!(--indegree[v]))
Push(S,v); //入度为0,则入栈
}
}
if(count<G.vexnum)
return false; //排序失败
else
return true; //排序成功
}
AOE⽹具有以下两个性质:
在 AOE ⽹中仅有⼀个⼊度为 0 的顶点,称为开始顶点(源点),它表示整个⼯程的开始; 也仅有⼀个出度为 0 的顶点,称为结束顶点(汇点),它表示整个⼯程的结束。
从源点到汇点的有向路径可能有多条,所有路径中,具有最⼤路径⻓度的路径称为关键路径,⽽把关键路径上的活动称为关键活动。完成整个⼯程的最短时间就是关键路径的⻓度,若关键活动不能按时完成,则整个 ⼯程的完成时间就会延⻓。
术语:
求关键路径的步骤:
关键活动、关键路径的特性:
只进⾏查找操作最好使用静态查找表,若需要进⾏大量插入删除操作可使用动态查找表。
A S L = ∑ i = 1 n P i C i ASL=\sum ^n_{i=1}P_iC_i ASL=i=1∑nPiCi
typedef struct{ //查找表的数据结构(顺序表)
ElemType *elem; //动态数组基址
int TableLen; //表的长度
}SSTable;
//顺序查找
int Search_Seq(SSTable ST,ElemType key){
int i;
for(i=0;i<ST.TableLen && ST.elem[i]!=key;++i);
// 查找成功返回数组下标,否则返回-1
return i=ST.TableLen? -1 : i;
}
typedef struct{ //查找表的数据结构(顺序表)
ElemType *elem; //动态数组基址
int TableLen; //表的长度
}SSTable;
//顺序查找
int Search_Seq(SSTable ST,ElemType key){
ST.elem[0]=key;
int i;
for(i=ST.TableLen;ST.elem[i]!=key;--i)
// 查找成功返回数组下标,否则返回0
return i;
}
typedef struct{
ElemType *elem;
int TableLen;
}SSTable;
// 折半查找
int Binary_Search(SSTable L,ElemType key){
int low=0,high=L.TableLen,mid;
while(low<=high){
mid=(low+high)/2;
if(L.elem[mid]==key)
return mid;
else if(L.elem[mid]>key)
high=mid-1; //从前半部分继续查找
else
low=mid+1; //从后半部分继续查找
}
return -1;
}
// 索引表
typedef struct{
ElemType maxValue;
int low,high;
}Index;
// 顺序表存储实际元素
ElemType List[100];
⽤顺序查找查索引表,则 L I = 1 + 2 + . . . + b b = b + 1 2 L_I=\frac{1+2+...+b}{b}=\frac{b+1}{2} LI=b1+2+...+b=2b+1, L S = 1 + 2 + . . . + S S = S + 1 2 L_S=\frac{1+2+...+S}{S}=\frac{S+1}{2} LS=S1+2+...+S=2S+1, A S L = s 2 + 2 s + n 2 s ASL=\frac{s^2+2s+n}{2s} ASL=2ss2+2s+n。故当 s = n s=\sqrt n s=n时,ASL 最小,为 n + 1 \sqrt{n}+1 n+1。
⽤顺序查找查索引表,则 L I = ⌈ l o g 2 ( b + 1 ) ⌉ L_I=\lceil log_2(b+1)\rceil LI=⌈log2(b+1)⌉, L S = 1 + 2 + . . . + S S = S + 1 2 L_S=\frac{1+2+...+S}{S}=\frac{S+1}{2} LS=S1+2+...+S=2S+1,则 A S L = S + 1 2 + ⌈ l o g 2 ( b + 1 ) ⌉ ASL=\frac{S+1}{2}+\lceil log_2(b+1)\rceil ASL=2S+1+⌈log2(b+1)⌉
B树,⼜称多路平衡查找树,B树中所有结点的孩⼦个数的最⼤值称为B树的阶,通常⽤m表示。⼀棵m阶B树或为空树,或为满⾜如下特性的m叉树:
树中每个结点⾄多有 m 棵⼦树,即⾄多含有 m-1 个关键字。
若根结点不是终端结点,则⾄少有两棵⼦树。
除根结点外的所有⾮叶结点⾄少有 ⌈ m / 2 ⌉ \lceil m/2\rceil ⌈m/2⌉ 棵⼦树,即⾄少含有 $\lceil m/2\rceil-1 $个关键字。(为了保证查找效率,每个结点的关键字不能太少)
所有⾮叶结点的结构如下:
其中, K i ( i = 1 , 2 , … , n ) K_i(i = 1, 2,…, n) Ki(i=1,2,…,n) 为结点的关键字,且满⾜ K 1 < K 2 < … < K n K_1 < K_2 <…< K_n K1<K2<…<Kn; P i ( i = 0 , 1 , … , n ) P_i(i = 0,1,…, n) Pi(i=0,1,…,n) 为指向⼦树根结点的指针,且指针 P i − 1 P_{i-1} Pi−1 所指⼦树中所有结点的关键字均⼩于 K i K_i Ki, P i P_i Pi 所指⼦树中所有结点的关键字均⼤于 K i Ki Ki, n ( ⌈ m / 2 ⌉ − 1 ≤ n ≤ m − 1 ) n(⌈m/2⌉- 1≤n≤m - 1) n(⌈m/2⌉−1≤n≤m−1) 为结点中关键字的个数。
所有的叶结点都出现在同⼀层次上,并且不带信息(可以视为外部结点或类似于折半查找判定树的查找失败结点,实际上这些结点不存在,指向这些结点的指针为空)。
m阶B树的核⼼特性:
根节点的⼦树数 ∈ [ 2 , m ] ∈[2, m] ∈[2,m],关键字数 ∈ [ 1 , m − 1 ] ∈[1, m-1] ∈[1,m−1]。
其他结点的⼦树数 ∈ [ ⌈ m / 2 ⌉ , m ] ∈[⌈m/2⌉ , m] ∈[⌈m/2⌉,m];关键字数 ∈ [ − 1 , m − 1 ] ∈[ -1, m-1] ∈[−1,m−1]。
对任⼀结点,其所有⼦树⾼度都相同。
关键字的值:⼦树0 < 关键字1 < ⼦树1 < 关键字2 < ⼦树2 <…. (类⽐⼆叉查找树左<中<右)
B树的⾼度:含 n 个关键字的 m叉B树,最⼩⾼度、最⼤⾼度是多少?
l o g m n + 1 ≤ h ≤ l o g ⌈ m / 2 ⌉ n + 1 2 + 1 log_m{n+1}≤h≤log_{⌈m/2⌉}\frac {n+1}2+1 logmn+1≤h≤log⌈m/2⌉2n+1+1
m阶B树 | m阶B+树 |
---|---|
结点中n个关键字对应n+1棵子树 | 结点中n个关键字对应n棵子树 |
根节点关键字数n ∈ [ 1 , m − 1 ] ∈[1,m-1] ∈[1,m−1] 其他结点关键字数n ∈ [ ⌈ m / 2 ⌉ − 1 , m − 1 ] ∈[⌈m/2⌉-1,m-1] ∈[⌈m/2⌉−1,m−1] |
根节点关键字数n ∈ [ 1 , m ] ∈[1,m] ∈[1,m] 其他结点关键字数n ∈ [ ⌈ m / 2 ⌉ , m ] ∈[⌈m/2⌉,m] ∈[⌈m/2⌉,m] |
各结点包含的关键字是不重复的 | 叶子结点包含全部关键字,非叶结点出现过的关键字也会出现在叶子结点中 |
结点中包含了关键字对应的记录的存储地址 | 叶子结点包含信息,非叶结点只起索引作用。非叶结点中的每个索引项只含有对应子树的最大关键字和指向该字树的指针,不含有该关键字对应记录的存储地址 |
不支持顺序查找。查找成功时可能停在任何一层结点,查找速度不稳定 | 支持顺序查找。查找成功或失败一定会达到最下一层结点,查找速度稳定 |
直接定址法:直接取关键字的某个线性函数值为散列地址,散列函数为 H ( k e y ) = k e y H(key)=key H(key)=key 或 H ( k e y ) = a × k e y + b H(key)=a×key+b H(key)=a×key+b。这种方法计算简单,不会产生冲突。缺点是空位较多,会造成存储空间浪费。
除留余数法:假定散列表表长 m,取一个不大于但最接近 m 的质数 p,利用散列函数 H ( k e y ) = k e y % p H(key)=key\%p H(key)=key%p 将关键字转换为散列地址。p取质数是因为这样可以使关键字通过散列函数转换后等概率地映射到散列空间上的任一地址。
数字分析法:假设关键字是 r进制数,而r个数码在个位上出现的频率不一定相同,可能在某些位上分布的均匀一些,而在某些位分布的不均匀。此时应选数码分布均匀的若干位作为散列地址。
平方取中法:这种方法取关键字平方值的中间几位作为散列地址,具体取多少位视具体情况而定。这种方法得到的散列地址与关键字的每一位都有关系,因此使得散列地址分布比较均匀。适用于关键字每位取值都不够均匀或均小于散列地址所需的位数。
开放定址法:指可存放新表项的空闲抵制既向它的同义词表项开放,又向它的非同义词表项开放。其数学递推公式为:
H i = ( H ( k e y ) + d i ) % m H_i=(H(key)+d_i)\%m Hi=(H(key)+di)%m
其中, H ( k e y ) H(key) H(key)为散列函数, i = 0 , 1 , 2 , . . . , k ( k ≤ m − 1 ) i=0,1,2,...,k(k≤m-1) i=0,1,2,...,k(k≤m−1),m表示散列表表长, d i d_i di为增量序列。增量序列通常有四种取法:
拉链法:对于不同的关键字通过散列函数映射到同一地址时,为了与避免非同义词发生冲突,可以把所有的同义词存储到一个线性链表中。拉链法适用于经常进行插入和删除的方法。
例:现有长度为11且初始为空的散列表HT,散列函数 H ( k e y ) = k e y % 7 H(key) = key \% 7 H(key)=key%7,采用线性探查法解决冲突。将关键字序列87,40,30,6,11,22,98,20 ,38依次插入HT后,HT的查找失败的平均长度是多少? 查找成功的平均查找长度又是多少?
答:关键字序列依次插入后,散列表HT如下所示:
散列地址 | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 |
---|---|---|---|---|---|---|---|---|---|---|---|
关键字 | 98 | 22 | 30 | 87 | 11 | 40 | 6 | 20 | 38 | ||
冲突次数 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 1 | 5 |
A S L 查 找 成 功 = ( 1 + 1 + 1 + 1 + 1 + 1 + 1 + 2 + 6 ) / 9 = 5 / 3 ASL_{查找成功}=(1+1+1+1+1+1+1+2+6)/9=5/3 ASL查找成功=(1+1+1+1+1+1+1+2+6)/9=5/3
A S L 查 找 失 败 = ( 10 + 9 + 8 + 7 + 6 + 5 + 4 ) / 7 = 7 ASL_{查找失败}=(10+9+8+7+6+5+4)/7=7 ASL查找失败=(10+9+8+7+6+5+4)/7=7
// 对A[]数组中共n个元素进行插入排序
void InsertSort(int A[],int n){
int i,j,temp;
for(i=1; i<n; i++){
if(A[i]<A[i-1]){ //如果A[i]关键字小于前驱
temp=A[i];
for(j=i-1; j>=0 && A[j]>temp; --j)
A[j+1]=A[j]; //所有大于temp的元素都向后挪
A[j+1]=temp;
}
}
}
// 对A[]数组中共n个元素进行插入排序
void InsertSort(int A[], int n){
int i,j;
for(i=2; i<=n; i++){
if(A[i]<A[i-1]){
A[0]=A[i]; //复制为哨兵,A[0]不放元素
for(j=i-1; A[0]<A[j]; --j)
A[j+1]=A[j];
A[j+1]=A[0];
}
}
}
//对链表L进行插入排序
void InsertSort(LinkList &L){
LNode *p=L->next, *pre;
LNode *r=p->next;
p->next=NULL;
p=r;
while(p!=NULL){
r=p->next;
pre=L;
while(pre->next!=NULL && pre->next->data<p->data)
pre=pre->next;
p->next=pre->next;
pre->next=p;
p=r;
}
}
//对A[]数组中共n个元素进行折半插入排序
void InsertSort(int A[], int n){
int i,j,low,high,mid;
for(i=2; i<=n; i++){
A[0]=A[i]; //将A[i]暂存到A[0]
low=1; high=i-1;
while(low<=high){ //折半查找
mid=(low+high)/2;
if(A[mid]>A[0])
high=mid-1;
else
low=mid+1;
}
for(j=i-1; j>high+1; --j)
A[j+1]=A[j];
A[high+1]=A[0];
}
}
// 对A[]数组共n个元素进行希尔排序
void ShellSort(ElemType A[], int n){
int d,i,j;
for(d=n/2; d>=1; d=d/2){ //步长d递减
for(i=d+1; i<=n; ++i){
if(A[i]<A[i-d]){
A[0]=A[i]; //A[0]做暂存单元,不是哨兵
for(j=i-d; j>0 && A[0]<A[j]; j-=d)
A[j+d]=A[j];
A[j+d]=A[0];
}
}
}
}
算法思路:从后往前(或从前往后)两两比较相邻元素的值,若为逆序(即 A [ i − 1 ] > A [ i ] A[i-1]>A[i] A[i−1]>A[i]),则交换它们,直到序列比较完。如此重复最多 n-1 次冒泡就能将所有元素排好序。为保证稳定性,关键字相同的元素不交换。
代码实现:
// 交换a和b的值
void swap(int &a, int &b){
int temp=a;
a=b;
b=temp;
}
// 对A[]数组共n个元素进行冒泡排序
void BubbleSort(int A[], int n){
for(int i=0; i<n-1; i++){
bool flag = false; //标识本趟冒泡是否发生交换
for(int j=n-1; j>i; j--){
if(A[j-1]>A[j]){
swap(A[j-1],A[j]);
flag=true;
}
}
if(flag==false)
return; //若本趟遍历没有发生交换,说明已经有序
}
}