A. 给定一个最多包含40亿个随机排列的32位整数的顺序文件,找出一个不在文件中的32位整数(在文件中至少缺少一个这样的数---为什么?)。在具有足够内存的情况下,如何解决该问题?如果有几个外部的“临时”文件可用,但是仅有几百字节的内村,又该如何解决该问题?
因为2^32 大于40亿,所以文件中至少缺失一个整数。我们从表示每个整数的32位的视角来考虑二分搜索,算法的第一趟(最多)读取40亿个输入整数,并把起始位为0的整数写入一个顺序文件,把起始位为1的整数写入另一个顺序文件。这两个文件中,有一个文件最多包含20亿个整数,接下来将该文件用作当前输入并重复探测过程,但这次探测的是第二个位。如果原始的输入文件包含n个元素,那么第一趟将读取n个整数,第二趟最多读取n/2个整数,第三趟最多读取n/4个整数,依次类推,所以总的运行时间正比于n。
如果内存足够,采用位图技术,通过排序文件并扫描,也能够找到缺失的整数,但是这样做会导致运行时间正比于nlog(n).
1. 考虑查找给定输入单词的所有变位词的问题。仅给定单词和字典的情况下,如何解决该问题?如果有一些时间和空间可以在响应任何查询之前预先处理字典,又会如何?
首先计算给定单词的标识,若果不允许预处理,那么久只能顺序读取整个文件,计算每个单词的标识,并于给定单词的标识进行比较。
//压缩一个单词,形成其标识,设定单词中相同字母不会超过99个
void compress(char * pWord, int len, char * pFlag)
{
sort(pWord, pWord+len); //对单词进行排序
int i = 0;
int nCount; //计数重复字母的个数
char chCount[3]; //存放整数到字符的转换值,整数最大为99
while (*pWord != '\0')
{
char chTemp = *pWord;
char *pTemp = pWord + 1;
nCount = 1;
while (true)
{
if (chTemp == *pTemp++)
{
++nCount;
}
else
{
*(pFlag + i++) = *pWord;
// ++i;
memset(chCount, '\0', 3);
_itoa(nCount, chCount, 10);
if (nCount >= 10)
{
*(pFlag + i++) = *(chCount + 0);
// i++;
*(pFlag + i++) = *(chCount + 1);
// ++i;
}
else
{
*(pFlag + i++) = *(chCount + 0);
// ++i;
}
pWord = pWord + nCount;
break;
}
}
}
}
如果允许进行预处理,我们可以在一个预先计算好的结构中执行二分查找,该结构中包含按标识排序的(标识,单词)对。
2. 给定包含4300 000 000 个32位整数的顺序文件,如何找出一个出现至少两次的整数?
方法一:
二分搜索通过递归搜索包含半数以上整数的子区间来查找至少出现两次的单词。因为4300000000 > 2^32,所以必定存在重复的整数,搜索范围从[0, 2^32)开始,中间值mid为2^31,若区间[0, 2^31)内的整数个数大于2^31个,则调整搜索区间为[0, 2^31),反之则调整搜索区间为[2^31, 2^32),然后再对整个文件再遍历一遍,直到得到最后的结果。这样一共会有log2(n)次的搜索,每次遍历n个整数(每次都是完全遍历),总体的复杂度为o(nlog2(n))。
#include <iostream>
using namespace std;
int pow2(int n) //求2的n次幂
{
int i;
int r = 1;
for (i = 0; i < n; i++)
{
r *=2;
}
return r;
}
int _tmain(int argc, _TCHAR* argv[])
{
int arr[] = {4,2,5,1,6,3,8,0,7,6,11,12,14,9,15,10,13};
int len = sizeof(arr) / sizeof(int);
int nCount = 0;
int bit = 4;
int low = 0;
int high = pow2(bit);
int mid = (low +high) / 2;
int i;
while (low <= high )
{
mid = (low + high) / 2; //取中间值
nCount = 0;
for (i = 0; i < len; i++) //计数[low, mid)范围内整数的个数
{
if (arr[i] < mid && arr[i] >= low)
{
++nCount;
}
} //end for
if (nCount == 0) //若nCount为0,则mid即为重复的整数
{
cout << mid<<endl;
break;
}
else
{
if (nCount > (mid - low)) //若大于mid与low的差值,
{ //表明重复的整数落在区间[low, mid)
high = mid; //缩小区间
}
else
{
low = mid;
} //end if
} //end if () else()
} //end while
}
方法二:
#include <iostream>
#include <algorithm>
using namespace std;
int _tmain(int argc, _TCHAR* argv[])
{
int arr[] = {4,2,5,1,7,3,8,0,7,6,11,12,14,9,15,10,13};
int len = sizeof(arr) / sizeof(int);
sort(arr, arr + len); //先进行排序
int i;
int increase = arr[0];
for (i = 0; i < len; i++)
{
if (arr[i] > (i + increase))
{
increase += (arr[i] - i - increase);
continue;
}
if (arr[i] < (i + increase))
{
cout << arr[i] << endl;
break;
}
}
}
3. 前面涉及了两个需要精巧代码来实现的向量旋转算法,将其分别作为独立的程序实现。在每个程序中,i和n的最大公约数如何实现?
采用辗转相除法求两个整数的最大公约数。
int gcd(int a, int b)
{
int temp;
if (a < b) //使a始终为最大数
{
temp = a;
a = b;
b = temp;
}
while (b != 0)
{
temp = a % b;
a = b;
b = temp;
}
return a;
}
方法一:海豚算法
void Shifting(char * pArry, int rotdistance, int len)
{
int i, j;
char temp;
int igcd = gcd(rotdistance, len);
for (i = 0; i < igcd; i++)
{
temp = pArry[i];
j = i;
for (; ;)
{
int k = j + rotdistance;
k %= len;
if ( k == i)
{
break;
}
pArry[j] = pArry[k];
j = k;
}
pArry[j] = temp;
}
}
方法二:块交换算法
#include <iostream>
using namespace std;
//交换pArry[a...a+m-1]和pArry[b...b+m-1]
void myswap(char *pArry, int a, int b, int m)
{
char temp;
for (int i = 0; i < m; i++)
{
temp = pArry[a + i];
pArry[a + i] = pArry[b + i];
pArry[b + i] = temp;
}
}
void Shifting(char * pArry, int rotdistance, int len)
{
if (rotdistance == 0 || rotdistance == len)
{
return;
}
int i, j, p;
i = p = rotdistance;
j = len - p;
while (i != j)
{
if (i > j)
{
myswap(pArry, p - i, p, j);
i -= j;
}
else
{
myswap(pArry, p - i, p + j - i, i);
j -= i;
}
}
myswap(pArry, p - i, p, i);
}
int _tmain(int argc, _TCHAR* argv[])
{
char arry[] = "abcdefghijklmn";
int len = strlen(arry);
Shifting(arry, 10, len);
return 0;
}
方法三:求逆算法
根据矩阵的转置理论,对于矩阵AB,要得到BA,则分别求A和B的转置A', B',然后对(A'B')转置,即(A'B')' = BA。同理,可以得到另一种一维向量向左旋转的算法。将要被旋转的向量x看做两部分ab,这里a代表x中的前rotdistance个元素。首先对a部分进行反转,再对b部分进行反转,最后对整个向量x进行反转即可。
对于字符串“abcdefgh”, rotdistance = 3, len = 8:
reverse(1, rotdistance); //cbadefgh
reverse(rotdistance+1, len); //cbahgfed
reverse(1, len); //defghabc
#include <iostream>
using namespace std;
//对字符串中第i个字符到第j个字符进行反转,i、j>=1
void MyReverse(char * pArry, int i, int j)
{
int front = i;
int tail = j;
char temp;
while (front != tail && front < tail)
{
temp = pArry[front - 1];
pArry[front - 1] = pArry[tail - 1];
pArry[tail - 1] = temp;
++front;
--tail;
}
}
//将字符串左旋转rotdistance个字符
void Shifting(char * pArry, int rotdistance, int len)
{
if (rotdistance == 0 || rotdistance == len)
{
return;
}
MyReverse(pArry, 1, rotdistance);
MyReverse(pArry, rotdistance + 1, len);
MyReverse(pArry, 1, len);
}
int _tmain(int argc, _TCHAR* argv[])
{
char arry[] = "abcdefgh";
int len = strlen(arry);
Shifting(arry, 5, len);
cout << arry << endl;
return 0;
}
5. 向量旋转函数将向量ab变为ba。如何将向量abc变成cba?(这个交换非相邻内存块的问题进行了建模)
可以将bc看做一个整体,然后运用向量旋转算法,得到bca。然后对bc运用向量旋转算法,得到cb。最后变换后的向量为即cba.
//交换pArry[a...a+m-1]和pArry[b...b+m-1]
void myswap(char *pArry, int a, int b, int m)
{
char temp;
for (int i = 0; i < m; i++)
{
temp = pArry[a + i];
pArry[a + i] = pArry[b + i];
pArry[b + i] = temp;
}
}
//对向量pArry中起始于ibegainPos位置的irotdistance-ibegainPos个元素
//与起始于ibegainPos+irotdistance位置到位置iendPos之前的元素进行交换
void SuccessiveSwap(char * pArry, int ibegainPos, int irotdistance, int iendPos)
{
int i, j;
i = irotdistance - ibegainPos;
j = iendPos - irotdistance;
while (i != j)
{
if (i > j)
{
myswap(pArry, irotdistance - i, irotdistance, j);
i -= j;
}
else
{
myswap(pArry, irotdistance - i, irotdistance + j - i, i);
j -= i;
}
}
myswap(pArry, irotdistance - i, irotdistance, i);
}
6. 如何实现一个以名字的按键编码为参数,并返回所有可能的匹配名字的函数
用按键编码标识每一个名字,并根据标识排序,然后顺序读取排序后的文件并输出具有不同名字的相同标识。为了检索出给定按钮编码的名字,可以使用一种包含编码标识和其他数据的结构。然后对该结构排序,使用二分搜索查询按键编码。
7. 转置一个存储在磁带上的4000x4000的矩阵(每条记录的格式相同,为数十个字节)。如何将运行的时间减少到半个小时?
给每条记录插入列号和行号,然后调用系统的磁带排序程序先按列排序再按行排序,最后使用另一个程序删除列号和行号。
8. 给定一个n元实数集合、一个实数t和一个整数k,如何快速确定是否存在一个k元子集,其元素之和不超过t?
对n元实数集合先进行排序,然后计算前k个元素的和既可以确定是否存在这样一个子集。若采用快速排序,时间复杂度为nlog10(n)。
9. 顺序搜素和二分搜索代表了搜索时间和预处理时间之间的折中。处理一个n元表格时,需要执行多少次二分搜索才能弥补对表进行排序所消耗的预处理时间?
对于顺序搜索,搜索k次的时间复杂度为O(kn);若采用二分搜索则需要先排序,则二分搜索的时间复杂度为O(nlog10(n)+log2(n))
变位词程序的实现
//对从文件中读入的每个单词调用qsort库函数排序,输出到新的文件中
void mysign(FILE * pFile1, FILE * pFile2)
{
char word[20];
char sig[20];
pFile1 = fopen("..\\file1.txt", "r");
if ( NULL == pFile1)
{
cout << "Open file1 error!" << endl;
return ;
}
pFile2 = fopen("..\\file2.txt", "w");
if (NULL == pFile2)
{
cout << "Open file2 error!" << endl;
return ;
}
while (!feof(pFile1))
{
memset(word, '\0', 20);
memset(sig, '\0', 20);
fscanf(pFile1, "%s", word);
strncpy(sig, word, strlen(word));
qsort(sig, strlen(sig), sizeof(char), charcomp);
fprintf(pFile2, "%s %s\n", sig, word);
}
fclose(pFile1);
fclose(pFile2);
}
使用sort程序将具有相同标识的单词归拢到一起,形成一个新的文件。最后调用squash()函数将具有相同变位词的单词在同一行输出。
//将具有相同变位词的单词在同一行输出
void squash()
{
FILE * pFile3 = fopen("..\\file3.txt", "r");
if ( NULL == pFile3)
{
cout << "Open file3 error!" << endl;
return ;
}
FILE * pFile4 = fopen("..\\file4.txt", "w");
if (NULL == pFile4)
{
cout << "Open file4 error!" << endl;
}
char word[20];
char sig[20];
char oldsig[20];
int linenum = 0;
memset(oldsig, '\0', 20);
while (!feof(pFile3))
{
memset(word, '\0', 20);
memset(sig, '\0', 20);
fscanf(pFile3, "%s %s", sig, word);
if (strncmp(sig, oldsig, strlen(sig)) != 0 )
{
fprintf(pFile4, "\n");
}
strncpy(oldsig, sig, strlen(sig));
fprintf(pFile4, "%s ", word);
}
fclose(pFile3);
fclose(pFile4);
}