硅基计划4.0 算法 链表

硅基计划4.0 算法 链表


1740030372877



链表是一个逻辑性非常强的数据结构,在做这类题的时候,我们一般有几个技巧

  1. 引入虚拟头节点,避免一些边界情况(比如原链表头节点变动等情况)
  2. 尽量多的定义变量以理清各个节点之间的关系
    比如一个双向链表[1](preV)<-->[2],我要在其中插入一个节点[3](current)
    那我是不是得用一系列的关系指向避免链表中断
    我们不妨把下一个节点定义为nextNode,然后让preV,current,nextNode之间正常连接就好
  3. 常用双指针寻找链表换入口,倒数第几个节点,判断环等等,之前数据结构的时候已经有讲过了

一、两数相加

题目链接
题目中的示例是这样的
2 4 3 --> 3 4 25 6 4 --> 4 6 5,然后3 4 2 + 4 6 5 = 8 0 7
结果还要再逆置7 0 8

再比如
2 4 --> 4 25 6 4 --> 4 5 6,然后4 2 + 4 5 6 = 5 0 7,逆置7 0 5

题目为什么要直接给逆序的呢,是便于我们加法,比如刚刚的2 45 6 4
2 4
5 6 4
————
7 0 5
我们可以看到,我们每个链表的每一位是要对齐的,一个链表为空了如果另一个不为空还是要进行计算的
也就是说,我们定义两个指针,如果一个指针是空,也要进行计算
我们还要存储两数相加结果,我们可以相加计算完后,我们提取出结果的个位数,这样就方便处理进位的情况
提取完毕后再除以10,这样做是为了保留进位

/**
 * 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 newHead = new ListNode();
        ListNode newCurrent = newHead;
        ListNode current1 = l1;
        ListNode current2 = l2;
        int num = 0;

        while(current1 != null || current2 != null || num != 0){
            //当两个指针为空的时候,但是进位num里还是存在数据,也要进行加法运算
            if(current1 != null){
                num += current1.val;
                current1 = current1.next;
            }
            if(current2 != null){
                num += current2.val;
                current2 = current2.next;
            }
            ListNode node = new ListNode(num%10);
            newCurrent.next = node;
            newCurrent = newCurrent.next;
            num /= 10;
        }

        return newHead.next;
    }
}

二、两两交换链表中的节点

题目链接
我们使用虚拟头节点就很方便,我们两个节点为一组,进行翻转,最后若剩下节点不足两个,就不用变化
image-20250830101236434
我们定义preV指向交换区域的前置节点,node1 node2是两个待交换的节点,而node3是后面的节点,以防链表断开
可以看到,如果我们链表节点是偶数个,当我们的node1是空的就可以停止交换了
image-20250830101501609
可以看到如果是奇数个,当我们的node2是空的也可以停止交换了
但是我们在循环的时候,想要进行下一次交换的时候,得提前判断node1node2是否是空的,不然会发生空指针异常

/**
 * 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;
        }
        if(head.next == null){
            return head;
        }
        //使用哨兵节点
        ListNode newHead = new ListNode();

        ListNode preV = newHead;
        ListNode node1 = head;
        ListNode node2 = node1.next;
        ListNode node3 = node2.next;

        while(node1 != null && node2 != null){
            preV.next = node2;
            node2.next = node1;
            node1.next = node3;
            //注意交换后节点位置变化
            preV = node1;
            node1 = preV.next;
            //提前判断,防止空指针异常
            if(node1 != null){
                node2 = node1.next;
            }
            if(node2 != null){
                node3 = node2.next;
            }
        }

        return newHead.next;
    }
}

三、重排链表

题目链接
这一题核心就是两个链表合并,哪两个链表,对于原始链表的前半部分和后半部分的逆序的合并
因此我们要先找到中间节点,然后断开链表,再合并就好
逆序我们使用头插法就好,双指针也行但麻烦

好,问题来了,我们找到中间节点后
究竟是要逆置中间节点后面(不包括中间节点)链表还是中间节点及以后的链表呢?我们可以来对比下
image-20250830103131209
但是为什么推荐上面那一种,因为在图中你也能看到,中间节点和后面节点还有联系
本应该中间节点以后的节点是逆置的,是一个独立的链表,因此我们要断开两个链表联系
如果我们采用上面那一种,我们直接让middle.next = null就可以了
而下面那一种还要去寻找middle的前置节点,比较麻烦

讲起来很容易,但是还是要画图,尤其是两个指针的位置你要搞明白
什么时候头插逆置链表,什么时候尾插合并链表,都要搞得很清楚

这些代码我整整花了两个小时梳理QAQ

/**
 * 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) {
        if(head == null || head.next == null || head.next.next == null){
            return;
        }

        //寻找中间节点
        ListNode fast = head;
        ListNode slow = head;
        while(fast != null && fast.next != null){
            fast = fast.next.next;
            slow = slow.next;
        }

        //分割链表
        ListNode newSlow = slow.next;
        slow.next = null;

        //逆置slow以后的链表
        ListNode head2 = new ListNode(0);
        while(newSlow != null){
            ListNode tmpNewSlow = newSlow.next;
            newSlow.next = head2.next;
            head2.next = newSlow;
            newSlow = tmpNewSlow;
        }

        //合并两个链表
        ListNode current1 = head;
        ListNode current2 = head2.next;
        ListNode retHead = new ListNode(0);
        ListNode newCurrent = retHead;
        while(current1 != null){
            newCurrent.next = current1;
            newCurrent = current1;
            current1 = current1.next;
            if(current2 != null){
                newCurrent.next = current2;
                newCurrent = current2;
                current2 = current2.next;
            }
        }
    }
}

另外再附上我自己觉得巧妙的一种解法(可以不用看)

/**
 * 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) {
        if(head == null){
            return;
        }
        if(head.next == null){
            return;
        }
        //我们发现中间节点永远都在最后,不论奇数偶数
        ListNode middle = searchMiddle(head);

        //不是双向链表,无法定义两个指针从两边向中间靠拢
        //如果硬是要这么干,会导致每一次中间节点的后面节点排序都要找到前置节点,时间复杂度很高
        //但是我们是不是可以把中间节点以后的节点进行逆置,对于偶数,我们中间节点是第二个

        //我们要定义一个临时变量去代替中间节点行动
        ListNode currentMiddle = middle;

        //我们先设定好反转后的头节点
        ListNode preV = currentMiddle.next;

        //先把中间节点的next地址置空,防止成环
        middle.next = null;

        //开始反转,preV头节点位置一直变化,直到到达链表末尾的空节点位置
        while(preV != null){
            ListNode tmp = preV.next;
            preV.next = currentMiddle;
            currentMiddle = preV;
            preV = tmp;
        }

        //由于此时preV为空,因此我们要让它等于链表最后一个节点
        preV = currentMiddle;

        //中间变量代替头节点行动
        ListNode currentHead = head;

        //开始重排链表,排着排着后面的头节点preV会越来越接近链表中间节点
        //直到重合,偶数奇数都一样
        //不能是currentHead,虽然奇数个节点会和中间节点重合,但是偶数个节点会停在中间节点的前一个节点
        while(preV != middle){
            ListNode tmpLeft = currentHead.next;
            ListNode tmpRight = preV.next;

            currentHead.next = preV;
            preV.next = tmpLeft;

            preV = tmpRight;
            currentHead = tmpLeft;
        }
    }

    private ListNode searchMiddle(ListNode head){
        //我们的老熟人快慢指针
        ListNode fast = head;
        ListNode slow = head;

        //防止fast越界且判断奇数和偶数节点情况
        while(fast != null && fast.next != null){
            fast = fast.next.next;
            slow = slow.next;
        }

        return slow;
    }
}

四、k个一组翻翻转链表——hard

题目链接
我们可以先计算出链表的节点个数,然后决定翻转多少组,每一组内部再进行头插法逆置链表即可
我们使用preV来表示每一组的头插的头节点
但是,我们完成一组逆序后,我们要进行下一组逆序,此时的头插的头节点会改变,因此我们提前要标记好下一次头插的头节点位置sign
image-20250830104619903

/**
 * 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 current = head;
        int count = 0;
        while(current != null){
            count++;
            current = current.next;
        }
        //翻转多少组
        int times = count/k;
        //翻转每组需要多少次
        int step = k;
        //current回到起始位置
        current = head;
        //设立新链表接受翻转
        ListNode retHead = new ListNode(0);
        ListNode preV = retHead;
        //记录每次头插位置
        while(times > 0){
            ListNode sign = current;
            for(int i = 0;i < step;i++){
                ListNode nextNode = current.next;
                current.next = preV.next;
                preV.next = current;
                current = nextNode;
            }
            preV = sign;
            times--;
        }
        //处理剩下节点,此时sign标记是在下一次插入的位置
        //但是此时已经循环完了,因此后续节点无需逆序,直接放入就好
        preV.next = current;
        return retHead.next;
    }
}

还剩下一个很难的链表题没讲,以后有机会再讲,希望本文章对您有帮助

END
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值