算法进化历程之“水壶问题”
算法进化历程之“水壶问题”巧若拙(欢迎转载,但请注明出处:http://blog.)
问题描述:假设给定了n个红色的水壶和n个蓝色的水壶,它们的形状和尺寸都不相同。所有红色水壶中所盛水的量都不一样,蓝色水壶也是一样。此外,对于每个红色的水壶,都有一个对应的蓝色水壶,两者所盛的水量是一样的。反之亦然。
你的任务是将所盛水量一样的红色水壶和蓝色水壶找出来。为了达到这一目的,可以执行如下操作:挑选出一对水壶,其中一个是红色的,另一个是蓝色的:将红色水壶中倒满水;再将水倒入到蓝色的水壶中。通过这个操作,可以判断出来这两只水壶的容量哪一个大,或者是一样大。假设这样的比较需要一个时间单位。你的目标是找出一个算法,它通过执行最少次数的比较,来确定分组和配对问题。记住不能直接比较两个红色的或两个蓝色的水壶。
思路分析:
为了验证配对是否正确,我在设计水壶的数据结构时安排了两个属性:水壶的储水量和水壶的编号。配对结束后,表示红蓝水壶的数组中下标相同的水壶储水量相同,但编号不一定相同。
数据结构如下:
typedef struct Kettle
{
int value;//水壶的储水量
int number; //水壶的编号
} Kettle;
为了确保每个红色水壶中所盛水的量都不一样,并且都有一个对应的蓝色水壶,我设计了一个函数为红壶和蓝壶随机初始化各不相同的储水量。代码如下:
void CreatKettle(Kettle BlueK[], Kettle RedK[], int n)
{
int i, j, pos, temp;
for (i=0; i<n; i++)
{
BlueK[i].value = RedK[i].value = BlueK[i].number = RedK[i].number = i + 1;
}
for (i=0; i<n; i++) //采用插入式洗牌,只改变储水量,不改变序号
{
pos = rand() % n;
temp = BlueK[0].value;
for (j=0; j<pos; j++)
BlueK[j].value = BlueK[j+1].value;
BlueK[pos].value = temp;
pos = rand() % n;
temp = RedK[0].value;
for (j=0; j<pos; j++)
RedK[j].value = RedK[j+1].value;
RedK[pos].value = temp;
}
}
版本一:简明易懂的版本
先介绍最简单的方法,拿出第1个红色水壶,然后从n个蓝色水壶中去配对,需要Θ(n)次比较,找到配对的蓝水壶后,将其交换到最左边,使其下标与对应红水壶的下标相同。
重复上述操作,直到所有的红色水壶都完成了配对。总的时间复杂度为Θ(n^2)。
代码如下:
void Match_1(Kettle BlueK[], Kettle RedK[], int n)//最简单的配对算法,时间复杂度为 Θ(n^2)
{
int i, j;
for (i=0; i<n; i++)
{
for (j=i; j<n; j++)
{
if (RedK[i].value == BlueK[j].value)
{
Swap(&BlueK[j], &BlueK[i]);
break;
}
}
}
}
void Swap(Kettle *a, Kettle *b)
{
Kettle temp = *a;
*a = *b;
*b = temp;
}
版本二:在配对某个红壶的同时将蓝壶分成两堆,以便缩小下一次配对的范围
在版本一中,每次为红壶配对,都要遍历未配对的所有蓝壶,效率较低。我们可以在配对某个红壶的同时将蓝壶分成两堆,以便缩小下一次配对的范围,提高效率。代码如下:
void Match_2(Kettle BlueK[], Kettle RedK[], int n)
{
int i, pos;
pos = Partition(BlueK, RedK[0].value, 0, n-1);//先配对第一个红壶
Swap(&BlueK[0], &BlueK[pos]);//将已配对水壶交换到最左边
for (i=1; i<n; i++)//依次配对剩下的红壶
{
if (RedK[i].value < BlueK[i-1].value)
pos = Partition(BlueK, RedK[i].value, i, pos);//在[i,pos]范围内寻找配对
else
pos = Partition(BlueK, RedK[i].value, pos+1, n-1);//在[pos+1, n-1]范围内寻找配对
Swap(&BlueK[i], &BlueK[pos]);//将已配对水壶交换到最左边
}
}
Match_2()用到一个分割函数Partition(),这是一个类似快速排序的分割函数,可以以值为x的元素为枢纽元,将数组K[]分成两部分,并返回枢纽元的位置。代码如下:
int Partition(Kettle K[], int x, int left, int right)
{
while (left < right)
{
while (K[left].value < x)
left++;
while (K[right].value > x)
right--;
Swap(&K[left], &K[right]);
}
return left;
}
版本三:类快速排序算法
由于每个红色水壶中所盛水的量都不一样,并且都有一个对应的蓝色水壶,因此我们只需分别将其进行排序即可。但是不能直接比较同种颜色的水壶,只有颜色不一样的水壶才能进行比较,因此需要交叉比较,互为枢纽元素。
整个排序过程中利用快速排序的思想,调用了版本二中的分割函数Partition(),采用交叉分割的方法,具体描述如下:
1. 从红色水壶序列中随机选择一个作为枢轴元素
2. 利用红色水壶枢轴元素RedK[posRedK]对蓝色水壶序列进行分割,并返回对应蓝色水壶的编号posBlueK。
3. 利用BlueK[posBlueK]对红色水壶序列进行进行分割,并返回对应红色水壶的编号posRedK,很显然此时posRedK ==posBlueK。
4.递归调用函数QuickMatch(),对分割好的序列进行配对。
算法的时间复杂度为O(nlgn),最坏的情况下时间复杂度为O(n^2)。
代码如下:
void Match_3(Kettle BlueK[], Kettle RedK[], int n)//快速配对算法的驱动函数
{
QuickMatch(BlueK, RedK, 0, n-1);
}
void QuickMatch(Kettle BlueK[], Kettle RedK[], int left, int right)//类似快速排序的快速配对算法
{
int posBlueK, posRedK;
if (left < right)
{
posRedK = rand() % (right-left+1) + left; //从红色水壶中随机选择一个作为枢轴元素
posBlueK = Partition(BlueK, RedK[posRedK].value, left, right);//将蓝壶分成两堆,并返回配对蓝壶的编号
posRedK = Partition(RedK, BlueK[posBlueK].value, left, right); //将红壶分成两堆,并返回配对红壶的编号
//递归调用函数QuickMatch(),对分割好的序列进行配对
QuickMatch(BlueK, RedK, left, posRedK-1);
QuickMatch(BlueK, RedK, posRedK+1, right);
}
}
测试主函数:
int main(void)
{
Kettle BlueK[MAXSIZE], RedK[MAXSIZE];
int i, n = 6;
CreatKettle(BlueK, RedK, n);
Print(BlueK, RedK, n);
Match_1(BlueK, RedK, n);
// Match_2(BlueK, RedK, n);
// Match_3(BlueK, RedK, n);
Print(BlueK, RedK, n);
return 0;
}
输出数据函数:
void Print(Kettle BlueK[], Kettle RedK[], int n)
{
int i;
for (i=0; i<n; i++)
{
printf ("BlueK[%d]: %d, %d RedK[%d]: %d, %d\n", i, BlueK[i].number, BlueK[i].value, i, RedK[i].number, RedK[i].value);
}
printf("\n");
}