一、介绍
今天主要介绍反转链表的几个经典问题,通过操作反转链表,进而深入的理解和掌握链表这一基础的数据结构。
二、反转单链表
首先介绍一下单链表的结构,单链表这一数据结构的特点在于每个链表的节点只会存储一个指向下一节点的指针,以下代码为一个单链表的节点结构。
class ListNode{
int value; // 当前节点存储的数据
ListNode next; // 当前节点指向下一节点的指针
ListNode(int val){
this.value = val;
}
}
一个单链表的基本结构如下所示
[head]头结点 [tail]尾节点
Node1(1) -> Node2(2) -> Node3(3) -> ... Noden -> null
如果从头遍历这个链表,得到的输出结果会是:1,2,3,... n 而反转一个单链表就是要将上述结构的头节点变成尾节点,尾节点变成头节点, 这时再遍历该链表,得到的输出结果就会为:n ... 3,2,1,我们先来看代码:
// 反转单链表
public ListNode reverse(ListNode head) {
if (head == null || head.next == null) {
return head;
}
ListNode prev = null;
ListNode next;
while (head != null) {
next = head.next;// 缓存当前节点的下一个节点
head.next = prev;// 当前节点的指针指向前一个节点(开始反转)
prev = head;// 当前节点缓存为前一节点
head = next;// 当前节点前进一步
}
return pre;// 反转后的头节点
}
三、反转双向链表
单链表反转是不是特别简单,接下来让我们看看如果是双向链表的反转,该如何处理呢?首先看下双向链表的节点数据结构:
class DoubleListNode{
int value; // 当前节点存储的数据
DoubleListNode next; // 当前节点指向下一节点的指针
DoubleListNode prev; // 当前节点指向前一节点的指针
DoubleListNode(int val){
this.value = val;
}
}
一个双向链表的基本结构如下所示
Node1(1) <- Node2(2) <- Node3(3) <- ... <- Noden
Node1(1) -> Node2(2) -> Node3(3) -> ... -> Noden -> null
其实双向链表相比于单链表,区别只是在于每个节点多了一个指向前一节点的指针,在反转的时候,流程与反转单链表是一样的,只是需要把多出来的指针考虑进去,我们来看下代码。
// 反转双向链表
public DoubleListNode reverseDouble(DoubleListNode doubleNode) {
if (doubleNode == null || doubleNode.next == null)
return doubleNode;
DoubleListNode pre = null;
DoubleListNode next;
while (doubleNode != null) {
next = doubleNode.next;// 缓存当前节点的下一个节点
doubleNode.pre = next;// 处理当前节点的前向指针,把它指向新的前向节点
doubleNode.next = pre;// 当前节点的指针指向前一个节点(开始反转)
pre = doubleNode;// 当前节点缓存为前一节点
doubleNode = next;// 当前节点前进一步
}
return pre;
}
四、反转单链表的给定范围
单链表和双向链表的反转都很简单,我们来点有难度的,如果给定一个单链表,再给定一个节点位置的范围m到n,我们要做的是在这个范围上,反转单链表,那该怎么做呢?
位置1 位置2 位置3 位置4 位置5
Node1(1) -> Node2(2) -> Node3(3) -> Node4(4) -> Node5(5) -> null
比如,给定一个上图所示单链表,再给定一个m=2,n=4范围,未反转前,遍历该链表的结果应当为:1,2,3,4,5, 翻转过后,遍历链表的结果为:1,4,3,2,5。我们来看看具体代码怎么实现。
public ListNode reverseFromMtoN(ListNode head, int m, int n) {
if (head == null) {
return null;
}
ListNode cur = head;// 当前节点
ListNode prev = null;// 当前节点的前一节点
while (m > 1) {
prev = cur;// 我们将当前指针走到m这个位置
cur = cur.next;// prev存储的就是m位置前一个节点的指针
m--;
n--;// n最终变为 m 到 n 之间的距离
}
// 这里分几种情况,如果 m > 1且 n 小于链表的长度 m 和 n 会将链表分为三段,con代表第一段的尾点,
// 如果 m = 1, n 小于链表的长度,相当于第一段的长度为0,
// 如果 m = 1, n 等于链表的长度,那这个问题就变成从头到尾反转单链表的问题
ListNode con = prev;
ListNode tail = cur; // tail代表 m 到 n 段反转后的尾节点
while (n > 0) { // 这个while做的就是反转从 m 到 n 这个范围上的链表
ListNode tmp = cur.next;
cur.next = prev;
prev = cur;// 反转过后,prev变为反转端的头节点
cur = tmp;
n--;
}
if (con != null) { // 这里隐含判断 m 和 n 分开的三段,第一段是否长度为0,如果不是,将第一段第二段连接
con.next = prev;
} else {
head = prev;// 如果第一段的长度为0 那么头节点就是反转过后一段的头节点
}
tail.next = cur;// 第二、三段进行连接
return head;
}
五、总结
反转链表的操作属于对链表的操作中比较基础的范围,如果想学习链表,我们都应该掌握这一知识点。操作反转链表上指定的范围,需要考虑的边界条件很多,需要注意避免踩坑。后续我们会继续总结链表操作的一系列知识点,从而能更深入理解这一基础的数据结构。