【算法】链表

链表

链表是我们的老朋友的,我们也许对链表的操作聊熟于心,但是对于链表的套路题,我们还真不一定了解

一、基本操作

206-反转链表(必须牢记)

题目

反转一个单链表。

示例:

输入: 1->2->3->4->5->NULL
输出: 5->4->3->2->1->NULL

进阶:
你可以迭代或递归地反转链表。你能否用两种方法解决这道题?

思考

这是链表的经典题目了

我们尝试用递归和迭代两种解法去解

  • 迭代 gif

只要想起这张图,就一定能把代码写出来

20200926222143340

  • 递归

递归的实现要稍微复杂一点

1、假设原本的链表长这个样子

image-20210704135222189

2、在某个中间过程中,已经有部分被倒转了

image-20210704135424088

3、部分代码解释,在图上标明了

image-20210704135446633

题解
  • 递归

在写递归的代码时候,一定要牢记递归解释的2、3张图

 public ListNode reverseList(ListNode head) {
        /**
         * 方法1 递归
         */
        if (head==null || head.next==null) return head;
        ListNode newHead = reverseList(head.next);
        head.next.next=head;
        head.next=null;
        return newHead;
    }
  • 迭代
public ListNode reverseList(ListNode head) {
        /**
         * 方法2 迭代
         * 在做这道题的时候,去想那个 gif
         */
        ListNode pre=null;
        ListNode curr=head;
        while (curr!=null) {
            ListNode tmp = curr.next;
            curr.next=pre;
            pre=curr;
            curr=tmp;
        }
        return pre;
    }

21-合并有序链表

题目

将两个升序链表合并为一个新的 升序 链表并返回。新链表是通过拼接给定的两个链表的所有节点组成的。

示例 1:

img

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

示例 2:

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

示例 3:

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

提示:

  • 两个链表的节点数目范围是 [0, 50]
  • -100 <= Node.val <= 100
  • l1l2 均按 非递减顺序 排列
思考

一样,有递归和非递归两种写法

  • 递归

递归的过程,可以想成成,将两根橡皮泥,搓成一根橡皮泥的过程

  • 非递归

迭代要注意几点

1、记得最后的收尾工作

2、头部可以在一开始设置一个假头部,这样,可以避开每次都要判断头部是否为空的问题

题解
  • 递归
public ListNode mergeTwoLists(ListNode l1, ListNode l2) {
        if (l1==null) return l2;
        if (l2==null) return l1;
        if (l1.val<=l2.val) {
            l1.next=mergeTwoLists(l1.next,l2);
            return l1;
        }
        if (l2.val<l1.val) {
            l2.next=mergeTwoLists(l1,l2.next);
            return l2;
        }
        return null;
    }
  • 非递归
public ListNode mergeTwoLists(ListNode l1, ListNode l2) {
       
        ListNode head=new ListNode(-1);
        ListNode tail=head;
        while (l1!=null && l2!=null) {
            if(l1.val<=l2.val) {
                tail.next=l1;
                l1=l1.next;
            } else {
                tail.next=l2;
                l2=l2.next;
            }
            tail=tail.next;
        }

        // 收尾工作
        tail.next=l1==null?l2:l1;

        return head.next;
    }

24-两两交换链表节点

题目

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

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

示例 1:

img

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

示例 2:

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

示例 3:

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

我的第一反应就是递归(后来发现超越 100%,还是很 nice 的)

这里要注意,要定义一个 fake 节点,用来指向头结点

因为 head 指针在交换的过程中,会变化位置,无法用它来指向链表第一个位置

这也是链表题中的一个小技巧

题解
public ListNode swapPairs(ListNode head) {
        // 用一个 fake 节点,指向头结点,这样,头结点永远不会丢失
        ListNode fake = new ListNode(-1);
        fake.next=head;
        swapRecursion(fake,head);
        return fake.next;
    }

    public void swapRecursion(ListNode pre,ListNode node) {
        if (node==null || node.next==null) return;
        // 交换节点
        ListNode next = node.next;
        node.next=next.next;
        next.next=node;
        pre.next=next;

        // 递归
        swapRecursion(node,node.next);
    }

二、其他操作

160-相交链表

题目

给你两个单链表的头节点 headAheadB ,请你找出并返回两个单链表相交的起始节点。如果两个链表没有交点,返回 null

图示两个链表在节点 c1 开始相交**:**

img

题目数据 保证 整个链式结构中不存在环。

注意,函数返回结果后,链表必须 保持其原始结构

示例 1:

img

输入:intersectVal = 8, listA = [4,1,8,4,5], listB = [5,0,1,8,4,5], skipA = 2, skipB = 3
输出:Intersected at '8'
解释:相交节点的值为 8 (注意,如果两个链表相交则不能为 0)。
从各自的表头开始算起,链表 A 为 [4,1,8,4,5],链表 B 为 [5,0,1,8,4,5]。
在 A 中,相交节点前有 2 个节点;在 B 中,相交节点前有 3 个节点。

示例 2:

img

输入:intersectVal = 2, listA = [0,9,1,2,4], listB = [3,2,4], skipA = 3, skipB = 1
输出:Intersected at '2'
解释:相交节点的值为 2 (注意,如果两个链表相交则不能为 0)。
从各自的表头开始算起,链表 A 为 [0,9,1,2,4],链表 B 为 [3,2,4]。
在 A 中,相交节点前有 3 个节点;在 B 中,相交节点前有 1 个节点。

示例 3:

img

输入:intersectVal = 0, listA = [2,6,4], listB = [1,5], skipA = 3, skipB = 2
输出:null
解释:从各自的表头开始算起,链表 A 为 [2,6,4],链表 B 为 [1,5]。
由于这两个链表不相交,所以 intersectVal 必须为 0,而 skipA 和 skipB 可以是任意值。
这两个链表不相交,因此返回 null 。

提示:

  • listA 中节点数目为 m
  • listB 中节点数目为 n
  • 0 <= m, n <= 3 * 104
  • 1 <= Node.val <= 105
  • 0 <= skipA <= m
  • 0 <= skipB <= n
  • 如果 listAlistB 没有交点,intersectVal0
  • 如果 listAlistB 有交点,intersectVal == listA[skipA + 1] == listB[skipB + 1]
思考

**方法一:**使用集合

判断两个链表是否相交,可以使用哈希集合存储链表节点。

首先遍历链表 headA,并将链表 headA 中的每个节点加入哈希集合中。然后遍历链表 headB,对于遍历到的每个节点,判断该节点是否在哈希集合中:

  • 如果当前节点不在哈希集合中,则继续遍历下一个节点;

  • 如果当前节点在哈希集合中,则后面的节点都在哈希集合中,即从当前节点开始的所有节点都在两个链表的相交部分,因此在链表 headB 中遍历到的第一个在哈希集合中的节点就是两个链表相交的节点,返回该节点。

如果链表 headB 中的所有节点都不在哈希集合中,则两个链表不相交,返回 null。

**方法二:**双指针

假设链表 A 的头节点到相交点的距离是 a,链表 B 的头节点到相交点的距离是 b,相交点 到链表终点的距离为 c。我们使用两个指针,分别指向两个链表的头节点,并以相同的速度前进, 若到达链表结尾,则移动到另一条链表的头节点继续前进。按照这种前进方法,两个指针会在 a + b + c 次前进后同时到达相交节点。

我们的中心思想,是让两个节点,在同一时间,走完 a+b+c 长度的路程,这时候,他们一定是在交汇点

题解
  • 使用 集合
public ListNode getIntersectionNode(ListNode headA, ListNode headB) {        Set<ListNode> set = new HashSet<>();        while (headA!=null) {            set.add(headA);            headA=headA.next;        }        while (headB!=null) {            if (set.contains(headB)) return headB;            headB=headB.next;        }        return null;    }
  • 双指针
public ListNode getIntersectionNode(ListNode headA, ListNode headB) {        ListNode h1 = headA;        ListNode h2 = headB;        while (h1!=h2) {            h1=h1==null?headB:h1.next;            h2=h2==null?headA:h2.next;        }        return h1;    }

这里可能会有个疑问:

代码这么写,如果是两个链表不相交的情况,不会出现死循环吗?

image-20210704155024590

其实不会

链表 headA 和 headB 的长度分别是 mm 和 nn。考虑当 m=n 和 m!=n 时,两个指针分别会如何移动:

如果 m=n,则两个指针会同时到达两个链表的尾节点,然后同时变成空值 null,此时返回 null;

如果 m!=n,则由于两个链表没有公共节点,两个指针也不会同时到达两个链表的尾节点,因此两个指针都会遍历完两个链表,在指针 pA 移动了 m+n 次、指针 pB 移动了 n+m 次之后,两个指针会同时变成空值 null,此时返回 null。

234-回文链表

题目

请判断一个链表是否为回文链表。

示例 1:

输入: 1->2输出: false

示例 2:

输入: 1->2->2->1输出: true

进阶:
你能否用 O(n) 时间复杂度和 O(1) 空间复杂度解决此题?

思考

**方法一:**将链表转换为数组,然后判断数组是否回文

**方法二:**先使用快慢指针找到链表中点,再把链表切成两半;然后把后半段翻转,最后比较两半是否相等。

题解

方法一:

public boolean isPalindrome(ListNode head) {        /**         * 方法一: 链表转数组         */        if (head==null) return true;        List<Integer> list = new ArrayList<>();        while (head!=null) {            list.add(head.val);            head=head.next;        }        int i=0,j=list.size()-1;        while (i<=j) {            if (list.get(i)!=list.get(j)) return false;            i++;            j--;        }        return true;    }

方法二:

public boolean isPalindrome(ListNode head) {        /**         * 方法二:折半翻转,再判断         */        if (head==null || head.next==null) return true;        // 使用快慢指针,找链表中点        ListNode fast = head;        ListNode slow = head;        while (fast.next!=null && fast.next.next!=null) {            fast=fast.next.next;            slow=slow.next;        }        slow.next=reverse(slow.next);        slow=slow.next;        while (slow!=null) {            if (head.val!=slow.val) return false;            head=head.next;            slow=slow.next;        }        return true;    }    /**     * 翻转链表     * @param head     * @return     */    ListNode reverse(ListNode head) {        if (head==null || head.next==null) return head;        ListNode pre=null;        ListNode curr=head;        while (curr!=null) {            ListNode tmp = curr;            curr=curr.next;            tmp.next=pre;            pre=tmp;        }        return pre;    }

小结

  • 担心头是空的?无法判断头的位置?在 new 一个 fake 节点,指向头吧

image-20210704150955904

image-20210704151004544

image-20210704151036140

用到该技巧的题目有 21 160 题

  • 链表判环,固定使用快慢指针法!
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

FARO_Z

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值