算法通关村第一关 | 链表经典问题黄金挑战 | Java

97 阅读5分钟

1.链表中的环的问题

图片.png

1.1集合解法

将链表元素依次存入Set中,因为Set集合是不允许存入重复元素,若存入失败,则是环形链表,并返回环的入口

public static 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;  
}

1.2双指针解法

使用集合解法很简单,但是需要开辟O(n)的空间,而双指针只需O(1)的空间,因此在该题中更加高效。

一个快指针(一次走两步),一个慢指针(一次走一步),若有环,两个指针一定会相遇。

图片.png 确定有没有环很简单,找到环的入口的思路却不简单。

思路:在快慢指针相遇的那个节点(假定为下图的Z节点)。让其中一个指针回到头节点(X),另一个仍在相遇节点(Z),然后让快慢指针以同一速度开始遍历,当它们再次相遇的节点(Y)就是环的入口

图片.png 快指针在环内跑一圈后相遇:

第一次相遇在Z节点,此时快指针的路径为:a+b+c+b,慢指针为:a+b

因为快指针的速度是慢指针的一倍,所以:a+b+c+b = 2(a+b)

即a = c

因此让slow从Z继续向前走,fast回到起点,两个指针以同一速度遍历(一次一步),slow走路径c,fast走路径a,最后会相遇在Y节点,即环的入口

快指针在环内跑多圈后相遇

快指针路径:a+n(b+c)+b

因为快指针的速度是慢指针的一倍,所以:a+n(b+c)+b = 2(a+b)

即:a = (n-1)b+nc ,又因b+c为环的长度,假定为len

即:a = c+(n-1)len ,多次相遇前,快指针已经在环里跑了(n-1)len圈

n=1 时,就是上述跑一圈后相遇的状况了,n=2、3、4...时,意思是slow指针重新定位到头节点(X),fast指针在相遇节点(Z),它们以同一速度遍历时,都会在环入口处相遇,只不过fast指针要先在环内跑(n-1)len圈

 public ListNode detectCycle(ListNode head) {
        if(head==null) return null;
        ListNode fast = head;
        ListNode slow = head;
        while(fast!=null){
            slow = slow.next;
            if(fast.next!=null){
                fast = fast.next.next;
            }else return null;
            if(fast==slow){
                slow = head; //重新定位到头节点
                while(fast!=slow){
                    fast = fast.next;
                    slow = slow.next;
                }
                return slow;
            }
        }
        return null;
    }

2.双向链表

图片.png

/**  
* 创建双向链表结点  
*/  
class DoubleNode {  
    public int data; //数据域  
    public DoubleNode next; //指向下一个结点  
    public DoubleNode prev;  
  
    public DoubleNode(int data) {  
        this.data = data;  
    }  
  
    //打印结点的数据域  
    public void displayNode() {  
        System.out.print("{" + data + "} ");  
    }  
}

2.1遍历链表

    //从头部开始演绎  
public void displayForward() {  
    System.out.print("List(first--->last): ");  
    DoubleNode current = first;  
    while (current != null) {  
        current.displayNode();  
        current = current.next;  
    }  
    System.out.println();  
}  
  
//从尾部开始演绎  
public void displayBackward() {  
    System.out.print("List(last--->first): ");  
    DoubleNode current = last;  
    while (current != null) {  
        current.displayNode();  
        current = current.prev;  
    }  
    System.out.println();  
}

2.2插入元素

2.2.1头尾插入
//从头部删除结点  
public DoubleNode deleteFirst() {  
    DoubleNode temp = first;  
    if (first.next == null) { //若链表只有一个结点,删除后链表为空,将last指向null  
        last = null;  
    } else {  
        first.next.prev = null; //若链表有两个(包括两个)以上的结点 ,因为是头部插入,则first.next将变成第一个结点,其previous将变成null  
    }  
    first = first.next; //将first.next赋给first  
    return temp; //返回删除的结点  
}  
  
//从尾部删除结点  
public DoubleNode deleteLast() {  
    DoubleNode temp = last;  
    if (first.next == null) { //如果链表只有一个结点,则删除以后为空表,last指向null  
        first = null;  
    } else {  
        last.prev.next = null; //将上一个结点的next域指向null  
    }  
    last = last.prev; //上一个结点称为最后一个结点,last指向它  
    return temp; //返回删除的结点  
}
2.2.2 中间插入

图片.png

//某个结点的后部插入  
public void insertAfter(int key, int data) {  
    DoubleNode newDoubleNode = new DoubleNode(data);  
    DoubleNode current = first;  
    while ((current != null) && (current.data != key)) {  
        current = current.next;  
    }  
    //若当前结点current为空  
    if (current == null) { //current为null有两种情况 一种是链表为空,一种是找不到key值  
        if (isEmpty()) { //1、链表为空  
            first = newDoubleNode; //则插入第一个结点(其实可以调用其它的Insert方法)  
            last = newDoubleNode; //first和last均指向该结点(第一个结点)  
        } else {  
            last.next = newDoubleNode; //2、找不到key值  
            newDoubleNode.prev = last; //则在链表尾部插入一个新的结点  
            last = newDoubleNode;  
        }  
    } else {  
        if (current == last) { //第三种情况,找到了key值,分两种情况  
            newDoubleNode.next = null; //1、key值与最后结点的data相等  
            last = newDoubleNode; //由于newNode将是最后一个结点,则将last指向newNode  
        } else {  
            newDoubleNode.next = current.next; //2、两结点中间插入 四  
            current.next.prev = newDoubleNode; //将current当前结点的下一个结点赋给newNode.next  
        } //将current下一个结点即current.next的previous域指向current  
        current.next = newDoubleNode; //将当前结点的next域指向newNode  
        newDoubleNode.prev = current; //将新结点的previous域指向current(current在newNode前面一个位置)  
      }  
  }

2.2删除节点

2.2.1删除头尾
//从头部删除结点  
public DoubleNode deleteFirst() {  
    DoubleNode temp = first;  
    if (first.next == null) { //若链表只有一个结点,删除后链表为空,将last指向null  
        last = null;  
    } else {  
        first.next.prev = null; //若链表有两个(包括两个)以上的结点 ,因为是头部插入,则first.next将变成第一个结点,其previous将变成null  
    }  
    first = first.next; //将first.next赋给first  
    return temp; //返回删除的结点  
}  
  
//从尾部删除结点  
public DoubleNode deleteLast() {  
    DoubleNode temp = last;  
    if (first.next == null) { //如果链表只有一个结点,则删除以后为空表,last指向null  
        first = null;  
    } else {  
        last.prev.next = null; //将上一个结点的next域指向null  
    }  
    last = last.prev; //上一个结点称为最后一个结点,last指向它  
    return temp; //返回删除的结点  
}
2.2.2删除中间节点

图片.png

//按值删除  
public DoubleNode deleteKey(int key) {  
    DoubleNode current = first;  
    while (current != null && current.data != key) { //遍历链表寻找该值所在的结点  
        current = current.next;  
    }  
    if (current == null) { //若当前结点指向null则返回null,  
        return null; //两种情况当前结点指向null,一是该链表为空链表,而是找不到该值  
    } else {  
        if (current == first) { //如果current是第一个结点  
            first = current.next; //则将first指向它,将该结点的previous指向null,其余不变  
            current.next.prev = null;  
        } else if (current == last) { //如果current是最后一个结点  
            last = current.prev; //将last指向当前结点的上一个结点(我们将当前结点除名了以后它便不再是最后一个了)  
            current.prev.next = null; //相应的要删除结点的上一个结点的next域应指向null  
        } else {  
            current.prev.next = current.next; //当前结点的上一个结点的next域应指向当前的下一个结点  
            current.next.prev = current.prev; //当前结点的下一个结点的previous域应指向当前结点的上一个结点  
        }  
    }  
    return current; //返回  
}