算法笔记-链表

链表

翻转链表

206.反转链表
92. 反转链表 II
25. K 个一组翻转链表

Node定义

public class Node {
    // 链表
    Node head;
    int val;
    Node next;

    public Node() {
    }

    public Node(int val) {
        this.val = val;
    }

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

    public Node(int ... nodes) {
        if (nodes.length > 0) {
            head = new Node(nodes[0]);
            Node cur = head;
            for (int i = 1; i < nodes.length; i++) {
                cur = cur.next = new Node(nodes[i]);
            }
        }
    }

    public Node getHead() {
        return head;
    }

    public void setHead(Node head) {
        this.head = head;
    }

    public int getVal() {
        return val;
    }

    public void setVal(int val) {
        this.val = val;
    }

    public Node getNext() {
        return next;
    }

    public void setNext(Node next) {
        this.next = next;
    }
}


双指针

反转链表

:::info

  1. 假如链表是a,b,c,d
  2. 翻转后的链表a是不指向任何节点,所以注意把a–>b断开
  3. cur的存在是表示当前操作的结点,也就是如果cur是b,就表示把b的指针改变方向
  4. pre的存在是为了保存前一个节点,因为要改变b的指针方向,需要知道b将来应该指向谁,这里就是要指向pre
  5. next的存在是为了提前保存c节点,因为一旦b的节点改变了方向指向了a,不提前保存c,就永远无法找到c了,从而无法完成后面的翻转
  6. 操作完一个节点后,就操作下一个节点,但是前提是下一个节点不为空,所以while的终止条件就是next==null,因为此时next赋值给cur就没有操作的意义了
@Test
public void test0() throws Exception {
    Node head = new Node(1, 2, 3, 4, 5).getHead();

    if (head == null) {
        return;
    }
    Node pre = null;
    Node cur = head;
    Node next = cur.getNext();
    cur.setNext(null);

    while (next != null) {
        pre = cur;
        cur = next;
        next = next.getNext();
        cur.setNext(pre);
    }
    while (cur != null) {
        System.out.println(cur.getVal());
        cur = cur.getNext();
    }
}

反转链表 II

:::info
给你单链表的头指针 head 和两个整数 left 和 right ,其中 left <= right 。请你反转从位置 left 到位置 right 的链表节点,返回 反转后的链表 。

  1. 反转链表的基础上完成
  2. 添加一个i计数,标识cur的位置,cur到left索引指定的位置才会开始反转,到right索引为止停止反转
  3. 需要 preLast、midFirst,分别保存原链表 left-1 位置、left位置 的节点
  4. 注意边界问题,可以将边界问题转化成:执行多少次循环==修改多少次指向(也就是i < right ,而不是i <= right)
  5. 如果 left==1 表示从头开始反转,head就动了,就不能直接返回原链表的head
    :::
@Test
public void test1() throws Exception {
    Node head = new Node(1, 2, 3, 4, 5, 6).getHead();
    int left = 2, right = 4;


    if (head == null) {
        return;
    }
    Node pre = null;
    int i = 1;
    Node cur = head;
    Node next = cur.getNext();

    while (next != null && i < left) {
        pre = cur;
        cur = next;
        next = next.getNext();
        i++;
    }
    // 保存前一个链表的最后一个节点
    Node preLast = pre;
    // 保存待反转链表的第一个节点
    Node midFirst = cur;
    while (next != null && i < right) {
        pre = cur;
        cur = next;
        next = next.getNext();
        cur.setNext(pre);
        i++;
    } 
    if (null != preLast) {
        preLast.setNext(cur);
    }
    midFirst.setNext(next);

    // 注意这里,因为left==1 表示从头开始反转,head就动了
    if (left == 1){
        head = cur;
    }

    while (head != null) {
        System.out.println(head.getVal());
        head = head.getNext();
    }
}

K 个一组翻转链表

:::info
给你链表的头节点 head ,每 k 个节点一组进行翻转,请你返回修改后的链表。
k 是一个正整数,它的值小于或等于链表的长度。如果节点总数不是 k 的整数倍,那么请将最后剩余的节点保持原有顺序。
你不能只是单纯的改变节点内部的值,而是需要实际进行节点交换。

  1. 下面这是一个循环
    1. int i;计数cur走过的索引下标
      1. 达到k个就执行反转
      2. 不达到k个,指的是前面几组反转完成后,剩下的节点不足k个
    2. k个节点一组
      1. k个节点,断开这k个节点子链表的前后链表(其实只需要断开后面,因为前面的已经被上一组断开了)
      2. 反转这个子链表
      3. 声明resLast保存上一组子链表的最后一个节点,因为当前子链表反转完成后需要拼接到上一组后面
      4. 在链表断开前,声明nextFirst保存下一组子链表的第一个节点,因为还需要继续反转下一组
      5. i<k的时候,剩下的子链表不需要翻转,可以 把 nextFirst直接拼接到 resLast 后面即可
  2. 注意边界问题
    1. 外层循环 终止条件是 cur != null ,需要保证所有节点拼接到resLast后,cur赋值为null,否则无法结束循环(因为在最后一组k个节点反转循环中 设置了 cur = headTmp)
    2. 但是如果外层循环 终止条件是 cur.next != null ,就会导致 在 最后不足k个的节点是一个的时候,出现这个节点没有拼接到resLast的问题,因为 cur = headTmp; 会导致 cur.next==null,直接进入不了外层循环,就把最后一个节点落下了
      :::
class Solution {
    public ListNode reverseKGroup(ListNode head, int k) {

        if (head == null) {
            return head;
        }
        /*
        head
            如果k>链表长度,head不变
            否则 第一组的 结果链表的 头结点 是最终结果链表的head
        下面这是一个循环
            k个一组,断开翻转,再接上,
                断开前,需要保存 下一个节点,nextFirst 保存
                等第二组反转完成在接到第一组后面
            resLast 保存上一组的最后一个节点,用来拼接
            headTmp 每一组要反转的节点的head
            int i 索引位置计数,达到k就断开,断开后重置
            需要用 flag 标识是否是第一次循环,因为只有第一组的结果链表的 尾结点 才是最终的head
        */

        int i = 1;
        ListNode nextFirst = head;
        ListNode headTmp = nextFirst;
        ListNode cur = headTmp;
        ListNode resLast = headTmp;
        // 是第一次循环吗,是第一组吗
        boolean flag = true;

        while (cur != null) {
            while (cur.next != null && i < k) {
                cur = cur.next;
                i++;
            }
            // k个才能成为一组,小于k个不反转
            if (i == k) {
                i = 1;
                nextFirst = cur.next;
                // 断开
                cur.next = null;
                // 反转后 headTmp 成为了结果链表的尾结点
                ListNode reverse = reverse(headTmp);
                if (flag) {
                    head = reverse;
                    flag = false;
                } else {
                    // 接上
                    resLast.next = reverse;
                }
                resLast = headTmp;
                headTmp = nextFirst;
                cur = headTmp;
            } else {
                // 拼接最后一组不足k个的链表
                resLast.next = headTmp;
                // 为了终止外层循环
                cur = null;
            }
        }

        return head;
    }

    
    public ListNode reverse(ListNode head) {

        if (head == null) {
            return head;
        }
        ListNode pre = null;
        ListNode cur = head;
        ListNode next = cur.next;
        cur.next = null;

        while (next != null) {
            pre = cur;
            cur = next;
            next = next.next;
            cur.next = pre;
        }

        return cur;
    }
}

递归方法

:::info
递归方法这里其实只需要看下第一个就可以,因为其他的进阶的链表反转的题目都跟指针法一样,都是在基础的基础上加上计数

  1. 递归就是大问题化成小问题
  2. 因此想象只有一个 head 节点的情况,反转的结果就是 返回head
  3. 两个节点:head、head.next
    1. 递归调用 反转 head.next,返回的就是当前的head.next
    2. 返回结果赋值为 last
    3. head.next.next=head; // 指向自己,实现反转
    4. head.next=null; // 断开链表,避免循环
    5. return last; // 其实每一层递归都是返回last,因为last是结果链表的head,也就是最终返回结果
    6. 除了last,last之前的所有节点都在自己的那一层递归中通过head和head.next引用,从而实现反转
      :::
/**
 * 反转链表 递归
 */
public class a92 {

    /**
     * 递归反转,想象head是有两个节点组成的链表
     *
     * @param head
     * @return
     */
    ListNode reverse(ListNode head) {
        if (head.next == null) return head;
        ListNode last = reverse(head.next);
        head.next.next = head;
        head.next = null;
        return last;
    }

    //后继结点
    ListNode successor = null;

    /**
     * 反转前n个节点
     * 化整为零:
     * 要反转的最后一个节点,下一个节点是后继结点
     * 反转两个 1 个节点
     */
    ListNode reverseN(ListNode head, int n) {
        if (n == 1) {
            successor = head.next;
            return head;
        }
        //被反转部分的最后一个节点,将来是反转后链表的head,所以最后return head
        ListNode last = reverseN(head.next, n - 1);
        //想象反转两个节点
        head.next.next = head;
        head.next = successor;
        return last;
    }

    /**
     * 反转 m到n个节点
     * 化整为零:
     * head 一直往后走,把这个题目转变成反转前 n 个节点
     * m==1,说明是反转前n个节点
     * m!=1,head.next 是要被操作的链表,此时head还是没动,因此返回head
     */
    ListNode reverseMtoN(ListNode head, int m, int n) {
        if (m == 1) {
            return reverseN(head, n);
        }

        head.next = reverseMtoN(head.next, m - 1, n - 1);

        return head;
    }


}

单链表是否有环

  1. set
  2. 双指针
    1. 快慢指针
    2. 快指针一次走两步
    3. 慢指针一次走一步
    4. 快慢指针相遇就表示有环

找到环入口

快慢指针,按照上面的步骤得知:
快慢指针相遇的时候:

  1. 快指针走了 y 步,长度就是 2y
  2. 慢指针 在环里 走了 x,(慢指针共走了 l+x)
  3. 起点到环的距离是 l
  4. 环的周长是 s
  5. 所以
    1. y=l+x
    2. 2y=l+x+ns
  6. 所以
    1. y=ns=l+x
    2. l=ns-x
    3. l=(n-1)s+(s-x)
  7. (s-x) 相当于从快慢指针相遇点到环的起点的距离
  8. 所以 快指针变成慢指针从相遇点开始走,慢指针从起始点开始走,再次相遇的位置就是环的起点

删除链表的倒数第n个节点

:::info
right先走n步,left开始走,左右指针的间隔为n,同时走到right.next==null,就把left.next位置的节点删除
:::

public ListNode removeNthFromEnd(ListNode head, int n) {
    // 由于可能会删除链表头部,用哨兵节点简化代码
    ListNode dummy = new ListNode(0, head);
    ListNode left = dummy, right = dummy;
    while (n-- > 0) {
        right = right.next; // 右指针先向右走 n 步
    }
    while (right.next != null) {
        left = left.next;
        right = right.next; // 左右指针一起走
    }
    left.next = left.next.next; // 左指针的下一个节点就是倒数第 n 个节点
    return dummy.next;
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值