常用数据结构与算法

基本数据结构与算法

数据结构

  • 线性表
  • 数组
  • 链表
  • 栈、队列
  • 树、二叉树
  • 二叉树
  • 二分搜索树
  • 平衡二叉树
  • 红黑树
  • 二叉堆
  • 线段树
  • Trie
  • 集合、映射
  • 并查集

算法

  • 排序算法
  • 二分查找
  • DFS、BFS、回溯
  • 贪心
  • 分治法
  • 最短路径
  • 字符串匹配
  • 动态规划
  • 蒙特卡洛

什么是数据结构

数据结构是计算机存储、组织数据的方式。数据结构是指相互之间存在一种或多种特定关系的数据元素的集合。通常情况下,精心选择的数据结构可以带来更高的运行或者存储效率。

线性表
  • 数组的优缺点:

可以直接利用索引进行访问,速度快,在随机访问需求大的时候利用数组来存储数据是不二的选择;但对于一般的编程语言而言,数组的长度是不可变的,这就导致如果对其进行增删操作,我们必须新开辟一个数组,并将原数组的所有元素赋值到新数组相应的位置,是一个相对比较复杂的操作。

  • 链表的优缺点:

对于增删操作,链表仅需要改变涉及到的节点的next即可;但对于随机访问而言,由于链表是从头开始访问的,我们对其中的某一个节点进行访问时都需要把它前面的节点访问到,这个过程产生了不必要的时间上的浪费,因此链表是不擅于随机访问的。

  • 链表的应用:许多的高级数据结构都用到它,例如Java的HashMap使用链表解决hash冲突,还有经典小游戏“贪吃蛇”
栈与队列
  • 栈:后进先出(LIFO),是一种操作受限的线性表,其限制仅允许在表的一端进行插入和删除操作。

在这里插入图片描述

  • 栈的应用:数制转换、括号匹配、Ctrl+Z操作、Java虚拟机栈…
  • 队列:先进先出(FIFO),和栈一样,也是一种操作受限制的线性表,其只允许在表的一端进行插入操作,而在表的另一端进行删除操作。
  • 队列的应用:打印机

每个节点都有N(N>=0)个子节点,每个节点最多只有 1 个父节点

在这里插入图片描述

二叉树

二叉树是每个结点最多有两个子树的树结构。通常子树被称作“左子树”(left subtree)和“右子树”(right subtree)。

在这里插入图片描述

  • 二叉树有不同类型的定义:
  • 完全二叉树:除最底层外树的各层都达到该层的最大数,并且最底层叶子节点都是从左到右依次排布。
  • 满二叉树:除叶子节点外每个节点都有左右子节点,并且叶子节点都在树的最底层。
  • 平衡二叉树:左右子树的高度差不超过 1 ,且左右子树都是平衡二叉树。
二分搜索树

若它的左子树不空,则左子树上所有结点的值均小于它的根结点的值; 若它的右子树不空,则右子树上所有结点的值均大于它的根结点的值; 它的左、右子树也分别为二叉排序树。若要处理[12,5,2,18,19,15,17,16,9]这样的数组,则可以将其转化为下图的二分搜索树。

在这里插入图片描述

从上图可以很轻易地看出,若需要将这些数据进行排序处理,只需中序遍历二分搜索树即可,其不失为一种排序手段。对于数据的查找则只需依照二分搜索树的定义进行搜索,一般情况下的时间复杂度为O(logN),而在某些特殊情况下,二分搜索树会退化为链表,例如[1,2,3,4,5],如此搜索的时间复杂度则变为O(N)。

在这里插入图片描述

平衡二叉树

它是一棵空树或它的左右两个子树的高度差的绝对值不超过1,并且左右两个子树都是一棵平衡二叉树。

右旋:根是Root,右子树是x,左子树的根为RootL,左子树的两个孩子树分别为LLeftChild和LRightChild。则右旋后,该子树的根为RootL,左子树为LLeftChild,右子树的根为Root,Root的两个孩子树分别为LRightChild(左)和x(右)。

在这里插入图片描述

左旋:根是Root,右子树是x,左子树的根为RootL,左子树的两个孩子树分别为LLeftChild和LRightChild。则右旋后,该子树的根为RootL,左子树为LLeftChild,右子树的根为Root,Root的两个孩子树分别为LRightChild(左)和x(右)。

让我们来看看如何实现如下二分搜索树[ 3,2,1,4,5,6,7,10,9,8 ]的转换:

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

由于平衡二叉树的高度平衡,导致其维护的代价远大于它所带来的收益,故而应用不多。但其搜索性能是优于红黑树的。

红黑树

红黑树和AVL树类似,都是在进行插入和删除操作时通过特定操作保持二叉查找树的平衡,从而获得较高的查找性能,它的性质限制决定了它从根到叶子的最长的可能路径不多于最短的可能路径的两倍长,因此它并非高度平衡而只是大致平衡。相比AVL的优点便是对于自身结构性质的恢复操作更加简单。

它虽然是复杂的,但它的最坏情况运行时间也是非常良好的,并且在实践中是高效的: 它可以在O(log n)时间内做查找,插入和删除,这里的n 是树中元素的数目。

红黑树是每个节点都带有颜色属性的二叉查找树,颜色或红色或黑色。在二叉查找树强制一般要求以外,对于任何有效的红黑树我们增加了如下的额外要求:

  1. 节点是红色或黑色。
  2. 根节点是黑色。
  3. 每个叶节点(NIL节点,空节点)是黑色的。
  4. 每个红色节点的两个子节点都是黑色。(从每个叶子到根的所有路径上不能有两个连续的红色节点)
  5. 从任一节点到其每个叶子的所有路径都包含相同数目的黑色节点。

put过程:

在这里插入图片描述

详见:http://www.sohu.com/a/201923614_466939

https://www.cnblogs.com/CarpenterLee/p/5503882.html

其主要应用于:C++ STL中的map、set,Java的TreeMap、HashMap,nginx中管理timer…

二叉堆

二叉堆是一颗完全二叉树,分两种:最大堆和最小堆。最大堆:父结点的键值总是大于或等于任何一个子节点的键值;最小堆:父结点的键值总是小于或等于任何一个子节点的键值。即便它是树形结构,我们一般还是使用数组来表示它。

构造一棵最大堆(heapify):

在这里插入图片描述

TOP N 问题主要算法过程(Shift Down):

在这里插入图片描述

更多基本操作参考:https://www.cnblogs.com/skywang12345/p/3610187.html

主要应用:优先队列(Priority Queue),多个有序元素的合并,TOP N 问题,堆排序…

线段树

线段树是一种平衡的二叉搜索树,与区间树相似,它将一个区间划分成一些单元区间,每个单元区间对应线段树中的一个叶结点。对于线段树中的每一个非叶子节点[a,b],它的左儿子表示的区间为[a,(a+b)/2],右儿子表示的区间为[(a+b)/2+1,b],最后的子节点数目为N,即整个线段区间的长度。对于线段树,我们也可以使用数组来表示。

在这里插入图片描述

对于查询某一个区间的值,只需通过简单的递归搜索便可得到,其时间复杂度为O(logN),若使用数组表示,则最大空间复杂度为O(4N)。详情参考:https://www.cnblogs.com/TheRoadToTheGold/p/6254255.html

#####Trie(字典树)
Trie又称字典树、前缀树、单词查找树或键树,是一种树形结构。典型应用是用于统计,排序和保存大量的字符串(但不仅限于字符串),所以经常被搜索引擎系统用于文本词频统计。它的优点是:利用字符串的公共前缀来减少查询时间,最大限度地减少无谓的字符串比较,查询效率比哈希树高。

Trie的核心思想是空间换时间,利用字符串的公共前缀来降低查询时间的开销以达到提高效率的目的。它有3个基本性质:

  1. 根节点不包含字符,除根节点外每一个节点都只包含一个字符。
  2. 从根节点到某一节点,路径上经过的字符连接起来,为该节点对应的字符串。
  3. 每个节点的所有子节点包含的字符都不相同。

在这里插入图片描述

通常我们会将某个节点标识出来以代表其前缀(从根节点到该节点的父节点)和该节点构成一个完整的单词。字典树也有许多种优化和变种,例如:将最大公共前缀存放在一个节点中可进一步优化查询效率;B树。

集合与映射
集合

是承载元素的容器,但每个元素都只能存在一次;能够快速实现“去重”工作。集合可以由多种数据结构和方法进行实现,例如:数组,链表,树。不同的实现相应也有不同的使用场景,精心选择数据结构来进行实现能够极大地提升对集合操作的时间性能。例如:C++ STL 的Set使用红黑树,Java 的 ArrayList、LinkedList、HashSet底层分别对应三种数据结构。

映射

在数学里,映射是个术语,指两个元素的集之间元素相互“对应”的关系,即 f(x)= y 或者f(x)-> y 。在计算机中,通常称之为 哈希表(Hash Table),是根据关键码值(Key value)而直接进行访问的数据结构。也就是说,它通过把关键码值映射到表中一个位置来访问记录,以加快查找的速度。这个映射函数f(x)叫做散列函数,存放记录的列表叫做散列表。

通过散列函数 f(x) 运算之后的值 y 可能造成冲突(即f(x1) = f(x2)),其称之为 哈希冲突。

在数据结构中,能够通过位置直接定位到数据的容器则只有数组,因此这个散列表通常使用数组实现,但由于可能发生哈希冲突问题,为此,在数组后跟链表来存储哈希冲突的数据以解决哈希冲突问题。

在这里插入图片描述

并查集

并查集是一种树型的数据结构,用于处理一些不相交集合(Disjoint Sets)的合并及查询问题。常常在使用中以森林来表示,但每个节点的指向和普通树结构相反,是由子节点指向父节点。

如需对下图进行关系查询:

在这里插入图片描述

使用并查集改进后:

在这里插入图片描述

对于该图中,如需查询“金毛狮王”和“胡青牛”是否为“同一门派”,则只需查询他们最终指向的人(根节点)是否为同一人即可。该图也可表示为如下森林:

在这里插入图片描述

在并查集中,我们一般不会过多考虑两个节点的指向问题,即便是将箭头反向,对于结果都是不会存在任何差异的,而只是会改变程序执行所需要的时间。并查集基本操作参考:https://blog.csdn.net/johnny901114/article/details/80721436


什么是算法

以一步接一步的方式来详细描述计算机如何将输入转化为所要求的输出的过程,或者说,算法是对计算机上执行的计算过程的具体描述。同时它必须满足以下4个性质:

  1. 有穷性
  2. 确切性
  3. 可行性
  4. 有输出
排序算法

排序算法有一个比较重要的概念——稳定性:当有两个相等记录的关键字R和S,且在原本的列表中R出现在S之前,在排序过的列表中R也是在S之前,那么这个排序算法就是稳定的。

在这里插入图片描述

各排序算法详细参考:https://www.cnblogs.com/onepixel/articles/7674659.html (动画演示)

二分查找

二分查找也称折半查找(Binary Search),它是一种效率较高的查找方法,在线性顺序表中,折半进行“大小”对比以确定目标处于哪一半,重复该操作直到找到目标为止。以下为二分查找与线性查找的对比:

  1. 检索 23

在这里插入图片描述

  1. 检索 1

在这里插入图片描述

当然这只是两个极端情况,但依旧可以看出,即便是在最坏的情况下,使用二分查找最多只需要 logN 次(O(logN)时间复杂度)而普通线性查找平均需要 N/2 次(O(N)时间复杂度),对于 O(logN) 这样的时间复杂度是我们完全可以接受的。

public class BinarySearch {

    /**
     * 检索 val 在 arr 数组中的索引
     */
    public static int search(int[] arr, int key) {
        return binarySearch(arr, key, 0, arr.length - 1);
    }

    /**
     * 使用二分查找检索 val 在 arr数组中的索引,若不存在则返回 -1
     * 
     * @param left 检索的左边界
     * @param right 检索的右边界
     */
    private static int binarySearch(int[] arr, int key, int left, int right) {
        if (left == right) {
            return key == arr[left] ? left : -1;
        }
        int mid = (left + right) >>> 1;
        if (key == arr[mid]) {
            return mid;
        } else if (key < arr[mid]) {
            return binarySearch(arr, key, left, mid);
        } else {
            return binarySearch(arr, key, mid + 1, right);
        }
    }

    public static void main(String[] args) {
        System.out.println(search(new int[] {1, 2, 3, 4, 5, 6, 7, 8}, 5));
    }

}
DFS、BFS、回溯

DFS(深度优先搜索)、BFS(广度优先搜索)事实上都属于图算法。算法思想也正如其名。

DFS 是将图中一条路径走到最后再退回上一步继续走另一条路径,直至将所有路径一条一条走完。

BFS 是都会对所有路径同时走,每一条路径以终点作为结束而不会退回上一步,但这并不意味着 BFS 快于 DFS,因为它们都将所有的路径遍历了一次,在单线程中 BFS 和 DFS 并没有多大的区别。

DFS 走迷宫:

在这里插入图片描述

自然界中的BFS:

在这里插入图片描述

以下为二叉树中的 DFS、BFS:

public class BinaryTree {
    int val;
    BinaryTree left;
    BinaryTree right;

    /**
     * DFS 深度优先搜索
     */
    public void dfs(BinaryTree root) {
        if (root == null) {
            return;
        }
        visit(root);
        dfs(root.left);
        dfs(root.right);
    }

    /**
     * BFS 广度优先搜索
     */
    public void bfs(BinaryTree root) {
        if (root == null) {
            return;
        }
        Queue<BinaryTree> queue = new LinkedList<>();
        queue.add(root);
        while (!queue.isEmpty()) {
            BinaryTree node = queue.poll();
            visit(node);
            if (node.left != null) {
                queue.add(node.left);
            }
            if (node.right != null) {
                queue.add(node.right);
            }
        }
    }

    /**
     * 自定义访问操作
     */
    private void visit(BinaryTree node) {
        System.out.println(node.val);
    }

}

回溯法是一种选优搜索法,又称为试探法,按选优条件向前搜索,以达到目标。但当探索到某一步时,发现原先选择并不优或达不到目标,就退回一步重新选择,而满足回溯条件的某个状态的点称为“回溯点”。往往使用在 DFS 中,走迷宫就使用到了 DFS + 回溯法。

贪心算法

贪心算法(又称贪婪算法)是指,在对问题求解时,总是做出在当前看来是最好的选择。也就是说,不从整体最优上加以考虑,他所做出的是在某种意义上的局部最优解,因此,选择的贪心策略必须具备无后效性,即某个状态以前的过程不会影响以后的状态,只与当前状态有关。

举个栗子:假设有多张面值为1元、2元、5元、10元、20元、50元、100元的纸币。现在要用这些钱来支付 442 元,至少要用多少张纸币?用贪心算法的思想,很显然,每一步尽可能用面值大的纸币即可。则得到 100 x 4+20 x 1+10 x 1+5 x 2+2 x 1,显然答案是正确的,然而如果现在只有面值为3元、5元、10元、50元、100元的纸币,需要支付 416 元呢?同样使用贪心算法的思想,得到的是 100 x 4+10 x 1+5 x 1,显然并非正确答案,因此这种情况下是贪心算法是不适用的。但一些情况其实我们并不关心它是否精确,只需得到一个近似解即可,对于这种情况,贪心算法也是具有很大的价值的。

分治法

分治算法的基本思想是将一个规模为N的问题分解为K个规模较小的子问题,这些子问题相互独立且与原问题性质相同。求出子问题的解,就可得到原问题的解。即一种分目标完成程序算法,简单问题可用二分法完成。分治法解题的一般步骤:

  1. 分解,将要解决的问题划分成若干规模较小的同类问题;
  2. 求解,当子问题划分得足够小时,用较简单的方法解决;
  3. 合并,按原问题的要求,将子问题的解逐层合并构成原问题的解。

分治法和贪心区别在于,分治法最终会将分解的子问题的解合并构成原问题的解,而贪心则几乎不会做合并这一步操作。

分治法在排序中的应用(归并排序):

在这里插入图片描述

最短路径

从某顶点出发,沿图的边到达另一顶点所经过的路径中,各边上权值之和最小的一条路径叫做最短路径。事实上这个问题是属于图论中的一种,解决最短路的问题有以下算法,Dijkstra算法,Floyd算法和SPFA算法等。

以 Dijkstra 为例:

在这里插入图片描述

Dijkstra 算法是一种类似于贪心的时间复杂度为O(n^2 )的算法,它能够在已知源点后求出从源点到其他每个点的最短距离,步骤如下:

  1. 当到某一个时间点时,图上部分的点的最短距离已确定,部分点的最短距离未确定。
  2. 选一个所有未确定点中离源点最近的点,把他认为成最短距离。
  3. 再把这个点所有出边遍历一遍,并更新从源点到所有点的最短距离。
public class Dijkstra {
    // 表示从源点(指定点)到其他个点的最短距离
    static long[] dis;
    // 表示图,例如 map[1][2] 则表示从点 1 到 2 的边距
    static long[][] map;
    // 记录点是否被当做源点搜索过,0 表示否,否则表示是
    static int[] visited;
    /**
     * 
     * 使用 dijkstra 算法求出点 r 到其他各点的最短距离
     * 
     * @param start 指定源点
     * @param size 需要搜索最短路径的点的数量
     */
    public static void dijkstra(int start, int size) {
        // 第一次更新到每一点的距离
        for (int i = 1; i <= size; i++) {
            dis[i] = map[start][i];
        }
        // 定义最近的点
        int minn = start;
        // 外圈表示最多需要搜索点的次数
        for (int i = 0; i < size; i++) {
            // 因为最短路径可能为 Integer.MAX_VALUE ,故定义为 Integer.MAX_VALUE+1
            long min = Integer.MAX_VALUE + (long)1;
            // 循环检测哪一个点为最近且未被当做源点搜索过的点
            for (int j = 1; j <= size; j++) {
                if (visited[j] == 0 && dis[j] < min) {
                    min = dis[j];
                    minn = j;
                }
            }
            // 表示该点已经被当做源点搜索过
            visited[minn] = 1;
            // 更新各点的 dis
            for (int j = 1; j <= size; j++) {
                dis[j] = Math.min(dis[j], dis[minn] + map[minn][j]);
            }
        }
    }
    /*
      输入样例:
    5 6
    1 2 5
    1 3 8
    2 3 1
    2 4 3
    4 5 7
    2 5 2
     */
    @SuppressWarnings("resource")
    public static void main(String[] args) {
        Scanner in = new Scanner(System.in);
        // n 为有多少个点
        int n = in.nextInt();
        // m 为有多少边
        int m = in.nextInt();
        // 初始化visited、dis、map,长度 +1 是因为索引不从0开始而是从 1 开始
        visited = new int[n + 1];
        map = new long[n + 1][n + 1];
        dis = new long[n + 1];
        // 初始假定所有最短路径值都非常大,图中所有的边长都非常大
        for (int i = 1; i <= n; i++) {
            dis[i] = Integer.MAX_VALUE;
            for (int j = 1; j <= n; j++) {
                if (i != j) {
                    map[i][j] = Integer.MAX_VALUE;
                }
            }
        }
        // 输入两点及其距离
        for (int i = 0; i < m; i++) {
            int a = in.nextInt();
            int b = in.nextInt();
            int v = in.nextInt();
            map[a][b] = v;
            map[b][a] = v;
        }
        dijkstra(5, n);
        for (int i = 1; i <= n; i++) {
            System.out.println(dis[i]);
        }
    }
}
字符串匹配

顾名思义,就是给定一个原字符串和模式串,判定模式串是否出现在原字符串中,并给出(首次或多次)出现的位置。主要算法有:BF(暴力匹配算法)、BK(哈希检索算法)、KMP算法、BM算法(Boyer Moore)、Sunday算法等。

以 KMP 为例:

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

KMP 是将匹配失败后的信息利用起来,让模式串的移动量尽可能的大,从而减少匹配次数。对于上面的例子,如果我们能得到一个数组 next[] = {-1,0,0,0,0,1,2} 来表示在某个位置匹配失败后模式串将要移动的位置。

public class KMP {
    /**
     * 计算 next 数组
     */
    static int[] getNexts(String str) {
        char[] c = str.toCharArray();
        int[] next = new int[c.length];
        next[0] = -1;
        int k = 0;
        for (int j = 2; j < c.length; j++) {
            if (c[k] == c[j - 1]) {
                next[j] = ++k;
            } else {
                k = 0;
                if (c[k] == c[j]) {
                    next[j] = -1;
                } else {
                    next[j] = k;
                }
            }
        }
        return next;
    }
    /**
     * KMP 匹配
     */
    static int kmp(String content, String str) {
        int[] next = getNexts(str);
        int length = content.length();
        int j = 0;
        int i = 0;
        while (i < length) {
            char p = content.charAt(i);
            char b = str.charAt(j);
            if (p != b) {
                j = next[j];
                if (j < 0) {
                    j = 0;
                    i++;
                }
            } else {
                i++;
                j++;
            }
            if (j >= str.length()) {
                return i - j;
            }
        }
        return -1;
    }
    public static void main(String[] args) {
        String content = "ghghh gakjdh fue32asd546";
        String str = "32a";
        System.out.println(kmp(content, str));

    }
}

BM、Sunday算法思想各有不同,参考:https://www.cnblogs.com/Franky-ln/p/5890201.html

动态规划

动态规划(dynamic programming)是把多阶段过程转化为一系列单阶段问题,利用各阶段之间的关系,逐个求解的数学思想。换言之就是,大事化小,小事化了。动态规划一般可分为线性动规,区域动规,树形动规,背包动规四类。

以 01背包 为例,给定背包体积 vp,物品种类数量 n,各物品体积 v [ i ]、价值 w [ i ],每种物品只有一个,求出背包所能容纳的最大价值。解题思路:

首先,依次选择物品,面对每个物品,我们只有装或不装两种选择,如果我们选择不装,那么当前的最大价值是不变的,如果选择装,那么则需要判断背包是否能装下,并且装下后是否是最大价值。举个栗子,当面对第 i 个物品时,背包总体积为 j,那么当前的最大价值可以表示为 m [ i ][ j ],若 j < v [ i ],则表示背包不能装下这个物品,则 m [ i ][ j ] = m [ i - 1 ][ j ],否则将取装与不装的最大值,则 m [ i ][ j ] = MAX(m [ i - 1 ][ j ],m [ i - 1 ][ j - v [ i ] ] + w [ i ]),这里的 [ j - v [ i ] ] 表示在选择装的情况下我们需要为这个物品腾出空间,因此 m [ i - 1 ][ j - v [ i ] ] 则表示在体积有 j - v [ i ] 的背包中能装下的最大价值。于是我们就得出了它的状态方程:

if (j >= v[i]) {
    m[i][j] = Math.max(m[i - 1][j], m[i - 1][j - v[i]] + w[i]);
} else {
    m[i][j] = m[i - 1][j];
}

完整代码:

public class Package01 {
    // 代表背包体积
    static int vp;
    // 代表有多少种物品
    static int n;
    // 存储各物品体积
    static int[] v;
    // 存储各物品价值
    static int[] w;
    /**
     * 动态规划 求出背包所能装下的最大价值(状态方程:m[i][j] = Math.max(m[i - 1][j], m[i - 1][j - v[i]] + w[i]))
     */
    static int dp() {
        // 定义在每种情况下的最大价值数组,例如 m[i][j] 表示在背包体积为 j 并且在选择第 i 件物品后的最大价值
        int[][] m = new int[n + 1][vp + 1];
        for (int i = 1; i <= n; i++) {
            for (int j = 1; j <= vp; j++) {
                if (j >= v[i]) {
                    m[i][j] = Math.max(m[i - 1][j], m[i - 1][j - v[i]] + w[i]);
                } else {
                    m[i][j] = m[i - 1][j];
                }
            }
        }
        return m[n][vp];
    }
    /*
    输入样例
    12 6
    4 8
    6 10
    2 6
    2 3
    5 7
    1 2
     */
    public static void main(String[] args) {
        Scanner in = new Scanner(System.in);
        // 输入背包体积、物品种数
        vp = in.nextInt();
        n = in.nextInt();
        v = new int[n + 1];
        w = new int[n + 1];
        // 输入每种物品体积、价值
        for (int i = 1; i <= n; i++) {
            v[i] = in.nextInt();
            w[i] = in.nextInt();
        }
        System.out.println(dp());
    }
}

再来一题:当前有 10 级台阶,你上楼梯每一步只能跨 1 级或 2 级台阶,请算出有多少种上楼梯的走法。解题思路:

我们依旧将问题分阶段考虑。当我们面临最后一步时(即下一步就是第 10 级台阶),这时候我们所处的位置只有两种可能,要么正在第 9 级,要么正在第 8 级,假设我们已知上 9 级台阶有 X 种走法,上 8 级台阶有 Y 种走法,那么上 10 级台阶有多少种呢?显然等于 X + Y,那么我们就得出了 F(10) = F(9) + F(8),同理 F(i) = F(i-1) + F(i-2),这便是该题目的状态方程,实现起来就非常方便了。

蒙特卡洛

蒙特卡洛是一种通过模拟统计计算问题答案的思想,原理是通过大量随机样本,去了解一个系统,进而得到所要计算的值。早在 1777 年,便有人提出用投针实验求出圆周率 π 的计算方法,在纸上画一个正方形,并在正方形边长为直径在其中心为圆点画一个圆形,然后随机的向这张纸上投针数枚,最后利用落在正方形和圆形上的针的数量求出 π。

在这里插入图片描述
在这里插入图片描述

有了计算机,蒙特卡洛有了巨大的用武之地。例如使用程序来模拟求出圆周率:

public class MonteCarlo {
    /**
     * 使用蒙特卡洛方法求 π
     */
    public static void main(String[] args) throws InterruptedException {
        // 定义 π
        double pi = 0;
        // 定义正方形边长,即在坐标轴的坐标,左下角为(0,0),右上角为(c,c)
        double c = 2;
        // 定义圆点坐标(dot,dot)、半径 r、面积 v
        double dot = c / 2;
        double r = dot;
        double v = 0;
        // 定义随机生成的坐标(x,y)
        double x = 0;
        double y = 0;
        // 定义随机生成的坐标与圆点的距离
        double dis = 0;

        // “投针”次数
        double count = 0;
        // 落在圆上的“针”数
        double num = 0;

        Random random = new Random();
        while (true) {
            Thread.sleep(1);
            count++;
            // 随机生成坐标
            x = random.nextDouble() * c;
            y = random.nextDouble() * c;
            // 计算到圆点的距离,以确定是否在圆内
            dis = Math.sqrt(Math.pow(x - dot, 2) + Math.pow(y - dot, 2));
            if (dis < r) {
                num++;
            }
            // 求出圆的面积,v = num / count * (c * c)
            v = num / count * (c * c);
            // 求出 π,π = v / r^2
            pi = v / (r * r);
            System.out.println(pi);
        }
    }
}

除了求圆周率,蒙特卡洛还被运用于求定积分、 AI(AlphaGo) 中…

问题:有 100 个人,每人均有 100 元,现随机选择个人扣除 1 元,再随机选择一个人增加 1 元,重复此操作,一段时间后他们的经济分布是怎样的?

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值