编码技巧之递归【笔记】
把想法用程序写出来是很重要的
使用数学归纳法的思想来进行编程
首先要明白数学归纳法怎么用,数学归纳法是用于证明断言对所有自然数成立,首先证明对于n=1成立,然后证明n>1时:如果对于n-1成立,那么就对于n成立
那么对整个过程进行程序化我们就可以得到
递归控制
如何证明递归函数正确执行?
使用数学归纳法中的数学/自然语言,然后将其变成程序语言
递归书写的方法
严格定义递归函数作用,包括参数,返回值,side-effect
先写一般的情况,然后再写特殊的情况
每次调用必须缩小问题的规模,这是很重要的,不然就可能进入死循环
而且每次问题缩小的规模程序必须设为1
例题一:链表的创建
给出一个数组,将数组的每一个元素都生成一个节点,将节点首尾相接,还有两点,第一,链表必须以null结尾,第二,必须将第一个节点返回作为链表头
基础Node.java
代码如下:
package interview.common;
public class Node<T> {
private final T value;
private Node<T> next;
public Node(T value) {
this.value = value;
this.next = null;
}
public T getValue() {
return value;
}
public Node<T> getNext() {
return next;
}
public void setNext(Node<T> next) {
this.next = next;
}
public static <T> void printLinkedList(Node<T> head) {
while(head != null) {
System.out.print(head.getValue());
System.out.print(" ");
head = head.getNext();
}
System.out.println();
}
}
具体代码如下:
package interview.recursion;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import interview.common.Node;
public class LinkedListCreator {
/**
* 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 <T> Node<T> createLinkedList(List<T> data) {
if (data.isEmpty()) {
return null;
}
Node<T> firstNode = new Node<>(data.get(0));
firstNode.setNext(
createLinkedList(data.subList(1, data.size())));
return firstNode;
}
public Node<Integer> createLargeLinkedList(int size) {
Node<Integer> prev = null;
Node<Integer> head = null;
for (int i = 1; i <= size; i++) {
Node<Integer> node = new Node<>(i);
if (prev != null) {
prev.setNext(node);
} else {
head = node;
}
prev = node;
}
return head;
}
public static void main(String[] args) {
LinkedListCreator creator = new LinkedListCreator();
Node.printLinkedList(
creator.createLinkedList(new ArrayList<>()));
Node.printLinkedList(
creator.createLinkedList(Arrays.asList(1)));
Node.printLinkedList(
creator.createLinkedList(Arrays.asList(1, 2, 3, 4, 5)));
}
}
例题二:链表反转
将例题一的链表反转过来
我们假设除了一以外的这个链表可以正确的反转,反转以后需要将一想办法加入其中,只要将一和二的位置换一下就行了
具体代码如下:
package interview.recursion;
import java.util.ArrayList;
import java.util.Arrays;
import interview.common.Node;
public class LinkedListReverser {
/**
* Reverses a linked list.
*
* @param head the linked list to reverse
* @return head of the reversed linked list
*/
public <T> Node<T> reverseLinkedList(Node<T> head) {
// size == 0 or size == 1
if (head == null || head.getNext() == null) {
return head;
}
Node<T> newHead = reverseLinkedList(head.getNext());
head.getNext().setNext(head);
head.setNext(null);
return newHead;
}
public static void main(String[] args) {
LinkedListCreator creator = new LinkedListCreator();
LinkedListReverser reverser = new LinkedListReverser();
Node.printLinkedList(reverser.reverseLinkedList(
creator.createLinkedList(new ArrayList<>())));
Node.printLinkedList(reverser.reverseLinkedList(
creator.createLinkedList(Arrays.asList(1))));
Node.printLinkedList(reverser.reverseLinkedList(
creator.createLinkedList(Arrays.asList(1, 2, 3, 4, 5))));
System.out.println("Testing large data. Expect exceptions.");
reverser.reverseLinkedList(
creator.createLargeLinkedList(1000000));
System.out.println("done");
}
}
例题三:列出所有组合
将数组中的所有数字进行任意组合,其中包含n个数,全部输出
要点:多个参数的初始值以及side-effect的维护
具体代码如下:
package interview.recursion;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
public class Combinations {
/**
* Generates all combinations and output them,
* selecting n elements from data.
*/
public 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.print(" ");
}
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);
}
public static void main(String[] args) {
Combinations comb = new Combinations();
System.out.println("Testing normal data.");
comb.combinations(
new ArrayList<>(), Arrays.asList(1, 2, 3, 4), 2);
System.out.println("==========");
}
}
递归的缺点
简单来说就是stack,每一次递归调用都会将函数放进调用堆栈,调用的开销很大,可能导致stack overflow
这样会让时间和空间消耗比较大
同时递归本质是把一个问题分解为多个问题,很容易出现重复的运算
而且递归还存在栈溢出的情况,我们知道进程的栈容量都是有限的,递归需要堆栈,所以空间消耗要比非递归代码要大
但是也不要尝试将递归变成非递归,因为一般化的方法都是需要用到栈的,并且代码复杂,同时很难从根本上解决问题