美文网首页
数据结构与算法

数据结构与算法

作者: OOMNPE | 来源:发表于2020-06-15 18:48 被阅读0次

    1. 为什么要学习数据结构和算法?

    1. 直接好处就是写出性能更优的代码;
    2. 算法,是一种解决问题的思路和方法,有机会应用到生活和事业的其他方面;
    3. 长期来看,大脑的思考能力是一个人的核心竞争能力,而算法是为数不多的能够有效训练大脑思考能力的途径之一;

    2. 怎么学

    1. 多思考,死记硬背不如不学
    2. 一定要敲代码,适当刷题
    3. 学习笔记,留言区互动
    4. 遇到难点反复迭代

    3. 结构图谱

    image.png
    10个主要数据结构

    数组、链表、栈、队列、散列表、二叉树、堆、跳表、图、Trie 树

    10个主要算法

    递归、排序、二分查找、搜索、哈希算法、贪心算法、分治算法、回溯算法、动态规划、字符串匹配算法

    4. 效率和资源消耗的度量衡--复杂度分析

    4.1 时间复杂度

    渐进时间复杂度,标识算法的执行时间与数据规模之间的增长关系.

    1. 只关注循环执行次数最多的那段代码
    2. 加法法则:总复杂度等于量级最大的那段代码的复杂度
    3. 乘法法则:嵌套代码的复杂度等于嵌套内外代码复杂度的乘积
    4. logn表示以任意数字为底数的时间复杂度,O(log3n) = O(C * log2n) = O(log2n)


      image.png
    4.2 空间复杂度

    渐进空间复杂度,表示算法的存储空间与数据规模之间的增长关系.
    注意是因为算法而额外增加的存储空间,不包括数据本身的存储空间;

    5. 数组 & 链表

    image.png
    5.1 对比
    1. 数组是内存中连续的一块内存空间,初始化时,必须要有这一块能够满足的内存用以开辟;而链表支持动态扩容
    2. 数组访问比链表速度快,数组根据首地址 + 偏移量 * 元素大小 寻址,而链表需要遍历;
    3. 链表比数组插入快;

    基础的数据结构就是数组和链表, 而后面更加复杂的 树 队列 图 等等 都可以通过数组和链表等方式存储, 出现树 队列 图 等数据结构的原因 就是为了解决 部分问题处理过程中时间复杂度过高的问题, 所以数据结构就是为了算法而生的

    5.2 如何用链表实现LRU缓存淘汰算法
    1. 指导思想
      单向链表,越靠后的越是最早访问
    2. 流程
      每次获取遍历链表找到节点,找到后,删除原位置数据,在头节点重新插入;没找到时,直接在头节点插入;
      如果链表超过容量,则删除最末尾节点
    3. 优化方案
      此方案每次需要遍历链表,时间复杂度为O(n),可考虑散列表来记录每个数据的位置,将时间复杂度降到O(1);

    6. 栈

    见代码

    7. 队列

    见代码

    8. 递归算法

    8.1 使用条件
    1. 问题可以分解为多个子问题
    2. 问题和子问题除了数据规模不同,解题思路一样
    3. 存在递归终止条件
    8.2 递归常见问题及解决方案
    1. 警惕堆栈溢出:可以声明一个全局变量来控制递归的深度,从而避免堆栈溢出(或者改为迭代循环的非递归写法)。
    2. 警惕重复计算:通过某种数据结构来保存已经求解过的值,从而避免重复计算。
    8.3 如何将递归改写为非递归代码?

    笼统的讲,所有的递归代码都可以改写为迭代循环的非递归写法。如何做?抽象出递推公式、初始值和边界条件,然后用迭代循环实现。

    9. 排序算法

    实现代码

    image.png

    在小规模数据面前,O(n2) 时间复杂度的算法并不一定比 O(nlogn) 的算法执行时间长

    10. 二分查找

    O(logn) 惊人的查找速度
    10.1 局限性
    1. 首先,二分查找依赖的是顺序表结构,简单点说就是数组。
    2. 其次,二分查找针对的是有序数据。
    3. 再次,数据量太小不适合二分查找。(顺序扫即可)
    4. 最后,数据量太大也不适合二分查找。(数组-连续内存空间)

    11. 散列表

    11.1 散列表由来
    1. 散列表来源于数组,利用数组按照下标随机访问的特性;
    2. 需要存储在散列表中的数据称为键,将键转化为数组下标的方法叫做散列函数,散列函数计算的结果叫做散列值,数据内容存储在散列值对应的数组下标位置;
    11.2 散列冲突的解决方案
    1. 开放寻址法
    2. 链表法(常用)
    为什么散列表和链表经常放在一起使用?

    虽然散列表支持高效的插入、删除、查找数据,但是数据都是通过散列函数打乱存储的,不支持排序,结合链表,可以做到既能快速增删查,也能排序查询;


    redis如何用O(1)实现LRU算法

    12. 哈希算法

    将任意长度的二进制值串映射成固定长度的二进制值串,这个映射的规则就是哈希算法,而通过原始数据映射之后得到的二进制值串就是哈希值

    12.1 如何设计一个优秀的哈希算法
    1. 从哈希值不能反向推导出原始数据(所以哈希算法也叫单向哈希算法);
    2. 对输入数据非常敏感,哪怕原始数据只修改了一个 Bit,最后得到的哈希值也大不相同;
    3. 散列冲突的概率要很小,对于不同的原始数据,哈希值相同的概率非常小;4. 哈希算法的执行效率要尽量高效,针对较长的文本,也能快速地计算出哈希值。
    12.2 一致性哈希算法

    在分布式存储应用中,利用一致性哈希算法,可以解决缓存等分布式系统的扩容、缩容导致数据大量搬移的难题。


    image.png

    13. 二叉树

    几个概念
    13.1 二叉树有哪几种存储方式
    1. 链式存储法
      每个节点由3个字段,其中一个存储数据,另外两个是指向左右子节点的指针。我们只要拎住根节点,就可以通过左右子节点的指针,把整棵树都串起来。这种存储方式比较常用,大部分二叉树代码都是通过这种方式实现的。
    2. 顺序存储
      数组来存储,对于完全二叉树,如果节点X存储在数组中的下标为i,那么它的左子节点的存储下标为2i,右子节点的下标为2i+1,反过来,下标i/2位置存储的就是该节点的父节点。注意,根节点存储在下标为1的位置。完全二叉树用数组来存储时最省内存的方式。
      完全二叉树定义的由来
    13.2 二叉树的遍历
    二叉树的遍历
    13.3 二叉查找树

    在树中的任意一个节点,其左子树中的每个节点的值,都要小于这个节点的值,而右子树节点的值都大于这个节点的值

    13.4 有了如此高效的散列表,为什么还需要二叉树?
    1. 散列表无序存储,如果要有序输出,则需要排序,而二叉树用时间复杂度为O(n)的中序遍历即可
    2. 散列表插入时有扩容、缩容的话性能低下
    3. 散列表基于数组,需要连续的内存空间,如果数据量大,不一定满足
    4. 散列表如果hash冲突,不一定比二叉树高效;
    13.5 平衡二叉查找树

    二叉树中任意一个节点的左右子树的高度相差不能大于 1;
    平衡二叉查找树中“平衡”的意思,其实就是让整棵树左右看起来比较“对称”、比较“平衡”,不要出现左子树很高、右子树很矮的情况。这样就能让整棵树的高度相对来说低一些,相应的插入、删除、查找等操作的效率高一些;

    13.6 红黑树

    reference
    红黑树是最常用的一种平衡二叉查找树;它是「近似平衡」的。它是为了解决普通二叉查找树在数据更新的过程中,复杂度退化的问题而产生的。红黑树的高度近似 log2n,所以它是近似平衡,插入、删除、查找操作的时间复杂度都是 O(logn)。

    • 根节点是黑色的;
    • 每个叶子节点都是黑色的空节点(NIL),也就是说,叶子节点不存储数据;
    • 任何相邻的节点都不能同时为红色,也就是说,红色节点是被黑色节点隔开的;
    • 每个节点,从该节点到达其可达叶子节点的所有路径,都包含相同数目的黑色节点;
    13.7 红黑树自平衡的三种操作:左旋、右旋和变色。
    左旋、右旋
    1. 插入的节点必须是红色
    2. 红黑树的平衡过程是自底向上的递归操作

    14. 四个基本的算法思想

    前文介绍了基础的数据结构和算法,接下来,介绍几个更加基本的算法;确切的说,它们是算法思想,并不是具体的算法,常用来指导我们设计具体的算法和编码;

    14.1 贪心算法

    贪心算法使用的场景有限;这种算法思想更多的是指导设计基础算法,比如最小生成树算法、单源最短路径算法、霍夫曼编码;
    贪心算法的最难的一块是如何将要解决的问题抽象成贪心算法模型,只要这一步搞定之后,贪心算法的编码一般很简单;
    贪心算法解决问题的正确性虽然很多时候都看起来是显而易见的,但是要严谨地证明算法能够得到最优解,并不是件容易的事。所以,很多时候,我们只需要多举几个例子,看一下贪心算法的解决方案是否真的能得到最优解就可以了。
    因此不要刻意去记忆贪心算法的原理,多练习才是最有效的学习手段;

    14.2 分治算法

    核心思想就是分而治之,一般用递归来实现(分治算法是一种处理问题的思想,而递归是一种编程技巧);

    分治算法解决的问题需要满足几个条件:
    • 原问题与分解成的小问题具有相同的模式;
    • 原问题分解成的子问题可以独立求解,子问题之间没有相关性,这一点是分治算法跟动态规划的明显区别;
    • 具有分解终止条件,也就是说,当问题足够小时,可以直接求解;
    • 可以将子问题合并成原问题,而这个合并操作的复杂度不能太高,否则就起不到减小算法总体复杂度的效果了。

    14.3 回溯算法

    一句话描述就是:枚举所有情况,剪枝掉不满足条件的,找最优的
    回溯算法一般采用递归的方式来实现
    回溯的处理思想,有点类似枚举搜索;枚举所有的解,找到满足期望的解;
    为了有规律地枚举所有可能的解,避免遗漏和重复,我们把问题求解的过程分为多个阶段。每个阶段,我们都会面对一个岔路口,我们先随意选一条路走,当发现这条路走不通的时候(不符合期望的解),就回退到上一个岔路口,另选一种走法继续走。
    回溯算法可以用来指导像深度优先搜索这种算法,除此之外,很多经典的数学问题都可以用回溯来解决:数独、八皇后、0-1背包、图的着色、旅行商问题、正则表达式;

    14.3.1 0-1背包问题的回溯解法

    0-1背包问题的经典解法是动态规划,不过还有一种简单但是没那么高效的算法:回溯算法;

    
    public int maxW = Integer.MIN_VALUE; //存储背包中物品总重量的最大值
    // cw表示当前已经装进去的物品的重量和;i表示考察到哪个物品了;
    // w背包重量;items表示每个物品的重量;n表示物品个数
    // 假设背包可承受重量100,物品个数10,物品重量存储在数组a中,那可以这样调用函数:
    // f(0, 0, a, 10, 100)
    public void f(int i, int cw, int[] items, int n, int w) {
            if (i == n) { // i==n表示已经考察完所有的物品
                if (cw > maxW && cw <= w) // 不能超过背包重量
                    maxW = cw;
                return;
            }
            f(i + 1, cw, items, n, w); // 当前物品先选择不装进背包
            f(i + 1, cw + items[i], items, n, w); // 又回溯回来,当前物品装进背包
    }
    

    14.4 动态规划

    大部分动态规划能解决的问题,都可以通过回溯算法来解决;
    上面的0-1背包问题用回溯法时间复杂度是:O(2^n),而通过动态规划可以将时间复杂度缩短至:O(n*w)。n 表示物品个数,w 表示背包可以承载的总重量。当n很大时,指数级的时间复杂度要比O(n*w)高很多!
    动态规划比较适合用来求解最优问题,比如求最大值、最小值等等,它可以非常显著地降低时间复杂度;

    适合用动态规划解决的问题
    • 一个模型
    • 三个特征
      最优子结构、无后效性、重复子问题
    动态规划解决问题的思路:

    我们把问题分解为多个阶段,每个阶段对应一个决策。我们记录每一个阶段可达的状态集合(去掉重复的),然后通过当前阶段的状态集合,来推导下一个阶段的状态集合,动态地往前推进。这也是动态规划这个名字的由来。


    01背包问题的动态规划解题过程
    • 状态转移表法
      回溯算法实现 - 定义状态 - 画递归树 - 找重复子问题 - 画状态转移表 - 根据递推关系填表 - 将填表过程翻译成代码
    • 状态转移方程法
      找最优子结构 - 写状态转移方程 - 将状态转移方程翻译成代码
    动态规划的弊端

    尽管动态规划的执行效率比较高,但是额外申请一个n乘w+1的二维数组,对空间的消耗比较多;所以,动态规划是一种空间换时间的解决思路;
    实际上,我们只需要一个w+1的一维数组就可解决问题;动态规划状态转移的过程,都可以基于这个一维数组来操作;

    0-1背包问题的动态规划解法:
    public static int knapsack2(int[] items, int n, int w) {
      boolean[] states = new boolean[w+1]; // 默认值false
      states[0] = true;  // 第一行的数据要特殊处理,可以利用哨兵优化
      if (items[0] <= w) {
        states[items[0]] = true;
      }
      for (int i = 1; i < n; ++i) { // 动态规划
        for (int j = w-items[i]; j >= 0; --j) {//把第i个物品放入背包
          if (states[j]==true) states[j+items[i]] = true;
        }
      }
      for (int i = w; i >= 0; --i) { // 输出结果
        if (states[i] == true) return i;
      }
      return 0;
    }
    

    14.5 贪心 vs 回溯 vs 动态规划

    • 贪心
      一条路走到黑,就一次机会,只能哪边看着顺眼走哪边
    • 回溯
      一条路走到黑,无数次重来的机会,还怕我走不出来(Snapshot View)
    • 动态规划
      拥有上帝视角,手握无数平行宇宙的历史存档,同时发展出无数个未来(Versioned Archive View)

    相关文章

      网友评论

          本文标题:数据结构与算法

          本文链接:https://www.haomeiwen.com/subject/chyhxktx.html