21. 合并两个有序链表

可以和后面的 23. 合并 K 个升序链表

结合在一起看,不过这里只有两个链表,不用优先级队列,简单一比较就好

注意这个

    // 这个head是特意造的,只是为了后面插入新节点的时候好插入,可以不用对头节点做特殊判断。最后返回head.next即可
        ListNode head = new ListNode(0);
        // 下一个要插入的节点的前驱节点
        ListNode curPre = head;

然后后面就是每次找 cur (两个链表都没完的时候就是两个中较小的那个p,一个已经完了就是另一个还没完的链表的p)

再调整关系即可

      // 调整关系
            curPre.next = cur;
            curPre = cur;

还有一个注意点就是,while 的条件,里面 else if 的条件

/**
 * Definition for singly-linked list.
 * public class ListNode {
 *     int val;
 *     ListNode next;
 *     ListNode() {}
 *     ListNode(int val) { this.val = val; }
 *     ListNode(int val, ListNode next) { this.val = val; this.next = next; }
 * }
 */
class Solution {
    public ListNode mergeTwoLists(ListNode list1, ListNode list2) {
        // 这个head是特意造的,只是为了后面插入新节点的时候好插入,可以不用对头节点做特殊判断。最后返回head.next即可
        ListNode head = new ListNode(0);
        // 下一个要插入的节点的前驱节点
        ListNode curPre = head;
        ListNode p1 = list1;
        ListNode p2 = list2;
        // 注意条件是或
        while (p1 != null || p2 != null) {
            ListNode cur = null;
            // p1 已经完了,现在搞 p2
            if (p1 == null) {
                cur = p2;
                p2 = p2.next;
            }
            // p2 已经完了,现在搞 p1
            else if (p2 == null) {
                cur = p1;
                p1 = p1.next;
            }
            // 两个都没玩,cur=小的那个,同时小的那个往下走一步(注意另一个不用走)
            else {
                if (p1.val<=p2.val) {
                    cur = p1;
                    p1 = p1.next;
                }
                else {
                    cur = p2;
                    p2 = p2.next;
                }
            }
            // 调整关系
            curPre.next = cur;
            curPre = cur;
        }
        return head.next;
    }

}

 

 

25. K 个一组翻转链表

写一个反转链表的公共方法 revers(ListNode listHead)

ki 表示走到了 k 组第几个;k 组第一个记为 kStart ; k 组最后一个记为 kEnd;前一个 k 组的尾巴记为 preKTail

每次走到 k 组最后一个的时候

  • ki 清零
  • kEnd 指向 null ,调用反转方法反转 kStart
  • 如果 preKTail 不为空,preKTail指向 kEnd,preKTail 变为 kStart (反转后 start 成了尾)

如果走到了末尾,而 ki >1 说明后面的不够 k 个一组,不用反转。因此 preKTail 直接指向 kStart

/**
 * Definition for singly-linked list.
 * public class ListNode {
 *     int val;
 *     ListNode next;
 *     ListNode() {}
 *     ListNode(int val) { this.val = val; }
 *     ListNode(int val, ListNode next) { this.val = val; this.next = next; }
 * }
 */
class Solution {

    public ListNode reverseKGroup(ListNode head, int k) {
        ListNode kStart = null;
        ListNode kCur = head;
        ListNode kEnd = null;

        ListNode preKtail = null;

        ListNode resHead = null;
        int ki = 1;
        while(kCur != null) {
            // 每次先缓存下一个节点,防止后面有变化
            ListNode nextNode = kCur.next;
            if (ki == 1) {
                // k 个一组的起始
                kStart = kCur;
            }
            if (ki == k) {
                kEnd = kCur;
                if (resHead == null) {
                    resHead = kEnd;
                }
                ki=0;
                // 切断与下一个 k 组的联系,使得反转链表的时候有尾指向null
                kEnd.next = null;
                // 上一个链表尾指向这个的最后一个(待翻转)
                if (preKtail != null) {
                    preKtail.next = kEnd;
                }
                // 反转链表
                reverseList(kStart);
                // 链表已经反转过,头成了尾巴
                preKtail = kStart;
            }
            ki++;
            kCur = nextNode;
            if (kCur == null && ki > 1) {
                // 没有任何完整的一组的情况
                if (resHead == null) {
                    resHead = kStart;
                }
                // 这些剩下的不用反转,因此上一个尾指向这一个头
                if (preKtail != null) {
                    preKtail.next = kStart;
                }
                break;
            }
        }
        return resHead;
    }

    private void reverseList(ListNode head) {
        ListNode cur = head;
        ListNode pre = null;
        while(cur != null) {
            // 先缓存下一个节点。注意怎么反转链表,这个 Next 指针要每次循环新生成一个,不能定义一个全局的
            ListNode next = cur.next;
            cur.next = pre;
            pre = cur;
            cur = next;
        }
    }
}

 

剑指 Offer 22. 链表中倒数第k个节点

双指针

/**
 * Definition for singly-linked list.
 * public class ListNode {
 *     int val;
 *     ListNode next;
 *     ListNode(int x) { val = x; }
 * }
 */
class Solution {
    public ListNode getKthFromEnd(ListNode head, int k) {

        ListNode p=head;
        ListNode q=head;
        // q 先走 k 步
        for (int i=0;i<k;i++) {
            q=q.next;
        }
        // p 和 q 再一起走
        while(q!=null) {
            p=p.next;
            q=q.next;
        }
        return p;

    }
}

 

143. 重排链表

1、找到链表中点(方法:快慢指针,快指针走每两步,慢指针走一步),将链表分为两段

2、将后半段链表倒排(倒排方法要熟练掌握)

3、将前半段链表 和 倒排后的后半段链表 穿插合并

/**
 * Definition for singly-linked list.
 * public class ListNode {
 *     int val;
 *     ListNode next;
 *     ListNode() {}
 *     ListNode(int val) { this.val = val; }
 *     ListNode(int val, ListNode next) { this.val = val; this.next = next; }
 * }
 */
class Solution {
    public void reorderList(ListNode head) {
        // 找到整个链表的中间节点
        ListNode midNode = findMidNode(head);
        // 反转后半部分
        ListNode reversedRightHead = reverseList(midNode);
        // 将两个列表相间合并
        mergeList(head, reversedRightHead);
    }

    private ListNode findMidNode(ListNode head) {
        ListNode fastNode = head;
        ListNode slowNode = head;
        // 要找到中间节点的前一个节点,从中间断开
        ListNode midPre = null;
        while (fastNode != null) {
            // 快节点走2步,慢节点走1步
            fastNode = fastNode.next;
            if (fastNode != null) {
                fastNode = fastNode.next;
            }
            midPre = slowNode;
            slowNode = slowNode.next;
        }
        // 使链表从中间节点断开
        midPre.next = null;
        return slowNode;
    }

    private ListNode reverseList(ListNode head) {
        ListNode tail=null;

        // 第一个是 head 的话
        ListNode cur = head;
        // 它的 pre 就是 null
        ListNode pre = null;
        while (cur != null) {
            if (cur.next == null) {
                tail = cur;
            }
            // next 不能定义成全局的,要在 while 里面
            ListNode next = cur.next;
            cur.next = pre;
            // pre = cur 要在 cur = next 的前面
            pre = cur;
            cur = next;
        }
        return tail;
    }

    // 将两个列表相间合并
    private void mergeList(ListNode head1, ListNode head2) {
        ListNode p = head1;
        ListNode q = head2;
        while (p != null && q != null) {
            // 暂存 p 本来的 next
            ListNode tmpnext = p.next;
            // 暂存 q 本来的 next
            ListNode tmqnext = q.next;
            
            // 如 1->2 和 4->3,p=1,q=4
            // 那么 p(1)->4 q(4)->2
            // 1->4->2->3
            p.next = q;
            q.next = tmpnext;

            //p 和 q 再指向它们本来的下一个
            p = tmpnext;
            q = tmqnext;
        }
    }
}

 

876. 链表的中间结点

快指针每次走两步,慢指针每次走一步

最后返回慢指针

/**
 * Definition for singly-linked list.
 * public class ListNode {
 *     int val;
 *     ListNode next;
 *     ListNode() {}
 *     ListNode(int val) { this.val = val; }
 *     ListNode(int val, ListNode next) { this.val = val; this.next = next; }
 * }
 */
class Solution {
    public ListNode middleNode(ListNode head) {
        ListNode slowNode = head;
        ListNode fastNode = head;
        while(fastNode != null) {
            fastNode = fastNode.next;
            if (fastNode != null) {
                fastNode = fastNode.next;
                slowNode = slowNode.next;
            }
        }
        return slowNode;
    }
}

 

707. 设计链表

在链表中找到下标为 index 节点的正确方法

Node curNode = head;
Node preNode = null;
int cnt = 0;
// 找到下标为 index 的 curNode
while(curNode != null && cnt<index) {
      preNode = curNode;
      curNode = curNode.next;
      cnt++;
}

之前用这个错误方法,curNode = head.next 并且判断条件是 curNode != null 这样 curNode 是以第二个节点下标为 1 起始的,而不是以第一个节点下标为 0 起始的

Node curNode = head.next;
Node preNode = head;
int cnt = 0;
// 找到下标为 index 的 curNode
while(curNode != null && cnt<index) {
      curNode = curNode.next;
      preNode = preNode.next;
      cnt++;
}
  • get(int index) 
    • 就用上面的方法
  • addAtHead(int val)
    • 特例:链表为空的情况。调整的太简单,不说了。记得 length++ 。记得把后面所有节点的 index +1
  • addAtTail(int val)
    • 特例:链表为空的情况。调整的太简单,不说了。记得 length++。
  • addAtIndex(int index, int val)
    • 排除 index < 0 || index > length
    • 特例:index == 0 直接调用 addAtHead(val)  ;index == length 直接调用 addAtTail(val)
    • 找到下标为 index 的节点,和它的 preNode,然后进行调整
    • 最后从 index 后面那个节点开始,所有节点的下标 +1
  • deleteAtIndex(int index)
    • 排除 index < 0 || index > length-1
    • 找到下标为 index 的节点进行调整
    • 要注意 index==0 即要删头节点时,要改变头节点指向;index == length-1 即要删尾节点时,要改变 tail 指向
    • 最后记得释放 delete 的那个节点的内存,并将 delete 的那个节点的后面所有节点的下标 -1
class MyLinkedList {

    private Node head;

    private Node tail;

    private int length;

    public MyLinkedList() {
        length = 0;
    }

    public class Node {

        public int index;
        public int val;
        public Node next;

        public Node (int val) {
            this.val = val;
        }

        public Node (int index, int val) {
            this.index = index;
            this.val = val;
        }
    }
    
    public int get(int index) {
        if (head == null || index <0 || index>length-1) {
            return -1;
        }
        Node curNode = head;
        int cnt = 0;
        while (curNode != null && cnt<index) {
            curNode = curNode.next;
            cnt++;
        }
        return curNode.val;
    }
    
    public void addAtHead(int val) {
        Node node = new Node(0, val);
        if (length == 0) {
            head = node;
            tail = node;
            node.next = null;
        }
        else {
            node.next = head;
            head = node;
            // 要把后面的 index 都 +1
            Node curNode = head.next;
            while(curNode != null) {
                curNode.index++;
                curNode=curNode.next;
            }
        }
        length++;
    }
    
    public void addAtTail(int val) {
        Node node = new Node(length, val);
        if (length == 0) {
            node.next = null;
            head = node;
            tail = node;
        }
        else {
            tail.next = node;
            node.next = null;
            tail = node;
        }
        length++;
    }
    
    public void addAtIndex(int index, int val) {
        if (index<0 || index>length) {
            return;
        }
        // index 刚好等于 length 插到链表尾
        if (index == length) {
            addAtTail(val);
            return;
        }
        // 有可能插到头节点前面, 这也是一种例外情况
        if (index == 0) {
            addAtHead(val);
            return;
        }
        Node newNode = new Node(index, val);
        Node curNode = head;
        Node preNode = null;
        int cnt = 0;
        // 找到下标为 index 的 curNode
        while(curNode != null && cnt<index) {
            preNode = curNode;
            curNode = curNode.next;
            cnt++;
        }
        // 插到它的前面
        preNode.next = newNode;
        newNode.next = curNode;
        // 从 curNode 开始,后面所有节点的 index+1
        while(curNode != null) {
            curNode.index++;
            curNode = curNode.next;
        }
        length++;
    }
    
    public void deleteAtIndex(int index) {
        if (index<0 || index>length-1) {
            return;
        }
        
        Node preNode = null;
        Node curNode = head;
        int cnt = 0;
        // 找到下标为 index 的节点 curNode
        while(curNode != null && cnt<index) {
            preNode = curNode;
            curNode = curNode.next;
            cnt++;
        }

        Node indexNext = curNode.next;
        if (index == 0) {
            // 释放现在被删掉的头节点的内存
            head = null;
            // 头指向原先的下一个
            head = indexNext;
        }
        else {
            // 调整
            preNode.next = indexNext;
            curNode = null;
        }

        // 如果下标为 index 的节点是最后一个节点
        // 那么删掉后要改变 tail
        if (index == length-1) {
            tail = preNode;
        }
        
        length--;
        // 删完要将它后面所有节点的 下标 -1
        curNode = indexNext;
        while (curNode != null) {
            curNode.index--;
            curNode = curNode.next;
        }
        

    }
}

/**
 * Your MyLinkedList object will be instantiated and called as such:
 * MyLinkedList obj = new MyLinkedList();
 * int param_1 = obj.get(index);
 * obj.addAtHead(val);
 * obj.addAtTail(val);
 * obj.addAtIndex(index,val);
 * obj.deleteAtIndex(index);
 */

 

160. 相交链表

方法一:哈希集合

从头遍历 A 链表,把所有节点放到 HashSet 中

再从头变脸 B 链表,每次判断节点是否已经在 HashSet 中。第一个已经在 HashSet 中的节点就是相交节点

  • 时间复杂度:O(m+n)
  • 空间复杂度:O(m)

方法二:双指针

将2个链表在末尾加上对方,碰到第一个相同的点,要么是交点,要么是末尾

  • 相交
  • A: 1 -> 2 -> 3 -> C -> 4 -> 5 -> null

  • B: 6 -> 7 -> C -> 4 -> 5 -> null

  • A + B: 1 -> 2 -> 3 -> C -> 4 -> 5 -> 6 -> 7 -> C -> 4 -> 5 -> null

  • B + A: 6 -> 7 -> C -> 4 -> 5 -> 1 -> 2 -> 3 -> C -> 4 -> 5 -> null

  • -------------------------------------------------------------------------------------------

  • 不相交
  • A: 1 -> 2 -> 3 -> 4 -> 5 -> null

  • B: 6 -> 7 -> 8 -> 3 -> null

  • A + B: 1 -> 2 -> 3 -> 4 -> 5 -> 6 -> 7 -> 8 -> 3 -> null

  • B + A: 6 -> 7 -> 8 -> 3 -> 1 -> 2 -> 3 -> 4 -> 5 -> null

不用物理意义的加到末尾,只需要 pA pB 两指针:

  • pA 不为空,pA 每次走到下一步 next ;pB 不为空,pB 每次走到下一步 next 
  • pA 为空时,pA 去 B 的头节点 headB;pB 为空时,pB 去 A 的头节点 headA
  • 当连着相等时:如果不为 null ,说明走到了相交节点返回;如果为 null,说明两链表没有相交返回 null

此解法可以降低空间复杂度

  • 时间复杂度:O(m+n)
  • 空间复杂度:O(1)
/**
 * Definition for singly-linked list.
 * public class ListNode {
 *     int val;
 *     ListNode next;
 *     ListNode(int x) {
 *         val = x;
 *         next = null;
 *     }
 * }
 */
public class Solution {
    public ListNode getIntersectionNode(ListNode headA, ListNode headB) {
        ListNode pA = headA;
        ListNode pB = headB;
        while (true) {
            if (pA == pB) {
                if (pA == null) {
                    return null;
                }
                else {
                    return pA;
                }
            }
            pA = (pA == null ? headB:pA.next);
            pB = (pB == null ? headA:pB.next); 
        }
    }
}

 

206. 反转链表

/**
 * Definition for singly-linked list.
 * public class ListNode {
 *     int val;
 *     ListNode next;
 *     ListNode() {}
 *     ListNode(int val) { this.val = val; }
 *     ListNode(int val, ListNode next) { this.val = val; this.next = next; }
 * }
 */
class Solution {
    public ListNode reverseList(ListNode head) {
        ListNode pre = null;
        ListNode p = head;
        while (p != null) {
            ListNode next = p.next;
            p.next = pre;
            pre = p;
            p = next;
        }
        return pre;
    }
}

 

234. 回文链表

方法一:

找到中间节点,反转链表的后半部分

p 指向前半部分头(即原链表头),q 指向反转后的后半链表头,依次往后比较

方法二:

slow fast 找中间节点的同时,把前半部分链表的值加入 stack

后半链表用 slow 来遍历,与 stack 顶部元素比较,如果不相等就返回 false。相等就出栈并继续往下比较。

/**
 * Definition for singly-linked list.
 * public class ListNode {
 *     int val;
 *     ListNode next;
 *     ListNode() {}
 *     ListNode(int val) { this.val = val; }
 *     ListNode(int val, ListNode next) { this.val = val; this.next = next; }
 * }
 */
class Solution {
    public boolean isPalindrome(ListNode head) {
        Stack<Integer> stack = new Stack();
        ListNode slow = head;
        ListNode fast = head;
        // 把前半部分元素放入栈中。当 fast == null 时退出循环
        while(fast != null) {
            fast = fast.next;
            if (fast != null) {
                fast = fast.next;
                // 把 stack 放在里面,也是为了奇数时最中间那个元素不被加进去
                stack.push(slow.val);
            }
            // 如果 slow 在 if (fast != null) 的里面,那么如果是奇数个元素,返回的就是最中间的那个
            // 如果把 slow 放在 if (fast != null) 的外面,那么如果是奇数个元素,返回的就是最中间的下一个
            slow = slow.next;
        }
        // 后半部分元素与栈中的逐个比较
        while(slow != null) {
            int top = stack.peek();
            if (top != slow.val) {
                return false;
            }
            else {
                stack.pop();
                slow = slow.next;
            }
        }
        return true;
    }

    public boolean isPalindrome2(ListNode head) {
        if (head.next == null) {
            return true;
        }
        // 找到中间节点
        ListNode midNode = findMidNode(head);
        // 翻转后半部分
        ListNode newMidNode = reversList(midNode);
        // p指向前半部分开头。q指向后半部分开头,依次比较
        ListNode p = head;
        ListNode q = newMidNode;
        while(q != null) {
            if (p.val != q.val) {
                return false;
            }
            p = p.next;
            q = q.next;
        }
        return true;
    }

    private ListNode findMidNode(ListNode head) {
        ListNode slow = head;
        ListNode fast = head;
        while (fast != null) {
            fast = fast.next;
            if (fast == null) {
                return slow;
            }
            fast = fast.next;
            slow = slow.next;
        }
        return slow;
    }

    private ListNode reversList(ListNode head) {
        ListNode pre = null;
        ListNode p = head;
        while (p != null) {
            ListNode next = p.next;
            p.next = pre;
            pre = p;
            p = next;
        }
        return pre;
    }
}

 

141. 环形链表

方法一:哈希表

最容易想到的方法是遍历所有节点,每次遍历到一个节点时,判断该节点此前是否被访问过。

具体地,我们可以使用哈希表来存储所有已经访问过的节点。每次我们到达一个节点,如果该节点已经存在于哈希表中,则说明该链表是环形链表,否则就将该节点加入哈希表中。重复这一过程,直到我们遍历完整个链表即可。

复杂度分析

  • 时间复杂度:O(N) 最坏情况下我们需要遍历每个节点一次。
  • 空间复杂度:O(N) 主要为哈希表的开销,最坏情况下我们需要将每个节点插入到哈希表中一次。

方法二:快慢指针

本方法需要读者对「Floyd 判圈算法」(又称龟兔赛跑算法)有所了解。

假想「乌龟」和「兔子」在链表上移动,「兔子」跑得快,「乌龟」跑得慢。当「乌龟」和「兔子」从链表上的同一个节点开始移动时,如果该链表中没有环,那么「兔子」将一直处于「乌龟」的前方;如果该链表中有环,那么「兔子」会先于「乌龟」进入环,并且一直在环内移动。等到「乌龟」进入环时,由于「兔子」的速度快,它一定会在某个时刻与乌龟相遇,即套了「乌龟」若干圈。

我们可以根据上述思路来解决本题。具体地,我们定义两个指针,一快一慢。慢指针每次只移动一步,而快指针每次移动两步。初始时,慢指针在位置 head,而快指针在位置 head.next。这样一来,如果在移动的过程中,快指针反过来追上慢指针,就说明该链表为环形链表。否则快指针将到达链表尾部,该链表不为环形链表。

时间复杂度:O(N)

  • 当链表中不存在环时,快指针将先于慢指针到达链表尾部,链表中每个节点至多被访问两次。
  • 当链表中存在环时,每一轮移动后,快慢指针的距离将减小一。而初始距离为环的长度,因此至多移动 N 轮

空间复杂度:O(1)。我们只使用了两个指针的额外空间。

/**
 * Definition for singly-linked list.
 * class ListNode {
 *     int val;
 *     ListNode next;
 *     ListNode(int x) {
 *         val = x;
 *         next = null;
 *     }
 * }
 */
public class Solution {
    public boolean hasCycle(ListNode head) {
        // 因为最后返回的是 slow==fast。而只有一个节点时,slow和fast都是初始的head不会往下走,会返回true,但其实应该是false
        if (head == null || head.next == null) {
            return false;
        }
        ListNode slow = head;
        ListNode fast = head;
        // slow 走一步,fast 走两步
        do {
            slow = slow.next;
            fast = fast.next;
            if (fast != null) {
                fast = fast.next;
            }
        }
        // do while 是因为,一开始 slow和fast 都在 head,如果是 while 的话就往下走不了了
        while (slow != fast && fast != null);
        // slow 和 fast 相遇的话说明有环
        return slow == fast;
        
    }
}

 

 

142. 环形链表 II

环外部分长度为 a

slow 进入环以后,又走了 b 与 fast 相遇。slow 走过的距离为 a+b(slow 一定是未满一圈就和 fast 相遇的,可以证明)

相遇时 fast 已经走完了环的 n 圈,fast 走过的距离为 a+n(b+c)+b = a+(n+1)b+nc

任意时刻,fast 走过的长度都为 slow 的两倍,即 a+(n+1)b+nc = 2(a+b) ——> a = c+(n-1)(b+c)

由 a = c+(n-1)(b+c) 可知,所求的入环点距离 a 为 相遇距离c 加上 n-1 圈的环长

因此,当发现 slow 和 fast 相遇后,再有一个新指针指向头 head,和 slow 每次同步移动一步,最后它们会在所求入环点相遇

 

/**
 * Definition for singly-linked list.
 * class ListNode {
 *     int val;
 *     ListNode next;
 *     ListNode(int x) {
 *         val = x;
 *         next = null;
 *     }
 * }
 */
public class Solution {
    public ListNode detectCycle(ListNode head) {
        if (head == null || head.next == null) {
            return null;
        }
        ListNode slow = head;
        ListNode fast = head;
        // 快指针走两步,慢指针走一步
        do {
            slow = slow.next;
            fast = fast.next;
            if (fast != null) {
                fast = fast.next;
            }
        }
        // do while 是因为刚开始 slow 和 fast 都为 head,如果 while 的话没法下去
        while (slow != fast && fast!= null);
        // slow 和 fast 相遇说明有环,没相遇,直接返回 null 
        if (slow != fast) {
            return null;
        }
        // 相遇后让一个新指针指向头部
        ListNode newp = head;
    // 新指针和慢指针同步走一步
        while (slow != newp) {
            slow = slow.next;
            newp = newp.next;
        }
        // 最后新指针和慢指针会在入环点相遇
        return newp;
    }
}

 

 

 

19. 删除链表的倒数第 N 个结点

 快指针先走 n 步,慢指针再跟着一起走

/**
 * Definition for singly-linked list.
 * public class ListNode {
 *     int val;
 *     ListNode next;
 *     ListNode() {}
 *     ListNode(int val) { this.val = val; }
 *     ListNode(int val, ListNode next) { this.val = val; this.next = next; }
 * }
 */
class Solution {
    public ListNode removeNthFromEnd(ListNode head, int n) {

        ListNode fast = head;
        ListNode slow = fast;
        ListNode slowPre = null;
        for (int i=0;i<n;i++) {
            fast = fast.next;
        }
        while (fast != null) {
            slowPre = slow;
            slow = slow.next;
            fast = fast.next;
        }
        if (slowPre != null) {
            slowPre.next = slow.next;
            slow = null;
        }
        else {
            // 倒数第 n 个可能就是头节点
            head = slow.next;
            slow = null;
        }
        return head;
    }
}

 

24. 两两交换链表中的节点

 与 k 个一组翻转链表类似,只是这里 k=2

写一个反转链表的公共方法 revers(ListNode listHead)

ki 表示走到了 k 组第几个;k 组第一个记为 kStart ; k 组最后一个记为 kEnd;前一个 k 组的尾巴记为 preKTail

每次走到 k 组最后一个的时候

  • ki 清零
  • kEnd 指向 null ,调用反转方法反转 kStart
  • 如果 preKTail 不为空,preKTail指向 kEnd,preKTail 变为 kStart (反转后 start 成了尾)

如果走到了末尾,而 ki >1 说明后面的不够 k 个一组,不用反转。因此 preKTail 直接指向 kStart

/**
 * Definition for singly-linked list.
 * public class ListNode {
 *     int val;
 *     ListNode next;
 *     ListNode() {}
 *     ListNode(int val) { this.val = val; }
 *     ListNode(int val, ListNode next) { this.val = val; this.next = next; }
 * }
 */
class Solution {
    public ListNode swapPairs(ListNode head) {
        ListNode resHead = null;
        ListNode preKEnd = null;
        ListNode kp = head;
        ListNode kStart = head;
        ListNode kEnd = null;
        ListNode nextKStart = head;
        int k=2;
        boolean isFirstK = true;

        while (true) {
            kStart = nextKStart;
            int ki=0;
            for (int i=0;i<k;i++) {
                if (kp != null) {
                    kp = kp.next;
                    if (i==k-2) {
                        kEnd = kp;
                    }
                }
                else {
                    break;
                }
                ki++;
            }
            if (ki == k) {
                nextKStart = kp;
                kEnd.next = null;
                reverseList(kStart);
                if (preKEnd != null) {
                    preKEnd.next = kEnd;
                }
                preKEnd = kStart;
                if (isFirstK) {
                    resHead = kEnd;
                }
            }
            else {
                if (preKEnd != null) {
                    preKEnd.next = kStart;
                }
                if (isFirstK) {
                    resHead = kStart;
                }
                break;
            }
            isFirstK = false;
        }
        return resHead;
    }

    private ListNode reverseList(ListNode head) {
        ListNode pre = null;
        ListNode p = head;
        while (p != null) {
            ListNode next = p.next;
            p.next = pre;
            pre = p;
            p = next;
        }
        return pre;
    }
}

 

 

138. 复制带随机指针的链表

方法一:

有一个 Map<Node,Node> 来存所有 旧节点-新节点 的对应

先遍历一次,复制链表中所有的节点,复制的过程中把 旧节点-新节点 放入 map

再遍历一次:

       Node curCopy = oldNode2NewNodeMap.get(curOld);
            curCopy.next = oldNode2NewNodeMap.get(curOld.next);
            curCopy.random = oldNode2NewNodeMap.get(curOld.random);

方法二:

比上面进步的是,上面空间复杂度 O(n),这个是 O(1)

1.每个节点的后面搞一个 S',是这个节点的拷贝。注意S'在遍历过程中就加进去了,所以每次走两步 cur=cur.next.next
2.random指针:由于每个节点的后面就是它的拷贝,所以也很容易定位 random 的拷贝就是 randomOld 的 next:
Node randomOri = cur.random;
Node curCopy = cur.next;
curCopy.random = randomOri.next;
3.重新分开为两个链表。注意S'在遍历过程中就去掉了,所以每次走一步 cur=cur.next
/*
// Definition for a Node.
class Node {
    int val;
    Node next;
    Node random;

    public Node(int val) {
        this.val = val;
        this.next = null;
        this.random = null;
    }
}
*/

class Solution {
     /* 
       时间复杂度 O(n)
       空间复杂度 O(n)
    */
    public Node copyRandomList2(Node head) {
        // 旧节点与新节点一一对应
        Map<Node, Node> oldNode2NewNodeMap = new HashMap();
        for (Node curOld=head;curOld!=null;curOld=curOld.next) {
            Node newNode = new Node(curOld.val);
            oldNode2NewNodeMap.put(curOld, newNode);
        }
        for (Node curOld=head;curOld!=null;curOld=curOld.next) {
            Node curCopy = oldNode2NewNodeMap.get(curOld);
            curCopy.next = oldNode2NewNodeMap.get(curOld.next);
            curCopy.random = oldNode2NewNodeMap.get(curOld.random);
        }
        return oldNode2NewNodeMap.get(head);
    }

    /* 
       时间复杂度 O(n)
       空间复杂度 O(1)
    */
    public Node copyRandomList(Node head) {
        if (head == null) {
            return null;
        }
        // 1.每个节点的后面搞一个 S'。注意S'在遍历过程中就加进去了,所以每次走两步 cur=cur.next.next
        for (Node cur=head;cur!=null;cur=cur.next.next) {
            Node newNode = new Node(cur.val);
            newNode.next = cur.next;
            cur.next = newNode;
        }
        // 2.random指针
        for (Node cur=head;cur!=null;cur=cur.next.next) {
            Node randomOri = cur.random;
            Node curCopy = cur.next;
            if (randomOri != null) {
                curCopy.random = randomOri.next;
            }
        }
        // 3.分开成两个链表。注意S'在遍历过程中就去掉了,所以每次走一步 cur=cur.next
        Node resHead = head.next;
        for (Node cur=head;cur!=null;cur=cur.next) {
            Node curCopy = cur.next;
            cur.next = curCopy.next;
            if (curCopy.next != null) {
                curCopy.next = curCopy.next.next;
            }
        } 
        return resHead;
    }
}
 

23. 合并 K 个升序链表

优先队列(原理是堆排序,每次加入和取出的时间复杂度都是 logk)

要重写compare方法,确保每次从队列中取出来的,都是ListNode val最小的那个。

因为要重写 compareTo 方法,而 leetcode 已经定义好好的 ListNode 我们是不能改变更别说重写 compareTo 的。因此新定义一个类把 ListNode 包进去,再重写 compareTo 方法。这个类要 implements Comparable<Status>,然后重写 compareTo() 方法

1.初始化优先队列:将所有链表的头节点加入队列

2.定义:

    • ListNode head = new ListNode(0) 这个head是特意造的,只是为了后面插入新节点的时候好插入,可以不用对头节点做特殊判断。最后结果返回head.next即可
    • ListNode curPre = head 每次插入位置的前驱节点

3.while队列不为空

    • ListNode cur = queue.poll().node 即从优先队列中取出最小的那个node
    • curPre.next=cur;
    • curPre = cur;
    • 再把这个 cur 在它所在链表的下一个节点加入优先级队列

时间复杂度 O(kn * logk)

优先队列元素不超过k个,每个元素插入和删除复杂度都是 logk。所有链表的所有元素一共是 kn 个,它们每个都要在优先列表中插入和删除一次,所以一共 O(kn * logk)

空间夫再度 O(k)

/**
 * Definition for singly-linked list.
 * public class ListNode {
 *     int val;
 *     ListNode next;
 *     ListNode() {}
 *     ListNode(int val) { this.val = val; }
 *     ListNode(int val, ListNode next) { this.val = val; this.next = next; }
 * }
 */
class Solution {

    class Status implements Comparable<Status> {
        
        public ListNode node;
        // 构造方法
        public Status(ListNode node) {
            this.node = node;
        }
        // leetcode 已经定义好好的 ListNode 我们是不能改变更别说重写 compareTo 的。因此新定义一个类把 ListNode 包进去再重写
        public int compareTo(Status status2) {
            return this.node.val - status2.node.val;
        }
    }

    private PriorityQueue<Status> queue = new PriorityQueue();

    public ListNode mergeKLists(ListNode[] lists) {
        // 1.初始化优先队列:把所有链表的头节点加进去
        for(ListNode listHead : lists) {
            if (listHead != null) {
                queue.offer(new Status(listHead));
            }
        }
        // 这个head是特意造的,只是为了后面插入新节点的时候好插入,可以不用对头节点做特殊判断。最后返回head.next即可
        ListNode head = new ListNode(0);
        // curPre,下一个要插入位置的前驱
        ListNode curPre = head;
        while (!queue.isEmpty()) {
            // 队首的即为所有链表中最小的那个
            ListNode cur = queue.poll().node;
            // 把它加到 curPre 的后面
            curPre.next = cur;
            curPre = cur;
            // 把刚刚那个cur在它所在链表的next加到queue中
            if (cur.next != null) {
                queue.offer(new Status(cur.next));
            }
        }
        return head.next;
    }
}

 

148. 排序链表

一般都用归并排序,因为是单向链表,其它排序算法根据下标找元素,向前遍历等都比较困难

主函数流程是:

  • 如果 head==null || head.next==null return head。因为 head.next == null 即只有一个元素时,不用再划分了,而且一个元素本身也是有序的,所以返回就返回这一个元素
  • 通过找链表中点算法找到 midNode
  • 左半数组是 head~midNode,右半数组是 midNode.next~结尾null 。因此令 rightListHead=midNode.next。此外为了使坐半链表有正确的边界,结尾指向null,把链表从中间分割开来,令 midNode.next = null
  • 左半递归 rightHead = sortList(head) 右半递归  rightHead = sortList(rightListHead) 
  • 最后合并左半和右半两个有序链表 return merge2OrderList(leftHead, rightHead)

过程中会用到链表的两个经典算法:

  • 合并两个有序链表:虚拟头节点,ListNode head = new ListNode(0); curPre = head 最后返回 head.next。while(p1!=null || p2!=null) if(p1==null)......
  • 找链表中点:快指针走两步,慢指针走一步,但又一点于以前不同: 我们希望奇数个如 1 2 3 时返回 2,偶数个如 1 2 3 4 时返回 2 。因为我们认为 mid.next 是右半的起点,并且在这时候会令 mid.next = null。这与之前的不同,之前偶数个如 1 2 3 4 时希望返回 3

之前的找链表中点:奇数个如 1 2 3 时返回 2,偶数个如 1 2 3 4 时返回 3 。

private static ListNode findMidNode(ListNode head) {
        ListNode fast = head;
        ListNode slow = head;
        while (fast != null) {
            fast = fast.next;
            if (fast != null) {
                fast = fast.next;
                slow = slow.next;
            }
        }
        return slow;
    }
  • 奇数 1 2 3:初始 fast=1 slow=1 ;第一次循环:fast=2 fast=3 slow=2  ;第二次循环 fast=3 ;最后返回 slow=2
  • 偶数 1 2 3 3:初始 fast=1 slow=1 ;第一次循环:fast=2 fast=3 slow=2 ;第二次循环 fast=4 fast=null slow=3 ;最后返回 slow=3

现在的找链表中点:奇数个如 1 2 3 时返回 2,偶数个如 1 2 3 4 时返回 2 。

private static ListNode findMidNode(ListNode head) {
        ListNode fast = head;
        ListNode slow = head;
        while (fast != null) {
            fast = fast.next;
            // 这里要上一个  && fast.next != null 的条件
            if (fast != null && fast.next != null) {
                fast = fast.next;
                slow = slow.next;
            }
        }
        return slow;
    }
  • 奇数 1 2 3:初始 fast=1 slow=1 ;第一次循环:fast=2 fast=3 slow=2 ;第二次循环 fast=3 ;最后返回 slow=2
  • 偶数 1 2 3 3:初始 fast=1 slow=1;第一次循环:fast=2 fast=3 slow=2 ;第二次循环 fast=4 因为fast.next==null退出了循环;最后返回 slow=2
/**
 * Definition for singly-linked list.
 * public class ListNode {
 *     int val;
 *     ListNode next;
 *     ListNode() {}
 *     ListNode(int val) { this.val = val; }
 *     ListNode(int val, ListNode next) { this.val = val; this.next = next; }
 * }
 */
class Solution {

    public ListNode sortList(ListNode head) {
        if (head == null || head.next == null) {
            // head.next == null 即只有一个元素时,不用再划分了,而且一个元素本身也是有序的,所以返回就返回这一个元素
            // 而且只有一个元素时,也没法找中点
            return head;
        }
        ListNode midNode = findMidNode(head);
        // 右半数组的起始是中间节点的下一个
        ListNode rightListHead = midNode.next;
        // 为了使链表有正常的边界,从中间节点断开
        midNode.next = null;
        ListNode leftHead = sortList(head);
        ListNode rightHead = sortList(rightListHead);
      // 合并两个有序链表
      ListNode list = merge2OrderList(leftHead, rightHead);
     return list;
    }

    /*
        经典链表算法:合并两个有序链表
     */
    private static ListNode merge2OrderList(ListNode head1, ListNode head2) {
        // 这个 head 是可以造的,只是为了后面可以不对头节点做特殊判断
        ListNode head = new ListNode(0);
        ListNode curPre = head;
        ListNode p1 = head1;
        ListNode p2 = head2;
        // 两个有一个没完,就继续循环,注意是 || 不是 &&
        while (p1 != null || p2 != null) {
            // list1完了。list2没完
            // 请注意:【刚开始写成了p1!=null】 应该是【p1==null】
            if (p1 == null) {
                curPre.next = p2;
                curPre = p2;
                p2 = p2.next;
            }
            // list2完了。list1没完
            else if (p2 == null) {
                curPre.next = p1;
                curPre = p1;
                p1 = p1.next;
            }
            else {
                if (p1.val <= p2.val) {
                    curPre.next = p1;
                    curPre = p1;
                    p1 = p1.next;
                }
                else {
                    curPre.next = p2;
                    curPre = p2;
                    p2 = p2.next;
                }
            }
        }
        // 最后返回 head.next,因为 head 就是个虚拟结点
        return head.next;
    }

    /*
        经典链表算法:合并两个有序链表
     */
    private static ListNode findMidNode(ListNode head) {
        // 我们希望奇数个如 1 2 3 时返回 2,偶数个如 1 2 3 4 时返回 2
        // 因为我们认为 mid.next 是右半的起点,并且在这时候会令 mid.next = null
        // 这与之前的不同,之前偶数个如 1 2 3 4 时希望返回 3
        ListNode fast = head;
        ListNode slow = head;
        while (fast != null) {
            fast = fast.next;
            // 所以这里要上一个  && fast.next != null 的条件
            if (fast != null && fast.next != null) {
                fast = fast.next;
                slow = slow.next;
            }
        }
        return slow;
    }
}