【左程云 数据结构与算法笔记】P9 详解前缀树和贪心算法

737 阅读2分钟

持续创作,加速成长!这是我参与「掘金日新计划 · 10 月更文挑战」的第18天,点击查看活动详情 下面是我整理的跟着b站左程云的数据结构与算法学习笔记,欢迎大家一起学习。

图的存储方式

  1. 邻接表
  2. 邻接矩阵
  3. 如何表达图,生成图 :由点集边集组成 邻接表法可以直接查出点所有邻居,即点集

邻接矩阵法可以直接查出所有边,即边集

邻接表法和邻接矩阵法都可以表示有向图和无向图 某些特殊的图也可以使用数组表示(每一个点都有往上的点且没有环) 可以用自己喜欢表达图的方式把所有算法实现一遍作为模板,在题目出现新的图的表达方式,只要写一个图的转化到自己的模板上,将题目出现的图结构变成自己喜欢表达的图结构,即遇到表达图的特征比较特殊的题,转化成可以调模板函数的结构

宽度优先遍历

思路分析 得到图的首节点,通过set元素不可重复的特性将节点入队Queue中,Queue出队的顺序就是宽度优先遍历的结果 代码实现

public static void  bfs(Node head){  
    if (head==null){  
        return;  
    }  
    Queue<Node> queue=new LinkedList<>();  
    HashSet<Node> set = new HashSet<>();  
    queue.add(head);  
    set.add(head);  
    while(!queue.isEmpty()){  
        Node node=queue.poll();  
        System.out.println(node.value);  
        for(Node next:node.nexts){  
            if (!set.contains(node)){  
                set.add(node);  
                queue.add(node);  
            }  
        }  
    }  
}

使用set是为了节点不重复加进queue,防止程序进入死循环 通过输出得出遍历顺序

深度优先遍历

深度优先遍历的思路是一旦发现后续的路没走过,根据返回的节点继续走深度,直到把这条路都走完 代码实现

public static void dfs(Node node) {  
    if (node == null) {  
        return;  
    }  
    Stack<Node> stack = new Stack<>();  
    HashSet<Node> set = new HashSet<>();  
    stack.add(node);  
    set.add(node);  
    System.out.println(node.value);  
    while (!stack.isEmpty()) {  
        Node cur = stack.pop();  
        for (Node next : cur.nexts) {  
            if (!set.contains(next)) {  
                stack.push(cur);  
                stack.push(next);  
                set.add(next);  
            System.out.println(next.value);  
                break;  
            }  
        }  
    }  
}

注意看,当发现set里面未包含指定的节点时,先将此节点包含他的邻居节点一起送回Queue中,break跳出当前循环并从之前未发现的节点处遍历

拓扑排序 (适合有向图)

在指定有向图中,如何确定依次遍历的顺序,最终所有的路径都遍历 以该图为例 先找到入度为0的点,取出并去掉他的影响(即将从他出发的线一一去除),以此不断循环,最终得到拓扑排序的结果 代码实现

public static List<Node> sortedTopology(Graph graph) {  
    HashMap<Node, Integer> inMap = new HashMap<>();  
    Queue<Node> zeroInQueue = new LinkedList<>();  
    for (Node node : graph.nodes.values()) {  
        inMap.put(node, node.in);  
        if (node.in == 0) {  
            zeroInQueue.add(node);  
        }  
    }  
    List<Node> result = new ArrayList<>();  
    while (!zeroInQueue.isEmpty()) {  
        Node cur = zeroInQueue.poll();  
        result.add(cur);  
        for (Node next : cur.nexts) {  
            inMap.put(next, inMap.get(next) - 1);  
            if (inMap.get(next) == 0) {  
                zeroInQueue.add(next);  
            }  
        }  
    }  
    return result;  
}

kruskal算法(适用无向图)

从边的角度出发生成最小生成树,依次选择最小的边,将所有边排好序 ,从最小的边开始考虑,只需要考虑加上这条边之后不会形成环 如何保证不会生成环 在最开始,将不同点分进不同的集合中,如图 在连接AC时,查看A所在的集合和C所在的集合是否相同,

  • 不同则不会形成环,之后把AC两个集合合在一起
  • 相同则会形成环,不能要这条边 以此类推,按边的长度由小到大依次比较,已经在同一个集合中的点再出现连线就会形成环,直到所有的点都在同一个集合中,此时最小生成树生成成功 代码实现
public static class MySets{  
    public HashMap<Node, List<Node>> setMap;  
    public MySets(List<Node> nodes){  
        for(Node cur:nodes){  
           List<Node>  set = new ArrayList<Node>();  
           set.add(cur);  
           //此时每个点的集合中只有自己本身  
           setMap.put(cur, set);  
        }  
    }  
    public boolean isSameSet(Node from ,Node to){  
        List<Node> fromSet = setMap.get(from);  
        List<Node> toSet = setMap.get(to);  
        return fromSet==toSet;  
    }  
    public void union(Node from,Node to){  
        List<Node> fromSet = setMap.get(from);  
        List<Node> toSet = setMap.get(to);  
        for (Node toNode:toSet) {  
            fromSet.add(toNode);  
            //将toSet中节点的集合改为fromSet  
            setMap.put(toNode, fromSet);  
        }  
    }  
}

也可以采用并查集的结构

public static class UnionFind {  
    private HashMap<Node, Node> fatherMap;  
    private HashMap<Node, Integer> rankMap;  
  
    public UnionFind() {  
        fatherMap = new HashMap<Node, Node>();  
        rankMap = new HashMap<Node, Integer>();  
    }  
  
    private Node findFather(Node n) {  
        Node father = fatherMap.get(n);  
        if (father != n) {  
            father = findFather(father);  
        }  
        fatherMap.put(n, father);  
        return father;  
    }  
  
    public void makeSets(Collection<Node> nodes) {  
        fatherMap.clear();  
        rankMap.clear();  
        for (Node node : nodes) {  
            fatherMap.put(node, node);  
            rankMap.put(node, 1);  
        }  
    }  
  
    public boolean isSameSet(Node a, Node b) {  
        return findFather(a) == findFather(b);  
    }  
  
    public void union(Node a, Node b) {  
        if (a == null || b == null) {  
            return;  
        }  
        Node aFather = findFather(a);  
        Node bFather = findFather(b);  
        if (aFather != bFather) {  
            int aFrank = rankMap.get(aFather);  
            int bFrank = rankMap.get(bFather);  
            if (aFrank <= bFrank) {  
                fatherMap.put(aFather, bFather);  
                rankMap.put(bFather, aFrank + bFrank);  
            } else {  
                fatherMap.put(bFather, aFather);  
                rankMap.put(aFather, aFrank + bFrank);  
            }  
        }  
    }  
}  
  
  
public static Set<Edge> kruskalMST(Graph graph) {  
    UnionFind unionFind = new UnionFind();  
    unionFind.makeSets(graph.nodes.values());  
    PriorityQueue<Edge> priorityQueue = new PriorityQueue<>(new EdgeComparator());  
    for (Edge edge : graph.edges) {  
        priorityQueue.add(edge);  
    }  
    Set<Edge> result = new HashSet<>();  
    while (!priorityQueue.isEmpty()) {  
        Edge edge = priorityQueue.poll();  
        if (!unionFind.isSameSet(edge.from, edge.to)) {  
            result.add(edge);  
            unionFind.union(edge.from, edge.to);  
        }  
    }  
    return result;  
}

k算法是从线的角度实现最小生成树

Prim算法(要求无向图)

可以从任意一个点出发,当线被标志时才被解锁, 当边被解锁时,划勾 ,如在A时,AB,AC,AD都被解锁,每次添加新的点都会将该点的边加进priorityQueue中,在下一次在被解锁的边中且没有划下滑线的边中寻找权最小的边,若左右两个点都在set中,则需要选择其他点,假如到D时,此时AD都已在set集合中,需要选择BC,再从B到E 每次选的边只会把一个新的点加进来,检查一条边的时候判断左右两点是否在集合中,不需要这么复杂的集合,只需要放进一个HashMap中 代码

public static class EdgeComparator implements Comparator<Edge> {  
  
    @Override  
    public int compare(Edge o1, Edge o2) {  
        return o1.weight - o2.weight;  
    }  
  
}  
  
public static Set<Edge> primMST(Graph graph) {  
    PriorityQueue<Edge> priorityQueue = new PriorityQueue<>(  
            new EdgeComparator());  
    HashSet<Node> set = new HashSet<>();  
    Set<Edge> result = new HashSet<>();  
    //使用for循环解决森林问题 即所有的图都没有交集  
    for (Node node : graph.nodes.values()) {  
        if (!set.contains(node)) {  
            set.add(node);  
            for (Edge edge : node.edges) {  
                priorityQueue.add(edge);  
            }  
            while (!priorityQueue.isEmpty()) {  
                Edge edge = priorityQueue.poll();  
                Node toNode = edge.to;  
                if (!set.contains(toNode)) {  
                    set.add(toNode);  
                    result.add(edge);  
                    for (Edge nextEdge : toNode.edges) {  
                        priorityQueue.add(nextEdge);  
                    }  
                }  
            }  
        }  
    }  
    return result;  
}

可能会重复边,即便重复了边,其点已经在set中,会被跳过,可以加一个存放边的set进行判断,可以减少部分的时间复杂度

Dijkstra算法 --单元最短路径算法

适用于没出现累加和为负数的环,陷入死循环 规定出发点,到某一点的最短路径 如图,以A为出发点,生成到各点的最短距离 解题流程 A到各点的距离初始化时除了本身为0之外其他都为正无穷,当发现从A出发到各点距离有比之前距离更小时,替换,遍历完一遍后,A到A本身的距离确定,锁死 在剩下的记录中找到最短的长度为B 依次遍历B的所有边,从原路径A到B再到B边上的点,查看是否有更短路径 当遍历完所有记录都锁死,循环遍历结束

为什么不能出现累加和为负数的环

如果出现负数,不断循环可以将权值变小,不断变小陷入死循环 被锁死的记录也会出现不对的情况 代码实现

public static HashMap<Node, Integer> dijkstra1(Node head) {  
    HashMap<Node, Integer> distanceMap = new HashMap<>();  
    distanceMap.put(head, 0);  
    HashSet<Node> selectedNodes = new HashSet<>();  
  
    Node minNode = getMinDistanceAndUnselectedNode(distanceMap, selectedNodes);  
    while (minNode != null) {  
        int distance = distanceMap.get(minNode);  
        for (Edge edge : minNode.edges) {  
            Node toNode = edge.to;  
            if (!distanceMap.containsKey(toNode)) {  
                distanceMap.put(toNode, distance + edge.weight);  
            }  
            distanceMap.put(edge.to, Math.min(distanceMap.get(toNode), distance + edge.weight));  
        }  
        selectedNodes.add(minNode);  
        minNode = getMinDistanceAndUnselectedNode(distanceMap, selectedNodes);  
    }  
    return distanceMap;  
}  
  
public static Node getMinDistanceAndUnselectedNode(HashMap<Node, Integer> distanceMap,  
                                                   HashSet<Node> touchedNodes) {  
    Node minNode = null;  
    int minDistance = Integer.MAX_VALUE;  
    for (Map.Entry<Node, Integer> entry : distanceMap.entrySet()) {  
        Node node = entry.getKey();  
        int distance = entry.getValue();  
        if (!touchedNodes.contains(node) && distance < minDistance) {  
            minNode = node;  
            minDistance = distance;  
        }  
    }  
    return minNode;  
}

缺陷是 可能没有判断是否存在累加和为负数的环Dijkstra算法的优化可以从堆入手,堆可以吐出长度最小的边,但是不能修改堆里的数据,遍历完一个节点的边后可能要重新维护堆