每天一道算法题(java数据结构与算法)——>重排链表

Posted stormzhuo

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了每天一道算法题(java数据结构与算法)——>重排链表相关的知识,希望对你有一定的参考价值。

这是LeetCode上的 [026,重排链表],难度为 [中等]

题意

给定一个单链表 L 的头节点 head ,单链表 L 表示为:

L0 → L1 → … → Ln-1 → Ln

请将其重新排列后变为:

L0 → Ln → L1 → Ln-1 → L2 → Ln-2 → …

不能只是单纯的改变节点内部的值,而是需要实际的进行节点交换。

示例1

输入: head = [1,2,3,4]
输出: [1,4,2,3]

示例2

输入: head = [1,2,3,4,5]
输出: [1,5,2,4,3]

题解1(线性表)

思路分析

根据题意,重建链表的规则时,第一个结点指向最后一个结点,最后一个结点指向第一个结点的下一个结点,第一个结点的下一个结点指向最后一个结点的前一个结点,以此类推

因为链表不支持下标访问,所以我们无法随机访问链表中任意位置的元素。

因此比较容易想到的一个方法是,我们利用线性表存储该链表,然后利用线性表可以下标访问的特点,直接按顺序访问指定元素,重建该链表即可。

步骤

  1. 定义一个线性表来存放链表结点,使用集合类ArrayList,里面封装了线性表(动态数组实现)
  2. 遍历链表,遍历过程中把结点添加到线性表中
  3. 定义两个索引 i,j 用于双向遍历线性表,i初始为0,j初始为线性表的长度减一(索引是从0开始,长度是从1开始)
  4. 双向遍历线性表(从头部,尾部同时遍历),终止条件为到达线性表的中点,遍历过程中执行如下操作
    • 把线性表的序号为 i 的结点指向序号为 j 的结点(例如:第一次循环是第一个结点指向最后一个结点)
    • 把 i 的值加1,让它当前结点变成它的下一个结点(例如:第一次循环是第一个结点变成它的下一个结点)
    • 判断 i 是否等于 j,当线性表的长度为偶数时,最后一次循环 i + 1后会等于 j,即到达线性表的中点
    • 把线性表的的序号为 j 的结点指向序号为 i 结点(例如:第一次循环是最后一个结点指向第一个结点的下一个结点,因为 i已经在上一步加 1)
    • 把 j 的值减1,让它当前结点变成它的上一个结点(例如:第一次循环是最后一个结点变成它的上一个结点)
  5. 遍历完后需要把最后一个结点指向null成为尾结点,索引 i 或 j 的值都是最后一个元素

代码实现

public class Solution 

 public void reorderList(ListNode head) 
         // 创建一个线性表,存放链表的结点
        ArrayList<ListNode> list = new ArrayList<>();
        // 遍历链表,遍历过程中把结点添加到线性表中
        while (head != null) 
            list.add(head);
            head = head.next;
        
        // 初始化两个下标,i从线性表头部开始,j从尾部开始
        int i = 0;
        int j = list.size() - 1;
        // 双向同时遍历线性表
        while (i < j) 
            // i的结点指向j的结点,即第一次循环时,第一个结点指向最后一个结点
            list.get(i).next = list.get(j);
            // 让i的结点变成它的下一个结点
            i++;
            // 当线性表长度为偶数时,最后一次循环i+1后会等于j,即到达线性表的中点
            if (i == j) 
                break;
            
            // j的结点指向i的结点(i已在前面加1),即第一次循环时,最后一个结点指向第一个结点的下一个结点
            list.get(j).next = list.get(i);
            // 让j的结点变成它的上一个结点
            j--;
        
        // 让最后一个结点变成尾结点
        list.get(j).next = null;
    

复杂度分析

假设链表长度为n

时间复杂度:

在最初遍历链表添加结点到线性表时,时间复杂度为O(n),在双向遍历线性表时,时间复杂度为O(n/2),故总的时间复杂度为O(n) + O(n/2) = O(n)

空间复杂度:

需要一个线性表来存放链表结点,故空间复杂度为O(n)

题解2(寻找链表中点 + 反转链表)

思路分析

要想不用存储空间的情况下(使用线性表),只能遍历链表来实现,但链表不能通过下标来访问指定的位置,因此需要调整原有链表的结构

根据题意以及题解1,我们已经深刻体会重排链表的特点,例如第一个结点指向最后一个结点,最后一个结点指向第一个结点的下一个结点,以此类推

因此我们可以把链表拆分为两半,后一段链表反转,此时同时遍历两链表,就能访问到指定的位置,例如第一次循环时,两链表都在第一个结点,让前一段链表的第一个结点指向后一段链表的第一结点(已反转),让后一段链表的第一个结点指向前一段链表的第一个结点的下一个结点,以此类推

步骤

第一步

找到原链表的中间结点(在以往文章以讲过,这里不再重复),断开链表

第二步

将原链表的右半段反转(在以往文章以讲过,这里不再重复))

第三步

合并链表

同时遍历两链表,遍历过程中把前一段链表的当前结点指向后一段链表的当前结点,后一段链表的当前结点指向前一段链表的当前结点的下一个结点

循环终止时分为两种情况

当链表长度为偶数时,此时断开的链表前一段链表结点数比后一段链表的结点数多2个,因此遍历完后一段链表时循环终止,但前一段链表还有2个结点

当链表长度为奇数时,此时断开的链表前一段链表结点数比后一段链表的结点多1个

由于在遍历时,我们已经让后一段链表的当前节点指向前一段链表的当前结点的下一个结点,因此链表并不会断开

代码实现

public class Solution 

 public void reorderList(ListNode head) 
       // 获取链表中间结点
        ListNode middleNode = middleNode(head);
        // 中间结点的下一个结点就是后一段链表的头结点
        ListNode tempNode = middleNode.next;
        // 断开链表
        middleNode.next = null;
        // 重组链表
        link(head, reverserList(tempNode));
    

     public ListNode middleNode(ListNode head) 
        ListNode slow = head;
        ListNode fast = head;
        while (fast.next != null && fast.next.next != null) 
            slow = slow.next;
            fast = fast.next.next;
        
        return slow;
    

      public ListNode reverserList(ListNode head) 
        ListNode preNode = null;
        ListNode currNode = head;
        while (currNode != null) 
            ListNode nextNode = currNode.next;
            currNode.next = preNode;
            preNode = currNode;
            currNode = nextNode;
        
        return preNode;
    

     private void link(ListNode node1, ListNode node2) 
        // 存放在遍历链表过程中的当前结点的下一个结点
        ListNode nextNode1 = null;
        ListNode nextNode2 = null;
        // 同时遍历两链表
        while (node1 != null && node2 != null) 
            // 前一段链表的当前结点的下一个结点赋给nextNode1
            nextNode1 = node1.next;
            // 前一段链表的当前结点指向后一段链表的当前结点
            node1.next = node2;
            // 后一段链表的当前结点的下一个结点赋给nextNode2
            nextNode2 = node2.next;
            // 后一段链表的当前结点指向前一段链表的当前结点的下一个结点
            node2.next = nextNode1;
            // 重置当前结点,把当前结点的下一个结点作为当前结点,实现遍历
            node1 = nextNode1;
            node2 = nextNode2;
        
    

复杂度分析

假设链表长度为n

时间复杂度:

在最初找中间结点时,需要遍历链表,时间复杂度为O(n)。在反转后一段链表时,时间复杂度为O(n/2),在同时遍历两断开链表时,时间复杂度为O(n/2),故总的时间复杂度为O(n) + 2O(n/2) = O(n)

空间复杂度:

只声明了几个固定的结点,为常数,空间复杂度为O(1)

以上是关于每天一道算法题(java数据结构与算法)——>重排链表的主要内容,如果未能解决你的问题,请参考以下文章

每天一道算法题(java数据结构与算法)——> 链表的中间结点

每天一道算法题(java数据结构与算法)——>链表中环的入口节点

每天一道算法题(java数据结构与算法)——> 链表中的两数相加

每天一道算法题(java数据结构与算法)——>链表中环的入口节点

每天一道算法题(java数据结构与算法)——>两个链表的第一个公共节点

每天一道算法题(java数据结构与算法)——>删除链表的倒数第 N 个结点