数据结构——基础测试

 

数据结构基础


前言

数据结构主要要解决的问题是:

  1. 数据如何在内存中存储,以什么结构存储。
  2. 对应不同的存储结构应该用什么样的方法对数据进行操作来满足相应的需求。

简单来说,上面第一点就是数据结构,第二点就是算法。不同的数据结构必然会有不同的算法来对数据进行操作。

学习需要的基础

  1. 任意一种编程语言基础。(比如C语言
  2. 熟悉理解内存的结构并能精确控制。(比如利用C的指针正确控制内存
  3. 面向对象的编程思想。

基础测试

阅读下面的代码,回答后面的问题,如果你都能和答案理解意思一样,说明:

  1. 你C语言基础已经可以学习数据结构并且不会有什么大问题。
  2. 你对内存结构有了深刻的理解。
  3. 你有了基本的面向对象思想,以及明白了面向对象在底层的部分实质。

代码如下:

问题:

  1. 第9行的目的是什么?
  2. newStudent的意义是什么,它的返回值类型为什么是Student?
  3. Student的本质是什么?
  4. 21行写成Student stu1;同时删掉22行行不行?
  5. 23行、28行为什么要用->,能不能用.(这是一个点)

答案解析将在之后更新。。。如果你无法回答,可以阅读思维导图指针教程部分的内容

答案

  1. 第9行的目的是把一组数据集合的指针定义为一种新的类型。
  2. newStudent的意义是创建新的学生,返回值是指向学生的指针。
  3. Student的本质是结构体的指针。
  4. 不行,仅有指针并没有申请内存,数据实际还不存在。
  5. 因为是结构体的指针,而不是结构体本身,所以用->访问。

内存分析

第15行
调用newStudent函数,传入参数。
申请1块内存stu1,把函数返回值赋值给stu1。

第20行:函数调用内部
申请一个结构体指针ret初始化为空。
用malloc动态申请一个结构体大小的内存,并把首地址赋给ret。
利用传入的参数初始化结构体的内容。
把ret里面的内容返回,ret在函数结束后被自动回收。

回到16行
把stu1的内容传入show函数。

第27行:函数调用内部
根据传入的stu访问里面的数据。

回到17行程序结束。

内存图解

15行函数调用前

mxdxjc1

函数调用过程,21行开始执行

mxdxjc2

21行执行完毕,开始执行22行

mxdxjc3

mxdxjc4

22行执行完毕,开始执行23行

mxdxjc5

23行执行完毕,返回15行,传回ret

mxdxjc6

开始执行16行

mxdxjc7

28-30行开始执行,根据this指针访问内存。函数结束,回收show函数空间。

mxdxjc8

 

总结

看到this是不是很熟悉??面向对象其实就是对一个数据集做一些操作,这些数据就叫属性,这些操作就叫方法。改改语法是不是和Java或者C++很相似??这些既是面向对象的底层部分细节,也是学习数据结构必要的知识,只有对内存了如指掌才有可能灵活的控制使用内存。

2015.11.6.KMP算法

 

       KMP算法是要解决的问题是一个字符串中查找指定子串的问题。由于臻臻问我这个问题,所以我不得不再次看看KMP -_-|| ,同时感谢袁涛和王宇晖Orz,(另:默默等待王霸的KMP第三层优化,但愿他能成功)。

       本人水平不高,有写错的地方还请批评指正。

       先说传统的匹配算法。以图中的字符串为例,第一行是要匹配的目标字符串(称之为主串。其中第i个简写为S[i]),要匹配的字符串称之为子串(也叫模式串,其中第i个简写为P[i])接下来每行代表一次匹配操作,黄色部分代表匹配成功的部分,红色字符为发现的第一个不匹配的字符。所有字符计数从1开始。所有字符计数从1开始。所有字符计数从1开始。(重要的事情说三遍)

 

左侧第一列是每一次匹配情况。下面是详细的匹配过程描述。为了方便观察变化,上图中每次移动子串将在新行展现。实际匹配时只在当前行向后移动。

1.P[1]和S[1]对齐。

2. 比较S[1]与P[1]是否相同结果是不相同)。图中第1行

3. 将P[1]和S[2]对齐(把模式串向后移动1个字符的位置)。

4. 比较P[1]与S[2]是否相同(结果是相同)。

5. 比较P[2]与S[3]是否相同(结果是相同)。

6. 比较P[3]与S[4]是否相同(结果是相同)。

7. 比较P[4]与S[5]是否相同(结果是不相同)。图中第2行

8. 将P[1]和S[3]对齐(把模式串向后移动1个字符的位置)。

9. 比较P[1]与S[3]是否相同(结果是不相同)。图中第3

10. 将P[1]和S[4]对齐(把模式串向后移动1个字符的位置)。

11. 比较P[1]与S[4]是否相同(结果是不相同)。图中第4

12. 将P[1]和S[5]对齐(把模式串向后移动1个字符的位置)。

13. 比较P[1]与S[5]是否相同(结果是不相同)。图中第5

14. 将P[1]和S[6]对齐(把模式串向后移动1个字符的位置)。

15. 比较P[1]与S[6]是否相同(结果是相同)。

16. 比较P[2]与S[7]是否相同(结果是相同)。

17. 比较P[3]与S[8]是否相同(结果是相同)。

18. 比较P[4]与S[9]是否相同(结果是相同)。

19. 比较P[5]与S[10]是否相同(结果是相同)。

20. 比较P[6]与S[11]是否相同(结果是相同)。

21. 比较P[7]与S[12]是否相同(结果是相同)。

22. 比较P[8]与S[13]是否相同(结果是不相同)。图中第6

23. 将P[1]和S[7]对齐(把模式串向后移动1个字符的位置)。

24. 比较P[1]与S[7]是否相同(结果是不相同)。图中第7

25. 将P[1]和S[8]对齐(把模式串向后移动1个字符的位置)。

26. 比较P[1]与S[8]是否相同(结果是不相同)。图中第8行

27. 将P[1]和S[9]对齐(把模式串向后移动1个字符的位置)。

28. 比较P[1]与S[9]是否相同(结果是相同)。

29. 比较P[2]与S[10]是否相同(结果是相同)。

30. 比较P[3]与S[11]是否相同(结果是相同)。

31. 比较P[4]与S[12]是否相同(结果是相同)。

32. 比较P[5]与S[13]是否相同(结果是不相同)。图中第9

33. 将P[1]和S[10]对齐(把模式串向后移动1个字符的位置)。

34. 比较P[1]与S[10]是否相同(结果是不相同)。图中第10行

35. 将P[1]和S[11]对齐(把模式串向后移动1个字符的位置)。

36. 比较P[1]与S[11]是否相同(结果是不相同)。图中第11

37. 将P[1]和S[12]对齐(把模式串向后移动1个字符的位置)。

38. 比较P[1]与S[12]是否相同(结果是相同)。

39. 比较P[2]与S[13]是否相同(结果是不相同)。图中第12

40. 将P[1]和S[13]对齐(把模式串向后移动1个字符的位置)。

41. 比较P[1]与S[13]是否相同(结果是相同)。

42. 比较P[2]与S[14]是否相同(结果是相同)。

43. 比较P[3]与S[15]是否相同(结果是相同)。

44. 比较P[4]与S[16]是否相同(结果是相同)。

45. 比较P[5]与S[17]是否相同(结果是相同)

46. 比较P[6]与S[18]是否相同(结果是相同)

47. 比较P[7]与S[19]是否相同(结果是相同)

48. 比较P[8]与S[20]是否相同(结果是不相同图中第13

49. 将P[1]和S[14]对齐(把模式串向后移动1个字符的位置)。

50. 比较P[1]与S[14]是否相同(结果是不相同)。图中第14

51. 将P[1]和S[15]对齐(把模式串向后移动1个字符的位置)。

52. 比较P[1]与S[15]是否相同(结果是不相同)。图中第15

53. 将P[1]和S[16]对齐(把模式串向后移动1个字符的位置)。

54. 比较P[1]与S[16]是否相同(结果是相同)。

55. 比较P[2]与S[17]是否相同(结果是相同)。

56. 比较P[3]与S[18]是否相同(结果是相同)。

57. 比较P[4]与S[19]是否相同(结果是相同)。

58. 比较P[5]与S[20]是否相同(结果是相同)

59. 比较P[6]与S[21]是否相同(结果是相同)

60. 比较P[7]与S[22]是否相同(结果是相同)

61. 比较P[8]与S[23]是否相同(结果是相同)

62. 比较P[9]与S[24]是否相同(结果是相同)

63. 比较P[10]与S[25]是否相同(结果是相同)

64. 匹配成功。图中第16行

 

仔细观察原始匹配算法的规律,以上述第15步开始为例。

模式串的前7个字符P[1 2 3 4 5 6 7]与主串S中的某个子部S[6 7 8 9 10 11 12]相匹配时,如果P[8](图中红色的c)S[13](a)匹配失败,下一次将会把P[1]S[7]对齐,然后从P[1]与S[7]开始向后逐一进行匹配。

也就是说:

当模式串的前i个字符P[1 – i]与主串S中的某个子部S[m ···· m + (i – 1)]相匹配时,如果P[i + 1](图中红色的c)S[m + i](a)匹配失败,下一次将会把P[1]S[m + 1]对齐,然后从P[1]与S[m + 1]开始向后逐一进行匹配。

至此原始的字符串匹配算法结束。

 

我们最开始的目的是找到主串S中是否存在子串P,也就是说我们希望尽可能多的匹配子串P的内容。观察图中第6-9行的过程(对应上文步骤14开始的过程)。

 

当P[8](图中红色的c)S[13](a)匹配失败,会有两次后移子串的操作,这时会发现在第9行中子串P的前4个字符组成的字符串和第6行中前7个字符组成的子串的后4个字符组成的字符串是一样的。也就是图中蓝色框线部分。放到一起来看是这样的:

换句话说,也就是子串前7个字符组成的字符串的头部和尾部有4个字符匹配(下文称之为前缀和后缀)。

这时观察上面的图,那么可以思考一个改进方案,就是当发现P[8]与S[13]不匹配的时候,下一步直接比较P[5]和S[13](因为前4个已经在图中第6行匹配过了),也就是把子串S移动了3个字符的距离

在这里我们引入跳转表如下(先不要管怎么算的)

含义是:当P[j]与主串中某个字符(设为t,在上例中是S[13]不匹配时,从P[next[j]](P[5])开始继续与主串中的t(S[13])比较(也就是把子串后移j next[j]个字符的距离),例如上例中的next[8] = 5,含义就是当P[8]与t(S[13])匹配不成功时,直接从P[5]开始继续与主串中的t(S[13])比较(也就是把子串后移8-5个字符的距离)。

可以发现,next的数值是和主串无关的,只和这个子串本身有关。那么这样,最原始的匹配步骤就可以变成下面图中的样子。也就是每次尽可能多的匹配子串与主串,不要每次都移动一个进行尝试。

 

那么问题来了(不是蓝翔),怎么计算next[j]?

根据前面的例子可以看出,next[j]的值就是P[1 ····· j – 1]中可以找到的最长且相等的前缀与后缀的长度再加1(规定第1个值为0,因为如果第一个字母都不匹配的话就要向后移动1个字符的长度,1 = j – next[j] = 1 – 0),比如前面例子中子串前7个字符中可以找到最长且相等的前缀和后缀就是abca,长度是4,所以next[8]的值就是4 + 1 = 5。按照这个原则,就可以构造出上面的跳转表了。

上面的方法是人眼非常方便的做法,但是如果要写程序则会变得非常复杂。并且当字符串变长或者字的数量变多的时候直接查找会变的非常低效。所以需要考虑程序怎么方便快速的实现,在这种时候通常的思路是充分利用之前的结果。

例如想求next[5],假设next[4]以及以前的next值都已经计算完毕如图。

想求next[5],也就是模式串前4个字符中能找到最长的一对前后缀长度+1, 这时我们已知next[4] = 1也就是说模式串前3个字符找不到一对前后缀相等,我们尝试考虑加上第4个字符P[4]看看能不能找到,既然已经知道前3个中找不到一对相等的前后缀,那么只需要判断要加入的字符P[4]和P[1]是不是相同,如果相同,那么代表加入P[4]后最长前后缀的长度可以增加1,也就是next[5] = next[4] + 1 = 1 + 1 = 2

另外一个例子

       想求next[8],假设next[7]以及之前的值已知如图。

想求next[8],也就是模式串前7个字符中能找到最长的一对前后缀长度+1, 这时我们已知next[7] = 4也就是说模式串前6个字符找到最长且相等的一对前后缀长度为3(图中红色和蓝色部分),我们尝试考虑加上第7个字符P[7]看看能不能延长最长前后缀的长度,既然已经知道前6个中找到最且相等的一对前后缀长度为3,那么只需要判断要加入的字符P[7]与P[4]是不是相同(红框后的红圈字符和篮框后的红圈字符),如果相同,那么代表加入P[7]后最长前后缀的长度可以增加1,也就是可以延长也就是next[8] = next[7] + 1 = 4 + 1 = 5

       分析上面的过程,其实就是尝试尽可能添加进更多的字符进入已经找到的最长且相等的前后缀中,第一个字符的next值一定是0,从第二个开始使用上面的规律,如果最后一个字符的加入可以延长最长前后缀,那么就加进去,如果之前已经没有最长的前后缀,那么直接写1。

       再找一个特殊情况如下(为了说明另一种情况我换了个字符串)。想求next[5]

想求next[5],也就是模式串前4个字符中能找到最长的一对前后缀长度+1, 这时我们已知next[4] = 2也就是说模式串前3个字符找到最长且相等的一对前后缀长度为1(图中红色和蓝色部分),我们尝试考虑加上第4个字符P[4]看看能不能延长最长前后缀的长度,既然已经知道前3个中找到最且相等的一对前后缀长度为1,那么只需要判断要加入的字符P[4]与P[2]是不是相同(红框后的红圈字符和篮框后的红圈字符),如果相同,那么代表加入P[4]后最长前后缀的长度可以增加1,但是显然P[4]与P[2]不等,加入后无法延长目前最长前后缀的长度。这时候却不能结束,因为你明显能看到前4个字符最长的前后缀长度明明是1,所以要继续尝试将P[4]与P[1]比较,如果相同,那么说明最长前后缀长度可以从0增加到1,所以next[5] = next[2] + 1 = 1 + 1 = 2

说了这么多,求next[j]方法推导总结如下:

next[1] = 0 (j = 1)

假设next[j] = k,求next[j + 1]。

next[j] = k (由假设),所以有:

P[1 ···· (k – 1)] = P[( j – (k – 1)) ···· (j – 2) (j – 1)] (由定义)

分两种情况讨论:

P[k]=P[j],则有P[1 ···· (k-1) k] = P[ (j – (k – 1)) (j – (k – 2)) ···· (j -1)  j ],且不存在k’ > k 满足等式

(图中取j = 7的情况)

所以,next[j + 1] = k + 1=next[j] + 1

P[k] P[j]( k < j ),则将求next函数值的问题看成一个模式匹配问题,整个模式串既是主串又是模式串。(图中取j=8的情况)

 

继续设已知next[k] = k’(也就是图中的next[5] = 2)

分两种情况讨论:

P[k’] = P[j],则有P[1 2 ···· (k’ – 1) k’] = P[(j – (k’ – 1)) (j – (k’ – 2)) ···· (j – 1) j ] (1 < k’ < k < j )

那么next[j + 1] = k’ + 1 = next[k] + 1

P[k’] P[j] ( k < j ),则又将求next函数值的问题看成一个模式匹配问题,整个模式串既是主串又是模式串

 

重复以上讨论:直至P[j]和模式串中某个字符匹配成功,即找到某个k’’使next[j + 1]=k’’ + 1=next[k’] + 1

反之不存在任何k*(1 < k* < j),则next[j + 1] = 1。

     

到此为止,搜索的过程就可以优化成上图的样子了。但是还没完,仔细看,第2次匹配,当发现P[4](a)与S[5](b)匹配失败时,下一次匹配按照跳转表应该是P[1]继续和S[5]匹配,但是可以发现P[4]和P[1]是一样的,那么进行两次匹配是没必要的,所以我们就可以再进行一次优化。第9行的情况也同理。

可以简化成下图这样。对应跳转表也在下面。

在第一次的跳转表的基础上,如果发现要跳转的字符和自身相等,那么可以把自身的next值改为要跳转字符的next值,否则保持不变,计算过程中依然充分利用已知的nextval值。举几个例子:

·······

上图中的next[2] = 1,而P[2] ≠ P[1],所以nextval[2] = next[2] = 1。

·······

上图中的next[4] = 1,而P[4] = P[1],所以nextval[4] = nextval[1] = 0。

上图中的next[6] = 3,而P[6] = P[3],所以nextval[6] = nextval[3] = 1。

上图中的next[10] = 2,而P[10] = P[2],所以nextval[10] = nextval[2] = 1。

 

经过这两次优化,可以发现,原本需要匹配16次的事情现在只需要匹配6次即可完成。是不是非常神奇?

 

C基础系列教程11——常见错误总结

该系列文章内容可能来源我本人或者zhrmoe(他的主页:https://zhr.moe)的编写。文章如果有错误欢迎批评指正,谢谢!转载请注明来自本站,另外,本系列教程中的代码建议初学者自己手打一遍,不要直接复制(由于某些奇怪的原因可能会导致你复制的代码出现错误!相信自己的双手吧=-=
在这里总结一些初学者常见错误,建议在考试前看一遍。

C基础系列教程导航

该系列文章内容可能来源我本人或者zhrmoe(他的主页:https://zhr.moe/)的编写。文章如果有错误欢迎批评指正,谢谢!转载请注明来自本站,另外,本系列教程中的代码建议初学者自己手打一遍,不要直接复制(由于某些奇怪的原因可能会导致你复制的代码出现错误!相信自己的双手吧=-=

如果链接不生效,请自行复制地址访问

计算机系列网课
http://study.163.com/curricula/cs.htm

翁恺 —— C语言程序设计
http://mooc.study.163.com/course/1000002011?tid=2001530003#/info

浙江大学基础练习网站
http://pta.patest.cn/pta/test/14/exam/4

C基础系列教程基本内容更新完毕,后期会逐步添加练习题和课后习题=-=
张浩然博客
https://zhr.moe

C基础系列教程1——基础数据类型以及输入输出语句

C基础系列教程1——基础数据类型以及输入输出语句

C基础系列教程2——逻辑表达式和判断语句

C基础系列教程2——逻辑表达式和判断语句

C基础系列教程3——switch和数组基础

C基础系列教程3——switch和数组基础

C基础系列教程4——简单循环

C基础系列教程4——简单循环

C基础系列教程5——多重循环

C基础系列教程5——多重循环

循环练习题2 HDOJ 2010
http://acm.hdu.edu.cn/showproblem.php?pid=2010

C基础系列教程6——函数

C基础系列教程6——函数

C基础系列教程7——指针1

C基础系列教程7——指针1

C基础系列教程8——指针2

C基础系列教程8——指针2

C基础系列教程9——指针3

C基础系列教程9——指针3

C基础系列教程10——结构体

C基础系列教程10——结构体

C基础系列教程11——常见错误总结

C基础系列教程11——常见错误总结

C基础系列教程10——结构体

该系列文章内容可能来源我本人或者zhrmoe(他的主页:https://zhr.moe)的编写。文章如果有错误欢迎批评指正,谢谢!转载请注明来自本站,另外,本系列教程中的代码建议初学者自己手打一遍,不要直接复制(由于某些奇怪的原因可能会导致你复制的代码出现错误!相信自己的双手吧=-=
如果你觉得进度太快,可以等你熟悉了前面的内容后再继续往下看

 

先思考一个问题:
(亲爱的小明出场) 假如一个小组有10个同学,需要记录在程序中,其中几个同学分别叫小明、小红、小绿、小然、小琪、小玮、小霄·········(这些只是随便起的名字,臻的是随便起的233333)
现在需要分别记录TA们的编号、身高、体重什么的,为了方便写代码所以只出现数字,都是int,应该不会有越界情况发生(逃)。请思考如何声明变量。
先想想,再往下看
————————— 我 是 萌 萌 哒 分 割 线 ●▽● —————————
你可能写出这样的代码(这是个错误的示范)

每个人的每个数据(以后可以称之为属性)都声明一个变量。这样当然是可以的(如果你不嫌累)。
顺便一说,变量的命名要简洁明了,不要这么啰嗦=-=并且不要用拼音。

不多嘛,30行就写完了。然后呢,等你写完之后突然有人又告诉你(客户改需求了),需要每个人再增加一个年龄。。你内心一定是这样的
————————— 我 是 萌 萌 哒 分 割 线 ●▽● —————————
现在遇到的问题就是这样,今天介绍一个新的东西,struct。叫做结构体,一个结构体可以包括多个变量, 定义方式如下。
你需要新建一个文件,文件名叫做person.h,这种后缀的文件我们称作头文件,内容是这样的

 

这样就定义了1个结构体类型叫做person,注意不要丢了右大括号后面的那个分号。在主程序中我们需要在#include<stdio.h>的后面加一行 #include “person.h” ,这种自己写的头文件我们用双引号引起来以便于和C自带的头文件混淆。

接下来就可以定义变量了,代码是这样的。

 


第一行声明了一个叫做xiaoming的变量,这是个特殊的变量,因为它是个结构体,里面包含了刚刚定义的三个属性,id、身高、体重。我们使用.(这是一个点儿)来访问结构体变量中的属性给它赋值(当然也可以读取,比如最后一行)。

这样的话定义起来就会方便很多,增加属性的话直接去更改头文件的定义就好了。

————————— 我 是 萌 萌 哒 分 割 线 ●▽● —————————
接下来会出现一个问题,你每次定义的时候都要敲一个struct person,这令人十分讨厌(主要还是懒)。所以呢,我们在把头文件改成这样

我把原来的struct person改成了struct _person,加了一个下划线,是为了下面这样做,typedef的意思是定义一个新的类型,把struct _person 叫做person,也就是说你待会输入person就相当于输入了struct _person,这样就少写了一个单词 (我臻是太懒了=-=)
那么这样做之后你会发现原来的程序报错了,因为我们加了个下划线,所以源程序有两种改法
第一种,跟着加下划线,这种方法显然更麻烦
第二种,直接删掉struct,这也是我们这样做的目的(其实还是为了偷懒)。

更改后代码如下,小红就删了啊=-=


————————— 我 是 萌 萌 哒 分 割 线 ●▽● —————————
今天有时间,就接着往下写吧=-=一次看不完可以下次接着看

在数据结构中,不光要储存数据,还要对应数据的一些操作,这些操作的集合我们叫做操作集,也就是一堆函数。
所以,在数据结构中很少通过定义一个本地的变量来操作,都是通过函数来进行的,那么这里就有一个问题,在函数中对本地变量操作无法直接传参数,需要传地址,也就是指针。

所以需要更改声明为

 

第一行是原来的声明,需要改写成第二行的样子,之所以注释掉是因为太长(虽然更好理解,也就是小明所在内存中的地址),所以依然叫做xiaoming(还是太懒) ,现在你要注意,接下来的 “xiaoming” 的意思是小明所在的地址,不再是之前的xiaoming了。还有一点要注意,指针的声明要初始化为NULL,否则叫做野指针,不知道指向哪里。

我们来写一个函数来进行和之前同样的操作。主程序是这样的,注意多加了一个stdlib.h

其中需要解释的是malloc这一行,意思是分配一块大小刚好能放得下person结构体的内存,这时候电脑并不知道这块内存要放什么,你需要强制类型转换成person指针类型来告诉电脑这里要放的是一个person。其中sizeof求出的是一个person占多大的内存,malloc负责向系统申请一块这么大的内存,然后返回一个内存的地址,但是是不知道类型的地址(也就是void *),最后强制转换成person*
这样的做法就相当于之前的person xiaoming;然后把小明的地址记下来。最后一行把小明的地址传给fun函数处理。

接下来写fun函数,我们接收到的是person的指针,叫做sb(somebody-_-||不要多想),这个sb就是一个指向了person的指针。

我们的做法和之前的整数交换差不多,*sb代表的是间接取出sb指向的内容,也就是小明,之后在用.(这是一个点儿)来访问小明的id、身高、体重。之所以加括号是因为那个“点”的运算优先级要高于间接取值的星号。
但是人总是懒的=-=又要打括号星号还有点太麻烦了→_→所以呢就有这这种写法,和上面完全等价

是的你没看错就是一个箭头,一个减号和一个大于号。
注意这俩符号中间不能加空格
注意这俩符号中间不能加空格
注意这俩符号中间不能加空格
重要的事情说三遍。

————————— 我 是 萌 萌 哒 分 割 线 ●▽● —————————
补充说明:.(这是一个点)运算用在访问一个结构体变量的属性时,那个箭头用于指针指向的结构体属性时候,一个是直接访问一个是间接访问,后续还可能会出现结构体的属性指向结构体,就可能出现先箭头再一个点来访问,比如链表。。。这是小然提出来的补充哦(逃
————————— 我 是 萌 萌 哒 分 割 线 ●▽● —————————
至此,数据结构基础告一段落,如果还有补充的我会继续发=-=
下面是我的github,这里有一些数据结构的源码。。。还在学习ing=-=(请勿抄袭!请勿抄袭!请勿抄袭!)

https://github.com/956237586/DataStructure-C/tree/master/DataStructure-C
————————— 我 是 萌 萌 哒 分 割 线 ●▽● —————————
题外话,关于箭头还有个黑魔法,依然转自知乎 http://www.zhihu.com/question/27417946

C基础系列教程9——指针3

该系列文章内容可能来源我本人或者zhrmoe(他的主页:https://zhr.moe)的编写。文章如果有错误欢迎批评指正,谢谢!转载请注明来自本站,另外,本系列教程中的代码建议初学者自己手打一遍,不要直接复制(由于某些奇怪的原因可能会导致你复制的代码出现错误!相信自己的双手吧=-=
如果你觉得进度太快,可以等你熟悉了前面的内容后再继续往下看

当你在C中需要储存大量的数据的时候,最常见的一种做法就是数组,今天将介绍数组和指针之间的关系,展示数组真实的模样。

本次主要为一维数组,多维数组就是一维数组的推广而已,可以仿照本文方法分析研究。

首先编写测试代码如下

在这段程序中定义了一个整数数组,一个整数指针变量
断点调试观察内存的状态是这样的

这是整个数组,图中每个红框就是一个int,是数组中的一个单元,从下往上看,可以发现分别就是arr[4]一直到a[0]
————————我 是 萌 萌 哒 分 割 线 ●▽●——————


这是指针pa
————————我 是 萌 萌 哒 分 割 线 ●▽●——————
在接下来为了节省地方,我会把四个字节的内容放到一行,省略一些地址的显示,如下图

————————我 是 萌 萌 哒 分 割 线 ●▽●——————

这两张和之前的两张意思是一样的,只是省略了部分地址的显示,便于观察,看的时候从下向上,从右至左
————————我 是 萌 萌 哒 分 割 线 ●▽●——————

观察上图不难发现,取数组的地址的结果就是数组最开始的地址,指针也指向这个位置。继续增加代码如下

运行结果如图

让我们来一行一行的分析
第1行、第2行、第4行,如果我们尝试直接打印数组名字,会发现打印出来的就会和数组第一个元素的结果是一样的,和指针的内容也是一样的,这说明,数组的名字就代表了数组的首元素地址。指针指向数组的时候也会指向这个数组的首元素地址。

当然,打印出来的arr[0]也肯定会和*pa一样,因为pa的值就是第一个元素的地址。这没什么好解释的,自习对照上面的内存截图很容易就能看出来。

接下来我拿第3个元素作为例子,也就是arr[2],剩下的几行你会发现,pa + 2 我们称作指针的偏移,偏移2个单位,由于一个整数占4个字节也就是四个格子,所以也就偏移到了数组中的第3个数字,你可以把2改成其他值观察结果是怎样的,最后的结果你会发现实际上arr[2]的含义可以理解为从数组首地址也就是arr先偏移2再取值,也就是最后一行写的那样。

(题外话,这里还有个黑魔法,转自知乎)


如果你明白了刚才说的数组,那么这个黑魔法的原理你也一定能很快的想明白,这个就留给大家思考吧=-=
温馨提示,你可以编写一些代码验证这个黑魔法,断点调试观察内存,尝试更换等价表达式来验证你的猜想是否正确

下次介绍struct的基本使用

C基础系列教程8——指针2

该系列文章内容可能来源我本人或者zhrmoe(他的主页:https://zhr.moe)的编写。文章如果有错误欢迎批评指正,谢谢!转载请注明来自本站,另外,本系列教程中的代码建议初学者自己手打一遍,不要直接复制(由于某些奇怪的原因可能会导致你复制的代码出现错误!相信自己的双手吧=-=
如果你觉得进度太快,可以等你熟悉了前面的内容后再继续往下看

上次我们说到,指针的一个用途,可以当作参数传递给函数进行使用,废话不说,直接上代码

 

一定要记的上次说的,星号的两种使用,一种是在声明的时候,代表变量类型为指针,第二种是使用的时候根据指针所指的地址间接取值。同理,在参数的声明时候也一样。在上面的代码中,声明了两个参数,类型都是整数指针,也就是将要储存整数变量所在地址的变量(有点绕,配合上次的内存截图理解),接下来的三行是整数交换的操作。注意,t是一个局部的整数变量,pa是个整数指针,只有间接取值的结果才是一个整数。同理,*pb代表的是pb指向位置的内容,这是第三个星号的使用:间接的存储。这三行看似简单,但是如果不能理解指针就会非常容易出错,究竟是不是应该加星号是最容易纠结的地方。接下来的几张图将表示函数执行的过程(图中的地址只是为了便于理解,并不符合真实内存的分配规律,上次的截图才是真实的内存状态)。

假设两个数 a = 1, b = 2,调用语句是这样的swap2(&a, &b); 这样就传入了两个变量的地址。

传入的地址的值会被原封不动复制到pa和pb两个新的指针变量的内容中,如上图

第1行,分配一块内存当作t,并且存入pa所指向的内容,如上图

第2行,改写pa指向地址的值,如上图

第3行,改写pb指向地址的值,如上图

最后, 函数结束,回收pa,pb,t三个变量占用的内存,如上图

这就是通过传入地址来间接交换两个变量,你可以用下面的代码观察结果

为了更好的理解值传递,下面的几张图将表示上一次的交换函数执行过程,也就是上面代码中的swap1,一样假设两个数a = 1, b = 2(图中的地址只是为了便于理解,并不符合真实内存的分配规律,上次的截图才是真实的内存状态)。

—————————我 是 萌 萌 哒 分 割 线 ●▽● ———————————

参数传入后会像上次说的那样原封不动的复制一份到新的参数中,虽然同名(也可以不同名),但是本质上已经不是同一个变量了, 如上图所示
—————————我 是 萌 萌 哒 分 割 线 ●▽● ———————————

—————————我 是 萌 萌 哒 分 割 线 ●▽● ———————————

—————————我 是 萌 萌 哒 分 割 线 ●▽● ———————————

—————————我 是 萌 萌 哒 分 割 线 ●▽● ———————————


最后, 函数结束,回收a,b,t三个变量占用的内存,如上图
—————————我 是 萌 萌 哒 分 割 线 ●▽● ———————————
下次介绍指针与数组的关系