最近在看谷歌面试官讲解的视频,特来分享一波~ 这个系列主要是讲编程思想(或者说技巧),主要包括:递归控制、循环控制、边界控制和一些数据结构的知识。这一节主要说下递归。
大佬们讲课很深奥,首先说了下数学归纳法,并用典型的求和的递推公式:首项加尾项乘以项数除以2,即n(n+1)/2讲解了数学归纳法,这也是递归思想的一部分理论基础。朋友们可以看看这些思想~ 下面说了递归的知识:
(1). 如何保证递归函数正确执行?
使用数学归纳法/自然语言,推纳出来之后转化为程序语言。
(2). 递归书写方法(套路)
- 严格定义递归函数作用,包括参数,返回值,side-effect
- 先一般,后特殊
- 每次调用必须缩小规模
- 每次问题规模缩小程度必须为1
根据上述套路我们就可以写出一个递归函数,下面举例说明:
例子1:链表创建,返回头节点,链表尾部指向null
描述:使用java创建链表。
ps:面试喜欢问链表是因为,链表容易理解但是代码难写。
下面java代码就用递归实现一个链表:
如图:
首先定义一个Node类,这个很好理解,每个Node包括节点值value,和下一个节点。
public class Node {
private final int value;
private Node next;
public Node(int value){
this.value=value;
this.next=null;
}
public int getValue() {
return value;
}
public Node getNext() {
return next;
}
public void setNext(Node next) {
this.next = next;
}
public static void printLinkedList(Node head){
while (head!=null){
System.out.print(head.getValue());
System.out.print(" ");
head=head.getNext();
}
System.out.println();
}
}
下面使用递归思想实现链表创建:
public class LinkedListCreator {
//1.严格定义递归函数作用,包括参数,返回值,side-effect
/**
* Creates a linked list.
* @param data the data to create the list
* @return head of the linked list. The returned linked list
* ends with last node with getNext() == null.
*/
public static Node createLinkedList(List<Integer> data){
//2.先一般,后特殊。递归退出条件
if (data.isEmpty()){
return null;
}
Node firstNode = new Node(data.get(0));
//每次调用必须缩小规模,每次问题规模缩小程度为1
Node headOfSubList = createLinkedList(data.subList(1,data.size()));
firstNode.setNext(headOfSubList);
return firstNode;
}
}
链表的创建比较简单,使用递归几行代码就搞定了。下面看下链表反转的例子。
例子2:链表反转,将上述链表反转
描述:将一个链表反转。
实现如图:
主要思想也是使用递归,不断缩小反转的链表长度,最后把第一个元素指向null,并把每次递归的第一个元素的next节点指向第一个节点就ok了
public class LinkedListReverse {
/**
* Reverses a linked list.
* @param head the linked list to reverse
* @return head of the reversed linked list
*/
public Node reverseLinkedList(Node head){
//size=0 or size=1 递归退出条件
if (head==null||head.getNext()==null){
return head;
}
//递归调用,每次都缩小规模
Node newHead = reverseLinkedList(head.getNext());
//把每次递归的第一个元素的next节点指向第一个节点
head.getNext().setNext(head);
//第一个元素指向null
head.setNext(null);
return newHead;
}
}
递归反转使用递归也很容易理解啊!之前想破脑袋写了一大堆的代码还错了,使用递归确实代码异常简介,而且一目了然。
例子3:列出给定数列的所有组合
描述:给定一个任意长度的数列,求出该数列使用n个元素能组成的所有子集。
如图:第一个参数为数列的元素集合,第二个n为子集的长度。[1,2,3,4]中使用2个元素能组成的所有可能:
使用递归的实现思路如下:
combinations([1,2,3,4],2)
选1(即数组第0个元素)–>调用combinations([2,3,4],1)
不选1—>调用combinations([2,3,4],2)
代码实现:
/**
* /**
* Generates all combinations and output them,
* selecting n elements from data.
* @param data
* @param n
*/
public static void combinations(List<Integer> selected, List<Integer> data,int n){
if (n==0){
//output all selected elements
for (Integer i : selected){
System.out.print(i);
System.out.println(" ");
}
System.out.println();
return;
}
if (data.isEmpty()){
return;
}
//select element 0
selected.add(data.get(0));
combinations(selected,data.subList(1,data.size()),n-1);
//un-select element 0
selected.remove(selected.size()-1);
combinations(selected,data.subList(1,data.size()),n);
}
}
使用递归的好处想必很明显了,代码简洁明了,方便理解,也方便编码,缺点就是stack的压力太大,效率很低,时间空间开销都很大。下一节使用循环解决这个问题~
以上递归算法小伙伴们可以自己调试,方便大家理解递归的运行过程