JS递归算法随笔

276 阅读3分钟

持续创作,加速成长!这是我参与「掘金日新计划 · 6 月更文挑战」的第16天

简单了解一下迭代,循环,遍历,递归的区别

  • 循环(loop):指的是在满足条件的情况下,重复执行同一段代码。比如,while语句。 循环则技能对应集合,列表,数组等,也能对执行代码进行操作。

  • 迭代(iterate):指的是按照某种顺序逐个访问列表中的每一项。比如,for语句。

迭代只能对应集合,列表,数组等。不能对执行代码进行迭代。

  • 遍历(traversal):指的是按照一定的规则访问树形结构中的每个节点,而且每个节点都只访问一次。

遍历同迭代一样,也不能对执行代码进行遍历。

  • 递归(recursion):指的是一个函数不断调用自身的行为。比如,以编程方式输出著名的斐波纳契数列。

关于递归和迭代

递归,是一种程序有限次调用自身的方法。递归运算有许多好处,它能帮我们把一个大问题一层一层的转化为若干个小问题,使我们的代码更精简、可读性更高。

递归过程中每一次函数调用,都需要在栈内存上分配空间,以保存当前函数参数、临时变量及函数地址,在堆内存中分配函数地址指向的函数内存空间。虽然递归过程造成的堆栈消耗会在当前递归结束后根据垃圾回收机制自动回收,但程序的堆栈运行空间是有限的,一次性递归太深而占用过多的堆栈内存,程序很容易发生堆栈溢出而无法正常运行的情况。因此,递归是一种比较耗时间和空间的运算方法。

相比较而言,迭代(循环)虽然不如递归代码简洁,但迭代是利用某个变量的原值不断更新为新的值,没有额外的空间开销,其时间消耗只与循环次数有关,因此迭代的运行效率更高。

递归和迭代密切相关,很多时候程序可以使用递归或迭代返回相同的结果。但有些情况,递归才能更好的解决我们的问题。

递归相关算法

实现阶乘

// 递归实现
function factorial(n) {
  if (n == 1) {
    return 1;
  } else if (n > 1) {
    return n * factorial(n - 1);
  }
}
console.log(factorial(5)); // 120

// 迭代实现
var num = 1;
for (let i = 1; i <= 5; i++) {
  num = num * i;
}
console.log(num); // 120

在这种情况下,递归和迭代的时间复杂度都为O(n)。 但递归由于每一次调用函数都要开辟一段内存空间,所以递归的空间复杂度为O(n)。而迭代每次都是在同一个 num 值上进行数值更新,并没有开辟更多的内存空间,所以此处迭代的空间复杂度为O(1)。显然这种情况下使用迭代比使用递归更合适些。

求斐波那契数列第 n 个数字的值(1、1、2、3、5、8、13、21、34、…… )

// 递归实现
function fibonacci(n) {
  if (n <= 2) {
    return 1;
  } else {
    return fibonacci(n - 1) + fibonacci(n - 2);
  }
}
console.log(fibonacci(5)); //5

该递归过程刚好满足于一颗二叉树的排列方式,算法的时间复杂度为O(2^n –1) = O(2^n )

//迭代实现
function fibonacci(n) {
  let a = 1,
    b = 1;
  for (let i = 1; i < n; i++) {
    res = a + b;
    a = b;
    b = res;
  }
  return a;
}
console.log(fibonacci(5)); //5

迭代算法的时间复杂度为O(n),空间复杂度为O(1)

补充:求斐波那契数列中前n项的值( for 循环+动态数组 )

function fibonacci(n) {
  let res = [1, 1];
  if (n == 1 || n == 2) {
    return 1;
  }
  for (let i = 2; i < n; i++) {
    res[i] = res[i - 1] + res[i - 2];
  }
  return res[n - 1];
}
console.log(fibonacci(5)); //5

这里要注意理解:在强类型语言中,静态数组保存在栈内存中,创建静态数组会在栈上给该数组分配大小固定的空间,并且在运行时这个大小不能改变。动态数组保存在堆内存中,在栈内存中保存指向堆内存的指针。当需要给动态数组动态添加元素时,系统首先会在堆内存中开辟一段新的空间,大小为动态添加元素后所需要的数组的大小。接着先将原来堆内存中的数组元素拷贝到新的堆内存中,然后在新的堆内存空间中添加剩余所指定的元素,再把栈内存中保存的指针指向新的堆内存空间,原来的堆内存空间就等待垃圾回收机制清除。在这里由于javaStript是一门弱类型语言,它的数组长度本身就是不固定的,不需要运用其它语法来定义动态数组

尺子刻度算法

给出一个中间数,根据中间的数字打印出如下格式的数字串:

1

1  2  1

1  2  1  3  1  2  1

1  2  1  3  1  2  1  4  1  2  1  3  1  2  1

……

每一段中间的数左右两边的数字,都是上一列的运算结果

// 方法一 时间复杂度O(2^n)
function ruler1(n) {
  if (n == 1) {
    return "1";
  } else {
    return ruler1(n - 1) + "" + n + ruler1(n - 1);
  }
}
console.log(ruler1(4)); //121312141213121

这种方式跟我们之前就斐波那契数列类似,在递归调用过程中都形成了二叉树,因此这种算法的时间复杂度也为O(2^n)。即然左右两边求的都是同一组数,那我们是否可以将其进一步优化呢?

答案是肯定的,即然中间数左右两边的数字串都是一样的,我们可以提前将一边的字符串保存下来,然后在最后直接在左右两边拼接所保存的字符串,这样就只是对单边进行递归计算而不是两边重复递归计算,时间复杂度也由原来的O(2^n)降为O(n)

// 方法二 时间复杂度O(n)
// 递归实现
function ruler1(n) {
  if (n == 1) {
    return "1";
  } else {
    let t = ruler1(n - 1);
    return t + "" + n + t;
  }
}
console.log(ruler1(4)); //121312141213121

// 迭代实现
function ruler(n) {
  let result = "";
  for (let i = 1; i <= n; i++) {
    result = result + "" + i + result;
  }
  return result;
}
console.log(ruler(4)); // 121312141213121

求最短数学表达式

输入 a,b 两个数,b 大于 a, a 只能通过 ×2 或者+1 等于 b,并且 a 在式中只能出现一次(排除 a 刚好是 1 或者 2 的情况),求出 a 运算后等于 b 的最短表达式。

function intSeq(a, b) {
  if (a == b) {
    return "a";
  }
  // 做奇偶性判断
  if (b % 2 == 1) {
    // 注意是拿 a 表示 b,所以要对b进行操作。对 a 操作会出错
    return "(" + intSeq(a, b - 1) + "+ 1 )"; // 加法时添上括号防止与乘法错位
  }
  if (b < a * 2) {
    return "(" + intSeq(a, b - 1) + "+ 1 )";
  }
  if (b >= a * 2) {
    return intSeq(a, b / 2) + "*2";
  }
}
console.log(intSeq(5, 11)); // (a*2+ 1 )

汉诺塔算法

有三根柱子,在一根柱子上从下往上根据大小顺序依次摆放 n 个圆盘(底部圆盘最大)。现在对圆盘进行移动,要求把这堆圆盘按照同样排列顺序转移到另一根柱子上。规定在移动的过程中,三根柱子之间每次只能移动一个圆盘,并且小圆盘上不能放在大圆盘上面,请写一套算法,给出对于 n 个圆盘的移动过程。

function hanota(n, From, By, To) {
  if (n == 1) {
    console.log("从" + From + "移动到" + To);
  } else {
    hanota(n - 1, From, To, By); // 上面盘子 1~3 步移动过程
    hanota(1, From, By, To); // 最底下一个盘子移动过程
    hanota(n - 1, By, From, To); //上面盘子 5~7 步移动过程
  }
  return "结束";
}

我们将圆盘一开始在的柱子叫做起始柱子From,最后要放的柱子叫做目标柱子To,而中间暂时过渡的柱子叫做过渡柱子By。汉诺塔算法的时间复杂度为O(2^n)

求不重复元素集合的所有子集

给定一个数组,里面的元素不重复,请求出该数组的所有子集。

// 递归回溯
var subsets = function (nums) {
  let n = nums.length;
  let tmpPath = [];
  let res = [];
  let backtrack = (tmpPath, start) => {
    res.push(tmpPath);
    for (let i = start; i < n; i++) {
      tmpPath.push(nums[i]);
      backtrack(tmpPath.slice(), i + 1);
      tmpPath.pop();
    }
  };
  backtrack(tmpPath, 0);
  return res;
};

// 迭代
var subsets = function (nums) {
  let res = [[]];
  for (let i = 0; i < nums.length; i++) {
    let len = res.length;
    for (let j = 0; j < len; j++) {
      let sub = res[j].slice();
      sub.push(nums[i]);
      res.push(sub);
    }
  }
  return res;
};