链表常见的问题【转】

转自: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

链表常见的问题【转】的相关文章

链表常见题型(java版)

直接上干货..... 链表常见题型: 找到单链表的倒数第k个节点. 删除单链表中的某个结点(O(1)). 反转链表. 两个链表的第一个公共结点. 有环链表返回环路的开头节点(及判断是否有环). 合并两个排序的链表. 删除链表中重复的结点. 先给出链表的定义: /** * 单链表定义 */ public static class Node<E>{ private E element;//节点保存的元素 private Node<E> next;//指向下一个节点的引用 public

java数据结构:单链表常见操作代码实现

一.概述: 本文主要总结单链表常见操作的实现,包括链表结点添加.删除:链表正向遍历和反向遍历.链表排序.判断链表是否有环.是否相交.获取某一结点等. 二.概念: 链表: 一种重要的数据结构,HashMap等集合的底层结构都是链表结构.链表以结点作为存储单元,这些存储单元可以是不连续的.每个结点由两部分组成:存储的数值+前序结点和后序结点的指针.即有前序结点的指针又有后序结点的指针的链表称为双向链表,只包含后续指针的链表为单链表,本文总结的均为单链表的操作. 单链表结构: Java中单链表采用No

链表常见的题型(java实现)

链表是面试中最常见的一种题型,因为他的每个题的代码短,短短的几行代码就可以体现出应聘者的编码能力,所以它也就成为了面试的重点. 链表常见的操作有1.打印链表的公共部分,2.删除链表的倒数第K个节点,3.翻转单向链表,4.环形约瑟夫环问题,5.判断链表是否是一个回文链表,6.两个链表生成相加链表,7.删除无序链表中重复出现的节点,8.删除指定值得节点,9.合并两个有序的单链表,10.环形链表的插入 import java.util.*; /********** *@Author:Tom-shush

Java 链表常见考题总结

首先定义自定义结点类,存储节点信息: public class Node { Node next=null; int data; public Node(int data){ this.data=data; } } 获取链表长度: private int length() { int length=0; Node temp=head; while(temp!=null){ length++; temp=temp.next; } return length; } 打印链表: public void

链表常见题目总结

1:链表相加问题      (思路) 2:链表的部分翻转 3:链表去掉重复元素,只保留第一个重复元素 4:链表去掉所有重复的元素 5:链表按照某一个结点划分 6:单链公共结点问题    7:删除链表的倒数第n个结点 8:获得链表的倒数第n个结点 9:合并两个有序链表 10:两两交换链表中的结点 11:旋转链表 12:有序链表转化为二叉搜索树 13:复制带随机指针的链表 14:环形链表 15:环形链表删除第一个入环点 原文地址:https://www.cnblogs.com/xiaxj/p/957

链表-常见题(二)

回文链表: 1.首先快慢指针获取中间节点 2.利用栈存储后半段节点值 或者反转后半段 3.前半部分链表与栈进行值比较,出现不等则表示非回文链表 合并两个有序链表: 1.空表检查 2.新建表头 3.表1和表2进行对比,逐个插入到表头 4.返回链表头节点 原文地址:https://www.cnblogs.com/hzk-note/p/9589071.html

链表常见题目--附具体分析和代码

一.链表的反转 示例: 输入: 1->2->3->4->5->NULL 输出: 5->4->3->2->1->NULL 分析:刚开始的时候很自然的想到,创建一个列表,然后遍历链表,将每个节点保存在列表中,然后根据列表,反向构造一个新的链表.但是这个很明显很low,一方面是空间复杂度为O(n),一方面是要遍历两遍.后来想都到了另外一种方法,只需要遍历一遍,然后所需的额外空间也非常少. 核心思想就是:遍历链表,创建一个新节点,保存当前节点的值.一个节

链表常见操作

1.迭代. 链表最常见的操作就是迭代. while (head.next != null) { head = head.next; } 2.链表转化为数组 涉及到下标的问题,都可以将链表转化为数组解决,数组的每一个元素都是一个节点.. 示例题目,返回链表的中间节点 public ListNode middleNode(ListNode head) { ListNode[] A = new ListNode[100]; int t = 0; while (head.next != null) {

Java链表常见操作【剑指Offer】03:从尾到头打印链表

题目描述 输入一个链表,按链表从尾到头的顺序返回一个ArrayList. 题解一:递归 1 /* 2 在最后一次递归方法返回以后,每一层的递归方法都会做一个arrayList.add(listNode.val)这个操作, 3 从最后一次到第一次,逆向的调用了后面的方法 4 */ 5 static ArrayList<Integer> list = new ArrayList<>(); 6 public static ArrayList<Integer> printLis