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

74 阅读5分钟

链表的概念?

什么是链表?

单向链表就像一个铁链一样,元素之间相互连接,包含多个结点,每个结点有一个指向后继元素的next指针。表中最后一个元素的next指向null。如下图:

image.png 注意:链表中一个节点只能有一个后继,可以有多个前继

链表相关概念

节点和头节点: 在链表中 ,每个点都由值和指向下一个结点的地址组成的独立的单元,称为一个结点,有时也称为节点,含义都是一样的。 对于单链表,如果知道了第一个元素,就可以通过遍历访问整个链表,因此第一个结点最重要,一般称为头结点。

虚拟节点: 在做题以及在工程里经常会看到虚拟结点的概念,其实就是一个结点dummyNode,其next指针指向head,也就是dummyNode.next=head。 因此,如果我们在算法里使用了虚拟结点,则要注意如果要获得head结点,或者从方法(函数)里返回的时候,则应使用dummyNode.next。 另外注意,dummyNode的val不会被使用,初始化为0或者-1等都是可以的。既然值不会使用,那虚拟结点有啥用呢?简单来说,就是为了方便我们处理首部结点,否则我们需要在代码里单独处理首部结点的问题。在链表反转里,我们会看到该方式可以大大降低解题难度。

如何创建一个链表

根据面向对象的理论,在Java里规范的链表应该这么定义:

public class ListNode {     
    private int data;     
    private ListNode next;     
    public ListNode(int data) {         
        this.data = data;     
    }     
    public int getData() {         
        return data;     
    }     
    public void setData(int data) {         
        this.data = data;     
    }     
    public ListNode getNext() {         
        return next;     
    }     
    public void setNext(ListNode next) {         
        this.next = next;     
    } 
}

但是在LeetCode中算法题中经常使用这样的方式来创建链表:

public class ListNode {
    public int val;
    public ListNode next;
    
    ListNode (int x) {
        this.val = x;
        //JVM会将指针设置为空,其实不需要再写
        this.next = null;
    }
}

这里的val就是当前结点的值,next指向下一个结点。因为两个变量都是public 的,创建对象后能直接使用listnode.val 和listnode.next来操作,虽然违背了面向对象的设计要求,但是上面的代码更为精简,因此在算法题目中应用广泛。

链表的增删改查

链表的遍历

遍历链表时,一定要记得重置header节点。

public static int getListLength(Node head) { 
    int length = 0; 
    Node node = head; 
    while (node != null) { 
        length++; node = node.next; 
    } 
    return length; 
}

链表插入

单链表的插入,和数组的插入一样,过程不复杂,但是在编码时会发现处处是坑。单链表的插入操作需要要考虑三种情况:首部、中部和尾部。

链表头部插入

链表表头插入新结点非常简单,容易出错的是经常会忘了head需要重新指向表头。 我们创建一个新结点newNode,怎么连接到原来的链表上呢?执行newNode.next=head即可。之后我们要遍历新链表就要从newNode开始一路next向下了是吧,但是我们还是习惯让head来表示,所以让head=newNode就行了,如下图:

image.png

链表中间插入

在中间位置插入,我们必须先遍历找到要插入的位置,然后将当前位置接入到前驱结点和后继结点之间,但是到了该位置之后我们却不能获得前驱结点了,也就无法将结点接入进来了。这就好比一边过河一边拆桥,结果自己也回不去了。 为此,我们要在目标结点的前一个位置停下来,也就是使用cur.next的值而不是cur的值来判断,这是链表最常用的策略。 例如下图中,如果要在7的前面插入,当cur.next=node(7)了就应该停下来,此时cur.val=15。然后需要给newNode前后接两根线,此时只能先让new.next=node(15).next(图中虚线),然后node(15).next=new,而且顺序还不能错。 想一下为什么不能颠倒顺序? 由于每个节点都只有一个next,因此执行了node(15).next=new之后,结点15和7之间的连线就自动断开了,如下图所示:

image.png

链表结尾插入

表尾插入就比较容易了,我们只要将尾结点指向新结点就行了。

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;         
        // 这里可以直接 return nodeInsert;还可以这么写:         
        head = nodeInsert;         
        return head;     
    }      
    Node pNode = head;     
    int count = 1;    
    //这里position被上面的size被限制住了,不用考虑pNode=null     
    while (count < position - 1) {         
        pNode = pNode.next;         
        count++;     
    }      
    nodeInsert.next = pNode.next;     
    pNode.next = nodeInsert;      
    return head; 
}

这里需要再补充一点head = null的时候该执行什么操作呢?如果是null的话,你要插入的结点就是链表的头结点,也可以直接抛出不能插入的异常,两种处理都可以,一般来说我们更倾向前者。 如果链表是单调递增的,一般会让你将元素插入到合适的位置,序列仍然保持单调,你可以尝试写一下该如何实现。

链表删除

删除同样分为在删除头部元素,删除中间元素和删除尾部元素。

删除头部节点

把链表的头节点设为原先头节点的next指针 即可。 一般只要执行head = head.next 就行了,如下图,将 head向前移动一次之后,原来的节点不可达,会被JVM回收掉

删除尾部节点

把倒数第2个节点的next指针指向空 即可

删除中间节点

把要删除节点的前置节点的next指针,指 向要删除元素的下一个节点即可

删除中间节点时,也会要用 cur.next来比较,找到位置后,将cur.next 指针的值更新为cur.next.next即可。