数据结构与算法
一、绪论
什么是数据结构?看到数据结构这四个字,我们大脑中不自主想到的就是数据、结构这两个词语,那什么又是数据呢?什么叫做数据的结构?下面我们就开始学习一些基本的概念帮助我们清楚的理解
1.数据结构基本概念及术语
数据:信息的载体,是描述客观事物属性的数、字符及所有能输入到计算机中并被计算机程序识别和处理的符号的集合
数据对象: 具有相同性质的数据元素的集合,是数据的一个子集
数据元素: 数据的基本单位,通常作为一个整体进行考虑和处理。
数据项: 构成数据元素的不可分割的最小单位
举个简单的例子来理解这几个概念之间的关系:有一个具有人和动物的数据集合,其中所有的人物就是一个数据对象,它是具有相同性质的数据集合;其中每一个人物就是一个数据元素,人物的姓名、年龄等是不可分割的最小单位,也就是数据项。
从下面的表格中我们可以看到小明、小红、小华是一个数据对象,而其中小明的所有信息是一个数据元素,小明的姓名是数据项,不可分割的最小单位。
姓名 | 班级 | 成绩 |
---|---|---|
小明 | 1 | 100 |
小红 | 2 | 99 |
小华 | 3 | 97 |
在这个表格中不仅仅有数据对象、数据元素、数据项,还有小明排列在小红前面这样的关系,那么这样的关系叫什么呢?这种数据间的关系就叫做数据结构。
数据不是孤立存在的,他们存在着某种关系,这种相互关系就叫做结构。
数据结构是相互之间存在一种或多种特定关系的数据元素的集合。
2.数据结构的三要素
数据结构三要素:逻辑结构、物理结构、数据的运算
逻辑结构主要包括线性结构、集合、树形结构、图状结构,而集合,树、图属于非线性结构。
存储结构也可以称为物理结构,包括顺序、链式、索引、散列存储,其中最重要的是顺序存储和链式存储。
数据的运算主要包括定义和实现,定义主要针对逻辑结构,实现针对存储结构。数据结构三要素的结构图如下图所示。
3.算法基本概念及术语
算法:对特定问题求解步骤的一种描述,它是指令的有限序列,其中的每条指令表示一个或多个操作。
算法的特性:输入、输出、又穷、确定、可行
算法(指导者)VS 程序(实施者)
算法 | 程序 |
---|---|
算法必须是有穷的 | 程序可以是无穷的 |
算法必须是正确的 | 程序可以是错误的 |
算法可以用伪代码、程序设计语言描述 | 程序智能用程序语言编写 |
4.算法效率的量度
如何设计一个“好”算法?
正确性:能正确解决问题
可读性:有良好的可读性,帮助人们理解
健壮性:输入非法数据时,算法能适应的做出反应或进行处理
效率与存储量:效率是指算法执行时间,存储量需求指的是算法执行过程中所需要的最大存储空间。
算法时间复杂度:执行当前算法所消耗的时间
时间复杂度的公式是: T(n) = O( f(n) ),其中f(n) 表示每行代码执行次数之和,而 O 表示正比例关系,这个公式的全称是:算法的渐进时间复杂度
算法空间复杂度:执行当前算法需要占用多少内存空间,记为S(n) = O( g(n) )
二、线性表
1.线性表的定义和基本操作
线性表是具有相同类型的n(n>=0)个元素的有限序列,其中n为表长,当n=0时,该表为空表。
若L命名为线性表,则一般表示为:L=(a1,a2,…,ai,ai+1,…an)
在线性表中除了表头元素,每一个元素都有一个前驱结点,除了表尾元素,每一个元素都有一个后继结点。
线性表的特点:
表中元素个数有限、元素具有逻辑上的顺序性,都是数据元素,数据类型相同,具有抽象性,线性表是一种逻辑结构,表示元素之间一对一相邻的关系。
2.线性表的顺序表示
线性表的顺序存储又称顺序表
一组地址连续存放的存储单元依次存放线性表的元素,从而使得逻辑上相邻的两个元素在物理位置上也相邻。(逻辑顺序与物理顺序相同)
顺序表一般用数组的方式来实现:
数组下标 | 顺序表 |
---|---|
0 | a1 |
1 | a2 |
… | … |
2 | a3 |
i-1 | ai |
… | … |
n-1 | an |
… | … |
MaxSize-1 | … |
数组静态分配
#define MaxSize = 50 //宏定义
typedef struct{
ElemType data[MaxSize];
int length;
}SqList;
数组动态分配
#define MaxSize = 50 //宏定义
typedef struct{
ElemType *data;
int length;
}SqList;
使用数组动态分配,需要动态分配语句来分配空间,否则只是一个地址,并没有动态空间。
L.data = (ElemType*)malloc(sizeof(ElemType)*InitSize)
使用静态分配,在运行前就确定了空间大小,若数组大小超过50,则会溢出产生错误。而动态分配在运行前没有确定空间大小,在运行时才将大小分配,如果空间大小超过50,可以在分配一块新的存储空间,把旧的存储空间转移到新的存储空间上,不会出现溢出的问题。
顺序表插入操作
L为引用类型,i表示顺序表的标号,e代表插入元素
bool ListInsert(SqList &L.int i,ElemType e){
if(i<1 || i>L.length+1)
return false;
if(L.length>=MaxSize)
return false;
for(int j = L.length; j >= i ; j--)
L.data[j] = L.data[j-1];
L.data[i-1] = e;
L.length++;
return true;
}
最好时间复杂度:O(1) 在顺序表末尾插入
平均时间复杂度:O(n)
最坏时间复杂度:O(n) 在顺序表第一个元素前插入
顺序表删除操作
L为引用类型,i表示顺序表的标号,e代表删除元素,为引用类型
bool ListDelete(SqList &L, int i, ElemType &e){
if(i<1 || i>L.length)
return false;
e = L.data[i-1];
for(int j = i; j < L.length; j++)
L.data[j-1] = L.data[j];
L.length--;
return true;
}
最好时间复杂度: O(1)
平均时间复杂度: O(n)
最坏时间复杂度: O(n)
顺序表按值查找操作
L表示线性表,e表示按值查找的元素,这里没有用引用类型,是因为按值查找,并不需要修改元素的值
int LocateElem(SqlList L, ElemType e){
int i;
for(i = 0; i<L.length; i++)
if(L.data[i]==e)
return i+1;
return 0;
}
最好时间复杂度: O(1)
平均时间复杂度: O(n)
最坏时间复杂度: O(n)
3.线性表的链式表示
线性表的链式存储又称为单链表
通过一组任意的存储单元来存储线性表中的数据元素,通过指针实现线性逻辑关系,逻辑位置相邻,物理位置不一定相邻。
当前元素地址 | 元素 | 下一个元素地址 |
---|---|---|
addr0 | a1 | addr4 |
… | … | … |
addr4 | a2 | addr6 |
… | … | … |
addr6 | a3 | addr7 |
addr7 | a4 | addr9 |
… | … | … |
addrr | an | null |
单链表的结点存储了当前结点的数据以及下一个结点的指针域
typedef struct LNode{
ElemType data;
struct LNode *next;
}LNode, *LinkList;
单链表的头插法
LinkList List_HeadInsert(LinkList &L){
LNode *s; int x;
L = (LinkList)malloc(sizeof(LNode));
L->next = NULL;
scanf("%d",&x);
while(x!=9999){
s = (LNode*)malloc(sizeof(LNode))
s->data = x;
s->next = L->next;
L->next = S;
scanf("%d",&x);
}
return L;
}
单链表的尾插法
LinkList List_TailInsert(LinkList &L){
int x;
L = (LinkList)malloc(size(LNode));
LNode *s, *r=L;
scanf("%d",&x);
while(x!=9999){
s = (LNode*)malloc(sizeof(LNode))
s->data = x;
r->next = s;
r = s;
scanf("%d",&x);
}
r->next = NULL;
return L;
}
按序号查找
LNode *GetElem(LinkList L,int i){
int j = 1;
LNode *p = L->next;
if(i == 0 )
return L;
if(i<1)
return Null;
while(p&&j<i){
p = p->next;
j++;
}
return p;
}
按值查找
LNode *LocateElem(LinkList L,ElemType e){
LNode *p = L->next;
while(p!=NULL && p->data!=e)
p = p->next;
return p;
}
插入结点
p = GetElem(L,i-);
s->next = p->next;
p->next = s;
删除结点
p = GetElem(L,i-1);
q = p->next;
p->next = q->next;
free(q);
求表长
int count = 0;
p = head
while(p->next != NULL){
count++;
p = p->next;
}
三、栈和队列
1.栈的基本概念
栈(Stack)只允许在一端进行插入和删除操作的线性表,具有后进先出的特点。
顺序栈采用顺序存储的栈
#define MaxSize 50
typedef struct{
ElemType data[MaxSize];
int top; //指向栈顶的指针
}SqStack;
栈空条件:
S.top == -1
栈满条件:
S.top == MaxSize-1
栈长:
S.top+1
进栈操作
bool Push(SqStack &S, ElemType x){
if(S.top == MaxSize-1)
return false;
S.data[++S.top] = x;
return true;
}
出栈操作
bool Pop(SqStack &S, ElemType &x){
if(S.top == -1)
return false;
x = S.data[S.top--];
return true;
}
读栈顶元素
bool GetTop(SQStack S,ElemType &x){
if(S.top == -1)
return false;
x = S.data[S.top];
return true;
}
共享栈
将两个栈底设置在共享空间的两端,栈顶向空间中延伸
判空:0号栈 top == -1
1号栈 top ==MaxSize
栈满:top1 - top0 ==1
链栈采用链式存储的栈
typedef struct Linknode{
ElemType data;
struct Linknode *next;
} *LiStack;
所有的操作都在表头进行
2.队列的基本概念
队列(Queue)只允许在表的一端进行插入,表的另一端进行删除操作的线性表
在队头出队,队尾入队,具有先进先出的特点。
顺序队采用顺序存储的队列
#define MaxSize 50
typedef struct{
ElemType data[MaxSize];
int front,rear;
}SqQueue;
//front指向队头元素,rear指向队尾元素的下一位置或者 front指向队头元素的前一位置,rear指向队尾元素
初始时,front == rear ==0
队空条件:
Q.front == Q.rear
思考:为什么对空的条件不是Q.front == Q.rear ==0?(不是充分必要条件)
求队列长度:
Q.rear - Q.front
队满:
Q.rear == MaxSize 会出现假溢出
循环队列:把存储队列的顺序队列在逻辑上视为一个环
front指针移动:
Q.front = (Q.front + 1) % MaxSize
rear指针移动:
Q.rear = (Q.rear + 1)% MaxSize
队列长度:
(Q.rear + MaxSize - Q.front)% MaxSize
队空条件:
Q.front == Q.rear
队满条件:
Q.front == (Q.rear + 1) % MaxSize
入队
bool EnQueue(SqQueue &Q, ElemType x){
if(Q.rear+1)%MaxSize == Q.front)
return false;
Q.data[Q.rear] = x;
Q.rear = (Q.rear + 1)%MaxSize;
return true;
}
出队
bool DeQueue(SqQueue &Q, ElemType &x){
if(Q.rear == Q.front)
return false;
x = Q.data[Q.front]
Q.front = (Q.front + 1)%MaxSize;
return true;
}
队列的链式存储
链队 采用链式存储的队列
typedef struct{
ElemType data;
struct LinkNode *next;
}LinkNode;
typedef struct{
LinkNode *front,*rear;
}LinkQueue;
链队初始化
void InitQueue(LinkQueue &Q){
Q.front =(LinkNode*)malloc(sizeof(LinkNode));
Q.rear = Q.front;
Q.front->next = NULL;
}
链队 入队操作(尾插法)
void EnQueue(LinkQueue &Q, ElemType x){
LinkNode *s = (LinkNode *)malloc(sizeof(LinkNode));
s->data = x;
s->next = NULL;
Q.rear->next = S;
Q.rear =S;
}
链队 出队操作
bool DeQueue(LinkQueue &Q, ElemType &x){
if(Q.front == Q.rear)
return false;
LinkNode *p = Q.front->next;
x = p->data;
Q.front->next = p->next;
if(Q.rear == p)
Q.rear = Q.front;
free(p);
return true;
}
3.栈的应用
括号匹配
算法思想:
1.初始一个空栈,顺序读入括号
2.若是右括号,则与栈顶元素进行匹配 若匹配,则弹出栈顶元素并进行下一个元素 若不匹配,则该序列不合法
3.若是左括号,则压入栈中
4.若全部元素遍历完毕,栈中非空则序列不合法
//以栈先进后出思想来解决括号匹配问题。
// 遍历字符串。
// 将所有左括号压栈,栈顶“指针”加一。
// 遇右括号则将栈顶出栈,检查栈顶是否匹配,栈顶“指针”减一
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#define Maxsize 50
typedef char Datatype;
typedef struct{
Datatype data[Maxsize];
int top;
}SqStack;
void InitStack(SqStack *S)
{
S->top=-1;
}
int StackEmpty(SqStack *S)
{
if(S->top==-1)
return 0;
else
return 1;
}
int Push (SqStack *S,Datatype x)
{
if(S->top==Maxsize-1)
return 0;
else
S->data[++(S->top)]=x;
return 1;
}
int Pop(SqStack *S, Datatype *x)
{
if(S->top==-1)
return 0;
else
*x=S->data[(S->top)--];
return 1;
}
int GetTop(SqStack S,Datatype *x)
{
if(S.top==-1)
return 0;
else
*x=S.data[S.top];
return 1;
}
int Bracketscheck(SqStack *S,char *str)
{
InitStack(S);
char e;
int i=0;
while(str[i]!='\0')
{
switch(str[i]){
case '(':
Push(S,'(');
break;
case '{':
Push(S,'{');
break;
case '[':
Push(S,'[');
break;
case ')':
Pop(S,&e);
if(e!='(')
return 0;
break;
case '}':
Pop(S,&e);
if(e!='{')
return 0;
break;
case ']':
Pop(S,&e);
if(e!='[')
return 0;
break;
default:
break;
}
i++;
}
int h=StackEmpty(S);
if(h==1)
return 0;
else
return 1;
}
int main()
{ SqStack S;
char str[Maxsize];
printf("请输入你要收入的字符串:");
scanf("%s",str);
int h=Bracketscheck(&S,str);
if(h==0)
printf("括号不匹配");
else
printf("括号匹配");
return 0;
}
四、树
前面提到的线性结构的特点是第一个元素无前驱节点,最后一个元素无后继节点,其他元素有一个前驱节点和一个后继节点。
树结构的特点是根节点无前驱,多个叶子节点无后继,其他元素有一个前驱节点和一个后继节点。
1.二叉树
定义:每个结点的度都不大于2,每个结点的孩子结点次序不能任意颠倒。
二叉树有五种形态结构:空树、只含根节点、右子树为空、左子树为空,左右子树均不为空
二叉树的重要性质:
1.在二叉树的第i层上至多有2^i-1个结点
2.深度为k的二叉树至多有2^k -1 个结点
3.对任何一颗二叉树,若它含有n0个叶子结点,n2个度为2的结点,则必存在n0 = n2 + 1
4.具有n个结点的完全二叉树的深度为[log2n]+1
5.如果对一颗有n个结点的完全二叉树(其深度为[log2n]+1)的结点按层序编号(从第1层到第[log2n]+1层,每层从左到右),则对任一结点i(1≤i≤n),有:
① 如果i=1,则结点i是二叉树的根,无双亲;如果i≥1,则其双亲PARENT(i)是节点[i/2] 。
②如果2i>n,则结点i无左孩子(节点i为叶子节点);否则其左孩子LCHILD(i)是节点2i 。
③如果2i+1>n,则结点i无右孩子;否则其右孩子RCHILD(i)是结点2i+1
二叉树的顺序存储为一维数组,只适用于完全二叉树和满二叉树
顺序存储结构显然有一个很大的局限性,不便于存储任意形态的二叉树。
观察二叉树的状态可以发现是一个根节点与两棵子树之间的关系。因此设计出了含有一个数据域和两个指针域的链式存储结构。
typedef struct BinaryNode {
char data; //数据域
struct BinaryNode* left; //指针域 左孩子
struct BinaryNode* right; //指针域 右孩子
}Tree;
2.二叉树遍历
指从根结点出发,按照某种次序依次访问二叉树中所有的结点,使得每个结点被访问依次且仅被访问一次。
四种遍历方式分别为:先序遍历、中序遍历、后序遍历、层序遍历。
先序遍历:根左右
void PreOrder(BiTree root)
{
if(root!=NULL)
{
printf("%c",root->data);
PreOrder(root->lchild);
PreOrder(root->rchild);
}
}
中序遍历:左根右
void InOrder(BiTree root)
{
if(root!=NULL)
{
InOrder(root->lchild);
printf("%c",root->data);
InOrder(root->rchild);
}
}
后续遍历:左右根
void PostOrder(BiTree root)
{
if(root!=NULL)
{
PostOrder(root->lchild);
PostOrder(root->rchild);
printf("%c",root->data);
}
}
先序遍历:A B D E H I C F J G K
中序遍历:D B E H I A F J C G K
后续遍历: D I H E B J F K G C A
3.统计二叉树叶子结点数目
int leaf(BiTree root)
{
int LeafCount;
if(root == NULL)
LeafCount = 0;
else if((root->lchild==NULL)&&(root->rchild==NULL))
LeafCount = 1;
else
LeafCount = leaf(root->lchild) + leaf(root->child)
return LeafCount;
}
//其他应用
//1.统计二叉树中度为1的结点个数
(root->lchild == NULL && root->rchild != NULL)||(root->lchild != NULL && root->rchild == NULL)
//2.统计二叉树中度为2的结点个数
root->lchild!=NULL && root->rchild != NULL
//3.统计二叉树中结点值等于x的结点个数
root->data == x
4.求二叉树的高度
设函数表示二叉树bt的高度
则递归定义如下:
若bt为空,则高度为0
若bt非空,其高度应为其左右子树高度的最大值加1
high =max(hl,hr)+1
int PostTreeDepth(BiTree)
{
int hl,hr,max;
if(root!=NULL)
{
hl = PostTreeDepth(root->lchild);
hr = PostTreeDepth(root->rchild);
max = hl>hr?hl:hr;
return max+1;
}
else
return 0;
}
5.线索二叉树
通过二叉树及其遍历的学习,了解到:
1.二叉树的遍历过程较为复杂,需要用递归或栈
2.二叉树结点的遍历序列是一个线性序列
3.二叉链表难以获得遍历序列前驱、后驱结点信息
4.二叉链表中,N个结点,2N个指针,N+1个空指针
是否可以利用空指针域,指向遍历序列中的前驱、后继结点,从而不再需要递归或栈,也能方便的实现二叉树遍历?——线索二叉树
基本概念:
指向遍历序列前驱、后继结点的指针称为线索
把空指针修改为线索的过程称为线索化;
经过线索化的二叉树称为线索二叉树
含有线索的二叉树链表称为线索链表
线索二叉树的结点结构:
Ltag = 0 表示 LChild域指示结点的左孩子
Ltag = 1 表示 LChild域指示结点的遍历前驱
Rtag = 0 表示RChild域指示结点的右孩子
Rtag = 1 表示 RChild域指示结点的遍历后继
线索化的过程: 设两个指针:p 和 pre ,指针p指向当前访问的结点,指针pre指向当前访问结点p的前驱结点
修改指针:
p的Lchild指针为空,则修改为指向前驱结点pre
pre的Rchild指针为空,则修改为指向后继结点p
//中序线索化
void InThreading(BiThrTree p)
{
if (p)
{
InThreading(p->lchild); //左子树线索化
if (!p->lchild) //前驱线索
{
p->LTag = Thread;
p->lchild = pre;
}
if (!pre->rchild) //后继线索
{
pre->RTag = Thread;
pre->rchild = p;
}
pre = p;
InThreading(p->rchild); //右子树线索化
}
}
五、图
1.图的基本概念
定义:图是由顶点集V和弧集R构成的数据结构Graph(V,R)
有向图:由于“弧”是有方向的,因此称由顶点集和弧集构成的图为有向图
无向图:由顶点集和边集构成的图称作无向图
有向图或无向图中弧或边带权后的图分别称作有向网或者无向网
假设图中有n个顶点,e条边,则
含 e = n(n-)/2条边的无向图称作完全图;
含e = n(n-1)条弧的有向图称作有向完全图
若边或弧的个数 e < nlogn,称作稀疏图,否则称作稠密图。
2.图的存储结构
图的邻接矩阵表示法
图的邻接表表示法
有向图的十字链表表示法
无向图的邻接多重表表示法
图的邻接矩阵表示法(数组表示法)
一维数组:用于存储顶点信息
二维数组:用于存储图中顶点之间双联关系——邻接矩阵
图的邻接表表示法(链式存储法)
对图中每个顶点建立一个单链表,第i个单链表中的结点表示依附于顶点v的边
,
3.图的遍历
深度优先搜索:
算法思想:递归
首先依次访问出发点v0
依次以v0的未被访问的邻接点为出发点,深度优先搜索图,直至图中所有与v0有路径相通的顶点都被访问
对于非连通图,则图中一定还有顶点未被访问,要从图中选一个未被访问的顶点作为起始点,重复上述深度优先搜索过程。
void DepthFirstSearch(Graph g, int v0)
{
visit(v0);
visited[v0] = True;
w = FirstAdjVertex(g,v0);
while(w!=-1)
{
if(!visited[w]) DepthFirstSearch(g,w);
w = NextAdjVertex(g,v0,w);
}
}
广度优先搜索:
首先访问起始顶点v;
接着由出发依次访问v的各个未被访问过的邻接顶点w1,w2,…,wi;
然后依次访问w1,w2,…wi的所有未被访问过的邻接顶点;
在从这些访问过的顶点出发,访问它们所有未被访问过的邻接顶点;
…,依次类推;
算法思想:队列+辅助标记数组
bool Visited[MAX_TREE_SIZE];
void BFSTraverse(Graph G){
for(int i = 0; i<G.vexnum;++i)
visited[i] = FALSE;
InitQueue(Q);
for(int i; i < G.vexnum; ++i)
if(!visited[i])
BFS(G,i);
}
void BFS(Graph G, int v){ //图G,初试顶点编号v
visit(v); //访问初始结点
visited[v] = TRUE; //用visited数组表示是否入队
EnQueue(Q,v);
while(!isEmpty(Q)){ //循环判断队列是否为空
DeQueue(Q,v); //出队对头元素
for(w=FirstNeighbor(G,v);w>=0; w = NextNeighbor(G,v,w))
if(!visited[w]){
visited[w];
visited[w]=TRUE;
EnQueue(Q,w);
}
}
}
4.图的应用—最小生成树
假设要在n个城市之间建立通讯联络网,则连通n个城市只需要修建n-1条线路,如何在最节省经费的前提下建立这个通讯网?
构造网的一颗最小生成树,即:在e条带权的边中选取n-1条边(不构成回路),使“权值之和”为最小
最小生成树要解决两个问题:
1.尽可能选取权值小的边,但不能构成回路
2.选取n-1条恰当的边以连接网的n个顶点
算法一:普里姆算法(Prim)
算法二:克鲁斯卡尔算法(Kruskal)
普里姆算法(Prim)可以称为“加点法”
基本思想:
取图中任意一个顶点v作为生成树的根,,每次迭代选择代价最小的边对应的点,加入到最小生成树中。算法从某一个顶点s开始,逐渐长大覆盖整个连通网的所有顶点。
1.图的所有顶点集合为V;初始令集合u={s},v=V−uu={s},v=V−u;
2.在两个集合u,vu,v能够组成的边中,选择一条代价最小的边(u0,v0)(u0,v0),加入到最小生成树中,并把v0v0并入到集合u中。
3.重复上述步骤,直到最小生成树有n-1条边或者n个顶点为止。
//首先初始化辅助数组
//数组大小为图G的顶点数目vexnum
//在图的存储方式为邻接矩阵法下实现的算法
void MST_Prim(Graph G){
int min_weight[G,vexnum]; //选择的顶点到其他顶点的权值
int adjvex[G.vexnum]; //被引入顶点的邻接节点下标
for(int i = 0; i<G.vexnum; i++) //初始化数组的过程
{
min_weight[i] = G.Edge[0][i]; //初始化min_weight数组为下标为0的顶点到其他顶点的权值
adjvex[i] = 0; //将adjvex数组初始化为0
}
int min_arc; //当前挑选的最小的边的权值
int min_vex; //当前挑选的边到另一个顶点数组下标
for(int i = 1; i<G.vexnum; i++) //已经加入一个顶点,剩下循环n-1次
{
min_arc = MAX; //最小边权值置为MAX,表示无穷大求最小值
for(int j = i; j<G.vexnum ; j++)
{
if(min_weight[j]!=0 && min_weight[j]<min_arc) //不等于0表示还未被添加进来,找最小权值的边不断进行修改
{
min_arc = min_weight[j]; //找最小的权值
min_vex = j; //最小权值边对应的数组下标
}
min_weight[min_vex] = 0; //选择添加进来的最小权值的边置为0
for(int j=0; j<G.vexnum; j++) //修改对应数组下标下的值
{
if(min_weight[j]!=0 && G.Edge[min_arc][j]<min_weight[j]) //若新加入的顶点和该顶点之间存在边且与之前的权值相比变小
{
min_weight[j] = G.Edge[min_arc][j]; //修改边的权值为新加入的权值所对应的边
adjvex[j] = min_arc; //修改辅助数组的值
}
}
}
}
}
克鲁斯卡尔算法(Kruskal)可以称为“加边法”
初始最小生成树边数为0,每迭代一次就选择一条满足条件的最小代价边,加入到最小生成树的边集合里。
- 把图中的所有边按代价从小到大排序;
- 把图中的n个顶点看成独立的n棵树组成的森林;
- 按权值从小到大选择边,所选的边连接的两个顶点ui,viui,vi,应属于两颗不同的树,则成为最小生成树的一条边,并将这两颗树合并作为一颗树。
- 重复(3),直到所有顶点都在一颗树内或者有n-1条边为止。
#include <stdio.h>
#define MAXE 100
#define MAXV 100
typedef struct
{
int vex1; //边的起始顶点
int vex2; //边的终止顶点
int weight; //边的权值
}Edge;
void kruskal(Edge E[],int n,int e)
{
int i,j,m1,m2,sn1,sn2,k,sum=0;
int vset[n+1];
for(i=1;i<=n;i++) //初始化辅助数组
vset[i]=i;
k=1;//表示当前构造最小生成树的第k条边,初值为1
j=0;//E中边的下标,初值为0
while(k<e)//生成的边数小于e时继续循环
{
m1=E[j].vex1;
m2=E[j].vex2;//取一条边的两个邻接点
sn1=vset[m1];
sn2=vset[m2]; //分别得到两个顶点所属的集合编号
if(sn1!=sn2)//两顶点分属于不同的集合,该边是最小生成树的一条边
{ //防止出现闭合回路
printf("V%d-V%d=%d\n",m1,m2,E[j].weight);
sum+=E[j].weight;
k++; //生成边数增加
if(k>=n)
break;
for(i=1;i<=n;i++) //两个集合统一编号
if (vset[i]==sn2) //集合编号为sn2的改为sn1
vset[i]=sn1;
}
j++; //扫描下一条边
}
printf("最小权值之和=%d\n",sum);
}
int fun(Edge arr[],int low,int high)
{
int key;
Edge lowx;
lowx=arr[low];
key=arr[low].weight;
while(low<high)
{
while(low<high && arr[high].weight>=key)
high--;
if(low<high)
arr[low++]=arr[high];
while(low<high && arr[low].weight<=key)
low++;
if(low<high)
arr[high--]=arr[low];
}
arr[low]=lowx;
return low;
}
void quick_sort(Edge arr[],int start,int end)
{
int pos;
if(start<end)
{
pos=fun(arr,start,end);
quick_sort(arr,start,pos-1);
quick_sort(arr,pos+1,end);
}
}
int main()
{
Edge E[MAXE];
int nume,numn;
freopen("1.txt","r",stdin);//文件输入
printf("输入顶数和边数:\n");
scanf("%d%d",&numn,&nume);
for(int i=0;i<nume;i++)
scanf("%d%d%d",&E[i].vex1,&E[i].vex2,&E[i].weight);
quick_sort(E,0,nume-1);
kruskal(E,numn,nume);
}