一、链表简介
上期的顺序表中浅浅地介绍了一下链表,先来回顾一下。链表就是一些相同类型元素的链式存储,在C语言中,通过地址把每个独立的结点相连。在Java中的原理也可以这么理解,但C中的地址在Java中叫做引用,它也指向它所指向的对象,方便理解也可以叫做地址。
这期讲一下Java中的单链表以及双向链表。单链表指的是单向无头链表,双向链表是双向无头链表。单链表的结构如下图,每个next就是存放下一个结点地址的值,以此相连成线性
这里单链表的结点采用只有两个域的形式呈现
注:虽然这里面有head但并不是真的代表头指针,真正的头指针是在整个链表前的一个指针,它存储的值一般情况下无意义,但它的next指向链表开始的第一个结点。所以单链表被叫做单向无头链表。
Linkedlist在Java中也继承了集合类,可以直接使用,我本期介绍它的模拟实现。
二、链表的模拟实现
首先还是定义了如图的接口,并逐一实现其中的方法。
先来看三个比较简单的方法,size();display();contains();和clear();为什么把这四个叫做最简单的方法,因为他们的思路都是相同的,只需要遍历即可。那在这之前需要先创建一个链表,久用最朴素的方式建立
这里直接建立了createlisti方法创建链表,手动申请了五个节点后让他们一次相连,最后让第一个结点当头结点。
先看size方法,它是一个用来求链表长度的方法,只需要遍历整个链表,只要是一个有效结点就把长度+1。
要遍历整个链表,可以定义一个cur指针,先把头指针的值给它。因为单链表中每个结点都用next和下一个结点相连,所以让cur的next指针赋值给cur就得到了当前指针的后一个指针(cur=cur.next),因此可以遍历整个链表。只要cur不是空指针,就让len的值+1,最终返回len值就得到了链表的长度。
用while循环是因为只要cur不是空就计数且最后一个结点的next是null,所以当cur为空就认为链表已经遍历完。
在单链表的循环中一般不用head=head.next方法去遍历,头指针是链表最重要的一个东西,它指向链表的开始,如果用头指针去循环那么循环结束的时候head就指向链表的最后一个结点。所以一般都是定义一个中间变量去遍历链表。
display方法用来打印链表中的值,直接遍历即可
contains方法也是直接遍历即可
我这里使用的元素类型是int,先来看一下display和size方法的效果
能正常打印链表和结点个数,没有问题
前面说顺序表就是对元素的增删查改,链表也是如此
Linkedlist中add默认是尾插法,为了方便区分我定义成addlast
尾插该如何插入?思路:既然是尾插,那就是要找到末尾结点的位置,让它的next指向要插入的结点位置。后面是几个重要的点①要找到尾部位置所以还是要用遍历②循环的结束条件③空链表怎么插入
先给出代码再做解释
既然是插入一个结点,所以上来就要为其申请一个新的结点对象
这里的循环结束条件和上文遍历不同,因为是需要找到尾部结点,结束条件不能是cur!=null而是cur.next!=null,这样循环结束的时候cur的下一个为空,那么就是链表最后一个结点,否则循环结束的时候cur是null。
还要注意链表是不是空链表(一个元素都没有的情况),此时的head就是所要插入的结点
测试特殊情况也没问题
接下来模拟实现头插法
头插法顾名思义,在链表的头部插入一个结点。思路:①首先还是直接申请一个新的结点node(data域存放想插入的值)②让node的next指向原来的头(node.next=head)再更新node为新的head(head=node)。
先测试一下正常的情况下
再来看链表本身是空的特殊情况
也没有问题
如果链表本身为空,head指向nul,node的next就等于null,更新node为新的head,head的next是null,逻辑没有问题
接下来在指定位置插入addindex
思路:还是先考虑链表已经有多个元素的正常情况下。思路:①既然是指定位置插入,那么就要先找到所需要插入的位置,如何找到?链表每个元素都相互连接,所以依旧定义一个cur=head后去遍历链表
②这里的下标还是定义为从0开始,如果要在2位置插入新的结点,那意思是让新的结点代替2的位置,后面的结点都需要往后挪。
③如果在2位置插入,那么是否让cur走到2的位置呢?显然不是,如果cur到了2的位置,前一个结点就找不到了,插入需要让1的next指向新结点,新结点的next指向原来的2位置结点。那只能让cur在1的位置就停下,不能继续走到2。意思就是让cur从head开始向后走index-1步,找到插入位置的前一个结点。使用while循环找到1结点
这里先让len=链表的长度,防止每次使用长度时都需要调用方法消耗内存资源
此外还要考虑插入的位置是否合法,插入位置<0或者>链表长度都是不合法的
需要注意的是while循环找到的是一般情况下插入的前一个位置,在插入时一般先处理node的next指向
此外还需要分别考虑插入位置是头或者尾部
如果index=0就是用头插法,直接调用addFirst即可
index=len,直接调用尾插法
特殊位置和正常情况都测试无问题
插入方法解决完后就是删除结点方法remove和removeallkey
如果现在删除的结点是cur,能否正确删除?拿不到cur前一个指针,这个结点还是与之前的结点相连的,无法删除。所以需要拿到要删除结点的前一个结点地址
定义一个findkeynode方法用来找到删除结点的前一个结点地址
当下一个结点不为空就继续才往下找,因为是找删除结点的前一个结点位置,这里的判断条件是cur.next.val==key,所以循环条件是cur.next不为空,否则会引发空指针异常,如果能找到返回这个结点,循环结束就代表未找到,出了循环返回null即可
找到删除结点的前一个结点后该如何操作?
只需要让cur.next==del.next,意思就是跳过del,没有引用指向它,在Java中就会被系统回收
除了一般情况,还需要考虑空链表,头结点为删除结点的情况。空链表不需要任何操作,直接返回。如果删除的是头结点,让head更新为后一个结点为新head即可
上述是删除第一个出现的key,如果要删除链表中所有的key就需要用removeallkey
removeallkey的思路和remove很相似,只不过removeallkey要删除所有的key就需要多一层循环,既然要删除所有的key,就需要遍历整个链表找到相应值后删除结点,所以循环结束的条件还是和遍历相同,当cur=null就停下。
由于不是双向链表,所以还是需要知道删除结点的前一个结点地址(此处定义为prev),cur从第二个结点开始向后遍历。如果是要删除的结点,删除后让cur更新,prev不动,否则prev和cur一起向前。上述是正常有多个元素的情况下,还需要考虑链表为空的情况,如果是空则不需要删除直接返回。最后删除完毕还要注意一点的是while循环中没有遍历到第一个结点的值,最后还需要加个if条件判断一下。
上述方法均已测试过,目前未发现问题。
本期描述了单向无头链表的原理以及模拟实现,下期讲继续讲述双向无头链表。