@原文url:http://www.cnblogs.com/huangxincheng/category/340148.html
很多人一谈到学编程,都是说要学会XX语言就OK了,其实我们理解的有一点点的偏差,因为我们只说到了三分之一,其实真正的编程应该是:编程=数据结构+算法+XX语言。
一,递推思想
1,递推思想
(1): 概念
通过已知条件,利用特定关系逐步递推,最终得到结果为止,核心就是不断的利用现有信息推导出新的东西。
(2):分类
当然递推中有两种,“顺推”和“逆推“
顺推:从条件推出结果。http://www.cnblogs.com/feichengwulai/articles/3587848.html中第1题就是顺推。
逆推:从结果推出条件。http://www.cnblogs.com/feichengwulai/articles/3587848.html中第17题就是逆推。
呵呵,是不是觉的有一种policeman破案的感觉。
2,逆推的例子
这个一个关于存钱的问题,一个富二代给他儿子的四年大学生活存一笔钱,富三代每月只能取3k作为下个月的生活费,采用的是整存零取?的方式,年利率在1.71%,请问富二代需要一次性存入多少钱。
这里首先要读懂题:
(1),年利率1.71%是什么?1.71是年利息!除以12个月乘以3就是3个月的利息。例如存款5000,存3个月,那么利息就是5000*((0.0171/12)*3)=21.375。
(2),什么是整存领取?指本金一次存入,分次支取本金的一种储蓄。注意,当月支取的本金也有利息的,即最后一个月,富三代除了那3000,还有3000*(0.0171/12)=4.275的利息。即当月取走金额,当月扔计算利息,下个月才减去上个取走金额。 ---银行的计算公式。。。
(3),思路: 这个题目是我们知道了结果,需要逆推条件, 第48月富三代要连本带息的把3k一把取走,那么
第47月存款应为: (第48个月的存款+3000)/(1+0.0171/12(月)); 5991.462166 本月利息8.53783358655 假如富三代只上2个月的话,存这么多钱,就够保证他每个月3000的生活费了。
第46月存款应为: (第47个月的存款+3000)/(1+0.0171/12(月));
..... .....
第1个月存款应为: (第2个月的存款+3000)/(1+0.0171/12(月)); 139093.855697938(4年总花费,一次性存入这么多前钱。) < 3000*48=144000,节省了4906.14431
(4),为什么是(第48个月的存款+3000)/(1+0.0171/12(月)),而不是(第48个月的存款+3000)*(1-0.0171/12)。留一个疑问?
(5),代码如下:
    //银行取钱问题
            double[] month = new double[49];
           ///最后一个月的连本带息是3000
            month[48] = 3000;
            double rate = 0.0171;
            //逆推
            for (int i = 47; i > 0; i--)
            {
                month[i] = (month[i + 1] + month[48]) / (1 + rate / 12);
            }
            for (int i = 48; i > 0; i--)
            {
      //month[i] = (month[i + 1] + month[48]) * (1-rate / 12);  为什么不是这样。。。
                Console.WriteLine("第{0}个月末本利合计:{1}", i, month[i]);
            }
二,递归思想
1,递归,说白了就是直接或者间接的调用自己的一种算法。它是把求解问题转化为规模较小的子问题,然后通过多次递归一直到可以得出结果的最小解,然后通过最小解逐层向上返回调用,最终得到整个问题的解。总之递归可以概括为一句话就是:“能进则进,不进则退”。
2,递归三要素
<1> 递归中每次循环都必须使问题规模有所缩小。
<2> 递归操作的每两步都是有紧密的联系,如在“递归”的“归操作时”,前一次的输出就是后一次的输入(重点!!!)。
<3> 当子问题的规模足够小时,必须能够直接求出该规模问题的解,其实也就是必须要有结束递归的条件。
3,三注意点
<1> 前面也说了,递归必须要有一个递归出口。
<2> 深层次的递归会涉及到频繁进栈出栈和分配内存空间,所以运行效率比较低,当问题规模较大时,不推荐使用。
<3> 在递归过程中,每次调用中的参数,方法返回点,局部变量都是存放在堆栈中的,如果当问题规模非常大时,容易造成堆栈溢出。
4,举二个例子
<1> 相信大家在初中的时候都学过阶乘吧,比如:5!=5*4*3*2*1
思路:根据上面的阶乘特征很容易我们就可以推导出n!=n*(n-1)*(n-2)....*2*1,
     public static void RunSnippet()
    {
        int n=5;
        Console.WriteLine(f(n));
        Console.ReadKey();
    }
    public static int f(int n)
    { 
        if(n==1)    //注意不能这样写f(1)=1,return=1,即f(1)=1。
        {
            return 1;   //三要素3,这里就是递归的出口,结束条件
        }
        else
        {
            //三要素1,递归的规模有所缩小
            return n*f(n-1);  //三要素2,前一次的输出,是后一次的输入,n的值在改变
            //注意,不能这样return f(n)*f(n-1),递归到最后,就是f(5)*f(4)*f(3)*f(2)*1,无解。
        }
    }  
 
 
第一次: 输入5的时候能够正确求出。
第二次: 输入10的时候求出来竟然362万之多,可见多恐怖,如果俺们的时间复杂度是n!,那程序也就Game Over了,
第三次:输入100,已经超过了int.MaxValue了,
第四次: 输入10w,蹦出著名了“堆栈溢出”,好家伙,我们知道“递归”在程序中使用“栈”的形式存放的,每一次“递归”中,方法的返回值包括函数中的参数都会存放在栈中,C#中每个线程分配的栈空间为1M,所以当N的规模非常大时,就把栈玩爆了。
三,贪心思想(略)
四,枚举思想
1,这种思想也常是码畜,码奴常用的手段,经常遭到码农以上级别的鄙视,枚举思想可以说是在被逼无奈时最后的狂吼。 有时我们解决某个问题时找不到一点规律,此时我们很迷茫,很痛苦,很蛋疼,突然我们灵光一现,发现候选答案的问题规模在百万之内,此时我们就想到了从候选答案中逐一比较,一直找到正确解为止。
2,前面也说了,枚举是我们在无奈之后的最后一击,那么使用枚举时我们应该尽量遵守下面的两个条件。
① 地球人都不能给我找出此问题的潜在规律。
② 候选答案的集合是一个计算机必须能够承受的。
五,分治思想
有时候我们处理一个复杂的问题,可能此问题求解步骤非常杂,也可能是数据非常多,导致我们当时很难求出或者无法求出,古语有云:步步为营,各个击破,这个思想在算法中称为分治思想,就是我们可以将该问题分解成若干个子问题,然后我们逐一解决子问题,最后将子问题的答案组合成整个问题的答案。
1,条件
当然各个思想都有它的使用领域,所以玩这场分治游戏就要遵守它的游戏规则。
① 求解问题确实能够分解成若干个规模较小的子问题,并且这些子问题最后能够实现接近或者是O(1)时间求解。
② 各个子问题之间不能有依赖关系,并且这些子问题确实能够通过组合得到整个问题的解。
2,步骤
通过上面对分治的说明,可以看到分治分三步走:
① 分解: 将问题分解成若干了小问题。
② 求解: O(1)的时间解决该子问题。
③ 合并: 子问题逐一合并构成整个问题的解。
3,举例
有n位选手参加羽毛球赛,比赛要进行n-1天,每位选手都要与其他每一个选手比赛一场并且每位选手每天都要比赛一场,请根据比赛要求排出选手的比赛日程表。
(1),相信2位选手1天的比赛安排大家都会吧,如图:
  
(2),4位选手3天的比赛安排,如图:
在图中可以看出:
第一天:将1,2位和3,4位选手的日程合并。
第二天,第三天:这两天的比赛安排其实可以发现规律的,整个表格可以划分四格,对角赋值。
     
 如图中所示,12和34合,就解决了4位选后的分配问题。
(3),当n是8,16,32时,面对这么一个庞大的问题我们可能就崩溃了,因为我们实在无法求出来,此时我们就要想想是否可以分治一下。
① 就拿16个选手的比赛安排来说,需要比赛15天。
② 分成2个8位选手7天的比赛安排。
③ 分为4个4位选手3天的比赛安排。
④ 分为8个2位选手1天的比赛安排。
(4),程序代码如下:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
namespace Fenzhi
{
    public class Program
    {
        //这里
        static int[,] GameList = new int[8, 8];
        static void Main(string[] args)
        {
            Console.Write("请输入参赛选手的人数:\t");
            int person = Convert.ToInt32(Console.ReadLine());
            //参数检查合法性
            if (person % 2 != 0)
            {
                Console.WriteLine("输入的人数必须是2的倍数!");
                return;
            }
            //因为我定了只能容纳8位选手的日程的比赛安排,多的话就会爆掉
            if (person > 8)
            {
                Console.WriteLine("对不起,最多8位选手");
                return;
            }
            //调用分值计算函数
            GameCal(1, person);
            Console.Write("\n编号\t");
            //最后就是将数组表头
            for (int i = 1; i < person; i++)
            {
                Console.Write("第{0}天\t", i);
            }
            //换行
            Console.WriteLine();
            //输出数组内容
            for (int i = 0; i < person; i++)
            {
                for (int j = 0; j < person; j++)
                {
                    Console.Write("{0}\t", GameList[i, j]);
                }
                Console.WriteLine();
            }
            Console.ReadLine();
        }
        /// <summary>
  /// 分治计算
  /// </summary>
  /// <param name="index">起始选手编号</param>
  /// <param name="num">选手的人数(因为是分治,所以每次砍半)</param>
        static void GameCal(int index, int num)
        {
            //如果人数为2,则说明已经分治到了最简问题
            if (num == 2)
            {
                //参赛选手编号
                GameList[index - 1, 0] = index;
                //对阵选手编号
                GameList[index - 1, 1] = index + 1;
                //参赛选手编号
                GameList[index, 0] = index + 1;
                //对阵选手编号
                GameList[index, 1] = index;
            }
            else
            {
                //折半递归
                GameCal(index, num / 2);
                //折半递归
                GameCal(index + num / 2, num / 2);
                /* 子问题都结束后就要想办法合并,根据发现的规律进行合并 */
                //用于将“左下角”填充到“右上角”
      //控制横坐标
                for (int i = index; i < index + num / 2; i++)
                {
                    //控制“纵坐标”
                    for (int j = num / 2; j < num; j++)
                    {
                        //对角赋值
                        GameList[i - 1, j] = GameList[(i - 1) + num / 2, j - num / 2];
                    }
                }
                //用于将“左上角”填充到“右下角”
      //控制横坐标
                for (int i = index + num / 2; i < index + num; i++)
                {
                    //控制纵坐标
                    for (int j = num / 2; j < num; j++)
                    {
                        //对角赋值
                        GameList[i - 1, j] = GameList[(i - 1) - num / 2, j - num / 2];
                    }
                }
            }
        }
    }
}


六,回溯思想(试探思想)
记得广告中经常听到过,抱着试试看的态度买了3个疗程,效果不错........ 也经常听人说过什么车到山前必有路,船到桥头自然直。哈哈,这种思想就是回溯思想,也可称为试探思想。
(1),思想
有时我们要得到问题的解,先从其中某一种情况进行试探,在试探过程中,一旦发现原来的选择是错误的,那么就退回一步重新选择,然后继续向前试探,反复这样的过程直到求出问题的解。
(2),场景
回溯思想是一个非常重要的思想,应用场景也是非常广泛。
① “下棋”: 每一次走棋的位置都要考虑到是否是损人利己,如果是害人害己的走法就要回撤,找下一步损人利己的走法。
② “迷宫”: 这种问题用试探法来解决相信我也不用向大家介绍了,其实迷宫问题抽象起来就是“对图的遍历问题“,当然对图的遍历我先前的文章是有的,有兴趣的可以自己看一看。
(3),举例
记得我写第一篇文章的时候有园友希望我能找些实际的项目案例,这不,今天就给大家带来了,首先就拿博客园的“网站分类”层级菜单来说吧,首先上图:
    
针对这样的层级结构我们设计数据表一般都会设计成无限极分类,如下图:
    
那么问题来了,针对这样的数据,我们该如何在页面上呈现呢?码农的做法就是点击一个父节点然后异步去数据库读取子节点,好一点的做法就会有人把数据放在xml里面,但是都逃避不了多次与服务器进行交互,带来比较大的性能问题。
我们这里要讲的当然是减轻服务器的压力,页面呈现的时候直接Load出所有数据,然后序列化为Json,就如上面的图中一样,我们用算法来解剖上面的json数据。
首先上面的json数据是由多个多叉树组成的森林,画图如下:
    
   
那么接下来如何遍历这个森林,数据结构中,森林是可以转化为二叉树的,然后采用”先序,中序 或者 后序”,当然对森林遍历也可以采用“深度优先,广度优先”。
好了,分析了这么多,其实也就是二步走:
第一: 将Json数据变成森林的数据结构模型。
第二:对森林进行遍历,这里就采用深度优先。
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
    <title></title>
    <script src="Scripts/jquery-1.4.1.js" type="text/javascript"></script>
    <script type="text/javascript">
        $(document).ready(function () {
            var zNodes = [
            { id: 1, pId: 0, name: ".Net技术" },
            { id: 2, pId: 0, name: "编程语言" },
            { id: 3, pId: 0, name: "软件设计" },
            { id: 4, pId: 1, name: ".Net新手区" },
            { id: 5, pId: 1, name: "Asp.Net" },
            { id: 6, pId: 1, name: "C#" },
            { id: 7, pId: 1, name: "WinForm" },
            { id: 8, pId: 4, name: ".Net码畜区" },
            { id: 9, pId: 2, name: "Java" },
         ];
            var setting = ["id", "pId"];
            //第一步: 转化数据结构模型
            var result = ToForest(zNodes, setting);
            var mynode = "<ul>" + GetNodes(result) + "</ul>";
            $("body").append(mynode);
        });
        var html = "";
        //第二步:深度优先(这里面的html格式可以自己更改)
        function GetNodes(result) {
            for (var i = 0; i < result.length; i++) {
                html += "<li>" + result[i].name;
                if (result[i].childs != undefined) {
                    html += "<ul>";
                    GetNodes(result[i].childs);
                    html += "</ul>";
                }
                html += "</li>";
            }
            return html;
        }
        //setting的格式:[ID,Name,PID]
        function ToForest(sNodes, setting) {
            var i, l,
            //主键ID
            key = setting[0];
            //parentID
            parentKey = setting[1];
            //childs
            childsKey = "childs";
            //参数检查
            if (!key || key == "" || !sNodes)
                return [];
            if ($.isArray(sNodes)) {
                //存放森树形式的数据模型
                var r = [];
                //存放以ID为key,ID对应的实体为value
                var tmpMap = [];
                //赋值操作
                for (i = 0; i < sNodes.length; i++) {
                    //获取当前的id
                    var id = sNodes[i][key];
                    tmpMap[id] = sNodes[i];
                }
                //对json逐层遍历确定层级关系
                for (i = 0; i < sNodes.length; i++) {
                    //获取当前的pid
                    var pid = sNodes[i][parentKey];
                    //判断是否是顶级节点
                    if (tmpMap[pid]) {
                        //判断该节点是否有孩子节点
                        if (!tmpMap[pid][childsKey])
                            tmpMap[pid][childsKey] = [];
                        //将此节点放在该节点的孩子中
                        tmpMap[pid][childsKey].push(sNodes[i]);
                    } else {
                        //如果是顶级节点直接存放
                        r.push(sNodes[i]);
                    }
                }
                return r;
            } else {
                return [sNodes];
            }
        }
    </script>
</head>
<body>
</body>
</html>

七,动态规划(略)
八,概率思想
概率算法每一步的选择都是随机的,当在某些领域问题中通常比最优选择省时,所以就大大提高了算法的效率,降低了复杂度。
(1),思想
这里主要讲一下“数值概率算法”,该算法常用于解决数值计算问题,并且往往只能求得问题的近似解,同一个问题同样的概率算法求解两次可能得到的结果大不一样,不过没关系,这种“近似解”会随时间的增加而越接近问题的解。
(2),特征
现实生活中,有很多问题我们其实都得不到正确答案,只能得到近似解,比如“抛硬币”求出正面向上的概率,”抛骰子“出现1点的概率,再如:求“无理数π”的值,计算"“定积分”等等。针对这样如上的情况,使用概率算法求解是再好不过的了。
(3),举例(没学过微积分不懂。。。)
数值概率中,最经典的一个题目就是“计算定积分”,设f(x)=1-x2 ,计算定积分:I = ∫01 (1-x2)dx 的值。
                    
                
                
            
        
浙公网安备 33010602011771号