了解合并排序优化:避免复制

时间:2011-09-28 02:54:10

标签: algorithm

我在算法书中有下面的合并排序程序,提到主要问题是合并两个排序列表需要线性额外内存,并且在整个算法中复制到临时数组并返回的额外工作具有大大减慢排序的效果。通过在递归的交替级别明智地切换“a”和“tmp_array”的角色,可以避免这种复制。

我的问题是作者的意思是“通过在递归的交替级别明智地切换a和tmp_array的角色可以避免复制”以及如何在下面的代码中实现?请求展示我们如何实现这一目标的示例?

void mergesort( input_type a[], unsigned int n ) {

    input_type *tmp_array;
    tmp_array = (input_type *) malloc( (n+1) * sizeof (input_type) );
    m_sort( a, tmp_array, 1, n );
    free( tmp_array );
}

void m_sort( input_type a[], input_type tmp_array[ ], int left, int right ) {

    int center;
    if( left < right ) {

    center = (left + right) / 2;
    m_sort( a, tmp_array, left, center );
    m_sort( a, tmp_array, center+1, right );
    merge( a, tmp_array, left, center+1, right );
    }
}

void merge( input_type a[ ], input_type tmp_array[ ], int l_pos, int r_pos, int right_end ) {

    int i, left_end, num_elements, tmp_pos;
    left_end = r_pos - 1;
    tmp_pos = l_pos;
    num_elements = right_end - l_pos + 1;

    /* main loop */

    while( ( 1_pos <= left_end ) && ( r_pos <= right_end ) )

        if( a[1_pos] <= a[r_pos] )
            tmp_array[tmp_pos++] = a[l_pos++];
        else
            tmp_array[tmp_pos++] = a[r_pos++];

    while( l_pos <= left_end )  /* copy rest of first half */
        tmp_array[tmp_pos++] = a[l_pos++];

    while( r_pos <= right_end ) /* copy rest of second half */
        tmp_array[tmp_pos++] = a[r_pos++];

    /* copy tmp_array back */
    for(i=1; i <= num_elements; i++, right_end-- )
        a[right_end] = tmp_array[right_end];

}

4 个答案:

答案 0 :(得分:9)

我将假设,在不查看此代码的情况下,它通过声明与原始内存大小相同的连续内存块来执行合并排序。

所以通常合并排序是这样的:

  • 将数组分成两半
  • 通过递归调用MergeSort来对半数组进行排序
  • 合并半数组

我假设它是递归的,所以在我们排序大小为2的子数组之前不会做任何副本。现在会发生什么?

_ means it is memory we have available, but we don't care about the data in it

original:
   8    5    2    3      1    7    4    6
   _    _    _    _      _    _    _    _

开始递归调用:

recursive call 1:
  (8    5    2    3)    (1    7    4    6)
   _    _    _    _      _    _    _    _

recursive call 2:
 ((8    5)  (2    3))  ((1    7)  (4    6))
   _    _    _    _      _    _    _    _

recursive call 3:
(((8)  (5))((2)  (3)))(((1)  (7))((4)  (6)))
   _    _    _    _      _    _    _    _

递归调用使用合并进行解析,PLUS COPYING(较慢的方法):

merge for call 3, using temp space:
(((8)  (5))((2)  (3)))(((1)  (7))((4)  (6)))    --\ perform merge
(( 5    8 )( 2    3 ))(( 1    7 )( 4    6 ))   <--/ operation

UNNECESSARY: copy back:
(( 5    8 )( 2    3 ))(( 1    7 )( 4    6 ))   <--\ copy and
   _    _    _    _      _    _    _    _       --/ ignore old

merge for call 2, using temp space:
(( 5    8 )( 2    3 ))(( 1    7 )( 4    6 ))    --\ perform merge
(  2    3    5    8  )(  1    4    6    7  )   <--/ operation

UNNECESSARY: copy back:
(  2    3    5    8  )(  1    4    6    7  )   <--\ copy and
   _    _    _    _      _    _    _    _       --/ ignore old

merge for call 1, using temp space:
(  2    3    5    8  )(  1    4    6    7  )    --\ perform merge
   1    2    3    4      5    6    7    8      <--/ operation

UNNECESSARY: copy back:
   1    2    3    4      5    6    7    8      <--\ copy and
   _    _    _    _      _    _    _    _       --/ ignore old

作者的建议 递归调用使用合并解析,无需复制(更快的方法):

merge for call 3, using temp space:
(((8)  (5))((2)  (3)))(((1)  (7))((4)  (6)))    --\ perform merge
(( 5    8 )( 2    3 ))(( 1    7 )( 4    6 ))   <--/ operation

merge for call 2, using old array as temp space:
(  2    3    5    8  )(  1    4    6    7  )   <--\ perform merge
(( 5    8 )( 2    3 ))(( 1    7 )( 4    6 ))    --/ operation (backwards)

merge for call 1, using temp space:
(  2    3    5    8  )(  1    4    6    7  )    --\ perform merge
   1    2    3    4      5    6    7    8      <--/ operation

你去了:只要你在锁定步骤中执行合并排序树的每个“级别”,就不需要复制,如上所示。

您可能会遇到一个小问题,如上所示。也就是说,您的结果可能在temp_array。您要么有三个选项来处理这个问题:

  • 返回temp_array作为答案,并释放旧内存(如果您的应用程序没有问题)
  • 执行单个数组复制操作,将temp_array复制回原始数组
  • 允许自己消耗仅两倍的内存,并执行从temp_array1temp_array2然后再回到original_array的单个合并循环,然后释放temp_array2 }。应该解决平价问题。

答案 1 :(得分:2)

首先考虑以这种方式进行合并排序。

  

0:将输入数组A0视为有序序列的集合   长度1。

     

1:合并来自A0的每个连续序列对,构建一个   新的临时数组A1。

     

2:合并来自A1的每对连续序列,构建一个   新的临时数组A2。

     

...

     

当最后一次迭代产生单个序列时完成。

现在,通过执行此操作,您显然可以只使用一个临时数组:

  

0:将输入数组A0视为有序序列的集合   长度1。

     

1:合并来自A0的每个连续序列对,构建一个   新的临时数组A1。

     

2:合并来自A1的每对连续序列,覆盖A0   结果。

     

3:合并A0中的每对连续序列,覆盖A1   结果。

     

...

     

当最后一次迭代产生单个序列时完成。

当然,你可以比这更聪明。如果你想更好地使用缓存,你可能决定自上而下排序,而不是自下而上。在这种情况下,当你的教科书指的是在不同的递归级别跟踪数组的角色时,它有望变得明显。

希望这有帮助。

答案 2 :(得分:1)

这是我的实施,没有额外的副本。

public static void sort(ArrayList<Integer> input) {
    mergeSort(input, 0, input.size() - 1);
}

/**
 * Sorts input and returns inversions number
 * using classical divide and conquer approach
 *
 * @param input Input array
 * @param start Start index
 * @param end   End index
 * @return int
 */
private static long mergeSort(ArrayList<Integer> input, int start, int end) {
    if (end - start < 1) {
        return 0;
    }

    long inversionsNumber = 0;

    // 1. divide input into subtasks
    int pivot = start + (int) Math.ceil((end - start) / 2);
    if (end - start > 1) {
        inversionsNumber += mergeSort(input, start, pivot);
        inversionsNumber += mergeSort(input, pivot + 1, end);
    }

    // 2. Merge the results
    int offset = 0;
    int leftIndex = start;
    int rightIndex = pivot + 1;
    while (leftIndex <= pivot && rightIndex <= end) {
        if (input.get(leftIndex + offset) <= input.get(rightIndex)) {
            if (leftIndex < pivot) {
                leftIndex++;
            } else {
                rightIndex++;
            }
            continue;
        }

        moveElement(input, rightIndex, leftIndex + offset);
        inversionsNumber += rightIndex - leftIndex - offset;
        rightIndex++;
        offset++;
    }

    return inversionsNumber;
}

private static void moveElement(ArrayList<Integer> input, int from, int to) {
    assert 0 <= to;
    assert to < from;
    assert from < input.size();

    int temp = input.get(from);
    for (int i = from; i > to; i--) {
        input.set(i, input.get(i - 1));
    }
    input.set(to, temp);
}

答案 3 :(得分:0)

查看合并函数的最后一部分。如果不是复制该数据,而是在函数返回时使用了排序部分现在位于tmp_array而不是a的知识,并且a可用作温度。

详细信息留给读者练习。