数据结构1、链表专题(环/反转/合并/分割)

指针操作小技巧:

1、一旦出现类似 nxt.next 这种操作,就要条件反射地想到,先判断 nxt 是否为 null,否则容易出现空指针异常。

2、注意循环的终止条件。你要知道循环终止时,各个指针的位置,这样才能保返回正确的答案。如果你觉得有点复杂想不清楚,那就动手画一个最简单的场景跑一下算法,比如这道题就可以画一个只有两个节点的单链表 1->2,然后就能确定循环终止后各个指针的位置了。 

1.1合并两个有序链表

. - 力扣(LeetCode)

创建结果链表的哨兵节点 sentinel ;

然后用两个指针遍历两个链表 l1 和 l2,每次将较小的节点添加到结果链表中,直到其中一个链表为空跳出循环。

最后,将剩余的链表直接接到结果链表的尾部,返回 sentinel.next 即可。

速度击败100%

class Solution {
    ListNode mergeTwoLists(ListNode l1, ListNode l2){
        ListNode sentinel = new ListNode(-1, null);
        ListNode l = sentinel;
        while(l1 != null && l2 != null){//
            if(l1.val < l2.val){
                l.next = l1;
                l1 = l1.next;
                l = l.next;
            }
            else{
                l.next = l2;
                l2 = l2.next;
                l = l.next;
            }
        }
        if(l1 != null){
            l.next = l1;
        }
        if(l2 != null){
            l.next = l2;
        }
        return sentinel.next;
    }

}
1.2合并K个升序链表

. - 力扣(LeetCode)

创建结果链表的sentienl,若K == 0则直接返回null,防止创建优先级队列的时候出错;

把这K个升序链表非空链表的头节点放入优先级队列;

循环弹出队列头即当前结点中最小的结点,连接到结果链表上,若该结点不是尾结点,则把它的后一个结点加入优先级队列,直到优先级队列为空。返回sentinel.next;

class Solution {
    public ListNode mergeKLists(ListNode[] lists){
        if(lists.length == 0)//必须有,否则创建优先级队列的时候会出错
            return null;
        ListNode sentinel = new ListNode(-1, null);
       PriorityQueue<ListNode> pq = new PriorityQueue<>(lists.length,  (a, b)->(a.val - b.val));
        for(int i = 0; i < lists.length; i++){
            if(lists[i] != null)
                pq.add(lists[i]);
        }
        ListNode l = sentinel;
        while(!pq.isEmpty()){
            l.next = pq.poll();
            l = l.next;
            if(l.next != null){
                pq.add(l.next);
            }
        }
        return sentinel.next;
    }
}
1.3根据特定值分割链表

. - 力扣(LeetCode)

创建两个哨兵节点 sentinelSmall 和 sentinelBig,分别用于连接小于 x 和大于等于 x 的节点。

遍历链表,将小于 x 的节点添加到 sentinelSmall 链表中,其他节点添加到 sentinelBig 链表中。

把sentinelBig链表的结尾置为null,防止出现环,最后再将 sentinelBig 链表链接到 sentinelSmall 链表后,返回 sentinelSmall.next 作为结果。

/* 创建两个senitnel,分别连接比x小的和比x大的结点,最后把这两个链表连接起来
     注意:必须把大的那个链表结尾置为null,防止出现环!*/
class Solution {
    public ListNode partition(ListNode head, int x){
      ListNode sentinelSmall = new ListNode(-1, null);
      ListNode sentinelBig = new ListNode(-1, null);
      ListNode lS = sentinelSmall, lB = sentinelBig;
      while(head != null){
        if(head.val < x){
            lS.next = head;
            head = head.next;
            lS = lS.next;
        }
        else{
            lB.next = head;
            head = head.next;
            lB = lB.next;
        }
      }
      lB.next = null;//注意:
      lS.next = sentinelBig.next;
      return sentinelSmall.next;
    }
}

二、快慢指针

2.1删除倒数第k个结点

. - 力扣(LeetCode)

要删除倒数第n个结点,就要找到倒数第n+1个结点:

用两个指针遍历,让第一个指针先走n+1步后,另一个再开始同步遍历,直到第一个指针到走null 就找到了倒数第N+1个结点

需要注意的是:删除结点必须要有sentinel结点,因为有可能要删除的是头节点!!所以应将两个指针初始化为sentinel,最后返回sentinel.next;

/*要删除倒数第n个结点,就要找到倒数第n+1个结点:
  用两个指针遍历,第一个指针先走n+1步后,另一个再开始同步遍历,直到第一个指针到null 
   注意:删除结点必须要有sentinel,因为有可能要删除的是头节点!!*/
class Solution {
    public ListNode removeNthFromEnd(ListNode head, int k){
        ListNode sentinel = new ListNode(-1, head);
        ListNode l1 = sentinel;
        ListNode l2 = sentinel;
        for(int i = 0; i < k+1; i++){
            l1 = l1.next;
        }
        while(l1 != null){
            l1 = l1.next;
            l2 = l2.next;
        }
        l2.next = l2.next.next;
        //此时因为倒数第n个结点没有了reference,所以会被java清理掉
        return sentinel.next;
    }
}
2.2返回单链表的中间结点(若有两个中间结点返回第二个)

. - 力扣(LeetCode)

   法1:使用sentinel结点统一

快慢指针同时同地开始遍历,一次fast走两步/一步,slow走一步,直到fast走到null,快慢指针走的次数应相同。注❗️:这里的一次指的是代码1中的一次循环,该写法只要fast不为null就会进入循环即会至少走一步。

若没有虚拟结点sentienl,那么:

[1, 2, 3, 4, 5, null]: fast应走5步到null(即3次),slow应走2步(即2次)

[1, 2, 3, 4, 5, 6, null]: fast应走6步到null(即3次),slow应该走3步(即3次)

第一种偶数个情况的快慢指针走的次数不同,所以不能同时从头节点出发,若加上sentienl,就是:

第一种就是fast走6步->3次,slow走3次->3步,就对了

第二种就是fast走7步->4次, slow走4次->4步,也对了

所以该题只需添加sentinel结点,快慢指针同时从sentinel结点开始遍历就能统一奇偶数的情况。

若想要返回第一个中间结点:

[ 1, 2, 3, 4, 5, null]:fast应走5步到null(即3次),slow应走2步(即2次)

[1, 2, 3, 4, 5, 6, null]: fast应走6步(即3次),slow应该走2步(即2次)

那么就不需sentinel结点,快指针先走一次2步,然后慢指针从头节点出发再一起遍历。

class Solution {
    public ListNode middleNode(ListNode head) {
       ListNode sentinel = new ListNode(-1, head);
       ListNode fast = sentinel;
       ListNode slow = sentinel;
       while(fast != null){
        fast = fast.next;
        if(fast != null)
            fast = fast.next;
        slow = slow.next;
       }
       return slow;
    }
}
  法2  可判断结点是奇数/偶数个

但循环也有另一种写法:这种就不需要sentinel结点,且可以判断该链表的结点是奇数个还是偶数个!

while(fast != null && fast.next != null){
    slow = slow.next;
    fast = fast.next.next;
}//此循环执行一次fast必走2步;。
if(fast != null)//说明该链表有奇数个结点,slow,fast还需继续往后走一步;
{    slow = slow.next;
     fast = fast.next;
}
2.3找到链表的环入口点

. - 力扣(LeetCode)

一个链表最多只有一个环

  快慢指针同时从头节点出发,设到达相遇点时,慢指针走了k步,那么快指针就多走了k步,再设相遇点到环入口点为x步,那么从快指针的角度,可以得到从相遇点走k-x步就到了环入口点,因为:

快指针比慢指针多走的步数就是在环里打转,也即k步是环长度的整数倍,因此从相遇点走k-x步就是走环的整数倍再减去x步,也就是走到了环入口点。

 同时显然,若到达相遇点后慢指针回到头节点,走k-x步,也是到了入口点。

 因此,到达相遇点后,慢指针回到头节点,快指针依然在相遇点,然后两个指针同时同速度出发,再次相遇时就是环入口点(都走了k-x步)。7.

public class Solution {
    public ListNode detectCycle(ListNode head) {
        if(head == null)
            return null;
        ListNode fast = head;
        ListNode slow = head;
        while(fast != null){
            fast = fast.next;
            if(fast == null)//防止接下来的fast.next出现nullpointerError
                return null;
            fast = fast.next;//
            slow = slow.next;
            if(slow == fast)
                break;
        }
        if(fast == null)
            return null;
        slow = head;
        while(fast != slow){
            slow = slow.next;
            fast = fast.next;
        }
        return slow;//or fast;
    }
}
2.4判断两个链表是否有相交结点并返回

. - 力扣(LeetCode)

把A链表的尾结点与B链表的头结点相连,就变成了判断是否有环和若有环则寻找环入口结点的问题,即若连接后不存在环说明没有相交结点。但要特别注意以下三点:

1.fast每走一步前都要先判断是否为尾结点(fast.next == null),若是则需先连接另一个链表;

2.必须保证两链表只能连接一次,否则当两链表没有相交结点的情况下,会因为连接两次出现lastA->headB->lastB->headB链表B整个变成环的情况,导致无法判断两链表没有相交的情况。

那么如何判断没有相交结点:即两链表已经连接过一次,但fast依然出现了为null的情况(不存在环),

因此额外设置一个hasLinked变量标记是否已经连接过链表;

3.题目要求返回结果不改变原链表的结构,所以返回前要还原链表!那就额外用一个变量指向尾结点,在返回前把尾结点.next设置回null;

public class Solution {
    public ListNode getIntersectionNode(ListNode headA, ListNode headB) {
            ListNode fast = headA;
            ListNode slow = headA;
            ListNode lastNode = null;
            boolean hasLinked = false;//
            while(fast != null){
                //若当前fast是尾结点且两链表还未连接过(即当前fast是A链表的尾结点)
                if(fast.next == null && !hasLinked){
                    lastNode = fast;
                    lastNode.next = headB;
                    hasLinked = true;
                }
                fast = fast.next;
                //若已经连接过一次,但又出现fast为null则说明两链表没有相交结点!
                if(fast == null && hasLinked){
                    lastNode.next = null;
                    return null;
                }
                //若当前fast是尾结点且两链表还未连接过(即当前fast是A链表的尾结点)
                if(fast.next == null && !hasLinked){
                    lastNode = fast;
                    lastNode.next = headB;
                    hasLinked = true;
                }
                fast = fast.next;
                slow = slow.next;
                if(slow == fast)
                    break;
            }
        //必须要有这个判断,因为循环结束有两种可能,要么fast == null要么slow == fast !=null
            if(fast == null && hasLinked){
                    lastNode.next = null;
                    return null;
                }
            slow = headA;
            while(fast != slow){
                slow = slow.next;
                fast = fast.next;
            }
            lastNode.next = null;
            return slow;//or fast;

    }
}

三、反转链表相关

3.1基础:反转整个链表:

. - 力扣(LeetCode)

3.1.1三指针迭代法 

初始化三个指针:pre为null, cur为head, nxr为head.next,所以要先判断head(以及head.next)是否为null,若是则直接返回head.

利用while循环,把cur.next指向pre, pre, cur, nxt都往后移一个,注意要先判断nxt不为null,才能将nxt往后移(即若出现nxt.next,必须先判断nxt不为null,否则会出现nullpointerError),直到cur为null结束循环,最后返回pre;

class Solution {
    public ListNode reverseList(ListNode head) {
        if(head == null || head.next == null) return head;
        ListNode pre, cur, nxt;
        pre = null;
        cur = head;
        nxt = head.next;
        while(cur != null){
            cur.next = pre;
            pre = cur;
            cur = nxt;
            if(nxt != null){
                nxt = nxt.next;
            }
        }
        return pre;
    }
}
3.1.2 递归法

对于1->2->3->4->null这样的单链表,递归函数应该做两件事:

1.先递归反转除第一个结点组成的子链表,得到1->reverseList(2->3->4->null),注意head.next依然指向原来的第二个结点,如图:

2.再把第一个结点改连接到最后:

base case:当前需要处理的链表为null或者只有一个结点;

必须先反转子链表,再改连接第一个结点,因为最终连接的顺序是2->1

class Solution {
    public ListNode reverseList(ListNode head) {
        if(head == null || head.next == null) //
            return head;
        ListNode List = reverseList(head.next);
        head.next.next = head;
        head.next = null;
        return List;
    }
}
3.2 反转固定区间【left, right]的结点

. - 力扣(LeetCode)

  3.2.1总思路:

  将第left个结点作为头节点组成子链表,反转它的前right-left+1个结点,所以我们可以写一个辅助方法reverseN即反转链表的前n个结点method并返回反转后的头结点,

   最后再把left结点的前一个结点.next指向返回的头结点, 所以需要记录第left-1个结点,也因此当left是头结点即left==1时要记录它的前一个结点,就需要有sentinel结点了 , 最后返回sentinel.next。

3.2.2基础进阶:反转链表的前n个结点 reverseN(ListNode head,  int n)

数据保证了n<=链表长度

使用三指针迭代法:当cur处理到第n+1个结点时跳出循环,再把head.next指向cur结点,同时把head指向pre结点 ,最后返回head。

使用递归法: 

class Solution {
    /* 反转链表的前n个结点  数据保证了n<=链表长度
      使用三指针迭代法,当cur处理到第n+1个结点时跳出循环,再把head.next指向cur结点,同时把head指向pre结点 ,最后返回head。 */
    private ListNode reverseN(ListNode head, int n){
        if(head == null || head.next == null) 
            return head;
        ListNode pre = null, cur = head, nxt = head.next;
        int cnt = 1;
        while(cnt <= n){//cnt==n时进入循环处理的是第n个结点
            cur.next = pre;
            pre = cur;
            cur = nxt;
            if(nxt != null){
                nxt = nxt.next;
            }
            cnt ++;
        }
        head.next = cur;
        head = pre;
        return head;
    }
    /* 将第left个结点作为头节点组成子链表,反转它的前right-left+1个结点,所以我们可以写一个辅助方法即反转链表的前n个结点method并返回反转后的头结点,
   最后再把left结点的前一个结点.next指向返回的头结点, 所以需要记录left的前一个结点,
    也因此当left是头结点的,这时就需要有sentinel结点了 .最后返回sentinel.next. */
    public ListNode reverseBetween(ListNode head, int left, int right) {
         ListNode sentinel = new ListNode(-1, head);
         ListNode preLeft = sentinel;
         for(int i = 1; i <= left-1; i++){
            preLeft = preLeft.next;
         }
         preLeft.next = reverseN(preLeft.next, right-left+1);
         return sentinel.next;
        }
    }

3.3 以K个一组依次反转链表

若最后一组个数少于k则不予改变

. - 力扣(LeetCode)

从头节点开始,利用reverseBetween(head, int left, int right),套一层for循环,初始化left = 1, right = left+k-1 ;循环步长为k, left, right同时增加;当right > 链表长度时结束,所以要先遍历计算长度:

/*从头节点开始,利用reverseBetween(head, int left, int right),套一层for循环,初始化left = 1, right = left+k-1 ;循环步长为k, left, right同时增加;当right > 链表长度时结束,所以要先遍历计算长度*/
    public ListNode reverseKGroup(ListNode head, int k) {
        ListNode p = head;
        int n = 0;
        while(p != null){
            n ++;
            p = p.next;
        }
        for(int left = 1, right = left+k-1; right <= n; left+= k, right += k){
         head = reverseBetween(head, left, right);   
        }
        return head;
    }

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值