【算法专场】链表篇(下)

目录

前言

面试题 02.07. 链表相交 - 力扣(LeetCode)

算法思路

算法代码

23. 合并 K 个升序链表 - 力扣(LeetCode)

​编辑算法思路

解法一:优先级队列

算法代码

解法二:分治归并        

25. K 个一组翻转链表

算法思路

算法代码

61. 旋转链表 - 力扣(LeetCode)

算法思路

解法一

算法代码

算法代码

解法二

算法代码


前言

在上一篇中,已经讲解了链表中常用的一些技巧和操作,本篇我们继续通过相关算法题目来加深对链表的印象。

面试题 02.07. 链表相交 - 力扣(LeetCode)

算法思路

这道题是要判断两个链表是否相交,我们可以先让长的链表移动到和短的链表一样的长度,再进行判断两链表是否相交。

  1. 第一步:先判断两链表长度(假设两链表的长度差值为len),让长的链表先走len步
  2. 第二步:进行判断链表节点是否相等,如果相等,那么就返回true。若链表走到末尾都没有相交,则返回false。 

算法代码

法一:

/**
 * 获取两个链表的交点节点
 * 如果两个链表有交点,则从交点开始,两个链表的节点是一样的
 * 解决这个问题的关键在于,通过巧妙地交换两个链表的遍历顺序,使两个链表从尾部对齐
 * 这样,如果两个链表有交点,它们会在到达交点前遍历相同长度的路径
 * 
 * @param headA 第一个链表的头节点
 * @param headB 第二个链表的头节点
 * @return 返回两个链表的交点节点,如果没有交点则返回null
 */
public ListNode getIntersectionNode1(ListNode headA, ListNode headB) {
    // 检查输入的链表头节点是否为null,如果是,则不可能有交点,直接返回null
    if(headA==null||headB==null) return null;
    
    // 初始化两个指针p和q,分别指向两个链表的头节点
    ListNode p=headA,q=headB;
    
    // 循环直到p和q指向同一个节点,即找到交点或者都为null(表示没有交点)
    while(p!=q){
        // 如果p遍历到链表末尾,则将其指向另一个链表的头节点;否则,继续遍历下一个节点
        p=p==null?headB:p.next;
        // 如果q遍历到链表末尾,则将其指向另一个链表的头节点;否则,继续遍历下一个节点
        q=q==null?headA:q.next;
    }
    
    // 返回p(或者q,因为此时p和q要么同时为null,要么同时指向交点)
    return p;
}

法二:

/**
 * 获取两个链表的交点节点
 * 如果链表相交,则从交点到链表的末尾是共有的节点
 * 解决这个问题的关键在于找到第一个共有节点,即为交点
 * 如果链表长度不同,直接比较对应位置的节点是不合理的
 * 因此,使用哈希集合来存储一个链表的所有节点,然后遍历另一个链表,找到第一个在集合中出现的节点
 * 
 * @param headA 第一个链表的头节点
 * @param headB 第二个链表的头节点
 * @return 返回两个链表的交点节点,如果没有交点则返回null
 */
public ListNode getIntersectionNode(ListNode headA, ListNode headB) {
    // 检查任一链表为空时,直接返回null,因为不可能有交点
    if(headA==null||headB==null) return null;
    // 使用HashSet存储链表A的所有节点
    Set<ListNode> set=new HashSet<>();
    // 遍历链表A,将所有节点加入到集合中
    while(headA!=null){
        set.add(headA);
        headA=headA.next;
    }
    // 遍历链表B,检查节点是否在集合中,即是否为链表A中出现过的节点
    while(headB!=null){
        // 如果在集合中找到了相同的节点,说明这是两个链表的交点
        if(set.contains(headB)){
            // 返回这个交点节点
            return headB;
        }
        headB=headB.next;
    }
    // 如果没有找到交点,返回null
    return null;
}

 

 

23. 合并 K 个升序链表 - 力扣(LeetCode)

算法思路

其实就是合并有序链表,但这里有K个链表。

如果采用暴力解法的话,就是先合并前两个链表,再和第3个、第4个、到第k个。这样的时间复杂度我们可以算一下:总共要合并k-1次,平均每个链表的长度为n个节点:

第一组n(k-1),第二组为n(k-2)+....+n=n*k*(k-1)/2,即为n*k^2

但这种方法的效率明显低了不小,那么我们就需要对算法进行优化:

解法一优先级队列

        合并k个有序链表,我们可以利用小根堆的特点,将链表数组中的元素全部添加到小根堆中,每次堆顶的节点其实就是最小值,那么我们就将堆顶元素取出,接到虚拟节点dummy之后。

当然,在弹出堆顶节点之后,我们还需要判断弹出的节点的指针next是否为空,不为空说明还有下一个节点。那么我们就将下一个节点重新插入小根堆。直到小根堆为空,说明我们的链表已经合并完成。

算法代码

 /**
     * 合并K个升序链表的主方法
     *
     * @param lists 一个包含多个升序链表节点数组
     * @return 合并后的单个升序链表的头节点
     */
    public ListNode mergeKLists(ListNode[] lists) {
        // 利用最小堆来存储链表节点
        // 堆中元素通过比较器进行排序,确保堆顶元素是最小的
        PriorityQueue<ListNode> queue = new PriorityQueue<>((a, b) -> a.val - b.val);
        // 遍历所有链表,将非空节点加入优先队列
        for (ListNode node : lists) {
            if (node != null) {
                queue.offer(node);
            }
        }
        // 创建一个虚拟头节点,简化链表的合并操作
        ListNode dummy = new ListNode(-1);
        // cur用于遍历和链接节点
        ListNode cur = dummy;
        // 当队列不为空时,循环合并节点
        while (!queue.isEmpty()) {
            // 从队列中取出最小的节点
            ListNode node = queue.poll();
            // 将最小节点链接到cur后面
            cur.next = node;
            // 移动cur到下一个节点
            cur = cur.next;
            // 如果当前节点还有下一个节点,将下一个节点加入优先队列
            if (node.next != null) {
                queue.offer(node.next);
            }
        }
        // 返回合并后的链表的头节点
        return dummy.next;
    }

解法二:分治归并        

        我们可以采用递归的方法来解决,将k个链表分解成1个个链表,再对两两链表中的节点进行排序合并,就可以得到排序后的链表。

 // 法二、分支归并
    /**
     * 合并K个排序链表
     * 使用分支归并的方法,两两合并链表,直到所有链表合并成一个
     * 
     * @param lists 一个包含多个排序链表的数组
     * @return 合并后的单一排序链表
     */
    public ListNode mergeKLists1(ListNode[] lists) {
        return merge(lists, 0, lists.length - 1);
    }
    
    /**
     * 递归地合并排序链表数组中的链表
     * 
     * @param lists 排序链表数组
     * @param left  合并的起始索引
     * @param right 合并的结束索引
     * @return 合并后的链表头节点
     */
    private ListNode merge(ListNode[] lists, int left, int right) {
        // 当左索引大于等于右索引时,返回该索引位置的链表,作为递归的终止条件
        if (left >= right) return lists[left];
        
        // 计算中间索引,用于将数组分为两部分
        int mid = left + (right - left) / 2;
        
        // 递归合并左半部分链表
        ListNode leftHead = merge(lists, left, mid);
        
        // 递归合并右半部分链表
        ListNode rightHead = merge(lists, mid + 1, right);
        
        // 创建一个哑节点作为初始节点,便于后续操作
        ListNode dummy = new ListNode(-1);
        
        // 当前操作节点指针
        ListNode cur = dummy;
        
        // 合并左右两部分链表,每次取较小值节点
        while (leftHead != null && rightHead != null) {
            if (leftHead.val < rightHead.val) {
                cur.next = leftHead;
                leftHead = leftHead.next;
            } else {
                cur.next = rightHead;
                rightHead = rightHead.next;
            }
            cur = cur.next;
        }
        
        // 如果左半部分链表还有剩余,直接连接到合并链表的末尾
        while (leftHead != null) {
            cur.next = leftHead;
            cur = cur.next;
            leftHead = leftHead.next;
        }
        
        // 如果右半部分链表还有剩余,直接连接到合并链表的末尾
        while (rightHead != null) {
            cur.next = rightHead;
            cur = cur.next;
            rightHead = rightHead.next;
        }
        
        // 返回合并后的链表头节点
        return dummy.next;
    }

25. K 个一组翻转链表

算法思路

这道题我们可以用模拟的思想去解决。

  1. 首先遍历链表节点,判断一共有多少组需要逆序;
  2. 利用双for循环,外循环表示要逆序的组数,内循环重复k次长度进行逆序。 

此外,利用头插的方法。需要用一个pre节点来记录记录前驱节点,用tmp来记录每次逆序的第一个节点,方便我们更新pre。(每次逆序的第一个节点在逆序后是最后一个数) 

算法代码

/**
     * 将链表中的节点每k个进行反转
     * 
     * @param head 链表的头节点
     * @param k 每组的长度,即每k个节点进行反转
     * @return 反转后的链表的头节点
     */
    public ListNode reverseKGroup(ListNode head, int k) {
        //先判断用多少组需要翻转
        int n=0;
        ListNode cur=head;
        //遍历链表,统计链表长度
        while (cur!=null){
            n++;
            cur=cur.next;
        }
        //计算可以反转的组数
        n/=k;
        //创建一个虚拟头节点,方便处理反转后的链表连接
        ListNode dummy=new ListNode(-1);
        cur=head;
        //pre用于保存上一组的最后一个节点
        ListNode pre=dummy;
        //根据计算出的组数进行反转操作
        for(int i=0;i<n;i++){
            //每次保存上一组最后一个节点
            ListNode tmp=cur;
            //对当前组内的k个节点进行反转
            for(int j=0;j<k;j++){
                //保存当前节点的下一个节点
                ListNode next=cur.next;
                //将当前节点插入到上一组最后一个节点的后面
                cur.next=pre.next;
                pre.next=cur;
                cur=next;
            }
            //更新pre为当前组的最后一个节点,为下一组的反转做准备
            pre=tmp;
        }
        //将反转后的链表的尾部与剩余链表连接
        pre.next=cur;
        //返回反转后的链表的头节点
        return dummy.next;
    }

61. 旋转链表 - 力扣(LeetCode)

算法思路

根据题目,我们需要将链表的节点往后移动k个位置,后面的k个节点要按照原来的顺序放到前面。

解法一

我们可以先将链表分为两部分:

  1. 前n-k个节点进行逆转,再将后面的k个节点进行逆转
  2. 将逆转完的两部分进行合并
  3. 将合并完的链表再进行翻转

算法代码

    public ListNode rotateRight(ListNode head, int k) {
        if (head == null || head.next == null || k == 0) {
            return head;
        }

        // 计算链表长度
        int count = 1;
        ListNode tail = head;
        while (tail.next != null) {
            tail = tail.next;
            count++;
        }

        // 如果 k 是链表长度的倍数,直接返回原链表
        k = k % count;
        if (k == 0) {
            return head;
        }

       //找到第n-k个节点
        ListNode firstTail=head;
        for (int i = 0; i < count - k - 1; i++) {
            firstTail = firstTail.next;
        }

        //分割链表
        ListNode secondHead=firstTail.next;
        firstTail.next=null;

        // 再次反转前 k 个节点
        ListNode reversedFirstPart = reverse(head);
        // 反转后 k 个节点
        ListNode reversedSecondPart = reverse(secondHead);
        //合并两部分
        ListNode dummy = new ListNode(-1);
        ListNode cur = dummy;
        cur.next = reversedFirstPart;
        while(cur.next!=null){
            cur=cur.next;
        }
        cur.next=reversedSecondPart;
        // 再次反转整个链表
        return reverse(dummy.next);
    }

   
    // 反转整个链表
    private ListNode reverse(ListNode head) {
        ListNode pre = null;
        ListNode cur = head;
        while (cur != null) {
            ListNode next = cur.next;
            cur.next = pre;
            pre = cur;
            cur = next;
        }
        return pre;
    }

算法代码

解法二

 先找到要新的头结点,新的头结点就是倒数第k个节点,以例1为例,我们可以先找到4的位置,将3连4的地方断开,接着1、2、3连接到5之后。

算法代码

public ListNode rotateRight(ListNode head, int k) {
    if (head == null || head.next == null || k == 0) {
        return head;
    }

    // 计算链表长度
    int count = 1;
    ListNode tail = head;
    while (tail.next != null) {
        tail = tail.next;
        count++;
    }

    // k 可能大于链表长度,需要取模
    k = k % count;
    if (k == 0) {
        return head; // 如果 k 是链表长度的倍数,无需旋转
    }

    // 找到倒数第 k+1 个节点
    ListNode newTail = head;
    for (int i = 0; i < count - k - 1; i++) {
        newTail = newTail.next;
    }

    // 新的头节点是倒数第 k 个节点
    ListNode newHead = newTail.next;

    // 断开链表
    tail.next = head; // 将尾节点连接到原头节点,形成环
    newTail.next = null; // 断开环,形成新的链表

    return newHead;
}

第一种解法比较容易出错,推荐第二种。


以上就是链表篇的所有内容~当然,有关链表的题目还有很多,还需要多加练习。

若有不足,欢迎指正~

评论 10
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

小猪同学hy

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

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

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

打赏作者

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

抵扣说明:

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

余额充值