六、递归的思想与应用

递归是一种数学上分而治之的思想:

将原问题分解为规模较小的问题进行处理;

分解后的问题与原问题的类型完全相同,但规模较小

通过小规模问题的分解,能够轻易求得原问题的解

问题的分解是有限的(递归不能无限进行)

当边界条件不满足时,分解问题(递归继续进行)

当边界条件满足时,直接求解(递归结束)

递归在程序设计中的应用:

递归函数:

            函数体中存在自我调用的函数;——问题的分解

            递归函数必须有递归出口(边界条件);

            函数的无限递归将导致程序崩溃。

下面分别实现三个递归函数:求和

unsigned int sum( unsigned int n)
{
    if( n > 1)
    {
        return n+ sum(n -1);
    }
    else
    {
        return 1;
    }

}

斐波那契数列:

unsigned  int fac(unsigned int n)
{
    if( n > 2)
    {
        return fac(n-1) + fac(n-2);
    }

    if( n == 2)
    {
        return 1;
    }

    if( n == 1)
    {
        return 1;
    }

    return 0;
}

函数求字符串长度:

unsigned int _strlen_(const char* s)
{
    /*if( *s != '\0')
    {
        return 1 + _strlen_(s+1);
    }
    else {
        return 0;
    }*/
//*************一行代码实现***************************//
    
}
return s ? (*s ? (1 + _strlen_(s+1)):0):0;  //先判断s是否为零,再判断字符是否为 '\0'

递归的思想在于分而治之,首先建立递归的模型,模型中必须要有边界条件,最好能先写出递归函数的数学表达。


注意:不要陷入递归函数的执行细节,要学会通过代码来描述递归问题。

单向链表的转置:


先定义一个链表结点

struct Node{
    int value;
    Node* next;
};

实现三个和链表相关的函数:

Node* create_list(int v, int len)
{
    Node* ret = NULL;
    Node* slider = NULL;

    for(int i=0; i<len; i++)
    {
        Node* n = new Node();

        n->value = v++;
        n->next = NULL;

        if( slider == NULL)
        {
            slider = n;
            ret = n;
        }
        else
        {
            slider->next = n;
            slider = n;
        }
    }

    return ret;
}

void destory_list(Node* list)
{
    while (list)
    {
        Node* del = list;

        list = list->next;

        delete del;
    }
}

void print_list(Node* list)
{
    while (list)
    {
        cout<< list->value <<"->";

        list = list->next;
    }

    cout<< "NULL"<<endl;
}

转置函数的实现:

Node* reverse(Node* list)
{
    if( (list == NULL) || (list->next == NULL))
    {
        return list;  //先找出口
    }
    else
    {
        Node* guard = list->next;    //将大问题分解为子表的递归
        Node* ret = reverse(list->next);  //子表的转置

        guard->next = list;
        list->next = NULL;

        return ret;
    }
}

单向排序链表的合并:(合并之后也要单向有序)

先找到出口条件:


Node* merge(Node* list1, Node* list2)
{
   if( list1 == NULL)
   {
       return list2;
   }
   else if( list2 == NULL)
   {
       return list1;
   }
   else if(list1->value < list2->value)
   {
       Node* list1_ = list1->next;
       Node* list = merge(list1_, list2);

       list1->next = list;

       return list1;
       //优化::: return (list1->next = merge(list1->next, list2), list1);  逗号表达式
   }
   else
   {
       Node* list2_ = list2->next;
       Node* list = merge(list1, list2_);

       list2->next = list;

       return list2;
       //优化::: return (list2->next = merge(list1, list2->next), list2);  逗号表达式
   }
}

汉诺塔问题:



void HanoiTower(int n, char a, char b, char c)// a ==>src b==>middle c==>dest
{
    if( n == 1)
    {
        cout<< a << "-->" << c <<endl;  //打印移动步骤
    }
    else
    {
        HanoiTower(n-1, a, c, b);  //第一次,先将n-1个木块,从a始发地,借助于c,移动到b
        HanoiTower(1, a, b, c);  //将最后一个木块(在a上面),直接移动到c
        HanoiTower(n-1, b, a, c);  //剩下的n-1个木块在b上面,借助于中转站a移动到目的地c
    }
}

全排列问题:


void permutation(char* s, char* e)  //  e表示始终指向首元素的地址
{
    if( *s == '\0')  //不参与全排列
    {
        cout<< "look here:" <<endl;
        cout<< e <<endl;
    }
    else
    {
       int len(strlen(s));  //初始化的一种

       for(int i=0; i<len ; i++)   //注意这里的遍历,是S[0]不断与每个元素进行交换
       {
        if( (i == 0) || (s[0] != s[i]))  //避开集合中有相同元素的情况
        {
            swap(s[0], s[i]);  //没考虑元素是否一样
            permutation(s+1, e);   //交换过后,对其余元素进行全排列。
            swap(s[0], s[i]);  //交换回来  
        }
       }
    }
}

测试代码:

int main()
{
    char s[] = "cba";

    permutation(s, s);

    /*
    Node* list1 = create_list(1,3);
    Node* list2 = create_list(2,3);

    print_list(list1);
    print_list(list2);

    Node* list = merge(list1, list2);
    print_list(list);

    destory_list(list);
    destory_list(list1);
    destory_list(list2);
    */

    //HanoiTower(3,'a','b','c');

    return 0;
}

使用递归来进行回溯算法的设计

函数调用过程的回顾:

程序运行后有一个特殊的内存区域供函数调用使用:比如全局变量在全局数据区,数据段;代码放在代码段。

这块区域用于保存函数中的实参,局部变量、临时变量等,从起始地址开始往一个方向增长,(如高地址--->低地址),并且有一个专用的“指针”标识当前已经使用内存的“顶部”。

特点:只在内存某块区域的一端进行操作。

程序中的栈区:一段特殊的专用内存区(供函数调用)


回溯算法的核心:g()函数调用结束之后,f()函数调用对应的栈空间的数据不会发生变化。

示例1:逆序打印单链表中的偶数结点


void r_print_even(Node* list)
{
    if(list != NULL)  //为NULL则直接返回了
    {
       r_print_even(list->next);  //先逆序打印子表

       if( (list->value % 2) == 0)  //出口
       {
           cout<< list->value <<endl;
       }
    }
}
问:如何实现逆序的?

每次函数递归调用的时候,都会在内存中分配空间,直到空指针出现。然后出现退栈,函数返回。一层层退栈,然后不断进行判断,符合条件则打印,然后连续退栈、判断。

打印的过程就是回溯的过程。开始退栈的时候,就要决定是否打印。打印的内容来自于栈上面的参数,此处的参数是指针。在递归调用函数的时候,只是使用栈上的参数(指针)保存数据,数据暂时没有使用,只是使栈上的指针指向对应的数据(结点),继续建栈,在函数调用返回(退栈)的时候,才使用这些数据。(类似于迷宫中的标记(指针))

回溯算法设计的本质,就是在栈上做标记,方便回退。利用栈保存数据,回溯的时候再利用数据再进行计算(判断)。


测试代码示例:

int main()
{
    /*Node* list = create_list(2,5);

    print_list(list);

    r_print_even(list);
    print_list(list);


    //char s[] = "cba";

    //permutation(s, s);


    Node* list1 = create_list(1,3);
    Node* list2 = create_list(2,3);

    print_list(list1);
    print_list(list2);

    Node* list = merge(list1, list2);
    print_list(list);

    destory_list(list);
    destory_list(list1);
    destory_list(list2);
    */

    //HanoiTower(3,'a','b','c');

    return 0;
}
请读者自行测试


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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值