导航:首页 > 程序命令 > 程序员怎么算反转链表

程序员怎么算反转链表

发布时间:2023-03-27 14:02:33

程序员在设计和编写程序时,经常采用哪两种编程模式

你好
程序员编程时有14种模式
1.滑动窗口
2.二指针或迭代器
3.快速和慢速指针或迭代器
4.合并区间
5.循环排序
6.原地反转链表
7.树的宽度优先搜索(Tree BFS)
8.树的深度优先搜索(Tree DFS)
9.Two Heaps
10.子集
11.经过修改的二叉搜索
12. 前 K 个元素
13. K 路合并
14.拓扑排序
经常用哪个哪些,看个人习惯了

Ⅱ 程序员算法实现-买卖股票的最佳时机系列问题

主要思路:因为只有一股可以交易,所以我们可以枚举 必须以i位置作为卖出时机的情况下,得到的最大收益是多少。如果我们得到每个i位置的最大收益,那么最大收益必是所有位置的最大收益的最大值

使用两个变量:

min变量:表示遍历到的位置之前的最小值是什么。

max变量:表示当前收集到必须以i位置卖出的最大收益是多少。

遍历数组一遍,在遍历到i位置的时候,min和max的更新逻辑如下:

遍历完数组,返回max的值就是最终答案。完整代码见:

主要思路:由于可以进行任意次的交易,但是任何时候最多只能持有一股股票,所以我们可以把股票曲线的所有 上升段 都抓取到,累加收益就是最大收益。遍历数组,遍历到的位置减去前一个位置的值,如果是正数,就收集,如果是负数,就把本次收益置为0(就等于没有做这次交易),这样遍历一遍数组,就不会错过所有的收益。

设置一个变量max,初始为0,用于收集最大收益值,来到i位置,max更新逻辑如下:

完整代码如下:

由本题可以简单得出一个结论: 如果数组元素个数为N,则最多执行N/2次交易就可以抓取所有的上升段的值(极端情况下,当前时刻买,下一个时刻卖,保持这样的交易一直到最后,执行的交易次数就是N/2)

主要思路:

在第2种情况下,我们定义

其中dp[i][j]表示[0...i]范围内交易j次获得的最大收益是多少。如果可以把dp这个二维表填好,那么返回dp[N-1][k]的值就是题目要的答案。

dp这个二维矩阵中,

第一行的值表示数组[0..0]范围内,交易若干次的最大收益,显然,都是0。

第一列的值表示数组[0...i]范围内,交易0次获得的最大收益,显然,也都是0。

针对任何一个普遍位置dp[i][j]的值,

我们可以枚举i位置是否参与交易,如果i位置不参与交易,那么dp[i][j] = dp[i-1][j],如果i位置参与交易,那么i位置一定是最后一次的卖出时机。

那最后一次买入的时机,可以是如下情况:

最后一次买入的时机在i位置,那么dp[i][j] = dp[i][j-1] - arr[i] + arr[i]

最后一次买入的时机在i-1位置,那么dp[i][j] = dp[i-1][j-1] - arr[i-1] + arr[i]

最后一次买入的时机在i-2位置,那么dp[i][j] = dp[i-2][j-1] - arr[i-2] + arr[i]

...

最后一次买入的时机在0位置,那么dp[i][j] = dp[0][j-1] - arr[0] + arr[i]

完整代码如下:

上述代码中包含一个枚举行为

增加了时间复杂度,我们可以优化这个枚举。

我们可以举一个具体的例子来说明如何优化,

比如,

当我们求dp[5][3]这个值,我们可以枚举5位置是否参与交易,假设5位置不参与交易,那么dp[5][3] = dp[4][3],假设5位置参与交易,那么5位置一定是最后一次的卖出时机。那最后一次买入的时机,可以是如下情况:

最后一次买入的时机在5位置,那么dp[5][3] = dp[5][2] - arr[5] + arr[5]

最后一次买入的时机在4位置,那么dp[5][3] = dp[4][2] - arr[4] + arr[5]

最后一次买入的时机在3位置,那么dp[5][3] = dp[3][2] - arr[3] + arr[5]

最后一次买入的时机在2位置,那么dp[5][3] = dp[2][2] - arr[2] + arr[5]

最后一次买入的时机在1位置,那么dp[5][3] = dp[1][2] - arr[1] + arr[5]

最后一次买入的时机在0位置,那么dp[5][3] = dp[0][2] - arr[0] + arr[5]

我们求dp[4][3]这个值,我们可以枚举4位置是否参与交易,假设4位置不参与交易,那么dp[4][3] = dp[3][3],假设4位置参与交易,那么4位置一定是最后一次的卖出时机。那最后一次买入的时机,可以是如下情况:

最后一次买入的时机在4位置,那么dp[4][3] = dp[4][2] - arr[4] + arr[4]

最后一次买入的时机在3位置,那么dp[4][3] = dp[3][2] - arr[3] + arr[4]

最后一次买入的时机在2位置,那么dp[4][3] = dp[2][2] - arr[2] + arr[4]

最后一次买入的时机在1位置,那么dp[4][3] = dp[1][2] - arr[1] + arr[4]

最后一次买入的时机在0位置,那么dp[4][3] = dp[0][2] - arr[0] + arr[4]

比较dp[5][3]和dp[4][3]的依赖关系,可以得到如下结论:

假设在求dp[4][3]的过程中,以下递推式的最大值我们可以得到

dp[4][2] - arr[4]

dp[3][2] - arr[3]

dp[2][2] - arr[2]

dp[1][2] - arr[1]

dp[0][2] - arr[0]

我们把以上式子的最大值定义为best,那么

dp[5][3] = Math.max(dp[4][3],Math.max(dp[5][2] - arr[5] + arr[5], best + arr[5]))

所以dp[5][3]可以由dp[4][3]加速得到,

同理,

dp[4][3]可以通过dp[3][3]加速得到,

dp[3][3]可以通过dp[2][3]加速得到,

dp[2][3]可以通过dp[1][3]加速得到,

dp[1][3]可以很简单得出,dp[1][3]有如下几种可能性:

可能性1,1位置完全不参与,则

可能性2,1位置作为最后一次的卖出时机,买入时机是1位置

可能性3,1位置作为最后一次的卖出时机,买入时机是0位置

此时,best的值为

然后通过dp[1][3]加速dp[2][3],通过dp[2][3]加速dp[3][3]......,所以二维dp的填写方式是按列填,

先填dp[1][0],dp[1][2]一直到dp[1][k],填好第一列;

然后填dp[2][0],dp[2][1]一直到dp[2][k],填好第二列;

...

依次填好每一列,直到填完第N-1列。

枚举行为被优化,优化枚举后的完整代码如下:

主要思路:上一个问题中,令k=2就是本题的答案。

主要思路:因为有了冷冻期,所以每个位置的状态有如下三种:

定义三个数组,分别表示i位置这三种情况下的最大值是多少

显然有如下结论:

针对一个普遍位置i

最大收益就是如上三种方式的最大值。完整代码见:

由于三个数组有递推关系,所以可以用三个变量替换三个数组,做空间压缩,优化后的代码如下:

主要思路:由于没有冷冻期,所以在i位置的时候,状态只有两种

针对0位置

针对普遍位置i

完整代码如下:

同样的,两个数组都有递推关系,可以做空间压缩,简化后的代码如下:

原文链接:买卖股票的最佳时机系列问题 - Grey Zeng - 博客园

Ⅲ 请各位高手点一下二级C语言考试涉及什么算法。还有C语言常涉及那些算法。俺定高分奖赏!

如果你担心C二级过不去,做完南开100题就行,C二级涉及的稍微深一点(相比其它题)的是链表的基本操作(建立动态链表,查找,插入,删除),很基础,自己拿TC或VC试试就会了,还有递归函数,但是不要求你会编递归,只要能看懂,能推出最后的返回值就行。总之,很儿戏。
如果想做程序员,实际工作中学或者有天分的话自己钻。
下面是一个程序员的自述,希望能给你些启发:
这些日子我一直在写一个实时操作系统内核,已有小成了,等写完我会全部公开,希望能够为国内IT的发展尽自己一份微薄的力量。最近看到很多学生朋友和我当年一样没有方向,所以把我的经历写出来与大家共勉,希望能给刚入行的朋友们一点点帮助。

一转眼我在IT行业学习工作已经七年多了,这期间我做过网页,写过MIS、 数据库,应用程序,做过通信 >软件、硬件驱动、 协议栈,到现在做操作系统内核和IC相关开发,这中间走了很多弯路,也吃了不少苦。

我上的是一个三流的高校,就连同一个城市的人多数都不知道。因为学校不好也就没有指望能靠学校名气找一个好工作。所有的希望都寄托在自己的努力上了,大一开学前的假期我就开始了学习,记得我买的第一本书是《 计算机基础DOS3.0》,大家别吓着了,其实当时已经普及了DOS6.22了,只是我在书店里看到了DOS4.0,5.0,6.0的书,以为像英语那样是第四、五、六册,记得当时到处找DOS1.0,现在想想也幸好我没有找到:)开学前我学完了PASCAL,那时既没有 计算机也没有人可以请教,我连程序是什么的概念都没有,只好死记硬背 代码,然后拿纸写,我一直到大三才有了一台486,在这之前用纸写了多少程序我也记不清楚了,只知道最长的一个我拿A4大小的草稿纸写了30多页,我的C语言、C++、VC都是在这样的条件下 入门的。所以说条件是可以克服的,希望我的经历多少给条件艰苦的同学们一点信心。第一次上机是在我姐夫的机房,我的心情激动的无与伦比,但是一上机我立刻傻了眼,他们用的是英文版的in3.1,我的那点DOS 知识都见了鬼,上机提心吊胆的一阵瞎摸,一不小心把Word弄成了全屏,怎么都还不了原,当时真是心急如焚,我以为机器被我弄坏了。第一个C语言程序,就是那个经典的HelloWorld,我调了几个星期,上机机会非常少,也没有书告诉我开发环境(TC2.0)需要设置,而且开始我都不知道有编译器,我甚至自作聪明把写好的程序扩展名从.c改成.exe,结果可想而知。大一学完了C、X86的汇编、数据结构、C++。由于精力都花在自学上了,大一下四门课挂了彩,三类学校就是这点好,挂上一二十门也照样毕业。不过扯远点说,我那么刻苦都及不了格,可见我们国家的 计算机教育有多死板。

大二准备学VC和BC,当时难以取舍,后来选了VC,不为别的,只为书店里两本书,VC那本便宜6块钱。我的努力在班上无人能及,学的日夜不分,大三有了 计算机后更是如此,很多次父亲半夜教训我说我不要命了,我一直觉得自己基础差,记忆又不行,条件也不好,所以觉得只有多花点时间才能赶上别人。居然后来有许多朋友说我有学 计算机的天赋,让我哭笑不得。我用的是486,16M内存,1G硬盘,当时同学们的配置都是P166MMX,我安装一个indowsNT4.0需要一个通宵,编译一个BC5.0向导生成的程序需要近两个小时,我的显示器是个二手的,辐射非常大,开机屏幕冒火花,看起来很酷的:),有一次程序写的太久,觉得怎么白色的编辑器背景变成了紫色,以为显示器坏了,后来才发现眼睛不行了,不过说来也奇怪,到今天我的视力还能保持1.5,真是个奇迹。但是就是那台破机器陪伴了我两年,让我学会了VC、Delphi、SQLServer等。后来那台机器给我阿姨打字用,据她说一天她正打的开心,一股青烟夹着火苗从显示器钻出来,之后它才寿终正寝。

大三假期找了个机会在一个 计算机研究所实习,与其说实习不如说是做义工,工作了两个月一分钱没有拿。但是这两个月对我的发展帮助很大,让我早一步了解了社会,刚去的时候我当然是一窍不通,在那里我熟悉了网络,学会了Delphi和Oracle。由于工作很认真,得到了比较好的评价,在一位长者的引荐下,我开始和他们一起做项目,这使我在大三大四就有了自己的收入,大四又找了两家MIS公司兼职,虽然钱不多,但是在学生期间有1000多的收入我已经非常满足了,我终于用自己赚的钱把 计算机换了。大四下开始找工作,这时我的工作经验已经比较多(当然现在想想非常幼稚),开始听父母的想去那个研究所,实习过那个部门也希望我能去,但是不知道为什么最后不了了之,这种单位就是比较官僚,我一气之下就到了我兼职的一个公司做MIS的TeamLeader。在大三到毕业一年的时间,做过了各种MIS,从煤气、烟厂、公安、铁路、饮食到高校,什么有钱做什么,工作也很辛苦,经常加班和熬通宵,从跟客户谈需求到设计、编码、测试、交付都要上。那时觉得很有成就感,觉得自己还不错,现在想想真是很肤浅。

刚走上工作岗位的学生很容易被误导,各种开发 工具让人眼花缭乱,同时也觉得很受公司器重,但这样工作永远是一个低层次的开发者。不要跟我说什么系统分析有多么多么重要,多么多么难。你以为自己跟用户谈需求做设计就是系统分析和设计了吗,国内又有几个公司能够做的很到位很规范?我是ISO9000内审员,也在Rational公司受过多次培训,拿了4个证书,还有一个公司让我去做CMM。这些我听过很多,但是很多事情到国内就变了性质,一个公司不是通过了ISO9000或者CMM就能规范了,我现在在一家有几十年历史的外企工作,里面的管理不是一般国内企业能及的。作为一个毕业不久以前没有步入过社会的学生,几乎不可能在很短的时间掌握系统分析和设计,面向对象、UML只是一个 工具,关键是人本身的思想,不是说你熟悉了C++、Rose就能够做出好的设计,相反如果你具备了很高的素质,你可以用C写出比别人用C++更加模块化的程序。

话说远一些,国内 >软件开发行业有一个怪圈,很多人觉得VC>Delphi>VB,真是很搞笑。这几个 >软件我都做过开发,说白了他们都是 工具,应该根据应用的需要选择采用哪个,而不是觉得哪个上层次。如果你因为用某个开发 工具很有面子而选择的话,只能说明你很浅薄。如果说层次,那么这些 工具都不上层次,因为它们用来用去都是一些系统的API,微软的朋友不会因为你记住他们多少个API或者多少个类就会觉得你很了不起,你永远只是他们的客户,他们看重的是你口袋里的银子。我也做过系统内核,我也封装过很多API,同样我也不会看重那些使用这些API做二次开发的客户,除非他能够作出自己独到的设计。

至于有人认为C++>C那更是让人笑掉大牙,不妨你去打听一下,现在有几个操作系统内核是用C++写的,又有几个实时系统用的是C++,当然我也不是说C++不好,但是目前的内核和实时系统中C++还无法与C匹敌,至于说C++适合做应用系统的开发那是另外一回事。所以我的观点是不在于你用什么 工具和语言,而在于你干什么工作。你的设计体现了你的 技术层次。
这样干了一年我觉得非常苦闷,做的大多数都是熟练工种的活,个人 技术上没有太多的提高也看不到方向。所以决定离开这个城市去上海,寻求更好的发展,并且打算放弃我以前的MIS转到通信行业。

写到这里不能不提到我女朋友,我们是在来上海前半年认识的,她大四在我公司实习,公司派她给我写文档,我们的感情发展的很快。她告诉我很多事情,她家原本是改革开放的第一批暴发户,她母亲爱打牌,输掉了几百万,还欠了很多债,她有男朋友,但是她对他没有感情,只因为他给了她母亲两万多块钱,后来还强迫她写了四万块的借条,她男朋友背叛过她并且不止一次打她,现在逼她结婚不然就要她还钱。这人居然还是一个高校的老师!她母亲把父亲给她的学费花了,因为拖欠学费她没有办法拿到毕业证。她母亲现在有病需要钱,我拿出了自己的一点积蓄并且跟朋友们接了一些,替她交了学费并给她母亲看病(后来才知道看病的钱又不知所终,就连她母亲是不是有病我都不知道,但她也是没有办法)。这个时候我家知道了一些事情,坚决反对我和她在一起,她原来的男朋友也极力破坏。无奈之下我们决定早一定离开这个伤心的城市,并且瞒着我们家。由于时间仓促,我只准备了4000块钱,她仅有的几百块钱也被她母亲要去了,我买了三张票,一张是中午的,两张是晚上的,中午我的家人把我送上船,他们一离开我就下了船,我和她乘坐晚上的船离开了这个我和她生活了很多年的城市,带走的只是一身债务。没有来过上海的我们两个性倔强,都不愿意去麻烦同学和朋友。来到上海是傍晚6点半,我们都不知道该去哪里,我们找了一个20块钱的旅馆,这个房间连窗户都没有,7月份的天气酷热难耐,房间里非常闷热。第二天我们开始租房子,因为身上的钱不多,我们基本都是步行,花了一个星期时间,不知道在浦东转了多少圈后找到了一个400块的房子,但是我们都不了解上海是付三压一,还要付半个月的中介费,买了一些锅碗瓢盆后,我们身上只有800块钱了,工作都还没有着落,这800块钱要支持到我们拿到第一个月工资,为了省钱我们自己做饭,每天买菜只花两块钱,她非常喜欢吃(也可能她在大学经常挨饿的愿意),看到她现在这样省吃俭用我真的很不忍心。她以前的男朋友也没有放过她,经常打电话来骚扰,并且来上海看她,还说了不少恐吓她的话,她过于善良,说他以前毕竟帮助过她,叫我不要与他一般见识。以后的每天在家就是苦等面试通知,原本我想迅速找一家MIS公司解决眼前的困难,但是她坚持让我不要放弃自己的理想,终于功夫不负有心人,我找到了一家通信公司,4000块的工资虽然赶不上MIS公司给我开出的价位,但也够在上海生存。她也找到了工作,第一天上班她哭了,这是她来上海第一次流泪,我心里很难受也很感动。

由于是全新的行业,我把自己降到了零点,我学的VC、Delphi、 数据库派不上用场,摆在我面前的是嵌入式、协议、信令一些我从未接触过的 知识。我知道我没有退路,于是拼命的学习,我把自己当做一个应届毕业生一样,一分努力一分收获,半年过去我终于熟悉了工作,并且得到了公司的表彰,薪水也加了一级。后面的日子里我们省吃俭用,把欠朋友的1万多块钱还了,日子终于上了正轨。这时女朋友告诉我她想考研究生,我也很支持,于是她辞职在家备考。

另外,在这里我要感谢我的ProjectManager,他原来是一个大通信公司的产品经理,对人非常和善,我从他那里学到了很多 知识,而且他也给了我许许多多无私的帮助。在工作上他给我充分的空间和信任。记得公司安排我维护一个接入服务器 >软件,由于 代码量不算太小(5万行),资料和文档都不齐全,我维护起来非常吃力,所以想重新把它做一遍,公司领导不太支持,可能觉得工作量太大,但是他极力支持我,私下里他让我放手去做,我的维护工作他挤时间做。在他的支持下,我花了半年时间完成了接入服务器的 >软件,并且实现了一个相对完整的 TCP/IP 协议栈。在这里我学会了嵌入式系统设计、驱动开发、 TCP/IP和很多通信的 知识,我花了一年时间终于使自己从MIS开发转到了通信行业,并且站稳了脚跟。我的开发大量是对硬件的直接操作,不再受微软的操作系统,VC、Delhpi这些开发 工具的约束,我终于看到了另外一片天空。

我做事情喜欢追根问底,随着开发的深入, >软件开发与硬件联系越来越紧密,硬件 知识的匮乏又对我的发展产生了障碍,而且芯片 技术基本上掌握在国外公司的手里,这对做系统级设计是一个非常大的制约,一个新产品出来,第一道利润(也往往是最丰厚的利润)常常都被IC公司如Intel、Motorola赚去了,国内的厂商只能喝点汤。所以我决心解决自己的硬件 技术障碍,并打算离开通信行业,进入IC设计相关领域。
当然我明白如果我对硬件了解的非常少,没有哪家IC公司会仁慈到招我这样一个一窍不通的人来培训。所以我必须努力打好基础,学一些相关 知识为以后做准备。就像我开始从MIS转到通信一样,我看过大量通信方面的书,并且给一个ISP做过RADIUS计费分拣台,在这样的背景下这家通信公司才给了我这个机会。我在的通信公司是做系统设计的,有不少PCBLayout硬件人员,平常我就注意向他们学习,由于我做的是 >软件,在公司看硬件资料不好意思,所以开始只好在家看,刚来上海工作我连续一年都在加班,后来不加了,因为我要挤出时间学习,通常我12点左右睡,第二天5点半起,我上班比较早,地铁上如果人不多我也用来看书。学习当然不会是一帆风顺的,有些实在不懂的问题就积累起来问硬件人员,他们的帮助使我学习进度快了很多,因为在没有人点拨的情况下自学,我的一半时间是花在解决疑难问题上,但这种问题经常是别人的一句话就可以让我豁然开朗,我非常庆幸我有这样的学习环境。在后面的一年里,我学会了看硬件原理图,学会了简单的硬件设计(模拟电路方面还有不小的差距),事情就是这样的,当你安安份份做 >软件,别人永远认为你是 >软件开发人员,在你开始学习硬件时别人未必会认同,有位中兴通讯的朋友还对我说过,一个人不可能把所有东西都学完。我也明白这一点,但我希望自己做的更好。但当你熟悉硬件后大家又会觉得你好像原本就是软硬件都懂的,同事们也都习以为常了。这个时候我可以把硬件资料堂堂正正的拿到公司看,没有人再大惊小怪了。让我比较自豪的是我通过自己的努力做了一个IAD(软交换的终端设备)系统方案,包含软硬件的选型、设计等内容,这个方案得到了公司和同事们的认同,让我感到非常欣慰。

技术是相辅相成的,当我的硬件有了一定的进步后,我的 >软件设计也有了很大的提高,我可以从更深层次理解问题,我做的接入服务器CPU是MotorolaPowerPC860,熟悉的朋友都知道860QMC与 >软件的批量数据传输通常采用BD表的方式,硬件人员做驱动的时候习惯采用固定BD表,每接收或发送数据都将数据从BD表拷贝到用户Buffer,或从用户Buffer拷
贝到BD表,由于理解的比较深入,我自己重新实现了这个过程,采用动态BD表的方式,驱动从一个网口接收数据,提交给我的 >软件进行三层交换,直至从另外的接口发送出去,没有进行一次拷贝。这样的设计大大提高了性能,使系统的指标接近理论值。软硬件的结合使我的设计水平上了一个台阶。我现在写的这个操作系统,编译后我把程序反编译成汇编,找出其中不优化的 代码,然后在C程序中进行调整。举个例子,很多CPU没有专门的乘法指令,这个大家应该都知道,在这种CPU上进行一个乘法操作常常会花费大量的指令周期,有的朋友会说这个我知道,我会尽量避免采用×号,但是事情往往不是那么简单,你知道C语言中数组的下标操作是怎么实现的吗?仔细看看反汇编的 代码你就会明白,同样是通过下标的定位操作,C编译器会有时候会产生位移指令,但有时候会用乘法实现,两者效率往往是天壤之别,所以明白这些问题你才能将系统性能提升到极致。这些问题就不多说了,有兴趣的话以后可以共同探讨。
话说远一点,我由衷的希望在 >软件上做的比较深入的朋友们有机会学学硬件以及其它相关 知识,尤其是做底层开发和嵌入式设计的。这对 >软件 技术的提高有非常大的帮助,否则很多事情你只知道该这样但不会明白为什么该这样。我这个观点在我现在的IC公司ProjectManager那里也得到了验证。他告诉我们公司现在的802.11芯片产品的 >软件经理原本是做该芯片硬件设计的,某某某原本是做 >软件的,现在在做IC,类似的例子还有很多,只是在国内这样的风气不是非常流行。

我有一些心得体会与大家分享,只有当我干好本职工作后,我才会学习与工作关系不大的 技术,这样公司的上司才不至于反感,在 入门阶段的问题我通常不去问那些资深人士,而是问一些资历比较浅的朋友,比如刚毕业不久的学生,因为他们往往会跟你详细的讲解,而资深人士通常觉得你的问题太简单,所以回答的也很简单,我又不好意思多问。等 技术上了一定的层次后我才会问他们,他们也能给你比较深入的回答。另外,有些朋友说我机会比较好,他们也希望能从事新的工作可惜没有机会,我听了只有苦笑,我的机会了解的人都应该知道,我没有出生在什么IT世家:)也没有谁一路提拔我,所有的路都是自己走出来的,我母亲去世比较早,我的后母(我叫她阿姨)看着我努力过来的,一次她看我大年30还在写程序,她说像我这样努力木头都能学出来。

我的最终目的是IC而不是PCB,所以我下一步的准备开始学习IC设计的 知识。公司的同事没有懂IC设计的,后面的路又要靠自己了,我买了不少相关的书,在网上也查了很多的资料,我花了大量的时间去学习VHDL,并且用 >软件进行了一些简单的设计和仿真(没有设计ASIC,只是针对FPGA),随着学习的深入,我渐渐明白了IC设计的基本流程,同时也明白了这条路的艰辛。这个时候我已经做好了跳槽的准备,我向一家业界又一定知名度的IC设计公司投了简历,并通过了漫长的面试(4个多小时)。其他的一切我都比较满意,唯独薪资差强人意,我也明白原因,因为我是这个行业的新人,我没有经验,我再一次将自己清零了。公司老板问我6000多一个月能不能接受,我知道他也是照章办事。想想我通信行业的朋友们,基本上都是年薪10万以上,月薪过万的也比比皆是,朋友们也帮我介绍了不少待遇不错的公司,我该怎么选择,当时我很犹豫,我热爱我的事业,我向往我的追求,但我也是一个普通的人,我也需要养家糊口,我也想早一点买房买车。生活给我出了一道难题。

爱因斯坦在63岁时说过"一个人没有在30岁以前达成科学上的最大成就,那他永远都不会有。"这句话给了我很大的压力和震动,我马上就26岁了,离30只有四年时间,我必须抓紧这几年宝贵的时间,努力达到我 技术上的最高峰。为了这个理想,为了能离自己的梦更近一些,我选择了这家IC公司,我明白自己的薪资和公司刚进来的硕士研究生相差无几,但为了今后的发展只能忍受,一切又得重新开始。换行业是一个非常痛苦的过程,尤其从一个春风得意的位置换到一个陌生的岗位,感觉象从温暖的被子里钻出来跳进冰水中,让人难以接受。在原来那家通信公司,我是唯一两年时间涨了五次工资的员工,公司和同事都给了我极大的认可,工作上也常常被委以重任。但现在这一切都成了过去,在新的公司我只是一个新人,没有人知道也没有人在意我过去的成绩。我决定重新开始,我把自己看作新毕业的学生,我要用自己的努力得到公司的认可。进入新的行业是非常痛苦的,我告诉自己必须忍受这一切,虽然外面有很多诱惑,但是既然作出了选择我就不允许自己轻易放弃。

我现在已经在这家新公司上了一个多月的班,开始非常艰难,现在慢慢适应了。第一个月结束时,TeamLeader找我谈话,说我是新进员工中最优秀的一个,我心里很欣慰,这也算对我努力的一个肯定吧。在这里还要感谢我的女朋友,她给了我很大的支持和鼓舞,每次在我动摇的时候她都在鼓励我,让我坚持自己的理想,刚来上海是她让我不要勉强去做MIS,这次也是她让我顶住了月薪过万的诱惑,没有她我可能不会有今天的成绩。

现在的公司有自己的操作系统,自己的CPU、DSP和其它芯片,在这里我能学到世界上最先进的 技术,我们的设计开发不再完全依赖别人的硬件和系统,这让我很开心。我打算等工作步入正轨后,全力学习新的 知识,实现我的理想。

在后面的两年里我给自己定下了几个目标:

一.努力做好本职工作,在工作上得到公司和同事们的认同;
二.努力学习IC硬件设计 知识,多向同事请教,并利用一切机会多实践;
三.实现我的实时操作系统的主要部分,完成 TCP/IP 协议栈模块,并免费发布源 代码;
四.和我女朋友结婚并买一套小房子,这是最重要的,因为我明白事业是可以重来的,但是珍贵的感情很难失而复得。

在这里提一下我现在开发的操作系统,它是一个实时嵌入式系统,目前支持以下特性:

a.支持时间片轮转调度和基于优先级调度,最多64个优先级;
b.抢占式实时内核;
c.为了便于移植,主体用标准C实现;
d.汇编 代码非常少,不到100行;
e.支持任务管理,各任务有独立的堆栈;
f.进程同步和通信目前完成了Semaphore,MessageQueue正在调试;
g.实现了定时系统调用;
h.可以在windows上仿真调试
我还打算下一步实现优先级反转保护,EventFlag,DataPipe,内存管理(以前实现过)、驱动接口等。在这之后我还会努力完善它,比如加入文件系统, 协议栈、调试接口等。希望朋友们提出自己的意见和建议,在此不胜感激!

后记:
就像有的朋友说的,我的经历或许会给一些朋友产生误导,在这里我必须说明一下。我来上海以前学习过于拼命,常常晚上只睡3个多小时,我身高1米71,那时只有108斤(我现在130多),家人也说我这样拼命活不过60岁,但是当时的我太固执,我对他们说只要能实现理想活50岁我就够了。那时的拼命使我的身体受到了影响,有一次早上突然腰肌剧痛难忍,痛的我倒在床上站不起来。虽然我现在已经比较注意,但有时候还会隐隐作痛。后来在女朋友说服了我,来上海以后我不再如此。我经常引用父亲的一句话"身体是革命的本钱"。

而且我也发现拼命不是办法,我可以熬一两个通宵,最多的一次我连续工作了三天三夜,但是我半个月都没有恢复过来,这样是不是得不偿失?学习工作应该是一个长期的过程,像马拉松而不是百米冲刺。我现在非常注意调整学习和工作的强度,我要保证每天尽量有相对充沛的精力,一些年轻的朋友觉得自己也应该拼命努力,这让我多少有些担心,如果我的故事能让你在学习工作上多一点兴趣,我会感到很开心,但如果误导了某些朋友,让你做一些不值得的付出,我会感到很内疚。

技术没有贵贱只分,我以前换行业是因为自己的兴趣所致,而不是对哪个行业有什么偏见。我希望我的经历不要给朋友一个错误的导向,觉得我始终向更高的 技术发展。其实各行各业做到顶尖都是很困难的。话又说回来虽然 技术没有贵贱,但是门槛是有高低的,无论如何,做IC的门槛要比做网页的高,这一点无可否认。国家各种人才都是需要的,但是作为个人奋发向上的想法还是应该有的,努力在自己喜欢的行业上做的更好,而不应该停留在比较肤浅的层次上。
我是一个自己觉得比较有自知之明的人,或许我最大的优点就是知道自己有很多缺点:)。我的故事中很多的曲折和错误都是由我的缺点造成的,希望大家用审慎的眼光看待我的经历,不要被我的"花言巧语"所迷惑。我学习有些随心所欲,这给我带来了无尽的麻烦,也大大阻碍的我的发展。记得我小时候成绩比较出色,但是后来学习严重偏科,导致我中学成绩一再滑坡,也没有考上什么好的学校,小时候的一个朋友,当时的成绩和我相仿,但是没有我这个缺点,她上了清华,后来在去了美国深造,在一个着名导师手下研究理论科学,这未尝不是一条更好的出路。另外我的学习方法也是在不断改善中的,过去的学习过于讲究数量和时间,那样学习既苦而已效率不高,现在我非常注意学习的效率和技巧,这样才是学习的捷径(当然不是指投机取巧),比如说学一相对陌生的 技术,如果有条件,不妨问一问有经验的人,不需要问很多,往往他不经意的几句话会给你非常大的帮助,甚至超过你看一个星期的书。带着这样的思想再去学习你会节省很多时间,这样何乐不为呢?这些年中我学了不少的东西,由于开始非常盲目,所以学的东西杂乱无章,现在回想起来让我啼笑皆非,我把大量的时间浪费在一些没有必要深入了解的 知识上,毕竟一个人的精力是有限度的。很多朋友很我一样都背过五笔字形,的确它是个不错的输入法,但是对一个研发人员它绝对不值得你去背,你的时间应该花在有价值的地方。我这样的事情还做过很多,我背过CCED、WPS的命令和快捷键,在dBase基本退出历史舞台后我还花了很多时间去学习它的使用。所以我的学习在前期缺乏规划,没有明确的短期目的、中期目标,只有一个虚无飘渺的长期的理想。这就像做设计一样,好的设计是从需求抽象到 代码有很多过程,而不能得到了需求就立刻开始开始编码。
最后赠你一句心法吧:只要梦想能够延续,心愿总会有实现的一天。

Ⅳ c++链表反转。有要求

#include<cstdio>

#include<cstdlib>

struct Node{

char name[50];

int salary;

Node* next;

};

Node* createNode()

{

Node* lastnode = 0;

Node* head = 0;

int num;printf("请输入员工野凳的个数:");scanf("%d",&num);

char buf[50];

for(int i=0;i<num;++i)

{

Node* node = new Node;

printf("请输入员闹念工%d的名字液脊困:",i+1);scanf("%s",node->name);

printf("请输入员工%d的工资:",i+1);scanf("%s",buf);node->salary=atoi(buf);

if(lastnode!=0) lastnode->next=node; else head=node;

lastnode=node;

}

lastnode->next=0;

return head;

}

Node* reverseNode(Node* head)

{

Node* iter = head;

Node* lastiter = 0;

while(iter!=0)

{

Node* temp=iter->next;

iter->next=lastiter;

lastiter=iter;

iter=temp;

}

return lastiter;

}

Node* printNode(Node* head)

{

Node* iter = head;

while(iter!=0)

{

printf("员工的名字:%s 员工的工资:%d ",iter->name, iter->salary);

iter=iter->next;

}

}



int main()

{

Node* head = createNode();

printNode(head);

printf("反转: ");

Node* head2 = reverseNode(head);

printNode(head2);

return 0;

}

Ⅳ 数据结构面试常见问题

数据结构面试常见问题

数据结构是计算机存储、组织数据的方式。数据结构是指相互之间存在一种或多种特定关系的数据元素的集合。下面就是我整理的数据结构面试常见问题,一起来看一下吧。

数据结构面试常见问题 篇1

数据结构与算法,这个部分的内容其实是十分的庞大,要想都覆盖到不太容易。在校学习阶段我们可能需要对每种结构,每种算法都学习,但是找工作笔试或者面试的燃银坦时候,要在很短的时间内考察一个人这方面的能力,把每种结构和算法都问一遍不太现实。所以,实际的情况是,企业一般考察一些看起来很基本的概念和算法,或者是一些变形,然后让你去实现。搏弯也许看起来简单,但是如果真让你在纸上或者是计算机上快速地完成一个算法,并且设计测试案例,最后跑起来,你就会发现会很难了。这就要求我们要熟悉,并牢固掌握常用的算法,特别是那些看起来貌似简单的算法,正是这些用起来很普遍的算法,才要求我们能很扎实的掌握,在实际工作中提高工作效率。遇到复杂的算法,通过分析和扎实的基本功,应该可以很快地进行开发。

闲话少说,下面进入正题。

一.数据结构部分

1.数组和链表的区别。(很简单,但是很常考,记得要回答全面)

C++语言中可以用数组处理一组数据类型相同的数据,但不允许动态定义数组的大小,即在使用数组之前必须确定数组的大小。而在实际应用中,用户使用数组之前有时无法准确确定数组的大小,只能将数组定义成足够大小,这样数组中有些空间可能不被使用,从而造成内存空间的浪费。链表是一种常见的数据组织形式,它采用动态分配内存的形式实现。需要时可以用new分配内存空间,不需要时用将已分配的空间释放,不会造成内存空间的浪费。

从逻辑结构来看:数组必须事先定义固定的长度(元素个数),不能适应数据动态地增减的情况,即数组的大小一旦定义就不能改变。当数据增加时,可能超出原先定义的元素个数;当数据减少时,造成内存浪费;链表动态地进行存储分配,可以适应数据动态地增减的.情况,且可以方便地插入、删除数据项。(数组中插入、删除数据项时,需要移动其它数据项)。

从内存存储来看:(静态)数组从栈中分配空间(用NEW创建的在堆中), 对于程序员方便快速,但是自由度小;链表从堆中分配空间, 自由度大但是申请管理比较麻烦.

1.从访问方式来看:数组在内存中是连续存储的,因此,可以利用下标索引进行随机访问;链表是链式存储结构,在访问元素的时候只能通过线性的方式由前到后顺序访问,所以访问效率比数组要低。

2.链表的一些操作,如链表的反转,链表存在环路的判断(快慢指针),双向链表,循环链表相关操作。

3.队列(特殊的如优先级队列),栈的应用。(比如队列用在消息队列,栈用在递归调用中)

4.二叉树的基本操作

二叉树的三种遍历方式(前序,中序,后序)及其递归和非递归实皮桐现,三种遍历方式的主要应用(如后缀表达式等)。相关操作的时间复杂度。

5.字符串相关

整数,浮点数和字符串之间的转换(atoi,atof,itoa)

字符串拷贝注意异常检查,比如空指针,字符串重叠,自赋值,字符串结束符'/0'等。

二.算法部分

1.排序算法:

排序可以算是最基本的,最常用的算法,也是笔试面试中最常被考察到的算法。最基本的冒泡排序,选择排序,插入排序要可以很快的用代码实现,这些主要考察你的实际编码能力。堆排序,归并排序,快排序,这些算法需要熟悉主要的思想,和需要注意的细节地方。需要熟悉常用排序算法的时间和空间复杂度。

各种排序算法的使用范围总结:

(1)当数据规模较小的时候,可以用简单的排序算法如直接插入排序或直接选择排序。

(2)当文件的初态已经基本有序时,可以用直接插入排序或冒泡排序。

(3)当数据规模比较大时,应用速度快的排序算法。可以考虑用快速排序。当记录随机分布的时候,快排的平均时间最短,但可能出现最坏的情况,这时候的时间复杂度是O(n^2),且递归深度为n,所需的栈空间问O(n)。

(4)堆排序不会出现快排那样的最坏情况,且堆排序所需的辅助空间比快排要少。但这两种算法都不是稳定的,若要求排序时稳定的,可以考虑用归并排序。

(5)归并排序可以用于内排序,也可以用于外排序。在外排序时,通常采用多路归并,并且通过解决长顺串的合并,产生长的初始串,提高主机与外设并行能力等措施,以减少访问外存额次数,提高外排序的效率。

2,查找算法

能够熟练写出或者是上机编码出二分查找的程序。

3.hash算法

4.一些算法设计思想。

贪心算法,分治算法,动态规划算法,随机化算法,回溯算法等。这些可以根据具体的例子程序来复习。

5.STL

STL(Standard Template Library)是一个C++领域中,用模版技术实现的数据结构和算法库,已经包含在了C++标准库中。其中的vecor,list,stack,queue等结构不仅拥有更强大的功能,还有了更高的安全性。除了数据结构外,STL还包含泛化了的迭代器,和运行在迭代器上的各种实用算法。这些对于对性能要求不是太高,但又不希望自己从底层实现算法的应用还是很具有诱惑力的。

数据结构面试常见问题 篇2

1. 什么是数据结构?

数据结构是数据组织(存储)和操作进行检索和访问的方式。它还定义了不同数据集相互关联、建立关系和形成算法的方式。

2. 描述数据结构的类型?

列表:链接到先前或/和后续数据项的相关事物的集合。

数组:所有相同的值的集合。

Records:字段的集合,每个字段都包含来自单一数据类型的数据。

树:在分层框架中组织数据的数据结构。这种形式的数据结构遵循数据项插入、删除和修改的顺序。

表格:数据以行和列的形式保存。这些与记录相当,因为数据的结果或更改反映在整个表中。

3. 什么是线性数据结构?请举例

如果数据结构的所有元素或数据项都按顺序或线性顺序排列,则数据结构是线性的。元素以非分层方式存储,因此除了列表中的第一个和最后一个元素外,每个项目都有后继者和前驱者。数组、堆栈、字符串、队列和链表,都属于线性数据结构。

4. 数据结构有哪些应用?

数值分析、操作系统、人工智能、编译器设计、数据库管理、图形、统计分析和仿真。

5、文件结构和存储结构有什么区别?

区别在于访问的内存区域。存储结构是指计算机系统内存中的数据结构,而文件结构是指辅助存储器中的存储结构。

6、什么是多维数组?

多维数组的意思是指三维或者三维以上的数组。 三维数组具有高、宽、深的概念,或者说行、列、层的概念,即数组嵌套数组达到三维及其以上。是最常见的多维数组,由于其可以用来描述三维空间中的位置或状态而被广泛使用。

7. 什么是链表数据结构?

这是最常见的数据结构面试问题之一,面试官希望你能给出全面的答案。尝试尽可能多地解释,而不是用一句话来完成你的答案!

它是一个线性数据结构或一系列数据对象,其中元素不存储在相邻的内存位置。元素使用指针链接以形成链。每个元素都是一个单独的对象,称为节点。每个节点有两项:数据字段和对下一个节点的引用。链表中的入口点称为头。如果列表为空,则头部为空引用,最后一个节点具有对空的引用。

一个链表是一个动态的数据结构,其中节点的数量是不固定的,这样的例子有扩大和缩小需求的能力。

它适用于以下情况:

我们处理未知数量的对象或不知道列表中有多少项目;

我们需要从列表中进行恒定时间的插入/删除,就像在时间可预测性至关重要的实时计算中一样;

不需要随机访问任何元素;

该算法需要一个数据结构,无论对象在内存中的物理地址如何,都需要在其中存储对象;

我们需要在列表中间插入项目,就像在优先队列中一样;

一些实现是堆栈和队列、图形、名称目录、动态内存分配以及对长整数执行算术运算

8.什么是双向链表?请举例

它是链表的一种复杂类型(双端 LL),其中一个节点有两个链接,一个连接到序列中的下一个节点,另一个连接到前一个节点。这允许在两个方向上遍历数据元素。

举例:

带有下一个和上一个导航按钮的音乐播放列表

具有 BACK-FORWARD 访问页面的浏览器缓存

浏览器上的撤消功能

9. 为什么要做算法分析?

一个问题可以使用多种解决算法以多种方式解决。算法分析提供对算法所需资源的估计,以解决特定的计算问题。还确定了执行所需的时间和空间资源量。

算法的时间复杂度量化了算法运行所花费的时间,作为输入长度的函数。空间复杂度量化了算法占用的空间或内存量,以作为输入长度的函数运行。

;

Ⅵ 程序员面试笔试宝典的目录

前言
上篇 面试笔试经验技巧篇
第1章 面试官箴言 2
1.1 有道无术,术可求;有术无道,止于术 2
1.2 求精不求全 3
1.3 脚踏实地,培养多种技能 4
1.4 保持空杯心态 6
1.5 职场是能者的舞台 7
1.6 学会“纸上谈兵” 8
1.7 小结 8
第2章 面试心得交流 9
2.1 心态决定一切 9
2.2 假话全不说,真话不全说 10
2.3 走自己的路,让别人去说吧 12
2.4 夯实基础谋出路 14
2.5 书中自有编程法 15
2.6 笔试成绩好,不会被鄙视 17
2.7 不要一厢情愿做公司的备胎 18
2.8 小结 19
第3章 企业面试笔试攻略 20
3.1 互联网企业 20
3.2 网络设备提供商 25
3.3 外企 29
3.4 国企 32
3.5 研究所 35
3.6 创业型企业 37
3.7 如何抉择 41
第4章 面试笔试技巧 42
4.1 不打无准备之仗 42
4.1.1 如何获取求职信息 42
4.1.2 如何制作一份受用人单位青睐的简历 43
4.1.3 如何高效地网申简历 47
4.1.4 面试考查什么内容 48
4.1.5 霸王面合适吗 50
4.1.6 非技术类笔试如何应答 50
4.1.7 什么是职场暗语 51
4.1.8 如何克服面试中的紧张情绪 54
4.1.9 面试礼仪有哪些 55
4.1.10 面试需要准备什么内容 56
4.1.11 女生适合做程序员吗 57
4.1.12 程序员是吃青春饭的吗 58
4.1.13 为什么会被企业拒绝 58
4.1.14 如何准备集体面试 59
4.1.15 如何准备电话面试 61
4.2 从容应对 62
4.2.1 如何进行自我介绍 63
4.2.2 你对我们公司有什么了解 64
4.2.3 如何应对自己不会回答的问题 65
4.2.4 如何应对面试官的“激将法”语言 65
4.2.5 如何处理与面试官持不同观点的问题 66
4.2.6 如果你在这次面试中没有被录用,你会怎么办 66
4.2.7 如果你被我们录取了,接下来你将如何开展工作 66
4.2.8 你怎么理解你应聘的职位 67
4.2.9 你有哪些缺点 67
4.2.10 你有哪些优点 68
4.2.11 你没有工作经验,如何能够胜任这个岗位 69
4.2.12 你的好朋友是如何评价你的 69
4.2.13 你与上司意见不一致时,该怎么办 70
4.2.14 你能说说你的家庭吗 71
4.2.15 你认为自己最适合做什么 72
4.2.16 你如何看待公司的加班现象 72
4.2.17 你的业余爱好是什么 73
4.2.18 你和别人发生过争执吗?你怎样解决 74
4.2.19 你如何面对压力 74
4.2.20 你为什么离开了原来的单位 75
4.2.21 你为什么更倾向于我们公司 75
4.2.22 你觉得我们为什么要录用你 76
4.2.23 你的职业规划是什么 76
4.2.24 你对薪资有什么要求 77
4.2.25 你有什么需要问我的问题吗 77
4.3 签约这点事 78
4.3.1 风萧萧兮易水寒,offer多了怎么办 78
4.3.2 签约、违约需要注意哪些事项 78
4.4 小结 81
第5章 英文面试攻略 82
5.1 注意事项 82
5.2 英文自我介绍 83
5.3 常见的英文面试问题 85
5.4 常见计算机专业词汇 94
5.4.1 计算机专业相关课程 94
5.4.2 操作系统相关术语 95
5.4.3 算法相关术语 96
5.4.4 数据结构相关术语 97
5.4.5 计算机网络相关术语 100
第6章 智力题攻略 102
6.1 推理类 102
6.2 博弈类 107
6.3 计算类 109
6.4 作图类 111
6.5 倒水类 112
6.6 称重类 113
6.7 最优化类 114
6.8 IT思想类 115
6.9 过桥类 118
6.10 概率类 119
下篇 面试笔试技术攻克篇
第7章 程序设计基础 122
7.1 C/C++关键字 122
7.1.1 static(静态)变量有什么作用 122
7.1.2 const有哪些作用 124
7.1.3 switch语句中的case结尾是否必须添加break语句?为什么 127
7.1.4 volatile在程序设计中有什么作用 128
7.1.5 断言ASSERT( )是什么 129
7.1.6 枚举变量的值如何计算 130
7.1.7 char str1[] = abc; char str2[] = abc; str1与str2不相等,为什么 130
7.1.8 为什么有时候main( )函数会带参数?参数argc与argv的含义是什么 131
7.1.9 C++里面是不是所有的动作都是main( )函数引起的 132
7.1.10 *p++与(*p)++等价吗?为什么 132
7.1.11 前置运算与后置运算有什么区别 132
7.1.12 a是变量,执行(a++) += a语句是否合法 133
7.1.13 如何进行float、bool、int、指针变量与“零值”的比较 134
7.1.14 new/delete与malloc/free的区别是什么 135
7.1.15 什么时候需要将引用作为返回值 137
7.1.16 变量名为618Software是否合法 137
7.1.17 C语言中,整型变量x小于0,是否可知x×2也小于0 138
7.1.18 exit(status)是否跟从main( )函数返回的status等价 138
7.1.19 已知String类定义,如何实现其函数体 138
7.1.20 在C++中如何实现模板函数的外部调用 140
7.1.21 在C++中,关键字explicit有什么作用 140
7.1.22 C++中异常的处理方法以及使用了哪些关键字 141
7.1.23 如何定义和实现一个类的成员函数为回调函数 141
7.2 内存分配 142
7.2.1 内存分配的形式有哪些 142
7.2.2 什么是内存泄露 143
7.2.3 栈空间的最大值是多少 144
7.2.4 什么是缓冲区溢出 144
7.3 sizeof 146
7.3.1 sizeof是关键字吗 146
7.3.2 strlen()=?sizeof()=? 146
7.3.3 对于结构体而言,为什么sizeof返回的值一般大于期望值 148
7.3.4 指针进行强制类型转换后与地址进行加法运算,结果是什么 149
7.4 指针 150
7.4.1 使用指针有哪些好处 150
7.4.2 引用还是指针 150
7.4.3 指针和数组是否表示同一概念 152
7.4.4 指针是否可进行>、<、>=、<=、==运算 152
7.4.5 指针与数字相加的结果是什么 152
7.4.6 野指针?空指针 153
7.5 预处理 154
7.5.1 C/C++头文件中的ifndef/define/endif的作用有哪些 154
7.5.2 #include <filename.h>和#include “filename.h” 有什么区别 155
7.5.3 #define有哪些缺陷 155
7.5.4 如何使用define 声明一个常数,用以表明1年中有多少秒(忽略闰年问题) 155
7.5.5 含参数的宏与函数有什么区别 156
7.5.6 宏定义平方运算#define SQR(X) X*X是否正确 156
7.5.7 不能使用大于、小于、if语句,如何定义一个宏来比较两个数a、b的大小 157
7.5.8 如何判断一个变量是有符号数还是无符号数 158
7.5.9 #define TRACE(S) (printf(%s , #S), S)是什么意思 159
7.5.10 不使用sizeof,如何求int占用的字节数 160
7.5.11 如何使用宏求结构体的内存偏移地址 161
7.5.12 如何用sizeof判断数组中有多少个元素 162
7.5.13 枚举和define有什么不同 162
7.5.14 typdef和define有什么区别 162
7.5.15 C++中宏定义与内联函数有什么区别 164
7.5.16 定义常量谁更好?#define还是const 164
7.6 结构体与类 165
7.6.1 C语言中struct与union的区别是什么 165
7.6.2 C和C++中struct的区别是什么 165
7.6.3 C++中struct与class的区别是什么 166
7.7 位操作 166
7.7.1 一些结构声明中的冒号和数字是什么意思 166
7.7.2 最有效的计算2乘以8的方法是什么 167
7.7.3 如何实现位操作求两个数的平均值 167
7.7.4 unsigned int i=3;printf(%u ,i*-1)输出为多少 168
7.7.5 如何求解整型数的二进制表示中1的个数 169
7.7.6 不能用sizeof( )函数,如何判断操作系统是16位还是32位的 170
7.7.7 嵌入式编程中,什么是大端?什么是小端 171
7.7.8 考虑n位二进制数,有多少个数中不存在两个相邻的1 174
7.7.9 不用除法操作符如何实现两个正整数的除法 175
7.8 函数 179
7.8.1 怎么样写一个接受可变参数的函数 179
7.8.2 函数指针与指针函数有什么区别 179
7.8.3 C++函数传递参数的方式有哪些 183
7.8.4 重载与覆盖有什么区别 185
7.8.5 是否可以通过绝对内存地址进行参数赋值与函数调用 188
7.8.6 默认构造函数是否可以调用单参数构造函数 190
7.8.7 C++中函数调用有哪几种方式 191
7.8.8 什么是可重入函数?C语言中如何写可重入函数 192
7.9 数组 192
7.9.1 int a[2][2]={{1},{2,3}},则a[0][1]的值是多少 192
7.9.2 如何合法表示二维数组 193
7.9.3 a是数组,(int*)(&a+1)表示什么意思 193
7.9.4 不使用流程控制语句,如何打印出1~1000的整数 194
7.9.5 char str[1024]; scanf(%s,str)是否安全 197
7.9.6 行存储与列存储中哪种存储效率高 197
7.10 变量 197
7.10.1 全局变量和静态变量有什么异同 197
7.10.2 局部变量需要“避讳”全局变量吗 199
7.10.3 如何建立和理解非常复杂的声明 199
7.10.4 变量定义与变量声明有什么区别 200
7.10.5 不使用第三方变量,如何交换两个变量的值 201
7.10.6 C与C++变量初始化有什么不同 202
7.11 字符串 202
7.11.1 不使用C/C++字符串库函数,如何自行编写strcpy( )函数 203
7.11.2 如何把数字转换成字符串 205
7.11.3 如何自定义内存复制函数memcpy( ) 206
7.12 编译 207
7.12.1 编译和链接的区别是什么 207
7.12.2 编译型语言与解释型语言的区别是什么 208
7.12.3 如何判断一段程序是由C编译程序还是由C++编译程序编译的 208
7.12.4 在C++程序中调用被C编译器编译后的函数,为什么要加extern “C” 209
7.12.5 两段代码共存于一个文件,编译时有选择地编译其中的一部分,如何实现 210
7.13 面向对象相关 210
7.13.1 面向对象与面向过程有什么区别 210
7.13.2 面向对象的基本特征有哪些 211
7.13.3 什么是深复制?什么是浅复制 212
7.13.4 什么是友元 213
7.13.5 复制构造函数与赋值运算符的区别是什么 214
7.13.6 基类的构造函数/析构函数是否能被派生类继承 216
7.13.7 初始化列表和构造函数初始化的区别是什么 216
7.13.8 类的成员变量的初始化顺序是按照声明顺序吗 217
7.13.9 当一个类为另一个类的成员变量时,如何对其进行初始化 217
7.13.10 C++能设计实现一个不能被继承的类吗 218
7.13.11 构造函数没有返回值,那么如何得知对象是否构造成功 219
7.13.12 C++中的空类默认产生哪些成员函数 219
7.13.13 如何设置类的构造函数的可见性 219
7.13.14 public继承、protected继承、private继承的区别是什么 220
7.13.15 C++提供默认参数的函数吗 221
7.13.16 C++中有哪些情况只能用初始化列表而不能用赋值 222
7.14 虚函数 223
7.14.1 什么是虚函数 223
7.14.2 C++如何实现多态 225
7.14.3 C++中继承、虚函数、纯虚函数分别指的是什么 226
7.14.4 C++中的多态种类有哪几种 226
7.14.5 什么函数不能声明为虚函数 227
7.14.6 是否可以把每个函数都声明为虚函数 229
7.14.7 C++中如何阻止一个类被实例化 229
7.15 编程技巧 229
7.15.1 当while( )的循环条件是赋值语句时会出现什么情况 229
7.15.2 不使用if/:?/switch及其他判断语句如何找出两个int型变量中的最大值和最小值 230
7.15.3 C语言获取文件大小的函数是什么 231
7.15.4 表达式a>b>c是什么意思 231
7.15.5 如何打印自身代码 232
7.15.6 如何实现一个最简单病毒 232
7.15.7 如何只使用一条语句实现x是否为2的若干次幂的判断 233
7.15.8 如何定义一对相互引用的结构 233
7.15.9 什么是逗号表达式 234
7.15.10 是否与 等价 235
7.15.11 什么是短路求值 235
7.15.12 已知随机数函数rand7( ),如何构造rand10( )函数 236
7.15.13 printf(%p ,(void *)x)与printf (%p ,&x)有何区别 237
7.15.14 printf( )函数是否有返回值 237
7.15.15 不能使用任何变量,如何实现计算字符串长度函数Strlen( ) 237
7.15.16 负数除法与正数除法的运算原理是否一样 238
7.15.17 main( )主函数执行完毕后,是否可能会再执行一段代码 238
第8章 数据库 240
8.1 数据库概念 240
8.1.1 关系数据库系统与文件数据库系统有什么区别 240
8.1.2 SQL语言的功能有哪些 240
8.1.3 内连接与外连接有什么区别 242
8.1.4 什么是事务 243
8.1.5 什么是存储过程?它与函数有什么区别与联系 244
8.1.6 什么是主键?什么是外键 244
8.1.7 什么是死锁 245
8.1.8 什么是共享锁?什么是互斥锁 245
8.1.9 一二三四范式有何区别 246
8.1.10 如何取出表中指定区间的记录 247
8.1.11 什么是CHECK约束 247
8.1.12 什么是视图 247
8.2 SQL高级应用 248
8.2.1 什么是触发器 248
8.2.2 什么是索引 249
8.2.3 什么是回滚 250
8.2.4 数据备份有哪些种类 251
8.2.5 什么是游标 251
8.2.6 并发环境下如何保证数据的一致性 252
8.2.7 如果数据库日志满了,会出现什么情况 252
8.2.8 如何判断谁往数据库中插入了一行数据 252
第9章 网络与通信 254
9.1 网络模型 254
9.1.1 OSI七层模型是什么 254
9.1.2 TCP/IP模型是什么 255
9.1.3 B/S与C/S有什么区别 255
9.1.4 MVC模型结构是什么 256
9.2 网络设备 258
9.2.1 交换机与路由器有什么区别 258
9.2.2 路由表的功能有哪些 259
9.3 网络协议 260
9.3.1 TCP和UDP的区别有哪些 260
9.3.2 什么叫三次握手?什么叫四次断开 260
9.3.3 什么是ARP/RARP 262
9.3.4 IP Phone的原理是什么?都用了哪些协议 263
9.3.5 Ping命令是什么 263
9.3.6 基本的HTTP流程有哪些 264
9.4 网络编程 264
9.4.1 如何使用Socket编程 264
9.4.2 阻塞模式和非阻塞模式有什么区别 265
9.5 网络其他问题 266
9.5.1 常用的网络安全防护措施有哪些 266
9.5.2 什么是SQL注入式攻击 267
9.5.3 电路交换技术、报文交换技术和分组交换技术有什么区别 268
9.5.4 相比IPv4,IPv6有什么优点 269
第10章 操作系统 270
10.1 进程管理 270
10.1.1 进程与线程有什么区别 270
10.1.2 线程同步有哪些机制 271
10.1.3 内核线程和用户线程的区别 271
10.2 内存管理 272
10.2.1 内存管理有哪几种方式 272
10.2.2 分段和分页的区别是什么 272
10.2.3 什么是虚拟内存 272
10.2.4 什么是内存碎片?什么是内碎片?什么是外碎片 273
10.2.5 虚拟地址、逻辑地址、线性地址、物理地址有什么区别 273
10.2.6 Cache替换算法有哪些 274
10.3 用户编程接口 275
10.3.1 库函数与系统调用有什么不同 275
10.3.2 静态链接与动态链接有什么区别 276
10.3.3 静态链接库与动态链接库有什么区别 276
10.3.4 用户态和核心态有什么区别 276
10.3.5 用户栈与内核栈有什么区别 277
第11章 软件工程 278
11.1 软件工程过程与方法 278
11.1.1 软件工程过程有哪些 278
11.1.2 常见的软件开发过程模型有哪些 279
11.1.3 什么是敏捷开发 283
11.1.4 UML中一般有哪些图 285
11.2 软件工程思想 285
11.2.1 什么是软件配置管理 285
11.2.2 什么是CMMI 286
11.2.3 如何提高软件质量 287
第12章 发散思维 289
12.1 设计模式 289
12.1.1 什么是单例模式 289
12.1.2 什么是工厂模式 290
12.1.3 什么是适配器模式 290
12.1.4 什么是享元模式 291
12.1.5 什么是观察者模式 291
12.2 新技术 291
12.2.1 什么是云计算 291
12.2.2 什么是物联网 292
12.2.3 你平时读的专业书籍有哪些 293
第13章 数据结构与算法 295
13.1 数组 295
13.1.1 如何用递归实现数组求和 295
13.1.2 如何用一个for循环打印出一个二维数组 296
13.1.3 在顺序表中插入和删除一个结点平均移动多少个结点 297
13.1.4 如何用递归算法判断一个数组是否是递增 297
13.1.5 如何分别使用递归与非递归实现二分查找算法 298
13.1.6 如何在排序数组中,找出给定数字出现的次数 299
13.1.7 如何计算两个有序整型数组的交集 300
13.1.8 如何找出数组中重复次数最多的数 301
13.1.9 如何在O(n)的时间复杂度内找出数组中出现次数超过了一半的数 303
13.1.10 如何找出数组中唯一的重复元素 305
13.1.11 如何判断一个数组中的数值是否连续相邻 308
13.1.12 如何找出数组中出现奇数次的元素 309
13.1.13 如何找出数列中符合条件的数对的个数 311
13.1.14 如何寻找出数列中缺失的数 313
13.1.15 如何判定数组是否存在重复元素 314
13.1.16 如何重新排列数组使得数组左边为奇数,右边为偶数 315
13.1.17 如何把一个整型数组中重复的数字去掉 316
13.1.18 如何找出一个数组中第二大的数 318
13.1.19 如何寻找数组中的最小值和最大值 319
13.1.20 如何将数组的后面m个数移动为前面m个数 320
13.1.21 如何计算出序列的前n项数据 321
13.1.22 如何找出数组中只出现一次的数字 322
13.1.23 如何判断一个整数x是否可以表示成n(n≥2)个连续正整数的和 324
13.2 链表 325
13.2.1 数组和链表的区别是什么 325
13.2.2 何时选择顺序表、何时选择链表作为线性表的存储结构为宜 325
13.2.3 如何使用链表头 326
13.2.4 如何实现单链表的插入、删除操作 327
13.2.5 如何找出单链表中的倒数第k个元素 328
13.2.6 如何实现单链表反转 329
13.2.7 如何从尾到头输出单链表 331
13.2.8 如何寻找单链表的中间结点 331
13.2.9 如何进行单链表排序 332
13.2.10 如何实现单链表交换任意两个元素(不包括表头) 334
13.2.11 如何检测一个较大的单链表是否有环 335
13.2.12 如何判断两个单链表(无环)是否交叉 337
13.2.13 如何删除单链表中的重复结点 338
13.2.14 如何合并两个有序链表(非交叉) 339
13.2.15 什么是循环链表 340
13.2.16 如何实现双向链表的插入、删除操作 342
13.2.17 为什么在单循环链表中设置尾指针比设置头指针更好 343
13.2.18 如何删除结点的前驱结点 343
13.2.19 如何实现双向循环链表的删除与插入操作 343
13.2.20 如何在不知道头指针的情况下将结点删除 344
13.3 字符串 345
13.3.1 如何统计一行字符中有多少个单词 345
13.3.2 如何将字符串逆序 346
13.3.3 如何找出一个字符串中第一个只出现一次的字符 350
13.3.4 如何输出字符串的所有组合 351
13.3.5 如何检查字符是否是整数?如果是,返回其整数值 353
13.3.6 如何查找字符串中每个字符出现的个数 353
13.4 STL容器 354
13.4.1 什么是泛型编程 354
13.4.2 栈与队列的区别有哪些 354
13.4.3 vector与list的区别有哪些 355
13.4.4 如何实现循环队列 355
13.4.5 如何使用两个栈模拟队列操作 357
13.5 排序 359
13.5.1 如何进行选择排序 359
13.5.2 如何进行插入排序 360
13.5.3 如何进行冒泡排序 361
13.5.4 如何进行归并排序 364
13.5.5 如何进行快速排序 366
13.5.6 如何进行希尔排序 368
13.5.7 如何进行堆排序 369
13.5.8 各种排序算法有什么优劣 371
13.6 二叉树 372
13.6.1 基础知识 372
13.6.2 如何递归实现二叉树的遍历 373
13.6.3 已知先序遍历和中序遍历,如何求后序遍历 374
13.6.4 如何非递归实现二叉树的后序遍历 376
13.6.5 如何使用非递归算法求二叉树的深度 378
13.6.6 如何判断两棵二叉树是否相等 381
13.6.7 如何判断二叉树是否是平衡二叉树 381
13.6.8 什么是霍夫曼编解码 382
13.7 图 383
13.7.1 什么是拓扑排序 384
13.7.2 什么是DFS?什么是BFS 385
13.7.3 如何求关键路径 386
13.7.4 如何求最短路径 388
第14章 海量数据处理 390
14.1 问题分析 390
14.2 基本方法 390
14.3 经典实例分析 403
14.3.1 top K问题 403
14.3.2 重复问题 405
14.3.3 排序问题 407
致谢 409

java和Rust在实现多线程编程时的异同

Java的实现
打开Follower.java里的这个函数

这里的Follower.this.invitations就是我们的消息队列,定义是:private LinkedList<Invitation> invitations;LinkedList不是线性安全的集合,需要我们加同步。具体的同步方法就是函数里写的,通过Java常见的用wait,notify和notifyall给对象加锁。
处理并发有wait、notify和notiyall,有兴趣的朋友可以去这里了解一下:http://www.importnew.com/16453.html。Follower就是一个等待leader发送invitation,处理并返回结果的过程。
Leader.java
这么一段代码:

里面就是Leader发送邀请inv,并等待follower返回结果的大概逻辑,通过对消息体加锁,是Java传统的实现多线程并发的方式。还有消费者的消息队列也会加锁,在Java里,有个对象叫LinkedBlockingQueue,是不用加锁就可以put和take的,但在例子里,我们选用了更简单的LinkedList,也是为了表现一下加锁的逻辑。
Rust的实现
Leader的结构为:

Follower的结构为:

对于其他语言转过来的同学,这里的Vec,i32,bool都很好理解,不过里面出现的Arc和Mutex,Sender,Receiver就是新东西了,上面这4个都是Rust标准库的东西,也是这次分享要介绍的重点对象,是这4个东西共同实现了消息的生产,传递和消费。
下面简单介绍一下分别是做什么用的:
Arc<T>实现了sync接口。Sync接口是做什么呢?权威资料是这么说的:当一个类型T实现了Sync,它向编译器表明这个类型在多线程并发时没有导致内存不安全的可能性。
如果看不懂不要紧,我们先看看实际中是怎么用的:

在这个例子里,我们关注这几句:
let data = Arc::new(Mutex::new(vec![1u32, 2, 3]));
let data = data.clone();
let mut data = data.lock().unwrap();
下面分别解释一下是做什么的:
简单的说Arc::new表明了这是通过clone()方法来使用的,每clone,都会给该对象原子计数+1,通过引用计数的方法来保证对象只要还被其中任何一个线程引用就不会被释放掉,从而保证了前面说的:这个类型在多线程并发时没有导致内存不安全的可能性。
如果我们不定义为Arc<>就传到其他线程使用,编译器会报:
error: capture of moved value: `data`
data[i] += 1;
我们可以记住clone()就是Arc的用法。
接下来我们看Mutex:
Mutex实现了send接口。同样,在权威资料里是这么描述的:这个类型的所有权可以在线程间安全的转移
那我们又是怎么用Mutex的呢?就是用lock().unwrap()。lock()的作用是获取对象,如果当前有其他线程正在使用Mutex<T>里面的T对象时,本线程就会阻塞,从而保证同时只有一个线程来访问对象,mutex也另外提供了try_lock()的方法,是不阻塞的,只要其他线程被占用,就返回err,通常Arc和Mutex都是一起使用的。
回到我最原始的题目,Mutex和Arc实现了对象本身的线程共享,但是在线程间如何传递这个对象呢?就是靠channel,channel通常是这么定义的let (tx, rx) = mpsc::channel();它会返回两个对象tx和rx,就是之前我提到的sender和receiver。
在我的Rust实现里,关键的语句是以下几个:
let leaders = (0..leader_cnt).map(|i|
Arc::new(Mutex::new(Leader::new(i,dance_types.len() as i32)))
).collect::<Vec<_>>();
这一句是new一堆leader出来,Arc和Mutex表明leader是可以多线程共享和访问的。
同样Follower也是:
let followers = (0..follower_cnt).map(|i|
Arc::new(Mutex::new(Follower::new(i,dance_types.len() as i32,leader_cnt)))
).collect::<Vec<_>>();
接下来这几句就有点不好理解了。

这里定义了一堆的sender和receiver,其中把他们都作为leader和follower的成员变量存起来。大概意思就是每一个leader都通过sender列表可以发送invitation给所有follower,同时又有单个receiver来接受所有follower发给自己的处理结果inviresult。
同样follower也是这么做。这样在之后每一个follower和leader作为一个线程跑起来之后,都能在相互之间建立了一条通信的通道。
这个是和Java实现多线程并发最大的不同之处!Java是通过给对象加锁,Rust是通过channel转移对象的所有权,在代码里,leader发送inv给folloer是下面这一句
match self.senders[*follower_id as usize].lock().unwrap().send(inv){,其中的lock().unwrap()是获得该leader对该follower的发送通道的所有权,send(inv)就是转移具体的发送对象invitation所有权了。
这个转移按照我的理解,应该是内存拷贝。就是在follower接收的时候,let inv = match self.receiver.recv() { ,原来leader里面的inv在send之后已经是不可访问了,如果你之后再次访问了inv,会报use of moved value错误,而follower里面的inv则是在follower的栈里新生成的对象,所以,在Java里面我只定义了invitation对象,但是在Rust里面,我要再定义一个InviResult,因为我即使在follower线程里面填了result字段,leader线程也不能继续访问inv了。所以需要依靠follower再次发送一个invresult给leader,所以整个Rust程序大概就是这么一个思路。
实践总结
之前我测试比较Java和Rust实现的性能时,由于没有把调试信息去掉,导致Java比Rust慢很多,特别是那些调试信息都是调用String.format,这是比几个string相加慢上10倍的方法,两者都去掉调试信息后,leader和follower都会2000的时候,在我低端外星人笔记本里,性能差别大概是2倍吧,没我想象中大,Rust的程序整个写下来比较费力,一方面是对ownership机制不熟,思维没有转变过来,另一方面Rust的确需要开发者分部分精力到语法细节上。
编者注:冯总也有一些其它的实践体会,请参见CSDN对冯耀明的专访,请戳这里。也可以查看他的个人博客里的总结。
下面摘录采访中关于Rust的内容过来:
首先Rust里面的ownership和lifetime概念真的很酷,就因为这个概念实现无内存泄露,野指针和安全并发。
其次,Rust的语法不简单,也是有不少坑的,据说Rust的潜在用户应该是现在的C和C++程序员,他们可能会觉得比较习惯,说不定还 觉得更简单。由于ownership机制,一些在其他语言能够跑通的程序在Rust下就要调整实现了,它会改变你写程序的思维方式。据说一些写Rust超 过半年的程序员已经爱上它了!
我对Rust感受较深的是下面几点:
初学者不熟悉ownership机制,会无数次编译失败。但一旦编译成功,那么程序只剩下逻辑错误了。同样,由于ownership机制,将来在项目里修改Rust代码将可能是痛苦的过程,因为原来编译通过的代码可能加入新功能就编译不过了,这是我的猜测。
Rust编译速度慢,不过据说最近每一个Rust新发布的版本编译速度都比之前的版本提高了30%。
Rust没有类,有的是结构体加方法,我喜欢这种简单的概念。
Rust没有类继承,只有接口,虽然接口可以提供默认的实现。这样一来,在大型项目里原来类继承来重用代码的效果是否就要用成员变量实例来完成呢?
Rust没有null,取而代之的是None和Option<T>,也因此,结构体在初始化的时候必须初始化所有字段。
Rust有我一直很想要的错误值返回机制,而不必通过抛异常或者需要每每定义包含结果和错误体实现。
Rust用send和sync两个接口来处理多线程并发,其中Arc<T>和Mutex<T>分别实现了这两个接口,简单易用。
Rust目前没有一个强大的IDE,支持断点调试,变量监控等。
它跟现在动态语言是两个截然不同的方向,它适合一些资深的程序员,我倒是觉得有必要有这么一本书,叫《从C++到Rust,你需要改善的20个编程 习惯》,能从实践上告诉开发者Rust里我们应该遵从什么样的编程习惯。Rust未来是否像C那样流行开来成为新一代的主流语言没有人能够知道,但它绝对 是值得你去了解和关注的语言。
进一步的思考:反转链表 - Java和Rust的不同实现
Rust的list应该怎么定义,譬如反转列表又是怎么做呢?
由于ownership的机制和不存在空指针的情况,很多在其他带GC的语言能够跑起来的程序在Rust下面就要换一种做法。最近试用Rust的基础数据结构时,更加加强了我的看法。下面以最原始的链表list为例。
在Java中,考虑最基本的链表定义
class ListNode {
int val;
ListNode next;

ListNode(int x) {
val = x;
}

@Override
public String toString() {
StringBuilder sb = new StringBuilder();
sb.append("[");
sb.append(val);
ListNode pNext = this.next;
while (pNext != null) {
sb.append(",");
sb.append(pNext.val);
pNext = pNext.next;
}
sb.append("]");
return String.format("%s", sb.toString());
}
}
如果我们要反转链表,可以这么做:
public ListNode reverseList(ListNode head) {
if (head == null) {
return null;
}
ListNode pNext = head.next;
ListNode pPrevious = null;
while (head != null) {
pNext = head.next;
head.next = pPrevious;
pPrevious = head;
head = pNext;
}
return pPrevious;
}
那如果我们按照一般思维,在Rust里对应的实现就是这样子的:
struct ListNode{
id :i32,
next :Option<Box<ListNode>>
}
反转链表:
fn reverseList2(head :&mut Option<Box<ListNode>>) -> Option<Box<ListNode>> {
match *head{
None => None,
Some(head) => {
let mut head = Some(head);
let mut pNext = head.unwrap().next;
let mut pPrevious:Option<Box<ListNode>> = None;
while true {
match head {
None =>{break;}
_ =>{}
}
pNext = head.unwrap().next;
head.unwrap().next = pPrevious;
pPrevious = head;
head = pNext;
}
pPrevious
}
}
}
然后编译,报了以下错误:
=》match *head{

ERROR:cannot move out of borrowed content
=》 pNext = head.unwrap().next;
ERROR:cuse of moved value: `head`

这些错误就是因为Rust的ownership机制,让我们无法像Java或者C++里保存临时变量,特别是在循环里。反复试过各种写法,都行不通。
最后,换成这么来做
链表定义:
use List::*;

enum List {
Cons1(i32, Box<List>),
Nil,
}

// Methods can be attached to an enum
impl List {
#[inline]
fn new() -> List {
Nil
}

#[inline]
fn prepend(self, elem: i32) -> List {
Cons1(elem, Box::new(self))
}

fn len(&self) -> i32 {
match *self {
Cons1(_, ref tail) => 1 + tail.len(),
Nil => 0
}
}

fn stringify(&self) -> String {
match *self {
Cons1(head, ref tail) => {
format!("{}, {}", head, tail.stringify())
},
Nil => {
format!("Nil")
},
}
}
}

fn reverseList(list:List, acc:List ) -> List{
match list{
Cons1(val,tail) => {
reverseList(*tail,acc.prepend(val))
}
Nil => acc
}
}

fn main() {
let mut head = List::new();
let mut i=0;
while i < 10 {
i+=1;
head = head.prepend(i);
}
println!("{:30}",head.stringify());
let result = List::new();
let result = reverseList(head,result);
<span style="white-space:pre"> </span>println!("{:30}",result.stringify());
}
从结果可以看到,链表已经实现反转了。所以在Rust下面,很多做法都要换一下。有人说这就是Rust函数式编程的思维。我但愿这种递归式的做法不会有溢出。

Ⅷ 程序员开发用到的十大基本算法

算法一:快速排序算法
快速排序是由东尼·霍尔所发展的一种排序算法。在平均状况下,排序 n 个项目要Ο(n log n)次比较。在最坏状况下则需要Ο(n2)次比较,但这种状况并不常见。事实上,快速排序通常明显比其他Ο(n log n) 算法更快,因为它的内部循环(inner loop)可以在大部分的架构上很有效率地被实现出来。

快速排序使用分治法(Divide and conquer)策略来把一个串行(list)分为两个子串行(sub-lists)。

算法步骤:
1 从数列中挑出一个元素,称为 “基准”(pivot),
2 重新排序数列,所有元素比基准值小的摆放在基准前面,所有元素比基准值大的摆在基准的后面(相同的数可以到任一边)。在这个分区退出之后,该基准就处于数列的中间位置。这个称为分区(partition)操作。
3 递归地(recursive)把小于基准值元素的子数列和大于基准值元素的子数列排序。

递归的最底部情形,是数列的大小是零或一,也就是永远都已经被排序好了。虽然一直递归下去,但是这个算法总会退出,因为在每次的迭代(iteration)中,它至少会把一个元素摆到它最后的位置去。

算法二:堆排序算法
堆排序(Heapsort)是指利用堆这种数据结构所设计的一种排序算法。堆积是一个近似完全二叉树的结构,并同时满足堆积的性质:即子结点的键值或索引总是小于(或者大于)它的父节点。堆排序的平均时间复杂度为Ο(nlogn) 。

算法步骤:
1.创建一个堆H[0..n-1]
2.把堆首(最大值)和堆尾互换
3.把堆的尺寸缩小1,并调用shift_down(0),目的是把新的数组顶端数据调整到相应位置
4.重复步骤2,直到堆的尺寸为1

算法三:归并排序
归并排序(Merge sort,台湾译作:合并排序)是建立在归并操作上的一种有效的排序算法。该算法是采用分治法(Divide and Conquer)的一个非常典型的应用。

算法步骤:

算法四:二分查找算法
二分查找算法是一种在有序数组中查找某一特定元素的搜索算法。搜素过程从数组的中间元素开始,如果中间元素正好是要查找的元素,则搜 素过程结束;如果某一特定元素大于或者小于中间元素,则在数组大于或小于中间元素的那一半中查找,而且跟开始一样从中间元素开始比较。如果在某一步骤数组 为空,则代表找不到。这种搜索算法每一次比较都使搜索范围缩小一半。折半搜索每次把搜索区域减少一半,时间复杂度为Ο(logn) 。

算法五:BFPRT(线性查找算法)
BFPRT算法解决的问题十分经典,即从某n个元素的序列中选出第k大(第k小)的元素,通过巧妙的分 析,BFPRT可以保证在最坏情况下仍为线性时间复杂度。该算法的思想与快速排序思想相似,当然,为使得算法在最坏情况下,依然能达到o(n)的时间复杂 度,五位算法作者做了精妙的处理。

算法步骤:

终止条件:n=1时,返回的即是i小元素。

算法六:DFS(深度优先搜索)
深度优先搜索算法(Depth-First-Search),是搜索算法的一种。它沿着树的深度遍历树的节点,尽可能深的搜索树的分 支。当节点v的所有边都己被探寻过,搜索将回溯到发现节点v的那条边的起始节点。这一过程一直进行到已发现从源节点可达的所有节点为止。如果还存在未被发 现的节点,则选择其中一个作为源节点并重复以上过程,整个进程反复进行直到所有节点都被访问为止。DFS属于盲目搜索。

深度优先搜索是图论中的经典算法,利用深度优先搜索算法可以产生目标图的相应拓扑排序表,利用拓扑排序表可以方便的解决很多相关的图论问题,如最大路径问题等等。一般用堆数据结构来辅助实现DFS算法。

算法步骤:

上述描述可能比较抽象,举个实例:
DFS 在访问图中某一起始顶点 v 后,由 v 出发,访问它的任一邻接顶点 w1;再从 w1 出发,访问与 w1邻 接但还没有访问过的顶点 w2;然后再从 w2 出发,进行类似的访问,… 如此进行下去,直至到达所有的邻接顶点都被访问过的顶点 u 为止。

接着,退回一步,退到前一次刚访问过的顶点,看是否还有其它没有被访问的邻接顶点。如果有,则访问此顶点,之后再从此顶点出发,进行与前述类似的访问;如果没有,就再退回一步进行搜索。重复上述过程,直到连通图中所有顶点都被访问过为止。

算法七:BFS(广度优先搜索)
广度优先搜索算法(Breadth-First-Search),是一种图形搜索算法。简单的说,BFS是从根节点开始,沿着树(图)的宽度遍历树(图)的节点。如果所有节点均被访问,则算法中止。BFS同样属于盲目搜索。一般用队列数据结构来辅助实现BFS算法。

算法步骤:

算法八:Dijkstra算法
戴克斯特拉算法(Dijkstra’s algorithm)是由荷兰计算机科学家艾兹赫尔·戴克斯特拉提出。迪科斯彻算法使用了广度优先搜索解决非负权有向图的单源最短路径问题,算法最终得到一个最短路径树。该算法常用于路由算法或者作为其他图算法的一个子模块。

该算法的输入包含了一个有权重的有向图 G,以及G中的一个来源顶点 S。我们以 V 表示 G 中所有顶点的集合。每一个图中的边,都是两个顶点所形成的有序元素对。(u, v) 表示从顶点 u 到 v 有路径相连。我们以 E 表示G中所有边的集合,而边的权重则由权重函数 w: E → [0, ∞] 定义。因此,w(u, v) 就是从顶点 u 到顶点 v 的非负权重(weight)。边的权重可以想象成两个顶点之间的距离。任两点间路径的权重,就是该路径上所有边的权重总和。已知有 V 中有顶点 s 及 t,Dijkstra 算法可以找到 s 到 t的最低权重路径(例如,最短路径)。这个算法也可以在一个图中,找到从一个顶点 s 到任何其他顶点的最短路径。对于不含负权的有向图,Dijkstra算法是目前已知的最快的单源最短路径算法。

算法步骤:

重复上述步骤2、3,直到S中包含所有顶点,即W=Vi为止

算法九:动态规划算法
动态规划(Dynamic programming)是一种在数学、计算机科学和经济学中使用的,通过把原问题分解为相对简单的子问题的方式求解复杂问题的方法。 动态规划常常适用于有重叠子问题和最优子结构性质的问题,动态规划方法所耗时间往往远少于朴素解法。

动态规划背后的基本思想非常简单。大致上,若要解一个给定问题,我们需要解其不同部分(即子问题),再合并子问题的解以得出原问题的解。 通常许多 子问题非常相似,为此动态规划法试图仅仅解决每个子问题一次,从而减少计算量: 一旦某个给定子问题的解已经算出,则将其记忆化存储,以便下次需要同一个 子问题解之时直接查表。 这种做法在重复子问题的数目关于输入的规模呈指数增长时特别有用。

关于动态规划最经典的问题当属背包问题。

算法步骤:

算法十:朴素贝叶斯分类算法
朴素贝叶斯分类算法是一种基于贝叶斯定理的简单概率分类算法。贝叶斯分类的基础是概率推理,就是在各种条件的存在不确定,仅知其出现概率的情况下, 如何完成推理和决策任务。概率推理是与确定性推理相对应的。而朴素贝叶斯分类器是基于独立假设的,即假设样本每个特征与其他特征都不相关。

朴素贝叶斯分类器依靠精确的自然概率模型,在有监督学习的样本集中能获取得非常好的分类效果。在许多实际应用中,朴素贝叶斯模型参数估计使用最大似然估计方法,换言之朴素贝叶斯模型能工作并没有用到贝叶斯概率或者任何贝叶斯模型。

尽管是带着这些朴素思想和过于简单化的假设,但朴素贝叶斯分类器在很多复杂的现实情形中仍能够取得相当好的效果。

Ⅸ 计算机面试会问什么问题

问的一般都是很基础很基础的理论问题

Ⅹ 程序员必备知识(操作系统5-文件系统)

本篇与之前的第三篇的内存管理知识点有相似的地方

对于运行的进程来说,内存就像一个纸箱子, 仅仅是一个暂存数据的地方, 而且空间有限。如果我们想要进程结束之后,数据依然能够保存下来,就不能只保存在内存里,而是应该保存在 外部存储 中。就像图书馆这种地方,不仅空间大,而且能够永久保存。

我们最常用的外部存储就是 硬盘 ,数据是以文件的形式保存在硬盘上的。为了管理这些文件,我们在规划文件系统的时候,需要考虑到以下几点。

第一点,文件系统要有严格的组织形式,使得文件能够 以块为单位进行存储 。这就像图书馆里,我们会给设置一排排书架,然后再把书架分成一个个小格子,有的项目存放的资料非常多,一个格子放不下,就需要多个格子来进行存放。我们把这个区域称为存放原始资料的 仓库区 。

第二点,文件系统中也要有 索引区 ,用来方便查找一个文件分成的多个块都存放在了什么位置。这就好比,图书馆的书太多了,为了方便查找,我们需要专门设置一排书架,这里面会写清楚整个档案库有哪些资料,资料在哪个架子的哪个格子上。这样找资料的时候就不用跑遍整个档案库,在这个书架上找到后,直奔目标书架就可以了。

第三点,如果文件系统中有的文件是热点文件,近期经常被读取和写入,文件系统应该有 缓存层 。这就相当于图书馆里面的热门图书区,这里面的书都是畅销书或者是常常被借还的图书。因为借还的次数比较多,那就没必要每次有人还了之后,还放回遥远的货架,我们可以专门开辟一个区域, 放置这些借还频次高的图书。这样借还的效率就会提高。

第四点,文件应该用 文件夹 的形式组织起来,方便管理和查询。这就像在图书馆里面,你可以给这些资料分门别类,比如分成计算机类.文学类.历史类等等。这样你也容易管理,项目组借阅的时候只要在某个类别中去找就可以了。

在文件系统中,每个文件都有一个名字,这样我们访问一个文件,希望通过它的名字就可以找到。文件名就是一个普通的文本。 当然文件名会经常冲突,不同用户取相同的名字的情况还是会经常出现的。

要想把很多的文件有序地组织起来,我们就需要把它们成为 目录 或者文件夹。这样,一个文件夹里可以包含文件夹,也可以包含文件,这样就形成了一种 树形结构 。而我们可以将不同的用户放在不同的用户目录下,就可以一定程度上避免了命名的冲突问题。

第五点,Linux 内核要在自己的内存里面维护一套数据结构,来保存哪些文件被哪些进程打开和使用 。这就好比,图书馆里会有个图书管理系统,记录哪些书被借阅了,被谁借阅了,借阅了多久,什么时候归还。

文件系统是操作系统中负责管理持久数据的子系统,说简单点,就是负责把用户的文件存到磁盘硬件中,因为即使计算机断电了,磁盘里的数据并不会丢失,所以可以持久化的保存文件。

文件系统的基本数据单位是 文件 ,它的目的是对磁盘上的文件进行组织管理,那组织的方式不同,就会形成不同的文件系统。

Linux最经典的一句话是:“一切皆文件”,不仅普通的文件和目录,就连块设备、管道、socket 等,也都是统一交给文件系统管理的。

Linux文件系统会为每个文件分配两个数据结构: 索引节点(index node) 和 目录项(directory entry) ,它们主要用来记录文件的元信息和目录层次结构。

●索引节点,也就是inode, 用来记录文件的元信息,比如inode编号、文件大小访问权限、创建时间、修改时间、 数据在磁盘的位置 等等。 索引节点是文件的唯一标识 ,它们之间一一对应, 也同样都会被 存储在硬盘 中,所以索引节点同样占用磁盘空间。

●目录项,也就是dentry, 用来记录文件的名字、索引节点指针以及与其他目录项的层级关联关系。多个目录项关联起来,就会形成 目录结构 ,但它与索引节点不同的是,目录项是由内核维护的一个数据结构,不存放于磁盘,而是 缓存在内存 。

由于索引节点唯一标识一个文件,而目录项记录着文件的名,所以目录项和索引节点的关系是多对一,也就是说,一个文件可以有多个别字。比如,硬链接的实现就是多个目录项中的索引节点指向同一个文件。

注意,目录也是文件,也是用索引节点唯一标识,和普通文件不同的是,普通文件在磁盘里面保存的是文件数据,而目录文件在磁盘里面保存子目录或文件。

(PS:目录项和目录不是一个东西!你也不是一个东西(^_=), 虽然名字很相近,但目录是个文件。持久化存储在磁盘,而目录项是内核一个数据结构,缓存在内存。

如果查询目录频繁从磁盘读,效率会很低,所以内核会把已经读过的目录用目录项这个数据结构缓存在内存,下次再次读到相同的目录时,只需从内存读就可以,大大提高了 文件系统的效率。

目录项这个数据结构不只是表示目录,也是可以表示文件的。)

磁盘读写的最小单位是 扇区 ,扇区的大小只有512B大小,很明显,如果每次读写都以这么小为单位,那这读写的效率会非常低。

所以,文件系统把多个扇区组成了一个 逻辑块 ,每次读写的最小单位就是逻辑块(数据块) , Linux中的逻辑块大小为4KB,也就是一次性读写 8个扇区,这将大大提高了磁盘的读写的效率。

以上就是索引节点、目录项以及文件数据的关系,下面这个图就很好的展示了它们之间的关系:

索引节点是存储在硬盘上的数据,那么为了加速文件的访问,通常会把索引节点加载到内存中。

另外,磁盘进行格式化的时候,会被分成三个存储区域,分别是超级块、索引节点区和数据块区。

●超级块,用来存储文件系统的详细信息,比如块个数、块大小、空闲块等等。

●索引节点区,用来存储索引节点;

●数据块区,用来存储文件或目录数据;

我们不可能把超级块和索引节点区全部加载到内存,这样内存肯定撑不住,所以只有当需要使用的时候,才将其加载进内存,它们加载进内存的时机是不同的.

●超级块:当文件系统挂载时进入内存;

●索引节点区:当文件被访问时进入内存;

文件系统的种类众多,而操作系统希望 对用户提供一个统一的接口 ,于是在用户层与文件系统层引入了中间层,这个中间层就称为 虚拟文件系统(Virtual File System, VFS) 。

VFS定义了一组所有文件系统都支持的数据结构和标准接口,这样程序员不需要了解文件系统的工作原理,只需要了解VFS提供的统一接口即可。

在Linux文件系统中,用户空间、系统调用、虚拟机文件系统、缓存、文件系统以及存储之间的关系如下图:

Linux支持的文件系统也不少,根据存储位置的不同,可以把文件系统分为三类:

●磁盘的文件系统,它是直接把数据存储在磁盘中,比如Ext 2/3/4. XFS 等都是这类文件系统。

●内存的文件系统,这类文件系统的数据不是存储在硬盘的,而是占用内存空间,我们经常用到的/proc 和/sys文件系统都属于这一类,读写这类文件,实际上是读写内核中相关的数据。

●网络的文件系统,用来访问其他计算机主机数据的文件系统,比如NFS. SMB等等。

文件系统首先要先挂载到某个目录才可以正常使用,比如Linux系统在启动时,会把文件系统挂载到根目录。

在操作系统的辅助之下,磁盘中的数据在计算机中都会呈现为易读的形式,并且我们不需要关心数据到底是如何存放在磁盘中,存放在磁盘的哪个地方等等问题,这些全部都是由操作系统完成的。

那么,文件数据在磁盘中究竟是怎么样的呢?我们来一探究竟!

磁盘中的存储单元会被划分为一个个的“ 块 ”,也被称为 扇区 ,扇区的大小一般都为512byte.这说明即使一块数据不足512byte,那么它也要占用512byte的磁盘空间。

而几乎所有的文件系统都会把文件分割成固定大小的块来存储,通常一个块的大小为4K。如果磁盘中的扇区为512byte,而文件系统的块大小为4K,那么文件系统的存储单元就为8个扇区。这也是前面提到的一个问题,文件大小和占用空间之间有什么区别?文件大小是文件实际的大小,而占用空间则是因为即使它的实际大小没有达到那么大,但是这部分空间实际也被占用,其他文件数据无法使用这部分的空间。所以我们 写入1byte的数据到文本中,但是它占用的空间也会是4K。

这里要注意在Windows下的NTFS文件系统中,如果一开始文件数据小于 1K,那么则不会分配磁盘块来存储,而是存在一个文件表中。但是一旦文件数据大于1K,那么不管以后文件的大小,都会分配以4K为单位的磁盘空间来存储。

与内存管理一样,为了方便对磁盘的管理,文件的逻辑地址也被分为一个个的文件块。于是文件的逻辑地址就是(逻辑块号,块内地址)。用户通过逻辑地址来操作文件,操作系统负责完成逻辑地址与物理地址的映射。

不同的文件系统为文件分配磁盘空间会有不同的方式,这些方式各自都有优缺点。

连续分配要求每个文件在磁盘上有一组连续的块,该分配方式较为简单。

通过上图可以看到,文件的逻辑块号的顺序是与物理块号相同的,这样就可以实现随机存取了,只要知道了第一个逻辑块的物理地址, 那么就可以快速访问到其他逻辑块的物理地址。那么操作系统如何完成逻辑块与物理块之间的映射呢?实际上,文件都是存放在目录下的,而目录是一种有结构文件, 所以在文件目录的记录中会存放目录下所有文件的信息,每一个文件或者目录都是一个记录。 而这些信息就包括文件的起始块号和占有块号的数量。

那么操作系统如何完成逻辑块与物理块之间的映射呢? (逻辑块号, 块内地址) -> (物理块号, 块内地址),只需要知道逻辑块号对应的物理块号即可,块内地址不变。

用户访问一个文件的内容,操作系统通过文件的标识符找到目录项FCB, 物理块号=起始块号+逻辑块号。 当然,还需要检查逻辑块号是否合法,是否超过长度等。因为可以根据逻辑块号直接算出物理块号,所以连续分配支持 顺序访问和随机访问 。

因为读/写文件是需要移动磁头的,如果访问两个相隔很远的磁盘块,移动磁头的时间就会变长。使用连续分配来作为文件的分配方式,会使文件的磁盘块相邻,所以文件的读/写速度最快。

连续空间存放的方式虽然读写效率高,但是有 磁盘空间碎片 和 文件长度不易扩展 的缺陷。

如下图,如果文件B被删除,磁盘上就留下一块空缺,这时,如果新来的文件小于其中的一个空缺,我们就可以将其放在相应空缺里。但如果该文件的大小大于所

有的空缺,但却小于空缺大小之和,则虽然磁盘上有足够的空缺,但该文件还是不能存放。当然了,我们可以通过将现有文件进行挪动来腾出空间以容纳新的文件,但是这个在磁盘挪动文件是非常耗时,所以这种方式不太现实。

另外一个缺陷是文件长度扩展不方便,例如上图中的文件A要想扩大一下,需要更多的磁盘空间,唯一的办法就只能是挪动的方式,前面也说了,这种方式效率是非常低的。

那么有没有更好的方式来解决上面的问题呢?答案当然有,既然连续空间存放的方式不太行,那么我们就改变存放的方式,使用非连续空间存放方式来解决这些缺陷。

非连续空间存放方式分为 链表方式 和 索引方式 。

链式分配采取离散分配的方式,可以为文件分配离散的磁盘块。它有两种分配方式:显示链接和隐式链接。

隐式链接是只目录项中只会记录文件所占磁盘块中的第一块的地址和最后一块磁盘块的地址, 然后通过在每一个磁盘块中存放一个指向下一 磁盘块的指针, 从而可以根据指针找到下一块磁盘块。如果需要分配新的磁盘块,则使用最后一块磁盘块中的指针指向新的磁盘块,然后修改新的磁盘块为最后的磁盘块。

我们来思考一个问题, 采用隐式链接如何将实现逻辑块号转换为物理块号呢?

用户给出需要访问的逻辑块号i,操作系统需要找到所需访问文件的目录项FCB.从目录项中可以知道文件的起始块号,然后将逻辑块号0的数据读入内存,由此知道1号逻辑块的物理块号,然后再读入1号逻辑块的数据进内存,此次类推,最终可以找到用户所需访问的逻辑块号i。访问逻辑块号i,总共需要i+ 1次磁盘1/0操作。

得出结论: 隐式链接分配只能顺序访问,不支持随机访问,查找效率低 。

我们来思考另外一个问题,采用隐式链接是否方便文件拓展?

我们知道目录项中存有结束块号的物理地址,所以我们如果要拓展文件,只需要将新分配的磁盘块挂载到结束块号的后面即可,修改结束块号的指针指向新分配的磁盘块,然后修改目录项。

得出结论: 隐式链接分配很方便文件拓展。所有空闲磁盘块都可以被利用到,无碎片问题,存储利用率高。

显示链接是把用于链接各个物理块的指针显式地存放在一张表中,该表称为文件分配表(FAT, File Allocation Table)。

由于查找记录的过程是在内存中进行的,因而不仅显着地 提高了检索速度 ,而且 大大减少了访问磁盘的次数 。但也正是整个表都存放在内存中的关系,它的主要的缺点是 不适 用于大磁盘 。

比如,对于200GB的磁盘和1KB大小的块,这张表需要有2亿项,每一项对应于这2亿个磁盘块中的一个块,每项如果需要4个字节,那这张表要占用800MB内存,很显然FAT方案对于大磁盘而言不太合适。

一直都在,加油!(*゜Д゜)σ凸←自爆按钮

链表的方式解决了连续分配的磁盘碎片和文件动态打展的问题,但是不能有效支持直接访问(FAT除外) ,索引的方式可以解决这个问题。

索引的实现是为每个文件创建一个 索引数据块 ,里面存放的 是指向文件数据块的指针列表 ,说白了就像书的目录一样,要找哪个章节的内容,看目录查就可以。

另外, 文件头需要包含指向索引数据块的指针 ,这样就可以通过文件头知道索引数据块的位置,再通过索弓|数据块里的索引信息找到对应的数据块。

创建文件时,索引块的所有指针都设为空。当首次写入第i块时,先从空闲空间中取得一个块, 再将其地址写到索引块的第i个条目。

索引的方式优点在于:

●文件的创建、增大、缩小很方便;

●不会有碎片的问题;

●支持顺序读写和随机读写;

由于索引数据也是存放在磁盘块的,如果文件很小,明明只需一块就可以存放的下,但还是需要额外分配一块来存放索引数据,所以缺陷之一就是存储索引带来的开销。

如果文件很大,大到一个索引数据块放不下索引信息,这时又要如何处理大文件的存放呢?我们可以通过组合的方式,来处理大文件的存储。

先来看看 链表+索引 的组合,这种组合称为 链式索引块 ,它的实现方式是在 索引数据块留出一个存放下一个索引数据块的指针 ,于是当一个索引数据块的索引信息用完了,就可以通过指针的方式,找到下一个索引数据块的信息。那这种方式也会出现前面提到的链表方式的问题,万一某个指针损坏了,后面的数据也就会无法读取了。

还有另外一种组合方式是 索引+索引 的方式,这种组合称为多级索引块,实现方式是通过一个索引块来存放多个索引数据块,一层套一层索引, 像极了俄罗斯套娃是吧๑乛◡乛๑ 

前面说到的文件的存储是针对已经被占用的数据块组织和管理,接下来的问题是,如果我要保存一个数据块, 我应该放在硬盘上的哪个位置呢?难道需要将所有的块扫描一遍,找个空的地方随便放吗?

那这种方式效率就太低了,所以针对磁盘的空闲空间也是要引入管理的机制,接下来介绍几种常见的方法:

●空闲表法

●空闲链表法

●位图法

空闲表法

空闲表法就是为所有空闲空间建立一张表,表内容包括空闲区的第一个块号和该空闲区的块个数,注意,这个方式是连续分配的。如下图:

当请求分配磁盘空间时,系统依次扫描空闲表里的内容,直到找到一个合适的空闲区域为止。当用户撤销一个文件时,系统回收文件空间。这时,也需顺序扫描空闲表,寻找一个空闲表条目并将释放空间的第一个物理块号及它占用的块数填到这个条目中。

这种方法仅当有少量的空闲区时才有较好的效果。因为,如果存储空间中有着大量的小的空闲区,则空闲表变得很大,这样查询效率会很低。另外,这种分配技术适用于建立连续文件。

空闲链表法

我们也可以使用链表的方式来管理空闲空间,每一个空闲块里有一个指针指向下一个空闲块,这样也能很方便的找到空闲块并管理起来。如下图:

当创建文件需要一块或几块时,就从链头上依次取下一块或几块。反之,当回收空间时,把这些空闲块依次接到链头上。

这种技术只要在主存中保存一个指针, 令它指向第一个空闲块。其特点是简单,但不能随机访问,工作效率低,因为每当在链上增加或移动空闲块时需要做很多1/0操作,同时数据块的指针消耗了一定的存储空间。

空闲表法和空闲链表法都不适合用于大型文件系统,因为这会使空闲表或空闲链表太大。

位图法

位图是利用二进制的一位来表示磁盘中一个盘块的使用情况,磁盘上所有的盘块都有一个二进制位与之对应。

当值为0时,表示对应的盘块空闲,值为1时,表示对应的盘块已分配。它形式如下:

在Linux文件系统就采用了位图的方式来管理空闲空间,不仅用于数据空闲块的管理,还用于inode空闲块的管理,因为inode也是存储在磁盘的,自然也要有对其管理。

前面提到Linux是用位图的方式管理空闲空间,用户在创建一个新文件时, Linux 内核会通过inode的位图找到空闲可用的inode,并进行分配。要存储数据时,会通过块的位图找到空闲的块,并分配,但仔细计算一下还是有问题的。

数据块的位图是放在磁盘块里的,假设是放在一个块里,一个块4K,每位表示一个数据块,共可以表示4 * 1024 * 8 = 2^15个空闲块,由于1个数据块是4K大小,那么最大可以表示的空间为2^15 * 4 * 1024 = 2^27个byte,也就是128M。

也就是说按照上面的结构,如果采用(一个块的位图+ 一系列的块),外加一(个块的inode的位图+一系列的inode)的结构能表示的最大空间也就128M,

这太少了,现在很多文件都比这个大。

在Linux文件系统,把这个结构称为一个 块组 ,那么有N多的块组,就能够表示N大的文件。

最终,整个文件系统格式就是下面这个样子。

最前面的第一个块是引导块,在系统启动时用于启用引导,接着后面就是一个一个连续的块组了,块组的内容如下:

● 超级块 ,包含的是文件系统的重要信息,比如inode总个数、块总个数、每个块组的inode个数、每个块组的块个数等等。

● 块组描述符 ,包含文件系统中各个块组的状态,比如块组中空闲块和inode的数目等,每个块组都包含了文件系统中“所有块组的组描述符信息”。

● 数据位图和inode位图 ,用于表示对应的数据块或inode是空闲的,还是被使用中。

● inode 列表 ,包含了块组中所有的inode, inode 用于保存文件系统中与各个文件和目录相关的所有元数据。

● 数据块 ,包含文件的有用数据。

你可以会发现每个块组里有很多重复的信息,比如 超级块和块组描述符表,这两个都是全局信息,而且非常的重要 ,这么做是有两个原因:

●如果系统崩溃破坏了超级块或块组描述符,有关文件系统结构和内容的所有信息都会丢失。如果有冗余的副本,该信息是可能恢复的。

●通过使文件和管理数据尽可能接近,减少了磁头寻道和旋转,这可以提高文件系统的性能。

不过,Ext2 的后续版本采用了稀疏技术。该做法是,超级块和块组描述符表不再存储到文件系统的每个块组中,而是只写入到块组0、块组1和其他ID可以表示为3、5、7的幂的块组中。

在前面,我们知道了一个普通文件是如何存储的,但还有一个特殊的文件,经常用到的目录,它是如何保存的呢?

基于Linux 一切切皆文件的设计思想,目录其实也是个文件,你甚至可以通过vim打开它,它也有inode, inode 里面也是指向一些块。

和普通文件不同的是, 普通文件的块里面保存的是文件数据,而目录文件的块里面保存的是目录里面一项一项的文件信息 。

在目录文件的块中,最简单的保存格式就是 列表 ,就是一项一项地将目录下的文件信息(如文件名、文件inode.文件类型等)列在表里。

列表中每一项就代表该目录下的文件的文件名和对应的inode,通过这个inode,就可以找到真正的文件。

通常,第一项是“则”,表示当前目录,第二项是.,表示上一级目录, 接下来就是一项一项的文件名和inode。

如果一个目录有超级多的文件,我们要想在这个目录下找文件,按照列表一项一项的找,效率就不高了。

于是,保存目录的格式改成 哈希表 ,对文件名进行哈希计算,把哈希值保存起来,如果我们要查找一个目录下面的文件名,可以通过名称取哈希。如果哈希能够匹配上,就说明这个文件的信息在相应的块里面。

Linux系统的ext文件系统就是采用了哈希表,来保存目录的内容,这种方法的优点是查找非常迅速,插入和删除也较简单,不过需要一些预备措施来避免哈希冲突。

目录查询是通过在磁盘上反复搜索完成,需要不断地进行/0操作,开销较大。所以,为了减少/0操作,把当前使用的文件目录缓存在内存,以后要使用该文件时只要在内存中操作,从而降低了磁盘操作次数,提高了文件系统的访问速度。

感谢您的阅读,希望您能摄取到知识!加油!冲冲冲!(发现光,追随光,成为光,散发光!)我是程序员耶耶!有缘再见。<-biubiu-⊂(`ω´∩)

阅读全文

与程序员怎么算反转链表相关的资料

热点内容
python数据分析实战pdf 浏览:983
u盘插入文件夹 浏览:916
华为amd云服务器 浏览:495
汉化编程卡是什么意思 浏览:126
python学习pdf 浏览:313
祝绪丹程序员那么可爱拍吻戏 浏览:198
asp源码会员消费系统 浏览:113
java反射设置 浏览:152
python一行文 浏览:439
排序算法优缺点 浏览:563
恶搞加密文件pdf 浏览:674
gif怎么压缩图片大小 浏览:217
命令选择当前不可用 浏览:158
欧几里得算法如何求逆元 浏览:506
男中学生上课解压神器 浏览:373
加密狗拔掉之后怎么办 浏览:27
云储存平台源码 浏览:847
解压文件苹果手机rar 浏览:149
centos开机命令行模式 浏览:697
遍历所有listpython 浏览:660