算法通关村第一关——链表黄金挑战笔记

双向链表(带哨兵)

public class DoublyLinkedListSentinel implements Iterable<Integer> {
​
    private final Node head;
    private final Node tail;
​
    public DoublyLinkedListSentinel() {
        head = new Node(null, 666, null);
        tail = new Node(null, 888, null);
        head.next = tail;
        tail.prev = head;
    }
​
    private Node findNode(int index) {
        int i = -1;
        for (Node p = head; p != tail; p = p.next, i++) {
            if (i == index) {
                return p;
            }
        }
        return null;
    }
​
    public void addFirst(int value) {
        insert(0, value);
    }
​
    public void removeFirst() {
        remove(0);
    }
​
    public void addLast(int value) {
        Node prev = tail.prev;
        Node added = new Node(prev, value, tail);
        prev.next = added;
        tail.prev = added;
    }
​
    public void removeLast() {
        Node removed = tail.prev;
        if (removed == head) {
            throw illegalIndex(0);
        }
        Node prev = removed.prev;
        prev.next = tail;
        tail.prev = prev;
    }
​
    public void insert(int index, int value) {
        Node prev = findNode(index - 1);
        if (prev == null) {
            throw illegalIndex(index);
        }
        Node next = prev.next;
        Node inserted = new Node(prev, value, next);
        prev.next = inserted;
        next.prev = inserted;
    }
​
    public void remove(int index) {
        Node prev = findNode(index - 1);
        if (prev == null) {
            throw illegalIndex(index);
        }
        Node removed = prev.next;
        if (removed == tail) {
            throw illegalIndex(index);
        }
        Node next = removed.next;
        prev.next = next;
        next.prev = prev;
    }
​
    private IllegalArgumentException illegalIndex(int index) {
        return new IllegalArgumentException(
                String.format("index [%d] 不合法%n", index));
    }
​
    @Override
    public Iterator<Integer> iterator() {
        return new Iterator<Integer>() {
            Node p = head.next;
​
            @Override
            public boolean hasNext() {
                return p != tail;
            }
​
            @Override
            public Integer next() {
                int value = p.value;
                p = p.next;
                return value;
            }
        };
    }
​
    static class Node {
        Node prev;
        int value;
        Node next;
​
        public Node(Node prev, int value, Node next) {
            this.prev = prev;
            this.value = value;
            this.next = next;
        }
    }
}

链表中环的问题

判断是否有环方法

判断是否有环,最容易的方法是使用Hash,遍历的时候将元素放入到map中,如果有环一定会发生碰撞。发生碰撞的位置也就是入口的位置

public ListNode detectCycle(ListNode head) {
    ListNode pos = head;
    Set<ListNode> visited = new HashSet<ListNode>();
    while (pos != null) {
        if (visited.contains(pos)) {
            return pos;
        } else {
            visited.add(pos);
        }
        pos = pos.next;
    }
    return null;
}

如果只用 O(1)的空间该怎么做呢?我们逐步来分析。首先我们先来思考,为什么快慢指针一定会相遇,之后再来看如何解决问题。

确定是否有环,最有效的方法就是双指针,一个快指针(一次走两步),一个慢指针(一次走一步)。如果快的能到达表尾就不会有环,否则如果存在环,则慢指针一定会在某个位置与快指针相遇,并且快指针不会跳过慢指针,一定会追上(原因请看下图)

当fast快要追上slow的时候,fast一定距离slow还有一个空格,或者两个空格,不会有其他情况。

使用双指针思想寻找是否存在环的方法:

public boolean hasCycle(ListNode head) {
    if(head==null || head.next==null){
        return false; 
    }
    ListNode fast=head;
    ListNode slow=head;
    while(fast!=null && fast.next!=null){
        fast=fast.next.next;
        slow=slow.next;
        if(fast==slow)
            return true;
    }
    return false;
}

确定入口的方法

第一次相遇:
那么我们可以知道fast指针走了a+b+c+b步,slow指针走了a+b步
那么:
2*(a+b) = a+b+c+b
所以a = c
因此此时让slow从Z继续向前走,fast回到起点,两个同时开始走(两个每次都走一步),一次走一步那么它们最终会相遇在y点,正是环的起始点。

如果多圈之后才相遇
如果是走了多圈之后才遇到会怎么样呢? 设链表中环外部分的长度为 a。slow 指针进入环后,又走了 b 的距离与 fast 相遇。此时,fast 指针已经走完了环的 n 圈,因此它走过的总距离为:
Fast: a+n(b+c)+b=a+(n+1)b+nc
根据题意,任意时刻,fast 指针走过的距离都为 slow 指针的 2 倍。因此,我们有:
a+(n+1)b+nc=2(a+b)
由于b+c就是环的长度,假如为LEN,则:
a=c+(n-1)LEN
这说明什么呢?说明相遇的时候快指针在环了已经转了(n-1)LEN圈,如果n-1就退化成了我们上面说的一圈的场景。假如n是2 ,3,4,...呢,这只是说明当一个指针p1重新开始从head走的时候,另一个指针p2从Z点开始,两者恰好在入口处相遇,只不过p2要先在环中转n-1圈。
当然上面的p1和p2要以相同速度,我们发现slow和fast指针在找到位置Z之后就没有作用了,因此完全可以用slow和fast来代表p1和p2。因此代码如下:

public ListNode detectCycle(ListNode head) {
if (head == null) {
    return null;
}
ListNode slow = head, fast = head;
while (fast != null) {
    slow = slow.next;
    if (fast.next != null) {
        fast = fast.next.next;
    } else {
        return null;
    }
    if (fast == slow) {
        ListNode ptr = head;
        while (ptr != slow) {
            ptr = ptr.next;
            slow = slow.next;
        }
        return ptr;
    }
}
return null;
}

确认入口的位置

如果想要确定入口的位置,需要采用三次双指针

第一次使用快慢指针判断是否存在环, fast一次走两步,slow一次走一步来遍历,如果最终相遇说明链表是否存在环。
第二次使用双指针判断环的大小,一个固定在相遇位置不动,另一个从相遇位置开始遍历,当两者再次相等的时候就找到了环的大小,假如为K。
第三次使用找倒数第K个结点的方法来找入口,根据上面2.4.2介绍的方法找倒数第K个元素的方法来找环的入口位置。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值