问题:如何交换两段不连续的内存块?
分析篇
这道题是 < 编程珠玑,第二版>第二章后的一个习题。看过这本书的朋友一定会对书中第二章介绍的用来交换两段连续的内存块的"reversal algorithm"一定记忆犹新。"reversal algorithm"无论在时间复杂度还是在空间复杂度上都有良好的表现,更重要的是它的实现相当简单:
1。假设有两段连续的内存块a和b;
2。首先对内存块a进行反转:a' = reverse(a);
3。接着对内存块b进行反转:b' = reverse(b);
3。最后对内存块a'b'进行反转:(a'b')' = ba;
是不是很神奇?当我第一次看到这个算法的时候,我就惊叹为什么这三步简单的操作就能解决看似很复杂的问题?如果你还不相信的话(我一开始和你一样也是不相信),请看下面的例子:
1。假如有两段连续的内存块,a内存块的内容是"abcd",b内存块的内容是"efgh";
2。首先对内存块a进行反转:a' = reverse("abcd") = "dcba";
3。接着对内存块b进行反转:b' = reverse("efgh") = "hgfe";
4。最后对内存块a'b'进行反转:(a'b')' = reverse("dcbahgfe") = efghabcd;
那"reversal algorithm"和本文中需要解决的问题有什么联系呢?在看到这个问题的时候,我就立即联想到了"reversal algorithm",我试图通过已有的方法去解决类似的问题。幸运的是经过简单的分析和推论,我就找到了一个简单的方法,这个方法只要对"reversal algorithm"进行简单的修改就可以解决本文的问题:
1> 假设有三段连续的内存a,b和c,这样a和c是不连续的;
2> 首先对内存块a进行反转:a' = reverse(a);
3> 接着对内存块b进行反转:b' = reverse(b);
4> 接着对内存块c进行反转:c' = reverse(c);
5> 最后对内存块a'bc'进行反转:reverse(a'b'c') = cba;
是不是超简单?这里使用了和"reversal algorithm"一样的思路。最后一次反转,不仅分别将内存块a和c中的数据调整为初始状态,而且还交换它们的位置。由于内存块b处于中间的位置,最后一次反转只将它的内容调整到初始状态。
实现篇
有了上面的分析,编写代码实现这个算法就显得相对比较简单了:
1。首先我们需要一个反转内存块的函数。这个函数应该说比较简单:
/**/
//
//
swap one adjacent memory block
//
The memory layout like this:
//
1--------------|
//
| memTotalSize |
//
RETURN VALUE: The address of the memory just being swapped
void
*
swapMemory(
void
*
pMemory,size_t memTotalSize)

...
{
if (NULL == pMemory) return pMemory;
if (memTotalSize < 2) return pMemory;

unsigned char* pByteMemory = reinterpret_cast<unsigned char*>(pMemory);

for (size_t i = 0; i < memTotalSize/2; i++) ...{
unsigned char tempByte = *(pByteMemory+i);
*(pByteMemory + i) = *(pByteMemory+memTotalSize-1 - i);
*(pByteMemory+memTotalSize-1 - i) = tempByte;
}

return pMemory;
}
就这个函数的实现,有以下几点需要说明:
1。在函数的开始,我对函数的参数进行了一定的约束,这个在"Design By Contract"中被称作前项约束"Precondition"。相应的,在函数的最后还有一个后项约束"Postcondition"。在函数中间的部分,应该还有一个被称为不变式(Invariant)的东西。这些约束的目的就是保证函数在开始/中间/结束过程中处于一个正确的状态。
2。如果要对内存进行逐字节的访问,我们可以通过一个字符指针来完成。一个字符所占的内存空间是一个字节,它在内存块中的索引值就等于它相对于内存块首地址的偏移量。
2。有了上面的函数,按照本文前面的分析,我们就可以很容易实现本文的算法了:
//
swap two nonadjacent memory block
//
The memory layout like this:
//
|-------------|-------------------------------------|------------|
//
| memHeadSize | memTotalSize-memHeadSize-memEndSize | memEndSize |
//
The start/end index for the three memory blocks are:
//
The head block: [0,(memHeadSize-1)];
//
The middle block: [memHeadSize, (memTotalSize-memEndSize-1)];
//
The end block: [(memTotalSize-memEndSize), (memEndSize-1)];
//
RETURN VALUE: The address of the memory just being swapped
void
*
swapNonadjacentMemory(
void
*
pMemory,size_t memTotalSize,
size_t memHeadSize,size_t memEndSize)

...
{
if (NULL == pMemory) return pMemory;
if (memTotalSize < 3) return pMemory;
if (memTotalSize < memEndSize) return pMemory;
if (memTotalSize < (memHeadSize+memEndSize)) return pMemory;

unsigned char* pByteMemory = reinterpret_cast<unsigned char*>(pMemory);

//step1: reverse the head block
swapMemory(pByteMemory,memHeadSize);
//step2: reverse the middle block
swapMemory((pByteMemory+memHeadSize),(memTotalSize-memHeadSize-memEndSize));

//step3: reverse the end block
swapMemory((pByteMemory+memTotalSize-memEndSize),memEndSize);

//step4: reverse the whole block
swapMemory(pByteMemory,memTotalSize);

return pMemory;
}
在这个函数的前项约束中,第一个和第二个约束很好理解,第三个和第四个约束是如何得到的?一开始我也就写了前两个约束,当我写完了所有的代码后进行复查的时候,我看到了这样的函数调用:
swapMemory((pByteMemory+memTotalSize-memEndSize),memHeadSize);
如果 (memTotalSize-memEndSize)小于0,那就有可能访问到不属于它的内存,这个操作是非常危险的,是绝对要禁止的,所在我们需要添加约束来避免这样的参数进入到函数的内部。接着我又在随后的代码中找到需要约束的地方:
swapMemory((pByteMemory+memHeadSize),(memTotalSize-memHeadSize-memEndSize));
这里需要 约束(memTotalSize-memHeadSize-memEndSize)不小于0。
经过以上的代码复查过程,我就添加了第三个和第四个前项约束。
测试篇
我写了一个函数用来测试本文实现的算法:
void
Test_swapNonadjacentMemory()
...
{
//table-driven test case

static const char* testString[] = ...{
"",
"ab",
"abc",
"abcdefgh"
};
void* pMemory = malloc(MAXMEMBUFFERSIZE);
if (NULL == pMemory) return;

for (int i = 0 ; i < sizeof(testString)/sizeof(const char*); i++) ...{
size_t iStringLength = strlen(testString[i]);
//clear the memory block
memset(pMemory,0,MAXMEMBUFFERSIZE);
//copy from the string
memcpy(pMemory,testString[i],iStringLength);
//reverse the memory block
swapNonadjacentMemory(pMemory,iStringLength,3,3);
}
free(pMemory);

}
值得注意的是,这里我把所有的测试字符串都放在一个"表"中,然后用一个简单的循环语句就可以对所有的测试字符串进行测试,同时如果想修改测试字符串或者添加新的测试用例,只需要对表进行操作就可以了。
后记
在随后的文章中,我将使用本文的算法去解决更复杂的问题:反转一个字符串中所有单词。有兴趣的朋友也可以思考一下这个问题,据说这个问题是微软的一个面试题。
参考资料
1。《编程珠玑,第二版》
本人强烈推荐此书!此书博大精深,是修炼内功的好资料。看看大家是怎么评价这本书的吧:
http://www.amazon.com/Programming-Pearls-2nd-Jon-Bentley/dp/0201657880
这里还有此书的配套网站: http://www.cs.bell-labs.com/cm/cs/pearls/
(注:我看此书的时候觉得有点吃力,我很愿意结交同样也在看这本书的朋友,大家可以相互交流学习心得,讨论问题。)
历史:
12/10/2006 v1.0
原文的第一个正式版
12/15/2006 v1.1
修改:把对中间内存块b的反转放到了反转整个内存块a'b'c'的前面, 同时代码也做了相应的修改 。这样的修改并没有什么本质的变化,只是我觉得这样整个算法显得更合理点。
分析篇
这道题是 < 编程珠玑,第二版>第二章后的一个习题。看过这本书的朋友一定会对书中第二章介绍的用来交换两段连续的内存块的"reversal algorithm"一定记忆犹新。"reversal algorithm"无论在时间复杂度还是在空间复杂度上都有良好的表现,更重要的是它的实现相当简单:
1。假设有两段连续的内存块a和b;
2。首先对内存块a进行反转:a' = reverse(a);
3。接着对内存块b进行反转:b' = reverse(b);
3。最后对内存块a'b'进行反转:(a'b')' = ba;
是不是很神奇?当我第一次看到这个算法的时候,我就惊叹为什么这三步简单的操作就能解决看似很复杂的问题?如果你还不相信的话(我一开始和你一样也是不相信),请看下面的例子:
1。假如有两段连续的内存块,a内存块的内容是"abcd",b内存块的内容是"efgh";
2。首先对内存块a进行反转:a' = reverse("abcd") = "dcba";
3。接着对内存块b进行反转:b' = reverse("efgh") = "hgfe";
4。最后对内存块a'b'进行反转:(a'b')' = reverse("dcbahgfe") = efghabcd;
那"reversal algorithm"和本文中需要解决的问题有什么联系呢?在看到这个问题的时候,我就立即联想到了"reversal algorithm",我试图通过已有的方法去解决类似的问题。幸运的是经过简单的分析和推论,我就找到了一个简单的方法,这个方法只要对"reversal algorithm"进行简单的修改就可以解决本文的问题:
1> 假设有三段连续的内存a,b和c,这样a和c是不连续的;
2> 首先对内存块a进行反转:a' = reverse(a);
3> 接着对内存块b进行反转:b' = reverse(b);
4> 接着对内存块c进行反转:c' = reverse(c);
5> 最后对内存块a'bc'进行反转:reverse(a'b'c') = cba;
是不是超简单?这里使用了和"reversal algorithm"一样的思路。最后一次反转,不仅分别将内存块a和c中的数据调整为初始状态,而且还交换它们的位置。由于内存块b处于中间的位置,最后一次反转只将它的内容调整到初始状态。
实现篇
有了上面的分析,编写代码实现这个算法就显得相对比较简单了:
1。首先我们需要一个反转内存块的函数。这个函数应该说比较简单:























就这个函数的实现,有以下几点需要说明:
1。在函数的开始,我对函数的参数进行了一定的约束,这个在"Design By Contract"中被称作前项约束"Precondition"。相应的,在函数的最后还有一个后项约束"Postcondition"。在函数中间的部分,应该还有一个被称为不变式(Invariant)的东西。这些约束的目的就是保证函数在开始/中间/结束过程中处于一个正确的状态。
2。如果要对内存进行逐字节的访问,我们可以通过一个字符指针来完成。一个字符所占的内存空间是一个字节,它在内存块中的索引值就等于它相对于内存块首地址的偏移量。
2。有了上面的函数,按照本文前面的分析,我们就可以很容易实现本文的算法了:























swapMemory((pByteMemory+memHeadSize),(memTotalSize-memHeadSize-memEndSize));









在这个函数的前项约束中,第一个和第二个约束很好理解,第三个和第四个约束是如何得到的?一开始我也就写了前两个约束,当我写完了所有的代码后进行复查的时候,我看到了这样的函数调用:
swapMemory((pByteMemory+memTotalSize-memEndSize),memHeadSize);
如果 (memTotalSize-memEndSize)小于0,那就有可能访问到不属于它的内存,这个操作是非常危险的,是绝对要禁止的,所在我们需要添加约束来避免这样的参数进入到函数的内部。接着我又在随后的代码中找到需要约束的地方:
swapMemory((pByteMemory+memHeadSize),(memTotalSize-memHeadSize-memEndSize));
这里需要 约束(memTotalSize-memHeadSize-memEndSize)不小于0。
经过以上的代码复查过程,我就添加了第三个和第四个前项约束。
测试篇
我写了一个函数用来测试本文实现的算法:























free(pMemory);


值得注意的是,这里我把所有的测试字符串都放在一个"表"中,然后用一个简单的循环语句就可以对所有的测试字符串进行测试,同时如果想修改测试字符串或者添加新的测试用例,只需要对表进行操作就可以了。
后记
在随后的文章中,我将使用本文的算法去解决更复杂的问题:反转一个字符串中所有单词。有兴趣的朋友也可以思考一下这个问题,据说这个问题是微软的一个面试题。
参考资料
1。《编程珠玑,第二版》
本人强烈推荐此书!此书博大精深,是修炼内功的好资料。看看大家是怎么评价这本书的吧:
http://www.amazon.com/Programming-Pearls-2nd-Jon-Bentley/dp/0201657880
这里还有此书的配套网站: http://www.cs.bell-labs.com/cm/cs/pearls/
(注:我看此书的时候觉得有点吃力,我很愿意结交同样也在看这本书的朋友,大家可以相互交流学习心得,讨论问题。)
历史:
12/10/2006 v1.0
原文的第一个正式版
12/15/2006 v1.1
修改:把对中间内存块b的反转放到了反转整个内存块a'b'c'的前面, 同时代码也做了相应的修改 。这样的修改并没有什么本质的变化,只是我觉得这样整个算法显得更合理点。