两个链表问题

单向链表可能存在环形,写一段代码判断是否有环。

首先定义链表的节点:

class Node {
    int value;
    Node next;

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;

        Node node = (Node) o;

        if (value != node.value) return false;
        return next != null ? next.equals(node.next) : node.next == null;
    }

    @Override
    public int hashCode() {
        int result = value;
        result = 31 * result + (next != null ? next.hashCode() : 0);
        return result;
    }
}

上面这段代码需要注意equals和hashCode的写法。

常识告诉我们,两个人围绕圆环在同一地点同时出发,那么快者必将追上慢者,这个命题的逆命题也是成立的。

问题是怎么实现呢?假设有两者指针,慢指针每次走一步,快指针每次走两步,这样快指针一定能追上慢指针吗?很多文章都默认这是正确的,其实这是需要严格证明的。

boolean isHasCircle(Node node) {
    if (node == null) {
        return false;
    }
    Node fast = node;
    Node slow = node;
    while (fast != null && fast.next != null) {
        fast = fast.next.next;
        slow = slow.next;
        if (fast == slow) {
            return true;
        }
    }
    return false;
}

这个问题的深化问题是返回环的首节点,以及求环的长度等问题。

Node findLoopPort(Node head) {
    if (head == null) {
        return null;
    }
    Node fast = head;
    Node slow = head;
    while (fast != null && fast.next != null) {
        fast = fast.next.next;
        slow = slow.next;
        if (fast == slow) {
            break;
        }
    }
    if (fast != slow) {
        return null;
    }
    fast = head;
    while (fast != slow) {
        fast = fast.next;
        slow = slow.next;
    }
    return fast;
}

上面这段代码可以求出环的入口节点。关于算法的原理请见参考资料,这里就不赘述了。

如果一段简短清晰的代码能解决一个复杂棘手的问题,那么这段代码就可以称之为优雅或者高效了,其背后的原因就是算法含量高,体现在两个方面:

  1. 使用了恰当的数据结构
  2. 发现了深层次的规律
    本题的解法倚赖于发现两个指针的运动规律,推理过程很像分析物理问题。

到此为止了吗?不,还有更简单的算法——断链法。

Node findLoopPort(Node head) {
    if (head == null) {
        return null;
    }
    Node fast = head;
    Node slow = head;
    while (fast != null && fast.next != null) {
        fast = fast.next.next;
        slow = slow.next;
        if (fast == slow) {
            break;
        }
    }
    if (fast != slow) {
        return null;
    }

    Node previous = head;
    Node current = head.next;
    while (current != null) {
        previous.next = null;
        previous = current;
        current = current.next;
    }
    return previous;
}

这种算法的缺点是破坏了原链表的结构。

这段代码可以学到循环的技巧(初始化-计算-更新循环变量),还有影子指针的操作。

关于链表的另一个常见问题是求两个无环链表求第一个交点。可以用hashtable,但这需要额外的存储空间,而且用Java不容易写对。下面介绍一种错误写法。

Node firstNode(Node node1, Node node2) {
    if (node1 == null || node2 == null) {
        return null;
    }
    HashSet set = new HashSet<>();
    while (node1 != null) {
        set.add(node1);
        node1 = node1.next;
    }
    while (node2 != null) {
        if (set.contains(node2)) {
            return node2;
        }
    }
    return null;
}

上面的代码看似无懈可击,但忽略了contain的使用条件,它比较的是equals和hashcode方法,而不是引用地址。

可以轻松地找出反例:两个长度是1且不相交的单向链表,他们的节点的value相等。

正确的算法是计算两个链表的长度差,代码省略。

参考:面试精选:链表问题集锦,剑指Offer--056-链表中环的入口结点 - CSDN博客

你可能感兴趣的:(两个链表问题)