一.链表
链表的形式从如下三个角度考虑:
1.有头还是无头
对于有头的,head节点的value域为空,而且插入数据时,必须插在head之后(不一定紧挨着),对于无头的,差在哪里都可以
2.单向还是双向
3.循环还是非循环
若循环,则是首尾相连
我们只讨论俩种链表:无头单向非循环链表和无头双向链表(我们即将提到的LinkedList在JDK1.7以后就是无头双向非循环链表)
1.无头单向非循环链表的模拟实现
首先要有一个内部类来表示各个节点,再在外部将节点组织起来
虽然这是个无头链表,但我们也需要定义一个节点,通过它来标记链表的开始,所以要定义一个head,只要它的val域不为空即可
头插法:
其实不加if的判断也可以,直接进行else也不会出现空指针异常的情况,只不过加上了显得考虑更全面
尾插法:
尾插法的关键是找到最后一个元素,所以要用cur.next!=null来判断,注意不是cur!=null;
那么上面的代码正确吗?想一下head==null的情况,此时就不能直接访问cur的next域,否则就会空指针异常,所以要如下修改:
指定下标插入元素:
先写一个雏形
首先要检查下标的合法性,不合法时要抛出一个自定义的异常:
其次,这个代码的关键是找到要插入下标的前一个节点,我们来看看前面这段代码对不对:
cur是记录当前节点,而prev是记录前驱节点,index--,直到index为0时,cur所在位置就是新节点该插入到位置,prev就是它的前驱节点。但想一个特殊情况,head=null时,首先检查下标合理性,如果能执行到这一步,就说明index=0,所以就直接出了while语句,进行下一个语句,但是,prev指向null,就会出现空指针异常的情况,所以要加一条判断head是否为空:
有人发现了,最后也没有用到cur,那可不可以改成下面的代码?
全面考虑,当head为null时,这段代码会出现bug吗?会不会由空指针的现象呢?答案是不会的。假设head==null,首先,能进入到这一步,就说明下标是合法的,所以index==0;但注意,按理说index已经等于0了,就不应该进入while语句了,但index-1是-1,的确不是0,就会进入while语句,就出错了,所以首先要进行如下修改:
然后再想,如果head==null,那么prev就是null,就不能访问next域了,否则就会空指针异常。所以这个代码也要加上判断head是否为空的代码,如下:
此时,如果可以进入到prev=head这一块,就说明head不为空,但又要考虑,这段代码支持在0下标添加元素吗?显然不支持,所以还得进行如下修改:
这样就没问题了,所以代码最终为:
不过,代码还可以继续优化。仔细想想,之后进行指定下标删除元素时也要进行寻找前驱节点的操作,所以我们可以把这一操作封装成一个方法,如下修改:
查找是否包含某元素:
删除第一次出现的数字域为val的节点:
这个写法对吗?首先判断是否包含val,如果包含,就进入if语句。那么,假如head.val==val,那还正确吗?不正确了,此时cur就是head,prev就是null,所以就没有prev.next,否则会有空指针异常。所以要进行如下修改:
这回就对了
删除所有值为key的节点:
首先判断链表是否为空,然后判断是否需要删除头节点。但有一个问题,如果删除了头节点后,新的头节点也是需要删除的,那该怎么办?这就需要将第二个if改成循环,而且不能return!!!因为是要删除所有key的节点,如下:
走到prev=head后,头节点就不是要删除的节点了,所以就可以进入到下一步,逐个寻找前驱节点。这样就对了。
后面寻找前驱节点这一段代码也可以修改为:
清除链表:
注意,这种写法只适用于value域是基本数据类型的。如果存放的是引用类型,就没有这么简单了,代码就应该如下修改:
也就是要加一句,将cur的value域置为null。
遍历并打印所有value:
2.无头双向非链表的模拟实现(LinkedList的模拟实现)
首先要有以上这几个成员变量
头插法:
首先判断链表是否为空,如果是空,就要对first和last都进行更新,反之进行头插。
尾插法:
指定位置插入:
首先还是检查下标是否合法,然后看链表是否为空,如果不为空,看看是不是头插或者尾插,如果不是,就去找下标对应的位置,最后要注意几个量的更新顺序,共有四个量要进行更新,分别是上一个节点的next域,新节点的prev和next域,cur节点的prev域,一般是从前往后更新,这样就能保证寻找到所有节点
查找是否包含某元素:
删除第一次出现的值为val的节点:
首先看是否包含该节点,如果包含,再看链表中是不是只有一个元素,如果是,则直接将first,last置为null;
再看是不是头节点和尾节点。这里的代码要注意,头节点的prev域一定是null,尾节点的next域一定是null,当修改了头节点和尾节点后,一定要记得修改这俩个域的值。
如果都不是上述情况,则就是一般情况。首先要找到这个待删除的节点,然后修改它上一个节点的next域和它下一个节点的prev域。
删除所有值为value的节点:
仔细看,会发现这段代码有错。如果包含这些节点,就会进入if,首先处理只有一个节点的情况,其次进行头节点的删除,这里就出现问题了
我们为什么要先处理只有一个节点的情况呢?就是为了防止空指针的情况。而当删除头节点到只剩一个节点且此节点也是需要删除的节点时,while代码块中first=first.next可以正常执行,但first.prev就不行了,因为当执行了第一条语句后,first就指向空了,就没有prev域了,所以还要进行修改:
把判断只有一个元素的情况放到删除头节点的代码内部。这样就对了。
遍历并打印所有val:
清除链表:
如果存放的是引用类型,则加上cur.val=null。
二.LinkedList的使用
1.基本成员
首先,它有一个静态内部类,用来表示每个节点,因为是双向的,所以会有prev域和next域。
然后有first和last,来记录链表的首尾。
2.LinkedList的构造
(1).无参构造
(2).有参构造
参数是一个实现了Collection接口的集合,它里面的元素必须是我们的LinkedList中存放的元素的子类或同类
3.插入元素
第一个是头插法,第二个是尾插法,都调用了另一个方法。先看
头插法:
看这个头插法,它一上来并没有先判断是否为空。而是先把它变成头节点,这时不会涉及到空指针问题。然后判断f是否为空,因为如果不为空,就要更新它的prev域。
尾插法:
尾插法也同理,先变成尾节点,再判断要不要更新prev域。
然后再看
在指定位置插入元素:
首先检查下标合法性,然后,判断是否为尾插法,反之使用LinkBefore,为啥要判断是否为尾插法呢?我们先看一下LinkBefore:
首先把newNode插入到succ前面,即让newNode的prev域指向succ.prev(这里不会有空指针异常问题,因为既然能走到这一步,就说明下标合法且下标不等于size,也就是说这个下标一定对应一个元素),让prev的next域指向succ。然后更新succ的prev域(也不会空指针异常)
然后就该更新succ的前一个节点的next域了,这时,succ一定有,但它的prev可不一定有,所以要判断一下,如果有,才更新,反之不更新。
添加全部元素:
先看前半部分,主要是在确定插入时的起始位置(succ)以及它的上一个节点(pred)。
首先检查下标是否合法,如果合法,再看是否是size,如果是的话,succ指向的就是last.next即空节点,pred指向的就是last。接着,走到下一步就说明index一定指向某个节点,所以succ就是node(index),perd就是它的前一个节点。
到这里,上半部分就执行完毕了,有几种可能,一种是prev指向last,一种是succ指向first,还有就是两者分别指向一个节点。
然后开始插入元素,主要分为两步,首先在prev这边添加元素,并不断向后移动prev;然后将这一段链表和succ连接上。具体请看代码:
通过for循环来添加元素,每一个新节点创建时,它的prev域都指向pred,next域都指向null,然后更新pred的next域,但在更新前要判断pred是否为null,避免出现空指针异常的问题,最后更新pred。
for循环走完,链表就基本上创建好了,然后就是把它和succ相连,也就是把pred的next域指向succ,再把succ的prev域指向pred,但在进行这一步时,要判断succ是否为null,如果是null,说明新链表的结尾就是原始链表的结尾了,就直接更新last即可。
最后还有一个在旧链表的末尾添加元素,也是调用上述方法:
4.删除元素
指定value域进行删除:
首先要判断value是否为null,如果是,则去找链表中value为空的节点去删除
如果不是空才能进入到else语句。为什么要判断是否为空?注意else中,会用到对象的比较,只有当o不为空时,才可以调用equals方法
删除节点时用到了unlink方法:
删除时还是要判断删除的是否是头节点或者尾节点,要删除一个节点,主线任务还是更新prev的next域以及next的prev域,然后还要讲待删除节点的prev和next置为null。起始就可以分为前半部分和后半部分,前半部分就是prev.next和x.prev
要想更新prev.next,首先要判断prev是否为空,如果prev==null,就说明在删除头节点,这就很简单了,直接更新first即可,这时也不用写x.prev=null了;如果不为空,就要更新,让prev.next指向原来的next,然后x.prev=null。
之后进入到后半部分,next.prev和x.next。逻辑还是同上。
注意,如果删除的是头节点,那么一定是进入第一个if和第二个else,如果是尾节点,就是第一个else和第二个if,反之就是俩个else
指定下标进行删除:
直接调用unlink即可
删除头节点或尾节点:
都调用了另一个函数:
首先看删除头节点:
因为它的返回值是被删除节点的value域,所以在删除之前,要先将该val记录下来,并把下一个节点记录下来,以防找不到下一个节点。然后把value域和next域置为空,也就是与链表断开联系。然后将first后移。然后该更新头结点的prev域为null,但在更新之前,要看first现在是不是空,如果是空,说明链表空了,就没有节点了,就得把last给更新了
再看删除尾节点:
和上面的思路类似,首先要记录下value域和前一个节点,然后将value域和prev域置为空,然后更新last,然后看是否需要更新当前last的next域为空,这就要看prev是否为空,如果是,就说明此时链表为空,就要把头节点也置为空,反之更新当前last的next域
5.获取某一结点
获取指定下标的节点:
调用了node函数:
大方向:如果在链表的前半部分,就从头开始查找,如果在链表的后半部分,就从尾开始查找
获取头节点或尾节点:
注意是否为空的判断。
6.修改某一结点的value域
这里不是插入元素,而是修改该节点的value域
7.返回指定元素对应的索引
返回指定元素第一次出现时的索引:
这个和node方法的思路类似,只不过用到了equals方法。
返回指定元素最后一次出现时的索引:
这个是从后往前找
三.LinkedList的遍历
和arrayList很像
第一种:
第二种:
第三种,用迭代器正向遍历:
注,这里是和ArrayList不同的,他用的是ListIterator接口,调用了LinkedList的listIterator方法(该方法可以生成一个迭代器)
第四种,用迭代器反向遍历:
为了了解正反遍历的区别,我们来看一下源码:
首先会new一个ListItr对象,这是LinkedList内部的一个内部类。
这是它的一些成员变量
lastReturned记录的是该对象所拥有的最后一次访问到的节点的记录,next表示的是下一次即将访问到的元素(注意,可不一定是下一个,也有可能是下一个,因为LinkedList是一个双向链表)
这是这个迭代器的构造方法,参数是一个下标,可以表示你想让迭代器从哪里开始遍历,当index==size时,就是指向了末尾后面,反之就调用node方法,注意,这个node方法是LinkedList的一个普通成员方法,这是静态内部类调用外部类的成员方法。
这一组方法是正向遍历的实现,首先是hasNext判断是否有下一个节点,然后用next返回下一个节点的value域主要就是更新lastReturned和next引用,都是向后走一个
这一组方法是反向遍历。注意,首先检查下标的合法性,然后看看是否有上一个元素,如果有,就开始更新lastReturned和next。注意,要是用来previous方法,就说明是从后开始遍历,那么,最初的next就指向null(这是构造方法给出的)所以第一次调用previous,更新后的lastReturned和next都指向last,并且最后返回last的value域。
反向遍历和正向遍历有区别,反向遍历时lastReturned和next指向的都是最后一次访问到的节点,而正向遍历时,next指向的时即将访问的节点。这是区别。
然后再看它的remove方法:
这个方法是删除最后访问到的元素,也就是删除lastReturned节点。这个方法就很好的告诉我们正向遍历和反向遍历时,为什么lastReturned和next的分布不同。我们来深度解析一下这个方法:
首先检查lastReturned是否为null,因为要删除的就是它,所以只有它不为空才能删除。如果不为空,就走向下一步,记录下待删除节点的下一个节点,并调用外部类的unlink方法去删除节点,然后就到了关键的一步,更新nextindex。如果删除时遍历是使用的正向遍历,那么nextindex就是next所在的位置,而所删除的节点在next之前,也就是nextindex之前少了一个下标,所以随之nextindex就要减一,同时,next不用进行更新,因为删完lastReturned之后就正好轮到了当前的next,这就是else语句;而如果遍历时是使用的反向遍历,那么删除的节点既是lastReturned,又是next,所以要更新next的值为lastnext,同时,不需要更新nextindex,因为nextindex应该指向next,而删了lastReturned之后,lastnext就补到了这个位置。
分析源码发现remove方法没有让lastReturned更新,仅仅是让它成了空。所以在使用remove之前都要用到next或previous方法
以上就是对ListItr这个内部类的分析。
讲到最后,有同学就有疑问了,正向遍历的时候,我们在调用listIterator方法时没有传参呀,可这个LinkedList只有一个有参数的listIterator方法呀?这是因为,它用到了父类List的listIterator方法,在接口List中,有一个无参的listIterator方法,如下:
四.LinkedList与ArrayList的区别
1.前者逻辑空间上连续,但物理空间上不一定炼狱;后者物理空间连续。
2.前者在指定下标插入删除元素时不需要进行对其他元素的移动,后者需要移动其他元素。但注意,这并不代表Linked List插入元素的效率就高,因为它还是得遍历链表从而找到要删除或插入的位置,时间复杂度是O(N),而ArrayList在指定下标时,虽然不用遍历数组,但还需要重新拷贝数组,时间复杂度也是O(N)。
3.前者头插和尾插时的时间复杂度为O(1);中间插入的时间复杂度为O(N),因为要遍历列表找到指定位置
后者在末尾添加元素的时间复杂度为O(1),指定下标或规定节点时的时间复杂度为O(N),主要复杂在数组拷贝上。
4.前者不用考虑容量,后者在空间不够时需要扩容,可能会有空间的浪费。
5.当随机想要访问某个下标的值时,前者需要从头或从尾遍历,时间复杂度为O(N),而后者直接访问即可,时间复杂度为O(1)。
6.前者适用于频繁地在任意位置插入删除节点的场景,后者适用于元素高效的顺序存储以及频繁地通过下标进行访问