Algorithm 理解合并排序优化:避免复制
我在《算法》一书中介绍了下面的合并排序程序,其中提到的主要问题是合并两个排序列表需要线性额外内存,并且在整个算法中,复制到临时数组和复制回临时数组所花费的额外工作会大大降低排序速度。通过在递归的交替级别明智地切换“a”和“tmp_数组”的角色,可以避免这种复制 我的问题是,作者所说的“通过在递归的交替级别上明智地切换和tmp_数组的角色,可以避免复制”是什么意思,以及在下面的代码中如何做到这一点?要求举例说明我们如何实现这一目标Algorithm 理解合并排序优化:避免复制,algorithm,Algorithm,我在《算法》一书中介绍了下面的合并排序程序,其中提到的主要问题是合并两个排序列表需要线性额外内存,并且在整个算法中,复制到临时数组和复制回临时数组所花费的额外工作会大大降低排序速度。通过在递归的交替级别明智地切换“a”和“tmp_数组”的角色,可以避免这种复制 我的问题是,作者所说的“通过在递归的交替级别上明智地切换和tmp_数组的角色,可以避免复制”是什么意思,以及在下面的代码中如何做到这一点?要求举例说明我们如何实现这一目标 void mergesort( input_type a[], u
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];
}
void mergesort(输入类型a[],无符号整数n){
输入_类型*tmp_数组;
tmp_数组=(输入_类型*)malloc((n+1)*sizeof(输入_类型));
m_排序(a,tmp_数组,1,n);
自由(tmp_阵列);
}
无效m_排序(输入类型a[],输入类型tmp_数组[],左整数,右整数){
国际中心;
if(左<右){
中心=(左+右)/2;
m_排序(a,tmp_数组,左,中);
m_排序(a,tmp_数组,中间+1,右);
合并(a,tmp_阵列,左侧,中间+1,右侧);
}
}
无效合并(输入类型a[],输入类型tmp\U数组[],整数l\U位置,整数r\U位置,整数右端){
int i,左端,num元素,tmp位置;
左端=右端位置-1;
tmp_pos=l_pos;
num\u elements=右端-l\u位置+1;
/*主回路*/
而((1_pos以这种方式考虑合并排序
0:考虑输入数组A0作为有序序列的集合
长度1
1:合并A0中的每一个连续序列对,构建一个
新的临时阵列A1
2:合并A1中的每一个连续序列对,构建一个
新的临时阵列A2
当最后一次迭代产生单个序列时完成
现在,通过执行以下操作,您显然可以只使用一个临时数组:
0:考虑输入数组A0作为有序序列的集合
长度1
1:合并A0中的每一个连续序列对,构建一个
新的临时阵列A1
2:合并A1中的每个连续序列对,覆盖A0
结果呢
3:合并A0中的每个连续序列对,覆盖A1
结果呢
当最后一次迭代产生单个序列时完成
当然,你甚至可以比这更聪明。如果你想对缓存更友好,你可以决定从上到下排序,而不是从下到上排序。在这种情况下,当你的教科书提到在不同递归级别跟踪数组的角色时,希望你能明白它的意思
希望这有帮助。查看合并函数的最后一部分。如果您不复制该数据,而是在函数返回时使用了排序部分现在位于tmp\u数组中的知识,而不是a
,并且a
可用作临时数据,该怎么办
详细信息留给读者作为练习。我将假设,在不查看此代码的情况下,它通过声明与原始内存块大小相同的连续内存块来执行合并排序
所以通常合并排序是这样的:
- 一分为二
- 通过递归调用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)))
_ _ _ _ _ _ _ _
递归调用通过合并和复制进行解析(使用更多内存,或者“较慢”):
如果您执行第k个统计中值中值算法的一些奇怪变体,将合并到两个数组的中间,而不是开始(从特定选择的元素向外合并,同时向左/递减和向右/递增),那么您可能能够非常聪明地在适当的位置进行合并我不确定人们会如何实现这一点,或者这一预感是否属实
(非常次要的注意:可能熟悉排序算法的人应该小心比较传统的交换操作传统的swap
操作,该操作涉及寄存器中的tmp
变量,即从缓存中两次读取,两次写入缓存,与不就地复制到其他内存位的操作,而不进行每次操作计数。)(这是一个令人惊讶的论点。)
当然,OP的方法非常简单,只需两倍的内存即可编写代码。这是我的实现,无需额外拷贝
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);
}
公共静态无效排序(ArrayList输入){
mergeSort(输入,0,输入.size()-1);
}
/**
*对输入进行排序并返回倒数
*使用经典的分治方法
*
*@param输入数组
*@param开始索引
*@param-end-index
*@return int
*/
私有静态长合并排序(ArrayList输入、int开始、int结束){
如果(结束-开始<1){
返回0;
}
长反转数=0;
//1.将输入划分为子任务
int pivot=start+(int)Math.ceil((end-start)/2);
如果(结束-开始>1){
inversionsNumber+=合并排序(输入、开始、透视);
inversionsNumber+=合并排序(输入,轴+1,结束);
}
//2.合并结果
整数偏移=0;
int leftIndex=start;
int rightIndex=pivot+1;
while(leftIndex
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
( 4 6 7 8 10)(1 2 3 5 9 11)(... other sub-arrays)
( 1)(6 7 8 10)(4)(2 3 5 9 11)(...
( 1 2)(7 8 10)(4 6)(3 5 9 11) ...
( 1 2 3)(8 10)(4 6 7)(5 9 11)
( 1 2 3 4(10)(8)(6 7)(5 9 11) ooph :-(
( 1 2 3 4 5)(8)(6 7)(10)(9 11) ooph
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);
}