算法学习笔记——链表经典问题

💡 链表的算法题比数组少很多,而在回溯贪心动规等高级算法中很少见到链表的影子。我们这里就集中研究一些出现频率特别高的算法题。

1.第一个公共子节点问题

这是一道经典的链表问题,剑指offer52 先看一下题目:输入两个链表,找出它们的第一个公共节点。例如下面的两个链表:

image.png

两个链表的头结点都是已知的,相交之后成为一个单链表,但是相交的位置未知,并且相交之前的结点数也是未知的,请设计算法找到两个链表的合并点。
没有思路时该怎么解题?
单链表中每个节点只能指向唯一的下一个next,但是可以有多个指针指向一个节点。例如上面c1就可以被a2,b3同时指向。该怎么入手呢?如果一时想不到该怎么办呢?
告诉你一个屡试不爽的方法:将常用数据结构和常用算法思想都想一遍,看看哪些能解决问题。
常用的数据结构有数组、链表、队、栈、Hash、集合、树、堆。常用的算法思想有查找、排序、双指针、递归、迭代、分治、贪心、回溯和动态规划等等。
首先想到的是蛮力法,类似于冒泡排序的方式,将第一个链表中的每一个结点依次与第二个链表的进行比较,当出现相等的结点指针时,即为相交结点。虽然简单,但是时间复杂度高,排除!
再看Hash,先将第一个链表元素全部存到Map里,然后一边遍历第二个链表,一边检测当前元素是否在Hash中,如果两个链表有交点,那就找到了。OK,第二种方法出来了。既然Hash可以,那集合呢?和Hash一样用,也能解决,OK,第三种方法出来了。
队列和栈呢?这里用队列没啥用,但用栈呢?现将两个链表分别压到两个栈里,之后一边同时出栈,一边比较出栈元素是否一致,如果一致则说明存在相交,然后继续找,最晚出栈的那组一致的节点就是要找的位置,于是就有了第四种方法。
这时候可以直接和面试官说,应该可以用HashMap做,另外集合和栈应该也能解决问题。面试官很明显就会问你,怎么解决?
然后你可以继续说HashMap、集合和栈具体应该怎么解决。
假如你想错了,比如你开始说队列能,但后面发现根本解决不了,这时候直接对面试官说“队列不行,我想想其他方法”就可以了,一般对方就不会再细究了。算法面试本身也是一个相互交流的过程,如果有些地方你不清楚,他甚至会提醒你一下,所以不用紧张。
除此上面的方法,还有两种比较巧妙的方法,我们一个个看:

1.1 哈希和集合

先将一个链表元素全部存到Map里,然后一边遍历第二个链表,一边检测Hash中是否存在当前结点,如果有交点,那么一定能检测出来。 对于本题,如果使用集合更适合,而且代码也更简洁。思路和上面的一样,直接看代码:

/**
  * 方法1:通过Hash辅助查找
  *
  * @param pHead1
  * @param pHead2
  * @return
  */
public static ListNode findFirstCommonNodeByMap(ListNode pHead1, ListNode pHead2) {
    if (pHead1 == null || pHead2 == null) {
        return null;
    }
    ListNode current1 = pHead1;
    ListNode current2 = pHead2;

    HashMap<ListNode, Integer> hashMap = new HashMap<ListNode, Integer>();
    while (current1 != null) {
        hashMap.put(current1, null);
        current1 = current1.next;
    }

    while (current2 != null) {
        if (hashMap.containsKey(current2))
            return current2;
        current2 = current2.next;
    }

    return null;
}

1.2 使用栈

这里需要使用两个栈,分别将两个链表的结点入两个栈,然后分别出栈,如果相等就继续出栈,一直找到最晚出栈的那一组。

/**
  * 方法2: 通过栈
  *
  * @param headA
  * @param headB
  * @return
  */
public static ListNode findFindFirstCommonNodeByStack(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 (stackB.size() > 0 && stackA.size() > 0) {
        if (stackA.peek() == stackB.peek()) {
            preNode = stackA.pop();
            stackB.pop();
        } else {
            break;
        }
    }
    return preNode;
}

1.3 拼接两个字符串

先看下面的链表A和B:
A: 0-1-2-3-4-5
B: a-b-4-5
如果分别拼接成AB和BA会怎么样呢?
AB:0-1-2-3-4-5-a-b-4-5
BA:a-b-4-5-0-1-2-3-4-5
我们发现拼接后从最后的4开始,两个链表是一样的了,自然4就是要找的节点,所以可以通过拼接的方式来寻找交点。这么做的道理是什么呢?我们可以从几何的角度来分析。我们假定A和B有相交的位置,以交点为中心,可以将两个链表分别分为left_a和right_a,left_b和right_b这样四个部分,并且right_a和right_b是一样的,这时候我们拼接AB和BA就是这样的结构:

image.png

我们说right_a和right_b是一样的,那这时候分别遍历AB和BA是不是从某个位置开始恰好就找到了相交的点了?
这里还可以进一步优化,如果建立新的链表太浪费空间了,我们只要在每个链表访问完了之后,调整到一下链表的表头继续遍历就行了,于是代码就出来了:

    /**
     * 方法3: 通过序列拼接
     */
    public static ListNode findFirstCommonNodeByCombine(ListNode pHead1,ListNode pHead2){
        if (pHead1 == null || pHead2 == null){
            return null;
        }
        ListNode p1 = pHead1;
        ListNode p2 = pHead2;
        while (p1 != p2){
            p1 = p1.next;
            p2 = p2.next;
            if (p1 != p2){
                if (p1 == null){
                    p1 = pHead2;
                }
                if (p2 == null){
                    p2 = pHead1;
                }
            }
        }
        return p1;
    }

1.4 差和双指针

我们再看另一个使用差和双指针来解决问题的方法。假如公共子节点一定存在第一轮遍历,假设La长度为L1,Lb长度为L2.则|L2-L1|就是两个的差值。第二轮遍历,长的先走|L2-L1|,然后两个链表同时向前走,结点一样的时候就是公共结点了。

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

234. 回文链表 - 力扣(LeetCode)

LeetCode234,这也是一道简单,但是很经典的链表题,判断一个链表是否为回文链表。

示例1:
输入: 1->2->2->1
输出: true
进阶:你能否用 O(n) 时间复杂度和 O(1) 空间复杂度解决此题?

看到这个题你有几种思路解决,我们仍然是先将常见的数据结构和算法思想想一遍,看看谁能解决问题。
方法1:将链表元素都赋值到数组中,然后可以从数组两端向中间对比。这种方法会被视为逃避链表,面试不能这么干。
方法2:将链表元素全部压栈,然后一边出栈,一边重新遍历链表,一边比较两者元素值,只要有一个不相等,那就不是。
方法3:优化方法2,先遍历第一遍,得到总长度。之后一边遍历链表,一边压栈。到达链表长度一半后就不再压栈,而是一边出栈,一边遍历,一边比较,只要有一个不相等,就不是回文链表。这样可以节省一半的空间。
方法4:优化方法3:既然要得到长度,那还是要遍历一次链表才可以,那是不是可以一边遍历一边全部压栈,然后第二遍比较的时候,只比较一半的元素呢?也就是只有一半的元素出栈, 链表也只遍历一半,当然可以。
方法5:反转链表法, 先创建一个链表newList,将原始链表oldList的元素值逆序保存到newList中,然后重新一边遍历两个链表,一遍比较元素的值,只要有一个位置的元素值不一样,就不是回文链表。
方法6:优化方法5,我们只反转一半的元素就行了。先遍历一遍,得到总长度。然后重新遍历,到达一半的位置后不再反转,就开始比较两个链表。
方法7:优化方法6,我们使用双指针思想里的快慢指针 ,fast一次走两步,slow一次走一步。当fast到达表尾的时候,slow正好到达一半的位置,那么接下来可以从头开始逆序一半的元素,或者从slow开始逆序一半的元素,都可以。
方法8:在遍历的时候使用递归来反转一半链表可以吗?当然可以,再组合一下我们还能想出更多的方法,解决问题的思路不止这些了,此时单纯增加解法数量没啥意义了。
上面这些解法中,各有缺点,实现难度也不一样,有的甚至算不上一个独立的方法,这么想只是为了开拓思路、举一反三。我们选择最佳的两种实现,其他方法请同学自行写一下试试。
这里看一下比较基本的全部压栈的解法。
将链表元素全部压栈,然后一边出栈,一边重新遍历链表,一边比较,只要有一个不相等,那就不是回文链表了,代码:

public class IsPalindromic {
    public static void main(String[] args) {
        int[] a = {1, 2, 3, 4, 5, 4, 3, 2, 1};
        ListNode listNode = initLinkedList(a);
        boolean result = isPalindrome(listNode);
        System.out.println("result: " + result);
    }

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

    private static ListNode initLinkedList(int[] array) {
        ListNode head = null, cur = null;
        for (int i = 0; i < array.length; i++) {
            ListNode newNode = new ListNode(array[i]);
            newNode.next = null;
            if (i == 0) {
                head = newNode;
                cur = head;
            } else {
                cur.next = newNode;
                cur = newNode;
            }
        }
        return head;
    }

    static class ListNode {
        int val;
        ListNode next;

        public ListNode(int val) {
            this.val = val;
            next = null;
        }
    }
}

3. 合并有序链表

数组中我们研究过合并的问题,链表同样可以造出两个或者多个链表合并的问题。两者有相似的地方,也有不同的地方,你能找到分别是什么吗?

3.1 合并两个有序链表

21. 合并两个有序链表 - 力扣(LeetCode)

LeetCode21 将两个升序链表合并为一个新的升序链表并返回,新链表是通过拼接给定的两个链表的所有节点组成的。
本题虽然不复杂,但是很多题目的基础,解决思路与数组一样,一般有两种。一种是新建一个链表,然后分别遍历两个链表,每次都选最小的结点接到新链表上,最后排完。另外一个就是将一个链表结点拆下来,逐个合并到另外一个对应位置上去。这个过程本身就是链表插入和删除操作的拓展,难度不算大,这时候代码是否优美就比较重要了。先看下面这种:

public static ListNode mergeTwoLists(ListNode list1, ListNode list2) {

    ListNode newHead = new ListNode(-1);
    ListNode res = newHead;
    while (list1 != null || list2 != null) {

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

        }
        //下面的两个while最多只有一个会执行
        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;
}

拓展 进一步优化代码
进一步分析,我们发现两个继续优化的点,一个是上面第一个大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 mergeTwoLists(ListNode list1, ListNode list2) {
        ListNode prehead = new ListNode(-1);
        ListNode prev = prehead;
        while (list1 != null && list2 != null) {
            if (list1.val <= list2.val) {
                prev.next = list1;
                list1 = list1.next;
            } else {
                prev.next = list2;
                list2 = list2.next;
            }
            prev = prev.next;
        }
        // 最多只有一个还未被合并完,直接接上去就行了,这是链表合并比数组合并方便的地方
        prev.next = list1 == null ? list2 : list1;
        return prehead.next;
    }

3.2 合并K个链表

合并k个链表,有多种方式,例如堆、归并等等。如果面试遇到,我倾向先将前两个合并,之后再将后面的逐步合并进来,这样的的好处是只要将两个合并的写清楚,合并K个就容易很多,现场写最稳妥:

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

3.3 一道很无聊的好题

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

1669题的意思就是将list1中的[a,b]区间的删掉,然后将list2接进去,如下图所示:

image.png

你觉得难吗?如果这也是算法的话,我至少可以造出七八道题,例如:

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

(2)list2也将区间[a,b]的元素删掉,然后将list1和list2合并成一个链表。

(3)定义list2的[a,b]区间为list4,将list2和list4合并成有序链表。

看到了吗?掌握基础是多么重要,我们自己都能造出题目来。这也是为什么算法会越刷越少,因为到后面会发现套路就这样,花样随便变,以不变应万变就是我们的宗旨。

具体到这个题,按部就班遍历找到链表1保留部分的尾节点和链表2的尾节点,将两链表连接起来就行了。

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

4. 双指针专题

在数组里我们介绍过双指针的思想, 可以简单有效的解决很多问题,而所谓的双指针只不过是两个变量而已。在链表中同样可以使用双指针来轻松解决一部分算法问题。这类题目的整体难度不大,但是在面试中出现的频率很高,我们集中看一下。

4.1 寻找中间结点

876. 链表的中间结点 - 力扣(LeetCode)

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

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

这个问题用经典的快慢指针可以轻松搞定,用两个指针 slow 与 fast 一起遍历链表。slow 一次走一步,fast 一次走两步。那么当 fast 到达链表的末尾时,slow 必然位于中间。
这里还有个问题,就是偶数的时候该返回哪个,例如上面示例2返回的是4, 而3貌似也可以,那该使用哪个呢?如果我们使用标准的快慢指针就是后面的4,而在很多数组问题中会是前面的3,想一想为什么会这样。

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

4.2 寻找倒数第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;
    }

4.3 旋转链表

61. 旋转链表 - 力扣(LeetCode)

Leetcode61.先看题目要求:给你一个链表的头节点 head ,旋转链表,将链表每个节点向右移动 k 个位置。

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

输出:[4,5,1,2,3]

img

这个题有多种解决思路,首先想到的是根据题目要求硬写,但是这样比较麻烦,也容易错。这个题是否在数组里见过类似情况?
观察链表调整前后的结构,我们可以发现从旋转位置开始,链表被分成了两条,例如上面的{1,2,3}和{4,5},这里我们可以参考上一题的倒数K的思路,找到这个位置,然后将两个链表调整一下重新接起来就行了。具体怎么调整呢?脑子里瞬间想到两种思路:
第一种是将整个链表反转变成{5,4,3,2,1},然后再将前K和N-K两个部分分别反转,也就是分别变成了{4,5}和{1,2,3},这样就轻松解决了。这个在后面学习了链表反转之后,请读者自行解决。
第二种思路就是先用双指针策略找到倒数K的位置,也就是{1,2,3}和{4,5}两个序列,之后再将两个链表拼接成{4,5,1,2,3}就行了。具体思路是:
因为k有可能大于链表长度,所以首先获取一下链表长度len,如果然后k=k % len,如果k == 0,则不用旋转,直接返回头结点。否则:

  1. 快指针先走k步。
  2. 慢指针和快指针一起走。
  3. 快指针走到链表尾部时,慢指针所在位置刚好是要断开的地方。把快指针指向的节点连到原链表头部,慢指针指向的节点断开和下一节点的联系。
  4. 返回结束时慢指针指向节点的下一节点。
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;
     //这里head先走一遍,统计出链表的元素个数,完成之后head就变成null了
      while(head != null){
          head = head.next;
          len++;
      }
      if(k % len == 0){
          return temp;
      }
      // 从这里开始fast从头结点开始向后走
      //这里使用取模,是为了防止k大于len的情况
      //例如,如果len=5,那么k=2和7,效果是一样的 
      while((k % len) > 0){
          k--;
          fast = fast.next;
      }
     // 快指针走了k步了,然后快慢指针一起向后执行
     // 当fast到尾结点的时候,slow刚好在倒数第K个位置上
      while(fast.next != null){
          fast = fast.next;
          slow = slow.next;
      }
      ListNode res = slow.next;
      slow.next = null;
      fast.next = temp;
      return res;
  }

5. 删除链表元素专题

如果按照LeetCode顺序一道道刷题,会感觉毫无章法而且很慢,但是将相似类型放在一起,瞬间就发现不过就是在改改条件不断造题。我们前面已经多次见证这个情况,现在集中看一下与链表删除相关的问题。如果在链表中删除元素搞清楚了,一下子就搞定8道题,是不是很爽?

  • LeetCode 237:删除某个链表中给定的(非末尾)节点。传入函数的唯一参数为要被删除的节点 。
  • LeetCode 203:给你一个链表的头节点 head 和一个整数 val ,请你删除链表中所有满足 Node.val == val 的节点,并返回新的头节点 。
  • LeetCode 19. 删除链表的倒数第 N 个节点。
  • LeetCode 1474. 删除链表 M 个节点之后的 N 个节点。
  • LeetCode 83 存在一个按升序排列的链表,请你删除所有重复的元素,使每个元素只出现一次。
  • LeetCode 82 存在一个按升序排列的链表,请你删除链表中所有存在数字重复情况的节点,只保留原始链表中没有重复出现的数字。

我们在链表基本操作部分介绍了删除的方法,至少需要考虑删除头部,删除尾部和中间位置三种情况的处理。而上面这些题目就是这个删除操作的进一步拓展。

5.1 删除特定结点

203. 移除链表元素 - 力扣(LeetCode)

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

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

我们前面说过,我们删除节点cur时,必须知道其前驱pre节点和后继next节点,然后让pre.next=next。这时候cur就脱离链表了,cur节点会在某个时刻被gc回收掉。

img

对于删除,我们注意到首元素的处理方式与后面的不一样。为此,我们可以先创建一个虚拟节点 dummyHead,使其指向head,也就是dummyHead.next=head,这样就不用单独处理首节点了。

完整的步骤是:

  • 1.我们创建一个虚拟链表头dummyHead,使其next指向head。
  • 2.开始循环链表寻找目标元素,注意这里是通过cur.next.val来判断的。
  • 3.如果找到目标元素,就使用cur.next = cur.next.next;来删除。
  • 4.注意最后返回的时候要用dummyHead.next,而不是dummyHead。

代码实现过程:

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

我们继续看下面这两个题,其实就是一个题:
LeetCode 19. 删除链表的倒数第 N 个节点
LeetCode 1474. 删除链表 M 个节点之后的 N 个节点。
既然要删除倒数第N个节点,那一定要先找到倒数第N个节点,前面已经介绍过,而这里不过是找到之后再将其删除。

5.2 删除倒数第n个结点

LeetCode19题要求:给你一个链表,删除链表的倒数第n个结点,并且返回链表的头结点。进阶:你能尝试使用一趟扫描实现吗?

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

img

我们前面说过,遇到一个题目可以先在脑子里快速过一下常用的数据结构和算法思想,看看哪些看上去能解决问题。为了开拓思维,我们看看能怎么做:
第一种方法:先遍历一遍链表,找到链表总长度L,然后重新遍历,位置L-N+1的元素就是我们要删的。
第二种方法:貌似栈可以,先将元素全部压栈,然后弹出第N个的时候就是我们要的是不?OK,又搞定一种方法。
第三种方法:我们前面提到可以使用双指针 来寻找倒数第K,那这里同样可以用来寻找要删除的问题。
上面三种方法,第一种比较常规,第二种方法需要开辟一个O(n)的空间,还要考虑栈与链表的操作等,不中看也不中用。第三种方法一次遍历就行,用双指针也有逼格。接下来我们详细看一下第一和三两种。

方法1:计算链表长度
首先从头节点开始对链表进行一次遍历,得到链表的长度 L。随后我们再从头节点开始对链表进行一次遍历,当遍历到第L−n+1 个节点时,它就是我们需要删除的节点。代码如下:

public ListNode removeNthFromEnd(ListNode head, int n) {
    ListNode dummy = new ListNode(0);
    dummy.next = head;
    int len = getLength(head);
    ListNode cur = dummy;
    for (int i = 1; i < len - n + 1; i++) {
        cur = cur.next;
    }
    cur.next = cur.next.next;
    ListNode res = dummy.next;
    return res;
}
private static int getLength(ListNode head) {
    int length = 0;
    while (head != null) {
        length++;
        head = head.next;
    }
    return length;
}

方法2: 双指针
我们定义first和second两个指针,first先走N步,然后second再开始走,当first走到队尾的时候,second就是我们要的节点。代码如下:

public ListNode removeNthFromEnd(ListNode head, int n) {
    ListNode dummy = new ListNode(0);
    dummy.next = head;
    ListNode first = head;
    ListNode second = dummy;
    for (int i = 0; i < n; ++i) {
        first = first.next;
    }
    while (first != null) {
        first = first.next;
        second = second.next;
    }
    second.next = second.next.next;
    ListNode res = dummy.next;
    return res;
}

5.3 删除重复元素

我们继续看关于结点删除的题:
LeetCode 83 存在一个按升序排列的链表,请你删除所有重复的元素,使每个元素只出现一次。
LeetCode 82 存在一个按升序排列的链表,请你删除链表中所有存在数字重复情况的节点,只保留原始链表中没有重复出现的数字。
两个题其实是一个,区别就是一个要将出现重复的保留一个,一个是只要重复都不要了,处理起来略有差别。LeetCode 1836是在82的基础上将链表改成无序的了,难度要增加不少,感兴趣的同学请自己研究一下。

5.3.1 重复元素保留一个

83. 删除排序链表中的重复元素 - 力扣(LeetCode)

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

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

img

由于给定的链表是排好序的,因此重复的元素在链表中出现的位置是连续的,因此我们只需要对链表进行一次遍历,就可以删除重复的元素。具体地,我们从指针 cur 指向链表的头节点,随后开始对链表进行遍历。如果当前 cur 与cur.next 对应的元素相同,那么我们就将cur.next 从链表中移除;否则说明链表中已经不存在其它与cur 对应的元素相同的节点,因此可以将 cur 指向 cur.next。当遍历完整个链表之后,我们返回链表的头节点即可。
另外要注意的是 当我们遍历到链表的最后一个节点时,cur.next 为空节点,此时要加以判断,上代码:

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.2 重复元素都不要

82. 删除排序链表中的重复元素 II - 力扣(LeetCode)

LeetCode82:这个题目的要求与83的区别仅仅是重复的元素都不要了。例如:

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

img

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

public static ListNode deleteDuplicates(ListNode head) {
    if (head == null) {
        return head;
    }
    ListNode dummy = new ListNode(0);
    dummy.next = 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
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
哈希表是一种高效的数据结构,可以用来存储和查找键值对。其,哈希函数将键映射到一个特定的桶,每个桶存储一组键值对。在哈希表,如果两个键被映射到同一个桶,就会发生碰撞。为了解决这个问题,可以使用链表法。 链表法是一种解决哈希表碰撞问题的方法。具体来说,对于哈希表的每个桶,可以使用一个链表来存储所有映射到该桶的键值对。如果发生碰撞,只需要将新的键值对添加到链表的末尾即可。 下面是一个使用链表法实现哈希表的示例代码: ```python class Node: def __init__(self, key, value): self.key = key self.value = value self.next = None class HashTable: def __init__(self, capacity): self.capacity = capacity self.buckets = [None] * capacity def hash_function(self, key): return hash(key) % self.capacity def put(self, key, value): index = self.hash_function(key) node = self.buckets[index] while node: if node.key == key: node.value = value return node = node.next new_node = Node(key, value) new_node.next = self.buckets[index] self.buckets[index] = new_node def get(self, key): index = self.hash_function(key) node = self.buckets[index] while node: if node.key == key: return node.value node = node.next return None def remove(self, key): index = self.hash_function(key) node = self.buckets[index] prev = None while node: if node.key == key: if prev: prev.next = node.next else: self.buckets[index] = node.next return prev = node node = node.next ``` 在这个示例,我们定义了一个Node类来表示哈希表的每个节点,每个节点包含一个键、一个值和一个指向下一个节点的指针。我们还定义了一个HashTable类来实现哈希表,其包含一个桶数组和一些基本的操作方法,如put、get和remove。 在put方法,我们首先使用哈希函数计算出键的索引,然后遍历桶链表,查找该键是否已经存在于哈希表。如果找到了该键,我们只需要更新其对应的值即可。否则,我们创建一个新的节点,并将其添加到链表的开头。 在get方法,我们同样使用哈希函数计算出键的索引,然后遍历桶链表,查找该键的值。如果找到了该键,我们返回其对应的值。否则,返回None。 在remove方法,我们首先使用哈希函数计算出键的索引,然后遍历桶链表,查找该键。如果找到了该键,我们将其从链表删除即可。 总的来说,链表法是一种简单且常用的哈希表解决碰撞问题的方法。

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值