据我了解,前端程序员有至关一部分对“数据结构”和“算法”的基础概念都不是很清晰,这直接致使不少人在看到有关这部分的内容就会望而却步。html
实际上,当你了解了“数据结构”和“算法”存在的真正意义,以及一些实际的应用场景,对它有了一个总体的认知以后,你可能会对它产生强烈的兴趣。固然,它带将带给你的收益也是至关可观的。前端
不少前端同窗在看到“数据结构”和“算法”后会有必定的抵触心理,或者尝试去练习,可是被难倒,从而放弃。git
这很大一部分缘由是由于你还不够了解学习他们的意义,或者没有掌握合理的练习方法。程序员
实际上,当你有了必定的目的性,而且有了合理的练习方法,再来学习这部份内容会变得驾轻就熟。
å
在本文中,我就来分享一下我学习“数据结构”和“算法”的一些经验和方法。github
后面我也会针对全部常见的数据结构和算法分类,进行全方位的梳理。面试
数据结构和算法的种类很是之多,拿树举例,树的种类包括:二叉树、B树、B+树、Trie树、红黑树等等,本文只选择了二叉树。算法
对前端来说,没有必要对某些比较偏的类型和解法多作了解,一是浪费宝贵的时间,二是应用的很少。编程
本文选择的数据结构和算法的类别均是出现频率最高,以及应用最广的类别。segmentfault
另外,作题时找对典型题目很是重要,可让你更快速更高效的掌握知识,本文后面也会给出每种类型的典型题目供你们参考。数组
题目来源:
awesome-coding-js
:个人前端算法开源项目,包括我作过的题目以及详细解析leetcode
剑指offer
另外,我会在后面长期更新一个前端算法的专栏,对每类数据结构和算法进行详细的讲解,敬请期待。
在学习某块内容以前,咱们必定要首先明确为何要学,而不是盲目的跟风。
这将更有利于你从学习的过程当中得到收益,并且会为你的学习带来动力。
首先明确一点,学习数据结构和算法不必定就是记住二叉树、堆、栈、队列等的解题方法也不是死记硬背一些题目,若是你仅仅停留在这样的表面思想,那么你学习起来会很是痛苦。
计算机只是一个很冰冷的机器,你给他下发什么样的指令,它就能做出什么样的反应。
而开发工程师要作的是如何把实际的问题转化成计算机的指令,如何转化,来看看《数据结构》的经典说法:
设计出数据结构, 在施加以算法就好了。
因此,很重要的一点,数据结构和算法对创建解决问题的思想很是重要。
若是说 Java 是自动档轿车,C 就是手动档吉普。数据结构呢?是变速箱的工做原理。你彻底能够不知道变速箱怎样工做,就把自动档的车子从 A 开到 B,并且未必就比懂得的人慢。写程序这件事,和开车同样,经验能够起到很大做用,但若是你不知道底层是怎么工做的,就永远只能开车,既不会修车,也不能造车。若是你对这两件事都不感兴趣也就罢了,数据结构懂得用就好。但若你今生在编程领域还有点更高的追求,数据结构是绕不开的课题。
这是很是现实的一点,也是不少前端学习数据结构和算法的缘由。
通常对待算法的态度会分为如下几类:
Google
、Microsoft
等知名外企在面试工程师时,算法是起决定性因素的,前端工程师也是同样,基本是每一轮都会考察,即便你有很是强的背景,也有可能由于一两道算法答的很差而与这样的企业失之交臂。
第二类,算法占重要因素的,国内的某些大厂在面试时,也会把数据结构和算法做为重要的参考因素,基本是面试必考,若是你达不到必定的要求,会直接挂掉。
第三类,起加分做用,不少公司不会把数据结构和算法做为硬性要求,可是也会象征性的出一些题目,当你把一道算法题答的很漂亮,这绝对是加分项。
可见,学好数据结构和算法对你跳槽更好的公司或者拿到更高的薪水,是很是重要的。
了解了数据结构和算法的重要性,那么究竟该用什么样的方法去准备呢?
在学习和练习以前,你必定要对数据结构和算法作一个全方位的了解,对数据结构和算法的定义、分类作一个全面的理解,若是这部分作的很差,你在作题时将彻底不知道你在作什么,从而陷入盲目寻找答案的过程,这个过程很是痛苦,并且每每收益甚微。
本文后面的章节,我会对常见的数据结构和算法作一个全方位的梳理。
当你对数据结构和算法有了一个总体的认知以后,就能够开始练习了。
注意,必定是分类练习!分类练习!分类练习!重要的事情说三遍。
我曾见过很是多的同窗带着一腔热血就开始刷题了,从leetcode
第一题开始,刚开始每每很是有动力,可能还会发个朋友圈或者沸点什么的😅,而后就没有而后了。
由于前几题很是简单,可能会给你必定的自信,可是,按序号来的话,很快就会遇到hard
。或者有的人,干脆只刷简单,先把全部的简单刷完。
可是,这样盲目的刷题,效果是很是差的,有可能你坚持下来,刷了几百道,也能有点效果,可是整个过程可能很是慢,并且效果远远没有分类练习要好。
所谓分类练习,即按每种类别练习,例如:这段时间只练习二叉树的题目,后面开始练习回溯算法的题目。
在开始练习以前,你每每还须要对这种具体的类别进行一个详细的了解,对其具体的定义、相关的概念和应用、可能出现的题目类型进行梳理,而后再开始。
在对一个类型针对练习一些题目以后,你就能够发现必定的规律,某一些题目是这样解,另外一些题目是那样解...这是一个很正常的现象,每种类型的题目确定是存在必定规律的。
这时候就能够开始对此类题目进行总结了,针对此类问题,以及其典型的题目,发现的解题方法,进行总结。当下次你再遇到这种类型的题目,你就能很快想到解题思路,从而很快的解答。
因此,当你看到一个题目,首先你要想到它属于哪一种数据结构或算法,而后要想到这是一个什么类型的问题,而后是此类问题的解决方法。
若是你看到一个新的问题还不能作到上面这样,那说明你对此类题目的掌握程度还不够,你还要多花一些经从来进行练习。
固然,后面我会把我在这部分的总结分享出来,帮助你们少走一些弯路。
关于题目来源,这里我推荐先看《剑指offer
》,而后是leetcode
,《剑指offer
》上能找到很是多的典型题目,这对你发现和总结规律很是重要。看完再去刷leetcode
你会发现更加轻松。
关于难度的选择, 这里我建议leetcode
简单、中等难度便可,由于咱们要作的是寻找规律,即掌握典型题目便可,当你掌握了这些规律,再去解一些hard
的问题,也是能够的,只是多花些时间的问题。切忌不要一开始就在不少刁钻古怪的问题上耗费太多时间。
通过上面的方法,我在练习一段时间后,基本leetcode
中等难度的问题能够在20min
内AC
,另外在最近跳槽的过程当中,基本全部的算法问题我都能很快的手写出来,或者很快的想到解题思路。但愿你们在看到个人经验和方法后也能达到这样的效果,或者作的比我更好。
在开始学习以前,咱们首先要搞懂时间复杂度和空间复杂度的概念,它们的高低共同决定着一段代码质量的好坏:
一个算法的时间复杂度反映了程序运行从开始到结束所须要的时间。把算法中基本操做重复执行的次数(频度)做为算法的时间复杂度。
没有循环语句,记做O(1)
,也称为常数阶。只有一重循环,则算法的基本操做的执行频度与问题规模n呈线性增大关系,记做O(n)
,也叫线性阶。
常见的时间复杂度有:
O(1)
: Constant Complexity: Constant 常数复杂度O(log n)
: Logarithmic Complexity: 对数复杂度O(n)
: Linear Complexity: 线性时间复杂度O(n^2)
: N square Complexity 平⽅方O(n^3)
: N square Complexity ⽴立⽅方O(2^n)
: Exponential Growth 指数O(n!)
: Factorial 阶乘一个程序的空间复杂度是指运行完一个程序所需内存的大小。利用程序的空间复杂度,能够对程序的运行所须要的内存多少有个预先估计。
一个程序执行时除了须要存储空间和存储自己所使用的指令、常数、变量和输入数据外,还须要一些对数据进行操做的工做单元和存储一些为现实计算所需信息的辅助空间。
数据结构这个词相信你们都不陌生,在不少场景下可能都听过,但你有没有考虑过“数据结构”到底是一个什么东西呢?
数据结构即数据元素相互之间存在的一种和多种特定的关系集合。
通常你能够从两个维度来理解它,逻辑结构和存储结构。
简单的来讲逻辑结构就是数据之间的关系,逻辑结构大概统一的能够分红两种:线性结构、非线性结构。
线性结构:是一个有序数据元素的集合。 其中数据元素之间的关系是一对一的关系,即除了第一个和最后一个数据元素以外,其它数据元素都是首尾相接的。
经常使用的线性结构有: 栈,队列,链表,线性表。
—非线性结构:各个数据元素再也不保持在一个线性序列中,每一个数据元素可能与零个或者多个其余数据元素发生联系。
常见的非线性结构有 二维数组,树等。
逻辑结构指的是数据间的关系,而存储结构是逻辑结构用计算机语言的实现。常见的存储结构有顺序存储、链式存储、索引存储以及散列存储。
例如:数组在内存中的位置是连续的,它就属于顺序存储;链表是主动创建数据间的关联关系的,在内存中却不必定是连续的,它属于链式存储;还有顺序和逻辑上都不存在顺序关系,可是你能够经过必定的方式去放问它的哈希表,数据散列存储。
树是用来模拟具备树状结构性质的数据集合。根据它的特性能够分为很是多的种类,对于咱们来说,掌握二叉树这种结构就足够了,它也是树最简单、应用最普遍的种类。
二叉树是一种典型的树树状结构。如它名字所描述的那样,二叉树是每一个节点最多有两个子树的树结构,一般子树被称做“左子树”和“右子树”。
重点中的重点,最好同时掌握递归和非递归版本,递归版本很容易书写,可是真正考察基本功的是非递归版本。
根据前序遍历和中序遍历的特色重建二叉树,逆向思惟,颇有意思的题目
二叉搜索树是特殊的二叉树,考察二叉搜索树的题目通常都是考察二叉搜索树的特性,因此掌握好它的特性很重要。
二叉树的深度为根节点到最远叶子节点的最长路径上的节点数。平衡二叉树:左右子树深度之差大于1
用一组任意存储的单元来存储线性表的数据元素。一个对象存储着自己的值和下一个元素的地址。
链表在开发中也是常常用到的数据结构,React16
的 Fiber Node
链接起来造成的Fiber Tree
, 就是个单链表结构。
主要是对链表基本概念和特性的应用,若是基础概念掌握牢靠,此类问题便可迎刃而解
环类题目即从判断一个单链表是否存在循环而扩展衍生的问题
双指针的思想在链表和数组中的题目都常常会用到,主要是利用两个或多个不一样位置的指针,经过速度和方向的变换解决问题。
对于单链表,由于咱们只能在一个方向上遍历链表,因此第一种情景可能没法工做。然而,第二种情景,也被称为慢指针和快指针技巧,是很是有用的。
双链还有一个引用字段,称为prev
字段。有了这个额外的字段,您就可以知道当前结点的前一个结点。
数组是咱们在开发中最多见到的数据结构了,用于按顺序存储元素的集合。可是元素能够随机存取,由于数组中的每一个元素均可以经过数组索引来识别。插入和删除时要移动后续元素,还要考虑扩容问题,插入慢。
数组与平常的业务开发联系很是紧密,如何巧妙的用好数组是咱们可否开发出高质量代码的关键。
上面链表中提到的一类题目,主要是利用两个或多个不一样位置的指针,经过速度和方向的变换解决问题。注意这种技巧常常在排序数组中使用。
很是常见的问题,基本上都是一个套路,主要考虑如何比暴利法下降时间复杂度,并且也会用到上面的双指针技巧
创建必定的抽象建模能力,将实际中的不少问题进行抽象
数组少不了的就是统计和计算,此类问题考察如何用更高效的方法对数组进行统计计算。
在上面的数组中,咱们能够经过索引随机访问元素,可是在某些状况下,咱们可能要限制数据的访问顺序,因而有了两种限制访问顺序的数据结构:栈(后进后出)、队列(先进先出)
哈希的基本原理是将给定的键值转换为偏移地址来检索记录。
键转换为地址是经过一种关系(公式)来完成的,这就是哈希(散列)函数。
虽然哈希表是一种有效的搜索技术,可是它还有些缺点。两个不一样的关键字,因为哈希函数值相同,于是被映射到同一表位置上。该现象称为冲突。发生冲突的两个关键字称为该哈希函数的同义词。
如何设计哈希函数以及如何避免冲突就是哈希表的常见问题。
好的哈希函数的选择有两条标准:
例以下面的题目:
当用到哈希表时咱们一般是要开辟一个额外空间来记录一些计算过的值,同时咱们又要在下一次计算的过程当中快速检索到它们,例如上面提到的两数之和、三数之和等都利用了这种思想。
堆的底层其实是一棵彻底二叉树,能够用数组实现
堆在处理某些特殊场景时能够大大下降代码的时间复杂度,例如在庞大的数据中找到最大的几个数或者最小的几个数,能够借助堆来完成这个过程。
排序或许是前端接触最多的算法了,不少人的算法之路是从一个冒泡排序开始的,排序的方法有很是多中,它们各自有各自的应用场景和优缺点,这里我推荐以下6种应用最多的排序方法,若是你有兴趣也能够研究下其余几种。
选择一个目标值,比目标值小的放左边,比目标值大的放右边,目标值的位置已排好,将左右两侧再进行快排。
将大序列二分红小序列,将小序列排序后再将排序后的小序列归并成大序列。
每次排序取一个最大或最小的数字放到前面的有序序列中。
将左侧序列当作一个有序序列,每次将一个数字插入该有序序列。插入时,从有序序列最右侧开始比较,若比较的数较大,后移一位。
循环数组,比较当前元素和下一个元素,若是当前元素比下一个元素大,向上冒泡。下一次循环继续上面的操做,不循环已经排序好的数。
建立一个大顶堆,大顶堆的堆顶必定是最大的元素。交换第一个元素和最后一个元素,让剩余的元素继续调整为大顶堆。从后往前以此和第一个元素交换并从新构建,排序完成。
查找是计算机中最基本也是最有用的算法之一。 它描述了在有序集合中搜索特定值的过程。
二分查找维护查找空间的左、右和中间指示符,并比较查找目标或将查找条件应用于集合的中间值;若是条件不知足或值不相等,则清除目标不可能存在的那一半,并在剩下的一半上继续查找,直到成功为止。若是查以空的一半结束,则没法知足条件,而且没法找到目标。
递归是一种解决问题的有效方法,在递归过程当中,函数将自身做为子例程调用。
你可能想知道如何实现调用自身的函数。诀窍在于,每当递归函数调用自身时,它都会将给定的问题拆解为子问题。递归调用继续进行,直到到子问题无需进一步递归就能够解决的地步。
为了确保递归函数不会致使无限循环,它应具备如下属性:
一些问题使用递归考虑,思路是很是清晰的,可是却不推荐使用递归,例以下面的几个问题:
这几个问题使用递归都有一个共同的缺点,那就是包含大量的重复计算,若是递归层次比较深的话,直接会致使JS进程崩溃。
你可使用记忆化
的方法来避免重复计算,即开辟一个额外空间来存储已经计算过的值,可是这样又会浪费必定的内存空间。所以上面的问题通常会使用动态规划求解。
因此,在使用递归以前,必定要判断代码是否含有重复计算,若是有的话,不推荐使用递归。
递归是一种思想,而非一个类型,不少经典算法都是以递归为基础,所以这里就再也不给出更多问题。
广度优先搜索(BFS
)是一种遍历或搜索数据结构(如树或图)的算法,也能够在更抽象的场景中使用。
它的特色是越是接近根结点的结点将越早地遍历。
例如,咱们可使用 BFS
找到从起始结点到目标结点的路径,特别是最短路径。
在BFS
中,结点的处理顺序与它们添加到队列的顺序是彻底相同的顺序,即先进先出,因此广度优先搜索通常使用队列实现。
和广度优先搜索同样,深度优先搜索(DFS
)是用于在树/图中遍历/搜索的一种重要算法。
与 BFS
不一样,更早访问的结点可能不是更靠近根结点的结点。所以,你在DFS
中找到的第一条路径可能不是最短路径。
在DFS
中,结点的处理顺序是彻底相反的顺序,就像它们被添加到栈中同样,它是后进先出。因此深度优先搜索通常使用栈实现。
从解决问题每一步的全部可能选项里系统选择出一个可行的解决方案。
在某一步选择一个选项后,进入下一步,而后面临新的选项。重复选择,直至达到最终状态。
回溯法解决的问题的全部选项能够用树状结构表示。
回溯算法适合由多个步骤组成的问题,而且每一个步骤都有多个选项。
动态规划每每是最能有效考察算法和设计能力的题目类型,面对这类题目最重要的是抓住问题的阶段,了解每一个阶段的状态,从而分析阶段之间的关系转化。
适用于动态规划的问题,须要知足最优子结构和无后效性,动态规划的求解过程,在于找到状态转移方程,进行自底向上的求解。
自底向上的求解,能够帮你省略大量的复杂计算,例如上面的斐波拉契数列,使用递归的话时间复杂度会呈指数型增加,而动态规划则让此算法的时间复杂度保持在O(n)
。
贪心算法:对问题求解的时候,老是作出在当前看来是最好的作法。
适用贪心算法的场景:问题可以分解成子问题来解决,子问题的最优解能递推到最终问题的最优解。这种子问题最优解成为最优子结构
贪心算法与动态规划的不一样在于它对每一个子问题的解决方案都做出选择,不能回退,动态规划则会保存之前的运算结果,并根据之前的结果对当前进行选择,有回退功能,而回溯算法就是大量的重复计算来得到最优解。
有不少算法题目都是能够用这三种思想同时解答的,可是总有一种最适合的解法,这就须要不断的练习和总结来进行深刻的理解才能更好的选择解决办法。
这部分是与前端开发贴近最紧密的一部分了,在写业务代码的同时,咱们也应该关心一些类库或框架的内部实现。
大多数状况下,咱们在写业务的时候不须要手动实现这些轮子,可是它们很是考察一个前端程序员的编码功底,若是你有必定的算法和数据结构基础,不少源码看起来就很是简单。
下面我拣选了一些问题:
本文的部分图片来源于网络,若有侵权,请联系我删除,谢谢。
本文并无对每一个点进行深刻的分析,而是从为何、怎么作、作什么的角度对数据结构和算法进行的全面分析(针对前端角度),但愿看完本片文章能对你有以下帮助:
若是你还想更深刻的学习数据结构和算法,请关注个人后续文章。
推荐个人算法总结:awesome-coding-js
:https://github.com/ConardLi/a...
文中若有错误,欢迎在评论区指正,若是这篇文章帮助到了你,欢迎点赞和关注。
想阅读更多优质文章、可关注个人github博客,你的star✨、点赞和关注是我持续创做的动力!
推荐关注个人微信公众号【code秘密花园】,天天推送高质量文章,咱们一块儿交流成长。