每天学一点系列~还在困惑数据结构(尤其是链表)里指针的看这里!!!

Posted 白龙码~

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了每天学一点系列~还在困惑数据结构(尤其是链表)里指针的看这里!!!相关的知识,希望对你有一定的参考价值。

Part I、说在前面儿

正在学习数据结构的同学,尤其是正在学习链表的同学往往会有这样的疑问:为什么有的地方传一级指针,有的地方传二级指针?为什么带头跟不带头差别那么大…对于C语言的基础,尤其是指针的基础不是那么好的同学,数据结构简直就是劝退的拦路虎。

说到这里不知道你的DNA是不是动了一下呢?头又晕了?没关系,今天我们就来克服指针恐惧症!

Part II、指针它是个啥?

我们想理清指针,就先从它的本质说起。别划过,基础不牢,地动山摇哈。

1、地址

电脑的内存可以按照字节划分,为了区分这些字节,我们用十六进制数字给它们进行编号,这些编号就是所谓的地址。
关键字提炼:地址对应内存上的一个字节

2、指针的指向作用

指针,就是个箭头。生活中我们见过路牌,见过指南针,它们都差不多,都是引路者。路牌和指南针都能带我们回家,而指针的作用,也是告诉我们地址。

试问:路牌没了,路还在吗?
答案是不是很明显?当然存在!
所以这里就要明白第一个点:指针只能告诉我们地址,指引我们找到一块内存,但是当这个指针被我们销毁,或者说,让它指向别的地方了,这块内存上的东西是不是还在?是!

2、地址与数据


我们知道,指针里存储的是一块地址,那这块地址上存储的内容是啥呢?
举个例子,假设我们有一个int类型的变量a,里面存储着数据1,十六进制表示就是0x00000001。那么他在内存中的存储形式就是这样的:

a占四个字节,而一个变量只对应了一个指针,这个指针保存的是第一个字节。
可以看到:指针p保存的是地址,而地址对应字节上的内容。

3、解引用

一个东西的存在让指针有了新的能力——解引用符号*
如果说指针是路牌,那解引用就是一辆车。有了指向,有了车,我们就可以直达那块“地址”!
既然到了这块地址,那么我们能做的就多了:利用这块地址上的东西,或者改变这块地址上的东西。
提炼:指针存储的是地址,对指针解引用其实就是对地址解引用。地址对应内存,地址上的东西也就是内存上存储的数据。解引用的操作就是赋予我们访问和改变这些数据的能力!

4、二级指针

认识了解引用,我们就能学习下一个东西了:二级指针。二级指针又是个啥呢?
首先我们需要清楚一点:指针是不是数据?
指针当然是数据!这个数据就是地址。而地址的本质其实是十六进制的数据。这些数据存放在一种叫指针的变量当中。
既然要存储数据,那么指针是不是也占内存?既然是内存,那么它是不是也有一个地址?这个地址就可以存放在一个二级指针当中。

如果我们对二级指针解引用,就可以获得一级指针上存储的内容(也是一个地址),然后再解引用,就能获得一级指针存储的地址上存储的数据。(有点绕哦,对应这上面的图理一下)
ok,继续!

5、传址调用

这个很重要!!!这个很重要!!!这个很重要!!!
为什么有传址调用这个东西?因为形参与实参之间几乎没有任何联系。实参对应一个地址,形参对应另外一个地址。当我们改变形参,只是把它的地址上存的数据改了,但是实参对应的地址上的东西还是没变。

那如果我们传的是实参的地址呢?此时形参就是一级指针,这个指针存储的是实参的地址,对它进行解引用,就能访问或修改实参的地址上存储的数据了!
典型的例子就是交换两个数的Swap函数。

void Swap(int* x , int*y)
{
	int tmp = *x;
	*x = *y;
	*y = tmp;
}

认识了这些,就让我们直接进入数据结构的正题。

Part III、数据结构里的指针

1、不带头单链表

链表的节点的定义一上来就会给我们一个下马威:

typedef struct Node
{
	int data;
	struct Node* next;
}Node;

这里有一个自引用next,它指向的也是一个Node类型的指针。
意思就是说:

一个节点存一个数据,然后又存了一个指针,这个指针的内容是下一个节点的地址,换句话说,这个指针指向下一个节点,因此链表才能真正意义上“链接”起来。


我们定义了一个pList指针专门指向链表的第一个节点,也就是头(head)。因为它指向一个节点,所以它是一个Node类型的指针,即Node*。

由于一开始链表没有元素,自然也没有头结点,所以我们令pList = NULL,让它指向空。
紧接着我们就会写头插头删和尾插尾删函数,于是,诡异的二级指针就来了。

我们不妨看一个头插函数的原型:

void SListPushFront(Node** ppList, int x);

而我们调用它时就要这样:

void SListPushFront(&pList,0);

为什么要取地址呢?换句话说,为什么要传址呢?

原因很简单:因为我们的操作会改变这里的pList。(注意改变二字)

头插意味着链表的头不再是当前pList指向的那个节点,而是另一个新插入的newnode。我们让newnode的next指向当前的pList,然后再让pList指向newnode。
但是,如果传的是pList而不是&pList,那么由于传值调用,形参的改变不影响实参——尽管你让形参指向了newnode,但是pList存储的还是之前的那个节点的地址,然后出现这样的窘境:

pList指向链表的第二个结点而非第一个。
以后我们遍历链表都是从pList指向的位置开始的,但是pList由于是传值,它永远都指向同一块地方,不会改变。因此我们能遍历到的,也永远只有从当前位置开始,到NULL结束。

那么为什么尾插尾删这些函数也要传二级指针呢?设想:如果链表为空,尾插的那个元素就是新的头结点,是不是就会改变pList?如果链表只有一个元素,此时它就是链表的头,那尾删是不是也会改变pList?

总而言之,对于不带头的链表来说,可能改变链表的头的函数,都要用二级指针。只有传二级指针,也就是pList的地址,才能在函数的内部改变pList,让pList指向新的头!

2、带头链表

同学们好不容易弄清链表需要传二级指针,突然又遇到了一种不用传二级指针的链表,啥情况??不让人活了?别慌,让我们一起看看。

带头的链表与不带头的链表的区别就是:不带头的链表用一个pList指针来维护,而带头的链表用一个哨兵位来维护。什么是哨兵位?其实哨兵位也是一个结点,但是它并不存储任何的有效数据。

与不带头的单链表相比,带哨兵位的好处多多,这个在各位刷到链表相关的算法题之后就能察觉到,它能带来很多代码上的优化,可以少考虑很多边界情况。
不过当前, 它的最大的优点就是:不用传二级指针。为啥?

ListNode* list = ListCreate();

我们先创建一个哨兵位list。

可以看到,哨兵位是头结点前的一个节点,头结点的地址就是哨兵位的next指针存储的内容。
当我们进行传参时,直接把list这个指针传给形参,形参作为实参的临时拷贝,里面存储的内容就跟list指针完全一样,也就是说:
形参里也有一个指针,这个指针存储的内容跟哨兵位的next指针存储的内容一样,都是头结点的地址!有了这个地址,我们就可以修改这个地址上的内容,也就是说:修改头结点。

看到这里,我们应该明白:1、形参和实参的next指针肯定是两个不同的变量,但是它们存储的内容是相同的,都是头结点的地址。2、只要有地址,我们就能修改地址上的内容,无所谓存储这个地址的指针是谁。

刚刚不带哨兵位的情况下,pList需要传二级指针,因为它需要始终保持指向头指针,而这里的list永远指向的是哨兵位,head的改变只会影响list的next。当我们直接传list时,形参部分接收的就是list的内容(包括那个指向头结点的指针),通过它我们就可以直接访问list里的next,从而改变它。

Part IV、总结一下(都是精华啊)

想玩转指针,最重要的是什么?是要知道:
1、指针存储的是地址
2、地址对应内存
3、只要我们能得到这个地址,就能通过解引用访问或修改对应内存上的数据
4、如果传址,形参接收实参的地址,对形参解引用得到的就是实参这个实体。
5、如果是传值,形参会复刻实参的内容,如果内容中存在指针,那么我们同样可以通过这个指针修改对应内存上的数据。
6、如果我们想通过形参达到修改实参的目的,必须要传址!

最重要的一点:对于实参是指针的情况,如果我们仅仅想操作它存储的地址上的内容,那么传值就足够了。如果还想额外地改变实参,让实参存储别的地址,那么就需要传地址了!

不知道读者听懂了嘛,有任何问题可以在评论区反馈给我哦
关注笔者,一起学习,一起进步!

以上是关于每天学一点系列~还在困惑数据结构(尤其是链表)里指针的看这里!!!的主要内容,如果未能解决你的问题,请参考以下文章

每天学一点系列~诡异的死循环

每天学一点系列~“Hello World“的诞生

每天学一点系列~字符串左/右旋的本质,你真的认清了嘛?

每天学一点系列~看得见摸不着的“隐式类型转换”

每天学一点系列~这些内存函数你知道么?还记得么[doge]

Python每天学一点之argparse