[数据结构与算法-iOS 实现]图的实现及搜索排序附demo

基础知识

图由顶点(vertex)和边(edge)组成,通常表示为G=(V,E)

顶点集 V 又穷且非空

任意两个顶点之间,都可以用边来表示他们的关系,边集 E 可以为空

有向图

有向图的边是有明确方向的

有向无环图(DAG)

如果一个有向图,从任意顶点出发,无法经过若干点边回到该顶点,那么就为有向无环图

出度 入度

出度和入度适用于有向图

出度:一个顶点的出度为 X,指的是有 X 条边以该顶点为起点

入度:一个顶点的入度为 X,指的是有 X 条边以该顶点为终点

无向图

无向图的边是没有方向的

混合图

混合图的边可以是有方向的,也可以是无方向的

平行边

在无向图中,关联一对顶点的无向边如果大于一条,则称这些边为平行边

在有向图中,关联一对顶点的有向边数量如果大于一,并且他们的方向是相同的,那么这些边为平行边

多重图

有平行边或者有自环的图

简单图

既没有平行边也没有自环的图

无向完全图

无向完全图的任意两个顶点之间都存在边

n 个顶点的无向完全图有n(n-1)/2条边

有向完全图

有向完全图的任意两个顶点之间都存在方向相反的两条边
n个顶点的有向完全图,有n(n-1)条边

有权图

有权图的边可以拥有权值

连通图

如果顶点 x 和 y 之间存在可以相互抵达的路径(直接或者间接的路径),则 x 和 y 是连通的

如果无向图G中,任意两个顶点都是连通的,则G为连通图

连通分量

连通分量:无向图中的极大连通子图

连通图只有一个连通分量,是它本身。

非连通的无向图有多个连通分量

强连通图

如果有向图G中,任意两个顶点都是连通的,则为强连通图

强连通分量

强连通分量:有向图的极大强连通子图

强连通图只有一个强连通分量,他本身,非强连通的有向图有多个强连通分量

图的遍历

从图中某一顶点出发访问图中其余顶点,且每个顶点仅被访问一次

广度优先搜索(Breadth First Search,BFS),又称宽度优先搜索,横向优先搜索

如之前的二叉树遍历,就是广度优先搜索,一层一层遍历

深度优先搜索(Depth First Search,DFS)

二叉树的前序遍历,就相当于深度优先搜索,根左右

拓扑排序

代码

接口:

//
//  SCXGraph.h
//  TestArithmetic
//
//  Created by 孙承秀 on 2020/8/2.
//  Copyright © 2020 孙承秀. All rights reserved.
//

#import <Foundation/Foundation.h>

NS_ASSUME_NONNULL_BEGIN

/// V:顶点 E:边
@interface SCXGraph<V,E> : NSObject

/// 顶点数
- (NSUInteger)verticesSize;

/// 边的个数
- (NSUInteger)edgesSize;

/// 添加顶点
/// @param v 顶点
- (void)addVertex:(V)v;

/// 添加边
/// @param from 边的起点
/// @param to 边的终点
- (void)addEdge:(V)from to:(V)to;

/// 添加边,带有权重
/// @param from 边的起点
/// @param to 边的终点
/// @param weight 权值
- (void)addEdge:(V)from to:(V)to weigth:(E)weight;

/// 移除顶点
/// @param v 要移除的顶点
- (void)removeVertex:(V)v;

/// 移除一条边
/// @param from 边的起点
/// @param to 边的终点
- (void)removeEdge:(V)from to:(V)to;

/// 打印图
- (void)printGraph;

/// 广度优先搜索
/// @param begin 遍历的起始节点
- (void)BFS:(V)begin;

/// 深度优先搜索
/// @param begin 遍历的起始节点
- (void)DFS:(V)begin;


/// 拓扑排序
- (NSArray *)topologicalSort;
@end

NS_ASSUME_NONNULL_END

实现:

//
//  SCXListGraph.m
//  TestArithmetic
//
//  Created by 孙承秀 on 2020/8/2.
//  Copyright © 2020 孙承秀. All rights reserved.
//

#import "SCXListGraph.h"
#import "SCXCircleArrayQueue.h"
#import "SCXStack.h"
#import "SCXCircleArrayQueue.h"
#pragma mark - vertex
@class SCXGraphEdge;
/// 图的顶点:V->SCXGraphVertex
@interface SCXGraphVertex<V,E> : NSObject<NSCopying>

/// 顶点的值
@property (nonatomic,strong) V vertex;

/// 所有以该顶点为终点的边,入度的边
@property (nonatomic , strong)NSHashTable<SCXGraphEdge *> *inEdges;

/// 所有以该顶点为起点的边,出度的边
@property (nonatomic , strong)NSHashTable<SCXGraphEdge *> *outEdges;

- (instancetype)initWithVertex:(V)vertex;

@end

@implementation SCXGraphVertex
// 这样就可以这个对象当做key放到字典中了。
- (id)copyWithZone:(NSZone *)zone {
    SCXGraphVertex *ver = [[SCXGraphVertex alloc] init];
//    ver.inEdges = self.inEdges;
//    ver.outEdges = self.outEdges;
    ver.vertex = self.vertex;
    return ver;
}
- (instancetype)initWithVertex:(id)vertex{
    if (self = [super init]) {
        self.vertex = vertex;
        self.inEdges = [NSHashTable hashTableWithOptions:(NSPointerFunctionsStrongMemory)];
        self.outEdges = [NSHashTable hashTableWithOptions:NSPointerFunctionsStrongMemory];
    }
    return self;
}
/// 顶点的值相等就认为是一个顶点
- (BOOL)isEqual:(id)object{
    SCXGraphVertex *vertex = (SCXGraphVertex *)object;
    return [vertex.vertex isEqual:self.vertex];
}
- (NSUInteger)hash{
    return [super hash];
}
- (NSString *)description{
    return self.vertex;
}
@end



#pragma mark - edge

/// 图的边:E->SCXGraphEdge
@interface SCXGraphEdge<V,E> : NSObject
// 边的权重
@property (nonatomic , strong) E weigth;
// 边的起点
@property (nonatomic , strong) SCXGraphVertex<V , E> *from;

/// 边的终点
@property (nonatomic , strong) SCXGraphVertex<V , E> *to;

@end

@implementation SCXGraphEdge

- (instancetype)initWithFrom:(id)from to:(id)to{
    if (self = [super init]) {
        self.from = from;
        self.to = to;
    }
    return self;
}

/// 边的起点和终点相等,就认为是同一条边
- (BOOL)isEqual:(id)object{
    SCXGraphEdge *edge = (SCXGraphEdge *)object;
    return [self.from isEqual:edge.from] && [self.to isEqual:edge.to];
}
- (NSUInteger)hash{
    return self.from.hash ^ self.to.hash;
}
- (NSString *)description{
    return [NSString stringWithFormat:@"from:%@,to:%@,weigth:%@",self.from,self.to,self.weigth];
}
@end

#pragma mark - graph
@interface SCXListGraph ()

/// 所有顶点的集合,v->SCXGraphVertex,一个顶点对应一个对象
@property (nonatomic , strong) NSMapTable<id , SCXGraphVertex *> *vertices;

/// 存放所有的边
@property (nonatomic , strong) NSHashTable<SCXGraphEdge *> *edges;
@end
@implementation SCXListGraph
- (instancetype)init{
    if (self = [super init]) {
        self.vertices = [NSMapTable mapTableWithKeyOptions:(NSPointerFunctionsStrongMemory) valueOptions:(NSPointerFunctionsStrongMemory)];
        self.edges = [NSHashTable hashTableWithOptions:NSPointerFunctionsStrongMemory];
    }
    return self;;
}
- (NSUInteger)verticesSize{
    return self.vertices.count;
}
- (NSUInteger)edgesSize{
    return self.edges.count;
}

/// 添加顶点,将顶点全局保存一份,顶点的值为key,value为SCXGraphVertex对象
/// @param v 顶点的值
- (void)addVertex:(id)v{
    if (!v) {
        return;
    }
    if ([self.vertices objectForKey:v]) {
        return;
    }
    [self.vertices setObject:[[SCXGraphVertex alloc] initWithVertex:v] forKey:v];
}

/// 添加边
/// @param from 边的起点
/// @param to 边的终点
- (void)addEdge:(id)from to:(id)to{
    [self addEdge:from to:to weigth:[NSNumber numberWithInteger:NSIntegerMax]];
}
- (void)addEdge:(id)from to:(id)to weigth:(id)weight{
    if (!from || !to) {
        return;
    }
    // 找到之前存在的起点和终点
    SCXGraphVertex *fromVeretex = [self.vertices objectForKey:from];
    SCXGraphVertex *toVeretex = [self.vertices objectForKey:to];
    if (!fromVeretex) {
        fromVeretex = [[SCXGraphVertex alloc] initWithVertex:from];
        [self.vertices setObject:fromVeretex forKey:from];
    }
    if (!toVeretex) {
        toVeretex = [[SCXGraphVertex alloc] initWithVertex:to];
        [self.vertices setObject:toVeretex forKey:to];
    }
    
    // 先找边,将之前删除,有可能有边,但是权值不同
    SCXGraphEdge *edge = [[SCXGraphEdge alloc] initWithFrom:fromVeretex to:toVeretex];
    edge.weigth = weight;
    // 调用 isEqual 判断边是否相等。
    // 将原来的边删除,将新的边加进去
    // 出
    if ([fromVeretex.outEdges containsObject:edge]) {
        [fromVeretex.outEdges removeObject:edge];
    }
    [fromVeretex.outEdges addObject:edge];
    
    // 入
    if ([toVeretex.inEdges containsObject:edge]) {
        [toVeretex.inEdges removeObject:edge];
    }
    [toVeretex.inEdges addObject:edge];
    
    // 总
    if ([self.edges containsObject:edge]) {
        [self.edges removeObject:edge];
    }
    [self.edges addObject:edge];
}
- (void)removeVertex:(id)v{
    // 先从所有顶点缓存中删除这个顶点
    SCXGraphVertex *vertex = [self.vertices objectForKey:v];
    if (!vertex) {
        return;
    }
    
    // 删除和这个顶点有关的边
    // 先删除以这个顶点为起点的边
    NSEnumerator *outObjEnumerator = vertex.outEdges.objectEnumerator;
    SCXGraphEdge *outEdge;
    while ((outEdge = outObjEnumerator.nextObject) != nil) {
        // 取出每一条边
        // 取出这条边之后,将这条边的终点的顶点,到这个终点的顶点的边删除
        [outEdge.to.inEdges removeObject:outEdge];
        [self.edges removeObject:outEdge];
    }
    [vertex.outEdges removeAllObjects];
    // 先删除以这个顶点为终点的边
    NSEnumerator *inObjEnumerator = vertex.inEdges.objectEnumerator;
    SCXGraphEdge *inEdge;
    while ((inEdge = inObjEnumerator.nextObject) != nil) {
        // 取出每一条边
        // 取出这条边之后,将这条边的终点的顶点,到这个终点的顶点的边删除
        [inEdge.from.outEdges removeObject:inEdge];
        [self.edges removeObject:inEdge];
    }
    [vertex.inEdges removeAllObjects];
    [self.vertices removeObjectForKey:v];
}

/// 移除一条边
/// @param from 边的起点
/// @param to 边的终点
- (void)removeEdge:(id)from to:(id)to{
    // 找到起点和终点对应的顶点值
    SCXGraphVertex *fromVertex = [self.vertices objectForKey:from];
    SCXGraphVertex *toVertex = [self.vertices objectForKey:to];
    if (!fromVertex || !toVertex) {
        return;
    }
    
    // 根据这两个顶点构建一条边
    // 边的起点和终点相等,就认为是同一条边
    SCXGraphEdge *edge = [[SCXGraphEdge alloc] initWithFrom:from to:to];
    if ([fromVertex.outEdges containsObject:edge]) {
        [fromVertex.outEdges removeObject:edge];
    }
    if ([toVertex.inEdges containsObject:edge]) {
        [toVertex.inEdges removeObject:edge];
    }
    if ([self.edges containsObject:edge]) {
        [self.edges removeObject:edge];
    }
}
- (void)BFS:(id)begin{
    // 广度优先搜索,就和之前的二叉树遍历一样,利用队列,先将一个顶点入队,然后将这个顶点出对,然后将这个顶点相关联的子节点,这里是以这个顶点为起点的边,也就是outEdges里面的边,依次再入队,然后这样循环下去
    SCXGraphVertex *beginVertex = [self.vertices objectForKey:begin];
    if (!beginVertex) {
        return;
    }
    // 创建队列
    SCXCircleArrayQueue *queue = [SCXCircleArrayQueue arrayQueue];
    // 入队
    [queue enqueue:beginVertex];
    // 保存已经访问过的顶点
    NSMutableArray *visitedVertex = [NSMutableArray array];
    [visitedVertex addObject:beginVertex];
    
    while (!queue.isEmpty) {
        // 出对一个
        SCXGraphVertex *v = queue.dequeue;
        NSLog(@"%@",v.vertex);
        
        // 将跟这个顶点有关的边入队
        for (SCXGraphEdge *edge in v.outEdges) {
            if ([visitedVertex containsObject:edge.to]) {
                continue;
            }
            [queue enqueue:edge.to];
            [visitedVertex addObject:edge.to];
        }
    }
}
// 深度优先搜索,利用递归
- (void)DFS:(id)begin{
    SCXGraphVertex *beginVertex = [self.vertices objectForKey:begin];
    // 防止重复访问
    NSMutableArray *visitedVertices = [NSMutableArray array];
    if (!beginVertex) {
        return;
    }
    //    [self DFSrecursion:beginVertex visitedVertices:visitedVertices];
    [self DFSStack:beginVertex visitedVertices:visitedVertices];
}
/// 利用递归来实现
- (void)DFSrecursion:(SCXGraphVertex *)vertex visitedVertices:(NSMutableArray *)visited{
    // 沿着根节点,一直向下找,直到,到底,到底了之后,一次向上回退,然后再找另一个条路径,也就相当于左右节点路径,这里是相当于每一条边,利用递归可以做到,一直向下找,然后回退
    NSLog(@"%@",vertex.vertex);
    [visited addObject:vertex];
    for (SCXGraphEdge *edge in vertex.outEdges) {
        if ([visited containsObject:edge.to]) {
            continue;
        }
        [self DFSrecursion:edge.to visitedVertices:visited];
    }
}
/// 利用栈来实现
- (void)DFSStack:(SCXGraphVertex *)veretex visitedVertices:(NSMutableArray *)visited{
    // 然后将当前节点添加到已经访问的集合中去
    [visited addObject:veretex];
    // 创建栈
    SCXStack *stack = [[SCXStack alloc] init];
    // 入栈
    [stack push:veretex];
    // 将第一个节点打印
    NSLog(@"%@",veretex.vertex);
    
    while (!stack.isEmpty) {
        // 出栈
        SCXGraphVertex *cur = [stack pop];
        // 将出栈的这个顶点的from 和 to 入栈,from 入栈的原因是
        // 访问到头了的时候,回退,当回退到这个顶点的时候,访问这个顶点的其余路径
        // 判断是否访问过to,如果访问过,那么这条路径就访问过,如果没有访问过就访问to
        // 从当前节点依次找每一条路径,找到一条路径就break,沿着to继续向下找
        for (SCXGraphEdge *edge in cur.outEdges) {
            if ([visited containsObject:edge.to]) {
                continue;
            }
            // from 入栈
            [stack push:edge.from];
            // to 入栈
            [stack push:edge.to];
            // 将to添加到访问过得节点中
            [visited addObject:edge.to];
            // 打印to
            NSLog(@"%@",edge.to.vertex);
            // 退出这条路径,继续沿着to访问
            break;
        }
    }
}
- (void)printGraph{
    NSEnumerator *enumerator = self.vertices.keyEnumerator ;
    id obj;
    NSLog(@"【顶点-------】");
    while ((obj = enumerator.nextObject) != nil) {
        SCXGraphVertex *v = [self.vertices objectForKey:obj];
        NSLog(@"key:%@",obj);
        NSLog(@"outEdges:%@",v.outEdges);
        NSLog(@"inEdgesL%@",v.inEdges);
    }
    NSLog(@"【边-------】");
    NSEnumerator *edgeEnumerator = self.edges.objectEnumerator;
    SCXGraphEdge *edge ;
    while ((edge = edgeEnumerator.nextObject) != nil) {
        NSLog(@"%@",edge);
    }
}

/// 拓扑排序
/*
 1. 每个顶点只出现依次
 2. 没有一个节点指向他节点的前面
 
 
 1. 先遍历所有的节点,将入度为0的节点,放在q1队列里面,度不为0的节点,我们放到一个map里面,key为这个节点,value为这个节点的入度
 2. 从队列q1中取出一个节点,如果队列不为空,一直循环取
 3. 从上面队列中取出一个度为0的节点后,放到我们最终要的结果数组里面arr1
 4. 拿到所有跟这个顶点有关的边,将这些变得终点的入度,减去1,这些顶点其实是都放在上面的map中
 5. 重复2,3,4
 */
- (NSArray *)topologicalSort{
    // 存放所有度为0的节点
    SCXCircleArrayQueue *queue = [SCXCircleArrayQueue arrayQueue];
    // 存放除了度为0的节点之外,其余的所有节点,key为这个节点,value为这个节点的入度
    //    NSMapTable *map = [NSMapTable mapTableWithKeyOptions:(NSPointerFunctionsStrongMemory) valueOptions:NSPointerFunctionsStrongMemory];
    NSMutableDictionary *map = [NSMutableDictionary dictionary];
    // 存放最终的结果
    NSMutableArray *result = [NSMutableArray array];
    
    // 先找出所有的入度为0的节点
    NSEnumerator *enumerator = self.vertices.keyEnumerator;
    id key ;
    while ((key = enumerator.nextObject) != nil) {
        SCXGraphVertex *vertex = [self.vertices objectForKey:key];
        NSInteger size = vertex.inEdges.count;
        if (size == 0) {
            [queue enqueue:vertex];
        } else {
            NSNumber *num = [NSNumber numberWithInteger:size];
            [map setValue:num forKey:vertex.vertex];
        }
        
    }
    // 挨个从队列里面取出入度为0的节点,将它放到最终的结果数组里面去
    while (!queue.isEmpty) {
        SCXGraphVertex *zeroVertex = [queue dequeue];
        // 放到结果数组中去
        [result addObject:zeroVertex.vertex];
        
        // 将跟这个顶点有关的顶点的入度都减去1
        for (SCXGraphEdge *outEdge in zeroVertex.outEdges) {
            SCXGraphVertex *outVertext = outEdge.to;
            NSNumber *priSizeNum = [map objectForKey:outVertext.vertex];
            NSInteger outSize = priSizeNum.integerValue - 1;
            if (outSize == 0) {
                [queue enqueue:outVertext];
            } else {
                [map setValue:[NSNumber numberWithInteger:outSize] forKey:outVertext.vertex];
            }
        }
    }
    
    
    return result.copy;
}
@end


demo

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,126评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,254评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,445评论 0 341
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,185评论 1 278
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,178评论 5 371
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,970评论 1 284
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,276评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,927评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,400评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,883评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,997评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,646评论 4 322
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,213评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,204评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,423评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,423评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,722评论 2 345