左神算法课PART3:图、BFS、DFS、拓扑排序、最小生成树、异或运算

lesson 8

前期准备

class Edge;
class Node{
public:
	int value;
	int in;
	int out;
	list<Node*> next;
	list<Edge*> edges;
	Node(int value){
		this->value = value;
		in = 0;
		out =0;
	}
};
class Edge{
public:
	int weight;
	Node* from;
	Node* to;
 
	Edge(int weight,Node* from, Node* to){
		this->weight = weight;
		this->from = from;
		this->to = to;
	}
};
class Graph{
public:
	unordered_map<int, Node*> nodes;
	unordered_set<Edge*> edges;
 
};

图的生成

class GraphGenerator{
public:
	//输入为边的矩阵,每一行三个数据:权值、出发节点fromm、到达节点to
	Graph createGraph(int matrix[][3],int rows, int col){
		Graph graph;
		for(int i=0;i<rows;i++){
			int weight = matrix[i][0];
			int from = matrix[i][1];
			int to = matrix[i][2];
			if(graph.nodes.find(from) == graph.nodes.end()){
				graph.nodes[from] = new Node(from);
			}
			if(graph.nodes.find(to) == graph.nodes.end()){
				graph.nodes[to] = new Node(to);
			}
			//以上两个if操作后,必能找到 from 好人to节点
			Node* fromNode = graph.nodes.find(from)->second;
			Node* toNode = graph.nodes.find(to)->second;
			//为 graph 和 from所在的node 准备 一条边
			Edge* newEdge = new Edge(weight, fromNode, toNode);
			//对于新增的一条边, 被指向节点的入度+1
			toNode->in++;
			//对于新增的一条边, 指向节点的出度+1,所指向的节点确定,指向该节点的边确定
			fromNode->out++;
			fromNode->next.push_back(toNode);
			fromNode->edges.push_back(newEdge);
			//两个if会保证建立节点,这里保证 边的存在。
			graph.edges.insert(newEdge);
 
		}
		return graph;
	}
};

BFS

用set哈希表来记录访问过的节点

void bfs(Node* node){
	if(node == nullptr)
		return ;
	queue<Node*> queue;
	unordered_set<Node*> set;
	Node* help;
	queue.push(node);
	set.insert(node);
	while(!queue.empty()){
		help = queue.front();
		queue.pop();
		cout << help->value<<endl;
		//用出队的当前节点来找进栈节点
		for(auto node: help->next){
			if(set.find(node) == set.end()){
				queue.push(node);
				set.insert(node);
			}
		}
	}
}

DFS

根节点每个子节点遍历完毕再遍历下一个子节点。

void dfs(Node* node){
	stack<Node*> stack;
	unordered_set<Node*> set;
	Node* help;
	//一进来就入栈并打印
	stack.push(node);
	set.insert(node);
	cout << node->value<<endl;
	while(!stack.empty()){
		help = stack.top();
		stack.pop();
		//对出栈的元素进行判断,若该节点的关联节点都被打印过了 或者该节点没其他关联接点,那么这个元素就ok了
		//若该节点还有其他关联节点没被访问,则直接访问后,再把弹出节点和访问节点都入栈
		for(auto node: help->next){
			if(set.find(node) == set.end()){
				cout << node->value<<endl;
				stack.push(help);
				stack.push(node);
				set.insert(node);
				break;
			}
		}
	}
}

拓扑排序

适用范围:要求①有向图②有入度为0的节点③没有环
【TIP】可以判断有向图是否有环

分析:在程序编译时,往往会有头文件互相依赖的情况,在图中箭头被指向的节点可以看做依赖于指向它的节点。如下图a依赖于b,c,d,而b又依赖c,k;d依赖k,那么拓扑排序的输出顺序是不依赖别的点的先输出。先输出k,c,删去k,c这时没有别的节点指向b,d了,输出b,d,最后,节点只剩下a再输出。
在图中可以用入度表示依赖情况,入度为零就是没有别的点指向它,可以先输出。输出后其指向的节点入度减一视为删去输出的点。

在这里插入图片描述
排序步骤:
①为了方便查找,使用map存储节点与入度方便查找与修改入度,使用queue存储入度为零的节点用于暂存需要输出的点;
②遍历图中的所有节点,把所有入度为零的节点放入队列;
③输出入度为零的节点;
④把入度为零的节点所指向的节点的入度减一(相当于删去当前节点)若减一后入度为零加入队列;
⑤在入度为0的队列不空的情况下重复③④。

list<Node*> sortedTopology(Graph graph)
{
	unordered_map<Node*,int> inMap;			//存储节点与入度 
	queue<Node*> zeroInQueue;				//存储入度为零的节点 
	unordered_map<int,Node*>::iterator ite = graph.nodes.begin();
	//遍历图中的所有节点,把所有入度为零的节点放入队列 
	while(ite != graph.nodes.end())								
	{
		inMap.insert(pair<Node*,int>(ite->second,ite->second->in));
		if(ite->second->in == 0)
			zeroInQueue.push(ite->second);
		ite++;
	}
	//输出入度为零的节点
	//把入度为零的节点所指向的节点的入度减一(相当于删去当前节点)
	//若又有入度为零的节点加入队列
	list<Node*> result;
	while(!zeroInQueue.empty())
	{
		Node* help = zeroInQueue.front();
		zeroInQueue.pop();
		result.push_back(help);
		cout<<help->value<<" ";
		for(Node* t : help->nexts)//通过在上方记录每个节点后置节点数组来省略此步
		{
			if(--inMap.find(t)->second == 0)
				zeroInQueue.push(t);
		}
	}
	return result;
}

在这里插入图片描述在这里插入图片描述

class Solution {
public:
    bool canFinish(int numCourses, vector<vector<int>>& prerequisites) {
        vector<int> indegree(numCourses,0);//入度表
        vector<vector<int>> post(numCourses);//记录该节点指向的所有节点
        queue<int> myque;//入度为0 数 的队列
        for(auto p:prerequisites){
            indegree[p[0]]++;
            post[p[1]].push_back(p[0]);
        }
        for(int i=0;i<numCourses;i++){
            if(indegree[i]==0)
                myque.push(i);
        }
        while(!myque.empty()){
            int temp=myque.front();
            myque.pop();
            numCourses--;
            for(int num:post[temp]){
                indegree[num]--;
                if(indegree[num]==0)
                    myque.push(num);
            }
        }
        return numCourses==0;
    }
};

最小生成树

定义:使无向图所有节点连通且权重最小的边集,是最小权重生成树的简称。
算法:kruskal算法、prim算法

kruskal算法

Kruskal算法是基于贪心的思想得到的。首先我们把所有的边按照权值先从小到大排列,接着按照顺序选取每条边,如果选取的这条边不会构成环,则加入这条边。

Q:如何判断选取这条边后是否会构成环
A:通过并查集实现,将之前加入的边的顶点都放入并查集,选取的这条边的两个顶点如果在同一个并查集内,则说明会构成环,跳过这条边。

在这里插入图片描述
核心代码:

	public static class EdgeComparator implements Comparator<Edge> {

		@Override
		public int compare(Edge o1, Edge o2) {
			return o1.weight - o2.weight;
		}

	}
	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;
	}

并查集部分:

	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);
				}
			}
		}
	}

Prim算法

在这里插入图片描述用小顶堆实现找出最小边,用hashset来记录已经加入的节点。

 
//primMST
unordered_set<Edge, EdgeHash, Equal_Edge> primMST(Graph graph){
	//装边的最小堆
	priority_queue<Edge,vector<Edge>,greater<Edge> > small_queue;
	//判断节点是否访问过了
	unordered_set<Node,NodeHash,Equal_Node> node_set;
	unordered_set<Edge,EdgeHash,Equal_Edge> result;
	//默认解决森林情况问题,单个图只需内层if,不需要该for循环语句
	for(auto ite: graph.nodes){
		//对每一个节点,都问一下是否访问过了,如果没放过过,做以下操作
		if(node_set.find(*ite.second) == node_set.end()){
			//把该节点表示为访问过了,再把该节点所解锁的边都加入最小堆中
			node_set.insert(*ite.second);
			for(Edge* edge: ite.second->edges){
				small_queue.push(*edge);
			}
			//在当前这个图中,取找最小生成树
			while(small_queue.size() != 0){
				//从最小堆中拿出一个最小权重边,并取得这条边所对应的节点
				Edge help_edge = small_queue.top();
				small_queue.pop();
				Node edge_to = *(help_edge.to);
				//判断这个节点是否已经被访问过了,如果没有,则这条边就加入解集,并且我们 认可该点现在被访问了,把该点所有连接的边都加入最小堆
				if( node_set.find(edge_to) == node_set.end()){
					result.insert(help_edge);
					node_set.insert(edge_to);
					for(Edge *newEdge : edge_to.edges){
						small_queue.push(*newEdge);
					}
				}
			}
		}
	}
	return result;
}

扩充

异或运算

在这里插入图片描述
1.异或运算又可以看成:无进位相加 ,且0^N=N,N ^N=0
2.满足:交换律和结合律
用无进位相加来证明很容易
3.不用额外变量交换两个数

int a=7;
int b=103
a=a^b;
b=a^b;
a=a^b;

证明:
int a=甲,int b=乙;
1)a=a^b => a=甲 ^ 乙 ,b=乙
2)b=a^b => a=甲 ^ 乙,b=甲 ^ 乙 ^ 乙=甲 ^ 0=甲
3)a=a^b => a=甲 ^ 乙 ^ 甲 =甲 ^甲 ^乙=乙 ,b=甲

4.题目:返回奇数词频的数
arr[]数组有一堆数,其中一种数出现了奇数次,其他所有数出现了偶数次。用O(1)空间复杂度求出该数。
思路:设一变量eor=0,依次异或数组所有数,最后结果就是出现奇数次的那个数。

5.一个数组中有两种数出现了奇数次,其他数都出现了偶数次,怎么找到这两个数。
提取出一个数二进制码最右侧的1:
int rightone=eor & (~eor =1)

  • 0
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值