一切的前提——推导大 O 阶
BigO notation :描述算法运行需要多少时间和空间
例如最经典的序列求和:
求 1+2+3+…+n 的值
很简单吧,只需要我们遍历 1 到 n,然后累加,便能得出答案
这段算法用 Big O 描述为: O(n)
n 表示算法里的变量 n
受循环的影响,整个程序的运算时间随着 n 的变大而变大
不信的话,我们来看下这个小算法的运行时间与占用内存
我们可以看到第一次计算花了 62ms,到了第十次居然花了 531ms,尽管在现代计算机中几乎可以忽略掉这几百 ms,但是无数个几百 ms 如果累加在一起那耽误的可不可能被忽略了
像魔笛手的创始者 Pied Piper,当初就是因为创造了一个强大的压缩算法而被美国互利公司以一千万美元收购,尽管 Pied Piper 当时选择了拒绝,但在一个听歌需要等待的时代,在 Pied Piper 强大的压缩算法下几乎没有延迟,就像你用 5G 网络玩游戏突然没了延迟,你还会用回 460ms 的 4G 吗?
做为初学者,在编码时我们应以完美主义的眼光去制作自己的作品,尽管我们能力有限
这便是大 O 推导阶能帮我们的
如果你还不感冒,那他至少可以帮我们看看,我们自己写的程序怎么样子,尽可能地,在程序的运行时间和占用空间上做到“完美”
序列求和的第二种写法:
是不是觉得代码少了很多?
这次我们没用循环,而是直接使用通项公式
此时的算法时间复杂度为:O(1)
为啥?因为我就计算一次,(1+n)*n/2 这套公式我就用一次
Big O 有个特点就是不在乎常量,例如我把(1+n)*n/2 写两次,输出两次,他还是 O(1)
*因为我就算一次*
我们来看下改良后的时间复杂度是多少:
531ms 到 171ms,时间减少了三倍
恐怕没有比这更令人兴奋的事情了吧
书里面还有一大堆的 O(n^m)、O(lnN)等等等,
这些都是常用的
按时间复杂度所耗费的时间从大到小排序依次为:
别看到公式就被吓倒了,例如 O(n^m)不就是嵌套 m 次循环嘛,以编码的角度来理解你就没有什么可害怕的了
时间复杂度还分三种情况 1.最好情况时间复杂度:
顾名思义,看名字你就知道,就是代码执行的次数为一次即为最好的 O(1)。 这是要我们写代码最想要的。但是这是不现实的。
2.最坏情况时间复杂度:
同样的看名字你也可以知道,这是代码执行的总次数很多,每次都要运行 n 次, 所以表示为 O(n)。这是我们写代码最不想要的。当然这也是不现实的。
3.平均时间复杂度: 就是把最好情况时间复杂度和最坏情况时间复杂度求取一平均值, 这是我们写代码最有意义的,因为这是期望的运行时间,所以在写代码时应当考虑这一点。
常见复杂度:
还有一个重要的概念:算法空间复杂度
所谓算法的空间复杂度就是通过计算机算法所需求的存在空间实现。 计算公式可以表示为:S(n) = O( f(n) ),其中,n 为问题的规模, f(n) 为语句关于 n 所占存储空间的函数。
一般情况下,一个程序在机器上运行时,除了考虑到程序的本身运行指令, 常数,变量和输入数据外,还需要考虑存储对数据操作的存储单元.我们在写代码 时完全可以用空间换取时间,两者不存在绝对的好与坏,这么用好二者关系取决于 你用在什么地方。所以,实际情况还是要根据工程代码做最完美的选择。
时间复杂度大 O 表示方法的由来。 大 O 推导的表示方法和常用的大 O 表示法时间复杂度。 时间复杂度的三种情况:最好情况、最坏情况和平均情况。 算法空间复杂度,适当情况可以用空间换取时间。
但不论怎么样,我们学习 Big O 的根本目的就是:用 Big O 方法帮帮助我们从各种编码思路中,找到最合适的那一种方案
数据结构是啥?
还有朋友可能不太理解什么是数据结构,就像喝水,你总是喝到最上面的一层,我称之为“水的栈”这便是一种结构
顾名思义数据结构,便是数据的结构,数据结构并不是什么很高深的东西
起初数据结构并没有一门专门的学科,并不是没有就代表不存在
我问大家一个问题,先有数据结构还是先有的编程?
瑞典计算机科学家 Niklaus Wirth 在 1976 年写了一本书,叫作《Algorithms + Data Structures = Programs》,数据结构的学科便从此发展的不可收拾
要知道早在 1949 年汇编就诞生了
其实深究这个问题是没有任何意义的,如 Niklaus Wirth 的书名:《Algorithms + Data Structures = Programs》
学习数据结构的目的就是为了让我们理解数据在计算机里的存储结构
树?图?栈?
别被这些名词给吓到了,例如"树",你就可以理解数据在计算机中是像树一样的存储,不妨打开你的电脑,随便看一看自己的文件目录,是不是就像一颗树一样
数组
关于数组的知识,我想是我们每个初学者的必学必备
但我想没多少人能真正理解到——数组其实就是一种数据结构
一维数组就是一行数据,n 个数据,二维数组就是 n 行,m 列,n*m 个数据,那三维数组呢?除此之外还有五维、六维。。。。直到内存爆炸,这该怎么描述?所以死记硬背是行不通的,只有了解其底层含义才能通向罗马。
如图:不同维度的数组长这样
很简单是不是?
高纬度嵌套低纬度,同时高纬度等于下一级维度的 2 倍
接下来就是数组优点:
1、按照索引查询元素速度快
例如 j[1]
2、按照索引遍历数组方便
例如 j[i]
缺点:
1、数组的大小固定后就无法扩容了
2、数组只能存储一种类型的数据
3、添加,删除的操作慢,因为要移动其他的元素。
适用场景:
频繁查询,对存储空间要求不大,很少增加和删除的情况。
扩展:
我们也可以
创造一个简单
存储字符串的动态数组
像 List 一样存储数据:
运行结果:
感受到数据结构的魅力了吗?
你所使用的容器,也都是被前辈们这样基于底层创造出来的,当然了,他们更强大
栈
栈的结构如图:
之前我们提到了:
数据结构就像喝水,你总是喝到最上面的一层,我称之为“水的栈”这便是一种结构
所以你可以把栈的结构理解为一杯水
栈顶为杯口,栈底理解为杯底
而水只能从杯口出入,栈内的数据也只能从栈顶提取/输入
不借助任何工具,喝水时你总是喝到离杯口最近的水,往水杯里加水你也不可能从水杯中间加水
栈也是如此,你只能从栈顶按顺序提取数据,所以插入数据你需要将索引位置以上的数据都提取出来,再插入,而后将提取出的数据再依次存入
基本算法
1.进栈(PUSH)算法
① 若 TOP≥n 时,则给出溢出信息,作出错处理(进栈前首先检查栈是否已满,满则溢出;不满则作 ②);
② 置 TOP=TOP+1(栈指针加 1,指向进栈地址);
③S(TOP)=X,结束(X 为新进栈的元素);
2.退栈(POP)算法
① 若 TOP≤0,则给出下溢信息,作出错处理(退栈前先检查是否已为空栈, 空则下溢;不空则作 ②);
②X=S(TOP),(退栈后的元素赋给 X):
③TOP=TOP-1,结束(栈指针减 1,指向栈顶)。
栈常应用于实现递归功能方面的场景,例如斐波那契数列。
好理解吧!
栈就是一种特殊的线性表,仅能在线性表的一端操作,栈顶允许操作,栈底不允许操作
栈的特点是:先进后出,或者说是后进先出,从栈顶放入元素的操作叫入栈,取出元素叫出栈。
队列
队列和栈一样,也是一种线性表
队列的结构长这样:
但也有队列也有和栈的不同之处:
可以简单理解为队列为没有杯底的杯子,一端负责进水,一端负责出水
也就是:先进先出,从一端放入元素的操作称为入队,取出元素为出队
使用场景:因为队列先进先出的特点,在多线程阻塞队列管理中非常适用
链表
把一堆扑克牌丢地上,反面朝上,你能找到,哪张是梅花 A 吗?
如果正面朝上呢?
上面的例子很清晰的描述了链表的特点
链表是一种物理存储结构上非连续,非顺序的存储结构
数据元素的逻辑顺序是通过链表中的指针链接次序实现的
指针指向的就是数据的地址
反面朝上的扑克牌代表内存里的数据,如果正面朝上就代表我们赋予其地址,根据地址来操纵数据
此时你还能找到梅花 A 吗?
答:能!
*p = ♣A
所以链表的结构长这样:
无头单向非循环列表:结构简单,一般不会单独用来存放数据。实际中更多是作为其他数据结构的子结构,比如说哈希桶等等
带头双向循环链表:结构最复杂,一般单独存储数据。实际中经常使用的链表数据结构,都是带头双向循环链表。这个结构虽然复杂,但是使用代码实现后会发现这个结构会带来很多优势,实现反而简单了
单链表和双链表的区别:
单链表的每一个节点中只有指向下一个结点的指针,不能进行回溯,适用于节点的增加和删除。
双链表的每一个节点给中既有指向下一个结点的指针,也有指向上一个结点的指针,可以快速的找到当前节点的前一个节点,适用于需要双向查找节点值的情况
为啥可以这么多样式?
因为指针就是这么方便!
话说。。有没有感觉和数组有点像了?
链表和数组的区别:
数组静态分配内存,链表动态分配内存
数组在内存中是连续的,链表是不连续的
数组利用下标定位,查找的时间复杂度是 O(1),链表通过遍历定位元素,查找的时间复杂度是 O(N)
数组插入和删除需要移动其他元素,时间复杂度是 O(N),链表的插入或删除不需要移动其他元素,时间复杂度是 O(1)
稍微分析下是不是就会发现,各有各的优缺点?
所以存在即合理,每一种数据结构都有它的特点,并不是哪种好,哪种坏
树
直接放图:
还有比这更容易理解的了吗?
如果不理解就把"A"节点当作爷爷,“D”节点当作父亲,"H"节点当作小孩
树是一种数据结构
它是由 n(n>=1)个有限节点组成一个具有层次关系的集合。把它叫做 “树” 是因为它看起来像一棵倒挂的树,也就是说它是根朝上,而叶朝下的。它具有以下的特点:
每个节点有零个或多个子节点;
没有父节点的节点称为根节点;
每一个非根节点有且只有一个父节点;
除了根节点外,每个子节点可以分为多个不相交的子树;
在日常的应用中,我们讨论和用的更多的是树的其中一种结构,就是
二叉树
为啥?
因为二叉树十分强大
二叉树是树的特殊一种,具有如下特点:
1、每个结点最多有两颗子树,结点的度最大为 2。
2、左子树和右子树是有顺序的,次序不能颠倒。
3、即使某结点只有一个子树,也要区分左右子树。
二叉树是一种比较有用的折中方案,它添加,删除元素都很快,并且在查找方面也有很多的算法优化,所以,二叉树既有链表的好处,也有数组的好处,是两者的优化方案,在处理大批量的动态数据方面非常有用。
扩展:
二叉树有很多扩展的数据结构,包括:
平衡二叉树、红黑树、B+树等
这些数据结构二叉树的基础上衍生了很多的功能
在实际应用中广泛用到,例如:
mysql的数据库索引结构用的就是B+树;还有HashMap的底层源码中用到了红黑树
这些二叉树的功能强大,但算法上比较复杂, 我还需要花时间去深入
哈希表
Hash table = 散列表
这两是一个东西
先看百度百科怎么说:
散列表(Hash table,也叫哈希表),是根据键(Key)而直接访问在内存存储位置的数据结构。也就是说,它通过计算一个关于键值的函数,将所需查询的数据映射到表中一个位置来访问记录,这加快了查找速度。这个映射函数称做散列函数,存放记录的数组称做散列表
哈希表其实本质是数组
这要说到 Hash table 的两种实现方法:
1、数组+链表
2、数组+二叉树
区别在于:
数组中一般就是存放的单一的数据 而哈希表中存放的是一个键值
结构示例图:
右边是数组
数组的每个成员包括一个指针,指向一个链表的头,当然这个链表可能为空,也可能元素很多
我们可以根据元素的一些特征把元素分配到不同的链表中去,也是根据这些特征,找到正确的链表,再从链表中找出这个元素
是不是感觉特别强大?
Hash Table 的查询速度非常的快,几乎是 O(1)的时间复杂度。
Java 中有些集合类就是借鉴了哈希原理构造的,例如 HashMap,HashTable 等
hash 就是找到一种数据内容和数据存放地址之间的映射关系
数据结构的魅力这不就来了吗?
讲到这里,就必须提到 Hash table 的应用了:
Hash 的应用
Hash 主要用于信息安全领域中加密算法,它把一些不同长度的信息转化成杂乱的 128 位的编码,这些编码值叫做 Hash 值.
也可以说,Hash 就是找到一种数据内容和数据存放地址之间的映射关系
查找:哈希表,又称为散列,是一种更加快捷的查找技术。我们之前的查找,都是这样一种思路:集合中拿出来一个元素,看看是否与我们要找的相等,如果不等,缩小范围,继续查找。而哈希表是完全另外一种思路:当我知道 key 值以后,我就可以直接计算出这个元素在集合中的位置,根本不需要一次又一次的查找!
举一个例子,假如我的数组 A 中,第 i 个元素里面装的 key 就是 i,那么数字 3 肯定是在第 3 个位置,数字 10 肯定是在第 10 个位置。哈希表就是利用利用这种基本的思想,建立一个从 key 到位置的函数,然后进行直接计算查找。
Hash 表在海量数据处理中有着广泛应用
哈希表的应用场景很多,当然也有很多问题要考虑,比如哈希冲突的问题,如果处理的不好会浪费大量的时间,导致应用崩溃
堆
谈到堆,又得扯到数组身上了
例如现在数组长这样:
通常用数组来存储堆中的元素,但是我们却可以把数组中元素视为树:
原来虽然我们是在数组中存储的堆元素,但是这里面有一条隐藏的规律,如果你仔细看上图就会发现:
每一个左子树节点的下标是父节点的 2 倍
每一个右子树节点的下标是父节点的 2 倍再加 1
数组中实际上隐藏了上面的这两条规律
早说了数据结构就是数据的存储规律吧
堆这种数据结构最棒的地方在于我们无需像树一样存储左右子树的信息
只需要通过下标运算就可以轻松的找到一个节点的左子树节点、右子树节点以及父节点
相对于树这种数据结构来说堆更加节省内存
你还可以这样理解堆:
将根节点最大的堆叫做最大堆或大根堆
根节点最小的堆叫做最小堆或小根堆
常见的堆有二叉堆、斐波那契堆等
因为堆有序的特点,一般用来做数组中的排序,称为堆排序
图
图的结构长这样:
节点可以具有零个或多个相邻元素
两个节点之间的连接称为边。
节点也可以称为顶点。
图的应用
例如广度优先搜索(BFS)-图形搜索
深度优先搜索(DFS)-图形搜索
给大家推荐一篇文章:
《Graph Data Structures in JavaScript for Beginners》
作者用 Javascript 实现图形数据结构,通俗易懂
截图: