一、問題
能否快速找出一個數組中的兩個數字,讓這兩個數字之和等於一個給定的數字,爲了簡化起見,我們假設這個數組中肯定存在至少一組符合要求的解。
問題分析:
輸入:一個長度爲N的數組和一個給定的數X。
輸出:數組中的兩個數字A和B。
約束:X = A + B,且A和B至少存在一組。
其他:題目中只說了數字,說明這些數可能爲正整數、負整數、零或浮點數等,不太可能通過給定數字X並拋開數組來遍歷各種A和B的組合並判斷它們是否在數組中。並且題目也沒有告訴我們關於數組的數據規模大小和數據特點。
二、解法
版本一:暴力破解法——遍歷數組
算法:最簡單的思路遍歷一遍數組,對於數組中的每個值A,求解X-A,並在數組未被遍歷的部分中對數組進行第二層遍歷來查找是否含有值B=X-A的數,若找到則輸出A和B。
時間複雜度:最好的情況是O(1),數組中的前兩個數A和B,恰好滿足A+B=N;最差的情況是O(n^2),數組中只存在末尾兩個數A和B,滿足A+B=N。
分析:《編程之美》也提到了這種方法(窮舉法,我這裏使用的名字"暴力破解法"源自《算法導論》,它們的思路是一致的),算法時間複雜度爲O(n^2),效率較低 。
算法C實現:
1.輸入時把最後一個輸入的值作爲題目輸入中的一個給定的數X,把它前面的數作爲題目輸入中的數組數據。
2.儘管題目約束條件講了滿足X=A+B的A和B至少存在一組,算法還是對A和B是否存在進行了檢測,它利用函數返回的標誌位檢測是否找到這兩個值。
/**
* @brief find two numbers value_a and value_b in the array
* (with limit length) with given sum to make: sum = value_a + value_b
*
* @param[in] array input array
* @param[in] length array length
* @param[in] sum sum that makes sum = value_a + value_b
* @param[out] value_a value_a in the array
* @param[out] value_b value_b in the array
*
* @return if get value_a and value_b, return 1, else return 0
*/
TYPE find_two_numbers(TYPE* array, TYPE length, TYPE sum,
TYPE* value_a, TYPE* value_b)
{
assert(array != NULL && length >= 2 && value_a != NULL && value_b != NULL);
TYPE i, j;
for (i = 0; i < length - 1; i++) {
/// set value a
*value_a = array[i];
/// find value b
*value_b = sum - array[i];
for (j = i + 1; j < length; j++) {
if (*value_b == array[j])
return 1;
}
}
return 0;
}
版本二:排序+二分查找
自己的思路:在前面問題分析中說過不太可能拋開數組只從給定的數X入手,可以從“對數組進行預處理”這個角度來思考問題。我們可以對數組從小到大進行排序,利用快速排序的時間複雜度是O(nlgn),接下來的我們可以發現,按照版本一的思路遍歷數組求解A和B時,對於遍歷數組時的一個數a1,我們先找到數組中滿足a1 + am >= X的最小的數am,接下來進行第二層遍歷尋找b1=X-a1這個數時,我們只需要讓b1從am開始進行遍歷即可。如果我們採用遍歷的方式來搜索am(算法複雜度O(n)),那麼算法的總時間複雜度與版本一是一致的,只是這裏利用了比較語句而不是判斷語句。但若採用更高效的搜索方法(如二分搜索等)來搜索am,那麼算法的總時間複雜度肯定會比版本一的總時間複雜度低。
思路補充:《編程之美》中也提出了對排序數組進行二分查找(或其他高效查找方法)的方式,與我所想的思路的區別是:在尋找上面定義的b1時就採用二分搜索,而不是去先二分搜索am再遍歷搜索b1,比我上面的思路更加開闊,全面利用了二分查找的快速性。
(《編程之美》原話:學過編程的人都知道,提高查找效率通常可以先將要查找的數組排序,然後用二分查找等方法進行查找,就可以將原來O(N)的查找時間縮短找O(lgN)。)
時間複雜度:快速排序算法時間複雜度O(nlgn);遍歷數組中的每個數A的時間複雜度爲O(n),而遍歷過程中查找對應的數B(滿足X = A + B)時使用二分查找算法,二分查找算法時間複雜度爲O(lgn),所以查找A和B總的時間複雜度爲O(nlgn)。排序算法和查找算法按序進行,因此總的時間複雜度也是O(nlgn)。
算法C實現:
1.對數組進行排序採用快速排序算法。
2.對數組進行查找採用二分查找算法。
/**
* @brief select the last element as a pivoit.
* Reorder the array so that all elements with values less than the pivot
* come before the pivot, while all elements with values greater than the pivot
* come after it (equal values can go either way). After this partitioning, the
* pivot is in its final position.
*
* @param[in,out] array input and output array
* @param[in] index_begin the begin index of the array(included)
* @param[in] index_end the end index of the array(included)
*
* @return the position of the pivot(index from the array)
*/
TYPE partition(TYPE* array, TYPE index_begin, TYPE index_end)
{
/// pick last element of the array as the pivot
TYPE pivot = array[index_end];
/// index of the elments that not greater than pivot
TYPE i = index_begin - 1;
TYPE j, temp;
/// check array's elment one by one
for (j = index_begin; j < index_end; j++) {
if (array[j] <= pivot) {
/// save the elements not greater than pivot to left index of i.
i++;
temp = array[j];
array[j] = array[i];
array[i] = temp;
}
}
/// set the pivot to the right position
array[index_end] = array[++i];
array[i] = pivot;
/// return the position of the pivot
return i;
}
/**
* @brief quick sort method for input array from index_begin to index_end.
*
* @param[in,out] array input and output array
* @param[in] index_begin the begin index of the array(included)
* @param[in] index_end the end index of the array(included)
*/
void quick_sort(TYPE* array, TYPE index_begin, TYPE index_end)
{
/// sort only under the index_begin < index_end condition
if ( index_begin < index_end) {
/// exchange elements to the pivot position by partition function
TYPE index_pivot = partition(array, index_begin, index_end);
/// sort the array before the pivot position
quick_sort(array, index_begin, index_pivot - 1);
/// sort the array after the pivot position
quick_sort(array, index_pivot + 1, index_end);
}
}
/**
* @brief search the value in the array of the index by binary search method.
*
* @param[in] array input array
* @param[in] count array length
* @param[in] value search value
*
* @warning array index begin from 0
*
* @return index if success, else return -1
*/
TYPE binary_search(TYPE* array, TYPE count, TYPE value)
{
assert(array != NULL && count >= 0);
TYPE middle;
TYPE index_begin = 0;
TYPE index_end = count - 1;
while (index_begin <= index_end) {
middle = index_begin + ((unsigned)(index_end - index_begin) >> 1);
if (array[middle] == value)
return middle;
else if (array[middle] < value)
index_begin = middle + 1;
else
index_end = middle - 1;
}
return -1;
}
/**
* @brief find two numbers value_a and value_b in the array
* (with limit length) with given sum to make: sum = value_a + value_b
*
* @param[in] array input array
* @param[in] length array length
* @param[in] sum sum that makes sum = value_a + value_b
* @param[out] value_a value_a in the array
* @param[out] value_b value_b in the array
*
* @return if get value_a and value_b, return 1, else return 0
*/
TYPE find_two_numbers(TYPE* array, TYPE length, TYPE sum,
TYPE* value_a, TYPE* value_b)
{
assert(array != NULL && length >= 2 && value_a != NULL && value_b != NULL);
/// sort array by quick sort method
quick_sort(array, 0, length - 1);
/// iterative for all value in array
TYPE i;
for (i = 0; i < length - 1; i++) {
/// set value a
*value_a = array[i];
/// find value b
*value_b = sum - array[i];
/// search value by binary search method
if (binary_search(array, length, *value_b) != -1)
return 1;
}
return 0;
}
版本三:哈希查找
思路(引自《編程之美》):查找使用hash表。因爲給定一個數字,根據hash映射查找另一個數字是否在數組中,只需用O(1)時間。這樣的話,總體的算法複雜度可以降低到O(N),但這種方法需要額外增加O(N)的hash表存儲空間。在有的情況下,用空間換時間也並不失爲一個好方法。
時間複雜度:O(n);空間複雜度O(n)。
分析:和其他算法相比,這個hash表算法是典型地使用空間換時間的一種查找算法。
版本四:排序+兩個指針兩端掃描法
思路(引自《編程之美》):可以直接對兩個數字的和進行一個有序的遍歷,從而降低算法的時間複雜度。
步驟:
1.首先對數組進行排序,時間複雜度爲O(nlgn)。
2.然後令i = 0,j = n-1,看arr[i] + arr[j] 是否等於Sum,如果是,則結束。如果小於Sum,則i = i + 1;如果大於Sum,則 j = j – 1。這樣只需要在排好序的數組上遍歷一次,就可以得到最後的結果,時間複雜度爲O(n)。
時間複雜度:O(nlgn + n) = O(nlgn);空間複雜度O(1)。
分析:算法關鍵的一步是利用排序後指向數組兩端的兩個指針,通過判斷Sum的大小來移動指針,在查找過程每次只移動其中一個指針,從而實現了O(n)的效率來查找滿足條件的兩個數(數組中利用兩個下標進行遍歷)。如果原來輸入數組就是有序的,那麼使用這個算法解決本問題的效率是最佳的。
算法C實現:
/**
* @brief select the last element as a pivoit.
* Reorder the array so that all elements with values less than the pivot
* come before the pivot, while all elements with values greater than the pivot
* come after it (equal values can go either way). After this partitioning, the
* pivot is in its final position.
*
* @param[in,out] array input and output array
* @param[in] index_begin the begin index of the array(included)
* @param[in] index_end the end index of the array(included)
*
* @return the position of the pivot(index from the array)
*/
TYPE partition(TYPE* array, TYPE index_begin, TYPE index_end)
{
/// pick last element of the array as the pivot
TYPE pivot = array[index_end];
/// index of the elments that not greater than pivot
TYPE i = index_begin - 1;
TYPE j, temp;
/// check array's elment one by one
for (j = index_begin; j < index_end; j++) {
if (array[j] <= pivot) {
/// save the elements not greater than pivot to left index of i.
i++;
temp = array[j];
array[j] = array[i];
array[i] = temp;
}
}
/// set the pivot to the right position
array[index_end] = array[++i];
array[i] = pivot;
/// return the position of the pivot
return i;
}
/**
* @brief quick sort method for input array from index_begin to index_end.
*
* @param[in,out] array input and output array
* @param[in] index_begin the begin index of the array(included)
* @param[in] index_end the end index of the array(included)
*/
void quick_sort(TYPE* array, TYPE index_begin, TYPE index_end)
{
/// sort only under the index_begin < index_end condition
if ( index_begin < index_end) {
/// exchange elements to the pivot position by partition function
TYPE index_pivot = partition(array, index_begin, index_end);
/// sort the array before the pivot position
quick_sort(array, index_begin, index_pivot - 1);
/// sort the array after the pivot position
quick_sort(array, index_pivot + 1, index_end);
}
}
/**
* @brief find two numbers value_a and value_b in the array
* (with limit length) with given sum to make: sum = value_a + value_b
*
* @param[in] array input array
* @param[in] length array length
* @param[in] sum sum that makes sum = value_a + value_b
* @param[out] value_a value_a in the array
* @param[out] value_b value_b in the array
*
* @return if get value_a and value_b, return 1, else return 0
*/
TYPE find_two_numbers(TYPE* array, TYPE length, TYPE sum,
TYPE* value_a, TYPE* value_b)
{
assert(array != NULL && length >= 2 && value_a != NULL && value_b != NULL);
/// sort array by quick sort method
quick_sort(array, 0, length - 1);
/// search by two pointers at the begining and end of the array
TYPE i = 0, j = length - 1, sum_temp;
while (i < j) {
sum_temp = array[i] + array[j];
if (sum_temp < sum) {
i++;
} else if (sum_temp > sum) {
j--;
} else {
*value_a = array[i];
*value_b = array[j];
return 1;
}
}
return 0;
}
三、拓展
1.如果把這個問題中的“兩個數字”改爲“三個數字”或“任意個數字”時,如何求解?
思路:
首先分析“兩個數字”問題時解法:如果數組本身是有序的,那麼不必對數組進行排序,此時採用版本一暴力破解法時時間複雜度爲O(n^2),採用版本五兩個指針兩端掃描法時時間複雜度爲O(n),兩個方法相比後一個方法時間複雜度降了一個數量級n。
進一步分析可以得到“三個數字”問題時解法:如果數組本身是有序的,那麼不必對數組進行排序,此時採用暴力破解法時時間複雜度爲O(n^3),採用兩個指針兩端掃描法,此時遍歷時(設三個待求解的數分別爲A,B,C)固定一個數A,對另外兩個數B和C採用兩個指針兩端掃描,時間複雜度爲O(n^2),兩個方法相比後一個方法時間複雜度也是降了一個數量級n。
對於“任意個數字”問題時解法:根據上面的思路,可以逐步增加數字的個數m並結合兩個指針兩端掃描法綜合求解,數字個數爲1時時間複雜度O(1),數字個數爲2時時間複雜度O(n^2),,數字個數爲3時時間複雜度O(n^3),當數字個數m大於數組長度(N)一半時,我們可以先求整個數組的和S(注意溢出),遍歷時改爲遍歷N-m個數並結合兩個指針兩端掃描法對(N-m)個數求和,與(S - X) 判斷,相等時得解,但此時解時另外m個數(數組中剔除遍歷時用到的N-m個數)。這樣,“任意個數字”時算法的時間複雜度最高情況下爲O(n^(k)),k = n / 2。可以預計到時間複雜度會比直接採用暴力破解法降低一個數量級n。
優化:
1.“三個數字“或“任意個數字”採用兩端指針掃描法時可以採用遞歸實現,遞歸接口包含了數組個數這個參數,簡化代碼。(遞歸實現想法參考自:http://blog.csdn.net/hackbuteer1/article/details/6699642)
注意:
1.數組只用排序一次。
2.“三個數字”問題在使用遞歸實現時,數組排好序後,從做到右遍歷A,對於B和C,只需在A右端的剩餘數組中進行遞歸實現。
2.如果完全相等的一對數字對找不到,能否找出和最接近的解?
思路:找最接近的解,可轉化爲求使得abs(A+B - X)最小的值,通過遍歷A和B時記錄使前面等式達到更小時A和B的值來實現。遍歷過程中,當等式值爲0時,說明存在,直接輸出;若等式一直不爲零,則完全遍歷A和B後再輸出A和B的值,此時A和B就是最接近的解了。
算法C實現:
注意:
1.算法實現基於上面排序+兩個指針兩端掃描法實現。
1.函數返回1時表示找到完全相等的解value_a和value_b,函數返回0時表示找到最接近的解value_a和value_b。
/**
* @brief select the last element as a pivoit.
* Reorder the array so that all elements with values less than the pivot
* come before the pivot, while all elements with values greater than the pivot
* come after it (equal values can go either way). After this partitioning, the
* pivot is in its final position.
*
* @param[in,out] array input and output array
* @param[in] index_begin the begin index of the array(included)
* @param[in] index_end the end index of the array(included)
*
* @return the position of the pivot(index from the array)
*/
TYPE partition(TYPE* array, TYPE index_begin, TYPE index_end)
{
/// pick last element of the array as the pivot
TYPE pivot = array[index_end];
/// index of the elments that not greater than pivot
TYPE i = index_begin - 1;
TYPE j, temp;
/// check array's elment one by one
for (j = index_begin; j < index_end; j++) {
if (array[j] <= pivot) {
/// save the elements not greater than pivot to left index of i.
i++;
temp = array[j];
array[j] = array[i];
array[i] = temp;
}
}
/// set the pivot to the right position
array[index_end] = array[++i];
array[i] = pivot;
/// return the position of the pivot
return i;
}
/**
* @brief quick sort method for input array from index_begin to index_end.
*
* @param[in,out] array input and output array
* @param[in] index_begin the begin index of the array(included)
* @param[in] index_end the end index of the array(included)
*/
void quick_sort(TYPE* array, TYPE index_begin, TYPE index_end)
{
/// sort only under the index_begin < index_end condition
if ( index_begin < index_end) {
/// exchange elements to the pivot position by partition function
TYPE index_pivot = partition(array, index_begin, index_end);
/// sort the array before the pivot position
quick_sort(array, index_begin, index_pivot - 1);
/// sort the array after the pivot position
quick_sort(array, index_pivot + 1, index_end);
}
}
/**
* @brief find two numbers value_a and value_b in the array
* (with limit length) with given sum to make: sum closest to value_a + value_b
*
* @param[in] array input array
* @param[in] length array length
* @param[in] sum sum that makes sum = value_a + value_b
* @param[out] value_a value_a in the array
* @param[out] value_b value_b in the array
*
* @return if get value_a and value_b, return 1, else return 0
*/
TYPE find_best_two_numbers(TYPE* array, TYPE length, TYPE sum,
TYPE* value_a, TYPE* value_b)
{
assert(array != NULL && length >= 2 && value_a != NULL && value_b != NULL);
/// sort array by quick sort method
quick_sort(array, 0, length - 1);
/// search by two pointers at the begining and end of the array
TYPE i = 0, j = length - 1, sum_diff_min = INT_MAX, sum_diff;
while (i < j) {
sum_diff = abs(array[i] + array[j] - sum);
/// record closet value_a and value_b
if (sum_diff < sum_diff_min) {
sum_diff_min = sum_diff;
*value_a = array[i];
*value_b = array[j];
}
if (sum_diff < 0) {
i++;
} else if (sum_diff > 0) {
j--;
} else {
/// get value_a and value_b with no error
return 1;
}
}
/// get value_a and value_b with error
return 0;
}
3.把上面的兩個問題綜合起來,就得到這樣一個題目:給定一個數N,和一組數字集合S,求S中和最接近N的子集。