剑指offer(链表专题)

该专题为剑指offer专题,题目均来自《剑指offer》,里面每道题带有练习模式和考试模式,可还原考试模式进行模拟,也可通过练习模式进行练习。
里面也有之前101相关必刷题,相同的题目也作为一种回顾,继续写一遍!

文章目录

  • 知识分类篇-- 数据结构
  • 链表专题
    • 1、从尾到头打印链表
      • 思路1、栈
      • 思路2、递归
    • 2、反转链表
    • 3、合并两个排序的链表
      • 思路1、递归
      • 思路2、迭代版本
      • 思路3、借助额外数组
    • 4、 两个链表的第一个公共结点
      • 方法一分析:双指针
      • 、方法二、用集合Set
    • 5、链表中环的入口结点
      • 方法1:hash法,记录第一次重复的结点
      • 方法2:快慢指针
    • 6、链表中倒数最后k个结点
      • 方法1:使用辅助列表方法
      • 方法2:快慢指针
    • 7、复杂链表的复制
    • 8、删除链表中重复的结点
      • 思路一:直接比较删除(推荐使用)
      • 思路二、递归
    • 9、删除链表的节点
      • 思路、迭代遍历

知识分类篇-- 数据结构

链表专题

1、从尾到头打印链表

题目描述:输入一个链表的头节点,按链表从尾到头的顺序返回每个节点的值(用数组返回)。
如输入{1,2,3}的链表如下图:
在这里插入图片描述
返回一个数组为[3,2,1]

思路1、栈

栈的特性是先进后出,符合逆序的特点
栈是一种仅支持在表尾进行插入和删除操作的线性表,这一端被称为栈顶,另一端被称为栈底。元素入栈指的是把新元素放到栈顶元素的上面,使之成为新的栈顶元素;元素出栈指的是从一个栈删除元素又称作出栈或退栈,它是把栈顶元素删除掉,使其相邻的元素成为新的栈顶元素。

具体步骤:
step 1:我们可以顺序遍历链表,将链表的值push到栈中。
step 2:然后再依次弹出栈中的元素,加入到数组中,即可实现链表逆序。
剑指offer(链表专题)_第1张图片

import java.util.*;
public class Solution {
    public ArrayList<Integer> printListFromTailToHead(ListNode listNode) {
        // 使用栈
        ArrayList<Integer> res = new ArrayList<>();
        Stack<Integer> s = new Stack<>();
        while(listNode !=null){
            s.push(listNode.val);
            listNode = listNode.next;
        }
        // 将栈中元素输出
        while(!s.isEmpty()){
            res.add(s.pop());
        }
        return res;
    }

复杂度分析
时间复杂度:O(n),遍历链表是一个O(n),弹空一个栈需要O(n)
空间复杂度:O(n),栈空间最大长度是链表的长度n

思路2、递归

递归是一个过程或函数在其定义或说明中有直接或间接调用自身的一种方法,它通常把一个大型复杂的问题层层转化为一个与原问题相似的规模较小的问题来求解。因此递归过程,最重要的就是查看能不能讲原本的问题分解为更小的子问题,这是使用递归的关键。

我们都知道链表无法逆序访问,那肯定无法直接遍历链表得到从尾到头的逆序结果。但是我们都知道递归是到达底层后才会往上回溯,因此我们可以考虑递归遍历链表,因此三段式如下:

终止条件: 递归进入链表尾,即节点为空节点时结束递归。
返回值: 每次返回子问题之后的全部输出。
本级任务: 每级子任务递归地进入下一级,等下一级的子问题输出数组返回时,将自己的节点值添加在数组末尾。

具体步骤:
step 1:从表头开始往后递归进入每一个节点。
step 2:遇到尾节点后开始返回,每次返回依次添加一个值进入输出数组。
step 3:直到递归返回表头。

import java.util.*;
public class Solution {
    
    public void f1(ArrayList<Integer> res, ListNode head){
        if(head !=null){
            // 继续往深处进行探索,直到最后一个元素
            f1(res, head.next);
            res.add(head.val);
        }
    }
    
    public ArrayList<Integer> printListFromTailToHead(ListNode listNode){
        // 使用递归
        ArrayList<Integer> res = new ArrayList<>();
        f1(res, listNode);
        return res;
    }
}

2、反转链表

牛客TOP101之链表篇(高效刷题,带你手撕算法)
题目描述:给定一个单链表的头结点pHead(该头节点是有值的,比如在下图,它的val是1),长度为n,反转该链表后,返回新链表的表头。

如当输入链表{1,2,3}时, 经反转后,原链表变为{3,2,1},所以对应的输出为{3,2,1}。 以上转换过程如下图所示:
输入:{1,2,3}
返回值:{3,2,1}
输入:{}
返回值:{}
说明:空链表则输出空

剑指offer(链表专题)_第2张图片
常规思路:迭代
具体细节:在遍历链表时,将当前节点的next 指针改为指向前一个节点。由于节点没有引用其前一个节点,因此必须事先存储其前一个节点。在更改引用之前,还需要存储后一个节点。最后返回新的头引用。
剑指offer(链表专题)_第3张图片
拆解步骤

1、因为链表结尾是 null,所以让 pre 的值是 null, p 就表示我们的头部
剑指offer(链表专题)_第4张图片
2、因为 p 的 next 成员马上就要指向 pre, 如果不保存 p 的下一个节点就会使其丢失,所以通过临时变量 t 保存它
剑指offer(链表专题)_第5张图片
3、让 P 的 next 成员指向 pre
剑指offer(链表专题)_第6张图片
4、pre 移动到 p 的位置,p 移动到 t 的位置,此时我们就回到了第一步中的情况
剑指offer(链表专题)_第7张图片
5、保持这个循环不变式直到 p 移动到原链表结尾我们就获得了翻转之后的链表。

/*
public class ListNode {
    int val;
    ListNode next = null;

    ListNode(int val) {
        this.val = val;
    }
}*/
public class Solution {
    public ListNode ReverseList(ListNode head) {
        //pre指针:用来指向反转后的节点,初始化为null
        ListNode pre = null;
         //当前节点指针
        ListNode cur = head;
        //循环迭代
        while(cur!=null){
            //Cur_next 节点,永远指向当前节点cur的下一个节点
            ListNode Cur_next = cur.next;
            //反转的关键:当前的节点指向其前一个节点(注意这不是双向链表,没有前驱指针)
            cur.next = pre;
            //更新pre
            pre = cur;
            //更新当前节点指针
            cur = Cur_next ;
        }
        //为什么返回pre?因为pre是反转之后的头节点
        return pre;
    }
}

3、合并两个排序的链表

题目描述:输入两个递增的链表,单个链表的长度为n,合并这两个链表并使新链表中的节点仍然是递增排序的。
剑指offer(链表专题)_第8张图片

此题leetcode上也有一样的题目:21
主要介绍几种方法,方便理解

思路1、递归

函数功能:合并两个单链表,返回两个单链表头结点值小的那个节点。
如果知道了这个函数功能,那么接下来需要考虑2个问题:

递归函数结束的条件是什么?
递归函数一定是缩小递归区间的,那么下一步的递归区间是什么?
对于问题1.对于链表就是,如果为空,返回什么
对于问题2,跟迭代方法中的一样,如果list1的所指节点值小于等于list2所指的结点值,那么list1后续节点和list2节点继续递归

递归版本很好理解
public class Solution {
    public ListNode Merge(ListNode list1,ListNode list2) {
        if(list1 == null)
            return list2;
        if(list2 == null)
            return list1;
        if(list1.val <= list2.val){
            list1.next = Merge(list1.next,list2);
            return list1;
        }else{
            list2.next = Merge(list2.next,list1);
            return list2;
        }
        
    }

思路2、迭代版本

其实用迭代方法,也是常规考虑方法
步骤:
1、首选新创建一个链表head
2、如果list1指向的结点值小于等于list2指向的结点值,则将list1指向的结点值链接到head的next指针,然后head指向list1,list1继续指向下一个结点值;
3、否则,则将list2指向的结点值链接到head的next指针,然后head指向list2,让list2指向下一个结点值;继续循环步骤2,3
4、直到list1或者list2为空,将list1或者list2剩下的部分链接到head的后面

public ListNode Merge(ListNode list1,ListNode list2) {
        // 单链表完成
        ListNode head = new ListNode(-1);
        head.next = null;
        ListNode root = head;
        while(list1 !=null && list2 !=null){
            if(list1.val <= list2.val){
                head.next = list1;
                head = list1;
                list1 = list1.next;
            }else{
                head.next = list2;
                head = list2;
                list2 = list2.next;
            }
        }
        // 把剩余的链表直接连接到合并后的为尾部
        if(list1 !=null){
            head.next = list1;
        }
        if(list2 !=null){
            head.next = list2;
        }
        return root.next;
    }

思路3、借助额外数组

(1) 创建额外存储数组 nums
(2) 依次循环遍历 pHead1, pHead2,将链表中的元素存储到 nums中,再对nums进行排序
(3) 依次对排序后的数组 nums取数并构建合并后的链表
剑指offer(链表专题)_第9张图片

public ListNode Merge(ListNode list1,ListNode list2) {
         // list1 list2为空的情况
        if(list1==null) return list2;
        if(list2==null) return list1;
        if(list1 == null && list2 == null){
            return null;
        }
        //将两个两个链表存放在list中
        ArrayList<Integer> list = new ArrayList<>();
        // 遍历存储list1
        while(list1 !=null){
            list.add(list1.val);
            list1 = list1.next;
        }
        // 遍历存储list2
        while(list2 !=null){
            list.add(list2.val);
            list2 = list2.next;
        }
        Collections.sort(list);
         // 将list转换为 链表
        ListNode newHead = new ListNode(list.get(0));
        ListNode cur = newHead;
        for(int i=1;i<list.size();i++){
            cur.next = new ListNode(list.get(i));
            cur = cur.next;
        }
        // 输出合并链表
        return newHead;
    }

4、 两个链表的第一个公共结点

题目描述:输入两个无环的单向链表,找出它们的第一个公共结点,如果没有公共节点则返回空。
剑指offer(链表专题)_第10张图片

方法一分析:双指针

1、使用两个指针N1,N2,一个从链表1的头节点开始遍历,我们记为N1,一个从链表2的头节点开始遍历,我们记为N2。

2、让N1和N2一起遍历,当N1先走完链表1的尽头(为null)的时候,则从链表2的头节点继续遍历,同样,如果N2先走完了链表2的尽头,则从链表1的头节点继续遍历,也就是说,N1和N2都会遍历链表1和链表2。

3、因为两个指针,同样的速度,走完同样长度(链表1+链表2),不管两条链表有无相同节点,都能够到达同时到达终点。(N1最后肯定能到达链表2的终点,N2肯定能到达链表1的终点)。
(判断条件)如何得到公共节点:
1、有公共节点的时候,N1和N2必会相遇,因为长度一样嘛,速度也一定,必会走到相同的地方的,所以当两者相等的时候,则会第一个公共的节点。
2、无公共节点的时候,此时N1和N2则都会走到终点,那么他们此时都是null,所以也算是相等了。
剑指offer(链表专题)_第11张图片

public ListNode FindFirstCommonNode(ListNode pHead1, ListNode pHead2) {
        ListNode l1 = pHead1, l2 =pHead2;
        while(l1 != l2){
            l1 =(l1 == null)? pHead2 :l1.next;
            l2 =(l2 == null)? pHead1 :l2.next;
        }
        return l1;
    }

、方法二、用集合Set

具体步骤:
1、就是先把第一个链表的节点全部存放到集合set中。
2、然后遍历第二个链表的每一个节点,判断在集合set中是否存在,如果存在就直接返回这个存在的结点。
3、如果遍历完了,在集合set中还没找到,说明他们没有相交,直接返回null即可。

public ListNode FindFirstCommonNode(ListNode pHead1, ListNode pHead2) {
        // 创建集合set
        HashSet set = new HashSet<>();
        //把链表1的结点放在set中
        while(pHead1 !=null){
            set.add(pHead1);
            pHead1 = pHead1.next;
        }
        
        //访问链表2
        while(pHead2 !=null){
            if(set.contains(pHead2))
                return pHead2;
            pHead2 = pHead2.next;
        }
        
        return null;
    }

5、链表中环的入口结点

题目描述:给一个长度为n链表,若其中包含环,请找出该链表的环的入口结点,否则,返回null。
剑指offer(链表专题)_第12张图片

方法1:hash法,记录第一次重复的结点

通过使用set或者map来存储已经遍历过的结点,当第一次出现重复的结点时,即为入口结点。

import java.util.HashSet;
public class Solution {

    public ListNode EntryNodeOfLoop(ListNode pHead) {
        // 使用hash法,记录第一次重复的结点
        HashSet<ListNode> hash = new HashSet<>();
        // 存储的结点只要出现重复的,即为环的入口
        while(pHead !=null){
            if(hash.contains(pHead)){
                return pHead;
            }
            // 添加未重复的结点
            hash.add(pHead);
            pHead = pHead.next;
        }
        return null;
    }
}

时间复杂度:O(n),需要将链表的所有结点遍历一遍,时间复杂度为O(n);
空间复杂度:O(n),需要额外的n空间的hashset来存储结点。

方法2:快慢指针

这个之前在leetcode快慢指针篇中介绍过,通过定义slow和fast指针,slow每走一步,fast走两步,若是有环,则一定会在环的某个结点处相遇(slow == fast),根据下图分析计算,可知从相遇处到入口结点的距离与头结点与入口结点的距离相同。
剑指offer(链表专题)_第13张图片

public ListNode EntryNodeOfLoop(ListNode pHead){
        ListNode fast,slow;
        fast = slow = pHead;
        while(fast !=null && fast.next !=null){
            slow = slow.next;
            fast = fast.next.next;
            // 记录快慢指针第一次相遇的结点
            if(fast == slow)
               break;
        }
        // 如果快指针指向空,则不存在环
        if(fast == null || fast.next ==null) return null;
        // 重新指向链表头部
        fast = pHead;
        // 与第一次相遇的结点相同速度出发,相遇结点为入口结点
        while(fast != slow){
            fast = fast.next;
            slow = slow.next;
        }
        return fast;
    }

时间复杂度:O(n),需要将链表的所有结点遍历一遍,时间复杂度为O(n);
空间复杂度:O(1),需要额外两个快慢指针来遍历结点。

6、链表中倒数最后k个结点

输入一个长度为 n 的链表,设链表中的元素的值为 ai ,返回该链表中倒数第k个节点。
如果该链表长度小于k,请返回一个长度为 0 的链表。
剑指offer(链表专题)_第14张图片

输入:{1,2,3,4,5},2
返回值:{4,5}
说明:返回倒数第2个节点4,系统会打印后面所有的节点来比较。

方法1:使用辅助列表方法

先把所有输入值全保存在列表中,然后根据下标找到倒数第二的节点

public ListNode FindKthToTail (ListNode pHead, int k) {
        // write code here
       ArrayList<ListNode> list = new ArrayList<>();
       while(pHead !=null){
            list.add(pHead);
            pHead = pHead.next;
       }
       if(k > list.size() || k == 0)
           return null;
       
       return list.get(list.size()-k);
    }

方法2:快慢指针

让快指针先走k步,然后开始快慢指针同速前进,当快指针⾛到链表末尾 null 时,慢指针所在的位置就是倒数第 k个链表节点。

public ListNode FindKthToTail (ListNode pHead, int k) {
        // 使用快慢指针
        if(pHead == null)
            return pHead;
        ListNode slow,fast;
        slow = fast = pHead;
        // 将快指针向后移动k个位置
        while(k!=0){
            //如果k值过大,导致快指针为空,则返回null
            if(fast == null)
                return null;
            fast = fast.next;
            k--;
        }
        //快慢指针开始一起移动
        while(fast != null){
            slow = slow.next;
            fast = fast.next;
        }
        return slow;
    }

7、复杂链表的复制

题目描述:输入一个复杂链表(每个节点中有节点值,以及两个指针,一个指向下一个节点,另一个特殊指针random指向一个随机节点),请对此链表进行深拷贝,并返回拷贝后的头结点。

示例
输入:{1,2,3,4,5,3,5,#,2,#}
输出:{1,2,3,4,5,3,5,#,2,#}
我们将链表分为两段,前半部分{1,2,3,4,5}为ListNode,后半部分{3,5,#,2,#}是随机指针域表示。
以上示例前半部分可以表示链表为的ListNode:1->2->3->4->5 后半部分,3,5,#,2,#分别的表示为1的位置指向3,2的位置指向5,3的位置指向null,4的位置指向2,5的位置指向null 如下图:
剑指offer(链表专题)_第15张图片

思路:哈希表

我们正常拷贝链表节点,只要能够保证我们可以随机访问就行了。因此我们可以考虑哈希表,利用哈希表对链表原始节点和拷贝节点之间建立映射关系,因此原始链表支持随机指针访问,这样我们建立映射以后,可以借助哈希表与原始链表的随机指针,在拷贝链表上随机访问。

step 1:建立哈希表,key为原始链表的节点,value为拷贝链表的节点。
step 2:遍历原始链表,依次拷贝每个节点,并连接指向后一个的指针,同时将原始链表节点与拷贝链表节点之间的映射关系加入哈希表。
step 3:遍历哈希表,对于每个映射,拷贝节点的ramdom指针就指向哈希表中原始链表的random指针。

import java.util.*;
public class Solution {
    public RandomListNode Clone(RandomListNode pHead) {
        // 空节点直接返回
        if(pHead == null)
            return pHead;
        // 添加一个头部节点
        RandomListNode res = new RandomListNode(0);
        //哈希表,key为原始链表的节点,value为拷贝链表的节点
        HashMap<RandomListNode, RandomListNode> mp = new HashMap<>();
        RandomListNode cur = pHead;
        RandomListNode pre = res;
        // 遍历原始链表
        while(cur !=null){
            //拷贝节点
            RandomListNode clone = new RandomListNode(cur.label);
            mp.put(cur, clone);
            // 连接
            pre.next = clone;
            pre = pre.next;
            //遍历
            cur = cur.next;
        }
        // 遍历哈希表
        for(HashMap.Entry<RandomListNode, RandomListNode> entry : mp.entrySet()){
            //初始链表中random为空
            if(entry.getKey().random == null)
                entry.getValue().random = null;
            else
                entry.getValue().random = mp.get(entry.getKey().random);
        
        }
        // 返回去掉头
        return res.next;
        
    }
}

8、删除链表中重复的结点

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

例如输入{1,2,3,3,4,4,5}时,对应的输出为{1,2,5},对应的输入输出链表如下图所示:
剑指offer(链表专题)_第16张图片

思路一:直接比较删除(推荐使用)

这是一个升序链表,重复的节点都连在一起,我们就可以很轻易地比较到重复的节点,然后将所有的连续相同的节点都跳过,连接不相同的第一个节点。

//遇到相邻两个节点值相同
if(cur.next.val == cur.next.next.val){
    int temp = cur.next.val;
    //将所有相同的都跳过
    while (cur.next != null && cur.next.val == temp)
        cur.next = cur.next.next;

step 1:给链表前加上表头,方便可能的话删除第一个节点。

ListNode res = new ListNode(0);
//在链表前加一个表头
res.next = pHead;

step 2:遍历链表,每次比较相邻两个节点,如果遇到了两个相邻节点相同,则新开内循环将这一段所有的相同都遍历过去。
step 3:在step 2中这一连串相同的节点前的节点直接连上后续第一个不相同值的节点。
step 4:返回时去掉添加的表头。
剑指offer(链表专题)_第17张图片

public class Solution {
    public ListNode deleteDuplication(ListNode pHead) {
        //空链表
        if(pHead == null) 
            return null;
        ListNode res = new ListNode(0);
        //在链表前加一个表头
        res.next = pHead; 
        ListNode cur = res;
        while(cur.next != null && cur.next.next != null){ 
            //遇到相邻两个节点值相同
            if(cur.next.val == cur.next.next.val){ 
                int temp = cur.next.val;
                //将所有相同的都跳过
                while (cur.next != null && cur.next.val == temp) 
                    cur.next = cur.next.next;
            }
            else 
                cur = cur.next;
        }
        //返回时去掉表头
        return res.next; 
    }
}

思路二、递归

step 1:首先判断重复值,采取删除操作
step2 :用递归方法判断下一个节点是否出现与删除结点相同,相同则进行删除
step3 :一直进行递归操作,遇到重复直接按照step1、2 进行操作,没有重复值则继续向下递归。

public class Solution {
    public ListNode deleteDuplication(ListNode pHead) {
    	//递归
        if(pHead == null)
            return null;
        //发现有重复值
        if(pHead.next !=null && pHead.val == pHead.next.val){
            while(pHead.next !=null && pHead.val == pHead.next.val){
                pHead = pHead.next; // 直接进行删除
            }
            //把与删除的那个结点相同的结点也进行删除
            return deleteDuplication(pHead.next);
        }
        //当没有发现重复值的情况,就一直进行递归操作
        pHead.next = deleteDuplication(pHead.next);
        return pHead;
    }
}

9、删除链表的节点

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

1.此题对比原题有改动
2.题目保证链表中节点的值互不相同
3.该题只会输出返回的链表和结果做对比,所以若使用 C 或 C++ 语言,你不需要 free 或 delete 被删除的节点

输入:{2,5,1,9},5
返回值:{2,1,9}
说明:给定你链表中值为 5 的第二个节点,那么在调用了你的函数之后,该链表应变为 2 -> 1 -> 9

思路、迭代遍历

剑指offer(链表专题)_第18张图片
step 1:首先我们加入一个头部节点,方便于如果可能的话删除掉第一个元素。
step 2:准备两个指针遍历链表,一个指针指向当前要遍历的元素,另一个指针指向该元素的前序节点,便于获取它的指针。
step 3:遍历链表,找到目标节点,则断开连接,指向后一个。
step 4:返回时去掉我们加入的头节点。

 public ListNode deleteNode (ListNode head, int val) {
        //加入一个头节点
        ListNode res = new ListNode(0);
        res.next = head;
        //前序节点
        ListNode pre = res;
        //当前节点
        ListNode cur = head;
        //遍历链表
        while(cur != null){
            //找到目标节点
            if(cur.val == val){
                //断开连接
                pre.next = cur.next;
                break;
            }
            pre = cur;
            cur = cur.next;
        }
        //返回去掉头节点
        return res.next;

你可能感兴趣的:(小曾带你刷牛客,链表,数据结构,剑指offer,刷题,Java)