编程导航算法通关村第一关 | 链表高频算法题

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

输入两个链表,找出它们的第一个公共节点。例如下面的两个链表:
在这里插入图片描述
两个链表的头结点都是已知的,相交之后成为一个单链表,但是相交的位置未知,并且相交之前的结点数也是未知的,请设计算法找到两个链表的合并点。

1.1 哈希和集合

先将一个链表元素全部存到Map里, 然后一边遍历第二个第二个链表, 一边检测Hash中是否存在当前节点, 如果有交点, 那么一定能够检测出来。对于本题, 使用集合更加适合, 代码也更加整洁。

 /**
   * 使用集合找出两个链表的第一个公共子节点
   * @param headA
   * @param headB
   * @return
   */
  public ListNode findFirstCommonNodeBySet(ListNode headA, ListNode headB) {
    Set<ListNode> set = new HashSet<>();
    // 先遍历链表A, 将所有节点保存到set集合中
    while (headA != null) {
      set.add(headA);
      headA = headA.next;
    }
    
    // 再遍历链表B, 判断set集合中是否包含链表B的节点
    while (headB != null) {
      if (set.contains(headB)) {
        return headB;
      }
      headB = headB.next;
    }
    
    return null;
  }

1.2 使用栈

分别将两个链表压入两个栈中, 然后同时出栈, 如果相同就继续出栈, 一直找到最晚出栈的那一组。

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

代码实现:

/**
   * 使用栈找出两个链表的第一个公共子节点
   * @param headA
   * @param headB
   * @return
   */
  public ListNode findFirstCommonNodeByStack(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.size() > 0 && stackB.size() > 0) {
      if (stackA.peek() == stackB.peek()) {
        preNode = stackA.pop();
        stackB.pop();
      }else {
        break;
      }
    }
    return preNode;
  }

1.3 拼接两个字符串

将链表A和B拼接形成两个新链表, 新链表形式为AB和BA, 通过这种方式消除了两个链表非公共部分长度差, 分别遍历两个链表, 出现的第一个相同节点即为第一个公共子节点。

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

代码实现:

  /**
   * 思路: 将两个链表拼接成AB和BA形式
   * 通过拼接消除非公共部分长度差 (因为两个链表有公共子节点, 拼接后, 新形成的两个链表长度一致, 所以链表的公共部分就都到了后面)
   * @param headA
   * @param headB
   * @return
   */
  public ListNode findFirstCommonNode(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;
  }

1.4 差和双指针

思路和拼接字符串方法类似, 只不过该方法不是消除非公共部分长度差, 而是先让长的那个链表先遍历两个链表的长度差, 再同时遍历, 第一个相同的节点即为第一个公共子节点。

/**
     * 方法5:通过差值来实现
     *
     * @param pHead1
     * @param pHead2
     * @return
     */
    public static ListNode findFirstCommonNodeBySub(ListNode pHead1, ListNode pHead2) {
        if (pHead1 == null || pHead2 == null) {
            return null;
        }
        // 计算两个链表的长度
        ListNode current1 = pHead1;
        ListNode current2 = pHead2;
        int l1 = 0, l2 = 0;
        while (current1 != null) {
            current1 = current1.next;
            l1++;
        }

        while (current2 != null) {
            current2 = current2.next;
            l2++;
        }
        current1 = pHead1;
        current2 = pHead2;

        // 两个链表的长度差
        int sub = l1 > l2 ? l1 - l2 : l2 - l1;

        // 长度较长的先遍历长度差个节点
        if (l1 > l2) {
            int a = 0;
            while (a < sub) {
                current1 = current1.next;
                a++;
            }
        }

        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. 判断两个链表是否为回文链表

示例:
输入: 1->2->2->1
输出: true

2.1 使用栈实现

将链表元素全部压栈, 然后一边出栈, 一边重新遍历链表, 比较两者元素值, 只要有一个不相等, 就不是回文链表。

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

代码实现:

 /**
     * 全部压栈
     *
     * @param head
     * @return
     */
    public static boolean isPalindromeByAllStack(ListNode head) {
        ListNode temp=  head;
        Stack<Integer> stack = new Stack<>();
        while (temp != null) {
            stack.push(temp.val);
            temp = temp.next;
        }
        
        while (stack.size() > 0) {
            if (head.val != stack.pop()) {
                return false;
            }
            temp = head.next;
        }
        
        return true;
    }

优化:
先得到链表的总长度, 之后一边遍历链表, 一边压栈。到达链表长度一半后就不再压栈, 而是一边出栈, 一边继续遍历链表, 同时比较两者元素值, 只要有一个不相等, 就不是回文链表, 这样可以节省一半的空间。

在这里插入图片描述

/**
     * 只将一半的数据压栈
     *
     * @param head
     * @return
     */
    public static boolean isPalindromeByHalfStack(ListNode head) {
        if (head == null) {
            return true;
        }
        ListNode temp = head;
        Stack<Integer> stack = new Stack<>();
        // 链表长度
        int length = 0;
        while (temp != null) {
            // 将链表节点的值压入栈中
            stack.push(temp.val);
            temp = temp.next;
            length++;
        }
        // 链表长度除以2
        length >>= 1;
        while (length-- >= 0) {
            if (head.val != stack.pop()) {
                return false;
            }
            head = head.next;
        }
        return true;
    }

2.2 反转链表法

将原链表逆序保存到一个新链表中, 然后重新一边遍历两个链表,一遍比较元素的值,只要有一个位置的元素值不一样,就不是回文链表。

/**
     * 反转链表法
     *
     * @param head
     * @return
     */
    public static boolean isPalindromeByReverse(ListNode head) {
        // 将链表反转
        ListNode prev = null;
        ListNode curr = head;
        while (curr != null) {
            ListNode temp = curr.next;
            curr.next = prev;
            prev = curr;
            curr = temp;
        }
        // 遍历两个链表并比较元素值
        while (prev != null && head != null) {
            if (prev.val != head.val) {
                return false;
            }
            prev = prev.next;
            head = head.next;
        }
        return true;
    }

优化:
使用快慢指针, 快指针一次走两步, 慢指针一次走一步。当快指针到达表尾的时候, 慢指针正好到达一半的地方, 那么接下来可以从头开始逆序一半的元素,或者从slow开始逆序一半的元素,都可以。

/**
     * 通过双指针的方式来判断
     *
     * @param head
     * @return
     */
    public static boolean isPalindromeByTwoPoints(ListNode head) {
        if (head == null || head.next == null) {
            return true;
        }
        // 定义快慢指针
        ListNode fast = head, slow = head;
        ListNode curr = head, prev = null;
        while (fast != null && fast.next != null) {
            curr = slow;
            slow = slow.next;
            fast = fast.next.next;
            curr.next = prev;
            prev = curr;
        }
        // fast指针走到倒数第二个节点的情况
        if (fast != null) {
            slow = slow.next;
        }
        while (curr != null && slow != null) {
            if (curr.val != slow.val) {
                return false;
            }
            curr = curr.next;
            slow = slow.next;
        }
        return true;
    }

3. 合并有序链表

3.1 合并两个有序链表

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

解决思路:

新建一个链表, 分别遍历两个链表, 每次将最小的那个节点接到新链表上。

/**
     *
     * @param list1
     * @param list2
     * @return
     */
    public static ListNode mergeTwoLists(ListNode list1, ListNode list2) {
        ListNode current = new ListNode(-1);
        // 虚拟头节点
        ListNode newHead = current;
        while (list1 != null && list2 != null) {
            if (list1.val < list2.val) {
                current.next = list1;
                // 链表1接着遍历, 链表2暂不遍历, 等待链表1下一个节点继续比较元素值
                list1 = list1.next;
            } else {
                current.next = list2;
                list2 = list2.next;
            }
            current = current.next;
        }
        //下面的两个while最多只有一个会执行
        while (list2 != null) {
            current.next = list2;
            list2 = list2.next;
            current = current.next;
        }
        while (list1 != null) {
            current.next = list1;
            list1 = list1.next;
            current = current.next;
        }
        return newHead.next;
    }

优化代码:

最后的两个while循环最多只会执行一个, 所以只需要判断哪一个链表不为空, 并将那一个链表后面的部分全部接入新链表表尾即可。

 /**
     * 优化后的实现方法
     *
     * @param list1
     * @param list2
     * @return
     */
    public static ListNode mergeTwoListsMoreSimple(ListNode list1, ListNode list2) {
        ListNode current = new ListNode(-1);
        // 虚拟头节点
        ListNode newHead = current;
        while (list1 != null && list2 != null) {
            if (list1.val < list2.val) {
                current.next = list1;
                // 链表1接着遍历, 链表2暂不遍历, 等待链表1下一个节点继续比较元素值
                list1 = list1.next;
            } else {
                current.next = list2;
                list2 = list2.next;
            }
            current = current.next;
        }
        // 最多只有一个还未被合并完,直接接上去就行了,这是链表合并比数组合并方便的地方
        current.next = list1 == null ? list2 : list1;
        return newHead.next;
    }

3.2 合并K个链表

在合并两个链表的基础上, 先将前两个链表合并, 之后再将后面的链表逐步合并起来即可。

/**
     * 合并K个链表
     *
     * @param lists
     * @return
     */
    public static ListNode mergeKLists(ListNode[] lists) {
        ListNode res = null;
        for (ListNode list : lists) {
            res = mergeTwoListsMoreSimple(res, list);
        }
        return res;
    }

4 双指针

4.1 寻找中间节点

给定一个头结点为 head 的非空单链表,返回链表的中间结点。如果有两个中间结点,则返回第二个中间结点。

示例1
输入:[1,2,3,4,5]
输出:此列表中的结点 3
示例2:
输入:[1,2,3,4,5,6]
输出:此列表中的结点 4

使用快慢指针, 慢指针一次走一步, 快指针一次走两步。当快指针到达表尾时, 慢指针必然位于中间位置。

/**
     * 寻找中间节点
     * @param head
     * @return
     */
    public static ListNode middleNode(ListNode head) {
        ListNode fast = head;
        ListNode slow = head;
        while (fast != null && fast.next != null) {
            fast = fast.next.next;
            slow = slow.next;
        }
        return slow;
    }

4.2 寻找倒数第K个元素

输入一个链表,输出该链表中倒数第k个节点。本题从1开始计数,即链表的尾节点是倒数第1个节点。

示例
给定一个链表: 1->2->3->4->5, 和 k = 2.
返回链表 4->5.

使用快慢指针, 先将快指针向后遍历到 k + 1个, 慢指针仍然指向链表的第一个节点, 此时两个指针之间刚好间隔 k 个节点。之后两个指针同时向后走, 当快指针到达链表的尾部空节点时, 慢指针刚好指向链表的倒数第 k 个节点。

在这里插入图片描述

代码实现:

/**
     * 找链表倒数第K个结点
     * @param head
     * @param k
     * @return
     */
    public static ListNode getKthFromEnd(ListNode head, int k) {
       ListNode fast = head, slow = head;
        while (fast != null && k-- > 0 ) {
            fast = fast.next;
        }
        while (fast != null) {
            fast = fast.next;
            slow = slow.next;
        }
        return slow;
    }

4.3 旋转链表

给你一个链表的头节点 head ,旋转链表,将链表每个节点向右移动 k 个位置。

示例1:
输入:head = [1,2,3, 4,5], k = 2
输出:[4,5,1,2,3]

思路:

先使用双指针策略找到倒数 k 的位置, 将 倒数 k 位置前的部分断开并接入到表尾。

需要注意的是:

k可能大于链表长度, 所以首先应获取到链表长度len, 如果 k % len == 0, 则不用旋转, 直接返回头节点。
在这里插入图片描述

实现步骤:

  1. 快指针先走 k 步
  2. 慢指针和快指针一起走
  3. 快指针到达链表尾部时, 慢指针所在位置刚好是要断开的位置。将快指针指向的节点连到原链表头部, 慢指针指向的节点断开和下一个节点的联系 (指向 null 即可)
  4. 返回结束时慢指针指向节点的下一个节点

代码实现:

/**
     * 旋转链表
     * @param head
     * @param k
     * @return
     */
    public static ListNode rotateRight(ListNode head, int k) {
        if (head == null || k == 0) {
            return head;
        }
        // 获取链表长度
        int len = 0;
        ListNode temp = head;
        while (head != null) {
            len++;
            head = head.next;
        }
        head = temp;
        if (k % len == 0) {
            return head;
        }
        ListNode fast = head, slow = head;
        // 1. 快指针先走 k 步
        while ( (k % len) > 0) {
            k--;
            fast = fast.next;
        }
        // 2. 慢指针和快指针一起走
        while (fast.next != null) {
            fast = fast.next;
            slow = slow.next;
        }
        // 3. 快指针到达链表尾部时, 慢指针所在位置刚好是要断开的位置。将快指针指向的节点连到原链表头部, 慢指针指向的节点断开和下一个节点的联系 (指向 null 即可)
        fast.next = head;
        temp = slow.next;
        slow.next = null;
        // 4. 返回结束时慢指针指向节点的下一个节点
        return temp;
    }

5. 删除链表元素

5. 1 删除特定节点

给你一个链表的头节点 head 和一个整数 val ,请你删除链表中所有满足 Node.val == val 的节点,并返回新的头节点 。

示例1:
输入:head = [1,2,6,3,4,5,6], val = 6
输出:[1,2,3,4,5]

遍历链表, 找出满足要求的节点, 将该节点的前驱节点指向该节点的下一个节点, 该节点会在某个时刻被gc回收掉。

实现步骤:

  1. 创建一个虚拟链表头节点dummyHead, 使其next指向head
  2. 开始循环链表寻找目标元素
  3. 找到目标元素, 就使用cur.next = cur.next.next来删除
  4. 最后返回dummyHead.next

代码实现:

/**
     * 删除特定值的结点
     *
     * @param head
     * @param val
     * @return
     */
    public static ListNode removeElements(ListNode head, int val) {
        ListNode dummyHead = new ListNode(0);
        dummyHead.next = head;
        ListNode temp = dummyHead;
        while (temp.next != null) {
            if (temp.next.val == val) {
                temp.next = temp.next.next;
            } else {
                temp = temp.next;
            }
        }
        return dummyHead.next;
    }

5.2 删除倒数第 n 个节点

给你一个链表,删除链表的倒数第n个结点,并且返回链表的头结点。

示例1:
输入:head = [1,2,3,4,5], n = 2
输出:[1,2,3,5]

方法1: 计算链表长度

首先遍历一次链表, 得到链表的长度 L, 然后重新遍历链表, 当遍历到 L - n + 1 个节点时, 就是我们要删除的节点。

代码实现:

 /**
     * 方法1:利用链表长度
     *
     * @param head
     * @param n
     * @return
     */
    public static ListNode removeNthFromEndByLength(ListNode head, int n) {
        ListNode dummyHead = new ListNode(0);
        dummyHead.next = head;
        int length = getLength(head);
        ListNode current = head;
        int index = 1;
        while (index++ != length - n) {
            current = current.next;
        }
        current.next = current.next.next;
        return dummyHead.next;
    }

方法2: 双指针

快指针先走 n 步, 然后两个指针同时向前走, 当快指针走到队尾的时候, 慢指针所指向的就是我们要找的节点。

代码实现:

/**
     * 方法2:通过双指针
     *
     * @param head
     * @param n
     * @return
     */
    public static ListNode removeNthFromEndByTwoPoints(ListNode head, int n) {
        ListNode dummy = new ListNode(0);
        dummy.next = head;
        ListNode fast = head, slow = dummy;
        while (fast != null && n-- > 0) {
            fast = fast.next;
        }
        while (fast != null) {
            fast = fast.next;
            slow = slow.next;
        }
        slow.next = slow.next.next;
        return dummy.next;
    }

5.3 删除重复节点

5.3.1 重复元素保留一个

存在一个按升序排列的链表,请你删除所有重复的元素,使每个元素只出现一次。

示例1:
输入:head = [1,1,2,3,3]
输出:[1,2,3]

思路:

由于给定的链表是排好序的, 因此重复的元素在链表中的位置是连续的, 因此我们只需要对链表进行一次遍历就可以删除重复的元素。具体
地, 从头节点开始遍历, 如果当前节点 cur 与 cur.next 的元素相同, 我们就将cur.next从链表中删除; 否则说明链表中已经不存在其他与cur对应元素相同的节点, 因此可以将cur指向cur.next。

代码实现:

 /**
     * 重复元素保留一个
     *
     * @param head
     * @return
     */
    public static ListNode deleteDuplicate(ListNode head) {
        if (head == null) {
            return head;
        }
        ListNode cur = head;
        while (cur.next != null) {
            if (cur.val == cur.next.val) {
                cur.next = cur.next.next;
            } else {
                cur = cur.next;
            }
        }
        return head;
    }

5.3.3 重复元素都不要

存在一个按升序排列的链表,请你删除链表中所有存在数字重复情况的节点,只保留原始链表中没有重复出现的数字。

示例1:
输入:head = [1,2,3,3,4,4,5]
输出:[1,2,5]

思路:

当一个都不要时,链表只要直接对cur.next 以及 cur.next.next 两个node进行比较就行了,这里要注意两个node可能为空,稍加判断就行了。

代码实现:

 /**
     * 重复元素都不要
     *
     * @param head
     * @return
     */
    public static ListNode deleteDuplicates(ListNode head) {
        if (head == null) {
            return head;
        }
        ListNode dummyHead = new ListNode(0);
        dummyHead.next = head;
        ListNode current = dummyHead;
        // 从头节点开始比较
        while (current.next != null & current.next.next !=null) {
            if (current.next.val == current.next.next.val) {
                int x = current.next.val;
                while (current.next != null && current.next.val == x) {
                    current.next = current.next.next;
                }
            }else {
                current = current.next;
            }
        }
        return dummyHead.next;
    }
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值