输入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个), 无论采用哪种算法, 都会很快找到结果.所以效率也不是那么重要了.