两个链表的第一个公共节点, Masonry寻找2个view的公共父视图

输入2个链表, 找出他们的第一个公共节点.  


这两个链表是单向链表。如果两个单向链表有公共的结点,那么这两个链表从某一结点开始,它们的next都指向同一个结点。但由于是单向链表的结点,每个结点只有一个 next,因此从第一个公共结点开始,之后它们所有结点都是重合的,不可能再出现分叉。所以两个有公共结点而部分重合的链表,拓扑形状看起来像一个Y,而不可能像ⅹ。

先用最直观的暴力法, 遍历第一个链表, 把第一个链表的每个元素依次拿出来, 与第二个链表中的元素的元素进行比对, 地址相等就说明找到了.  Masonry就使用了这种方式. 时间复杂度是O(MN).

- (void)viewDidLoad {
    [super viewDidLoad];
    
    [self findCommonNode];
    
}

// 找到2个链表的第一个公共节点
- (void)findCommonNode {
    
    Node * first = [Node nodeWithArray:@[@(1),@(2),@(3),@(4),@(5),] ];
    Node * second = [Node nodeWithArray:@[@(24),@(33)] ];

    Node * commonNode = [Node nodeWithArray:@[@(100),@(101),@(102)] ];

    
    // 找到first和second的最后一个节点, 把最后一个节点的next指向commonNode
    Node * temp = first;
    while (temp.next) {
        temp = temp.next;
    }
    temp.next = commonNode;
    
    temp = second;
    while (temp.next) {
        temp = temp.next;
    }
    temp.next = commonNode;
    
    NSLog(@"first : %@",first);
    NSLog(@"second : %@",second);

    // 数据准备完成, 开始算法
    [self __findCommonNode:first secondNode:second];
    [self __findCommonNodeStack:first secondNode:second];
    [self __findCommonNodeHash:first secondNode:second];
    [self __findCommonNodeStep:first secondNode:second];
    [self __findCommonNodeStep2:first secondNode:second];
}

// 暴力法O(MN)
- (void)__findCommonNode:(Node *)firstNode secondNode:(Node *)second {
    if (firstNode==nil || second==nil) {
        return;
    }
    
    // 遍历第一个链表
    while (firstNode) {

        Node * secondHeader = second;
        while (secondHeader) {

            if (secondHeader == firstNode) {
                NSLog(@"暴力法 找到了, 是%p %d",secondHeader,secondHeader.data);
                return ;
            }
            secondHeader = secondHeader.next;
        }
        firstNode = firstNode.next;
    }

    NSLog(@"暴力法 没找到");

    
}

在Masonry中, 寻找2个子view的公共父视图也是采用了这种方法,view.superView就相当于node.next,  2个循环搞定.


通过上面的图可以看到,  如果没有公共节点, 最后一个肯定不是一样的;  但是如果有公共节点,最后一个肯定是其中的一个公共节点,  但是不太能知道是不是第一个, 还需要往前找才可以, 但是链表是不能反向找的, 需要借助栈结构, 后进先出就可以. 

遍历链表, 加入到一个栈结构中, 然后从栈顶pop, 找到第一个不一致的节点 , 第一个不一致节点.next就是结果了.

// 辅助栈,O(M+N)
- (void)__findCommonNodeStack:(Node *)firstNode secondNode:(Node *)second {
    if (firstNode==nil || second==nil) {
        return;
    }
    
    // 用数组模拟栈
    NSMutableArray * firstArray = [NSMutableArray array];
    NSMutableArray * secondArray = [NSMutableArray array];

    // 分别加入到栈中
    while (firstNode) {
        [firstArray addObject:firstNode];
        firstNode = firstNode.next;
    }
    while (second) {
        [secondArray addObject:second];
        second = second.next;
    }
    
    Node * resultNode = nil;
    // 如果最后一个节点都不相等, 那么这个肯定就没有相等的节点
    while (firstArray.lastObject == secondArray.lastObject && firstArray.count>0 && secondArray.count>0) {
        resultNode = firstArray.lastObject;
        [firstArray removeLastObject];
        [secondArray removeLastObject];
    }
    
    if (resultNode) {
        NSLog(@"辅助栈 找到了, 是%p %d",resultNode,resultNode.data);
        return;
    }
    
    NSLog(@"辅助栈 没找到相等的节点");
}

这个思路是是对暴力法的改进, 借助hash算法, 判断一个集合中是否包含一个元素, 如果集合是数组, 那需要遍历数组,是O(n), 但是如果是字典或者NSSet, 因为系统帮我们实现了hash, 只需要O(1)的时间就可以知道是否包含了. 

遍历其中一个链表, 把链表元素加入到set中,  然后遍历第二个链表,  依次判断set中是否包含第二个链表的元素. 总共的时间复杂度是O(m+n), 需要额外的空间O(n)

// 哈希表/Set,O(M+N)
- (void)__findCommonNodeHash:(Node *)firstNode secondNode:(Node *)secondNode {
    if (firstNode==nil || secondNode==nil) {
        return;
    }
    
    NSMutableSet * set = [NSMutableSet set];
    while (firstNode) {
        [set addObject:firstNode];
        firstNode = firstNode.next;
    }
    
    
    while (secondNode) {
        
        if ([set containsObject:secondNode]) {
            NSLog(@"hash法 找到了, 是%p %d",secondNode,secondNode.data);
            return;
        }
        
        secondNode = secondNode.next;
    }
    
    NSLog(@"hash法 没找到");
    
}

到这里就结束了吗? 不, 还有更好的. 

首先遍历两个链表得到它们的长度,就能知道哪个链表比较长,以及长的链表比短的链表多几个结点。在第二次遍历的时候,在较长的链表上先走若干步,接着再同时在两个链表上遍历,找到的第一个相同的结点就是它们的第一个公共结点。如果有公共节点, 他们一定会同时到达那个公共节点.

// 同步法,O(M+N)
- (void)__findCommonNodeStep:(Node *)firstNode secondNode:(Node *)secondNode {
    if (firstNode==nil || secondNode==nil) {
        return;
    }
    
    Node * tempNode = firstNode;
    int firstCount = 0;
    int secondCount = 0;

    // 分别统计有几个节点, 然后节点多的先走,
    while (tempNode) {
        firstCount ++;
        tempNode = tempNode.next;
    }
    tempNode = secondNode;
    while (tempNode) {
        secondCount++;
        tempNode = tempNode.next;
    }
    
    // 根据长度进行操作了, 先假设第一个链表的节点长
    Node * longNode = firstNode;
    Node * shortNode = secondNode;
    
    int longCount = firstCount;
    int shortCount = secondCount;
    
    if (firstCount<secondCount) {
        longNode = secondNode;
        shortNode = firstNode;
        
        longCount = secondCount;
        shortCount = firstCount;
    }
    
    
    // longNode先走几步
    while (longCount > shortCount) {
        longNode = longNode.next;
        longCount -- ;
    }
    
    while (longNode) {
        
        if (longNode == shortNode) {
            NSLog(@"同步法 找到了, 是%p %d",longNode,longNode.data);
            return ;
        }
        
        longNode = longNode.next;
        shortNode = shortNode.next;

    }
    
    NSLog(@"同步法 没找到");
    
}

效率是挺高的, 但是代码量有点大, 虽然思路比较清晰, 但就是看着累.   emmm, 没关系, 还有更给力的.

我们使用两个指针 h1 ,h2 分别指向两个链表 first,second 的头结点,然后同时分别逐结点遍历,当 h1 到达链表 first 的末尾时,重新定位到链表 second 的头结点;当 h2 到达链表 second 的末尾时,重新定位到链表 first 的头结点。

这样,当它们相遇时,所指向的结点就是第一个公共结点。 这样不要好理解啊, 先来一个图.

假设first独享的链表长度是a, second独享的链表长度是b, 公共部分长度为c.

那么a+c+b==b+c+a,  而他们第一次相遇的点肯定是c的第一个节点. 

h1走的路径是 :  1->2->3->6->7->4->5->6->7->null

h2走的路径是:   4->5->6->7->1->2->3->6->7->null

// 最优解了, 时间复杂度O(m+n),空间负责度O(1)
- (void)__findCommonNodeStep2:(Node *)firstNode secondNode:(Node *)secondNode {
    
    Node * h1 = firstNode;
    Node * h2 = secondNode;
    
    while (h1 != h2) {
        
        if (h1 == nil) {
            h1 = secondNode;
        } else {
            h1 = h1.next;
        }
        
        if (h2 == nil) {
            h2 = firstNode;
        } else {
            h2 = h2.next;
        }
        
    }
    
    if (h1) {
        NSLog(@"同步法优化 找到了, 是%p %d",h1,h1.data);
        return;
    }
    NSLog(@"同步法优化 没找到");
}


综上所有算法, 暴力法是比较直观好想到的, 在使用NSet优化后也能达到O(m+n)的时间复杂度, 属于空间换时间的优化,  最优解法, 当然是同步法了, 采用了更高效的算法 , 同时时间复杂度也是最低的, 但是不太好想到.  

masonry采用了第一种方法, 一般情况下, view的superview不会有很多(不超过10个), 无论采用哪种算法, 都会很快找到结果.所以效率也不是那么重要了.

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值