剑指offer(一)链表专题

本文详细介绍了链表相关算法,包括从尾到头打印链表、反转链表、合并两个排序链表、找链表的第一个公共结点、删除链表节点、复杂链表的复制以及删除链表中重复节点。通过多种解法如栈、双指针、递归等,展示了链表操作的常见思路和技巧。
摘要由CSDN通过智能技术生成

于2021.12.01开始记录。剑指offer在牛客网或者力扣上面都可以刷的,我选择了牛客网,下面的题目顺序也是按照牛客网上面的顺序来的。


JZ6 从尾到头打印链表

题目描述:输入一个链表的头节点,按链表从尾到头的顺序返回每个节点的值(用数组返回)。

1.1 解法1:利用栈

思路:利用栈先进后出的特点,首先遍历链表,将链表中的结点压入堆栈;然后将结点弹出,放入到结果list中。

import java.util.ArrayList;
import java.util.Stack;
public class Solution {
    public ArrayList<Integer> printListFromTailToHead(ListNode listNode) {
        Stack<ListNode> stack = new Stack<>();
        ListNode cur = listNode;
        while(cur != null){
            stack.push(cur);
            cur = cur.next;
        }
        ArrayList<Integer> res = new ArrayList<>();
        while(!stack.empty()){
            res.add(stack.pop().val);
        }
        return res;
    }
}

1.2 解法2:反转链表后输出

思路:首先利用反转链表的思想,将原链表进行反转,然后在遍历反转以后的链表,进行输出。

import java.util.ArrayList;
import java.util.Stack;
public class Solution {
    public ArrayList<Integer> printListFromTailToHead(ListNode listNode) {
        ListNode cur = listNode;
        ListNode pre = null;
        while(cur != null){
            ListNode tmp = cur.next;
            cur.next = pre;
            pre = cur;
            cur = tmp;
        }
        ArrayList<Integer> res = new ArrayList<>();
        while(pre != null){
            res.add(pre.val);
            pre = pre.next;
        }
        return res;
    }
}

JZ24 反转链表

题目描述:给定一个单链表的头结点pHead,长度为n,反转该链表后,返回新链表的表头。

1.1 解法1:双指针迭代法

思路:

首先申请两个指针,第一个指针为pre,最初是指向null的,第二个指针指向head,然后让cur去遍历原链表。
每遍历一次,先用辅助变量tmp将cur的下一个节点保存下来,然后cur的next指向pre,再将pre和cur前进一步。当cur为null时,说明遍历完了,此时pre为原链表中的最后一个结点。

class Solution {
    public ListNode reverseList(ListNode head) {
        ListNode pre = null;//创建一个结点pre指向null
        ListNode cur = head;
        while(cur != null){
            ListNode tmp = cur.next;//创建辅助结点,用于保存当前结点的下一个结点
            cur.next = pre;//当前结点指向cur
            //pre和cur结点都前进一步
            pre = cur;
            cur = tmp;
        }
        return pre;
    }
}

1.2 解法2:递归法

递归的方法不太好理解,借鉴了力扣大佬的讲解:递归法力扣讲解,这个链接中幻灯片部分解释的很好,可以看看。

递归的理解难点:返回值cur为什么保存不变?
        将cur理解为返回的是已经反转过的部分的头指针,而这个头指针是确定的。每次递归,cur并没有改变,改变的是head。

递归的作用就是为了找到链表的最后一个结点,然后head一直改变,从而改变了指针的方向。

下面我放了一张图片,演示了程序的运行流程。
递归流程图

public class Solution {
    public ListNode ReverseList(ListNode head) {
        if(head == null || head.next == null){
            return head;
        }
        ListNode cur = ReverseList(head.next);
        head.next.next = head;
        head.next = null;
        return cur;
    }
}

不得不说,递归实现的代码还是很简单的,就是一开始不理解递归,后面还是要多练习的。


JZ25 合并两个排序的链表

题目描述:输入两个递增的链表,单个链表的长度为n,合并这两个链表并使新链表中的节点仍然是递增排序的。

1.1 解法1:迭代法

思路:

①首先定义一个虚拟头结点,用于表示合并后的新链表。然后定义一个cur,去指向这个合并的新链表。
②当l1和l2都不为空时,比较哪一个链表的结点值更小,将较小值结点添加到结果链表中,然后对应链表中的结点后移一位,同时结果链表指针也要后移一位。
③最后肯定是有一个链表先遍历完成,此时另一个链表还没有挂在结果链表上,因此需要进行判断。当循环终止时,假如l1先为null,此时l2中剩余的部分元素还未添加到结果链表中,因此需要进行添加;假如l2先为null,此时l1中剩余的部分元素还未添加到结果链表中,因此需要进行添加。

此种方法为了记忆,我简称为三指针拉链法:其中链表1对应一个指针l1,链表2对应一个指针l2,结果链表对应一个指针cur,整个过程类似于拉拉链,将两个链表都要穿起来。

总结:整个方法在LeetCode88合并两个有序数组以及归并排序中都有体现,相同的思路。

public class Solution {
    public ListNode Merge(ListNode list1,ListNode list2) {
        ListNode dummyHead = new ListNode(-1);
        ListNode cur = dummyHead;
        while(list1 != null && list2 != null){
            if(list1.val <= list2.val){
                cur.next = list1;
                list1 = list1.next;
            }
            else{
                cur.next = list2;
                list2 = list2.next;
            }
            cur = cur.next;
        }
        cur.next = (list1 == null ? list2 : list1);
        return dummyHead.next;
    }
}

1.2 解法2:递归法

参考链接:递归法实现合并,看下面的幻灯片讲解部分。

思路:
①递归的终止条件:当链表l1为空或者l2为空时,结束
②判断l1和l2哪个更小,然后让较小结点的next指向其余结点的合并结果(调用递归)。

class Solution {
    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;
        }
    }
}

JZ52 两个链表的第一个公共结点

题目描述:输入两个无环的单向链表,找出它们的第一个公共结点,如果没有公共节点则返回空。

1.1 解法1:哈希表

本题要找两个链表的公共结点,其实就是在链表2中找链表1中曾经出现过的结点。由于哈希表结构常常用来判断一个元素是否出现过,因此本题考虑使用哈希表解决。

思路:
①首先遍历链表pHead1,将链表中的每个结点添加到哈希表中。这里只需要存储链表结点即可,因此使用HashSet即可。这里要注意存储的是对象,而不是数值,虽然有时里面的数值相同但是对象是不一样的,比如4-1-8-4-5的情况,当存储时第一个4和第二个4都会存储进去,因为它们的地址不相同,不是同一个对象,只是里面的数值是相同的。
②然后遍历链表pHead2,对于遍历到的每一个结点,都判断该结点是否出现在哈希表中:
          如果当前结点不在哈希表中,则继续遍历下一个结点;
​          如果当前结点在哈希表中,那么后面的所有节点都在哈希表中,因此这个结点就是两个链表相交的结点,返回即可。

import java.util.HashSet;
public class Solution {
    public ListNode FindFirstCommonNode(ListNode pHead1, ListNode pHead2) {
        HashSet<ListNode> nodes = new HashSet<>();
        while(pHead1 != null){
            nodes.add(pHead1);
            pHead1 = pHead1.next;
        }
        while(pHead2 != null){
            if(nodes.contains(pHead2)){
                return pHead2;
            }
            else{
                pHead2 = pHead2.next;
            }
        }
        return null;
    }
}

1.2 解法2:转换为环形链表找入口问题思路:

思路:

将其中一个链表首尾相接,构成环,问题转换为判断另一个链表是否有环路,如果有环路找到环路入口的问题。
第一个链表首尾相接时,要记下相接的位置,最后记着拆掉。

public class Solution {
    public ListNode FindFirstCommonNode(ListNode pHead1, ListNode pHead2) {
        if(pHead1 == null || pHead2 == null){
            return null;
        }
        ListNode last = pHead2;
        while(last.next != null){
            last = last.next;
        }
        last.next = pHead2;
        ListNode slow = pHead1;
        ListNode fast = pHead1;
        while(fast != null && fast.next != null){
            slow = slow.next;
            fast = fast.next.next;
            if(slow == fast){
                ListNode slow1 = slow;
                ListNode fast1 = pHead1;
                while(slow1 != fast1){
                    slow1 = slow1.next;
                    fast1 = fast1.next;
                }
                last.next = null;
                return slow1;
            }
        }
        last.next = null;
        return null;
    }
}

注意:此种解法首先要进行特殊情况判断,否则会出现空指针异常。针对上面程序,假如链表2为空,从而last.next = pHead2;就报错,因为last本来就是空的,根本就没有next。

1.3 解法3:双指针,互走对方的路

在力扣下面看到有意思的评论:走到尽头见不到你,于是走过你来时的路,等到相遇时才发现,你也走过我来时的路。

思路:

首先进行特殊情况判断:当链表pHead1或pHead2为空时,一定不相交,直接返回null

当pHead1和pHead2都不为空时,让pA指向pHead1,pB指向pHead2。如果pA不为空,那么pA移到下一个结点;如果为空,让pA指向headB的头结点。如果pB不为空,那么pB移到下一个结点;如果为空,让pB指向headA的头结点。当pA和pB指向同一个结点时,此时找到交点。

为什么这种方法可行呢?下面图片针对链表相交以及不相交的情况都给出了证明。
在这里插入图片描述

public class Solution {
    public ListNode FindFirstCommonNode(ListNode pHead1, ListNode pHead2) {
         if(pHead1 == null || pHead2 == null){
             return null;
         }
        ListNode pA = pHead1;
        ListNode pB = pHead2;
        while(pA != pB){
            pA = (pA == null ? pHead2 : pA.next);
            pB = (pB == null ? pHead1 : pB.next);
        }
        return pA;
    }
}

JZ23 链表中环的入口结点

题目描述:给一个长度为n链表,若其中包含环,请找出该链表的环的入口结点,否则,返回null

1.1 解法1:哈希表

思路:遍历链表中的所有节点,如果当前节点已经存在于哈希表中,则说明该链表是环形链表;否则就将该节点放入哈希表中,继续遍历下一个节点,重复此过程。

import java.util.HashSet;
public class Solution {

    public ListNode EntryNodeOfLoop(ListNode pHead) {
        HashSet<ListNode> nodes = new HashSet<>();
        ListNode cur = pHead;
        while(cur != null){
            if(nodes.contains(cur)){
                return cur;
            }
            else{
                nodes.add(cur);
                cur = cur.next;
            }
        }
        return null;
    }
}

1.2 解法2:快慢指针

①判断链表是否有环:链表找环路问题的通用解法就是用快慢指针,即Floyed判圈法(龟兔赛跑算法)。给定快慢指针fast和slow,起始位置都在链表的开头。每次快指针fast前进2步,慢指针slow前进1步。如果fast指针可以走到头,那说明链表无环;如果存在环路,那么两个指针一定会相遇。
②如何寻找环的入口:当slow和fast第一次相遇时,将fast重新移动到链表开头,然后让slow和fast每次都前进一步,当slow和fast第二次相遇时,相遇的节点即为环路的开始节点。

public class Solution {

    public ListNode EntryNodeOfLoop(ListNode pHead) {
        if(pHead == null){
            return null;
        }
        ListNode slow = pHead;
        ListNode fast = pHead;
        while(fast != null && fast.next != null){
            slow = slow.next;
            fast = fast.next.next;
            if(slow == fast){
                ListNode slow1 = slow;
                ListNode fast1 = pHead;
                while(slow1 != fast1){
                    slow1 = slow1.next;
                    fast1 = fast1.next;
                }
                return slow1;
            }
        }
        return null;
    }
}

JZ22 链表中倒数最后k个结点

题目描述:输入一个长度为 n 的链表,设链表中的元素的值为 ai ,返回该链表中倒数第k个节点。将倒数问题转换为正数问题。

1.1 解法1:两次遍历

思路:第一轮遍历,确定链表的长度;第二轮遍历,找到这个倒数第k个结点,然后进行输出。
注意:题目中说明如果该链表长度小于k,请返回一个长度为 0 的链表,因此这个特殊情况,要单独判断下

这行代码不能缺少:if(length < k) return null; //特殊情况判断

public class Solution {
    /**
     * 代码中的类名、方法名、参数名已经指定,请勿修改,直接返回方法规定的值即可
     *
     * 
     * @param pHead ListNode类 
     * @param k int整型 
     * @return ListNode类
     */
    public ListNode FindKthToTail (ListNode pHead, int k) {
        // write code here
        int length = getLength(pHead);
        if(length < k) return null;
        ListNode cur = pHead;
        for(int i = 0;i < length - k;i++){
            cur = cur.next;
        }
        return cur;
    }
    public int getLength(ListNode head){
        int length = 0;
        while(head != null){
            length++;
            head = head.next;
        }
        return length;
    }
}

这种解法能实现,但是好像不太符合题目要求的时间复杂度为 O ( n ) O(n) O(n),因为两次遍历其时间复杂度为 O ( n 2 ) O(n^2) O(n2)

1.2 解法2:先后指针

思路:定义先后两个指针slow和fast,让fast先走k步,然后slow和fast同时走,当fast为null时,slow刚好走到倒数第k个结点,然后输出此时的slow。

特殊情况判断:fast先走k步时,边走边判断。假如fast为空了,直接返回null,此时就说明k大于链表的长度。

public class Solution {
    /**
     * 代码中的类名、方法名、参数名已经指定,请勿修改,直接返回方法规定的值即可
     *
     * 
     * @param pHead ListNode类 
     * @param k int整型 
     * @return ListNode类
     */
    public ListNode FindKthToTail (ListNode pHead, int k) {
        // write code here
        ListNode slow = pHead;
        ListNode fast = pHead;
        for(int i = 0;i < k;i++){
            if(fast == null) return null;
            fast = fast.next;
        }
        while(fast != null){
            slow = slow.next;
            fast = fast.next;
        }
        return slow;
    }
}

1.3 解法3:栈

思路:利用栈先进后出的特点,可以方便的提取出后面倒数的元素。因此后面的结点会放在栈的栈顶,从而可以方便的弹出。
首先从头到尾遍历链表,将结点放入到栈中。然后进行特殊情况判断,如果栈的大小比k要小,直接return null即可。然后从栈顶弹出k个元素,最后一个出栈的元素就是结果。

import java.util.Stack;
public class Solution {
    /**
     * 代码中的类名、方法名、参数名已经指定,请勿修改,直接返回方法规定的值即可
     *
     * 
     * @param pHead ListNode类 
     * @param k int整型 
     * @return ListNode类
     */
    public ListNode FindKthToTail (ListNode pHead, int k) {
        // write code here
        Stack<ListNode> stack = new Stack<>();
        ListNode cur = pHead;
        while(cur != null){
            stack.push(cur);
            cur = cur.next;
        }
        if(stack.size() < k) return null;
        ListNode res = null;
        for(int i = 0;i < k;i++){
            res = stack.pop();
        }
        return res;
    }
}

JZ18 删除链表的节点

题目描述:给定单向链表的头指针和一个要删除的节点的值,定义一个函数删除该节点。返回删除后的链表的头节点。

1.1 解法1

同leetcode203.移除链表元素的题目。
分析两个题目,有不同点:203题中没有指明链表中节点值互不相同(意味着可能要删除的数值对应着多个节点),因此while循环一直判断,直到cur.next=null;而本题中,题目说明了保证链表中节点的值互不相同,因此一旦找到了等于删除数值的节点,就不用在执行while循环了,直接break即可。

思路:
需要创建一个虚拟头结点,然后定义一个临时结点cur,初始时指向虚拟头结点。接着让cur去遍历链表,注意循环的条件为cur.next不为空。
循环中,如果当前结点的下一个结点对应的数值等于val,那么就要删除掉当前结点的下一个结点,于是让cur.next=cur.next.next;如果不等于,此时就让cur往前移动一位,即cur=cur.next。
由于题目中保证链表中节点的值互不相同,因此可以找到了待删除结点,就直接break。

public class Solution {
    /**
     * 代码中的类名、方法名、参数名已经指定,请勿修改,直接返回方法规定的值即可
     *
     * 
     * @param head ListNode类 
     * @param val int整型 
     * @return ListNode类
     */
    public ListNode deleteNode (ListNode head, int val) {
        // write code here
        ListNode dummyHead = new ListNode(-1);
        dummyHead.next = head;
        ListNode cur = dummyHead;
        while(cur.next != null){
            if(cur.next.val == val){
                cur.next = cur.next.next;
                break; //比203题,多的一行判断
            }
            else{
                cur = cur.next;
            }
        }
        return dummyHead.next;
    }
}

JZ35 复杂链表的复制

题目描述:输入一个复杂链表(每个节点中有节点值,以及两个指针,一个指向下一个节点,另一个特殊指针random指向一个随机节点),请对此链表进行深拷贝,并返回拷贝后的头结点。 下图是一个含有5个结点的复杂链表。图中实线箭头表示next指针,虚线箭头表示random指针。为简单起见,指向null的指针没有画出。

1.1 解法1:哈希表

import java.util.HashMap;
public class Solution {
    public RandomListNode Clone(RandomListNode pHead) {
        if(pHead == null){
            return null;
        }
        HashMap<RandomListNode,RandomListNode> map = new HashMap<>();
        RandomListNode cur = pHead;
        while(cur != null){
            map.put(cur,new RandomListNode(cur.label));
            cur = cur.next;
        }
        cur = pHead;
        while(cur != null){
            map.get(cur).next = map.get(cur.next);
            map.get(cur).random = map.get(cur.random);
            cur = cur.next;
        }
        return map.get(pHead);
    }
}

1.2 解法2:拼接+拆分(原地法)

public class Solution {
    public RandomListNode Clone(RandomListNode pHead) {
        if(pHead == null){
            return null;
        }
        RandomListNode cur = pHead;
        // 复制各节点,并构建拼接链表
        while(cur != null){
            RandomListNode tmp = new RandomListNode(cur.label);
            tmp.next = cur.next;
            cur.next = tmp;
            cur = tmp.next;
        }
        // 构建新节点的random指向
        cur = pHead;
        while(cur != null){
            if(cur.random != null){
                cur.next.random = cur.random.next;
            }
            cur = cur.next.next;
        }
        // 拆分两个链表
        RandomListNode pre = pHead; //pre指向原链表头结点
        cur = pHead.next; //cur指向新链表头结点
        RandomListNode pNewHead = pHead.next; //pNewHead为新链表头结点
        while(cur.next != null){
            pre.next = pre.next.next;
            cur.next = cur.next.next;
            pre = pre.next;
            cur = cur.next;
        }
        pre.next = null; //处理原链表末尾节点
        return pNewHead;
    }
}

JZ76 删除链表中重复的结点

题目描述:在一个排序的链表中,存在重复的结点,请删除该链表中重复的结点,重复的结点不保留,返回链表头指针。 例如,链表 1->2->3->3->4->4->5 处理后为 1->2->5

1.1 解法1:哈希表+创建新链表

思路:本质就是创建一个新链表,将不重复的元素挂在新链表的后面。

①第一次遍历链表,统计链表中元素出现的次数,将链表中元素及其出现的次数放入哈希表中。
②然后创建一个新的虚拟头结点,作为结果链表的头结点,其实相当于把这个结果链表给创建出来。
③第二次遍历链表时,用一个指针cur去遍历原链表,用一个指针pre去指向新链表。如果链表中该元素出现的次数为1,那么将该元素添加到结果链表的后面。另外,无论这个次数是否等于1,cur指针都要向前移动,因此这个语句不是在if对应的else中,而是单独的列出来。

import java.util.HashMap;
public class Solution {
    public ListNode deleteDuplication(ListNode pHead) {
        HashMap<Integer,Integer> map = new HashMap<>();
        ListNode cur = pHead;
        while(cur != null){
            map.put(cur.val,map.getOrDefault(cur.val,0) + 1);
            cur = cur.next;
        }
        cur = pHead;
        ListNode dummyHead = new ListNode(-1);
        ListNode pre = dummyHead;
        while(cur != null){
            if(map.get(cur.val) == 1){
                pre.next = new ListNode(cur.val);
                pre = pre.next;
            }
            cur = cur.next;
        }
        return dummyHead.next;
    }
}

1.2 解法2:双指针原地删除

这个解法并没有创建新的链表,而是通过双指针,直接在原链表上面进行操作,是一种原地删除的方法。

思路:
①定义一个 dummy 头结点,链接上原链表,cur 指向原链表头部,pre指向dummy头结点
②当前结点value != 当前结点的下一结点value。那么让pre指针和当前指针都前进一步
③当前结点value == 当前结点的下一结点value。那么就让 cur 一直往下走直到 当前结点value != 当前结点的下一结点value,然后此时的cur指向了重复元素的最后一个元素,由于后面还有可能出现其他数值是重复的元素,因此不能动pre指针,所以让 pre->next = cur->next,然后当前结点移至下一结点。

public class Solution {
    public ListNode deleteDuplication(ListNode pHead) {
        ListNode dummyHead = new ListNode(-1);
        dummyHead.next = pHead;
        ListNode cur = pHead;
        ListNode pre = dummyHead;
        while(cur != null && cur.next != null){
            if(cur.next.val == cur.val){
                while(cur.next != null && cur.next.val == cur.val){
                    cur = cur.next;
                }
                pre.next = cur.next;
                cur = cur.next;
            }
            else{
                pre = cur;
                cur = cur.next;
            }
        }
        return dummyHead.next;
    }
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值