第五章

图的简介与二维数组

就是由顶点和连接顶点的组成的

表示图

我们利用二维数组表示图
二维数组的两个参数行列,均表示顶点,二者相交得到的即是距离
我们用正无穷(一般可以用999999代替)表示二者之间没有边,0是某点到自身的距离
比如e[a][b] = 3 ;就代表a到b的距离是3

图还分为有向图和无向图
顾名思义,有向图是有方向属性的,无向图则没有
换言之,对于无向图,我们有:

e[a][b] = e[b][a]

图的遍历

我们上一章学习了深度优先算法和广度优先算法
现在我们就利用它们来完成图的遍历
我们还是以一个例子来说吧

例题

假设我们在一个道路具有单向型的地方行走,我们想得到两点之间的路径和距离
1->5 10
1->2 2
2->5 7
2->3 3
3->1 3
3->4 4
4->5 5
5->3 3
现在,我们想要从1到达5的最短路径

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
#include <stdio.h>
int min =9999999 ; //预设距离最小值
int book[101] ; //标记数组
int e[101][101] ; //图
int n ;//目标城市

//先写一个DFS方法
void dfs(int cur,int dis){
int j ;

//如果走过的路程已经大于了最短路径,那就直接返回,因为不可能是最短路径
if(dis>min) return ; //其实这一步也让99999999代表正无穷成立
if(cur == n)
{
//如果到达了目标城市
if(dis<min) min = dfs ; //更新最小值
return ;
}

for(j=1;j<=n;j++)
{
//判断路径
if(e[cut][j] != 9999999 && book[j] = 0)
{
book[j] = 1 ; //mark
dfs(j,dis+e[cur][j]) ; //继续搜寻
book[j] = 0 ;//之后递归回来的时候清掉标记
}
}
return ;
}

int main()
{
int i,j,m,a,b,c;
scanf("%d %d",&n,&m);

//初始化二维矩阵
for(i=1;i<=n;i++)
for(j=1;j<=n;j++)
if(i==j)
e[i][j] = 0;
else
e[i][j] = 9999999 ;

//读入城市之间的道路
for(i=1;i<=m;i++)
{
scanf("%d %d %d",&a,&b,&c);
e[a][b] = c ;
}

//从1号城市出发
book[1] = 1 ;
dfs(1,0) ; //1是当前城市编号,0是走过的路程

printf("%d",min) ;

getchar();getchar();
return 0 ;

}

第七章

由于第六章长度较长,先记下第七章

树的基本介绍

介绍

树其实就是不包含回路的连通无向图
因为这个特点,我们为树赋予了这些特性:
1.一棵树中的任意两个结点,有且仅有唯一的一条路径连通
2.一棵树如果有n个结点,那么它一定恰好有n-1条边
3.在一棵树中,加上一条边,那么就会得到一个回路

树是指任意两个结点间有且仅有一条路径的无向图
只要是没有回路的连通无向图,就是树

结点节点

我们把树中的每一个点叫做结点,当然也要叫节点的,无伤大雅
又叫做根节点,即最开始的节点。一棵树有且仅有一个根节点
父亲结点简称父结点,儿子结点简称子结点

如果一个结点没有子结点,我们称它为叶结点
如果一个结点没有父结点,那它就是根结点
如果一个结点不是根结点也不是叶结点,那么它就是内部结点
深度:指从根到这个结点的层数,根结点所在的层数是第一层

二叉树与堆

二叉树是一种特殊的、常见的树

简介

二叉树的特点在于每个结点最多只有两个儿子
如果要使用更严格的递归定义,则是:

二叉树要么为空,要么由根结点、左子树、右子树组成
而左子树、右子树分别是一棵二叉树

二叉树是使用范围极广的树,一棵多叉树也可以转换为二叉树

二叉树类型

满二叉树:如果二叉树中每个内部结点都有两个儿子,这样的二叉树叫做满二叉树
完全二叉树:如果一棵二叉树除了最右边的位置上有一个或几个叶节点缺少外,其他都是丰满的,那么它就是完全二叉树
我们可以把满二叉树理解为一种特殊的,极其完美的完全二叉树
我们只需要用一个一维数组就能存储完全二叉树

我们将完全二叉树进行从上到下,从左到右的编号,可以得到下面这个结论:
如果已知子结点的编号是x,那么它父结点的编号就是x/2

符合所有父结点都比子结点要小的树,就是最小堆
符合所有父结点都比子结点要大的树,就是最大堆

那么想要向堆中加入一个数据,需要怎么做呢?
比如说我们想要把23加入到一个最小堆
1.我们把23放入堆顶,检查是否合适
2.如果不合适,我们就将这个数和它的两个儿子比较,并且选择较小者交换位置
3.继续向下交换,检查。重复1-2直到符合条件

当新增加一个数被放置到堆顶时,如果此时不符合最小堆的特性,则需要将这个数向下调整,直到找到合适的位置为止

代码实例_向下调整

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
void siftdown(int i)
{
//传入一个需要向下调整的结点编号i
//这里传入1,即从堆的顶点开始向下调整

int t,flag = 0 ;
//flag用于标记是否需要继续向下调整
//当i结点有儿子(其实至少是有左儿子的情况下)并且有需要调整的时候,循环就执行
while( i*2 <= n && flag == 0 ){
//先判断它和左儿子的关系,用t记录值比较小的结点编号
if(h[i] > h[i*2])
t = i*2 ;
//子节点是较小的
//和/2得到父节点相对,*2自然得到子节点(左)
else
t = 1 ;
//1是较小值

//如果有右儿子,再对右儿子(*2+1)进行讨论
if(i*2+1 <= n)
{
//如果右儿子的值更小,更新较小的结点编号
if(h[t] > h[i*2 + 1])
t = i*2+1 ;
}

//如果发现最小的结点编号不是自己,那么就说明子结点中,有比父结点更小的
if(t!=i)
{
swap(t,i);//交换它们,这里记得自己写一个swap函数
i = t ;
}
else{
falg = 1 ;
}
}
return ;
}

如果只是想新增一个值,而不要删除掉最小值,那么该怎么做呢?
直接将新元素插入到末尾,再根据情况判断元素是否需要上移,直到满足堆的特性为止

堆排序

与快速排序一样,,堆排序的时间复杂度是O(NlogN)
进行堆排序,需要我们建立对应的堆,每次删除顶部元素并将顶部元素输出或者放到一个数组中,直到堆空
最后输出的或者存放在新数组中的数,就是已经排序好的
下面以从小到大排序为例

代码实例_从小到大堆排序

1
2
3
4
5
6
7
8
9
10
11
//先整个删除最小元素的
int deletemin()
{
int t ;
t = h[1];//用一个临时变量记录堆顶点的值
h[1] = h[n];//将堆的最后一个点赋值到堆顶
n--;//使堆的元素减少1
siftdown(1) ; //向下调整
return t ; //返回在之前记录的堆的顶点的最小值
}

完整的代码组成如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
#include <stdio.h>
int h[101];//用来存放堆的数组
int n ; //用来存储堆中元素的个数,实质上就是堆的大小

//先整个交换函数
void swap(int x ,int y)
{
int t ;
t = h[x];
h[x] = h[y];
h[y] = t ;
return
}

//向下调整的函数,上文已经写过了
void siftdown(int i)
{
//传入一个需要向下调整的结点编号i,这里传入1,即从堆的顶点开始向下调整

int t,flag = 0 ;
//flag用于标记是否需要继续向下调整
//当i结点有儿子(其实至少是有左儿子的情况下)并且有需要调整的时候,循环就执行
while( i*2 <= n && flag == 0 ){
//先判断它和左儿子的关系,用C记录值比较小的结点编号
if(h[i] > h[i*2])
t = i*2 ;
else
t = 1 ;

//如果有右儿子,再对右儿子进行讨论
if(i*2+1 <= n)
{
//如果右儿子的值更小,更新较小的结点编号
if(h[t] > h[i*2 + 1])
t = i*2+1 ;
}

//如果发现最小的结点编号不是自己,那么就说明子结点中,有比父结点更小的
if(t!=i)
{
swap(t,i);//交换它们,这里记得自己写一个swap函数
i = t ;
}
else{
falg = 1 ;
}
}
return ;
}

//建立堆的函数
void creat()
{
int i ; //从最后一个非叶结点到第一个结点依次进行向下调整

for(i=n/2 ; i>=1;i--)
{
siftdown(i);
}

return ;
}

//删除最大的元素
int deletmax()
{
int t ;
t = h[1];
h[1] = h[n];
b-- ;
siftdown(1);//向下调整
return t ;
}

int main(){
int i , num ;
//读入要排序的数字个数
scanf("%d",&num);

//读入数字
for(i=1;i<num;i++)
scanf("%d",&h[i]);
n = num ;

//建堆
creat();s

//删除顶部元素,连续删除n次,实际上就是从小到大把数输出
for(i=1;i<=num;i++)
printf("%d",deletemax());

getchar();getchar();
return 0 ;
}

小结

像这种,支持插入元素和寻找最值元素的数据结构被称为优先队列
堆就是一种优先队列的实现

并查集算法

并查集算法是一个利用结点关系,进行分类合组的算法

简介

并查集可以通过一个一维数组来实现
我们把每一个点视作一个”独立的,只有一个结点”的树
之后我们可以通过一些条件,逐渐将这些树合并成一棵大树

合并的过程,其实就是找统一的父节点的过程,我们可以自定两条原则:
1.相异的情况下,把右边的父节点改为左边的父节点。视作二者合并成了一组
2.通过两者最高的父节点来进行比较

另外,既然我们通过层层推进找到了某个点的父节点,那么我们在”统一”之后,也可以顺便的把路上的其它结点修改,这样会方便我们的二次寻找

代码示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
#include <stdio.h>
int f[1001] = {0} s,n,m,sum = 0 ;

//最开始的初始化
void init()
{
int i ;
for(i=0;i<=n;i++)
f[i] = i ;//最开始的时候,每个点的最高父节点就是它自己
return ;
}

//这是找爹的递归函数,不停的寻找直到找到最高父节点为止
int getf(int v)
{
if(f[v] == v){
return ; //“集团”内只有一个人的情况
}
else
{
//路径压缩,每次在函数返回的时候,顺带把路径上的结点都修改
f[v] = getf(f[v]) ;

return f[v];
}
}

//合并两个子集的函数、
void merge(int v , int u)
{
int t1,t2 ; //t1,t2指的是两个子集的最高父节点

t1 = getf(v);
t2 = getf(u);

if(t1!=t2){
//两者不在同一集合中,才进行合并
f[t2] = t1 ; //向左合并原则
}
return ;
}

//主程序
int main()
{
int i,x,y;
scanf("%d %d",&n,&m);

init();
for(i=1;i<=m;i++)
{
scanf("%d %d",&x,&y);
merge(x,y);
}

//扫描打印结果
for(i=1;i<=n;i++)
{
if(f[i] == i )
sum++;
}
printf("%d \n",sum);


getchar();getchar()
return 0 ;
}

并查集也被称为“不相交集”数据结构