链表的概念?
什么是链表?
单向链表就像一个铁链一样,元素之间相互连接,包含多个结点,每个结点有一个指向后继元素的next指针。表中最后一个元素的next指向null。如下图:
注意:链表中一个节点只能有一个后继,可以有多个前继
链表相关概念
节点和头节点: 在链表中 ,每个点都由值和指向下一个结点的地址组成的独立的单元,称为一个结点,有时也称为节点,含义都是一样的。 对于单链表,如果知道了第一个元素,就可以通过遍历访问整个链表,因此第一个结点最重要,一般称为头结点。
虚拟节点: 在做题以及在工程里经常会看到虚拟结点的概念,其实就是一个结点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就行了,如下图:
链表中间插入
在中间位置插入,我们必须先遍历找到要插入的位置,然后将当前位置接入到前驱结点和后继结点之间,但是到了该位置之后我们却不能获得前驱结点了,也就无法将结点接入进来了。这就好比一边过河一边拆桥,结果自己也回不去了。 为此,我们要在目标结点的前一个位置停下来,也就是使用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之间的连线就自动断开了,如下图所示:
链表结尾插入
表尾插入就比较容易了,我们只要将尾结点指向新结点就行了。
综上 ,我们写出链表插入的方法如下所示:
/**
* 链表插入
* @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即可。