递归与分治
- 1、下载文档前请自行甄别文档内容的完整性,平台不提供额外的编辑、内容补充、找答案等附加服务。
- 2、"仅部分预览"的文档,不可在线预览部分如存在完整性等问题,可反馈申请退款(可完整预览的文档不适用该条件!)。
- 3、如文档侵犯您的权益,请联系客服反馈,我们会尽快为您处理(人工客服工作时间:9:00-18:30)。
分治算法
一、分治算法
分治算法的基本思想是将一个规模为N的问题分解为K个规模较小的子问题,这些子问题相互独立且与原问题性质相同。
求出子问题的解,就可得到原问题的解。
分治法解题的一般步骤:
(1)分解,将要解决的问题划分成若干规模较小的同类问题;
(2)求解,当子问题划分得足够小时,用较简单的方法解决;
(3)合并,按原问题的要求,将子问题的解逐层合并构成原问题的解。
当我们求解某些问题时,由于这些问题要处理的数据相当多,或求解过程相当复杂,使得直接求解法在时间上相当长,或者根本无法直接求出。
对于这类问题,我们往往先把它分解成几个子问题,找到求出这几个子问题的解法后,再找到合适的方法,把它们组合成求整个问题的解法。
如果这些子问题还较大,难以解决,可以再把它们分成几个更小的子问题,以此类推,直至可以直接求出解为止。
这就是分治策略的基本思想。
下面通过实例加以说明。
【例1】[找出伪币] 给你一个装有1 6个硬币的袋子。
1 6个硬币中有一个是伪造的,并且那个伪造的硬币比真的硬币要轻一些。
你的任务是找出这个伪造的硬币。
为了帮助你完成这一任务,将提供一台可用来比较两组硬币重量的仪器,利用这台仪器,可以知道两组硬币的重量是否相同。
比较硬币1与硬币2的重量。
假如硬币1比硬币2轻,则硬币1是伪造的;假如硬币2比硬币1轻,则硬币2是伪造的。
这样就完成了任务。
假如两硬币重量相等,则比较硬币3和硬币4。
同样,假如有一个硬币轻一些,则寻找伪币的任务完成。
假如两硬币重量相等,则继续比较硬币5和硬币6。
按照这种方式,可以最多通过8次比较来判断伪币的存在并找出这一伪币。
另外一种方法就是利用分而治之方法。
假如把1 6硬币的例子看成一个大的问题。
第一步,把这一问题分成两个小问题。
随机选择8个硬币作为第一组称为A组,剩下的8个硬币作为第二组称为B组。
这样,就把1 6个硬币的问题分成两个8硬币的问题来解决。
第二步,判断A和B组中是否有伪币。
可以利用仪器来比较A组硬币和B组硬币的重量。
假如两组硬币重量相等,则可以判断伪币不存在。
假如两组硬币重量不相等,则存在伪币,并且可以判断它位于较轻的那一组硬币中。
最后,在第三步中,用第二步的结果得出原先1 6个硬币问题的答案。
若仅仅判断硬币是否存在,则第三步非常简单。
无论A组还是B组中有伪币,都可以推断这1 6个硬币中存在伪币。
因此,仅仅通过一次重量的比较,就可以判断伪币是否存在。
现在假设需要识别出这一伪币。
把两个或三个硬币的情况作为不可再分的小问题。
注意如果只有一个硬币,那么不能判断出它是否就是伪币。
在一个小问题中,通过将一个硬币分别与其他两个硬币比较,最多比较两次就可以找到伪币。
这样,1 6硬币的问题就被分为两个8硬币(A组和B组)的问题。
通过比较这两组硬币的重量,可以判断伪币是否存在。
如果没有伪币,则算法终止。
否则,继续划分这两组硬币来寻找伪币。
假设B是轻的那一组,因此再把它分成两组,每组有4个硬币。
称其中一组为B1,另一组为B2。
比较这两组,肯定有一组轻一些。
如果B1轻,则伪币在B1中,再将B1又分成两组,每组有两个硬币,称其中一组为B1a,另一组为B1b。
比较这两组,可以得到一个较轻的组。
由于这个组只有两个硬币,因此不必再细分。
比较组中两个硬币的重量,可以立即知道哪一个硬币轻一些。
较轻的硬币就是所要找的伪币。
【例2】在n个元素中找出最大元素和最小元素。
我们可以把这n个元素放在一个数组中,用直接比较法求出。
算法如下:
void maxmin1(int A[],int n,int *max,int *min)
{ int i;
*min=*max=A[0];
for(i=2;i < n;i++)
{ if(A> *max) *max= A;
if(A < *min) *min= A;
}
}
上面这个算法需比较2(n-1)次。
能否找到更好的算法呢?我们用分治策略来讨论。
把n个元素分成两组:
A1={A[1],...,A[int(n/2)]}和A2={A[INT(N/2)+1],...,A[N]}
分别求这两组的最大值和最小值,然后分别将这两组的最大值和最小值相比较,求出全部元素的最大值和最小值。
如果A1和A2中的元素多于两个,则再用上述方法各分为两个子集。
直至子集中元素至多两个元素为止。
例如有下面一组元素:-13,13,9,-5,7,23,0,15。
用分治策略比较的过程如下:
图中每个方框中,左边是最小值,右边是最大值。
从图中看出,用这种方法一共比较了10次,比直接比较法的14次减少4次,即约减少了1/3。
算法如下:
void maxmin2(int A[],int i,int j,int *max,int *min)
/*A存放输入的数据,i,j存放数据的范围,初值为0,n-1,*max,int *m in 存放最大和最小值*/
{ int mid,max1,max2,min1,min2;
if (j==i) {最大和最小值为同一个数;return;}
if (j-1==i) {将两个数直接比较,求得最大会最小值;return;}
mid=(i+j)/2;
求i~mid之间的最大最小值分别为max1,min1;
求mid+1~j之间的最大最小值分别为max2,min2;
比较max1和max2,大的就是最大值;
比较min1和min2,小的就是最小值;
}
利用分治策略求解时,所需时间取决于分解后子问题的个数、子问题的规模大小等因素,而二分法,由于其划分的简单和均匀的特点,是经常采用的一种有效的方法,例如二分法检索。
运用分治策略解决的问题一般来说具有以下特点:
1、原问题可以分解为多个子问题,这些子问题与原问题相比,只是问题的规模有所降低,其结构和求解方法与原问题相同或相似。
2、原问题在分解过程中,递归地求解子问题,由于递归都必须有一个终止条件,因此,当分解后的子问题规模足够小时,应能够直接求解。
3、在求解并得到各个子问题的解后,应能够采用某种方式、方法合并或构造出原问题的解。
不难发现,在分治策略中,由于子问题与原问题在结构和解法是的相似性,用分治方法解决的问题,大都采用了递归的形式。
在各种排序方法中,如归并排序、堆排序、快速排序等,都存在有分治的思想。
递归算法
递归算法:是一种直接或者间接地调用自身的算法。
在计算机编写程序中,递归算法对解决一大类问题是十分有效的,它往往使算法的描述简洁而且易于理解。
递归算法的特点
递归过程一般通过函数或子过程来实现。
递归算法:在函数或子过程的内部,直接或者间接地调用自己的算法。
递归算法的实质:是把问题转化为规模缩小了的同类问题的子问题。
然后递归调用函数(或过程)来表示问题的解。
递归算法解决问题的特点:
(1) 递归就是在过程或函数里调用自身。
(2) 在使用递增归策略时,必须有一个明确的递归结束条件,称为递归出口。
(3) 递归算法解题通常显得很简洁,但递归算法解题的运行效率较低。
所以一般不提倡用递归算法设计程序。
(4) 在递归调用的过程当中系统为每一层的返回点、局部量等开辟了栈来存储。
递归次数过多容易造成栈溢出等。
所以一般不提倡用递归算法设计程序。
递归算法所体现的“重复”一般有三个要求:
一是每次调用在规模上都有所缩小(通常是减半);
二是相邻两次重复之间有紧密的联系,前一次要为后一次做准备(通常前一次的输出就作为后一次的输入);
三是在问题的规模极小时必须用直接给出解答而不再进行递归调用,因而每次递归调用都是有条件的(以规模未达到直接解答的大小为条件),无条件递归调用将会成为死循环而不能正常结束。
例子如下:
描述:把一个整数按n(2<=n<=20)进制表示出来,并保存在给定字符串中。
比如121用二进制表示得到结果为:“1111001”。
参数说明:s: 保存转换后得到的结果。
n: 待转换的整数。
b: n进制(2<=n<=20)
void
numbconv(char *s, int n, int b)
{
int len;
if(n == 0) {
strcpy(s, "");
return;
}
/* figure out first n-1 digits */
numbconv(s, n/b, b);
/* add last digit */
len = strlen(s);
s[len] = "0123456789ABCDEFGHIJKLMNOPQRSTUVW XYZ"[n%b];
s[len+1] = '\0';
}
void
main(void)
{
char s[20];
int i, base;
FILE *fin, *fout;
fin = fopen("palsquare.in", "r");
fout = fopen("palsquare.out", "w");
assert(fin != NULL && fout != NULL);
fscanf(fin, "%d", &base);
/*PLS set START and END*/
for(i=START; i <= END; i++) {
numbconv(s, i*i, base);
fprintf(fout, "%s\n", s);
}
exit(0);
}
递归算法简析(PASCAL语言)
递归是计算机科学的一个重要概念,递归的方法是程序设计中有效的方法,采用递归编写
程序能是程序变得简洁和清晰.
一递归的概念
1.概念
一个过程(或函数)直接或间接调用自己本身,这种过程(或函数)叫递归过程(或函数).
如:
procedure a;
begin
.
.
.
a;
.
.
.
end;
这种方式是直接调用.
又如:
procedure b; procedure c;
begin begin
. .
. .
. .
c; b;
. .
. .
. .
end; end;
这种方式是间接调用.
例1计算n!可用递归公式如下:
1 当n=0 时
fac(n)={n*fac(n-1) 当n>0时
可编写程序如下:
program fac2;
var
n:integer;
function fac(n:integer):real;
begin
if n=0 then fac:=1 else fac:=n*fac(n-1) end;
begin
write('n=');readln(n);
writeln('fac(',n,')=',fac(n):6:0);
end.
例2 楼梯有n阶台阶,上楼可以一步上1阶,也可以一步上2阶,编一程序计算共有多少种不同的走法.
设n阶台阶的走法数为f(n)
显然有
1 n=1
f(n)={
f(n-1)+f(n-2) n>2
可编程序如下:
program louti;
var n:integer;
function f(x:integer):integer;
begin
if x=1 then f:=1 else
if x=2 then f:=2 else f:=f(x-1)+f(x-2);
end;
begin
write('n=');read(n);
writeln('f(',n,')=',f(n))
end.
二,如何设计递归算法
1.确定递归公式
2.确定边界(终了)条件
三,典型例题
例3 梵塔问题
如图:已知有三根针分别用1,2,3表示,在一号针中从小放n个盘子,现要求把所有的盘子
从1针全部移到3针,移动规则是:使用2针作为过度针,每次只移动一块盘子,且每根针上
不能出现大盘压小盘.找出移动次数最小的方案.
程序如下:
program fanta;
var
n:integer;
procedure move(n,a,b,c:integer);
begin
if n=1 then writeln(a,'--->',c)
else begin
move(n-1,a,c,b);
writeln(a,'--->',c);
move(n-1,b,a,c);
end;
end;
begin
write('Enter n=');
read(n);
move(n,1,2,3);
end.
例4 快速排序
快速排序的思想是:先从数据序列中选一个元素,并将序列中所有比该元素小的元素都放到它的右边或左边,再对左右两边分别用同样的方法处之直到每一个待处理的序列的长度为1, 处理结束.
程序如下:
program kspv;
const n=7;
type
arr=array[1..n] of integer;
var
a:arr;
i:integer;
procedure quicksort(var b:arr; s,t:integer);
var i,j,x,t1:integer;
begin
i:=s;j:=t;x:=b ;
repeat
while (b[j]>=x) and (j>i) do j:=j-1;
if j>i then begin t1:=b; b:=b[j];b[j]:=t1;end;
while (b<=x) and (i<j) do i:=i+1;
if i<j then begin t1:=b[j];b[j]:=b;b:=t1; end
until i=j;
b:=x;
i:=i+1;j:=j-1;
if s<j then quicksort(b,s,j);
if i<t then quicksort(b,i,t);
end;
begin
write('input data:');
for i:=1 to n do read(a);
writeln;
quicksort(a,1,n);
write('output data:');
for i:=1 to n do write(a:6);
writeln;
end.
二叉树遍历
二叉树的遍历搜索路径
遍历概念:
所谓遍历(Traversal)是指沿着某条搜索路线,依次对树中每个结点均做一次且仅做一次访问。
访问结点所做的操作依赖于具体的应用问题。
遍历是二叉树上最重要的运算之一,是二叉树上进行其它运算之基础。
遍历方案:
1.遍历方案
从二叉树的递归定义可知,一棵非空的二叉树由根结点及左、右子树这三个基本部分组成。
因此,在任一给定结点上,可以按某种次序执行三个操作:(1)访问结点本身(N),
(2)遍历该结点的左子树(L),
(3)遍历该结点的右子树(R)。
以上三种操作有六种执行次序:
NLR、LNR、LRN、NRL、RNL、RLN。
注意:
前三种次序与后三种次序对称,故只讨论先左后右的前三种次序。
2.三种遍历的命名
根据访问结点操作发生位置命名:
①NLR:前序遍历(PreorderTraversal亦称(先序遍历))
——访问结点的操作发生在遍历其左右子树之前。
②LNR:中序遍历(InorderTraversal)
——访问结点的操作发生在遍历其左右子树之中(间)。
③LRN:后序遍历(PostorderTraversal)
——访问结点的操作发生在遍历其左右子树之后。
注意:
由于被访问的结点必是某子树的根,所以N(Node)、L(Left subtree)和R (Right subtree)又可解释为根、根的左子树和根的右子树。
NLR、LNR和LR N分别又称为先根遍历、中根遍历和后根遍历。
遍历算法
1.中序遍历的递归算法定义:
若二叉树非空,则依次执行如下操作:
(1)遍历左子树;
(2)访问根结点;
(3)遍历右子树。
2.先序遍历的递归算法定义:
若二叉树非空,则依次执行如下操作:
(1) 访问根结点;
(2) 遍历左子树;
(3) 遍历右子树。
3.后序遍历得递归算法定义:
若二叉树非空,则依次执行如下操作:
(1)遍历左子树;
(2)遍历右子树;
(3)访问根结点。
4.中序遍历的算法实现
用二叉链表做为存储结构,中序遍历算法可描述为:
void InOrder(BinTree T)
{ //算法里①~⑥是为了说明执行过程加入的标号
①if(T) { // 如果二叉树非空
②InOrder(T->lchild);
③printf("%c",T->data);// 访问结点
④InOrder(T->rchild);
⑤}
⑥} // InOrder
遍历序列
1.遍历二叉树的执行踪迹
三种递归遍历算法的搜索路线相同(如下图虚线所示)。
具体线路为:
从根结点出发,逆时针沿着二叉树外缘移动,对每个结点均途径三次,最后回到根结点。
2.遍历序列
A
/ \
B C
/ / \
D E F
图
(1)中序序列(inorder travers al)
中序遍历二叉树时,对结点的访问次序为中序序列
【例】中序遍历上图所示的二叉树时,得到的中序序列为:
D B A
E C F
(2)先序序列(preorder traversal)
先序遍历二叉树时,对结点的访问次序为先序序列
【例】先序遍历上图所示的二叉树时,得到的先序序列为:
A B D C E F
(3)后序序列(postorder traversal)
后序遍历二叉树时,对结点的访问次序为后序序列
【例】后序遍历上图所示的二叉树时,得到的后序序列为:
D B
E
F C A
(4)层次遍历(level traversal)二叉树的操作定义为:若二叉树为空,则退出,否则,按照树的结构,从根开始自上而下,自左而右访问每一个结点,从而实现对每一个结点的遍历
注意:
(1)在搜索路线中,若访问结点均是第一次经过结点时进行的,则是前序遍历;若访问结点均是在第二次(或第三次)经过结点时进行的,则是中序遍历(或后序遍历)。
只要将搜索路线上所有在第一次、第二次和第三次经过的结点分别列表,即可分别得到该二叉树的前序序列、中序序列和后序序列。
(2)上述三种序列都是线性序列,有且仅有一个开始结点和一个终端结点,其余结点都有且仅有一个前趋结点和一个后继结点。
为了区别于树形结构中前趋(即双亲)结点和后继(即孩子)结点的概念,对上述三种线性序列,要在某结点的前趋和后继之前冠以其遍历次序名称。
【例】上图所示的二叉树中结点C,其前序前趋结点是D,前序后继结点是E;中序前趋结点是E,中序后继结点是F;后序前趋结点是F,后序后继结点是A。
但是就该树的逻辑结构而言,C的前趋结点是A,后继结点是E和F。
二叉链表的构造
1.基本思想
基于先序遍历的构造,即以二叉树的先序序列为输入构造。
注意:
先序序列中必须加入虚结点以示空指针的位置。
【例】
建立上图所示二叉树,其输入的先序序列是:ABD∮∮∮CE∮∮F∮∮。
2.构造算法
假设虚结点输入时以空格字符表示,相应的构造算法为:
void CreateBinTree (BinTree *T)
{ //构造二叉链表。
T是指向根指针的指针,故修改*T就修改了实参(根指针)本身
char ch;
if((ch=getchar())=='') *T=NULL;//读人空格,将相应指针置空
else{ //读人非空格
*T=(BinTNode *)malloc(sizeof(BinTNode));//生成结点
(*T)->data=ch;
CreateBinTree(&(*T)->lchild);//构造左子树
CreateBinTree(&(*T)->rchild);//构造右子树
}
}
注意:
调用该算法时,应将待建立的二叉链表的根指针的地址作为实参。
【例】
设root是一根指针(即它的类型是BinTree),则调用CreateBinTree(&roo t)后root就指向了已构造好的二叉链表的根结点。
二叉树建立过程见/course_ware/data_structure/we b/flashhtml/erchashujianli.htm
下面是关于二叉树的遍历、查找、删除、更新数据的代码(递归算法):
[code]
#include <iostream>
using namespace std;
typedef int T;
class bst{
struct Node{
T data;
Node* L;
Node* R;
Node(const T& d, Node* lp=NULL, Node* rp=NULL):data(d),L(lp),R (rp){}
};
Node* root;
int num;
public:
bst():root(NULL),num(0){}
void clear(Node* t){
if(t==NULL) return;
clear(t->L);
clear(t->R);
delete t;
}
~bst(){clear(root);}
void clear(){
clear(root);
num = 0;
root = NULL;
}
bool empty(){return root==NULL;} int size(){return num;}
T getRoot(){
if(empty()) throw "empty tree"; return root->data;
}
void travel(Node* tree){
if(tree==NULL) return;
travel(tree->L);
cout << tree->data << ' ';
travel(tree->R);
}
void travel(){
travel(root);
cout << endl;
}
int height(Node* tree){
if(tree==NULL) return 0;
int lh = height(tree->L);
int rh = height(tree->R);
return 1+(lh>rh?lh:rh);
}
int height(){
return height(root);
}
void insert(Node*& tree, const T& d){ if(tree==NULL)
tree = new Node(d);
else if(ddata)
insert(tree->L, d);
else
insert(tree->R, d);
}
void insert(cons t T& d){
insert(root, d);
num++;
}
Node*& find(Node*& tree, const T& d){ if(tree==NULL) return tree;
if(tree->data==d) return tree;
if(ddata)
return find(tree->L, d);
else
return find(tree->R, d);
}
bool find(const T& d){
return find(root, d)!=NULL;
}
bool erase(const T& d){
Node*& pt = find(root, d);
if(pt==NULL) return false;
combine(pt->L, pt->R);
Node* p = pt;
pt = pt->R;
delete p;
num--;
return true;
}
void combine(Node* lc, Node*& rc){
if(lc==NULL) return;
if(rc==NULL) rc = lc;
else combine(lc, rc->L);
}
bool update(const T& od, const T& nd){ Node* p = find(root, od);
if(p==NULL) return false;
erase(od);
insert(nd);
return true;
}
};
int main()
{
bst b;
cout << "input som e integers:";
for(;;){
int n;
cin >> n;
b.insert(n);
if(cin.peek()=='\n') break;
}
b.travel();
for(;;){
cout << "input data pair:";
int od, nd;
cin >> od >> nd;
if(od==-1&&nd==-1) break;
b.update(od, nd);
b.travel();
}
}
[/code]。