算法与数据结构学习预储备

很遗憾还没来得及系统的学习数据结构,为了更快的融入互联网公司实习和秋招,记录一点对数据结构宏观上的理解和初步学习心得。

——————————————-

机会总是稍纵即逝,请你时刻准备着! ——EX FOR TX GO FOR IT!

——————————————-

从C语言的数组谈起

至今只是对C语言应用的比较多,认识的比较深刻,所以从数组出发来理解数据结构。

对于计算机的学习,我们最熟悉也是最基本的数据结构就是数组了。当然,更小的是一个字节,一个位的存储单元,但是这个时候就谈不上结构了。

其实我们的软件设计和开发的逻辑本质上也就是针对数据结构的一系列操作。但是再复杂的数据结构,都可以看成是基于数组演化而来的。数组可以被规约为一组有连续下标(编号)的盒子,编号大于等于零。那么针对数组的基本操作就有检索(Search)、修改(modify)、添加(Append)、插入(Insert)、删除(delete)等。

最基本的操作——检索
为什么说检索是最基本的操作呢?
第一,你无论是要查找修改还是要插入删除,你都得先找到它的位置;
第二,程序逻辑进行正确的操作,首先要确保的是对正确位置的数据进行操作。
对于数组的检索方式,有按下标检索和按值检索这两种方式。
对应的时间复杂度分别是:

  • 按照下标检索=O(1)
  • 按照值检索=O(n),n为数组长度。

对于下标检索很简单,告诉我下标,找到具体的内容,一步到位。
对于值检索,当你不知道具体在数组的哪个位置的时候,可能就需要一个一个取比对了。但是当数组为一个有序数组的时候,我们检索的速度就会快很多。利用我们数学里学的二分法,先比对中间的数,一步一步比下去,相对于盲目的一个一个比对,整体效率提高了很多。也就是说,时间复杂度从O(n),变成了O(log2n)。

上面说的二分查找也就是折半查找,所以log的底就是2。可能你会想log4,log10不是更小吗,也就是底数为什么选择2,为什么要从中间开始找呢?其实道理很简单,折半查找时目标要么在左侧要么在右侧,左右两边相等,所以无论在左侧还是右侧,算法都所消耗的比较次数都相等;但是如果以更高数为底(比如10),运气不好的话目标值总是落到十分之九那一侧,算法就会恶化,所以选择最平衡的2分法是稳妥高效的。

于是有序数组的按值检索时间复杂度=O(logn),对于按值检索来说,折半查找是目前已知的稳定的最快的检索算法了。

——————————————-

那我们如何让数组变得有序呢

数组的排序

选择排序

对一个乱序的数组进行排序,最简单的想法就是选择排序了。
我们先将第一个记为最小的,然后比对每一个,将找到的最小的数放到第一个(也可以创建一个新的数组来存放),再找第二小放到第二个,重复操作,最后实现数组有序。

对于选择排序,它的时间复杂度是O(n^2)。我们可以这样理解,把选择排序看成是对无序数组进行的n次检索。因为对无序数组按值检索的时间复杂度为O(n),所以选择排序的时间复杂度就可以近似的等于n*O(n)=O(n^2)。显然,随着n的增长,它的时间复杂度越来越大,故这种排序方法是不可取的。

选择排序的优化

选择排序看成是对无序数组进行的n次检索。那么很自然的,我们会想到将无序检索变为有序检索。同样的,我们创建一个空的数组,并且从无序数组中取得一个值放入空数组中,当我们取第二个值放入空数组中时根据与第一个值比较的大小,选择将这个值插入到第一个值得前面或后面,这样就形成了一个有序的数组;依次类推,每次取得一个值都通过折半查找的方式在有序数组中找到合适的位置插入,最终完成排序。假设我们忽略数组插入的损耗,那么由于有序数组的时间复杂度为O(logn),最终这种算法的时间复杂度就近似的等于n* O(logn)= O(n*logn)。

由于O(logn)是目前已知的稳定的最快的按值检索的算法,所以无论任何一种排序的变种,只要是按照内容(值)进行比较的,都不可能比这个更能同时满足快和稳定了。唯一能够缩减的,就是我们上面忽略的插入损耗,这也是所有按值排序变种所优化的地方。

——————————————-

基于二分查找对各排序方法的理解

快速排序(Quicksort)

快速排序的基本思路是:将数组分成左右两半,让左边的数都比右边的数小;然后再次对左右两侧折半,直到完成。

百度百科说快速排序是冒泡的变种,其实我觉得这是典型的二分法的变种。只不过,由于快排的二分策略是基于某一个随机选定的数值的(a[0]),所以不一定能够恰巧选择一个中间值,因此快排是有可能恶化的,但也正是由于这一点使得log的底可以大于2,所以快排有可能比二分更快。关于快排的具体算法——快速排序

归并排序(Mergesort)

归并排序的基本思路是:将两个有序数组合并成一个有序数组,从无序数组的初始态开始,每一个元素都可以看作一个有序数组,所以第一次合并近似形成n/2个拥有两个元素的有序数组,第二次合并近似形成n/4个拥有4个元素的有序数组,依次类推最终得到排好序的数组。

这个过程实际上是个颠倒的二分查找过程,我们习惯于说这是通过分治法的原则来解决问题,实际上二分查找也算分治法的一种实现。

关于归并排序的时间复杂度,一个颠倒的二分查找是O(log2n),每次的合并需要比较最多数组的大小n次,所以归并排序的时间复杂度依然是O(n*logn),我们前面说过这是不可能突破的物理极限了。由于归并算法是标准的折半,所以其稳定度是有保障的。这种算法的好处是通过增加辅助的空间,来降低数组的插入损耗。

——————————————-

基于数组对各数据结构的理解

在继续探讨更多排序算法之前,我们不得不停下来引入一些新的数据结构,因为数组这个结构实在是太基础了,不能够承载太多的算法。

堆和栈

堆和栈是数组最简单的变种,我相信堆先进先出和栈先进后出的概念已经融入程序员的血液之中了,我这里就不说了。

链表

链表是数组的一种非常极端的变种。链表要解决的是数组的插入损耗问题,当我们完全不需要在数组中进行检索的时候,插入损耗成为最大的消耗。于是,我们将数组拆成单一元素的数组,然后用头尾两个指针进行串接,就形成了链表。

链表的最大好处就在于,插入或删除一个元素的时候,比数组快n倍是O(1)。其坏处是,无法用折半的方式进行检索。

哈希表

哈希表是数组的一个非常重要的变种。我们知道数组最快的检索方式是按照下标检索,其时间复杂度是O(1),那么哈希表就是这么一个天才的设计,他能够将按值检索的方式转变成按照下标检索。

我们知道,当数组的内容是纯粹的数字时,理论上我们可以将内容作为下标,这样的话我们就可以用内容作为下标直接检索到相应的内容了,当然这可能没有任何实际意义。哈希表将存储的内容通过一个精心挑选的计算方法,变换成为一个有限的数字,然后用这个数字作为下标来存储相应的内容,使得检索的时间复杂度降为O(1)。所付出的代价就是,可能会有大量的被浪费的存储空间,这是典型的用空间换取时间的方法。

当然,哈希表还有其他问题,比如冲突,当两个或以上的内容计算的哈希值相同的时候,冲突就产生了,这种情况,通常的做法是使用链表来存储冲突的内容。

树和二叉树

树是链表和数组的结合体,与链表的差异是,树不会将数组拆分成单一元素,而是拆分成不同大小的数组,有些元素内部会有一个指针指向被拆分出来的某一个子数组,这样就形成了父子关系。

二叉树是一种特殊的树,树上每一个父节点最多有两个孩子,看到这里我们很容易的将二叉树和折半联系到一起,事实上二叉树就是为了实现更高效的折半算法而生的。

二叉排序树

二叉排序树是一棵用二叉树的形式表达的有序数组,其检索的过程与用折半法检索数组几乎相同。二叉排序树的特点是左侧子节点一定小于右侧子节点,左侧的孩子一定小于父亲,右侧的孩子一定大于父亲。比如一个1~15的顺序数字组成的数组,按照折半检索形成的二叉排序树如下:
二叉排序树的特点是,既有有序数组可以折半检索的优势,同时又具有链表的很低的插入损耗。缺点是当随着插入数据的增多,树的左右分支数量会出现不平衡,严重的不平衡就会导致检索效率的降低,因此需要根据一定的阀值来决定是否重构一棵平衡的二叉树。

一个平衡的二叉排序树的检索时间复杂度为O(logn),插入是O(1)。

——————————————-

基于复杂数据结构的排序算法

堆排序(Heapsort)

最常用的堆排序是二叉堆排序,二叉堆排序的原理是这样的:

将一个无序的数组,任意的形成一棵二叉树。

使二叉树的每一个节点,都比它的子节点要小(或者大),这样的二叉树命名为最小二堆(或最大堆)。

取出二叉树的根节点(最小或最大值)放入空数组的第一位,然后重构剩余部分为新的最小(最大)二叉树。

重复第三步,直到最后一个元素。

上述是思路原理,真实的堆排序是不需要构造一棵实际存在的二叉树的,只需要在原本的数组中虚拟出一棵二叉树就行了。

由于使用了折半的方法,所以堆排序的时间复杂度也是O(n*logn)。与快排类似的,由于堆排序不是真正的折半,所以存在不稳定性。

桶排序(Bucketsort)

桶排序与上述排序的原理都不同,它并不是基于内容比较的排序方法,桶排序是利用类似哈希表的下标检索的方式进行检索排序的。

基本思路是这样的:

假设有无序数组中有n个数,找到一种哈希算法,使得计算结果保持原有数据的大小顺序(例如,35和67这两个数经过计算后得到3和6,不改变35和67的大小顺序)。

如果产生冲突则冲突的数据在链表中顺序排列。

按数组的方式从小坐标到最大坐标遍历哈希表,抛弃空的位置,将所有有数据的位置依次放入目标数组,即得到排好序的数组了。

桶排序的时间复杂度为O(k),其中k>n,k值的大小与选择的哈希算法有关。桶排序是一种线性排序算法,其缺点就是无法通用的解决所有数据集合的排序,针对特定的数据集合需要特定的哈希算法才能达到最佳的空间利用率。

——————————————-

总结

最基本的思考逻辑是将复杂问题简单化。

对典型的数据结构和算法的进行分析,来阐述如何从最基本的逻辑单元逐步演化成复杂的算法和数据结构,建立在最基本的数据结构之上的算法才是所有复杂算法的基础。

数据结构与算法正在学习中,新的心得体会待记录….


以下内容整理自网络


————————————————————————–

初学者 I

首先,数据结构是一门计算机语言学的基础学科,它不属于任何一门语言,其体现的是几乎所有标准语言的算法的思想。

上面的概念有一些模糊,我们现在来具体说一说,相信你们的数据结构使用的是一门具体的语言比如C/C++语言来说明,那是为了辅助的学习数据结构,而数据结构本身不属于任何语言(相信你把书上的程序敲到电脑里面是不能通过的吧,其只是描述了过程,要调试程序,还需要修改和增加一些东西)。

你们的书上开始应该在讲数据的物理存储结构/逻辑存储结构等概念,说明数据结构首先就是“数据的结构”,在内存上的存储方式,就是物理的存储结构,在程序使用人员的思想上它是逻辑的,比如:你们在C/C++中学习到链表,那么链表是什么一个概念,你们使用指针制向下一个结点的首地址,让他们串联起来,形成一个接一个的结点,就像现实生活中的火车一样。而这只是对于程序员的概念,但是在内存中存储的方式是怎样的那?对于你程序员来说这是“透明”的,其内部分配空间在那里,都是随机的,而内存中也没有一个又一根的线将他们串联起来,所以,这是一个物理与逻辑的概念,对于我们程序员只需要知道这些就可以了,而我们主要要研究的是“逻辑结构”。

我可以给你一个我自己总结的一个概念:所有的算法必须基于数据结构生存。也就是说,我们对于任何算法的编写,必须依赖一个已经存在的数据结构来对它进行操作,数据结构成为算法的操作对象,这也是为什么算法和数据结构两门分类不分家的概念,算法在没有数据结构的情况下,没有任何存在的意义;而数据结构没有算法就等于是一个尸体而没有灵魂。估计这个对于算法的初学者可能有点晕,我们在具体的说一些东西吧:
我们在数据结构中最简单的是什么:我个人把书籍中线性表更加细化一层(这里是为了便于理解在这样说的):单个元素,比如:int i;这个i就是一个数据结构,它是一个什么样的数据结构,就是一个类型为int的变量,我们可以对它进行加法/减法/乘法/除法/自加等等一系列操作,当然对于单个元素我们对它的数据结构和算法的研究没有什么意义,因为它本来就是原子的,某些具体运算上可能算法存在比较小的差异;而提升一个层次:就是我们的线性表(一般包含有:顺序表/链表)那么我们研究这样两种数据结构主要就是要研究它的什么东西那?一般我们主要研究他们以结构为单位(就是结点)的增加/删除/修改/检索(查询)四个操作(为什么有这样的操作,我在下面说到),我们一般把“增加/删除/修改”都把它称为更新,对于一个结点,若要进行更新一类的操作比如:删除,对于顺序表来说是使用下标访问方式,那么我们在删除了一个元素后需要将这个元素后的所有元素后的所有元素全部向前移动,这个时间是对于越长的顺序表,时间越长的,而对于链表,没有顺序的概念,其删除元素只需要将前一个结点的指针指向被删除点的下一个结点,将空间使用free()函数进行释放,还原给操作系统。当执行检索操作的时候,由于顺序表直接使用下标进行随机访问,而链表需要从头开始访问一一匹配才可以得到使用的元素,这个时间也是和链表的结点个数成正比的。所以我们每一种数据结构对于不同的算法会产生不同的效果,各自没有绝对的好,也没有绝对的不好,他们都有自己的应用价值和方式;这样我们就可以在实际的项目开发中,对于内部的算法时间和空间以及项目所能提供的硬件能力进行综合评估,以让自己的算法能够更加好。

(在这里只提到了基于数据结构的一个方面就是:速度,其实算法的要素还应该包括:稳定性、健壮性、正确性、有穷性、可理解性、有输入和输出等等)。为什么要以结点方式进行这些乱七八糟的操作那?首先明确一个概念就是:对于过程化程序设计语言所提供的都是一些基础第一信息,比如一些关键字/保留字/运算符/分界符。而我们需要用程序解决现实生活中的问题,比如我们要程序记录某公司人员的情况变化,那么人员这个数据类型,在程序设计语言中是没有的,那么我们需要对人员的内部信息定义(不可能完全,只是我们需要那些就定义那些),比如:年龄/性别/姓名/出生日期/民族/工作单位/职称/职务/工资状态等,那么就可以用一些C/C++语言描述了,如年龄我们就可以进行如下定义:
int age;/age变量,表示人员公司人员的年龄/
同理进行其他的定义,我们用结构体或类把他们封装成自定义数据类型或类的形式,这样用他们定义的就是一个人的对象的了,它内部包含了很多的模板数据了。

我就我个人的经历估计的代码量应该10000以内的(我个人的经历:只是建议,从你的第一行代码开始算,不论程序正确与否,不论那一门语言,作为一个标准程序员需要十万行的代码的功底(这个是我在大学二年级感觉有一定时候的大致数据,不一定适合其他人),而十万行代码功底一般需要四门基础远支撑,若老师没有教,可以自学一些语言)。

                                              ————来自百度问答

初学者 II

一般来说,数据结构和算法这本书上提到的任何算法/数据结构,你都不会有机会重新实现一次。因为,早有就各种各样的库,对外提供了工业级的、充分泛化的实现,只需拿来用就是了。重写的话,一个代码质量/执行速度,显然都极难超过经过千锤百炼的、在无数项目中经过充分测试的库实现;另一个,书上都是为了教学而做的简化实现,实际使用中,需要对算法做一定的泛化。(比如,c的qsort库函数,只要保证数据是以指针数组索引的、对自定义数据须传入比较大小的功能函数,那么任何数据都可以用这个qsort算法排序;C++里面呢,则是和容器、迭代器之类东西结合通盘考虑的,可泛用于任何符合规范的容器和原生数据类型。而课本上的实现,仅能支持数组中的整数。想做到工业水平,没有足够的经验是不可能的。)

但,另一方面,所有这些算法/数据结构的设计思路,却会贯穿于绝大部分项目之中。比如说,简单的冒泡算法,它是不是只是“多次扫描一个数组,交换遇到的每一对相邻的、顺序反了的数字;当不再发生交换时,数组已完成排序”甚至”好不容易才死记硬背下来的一段代码“?如果你只学会了这个,那么,你真就完全白学了。作为一个表现一般的排序算法,冒泡排序本身出场率就不高;何况还有各种提供了泛化的sort算法的库:如果仅仅记下了这个,那么你一辈子都不会遇到”必须重写冒泡算法“的场合。但,如果你把冒泡算法记成:就好象水中的气泡一样,每次只执行“相邻的元素比较密度(或其它特征),密度小的上浮,密度大的下沉”这个局部物理过程;多次进行后,局部有序就会变成(相关特征上的)整体有序。甚至:模仿各种会导致整体有序现象的局部过程去处理数据,可以使得数据整体上满足类似的排布。甚至:考察任何自然规律,看它会产生什么有趣的后果;那么当需要达到类似的效果时,不妨尝试用程序模拟出这个规律,很可能就已经得到了想要的效果。那,你这一生,可就受用不尽了。比如说,”高大上“的”神经网络“”遗传算法“”蚁群算法“等等等等,其实骨子里不都是这个”冒泡思路“吗?类似的,各种树都是链表的”钩挂“思想+数组的”索引“思想的结合体;”模拟退火算法“又是冒泡思路结出来的另一颗果子;音频滤波算法就是简谐振动计算公式;面向对象的”继承“不过是常见的”归一化“手法的另一个表述方式……可以说,如果能像对冒泡算法的真正理解一样,彻底弄明白各种算法的设计思路并加以借鉴,那么你对这个世界的各种规律了解的有多透彻,你的程序就可以写的有多灵动。一旦掌握这个,从此,你再不必像那些菜鸟一样,绞尽脑汁敲出无数代码去”凑“需求;而是只需用代码编织出需要的规律,然后丢给CPU执行,你真正想要的东西就会自然”涌现“:现在,你只要找出”结果已经出现“的识别方法,用它来结束你的逻辑就行了。(当然,达成一个目的往往可以有多个不同的途径,不同途径利用不同的规律;那么哪个途径最优呢?算法课教过你:这就是所谓的“算法复杂度”)——冒泡算法可不就是用代码编织了一个”数值大者靠前(或靠后)“的规律,然后丢给CPU一跑,一大片数据就有序了?——遗传算法呢,不正是”抄袭“了自然界的自然选择规律吗?把这个规律丢给CPU一跑,居然连AI都弄出来了!这些只是一些特别经典、特别著名的案例而已。

实际工作中,也是时刻都可能遇到一些新鲜的需求/场景;要完成工作,除了出苦力一行行码代码外,一样可以通过观察找到其中的规律,然后用代码编织规律,再让这些规律去替你完成需求:后者往往会比前者简洁的多得多,执行速度一般也会快得多得多。这类随时随地“发明”的算法实在不值一提,不能让你像那些著名案例一样一举成名;但它们却实实在在可以提高你的工作效率,让其他人望尘莫及。——经常有高手骄傲的宣称,别人几万、几十万行代码都解决不了的问题,他数百行代码就清楚漂亮的解决了,执行效率还高出许多倍:他们就是这样做到的。

                                             ————来自知乎问答

实践
写一些程序,尤其是比较底层的程序。就明白它们的用处了。列举下我们当初的作业(其实是老师从UC Santa Barbara\UC Berkley CS作业直接copy来题目)

(1)实现一个简单的 TCP 传输层的协议机制自己去设计协议,不用照搬 RFC 的标准,其实就是数据结构的用场。需要考虑到数据包丢失(Loss)、损坏(Corruption)、乱序(Disorder)这样的情况。

(2)实现操作系统的虚拟内存机制(基于Nachos系统)如何去设计页表。如何使用置换算法。以及应用程序请求页的时候,发生缺页,从而导致的中断如何处理。

(3)实现一个简单的编译器(MiniJava)词法:字符串匹配,表达式求值 等算法;语法:生成抽象语法树;语义:采用适当的设计模式(Visitor)来生成语义表、字典、然后转化为目标代码(可以是汇编、或者是类似的 Three-Address Code)如果以上三个任务都完成并搞懂了,那么恭喜:你不仅掌握了数据结构、算法,而且也学习了计算机网络、操作系统、编译原理中大部分的知识。

修炼

  • 初级篇
    • 记住都有哪些算法,解决什么问题
    • 去试图解决实际的问题,自然会碰到之前算法解决的问题,使用这些算法
  • 中极篇
    • 先完成初级篇
    • 记住算法的具体解决办法
    • 实际的问题如果有与标准算法相似但是不完全一样的,仔细分析差别,修改原有算法
  • 高级篇
    • 先完成中极篇
    • 分析一下算法的解决办法是如何才能想到,最核心和最精妙的地方在哪儿
    • 实际的问题如果与标准算法都不太象,仔细想想这个问题的本质,借鉴经典算法精妙之处,自己设计自己要用的算法
  • 骨灰篇
    • 先完成高级篇
    • 忘掉所有算法
    • 解决实际问题

理解方式

数据结构和算法是计算机科学中最重要的课程,作为一名Google的软件工程师,我经常看到一些求职者或刚毕业的学生,他们对于数据结构和算法的学习是远远不够的。这不是说他们看的书是有问题的,或教授们教错了内容,而是学生对这个课程的理解是不到位的。

扎实掌握数据结构和算法的关键并不是要对每一种数据结构和它的子形式都做详尽的调查,然后记住它们的时间复杂度和空间复杂度。记住这些看起来很棒,也很吸引人,但说实话,你在实际中很少会用到它们。不管怎样,在你的职业生涯中都不会让你实现一个红黑树结点删除的算法,但是,你必须要做到而且轻松的发现在什么时候你需要用二叉搜索树来解决问题,这个是你经常要用到的技能。

所以,停止背诵那些没用的东西吧,从现在开始学好下面这两件基本并且重要的事情:

1.形象化数据结构。直观的理解某种数据结构是什么样的,使用起来是什么样的,在抽象和实际的内存中是如何存储的。这是一件单独的并且很重要的事情,从最简单的栈和队列到很复杂的平衡树。你可以把它们画出来,直观地展现在你脑海里,无论你用什么方式,最重要的就是你要直观地去理解。

2.如何运用到实践。知道在自己的代码中何时,并如何去使用这些不同的数据结构和算法。这一点对于学生来说可能有点难,因为在他们的作业中不需要思考这些。没有关系,你以后会意识到如果你不参加一个实际的项目,你就永远不会理解数据结构,你也不会发现哈希算法是你解决性能问题的方法。但是,即使是学生,你也应该学习一些实用性强的内容,比如什么时候用哈希表?什么时候用树结构?什么时候最小堆是最好的解决方案?

在Google面试时,我会问一个可以把二叉搜索树作为潜在答案的问题,一个较好的回答可以在几分钟后就可以想到二叉搜索树,并顺着它用10-15分钟去解决那些我设置的问题,最终得出答案。但是有一次,我遇到了一个更优秀的面试者,他很形象地理解了树的概念,可以直观地把我的问题展现出来,他可能会被算法精确的复杂度卡住,但是他可以一直在解决问题,因为他做到了把“树结构”直观的展现出来。他最终也得到了这个职位。

原文

I see it time and again in Google interviews or new-grad hires: The way data structures and algorithms—among the most important subjects in a proper computer science curriculum—are learnt is often insufficient. That’s not to say students read the wrong books (see my recommendation below) or professors teach the wrong material, but how students ultimately come to understand the subject is lacking.

The key to a solid foundation in data structures and algorithms is not an exhaustive survey of every conceivable data structure and its subforms, with memorization of each’s Big-O value and amortized cost. Such knowledge is great and impressive if you’ve got it, but you will rarely need it. For better or worse, your career will likely never require you to implement a red-black tree node removal algorithm. But you ought be able—with complete ease!—to identify when a binary search tree is a useful solution to a problem, because you will often need that skill.

So stop trying to memorize everything. Instead, start with the basics and learn to do two things:

  • Visualize the data structure. Intuitively understand what the data structure looks like, what it feels like to use it, and how it is structured both in the abstract and physically in your computer’s memory. This is the single most important thing you can do, and it is useful from the simplest queues and stacks up through the most complicated self-balancing tree. Draw it, visualize it in your head, whatever you need to do: Understand the structure intuitively.

  • Learn when and how to use different data structures and their algorithms in your own code. This is harder as a student, as the problem assignments you’ll work through just won’t impart this knowledge. That’s fine. Realize you won’t master data structures until you are working on a real-world problem and discover that a hash is the solution to your performance woes. But even as a student you should focus on learning not the minutia details but the practicalities: When do you want a hash? When do you want a tree? When is a min-heap the right solution?

  • One of the questions I ask in Google engineering interviews has a binary search tree as a potential solution (among others). Good candidates can arrive at the binary search tree as the right path in a few minutes, and then take 10-15 minutes working through the rest of the problem and the other roadblocks I toss out. But occasionally I get a candidate who intuitively understands trees and can visualize the problem I’m presenting. They might stumble on the exact algorithmic complexity of some operation, but they can respond to roadblocks without pause because they can visualize the tree. They get it.

  • As for a book, there is but one: Introduction to Algorithms by Cormen, Leiserson, Rivest, and Stein, otherwise known as CLRS. If you want another text, perhaps one with more examples in a specific language, I recommend Robert Sedgewick’s Algorithms in C++ or Algorithms in Java, as appropriate. I prefer CLRS as a text, but you might find these a better teaching aid.

酷炫分析

以非常不专业的语言,分享一下自己的理解,帮大家感性的认识一下数据结构。

想象一下你有一条非常非常长的纸条。这张纸条只能写一行字。现在要你把一些描述现实世界的东西写在这张纸条上。然后把这张纸条给别人。别人通过这张纸条重构你所描述的世界,或者在里面查找、推演出自己所需要的信息。

1.这张纸条就是信息的载体,包括硬盘、内存、磁盘、甚至磁带…说白了他们都是一张转着圈或者拐着弯的纸条。

2.给别人的过程就是读写硬盘过程、网络传输过程等等……

3.数据结构所解决的问题就是,你怎么用一行字啰里八嗦的把这些东西描述出来,别人怎么读懂这些啰里吧嗦的东西(注:你看着啰嗦没事,机器看着不罗嗦就行)

4.编程,就是怎么解决3,怎么解决3之后解决你重构出来的世界的一些具体问题…

结语:只要把你的世界观从三维转到一维,你就能学懂数据结构了………

在此,华丽丽的脑补一下黑客帝国中,现实世界碎成渣,变为一串串帅气的数字雨


这里写图片描述

数字雨又华丽丽的重组,形成了一个一模一样的世界

这里写图片描述

当然我们遇到的工程问题,远没有真实世界这么复杂。如果脑洞还够大的话,可以想象一下,你是一个间谍,要把敌人机构的层级关系,用摩斯密码发出去。要把敌人网状的地下通道的结构,用摩斯密码发出去…..(是不是抱着发报机,哭着后悔没学好数据结构了,学渣同学)

你以为你在看一个网页?右键单击网页,查看源代码。其实,你看的是一串字符,换行处的字符是\r\n这是结构化的一行数据。浏览器理解了这种结构,显示给你看到了这个漂亮的网页。

你以为你看的是一个图片?他也是一圈蛇形排列的数值串,(特定格式)操控着显示器的色彩明暗,虚拟一个现实给你,也是结构化的一行数据。

你以为word很神秘?后缀改成.zip,然后解压,你看到的,还是一堆结构化的数据。结构是人为的规则,书里讲的数据结构,是数据组织最基本的规则。还有各种各样的数据标准,文件格式,只不过是更高级别的数据组织规则。再继续深入的话,语言,思维,规律,数学,哲学,万物,三,二,一,道,自然

题外话:

  1. 一个工程问题首先抽象成理论问题,然后利用数学方法进行推演解决,获得了理论知识。然后一套理论知识形成一本书或者一门课。学一门课首先或者最终一定要搞清它解决的是什么样的工程问题,才算是学懂了。换句话说,你要知道当初那个人是遇到了多么苦逼的问题,才提出一个这么牛逼的解决办法。

  2. 学一门课,最重要的是理解,遇到实际问题要知道用什么方法去解决。打个比方就是为知识在大脑内存里建立索引,将知识存在外存,比如书本,百度之类,用到了再去复习一下读进内存。(不排除有内存够大的同学都记在脑子里)比如当要你用关系数据库去存储树结构的时候,当给你链表让你拼写返回json数据的时候,当你要搜索一个矩阵中的连通区域的时候,最最起码要做到的是,知道自己现在需要一本数据结构的书,再好点要知道用哪一个章节的哪个模型。学霸可能已经动手写出来了,但作为学渣翻翻书再写出来,也不会很丢人吧。而且,下次你就可能就用不到翻书了。

  3. 不要让死气沉沉的概念、定义,约束了自己的想象力。一开始定义这个定义的人,沉淀了自己的思想,做出了精准的描述,为的是更好的与别人交流这个东西,而不是用来约束别人的思想。

  4. 当然,自己的专业知识、专业技能,最终还是要沉淀下来,毕竟还要靠它吃饭。最后还是赶快听楼上几位大牛的忠告,脚踏实地的去码代码吧…

理清数据结构的目的

其实学习数据结构重点并不在于哪本书,哪个教程,那个网课。

首要的问题是,要搞清楚数据结构的目的。故名思议,数据结构这门学科就是为了让计算机能够以更加高效,简单,便捷的方式来存储和使用数据而产生的。所有的目标都围绕着存和取两个目标打转。在这两个目标下,有几个评估的指标,存取效率,可扩展性,顺序性,可排序性这几个特征。

要明白这几种特征,就需要先搞清楚几个前提。

一、内存的数据存储模式,是以顺序排列的方式存储的。也就是说,内存中的数据的起点是0,终点根据内存的大小和操作系统而定的一个顺序的序列。0被占用,后面存入的数据则依次存入。

二、程序在一个线程中,只能同时从一个地址来取数据。所以,除了图之外,所有的数据结构都有且只有唯一的取数入口。所以,必须从一个入口来进行取数。

三、内存的数据存取并不是只有一个线程,是很多个程序不断的进行操作的。所以数据的分布是不连续的,并不一定可以一次性找到足够大的一块内存来存放一次操作所需要的所有数据。可以用磁盘碎片整理的图示来类比一下,使用一段时间以后,内存中的数据大概就是这个样子的,此时要想一次性开出一大块数据,是不可能的,所以要想办法解决数据量无限增加时的数据分配。

这里写图片描述

四、数据结构的基础是最吃理解的一门课程,只要理解了,除了遍历算法之外,都豁然开朗,所以刷题毫无意义。关键是理解集中数据结构存在的目的,和之间的区别。

数据结构分为三大类。

一、线性表:这个是为了解决单线存储而出现的,数组就是最简单粗暴的存储方法。就是直接拉出一大块数据存在那里。数组的快速存取其实只是一个副作用,因为所有的数据都在一起,可以直接算出来数据的地址。链表则是为了解决可以无线增长的需求的。因为找不到一大块可以连续的存入数据,甚至也不知道程序可能使用的数据总量,所以就没办法划分一块数据来使用,划小了不够用,划大了浪费(这在早年是非常大的事情)。所以必须想办法解决问题。最后采用的方法就是从入口开始,每一个数据块不仅仅有数据,还会有指向下一个数据块的线索,用来寻找下一个数据。这就是链表。所谓的双向链表,只是加了一个向前的线索的链表而已。队列,栈,都是线性表的特殊形态。进行了操作上的限制罢了。没有什么太复杂的。

二、树:树是为了解决单一入口下的非线性关联性的数据存储或者排序这样的功能而来的。最常见的应用是编程时候的map,就是利用了二叉树的可排序和可以快速插入并且保持序列完整的特性来构建键值数据对,来实现数据的插入增加以及快速查找的能力的。还有做语法解析,文字处理等等很多场景也会用到树。这就不一一赘述了。当然在吃透线性表的基础上,再去理解树也并不难。因为树相对于链表,就是每个节点不止有一个后续节点但是只有一个前置节点。

三 图:图是数据结构里最难的一部分,但是很多学校并不作为重点,因为确实大部分人不会用到。图其实就是把线性表进一步扩展,每个节点会有不止一个前置和后缀节点,而且前置和后缀的概念也不再明晰,变成了关联节点而已。具体的应用主要是一些特殊的算法和图形学上的一些使用。我自己也没有用过。没办法细讲了。总之数据结构的前期学习要重理解。以倒推的方式,搞清楚每种数据结构产生的目标。多画画图,思考一下,理解透彻以后。再去做练习题会事半功倍。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值