线性表基础:链表及经典问题(二)经典算法题刷题(链表的反转、链表的节点删除)

紧接上一章

3.2 链表的反转

3.2.1 LeetCode #206 反转链表

题目描述
反转一个单链表。

进阶:你可以迭代或递归地反转链表。你能否用两种方法解决这道题?
在这里插入图片描述

解题思路:

  • 思路1:迭代反转
  • 思路2:递归反转,一次拆掉一个节点并递归处理剩余的子链表

解法一:迭代反转

定义三个指针:
pre指针指向的是已翻转链表的头节点
cur指针指向的是未翻转部分的头节点
next指针指向的是未翻转部分的头节点的下一个节点
在这里插入图片描述

  • 翻转节点
    • 将cur指针所指向的节点指向pre指针所指向的节点
    • 然后移动pre指针到cur所在的位置,移动cur指针到next所在位置,next指针移动到cur指针所指向节点的下一个节点,此时已经翻转了第一个节点
  • 循环翻转节点的操作,直到当cur指针指向null时,说明已经完成整个链表的翻转
/**
 * 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) {
    
    
        if (head == null) {
    
    
            return null;
        }
        ListNode pre = null;//已经翻转链表的头节点
        ListNode cur = head;//待翻转链表的头节点
        ListNode next = head.next;//待翻转链表头节点下一个节点
        while (cur != null) {
    
    
            cur.next = pre;
            pre = cur;
            cur = next;
            next = cur == null ? null : cur.next;
        }
        return pre;
    }
}

解法二:递归反转

递归思想:先递归遍历整个链表的最后一个节点,然后往回返的过程中在处理,往回返的过程其实就是逆序的遍历每个节点,递归的回溯过程其实就相当于在逆序遍历链表

/**
 * 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) {
    
    
        if(head == null || head.next == null){
    
    
            //如果链表为null或者只有一个节点,直接返回该头节点,不需要翻转
            return head;
        }
        //否则翻转,但是只处理head一个节点,将head.next委托递归给reverseList
        ListNode tail = head.next;//记录将来已翻转区域的尾节点
        ListNode newHead = reverseList(head.next);//将当前节点next后面的链表翻转,返回翻转后的头节点

        tail.next = head;//将已翻转区域的尾节点接上当前节点
        head.next = null;
        return  newHead;
    }
}

就性能来说递归方式没有迭代方式好,但是我们主要是为了锻炼编码能力

扩展:添加参数控制反转链表要翻转的节点个数

实例,比如:
输入: 1->2->3->4->5->NULL,我要翻转前3个节点
输出: 3->2->1->4->5->NULL

public class LeetCode206_Ext {
    
    
    public static void main(String[] args) {
    
    
        ListNode head = new ListNode(1);
        head.next = new ListNode(2);
        head.next.next = new ListNode(3);
        head.next.next.next = new ListNode(4);
        head.next.next.next.next = new ListNode(5);

        ListNode pre = head;//专门定义一个指针遍历
        while (pre != null) {
    
    
            System.out.print(pre.val);
            pre = pre.next;
        }

        System.out.println("\n---");

        ListNode newHead = reverseListNum(head, 3);
        while (newHead != null) {
    
    
            System.out.print(newHead.val);
            newHead = newHead.next;
        }
    }

    public static ListNode reverseListNum(ListNode head, int num) {
    
    
        if (head == null || head.next == null || num == 1) {
    
    
            //如果链表为null或者只有一个节点,直接返回该头节点,不需要翻转
            //或者超过指定数量
            return head;
        }
        //否则翻转,但是只处理head一个节点,将head.next委托递归给reverseList
        ListNode tail = head.next;//记录将来已翻转区域的尾节点,注意是 "已翻转区域的尾节点"
        ListNode newHead = reverseListNum(head.next, num - 1);//将当前节点next后面的链表翻转,返回翻转后的头节点

        ListNode pre = tail.next;//此时需要定义一个指针维护已翻转区域尾节点后面没有翻转的节点
        //因为head.next后面的链表区域并不是翻转所有节点
        tail.next = head;//将已翻转区域的尾节点接上当前节点
        head.next = pre;//将当前节点接上后面没有翻转的节点
        return newHead;
    }
}

3.2.2 LeetCode #92 反转链表II

题目描述
反转从位置 m 到 n 的链表。请使用一趟扫描完成反转。

说明:1 ≤ m ≤ n ≤ 链表长度。
在这里插入图片描述

解题思路:

  • 技巧:使用虚拟头结点(dummy head)
    • 因为如果从头节点开始翻转,那么链表的首地址会发生改变,为了简化临界条件判断,添加一个虚拟节点
  • 思路1:迭代反转
    • LeetCode206我们知道翻转一个链表最少要3个指针,现在我们要做的就是在翻转前先确定要翻转的链表区域
  • 思路2:递归反转
    • LeetCode92扩展中我们已经实现了递归翻转指定个数的前几个节点,已经确定了要翻转区域的尾节点,在这个的基础上只要再确定从哪开始翻转就可以了

解法一:迭代反转

LeetCode206我们知道翻转一个链表最少要3个指针:

  • pre指针指向的是已翻转链表的头节点
  • cur指针指向的是未翻转部分的头节点
  • next指针指向的是未翻转部分的头节点的下一个节点

现在我们要做的就是在翻转前先确定要翻转的链表区域,我们需要额外定义2个指针:

  • start为反转区域翻转后的前驱连接节点
  • end为反转区域翻转后的后驱连接节点

在这里插入图片描述

首先创建虚拟头节点,然后确定翻转区域

  • 定义start指针指向虚拟头节点,向后移动m-1个节点
  • 定义end指针指向虚拟头节点,向后移动n+1个节点

翻转待翻转区域

  • 定义pre指针,指向end指针指向的节点,为待反转区域翻转后的头节点
  • 定义cur指针为start.next指向的节点,为未翻转部分的头节点
  • 定义next指针为cur.next指向的节点,为未翻转部分的头节点的下一个节点

按照206翻转逻辑循环翻转,直到cur指针到达了end指针结束循环,完成翻转。

翻转完成以后将start指针指向翻转区域的头节点即可。

/**
 * 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 reverseBetween(ListNode head, int left, int right) {
    
    
        ListNode hair = new ListNode(0, head);//定义虚拟节点
        //确定翻转范围
        ListNode start = hair;
        ListNode end = hair;
        int pace = left - 1;//定义start指针的步伐
        while (pace > 0) {
    
    
            start = start.next;
            pace--;
        }
        pace = right - left + 2;//end指针步伐
        end = start;//end从start开始走,少走几步
        while (pace > 0) {
    
    
            end = end.next;
            pace--;
        }
        //确定范围以后开始翻转
        ListNode pre = end;
        ListNode cur = start.next;
        ListNode next = cur.next;
        while (cur != end) {
    
    
            cur.next = pre;
            pre = cur;
            cur = next;
            next = cur == null ? null : cur.next;
        }
        //翻转完毕后
        start.next = pre;

        return hair.next;
    }
}

解法二:递归反转

递归思路:之前我们已经实现了一个函数可以指定反转链表的头n位,现在我们只需要找到待反转区域的前一位,然后再反转待反转区域的头n-m+1个节点,最后将待反转区域的前一位节点指向已经反转完成的区域的头节点

/**
 * 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 reverseBetween(ListNode head, int left, int right) {
    
    
        //先找到待反转区域的前一个节点,有可能反转第一个节点,所以创建一个虚拟头节点
        ListNode hair = new ListNode(0, head);

        ListNode pre = hair;//pre为待反转区域前一个节点
        int num = right - left + 1;
        while (--left > 0) {
    
    
            pre = pre.next;
        }
        pre.next = reverseListNum(pre.next, num);
        return hair.next;
    }

    public ListNode reverseListNum(ListNode head, int num) {
    
    
        if (head == null || head.next == null || num == 1) {
    
    
            //如果链表为null或者只有一个节点,直接返回该头节点,不需要翻转
            //或者超过指定数量
            return head;
        }
        //否则翻转,但是只处理head一个节点,将head.next委托递归给reverseList
        ListNode tail = head.next;//记录将来已翻转区域的尾节点,注意是 "已翻转区域的尾节点"
        ListNode newHead = reverseListNum(head.next, num - 1);//将当前节点next后面的链表翻转,返回翻转后的头节点

        head.next = tail.next;//将当前节点接上后面没有翻转的节点
        //因为head.next后面的链表区域并不是翻转所有节点
        tail.next = head;//将已翻转区域的尾节点接上当前节点
        return newHead;
    }
}

3.2.3 LeetCode #25 K个一组翻转链表

题目描述
给你一个链表,每 k 个节点一组进行翻转,请你返回翻转后的链表。

k 是一个正整数,它的值小于或等于链表的长度。

如果节点总数不是 k 的整数倍,那么请将最后剩余的节点保持原有顺序。

进阶:
你可以设计一个只使用常数额外空间的算法来解决此问题吗?
你不能只是单纯的改变节点内部的值,而是需要实际进行节点交换。
在这里插入图片描述

解题思路:

  • 思路:先判断是否有K个元素,有的话对这K个节点进行反转 然后拼接一下首尾部分,然后再循环判断剩余未翻转区域是否还有K个元素,有的话继续重复翻转操作,拼接首尾节点,直到不满足K个元素

迭代解法:

和上一题类似,首先翻转一个链表最少要3个指针:

  • pre指针指向的是已翻转链表的头节点
  • cur指针指向的是未翻转部分的头节点
  • next指针指向的是未翻转部分的头节点的下一个节点

其次需要额外定义2个指针,在翻转前先确定待翻转的链表区域:

  • start为反转区域翻转后的前驱连接节点
  • tail为反转区域翻转前的反转区域内的最后一节点(需要tail判空决定是否有K个节点)
  • end为反转区域翻转后的后驱连接节点

这里判断区域,只需要满足从头开始每K个节点为一个区域进行翻转即可,不满足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 {
    
    
    public ListNode reverseKGroup(ListNode head, int k) {
    
    
        if (head == null) {
    
    
            return head;
        }
        //创建虚拟头节点
        ListNode hair = new ListNode(0, head);
        //先判断是否有K个节点
        ListNode start = hair;
        ListNode tail = getEndByK(start, k);
        while (tail != null) {
    
    
            //tail 不为null说明满足K个节点
            ListNode pre = tail.next;
            ListNode cur = start.next;
            ListNode newTail = cur;
            ListNode next = cur.next;

            //开始反转
            ListNode end = tail.next;//记录停止反转的边界条件
            while (cur != end) {
    
    
                cur.next = pre;
                pre = cur;
                cur = next;
                next = cur == null ? null : cur.next;
            }
            //反转完成后连接头部
            start.next = pre;

            //继续找下一个K个节点的反转区域
            //start = getEndByK(start, k);
            start = newTail;
            tail = getEndByK(start, k);
        }

        return hair.next;
    }

    /**
     * @param start 待反转区域的前驱连接节点
     * @param k     向后找第几个节点
     * @return
     */
    private ListNode getEndByK(ListNode start, int k) {
    
    
        while (k-- > 0 && start != null) {
    
    
            start = start.next;
        }
        return start;
    }
}

递归解法:

利用LeetCode #206 中已经实现的可以指定反转链表的头n位的函数,只需要循环判断是否有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 {
    
    
    public ListNode reverseKGroup(ListNode head, int k) {
    
    
        ListNode hair = new ListNode(0, head);
        ListNode pre = hair;
        //循环寻找k个节点
        ListNode end = getTail(pre, k);
        while (end != null) {
    
    
            ListNode tail = pre.next;
            ListNode newHead = reverseListNum(pre.next, k);
            pre.next = newHead;
            pre = tail;
            end = getTail(pre, k);
        }

        return hair.next;
    }

    private ListNode getTail(ListNode pre, int k) {
    
    
        while (k-- > 0 && pre != null) {
    
    
            pre = pre.next;
        }
        return pre;
    }

    public static ListNode reverseListNum(ListNode head, int num) {
    
    
        if (head == null || head.next == null || num == 1) {
    
    
            //如果链表为null或者只有一个节点,直接返回该头节点,不需要翻转
            //或者超过指定数量
            return head;
        }
        //否则翻转,但是只处理head一个节点,将head.next委托递归给reverseList
        ListNode tail = head.next;//记录将来已翻转区域的尾节点,注意是 "已翻转区域的尾节点"
        ListNode newHead = reverseListNum(head.next, num - 1);//将当前节点next后面的链表翻转,返回翻转后的头节点

        head.next = tail.next;//此时需要定义一个指针维护已翻转区域尾节点后面没有翻转的节点
        //因为head.next后面的链表区域并不是翻转所有节点
        tail.next = head;//将已翻转区域的尾节点接上当前节点
        return newHead;
    }
}

3.2.4 LeetCode #61 旋转链表

题目描述:
给定一个链表,旋转链表,将链表每个节点向右移动 k 个位置,其中 k 是非负数。
在这里插入图片描述

解题思路

思路:把整个链表首尾相接,然后从头节点向后走 length-(K%length)-1 位(或者从尾节点向后走length-(k%length)),找到新链表Head的前一位,将环拆开即可
在这里插入图片描述

/**
 * 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 rotateRight(ListNode head, int k) {
    
    
        if (head == null) {
    
    
            return null;
        }
        int length = 1;//链表长度
        ListNode tail = head;
        while (tail.next != null) {
    
    
            tail = tail.next;
            length++;
        }
        //连接成环
        tail.next = head;

        k = length - (k % length);

        while (k-- > 0) {
    
    
            tail = tail.next;
        }
        head = tail.next;
        tail.next = null;
        return head;
    }
}

3.2.5 LeetCode #24 两两交换链表中的节点

题目描述
给定一个链表,两两交换其中相邻的节点,并返回交换后的链表。

你不能只是单纯的改变节点内部的值,而是需要实际的进行节点交换。
在这里插入图片描述

解题思路

  • 思路1:与LeetCode #25完全一致,是K = 2的简单情形。(略)
  • 思路2:可以简化不需要那么多指针了,只需要四个指针即可。

思路2解法:

定义4个指针

  • start为反转区域翻转后的前驱连接节点
  • end为反转区域翻转后的后驱连接节点
  • one指针指向的是要翻转的第一个节点
  • two指针指向的是要翻转的第二个节点

在这里插入图片描述

  • 每次按顺序让one指向end,two指向one,start指向two,完成一次翻转
  • 然后移动指针start到one,one到start.next,two到one.next,end到two.next
  • 只要one或者two为null说明不够翻转,结束
/**
 * 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) {
    
    
        if (head == null) {
    
    
            return null;
        }
        ListNode hair = new ListNode(0, head);
        ListNode start = hair;
        ListNode one = null;
        ListNode two = null;
        //ListNode end = null;
        while (start.next != null && start.next.next != null) {
    
    
            one = start.next;
            two = start.next.next;
            one.next = two.next;//省去了end指针
            two.next = one;
            start.next = two;
            //完成翻转
            start = one;
        }
        return hair.next;
    }
}

3.3 链表的节点删除

3.3.1 LeetCode #19 删除链表的倒数第N个节点

题目描述
给你一个链表,删除链表的倒数第 n 个结点,并且返回链表的头结点。

进阶:你能尝试使用一趟扫描实现吗?
在这里插入图片描述

解题思路:

  • 思路一:第一次遍历计算链表length,第二次遍历指针移动 length-n-1步找到要删除节点前一个节点进行删除(略)
  • 思路二:一趟扫描,找到待删除节点待前一个节点(需要虚拟头),需要两个指针一开始相距n个节点,然后一起向后走,当后面指针走到null时,说明前个指针指向的就是要删除节点的前一个节点 ,然后进行删除操作调整指针

解法二:

在这里插入图片描述
在这里插入图片描述

/**
 * 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 hair = new ListNode(0, head);
        ListNode q = hair;
        ListNode p = head;
        while (n-- > 0 && p != null) {
    
    
            p = p.next;
        }
        while (p != null) {
    
    
            p = p.next;
            q = q.next;
        }
        q.next = q.next.next;
        return hair.next;
    }
}

3.3.2 LeetCode #83 删除排序链表中的重复节点

题目描述
给定一个排序链表,删除所有重复的元素,使得每个元素只出现一次。
在这里插入图片描述

解题思路:

定义快慢两个指针指向头节点,判断两个节点值是否相等

  • 如果相等,快指针往后走一步,慢指针不动
  • 如果不相等,将慢指针指向快指针所指向的节点,并将慢指针移到快指针的位置

一直循环操作,直到快指针的下一个指针为null结束。

在这里插入图片描述

一个指针解法:

/**
 * 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 deleteDuplicates(ListNode head) {
    
    
        if (head == null) {
    
    
            return head;
        }
        ListNode p = head;
        while (p.next != null) {
    
    
            if (p.val == p.next.val) {
    
    
                //相等删除下个节点
                p.next = p.next.next;
            } else {
    
    
                p = p.next;
            }
        }
        return head;
    }
}

3.3.3 LeetCode #82 删除排序链表中的重复节点II

题目描述:
给定一个排序链表,删除所有含有重复数字的节点,只保留原始链表中 没有重复出现 的数字。
在这里插入图片描述

解题思路:

  • 需要定义一个虚拟头节点,因为如果头节点重复删除的话头节点地址会发生变化
  • 定义两个指针
    • 定义指针pre,一开始指向虚拟头节点,作为待删除区的前置节点指针,因为现在是重复的全部删除,需要维护删除节点的前一个节点才能进行删除,比较的时候比较pre.next和pre.next.next
    • 定义指针notEqual,当发现

遍历的时候比较cur.val 和 cur.next.val

  • 如果不相等,那么pre移动到cur,cur移动到cur.next
  • 如果相等,pre不动,cur移动到cur.next,继续重复比较

重复上述操作,直到cur.next为null结束。

解法:

/**
 * 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 deleteDuplicates(ListNode head) {
    
    
        if (head == null) {
    
    
            return null;
        }
        ListNode hair = new ListNode(0, head);
        ListNode notEqual = null;//定义一个寻找不相等节点的指针
        ListNode pre = hair;
        while (pre.next != null) {
    
    
            if (pre.next.next != null && pre.next.val == pre.next.next.val) {
    
    
                //相等,寻找下个不相等的节点
                notEqual = pre.next.next;
                while (notEqual != null && notEqual.val == pre.next.val) {
    
    
                    notEqual = notEqual.next;
                }
                //此时unlikeness.next为不相等的节点
                pre.next = notEqual;

            } else {
    
    
                //如果pre后面两个节点值不相等,可以往前走一步
                pre = pre.next;
            }
        }
        return hair.next;
    }
}

3.4 总结

什么情况下才需要使用虚拟头节点?

  • 链表的头地址有可能改变的情况,才考虑用虚头简化操作(比如插入、删除操作,都有可能改变头地址)
  • 虚头的作用简化了链表操作边界条件的处理

猜你喜欢

转载自blog.csdn.net/weixin_41947378/article/details/114807989