单链表倒转 和 单链表奇偶节点分离
何为单链表,一般指单向链表,由单向的节点组成,也就是节点下只有一个 next 方向属性的,单链表只能单方向遍历访问。
相对的,双向链表的双向节点一般有 before 和 after 属性,可以从前往后遍历也可以从后往前遍历。
单链表最关键的就是子节点的 next 指向,若指不好,链表可能断裂;也可能会形成一个环,在遍历访问的时候,出现栈溢出等未知错误情况。
日常开发的时候,在完成开发后,非常有必要写些单元测试,验证单链表操作的准确性!
来看下面经典的数据结构题吧,开阔一下链表思路;
定一个简单的节点类,下面的示例代码都基于这个数据模型:
class Node{
int val;
public Node next;
public Node(int val){
this.val = val;
}
}
1. 单向链表倒转
已知一个单向链表,将其的顺序倒转,须考虑最优的时间复杂度空间复杂度。 示例如下(长度为8的一个单链表):
1 —> 2 —> 3 —> 4 —> 5 —> 6 —> 7 —> 8 —> null
链表头为节点 1
倒转后为:
null <— 1 <— 2 <— 3 <— 4 <— 5 <— 6 <— 7 <— 8
链表头为节点 8
1.1 解法分析:
-
使用栈之前想过,先把链表遍历后放入一个 栈中,因为栈有先进后出的特性,再遍历栈,把栈 pop 出的节点依次重新指向,得到一个反序的链表;但是需要新建一个栈,会有额外的O(n)空间复杂度;
-
原地算法:
修改附近节点的指向,一次循环涉及 3 个节点,这边暂且称之为
前、中、后。在未倒转前,这三个节点指向为: 前 -> 中 -> 后。也就是
前.next == 中,中.next == 后均为 true。在每次遍历中,需要改变的指向仅是:改变
中的指向,把中指向前。先不管后的指向,因为本次遍历的后其实就是下次遍历时候的中,下次遍历自然会处理它的指向。在每次遍历中,修改完
中的指向后,我们还需要修改前 中 后的赋值,准备下一轮的遍历;也就是:// 注意,这边有顺序要求; 前 = 中; 中 = 后; 后 = 后.next;- 初始状态:把当前节点作为 中 节点,也就是中 = 1 ,后 = 2。没有谁是指向中的,那么,前 = null;
- 第一次访问:中 —> 前 : 1 由原来的指向 2 ,变为 指向 null; 准备下次访问赋值:前 = 1,中 = 2, 后 = 3
- 第二次访问:中 —> 前 : 2 指向 1 ;准备下次访问赋值: 前 = 2,中 = 3,后 = 4
- 第三次访问:中 —> 前 : 3 指向 2 ;准备下次访问历赋值: 前 = 3,中 = 4,后 = 5
- 省略...
- 第六次访问:中 —> 前 : 6 指向 5 ;准备下次访问赋值: 前 = 6,中 = 7,后 = 8
- 第七次访问:中 —> 前 : 7 指向 6 ;准备下次访问赋值: 前 = 7,中 = 8,后 = null
- 第八次访问:中 —> 前 : 8 指向 7 ;准备下次访问赋值: 前 = 8,中 = null,后 = null
第九次访问:没有第九次了,节点指向已经全部改完。注意:这时候,前为链表的脑袋,此链表的节点对象还是原来的节点对象,只是修改了其next指向方向;
可见时间复杂度为 O(n),n为单链表长度;
1.2 盲眼手搓
public static Node reverse(Node head){
if(head == null || head.next == null){
return head;
}
Node before = null;
Node middle = head;
Node after = head.next;
while(middle != null){
// 修改 中 指向 前
middle.next = before;
before = middle;
middle = after;
if(after != null){
after = after.next;
}
}
// 这个时候 before 为链表头,middle 和 after 都是 null
return before;
}
Ctrl C / V 了,记得测一下,我瞎写的,不知道对不对...
2. 单向链表节点的奇偶排序
给定一个单链表,把所有的奇数节点和偶数节点分别排在一起。请注意,这里的奇数节点和偶数节点指的是节点编号的奇偶性,而不是节点的值的奇偶性。
请尝试使用原地算法完成。你的算法的空间复杂度应为 O(1),时间复杂度应为 O(nodes),nodes 为节点总数。
例如,原单链表为:
A —> B —> C —> D —> E —> F —> G —> H —> I —> null
节点奇偶排序后为:
A —> C —> E —> G —> I —> B —> D —> F —> H —> null
2.1 原地解法分析:
要实现上述排列效果,抓住两个关键点:隔位指向;奇偶拼接;
链表的头我们认为序号为 1 ,从字母 A(第1个)开始到字母 I (第9个)
-
隔位指向:
以A为起点,进行隔位指向,A 指 C,C 指 E ... 。顺序变成:
A C E G I。这是奇数序号的重排链表。同时再起一个头。以 B 为起点,新建一个临时引用 Temp,赋值 B,后面链表拼接的时候会用到。再进行隔位指向,顺序变为
B D F H。这是偶数序号的重排链表。注意:上述的奇偶隔位指向是同时进行的,并不是先指完奇数序列再指偶数序列。
-
奇偶拼接:
在遍历结束的时候,奇数的指针在 I 上, 偶数的指针在 H 上,怎么把 I 跟 B 拼接起来呢。之前建了一个临时引用 Temp ,它就是 B 节点。再将 I 指向 Temp,也就是 I 指向了 B。
2.1.1 遍历过程:

2.2 手撸代码:
更多的访问细节,补充在注释中:
public static Node oddEven(Node head) {
// 防御性编程,刷一波啊
if(head == null || head.next == null){
return head;
}
//奇数序号链表
Node odd = head;
//偶数序号链表
Node even = head.next;
//新建 temp 引用,赋值偶数序号链表的头,用于最后拼接;
Node temp = even;
// 使用下次访问的奇偶序号 作为判空条件,目的是保证下次奇偶序号必须存在;
// 循环内已完成当前奇偶序号的指向下次,下次为空不管;
while(odd.next != null && even.next != null){
//隔位指向:当前奇数指向下个奇数序号,下个奇数序号在当前偶数序号的后面;
odd.next = even.next;
//更新下次访问的奇数序号
odd = odd.next;
// 现在的情况是:当前偶数 -> 下次奇数 —> 下次偶数;
// 隔位指向:当前偶数序号指向下个偶数序号。
even.next = odd.next;
// 更新下次访问的偶数序号
even = even.next;
}
// 奇偶拼接
odd.next = temp;
// 各节点的指向已经更新完毕!
// 返回 head,head 节点至始至终都未被赋值,还是最初的头节点;
return head;
}
下期接着聊:排序双响炮:快排 & 归并
招聘广告 🐂
【优酷】杭州团队,长期招聘!!!
- 前端「急」
- Java 后端 「爆」
- 移动端:安卓 & iOS 「热」
办公地点:蚂蚁Z空间。
面试方式:电话&视频优先。
主要有优酷少儿、创新项目等业务,P6/P7 都有。
想试一试的小伙伴,邮件联系 hdtpjhz@163.com