今天想练习下单链表反转, 实现思路不难, 没想到写代码出现这么多问题
还是基础不扎实, 必须记录问题复盘一下!
构建一个单链表类
构建一个单链表的节点类, 单链表需要有:
- 当前节点的值 (这是使用字符Character, 为了方便我通过字符串来构建一个字符链表)
- 指向下一个节点的引用
public class Node {
private Node next;
private Character value;
public Node getNext() {
return next;
}
public void setNext(Node next) {
this.next = next;
}
public Character getValue() {
return value;
}
public Node() {
}
public Node(Character value) {
this.value = value;
}
/**
* 根据字符串构造单向链表
*
* @param str
*/
public static Node build(String str) {
Node next = null;
for (int i = str.length() - 1; i >= 0; i--) {
Node node = new Node(str.charAt(i));
node.setNext(next);
next = node;
}
return next;
}
@Override
public String toString() {
final StringBuilder sb = new StringBuilder("Node{");
sb.append("value=").append(value);
sb.append(", next=").append(next);
sb.append('}');
return sb.toString();
}
}
反转代码实现
这里整理下思路: 单链表反转需要记录三个指针, last->item->next,
每次反转last<-item后, 把last,item.next整体前移一位, 直至next=null, 到达链表的末端
1. 第一次错误的的实现
第一次代码实现: 这里我犯了几个大错误! 也是对Java的引用理解不到位容易出现的错误!
有兴趣可以看下我这段代码的问题, 或者直接拉倒最后看正确的。
public static void reverse(Node node){
//单链表反转需要记录三个指针, last->item->next,
// 反转last<-item后, 把last,item.next整体前移一位
Node first = new Node();
first.setNext(node);
Node last = first;
Node item = node;
Node next = node.getNext();
while(next != null){
//反转
item.setNext(last);
//前移
last = item;
item = next;
next = next.getNext();
}
//处理尾部边界问题 last->item->next(null)
item.setNext(last);
node = item;
//还原边界
first = null;
}
存在的第一个问题
首先第一个问题, 如果你调用上面的方法, 并打印node, 将会导致内存溢出
原因在于在第一次反转后, first->node的引用没有移除, 而反转后, node的next变成了first
也就是first->next, next->first, 形成了死环
错误出现在打印时的toString()方法, 形成死循环, 导致内存溢出。
实际上边界节点first不需要指向node, 因为我们已经有item记录了首节点node的地址
所以我的第一次修改是, 而且后面还要还原边界节点 first = null;
//first.setNext(node);
first.setNext(null);
实际上这又犯了引用的第二个错误
因为first只是对new Node()的一个引用, 当第一次反转
Node last = first;
item.setNext(last);
item的next指向的是first实际记录的Node对象的地址值, 而不是first的值,
first->new Node()
item.next->first->new Node()
item.next->new Node()
我修改first=null, 并不能修改item.next的引用
first->null
item.next->new Node()
实际上边界节点可以直接是null
//初始化时首节点为边界null
Node last = null;
Node item = node;
Node next = node.getNext();
存在的第二个问题
第二个问题, 还是引用的问题
Java方法的参数传递是值传递, 这句话可能听过很多遍, 但一直没有真正的理解!
我一开始的想法是: Node是我定义的class, 所以这个反转的方法不需要返回值, 直接修改Node的值即可, 这样的思路其实没问题, 因为方法形参传递的是Node的引用, 即对象在堆中的地址值。问题出在我代码的最后一行
node = item;
这里item指向堆中的Node对象, 也就是原来链表的尾节点。
item->Node(null)
node->item->Node(null)
node->Node(null)
那么方法的调用方能不能访问这个Node(null)呢, 答案是不能!
//方法调用方
public static void main(String[] args) {
Node abcdefg = Node.build("abcdefg");
reverse(abcdefg);
System.out.println(reverse);
}
//栈中的方法
public static void reverse(Node node){
...
node = item;
}
方法在调用时, 参数的传递是值传递, 方法压栈的时候, 在栈中创建了局部变量node, 调用方把 abcdefg的值也就是 Node(“abcdefg”) 对象在堆内存中的地址值传递给了局部变量node, 此时:
abcdefg->Node(“abcdefg”)
node->abcdefg->Node(“abcdefg”)
如果我们在方法体中把item赋值给node, 则有
abcdefg->Node(“abcdefg”)
node->item->Node(null)
但要注意, 此时的abcdefg还是指向链表原来的首节点Node(“abcdefg”), 我们并不能通过这种方式在调用reverse方法后直接访问到链表的尾节点,
我们只能访问到首节点值的修改, 这里首节点的next引用已经从Node(“b”)变成了Node(null), 因为这些是在堆内存中完成的。
修改后可行的代码
public static Node reverse(Node node){
//单链表反转需要记录三个指针, last->item->next,
// 反转last<-item后, 把last,item,next整体前移一位
// 带头哨兵解决边界问题
Node last = null;
Node item = node;
Node next = node.getNext();
while(next != null){
//反转
item.setNext(last);
//前移
last = item;
item = next;
next = next.getNext();
}
//处理尾部边界问题 last->item->next(null)
item.setNext(last);
return item;
复杂度
时间复杂度O(n), 空间复杂度O(1)