【兔子都能懂的算法】递归,快速排序,归并排序,二分查找

830 阅读3分钟

前言

面试官: 你可以在白板手写一下快速排序/归并排序/二分查找算法吗?

Recursion:(函式) 中有呼叫自己 (Self Calling)

重复执行

-   重复执行一段程式,可用

    -   循环 (Iteration)
    -   递回 (Recursion)

Recursion算法

  • 递回的要素:

    • 递回关系式:找出问题共通的关系,以便反复呼叫自己
    • 终止条件:递回结束的条件
  • 呼叫函式前要将目前的变数值及状态 Push 到 Stack 中

    • 参数值 (Parameter)
    • 区域 / 暂存变数值 (Local Variable)
    • 返回位址 (Return Address)
  • 将控制权转移到呼叫的函式

  • 呼叫函式后要将变数值及状态由 Stack 中 Pop 出来

直接递回 (Direct Recursion)间接递回 (Indirect Recursion)尾端递回 (Tail Recursion)
image.pngimage.pngimage.png
函式直接呼叫自己函式呼叫另一函式,再由另一函式呼叫自己直接递回的特例,函式在结束的前一行呼叫自己

image.png

  • 每次找到null前:+1,不断调用(inorder(curr.left), inorder(curr.right))+1压入栈,不断+1
  • 找到null后:持续与当前maximum比较
// 每次找到null前+1,找到null后就持续与当前 maximum 比较
var maxDepth = function(root) {
    return inorder(root);
};

function inorder(curr){
    if(curr == null){
        return 0;
    }
// 递归分解为左分支和右分支,直到null才回传比较
    return Math.max(inorder(curr.left), inorder(curr.right))+1;
}

快速排序采用分治法 (Divide and Conquer)

排序

  • 选定一个基准值 (Pivot)
  • 将比基准值 (Pivot)  小的数值移到基准值左边,形成左子串列
  • 将比基准值 (Pivot)  大的数值移到基准值右边,形成右子数列

分割(通过递归):将数列依基准值分成三部份

  1. 左子数列:比基准值小的数值
  2. 中子数列:基准值
  3. 右子数列:比基准值大的数值
function quickSort(arr) {
  if(arr.length <2) return arr;
  const [p, ...ary]= arr
  const left = [], right = [];

  ary.forEach(c => {
    if(c < p) left.push(c)
    else right.push(c)
  })

  return [...quickSort(left), p,...quickSort(right)]
}

合并排序法采用分治法 (Divide and Conquer)

分割(递归)

  • 将数列对分成左子数列右子数列

  • 分别对左子数列右子数列作上一个步骤⇒递回 (Recursive)

    • 直到左子数列右子数列被分割成只剩一个元素为止
    • 将仅剩的一个元素作为递回的结果回传
  • 对回传的左子数列右子数列 依大小排列合并

  • 合并的结果作为递回的结果回传

合并:将左子数列右子数列依大小合并成一个新的数列

  • 左子数列的数值都已填到新的数列⇒将右子数列中未填过的最小值填入新数列
  • 右子数列的数值都已填到新的数列⇒将左子数列中未填过的最小值填入新数列
  • 左子数列右子数列中,未填过的最小值填到新的数列
/**
 * postOrder searching:先分支,再排序
 * @param {mergeSort (arr)} 取中间值,分支为 【左子数列】,【右子数列】,通过递回不断将资料分割 => [只要资料列只剩一个元素,就直接回传该资料列,进入Sorting()]
 * @param {merge()}: 回传重新合并并排序过的资料列
 * @returns 
 */
function mergeSort (arr) {
  if (arr.length === 1) return arr;

  const splitIndex = Math.floor(arr.length / 2)

  // 取得分开的【左子数列】【右子数列】
  const left = arr.slice(0, splitIndex)
  const right = arr.slice(splitIndex)

  // 通过递回不断将资料分割
  return Sorting(mergeSort(left), mergeSort(right))
}

/**
 * @function 将【左子数列】【右子数列】排序 => 合并成一个数列
 * @option 若【左子数列】的值都已填入新的数列 => 将【右子数列】中未填过的最小值填入新数列
 * @option 若【右子数列】的值都已填入新的数列 => 将【左子数列】中未填过的最小值填入新数列
 * @option 将【左子数列】【右子数列】未填过的最小值填到新的数列
 * @return {Array} 排好序的数列
 */
function Sorting (left, right) {

  let result = [];

  // 比较【左子数列】【右子数列】的值
  while (left.length && right.length) {
    if (left[0] < right[0]) {
      result.push(left.shift());
    } else if (left[0] > right[0]) {
      result.push(right.shift());
    } else {
      result.push(left.shift(), right.shift());
    }
  }
  return [...result, ...left, ...right];
}

const example = [7, 8, 9, 10, 11, 12, 1, 6, 3, 2]
console.log(mergeSort(example));

搜寻的资料必须经过排序=> 二分搜索

对于已排序好的资料,利用已排序的特性来加快搜寻速度

  • Middle = ⌊(Left + Right)/2⌋, 将键值 key 与搜寻范围的中间资料 data [Middle] 作比对

    • key = data [Middle]:找到
    • key <data [Middle]:缩小搜寻范围 ⇒ Right = Middle-1
    • key > data [Middle]:缩小搜寻范围 ⇒ Left = Middle+1
  • 重复上步骤,直到找到资料或搜寻范围交叉 (找不到)

function binarySearch(data,key) {
  let left = 0, right = data.length -1, middle

/**
 * @param {left <= right}在左边界点小于右边界点的情况下设定中间值的位置
 * @option 若寻找值和中间值刚好相同,直接回传中间值的索引位置,假如寻找值比中间值大时,让中间值后的一个元素位置指定 left,在符合条件的状况下重新执行 while loop,再次取到新的中间值,然后不断比较下去,直到找到和寻找值相同的值为止。
 * @returns 若都没有找到值相同的值,回传-1
 */
  while (left <= right) {
    middle = Math.floor((left + right) / 2)

    if (data[middle] == key) {
      return middle
    }else if (key > data[middle]){
      left = middle + 1
    }else {
      right = middle - 1
    }
  }
  return -1;
}