哈佛-CS50-计算机科学导论笔记-九-

哈佛 CS50 计算机科学导论笔记(九)

哈佛CS50-CS | 计算机科学导论(2020·完整版) - P6:L3- 算法(结构体、搜索与排序)1 - ShowMeAI - BV1Hh411W7Up

三个。

你会记得上周我们讨论了解决问题的方法,不仅是我们提出的问题,还有你自己代码中的问题,也就是错误。这些工具涉及到帮助你解决编译器可能吐出的神秘错误消息,风格检查工具会给你关于代码风格的一些反馈。

check 50会检查你的代码在给定问题集或实验中的正确性,而printf是几乎所有编程语言中都存在的一种函数,这也是你最终可能学到的。这只是打印你想在屏幕上显示的任何内容的一种方式,接下来这些工具中最强大的就是debug 50。

交互式调试器,尽管这个命令debug 50有些特定于cs50,但它会触发一个小侧窗口,在那里你可以查看在某些断点时调用的内容,以及在代码执行的某个时刻你可能定义的局部变量,这是一个非常常见的约定。

任何调试器的特性与大多数语言相符,最后回想一下,还有这个ddb鸭子调试器,当然它以这种物理形态存在,如果你恰好有一个可以交谈的鸭子,但我很高兴地说,如果你目前没有,在家时cs50区的Kareem、Brenda和Sophie已经做了很棒的补充,如果你还没有注意到的话。

同样的虚拟鸭子在cs50 ide中,所以如果你点击左上角。

你实际上可以开始和橡皮鸭进行某种对话,虽然这无疑是同一概念的更具玩味的表现形式,但我们真的无法过分强调,和其他人或其他东西谈论代码中的问题时,讨论问题的价值。

这并不是其他人说了什么,而是你说了什么以及你自己听到自己说的内容,这无疑是这个过程最有价值的部分,因此我们感谢Kareem、Brenda和Sophie。上周我们也仔细看看了电脑的内存,字面意义上在你的笔记本电脑里。

更艺术地思考这个问题,将其视为一个字节网格,在这个芯片内有一堆位,如果你一次查看八个,它们就形成了一堆字节,想象这个作为第三个字节,依此类推,将其形象化分割成一个完整的内存。

回想一下,如果我们放大并专注于一个连续的数组。我们可以在这个数组中做一些事情,比如存储一堆不同的值。回想一下上周,我们开始时定义了一些有点傻的、几乎相同名称的多个变量,比如 scores one、scores two 和 scores three,然后我们。

开始通过引入一个数组来清理我们的代码设计,这样我们就可以有一个名为 scores 的变量,大小为三,有空间存储多个值,这是许多编程语言的一个特性,能够在计算机的内存中连续存储数据,因为这个非常简单的布局,这个非常简单的特性。

这个语言将开启各种强大的特性,实际上。我们甚至可以重温上周我们尝试解决的一些问题。这是因为即使你我可以一眼看清屏幕上的这个图片,并立即看到,尽管屏幕上有七个框,但实际上是七个。

存储值的位置,你我可以在一定程度上拥有这种鸟瞰视角,看到整个数组内部,方式更为系统,更算法化,如果你愿意,所以尽管计算机非常强大,技术上只能一次查看数组中的一个位置,因此你我可以。

一瞥这个并试图一下子理解,计算机无法一眼看清它的内存并一次性接受所有值。它可能从左到右,或从右到左,也可能是中间开始,但它必须是一个算法,确实。某种程度上掩盖了这个数组无法一次性看到的事实,你只能。

在某一时刻查看数组中的一个位置,这将有非常现实的影响。例如,如果我们考虑第一个问题,在第一周我们尝试在电话簿中找到我的电话号码,开始时。并从左到右搜索,我们之后尝试了一些变体,但问题。

简单地说,搜索和科学是非常常见的,确实你我作为用户在谷歌等网站上整天搜索东西,因此设计一个优秀的搜索算法无疑是许多今天工具的一个引人注目的特性,你我都在使用。所以如果我们真的把这看作是一个。

要解决的问题是,我们有一些输入,可能是一个数字数组,或者在谷歌的情况下,可能是一个网页数组。目标是得到一些输出,因此如果问题的输入是一个值的数组,输出希望是一些简单的东西。

作为一个布尔值,“是”或“否”是你正在寻找的值,即这个值。“是”或“否”,“真”或“假”,现在在这个黑箱中,回想一下,会有一些算法,这就是今天我们大部分时间要讨论的内容,实际上我们不会真正介绍那么多 C 的其他特性,我们将再次专注于,现在理所当然的想法。

你在工具箱中有更多工具,除了循环、条件和布尔表达式。现在我们有这个被称为数组的其他工具,但首先让我们介绍一些相关术语,关于我们将称之为运行时间的东西。当我们思考这些时,我们提到过几次。

我们描述算法的好坏时,会说明它的运行时间。也就是说,它的运行时间取决于需要多少步骤、多少秒、多少次迭代,这些单位并不重要。运行时间只是指需要多久。

算法需要多少时间,我们可以更正式地思考这个问题。在这一周,我们没有给它这个名字,这个斜体的 O,屏幕上的大 O 符号被称为大O表示法,计算机科学家们使用这个符号来描述算法的运行时间,或数学上像一个函数。

回想一下这个画面,实际上当我们在查找电话簿时。我们是以“好、好点、最好”的顺序进行的,线性搜索,一个页面一个页面地搜索。我们通过每次搜索两页的方式提高了速度,然后我们进行了对数搜索,通过不断对半分割来实现。

页面中 n 只是一个数字,在计算机科学术语中。我们可能会描述第一个算法的运行时间或步骤数,可能在最坏情况下需要 n 步,如果你在电话簿中寻找的人,可能有一个以 z 开头的姓氏。

用英语来说,z 可能位于电话簿的最后,因此在最坏情况下你可能需要查找整个电话簿,而第二个算法却快了两倍,因为我们每次查看两页,所以我们可以将其运行时间描述为 n/2,然后第三个算法是我们将问题进行了分割。

一直对半分割,实际上是一次又一次地丢弃一半的问题,n。这再次是一个数学公式,指的是不断重复某个操作,当然在这种情况下你一开始有 n 页。结果证明计算机在这些数学细节上表现得相当出色。

事实上,我们不会养成写非常精确的数学公式的习惯,而是会尽量理解算法的概念,粗略地了解它是多么快或多么慢,但仍然使用一些符号,比如 n 作为占位符。因此,计算机科学家会描述三者的运行时间。

这些算法来自于零周,作为大O,O(n) 或大O,O(n/2) 或大O,O(log₂(n)),所以大O只是表示在某个顺序上,这可能是一种随意的表示。它是 n 减 1,也许是 n 加 1,甚至可能是 2n,但它在 n 的顺序上,注意这个图表,这里有些,奇怪的地方,比如这两个算法,来自于零周。

从图像上看,几乎看起来是一样的,毫无疑问,黄色线略低,因此稍微好一些,稍微快一些,但它们有相同的形状。事实上,我打赌如果我们大幅缩小,这两条直线会变得相当大,足够大且高,这就是绿色线。

是根本不同的,所以这种不纠结细节的倾向,零。是更好的,是的,这条黄色线是算法,其运行时间的量级是n。也就是说,计算机科学家往往会忽略常数因子,比如1/2或除以2,他们倾向于只关注主导因子。

在那个数学表达式中,哪个值会增长得最快,n。除以2,n会随着时间的推移占主导地位,电话簿越大,你拥有的页面就越多。真正重要的是n,而不是除以二的结果。同样,如果你熟悉并记得你的对数。

我们其实不需要关心2,但是,我们可以将这个对数乘以其他数字,将其转换为我们想要的任何基数,比如10,3,7。任何基数,所以我们可以说它的量级是log n,这很好,因为这意味着我们不会浪费时间深入细节。

在数学上,当我们谈论算法的效率时,是以n这个变量为基础的。如果你愿意,让我们缩小视野,如果我在这个图像上缩小,你开始看到,确实这些看起来几乎是相同的。如果我们继续缩小,你会看到它们本质上是一回事。

绿色的那个很突出,所以这确实是以log n为量级,而不是n本身。所以这里有一个小的备忘单,在算法分析中,我们的确是这样的。

我们会看到一些常见的公式,像是我们刚刚看到的量级是n。我们看到量级是log n,结果是非常常见的2n平方。然后甚至还有大O的1,最后的情况是,做一步或两步,可能甚至10步,但都是常数步数,所以这算是最好的情况。

至少在这些选项中,而n平方会开始需要很长时间,它会开始感觉慢,因为如果你取任何值的n并平方它,那将意味着更多的步骤。所以今天开场时先讲一点行话。我们现在有这样的词汇来描述。

算法的运行时间用这个大O符号来表示。大O表示运行时间的上限,比如说,算法最多可能需要多少步骤,最多需要多少时间,反之,算法的运行时间的下限是什么,我们并不需要。

另一个图示或其他公式我们可以,在这里,只是提出当描述算法时。你想提出一个下限,例如我的算法最少需要多少步。我们可以使用相同的数学公式,但我们用Ω而不是大O,所以看起来华丽,但实际上它只是指一个。

手挥动,试图大致估算,幸好。我们已经见过一些算法,包括在零周。现在我们要给它一个更正式的名称,线性搜索就是我们做的。我们通过逐页搜索电话簿来进行的。

在那个特定例子中寻找我的电话号码,因此今天的不同之处在于。与能够查看电话簿页面并一次看到许多名字和数字的人类不同,我们需要更加有条理,更加刻意,今天我们才能翻译。

伪代码,但实际上是C代码,所以很高兴,在哈佛校园这个学期我们与整个团队合作,他们更擅长。独自一人,我有这七扇美妙的门,它们之前出现在这个剧院里,我们甚至有商店,谁在后面制造了一些令人愉快的。

数字,并将它们带入生活,这意味着在这七扇门后面。每扇门后都有一个数字,这将是一个机会,真正强调当我们想在数组中搜索某个数字时。实际上就等同于搜索一个数字门,你我不能只是看所有的。

数字是,我们必须更加有条理,我们需要从这些门开始搜索,可能从左到右,可能从右到左,也可能从中间向外,但我们需要提出一个。代码的方案,例如假设我要搜索数字零,我们该如何有条理地搜索这七扇木门中的数字零呢?让我听听建议。

听众,你可能在这里采取什么方法,独自面对这些门,有什么建议?我该如何开始找到数字零,佛罗伦萨,你有什么提议?嗯,我会建议从左边开始,因为零是一个较小的数字。好的,等一下,稍等我一下,让我提出。

d并打开门,希望能找到,不,这是数字四,所以不是零。那么,佛罗伦萨,你提议我接下来做什么,嗯,我可能会从中间开始,比如一,所以,好的,可能是向下走,所以让我去哦d并试试,所以你提议。中间,我可以去这里,哇,不,这是数字二。

我想知道我还应该去哪里看,我有点好奇,也有点紧张,是否忽视了这些门,往前看看,哦不,那是数字六。我们继续看看这里的数字,往下走,所以弗朗西斯,我该如何完成对这个数字的搜索?还需要做什么?你会说应该从右边开始。

好吧,我可以从右边开始,也许就在这里走过来。瞧,它就在这里,我们找到了数字零。那么让我问弗朗西斯,你的算法是什么,你是怎么成功找到数字零的,比如,下降一步,如果数字不在这里,像是,我不知道。

轻松地问问,你在中间时,这样做效果如何?更好、坏、还是没有区别?我想,可能确实有点帮助,然后一直往右走,好的,是的,我们可能获得了一些信息,但让我们继续看一下所有的门。

那个四和六又来了,那个八又出现在中间,之前有个二,现在这里有个七,第一次在这里出现的五,当然还有零。如果你认真考虑这一切,弗朗西斯,你我其实没有做得更好,因为这些门。

这些数字实际上是随机排列在这些门后面,所以你随意跳动其实并没有什么坏处,尽管缺点是,如果你跳来跳去,你和我作为人类可以相对轻松地记住我们去过哪里,但如果你想一下如何将其转换为代码。

我觉得我们开始积累了一堆变量,可能是因为你得跟踪这些,所以坦白说,最简单的解决方案可能是从第0周开始,我们采取非常简单的天真的方法,从这个大小为七的数组开始,后面有一些数字,如果你。

我对那些数字一无所知,坦白说,你能做的最好就是从第零周开始那种线性搜索,一次检查一个值在这些门后,只希望最终能找到。所以,这已经占用了很多时间,如果我这样做线性。

这种搜索方法就像我在第0周所做的一样,我得在所有这些门后面搜索,所以,让我们更正式地考虑一下,究竟如何至少实现那个算法,因为我可以采取弗朗西斯提出的方案,只是跳来跳去,可能用点直觉。

不过这其实并不算一个算法,我们确实需要更逐步的方法。同时,让我们前进,,拉上窗帘,看看是否能用另一个问题来解决这些问题,稍后我们再考虑。所以,关于线性搜索,我想提出这个。

我们可以在伪代码中实现它,首先如果你喜欢这样。对于 i 从零到 n 减一,好吧,我们看看这将如何进行。如果数字在第 i 个门后面,返回 true,否则在最后。转换成伪代码,像我们之前做的那样。

电话簿,早些时候,为什么这些值,因为我更像 c。尽管它仍然是伪代码,所以对于 i 从零到 n 减一,所以计算机科学家倾向于从零开始计数。

在这种情况下,从零到 n 减一,这只是一种非常常见的方法。通过设置一个 for 循环,可能是在 c 中,也可能是伪代码,在这个条件下。数字在第 i 个门后面,我只是用一种口语化的方式来说。第 i 个位置的门后面是什么,继续并返回 true,我找到了。

我想要的数字,例如,数字零,然后注意到这个返回 false。并不是 part of an else,因为我不想中止这个。

算法过早终止,并且仅仅因为一个数字不在当前门后面。我基本上想等到算法的最后。

在检查完所有 n 个门后,如果我仍然没有,只有那时。我要返回 false,所以一个非常常见的编程错误可能是把这个嵌套在内部,考虑事情的 if 和 else,但你不需要有一个 else。这在最后算是一种通用处理,但现在让我们考虑。

搜索,搜索,效率如何。!

搜索,也就是说这个算法设计得如何,我们给自己设置了一个框架,刚才提到的大 O 表示法,它是一个上限。现在我们可以把它理解为,像是最坏情况,在最坏情况下,我可能需要多少步才能找到,数字零或任何数字。

事物之间,n 的情况。

时间 log n,大 O 表示法 n,大 O 表示法 log n。

常数,固定步数,嗯,布赖恩,我们能否继续并拉出这个问题。让我也在我的屏幕上拉出来,如果你去我们平常的地方,稍等一下。现在的问题看起来,如果你去 polev.com cs50,你很快就会看到结果,给你几秒钟时间。

你认为线性搜索的运行时间的上限是什么,在这里用这个伪代码实现,同时,我要继续并在这里登录。技术问题,如果你不介意,原谅我,让我暂停一下,快速修复一下,这并不是技术问题。

由于我没有提前完成这项工作,这是我的用户错误,所以我。几乎在网上修复好了,哇,看到结果将会是多么有趣。好吧,差不多到了,我们会去掉所有这些尴尬,让我继续,好吧。给我一秒钟让它出现。

好吧,我很抱歉,那么线性搜索的运行时间的上界是什么?看起来几乎所有人都回答了 n 的大 O,所以你们中有 86% 的人,这确实是。

这种情况,我们确实可以在我们的整个图表中看到这一点。如果我们考虑运行时间,抱歉,我在这里是新人,好吧,修复一下,这里可以了。好吧,确实,如果我们现在考虑,这应该是 n 的大 O,为什么呢?在最坏情况下,我正在寻找的数字 0,可能就在最后。

列表的步骤将是 n 步,或者在这种情况下,确切地说是 n 步,这是一种思考方式。

问题,符号。

这是一个算法运行时间的下界,布莱恩,我们能否继续询问这个。

下一个问题,在同一网址,我们将看到一个问题,询问线性搜索运行时间的下界的可能答案,所以让我们继续看看这个。很快我们会看到。

大约有 75% 以上的回复,你们提出。实际上它是 omega 的一,omega 是下界,一指的是常数时间,这一点为什么重要。

步骤,或常数步数,为什么是这样,你怎么想?呃,是的,你可以打开它,走运地在第一扇门找到它。是的,所以这确实表明,你可能只会走运,正在寻找的数字可能就在第一扇门后,所以在最佳情况下的下界。

线性搜索的算法可能确实是 omega 的一。

正是出于这个原因,你必须走运,元素可能在开头就在那里,所以这相当。

很好,我们真的做不到比这更好了,所以我们现在有一个范围,来自于 omega 的下界。

直至大 O 的 n 成为线性搜索运行时间的上界,但当然我们还有。这个工具箱中的其他算法,回想一下从零周。我们看过二分搜索,虽然。

并不一定是按名称来分而治之的第三种算法,我们拿起电话,分成两半。再一次,现在我在那里笨拙地处理,而乔在一旁。

乔好心地给我们了一组新的门,如果乔,我们再次有门。后面仍然有一些数字,但我想这次我会去找门。好了,后面是我们同样的七扇门,但这次那些门后面是不同的数字排列,假设这次我想找到数字六。

所以数字六会稍微改变问题,但这次我会给你一个关键的要素,这将是这个工作的关键。为什么弗洛伦斯和我之前只能做到线性搜索?为什么弗洛伦斯和我只能做到。

上次随机搜索。

数字数组或者门数组有什么特征,让我之前无法使用呢?嗯,因为我们不知道这些数字是否是排序的。是的,我们不知道数字是否排序,而确实,除了那个细节,弗洛伦斯和我。比线性搜索好,所以这次,乔好心地进行了排序。

一些数字在这些门后面,所以如果我想搜索数字六。现在我可以开始利用这些信息,你知道我将开始,就像我们处理电话簿一样,大致从中间开始,瞧。数字五,好吧,所以我们很接近,我们很接近,但问题是。

关于二分搜索,回想一下这是有用的信息。如果这些门后面的数字是排序的,左边的所有门,右边的所有门应该大于5。现在我可能会在这里走捷径,想如果这是5,6可能就在隔壁,但再次,从算法的角度来看,我们该如何做到这一点呢?

不想特别考虑这些特殊情况,所以更多的大小为三的数组,所以让我去应用相同的算法,瞧。现在我到中间,得到了数字七,现在变得相当清楚。如果数字六存在,可能在这扇门后面,确实是,大小为一,数字六。

门,而不是所有七扇门,或者也许六扇门,找到我的数字,因为我得到了这些额外的要素,即所有的数字都是排序的。因此,看来你可以应用更好、更高效的搜索。如果只有像乔这样的人提前为你排序数字,那就好了,所以现在我们来考虑一个。

从算法的角度来看,我们可能如何实现这个,因此用二分搜索让我提出这个伪代码,返回真,我们找到了它,所以如果我们运气好,那么我们可能找到了这个数字,完成了,但这没有发生,而在一般情况下,这可能不会发生,所以如果数字小于。

在中间门后面,然后就像查电话簿一样,我将去,剩余门的左半部分。如果这个数字大于在中间门后面。然后像电话簿一样,我将去,电话簿,但可能还有一个最终的情况。潜在地,如果根本没有门,或者根本就没有门。

我至少应该有一个特殊情况,例如如果六因为某种原因不在那些门中,而我正在搜索。我仍然需要能够明确处理,如果我没有进一步的门可搜索,则返回false。那么这里可能是这个算法的伪代码,更正式一点。

现在让我们考虑之前分析的内容,在线性查找为O(n)时。线性查找为O。

这次让我们考虑二分查找实际适用的地方,问一个不同的问题。我将继续并回去问这个问题。二分查找的运行时间的上限是什么,二分查找的运行时间的上限是什么,继续并像以前一样发言。

二分查找的运行时间的上限是什么,你可以在这里看到答案在增多。

在O(log n)附近非常占主导地位,确实与我们在O(n)中的结果一致。因为这将是给定大小数组的最大值,并将其对半分割。你正在寻找,同时如果我们现在考虑的不仅仅是这个算法的上限。那么在最坏的情况下,二分查找的时间复杂度为O(log n),现在让我们考虑一个相关问题。

什么是下限,什么是运行时间的下限,我将继续参考到目前为止的一些建议。在最佳情况下,也许是两个。你运气好,正在寻找的数字六或其他某个数字恰好在数组的中间,因此也许确实可以做到。

只需一步,确实是二分查找的下限。

可能实际上只是一个Ω(1),因为在最佳情况下。你运气好,它正好在你开始的地方,在这种情况下在中间,所以我们似乎有一个范围,但严格来说,二分查找似乎比线性查找好,因为当n变大时,差异实际上会变得明显。

回想一下从零周,我们玩了一点这些灯泡,现在这六十四个灯泡都亮着,让我们考虑一下,为了将其放入视角,使用线性查找在这六十四个灯泡中找到一个灯泡需要多长时间。灯泡。

或者我们正在寻找的数字,在那里的一端,但我们事先并不知道。因此,Sumner,如果你不介意对这些灯泡执行线性搜索,让我们感受一下这个算法的效率或低效,你会注意到,一个灯泡一次亮起,意味着我已经搜索过那扇门,搜索过那扇门。

我们搜索了那扇门,但只经过了十个左右的灯泡,还有超过50个灯泡要处理。可以看到,如果我们每秒检查一个灯泡,实际上会花费很长时间,等到最后似乎没有必要。因此,如果你愿意,让我们一起把所有的灯光带回,算法。

这个二分搜索,再次感受一下,像二分搜索这样在对数时间内运行的算法的运行时间。所以,稍后我们将继续执行这些灯泡上的二分搜索,想法是:有一个灯泡我们关心,让我们看看,我们能多快找到这个灯泡。

从 64 个灯泡中熄灭。因此,Sumner,准备好,开始,几步之后我们就完成了。然后我们得到了这个灵魂灯泡,它快得多,实际上我们故意这样做。一轮接一轮,刚刚执行的算法,在 Sumner 和 Matt 的帮助下。算法的运行频率是 1 赫兹,如果你不熟悉赫兹。

每秒仅表示一次,这在物理学中经常使用,或者更广泛地讨论电力,实际上在这个例子中,那个第一个算法线性搜索可能会到达最后一个灯泡,但第二个算法是对数的,因此从 64 到 32。

到 16 到 4 到 2 到 1,我们得到了。

这样可以更快地得到最终结果,即使以相同的速度运行,所以如果你想象一下你的电脑 CPU。

CPU 也是以赫兹(hertz)为单位测量的,h-e-r-t-z,可能以千兆赫(gigahertz)为单位测量,即每秒数十亿赫兹,因此你的 CPU,电脑的大脑,字面上可以同时做一十亿件事,而在这里,我们有这种更简单的设置,仅仅是每秒做一件事的灯泡,你的电脑可以做到一。

这种操作的数量达到数十亿,因此想象一下究竟有多少。

这些节省时间的优势随着时间的推移而累积,处理多个问题,而不是像我们在第零周那样逐步进行,一次一步。好了,现在让我们继续,将其翻译成代码,我们的工具箱中有足够的工具,我认为基于我们上周对数组的讨论,我们可以。

现在我们实际上开始自己用代码构建一些东西,所以我将继续在cs50 ide中创建一个文件,叫做numbers.c。让我将其翻译为一个名为numbers.c的C代码文件,手头的目标只是实现线性搜索代码,而不是伪代码。

但我们要更具体一点,所以我将继续并包含cs50.h。我将继续并包含stdio.h,并且我将从没有命令行参数开始,就像我们上周所做的那样,但只是用,并且我将继续并给自己一个包含七个数字的数组,声明为int numbers,然后这在计算机的内存中稍微有点右。

这对于在你提前知道想要什么数字时创建数组很方便,而我正是这样做的。因为我要模拟乔为我们设置的这些门。我将继续并要求给我一个等于4 6 8 2 7 5 0的数组。这是我们上周没有看到的特性,如果你提前知道数组的话。

你实际上不需要显式地处理,编译器可以智能地为你解决这个问题。用逗号来枚举从左到右你想放入数组的值。所以在第六行执行后,我的计算机中将留下一个名为numbers的数组,其中包含七个从左到右列出的整数。

现在我想用这些数字做什么呢?好吧,让我们实现线性搜索,线性搜索如我们之前提到的,通常从左向右进行。所以我将进行一个标准的for循环,声明int i = 0i <我将继续。

现在为了简单起见,我将硬编码想要的内容,并在每次迭代时执行i++。所以我很确定我的第八行将引发一个总共迭代八次的for循环。我想在每次迭代时问什么呢?嗯,如果数字数组在位置i等于,例如数字i

我最初要搜索的目标是零,然后我想做什么呢?让我去,但有用的知识,然后让我继续并为了好 measure,再返回零,我们稍后会再回到这个问题。但在这个程序结束时,我也会执行这个printf,输出not found并带上换行符。

我要继续进行并返回,不过在我们拆解这些聚合之前,先来看一下我的整个主函数。在第六行,我初始化了数组,就像我们一开始时用一个看似随机的数字列表那样。

这个for循环总共进行七次,每次递增i,然后第十行,就像我一个一个打开门一样,我将检查这个数组中的第i个数是否等于我关心的数字,0,基于那个第一次演示,我会打印找到。否则不是else per se,而是如果我遍历这个整个循环,检查如果。

如果我实际上从未找到零,我会在结尾加上这种“捕获所有”的处理。也就是说,无论如何,如果你到达第16行,就打印未找到,然后返回一。现在这是一个微妙之处,但能有人提醒我们,第13行的返回0和第17行的返回1是怎么回事吗?为什么是0和1,我为什么要返回它。

这解决了我什么问题,尽管我们大多数程序因此,这对我来说,返回。或者找到了,嗯,它会退出循环,说明,1,就像返回假一样,嗯,它也确实退出了,确实,退出在main中是一个重要的词。当你准备好退出程序时,就像我们用“退出”这个词一样。

在我们过去的一些伪代码中,你可以直接返回一个值。回想一下上周末,我们介绍了main总是返回一个答案。对此我至少忽视了一到两周,但有时,返回一个明确的值是有用的,无论是为了自动评分的目的。

无论是为了自动化测试你的代码在现实世界中的应用,还是仅仅是为了向用户传达确实出了问题的信号,所以你可以从main返回一个值,正如德米所建议的,零意味着一切良好,这有点违反直觉,因为到目前为止,真值往往是件好事,但在这种情况下,零是件好事,一切正常,就是成功。

如果你返回任何其他值,例如1,这表示某些事情出了错。所以我在“找到”这个词后打印的原因是,我返回零。有效地说,程序在那一点上退出,我不想在已经找到我关心的数字的情况下,一遍又一遍地继续。

在这里,这一行诚然不是,16,可能删除第17行,程序将。无论如何会结束,但不会有我们上周讨论的所谓,退出状态。简而言之,你可以通过这种方式传达,成功或失败,而零是一个好的信号,一个或任何其他数字则不是,程序会。

写或现实世界中的公司,当你得到那些错误,错误代码时。可能会有成千上万的问题,发生在计算机程序中。可能会出现那么多的错误代码,你在屏幕上看到的原因解释,比如,仅仅是一个值,世界已经决定,这意味着成功,所以只有一种方法可以得到你的,许多。

事情出错的方式有数百万种,这就是为什么人类采用了这个特定的方法。现在,不仅是数字,让我们让事情变得更有趣,假设我们实际上在后面有人的名字。好吧,让我们继续写一个程序,这次不仅搜索数字。

取而代之的是搜索名字,所以我在这里,称之为names.c。我将以类似的方式开始,我会在顶部包含cs50.h,顶部再包含standard io。这次我还将包括string.h,我们简要介绍过,用于获取字符串长度,以及其他一些函数。

让我继续声明int main,void,像往常一样,然后在这里我需要一些随意的名字。让我们想出七个名字,这里我也可以像之前一样声明一个数组,但它不必只存储整数,它可以存储字符串,因此我将数据类型从整数更改为字符串。

变量名称从数字更改为名称,表示法,可能还有。

charlie,也许还有fred,也许是george,也许是ginny,或许是percy,最后可能是一个像ron这样的名字,它刚好可以放在我的屏幕上。因此,话虽如此,我现在有了这个名字的数组。除此之外,可能还有一些。

显而易见的模式存在,但也有第二个不那么明显或许明显的模式。

你会如何描述这些我随意想到的名字列表?它们有什么有用的特征?你注意到这些名字有什么?对此问题,我认为至少有两个正确答案。

你注意到这些名字有什么?嗯,它们是按字母顺序排列的,是的。除了是哈利·波特中韦斯利家孩子的名字之外,它们也在。

按字母顺序排列,这对我们的目的来说是更突出的细节。这次我有了先见之明,提前对这些名字进行了排序。如果我已经对这些名字进行了排序,那么这隐含着我可以使用比线性搜索更好的算法,我可以使用例如我们旧的二分搜索。

但是我们先直接搜索它们,现在还是使用线性搜索,因为你知道我们还没有做的是比较字符串。我们进行了很多整数的比较,但名字呢?所以让我先进行这一操作,对于int。

i得到了0,就像之前一样,i小于7,i加加,我仅仅在做这个,七个名字。我想我们或许可以改善这个代码的设计,通过使用一个变量或常量来存储那个值,但我现在想保持简单。结果是,由于一些原因,我们将在下周更详细地探讨。

做之前的事情并不足够,如果我在寻找ron。结果是,在C语言中你不能使用等于等于,int,对于char我们在过去做过这两者,但有一个细微差别,我们将深入探讨。下周会更详细地讨论,这意味着你实际上不能这样做,这很奇怪,因为如果你。

有在像python这样的语言中的编程经验。你可以这样做,因此在C语言中你不能,但我们下次会看到原因,而现在。结果是C语言可以解决这个问题,历史上解决这个问题的方法是使用一个函数。因此在string.h头文件中,不仅有声明字符串长度的。

它实际上以ASCII顺序比较字符串,或称为ASCII比较,这是一种有点古怪的描述方式。

像上周提到的字符串,还有另一个函数叫做str_compare,简写为strcmp,允许我传入两个字符串,一个是我想要比较的字符串。

还有字符串比较,如果我们阅读它的文档,会告诉我们。它比较两个字符串,并且。

返回三个可能的值之一,如果这两个字符串相等,即完全相同。逐字比较的话,这个函数将返回零。如果第一个字符串在某种意义上按字母顺序排列,那么这个值,如果第一个字符串应该在前面。

在第二个字符串之后,按字母顺序排列的话,它将返回一个正值。

有三个可能的结果,或者等于零,实际上如果你查看,指定。

什么值小于零或什么值大于零。

你只需检查任何负值或任何正值。我刚才也撒了个小谎,这并没有按字母顺序检查,即使它偶然有时会这样。

所以语法不完全相同,确实有点难以阅读,不像等于等于那么简单。

这个函数从左到右查看两个字符串中的每个字符。它检查它们的ASCII值,然后逐字符比较这些ASCII值。如果ASCII值小于另一个,则返回负值。反之亦然,因此,如果你有,比如字母A,字符串中大写的A。

首先转换为65,然后如果你在其他地方有一个A,也转换为65。它们是相等的,但当然,字符是从左到右进行比较的,因此strcmp会检查每个字符,并在遇到结束的空字符时停止。记住,字符串在底层总是以这个反斜杠零\0结束。

有八个零位,因此这就是strcmp知道何时停止比较值的方式。但如果我继续进行,找某个人,比如说Unquote,像之前一样,我会继续进行,返回像Demi所提出的那样成功。否则,如果我到达代码的底部,我将打印出“未找到”,以告诉故事我们没有找到Ron

在这个数组中,尽管他确实在那里,我会继续进行,返回1。因此,尽管我把所有内容都硬编码了,硬编码的意思是明确地输入。你可以想象使用命令行参数,就像上周那样获取用户输入,问他们想搜索谁。

你可以想象使用getstring来获取用户输入,并问他们想搜索谁。但现在为了演示,我只用了Ron的名字。如果我没有打错,让我继续进行,输入make names,到目前为止还不错,./names,希望我们确实能找到,因为Ron确实在这个数组中。

在这里,新定义的数组为七个,我们声明一个固定大小的数组时,实际上不需要严格放一个数字,并且我们有这个大括号表示法。但是或许最后也是最强大的,我们在C中有一个叫做strcmp的函数,它将允许我们以这种方式存储和比较字符串。

让我在这里暂停,问一下是否有关于我们如何将这些思想转化为数字代码的问题,以及我们如何将这些思想转化为名称代码。每次使用线性搜索而不是二分搜索,Caleb问,我会静音。呃,是的,如果Ron例如是全大写的,那程序还会有效吗?

比如说,如果你试图搜索时,大写和小写不一样。这是个很好的问题,让我提出一个一般性的方法。当有疑问时,就尝试一下,所以我会完全这样做。虽然我确实知道答案,但假设我不知道,让我继续进行,把Ron改为全大写,只是因为人类的因素。

大写锁定键是开启的,他们输入时有点马虎,让我继续进行。并且不做其他更改,注意我在保持原始数组的情况下只把R大写。让我重做这个程序,执行./names,瞧,他确实还在,哦,好吧。

与罗恩一起工作是因为我没有真正练习我所宣扬的,所以凯勒,请暂时保持这个想法,这样我可以倒回去一点,修复我显然的错误,所以罗恩确实被找到,但他并不是因为罗恩被找到,我在这里做了一些愚蠢的事情,而现在可能更具教育意义的是。

程序,它也说,罗恩以大写字母显示,而你知道,让我稍微好奇一下,让我去搜索,不仅是罗恩,我们怎么搜索罗恩的妈妈莫莉,好的,现在只是想揭示我,确实做了一些愚蠢的事情。点斜杠名字,好的,现在显然有什么不对,对吧,我甚至可以搜索父亲亚瑟。

制作名字点斜杠名字,似乎我写了一个程序,它只会字面上总是说“找到”,所以我们不应该接受这个作为正确的。根据我到目前为止的定义,有人能发现这个错误吗?同时,这并不是一个真的糟糕的时机去打开鸭子,说,呃,你好鸭子,我遇到了问题。

我程序的输出总是打印“找到”,即使数组中没有某人。我可以继续向鸭子解释我的逻辑,但希望索非亚能比鸭子更快地指出解决方案,我们收到的。搅拌比较某些东西,所以我们需要这样,完美,所以我说了正确的事情,但我,我想检查。

对于相等,我确实需要检查。

当比较名字的括号我与罗恩相等时,返回值为零,因为只有在搅拌比较的返回值为零时,我才真正有一个匹配。

相比之下,如果函数返回一个负值,或者函数返回一个正值,那就意味着这不是匹配,这意味着其他,或在其他之后,但问题是,并不总是错误的语法,使用布尔表达式时,其中是像这样的函数调用,注意到我的全部发言。

为了搅拌比较,我传入两个输入,名字的括号我和引号罗恩,因此我期待搅拌比较返回,一个所谓的返回值,这个返回值将是负的。

清晰,账单,和名字的括号我或名字的括号,零是账单,账单逗号罗恩实际上是我在第一次迭代中的输入。

按字母顺序和ASCII顺序,账单在罗恩之前,这意味着它应该返回一个负值给我,而布尔表达式的问题是,在这个上下文中实现的,只有零是假的,任何其他返回值根据定义都是真的,或者是“是”的回答,无论是负一还是正一,负一百万或正一百万。

在计算机语言中,像C这样的语言中的任何非零值都被视为真,也称为*****。

虽然是错误的,但只有那个值被视为假,所以我实际上一开始很幸运,因为我的程序是为“RON”设计的。然后当我再次为凯勒布执行时,我把“RON”大写,我就不再幸运,因为突然间我知道大写的“RON”不在数组中。然而,我仍然说他被找到了,但那是因为我没有练习我。

传教士每个智慧都是好的,所以如果我实际上将这与零进行比较,现在凯勒布我们就回到了你的问题,我用名字重建了这个程序。我现在执行dot slash names并搜索所有大写的“RON”,我应该能看到,谢谢。

未找到,所以我希望我能说这是故意的,但这实际上是常见的错误情况。所以在这里,我20年后在我的代码中制造错误。如果你这周遇到类似的问题,请放心,它永远不会结束,但希望你在做题时不会有几百人盯着你。

好的,还有其他问题吗?答案是没有。

是区分大小写的,因此不会找到“ROB”。关于使用字符串的线性搜索有任何问题吗?没有,好吧,让我们继续做一个最终的例子,我认为是关于搜索的。但让我们引入另一个特性,这实际上非常酷且强大。直到现在,我们一直在使用像intcharfloat等数据类型,你现在会看到。

有时实际上有理由创建我们自己的自定义数据类型,这些数据类型在C语言发明时并不存在。所以,例如,假设我想表示的不仅仅是一堆数字,而不仅仅是一堆名字,但假设我想实现的书当然包含名字和数字。

假设我想将这两个想法结合起来,如果我能有一个数据结构,那将是一个有某种结构的数据类型,能够同时存储两者,那不是很好吗?事实上,如果C语言,我想表示。

像电话簿中的一个人,既有名字又有号码。我实际上可以通过调用那种类型的变量“person”来实现这一点。当然,C语言的设计者并没有强迫创建一个名为“person”的数据类型。实际上,如果他们为每个数据类型都创建一个,那将是一个滑坡。

你可以想象的现实世界实体,但这个电话簿既有名字又有号码,我们可以这样理解,一个名字和一个号码都是字符串类型。快速检查一下,为什么我现在有点自作主张地将电话号码称为字符串?我们一直在谈论这些门后面的“ins”,我们一直在代码中搜索“ins”。

但我为什么会假设我们改为实现一个,数字呢。呃,是的,因为我们并不是在做,数学,这就像,电话号码可以是字母,随我们怎么想,实际上我,想说有时候你会看到像,1-800 contacts这样的东西,也许我们想允许这样。

是的,绝对是一个电话号码,尽管它的名字不一定只是一个数字。

它可能是1 800 contacts,这个是一个英文单词,它可能有连字符或破折号,可能有括号,它可能,有很多,我们绝对可以,表示并使用字符串,但我们无法表示。

在C语言中使用蚂蚁,实际上即使在现实世界中,有这些数字,你我偶尔会提到,比如电话号码,也许,在美国的社会安全号码,信用卡号码,那些不一定是你想要的,处理为实际整数,实际上,你们中那些做了信用问题的人。

并尝试验证信用卡,挑战,卡号,回想起来,可能对你来说,把信用卡号视为字符串会更容易,当然设计的陷阱是你没有,至少,在C语言中。所以假设我想创建我自己的自定义数据类型,封装如果你愿意,两个不同的值类型,一个人应该是。

从此,名字和数字,结果是C语言给了我们这个语法。这是今天我们将看到的唯一一段新的,语法,除了刚才的花括号。类型定义,正如名字简洁地暗示的,这允许你,定义一个类型,而该类型将是某种结构。

所以在编程语言中,数据结构通常是一个有某种结构的数据类型。它通常包含一个或多个值,内部使用,typedef,并依次使用 struct 关键字,我们可以创建我们自己的自定义的,多个其他数据类型。因此,如果我们想要,把人们,作为他们自己的自定义数据类型,语法在这里有点晦涩。

字面上输入 def struct,打开花括号,然后逐行输入。

指定你想要的数据类型,以及你想要给那些数据类型的名称,例如名字和数字,然后在关闭的花括号外面,你实际上写上,数据类型。

你想发明的,那么我们如何可以,更强大地使用它呢?好吧,让我们继续做一些事情。

在没有这个功能的情况下,以错误的方式进行,这样可以激励其存在。

让我继续将这个文件保存为phonebook.c,然后像往常一样以#include cs50.h开始。接着让我继续包含#include stdio.h,最后让我也包含#include string.h,因为我知道我需要字符串函数,让我继续为这个程序的第一个版本设置一堆名字。

具体来说,布赖恩,戴维,我们保持简短,仅仅作为数据而已。他们在这里。然后布赖恩和我各自有电话号码数组,前面是,呃加上1 617 494 951000,确实,库尔茨的评论已经激励我们使用字符串,因为里面有加号和几个短横线。

然后我的数字在这里,所以我们做加法,,花括号和分号。因此,我已经声明了两个数字,我将进行一种默契协议,确保名字中的第一个与数字中的第一个对应,名字中的第二个与数字中的第二个对应,你可以想象这将如何工作。

好吧,只要你不犯任何错误,并且每个元素的数量刚好合适,现在让我。比两个更多的数字,我将暂时保持硬编码,只为演示。然后在这个循环里,让我继续搜索我的电话号码,直到结束。因此,如果strcmp(names[i], "david") == 0,我就不。

我不会再犯那个错误了,让我继续在这个循环里,在这个条件里进行下去。我将继续进行打印,比如说我的数字,我将把它插入,所以数字的括号是i,然后像之前一样,我将继续返回零。如果这个数组里没有,我将继续像之前一样打印“未找到”。

以分号结束,然后我将返回1,我可以返回负一百万,但你从1、0到1到2到3,如果有那么多可能的错误条件。好的,所以我在C语言中基本上实现了一个电话簿,算是一个零。现在我在代码中实现它,这是一个有限的电话簿,它只有两个名字。

有两个数字,但我当然可以通过使用Honor系统来实现这个电话簿,确保名字的第一个元素与数字的第一个元素对应,依此类推。现在希望如果我没有打错,让我继续做这个电话簿,好的,它编译成功了,运行命令./phonebook

它找到的似乎是我的号码,所以看起来工作正常。虽然我之前试图将那个号码拉过来,但我很确定这个实际上是正确的。

所以我们找到了我的名字和实习生编号,但为什么这个代码的设计不一定是最好的呢?这开始变得更微妙,确实如此。我们已经看到可以以不同的方式做到这一点,但这里让你感觉不舒服的是,这又是我们可能称之为代码异味的一个例子,感觉有点奇怪。

啊,这可能不是最好的解决方案。我想的是,像你之前创建数据框一样,将新的数据结构组合在一起。在这种情况下,我们只寄希望于不从相同的链中搞砸。

一般来说,你的直觉是正确的,作为一个程序员,虽然你可能渴望成为更好,但你并没有那么完美,你会犯错误。而且你编写的代码越是反映出你自己,你的代码就会越正确,你将能够更轻松地。

如果你选择在现实世界中合作,参与真实的编程项目,比如一个研究项目,那么一般来说,你不应该完全信任自己或其他与你一起编写代码的人。你应该有尽可能多的防御机制,就像这样,因此,虽然这听起来是对的,但如前所述,如果你出错了。

也许你会遇到错位错误,或者交换了两个名字或两个数字。想象一下,如果你有几十个、几百个,甚至几千个名字和数字,出现顺序错误的几率可能会太高,可能会出现混乱。

保持相关数据在一起,这是我的数组对齐。我只会确保它们长度相同。我们可以做得更好,让我们保持相关数据在一起,并更干净地设计它。我可以通过定义自己的类型来做到这一点,举个例子,称之为“人”。

在主函数之前,我将定义一个结构体,其中包含我关心的两种数据类型。请注意,我所做的不是给自己一个数组,而是给自己一个名字和一个数字。在这个花括号外面,我将给这个数据类型一个名字。

我可以给它任何名称,在这个例子中。而现在在这里,我将稍微改变一下这个代码,我将仍然给自己一个数组,但这次我将给自己一个“人”的数组,我将以某种俏皮的方式称呼这个数组为“people”。在这个程序中,我和布莱恩现在想要。

去填充这个数组,我想用值填充它,以便我们能够实际存储值在结构内。如果我想索引这个数组,我就用people[0],这将给我第一个人变量,所以可能布莱恩应该在这里。我需要的最后一部分语法是,如何进入里面。

结构化那个个人数据结构,并访问个人的名字。我实际上只需做一个点,因此people[0]给我第一个人,然后点表示进入并抓取。我要继续将这个名字设置为“布莱恩”。

现在他的名字的语法几乎是相同的people[0][617495];同时,如果我想访问我的位置,我将继续并设置location,名字将是“戴维”,然后在这里我将做people[1].number = "494"

六八二七,有点冗长,诚然,但你可以想象,如果我们让思绪自由奔放。如果你使用getstring,你可以在某种程度上自动完成这项工作;如果你使用命令行参数,或许可以填充一些内容。我们不必仅仅将它硬编码到这个程序中,你可以想象更灵活的做法。

动态地使用我们的一些技术,使用getstring等。从第一周开始,但现在只是为了演示,所以如果我想搜索这个新数组,这个单一的人员数组,我认为我的for循环可以保持不变,我仍然可以使用str_compare,但现在我需要进入的不是名字。

但是在people中查找点名称字段,所以数据结构中有字段或变量,所以我也将使用点表示法,进入people数组中的第i个人并比较“戴维”,如果我在这种情况下找到了戴维,继续访问people数组,再次用printf打印。

所以再次强调,点操作符是唯一的新语法,让我们能够进入这个被称为数据结构的新特性。如果我继续再做一次电话簿,在做完这些更改后,一切正常,编译也没问题,如果我再次运行./phonebook。这里似乎是一个无用的练习,我所做的其实就是。

重新实现同样的程序,使用更多。

复杂,但现在设计得更好,或者说是朝着更好的设计迈出了一步。因为现在我将所有内容封装在一个变量中。

例如people[0]people[1],所有我们关心的信息都与布莱恩,或我,或任何我们可能放入这个程序的人有关,实际上这就是。

程序是谷歌和脸书这样的公司如何存储大量信息的方式,考虑一下你的任何社交媒体账户,如Instagram、Facebook或Snapchat,以及与您在所有这些平台上相关的数据,不仅仅是你的用户名和帖子,还有你的朋友和粉丝。

所以这些公司收集关于我们的大量信息是有利的或不利的,大数组。

所有的用户名放在一个大数组中,所有的密码放在一个大数组中,所有的朋友,如你所想,确实在规模上这肯定是一个糟糕的设计,只是相信你会正确地排序所有这些东西。他们并没有这样做,而是用某种语言编写代码,某种方式进行封装。

所有与我、布莱恩和你相关的信息,存储在某种数据结构中。这就是他们放入他们的数据库或其他服务器的内容。在他们的后台,这种封装是我们在C语言中现在拥有的一个特性,它允许我们创建自己的数据结构,随后可以使用。

为了将相关数据放在一起,好的,有关数据结构或更具体地说,typedef和struct的任何问题,这些是C语言的关键字,通过它们你可以创建自己的自定义类型,呃,所以在main外定义新的数据结构是否典型,像在头文件中,真是个好问题,它在main外是否典型并不重要。

因为在这个程序中我只有一个函数,但正如我们本周、下周及之后将看到的那样,我们的程序会开始变得有些复杂。因为自然而然会有更多的功能,一旦你有更多的功能,你可能会有更多的函数,而当你有更多的函数时,你也会有更多的。

所有这些函数,我们将开始看到一些这些结构的定义,实际上是在我们自己的函数、类和头文件之外,或者我们是否会继续在main外定义它们,这真是个好问题,我们是否会在头文件中定义我们自己的类型和数据结构,最终我们会做到这一点。

迄今为止,你我只使用了别人写的头文件。我们一直在使用标准输入输出头文件(stdio.h)、字符串头文件(string.h),这些是C语言的作者创建的,你一直在使用CS50.h,这是工作人员编写的,结果是你也可以创建自己的头文件,你自己的.h文件,其中包含一些代码。

你想在多个文件中共享你的内容,我们还没有完全到达那里,但没错,彼得。

这也是解决这个问题的一种方案,将其放在一个地方。我当时在想,解决这些问题需要足够的信息,因为我觉得有些误导。我是一名新生,我在专注,但我无法继续。关于这些问题,有没有我遗漏的?这是一个非常好的问题,确实如此。

嗯,确实记得从零周的“消防水管”隐喻,这个我借鉴自MIT。这个案例中,有很多新语法和新概念一下子涌现,但当涉及到单独的问题和问题集时,要意识到你应该去完成那些工作。

复杂的内容,在每一节讲座中,以及通过课程网站上预先制作的示例进行复习,总是有一些小线索、提示或示例,你可以去做,比如实验室等,你会看到额外的构建块,所以,随时可以更个别地联系我。

之后很乐意向你推荐一些资源,实际上最近你会注意到课程网站上我们所称的。

shorts 是由lloyd制作的较短视频。

这些实际上是关于非常特定主题的短视频,所以在今天之后。你会看到Doug制作的短视频,提供对线性搜索、二进制搜索以及其他一些算法的不同视角。嗯,我想知道我们有什么返回值。比如我们有什么示例。

有几种不同的情况,我们想要以某种方式跟踪它们。实际上正是后者,所以现在说实话。

值得花时间返回零或返回。

因为我们没有使用信息,但我们想要做的是铺垫。

为更复杂的程序打下基础,实际上这周和下周。

更长的时间,当我们开始提供课程时。

伴随起始代码或分发代码,也就是员工提供的代码行。

我写下的内容是你接下来必须建立的,这将是一个非常有用的机制,以便能够标示出这些内容。

错误或其他事情出现问题,所以我们所做的只是准备。为了那种不可避免的情况。

如果现在似乎没有解决什么问题,我只是想快速问一下。显然在这段代码中我们有人员,所以假设我们有10、20甚至30个人,我知道这是聊天中的一个问题,但我只想为自己澄清一下。然后这个“如果”意味着什么,什么会改变,或者这个问题的结局是什么。

嗯,什么会改变代码或什么问题,啊,好问题,所以如果我们有更多名字,比如第三个名字或第十个名字,唯一需要在这个版本的程序中更改的事情是,首先在第14行,people的大小。我们需要提前决定我们将有10个人,最好是。

例如,我可以在这里分配一个常量,所以让我实际上回到这里,就像我们在以前的课程中做的那样,做类似const inst等于10的事情。请记住,const意味着常量,这意味着这个变量不能改变。int当然意味着它是一个整数,我将它大写只是为了。

人类习惯使得在视觉上稍微清晰一点,以便你不忘记这是一个常量,但它没有功能作用,然后,这当然只是一个分配给数字的值。然后我可以在第16行下去,插入那个变量,以便我不必硬编码人们所称的神奇数字,这只是一个出现的数字。

现在我似乎突然之间将我所有的特殊数字放到了文件中。现在我在使用这个变量,然后我可以做的事情,我之前只是口头提到过,我可以绝对开始硬编码,例如蒙太古的名字和号码,其他的,但老实说,这似乎有点愚蠢。

如果你只是硬编码所有这些名字和数字,而在几周后用相同的信息,例如电子表格或称为CSV文件的逗号分隔值,甚至在一个适当的数据库中,像Facebook和Google这样的公司会使用。但我现在可以做的事情是类似这样的,int i等于0,加上。

也许我可以这样做,people[i].name = get_string("名字是什么?"),然后在这里我可以做people[i].number = get_string("他们的号码是什么?"),我也可以问那个问题。

现在程序设计得更好一些,我不是布莱恩。现在它是动态的,技术上是当前的,但我也可以让它动态,我也可以调用getint,或者像你上周那样使用命令行参数,并将代码参数化,以便它实际上可以适用于两个人或十个人。

不管你想要什么,程序都可以动态适应。关于结构或类型的其他问题吗?没有,好吧,那么我们是如何到达这里的?搜索,我们只是想找一个人在门口,我们只是想在数组中找到某个人,我们在某种程度上将事情升级得很快。

快速找到的不仅仅是数字或名字,而是现在,这些数据结构中带有数字的名字,但要有效地做到这一点,确实需要一个更智能的算法,比如二分查找。到目前为止,我们只在C代码中使用了线性搜索,即使如此。

请回忆一下我们手头有这个伪代码。

二分查找,但是通过二分查找,已排序,所以如果你想获得更快的搜索速度,必须有某种方式将数字排序。

必须为我们做到这一点,比如乔就在幕后为我们整理了所有这些数字,但他用了什么算法,这将如何进行。

高效地对数字进行排序,实际上,如果你是谷歌、脸书和世界上的Instagram。

在用户中,你当然希望保持数据排序。

大概是这样你才能使用像二分查找这样的算法快速找到信息。我们来吧,休息五分钟,然后谈谈用于排序的算法,这将使我们能够。

哈佛CS50-CS | 计算机科学导论(2020·完整版) - P7:L3- 算法(结构体、搜索与排序)2 - ShowMeAI - BV1Hh411W7Up

做一切。

好吧,我们回来了,简单回顾一下,我们有几种不同的算法,搜索。二分查找显然是所有衡量标准中,胜出者。关键在于数据需要,提前排序,以便应用该算法,所以,让我们给自己一个可行的。

对于排序某样东西的模型,像往常一样,如果你想到了,解决。它有输入和输出,目标是将该输入转换为。输出,那么输入是什么呢?它将是一堆未排序的值。目标当然是得到已排序的,值,所以有趣的部分在于,中间。

但为了更加具体,如果我们现在考虑这个。未排序的输入作为一个输入数组,因为毕竟这或许,至今。为了同时传递一堆值,仅使用一个变量名。我们可能有一个这样的数组,63852741,这似乎确实是随机,排序的,即未排序。

我们想把它变成,三四五,六七八,所以八个数字,这。

时间不是七,而这次的目标不是搜索,值本身,而是对它们进行排序。但在我自言自语之前,有人能对此整个智力练习,提出反对意见吗?我们即将进行,排序的事情,比如,有人能争论一下。为什么我们可能不想费心使用,费心,排序这些元素,反正就让我们这样。

使用线性查找,找到某个元素,无论它是否是,门后面的一个数字。

在数组中一个名字,比如我们什么时候可能会,想要使用线性查找。

而且不需要去排序,呃,索非亚,我们该做什么。

这可能在某种程度上,比如如果我们能找到某个东西,而线性查找我们知道我们可以找到它。好的,公正,我承认,实施二分查找并非伪,代码。实际上更困难,因为你必须处理四舍五入,特别是如果是奇数。门与偶数门,或这些长度的数组。

老实说,你必须处理这些,四舍五入,因为每当你将某物除以。二时,你可能得到一个小数值,或者,你可能得到一个整数,所以我们。

需要做出一些决策,所以这是完全可解决的,人类几十年来一直在编写。实现二分查找的code

完全可能,你可以使用库,但这确实更具挑战性,而且你会面临风险,但让我声明这是可以的,我,我在我的进程中,已经足够好到,我相当确定我能正确实施它。所以正确性不是我的,担忧。

排序,一个元素数组,是什么可能让我动机啊。

就用线性搜索吧,它太简单了,谁能提出为什么,奥利维亚,如果游戏的目标是效率,那么,你不妨直接搜索,而不是排序,这会额外花费一些开销。是的,真的说得好,如果你有一个相对较小的数据集,而你的计算机的处理速度达到十亿。

每秒操作次数,例如,天啊,如果你的代码糟糕且有点慢谁在乎呢?只需用不高效的方式去做,为什么呢?因为实现一个更简单的算法,比如线性搜索,可能只需几分钟。尽管它的运行时间会更长,而这可能需要你花费数十分钟。

可能需要几分钟,甚至一个小时,不仅要编写,还要调试像更复杂的算法。比如二分搜索,此时你可能花了更多的时间。

编写代码比运行慢代码更快,你只需这样做,我可以说。

这让我想起我研究生时期,我所做的一些研究涉及到非常大数据的分析。

集合,我必须编写代码来分析这些数据,我可能花了几个小时、几天,甚至。编写我能设计的最佳算法,以尽可能高效地分析数据。或者说,我可以写一个糟糕的版本的代码,然后去睡觉。

八个小时,我的代码将在早上生成我想要的输出。

这是一种非常现实且合理的权衡,确实,在接下来的几周里,这将成为课程的主题。将会有这种权衡,而这种权衡往往是时间、复杂性,或者你所使用的空间或内存量。

而成为程序员的艺术之一,就是尝试决定界限在哪里。

你需要在前期投入更多的努力,以创造一个更好、更快、更高效的算法。

或许在那里可以稍微简化一些,所以资源。

挑战性问题,因此我们在课程的问题集和实验室中总是会规定。最重要的是什么,但在几周后,你将会实现你自己的拼写检查器,其中的目标之一是。最小化你的代码运行所需的时间,以及最小化占用的空间或内存。

运行,所以我们会越来越欣赏这些权衡。但确实如此,我真的很喜欢奥利维亚的说法。如果你的数据集很小,可能不值得编写最快、最好设计的算法,只需简单而正确地编写即可。

快速得到答案并继续前进。

但这并不是许多问题的情况,他们说大多数生活中的问题。

如果你正在构建Facebook、Instagram、WhatsApp或任何今天最受欢迎的服务,它们在短时间内获取成千上万的新数据。你不能仅仅线性搜索你在LinkedIn上的所有朋友或连接,高效地进行,你不能这样做。

线性搜索谷歌和微软在其搜索中索引的数十亿网页。

而且无疑,你的程序、代码、网站、应用程序等越成功,设计就越重要。因此,目标不是一次性搜索这些门,目标不是一次性搜索这些灯泡,目标不是一次性搜索电话簿,而是一次又一次。

如果是这样的话,我们可能应该花更多的时间和一点复杂性,提前让我们的代码不仅正确,而且高效。这样我们才能从这种效率中一次又一次地受益。

也许让布莱恩帮忙,布莱恩,你愿意帮忙排序吗?是的,当然可以,我有八个数字,你可以开始排序这八个数字。我们将把它们按照排序顺序放好,确实,我同意,现在让我们听听观众的意见,有人愿意解释一下布莱恩是如何排序的吗?

那么这八个数字,布莱恩是如何一步一步地达到最终结果进行排序的呢?彼得,你看到他做了什么,呃,他一步一步地检查,如果它们没有排好,就一直进行下去,直到它们全部正确。他不断地寻找小值并将其移动到左边。

并寻找大值并将其移动到右边,因此有效地一次选择一个数字。如果你愿意,布莱恩,我们可以看看更慢一点的过程。如果你能更有条理地进行,我看到你已经将数字重置为它们的原始无序状态,为什么我们不更有条理地开始呢。

你能再慢一点,选择最小的值吗?因为我认为,彼得,它需要放在最左边。呃,当然可以,我正在看这些数字,1是最小的。好的,我现在有了最小值,你做得很快,但。

我觉得你太随意了,作为一个人可以有这种感觉。但如果你能更像计算机一点,如果这八个数字实际上是一个数组。就像我这里的七扇门一样,每次只能看一个数字。你能不能更有条理、更谨慎地告诉我们如何。

你找到最小的数字放入位置,当然,我想既然计算机一次只能看一个数字,我会从这个数组的左侧开始。然后逐步向右查看每个数字。所以我可能从六开始,问,好的,现在这是。

这是我目前看到的最小数字,但我再看下一个数字,六。所以现在的三是最小数字,继续查看,八比三大,所以我。无需担心,五也比三大,二比三小,所以。现在这是我找到的最小数字,但我还没完成,所以我会继续。

看看七比二大,四比二大。但一比二小,所以我现在已经走到了数组的尽头。可以说一是我找到的最小数字,好的,所以我听到的是你在做这些比较,和彼得暗示的也很相似。

你一直在检查这个小吗?这个小吗?这个小吗?你在跟踪当前看到的最小数字。是的,听起来没错。所以你找到了,我觉得它应该放在开头。那么我们现在如何将其放入位置?是的,我想把它放在开头,但没有真正的空间。

所以我可以通过将这些数字移动来为它腾出空间。好的,等等,但我觉得你刚才增加了工作量。我觉得。不要这样做,那感觉你要做更多步骤,比我们需要的多。我们还可以做些什么?好的,另一个选项是它需要。

像这样在数组的第一个位置,所以我可以把它放在那里。但如果这样做,我必须把现在在那里的六拿出来。好的位置,但六不在。我同意,但我认为这样没关系。因为这些数字是随机开始的,所以六在错误的位置。

无论如何,我认为通过仅仅移动它,我们并没有让问题变得更糟。事实上,我认为交换两个数字,把一个移动到另一个,反之亦然,然后移动之间的所有数字,这样会更快。是的,所以我把一从数组的最末尾位置拿出来,全部在右侧,所以我。

我想我可以把六放在那里,因为那是,数字。是的,虽然不完全在正确的位置,但没关系,所以我。喜欢这样,但现在这个一在正确的位置上,而且你确实照亮了它,表明这一点,我觉得我们可以。

从此基本上忽略这个一,现在只选择下一个最小的元素。所以你能带我们走过这个过程吗?是的,我想我会重复同样的过程,我从三开始,这是我目前找到的最小数字。我会继续寻找,八更大,三,二比三小。

我会记住二是我见过的最小的数字。然后我只需要检查一下是否有比二更小的。我看看七、四和六,这些都没有比二小,所以我可以说二是下一个最小的数字。

那个需要放在第二个位置,所以我需要把三拿出来。我想我可以把三放到这个空位置,那里有可用的空间。是的,我觉得这开始变得清晰了,我们进入了一种循环,因为你几乎又讲了同样的故事,但换了一种方式。

不同的数字,你介意继续算法直到结束,选择下一个最小的、下一个最小的、下一个最小的并使其排序吗?当然,所以我们得到了八,五比那个小,三比那个小,然后剩下的数字是七,***** 这些都是更大的。

所以三将放入已排序的位置,我会把八换掉。现在我会看 5、8 和 7,5,但六更大,所以四是我目前见过的最小数字,所以四将放在这个位置,我会把它和五交换。现在我有了八,七比八小,所以我会记住。

五比那个小,但六比那个大,所以五将是下一个数字。现在我剩下七,八更大,所以七仍然是我见过的最小的,但六比最后两个小,而在最后两个之间,八和七,七是更小的,所以七将放在这个位置。

目前我只剩下一个数字,所以这个数字必须在已排序的位置上,现在我可以说这是一个已排序的数字数组,看起来确实是正确的,感觉有点慢,但当然我们使用的是实际的数组,如果你不介意观察一下,看起来如果我们有八。

一开始有多少个数字,或者说 n 个数字,进行了 n 减 1 次比较,因为你一直在比较数字,其实你做了 n 次比较。你看了第一个数字,然后又一遍一遍地比较了所有其他可能的值,以找到最小的元素。

是的,因为对于每一个数字,我要看看它是否比最小的那个小。然后我需要记住这一点,所以在每一轮中,你考虑了每个数字,总共 n 个数字,首先直到你找到了一个数字,接下来要清楚 n 减 1 的数字,然后是 n 减 2 的数字。

减去三个数字……一直到最后一个数字。所以我认为这是正确的,我认为,这是一种相当有意识的方式。来对这些元素进行排序,比起你最初的方法,布莱恩我可能会描述为更有机一点,你有点像。

更像是人类,只是稍微观察一下,然后移动东西。但如果我们要把这个,翻译成代码,请记住我们必须非常,精确。所以让我考虑一下,究竟如何将布莱恩所做的,最终再翻译成。伪代码,所以他所做的实际上是一个有名字的算法,叫做选择。

排序,为什么呢?因为最终它是在排序这些元素,而是通过让,布莱恩或实际上是计算机,一次又一次地进行操作,一旦你找到了每个这样的小,实际上忽略它。确实,每次布莱恩亮起一个数字,他并不需要继续比较它。所以我们所做的工作量,在每次迭代中都在减少。

n 个数字,然后 n 减 1,然后 n 减 2,n 减 3,等等,所以我们可以把这个算法的运行。时间视为它实际的伪代码。那么我们该如何定义这个。伪代码呢?让我提议我们把它想象成这样,从 0 到 n 减 1 的 i。现在,毫无疑问这是。

可能是最难懂的行,屏幕,但再说一次,这正是我们本能地使用代码看到的类型,看看你如何写一个 for 循环。for 循环通常按惯例从零开始计数,但如果你有 n 个元素,你并不想,数到 n,而是想数到 2 n,或者等效地数到 n 减 1。所以。

从 0 到 n 减 1。好吧,现在在第一次迭代中,我想要做什么,项。和最后一项,所以这一点在第一眼看上去并不是很明显,但我认为这公正地描述了,布莱恩所做的,因为如果 i 初始化为零,那么就是架子上最左边的第一个,数字,而他接下来做的是找到。

最小元素,在第 i 个项与第一个项零之间,以及最后一个项,所以这有点像是。非常花哨的说法,布莱恩找到了,所有 n 个元素中的最小元素,然后他所做的就是将最小项与,第 i 个项交换,所以我们就这样,交换了一切。他只是通过将它与,错误位置中的值交换来为它腾出空间,但。

现在在这个循环的下一次迭代中,考虑一下 for 循环是如何工作的,你执行 i 加 1。在伪代码中,这正是这里发生的事情,所以现在 i 等于 1,项。项 1 零索引和最后一项,所以这是一种花哨的说法,布莱恩再次检查所有的。n 个元素,除了第一个,因为现在你是从位置开始的。

从位置一而不是零,现在算法继续进行。所以你可以用不同的方式用英文写出这段代码,就像伪代码,但这似乎是对该算法的合理表述。让我们更直观地看看,人类在数字中移动,让我。

继续使用这个可视化,我们将在课程网站上放一个链接。如果你也想尝试一下,这只是某人的数字数组的可视化,但这次不是用符号、十进制数字来表示数字。现在这个人使用垂直条,就像条形图一样,这意味着。

小条就像小数字,大条就像大数字,所以目标是对这些条进行排序,这也可以看作是从短条到高条的数字排序,从左到右。我将继续在菜单顶部选择我的排序算法,就是我们刚刚描述的那个。

回忆一下选择排序,注意一下,我想要花点时间理解这里发生的事情,但注意这条粉色线是从左到右移动的。因为这正是布莱恩所做的,他在数字架上来回走,寻找下一个最小的数字。

他把最小的数字放在左边,正好在它应该的位置上。

的确,这就是为什么在这个可视化中,你看到小数字开始被放置在左边,我们不断地扫描,但注意,彩色条逐渐向右移动,就像布莱恩不再回头一样,一旦他点亮了数字,就让它们保持不变。

瞧,这些数字现在都已排序,这只是以图形化的方式来思考相同的算法,但那样效率如何呢?让我们看看能否在这里应用一些数字,但还有其他的方法可以做到,所以如果第一次通过数字时,他有八个数字可用,他。

查看所有八个数字的顺序,所以这是最初的结束步骤,下次他经过货架时,他忽略了已经被点亮的数字一,因为根据他已经做过的定义,它已经在正确的位置上。现在他只需进行n减一的步骤。然后他进行了n减二的步骤,接着是n减三、n减四。

从五,一直到最后一步,他只需找到并留在原地的数字八,因为这是最大的数字,所以只需一步。这是某种数学序列,你可能会记得在数学书的最后一页或高中的时候。

在你的物理教科书或类似的书籍中,结果证明这实际上归结为这个公式。n 乘以 n 加 1 除以 2。如果你对此不熟悉,没关系,只要让我说明,我们开始于 n 加 n,减去 1,加 n 减去 2,加 n 减去 3,依此类推,最终简单地归结为更简洁的 n 乘以 n 加 1 除以 2。

这个 squred 加上提议,给我们的是是的,这个 n squred 除以 2,加上 n 除以 2。所以如果我们真的想要斤斤计较,这是总步骤数或操作数或秒,无论我们想如何衡量,布赖恩的运行时间,这似乎是精确的数学公式。

但是在本周初,我们再次考虑了那种大 O 符号,挥一挥手,我们更关心的是算法运行的数量级,我真的不在乎这些,分之二和 n 除以二,因为当 n 变大时,哪一个因素会重要。

电话簿越大,我们拥有的门越多,灯泡越多,架子上的数字越多,n 会不断变得越来越大,考虑到这个主导因素,如果我们可以请来某个人,哪一个因素,n squ*red 除以 2 还是 n 除以 2,问题。

随着 n 的增大,变得越来越大,主导着,没有问题的将是 n squred。是的,n squred 对于任何 n 的值,你只需将其平方,结果会比仅仅做 n 除以 2 的值大得多,因此在我们的大 O 符号中,我们可以描述布赖恩的运行时间在 n 的数量级上。

squred 是的,我忽略了一些数字,是的,如果我们真的想要斤斤计较并计算布赖恩每一步,确实是 n squred 除以二加上 n 除以二,但再说一次,如果你考虑这个问题的时间,以及 n 变得非常大,类似 Facebook、Twitter 和 Google 的规模,数学上真正主导的就是这个。

这里更大的因素会使总步骤数远远大于那些较小的有序项,所以在大 O 符号中,选择排序似乎是在 n squ*red 的数量级上。所以如果我们考虑之前的图表,其中我们对线性和二分搜索算法的上界进行了划分,这个是。

不幸的是,这真的是个小问题。

这个特定运行时间列表的顶部,还有无数更多的。这些只是计算机科学家可能使用和思考的更常见公式的子集,选择排序算是列表的顶部,排在第一位显然是糟糕的,n squ*red 绝对比常数时间慢得多。

或者说一步,所以我在想我们是否可以做得更好,我想知道。彼得实际上之前说过一些其他的事情,关于比较两个数字并修复它。让我建议我们让布莱恩回到你这里,看看可能会被称为其他算法的东西。

冒泡排序,冒泡排序是一种不同的算法,它试图更局部地修复问题。实际上,布莱恩,如果你看看你面前的数字,你的位置,我感觉如果我们关注小数字,就像上次我们试图解决的问题那样,如果我们只看数字对。

那些相邻的数字,我们可以稍微做一些小的调整,比如说。布莱恩,六和三,你能给我们什么观察?是的,当然可以,六和三是数组中的第一对数字。如果我想让数组被排序,我希望较小的数字在右边。

所以只看这一对,我可以告诉你六和三是乱序的,三应该在左边,而六应该在右边。好的,那我们来做吧,修复这两个,只修复一个小问题。现在让我们重复这个过程,对吧,我们的算法,所以六和八是下一个这样的对,那是什么呢?

那一对似乎没问题,因为六比较小,而且它已经在左边,所以我想我可以让这一对保持不变。好吧,八和五呢?呃,八比五大,所以我要交换这两个,五应该在八的左边。好的,八和二也是一样的情况。

这里八比二大,所以八要和二交换。好的,八和七,八比七大,所以我应该和七交换,好的,八和四也是一样,八比四大,八和一我可以最后再做一次。

这次八比一大,我已经交换了,并且有一个很不错的戏剧性效果,如果你走到一边,瞧,实际上没有排序,事实上,看起来并没有好多少,但我确实认为布莱恩做了一些聪明的事情。布莱恩,你能谈谈至少一些边际改善吗?

你做出的改进是的,至少有一些改善,最初的一个向后移动了一格,而我认为另一个改善是,八最初在某个地方。但因为八是最大的数字,我不断地将它交换,直到它完全到达了末尾。所以现在实际上我认为这个八是在正确的位置。

它最终移动到了数组的右侧。是的,这就是我们稍后会看到的算法的名称来源。冒泡排序暗示着最大的数字开始。逐渐上升到列表的顶部或末尾,注意。

正如布赖恩所做的那样,数字一只移动了一位,所以显然还有更多的工作要做,那也是错位的,但我们已经改善了一些,八在位置上,而一更接近于到达位置。那么接下来我们可能怎么做呢?好吧,布赖恩。让我们继续解决一些小的、易处理的问题,从。

再次开始,三个和六,确保这三个和六看起来是有序的。所以我就不动它们,六和五,六和五是不对的,所以。要往右移动,六和二也是不对的,所以我将二和六交换。六和七,六和七是好的,它们是有序的,七和四。

那些是不对的,所以我将四和七、七和一交换。那两个也不对,七已经到达排序位置。确实如此,现在我们正在取得一些进展,七已经冒泡到列表的顶部,停在八之前,而一的位置。

所以我敢打赌,布赖恩,如果我们不断这样做,只要列表保持部分未排序,我想我们可能会到达终点。你想接手并排序剩下的部分吗?好的,当然,所以我再重复一遍这个过程。三和五是好的,而二和五是不对的。

所以我将它们交换,五和六作为一对是好的。是不对的,六和一也是不对的,所以我将它们交换,现在六,我可以说是处于正确的位置,我再重复一遍,三和二是不对的。

所以它们被交换,三和五是好的,五和四是不对的,所以它们被交换,然后五和一也需要交换。所以现在五在排序位置,而我剩下的四个,二和三是好的,三和四是好的,但四和一是不对的。

所以它们被交换,现在四在它的位置上,二和三是好的,但。那些,现在三进入了它的排序位置,然后最后一对需要考虑的就是二和一,呃,它们是不对的,所以我将它们交换。现在二在位置上,而一是唯一剩下的数字。

我可以说那个在位置上,二,现在我认为我们有一个排序好的数组。不错,所以这感觉像是一种根本不同的方法,但我们。仍然达到了相同的终点,这确实引发了一个问题。泡排序是否更好,还是更糟,或者也许没有区别。

但也要注意,我们从根本上用不同的方式解决了同样的问题。第一次我们采用了更人性化、自然的直觉,只是找到最小的元素,好的,重复一遍,重复一遍,这次我们从不同的角度看待问题。

列表未排序,正如彼得指出的,当事情像这样失序时,非常基本的原始状态表明了一种方式。只需修复所有微小的循环,如果我们重复这个直觉,它最终会通过修复所有小问题而得到回报。

直到大的一次似乎会消失,让我回到之前的可视化。重新随机化条形,短条形是小数字,大条形是大数字。让我去运行气泡排序算法,你会注意到,现在的颜色是两个,再次,你会看到这次条形。

它们表现得有点聪明,并且不是每次都走到最后。就像布莱恩照亮了数字,停止查看8和位置一样。但是他和这个可视化确实会返回到开始,进行另一次遍历,再一次遍历。

总共进行了多少次比较呢?

这次似乎是第一次,通过条形或者同样的,通过货架,布莱恩在这个可视化中进行了n-1次比较,从左到右比较n个元素的n-1个相邻元素,直到n-4和n-5。

当剩下两个或一个时,你就完成了。虽然这个算法在根本上达到相同的目标,但它成功地对元素进行了排序。让我们考虑它在代码中的实现,以及它是否真的快一点或慢一点,让我们设置一个最终的边界,实际上是两个。

在选择排序中,我们有一些可以比较的内容。让我们考虑一下选择排序在最佳情况下的下限场景,如果你有n个元素,并且不断寻找下一个最小元素,实际上,这里我们的朋友就是这个图表。

在我们讨论的Omega符号中,线性搜索和二分搜索可能会非常幸运,只需一步,如果你正好在寻找,正如我们在布莱恩和可视化中实现的那样。

不幸的是,这并不好,因为每次他搜索一个数字时,都是从左到右。公平地说,他确实忽略了已经到位的数字,所以我们没有继续查看那个他没有保持在的位置的数字,而是多次触碰那些数字。

所以即使你和我,人类可以看这些数字并且觉得,显然这是一个,显然这是两个,显然这是三个,布莱恩必须以更系统的方法来处理,事实上,即使那串数字是完美排序的,他也会浪费同样多的时间,事实上,布莱恩,如果你不介意,再说一遍。

布莱恩,如果我们从一个已排序的列表开始,这是一种有趣的扭曲,供你考虑。从算法的角度来看,在分析算法时,有时你想考虑最佳情况和最坏情况,而似乎没有比已经排序的列表更好的情况了,你运气真好,实际上没有工作要做,最坏情况是列表可能是。

完全相反,这是一项巨大的工作量要完成。

不幸的是,选择排序并没有真正优化这个幸运的情况,即它们已经排序。所以布莱恩,我看到你已经将数字从左到右重新排序。如果我们像之前那样重新执行选择排序,你会如何去找最小的数字。我们之前决定,要找最小的数字,我需要。

从左到右查看数组中的所有数字,每次检查是否找到更小的东西,所以我会从目前为止看到的开始,但我必须继续查看,因为可能后面会有零或负数,我需要检查是否有更小的东西,所以我会检查二是否更大,所有的都更大。

所以结果证明我一直是对的,那个是最小的数字,已经在正确的位置,所以现在,那个数字在正确的位置,然后要找到下一个最小的数字,你会怎么做,我会做同样的事,二是我目前找到的最小数字。然后我会查看所有数字,除了二,我会查看三、四、五、六。

七、八,没有比二更小的,所以我会回到二,说明那个数字现在必须在它的排序位置上,确实如此,而这个故事对于三、四、五也是一样的,选择排序的伪代码或实际代码中并没有任何智能,去判断数字是否已经排序,如果已经排序就停止。

就像没有机会提前中断并终止这个算法,布莱恩实际上会做同样的工作,无论它们从一开始就是完全排序还是完全未排序,甚至是反向的,所以,非常,非常希望冒泡排序确实会朝着这个方向发展,让我们来看看一些提议的冒泡排序伪代码。

假设输入可以是任何内容,无论是已排序还是未排序,伪代码总是会像这样,直到n。

从第一个元素到最后一个元素,范围是0到n减2,表示从第一个元素到倒数第二个,为什么我这样做呢?我们马上就会看到,如果i的顺序,聪明。如果你考虑所有这些数字都在一个数组中或在门后面,如果你从零迭代到n减二,那就像从第一扇门到。

倒数第二扇门,但这很好,因为我的条件是检查门i和i加一。因此,如果我从这里的开始处开始,并且我只迭代到这扇门,那是好事。因为当我比较门时,门的比较是将门i与门i加一进行比较,而这并不存在,确实,这将导致一个错误。

可能你们所有人在某个时候都会遇到,涉及到一个或多个空间的内存问题。尽管没有分配内存,但数组中的某个位置可能会超出范围。所以这看起来似乎很聪明,实际上表现得可能与理想的状态一样好。以冒泡排序为例,假设这个列表,你,呃,排序并重启数字太多了。

你介意再给我们一次排序的列表吗?我想看看我们是否考虑到,和之前相同的已排序列表,这次使用冒泡排序。

我们能否从根本上做得更好?我有这段代码,说的是重复直到。排序,所以这可能会有什么变化?布莱恩,你又得到了已排序的数字。这应该是一个好例子,但选择排序并没有从这个输入中受益,尽管我们可能会幸运地使用冒泡排序,你的思维过程会是怎样的?

所以冒泡排序的思维过程,是一次检查每一对,看是否需要为那一对做交换。所以我会看一下一和二,如果二和三是可以的,我就不需要在这里交换,三和四也可以,同样五和六,以及八。

所以我完成了对所有,任何交换的遍历,因为每一对我查看的都是彼此,确实如此,因此这次如果布莱恩真的回顾了这些步骤,并在n减1个元素中再次执行,然后在n减2个元素中再执行,那就太愚蠢了。

任何工作、任何交换,在第一次遍历中,他实际上是在浪费自己的时间,甚至再做一次遍历或另一遍,代码,这个重复直到排序,尽管它并不完全可以转换成C语言中的for循环或while循环,但直观上它表达了他应该做什么:重复直到排序,布莱恩已经识别出这个事实。

由于他没有进行任何交换,这个列表就是已排序的。因此,他可以停止这个循环,我们可以将其更明确地映射到C语言代码中,我们可以默认说执行以下操作n减1次。因为在n个元素中,你可以查看n减1总对,但请注意。

我可以在这里添加一行额外的代码,可能会说。

这我可以说是增加一行代码,如果没有交换则完全退出算法,只要布赖恩跟踪他在一次遍历中做了多少交换或者没有做多少交换,使用一个叫做计数器的变量,他可以简单地提前中止这个算法,这样确实可以为我们节省一些时间。

让我们暂时考虑一下冒泡排序的运行时间。在最坏情况下的上界,如果你注意到冒泡排序,做某些事情,n-1次,所以再次重复n-1次,字面上说。执行以下操作n-1次的for循环,这只是一种不同的方式。

在伪代码中表达类似的思想,但这次给我们一个变量。对于i从0到n-1,n-2,总共是n-1次比较,因此这是一个n-1的事情,出现在repeat内部和repeat外部。所以我认为这给我的是n-1个事情。

n-1次乘以n,所以现在如果我将这个稍微整理一下,像高中或初中的数学,n² - 1n - 1n + 1。我们可以合并同类项,得出n² - 2n + 1。但根据我们之前的讨论,这真的有些复杂。谁在乎2n或1,当n变大时,主导因素肯定是n。

冒泡排序和公式似乎会有n²步的上界,所以在这个意义上,它与选择排序是等价的,从根本上来说并没有更好。我们可以说从渐近的角度来看,这个公式在所有意图和目的上是相同的,尽管它们在低阶项上略有不同。

就所有意图和目的而言,它们的复杂度大约是n²,但如果我们考虑一个下界,尽管冒泡排序有相同的上界运行时间,如果我们考虑一个下界,使用这个更智能的代码,布赖恩可能会注意到等一下,我没有进行任何交换。

我只是打算退出这个循环,提前结束,而不是过早结束。因为继续做更多工作是没有意义的,我们可以逐步减少这个运行时间。我认为,这并不如常数时间的Ω(1)那么好。就像你不能确定一个数组是已排序的。

除非你至少看过所有元素一次,所以常数时间是完全天真和不现实的,你不能只看一个元素、两个或三个就说是已排序的,必须至少看过元素一次。因此,这似乎暗示了冒泡排序运行时间的Ω符号是它的下界。

如果我们聪明一点,不要不必要地重走我们之前的步骤,这个复杂度是Ω(n),或者从技术上讲是n-1步,因为如果你有n个元素,比较这两个、这两个、这两个、这两个,总共是n-1次比较。但谁在乎减去的1呢?它是n的阶数。

这里是 n 或 omega 的 n 表示法,所以总结一下选择排序选择了,再一次。不幸的是,根据它的代码,它是大 O 的 n 平方,但它总是会在渐近意义上花费相同的时间,随着 n 的增大,看起来更好。在上界方面,它将花费多达 n 的时间。

平方步骤太多,但至少在使用像已经排序的输入时,它可以自我短路。平方是不好的,像 n 平方会迅速累积,如果你有 n 平方而 n 是一百万或者 n 是十亿,天哪,那可真是一大堆零。

在你的算法的总运行时间中,有很多步骤,我们能做得更好吗?我们能做得更好吗?结果证明我们可以,今天我们将考虑一个最后的算法,它从根本上做得更好,就像在零周时,我们稍微接触到二分查找一样,而今天,显然比线性查找要好得多。

我认为我们可以比冒泡排序和选择排序做得根本更好,排序可能是我在研究生院用来快速编写代码然后去睡觉的东西,但对于非常大的数据集,它不会工作,坦白说,如果我不想的话,它也不会工作。

只是睡过这个问题,相反,我们希望从一开始就尽可能高效地完成事情。让我提议我们利用一种技术,这是一种你几乎可以在任何编程语言中使用的技术,递归,简单来说就是一个函数调用自己。到目前为止,我们还没有看到任何示例,我们看到的是函数调用其他函数。

函数主,不断调用 printf 主已经开始调用 sterling 主。早些时候主调用了 stir comp compare,但我们从未见过主调用主。人们不会那样做,所以这并不能解决问题。但我们可以实现自己的函数。

并且有我们的函数可以调用自己,这在原则上看起来似乎是个坏主意。如果一个函数调用自己,天哪,这会到哪里结束呢?似乎会永远做某件事,然后可能会出问题。

这可能会发生,这就是使用递归的危险,你很容易搞砸。但是这也是一种非常强大的技术,因为它让我们以一种非常有趣的,敢说优雅的方式思考潜在的问题解决方案。所以我们不仅能够实现正确性,而且还。

更好的设计因为更好的效率,看起来在这里是这样。所以让我提议回顾一下这段代码,来自零周的伪代码。用于在电话簿中查找某人的伪代码的特点之一,就是这些行,这里回到第三行,我们在零周时描述过。

作为循环的代表,发生了一次又一次。但你知道吗,有一种技术叫做递归,称为迭代,它完全是基于循环。它字面意思是让我回到这一行,回到这一行,回到这一行,没有自我调用。但如果我改变第零周的伪代码呢?

让我更像这样,让我去掉的不仅仅是那一行,而是这两行条件。让我简单地说,而不是打开书的左半边中间,然后返回到第三行,或打开书的右半边中间,然后返回到第三行。

为什么我不更优雅地说,搜索书的左半边,搜索书的右半边。现在我可以立刻缩短一些代码,我声称通过只说搜索书,这就是足够的信息来实现同样的算法,但它并不是使用循环。人类会再次参与,但还有其他的方法来做事情。

一次又一次,而不是通过 for 循环、while 循环或 do while 循环。或者重复块或永久块,你实际上可以使用递归。递归是这种技术,函数可以调用自身。如果我们考虑一下,毕竟我们正在看的伪代码是搜索的伪代码。

在第七行和第九行,我现在字面上说,搜索书的左半边和搜索右半边。这甚至在伪代码形式中,已经是递归的一个例子。在这里,我用11行代码实现了一个算法或函数,九行代码字面上具体搜索电话簿的一半。

递归真正发挥作用的地方,简单直接调用函数自身,输入相同是愚蠢且错误的。因为如果输入始终相同,期待不同的输出简直是疯狂。

这不是我们在第零周做的,也不是我们现在正在做的。如果你使用相同的函数或等效算法,越来越小,函数调用自身可能是可以的,代码中非常智能地说如果你在页面上。你需要一个所谓的基本情况,需要一些代码。

那个会注意到,等等,这里没有更多的问题需要解决。现在退出,那么我们怎么将这映射到实际的代码呢?好吧,让我们考虑一些非常熟悉的东西。回想一下你重建马里奥金字塔时的样子。看起来有点像这样,让我们考虑这是一个砖块金字塔。

这高度是四的,y4,那么从上到下有一砖、两砖、三砖、四砖,所以这里的总高度是四。但让我问一个问题,你怎么去打印高度为四的金字塔?结果证明,这个简单的马里奥去掉了不必要的背景是递归的。

这是一种递归的物理结构,为什么呢?好吧,注意这个结构,这个砖块,这个金字塔在某种程度上是用自身来定义的,为什么?好吧,如何制作一个高度为四的金字塔,我会有点恼人地、循环地争论,你创建一个高度为三的金字塔,然后再加上一行砖块,好的。

好吧,让我们继续这个逻辑,好吧,如何构建一个高度为三的金字塔?好吧,你微笑着说,你需要建造一个高度为二的金字塔。然后再加上一层,好吧,如何构建一个高度为二的金字塔?你建造一个高度为一的金字塔,然后再加上一层,如何构建一个高度为。

高度为一,你只需把这个愚蠢的砖块放下,你有一个基本情况,在这里你有点陈述了显而易见的事情,只需做一次,你硬编码逻辑,但请注意,什么是有点让人费解或恼人的在人际互动中。就像你只是用事物定义答案,但没关系,因为金字塔更小。

直到我能处理那个特殊的情况,所以我们可以为乐趣做这个实例。如果我想建造一个高度为四的金字塔,我该怎么做?好吧,我可以建造一个高度为三的金字塔,好吧,让我去啊d建造一个高度为三的金字塔,呃,我该如何建造一个高度为三的金字塔,好吧,我建造一个金字塔,好的。

我该如何构建一个高度为二的金字塔,高度为一,我该怎么做?好吧,你只需放下砖块,所以这里的事情有点停滞不前,不再是循环的论证。你最终只是做一些实际的工作,但在我心里,我必须记住你刚刚给我的所有指示,或者我给自己的指示,我必须建立一个高度为四的金字塔。

不,三,不,二,不,一,现在我真的在做这个,所以这是高度为一的金字塔。我现在该如何构建高度为二的金字塔?好吧,回顾一下故事。要构建一个高度为二的金字塔,你需要建造一个高度为一的金字塔。然后再加一层,所以我想再加一层。

我基本上需要做到这一切,现在我有一个高度为二的金字塔。但是等一下,故事开始于,我该如何建立一个高度为三的金字塔。好吧,你需要一个高度为二的金字塔,我这里有一个,然后再加上一层额外的层,所以我得构建这一额外的层,我要去啊d给自己一个结构。

层,层,然后我将把它放上去,瞧,这就是高度为三的金字塔。好吧,我是怎么到这里的?让我继续回顾这个故事,我问自己的第一个问题是如何建造一个高度为四的金字塔。好吧,答案是建造一个高度为三的金字塔,太好了,完成了。

然后再加上一层额外的层,如果我有更多的手,我可以做得更优雅,但让我去啊d,把这个铺出来,这就是高度为三的新层。现在我要去做四,现在我要把高度为三的金字塔放在上面,直到瞧,我有这个形式在这里,循环的。

每当我要求自己构建一定高度的金字塔时,我总是推迟说,不,构建这个高度的金字塔,不,构建这个高度的金字塔,不,构建这个高度的金字塔,但这个算法的魔力是多做一点工作,构建一层,再多做一点工作。

一层又一层,金字塔本身的最终目标实际上出现了,因此你可以用 for 循环或 while 循环来实现同样的东西,坦率地说,你确实这样做了,只是形状稍有不同,但你用循环做了同样的事情,你有点。

按照我们规定的方式进行,因为使用 printf,你必须从屏幕顶部打印到底部,然而,得先打印一层,然后再回到顶部,所以我在这里有点对现实世界的自由裁量,抬起这些东西并移动它们,你必须有一点。

代码中更聪明,但想法是相同的,因此即使是像这样的物理对象,也可以有一些递归定义,因此我们展示了这个有点搞笑的例子,因为递归的概念是一种基本的编程技术,你可以利用它来解决问题,我认为为了这个我们需要最后一个。

在布莱恩的帮助和计算机的帮助下可视化归并排序,归并排序将是一个算法,它的伪代码可以说是到目前为止最简单的,但却具有欺骗性,归并排序的伪代码非常简单,就是对左半部分数字进行排序,对右半部分数字进行排序。

合并已排序的两部分,注意即使不公平,这里有一个排序算法,然而我在我的排序算法中字面上使用了“排序”这个词,就像在英语中,如果你被要求定义一个词,而你字面上在定义中使用这个词,这种情况很少成立,因为你只是制造了一个。

循环论证,但在代码中没关系,这样做有点不同,只要问题确实是,这个伪代码并不是在说排序数字,而是将问题分成两半,然后解决另一半,迭代,现在我会免责声明,我们将需要。

所谓的基本情况,再次,我需要做一些愚蠢但必要的事情,假设只有一个数字,就可以停止,它是排序的,这就是所谓的基本情况,递归情况是函数调用自身。但这确实是我们的第三个也是最终的排序算法,称为归并排序。

我们这里真正关注的是最精华的部分,一个就是合并的概念,因此布莱恩,我们能不能转到你这边,这样我们可以在查看归并排序算法本身之前先定义一下,当我们说合并已排序的部分时,我们究竟是什么意思?例如,布莱恩的架子上有两个大小为四的数组,左边的第一个数组有四个右边的四个数字。

左边是已排序的,右边也是已排序的,但现在布赖恩,我希望你合并这些已排序的部分,告诉我们这是什么意思。当然,如果我有一个从最小到最大排序的左半部分,还有一个同样从最小到最大排序的右半部分,我想将它们合并成一个新的列表。

这个组合数组的所有数字也都是从最小到最大,我想我可以从这里开始。最小的数字需要从左半部分的最小数字或右半部分的最小数字开始。因为在左边,最小的数字是三,而在右边,最小的数字是。

在这两个数字中,必须有一个是整个数组的最小数字。在三和一之间,一更小,所以我会取那一个。这将是合并两个半部分的第一个数字,最小的数字,然后我想我会再次重复这个过程,右边的三是最小的数字。

是二,在三和二之间,二更小。所以我会取二,这将是下一个数字。我正在慢慢构建这个已排序的数组,这是将左边与右边的四结合的结果。在三和四之间,三更小,所以我们取三,并将其放入位置。

现在我在将左边的五与右边的四进行比较。在五和四之间,四更小,所以它进入了位置。接下来,我将左边的五与右边的七进行比较。五更小,所以五放在了下一个位置。接下来我在比较左边的六与右边的七。

右边的六仍然更小,所以这个数字将下一个。我现在在比较八和七,只有两个数字剩下,七在这两者中更小。因此,我会取七并放入位置,现在我只需放入两个半部分的合并,这就是数字八。

所以这个数字将占据最后的位置,现在我已经取了这两个半部分,每个部分最初都是已排序的,组成一个完整的数组,所有这些数字都按顺序排列。确实,考虑一下我们所做的,实际上定义了一个助手函数,我们自己的自定义函数,意味着。

合并两个数组,特别是合并两个已排序的数组,因为为什么呢?这是一个我认为我们在这个归并排序算法中想要的构建块。因此,就像在实际的C代码中,你可能会定义一个执行一些小任务的函数,我们现在也以口头和实际的方式定义了合并的概念。

这里令人费解的部分是,排序的左半部分和排序的右半部分已经被实现,没有更多的工作要让布赖恩或我来定义。剩下的就是我们执行这个算法,特别关注这三行高亮的代码,并让我声明的是。

到目前为止我们看的算法,可能这个是最不容易快速理解的。即使其他算法可能让你花了一天、一周去消化,或者也许你还没有完全掌握,这都没关系。归并排序是个让人头疼的,因为它似乎是魔法般的工作,但实际上它更智能地工作。你将开始获得各种原始数据,以便我们最终能解决问题。

乖乖地把数字放回最上面的架子上,他把它们放回原来的无序状态,就像选择排序和冒泡排序一样。布赖恩,我现在想提议的是……

我们执行这个归并排序算法,如果你不介意,我先复述一下最初的几个步骤。这是一个大小为8的无序数组,目标是排序。请记住,归并排序本质上就是三个步骤:排序左半部分、排序右半部分、合并已排序的两半。布赖恩,看着那些数字,你能……

好的,所以这里有8个数字,左半部分是这四个数字,所以我将对这些进行排序。只是我现在不太确定如何排序这四个数字。是的,考虑到我们已经看过选择排序,回到那些较旧的、较慢的算法,布赖恩,我可以在这里稍微聪明一点。好吧,我给你一个排序算法,所以现在你实际上有一个更小的。

问题是一个大小为4的数组,我很确定我们可以使用相同的算法归并排序,通过对左半部分进行排序,接着对已排序的两半进行排序。那么你能帮我排序这四个数字的左半部分吗?好的,我有这四个数字,我想排序左半部分,也就是这两个数字,现在我需要做的就是这样。

我们凭借人类的直觉,显然知道我们该怎么做,但再说一遍。让我们应用算法,排序左半部分、排序右半部分、合并已排序的两半。布赖恩,你能排序这个大小为2的数组的右半部分吗?我得到的数组是2,所以我先对这个大小为2的数组的左半部分进行排序,也就是6。

这就是幻灯片中基础案例发挥作用的地方。如果只有一个数字,那就停了。布赖恩,我可以让你松一口气,那个大小为1的列表里有个数字6,已经排好了,所以这是三步中的第一步完成了。布赖恩,你能排序那个大小为2的数组的右半部分吗?右半部分是数字3,已经完成,好的。想想我们在故事中的位置。

我们已经排序了左半部分,排序了右半部分,甚至还没有做任何有用的工作,但现在魔法发生了,布赖恩,你现在有两个大小为1的数组。你能把它们合并在一起吗?好的,我要将这两个合并在一起。先把3放进去,然后把6放进去。

这两个数字现在已经完成了,好的,现在是时候开始动脑筋了。事情堆积起来,我们是怎么到达这一点的?我们开始时有一个大小为8的列表。然后我们查看左半部分,发现它是一个大小为4的数组。接着我们查看那部分的左半,发现它是一个大小为2的数组。

然后有两个大小为一的数组,然后我们,想,现在如果我倒回那段故事,布莱恩你。需要对原始数字的左半部分的右半部分进行排序,四。左半部分的右半部分现在是,这两个数字。所以现在要排序这两个,我想我,会再次重复这个过程看。

我单独看这两个的左半部分。是八,那一个完成了,还有五,那一个也完成了。好的,所以第三步是,合并这两个已排序的部分。好的,所以在八和五之间,五更小,所以那个,先放进去。

然后八会在后面,现在我有第二个大小为二的数组,它现在也是已排序的,提醒一下,现在,左。半部分和左半部分的右半部分,所以我认为这个故事的第三个也是最后一个步骤是布莱恩,现有的大小为二,好的我有两个。

每个大小为二的已排序数组,我需要合并,所以我将比较每个的最小数字。我会比较,三和五,三更小,所以那个会先放。现在在这两个数组之间,我有一个六和一个五要比较。五更小,所以我接下来会看六和八,六更小。

我剩下的只有八个,如果,回到我正在排序的原始八个数字,我想,我现在已经排序了原始数组的左半部分四个数字,确实如此,所以如果你在家里玩,想想你有这些,想法可能堆积在一起。

你心里确实应该是这样的,坦白说,很难。跟踪所有这些,做同样的事情,现在通过排序右半部分直到完成,布莱恩如果你可以,好的,所以右半部分我们得到了四。个数字我将开始对,右半部分的左半部分进行排序。

这就是这两个数字,要做到这一点我会重复同样的过程。它是这两个数字的左半部分,只有两个,那一个完成了,它只有一个数字。右半部分同样,七只有一个数字,所以也完成了。现在我会将已排序的部分合并,两个和七之间,两个更小。

然后是七,所以这里现在是,右半部分的左半部分。一个已排序的大小为二的数组,我会对右半部分的右半部分做同样的事情。从左半部分开始是,四,已经完成,那个一也完成了。现在要把这两个合并,我会比较它们,发现那个一更小。

所以我们把一放下,然后是四,所以现在我有两个大小为二的已排序数组。我现在需要回溯并且,合并在一起形成一个大小为四的数组,所以我会比较这两个之间的那个,一是更小的。然后我会把两个与四进行比较,两个更小,然后我会比较七和。

四个元素中,四是较小的那个,然后是最后一个数字,将其放入最终的位置,所以数字。我现在已经对左半边进行了排序,对右半边也进行了排序,现在我们进入第三个也是最后一步,你能把这两个排序的半部分合并吗?是的,我认为这实际上是一个我们已经看到的例子。为了对这两个半部分进行排序。

我们只需从每个半部分取出较小的数字,再次进行比较。较小的数字放入合适的位置,然后在三和二之间,二较小,所以我们取出二并将其放入四中,进行比较。将五与四进行比较,四较小,因此四放入合适的位置。

现在我在比较五和七,五较小。因此,它进入合适的位置。接下来我在比较六和七,六较小,所以六进入下一位,七则是两个中较小的数字,所以它接着进入。此时我只剩下一个数字,即八。

所以,一个数字会在数组的末尾进入其排序位置,尽管感觉我们并没有真的在做,但当我们开始合并、合并、再合并这些列表,并有效地自上而下地划分列表时,这一切都聚在了一起。我们从一个八个元素的列表开始,然后本质上进行了处理。

两个大小为四、一个大小为一的列表,尽管它并不是完全按照那个顺序,如果你回放并分析所有内容,数字从八变成两个四,再到四个二,最后到八个一。这就是他为什么将这些数字从顶层架子上移动,即二的原因。他总共移动了三次,在每个架子上,他需要合并多少数字。

在每个架子上,他首先插入最小的数字,然后是第二小的数字,再到第三小的数字,但与选择排序不同,半部分。他只是在不断地从每个半部分的开头取出数字,假设是n步,因为他在合并所有n。

那个架子的元素,但他合并n个元素的次数是多少?他总共做了三次,但如果你考虑二分查找,以及更广泛的分而治之的过程。每当你将某样东西分成一半,再一半、再一半,就像他从八到四、到二、到一的过程,这就是对数,底数为二的对数。

事实上,这正好是这个架子的高度,如果你在架子上有八个元素。布赖恩使用的额外架子数为三,正好是通过计算得到的,底数为二的对数,八的对数,也就是说布赖恩做了n次,log n次,再加上计算机科学家挥手表示的不提及底数的log n。

布莱恩进行了 n 次操作,每次 log n 次,因此如果我们考虑该算法的渐近复杂性,也就是算法的运行时间,它的表现严格优于选择排序和冒泡排序,复杂度为 n log n,甚至再次强调,如果你对对数有些生疏,我们在零周的二分查找中看到 log n 确实是相乘的。

n log n 是 n 乘以 log n,这在数学上确实优于 n 的平方。虽然在考虑下界时,归并排序有点像选择排序,因为它并没有自我优化,早早退出算法,它始终是在合并这些部分。

n log n 所以它的下界是 omega(n log n),这有时可能不可接受,你可能会有某些数据输入,可能趋向于已排序,而你不想使用冒泡排序。

但老实说,随着 n 的增大,输入到你的排序算法中的数据,偶然间恰好已排序的概率是非常非常低的,因此在一般情况下,使用像归并排序这样的算法(其复杂度为 n log n)会更好。我们也可以通过我们的条形图直观地看到这一点,并注意到,就像布莱恩在分割时所做的。

并通过将问题一分为二,再合并这些部分,你可以在视觉上看到,实际上这里发生了很多事情,过一会儿,这一切看似神奇地运作,但你可以在淡紫色的条形图中看到这些部分。

合并这些部分的可视化稍有不同。它没有三层存储的奢侈,只是从上到下移动。老实说,布莱恩在这里可以更优化一些。我们想清楚地说明总共需要多少,实际上他完全可以直接移动。

数字上下波动,确实这就是使用归并排序所付出的代价,尽管 n log n 优于 n 的平方,因此归并排序可以说优于选择排序和冒泡排序,你付出了代价,这表明我之前提到的权衡。几乎总是,当你在代码中做得更好,或者更智能地解决问题时。

你可能付出了代价,也许你作为人类编写代码时花费了更多时间,因为这更复杂,需要更多的技巧,这是一种成本,也许你需要使用更多的空间,暂时存放数字,以便在合并时来回移动,如果是三。

分开的数组或四个分开的数组,但根据归并排序的图形表示,只使用第二个数组就足够了。现在这可能看起来没什么大不了,但隐含地你需要两倍的空间,这在你有一百万个需要排序的东西时可能是个大问题,现在你需要两个数组。

这需要两百万块内存,可能这并不可行。因此,这里也会有权衡,可能选择排序或气泡排序速度更慢,但也许因为它在空间上更有效,具体取决于你关心什么,以及你想优化什么。

说实话,金钱有时是一个因素,在现实世界中,也许稍微多花点钱,购买两倍数量的服务器或两倍的计算机内存是更好的选择,这取决于资源的情况,时间、你的钱包或其他资源。因此我们会继续看到这些权衡,但也许最令人震惊的是,我们可以做到的事情。

在结束之前,我分享一些如何这些算法实际比较的可视化,最后一个术语是这个,最终的希腊符号 theta,结果证明,得益于选择排序,更具体的术语是这个 theta 符号。任何时候当一个算法具有相同的时间,你实际上可以用一个来描述它。

在 theta 符号中用一个句子而不是两个,因此因为选择排序在大 O 记号中是 n 的平方,同时也是 omega 的 n 的平方,你实际上可以简单地说,它在 theta 的 n 的平方中,无论是上界还是下界,归并排序也是如此,它在 theta 的 n log n 中,而我们不能为气泡排序使用 theta。

二分搜索或线性搜索因为它们有不同的上下界,但让我现在继续,准备一个最终演示,这次使用一些随机输入,你会看到这里有一个视频比较排序。所有三者都从随机数据开始,但让我们看看这对一个算法意味着什么。

在最坏情况下,算法是 n 的平方或接近 n log n。让我们以戏剧性的方式,现在比较选择排序、归并排序和气泡排序,选择排序在上面,气泡排序在下面,归并排序在中间,已经,[音乐]。完成,同时我们有一些非常时尚的音乐可以听,实际上只是为了让气氛更好。

让我们不要忽视在实践中,n 的平方是多么缓慢,注意这里没有多少条,这里也许只有一百条,像 n 是一百,这在我们谈论的世界中并不算大值,这些都是微不足道的大小,但天哪,我们仍在等待选择排序和气泡排序完成。

你会看到,运用更多的智慧,利用更高效的算法时,事情真的很重要,最后,选择排序已经完成。气泡排序仍在这里花费更长的时间,这将取决于运气。

但我认为很有说服力的是,归并排序在这种情况下赢了,让我们考虑一个更具体的例子。假设在最坏情况下,列表或数组最初完全相反,让我们考虑这些算法的功能,现在我们想从最小到最大,你仍然可以看到归并排序。

反复从这个问题中提取出一半的字节,然后重构解决方案,哇,这就是 n log n,即使只有这几条信息,你也可以真正看到冒泡排序中大元素的上浮,而选择排序中小元素的向左渗透,但我的天,我没有足够的词语来描述。

带我们到达终点线,尽管我们今天只看了两种搜索:线性和二进制,以及三种排序:选择、冒泡和归并排序,但还有很多其他的搜索,通常来说,在排序数据时,你并不会自己写代码,你可能会在课堂上或实验室中这样做。

但在现实世界中,你会发现其他人对常用功能的正确实现,以便你可以“站在他们的肩膀上”,真正关注你关心的问题,而不是这些已经被其他人解决的更常见的问题,只是给你一个瞥见。

我们将在这里中止冒泡排序,因为这会花费太多时间,这里有一个最终的可视化,这个更具声学性质的可视化也将声音与这些算法关联起来,所以如果你更能“听到”这些差异。

这是一种叫做插入排序的算法。

排序,再次在这种脉动中,你可以隐约听到冗余工作,冗余工作,冗余工作,这就是为什么 n² 在进行如此多的多余比较时会累加,这现在是选择排序,所以注意小元素最终位于左侧。

[音乐]。

也许这就是最令人满意的部分。

哈佛CS50-CS | 计算机科学导论(2020·完整版) - P8:L4- IO、存储与内存管理 1 - ShowMeAI - BV1Hh411W7Up

这是cs50。

这是cs50,这是第四周,在过去的几周里,我们有了某种辅助轮,使用这种称为c的语言,而这些辅助轮的形式是cs50库,你当然通过选择和包含cs50.h在你的代码顶部来使用这个库,然后如果你考虑。

clang的工作原理你一直在链接,直到现在这已经为你自动化了。今天我们将从上周对机器的关注转移。我们现在使用的机器来更强有力地实现这些算法,随着我们开始去掉这些辅助轮,看看真正发生了什么。

在你的计算机的内部,尽管某些c语言的方面很复杂。编程可能对你来说是全新的,但你会意识到在内部并没有太多事情。我们需要理解,以便继续前进并开始解决更有趣、更复杂、也更有趣的问题,我们只需要几个,首先做这个。

通过重新学习如何计数,呃,比如说我们将称之为网格的东西。我们可以对你计算机内存中的所有字节进行编号,我们可以称之为字节编号0、1、2、3、4,一直到字节15,等等,但事实证明,在谈论计算机内存时,呃,计算机和计算机科学家以及十进制。

他们不会,他们绝对不倾向于,做一些叫做。十六进制的事情,十六进制是一个不同的基数系统,它不是使用10个数字或2个数字,而是使用16个,因此,当计算机科学家谈论计算机内存时,仍然会使用0、1、2、3、4、5、6、7、8、9,但在那之后,不是继续使用十进制到10。

通常,我们会开始使用字母表中的几个字母,以十六进制的方式计数。这个不同的基数系统基数16,你仍然从零开始计数。你计数到2和9,但当你到达10时,就转向a、b、c、d、e和f,这样的好处是。在十六进制中,hex表示16,总共有16个独立的数字0到9和。

现在是a到f,因此我们不需要引入第二个数字,只需计数到16。我们可以使用单个数字0到f,并且我们可以通过使用多个十六进制数字继续计数,但为了实现这一点,让我们引入这个词汇,所以在二进制中,当然我们使用零和一。呃,在十进制中,我们当然使用零,清楚地说我们将使用。

16,这只是我们使用的约定,通过f我们本可以使用任何其他六个符号,但这些是人类所选择的,因此十六进制的工作原理与我们熟悉的十进制系统非常相似,甚至对现在你所知道的二进制系统也相当熟悉,如下所示,让我们考虑一个使用的两位数值。

十六进制而不是十进制,也不是二进制。

就像在十进制世界中,我们使用了基数10,或者在二进制世界中我们使用了基数2,现在我们将使用基数16,因此十六进制就是16。这是16的第一,当然如果我们把它乘开。它只是个位列,现在是十六位列,因此如果你想以通常的方式数。

然后是零一零二零三零,四零五零六,*****,事情变得有趣了。现在你不去到一零,因为那是错误的,就像16一样,我们想要。在我们知道的数字9之后,我们现在再数到a,然而,就像在十进制系统中,当你数到99时,你必须开始进位,这里也是同样的情况,如果你想计数。

在f之后你进位,所以现在要表示一个大于f的值,不是10。在十六进制中是1 0。16乘1,给我们16,1乘0给我们0,当然那就是16。所以我们不再引入更多的基数系统,但让我声明。仅仅通过使用这些列,假设,你现在可以实现任何基数系统。

偶然的是在计算机的世界中,今天在内存的世界里,马上也会在文件中。能够识别和使用十六进制是非常常见的,实际上人类喜欢十六进制是有原因的。

至少一些人类计算机科学家,如果我们数到这个情况下的ff,我们仍然会做同样的数学运算,即16乘15加上1乘15,当然。240加15或255,我做得很快,但这就是那种列。根据它里面的值,再次,每一个f都是数字,但回忆一下我们以前见过255。

当我们几周前讨论二进制时,255恰好是位。使用二进制,因此计算机科学家倾向于喜欢十六进制的原因是,你知道吗,在八位中实际上有两个对。如果我们稍微移动这些东西,结果是,表示。

16个可能的值,这是一个完美的系统,用来一次表示四个位。毕竟,如果你有四个位,每个位可以是零或一,那就是两次。两次两次两次两次可能的值,或者说16个总值。这就是说,在计算机的世界里,如果你想用四个位的单位来交流。

使用十六进制是非常方便的,因为一个十六进制数字恰好等于四个二进制零。一直到一一一一,这是人类的约定,因此由于这种便利性,现在一些人。实际上,回忆一下我们在零周的讨论,关于RGB,我们讨论了。

呃,使用一些红色、绿色和蓝色的组合来表示颜色。在我们使用这个例子的时候,我们把例子脱离了上下文。我们不是用hi作为一串文本,而是重新解读了72、73和33作为颜色序列,你想要多少红色,多少绿色,多少蓝色。

你想要的是完全可以的,十进制没问题,但计算机科学家们在颜色和内存的上下文中倾向于使用称为十六进制的东西,而这里的十六进制实际上只需将这些值从72、73、33转换为相应的十六进制表示,我们不会仅仅规定为484921。

在十六进制中,显然如果你瞥一眼这三个数字,很明显你并不能确定这些是十六进制数字还是十进制数字,因为它们使用相同的子集0到9。因此,计算机世界中的一个约定是,每当你表示十六进制数字时,使用0x,并且这没有数学意义。

这里的零或x只是一个前缀,用于让观众明确这些是十六进制数字,即使它们看起来像十进制数字。那么,我们要去哪里呢?那些曾经尝试制作自己网页并让其多彩的人。

对于那些艺术家们,如果你们使用过像photoshop这样的程序,那么你们很可能见过这些代码。事实上,这里有一些photoshop本身的截图。如果你在photoshop中点击一个颜色,弹出的窗口里,你可以将你在屏幕上绘制的颜色更改为任何颜色。

彩虹的颜色更加深奥,如果你看看下面,你可以看到这些十六进制代码,因为多年来,人类已经习惯使用十六进制来表示不同数量的红色、绿色和蓝色。所以如果你没有红色、没有绿色、没有蓝色,也就是表示为000000,那么这将给你我们所知道的颜色。

黑色可以说是任何波长光的缺失。如果你把所有的值改变一下,再说一遍,十六进制范围内是0到f,十进制范围是0到15,ff或fff就代表了大量的红色、绿色或蓝色,最终得到的颜色我们称之为白色。

现在你可以想象,结合不同数量的红色、绿色或蓝色。例如,在十六进制中,ff000是我们所知道的红色,绿色,最后00ff是我们所知道的蓝色,因为我们一直使用的系统确实是rgb系统。

在这里不是因为你必须以不同的方式思考,因为在第零周。但是你会开始在示例和程序中看到数字以十六进制出现,而不是被解释为十进制。因此,如果我们现在考虑我们电脑的内存,我们将开始这样思考。

完整的内存画布中,所有这些字节在我们电脑的内存中。作为可枚举的,比如零、一个、二,一直到f,如果我们继续计数,我们可以到one zeroone oneone twoone threeone fourone nineone aone bone cone d,依此类推,没关系,如果没有。

看这些东西时,十进制等值是什么并不明显。这没问题,这只是一种不同的思考方式,关于计算机内存中位置的表示,或者一种颜色的表示。好了,现在让我们用这个作为一个机会来考虑。

实际上,我们的电脑内存中存储的内容是什么?为了明确起见,我会给所有这些内存地址加上前缀0x,以表明我们现在正在讨论的内容。这里有一行简单的代码,脱离上下文,我们实际上需要把它放在main或者其他程序中才能做任何事情。

但我们之前见过很多次,例如,它的类型,然后甚至可能给它赋值。那么实际上存储在我们电脑内存中的是什么呢?

在我们的电脑中,好的,让我们继续把这个东西在一个实际程序中实现,让我创建一个叫做address.c的文件,因为我想开始实验。

内存,我将继续包含stdio.h,我将给自己定义int main void,在这里我将定义一个变量,int n equals 50,然后我将打印出n的值,因此没什么有趣的内容,没什么太复杂的。我将继续创建地址,然后我将继续执行。

第一周,我们希望能看到数字50。但今天我们将为你提供一些更多的工具,以便你可以开始探查电脑的内存。不过首先,让我们考虑这一行代码在电脑硬件的背景下。因此,如果你正在写一个包含这一行代码的程序,n需要在你的。

将某个内容放在你电脑的内存中,因此如果我们再次考虑这是我们电脑内存的一部分,几个变量被故意画成四个字节,四个方块,因为记住一个整数通常在cs50 ide和现代系统上,往往是四个字节,所以我确保。

让它填充四个完整的方框,那么值可能是50,这实际上存储在那里。事实证明,在你的电脑内存中,还有这些隐含存在的地址。因此,即使是的,我们可以根据我在代码中给它的变量名来引用这个变量n,这变量肯定存在于一个。

内存中的特定位置我不知道它具体在哪里,但让我假设也许它在位置0x12345678。这只是一个任意地址,我实际上不知道它在哪里,但它确实有一个地址,因为这些方框代表逻辑和。

也许50最终位于内存地址,这个有趣的是,完全没有双关的意思。所以让我去ahd并修改这个程序,引入一些新的语法,让我们能够开始探索计算机内存的内部,这样我们就可以真正看到下面发生了什么。因此,我将要这样做,取而代之的是,我将去ahd并说,值n,当然是50。让我看看。

出于好奇,n的实际地址是什么?要做到这一点。

今天我们将介绍一条新的语法,它就是这里的这个。今天C语言中有两个新的运算符,第一个是&符号,它的作用是。

记得几周前我们看到了表达式。你使用两个&符号,不幸的是,单独的&符号今天会意味着不同的东西,具体来说这个&符号将成为我们的地址运算符,只需在任何变量名之前加上前缀。

使用&符号,我们可以告诉C,请告诉我这个变量存储在什么地址,以及这个星号在今天的上下文中还有另一层含义。当你使用这个星号时,你实际上可以告诉你的程序去查看特定内存地址的内容,因此&符号告诉你什么是地址。

变量是星号运算符,运算符意味着去到下一个地址。因此,它们是一种反向操作,一个是确定地址,这里是我的程序中的n,n的&符号是n的地址。那么我如何打印出地址呢?这只是一个数字,但实际上printf支持不同的格式。

地址的格式代码,你可以使用%p,出于我们将要看到的原因,这表示打印出这个变量的地址,因此我要去ahd并创建地址。经过仅仅对这个文件做两处修改,一切似乎都能正常编译。现在我要去ahd并运行地址程序,变量fd80792f7c。

fd80792f7c,现在这有用吗?在实践中并不一定,我们将通过利用这些地址使其变得有用,但具体地址并不重要。我瞥了一眼这个数字,我不知道这个数字在十进制中是什么,我需要做一些数学运算,或者坦率地说,直接去谷歌找个转换器来帮我。

所以再次强调,这并不是有趣的部分,事实上这是十六进制,且再一次。我们并不一定想要这样做,但为了清楚起见,这些运算符之一&符号获取地址,而星号运算符。

访问一个地址,我们实际上可以撤销这些东西的影响,例如,如果我现在打印出,不是&n,而只是出于好奇的星号&n,我可以在某种程度上撤销这个&n的效果。它会告诉我n的地址,而星号&n会告诉我去那个地址。

这有点无意义的练习,因为如果我只想要began中的内容。但再说一次,作为一种智力练习,如果我用地址运算符给n加上前缀,然后使用星号,去那个地址,这和直接打印n本身是完全一样的。所以,整数,不是%p,让我去做d并现在获取地址。

编译似乎没问题并运行地址,瞧,我们回到了50。开始感受到。意识到这些运算符在一天结束时是相对简单的,如果你理解一个只是撤销构建的,你可以用它们构建一些相当有趣的程序,我们将通过利用一种特殊类型的变量来做到这一点。

被称为指针的变量,p在%p中,指针是一个变量,它包含某个其他值的地址。我们之前见过整数,见过浮点数、字符和字符串及其他类型,现在指针只是另一种变量。你可以有指向整数的指针,指向字符的指针。

指向布尔值或任何其他数据类型的指针,指针引用的是它实际所指向值的特定类型。因此,让我们更具体地看看,让我回到我的程序这里,并引入另一个变量,而不是立即打印出像n这样的东西,呃,让我去做d并引入第二个。

变量是类型为int star,我承认这可能是我们在C语言中见到的最混乱的语法。因为天哪,星号现在被用作乘法运算符。这个设计决定可以说不是最好的选择,但几十年前做出的决定就是这样。所以,这就是我们所拥有的,但是如果我现在做n star p等于&n,那么我可以在这里做些什么。

打印出n的地址,通过暂时将其存储在一个变量中,因此我现在还没有做任何新的事情。我在第5行仍然声明一个名为n的整数,第6行是我引入了一种新的变量类型,这种变量被称为指针。指针就是存储地址的变量。

声明一个指向整数的指针实际上是说,int,因为这是你要指向的类型,星号,然后是你想要创建的变量名,我可以称之为succinct。再次,在等号右侧是与n地址相同的运算符。它只是&n,因此我们可以将这个地址存储在某个地方,以便在较长的时间内使用。

现在我暂时在第六行存储,那个地址在一个新变量中。叫做p,其类型是技术上说,因此说int p = &n是不正确的,实际上我们的编译器clang不会接受,它很可能不会让你编译代码。因此我会用*p来明确我知道自己在做什么。

我正在存储一个整数的地址,不,保存这个重新编译的地址。注意我更改了一行代码,之前我返回到%p。打印一个指向地址的指针,我正在打印p的值,而不再是n的值。那是神秘的地址,这些地址,反映了发生了什么。

在你的程序或系统的其他地方,这些地址每次可能会不同。这是可以预期的,不是值得依赖的,但这显然是某个随机的神秘地址,类似于我任意的0x12345678。但是现在让我们撤销这个操作,以便我们可以完整地结束。

在这里圈出,让我现在提出如何打印n的值,让我请到,第7行。不再打印n的地址,而是使用p打印n本身。我将去改成d并更改,我的简写符号显然就是打印n。但是假设我不想在这个练习中打印n,我现在该如何打印这个值。

在n中通过p来引用它,我应该字面上输入什么作为printf的第二个参数,来以某种方式打印n的值。这里有什么想法,我该如何打印出来,布赖恩,勇敢的志愿者,嗯,让我们请乔舒亚。 我相信如果你在p之前使用&符号,我相信如果你在p之前使用&符号。

你可能会没问题地使用& p,让我去尝试一下,让我们尝试& p。打印这个值,所以& p,我将保存文件,但似乎并不是这样。注意我得到了一个错误,它有点晦涩。格式指定类型int,但参数类型是int **,关于这一点以后再说,所以要注意。

还有一个建议,因为,某些东西,但是p已经是一个地址了,所以乔舒亚。你技术上提议的是,给我地址的地址。那不是我们想走的方向,我们想去看那个地址里的内容。是的,所以我有一点困难听到, 是的,所以我有一点困难听到。

但是我认为如果我们使用不是&运算符而是*运算符,那确实会去获取p中的值。如果p中的值是一个地址,我想让我们尝试一下,编译地址,是的,让它编译通过。现在如果我执行./address,希望我能确实看到数字50。

所以再说一次,到头来我们似乎并没有取得任何根本性的进展,我仍然只是打印出 n 的值,但我们引入了这个新的原语。这块新的拼图,如果你愿意的话,允许你以编程方式找出计算机内存中某个东西的地址,并实际上到达那个地址。

我们很快也会看到它,但让我们回到一个图形表示,并考虑一下我们在这段代码的上下文中刚刚做了什么。所以在我的 main 中,真正有趣的两行代码就是这两行,首先在我们进行 Sophia 的编辑之前,实际上对 p 解引用并打印出来。

printf,但让我们暂时考虑一下,这些值在计算机内存中看起来是什么样的。语法有点神秘,因为我们现在有一个星号和一个和号,但这只是意味着现在我们可以访问计算机的内存。因此,例如,这里是我计算机内存的一个网格,也许。

例如,50 和 n 最终在下方,它们可以出现在任何地方,甚至在这里的屏幕上都没有显示,它们出现在计算机的内存中。到目前为止,为我们的目的而言,但它技术上确实存在于一个地址中。让我简化这个地址,以便更快地表达,现在这个 50 存储在变量 n 中。

也许它确实存在于地址 ox123,我不知道它在哪里,但我们已经明确看到它可以存在于,p。p 技术上是一个变量。它是一个存储其他东西地址的变量,但它仍然是一个变量,这意味着当你声明 p 时,占用了屏幕上的一些内存字节。

所以让我继续提议,现在 p 被故意画得更长。我这次消耗了总共八个字节,因为在现代计算机系统中,包括 CS50 IDE,指针往往占用八个字节,所以不是一个,也不是四个,而是八个字节,因此我只是将它画得更大。那么在变量 p 中实际存储了什么呢?

结果发现,它只是存储某个值的地址。所以如果整数 n 本身存储了 50,并且位于位置 ox123,而指针 p 被分配到那个地址,这就像是在说,存储在变量 p 中的实际上只是一个以十六进制表示的数字。

ox123,因此在计算机内存中就发生了这些事情,这两行代码没有什么根本性的变化,除了我们引入了一种新的语法来显式地引用这些地址。这里是 n,这里是 p,而 p 的值恰好是。

一个地址,我一直在说,这些地址有点神秘。它们有点任意,而且老实说,通常不会让人感到启发。作为一个人,知道这个整数 n 实际上处于哪个地址,谁在乎它是在 ox123 还是 ox456。通常我们并不关心,因此计算机内存往往不会在这么低的层面上进行交流。

具体数字的细节,相反,它们往往简化了画面,抽象掉了所有其他内存,这实际上与讨论无关。因此我就说,你知道的,我知道 p 存储着一个地址,而那个地址恰好是下面的 50,但我在日常生活中并不关心。

编程生活中,这些具体的地址是什么,所以你知道的。我们就把它抽象成一个箭头,再一次,抽象就是简化低层次的理解,但你不一定需要持续思考这个层面,所以我们不妨以图形方式绘制一个指针,指向某个值和。

不管实际地址是什么,所以在约定中确实如此。乍一看,我们可能会在日常生活中使用这些相同的机制,例如如果你在街上、家里,或哈佛科学楼的地下室有一个邮箱。

在校园中心,或者在校园内,它可能看起来像这样,至少更像是住宅。如果这个邮箱代表故事中的 p,它存储的是一个指针,也就是某个其他事物的地址。如果街上有很多其他的邮箱,我们可以随意放入任何东西。

这些邮箱里我们可以放明信片、信件、包裹,就像在现实世界中一样。我们能否在虚拟中做同样的事?我可以存储字符、整数或其他东西,包括地址。例如,布莱恩,我想你在别处有自己的邮箱,当然布莱恩也有一个邮箱,其本身有一个唯一的地址。

那么布莱恩,实际上你街上邮箱的唯一地址是什么?是的,这里是我的邮箱,上面标有 n,它的地址在这里。我的邮箱的地址似乎是 ox123,是的,所以我的邮箱 2 也有一个地址。坦白说,我并不在乎,所以我甚至没把它写上。

这里的邮箱,如果我的邮箱代表 p,一个指针,而布莱恩的邮箱代表整数 n。那么这应该意味着,如果我查看我的指针的内容,看到值是 ox123,那就是我的线索,一种面包屑,让我去查看布莱恩的邮箱,布莱恩,如果你不介意为我们做这件事。

那么在那个地址你有什么?如果我在地址 ox123 的邮箱里查看,我里面有数字 50。确实是这样,所以在这种情况下,它恰好存储的是,我们通常并不关心这些具体地址,一旦你理解了隐喻,我们可以将邮箱视为存储一个值,这就像是指向。

在布莱恩的邮箱那里,有某种间接性,以箭头图形化表示。这里有一个傻乎乎的泡沫手指,或者如果你愿意,可以是一个泡沫耶鲁手指,指向布莱恩的邮箱,就像是一种面包屑,引导我们去找屏幕上的某个其他值。因此,当我们今天以及今后谈论地址时。

这就是我们所谈论的内容,我们人类,地址已经存在了数百万年,以独特地识别我们的住所或商家,计算机在内存中做的事情是完全相同的。所以让我在这里暂停,看看是否有关于存储地址的指针变量的任何问题,或者关于新的操作符,比如&符号或星号。

从今天起,它有了新的意义,有关指针的任何问题或困惑吗?现在我将慢慢回到这里,没什么,好的,看到没有。好吧,让我们考虑一下这个相同的故事在数据类型中,但是考虑到我们花了很多时间在字符串上。

嗯,使用它们进行加密并解决实现选举算法,使用用户输入。所以让我们考虑一种根本不同的数据类型,它存储的不是单个整数,而是文本字符串。对于字符串,你可能有一行代码看起来像这样:string s = "HI";(全大写)。

带有感叹号的内容可能会让你看到目前为止,实际上发生在计算机内部的事情,它最终落在你的计算机的某个地方,上周是反斜杠零,或者两周前是反斜杠零,结束于那段字符串,但让我们仔细看看发生了什么。在这个引擎盖下,从技术上讲,我可以访问。

我们在第二周看到的那些个别字符,使用括号表示法,例如s[0],s[1],s[2]和s[3]。我们使用方括号表示法将字符串视为数组,实际上,它们也可以通过其地址进行操作。例如,也许这个完全相同的字符串“嗨”是。

存储在内存地址ox123处,然后是ox124、ox125和ox126。注意,它们是故意连续的地址,前后相连,并且相距仅一个字节,因为这些字符在C语言中当然每个只占一个字节。所以这些数字并不重要,但它们彼此之间相距一个字节。

这很重要,因为这就是字符串的定义,实际上是一个回溯。那么,s究竟是什么?s是我给它的变量名,我是s。等于量子引用“嗨”,那么s是什么呢?s是一个变量,它必须存储在计算机的内存中。假设s确实是“嗨”,它恰好存在于,想象一下s,字符串。

但在更低的层面上,它只是字符串的地址,更具体地说,让我们开始将字符串视为技术上仅仅是一个地址。

字符串中的第一个字符,你可能会想到这可能会给你提供第一个字符。你将如何记住这一点,等一下,这个字符串不仅在ox123处,还在ox124、ox125等处继续。但让我暂停并询问在场的各位计算机和程序员们,想想字符串的定义。

第一个字节的地址,为什么这就足够了,无论字符串多长,即使是整段文本,为什么把字符串视为等同于第一个字节的地址是非常聪明的?嗯,是的,我听得见你吗?是的,我们能听到你。

好吧,可能是因为每当我们定义任何字符串时,它都会是g,i,ni的组合。因此,如果某个东西指向我名字的第一个字符,那就足够了。这样我就可以跟踪第一个字符,然后获取所有的内容。

在字符之后完美,所以所有这些基本定义我们都一起拥有。如果字符串只是字符数组,根据数组的定义,两个星期前每个字符串都以这个传统的反斜杠零或空字符结尾。当考虑字符串时,你需要做的就是开始,因为你可以使用for循环或一个。

使用while循环或其他启发式方法,带有条件和布尔表达式,以确定字符串的结束长度。也就是说,暂时让我们将字符串简单地视为在字符串中的内容。如果我们将其视为事实,那我们现在就可以开始进行实验了。

这个程序不使用整数,而是使用字符串。使用这个基本原语,让我去删除之前在address.c中写的代码。让我只需将其更改为字符串,等于“高”。分号,并注意我并没有手动输入任何反斜杠零。

当你关闭引号时,C会自动为我们处理这个,编译器会为你添加那个反斜杠零。现在我将继续下一个s\n,s,如果我想打印出那个字符串,现在这个程序根本不写成。好吧,是的,这很有趣,因为我搞砸了,所以有五个错误。

写了七行代码和五个错误,让我们看看发生了什么。像往常一样,总是回到顶部,因为很可能有一些混淆的级联效果。我看到的第一个错误是使用未声明的标识符string,我是否意味着standard n?我并不意味着标准的n,而是string string string,所以我可以让我的朋友在这里运行帮助50。

但老实说,我经常犯这个错误,我现在有点知道我忘了包含cs50.h。实际上,如果我现在这样做并重新编译make address,所有五个错误都只通过这个简单的更改消失了。如果我现在运行address,它会简单地说“嗨”,但现在让我们开始考虑一下。

在这个程序的底层运行着,假设我很好奇,想要打印出这个字符串实际所在的地址。那么,结果是,让我聪明一点,打印出不是格式代码的%s,而是%p,给我显示这个字符串作为地址。让我去重新编译并制作地址。

看起来编译得不错,让我运行,打印 s,尽管它是一个指针,有趣的是,这并不和之前相同,但这又是合理的,因为内存地址。并不总是会相同,但,这无所谓。虽然这有点有趣,所有这些时间,每当你在使用字符串。

如果你只是把你的 %s 改成一个,内存,那字符串实际上开始的地方,它在功能上对我们来说还没有用,但它一直在那里。让我继续,现在做以下操作,假设我变得有点好奇。再进一步,我做 printf,让我继续,打印出另一个地址。

后面跟着一个换行符,让我继续,打印出,第一个字符的地址。所以再次,这有点奇怪,我们通常不会这样做,这些操作符。给我们很简单的问题答案,比如这个东西的地址是什么,表示。零索引意味着,s[0] 是第一个,s[1] 是第二个。如果我玩弄一下。

今天的新操作符这个符号,第二个,字符,实际上让我继续,前进。并且更明确,括号零,并在这里放一个符号,让我继续。前进,现在让这个程序生成,地址,好吧,有点奇怪哦,我刚刚错过了一个;分号,所以在这里很容易修复,让我继续,前进并用 make 重新编译。

地址让我继续,前进并运行 ./address,嗯,有趣,哦,也许对我来说有趣。所以你现在看到两个地址,第一个是 0x4006。一个四,显然是 s 中第一个字符的地址。但是请注意下一个地址的奇怪之处,它几乎是相同的。

除非字节相距一个进一步,我敢打赌,如果我这样做,不仅是 h 和 i,还有感叹号,让我再做一行几乎,完全相同的代码。只是为了表明,所有这些,时间确实是这样。

字符串中的所有字符都是紧挨着的,现在你可以在代码中看到。b4、b5、b6 之间仅相差一个字节,所以我们现在看到一些视觉确认,字符串确实在内存中就是这样布局的。再说一次,这不是一个非常有用的程序练习,像查看个别字符的地址,但。

再次强调,这只是为了强调,在底层,一些相对简单的操作正通过这个新的符号和反之,通过星号操作符来启用。所以让我们考虑一下这个内存,低层次的,是的 s 技术上是一个地址,是的,它技术上是第一个字节的地址,在实际计算机中看起来。

不同,但在我的幻灯片中,我只是,任意提出它在,0x123、0x124、0x125。但再次让我们不在乎那个,细节层面,让我们只是挥手,抽象掉这些地址,并且现在开始思考,s 作为一个字符串,技术上只是一个,指针。结果是。

尽管将字符串视为显然是字符序列非常有用且常见,而这从第一周就已经成立。你也可以将它们视为数组,紧挨着的字符序列。结果是,从今天开始,你也可以将它们视为指针。

字符在计算机内存中的某个地址,正如 guinea 指出。因为字符串中的所有字符根据定义是紧挨着的,并且所有字符串根据定义都以反斜杠零结束,这实际上是知道所有字符串的最小和唯一信息,只需记住。

第一个字符到结束的地址,记住这个反斜杠零实际上只是八个零位,其他形式表示为反斜杠零。因此,我们当然可以有一个条件,就像两周前的字符串一样。所以当我说我们正在去掉一些辅助工具时,它们就来了,所以到现在为止,我们已经……

我们一直在使用 cs50 库,它方便地为我们提供了像 forth 这样的函数。但这段时间 cs50 库,特别是文件 cs50.h,实际上有些教学上的简化。回想上周,你可以定义你自己的自定义数据类型。结果发现,这段时间我们一直声称字符串存在。

在你的程序中,你可以使用这些,字符串确实存在于 C 中,也存在于 Python、JavaScript、Java 和 C++ 等许多其他语言中,这不是 cs50 的术语。但字符串在 C 中技术上并不存在作为数据类型,它实际上更隐晦。

更低级的被称为 char star,char star 这是什么意思呢?好吧,char star 就像几分钟前的 int star,表示字符的地址。再次说,如果你同意我现在可以将字符串视为字符序列,或者更具体地说,是字符数组。

更具体地说,从今天开始,就是第一个字符的地址。因此,实际上我们现在可以将这种新的术语“指针”应用到我们熟悉的字符串上,你将为 char star 而感到高兴,而在 cs50 的代码中,简化或抽象掉 char star,实际上没有人想要这样。

想想或者在课程的第一周挣扎,别说前两三周了。课程中这是一个简化的自定义数据类型,我们称之为字符串,以便你不必思考这是什么星号地址,但今天我们可以去掉这些辅助工具,揭示出这些年来,你一直在操作字符。

在特定地址,我们之前使用过这种技术,抽象掉这些低级细节。例如,回想上周我们介绍了结构体这个概念,作为数据类型,你可以通过将名字和数字包裹在自定义数据类型中来定制它。

在我们称之为“person”的结构中,每个人都包含一个名字和一个数字,通过 C 的 typedef 特性,我们可以定义一个新的类型,而这个类型的名称上周刚好是“person”,所以我们已经在使用它。

C 类中的一行代码实际上看起来像这样。这确实是 cs50.h 中的一行代码。

它显示 typedef,意味着给我一个自定义类型,并创建一个名为 string 的 char star 的同义词,这是一种隐藏 funky char star 的方式,特别是隐藏星号,这在最初的几天里玩起来并不有趣,但没有改变字符串的定义。

字符串在 C 中存在,通过这种定义使它存在。好了,让我暂停一下,看看字符串,或者说这些新的思维方式,有没有关于字符串或 char 的问题,我知道这很多,算是比较底层的,有没有问题?好吧,如果没有问题。

为什么我们不先休息五分钟,然后再回来,再看看我们现在能做什么。好了,我们回来了,现在我们有能力和代码获取某个变量的地址,也可以去到相应的位置,我们考虑字符串时并不需要。

只有连续的字符序列,但现在还有实际地址,第一个字符的地址,然后我们能否程序化地找到结尾,多亏了那个空字符,但事实证明我们可以用这些地址或指针做另一件事,那就是指针运算,因此任何事情都是*****。

数学不会复杂,但对我们来说会很强大,所以我将返回到我最新的 address.c 状态。现在让我再次重申,我们可以像第二周那样打印字符串中的单个字符,使用我们的方括号。

我正在清除所有那些地址的证据,暂时不需要。我将这个程序重新编译为 make address,然后我将运行 dot slash address。现在我看到 h 感叹号每行一个字符,但现在考虑一下,实际上不需要字符串数据类型,事实上我们可以去掉这个辅助工具。

尽管一开始可能会感到有点不舒服,如果我完全删除这一行,反正我有时会意外省略。其实我不需要不断地口头称呼这些东西,我可以把它们视为字符串,因为字符串在许多不同的编程语言中都是一个东西,但在 C 中默认情况下并不存在。

存在一种类型,名为 char star,但再说一遍,这意味着的是某物。char 意味着这是一个字符的地址,所以 char star 将指向一个字符,因此如果 s 是这个,我实际上可以以相同的方式对待它,没有理由我不能继续使用 s 像在第二周那样,使用我们的方括号表示法。

我可以继续打印出嗨,感叹号,但还有另一种方式可以做到这一点,如果我现在知道 s 其实只是一个地址。我可以去掉这个方括号表示法,实际上可以只做 star s,因为要记住 star 除了是新符号外。

我们在这里声明指针时使用,确实,我们用它来访问一个地址,所以如果 s 存储了一个地址,按照指针的定义,我之前的图像似乎是这样的。

s 很可能是一个地址,起始于类似 0x123 的地方,它并不会。

在我的实际 IDE 中,它将是计算机所拥有的,完全相同的想法,所以让我继续走到 star s,为了好玩,让我只保留这一行。让我继续并重新运行这个作为 make address,好的,现在点 /address,我希望能看到一个大写的,s,字符串在技术上只是一个地址。

我现在实际上可以在上面进行数学运算,我可以向前走,打印出另一个字符,后面跟着一个换行符,我可以去 s1,所以我可以在那个指针上进行一些非常简单的算术。让我继续向前,重新编译这个,所以啊,地址,我应该能看到。

嗨,如果我再多写一行代码,比如这个 printf,百分号 c,反斜杠 n,星号 s。加上 2,我现在可以去到距离 s 两个字节的字符,这又是。重打印,带有感叹号的高位,这个花哨的。

方括号表示法在某种意义上只是一种花哨的说法。我实际上是在操控 s 的真实含义,它只是一个地址,而在此之前。这个方括号表示法并没有任何根本上与这些星号和地址不同的东西,它只是做得更用户友好。

我个人还是更喜欢第二周的方括号表示法,但这其实是一样的。你自己,所以 c 只是给我们提供了这个方便的特性,使用方括号,为你处理所有这些所谓的指针运算,但再说一遍。我们深入探讨这个低层次,仅仅是为了强调究竟发生了什么,终究在底层。

好的,让我在这里暂停,看看是否有任何问题。我看到有个问题问,如果你尝试打印 star s3 会发生什么?好问题,如果我尝试打印 star s3。我直观地认为这将是换行符,抱歉,空字符。

结束字符串,但让我们看看。

我们可以确认,让我继续并打印出星号,因为我知道重置,所以我很确定这会打印出空值,像这样,%c\n,星号,冒险点去查看一些我也许不该看的东西,因为那是底层实现的细节,但让我们看看会发生什么。

编译没问题,./address,看起来是空的,可能是空字符。老实说,它并不应该是一个可打印字符,而是表示字符串结束的特殊哨兵值,但我可以这么做。我知道从第二周开始,字符是整数,而整数也是字符,如果我。

我希望以那种方式思考它们,所以让我只改动最后的i,然后运行。地址,然后出现了“嗨!”的感叹号,所有的位都是零。多亏了百分比i,我现在可以变得非常疯狂,为什么我们不继续并打印出来呢?不仅仅是这个序列之后的字符“嗨!”的感叹号和空字符。

我们去看看,真的深入我的地址,好吧,那里没什么。距离10,000字节的地方让我继续,做地址,让我继续运行这个。你可能是少数几个见过这个错误的人之一。因为触碰了不该触碰的内存,我们将故意考虑。

今天出现了一个分段错误,代码某处出了问题,内存出现了不应有的情况,而我根本不该去看。距离我知道属于这个字符串的内存大约10,000字节,就像是在随意查看你电脑的内存,这显然不是一个好的做法。

这是个好主意,但稍后再谈,所以现在我们考虑一下这些底层实现的实现细节,并回顾一下上周我们为什么以这种方式做某些事情,实际上在过去的几周中。字符串只是一个char*,让我们现在考虑一个例子,让我缩小视野。

在我的内存上,让我能一次性塞入更多内容,我们考虑一个例子。假设我想写一个程序来比较两个字符串,让我在一个新文件中写点新代码,暂且称之为compare.c。我这个程序的目标很简单,就是打印内容,或者说比较。

两个用户可能输入的字符串,我将继续并包含cs50.h。并不是因为我想要字符串本身,而是因为我想用getstring,只是为了方便,不过我们稍后会把这个辅助工具去掉。在这个程序中,我将先不使用getstring

让我继续保持简单,先从get int开始,我会询问用户一个变量i。让我再做一个这样的,get int并询问用户一个值,简单地说如果i输出。否则让我继续并打印出不同,所以这有点像第一周的内容,我使用了一些分支,并使用printf进行打印。

打印出这两个变量i和j是否相同,运行比较,让我给它数字1和2,确实它们是不同的,让我再给它1和1,它们是相同的,所以我认为逻辑上,uh,通过例子证明,这个程序看起来是正确的,但让我迅速让它看起来似乎。

不正确,因为没有使用,取而代之,让我给自己一个字符串,不再是“wheel”。我们就写“char star s = get string of s”,但即使我称之为“char star”,它仍然是一个字符串,就像几周前一样,让我再给自己一个叫“t”的字符串,uh,t会得到那个值,让我很天真,但也算合理地说,如果s。

“equals equals t”,让我们继续打印出相同,其他情况则打印出不同,所以相同的代码,只是数据类型不同,使用“get string”而不是“get int”。让我继续,比较似乎编译正常,点,斜杠比较,让我继续,输入“uh hi”,哎呀,“hi”,让我继续再输入一次“hi”,瞧。

不同,嗯,我忘了我的反斜杠,出问题了,让我重新编译,进行比较,现在让我再运行一次,怎么样,我们来个快速测试,“david”和“brian”,这些肯定是可行的,怎么样,“david”也不同,嗯,让我再试一次,“brian”,“brian”也不同,但我很确定这些字符串是一样的。

为什么这个程序可能有缺陷,问题是什么,这里有什么问题,有什么想法?关于这里的问题,有什么想法?关于相机或者“brian”,如果你想把一些想法口头表达出来,任何关于为什么“david”和“david”是高和高不同的想法,聊天室里有几个人说我们实际上没有。

比较字符,我们在比较地址,是的,这就是今天关于字符串真正定义的逻辑结论,如果字符串仅仅是它第一个字符的地址,那么如果你真的在做“s equals”,地址,可能会是不同的,即使我输入的是一样的。

每次我们调用“get int”或“get string”时,用户的输入都会被放到我计算机的内存中,但我们现在确实有工具,可以回答这个问题,验证这个答案,让我继续,简化这个程序,快速检查一下,打印出s,让我们继续打印出t。

嗯,使用换行符让我们可以清楚地看到字符串,让我再做一次,让我们比较一下,好的,编译正常,点,斜杠比较,让我输入“hi”。“hi”,它们看起来是一样的,但请记住,现在我有了另一种格式代码,这样我就可以开始把字符串当作它们技术上是的地址来处理。

所以让我把“percent s”改为“percent p”,在两个地方都改,然后让我重新编译程序。现在用相同的高和高重新运行比较,但注意到。

尽管我恰好输入了相同的内容,它们最终在稍微不同的内存位置,即 c 和我的计算机不会那么自以为是地为这两个字符串使用相同的字节,这样做对我来说并没有太大好处。简而言之,它将非常简单地把一个放在这块内存中。

在这块内存的其他地方,这些地址是各自的,但又是任意的。0x22 fe 670 和 0x22 fe 6b,哦,所以它们是分开的,计算机决定实际放置这些的地方。那么计算机内存中到底发生了什么呢?我们考虑一下,如果这是 s,我的指针或。

真的我的字符串,但它只是一个指针,现在是某个东西的地址。注意我画成占据了八个方块,因为在现代系统中,指针是八个字节,所以这就是为什么这个东西这么大。与此同时,当我输入像高这样的内容并加上感叹号。

然后它最终会在内存的某个地方,我们随便说,它恰好在我计算机的内存中出现。现在每一个字节当然都有一个地址。我不一定知道或关心它们是什么,但为了说明的方便,我们再编号一下,0x123 0x124 0x125 0x126。当我赋值时。

左边的 s 是来自 getstring 的值,右边的 getstring 将要做什么呢?你一直在使用它,确实是获取一个字符串并将其作为返回值交还给你。但这实际上意味着什么呢?如果字符串只是一个地址。那么像 getstring 这样的函数的返回值就是返回,而不是字符串本身的概念。

getstring 一直在为我们做的事情是返回字符串的地址,或更具体地说,是存储在 s 中的第一个字符的地址。为了清楚起见,那个地址是 0x123,它并没有返回整个字符串,h、i 和感叹号,而是仅仅返回给你一个值。

它仅仅返回给你该字符串第一个字符的地址,但再一次,这对于 s 来说都很好,t 的情况也是类似的,因为我再次调用 getstring,t 将被赋值为这个版本的高的第一个字符的地址,我们随便说,它在 0x456。

0x457 0x458 和 0x459,此时 t 将获取 0x456 的值。老实说,现在我们真的进入了细节,让我们开始将这一切抽象化,确实,当我们不再关注特定的地址时,s 只是一个指针,一个指向高的第一个字符的变量 t。

只是一个指向第一个的变量。

字符 hi,字符串,就像我之前在程序的早期版本中那样。

t。

我确实是在比较st,但st分别是什么呢?st的实际值是ox123ox456,或者其他什么,这些值不会相同,因为它们指向不同的内存块。

让我在这里暂停一下,看看布莱恩是否有任何问题或困惑。有没有问题或困惑?没有,好吧,谁在乎呢?就像这一切都有点无所谓。那么我们如何解决这个问题呢?让我们考虑一下我在之前演示中实际上做了什么。我有一个string compare函数,可以让你比较两个字符串,我保证。

我们最终会解释为什么使用string compare,而不是仅仅使用==。好吧,使用这个函数我需要在这里添加string.h,但如果使用string compare,让我继续重新编译这个compare./compare,现在让我输入hi,而且hi是一样的,但它们看起来还是不同。

我真是太糟糕了,犯了和上次一样的愚蠢错误,有人知道我犯了什么错误吗?在比较两个字符串时,我不知怎么地犯了错。伊布拉欣建议你加上,没错。是的,如果它们相同则返回零,如果不同则返回负数或正数,正如字母顺序所示。因此,我应该在上次和这次都这样做。

检查与零的相等性,让我继续重新编译这个程序。好的,现在让我重新运行这个程序,输入hi两次,瞧,它们是一样的。为了确保这一点,让我再做一次检查,输入davidbrian,它们应该确实不同。所以现在我再次没有真正做到,但我现在在考虑这些字符串。

从根本上讲,它们只是地址而已,所以现在让我们真正进入正题,让我继续创建一个全新的文件,合理地尝试复制一个字符串并对其进行修改。因此,我在这里继续,为了方便,我仍然会使用cs50库,而不是字符串数据。

只是针对getstring函数进行类型处理,我们将看到这比其他方法更方便。我要继续包含stdio.h,然后再包含string.h,让我在主函数中继续进行。让我在这个程序中继续并获取一个字符串,但我们不会称之为star

所以再次开始处理这个字符串,叫做s,然后我将获取另一个字符串,但我不会称之为那个,我会称之为char *t,我想要复制s,所以你可能会想,基于第一周和第二周的变量,直接去做。我的意思是,我们已经使用赋值运算符从右到左复制一个变量。

整数用于字符和其他数据类型,或许也是,我现在要继续对原始字符串进行更改,所以让我继续这样做,让我继续说。让我们把 t 的第一个字符改为大写,回想一下有这个函数 two upper,它接受一个像 t 中第一个字符这样的字符作为输入。

并返回大写版本。现在要使用 two upper,我需要另一个头文件,我几周前记得我需要 ctype.h。所以让我提前回去把它放上,现在让我继续打印这两个字符串,让我继续打印出 s,作为这个 percent s 的值。

t 的 percent s 如下,所以我再次所做的是从用户那里获取一个字符串,而这里唯一的新内容是 char star,因为在第 10 行,我是从右向左复制字符串,然后我只对复制的第一个字母进行大写,也就是 t。

然后我只是把两个都打印出来,所以让我继续做复制,好的,编译也没问题,输入 h,回车,瞧!看起来我不知怎么地同时将 s 和 t 变成了大写,尽管我只对 t 调用了 to upper。布莱恩,有没有人对此组有什么想法,为什么我意外地并且错误地这样做了?有几个人说 t 是。

有几个人说 t 只是 s 的别名,s 只是 s 的别名,这是一种合理的思维方式。确实,更准确的说法是,有没有其他想法说明这在某种程度上是错误的?彼得现在建议它们具有相同的地址,是将 s 复制到 t,但再说一遍,s 到今天为止,它只是一个地址,所以是的,我复制了 s,但我复制了。

地址 ox123 或者其他的,然后在第 12 行注意到我通过将 t 转为大写来改变 t。但是 t 和 s 在同一个地址,所以实际上我在改变同一字符串。如果我们从计算机的内存角度考虑这一点,让我们考虑我刚做的事情,让 s 和之前一样放下,让我把高放下,但这次全部小写。

并且回想一下,它可能位于地址 ox123one two four one two fiveone, two six。现在,如果我们考虑到 s 技术上包含了第一个字符的地址,变量 t。并将 t 的值赋为 s,我得字面意思去理解这个语句。我确实是把 ox123 放在这里,如果我们现在抽象掉这些。

细节只是为了让它更清晰,从视觉上看发生了什么,这几乎就像在说,s 和 t 是的,在这个意义上,t 只是 s 的别名,实际上 t 是与 s 完全相同的。因此,当你使用方括号表示法访问 t 的第一个字符时,s 中的字符是一样的。所以当我调用 to upper 时。

我是在这个字符上调用它,当然这是故事中唯一的一个 h,当我打印 s 和 t 时,printf 跟随那些相同的线索,最终显示出同样的值。看起来我们确实需要从根本上重新思考我们如何复制字符串。

让我问一下,如果这是把一个字符串复制到另一个的错误方式,正确的方式是什么?即使你没有想到正确的函数或词汇,只是凭直觉,如果我们想要以人类的方式复制一个字符串,就像复制照片一样,我们想怎么做?你有什么想法,布赖恩?

是的,索非亚建议我们想要以某种方式遍历 s 中的元素并将其放入 t 中,是的,我喜欢这个,所以遍历 s 中的元素并将其放入 t 中。这听起来更繁琐,但如果我们想接受这些事实,我们将不得不这样做。

字符串 s 和 t 只是地址,我们现在需要去跟踪这些线索,所以让我们考虑这个程序的一个变体,让我继续这里,并更改它,以便我仍然得到一个字符串 s。但现在让我继续并提出,我们逐个复制。

字符,但我需要把它们复制到某个地方,所以我觉得这个复制字符串的过程还需要一步,就是给自己额外的内存。如果我有 h。i 感叹号和空字符,我现在需要控制我拥有的代码。字符,所以今天有一个新函数,如果我想创建一个字符串 t。

今天也称为 char star,我们可以使用一个新的函数,分配。这是一个相当复杂的函数,但幸运的是,它使用起来相当简单。它只需要一个输入,询问你想要多少字节的内存,所以我该怎么做呢?h i 感叹号,反斜杠 0。我可以直接说 4。

但这感觉并不是很动态,我想我可以更优雅地以编程方式实现这个,让我先说一下,s 中有一个加一的字符。加一,为什么我要这么做?好吧,h i,感叹号,空字符,这在技术上是存储在底层的,但你我认为高的长度是什么。

作为“好吧”,在现实世界中很可能是 h i 感叹号,谁在乎这个低级细节,这个空终止符,你不需要包含那个单词。你只需考虑你能看到的实际字符,因此高的长度是 h i 感叹号三,但我确实需要聪明地再加一个字节,字符。

因为我也需要把它复制过来,否则如果没有一个相同的空字符,t 将不会有明显的结尾。那么我现在该怎么把一个字符串复制到另一个呢?让我先取出一个,实际上,n 等于 s 的字符串长度,我们以前做过这个把戏,i 小于 n,简单地说。

t[i]得到s[i],所以这将字面上从s复制到t。但我现在需要比n更聪明,我实际上会非常激进地说i小于或等于n,为什么我要比我们在字符串迭代时通常做的更进一步,以及迭代一个凯撒密码或足够的字符串。

在这个上下文中,布莱恩,你有什么想法,为什么我要从i小于或等于n开始?这里,塞琳娜第一次建议我们需要包含空字符。是的,如果我现在理解字符串的工作方式,仅仅复制“hi”这个感叹号是不够的,我需要进一步一步。

进一步比长度多一个,会小于或等于n,或者我可以在那加一个,或者我可以这样做,但我认为i小于或等于是合理的方法。现在让我们下到底部,实际上执行这个大写,让我们现在将t中的第一个字符改为。

调用touppert的第一个字符上的结果,打印出任何内容,打印出t是什么,并希望现在只有t被大写了,但我现在需要做一个更改。结果证明这个malloc函数在一个叫做standard lib.h的文件中,再次,这种事情总是可以谷歌,甚至我也会忘记很多。

这些函数有时在什么头文件中声明,但碰巧有一个新的,叫做标准库,它可以让你访问malloc,所以让我现在去比较一下。到目前为止,一切顺利,点斜杠比较,哦,不,这是比较,我的天,哦,我。看起来我忘了in的类型,所以让我进入我的循环中添加。

int是我的错误,让我再复制一次,好吧,所有七个错误幸运地消失了。再复制一下,让我们现在输入小写的high

大写了。

只是S-A-K-A-T的副本,明确说一下,我已经回到了我的方括号中。可以接受,这非常可读,但注意如果我真的想炫耀。我加上i的位置,然后这样做,这样的可读性。但再说一次,这里有等价性,方括号语法是一样的。

作为指针算术,如果你想到t + i的地址,以自己偏移一个或多个字节,你完全可以做到。如果我想要炫耀,我可以在这里说去t中的第一个字符并将其大写,但我认为尽管是这样,你还是非常。

聪明的是你理解指针和地址,如果你正在写这样的代码。老实说,这并不一定那么可读,所以坚持使用第二周的方括号语法是完全合理、正确且设计良好的。尽管我应该小心,这行代码有点风险。

对我来说,因为如果用户只是按回车而不输入hi。或者david或brian,如果他们什么都不输入,只按回车,那么字符串的长度。可能是零,那我可能不应该将字符串中的第一个字符大写。那可能有一些错误检查,比如如果t的字符串长度至少是这样做。

但这只是一个示例,关于程序,实际上我应该在一个完全正确的程序中做更多的错误检查,就像你在问题集中应该做的那样。有时事情会出错,如果你的程序如此庞大、如此华丽、如此占用内存,以至于你正在申请大量内存。

在程序中你不会这样做,但随着时间的推移,你可能会需要越来越多的内存。我们还应该确保t实际上有一个有效的地址,时间。会返回给你地址,就像getstring一样,它会返回找到的那块内存的第一个字节的地址。

然而,有时事情可能会出错,有时你的计算机可能会没有。内存,你可能见过你的mac或pc冻结、挂起或重启,这往往是内存错误的结果。因此,我们实际上应该检查类似这样的东西,如果t等于这个特殊值null,那么我会去啊**d并且退出并返回一个。

退出,让我们退出程序,这不会工作,这可能只会发生一百万次中的一次,但现在检查null更为正确。遗憾的是,c的设计者或更一般的程序员使用了这个n-u-l。也称为反斜杠零,遗憾的是这是一个不同的值。

n-u-l-l表示一个空指针,它是一个虚假的地址,即地址。零,它与反斜杠零不同,你在指针的上下文中使用空指针。通常口头上称为nul或null,在字符的上下文中,反斜杠零用于字符,而ull大写是用于指针的,它只是一个新符号。

我们今天介绍的,带有这个标准lib,dot h文件,好的,结果是,老实说,我不需要做一些工作,结果是,如果我想将一个字符串复制到另一个字符串,有一个函数,来完成这个,越来越多的时候你不需要像以前那样写那么多代码,因为如果你查阅手册。

页面,或者你听说过,或者在网上找到另一个,类似的函数,叫做stir copy。实际上你可以更简单地做,像这样,所以尽管我真的很喜欢这个主意,并且使用像for循环来将所有字符从s复制到t是正确的,但有一个函数为此,它叫做stir copy,它需要两个。

参数,第一个是目标,后面是源,它会为我们处理所有循环和复制,包括反斜杠零。这样我就可以专注于我想做的事情,在这种情况下,实际上是大写字母。所以如果我们现在考虑这个例子,结合我计算机的内存。

我们会看到它的布局稍有不同,但我首先想修复一个错误。这是我们尚未处理的事情,结果是,malloc。你请求计算机分配内存,责任在于你,程序员,最终要把它归还。我的意思是,如果你分配了四个。

字节或四百万字节的内存用于一个更大的程序。你最好把它还给计算机,更具体地说,是操作系统,无论是Linux、Mac OS还是Windows,这样你的计算机最终就不会要求更多内存。要求更多内存显而易见,最终你的计算机将运行。

因为它只有有限的量。

哈佛CS50-CS | 计算机科学导论(2020·完整版) - P9:L4- IO、存储与内存管理 2 - ShowMeAI - BV1Hh411W7Up

硬件,记住,当你完成内存的使用时,最好的做法是,随后释放它,对应于malloc的相反操作是一个叫做free的函数。它的输入是malloc的输出,记住malloc就是分配给你的内存的第一个字节的地址。

如果你像我几行前用malloc请求四个字节,那么你会得到这些字节的第一个地址。你需要记住你请求了多少字节,在free的情况下,你只需告诉free,malloc给了你什么,因此如果你像我一样将那个地址存储在变量t中。

当你完成那段内存的使用时,只需调用free。

t和计算机会为你释放那段内存,你也许会在之后再得到它,但至少你的计算机不会那么快用完内存,因为它现在可以将那段空间重用于其他用途。好吧,让我继续并提议我们现在绘制一幅图,展示这个新的内存中的程序。

记得我们复制的内容是在这里,这是我们之前比较时的停留点。两个字符串如果这是,s,而s指向h i,小写的感叹号。这是我在复制中的新版本代码。

但malloc的返回值将是那段内存的第一个字节的地址,例如ox456或其他,后续的字节会一个接一个地递增。ox457 ox458 ox4,所以当我将malloc的返回值赋给t时,最终存储在t中的就是那个地址。

但我们再回过头来看,这已经是30分钟前的事了,现在我们关注的只是指针的抽象,指针就是一个指向变量的箭头,指向内存中的实际位置。所以现在,如果我开始在我的for循环中复制s,会发生什么呢?我将h从s复制到t,将i从s复制到t。

感叹号,从s复制到t,最后是终止的空字符。从s复制到t,所以现在的图在根本上是不同的,t不再指向相同的东西,它指向自己的那一块内存,现在通过一步一步的方式,复制了所有内容,而我,作为人类,会认为这显然是一个合适的程序副本,有什么问题吗?

关于我们刚刚做的事情,介绍了malloc和free,前者分配内存并给你可以使用的第一字节的地址,后者将其交还给操作系统并说我完成了它。现在可以被重用于其他变量,也许在我们的程序后续中。

更长的布莱恩,任何问题或困惑,有人问即使你在使用strcpy。有人问即使你在使用strcpy来复制字符串。代替自己逐个字符复制,是否仍然需要,释放内存。好问题,即使你在使用strcpy,你仍然需要使用free

是的,每次使用malloc后,你必须使用free,每次使用malloc时,你必须使用。free来释放内存,strcpy是将一块。内存的内容复制到另一块,它并不为你分配或管理那。块内存,它只是基本上实现,那个循环,也许是时候。

我可以在另外的训练中卸下另一只辅助轮,口头上结果表明。getstring一直以来都是一种,有点神奇的东西,因为getstring来自cs50库,毕竟,当我们工作人员多年前编写getstring时。我们没有想到你将输入什么句子。

你打算分析什么文本,以便用于像readability这样的程序。因此,我们必须以这样的方式实现getstring,让你可以输入尽可能少的字符,或根据需要。我们将确保有足够的内存来存储那个字符串,因此,如果你。查看代码,我们的工作人员,调用malloc,我们调用malloc以获得。

确保有足够的内存来适应那个字符串,然后cs50库还在。暗中为你调用free,实际上有一种花哨的方式,你可以编写一个程序,一旦main即将退出。或返回到你的闪烁提示,一些我们写的特殊代码,处理不再使用的内存。

呃,耗尽了内存,呃,因为我们,但你们在使用malloc时都必须调用free。因为这个库不会做到,今天、下周及以后要停止。使用cs50库,最终,完全停止,以便你自己管理。还有其他问题吗?还有其他问题吗?没有,好吧,让我们。

我认为不公平,如果我们引入所有这些花哨的新技术却不。必要地提供任何工具来处理,花哨的代码。或者解决现在与内存相关的问题,值得庆幸的是,有程序可以。除了printf之外,帮助你完成函数和50,以及一般来说。

这个程序,它实际上是最后一个,你看到的,是叫做valgrind的程序。这个程序存在于cs50 ide中,但它也存在于,Mac和PC以及,Linux计算机上,任何地方你可以。运行你自己的代码来检测,是否在内存方面做错了什么。你可能会在内存方面做错什么?好吧,之前我记得我。

触发了那个分段错误,我触碰了不该触碰的内存。valgrind是一个可以帮助你,找出你触碰了不该触碰的内存的位置的工具。因此,专注于你自己的人,可能有漏洞,valgrind还可以检测你是否忘记。调用free,如果你调用了malloc一次或多次,但是,valgrind是一个可以察觉的程序。

这样告诉你你有什么,称为内存泄漏,实际上这与我们自己的mac和pc相关。如果你长时间使用你的mac或pc,或者有时候甚至你的手机,打开了很多浏览器标签,很多不同的程序同时打开,你的mac或pc可能确实已经。

开始慢得令人抓狂,可能使用起来很烦人,甚至不可能,因为一切都如此缓慢,可能是因为你正在使用的一个或多个程序在内存中有一些错误,从未调用释放。它们没有预料到你会打开这么多窗口,但valgrind可以检测到。

像这样的错误,老实说,如果你像我一样,可能你,可能会有10、20、50个不同的浏览器标签页同时打开,想着哦,我总有一天会回来查看,尽管我们从未这样做。每个标签页占用内存,实际上每当你打开一个浏览器标签时,想想它真的是。

不论你使用的是chrome、edge、firefox还是其他的,在后台,它们可能正在调用mac os或windows上的一个函数,比如malloc,给你更多内存来临时容纳那个网页的内容,如果你继续打开越来越多的浏览器标签,就像调用malloc,malloc,malloc,最终你会耗尽内存。

可以暂时从内存中移除一些东西到内存,但最终某些东西会崩溃,可能会影响你的用户体验,当事情变得如此缓慢,以至于你真的不得不退出程序,或者甚至重启你的电脑。那么我们如何使用valgrind呢?好吧,程序,什么也不做的有用的东西,但。

这展示了多个与内存相关的错误,我将这个文件命名为memory.c。我将去d并打开文件memory.c,在顶部包含io.h,然后我还会提前包含标准库lib.h,记住malloc就在这里。int main void,我将保持这个简单,我会去d,给自己。

一大堆整数,这其实有点酷,结果是,嗯,我们继续,好的,我可以这样做,让我们继续。char star s获取malloc,让我先给自己,好的,实际上,让我们去**d并说,s括号0等于72,s括号1,实际上我就这样做。

手动让我们做,h让我们做i,让我们做我们习惯的感叹号,然后为了保险起见加上s。括号三个引用,反斜杠零就像这样,实际上这是非常手动的方式。这实际上是非常手动的方式,构建一个字符串,但让我引入一个错误,字节。尽管我显然需要一个第四个来结束空字符。

注意没有释放,调用释放,现在我要去**d并编译。这个程序,嗯,内存,好吧它编译成功了,所以这很好,点,斜杠内存。好吧,什么也没发生,但这似乎是合理的,因为我没有告诉它去做任何事情。为了好玩,让我们打印出那个字符串,重新编译。

内存仍然可以编译,让我运行 dot slash memory,好吧,似乎可以工作,所以乍一看。你可能会非常自豪,你又写了一个正确的程序。

似乎通过了检查 50,你提交了,然后继续你的日子,但几天后你非常失望。因为你意识到该死,我在这上没有得到满分,因为实际上你的代码中有一个潜在的问题,你不一定能在视觉上看到,也不一定在运行时体验到,但最终在足够多次运行时可能会出现错误。

最终,计算机可能会注意到你做错了什么。幸运的是,像 valgrind 这样的工具可以让你检测到这一点,所以让我继续,增加我的终端窗口大小。让我继续并在 dot slash memory 上运行 valgrind,就像 debug 50 一样。

而不是运行 debug 50 然后 dot slash 无论程序是什么。你运行 valgrind dot slash memory,不幸的是这个只有命令行界面,没有像 debug 50 那样的图形用户界面,老实说,这是一串可怕的输出,乍一看可能会让你感到不知所措。

这不是最好的设计程序,真的只是为了最舒适的人。虽然如此,我们仍然可以从中提取一些有用的东西,像往常一样让我滚动到输出的最顶部,指引你注意几个开始跳出来的事情。

如果你对 valgrind 的输出感到困惑,帮助 50 可以帮助你。重新运行它,但在开头放上帮助 50,就像我现在要做的那样,口头上可以帮助你。帮助你注意到这个混乱输出中的重要内容,这一行,这里是第 10 行内存。我们稍后会看一下,如果我继续向下滚动,发现大小为 1 的无效读取。

而且这似乎也在这里,看起来在内存的第 11 行。如果我继续滚动,继续滚动,继续滚动,块,无论那是什么,但。在一个块中有三字节,显然有一个块是丢失的,然后在这里泄漏总结。显然丢失了一个块中的三字节。

正确的语法,这就是当你的程序没有一个检查条件的 if 时发生的。如果数字是 1、正数或 0,你可以用一个简单的 if 条件修复这个问题。老实说,他们在几年前编写这个程序时并没有这样做,所以这里有两三个错误,一个是某种无效的读取或写入。

另一个是这个泄漏,好吧,值是什么,读取只是指读取或使用。

第10行。如果我向下滚动回我的代码,查看第10行,这个是无效的右边无效。好吧,为什么它无效呢?根据今天的定义,如果你分配了三个字节,第二个字节和第三个字节,但你没有理由去接触第四个字节,如果你只要求三个,这就像一个小规模的版本。

非常冒险和不当,像是10,000字节远,即使是看起来一字节远也是潜在的bug,并且可能导致程序崩溃。同时,第11行也是有问题的,这是一个无效的读取,因为现在你说去打印这个字符串,但那个字符串包含了你不应该有的内存地址。

首先触碰的,以及内存泄漏,第三个问题源于我没有释放那块内存。因此,它需要一些练习和经验,以及你自己的错误来修正。让我先给自己四个字节,让我修复第二个或第三个问题。

通过在最后释放s,因为再次,每当你使用malloc时,你必须使用free,让我去重新编译memory。

似乎可以编译,让我重新运行,它在视觉上仍然是相同的。但现在让我们在它上面重新运行valgrind,valgrind,点斜杠memory,输出仍然看起来非常神秘,但注意所有堆块都被释放。无论那意味着什么,没有泄漏是可能的,它确实没有更。

明确一点,这是好事,如果我向上滚动,我没有看到任何无效读取或写入的提及。所以从本周的问题集开始,以及下周的C语言问题集,你不仅希望使用50并检查50,即使你认为你的代码是正确的,输出看起来正确,你可能还有一个潜在的错误。

当你的程序很小的时候,它们可能不会崩溃计算机,它们可能不会导致那种分段故障,最终它们会,而你确实希望使用这样的工具来追查可能发生的事情。让我去揭示一个示例,这里有一些代码是有点危险的。

例如,这里是一个示例,我在函数顶部声明,uh int star x和int star y。那么这仅仅意味着,给我一个指向名为x的整数的指针。

换句话说,给我一个名为x的变量,我可以存储一个int的地址;给我一个名为y的变量,我可以存储另一个int的地址。但注意,在前两行我实际上并没有给它们赋值,直到第三行,尽管这很奇怪,这并不是之前的方式。

没有理由你不能使用mallocsizeofsizeof是新的,它只是C语言中的一个运算符,告诉你数据类型的大小,比如int的大小。所以也许你忘了int四个字节,而实际上int通常是四个字节,但并不总是每个系统都是四个字节,所以sizeof int只是确保它。

不论你是在使用现代计算机,还是旧计算机,它总会给你正确的答案。因此,这只是意味着在现代系统上为我分配四个字节,并存储x中第一个字节的地址。有人能翻译成普通话吗,star x = 42是在做什么,star是解除引用操作符。

这意味着去到地址,一个口头评论star x = 42在做什么,布赖恩你介意吗?你会如何描述那一行在做什么?是的,索非亚建议在那个地址我们将放置42。完美,在那个地址放42,同样去那个地址x

42放在那里就像去布赖恩的邮箱,把42放进他的邮箱,而不是我们之前放的数字50。接下来的第五行star y = 13,布赖恩,你能为我们口头解释一下吗?star y = 13对我们有什么作用,13不是一个意外。

彼得说倾向于不幸,将13放在地址y,在地址y13,或者换句话说,去地址y并把13放在那里。但是这里有一个逻辑问题,如果我倒回去,y里是什么,如果我最初不这样做。即便我没有给x赋值,我最终还是没有赋值。

当在这里声明它作为一个变量时,我最终开始存储实际的地址。现在只是为了确保程序在检查null以防有什么问题。更严重的问题是我甚至没有给y赋值,这里我们可以揭示关于计算机的一个其他细节,到目前为止我们一直在理所当然地认为。

你和我几乎总是初始化一个char、一个int和一个string,我们字面上在程序中输入它,以便在需要时它就在那儿。但是如果我们考虑这里的这个图像,它现在只是你计算机内存某些内容的物理体现,幽默地贴上了很多奥斯卡·怪兽的标签,这是因为你永远不应该信任。

计算机内存中的内容,有一个编程术语叫做垃圾值,如果你自己没有放入一个值,内存中的某个地方你应该安全地假设它是一个所谓的垃圾值abc你根本不知道它是什么。因为如果你的程序在运行,随着时间的推移,你在调用函数,调用其他。

函数和函数返回的值在计算机内存中不断变化,当你释放内存时,内存会被重用,这并不会清除它,也不会将其全部重置为零或将其全部重置为一。它只是保持不变,以便你可以重用,这意味着随着时间的推移,你的计算机包含。

你程序中所有变量的残余。在这里,在那里,所以在这样的程序中,如果你没有明确地初始化 y,你应该假设“奥斯卡”大概在那个位置,那是一个看似地址但并不是真正地址的垃圾值。

有效地址,因此当你说 *y = 13 时,这意味着去那个地址,但实际上是去那个虚假的地址并在那里放置一些东西,结果你的程序很可能会崩溃,你会遇到段错误,因为访问了某个任意的垃圾值地址,就像拿起一张随机的纸,上面写着一个数字一样。

然后去那个邮箱,像是为什么它不属于你,如果你尝试解除引用一个未初始化的变量,你的程序很可能会崩溃,而这可能在我们的朋友中表现得最好,尼克·帕兰特是斯坦福大学的教授,他赋予了一个粘土动画角色“宾基”生命,我们有一段两分钟的剪辑。

这确实描绘了当你接触不该接触的内存时会发生的坏事情,所以希望这是一个有用的提醒,告诉你该做什么和不该做什么。嘿,宾基,醒醒,时间到了,进行指针的乐趣!

指针学习是什么,哦,好吧,开始吧,我想我们需要几个指针,好的,这段代码分配了两个指针,它们可以指向整数。好的,我看到了这两个指针,但一开始,它们确实不指向任何东西,它们指向的东西叫做被指对象,设置它们是一个单独的步骤。

哦,对对,我知道被指对象是分开的,那么如何分配一个被指对象呢?好的,这段代码分配了一个新的整数被指对象,这部分将 x 设置为指向它。嘿,这看起来好一些,那就让它做点什么吧,好的,我会解除引用指针 x 将数字 42 存储到它的被指对象中,为这个技巧我需要解除引用的魔杖。

你的解除引用魔杖看起来像,我会先设置这个数字。嘿,看看它开始工作了,所以对 x 进行解除引用会跟随箭头访问它的被指对象。在这种情况下,存储 42 到那里,嘿,试着用另一个指针存储数字 13。为什么?好的,我就去 y 那里设置数字 13,然后。

拿起解除引用的魔杖,哦,嘿,这不行,说呃,宾基,我不知道,因为,呃,设置被指对象是一个单独的步骤,而我认为我们从未做到过。好点子,是的,我们分配了指针 y,但从未将其设置为指向一个被指对象。嗯,很有观察力,嘿,宾基,你看起来不错,能不能修复一下,让 y 指向那个。

和 x 一样尖锐,当然我会用我的指针魔法棒和赋值。这样会有问题吗,比如,指向的对象,它只是将一个指针改变为指向。和另一个相同的东西,哦我现在明白了,y 指向与 x 相同的地方。所以等等,现在 y 是固定的,它有一个点,e,所以你可以试试 d 的魔法棒。

再次引用,发送 13,呃,好吧,来了,嘿看看这个,现在解除引用有效。共享那一个点,无论如何,所以我们现在要交换位置吗,哦,看,我们没时间了。

好吧,所以我们还没有完全超出时间,但让我们继续 ah**d,进行我们的第二个五分钟休息,当奥斯卡,好的,我声称你计算机的内存中有所有这些垃圾值,但你怎么能。看见它们呢,呃,Binky 做的当然是尝试解除引用,一个垃圾值,当坏事发生时。

但我们实际上可以用自己的代码看到这一点,所以让我快速去 ahd,编写一个小程序,就像我们在第一周做的。或第二周,但没有做得很好,让我去 ahd,像往常一样包含 standardio.h。int main void,然后让我去 ah**d,给自己一个 scores 数组,如何。

一个三项的 scores 数组,我们之前做过,我们从用户那里收集了分数。可是这次我会故意犯错,不实际。初始化那些分数,甚至不询问人类这些分数。我只是盲目地从 i 等于零开始迭代。

迭代到三,每次迭代我只是冒昧地。打印出位于 scores[i] 的位置上的任何东西,所以逻辑上我的代码在 scores 中是正确的。但注意我故意没有初始化那个数组中的一、二、三的任何 scores,所以谁知道那里会有什么,确实应该是这样。

某种垃圾值,我们无法提前预测,所以让我去 ahd 制造垃圾,呃,因为这个程序在一个名为 garbage.c 的文件中。编译没问题,但当我现在运行 garbage 时,我们应该看到,三个 cryptically。负值 - 八三三零六零八,***,七百六十五和。

第三个恰好是零,所以有这些垃圾值。因为计算机不会为你初始化。那些值,现在有例外,我们偶尔使用像全局变量这样的常量,它在 main 的上下文之外,和我其他的所有函数,如果你不设置它们。

通常初始化为零或 null,但你应该。一般来说不要依赖这种行为,你的本能应该是。总是在考虑接触或读取它们之前,先初始化值,比如通过。printf 或其他机制,好吧,让我们看看这对内存的理解。

这可以引导我们解决问题,但也可能遇到新的问题,但这些问题我们现在希望能够理解。我要继续并创建一个新程序,并回忆起上周的内容。

我们常常希望交换值。当布莱恩为我们排序时,无论是选择排序还是冒泡排序,都有很多交换发生。然而,我们并没有为这些算法写任何代码,这没关系,但让我们考虑一下这个非常简单的基本操作:交换两个值。

比如交换两个整数,让我继续,在 swap.c 中开始一个程序,int main void。在 main 中,我将给自己两个整数,让我给自己一个叫 x 的变量并赋值为 1,然后给一个叫 y 的变量赋值为 2。接着让我继续并打印出这些值。

我只想说,字面上 x 是百分之一,y 也是百分之一。然后我将继续并分别打印出 x 和 y。这个函数叫做 swap,用于交换 x 和 y,但让我们先不这样做,因为我接下来想做的事情是,x 现在是百分之一,y 也是百分之一,x 和 y 将被交换,那么我该如何交换呢?

这两个值,好吧,让我继续,并实现我自己的函数,我认为它不需要返回类型。我会叫它 swap,它将接收两个参数作为输入,我们称之为 a 和 b,似乎合理,现在我想继续交换这两个值。布莱恩上周用他的双手做了这个,没问题,但我们应该考虑一下。

更仔细地看看,事实上,布莱恩用数字的方式不如用现实世界中的东西。我想你面前有几杯饮料。是的,我这里有一个红色玻璃杯和一个蓝色玻璃杯,我想我们可以用来代表两个变量。是的,我让我们假设我希望我说。

提前告诉你,我其实更希望红色液体在蓝色玻璃杯中,蓝色液体在红色玻璃杯中。你介意交换这两个值吗,就像你上周交换数字那样?当然可以,我可以把两个杯子拿过来,交换一下。等一下,但这没问题,这并不完全对。

如果你对我太过字面理解,我认为这里,如果我们现在把眼镜看作记忆中的特定位置,你不能像物理上移动电脑里的内存芯片那样,实际上需要把蓝色液体移到红色玻璃杯中。这样更像是电脑的内存,好的,我可以试试,不过我有点紧张。

但我有点紧张,因为我觉得我不能把蓝色液体倒入已经在里面的那杯。是的,所以这可能不会有好结果,如果他必须在两个杯子之间做某种交换,你有什么想法吗?就像布莱恩上周在交换两个内存位置的内容一样,内容有点奇怪。

嗯,布莱恩,如果你在关注聊天的话,是否有人有什么想法,关于我们如何交换这两种液体?是的,几杯。好吧,布莱恩,你那边是否恰好有第三个玻璃杯在后台?事实上,我想我有。所以我这儿有一个第三个玻璃杯,恰好是空的,好的,那你现在该怎么做呢?

关于交换这两种液体,我想我需要做的第一件事就是清空红色玻璃杯,以便为蓝色液体腾出空间。所以我会把红色液体倒入这个额外的玻璃杯中,暂时这样做。

只是为了暂时存储它,现在我想我可以将蓝色液体倒入原来的红色玻璃杯中,因为现在我可以这么做了。我认为我需要做的最后一件事是,这个原本装有蓝色液体的杯子现在是空的,所以红色的。

我可以把这个临时杯子里的液体,红色液体倒入这个杯子里。现在我并没有交换杯子的位子,但液体实际上已经交换了位置,现在蓝色液体在左边,红色液体在右边,太棒了,我想这就是一个更好的结果。

这是你上周所做的事情的逐字实现,所以看起来相当简单。我只需要一些空间,就像我需要一个临时变量一样,似乎我需要三步。我需要倒出一种液体,再倒出另一种液体,最后将另一种液体倒回去。

我想我可以将这个翻译成代码,给自己一个临时变量,就像布莱恩那样,我会称它为temp。这个名字在交换两个变量时是相当传统的。我将暂时将它赋值为a

a的内容改为b的内容,然后将b的内容改为temp的内容。这听起来相当合理,也很正确,因为这实际上就是对布莱恩在现实世界中所做的事情的逐字翻译,我觉得可以。

之前隐式声明的错误,哦天哪,错误太多了,我的天,函数swap的隐式声明。等一下,我以前见过这个,我也犯过这个错误。任何时候你看到这个,记住这只是因为你缺少原型。记住编译器会字面理解你所写的,如果没有。

它将无法成功编译,所以我们需要在文件顶部包含我的原型。让我现在去运行swap,回想一下在主函数中,我初始化了x为1,y为2,然后打印出xy的值,接着调用swap,然后再次打印出xy,我应该能看到1和2,然后是2和1,所以让我们看看。

进入,嗯,似乎不是这样的,给我加一些,printf是我的朋友,让我去啊d说a是。百分之一b是百分之一换行,a b所以我们打印出来,让我们。打印两次,这将是一个合理的调试技术,如果你,底下。添加一些print f的,让我去啊d,执行swap,编译了点斜杠swap,让我们看看。

a是1 b是2 a是2,我觉得,逻辑是对的,它在交换a和b。但它实际上并没有交换,x和y,我可以确认,调试这个。将是运行调试50设置一个断点,例如在第17行,逐步执行我的代码。

步入swap函数,工作是的,但main并没有真正看到那些结果,考虑这个。我的内存的现实世界体现,所以我可以实际移动东西。所有这一切都要感谢我们的,朋友们在剧院的道具店,如果我们把这看作是我的。计算机的内存,最初它都是,垃圾值,但我可以把它作为一个画布开始。

在内存中布局东西,但调用函数是我们迄今为止视为理所当然的,而事实证明,当你调用函数时。计算机默认以一种标准的方式使用这块内存。实际上,让我去啊**d画一个更,形象化的图,让我画一个更字面的图。

在这里,如果你愿意的话,计算机的内存,再次如此,如果这是计算机的内存,我们放大其中一个芯片,想象这个芯片。拥有很多字节,就像这样,让我们抽象出实际的硬件。把它看作我们一直在看的,它只是这个大矩形区域的,堆积。

刚才提到的,但按照惯例,你的计算机并不会随意地在内存中放置东西。它遵循某些经验法则,特别是它以不同的方式处理计算机内存的不同部分。随机使用它。例如,当你通过在cs50 IDE上执行点斜杠某个程序时。

或者在Linux上更一般地,或者在macOS上双击图标,计算机的硬盘被加载到这里,我们称之为,和零一,所以如果你再比喻一下。你的内存是这个矩形区域,那么机器码的零和一,程序。被加载到内存的顶部,再次顶部底部左右都有。

没有基本的技术含义,只是一个艺术家的表现。但它确实进入了一个标准位置,变量,或者你放在外面的常量。你的函数那些将在你计算机内存的顶部结束,下面是。所谓的堆,这是一个技术术语,指的是一大块内存。

返回,某个内存块的地址,在机器码下方的这个区域。位于你的全局变量下方,这算是一个大区域,但关键是。内存的其他部分被不同地使用,实际上,嗯,而堆被认为是。这里往下,有些令人担忧的是栈,被认为在这里。

这就是说,当你调用 malloc 并请求内存时,内存会在这里分配。当你调用一个函数时,那些函数,堆空间,swap,或者 stirling 或 string compare,或者你到目前为止使用的任何函数,你的计算机会自动将参数从这些函数传递到这里,这并不一定是最好的设计。

因为你可以看到两条箭头互相指向,就像两列火车在轨道上相向而行,坏事最终可能会发生。幸运的是,我们通常有足够的防止碰撞,但稍后再谈,所以再次,当你调用函数时,下面的内存被使用,当你使用 malloc 时,上面的内存被使用。现在对于我的 swap 函数,我不使用 malloc,所以我认为我不必担心堆。

而且我没有任何全局变量,我也不在乎我的机器代码,我只需要知道它存储在某个地方,但我们来考虑一下栈,这是一种动态的地方,内存不断被使用,main 在运行。

main 在这张图片的底部使用了一小块内存,如果你愿意的话,main 中的局部变量像 x 和 y 最终位于这块底部内存,当你调用 swap 时,swap 使用了一块内存,正好在 main 上方,b,swap 返回并完成执行后,这块内存基本上就消失了,现在它并不会明显地消失。

仍然有物理内存在那里,但这时我们又进入了垃圾值的讨论,它们像 oscar the grouches 一样到处都是,你只是不知道,确实存在一些值,这就是为什么我刚刚打印出的未初始化的分数数组,我看到了。

一些虚假的值因为仍然会有之前留下的零和一,问题是让我去看看这个。我们内存的物理体现,并且在增长,实际上如果我想有两个局部变量,就像我做的,x 和 y,让我们去想一想。

这里的内存行被视作 main 例如在这里,我将继续并替换掉所有这些垃圾值,用我在乎的实际值,和我在乎的实际变量,我们将称之为 x 和 y,恰好是一个字节,但一个 int 是四个字节,所以幸运的是我们在 prop blocks 的帮助下,我将继续滑动这个。

在这里,我们将把这个视作 x,实际上我将继续并给这个标记,然后我将继续再给自己一个大小为 4 的整数,并把它放在这里,我们将把它视作 y,并回想一下,我将这些值初始化为多少呢,初始值为一和二,但随后我。

调用swap函数,而swap,函数有两个参数a,和b,设计上这两个变成了。x和y,我定义swap为接受a和b,所以我认为我需要做的。物理上是现在把这,第二行内存视为现在属于swap函数。不是main,而在这第二行内存内,我将把它视为属于。

在swap的行中,我会再有一个大小为4的整数,我们叫这个为,a,哎呀,a。然后我会再有一个uh,b,和之前一样,因为那些只是参数x,y,1,和2,但swap有第三个变量,brian提出的一个。

临时变量,所以我会继续给自己,四个字节,从而去掉。无论那里是什么垃圾值,实际上把它设置为一个叫做temp的整数,所以我会叫这个东西,temp tmp,我首先做了什么,我把temp设为1。temp是1,然后我做了什么,我把a也设为2,然后,a也设为2,然后。

最后我做了什么,我把b设为temp,所以我必须继续,改变这个,使它成为temp的值,可以看到,swap是正确的,只要它在。交换a和b的值,但一旦swap返回,这些就回到被视为。垃圾值,main仍在运行中,swap不再运行,但这些。

值保持在那里,所以那些是垃圾,值我们恰好知道它们是什么,但它们不再有效,因为当时,x和y是什么,它们仍然是一样的,写代码。接受参数,你把参数从一个函数传递到另一个函数。这些参数是从一个函数复制到另一个函数的,确实。

x和y被复制到a和b,所以你的,代码可能看起来非常,正确,因为它在正确地交换。确实交换,但它仅在swap的上下文中交换,而不触及原始值。所以我认为我们需要,从根本上重新实现,swap,以这样的方式真正。改变x和y的一个值,但我们该怎么做,brian如果我们,可以叫一个人来这里。

swap的实现,这样它可以让我,改变x和y,而不是改变。x和y的副本,我可以传递什么,igor建议我们使用指针。igor建议我们使用指针,是的,所以或许是一个解决方案。如果指针本质上就像一张指向内存的藏宝图。

我真正应该做的是,从main传递到swap的不是。x和y字面上,但为什么我不传递,x和y的地址,地址。并实际上进行brian亲自实施的那种交换,所以给函数一种映射,指向那些值,然后去那些值。那么我们该怎么做。

我这样做时,代码必须稍微不同。当我调用 swap 时,这次我真正需要做的是传入这两个变量的地址,所以我不一定知道这些地址是什么,但为了叙述的需要,我们可以假设这个地址例如是 ox123,然后四个字节远。

从那里可能是 ox127,例如,但再次强调,这并不真的重要。但它们确实有地址,x 和 y,因此一个指针的大小通常比较大,所以我们需要得到一块更大的内存,八个字节,表示一个指针,实际上我需要使用更多的内存,而 swap 现在,如果我声明 a 不再是一个整数,而是一个指向整数的指针。

现在,我可以在其中存储 x 的地址,像 ox123,这是一个整型。那是另一个 inch star,恰好是八个字节,我将为这个东西使用稍多一点的内存,但没关系,它的名字将是 b,现在它将包含 ox127。我仍然需要一个临时变量,我仍然需要一个临时变量,但。

没问题,我只需要四个字节,因为变量本身只需要存储一个整型。就像布莱恩临时存储在杯子里一样,所以我只需要额外的四个字节,就像之前一样。这里是 main,swap 现在使用这三个变量,两个就可以了。它向上增长,正如我所提议的,x 在地址 ox123,y 在地址 ox127,因此,提议。

存储 x 和 y 的地址,分别,现在我的代码我认为需要说,这个去。并将变量 temp 中的内容存储在地址 a 所指向的地方,所以你可以把这看作是一个箭头,指向这里,好的,那地址 temp 是什么?就像之前一样,那我们该怎么做呢,现在我将去 ah**d 并进行更改。

不是 a 的值,但我将改变 a 所在位置的内容,让它变成 b 所在位置的内容,这里。现在,回想一下 b 指向的位置,恰好是 y,并将其改为 temp 的值,当然这在这里,并且在这个故事的这一点上,它仍然只是三行代码,情况是不同的。

这三行代码,但当 swap 执行完毕时,注意我们所做的,我们成功地交换了 x 和 y,让 swap 去访问那些地址,而不是简单地获取它们的值的副本。尽管这段代码看起来有点神秘,实际上它只是。

我们迄今为止看到的逻辑,我将进行版本更改,并将 swap 的定义改为不接受两个整数 a 和 b,而是两个指向整数的指针 a 和 b。声明指针的方式是指向的变量类型,后面跟着一个星号,然后是它的名称,坦率地说,我们在代码中还没有看到。

函数的上下文,接收参数但实际上很简单,我加了星号。这里我需要说存储在临时变量temp中,去那里,我该如何说去b并存储。无论temp中的内容,我都会加一个星号,所以temp只是一个简单的整数。它就像布莱恩的空玻璃,没有什么花哨的东西,所以我们不需要。

需要在temp周围加星号,但我现在需要更改使用a和b的方式。因为现在它们是我实际想要去的地址,在这个上下文中没有必要使用地址运算符,但在这里我需要进行更改。我确实需要更改原型以匹配,所以这只是复制粘贴,但我敢打赌你能想象得到。

最后在调用时需要更改的是,swap我不想简单地传递x。我想传递x和y的地址,这样swap就可以特殊访问这些内存位置的内容,从而实际上可以对其进行一些更改。如果我现在重新编译这个程序,生成swap。

我确实进行了交换,交叉手指,瞧。

代码,上周如果你在想,要进行交换,我们本可以,而不需要一个特殊的函数,你不一定需要,main,但我想引入一个抽象,这个函数进行交换,就像布莱恩为我们交换那些玻璃一样。要从一个函数传值到另一个函数,你需要理解。

在你计算机的内存中发生的事情,所以你实际上可以传入小的“面包屑”作为指向那些内存位置的藏宝图,再次感谢这些称为指针的东西。有什么问题吗?这无疑会涉及一些星号和符号,但现在肯定有问题。

概念或能力布莱恩那边有什么,要强调的是,这个堆的设计在顶部,malloc使用内存,而栈在底部,这是一个显然等待发生的问题,那些之前编程过的人可能知道这些术语中的一些,比如堆。

溢出或栈溢出,事实上,很多人可能知道stackoverflow.com,那个只是一个网站。

虽然有一个起源故事,关于调用函数如此多次以至于溢出。每次你像我在这里那样调用函数的时候,都会消耗内存。如果你一次又一次地调用如此多的函数,最终可能会崩溃,此时你的程序将崩溃,根本没有解决方案。

对于那个问题,除了不这样做,比如不要使用太多内存,但这可能很难做到,实际上这是今天编程的一大危险。我们实际上可以故意诱导这一点,实际上我认为我们可以回顾一下上次我们和马里奥讨论到的地方,就是这张图片。

这当然是一个比你可能在问题集零中玩过的简单得多的金字塔,但这是一个递归金字塔,因为你可以将高度为四的金字塔定义为高度为三的金字塔,继而是高度为二的金字塔和高度为一的金字塔,确实我上周使用这些非常的块构建了这个,你可以实现。

像这样使用几种样式迭代马里奥的金字塔,事实上让我去,啊继续快速做出一个确切的解决方案,让我去,啊继续称之为马里奥。c,我要去,啊继续包含,函数,我将使用标准io。h,我要做int main void,所有我想做的就是打印出这个。

这个金字塔,但我想问用户,height等于get int,我们会询问用户高度,就像你在问题集一中做的那样,然后我要去,啊继续画出这个高度的金字塔,现在绘制不存在,绘制这个现在。自己实现绘制,它不需要返回值,因为我只是打印。

屏幕上的内容,函数称为绘制,它将接受一个名为h的输入。例如,h表示高度,但我可以随意命名它的参数。然后我只是要做这个4 int,i等于1,i小于或等于h,i加一。然后在这个里面,这是你可能从某些问题中回忆起来的地方。

发现嵌套循环很有用,让我做一个j等于1,嗯j小于或等于i,j加一。这将类似但不完全相同于过去的马里奥的舒适或不舒适版本,因为这个金字塔。是朝不同方向形状的,让我在这里打印一个哈希。

然后让我继续,啊继续在这里打印一行新内容,所以我快速做了这个。但从逻辑上讲,我正在遍历每一行,从一到h。所以第一行、第二行、第三行、第四行,例如,然后在每一行我故意从一开始遍历,所以我打印一,接着是二、三、四,当然我可以设置为零上下文。

更用户友好,更容易理解,从一开始索引我觉得是完全合理的,如果你认为有一个令人信服的设计论点,所以让我去,啊继续做马里奥。啊,真糟糕,我错过了我的原型,所以请注意,它不理解绘制。所以解决方法是要么移动整个函数,要么如我们所宣传的那样。

只需将你的原型放在顶部,让我重新编译马里奥,好的,现在成功的马里奥,我们设置高度为四,瞧,现在我有一个相对简单的,尽管我确实进行了实践的马里奥金字塔实现,但事情变得相当酷。如果让我规定这是一种正确的迭代解决方案,即使它。

可能需要你一些步骤或者,基于迭代循环的代码,正确,让我把这现在改为递归,回想一下,递归函数是一个调用自身的函数,如何打印高度为h的金字塔,高度为h,减去1,然后你继续打印。多一行块,所以让我字面理解,int i,等于0,i小于h,i加1。

让我去继续并打印那额外的一行砖块,就这样。后面跟着一个换行,所以现在我做得有点快,但我在这里做什么,嗯,如果高度。一次,如果高度等于二,我希望它迭代两次,三次等等。所以我想使用我的零索引,技术,这也会有效。

但如果你愿意,我当然可以把这个改为一个,把这个改为。可是我想去继续,实际上,在这种情况下我想保持为。像我们通常做的那样,从零开始。好吧,让我去继续编译,这样让马里奥。好的,哎呀,有趣的是,所有路径通过,这个函数将自我调用。

所以clang在这里表现得相当聪明,因为它注意到。在我的绘制函数中,我在调用我的绘制,实际上让我看看我是否可以覆盖。让我手动使用clang,编译一个名为maro的程序,使用mario.c。让我去继续并链接cs50,所以,我使用的是我们第二周的老式语法。

好吧,编译成功了,为什么那能编译好呢?make,又是一个使用你的。编译器clang的程序,我们已经配置make,让它对你稍微保护。通过开启特殊功能,我们检测到像那样的问题,直接使用clang时,我现在禁用那些特殊检查,看看我现在运行马里奥时发生了什么,崩溃了。

它甚至什么都没打印就崩溃了,迅速崩溃,再次出现了段错误,应该不会。所以发生了什么呢?如果你把这块内存看作表示。主程序,但接着是绘制,绘制,绘制,绘制,绘制,如果你的每一个。绘制调用只是再次调用绘制,为什么它会停止呢?这里似乎不会停止。

所以看起来我在递归版本中缺少一个关键细节。你知道吗,如果没有什么可以绘制的,如果高度等于零。让我去继续然后立即返回,否则我将去继续绘制金字塔的一部分,然后添加新行,所以你需要这个所谓的基础情况,你。

字面意思上选择等于某个简单的值,比如高度为零,高度为一。任何硬编码的值,这样最终,绘制不会自我调用,所以让我去。继续使用clang重新编译,或者使用make让我重新运行,高度为四。瞧,它仍然像迭代版本一样工作,但现在使用的是。

递归,所以这是一个设计问题,迭代是否优于递归,这取决于。使用迭代版本时,迭代总是会工作,我不会溢出。堆栈并触及堆,为什么,因为我不在一次又一次地调用函数,只有主程序和一个绘制调用,但使用递归版本时,事情会像哎。

我可以画一个高度为h的金字塔,让我让你画一个行的金字塔。这是一个巧妙的循环论证,它确实工作得很优雅,但存在危险。事实上,尽管这个基本情况,可以持续那么长时间,也许让我们尝试一万次调用。所以那样可以,好的,它有点慢,控制C是你的朋友,让我再试一次。

让我继续做一些像2这样的事情。

亿,看看这是否有效,砰,所以即使那也不起作用,因此递归有这种固有的危险,尽管上周它使我们能够更有效地解决一个问题,通过归并排序。我们算是走运,因为我们没有尝试在布莱恩的架子上对超大数据进行排序。

因为如果你使用递归并多次调用自己,似乎是不应该的。那么这里的解决方案是什么,不幸的是,就是不要这样做,设计你的算法,选择你的输入,使其没有这样的风险,我们将在更复杂的数据结构中使用递归,但再次强调,总是有这种权衡。

因为你可以设计一些更优雅的东西,并不一定意味着它总是对你有用,但更常见的是你可能会遇到一种叫做的。缓冲区溢出,而这种情况你肯定会溢出,是当你分配一个数组并超出其末尾时,或者你使用malloc,但你仍然超出。

比你分配的内存块的末尾更远,缓冲区。可以说只是你可以根据需要使用的一块内存。缓冲区溢出意味着超出该数组的边界,你可能正在使用,现在视频,你可能知道缓冲这个短语。

视频像是在Netflix上缓冲,令人烦恼,因为有一个旋转的图标或其他什么。这意味着在YouTube或Zoom或Netflix的上下文中,缓冲区确实是一些通过malloc或其他类似工具检索到的,内存块,填充了包含你视频的字节,并且它是有限的,这就是为什么你可以。

视频,最终如果你离线,你会用完可以观看的视频内容,然后那个愚蠢的图标会出现,你将无法观看更多,因为一个缓冲区。只是内存的一块,一个内存数组,如果Netflix或谷歌或其他公司不安全地实施他们的代码,他们很可能会越过那个边界。

所以说到这里,让我们考虑一下,除了我们从中得到的,主要是为你去掉的,cs50库不仅提供了这个,字符串类型的抽象。再次强调,它并没有给你任何,新的功能,C语言中的字符串确实存在,只是它们的名称更为恰当,称为char。

星星,但这些功能在cs50库中,都可以用其他实际的C实现。使用一个,叫做scanf,但你会看到。

立即就能看到使用像 scanf 这样的旧式函数的危险,它并不是设计得像 cs50 的库那样具有自我防护能力。因此,很容易出错。让我继续创建一个文件,叫做 scanf.c,仅仅是为了演示这个库,标准输入输出头文件 stdio.h,我将会给。

我要继续给自己一个变量 x,然后我会打印出 “x:” 就像 cs50 的 getint 函数那样,然后我要调用 scanf,告诉它从用户的键盘输入一个整数,并存储在 x 的位置。接着我会再次打印出 x 和一个冒号,后面跟一个换行符。

%i\n,然后我要打印 x,那么这里发生了什么呢?在第五行,我声明了一个变量 x,就像第一周一样。第六行像第一周那样使用 printf,有趣的事情似乎在第七行,scanf 是一个从用户获取输入的函数。

就像 getgetstringgetfloat 等一样,但这仅仅是你需要理解。举个例子,如果你想让一个函数改变一个变量的内容,正如我们处理 a、b、x 和 y 时那样,你必须传入你想要改变值的变量的地址,你不能仅仅传入 x 本身。

如果你在第一周没有使用 cs50 库,你会写这样的代码来获取用户输入的整数,你必须理解指针,还得理解符号和星号等等。在我们第一周所关心的只是循环时,这实在太复杂了。

还有变量和条件等基础知识,但现在我们可以调用 scanf,告诉它从用户的键盘输入一个整数,或者用 %f 来获取一个浮点数,或者其他类似的代码,并传入 x 的地址,这样 scanf 就可以去那个地址,将用户键盘输入的整数放在那里。

第八行是第一周的内容,我只是打印出值。这是相当安全的,我要继续使用 scanf,它能编译通过。我要运行它,输入 50,瞧,奇怪的事情发生了,因为如果你运行这个程序并输入猫,x 就是零,并且没有错误,来看看这个。

cs50 库的一个特点是,我们会不断地提示用户。如果他们不合作,没给你一个整数,这就是你从库中得到的一个特性。但是,实际上 getstring 更加强大,因为如果我去改变这个程序,不再获取一个整数,而是获取一些更复杂的东西,比如字符串,等等。

我们现在称其为 char*,我要继续做一些非常相似的事情,我会提示用户输入一个字符串 s,并使用 scanf,用 %s,就像 printf 使用 %s 一样,我会做这个。s 从根本上是一个地址,因此只需传入你已经拥有的地址。

现在我要继续打印,s冒号%s换行符并打印s。但是当我用make编译这个scanf时,它不喜欢这个。当使用时,变量s未初始化,有点冒险,我可以覆盖make的保护,我可以自己手动编译,这样是可行的。

点斜杠scanf让我继续输入,比如说,嗨,你会看到奇怪的空值。幸运的是,创建一个内部clang,在某种程度上帮助我们帮助自己,它指出你声明了一个指针,但那里没有东西,那是一个垃圾值。因此没有地方可以放置这个,聪明的是不盲目地去那里。

hi、感叹号和空字符随意放置,他们只是将其留空,这个特性。如果你看到空值,你就搞砸了,表现得很慷慨,不会崩溃。如果我真的想要这个,我需要自己分配四个字节,正如我们今天早些时候所做的,或者我可以回到第二周的内容。

说点什么,比如给我四个字节,不过这在堆栈的某个地方给我四个字节,姑且称之为main的框架,这些行被称为框架。如果我使用malloc,它来自所谓的堆,虽然没有画出,但大致在上方。唯一的区别是,如果我使用malloc,我必须使用free,如果我使用堆栈。

就像我在第二周所做的那样,我不必使用,坦率地说,今天有太多新内容,我喜欢坚持使用老式数组。但现在如果我继续使用scanf,它在make时编译。如果我然后运行scanf并输入hi,哇,它似乎工作了,但那是因为我聪明,预见到了那四个字节。大卫,你明显超出了四个字节。

字节和我按下回车键,现在发生了一些奇怪的事情,其他的就完全丢失了。如果你试图获取类,这会非常令人恼火和沮丧。getstring可以为你避免这个问题,getstring会为你调用malloc,并为你分配一块尽可能大的内存。我们有点在关注他们的输入。

一个字符接一个字符,我们确保分配或重新分配足够的内存。scanf本质上就是一个像cs50库这样的函数在底层工作的方式。但它为你完成了所有这些事情,一旦你去掉像这样的训练轮,或者坦率地说,这样的库,这确实是,归根结底,它不仅仅是。

教学工具,这是一个有用的库,你必须开始自己实现更多的低级内容。所以,再次说,如果你不想这样,那没关系。现在的责任在于你避免所有这些可能的错误条件。好了,话虽如此,我们还有一个最后的特性,要为你提供,以激励本周的问题。

你将实际探索和操作并编写代码以更改文件,为此我们需要文件输入输出的最后一个主题,文件I/O是描述从文件中获取输入和输出的术语。到目前为止,我们编写的每个程序几乎只使用内存。

像这样,你可以放东西,结束,内容就消失了,内存的内容就没了。当然,文件是你我在计算机世界中保存我们的论文、文档、简历等所有东西的地方,永久保存于你的计算机上。在C中,你当然有能力自己编写代码来长期保存文件,所以例如让我继续编写我的。

在这里编写一个电话簿程序,将号码存储在文件中,我将继续包含cs50库,因为我不想处理scanf。我将把这个文件保存为phonebook.c。

也包含string.h,我将在我的主函数中继续使用一些新函数,虽然我们在这里只会简要看到,但在接下来的问题中,你将更详细地探索这些。我将给自己一个文件指针,结果奇怪的是,用大写的F-I-L-E,这是C语言中的一种新数据类型。

这个文件的表示方法是,我将为一个文件给自己一个指针。我可以将其命名为file,也可以叫f,或者叫x,我决定称之为小写的file,以便明确。我将使用file open,它需要两个参数,第一个参数是。

你想打开的文件名,我将打开一个名为phonebook.csv的文件,然后我将以不同的方式打开它,具体来说是以附加模式来读取它们,即查看其内容;写入则是完全更改其内容,逐行添加,以便不断增加。

为了向他们提供更多信息,我将继续并安全起见,我将说如果file等于null,因为回想一下,null表示出现了问题。我们就返回,可能我输入错误了文件名,可能它不存在,潜在地发生了什么问题,我将通过说如果来检查。

如果file等于null,就退出程序。我们可以称之为字符指针,现在叫name,我将询问用户的名字。我们之前做过,我将继续询问他们的电话号码,我们之前也做过,唯一的区别是我将调用字符串字符指针。

现在这很酷,如果我想将这个名称保存为 csv。如果不熟悉的话,这在咨询界和分析界很流行,它只是一个电子表格,一个用逗号分隔的表格。在 Excel、Numbers 或 Google 电子表格中,我要去,而不是打印 f,而是将字符串名称和数字 fprintf 到那个文件中,然后在这里我要关闭。

这个文件是新的,fprintf 不是 printf,后者是输出到你的屏幕。fprintf 是输出到一个文件,所以你需要一个指向你想要的文件的指针。然后你将这些新的字符串发送给它,你仍然需要提供一个格式字符串。这个字符串告诉 fprintf,这是我想要输出到文件的数据类型,然后你就可以插入。

使用 printf,最后我们关闭文件,所以简而言之,这个程序似乎会提示人类输入姓名和号码,然后它将去,文件。因此让我去,制作电话簿,好吧到目前为止没有错误,点斜杠电话簿大卫 949。468-2750,好吧让我再运行一次,尽管看起来没有发生任何事情。

布莱恩,怎么样 617 495 1000,按回车让我检查一下我的文件浏览器。

请注意我们今天创建的所有文件,包括如果我放大。

不仅仅是 phonebook.c,还有 phonebook.csv,如果我双击那个,注意里面有什么。我们的数字,甚至比这更酷,让我去,关闭这个,让我去,下载这个文件。

使用这个 IDE,它会将文件放入我的下载文件夹,让我去,点击它,它会打开。

Excel、Numbers 或你在 Mac 或 PC 上拥有的任何其他软件。

我要去,继续在这里格式化,但我打开了一个电子表格,是我自己使用 f openfprintff close 生成的。因此,现在我们有指针可以操作文件,这非常酷,但我们要记住,这种思维方式,如果你稍微瞥一眼。

这可能看起来很神秘,它看起来像机器代码,但并不是。这或许是文件中微笑表情最简单的表示形式。如果你有位图文件,一张比特图,多个比特的网格,这些比特很简单,可以是零和一。如果你将黑色分配给零,白色分配给一,你可以。

实际上想想这个相同的网格。

零和一确实代表了一个微笑的表情。换句话说,这里有一些像素,我们在零周讨论过像素。像素就是组成计算机上图形文件的点,而像素无处不在。现在所有人都通过 Zoom 或 YouTube 等平台实时收看,我们正在观看多幅图像的流。

每秒20到30帧,当然这些图像的清晰度是有限的,通常在电视和电影中都是如此。如果有一个坏人被监控视频拍到,通常人们会,增强视频并放大,看看那揭示了什么。

谁犯了一些罪,嗯,那都是些胡说八道,这源于,第一周。实际上只是为了嘲弄这个,让我来,继续播放这部美国的电视节目,叫做《CSI》,只是为了让你感受一下这种。

逻辑是我们知道的。

在9 15,雷·圣托亚在ATM前,所以问题是他在那儿做什么。

9 16,使用九毫米手枪射击。

也许他看到的是狙击手,或者在和它一起工作等等。

将他的脸放大到全屏,他的眼镜上有一个反射。

那是棒球队,那是他们的标志。

那是棒球队,那是他们的标志,他在和穿着夹克的人交谈。我们可能有目击者,目击了两起枪击事件。

所以不幸的是,今天将会毁掉你对很多电视和电影的期待,因为你不能无限放大,看到更多的信息,如果那信息根本不存在,最终只存在有限数量的位和,布莱恩。你可能会看到,哦,他眼中有一丝光芒,让我们看看。

他眼中反射的是什么,如果我们在这张布莱恩的图片上放大,或许我们再放大一点,实际上就只有这些。你不能只点击增强按钮看到更多,因为归根结底,这些只是像素,而像素在第一周就是,只有零和一,并且是有限的。

所以你所看到的就是你所得到的,现在,这里让我播放另外一个短片,来自《未来兄弟》,它也很好地强调了这一点。

但是更加调皮地放大那个死亡球体。

为什么仍然模糊,那就是我们所拥有的所有分辨率,放大并不会让它更清晰。

它在《CSI:迈阿密》中也有,所以我们有两个片段在互相谈论,但我必须为2020年更新一些内容,对吧?你如今在互联网上或杂志上,根本无法找到任何一篇不提到机器学习和人工智能,以及那些你能做以前无法做到的事情的复杂算法的内容。

是不太可能的,这实际上是你可能还记得的情况,从零周开始。我们在哈佛档案馆发现了一幅美丽的水彩画,总共大约只有11英寸高,而现在却在我身后达到了13英尺的高度。通常,如果你只是增强这幅水彩画。

如果不加限制地进行,它看起来会很快变得非常愚蠢,伴随着许多许多的相机。就像档案馆所做的那样,捕捉原始图像,但我们想把它放大到13英尺高,这样它会一直存在,在某种意义上,所以长话短说,使用更高级的人工智能机器学习,来实际分析数据。

并寻找那些并不一定在原始图像中可见的模式。开始放大,这个分辨率看起来相当不错,但实际上这是。涂在真实画布上的,这只是使用Photoshop放大,但当通过复杂的基于机器学习的软件时,改进它,实际上不仅仅是看到这个窗口。

从其中一栋建筑物的顶部,Photoshop,你可以开始看到更多细节,所以在Photoshop中。这是在实际应用了复杂的人工智能算法后,这些算法注意到,等一下,那儿有一点变色,等一下,那儿有一点变色。而如今,增强功能正变得越来越普遍,它仍在推断,并不是。

复原信息实际上是通过算法来重构的,进一步说,你可能会看到这真的开始模糊了,如果你只是使用。Photoshop并不断放大,但如果你通过足够复杂的算法处理它,并开始注意到人眼看起来微小的差异,我们甚至可以进一步增强。

这并不是无限的,所以在某种意义上,我们在创造并不一定存在的信息。因此,这些东西是否在法庭上能够成立是另一个问题,但它可以提高我们的忠实度。从11英寸放大到13英尺,因此在操纵图像时,最终。

我们确实有一些程序能力,包括这个文件指针,就像我们刚刚看到的。还有一些其他功能,我们在这里的最终示例将在接下来的一周中展示。也就是用新获得的指针和地址理解,操作你自己的图形文件,实例,我要去啊**d打开一个程序。

叫做等我一下,我要打开一个程序,叫做jpeg.c。

这个程序jpeg.c是我提前写好的,已在课程网站上发布,类型。叫做字节,事实证明在C语言中,没有对字节的共同定义。我们所知道的字节是八位,而创建字节的最简单方法就是自己定义,就像我们定义字符串一样。

就像我们定义其他类型一样,像学生,为了或一个人,实际上是为了。代码只是声明一个称为字节的数据类型,使用另一种更晦涩的数据类型,称为u。和一个下划线t,但在问题集中会更详细,这只是做了一个称为字节的事件。注意在这个程序中,我复活了第二周命令行中的概念,用户。

注意我在检查用户,立即返回1以表示第17行中的错误。我在使用我的新技术,打开一个文件,使用人类在命令行中输入的文件名,这次我打开它以读取,带引号的“r”,所以如果,bang文件,即如果感叹号,文件或如果文件等于不,那些意味着同样的事情。

我可以去ah**d并返回1,表示一个错误,在这里我做一些小聪明,结果是,以非常高的概率你可以,仅通过查看文件的前三个字节,确定任何文件是否为jpeg。许多文件格式在其文件开头有称为魔法数字的东西,而这些是行业标准。

一或二或三或更多的数字,通常被期望在文件的开头,以便程序可以快速检查这是否是jpeg。这是gif,是否是word文档,是否是excel文件,它们在开头往往有这些数字,而jpeg有一个字节序列,我们。

关于看到这行代码24,在接下来的问题集中,你将看到这如何给自己提供一个字节的缓冲区,特别是一个三字节的数组。接下来的这行代码,你将在本周看到,称为f read,f read顾名思义是读取一个文件,使用起来有点复杂,但。

随着时间的推移,你会对此感到更舒适,它读取到这个缓冲区的第一个。参数是这个数据类型的大小,即字节的大小,并且它从这个文件中读取这么多。这是四个参数,从我们看到的来看,这有点多,但它从这个文件中读取三个。

字节进入这个数组,也就是缓冲区,称为字节,所以这就是你,文件的方式。但从中读取它,然后注意这里,圆圈,如果字节的括号0等于oxf,且字节的括号1等于ox d8,且字节的括号2等于ox。ff 这对你来说确实显得神秘,但这只是因为我在手册中查找了jpeg。

结果几乎任何jpeg,必须以oxf oxd8 oxf开头。这是你在Mac、PC或互联网上的任何jpeg的前三个字节。总是有这三个字节,结果第四个字节进一步决定一个文件是否,实际上是jpeg,但算法很简单。

如果文件的前三个字节是这些,或许你有一个jpeg。但如果你没有完全是这些jpeg,我可以在今天的代码中这样做。让我去ah**d并抓取两个我带来的其他文件,其中一个恰好是一张照片,呃给我一秒,我带来了。

一些文件,其中一个叫做brian.jpg。

这是brian的同一张照片,然后我有一个gif,当然不是jpeg。

这里是一只猫在打字,而我面前实际上有一个程序,如果我生成jpeg,因为这个配置文件是jpeg.c,我运行./jpeg,可以在命令行输入类似cat.gif的内容作为参数,按下回车,我应该看到没有,作为参数的命令行,我也许会再看到一次。

也许只有因为实际上判定某事的算法,而不是那样。但确实我现在可以访问,单独的字节,因此,像素似乎是的fo的。这,让我去啊**d并给你展示一个我们故意提前写的,程序,只是为了让你尝试一下,接下来问题集的内容。

这个程序是你可能使用过一次或多次的程序cp的重新实现。回想一下cp是一个程序,在ide和更一般的linux中,允许你复制文件,你执行cp filename,这怎么工作,我现在拥有所有的构建块来复制,字节在这里,我将main定义为接受命令行参数。

这里的参数,注意一个变化,我没有使用cs50库,因此即使是之前的字符串在第二周,现在也是char *,即使在这里对于argv,我确保人类输入三句话,程序的名称,源文件和目标文件。我再次使用fopen,打开源文件,这里来自argv[1],我确保它是。

不是空的,然后我如果是空的就退出,接着这里有些新的东西,打开目标文件,这里也是使用fopen,但我使用的是“w”,我打开一个文件为“r”,一个文件为“w”,因为我想从一个读取并写入到另一个,然后在这里这个循环,另一个,我给自己一个一字节的缓冲区,所以只是一个临时变量,就像。

brian的临时或空玻璃,我使用这个函数fread,我正在通过它的地址将一个字节的大小读入那个缓冲区,具体来说是一个字节,使用相同的循环。我正在将这个缓冲区的字节大小,具体来说是一个字节,写入目标,所以字面上讲,你可能看到我使用的cp程序。

你自己已经用来复制文件,实际上是在做这个,它打开一个文件,遍历它的所有字节,然后最后,关闭文件,这最后两个例子故意快速,因为这一整周将花费时间深入文件输入输出和图像。呃,但我们所做的就是使用这个,freadfopenfwrite

fclose来操作那些文件,所以例如如果我现在这样做。让我做make cp,好像编译通过了,./cp

brian.jpg,如何关于brian2.jpg,按下回车似乎没有发生什么,但如果我进去这里,我们有第二个。

brian的实际文件副本,因此,这有多种图像文件格式,第一个是jpegs,我们将给你一个所谓的法医图像,来自数码记忆卡的一堆照片,事实上,今天这非常普遍,尤其是在执法中,获取硬盘、媒体棒、手机和其他设备的法医副本。

然后分析它们,以找回丢失、损坏或删除的数据。我们将确切地这样做,你将编写一个程序来恢复意外删除的jpegs,就像从数码记忆卡中删除的,并将给你那张记忆卡的所有副本,通过制作一个法医图像,也就是相机。

并将它们放在一个文件中,你可以读取,然后从中进行操作,我们还会介绍位图文件bmps,它们在Windows操作系统中被普及,用于墙纸等,但我们将利用它们来实现你自己的类似Instagram的滤镜,所以我们将以本周的步行桥这张图片为例。

在麻省剑桥的哈佛大学,我们将让你实现多个滤镜,举例来说,从这个原始图像开始,并通过自上而下、自左而右迭代所有像素,将其去饱和,使其变成黑白,识别任何颜色,如红色、绿色或蓝色,或介于两者之间的颜色。

并将它们更改为某种灰色阴影,做一个棕褐色滤镜,使事情看起来。老派,就像这张照片是多年前拍摄的一样,通过类似应用的启发式方法,改变这张图片中所有像素的颜色,我们会让你翻转它。因此你必须把这个像素放在这里,把那个像素放在那儿。

你会确切地理解文件是如何实现的,实际上。这并不是意外,使得这里的情况更难以看清,因为你开始在平均值之间来,变得更难以看清,所以即使你选择实现边缘检测,你会感觉更舒适。

这些图片中所有物理对象的边缘。为了在代码中实际检测它们,并创造出这样的视觉艺术,现在这确实是很多内容,我知道指针通常被认为是C语言中更具挑战性的特性,当然在编程中也是如此,因此如果你感觉。

已经过去了一段时间,但你现在有能力。无论是今天还是在不久的将来,甚至可以理解像这样的xkcd漫画,所以我们今天的最后一幕是关于这个笑话,你,时刻,一个非常。

狂 geeky 的笑声,我看到至少有一些微笑,这让人感到安慰。这就是cs50,我们下次见!

posted @   绝不原创的飞龙  阅读(27)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 被坑几百块钱后,我竟然真的恢复了删除的微信聊天记录!
· 没有Manus邀请码?试试免邀请码的MGX或者开源的OpenManus吧
· 【自荐】一款简洁、开源的在线白板工具 Drawnix
· 园子的第一款AI主题卫衣上架——"HELLO! HOW CAN I ASSIST YOU TODAY
· Docker 太简单,K8s 太复杂?w7panel 让容器管理更轻松!
点击右上角即可分享
微信分享提示