最近面试了一些公司,感触良多,从本科到现在,也接触 面试了非常多的公司,从早期热门的svm白板推导、逻辑回归的伪/工程代码手写到后来的实战应用的各种各样的技巧和经验再到现在的考核python语言内核机制与基本的数据结构与算法的掌握程度,业界对于算法工程师的要求可以说是越来越偏向“算法开发”——既要熟悉各类算法的基本原理,紧跟技术前沿,又要有丰富的实战经验,知道不同的数据场景下能够使用哪些合理的算法来处理,同时,还要有过硬的工程能力能够实现一些未开源的算法或者是针对开源算法的缺陷——比如性能不佳、应用场景较窄、难以满足场景化的需求等等。
个人认为,工程能力是最重要的,你可以不懂transformer在序列数据上的抽取,可以不了解GNN是如何针对复杂的异构图进行embedding,但是一定要熟练的掌握numpy的数据处理,pandas的groupby的内置函数与自定义函数的性能差异,list的append为何会随着循环的数量越来越慢,内存占用越来越大,毕竟大部分算法工程师面对的是实际的应用场景,落地,说白了就是要写一大堆针对性的功能代码,那么代码的性能和逻辑就非常重要了,我看过很多人写的python代码——能用矩阵乘的情况下用大量的循环代替,循环运行的过程中定义了太多不必要的临时变量。。。。。总的来说就是单纯以实现功能为目标而很少甚至不考虑整个过程的时间复杂度和空间复杂度,包括我本人也存在这样的问题,理论研究水平还行,应用经验一般,工程能力太弱,所以,在明年毕业之前,打算好好打几场比赛,然后认认真真的把cpython的internals研究清楚,把传统的数据结构与算法好好的复习一遍;
首先,需要知道的是,python是一门解释型、面向对象、动态类型的语言;
首先是解释型语言和编译型语言:
什么是编译型语言和解释型语言?www.jianshu.com相对于编译型语言存在的,源代码不是直接翻译成机器语言,而是先翻译成中间代码,再由解释器对中间代码进行解释运行。比如Python/JavaScript / Perl /Shell等都是解释型语言。
解释型语言:程序不需要编译,程序在运行时才翻译成机器语言,每执 行一次都要翻译一次。因此效率比较低。比如Basic语言,专门有一个解释器能够直接执行Basic程 序,每个语句都是执行的时候才翻译。(在运行程序的时候才翻译,专门有一个解释器去进行翻译,每个语句都是执行的时候才翻译。效率比较低,依赖解释器,跨 平台性好.)
计算机不能直接理解高级语言,只能直接理解机器语言,所以必须要把高级语言翻译成机器语言,计算机才能执行高级语言编写的程序。
一个是编译,一个是解释。两种方式只是翻译的时间不同。编译型语言写的程序执行之前,需要一个专门的编译过程,把程序编译成为机器语言的文件,比如exe文件,以后要运行的话就不用重新翻译了,直接使用编译的结果就行了(exe文件),因为翻译只做了一次,运行时不需要翻译,所以编译型语言的程序执行效率高,但也不能一概而论,部分解释型语言的解释器通过在运行时动态优化代码,甚至能够使解释型语言的性能超过编译型语言。
解释则不同,解释性语言的程序不需要编译,省了道工序,解释性语言在运行程序的时候才翻译,比如解释性basic语言,专门有一个解释器能够直接执行basic程序,每个语句都是执行的时候才翻译。这样解释性语言每执行一次就要翻译一次,效率比较低。解释是一句一句的翻译。
下面的这幅图来帮助进一步理解:
python的解释器包括了cpython、jython、pypy等等,目前我们市面上的各类python代码和应用都是使用cpython的解释器,也就是底层用C来实现的python的解释器;
关于python的基础知识,看完《python源代码剖析》和github上的cpython internals之后再总结吧。。。
很多材料或者书都不加区分的将数据结构与算法混在一起讲,这很容易产生误导作用,实际上,数据结构和算法是两个完全独立的学科,实际上二者是互利共赢的关系,举个例子,比如我们要解决某个问题:
1. 分析问题,从问题中提取出有价值的数据,将其存储;
2. 对存储的数据进行处理,最终得出问题的答案;
因此我们得出这样的结论,数据结构用于解决数据存储问题,而算法用于处理和分析数据,它们是完全不同的两类学科。
从分析问题的角度来说,每个问题都可以笼统分解为如下两个步骤:
1、分析问题,从问题中提取有价值的数据,选择合适的数据结构进行存储,这就涉及到数据结构的理论知识;
2、对存储的数据进行处理,最终得到问题的答案,这就涉及到算法(包括了传统的简单算法和机器学习、深度学习等算法这类更加广义的概念)
最优的存储结构来存储数据,而算法也要结合数据存储的特点,用最优的策略来分析并处理数据,由此可以最高效地解决问题。
数据结构,
数据的存储方式从大的层面上分为:
1、线性表:例如数组、链表等;
2、树:例如二叉树,多叉树;
3、图
同时,不同的存储方式又有顺序存储和链式存储 两种不同的种类;
数据结构整体上可以从两个角度来理解,一个是数据的逻辑结构,一个是数据的物理结构;
我们平常在leetcode接触的各种概念,队列、数组、二叉树等等,实际上都属于数据的逻辑结构,就是数据之间的逻辑关系,
逻辑结构的存在使得我们对于数据整体有一个比较直观清晰的感受,但是需要注意,逻辑结构和物理结构是不太一样的,因为我们最终是要将逻辑结构转化为物理结构然后在真实的计算机环境中实现存储的。例如上述的二叉树形式的数据逻辑结构,我们在实际的过程中可能就需要:
1、保存所有的家庭成员;
2、保存家庭成员之间的关系
数据的逻辑结构大体上分为:
1、一对一 :对应线性表
2、一对多:对应树
3、多对多:对应图
而数据的物理结构相对好理解,包括了:
1、连续存储,典型的连续存储的例子就是数组了,其底层实现使用的是一大块连续的内存空间进行存储;
2、分散存储,例如链表,不需要连续的内存空间,随机进行存储,占用的零散的内存空间
一般来说,连续存储非常方便于进行遍历,因为内存是连续的,只要按照顺序连续读取就可以,但是其更新或者删除比较麻烦,比如我们要对数组中间的部分插入新数据,就需要对插入部分后面的内存进行大量移动,而分散存储则没有这种问题,但是分散存储的遍历比较麻烦(这个地方需要进一步的研究???);
顺序表与数组:
顺序表常常通过数组实现,但是数组的作用远远不止是用于构造顺序表,需要注意,顺序表实际上是数据结构中的逻辑结构,而其物理结构是数组!
数组是在程序设计中,为了处理方便, 把具有相同类型的若干元素按有序的形式组织起来的一种形式。
首先,数组会利用 索引 来记录每个元素在数组中的位置,且在大多数编程语言中,索引是从 0 算起的。我们可以根据数组中的索引,快速访问数组中的元素。事实上,这里的索引其实就是内存地址。
其次,作为线性表的实现方式之一,数组中的元素在内存中是 连续 存储的,且每个元素占用相同大小的内存。
例如对于一个数组 ['oranges', 'apples', 'bananas', 'pears', 'tomatoes'],为了方便起见,我们假设每个元素只占用一个字节,它的索引与内存地址的关系如下图所示。
在具体的编程语言中,数组的实现方式具有一定差别。比如 C++ 和 Java 中,数组中的元素类型必须保持一致,而 Python 中则可以不同。相比之下,Python 中的数组(称为 list)具有更多的高级功能。
关于线性表和数组的关系,我们可以这么理解:
一维数组结构是线性表的基本表现形式,而 n 维数组可理解为是对线性存储结构的一种扩展。
需要注意的是,顺序表、链表、栈和队列存储的都是不可再分的数据元素(如数字 5、字符 'a' 等),而数组既可以用来存储不可再分的数据元素,也可以用来存储像顺序表、链表这样的数据结构。
因此,实际上数组的概念更加接近于python中的list,list可以存放不同类型的元素并且更重要的是,list可以存放大量可再分的数据元素例如一个模型model,一个矩阵甚至是另一个list,(不过实际上上述的这种描述是属于广义表的范畴);
数组作为一种线性存储结构,对存储的数据通常只做查找和修改操作,因此数组结构的实现使用的是顺序存储结构。要知道,对数组中存储的数据做插入和删除操作,算法的效率是很差的。
链表:
我们前面说过,顺序表通过数组来实现,其内存空间是连续的,而链表则不同,链表的内存空间是随机不连续的;
python本身是没有具体的实现链表的数据结构的(也可能我孤陋寡闻不知道。。),具体的python实现链表可见:
木头人:Python 数据结构之链表zhuanlan.zhihu.com链表(Linked List)是一种常见的基础数据结构,是一种线性表,但是并不会按线性的顺序存储数据,而是在每一个节点里存到下一个节点的指针(Pointer)。
由于不必须按顺序存储,链表在插入的时候可以达到 O(1)的复杂度,比另一种线性表 —— 顺序表(数组)快得多,但是查找一个节点或者访问特定编号的节点则需要 O(n) 的时间,而顺序表相应的时间复杂度分别是O(log n) 和 O(1)。
使用链表结构可以克服数组链表需要预先知道数据大小的缺点,链表结构可以充分利用计算机内存空间,实现灵活的内存动态管理。但是链表失去了数组随机读取的优点,同时链表由于增加了结点的指针域,空间开销比较大。
在计算机科学中,链表作为一种基础的数据结构可以用来生成其它类型的数据结构。链表通常由一连串节点组成,每个节点包含任意的实例数据(data fields)和一或两个用来指向上一个/或下一个节点的位置的链接(links)。链表最明显的好处就是,常规数组排列关联项目的方式可能不同于这些数据项目在记忆体或磁盘上顺序,数据的访问往往要在不同的排列顺序中转换。而链表是一种自我指示数据类型,因为它包含指向另一个相同类型的数据的指针(链接)。
链表允许插入和移除表上任意位置上的节点(删除a节点后,改变a节点的前一个节点指向它的指针,直接指向a节点的后一个节点的地址即可),但是不允许随机存取。链表有很多种不同的类型:单向链表,双向链表以及循环链表。
链表通常可以衍生出循环链表,静态链表,双链表等。对于链表使用,需要注意头结点的使用。
总结
1.数组
像军队,有序存储,占据一片连续内存
用下标查询方便,插入删除麻烦,适合多读少写
2.链表
像地下党,无序存储,在内存见缝插针
查询麻烦,需要从头开始依次查找;插入删除方便,适合少读多写
栈:
栈(Stack)又名堆栈,它是一种重要的数据结构。从数据结构角度看,栈也是线性表,其特殊性在于栈的基本操作是线性表操作的子集,它是操作受限的线性表,因此,可称为限定性的数据结构。限定它仅在表尾进行插入或删除操作(先进后出)。表尾称为栈顶,相应地,表头称为栈底。栈的基本操作除了在栈顶进行插入和删除外,还有栈的初始化,判空以及取栈顶元素等。
栈结构如图 3 所示,像一个木桶,栈中含有 3 个元素,分别是 A、B 和 C,从在栈中的状态可以看出 A 最先进的栈,然后 B 进栈,最后 C 进栈。根据“先进后出”的原则,3 个元素出栈的顺序应该是:C 最先出栈,然后 B 出栈,最后才是 A 出栈。
队列:
队列(Queue)是一种先进先出(FIFO,First-In-First-Out)的线性表。
在具体应用中通常用链表或者数组来实现。队列只允许在后端(称为 rear)进行插入操作,在前端(称为 front)进行删除操作。
队列的操作方式和堆栈类似,唯一的区别在于队列只允许新数据在后端进行添加(先进先出)。
队列常用的方法有:add、remove、element、offer、poll、peek、put、take。
字符串:
字符串是由零个或多个字符组成的有限序列。一般记为 s = 'a1a2a3a4.....'
。它是编程语言中表示文本的数据类型。字符串与数组有很多相似之处,比如使用 名称[下标] 来得到一个字符。然而,字符串有其鲜明的特点,即结构相对简单,但规模可能是庞大的。
以生物中的 DNA 序列为例,假设一个 DNA 序列为 "GCCGTAATATCG...",在人体中,该序列的长度可能会达到 n*10^8,然而,构成序列的基本碱基种类只有 "A"
, "T"
, "G"
, "C"
4 种。
这里我们可将"A"
,"T"
,"G"
,"C"
看作一个字符集,由字符集中的一些字符组合而成的 DNA 序列可以看作一个字符串。
在编程语言中,字符串往往由特定字符集内有限的字符组合而成,根据其特点,对字符串的 操作 可以归结为以下几类:
1、不同字符串之间的比较、连接操作(不同编程语言实现方式有所不同);
2、单一字符串涉及子串的操作,比如前缀,后缀等;
3、不同字符串之间的匹配操作,如 KMP 算法、BM 算法等。
哈希表
哈希表(Hash Table,也叫散列表),是根据键(Key)而直接访问在内存存储位置的数据结构。也就是说,它通过计算一个关于键值的函数,将所需查询的数据映射到表中一个位置来访问记录,这加快了查找速度。这个映射函数称做哈希函数,存放记录的数组称做哈希表。
一个通俗的例子是,为了查找电话簿中某人的号码,可以创建一个按照人名首字母顺序排列的表(即建立人名 x 到首字母 F(x) 的一个函数关系),在首字母为 W的表中查找 “王” 姓的电话号码,显然比直接查找就要快得多。这里使用人名作为关键字,“取首字母” 是这个例子中哈希函数的函数法则 F(X),存放首字母的表对应哈希表。关键字和函数法则理论上可以任意确定。
哈希表是使用 O(1) 时间进行数据的插入删除和查找,但是哈希表不保证表中数据的有序性,这样在哈希表中查找最大数据或者最小数据的时间是 O(N) 实现。
需要注意的是,数组可以实现简易hash表的功能,数组的索引为hash表的key,而内容为hash表的key对应的内容;
树是一种抽象数据类型(ADT)或是实现这种抽象数据类型的数据结构,用来模拟具有树状结构性质的数据集合。它是由 n(n>0)n(n>0) 个有限节点组成一个具有层次关系的集合。
把它叫做「树」是因为它看起来像一棵倒挂的树,也就是说它是根朝上,而叶朝下的。
它具有以下的特点:
1、每个节点都只有有限个子节点或无子节点;
2、没有父节点的节点称为根节点;
3、每一个非根节点有且只有一个父节点;
4、除了根节点外,每个子节点可以分为多个不相交的子树;
5、树里面没有环路。
注意,树是一种逻辑结构不是物理结构。
二叉树:
二叉树的概念很好理解,看图基本上就知道了。。。这里就不详细介绍了;
二叉树具有以下几个性质:
满二叉树
如果一棵二叉树的结点要么是叶子结点,要么它有两个子结点,这样的树就是满二叉树。
满二叉树除了满足普通二叉树的性质,还具有以下性质:
完全二叉树:
如果二叉树中除去最后一层节点为满二叉树,且最后一层的结点依次从左到右分布,则此二叉树被称为完全二叉树。因此,满二叉树实际上是二叉树的一种特殊形式;
如图 3a) 所示是一棵完全二叉树,图 3b) 由于最后一层的节点没有按照从左向右分布,因此只能算作是普通的二叉树。
完全二叉树的性质:
完全二叉树除了具有普通二叉树的性质,它自身也具有一些独特的性质,比如说,n 个结点的完全二叉树的深度为 ⌊log2n⌋+1。
⌊log2n⌋ 表示取小于 log2n 的最大整数。例如,⌊log24⌋ = 2,而 ⌊log25⌋ 结果也是 2。
对于任意一个完全二叉树来说,如果将含有的结点按照层次从左到右依次标号(如图 3a)),对于任意一个结点 i ,完全二叉树还有以下几个结论成立:
堆:
堆就是用数组实现的普通二叉树,
堆的常用方法:
堆分为两种:最大堆和最小堆,两者的差别在于节点的排序方式。
在最大堆中,父节点的值比每一个子节点的值都要大。在最小堆中,父节点的值比每一个子节点的值都要小。这就是所谓的“堆属性”,并且这个属性对堆中的每一个节点都成立。
例子:
这是一个最大堆,,因为每一个父节点的值都比其子节点要大。10
比 7
和 2
都大。7
比 5
和 1
都大。
堆常见的操作:
HEAPIFY 建堆:把一个乱序的数组变成堆结构的数组,时间复杂度为 O(n)O(n)。
HEAPPUSH:把一个数值放进已经是堆结构的数组中,并保持堆结构,时间复杂度为 O(log n)O(log n)。
HEAPPOP:从最大堆中取出最大值或从最小堆中取出最小值,并将剩余的数组保持堆结构,时间复杂度为 O(log n)O(log n)。
HEAPSORT:借由 HEAPFY 建堆和 HEAPPOP 堆数组进行排序,时间复杂度为 O(n log n)O(n log n),空间复杂度为 O(1)O(1)。
堆结构的一个常见应用是建立优先队列(Priority Queue)。
二叉搜索树:
二叉查找树(英语:Binary Search Tree),也称为 二叉搜索树、有序二叉树(Ordered Binary Tree)或排序二叉树(Sorted Binary Tree),是指一棵空树或者具有下列性质的二叉树:
若任意节点的左子树不空,则左子树上所有节点的值均小于它的根节点的值;
若任意节点的右子树不空,则右子树上所有节点的值均大于它的根节点的值;
任意节点的左、右子树也分别为二叉查找树;
没有键值相等的节点。
二叉查找树相比于其他数据结构的优势在于查找、插入的时间复杂度较低。为 O(log n)O(logn)。二叉查找树是基础性数据结构,用于构建更为抽象的数据结构,如集合、多重集、关联数组等。
二叉查找树的查找过程和次优二叉树类似,通常采取二叉链表作为二叉查找树的存储结构。中序遍历二叉查找树可得到一个关键字的有序序列,一个无序序列可以通过构造一棵二叉查找树变成一个有序序列,构造树的过程即为对无序序列进行查找的过程。每次插入的新的结点都是二叉查找树上新的叶子结点,在进行插入操作时,不必移动其它结点,只需改动某个结点的指针,由空变为非空即可。搜索、插入、删除的复杂度等于树高,期望 O(logn),最坏 O(n)(数列有序,树退化成线性表)。
虽然二叉查找树的最坏效率是 O(n),但它支持动态查询,且有很多改进版的二叉查找树可以使树高为 O(logn),从而将最坏效率降至O(logn),如 AVL 树、红黑树等。
参考资料来源:
数据结构树(Tree)详解data.biancheng.net 力扣leetcode-cn.com