首页 > 编程语言 >【数据结构与算法】之链表经典算法大集合

【数据结构与算法】之链表经典算法大集合

时间:2024-10-23 15:21:02浏览次数:3  
标签:head null ListNode next 链表 算法 数据结构 节点

本文主要内容是几个关于链表的初级经典算法的分享,都采用Java语言实现,话不多说,立马开始!

注意:以下代码有关链表的算法实现均基于以下链表节点类:

//链表节点类
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; }
}

相关教程:

一、合并两个有序链表

问题描述:

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

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

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

图示:

方法一:双指针二路归并

思路分析:

  • 谁小,把谁链给 p,p 和小的都向后平移一位

  • 当 p1、p2 有一个为 null,退出循环,把不为 null 的链给 p

p1
|
1	3	8	9	null

p2
|
2	4	null

--------------------------

p
|		
s	null

代码实现:

public ListNode mergeTwoLists(ListNode p1, ListNode p2) {
    ListNode s = new ListNode(-1, null);
    ListNode p = s;
    while (p1 != null && p2 != null) {
        if (p1.val < p2.val) {
            p.next = p1;
            p1 = p1.next;
        } else {
            p.next = p2;
            p2 = p2.next;
        }
        p = p.next;
    }
    if (p1 != null) {
        p.next = p1;
    }
    if (p2 != null) {
        p.next = p2;
    }
    return s.next;
}

方法二:递归

思路分析:

递归函数应该返回

  • 更小的那个链表节点,并把它剩余节点与另一个链表再次递归

  • 返回之前,更新此节点的 next

//伪代码分析
mergeTwoLists(p1=[1,3,8,9], p2=[2,4]) {
    1.next=mergeTwoLists(p1=[3,8,9], p2=[2,4]) {
        2.next=mergeTwoLists(p1=[3,8,9], p2=[4]) {            
            3.next=mergeTwoLists(p1=[8,9], p2=[4]) {
                4.next=mergeTwoLists(p1=[8,9], p2=null) {
                    return [8,9]
                }
                return 4
            }
            return 3
        }
        return 2
    }
	return 1
}

代码实现:

public ListNode mergeTwoLists(ListNode list1, ListNode list2) {
        
        if(list1 == null){
            return list2;
        }
        if(list2 == null){
            return list1;
        }
        //递归调用
        if(list1.val < list2.val){
            list1.next = mergeTwoLists(list1.next,list2);
            return list1;
        }else {
            list2.next = mergeTwoLists(list1,list2.next);
            return list2;
        }
        
}

二、合并多个有序链表

问题描述:

给你一个链表数组,每个链表都已经按升序排列。将所有链表合并到一个升序链表中,返回合并后的链表。

示例 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 = [[]]
输出:[]

方法一:递归

思路分析:

这里与合并两个有序链表的思路是一致的,只不过这里采用了分而治之的思想,分:将多个链表不断切分,递归切分为两个有序链表;治:将切分后的两个链表再调用合并两个有序链表的方法,从而实现最终多个有序链表的合并。

代码实现:

    public ListNode mergeKLists(ListNode[] lists) {
        if(lists.length == 0){
            return null;
        }
        return split(lists,0,lists.length - 1);
    }

    // 递归切分链表数组,分而治之
    // i表示链表数组起始索引,j表示链表数组最后索引
    public ListNode split(ListNode[] lists, int i, int j) {
        if (i == j) {
            return lists[i];
        }
        int m = (i+j) >>> 1;
        return mergeTwoLists(
            split(lists,i,m),
            split(lists,m+1,j)
        );
    }

    // 合并两个有序链表
    public ListNode mergeTwoLists(ListNode p1, ListNode p2) {
        ListNode s = new ListNode(-1, null);
        ListNode p = s;
        while (p1 != null && p2 != null) {
            if (p1.val < p2.val) {
                p.next = p1;
                p1 = p1.next;
            } else {
                p.next = p2;
                p2 = p2.next;
            }
            p = p.next;
        }
        if (p1 != null) {
            p.next = p1;
        }
        if (p2 != null) {
            p.next = p2;
        }
        return s.next;
    }

方法二:优先队列

思路分析:

  • 创建一个最小堆(优先队列),用于存储所有链表的头节点。
  • 每次从堆中取出最小的节点,将其添加到结果链表中,并将该节点的下一个节点添加回堆中。
  • 重复步骤2,直到堆为空。

代码实现:

import java.util.PriorityQueue;

class ListNode {
    int val;
    ListNode next;
    ListNode(int x) { val = x; }
}

public class Solution {
    public ListNode mergeKLists(ListNode[] lists) {
        PriorityQueue<ListNode> heap = new PriorityQueue<>(lists.length, (a, b) -> a.val - b.val);
        for (ListNode node : lists) {
            if (node != null) {
                heap.offer(node);
            }
        }

        ListNode dummy = new ListNode(0);
        ListNode tail = dummy;
        while (!heap.isEmpty()) {
            ListNode smallest = heap.poll();
            tail.next = smallest;
            tail = smallest;
            if (smallest.next != null) {
                heap.offer(smallest.next);
            }
        }
        return dummy.next;
    }
}

三、查找链表中间节点

问题描述:

给你单链表的头结点 head ,请你找出并返回链表的中间结点。

如果有两个中间结点,则返回第二个中间结点。

示例 1:

奇数节点时,是中间节点
输入:head = [1,2,3,4,5]
输出:[3,4,5]
解释:链表只有一个中间结点,值为 3 

示例 2:

偶数节点时,中间节点是靠右的那个
输入:head = [1,2,3,4,5,6]
输出:[4,5,6]
解释:该链表有两个中间结点,值分别为 3 和 4 ,返回第二个结点

方法一:快慢指针法

思路分析:

快慢指针,快指针一次走两步,慢指针一次走一步,当快指针到链表结尾时,慢指针恰好走到链表的一半

  p1p2
    |
    1	3	5   6   8   null


        p1  p2
        |   |
    1	3	5   6   8   null


            p1      p2
            |       |
    1	3	5   6   8   null

此时慢指针 p1 指向中间节点

代码实现:

public ListNode middleNode(ListNode head) {
    ListNode p1 = head;	// 慢指针,中间点
    ListNode p2 = head;	// 快指针
    while (p2 != null && p2.next != null) {
        p1 = p1.next;
        p2 = p2.next.next;
    }
    return p1;
}

方法二:递归

思路分析:

  • 如果链表为空或只有一个节点,直接返回头节点。
  • 递归地找到链表后半部分的中间节点。
  • 如果后半部分有奇数个节点,返回后半部分的中间节点。
  • 如果后半部分有偶数个节点,返回前半部分的最后一个节点。

代码实现:

    public ListNode findMiddle(ListNode head) {
        return findMiddleRecursive(head, null);
    }

    private ListNode findMiddleRecursive(ListNode curr, ListNode prev) {
        if (curr == null) return prev;
        ListNode next = curr.next;
        curr.next = prev;
        ListNode result = findMiddleRecursive(next, curr);
        // 恢复链表结构
        curr.next = next;
        return result;
    }

四、回文链表判断

问题描述:

给你一个单链表的头节点 head ,请你判断该链表是否为回文链表。如果是,返回 true ;否则,返回 false 。

示例 1:

输入:head = [1,2,2,1]
输出:true

示例 2:

输入:head = [1,2]
输出:false

方法一:快慢指针和反转链表

思路分析:

整体思路是先找到链表的中间节点,然后反转后半部分链表,最后比较前半部分和反转后的后半部分是否相同,从而判断链表是否为回文链表。

代码实现:

/*
    步骤1. 找中间点
    步骤2. 中间点后半个链表反转
    步骤3. 反转后链表与原链表逐一比较
*/
public boolean isPalindrome(ListNode head) {
    ListNode middle = middle(head);
    ListNode newHead = reverse(middle);
    while (newHead != null) {
        if (newHead.val != head.val) {
            return false;
        }
        newHead = newHead.next;
        head = head.next;
    }
    return true;
}
// 反转链表
private ListNode reverse(ListNode o1) {
    ListNode n1 = null;
    while (o1 != null) {
        ListNode o2 = o1.next;
        o1.next = n1;
        n1 = o1;
        o1 = o2;
    }
    return n1;
}

// 找到中间节点
private ListNode middle(ListNode head) {
    ListNode p1 = head; // 慢
    ListNode p2 = head; // 快
    while (p2 != null && p2.next != null) {
        p1 = p1.next;
        p2 = p2.next.next;
    }
    return p1;
}

以上代码经过优化后如下:

public boolean isPalindrome(ListNode h1) {
    if (h1 == null || h1.next == null) {
        return true;
    }
    ListNode p1 = h1; 	// 慢指针,中间点
    ListNode p2 = h1; 	// 快指针
    ListNode n1 = null;	// 新头
    ListNode o1 = h1;	// 旧头
    // 快慢指针找中间点
    while (p2 != null && p2.next != null) {
        p1 = p1.next;
        p2 = p2.next.next;

        // 反转前半部分
        o1.next = n1;
        n1 = o1;
        o1 = p1;
    }
    if (p2 != null) { // 节点数为奇数
        p1 = p1.next;
    }
    // 同步比较新头和后半部分
    while (n1 != null) {
        if (n1.val != p1.val) {
            return false;
        }
        p1 = p1.next;
        n1 = n1.next;
    }
    return true;
}

方法二:递归

思路分析:

  • 递归到链表的中间:首先,我们需要找到链表的中间节点。这可以通过递归实现,每次递归调用处理链表的后一半,直到链表的长度为1或2,这时我们可以认为到达了中间。

  • 递归反转后半部分链表:找到中间节点后,我们继续递归,但是这次是为了反转链表的后半部分。我们可以在到达中间节点后开始反转链表。

  • 递归比较前后节点:在反转了后半部分链表之后,我们可以从原始链表的头部和反转后的后半部分的头部开始,递归比较对应节点的值,直到所有节点都被比较完毕或者发现不匹配的节点。

代码实现:

public class ListNode {
    int val;
    ListNode next;
    ListNode(int x) { val = x; }
}

public class Solution {
    /**
     * 判断链表是否为回文链表
     * @param head 链表的头节点
     * @return 是否为回文链表
     */
    public boolean isPalindrome(ListNode head) {
        if (head == null || head.next == null) {
            return true;
        }
        
        // 步骤1:找到链表的中间节点
        ListNode middle = findMiddle(head);
        
        // 步骤2:反转链表的后半部分
        ListNode reversedHead = reverse(middle);
        
        // 步骤3:比较前半部分和反转后的后半部分
        return compareTwoLists(head, reversedHead);
    }
    
    /**
     * 通过快慢指针法找到链表的中间节点
     * @param head 链表的头节点
     * @return 中间节点
     */
    private ListNode findMiddle(ListNode head) {
        if (head == null) return null;
        ListNode fast = head, slow = head;
        while (fast.next != null && fast.next.next != null) {
            slow = slow.next;       // 慢指针每次走一步
            fast = fast.next.next;  // 快指针每次走两步
        }
        return slow;  // 返回中间节点
    }
    
    /**
     * 递归反转链表
     * @param head 需要反转的链表的头节点
     * @return 反转后链表的头节点
     */
    private ListNode reverse(ListNode head) {
        if (head == null || head.next == null) {
            return head;
        }
        ListNode newHead = reverse(head.next);
        head.next.next = head;  // 将当前节点的下一个节点的next指向当前节点,实现反转
        head.next = null;       // 将当前节点的next设置为null
        return newHead;        // 返回新的头节点
    }
    
    /**
     * 递归比较两个链表是否相等
     * @param l1 第一个链表的头节点
     * @param l2 第二个链表的头节点
     * @return 两个链表是否相等
     */
    private boolean compareTwoLists(ListNode l1, ListNode l2) {
        if (l1 == null || l2 == null) {
            return l1 == l2;  // 如果两个链表都为空,则认为它们相等
        }
        if (l1.val != l2.val) {
            return false;  // 如果当前节点的值不相等,则链表不相等
        }
        return compareTwoLists(l1.next, l2.next);  // 递归比较下一个节点
    }
}

五、删除链表中间节点

问题描述:

有一个单链表的 head,我们想删除它其中的一个节点 node

给你一个需要删除的节点 node 。你将 无法访问 第一个节点  head

链表的所有值都是 唯一的,并且保证给定的节点 node 不是链表中的最后一个节点。

删除给定的节点。注意,删除节点并不是指从内存中删除它。这里的意思是:

  • 给定节点的值不应该存在于链表中。
  • 链表中的节点数应该减少 1。
  • node 前面的所有值顺序相同。
  • node 后面的所有值顺序相同。

示例一:

输入:head = [4,5,1,9], node = 5
输出:[4,1,9]
解释:指定链表中值为 5 的第二个节点,那么在调用了你的函数之后,该链表应变为 4 -> 1 -> 9

示例二:

输入:head = [4,5,1,9], node = 1
输出:[4,5,9]
解释:指定链表中值为 1 的第三个节点,那么在调用了你的函数之后,该链表应变为 4 -> 5 -> 9

思路分析:

  • 节点值替换

    • 代码的第一行 node.val = node.next.val; 是将待删除节点的值替换为它的下一个节点的值。这样做的目的是为了保留下一个节点的值,因为如果直接删除下一个节点,它的值就会丢失。
  • 跳过下一个节点

    • 代码的第二行 node.next = node.next.next; 是将待删除节点的 next 指针指向下一个节点的 next 节点,从而跳过下一个节点。这样,原本的下一个节点(现在值已经被复制到了待删除节点)就从链表中被“删除”了,因为它的前驱节点不再指向它。

这种方法的关键在于,待删除的节点 node 并不是真正的被删除,而是它的值被替换为了下一个节点的值,并且它的 next 指针被调整以跳过下一个节点。这样做的好处是不需要找到待删除节点的前驱节点,可以在任意位置删除节点(只要该节点的下一个节点存在)。

代码实现:

    /**
     *
     * @param node 待删除节点, 已说明肯定不是最后一个节点
     */
    public void deleteNode(ListNode node) {
        node.val = node.next.val;		// 下一个节点值赋值给待"删除"节点
        node.next = node.next.next;		// 把下一个节点删除
    }

总结

以上就是关于链表的经典初级算法的集合,希望通过以上的联系,可以让大家更好的掌握关于链表的算法。下期见,谢谢~

标签:head,null,ListNode,next,链表,算法,数据结构,节点
From: https://blog.csdn.net/weixin_64178283/article/details/143177787

相关文章

  • 计算机毕业设计项目推荐,基于协同过滤算法的短视频推荐系统设计与实现30213(开题答辩+程
    摘 要现阶段,社会的发展和科技的进步,以及大数据时代下纷繁数据信息的融合,使得人们在生产及生活过程中,都将会接收到各种类型的数据信息,而通过计算机技术与网络技术,则能够将众多人们所不了解或不常用的信息,以简单的模式转化并传递给人们,使得人们的生产及生活质量得以显著提升......
  • 使用OpenSSl库实现AES-GCM-128算法(C语言)
    在C语言中使用OpenSSL库实现AES-GCM-128算法,并生成GMAC(GaloisMessageAuthenticationCode)消息认证码,通过以下步骤完成:初始化加密环境:创建一个EVP_CIPHER_CTX结构体,用于存储加密过程中的所有必要信息。设置加密算法:指定使用AES-GCM模式,以及密钥和IV(初始化向量)。处理附加认证......
  • 百度大模型算法工程师二面:我的亲身经历分享!
    百度大模型算法工程师面试题应聘岗位:百度大模型算法工程师面试轮数:第二轮整体面试感觉:偏简单面试过程回顾1.自我介绍在自我介绍环节,我清晰地阐述了个人基本信息、教育背景、工作经历和技能特长,展示了自信和沟通能力。2.Leetcode题具体题意记不清了,但是类似【2......
  • 稀疏八叉树算法(SVO)示例
    稀疏八叉树算法示例:frommatplotlibimportpyplotaspltimportnumpyasnpclassOctreeNode:def__init__(self,bounds,depth=0):self.bounds=bounds#体素的空间边界self.children=[]#存储子节点self.depth=depth#当前......
  • Ruby数据结构介绍
    Ruby中的String 对象存储并操作一个或多个字节的任意序列,通常表示那些代表人类语言的字符。最简单的字符串是括在单引号(单引号字符)内。在引号标记内的文本是字符串的值:'ThisisasimpleRubystringliteral'如果您需要在单引号字符串内使用单引号字符,那么需要在单引号......
  • 每日算法一练:剑指offer——数组篇(4)
    数据流中的中位数        中位数 是有序整数列表中的中间值。如果列表的大小是偶数,则没有中间值,中位数是两个中间值的平均值。例如,[2,3,4] 的中位数是 3[2,3] 的中位数是 (2+3)/2=2.5设计一个支持以下两种操作的数据结构:voidaddNum(intnum) -从数据......
  • 代码随想录-环形链表II
    题目与解析    题目链接:环形链表II本题两个关键点,1、确定有环2、确定环的入口位置 提供两种解法,第一种是我借助了一个辅助的列表来记录指针,空间复杂度O(n)比较无脑 第二种是Carl哥的双指针法,又是套圈问题,虽然很难想,但还是推荐这种方式,这才是算法解法一:publ......
  • 代码随想录-链表相交
    题解与说明要注意区分链表相交是指针相等,而不是值相等。这里当时没有想清楚,还以为leetcode的样例一给错了,原来人家是想强调这个问题哈哈这里给出三种解法:(leetcode格式)①看了代码随想录的解释后,自己写的代码。②看了代码随想录的代码后,对原有的代码循环进行优化。③......
  • 单链表的学习和总结
    单链表的学习和总结1.1 反转链表1.1.1 记录leetcode的题目206.反转链表92.反转链表II25.K个一组翻转链表1.1.2整理总结1.记录链表翻转的几种方法:目前我认为“头插法”更好理解https://leetcode.cn/problems/reverse-linked-list/solutions/2948411/dan-lian-......
  • SpringBoot-基于DFA算法实现敏感词过滤
    基于DFA实现敏感词过滤笔记部分来源自黑马程序员DFA全称为:DeterministicFiniteAutomaton,即确定有穷自动机。存储:一次性的把所有的敏感词存储到了多个map中,就是下图表示这种结构敏感词:冰毒、大麻、大坏蛋检索的过程开始实现1、创建数据库表CREATETABLE`sensit......