>先说一点题外话
还是我那个面试JAVA工程师的朋友,他问了我一个让我很蛋疼的问题:"Java没有指针,怎么写链表。"
我:囧....哥们你真的是去做JAVA工程师不是去做BUG工程师的么。
我想很多人和他一样可能会有这种疑问,在习惯了C语言中的指针之后,突然听说JAVA取消了指针,一定是一脸懵逼的:没有指针我怎么写链表。
其实我觉得这部分同学,你可能对C语言的指针有着莫大的误会。
C语言指针的本质,其实是一种引用,或者说引用本身也就是指针。C++老师一定对你们说过,引用是一个变量的别名。听起来好像这个别名不占空间,和指针完全不一样。其实不然,引用在处理上是一个const指针,也是要占空间的。
但无论是指针还是引用,其最最根源的本质是内存,是内存地址。
JAVA虽然丢弃了指针,但并没有说它丢弃了引用啊。而且你只要看了上面两行文字,就会觉得引用和指针二者留一个就行了,反正他们的作用就是对地址进行操作而已。相比于引用,指针在代码阅读性上更糟糕,所以丢了就丢了吧,反正也没必要。
那么,怎么用JAVA实现链表呢?
>单链表问题
很巧,我这位朋友还问了我一个问题:
如何用尽量少的额外空间开支和N级别算法,实现一个链表的反转?要求分别用递归和非递归方法实现。
首先,我们用java定义这么一个类:
class LinkNode{
public int value;
public LinkNode next;//引用
}
是不是看着和C++或者C语言里的结构体看着差不多:
struct LinkNode{
int value;
LinkNode* next;//指针
}
关于JAVA的引用,我改天会写个详细的博客。
现在只要知道:原始数据按值传递、对象按引用传递(你 new出来的就叫对象)。(String 有点特殊,需要另外介绍)
>递归
所以,递归单链表反转的算法就可以这么写了:
private static LinkNode recursivelyReverse1(LinkNode p,LinkNode head){
if(p==null)return head;
//当找到最后一个节点时
if(p.next==null){
head=p;//找到链表的末尾,设定其为反转链表的头节点
return head;
}
head = recursivelyReverse1(p.next,head);//递归
//p此时是从到倒二个点起
p.next.next=p;//反转
p.next=null;
return head;
}
1.不断递归,直到找到最后一个节点,把这个节点设为头结点,返回给上一层;
2.上一层中的p,此时是下一层所操作节点p的前一个节点,把这个节点的下一个节点的next引用设为其本身,实现反转,同时,把这个节点的next设为null;
3.重复这个步骤,直到递归完成。
很简单。没什么大的难度。现在分析一下它的复杂度和额外空间占用,不考虑函数栈和引用的大小,很显然,分别为N、0;
实际上进行分析后,上面这个函数还可以写得更精简一点:
private static LinkNode recursivelyReverse2(LinkNode p){
if(p==null || p.next==null)return p;
LinkNode head = recursivelyReverse2(p.next);//递归
p.next.next=p;//反转
p.next=null;
return head;
}
没想到吧,短到只有5行的函数体。惊不惊喜,开心不开心。哪怕硬背都能背得下来了。
>非递归
非递归的实现方法很多,我简单设计了一个算法如下:
private static LinkNode Reverse(LinkNode head){
if(head==null)return head;//空链返回
LinkNode save=head,cur=head.next;
//head.next=null;//可有可无
while(cur!=null){
save.next=cur.next;//保存数据//会洗掉save.next,置为null
cur.next=head;//反转
head=cur;//head向后移动
cur=save.next;//cur向后移动
}
return head;
}
主要的思路是:
1.两个指针,head与cur,开始时分别指向链表的头两个,每次运行完后向后移动一格。开始时直接把head.next置为null(可有可无,只是便于理解,反正最后会洗掉)。
2.每次循环进行反转操作,具体来说就是cur.next=head,也就是后面的节点指向前面的节点。
为什么要用一个save.next的额外引用空间呢?非递归的最大头疼点在于交换值的时候,必须要有一个中间变量,其意义就如同以下代码:
void cgInt(int a,int b){
int t=a;//t是中间变量
a=b;
b=t;
}
这么一说你应该也就明白了。
我为什么说head.next=null可有可无呢?因为当while循环循环到最后一个节点,cur.next==null,又save.next=cur.next,所以原来的头结点被save引用,使得原来的头结点自然变成了末尾的节点,其next的值为null。
分析一下这个算法,不算额外的引用空间,大小及时间复杂度和递归的一样,但是如果算上引用空间,非递归的引用空间占用得少得多,优于递归的。
完整测试样例代码:
class LinkNode{
public int value;
public LinkNode next;
}
public class LinkReverse {
private static LinkNode origin=new LinkNode();
public static void main(String args[]){
init();
print(origin);
origin=recursivelyReverse1(origin,origin);
print(origin);
origin=recursivelyReverse2(origin);
print(origin);
origin=Reverse(origin);
print(origin);
}
//递归实现之1
private static LinkNode recursivelyReverse1(LinkNode p,LinkNode head){
if(p==null)return head;
//当找到最后一个节点时
if(p.next==null){
head=p;//找到链表的末尾,设定其为反转链表的头节点
return head;
}
head = recursivelyReverse1(p.next,head);//递归
//p此时是从到倒二个点起
p.next.next=p;//反转
p.next=null;
return head;
}
//递归实现之2 (上一个 递归函数的精简版)
private static LinkNode recursivelyReverse2(LinkNode p){
//空链返回 或 移动p标记到了末尾
if(p==null || p.next==null)return p;
LinkNode head = recursivelyReverse2(p.next);//递归
//第一次返回时head是列表末的节点 且p.next=head
p.next.next=p;//反转
p.next=null;
return head;
}
//非递归实现
private static LinkNode Reverse(LinkNode head){
if(head==null)return head;//空链返回
LinkNode save=head,cur=head.next;
//head.next=null;//可有可无,反正会由save.next=cur.next;洗掉
while(cur!=null){
save.next=cur.next;//保存数据//会洗掉save.next,置为null
cur.next=head;//反转
head=cur;//head向后移动
cur=save.next;//cur向后移动
}
return head;
}
//初始化
private static void init(){
origin.value=10;
LinkNode p=origin;
for(int i=1;i<10;i++){
LinkNode temp=new LinkNode();
temp.value=i+10;
p.next=temp;
p=temp;
}
}
private static void print(LinkNode l){
while(l!=null){
System.out.print(l.value+" ");
l=l.next;
}System.out.println();
}
//By @Shenpibaipao : http://blog.csdn.net/shenpibaipao
}