一文通数据结构与算法之——链表+常见题型与解题策略+Leetcode经典题

文章目录

    • 1 链表
      • 1.1 常见题型及解题策略
        • 1.1.1 LeetCode中关于链表的题目有以下五种类型题:
        • 1.1.2 解题策略
      • 1.2 链表的基本内容
        • 1.2.1 链表的基本结构:
        • 1.2.2 插入新元素
        • 1.2.3 删除某个元素
        • 1.2.4 遍历单链表
      • 1.3 删除链表结点类题目
        • 1.3.1 题解方法
        • 1.3.2 可能出现的问题
        • 1.3.3 题库列表:
          • 237、删除链表中的节点
          • 203、移除链表元素
          • 剑指 Offer 18. 删除链表的节点
          • 面试题 02.01. 移除重复节点
          • 82. 删除排序链表中的重复元素 II
          • 19、删除链表的倒数第 N 个结点
          • 876、链表的中间结点
          • 86、分隔链表
          • 328、奇偶链表(分割链表的变形)
      • 1.4 反转链表节点类题目
        • 1.4.1 动图解释,双指针修改指针的方向
        • 1.4.2 题库列表
          • 206、反转链表
          • 92、反转链表 II
          • 234、 回文链表
          • 25. K 个一组翻转链表
      • 1.5 合并链表
        • 1.5.1 题库列表
          • 21、合并两个有序链表
          • 23、合并K个升序链表
      • 1.6 排序链表
        • 1.6.1 题库列表
          • 147、对链表进行插入排序
          • 148、排序链表:归并法
      • 1.7 环形链表
        • 1.7.1 题库列表
          • 160、相交链表
          • 141、环形链表
          • 142、环形链表II

1 链表

链表是最基本的数据结构,面试官常常用链表来考察面试者的基本能力,而且链表相关的操作相对而言比较简单,也适合考察写代码的能力。链表的操作也离不开指针,指针又很容易导致出错。综合多方面的原因,链表题目在面试中占据着很重要的地位。

以下内容思路主要参考:算法面试题 | 链表问题总结

1.1 常见题型及解题策略

1.1.1 LeetCode中关于链表的题目有以下五种类型题:

  • 删除链表节点
  • 反转链表
  • 合并链表
  • 排序链表
  • 环形链表

一文通数据结构与算法之——链表+常见题型与解题策略+Leetcode经典题_第1张图片


1.1.2 解题策略

  • dummy虚拟头节点,专门处理头结点可能会被改动的情况
  • 快慢双指针

1.2 链表的基本内容

推荐一篇文章:基础知识讲的很清楚,Java数据结构与算法之链表

链表的分类:单链表(分带头结点和不带头结点的单链表,就是head里面有没有data的区别)、双向链表、循环链表

image-20210920171724878

一文通数据结构与算法之——链表+常见题型与解题策略+Leetcode经典题_第2张图片

重点理解指针的概念

如下代码 ans.next 指向什么?
  
ans = ListNode(1)
ans.next = head   //ans.next 指向取决于最后切断 ans.next 指向的地方在哪,所以ans.next指向head
head = head.next  //ans 和 head 被切断联系了
head = head.next

一文通数据结构与算法之——链表+常见题型与解题策略+Leetcode经典题_第3张图片

ans = ListNode(1)
head = ans// ans和head共进退
head.next = ListNode(3)
head.next = ListNode(4)
// ans.next 指向什么?ListNode(3)
ans = ListNode(1)
head = ans           //head 和 ans共进退
head.next = ListNode(3)
head = ListNode(2)   //head 和 ans 的关系就被切断了
head.next = ListNode(4)

居然找到人跟我一样卡在这里了,笑死

一文通数据结构与算法之——链表+常见题型与解题策略+Leetcode经典题_第4张图片

1.2.1 链表的基本结构:

* public class ListNode {
*     int val;
*     ListNode next;
*     ListNode() {}
*     ListNode(int val) { this.val = val; }
*     ListNode(int val, ListNode next) { this.val = val; this.next = next; }
* }

1.2.2 插入新元素

//0.找到要插入的位置
temp = 待插入位置的前驱节点.next	      //1.先用一个临时节点把 待插位置后面的内容先存起来
待插入位置的前驱节点.next = 待插入指针  //2.将新元素插入
待插入指针.next = temp		     //3.再把后面的元素接到新元素的next

1.2.3 删除某个元素

待删除位置的前驱节点.next = 待删除位置的前驱节点.next.next

一文通数据结构与算法之——链表+常见题型与解题策略+Leetcode经典题_第5张图片

1.2.4 遍历单链表

当前指针 =  头指针
while 当前节点不为空 {
   print(当前节点)
   当前指针 = 当前指针.next
}

for (ListNode cur = head; cur != null; cur = cur.next) {
    print(cur.val)
}

1.3 删除链表结点类题目

1.3.1 题解方法

  1. 画草图:理解指针的变动与思考逻辑!!(重要!实用!)
  2. 边界条件:怎么处理不会有空指针异常?在循环里放什么停止条件
  • 如果是遍历链表元素,while(node!=null)
  • 如果是删除某个元素,需要,while(node.next!=null)
  • 需要考虑的仅仅是被改变 next 指针的部分,并且循环之后哪个指针在最后的节点处,就判断谁
//比如快慢指针,输出中间节点,slow和fast的指针都在变,但是fast先指向链表尾巴,所以判断 fast
//同时每个判断next.next的都必须先判断,next,才能保证 奇偶链长 中不会出现空指针异常
while(fast.next!=null && fast.next.next!=null){
            slow = slow.next;
            fast = fast.next.next;
        }
  1. 只要会删除头结点,都要进行dummy虚指针
  2. 特殊的需求可以考虑结合各种工具类,比如删除重复里面,利用HashSet,删除倒数第k个,利用栈LinkedList

1.3.2 可能出现的问题

NullPointerException,就是当前节点为空,我们还去操作它的 next

② 输出不了结果,一定是指针移动出了问题

1.3.3 题库列表:

237. 删除链表中的节点 ====面试题 02.03. 删除中间节点

203. 移除链表元素(虚拟头结点)

  • 83. 删除排序链表中的重复元素
  • 剑指 Offer 18. 删除链表的节点
  • 面试题 02.01. 移除重复节点
  • 82. 删除排序链表中的重复元素 II

19. 删除链表的倒数第 N 个结点(双指针经典类型)

  • 876. 链表的中间结点
  • 86. 分隔链表
  • 328. 奇偶链表
237、删除链表中的节点
//237.传入待删除结点,直接将当前节点的值改为next的值,next指向next.next,实现原地更新。
public void deleteNode(ListNode node) {
    node.val = node.next.val;
    node.next = node.next.next;
}
203、移除链表元素

① 如果删除的节点是中间的节点,则问题似乎非常简单:

  • 选择要删除节点的前一个结点 prev
  • prevnext 设置为要删除结点的 next

② 当要删除的一个或多个节点位于链表的头部时,要另外处理

三种方法:

  1. 删除头结点时另做考虑(由于头结点没有前一个结点)
  2. 添加一个虚拟头结点,删除头结点就不用另做考虑
  3. 递归
  4. 双指针法

即便是参考别人的代码,一看就看的懂,但其实我们有时候不知道内涵,只要自己闭着眼睛敲一遍,发现了问题,才知道是怎么考虑出来的

// 执行耗时:1 ms,击败了99.79% 的Java用户
// 内存消耗:39.4 MB,击败了49.10% 的Java用户
// 时间复杂度是O(n)。空间复杂度O(1)

public ListNode removeElements(ListNode head, int val) {
        //删除值相同的头结点后,可能新的头结点也值相等,用循环解决
        //比如输入 [7 7 7 7] 删除7,我一开始是直接用 if,发现有些案例无法通过才知道用while的原因
        while(head!=null && head.val==val){
            head = head.next;
        }
		//因为前面是对head的操作,所以极可能最后完了,head为空,所以把判断的过程放在后面
        //我本来是吧if放在删除头结点的前面打,结果报错空指针异常,所以才知道为什么判空的要放在后面
        if(head==null){
            return head;
        }

        ListNode temp = head;//临时指针
        while(temp.next!=null){
            if(temp.next.val==val){
                temp.next = temp.next.next;
            }else{
                temp = temp.next;
            }
        }
        return head;
    }

添加一个虚拟头结点

//执行耗时:1 ms,击败了99.79% 的Java用户
//内存消耗:39.2 MB,击败了82.52% 的Java用户
//时间复杂度是O(n)。空间复杂度O(1)
  
public ListNode removeElements(ListNode head, int val){
//        创建虚节点
        ListNode dummyNode = new ListNode(val-1);
        dummyNode.next = head;
  
        ListNode prev = dummyNode;
        while(prev.next!=null){
            if(prev.next.val==val){
                prev.next = prev.next.next;
            }else{
                prev = prev.next;
            }
        }
        return dummyNode.next;
    }

递归

//时间复杂度是O(n)。递归的方法调用栈深度是n,所以空间复杂度O(n),会超时
public ListNode removeElements(ListNode head, int val){
        if(head==null){
            return head;
        }
        // 因为递归函数返回的是已经删除节点之后的头结点
        // 所以直接接上在head.next,最后就只剩下判断头结点是否与需要删除的值一致了
        head.next = removeElements(head.next,val);
        if(head.val==val){
            return head.next;
        }else{
            return head;
        }
    }
剑指 Offer 18. 删除链表的节点

双指针

// 剑指 Offer 18. 删除链表的节点

public ListNode deleteNode(ListNode head, int val){
        if(head.val==val){//因为互不相等,如果头指针相等,直接返回
            return head.next;
        }
        //双指针
        ListNode pre = head;
        ListNode cur = head.next;
        while(cur!=null && cur.val!=val){//找元素
            pre = cur;
            cur = cur.next;
        }
        if(cur!=null){//找到了,进行删除操作
            pre.next = cur.next;
        }
        return head;//删完了,返回
    }
面试题 02.01. 移除重复节点
// 面试题 02.01. 移除重复节点
// 法一:借助HashSet的特征
// 移除未排序链表中的重复节点。保留最开始出现的节点,重复的元素不一定连续
    public ListNode removeDuplicateNodes(ListNode head) {
        if (head == null) {
            return head;
        }
        ListNode temp = head;
        HashSet<Integer> set = new HashSet<>();
        set.add(head.val);
        while(temp.next!=null){
            if(set.add(temp.next.val)){//加进去说明不重复
                temp = temp.next;
            }else{
                temp.next = temp.next.next;//原地删除
            }
        }
        return head;
    }
// 法二:用空间换时间
// 双重循环,一个定位一个遍历后序,用时间换空间
    public ListNode removeDuplicateNodes(ListNode head) {
        if(head==null){
            return head;
        }
        ListNode pre = head;
        while(pre!=null){
            ListNode cur = pre;
            while(cur.next!=null){
                if(cur.next.val==pre.val){
                    cur.next = cur.next.next;
                }else{
                    cur = cur.next;
                }
            }
            pre = pre.next;
        }
        return head;
    }
82. 删除排序链表中的重复元素 II
    //升序链表,删除链表中所有重复的节点【1 1 1 1 2 3】-->【2 3】
    //双指针记录pre 用cur记录相同的数,加虚头节点
    public ListNode deleteDuplicates(ListNode head) {
        if(head==null){
            return head;
        }
        ListNode dummy = new ListNode(0);//可能删除头结点,所以使用虚节点
        dummy.next = head;
        ListNode pre = dummy;
        ListNode cur = dummy.next;

        while(cur!=null && cur.next!=null){//画图最好理解
            if(cur.val==cur.next.val ){
                //如果有奇数个相同的值,就删不完,所以必须用while循环
                while(cur!=null && cur.next!=null && cur.val==cur.next.val ){
                    cur = cur.next;//找到最后一个相等的数
                }
                pre.next = cur.next;
                cur = pre.next;
            }else{
                pre = cur;
                cur = cur.next;
            }
        }
        return dummy.next;
    }
19、删除链表的倒数第 N 个结点
// 删除链表的倒数第 n 个结点,并且返回链表的头结点
// 双指针

public ListNode removeNthFromEnd(ListNode head, int k){
        if(head==null) return head;
//        可能会删除头结点
        ListNode dummy = new ListNode(0,head);
        ListNode pre = dummy.next;
        for (int i = 0; i < k; i++) {
            pre = pre.next;
        }
        ListNode cur = dummy;
        while(pre!=null){
            cur = cur.next;
            pre = pre.next;
        }
        cur.next = cur.next.next;
        return dummy.next;
    }
//    另外一个方法,利用栈的先进后出特点,效率会更低
//    执行耗时:1 ms,击败了19.42% 的Java用户
//    内存消耗:37.7 MB,击败了5.02% 的Java用户
public ListNode removeNthFromEnd(ListNode head, int n) {
    if(head==null){
        return head;
    }
    ListNode dummy = new ListNode(0,head);
    ListNode temp = dummy;
    LinkedList<ListNode> stack = new LinkedList<>();
    while(temp!=null){
        stack.push(temp);
        temp = temp.next;
    }
    for (int i = 0; i < n; i++) {
        stack.pop();
    }
    ListNode pre = stack.peek();
    pre.next = pre.next.next;
    return dummy.next;
}
876、链表的中间结点
//执行耗时:0 ms,击败了100.00% 的Java用户
//内存消耗:35.7 MB,击败了68.38% 的Java用户
public ListNode middleNode(ListNode head) {
        ListNode slow = head;
        ListNode fast = head;
        //如果不加fast != null,链表元素个数为偶数时会报空指针异常
        while(fast!=null && fast.next!=null){
            slow = slow.next;
            fast = fast.next.next;
        }
        return slow;
    }
86、分隔链表

两个临时链表

// 给你一个链表的头节点 head 和一个特定值 x ,请你对链表进行分隔,使得所有 小于 x 的节点都出现在 大于或等于 x 的节点之前。
// 另外创建一个链表,遍历原来的链表,删除小于的接上去。可能删除头结点
    public ListNode partition(ListNode head, int x) {
        ListNode small = new ListNode(0);//可能会动头结点,所以需要虚节点
        ListNode smallHead = small;//要记住头结点,所以需要另外设置Head
        ListNode large = new ListNode(0);
        ListNode largeHead = large;

        while(head!=null){
            if(head.val<x){
                small.next = head;
                small = small.next;
            }else{
                large.next = head;
                large = large.next;
            }
            head = head.next;
        }
        large.next = null;//再拼接两个链表,尾巴指向null
        small.next = largeHead.next;
        return smallHead.next;
    }
  
328、奇偶链表(分割链表的变形)

两个临时链表的变形

    // 给定一个单链表,把所有的奇数节点和偶数节点分别排在一起。
    // 请注意,这里的奇数节点和偶数节点指的是节点编号的奇偶性,而不是节点的值的奇偶性。
    // 法一,利用额外空间

public ListNode oddEvenList(ListNode head) {
        if(head==null){
            return head;
        }

        ListNode odd = new ListNode(0);
        ListNode oddHead = odd;
        ListNode even = new ListNode(0);
        ListNode evenHead = even;

        int count = 1;
        while(head!=null){
            if(count%2==1){//奇数
                odd.next = head;
                odd = odd.next;
            }else{
                even.next = head;
                even = even.next;
            }
            head = head.next;
            count++;
        }
        even.next = null;
        odd.next = evenHead.next;
        return oddHead.next;
    }

直接双指针前后遍历奇数偶数

//    不需要额外空间,双指针操作
    public ListNode oddEvenList(ListNode head) {
        if(head==null){
            return head;
        }

        ListNode odd = head;
        ListNode even = head.next;
        ListNode evenHead = even;

        while(even!=null && even.next!=null){
            odd.next = even.next;//先把奇数连起来
            odd = odd.next;//移动奇数的指针
            even.next = odd.next;//此时加偶数
            even = even.next;//移动偶数的指针
        }
        odd.next = evenHead;
        return head;
    }

1.4 反转链表节点类题目

反转的这些操作尤其需要记忆,要每天多写几遍才可以。


1.4.1 动图解释,双指针修改指针的方向

一文通数据结构与算法之——链表+常见题型与解题策略+Leetcode经典题_第6张图片

1.4.2 题库列表

206. 反转链表====剑指 Offer 24. 反转链表

92. 反转链表 II

234. 回文链表====面试题 02.06. 回文链表

25. K 个一组翻转链表

206、反转链表

双指针法迭代

//给你单链表的头节点 head ,请你反转链表,并返回反转后的链表。
    public ListNode reverseList(ListNode head) {
        if(head==null){
            return head;
        }
        ListNode pre = null;
        ListNode cur = head;
        while(cur!=null){
            ListNode temp = cur.next;
            cur.next = pre;
            pre = cur;
            cur = temp;
        }
        return pre;
    }

递归法:可参考文章:labuladong:递归反转链表的一部分

  • 使用递归的3个条件
    1. 大问题可以拆解成两个子问题
    2. 子问题的求解方式和大问题一样
    3. 存在最小子问题
//    递归法
    public ListNode reverseList(ListNode head) {
        if(head==null || head.next==null){
            return head;
        }
        //递归最重要的是明确递归函数的定义。
        //reverseList代表”「以 head 为起点」的链表反转,并返回反转之后的头结点“
        //所以last表示 head.next后面一整段反转之后的头结点。所以最后return last
        ListNode last = reverseList(head.next);
        //重点理解,此时head.next指向的已经是反转部分的尾巴,也就是图中的2
        head.next.next = head;
        head.next = null;//head指向null,表示此时head已经是尾巴了。
        return last;
    }

一文通数据结构与算法之——链表+常见题型与解题策略+Leetcode经典题_第7张图片

92、反转链表 II

① 穿针引线法,三个指针

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Mv8Avw5Y-1633245545523)(…/…/…/AppData/Roaming/Typora/typora-user-images/image-20210920172349817.png)]

node = cur.next;//cur表示当前操作的指针,node保存后面的顺序
cur.next = node.next;
node.next = pre.next;
pre.next = node;

方法一:迭代

// 反转位置left到位置right中间的部分,其余部分不变,
public ListNode reverseBetween(ListNode head, int left, int right) {
    ListNode dummy = new ListNode(-1,head);
    //迭代法,先找到起点
    ListNode pre = dummy;
    for (int i = 0; i < left-1; i++) {
        pre = pre.next;//来到 left 节点的前一个节点
    }
    ListNode cur = pre.next;//cur是真正反转的指针
    ListNode node;//node的保存cur.next 的临时指针
    for (int i = 0; i < right - left; i++) {
        node = cur.next;//保存后面的顺序
        cur.next = node.next;//穿针引线,其实很有规律
        node.next = pre.next;
        pre.next = node;
    }
    return dummy.next;
}

方法二,递归

//  递归法反转前n个元素
    public ListNode reverseN(ListNode head,int n){
        ListNode succssor = null;
        if(n==1){
            successor = head.next;//把后继记录下来,基线是只有一个元素
            return head;
        }
//        看递归不要进递归函数里面,而是看函数返回了什么结果
        ListNode last = reverseN(head.next,n-1);
//        整个链表 = head+reverseN(n-1)这一个已经反转的小团体 + succssor后继
//        注意此时head.next指向的是已经反转的小团体的末尾
        head.next.next = head;
        head.next = succssor;
        return last;
    }

    public ListNode reverseBetween(ListNode head, int left, int right) {
        if(left==1){
            return reverseN(head,right);
        }
        head.next = reverseBetween(head.next,left-1,right-1);
        return head;
    }
234、 回文链表

一文通数据结构与算法之——链表+常见题型与解题策略+Leetcode经典题_第8张图片

//    请判断一个链表是否为回文链表。
//    反转后半段,两头往中间遍历是否相等,返回前复原链表
    public boolean isPalindrome(ListNode head) {
//        1.快慢指针找到中间节点
        ListNode slow = head;
        ListNode fast = head;
        while(fast!=null && fast.next!=null){
            slow = slow.next;
            fast = fast.next.next;
        }
        ListNode point = slow;//存储中间的断点,用于恢复原来的顺序

        if(fast!=null){
            slow = slow.next;//slow要定位到中间的后一个位置
        }
//        2.反转slow后面的元素
        ListNode left = head;
        ListNode right = reverse(slow);

        ListNode q = right;//存储末尾的断点用于恢复原来链表的顺序

//        3.两头往中间遍历,是否相等,因为后半段尾巴是null
        while(right!=null){
            if(left.val!=right.val){
                return false;
            }
            left = left.next;
            right = right.next;
        }

        point.next = reverse(q);//还原链表
        return true;

    }

    public ListNode reverse(ListNode head){
        if(head==null){
            return head;
        }
        ListNode pre = null;
        ListNode cur = head;
        while(cur!=null){
            ListNode temp = cur.next;
            cur.next = pre;
            pre = cur;
            cur = temp;
        }
        return pre;
    }
25. K 个一组翻转链表

感觉一点设计这种一段一段的都可以考虑递归等算法,但对我的下意识总是,暴力求解。。。

学习!努力学习!

 public ListNode reverseKGroup(ListNode head, int k) {
        if(head==null){
            return head;
        }
//        1.反转前k个元素
        ListNode pre = head;
        ListNode cur = head;
        for (int i = 0; i < k; i++) {
            if(cur==null){
                return head;//不够长,保持不变
            }
            cur = cur.next;
        }
        ListNode newHead = reverse(pre,cur);
   
//        2.递归反转后面的,并将后续的连接
//        pre一直没有动,所以pre变成最后一个元素之后将next连上
        pre.next = reverseKGroup(cur,k);
        return newHead;
    }

//  反转区间[head end)之间的元素
    public ListNode reverse(ListNode head,ListNode end){
        ListNode pre = null;
        ListNode cur = head;
        while(cur!=end){
            ListNode temp = cur.next;
            cur.next = pre;
            pre = cur;
            cur = temp;
        }
        return pre;
    }

1.5 合并链表

合并有序链表问题在面试中出现频率 较高

1.5.1 题库列表

21. 合并两个有序链表

23. 合并K个升序链表

21、合并两个有序链表
/**
 * 将两个升序链表合并为一个新的 升序 链表并返回。新链表是通过拼接给定的两个链表的所有节点组成的
 */
public class _03_01_mergeList {
  
    public ListNode mergeTwoLists(ListNode l1, ListNode l2) {
//        双指针比较两位置大小,新建一链表
        ListNode newHead = new ListNode(0);
        ListNode head = newHead;

        while(l1!=null && l2!=null){
            if(l1.val>l2.val){
                newHead.next = l2;
                l2 = l2.next;
            }else{
                newHead.next = l1;
                l1 = l1.next;
            }
            newHead = newHead.next;
        }

//      只有一个是空的,那么便把另一个直接接上去就可以了
        newHead.next = l1==null?l2:l1;
        return head.next;
    }

    //    2.递归法
    public ListNode mergeTwoLists2(ListNode l1, ListNode l2) {
//        递归基线是当前数组为空,直接返回
        if(l1==null){
            return l2;
        }
        if(l2==null){
            return l1;
        }
//          判断当前的大小,原地修改
        if(l1.val<=l2.val){
            l1.next = mergeTwoLists(l1.next,l2);
            return l1;
        }else{
            l2.next = mergeTwoLists(l1,l2.next);
            return l2;
        }
    }
}
23、合并K个升序链表

方法一:逐个合并,方法二,递归分治,时间消耗相对少很多

public ListNode mergeKLists(ListNode[] lists) {
        if(lists.length==0){
            return null;
        }
//        遍历链表数组,每次选择两个链表,进行合并
        int i=0;
        ListNode head = null;
        while(i<lists.length){
            head = merge(head,lists[i]);
            i++;
        }
        return head;
    }

    public ListNode merge(ListNode l1,ListNode l2){
        ListNode newHead = new ListNode(0);
        ListNode head = newHead;

        while(l1!=null && l2!=null){
            if(l1.val>l2.val){
                newHead.next = l2;
                l2 = l2.next;
            }else{
                newHead.next = l1;
                l1 = l1.next;
            }
            newHead = newHead.next;
        }

//      只有一个是空的,那么便把另一个直接接上去就可以了
        newHead.next = l1==null?l2:l1;
        return head.next;
    }

一文通数据结构与算法之——链表+常见题型与解题策略+Leetcode经典题_第9张图片

	public ListNode mergeKLists(ListNode[] lists) {
        return mergeFrom(lists,0,lists.length-1);

    }

    public ListNode mergeFrom(ListNode[] lists,int left,int right){
        if(left==right){
            return lists[left];
        }
        if(left>right){
            return null;
        }
        int mid = left + (right-left)/2;
        //merge是合并两个链表的方法,如上
        return merge(
                mergeFrom(lists,left,mid),
                mergeFrom(lists,mid+1,right)
        );
    }

1.6 排序链表

1.6.1 题库列表

147. 对链表进行插入排序

148. 排序链表

147、对链表进行插入排序
 public ListNode insertionSortList(ListNode head) {
        if(head==null){
            return head;
        }
//        1.会移动头结点,所以用到虚拟头结点
        ListNode dummy = new ListNode(0);
        dummy.next = head;

//        2.外层循环遍历完链表所有数,遍历[head,lastSort]这段位置找插入
        ListNode cur = dummy.next;
        ListNode lastSort = head;//维护已排序部分的最后一个位置
        while(cur!=null){//cur为遍历的待插入元素
            if(lastSort.val<=cur.val){
                lastSort = cur;//大,直接后移
            }else{
                ListNode pre = dummy;//用来遍历已经排序的部分

//            3.从前往后比较,找插入的位置
                while(cur.val>pre.next.val){
                    pre = pre.next;
                }
//            4.找到位置进行插入操作
                lastSort.next = cur.next;
                cur.next = pre.next;
                pre.next = cur;
            }
//            5.指针后移
            cur = lastSort.next;
        }
        return dummy.next;
    }
148、排序链表:归并法

一文通数据结构与算法之——链表+常见题型与解题策略+Leetcode经典题_第10张图片

// 要求时间空间复杂度分别为O(nlogn)和O(1):归并排序
// 递归额外空间:递归调用函数将带来O(logn)的空间复杂度
//    使用递归版归并,会额外用到logn的时间复杂度

    public ListNode sortList(ListNode head) {
//        1.base line
        if(head==null || head.next==null){
            return head;
        }

//        2.找中点,偶数找的前面那个中点的位置,奇数找到中点
        ListNode slow =head;
        ListNode fast = head.next;
        while(fast!=null && fast.next!=null){
            slow = slow.next;
            fast = fast.next.next;
        }

//        3.将链表分割成两个子链表
        ListNode temp = slow.next;
        slow.next = null;
        ListNode left = sortList(head);
        ListNode right = sortList(temp);

//        4.新建一个链表,对已排序的链表进行归并操作
        ListNode newHead = new ListNode(0);
        ListNode res = newHead;
        while(left!=null && right!=null){
            if(left.val<right.val){
                newHead.next = left;
                left = left.next;
            }else{
                newHead.next = right;
                right = right.next;
            }
            newHead = newHead.next;
        }
        newHead.next = left==null?right:left;
        return res.next;
    }

1.7 环形链表

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ya2BbAvB-1633245545528)(image/算法/e1dbea51f21247a4972c2cb28855609a~tplv-k3u1fbpfcp-watermark.webp?lastModify=1632129814)]

1.7.1 题库列表

  • 160. 相交链表

  • 141. 环形链表

    • 142. 环形链表 II
160、相交链表
// 给你两个单链表的头节点 headA 和 headB ,请你找出并返回两个单链表相交的起始节点。如果两个链表没有交点,返回 null 。
// Set里放的是ListNode而不是ListNode.val,比较的是指针地址
// 方法一:哈希表
//    方法一:哈希表法。存的是ListNode,所以相等时代表地址相同,也就是同一个元素
//    即便val相等,在哈希判断是也不会相等,所以可以使用hash表的方法
    public ListNode getIntersectionNode2(ListNode headA, ListNode headB) {
        if(headA==null|| headB==null){
            return null;
        }
//      1.先将某一个链表中的元素存到 set 中
        HashSet<ListNode> set = new HashSet<>();
        ListNode cur = headA;
        while(cur!=null){
            set.add(cur);
            cur = cur.next;
        }

//      2.再遍历第二个链表,如果有就直接返回,如果没有继续遍历
        ListNode node= headB;
        while(node!=null){
            if(set.contains(node)){
                return node;
            }
            node = node.next;
        }

        return null;
    }

//方法二:双指针
 public ListNode getIntersectionNode(ListNode headA, ListNode headB) {
            if(headA==null || headB==null){
                return null;
            }
            ListNode nodeA = headA;
            ListNode nodeB = headB;
            while(nodeA!=nodeB){
                // 退出的关键是:指向同一个指针(不是值相等),或者都指向null
                nodeA = nodeA==null?headB:nodeA.next;
                nodeB = nodeB==null?headA:nodeB.next;
            }
            return nodeA;//如果没有相等的那么nodeA==nodeB==null
    }
  
141、环形链表
//    双指针
    public boolean hasCycle(ListNode head) {
        if(head==null ||head.next==null){
            return false;
        }
        ListNode slow = head;
        ListNode fast = head.next;
        while(slow!=fast){
//            因为快指针在前面,所以只要判断快指针是否达到了队尾就可以
            if(fast==null || fast.next==null){
                return false;
            }
            slow = slow.next;
            fast = fast.next.next;
        }
        return true;
    }

//    哈希表,耗时非常慢
    public boolean hasCycle(ListNode head) {
         if(head==null){
             return false;
         }
        HashSet<ListNode> set = new HashSet<>();
         ListNode cur = head;
         while(cur!=null){
             if(set.contains(cur)){
                 return true;
             }
             set.add(cur);
             cur = cur.next;
         }
         return false;
    }
142、环形链表II
//给定一个链表,返回链表开始入环的第一个节点。 如果链表无环,则返回 null。
//    方法一是哈希表,方法二双指针
    public ListNode detectCycle(ListNode head) {
        ListNode slow = head;
        ListNode fast = head;
//        1.快慢指针找重合点
        while(fast!=null && fast.next!=null){
            slow = slow.next;
            fast = fast.next.next;
//        2.重合了,这个时候,从头来一个指针遍历
            if(fast==slow){
                ListNode cur = head;
                while(cur!=slow){
                    cur = cur.next;
                    slow = slow.next;
                }
                return slow;
            }
        }
//        3没有环,返回null
        return null;
    }

你可能感兴趣的:(Java后端面试准备,数据结构与算法,链表,算法,数据结构)