数据结构与算法(二)

查找

二分查找

m i d = l o w + h i g h 2 = l o w + h i g h − l o w 2 mid=\frac{low+high}{2}=low+\frac{high-low}{2} mid=2low+high=low+2highlow

插值查找

m i d = l o w + k e y − a [ l o w ] a [ h i g h ] − a [ l o w ] ( h i g h − l o w ) mid=low+\frac{key-a[low]}{a[high]-a[low]}(high-low) mid=low+a[high]a[low]keya[low](highlow)

mid=low+(key-a[low])*(high-low)/(a[high]-a[low]);

斐波那契(黄金分割)查找

把数组填充到一个合适的F(k) 长度(拷贝一个新数组,末尾待填充的部分可以延拓最后一个值)
h i g h − l o w = F ( k ) = F ( k − 1 ) + F ( k − 2 ) high-low = F(k) = F(k-1)+F(k-2) highlow=F(k)=F(k1)+F(k2)
mid位于黄金分割点附近。令
m i d = l o w + F ( k − 1 ) − 1 mid = low+F(k-1)-1 mid=low+F(k1)1
往左是:

high=mid-1;
k--;

往右是:

low=mid+1;
k-=2;

哈希表

顺序存储二叉树

特点:

  • 完全二叉树
  • 序号为n的节点 左子节点 2 ∗ n + 1 2*n+1 2n+1
  • 右子节点 2 ∗ n + 2 2*n+2 2n+2
  • 父节点 ( n − 1 ) / 2 (n-1)/2 n1)/2

线索二叉树

利用二叉链表的空指针域,存放指向结点在某种遍历次序下的前驱和后继节点的指针。
与遍历次序相对应。

赫夫曼编码

9月3日看到 P121 05:19

二叉排序树 BST

Binary Sort/Search Tree

平衡二叉树AVL

  • 一颗空树或左右子树高度差的绝对值不超过 1 。并且左右子树都递归平衡
  • 常用实现方法:红黑树、AVL、替罪羊树、Treap、伸展树

AVL实现 代码

左旋:

右旋:

生成二叉树:

多路查找树

例如B树、B+树

  • 如果二叉树节点很多(海量数据),就需要进行多次IO操作。效率就较慢。
  • 同时二叉树的高度也会很高,查找效率也会较低。

多叉树

  • 每个节点有更多的数据项和更多的子节点

  • 相比较二叉树,能减少树的高度

  • 2-3树(最简单的B树结构)

    • 所有叶子节点都在同一层。
    • 有两个子叶子的节点叫二节点,二节点要么没有子节点,要么有两个。
    • 有三个子叶子的节点叫二节点,三节点要么没有子节点,要么有三个。
    • 2-3树是由二节点和三节点构成的树。
  • 插入规则

    • 当插入一个数到某个节点时,不能满足2-3树的特点要求,就需要拆,先向上拆,如果上层满,则拆本层,拆后仍需满足2-3树的基本特点。
    • 例如{16,24,12,32,14,26,34,10,8,28,38,20}
      • (1) |16|
      • (2) |16 | | 24|
      • (3) |16 | --> |12| |14|

B树

Balanced Tree

  • B树的阶:节点的最多子节点个数
  • B树的搜索,从根节点开始,对节点内的关键字序列进行二分查找,如果命中则结束,否则,进入查询关键字所属范围的子节点。直到对应的子节点指针为空,或已经是叶子节点。
  • 关键字集合分布在整个树中,即叶子节点和非叶子节点都存放数据。

B+树

  • 所有的关键字都在叶子节点的链表中,且链表中的关键字恰好是有序的。
  • 不可能在非叶子节点命中
  • 更适合文件索引系统
  • 和B树各有自己的应用场景,不能说哪个一定更好。

B*树

  • 是B+树的变体,在B+树的非根节点和叶子节点再增加指向兄弟的指针。
  • B*树定义了非叶子结点关键字个数至少为(2/3)*M,即块的最低使用率为2/3,而B+树的块的最低使用率为1/2。
  • 因此,B*树分配新节点的概率比B+树要低,空间使用率更高

图的表示方式

邻接矩阵(二维数组):为每个顶点分配n个边的空间,会造成空间的一定损失。
邻接表(数组+链表):只关心存在的边,不关心不存在的边。

深度优先访问

  1. 访问初始节点,并标记节点v为已访问
  2. 查找节点v的第一个邻接节点w
  3. 若w存在,则继续执行4;若w不存在,则回到第1步,访问v的下一个临界节点
  4. 若w未被访问,则对w进行深度优先遍历递归;
  5. 查找节点v的w邻接节点的下一个临界节点,转到第3步。

广度优先访问

分治算法

大整数乘法、棋盘覆盖、合并排序、快排等

  1. 分解
  2. 解决
  3. 合并

动态规划

动态规划求解的问题,经分解得到的子问题往往不是相互独立的,而分治往往子问题之间独立。

KMP算法

暴力匹配

一但失配,回溯

KMP

从头到尾彻底理解KMP(2014年8月22日版)—— v_JULY_v
KMP算法—终于全部弄懂了——DK丶S
KMP算法详解-彻底清楚了(转载+部分原创)——sofu6

贪心算法

解不一定是最优的。

应用——集合覆盖问题

找出覆盖所有地区的广播台的集合

广播台覆盖地区
K1“北京”,“上海”,“天津”
K2“广州”,“北京”,“深圳”
K3“成都”,“上海”,“杭州”
K4“上海”,“天津”
K5“杭州”,“大连”
  • 穷举:共n个广播台,广播台的组合为 2 n − 1 2^n-1 2n1
  • 贪心:优先找最多未覆盖地区的电台

Prim算法和Kruskal算法

应用场景——修路问题

  1. 有7个村庄ABCDEFG,需要修路将7个村庄联通
  2. 各个村庄的距离用边线表示(权)
  3. 如何修路保证各个村庄能连通,且总里程数最短。

思路

修路问题本质就是最小生成树(MST—— Minimum Cost Spanning Tree)

  • N个顶点,一定有N-1条边
  • 包含全部顶点
  • Prim算法和克鲁斯卡(Kruskal)算法

普利姆算法

步骤

  1. G = ( V , E ) G=(V,E) G=(V,E)是连通网, T = ( U , D ) T=(U,D) T=(U,D)是最小生成树,V、U是顶点集合,E、D是边的集合
  2. 若从顶点u开始构造最小生成树,则从集合V中取出顶点u放入结合U中,标记顶点v的state = 1(已访问);
  3. 若集合U中顶点 u i u_i ui与集合V-U中的顶点 v j v_j vj之间存在边,则寻找这些边中权值最小的边,但不能构成回路,将顶点 v j v_j vj假如集合U中,将边 ( u i , v j ) (u_i,v_j) (ui,vj)加入集合D中,标记 s t a t e [ v j ] = 1 state[v_j]=1 state[vj]=1
  4. 重复步骤2,直到U和V相等,即所有顶点都被标记为已访问过,此时D中有n-1条边
public void prim(int start) {
        int num = mVexs.length;         // 顶点个数
        int index=0;                    // prim最小树的索引,即prims数组的索引
        char[] prims  = new char[num];  // prim最小树的结果数组
        int[] weights = new int[num];   // 顶点间边的权值

        // prim最小生成树中第一个数是"图中第start个顶点",因为是从start开始的。
        prims[index++] = mVexs[start];

        // 初始化"顶点的权值数组",
        // 将每个顶点的权值初始化为"第start个顶点"到"该顶点"的权值。
        for (int i = 0; i < num; i++ )
            weights[i] = mMatrix[start][i];
        // 将第start个顶点的权值初始化为0。
        // 可以理解为"第start个顶点到它自身的距离为0"。
        weights[start] = 0;

        for (int i = 0; i < num; i++) {
            // 由于从start开始的,因此不需要再对第start个顶点进行处理。
            if(start == i)
                continue;

            int j = 0;
            int k = 0;
            int min = INF;
            // 在未被加入到最小生成树的顶点中,找出权值最小的顶点。
            while (j < num) {
                // 若weights[j]=0,意味着"第j个节点已经被排序过"(或者说已经加入了最小生成树中)。
                if (weights[j] != 0 && weights[j] < min) {
                    min = weights[j];
                    k = j;
                }
                j++;
            }

            // 经过上面的处理后,在未被加入到最小生成树的顶点中,权值最小的顶点是第k个顶点。
            // 将第k个顶点加入到最小生成树的结果数组中
            prims[index++] = mVexs[k];
            // 将"第k个顶点的权值"标记为0,意味着第k个顶点已经排序过了(或者说已经加入了最小树结果中)。
            weights[k] = 0;
            // 当第k个顶点被加入到最小生成树的结果数组中之后,更新其它顶点的权值。
            for (j = 0 ; j < num; j++) {
                // 当第j个节点没有被处理,并且需要更新时才被更新。
                if (weights[j] != 0 && mMatrix[k][j] < weights[j])
                    weights[j] = mMatrix[k][j];
            }
        }

        // 计算最小生成树的权值
        int sum = 0;
        for (int i = 1; i < index; i++) {
            int min = INF;
            // 获取prims[i]在mMatrix中的位置
            int n = getPosition(prims[i]);
            // 在vexs[0...i]中,找出到j的权值最小的顶点。
            for (int j = 0; j < i; j++) {
                int m = getPosition(prims[j]);
                if (mMatrix[m][n]<min)
                    min = mMatrix[m][n];
            }
            sum += min;
        }
        // 打印最小生成树
        System.out.printf("PRIM(%c)=%d: ", mVexs[start], sum);
        for (int i = 0; i < index; i++)
            System.out.printf("%c ", prims[i]);
        System.out.printf("\n");
    }
public void prim(int start) {
        int num = mVexs.length;         // 顶点个数
        int index=0;                    // prim最小树的索引,即prims数组的索引
        char[] prims  = new char[num];  // prim最小树的结果数组
        int[] weights = new int[num];   // 顶点间边的权值

        // prim最小生成树中第一个数是"图中第start个顶点",因为是从start开始的。
        prims[index++] = mVexs[start];

        // 初始化"顶点的权值数组",
        // 将每个顶点的权值初始化为"第start个顶点"到"该顶点"的权值。
        for (int i = 0; i < num; i++ )
            weights[i] = mMatrix[start][i];
        // 将第start个顶点的权值初始化为0。
        // 可以理解为"第start个顶点到它自身的距离为0"。
        weights[start] = 0;

        for (int i = 0; i < num; i++) {
            // 由于从start开始的,因此不需要再对第start个顶点进行处理。
            if(start == i)
                continue;

            int j = 0;
            int k = 0;
            int min = INF;
            // 在未被加入到最小生成树的顶点中,找出权值最小的顶点。
            while (j < num) {
                // 若weights[j]=0,意味着"第j个节点已经被排序过"(或者说已经加入了最小生成树中)。
                if (weights[j] != 0 && weights[j] < min) {
                    min = weights[j];
                    k = j;
                }
                j++;
            }

            // 经过上面的处理后,在未被加入到最小生成树的顶点中,权值最小的顶点是第k个顶点。
            // 将第k个顶点加入到最小生成树的结果数组中
            prims[index++] = mVexs[k];
            // 将"第k个顶点的权值"标记为0,意味着第k个顶点已经排序过了(或者说已经加入了最小树结果中)。
            weights[k] = 0;
            // 当第k个顶点被加入到最小生成树的结果数组中之后,更新其它顶点的权值。
            for (j = 0 ; j < num; j++) {
                // 当第j个节点没有被处理,并且需要更新时才被更新。
                if (weights[j] != 0 && mMatrix[k][j] < weights[j])
                    weights[j] = mMatrix[k][j];
            }
        }

        // 计算最小生成树的权值
        int sum = 0;
        for (int i = 1; i < index; i++) {
            int min = INF;
            // 获取prims[i]在mMatrix中的位置
            int n = getPosition(prims[i]);
            // 在vexs[0...i]中,找出到j的权值最小的顶点。
            for (int j = 0; j < i; j++) {
                int m = getPosition(prims[j]);
                if (mMatrix[m][n]<min)
                    min = mMatrix[m][n];
            }
            sum += min;
        }
        // 打印最小生成树
        System.out.printf("PRIM(%c)=%d: ", mVexs[start], sum);
        for (int i = 0; i < index; i++)
            System.out.printf("%c ", prims[i]);
        System.out.printf("\n");
    }
//https://www.cnblogs.com/skywang12345/
//https://blog.csdn.net/sunstar8921/article/details/81203445

Kruskal算法

按照权值从小到大的顺序选择n-1条边,并保证这些边不构成回路(!)。

public void kruskal() {
        int index = 0;                      // rets数组的索引
        int[] vends = new int[mEdgNum];     // 用于保存"已有最小生成树"中每个顶点在该最小树中的终点。
        EData[] rets = new EData[mEdgNum];  // 结果数组,保存kruskal最小生成树的边
        EData[] edges;                      // 图对应的所有边

        // 获取"图中所有的边"
        edges = getEdges();
        // 将边按照"权"的大小进行排序(从小到大)
        sortEdges(edges, mEdgNum);

        for (int i=0; i<mEdgNum; i++) {
            int p1 = getPosition(edges[i].start);      // 获取第i条边的"起点"的序号
            int p2 = getPosition(edges[i].end);        // 获取第i条边的"终点"的序号

            int m = getEnd(vends, p1);                 // 获取p1在"已有的最小生成树"中的终点
            int n = getEnd(vends, p2);                 // 获取p2在"已有的最小生成树"中的终点
            // 如果m!=n,意味着"边i"与"已经添加到最小生成树中的顶点"没有形成环路
            if (m != n) {
                vends[m] = n;                       // 设置m在"已有的最小生成树"中的终点为n
                rets[index++] = edges[i];           // 保存结果
            }
        }

        // 统计并打印"kruskal最小生成树"的信息
        int length = 0;
        for (int i = 0; i < index; i++)
            length += rets[i].weight;
        System.out.printf("Kruskal=%d: ", length);
        for (int i = 0; i < index; i++)
            System.out.printf("(%c,%c) ", rets[i].start, rets[i].end);
        System.out.printf("\n");
    }

利用群组的思想判断够不够成回路

/*
     * 获取i的终点
     */
    private int getEnd(int[] vends, int i) {
        while (vends[i] != 0)
            i = vends[i];
        return i;
    }

Dijkstra算法

应用——最短路径问题

计算从一个顶点 v 0 v_0 v0到其它顶点的最短距离。

维护一个距离的数组和一个前驱结点。dis[i]表示从 v 0 v0 v0经过 v p r e [ i ] v_{pre[i]} vpre[i] v i v_i vi的最短距离
再比较 v j v_j vj,若 w e i g h t ( v i , v j ) + d i s [ j ] < d i s [ i ] weight(v_i,v_j)+dis[j]<dis[i] weight(vi,vj)+dis[j]<dis[i],则更新 d i s [ i ] dis[i] dis[i]

public int[] dijkstra(Graph g,int beg){
        int n = g.size;
        // 参数空间准备
        boolean[] isVisited = new boolean[n];
        int[] dis = new int[n];//最小距离
        int[] pre = new int[n];//前驱节点
        int[][] w = g.edgeWei;

        // 初始化
        Arrays.fill(dis,Graph.MAX_WEIGHT);
        int pt = beg;
        for (int i = 0; i < w.length; i++) {
            if (w[pt][i]<Graph.MAX_WEIGHT){
                pre[i] = pt;
                dis[i] = w[pt][i];
            }
        }
        dis[pt] = 0;

        int visits = 0;
        while (visits<n){
            isVisited[pt] = true;
            visits++;
            //寻找距离最近的节点
            int min = Graph.MAX_WEIGHT;
            int mInd = -1;
            for (int i = 0; i < w.length; i++) {
                if (!isVisited[i] && w[pt][i]<min){
                    min = w[pt][i];
                    mInd = i;
                }
            }
            if (mInd==-1) {
                break;
            }
            pt = mInd;
            //更新最小距离
            for (int i = 0; i < w.length; i++) {
                if (w[mInd][i]+dis[mInd]<dis[i]){
                    pre[i] = mInd;
                    dis[i] = w[mInd][i]+dis[mInd];
                }
            }
        }

        for (int i = 0; i < pre.length; i++) {
            System.out.println("dis:"+dis[i]+" pre:"+(char)(pre[i]+'A'));
        }
        return dis;
    }

Floyd算法

应用——最短路径问题
计算出各个顶点之间的最短路径

相比较Dijkstra算法要更新的针对某一顶点的一维数组 d i s [ ] dis[] dis[],Floyd算法要计算各个顶点到其它顶点的最短路径,因此要维护一个二维数组。

 public int[][] floyd(Graph g){
        int n = g.size;
        // 参数空间准备
        int[][] dis = new int[n][n];
        int[][] w = g.edgeWei;
        // 初始化
        for (int i = 0; i < g.edgeWei.length; i++) {

            dis[i] = Arrays.copyOf(w[i],n);
        }

        //更新
        // if weight[j][k]+dis[k][i] < dis[j][i]  so  dis[j][i]= weight[j][k]+dis[k][i]
        for (int k = 0; k < n; k++) {
            for (int i = 0; i < n; i++) {
                for (int j = 0; j < n; j++) {
                    if (dis[j][k] == INF|| dis[k][i]==INF) continue;
                    int tmp = w[j][k] + dis[k][i];
                    if (tmp <dis[j][i]) {
                        dis[j][i] = tmp;
                        dis[i][j] = tmp;
                    }
                }
            }
        }

        return dis;
    }

骑士周游问题

  1. 二维数组表示棋盘
  2. 当前位置设置为已访问,下一步最多有8种可能
  3. 判断能否走通,能走通,则继续;否则,回溯
  4. 结束判断条件:走的总步数是否等于棋盘总格子数(要走遍棋盘)。
  5. 贪心优化:优先往下一步的可能多的位置走。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值