链表 | 热题Hot100 | Leetcode

这篇博客详细探讨了LeetCode中关于链表的热门问题,包括两数相加、删除链表倒数第N个节点、合并两个有序链表、排序链表等。作者分享了解题过程中遇到的问题及解决方案,同时对比了不同方法的时间和空间复杂度,提供了多种算法思路和优化技巧。
摘要由CSDN通过智能技术生成

链表

2. 两数相加

两个链表代表两个整数,第一个节点都是从个位开始。将两个整数相加,输出链表。

主要就是靠取模求值、除法求进位。

(1)我的求解过程遇到的问题

  • 问题1:是否要用额外的链表。最后用了,因为不用的话,要么在循环中修改L1指针的当前值,但当L1为空时,无法再在L1链表后面添加节点(有可能L2比L1长,或者还要再添加进位);要么在循环中修改L1.next的值,这样可以在L1.next为空时在L1链表后面添加节点,但这样也不行,当l1.next == null && l2.next != null时,L1.next会被添加新的节点作为下一位的结果,再回来想进入这个条件就无法进入了,因为L1.next != null了。除非当L1结束后,再循环L2剩余部分进行L2.val + add的操作,又很麻烦。
  • 问题2:不需要额外的指针指向L1和L2,可以直接动L1和L2。但需要一个额外的指针指向answer链表,因为answer需要记住头。
  • 注意边界条件,何时退出循环。
  • 我的代码:2ms / 99.89% / O(n) ; 40.3MB / 93.78% / O(n) 。
    public ListNode addTwoNumbers(ListNode l1, ListNode l2) {
        ListNode ans;
        ListNode p;
        int add = 0;
        if (l1 == null) {
            return l2;
        } else if (l2 == null) {
            return l1;
        }
        else {
            ans = new ListNode((l1.val + l2.val + add) % 10);
            p = ans;
            add = (l1.val + l2.val + add) / 10;
            l1 = l1.next;
            l2 = l2.next;
        }
        while (l1 != null || l2 != null || add == 1) {
            if (l1 == null && l2 != null) {
                p.next = new ListNode((l2.val + add) % 10);
                add = (l2.val + add) / 10;
                p = p.next;
                l2 = l2.next;
            } else if (l2 == null && l1 != null) {
                p.next = new ListNode((l1.val + add) % 10);
                add = (l1.val + add) / 10;
                p = p.next;
                l1 = l1.next;
            } else if (l2 == null && l1 == null) {
                p.next = new ListNode(1);
                break;
            } else {
                p.next = new ListNode((l1.val + l2.val + add) % 10);
                add = (l1.val + l2.val + add) / 10;
                p = p.next;
                l1 = l1.next;
                l2 = l2.next;
            }
        }
        return ans;
    }

(2)题解 3ms / 26.61% / O(n) ; 40.3MB / 93.78% / O(n) 。

public ListNode addTwoNumbers(ListNode l1, ListNode l2) {
    ListNode dummyHead = new ListNode(0);
    ListNode p = l1, q = l2, curr = dummyHead;
    int carry = 0;
    while (p != null || q != null) {
        int x = (p != null) ? p.val : 0;
        int y = (q != null) ? q.val : 0;
        int sum = carry + x + y;
        carry = sum / 10;
        curr.next = new ListNode(sum % 10);
        curr = curr.next;
        if (p != null) p = p.next;
        if (q != null) q = q.next;
    }
    if (carry > 0) {
        curr.next = new ListNode(carry);
    }
    return dummyHead.next;
}

主要收获:

  • 哑节点:省的初始化answer链表了。我前面写那一大堆实际上都是为了初始化这个链表。但是,这样运行时间多出了1ms,也许直接判断L1和L2是否为空能更省时间。
  • 代码更简洁,使用三目运算符简化了我的三个大if。不过p和q没必要。

19. 删除链表的倒数第N个节点

给定一个链表和一个n。

我的方法:双指针,两个指针之间差n

(1)我的求解过程遇到的问题

  • 考虑特殊情况,例如需要删除列表的头部(包括链表中只有一个节点)。这即是b为空的情况,删除链表的头部时,则head指向头,b指向null。所以直接return。解法没有什么复杂的,但是特殊情况需要着重考虑。

  • 我的代码:

    public static ListNode removeNthFromEnd(ListNode head, int n) {
        //第一个if也可以不写,考虑了链表只有一个节点的情况
        //但在if (b == null)这一语句中也可以处理链表只有一个节点的情况
        //如果删除这个if,时间会多1ms
        if(head.next == null) {
            return null;
        }
        ListNode a = head;
        ListNode b = head;
        //让b指向head节点后的第n个节点
        for (int i = 0; i < n; i++) {
            b = b.next;
        }
        //记得考虑b为空的情况
        if (b == null) {
            return a.next;
        }
        while (b.next != null) {
            b = b.next;
            a = a.next;
        }
        a.next = a.next.next;
        return head;
    }

执行用时:0 ms, 在所有 Java 提交中击败了100.00%的用户

内存消耗:38.1 MB, 在所有 Java 提交中击败了5.43%的用户

时间复杂度:O(L)

空间复杂度:O(1)

(2)题解

  • 方法一:两次遍历法

第一遍找到链表长度L,第二遍让指针找到第L-N个节点并删除

注意哑节点的设置,可以避免一些特殊情况,如需要删除链表头部

public ListNode removeNthFromEnd(ListNode head, int n) {
    ListNode dummy = new ListNode(0);
    dummy.next = head;
    int length  = 0;
    ListNode first = head;
    while (first != null) {
        length++;
        first = first.next;
    }
    length -= n;
    first = dummy;
    while (length > 0) {
        length--;
        first = first.next;
    }
    first.next = first.next.next;
    return dummy.next;
}

时间复杂度:O(L)

空间复杂度:O(1)

  • 方法二:双指针

思路与我的相同,但设置了一个哑节点,用于避免需要删除头节点。

但多花了1 ms, 时间为1ms,在所有 Java 提交中击败了28.94%的用户。内存一样。

我发现哑节点好用是好用,能够避免一些特殊情况,但是时间一般都会多1ms。如果直接if处理特殊情况会快一些。

在这里插入图片描述

21.合并两个有序链表

(1)我的方法

双指针无哑节点

内存用时同下

  • 注意特殊情况

  • 其中有有两处写的很蠢,以后注意修改

    public static ListNode mergeTwoLists(ListNode l1, ListNode l2) {
        //考虑链表为空的情况
        if (l1 == null) return l2;
        if (l2 == null) return l1;
        //!!!a和b其实没必要,直接用l1和l2就行!!!
        ListNode a = l1;
        ListNode b = l2;
        ListNode res;
        //因为没有哑节点,初始化result链表只能这样初始化
        if (a.val >= b.val) {
            res = new ListNode(b.val);
            b = b.next;
        } else {
            res = new ListNode(a.val);
            a = a.next;
        }
        ListNode p = res;
        //双指针,谁小把谁放进res。这里一开始也犯迷糊,只需要比较当前双指针即可,
        //不用管指针后面的数的大小
        while (a != null && b != null) {
            if (a.val <= b.val) {
                p.next = new ListNode(a.val);
                p = p.next;
                a = a.next;
            } else {
                p.next = new ListNode(b.val);
                p = p.next;
                b = b.next;
            }
        }
        //!!!处理遍历完其中一个链表的情况 这里非常蠢,不需要遍历!!!
        if (a == null) {
            while (b != null) {
                p.next = new ListNode(b.val);
                p = p.next;
                b = b.next;
            }
        } else {
            while (a != null) {
                p.next = new ListNode(a.val);
                p = p.next;
                a = a.next;
            }
        }
        return res;
    }

(2)题解

  • 方法1:双指针+哑节点

哑节点可以避免特殊情况

执行用时:1 ms, 在所有 Java 提交中击败了62.95%的用户

内存消耗:39.4 MB, 在所有 Java 提交中击败了57.94%的用户

        //哨兵节点,可以免去多余的if判断
        ListNode res = new ListNode(0);
        ListNode p = res;
        //根本不用ab指针,只要用l1和l2即可。
        while (l1 != null && l2 != null) {
            if (l1.val <= l2.val) {
                // 且根本不用新建节点,只需要指向l1点即可
                p.next = l1;
                l1 = l1.next;
            } else {
                p.next = l2;
                l2 = l2.next;
            }
            p = p.next;
        }
        //不需要循环插入,只要指向剩余的即可。
        if (l1 == null) {
            p.next = l2;
        } else {
            p.next = l1;
        }
       //上面这个if-else可以用三目运算符优化:
       //p.next = l1 == null ? l2 : l1;
        return res.next;

时间复杂度:O(n + m),其中 nn 和 mm 分别为两个链表的长度。因为每次循环迭代中,l1 和 l2 只有一个元素会被放进合并链表中, 因此 while 循环的次数不会超过两个链表的长度之和。所有其他操作的时间复杂度都是常数级别的,因此总的时间复杂度为 O(n+m)。

空间复杂度:O(1)。我们只需要常数的空间存放若干变量。

  • 方法2:递归

在这里插入图片描述
在这里插入图片描述

class Solution {
    public ListNode mergeTwoLists(ListNode l1, ListNode l2) {
        if (l1 == null) {
            return l2;
        }
        else if (l2 == null) {
            return l1;
        }
        else if (l1.val < l2.val) {
            l1.next = mergeTwoLists(l1.next, l2);
            return l1;
        }
        else {
            l2.next = mergeTwoLists(l1, l2.next);
            return l2;
        }
    }
}

时间复杂度:O(n + m),其中 n 和 m分别为两个链表的长度。因为每次调用递归都会去掉 l1 或者 l2 的头节点(直到至少有一个链表为空),函数 mergeTwoList 至多只会递归调用每个节点一次。因此,时间复杂度取决于合并后的链表长度,即 O(n+m)。

空间复杂度:O(n + m),其中 n 和 m 分别为两个链表的长度。递归调用 mergeTwoLists 函数时需要消耗栈空间,栈空间的大小取决于递归调用的深度。结束递归调用时 mergeTwoLists 函数最多调用 n+m次,因此空间复杂度为 O(n+m)。

148. 排序链表

(1)方法1:快慢指针找中点,递归二分,归并排序

题目要求时间空间复杂度分别为O(nlogn)和O(1),根据时间复杂度我们自然想到二分法,从而联想到归并排序;

对数组做归并排序的空间复杂度为 O(n),分别由新开辟数组O(n)和递归函数调用O(logn)组成,而根据链表特性:

​ 数组额外空间:链表可以通过修改引用来更改节点顺序,无需像数组一样开辟额外空间;
​ 递归额外空间:递归调用函数将带来O(logn)的空间复杂度,因此若希望达到O(1)空间复杂度,则不能使用递归。

通过递归实现链表归并排序,有以下两个环节:

  • 分割 cut 环节: 找到当前链表中点,并从中点将链表断开(以便在下次递归 cut 时,链表片段拥有正确边界);使用快慢指针找中点。

  • 合并 merge 环节: 将两个排序链表合并,转化为一个排序链表。直接使用21题的方法即可。

在这里插入图片描述

    public ListNode sortList(ListNode head) {
        if (head == null || head.next == null) {
            return head;
        }
        // 找中点
        ListNode fast = head;
        ListNode slow = head;
        ListNode lastSlow = head;
        while (fast != null && fast.next != null) {
            // 注意:快慢指针时,要让f和f.next都不为空。
            // 看代码中,用到f的字段,说明f不为空;用到f.next的字段,说明f.next也不为空
            fast = fast.next.next;
            lastSlow = slow;
            slow = slow.next;
        }
        lastSlow.next = null;
        // 分割cut环节
        ListNode left = sortList(head);
        ListNode right = sortList(slow);
        // 合并merge环节
        return mergeTwoLists(left, right);
    }

时间复杂度 O(nlogn),在每一层二分时,都要遍历一遍链表;合并时,还要遍历一遍链表。共二分logn次。

执行用时:4 ms, 在所有 Java 提交中击败了63.40%的用户

内存消耗:42 MB, 在所有 Java 提交中击败了5.88%的用户

(2)方法2:迭代二分,归并排序

对于非递归的归并排序,需要使用迭代的方式替换cut环节:

我们知道,cut环节本质上是通过二分法得到链表最小节点单元,再通过多轮合并得到排序结果。

每一轮合并merge操作针对的单元都有固定长度intv,例如:
第一轮合并时intv = 1,即将整个链表切分为多个长度为1的单元,并按顺序两两排序合并,合并完成的已排序单元长度为2。
第二轮合并时intv = 2,即将整个链表切分为多个长度为2的单元,并按顺序两两排序合并,合并完成已排序单元长度为4。
以此类推,直到单元长度intv >= 链表长度,代表已经排序完成。

根据以上推论,我们可以仅根据intv计算每个单元边界,并完成链表的每轮排序合并,例如:
当intv = 1时,将链表第1和第2节点排序合并,第3和第4节点排序合并,……。
当intv = 2时,将链表第1-2和第3-4节点排序合并,第5-6和第7-8节点排序合并,……。
当intv = 4时,将链表第1-4和第5-8节点排序合并,第9-12和第13-16节点排序合并,……。
此方法时间复杂度O(nlogn),空间复杂度O(1)。
在这里插入图片描述

具体实现:

  1. 求出链表的长度
  2. 设置哑节点(用于生成排序的答案)和intv,准备开始循环
  3. 最外层循环是每一层二分,也就是同样的intv为一层,用于处理每一层中的链表。在每次进入该层循环后,都需要重新设置result的指针p,因为需要重新排序;还要重新设置h,让它指向头(不是head,是目前的第一个数)。
  4. 第二层循环用于合并每一对h1和h2,即单独处理每一对h1和h2。首先找到h1,再跟着h移动找到h2.让h最终停在下一个h1处。接着合并h1和h2,与合并两个有序链表方法类似。最后处理p指针。
    public static ListNode sortList2(ListNode head) {
        // 求出链表的长度
        ListNode cur = head;
        int len = 0;
        while(cur != null) {
            len++;
            cur = cur.next;
        }

        int intv = 1;
        // 哑节点,用于合并两个链表时消除某些特殊情况
        ListNode result = new ListNode(0);
        // 需要进行这一步,是为了初始化,原因是在第一次进入循环执行ListNode h = result.next时,要指向head
        // 随着循环中的代码执行,result.next就不一定是head了,而是最小的
        result.next = head;
        while (intv < len) {
            // 注意h要放在这里,每一次进入循环都要指向头(不是head,是目前的第一个数)
            ListNode h = result.next;
            // result的指针,用于指向下一个最小的数,这里要指向result而不是head是因为,head不一定是最小的
            // 注意p要放在这里,因为每一次进入循环都要重新排一遍整个链表
            ListNode p = result;
            //------该循环用于合并每一对h1和h2--------
            // h不为空说明还有h1,虽然不一定有h2,但还是要进入切割与合并
            while (h != null) {
                // -----首先使用h来找h1和h2-------
                // h一开始指向的节点是h1的头
                ListNode h1 = h;
                //接下来移动h去找h2的头
                for (int i = 0; i < intv && h != null; i++){
                    h = h.next;
                }
                // h为空,说明没有h2了,退出合并
                if (h == null) break;
                // 否则h2就是h
                ListNode h2 = h;
                //再移动h去找下一轮循环的h1
                int i = 0;
                for (; i < intv && h != null; i++) {
                    h = h.next;
                }

                //--------接下来合并h1和h2--------
                int h1len = intv;
                int h2len = i;
                // 在原来的方法中,while的条件写为:h1 != null && h2 != null
                // 但本方法中,h1链表的结束并不意味着h1 == null,因此需要根据h1和h2链表的长度判断h1和h2是否到尾了。
                while (h1len != 0 && h2len != 0) {
                    if (h1.val <= h2.val) {
                        p.next = h1;
                        p = p.next;
                        h1 = h1.next;
                        h1len -= 1;
                    } else {
                        p.next = h2;
                        p = p.next;
                        h2 = h2.next;
                        h2len -= 1;
                    }
                }
                p.next = h1len == 0 ? h2 : h1;

                //在完成合并h1和h2后,还需要让result的指针p指向目前排好序的链表的最后,这样下次循环可以在result后面继续添加
                while (h1len > 0 || h2len > 0) {
                    p = p.next;
                    h1len -= 1;
                    h2len -= 1;
                }
                // 这样做的目的是让h为空时,让p的下一位指向空,完成排序,不要自我循环
                // 对循环本身并没有其他影响,因为没有加p = p.next。
                // 所以在下一次添加最小的进入result链表时,p.next = ??就会取代这一句设置的p.next = h
                p.next = h;
            }
            intv *= 2;
        }
        return result.next;
    }

太麻烦了…而且耗时和内存都更差了,玄学

执行用时:5 ms, 在所有 Java 提交中击败了45.50%的用户

内存消耗:42.4 MB, 在所有 Java 提交中击败了5.88%的用户

23. 合并K个排序链表

21题的进阶版

(1)我的求解过程

并没有过,因此只是写下自己的思路:递归,每次当前链表和下一个链表合并,让下一个链表等于新链表,然后指针移到下一个链表。然而,这样做时间花费巨大:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-13fjhx6C-1592816267309)(C:\Users\dell\AppData\Local\Packages\Microsoft.Windows.ShellExperienceHost_cw5n1h2txyewy\TempState\ScreenClip{EE81FBBD-12B5-4A3D-8D2C-7AE900F3AF2A}.png)]

当输入样例的K为好几千、n为1的时候,样例失败因为超过了时间限制。

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

    private ListNode[] helper(ListNode[] lists, int start) {
        if (start + 1== lists.length) {
            return lists;
        }
        lists[start + 1] = mergeTwoLists(lists[start], lists[start + 1]);
        return helper(lists, start + 1);
    }

(2) 题解:

  • 方法一:分治法

在这里插入图片描述
在这里插入图片描述

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

    private ListNode merge(ListNode[] lists, int left, int right) {
        if (left == right) return lists[left];
        int mid = left + (right - left) / 2;
        //注意,二分法这么写可以防止当两个数很大的时候溢出
        ListNode l1 = merge(lists, left, mid);
        ListNode l2 = merge(lists, mid + 1, right);
        return mergeTwoLists(l1, l2);
    }

执行用时:2 ms, 在所有 Java 提交中击败了95.46%的用户

内存消耗:40.8 MB, 在所有 Java 提交中击败了69.12%的用户

时间复杂度:第一轮合并k/2组链表,每一组用时2n,总的时间为kn;第二轮合并k/4组链表,每一组用时4n,总的时间为kn……一共合并了多少轮,也就是k个链表被多少次二分,即一共log_2_k次二分。总时间为O(kn*logk).

空间复杂度:递归会使用到O*(log*k) 空间代价的栈空间。

- 方法2:优先队列(待完成)

141. 链表是否有环

(1)基础知识

Java集合主要由2大体系构成,分别是Collection体系和Map体系,其中Collection和Map分别是2大体系中的顶层接口。

Collection主要有三个子接口,分别为List(列表)、Set(集)、Queue(队列)。其中,List、Queue中的元素有序可重复,而Set中的元素无序不可重复;

List中主要有ArrayList、LinkedList两个实现类;Set中则是有HashSet实现类;而Queue是在JDK1.5后才出现的新集合,主要以数组和链表两种形式存在。

Map同属于java.util包中,是集合的一部分,但与Collection是相互独立的,没有任何关系。Map中都是以key-value的形式存在,其中key必须唯一,主要有HashMap、HashTable、TreeMap三个实现类。
在这里插入图片描述
Java List详解(包括ArrayList和LinkedList的解析与操作)

(2)HashSet

Java中Set总结
底层由HashMap实现;
不允许出现重复因素;
允许插入Null值;
元素无序(添加顺序和遍历顺序不一致);
线程不安全,若2个线程同时操作HashSet,必须通过代码实现同步。

我的理解:可以将HashSet看作简单的集合。

  • 初始化:Set<Type> name = new HashSet<>();
  • 元素添加:hashSet.add(xxx);
  • 元素移除:hashSet.remove(xxx)
  • 元素清除:hashSet.clear();
  • 判断集合是否为空:hashSet.isEmpty() 返回boolean
  • xxx元素是否存在:hashSet.contains("hello"); 返回boolean
  • 元素数量:hashSet.size()
  • 迭代器:hashset.iterator()
    例:
    Set<String> hashset = new HashSet<>();
    hashset.add("First");
    hashset.add("Second");
    hashset.add("Third");
    Iterator<String> itr = hashset.iterator();
    while(itr.hasNext()){
        System.out.println(itr.next());
    }

(3)题目

给定一个链表,判断链表中是否有环。

为了表示给定链表中的环,我们使用整数 pos 来表示链表尾连接到链表中的位置(索引从 0 开始)。 如果 pos 是 -1,则在该链表中没有环。

示例 1:
输入:head = [3,2,0,-4], pos = 1
输出:true
解释:链表中有一个环,其尾部连接到第二个节点。

示例 2:
输入:head = [1,2], pos = 0
输出:true
解释:链表中有一个环,其尾部连接到第一个节点。

示例 3:
输入:head = [1], pos = -1
输出:false
解释:链表中没有环。

进阶:
你能用 O(1)(即,常量)内存解决此问题吗?

(4)题解

  • 方法一:哈希表

  • 思路

我们可以通过检查一个结点此前是否被访问过来判断链表是否为环形链表。常用的方法是使用哈希表。

  • 算法

我们遍历所有结点并在哈希表中存储每个结点的引用(或内存地址)。如果当前结点为空结点 null(即已检测到链表尾部的下一个结点),那么我们已经遍历完整个链表,并且该链表不是环形链表。如果当前结点的引用已经存在于哈希表中,那么返回 true(即该链表为环形链表)。

public boolean hasCycle(ListNode head) {
    Set<ListNode> nodesSeen = new HashSet<>();
    while (head != null) {
        if (nodesSeen.contains(head)) {
            return true;
        } else {
            nodesSeen.add(head);
        }
        head = head.next;
    }
    return false;
}
  • 复杂度分析

时间复杂度:O(n),对于含有 n 个元素的链表,我们访问每个元素最多一次。添加一个结点到哈希表中只需要花费 O(1)的时间。

空间复杂度:O(n),空间取决于添加到哈希表中的元素数目,最多可以添加 n 个元素。

  • 方法二:快慢指针

  • 思路

想象一下,两名运动员以不同的速度在环形赛道上跑步会发生什么?

  • 算法

通过使用具有 不同速度 的快、慢两个指针遍历链表,空间复杂度可以被降低至 O(1)。慢指针每次移动一步,而快指针每次移动两步。

如果列表中不存在环,最终快指针将会最先到达尾部,此时我们可以返回 false。

现在考虑一个环形链表,把慢指针和快指针想象成两个在环形赛道上跑步的运动员(分别称之为慢跑者与快跑者)。而快跑者最终一定会追上慢跑者。这是为什么呢?考虑下面这种情况(记作情况 A)- 假如快跑者只落后慢跑者一步,在下一次迭代中,它们就会分别跑了一步或两步并相遇。

其他情况又会怎样呢?例如,我们没有考虑快跑者在慢跑者之后两步或三步的情况。但其实不难想到,因为在下一次或者下下次迭代后,又会变成上面提到的情况 A。

public boolean hasCycle(ListNode head) {
    if (head == null || head.next == null) {
        return false;
    }
    ListNode slow = head;
    ListNode fast = head.next;
    while (slow != fast) {
        if (fast == null || fast.next == null) {
            return false;
        }
        slow = slow.next;
        fast = fast.next.next;
    }
    return true;
}
  • 复杂度分析

时间复杂度:O(n),让我们将 n 设为链表中结点的总数。为了分析时间复杂度,我们分别考虑下面两种情况。

  1. 链表中不存在环:
    快指针将会首先到达尾部,其时间取决于列表的长度,也就是 O(n)。

  2. 链表中存在环:
    我们将慢指针的移动过程划分为两个阶段:非环部分与环形部分:
    (1). 慢指针在走完非环部分阶段后将进入环形部分:此时,快指针已经进入环中 迭代次数 = 非环部分长度=N
    (2). 两个指针都在环形区域中:考虑两个在环形赛道上的运动员 - 快跑者每次移动两步而慢跑者每次只移动一步。其速度的差值为 1,因此需要经过 (二者之间距离÷速度差值)次循环后,快跑者可以追上慢跑者。这个距离几乎就是 “环形部分长度 K” 且速度差值为 1,我们得出这样的结论 迭代次数 近似于“环形部分长度 K".

因此,在最糟糕的情形下,时间复杂度为 O(N+K),也就是 O(n)。

空间复杂度:O(1),我们只使用了慢指针和快指针两个结点,所以空间复杂度为 O(1)。

142. 环形链表II

(1)题解

  • 方法1:哈希表

如果我们用一个 Set 保存已经访问过的节点,我们可以遍历整个列表并返回第一个出现重复的节点。

public class Solution {
    public ListNode detectCycle(ListNode head) {
        Set<ListNode> visited = new HashSet<ListNode>();

        ListNode node = head;
        while (node != null) {
            if (visited.contains(node)) {
                return node;
            }
            visited.add(node);
            node = node.next;
        }
        return null;
    }
  • 方法2:双指针+数学推理

设,非环长度为x,环的长度为y;快指针走了n圈环,慢指针走了m圈环;快慢指针相遇的地方与入环点的距离为a。快指针走的长度是慢指针的两倍,可以列出公式:

x + ny + a = 2(x + my + a)

得到:x + a =(n-2m)y

说明,x + a是y的整数倍。那么,让两个指针从0和x+a处以同样的速度走,从0开始的指针走到入环点x处走的路程是(n-2m)y - a;从x + a处开始的指针走同样的路程(n-2m)y - a正好也走到x处(直接想走了整数圈的y环,在走最后一圈环的时候少走了a步,正好到了入环点),两个指针相遇。

    public ListNode detectCycle(ListNode head) {
        if (head == null || head.next == null) return null;
        //注意head.next == null这个条件,否则当链表无环且只有一个节点时会出错
        ListNode f = head;
        ListNode s = head;
        while (f.next != null) {
            s = s.next;
            f = f.next.next;
            if(f == s) {
                break;
            }
            if(f == null) return null;
        }
        if (f != s) return null;
        s = head;
        while (f != s) {
            s = s.next;
            f = f.next;
        }
        return f;
    }

执行用时:0 ms, 在所有 Java 提交中击败了100.00%的用户

内存消耗:39.8 MB, 在所有 Java 提交中击败了7.14%的用户

160. 相交链表

  • 方法1:哈希表

遍历链表 A 并将每个结点的地址/引用存储在哈希表中。然后检查链表 B 中的每一个结点 b_i 是否在哈希表中。若在,则 b_i为相交结点。

时间复杂度 : O(m+n)。

空间复杂度 : O(m)或 O(n)。

  • 方法2:双指针法

指针a和指针b同时遍历A和B链表,当a到头之后让a=headB,b也同理。这样就能相当于消去了AB链表的长度差,让a和b在相交处相遇。

时间复杂度 : O(m+n)。

空间复杂度 : O(1)。

注意,不需要考虑那种 当第二轮循环没有遇到相交节点时要打断循环返回空值 的问题。因为如果没有遇到相交节点,a和b指针会在null处相等,因为走的长度一样,都是a + b的长度。

public ListNode getIntersectionNode(ListNode headA, ListNode headB) {
    if (headA == null || headB == null) return null;
    ListNode pA = headA, pB = headB;
    while (pA != pB) {
        pA = pA == null ? headB : pA.next;
        pB = pB == null ? headA : pB.next;
    }
    return pA;
}

206. 反转链表

  • 方法1::递归—我的方法
// 我的方法
public ListNode reverseList(ListNode head) {
        if (head == null) return null;
        ListNode rest = head.next;
        head.next = null;
        return reverseList(head, rest);
 }
    
private ListNode reverseList(ListNode reversed, ListNode rest) {
        if (rest == null) {
            return reversed;
        }
        ListNode newrest = rest.next;
        rest.next = reversed;
        return reverseList(rest, newrest);
 }

题解的递归方法:

public ListNode reverseList(ListNode head) {
    if (head == null || head.next == null) return head;
    ListNode p = reverseList(head.next);
    head.next.next = head;
    head.next = null;
    return p;
}

执行用时:0 ms, 在所有 Java 提交中击败了100.00%的用户

内存消耗:39.7 MB, 在所有 Java 提交中击败了5.06%的用户

时间复杂度:O(n)O(n),假设 nn 是列表的长度,那么时间复杂度为 O(n)O(n)。
空间复杂度:O(n)O(n),由于使用递归,将会使用隐式栈空间。递归深度可能会达到 nn 层。

  • 方法2:迭代
public ListNode reverseList(ListNode head) {
    ListNode prev = null;
    ListNode curr = head;
    while (curr != null) {
        ListNode nextTemp = curr.next;
        curr.next = prev;
        prev = curr;
        curr = nextTemp;
    }
    return prev;
}

执行用时:0 ms, 在所有 Java 提交中击败了100.00%的用户

内存消耗:39.5 MB, 在所有 Java 提交中击败了5.06%的用户

时间复杂度:O(n)O(n),假设 nn 是列表的长度,时间复杂度是 O(n)O(n)。

空间复杂度:O(1)O(1)。

234. 回文链表

  • 方法一:复制到数组再用双指针

我们可以使用双指针法来比较两端的元素,并向中间移动。一个指针从起点向中间移动,另一个指针从终点向中间移动。需要O(n)时间。O(n)空间。

  • 方法二:递归

如果使用递归反向迭代节点,同时使用递归函数外的变量向前迭代,就可以判断链表是否为回文。

currentNode 指针是先到尾节点,由于递归的特性再从后往前进行比较。frontPointer 是递归函数外的指针。若 currentNode.val != frontPointer.val 则返回 false。反之,frontPointer 向前移动并返回 true。

之所以起作用的原因是递归处理节点的顺序是相反的。由于递归,从本质上,我们同时在正向和逆向迭代。

class Solution {
    //class variable,用于从前向后迭代
    private ListNode frontPointer;
    
    private boolean recursivelyCheck(ListNode currentNode) {
        // 三种base case:
        //1.目前节点为空,说明比较已经结束;
        if (currentNode != null) {
            //2.该层的下一层比较结果为false,这一层将false结果传递给上一层
            if (!recursivelyCheck(currentNode.next)) return false;
            //3.从前向后迭代的节点 与 从后向前迭代的节点 相比较,如果值不同,说明不是回文
            if (currentNode.val != frontPointer.val) return false;
            //base case都不满足,则frontPointer从前向后加一个。
            frontPointer = frontPointer.next;
        }
        return true;
    }

    public boolean isPalindrome(ListNode head) {
        frontPointer = head;
        return recursivelyCheck(head);
    }
}

注意迭代方法的代码顺序。先迭代,再比较,再更改frontpointer。这三句顺序不能变,所以只能把if (currentNode != null)和return true写在最外层,因为必须要有一个不在if语句里面的return语句。

需要O(n)时间。O(n)空间。

  • 方法三(也是我的方法):快慢指针找中点,翻转后半部分链表

我们可以分为以下几个步骤:

  1. 找到前半部分链表的尾节点。
  2. 反转后半部分链表。
  3. 判断是否为回文。
  4. 恢复链表。
  5. 返回结果。
    public static boolean isPalindrome(ListNode head) {
        if( head == null || head.next == null) return true;
        // 找到中点slow
        ListNode f = head;
        ListNode s = head;
        while (f != null) {
            if (f.next == null) {
                s = s.next;
                break;
            }
            f = f.next.next;
            s = s.next;
        }
        //翻转后半部分链表
        ListNode firstPart = head;
        ListNode secondPart = reverseList(s);
        //两部分链表相比较
        while (secondPart != null) {
            if (secondPart.val != firstPart.val) {
                return false;
            }
            firstPart = firstPart.next;
            secondPart = secondPart.next;
        }
        // 恢复链表结构(对于解题没用,实际应用中需要)
        firstPart.next = reverseList(secondPart);
        return true;
    }

需要O(n)时间。O(1)空间。

执行用时:1 ms, 在所有 Java 提交中击败了99.67%的用户

内存消耗:43.3 MB, 在所有 Java 提交中击败了8.11%的用户

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
智慧校园建设方案旨在通过融合先进技术,如物联网、大数据、人工智能等,实现校园的智能化管理与服务。政策的推动和技术的成熟为智慧校园的发展提供了基础。该方案强调了数据的重要性,提出通过数据的整合、开放和共享,构建产学研资用联动的服务体系,以促进校园的精细化治理。 智慧校园的核心建设任务包括数据标准体系和应用标准体系的建设,以及信息化安全与等级保护的实施。方案提出了一站式服务大厅和移动校园的概念,通过整合校内外资源,实现资源共享平台和产教融合就业平台的建设。此外,校园大脑的构建是实现智慧校园的关键,它涉及到数据中心化、数据资产化和数据业务化,以数据驱动业务自动化和智能化。 技术应用方面,方案提出了物联网平台、5G网络、人工智能平台等新技术的融合应用,以打造多场景融合的智慧校园大脑。这包括智慧教室、智慧实验室、智慧图书馆、智慧党建等多领域的智能化应用,旨在提升教学、科研、管理和服务的效率和质量。 在实施层面,智慧校园建设需要统筹规划和分步实施,确保项目的可行性和有效性。方案提出了主题梳理、场景梳理和数据梳理的方法,以及现有技术支持和项目分级的考虑,以指导智慧校园的建设。 最后,智慧校园建设的成功依赖于开放、协同和融合的组织建设。通过战略咨询、分步实施、生态建设和短板补充,可以构建符合学校特色的生态链,实现智慧校园的长远发展。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值