上篇回顾
看过之前的文章,应该知道线性表包含顺序表和链表。线性表的典型是数组,List等。那么本篇来讲解一下链表的相关知识。
链表包含两种,或者说三种,即单链表, 多链表, 循环链表。其中单链表只表示的next的关系,而多链表即包含了next, 又包含了pre;循环链表则是单链表与多链表的衍生链表,即头尾相链,构成循环。循环链表又分为单循环链表及多循环链表。
有图为证
链表的定义
链表是一种物理存储单元上非连续、非顺序的存储结构,数据元素的逻辑顺序是通过链表中的指针链接次序实现的。
从定义可以得到三个有用的信息, 链表的空间不是连续的,是无序的, 是通过链接查询的。前面说过顺序表是通过位序查询的,所以必须要连续的空间,而且要有序。那么通俗地怎么理解链表的概念呢?举个例子,大家见过链子吗?一条链子由若干个铁环相互链接组成,如下
如果把这个链子拆分,每个铁环都是一个单位。链表中的单元称之为节点Node。两个铁坏之间是扣在一起的,如果找到铁环1,必然通过联系能找到铁环2;但是不能直接的通过铁环1,找到铁环3。这就是链表。
链表是由若干节点Node组成,相邻的两个节点之间存在单一(单链表)或相互(多链表)的联系。假设上图为链表LinkedTable,简称LT; 把每个铁环看成一个节点,分别命名为A,B,C,D; 相互关系为:A->B->C->D
这个节点可以设计为:
private static class Node<E> {
E item;
Node<E> next;
public Node(E item, Node<E> next) {
this.item = item;
this.next = next;
}
}
item为铁环赋予的数据, next为下一个铁环
Node<String> nodeD = new Node<>("A", null);
Node<String> nodeC = new Node<>("D", null);
Node<String> nodeB = new Node<>("C", null);
Node<String> nodeA = new Node<>("B", null);
上述代码创建了4个节点nodeA,nodeB,nodeC,nodeD; 分别赋予值A,B,C,D; 关系为A->B->C->D; 这就是上图的链表模型了。其中A的next是B, B的next是C, C的next是D。我们可以通过A找到B,但不能直接找到C。
链表的长度就是铁环的数量,也就是节点数。
在上述链表模型中,nodeA称为头节点, nodeD则称为尾节点。
以上的概念,称之为单链表。
既然已经了解单链表的概念,那么多链表就容易的多了。多链表在指向下一个节点的基础上,添加了上一个节点的指向。 我们修改一下上述Node
private static class Node<E> {
E item;
Node<E> next;
Node<E> pre;
public Node(E item, Node<E> next, Node<E> pre) {
this.item = item;
this.next = next;
this.pre = pre;
}
}
从开篇的示意图就能看出来,多的是一个向前的箭头指向。那么什么是循环链表呢? 看开篇的图,尾节点指向头节点就是循环链表。看到这里,你可以试着自己构建一下这三种链表..
明白了链表的概念, 如果已知一个链表
Node<Integer> n1 = new Node<>(1, null);
Node<Integer> n2 = new Node<>(2, null);
Node<Integer> n3 = new Node<>(3, null);
Node<Integer> n4 = new Node<>(4, null);
n1.next = n2;
n2.next = n3;
n3.next = n4;
根据链表的特性,如何来遍历所有节点呢?我想你一定用过Cursor,已知头节点,我们来遍历一下
Node<Integer> parent = n1;
while (parent != null) {
System.out.println(parent.data + "");
parent = parent.next;
}
就是根据第一个铁环, 然后找第二个;然后根据第二个,找第三个;以此类推...
链表的链式关系,就是如此,相信大家都已经明白了。
明白了链表的概念,那么要上硬菜了,也是链表中最难理清的节点的添加与删除。有一个链表A-B-C-D,如果我想在C与D之间插入一个节点E, 那么过程是这样的
根据上图的铁链,想象一下,如果要在第3个与第4个之间再加一个铁环,你会怎么做?是不是这样的
1) 首先, 找到第3个铁环
2) 其次,找到第4个铁环
因为新加的铁环要放在这两个铁环中间,要想加进去,必然要把新铁环和3和4链起来才行。 那么首先要找到3和4,这是准备工作。
3) 砸开3与4之间的连接
4)新铁环与3连接
5)新铁环与4连接
根据这个步骤,再看上图。假如已经找到了NodeC, 然后根据next找到NodeD, 那么我们准备工作就做好了,接下来就是断开原链接, 添加新链接的过程了。看代码
// 创建要插入的新节点
Node<String> E = new Node<>("E", null);
// 找到待插位置的节点C与D
Node<String> C = findNodeC();
Node<String> D = C.next;
// 开始插入
C.next = E;
E.next = D;
根据以上分析与代码,你知道怎么插入新节点了吗?总结如下:
1) 找到待插位置的两个节点
2) 断开原来的连接关系
3) 分别与前驱节点,后继节点建立新连系
如果明白了如何在单链表插入节点,那么恭喜你!删除节点你也会很快明白的。还是那个栗子,如果我想把第2个铁环去掉,怎么做呢?
1) 找到第1个和第3个铁环
2) 断开第1个和第2个, 第2个和第3个铁环的练习
3) 将第1个和第3个铁环连接
N2.next = null;
N1.next = N3;
按步骤,先断开三者的连系, N1与N2, N2与N3之间,N2.next = null把N2与N3的连系断开了;N1因为断开后还要和N3连接,这里直接把N1和N3连接,就相当于断开N1和N2的连系,同时创建N1和N3的新连接了。
有人问了,你把找Node节点的步骤省略了,真的好么? 好吧,知道头节点,还找不到想要的节点吗? 我们的方式就是遍历,遍历上面说了,怎么判断目标节点的前驱和后继呢? 在删除时, 前驱的next必然是target啊, target直接判断是同一个节点就行了啊, 知道target了,next就能得到后继了吧。
你看,就是这么简单,只是有点绕!结合现实理解一下,其实理解了很容易记忆的。
总结步骤:
1) 找
找什么呢? 插入时,根据插入的位置找插入后的前驱和后继,如在A与B之间插入C,那么插入后B肯定是C是后继,A是C的前驱; 删除时,根据待删除元素的位置查找受影响的前驱和后继,肯定是该元素本身的前驱和后继。
2)断
为什么是先找呢? 因为断了之后,可能就找不到了。 断字诀指的是切断原有的关系,即插入元素的前驱和后继的关系,也即删除元素与前驱, 删除元素与后继的关系。
3)连
旧的关系已断,新的关系建立起来。建立指定元素Target(指插入的元素或删除的元素)动作后,新的关系的建立。即在插入时插入后的target与前驱, target与后继的关系; 也即在删除时, 删除后原删除元素的前驱与后继的新关系。
多链表的插入与删除操作,相比于单链表来说,稍微复杂了一点,因为添加了指向前一个元素的指针。
先看插入,如果在A和B之间插入元素C
那么还是按照上面总结的单链表的三字诀,来试一下
//1. 首先切断A与B之间的关系,如图即A的next是B, B的pre是A
NA.next = null;
NB.pre = null;
// 2.建立A与C的新关系,如图即插入后,A的next是C, C的pre是A
NA.next = NC;
NC.pre = NA;
// 3. 建立B与C的新关系,如图即插入后,C的next是B, B的pre是C
NC.next = NB;
NB.pre = NC;
那么一次插入就完成了,相对单链表而言,只多了一层pre的关系。而这层关系还是相对的,A的next是B,那么B的pre必然是A。
相同的道理,如果A->B->C的链表中,要删除B,那么
1. 断开A-B的关系
NA.next = null;
NB.pre = null;
2.断开B-C的关系
NB.next = null;
NC.pre = NB;
// 3.在A与C之间建立新关系
NA.next = NC;
NC.pre = NA;
理解这个过程了,其实很好实现。 以上删除与插入的代码会有重复,为了完整的重现这个过程。根据情况可以进行合并,合并时一定要考虑清楚断开和连接的顺序。
以上就是链表的全部概念了,如果你还不理解,那么可以评论给我。