汇总了很多博客的内容,作为学习笔记~
图的遍历
从图中的某个顶点出发,按某种方法对图中的所有顶点访问且仅访问一次。为了保证图中的顶点在遍历过程中仅访问一次,要为每一个顶点设置一个访问标志。通常有两种方法:深度优先搜索(DFS)和广度优先搜索(BFS).这两种算法对有向图与无向图均适用。
深度优先搜索(DFS):
// 从第i个节点开始深度优先遍历
private void traverse(int i){
// 标记第i个节点已遍历
visited[i] = true;
// 打印当前遍历的节点
visit(i);
// 遍历邻接矩阵中第i个节点的直接联通关系
for(int j=0;j<vexnum;j++){
// 目标节点与当前节点直接联通,并且该节点还没有被访问,递归
if(arcs[i][j]==1 && visited[j]==false){
traverse(j);
}
}
}
// 图的深度优先遍历(递归)
public void DFSTraverse(){
// 初始化节点遍历标记
for (int i = 0; i < vexnum; i++) {
visited[i] = false;
}
// 从没有被遍历的节点开始深度遍历
for(int i=0;i<vexnum;i++){
if(visited[i]==false){
// 若是连通图,只会执行一次
traverse(i);
}
}
}
// 图的深度优先遍历(非递归)
public void DFSTraverse2(){
// 初始化节点遍历标记
for (int i = 0; i < vexnum; i++) {
visited[i] = false;
}
Stack<Integer> s = new Stack<Integer>();
for(int i=0;i<vexnum;i++){
if(!visited[i]){
//连通子图起始节点
s.add(i);
do{
// 出栈
int curr = s.pop();
// 如果该节点还没有被遍历,则遍历该节点并将子节点入栈
if(visited[curr]==false){
// 遍历并打印
visit(curr);
visited[curr] = true;
// 没遍历的子节点入栈
for(int j=vexnum-1; j>=0 ; j-- ){
if(arcs[curr][j]==1 && visited[j]==false){
s.add(j);
}
}
}
}while(!s.isEmpty());
}
}
}
广度优先搜索(BFS):
public class AMWGraph {
private ArrayList vertexList;//存储点的链表
private int[][] edges;//邻接矩阵,用来存储边
private int numOfEdges;//边的数目
public AMWGraph(int n) {
//初始化矩阵,一维数组,和边的数目
edges=new int[n][n];
vertexList=new ArrayList(n);
numOfEdges=0;
}
//得到结点的个数
public int getNumOfVertex() {
return vertexList.size();
}
//得到边的数目
public int getNumOfEdges() {
return numOfEdges;
}
//返回结点i的数据
public Object getValueByIndex(int i) {
return vertexList.get(i);
}
//返回v1,v2的权值
public int getWeight(int v1,int v2) {
return edges[v1][v2];
}
//插入结点
public void insertVertex(Object vertex) {
vertexList.add(vertexList.size(),vertex);
}
//插入边
public void insertEdge(int v1,int v2,int weight) {
edges[v1][v2]=weight;
numOfEdges++;
}
//删除结点
public void deleteEdge(int v1,int v2) {
edges[v1][v2]=0;
numOfEdges--;
}
//得到第一个邻接结点的下标
public int getFirstNeighbor(int index) {
for(int j=0;j<vertexList.size();j++) {
if (edges[index][j]>0) {
return j;
}
}
return -1;
}
//根据前一个邻接结点的下标来取得下一个邻接结点
public int getNextNeighbor(int v1,int v2) {
for (int j=v2+1;j<vertexList.size();j++) {
if (edges[v1][j]>0) {
return j;
}
}
return -1;
}
//私有函数,深度优先遍历
private void depthFirstSearch(boolean[] isVisited,int i) {
//首先访问该结点,在控制台打印出来
System.out.print(getValueByIndex(i)+" ");
//置该结点为已访问
isVisited[i]=true;
int w=getFirstNeighbor(i);//
while (w!=-1) {
if (!isVisited[w]) {
depthFirstSearch(isVisited,w);
}
w=getNextNeighbor(i, w);
}
}
//对外公开函数,深度优先遍历,与其同名私有函数属于方法重载
public void depthFirstSearch() {
for(int i=0;i<getNumOfVertex();i++) {
//因为对于非连通图来说,并不是通过一个结点就一定可以遍历所有结点的。
if (!isVisited[i]) {
depthFirstSearch(isVisited,i);
}
}
}
//私有函数,广度优先遍历
private void broadFirstSearch(boolean[] isVisited,int i) {
int u,w;
LinkedList queue=new LinkedList();
//访问结点i
System.out.print(getValueByIndex(i)+" ");
isVisited[i]=true;
//结点入队列
queue.addlast(i);
while (!queue.isEmpty()) {
u=((Integer)queue.removeFirst()).intValue();
w=getFirstNeighbor(u);
while(w!=-1) {
if(!isVisited[w]) {
//访问该结点
System.out.print(getValueByIndex(w)+" ");
//标记已被访问
isVisited[w]=true;
//入队列
queue.addLast(w);
}
//寻找下一个邻接结点
w=getNextNeighbor(u, w);
}
}
}
//对外公开函数,广度优先遍历
public void broadFirstSearch() {
for(int i=0;i<getNumOfVertex();i++) {
if(!isVisited[i]) {
broadFirstSearch(isVisited, i);
}
}
}
}
拓扑排序:
寻找一个入度为0的顶点,该顶点是拓扑排序中的第一个顶点序列,将之标记删除,然后将与该顶点相邻接的顶点的入度减1,再继续寻找入度为0的顶点,直至所有的顶点都已经标记删除或者图中有环。从上可以看出,关键是寻找入度为0的顶点。
先将入度为0的顶点放在栈或者队列中。当队列不空时,删除一个顶点v,然后更新与顶点v邻接的顶点的入度。只要有一个顶点的入度降为0,则将之入队列。此时,拓扑排序就是顶点出队的顺序。该算法的时间复杂度为O(V+E).
第一步:遍历图中所有的顶点,将入度为0的顶点 入队列。
第二步:从队列中出一个顶点,打印顶点,更新该顶点的邻接点的入度(减1),如果邻接点的入度减1之后变成了0,则将该邻接点入队列。
第三步:一直执行上面 第二步,直到队列为空。
最后结果可能不同,因为拓扑排序有多解
public ArrayList<Integer> topo() {
ArrayList<Integer> result = new ArrayList<>();
Queue<Integer> que = new LinkedList<>();
int[] ind = new int[V];
for(int i=0;i<V;i++)
ind[i] = 0;
for(LinkedList<Integer> nodes:adj)
for(int x:nodes)
ind[x]++; for(int i=0;i<V;i++)
if(ind[i]==0)
que.offer(i);
while(!que.isEmpty()){
int k = que.poll();
result.add(k);
for(int x:adj(k)) {
ind[x]--;
if(ind[x]==0)
que.offer(x);
}
}
return result;
}
定义: 拓扑排序是对有向无环图(DAG)的顶点的一种排序, 使得如果存在一条从v到w的路径,那么在排序中w就出现在v的后面。
如果图含有环,那么拓扑排序是不可能的。试想有3个正整数,a比b大,b比c大,c比a大,我们无法对abc排序
重构二叉树
输入某二叉树的前序遍历和中序遍历的结果,请重建出该二叉树。假设输入的前序遍历和中序遍历的结果中都不含重复的数字。例如输入前序遍历序列{1,2,4,7,3,5,6,8}和中序遍历序列{4,7,2,1,5,3,8,6},则重建二叉树并返回。
/*class TreeNode
{
int val;
TreeNode left;
TreeNode right;
TreeNode(int x) { val = x; }
}
*/
public class BulidTree
{
public TreeNode reConstructBinaryTree(int [] pre,int [] in)
{
if(pre == null || in == null || pre.length != in.length )//如果先序或者中序数组有一个为空的话,就无法建树,返回为空
return null;
else
{
return reBulidTree(pre,0,pre.length-1,in,0,in.length-1);
}
}
private TreeNode reBulidTree(int[] pre,int startPre,int endPre,int[] in,int startIn,int endIn)
{
if(startPre > endPre || startIn > endIn)//先对传的参数进行检查判断
return null;
int root = pre[startPre];//数组的开始位置的元素是跟元素
int locateRoot = locate(root,in,startIn,endIn);//得到根节点在中序数组中的位置 左子树的中序和右子树的中序以根节点位置为界
if(locateRoot == -1) //在中序数组中没有找到跟节点,则返回空
return null;
TreeNode treeRoot = new TreeNode(root);//创建树根节点
treeRoot.left = reBulidTree(pre,startPre + 1,startPre + locateRoot - startIn,in,startIn,locateRoot-1);//递归构建左子树
treeRoot.right = reBulidTree(pre,startPre+locateRoot-startIn+1,endPre,in,locateRoot+1,endIn);//递归构建右子树
return treeRoot;
}
//找到根节点在中序数组中的位置,根节点之前的是左子树的中序数组,根节点之后的是右子树的中序数组
private int locate(int root,int[] in,int startIn,int endIn)
{
for (int i = startIn; i < endIn; i++)
{
if(root == in[i])
return i;
}
return -1;
}
}
图的最短路径算法
问题:从某顶点出发,沿图的边到达另一顶点所经过的路径中,各边上权值之和最小的一条路径(最短路径)。解决最短路径有以下算法,Dijkstra算法,Bellman-Ford算法,Floyd算法和SPFA算法,另外还有启发式搜索算法A*。
Dijkstra(迪杰斯特拉)算法(贪心、动态规划)
典型的单源最短路径算法,用于计算从某个源点到其他所有节点的最短路径。主要特点是以起始点为中心向外层层扩展,直到扩展到终点为止。
问题:Dijkstar能否处理负权边?(来自《图论》)
答案:不能,这与贪心选择性质有关,每次都找一个距源点最近的点(dmin),然后将该距离定为这个点到源点的最短路径;但如果存在负权边,那就有可能先通过并不是距源点最近的一个次优点(dmin'),再通过这个负权边L(L<0),使得路径之和更小(dmin'+L<dmin),则dmin'+L成为最短路径,并不是dmin,这样dijkstra就被囧掉了。
(1) Dijkstra算法按路径长度递增次序产生最短路径。先把V分成两组:
S:已求出最短路径的顶点的集合
V-S=T:尚未确定最短路径的顶点集合
将T中顶点按最短路径递增的次序加入到S中,依据V0到T中顶点Vk的最短路径,或是从V0到Vk的直接路径或是从V0经S中顶点到Vk的路径权值之和。
- 求最短路径步骤
初使时令S={V0},T={其余顶点},T中顶点对应的距离值, 若存在<V0,Vi>,为<V0,Vi>弧上的权值,若不存在<V0,Vi>,为Inf。从T中选取一个其距离值为最小的顶点W(贪心),加入S,对T中顶点的距离值进行修改:若加进W作中间顶点,从V0到Vi的距离值比不加W的路径要短,则修改此距离值。重复上述步骤,直到S中包含所有顶点,即S=V为止。
Floyd算法(Floyd-Warshall algorithm)(动态规划)
又称为弗洛伊德算法、插点法,是解决给定的加权图中顶点间的最短路径的一种算法,计算每一对顶点之间的最短路径,可以正确处理有向图或负权的最短路径问题,同时也被用于计算有向图的传递闭包。
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的最短路径的距离。
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++) {//对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];
}
}
}
}
}
Bellman-Ford算法
为了能够求解边上带有负值的单源最短路径问题,Bellman(贝尔曼,动态规划提出者)和Ford(福特)提出了从源点逐次绕过其他顶点,以缩短到达终点的最短路径长度的方法。 Bellman-ford算法是求含负权图的单源最短路径算法,效率很低,但代码很容易写。即进行不停地松弛,每次松弛把每条边都更新一下,若n-1次松弛后还能更新,则说明图中有负环,无法得出结果,否则就成功完成。Bellman-ford算法有一个小优化:每次松弛先设一个flag,初值为FALSE,若有边更新则赋值为TRUE,最终如果还是FALSE则直接成功退出。Bellman-ford算法浪费了许多时间做无必要的松弛,所以SPFA算法用队列进行了优化,效果十分显著,高效难以想象。SPFA还有SLF,LLL,滚动数组等优化。
递推公式(求顶点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算法结束才确定下来。
SPFA算法
用一个队列来进行维护。初始时将源加入队列。每次从队列中取出一个元素,并对所有与他相邻的点进行松弛,若某个相邻的点松弛成功,则将其入队。直到队列为空时算法结束;这个算法,简单的说就是队列优化的bellman-ford,利用了每个点不会更新次数太多的特点发明的此算法。