【算法-链表】

链表基础

单链表

单链表

双链表

双链表
链表是由一个一个节点组成, 其相对于数组的优点是无需事先声明长度, 也不需要连续的存储地址, 以单链表为例:

public class ListNode {
    // 结点的值
    int val;

    // 下一个结点
    ListNode next;

    // 节点的构造函数(无参)
    public ListNode() {
    }

    // 节点的构造函数(有一个参数)
    public ListNode(int val) {
        this.val = val;
    }

    // 节点的构造函数(有两个参数)
    public ListNode(int val, ListNode next) {
        this.val = val;
        this.next = next;
    }
}

节点中包含两个字段, 节点所携带的值(可以是对象或其他任意类型), 和下一个节点的地址, 双节点则多了指向上一个节点的地址.

循环链表

循环链表

删除节点

删除节点

添加节点

添加节点

练习

203.移除链表元素

给你一个链表的头节点 head 和一个整数 val ,请你删除链表中所有满足 Node.val == 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
输出:[]

新增虚拟头节点移除元素

思路:

创建一个虚拟头节点, 作为原头节点的前置节点, 新建一个临时变量pre指向虚拟头节点.
再新建一个临时变量cur, 指向原头节点, 用于遍历链表.
则在遍历的过程中, 判断当前节点的值是否符合目标节点的值
若不符合, 则前置节点与当前节点一起后移, 遍历下一节点.
若相等, 则由前置节点直接指向当前节点的next 则在联表中已删除当前节点.
直至完成链表的遍历.
新增虚拟节点的好处是, 头节点无需特殊处理, 可以当作其他普通节点一样, 由前置节点定位, 判断.

代码实现:
/**
 * 移除聊表元素被
 */
public static ListNode removeElements(ListNode head, int val) {
    // 虚拟头节点
    ListNode dummy = new ListNode(-1, head);
    // 遍历链表当前节点
    ListNode cur = head;
    // 当前节点前置节点
    ListNode pre = dummy;
    // 遍历直至当前节点不为空
    while (cur != null) {
        // 当前节点值==目标值
        if (cur.val == val) {
            // 由前置节点直接指向当前节点的next 则在联表中已删除当前节点
            pre.next = cur.next;
        } else {
            // 若不相等 前置节点和当前节点一起后移
            pre = cur;
        }
        // 后移当前节点, 以实现遍历
        cur = cur.next;
    }
    return dummy.next;
}

707.设计链表

你可以选择使用单链表或者双链表,设计并实现自己的链表。

单链表中的节点应该具备两个属性:val 和 next 。val 是当前节点的值,next 是指向下一个节点的指针/引用。

如果是双向链表,则还需要属性 prev 以指示链表中的上一个节点。假设链表中的所有节点下标从 0 开始。

实现 MyLinkedList 类:

MyLinkedList() 初始化 MyLinkedList 对象。
int get(int index) 获取链表中下标为 index 的节点的值。如果下标无效,则返回 -1 。
void addAtHead(int val) 将一个值为 val 的节点插入到链表中第一个元素之前。在插入完成后,新节点会成为链表的第一个节点。
void addAtTail(int val) 将一个值为 val 的节点追加到链表中作为链表的最后一个元素。
void addAtIndex(int index, int val) 将一个值为 val 的节点插入到链表中下标为 index 的节点之前。如果 index 等于链表的长度,那么该节点会被追加到链表的末尾。如果 index 比长度更大,该节点将 不会插入 到链表中。
void deleteAtIndex(int index) 如果下标有效,则删除链表中下标为 index 的节点。

示例:
输入
["MyLinkedList", "addAtHead", "addAtTail", "addAtIndex", "get", "deleteAtIndex", "get"]
[[], [1], [3], [1, 2], [1], [1], [1]]
输出
[null, null, null, null, 2, null, 3]

解释
MyLinkedList myLinkedList = new MyLinkedList();
myLinkedList.addAtHead(1);
myLinkedList.addAtTail(3);
myLinkedList.addAtIndex(1, 2);    // 链表变为 1->2->3
myLinkedList.get(1);              // 返回 2
myLinkedList.deleteAtIndex(1);    // 现在,链表变为 1->3
myLinkedList.get(1);              // 返回 3
思路1: 单向链表

已知节点类信息为:

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

需要以此为基础设计一个链表, 实现,添加,删除,查找等方法.

  1. 我们以 MyLinkedList 作为链表类的名称, 在类中新增属性 dummy: 作为链表的虚拟头节点, 并非实际存储数据的节点. size: 维护一个链表(由0开始), 用以记录链表内节点的数量, 方便其他方法的实现.
class MyLinkedList {
    private final ListNode dummy;
    private int size;
    public MyLinkedList() {
        this.dummy = new ListNode(0);
        this.size = 0;
    }
    public int getSize() {
        return size;
    }
    // todo 添加... 删除... 查找...
}
  1. 添加节点方法 void addAtIndex(int index, int val)
/**
 * 将一个值为 val 的节点插入到链表中下标为 index 的节点之前。如果 index 等于链表的长度,那么该节点会被追加到链表的末尾。
 * 如果 index 比长度更大,该节点将 不会插入 到链表中。
 */
public void addAtIndex(int index, int val) {
    // 下标越界 无事发生
    if (index < 0 || index > size) {
        return;
    }
    // 前置节点
    ListNode pre = dummy;
    // 遍历值前置节点指向目标节点
    for (int i = 0; i < index; i++) {
        pre = pre.next;
    }
    // 新增节点
    ListNode add = new ListNode(val);
    // 新增节点指向原下标节点
    add.next = pre.next;
    // 前置节点指向新增节点
    pre.next = add;
    // 维护size
    size++;
}

void addAtHead(int val) 与 void addAtTail(int val) 可在该方法为基础上实现.

  1. 查找节点方法 int get(int index)
/**
 * 获取链表中下标为 index 的节点的值。如果下标无效,则返回 -1 。
 */
public int get(int index) {
    // 下标越界 返回 -1
    if (index < 0 || index >= size) {
        return -1;
    }
    // 当前遍历节点
    ListNode cur = dummy.next;
    // 遍历至目标下标
    for (int i = 0; i < index; i++) {
        cur = cur.next;
    }
    // 返回节点值
    return cur.val;
}
  1. 删除节点方法 void deleteAtIndex(int index)
/**
 * 如果下标有效,则删除链表中下标为 index 的节点。
 */
public void deleteAtIndex(int index) {
    // 下标越界 无事发生
    if (index < 0 || index >= size) {
        return;
    }
    // 当前节点前置节点
    ListNode pre = dummy;
    // 遍历至前置节点指向当前下标节点
    for (int i = 0; i < index; i++) {
        pre = pre.next;
    }
    // 前置节点.next.next = 当前节点.next
    pre.next = pre.next.next;
    // 维护size
    size--;
}
思路2: 双向链表

// todo…

206.反转链表

给你单链表的头节点 head ,请你反转链表,并返回反转后的链表。

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

206.反转链表

思路1:

使用双指针依次反指, 完成逆序.

代码实现:
 /**
  * 双指针法
  */
 public static ListNode reverseList(ListNode head) {
     // 当前节点
     ListNode cur = head;
     // 用以记录 cur的前节点
     ListNode pre = null;
     // 缓存节点 用以缓存 cur 要遍历的下一个节点
     ListNode temp;
     while (cur != null) {
         // 缓存 cur 要遍历的下一个节点
         temp = cur.next;
         // 当前节点指向前置节点 完成倒序的其中一步
         cur.next = pre;
         // 移动到当前节点位置 为下次倒序做准备
         pre = cur;
         // 当前节点指向缓存目标接待你
         cur = temp;
     }
     // cur 最终指向空 pre指向cur的前节点, 即最后一个节点
     return pre;
 }
思路递归:
代码实现:
/**
 * 递归
 */
public static ListNode reverseList(ListNode head) {
    return reverse(null, head);
}
private static ListNode reverse(ListNode prev, ListNode cur) {
    if (cur == null) {
        return prev;
    }
    ListNode temp = null;
    temp = cur.next;// 先保存下一个节点
    cur.next = prev;// 反转
    // 更新prev、cur位置
    // prev = cur;
    // cur = temp;
    return reverse(cur, temp);
}

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

给你一个链表,两两交换其中相邻的节点,并返回交换后链表的头节点。你必须在不修改节点内部的值的情况下完成本题(即,只能进行节点交换)。

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

与链表反转类似, 需要在循环中确定节点, 节点.next的指向

代码实现:
public ListNode swapPairs(ListNode head) {
    ListNode dummy = new ListNode(0, head);
    // 前置节点
    ListNode pre = dummy;
    // 当前节点
    ListNode cur = head;
    // 缓存节点
    ListNode temp;
    while (cur != null && cur.next != null) {
        // 缓存下一次的起始节点
        temp = cur.next.next;
        // 前置节点指向第二个节点 (此时cur.next已发生变化, 所以需要缓存)
        pre.next = cur.next;
        // 第二个节点指向当前节点
        pre.next.next = cur;
        // 当前节点再指向 下一次缓存 的节点
        cur.next = temp;
        // 节点后移
        pre = cur;
        cur = cur.next;
    }
    return dummy.next;
}

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步, 快慢指针再同时移动, 当快指针指向null时, 慢指针即指向目标指针, 此时既可以完成删除操作.

代码实现:
public static ListNode removeNthFromEnd(ListNode head, int n) {
    ListNode dummy = new ListNode(0, head);
    // 快指针先行出发n步
    ListNode fast = dummy;
    for (int i = 0; i < n; i++) {
        fast = fast.next;
    }
    // 快慢指针同时出发, 直至 快指针指向null
    // 则慢指针恰好指向目标节点
    ListNode slow = dummy;
    while (fast.next != null) {
        fast = fast.next;
        slow = slow.next;
    }
    // 删除目标节点
    slow.next = slow.next.next;
    
    return dummy.next;
}

面试题 02.07. 链表相交

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

题目数据 保证 整个链式结构中不存在环。

注意,函数返回结果后,链表必须 保持其原始结构 。

示例 1:
输入:intersectVal = 8, listA = [4,1,8,4,5], listB = [5,0,1,8,4,5], skipA = 2, skipB = 3
输出:Intersected at '8'
解释:相交节点的值为 8 (注意,如果两个链表相交则不能为 0)。
从各自的表头开始算起,链表 A 为 [4,1,8,4,5],链表 B 为 [5,0,1,8,4,5]。
在 A 中,相交节点前有 2 个节点;在 B 中,相交节点前有 3 个节点。
示例 2:
输入:intersectVal = 2, listA = [0,9,1,2,4], listB = [3,2,4], skipA = 3, skipB = 1
输出:Intersected at '2'
解释:相交节点的值为 2 (注意,如果两个链表相交则不能为 0)。
从各自的表头开始算起,链表 A 为 [0,9,1,2,4],链表 B 为 [3,2,4]。
在 A 中,相交节点前有 3 个节点;在 B 中,相交节点前有 1 个节点。
示例 3:
输入:intersectVal = 0, listA = [2,6,4], listB = [1,5], skipA = 3, skipB = 2
输出:null
解释:从各自的表头开始算起,链表 A 为 [2,6,4],链表 B 为 [1,5]。
由于这两个链表不相交,所以 intersectVal 必须为 0,而 skipA 和 skipB 可以是任意值。
这两个链表不相交,因此返回 null 。
思路:

分别求出两条链表的长度, 求出长度差, 将长的那条提前移动长度差的数值, 使得两条链表需要遍历的节点数相同, 即可开始同时遍历两条链表, 当两条链表存在相同节点时, 即可说明两条链表节点相交.

代码实现:
public static ListNode getIntersectionNode(ListNode headA, ListNode headB) {
   if (headA == null || headB == null) {
       return null;
   }
   // 获取链表长度差
   int gap = getLength(headA) - getLength(headB);
   // 根据长度差 使得长链表先行gap步
   if (gap > 0) {
       return getListNode(headA, headB, gap);
   } else {
       return getListNode(headB, headA, -gap);
   }
}
private static int getLength(ListNode head) {
   int length = 0;
   ListNode cur = head;
   while (cur.next != null) {
       length++;
       cur = cur.next;
   }
   return length;
}
private static ListNode getListNode(ListNode nodeLong, ListNode nodeShort, int gap) {
   // 长链表先走gap步
   ListNode curLong = nodeLong;
   for (int i = 0; i < gap; i++) {
       curLong = curLong.next;
   }
   // 长短链表一起出发
   ListNode curShort = nodeShort;
   while (curShort != null) {
       // 若有节点相同则链表相交
       if (curLong == curShort) {
           return curLong;
       }
       curLong = curLong.next;
       curShort = curShort.next;
   }
   return null;
}

142.环形链表II

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

如果链表中有某个节点,可以通过连续跟踪 next 指针再次到达,则链表中存在环。 为了表示给定链表中的环,评测系统内部使用整数 pos 来表示链表尾连接到链表中的位置(索引从 0 开始)。如果 pos 是 -1,则在该链表中没有环。注意:pos 不作为参数进行传递,仅仅是为了标识链表的实际情况。

不允许修改 链表。

示例 1:
输入:head = [3,2,0,-4], pos = 1
输出:返回索引为 1 的链表节点
解释:链表中有一个环,其尾部连接到第二个节点。
示例 2:
输入:head = [1,2], pos = 0
输出:返回索引为 0 的链表节点
解释:链表中有一个环,其尾部连接到第一个节点。
示例 3:
输入:head = [1], pos = -1
输出:返回 null
解释:链表中没有环。
思路:
2(x+y) = x + n(y+z) + y
x = (n-1)(y+z) + z

其中y为环上已走的距离, z为环上未走完的距离.
注:
x+y 为慢节点所走的路程 2(x+y) 即为快节点走完的路程. x+n(y+z)+y 为快节点所走的路程.

由 x = (n-1)(y+z) + z
可知, 快指针必定比慢指针多走一圈, 所以n>1.
又 由y出发走z则回到环形的原点, 再多走(n-1)(x+y)仍然回到原点.
则此时再派出一个点由原点出发, 另一个点由y点出发, 一行一步, 两点相遇时, 即为换的出发点.

代码实现:
/**
 * 2(x+y) = x + n(y+z) + y
 * x = (n-1)(y+z) + z
 * 其中y为环上已走的距离, z为环上未走完的距离
 * 注:
 * x+y 为慢节点所走的路程 2(x+y) 即为快节点走完的路程
 * x+n(y+z)+y 为快节点所走的路程
 * 由 x = (n-1)(y+z) + z
 * 可知, 快指针必定比慢指针多走一圈, 所以n>1
 * 又 由y出发走z则回到环形的原点, 再多走(n-1)(x+y)仍然回到原点
 * 则此时再派出一个点由原点出发, 另一个点由y点出发, 一行一步, 两点相遇时, 即为换的出发点
 */
public ListNode detectCycle(ListNode head) {
    // 快指针 每次走两步
    ListNode fast = head;
    // 慢指针 每次走一步
    ListNode slow = head;
    while (fast != null && fast.next != null) {
        fast = fast.next.next;
        slow = slow.next;
        // 若快慢指针相遇则说明存在环
        // 不相遇, 当快指针走到尽头时, 跳出循环, 返回null
        if (fast == slow) {
            // 存在环时, 再派出一个点由原点出发, 慢节点重新出发 一行一步
            ListNode start = head;
            while (start != fast) {
                start = start.next;
                slow = slow.next;
            }
            // 当两点相遇时, 即为环的起始点
            return slow;
        }
    }
    return null;
}
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值