算法通关村第一关——链表经典问题笔记

Listcode定义

class ListNode {
        public int val;
        public ListNode next;

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

1.五种方法解决两个链表的第一个公共子节点问题

原题:输入两个链表,找出它们的第一个公共节点。例如下面的两个链表:
在这里插入图片描述

两个链表的头结点都是已知的,相交之后成为一个单链表,但是相交的位置未知,并且相交之前的结点数也是未知的,请设计算法找到两个链表的合并点。

(1)通过哈希或集合实现
  • Hash

    public ListNode findFirstCommonNodeByHashMap(ListNode headA,ListNode headB){
            HashMap<ListNode,ListNode> map = new HashMap<>();
            while (headA != null){
                map.put(headA,headA);
                headA = headA.getNext();
            }
    
            while (headB != null){
                if (map.containsKey(headB)){
                    return headB;
                }
                headB = headB.getNext();
            }
            return null;
        }
    
  • Collection

    public ListNode findFirstCommonNodeBySet(ListNode headA,ListNode headB){
            Set<ListNode> set = new HashSet<>();
            while (headA != null){
                set.add(headA);
                headA = headA.getNext();
            }
            while (headB != null){
                if (set.contains(headB)){
                    return headB;
                }
                headB = headB.getNext();
            }
            return null;
        }
    
(2)通过栈实现
   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 pre = null;
        while (stackA.size() > 0 && stackB.size() > 0) {
            if (stackA.peek() == stackB.peek()) {
                pre = stackA.pop();
                stackB.pop();
            } else {
                break;
            }
        }
        return pre;
    }
(3)通过双指针法实现
   public ListNode findFirstCommonNodeByDoublePointer(ListNode headA,ListNode headB){
        if (headA == null || headB == null) {
            return null;
        }
        ListNode node1 = headA;
        ListNode node2 = headB;
        while (node1 != node2) {
            node1 = node1.next;
            node2 = node2.next;
            if (node1 != node2) {
                if (node1 == null) {
                    node1 = headB;
                }
                if (node2 == null) {
                    node2 = headA;
                }
            }
        }
        return node1;
    }
(4)通过差和双指针实现

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

public ListNode findFirstCommonNode(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;
        //长的先走sub步
       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:
输入: 1->2->2->1
输出: true
进阶:你能否用 O(n) 时间复杂度和 O(1) 空间复杂度解决此题?
(1)方法1

将链表元素都赋值到数组中,然后可以从数组两端向中间对比。这种方法会被视为逃避链表,面试不能这么干。

public boolean checkIsPalindromicByList(ListNode head) {
        if (head == null) {
            return true;
        }
        List<ListNode> list = new ArrayList<>();
        while (head != null) {
            list.add(head);
            head = head.next;
        }
        int i = 0;
        int length = list.size();
        while (i < length - 1) {
            if (list.get(i).val != list.get(length - 1 - i).val) {
                return false;
            }
            i++;
        }
        return true;
    }
(2)方法2

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

public boolean isPalindrome(ListNode head) {
    ListNode temp = head;
    Stack<Integer> stack = new Stack();
    //把链表节点的值存放到栈中
    while (temp != null) {
        stack.push(temp.val);
        temp = temp.next;
    }
    //之后一边出栈,一遍比较
    while (head != null) {
        if (head.val != stack.pop()) {
            return false;
        }
        head = head.next;
    }
    return true;
}
(3)方法3

先遍历第一遍,得到总长度。之后一边遍历链表,一边压栈。到达链表长度一半后就不再压栈,而是一边出栈,一边遍历,一边比较,只要有一个不相等,就不是回文链表。这样可以节省一半的空间。

public boolean isPalindrome(ListNode head) {
        if (head == null || head.next == null) {
            return true;
        }
        
        // 计算链表长度
        int length = 0;
        ListNode p = head;
        while (p != null) {
            length++;
            p = p.next;
        }
        
        // 反转链表
        ListNode prev = null;
        ListNode curr = head;
        for (int i = 0; i < length / 2; i++) {
            ListNode nextTemp = curr.next;
            curr.next = prev;
            prev = curr;
            curr = nextTemp;
        }
        
        // 判断是否为回文链表
        ListNode p1 = head;
        ListNode p2 = prev;
        while (p2 != null) {
            if (p1.val != p2.val) {
                return false;
            }
            p1 = p1.next;
            p2 = p2.next;
        }
        return true;
    }
(4)通过快慢指针算法实现
public boolean checkIsPalindromeBySlowFastPointer(ListNode head) {
        ListNode slow = head;
        ListNode fast = head;
        while (fast.next != null && fast.next.next != null) {
            slow = slow.next;
            fast = fast.next.next;
        }

        Stack<Integer> stack = new Stack<>();
        while (slow != null) {
            stack.push(slow.val);
            slow = slow.next;
        }

        while (!stack.isEmpty()){
            if(head.val != stack.pop()){
                return false;
            }
            head = head.next;
        }
        
        return true;
    }

3.合并有序链表

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

(1)暴力解法
public ListNode mergeTwoLists (ListNode list1, ListNode list2) {
     ListNode newHead=new ListNode(-1);
     ListNode res=newHead;
     while(list1!=null||list2!=null){ 
         //情况1:都不为空的情况
         if(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;
          //情况2:假如还有链表一个不为空
         }else if(list1!=null&&list2==null){
             newHead.next=list1;
             list1=list1.next;
             newHead=newHead.next;
         }else if(list1==null&&list2!=null){
             newHead.next=list2;
             list2=list2.next;
             newHead=newHead.next;
         }
     }
     return res.next;
 }

优化代码:第一个while只处理两个list 都不为空的情况,之后单独写while分别处理list1或者list2不为null的情况

public ListNode mergeTwoLists(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;
            //情况2:假如还有链表一个不为空
        }
        while (list1 != null && list2 == null) {
            newHead.next = list1;
            list1 = list1.next;
            newHead = newHead.next;
        }
        while (list1 == null && list2 != null) {
            newHead.next = list2;
            list2 = list2.next;
            newHead = newHead.next;
        }
        return res.next;
    }

继续优化代码

进一步分析,我们发现两个继续优化的点,一个是上面第一个大while里有三种情况,我们可以将其合并成两个,如果两个链表存在相同元素,第一次出现时使用if (l1.val <= l2.val)来处理,后面一次则会被else处理掉,什么意思呢?我们看一个序列。
假如list1为{1, 5, 8, 12},list2为{2, 5, 9, 13},此时都有一个node(5)。当两个链表都到5的位置时,出现了list1.val == list2.val,此时list1中的node(5)会被合并进来。然后list1继续向前走到了node(8),此时list2还是node(5),因此就会执行else中的代码块。这样就可以将第一个while的代码从三种变成两种,精简了很多。
第二个优化是后面两个小的while循环,这两个while最多只有一个会执行,而且由于链表只要将链表头接好,后面的自然就接上了,因此循环都不用写,也就是这样:

 public 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 {
                newHead.next = list2;
                list2 = list2.next;
            }
            newHead = newHead.next;
        }
        // 最多只有一个还未被合并完,直接接上去就行了,这是链表合并比数组合并方便的地方
        newHead.next = list1 == null ? list2 : list1;
        return res.next;
    }
(2)合并K个链表

第一种方法: 使用上面合并两个链表的方法,多次合并

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

第二种方法: 使用小根堆维护 k 个链表的头部,每次比较 K个链表的头结点求出最小值,然后poll出来,若该节点的 next 不为空,则继续加入到堆中

 public ListNode mergeLists(ListNode[] lists) {
        int length = lists.length;
        if (length == 0) {
            return null;
        }

        PriorityQueue<ListNode> pq = new PriorityQueue<>(Comparator.comparingInt(o -> o.val));
        for (ListNode list : lists) {
            if (list!=null){
                pq.add(list);
            }
        }
        ListNode res = new ListNode(0);
        ListNode cur = res;
        while (!pq.isEmpty()){
            ListNode tmp = pq.poll();
            cur.next = tmp;
            cur = cur.next;
            if (tmp.next!=null){
                pq.add(tmp.next);
            }
        }
        return res.next;
    }
(3)牛刀小试

LeetCode1669:给你两个链表 list1 和 list2 ,它们包含的元素分别为 n 个和 m 个。请你将 list1 中下标从a到b的节点删除,并将list2 接在被删除节点的位置。

public ListNode mergeInBetween(ListNode list1, int a, int b, ListNode list2) {
        ListNode pre1 = list1, post1 = list1, post2 = list2;
        int i = 0, j = 0;
        while(pre1 != null && post1 != null && j < b){
            if(i != a - 1){
                pre1 = pre1.next;
                i++;
            } 
            if(j != b){
                post1 = post1.next;
                j++;
            } 
        }
        post1 = post1.next;
        //寻找list2的尾节点
        while(post2.next != null){
            post2 = post2.next;
        }
        //链1尾接链2头,链2尾接链1后半部分的头
        pre1.next = list2;
        post2.next = post1;
        return list1;
    }

1.变化1:定义list1的[a,b]区间为list3,将list3和list2按照升序合并成一个链表。

    public LinkNode mergeInBetween(LinkNode list1, int a, int b, LinkNode list2) {
        LinkNode pre1 = list1, post2 = list2;
        LinkNode tmp = new LinkNode(-1, null);
        LinkNode list3;
        int i = 1;
        PriorityQueue<LinkNode> pq = new PriorityQueue<>(Comparator.comparingInt(LinkNode::getValue));
        while (pre1 != null) {
            if (i < a || i > b) {
                pq.add(pre1);
            }
            pre1 = pre1.getNext();
            i++;
        }
        while (post2 != null) {
            pq.add(post2);
            post2 = post2.getNext();
        }
        list3 = tmp;
        while (!pq.isEmpty()) {
            tmp.setNext(pq.poll());
            tmp = tmp.getNext();
        }
        return list3;
    }

2.变化2:list2也将区间[a,b]的元素删掉,然后将list1和list2合并成一个链表

    public LinkNode mergeInBetween(LinkNode list1, int a, int b, LinkNode list2) {
        LinkNode pre1 = list1, post2 = list2;
        LinkNode tmp = new LinkNode(-1, null);
        LinkNode list3;
        int i = 1;
        PriorityQueue<LinkNode> pq = new PriorityQueue<>(Comparator.comparingInt(LinkNode::getValue));
        while (pre1 != null) {
            if (i < a || i > b) {
                pq.add(pre1);
            }
            pre1 = pre1.getNext();
            i++;
        }
        i = 1;
        while (post2 != null) {
            if (i < a || i > b) {
                pq.add(post2);
            }
            post2 = post2.getNext();
            i++;
        }
        list3 = tmp;
        while (!pq.isEmpty()) {
            tmp.setNext(pq.poll());
            tmp = tmp.getNext();
        }
        return list3;
    }

4.寻找中间结点

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

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

这个问题用经典的快慢指针可以轻松搞定,用两个指针 slow 与 fast 一起遍历链表。slow 一次走一步,fast 一次走两步。那么当 fast 到达链表的末尾时,slow 必然位于中间。

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

5.寻找倒数第K个元素

输入一个链表,输出该链表中倒数第k个节点。本题从1开始计数,即链表的尾节点是倒数第1个节点。
示例
给定一个链表: 1->2->3->4->5, 和 k = 2.
返回链表 4->5.

这里可以使用快慢双指针,先将fast 向后遍历到第 k+1 个节点, slow仍然指向链表的第一个节点,此时指针fast 与slow 二者之间刚好间隔 k 个节点。之后两个指针同步向后走,当 fast 走到链表的尾部空节点时,slow 指针刚好指向链表的倒数第k个节点。
这里需要特别注意的是链表的长度可能小于k,寻找k位置的时候必须判断fast是否为null

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) {
            fast = fast.next;
            slow = slow.next;
        }
        return slow;
    }

6.旋转链表

7.删除链表特定节点

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

实现步骤

  • 1.创建一个虚拟链表头dummyHead,使其next指向head。
  • 2.开始循环链表寻找目标元素,注意这里是通过cur.next.val来判断的。
  • 3.如果找到目标元素,就使用cur.next = cur.next.next;来删除。
  • 4.注意最后返回的时候要用dummyHead.next,而不是dummyHead。
    public LinkNode deleteNodeByVal(LinkNode head, int val) {
        if (head == null) {
            return head;
        }
        LinkNode cur = new LinkNode(-1, head);
        LinkNode res = cur;
        while (cur != null && cur.getNext() != null) {
            if (cur.getNext().getValue() == val) {
                cur.setNext(head.getNext().getNext());
            } else {
                cur = cur.getNext();
            }
        }
        return res;
    }

8.删除链表倒数第K个结点

题目:删除链表的倒数第n个结点,并且返回链表的头结点。

示例1:
输入:head = [1,2,3,4,5], n = 2
输出:[1,2,3,5]
/**
 * 首先从头节点开始对链表进行一次遍历,得到链表的长度 L。随后再从头节点开始对链表进行一次遍历,当遍历到第L−n+1个节点时,则是删除的节点。
 */
public ListNode removeNthFromEnd(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 int getLength(ListNode head) {
        int length = 0;
        while (head != null) {
            ++length;
            head = head.next;
        }
        return length;
    }

进阶:尝试使用一趟扫描实现

双指针查找:

  public LinkNode removeNthFromEnd2(LinkNode head, int n) {
        LinkNode res = head;
        LinkNode slow = head;
        LinkNode fast = head;
        for (int i = 0; i < n; i++) {
            fast = fast.getNext();
        }
        while (fast != null) {
            slow = slow.getNext();
            fast = fast.getNext();
        }
        slow.setNext(slow.getNext().getNext());
        return res;
    }

9.删除链表中的重复元素

(1)重复元素保留一个

存在一个按升序排列的链表,给你这个链表的头节点 head ,请你删除所有重复的元素,使每个元素只出现一次 。返回同样按升序排列的结果链表。

示例1:
输入:head = [1,1,2,3,3]
输出:[1,2,3]
    public LinkNode deleteDuplicates(LinkNode headNode){
        if (headNode == null){
            return headNode;
        }

        LinkNode cur = headNode;
        while (cur.getNext() != null){
            if (cur.getValue() == cur.getNext().getValue()) {
                cur.setNext(cur.getNext().getNext());
            } else {
                cur = cur.getNext();
            }
        }
        return headNode;
    }
(2)重复元素都不要
示例1:
输入:head = [1,2,3,3,4,4,5]
输出:[1,2,5]
public ListNode deleteDuplicates(ListNode head) {
        if (head == null) {
            return head;
        }
        
        ListNode dummy = new ListNode(0, head);

        ListNode cur = dummy;
        while (cur.next != null && cur.next.next != null) {
            if (cur.next.val == cur.next.next.val) {
                int x = cur.next.val;
                while (cur.next != null && cur.next.val == x) {
                    cur.next = cur.next.next;
                }
            } else {
                cur = cur.next;
            }
        }
        return dummy.next;
    }
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值