找數字

找一個數字

題目描述:從1-100中刪去任意一個數字,而後將剩下的99個數字打亂,獲得無序序列a_1,a_2,...,a_{99}。如今須要用一種方法找到這個數字。c++

加法

這是最簡單的方法,將全部的數字求和S_1=\sum_{i=1}^{99}{a_i},而後用S_2=\sum_{i=1}^{100}{i}=5050減去S_1就得到答案。函數

複雜度分析: 若是不算上保存99個數的空間,那麼該方法只須要O(1)的空間記錄求和,以及O(n)的時間遍歷序列。ui

桶排序

另一個容易想到的的方法,因爲數字連續而且數據規模小,因此只須要構建100個「桶」。從頭遍歷給定的99個數字,將其放入數值對應的「桶」中。最後從頭開始找這100個「桶」,哪一個桶空就說明缺乏了哪一個數字。spa

複雜度分析: 該方法須要用到O(n)的額外空間,以及O(n)的時間複雜度。code

異或

對於任意整數n,有n \land n = 0,根據這個性質,就能夠預先將1-100全部的整數異或,獲得「異或和」X,而後再將X和序列中的每個元素異或,最後獲得的答案就是被摳掉的數字。排序

複雜度分析: 和作加法相似,時間複雜度爲O(n),空間複雜度爲O(1),不過使用異或方法不須要考慮溢出的狀況。遞歸

分治法

假設被摳掉的數字是n,那麼n必定屬於1~50或51~100兩個區間。若是n屬於前一個區間,那麼n也必定屬於1~25或26~50兩個區間。這個過程能夠一直劃分,直到找到這個數字。string

如今須要肯定這個數字是多少,那麼仍是相似的思想。若是被摳掉的數字屬於1~50,那麼小於等於50的數字只有49個。如今數一數a_i有多少個落入1~50區間,不妨假設只有49,那麼又能夠繼續劃分爲計算1~25中有多少個數字的問題。這個過程能夠一直劃分,直到找到這個數字。it

上面這個過程耗時最大的步驟就是統計區間的元素個數。一遍一遍掃描確定是不夠好的,能夠參考快速排序的元素劃分方式。io

  1. 初始化查找區間[p,q],一開始就是[1,100]
  2. 選擇該區間的中值k,將集合中全部小於等於k的數字和大於k的數字分開,最後得到兩個子集合[p,k][k+1,q];固然在作劃分的時候能夠順帶統計元素個數了;
  3. 若是小於等於k的元素集合數量不足,那麼缺失的數字就包含於區間[p,k],不然就在另外一個區間;
  4. 通過步驟二、3就縮小了查找區間爲原來的一半,對該區間重複步驟二、3,直到區間中只剩下一個元素,而區間對應的集合爲空的元素就是缺失的數字。

每次搜索的區間都會縮小一半,所以只須要通過O(log(n))步就能夠找到缺乏的元素。具體計算以下。用T(n)表示父查詢任務的時間複雜度,則有T(n) = T(n/2) + f(n),當n = 1時,T(n) = f(1)f(n)的時間複雜度爲O(n)

\begin{aligned}
T(n) &= T(n/2) + f(n) \\
&= T(n/4) + 2f(n) + f(n) \\
&= \dots \\
&= T(1) +f(n)\sum_{i=0}^{k-1}{2^i} \\
&= T(1) + (2^k - 1)f(n) \\
&= O(n)
\end{aligned}

複雜度分析: 已經證實時間複雜度爲O(n)。空間複雜度O(n)

找兩個數字

題目描述:從1-100中刪去任意兩個數字,而後將剩下的98個數字打亂,獲得無序序列a_1,a_2,...,a_{98}。如今須要用一種方法找到這兩個數字。

桶排序

這個方法哪怕n爲其餘值也是可使用的,十分簡單再也不贅述。

方程組

此時簡單的差值求解不奏效了,只能求解獲得兩個數字的和。若是須要解出這兩個數字,那就須要用一個等式構成方程組。

可否使用異或的方式獲取一個等式構成方程組呢?即解方程組

\begin{cases}
x + y = m \\
x \land y = n   
\end{cases}

要判斷是否可行,能夠列一下真值表:

x_i y_i + \land
0 0 0 0
0 1 1 1
1 0 1 1
1 1 0 0

暫時先不考慮進位對結果的影響,也就是最低位的狀況。顯然最低位已經沒法根據加法值和異或值肯定結果了(第二、3行),因此這個方法是不行的。

而若是使用方程組

\begin{cases}
x + y = m \\
x \times y = n   
\end{cases}

雖然理論可行,可是計算100!在程序上會致使溢出。

若是改用對數方程是否可行?

\begin{cases}
x + y = m \\
log(x) + log(y) = n   
\end{cases}

雖然可能存在精度的問題,可是仍是決定實驗一下。代碼以下,x和y的值能夠任意更換。

#include <bits/stdc++.h>
using namespace std;

int main() {
    int x = 1, y = 2;
    int sum1 = 5050, sum2 = 0.0;
    double logsum1 = 0, logsum2 = 0.0;

    for (int i = 1; i <= 100; ++i) {
        logsum1 += log(i);
    }
    for (int i = 1; i <= 100; ++i) {
        if (i == x || i == y) continue;
        logsum2 += log(i);
        sum2 += i;
    }

    double x_plus_y = sum1 - sum2;
    double x_mult_y = exp(logsum1 - logsum2);

    cout << "x + y = " << x_plus_y << endl;
    cout << "x * y = " << x_mult_y << endl;
    return 0;
}
複製代碼

通過測驗,對於上面的代碼,使用double型變量能夠提供足夠的精度,一方面是double有52bits的有效數據位,足夠精細,另外一方面是因爲100之內數的對數值並不會相差很大,因此浮點數相加並不會丟失不少精度。一樣的代碼若是使用float存儲浮點結果,就會出現精度丟失的問題。

分治

分治法仍是能夠用來解決這個問題。仍是用以前的假設:查詢區間爲[p,q],中值爲k,左查詢區間爲[p,k],右查詢區間爲[k+1,q]

不管缺失的兩個數字取自哪一個子查詢區間,區間對應元素集合就會缺乏數字,那麼就將父查詢任務轉化爲有數字缺乏的區間的子查詢任務。最差狀況下,缺失的兩個數字分別屬於兩個子查詢區間。

借鑑「找一個數字」中分治法的符號表示,有T(n) \le 2T(n/2) + f(n),當n = 1時,T(n) = f(1)f(n)的時間複雜度爲O(n)

求解遞推式:

\begin{aligned}
T(n) &\le 2T(n/2) + f(n) \\
&= 2^2T(n/4) + 2f(n) + f(n) \\
&= \dots \\
&= 2^kT(1) +f(n)\sum_{i=0}^{k-1}{2^i} \\
&= 2^kT(1) + (2^k - 1)f(n) \\
&= O(n)
\end{aligned}

複雜度分析: T(n/2) + f(n) \le T(n) \le 2T(n/2) + f(n),所以找兩個數字的時間複雜度爲O(n);空間複雜度仍是O(n)

異或

雖然加法方程和異或方程聯立沒法解得結果,可是異或結果仍是說明了x和y數位上的特色。

從異或結果中看看從1-8中摳掉2個數字,求摳掉的兩個數字。

十進制 二進制
1 0001
2 0010
3(摳掉) 0011
4 0100
5 0101
6(摳掉) 0110
7 0111
8 1000

令x=3,y=6,則異或結果爲0101,代表第1位(從低到高)和第3位不同。若是按照第1位是否1將原來的數字分紅兩組,實際上就是將找兩個數的問題轉化爲找1個數的問題。

複雜度分析 和找1數字的時候同樣,時間複雜度爲O(n),空間複雜度爲O(1)

更多數字?

若是如今問題變成了:從1-100中刪去任意n個數字,而後將剩下的100-n個數字打亂,獲得無序序列a_1,a_2,...,a_{100-n}。如今須要用一種方法找到這n個數字。

桶排序

依然可用。

分治法

可使用,可是當n較大的時候,實際上就退化成對數字排序的問題。花在遞歸函數調用上的時間影響較大。

異或

策略是不斷使用比特的特徵將找n個數的問題轉化爲找n-1個數的問題。只不太重複循環的時間複雜度就比較高。

相關文章
相關標籤/搜索