數據結構與算法:分治法應用總結

介紹分治法應用之前首先介紹分治法的概念和步驟:

分治策略:將原問題劃分爲n個規模較小而結構與原問題相似的子問題;遞歸地解決這些子問題,然後再合併其結果,就得到原問題的解。

步驟:

1、分解(Divide):將原問題分解成一系列子問題;

2、解決(Conquer):遞歸地解各子問題。若子問題足夠小,則直接求解;

3、合併(Combine):將子問題的結果合併成原問題的解。

下面主要介紹分治法的幾種應用:

一、歸併排序

歸併排序按照上述的步驟可如下操作:

1、分解:將n個元素分成各含n/2個元素的子序列;

2、解決:用歸併排序法對兩個子序列遞歸地排序;

3、合併:合併兩個已排序的子序列以得到排序結果。

歸併排序的具體步驟和實現代碼已經在前面的博客中詳細介紹

博客地址:http://blog.csdn.net/dabusideqiang/article/details/23474963


二、二分查找

二分查找法:

在有序表中,把待查找數據值與查找範圍的中間元素值進行比較:

1)待查找數據值與中間元素值正好相等,則放回中間元素值的索引;

2)待查找數據值比中間元素值小,則遞歸查找整個查找範圍的前半部分;

3)待查找數據值比中間元素值大,則遞歸查找整個查找範圍的後半部分;

4)如果最後找不到相等的值,則返回錯誤提示信息。

對應代碼如下:

/*****************二分查找*******************/

/**
* BinarySearch函數:在數組a中查找數據e (非遞歸法)
* @param [in] a:查找數組
* @param [in] e:待查找數據
* @param [in] first:起始位置
* @param [in] last:結束位置
* @return int:待查找數據所在的位置
*/ 

int BinarySearch(int a[], int e, int first, int last)
{
	if (a==NULL || first > last)
	{
		return -1;
	}
	int mid;//中間位置
	while(first <= last)
	{
		mid = (first + last) / 2;
		if (e == a[mid] )
		{
			return mid;
		}
		else if (a[mid] < e)
		{
			first = mid + 1;
		}
		else if (a[mid] > e)
		{
			last = mid - 1;
		}
	}
	return -1;
}

/**
* BinarySearch_Recursion函數:在數組a中查找數據e (遞歸法)
* @param [in] a:查找數組
* @param [in] e:待查找數據
* @param [in] first:起始位置
* @param [in] last:結束位置
* @return int:待查找數據所在的位置
*/ 
int BinarySearch_Recursion(int a[], int e, int first, int last)
{
	if (a==NULL || first > last)
	{
		return -1;
	}
	int mid = (first + last) / 2;
	if (e == a[mid])
	{
		return mid;
	}
	else if (e < a[mid])
	{
		return BinarySearch_Recursion(a, e, first, mid - 1);
	}
	else if (e > a[mid])
	{
		return BinarySearch_Recursion(a, e, mid + 1, last);
	}
	return -1;
}
二分查找時間複雜度爲O(logn)

三、求數值的整數次方(xn)x是浮點數。

1、樸素算法:

針對這個問題要考慮以下幾方面:

1)要考慮指數是0或負數的情況

2)要考慮指數爲負數且底數是0的情況

3)由於底數是double型,當判斷底數是否等於0時,不能直接用==,而是判斷它們之差的絕對值是不是在一個很小的範圍內。

對應代碼如下:

bool g_InvalidInput = false;
bool equal(double num1, double num2);
double PowerWithUnsignedExponent(double base, unsigned int exponent);
/**
* Power函數:求數值的整數次方
* @param [in] base:底數
* @param [in] exponent:指數
* @return double:返回結果
*/ 
double Power(double base, int exponent)
{
    g_InvalidInput = false;
    if(equal(base, 0.0) && exponent < 0)
    {
        g_InvalidInput = true;
        return 0.0;
    }
    unsigned int absExponent = (unsigned int)(exponent);
    if(exponent < 0)
        absExponent = (unsigned int)(-exponent);
    double result = PowerWithUnsignedExponent(base, absExponent);
    if(exponent < 0)
        result = 1.0 / result;
    return result;
}
 
/**
* PowerWithUnsignedExponent函數:求數值的非負整數次方(樸素法)
* @param [in] base:底數
* @param [in] exponent:非負指數
* @return double:返回結果
*/
double PowerWithUnsignedExponent(double base, unsigned int exponent)
{
    double result = 1.0;
    for(int i = 1; i <= exponent; ++i)
        result *= base;
    return result;
}
/**
* equal函數:判斷兩個浮點數是否相等
* @param [in] num1:浮點數1
* @param [in] num2:浮點數2
* @return bool:返回結果
*/
bool equal(double num1, double num2)
{
    if((num1 - num2 > -0.0000001)
        && (num1 - num2 < 0.0000001))
        return true;
    else
        return false;
}

注:程序中定義了全局變量g_InvalidInput,是爲了標識程序是否出錯,因爲程序出錯時返回0,而底數爲0時程序正常運行也返回0,爲了區分這個,設置一個全局變量g_InvalidInput,當程序出錯時設置爲true。

樸素算法的複雜度爲O(n)

2、分治法

上述算法中需要循環做n-1次乘法。這裏我們用分治法可以將n次方分解,比如求x8,我們只需要求x4就可以了,對x4再求平方就可以得到x8

上述表述可由下面的公式表示:


這個公式就可以通過遞歸實現了,實現代碼如下:

/**
* PowerWithUnsignedExponent函數:求數值的非負整數次方(分治法)
* @param [in] base:底數
* @param [in] exponent:非負指數
* @return double:返回結果
*/
double PowerWithUnsignedExponent(double base, unsigned int exponent)
{
    if(exponent == 0)
        return 1;
    if(exponent == 1)
        return base;
    double result = PowerWithUnsignedExponent(base, exponent >> 1);
    result *= result;
    if((exponent & 0x1) == 1)
        result *= base;
    return result;
}
分治法複雜度爲O(logn)

四、斐波那契數列(Fibonacci)

Fibonacci數列是一個比較經典的例子,很多書中介紹遞歸時都用它作爲例子。

  • F_0=0
  • F_1=1
  • F_n = F_{n-1}+ F_{n-2}(n≧2)
那麼首先我們介紹最爲熟悉的遞歸法

1、遞歸法

大家對遞歸法應該都很熟悉,直接上代碼:

/****************遞歸法*******************/
/**
* Fbi1函數:求斐波那契數列
* @param [in] n:第n項
* @return long long:返回結果
*/ 

long long Fbi1(int n) 
{
	if( n < 2 )
		return n == 0 ? 0 : 1;  
	return Fbi1(n - 1) + Fbi1(n - 2);  /* 這裏Fbi就是函數自己,等於在調用自己 */
}

我們以求f(10)爲例分析求解過程,求f(10),需要求f(9)和f(8),求f(9)需要求f(8)和f(7),這樣以此類推,可由下圖表示依賴關係。

由圖可以發現,遞歸時會重複計算一些數據,這樣會增加複雜度。下面介紹一種迭代法,避免重複計算。

2、迭代法

爲了避免重複計算,我們可以從下往上計算,可以將前一次計算的結果保存起來用於下一次計算,如先根據f(0)和f(1)求出f(2),然後利用f(1)和f(2)求出f(3)。以此類推、、、

對應代碼如下:

/****************迭代法*******************/
/**
* Fbi2函數:求斐波那契數列
* @param [in] n:第n項
* @return long long:返回結果
*/ 

long long Fbi2(int n) 
{
	if( n < 2 )
		return n == 0 ? 0 : 1;  
	long long fb1=1;
	long long fb2=0;
	long long fbn=0;
	for(int i=2;i<=n;i++)
	{
		fbn=fb1+fb2;
		fb2=fb1;
		fb1=fbn;
	}
	return fbn;
} 
上述的迭代法已經比較高效了,時間複雜度爲0(n);下面來介紹今天的主題方法,分治法,時間複雜度達到O(logn),可能比較生僻,當時可以用來理解分治法。

3、基於矩陣的分治法

介紹該方法之前,首先要介紹一個數學公式:

這個公式很容易用歸納法證明,這裏就不再證明了。

根據上述公式求f(n)可以轉化爲求矩陣的n-1次方。求數值的n次方是不是很熟悉,本文的分治法第三種應用就是求數值的整數次方,只不過這裏的數值換成了矩陣。具體方法不再贅述。

對應代碼如下:

/****************基於矩陣的分治法*******************/
/**
* 矩陣結構體Matrix2By2,用於創建和初始化矩陣
*/ 
struct Matrix2By2
{
	Matrix2By2
		(
		long long m00 = 0, 
		long long m01 = 0, 
		long long m10 = 0, 
		long long m11 = 0
		)
		:m_00(m00), m_01(m01), m_10(m10), m_11(m11) 	{
	}
	long long m_00;
	long long m_01;
	long long m_10;
	long long m_11;
};
/**
* MatrixMultiply函數:矩陣相乘
* @param [in] matrix1:矩陣1
* @param [in] matrix2:矩陣2
* @return Matrix2By2:返回矩陣結果
*/ 
Matrix2By2 MatrixMultiply
(
 const Matrix2By2& matrix1, 
 const Matrix2By2& matrix2
 )
{
	return Matrix2By2(
		matrix1.m_00 * matrix2.m_00 + matrix1.m_01 * matrix2.m_10,
		matrix1.m_00 * matrix2.m_01 + matrix1.m_01 * matrix2.m_11,
		matrix1.m_10 * matrix2.m_00 + matrix1.m_11 * matrix2.m_10,
		matrix1.m_10 * matrix2.m_01 + matrix1.m_11 * matrix2.m_11);
}
/**
* MatrixPower函數:求矩陣的整數次方
* @param [in] n:指數
* @return Matrix2By2:返回矩陣結果
*/ 
Matrix2By2 MatrixPower(unsigned int n)
{
	assert(n > 0);
	Matrix2By2 matrix;
	if(n == 1)
	{
		matrix = Matrix2By2(1, 1, 1, 0);
	}
	else if(n % 2 == 0)//偶數
	{
		matrix = MatrixPower(n / 2);
		matrix = MatrixMultiply(matrix, matrix);
	}
	else if(n % 2 == 1)//奇數
	{
		matrix = MatrixPower((n - 1) / 2);
		matrix = MatrixMultiply(matrix, matrix);
		matrix = MatrixMultiply(matrix, Matrix2By2(1, 1, 1, 0));
	}
	return matrix;
}
/**
* Fbi3函數:求斐波那契數列
* @param [in] n:第n項
* @return long long:返回結果
*/ 
long long Fbi3(unsigned int n)
{
	if( n < 2 )
		return n == 0 ? 0 : 1;  
	Matrix2By2 PowerNMinus2 = MatrixPower(n - 1);
	return PowerNMinus2.m_00;
}
針對上述的三種方法求Fibonacci數列,可以運行試試,會發現遞歸法速度明顯慢於其他兩種方法。遞歸法由於是函數調用自身,而函數調用是有時間和空間的消耗,每一次調用都需要在內存棧中分配空間以保存參數、返回地址及臨時變量,而往棧裏壓入和彈出數據都需要時間。遞歸本質是將一個問題分解成兩個或多個小問題,如果小問題存在重複,那麼就會重複計算增加複雜度。第一種遞歸法就是由於重複計算導致速度很慢。



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