算法学习笔记:分治策略

        分治策略是一种常用的算法设计技术,适用分治策略设计的算法通常是递归算法。字面上的解释是【分而治之】,就是把一个复杂的问题分成两个或更多的相同的或相似的子问题,直到最后子问题可以简单的直接求解,原问题的解即子问题的解的合并。

        这个技巧是很多高效算法的基础,如排序算法(快速排序、归并排序)、傅里叶变换(快速傅里叶变化)。

        递归和分治的区别:

  • 递归和分治是两个不同维度的概念
  • 递归是程序的是实现方式,指程序调用自身
  • 分治是一种算法,其思想是将原问题拆分味多个无重叠的子问题,当子问题解决后台合并子问题得到原问题的解
  • 递归可以用来实现分治,也可以实现别的算法,如二叉树的遍历,图的深度优先遍历等

分治算法通常以数学归纳法来验证,而它的计算则多数以解递归关系来判定。

1、折半查找

在升序的数组中查找关键字,找到了返回下标,没有找到返回-1.

int _BinSearch(int* arr, int low, int high, int key)
{
	if (high < low)//没有数据
		return -1;
	int mid = low + (high - low) / 2;
	if (arr[mid] == key)//找到了
		return mid;
	else if (arr[mid] > key)//在左边找
		return _BinSearch(arr, low, mid - 1, key);
	else
		return _BinSearch(arr, mid + 1, high, key);
}

//在长度为len的arr数组中查找关键字key
int BinSearch(int* arr, int len, int key)
{
	return _BinSearch(arr, 0, len - 1, key);
}

int main()
{
	int arr[] = { 1,3,5,6,7,9,11,12,25,29 };//10个元素
	printf("%d\n", BinSearch(arr, sizeof(arr) / sizeof(arr[0]), 11));
	printf("%d\n", BinSearch(arr, sizeof(arr) / sizeof(arr[0]), 29));
	printf("%d\n", BinSearch(arr, sizeof(arr) / sizeof(arr[0]), 6));
	printf("%d\n", BinSearch(arr, sizeof(arr) / sizeof(arr[0]), 0));

	return 0;
}

使用STL的二分查找

int main()
{
	int arr[] = { 1,3,5,6,7,9,11,12,25,29 };
	int len = sizeof(arr) / sizeof(arr[0]);
	if (binary_search(arr, arr + len, 11))
		cout << "11找到了" << endl;
	else
		cout << "11没有找到" << endl;
	return 0;
}

但binary_search算法只能判断要找的值在不在容器中,不能确定位置,如果需要确切的位置,需要使用equal_range

2、快速排序

给的一组无序数据,利用快速排序使其有序

int Partition(int* arr, int low, int high)//快速排序的一趟划分
{
	int tmp = arr[low];//基准
	while (low < high)
	{
		while (low < high && tmp <= arr[high])//从后往前,找比基准小的数据
			high--;
		arr[low] = arr[high];
		while (low < high && tmp >= arr[low])//从前往后,找比基准大的数据
			low++;
		arr[high] = arr[low];
	}
	arr[low] = tmp;
	return low;
}

void QickSort(int* arr, int low, int high)//快速排序的递归
{
	if (low < high)
	{
		int par = Partition(arr, low, high);
		QickSort(arr, low, par - 1);//递归遍历左区间
		QickSort(arr, par + 1, high);//递归遍历右区间
	}
}

//快速排序
void Qsort(int* arr, int len)
{
	QickSort(arr, 0, len - 1);
}

int main()
{
	int arr[] = { 5,12,34,66,45,24,90,65,43,21,66,4,8,9 };
	Qsort(arr, sizeof(arr) / sizeof(arr[0]));
	for (int i = 0; i <14; i++)
	{
		std::cout << arr[i] << " ";
	}
	cout << endl;

	for (auto x : arr)
		cout << x << " ";
	cout << endl;

	return 0;
}

3、棋盘覆盖

        在一个2^k * 2^k个方格组成的棋盘中,若恰有一个方格与其它方格不同,则称该方格为特殊方格,且称棋盘为特殊的棋盘。显然特殊方格在棋盘上出现的位置有4^k种情形,因而对任何k>=0,有4^k种不同的特殊棋盘。下图的特殊棋盘是当k=2时16个特殊棋盘种的一个。

        在棋盘覆盖问题中,我们要用下图所示的4种不同形态的L骨型牌覆盖一个给定的特殊棋盘上除特殊方格以外的所有方格,且任何2个L型骨牌不得重叠覆盖。

        易知,在任何一个2^k * 2^k的棋盘覆盖中,用到的L型骨牌个数恰为(4^k-1)/3。

        用分治策略,我们可以设计除解棋盘覆盖问题的一个简捷的算法。

        当k>0时,我们将2^k * 2^k棋盘分割为4个2^(k-1) * 2^(k-1)子棋盘如下图所示。

        特殊方格必位于4个较小子棋盘之一中,其余3个子棋盘中无特殊方格。为了将这3个无特殊方格的子棋盘转化为特殊棋盘,我们可以用一个L型骨牌覆盖这3个较小棋盘的会合处,如下图所示

        这3个子棋盘上被L型骨牌覆盖的方格就称为该棋盘上的特殊方格,从而将原问题转化为4个较小规模的棋盘覆盖问题。递归地使用这种分割,直至棋盘简化为1*1棋盘。

        实现这种分治策略的算法ChessBoard如下:

#define K 2
#define SIZE 1<<K  //棋盘大小(边长),K为2时边长为4,K为3时边长为8
int Board[SIZE][SIZE] = { 0 };//棋盘
int tile = 0;//L型骨牌的编号

//tr:棋盘左上角的行号
//tc:棋盘左上角的列号
//dr:特殊方格所在的行号
//dc:特殊方格所在的列号
//size:棋盘的边长,棋盘规格为size*size
void ChessBoard(int tr, int tc, int dr, int dc, int size)//覆盖特殊棋盘
{
	if (size == 1)//边长是1,只能是特殊方格,没有其它方格需要覆盖
		return;
	int t = ++tile;//L型骨牌号
	int s = size / 2;//分割棋盘

	//覆盖左上角子棋盘
	if (dr < tr + s && dc < tc + s)//特殊方格在左上角子棋盘
		ChessBoard(tr, tc, dr, dc, s);
	else//特殊方格不在左上角子棋盘
	{
		Board[tr + s - 1][tc + s - 1] = t;//用t号L型骨牌覆盖右下角
		ChessBoard(tr, tc, tr + s - 1, tc + s - 1, s);//覆盖其余方格
	}

	//覆盖右上角子棋盘
	if (dr < tr + s && dc >= tc + s)//特殊方格在右上角子棋盘
		ChessBoard(tr, tc + s, dr, dc, s);
	else//特殊方格不在右上角子棋盘
	{
		Board[tr + s - 1][tc + s] = t;//用t号L型骨牌覆盖左下角
		ChessBoard(tr, tc + s, tr + s - 1, tc + s, s);//覆盖其余方格
	}

	//覆盖左下角子棋牌
	if (dr >= tr + s && dc < tc + s)//特殊方格在左下角棋盘
		ChessBoard(tr + s, tc, dr, dc, s);
	else//特殊方格不在左下角子棋盘
	{
		Board[tr + s][tc + s - 1] = t;//用t号L型骨牌覆盖右上角
		ChessBoard(tr + s, tc, tr + s, tc + s - 1, s);//覆盖其余方格
	}

	//覆盖右下角子棋盘
	if (dr >= tr + s && dc >= tc + s)//特殊方格在右下角子棋盘
		ChessBoard(tr + s, tc + s, dr, dc, s);
	else//特殊方格不在右下角子棋盘
	{
		Board[tr + s][tc + s] = t;//用t号L型骨牌覆盖左上角
		ChessBoard(tr + s, tc + s, tr + s, tc + s, s);//覆盖其余方格
	}
}

//输出整个棋盘的数据
void Show()
{
	for (int i = 0; i < SIZE; i++)
	{
		for (int j = 0; j < SIZE; j++)
		{
			printf("%-3d", Board[i][j]);
		}
		printf("\n");
	}
}

int main()
{
	int dr = 3;//特殊方格的行号,这个可以是合法的任意值
	int dc = 2;//特殊方格的列号,这个可以是合法的任意值
	ChessBoard(0, 0, dr, dc, SIZE);
	Show();

	return 0;
}

时间复杂度分析

        设T(k)是算法ChessBoard覆盖一个2^k * 2^k棋盘所需的时间,则从算法的分治策略可知,T(k)满足如下递归方程:

T(k)=\binom{\begin{Bmatrix} O(1) &k=0 \\4T(k-1)+O(1) &k>0 \end{Bmatrix}}{}

        解此递归方程可得T(k)=O(4^k),由于覆盖一个2^k * 2^k棋盘所需的L型骨牌个数为(4^k-1)/3,故算法ChessBoard是最优的算法。

4、线性时间选择

        给定n个元素和一个整数k,1<=k<=n,要求找出这n个元素中第k小的元素,即如果将这n个元素依其线性序排列时,排在第k个位置的元素即为我们要找的元素。当k=1时,就是要找最小元素;当k=n时,就是要找最大元素;当k=(n+1)/2时,称为找中位数。

        在某些特殊情况下,很容易设计出解决选择问题的线性时间算法。例如,找n个元素的最小元素和最大元素显然可以在O(n)时间完成。而对于一般的k的选择问题也可以在O(n)时间内得到解决。下面我们讨论解一般选择问题的一个分治算法RandomizedSelect,该算法实际上是模仿快速排序算法设计出来的。其基本思想也是对输入数组进行递归划分。与快速排序算法不同的是,它只对划分出的子数组之一进行递归处理。

int Partition(int* arr, int start, int end)
{
	int tmp = arr[start];//基准
	while (start < end)
	{
		while (start < end && arr[end] >= tmp)
			end--;
		arr[start] = arr[end];
		while (start < end && arr[start] <= tmp)
			start++;
		arr[end] = tmp;
	}
	arr[start] = tmp;
	return start;
}

//找第k小的数字,注意k从1开始
int SelectK(int* arr, int start, int end, int k)
{
	int par = Partition(arr, start, end);
	if (par == k - 1)
		return arr[par];
	else if (k <= par)//更小,在前面找
	{
		return SelectK(arr, start, par - 1, k);
	}
	else//更大
	{
		return SelectK(arr, par + 1, end, k);
	}
}

int main()
{
	int arr[] = { 3,6,9,1,2,5,10 };
	int len = sizeof(arr) / sizeof(arr[0]);
	printf("最小的数字:%d\n", SelectK(arr, 0, len - 1, 1));
	printf("最大的数字:%d\n", SelectK(arr, 0, len - 1, len));
	printf("中位数:%d\n", SelectK(arr, 0, len - 1, (len + 1) / 2));
}

5、面试题16:数值的整数次方

实现pow(x,y),即计算x的y次幂函数。不得使用库函数,同时不需要考虑大多数问题。

这个题需要注意y<=0的情况,这是一个陷阱。

算法1:直接相乘

double Pow(double x, int y)//这个函数只考虑y>0
{
	double tmp = 1;//乘积
	for (int i = 0; i < y; i++)//只考虑y>0的情况
		tmp *= x;
	return tmp;
}

double myPow(double x, int y)//O(n)
{
	if (y == 0)
		return 1;
	if (x == 0)
		return 0;

	long long z = y > 0 ? y : -y;//z保存y的绝对值
	double tmp = Pow(x, z);
	return y > 0 ? tmp : 1 / tmp;
}

int main()
{
	printf("%.3lf\n", myPow(2, 3));
	printf("%.3lf\n", myPow(2, 0));
	printf("%.3lf\n", myPow(2, -3));
	printf("%.3lf\n", myPow(1,2147483647));//这一句执行时间较长,在线答题会超时
}

算法2:利用分治算法计算

double Pow(double x, unsigned int n)
{
	if (n == 0)
		return 1;
	if (n == 1)
		return x;
	double tmp = Pow(x, n / 2);//计算x的n/2次方
	tmp *= tmp;
	if (n % 2 == 1)//奇数
		tmp *= x;
	return tmp;
}

double Mypow(double x, int y)//y一定是非负
{
	if (y == 0)
		return 1;
	if (x == 0)//不太好
		return 0;
	long long z = y > 0 ? y : -(long long)y;//z保存y的绝对值
	double tmp = 1;
	tmp = Pow(x, z);
	return y > 0 ? tmp : 1 / tmp;	
}

int main()
{
	printf("%.3lf\n", Mypow(2, 3));
	printf("%.3lf\n", Mypow(2, 0));
	printf("%.3lf\n", Mypow(2, -3));
	printf("%.3lf\n", Mypow(1,2147483647));

	return 0;
}

        算法2的时间复杂度为O(logn)。

  • 24
    点赞
  • 29
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值