• 大话数据结构-查找


    大话数据结构-查找

    文章知识点来至于大话数据结构里边章节知识, 这篇主要介绍查找功能在计算机中存储形式, 以及在某些算法领域中对图的相关应用。本篇涉及到的知识点也比较多在查找中介绍了线性查找、折半查找、二叉排序树。该篇主要以二叉排序树为例,通过流程图以及算法实现来掌握二叉排序树的增删改查功能。相关代码源码请查看文章最后。本篇最后的算法描述和流程图以及代码实现是重点,如果对本篇感兴趣一定要通过该部分来巩固数据机构。

    查找

             查找就是根据给定的某个值,在查找表中确定一个其关键字等于给定值的数据元素。

    1 顺序表查找

             定义:又叫线性查找,是最基本的查找技术,它的查找过程是:从表中第一个(或最后一个)记录开始,逐个进行关键之与给定值比较,若某个记录的关键字和给定值相等,则查找成功,找到所查的记录;如果知道最后一个记录,其关键字与给定值比较都不等时,则表中没有所查找的记录,查找不成功。

    2 有序表查找

             折半查找:又称二分查找。它的前提是线性表中的记录必须是关键码有序的(通常是从小到大有序),线性表必须采用顺序存储。

             插值查找:是根据要查找的关键字key与查找表中最大最小记录的关键字比较后的查找方法,其核心就在于插值的计算公式:                        

    线性索引查找:索引就是把一个关键字与他对应的记录想关联的过程。索引按照结构可以分为:线性索引、树形索引、多级索引。所谓线性索引就是将索引项集合组织为线性表,也称为索引表。常见线性索引包括:稠密索引、分块索引、倒排索引。

    (1)稠密索引:是指在线性索引,将数据集中的每个记录对应一个索引项。

     

             缺点:如果数据量非常大,上亿条。那么索引也得同样的数据集长度规模。

    (2) 分块索引(关系型数据库使用):特点是分块有序,是把数据集的记录分成了若干块,并且这些块需要满足两个条件:

             *块内无序,即每个块内不要求有序;

             *块间有序,例如第二块的所有记录的关键字均要大于第一块中的所以记录的关键字。

             对于分块有序的数据集,将每块对应一个索引项,这种索引方法叫做分块索引。索引项机构分为三个数据项:

                       *最大关键码,存储每一块中最大的关键字;

                       *存储了每块的记录数,以便于循环使用;

                       *用于指向块首数据元素的指针;

     

    (3)倒排索引(搜索引擎使用):首先看图,图表示单词以及档次在哪些文章中出现。这张单词表就是索引表. 

             索引项的通用结构:

           其中记录号表存储具有相同次关键字的所有记录的记录号(可以是指向记录的指针或者是该记录的主关键字)。这样的索引方法就是倒排序索引。

    3 二叉排序树

             定义:二叉排序树(Binary Sort Tree),又称为二叉查找树。它或者是一颗空树,或者是具有下列性质的二叉树。

             若它的左子树不空,则左子树上所有的节点的值均小于根节点值;

             若它的右子树不空,则右子树所有的节点的值均大于根节点值;

             它的左、右子树也分别为二叉排序树

             二叉排序树查找方法

            

             

             

             二叉排序树插入方法

     

     

             二叉排序树删除方法

     

     

             二叉排序树优点

                       二叉排序树是以连接的方式存储,保持了连接存储结构在执行插入或删除操作时不用移动元素的优点,只要找到合适的插入或删除位置后,仅需修改连接指针即可。插入和删除的事件性能比较好。

    4 平衡二叉树(AVL树)

             定义:平衡二叉树是一种二叉排序树,其中每一个节点的的左子树和右子树的高度差之多等于1。我们将二叉树上结点的左子树深度减去右子树深度的值称为平衡因子BF(Balance Factor)。

             最小不平衡子树:距离插入节点最近的,且平衡因子的绝对值大于1的节点为根的子树,我们称为最小不平衡子树。

    节点定义:

    右旋操作:

    左旋操作:

     

    5 多路查找树

             定义:多路查找树(mutil-way search tree),其每一个节点的孩子树可以多于两个,且每个节点处可存储多个元素。

    6 散列表查找

             定义:散列技术是在记录的存储位置和它的关键字之间建立一个确定的对应关系f,使得每个关键字key对应一个存储位置f(key)。

             散列技术最适合的求解问题是查找与给定值相等的记录。

             散列函数的构造方法:

                       直接地址法:

                       数字分析法:数字分析法通常适合处理关键字位数比较大的情况, 如果事先知道关键字的分布且关键字的诺干位分布较均匀,就可考虑数字分析法。

                       平方取中发:

                       折叠法:折叠法是将关键字从左到右分割成位数相等的几部分(注意最后一部分位数不够时可以短些),然后将这几部分叠加求和,并按散列列表表长,取后几位作为散列地址。

                       除留余数法:

                       随机数法:    

                       总之,在现实中,应该视不同情况采用不同的散列函数,参考因素:

                     

    7 二叉排序树流程图

     a  查找:

     b 插入:

    c 删除:

    8 算法实现

    复制代码
     树节点定义
    复制代码
    复制代码
     查找
    复制代码
    复制代码
     插入
    复制代码
    复制代码
     删除
    复制代码

    9 单元测试

    复制代码
     单元测试
    复制代码

    最后附上源代码下载地址:

    http://download.csdn.net/detail/w_wanglei/5689883

     

    彻底弄懂最短路径问题

    Posted on 2013-08-20 16:12 DM张朋飞 阅读(297) 评论(11编辑 收藏

            只想说:温故而知新,可以为师矣。我大二的《数据结构》是由申老师讲的,那时候不怎么明白,估计太理论化了(ps:或许是因为我睡觉了);今天把老王的2011年课件又看了一遍,给大二的孩子们又讲了一遍,随手谷歌了N多资料,算是彻底搞懂了最短路径问题。请读者尽情享用……

            我坚信:没有不好的学生,只有垃圾的教育。不过没有人理所当然的对你好,所以要学会感恩。

    一.问题引入

            问题:从某顶点出发,沿图的边到达另一顶点所经过的路径中,各边上权值之和最小的一条路径——最短路径。解决最短路的问题有以下算法,Dijkstra算法,Bellman-Ford算法,Floyd算法和SPFA算法,另外还有著名的启发式搜索算法A*,不过A*准备单独出一篇,其中Floyd算法可以求解任意两点间的最短路径的长度。笔者认为任意一个最短路算法都是基于这样一个事实:都基于这样一个事实:从任意节点A到任意节点B的最短路径不外乎2种可能,1是直接从A到B,2是从A经过若干个节点到B。

    二.Dijkstra算法

            该算法在《数据结构》课本里是以贪心的形式讲解的,不过在《运筹学》教材里被编排在动态规划章节,建议读者两篇都看看。

               image

            观察右边表格发现除最后一个节点外其他均已经求出最短路径。

            (1)   迪杰斯特拉(Dijkstra)算法按路径长度(看下面表格的最后一行,就是next点)递增次序产生最短路径。先把V分成两组:

    • S:已求出最短路径的顶点的集合
    • V-S=T:尚未确定最短路径的顶点集合

            将T中顶点按最短路径递增的次序加入到S中,依据:可以证明V0到T中顶点Vk的最短路径,或是从V0到Vk的直接路径的权值或是从V0经S中顶点到Vk的路径权值之和(反证法可证,说实话,真不明白哦)。

            (2)   求最短路径步骤

    1. 初使时令 S={V0},T={其余顶点},T中顶点对应的距离值, 若存在<V0,Vi>,为<V0,Vi>弧上的权值(和SPFA初始化方式不同),若不存在<V0,Vi>,为Inf。
    2. 从T中选取一个其距离值为最小的顶点W(贪心体现在此处),加入S(注意不是直接从S集合中选取,理解这个对于理解vis数组的作用至关重要),对T中顶点的距离值进行修改:若加进W作中间顶点,从V0到Vi的距离值比不加W的路径要短,则修改此距离值(上面两个并列for循环,使用最小点更新)。
    3. 重复上述步骤,直到S中包含所有顶点,即S=V为止(说明最外层是除起点外的遍历)。

            下面是上图的求解过程,按列来看,第一列是初始化过程,最后一行是每次求得的next点。

               image

            (3)   问题:Dijkstar能否处理负权边?(下面的解释引自网上某大虾)

                 答案是不能,这与贪心选择性质有关(ps:貌似还是动态规划啊,晕了),每次都找一个距源点最近的点(dmin),然后将该距离定为这个点到源点的最短路径;但如果存在负权边,那就有可能先通过并不是距源点最近的一个次优点(dmin'),再通过这个负权边L(L<0),使得路径之和更小(dmin'+L<dmin),则dmin'+L成为最短路径,并不是dmin,这样dijkstra就被囧掉了。比如n=3,邻接矩阵: 
    0,3,4 
    3,0,-2 
    4,-2,0,用dijkstra求得d[1,2]=3,事实上d[1,2]=2,就是通过了1-3-2使得路径减小。不知道讲得清楚不清楚。

    二.Floyd算法

            参考了南阳理工牛帅(目前在新浪)的博客。

            Floyd算法的基本思想如下:从任意节点A到任意节点B的最短路径不外乎2种可能,1是直接从A到B,2是从A经过若干个节点到B,所以,我们假设dist(AB)为节点A到节点B的最短路径的距离,对于每一个节点K,我们检查dist(AK) + dist(KB) < dist(AB)是否成立,如果成立,证明从A到K再到B的路径比A直接到B的路径短,我们便设置 dist(AB) = dist(AK) + dist(KB),这样一来,当我们遍历完所有节点K,dist(AB)中记录的便是A到B的最短路径的距离。

            很简单吧,代码看起来可能像下面这样:

    for (int i=0; i<n; ++i) {
    
      for (int j=0; j<n; ++j) {
    
        for (int k=0; k<n; ++k) {
    
          if (dist[i][k] + dist[k][j] < dist[i][j] ) {
    
            dist[i][j] = dist[i][k] + dist[k][j];
    
          }
    
        }
    
      }
    
    }

            但是这里我们要注意循环的嵌套顺序,如果把检查所有节点K放在最内层,那么结果将是不正确的,为什么呢?因为这样便过早的把i到j的最短路径确定下来了,而当后面存在更短的路径时,已经不再会更新了。

            让我们来看一个例子,看下图:

    image

            图中红色的数字代表边的权重。如果我们在最内层检查所有节点K,那么对于A->B,我们只能发现一条路径,就是A->B,路径距离为9,而这显然是不正确的,真实的最短路径是A->D->C->B,路径距离为6。造成错误的原因就是我们把检查所有节点K放在最内层,造成过早的把A到B的最短路径确定下来了,当确定A->B的最短路径时dist(AC)尚未被计算。所以,我们需要改写循环顺序,如下:

            ps:个人觉得,这和银行家算法判断安全状态(每种资源去测试所有线程),树状数组更新(更新所有相关项)一样的思想。

    for (int k=0; k<n; ++k) {
    
      for (int i=0; i<n; ++i) {
    
        for (int j=0; j<n; ++j) {
    
                /*
    
                实际中为防止溢出,往往需要选判断 dist[i][k]和dist[k][j
    
                都不是Inf ,只要一个是Inf,那么就肯定不必更新。 
    
                */
    
          if (dist[i][k] + dist[k][j] < dist[i][j] ) {
    
            dist[i][j] = dist[i][k] + dist[k][j];
    
          }
    
        }
    
      }
    
    }

            如果还是看不懂,那就用草稿纸模拟一遍,之后你就会豁然开朗。半个小时足矣(早知道的话会节省很多个半小时了。。狡猾

           再来看路径保存问题:

    void floyd() {
    
          for(int i=1; i<=n ; i++){
    
            for(int j=1; j<= n; j++){
    
              if(map[i][j]==Inf){
    
                   path[i][j] = -1;//表示  i -> j 不通 
    
              }else{
    
                   path[i][j] = i;// 表示 i -> j 前驱为 i
    
              }
    
            }
    
          }
    
          for(int k=1; k<=n; k++) {
    
            for(int i=1; i<=n; i++) {
    
              for(int j=1; j<=n; j++) {
    
                if(!(dist[i][k]==Inf||dist[k][j]==Inf)&&dist[i][j] > dist[i][k] + dist[k][j]) {
    
                  dist[i][j] = dist[i][k] + dist[k][j];
    
                  path[i][k] = i;
    
                  path[i][j] = path[k][j];
    
                }
    
              }
    
            }
    
          }
    
        }
    
        void printPath(int from, int to) {
    
            /*
    
             * 这是倒序输出,若想正序可放入栈中,然后输出。
    
             * 
    
             * 这样的输出为什么正确呢?个人认为用到了最优子结构性质,
    
             * 即最短路径的子路径仍然是最短路径
    
             */
    
            while(path[from][to]!=from) {
    
                System.out.print(path[from][to] +"");
    
                to = path[from][to];
    
            }
    
        }

            《数据结构》课本上的那种方式我现在还是不想看,看着不舒服……

            Floyd算法另一种理解DP,为理论爱好者准备的,上面这个形式的算法其实是Floyd算法的精简版,而真正的Floyd算法是一种基于DP(Dynamic Programming)的最短路径算法。设图G中n 个顶点的编号为1到n。令c [i, j, k]表示从i 到j 的最短路径的长度,其中k 表示该路径中的最大顶点,也就是说c[i,j,k]这条最短路径所通过的中间顶点最大不超过k。因此,如果G中包含边<i, j>,则c[i, j, 0] =边<i, j> 的长度;若i= j ,则c[i,j,0]=0;如果G中不包含边<i, j>,则c (i, j, 0)= +∞。c[i, j, n] 则是从i 到j 的最短路径的长度。对于任意的k>0,通过分析可以得到:中间顶点不超过k 的i 到j 的最短路径有两种可能:该路径含或不含中间顶点k。若不含,则该路径长度应为c[i, j, k-1],否则长度为 c[i, k, k-1] +c [k, j, k-1]。c[i, j, k]可取两者中的最小值。状态转移方程:c[i, j, k]=min{c[i, j, k-1], c [i, k, k-1]+c [k, j, k-1]},k>0。这样,问题便具有了最优子结构性质,可以用动态规划方法来求解。

            看另一个DP(直接引用王老师课件)

                           image

            说了这么多,相信读者已经跃跃欲试了,咱们看一道例题,以ZOJ 1092为例:给你一组国家和国家间的部分货币汇率兑换表,问你是否存在一种方式,从一种货币出发,经过一系列的货币兑换,最后返回该货币时大于出发时的数值(ps:这就是所谓的投机倒把吧),下面是一组输入。 
    3    //国家数 
    USDollar  //国家名 
    BritishPound 
    FrenchFranc 
       3    //货币兑换数 
    USDollar 0.5 BritishPound  //部分货币汇率兑换表 
    BritishPound 10.0 FrenchFranc 
    FrenchFranc 0.21 USDollar

            月赛做的题,不过当时用的思路是求强连通分量(ps:明明说的,那时我和华杰感觉好有道理),也没做出来,现在知道了直接floyd算法就ok了。

            思路分析:输入的时候可以采用Map<String,Integer> map = new HashMap<String,Integer>()主要是为了获得再次包含汇率输入时候的下标以建图(感觉自己写的好拗口),或者第一次直接存入String数组str,再次输入的时候每次遍历str数组,若是equals那么就把str的下标赋值给该币种建图。下面就是floyd算法啦,初始化其它点为-1,对角线为1,采用乘法更新求最大值。

    三.Bellman-Ford算法

            为了能够求解边上带有负值的单源最短路径问题,Bellman(贝尔曼,动态规划提出者)和Ford(福特)提出了从源点逐次绕过其他顶点,以缩短到达终点的最短路径长度的方法。 Bellman-ford算法是求含负权图的单源最短路径算法,效率很低,但代码很容易写。即进行不停地松弛,每次松弛把每条边都更新一下,若n-1次松弛后还能更新,则说明图中有负环,无法得出结果,否则就成功完成。Bellman-ford算法有一个小优化:每次松弛先设一个flag,初值为FALSE,若有边更新则赋值为TRUE,最终如果还是FALSE则直接成功退出。Bellman-ford算法浪费了许多时间做无必要的松弛,所以SPFA算法用队列进行了优化,效果十分显著,高效难以想象。SPFA还有SLF,LLL,滚动数组等优化。

            关于SPFA,请看我这一篇http://www.cnblogs.com/hxsyl/p/3248391.html

            递推公式(求顶点u到源点v的最短路径):

             dist 1 [u] = Edge[v][u]

             dist k [u] = min{ dist k-1 [u], min{ dist k-1 [j] + Edge[j][u] } }, j=0,1,…,n-1,j≠u

             Dijkstra算法和Bellman算法思想有很大的区别:Dijkstra算法在求解过程中,源点到集合S内各顶点的最短路径一旦求出,则之后不变了,修改  的仅仅是源点到T集合中各顶点的最短路径长度。Bellman算法在求解过程中,每次循环都要修改所有顶点的dist[ ],也就是说源点到各顶点最短路径长度一直要到Bellman算法结束才确定下来。

            算法适用条件

    • 1.单源最短路径(从源点s到其它所有顶点v)
    • 有向图&无向图(无向图可以看作(u,v),(v,u)同属于边集E的有向图)
    • 边权可正可负(如有负权回路输出错误提示)
    • 差分约束系统(至今貌似只看过一道题)

            Bellman-Ford算法描述:

    1. 初始化:将除源点外的所有顶点的最短距离估计值 d[v] ←+∞, d[s] ←0
    2. 迭代求解:反复对边集E中的每条边进行松弛操作,使得顶点集V中的每个顶点v的最短距离估计值逐步逼近其最短距离;(运行|v|-1次,看下面的描述性证明(当做树))
    3. 检验负权回路:判断边集E中的每一条边的两个端点是否收敛。如果存在未收敛的顶点,则算法返回false,表明问题无解;否则算法返回true,并且从源点可达的顶点v的最短距离保存在d[v]中

            描述性证明:(这个解释很好)

            首先指出,图的任意一条最短路径既不能包含负权回路,也不会包含正权回路,因此它最多包含|v|-1条边。

    其次,从源点s可达的所有顶点如果 存在最短路径,则这些最短路径构成一个以s为根的最短路径树。Bellman-Ford算法的迭代松弛操作,实际上就是按顶点距离s的层次,逐层生成这棵最短路径树的过程。

    在对每条边进行1遍松弛的时候,生成了从s出发,层次至多为1的那些树枝。也就是说,找到了与s至多有1条边相联的那些顶点的最短路径;对每条边进行第2遍松弛的时候,生成了第2层次的树枝,就是说找到了经过2条边相连的那些顶点的最短路径……。因为最短路径最多只包含|v|-1条边,所以,只需要循环|v|-1 次。

    每实施一次松弛操作,最短路径树上就会有一层顶点达到其最短距离,此后这层顶点的最短距离值就会一直保持不变,不再受后续松弛操作的影响。(但是,每次还要判断松弛,这里浪费了大量的时间,这就是Bellman-Ford算法效率底下的原因,也正是SPFA优化的所在)。

    image,如图(没找到画图工具的射线),若是B和C的最短路径不更新,那么点D的最短路径肯定也无法更新,这就是优化所在。

    如果没有负权回路,由于最短路径树的高度最多只能是|v|-1,所以最多经过|v|-1遍松弛操作后,所有从s可达的顶点必将求出最短距离。如果 d[v]仍保持 +∞,则表明从s到v不可达。

    如果有负权回路,那么第 |v|-1 遍松弛操作仍然会成功,这时,负权回路上的顶点不会收敛。

                参考来源:http://blog.sina.com.cn/s/blog_6803426101014l1v.html

            问题:Bellman-Ford算法是否一定要循环n-1次么?未必!其实只要在某次循环过程中,考虑每条边后,都没能改变当前源点到所有顶点的最短路径长度,那么Bellman-Ford算法就可以提前结束了(开篇提出的小优化就是这个)。

            上代码(来自牛帅)

    #include<iostream>
    
    #include<cstdio>
    
    using namespace std;
    
    
    
    #define MAX 0x3f3f3f3f
    
    #define N 1010
    
    int nodenum, edgenum, original; //点,边,起点
    
    
    
    typedef struct Edge //边
    
    {
    
      int u, v;
    
      int cost;
    
    }Edge;
    
    
    
    Edge edge[N];
    
    int dis[N], pre[N];
    
    
    
    bool Bellman_Ford()
    
    {
    
      for(int i = 1; i <= nodenum; ++i) //初始化
    
        dis[i] = (i == original ? 0 : MAX);
    
      for(int i = 1; i <= nodenum - 1; ++i)
    
        for(int j = 1; j <= edgenum; ++j)
    
          if(dis[edge[j].v] > dis[edge[j].u] + edge[j].cost) //松弛(顺序一定不能反~)
    
          {
    
            dis[edge[j].v] = dis[edge[j].u] + edge[j].cost;
    
            pre[edge[j].v] = edge[j].u;
    
          }
    
          bool flag = 1; //判断是否含有负权回路
    
          for(int i = 1; i <= edgenum; ++i)
    
            if(dis[edge[i].v] > dis[edge[i].u] + edge[i].cost)
    
            {
    
              flag = 0;
    
              break;
    
            }
    
            return flag;
    
    }
    
    
    
    void print_path(int root) //打印最短路的路径(反向)
    
    {
    
      while(root != pre[root]) //前驱
    
      {
    
        printf("%d-->", root);
    
        root = pre[root];
    
      }
    
      if(root == pre[root])
    
        printf("%d
    ", root);
    
    }
    
    
    
    int main()
    
    {
    
      scanf("%d%d%d", &nodenum, &edgenum, &original);
    
      pre[original] = original;
    
      for(int i = 1; i <= edgenum; ++i)
    
      {
    
        scanf("%d%d%d", &edge[i].u, &edge[i].v, &edge[i].cost);
    
      }
    
      if(Bellman_Ford())
    
        for(int i = 1; i <= nodenum; ++i) //每个点最短路
    
        {
    
          printf("%d
    ", dis[i]);
    
          printf("Path:");
    
          print_path(i);
    
        }
    
      else
    
        printf("have negative circle
    ");
    
      return 0;
    
    }

    四.SPFA算法

            用一个队列来进行维护。初始时将源加入队列。每次从队列中取出一个元素,并对所有与他相邻的点进行松弛,若某个相邻的点松弛成功,则将其入队。直到队列为空时算法结束;这个算法,简单的说就是队列优化的bellman-ford,利用了每个点不会更新次数太多的特点发明的此算法(看我上面那个图,只有相邻点更新了,该点才有可能更新) 。

             代码参见  http://www.cnblogs.com/hxsyl/p/3248391.html

    五.趣闻

            整理该篇博文的时候,一哥们发布网站到我们群,网站很精美,一牛神(acmol)使用fork炸弹,结果服务器立马挂啦,更改后又挂啦,目测目前无限挂中。。。

     
     
    标签: 数据结构
  • 相关阅读:
    利用window.onerror收集js代码异常
    js 基础题复习
    正则12和\1的理解
    CodeReview 方法 规范
    前端的登陆 几种类型
    http协议相关知识
    javascript中apply、call和bind的区别 详细易懂
    http常见的状态码,400,401,403 前端看
    vue中使用file-saver 下载各类文件
    js下载文件到本地各种方法总结
  • 原文地址:https://www.cnblogs.com/Leo_wl/p/3270690.html
Copyright © 2020-2023  润新知