考纲
(一)数据结构部分;
- 线性表
- 栈、队列、数组
- 查找和内部排序
- 树和图
(二)计算机算法设计部分:
- 递归与分省策路2、回溯法5
- 贪心算法4、分支限界法6、动态规划3
- 算法设计中的数据结构运用
(三)程序设计基础(C或C++)部分:
- 基本数据类型、各种运算符和表达式、基本控制结构。
- 数组的定义、数组元素的引用、数组的初始化,掌握与字符串相关的库函数
- 函数的定义语法,函数调用中参数的传递机制:局部和全局变量的有效范围
- 结构体类型变量的定义、引用、初始化方法,结构体数组的定义、初始化和应用,共同体变量的定义和使用方法。
- 地址和指针的基本概念,如何使用指针来处理数组、字符串以及结构体,函数指针的基本概念以及使用。
- 文件的定义以及对文件进行的各种操作的库函数。
数据结构
第二章 线性表
1.顺序存储
1.1顺序表
顺序表的特点是表中元素的逻辑顺序与其存储的物理顺序相同。
1.1.1静态分配
#define MaxSize 50 //定义线性表的最大长度
typedef struct{
ElemType data[MaxSize]; //顺序表的元素
int length; //顺序表的当前长度
)SqList; //顺序表的类型定义
1.1.2动态分配
#define Initsize 100 //表长度的初始定义
typedef struct{
ElemType *data; //指示动态分配数组的指针
int MaxSize,length; //数组的最大容量和当前个数
}SeqList; //动态分配数组顺序表的类型定义
1.2顺序表的基本操作
1.2.1初始化操作
//静态分配
//sqList L; //声明一个顺序表
void Initlist(SqList &L)(
L.length=0; //顺序表初始长度为0
)
静态分配在声明一个顺序表时,就已为其分配了数组空间,因此初始化时只需将顺序表的当前长度设为0。
//动态分配
void Initlist(SeqList &L){
L.data=(ElemType *)malloc(MaxSize*sizeof(ElemType));//分配存储空间
L.length=0; //顺序表初始长度为0
L.MaxSize=Initsize; //初始存储容量
)
动态分配的初始化为顺序表分配一个预定义大小的数组空间,并将顺序表的当前长度设为0。
MaxSize指示顺序表当前分配的存储空间大小,一旦因插入元素而空间不足,就进行再分配。
1.2.2插入操作O(n)
bool ListInsert(Sqlist & L,int i,ElemType 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--) //将第1个元素及之后的元素后移
L.data[j]=L.data[j-1];
L.data[i-1]=e; //在位置i处放入e
L.length++; //线性表长度加1
return true;
}
在顺序表L的第i(1<=i<=L.length+1)个位置插入新元素e。若i的输入不合法,则返回 false,表示插入失败;否则,将第i个元素及其后的所有元素依次往后移动一个位置,腾出一个空位置插入新元素 e,顺序表长度增加1,插入成功,返回 true。
1.2.3删除操作O(n)
bool ListDelete(SqList 6L,int i,ElemType &e){
if(i<1 || i>L.length) //判断i的范围是否有效
return false;
e=L.data[i-1]; //将被删除的元素赋值给e
for(int j=i;j<L.length;j++) //将第1个位置后的元素前移
L.data[j-1]=L.data[j];
L.length--; //线性表长度减1
return true;
}
删除顺序表L中第i(1<=i<=L.length)个位置的元素,用引用变量e返回。若i的输入不合法,则返回false;否则,将被删元素赋给引用变量e,并将第i+1个元素及其后的所有元素依次往前移动一个位置,返回true。
1.2.4查找操作(按值查找)O(n)
int LocateElem(SqList L,ElemType e){
int i;
for(i=0;i<L.length;i++)
if(L.data[i]==e)
return i+1; //下标为i的元素值等于e,返回其位序i+1
return 0; //退出循环,说明查找失败
}
2链式存储
2.1单链表
利用单链表可以解决顺序表需要大量连续存储单元的缺点,但附加的指针域,也存在浪费存储空间的缺点。由于单链表的元素离散地分布在存储空间中,因此是非随机存取的存储结构,即不能直接找到表中某个特定结点。查找特定结点时,需要从表头开始遍历,依次查找。
头结点和头指针的关系:不管带不带头结点,头指针都始终指向链表的第一个结点,而头结点是带头结点的链表中的第一个结点,结点内通常不存储信息。
引入头结点后,可以带来两个优点:
①由于第一个数据结点的位置被存放在头结点的指针域中,因此在链表的第一个位置上的操作和在表的其他位置上的操作一致,无须进行特殊处理。
② 无论链表是否为空,其头指针都是指向头结点的非空指针(空表中头结点的指针域为空),因此空表和非空表的处理也就得到了统一。
2.1.1单链表初始化
带头结点的单链表初始化时,需要创建一个头结点,并让头指针指向头结点,
头结点的 next 域初始化为 NULL。
bool Initlist(LinkList &L){ //带头结点的单链表的初始化
L=(LNode*)malloc(sizeof(LNode)); //创建头结点
L->next=NULL; //头结点之后暂时还没有元素结点
return true;
}
不带头结点的单链表初始化时,只需将头指针L初始化为NULL。
bool InitList(LinkList &L){ //不带头结点的单链表的初始化
L=NULL;
return true;
}
2.1.2求表长操作
求表长操作是计算单链表中数据结点的个数,需要从第一个结点开始依次访问表中每个结
点,为此需设置一个计数变量,每访问一个结点,其值加1,直到访问到空结点为止。
int Length(LinkList L){
int len=0; //计数变量,初始为0
LNode *p=L;
while(p->next!=NULL){
p=p->next;
len++; //每访问一个结点,计数加1
}
return len;
}
求表长操作的时间复杂度为O(n)。另需注意的是,因为单链表的长度是不包括头结点的,因
此不带头结点和带头结点的单链表在求表长操作上会略有不同
2.1.3按序号查找结点
从单链表的第一个结点开始,沿着 next域从前往后依次搜索,直到找到第i个结点为止,
则返回该结点的指针;若i小于单链表的表长,则返回NULL。
LNode *GetElem(LinkList L,int i){
LNode *p=L; //指针p指向当前扫描到的结点
int j=0; //记录当前结点的位序,头结点是第0个结点
while(p!=NULL&&j<i){ //循环找到第1个结点
p=p->next;
j++;
}
return p; //返回第1个结点的指针或NULL
}
按序号查找操作的时间复杂度为O(n)。
2.1.4按值查找表结点
从单链表的第一个结点开始,从前往后依次比较表中各结点的数据域,若某结点的data域
等于给定值e,则返回该结点的指针:若整个单链表中没有这样的结点,则返回NULL。
LNode *LocateElem(LinkList L, ElemType e) {
LNode *p = L->next;
while (p!= NULL && p->data!= e) {
p = p->next;
}
return p; // 找到后返回该节点指针,否则返回 NULL
}
按值查找操作的时间复杂度为O(n)。
2.1.5插入结点操作
首先查找第i-1个结点,假设第i-1个结点为*p,然后令新结点*s 的指针域指向*p的后
继,再令结点*p的指针域指向新插入的结点*s。
bool ListInsert(LinkList *L, int i, ElemType e) {
LNode *p = *L; // 指针 p 指向当前扫描到的结点
int j = 0; // 记录当前结点的位序,头结点是第 0 个结点
while (p!= NULL && j < i - 1) { // 循环找到第 i - 1 个结点
p = p->next;
j++;
}
if (p == NULL)
return false; // i 值不合法
LNode *s = (LNode *)malloc(sizeof(LNode));
s->data = e;
s->next = p->next; // 图 2.5 中操作步骤①
p->next = s; // 图 2.5 中操作步骤②
return true;
}
插入时,①和②的顺序不能颠倒,否则,先执行p->next=s 后,指向其原后继的指针就不
存在了,再执行s->next=p->next时,相当于执行了s->next=s,显然有误。本算法主要的
时间开销在于查找第i-1个元素,时间复杂度为O(n)。若在指定结点后插入新结点,则时间复杂
度仅为O(1)。需注意的是,当链表不带头结点时,需要判断插入位置i是否为1,若是,则要做
特殊处理,将头指针L指向新的首结点。当链表带头结点时,插入位置i为1时不用做特殊处理。
扩展:对某一结点进行前插操作。
前插操作是指在某结点的前面插入一个新结点,后插操作的定义刚好与之相反。在单链表插
入算法中,通常都采用后插操作。以上面的算法为例,先找到第i-1个结点,即插入结点的前驱,
再对其执行后插操作。由此可知,对结点的前插操作均可转化为后插操作,前提是从单链表的头
结点开始顺序查找到其前驱结点,时间复杂度为O(n)。
此外,可采用另一种方式将其转化为后插操作来实现,设待插入结点为*s,将*s插入到*p
的前面。我们仍然将*s插入到*p的后面,然后将p->data与s->data 交换,这样做既满足逻
辑关系,又能使得时间复杂度为O(1)。该方法的主要代码片段如下
s->next=p->next; //修改指针域,不能颠倒
p->next=s;
temp=p->data; //交换数据域部分
p->data=s->data;
s->data=temp;
2.1.6删除节点操作
假设结点*p 为找到的被删结点的前驱,为实现这一操作后的逻辑关系的变化,仅需修改*p的指针域,将*p的指针域 next 指向*g的下一结点,然后释放*q的存储空间。
bool ListDelete(LinkList &L, int i, ElemType &e) {
LNode *p = L;// 指针 p 指向当前扫描到的结点
int j = 0; // 记录当前结点的位序,头结点是第 0 个结点
while (p!= NULL && j < i - 1) {
p = p->next;
j++;
}
if (p == NULL || p->next == NULL)
return false; // i 值不合法
LNode *q = p->next; // 令 q 指向被删除结点
e = q->data; // 用 e 返回元素的值
p->next = q->next; // 将 *q 结点从链中“断开”
free(q); // 释放结点的存储空间
return true;
}
同插入算法一样,该算法的主要时间也耗费在查找操作上,时间复杂度为O(n)。当链表不带头结点时,需要判断被删结点是否为首结点,若是,则要做特殊处理,将头指针L指向新的首结点。当链表带头结点时,删除首结点和删除其他结点的操作是相同的。
扩展:删除结点*p.
要删除某个给定结点*p,通常的做法是先从链表的头结点开始顺序找到其前驱,然后执行删
除操作。其实,删除结点*p的操作可用删除*p的后继来实现,实质就是将其后继的值赋予其自
身,然后再删除后继,也能使得时间复杂度为O(1)。该方法的主要代码片段如下:
q=p->next; //令q指向*p的后继结点
p->data=p->next->data; //用后继结点的数据域覆盖
p->next=q->next; //将*q结点从链中“断开”
free(q); //释放后继结点的存储空间
2.1.7头插法建立单链表
// 逆向建立单链表
LinkList List_HeadInsert(LinkList L) {
LNode *s;
int x; // 设元素类型为整型
/* 创建头结点 */
L = (LNode*)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);
}
// 执行 free(q)的作用是由系统回收一个 LNode 型结点,回收后的空间可供再次生成结点时用。
return L;
}
采用头插法建立单链表时,读入数据的顺序与生成的链表中元素的顺序是相反的,可用来实
现链表的逆置。每个结点插入的时间为O(1),设单链表长为n,则总时间复杂度为O(n)。
2.1.8尾插法建立单链表
// 正向建立单链表
LinkList list_TailInsert(LinkList &L) {
int x; // 设元素类型为整型
L = (LNode*)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;
}
因为附设了一个指向表尾结点的指针,所以时间复杂度和头插法的相同
3.1双链表
3.1.1插入操作
s->next=p->next; //将结点*s插入到结点*p之后
p->next->prior=s
s->prior=p;
p->next=s;
3.1.2删除操作
p->next=q->next; //图2.11 中步骤①
q->next->prior=p; //图 2.11 中步骤②
free(q); //释放结点空间
3.2循环链表
3.3静态列表
3.顺序表和链表的比较
1.存取(读/写)方式
顺序表可以顺序存取,也可以随机存取,链表只能从表头开始依次顺序存取。例如在第i个位置上执行存取的操作,顺序表仅需一次访问,而链表则需从表头开始依次访问i次。
2.逻辑结构与物理结构
采用顺序存储时,逻辑上相邻的元素,对应的物理存储位置也相邻。而采用链式存储时,逻辑上相邻的元素,物理存储位置不一定相邻,对应的逻辑关系是通过指针链接来表示的。
3.查找、插入和删除操作
对于按值查找,顺序表无序时,两者的时间复杂度均为O(n);顺序表有序时,可采用折半查找,此时的时间复杂度为O(log?n)。
对于按序号查找,顺序表支持随机访问,时间复杂度仅为O(1),而链表的平均时间复杂度为O(n)。顺序表的插入、删除操作,平均需要移动半个表长的元素。链表的插入、删除操作,只需修改相关结点的指针域即可。
4.空间分配
顺序存储在静态存储分配情形下,一旦存储空间装满就不能扩充,若再加入新元素,则会出现内存溢出,因此需要预先分配足够大的存储空间。预先分配过大,可能会导致顺序表后部大量闲置;预先分配过小,又会造成溢出。动态存储分配虽然存储空间可以扩充,但需要移动大量元素,导致操作效率降低,而且若内存中没有更大块的连续存储空间,则会导致分配失败。
链式存储的结点空间只在需要时申请分配,只要内存有空间就可以分配,操作灵活、高效。此外,由于链表的每个结点都带有指针域,因此存储密度不够大。
在实际中应该怎样选取存储结构呢?
1.基于存储的考虑
难以估计线性表的长度或存储规模时,不宜采用顺序表;
链表不用事先估计存储规模,但链表的存储密度较低,显然链式存储结构的存储密度是小于1的。
2.基于运算的考虑
在顺序表中按序号访问a,的时间复杂度为O(1),而链表中按序号访问的时间复杂度为O(n),
因此若经常做的运算是按序号访问数据元素,则显然顺序表优于链表。
在顺序表中进行插入、删除操作时,平均移动表中一半的元素,当数据元素的信息量较大且
表较长时,这一点是不应忽视的;在链表中进行插入、删除操作时,虽然也要找插入位置,但操
作主要是比较操作,从这个角度考虑显然后者优于前者。
3.基于环境的考虑
顺序表容易实现,任何高级语言中都有数组类型;链表的操作是基于指针的,相对来讲,前
者实现较为简单,这也是用户考虑的一个因素。
总之,两种存储结构各有长短,选择哪一种由实际问题的主要因素决定。通常较稳定的线性
表选择顺序存储,而频繁进行插入、删除操作的线性表(即动态性较强)宜选择链式存储
顺序表的主要优点:
①可进行随机访问,即可通过首地址和元素序号可以在O(1)时间内找到
指定的元素;
②存储密度高,每个结点只存储数据元素。
顺序表的缺点也很明显:
①元素的插入和删除需要移动大量的元素,插入操作平均需要移动n/2个元素,删除操作平均需要移动(n-1)/2个元素;
②顺序存储分配需要一段连续的存储空间,不够灵活。
第三章 栈、队列和数组
1.栈
1.1顺序栈
1.1.1顺序栈的实现
#define MaxSize 50 // 定义栈中元素的最大个数
typedef struct {
Elemtype data[MaxSize]; // 存放栈中元素
int top; // 栈顶指针
} SqStack;
1.1.2顺序栈的基本操作
(1)初始化
void Initstack(SqStack &S) {
s.top = -1; // 初始化栈顶指针
}
----------------------------------------------------------------------------------
(2)判栈空
bool StackEmpty(SqStack S) {
if (s.top == -1) // 栈空
return true;
else // 不空
return false;
}
----------------------------------------------------------------------------------
(3)进栈
bool Push(SqStack &S, ElemType x) {
if (S.top == MaxSize - 1) // 栈满,报错
return false;
S.data[++S.top] = x; // 指针先加 1,再入栈
return true;
}
----------------------------------------------------------------------------------
(4)出栈
bool Pop(SqStack &S, ElemType &x) {
if (S.top == -1) // 栈空,报错
return false;
x = S.data[S.top--]; // 先出栈,指针再减 1
return true;
}
----------------------------------------------------------------------------------
(5)读栈顶元素
bool GetTop(SqStack S, ElemType &x) {
if (S.top == -1) // 栈空,报错
return false;
x = S.data[S.top]; // x 记录栈顶元素
return true;
}
//仅为读取栈顶元素,并没有出栈操作,因此原栈顶元素依然保留在栈中。
1.2链栈
1.21栈的链式存储结构
typedef struct Linknode {
ElemType data; // 数据域
struct Linknode *next; // 指针域
} Listack; // 栈类型定义
1.3共享栈
2.队列
2.0队列的顺序存储
#define MaxSize 50
typedef struct {
// 定义队列中元素的最大个数
ElemType data[MaxSize];
// 用数组存放队列元素
int front, rear;
// 队头指针和队尾指针
} SqQueue;
2.1循环队列
2.1.1循环队列的操作
(1)初始化
void InitQueue(SqQueue &Q) {
Q.rear = Q.front = 0; // 初始化队首、队尾指针
}
---------------------------------------------------------------------
(2)判队空
bool isEmpty(SqQueue Q) {
if (Q.rear == Q.front) // 队空条件
return true;
else
return false;
}
---------------------------------------------------------------------
(3)入队
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; // 队尾指针加 1 取模
return true;
}
---------------------------------------------------------------------
(4)出队
bool DeQueue(SqQueue &Q, ElemType &x) {
if (Q.rear == Q.front) // 队空则报错
return false;
x = Q.data[Q.front];
Q.front = (Q.front + 1) % MaxSize; // 队头指针加 1 取模
return true;
}
2.2链式队列
2.2.1队列的链式存储
typedef struct LinkNode { // 链式队列结点
ElemType data;
struct LinkNode *next;
} LinkNode;
typedef struct { // 链式队列
LinkNode *front, *rear; // 队列的队头和队尾指针
} LinkQueue;
// 不带头结点时,当 Q.front==NULL 且 Q.rear==NULL 时,链式队列为空。
2.2.2链式队列的基本操作
(1)初始化
void InitQueue(LinkQueue &Q) { // 初始化带头结点的链队列
Q.front = Q.rear = (LinkNode*)malloc(sizeof(LinkNode)); // 建立头结点
Q.front->next = NULL; // 初始为空
}
----------------------------------------------------------------------------
(2)判队空
bool IsEmpty(LinkQueue Q) {
if (Q.front == Q.rear) // 判空条件
return true;
else
return false;
}
----------------------------------------------------------------------------
(3)入队
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; // 修改尾指针
}
----------------------------------------------------------------------------
(4)出队
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;
if (Q.rear == p)
Q.rear = Q.front; // 若原队列中只有一个结点,删除后变空
free(p);
return true;
}
2.3双端队列
3.数组
3.1一维数组
3.2多维数组:压缩存储、稀疏矩阵
总结
第五章 树与二叉树
1.二叉树
1.1概念:定义、存储结构
1.1.1基本术语
结点的度和树的度。
能往下分几个,度就是几。一般的数度为2
结点的深度、高度和层次。
高度:圈的层数
1.1.2树的性质
1)树的结点数n等于所有结点的度数之和加1.
2)度为m的树中第i层上至多有m-'个结点(i≥1)。
3)高度为h的m叉树至多有(m?-1)/(m-1)个结点。
4)度为m、具有n个结点的树的最小高度h为[log。(n(m-1)+1)1.
5)度为m、具有n个结点的树的最大高度h为n-m+1。
1.2操作
1.2.1三种遍历
1.先序遍历(PreOrder)根左右
void PreOrder(BiTree T) {
if (T!= NULL) { // 若树不为空
visit(T); // 访问根结点
PreOrder(T->lchild); // 递归遍历左子树
PreOrder(T->rchild); // 递归遍历右子树
}
}
2.中序遍历(InOrder)左根右
void InOrder(BiTree T) {
if (T!= NULL) { // 若树不为空
InOrder(T->lchild); // 递归遍历左子树
visit(T); // 访问根结点
InOrder(T->rchild); // 递归遍历右子树
}
}
3.后序遍历(PostOrder)左右根
void PostOrder(BiTree T) {
if (T!= NULL) { // 若树不为空
PostOrder(T->lchild); // 递归遍历左子树
PostOrder(T->rchild); // 递归遍历右子树
visit(T); // 访问根结点
}
}
1.2.2线索二叉树
typedef struct ThreadNode {
ElemType data; // 数据元素
struct ThreadNode *lchild, *rchild; // 左、右孩子指针
int ltag, rtag; // 左、右线索标志
} ThreadNode, *ThreadTree;
1.3应用
1.3.1并查集
并查集的结构定义如下
#define SIZE 100
int UFSets[SIZE]; // 集合元素数组(双亲指针数组)
---------------------------------------------------------------------------------------
(1)并查集的初始化操作
void Initial(int s[]) { // s 即为并查集
for (int i = 0; i < SIZE; i++) // 每个自成单元素集合
s[i] = -1;
}
---------------------------------------------------------------------------------------
(2)并查集的 Find 操作
在并查集 s 中查找并返回包含元素 x 的树的根。
int Find(int s[], int x) {
while (s[x] >= 0) // 循环寻找 x 的根
x = s[x];
return x; // 根的 s[]小于 0
}
判断两个元素是否属于同一集合,只需分别找到它们的根,再比较根是否相同即可。
---------------------------------------------------------------------------------------
(3)并查集的 Union 操作
求两个不相交子集合的并集。若将两个元素所在的集合合并为一个集合,则需要先找到两个
元素的根,再令一棵子集树的根指向另一棵子集树的根。
void Union(int s[], int Root1, int Root2) {
if (Root1 == Root2) return; // 要求 Root1 与 Root2 是不同的集合
s[Root2] = Root1; // 将根 Root2 连接到另一根 Root1 下面
}
Find 操作和 Union 操作的时间复杂度分别为 O(d) 和 O(1),其中 d 为树的深度。
查并集的优化
1.3.2哈夫曼树
2.树和森林
2.1概念:定义、存储结构
树的存储结构
1.双亲表示法
#define MAX_TREE_SIZE 100
typedef struct {
ElemType data; // 数据元素
int parent; // 双亲位置域
} PTNode; // 树的结点定义
typedef struct { // 树的类型定义
PTNode nodes[MAX_TREE_SIZE]; // 双亲表示
int n; // 结点数
} PTree; // 树中最多结点数
2.2操作
2.2.1与二叉树的转换
2.2.2遍历
2.3应用:并查集
第6章 图
6.1图的定义
6.2图结构的存储
6.2.1邻接矩阵法
#define MaxVertexNum 100 // 顶点数目的最大值
typedef char VertexType; // 顶点对应的数据类型
typedef int EdgeType; // 边对应的数据类型
typedef struct {
VertexType vex[MaxVertexNum]; // 顶点表
EdgeType edge[MaxVertexNum][MaxVertexNum]; // 邻接矩阵,边表
int vexnum, arcnum; // 图的当前顶点数和边数
} MGraph;
6.2.2邻接表法
#define MaxVertexNum 100 // 图中顶点数目的最大值
typedef struct ArcNode {
int adivexi; // 该弧所指向的顶点的位置
struct ArcNode *nextarc; // 指向下一条弧的指针
// InfoType info; // 网的边权值
} ArcNode; // 边表结点
typedef struct VNode { // 顶点表结点
VertexType data; // 顶点信息
ArcNode *firstarc; // 指向第一条依附该顶点的弧的指针
} VNode, AdjList[MaxVertexNum];
typedef struct {
AdjList vertices; // 邻接表
int vexnum, arcnum; // 图的顶点数和弧数
} ALGraph; // ALGraph 是以邻接表存储的图类型
6.2.3邻接多重表
6.2.4十字链表
图的四种存储方式的总结
6.3图的遍历
广度优先遍历
广度优先搜索算法的伪代码如下:
bool visited[MAX_VERTEX_NUM]; // 访问标记数组
void BFSTraverse(Graph G) { //对图g进行广度优先遍历
for (i = 0; i < G.vexnum; ++i)
visited[i] = FALSE; // 访问标记数组初始化
InitQueue(Q); // 初始化辅助队列 Q
for (i = 0; i < G.vexnum; ++i) // 从 0 号顶点开始遍历
if (!visited[i]) // 对每个连通分量调用一次 BFS()
BFS(G, i); // 若 v 未访问过,从 v 开始调用 BFS()
}
-----------------------------------------------------------------------------------
用邻接表实现广度优先搜索的算法如下:
void BFS(ALGraph G, int i) {
visit(i); // 访问初始顶点 i
visited[i] = TRUE; // 对 i 做已访问标记
EnQueue(Q, i); // 顶点 i 入队
while (!IsEmpty(Q)) {
DeQueue(Q, v); // 队首顶点 v 出队
for (p = G.vertices[v].firstarc; p; p = p->nextarc) { // 检测 v 的所有邻接点
w = p->adjvex;
if (visited[w] == FALSE) {
visit(w); // w 为 v 的尚未访问的邻接点,访问 w
visited[w] = TRUE; // 对 w 做已访问标记
EnQueue(Q, w); // 顶点 w 入队
}
}
}
}
-----------------------------------------------------------------------------------
用邻接矩阵实现广度优先搜索的算法如下:
void BFS(MGraph G, int i) {
visit(i); // 访问初始顶点 i
visited[i] = TRUE; // 对 i 做已访问标记
EnQueue(Q, i); // 顶点 i 入队
while (!IsEmpty(Q)) {
DeQueue(Q, v); // 队首顶点 v 出队
for (w = 0; w < G.vexnum; w++) // 检测 v 的所有邻接点
if (visited[w] == FALSE && G.edge[v][w] == 1) {
visit(w); // w 为 v 的尚未访问的邻接点,访问 w
visited[w] = TRUE; // 对 w 做已访问标记
EnQueue(Q, w); // 顶点 w 入队
}
}
}
// 辅助数组 visited[]标志顶点是否被访问过,其初始状态为 FALSE。在图的遍历过程
// 中,一旦某个顶点 v 被访问,就立即置 visited[i]为 TRUE,防止它被多次访问
深度优先遍历
bool visited[MAX_VERTEX_NUM]; // 访问标记数组
void DFSTraverse(Graph G) {
for (i = 0; i < G.vexnum; i++)
visited[i] = FALSE; // 初始化已访问标记数组
for (i = 0; i < G.vexnum; i++)
if (!visited[i])
DFS(G, i); // 本代码中是从 v?开始遍历,对尚未访问的顶点调用 DFS()
} // 对图 G 进行深度优先遍历
-----------------------------------------------------------------------------------
// 用邻接表实现深度优先搜索的算法如下:
void DFS(ALGraph G, int i) {
visit(i); // 访问初始顶点 i
visited[i] = TRUE; // 对 i 做已访问标记
for (p = G.vertices[i].firstarc; p; p = p->nextarc) { // 检测 i 的所有邻接点
j = p->adjvex;
if (visited[j] == FALSE)
DFS(G, j); // j 为 i 的尚未访问的邻接点,递归访问 j
}
}
-----------------------------------------------------------------------------------
// 用邻接矩阵实现深度优先搜索的算法如下:
void DFS(MGraph G, int i) {
visit(i); // 访问初始顶点 i
visited[i] = TRUE; // 对 i 做已访问标记
for (j = 0; j < G.vexnum; j++) { // 检测 i 的所有邻接点
if (visited[j] == FALSE && G.edge[i][j] == 1)
DFS(G, j); // j 为 i 的尚未访问的邻接点,递归访问 j
}
}
6.4图的相关应用
6.4.1最小生成树
Prim 算法
Kruskal 算法
最短路径:Dijkstra 算法、Floyd 算法
拓扑排序:
bool TopologicalSort(Graph G) {
Initstack(S); // 初始化栈,存储入度为 0 的顶点
int i;
for (i = 0; i < G.vexnum; i++) {
if (indegree[i] == 0)
Push(S, i); // 将所有入度为 0 的顶点进栈
}
// 计数,记录当前已经输出的顶点数
int count = 0;
while (!IsEmpty(S)) { // 栈不空,则存在入度为 0 的顶点
Pop(S, i);
print[count++] = i; // 输出顶点 i
for (p = G.vertices[i].firstarc; p; p = p->nextarc) {
// 将所有 i 指向的顶点的入度减 1,并且将入度减为 0 的顶点压入栈 S
v = p->adjvex;
if (!(--indegree[v]))
Push(S, v); // 入度为 0,则入栈
}
}
if (count < G.vexnum)
return false; // 排序失败,有向图中有回路
else
return true; // 拓扑排序成功
}
AOV 网
关键路径:AOE网
第7章 查找
7.1基本概念:静态查找、动态查找
7.2线性结构
7.2.1顺序查找
typedef struct { // 查找表的数据结构(顺序表)
ElemType *elem; // 动态数组基址
int TableLen; // 表的长度
} SSTable;
int Search_seq(SSTable ST, ElemType key) {
ST.elem[0] = key; // “哨兵”
for (int i = ST.TableLen; ST.elem[i]!= key; --i); // 从后往前找
return i; // 若查找成功,则返回元素下标;若查找失败,则返回 0
}
7.2.2折半查找
int Binary_search(SSTable L, ElemType key) {
int low = 0, high = L.TableLen - 1, 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; // 查找失败,返回 -1
}
7.2.3分块查找
7.3树形结构
7.3.1二叉排序树
二叉排序树的查找
二叉排序树的非递归查找算法:
BSTNode *BST_Search(BiTree T, ElemType key) {
while (T!= NULL && key!= T->data) { // 若树空或等于根结点值,则结束循环
if (key < T->data)
T = T->lchild; // 小于,则在左子树上查找
else
T = T->rchild; // 大于,则在右子树上查找
}
return T;
}
--------------------------------------------------------------------------------------
二叉排序树的插入
int BST_Insert(BiTree *T, KeyType k) {
if (*T == NULL) { // 原树为空,新插入的记录为根结点
*T = (BiTree)malloc(sizeof(BSTNode));
(*T)->data = k;
(*T)->lchild = (*T)->rchild = NULL;
return 1; // 返回 1, 插入成功
}
else if (k == (*T)->data) // 树中存在相同关键字的结点,插入失败
return 0;
else if (k < (*T)->data) // 插入*T 的左子树
return BST_Insert(&((*T)->lchild), k);
else // 插入*T 的右子树
return BST_Insert(&((*T)->rchild), k);
}
-------------------------------------------------------------------------------------
二叉排序树的构造
void Creat_BST(BiTree *T, KeyType str[], int n) {
*T = NULL; // 初始时 T 为空树
int i = 0;
while (i < n) { // 依次将每个关键字插入二叉排序树
BST_Insert(T, str[i]);
i++;
}
}
7.3.2二叉平衡树
7.3.3红黑树
7.3.4B树、B+树
7.4散列结构--散列表
7.4.1性能分析
7.4.2冲突处理
7.5效率指标--平均查找长度
7.5.1查找成功
7.5.2查找失败
第8章 排序
插入排序
直接插入排序
void InsertSort(ElemType A[], int n) {
int i, j;
for (i = 2; i <= n; i++) { // 依次将 A[2]~A[n]插入前面已排序序列
if (A[i] < A[i - 1]) { // 若 A[i]关键码小于其前驱,将 A[i]插入有序表
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]; // 复制到插入位置
}
}
}
折半插入排序
void InsertSort(ElemType A[], int n) {
int i, j, low, high, mid;
for (i = 2; i <= n; i++) { // 依次将 A[2]~A[n]插入前面的已排序序列
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]; // 插入操作
}
}
希尔排序
void ShellSort(ElemType A[], int n) {
// A[0]只是暂存单元,不是哨兵,当 j<=0 时,插入位置已到
int dk, i, j;
for (dk = n / 2; dk >= 1; dk = dk / 2) // 增量变化(无统一规定)
for (i = dk + 1; i <= n; ++i)
if (A[i] < A[i - dk]) { // 需将 A[i]插入有序增量子表
A[0] = A[i]; // 暂存在 A[0]
for (j = i - dk; j > 0 && A[0] < A[j]; j -= dk)
A[j + dk] = A[j]; // 记录后移,查找插入的位置
A[j + dk] = A[0]; // 插入
}
}
选择排序
简单选择排序
void SelectSort(ElemType A[], int n) {
for (int i = 0; i < n - 1; i++) { // 一共进行 n - 1 趟
int min = i; // 记录最小元素位置
// 在 A[i…n - 1]中选择最小的元素
for (int j = i + 1; j < n; j++)
if (A[j] < A[min])
min = j; // 更新最小元素位置
// 封装的 swap()函数共移动元素 3 次
if (min!= i)
swap(A[i], A[min]);
}
}
堆排序
void HeapSort(ElemType A[], int len) {
BuildMaxHeap(A, len); // 初始建堆
for (int i = len; i > 1; i--) { // n - 1 趟的交换和建堆过程
// 输出堆顶元素(和堆底元素交换)
Swap(A[i], A[1]);
HeadAdjust(A, 1, i - 1); // 调整,把剩余的 i - 1 个元素整理成堆
}
}
大根堆
void BuildMaxHeap (ElemType A[], int len) {
for (int i = len / 2; i > 0; i--) { // 从 i = [n/2] ~ 1,反复调整堆
HeadAdjust(A, i, len);
}
}
void HeadAdjust(ElemType A[], int k, int len) {
// 函数 HeadAdjust 对以元素 k 为根的子树进行调整
A[0] = A[k]; // A[0] 暂存子树的根结点
for (int i = 2 * k; i <= len; i *= 2) { // 沿 key 较大的子结点向下筛选
if (i < len && A[i] < A[i + 1])
i++; // 取 key 较大的子结点的下标
if (A[0] >= A[i])
break; // 筛选结束
else {
A[k] = A[i]; // 将 A[i] 调整到双亲结点上
k = i; // 修改 k 值,以便继续向下筛选
}
}
A[k] = A[0]; // 被筛选结点的值放入最终位置
}