这是我同实验室的同学在面试中遇到的比较经(qi)典(pa)的问题,题目要求如下:
给定一数组:A=[a1, a2, a3, a4, a5 ... ...] 和一常数:B
从A中取出4个数字(原文是叫四元组)x1, x2, x3, x4,使其和为B,即:x1+x2+x3+x4 = B
求出A中所有可能的四元组。
其它要求:
1. 四元组中的四个数字不能重复。
2. 数字的顺序不影响这是同一个四元组,也即[0,1,2,3]和[3,1,0,2]不能同时出现在结果里。
要给出时间复杂度分别为O()和O(n·logn)
那面对这个问题我们最先而且最容易想到的方法肯定是暴力破解,用C语言格式的伪代码简单写一下就是:
List = [12, 23, 23, 45, 55...]
sum = 98
result = [] // 存放结果的数组
for(int i=0;i<list.length;i++)
for(int j=0;j<list.length;j++)
for(int k=0;k<list.length;k++)
for(int l=0;l<list.length;l++)
if(i+j+k+l == sum && i!=j && i!=k ...... && [i,j,k,l] not in result)
result.add([i,j,k,l])
这个算法的时间复杂度是在O()级别,甚至在最差时有可能达到O(),因为其中还有个判断这个四元组是不是已经在列表中存在的步骤。
因此我们将此进行思路上的简化,首先四元组中的四个数字均不能重复,因此我们先将列表中重复的数据删除(在本文接下来的内容中,会将此操作称之为“去重”)。其次数字每种组合只能出现一次,因此我们可以对列表进行排序(本文称之为“排序”),以便使重复性的组合不出现,也方便我们查找,这都是很容易理解的,于是我们在思路上也变为了如下所示:
对于去重复和排序都有很多成型算法,先排序而后去重复,或者一边排序一边去重复都可以,这些算法中也包含时间复杂度级别为O()【冒泡排序】和 O(n·logn)【堆排序】等等的算法,因为都是比较大众且经典的算法,就不再描述了,那我们接下来将焦点转到如何从一个有序无重复列表中选取这个四元组。
一般来说,正常人的理解范畴,是两数求和问题,我们就将这些数进行两两求和 ( 时间复杂度 ) ,再在其中寻找相加为常数B的两个数,那么这两个数实际上代表四个数,也就是我们要选取的四元组,具体操作如下图所示。
这其中有个很明显的BUG,那就是其实在两两求和这一步结束后,对于和列表,我们还需要进行一次去重和排序(时间复杂度为O( log ) = O( ) ),这很显然已经超出了题目要求。
但这种做法还是给我们带来很大的启发,我们看这道题的最后一步,当总和比常数大的时候,我们就将大值前移一个最小差量,当总和比常数小的时候,我们就将小值后移一个最小差量,这使得我们能找全所有和为常数B的二元组。
那将这个概念进行拓展,当四元组更替之时,会在数组上出现四个指针,假定两个在前两个在后,前面两个向后移动,后面两个向前移动。那么当和大于B时,我们比较后方两个指针的前移最小差量,将较小的前移。那么当和小于B时,我们比较前方两个指针的后移最小差量,将较小的后移。这样便使得我们无需求和便能达到与上述所示同样的效果。
这样算法的时间复杂度最差在O( )级别。
class FourInList:
one = -1
two = -1
three = -1
four = -1
sum = 0
TheList = []
result = []
BackFor = []
def __init__(self, the_list, the_sum):
self.TheList = the_list
self.sum = the_sum
def bubble_sort_and_delete_repeat(self):
pass # 在此处有冒泡排序及其去重复实现
def heap_sort_and_delete_repeat(self):
pass # 在此处有堆排序及其去重复实现
def find_fourth(self, default_list):
if len(self.TheList) < 4:
print("列表过小,无法选取有效四元组!")
return
self.one = default_list[0]
self.two = default_list[1]
self.three = default_list[2]
self.four = default_list[3]
while self.three - 1 > self.two:
print("-"+str(self.one) + "+" + str(self.two) +
"+" + str(self.three) + "+" + str(self.four))
if(self.TheList[self.one] + self.TheList[self.two] +
self.TheList[self.three] + self.TheList[self.four]) > self.sum:
if self.four - self.three <= 1:
self.three -= 1
elif self.TheList[self.four] - self.TheList[self.four-1] > \
self.TheList[self.three] - self.TheList[self.three - 1]:
self.three -= 1
elif self.TheList[self.four] - self.TheList[self.four-1] < \
self.TheList[self.three] - self.TheList[self.three - 1]:
self.four -= 1
else:
self.BackFor.append([self.one, self.two, self.three, self.four - 1])
self.three -= 1
elif(self.TheList[self.one] + self.TheList[self.two] +
self.TheList[self.three] + self.TheList[self.four]) < self.sum:
if self.two - self.one <= 1:
self.two += 1
elif self.TheList[self.one + 1] - self.TheList[self.one] > \
self.TheList[self.two + 1] - self.TheList[self.two]:
self.two += 1
elif self.TheList[self.one + 1] - self.TheList[self.one] < \
self.TheList[self.two + 1] - self.TheList[self.two]:
self.one += 1
else:
self.BackFor.append([self.one + 1, self.two, self.three, self.four])
self.two += 1
else:
if [self.one, self.two, self.three, self.four] not in self.result:
self.result.append([self.one, self.two, self.three, self.four])
if self.two - self.one <= 1:
self.two += 1
elif self.TheList[self.one + 1] - self.TheList[self.one] > \
self.TheList[self.two + 1] - self.TheList[self.two]:
self.two += 1
elif self.TheList[self.one + 1] - self.TheList[self.one] < \
self.TheList[self.two + 1] - self.TheList[self.two]:
self.one += 1
else:
self.BackFor.append([self.one + 1, self.two, self.three, self.four])
self.two += 1
while self.four > self.three and self.two > self.one:
print("--"+str(self.one) + "+" + str(self.two) +
"+" + str(self.three) + "+" + str(self.four))
if (self.TheList[self.one] + self.TheList[self.two] +
self.TheList[self.three] + self.TheList[self.four]) > self.sum:
self.four -= 1
elif (self.TheList[self.one] + self.TheList[self.two] +
self.TheList[self.three] + self.TheList[self.four]) < self.sum:
self.one += 1
else:
if [self.one, self.two, self.three, self.four] not in self.result:
self.result.append([self.one, self.two, self.three, self.four])
self.one += 1
def print_result(self):
for i in self.result:
print(str(i[0]) + "+" + str(i[1]) + "+" + str(i[2]) + "+" + str(i[3]) + " = " +
str(self.sum))
print("The number of the fourth is "+str(len(self.result)))
if __name__ == '__main__':
li = FourInList([0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10], 6)
li.find_fourth([0, 1, len(li.TheList)-2, len(li.TheList)-1])
while len(li.BackFor) > 0:
back = li.BackFor.pop()
li.find_fourth([back[0], back[1], back[2], back[3]])
li.print_result()
说实话,这个代码实现,其时间复杂度最差也是在O(),但如果继续改造这个函数,就会变得有些晦涩难懂,我感觉可能需要使用递归才能使代码更简洁一些,并且效果也能达到O()。在代码中,迫不得已之下,我增加了一个回退数组,记录下当变化量相同时,需要回退再重计的一些节点,这种回退也使得代码效果下降了很多。
那我们考虑另外一种思路,那便是O(n·logn)级别的查找方法。
即这个四元组的选取范围是否是有限制的。
我们将从最小数(比如说0)到(要求的和B - 最小数)这个范围分为4份,每份分到的数字个数出现的可能性如下:
取值共有6种可能性,第一种确定了1/4,第二种确定了1/2,第三种确定了3/4,第四种确定了1/2,第五种确定了3/4,第六种确定了3/4。那么剩下的3/4、1/2、1/4、1/2、1/4、1/4也可分成四份,在减去已经确定的部分后,依旧可将四元组的每个数出现的频次放入这四份种,也依旧是6种可能。
如此递归,直到最终每个数字只对应一个列表中给定的数字为止,递归结束,时间复杂度约在O()。