P19 分治策略的基本思想
分治算法的算法设计
1、确定子问题
2、确定递归的边界(问题规模小于c是可以直接求解)
3、确定解的递推方程
相当于规模为p的问题
1、当问题规模小于c是可以直接求解
2、将问题p划分为k个子问题
3、子问题求解
4、将子问题的解合并
以下是分治算法的实例运用
二分检索问题
问题输入:顺序数组T,下标从 l 到 r ;数x;
问题输出:j //若x在T中,j为下标;否则为0
分治求解:
1、确定子问题
首先确定原问题形式为 BinnarySearch(T,l,r, x);
子问题缩小问题的规模,也就是缩小T的规模,并且是原问题的解不受影响;
由于T是顺序数组,通过x与T的中位数比较,可以将原问题归结为规模减半的子问题;
2、递归的边界
当x与中位数相等时返回或者当T的规模等于1时返回;
复杂度分析
T(n) = T(n/2) + 1;
T(1) = 1;
递归树求解可知T(n) = logn
老师的分析
二分检索迭代方式伪代码
二分 检索设计
复杂度分析
java代码实现
public int binarySearch(Integer[] arr, int target, int start, int end) {
int mid = (end - start) / 2 + start;
if (target == arr[mid]) {
return mid;
}
if(end - start == 0) {
return -1;
}
if (target > mid) {
return binarySearch(arr, target, mid + 1, end);
} else {
return binarySearch(arr, target, start, mid - 1);
}
}
二分归并排序
问题输入:数组T【p…r】;
问题输出:元素按从小到大的顺序排列
分治求解:
1、确定子问题
首先确定原问题形式为 MergeSort(T,start, end);
子问题缩小问题的规模,也就是缩小T的规模,并且使原问题的解不受影响;
把原问题均分为两个相等的规模子问题,然后两个子问题合并求得原始问题;
2、递归的边界
边界可以当规模为1时直接返回进行合并;
也可以当规模为2时,比较一次得到正确的顺序后返回进行合并;
3、解的递推方程
MergeSort(T,start, end) = MergeSort(T/2,start, (end-start)/2) + MergeSort(T/2,start, (end-start)/2 +1 ) + F(合并过程);
合并过程算法描述
两个排好序的数组,i,j分别指向两个数组的首元素,依次比较,较小的依次放入新数组同时指针加一;
复杂度分析
T(n) = 2T(n/2) + n-1;
T(1) = 0;
递归树求解可知T(n) = nlogn
老师的分析
二分设计思想
算法复杂度分析
java代码实现
// 二分归并排序 nlogn
/**
* 递归排序 递归参数 待排序数组arr,开始下标start,结束下标end 递归内容: 将待排序的数组 平均分成两组;
* 不断划分直到数组的长度小于等于2,当长度小于等于2时,将两个元素正确排序(小于2就不排序),返回; 递归返回后: 将两个数组合并 新建一个空数组
* 两个数组的首元素比较,如果是升序则谁小谁先放入数组; 返回 递归边界: 数组的长度小于等于2
*/
public Integer[] MergeSort(Integer[] array, int start, int end) {
if (end - start <= 1) {
if (array[start] > array[end]) {
int tem = array[start];
array[start] = array[end];
array[end] = tem;
}
return array;
} else {
sort7(array, start, (end - start + 1) / 2 - 1 + start);
sort7(array, (end - start + 1) / 2 + start, end);
}
Integer[] tempArr = new Integer[end - start + 1];
int i = start;
int j = (end - start + 1) / 2 + start;
for (int k = 0; k < tempArr.length; k++) {
if (j == end + 1) {
tempArr[k] = array[i];
i++;
continue;
}
if (i == (end - start + 1) / 2 + start) {
tempArr[k] = array[j];
j++;
continue;
}
if (array[i] < array[j]) {
tempArr[k] = array[i];
i++;
} else {
tempArr[k] = array[j];
j++;
}
}
for (int k = 0; k < tempArr.length; k++) {
array[start + k] = tempArr[k];
}
return array;
}
Hanoi塔的递归算法
问题描述:有三根柱子A,B,C 在A柱子从下往上按照大小顺序摞着n片黄金圆盘
规定任何时候小圆盘上都不能放大圆盘,且在三根柱子之间一次只能移动一个圆盘。问应该如何操作,把n片圆盘从A移动到C
分治求解:
1、确定子问题
首先确定原问题形式为 Hanoi(A,C, n);
子问题缩小问题的规模,也就是缩小n的规模,并且使原问题的解不受影响;
可以想象得到,当将第n个圆盘从A移动到C时,此时n-1个圆盘在B上;
所以只要解决 Hanoi(A,B, n-1);
2、递归的边界
当规模为1时直接移动;
3、解的递推方程
Hanoi(A,C, n) = Hanoi(A,B, n-1) + Hanoi(A,C, 1) + Hanoi(B,C, n-1);
复杂度分析
T(n) = 2T(n-1) + 1;
T(1) = 1;
求解可知T(n) = 2^n
老师的分析
java代码实现
//Hanoi塔问题
//问题描述:有三根柱子A,B,C 在A柱子从下往上按照大小顺序摞着n片黄金圆盘
//规定任何时候小圆盘上都不能放大圆盘,且在三根柱子之间一次只能移动一个圆盘。问应该如何操作,把n片圆盘从A移动到C
public void hanoi(char src, char desc, char mid, int n) {
if(n == 1) {
System.out.println(src + " 移动到 " + desc);
}else {
hanoi(src, mid, desc, n-1);
System.out.println(src + " 移动到 " + desc);
hanoi(mid, desc, src, n-1);
}
}
快速排序算法
问题输入:数组T【p…r】;
问题输出:元素按从小到大的顺序排列
分治求解:
1、确定子问题
首先确定原问题形式为 QuickSort(T,start, end);
2、划分子问题
以首元素为标准,将比首元素大的元素放到数组右边,比首元素小的元素放到数组左边;最后把首元素放入中间,也就是首元素左边是都比他小的元素数组,右边是都比它大的元素数组;左边数组和右边数组形成子问题;
划分算法描述:将小的放在右边大的放到左边的算法
i指向首元素后第一个元素,j指向最后一个元素,i从左至右移动,j从右到左移动,i找到比首元素大的数停止移动等待j找到一个比首元素小的数,找到后他们交换;
当i和j碰撞时算法结束;
3、递归边界
当n=1时,不再需要排序直接返回;
4、解的递推方程
QuickSort(T,start, end)
不需要合并子问题的解;
复杂度分析
最坏情况,刚好首元素就是第一个或者最后一个,那么子问题的规模只减1
T(n) = T(n-1)+ n-1; n-1是指需要n-1次的比较
T(1) = 0;
T(n)= n(n-1)/2
最好情况,刚好划到中间
T(n) = 2T(n/2)+ n-1
T(1) = 0;
T(n)= nlogn
老师分析
划分过程
划分实例
复杂度分析
递归树分析
平均复杂度分析
java代码实现
// 快速排序1
/**
* 递归排序 递归参数 待排序数组,开始下标,结束下标
*
* 递归内部: 数组第一个数字 作为“排序目标” 用来比较 i指针从小到大遍历,j指针从大到小遍历
* 假定是升序,则较小的数字应在坐标,较大的数字应在右边。 那么i指针找到比“排序目标”大的数,找到后 等待j指针
* j指针找到比“排序目标”小的数,找到后i 和 j 所指的数字进行交换 (也就是大的数往右边放,小的数往左边放)
* i不能与j交叉或者相等,即i++和j--的限制条件是相邻,当ij相碰则结束(j>i); 假如i未找到比“排序目标”大的数 或者
* 假如j未找到比“排序目标”小的数则交换结束
*
* 结束后划分数组 假如arr[i] < “排序目标”,则 “排序目标”与i下标交换 反之与i-1下标交换
* 这样“排序目标”的位置已经排好,且“排序目标”左边都是比它小的数,排序目标右边都是比它大的数; 排序目标左边,排序目标右边继续进入新的递归
*
* 递归边界: end = start 意味着只有一个数 end < start
*/
public Integer[] sort5(Integer[] array, int start, int end) {
// 递归边界 end = start 意味着只有一个数
if (end <= start) {
return array;
}
// 也可以对 只有两个数的数组单独处理
// if(end - start == 1) {
// //比较这两个数然后交换
// }
// 定义i指针 i指针指向第二个数 第一个数单独拿出来作比较
int i = start + 1;
// j指针从后面开始
int j = end;
//
while (j > i) {
boolean iFlag = false;
while (array[i] < array[start]) {
if (i < j - 1) {
i++;
} else {
iFlag = true;
break;
}
}
if (iFlag) {
break;
}
boolean jFlag = false;
while (array[j] > array[start]) {
if (j > i + 1) {
j--;
} else {
jFlag = true;
break;
}
}
if (jFlag) {
break;
}
int tem = array[i];
array[i] = array[j];
array[j] = tem;
}
// 递归
if (array[start] > array[i]) {
int tem = array[i];
array[i] = array[start];
array[start] = tem;
sort5(array, start, i - 1);
sort5(array, i + 1, end);
} else {
int tem = array[i - 1];
array[i - 1] = array[start];
array[start] = tem;
sort5(array, start, i - 2);
sort5(array, i, end);
}
return array;
}
// 快速排序2 最坏n^2 平均nlogn
/**
* 递归排序 递归参数 待排序数组arr,开始下标start,结束下标end
*
* 递归内部: 数组第一个数字 作为“排序目标”arr[start]用来比较 暂存arr[start]的值为first swapindex为交换的下标
* 初始值为start 假定升序排序:i指针从start+1起从左至右寻找比first大的值,找到后与swapindex交换,新的swapindex
* = i; j指针从end起从右至左寻找比first小的值,找到后与swapindex交换,新的swapindex = j; 在寻找的过程中
* j不能小于i,否则寻找结束; 结束后把first放入arr[swapindex]
*
* 递归结束后,swapindex左边都是比first小的数,右边都是比first大的数; 划分数组 递归边界: end <= start
*/
public Integer[] sort6(Integer[] array, int start, int end) {
// 递归边界
if (end <= start) {
return array;
}
int i = start + 1;
int j = end;
// 记录初始start的值 和swapindex
int first = array[start];
int sawpIndex = start;
// j不能小于i,存在等于是因为可能存在{8,7}这样长度为2的未排序数组
while (j >= i) {
boolean jFlag = false;
while (array[j] > first) {
if (j > i + 1) {
j--;
} else {
jFlag = true;
break;
}
}
if (jFlag) {
break;
}
array[sawpIndex] = array[j];
sawpIndex = j;
boolean iFlag = false;
while (array[i] < first) {
if (i < j - 1) {
i++;
} else {
iFlag = true;
break;
}
}
if (iFlag) {
break;
}
array[sawpIndex] = array[i];
sawpIndex = i;
}
array[sawpIndex] = first;
sort6(array, start, sawpIndex - 1);
sort6(array, sawpIndex + 1, end);
return array;
}
幂乘算法
问题描述:a为给定实数,n为自然数,求a的n次方
传统算法,temp=a 循环temp*=a 复杂度为T(n)=n;
分治求解:
1、确定子问题
确定原问题形式为F(a,n)
那么子问题可为F(a,n/2);
2、划分子问题
直接对半划分子问题
3、递归边界
当n=1时可以直接返回n
或当n=2时直接返回n*n
4、递推方程
当n的偶数时,
F(a,n) = F(a,n/2) 乘 F(a,n/2);
当n为奇数时,
F(a,n) = F(a,(n-1)/2) 乘 (a,(n-1)/2)乘 F(a, 1);
算法复杂度分析
T(n)=T(n/2) + 1;
T(n) = logn
老师分析
java实现
//幂乘算法
//问题描述:a为给定实数,n为自然数,求a的n次方
public long powerMultiplyAlgorithm(int a, int n) {
if(n <= 2) {
if(n == 1) {
return a;
}else {
return a * a;
}
}
if(n % 2 == 0) {
long temp = powerMultiplyAlgorithm(a, n/2);
return temp * temp;
}else {
long temp = powerMultiplyAlgorithm(a, (n-1)/2);
return temp * temp * a;
}
}
芯片测试问题
问题描述:n片芯片,其中好芯片至少比坏芯片多一片;好芯片能正确测试出芯片好坏,坏芯片测试结果可能是好也可能是坏;设计一种测试方法,通过测试从n片芯片中跳出一块好的芯片;
蛮力算法:
任取一块芯片,作为被测试芯片;其他芯片依次对该芯片进行测试,若报告是好芯片的次数大于等于n/2则说明是好芯片,停止测试;若报告是坏芯片的次数大于等于n/2+1,则说明是该芯片是坏芯片;
最好的情况第一块就是好芯片复杂度为n
最坏的情况下,前面测试的全是换芯片,
复杂度=n-2 + n-3 + …+n-n/2-1
总计复杂度=O(n^2)
分治解法:
1、确定子问题
确定原问题形式为F(n)
如何将n的规模减小
2、划分子问题
把n个芯片两两分组
两两分组的芯片互相测试
结果为两个好的任意保留一块
其他情况的全部抛弃
保留的芯片进入子问题(如此需要证明子问题与原问题性质一样,保持好的芯片比坏芯片多)
证明:
假如两两分组的芯片,结果为两个好的有 i 组,一个好一个坏的有 j 组,两个坏的 k 组;
那么有如下关系,2(i+j+k)= n;好的芯片数树比坏的芯片数多,那么有 2i+j > 2k+j
而通过如上的分组两个好的有i组,将留下有i个,两个坏的k组,假设都是测出来两个好也全部留下,有k个,那么最终子问题有i+k个,i>k,故好的比坏的多,任保留了原问题的性质;
当n是奇数时,单独出来的芯片,采用暴力算法单独测试,是好的直接返回结果;
3、递归的边界
当n=3时,只需要任意比较一次就可以确定好的芯片
n<3时,都是好的,不需要测试,直接返回
4、子问题合并
子问题不需要合并
5、算法复杂度分析
T(n)= T(n/2) + O(n)
T(3) = 1
T(2)=0
T(1)=0
T(n) = O(n) 为什么不是nlogn
老师分析
问题
测试结果分析
java实现
// 芯片测试
// 题目:n片芯片,其中好芯片至少比坏芯片多一片;好芯片能正确测试出芯片好坏,坏芯片测试结果可能是好也可能是坏;
// 设计一种测试方法,通过测试从n片芯片中跳出一块好的芯片
class Chip {
public boolean test;
public int id;
//芯片测试规则
public boolean testChip(Chip b) {
Random random = new Random();
if (test) {
return b.test;
} else {
return random.nextInt() % 2 == 0 ? false : true;
}
}
}
// 构造一堆随机芯片
public List<Chip> getChipArr(int n) {
List<Chip> chipList = new ArrayList<Chip>(n);
Random random = new Random();
int j = n / 2 + 1;
for (int i = 1; i <= n; i++) {
Chip chip = new Chip();
chip.id = i;
if (i + j >= n) {
chip.test = true;
} else {
if (random.nextInt() % 2 == 0) {
chip.test = true;
j--;
} else {
chip.test = false;
}
}
chipList.add(chip);
}
return chipList;
}
//找出一片正确的芯片
public Chip ChipTest(List<Chip> chipList) {
if (chipList.size() % 2 != 0) {
if (singerChipTest(chipList)) {
return chipList.get(chipList.size() - 1);
} else {
chipList.remove(chipList.size() - 1);
}
;
}
if (chipList.size() == 3) {
if (chipList.get(0).testChip(chipList.get(0))) {
return chipList.get(1);
} else {
return chipList.get(2);
}
} else if (chipList.size() < 3) {
return chipList.get(0);
}
List<Chip> newList = new ArrayList<Chip>();
for (int i = 0; i < chipList.size(); i += 2) {
if (chipList.get(i).testChip(chipList.get(i + 1)) && chipList.get(i + 1).testChip(chipList.get(i))) {
newList.add(chipList.get(i));
}
;
}
return ChipTest(newList);
}
// 单片芯片测试
public boolean singerChipTest(List<Chip> chipList) {
Chip singerChip = chipList.get(chipList.size() - 1);
int trueCount = (chipList.size() - 1) / 2;
int falseCount = trueCount + 1;
for (int i = 0; i < chipList.size() - 1; i++) {
if (chipList.get(i).testChip(singerChip)) {
trueCount--;
} else {
falseCount--;
}
if (trueCount == 0) {
return true;
}
if (falseCount == 0) {
return false;
}
}
return false;
}