1.链表的概念
通过多个节点将多个数据元素组成一个链表。
- 节点:数据元素的存储映像, 由数据域和指针域两部分组成。
优点
- 数据元素的个数可以自由扩充
- 插入、删除等操作不必移动数据,只需修改链接指针,修改效率较高
缺点
- 存储密度小
- 存取效率不高,必须采用顺序存取,即存取数据元素时,只能按链表的顺序进行访问(顺藤摸瓜)
2.创建一个单向列表
单向列表即每个节点之间的联系是单向的,仅通过一个指针链接。
创建列表有两种方法:前插法和后插法。
前插法
新的节点链接到上一节点的开头
1 | typedef struct list |
前插法创建链表的步骤为(以创建两个节点为例):
1 | void setlist(test* linkhead, int data) |
也就是说,创建第一个节点时,该节点的指针域先指向NULL后,linkhead指针域再指向该节点首地址。
当刚创建第二个节点时,情况如下:
当执行tmp->last=linkhead->last;时,由于linkhead的指针域此时指向tmp(1)(这里的1指第一块节点的内存空间)的首地址,所以tmp(2)的指针域会通过linkhead直接也指向tmp(1)的首地址:
这样之后,两个节点就通过指针链接在了一起,且tmp(2)在tmp(1)之前。
由此可以看出,前插法的原理就是以linkhead为媒介来链接节点,linkhead本身是不存储数据的
最后,执行linkhead->last=tmp;,将linkhead指针域指向第二个节点首地址,以供第三个节点的指针域找到第二个节点。
以此类推,假如现在创建了4个节点,那情况就应该像这样:
在例程中,由于前插法得到的链表顺序为最后输入的数据开始,所以最后打印的结果为3 2 1
尾插法
相对于前插法,尾插法使用起来逻辑会更加清晰,因为其链接顺序是在每一个节点后,所以尾插法要更容易理解一些。
在结构体复习 | 小董的BLOG (gitee.io)中讲到的列表就是使用的尾插法,核心代码如下:
1 | typedef struct list |
如果说前插法是通过linkhead为媒介进行链接,那尾插法就是通过linkend为媒介链接各节点。
3.双向链表的创建
同样分为使用前插法和后插法,但是尾插法会简单很多,所以先讲一下尾插法
尾插法创建双向链表
1 | typedef struct list |
尾插法由于其创建链表的方式为顺序创建,因此只需在原有的基础上添加存放上一节点的指针就行了
使用双向链表会增加内存,但是就可以通过任意节点找到其他节点了。
使用前插法创建双向链表
使用前插法的话会稍微麻烦一些,而且理解起来也会有点绕,感觉用尾插法好一些。
(这是我自己写的方法,感觉有点绕)
1 | typedef struct list |
在写这个双向链表时我遇见了个问题:对于linkend变量,我在主函数中定义后,像通过像linkhead一样的方式对linkend进行改变,但是始终无法实现:
1 | void InsertList(test *head,test *end;int val) |
如果在主函数中没有对linkend分配内存,linkend将成为一个野指针,无法指向字符值常量。
但是如果分配了内存,由于linkhead是通过last成员来寻找链表,但是linkend却无法像linkhead一样正常实现功能,在经过整整一天的查找资料&调试后,终于发现了原因:
当一个函数的形参是结构体指针时,这个形参本身的地址(不是所指向的地址)和实参的地址(也不是所指向的地址)是不同的。用例程举例:
- &linkend≠&end,因此,在函数中改变形参end所指向的内容是完全不会影响实参linkend的,因为此时这两位可以理解成两个变量
那这时大家肯定会有个疑惑:那linkhead为什么就可以实现想要链表功能呢?
我们仔细看InsertList函数中的内容,你肯定会发现,函数中没有出现直接操作linkhead的操作,只有操作linkhead成员的操作,这里就引出了一个折磨了我半天的知识点:
对于一个结构体指针形参,形参名的地址和实参名的地址是不能划等号的,但是形参成员的地址和实参成员的地址是完全相同的,用例程举个例子
1
2&linkend≠&end
linkend->next=end->next (=是等于的意思,不是赋值的意思)
其实这里的解释有点类似于在结构体复习 | 小董的BLOG (gitee.io)最后我对一重指针的解释:
- 假如有两个并列的指针指向同一个变量,你不能通过改变其中一个指针的指向来影响另一个指针的指向
那么这时又引出了一个问题,形参end和实参linkend指向哪里?
先说一下结论:linkend和end都指向linkend的第一个成员num的地址。
也就是说,形参end和实参linkend作为两个不同的指针变量,他们指向了同一个地址(也许可以通过这一特性来写一个链表?但是我不想写了😋)
其实这个知识点符合我们对形参和指针变量的认知,所以这里比较重要的点其实是:
- 不同于数组,结构体指针名地址≠第一个成员的地址,但指针名指向第一个成员的地址,可以理解为,结构体指针名指向一块内存,该内存是由若干个结构体成员组成的,这些成员的类型在定义结构体时确定。
这也加深了我对end=(LNode*)malloc(sizeof (LNode));的理解,其实就是一个指针指向了一块内存,但是可以通过结构体的方式访问这块内存中的成员
总结
- 当一个函数的参数是结构体指针时,操作这个函数的形参名不会影响实参,同时这个形参名的作用域只限于函数体;但是操作这个形参的任何成员等同于直接操作实参的对应成员,因为它们本身的地址是相同的
- 结构体指针名指向第一个成员的地址
所以,结构体指针并不一定要指向一个明确的结构体,当他指向一块内存时,可以通过直接赋值结构体指针成员的方式操作这块内存(这块内存其实就是一个隐式的,没有具体名字的结构体)
一些感想:好久没有遇到过这么折磨的问题了,一开始完全没有头绪,网络上对于这方面的讲解也是几乎没有,还好找到了这篇博客启发了我C语言理解指针作为形参作用(博客中用的C++,但是原理是完全相同的)