一.绪论
1.什么是数据结构?
数据结构是一门研究非数值计算的程序设计问题中计算机的操作对象以及他们之间的关系和操作等的学科。
2.基本概念和术语。
数据:数据是信息的载体。是对客观事物的符号表示,在计算机科学中是指所有能输入到计算机中并被计算机程序处理的符号的总称。是计算机程序加工的“原料”。
数据元素:数据的基本单位,在计算机程序中通常作为一个整体进行考虑和处理。一个数据元素可由若干数据项组成,数据项是构成数据元素的不可分割的最小单位。
数据对象:性质相同的数据元素的集合,数据的一个子集。
数据结构:相互之间存在一种或多种特定关系的数据元素的集合。(这种关系叫做结构)
逻辑结构(四类基本结构):
1.集合。
2.线性结构。数据元素之间存在一个对一个的关系。
3.树形结构。数据元素之间存在一个对多个的关系。
4.图状结构或网状结构。数据元素之间存在多个对多个的关系。
存储结构(物理结构):数据结构在计算机中的表示(映射)。包括顺序映像和非顺序映像,因此可得到两种不同的存储结构:顺序存储结构和链式存储结构。
数据类型:一个值的集合和定义在此集合上的一组操作的总称。
1.原子类型。其值不可再分的数据类型。(比如:C语言的基本数据类型)
2.结构类型。其值可以再分解为若干成分(分量)的数据类型。(比如:数组)
抽象数据类型:一个数据模型以及定义在该模型上的一组操作。第2、3种可称为结构类型。
1.原子类型。
2.固定聚合类型。
3.可变聚合类型。
3.算法和算法分析。
算法:对特定问题求解步骤的一种描述,是指令的有限序列,每一条指令表示一个或多个操作。五大重要特性:
1.有穷性 2.确定性 3.可行性 4.数入 5.输出
算法设计的要求:
1.正确性 2.可读性 3.健壮性 4.效率与低存储量需求
算法效率的度量:时间复杂度和空间复杂度
二.线性表。
1.线性表的定义。
线性表是n个数据元素的有限序列。
2.线性表的顺序表示及实现。
线性表的顺序存储(顺序表):用一组地址连续的存储单元依次存储线性表的数据元素。
//一:静态初始化表
# define MaxSize 10 //定义最大长度
typedef struct{
int data[MaxSize]; //用静态数组存放数据元素
int length; //顺序表的当前长度
}SqList; //顺序表名称
void InitializeList(SqList &L){
for (int i = 0; i < MaxSize; i++)
{
L.data[i] = 0; //将所有数据元素初始化为0
}
L.length = 0;
}
//二:动态初始化表
#define InitSize 10 //默认的最大长度
typedef struct{
int *data; //指向动态分配数组的指针
int MaxSize; //顺序表的最大容量
int length; //顺序表的当前长度
}SeqList;
void InitializeList(SeqList &L){
//c语言用malloc函数申请一片连续的存储空间,返回一个指针变量
L.data = (int *)malloc(InitSize*Sizeof(int));
//c++中:L.data = new int(InitSize);
L.length = 0;
L.MaxSize = InitSize;
}
int main(){
SqList L; //声明一个顺序表
InitializeList(L) //初始化顺序表
......
}
3.顺序表的基本操作。
//一:静态初始化表
# define MaxSize 10 //定义最大长度
typedef struct{
int data[MaxSize]; //用静态数组存放数据元素
int length; //顺序表的当前长度
}SqList; //顺序表名称
//一:在顺序表位置i插入元素datas
//L为链表,i为需插入链表中的位置,datas为插入的值
bool InsertList(SqList L,int i,int datas){
if(i < 0 || i > L.length) return false; //i的位置范围是否有效
if(L.length >= maxSize) return false; //链表的长度是否超出最大长度
//顺序表后移
for(int j = L.length;j >= i;j--) L.data[j] = L.data[j-1];
//插入i位置
L.data[i-1] = datas;
//顺序表长度加一
L.length++;
return true;
}
//二:删除第i个位置元素
bool DeleteList(SqList L,int i){
if(i < 0 || i > L.length) return false; //i的位置范围是否有效
int temp = L.data[i-1];
//顺序表前移
for(int j = i;j <= L.length;j++) L.data[j-1] = L.data[j];
//顺序表长度减一
L.length--;
return true;
}
4.线性表的链式表示及实现。
线性表的链式存储(单链表):通过一组任意的的存储单元存储线性表的数据元素。每个数据元素的存储映像称之为结点,每个节点包括数据域和指针域。
typedef struct LNode{
int data; //数据域
struct LNode *next; //指针域
}LNode,*LinkList;
5.单链表的基本操作。
typedef struct LNode{
int data; //数据域
struct LNode *next; //指针域
int size; //表示链表长度
}LNode,*LinkList;
//头插法建立链表,length表示要插入的个数
LinkList insertList(LinkList &L,int length){
LNode *s;int data;
L = (LinkList)malloc(sizeof(LNode)); //建立头节点
L->next = null; //初始为空链表
for(int i = 0;i < length;i++){
scanf("请输入:%d",&data);
s = (LinkList)malloc(sizeof(LNode)); //创建新节点
//头插法
s.data = data;
s->next = L->next;
L->next = s;
}
L.size = length;
return L;
}
//尾插法建立链表,length表示要插入的个数
LinkList insertList(LinkList &L,int length){
LNode *s,*p;int datas;
L = (LinkList)malloc(sizeof(LNode)); //建立头节点
L->next = null; //初始为空链表
p = L;
for(int i = 0;i < length;i++){
scanf("请输入:%d",&data);
s = (LinkList)malloc(sizeof(LNode)); //创建新节点
//尾插法
s.data = datas;
p->next = s;
p = s;
}
p->next = null;
L.size = length;
return L;
}
//在位置i插入节点
bool insertNode(LinkList &L,int i,int datas){
if(i < 0 || i > L.size) return false; //i是否合理
LNode *s,*p = L;
for(int j = 0;j < i-1;j++){ //找到i位置前一个节点
p = p->next;
}
s = (LinkList)malloc(sizeof(LNode)); //创建新节点
s.data = datas;
s->next = p->next;
p->next = s;
L.size++;
return L;
}
//删除i位置节点
bool insertNode(LinkList &L,int i,int datas){
if(i < 0 || i > L.size) return false; //i是否合理
LNode *p = L,*temp;
for(int j = 0;j < i-1;j++){
p = p->next;
}
temp = p->next;
p->next = temp->next;
free(temp);
L.size--;
return L;
}
6.双链表的基本操作。
typedef struct LNode{
int data; //数据域
struct LNode *next,*front; //指针域
}LNode,*LinkList;
//建立双链表
LinkList insertList(LinkList &L,int length){
LNode *s;int data;
L = (LinkList)malloc(sizeof(LNode)); //建立头节点
L->next = null; //初始为空链表
L->front = null;
for(int i = 0;i < length;i++){
scanf("请输入:%d",&data);
s = (LinkList)malloc(sizeof(LNode)); //创建新节点
s.data = data;
//头插法
s->next = L->next;
if(!s->next) s->next->front = s;
s->front = L;
L->next = s;
}
return L;
}
//删除i位置节点
bool insertNode(LinkList &L,int i,int datas){
if(i < 0 || i > L.size) return false; //i是否合理
LNode *p = L,*temp;
for(int j = 0;j < i-1;j++){ //找到要删除的节点前一个节点p
p = p->next;
}
temp = p->next; //temp为要删除的节点
p->next = temp->next;
temp->next->front = p;
free(temp);
return L;
}
7.循环链表。
循环单链表:单链表中最后一个节点指向头节点。
循环双链表:双链表中最后一个节点next指针指向头节点,头节点的front指针指向最后一个节点。
三.栈和队列
1.栈的相关概念和操作。
栈:只允许在一端进行插入和删除操作,是一种操作受限制的线性表。。(后进先出)
1.顺序栈基本操作:
#define MAX 50 //栈的最大容量
typedef struct{
int data[MAX];
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,int x){
if(S.top == MAX-1){ //栈是否已满
return false;
}
S.data[++S.top] = x;
return true;
}
//出栈
bool pop(sqstack &S , int &x){
if(S.top == -1){ //栈是否为空
return false;
}
x = S.data[S.top--];
return true;
}
2.链栈的基本操作:
具体实现和单链表差不多,进栈和出栈在表头进行。
2.队列的相关概念和操作。
队列:只允许在表的前端(front)进行删除操作,而在表的后端(rear)进行插入操作,是一种操作受限制的线性表。进行插入操作的端称为队尾,进行删除操作的端称为队头。队列中没有元素时,称为空队列。(先进先出)
1.队列的顺序存储及操作:
#define maxSize 50 //定义队列最大容量
typedef struct{
int queue[maxSize];
int rear; //队尾
int front; //对头
}ArrayQueue; //队列结构体
//初始化队列
void QueueInitiate(ArrayQueue Q){
Q->front = Q->rear = -1; //初始化队尾 对头
}
//判断队列满
int isFull(ArrayQueue Q){
if(Q.rear == maxSize - 1)
return 1; //队列满返回1
else
return 0; //队列没满返回0
}
//判断队列是否空
int isEmpty(ArrayQueue Q){
if(Q.rear == Q.front)
return 1; //队列空返回1
else
return 0; //队列不空返回0
}
//添加数据到队列
int addQueue(ArrayQueue Q, int n) {
if(Q->rear == maxSize - 1) { //判断队列是否满,满了就无法添加数据
return 0;
}
Q.rear++; //rear后移
Q.queue[Q.rear] = n;
return 1;
}
//数据出队列, 取数据
int getQueue(ArrayQueue Q, int d){
//判断队列是否空
if(Q.rear == Q.front){
return 0;
}
Q.front++; //front后移
d = Q.queue[Q.front]; //将数据传给d,让d带回main函数
Q.queue[Q.front] = 0; //取出后赋值为0
return 1;
}
2.循环队列的基本操作
#define maxSize 50 //定义队列最大容量
typedef struct{
int queue[maxSize];
int rear; //队尾
int front; //对头
}ArrayQueue; //队列结构体
//初始化队列
void QueueInitiate(ArrayQueue Q){
Q->front = Q->rear = 0; //初始化队尾 对头
}
//判断队列满
int isFull(ArrayQueue Q){
if((Q.rear+1)%maxSize == Q.font) return 1; //队列满返回1
else return 0; //队列没满返回0
}
//判断队列是否空
int isEmpty(ArrayQueue Q){
if(Q.rear == Q.front) return 1; //队列空返回1
else return 0; //队列不空返回0
}
//添加数据到队列
int addQueue(ArrayQueue Q, int n) {
if((Q.rear+1)%maxSize == Q.font) { //判断队列是否满
return 0;
}
Q.rear++; //rear后移
Q.queue[Q.rear] = n;
return 1;
}
//数据出队列, 取数据
int getQueue(ArrayQueue Q, int d){
if(Q.rear == Q.front) return 0; //判断队列是否空
d = Q.queue[Q.front]; //将数据传给d,让d带回main函数
Q.queue[Q.front] = 0; //取出后赋值为0
Q.front = (Q.front+1)%maxSize; //front后移
return 1;
}
3.队列的链式存储。
typedef struct QNode
{
int data;
struct QNode *next;
}QNode,*QueuePtr;
typedef struct
{
QueuePtr front,rear; //指向头结点的front和指向队尾的rear
}LinkQue,*LinkQueue;
//初始化队列
void InitQueue(LinkQueue &Q)
{
Q=(LinkQueue)malloc(sizeof(LinkQue));
QueuePtr s=(QueuePtr)malloc(sizeof(QNode));
Q->front=s;
Q->rear=s;
}
//若队列为空返回true,否则返回false
bool QueueEmpty(LinkQueue &Q)
{
return Q->rear==Q->front;
}
//插入e到队尾
bool EnQueue(LinkQueue &Q,int e)
{
QueuePtr s=(QueuePtr)malloc(sizeof(QNode));
if(!s) return false;
s->data=e;
s->next=NULL;
Q->rear->next=s;
Q->rear=s;
retrurn true;
}
//出队
bool DeQueue(LinkQueue &Q,int e)
{
if (QueueEmpty(Q)) return false;
QueuePtr p = Q->front->next;
e = p->data;
Q->front->next = p->next;
if (Q->rear == p)
{
Q->rear = Q->front;
}
free(p);
return true;
}
3.串的相关概念和操作。
串( string):是由零个或多个字符组成的有限序列,又名叫字符串。
#define MAXLEN 255 //预定义最大串长为255
//定长顺序存储表示
typedef unsigned char SString[MAXLEN + 1]; //每个分量存储一个字符,0号存放串的实际长度
//堆分配存储表示
typedef struct{
char *ch; //按串长分配存储区,ch指向串的基地址
int length; //串的长度
}HString;
//块链存储表示(类似于线性表的链式存储结构)
串的模式匹配:子串的定位操作通常称为 串的模式匹配
#define MAXLEN 255 //预定义最大串长为255
//定长顺序存储表示
typedef unsigned char SString[MAXLEN + 1]; //每个分量存储一个字符,0号存放串的实际长度
//简单的模式匹配算法,S:主串,T:模式串,返回S匹配的起始位置,最坏时间复杂度为O ( n m )
int Index(SString S, SString T){
int i = 1, j = 1; //i为S下标,j为T下标.从1开始
while(i <= S[0]&& j <= T[0]){
if(S[i] == T[j]){
++i; ++j; //继续比较后继字符
}else{
//指针后退重新开始匹配
i = i-j+2;
j = 1;
}
}
if(j > T[0]){
return i - T[0];
}else{
return 0;
}
}
//KMP算法,寻找next数组
void get_next(SString T, int next[]){
int i = 1, j = 0;
next[1] = 0;
while (i < T[0]){
if(j == 0 || T[i] == T[j]){ //[i]表示后缀的单个字符,[j]表示前缀的单个字符
++i; ++j;
next[i] = j; //若pi = pj, 则next[j+1] = next[j] + 1
}else{
j = next[j]; //否则令j = next[j],j值回溯,循环继续
}
}
}
//KMP算法
int Index_KMP(SString S, SString T){
int i=1, j=1;
int next[255]; //定义next数组
get_next(T, next); //得到next数组
while(i <= S[0] && j <= T[0]){
if(j==0 || S.[i] == T[j]){ //字符相等则继续
++i; ++j;
}else{
j = next[j]; //模式串向右移动,i不变
}
}
if(j > T[0]){
return i - T[0]; //匹配成功
}else{
return 0;
}
}
四.数组和广义表。
1.数组的定义。
数组:数组是由n (n> 1)个相同类型的数据元素构成的有限序列,每个数据元素称为一个数组元素,每个元素在n个线性关系中的序号称为该元素的下标,下标的取值范围称为数组的维界。
数组与线性表的关系:数组是线性表的推广。一维数组可视为一个线性表;二维数组可视为
其元素也是定长线性表的线性表,以此类推。数组一旦被定义,其维数和维界就不再改变。因此,
除结构的初始化和销毁外,数组只会有存取元素和修改元素的操作。
2.矩阵的压缩存储。
压缩存储:指为多个值相同的元素只分配一个存储空间,对零元素不分配存储空间。其目的
是节省存储空间。
特殊矩阵:指具有许多相同矩阵元素或零元素,并且这些相同矩阵元素或零元素的分布有一
定规律性的矩阵。常见的特殊矩阵有对称矩阵、上(下)三角矩阵、对角矩阵等。
特殊矩阵的压缩存储方法:找出特殊矩阵中值相同的矩阵元素的分布规律,把那些呈现规律
性分布的、值相同的多个矩阵元素压缩存储到一个存储空间中。
对称矩阵:a[i][j]
三角矩阵:上三角区的所有元素均为同一常量
稀疏矩阵: 矩阵中非零元素的个数,相对矩阵元素的个数来说非常少。因此,将非
零元素及其相应的行和列构成一个三元组(行标,列标,值),然后按照某种规律存储这些三元组。稀疏矩阵压缩存储后便失去了随机存取特性。稀疏矩阵的三元组既可以采用数组存储,也可以采用十字链表法存储。
#define maxsize 100
struct node
{
int i,j; //定义三元组的行、列号
int v; //三元组的值
}sparmatrix;
sparmatrix stu[maxsize];
//压缩存储
void compress()
{
int t = 0;
for(int r = 0;r < m;r++)
{
for(int c = 0;c < n;c++)
{
if(a[r][c] != 0) //只存储不为零的元素
{
stu[t].i = r; //行下标
stu[t].j = c; //列下标
stu[t].value = a[r][c];//压缩后矩阵的值用二维数组的下标表示
t++;
}
}
}
}
3.广义表。
广义表(Lists,又称列表):是一种非线性的数据结构,是线性表的一种推广。即广义表中放松对表元素的原子限制,容许它们具有其自身结构(即可以有子表)。
广义表是n个元素a1,a2,a3…. an组成的有限序列,通常记为:LS = (a1,a2,a3…. an);其中,LS代表表名,ai 代表表中元素即为表元素,其既可以为单个原子元素又可以是一个广义表,当ai为为广义表是成为LS的一个子表。
表的深度:表展开后所含括号的层数;
表的长度:长度为第一层的元素个数(原子和子表都只算一个);
表头:表的第一个表元素(a1)。
表尾:其它表元素组成的表(a2,a3 ……an)。
广义表的存储结构: 由于表元素的特殊性,通常采用链式存储,而不采用顺序存储,由于可能会有两种表元素,所以需要采用两种结构的节点,分别为原子节点和表结点。
Typedef enum {ATOM,LIST} ElemTag;
Typedef struct GLNode
{
ElemTag tag; //tag为0表示当前为单个数据元素,为1表示当前为子广义表
//存放数据项,为节省类型使用union共用体,当单个节点时使用data域,子广义表时使用slist域
Union{
ElemType data; //ElemType 自己定义的数据类型如int
Struct {
struct GLNode *hp,*tp; //hp tp 表头表尾指针
}sublist;
};
}*Glist;
//求广义表的长度
int GLisitLength (Glist L)
{
if (!L)
return 0;
return 1+GLisitLength(L->ptr.tp);
}
//求广义表的深度
int GLisitDepth (Glist L)
{
if (!L)
return 1;
if (L->tag == 0)
return 0;
for (maxL = 0,p = L;p;p = p->ptr.tp)
{
dep = GLisitDepth(p->ptr.hp);
if (dep > maxL)
max = dep;
}
return maxL+1;
}
五.树和二叉树。
1.二叉树的概念、相关术语、性质及存储结构。
树:是一种非线性的数据结构,它是由n(n>=0)个有限结点组成一个具有层次关系的集合。也是一种逻辑结构、分层结构。
树的特点:
- 有一个特殊的结点,称为根结点,根节点没有前驱结点。
- 除根节点外,其余结点被分成M(M>0)个互不相交的集合T1、T2、……、Tm,其中每一个集合Ti(1<= i <= m)又是一棵结构与树类似的子树。每棵子树的根结点有且只有一个前驱,可以有0个或多个后继。
- 树是递归定义的。
树的性质:
- 树中的结点数 = 所有结点的度数 + 1
- 度为m的树中,第i层上至多有mi-1个结点 m*m*(第 i-2 层的结点个数) = … = m^(i-1)*1(第1层的结点数)= mi-1(共乘了i次)。
- 高度为h的m叉树至多有(m^h-1)/(m-1)个结点 S = m^(h-1) + m^(h-2) + m^(h-3) + … + m + 1 = (m^h-1)/(m-1)
- 具有n个结点的m叉树的最小高度为 logm (n(m-1)+1) 如: n ≤ (mh-1)/(m-1)
最小高度h也可以为 logm((n-1)(m-1)+1)+ 1 如:(mh-1-1)/(m-1) +1≤ n ≤ (mh-1)/(m-1)
基本术语:
节点的度:一个节点含有的子树的个数称为该节点的度;
叶节点或终端节点:度为0的节点称为叶节点;
非终端节点或分支节点:度不为0的节点;
双亲节点或父节点:若一个节点含有子节点,则这个节点称为其子节点的父节点;
孩子节点或子节点:一个节点含有的子树的根节点称为该节点的子节点;
兄弟节点:具有相同父节点的节点互称为兄弟节点;
树的度:一棵树中,最大的节点的度称为树的度;
节点的层次:从根开始定义起,根为第1层,根的子节点为第2层,以此类推;
树的高度或深度:树中节点的最大层次;
堂兄弟节点:双亲在同一层的节点互为堂兄弟;
节点的祖先:从根到该节点所经分支上的所有节点;
子孙:以某节点为根的子树中任一节点都称为该节点的子孙。
森林:由m(m>0)棵互不相交的树的集合称为森林;
二叉树:是n(n≥0)个结点的有限集合,该集合或者为空集(空二叉树),或者由一个根结点和两棵互不相交的,分别称为根结点的左子树和右子树的二叉树组成。
满二叉树:树中所有的分支结点都存在左子树和右子树,所有的叶子都在同一层上,这样的树叫做满二叉树。
完全二叉树:深度为k的具有n个结点的二叉树,当且仅当其每一个结点都与深度为k的满二叉树中编号为1~n的结点一一对应时,称之为完全二叉树。
1.叶子只可能分布在层次最大的两层上。
2.对任一结点,如果其右子树的最大层次为i,则其左子树的最大层次必为i或i+1。
二叉树的存储结构:二叉树一般可以使用两种结构存储,一种顺序结构,一种链式结构。
二叉树的性质:
- 在二叉树的第i层上至多有2^(i-1)个结点(i>=1)。
- 深度为k的二叉树至多有2^k-1个结点(k>=1).
- 对任何一棵二叉树T,如果叶子数为n0,度为2的结点数为n2,则n0=n2+1
- 具有n个结点的完全二叉树的深度为⎿log2n⏌+1
- 如果对一棵树有n个结点的完全二叉树(深度为⎿log2n⏌+1)的结点按层序编号(从第一层到第⎿log2n⏌+1层,每层从左到右),则对任意结点i(1<=i<=n),有:
- 如果i=1,则结点i是二叉树的根,无双亲;如果i>1,则其双亲是结点⎿i/2⏌。
- 如果2i>n,则结点i为叶子结点,无左孩子;否则,其左孩子是结点2i。
- 如果2i+1>n,则结点i无右孩子;否则,其右孩子是结点2i+1。
1.顺序结构存储就是使用 数组来存储,一般使用数组 只适合表示完全二叉树,因为不是完全二叉树会有空间的浪费。 二叉树顺序存储在物理上是一个数组,在逻辑上是一颗二叉树。
2.二叉树的链式存储结构是指,用 链表来表示一棵二叉树,即用链来指示元素的逻辑关系。 通常的方法是链表中每个结点由三个域组成,数据域和左右指针域,左右指针分别用来给出该结点左孩子和右孩子所在的链结点的存储地址 。
typedef int BTDataType;
// 二叉链
struct BinaryTreeNode
{
struct BinTreeNode* _pLeft; // 指向当前节点左孩子
struct BinTreeNode* _pRight; // 指向当前节点右孩子
BTDataType _data; // 当前节点值域
}
// 三叉链
struct BinaryTreeNode
{
struct BinTreeNode* _pParent; // 指向当前节点的双亲
struct BinTreeNode* _pLeft; // 指向当前节点左孩子
struct BinTreeNode* _pRight; // 指向当前节点右孩子
BTDataType _data; // 当前节点值域
};
2.二叉树的遍历算法及应用。
typedef char tree_datatype;
typedef struct tree_t
{
tree_datatype data; //数据域
struct tree_t *lchild; //指向左子树的结构体指针
struct tree_t *rchild; //指向右子树的结构体指针
}bitree_t;
//1.创建一个树
bitree_t *CreateBitree(void)
{
char ch;
bitree_t *r = NULL; //将要建立的节点
scanf("%c",&ch);
if(ch == '#')
{
return NULL;
}
r = (bitree_t *)malloc(sizeof(bitree_t));
if(NULL == r)
{
printf("create fail\n");
}
r->data = ch;
r->lchild = CreateBitree(); //递归建立
r->rchild = CreateBitree();
return r;
}
//2.前序遍历树
void PreShowBitree(bitree_t *r)
{
if(r == NULL)
{
return;
}
printf("%c",r->data);
PreShowBitree(r->lchild);
PreShowBitree(r->rchild);
}
//3.中序遍历树
void midShowBitree(bitree_t *r)
{
if(r == NULL)
{
return;
}
midShowBitree(r->lchild);
printf("%c",r->data);
midShowBitree(r->rchild);
}
//4.后序遍历树
void PostShowBitree(bitree_t *r)
{
if(r == NULL)
{
return;
}
PostShowBitree(r->lchild);
PostShowBitree(r->rchild);
printf("%c",r->data);
}
//5.层次遍历,借助队列先进先出的特点
void levelShowBitree(bitree_t *r)
{
if (r == NULL)
{
return;
}
queue_t *p = CreateQueue(); //创建队列
IntoQueue(p,r); //根节点入队
while(!isEpQueue(p))
{
r = OutQueue(p); //节点出队
printf("%c",r->data);
if(r->lchild != NULL)
{
IntoQueue(p,r->lchild);
}
if(r->rchild != NULL)
{
IntoQueue(p,r->rchild);
}
}
}
复制二叉树(利用先序遍历)
Status Copy(bitree_t T,bitree_t &NewT){
if(T == NULL){ //如果是空树递归结束
NewT = NULL;
return 0;
}else{
NewT = new bitree_t ;
NewT->data = T->data; //复制根结点
Copy(T->lchild,NewT->lchild); //递归创建左子树
Copy(T->rchild,NewT->rchild); //递归创建右子树
}
}
3.线索二叉树。
线索二叉树:利用二叉链表中的空指针域,如果某个结点的左孩子为空,则将空的左孩子指针域改为指向其前驱;如果某结点的右孩子为空,则将空的右孩子指针域改为指向其后继,这种改变指向的指针称为"线索"。加上了线索的二叉树称为线索二叉树。
线索二叉树的结点结构为:
ltag为 0是指向该结点的左孩子,为 1时指向该结点的前驱
rtag为 0是指向该结点的右孩子,为 1时指向该结点的后继
lchild ltag data rtag rchild typedef struct BiThrNode{ int data; int ltag,rtag; struct BiThrTree *lchild,rchild; }BiThrNode,*BiThrTree;
线索二叉树的操作:
typedef struct BiThrNode{
int data;
int ltag,rtag;
struct BiThrTree *lchild,rchild;
}BiThrNode,*BiThrTree;
ThTree pre = NULL; // 全局变量,记录前驱
<!--------------------------------------------------------------------------!>
/* 中序线索二叉树线索化 2,后序改变该函数遍历次序就可以*/
void inOrderThTree(BiThrTree BT){
if (BT){
inOrderThTree(BT->lchild); // 遍历左子树
visit(BT);
inOrderThTree(BT->rchild); // 遍历右子树
}
}
/* 中序线索二叉树线索化 3 */
void visit(BiThrNode p){
if (!BT->lchild){ // 没有左孩子时
BT->ltag = 1; // 标记tag
BT->lchild = pre; // 前驱
}
if (pre && !pre->rchild){ // 前驱不为空且前驱的右孩子为空
pre->rchild = BT;
pre->rtag = 1;
}
pre = BT; // 更新前驱为当前节点
}
/* 中序线索二叉树线索化 1 */
void createBiThrTree(BiThrTree BT){
if(BT){
inOrderThTree(BT);
if(!pre->rchild){ //解决最后一个节点后序问题
pre->rtag = 1;
}
}
}
<!--------------------------------------------------------------------------!>
/* 前序线索二叉树线索化 2,*/
void inOrderThTree(BiThrTree BT){
if (BT){
visit(BT);
if(!BT->ltag) inOrderThTree(BT->lchild); // 遍历左子树,解决一直循环问题
inOrderThTree(BT->rchild); // 遍历右子树
}
}
/* 前序线索二叉树线索化 3 */
void visit(BiThrNode p){}
/* 前序线索二叉树线索化 1 */
void createBiThrTree(BiThrTree BT){}
4.树和森林。
1.三种树的存储结构。
1.双亲表示法:用一个一组连续的空间存储每个结点,数组的下标就是结点双亲的位置,每个结点包括一个数据与与指向父亲结点的数组下标的域。
2.孩子链表表示法:用一个线性表来存储树的所有结点信息,称为结点表。每个结点建立一个孩子表,孩子表中只存储孩子结点的地址信息(可以是指针、数组下标或者内存地址)。
3.孩子兄弟表示法:又称二叉树表示法,每个结点除了数据域外,还包含第一个孩子和右邻兄弟 。
<!---------------------树的双亲表节点存储表示--------------->
typedef struct PTNode{
char data;
int parent;
}PTNode;
// 树结构
# define MAX_TREE_SIZE 100
typedef struct{
PTNode nodes[MAX_TREE_SIZE];
int r,n; //根的位置和节点数
}PTree;
<!---------------------树的孩子链表存储表示--------------->
# define MAX_TREE_SIZE 100
// 孩子节点
typedef struct CTNode{
int child;
struct CTNode *next;
}*ChildPtr;
// 双亲结点
typedef struct{
char data;
ChildPtr firstchild;
}CTBox;
// 树结构
typedef struct{
CTBox node[MAX_TREE_SIZE];
int r,n; //根的位置和节点数
}CTree;
<!---------------------树的孩子兄弟存储表示--------------->
typedef struct CSNode{
char data;
struct CSNode *firstchild,*nextsibling;
}CSNode,*CsTree;
2.二叉树和树之间的转换。
将树转换成二叉树:
加线:在兄弟之间加一连线
抹线:对每个结点,除了其左孩子,去除其余孩子之间的关系
旋转:以树的根结点为轴心,整树顺时针转45°
将二叉树变成树:
加线:若p结点是双亲的左孩子,则将p的右孩子,右孩子的右孩子……沿分支找到的所有右孩子,都与p的双亲用线连起来
抹线:抹掉原二叉树中双亲与右孩子之间的连线
调整:将节点按层次排列,形成树结构。
二叉树变树:左孩右右连双亲,去掉原来右孩线。
森林变二叉树:
二叉树与多颗树之间的关系:
将各课树分别转换成二叉树
将每棵树的根节点用线相连
以第一棵树根结点为二叉树的根,再以根结点为轴心,顺时针旋转,构成二叉树型结构
森林变二叉树:森变二叉根相连。
二叉树变成森林:
抹线:将二叉树中根节点与其右孩子的连线,及沿右分支搜索到的所有右孩子间连线全部抹掉,使之变成孤立的二叉树。
还原:将孤立的二叉树还原成树。
二叉树变森林:去掉全部右孩线,孤立二叉再还原。
3.树和森林的遍历。
树:
先根遍历
如果树非空,则
先访问根节点
,然后按从左向右的顺序,先跟遍历根节点的每一棵子树。(像是对树进行类似二叉树的先序遍历)树的先根遍历顺序与该树
对应的二叉树
的先序遍历顺序相同。后根遍历
- 如果树非空,则按从左向右的顺序,后根遍历根节点的每一棵子树,
然后访问根节点
。(像是对树进行类似二叉树的后序遍历步骤)- 树的后根遍历顺序与该树
对应的二叉树
的中序遍历顺序相同。森林:
先序遍历:
若森林不空,则:
访问森林中第一棵树的根节,先序遍历森林中第一棵树的子树森林
先序遍历森林中(除第一棵树之外)其余树构成的森林。
- 森林的先序遍历顺序与该森林
对应的二叉树
的先序遍历顺序相同。即:依次从左至右对森林中的每一棵树进行先根遍历。
中序遍历
若森林不空,则
中序遍历森林中第一棵树的子树森林。
访问森林中第一棵树的根节点;
中序遍历森林中(除第一棵树之外)其余树构成的森林
即:依次从左至右对森林中的每一棵树进行后根遍历。
4.哈夫曼(Huffman)树的构造及应用。
专业术语:
- 在一棵树中,从一个结点往下可以达到的结点之间的通路,称为路径。
- 某一路径所经过的“边”的数量,称为该路径的路径长度。
- 若将树中结点赋给一个带有某种含义的数值,则该数值称为该结点的权。从根结点到该结点之间的路径长度与该结点的权的乘积,称为该结点的带权路径长度。
- 树的带权路径长度规定为所有叶子结点的带权路径长度之和,记为WPL。
哈夫曼树:给定n个权值作为n个叶子结点,构造一棵二叉树,若该树的带权路径长度达到最小,则称该二叉树为哈夫曼树,也被称为最优二叉树。
应该尽可能地让权值大的叶子结点靠近根结点,让权值小的叶子结点远离根结点,这样便能使得这棵二叉树的带权路径长度达到最小。
哈夫曼编码:将所有左分支都标记为0,所有右分支都标记为1。
typedef double DataType; //结点权值的数据类型
typedef struct HTNode //单个结点的信息
{
DataType weight; //权值
int parent; //父节点
int lc, rc; //左右孩子
}*HuffmanTree;
typedef char **HuffmanCode; //字符指针数组中存储的元素类型
//在下标为1到i-1的范围找到权值最小的两个值的下标,其中s1的权值小于s2的权值
void Select(HuffmanTree& HT, int n, int& s1, int& s2)
{
int min;
//找第一个最小值
for (int i = 1; i <= n; i++)
{
if (HT[i].parent == 0)
{
min = i;
break;
}
}
for (int i = min + 1; i <= n; i++)
{
if (HT[i].parent == 0 && HT[i].weight < HT[min].weight)
min = i;
}
s1 = min; //第一个最小值给s1
//找第二个最小值
for (int i = 1; i <= n; i++)
{
if (HT[i].parent == 0 && i != s1)
{
min = i;
break;
}
}
for (int i = min + 1; i <= n; i++)
{
if (HT[i].parent == 0 && HT[i].weight < HT[min].weight&&i != s1)
min = i;
}
s2 = min; //第二个最小值给s2
}
//构建哈夫曼树
void CreateHuff(HuffmanTree& HT, DataType* w, int n)
{
int m = 2 * n - 1; //哈夫曼树总结点数
HT = (HuffmanTree)calloc(m + 1, sizeof(HTNode)); //开m+1个HTNode,因为下标为0的HTNode不存储数据
for (int i = 1; i <= n; i++)
{
HT[i].weight = w[i - 1]; //赋权值给n个叶子结点
}
for (int i = n + 1; i <= m; i++) //构建哈夫曼树
{
//选择权值最小的s1和s2,生成它们的父结点
int s1, s2;
Select(HT, i - 1, s1, s2); //在下标为1到i-1的范围找到权值最小的两个值的下标,其中s1的权值小于s2的权值
HT[i].weight = HT[s1].weight + HT[s2].weight; //i的权重是s1和s2的权重之和
HT[s1].parent = i; //s1的父亲是i
HT[s2].parent = i; //s2的父亲是i
HT[i].lc = s1; //左孩子是s1
HT[i].rc = s2; //右孩子是s2
}
//打印哈夫曼树中各结点之间的关系
printf("哈夫曼树为:>\n");
printf("下标 权值 父结点 左孩子 右孩子\n");
printf("0 \n");
for (int i = 1; i <= m; i++)
{
printf("%-4d %-6.2lf %-6d %-6d %-6d\n", i, HT[i].weight, HT[i].parent, HT[i].lc, HT[i].rc);
}
printf("\n");
}
//生成哈夫曼编码
void HuffCoding(HuffmanTree& HT, HuffmanCode& HC, int n)
{
HC = (HuffmanCode)malloc(sizeof(char*)*(n + 1)); //开n+1个空间,因为下标为0的空间不用
char* code = (char*)malloc(sizeof(char)*n); //辅助空间,编码最长为n(最长时,前n-1个用于存储数据,最后1个用于存放'\0')
code[n - 1] = '\0'; //辅助空间最后一个位置为'\0'
for (int i = 1; i <= n; i++)
{
int start = n - 1; //每次生成数据的哈夫曼编码之前,先将start指针指向'\0'
int c = i; //正在进行的第i个数据的编码
int p = HT[c].parent; //找到该数据的父结点
while (p) //直到父结点为0,即父结点为根结点时,停止
{
if (HT[p].lc == c) //如果该结点是其父结点的左孩子,则编码为0,否则为1
code[--start] = '0';
else
code[--start] = '1';
c = p; //继续往上进行编码
p = HT[c].parent; //c的父结点
}
HC[i] = (char*)malloc(sizeof(char)*(n - start)); //开辟用于存储编码的内存空间
strcpy(HC[i], &code[start]); //将编码拷贝到字符指针数组中的相应位置
}
free(code); //释放辅助空间
}
六.图。
1.图的定义与术语。
图:图(Graph)是由顶点的有穷非空集合V ( G ) 和顶点之间边的集合E ( G ) 组成。通常表示为: G = ( V , E ) 。G 表示图,V 是图G 中顶点的集合,E 是图G 中边的集合。
线性表可以是空表,树可以是空树,但图不可以是空图。就是说,图中不能一个顶点也没有,图的顶点集V一定非空,但边集E可以为空,此时图中只有顶点而没有边。
有向图:也若E是有向边(称弧)的有限集合时,则图G为有向图。弧是顶点的有序对,记为<v, w>,其中v,w是顶点,v称为弧尾,w称为弧头,<v,w>称为从顶点v到顶点w的弧,也称v邻接到w,或w邻接自v。
无向图:若E是无向边(简称边)的有限集合时,则图G为无向图。边是顶点的无序对,记为(v, w)或(w,v)。
简单图:一个图G若满足:①不存在重复边;②不存在顶点到自身的边。则称图G为简单图。
多重图:若图G 中某两个结点之间的边数多于一条,又允许顶点通过同一条边和自己关联,则G 为多重图。
完全图(也称简单完全图):对于无向图,∣ E ∣的取值范围是0 到n ( n − 1 ) / 2 ,有n ( n − 1 ) / 2 条边的无向图称为完全图,在完全图中任意两个顶点之间都存在边。对于有向图,∣ E ∣ 的取值范围是0 到n ( n − 1 ) ,有n ( n − 1 ) 条弧的有向图称为有向完全图,在有向完全图中任意两个顶点之间都存在方向相反的两条弧。
子图:设有两个图G = ( V , E ) 和G ′ = ( V ′ , E ′ ),若V '是V的子集,且E ′ 是E的子集,则称G ′ 是G的子图。
连通、连通图和连通分量:在无向图中,若从顶点v到顶点w有路径存在,则称v和w是连通的。若图G中任意两个顶点都是连通的,则称图G为连通图,否则称为非连通图。无向图中的极大连通子图称为连通分量。极大即要求该连通子图包含其所有的边;极小连通子图是既要保持图连通又要使得边数最少的子图。
强连通图、强连通分量:在有向图中,若从顶点v 到顶点w 和 从顶点w到项点v之间都有路径,则称这两个顶点是强连通的。若图中任何一对顶点都是强连通的,则称此图为强连通图。有向图中的极大强连通子图称为有向图的强连通分量。
生成树、生成森林 :连通图的生成树是包含图中全部顶点的一个极小连通子图。若图中顶点数为n,则它的生成树含有n − 1条边。
在非连通图中,连通分量的生成树构成了非连通图的生成森林。
顶点的度、入度和出度
边的权和网:在一个图中,每条边都可以标上具有某种含义的数值,该数值称为该边的权值。这种边上带有权值的图称为带权图,也称网。
稠密图、稀疏图:边数很少的图称为稀疏图,反之称为稠密图。
路径、路径长度和回路
简单路径、简单回路:在路径序列中,顶点不重复出现的路径称为简单路径。除第一个顶点和最后一个顶点外,其余顶点不重复出现的回路称为简单回路。
距离:从顶点u出发到顶点v 的最短路径若存在,则此路径的长度称为从u 到v 的距离。若从u到v 根本不存在路径,则记该距离为无穷( ∞ )。
有向树:一个顶点的入度为0、其余顶点的入度均为1的有向图,称为有向树。
2.图的存储结构。
1.邻接矩阵。
图的邻接矩阵(Adjacency Matrix) 存储方式是用两个数组来表示图。一个一维数组存储图中顶点信息,一个二维数组(称为邻接矩阵)存储图中的边或弧的信息。
#define MaxVertexNum 100 //顶点数目的最大值
typedef char VertexType; //顶点的数据类型
typedef int EdgeType; //带权图中边上权值的数据类型
typedef struct{
VertexType Vex[MaxVertexNum]; //顶点表
EdgeType Edge[MaxVertexNum][MaxVertexNum]; //邻接矩阵,边表
int vexnum, arcnum; //图的当前顶点数和弧数
}MGraph;
2.邻接表。
指对图G中的每个顶点vi建立一个单链表,第i 个单链表中的结点表示依附于顶点v i 的边(对于有向图则是以顶点v i 为尾的弧),这个单链表就称为顶点v i 的边表。
#define MAXVEX 100 //图中顶点数目的最大值
typedef char VertexType; //顶点类型应由用户定义
typedef int EdgeType; //边上的权值类型应由用户定义
/*边表结点*/
typedef struct EdgeNode{
int adjvex; //该弧所指向的顶点的下标或者位置
EdgeType weight; //权值,对于非网图可以不需要
struct EdgeNode *next; //指向下一个邻接点
}EdgeNode;
/*顶点表结点*/
typedef struct VertexNode{
Vertex data; //顶点域,存储顶点信息
EdgeNode *firstedge //边表头指针
}VertexNode, AdjList[MAXVEX];
/*邻接表*/
typedef struct{
AdjList adjList;
int numVertexes, numEdges; //图中当前顶点数和边数
}
3.十字链表。
十字链表是有向图的一种链式存储结构。只能存储有向图。
typedef struct Bow //定义弧节点
{
char head,tail;
struct Bow *hlink,*tlink;
}Bow;
typedef struct FirstNode //定义头结点
{
char data;
Bow *firIn;
Bow *firOut;
}FirstNode;
typedef struct //记录顶点的数量和弧的数量,先定义100顶点
{
FirstNode list[100];
int peak,edge;
}total;
4.邻接多重表。
typedef struct EBox
{
int ivex, jvex; //依附该边的两个顶点
struct EBox *ilink, *jlink; //指向各自依附这两个顶点的下一条边
InfoType *info; //该边信息指针
} EBox; // 边结点
typedef struct VexBox
{
VertexType data; //存储顶点相关信息
EBox *firstedge; //指向第一条依附于该顶点的边
} VexBox; // 顶点结点
typedef struct
{
VexBox adjmulist[MAX_VERTEX_NUM];
int vexnum, edgenum; //无向图的顶点数和边数
} AMLGraph; //一个无向图的邻接多重表
3.图的遍历。
1.深度优先搜索(DFS)。
bool visited[MAX_VERTEX_NUM]; //访问标记数组
/*从顶点出发,深度优先遍历图G*/
void DFS(Graph G, int v){
int w;
visit(v); //访问顶点
visited[v] = TRUE; //设已访问标记
//FirstNeighbor(G,v):求图G中顶点v的第一个邻接点,若有则返回顶点号,否则返回-1。
//NextNeighbor(G,v,w):假设图G中顶点w是顶点v的一个邻接点,返回除w外顶点v
for(w = FirstNeighbor(G, v); w >= 0; w = NextNeighor(G, v, w)){
if(!visited[w]){ //w为u的尚未访问的邻接顶点
DFS(G, w);
}
}
}
/*对图进行深度优先遍历*/
void DFSTraverse(MGraph G){
int v;
for(v=0; v<G.vexnum; ++v){
visited[v] = FALSE; //初始化已访问标记数据
}
for(v=0; v<G.vexnum; ++v){ //从v=0开始遍历
if(!visited[v]){
DFS(G, v);
}
}
}
2.广度优先遍历(BFS)。
/*邻接矩阵的广度遍历算法*/
void BFSTraverse(MGraph G){
int i, j;
Queue Q;
for(i = 0; i < G,numVertexes; i++){
visited[i] = FALSE;
}
InitQueue(&Q); //初始化一辅助用的队列
for(i = 0; i < G.numVertexes; i++){
if(!visited[i]){
vivited[i] = TRUE; //设置当前访问过
visit(i); //访问顶点
EnQueue(&Q, i); //将此顶点入队列
while(!QueueEmpty(Q)){ //若当前队列不为空
DeQueue(&Q, &i); //顶点i出队列
//FirstNeighbor(G,v):求图G中顶点v的第一个邻接点,若有则返回顶点号,否则返回-1。
//NextNeighbor(G,v,w):假设图G中顶点w是顶点v的一个邻接点,返回除w外顶点v
//寻找顶点i周围的顶点,加入队列。
for(j = FirstNeighbor(G, i); j>=0; j = NextNeighbor(G, i, j)){
//检验i的所有邻接点
if(!visited[j]){
visit(j); //访问顶点j
visited[j] = TRUE; //访问标记
EnQueue(Q, j); //顶点j入队列
}
}
}
}
}
}
4.最小生成树。
1.普里姆(Prim) 算法。
从一个顶点出发,在保证不形成回路的前提下,每找到并添加一条最短的边,就把当前形成的连通分量当做一个整体或者一个点看待,然后重复“找最短的边并添加”的操作。
#define MaxVertexNum 100 //顶点数目的最大值
typedef char VertexType; //顶点的数据类型
typedef int EdgeType; //带权图中边上权值的数据类型
typedef struct{
VertexType Vex[MaxVertexNum]; //顶点表
EdgeType arc[MaxVertexNum][MaxVertexNum]; //邻接矩阵,边表
int numVertexes, arcnum; //图的当前顶点数和弧数
}MGraph;
/*Prim算法生成最小生成树,邻接矩阵存储*/
void MiniSpanTree_Prim(G){
int min, i, j, k;
int adjvex[MAXVEX]; //保存相关顶点 下标
int lowcost[MAXVEX]; //保存相关顶点 边的权值
lowcost[0] = 0; //初始化第一个权值为0,即v0加入生成树
adjvex[0] = 0; //初始化第一个顶点下标为0
for(i = 1; i < G.numVertexes; i++){
lowcost[i] = G.arc[0][i]; //将v0顶点与之组成边的权值存入数组
adjvex[i] = 0; //初始化都为v0的下标
}
for(i = 1; i < G.numVertexes; i++){
min = INFINITY; //初始化最下权值为∞,通常设置一个不可能的很大的数字
j = 1; k = 0;
//循环全部顶点
while(j < G.numVertexes){
if(lowcost[j] != 0 && lowcost[j] < min){ //如果权值不为0且权值小于min
min = lowcost[j]; //则让当前权值成为最小值
k = j; //将当前最小值的下标存入k
}
j++;
}
print("(%d, %d)", adjvex[k], k); //打印当前顶点中权值的最小边和顶点
//循环全部顶点,更新顶点最小权值
for(j = 1; j < G.numvertexes; j++){
if(lowcost[j] != 0 && G.arc[k][j] < lowcost[j]){ //若下标为k顶点 各边权值小于此前这些顶点未被加入生成树权值
lowcost[j] = G.arc[k][j]; //将较小权值存入lowcost
adjvex[j] = k; //将下标为k的顶点存入adjvex
}
}
}
}
2.克鲁斯卡尔(Kruskal)算法。
初始时为只有n个顶点而无边的非连通图T = V,每个顶点自成一个连通分量,然后按照边的权值由小到大的顺序,不断选取当前未被选取过且权值最小的边,若该边依附的顶点落在T中不同的连通分量上,则将此边加入T ,否则舍弃此边而选择下一条权值最小的边。以此类推,直至T中所有顶点都在一个连通分量上。
邻接矩阵通过程序转化为边集数组:
/*对边集数组Edge结构的定义*/
typedef struct{
int begin;
int end;
int weight;
}Edge;
/*Kruskar算法生成最小生成树*/
void MiniSpanTree_Kruskal(MGraph G){
int i, n, m;
Edge edges[MAXEDGE]; //定义边集数组
int parent[MAXVEX]; //定义一数组用来判断 边与边是否形成环路
/*此处省略将邻接矩阵G转化为边集数组edges并按照权由小到大排序的代码*/
for(i=0; i<G.numVertexes; i++){
parent[i] = 0; //初始化数组为0
}
for(i=0; i < G.numVertexes; i++){
n = Find(parent, edges[i].begin);
m = Find(parent, edge[i],end);
/*假如n与m不等,说明此边没有与现有生成树形成环路*/
if(n != m){
/*将此边的结尾顶点放入下标为起点的parent中
表示此顶点已经在生成树集合中*/
parent[n] = m;
printf("(%d, %d, %d)", edges[i].begin, edges[i].end, edges[i].weight);
}
}
}
/*查找连线顶点的尾部下标*/
int Find(int *parent, int f){
while(parent[f] > 0){
f = parent[f];
}
return f;
}
5.最短路径。
1.迪杰斯特拉( Dijkstra )算法。
Dijkstra算法用于构建单源点的最短路径,即 图中某个点到其他点的距离的最短路径。(不适合代负权值的图)
/*
* Dijkstra最短路径。
* 即,统计图(G)中"顶点vs"到其它各个顶点的最短路径。
*
* 参数说明:
* G -- 图
* vs -- 起始顶点(start vertex)。即计算"顶点vs"到其它顶点的最短路径。
* prev -- 前驱顶点数组。即,prev[i]的值是"顶点vs"到"顶点i"的最短路径所经历的全部顶点中,位于"顶点i"之前的那个顶点。
* dist -- 长度数组。即,dist[i]是"顶点vs"到"顶点i"的最短路径的长度。
*/
void dijkstra(Graph G, int vs, int prev[], int dist[])
{
int i,j,k;
int min; //记录顶点间最小路径
int tmp; //辅助变量,更新路径时使用
int flag[MAX]; // flag[i]=1表示"顶点vs"到"顶点i"的最短路径已成功获取。
for (i = 0; i < G.vexnum; i++){
flag[i] = 0; // 顶点i的最短路径还没获取到。
prev[i] = 0; // 顶点i的前驱顶点为0。
dist[i] = G.matrix[vs][i]; // 顶点i的最短路径为"顶点vs"到"顶点i"的权。
}
flag[vs] = 1; // 对"顶点vs"自身进行初始化
dist[vs] = 0;
for (i = 1; i < G.vexnum; i++){ // 遍历G.vexnum-1次;每次找出一个顶点的最短路径。
// 寻找当前最小的路径;在未获取最短路径的顶点中,找到离vs最近的顶点(k)。
min = INF;
for (j = 0; j < G.vexnum; j++){
if (flag[j] == 0 && dist[j] < min){
min = dist[j];
k = j;
}
}
flag[k] = 1; // 标记"顶点k"为已经获取到最短路径
// 修正当前最短路径和前驱顶点。当已经"顶点k的最短路径"之后,更新"未获取最短路径的顶点的最短路径和前驱顶点"。
for (j = 0; j < G.vexnum; j++){
tmp = (G.matrix[k][j] == INF ? INF : (min + G.matrix[k][j])); // 防止溢出
if (flag[j] == 0 && (tmp < dist[j]) ){
dist[j] = tmp;
prev[j] = k;
}
}
}
printf("dijkstra(%c): \n", G.vexs[vs]); // 打印dijkstra最短路径的结果
for (i = 0; i < G.vexnum; i++)
printf(" shortest(%c, %c)=%d\n", G.vexs[vs], G.vexs[i], dist[i]);
}
2.弗洛伊德( Floyd )算法。
Floyd算法允许图中有带负权值的边,但不允许有包含带负权值的边组成的回路。Floyd 算法同样适用于带权无向图,因为带权无向图可视为权值相同往返二重边的有向图。
void floyd(){
int len=0;
for(int k = 0; k < dis.length; k++){ //对中间顶点遍历,k就是中间顶点的下标[A,B,C,D,E,F,G]
for(int i = 0; i < dis.length; i++){ //从j 顶点出发遍历 [A,B,C,D,E,F,G]
for(int j = 0; j < dis.length; j++){ //求出从i顶点出发,经过k中间顶点,到达j顶点距离
len = dis[i][k] + dis[k][j];
if (len < dis[i][j]) {
dis[i][j] = len; //更新距离
pre[i][j] = pre[k][j]; //更新前驱顶点
}
}
}
}
}
6.拓扑排序。
在一个表示工程的有向图中,用顶点表示活动,用弧表示活动之间的优先关系,这样的有向图为顶点表示活动的网,我们称为AOV网( Activity On VertexNetwork)。
- ①从AOV网中选择一个没有前驱的顶点并输出。
- ②从网中删除该顶点和所有以它为起点的有向边。
- ③重复①和②直到当前的AOV网为空或当前网中不存在无前驱的顶点为止。如果输出顶点数少了,哪怕是少了一个,也说明这个网存在环(回路),不是AOV网。
//图为邻接表结构。indegree数组记录每个顶点的入度
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)){ //栈不空,则存在入度为0的顶点
Pop(S, i); //顶点元素出栈
printf("%d ", i); //输出顶点i
count++;
for(p = G.vertices[i].finstarc; 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网时,应注意以下问题:
①入度为零的顶点,即没有前驱活动的或前驱活动都已经完成的顶点,工程可以从这个顶点所代表的活动开始或继续。
②若一个顶点有多个直接后继,则拓扑排序的结果通常不唯一;但若各个顶点已经排在一个线性有序的序列中,每个顶点有唯一的前驱后继关系,则拓扑排序的结果是唯一的。
③由于AOV网中各顶点的地位平等,每个顶点编号是人为的,因此可以按拓扑排序的结果重新编号,生成AOV网的新的邻接存储矩阵,这种邻接矩阵可以是三角矩阵;但对于一般的图来说,若其邻接矩阵是三角矩阵,则存在拓扑序列;反之则不一定成立。
7.关键路径。
在带权有向图中,以顶点表示事件,以有向边表示活动,以边上的权值表示完成该活动的开销(如完成活动所需的时间),称之为用边表示活动的网络,简称AOE网。
- 事件的最早发生时间ve:即顶点Vk的最早发生时期。从源点 Vi 出发到达顶点 vk 经过的路径上的权值之和(最大值)。
- 事件的最晚发生时间vl:即顶点Vk的最晚发生时间,也就是每个顶点对应的事件最晚需要开始的时间,超出此时间将会延误整个工期。(vl = min(ve(i)-weight(i)) )
- 活动的最早开始时间 e:即弧ai的最早发生时间。
- 活动的最迟开始时间 l:即弧ai 的最晚发生时间,也就是不推迟工期的最晚开工时间。vl()-weight()
- d(i)=l(i)−e(i):它是指该活动完成的时间余量,即 在不增加完成整个工程所需总时间的情况下,活动ai可以拖延的时间。若一个活动的时间余量为零,则说明该活动必须要如期完成,否则就会拖延整个工程的进度,所以d(i) = 0时的活动ai是关键活动,所有的ai是关键路径。
求关键路径的算法步骤如下:
- 从源点出发,令ve(源点)=0, 按拓扑排序求其余顶点的最早发生时间ve()。权值之和(最大值)
- 从汇点出发,令vl(汇点)=ve(汇点),按逆拓扑排序求其余顶点的最迟发生时间vl()。vl = min(ve(i)-weight(i))
- 根据各顶点的ve()值求所有弧的最早开始时间e()。e()=弧尾的顶点最早发生时间ve()。
- 根据各顶点的vl()值求所有弧的最迟开始时间l ( )。i() = 弧的终点的顶点vl()-weight()。
- 求AOE网中所有活动的差额d(), 找出所有d()=0的活动构成关键路径。d(i)=l(i)−e(i)。
七. 查找。
一、查找的基本概念。
查找(Searching):就是根据给定的某个值,在查找表中确定一个其关键字等于给定值的数据元素( 或记录)。
查找表(Search Table):是由同一类型的数据元素(或记录)构成的集合。
关键字(Key):数据元素中唯一标识该元素的某个数据项的值,使用基于关键字的查找,查找结果应该是唯一的。例如,在由一个学生元素构成的数据集合中,学生元素中“学号”这一数据项的值唯一地标识一名学生。
静态查找表(Static Search Table):只作查找操作的查找表。
动态查找表(Dynamic Search Table): 在查找过程中同时插入查找表中不存在的数据元素,或者从查找表中删除已经存在的某个数据元素。
平均查找长度:在查找过程中,一次查找的长度是指需要比较的关键字次数,而平均查找长度,则是所有查找过程中进行关键字的比较次数的平均值
二.查找种类。
1.顺序查找。
2.二分查找(折半查找)。
int binary_search(int *a, int head, int len, int key){
int left = 0,right = len-1;
int mid = 0;
while(left < right){
mid = left + (right - left) / 2;
if(mid == key){
return mid;
}
else if(key > mid){
low = mid + 1;
}
else{
high = mid - 1;
}
}
return 0;
}
平均查找长度ASL≈log2(n+1)−1
3.分块查找(索引顺序查找)。
基本思想:
分块查找过程分两步:即先确定块,然后再块内进行顺序查找(因为块内无序)。对于确定块来讲可以选择折半查找,也可以选择顺序查找。
4.树形查找。
二叉排序树(BST树):二叉排序树也称为二叉查找树,可以是一棵空树,然则就要满足以下条件:
- 若左子树非空,则左子树上所有节点的值一定小于根节点
- 若右子树非空,则右子树上所有节点的值一定大于根节点
- 左右子树也一定是一颗二叉排序树。
二叉排序树的查找:
//二叉排序树结点
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;
}
//查找的递归实现,效率不如上面的非递归的
BSTNode *BSTSearch(BSTree T,int key){
if(T == NULL)
return NULL;
if(key == T->key)
return T;
else if(key < T->key)
return BSTSearch(T->lchild,key);
else
return BSTSearch(T->rchild,key);
}
二叉排序树的插入:
//二叉排序树的插入
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; //插入成功,返回1
}
if(key == T->key){ //如果树中存在相同关键字的结点,插入失败
return 0;
}else if(key < T->key){ //插入到左孩子中
return BST_Insert(T->lchild,key);
}else{
return BST_Insert(T->rchild,key);
}
}
二叉排序树的删除:
- 若被删除的是叶结点,则直接删除。
- 若结点z只有一棵左子树或右子树,则让z的子树成为z的父结点的子树,替代z的位置。(说白了就是用z的子树去替代z的位置)。
- 若结点z有左、右两棵子树,则令z的直接后继(或直接前驱)替代z,然后从二叉排序树中删除去这个直接后继(或直接前驱),这样就转换成了第一种或第二种情况。
对于第三种情况:
- z的直接后继一定是z的右子树的最左下结点。(为什么呢?因为二叉排序树的性质其实就相当于进行中序遍历之后是一个递增序列,而中序遍历:【 左根((左根右)根右))】
- z的直接前驱一定是z的左子树的最右下结点。
二叉排序树的构造:
void Create_BST(BSTree &T,int str[],int n){ //注意这里参数是引用类型的。str[]是存储关键字的数组
T = NULL; //初始时T为空树
int i = 0;
while(i < n){
BST_Insert(T,str[i]); //依次将每个关键字插入到二叉排序树中
i++;
}
}
平均查找长度O(log2n),最坏为O(n)
5.平衡二叉树。
平衡二叉树也叫 AVL 树。平衡二叉树是具有以下特点的二叉查找树:它是一棵空树或它的左右两个子树的高度差的绝对值不超过 1, 并且左右两个子树都是一棵平衡二叉树。
某结点的左子树与右子树的高度(深度)差即为该节点的平衡因子(BF,Balance Factor)。平衡⼆叉树中不存在平衡因子大于于 1 的节点。
最小失衡子树:在新插入的结点向上找,以第一个平衡因子绝对值大于1的结点为根的子树称为最小失衡子树。平衡二叉树的失衡调整主要是通过旋转最小失衡子树来实现的,旋转分为左旋和右旋,其目的,就是减少树的高度(哪边高,就把那边向上旋转)。
1.平衡二叉树的插入。
LL:
//结点结构
typedef struct AVLNode
{
int data;
int height;
struct AVLNode* left;
struct AVLNode* right;
}Node;
/*插入左孩子的左子树-右旋*/
//传入参数为最小失衡结点tree,对tree进行右旋
Node* left_left(Node* tree)
{
//结点调整
Node* k = tree->left; //保存tree的左孩子,k将是最终的父节点
tree->left = k->right; //将k的右孩子接到tree的左子树
k->right = tree; //tree作为k的右子树
//高度调整(这里指深度:用左右子树来判断)
k->height = MAX(get_height(k->left), get_height(k->right)) + 1;
tree->height = MAX(get_height(tree->left), get_height(tree->right)) + 1;
return k;
}
RR:
/*插入右孩子的右子树-左旋*/
//传入参数为最小失衡结点tree,对tree进行左旋
Node* right_right(Node* tree)
{
//结点调整
Node* k = tree->right;
tree->right = k->left;
k->left = tree;
//高度调整
k->height = MAX(get_height(k->left), get_height(k->right)) + 1;
tree->height = MAX(get_height(tree->left), get_height(tree->right)) + 1;
return k;
}
LR:
/*插入左孩子的右子树-先左旋再右旋*/
//对tree->left左旋(left_left),对tree右旋(right_right)
Node* left_right(Node* tree)
{
tree->left = right_right(tree->left);
tree = left_left(tree);
return tree;
}
RL:
/*插入右孩子的左子树-先右旋再左旋*/
//对tree->right右旋(left_left),对tree左旋
Node* right_left(Node* tree)
{
tree->right = left_left(tree->right);
tree = right_right(tree);
return tree;
}
2.平衡二叉树的删除:(看王道视频)。
删除结点可能会导致树失衡,而且删除此结点,可能导致其他很多结点都失衡。插入只需要修正第一个非平衡结点(1个),即可平衡;而删除需要修正所有的非平衡结点。
删除结点的类型:
- 删除叶子结点
- 删除结点只有左子树
- 删除结点只有右子树
- 删除结点有左、右子树
6.哈希函数。
![](https://i-blog.csdnimg.cn/blog_migrate/fd9615c74b14237bee4bb9b37b33a3a6.png)
- 散列函数:一个把查找表中的关键字映射成该关键字对应的地址的函数,记作H a s h ( k e y ) = A d d r Hash(key)=AddrHash(key)=Addr。
- 散列函数可能会把两个或两个以上的不同关键字映射到同一地址,称这种情况为冲突。发生碰撞的不同关键字称为同义词。
- 散列表:根据关键字而直接进行访问的数据结构。散列表建立了关键字和存储地址之间的一种直接映射关系。
散列函数的构造方法:
- 直接定址法:直接取关键字的某个线性函数值为散列地址,散列函数为 H ( k e y ) = k e y 或 H ( k e y ) = a × k e y + b 。这种方法计算简单,不会产生冲突。缺点是空位较多,会造成存储空间浪费。
- 除留余数法:假定散列表表长 m,取一个不大于但最接近 m 的质数 p,利用散列函数 H ( k e y ) = k e y % p将关键字转换为散列地址。p取质数是因为这样可以使关键字通过散列函数转换后等概率地映射到散列空间上的任一地址。
- 数字分析法:假设关键字是 r进制数,而r个数码在个位上出现的频率不一定相同,可能在某些位上分布的均匀一些,而在某些位分布的不均匀。此时应选数码分布均匀的若干位作为散列地址。
- 平方取中法:这种方法取关键字平方值的中间几位作为散列地址,具体取多少位视具体情况而定。这种方法得到的散列地址与关键字的每一位都有关系,因此使得散列地址分布比较均匀。适用于关键字每位取值都不够均匀或均小于散列地址所需的位数。
平均查找长度(ASL):散列表查找成功的平均查找长度即找到表中已有表项的平均比较次数;散列表查找失败的平均查找长度即找不到待查的表项但能找到插入位置的平均比较次数。
八.排序。
1.插入排序。
直接插入排序:每次将一个待排序的记录按其关键字大小,插入到前面已经排好序的子序列中,直到全部记录插入完成。
// 对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,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-low)/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];
}
}
2.希尔排序。
先追求表中元素的部分有序,再逐渐逼近全局有序,以减小插入排序算法的时间复杂度。
// 对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];
}
}
}
}
- 时间复杂度:希尔排序时间复杂度依赖于增量序列的函数。最差情况O(
),n在某个特顶范围时可达O(
) 。
- 空间复杂度:O(1)
- 算法稳定性:不稳定。
3.冒泡排序。
// 交换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; //若本趟遍历没有发生交换,说明已经有序
}
}
4.快速排序 。
- 算法思路:在待排序表 L [ 1... n ] 中任选一个元素 pivot 作为枢轴(通常取首元素),通过一趟排序将待排序表分为独立的两部分 L [ 1... k − 1 ] 和 L [ k − 1... n ] 。使得 L [ 1... k − 1 ] 中的所有元素小于 pivot,L [ k − 1... n ]中的所有元素大于等于 pivot,则 pivot 放在了其最终位置 L [ k ]上。重复此过程直到每部分内只有一个元素或空为止。
- 快速排序是所有内部排序算法中性能最优的排序算法。
- 在快速排序算法中每一趟都会将枢轴元素放到其最终位置上。(可用来判断进行了几趟快速排序)
- 快速排序可以看作数组中n个元素组织成二叉树,每趟处理的枢轴是二叉树的根节点,递归调用的层数是二叉树的层数。
// 用第一个元素将数组A[]划分为两个部分
int Partition(int A[], int low, int high){
int pivot = A[low];
while(low < high){
while(low < high && A[high] >= pivot)
--high;
A[low] = A[high];
while(low < high && A[low] <= pivot)
++low;
A[high] = A[low];
}
A[low] = pivot;
return low;
}
// 对A[]数组的low到high进行快速排序
void QuickSort(int A[], int low, int high){
if(low<high){
int pivotpos = Partition(A, low, high); //划分
QuickSort(A, low, pivotpos - 1);
QuickSort(A, pivotpos + 1, high);
}
}
5.简单选择排序。
每一趟在待排序元素中选取关键字最小的元素与待排序元素中的第一个元素交换位置。
// 交换a和b的值
void swap(int &a, int &b){
int temp = a;
a = b;
b = temp;
}
// 对A[]数组共n个元素进行选择排序
void SelectSort(int A[], int n){
for(int i = 0; i < n-1; i++){ //一共进行n-1趟,i指向待排序序列中第一个元素
int min = i;
for(int j = i+1; j < n; j++){ //在A[i...n-1]中选择最小的元素
if(A[j] < A[min])
min = j;
}
if(min!=i)
swap(A[i], A[min]);
}
}
6.堆排序。
首先将存放在 L [ 1... n ] 中的n个元素建成初始堆,由于堆本身的特点,堆顶元素就是最大值。将堆顶元素与堆底元素交换,这样待排序列的最大元素已经找到了排序后的位置。此时剩下的元素已不满足大根堆的性质,堆被破坏,将堆顶元素下坠使其继续保持大根堆的性质,如此重复直到堆中仅剩一个元素为止。
// 对初始序列建立大根堆
void BuildMaxHeap(int A[], int len){
for(int i = len/2; i > 0; i--) //从后往前调整所有非终端结点
HeadAdjust(A, i, len);
}
// 将以k为根的子树调整为大根堆
void HeadAdjust(int A[], int k, int len){
A[0] = A[k];
for(int i = 2*k; i <= len; i* = 2){ //沿k较大的子结点向下调整
if(i < len && A[i] < A[i+1])
i++;
if(A[0] >= A[i])
break;
else{
A[k] = A[i]; //将A[i]调整至双亲结点上
k = i; //修改k值,以便继续向下筛选
}
}
A[k] = A[0]
}
// 交换a和b的值
void swap(int &a, int &b){
int temp = a;
a = b;
b = temp;
}
// 对长为len的数组A[]进行堆排序
void HeapSort(int 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);
}
}
堆的插入与删除。
7.归并排序。
把两个或多个已经有序的序列合并成一个新的有序表。k路归并每选出一个元素,需对比关键字k-1次。
先将数组进行拆分,每次拆成两份,然后继续拆分直到一组有两个元素为止,然后再进行两两整合排序,重复两两整合排序直至数组元素排序完成。
// 辅助数组B
int *B=(int *)malloc(n*sizeof(int));
// A[low,...,mid],A[mid+1,...,high]各自有序,将这两个部分归并
void Merge(int A[], int low, int mid, int high){
int i,j,k;
for(k = low; k <= high; k++)
B[k] = A[k];
for(i = low, j = mid+1, k = i; i <= mid && j <= high; k++){
if(B[i] <= B[j])
A[k] = B[i++];
else
A[k] = B[j++];
}
while(i <= mid)
A[k++] = B[i++];
while(j <= high)
A[k++] = B[j++];
}
// 递归操作
void MergeSort(int A[], int low, int high){
if(low < high){
int mid = (low + high)/2;
MergeSort(A, low, mid);
MergeSort(A, mid+1, high);
Merge(A,low,mid,high); //归并
}
}
8.基数排序。
把整个关键字拆分为d位,按照各个关键字位递增的次序(比如:个、十、百),做d趟“分配”和“收集”,若当前处理关键字位可能取得r个值,则需要建立r个队列。
- 分配:顺序扫描各个元素,根据当前处理的关键字位,将元素插入相应的队列。一趟分配耗时
- 收集:把各个队列中的结点依次出队并链接。一趟收集耗时
。