Java8实战笔记0x0a

函数式编程的技巧

无处不在的函数

高阶函数

  • 函数满足以下任一要求即可被称为告诫函数:
    • 接受至少一个函数作为参数
    • 返回结果是一个函数
Comparator<Apple> c = comparing(Apple::getWeight);

科里化

  • 科里化是指一个能将具有n个参数的函数转化为使用n个参数中部分参数的函数,该函数的返回值是另一个函数,参数为转化的函数未使用的参数,例如f(x, y) = (g(x))(y)
// 摄氏度转华氏度
static DoubleUnaryOperator curriedConverter(double f, double b) {
    return (double x) -> x * f + b;
}

DoubleUnaryOperator convertCtoF = curriedConverter(9.0/5, 32);
double c = convertCotF(25);
  • 一个函数使用所有参数仅有部分被传递时,通常称这个函数是部分应用的。

延迟计算

自定义延迟列表

interface MyList<T> {
    T head();

    MyList<T> tail();

    default boolean isEmpty() {
        return true;
    }

    default public MyList<T> filter(Predicate<T> p) {
        return isEmpty() ? this :
                p.test(head()) ?
                        new LazyList<>(head(), () -> tail().filter(p)) :
                        tail().filter(p);
    }
}

class MyLinkedList<T> implements MyList<T> {
    private final T head;
    private final MyList<T> tail;

    public MyLinkedList(T head, MyList<T> tail) {
        this.head = head;
        this.tail = tail;
    }

    @Override
    public T head() {
        return head;
    }

    @Override
    public MyList<T> tail() {
        return tail;
    }

    @Override
    public boolean isEmpty() {
        return false;
    }


}

class Empty<T> implements MyList<T> {
    @Override
    public T head() {
        throw new UnsupportedOperationException();
    }

    @Override
    public MyList<T> tail() {
        throw new UnsupportedOperationException();
    }
}

class LazyList<T> implements MyList<T> {
    final T head;
    final Supplier<MyList<T>> tail;

    public LazyList(T head, Supplier<MyList<T>> tail) {
        this.head = head;
        this.tail = tail;
    }

    @Override
    public T head() {
        return head;
    }

    @Override
    public MyList<T> tail() {
        return tail.get();
    }

    @Override
    public boolean isEmpty() {
        return false;
    }

}

public class Main {
    public static void test(Function<String, List<String>> findPrices) {
        long start = System.nanoTime();
        System.out.println(findPrices.apply("Fallout New Vegas"));
        long duration = (System.nanoTime() - start) / 1_000_000;
        System.out.println("Done in " + duration + " msecs");
    }

    public static MyList<Integer> primes(MyList<Integer> numbers) {
        return new LazyList<>(
                numbers.head(),
                () -> primes(
                        numbers.tail().filter(n -> n % numbers.head() != 0)
                )
        );
    }

    public static LazyList<Integer> from(int n) {
        return new LazyList<>(n, () -> from(n + 1));
    }
    static <T> void printAll(MyList<T> list) {
        while (!list.isEmpty()) {
            System.out.println(list.head());
            list = list.tail();
        }
    }
    public static void main(String[] args) throws Exception {
        printAll(primes(from(2)));
    }
}

没看懂

模式匹配

  • Java没有原生支持模式配配匹配,只能用if进行模拟。

剑指Offer面试题-斐波那契数列

斐波那契数列

要求输入一个整数n,请你输出斐波那契数列的第n项(从0开始,第0项为0)。

public class Q10 {
    public int Fibonacci(int n) {
        if (n < 0) return -1;
        if (n <= 1) return n;
        int current = 1, b1 = 1, b2 = 0;
        int i = 2;
        while (i <= n) {
            current = b1 + b2;
            b2 = b1;
            b1 = current;
            i++;
        }
        return current;
    }
}
  • 思路:动态规划,需要前两个结果。

跳台阶

一只青蛙一次可以跳上1级台阶,也可以跳上2级。求该青蛙跳上一个n级的台阶总共有多少种跳法(先后次序不同算不同的结果)。

public class Q10_2 {
    public int JumpFloor(int target) {
        if (target < 2) return 1;
        if (target == 2) return 2;
        int b1 = 2, b2 = 1;
        int current = 1, i = 3;
        while (i <= target) { current="b1" + b2; b2="b1;" b1="current;" i++; } return current; < code>
  • 思路:同上。

变态跳台阶

一只青蛙一次可以跳上1级台阶,也可以跳上2级……它也可以跳上n级。求该青蛙跳上一个n级的台阶总共有多少种跳法。

  • 思路:这个是数学题吧?可以把问题转化为青蛙可以以1-n次跳上台阶,求各种次数跳法之和。可以是往n个台阶之间插入挡板,有n-1个空,可插入0~n-1个挡板,则共有C(n-1, 0) + C(n-1, 1) + C(n-1, 2) + ... +C(n-1, n-1) = 2^(n-1)种方法。

剑指Offer面试题-用两个栈实现队列

用两个栈实现队列

用两个栈来实现一个队列,完成队列的PushPop操作。 队列中的元素为int类型。

public class Q9 {
    Stack<Integer> stack1 = new Stack<>();
    Stack<Integer> stack2 = new Stack<>();

    public void push(int node) {
        stack1.push(node);
    }

    private void fillStack2() {
        while (!stack1.empty()) {
            stack2.push(stack1.pop());
        }
    }
    public int pop() {
        if (stack2.empty()) {
            fillStack2();
        }
        return stack2.pop();
    }
}

  • 思路:一个栈用来入队,另一个栈用来出队。如果出队的栈为空,需要把入队的栈的值导入出队的栈。

剑指Offer面试题-二叉树的下一个结点

二叉树的下一个结点

给定一个二叉树和其中的一个结点,请找出中序遍历顺序的下一个结点并且返回。注意,树中的结点不仅包含左右子结点,同时包含指向父结点的指针。

class TreeLinkNode {
    int val;
    TreeLinkNode left = null;
    TreeLinkNode right = null;
    TreeLinkNode next = null;

    TreeLinkNode(int val) {
        this.val = val;
    }
}

public class Q8 {
    public TreeLinkNode GetNext(TreeLinkNode pNode) {
        if (pNode == null) return null;
        if (pNode.right != null) {
            TreeLinkNode l = pNode.right;
            while (l.left != null) {
                l = l.left;
            }
            return l;
        }
        TreeLinkNode t = pNode;
        while (t.next != null) {
            if (t.next.left == t) return t.next;
            t = t.next;
        }
        return null;
    }
}
  • 思路:分类讨论。如果给出节点有右孩子,从右孩子开始一直访问其左孩子,直到当前节点没有左孩子,返回当前节点。如果给出节点没有右孩子,那么如果它是其父节点的左孩子,则返回其父节点。如果不是其父节点的左孩子,则继续找父节点的父节点,直到当前节点是父节点的左孩子,返回当前节点的父节点。如果找到根节点,那么给出节点没有下一个节点。

剑指Offer面试题-重建二叉树

重建二叉树

输入某二叉树的前序遍历和中序遍历的结果,重建该二叉树,返回二叉树的根节点。假设输入的前序遍历和中序遍历的结果中都不含重复的数字。二叉树节点定义:

class TreeNode {
    public int value;
    public TreeNode left;
    public TreeNode right;
}
public class Solution {
    private static void buildTree(
            int[] preOrder, int preStart,
            int[] inOrder, int inStart, int inEnd,
            TreeNode current
    ) {
        int index;
        for (index = inStart; index <= inEnd; index++) {
            if (inOrder[index] == current.value) break;
        }
        int leftLength = index - inStart;
        int rightLength = inEnd - index;
        if (leftLength > 0) {
            TreeNode left = new TreeNode(preOrder[preStart + 1]);
            current.left = left;
            buildTree(
                    preOrder, preStart + 1,
                    inOrder, index - leftLength, index - 1,
                    left
            );
        }
        if (rightLength > 0) {
            TreeNode right = new TreeNode(preOrder[preStart + leftLength + 1]);
            current.right = right;
            buildTree(
                    preOrder, preStart + leftLength + 1,
                    inOrder, index + 1, index + rightLength,
                    right
            );
        }
    }
    public static TreeNode reConstructBinaryTree(int[] preOrder, int[] inOrder) {
        if (preOrder == null || preOrder.length == 0) return null;
        TreeNode root = new TreeNode(preOrder[0]);
        buildTree(
                preOrder, 0,
                inOrder, 0, inOrder.length - 1,
                root
        );
        return root;
    }
}
  • 思路:前序遍历的第一个元素就是当前树的根节点,在中序遍历中找到当前节点的位置就能算出左右序列长度,从而进行递归。

剑指Offer面试题-从尾到头打印链表

从尾到头打印链表

输入一个链表的头节点,从尾到头反过来打印出每个节点的值。链表定义如下:

class Node {
    public int value;
    public Node next;
}
class Node {
    public int value;
    public Node next;

    public Node(int value, Node next) {
        this.value = value;
        this.next = next;
    }
}
public class Q6 {
    public static void solution(Node head) {
        if (head != null) {
            if (head.next != null)
                solution(head.next);
            System.out.print(head.value + " ");
        }
    }
}
// Test
class Q6Test{
    private static final ByteArrayOutputStream outContent = new ByteArrayOutputStream();
    private static final ByteArrayOutputStream errContent = new ByteArrayOutputStream();
    private static final PrintStream originalOut = System.out;
    private static final PrintStream originalErr = System.err;

    @BeforeAll
    public static void setUpStreams() {
        System.setOut(new PrintStream(outContent));
        System.setErr(new PrintStream(errContent));
    }

    @AfterAll
    public static void restoreStreams() {
        System.setOut(originalOut);
        System.setErr(originalErr);
    }

    @Test
    void solution() {
        Node n1 = new Node(1, null);
        Node n2 = new Node(2, n1);
        Node n3 = new Node(3, n2);
        Node n4 = new Node(4, n3);
        Node n5 = new Node(5, n4);
        Q6.solution(n5);
        assertEquals("1 2 3 4 5 ", outContent.toString());
        outContent.reset();
        Q6.solution(n1);
        assertEquals("1 ", outContent.toString());
        outContent.reset();
        Q6.solution(null);
        assertEquals("", outContent.toString());
    }
}
  • 思路:使用栈达到先入后出的效果,同样可以使用递归。

Java8实战笔记0x09

函数式思考

实现和维护系统

共享可变数据

  • 如果一个方法既不修改它的内嵌类的状态,也不修改其他对象的状态,使用return返回所有的计算结果,那么称其为纯粹的或无副作用的。
  • 副作用:是指函数效果已经超出了函数自身范畴,包括:
    • 除了构造器内的初始化操作,对类中数据结构的任何修改,包括字段的赋值操作
    • 抛出一个异常
    • 进行输入/输出操作,比如向一个文件写数据

声明式编程

  • 命令式编程:描述如何做的编程风格,适合经典的面向对象编程,因为它的特点是它的指令和计算机底层的词汇非常接近,比如赋值、条件分支以及循环。
  • 声明式编程:描述要做什么的编程风格,由程序员来制定规则,给出希望实现的目标,让系统来决定如何实现这个目标。它带来的好处非常明显,用这种方式编写的代码更加接近问题陈述。

什么是函数式编程

函数式Java编程

  • 函数式的函数或方法都只能修改本地变量。除此之外,它引用的对象都应该是不可修改的对象。
  • 函数式的函数或方法不应该抛出任何异常。取而代之,可以使用Optional<T>
  • 如果函数或方法调用的库函数如果有副作用,那么必须设法隐藏这些非函数式的行为,否则就不能调用这些方法。

引用透明性

  • 引用透明性是指,如果一个函数只要传递同样的参数值,总是返回同样的结果,那么这个函数就是引用透明的。Random#nextInt就不是此类函数。在函数式编程中应该选择使用引用透明的函数。

Java8实战笔记0x08

CompletableFuture:组合异步编程(二)

对多个异步任务进行流水线操作

class Shop {
    private Random random = new Random();
    private String name;

    public Shop(String name) {
        this.name = name;
    }

    public String getName() {
        return name;
    }

    public static void delay() {
        try {
            Thread.sleep(1000L);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
    }

    private double calculatePrice(String product) {
        delay();
        return random.nextDouble() * product.charAt(0) + product.charAt(1);
    }

    // 同步方法
    public String getPrice(String product) {
       double price = calculatePrice(product);
       Discount.Code code = Discount.Code.values()[random.nextInt(Discount.Code.values().length)];
       return String.format("%s:%.2f:%s", name, price, code);
    }
}

class Discount {
    public enum Code {
        NONE(0), SILVER(5), GOLD(10), PLATINUM(15), DIAMOND(20);

        private final int percentage;

        Code(int percentage) {
            this.percentage = percentage;
        }
    }

    public static String applyDiscount(Quote quote) {
        return quote.getShopName() + " price is " + Discount.apply(quote.getPrice(), quote.getDiscountCode());
    }

    public static double apply(double price, Code code) {
        Shop.delay();
        return price * (100 - code.percentage) / 100;
    }
}

class Quote {
    private final String shopName;
    private final double price;
    private final Discount.Code discountCode;

    public Quote(String shopName, double price, Discount.Code discountCode) {
        this.shopName = shopName;
        this.price = price;
        this.discountCode = discountCode;
    }

    public static Quote parse(String s) {
        String[] split = s.split(":");
        String shopName = split[0];
        double price = Double.parseDouble(split[1]);
        Discount.Code discountCode = Discount.Code.valueOf(split[2]);
        return new Quote(shopName, price, discountCode);
    }

    public String getShopName() {
        return shopName;
    }

    public double getPrice() {
        return price;
    }

    public Discount.Code getDiscountCode() {
        return discountCode;
    }
}
public class Main {
    private static final List<Shop> shops = Arrays.asList(
            new Shop("BestPrice"),
            new Shop("LetsSaveBig"),
            new Shop("MyFavoriteShop"),
            new Shop("BuyItAll"),
            new Shop("Steam"),
            new Shop("Epic"),
            new Shop("GOG"),
            new Shop("Taptap"),
            new Shop("W"),
            new Shop("V"),
            new Shop("Z"),
            new Shop("Y"),
            new Shop("X")
    );
    private static final Executor executor = Executors.newFixedThreadPool(
            Math.min(shops.size(), 100),
            r -> {
                Thread t = new Thread(r);
                t.setDaemon(true);
                return t;
            });

   public static List<String> findPrices(String product) {
       return shops.stream()
               .map(shop -> shop.getPrice(product))
               .map(Quote::parse)
               .map(Discount::applyDiscount)
               .collect(Collectors.toList());
   }

   public static List<String> findPricesByFuture(String product) {
       List<CompletableFuture<String>> priceFuture = shops.stream()
               .map(shop -> CompletableFuture.supplyAsync(() -> shop.getPrice(product), executor))
               .map(future -> future.thenApply(Quote::parse))
               .map(future -> future.thenCompose(quote -> CompletableFuture.supplyAsync(
                       () -> Discount.applyDiscount(quote), executor
               )))
               .collect(Collectors.toList());
       return priceFuture.stream()
               .map(CompletableFuture::join)
               .collect(Collectors.toList());
   }

    public static void test(Function<String, List<String>> findPrices) {
        long start = System.nanoTime();
        System.out.println(findPrices.apply("Fallout New Vegas"));
        long duration = (System.nanoTime() - start) / 1_000_000;
        System.out.println("Done in " + duration + " msecs");
    }

    public static void main(String[] args) throws Exception {
        test(Main::findPrices);
        test(Main::findPricesByFuture);
    }
}
/* Output
[BestPrice price is 130.6535, LetsSaveBig price is 77.87200000000001, MyFavoriteShop price is 113.27, BuyItAll price is 152.532, Steam price is 113.49600000000001, Epic price is 115.32, GOG price is 106.46, Taptap price is 142.44299999999998, W price is 104.76, V price is 132.297, Z price is 136.6575, Y price is 99.2085, X price is 95.60700000000001]
Done in 26058 msecs
[BestPrice price is 126.36, LetsSaveBig price is 94.728, MyFavoriteShop price is 149.43, BuyItAll price is 107.01, Steam price is 83.936, Epic price is 153.5485, GOG price is 142.53300000000002, Taptap price is 99.2275, W price is 97.064, V price is 104.976, Z price is 87.40549999999999, Y price is 138.051, X price is 140.409]
Done in 2007 msecs
*/
  • thenApply:支持可以采用同步操作的过程,不会带来太多延迟。在上面代码中,thenApply方法将Stream中的每个CompletableFuture<String>对象转换为对应的CompletableFuture<Quote>对象。
  • thenCompose:支持异步执行过程,允许对两个异步操作进行流水线,第一个操作完成时,将其结果作为参数传递给第二个操作。

将两个CompletableFuture对象整合起来,无论它们是否存在依赖

  • thenCombine:可以将两个完全不相干的CompletableFuture对象的结果整合起来。它接受名为BiFunction的第二参数,这个参数定义了当两个CompletableFuture对象完成计算后,结果如何合并。
Future<Double> futurePriceInUSD = CompletableFuture.supplyAsync(
    () -> shop.getPrice(product)
).thenCombine(CompletableFuture.supplyAsync(
    () -> exchangeService.getRate(Money.EUR, Money.USD)
), (price, rate) -> price * rate);

相应CompletableFuture的completion事件

  • thenAccept:将CompletableFuture返回的结果作为参数,并定义如何处理该结果。一旦CompletableFuture计算得到结果,它就返回一个CompletableFuture<Void>
  • CompletableFuture.allOf:是一个工厂方法,接收一个CompletableFuture构成的数组,数组中的所有CompletableFuture对象执行完成后,它返回一个CompletableFuture<Void>对象。
  • CompletableFuture.anyOf:是一个工厂方法,接收一个CompletableFuture构成的数组,返回第一个执行完毕的CompletableFuture对象的返回值构成的CompletableFuture<Object>

剑指Offer面试题-二维数组中的查找

二维数组中的查找

在一个二维数组中(每个一维数组的长度相同),每一行都按照从左到右递增的顺序排序,每一列都按照从上到下递增的顺序排序。请完成一个函数,输入这样的一个二维数组和一个整数,判断数组中是否含有该整数。

public class Q4 {
    public static boolean solution(int[][] arr, int target) {
        if (arr == null || arr.length == 0 || arr[0] == null || arr[0]. length == 0)
            return false;
        if (arr[0][0] > target || arr[arr.length - 1][arr[0].length - 1] < target) return false;
        int column = arr[0].length - 1, row = 0;
        while (column >= 0 && row < arr.length) {
            if (arr[row][column] == target) return true;
            else if (arr[row][column] > target) column--;
            else row++;
        }
        return false;
    }
}
// Test
class Q4Test {

    @Test
    void solution() {
        int[][] arr = {
                {1, 2, 8, 9},
                {2, 4, 9, 12},
                {4, 7, 10, 13},
                {6, 8, 11, 15}
        };
        assertTrue(Q4.solution(arr, 7));
        assertTrue(Q4.solution(arr, 11));
        assertTrue(Q4.solution(arr, 4));
        assertTrue(Q4.solution(arr, 1));
        assertTrue(Q4.solution(arr, 15));
        assertFalse(Q4.solution(arr, 0));
        assertFalse(Q4.solution(arr, 20));
        assertFalse(Q4.solution(arr, 3));
        assertFalse(Q4.solution(null, 0));
        assertFalse(Q4.solution(new int[0][0], 5));
        assertFalse(Q4.solution(new int[1][0], 2));
    }
}

在二维数组中查找

  • 思路:从右上角开始,如图,由于9大于7,那么第四列所有的数都大于7,因此可以排除第四列。以此类推。如果当前值小于目标值,例如第二列的2小于7,那么2右面的所有数都小于7,因此可以排除第二行。

剑指Offer面试题-数组中的重复数字

数组中的重复数字

找出数组中重复的数字

在一个长度为n的数组里的所有数字都在0n-1的范围内。 数组中某些数字是重复的,但不知道有几个数字是重复的。也不知道每个数字重复几次。请找出数组中任意一个重复的数字。 例如,如果输入长度为7的数组{2, 3, 1, 0, 2, 5, 3},那么对应的输出是第一个重复的数字2

public class Q3 {
    public static int solution(int[] arr) {
        int i = 0;
        if (arr == null) return -1;
        for (int value : arr) {
            if (value < 0 || value > arr.length - 1) return -1;
        }
        while (i < arr.length) {
            if (arr[i] != i) {
                if (arr[i] == arr[arr[i]]) return arr[i];
                int t = arr[i];
                arr[i] = arr[arr[i]];
                arr[t] = t;
            } else {
                i++;
            }
        }
        return -1;
    }
}
// Test
class Q3Test {

    @Test
    void solution() {
        assertEquals(2, Q3.solution(new int[]{2, 3, 1, 0, 2, 5, 3}));
        assertEquals(-1, Q3.solution(new int[0]));
        assertEquals(-1, Q3.solution(null));
        assertEquals(-1, Q3.solution(new int[]{0, 1, 2, 3}));
        assertEquals(-1, Q3.solution(new int[]{1, 2, 3}));
    }
}
  • 思路:从0号位开始,将值和下标对应起来,例如arr[0] == 2,那么就看arr[2]是不是2,是则2是其中一个重复数字,否则交换。如果当前位置下标和当前值相等就向后移动。
  • 时间复杂度O(n),空间复杂度O(1)

不修改数组找出重复的数字

在一个长度为n + 1的数组里的所有数字都在1n的范围内。 数组中至少有一个数字是重复的,。请找出数组中任意一个重复的数字,但不修改数组。 例如,如果输入长度为8的数组{2, 3, 5, 4, 3, 2, 6, 7},那么对应的输出是重复的数字23

public class Q3_2 {
    public static int solution(int[] arr) {
        if (arr == null) return  0;
        int left = 1;
        int right = arr.length - 1;
        int big = 0, eq = 0, small = 0;
        int mid;
        while (left < right) {
            mid = left + (right - left >> 1);
            for (int value : arr) {
                if (value >= left && value <= right) {
                    if (value == mid) eq++;
                    else if (value < mid) small++;
                    else big++;
                }
            }
            if (eq > 1) return mid;
            if (small > big) right = mid;
            else left = mid + 1;
        }
        return 0;
    }
}
// Test
class Q3_2Test {

    @Test
    void solution() {
        int[] arr = {2, 3, 5, 4, 3, 2, 6, 7};
        assertThat(Q3_2.solution(arr), anyOf(equalTo(2), equalTo(3)));
        assertArrayEquals(new int[]{2, 3, 5, 4, 3, 2, 6, 7}, arr);
        // 无重复
        arr = new int[]{0, 1, 2};
        assertEquals(Q3_2.solution(arr), 0);
        // null
        assertEquals(Q3_2.solution(null), 0);
    }
}
  • 思路:针对值范围进行二分查找。例如1-n,中间值为mid,先遍历数组,分别找出小于mid的个数small、等于mid的个数eq、大于mid的个数big。如果eq大于1,那说明为mid的元素有两个,即找到结果。否则,看smallbig哪一个更大,则重复值在哪边。
  • 时间复杂度O(nlogn),空间复杂度O(1)