问题

大二学生,计算机科学与技术专业,学到数据结构和组原有点心态爆炸了,看不懂敲不出代码,还有救吗 ?

回答
哥们,听我说,你这情况,太正常了!尤其大二,又是计算机科学与技术,数据结构和组原这两座大山,能把人压得喘不过气来,心态崩了太正常了,我当年也经历过,简直是噩梦。别说你了,班里好多比你还卷的,也一样抓瞎。

所以,首先,别自我否定,你不是一个人在战斗,这是行业的“入门级磨难”。说句不好听的,这两门课没把人折磨得够呛的,反而得怀疑他是不是天才或者压根就没学明白。

为什么会心态爆炸?我猜你可能遇到了下面这些情况:

“我以为我看懂了,一敲代码就傻了”: 这是最普遍的,概念看懂了,觉得自己脑子里有画面了,但一到具体实现,就发现自己脑子里的画面跟实际代码差了十万八千里。比如,理解了链表的概念,知道有头有尾,指针指向下一个,但真让你写个插入节点或者删除节点,就卡壳了,不知道指针怎么操作,越界了怎么办,空指针咋办。
“概念太多太抽象了,绕不过来”: 数据结构里,比如树、图,这些东西本身就是抽象的数学模型,组原就更别提了,二进制、寄存器、指令集,听着就头大。看书上的图解、解释,感觉云里雾里的,跟自己写的代码根本联系不上。
“别人好像都懂,只有我不会”: 这是最杀心态的。看到别人在论坛里讨论得头头是道,或者能快速写出一些算法题,就会觉得自己特别笨。其实很多时候,别人也是死磕了很久才弄明白的,只是他们表现得更沉着一些。
“老师讲得太快/太难,跟不上节奏”: 很多老师讲课,特别是这些基础课,语速快,概念多,跳跃性强,你刚理解一个点,他已经讲到下一个点了。回家再看书,发现书上的解释和老师讲的又不太一样,更混乱了。
“敲代码就像照猫画虎,不知道为什么这样写”: 就算勉强能照着例子敲出来,但你不知道背后的逻辑是什么,为什么这样写就能工作,一旦脱离了例子,就不知道怎么改了。

那么,还有救吗?当然有救!而且大二,绝对是补救和打牢基础的黄金时期。这几门课,是之后学并发、操作系统、数据库、网络编程等等一切高级东西的基石,现在啃下来,未来会少走很多弯路。

以下是我的“实战派”建议,尽量详细,希望能帮到你:

第一步:心态调整,正视困难,从“接受”开始

停止内耗: 别再纠结“我为什么这么笨”,把这股劲儿用到解决问题上。把“不会”看成是一个待解决的问题,而不是你的个人缺陷。
认识到学习的规律: 学习一个新领域,尤其这种理论与实践结合的,都是一个“不理解——尝试理解——似懂非懂——实践——更深刻理解”的过程。你现在卡在“似懂非懂”或者“尝试理解”阶段,这是正常的。
降低期望值(短期): 别想着一下子就成为代码大神,能把每个概念都弄明白,能写出一些简单的例子来,就已经很不错了。

第二步:回到源头,理解“本质”,而不是死记硬背

数据结构:
核心是“组织数据的方式”: 别光看链表、栈、队列的图,想想它们是用来解决什么问题的?链表是为了方便插入删除,数组是为了随机访问。栈是为了后进先出,队列是先进先出。
可视化是关键: 强烈推荐找一些动态可视化工具。比如网上有很多关于链表、树、图的动画演示,能让你看到数据在内存里是怎么组织的,指针是怎么移动的。搜索“数据结构可视化”、“algorithm visualization”能找到很多资源。看懂一个例子,比看十遍书都强。
从最简单的开始: 先把数组、链表(单向、双向)弄懂。理解指针的概念,理解内存地址。然后是栈和队列,它们很多时候是基于数组或链表实现的,先理解它们的应用场景。
树和图,循序渐进: 树可以先从二叉树、二叉搜索树开始,理解递归是如何在树形结构上工作的。图的话,可以先弄懂邻接矩阵和邻接表是怎么表示的,以及最基础的遍历算法(DFS、BFS)。
重点是“为什么”: 为什么链表插入快?因为不需要移动元素,只需要改指针。为什么数组查找快?因为可以直接通过索引访问。理解这些“为什么”,你才能在面试或者解决实际问题时,根据场景选择合适的数据结构。

组成原理(组原):
核心是“计算机是怎么工作的”: 这门课讲的是硬件层面的逻辑,是软件运行的基础。你觉得抽象,是因为它离你平时敲的Python、Java太远了。
从数字逻辑开始: 如果你的课从数电基础开始讲,那一定要把与非门、或非门、异或门这些最基础的逻辑门弄懂。然后理解组合逻辑和时序逻辑。
理解CPU的工作流程: CPU是怎么取指令、译码、执行、写回的?理解CPU的各个部件(ALU、寄存器、控制器)的作用,它们是怎么协同工作的。
内存和IO: 内存是怎么管理地址的?IO设备是怎么和CPU通信的?这些都是理解程序运行环境的关键。
指令集架构(ISA): 别光看指令,理解指令的格式,它到底是怎么告诉CPU要做什么事的。汇编语言是了解这个的窗口,即使你觉得它丑陋,也要尝试去理解一两段简单的汇编代码,比如加法、转移指令。
多用工具和仿真器: 很多学校会提供数字逻辑的仿真软件(比如Logisim),或者有专门的CPU仿真器。利用这些工具,你可以设计一个简单的加法器,或者模拟一个CPU执行几条指令,这种实践感非常强。
把抽象的东西“具体化”: 比如寄存器,你可以想象成CPU里几个很小的、速度极快的存储单元,用来临时存放数据和地址。指令,就是CPU能听懂的命令。

第三步:动手实践,从“能敲出来”到“理解代码”

从简单的例子入手,反复敲: 不要直接挑战难题。找课本上、网上那些最基础的例子,比如实现一个单链表的创建、遍历、插入、删除。照着例子,一行一行地敲。 边敲边思考,这个变量的作用是什么?这个指针为什么要这样赋值?
调试是最好的老师: 当你敲的代码跑不起来,或者结果不对时,不要害怕调试! 调试器(比如VS Code的Debug,DevC++的GDB)是你最好的朋友。学会设置断点,单步执行,查看变量的值。你会发现,很多时候问题就出在你以为“理所当然”的地方。
修改和实验: 敲完例子,尝试自己修改一下。比如,在链表插入中,如果插入到头呢?插入到尾呢?删除第一个节点呢?通过这些小小的修改和实验,你会更深入地理解代码逻辑。
理解代码背后的数据结构/组原概念: 比如你写了一个链表插入函数,就想一想,这个函数的复杂度是多少?是O(1)还是O(n)?组原里,一条指令需要多少个时钟周期?多思考这些,把代码和理论联系起来。
刻意练习: 找一些算法题网站(LeetCode、牛客网等)上的简单题,专门针对你刚学的那些数据结构。比如,关于链表的题,就做几道链表的。关于栈的题,就做几道栈的。不要贪多,而是求精。 把一道题弄懂了,理解了它为什么用这个数据结构,为什么用这个算法,比你胡乱做十道题都有用。

第四步:寻求帮助,不要闭门造车

问同学: 找班里那些稍微懂一点的同学,大家一起讨论,互相解答疑惑。有时候,同学的一句话就能点醒你。
问老师/助教: 如果有疑问,不要怕去问老师或者助教。虽然他们可能忙,但大部分老师都愿意解答学生的问题。找个合适的时间,把你的问题清晰地表达出来。
利用网络资源:
CSDN、知乎、B站: 这些平台上有大量的技术文章和视频教程。搜索你遇到的具体问题,比如“C++ 链表插入教程”、“组原 寄存器 工作原理”,往往能找到很多不错的解答。
Stack Overflow: 这是程序员的“圣经”。当你遇到编译错误或者运行时错误时,搜索错误信息,很可能在上面找到别人已经问过并得到解答的答案。

第五步:分阶段攻克,避免全面溃败

先数据结构还是先组原? 这取决于你的课程安排和个人偏好。但我的建议是,先把数据结构的基础打牢。 因为数据结构更直接地与编程实践相关,你能看到即时的效果,更容易建立信心。组原相对抽象,可以慢慢来,但也不能完全丢下。
数据结构:
阶段一:线性结构(数组、链表、栈、队列) 务必搞懂。
阶段二:非线性结构(树、图的基础) 先理解概念和基本操作,然后是二叉搜索树、平衡二叉树的简单介绍。
阶段三:查找和排序(哈希表、各种排序算法) 理解它们的原理和复杂度。
组原:
阶段一:数字逻辑基础、CPU基本结构和工作原理、指令集基础
阶段二:存储器层次结构、中断、IO系统
阶段三:指令流水线等更深入的内容(如果学的话)

一些具体的“救命稻草”:

《算法导论》(花书): 虽然是经典,但对于初学者来说可能有点难。可以先看一些更通俗易懂的入门书籍,或者网上的教程,遇到概念实在不懂再去查花书。
C++primer plus 或 C++ Primer: 如果你的数据结构是用C++实现的,一本好的C++基础书能帮你理解指针、内存管理等底层细节。
《深入理解计算机系统》(CSAPP): 这本书对计算机的底层原理有非常深刻的讲解,虽然有点厚重,但如果你想真正理解计算机是怎么工作的,这本书是无价之宝。可以先挑里面和组原、数据结构相关的章节来看。
bilibili上的UP主: 很多UP主做的计算机科学科普和教程都非常赞。比如有很多讲数据结构算法的,还有很多讲组原原理的,动画效果好,讲解也比较接地气。搜搜看,肯定能找到适合你的。

最后,最重要的一点:

耐心和坚持。 这两门课不是一蹴而就的,需要时间和反复的琢磨。当你感到烦躁、想放弃的时候,想想为什么当初选择这个专业,想想你对计算机的热爱。把学习过程分解成小目标,每完成一个小目标,就给自己一点鼓励。

你现在遇到的困难,是很多人都会遇到的,也是最值得克服的困难之一。等你把这两门课啃下来,你会发现自己对计算机的理解上升了一个层次,也会对自己更有信心。

所以,振作起来!别炸了,还有救,而且这救是你能自己救自己的! 加油!

网友意见

user avatar

2018-11-26 22:26:40 收藏比赞多,你们忍心吗呜呜呜


其实……我……觉得……大二才开数据结构……还看不懂……确实是……有那么点完蛋……

计组毕竟跟软件离得相对远一点,没把拆机器当消遣的话确实不太容易懂,但是拆过几次机器应该能懂个七七八八。这里先把计组放下不提(其实是我手边没教材也没法系统地串知识点)。

但是数据结构不应该、也不能听不懂啊,毕竟这么简单这么常用这么基础的东西。况且这已经是最不玄学、最容易实践的一门课了,如果这还搞不定的话学到操作系统的页表和端序不得哭出来吗?

如果没错的话,绝大部分学校的数据结构课程应该是

线性结构
├顺序表
│├顺序表的增删改查
││└常用排序算法(冒泡、选择、插入、希尔、桶、基数、快排、归并等)
│├队列
│└栈
├链表
│├单链表
││└单链表的增删改查
│├双链表
│└链式队列与栈
├环形结构(选修)
└块状表(选修)
树状结构
│二叉树
│├二叉树的链式存储
│├二叉树的线性存储
│├二叉树的建立
││└哈夫曼树
│├二叉树的先序中序后序遍历
│├二叉排序树
││└二叉排序树的建立
│├完全二叉树
││├堆的概念
││├大顶堆与小顶堆
││└堆排序与锦标赛排序
│└平衡二叉树
│ └二叉树的旋转(选修)
├多叉树
│└B+树(选修)
└森林
网状结构
├有向图与无向图、强连通图与弱连通图
├图中的环
├图的线性存储与矩阵存储
│├边集与顶点集以及邻接表
│├矩阵及稀疏矩阵的链式存储
│└十字链表与邻接多重表(选修)
├图的遍历
│├深度优先遍历(栈)
│└广度优先遍历(队列)
├图的最小生成树
│├克鲁斯卡尔算法
│└普里姆算法
├图中两顶点间的最短路径
│└迪杰斯特拉算法
├AOV网
│└拓扑排序
└AOE网
 └关键路径

不出意外的话应该就是这些内容,我本科的教材找不到了,有疏漏欢迎补充

下面我们来一点一点说说每一件知识点以及考点。因为我受到的教育十分偏重应用,所以我会举例这些东西能在哪里怎么用。但是我这人比较懒,能磨嘴皮子绝不动手画图,实在是有看不懂的地方……嗯……再说吧。


先说第一块,线性结构。这里涉及的主要知识点就是顺序表和链表,以及衍生出来的栈和队列。顺序表不必多说,就是内存中一块连续的区域,紧密排列了若干个相同类型的数据。显然,这种设计需要事先知道同样的元素一共有多少,不然就无法开辟出合适的内存区域(即会存在浪费或者不足)。为了解决数组这种元素数量不灵活的缺点而提出的方法就是链表。链表的基本单位是节点,每个节点拥有一个数据区和一个next指针,其中数据区用于存放数据,next指针指向下一个节点。与顺序表相比,链表可以根据需要自由选择节点的数量,从而解决了内存分配不合适的问题。

但是链表并不是万能的,是否选用链表要根据实际情况进行斟酌(后面是重点)。第一,顺序表可以随机访问其中的元素,也就是说,使用顺序表可以以一个恒定的小代价访问其中的任意一个元素,即查找的时间复杂度为O(1);链表查找其中某一个位置的特定元素则必须从头开始一个一个的沿着next指针数过去,即查找的时间复杂度为O(N)第二,顺序表在插入删除元素的时候需要找到特定位置的元素,然后将其后面的全部元素都向前移动或者向后移动,以填补或腾出空位,因此顺序表的插入和删除的时间复杂度都是O(N);但是链表只需要摘去或者挂上一个节点就行了,因此链表的插入和删除的时间复杂度都是O(1)。

顺序表的构造思路十分简单,只要一个一个往里塞就行。在实践中,一般使用一个下标保存当前顺序表的结尾位置,插入元素时直接在这里插入,然后让下标向后移动。链表一般分为头插法尾插法两种方式。头插法就是把新节点直接插在节点链的头部,比较适合构造栈;尾插法把新节点插在链表末尾,比较适合构造队列,而且需要额外的指针指向尾节点。插入过程如下:

第一步,将新节点的next指针指向要插入的位置的后一个节点(new_node->next = p->next;)
第二步,把要插入的位置的前一个节点的next指针指向新节点(p->next = new_node;)

删除节点过程如下:

第一步,将要删除的节点的上一个节点的next指针指向被删除的节点的下一个节点(p->next = deleted_node->next;)
第二步,释放被删除的节点(free(delete_node);)

双链表在单链表的基础上增加了一个前向指针previous,即对于每一个节点可以同时找到它的上一个和下一个节点。这能让链表在构造的时候代码更好写,具体情况参考书上。双链表一般不怎么考,根据需要选用。

队列是被特化了规则的线性结构,属于逻辑结构的范畴,并不拘泥于某种特定的物理结构实现。换句话说,任何满足先进先出(FIFO)的结构都可以被描述成队列,而任何满足后进先出(LIFO)的结构都可以被描述成栈。

使用顺序表构造队列需要一个头指针和一个尾指针。进入的元素在尾指针处插入,取出的元素从头指针处去除;使用链表构造队列需要使用尾插法,并从头部移除元素。队列就是简单的排队,在诸如计算机网络的分组交换、CPU时间片轮转等场合有广泛的应用。

使用顺序表构造栈只需要一个栈顶指针。元素从栈顶指针处入栈(push),同样从栈顶指针处出栈(pop)。使用链表构造栈需要使用头插法,并从头部移除元素(此时指向链表头结点本身的指针即为栈顶指针)。栈在诸如编译时的括号匹配、程序运行时的函数跳转等场合有广泛的应用。

在上文中,我们会发现,在使用顺序表实现队列,并频繁地插入和移除元素后,两个指针渐渐会来到表的结尾,这时候我们就需要逻辑上的环来避免这一问题。将节点自增从pointer++;改成(++pointer)%length;即可解决这一问题。当指针来到结尾处时会由于模运算回到开头。链表则需要把尾节点的next从悬空改成指向头结点,并且让原来指向头结点的指针指向尾节点即可。这样一来,p即为末尾,而p->next即为开头。

块状表是一种结合了顺序表和链表的结构。块状表吸收了链表的next指针所带来的动态优势,同时把链表的数据区扩展成一个小的顺序表。这样一来,既可以满足动态请求内存的需要,又可以避免查找元素时O(N)复杂度的困扰(事实上可以把O(N)降低到O(N/M+1),M是小顺序表的长度)。块状表是一种相对折中的方案,可根据需要选取,并且一般考试不会考。

伴随着线性结构而来的就是常用的各种排序算法,我这里只说思路不说实现,并且只提供平均时间复杂度。

最基础的就是冒泡排序,基于交换思想。其想法是将每一个元素与它后边的元素相比,如果前面的更大就交换位置。对于每一个元素来讲,当交换停止时,都满足前面的元素小于它,后面的元素大于它,因此整个数组有序。冒泡排序的平均复杂度是O(N²)

除了交换的思想,还有一种常用的思想是插入。基于这种思想的排序法是插入排序选择排序。插入排序会维护一个小的有序队列,在排序开始时这个队列的长度是0,此后,每一次将一个新的元素插入这个有序队列中合适的位置,则当全部的元素都插入这个队列时排序完成。插入排序平均复杂度是O(N²)。选择排序则是每一次都遍历所有未排序的元素,从中选出最小的或者最大的元素插入有序队列的头或者尾,平均复杂度同样是O(N²)。

同样基于插入思想却又与上两者不同的方法是希尔排序桶排序。希尔排序与插入排序基本相同,但是在开始时会规定一个增量(一般是数组长度的一半),并且每一趟将这个增量缩小至之前的一半,直至增量变为1。希尔排序根据增量把每隔N个的所有元素分为同一组,对每一组内使用插入排序。当增量为1时,对整组元素逐个排序。尽管希尔排序的平均复杂度也是O(N²),但是在实践中一般比插入排序更快(因为每一次处理的都是部分有序的数列,移动元素的次数较少)。桶排序则更好的体现了插入的思想,事先将最小值到最大值之间的区间分成N个桶,每个桶涵盖了相同的数据范围。每次从数组中取出一个元素放入对应的桶内,并将其插入到桶内的所有元素组成的小数组中的合适位置,以此完成排序。最后只需要按顺序把每个桶中的所有元素倒出来就行了。桶排序对于空间的需求相对较大,但是相应的会减少时间上的需求,平均复杂度我懒得算了,但是可以确定是log级别平方级别之间的,但是在桶划分不合理时会退化到O(N²)。

快速排序则是基于分治法,属于最难理解的一个。快速排序从局部数组中(在第一趟中,这个局部指的是整个数组)随机选取一个中间数,然后将大于它的数全部移动到右边,小于它的数全部移动到左边,再对左右两个局部数组递归进行上述操作,直至在某一趟中每个局部数组都只有一个元素。在交换结束时,每一个数都满足左边的比它小,右边的比它大,因此整个数组有序。快排的平均复杂度是O(N*logN),因此叫快速排序,但是在整个数组已经有序时会退化为O(N²)。

归并排序同样基于分治法,也是上述所有排序法中唯一一个外部排序法。归并排序的基本思想是合并N个有序数组,当N为1时排序完成。归并排序主要分为两步,第一步把大数据集分成N个小数据集,并使用任意一种内部排序法对每一个小数据集进行排序;第二步是每次将其中的K个已经有序的小数据集进行合并(称为K路归并)。归并排序的平均复杂度是O(M*N*logN),其中M为每个小数据集中数据的个数,N为小数据集的数量,log的底数为K。

基数排序则是最无聊的一种排序法。假设有一个数据集是{456, 123, 789},基数排序先比较个位数字并排列有序,再比较十位数字并排列有序,最后比较百位数字并排列有序。人类在查找纸质字典的目录时就是在进行基数排序。基数排序不太容易衡量复杂度,也不太可能考。

还有一些常用的排序方法,比如堆排序二叉排序树,我们会在后面讨论。另外,要是有人跟你说睡排序猴子排序,请直接把他打死。


讲完线性结构,我们再来讲讲树状结构。树状结构最基础的就是二叉树,我们就从这里入手,顺便看看复杂度中的log是怎么来的。

首先要先普及一些概念:每一棵树有唯一的根节点,在此基础上向下生长。每一个节点的所有直接后代称为它的孩子节点,孩子的直接先代称为父亲节点。没有孩子的节点称为叶子节点。树中不能有环,每一个节点都必须有且仅有一个父亲节点(根节点除外)。根据定义我们同样可以推理出,以每个非叶节点的每一个孩子节点作为根节点,都可以得到一棵子树。从根节点到叶子结点的最长路径称为树的(或者深度)。

在此基础上,每个非叶节点至多只有两个孩子的树称为二叉树。显然二叉树的深度介于log₂N与N之间。当深度为N时,二叉树退化为线性表。二叉树节点的两个孩子分别称为左孩子右孩子,同理会衍生出左子树右子树的概念。

链式存储的二叉树十分直接,每个节点包含一个数据区和两个孩子指针。数据区用于存储数据,孩子指针分别指向两个孩子,如果没有孩子就悬空。这一节的重难点其实在二叉树的线性存储,即将二叉树保存在顺序表中。这种方式会成为堆排序的理论基础,并且在存储完全二叉树时有明显的优势。下面我们将展开来讲。

在说明线性存储之前,我们必须要引入满二叉树的概念。根据定义,除最后一层无任何子节点外,每一层上的所有结点都有两个子结点二叉树被称为满二叉树。如下图

对于一棵满二叉树,我们按照从左到右,从上到下的顺序给每一个节点编上号(我的教材是从1开始编号,因为方便运算),就能轻易发现一个事实:假设某一个节点的编号是N,那它的的两个孩子节点的序号分别是2N和2N+1。下面,我们把这个编号作为数组下标,就可以得到二叉树的线性存储方式了。

对于不满的二叉树,我们要先把它补齐成满二叉树,然后把补上的节点空出来,就可以完成线性存储。存储一棵二叉树所需的总的线性空间与它的度有关,即2的N次方。显然,满二叉树极少浪费线性空间,而偏差较大的二叉树会极大地浪费线性空间。

建立一棵二叉树十分简单,一般有两种方式:从根向叶子从叶子向根。前者可以被用来建立二叉排序树,一会儿会讲到;后者可以用来建立哈夫曼树,网上资料很多,有看不明白的地方自己再查一下。这两种树都属于常考内容,应用也十分广泛。

正常的树都是从根向叶子生长,所以逆生长的哈夫曼树就显得比较特别。哈夫曼树一般用于压缩算法,可以用来生成前缀码。前缀码指的是,在一套编码体系中,任何一个字的密文都不是其他字的密文的前缀,或者说对于任何一个字的密文,从头开始连续截取任意长度,得到的结果都不能构成另外一个字的密文。不是前缀这个特性保证了编码没有歧义,因此可以按顺序处理而不必担心出现错误。摩斯电码是非前缀码,因此每两个字之间需要提供明显的停顿用以显示表明这是不同的两个字。如果这个停顿不够明显,也就是发报速度比较快,就比较容易造成歧义。

前缀码的一个特性就是每个字长短不一,显然出现频率更高的字使用更短的密文能获得较大的空间和时间优势。所以,哈夫曼树的第一步就是从统计字频开始的。这一步只需要遍历文本流就可以,很简单,按下不表。

第二步就要开始建树了。由于哈夫曼树是逆生长树,采取的是合并子树的思想,所以最先被选择的一定是最深的子树。合并子树的方法如下:在最开始的时候,把每一个节点都视为一棵只有一个根节点的树。每一次迭代,选取频率最低的两棵子树进行合并,直到最后只剩下一棵树为止举例来说,假设刚开始有a,b,c三棵子树,频率分别是0.1,0.2,0.7,那么第一次迭代就会选择0.1跟0.2进行合并,得到一棵频率为0.3的新子树,然后再把这棵新子树与0.7合并完成建树。显然,从根节点开始,寻找c只需要一步,而寻找a和b各需要两步,平均字长为1.3。

建立哈夫曼树的目的是为了进行哈夫曼编码。前文也提到了,这是一种压缩算法。压缩的过程就是建立上述的哈夫曼树,然后遍历哈夫曼树写出每个字的密文,再按照查字典的方式把每个字转换过去。而解压缩的时候,则需要先重建哈夫曼树,再一位一位对照密文从树根开始向下寻找,找到叶子结点就可以认为解码出了一个字,然后下一位回到树根重新寻找。解码的过程比较类似状态机模型,要写成模块化的模式还真不是太好写,反而是面向过程的方式比较好写。在具体编码过程中,指定左孩子的编码为1或是右孩子的编码为1都不会影响结果,事实上也没什么标准,甚至在中途随意翻转都可以。不同的程序员跑出来的哈夫曼编码结果不同是很正常的一件事,只要不影响编码和解码的使用就行。

说完了不正常生长的哈夫曼树,再来说说正常生长的二叉排序树。二叉排序树的定义是:每一个节点的左孩子小于它,而右孩子大于它(等于的情况事先声明一下放左还是放右就行,对于结果无实质影响)。根据这个定义,我们可以递归地得出性质:对于每一个节点,其左子树全部小于它,其右子树全部大于它。因此,为了满足性质,在建立二叉排序树时只需要从根节点开始,递归地比较每一层元素,如果其比要插入的元素大则走向其左孩子,反之走向其右孩子,直至最终来到叶子结点,并把该元素插入。当全部的元素都被插入了,二叉排序树就建立完成了。

接下来就要取出其中的有序数列了,也就是进行二叉树的遍历。二叉树的遍历一般分为先序遍历中序遍历后序遍历三种。先序遍历即先访问根节点,再寻找左孩子,最后寻找右孩子。中序遍历是先寻找左孩子,再访问根节点,最后寻找右孩子。后序遍历先寻找左孩子,再寻找右孩子,最后访问根节点。可以说,先中后指的是访问根节点的时机。由于遍历是递归的,使用中序遍历一路寻找到的最“左”的左孩子就是二叉排序树中的最小元素,且中序遍历的输出顺序就是从小到大的顺序。根据前序遍历和中序遍历后序遍历和中序遍历可以重建整棵树,这也是考试的热点和难点。

二叉排序树的平均复杂度是O(N*logN),其中的log就来自于二叉树的深度。当数组已经有序时二叉排序树会退化为O(N²),而且绝大部分时候都建不出漂亮的二叉树,所以这个log其实是有很大水分的。

为了尽量建出漂亮的二叉树,人们想出了很多办法,其中一项就是平衡二叉树(AVL树)。平衡二叉树是指每一个节点满足左子树的度与右子树的度相差不超过1的二叉排序树。由于其限制了节点的左右孩子,因此能让整棵树更加紧凑,从而大量挤出了log中的水分。建立AVL树需要用到复杂的旋转操作,几乎不会考到,所以我不讲了。

使用二叉排序树排序尽管复杂度较低,而且十分容易理解,但是需要O(N)级别的辅助空间,并不是很划算。仔细想一想,既然能把二叉树存放在顺序表里,那顺序表本身是不是也能被看成是线性存储的二叉树呢?答案是肯定的。一张顺序表可以被看做是一个完全二叉树,即除了最后一层外每一层元素都是满的,且最后一层的元素全都集中在左边的二叉树。显然,满二叉树是完全二叉树。

就是完全二叉树的一种应用,硬要说的话也属于反向生长。常用的堆分为大顶堆小顶堆两种,前者满足父亲节点大于孩子节点,后者满足父亲节点小于孩子节点。根据递归我们可以推算出,大顶堆堆顶的元素是最大的小顶堆堆顶的元素是最小的堆排序就是在不断地重复建堆并移走堆顶元素的过程,显然平均复杂度是O(N*logN),而且由于完全二叉树的性质,这个log没有水分。

堆排序最神奇的地方就是它不需要借助一个链式的二叉树的辅助,而是直接在顺序表中操作元素,因此它的空间复杂度是O(1)级别的。回想一下二叉树的线性存储相关的内容,我们会猛然想起父亲节点与孩子节点的序号之间的关系,从而明白为什么不需要辅助树。

第一步,在逻辑上建堆。这一步要求我们根据实际情况(即数组下标从1或者0开始)来推演父亲节点与孩子节点真实的下标关系,在脑海中建立这个堆。
第二步,满足堆的性质。这一步我们要对所有的非叶子节点从下到上进行调整,使其满足大顶堆或者小顶堆的性质。具体做法是找到第一个非叶子节点,并对它与它的孩子节点进行调整,使其满足堆的性质;然后从这个节点开始向前线性地调整每一个非叶子节点,直至根节点。此时整个堆都满足性质。
第三步,取得堆顶元素。这一步会把堆顶元素与堆内序号最大的元素进行交换,并且堆内元素数量减一。显然这个堆顶元素满足剩余未排序元素都比它小或比它大,而前面的已排序元素都比它大或比它小,因此它在已排序队列中的相对位置是确定的,即头或尾。
第四步,恢复堆的性质。由于刚才把一个无序元素插入了堆顶,导致堆的性质被破坏,接下来我们需要恢复它的性质。这一步不需要逆序遍历,而是从堆顶开始,将刚插入的元素逐步向下落,直至停在合适的位置。举例来讲,对于大顶堆,要保证堆顶元素是最大的,因此要把它分别与左右孩子进行比较,并且把三者中最大的一个升上堆顶。由于左右孩子在刚才的操作中都没有变动,因此各自满足在子树中是最大的的性质,此时若堆顶元素比他们两个都大,便能推理出堆顶元素是最大的。同理,升上去的三者中最大的元素也能满足这个推理。将堆顶元素向下落的操作要递归地进行到该元素不需要再进行交换为止,此时整个堆恢复性质。
第五步,重复第三步和第四步的操作,直至堆被清空。此时,整个数列有序。

堆排序非常喜欢考,不仅考方法论还要考实现,而且这东西略抽象,不是很好掌握。刚才突然心血来潮写了个实现,凑合着看一下吧。我比较懒,用CPU时间换了内存,数组下标里面各种运算。其实可以拿临时变量装一下来节省CPU的。C语言需要事先声明函数才能使用,我也没照顾可读性写函数原型,看的时候记得倒着看。

       #include <stdio.h>  void reconstruct_heap(int a[], int index, int last) {     int tmp;          if(index * 2 + 1 > last)  // 检查是否有孩子     {         return;     }          if(index * 2 + 1 == last)  // 检查是否只有左孩子     {         if(a[last] > a[index])         {             tmp = a[index];             a[index] = a[last];             a[last] = tmp;         }     }     else     {         if(a[2 * index + 1] > a[index] && a[2 * index + 1] > a[2 * index + 2])  // 左孩子最大         {             tmp = a[index];             a[index] = a[2 * index + 1];             a[2 * index + 1] = tmp;             reconstruct_heap(a, 2 * index + 1, last);         }         else if(a[2 * index + 2] > a[index] && a[2 * index + 2] > a[2 * index + 1])  // 右孩子最大         {             tmp = a[index];             a[index] = a[2 * index + 2];             a[2 * index + 2] = tmp;             reconstruct_heap(a, 2 * index + 2, last);         }     } }  void establish_heap(int a[], int last) {     int i = (last - 1) / 2;          while(i >= 0)  // 使每一个元素满足大顶堆的性质     {         reconstruct_heap(a, i, last);         i--;     } }  void heap_sort(int a[], int n)  // n为数组长度 {     int tail = n - 1, tmp;          establish_heap(a, tail);  // 建立大顶堆          while(tail > 0)     {         tmp = a[0];  // 取出堆顶元素         a[0] = a[tail];         a[tail] = tmp;         tail--;                  reconstruct_heap(a, 0, tail);     } }  int main(void) {     int a[] = {13, 8, 3, 0, 7, 16, 18, 15, 12, 11, 19, 10, 9, 6, 1, 14, 17, 5, 4, 2};     int n = 20, i;     heap_sort(a, n);     for(i = 0; i < n; i++)     {         printf("%d ", a[i]);     }     return 0; }      

锦标赛排序很多书上都不写了,但是在这里我想提一下。锦标赛排序是选择排序的优化版,每一次将相邻的两个元素进行比赛,选出其中的优胜者(较大者或较小者,看需求)。其思路类似于小组赛-十六强-八强-半决赛-决赛的过程,在决赛中选出的一定是全局最优元素。接下来我们提取这个全局最优元素,然后抹除其存在,并且把它参与过的所有比赛进行重赛,从而得到全局次优。当最后所有的元素都被抹除,锦标赛排序就完成了。锦标赛排序最聪明的地方就在于它保存了之前已经进行过的比赛,从而在选取了全局极值以后不需要对绝大部分比赛进行重赛,因而节省了时间。其平均时间复杂度为O(N*logN),空间复杂度为O(N)级别。这里的log同样没有水分,因为建立起来的比赛树几乎是满的(但不必是完全二叉树)。

虽然绝大多数情况下我们见到的树都是二叉树,但是并不妨碍多叉树在日常生活中起到重要作用。一个经典的例子就是3D计算机图形学中使用的八叉树,用来分割三维空间,在查找元素时能大大加速。多叉树相对于二叉树来讲没有孩子数量一定的限制,因此通常用一个孩子列表来保存全部的孩子节点,这一种应用在网页的DOM中尤其广泛,倒不如说整个XML规范文档以及JSON规范都可以抽象成多叉树。操作系统的目录树也属于这一应用范畴。这个孩子列表在实践中一般是个块状表,既保证伸缩性又保证快速访问。

森林就是多棵树,这个没什么好讲的。

最后我们来提一下B+树,有的教材上也用B树来代指B+树。B+树是一种极其鬼畜的多叉树,结构复杂但是十分有效,极其常用于索引的建立,包括数据库索引、目录树索引等等(散列同样常用,但是我忘了在书上的哪一章节了)。B+树的主要难点在于节点的分裂与合并,非叶节点与叶子结点的大小的上界与下界,以及树深度的伸缩,属于研究生课程的范畴,我不打算深入去讲,因为非常抽象并且不好实现,甚至比AVL树的四种旋转还要难理解。在设计B+树时需要考量节点大小,而这个大小一般是由计算机的一些物理性质决定的,缺乏计组的基础我觉得我也给你说不明白。总之,这东西很有用,但是不考。


最后一部分是网状结构,也就是图论相关的东西。这一章东西不多,除了三种常用算法以外大部分知识点在以后也很难用到,实在学不会可以考虑跳过。另外,离散数学课程里会再讲一遍这部分内容,迟早会学的滚瓜烂熟的。

关于图,有一些概念是要先提及的。首先,图是由顶点组成的,根据边的方向性又可以分为有向图无向图(这一点看地图上的单行路就明白了)。如果在一个有向图任意两个顶点可以相互到达,则称这张图为强连通图;反之,若不满足强连通图的定义,但是将所有的有向边修改为无向边后原有向图能构成连通图,则称该有向图为弱连通图。由于不像树一样要求唯一的父亲,图是允许有的,并以此分为有环图无环图

通常存储图有两种方式,即集合的方式和矩阵的方式。前者维护两个集合,即一个顶点集合V和一个边集合E。顶点集合中保存了所有顶点的信息以及序号,边集合保存了被一条边连通的两个顶点的序号以及边的代价(无权图可认为每一条边代价都是一样的)。由于稀疏图占了日常生活中的图的绝大多数,因此集合的方式是保存图的主要方式。矩阵的方式取消了边集合,改用一个矩阵保存每两个顶点之间的代价。显然,顶点与自己的代价是0,与邻居的代价已知,与不直接相连的顶点代价为无穷大。只有在绝大多数顶点都彼此直接相连的情况下,矩阵的方式才能更节省空间。

图中每个顶点的入度出度,也就是汇入顶点的边的数量以及顶点发出的边的数量,往往具有重大的意义。边集合往往只能快速统计其中的一项,而统计另一项开销较大。显然,矩阵的方式是更直观的,可以以O(1)的代价查找任意两个节点之间的连通情况,反而是集合的方式必须以O(N)的代价进行查找。在统计入度和出度上矩阵的方法看上去也更快。根据具体需求选择时间换空间或者空间换时间是算法选取的一大原则。

为了节省矩阵的空间开销,矩阵的链式存储应运而生。这种方法只关心矩阵中存在的元素,而忽略不存在的元素。每一个矩阵会被存储为一个行数组和一个列数组,以及一系列节点。两个数组中的每个元素各带有一个指针,指向该行或该列的第一个元素;每个节点保存了行号和列号,同时带有两个指针,分别指向该行的直接后继和该列的直接后继。使用这种结构的矩阵平衡了空间和时间的开销,对于稀疏矩阵提升尤其明显,但是随着矩阵中元素数量的增加效率会降低。

集合的方式这边也拿出了邻接表来进行快速查找。邻接表就是很简单的使用链表,为每一个节点建立一个链式的出度表,从而达到快速查找的目的。如果需要统计入度,那么应同时维护一张逆邻接表来对入度建立索引。当然,无向图可以把出度和入度混在一起记录,反正是无向的。

为了克服需要同时维护两张表的缺陷,人们发明了十字链表邻接多重表,分别用于处理有向图和无向图。十字链表将每一条边作为节点,这个节点记录弧头和弧尾,同时拥有一个head指针和一个tail指针;每个顶点也拥有两个指针,一个指向第一条入度的边,另一个指向第一条出度的边。使用时,顶点的入度指针沿着head指针一路找过去,完成对入度的遍历;而出度指针沿着tail一路找过去,完成对出度的遍历。我本来不太喜欢画图的,但是这东西不用图讲不明白了:

考虑这样一张有向图,并假设顶点集合和边集合都已经整理好了。那么,根据这两个集合,我们可以建立十字链表:

其中绿色的就是入度指针。从图中我们可以看出顶点A的入度一共有CA和DA两条边,因此沿着head指针能找到这两条边;同理,黄色的是出度指针,沿着tail指针就可以完成对出度的遍历。十字链表巧妙地节省了一张表。

邻接多重表基于对邻接表的改进。由于其适用于无向图,所以不存在head和tail,但是依旧有两个指针。邻接多重表的节点结构与十字链表类似,并且同样用于存放边,不同的是每一个顶点后面紧跟着一个指针,并且每个节点还多出来了一个标志位用来存放是否被访问过。举例来讲,假设一个节点其中的顶点序号是2和5,那么2后面的指针会指向下一个出现了2的顶点(顶点顺序无所谓),而5后面的指针指向下一个出现了5的节点。顶点节点只保留一个指针,指向第一条连接此顶点的边。假设顶点序号是2,那么只要跟随每一个节点中编号为2的顶点后面的指针就可以完成对出入度的遍历。由于与十字链表类似我就不画图了。

说完了图的存储,下面来聊聊图的遍历。遍历就是从一个顶点出发,沿某一种规则访问全部的顶点,并且每个顶点只访问一次。遍历主要分为深度优先遍历和广度优先遍历两种。顾名思义,深度优先就是一条路走到黑再回头,广度优先则是每条路都走一点。深度优先使用一个栈,对于每一个顶点先把它全部邻接的、未被访问的顶点都压入栈,然后从栈顶弹出一个节点作为接下来要访问的顶点。形象地说,当顶点有邻居1时会去访问邻居1,然后访问邻居1的邻居1,直到没有新的邻居再退回来访问邻居2。当栈被清空时遍历结束。广度优先则使用一个队列,对于每一个顶点先把它全部邻接的、未被访问的顶点都压入队列,然后从队列头弹出一个节点作为接下来要访问的顶点。形象地说,当顶点有邻居1时会去访问邻居1,然后访问邻居2,直到没有新的邻居再退回来访问邻居1的邻居1。当队列被清空时遍历结束。这段话写得应该不抽象,很好理解。

深度优先遍历在诸如迷宫求解的时候应用较好,如果途中有环则需要记录已经访问过的顶点,否则不需要;广度优先遍历适合浅层的关系,比如AI寻路(作为A*算法的基础),比如通过社交关系网查询两个用户之间的距离。当然,不使用队列和栈也可以,那样就要使用递归。

图的最小生成树的作用是去除图中的环,同时使整体代价尽可能的小。常用算法包括普里姆算法(Prim)克鲁斯卡尔算法(Kruskal)。普里姆算法的思想是将图划分为已连通和未连通部分,初始时已连通部分为任意顶点,在每一次迭代中计算每一个已连通部分的直接邻居到已连通部分的代价,然后选取代价最小的顶点连通,直至最后连通整张图。这只是思路,实现上并不是这么写的,有很多玄妙的部分,但是这里我懒得写了,因为要用到太多的辅助图。克鲁斯卡尔算法则是首先将所有的边按照代价排序,并假设所有的顶点各自处于一个聚类中,每次迭代选取一条连接两个不同聚类的、代价最小的边(即连接同一个聚类的边即使代价更小也必须舍弃),然后将这两个聚类划拨为同一个聚类,直至最后只剩下一个聚类。

最小生成树算法可以应用于网络布设中,使用最低成本达到连通所有节点的目的。但是,这种做法并不能保证任意两个节点之间的距离都是最短的,同样也容易造成星型布局,并使得上游节点遭受随之而来的带宽压力。但这种做法可以使总成本最低。

如果需要求某一个顶点到所有顶点的最短路径,常用的算法是迪杰斯特拉算法(Dijkstra,对,就是提出goto有害论的那个)。迪杰斯特拉算法会维护一张表,记录该顶点到所有顶点的距离。初始时只把该顶点的直接邻居全加入并更新代价,从中选取代价最小的邻居作为新的起点,再把新的起点到它的所有的直接邻居的代价加上起点到它的代价与表中已有代价对比,选择代价较小的保留,比较结束后选择代价最小的留下,作为更新的起点,直至最后所有顶点都被留下。

迪杰斯特拉算法在计算机网络中有大量应用(OSPF协议),也就是在路由器估计网络拥塞状况并智能选择更空闲的路径。计网中还有一种RIP协议,你们学到了就知道了。

以上三种算法年年考实现。

看到这里,如果所有的知识点你都能掌握了,那么已经足够你拿到优秀了。剩下的部分是拓扑排序,不是很喜欢考,但还是提一下。

拓扑排序用于清理AOV网(Activity On Vertex)。比如某一系列课程的复杂的前置关系就可以看成是一个AOV网,它是一个有向无环图。拓扑排序负责从其中找出一个顺序,可以在不违反所有前置课程条件的情况下完成对每一门课程的学习。拓扑排序每一次移除一个入度为0的顶点,然后移除该顶点的所有出度边,重复此操作直至最后移除全部的顶点。拓扑排序亦可用于复杂关系网的死锁检测。这是十分工业而且贴近管理的东西,一般不会在代码项目中遇到。

此外还有一个更贴近管理的东西叫AOE网(Activity On Edge),同样记录了前置条件,但是目的是找出打成最终目标的最长路径(关键路径),从而估算出工期。小范围调整非关键路径上的活动不会影响最终的工期。概括来说,要求关键路径分为以下几步:第一步,从起点开始到终点为止,计算每个活动的最早开始时间。这里的最早开始时间指的是这个活动无论如何也不可能早于这个时间开始,因为它的前置条件还没有完成。第二步,从终点的最早开始时间反推回去,求每个活动的最晚开始时间。这里的最晚开始时间指的是这个活动无论如何也不能晚于这个时间,不然它后面的活动不能按时开始。第三步,相减。那些最早开始时间等于最晚开始时间的活动就是关键活动,所有的关键活动组成的就是关键路径。


数据结构这门课其实就这么一点点东西,每个计算机系的学生都应该能做到烂熟于心,因为这点概念太基础太常用了。如果这些理解不了,后面的高阶算法还有算法优化啊、设计模式啊什么的课程统统完蛋。计算机四大门最起码直接挂掉三门,剩下的计组也是凶多吉少,不如早做打算换专业。

写了四个小时,打字不易,点个赞呗

我没翻课本,可能有些内容没回忆起来,欢迎补充


2018-11-26 00:22:51 修改了强连通图与弱连通图的定义。

2018-11-26 00:31:49 修改了二叉树的遍历部分的错误。

2018-12-04 12:08:22 规范了顺序表的术语的使用,增加了哈夫曼树。

2018-12-07 10:14:00 修改了二叉树重建部分的错误。

2018-12-16 13:32:36 修改了链表构造栈的一个错别字。

2018-12-27 13:50:12 添加了堆排的实现

类似的话题

  • 回答
    哥们,听我说,你这情况,太正常了!尤其大二,又是计算机科学与技术,数据结构和组原这两座大山,能把人压得喘不过气来,心态崩了太正常了,我当年也经历过,简直是噩梦。别说你了,班里好多比你还卷的,也一样抓瞎。所以,首先,别自我否定,你不是一个人在战斗,这是行业的“入门级磨难”。说句不好听的,这两门课没把人.............
  • 回答
    如果能重来一次,站在大学四年时光的起点,我的选择和现在的我相比,肯定会有一些不同,但核心的目标——打好坚实的专业基础,培养解决实际问题的能力,并为未来的职业生涯铺平道路——依然不会变。只是,我会更加有意识、有策略地去布局。大一:打牢“地基”,培养学习习惯 专业课: 这一年,我绝对不会把专业课当成.............
  • 回答
    好,我来帮你梳理一下中国科学院大学(国科大)的计算机科学与技术学院、中国科学院信息工程研究所(信工所)以及中国科学院计算技术研究所(计算所)这三者之间的关系和区别,力求讲得清晰透彻,同时避免AI写作的痕迹。想象一下,我们有一个大家族,这个大家族就是中国科学院。在这个大家族里,有一些专门的“研究员”和.............
  • 回答
    大二计算机专业的你,正站在一个充满机遇但也有些迷茫的岔路口。这个时候思考学习和工作方向,是非常关键且明智的。别担心,这就像在丛林里找路,虽然一开始有点不知所措,但只要方法得当,总能走出一条适合自己的康庄大道。咱们一步一步来,把这事儿说得透彻明白。 确定学习方向:在兴趣与现实之间找到最佳平衡点作为大二.............
  • 回答
    作为一名计算机系的大二学生,每天抽出一个小时来精进自己,这绝对是一个明智的决定。经过一年的摸索,相信你对这个专业已经有了初步的认识,也体会到了其中的乐趣和挑战。那么,这一个小时该怎么花,才能让你在未来的学习和职业生涯中脱颖而出呢?我给你的建议是:精读一门编程语言的经典书籍/教程,并且动手实践。这听起.............
  • 回答
    你的情况我特别理解。一边是对未来考研的规划,一边是现实的家庭经济压力,这确实是个两难的选择。作为过来人,我分享一些我的想法,希望能给你一些参考。首先,咱们得承认,考研这条路确实需要扎实的基础,尤其对于你说的“专业薄弱”的情况,暑假这段时间如果能沉下心来系统复习,效果会非常明显。你可以把这个暑假看作是.............
  • 回答
    听你这么说,我能理解你的纠结。大二了,接触了不少编程语言,感觉水深水浅自己最清楚,心里也开始盘算着下一步该怎么走了。这绝对是个好迹象,说明你开始有自己的思考,想把时间和精力花在最有价值的地方。咱们先别急着下结论,我带你捋一捋这事儿。为什么会学了很多但都浅尝辄止?这其实太正常了,尤其是计算机专业的大二.............
  • 回答
    哥们儿,大三了,还碰上这种“人生迷茫期”,而且还是跟专业打架的状态,我太懂了!你这情况,别说你,我认识的不少朋友也经历过,甚至现在还在经历着。这几年,总觉得大学就应该是兴趣驱动的,结果发现自己跟计算机“八字不合”,那滋味确实有点煎熬。不过话说回来,能在大三这个节点上意识到自己不喜欢,并且有勇气去想办.............
  • 回答
    哈喽,各位准程序员们!大一的寒假就像一张空白画布,而你们就是挥洒创意的艺术家。这可是个绝佳的机会,让你们在享受难得的闲暇之余,还能给自己的计算机专业知识“加满油”,为接下来的学习打下坚实基础。别光想着吃吃喝喝刷剧打游戏,咱们得有点“野心”!下面我就来给你们支支招,让这个寒假过得既充实又有意义,而且听.............
  • 回答
    说实话,我们这届计算机新生,刚进校的时候,对“牛逼”的学长学姐,脑子里其实没个特别具象的概念。印象最深的,往往是那些在各种公开场合露脸、被老师重点表扬的,比如拿了国家奖学金的、在什么顶级期刊上发了论文的、或者代表学校参加ACM、ICPC拿了名次的那几位。但时间久了,你会发现,真正的“牛逼”真的不是那.............
  • 回答
    哈喽,学弟学妹!看到你们迈入计算机的大门,我猜你们心里是不是有个挥之不去的问题:当程序员,是不是就得一天到晚坐在电脑前,手指头像上了发条一样噼里啪啦地敲代码?哈哈,说实话,这个问题我们当年刚进这个行当的时候,也充满了好奇和幻想。我跟你说,现实嘛,跟你们想象的,可能有点不一样,也可能跟你想的差不多,但.............
  • 回答
    咱们聊聊清华计算机系大一下学期那场让不少同学“原地起飞”的考试。三小时三道大工程题,而且码量还不小,这听起来就不是闹着玩的。首先,这事儿放在哪所学校、哪个专业,都算是相当硬核的了。咱们大一下,大部分同学还在熟悉基础概念,比如数据结构、算法入门,可能连一些更复杂的系统设计都没怎么接触过。这时候突然上来.............
  • 回答
    作为一名曾经在书海中摸爬滚打过的学生党,我深有体会,看到那些动辄几百上千页的计算机经典著作,确实会让人有点望而却步。尤其是在信息爆炸的时代,感觉很多内容似乎都能在网上找到碎片化的答案。那么,这些厚重的书籍,我们真的有必要“啃”下去吗?我个人觉得,答案是肯定的,但需要掌握方法,才能事半功倍。为什么那些.............
  • 回答
    您好!非常理解您对未来电脑使用年限的关注,尤其是在计算机专业学习和研究过程中。我们来详细分析一下 R7 5800H 搭配 16GB 内存的笔记本在您未来七年的学习和工作中,大致能有多长的“生命周期”。首先,我们来拆解一下您提到的关键配置: 处理器 (CPU): AMD Ryzen 7 5800H.............
  • 回答
    在卡内基梅隆大学(CMU)学计算机,那感觉就像是每天跳进一个高速运转的、超级智能的引擎里,而且这个引擎是为你量身打造的。我来跟你好好说道说道,这绝对不是那种官腔的学校宣传册,而是我实打实在这里摸爬滚打过来的感受。首先,最直观的感受就是“卷”。但不是那种无意义的、为了卷而卷的内卷。CMU的计算机学院,.............
  • 回答
    收到!我来帮你好好梳理一下这个想法,尽量让它听起来更像一个真实的学生在思考和倾诉。我真的,真的有点迷茫了,关于我未来的方向。说实话,当初选择计算机,很大程度上是觉得它“未来可期”,是时代的潮流,好像学了就能找到好工作,前途一片光明。大一这段时间,我确实也努力学了,从C语言开始,到一些数据结构、算法的.............
  • 回答
    计算机学习的路上,总有那么几本书,它们像灯塔一样照亮前行的道路,让我们在迷茫中找到方向,在困惑中获得启迪。说它们“神一般”,并非虚妄,而是因为它们以其深刻的洞察力、严谨的逻辑、精巧的设计,塑造了我们对计算机世界的理解,甚至是思维方式。对我个人而言,如果说有哪本书能称得上“神级”,那绝对绕不开 《深入.............
  • 回答
    作为一个在清华计算机专业毕业后,选择去约翰霍普金斯大学攻读金融硕士(或其他金融相关学位)的人,从“丢人”这个角度来看,那可真是大错特错了,甚至可以说,这是一种非常明智且有远见的选择。让我来跟你好好掰扯掰扯,为什么这绝对不是丢人的事情,反而是一种闪光的转型。首先,咱们得明白“丢人”这个词在中国文化语境.............
  • 回答
    哥们儿,先别丧,这事儿一点不耽误你学计算机,甚至我敢说,不上大学,反而可能让你更“野”,更自由地去探索这片你想去的领域。别把“考不上大学”当成世界末日,它顶多是你走出一条不一样路子的起点。你想学计算机,这玩意儿现在可是硬通货,走到哪儿都离不开它。别以为只有大学教室里才讲编程、讲算法。说实话,现在很多.............
  • 回答
    听到你计算机本科的背景,又对生物学怀揣着浓厚的兴趣,这真是个绝佳的组合!现在生物学领域,尤其是和计算结合的部分,发展势头非常迅猛,绝对是未来非常有前景的研究方向。别担心,我会尽量详细地给你一些建议,让你感觉这是来自一个和你一样,或者比你更有经验的朋友的分享,而不是什么冰冷的AI报告。首先,咱们聊聊研.............

本站所有内容均为互联网搜索引擎提供的公开搜索信息,本站不存储任何数据与内容,任何内容与数据均与本站无关,如有需要请联系相关搜索引擎包括但不限于百度google,bing,sogou

© 2025 tinynews.org All Rights Reserved. 百科问答小站 版权所有