自学内容网 自学内容网

《数据结构》课程综合设计(zzu校园导航)(迪杰斯特拉算法)

一、系统(问题)描述

目前根据郑州大学主校区面积区域的广大,以及南、北核心教学楼的教室分布密集且较多;另外,多数地图软件无法精细导航到一个具体的地点,容易造成原地转圈的烦恼。但是,我们转换思路,仅仅根据我们对于校园充分的熟悉度,给出了郑州大学主校区柳、荷、菊、松四个园区和南、北核心教学楼各个教室的名称信息及有线路联通的地点之间的距离,利用郑州大学主校区教室导航系统计算出给定的起点到终点之间的最近距离及最优路线。

二、系统需求

zzu教室导航系统主要提供给用户三个选项进行操作,分别为输出地图、教室导航、退出。它主要用于教室导航,根据用户输入的起点和终点,为用户显示最佳路线。

程序具有的功能:

能够根据用户输入的起点和终点给出最短距离,输出最短路线。

1、InitOutput()目录:给出用户目录以供选择。

2、initInput(Pos p)初始化点信息:存储校园各点的地名信息;

3、initGraph() 初始化图:存储校园各个地点构成的边信息;

4、Dijkstra(Pos p, int Start, int End)求两点之间的最短路径:利用算法,快速给出用户到目的地的最短距离,并存储最佳路线;

5、PrintPath(Pos p, int start, int end, char *E)输出具体路线:给出用户具体的路线过程;

6、PrintRoad(char *d, char *end)d为进的门, end为教室:教学楼内部导航信息的建立;

三、设计思想

建图:将地点根据名称转换为数字来表示,建边和后序计算最短路均使用该点所对应的编号来操作。

求最短路:利用迪杰斯特拉算法+堆结构优化。

求最短路线:利用递归访问路线数组。

(一)数据结构的选择和设计

//位置类
typedef struct Position *Pos;
struct Position
{
    char name[Totalcharlen];//地点名
    int index; //编号
};

位置类Pos,用来根据编号来找地点名。后序进行迪杰斯特拉算法和输出道路时,均是通过编号来表示该点,即会用到Pos这个数据类型。

map<string, int> mpos; //名字对应位置

Map为c++的stl结构,利用map该数据结构实现给点编号,并可以通过点的名字来访问其的编号,在建点,建边中有用到。即通过点的名字,取得其的编号,然后按照编号来表示两点之间的关系。

//边
struct ty
{
    int t, len, next; // t:边终点,len:边长,next指向该点连的下一条边
} edge[TotalEdge];

链式前向星算法中用来存边的结构,给每条边设定编号,edge的下标为边的编号,每条边存该边的终点t,长度len,和与该边起点相同的边的上一条边的编号next。

//堆里数据
typedef struct Index *in;
typedef struct Index Index;
struct Index
{
    int x, dis; //最小堆里存放的,x:当前结点编号,dis:该点到其他点的距离
};

in为堆中数组里的元素,存放着该点的编号x,dis指的是起点到该点的距离。

//最小堆
typedef struct MinHeap *Mh;
struct MinHeap
{
    in i;     //数组
    int last; //堆尾。
};

Mh为数据结构最小堆,该堆中存储了数组i和队尾的下标last。堆是一个类似于二叉树的结构,与其不同的是其父亲结点比孩子大(小),为大(小)顶堆。这里利用数组结构来表示堆,其下标从1开始,设父亲节点为i,则左孩子为2*i,右孩子为2*i+1,且定义比较的大小是对in结构体数组里的dis(起点到该点的距离)的值来比较。

(二)算法设计

首先来说明下建图的过程,建图包括建点和建边两部分:

建点:

Pos initPos(int totalpos) //初始化点
{
    p = (Pos)malloc(sizeof(struct Position) * total); //分配点空间
    for (i = 0; i < totalpos; i++)
        p[i].index = 0; //初始化点的编号

    return p;
}
Status InsertPos(Pos p, char *name) //插入点函数
{
    mappos[name] = ++total; //给该点编号
    //存该点信息
    p[total].index = total;
    strcpy(p[total].name, name);
    return OK;
}
Status initInput(Pos p) //初始化点  //加点
{
    fp = fopen("datapos.txt", "r");
    if (fp == NULL) //如果文件指针为空
    {
        printf("文件打开失败\n");
        return ERROR;
    }
    else
    {
        printf("打开成功\n");
        fscanf(fp, "%d", &totaldata);
        for (i = 0; i < totaldata; i++)
        {
            fscanf(fp, "%s", pos); //该点的名
            InsertPos(p, pos);     //插入该点
        }
        printf("点信息输入完成!\n");
    }
    fclose(fp); //关闭文件
    return OK;
}

利用map数据结构,即程序中的mappos将图中的点根据名字的不同实现从1开始的编号。编号后再将其存入Pos--点数组里,实现可以通过点的名称和编号双向访问。程序中,是从文件中读取点然后将其插入。

建边:

Status AddEdge(PosType x, PosType y, EdgelenType len) //链式前向星,加边
{
    edge[++cnt].t = y;        //存边的尾
    edge[cnt].len = len;      //存边的长度
    edge[cnt].next = head[x]; //标志为以x的点为起点的边
    head[x] = cnt;            //更新以x为起点的边的标记
    return OK;
}
Status InsertEdge(char *name1, char *name2, EdgelenType len) //插边函数
{
    //找到两点编号
    PosType x1 = mpos[name1]; //找name1所对应的编号
    PosType x2 = mpos[name2]; //找name2所对应的编号
    AddEdge(x1, x2, len); //建x1->x2
    AddEdge(x2, x1, len); //建x2->x1
    return OK;
}
Status initGraph() //初始化图  //加道路
{
    fp = fopen("dataedge.txt", "r");

    if (fp == NULL) //如果文件指针为空
    {
        printf("文件打开失败\n");
        return ERROR;
    }
    else
    {
        printf("打开成功\n");
        printf("边信息输入完成!\n");
        fscanf(fp, "%d", &totaldata);
        for (i = 0; i < totaldata; i++)
        {
            char s1[Totalcharlen], s2[Totalcharlen];
            EdgelenType len;
            fscanf(fp, "%s %s %d", s1, s2, &len); //输入边的起始,终止点,长度
            InsertEdge(s1, s2, len);              //加双向边
        }
    }
    fclose(fp); //关闭文件
    return OK;
}

根据点的编号来建边,利用链式前向星的思想,先将head数组初始化为-1,head数组的下标代表点的编号,其存储的为该点所连的最后一条边的编号。每次建一条边,就用edge结构体数组来存储,其下标即为边的编号。

当在图中插入一条边时,先将边的总数编号+1,则此时总数为该边的编号,然后存储该边长度len,上一个同一起点的边的编号head[x该边起点)],更新head[起点] = 该边的编号。按照这样来建边。

按照这种方式来建边,使得每次访问以x为起点的边时,就从i = head[x]开始,即从以x为起点的最后一条边开始访问,edge[head[x]]即为该边信息。然后i = edge[i].next即为以x为起点的上一条边。程序中对于边的插入是通过文件读取每条边的起点,终点,长度来插入。

然后是来说明下如何求最短路:

//最短路算法
EdgelenType Dijkstra(Pos p, PosType Start, PosType End) //求两点之间的最短路径,并记下路径,返回路径
{
    memset(vis, 0, sizeof(vis));                   //初始化vis数组,指所有元素均未访问国
    memset(dis, Maxx, sizeof(dis));                //初始化dis数组,标记所有节点到start数组距离均为无穷大
    path = (int *)malloc(sizeof(int) * TotalEdge); //存经过路径
    Mh m = InitMinheap(TotalEdge);                 // 边的个数

    dis[Start] = 0;             //自己到自己的距离为0
    tmp.dis = 0, tmp.x = Start; //赋tmp值
    pushMinHeap(m, tmp);        //将第一个点放入堆中
    path[Start] = 0;            //记录下起点的path值为0,方便后序访问
    //遍历len,找更新所有和end的距离dis
    while (!IsEmptyHeap(m)) //如果堆不为空
    {
        tmp = getHeapTop(m);                           //取出堆顶元素(即dis最小的下标)
        popMinHeap(m);                                       //取出下标
        if (vis[tmp.x])                                      //如果该下标访问过
            continue;                                        //跳过
        vis[tmp.x] = 1;                                      //未访问过就将其标记
        for (i = head[tmp.x]; i != -1; i = edge[i].next) //遍历该点所连的边
        {
            y = edge[i].t;                     // y值为该边所指向的终点
            if (edge[i].len + dis[tmp.x] < dis[y]) //如果start到x的长度(dis[x]) + 该边长度 小于 start到X的长度
            {
                dis[y] = edge[i].len + dis[tmp.x]; //更新start到y的长度(dis[y])
                path[y] = tmp.x;                   //记录到y,所经过点x
                tmp2.x = y, tmp2.dis = edge[i].len + dis[tmp.x];
                pushMinHeap(m, tmp2); //压入堆
            }
        }
    }
    FreeMinHeap(m);  //释放堆内存
    return dis[End]; //返回start到end的最短距离
}

利用堆优化+迪杰斯特拉算法,vis数组表示起点到该点的最短距离是否已经求出。若为1,表示已求出,为0表示未求出。先初始化vis数组全为0,表示此时起点到该点的最短距离均未求出。dis数组表示起点到其他点的最短距离,先初始化为最大值。

开始计算最短路,每次从小顶堆顶中取出与起点距离最短的点,然后判断该点是否访问过,即是否已经求出起点到该点的最短距离,访问过就跳过该点,未访问过,就将该点x作为中间节点来更新起点到其他未求出最短距离的节点的距离,并标记该点访问过了。如果起点到该点的距离dis[x]+该点到其他点y的距离<起点到y的最短距离dis[y],就更新这个最短距离dis[y],然后将该点y的信息放入堆中。

输出道路:

//输出道路算法
void PrintPath(Pos p, PosType start, PosType end, char *E) //输出道路
{
    if (path[end] == 0) //因为起始点的path为0,因此此时递归到起始点
    {
        printf("%s", p[start].name); //输出起始点
        return;
    }
    else
    {
        PrintPath(p, start, path[end], E);   //一直递归到起始点后,开始输出
        if (!strcmp(p[end].name, E))         //如果当前点的下一个为终点
            strcpy(door, p[path[end]].name); //保存当前门(终点为教室,门为教室的上一个点)

        printf("->%s", p[end].name); //输出当前遍历的点名
    }
}

在迪杰斯特拉算法中,还维护了一个数组path。先将path[start] = 0,然后每次更新到起点到其他节点y的最短距离时,也同时更新y的path,即path[y] = x(中间节点),即path数组,除了起点外,其存储的为最短距离要经过path下标节点的道路的上一个节点。

然后在输出道路的函数中,递归输出,即从终点的path值开始递归,直到终点的path值为0,即此时终点的path值和起点相同,输出起点,这时回退,输出上一层终点的path值,再输出点,以此类推。

最小堆:

Status pushMinHeap(Mh m, Index x) //将x存入堆
{
    m->last++;                                      //堆数组最后的下标+1
    m->i[m->last] = x;                              //将X存入堆数组的最后一个位置。
    parent = m->last / 2, i = m->last;          // parent指向最后一个元素的父亲节点下标,i指向最后一个元素下标
    while (m->i[parent].dis > x.dis && parent != 0) //从下往上滤
    {
        swap(m->i[parent], m->i[i]); //因为为最小堆,如果父亲节点大于儿子节点,就交换
        i = parent;                  //儿子 = 父亲
        parent /= 2;                 //父亲 = 父亲的父亲
    }
    return OK;
}

对于堆的插入操作,首先是将堆的最后一个下标自增1,即last++,然后将插入的元素x插入堆中数组下标为last值的位置,开始从下到上调整堆的结构。即用一个指针i指向x元素的下标,一个指针p指向x元素的父亲节点,开始比较两者大小,如果父亲节点大于孩子节点并且此时父亲节点不为0(因为数组下标从1开始),说明不满足最小堆的要求,将二者调换,然后i指向p,p再指向i的父亲,再次比较,以此类推,直到父亲节点小于孩子节点,或者父亲节点下标为0为止。

Status popMinHeap(Mh m) //将堆头元素删除
{
    m->i[1] = m->i[m->last];                                           //先将第一个赋值为最后一个元素,也算删除了第一个元素
    i = 1;                                                         // i指向第一个元素的下标
    m->last--;                                                         //最后一个元素下标-1
    child = i * 2;                                                 //指向第一个元素的左孩子
    if (m->i[child].dis > m->i[child + 1].dis && child + 1 <= m->last) //如果有右孩子,且右孩子比左孩子小
        child++;                                                       //那么就指向右孩子
    while (m->i[child].dis < m->i[i].dis && child <= m->last)          //从上往下滤
    {
        swap(m->i[child], m->i[i]);                                        //如果i节点指向元素比最小的孩子还小,就交换
        i = child;                                                         // i指向孩子
        child *= 2;                                                        //孩子指向孩子的孩子
        if (m->i[child].dis > m->i[child + 1].dis && child + 1 <= m->last) //再次找最小的孩子
            child++;
    }
    return OK
}

对于堆的删除操作,因为堆中只能删除堆顶元素,即取出最小元素,那么就将堆中数组的最后一个元素提上来覆盖掉堆顶元素,然后开始从上往下调整。指针i指向根结点,指针c指向i的左孩子,先比较i的左孩子和右孩子谁更小,如果右孩子更小,则c指向右孩子。然后开始比较i指向的值和孩子节点值的大小,如果孩子节点的值小于父亲节点,说明不符合最小堆的性质,这时将两个节点进行交换,然后再将i指向c节点, c指向i的最小的孩子节点,然后再次比较,以此类推,直到符合最小堆性质为止。 

四、实现细节说明

求最短路径:迪杰斯特拉+堆优化,

利用迪杰斯特拉算法,用数组的下标表示点的编号,利用链式前向星思想来找每一个点的边,利用最小堆来存储每个点到起点的距离,然后每次从堆顶取出到起点距离最小的点来更新其他点到起点的最小距离,每次更新将该点的上一个点给记住,方便后序输出道路。

如以上界面,输入起始地点,终点。然后输出最短距离和道路。

系统根据用户输入的起点,先从系统中找该点是否存在,存在则继续下一步,,不存在就让用户再次输入。然后是判断用户输入的终点是否存在,且是否为教室,若不满足要求,则再次输入。若为教室则继续下一步,利用迪杰斯特拉算法计算两点间的最短距离,并保存路线,然后输出。

五、测试数据及测试结果

大体说明:

根据我们的测试结果,与我们校园实地的情况是一致的。

校园地图测试:

导航路线测试:

具体说明:

首先说明下系统中文件的导入:

点文件:用来存放所有地图上要初始化的点,即用户可以输入的地点,系统根据该文件来建点。

第一个311指总共有311个点。

然后每行一个地点。

边文件:用来存放所有边的文件,系统通过该文件内容建边。

第一个1121指总共有1121条边。

每行一条边,按照起点、终点、长度排列顺序。例如第二行的指从秋实西1到厚德北1长度为161米,且厚德北1到秋实西1也有一条161的边,实际上一行为两条边。

然后是介绍用户输入所实现的功能:

输入1,输出简化地图:

输入2,根据用户输入的起点和终点教室,输出路线。

如图所示,选择起点为本源南1,终点教室为南4-302。然后程序给出了具体走法,该走法可以参照地图来理解。

再次输入:

这次输入的起点为厚德北1,终点教室为为北3-204。

如图所示为程序给出的最佳路径。

输入3,退出系统,并释放内存。

六、总结

(一)系统特点及创新

特点:

1、简洁输出关键有用的信息,输出界面有简化地图的展示,输出距离和路线简洁明了。

2、更人性化,到达教学楼区域,有文字引导方向至具体教室。

创新性:

1、利用地点名到序号的映射,使代码写起来更清晰易懂。可以直接通过地点名来进行建图。

2、利用链式前向星的思想来建边,可以使访问一个点所连的边更快,避免了访问其他没有连边的节点。

3、利用堆来优化迪杰斯特拉算法,使算法效率提升。

4、利用数组递归方式来输出最短路线。

5、结合实际,将每个门如何到达教室的具体路线完善。

(二)遇到的问题及解决思路

1、路线距离测量困难

解决方法:我们通过查阅资料,在电脑端高德地图找到了测距尺,快速确定了各个地点间的距离。

2、地点名难与序号一一对应

解决方法:刚开始考虑的是利用哈希表,然后思考如何定义一种求有效快速的关键字的设计,实现更好的根据名字来定位元素。但是因为地点名为中文,有关中文如何编码不是很清楚,有想过将中文改为拼音来建图。但是拼音很容易就会打出错,对于系统维护来说不是很方便,因此就利用了c++的stl里面的map结构,来实现名字对应序号,至于序号对应名字,就可以利用结构体数组的下标来实现。

3、建点和建边部分的程序很长,且没太大意义

解决方法:将其用文件的方式输出。刚开始使用文件,发现根本运行不了,发现原来是对于中文的编码,文件要选择ANSI的格式。

4、程序输出最佳路线无法明确指向教室

解决方法:将教学楼的大门与每一个可以从该门口到达的教室连一条为0长度的边,这样用户输入教室终点时,其路线的终点就会指向教室了。

5、对于用户输入的起点和终点缺少判断,导致随便输入代码也会继续执行出错

解决方法:首先是对起始点的判断,起始点输入的,要为点文件中给出的已经初始化的点,通过map的性质进行判断,如果用户输入的点不在map中,那么直接该点的map值为0,而我们初始化点的序号从1开始,就可以判断了。对于终点输入是否为教室的判断,读取点文件,看输入的教室是否存在来判断。

最后,可能以上的程序还是存在瑕疵,大家可以适当修改和完善,感谢阅读!


原文地址:https://blog.csdn.net/m0_60531106/article/details/142847616

免责声明:本站文章内容转载自网络资源,如本站内容侵犯了原著者的合法权益,可联系本站删除。更多内容请关注自学内容网(zxcms.com)!