數據結構與算法分析-算法分析

算法分析

算法(algorithm)是爲求解一個問題需要遵循的、被清楚地指定的簡單指令的集合。

數學基礎

定義: 如果存在正常數ccn0n_0使得當Nn0N\geq n_0T(N)cf(N)T(N)\leq cf(N),則記爲T(N)=O(f(N))T(N)=O(f(N))

定義: 如果存在正常數ccn0n_0使得當Nn0N\geq n_0T(N)cg(N)T(N)\geq cg(N),則記爲T(N)=Ω(f(N))T(N)=\Omega(f(N))

定義:T(N)=Θ(h(N))T(N)=\Theta(h(N))當且僅當T(N)=O(h(N))T(N)=O(h(N))T(N)=Ω(h(N))T(N)=\Omega(h(N))

定義: 如果T(N)=O(p(N))T(N)=O(p(N))T(N)Θ(p(N))T(N)\neq\Theta(p(N)),則T(N)=o(p(N))T(N)=o(p(N))

這些定義的目的是要在函數之間建立一種相對的級別。給定兩個函數,通常存在一些點,在這些點上一個函數的值小於另一個函數的值,因此,像f(N)<g(N)f(N)<g(N)這樣的聲明是沒有什麼意義的。於是,我們比較它們的相對增長率relative rate of growth)。

如果用傳統的不等式來計算增長率,那麼第一個定義是說T(N)T(N)的正常率小於等於f(N)f(N)的增長率。第二個定義T(N)=Ω(g(N))T(N)=\Omega(g(N))(念成“Omega”)是說T(N)T(N)的增長率大於等於g(N)g(N)的增長率。第三個定義T)N=Θ(h(N))T)N=\Theta(h(N))(念成“Theta”)是說T(N)T(N)的增長率等於h(N)h(N)增長率。最後一個定義T(N)=o(p(N))T(N)=o(p(N))(念成“小o”)說的則是T(N)T(N)的增長率小於p(N)p(N)的增長率。它不同於大O,因爲大O包含增長率相同這種可能性。

當我們說T(N)=O(f(N))T(N)=O(f(N))時,我們是在保證函數T(N)T(N)是在不快於f(N)f(N)的速度增長;因此f(N)f(N)T(N)T(N)的一個上界(upper bound)。與此同時,f(N)=Ω(T(N))f(N)=\Omega(T(N))意味着T(N)T(N)f(N)f(N)的一個下界(lower bound)。

作爲一個例子,N3N^3的增長比N2N^2快,因此我們說N2=O(N3)N^2=O(N^3)N3=Ω(N2)N^3=\Omega(N^2)f(N)=N2f(N)=N^2g(N)=2N2g(N)=2N^2以相同的速率增長時,從而f(N)=O(g(N))f(N)=O(g(N))f(N)=Ω(g(N))f(N)=\Omega(g(N))都是正確的。直觀地說,如果g(N)=2N2g(N)=2N^2,那麼g(N)=O(N4)g(N)=O(N3)g(N)=O(N2)g(N)=O(N^4),g(N)=O(N^3),g(N)=O(N^2)從技術上都是成立的,但最後一個選擇是最好的答案。

法則1

如果T1(N)=O(f(N)T_1(N)=O(f(N)T2(N)=O(g(N))T_2(N)=O(g(N)),那麼

  • T1(N)+T2(N)=max(O(f(N),O(g(N))T_1(N)+T_2(N)=\max(O(f(N),O(g(N))
  • T1(N)T2(N)=O(f(N)g(N))T_1(N)*T_2(N)=O(f(N)*g(N))。

法則2

如果T(N)T(N)是一個kk次多項式,則T(N)=Θ(Nk)T(N)=\Theta(N^k)

法則3

對任意常數k,logkN=O(N)k,\log^kN=O(N)。它告訴我們對數增長得非常緩慢。

函數 名稱
cc 常數
logNlogN 對數級
log2Nlog^2N 對數平方根
NN 線性級
NlogNN\log N
N2N^2 平方級
N3N^3 立方級
2N2^N 指數級

我們總能通過計算極限limx0f(N)g(N)\lim_{x\to0}\frac{f(N)}{g(N)}來確定兩個函數f(N)f(N)g(N)g(N)的相對增長率,必要的時候可以使用洛必達法則。該極限可以有四種可能的值:

  • 極限是00:這意味着f(N)=o(g(N))f(N)=o(g(N))
  • 極限是c0c\neq 0:這意味着f(N)=Θ(g(N))f(N)=\Theta(g(N))
  • 極限是\infty:這意味着g(N)=o(f(N))g(N)=o(f(N))
  • 極限擺動:二者無關

運行時間計算

一個簡單的例子

這裏是計算i=1Ni3\sum_{i=1}^Ni^3的一個簡單的程序片段:

	int Sum(int N) {
        int i, PartialSum;
/* 1*/	PartialSum = 0;
/* 2*/  for (i = 1; i <= N; i++)
/* 3*/      PartialSum += i * i * i;
/* 4*/  return PartialSum;
    }

對這個程序的分析很簡單。聲明不計時間,第1行和第4行各佔一個時間單元。第3行每執行一次佔用四個時間單元(兩次乘法、一次加法和一次賦值),每執行NN次共佔用4N4N個時間單元。第二行在初始化ii、測試iNi\leq N和對ii的自增運算中隱含着開銷。所有這些的總開銷是初始化1個時間單元,所有的測試N+1N+1個時間單元,以及所有的自增運算NN個時間單元,共2N+22N+2。我們忽略調用函數和返回值的開銷,得到總量是6N+46N+4。因此,我們說該函數是O(N)O(N)

如果我們每次分析一個程序都要演示所有這些工作,那麼這項任務很快就會變成不可行的工作。幸運的是,由於我們有了大OO的結果,因此就存在許多可以採取的捷徑並且不影響最後的結果。這使得我們得到若干一般法則。

一般法則

法則1——for循環

一次for循環的運行時間至多是該for循環內語句(包括測試)的運行時間乘以迭代的次數。

法則2——嵌套的for循環

從裏向外分析這些循環。在一組嵌套循環內部的一條語句總的運行時間爲該語句的運行時間乘以該組所有的for循環的大小的乘積。

作爲一個例子,下列程序片段爲O(N2)O(N^2)

for (i=0; i<N; i++)
    for(j=0; j<N; i++)
        k++;

法則3——順序語句

將各個語句的運行時間求和即可(這意味着,其中的最大值就是所得的運行時間)。

作爲一個例子,下面程序片段先用去O(N)O(N),再花費O(N2)O(N^2),總的開銷也是O(N2)O(N^2)

for(i=0; i<N; i++)
    A[i] = 0;
for(i=0; i<N; i++)
    for(j=0; j<N; j++)
        A[i] += A[j] + i + j;

法則4——if/else語句

對於程序片段:

if(Condition)
    S1
else
    S2

一個if/else語句的運行時間從不超過判斷再加上S1和S2中運行時間長者的總的運行時間。

其他的法則都是顯然的,但是,分析的基本策略是從內部(或最深層部分)向外展開的。如果有函數調用,那麼這些調用要首先分析。如果有遞歸過程,那麼存在幾種選擇。若遞歸只是被薄面紗遮住的for循環,則分析通常是很簡單的。例如,下面的函數實際上就是一個簡單的循環,從而其運行時間爲O(N)O(N)

long int Factorial(int N) {
    if (N <= 1)
        return 1;
    else
        return N * Factorial(N - 1);
}

這個例子中對遞歸的使用實際上並不好。當遞歸被正常使用時,將其轉換成一個簡單的循環結構是相當困難的。在這種情況下,分析將涉及求解一個遞推過程。爲了觀察到這種可能發生的情形,考慮下列程序,實際上它對遞歸使用的效率低得令人詫異。

long int Fib(int N) {
/* 1*/	if (N <= 1)
/* 2*/		return 1;
    	else
/* 3*/		return Fib(N - 1) + Fib(N - 2);
}

T(N)T(N)爲函數Fib(N)的運行時間。如果N=0N=0N=1N=1,則運行時間是某個常數值,即第一行上做判斷以及返回所用的時間。若N>2N>2,則執行該函數時間是第一行上的常數工作加上第三行上的工作。第三行由一次加法和兩次函數調用組成。由於函數調用不是簡單的運算,必須通過它們本身來分析。第一次函數調用是Fib(N - 1),從而按照TT的定義,它需要T(N1)T(N-1)個時間單元。類似的論證指出,第二次函數調用需要T(N2)T(N-2)個時間單元。此時總的時間需求爲T(N1)+T(N2)+2T(N-1)+T(N-2)+2,其中"2"指的是第一行上的工作加上第三行上的加法。於是對於N2N\geq2我們有下列關於Fib(N)的運行時間公式:

T(N)=T(N1)+T(N2)+2T(N)=T(N-1)+T(N-2)+2

但是Fib(N)=Fib(N1)+Fib(N2)Fib(N)=Fib(N-1)+Fib(N-2),因此由歸納法容易證明T(N)Fib(N)T(N)\geq Fib(N)。而Fib(N)<(53)NFib(N)< (\frac{5}{3})^N,類似的計算可以證明(對於N>4N>4Fib(N)(32)NFib(N)\geq (\frac 32)^N,可見,這個程序的運行時間以指數的速度增長。

最大子序列和問題的解

給定整數A1,A2,,ANA_1,A_2,\cdots,A_N(可能有負數),求k=ijAk\sum_{k=i}^jA_k的最大值(爲方便起見,如果所有整數均爲負數,則最大子序列和爲0)。

我們將要敘述四個算法來求解最大子序列和問題。

第一個算法爲:

int MaxSunSequenceSum(coonst int A[], int N) {
		int ThisSum, MaxSum, i, j, k;
/* 1*/	MaxSum = 0;
/* 2*/	for(i=0; i<N; i++)
/* 3*/		for(j=i; j<N; j++) {
/* 4*/			ThisSum = 0;
/* 5*/			for(k=i; k<=j; j++)
/* 6*/				ThisSum += A[k];
/* 7*/			if(ThisSum > MaxSum)
/* 8*/				MaxSum = ThisSum;
			}
/* 9*/	return MaxSum;
}

它只是窮舉式地嘗試所有的可能。再有,本算法不計算實際的子序列;實際的計算還要添加一些額外的程序。

該算法肯定會正確運行。運行時間爲O(N3)O(N^3),這完全取決於第5行和第6行,第6行由一個含於三重嵌套for循環中的O(1)O(1)語句組成。第2行上的循環大小爲NN

第2個循環大小爲NiN-i,它可能要小,但也可能是NN。我們必須假設最壞的情況,而這可能會使得最終的界有些大。第3個循環的大小爲ji+1j-i+1,我們也要假設它的大小爲NN。因此總數爲O(1NNN)=O(N3)O(1\cdot N\cdot N\cdot N)=O(N^3)。語句1總共的開銷只是O(1)O(1),而語句7和8總共開銷也只不過O(N2)O(N^2),因爲它們只是兩層循環內部的簡單表達式。

事實上,考慮到這些循環的實際大小,更精確的分析指出答案是Θ(N3)\Theta(N^3),而我們上面的估計高出個因子6(不過這並無大礙,因爲常數不影響數量級)。一般來說,在這類問題中上述結論是正確的。精確的分析由i=0N1j=iN1k=ij1\sum_{i=0}^{N-1}\sum_{j=i}^{N-1}\sum_{k=i}j1得到,該和指出程序的第6行被執行的次數。經過計算我們得到i=0N1j=iN1k=ij1=N3+3N2+2N6\sum_{i=0}^{N-1}\sum_{j=i}^{N-1}\sum_{k=i}j1=\frac{N^3+3N^2+2N}{6}

下面指出一種改進的算法,該算法顯然是O(N2)O(N^2)

int MaxSunSequenceSum(const int A[], int N) {
		int ThisSum, MaxSum, i, j;
/* 1*/	MaxSum = 0;
/* 2*/	for(i=0; i<N; i++) {
/* 3*/		ThisSum = 0;
/* 4*/		for (j=i; j<N; j++) {
/* 5*/			ThisSum += A[j];
/* 7*/			if(ThisSum > MaxSum)
/* 8*/				MaxSum = ThisSum;
			}
		}
		return MaxSum;
}

對於這個問題有一個遞歸和相對複雜的O(NlogN)O(N\log N)解法,要是真的沒出現O(N)O(N)(線性的)解法,這個算法就會是體現遞歸威力的極好的範例了。

該算法採用一種“分治(divide-and-conquer)”策略。其想法就是把問題分成兩個大致相等的子問題,然後遞歸地對它們求解,這是“分”部分。“治”階段將兩個子問題的解合併到一起並可能再做些少量的附加工作,最後得到整個問題的解。

在這個問題中,最大子序列和可能在三處出現。或者整個出現在輸入數據的左半部,或者整個出現在右半部,或者跨越輸入數據的中部從而佔據左右兩半部分。前兩種情況可以遞歸求解。第三種情況的最大和可以通過求出前半部分的最大和(包含前半部分的最後一個元素)以及後半部分的最大和(包含後半部分的第一個元素)而得到。然後將這兩個和加在一起。作爲一個例子,考慮下列輸入:

前半部分 | 後半部分

4 -3 5 -2 -1 2 6 -2

其中前半部分的最大子序列和爲6而後半部分的最大子序列和爲8。

前半部分包含其最後一個元素的最大和是4,而後半部分包含其第一個元素的最大和是7。因此,橫跨這兩部分且通過中間的最大和爲4+7=114+7=11

下面提出了實現這種策略的一種實現手段。

int Max3(int a, int b, int c)
{
    return a > b ? a > c ? a : c : b > c ? b : c;
}

int MaxSubSum(const int A[], int Left, int Right)
{
    int MaxLeftSum, MaxRightSum;
    int MaxLeftBorderSum, MaxRightBorderSum;
    int LeftBorderSum, RightBorderSum;
    int Center, i;

    /* 1*/ if (Left == Right) /* Base Case */
        /* 2*/ if (A[Left] > 0)
            /* 3*/ return A[Left];
        else
            /* 4*/ return 0;

    /* 5*/ Center = (Left + Right) / 2;
    /* 6*/ MaxLeftSum = MaxSubSum(A, Left, Center);
    /* 7*/ MaxRightSum = MaxSubSum(A, Center + 1, Right);

    /* 8*/ MaxLeftBorderSum = 0;	LeftBorderSum = 0;
    /* 9*/ for (i = Center; i >= Left; i--)
    {
        /* 10*/ LeftBorderSum += A[i];
        /* 11*/ if (LeftBorderSum > MaxLeftBorderSum)
            /* 12*/ MaxLeftBorderSum = LeftBorderSum;
    }

    /* 13*/ MaxRightBorderSum = 0;	RightBorderSum = 0;
    /* 14*/ for (i = Center + 1; i <= Right; i++)
    {
        /* 15*/ RightBorderSum += A[i];
        /* 16*/ if (RightBorderSum > MaxRightBorderSum)
            /* 17*/ MaxRightBorderSum = RightBorderSum;
    }
    /* 18*/ return Max3(MaxLeftSum, MaxRightSum, MaxLeftBorderSum + MaxRightBorderSum);
}

int MaxSubSequnceSum3(const int A[], int N)
{
    return MaxSubSum(A, 0, N - 1);
}

第1行至第4行處理基準情況。如果Left == Right,那麼只有一個元素,並且當該元素非負時它就是最大子序列和。Left > Right的情況是不可能出現的,除非NN是負數。第6行和第7行執行兩次遞歸調用。我們可以看到,遞歸調用總是小於原問題的問題進行。第8行至12行以及第13行至第17行計算達到中間分界處的兩個最大和的和數。這兩個最大和的和爲擴展到左右兩邊的最大和。

T(N)T(N)是求解大小爲NN的最大子序列和問題所花費的時間。如果N=1N=1,則算法3執行程序第1行到第4行花費某個時間常量,我們稱之爲一個時間單元。於是,T(1)=1T(1)=1。否則,程序必須運行兩次遞歸調用,即在第9行和第17行之間的兩個for循環,還需某個小的簿記量,如在第5行和第18行。這兩個for循環總共接觸到A0A_0AN1A_{N-1}的每一個元素,而在循環內部的工作量是常量,因此,在第9到17行花費時間爲O(N)O(N)。第1行到第5行,第8、13和18行上的程序的工作量都是常量,從而與O(N)O(N)相比可以忽略。其餘就是第6、7行上運行的工作。這兩行求解大小爲N2\frac N2的子序列問題(假設NN是偶數)。因此 ,這兩行每行花費T(N2)T(\frac N2)個時間單元,共花費2T(N2)2T(\frac N2)個時間單元。算法3花費的總的時間爲2T(N2)+O(N)2T(\frac N2)+O(N)。我們得到方程組:

T(1)=1T(1)=1

T(N)=2T(N2)+O(N)T(N)=2T(\frac N2)+O(N)

爲了簡化計算,可以用NN代替上面方程中的O(N)O(N)項;由於T(N)T(N)最終還是要用大OO表示的,因此這麼做並不影響答案。如果T(N)=2T(N2)+NT(N)=2T(\frac N2)+N,且T(1)=1T(1)=1,那麼T(2)=4=22,T(4)=12=34,T(8)=32=48T(2)=4=2*2,T(4)=12=3*4,T(8)=32=4*8,以及T(16)=80=516T(16)=80=5*16。其形式是顯然並且可以得到,即若N=2kN=2^k,則T(N)=N(k+1)=NlogN+N=O(NlogN)T(N)=N*(k+1)=N\log N+N=O(N\log N)

下面是第4種算法的代碼:

int MaxSubSequenceSum(const int A[], int N)
{
    	int ThisSum, MaxSum, j;
/* 1*/	ThisSum = MaxSum = 0;
/* 2*/	for (j = 0; j < N; j++)
		{
/* 3*/		ThisSum += A[j];
/* 4*/		if (ThisSum > MaxSum)
/* 5*/			MaxSum = ThisSum;
/* 6*/		else if (ThisSum < 0)
/* 7*/				ThisSum = 0;
    	}
/* 8*/	return MaxSum;
}

該算法的一個附帶優點是,它只對數據進行一次掃描,一旦A[i]被讀入並被處理,它就不再需要被記憶。因此,如果數組在磁盤或磁帶上,它就可以被順序讀入,在主存中不必存儲數組的任何部分。不僅如此,在任意時刻,算法都能對它已經讀入的數據給出子序列問題的正確答案(其他算法不具有這個特性)。具有這種特性的算法叫做聯機算法(online algorithm)。僅需要常量空間並以線性時間運行的聯機算法幾乎是完美的算法。

運行時間中的對數

分析算法最混亂的方面大概集中在對數上面。我們已經看到,某些分治算法將以O(NlogN)O(NlogN)時間運行。除分治算法外,可將對數最常出現的規律概括爲下列一般法則:如果一個算法用常數時間(O(1)O(1))將問題的大小削減爲其一部分(通常是12\frac 12),那麼該算法就是O(NlogN)O(N\log N)。另一方面,如果使用常數時間只是把問題減少一個常數(如將問題減少1),那麼這種算法就是O(N)O(N)的。

顯然,只有一些特殊種類的問題才能夠呈現除O(logN)O(\log N)型。例如,若輸入NN個數,則一個算法只是把這些數讀入就必須耗費Ω(N)\Omega(N)的時間量。因此,當我們談到這類問題的O(logN)O(\log N)算法時,通常是假設輸入數據已經提前讀入。我們提供具有對數特點的三個例子。

對分查找(binary search)

給定一個整數XX和整數A0,A1,,AN1A_0,A_1,\cdots ,A_{N-1},後者已經預先排序並在內存中,求使得Ai=XA_i=X的下標ii,如果XX不再數據中,則返回i=1i=-1*

下面給出代碼實現:

int BinarySearch(const ElementType A[], ElementType X, int N)
{
    	int Low, Mid, High;
/* 1*/	Low = 0;	High = N - 1;
/* 2*/	while (Low <= High)
		{
/* 3*/		Mid = (Low + High) / 2;
/* 4*/		if (A[Mid] < X)
/* 5*/		Low = Mid + 1;
/* 6*/		else if (A[Mid] > X)
/* 7*/				High = Mid - 1;
				else
/* 8*/				return Mid; /* Found */
		}
/* 9*/	return NotFound; /* NotFound is defined as -1 */
}

顯然,每次迭代在循環內的所有工作花費爲O(1)O(1),因此分析需要確定循環的次數。循環從HighLow=N1High-Low=N-1開始並在HighLow1High-Low\geq-1結束。每次循環後HighLowHigh-Low的值至少將該次循環前的值折半;於是,循環的次數最多爲log(N1)+2\left \lceil log(N-1) \right \rceil+2。(例如,若HighLow=128High-Low=128,則在各次迭代後HighLowHigh-Low的最大值是64,32,16,8,4,2,1,0,-1。)因此,運行時間是O(logN)O(\log N)

歐幾里得算法

計算最大公因數的歐幾里得算法。兩個整數的最大公因數(GcdGcd)是同時整數二者的最大整數。於是,Gcd(50,15)=5Gcd(50,15)=5。下面代碼實現這個算法,假設MNM\geq N。(如果N>MN>M,則循環的第一次迭代將它們互相交換)。

unsigned int Gcd(unsigned int M, unsigned int N)
{
    	unsigned int Rem;
/* 1*/	while (N > 0)
    	{
/* 2*/		Rem = M % N;
/* 3*/		M = N;
/* 4*/		N = Rem;
		}
    	return M;
}

算法通過連續計算餘數直到餘數是-爲止,最後的非零餘數就是最大公因數。因此,如果M=1989M=1989N=1590N=1590,則餘數序列是399,393,6,3,0。從而,Gcd(1989,1590)=3Gcd(1989,1590)=3

在兩次迭代以後,餘數最多是原始值的一半。這就證明了,迭代次數至多是2logN=O(logN)2\log N=O(\log N)從而得到運行時間。

定理2.1

如果M>NM>N,則Mmod  N<M2M\mod N<\frac M2

歐幾里得算法在平均情況下的性能需要大量篇幅的高度複雜的數學分析,其迭代的平均次數約爲12ln2lnNπ2+1.47\frac{12\ln2\ln N}{\pi^2}+1.47

冪運算

將用乘法的次數作爲運行時間的度量。

計算XNX^N的常見的算法是使用N1N-1次乘法自乘。下面的 遞歸算法更好:

long int Pow(long int X, unsigned int N)
{
/* 1*/	if (N == 0)
/* 2*/		return 1;
/* 3*/	if (N == 1)
/* 4*/		return X;
/* 5*/	if (IsEven(N))
/* 6*/		return Pow(X * X, N / 2);
		else
/* 7*/		return Pow(X * X, N / 2) * X;
}

第1行至第4行處理基準情形。如果NN是偶數,我們有XN=XN2XN2X^N=X^{\frac N2}\cdot X^{\frac N2},如果NN是奇數,則XN=XN12XN12XX^N=X^{\frac{N-1}2}\cdot X^{\frac{N-1}2}\cdot X

例如,爲了計算X62X^{62},算法將如下進行,它只用到9次乘法:

X3=(X2)X,X7=(X3)2X,X15=(X7)2X,X31=(X15)2X,X62=(X31)2X^3=(X^2)X,X^7=(X^3)^2X,X^{15}=(X^7)^2X,X^{31}=(X^{15})^2X,X^{62}=(X^{31})^2

顯然,所需要的乘法次數最多是2logN2\log N,因爲把問題分半最多需要兩次乘法(如果NN是奇數)。

第3行到第4行實際上不是必須的,因爲如果NN是1,那麼第7行將做同樣的事情。第7行還可以寫成

/* 7*/	return Pow(X, N - 1) * X;

而不影響程序的正確性。事實上,程序仍將以O(logN)O(\log N)運行,因爲乘法的序列同以前一樣。

檢驗你的分析

一旦分析進行過後,則需要看一看答案是否正確,是否儘可能地好。一種實現方法是編程並比較實際觀察到的運行時間與通過分析所描述的運行時間是否相匹配。當NN擴大一倍時,則線性程序的運行時間乘以因子22,二次程序的運行時間乘以因子44,而三次程序則乘因子88。以對數時間運行的程序當NN增加一倍時其運行時間只是多加一個常數,而以O(NlogN)O(N\log N)運行的程序則花費比在相同環境下運行時間的兩倍稍多一些的時間。如果低階項的係數相對地大,並且NN又不是足夠地大,那麼運行時間的變化量很難觀察清楚。

檢驗一個程序是否時O(f(N))O(f(N))的另一個常用的技巧是對NN的某個範圍(通常用2的倍數隔開)計算比值T(N)f(N)\frac{T(N)}{f(N)},其中T(N)T(N)是憑經驗觀察到的運行時間。如果f(N)f(N)是運行時間的理想近似,那麼所算出的值收斂於一個正常數。如果f(N)f(N)估計過大,則算出的值收斂於00。如果f(N)f(N)估計過低從而程序不是O(f(N))O(f(N))的,那麼算出的值發散。

發佈了30 篇原創文章 · 獲贊 16 · 訪問量 2979
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章