前言
在之前文章中,我们学习了单向不带头非循环链表(简称单链表)。我们知道在实现单链表的插入、删除需要用到二级指针。原因是:
函数传参,如果我们在实参部分直接传 phead,而形参部分拿 SLTNode* 接收的话,这就属于 值传递,值传递相当于 形参是实参的一份临时拷贝,形参的改变并不会影响实参的值。
以单链表尾插为例:
void SLTPushBack(SLTNode* phead, SLTDataType x)
{
assert(phead);
//*phead 就是指向第一个节点的指针
//空链表和非空链表
SLTNode* newnode = SLTBuyNode(x);
if (phead == NULL)
{
phead = newnode;
}
else
{
//找尾
SLTNode* ptail = phead;
while (ptail->next)
{
ptail = ptail->next;
}
//ptail指向的就是尾结点
ptail->next = newnode;
}
}
int main()
{
SLTNode* plist = NULL;
SLTPushBack(plist, 1);
return 0;
}
由此可以看出尾插只是改变了形参,并没有影响实参。所以如果想要修改实参的值,就需要进行 传址 操作,在实参部分直接传 phead 的地址,那么形参部分就要拿 SLTNode** 二级指针接收。
// 因为传过来的是phead的地址,phead是一级指针的地址,所以要拿二级指针接收
void SListPushBack(SLTNode** pphead, SLTDateType x) {
assert(pphead); // pphead不可以为空指针.
// 先增加一个节点
SLTNode* newnode = BuyListNode(x);
// 如果头节点是空,那么直接把newnode的地址赋值给头节点
if (*pphead == NULL) {
// *pphead:对pphead解引用,其实访问的就是plist;
*pphead = newnode;
}
else {
// 找到尾节点
SLTNode* tail = *pphead;
while (tail->next != NULL) {
tail = tail->next;
}
tail->next = newnode;
}
}
这样通过*pphead 和实参产生了联系,*ppead的修改影响了实参。其实还可以通过设置哨兵位来影响实参。
哨兵位(头结点)
哨兵位(DummyNode)也是一个节点,但是该节点不存储任何的有效数据。哨兵位的创建方便我我们进行头插数据。如果有了哨兵位的话,头插的时候,我们就不再需要改变头节点了。
注意:哨兵位是一个附加的链表结点,该 结点 作为第一个节点,它的数据域不存储任何东西,只是为了操作的方便而引入的。
也就是说,如果一个链表有哨兵节点的话,那么链表表的第一个元素应该是链表的第二个节点。
哨兵位实现单链表
以尾插为例:
这里为啥没传二级指针也影响了实参呢?
虽然phead 是 plist 的临时拷贝,这俩本质是不同的指针变量,但是plist作为哨兵位,它的节点不能被删除,节点的地址也不能发生改变。
对于带头节点的链表来说,因为在初始化plist时,已经把头结点确定了,而且不会修改他的地址,在复制了带头节点的plist进行函数操作时,复制的头节点和在plist中的头节点指向下一个结点本质是一样的,也就是说带头结点的链表和其形参复制指向的下一个元素都是同一个元素(两个指针指向同一个next)。
哨兵位实现双向链表
以尾插为例:
//双向链表尾插
void ListPushBack(LTNode* phead, LTDataType x) {
assert(phead);
LTNode* tail = phead->prev; //尾节点就是头节点的前驱指针
LTNode* newnode = BuyLTNode(x); // 申请一个结点,数据域赋值为x
/*建立新结点与头结点之间的双向关系*/
newnode->next = phead;
phead->prev = newnode;
//建立新结点与tail结点之间的双向关系
tail->next = newnode;
newnode->prev = tail;
}
总结
总的来说:使用头节点(哨兵位头节点),不需要改变其大小时,可以直接使用一级指针,如果不带头结点(或者需要改变第一个结点的值),则需要二级指针。