算法通关村第一关——链表青铜挑战笔记

104 阅读3分钟

1. 链表的构造

虽然违背面向对象的设计要求,但是不用setter和getter, 直接操作两个public变量,更方便。

public class ListNode {
    int val;           // 用于存储节点的值
    ListNode next;     // 指向下一个节点的指针

    // 构造函数
    ListNode(int x) {
        val = x;
        next = null;
    }
}

2.链表插入节点

为什么要单独处理position==1和head==null
position == 1和其余位置的处理逻辑不同。
目标节点是position, 循环终止使获取position-1。
操作相邻两个节点(后‘节点’可以为null,因为后节点是被赋值而不会需要后节点.next)。
但是position==1为目标节点时,前面没有节点,所以需要单独处理。

image.png

/**
 * 链表插入节点
 * @param head 链表头节点
 * @param nodeInsert 待插入节点
 * @param position 插入位置, 从1开始
 * @return 插入后的链表头节点
 */
public static Node insertNode(Node head, Node nodeInsert, int position) {
    if (head == null) {
        // 这里可以认为待插入的节点就是链表的头节点, 也可以说此时不存在合适的节点
        return nodeInsert;
    }
    // 已经有的元素个数
    int size = getLength(head);
    if (position > size+1 || position < 1) {
        System.out.println("位置超出范围");
        return head;
    }
    // 头部插入
    if (position == 1) {
        nodeInsert.next = head;
        // 这里可以认为返回 nodeInsert; 也可以认为是:
        head = nodeInsert;
        return head;
    }

    Node pNode = head;
    int count = 1;
    // 这里position要减去1, 因为我们是在pNode后面插入
    while (count < position - 1) {
        pNode = pNode.next;
        count++;
    }
    nodeInsert.next = pNode.next;
    pNode.next = nodeInsert;

    return head;
}

/**
 * 链表插入节点 - 使用 dummy node 版本
 * @param head 链表头节点
 * @param nodeInsert 待插入节点
 * @param position 插入位置, 从1开始
 * @return 插入后的链表头节点
 */
public static Node insertNode(Node head, Node nodeInsert, int position) {
    Node dummy = new Node(-1); // 创建一个 dummy node 作为哑元头节点
    dummy.next = head;        // 将 dummy node 的 next 指向原始头节点
    
    int size = getLength(head);
    if (position > size+1 || position < 1) {
        System.out.println("位置超出范围");
        return head;
    }
    Node current = dummy;     // 使用 current 指针代替 pNode
    int count = 0;            // 从 dummy node 开始计数,所以初始为 0

    // 由于有了 dummy node,我们可以将头部插入纳入 while 循环中
    while (count < position - 1) {
        current = current.next;
        count++;
    }

    // 在指定位置插入新节点
    nodeInsert.next = current.next;
    current.next = nodeInsert;

    return dummy.next; // 返回 dummy node 的下一个节点作为新的头节点
}

3.链表删除节点

/**
 * 删除链表节点
 * @param head 链表头节点
 * @param position 删除节点的位置, 按位置从1开始
 * @return 删除后的链表头节点
 */
public static Node deleteNode(Node head, int position) {
    if (head == null) {
        return null;
    }
    int size = getListLength(head);
    // 检查下,这里为什么是size, 而不是size+1
    if (position > size || position < 1) {
        System.out.println("输入的参数有误");
        return head;
    }
    if (position == 1) {
        // curNode就是要删除的head
        return head.next;
    } else {
        Node cur = head;
        int count = 1;
        while (count < position - 1) {
            cur = cur.next;
            count++;
        }
        Node curNode = cur.next;
        cur.next = curNode.next;
        // 上面两行可以简化为: cur.next = cur.next.next
    }
    return head;
}

public static Node deleteNodeWithDummy(Node head, int position) {

    int size = getListLength(head);

    Node dummy = new Node(0); // 创建一个 dummy node
    dummy.next = head;
    
    if (position > size || position < 1) { 
        System.out.println("输入的参数有误");
        return head; 
    }
    Node current = dummy;
    int count = 0;

    // 找到要删除节点的前一个节点
    while (count < position - 1) {
        current = current.next;
        count++;
    }

    // 如果当前节点的下一个节点不为空,删除当前节点的下一个节点
    if (current.next != null) {
        current.next = current.next.next;
    } else {
        // 如果位置超出链表长度,输出错误信息
        System.out.println("输入的位置超出链表长度");
    }

    return dummy.next; // 返回 dummy node 的下一个节点,即更新后的头节点
}

反思: 本质上,链表的增删需要考虑三种处理逻辑(第二个是待删/增的node)

  1. node node node/null 【所以尾节点不需要特殊处理】
  2. head指针 node node 【通过position==1】
  3. head指针 node null 【通过head==null】

如果使用了dummynode,以上第二和第三种情况就被合并到第一种情况了。

todo: if(head==null){}case是否可以去掉,if (position > size || position < 1) {}已经过滤了。去掉会不健壮么。