单链数据结构
在讨论头插法,反转一条单链之前,先回顾一下单链的数据结构。单链是一种线性表结构,存储的数据内容统一,可重复的。而单链跟数组最大的区别是,单链底层存储内存可不连续的,而这也导致单链上每个节点都有一个后继指针Next。
看一个场景小模型
回顾完单链这种数据结构后,我们再看一个场景小模型。回想我们校园时代,是否经历过排队,特别像军训时一样,被教官训练整蛊,从矮到高,从左往右排。这时学生就真的从矮到高排完了,如下图A~E一样。
怎么样,学生排完了,在学生的角度,矮的确实在左边,高的在右边。但是作为教官的我们的角度,却是相反的,所以我们就需要让学生反转了。这时候学生队伍的反转,就跟我们的头插法反转单链场景很像了。
还是根据这个场景进行说明,先让我们角度看去的高个子E,跑到A的位置,相当于开了一条新队伍,此时这条队伍的第一个人是E,最后一个人也是E,这个E至关重要,看清楚了。
排完后,这时,E后面是没人了的,而后面D进来,E就永远是最后一个人了,同理C、B、A这样进去,最后就反转了这条队伍。
上代码
- 新建Node节点
public class Node {
public int val;
public Node next;
public Node(int val) {
this.val = val;
}
}
- 创建单链反转工具类
public class SingleLinkedList {
public Node head;
public SingleLinkedList() {
}
public SingleLinkedList(Node head) {
this.head = head;
}
public Node reverseList() {
if (this.head == null) {
return null; // 没有节点
}
if (this.head.next == null) {
return this.head; // 只有一个节点
}
// 至少有两个节点
// 先把当前链表的头取出来,当作一条新的链尾(this.head.next = null;),再把剩下的旧链部分当作当前链(Node cur = this.head.next;)
Node cur = this.head.next;
this.head.next = null;
// 因为至少有两个结点,所以第一次取,剩下的旧链肯定至少还有一个节点
while (cur != null) {
// 再把剩下的旧链拆分,把第一个,跟后面的分开,curNext就是后面的链
Node curNext = cur.next;
// 因为上一步拆开,所以可以把新链放到当前链头后面
cur.next = head;
// 把当前链看作新链,覆盖到新链上面去
this.head = cur;
// 把curNext后面链当作当前链,接着判断当前链,直至当前链为空,即整条链已经进行反转处理
cur = curNext;
}
return head;
}
}
- 测试类
public class App {
public static void main(String[] args) {
Node head = new Node(3);
Node node1 = new Node(2);
Node node2 = new Node(5);
head.next = node1;
node1.next = node2;
Node h = head;
while (null != h) {
System.out.print(h.val + " ");
h = h.next;
}
System.out.println("\n============");
SingleLinkedList singleLinkedList = new SingleLinkedList(head);
head = singleLinkedList.reverseList();
while (null != head) {
System.out.print(head.val + " ");
head = head.next;
}
}
}
代码分析
上面三个类,单链反转重点关注工具类中的reverseList()方法。
-
首先该方法,先判断特殊情况:
- 空链:直接返回null
- 该链只有一个节点:直接返回第一个节点
-
除了1的两种情况外,该链至少两个或以上的节点,则可以进行反转返回
-
在进行这个代码分析之前,先记住场景小模型的排队案例,会让你更好理解代码的分析。
-
首先节点对象含有两个属性,一个是val,我们现实案例中,可设为Object或泛型,这里为了便于分析设为了int。另外还有一个属性是next,从前面的单链数据结构中,不难看出,单链是有个后继指针的,这个后继指针的作用是为了指向我们链上的下一个节点的。
-
而此时我们有一条单链[4,3,8,9]要反转,最后的结果应该是[9,8,3,4]。核心代码中,首先要获取单链中除了头节点以外,剩下的一条链
Node cur = this.head.next;
。-
初学者,可能会觉得this.head.next是获取到3而已。其实不是的,参考第2步的节点对象,可以得知,3这个节点对象,还有next属性(该属性又是一个Node节点类型)。所以this.head.next是获取到[3,8,9]
-
而代码注释中,提到
先把当前链表的头取出来,当作一条新的链尾(this.head.next = null;)
,可能有人会问,为什么这段代码会在上一步代码的后面。细心的人应该也知道,如果先把第一个节点(head)的next置空的话,那再取链后面的内容就都为null了。 -
总结:这一步可以确定两段代码的含义,
cur
是可以看作当前队伍
,this.head
看作是一条新队伍
,对比上面场景模型的E
-
-
OK,现在进入循环,大家可以看到,上来又有一个叫
curNext
的变量,这个变量我们见名思其意,我们可以称为未来当前链
。然后Node curNext = cur.next;
跟第3步中的操作类比,curNext
应该是cur
这条链,除了第一个节点外,后面的节点,在这里即[8,9] -
在上一步操作看明白后,来到这一步
cur.next = head;
,可能大家有点懵,我一开始也懵在这里。-
首先再回想一下场景中E排好在新队伍后,D是怎么进去的,是不是跟C、B、A断开关系,然后去排在E前面。
-
好会回过来看一下,这时候上一步的
curNext
是不是就是相当于C、B、A的那一段,那cur的val属性是不是就是我们D,那我们把cur(D)的next(C、B、A即[8,9])替换成(E即[4]),是不是就有cur.next = head
。这时候的head
在第3步第二点的操作中,head
只剩下val属性值为4,next为null了。而cur.next
指向了head
,那cur
的val是不是就是3,next是不是就是head,而head没有next,所以cur
就是[3、4]了 -
cur
是[3、4],而第4步中curNext
是不是剩下[8、9]。
-
-
因为第3步总结说到,cur是
当前队伍
,this.head是新队伍
,所以在进入下次循环之前,要给他们定义好,所以就有this.head=cur
,cur=curNext
。当然,他们的顺序不能颠倒,要是把cur先设为curNext,那再设this.head就两条链内容一样,都为[8、9]了。
-
完整流程图
根据代码分析后,是不是觉得循环还没有结束,有点可惜,可以再看完代码分析,及文字解析后,我们看图走完这个[4、3、8、9]的头插法单链反转
流程吧
进入循环前
进入循环后
第一遍循环
第二遍循环
第三遍循环
第四遍循环
没有了,cur都null了,进不去了,这时候把最后一次循环的head返回即是倒转后的结果