【问题标题】:Explanation of Merge Sort for Dummies傻瓜合并排序的解释
【发布时间】:2012-05-17 04:02:06
【问题描述】:

我在网上找到了这段代码:

def merge(left, right):
    result = []
    i ,j = 0, 0
    while i < len(left) and j < len(right):
        if left[i] <= right[j]:
            result.append(left[i])
            i += 1
        else:
            result.append(right[j])
            j += 1
    result += left[i:]
    result += right[j:]
    return result

def mergesort(list):
    if len(list) < 2:
        return list
    middle = len(list) / 2
    left = mergesort(list[:middle])
    right = mergesort(list[middle:])
    return merge(left, right)

当我运行它时它可以 100% 工作。我只是不明白合并排序是如何工作的,或者递归函数如何能够正确地对左右排序。

【问题讨论】:

  • 查看 wiki 页面上的动画 gif:en.wikipedia.org/wiki/Merge_sort
  • 可能是合并排序最简洁的代码之一。这段代码的来源是什么?请给个链接。

标签: python algorithm sorting mergesort


【解决方案1】:

我认为理解归并排序的关键是理解以下原则——我称之为归并原则:

给定两个单独的列表 A 和 B 从最小到最大排序,通过反复比较 A 的最小值和 B 的最小值,删除较小的值,并将其附加到 C 上,构造一个列表 C。当一个列表是用尽,将另一个列表中的剩余项目按顺序附加到 C 上。列表 C 也是一个排序列表。

如果您手动完成几次,您会发现它是正确的。例如:

A = 1, 3
B = 2, 4
C = 
min(min(A), min(B)) = 1

A = 3
B = 2, 4
C = 1
min(min(A), min(B)) = 2

A = 3
B = 4
C = 1, 2
min(min(A), min(B)) = 3

A = 
B = 4
C = 1, 2, 3

现在A已经用完了,所以用B的剩余值扩展C:

C = 1, 2, 3, 4

合并原理也很容易证明。 A的最小值小于A的所有其他值,B的最小值小于B的所有其他值。如果A的最小值小于B的最小值,那么它也必须小于小于 B 的所有值。因此它小于 A 的所有值和 B 的所有值。

因此,只要您继续将满足这些条件的值附加到 C,您就会得到一个排序列表。这就是上面的merge 函数所做的。

现在,根据这个原则,很容易理解一种排序技术,它通过将列表划分为更小的列表、对这些列表进行排序,然后将这些排序后的列表合并在一起来对列表进行排序。 merge_sort 函数只是一个函数,它将一个列表分成两半,对这两个列表进行排序,然后以上述方式将这两个列表合并在一起。

唯一的问题是,因为它是递归的,所以当它对两个子列表进行排序时,它通过将它们传递给自己来做到这一点!如果您在这里难以理解递归,我建议您先研究更简单的问题。但是,如果您已经掌握了递归的基础知识,那么您所要做的就是意识到单项列表已经排序。合并两个单项列表会生成一个已排序的两项列表;合并两个二项列表生成一个排序的四项列表;等等。

【讨论】:

  • 优秀。合并排序的基本单元的演练非常好。
  • 我已经浏览了 10-20 个搜索结果,其中有可怕的图形解释。段落和段落的文本谈论一般排序。这正是我想要的。减少追逐。让我大致了解这个想法是什么。太感谢了。现在我必须用谷歌搜索类似的快速排序描述。
  • 我了解算法的“排序”部分是如何工作的。我只是不明白算法如何获取那些小的排序列表并与它们进行“合并”部分。
  • @user3932000 我并没有真正谈论排序部分。我主要是在谈论合并操作。上面,AB 是合并操作的输入,C 是输出。有什么具体的不清楚吗?
【解决方案2】:

当我难以理解算法的工作原理时,我会添加调试输出以检查算法内部实际发生的情况。

这里是带有调试输出的代码。尝试理解递归调用 mergesort 的所有步骤以及 merge 对输出的作用:

def merge(left, right):
    result = []
    i ,j = 0, 0
    while i < len(left) and j < len(right):
        print('left[i]: {} right[j]: {}'.format(left[i],right[j]))
        if left[i] <= right[j]:
            print('Appending {} to the result'.format(left[i]))           
            result.append(left[i])
            print('result now is {}'.format(result))
            i += 1
            print('i now is {}'.format(i))
        else:
            print('Appending {} to the result'.format(right[j]))
            result.append(right[j])
            print('result now is {}'.format(result))
            j += 1
            print('j now is {}'.format(j))
    print('One of the list is exhausted. Adding the rest of one of the lists.')
    result += left[i:]
    result += right[j:]
    print('result now is {}'.format(result))
    return result

def mergesort(L):
    print('---')
    print('mergesort on {}'.format(L))
    if len(L) < 2:
        print('length is 1: returning the list withouth changing')
        return L
    middle = len(L) / 2
    print('calling mergesort on {}'.format(L[:middle]))
    left = mergesort(L[:middle])
    print('calling mergesort on {}'.format(L[middle:]))
    right = mergesort(L[middle:])
    print('Merging left: {} and right: {}'.format(left,right))
    out = merge(left, right)
    print('exiting mergesort on {}'.format(L))
    print('#---')
    return out


mergesort([6,5,4,3,2,1])

输出:

---
mergesort on [6, 5, 4, 3, 2, 1]
calling mergesort on [6, 5, 4]
---
mergesort on [6, 5, 4]
calling mergesort on [6]
---
mergesort on [6]
length is 1: returning the list withouth changing
calling mergesort on [5, 4]
---
mergesort on [5, 4]
calling mergesort on [5]
---
mergesort on [5]
length is 1: returning the list withouth changing
calling mergesort on [4]
---
mergesort on [4]
length is 1: returning the list withouth changing
Merging left: [5] and right: [4]
left[i]: 5 right[j]: 4
Appending 4 to the result
result now is [4]
j now is 1
One of the list is exhausted. Adding the rest of one of the lists.
result now is [4, 5]
exiting mergesort on [5, 4]
#---
Merging left: [6] and right: [4, 5]
left[i]: 6 right[j]: 4
Appending 4 to the result
result now is [4]
j now is 1
left[i]: 6 right[j]: 5
Appending 5 to the result
result now is [4, 5]
j now is 2
One of the list is exhausted. Adding the rest of one of the lists.
result now is [4, 5, 6]
exiting mergesort on [6, 5, 4]
#---
calling mergesort on [3, 2, 1]
---
mergesort on [3, 2, 1]
calling mergesort on [3]
---
mergesort on [3]
length is 1: returning the list withouth changing
calling mergesort on [2, 1]
---
mergesort on [2, 1]
calling mergesort on [2]
---
mergesort on [2]
length is 1: returning the list withouth changing
calling mergesort on [1]
---
mergesort on [1]
length is 1: returning the list withouth changing
Merging left: [2] and right: [1]
left[i]: 2 right[j]: 1
Appending 1 to the result
result now is [1]
j now is 1
One of the list is exhausted. Adding the rest of one of the lists.
result now is [1, 2]
exiting mergesort on [2, 1]
#---
Merging left: [3] and right: [1, 2]
left[i]: 3 right[j]: 1
Appending 1 to the result
result now is [1]
j now is 1
left[i]: 3 right[j]: 2
Appending 2 to the result
result now is [1, 2]
j now is 2
One of the list is exhausted. Adding the rest of one of the lists.
result now is [1, 2, 3]
exiting mergesort on [3, 2, 1]
#---
Merging left: [4, 5, 6] and right: [1, 2, 3]
left[i]: 4 right[j]: 1
Appending 1 to the result
result now is [1]
j now is 1
left[i]: 4 right[j]: 2
Appending 2 to the result
result now is [1, 2]
j now is 2
left[i]: 4 right[j]: 3
Appending 3 to the result
result now is [1, 2, 3]
j now is 3
One of the list is exhausted. Adding the rest of one of the lists.
result now is [1, 2, 3, 4, 5, 6]
exiting mergesort on [6, 5, 4, 3, 2, 1]
#---

【讨论】:

  • 绝妙的解释方式.. 没有@senderle 的回答我无法理解你的回答,没有你的回答我也无法理解senderle!。
【解决方案3】:

归并排序一直是我最喜欢的算法之一。

您从短的排序序列开始,然后按顺序将它们合并成更大的排序序列。就这么简单。

递归部分意味着您正在向后工作 - 从整个序列开始并排序两半。每一半也被拆分,直到序列中只有零或一个元素时排序变得微不足道。正如我在最初的描述中所说的那样,随着递归函数返回排序后的序列会变大。

【讨论】:

    【解决方案4】:

    帮助自己理解这一点的几种方法:

    在调试器中单步调试代码并观察会发生什么。 要么, 在纸上逐步完成(带有一个非常小的示例)并观察会发生什么。

    (我个人觉得在纸上做这种事情更有指导意义)

    从概念上讲,它是这样工作的: 输入列表通过减半不断被切成越来越小的部分(例如,list[:middle] 是前半部分)。每一半一次又一次地减半,直到它的长度小于 2。即直到它什么都不是或一个元素。然后通过合并例程将这些单独的部分重新组合在一起,方法是将 2 个子列表附加或交错到 result 列表中,因此您得到一个排序列表。因为必须对 2 个子列表进行排序,所以追加/交错是一个快速 (O(n)) 操作。

    这个(在我看来)的关键不是合并例程,一旦您了解它的输入将始终被排序,这一点就很明显了。 “技巧”(我使用引号,因为它不是技巧,它是计算机科学:-))是为了保证合并的输入是排序的,你必须继续递归,直到你得到一个 的列表必须进行排序,这就是为什么你一直递归调用 mergesort 直到列表长度少于 2 个元素。

    当您第一次遇到递归和扩展合并排序时,它们可能并不明显。您可能想查阅一本好的算法书籍(例如,DPV 可在线合法且免费地获得),但您可以通过逐步阅读您拥有的代码来获得很长的路要走。如果您真的想参与其中,Stanford/Coursera algo course 将很快再次运行,他详细介绍了合并排序。

    如果您真的想了解它,请阅读该书参考的第 2 章,然后扔掉上面的代码并从头开始重新编写。认真的。

    【讨论】:

      【解决方案5】:

      一张图值一千字,一部动画值一万字。

      查看取自Wikipedia 的以下动画,它将帮助您直观地了解合并排序算法的实际工作原理。

      详细animation with explanation 为好奇者的排序过程中的每个步骤。

      各种排序算法的另一种interesting animation

      【讨论】:

        【解决方案6】:

        基本上你得到你的列表,然后你拆分它,然后对它进行排序,但是你递归地应用这个方法,所以你最终会再次拆分它,直到你有一个可以轻松排序的琐碎集合,然后合并所有简化解决方案以获得完全排序的数组。

        【讨论】:

        • 但是您如何实际对“平凡的集合”进行排序以及如何“合并简单的解决方案”?这不能解释 merge sort 的合并或排序,因此不能回答问题。
        【解决方案7】:

        您可以在这里很好地了解合并排序的工作原理:

        http://www.ee.ryerson.ca/~courses/coe428/sorting/mergesort.html

        我希望它有所帮助。

        【讨论】:

          【解决方案8】:

          正如Wikipedia 文章所解释的,有许多有价值的方法可以完成归并排序。完成合并的方式还取决于要合并的事物的集合,某些集合启用该集合可以使用的某些工具。

          我不会用 Python 来回答这个问题,只是因为我不会写;但是,总体而言,参与“合并排序”算法似乎确实是问题的核心。帮助我的资源是 K.I.T.E 在算法上相当过时的webpage(由教授编写),仅仅是因为内容的作者消除了上下文有意义的标识符。

          我的答案来自此资源。

          请记住,合并排序算法的工作原理是将提供的集合分开,然后将每个单独的部分重新组合在一起,在重新构建集合时将这些部分相互比较。

          这里是“代码”(查看末尾的 Java“小提琴”):

          public class MergeSort {
          
          /**
           * @param a     the array to divide
           * @param low   the low INDEX of the array
           * @param high  the high INDEX of the array
           */
          public void divide (int[] a, int low, int high, String hilo) {
          
          
              /* The if statement, here, determines whether the array has at least two elements (more than one element). The
               * "low" and "high" variables are derived from the bounds of the array "a". So, at the first call, this if 
               * statement will evaluate to true; however, as we continue to divide the array and derive our bounds from the 
               * continually divided array, our bounds will become smaller until we can no longer divide our array (the array 
               * has one element). At this point, the "low" (beginning) and "high" (end) will be the same. And further calls 
               * to the method will immediately return. 
               * 
               * Upon return of control, the call stack is traversed, upward, and the subsequent calls to merge are made as each 
               * merge-eligible call to divide() resolves
               */
              if (low < high) {
                  String source = hilo;
                  // We now know that we can further divide our array into two equal parts, so we continue to prepare for the division 
                  // of the array. REMEMBER, as we progress in the divide function, we are dealing with indexes (positions)
          
                  /* Though the next statement is simple arithmetic, understanding the logic of the statement is integral. Remember, 
                   * at this juncture, we know that the array has more than one element; therefore, we want to find the middle of the 
                   * array so that we can continue to "divide and conquer" the remaining elements. When two elements are left, the
                   * result of the evaluation will be "1". And the element in the first position [0] will be taken as one array and the
                   * element at the remaining position [1] will be taken as another, separate array.
                   */
                  int middle = (low + high) / 2;
          
                  divide(a, low, middle, "low");
                  divide(a, middle + 1, high, "high");
          
          
                  /* Remember, this is only called by those recursive iterations where the if statement evaluated to true. 
                   * The call to merge() is only resolved after program control has been handed back to the calling method. 
                   */
                  merge(a, low, middle, high, source);
              }
          }
          
          
          public void merge (int a[], int low, int middle, int high, String source) {
          // Merge, here, is not driven by tiny, "instantiated" sub-arrays. Rather, merge is driven by the indexes of the 
          // values in the starting array, itself. Remember, we are organizing the array, itself, and are (obviously
          // using the values contained within it. These indexes, as you will see, are all we need to complete the sort.  
          
              /* Using the respective indexes, we figure out how many elements are contained in each half. In this 
               * implementation, we will always have a half as the only way that merge can be called is if two
               * or more elements of the array are in question. We also create to "temporary" arrays for the 
               * storage of the larger array's elements so we can "play" with them and not propogate our 
               * changes until we are done. 
               */
              int first_half_element_no       = middle - low + 1;
              int second_half_element_no      = high - middle;
              int[] first_half                = new int[first_half_element_no];
              int[] second_half               = new int[second_half_element_no];
          
              // Here, we extract the elements. 
              for (int i = 0; i < first_half_element_no; i++) {  
                  first_half[i] = a[low + i]; 
              }
          
              for (int i = 0; i < second_half_element_no; i++) {  
                  second_half[i] = a[middle + i + 1]; // extract the elements from a
              }
          
              int current_first_half_index = 0;
              int current_second_half_index = 0;
              int k = low;
          
          
              while (current_first_half_index < first_half_element_no || current_second_half_index < second_half_element_no) {
          
                  if (current_first_half_index >= first_half_element_no) {
                      a[k++] = second_half[current_second_half_index++];
                      continue;
                  }
          
                  if (current_second_half_index >= second_half_element_no) {
                      a[k++] = first_half[current_first_half_index++];
                      continue;
                  }
          
                  if (first_half[current_first_half_index] < second_half[current_second_half_index]) {
                      a[k++] = first_half[current_first_half_index++];
                  } else {
                      a[k++] = second_half[current_second_half_index++];
                  }
              }
          }
          

          我还有一个版本,here,它会打印出有用的信息并提供更直观的表示上面发生的事情。如果有帮助的话,语法高亮也会更好。

          【讨论】:

            猜你喜欢
            • 1970-01-01
            • 1970-01-01
            • 2011-09-03
            • 2020-08-05
            • 1970-01-01
            • 1970-01-01
            • 2011-10-12
            • 2010-09-23
            • 1970-01-01
            相关资源
            最近更新 更多