Java-自学-9
链表&应用(递归思想)
链表的引入
说链表之前,不得不先提到一个词——recursion,也就是所谓的递归。什么是递归呢,可以理解成套娃吧,在一个方法中调用自己大概就可以算作是递归了。那么链表和递归有什么关系呢?
我们先不说链表,我们看一段语句:
public class IntList_new{
public int first;
public IntList_new rest;
/** construtor 1 */
public IntList_new(int f, IntList_new r){
first = f;
rest = r;
}
public static void main(String[] args){
IntList_new L = new IntList_new(200, null);
L = new IntList_new(250, L);
L = new IntList_new(350, L);
L = new IntList_new(450, L);
首先,我们有一个构建器,定义了俩东西,一个是f,对应着整个类中的first;另一个是r,对应着整个类中的rest。可以看到first的类型是int这没什么值得注意的,而rest的类型是IntList_new这个类,也就是rest这一个单元也具有着IntList_new这一性质。
那么在mian函数中,我们构造了这样一串东西,首先,创建一个类型为IntList_new的实例L,其等于IntList_new(200, null);通过这个语句,我们做出来了这么个东西:
L是一个指针,指向实例IntList(200, null);平平无奇对吧?我们看下一个语句:
L = new IntList_new(250, L);
我们重新将指针L指向一个新的实例IntList_new(250, L);我们看到,该语句将L传递给了rest。所以我们新的指针,相当于将原有的实例给了新的实例中的rest,此时我们得到了一个扩充的表格,如下图所示:
注意,我们最开始定义的L,反而被推到了第二个
OK,那么接下来的代码全部执行下去我们最终得到的结果如下图所示:
L作为指针指向一个实例化的链表,而链表之所以称之为链表,就是它们如上图所示,首尾相连,形成一种独特的结构。
链表的应用(递归的思想)
OK,我们来创建一些函数来探究一下链表的性质吧
1. 我们想得到链表的长度size():
如果我们要得到链表的长度,有一种办法就是我们设定一个指针,指向链表中的第一项,之后一步一步后移指针(根据rest中存储的地址直接访问当前表格后与之链接的下一个表格)当最后指针指向null时,表示我们遍历完整了这个链表,最终就可以返回链表的长度。如果我们对上图中的链表使用size()函数,我们希望它返回的结果是4。
我们首先先用最为朴素的方式尝试解决这个问题:
public int size(){
IntList_new p = this; //create a pointer
int totalsize = 0; //create a counter to calculate the length
while(p != null){ //p != null means after "this" box, we have another box, the link not end
totalsize += 1;
p = p.rest; //指针的值复制到当前的rest中存储指向下一个链表的地址
}
return totalsize; //finally return total length of this link.
但是我们能不能有一种更为简便的方法来设计这个函数呢?
答案是有的,那就是使用递归:
递归很有意思,也就是“我调用我自己”。但是调用自己的前提是当我们使用递归函数时,我们一定要有一个base case,什么意思呢,就是我们需要一个最终最终,函数执行的动作。光看文字可能没有感觉,看一段代码吧:
public int size(){
/** base case */
if(this.rest == null){
return 1;
}
return 1 + this.rest.size();
}
这里面的base case就是if循环中的语句,而当rest != null时,我们的返回值要在1的基础上加上对size()函数递归多次的结果,而递归的终点就是this.rest == null
2. 我们想得到链表中存储的内容(IntList.first)
还用我们之前创建的链表:
我们假设一个函数get()具有这样的功能:当我们输入L.get(0)时,其将返回链表中第一个first值——450,get(1)即返回3500,以此类推
关于这个函数,也是一样可以运用递归和非递归实现:
非递归实现:
public int get(int i){
IntList_new p = this; //create a pointer
int get_number = 0; //this is our return value
if(i == 0){ //assume the input i = 0, we'll return the value L.first
get_number = p.first;
}
while(i != 0){ //while, if i != 0, we have to change get_number to catch the next box after "this" box.also we change our pointer to p.rest.rest for the next step.
get_number = p.rest.first;
p.rest = p.rest.rest;
i -= 1; //we decrease i to open determine whether we begin the next loop
}
return get_number; //as a result, we return the value we want
}
我们使用递归的方式又该如何构建代码呢?
我们还是一样需要找到base case,我们想到最终肯定是要把i递减至0最后输出结果,所以我们的base case就是i = 0时响应应该是什么。如果i = 0我们应该是将链表中第一个表格中的first返回。在i != 0时,我们要返回的值要通过L.rest再使用get函数,但此时,get函数中的值不再是i,而应该是i - 1,因此递归的思路大致也就出来了:
public int get(int i){
IntList_new p = this;
int get_number = 0;
/** base case */
if (i == 0){
return get_number = p.first;
}
return get_number = p.rest.get(i - 1);
}
3. 对链表做一些计算(老师留的两个练习)
也就是老师给的课堂练习:
-
/** Returns an IntList identical to L, but with
- each element incremented by x. L is not allowed
- to change. */
我们观察到:**L is not allowed to change.**说明我们一定要用实例,而不能用指针解决问题,因为如果是指针的话,L也会随之改变!因此我们需要采用new IntList_new()语句生成一个新的实例化。
再分析第二个事,我们如果想用递归,那么base case是什么?
自然会想到是L == null时,我们直接返回null
那么 L != null时,我们应该怎么做呢?
继续我们的实例化思想,我们要返回一个实例new IntList_new(),但是其中的参数如何设定?
first项,我们希望使其增加x,所以第一项为L.first + x
rest项,我们希望指向下一个表格(L != null时),并且对下一个表格使用相同的方法incrList(),但是其中的参数为L.rest以及x。所以代码如下:
public static IntList_new incrList(IntList_new L, int x) {
/* Your code here. */
if (L == null){
return null;
}else{
return new IntList_new(L.first + x, incrList(L.rest, x));
}
}
-
/** Returns an IntList identical to L, but with
- each element incremented by x. Not allowed to use
- the ‘new’ keyword. */
注意到Not allowed to use the ‘new’ keyword.,因此我们不得不使用指针,L也无法避免的会被改变。整体思路基本和1变化不大,代码如下:
public static IntList_new dincrList(IntList_new L, int x) {
/* Your code here. */
IntList_new p = L;
if(L == null){
return null;
}else{
L.first = L.first + x;
L.rest = dincrList(L.rest, x);
return L;
}
}
这里我们注意到了,由于不能使用new,我们再else语句中只能分别对L.first、L.rest赋值,所以一定会对L有改变。
那么我们来看一下结果,首先我们再main函数中设定这四行代码:
public static void main(String[] args) {
IntList_new L = new IntList_new(5, null);
L.rest = new IntList_new(7, null);
L.rest.rest = new IntList_new(9, null);
/** use incrList method */
/** L was not changed */
IntList_new Q = incrList(L, 20);
System.out.println(Q.first);
System.out.println(L.first);
/** use incrList method */
/** L was changed! */
IntList_new M = dincrList(L, 20);
System.out.println(L.first);
System.out.println(M.first);
主要观察L的变化结果:
当我们调用incrList时,L未发生变化,为5
当我们调用dincrList时,L随M发生相同变化,为25