数据结构——链表相关题目

数据结构——链表相关题目

    • leetcode 203.移除链表元素
      • 1.直接用原来的链表进行删除操作
      • 2.利用虚拟头结点进行删除操作
    • leetcode 83.删除排序链表中的重复元素
      • 思路
    • leetcode 19.删除链表的倒数第 N 个结点
      • 双指针
    • leetcode 707.设计链表
      • 单链表操作
      • 双链表操作
    • leetcode 206.反转链表
      • 思路:
      • 双指针法
      • 递归法
    • leetcode 92.反转链表 II
      • 1.递归
      • 2.迭代(双指针头插法)
    • leetcode 25.K 个一组翻转链表
      • 1.递归
      • 2.迭代
    • leetcode 234.回文链表
      • 思路
      • 优化空间复杂度
    • leetcode 24.两两交换链表中的节点
      • 思路
      • 虚拟头节点迭代
      • 递归
    • leetcode 21.合并两个有序链表
    • leetcode 23.合并K个升序链表
      • 1.顺序合并
      • 2.分治合并
      • 3.优先级队列
    • leetcode 876.链表的中间结点
      • 常规思路
      • 双指针思路
    • leetcode 141.环形链表(判断链表是否有环)
      • 双指针
    • leetcode 142.环形链表 II(找链表的环入口)
      • 双指针
    • 相交链表
      • 思路1
      • 思路2
    • leetcode 146.LRU 缓存

leetcode 203.移除链表元素

leetcode 203.移除链表元素

删除链表中等于给定值 val 的所有节点,并返回新的头节点。

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

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

示例 3:
输入:head = [7,7,7,7], val = 7
输出:[]

删除非头节点让节点next指针直接指向下下一个节点

移除头结点和移除其他节点的操作是不一样的,因为链表的其他节点都是通过前一个节点来移除当前节点,而头结点没有前一个节点。将头结点向后移动一位就可以从链表中移除一个头结点

当我们遍历到链表的最后一个节点时,cur.next 为空节点,如果不加以判断,访问cur.next 对应的元素会产生运行错误。因此我们只需要遍历到链表的最后一个节点,而不需要遍历完整个链表

1.直接用原来的链表进行删除操作

需要单独写一段逻辑来处理移除头结点的情况

迭代写法:
时间复杂度:O(n),其中 n 是链表的长度。需要遍历链表一次。

空间复杂度:O(1)

class Solution {
    public ListNode removeElements(ListNode head, int val) {
        while (head!=null&&head.val==val) {//删除头节点
            head = head.next;
        }       
        ListNode temp = head;//删除非头节点
        while (temp!=null&&temp.next!=null) {
            if (temp.next.val==val) {
                temp.next = temp.next.next;
            } else {
                temp = temp.next;
            }
        }
        return head;
    }
}

递归写法:
时间复杂度:O(n),其中 n 是链表的长度。递归过程中需要遍历链表一次。

空间复杂度:O(n),其中 n 是链表的长度。空间复杂度主要取决于递归调用栈,最多不会超过 n 层。

class Solution {
    public ListNode removeElements(ListNode head, int val) {
        if (head == null) {
            return head;
        }
        head.next = removeElements(head.next, val);
        return head.val == val ? head.next : head;
    }
}

2.利用虚拟头结点进行删除操作

以一种统一的逻辑来移除链表的节点
数据结构——链表相关题目_第1张图片

给链表添加一个虚拟头结点为新的头结点,此时要移除这个旧头结点元素1,就可以使用和移除链表其他节点的方式统一
dummyNode->next是移除后的新头节点

class Solution {
    public ListNode removeElements(ListNode head, int val) {
        if (head == null) {
            return head;
        }
        //设置虚节点,统一操作
        ListNode dummy = new ListNode(-1,head);
        ListNode pre = dummy;
        ListNode cur = head;
        while (cur!=null) {
            if (cur.val==val) {
                pre.next = cur.next;
            } else {
                pre = cur;
            }
            cur = cur.next;
        }
        return dummy.next;
    }
}

leetcode 83.删除排序链表中的重复元素

leetcode 83.删除排序链表中的重复元素

给定一个已排序的链表的头 head , 删除所有重复的元素,使每个元素只出现一次 。返回 已排序的链表 。

思路

给定的链表是排好序的,因此重复的元素在链表中出现的位置是连续的,因此我们只需要对链表进行一次遍历,就可以删除重复的元素

当我们遍历到链表的最后一个节点时,cur.next 为空节点,如果不加以判断,访问cur.next 对应的元素会产生运行错误。因此我们只需要遍历到链表的最后一个节点,而不需要遍历完整个链表

时间复杂度: O ( n ) O(n) O(n) ,n是链表的长度

class Solution {
    public ListNode deleteDuplicates(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;
    }
}

leetcode 19.删除链表的倒数第 N 个结点

leetcode 19.删除链表的倒数第 N 个结点
给你一个链表,删除链表的倒数第 n 个结点,并且返回链表的头结点。

进阶:你能尝试使用一趟扫描实现吗?

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

双指针

双指针的经典应用,如果要删除倒数第n个节点,让fast移动n步,然后让fast和slow同时移动,直到fast指向链表末尾。删掉slow所指向的节点就可以了。

定义fast指针和slow指针,初始值为虚拟头结点,如图:
数据结构——链表相关题目_第2张图片

fast首先走n + 1步 ,为什么是n+1呢,因为只有这样同时移动的时候slow才能指向删除节点的上一个节点(方便做删除操作),如图:
数据结构——链表相关题目_第3张图片

fast和slow同时移动,直到fast指向末尾,如题:
数据结构——链表相关题目_第4张图片

删除slow指向的下一个节点,如图:
数据结构——链表相关题目_第5张图片
时间复杂度: O ( n ) O(n) O(n) ,n是链表的长度

class Solution {
    public ListNode removeNthFromEnd(ListNode head, int n) {
        ListNode dummyHead = new ListNode(0);
        dummyHead.next = head;
        ListNode fast = dummyHead,slow = dummyHead;
        while (n--!=0) {
            fast = fast.next;
        }
        fast = fast.next; // fast再提前走一步,因为需要让slow指向删除节点的上一个节点
        while(fast!=null) {
            fast = fast.next;
            slow = slow.next;
        }
        slow.next = slow.next.next;//删除操作
        return dummyHead.next;
    }
}

leetcode 707.设计链表

leetcode 707.设计链表

单链表中的节点应该具有两个属性:val 和 next。val 是当前节点的值,next 是指向下一个节点的指针/引用。如果要使用双向链表,则还需要一个属性 prev 以指示链表中的上一个节点。假设链表中的所有节点都是 0-index 的

在链表类中实现这些功能:

  • get(index):获取链表中第 index 个节点的值。如果索引无效,则返回-1。
  • addAtHead(val):在链表的第一个元素之前添加一个值为 val 的节点。插入后,新节点将成为链表的第一个节点。
  • addAtTail(val):将值为 val 的节点追加到链表的最后一个元素。
  • addAtIndex(index,val):在链表中的第 index 个节点之前添加值为 val 的节点。如果 index 等于链表的长度,则该节点将附加到链表的末尾。如果 index 大于链表长度,则不会插入节点。如果index小于0,则在头部插入节点。
  • deleteAtIndex(index):如果索引 index 有效,则删除链表中的第 index 个节点。
    数据结构——链表相关题目_第6张图片
    使用虚拟头节点更加方便

设计链表的五个接口:

  • 获取链表第index个节点的数值
  • 在链表的最前面插入一个节点
  • 在链表的最后面插入一个节点
  • 在链表第index个节点前面插入一个节点
  • 删除链表的第index个节点

单链表操作

class ListNode { //单链表
    int val;
    ListNode next;
    ListNode() {}
    ListNode(int val) {
        this.val = val;
    }
}

class MyLinkedList {
    int size;
    ListNode dummyHead;//虚拟头节点
    public MyLinkedList() {
        size = 0;
        dummyHead = new ListNode(0);
    }
    
    public int get(int index) { //index从0开始,0表示头节点
        if (index<0||index>=size) {
            return -1;
        }
        ListNode cur = dummyHead;
        for (int i=0;i<=index;i++) {//包含一个虚拟头节点,所以查找第index+1个
            cur = cur.next;
        }
        return cur.val;
    }
    //在链表最前面插入一个节点
    public void addAtHead(int val) {
        addAtIndex(0,val);
    }
    //在链表的最后插入一个节点
    public void addAtTail(int val) {
        addAtIndex(size,val);
    }
    // 在第 index 个节点之前插入一个新节点,例如index为0,那么新插入的节点为链表的新头节点。
    // 如果 index 等于链表的长度,则说明是新插入的节点为链表的尾结点
    // 如果 index 大于链表的长度,则返回空
    public void addAtIndex(int index, int val) {
        if (index>size) {
            return;
        }
        if (index<0) {
            index = 0;
        }
        size++;
        //找到要插入节点的前驱
        ListNode pre = dummyHead;
        for (int i=0;i<index;i++) {
            pre = pre.next;
        }
        ListNode toAdd = new ListNode(val);
        toAdd.next = pre.next;
        pre.next = toAdd;
    }
    
    public void deleteAtIndex(int index) {
        if (index<0||index>=size) {
            return;
        }
        size--;
        ListNode pre = dummyHead;
        for (int i=0;i<index;i++) {
            pre = pre.next;
        }
        pre.next = pre.next.next;
    }
}

双链表操作

class ListNode { //双链表
    int val;
    ListNode next,prev;
    ListNode(int val) {
        this.val = val;
    }
}
class MyLinkedList {
    int size;
    ListNode dummyHead,tail;//虚拟头节点
    public MyLinkedList() {
        size = 0;
        dummyHead = new ListNode(0);
        tail = new ListNode(0);
        dummyHead.next = tail;
        tail.prev = dummyHead;
    }
    
    public int get(int index) { 
        if (index<0||index>=size) {
            return -1;
        }
        ListNode cur = dummyHead;
        // 通过判断 index < (size - 1) / 2 来决定是从头结点还是尾节点遍历,提高效率
        if(index < (size - 1) / 2){
            for(int i = 0; i <= index; i++){
                cur = cur.next;
            }            
        }else{
            cur = tail;
            for(int i = 0; i <= size - index - 1; i++){
                cur = cur.prev;
            }
        }
        return cur.val;
    }
    //在链表最前面插入一个节点
    public void addAtHead(int val) {
        addAtIndex(0,val);
    }
    //在链表的最后插入一个节点
    public void addAtTail(int val) {
        addAtIndex(size,val);
    }
    // 在第 index 个节点之前插入一个新节点,例如index为0,那么新插入的节点为链表的新头节点。
    // 如果 index 等于链表的长度,则说明是新插入的节点为链表的尾结点
    // 如果 index 大于链表的长度,则返回空
    public void addAtIndex(int index, int val) {
        if (index>size) {
            return;
        }
        if (index<0) {
            index = 0;
        }
        size++;
        //找到要插入节点的前驱
        ListNode cur = dummyHead;
        for (int i=0;i<index;i++) {
            cur = cur.next;
        }
        ListNode toAdd = new ListNode(val);
        toAdd.next = cur.next;
        cur.next.prev = toAdd;
        toAdd.prev = cur;
        cur.next = toAdd;
    }  
    public void deleteAtIndex(int index) {
        if (index<0||index>=size) {
            return;
        }
        size--;
        ListNode pre = dummyHead;
        for (int i=0;i<index;i++) {
            pre = pre.next;
        }
        pre.next.next.prev = pre;
        pre.next = pre.next.next;
    }
}

插入头尾节点可单独写:

public void addAtHead(int val) {
    ListNode cur = head;
    ListNode newNode = new ListNode(val);
    newNode.next = cur.next;
    cur.next.prev = newNode;
    cur.next = newNode;
    newNode.prev = cur;
    size++;
}
public void addAtTail(int val) {
    ListNode cur = tail;
    ListNode newNode = new ListNode(val);
    newNode.next = tail;
    newNode.prev = cur.prev;
    cur.prev.next = newNode;
    cur.prev = newNode;
    size++;
}

leetcode 206.反转链表

leetcode 206.反转链表

反转一个单链表。

示例: 输入: 1->2->3->4->5->NULL 输出: 5->4->3->2->1->NULL

思路:

如果再定义一个新的链表,实现链表元素的反转,其实这是对内存空间的浪费。

其实只需要改变链表的next指针的指向,直接将链表反转 ,而不用重新定义一个新的链表,如图所示:

定义一个cur指针,指向头结点,再定义一个pre指针,初始化为null。

然后就要开始反转了,首先要把 cur->next 节点用temp指针保存,因为接下来要改变 cur->next 的指向了,将cur->next 指向pre ,此时已经反转了第一个节点了。

继续移动pre和cur指针。最后,cur 指针已经指向了null,循环结束,链表也反转完毕了。 此时return pre指针就可以了,pre指针就指向了新的头结点。

双指针法

时间复杂度: O ( n ) O(n) O(n) ,n是链表的长度

class Solution {
    public ListNode reverseList(ListNode head) {
        ListNode pre = null;
        ListNode cur = head;
        while (cur!=null) {
            ListNode temp = cur.next;
            cur.next = pre;//反转操作
            pre = cur;//更新pre和cur
            cur = temp;
        }
        return pre;
    }
}

递归法

时间复杂度: O ( n ) O(n) O(n) ,n是链表的长度
空间复杂度: O ( n ) O(n) O(n)
从前往后翻转指针指向:

class Solution {
    public ListNode reverse(ListNode pre,ListNode cur) {
        if (cur == null) return pre;
        ListNode temp = cur.next;
        cur.next = pre;
        //如下递归的写法,其实就是做了这两步
        // pre = cur;
        // cur = temp;
        return reverse(cur,temp);
    }
    public ListNode reverseList(ListNode head) {
        //初始化pre=null,cur=head
        return reverse(null,head);
    }
}

上面的递归写法和双指针法实质上都是从前往后翻转指针指向,还有另外一种与双指针法不同思路的递归写法:从后往前翻转指针指向

class Solution {
    public ListNode reverseList(ListNode head) {
        // 边缘条件判断 base case
        if (head == null) return null;
        if (head.next == null) return head;
        // 递归调用,翻转第二个节点开始往后的链表
        ListNode last = reverseList(head.next);
        // 翻转头节点与第二个节点的指向
        head.next.next = head;
        // 此时的 head 节点为尾节点,next 需要指向 NULL
        head.next = null;
        return last;
    }
}

leetcode 92.反转链表 II

leetcode 92.反转链表 II

给你单链表的头指针 head 和两个整数 left 和 right ,其中 left <= right 。请你反转从位置 left 到位置 right 的链表节点,返回反转后的链表 。
数据结构——链表相关题目_第7张图片

1.递归

先实现反转链表前 n 个节点
数据结构——链表相关题目_第8张图片

ListNode successor = null; // 后驱节点
// 反转以 head 为起点的 n 个节点,返回新的头结点
ListNode reverseN(ListNode head, int n) {
    if (n == 1) {
        // 记录第 n + 1 个节点
        successor = head.next;
        return head;
    }
    // 以 head.next 为起点,需要反转前 n - 1 个节点
    ListNode last = reverseN(head.next, n - 1);
	//反转指针
    head.next.next = head;
    // 让反转之后的 head 节点和后面的节点连起来
    head.next = successor;
    return last;
}

与完全反转的区别:

  1. base case 变为 n == 1,反转一个元素,就是它本身,同时要记录后驱节点。
  2. 直接把 head.next 设置为 null,因为整个链表反转后原来的 head 变成了整个链表的最后一个节点。但现在 head 节点在递归反转之后不一定是最后一个节点了,所以要记录后驱 successor(第 n + 1 个节点),反转之后将 head 连接上。

递归反转链表的一部分:
给一个索引区间 [m,n](索引从 1 开始),仅仅反转区间中的链表元素

ListNode reverseBetween(ListNode head, int m, int n) {
    // base case
    if (m == 1) {
        return reverseN(head, n);
    }
    // 前进到反转的起点触发 base case
    head.next = reverseBetween(head.next, m - 1, n - 1);
    return head;
}

2.迭代(双指针头插法)

  1. 定义两个指针,分别称之为 g(guard 守卫) 和 p(point)。
    根据参数 m 确定 g 和 p 的位置。将 g 移动到第一个要反转的节点的前面,将 p 移动到第一个要反转的节点的位置上。以 m=2,n=4为例。
  2. 将 p 后面的元素删除,然后添加到 g 的后面。即头插法。
  3. 根据 m 和 n 重复步骤(2)
  4. 返回 dummyHead.next
    数据结构——链表相关题目_第9张图片
    数据结构——链表相关题目_第10张图片
class Solution {
    public ListNode reverseBetween(ListNode head, int m, int n) {
        ListNode dummyHead = new ListNode(0);
        dummyHead.next = head;
        // 初始化指针
        ListNode g = dummyHead;
        ListNode p = dummyHead.next;
        // 将指针移到相应的位置
        for(int i = 0; i < m - 1; i++) {
            g = g.next; p = p.next;
        }
        // 头插法插入节点
        for (int j = 0; j < n - m; j++) {
            ListNode removed = p.next;
            p.next = p.next.next;
            removed.next = g.next;
            g.next = removed;
        }
        return dummyHead.next;
    }
}

leetcode 25.K 个一组翻转链表

leetcode 25.K 个一组翻转链表

给你一个链表,每 k 个节点一组进行翻转,请你返回翻转后的链表。

k 是一个正整数,它的值小于或等于链表的长度。

如果节点总数不是 k 的整数倍,那么请将最后剩余的节点保持原有顺序。

进阶:

  • 你可以设计一个只使用常数额外空间的算法来解决此问题吗?
  • 你不能只是单纯的改变节点内部的值,而是需要实际进行节点交换。

1.递归

1、先反转以 head 开头的 k 个元素。
2、将第 k + 1 个元素作为 head 递归调用 reverseKGroup 函数。
3、将上述两个过程的结果连接起来。

class Solution {
    public ListNode reverseKGroup(ListNode head, int k) {
        ListNode left = head,right = head;
        for (int i=0;i<k;i++) {
         // 不足 k 个,不需要反转,base case
            if (right==null) return head;
            right = right.next;
        }
        // 反转前 k 个元素
        ListNode newHead = reverse(left,right);
        // 递归反转后续链表并连接起来
        left.next = reverseKGroup(right,k);
        return newHead;
    }
    // 迭代反转区间 [left, right) 的元素,注意是左闭右开 
    ListNode reverse(ListNode left,ListNode right) {
        ListNode pre=null,cur = left;
        ListNode next;
        while (cur!=right) {
            next = cur.next;
            cur.next = pre;
            pre = cur;
            cur = next;
        }
        return pre;//返回反转后的头节点
    }
}

2.迭代

class Solution {
    public ListNode reverseKGroup(ListNode head, int k) {
        ListNode dummy=new ListNode(0);
        dummy.next=head;
        //pre指每次要翻转的链表的头结点的上一个节点。end指每次要翻转的链表的尾节点
        ListNode pre=dummy;
        ListNode end=dummy;
        while(end.next!=null){
            //循环k次,找到需要翻转的链表的结尾,这里每次循环要判断end是否等于空,因为如果为空,end.next会报空指针异常。
            for(int i=0;i<k&&end != null;i++){
                end=end.next;
            }
            //如果end==null,即需要翻转的链表的节点数小于k,不执行翻转。
            if(end==null){
                break;
            }
            //先记录下end.next,方便后面链接链表
            ListNode next=end.next;
            //然后断开链表
            end.next=null;
            //记录下要翻转链表的头节点
            ListNode start=pre.next;
            //翻转链表,pre.next指向翻转后的链表。
            pre.next=reverse(start);
            //翻转后头节点变到最后。通过.next把断开的链表重新链接。
            start.next=next;
            //将pre换成下次要翻转的链表的头结点的上一个节点。即start
            pre=start;
            //翻转结束,将end置为下次要翻转的链表的头结点的上一个节点。即start
            end=start;
        }
        return dummy.next;
    }
    private ListNode reverse(ListNode head) {
        ListNode pre = null;
        ListNode curr = head;
        while (curr != null) {
            ListNode next = curr.next;
            curr.next = pre;
            pre = curr;
            curr = next;
        }
        return pre;
    }
}

leetcode 234.回文链表

leetcode 234.回文链表

给你一个单链表的头节点 head ,请你判断该链表是否为回文链表。如果是,返回 true ;否则,返回 false 。

思路

寻找回文串的核心思想是从中心向两端扩展

判断一个字符串是不是回文串简单很多,不需要考虑奇偶情况,只需要用双指针,从两端向中间逼近即可。

但单链表无法倒着遍历,无法使用双指针技巧。

  1. 最简单的办法就是,把原始链表反转存入一条新的链表,然后比较这两条链表是否相同

  2. 借助二叉树后序遍历的思路,不需要显式反转原始链表也可以倒序遍历链表
    链表兼具递归结构,树结构不过是链表的衍生。那么,链表其实也可以有前序遍历和后序遍历

void traverse(ListNode head) {
    // 前序遍历代码位置
    traverse(head.next);
    // 后序遍历代码位置
}

如果想正序打印链表中的 val 值,可以在前序遍历位置写代码;反之,如果想倒序遍历链表,就可以在后序遍历位置操作

模仿双指针实现回文遍历:
时间复杂度: O ( n ) O(n) O(n),n为链表长度
空间复杂度: O ( n ) O(n) O(n)

class Solution {
    ListNode left;
    public boolean isPalindrome(ListNode head) {
        left = head;
        return traverse(head);
    }
    boolean traverse(ListNode right) {
        if (right == null) return true;
        boolean res = traverse(right.next);
        res = res && (right.val==left.val);
        left = left.next;
        return res;
    }
}

核心逻辑实际上就是把链表节点放入一个栈,然后再拿出来,这时候元素顺序就是反的,只不过我们利用的是递归函数的堆栈而已

优化空间复杂度

1、先通过 双指针技巧 中的快慢指针来找到链表的中点
2、如果fast指针没有指向null,说明链表长度为奇数,slow还要再前进一步
3、从slow开始反转后面的链表,就可以开始比较回文串了

空间复杂度: O ( 1 ) O(1) O(1)

class Solution {
    public boolean isPalindrome(ListNode head) {
        ListNode slow = head,fast = head;
        while (fast!=null&&fast.next!=null) {
            slow = slow.next;//用双指针找到链表中点
            fast = fast.next.next;
        }
        //若fast没有指向null,说明链表长度为奇数,slow还要再前进一步
        if (fast!=null) 
            slow = slow.next;
        //反转slow后的链表
        ListNode left = head;
        ListNode right = reverse(slow);
        //开始比较
        while(right!=null) {
            if (right.val!=left.val) {
                return false;
            }
            left = left.next;
            right = right.next;
        }
        return true;
    }
    //反转完整链表的方法
    ListNode reverse(ListNode head) {
        ListNode pre = null,cur = head;
        while(cur!=null) {
            ListNode next = cur.next;
            cur.next = pre;
            pre = cur;
            cur = next;
        }
        return pre;
    }
}

leetcode 24.两两交换链表中的节点

leetcode 24.两两交换链表中的节点

给定一个链表,两两交换其中相邻的节点,并返回交换后的链表。

你不能只是单纯的改变节点内部的值,而是需要实际的进行节点交换。
数据结构——链表相关题目_第11张图片

思路

画图分析,注意操作的先后顺序,用临时节点temp保存后续有用但在之后操作会被覆盖的信息
数据结构——链表相关题目_第12张图片

虚拟头节点迭代

时间复杂度: O ( n ) O(n) O(n) ,n是链表的长度

class Solution {
    public ListNode swapPairs(ListNode head) {
        ListNode dummyHead = new ListNode(0);
        dummyHead.next = head;
        ListNode cur = dummyHead;
        while (cur.next!=null&&cur.next.next!=null) {
            ListNode temp = cur.next;// 记录临时节点
            ListNode temp1 = cur.next.next.next;// 记录临时节点

            cur.next = cur.next.next;// 步骤一
            cur.next.next = temp;// 步骤二
            cur.next.next.next = temp1;// 步骤三

            cur = cur.next.next;// cur移动两位,准备下一轮交换
            
            //while循环内另一种写法,原理相同
            /*
            ListNode temp = head.next.next; 
      		cur.next = head.next;   // 步骤一       
      		head.next.next = head;  // 步骤二        
      		head.next = temp;       // 步骤三       
      		cur = head;                    // 步进1位
      		head = head.next;               // 步进1位*/
        }
        return dummyHead.next;
    }
}

递归

时间复杂度: O ( n ) O(n) O(n) ,n是链表的长度
空间复杂度: O ( n ) O(n) O(n)

class Solution {
    public ListNode swapPairs(ListNode head) {
        if(head == null || head.next == null) return head;
        // 获取当前节点的下一个节点
        ListNode cur = head.next;
        // 进行递归
        ListNode newNode = swapPairs(cur.next);
        // 这里进行交换
        cur.next = head;
        head.next = newNode;
        return cur;
    }
}

leetcode 21.合并两个有序链表

leetcode 21.合并两个有序链表

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

示例:
输入:l1 = [1,2,4], l2 = [1,3,4]
输出:[1,1,2,3,4,4]

解法:使用虚拟头节点

class Solution {
    public ListNode mergeTwoLists(ListNode list1, ListNode list2) {
        ListNode dummy = new ListNode(-1),p = dummy;//虚拟头节点
        ListNode cur1 = list1,cur2 = list2;
        while (cur1!=null&&cur2!=null) {
        //while循环内比较cur1,cur2值大小,小的接入p内
            if (cur1.val > cur2.val) {
                p.next = cur2;
                cur2 = cur2.next;
            } else {
                p.next = cur1;
                cur1 = cur1.next;
            }
            p = p.next;
        }
        //将链表中剩余元素全部并入p
        if (cur1!=null) p.next = cur1; 
        if (cur2!=null) p.next = cur2;
        return dummy.next;
    }   
}

leetcode 23.合并K个升序链表

leetcode 23.合并K个升序链表
数据结构——链表相关题目_第13张图片

1.顺序合并

用一个变量 ans 来维护以及合并的链表,第 i 次循环把第 i 个链表和 ans 合并,答案保存到 ans 中。实现了把k个有序链表的合并化为每次两个有序链表的合并,合并多次。

时间复杂度: O ( k 2 n ) O(k^2n) O(k2n)
假设每个链表的最长长度是 n。在第一次合并后,ans 的长度为 n;第二次合并后,ans 的长度为 2×n,第 i 次合并后,ans 的长度为 i×n。第 i 次合并的时间代价是 O(n+(i−1)×n)=O(i×n),那么总的时间代价为
O(∑ki=1(i×n)) = O ( ( 1 + k ) ⋅ k / 2 × n ) O( (1+k)⋅k/2 ×n) O((1+k)k/2×n)= O ( k 2 n ) O(k^2n) O(k2n)
空间复杂度: O ( 1 ) O(1) O(1)

class Solution {
    public ListNode mergeKLists(ListNode[] lists) {
        ListNode ans = null;
        for (int i=0;i<lists.length;i++) {
            ans = mergeTwoLists(ans,lists[i]);
        }
        return ans;
    }
    public ListNode mergeTwoLists(ListNode list1, ListNode list2) {
        //if (list1 == null || list2 == null) {
        //    return list1 != null ? list1 : list2;
        //} //这一段可有可无
        ListNode dummy = new ListNode(-1),p = dummy;
        ListNode cur1 = list1,cur2 = list2;
        while (cur1!=null&&cur2!=null) {
            if (cur1.val > cur2.val) {
                p.next = cur2;
                cur2 = cur2.next;
            } else {
                p.next = cur1;
                cur1 = cur1.next;
            }
            p = p.next;
        }
        if (cur1!=null) p.next = cur1; 
        if (cur2!=null) p.next = cur2;
        return dummy.next;
    }   
}

2.分治合并

3.优先级队列

合并 k 个有序链表的逻辑类似合并两个有序链表,难点在于,如何快速得到 k 个节点中的最小节点,接到结果链表上

这里我们就要用到 优先级队列(二叉堆) 这种数据结构,把链表节点放入一个最小堆,就可以每次获得 k 个节点中的最小节点

leetcode 876.链表的中间结点

leetcode 876.链表的中间结点

给定一个头结点为 head 的非空单链表,返回链表的中间结点。

如果有两个中间结点,则返回第二个中间结点。

常规思路

无法直接得到单链表的长度 n,常规方法就是先遍历链表计算 n,再遍历一次得到第 n / 2 个节点,也就是中间节点。

双指针思路

让两个指针 slow 和 fast 分别指向链表头结点 head。

每当慢指针 slow 前进一步,快指针 fast 就前进两步,这样,当 fast 走到链表末尾时,slow 就指向了链表中点

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

leetcode 141.环形链表(判断链表是否有环)

leetcode 141.环形链表

给你一个链表的头节点 head ,判断链表中是否有环。
如果链表中存在环 ,则返回 true 。 否则,返回 false 。

双指针

每当慢指针 slow 前进一步,快指针 fast 就前进两步。

如果 fast 最终遇到空指针,说明链表中没有环;如果 fast 最终和 slow 相遇,那肯定是 fast 超过了 slow 一圈,说明链表中含有环。

public class Solution {
    public boolean hasCycle(ListNode head) {
        ListNode slow = head,fast = head;
        while(fast!=null && fast.next!=null) {
            slow = slow.next;
            fast = fast.next.next;
            if (slow==fast) {
                return true;
            }
        }
        return false;
    }
}

leetcode 142.环形链表 II(找链表的环入口)

leetcode 142.环形链表 II

题意: 给定一个链表,返回链表开始入环的第一个节点。 如果链表无环,则返回 null。

为了表示给定链表中的环,使用整数 pos 来表示链表尾连接到链表中的位置(索引从 0 开始)。 如果 pos 是 -1,则在该链表中没有环。

说明:不允许修改给定的链表。

双指针

  1. 判断链表是否有环
  2. 如果有环,找链表的环入口

假设从头结点到环形入口节点的节点数为x。 环形入口节点到 fast 指针与slow指针相遇节点节点数为y。 从相遇节点再到环形入口节点节点数为 z。 如图所示:
数据结构——链表相关题目_第14张图片
相遇时: slow指针走过的节点数为: x + y, fast指针走过的节点数:x + y + n (y + z),n为fast指针在环内走了n圈才遇到slow指针,(y+z)为 一圈内节点的个数A。
因为fast指针是一步走两个节点,slow指针一步走一个节点, 所以 fast指针走过的节点数 = slow指针走过的节点数 * 2,化简得x + y = n (y + z)

因为要找环形的入口,那么要求的是x(头结点到环形入口节点的的距离)
x = n (y + z) - y
再从n(y+z)中提出一个(y+z)来,整理公式之后为如下公式:
x = (n - 1) (y + z) + z
注意这里n一定是大于等于1的,因为 fast指针至少要多走一圈才能相遇slow指针。

当 n为1的时候,意味着fast指针在环形里转了一圈之后,就遇到了 slow指针了,公式就化解为 x = z

结论:从头结点出发一个指针,从相遇节点也出发一个指针,这两个指针每次只走一个节点,那么当这两个指针相遇的时候就是环形入口的节点

n如果大于1,就是fast指针在环形转n圈之后才遇到 slow指针。这种情况和n为1的时候效果是一样的,一样可以通过这个方法找到环形的入口节点,只不过,index1 指针在环里多转了(n-1)圈,然后再遇到index2,相遇点依然是环形的入口节点。

public class Solution {
    public ListNode detectCycle(ListNode head) {
        ListNode slow = head,fast = head;
        while (fast!=null&&fast.next!=null) {
            slow = slow.next;
            fast = fast.next.next;
            if (fast==slow) break;
        }
        //fast遇空指针,说明无环
        if(fast==null||fast.next==null) {
            return null;
        }
        slow = head;
        while (slow!=fast) {
            slow = slow.next;
            fast = fast.next;
        }
        return slow;
    }
}

相交链表

leetcode 160.相交链表
leetcode 面试题 02.07.链表相交

给你两个单链表的头节点 headA 和 headB ,请你找出并返回两个单链表相交的起始节点。如果两个链表没有交点,返回 null 。

图示两个链表在节点 c1 开始相交:
数据结构——链表相关题目_第15张图片

题目数据 保证 整个链式结构中不存在环。
注意,函数返回结果后,链表必须 保持其原始结构 。

示例 1:
数据结构——链表相关题目_第16张图片
求两个链表交点节点的指针。 交点不是数值相等,而是指针相等。(引用完全相同,即:内存地址完全相同的交点)
难点在于,由于两条链表的长度可能不同,两条链表之间的节点无法对应

如果用两个指针 p1 和 p2 分别在两条链表上前进,并不能同时走到公共节点,也就无法得到相交节点 c1。

解决这个问题的关键是,通过某些方式,让 p1 和 p2 能够同时到达相交节点 c1

思路1

因为链表自交点后都相等,所以如果有交点则有公共尾部
所以求出两个链表的长度,并求出两个链表长度的差值,然后让curA移动到,和curB 末尾对齐的位置
此时我们就可以比较curA和curB是否相同,如果不相同,同时向后移动curA和curB,如果遇到curA == curB,则找到交点。否则循环退出返回空指针。

时间复杂度: O ( m + n ) O(m + n) O(m+n),m,n分别为两链表长度

public class Solution {
    public ListNode getIntersectionNode(ListNode headA, ListNode headB) {
        ListNode curA = headA;
        ListNode curB = headB;
        int lenA = 0,lenB = 0;
        while(curA!=null) {// 求链表A的长度
            lenA++;
            curA = curA.next;
        }
        while(curB!=null) {// 求链表B的长度
            lenB++;
            curB = curB.next;
        }
        curA = headA;//求长度时改变了curA,curB
        curB = headB;//此时要重新改回
        // 保证curA为最长链表的头,lenA为其长度
        if (lenB > lenA) {
            int temp = lenA;
            lenA = lenB;
            lenB = temp;
            ListNode tmp = curA;
            curA = curB;
            curB = tmp;
        }
        int sub = lenA - lenB;// 求长度差
        // 让curA和curB在同一起点上(末尾位置对齐)
        while (sub-- > 0) {
            curA = curA.next;
        }
        // 遍历curA 和 curB,遇到相同则直接返回
        while (curA!=null) {
            if (curA==curB) {
                return curA;
            }
            curA = curA.next;
            curB = curB.next;
        }
        return null;
    }
}

思路2

当链表 headA 和 headB 都不为空时,创建两个指针pA 和 pB,初始时分别指向两个链表的头节点 headA 和headB,然后将两个指针依次遍历两个链表的每个节点。这样相当于逻辑上两条链表接在了一起,就可以让 pA 和 pB 同时到达相交节点 c1。

情况一:两个链表相交

链表headA 和headB 的长度分别是 m 和 n。假设链表headA 的不相交部分有 a 个节点,链表headB 的不相交部分有 b 个节点,两个链表相交的部分有 c 个节点,则有 a+c=m,b+c=n。

  • 如果 a=b,则两个指针会同时到达两个链表相交的节点,此时返回相交的节点;

  • 如果 a!=b,则指针 pA 会遍历完链表 headA,指针 pB 会遍历完链表 headB,两个指针不会同时到达链表的尾节点,然后指针 pA 移到链表 headB 的头节点,指针 pB 移到链表 headA 的头节点,然后两个指针继续移动,在指针 pA 移动了 a+c+b 次、指针pB 移动了 b+c+a 次之后,两个指针会同时到达两个链表相交的节点,该节点也是两个指针第一次同时指向的节点,此时返回相交的节点。

情况二:两个链表不相交

链表 headA 和headB 的长度分别是 m 和 n。

  • 如果 m=n,则两个指针会同时到达两个链表的尾节点,然后同时变成空值 null,此时返回 null;

  • 如果m!=n,则由于两个链表没有公共节点,两个指针也不会同时到达两个链表的尾节点,因此两个指针都会遍历完两个链表,在指针 pA 移动了 m+n 次、指针 pB 移动了n+m 次之后,两个指针会同时变成空值null,此时返回 null。

public class Solution {
    public ListNode getIntersectionNode(ListNode headA, ListNode headB) {
        if (headA == null || headB == null) {
            return null;
        }
        ListNode pA = headA, pB = headB;
        while (pA != pB) {
            pA = pA == null ? headB : pA.next;
            pB = pB == null ? headA : pB.next;
        }
        return pA;
    }
}

PS:如果把两条链表首尾相连,那么「寻找两条链表的交点」的问题就转换成了前面的「寻找环起点」的问题
数据结构——链表相关题目_第17张图片

leetcode 146.LRU 缓存

leetcode 146.LRU 缓存 (哈希表+双向链表实现)综合复杂问题

369

你可能感兴趣的:(数据结构与算法,笔记,链表,数据结构,算法,学习,后端)