什么是链表
在正式开始链表的认识前,我们先来回顾一下数组。数组是一种线性表,它用一串连续的内存空间来储存同一种类型的数据,而链表与数组唯一的区别就是它可以不用连续的内存空间来储存数据,这是如何实现的呢?没错,就是通过指针(有的语言,比如java,python,没有指针,但其实它们内部都有类似结构),指针储存的是它指向的变量的地址,通过该地址我们就能找到变量对应的值。这里简单举个例子,比如你要去朋友家里玩,那你自然要问朋友家里的地址,然后找到这个地址你就能找到朋友本人了,指针储存的地址可以理解为朋友家的地址。我个人认为指针将不同地址储存的数据连接起来形成的一种链式结构的线性表就称之为链表了。
下面是对比,数组规规矩矩,链表见缝插针!
链表的结构
这里给大家画的是比较常见的一些链表结构
单链表
我们以单链表为例来具体梳理一下链表的结构,首先单链表有头结点(head)和尾结点(tail),顾名思义,排头的就是头结点,最后一个就是尾结点;头结点指向第一个结点,这样我们就可以一个接一个的向后访问所有的结点,尾结点指向NULL,NULL在C中代表是空,也就是链表的结束。其次,单链表是由一个一个独立的单元组成的,每一个单元都包括了该数据元素(data)和储存下一个单元位置(也就是后继)的指针(next指针)。
双链表
双链表比单链表多了一个储存前一个单元位置(也就是前驱)的指针(prev指针),这样双链表就能实现双向访问的功能了,不过,由于多储存了一个前驱指针,所以双链表占用的空间会比单链表大,插入或删除的开销也会增加(其实没什么差别,只是要多处理指针)。不过,在使用双链表删除元素的时候有了前驱指针确实会方便许多。
循环链表
与单链表相比就是的差别就是尾指针直接指向第一个单元,这样从尾部就可以直接回到头部了。
其他
我们当然也可以用上面的几种结构构造出一些新的结构,比如书中提到的双向循环链表。
增删改查
这里我们要注意不要造成指针的丢失,解决方法是违背直觉,先后再前!
增加(插入)一个单元
一般来说我们的直觉肯定是先把前面的单元链接上,再链接后面的单元,但在链表的处理中,我们得反过来。我们要先用指针记录前面单元的地址(oldNode->next,如果对于oldNode->next看得不习惯可以先进行赋值:tempNode = oldNode->next)再将其赋值给要插入的Node(newNode->next = oldNode->next
),最后才是将oldNode指向newNode(oldNode->next = newNode
)。
- 基础版——增加一个暂时的tempNode来理解
tempNode = oldNode->next; //将原本oldNode指向的单元的地址储存到tempNode中
oldNode->next = newNode; //将oldNode指向newNode
newNode->next = tempNode; //将newNode指向tempNode
- 高级版——违背直觉版
上面提到的是一般情况下的增加元素的操作,如果这时候我们打算在表头插入或在末尾插入,那就不太一样啦。在链表末尾插入十分简单,只要将原本的指针的next指针指向新的单元即可;在表头插入时,我们同样要遵循刚刚的原则,先记住原本的head,然后创建一个新head,再将新head指向原本的head。
删除
和插入类似,我们如果按照直觉先删掉中间的Node,再将前面的Node指向后面的Node,那么在删掉中间Node之后,我们就丢失了后面Node的地址。所以我们得先将前面的Node(Node1)指向后面的Node(Node2->next),然后再delete释放Node2占据的那一块内存空间,避免内存泄漏。
- 基础版——增加一个暂时的tempNode来理解
tempNode = Node2->next; // 将要删除的Node2的下一个单元赋给tempNode
delete Node2; //删除Node2
Node1->next = tempNode; //将Node1指向tempNode
- 高级版
上面提到的是一般情况下的删除元素的操作,如果这时候我们打算删除表头或删除末尾,那就不太一样啦。在删除末尾时,需要将原本的末尾用tempNode保存,然后将倒数第二个单元的next指针指向NULL,最后delete tempNode;在删除表头时,我们先将原本的head的下一个位置保存为tempNode,然后delete head,最后将head指向tempNode。
改和查
改和查其实是一样的,要修改数据首先得找到数据所在位置,链表在这方面就不如数组了,我们没办法直接通过下标访问某个元素(只需要O(1)),只能一个一个遍历(O(n))来找到我们想要的数据,找到数据后如果是查找,则返回数据;如果是修改,则直接将新数据赋值给老数据即可。
其他tricks
哑结点(dummy node)
我们提到了在增删两步骤的时候要注意头的删除部分,因为在删除前我们要判断是否是头,头和其他结点操作不一样,但如果我们引入了**表头(header)或哑结点(dummy node)**就可以解决这个问题。dummy这个字从字面意思上表达的就是一种虚设的意思,哑结点内只存next指针,不存data,真正的头是从哑结点的next指针指向的单元开始算起的,这样我们就永远不会在链表的头插入,而是在哑结点后插入,从而避免了判断是否是头的步骤,也就避免了犯错的机会。
参考
《数据结构与算法分析——C语言》