08查找满足条件的n个数
第一節、尋找和為定值的兩個數
? ? ? ? 題目:輸入一個數組和一個數字,在數組中查找兩個數,使得它們的和正好是輸入的那個數字。要求時間復雜度是O(n)。如果有多對數字的和等于輸入的數字,輸出任意一對即可。
? ? ? ??例如輸入數組1、2、4、7、11、15和數字15。由于4+11=15,因此輸出4和11。
?
??? 思路如下:
??? 1:直接窮舉,從數組中任意選取兩個數,判定它們的和是否為輸入的那個數字。此舉復雜度為O(n^2) 。很顯然,我們要尋找效率更高的解法。
?
??? 2:題目相當于,對每個a[i],查找sum-a[i]是否也在原始序列中,每一次要查找的時間都要花費為O(n),這樣下來,最終找到兩個數還是需要O(n^2) 的復雜度。那如何提高查找判斷的速度呢?
??? 答案是二分查找。二分查找的時間復雜度為O(lg n)?,如果原數組有序,直接二分查找,n個數的總時間為O(n lg n) ;如果原數組無序,則需要先排序后二分,復雜度同樣為O(nlgn + nlgn) =O(nlgn) ,空間復雜度為O(1) 。
???
??? 3:有沒有更好的辦法呢?可以依據上述思路2的思想,進一步縮小查找時間。如果數組無序,則先在O(nlgn)?時間內排序。
??? 舉個例子,如下:
原始序列:1、 2、 4、 7、11、15????
??? 用輸入數字15減一下各個數,得到對應的序列為:
對應序列:14、13、11、8、4、0?????
??? 第一個數組以一指針i 從數組最左端開始向右掃描,第二個數組以一指針j 從數組最右端開始向左掃描,誰指的元素小,誰先移動,如果a[*i]=a[*j],就找出這倆個數來了。兩端同時查找,時間復雜度瞬間縮短到了O(n),但卻同時需要O(n)的空間存儲第二個數組。
??? 因此,如果原數組無序,則需要時間O(n +nlgn) =O(nlgn)?;如果原數組有序,則需要時間O(n) 。
?
??? 4:針對上述思路進一步改進,使空間復雜度由O(n)變為O(1)。如果數組是無序的,則先在O(nlgn) 時間內排序,然后用兩個指針 i,j,各自指向數組的首尾兩端,令i=0,j=n-1,逐次判斷a[i]+a[j]?=sum,如果a[i]+a[j]>sum,則要想辦法讓a[i]+a[j]的值減小,所以此刻i不動,j--,如果某一刻a[i]+a[j]<sum,則要想辦法讓a[i]+a[j]的值增大,所以此 刻i++,j不動。所以,數組無序的時候,時間復雜度最終為O(n +nlgn)?=O(nlgn),若原數組是有序的,則不需要事先的排序,直接O(n)搞定,且空間復雜度還是O(1) ,此思路是相對于上述所有思路的一種改進。
?
??? 5:還可以構造hash表,正如編程之美上的所述,給定一個數字,根據hash映射查找另一個數字是否也在數組中,只需用O(1) 的時間,這樣的話,總體的算法通上述思路3 一樣,也能降到O(n) ,但有個缺陷,就是構造hash額外增加了O(n) 的空間,不過,空間換時間,仍不失為在時間要求較嚴格的情況下的一種好辦法。
?
??? 所以,要想達到時間O(n) ,空間O(1) 的目標,除非原數組是有序的(指針掃描法),不然,當數組無序的話,就只能先排序,后指針掃描法或二分查找(時間O(nlgn) ,空間O(1) ),或映射或hash(時間O(n) ,空間O(n) )。時間或空間,必須犧牲一個。
?
二:二分查找
??? “二分查找可以解決已排序數組的查找問題:只要數組中包含T(即要查找的值),那么通過不斷縮小包含T的范圍,最終就可以找到它。一開始,范圍覆蓋整個數組。將數組的中間項與T進行比較,可以排除一半元素,范圍縮小一半。就這樣反復比較,反復縮小范圍,最終就會在數組中找到T,或者確定原以為T所在的范圍實際為空。對于包含N個元素的表,整個查找過程大約要經過logN次比較。
??? 多數程序員都覺得只要理解了上面的描述,寫出代碼就不難了;但事實并非如此。我在貝爾實驗室和IBM的時候都出過這道考題。那些專業的程序員有幾個小時的時間,可以用他們選擇的語言把上面的描述寫出來。考試結束后,差不多所有程序員都認為自己寫出了正確的程序。于是,我們花了半個鐘頭來看他們編寫的代碼經過測試用例驗證的結果。幾次課,一百多人的結果相差無幾:90%的程序員寫的程序中有bug(我并不認為沒有bug的代碼就正確)。
??? 我很驚訝:在足夠的時間內,只有大約10%的專業程序員可以把這個小程序寫對。但寫不對這個小程序的還不止這些人:高德納在《計算機程序設計的藝術 第3卷 排序和查找》第6.2.1節的“歷史與參考文獻”部分指出,雖然早在1946年就有人將二分查找的方法公諸于世,但直到1962年才有人寫出沒有bug的二分查找程序。”——喬恩·本特利,《編程珠璣(第1版)》第35-36頁。
?
??? 二分查找算法的邊界,一般來說分兩種情況,一種是左閉右開區間,類似于[left, right),一種是左閉右閉區間,類似于[left, right].需要注意的是, 循環體外的初始化條件,與循環體內的迭代步驟, 都必須遵守一致的區間規則,也就是說,如果循環體外初始化時,是以左閉右開區間為邊界的,那么循環體內部的迭代也應該如此.如果兩者不一致,會造成程序的錯誤.比如下面就是錯誤的二分查找算法:
????left?=?0,?right?=?n;
????while?(left?<?right)
????{
????????middle?=?(left?+?right)?/?2;
????????if?(array[middle]?>?v)
????????{
????????????right?=?middle?-?1;
????????}
????????else?if?(array[middle]?<?v)
????????{
????????????left?=?middle?+?1;
????????}
????????else
????????{
????????????return?middle;
????????}
????}
??? 這個算法的錯誤在于, 在循環初始化的時候,初始化right=n,也就是采用的是左閉右開區間,而當滿足array[middle]> v的條件是, v如果存在的話應該在[left,middle)區間中,但是這里卻把right賦值為middle - 1了,這樣,如果恰巧middle-1就是查找的元素,而且middle-1=left,那么就會找不到這個元素。
?
??? 下面給出兩個算法, 分別是正確的左閉右閉和左閉右開區間算法,可以與上面的進行比較:
int?search2(int?array[],?int?n,?int?v)
{
????int?left,?right,?middle;
????left?=?0,?right?=?n?-?1;
????while?(left?<=?right)
????{
????????middle?=?(left?+?right)?/?2;
????????if?(array[middle]?>?v)
????????{
????????????right?=?middle?-?1;
????????}
????????else?if?(array[middle]?<?v)
????????{
????????????left?=?middle?+?1;
????????}
????????else
????????{
????????????return?middle;
????????}
????}
????return?-1;
}
?
int?search3(int?array[],?int?n,?int?v)
{
????int?left,?right,?middle;
????left?=?0,?right?=?n;
????while?(left?<?right)
????{
????????middle?=?(left?+?right)?/?2;
????????if?(array[middle]?>?v)
????????{
????????????right?=?middle;
????????}
????????else?if?(array[middle]?<?v)
????????{
????????????left?=?middle?+?1;
????????}
????????else
????????{
????????????return?middle;
????????}
????}
????return?-1;
}
???????? 另外:在循環體內,計算中間位置的時候,使用的是這個表達式:
middle?=?(left?+?right)?/?2;
??? 假如,left與right之和超過了所在類型的表示范圍的話,那么middle就不會得到正確的值.所以,更穩妥的做法應該是這樣的:
middle?=?left?+?(right?-?left)?/?2;
(http://www.cppblog.com/converse/archive/2009/10/05/97905.html)
?
三:編程求解
?????? 輸入兩個整數 n 和 m,從數列1,2,3.......n 中 隨意取幾個數,使其和等于 m ,要求將其中所有的可能組合列出來。
?????? 分析:用f(m, n)表示從1到n的序列中,和等于m的所有可能的組合,分幾種情況:
?????? 1:如果m<n,則m+1, m+2,..n這些數不可能出現在任何組合中,所以:
f(m, n) = f(m, m)。
?????? 2:如果m=n,則單獨的n是組合之一,所以f(m, n) = 。
?????? 3:如果m>n,則可以根據組合中是否包含n進行區分,所以:
f(m, n) = 。
?????? 所以,該題目的解法可以用遞歸實現,考慮到要保存中間結果以便打印出最終所有的組合,所以可以借用棧實現,下面的代碼直接使用list來模擬棧的操作:
void findsum(int sum, intn)
{
???????? static list<int> indexStack;?
?
???????? if(sum < 1)return;
???????? if(n < 1)return;
?
???????? if(sum < n)
???????? {
????????????????? return findsum(sum, sum);
???????? }
???????? else if(sum == n)
???????? {
????????????????? cout << sum <<"+";
????????????????? for(list<int>::iteratoriter = indexStack.begin();iter!=indexStack.end();++iter)?
????????????????? {
????????????????????????? cout<<*iter<<"+";?
????????????????? }
????????????????? printf("\b \n");?
????????????????? /*
????????????????????????? \b是退格符,顯示的時候是將光標退回前一個字符,
????????????????????????? 但不會刪除光標位置的字符,
????????????????????????? 如果后邊有新的字符,將覆蓋退回的那個字符,
????????????????????????? 這與我們在文本編器中按Backspace的效果不一樣。?? ???????? */
????????????????? return findsum(sum, sum-1);
???????? }
???????? else
???????? {
????????????????? indexStack.push_front(n);
????????????????? findsum(sum-n, n-1);
????????????????? indexStack.pop_front();
????????????????? findsum(sum, n-1);
???????? }
}
?
四:最長遞增子序列(LongestIncreasing Subsequence,LIS問題)
?????? 給定一個長度為n的數組,找出一個最長的單調遞增子序列(不一定連續)。 更正式的定義是:
?????? 設L=<a1,a2,…,an>是n個不同的實數的序列,L的遞增子序列是這樣一個子序列Lin=<ak1,ak2,…,akm>,其中k1<k2<…<km且ak1<ak2<…<akm。求最大的m值。
?????? 比如數組A 為{10, 11, 12, 13, 1,2, 3, 15},那么最長遞增子序列為{10,11,12,13,15}。
?
1:動態規劃(http://blog.chinaunix.net/uid-26548237-id-3757779.html)
?????? 對于數組a[n], 假設LIS[i]表示:以a[i]為結尾的,最長遞增子序列的長度。
?????? 初始情況下,LIS[i]=1,因為最長遞增子序列只包含a[i];
?????? 如果對于j,0<=j<i,假設LIS[j]已經求解出來了,則可以根據LIS[j]來計算LIS[i]:對于所有j(0<=j<i),如果a[i]>a[j],則LIS[j] + 1的最大值。也就是:
?????? LIS[i]= 。所以,代碼如下:
??????
?????? for(i = 0; i<len; i++)
???????? {
????????????????? LIS[i] = 1;
???????? }
?
???????? for(i = 0; i < len; i++)
???????? {
????????????????? for(j = 0; j < i; j++)
????????????????? {
????????????????????????? if(a[i] > a[j] &&LIS[i]<LIS[j]+1)
????????????????????????? {
?????????????????????????????????? LIS[i] = LIS[j]+1;
????????????????????????? }
????????????????? }
???????? }
?
???????? max = LIS[0];
???????? for(i = 1; i < len; i++)
???????? {
????????????????? printf("the LIS[%d] is %d\n", i, LIS[i]);
????????????????? if(max < LIS[i])
????????????????? {
????????????????????????? max = LIS[i];
????????????????? }
???????? }
?
2:二分查找法(http://www.felix021.com/blog/read.php?1587)
?? 假設存在一個序列d[1..9]= 2 1 5 3 6 4 8 9 7,可以看出來它的LIS長度為5。
下面一步一步試著找出它。
?????? 我們定義一個序列B,然后令 i = 1 to 9 逐個考察這個序列。B[i]中存儲的是:對于長度為i的遞增子序列的中的最大元素,其中的最小值。也就是長度為i的遞增子序列的最小末尾。
?????? 定義LEN記錄數組B中,已經得到的LIS的值。
?
?????? 首先,把d[1]有序地放到B里,令B[1] = 2,就是說當只有1一個數字2的時候,長度為1的LIS的最小末尾是2。這時Len=1
?
?????? 然后,讀取d[2],因d[2] < B[1],所以令B[1] = 1,就是說長度為1的LIS的最小末尾是1,d[1]=2已經沒用了,很容易理解吧。這時Len=1
?
?????? 接著,d[3]= 5,d[3]>B[1],所以令B[1+1]=B[2]=d[3]=5,就是說長度為2的LIS的最小末尾是5,很容易理解吧。這時候B[1..2]= 1, 5,Len=2
?
?????? 再來,d[4]= 3,它正好加在1,5之間,放在1的位置顯然不合適,因為1小于3,長度為1的LIS最小末尾應該是1,這樣很容易推知,長度為2的LIS最小末尾是3,于是可以把5淘汰掉,這時候B[1..2]= 1, 3,Len =2
?
?????? 繼續,d[5]= 6,它在3后面,因為B[2] = 3, 而6在3后面,于是很容易可以推知B[3]= 6, 這時B[1..3]= 1, 3, 6,還是很容易理解吧? Len= 3 了噢。
?
?????? 第6個, d[6] = 4,你看它在3和6之間,于是我們就可以把6替換掉,得到B[3]= 4。B[1..3] = 1, 3, 4, Len繼續等于3
?
?????? 第7個, d[7] = 8,它很大,比4大,嗯。于是B[4]= 8。Len變成4了
?
?????? 第8個, d[8] = 9,得到B[5] = 9,嗯。Len繼續增大,到5了。
?
?????? 最后一個,d[9] = 7,它在B[3]= 4和B[4] = 8之間,所以我們知道,最新的B[4]=7,B[1..5] = 1, 3,4, 7, 9,Len =5。
?
?????? 于是我們知道了LIS的長度為5。之所以B數組中記錄遞增子序列的最小末尾,是為了遍歷后續數組的時候,盡可能的增加遞增子序列的長度。比如某一時刻B[1]= 1, B[2]=5,說明目前的長度為2的遞增子序列中,最小末尾是5,之后碰到6,7,8,...等數的時候才會增加遞增子序列的長度。如果碰見了一個數3,因3<5,所以可以用3替換5:B[2]=3。這樣比如后續碰到了4,則就能增加遞增子序列的長度了。
?
?????? 然后應該發現一件事情了:在B中插入數據是有序的,而且是進行替換而不需要挪動——也就是說,我們可以使用二分查找,將每一個數字的插入時間優化到O(logN)~~~~~于是算法的時間復雜度就降低到了O(nlogn)~!代碼如下:
int BIN_LIS(int *a, intlen)
{
???????? int *b = malloc((len+2) * sizeof(int));
???????? int i;
???????? int max = 0;
???????? int left, right, mid;
?
???????? b[1] = a[0];
???????? max = 1;
???????? for(i = 1; i < len; i++)
???????? {
????????????????? if(a[i] > b[max])
????????????????? {
????????????????????????? max++;
????????????????????????? b[max] = a[i];
????????????????????????? continue;
????????????????? }
?????????????????
????????????????? left = 1;
????????????????? right = max;
?
????????????????? while(left <= right)
????????????????? {?????? //注意下面的邊界條件
????????????????????????? mid = left + (right -left)/2;
????????????????????????? if(b[mid] < a[i])
????????????????????????? {
?????????????????????????????????? left = mid +1;
????????????????????????? }
????????????????????????? else
????????????????????????? {
?????????????????????????????????? right = mid -1;
????????????????????????? }
????????????????? }
???????? ???????? b[left]= a[i];
???????? }
???????? return max;
}
?
五:雙端LIS問題
?????? 問題描述:從一列數中篩除盡可能少的數使得從左往右看,這些數是從小到大再從大到小的。
?????? 思路:這是一個典型的雙端LIS問題。
?????? 對于數組a,用數組b記錄從左向右看時的LIS,b[i]表示,以a[i]結尾的,從a[0]到a[i]之間的LIS。
?????? 用數組c記錄,從右向左看是的LIS,c[i]表示,以a[i]結尾的,從a[n-1]到a[i]之間的LIS。
?????? 所以,b[i] + c[i] - 1就表示以i為“頂點”的,滿足題目要求的最長序列。所以n-max{b[i] + c[i] -1}就是題目的解。
??? 這個問題同樣需要輔助數組LIS, LIS[i]中存儲的是:對于長度為i的遞增子序列的中的最大元素,其中的最小值。也就是遞增子序列的最小末尾。每當掃描到數組元素a[i]時,同LIS中的二分查找一樣,需要找到在LIS中合適的位置,這個位置,也就是b[i]或者c[i]的值。
?
代碼如下(自己寫的,JULY的程序有問題):
int Double_LIS(int *a,int len)
{
???????? int *Lis = malloc((len+2) * sizeof(int));
???????? int *b = malloc(len * sizeof(int));
???????? int *c = malloc(len * sizeof(int));
????????
???????? int i;
???????? int max = 0;
???????? int left, right, mid;
?
???????? int maxlen = 0;
???????? int index;
????????
???????? Lis[1] = a[0];
???????? max = 1;
???????? b[0] = max;
????????
???????? for(i = 1; i < len; i++)
???????? {
????????????????? if(a[i] > Lis[max])
????????????????? {
????????????????????????? max++;
????????????????????????? Lis[max] = a[i];
????????????????????????? b[i] = max;
????????????????????????? printf("b[%d] is %d\n", i, b[i]);
????????????????????????? continue;
????????????????? }
?????????????????
????????????????? left = 1;
????????????????? right = max;
????????????????? while(left <= right)
????????????????? {
????????????????????????? mid = left + (right - left)/2;
????????????????????????? if(Lis[mid] < a[i])
????????????????????????? {
?????????????????????????????????? left = mid + 1;
????????????????????????? }
????????????????????????? else
????????????????????????? {
?????????????????????????????????? right = mid - 1;
????????????????????????? }
????????????????? }
????????????????? Lis[left] = a[i];
????????????????? b[i] = left;
????????????????? printf("b[%d] is %d\n", i, b[i]);
???????? }
?
?
???????? memset(Lis, 0, (len+2) * sizeof(int));
???????? Lis[1] = a[len-1];
???????? max = 1;
???????? c[len-1] = max;
????????
???????? for(i = len - 2; i >= 0; i--)
???????? {
????????????????? if(a[i] > Lis[max])
????????????????? {
????????????????????????? max++;
????????????????????????? Lis[max] = a[i];
????????????????????????? c[i] = max;
????????????????????????? printf("c[%d] is %d\n", i, c[i]);
????????????????????????? continue;
????????????????? }
?????????????????
????????????????? left = 1;
????????????????? right = max;
????????????????? while(left <= right)
????????????????? {
????????????????????????? mid = left + (right - left)/2;
????????????????????????? if(Lis[mid] < a[i])
????????????????????????? {
?????????????????????????????????? left = mid + 1;
????????????????????????? }
????????????????????????? else
????????????????????????? {
?????????????????????????????????? right = mid - 1;
????????????????????????? }
????????????????? }
????????????????? Lis[left] = a[i];
????????????????? c[i] = left;
????????????????? printf("c[%d] is %d\n", i, c[i]);
???????? }
?
???????? maxlen = 0;
???????? for(i = 0; i < len; i++)
???????? {
????????????????? if(b[i]+c[i] > maxlen)
????????????????? {
????????????????????????? maxlen = b[i]+c[i];
????????????????????????? index = i;
????????????????? }
???????? }
???????? printf("index is %d\n", index);
???????? return len - maxlen + 1;
}
?
(http://blog.csdn.net/v_JULY_v/article/details/6419466)
轉載于:https://www.cnblogs.com/gqtcgq/p/7247179.html
總結
以上是生活随笔為你收集整理的08查找满足条件的n个数的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: SharePoint 2013 REST
- 下一篇: iOS 合并.a文件,制作通用静态库