当前位置: 首页 > news >正文

【算法通关村 Day1】链表的增删改查及链表中双指针法应用

链表青铜关卡

链表的增删改查

什么是链表?

链表(Linked List)是一种常用的数据结构,它由一系列节点组成,每个节点包含数据域和指针域。指针域存储了下一个节点的地址,从而建立起各节点之间的线性关系。

创建一个链表需要class类型,首先,我们需要定义链表节点。每个节点包含两部分:一个数据域(val)和一个指向下一个节点的引用(next)。(为简便将数据域类型设置为int) :

    public class ListNode {
        public int val;
        public ListNode next;

        public ListNode(int val) {
            this.val = val;
            this.next = null;
        }        
    }
  • 插入操作

    • insertAtHead: 在链表的头部插入一个新节点,首先创建一个新节点,并将新节点的 next 指向当前的头节点,然后更新头节点指向新节点。
    • insertAfterNode: 在指定的节点后面插入一个新节点,需要将新节点的 next 指向指定节点的 next,然后将指定节点的 next 指向新节点。
  • 删除操作

    • deleteNode: 删除值为 value 的节点。如果要删除的是头节点,直接更新头节点指向下一个节点。如果是链表中的其他节点,遍历链表找到该节点,并将前一个节点的 next 指向要删除节点的下一个节点。
  • 修改操作

    • updateNode: 遍历链表查找值为 oldValue 的节点,并将该节点的 val 修改为 newValue
  • 查询操作

    • searchNode: 遍历链表查找值为 value 的节点,返回该节点的指针。如果未找到,返回 null
public class ListNode {
    public int val;
    public ListNode next;

    public ListNode(int val) {
        this.val = val;
        this.next = null;
    }  
    
    //1.1 在链表的头部插入一个新节点 
    public static ListNode insertAtHead(ListNode head, int value) {
        ListNode newNode = new ListNode(value);
        newNode.next = head;
        head = newNode;
        return head;
    }

    //1.2 在指定的节点后面插入一个新节点
    public static ListNode insertAfterNode(ListNode node, int value) {
        if (node == null) return null; //先检查指定节点是否为空
        ListNode newNode = new ListNode(value);
        newNode.next = node.next;
        node.next = newNode;
        return node;
    }

    //2. 删除:删除值为value的节点
    public static boolean deleteNode(ListNode head, int value) {
        if (head == null) return false;

        //如果头节点就是要删除的节点
        if (head.val == value) {
            head = head.next;
            return true;
        }

        ListNode current = head;
        while (current.next != null && current.next.val != value) {
            current = current.next;
        }

        if (current.next != null) {
            current.next = current.next.next;
            return true;
        }

        return false;
    }

    //3. 修改:修改值为 oldValue 的节点为 newValue
    public static boolean updateNode(ListNode head, int oldValue, int newValue) {
        ListNode current = head;
        while (current != null) {
            if (current.val == oldValue) {
                current.val = newValue;
                return true;
            }
            current = current.next;
        }
        return false;
    }


    //4. 查询:查找值为value的节点
    public static ListNode searchNode(ListNode head, int value) {
        ListNode current = head;
        while (current != null) {
            if (current.val == value) {
                return current;
            }
            current = current.next;
        }
        return null;
    }
    
    //5.附加 打印链表
    public static void printList(ListNode head) {
        ListNode current = head;
        while(current != null) {
            System.out.println(current.val + " -> ");
            current = current.next;
        }
        System.out.println("null");
    }
    
    //主程序测试
    public static void main(String[] args) {
        ListNode head = new ListNode(1);
    
        //1.1 在链表的头部插入一个新节点 
        head = insertAtHead(head,2);
        head = insertAtHead(head, 3);
        printList(head);
        
        //1.2 在指定的节点后面插入一个新节点
        ListNode node2 = searchNode(head, 2);
        if (node2 != null) {
            insertAfterNode(node2, 4);
        }
        printList(head);

        //2. 删除:删除值为value的节点
        deleteNode(head, 4);
        printList(head);

        //3. 修改:修改值为 oldValue 的节点为 newValue
        updateNode(head, 1, 100);
        printList(head);

        //4. 查询:查找值为value的节点
        ListNode result = searchNode(head, 100);
        if (result != null) {
            System.out.println(result);
        } else {
            System.out.println("No ListNode Found");
        }
    }
}


链表白银关卡

两个链表第一个公共子节点问题

两个链表A和B,头节点已知,存在公共子节点c1,c1的位置和数量是不确定的,要求找到A,B的第一个公共子节点。
  • 假设链表A和链表B的长度不相同:

    • 当指针 h1 到达链表 A 的尾部时,它会被重定向到链表 B 的头部,继续遍历链表 B。
    • 同样,当指针 h2 到达链表 B 的尾部时,它会被重定向到链表 A 的头部,继续遍历链表 A。
    • 这样一来,两个指针最终会在公共节点处相遇。如果两个链表有公共部分,经过两次遍历(一次遍历链表 A,一次遍历链表 B),指针 h1h2 会最终相遇在第一个公共节点上。
    • 如果没有公共节点,两者都将同时遍历完两个链表的长度,最终会相遇在 null 上。
    • 由于 h1h2 在相遇时指向相同的节点,所以无论是返回 h1 还是 h2 都是正确的。最终返回的 h1 就是两个链表的第一个公共节点。如果没有公共节点,返回 null

public class IntersectionNode {
    static class ListNode {
        int val;
        ListNode next;

        ListNode(int val) {
            this.val = val;
            this.next = null;
        }
    }

    //双指针法
    public ListNode getIntersectionNode(ListNode headA, ListNode headB) {

        ListNode h1 = headA, h2 = headB;
        while (h1 != h2) {
            h1 = h1 == null ? headB : h1.next;
            h2 = h2 == null ? headA : h2.next;
        }
        return h1;
    }

    public static void main(String[] args) {
        IntersectionNode list = new IntersectionNode(); 

        //创建链表A: 1 -> 2 -> 3
        ListNode headA = new ListNode(1);
        headA.next = new ListNode(2);
        headA.next.next = new ListNode(3);

        //创建链表B:6 -> 5 -> 4 -> 2 -> 3
        ListNode headB = new ListNode(6);
        headB.next = new ListNode(5);
        headB.next.next = new ListNode(4);
        headB.next.next.next = headA.next;

        ListNode intersectionNode = list.getIntersectionNode(headA, headB);

        if (intersectionNode != null) {
            System.out.println("Intersection node value: " + intersectionNode.val);
        } else {
            System.out.println("No intersection");
        }
    }
}

结果:

Intersection node value: 2
  • 时间复杂度: O(N + M),其中 N 和 M 分别是链表 A 和链表 B 的长度。每个指针最多遍历两个链表一次。
  • 空间复杂度: O(1),只使用了常数的额外空间。

判断链表是否为回文序列

什么是回文序列?

如果一个链表是回文,那么链表节点序列从前往后看和从后往前看是相同的。例如:

通过快慢指针找到链表的中间节点。具体来说:

  • slow 每次移动一步,fast 每次移动两步。
  • fast 指针到达链表的末尾时,slow 正好到达链表的中间。
  • 链表的长度为奇数时: slow 会指向链表的中间节点,即正好有一个节点位于中间。
  • 链表的长度为偶数时: slow 会指向链表的第二个中间节点,即位于两个中间节点之间的位置。

反转指针:

  • nextTemp = curr.next:保存当前节点的下一个节点,因为反转指针后,curr.next 会指向 prev,而不是 curr.next,所以我们需要提前保存。
  • curr.next = prev:反转当前节点的指针,使其指向前一个节点。
  • prev = curr:将 prev 移动到当前节点,准备处理下一个节点。
  • curr = nextTemp:将 curr 移动到下一个节点。
public class PalindromeLinkedList {
    static class ListNode {
        int val;
        ListNode next;

        ListNode(int val) {
            this.val = val;
            this.next = null;
        }
    }

    public boolean isPalindrome(ListNode head){
        
        //1. 找到链表的中间节点
        ListNode slow = head;
        ListNode fast = head;
        while (fast != null & fast.next != null) {
            slow = slow.next;
            fast = fast.next.next;
        }

        // 2. 反转链表的后半部分
        ListNode reverseSecondHalf = reverse(slow);

        //3. 比较前半部分和反转的后半部分
        ListNode p1 = head;
        ListNode p2 = reverseSecondHalf;
        while (p2 != null) {
            if (p1.val != p2.val) {
                return false;
            }
            p1 = p1.next;
            p2 = p2.next;
        }

        return true; 
    }
    
    private ListNode reverse(ListNode head){
        ListNode prev = null;
        ListNode curr = head;
        while (curr != null) {
            ListNode nextTemp = curr.next;
            curr.next = prev;
            prev = curr;
            curr = nextTemp;
        }
        return prev;
    }
}


合并有序链表

存在链表A(1->2->4),和链表B(1->3->4),A和B都是有序链表,要求将A,B合成一个链表,合成后的链表包含A,B链表的所有元素,且仍然是有序的。

  • 初始化一个新的虚拟头节点 headC,它的 val 设置为 -1,只是为了简化操作。
  • iter 是一个指针,用来遍历新链表,并构建合并后的链表。开始时,iter 指向虚拟头节点 headC

  • 遍历两个链表

    • 使用 while 循环来遍历两个链表。当两个链表都不为空时,比较它们的节点值:
      • 如果 headA.val < headB.val,则将 headA 连接到合并链表,并让 headA 指向下一个节点。
      • 否则,将 headB 连接到合并链表,并让 headB 指向下一个节点。
    • 每次连接后,iter 都移动到当前插入的节点(iter = iter.next)。
  • 处理剩余节点

    • 当其中一个链表为空时,另一个链表可能还有剩余的节点。这时,只需要将剩余的节点直接连接到 iter
    • 如果 headA 还不为空,直接将 headA 剩余部分连接到 iter
    • 如果 headB 还不为空,直接将 headB 剩余部分连接到 iter
public class MergeTwoList {
    static class ListNode {
        int val;
        ListNode next;

        ListNode(int val) {
            this.val = val;
            this.next = null;
        }
    }

    public static ListNode mergeTwoList(ListNode headA, ListNode headB){
        ListNode headC = new ListNode(-1);
        ListNode iter = headC;
        
        while (headA != null && headB != null) {
            if (headA.val < headB.val) {
                iter.next = headA;
                headA = headA.next;
                iter = iter.next;
            } else {
                iter.next = headB;
                headB = headB.next;
                iter = iter.next;
            }
        }

        if (headA != null) {
            iter.next = headA;
        }
        if (headB != null) {
            iter.next = headB;
        }

        return headC.next;
    }
}


链表经典问题之双指针

寻找中间节点

前面已经做过。思路即设置两个指针fast,slow同时指向head,fast每次走两步,slow每次走一步,fast为空时,slow即为中间节点。

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

寻找倒数第K个节点

设置两个指针fast,slow同时指向head,fast先走K步,然后fast和slow一起走,当fast为空时,slow即是倒数第K个节点。

public ListNode getKthFromEnd(ListNode head, int k){
    ListNode fast = head;
    ListNode slow = head;

    while (fast != null && k > 0) {
        fast = fast.next;
        k--;
    }

    while (fast != null) {
        slow = slow.next;
        fast = fast.next;
    }

    return slow;
} 

旋转链表

旋转链表,将链表每个节点向右移动 k 个位置。

可以观察到,每一个节点向后移动K步,可以直接将倒数第K个节点以及之后的节点拼接到链表头部。

class Solution {
    public ListNode rotateRight(ListNode head, int k) {
        // 如果链表为空或者只有一个节点,直接返回
        if (head == null || head.next == null) {
            return head;
        }

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

        //调整k,避免k大于链表长度
        k = k % length;

        //刚好是head无需旋转
        if (k == 0) {
            return head;
        }

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

        // 将链表断开并重新连接
        newTail.next = null;
        tail.next = head;

        return newHead;
    }
}

链表黄金关卡

链表中环的问题

使用两个指针,快指针一次走两步,慢指针一次走一步。当两个指针相遇时,证明链表有环。然后,如果快指针到达链表的末尾(即 fast == nullfast.next == null),则链表无环。

假设链表中存在环,非环长度为a,环长度为b,则使用快慢指针一定会相遇。我们分析相遇时的情况。fast指针式slow指针速度的两倍,则f=2s,fast比slow指针多走了n个环的长度,f=s+nb,联立方程,得f=2nb s=nb,当s走到环的入口时s = a+nb,我们想要找到环的入口,只需要通过重新设置让fast指针指向head,每次走一步,slow与fast同步走,两者在一起走a步,在环入口相遇。

若题目要求返回是否存在环的布尔值(如Leetcode 141),只需调整返回值:

public boolean hasCycle(ListNode head) {
    if (head == null) return false;
    ListNode slow = head, fast = head;
    while (fast != null && fast.next != null) {
        slow = slow.next;
        fast = fast.next.next;
        if (slow == fast) return true;
    }
    return false;
}

若还需要返回环的入口节点,则需调整函数,类型为ListNode,如果没有环则返回null。

    public ListNode detectCycle(ListNode head) {
        if (head == null) return null;

        ListNode slow = head;
        ListNode fast = head;
        boolean hasCycle = false;

        // 步骤1:检测是否有环
        while (fast != null && fast.next != null) {
            slow = slow.next;
            fast = fast.next.next;
            if (slow == fast) {
                hasCycle = true;
                break;
            }
        }

        // 无环则返回null
        if (!hasCycle) return null;

        // 步骤2:找到环的入口
        slow = head;
        while (slow != fast) {
            slow = slow.next;
            fast = fast.next;
        }
        return slow;
    }

相关文章:

  • 讲讲Mysql主从复制原理与延迟
  • 代码随想录-训练营-day30
  • [转]Java面试近一个月的面试总结
  • 类加载机制及双亲委派模型
  • 使用 SDKMAN! 在 Mac(包括 ARM 架构的 M1/M2 芯片)安装适配 Java 8 的 Maven
  • CF 137B.Permutation(Java 实现)
  • 审计费用差10倍?项目规模如何影响报价
  • 【ISO 14229-1:2023 UDS诊断全量测试用例清单系列:第十五节】
  • P5693 EI 的第六分块 Solution
  • Transformer 模型介绍(三)——自注意力机制 Self-Attention
  • 第二章:12.6 偏差或方差与神经网络
  • Sentinel 源码深度解析
  • 136,【3】 buuctf web [极客大挑战 2020]Roamphp4-Rceme
  • vue若依框架dicts中字典项的使用:表格展示与下拉框示例
  • 《AI大模型开发笔记》Open-R1:对 DeepSeek-R1 的完全开源再现(翻译)
  • 静力触探数据智能预处理(6)
  • JavaScript 内置对象-Math对象
  • (学习总结23)Linux 目录、通配符、重定向、管道、shell、权限与粘滞位
  • [8-2-2] 队列实验_多设备玩游戏(红外改造)_重录
  • IWC万国表:源自瑞士的精密制表艺术(中英双语)
  • 白玉兰奖征片综述丨动画的IP生命力
  • 习近平会见哥伦比亚总统佩特罗
  • 王毅谈中拉命运共同体建设“五大工程”及落实举措
  • 国务院关税税则委:调整对原产于美国的进口商品加征关税措施
  • 山东省市监局“你点我检”专项抽检:一批次“无抗”鸡蛋农兽药残留超标
  • 最高降九成!特朗普签署降药价行政令落地存疑,多家跨国药企股价收涨