经典面试题:单链条反转的递归与非递归方法及分析[Java]

>先说一点题外话

还是我那个面试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
}



评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值