王道考研-数据结构算法

目录

第一章 绪论

第二章 线性表

第三章 栈和队列

3.1栈

3.11栈的基本操作

InitStack(&S): 初始化栈。构造一个空栈S,分配内存空间
DestroyStack(&L):销毁栈。销毁并释放栈S所占用的内存空间

Push(&S,x): 进栈,若栈S未满,则将x加入使之成为新栈顶
Pop(&S,x):出栈,若栈S非空,则弹出栈顶元素,并用x返回。

GetTop(S,&x): 读栈顶元素,若栈S非空,则用x返回栈顶元素。
StackEmpty(S): 判断一个栈S是否为空,若S为空,则返回true,否则返回false。

3.1.2栈的顺序存储结构

顺序栈

顺序栈的定义

#define MaxSize 10			 //定义栈中元素的最大个数
typedef struct{
	ElemType data[MaxSize];  //静态数组存放栈中元素
	int top;				//栈顶指针
}SqStack;     				//Sq:sequence:顺序

初始化操作

void InitStack(SqStack &S){
	S.top = -1;         //初始化栈顶指针
}

判断栈空

bool StackEmpty(SqStack S){
	if(S.top==-1)			//栈空
		return true;
	else					//不空
		return false;
}

进栈操作

#define MaxSize 10			 //定义栈中元素的最大个数
typedef struct{
	ElemType data[MaxSize];  //静态数组存放栈中元素
	int top;				//栈顶指针
}SqStack;     				//Sq:sequence:顺序
//新元素进栈
bool Push(SqStack &S,ElemType x){
	if(S.top==MaxSize-1)        //栈满,报错
		return false;
		
	S.top = S.top+1;            //指针先加1
	S.data[S,top]=x;   			//新元素入栈
	return true;
	//也可以写成 S.data[++S.top]=x;
}

出栈操作

#define MaxSize 10			 //定义栈中元素的最大个数
typedef struct{
	ElemType data[MaxSize];  //静态数组存放栈中元素
	int top;				//栈顶指针
}SqStack;     				//Sq:sequence:顺序

bool Pop(SqStack &S,ElemType &x){
	if(S.top==-1)		//栈空,报错
		return false;
	x=S.data[S.top];	//栈顶元素先出栈
	S.top=S.top-1;		//指针再减1
	return true;
	//也可以换成 x=S.data[S.top--];
}

读栈顶元素

#define MaxSize 10			 //定义栈中元素的最大个数
typedef struct{
	ElemType data[MaxSize];  //静态数组存放栈中元素
	int top;				//栈顶指针
}SqStack;     				//Sq:sequence:顺序

bool Pop(SqStack &S,ElemType &x){
	if(S.top==-1)		//栈空,报错
		return false;
	x=S.data[S.top];	//x记录栈顶元素
	return true; 
}

共享栈

#define MaxSize 10			 //定义栈中元素的最大个数
typedef struct{
	ElemType data[MaxSize];  //静态数组存放栈中元素
	int top0;				//0号栈顶指针
	int top1;				//1号栈顶指针
}SqStack;     				//Sq:sequence:顺序
//初始化栈
void InitStack(ShStack &S){
	S.top0=-1;			//初始化栈顶指针
	S.top1=MaxSize;
}
//栈满的条件:top0+1==top1;

链栈

栈的应用(括号匹配)

#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)
//栈顶元素出栈,用x返回
bool Pop(SqStack &s,&x)

bool bracketCheck(char str[],int length){
	SaStack S;
	InitStack(S);				//初始化一个栈
	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;
			Pop(S,topElem);		//栈顶元素出栈
			if(str[i]==')' && topElem!='(')
				retuen false;
			if(str[i]==']' && topElem!='[')
				retuen false;
			if(str[i]=='}' && topElem!='{')
				retuen false;
		}
	}
		StackEmpty(S);		//检索完全部括号后,栈空说明匹配成功
}

备注:首先定义一个栈并且初始化这个栈,接下来用一个循环从左往右扫描这些字符,如果此次扫描到的字符它是莫一种左括号的话,那么我们就把这个字符给他Push压入栈中;
如果只是扫描到的是右括号,那么就首先需要检查栈是否为空,栈空的话,就说明右括号单身,匹配失败;
如果栈不空我们就用Pop出栈的操作,把这样的元素弹出去,然后用topElem变量来存储。
然后就检查扫描到的右括号和当前栈顶的左括号是否匹配,如果只是扫描到的右小括号,而栈顶的不是左小括号,匹配失败。以此类推,处理完所有括号。
最后判断栈是否为空,空,匹配成功。

栈的应用(表达式求值)

中缀转后缀的手算方法

1.确定中缀表达式中各个运算符的运算顺序
2.选择下一个运算符,按照『左操作数 右操作数 运算符』的方式组合成一个新的操作数
3.如果还有运算符没有被处理,就继续步骤2
左优先原则:只要左边的运算符能计算,就优先算左边的

中缀转后缀(机算)

初始化一个栈,用于保存暂时还不能确定运算顺序的运算符。
从左到右处理各个元素,知道末尾。
可能遇到三种情况:
1、遇到操作数。直接加入后缀表达式。
2、遇到界限符。遇到"(“直接入栈,遇到”)“则依次弹出栈内运算符并加入后缀表达式,知道弹出”(“为止。注意:”(“不加入后缀表达式。
3、遇到运算符。依次弹出栈中优先级高于或等于当前运算符的所有运算符,并加入后缀表达式,若碰到”("或栈空则停止。之后再把当前运算符入栈。
按照上述方法处理完所有字符后,将栈中剩余运算依次弹出,并加入后缀表达式。

中缀转后缀(用栈实现)

初始化两个栈,操作数栈和运算符栈
若扫描到操作数,压入操作数栈
若扫描到运算符或界限符,则按照”中缀转后缀“相同的逻辑压入运算符栈(期间也会弹出运算符,每当弹出一个运算符时,就需要再弹出两个操作数栈的栈顶元素并执行相应运算,运算结果在再回操作数栈)

栈的应用(递归)

函数调用背后的过程
函数调用的特点:最后被调用的函数最想执行结束(LIFO)。
函数调用时,需要用一个栈存储:
1、调用返回地址
2、实参
3、局部变量

3.2队列

3.2.1队列的基本概念

队列常见的基本操作

InitQueue(&Q): 初始化队列。构造一个空队列Q。
DestroyQueue(&Q):销毁队列。销毁并释放队列Q所占用的内存空间。

EnQueue(&Q,x): 入队,若队列Q未满,将x加入,使之成为新的队尾。
DeQueue(&Q,&x): 出队,若队列Q非空,删除队头元素,并用x返回。

GetHead(Q,&x): 读队头元素,若队列Q非空,则将队头元素赋值给x。

3.2.2 队列的顺序存储

定义声明

#define MaxSize 10          //定义队列中元素的最大个数
typedef struct{
ElemType data[MaxSize];		//用静态数组存放队列元素
int front,rear;				//对头指针和队尾指针
} SqQueue;
void testQueue(){
SqQueue Q;					//声明一个队列
//.......
}

初始化

#define MaxSize 10          //定义队列中元素的最大个数
typedef struct{
	ElemType data[MaxSize];		//用静态数组存放队列元素
	int front,rear;				//对头指针和队尾指针
} SqQueue;
//初始化队列
void InitQueue(SqQueue &Q){
	//初始时  队头和队尾指针指向0
	Q.rear=Q.front=0;
}

void testQueue(){
	SqQueue Q;					//声明一个队列
	InitQueue(Q);
	//....
}

队列判空

#define MaxSize 10          //定义队列中元素的最大个数
typedef struct{
	ElemType data[MaxSize];		//用静态数组存放队列元素
	int front,rear;				//对头指针和队尾指针
} SqQueue;
//判空
bool QueueEmpty(SqQueue Q){
	if(Q.rear==Q.front){		//判空条件
		return true;
	else
		return false;
}

入队

#define MaxSize 10          //定义队列中元素的最大个数
typedef struct{
	ElemType data[MaxSize];		//用静态数组存放队列元素
	int front,rear;				//对头指针和队尾指针
} SqQueue;
//入队
bool EnQueue(SqQueue &Q,ElemType x){
	if(队列已满)
		return false;
	Q.data[Q.rear]=x;			//新元素插入队尾
	Q.rear=(Q.rear+1)%MaxSize;  //队尾指针加一  取余
	return true;
}

备注:Q.rear=(Q.rear+1)%MaxSize 就是相当于用模运算将储存空间在逻辑上变成了“环状”。
MaxSize=10,当队列时,从队头出去了3个元素,那么rear还表示满队列,取余之后(9+1)%10 = 0,rear就会返回到起点。

出队

#define MaxSize 10          //定义队列中元素的最大个数
typedef struct{
	ElemType data[MaxSize];		//用静态数组存放队列元素
	int front,rear;				//对头指针和队尾指针
} SqQueue;
//出队(删除一个队头元素,并用x返回)
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;
}

队列元素个数:(rear+MaxSize-front)%MaxSize

判断队满(方法一)

//入队
bool EnQueue(SqQueue &Q,ElemType x){
	if((Q.rear+1)%MaxSise==Q.front)
		return false;
	Q.data[Q.rear]=x;			//新元素插入队尾
	Q.rear=(Q.rear+1)%MaxSize;  //队尾指针加一  取余
	return true;
}

备注:必须牺牲一个单位,否则Q.rear==Q.front 是队空条件。

判断队列已满/已空(方法二)

#define MaxSize 10          //定义队列中元素的最大个数
typedef struct{
	ElemType data[MaxSize];		//用静态数组存放队列元素
	int front,rear;				//对头指针和队尾指针
	int size;					//队列当前长度
} SqQueue;
//插入成功size++;  删除成功size--;  队满条件size==MaxSize;  

备注:size:队列当前长度。 队满条件size==MaxSize; 插入成功size++; 删除成功size–;

判断队列已满/已空(方法三)

#define MaxSize 10          //定义队列中元素的最大个数
typedef struct{
	ElemType data[MaxSize];		//用静态数组存放队列元素
	int front,rear;				//对头指针和队尾指针
	int tag;					//最近进行的是删除/插入
} SqQueue;
//队满条件:(front==rear && tag==1);
//队空条件:(front==rear && tag==0);

每次删除操作成功时,都令tag=0;只有删除操作,才可能导致队空。
每次插入操作成功时,都令tag=1;只有插入操作,才可能导致队满。

队列的链式实现

定义

typedef struct LinkNode{
	ElemType data;
	struct LinkNode *next;
}LinkNode;
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(LinkQueue));
	Q.front->next=NULL;
}
void testLinkQueue(){
	LinkQueue();	//声明一个队列
	InitQueue();	//初始化队列
}

判空

bool IsEmpty(LinkQueue Q){
	if(Q.front==Q.rear)
		return true;
	else
		return false;

入队(带头节点)

//新元素入队(带头节点)
void EnQueue(LingkQueue &Q,ElemType x){
	LinkNode *s=(LinkNode *)malloc(sizeof(LinkNode));//申请新节点
	s->data=x;			//x放到新节点当中
	s-next=NULL;		//
	Q.rear->next=s;		//新节点插入到rear之后
	Q.rear=s;			//修改表尾指针
}

备注:入队或者说队列的插入操作其实是在表尾的位置进行的,因此新插入的节点肯定是队列当中的最后一个节点,所以我们需要把新节点中的next指针域设置为NULL;
rear指针指向的是当前的表尾节点,而我们新插入的新节点应该连到当前表尾结点之后,所以我们需要把rear指向的节点,他的next指针域让他指向新节点s,最后还要让表尾指针指向新节点

入队(不带头结点)

void EnQueue(LingkQueue &Q,ElemType x){
	LinkNode *s=(LinkNode *)malloc(sizeof(LinkNode));//申请新节点
	s->data=x;			//x放到新节点当中
	s-next=NULL;	
	if(Q.front==NULL){	//在空队列中插入第一个元素
	Q.front =s;			//不修改队头队尾指针
	Q.rear=s;			
	}else{
		Q.rear->next=s;	//新节点插入到 rear 结点之后
		Q.rear=s;		//修改rear指针
	}

备注:不带头结点的话,在第一个元素入队时,需要进行特殊处理,需要对rear,front指针都进行修改。
需要一个判断,来判断如果队列为空的话,就意味着新插入的节点是队列中的第一个节点,此时需要修改rear,front的指向,让他们都指向第一个节点。
继续插入的话,rear指针指向的节点进行后插操作,同时修改rear指针的指向,每一次插入的新节点之后,都让rear指针指向新的表尾节点。

出队(带头节点)

bool DeQueue(LinkQueue &Q,ElemType &x){
	if(Q.front==Q.rear)
		return false;			//空队
	LinkNode *p=Q.front->next;
	x=p->data;					//用变量x返回队头元素
	Q.front->next=p->next;		//修改头节点的next指针
	if(Q.rear==p)				//此次修改是最后一个节点出队
		Q.rear=Q.front;			//修改rear指针
	free(p);					//释放节点空间
	return true;
}

备注: 对于带头节点的队列,就是要删除这个头节点的后面一个节点。
先用变量x把此次要删除的数据元素带回去,所以用了&x,再往后就是修改这个头节点的next指针,指向被删除的next元素。
如果被删除的节点不是最后一个节点,直接释放就行了,如果是表尾节点,我们还需要修改表尾指针,让他指向头节点,rear和front指向同一位置,就代表着又变成了空队列。

出队(不带头节点)

bool DeQueue(LinkQueue &Q,ElemType &x){
	if(Q.front==NULL)
		return false;			//空队
	LinkNode *p=Q.front;		//p指向此次出队的节点
	x=p->data;					//用变量x返回队头元素
	Q.front=p->next;			//修改front指针
	if(Q.rear==p){				//此次是最后一个节点出队
		Q.front=NULL;			//修改front指针
		Q.rear=NULL;			//修改rear指针
	}
	free(p);					//释放节点空间
	return true;
}

备注:由于没有头节点,因此每一个队头元素出队之后都与要修改front指针的指向。在最后一个节点出队之后也需要把front和rear都指向NULL,即恢复成空队列。

3.4特殊矩阵的压缩矩阵

数组

ElemType a[10]; //ElemType型一维数组
一维数组元素a[i]的存放地址:LOC+i*sizeof(ElemType)
一维数组的大小:(1+n)*n/2

ElemType b[2] [4]; //2行4列的二维数组
行优先储存:b[i] [j]的存放地址:LOC+(i * N+j) * sizeof(ElemType) (M行N列)
列优先储存:b[i] [j]的存放地址:LOC+(j * M+i) * sizeof(ElemType) (M行N列)

3.43矩阵的压缩矩阵

对称矩阵

若n阶方阵中任意一个元素ai j都有ai j = aj i 则该矩阵称为对称矩阵

第四章 串

4.1串的定义和实现

4.1.2串的储存结构

定长顺序储存

#define MAXLEN 255			//预定义最大串长255
typedef struct{
	char ch[MAXLEN];		//每个分量储存一个字符
	int length;				//串的实际长度
}SString;

堆分配储存表示

typedef struct{
	char *ch;				//按串长分配存储地址,ch指向串的基地址
	int length;				//串的长度
}HString;
HString S;
S.ch = (char *)malloc(MAXLEN * sizeof(char)):
S.length = 0;

4.1.3串的基本操作

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<T,则返回值<0;

求子串

SubString(&Sub,S,pos,len):求子串。用Sub返回串S的第pos个字符起长度为len的子串

#define MAXLEN 255			//预定义最大串长255
typedef struct{
	char ch[MAXLEN];		//每个分量储存一个字符
	int length;				//串的实际长度
}SString;
//求子串
bool SubString(SString &Sub,SString S, int pos,int len){
	//子串范围越界
	if(pos + len -1 > S.length)
		retuen false;
	for (int i = pos;i<pos+len;i++)
		Sub.ch[i-pos+1] = S.ch[i];
	Sub.length = len;
	return true;
}

备注:先传入一个叫Sub的串,用串返回我们想要找到的子串的内容,然后for循环把特定范围内的各个字符把他依次都赋值到Sub的char数组里,并且还需要把Sub的length设置为len。
if是判断子串范围是否越界。

比较字符串

StrCompare(S,T): 比较操作。 若S>T,则返回>0;若S=T,则返回值=0;若S<T,则返回值<0;

#define MAXLEN 255			//预定义最大串长255
typedef struct{
	char ch[MAXLEN];		//每个分量储存一个字符
	int length;				//串的实际长度
}SString;

//比较操作
init StrCompare(SString S, SString T){
	for(int i=1;1<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;
}

定位操作

Index(S,T): 定位操作。若主串S中存在与串T值相同的子串,则返回他的主串S中第一次出现的位置,否则函数值为0.

int Index(SString S,SString T){
	int i=1,n=StrLength(S),m=StrLength(T);
	//StrLength只需要返回串结构体中的length即可
	SString sub;	//用于暂存子串
	while(i<=n-m+1){
		SubString(sub,S,i,m);
		if(StrCompare(sub,T)!=0) ++i;
		else return i;	//返回子串在主串中的位置
	}
	return 0;	//S中不存在与T相等的子串
}

4.2串的模式匹配

4.2.1朴素模式匹配算法

主串长度为n,模式串长度为m。
朴素模式算法:将主串中所有长度为m的子串依次与模式串对比,直到找到一个完全匹配的子串,或所有的子串都不匹配为止。
主串长度为n,那么在主串中,长度为m的子串有 (n-m+1)个。

Index(S,T): 定位操作。若主串S中存在与串T值相同的子串,则返回他的主串S中第一次出现的位置,否则函数值为0

int Index(SString S,SString T){
	int i = 1,n=StrLength(S),m=StrLength(T);
	String sub;				//用于暂存子串
	while(i<=n-m+1){		//最多对比n-m+1个子串
		SubString(sub,S,i,m);//取出从位置i开始,长度为m的子串
		if(StrCompare(sub,T)!=0) ++i;
		//子串和模式串对比,若不匹配,则匹配下一个子串
		else return i;		//返回子串在主串中的位置
	}
	return 0;				//S中不存在与T相等的子串
}

若当前子串匹配失败,则主串指针i指向下一个子串的第一个位置(i=i-j+2),模式串指针j回到模式串的第一个位置。
若j>T.length,则当前子串匹配成功,返回当前子串第一个字符的位置(i-T.length)

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{
			i=i-j+2;
			j=1;	//指针后退重新开始
		}
	}
	if(j>T.length)	return k;
	else return 0;	//j<=T.length,说明最后主串长度不够了,匹配失败
}

4.2.1 KMP算法

int Index_KMP(SString S,SString T,int next[]){
	int i=1,j=1;
	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;
}

求模式串的next数组(手算)

next[1]无脑写0
next[2]无脑写1
在不匹配的位置前边,画一条分界线,模式串一步一步向后移,知道分界线之前“能对上”,或模式串完全跨过分界线为止,此时j指向哪,next数组值就是多少

第五章 树与二叉树

5.1 树

树的性质

  1. 树中的结点等于所有结点的度数加1。
  2. 度为m的树中第i层上至多有m(i-1)个结点。m叉树的第i层至多有m(i-1)个结点。
  3. 高度为h的m叉树至多有(mh-1)/(m-1)个结点。
  4. 具有n个结点的m叉树的最小高度为logm(n(m-1)+1)向上取整。
  5. 高度为h的m叉树至少有h个结点。高度为h,度为m的树至少有h+m-1个结点。

二叉树的性质

二叉树的五种形态:
空二叉树,只有左子树,只有右子树,只有根节点,左右子树都有。

满二叉树:一棵高度为h,且含有2h-1个结点的二叉树。
特点:①只有最后一层有叶子节点。②不存在度为1的结点。③按层序从1开始编号,结点i的左孩子为2i,有孩子为2i+1,结点i的父节点为i/2向下取整。

完全二叉树:当且仅当其每个节点都与高度为h的满二叉树中编号为1~n的结点一一对应时,称为完全二叉树。
特点:①只有最后两层才可能有叶子结点。②最多只有一个度为1的结点。③同上。④i<=n/2向下取整为分支结点,i>n/2向下取整为叶子结点。

二叉排序树
特点左子树上所有结点的关键字均小于根结点的关键字;
右子树上所有结点的关键字均大于根结点的关键字;
左子树和右子树又各是一棵二叉排序树

平衡二叉树:树上任一结点的左子树和右子树的深度之差不超过1

二叉树的常考性质

  1. 设非空二叉树中度为0、1和2的结点个数分别为n0、n1和n2 ,则n0=n2+1(叶子结点比二分支结点多一个)。假设树总结点总数为n,则①n=n0+n1+n2 ②n=n1+2n2 +1。②-①=n0=n2+1
  2. m叉树的第i层至多有m(i-1)个结点。二叉树的第i层至多有2(i-1)个结点。
  3. 高度为h的m叉树至多有(mh-1)/(m-1)个结点。高度为h的二叉树至多有2h-1个结点(满二叉树)。

完全二叉树的常考性质

  1. 具有n个(n>0)结点的完全二叉树的高度h为log2(n+1)向上取整 或 log2n 向下取整+ 1。
    高为h的满二叉树共有2h-1个结点。
    高为h-1的满二叉树共有2h-1-1个结点。
    高为h的完全二叉树至少2h-1个结点,至多2h-1。
  2. 对于完全二叉树,可以由结点数n推出度为0、1和2的结点个数为n0、n1和n2 。完全二叉树最多只有一个度为1的结点,即n1=0或1
    n0=n2+1→n0+n2一定是奇数
    若完全二叉树又2k个偶数结点,则必有n1=1, n0=k , n1 n2=k-1
    若完全二叉树又2k-1个奇数结点,则必有n1=0, n0=k , n2=k-1

5.2 二叉树存储结构

顺序存储

定义一个长度为MaxSize的数组t,按照从上到下,从左至右的顺序依次存储完全二叉树中的各个节点

#define MaxSize 100
struct TreeNode{
	ElemType value;			//结点中的数据元素
	bool isEmpty;			//结点是否为空
};
TreeNode t[MaxSize];

初始化

for (int i=0; i<MaxSize; i++){
	t[i].isEmpty = true;
	//初始化时所有结点标记为空
}

二叉树的顺序存储结构,只适合存储完全二叉树

链式存储

typedef struct BiTNode{
	ElemType data;						//数据域
	struct BiTNode *lchild,*rchild;		//左、右孩子指针
}BiTNode,*BiTree;

在这里插入图片描述
n个结点的二叉链表共有n+1个空链域。空链域可以构造线索二叉树。

构建二叉树

struct ElemType{
	int value;
};
typedef struct BiTNode{
	ElemType data;
	struct BiTNode *lchild,*rchild;		//左、右孩子指针
}BiTNode,*BiTree;
//定义一棵空树
BiTree root = NULL;
//插入根结点
root = (BiTree) malloc(sizeof(BiTNode));
root->data = {1};
root->lchild = NULL;
root->rchild = NULL;

//插入新结点
BiTNode *p = (BiTNode *)malloc(sizeof(BiTNode));
p->data={2};
p->lchild = NULL;
p->rchild = NULL;
root->lchild = p;       		//作为根结点的左孩子

用这种方式,很容易找到值定结点的左孩子或右孩子,但找它的父节点,只能从根开始遍历寻找,对此,还可以在定义结构体的时候加上父结点指针(三叉链表),这样方便找父结点,但是不常考

typedef struct BiTNode{
	ElemType data;
	struct BiTNode *lchild,*rchild;
	struct BiTNode *parent;			//父结点指针
}

5.3二叉树的遍历

先序遍历

先序遍历:根左右(NLR)
1.若二叉树为空,则什么也不做。
2.若非空:
访问根结点
②先序遍历左子树
③先序遍历右子树

脑补空结点,从根节点出发,画一条路;如果左边还有没走的路,优先往左边走,走到路的尽头(空结点)就往回走,如果左边没路了,就往右边走,如果左、右都没路了,则往上面走。
先序遍历—第一次路过时访问结点(每个结点都会被路过三次)

typedef struct BiTNode{
	ElemType data;
	struct BiTNode *lchild,*rchild;		//左、右孩子指针
}BiTNode,*BiTree;
//先序遍历
void PreOrder(BiTree T){
	if(T != NULL){
		visit(T);						//访问根结点
		PreOrder(T->lchild);			//递归遍历左子树
		PreOrder(T->rchild)				//递归遍历右子树
	}
}

中序遍历

中序遍历:左根右(LNR)
1.若二叉树为空,则什么也不做。
2.若非空:
①中序遍历左子树
②访问根结点
③中序遍历右子树

脑补空结点,从根节点出发,画一条路;如果左边还有没走的路,优先往左边走,走到路的尽头(空结点)就往回走,如果左边没路了,就往右边走,如果左、右都没路了,则往上面走。
中序遍历——第二次路过时访问结点(每个结点都会被路过三次)

typedef struct BiTNode{
	ElemType data;
	struct BiTNode *lchild,*rchild;		//左、右孩子指针
}BiTNode,*BiTree;
//后序遍历
void InOrder(BiTree T){
	if(T != NULL){
		InOrder(T->lchild);				//递归遍历左子树
		visit(T);						//访问根结点
		InOrder(T->rchild)				//递归遍历右子树
	}
}

后序遍历

后序遍历:左右根(LRN)
1.若二叉树为空,则什么也不做。
2.若非空:
①后序遍历左子树
②后序遍历右子树
③访问根结点

脑补空结点,从根节点出发,画一条路;如果左边还有没走的路,优先往左边走,走到路的尽头(空结点)就往回走,如果左边没路了,就往右边走,如果左、右都没路了,则往上面走。
中序遍历——第三次路过时访问结点(每个结点都会被路过三次)

typedef struct BiTNode{
	ElemType data;
	struct BiTNode *lchild,*rchild;		//左、右孩子指针
}BiTNode,*BiTree;
//后序遍历
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);
		//树的深度=Max(左子树深度,右子树深度)+1
		return l>r ? l+1 : r+1;
	}
}

层序遍历

算法思想:
①初始化一个辅助队列
②根节点入队
③若队列非空,则队头结点出队,访问该结点,并将其左、右孩子插入队尾(如果有的话)
④重复③直至队列为空

//二叉树的结点(链式存储)
typedef struct BiTNode{
	ElemType data;
	struct BiTNode *lchild,*rchild;		//左、右孩子指针
}BiTNode,*BiTree;
//链式队列结点
typedef struct LinkNode{
	BiTNode data;
	struct LinkNode *next;
}LinkNode;
typedef struct{					//链式队列
	LinkNode *front,*rear;		//队列的队头和队尾指针
}LinkQueue;
//初始化队列(带头结点)
void InitQueue(LinkQueue &Q){
	//初始时, front、rear 都指向头节点
	Q.front=Q.rear=(LinkNode*)malloc(sizeof(LinkQueue));
	Q.front->next=NULL;
}
void testLinkQueue(){
	LinkQueue();	//声明一个队列
	InitQueue();	//初始化队列
}
//新元素入队(带头节点)
void EnQueue(LingkQueue &Q,ElemType x){
	LinkNode *s=(LinkNode *)malloc(sizeof(LinkNode));//申请新节点
	s->data=x;			//x放到新节点当中
	s-next=NULL;		//
	Q.rear->next=s;		//新节点插入到rear之后
	Q.rear=s;			//修改表尾指针
}
//出队
bool DeQueue(LinkQueue &Q,ElemType &x){
	if(Q.front==Q.rear)
		return false;			//空队
	LinkNode *p=Q.front->next;
	x=p->data;					//用变量x返回队头元素
	Q.front->next=p->next;		//修改头节点的next指针
	if(Q.rear==p)				//此次修改是最后一个节点出队
		Q.rear=Q.front;			//修改rear指针
	free(p);					//释放节点空间
	return true;
}
//层序遍历
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在中序遍历序列中的前驱?
②如何找到p的中序后继?

思路:从根节点出发,重新进行一次中序遍历,指针q记录当前访问的结点,指针pre记录上一个被访问的结点。
①当q == p时,pre为前驱
②当pre == p时,q为后继

中序线索二叉树

在这里插入图片描述

//线索二叉树结点
typedef struct ThreadNode{
	ElemType data;
	struct ThreadNode *lchild,*rchild;
	int ltag,rtag;							//左右线索标志
}ThreadNode, *ThreadTree;
// tag == 0,表示指针指向孩子
// tag == 1,表示指针是"线索"

在这里插入图片描述

先序线索二叉树

在这里插入图片描述

后序线索二叉树

在这里插入图片描述

二叉树的线索化

中序线索化

ThreadNode *pre=NULL   //全局变量pre,指向当前访问节点的前驱
//线索二叉树结点
typedef struct ThreadNode{
	ElemType data;
	struct ThreadNode *lchild,*rchild;
	int ltag,rtag;							//左右线索标志
}ThreadNode, *ThreadTree;
// tag == 0,表示指针指向孩子
// tag == 1,表示指针是"线索"

//中序线索化二叉树T
void CreateInThread(ThreadTree T){
	pre=NULL;							//pre初始为NULL
	if(T!=NULL){						//非空二叉树才能线索化
		InThread(T);					//中序线索化二叉树
		if(pre->rchild==NULL)
			pre->rtag=1;				//处理遍历的最后一个结点
	}
}
//中序遍历二叉树,一边遍里一边线索化
void InThread(ThreadTree T){
	if(T!=NULL){
		InThread(T->lchild);			//中序遍历左子树
		visit(T);						//访问根结点
		InThread(T->rchild);			//中序遍历右子树
	}
}

void visit(ThreadNode *p){
	if(q->lchild==NULL){				//左子树为空建立前驱线索
		q->lchild=pre;
		q->ltag=1;
	}
	if(pre!=NULL&&pre->rchild==NULL){
		pre->rchild=q;				 	//建立前驱节点的后继线索
		pre->rtag=1;
	}
	pre=q;
}

先序线索化

ThreadNode *pre=NULL   //全局变量pre,指向当前访问节点的前驱
//线索二叉树结点
typedef struct ThreadNode{
	ElemType data;
	struct ThreadNode *lchild,*rchild;
	int ltag,rtag;							//左右线索标志
}ThreadNode, *ThreadTree;
// tag == 0,表示指针指向孩子
// tag == 1,表示指针是"线索"

//先序线索化二叉树T
void CreateInThread(ThreadTree T){
	pre=NULL;							//pre初始为NULL
	if(T!=NULL){						//非空二叉树才能线索化
		PreThread(T);					//中序线索化二叉树
		if(pre->rchild==NULL)
			pre->rtag=1;				//处理遍历的最后一个结点
	}
}
//先序遍历二叉树,一边遍里一边线索化
void PreThread(ThreadTree T){
	if(T!=NULL){
		visit(T);						//访问根结点
		if(T->ltag==0)					//lchild不是前驱线索
			PreThread(T->lchild);			//先序遍历左子树
/*左孩子指针一旦被线索化之后,他所指向的结点就是当前访问结点的前驱结点。
如果我们按照前驱线索往回去访问上一个结点的话,那就相当于我们访问完一个节点之后,又回头访问他的前驱,就会导致转圈。*/	
		PreThread(T->rchild);			//先序遍历右子树
	}
}

void visit(ThreadNode *p){
	if(q->lchild==NULL){				//左子树为空建立前驱线索
		q->lchild=pre;
		q->ltag=1;
	}
	if(pre!=NULL&&pre->rchild==NULL){
		pre->rchild=q;				 	//建立前驱节点的后继线索
		pre->rtag=1;
	}
	pre=q;
}

后序线索化

ThreadNode *pre=NULL   //全局变量pre,指向当前访问节点的前驱
//线索二叉树结点
typedef struct ThreadNode{
	ElemType data;
	struct ThreadNode *lchild,*rchild;
	int ltag,rtag;							//左右线索标志
}ThreadNode, *ThreadTree;
// tag == 0,表示指针指向孩子
// tag == 1,表示指针是"线索"

//后序线索化二叉树T
void CreateInThread(ThreadTree T){
	pre=NULL;							//pre初始为NULL
	if(T!=NULL){						//非空二叉树才能线索化
		PostThread(T);					//后序线索化二叉树
		if(pre->rchild==NULL)
			pre->rtag=1;				//处理遍历的最后一个结点
	}
}
//后序遍历二叉树,一边遍里一边线索化
void PostThread(ThreadTree T){
	if(T!=NULL){
		PostThread(T->lchild);			//后序遍历左子树
		PostThread(T->rchild);			//后序遍历右子树
		visit(T);						//访问根结点
	}
}

void visit(ThreadNode *p){
	if(q->lchild==NULL){				//左子树为空建立前驱线索
		q->lchild=pre;
		q->ltag=1;
	}
	if(pre!=NULL&&pre->rchild==NULL){
		pre->rchild=q;				 	//建立前驱节点的后继线索
		pre->rtag=1;
	}
	pre=q;
}

中序线索二叉树找中序后继

中序线索二叉树中找到指定结点*p的中序后继next
①若p->rtag == 1,则next=p->rchild
②若p->rtag == 0

//找到以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;	//rtag==1直接返回后继线索
}

//对中序线索二叉树进行中序遍历(利用线索实现的非递归算法)
void Inorder(ThreadNode *T){
	for(ThreadNode *p=Firstnode(T);p!=NULL;p=Nextnode(p))
		visit(p);
}

中序线索二叉树找中序前驱

中序线索二叉树中找到指定结点*p的中序后继pre
①若p->ltag == 1,则pre=p->rchild
②若p->ltag == 0

//找到以P为根的子树中,最后一个被中序遍历的结点
ThreadNode *Firstnode(ThreadNode *p){
	//循环找到最右下结点(不一定是叶结点)
	while(p->rtag==0) p=p->rchild;
	return p;
}

//在中序线索二叉树中找到结点p的前驱结点
ThreadNode *Prenode(ThreadNode *p){
	//右子树中最左下结点
	if(p->ltag==0) return Lastnode(p->rchild);
	else return p->lchild;	//rtag==1直接返回后继线索
}

//对中序线索二叉树进行逆向中序遍历
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->ltag == 1,则pre=p->rchild
②若p->ltag == 0
在这里插入图片描述

后序线索二叉树找后序后继

先序线索二叉树中找到指定结点*p的先序后继next
①若p->rtag == 1,则next=p->rchild
②若p->rtag == 0
在这里插入图片描述

后序线索二叉树找后序前驱

先序线索二叉树中找到指定结点*p的先序后继pre
①若p->ltag == 1,则pre=p->rchild
②若p->ltag == 0

5.4 树和森林

双亲表示法

双亲表示法:每个结点中保存指向双亲的“指针”
找双亲容易,孩子难

#define MAX_TREE_SIZE 100			//树中最多结点数
typedef struct{						//树的结点定义
	ElemType data;					//数据元素	
	int parent;						//双亲位置域
}PTNode;
typedef struct{						//树的类型定义
	PTNode nodes[MAX_TREE_SIZE]		//双亲表示
	int n;							//结点数
}PTree;

孩子表示法(顺序+链式)

找孩子容易,双亲难

struct CTNode{
	int child;				//孩子结点在数组中的位置
	struct CTNode *next;	//下一个孩子
};
typedef struct {
	ElemType data;
	struct CTNode *firstChild;	//第一个孩子
}CTBox;
typedef struct{
	CTBox nodes[NAX_TREE_SIZE];
	int n,r;					//结点数和根的位置
	
}CTree;

孩子兄弟表示法(链式)

typedef struct CSNode{
	ElemType data;
	struct CSNode *firstchild,*nextsibling;//第一个孩子和右兄弟指针
}CSNode,*CSTree;

*firstchild可以看作左孩子。*nextsibling可以看作有孩子。

树和森林的遍历

树的先根遍历

先根遍历。若树非空,先访问根结点,再依次对每棵子树进行先根遍历。

void PreOrder (TreeNode *R){
	if (R!=NULL){
		visit(R);
		while(R还有下一个子树T);
			PreOrder(T);
	}
}

树的后根遍历

后根遍历。若树非空,先队每棵子树进行后根遍历,最后访问根结点。

void PostOrder (TreeNode *R){
	if (R!=NULL){
		
		while(R还有下一个子树T);
			PostOrder (T);
		visit(R);
	}
}

树的层次遍历

③层次遍历(用队列实现)
1.若树非空,则根结点入队
2.若队列非空,队头元素出队并访问,同时将该元素的孩子依次入队
3.重复2知道队列为空

二叉排序树

定义

左子树上所有结点的关键字均小于根结点的关键字。
右子树上所有结点的关键字均大于根结点的关键字。
左右子树又各是一棵二叉排序树。
进行中序遍历,可以得到一个递增的有序序列。

//二叉排序树结点
typedef struct BSTNode{
	int key;
	struct NSTNode *lchild,*rchild;
	
}BSTNode,*BSTree;

查找

若树非空,目标值与根结点的值比较,若相等,则查找成功;若小于根结点,则在左子树上找,否则在右子树上找。

//非递归算法
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 *BST_Search(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);	//在右子树查找
}

插入

若原二叉排序树为空,则直接插入结点;否则,若关键字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;				//返回1,插入成功
	}
	else if(k==T->key)			//树中存在相同关键字的结点,插入失败
		return 0;
	else if(k<T->key)			//插入到T的左子树
		return BST_Insert(T->lchild,k);
	else      					//插入到T的右子树
		return BST_Insert(T->rchlid,k);
}

构造

//按照 str[] 中的关键字序列建立二叉排序树
void Creat_BST(BSTree &T,int str[],int n){
	T=NULL;  			//初始化T为空树
	while(i<n){			//依次将每个关键字插入到二叉排序树中
		BST_Insert(T,str[i]);
		i++;
	}
}

删除

先搜索找到目标结点:
①若被删除结点z时叶子节点,则直接删除,不会破坏二叉排序树的性质。
②若结点z只有一颗左子树或者右子树,则让z的子树成为z父节点的子树,代替z的位置。
③若结点z有左右两棵子树,则令z的直接后继(或直接前驱)代替z,然后从二叉排序树中删去这个直接后继(或直接前驱),这样就转换成了第一或第二种情况。进行中序遍历,可以得到一个递增的有序序列
z的后继:z的右子树中最左下结点(该节点一定没有左子树)
z的前驱:z的左子树中最右下结点(该节点一定没有右子树)

平衡二叉树

树上任一结点的左子树和右子树的高度不超过1。结点的平衡因子=左子树高-右子树高

当插入操作中,只要将最小不平衡子树调整平衡,则其他祖先结点都会恢复平衡。

在这里插入图片描述

调整最小不平衡子树A:
LL: 在A的左孩子的左子树中插入导致不平衡
RR: 在A的右孩子的右子树中插入导致不平衡
LR: 在A的左孩子的右子树中插入导致不平衡
RL: 在A的右孩子的左子树中插入导致不平衡
只有左孩子才能右上旋,只有有孩子才能左上旋

LL:将A的左孩子右上旋
在这里插入图片描述

LL平衡旋转(右单旋转)。由于在结点A的左孩子(L)的左子树(L)上插入了新结点,A的平衡因子由1增至2,导致以A为根的子树失去平衡,需要一次向右的旋转操作。将A的左孩子B向右上旋转代替A成为根结点,将A结点向右下旋转成为B的右子树的根结点,而B的原右子树则作为A结点的左子树。

实现f向右下旋转p向右上旋转
其中f是爹,p为左孩子,gf为f他爹
①f->lchild=p->rchild;
②p->rchild = f;
③gf->lchild/rchild = p;
在这里插入图片描述

RR:将A的左孩子左上旋
在这里插入图片描述

RR平衡旋转(左单旋转)。由于在结点A的右孩子(R)的右子树®上插入了新结点,A的平衡因子由-1减至-2,导致以A为根的子树失去平衡,需要一次向左的旋转操作。将A的右孩子B向左上旋转代替A成为根结点,将A结点向左下旋转成为B的左子树的根结点,而B的原左子树则作为A结点的右子树

实现f向左下旋转p向左上旋转
其中f是爹,p为右孩子,gf为f他爹
①f->rchild=p->lchild;
②p->lchild = f;
③gf->lchild/rchild = p;
在这里插入图片描述

LR:将A的左孩子的右孩子,先左上旋再右上旋
在这里插入图片描述
RL:将A的右孩子的左孩子,先右上旋再左上旋
在这里插入图片描述

哈夫曼树

带权路径长
结点的:有某种现实意义的值(如:表示结点的重要性)
结点的带权路径长度:从树的根到该结点的路径长度(经过的边数)
最优二叉树:带权路径长度最小的二叉树称哈夫曼树也称最优二叉树。

哈夫曼树的构造
1)将这个n个结点分别作为n棵仅含一个结点的二叉树,构成森林F。
2)构造一个新结点,从F中选取两颗根结点权值最小的树作为新结点的左、右子树,并将新的结点的权值置为左、右子树上根结点的权值之和。
3)从F中删除刚才选出的两棵树,同时将新得到的树加入F中。
4)重复2、3步骤,直到F中只剩下一棵树为止。

每个初始节点最终都成为叶子节点,且权值越小的系欸但到根结点的路径长度越大
哈夫曼树的结点总数为2n-1
哈夫曼树中不存在度为1的结点。
哈夫曼树并不唯一,但WPL(带权路径长度)必然相同且最优。

哈夫曼编码

固定长度编码:每个字符用相等长度的二进制位表示
可变长度编码:允许不同字符用不等长的二进制位表示
若没有一个编码是另一个编码的前缀,则称这样的编码为前缀编码

第六章 图

图的基本概念

图的定义

图G由顶点集v和边集E组成,记为G=(V,E),其中V(G)表示图G中顶点的有限非空集;E(G)表示图G中顶点之间的关系(边〉集合。若V={v,2 … , vn},则用|V|表示图G中顶点的个数,也称图G的阶,E= {(u, v) | u∈V,v∈V},用|E|表示图G中边的条数。
在这里插入图片描述
无向图
在这里插入图片描述

若E是无向边(简称边)的有限集合时,则图G为无向图。边是顶点的无序对,记为(v, w)或(w, v),因为(v, w)=(w, v),其中v、w是顶点。可以说顶点w和顶点v互为邻接点。边(v, w)依附于顶点w和v,或者说边(v, w)和顶点v、w相关联。
G2 =(V2,E2)
V2={A,B,C,D,E}
E2= {(A,B),(B,D),(B,E),(C,D),(C,E),(D,E)}

对于无向图:顶点v的度是指依附于该顶点的边的条数,记为TD(v)。

有向图
在这里插入图片描述
若E是有向边(也称弧)的有限集合时,则图G为有向图。弧是顶点的有序对,记为<v, w>,其中v、w是顶点,v称为弧尾,w称为弧头,<v, w>称为从顶点v到顶点w的弧,也称v邻接到w,或w邻接自v。<v, w>≠<w, v>

G1=(V1,E1)
V1={A, B,C,D,E}
E1= {<A, B>,<A, C>,<A,D>,<A,E>,<B,A>,<B,C>,<B,E>,<C, D>}

对于有向图:
入度是以顶点v为终点的有向边的数目,记为ID(v);
出度是以顶点v为起点的有向边的数目,记为OD(v)。
顶点v的度等于其入度和出度之和,即TD(v)= ID(v)+ OD(v)。

路径――顶点v,到顶点v,之间的一条路径是指顶点序列,vp,v1,v2…vm,vq
回路――第一个顶点和最后一个顶点相同的路径称为回路或环
简单路径――在路径序列中,顶点不重复出现的路径称为简单路径。
简单回路――除第一个顶点和最后一个顶点外,其余顶点不重复出现的回路称为简单回路。
路径长度――路径上边的数目
点到点的距离――从顶点u出发到顶点v的最短路径若存在,则此路径的长度称为从u到v的距离若从u到v根本不存在路径,则记该距离为无穷(∞)

对于n个顶点的无向图G,
若G是连通图,则最少有n-1条边
若G是非连通图,则最多可能有C2n-1条边

对于n个顶点的有向图G
若G是强连通图,则最少有n条边(形成回路)

图的储存及基本操作

邻接矩阵法

在这里插入图片描述

#define MaxVertexNum 100			//顶点数目的最大值100x100
typedef struct{
	char Vex[MaxVertexNum];			//顶点表
	int Edge[MaxVertexNum] [MaxVertexNum]; //邻接矩阵,边表
	int vexnum,arcnum;				//图的当前顶点数和边数/弧数
}MGraph;

无向图:第i个结点的度 = 第i行(或第i列)的非零元素个数。

有向图:
第i个结点的出度 = 第i行的非零元素个数。
第i个结点的入度 = 第i列的非零元素个数。
第i个结点的度 = 第i行、第i列的非零元素个数之和。

邻接矩阵求顶点的度/出度/入度/的时间复杂的为O(|v|)

在这里插入图片描述

#define MaxVertexNum 100			//顶点数目的最大值100x100
#define INFINITY 最大的int值		//宏定义常量“无穷”
typedef char VertexType;			//顶点的数据类型
typedef int EdgeType;				//带权图中边上权值的数据类型
typedef struct{
	 VertexType[MaxVertexNum];			//顶点
	EdgeType[MaxVertexNum] [MaxVertexNum]; //边权
	int vexnum,arcnum;				//图的当前顶点数和弧数
}MGraph;

邻接矩阵的空间复杂度:O(|V|2) 只和顶点数相关,和实际的边无关。
适用于稠密图

邻接表法(顺序+链式存储)

在这里插入图片描述
边结点的数量是2|E|,整体空间复杂度为O(|V|+2|E|)

#define MaxVertexNum 100;
//边/弧
typedef struct ArcNode{
	int adjvex;					//边指向哪个结点
	struct ArcNode *next;				//指向下一条弧的指针
	//InfoType info;			//边权值
}ArcNode;
//顶点
typedef struct VNode{
	VertexType data;			//顶点信息
	ArcNode *first;				//第一条边/弧
	
}VNode,AdjList[MaxVertexNum];

//用邻接表存储的图
typedef struct{
	AdjList vertices;
	int vexnum,arcnum;
}AGraph;

在这里插入图片描述

边结点的数量是|E|,整体空间复杂度为O(|V|+|E|)

在这里插入图片描述

十字链表

在这里插入图片描述
沿着绿色指针线一直往后找,就可以找到从当前这个顶点往外发射的所有的弧、边。
顺着橙色找下去,就可以找到所有指向当前这个顶点的弧。

空间复杂度:O(|V|+|E|)

邻接多重表

在这里插入图片描述
空间复杂度:O(|V|+|E|)
删除边,删除结点等操作方便。

在这里插入图片描述

图的基本操作

Adjacent(G,x,y):判断图G是否存在边<x,y>或者(x,y)。
无向图:①邻接矩阵时间复杂度:O(1);②邻接表时间复杂度:O(1)~O(|V|);
有向图:①邻接矩阵时间复杂度:O(1);②邻接表时间复杂度:O(1)~O(|V|);

Neighbors(G,x):列出图G中与结点x邻接的边。
无向图:①邻接矩阵时间复杂度:O(V);②邻接表时间复杂度:O(1)~O(|V|);
有向图:①邻接矩阵时间复杂度:O(1);②邻接表时间复杂度:出边:O(1)~O(|V|);入边:O(|E|);

InsertVertex(G,x):在图G中插入顶点x。
无向图:①邻接矩阵时间复杂度:O(1);②邻接表时间复杂度:O(1);
有向图:①邻接矩阵时间复杂度:O(1);②邻接表时间复杂度:O(1);

DeleteVertex(G,x):从图G中删除顶点x。
无向图:①邻接矩阵时间复杂度:O(V);②邻接表时间复杂度:O(1)~O(|E|);
有向图:①邻接矩阵时间复杂度:O(1);②邻接表时间复杂度:删出边:O(1)~O(|V|);删入边:O(|E|);

AddEdge(G,x,y):若无向边(x, y)或有向边<x, y>不存在,则向图G中添加该边。
无向图:①邻接矩阵时间复杂度:O(V);②邻接表时间复杂度:O(1)~O(|E|);
有向图:①邻接矩阵时间复杂度:O(V);②邻接表时间复杂度:O(1)~O(|E|);

RemoveEdge(G,x,y):若无向边(x,y)或有向边<x, y>存在,则从图G中删除该边。

FirstNeighbor(G,x):求图G中顶点x的第一个邻接点,若有则返回顶点号。若x没有邻接点或图中不存在x,则返回-1。
无向图:①邻接矩阵时间复杂度:O(1)~O(|V|);②邻接表时间复杂度:O(1);
有向图:①邻接矩阵时间复杂度:O(1)~ O(|V|);②邻接表时间复杂度:找出边邻接点:O(1);找入边邻接点 :O(1)~O(|E|);

NextNeighbor(G,x,y):假设图G中顶点y是顶点x的一个邻接点,返回除y之外顶点x的下一个邻接点的顶点号,若y是x的最后一个邻接点,则返回-1。
无向图:①邻接矩阵时间复杂度:O(1)~O(|V|);②邻接表时间复杂度:O(1);

Get_edge_value(G,x,y):获取图G中边(x, y)或<x, y>对应的权值。
Set_edge_value(G,x,y,v):设置图G中边(x, y)或<x, y>对应的权值为v。
无向图:①邻接矩阵时间复杂度:O(1);②邻接表时间复杂度:O(1)~O(|V|);
有向图:①邻接矩阵时间复杂度:O(1);②邻接表时间复杂度:O(1)~O(|V|);

图的遍历

广度优先搜索

类似于树的层序遍历
广度优先遍历要点:
1.找到与一个顶点相邻的所有顶点。
2.标记哪些顶点被访问过。
3.需要一个辅助队列

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];			//访问标记数组
// 广度优先遍历
void BFS(Graph G,int v){				//从顶点v出发,广度优先遍历图G
	visit(v);							//访问初始顶点v
	visited[v] = TRUE;					//对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]){  			//w为v的尚未访问的邻接顶点
				visit(w);				//访问顶点w
				visited[w] = TRUE;		//对w做已访问标记
				EnQueue(Q,w);			//顶点w入队列
			}//if
	}//while
	

}

如果是非连通图,则无法遍历完所有结点

bool visited[MAX_VERTEX_NUM];			//访问标记数组
void BFSTraverse(Graph,G){				//对图G进行广度优先遍历
	for(i=0;i<G.vexnum;++1;)
		visited[i] = FAlSE;				//房屋内标记数组初始化
	InitQueue(Q);						//初始化辅助队列Q
	for(i=0;i<G.vexnum;++i)				//从0号顶点开始遍历
		if(!visited[i])					//对每个连通分量调用一次BFS
			BFS(G,i);					//vi为访问过,从vi开始BFS
}
// 广度优先遍历
void BFS(Graph G,int v){				//从顶点v出发,广度优先遍历图G
	visit(v);							//访问初始顶点v
	visited[v] = TRUE;					//对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]){  			//w为v的尚未访问的邻接顶点
				visit(w);				//访问顶点w
				visited[w] = TRUE;		//对w做已访问标记
				EnQueue(Q,w);			//顶点w入队列
			}//if
	}//while
	

}

广度优先遍历需要借助队列Q
邻接矩阵(无向图)存储的图时间复杂度:O(|V2|)
邻接表时间复杂度:O(|V|+|E|)

图的深度优先遍历

类似于树的先根遍历

bool visited[MAX_VERTEX_NUM];			//访问标记数组

void DFS(Graph G,int v){				//从顶点v出发,深度优先遍历图G
	visit(v);							//访问顶点v
	visited[v] = TRUE;					//设已访问标记
	for(w=FirstNeighbor(G,v);w>=0;w=NextNeighbor(G,v,w))
		if(!visited[w]){				//w为u的尚未访问的邻接顶点
			DFS(G,w);
		}//if

}

如果是非连通图,则无法遍历完所有结点

bool visited[MAX_VERTEX_NUM];			//访问标记数组
void DFSTraverse(Graph,G){				//对图G进行广度优先遍历
	for(v=0;v<G.vexnum;++1;)
		visited[v] = FAlSE;				//房屋内标记数组初始化
	for(v=0;v<G.vexnum;++v)				//从0号顶点开始遍历
		if(!visited[i])					//对每个连通分量调用一次DFS
			DFS(G,v);					//vi为访问过,从vi开始DFS
}
void DFS(Graph G,int v){				//从顶点v出发,深度优先遍历图G
	visit(v);							//访问顶点v
	visited[v] = TRUE;					//设已访问标记
	for(w=FirstNeighbor(G,v);w>=0;w=NextNeighbor(G,v,w))
		if(!visited[w]){				//w为u的尚未访问的邻接顶点
			DFS(G,w);
		}//if

}

就是执行完一边DFS之后,再次扫描数组看看有没有还是FALSE。

深度优先算法需要借助递归工作栈
空间复杂度:来自于函数调用栈,最好:O(1);最坏:O(|V|)

时间复杂度=访问各个结点所需要时间+探索各条边所需要时间
邻接矩阵:时间复杂度=O(|V|2)
邻接表:时间复杂度=O(|V|+|E|)

总结:对于无向图进行BFS/DFS遍历,调用BFS/DFS函数的次数=连通分量数。
对于连通图,只需要调用1次BFS/DFS
对于有向图进行BFS/DFS遍历,调用BFS/DFS函数的次数要具体分析,若起始顶点到其他各个顶点都有路径,则只需要调用1次BFS/DFS函数;对于强连通图,从任一结点出发都只需要调用1次BFS/DFS。

最小生成树

对于一个带权连通无向图G=(V, E),生成树不同,每棵树的权(即树中所有边上的秋值之和)也可能不同。设R为G的所有生成树的集合,若T为R中边的权值之和最小的生成
树,则T称为G的最小生成树(Minimum-Spanning-Tree, MST)。

Prim(普里姆)算法:

从某一个顶点开始构建生成树;每次将代价最小的新顶点纳入生成树,直到所有顶点都纳入为止。
时间复杂度:O(|V|2),适用于边稠密图。(只和顶点的个数有关)
在这里插入图片描述在这里插入图片描述同一个图可能有多个最小生成树,但 边的和都是一样的,都是最小的。

Kruskal(克鲁斯卡尔)算法:

每次选择一条权值最小的边,使这条边的两头连通(原本已经连通的就不选)直到所有结点连通。
时间复杂度:O(|E|log2|E|),适用于边稀疏图。(只和边有关)

最短路径

1.单源最短路径
BFS求无权图的单源最短路径:


// 广度优先遍历
void BFS_MIN_Distance(Graph G,int v){				//从顶点v出发,广度优先遍历图G
	//d[i]表示从u到i结点的最短路径
	for(i=0;i<G.vexnum;i++){
		d[i]=;					//初始化路径长度
		path[i]=-1;				//最短路径从哪个顶点过来
	}
	d[v]=0;
	visit(v);							//访问初始顶点v
	visited[v] = TRUE;					//对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]){  			//w为v的尚未访问的邻接顶点
				d[w]=d[v]+1;			//路径长度加1
				path[w] = v;			//最短路径应从v到w
				visited[w] = TRUE;		//对w做已访问标记
				EnQueue(Q,w);			//顶点w入队列
			}//if
	}//while
	

}

局限性:不适合带权路径。

Dijkstra(迪杰斯特拉)算法

时间复杂度:O(n2)或者O(|V|2)
不适合负权值的带权图。

Floyd(弗洛伊德)算法

Floyd算法:求出每一对顶点之间的最短路径
使用动态规划思想,将问题的求解分为多个阶段
对于n个顶点的图G,求任意一对顶点Vi->vj之间的最短路径可分为如下几个阶段:

#初始:不允许在其他顶点中转,最短路径是?
在这里插入图片描述

#0:若允许在V0中转,最短路径是?
在这里插入图片描述
在这里插入图片描述
#1∶若允许在V0、V1中转,最短路径是?
在这里插入图片描述
在这里插入图片描述
#2:若允许在V0、V1、V2中转,最短路径是?…
在这里插入图片描述
在这里插入图片描述
#n-1∶若允许在V0、V1、V2 …Vn-1中转,最短路径是?

从A(-1)和path(-1)开始,经过n轮递推,得到A(n-1)和path(n-1) n:结点个数

根据A(2)可知,V1到V2最短路径长度为4,根据path^(2)可知,完整路径信息为V1_V2

根据A(2)可知,V0到V2最短路径长度为10,根据path^(2)可知,完整路径信息为V0_V1_V2

根据A(2)可知,V1到V0最短路径长度为9,根据path^(2)可知,完整路径信息为V1_V2_V0

//.......准备工作,根据图的信息初始化矩阵A和path
for(int k=0;k<n;k++){			//考虑以Vk作为中转点
	for(int i=0;i<n;i++){		//遍历整个矩阵,i为行号,j为列号
		for(int j=0;j<n;j++){			
			if(A[i][j]>A[i][k]+A[k][j]){		//以Vk为中转点的路径更短
				A[i][j]>A[i][k]+A[k][j];		//更新最短路径长度
				path[i][j]=k;				//中转点
			}
		}
	}
}

Floyd算法不能解决带有“负权回路”的图(有负权值的边组成回路),这种图有可能没有最短路径。
时间复杂度:O(|V|3)
空间复杂度:O(|V|2)

总结

在这里插入图片描述
注:也可以用Dijkstra 算法求所有顶点的最短路径,重复|V|次即可,总的时间复杂度也是O(|V|3)

有向无环图

有向无环图:若一个有向图中不存在环,则称为有向无环图,简称DAG图
在这里插入图片描述

Step 1:把各个操作数不重复地排成一排
Step 2:标出各个运算符的生效顺序(先后顺序有点出入无所谓)
Step 3:按顺序加入运算符,注意“分层”在这里插入图片描述

Step 4:从底向上逐层检查同层的运算符是否可以合体
在这里插入图片描述

拓扑排序

AOV网(Activity on vertex NetWork,用顶点表示活动的网)∶
用DAG图(有向无环图)表示一个工程。顶点表示活动,有向边<Vi,VP>表示活动V必须先于活动V进行
在这里插入图片描述
拓扑排序:在图论中,由一个有向无环图的顶点组成的序列,当且仅当满足下列条件时,称为该图的一个拓扑排序:
①每个顶点出现且只出现一次。
②若顶点A在序列中排在顶点B的前面,则在图中不存在从顶点B到顶点A的路径。或定义为:拓扑排序是对有向无环图的顶点的一种排序,它使得若存在一条从顶点A到顶点B的路径,则在排序中顶点B出现在顶点A的后面。每个AOV网都有一个或多个拓扑排序序列。

拓扑排序的实现:
①从AOV网中选择一个没有前驱(入度为0)的顶点并输出。从网中删除该顶点和所有以它为起点的有向边。
③重复①和②直到当前的AOV网为空或当前网中不存在无前驱的顶点为止。

在这里插入图片描述

考量一个节点的入度。

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);			//栈顶元素出栈
		print[count++]=i;	//输出顶点i;
		for(p=G.vertices[i].firstarc;p;p=p->nextrac){
			//将所有i指向的顶点的入度减1,并将入度减为0的顶点压入栈S
			v=p->adjvex;
			if(!(--indegree[v]))
				Push(S,v);		//入度为0,则入栈
		}
	}//while
	if(count<G.vexnum)
		return false;		//排序失败,有向图中有回路
	else 
		return true;		//拓扑排序成功
}

时间复杂度:O(|V|+|E|)
若采用邻接矩阵,则需O(IV|2)

逆拓扑排序

对一个AOV网逆拓扑排序:
①从AOV网中选择一个没有后继(出度为O)的顶点并输出。
②从网中删除该顶点和所有以它为终点的有向边。
③重复①和②直到当前的AOV网为空。

考量一个结点的出度

在这里插入图片描述

逆拓扑排序的实现(DFS算法)

void DFSTraverse(Graph G){ 	//对图G进行深度优先遍历
	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);
}
void DFS(Graph G,int v){	//从顶点v出发,深度优先遍历图G
	visited[v]=TRUE;		//设已访问标记
	for(w=FirstNeighbor(G,v);w>=0; w=NextNeighor(G,v,w))
		if(!visited[w]){			// w为u的尚未访问的邻接顶点
			DFS(G,w);
		}//if
	print(v );		//输出顶点
}

关键路径

AOE网:在带权有向图中,以顶点表示事件,以有向边表示活动,以边上的权值表示完成该活动的开销(如完成活动所需的时间),称之为用边表示活动的网络,简称AOE网(Activity On Edge NetWork)

AOE网具有以下两个性质:
①只有在某顶点所代表的事件发生后,从该顶点出发的各有向边所代表的活动才能开始;
②只有在进入某顶点的各有向边所代表的活动都已结束时,该顶点所代表的事件才能发生。另外,有些活动是可以并行进行的

在AOE网中仅有一个入度为0的顶点,称为开始顶点〈源点)
,它表示整个工程的开始;
也仅有一个出度为0的顶点,称为结束顶点(汇点),它表示整个工程的结束。

从源点到汇点的有向路径可能有多条,所有路径中,具有最大路径长度的路径称为关键路径,而把关键路径上的活动称为关键活动
完我整个工程的最短时间就是天键路径的长度若关键活动不能按时完成,则整个工程的完成时间就会延长

事件vk的最早发生时间ve(k)——决定了所有从vk开始的活动能够开工的最早时间
活动ai的最早开始时间e(i)——指该活动弧的起点所表示的事件的最早发生时间
在这里插入图片描述

事件vk的最迟发生时间vl(k)——它是指在不推迟整个工程完成的前提下,该事件最迟必须发生的时间。
活动ai的最迟开始时间l(i)——它是指该活动弧的终点所表示事件的最迟发生时间与该活动所需时间之差。
在这里插入图片描述

活动ai的最早开始时间e(i)——指该活动弧的起点所表示的事件的最早发生时间
活动ai的最迟开始时间l(i)——它是指该活动弧的终点所表示事件的最迟发生时间与该活动所需时间之差。
在这里插入图片描述
活动ai的时间余量d(i)=l(i)-e(i),表示在不增加完成整个工程所需总时间的情况下,活动ai可以拖延的时间若一个活动的时间余量为零,则说明该活动必须要如期完成,d(i)=0即l(i) =e(i)的活动ai关键活动关键活动组成的路径就是关键路径

步骤:
①求所有事件的最早发生时间ve()
②求所有事件的最迟发生时间vl()
③求所有活动的最早发生时间e()
④求所有活动的最迟发生时间()
⑤求所有活动的时间余量d()

①按拓扑排序序列,依次求各个顶点的ve(k):
ve(源点)=0
ve(k)= Max {ve(j)+Weight(vj, vk)},vj为vk的任意前驱
②按逆拓扑排序序列,依次求各个顶点的vl(k):
vl(汇点)= ve(汇点)
vl(k)= Min{vl(j) - Weight(vk, vj)}, vj为vk的任意后继
③若边<vk, vj>表示活动ai,则有e(i)= ve(k)
④若边<vk,vj>表示活动ai,则有l(i)= vl(j) - Weight(vvk, vj)
⑤d(i)= l(i)-e(i)

第七章 查找

查找的基本概念

顺序查找和折半查找

顺序查找

顺序查找又称为线性查找,通常用于线性表(顺序、链式)。

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;			//0号位置存哨兵
	int i;
	for(i=ST.TableLen;ST.elem[i]!=key; --i);
	//查找成功,则返回元素下标;失败,返回-1.
	return i==ST.TableLen? -1 :i;
}
	

添加哨兵的好处就是 不用再去判断是否越界,效率更高。

查找效率分析 在这里插入图片描述
ASL成功=(n+1)/2
ASL失败=(n+1)时间复杂度O(n)

顺序查找的优化(对有序表)
在这里插入图片描述

折半查找

折半查找又称二分查找,仅适用于有序的顺序表

typedef struct{				//查找表的数据结构(顺序表)
	ElemType *elem;			//动态数组基址
	int TableLen;			//表长
}
//折半查找
int Binary_Search(SSTable L,ElemType key){
	int low=0,hight=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;			//从后半部分继续查找
		
	}
	
}

查找效率分析
在这里插入图片描述

查找成功
在这里插入图片描述

ASL成功=(1* 1+2* 2+3* 4+4* 4)/11=3
查找失败:
在这里插入图片描述
ASL失败=(3* 4+4* 8)/12

折半查找判定树的构造

在这里插入图片描述

如果当low和high之间有奇数个元素,则mid分隔后,左右两部分元素个数相等
如果当前low和high之间有偶数个元素,则mid分隔后,左半部分比右半部分少一个元素

折半查找的判定树中,若mid = ⌊(low + high)/2」,则对于任何一个结点)必有:右子树结点数-左子树结点数=0或1

折半查找的判定树一定是平衡二叉树
折半查找的判定树中,只有最下面一层不是满的
因此,元素个数为n时树高h=log2(n+1)向上取整

判定树结点关键字:左<中<右,满足二叉排序树的定义
失败结点:n+1个(等于成功结点的空链域数量)

树高h=og2(n+1)向上取整
查找成功的ASL<=h
查找失败的ASL<=h
时间复杂度:O(log2n)

分块查找

分块查找的基本思想:将查找表分为若干子块。块内元素可以是无序的,但块间是有序的,即第一个块中的最大关键字小于第二个块中的所有记录的关键字,第二个块中的最大关键字小于第三个块中的所有记录的关键字,以此类推。再建立一个索引表,索引表中的每个元素含有各块的最大关键字和各块中的第一个元素的地址,索引表按关键字有序排列。

块内无序,块间有序

//索引表
typedef struct{
	ElemType maxValue;
	int low,high;
}Index;

//顺序表存储实际元素
ElemType List[100];

分块查找,又称索引顺序查找,算法过程如下:
① 在索引表中确定待查记录所属的分块(可顺序,可折半)
②在块内顺序查找

若索引表中不包含目标关键字,则折半查找索引表最终停在low>high,要在low所指分块中查找
原因:最终low左边一定小于目标关键字,high右边一定大于目标关键字。而分块存储的索引表中保存的是各个分块的最大关键字。

查找效率分析(ASL)
假设,长度为n的查找表被均匀分配成b块,每块s个元素。
设索引查找和块内查找的平均查找长度为LI,LS,则分块查找的平均查找长度为:
用顺序查找索引表:LI=(b+1)/2; LS=(s+1)/2。则ASL=(b+1)/2+(s+1)/2=(s2+2s+n)/2s
当s=√n时,ASL最小=√n+1

B树和B+树

B树及基本操作

B树,又称多路平衡查找树,B树中所有结点的孩子个数的最大值称为B树的阶,通常用m表示。一棵m阶B树或为空树,或为满足如下特性的m叉树:
1)树中每个结点至多有m棵子树,即至多含有m-1个关键字。
2)若根结点不是终端结点,则至少有两棵子树。
3)除根结点外的所有非叶结点至少有「m/2]棵子树,即至少含有「m/2]-1个关键字。
5)所有的叶结点都出现在同一层次上,并且不带信息(可以视为外部结点或类似于折半查找判定树的查找失败结点,实际上这些结点不存在,指向这些结点的指针为空)。
在这里插入图片描述

m阶B树的核心特性:
1 ) 根节点的子树数∈[2,m],关键字数∈[1, m-1]。
其他结点的子树数∈[[m/2], m];关键字数∈[[m/2]-1, m-1]
2)对任一结点,其所有子树高度都相同
3)关键字的值:子树0<关键字1<子树1<关键字2<子树2<…(类比二叉查找树左<中<右)

注:大部分学校算B树的高度不包括叶子结点(失败结点)
问题:含n个关键字的m阶B树,最小高度、最大高度是多少?

最小高度——让每个结点尽可能的满,有m-1个关键字,m个分叉,则有n≤(m - 1)(1+m+ m2+m3+ …+mh-1-1)= mh -1,因此h ≥logm(n+1)

最大高度——让各层的分叉尽可能的少,即根节点只有2个分叉,其他结点只有[m/2]个分叉各层结点至少有:第一层1、第二层2、第三层2「m/2] …第h层2(「m/2])"-2
第h+1层共有叶子结点(失败结点)2(「m/2])h-1
n个关键字的B树必有n+1个叶子结点,则n+1 ≥2(「m/2])-1,即h ≤ log「m/2](n+1)/2 +1

B树的插入

在这里插入图片描述

核心要求
①对m阶B树——除根节点外,结点关键字个数「m/2]-1≤n≤m-1②子树0<关键字1<子树1<关键字2<子树2<…
新元素一定是插入到最底层“终端节点”,用“查找”来确定插入位置

在插入key后,若导致原结点关键字数超过上限,则从中间位置==(「m/2])将其中的关键字分为两部分==,左部分包含的关键字放在原结点中,右部分包含的关键字放到新结点中,中间位置(「m/2])的结点插入原结点的父结点。若此时导致其父结点的关键字个数也超过了上限,则继续进行这种分裂操作,直至这个过程传到根结点为止,进而导致B树高度增1。

B树的删除

若被删除关键字在非终端节点,则用直接前驱或直接后继来替代被删除的关键字
直接前驱:当前关键字左侧指针所指子树中“最右下”的元素
直接后继:当前关键字右侧指针所指子树中“最左下”的元素

兄弟够借。若被删除关键字所在结点删除前的关键字个数低于下限,且与此结点右(或左)兄弟结点的关键字个数还很宽裕,则需要调整该结点、右(或左)兄弟结点及其双亲结点(父子换位法)
说白了,当右兄弟很宽裕时,用当前结点的后继、后继的后继来填补空缺
当左兄弟很宽裕时,用当前结点的前驱、前驱的前驱来填补空缺

兄弟不够借。若被删除关键字所在结点删除前的关键字个数低于下限,且此时与该结点相邻的左、右兄弟结点的关键字个数均=「m/2]-1,则将关键字删除后与左(或右)兄弟结点及双亲结点中的关键字进行合并
在合并过程中,双亲结点中的关键字个数会减1。若其双亲结点是根结点且关键字个数减少至0(根结点关键字个数为1时,有2棵子树),则直接将根结点删除,合并后的新结点成为根;若双亲结点不是根结点,且关键字个数减少到「m/2]-2,则又要与它自己的兄弟结点进行调整或合并操作,并重复上述步骤,直至符合B树的要求为止。
在这里插入图片描述

B+树

在这里插入图片描述

B树和B+树区别

m阶B+树:
1)结点中的n个关键字对应n棵子树
m阶B树:
1)结点中的n个关键字对应n+1棵子树

m阶B+树:
2)根节点的关键字数n∈[1, m]
其他结点的关键字数n∈[「m/2], m]
m阶B树:
2)根节点的关键字数n∈[1, m-1]。
其他结点的关键字数n∈[「m/2]-1, m-1]

m阶B+树:
3)在B+树中,叶结点包含全部关键字,非叶结点中出现过的关键字也会出现在叶结点中
m阶B树:
3)在B树中,各结点中包含的关键字是不重复的

m阶B+树:
4)在B+树中,叶结点包含信息,所有非叶结点仅起索引作用,非叶结点中的每个索引项只含有对应子树的最大关键字和指向该子树的指针,不含有该关键字对应记录的存储地址。
m阶B树:
4)B树的结点中都包含了关键字对应的记录的存储地址

在这里插入图片描述

散列表

散列表(Hash Table),又称哈希表。是一种数据结构,特点是︰数据元素的关键字与其存储地址直接相关

1.除留余数法——H(key)= key % p
散列表表长为m,取一个不大于m但最接近或等于m的质数p。
设计目标是:让不同关键字的冲突尽可能地少。

2.直接定址法——H(key) = key或H(key)= a*key + b(例:学号)
其中,a和b是常数。这种方法计算最简单,且不会产生冲突。
它适合关键字的分布基本连续的情况,若关键字分布不连续,空位较多,则会造成存储空间的浪费。

3.数字分析法——选取数码分布较为均匀的若干位作为散列地址(例:手机号码
设关键字是r进制数((如十进制数),而r个数码在各位上出现的频率不一定相同,可能在某些位上分布均匀一些,每种数码出现的机会均等﹔而在某些位上分布不均匀,只有某几种数码经常出现,此时可选取数码分布较为均匀的若干位作为散列地址。这种方法适合于已知的关键字集合,若更换了关键字,则需要重新构造新的散列函数。

4.平方取中法――取关键字的平方值的中间几位作为散列地址。(例:身份证号
具体取多少位要视实际情况而定。这种方法得到的散列地址与关键字的每位都有关系,因此使得散列地址分布比较均匀,适用于关键字的每位取值都不够均匀或均小于散列地址所需的位数。

散列查找是典型的“用空间换时间”的算法,只要散列函数设计的合理,
则散列表越长,冲突的概率越低。

处理冲突的方法(开放定址法)

所谓开放定址法,是指可存放新表项的空闲地址既向它的同义词表项开放,又向它的非同义词表项开放。其数学递推公式为∶
H=(H(key) + di)% m
i = 0,1,2,…,k (k≤m- 1) ,m表示散列表表长;d为增量序列;i可理解为“第i次发生冲突”

①线性探测法一一 di=0,1,2,3,…, m-1;即发生冲突时,每次往后探测相邻的下一个单元是否为空

②平方探测法当di=02,12,-12,22…时,称为平方探测法,又称二次探测法其中ksm/2

第八章 排序

排序算法的评价指标:算法的稳定性。若待排序表中有两个元素Ri和Rj,其对应的关键字相同即keyi, = keyj,且在排序前Ri在Rj的前面,若使用某一排序算法排序后,Ri仍然在Rj的前面,则称这个排序算法是稳定的,否则称排序算法是不稳定的。

在这里插入图片描述

内部排序

数据都在内存中(关注如何使用算法,时间,空间复杂度更低)

插入排序

算法思想:每次将一个待排序的记录按其关键字大小插入到前面已排好序的子序列中,直到全部记录插入完成。

直接插入排序

// 直接插入排序
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];				//用temp暂存A[i]
			for(j==i-1;j>=0 && A[j]>temp;--j)	//检查所有前面已经排好序的元素
				A[j+1]=A[j];			//所有大于temp的元素都像后挪位
			A[j+1]=temp;			//复制到插入位置
		}
}
// 直接插入排序(带哨兵)
void InsertSort(int 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];			//复制到插入位置
		}
}

空间复杂度:O (1)
最好时间复杂度(全部有序)😮 (n)
最坏时间复杂度(全部逆序)😮 (n2)
平均时间复杂度:O (n2)
算法稳定性:稳定

折半插入排序

//折半查找排序
void InsertSort(int 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];					//插入操作
	} 
}

当low>high时折半查找停止,应将「low, i-1]内的元素全部右移,并将A[0]复制到 low 所指位置
当A[mid]==A[0]时,为了保证算法的“稳定性”,应继续在mid所指位置右边寻找插入位置

比起"直接插入排序”,比较关键字的次数减少,但是移动元素的次数没变,
整体来看时间复杂度依然是O(n2)

希尔排序

希尔排序:先追求表中元素部分有序,再逐渐逼近全局有序
希尔排序︰先将待排序表分割成若干形如L[i,i + d,i + 2d…, i + kd]的“特殊”子表,对各个子表分别进行直接插入排序。缩小增量d,重复上述过程,直到d=1为止。

//希尔排序
void ShellSort(int A[], int n){
	int d,i,j;
	//A[0]只是暂存单元,不是哨兵,当j<=0时,插入位置已到
	for(d=n/2;d>=1;d=d/2)			//步长变化
		for(i=d+1; i<=n;++i;)
			if(A[i]<A[i-d]){		//需要将A[i]插入有序增量子表
				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];		//插入 
			}//if

}

	**稳定性:不稳定
	适用性:只适用顺序表,不适用链表。**

交换排序

基于“交换”的排序︰根据序列中两个元素关键字的比较结果来对换这两个记录在序列中的位置

冒泡排序

从后往前(或从前往后)两两比较相邻元素的值,若为逆序(即A[i-1 ]>A[i]),则交换它们,直到序列比较完。称这样过程为“—趟”冒泡排序。

//交换
void swap(int &a,int &b){
	int temp =a ;
	a=b;
	b=temp;
}
//冒泡排序
void BubbeSort(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=ture;
			}
		if(flag==flase)
			return;					//本躺遍历结束后没有发生交换,说明表已经有序。
	}
}

空间复杂度:O(1)
时间复杂度:最好:O(n);最坏:O(n2) 平均:O(2)
稳定性:稳定
适用性:顺序表,链表

如果某一趟排序过程中未发生“交换”则算法可提前结束

快速排序

算法思想:在待排序表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)上,这个过程称为一次“划分”。然后分别递归地对两个子表重复上述过程,直至每部分内只有一个元素或空为止,即所有元素放在了其最终位置上。

//用第一个元素将待排序序列划分成左右两个部分
int Partition(int A[] ,int low,int high){
	int pivot=A[low];			//第一个元素作为枢轴
	while(low<high){			//用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;		//返回存放枢轴的最终位置
}

/快速排序
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);//划分右子表
	}
}

时间复杂度:O(n*递归层数)
最好时间复杂度:O(nlog2n)
最坏时间复杂度:O(n2)
平均**O(nlog2n)**

空间复杂度:O(递归层数)
最好空间复杂度:O(log2n)
最坏空间复杂度:O(n)

选择排序

选择排序:每一趟在待排序元素中选取关键字最小(或最大)的元素加入有序子序列

简单选择排序

//简单选择排序
void SelectSort(int A[],int n){
	for(int i=0; i<n-1; i++){		//一共进行n-1趟
		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] );//封装的swap()函数共移动元素3次
	}
}
//交换
void swap(int &a,int &b){
	int temp =a ;
	a=b;
	b=temp;
}

空间复杂度:O(1)
时间复杂度:O(n2)
稳定性:不稳定
适用性:顺序表,链表

堆排序

若n个关键字序列L[ 1…n]满足下面某一条性质,则称为堆(Heap):
①若满足︰L(i)≥L(2i)且L(i)≥L(2i+1) (1 ≤ i ≤n/2) -―大根堆(大顶堆)
②若满足︰L(i)≤L(2i)且L(i)≤L(2i+1) (1 ≤ i <n/2) -―小根堆(小顶堆)
大根堆:完全二叉树中,根≥左、右
小根堆:完全二叉树中,根≤左、右

思路:把所有非终端结点都检查一遍,是否满足大根堆的要求,如果不满足,则进行调整。
在顺序存储的完全二叉树中,非终端结点编号i<=[n/2」(向下取整)

检查当前结点是否满足根≥左、右若不满足,将当前结点与更大的一个孩子互换

若元素互换破坏了下一级的堆,则采用相同的方法继续往下调整(小元素不断“下坠”)

/建立大根堆
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];		//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];			//被筛选结点的值放入最终位置
}

选择排序:每一趟在待排序元素中选取关键字最大的元素加入有序子序列
堆排序∶每一趟将堆顶元素加入有序子序列(与待排序序列中的最后一个元素交换)
并将待排序元素序列再次调整为大根堆(小元素不断“下坠”)

/建立大根堆
void BuildMaxHeap(int A[] ,int len)
//将以k 为根的子树调整为大根堆
void HeadAdjust(int A[] ,int k,int len)
//堆排序的完整逻辑
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);	//把剩余的待排序元素整理成堆
	}
}

在这里插入图片描述
建堆的过程,关键字对比次数不超过4n,建堆时间复杂度=O(n)

根节点最多“下坠"h-1层,每下坠一层
而每“下坠”一层,最多只需对比关键字2次,因此每一趟排序复杂度不超过O(n)= O(log2n)

堆排序的时间复杂度=O(n)+O(nlog2n)= O(nlog2n)
堆排序的空间复杂度=O(1)
不稳定

堆排序的插入删除

插入
对于小根堆,新元素放到表尾与父节点对比(i/2向下取整),若新元素比父节点更小,则将二者互换。新元素就这样一路“上升”,直到无法继续上升为止

删除
被删除的元素用堆底元素替代,然后让该元素不断“下坠”,直到无法下坠为止

//将以k 为根的子树调整为大根堆
void HeadAdjust(int A[],int k,int len){
	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];			//被筛选结点的值放入最终位置
}

归并排序

归并∶把两个或多个已经有序的序列合并成一个

//  核心
int *B=(int *)malloc( n*sizeof(int) );//辅助数组B
//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];		//将A中所有元素复制到B中
	for( i=low, j=mid+1,k=i; i<=mid&&j<=high;k++){
		if(B[i]<=B[j])
			A[k]=B[i++];//将较小值复制到A中
		else
			A[k]=B[j++];
	}//for
	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);//归并
	}//if
}

在这里插入图片描述

二叉树的第h层最多有2h-1个结点若树高为h,则应满足n<= 2h-1即h-1 =log2n向下取整
结论:n个元素进行2路归并排序,归并趟数h-1 =log2n向下取整
每趟归并时间复杂度为O(n),则算法时间复杂度为O(nlog2n),k=空间复杂度=O(n),来自辅助空间B
稳定性:稳定。

基数排序

第一趟:以个位进行分配
在这里插入图片描述
第一趟收集结束:得到按个位递减排序的序列
在这里插入图片描述

第二趟:以“十位”进行“分配”

在这里插入图片描述
第二趟“收集”结束:得到按“十位"递减排序的序列,“十位"相同的按“个位"递减排序
在这里插入图片描述
第三趟:以“百位”进行“分配”
在这里插入图片描述
第三趟按“百位”分配、收集:得到一个按“百位"递减排列的序列,若"百位”相同则按“十位”递减排列,若“十位”还相同则按“个位”递减排列
在这里插入图片描述

在这里插入图片描述
在这里插入图片描述

基数排序得到递减序列的过程如下,
初始化:设置r个空队列,Qr-1,Qr-2,…,Q0
按照各个关键字位权重递增的次序(个、十、百),对d个关键字位分别做“分配”和“收集”
分配︰顺序扫描各个元素,若当前处理的关键字位=x,则将元素插入Qx队尾
收集:把Qr-1,Qr-2,…,Q0各个队列中的结点依次出队并链接

需要r个辅助队列,空间复杂度= O(r )
一趟分配O(n),一趟收集O(r ),总共d趟分配、收集,总的时间复杂度=O(d(n+r))
稳定性:稳定。

基数排序擅长解决的问题:
①数据元素的关键字可以后便地拆分为d组,且d较小
②每组关键字的取值范围不大,即r较小
③数据元素个数n较大

外部排序

数据太多,无法放入内存中。(还要关注如何使读、写磁盘次数更少)
在这里插入图片描述

外部排序时间开销= 读写外存的时间+内部排序所需时间+内部归并所需时间

重要结论:采用多路归并可以减少归并趟数,从而减少磁盘I/O(读写)次数
对r个初始归并段,做k路归并,则归并树可用k叉树表示若树高为h,则归并趟数= h-1 = logkr向上取整
推导:k叉树第h层最多有kh-1个结点,则r<=kh-1,(h-1)最小= logkr向上取整

多路归并带来的负面影响:
①k路归并时,需要开辟k个输入缓冲区,内存开销增加。
②每挑选一个关键字需要对比关键字(k-1)次,内部归并所需时间增加

败者树

败者树——可视为一棵完全二叉树(多了一个头头)。k个叶结点分别是当前参加比较的元素,非叶子结点用来记忆左右子树中的“失败者”,而让胜者往上继续进行比较,一直到根结点。

对于k路归并,第一次构造败者树需要对比关键字k-1次.
有了败者树,选出最小元素,只需对比关键字 [log2k] (向上取整)次

在这里插入图片描述

  • 4
    点赞
  • 24
    收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
©️2022 CSDN 皮肤主题:大白 设计师:CSDN官方博客 返回首页
评论

打赏作者

Jiang。

你的鼓励将是我创作的最大动力

¥2 ¥4 ¥6 ¥10 ¥20
输入1-500的整数
余额支付 (余额:-- )
扫码支付
扫码支付:¥2
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、C币套餐、付费专栏及课程。

余额充值