Java 数据结构和算法 - 递归

  • 什么是递归
  • 背景:数学归纳法证明
  • 基本递归
    • printing numbers in any base
    • 它为什么有效
    • 如何工作
    • 递归太多是危险的
  • 数值应用
    • 模幂运算
    • 最大公约数
    • rsa
  • 分治算法
    • 最大连续子序列和问题
  • 动态编程

什么是递归

recursive method就是直接或者间接地调用自己的方法。许多算法都适合用递归形式表达。

背景:数学归纳法证明

下面的定理,使用其他方法也可以证明,但是,数学归纳法是最简单的:
对于任何大于等于1的正整数,前N个数的和
∑ i = 1 N i = 1 + 2 + . . . + N , 等 于 N ( N + 1 ) / 2 \sum_{i=1}^{N}i = 1+2+...+N,等于N(N+1)/2 ∑i=1N​i=1+2+...+N,等于N(N+1)/2

下面是证明过程:
很明显,如果N=1,定理是正确的。假设对于1≤N≤k是真的。那么
∑ i = 1 k + 1 i = k + 1 + ∑ i = 1 k i \sum_{i=1}^{k+1}i = k+1 + \sum_{i=1}^{k}i ∑i=1k+1​i=k+1+∑i=1k​i
根据假设,定理对于k是真的,所以
∑ i = 1 k + 1 i = k + 1 + k ( k + 1 ) / 2 \sum_{i=1}^{k+1}i = k+1 + k(k + 1) / 2 ∑i=1k+1​i=k+1+k(k+1)/2
化简
∑ i = 1 k + 1 i = ( k + 1 ) ( k + 2 ) / 2 \sum_{i=1}^{k+1}i = (k + 1)(k + 2) / 2 ∑i=1k+1​i=(k+1)(k+2)/2

基本递归

有时候,数学函数是使用递归定义的。比如,让S(N)是前N个整数的和。那么S(1) = 1,我们可以写S(N) = S(N – 1) + N。在这里,我们根据函数S的自身的小实例定义自己。

        public static long s(int n) {if (n == 1)return 1;elsereturn s(n - 1) + n;}

递归的基本规则

  • Base case:不用递归的实例
  • Make progress:任何递归都向着Base case发展

printing numbers in any base

比如我们要打印一个10进制正整数,每次打印一位。比如要打印1369,先是1,然后是3,然后是6,最后是9。
要决定最后一位很容易,因为n%10(n小于10的时候)就是。但是,前面几位怎么办?使用递归可以很容易地解决。

        public static void printDecimal(long n) {if (n >= 10)printDecimal(n / 10);System.out.print((char) ('0' + (n % 10)));}

其他进制的数字也很容易打印

        private static final String DIGIT_TABLE = "0123456789abcdef";public static void printInt(long n, int base) {if (n >= base)printInt(n / base, base);System.out.print(DIGIT_TABLE.charAt((int) (n % base)));}

完整版本是

public class PrintInt {private static final String DIGIT_TABLE = "0123456789abcdef";private static final int MAX_BASE = DIGIT_TABLE.length();private static void printIntRec(long n, int base) {if (n >= base)printIntRec(n / base, base);System.out.print(DIGIT_TABLE.charAt((int) (n % base)));}public static void printInt(long n, int base) {if (base <= 1 || base > MAX_BASE)System.err.println("Cannot print in base " + base);else {if (n < 0) {n = -n;System.out.print("-");}printIntRec(n, base);}}public static void main(String[] args) {for (int i = 0; i <= 17; i++) {printInt(1000, i);System.out.println();}printInt(0x5DEECE66DL, 10);System.out.println();}
}

它为什么有效

当设计一个递归算法的时候,我们总是假设递归调用会工作。
和其他方法一样,递归方法也是要和其他方法组合起来解决问题,不过,所谓的其他方法可能就是该递归方法以前的实例。

如何工作

Java和C++一样,也是通过使用内部堆栈的激活记录(activation record)来实现方法。激活记录包含方法的相应信息,比如参数的值和局部变量。激活记录的实际信息是和系统相关的。
使用激活记录栈,是因为方法以它们的调用顺序相反的顺序返回。一般来说,栈顶保存的是当前执行的方法。当方法G被调用,它的激活记录就被push到栈,这样G成了当前的活动方法。当方法返回,栈pop,当前的活动方法成了栈顶上的新值。

递归太多是危险的

比如斐波那契数F0, F1, … , Fi是这样定义的:F0=0, F1=1,第i个斐波那契数等于第i-1个数和第i-2个数的和,即Fi = Fi-1 + Fi-2

    public static long fib( int n ) {if(n <= 1)return n;elsereturn fib(n - 1) + fib(n - 2);}

上面这个算法就设计得不好。在我们相对比较快的机器上,计算F40需要花费大约一分钟的时间,看下图,显示了计算的轨迹。

可以看到,做了大量的重复计算。要计算ib(n),我们递归计算fib(n-1)。当这个递归调用返回以后,我们使用另一个递归调用计算fib(n-2)。但是,在计算fib(n-1)的时候,我们已经计算过fib(n-2)了,所以,再计算一次fib(n-2)就属于浪费了,是重复计算。
每调用fib(n-1)和每次调用fib(n-2)都要调用fib(n-3),这样fib(n-3)被计算了三次。更悲惨的是,每次调用fib(n-2)或者调用fib(n-3)都要调用fib(n-4),这样fib(n-4)被调用了五次。这样,我们的计算有大量的重复。
让C(N)是计算fib(n)的过程中,调用fib的次数。很明显C(0) = C(1) = 1。对于N ≥ 3,C(N) = FN+2 + FN-1 - 1。于是,对于N = 40, F40 = 102,334,155,总的调用次数超过3亿次。

所以,递归调用的时候,不要做重复的工作

操作系统保存文件一般都使用数结构或者类似树的结构。树也用来实现编译器、文本处理和搜索算法。

看一个二分搜索的例子:

        public static <AnyType extends Comparable<? super AnyType>>int binarySearch(AnyType[] a, AnyType x) {return binarySearch(a, x, 0, a.length - 1);}/*** Hidden recursive routine.*/private static <AnyType extends Comparable<? super AnyType>> int binarySearch(AnyType[] a, AnyType x, int low, int high) {if (low > high)return NOT_FOUND;int mid = (low + high) / 2;if (a[mid].compareTo(x) < 0)return binarySearch(a, x, mid + 1, high);else if (a[mid].compareTo(x) > 0)return binarySearch(a, x, low, mid - 1);elsereturn mid;}

再看一个分形的例子:

画布初始是灰色的,在画布上画白色的方格。最后一个方格画在中心位置。

import java.awt.Frame;
import java.awt.Graphics;
import java.awt.Color;public class FractalStar extends Frame {private static final int theSize = 512;public void paint(Graphics g) {setBackground(Color.gray);g.setColor(Color.white);drawSpace(g, theSize / 2 + 10, theSize / 2 + 30, theSize);}private void drawSpace(Graphics g, int xCenter, int yCenter, int boundingDim) {int side = boundingDim / 2;if (side < 1)return;int left = xCenter - side / 2;int top = yCenter - side / 2;int right = xCenter + side / 2;int bottom = yCenter + side / 2;drawSpace(g, left, top, boundingDim / 2);drawSpace(g, left, bottom, boundingDim / 2);drawSpace(g, right, top, boundingDim / 2);drawSpace(g, right, bottom, boundingDim / 2);g.fillRect(left, top, right - left, bottom - top);}// Simple test program// For simplicity, must terminate from consolepublic static void main(String[] args) {Frame f = new FractalStar();f.setSize(theSize + 20, theSize + 40);f.setVisible(true);}
}

数值应用

模幂运算

可用来实现hash表结构。
如果两个数A和B除以N的余数相同,我们就说他们同余N,写做A ≡ B (mod N)。于是

  • 如果A ≡ B (mod N),对于任何C,有A + C ≡ B + C(mod N)
  • 如果A ≡ B (mod N),对于任何D,有AD ≡ BD(mod N)
  • 如果A ≡ B (mod N),对于任何正的P,AP ≡ BP (mod N)
    比如,对于33335555(mod 10),因为3333 ≡ 3(mod 10),所以我们需要计算35555(mod 10)。因为34=81,所以,34 ≡ 1(mod 10),再1388次幂得到35552 ≡ 1(mod 10)。两边都乘以33 = 27,得到35555 ≡ 27 ≡ 7(mod 10),完成了计算。

接下来,我们看怎么高效计算XN(mod P)。比较快的算法是,如果N是偶数,那么
XN = (X ⋅ X)N/2
如果N是奇数,那么
XN = X ⋅ XN-1 = X ⋅(X ⋅ X)(N-1)/2

代码如下,我们在每个乘法之后有一个%运算。

    /*** Return x^n (mod p)* Assumes x, n >= 0, p > 0, x < p, 0^0 = 1* Overflow may occur if p > 31 bits.*/public static long power(long x, long n, long p) {if (n == 0)return 1;long tmp = power((x * x) % p, n / 2, p);if (n % 2 != 0)tmp = (tmp * x) % p;return tmp;}

最大公约数

给两个正整数A和B,求他们的最大公约数gcd(A, B),这个最大的整数D都可以被A和B整除。比如,gcd(70, 25) = 5。
我们可以很容易证明gcd(A, B) ≡ gcd(A – B, B)。因为D能被A和B整除,肯定也能被A – B和B整除。这样一直减下去,如果A小于B了,就用B-A,直到B为0,即gcd(A, 0) ≡ A,A就是答案。这个算法叫欧几里德算法,2000年前发明的。
更高效的算法是gcd(A, B) ≡ gcd(B, A mod B),这是一个递归算法。于是,gcd(70, 25) ⇒ gcd(25, 20) ⇒ gcd(20, 5) ⇒ gcd(5, 0) ⇒ 5。

    /*** Return the greatest common divisor.*/public static long gcd(long a, long b) {if (b == 0)return a;elsereturn gcd(b, a % b);}

gcd算法用来解决类似的数学问题。对于方程AX ≡ 1(mod N),A关于模N的乘法逆为X(1 ≤ X < N且1 ≤ A < N)。比如,3关于模13的乘法逆是9,即3⋅9 mod 13产生1。
计算乘法逆的能力是重要的,因为可以用来解诸如3i ≡ 7(mod 13)这样的方程。这些方程有很多应用,包括接下来要讨论的加密算法。在本例中,如果我们乘以3的逆(9),就得到i ≡ 63(mod 13),所以i = 11。如果
AX ≡ 1(mod N),
那么 AX + NY = 1(mod N),对于任何Y也是真的。对于某些Y,左边等于1。这样方程
AX + NY = 1
当A有乘法逆的时候,有解。
给定A和B,我们看怎样找到X和Y满足AX + BY = 1。我们假设0 ≤ ⎥B⎪ < ⎥A⎪,扩展gcd算法计算X和Y。首先,考虑基本情况,B ≡ 0,这样,我们不得不解AX = 1,这意味着A和X都是1。事实上,如果A不是1,就没有乘法逆。于是,只有当gcd(A, N) = 1时,A有模N的乘法逆。
然后考虑B不是0的情况。根据gcd(A, B) ≡ gcd(B, A mod B),我们让A = BQ + R(Q是商、R是余数),这样递归地调用gcd(B, R)。假设我们能递归地解BX1 + RY1 = 1,因为R = A – BQ,我们有BX1 + (A – BQ)Y1 = 1,即AY1 + B (X1 – QY1) = 1。这样,X = Y1 和 Y = X1 – (A/B)Y1是AX + BY = 1的解。

    // Internal variables for fullGcdprivate static long x;private static long y;/*** Works back through Euclid’s algorithm to find* x and y such that if gcd(a,b) = 1,* ax + by = 1.*/private static void fullGcd(long a, long b) {long x1, y1;if (b == 0) {x = 1;y = 0;} else {fullGcd(b, a % b);x1 = x;y1 = y;x = y1;y = x1 - (a / b) * y1;}}/*** Solve ax == 1 (mod n), assuming gcd( a, n ) = 1.** @return x.*/public static long inverse(long a, long n) {fullGcd(a, n);return x > 0 ? x : x + n;}

rsa

首先,消息由由字符序列组成,每个字符都是位的序列。这样,消息也是位的序列,进而解释成一系列很大的数字。所以,我们要加密很大的数字,还要对加密后的结果解密。
RSA算法从接收者决定一些常量开始。首先,随机选择两个大的素数p和q。一般至少100位长。为了方便解释,我们假设p = 127,q = 211。接下来,接收者计算N = pq和N′ = (p – 1)(q – 1),即N = 26,797, N′ = 26,460。接收者继续选择任何e > 1,使得gcd(e, N′) = 1(即e和N′互质)。假如选择了e = 13,379,接下来,d关于模N的乘法逆是e,计算结果是d = 11,099。
接收者计算出所有的常量,接下来:首先,销毁p、q和N′(只要任何一个被泄漏,安全性就受影响)。然后告诉任何想要给他发消息的人,使用e和N加密信息,接收者还保留d。要加密整数M,发送者计算Me(mod N),然后发送。假如M = 10,237,发送的值就是8422。当接收到加密的整数R,接收者就计算Rd(mod N)。对于R = 8,422,他得到原始值M = 10,237。
算法能工作,是因为选择的e、d和N满足Med = M(mod N)。N和e唯一地确定了d。这就叫公钥加密,任何想接收消息的人,都可以发布加密信息。
更快的办法是DES,很像RSA,DES是单key算法-使用相同的key加密和解密,很像门上的钥匙。问题是,单key算法需要双方共享key。怎么让一方确认另一方拥有key呢?可以使用RSA解决这个问题。一般是这样做的:
比如,Alice随机生成DES加密的key,然后使用DES加密消息。它把加密消息传给Bob。Bob要解密消息,就需要Alice使用的DES key。DES key比较短,所以Alice可以使用RSA加密DES key,然后送给Bob。Bob解密该消息,就有了DES key。

分治算法

divide-and-conquer 算法是一种有效的递归算法,由两部分组成:

  • Divide:分成递归解决的小问题
  • Conquer:根据子问题的解决,解决原始问题

最大连续子序列和问题

给定的整数(包括负数)A1、A2、…、AN,找到 ∑ k = i j A k \sum_{k=i}^{j}A_k ∑k=ij​Ak​的最大值。如果所有的整数都是负数,最大连续子序列和是1。
有几个不同复杂度的算法。一个是穷举搜索的算法,效率最差(时间复杂度是O(N3)),选择每个子序列,求最大和。

    static private int seqStart = 0;static private int seqEnd = -1;/*** Cubic maximum contiguous subsequence sum algorithm.* seqStart and seqEnd represent the actual best sequence.*/public static int maxSubSum1(int[] a) {int maxSum = 0;for (int i = 0; i < a.length; i++)for (int j = i; j < a.length; j++) {int thisSum = 0;for (int k = i; k <= j; k++)thisSum += a[k];if (thisSum > maxSum) {maxSum = thisSum;seqStart = i;seqEnd = j;}}return maxSum;}

基于每个新的子序列可以由先前的子序列在常量时间内计算出来的事实。因为我们有O(N2)个子序列,所以可以直接检查所有的子序列

    /*** Quadratic maximum contiguous subsequence sum algorithm.* seqStart and seqEnd represent the actual best sequence.*/public static int maxSubSum2(int[] a) {int maxSum = 0;for (int i = 0; i < a.length; i++) {int thisSum = 0;for (int j = i; j < a.length; j++) {thisSum += a[j];if (thisSum > maxSum) {maxSum = thisSum;seqStart = i;seqEnd = j;}}}return maxSum;}

也可以使用线性时间计算出来,只测试几个子序列

    /*** Linear-time maximum contiguous subsequence sum algorithm.* seqStart and seqEnd represent the actual best sequence.*/public static int maxSubSum3(int[] a) {int maxSum = 0;int thisSum = 0;for (int i = 0, j = 0; j < a.length; j++) {thisSum += a[j];if (thisSum > maxSum) {maxSum = thisSum;seqStart = i;seqEnd = j;} else if (thisSum < 0) {i = j + 1;thisSum = 0;}}return maxSum;}

让我们考虑分治算法。假设输入是{4, –3, 5, –2, –1, 2, 6, –2}。我们把它从中间分成两半,最大和可能出现在

  • 位于前一半
  • 位于后一半
  • 跨两个部分

我们先看第三种情况。要想避免独立考虑所有N/2个起点和N/2个终点的嵌套循环,可以使用两个连续循环代替两个嵌套循环。
这些连续循环,每个长度是N/2,组合起来只需要线性的时间。

看上图,对于前一半,计算每个数到最右边的子序列和。对于后一半,我们计算每一个到最左边的自序列和。我们可以组合这两个子序列形成跨两个部分的最大的连续子序列。和是两个子序列的和,即4 + 7 = 11。
算法的步骤是

  • 递归计算位于前一半的最大连续子序列和
  • 递归计算位于后一半的最大连续子序列和
  • 通过两个连续循环计算,最大连续子序列和开始于前一半,结束于后一半
  • 选择这三个和中最大的
    /*** Recursive maximum contiguous subsequence sum algorithm.* Finds maximum sum in subarray spanning a[left..right].* Does not attempt to maintain actual best sequence.*/private static int maxSumRec(int[] a, int left, int right) {int maxLeftBorderSum = 0, maxRightBorderSum = 0;int leftBorderSum = 0, rightBorderSum = 0;int center = (left + right) / 2;if (left == right)  // Base casereturn a[left] > 0 ? a[left] : 0;int maxLeftSum = maxSumRec(a, left, center);int maxRightSum = maxSumRec(a, center + 1, right);for (int i = center; i >= left; i--) {leftBorderSum += a[i];if (leftBorderSum > maxLeftBorderSum)maxLeftBorderSum = leftBorderSum;}for (int i = center + 1; i <= right; i++) {rightBorderSum += a[i];if (rightBorderSum > maxRightBorderSum)maxRightBorderSum = rightBorderSum;}return max3(maxLeftSum, maxRightSum,maxLeftBorderSum + maxRightBorderSum);}/*** Return maximum of three integers.*/private static int max3(int a, int b, int c) {return a > b ? a > c ? a : c : b > c ? b : c;}/*** Driver for divide-and-conquer maximum contiguous* subsequence sum algorithm.*/public static int maxSubSum4(int[] a) {return a.length > 0 ? maxSumRec(a, 0, a.length - 1) : 0;}

动态编程

像前面的斐波那契数列的问题,要想使用好递归,可以使用动态编程重写递归算法-使用非递归的办法系统地在表格中记录子问题的解决。比如下面的找零钱问题
对于硬币C1、C2、…、CN,找K美分至少需要多少枚硬币?
美分有一下几种面值:1、5、10和25(忽略不常用的50)。63美分可以由两个25美分、一个10美分和三个1美分组成,一共6枚硬币。对于美元来说,找零钱问题相对容易-我们重复使用可用的最大的硬币。我们可以证明,对于美国货币,这种方法总是最小化硬币的总数,这是贪心(greedy)算法的一个例子。
贪心算法中,每个阶段,作出的决定似乎是最佳的,不用考虑未来。当一个问题可以用贪心算法实现,我们确实很高兴-贪心算法通常符合我们的直觉,实现代码也不那么痛苦。不幸的是,有时候不能用贪心算法。如果美国有21美分面值的硬币,使用贪心算法,答案仍然是六枚,但是,最优解是三枚(都是21美分的)。
那么如何解决任意硬币组的问题呢?我们假设肯定有1分的硬币。一个简单的策略是使用下面的递归:

  • 如果能恰好找一枚硬币,就是最小解
  • 否则,对每个可能的值i,我们能分别计算i分和k-i分的最小值。然后选择最小的和所用的i

比如,让我们找63分。显然,一枚硬币是不行的。我们可以计算需要1分和62分(需要4枚)。我们递归地获得这些结果,认为他们是最佳的。如果我们分成2分和61分,递归解决产生2和4,一共6枚。我们继续尝试更多的可能,其中一部分见下图。终于,我们分成21和42分,之需要三枚硬币。最后,分成31和32分,一共需要5枚。最小值是3。

实现代码是

    // Return minimum number of coins to make change.// Simple recursive algorithm that is very inefficient.public static int makeChange(int[] coins, int change) {int minCoins = change;for (int i = 0; i < coins.length; i++)if (coins[i] == change)return 1;// No match; solve recursively.for (int j = 1; j <= change / 2; j++) {int thisCoins = makeChange(coins, j)+ makeChange(coins, change - j);if (thisCoins < minCoins)minCoins = thisCoins;}return minCoins;}

结果是正确的,但是,有很多重复计算,太浪费时间了。
替代算法是指定一个硬币来递归地减少问题。比如,对于63分,可以通过下面的方法:

  • 一个1分加上递归的62分
  • 一个5分加上递归的58分
  • 一个10分加上递归的53分
  • 一个21分加上递归的42分
  • 一个25分加上递归的38分

见下图,只有五种递归调用。

代码如下

public class MakeChange {// Dynamic programming algorithm to solve change making problem.// As a result, the coinsUsed array is filled with the// minimum number of coins needed for change from 0 -> maxChange// and lastCoin contains one of the coins needed to make the change.public static void makeChange(int[] coins, int differentCoins,int maxChange, int[] coinsUsed, int[] lastCoin) {coinsUsed[0] = 0;lastCoin[0] = 1;for (int cents = 1; cents <= maxChange; cents++) {int minCoins = cents;int newCoin = 1;for (int j = 0; j < differentCoins; j++) {if (coins[j] > cents)   // Cannot use coin jcontinue;if (coinsUsed[cents - coins[j]] + 1 < minCoins) {minCoins = coinsUsed[cents - coins[j]] + 1;newCoin = coins[j];}}coinsUsed[cents] = minCoins;lastCoin[cents] = newCoin;}}// Simple test programpublic static void main(String[] args) {// The coins and the total amount of changeint numCoins = 5;int[] coins = {1, 5, 10, 21, 25};int change = 0;if (args.length == 0) {System.out.println("Supply a monetary amount on the command line");System.exit(0);}try {change = Integer.parseInt(args[0]);} catch (NumberFormatException e) {System.out.println(e);System.exit(0);}int[] used = new int[change + 1];int[] last = new int[change + 1];makeChange(coins, numCoins, change, used, last);System.out.println("Best is " + used[change] + " coins");for (int i = change; i > 0; ) {System.out.print(last[i] + " ");i -= last[i];}System.out.println();}
}

Java 数据结构和算法 - 递归相关推荐

  1. Java数据结构和算法 - 递归

    三角数字 Q: 什么是三角数字? A: 据说一群在毕达哥拉斯领导下工作的古希腊的数学家,发现了在数学序列1,3,6,10,15,21,--中有一种奇特的联系.这个数列中的第N项是由第N-1项加N得到的 ...

  2. java数据结构与算法之顺序表与链表深入分析

    转载请注明出处(万分感谢!): http://blog.csdn.net/javazejian/article/details/52953190 出自[zejian的博客] 关联文章: java数据结 ...

  3. Java数据结构与算法(二)

    Java数据结构与算法(二) 第六章 递归 1 递归应用场景 2 递归的概念 3 递归调用机制 4 递归能解决什么样的问题 5 递归需要遵守的重要规则 6 递归-迷宫问题 6.1 迷宫问题 6.2 代 ...

  4. java数据结构与算法之双链表设计与实现

    转载请注明出处(万分感谢!): http://blog.csdn.net/javazejian/article/details/53047590 出自[zejian的博客] 关联文章: java数据结 ...

  5. Java 数据结构与算法

    目录 Java 数据结构与算法 数据结构 数据结构的定义 数据的逻辑结构 数据的物理结构 数据存储结构 数据结构的分类 线性结构 非线性结构 常用的数据结构 数组(Array) 栈( Stack) 队 ...

  6. Java数据结构和算法(第二版)

    Java数据结构和算法(第二版) 下载地址 https://pan.baidu.com/s/112D5houIgu0eMs_i5o0Ujw 扫码下面二维码关注公众号回复 100066获取分享码 本书目 ...

  7. java数据结构与算法之(Queue)队列设计与实现

    [版权申明]转载请注明出处(请尊重原创,博主保留追究权) http://blog.csdn.net/javazejian/article/details/53375004 出自[zejian的博客] ...

  8. 数据结构 - Java -韩顺平 图解Java数据结构和算法

    数据结构 Lesson 1 数据结构的知识总结 1. 几个经典的算法面试题 2. 线性结构与非线性结构 2.1 稀疏数组 sparsearray 2.2 队列 2.2.1 顺序队列: 2.2.2 环形 ...

  9. 【Java面试高频问题】Java数据结构和算法基础知识汇总

    文章目录 Java数据结构和算法基础知识 一.Java数据结构 1. 线性结构:数组.队列.链表和栈 1.1 数组(Array) 1.2 稀疏数组 1.3 队列(Queue) 1.4 链表(Linke ...

最新文章

  1. JavaScript 立即执行函数的两种写法
  2. computed vs methods
  3. python压平嵌套列表
  4. 关于抢红包的_抢红包系统设计与设计
  5. 解决Picasso在Android 5.0以下版本不兼容https导致图片不显示
  6. jmeter 加密解密_使用Jmeter对SHA1加密接口进行性能测试
  7. pycharm2020版本界面中英文注释
  8. 通过流程构建组织的【个人能力】和【团队能力】
  9. limit mongodb 聚合_mongodb-$type、limit、skip、sort方法、索引、聚合
  10. php 钉钉 免登,免登的正确使用方式
  11. 爬楼梯算法-java(递归与非递归)
  12. STM32 PWM呼吸灯程序
  13. 10 个最佳 WordPress 幻灯片插件
  14. 一人一猫旅行记之浅析序列化及原理
  15. 电驴服务器更新的作用,用电驴,这些服务器知识你必知
  16. Docker(2) 安全加密,habor仓库和Docker网络
  17. 使用office的邮件合并和文档附件制作带照片的准考证
  18. 多因素身份认证之手机推送认证
  19. 耿丹CS16-2班课堂测试作业汇总
  20. Dart学习3、数据类型详解

热门文章

  1. 专利002 | 心理咨询师的好帮手
  2. 电子邮箱如何注册短靓号?邮箱靓号如何申请
  3. pcie dma 相关知识整理(xilinx平台)
  4. 1.10 Pet技术流导论+完全1.10Pet数据
  5. 【z12】【b092】hankson的趣味问题
  6. 备忘录误删了怎么返回上一步操作
  7. 古月居深度评测Transbot ROS机器人,快来看看古月老师怎么说的吧
  8. NAT简单配置,一目了然
  9. Python-用动漫小姐姐实现马赛克拼图
  10. 使用SQL2017的Date数据格式问题