转自:http://blog.csdn.net/goodluckwhh/article/details/8316357版权声明:本文为博主原创文章,未经博主允许不得转载。 目录(?)[-] 一 第一个问题如何判断单链表中是否存在循环并找出循环起点 方法一 方法二 方法三 二 第二个问题如何判断两个单链表是否交叉并找出交叉点 情形分析 情形一 情形二 情形三 解法 判断是否存在交叉 找出交叉点 关于单链表,常见的两个问题是 1.怎么判断一个单链表中是否存在循环,即出现如下情形 2.如何判断两个单链表是否交叉,即出现如下情形 一、 第一个问题,如何判断单链表中是否存在循环(并找出循环起点) 先来比较下遍历没有循环的单链表和遍历有循环的单链表时的区别: 遍历没有循环的单链表:所有结点均只出现一次 遍历包含循环的单链表:遍历进入死循环,一直重复循环的部分 很显然,如果在遍历中检测到一个结点被遍历了超过1次就可以认定该单链表中出现了循环,同时第一个重复出现的结点即是循环部分的起点。因此发现这个结点即是检测单链表是否出现循环的关键。 根据分析我们可以得到该问题的解法: 1. 方法一 可以对访问过的每个结点都做个标记,然后继续遍历单链表,如果遇到了已经做过标记的结点,就说明从这个结点开始出现了循环。该方法所需时间为:链表元素数目n + 1,因此算法复杂度为O(n)。 2. 方法二 很多时候由于各种限制,无法对已经访问过的结点做标记,那么我们可以考虑将遍历过的结点记录到某个地方(线性表、树等等,随你喜欢),在遍历每一个结点时,都先检查它是否已经出现在了我们的记录中,如果没有就将其加入记录中,然后继续遍历它的下一个结点。如果已经出现过了,则表明存在循环,并且该结点就是循环的起点。该方法所需时间为:(0+1+2+...+n-1 + 循环起点离链表头的距离(若不存在循环则为0)),因此算法复杂度为O(n2) 3. 方法三 如果情况再特殊些,内存空间有限,而单链表长度又可能很大,同时也没办法对结点做标记,那么前两种方法都会失效。怎么办呢,想象下,A和B去学校的体育场跑步,路线如图: 箭头出现的地方是体育场的400米场地的开始,A和B都从宿舍开始跑,然后在400米的场地跑很多圈,但是A的速度比B慢,他们一起出发,然后……显然在直线上A不可能追上B,当然在400圈上A也不可能在跑的距离上赶上B,但是他们肯定能碰到很多次。是不是有灵感了,对了,就是让俩速度不一样的指针在单链表上赛跑,如果它们美丽的邂逅了,那么单链表就存在循环^-^。 但是此时该怎么求循环的起点呢?注意到这实际上是一个追击问题,如果我们让一个指针fastp的速度为另一个指针slowp速度的2倍,并且记第一次两个指针相等时速度快的fastp走过的“路程”为s1,slowp走过的“路程”为s2,链表的节点数目为n,循环部分的长度为nloop,则必定有: s1 - s2 = nloop * N,N为大于等于1的正整数 假设此时fastp的前驱是prev,则将prev的后继指针设置为NULL就可以得到两个链表,一个是以原链表头为起点的链表,另一个是以cur为起点的链表,同时这两个链表存在交叉,现在需要求这个交叉点(它即是原链表的循环起点)。此时我们还知道以下信息: 交叉点离cur的距离不会超过s1-s2,因为从cur开始到交叉点的部分是属于原链表的循环部分的,所以其长度自然不会超过nloop 交叉点离原链表头的距离不会超过s2,因为slowp已经进入循环部分,因此它必定越过循环起点 采用判断单链表中是否存在交叉中的方法二,再结合以上两个信息即可得到一个较快的求出循环起点的方法。 再来看下从slowp走到循环起点后发生的事情: 假设循环起点即为链表起点,则很显然s2 = n时,二者“邂逅” 否则,假设此时fastp也走到了该点,则找到循环,此时s2 < n 否则此时fastp不在循环起点,则可以看作从此时开始的一个追击问题,由于fastp速度时slowp速度的2倍,所以fastp一定会在slowp走到尾结点之前追上它,所以 s2 < n 因此采用该方法时:s2 < n,同时因为fastp的速度是slowp的速度的2倍,所以有:s1 = 2 * s2或 s1 = 2 * s2 - 1; 该方法所需时间为 :s1 + s2 = 3 * s2 或3 * s2 - 1 < 3n,因此算法复杂度为O(n)。 使用该方法判断是否存在循环的代码: [cpp] view plain copy int is_loop_exist_v2(void *head) { void *fastp, *slowp; slowp = head; if (slowp == NULL || get_next_node(slowp) == NULL || get_next_node(get_next_node(slowp)) == NULL) { return 0; } fastp = get_next_node(get_next_node(slowp)); while (slowp != fastp) { slowp = get_next_node(slowp); if (get_next_node(fastp) == slowp) { return 1; } else if (get_next_node(fastp) == NULL || get_next_node(get_next_node(fastp)) == NULL) { return 0; } else { fastp = get_next_node(get_next_node(fastp)); } } return 1; } 使用该方法找出循环起点的代码: [cpp] view plain copy void *find_loop_node_v2(void *head, void **tail) { unsigned int slow, fast; void *fastp, *slowp, *ret; slowp = head; if (slowp == NULL || get_next_node(slowp) == NULL || get_next_node(get_next_node(slowp)) == NULL) { return NULL; } fastp = get_next_node(get_next_node(slowp)); slow = 1; fast = 2; while (slowp != fastp) { slowp = get_next_node(slowp); slow++; if (get_next_node(fastp) == slowp) { fast++; break; } else if (get_next_node(fastp)== NULL) { if (tail != NULL) { *tail = fastp; } return NULL; } else if (get_next_node(get_next_node(fastp)) == NULL) { if (tail != NULL) { *tail = get_next_node(fastp); } return NULL; } else { fastp = get_next_node(get_next_node(fastp)); fast += 2; } } ret = find_crossed_node_within_certain_steps_v1(head, fastp, slow, fast - slow); if (ret == NULL) { LOG_PRINT(LOG_ERR, "Error: maybe lack of memory or memory crush\r\n"); } return ret; } 上述三个方法都可以检测是否存在循环,也都给出了找到循环起点的方法,无论哪种方法,在寻找循环起点时,都需要进行检索,这个时候可以用哈希表来加快检索速度(使用哈希后,如果哈希得当,虽然方法2和方法3都是O(n)的算法,但是方法2只需要遍历一遍,而方法3需要遍历的结点数目则在 n/2 ~ 3*n之间,总体上这个时候方法2优于方法3,实际中应该根据自己的需求选择最合适的方法)。 二、 第二个问题,如何判断两个单链表是否交叉(并找出交叉点?) 1.情形分析 先来看看两个单链表出现交叉时会出现几种情形。 1. 情形一 给定的单链表不存在循环,此时它们必定是Y型交叉,如图所示: 2. 情形二 给定的单链表中存在循环,但是交叉点不在循环的那段子链表中,如图所示: 3. 情形三 给定的单链表中存在循环,而且交叉点属于循环的那段子链表,如图所示: 从图中可以看出,无论哪种情形,只要有交叉存在,则两个单链表必共享交叉点之后的部分。根据单链表的定义也可以得到相同的结论,因为单链表是用指针指示其后继的,因而只要两个单链表存在交叉,那么交叉点之后的部分都由该交叉点的后继指针唯一确定,因而从交叉点开始的部分是由两个单链表共享的。进一步的说,如果两个单链表存在交叉,则它们至少要共享一个结点。 2.解法 如果不存在循环,则对单链表的遍历很简单,如果存在循环就必须找到循环起点,否则就无法进行遍历(知道结点的数目是个例外),因此在本节的讨论中,如果是循环链表,则对其进行遍历的前提是已经找到了其循环起点。 1.判断是否存在交叉 对于情形1,由于两个单链表交叉时,它们至少要共享一个结点,因而我们只需要判断两个单链表的最后一个结点是否相同即可。但是通过判断尾结点的方法我们无法得到交叉点,此时所需时间为:(n1+n2),算法复杂度为O(n)。 对于情形2和3,它们有一个共同的特点就是循环部分必定是共享部分的子集。因此我们可以合并处理: 分别找出两个单链表的循环起点 比较两个起点是否相同,如果相同,则存在交叉,否则执行下一步 遍历其中一个单链表,检测另一个链表的循环起点是否在该链表中,如果在,则存在交叉,否则不存在 由于情形2的存在,所以对于循环链表,该方法也无法找出交叉点(对于情形3,显然循环起点就是交叉点)。此时所需时间为:(找出两个链表的循环起点的时间 + [遍历其中一个链表所需的时间]),若采用O(n2)的算法寻找循环起点,则算法复杂度为O(n2),否则操作都是线性的,因而时间复杂度为O(n)。 综上我们可以得到解决该问题的方法: 找出两个链表的循环起点(如果没有就返回空) 如果两个链表的查找结果都返回空,则执行情形1中的步骤 如果两个链表都返回了非空的循环起点,则执行情形2和3中的步骤 否则,不存在交叉 显然该方法无法返回交叉点。 相应的代码: [cpp] view plain copy int is_lists_crossed(void *head1, void *head2) { void *loop1, *loop2, *tail1, *tail2; tail1 = NULL; tail2 = (void *)0x00000001; loop1 = find_loop_node_v2(head1, &tail1); loop2 = find_loop_node_v2(head2, &tail2); if (loop1 == NULL && loop2 == NULL) { if (tail1 == tail2) { return 1; } else { return 0; } } else if ((loop1 == NULL && loop2 != NULL) || (loop1 != NULL && loop2 == NULL)) { return 0; } if (is_node_in_loop_list(head2, loop2, loop1) == 1) { return 1; } return 0; } 2.找出交叉点 要找出交叉点,就要遍历一个链表,然后检查它是否也存在于另一个链表中,如果存在,它就是交叉点,否则继续遍历。因而它的基础就是遍历。 对于情形1,直接遍历就可以。 对于情形2和3,由于存在循环,因而必须找出位于循环内的某一个结点,这样当从头遍历到该结点第2次时就可以确定链表遍历完成了。具体的实现中可以利用哈希表对遍历做优化: 遍历第一个链表,将其每个结点都记录到一个用链式解决冲突的哈希表中 遍历另一个链表,在遍历该链表的每个结点的时候,都检查该结点是否和哈希表中记录的某个结点相同,如果相同,则存在交叉,并且该结点就是交叉点;否则继续遍历下一个结点。 综上,可以得到解决该问题的方法: 找出两个链表的循环起点(如果没有就返回空,并且返回尾结点) 如果两个链表的查找结果都返回空,则判断尾结点是否相同,若不同,则不交叉,返回空,否则遍历两个链表(具体的遍历可以用哈希表进行优化) 如果两个链表都返回了非空的循环起点,则进行到某个循环点的遍历(具体的遍历可以用哈希表进行优化) 否则,不存在交叉,返回空 该方法的时间复杂度取决于查找循环起点的复杂度以及遍历哈希表的复杂度,最坏为找出循环起点的时间复杂度为O(n2),最好为O(n)。 代码: [cpp] view plain copy void * find_crossed_node_v2(void *head1, void *head2) { void *loop1, *loop2, *tail1, *tail2; tail1 = NULL; tail2 = (void *)0x00000001; loop1 = find_loop_node_v2(head1, &tail1); loop2 = find_loop_node_v2(head2, &tail2); if (loop1 == NULL && loop2 == NULL) { if (tail1 == tail2) { return find_crossed_node_within_no_loop_lists_v2(head1, head2); } else { return NULL; } } else if ((loop1 == NULL && loop2 != NULL) || (loop1 != NULL && loop2 == NULL)) { return NULL; } return find_crossed_node_within_loop_lists_v2(head1, head2, loop1, loop2); }
时间: 2024-10-22 17:13:00