【算法系列篇】与链表相关的算法

在这里插入图片描述

前言

链表是我们在日常生活中使用较为广泛的一种数据结构,链表因为其可扩展性高和方便插入、删除的特性在一些领域发挥着很大的作用。但是因为链表独特的结构,在内存上的逻辑连续而不是物理连续的特性,也使得当一些算法的作用对象是链表的时候就有些许的差别,那么今天我将为大家分享关于链表的一些常用算法。

在这里插入图片描述

1. 两数相加

https://leetcode.cn/problems/add-two-numbers/?envType=list&envId=9Lulrn6r

1.1 题目要求

给你两个 非空 的链表,表示两个非负的整数。它们每位数字都是按照 逆序 的方式存储的,并且每个节点只能存储 一位 数字。

请你将两个数相加,并以相同形式返回一个表示和的链表。

你可以假设除了数字 0 之外,这两个数都不会以 0 开头。

示例 1:
在这里插入图片描述

输入:l1 = [2,4,3], l2 = [5,6,4]
输出:[7,0,8]
解释:342 + 465 = 807.

示例 2:

输入:l1 = [0], l2 = [0]
输出:[0]

示例 3:

输入:l1 = [9,9,9,9,9,9,9], l2 = [9,9,9,9]
输出:[8,9,9,9,0,0,0,1]

提示:

每个链表中的节点数在范围 [1, 100] 内
0 <= Node.val <= 9
题目数据保证列表表示的数字不含前导零
/**
 * 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 addTwoNumbers(ListNode l1, ListNode l2) {
    
    

    }
}

1.2 做题思路

这个题目用的就是模拟算法的思路,就像平时我们加法一样,将两个数从个位开始相加,并且注意有没有进位,一直加到最后一位就可以了。这道题目给的链表是倒序的,所以我们可以直接从链表的头结点开始相加,用一个变量来记住进位,每次相同位相加然后再加上这个进位就得到了该位的相加结果。

在这里插入图片描述
当某一个链表提前到达尾部的时候,我们不停止相加,而是将这个为null的节点当作0与另一个节点相加,并且还需要注意的是,当两个链表都到达尾部的时候,如果进位不为0的话,就还需要创建一个节点记录这个进位。

1.3 Java代码实现

/**
 * 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 addTwoNumbers(ListNode l1, ListNode l2) {
    
    
        ListNode head = new ListNode();
        ListNode cur = head;
        int t = 0; //用来表示进位
        while(l1 != null || l2 != null || t != 0) {
    
    
            int sum = (l1 == null ? 0 : l1.val) + (l2 == null ? 0 : l2.val) + t;
            ListNode node = new ListNode(sum % 10);
            cur.next = node;
            cur = cur.next;
            t = sum / 10;

            l1 = (l1 == null ? null : l1.next);
            l2 = (l2 == null ? null : l2.next);
        }

        return head.next;
    }
}

在这里插入图片描述
这里为什么会选择创建一个head节点呢?这个节点类似于一个哨兵位,有了这个哨兵位就可以减少当返回的链表头结点为空时的判断。做链表相关的题目的时候,我们不要吝啬内存空间,通过创建额外的节点,可以省去很多不必要的麻烦。
在这里插入图片描述

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

https://leetcode.cn/problems/swap-nodes-in-pairs/?envType=list&envId=9Lulrn6r

2.1 题目要求

给你一个链表,两两交换其中相邻的节点,并返回交换后链表的头节点。你必须在不修改节点内部的值的情况下完成本题(即,只能进行节点交换)。

示例 1:
在这里插入图片描述

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

示例 2:

输入:head = []
输出:[]

示例 3:

输入:head = [1]
输出:[1]

提示:

链表中节点的数目在范围 [0, 100] 内
0 <= Node.val <= 100
/**
 * 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) {
    
    

    }
}

2.2 做题思路

这道题目每两个相邻的节点进行交换去,而不是整个链表的逆序,所以每次交换的时候就需要知道交换的是哪两个节点,并且还需要记住这两个节点的后一个节点,否则当我们交换完成之后,就不知道后面节点的内容了。
在这里插入图片描述

2.3 Java代码实现

/**
 * 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 || head.next == null) return head;
        ListNode newHead = new ListNode();
        ListNode prev = newHead, cur = head, next = head.next, nnext = next.next;
        //当链表的节点数为偶数的时候,循环结束的条件就是cur为null;
        //如果节点数为奇数的时候,循环结束的条件是next为null
        while(cur != null && next != null) {
    
    
            //交换相邻两个链表
            prev.next = next;
            next.next = cur;
            cur.next = nnext;

            //更新prev cur next 和nnext的信息
            prev = cur;
            cur = nnext;
            if(cur != null) next = cur.next;
            if(next != null) nnext = next.next;
        }

        return newHead.next;
    }
}

在这里插入图片描述

3. 重排链表

https://leetcode.cn/problems/reorder-list/?envType=list&envId=9Lulrn6r

3.1 题目要求

给定一个单链表 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, 5 * 104]
1 <= node.val <= 1000
/**
 * 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) {
    
    

    }
}

3.2 做题思路

根据题目的意思:需要将链表 L0 → L1 → … → Ln - 1 → Ln 重排列为 L0 → Ln → L1 → Ln - 1 → L2 → Ln - 2 → …,也就是第一个节点后面跟最后一个节点、然后最后一个节点后面跟第二个节点、第二个节点后面跟倒数第二个节点……但是用代码该怎么解决呢?因为链表在内存中的位置是随机的,我们不能够实现对链表的随机访问,那么该如何访问到第一个节点和最后一个节点并且同时还能记住第二个节点的位置呢?

我们可以将链表的后半部分给倒序,然后在前半部分的两个节点之间依次插入后半部分的每个节点。

在这里插入图片描述

3.3 Java代码实现

/**
 * 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) {
    
    
        //1.找到链表的中间节点
        ListNode slow = head, fast = head, thead = head;
        while(fast != null && fast.next != null) {
    
    
            slow = slow.next;
            fast = fast.next.next;
        }

        //将前半部分的最后一个节点的next置为null,防止出现环
        ListNode tmp = slow.next;
        slow.next = null;
        slow = tmp;
        
        //2.翻转slow后面部分的链表
        fast = null;
        while(slow != null) {
    
    
            ListNode next = slow.next;
            slow.next = fast;
            fast = slow;
            slow = next;
        }
        
        //3.将后半部分的链表依次加入到前半部分的两个节点之间
        slow = fast; //将slow更改为后半部分倒置后链表的第一个节点
        ListNode cur = thead;
        while(slow != null) {
    
    
            ListNode nextCur = cur.next, nextSlow = slow.next;
            slow.next = nextCur;
            cur.next = slow;
            cur = nextCur;
            slow = nextSlow;
        }
    }
}

在这里插入图片描述

这里为什么会选择翻转slow后面的部分,而不是从slow开始的后面的链表翻转呢?从slow开始到后面的链表部分翻转是可以的,但是这样会导致前半部分的链表长度短于后半部分,要想不丢失节点,就需要做出额外的判断。
在这里插入图片描述

4. 合并 k 个升序链表

https://leetcode.cn/problems/vvXgSW/?envType=list&envId=9Lulrn6r

4.1 题目要求

给定一个链表数组,每个链表都已经按升序排列。

请将所有链表合并到一个升序链表中,返回合并后的链表。

示例 1:

输入:lists = [[1,4,5],[1,3,4],[2,6]]
输出:[1,1,2,3,4,4,5,6]
解释:链表数组如下:
[
  1->4->5,
 1->3->4,
  2->6
]
将它们合并到一个有序链表中得到。
1->1->2->3->4->4->5->6

示例 2:

输入:lists = []
输出:[]

示例 3:

输入:lists = [[]]
输出:[]

提示:

k == lists.length
0 <= k <= 10^4
0 <= lists[i].length <= 500
-10^4 <= lists[i][j] <= 10^4
lists[i] 按 升序 排列
lists[i].length 的总和不超过 10^4
/**
 * 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 mergeKLists(ListNode[] lists) {
    
    

    }
}

4.2.1 做题思路一

这个题目要求是合并 k 个升序链表,跟合并两个升序链表是类似的,但是如果我们单纯使用合并两个升序链表的方法来解决这个 k 个升序链表的话,时间复杂度是比较高的,那么是否有办法可以降低时间复杂度呢?

这道题目有两个方法可以降低时间复杂度:

  1. 使用优先级队列。创建一个小根堆,先将每个链表的头结点放入优先级队列中,然后取出堆顶的节点(堆顶的节点就是当前堆中所有元素中最小的),因为链表是升序排列的,所以第一个取出的就是所有链表中最小的元素,然后取出的这个节点属于哪个链表,就继续将该链表的下一个节点放入优先级队列中,继续该操作。

在这里插入图片描述

4.3.1 方法一Java代码实现

/**
 * 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 mergeKLists(ListNode[] lists) {
    
    
        PriorityQueue<ListNode> heap = new PriorityQueue<>((v1, v2) -> v1.val - v2.val);
        //将每个链表的头节点放入优先级队列中
        for(ListNode l : lists) {
    
    
            if(l != null) {
    
    
                heap.offer(l);
            }
        }

        ListNode ret = new ListNode();
        ListNode cur = ret;
        while(!heap.isEmpty()) {
    
    
            //将堆顶的节点添加到返回链表中
            ListNode tmp = heap.poll();
            ListNode node = new ListNode(tmp.val);
            cur.next = node;
            cur = cur.next;
            //插入取出链表的下一个节点
            if(tmp.next != null) heap.offer(tmp.next);
        }

        return ret.next;
    }
}

在这里插入图片描述

4.2.2 做题思路二

因为前面我们知道如何合并两个有序的升序链表,但是如果使用暴力解法合并两个链表的话,时间复杂度会很高,那么我们是否有方法还是使用合并两个有序链表的思想,但是时间复杂度不至于很高的方法呢?也就是说如何用合并两个升序链表的方法合并 k 个升序链表呢?当然是有的,这种思想就是将大事化小的思想,而归并的思想也正是将大事化小,所以这个题的第二种方法就是使用归并的方法解决。

在这里插入图片描述

4.3.2 方法二Java代码实现

/**
 * 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 mergeKLists(ListNode[] lists) {
    
    
        return merge(lists, 0 ,lists.length - 1);
    }

    private ListNode merge(ListNode[] lists, int left, int right) {
    
    
        //当left > right 的时候,说明传入的lists不和逻辑
        if(left > right) return null;
        //如果left == right,说明lists中只有一个链表,直接返回
        if(left == right) return lists[left]; 
        int mid = left + (right - left) / 2;

        //递归
        ListNode l1 = merge(lists, left, mid);
        ListNode l2 = merge(lists, mid  + 1, right);

        //合并连个有序链表
        return mergeTwoLists(l1,l2);
    }

    private ListNode mergeTwoLists(ListNode l1, ListNode l2) {
    
    
        if(l1 == null) return l2;
        if(l2 == null) return l1;

        ListNode head = new ListNode();
        ListNode cur = head; 
        while(l1 != null && l2 != null) {
    
    
            if(l1.val < l2.val) {
    
    
                cur.next = l1;
                l1 = l1.next;
            }else {
    
    
                cur.next = l2;
                l2 = l2.next;
            }
            cur = cur.next;
        }

        if(l1 != null) cur.next = l1;
        if(l2 != null) cur.next = l2;

        return head.next;
    }
}

在这里插入图片描述

5. k 个一组翻转链表

https://leetcode.cn/problems/reverse-nodes-in-k-group/

5.1 题目要求

给你链表的头节点 head ,每 k 个节点一组进行翻转,请你返回修改后的链表。

k 是一个正整数,它的值小于或等于链表的长度。如果节点总数不是 k 的整数倍,那么请将最后剩余的节点保持原有顺序。

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

示例 1:
在这里插入图片描述

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

示例 2:
在这里插入图片描述

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

提示:

链表中的节点数目为 n
1 <= k <= n <= 5000
0 <= Node.val <= 1000
/**
 * 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) {
    
    

    }
}

进阶:你可以设计一个只用 O(1) 额外内存空间的算法解决此问题吗?

5.2 做题思路

这道题目的要求是 k 个一组翻转链表,其实本质上还是翻转链表,只是将一个链表分成了 n 组,每组 k 个节点进行翻转。那么我们可以先求出要翻转多少组链表,然后每一组进行翻转链表的操作。
在这里插入图片描述

5.3 Java代码实现

/**
 * 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) {
    
    
        //1.统计出需要翻转多少组

        //先预处理一遍,统计出链表节点的个数
        int n = 0;
        ListNode cur = head;
        while(cur != null) {
    
    
            n++;
            cur = cur.next;
        }
        n /= k;  //n就是需要翻转的组数

        //翻转这n组链表
        ListNode newHead = new ListNode();
        ListNode prev = newHead;
        cur = head;
        for(int i = 0; i < n; i++) {
    
    
            ListNode tmp = cur;
            //每组翻转 k 个节点
            for(int j = 0; j < k; j++) {
    
    
                ListNode next = cur.next;
                cur.next = prev.next;
                prev.next = cur;
                cur = next;
            }
            //当翻转完成一组之后,将prev更新为翻转完成之后的最后一个节点
            prev = tmp;
        }

        prev.next = cur;
        return newHead.next;
    }
}

在这里插入图片描述

总结

在做链表相关的算法题的时候,我们不要吝啬空间的浪费,额外创建一个虚拟节点可以省去很多不必要的麻烦;因为链表的内存的随机的而不是连续的,所以需要注意保存下一个节点的位置,否则就是导致后面部分的节点丢失。

猜你喜欢

转载自blog.csdn.net/m0_73888323/article/details/133298096