【问题标题】:javascript merge sort and recursionjavascript合并排序和递归
【发布时间】:2020-06-02 21:34:22
【问题描述】:

我正在尝试了解 JavaScript 合并排序功能的工作原理。我很难理解递归函数是如何工作的。这是代码:

const mergeSort = array => {
  if (array.length < 2) {
    //function stop here
    return array
  }

  const middle = Math.floor(array.length / 2);
  const leftSide = array.slice(0, middle);
  const rightSide = array.slice(middle, array.length);
  return merge(mergeSort(leftSide), mergeSort(rightSide))

};

const merge = (left, right) => {
  const result = [];

  while (left.length && right.length) {
    if (left[0] <= right[0]) {
      result.push(left.shift());
    } else {
      result.push(right.shift);
    }
  }

  while(left.length) result.push(left.shift());

  while(right.length) result.push(right.shift());

  return result;
}
mergeSort([5,3,8,10,4,1])

【问题讨论】:

  • 请注意,所有值要么是当前函数调用的本地值,要么是传递给它的参数。所以在[5][3, 8] 排序之后,return merge... 行调用merge[5][3, 8],返回[3, 5, 8]。类似的事情发生在右半部分:[10] 被返回,[4, 1] 被排序到[1, 4],然后[10][1, 4] 被合并到[1, 4, 10]。最后[3, 5, 8][1, 4, 10]合并成[1, 3, 4, 5, 8, 10]。每个递归调用都会保留自己的局部变量副本。
  • 把它写在纸上:你从[5, 3, 8, 10, 4, 1]开始,它在mergeSort中被分成两个数组。这两个数组都(递归地)再次传递给mergeSort,其中[5, 3, 8] 变为[5][3, 8]。对于[5],我们完成了,它按原样返回。对于[3,8],我们再次递归,它们最终返回为[3][8]。那些得到merged:在合并步骤中会发生什么?写出来:如果你想了解任何算法,这是一个重要的练习。
  • 但是首先像merge(3, 8)那样调用merge,然后执行但函数本身不能保留这些值,然后用5再次执行,但我不知道5是如何传递的merge([3,8,5]) 因为 mergeSort 只返回一个值...
  • 嘿,迈克,合并函数或合并中的拆分处理是否仅使用单个参数调用?
  • @shaunaa - 使用两个参数调用合并,每个参数都是一个数组,合并返回一个合并数组。这是实现归并排序的一种非常低效的方式。最好一次性分配第二个数组,然后在两个数组之间来回合并,将数组和索引作为参数传递。

标签: javascript arrays sorting recursion mergesort


【解决方案1】:

要了解递归,您可以使用缩进跟踪所有递归级别。例如:

const mergeSort = (array, level) => {
  logWithLevel(level, "Start sort array " + array);
  if(array.length < 2) {
    //function stop here
    logWithLevel(level, "Finish sort array " + array);
    return array;
  }

  const middle = Math.floor(array.length / 2);
  logWithLevel(level, "middle element is " + array[middle])
  const leftSide = array.slice(0, middle);
  const rightSide = array.slice(middle, array.length);
  var result = merge(mergeSort(leftSide, level + 1), mergeSort(rightSide, level + 1));
  logWithLevel(level, "Finish sort array " + result);
  return result;
};

const merge = (left, right) => {
  const result = [];

  while(left.length && right.length){
    if(left[0] <= right[0]){
      result.push(left.shift());
    }else{
      result.push(right.shift());
    }
  }

  while(left.length) result.push(left.shift());

  while(right.length) result.push(right.shift());

  return result;
}

const logWithLevel = (level, data) => {
    var s = ""
    for (i = 0; i < level; i++) {
        s += "    ";
    }
    console.log(s + data);
}

结果:

> mergeSort([5,3,8,10,4,1], 0)
    Start sort array 5,3,8,10,4,1
    middle element is 10
        Start sort array 5,3,8
        middle element is 3
            Start sort array 5
            Finish sort array 5
            Start sort array 3,8
            middle element is 8
                Start sort array 3
                Finish sort array 3
                Start sort array 8
                Finish sort array 8
            Finish sort array 3,8
        Finish sort array 3,5,8
        Start sort array 10,4,1
        middle element is 4
            Start sort array 10
            Finish sort array 10
            Start sort array 4,1
            middle element is 1
                Start sort array 4
                Finish sort array 4
                Start sort array 1
                Finish sort array 1
            Finish sort array 1,4
        Finish sort array 1,4,10
    Finish sort array 1,3,4,5,8,10

【讨论】:

    【解决方案2】:

    Merge Sort 的工作原理是分而治之。在这里,一个问题被划分为一个较小的子问题,并且一直持续到问题可以解决为止。然后我们通过结合较小的已解决问题来解决较大的问题。

    在归并排序中,我们将数组划分为更小的数组,直到它的大小为 1 并且大小为 1 的数组已经排序。之后,我们将合并较小的数组,这样新创建的数组也会被排序。

    在图表中,您可以看到在第四层中所有子数组的大小都是 1,从那里开始我们正在合并子数组。

    图片来源:GeekForGeeks

    function mergeSort(input) {
      const {length:arraySize} = input;
      if (arraySize < 2) return input;
      const mid = Math.floor(arraySize/2);
      const sortedLeftArray = mergeSort(input.slice(0,mid));
      const sortedRightArray = mergeSort(input.slice(mid, arraySize));
      return merge(sortedLeftArray, sortedRightArray);
    }
    
    function merge (left, right){
      let result = []
      while(left.length && right.length){
        if(left[0]< right[0]){
          result.push(left.shift())
        }else{
          result.push(right.shift())
        }
      }
      /* Either left/right array will be empty or both */
      return [...result, ...left, ...right];
    }
    
    console.log(mergeSort([5,3,8,10,4,1]))

    【讨论】:

    • OP 正在寻找代码的解释,而不是更多没有解释的代码......
    • 虽然这段代码可能会解决问题,但一个好的答案应该解释代码的什么以及如何它有帮助
    【解决方案3】:
    Sorting with recursive
    

    function sort(arr){
    const staticArr = [...arr]
      function recMaxSort(arrayARG,maxEL=0,resultArr = []){
        const [firstEl,...rest] = arrayARG;
        if (staticArr.length === resultArr.length){
          return resultArr
        }
        if (!firstEl){
          resultArr=[maxEL,...resultArr]
          const newArray = staticArr.filter(el=>{return !resultArr.includes(el)})
          return recMaxSort(newArray,0,resultArr)
        }
    
        if (maxEL>firstEl){
          return recMaxSort(rest,maxEL,resultArr)
        } else if (firstEl>=maxEL){
          return recMaxSort(rest,firstEl,resultArr)
        }
    
      }
    
      return  recMaxSort(arr)
    
    }
    
     
     console.log(sort([231,4,7,3,54,500]));

    【讨论】:

    • 你能补充一些解释吗?
    • @dan1st 你好!你到底不明白什么?我的函数基于数组解构。当递归进行时,它比较数组中的所有值。
    • 我之所以这么写,是因为我在 LQP 审核队列中得到了你的答案,而你只是写了代码,没有解释。
    【解决方案4】:

    递归就像一个循环,但又不同。
    一个循环迭代直到一个结束条件
    递归'调用自身'直到一个基本情况
    循环像故事一样从头到尾迭代;
    递归就像一个故事,每一章都包含在前一章中......直到你到达最里面的一章(基本案例)。只有读完这最里面的一章,你才能最终清楚地了解发生了什么。因此,现在您可以备份一章并重新阅读,并理解该章。随着您继续理解和备份章节的层次结构,您终于到达了故事的开头并完成了(理解)这本书。


    在本例中,基本情况array.length &lt; 2一个包含一个或零个元素的数组。根据定义,任何包含一/零元素的数组都已排序。
    像这样分解数组后,我们问:“我可以做哪些小工作来确保——当我们重新组合数组时——它将按排序顺序重新组合?”在这个例子中,工作是 merge 函数。

    mergeSort 划分数组参数并调用自身,直到数组完全划分为一个元素数组。它在这里调用自己:

    return merge( mergeSort(leftSide), mergeSort(rightSide))
    

    这行代码被添加到调用堆栈,但要评估merge,必须先评估mergeSort。所以mergeSort 被添加到调用堆栈中,然后运行。但每次运行时,都会有另一个merge() 的返回。这会导致调用堆栈堆积对merge 的调用。在我们停止调用它之前,我们不能开始返回调用堆栈并评估merge():当最终满足基本情况时。现在我们开始“退出我们的故事”,从最里面的章节开始,然后返回调用堆栈。

    当我们返回堆栈时,我们以合并函数的形式贡献工作:我们比较leftSiderightSide 的第一个元素并对它们进行排序。无论side 小于另一个,都会将pushed 放入result,而side 将获得shifted(替换)下一个元素。

    为什么会这样?
    该算法确保即使最小的元素最初排在最后(或最大的元素排在最前面),它们也会被排序,即使 工作 所做的只是每一步比较两个字符。通过以这种方式分解数组(并堆积调用堆栈),我们确保合并总是被调用足够的次数,这样最小的数字总是在每一步“到达队列的前面”,并且有将是“足够的步骤”,它将首先出现。

    在 Mohammed 答案的 GeekForGeeks 图像中,检查数字 3 的路径。合并函数 3 次“工作 on”。每次它“到达队列的前面”:第一次队列只有两个(长度元素),第二次队列是 4,第三次队列是 7(或 8)。即使 3 最初是最后一个(并且示例数组的长度为 8),它仍然只会“工作”三次,并且仍然会排在队列的前面。您可能会意识到,对于长度不超过 2n 的任何数组,最小元素(或通过扩展名:在每个元素上)将其带到队列的前面(或扩展名:排序位置),无论它在数组中的哪个位置开始。

    【讨论】: