算法通关村第一关——链表笔记(白银挑战)

1. 面试题 02.07. 链表相交

leetcode 面试题 02.07. 链表相交

image-20230719091825814

1. 思路

没有思路时的思考方式:

其实就是,我们看到题目,要去思考一下常用数据结构和常用的算法思想,这些常用的数据结构和算法思想适用于什么条件的题目,例如这题:

常用的数据结构:数组,链表,队,栈,Hash,集合,树,堆等。

常用的算法思想:查找,排序,双指针,递归,迭代,分治,贪心,回溯和动态规划等。

首先我看题目,可以看出有一段的链表相交,也就是有一段的节点是一样的,那么思路就是,用什么数据结构,将这些结点放进去之后,拿出来可以快速作比较。

  1. 数组?

    使用数组存取和读取都很废空间和时间,而且也很难存,不对

  2. Hash和集合?

​ 好像可以喔,Hash存取,集合存取,先把一条链表存到数据结构,再拿出来与另一条链表对比,即可

  1. 栈和队?

​ 队,好像没有什么用吧。

​ 栈,好像不错栈的话,存进去,先进后出,直接把尾部拿出来对比,完美

2. Hash和集合

HashMap的方式:

public class Solution {
    public ListNode getIntersectionNode(ListNode headA, ListNode headB) {
        if(headA == null || headB == null){
            return null;
        }
        ListNode curA = headA;
        ListNode curB = headB;

        HashMap<ListNode, Integer> hashMap = new HashMap<>();
        while(curA != null){
            hashMap.put(curA, null);
            curA = curA.next;
        }
        while(curB != null){
            if(hashMap.containsKey(curB)){
                return curB;
            }
            curB = curB.next;
        }
        return null;
    }
}

运行,没得问题,但是我们会发现,其实只是使用了key,那还不如直接用集合。

那集合有list和set,我们需要判断一个集合有没有出现某个节点,list只能一个个遍历,set可以contains直接查到,所以我们集合就使用set集合。

public class Solution {
    public ListNode getIntersectionNode(ListNode headA, ListNode headB) {
        if(headA == null || headB == null){
            return null;
        }

        Set<ListNode> set = new HashSet<>();

        while(headA != null){
            set.add(headA);
            headA = headA.next;
        }

        while(headB != null){
            if(set.contains(headB)){
                return headB;
            }
            headB = headB.next;
        }
        
        return null;
    }
}

没有问题啦~但是发现其实时间和Hash差不多,但是数据量大了肯定差别还是有的

3. 栈

使用栈的,时间复杂度为O(m+n),空间复杂度也是,所以简单学一学就行,因为需要使用栈额外的存储链表A和B

public class Solution {
    public ListNode getIntersectionNode(ListNode headA, ListNode headB) {
        Stack<ListNode> stackA = new Stack<>();
        Stack<ListNode> stackB = new Stack<>();

        while (headA != null) {
            stackA.push(headA);
            headA = headA.next;
        }
        while (headB != null) {
            stackB.push(headB);
            headB = headB.next;
        }

        ListNode preNode = null;
        while (!stackA.isEmpty() && !stackB.isEmpty()) {
            if (stackA.peek() == stackB.peek()) {
                preNode = stackA.pop();
                stackB.pop();
            } else {
                break;
            }
        }
        return preNode;
    }
}

4. 拼接字符串

假设,这两个链表有公共交点:

链表A:0-1-2-3-4-5

链表B:a-b-4-5

将两个链表互相拼接:

链表A:0-1-2-3-4-5-a-b-4-5

链表B:a-b-4-5-0-1-2-3-4-5

然后发现,最后两个相等的4-5就是公共交段

所以只需要,访问完本链表,再指针选到另一个链表,然后就能够一起到达公共交点位置

public class Solution {
    public ListNode getIntersectionNode(ListNode headA, ListNode headB) {
        if (headA == null || headB == null) {
            return null;
        }
        ListNode p1 = headA;
        ListNode p2 = headB;

        while(p1 != p2){
            p1 = p1.next;
            p2 = p2.next;
            if(p1 != p2){
                if(p1 == null){
                    p1 = headB;
                }
                if(p2 == null){
                    p2 = headA;
                }
            }
        }
        return p1;
    }
}

5. 双指针

其实根据拼接字符串我们就能感觉出来

假设链表A的长度为a,链表B的长度为b

遍历两遍,第一遍遍历得到|a-b|的差值,第二遍,先让长的链表走|a-b|,再一起遍历,就能找到同一个点

public class Solution {
    public ListNode getIntersectionNode(ListNode headA, ListNode headB) {
        if (headA == null || headB == null) {
            return null;
        }
        ListNode current1 = headA;
        ListNode current2 = headB;
        // 初始化两个链表的长度
        int l1 = 0, l2 = 0;
        // 拿到链表1的长度
        while (current1 != null) {
            current1 = current1.next;
            l1++;
        }
		// 拿到链表2的长度
        while (current2 != null) {
            current2 = current2.next;
            l2++;
        }
        current1 = headA;
        current2 = headB;
		// 计算两个链表的差值
        int sub = l1 > l2 ? l1 - l2 : l2 - l1;
		// 链表1长的情况,先让1走差值
        if (l1 > l2) {
            int a = 0;
            while (a < sub) {
                current1 = current1.next;
                a++;
            }
        }
		// 链表2长的情况,先让2走差值
        if (l1 < l2) {
            int a = 0;
            while (a < sub) {
                current2 = current2.next;
                a++;
            }
        }
		// 然后再一起往前走,相等的时候,就交点
        while (current2 != current1) {
            current2 = current2.next;
            current1 = current1.next;
        }

        return current1;
    }
}

2. 回文链表

leetcode 回文链表

力扣234题:回文链表

我这里一开始想到了两种方法:

  1. 使用栈,一边遍历一边记录,将前一半压入栈,到中间位置的时候,再遍历栈和后面一半
  2. 使用集合,直接按顺序记录,到时候从后往前遍历,即可

思考了一下,使用集合需要遍历两次,使用栈好一点

后面看了算法村的思路:可以直接使用双指针,边遍历边反转,然后再遍历,完美

1. 双指针+反转一半

public static boolean isPalindromeByTwoPoints(ListNode head) {
    if (head == null || head.next == null) {
        return true;
    }
    ListNode slow = head, fast = head;
    ListNode pre = head, prepre = null;
    while(fast != null && fast.next != null){
        // 5. 前指针移动
        pre = slow;
        // 1. 慢指针移动一个位置
        slow = slow.next;
        // 2. 快指针移动两个个位置
        fast = fast.next.next;
        // 3. 前指针,指向它的前一个位置,也就是反转
        pre.next = prepre;
        // 4. 记录前一个指针位置的指针,向前移动
        prepre = pre;
    }
    // 如果不为空,代表,链表个数是单数
    if (fast != null) {
        slow = slow.next;
    }
    // 这时候遍历pre和slow,pre记录了前半部分指针反转后样子和slow后半部分
    // 当都为null,说明是回文
    while (pre != null && slow != null) {
        if (pre.val != slow.val) {
            return false;
        }
        pre = pre.next;
        slow = slow.next;
    }
    return true;
}

那双指针可以找一半,那么压栈也可以找到一半。

时间复杂度为O(n),其中n为链表的长度。空间复杂度为O(1),只使用了常数级别的额外空间。

2. 双指针+压栈一半

可以使用栈结合双指针的方式,不反转链表,改成压栈,效果一样

public boolean isPalindrome(ListNode head) {
    if (head == null || head.next == null) {
        return true;
    }
    ListNode slow = head, fast = head;
    Stack<ListNode> stack = new Stack<>();
    while(fast != null && fast.next != null){
        // 3. 将慢指针移动的结点压进去
        stack.push(slow);
        // 1. 慢指针移动一个位置
        slow = slow.next;
        // 2. 快指针移动两个个位置
        fast = fast.next.next;
    }
    // 如果不为空,代表,链表个数是单数
    if (fast != null) {
        slow = slow.next;
    }
    // 当都为null,说明是回文
    while (slow != null && !stack.isEmpty()) {
        if (slow.val != stack.pop().val) {
            return false;
        }
        slow = slow.next;
    }

    return true;
}

时间复杂度为 O(n),其中 n 为链表的长度。空间复杂度为 O(n/2),即栈的大小,其中 n 为链表的长度。

所以:根据时间复杂度和空间复杂度,很明显还是“双指针+反转”靠谱点

3. 合并两个有序链表

leetcode 合并两个有序链表

3.1 力扣21题:合并两个有序链表

这道题最简单的方式就是新建一个链表,然后一路判断节点val,哪个小就指向谁,不过不够优雅

所以我一开始就直接想的是让两个链表判断互相指向,然后又参考了通关村的讲解进行了一步步修改

1. 直接在原链表进行修改
class Solution {
    public ListNode mergeTwoLists(ListNode list1, ListNode list2) {
        // 如果其中一个链表为空,直接返回另一个链表
        if (list1 == null) {
            return list2;
        }
        if (list2 == null) {
            return list1;
        }

        ListNode result;
        // 选择较小的节点作为结果链表的头节点,并将其保存在result中
        if (list1.val < list2.val) {
            result = list1;
            list1 = list1.next; // 移动到下一个节点
        } else {
            result = list2;
            list2 = list2.next; // 移动到下一个节点
        }

        ListNode current = result; // 当前节点指向结果链表的头节点

        // 循环比较两个链表的当前节点值,选择较小的节点连接到结果链表中
        while (list1 != null && list2 != null) {
            if (list1.val < list2.val) {
                current.next = list1; // 将list1的节点连接到结果链表中
                list1 = list1.next; // 移动到下一个节点
            } else {
                current.next = list2; // 将list2的节点连接到结果链表中
                list2 = list2.next; // 移动到下一个节点
            }
            current = current.next; // 将当前节点向后移动一个位置
        }

        // 将剩余未连接的节点连接到结果链表的末尾
        if (list1 != null) {
            current.next = list1;
        } else {
            current.next = list2;
        }

        return result; // 返回结果链表的头节点
    }
}

空间复杂度为O(1),时间复杂度为O(m+n)

2. 使用new的节点来指向

跟我的方式差不多,但是这个方式更好理解

public static ListNode mergeTwoLists2(ListNode list1, ListNode list2) {
    ListNode newHead = new ListNode(-1); // 新建一个虚拟头节点
    ListNode res = newHead; // 保存结果链表的头节点

    // 遍历两个链表,比较节点的值,并将较小的节点连接到结果链表中
    while (list1 != null && list2 != null) {
        if (list1.val < list2.val) {
            newHead.next = list1;
            list1 = list1.next;
        } else if (list1.val > list2.val) {
            newHead.next = list2;
            list2 = list2.next;
        } else { // 相等的情况,分别接两个链
            newHead.next = list2;
            list2 = list2.next;
            newHead = newHead.next;
            newHead.next = list1;
            list1 = list1.next;
        }
        newHead = newHead.next; // 将当前节点指针向后移动
    }

    // 将剩余未连接的节点连接到结果链表的末尾
    while (list1 != null) {
        newHead.next = list1;
        list1 = list1.next;
        newHead = newHead.next;
    }
    while (list2 != null) {
        newHead.next = list2;
        list2 = list2.next;
        newHead = newHead.next;
    }

    return res.next; // 返回结果链表的头节点
}

空间复杂度为O(1),时间复杂度为O(m+n)

3. 进一步优化

在循环中,我们通过比较节点的值来选择较小的节点连接到结果链表中,并将当前节点指针向后移动。

最后,我们只需要判断哪个链表还有剩余节点未连接完毕,然后将其直接接到结果链表的末尾。

public static ListNode mergeTwoListsMoreSimple(ListNode l1, ListNode l2) {
    ListNode prehead = new ListNode(-1); // 新建一个虚拟头节点
    ListNode prev = prehead; // 保存结果链表的当前节点

    // 遍历两个链表,比较节点的值,并将较小的节点连接到结果链表中
    while (l1 != null && l2 != null) {
        if (l1.val <= l2.val) {
            prev.next = l1;
            l1 = l1.next;
        } else {
            prev.next = l2;
            l2 = l2.next;
        }
        prev = prev.next; // 将当前节点指针向后移动
    }

    // 最多只有一个还未被合并完,直接接上去就行了
    prev.next = l1 == null ? l2 : l1;

    return prehead.next; // 返回结果链表的头节点
}
4. 使用递归

当使用递归解决问题时,可以遵循三要素:递归的定义、递归的拆解、递归的出口。以下是使用递归实现合并两个有序链表的详细步骤:

  1. 首先,定义递归的函数mergeTwoLists,该函数接收两个链表list1list2作为参数,并返回合并后的链表。
  2. 接下来,处理递归的出口条件:
    • 如果list1为空,说明已经遍历完了其中一个链表,直接返回另一个链表list2
    • 如果list2为空,同样说明已经遍历完了其中一个链表,直接返回另一个链表list1
  3. 然后,处理递归的拆解部分:
    • 比较list1list2的当前节点值的大小,如果list1.val小于等于list2.val,则将list1的当前节点连接到合并后的链表中,然后对剩余的部分继续递归调用mergeTwoLists函数,传入list1.nextlist2作为参数。
    • 如果list1.val大于list2.val,则将list2的当前节点连接到合并后的链表中,然后对剩余的部分继续递归调用mergeTwoLists函数,传入list1list2.next作为参数。
  4. 最后,返回合并后的链表。
  5. [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-RxX4LCCa-1690711991692)(C:\Users\Kaizhi\AppData\Roaming\Typora\typora-user-images\image-20230720122025449.png)]

从这个图可以看出,把每一个要指向的位置压进去,最后取出来,就是链表了~

下面是按照这个思路实现的代码:

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;
    }
}

3.2 合并k个升序链表

力扣23:合并K个升序链表

最简单的方式就是将链表两个两个合并,一路遍历过去,简单,面试的时候这么写比较不容易出错

class Solution {
    public ListNode mergeKLists(ListNode[] lists) {
        ListNode res = null;
        for(ListNode list : lists){
            res = mergeTwoLists(res, list);
        }
        return res;
    }

    public ListNode mergeTwoLists(ListNode l1, ListNode l2){
        ListNode humyNode = new ListNode(-1);
        ListNode prev = humyNode;

        while(l1 != null && l2 != null){
            if(l1.val >= l2.val){
                prev.next = l2;
                l2 = l2.next;
            }else{
                prev.next = l1;
                l1 = l1.next;
            }
            prev = prev.next;
        }
        
        prev.next = l1 == null ? l2 : l1;

        return humyNode.next;
    }
}

这个方法是时间最长的,但是呐,最简单的,还有其他方法,我没看,肝不动了

3.3 合并两个链表

leetcode 1669 合并两个链表

这道题有点点无聊,比简单题还简单,就没什么好说的,但是看到标题1669,那就说明算法题出到没得出了?啊哈

class Solution {
    public ListNode mergeInBetween(ListNode list1, int a, int b, ListNode list2) {
        ListNode l1 = list1;
        ListNode l2 = list2;
        for(int i=0; i<a-1; i++){
            l1 = l1.next;
        }
        ListNode la = l1;
        for(int i=a-1; i<b+1; i++){
            l1 = l1.next;
        }
        ListNode lb = l1;
        la.next = l2;
        while(l2.next != null){
            l2 = l2.next;
        }
        l2.next = lb;
        return list1;
    }
}

4. 双指针专题

4.1 寻找中间节点

leetcode 876. 链表的中间结点

快慢指针

这里我直接想到了快慢指针的方法,因为之前做过类似的题目,所以我也不写其他的方法了,这种又快又好

核心的地方就是,快指针比慢指针多走一个点,当快指针到null了,慢指针就是中间点

class Solution {
    public ListNode middleNode(ListNode head) {
        ListNode l1 = head;
        ListNode l2 = head;
        while(l2 != null && l2.next != null){
            l1 = l1.next;
            l2 = l2.next.next;
        }
        return l1;
    }
}

4.2 剑指 Offer 22. 链表中倒数第k个节点

剑指 Offer 22. 链表中倒数第k个节点

前后指针

我也是做过类似的题,所以这道题也是直接写出来了

核心就是:两个指针,一个先走k个节点,然后再一起走,直到null,后走的节点就是位置了

class Solution {
    public ListNode getKthFromEnd(ListNode head, int k) {
        ListNode slow = head;
        while(k != 0){
            head = head.next;
            k--;
        }
        ListNode fast = head;
        while(fast != null){
            slow = slow.next;
            fast = fast.next;
        }
        return slow;
        
    }
}

4.3 旋转链表

leetcode 61. 旋转链表

快慢指针

这道题的做法有点像上一题,一个先走,然后再一起走,到达位置,快指针指向链表头,慢指针指向null,over

class Solution {
    public ListNode rotateRight(ListNode head, int k) {
        if(head == null || k == 0){
            return head;
        }
        ListNode temp = head;
        ListNode fast = head;
        ListNode slow = head;
        int len = 0;
        while(head != null){
            head = head.next;
            len++;
        }
        if(k % len == 0){
            return temp;
        }
        while((k%len) > 0){
            k--;
            fast = fast.next;
        }
        while(fast.next != null){
            fast = fast.next;
            slow = slow.next;
        }
        ListNode res = slow.next;
        slow.next = null;
        fast.next = temp;
        return res;
    }
}

5. 删除链表元素专题

5.1 删除特定节点

leetcode 203. 移除链表元素

我们前面说过,我们删除节点cur时,必须知道其前驱pre节点和后继next节点,然后让pre.next=next。

对于删除,我们注意到首元素的处理方式与后面的不一样。为此,我们可以先创建一个虚拟节点dummyHead,使其指向head,也就是dummyHead.next=head,这样就不用单独处理首节点了
完整的步骤是:
1.我们创建一个虚拟链表头dummyHead,使其next指向head。
2.开始循环链表寻找目标元素,注意这里是通过cur.next.val来判断的。3.如果找到目标元素,就使用curnext = cur.next.next;来删除
4.注意最后返回的时候要用dummyHead.next,而不是dummyHead.代码实现过程:

class Solution {
    public ListNode removeElements(ListNode head, int val) {
        ListNode dummyNode = new ListNode(-1);
        dummyNode.next = head;
        ListNode cur = dummyNode;
        while(cur.next != null){
            if(cur.next.val == val){
                cur.next = cur.next.next;
            }else{
                cur = cur.next;
            }
        }
        return dummyNode.next;
    }
}

5.2 删除链表的倒数第 N 个结点

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

方法一:计算链表的长度

顾名思义,先计算链表的长度,然后再遍历一次,找到要删除的节点

public static ListNode removeNthFromEndByLength(ListNode head, int n) {
        ListNode dummy = new ListNode(0);
        dummy.next = head;
        int length = getLength(head);
        ListNode cur = dummy;
        for (int i = 1; i < length - n + 1; ++i) {
            cur = cur.next;
        }
        cur.next = cur.next.next;
        ListNode ans = dummy.next;
        return ans;
    }

    public static int getLength(ListNode head) {
        int length = 0;
        while (head != null) {
            ++length;
            head = head.next;
        }
        return length;
    }
方法二:双指针

思路跟前面的双指针专题里的解题思路差不多,主要是要找到那个节点的前一个节点

使用双指针的方法要注意,链表的长度小于等于n的情况

class Solution {
    public ListNode removeNthFromEnd(ListNode head, int n) {
        ListNode dummyNode = new ListNode(0);
        dummyNode.next = head;
        ListNode fast = head;
        ListNode slow = dummyNode;
        
        for (int i = 0; i < n; i++) {
            if (fast == null) { // 链表长度小于等于 n
                return dummyNode.next;
            }
            fast = fast.next;
        }
        
        while (fast != null) {
            fast = fast.next;
            slow = slow.next;
        }
        
        slow.next = slow.next.next; // 删除指定节点
        return dummyNode.next;

    }
}

5.3 删除重复元素

5.3.1 重复元素保留一个

重复元素保留一个,那就只需要判断当前元素的值与下一个元素的值是否相等即可

class Solution {
    public ListNode deleteDuplicates(ListNode head) {
        ListNode cur = head;
        while(cur != null && cur.next != null){
            if(cur.val == cur.next.val){
                cur.next = cur.next.next;
            }else{
                cur = cur.next;
            }
            
        }
        return head;
    }
}
5.3.2 重复元素都不要

leetcode 82. 删除排序链表中的重复元素 II

这题我的做法是双指针,然后使用一个变量记录是否为重复元素

class Solution {
    public ListNode deleteDuplicates(ListNode head) {
        if(head == null || head.next == null) return head;
        ListNode dummyNode = new ListNode(0);
        dummyNode.next = head;
        ListNode slow = dummyNode;
        ListNode fast = head;
        Boolean isDup = false;
        while(fast != null && fast.next != null){
            if(fast.val == fast.next.val){
                fast = fast.next;
                isDup = true;
            }else{
                if(isDup){
                    slow.next = fast.next;
                    fast = slow.next;
                }else{
                    slow = slow.next;
                    fast = fast.next;
                }
                isDup = false;
            }
        }
        if(isDup){
            slow.next = fast.next;
        }
        return dummyNode.next;
    }
}

结束啦!!!!

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值