重点是不用像其他文章里那样,用一个普通节点成员变量当头节点,节省一点空间占用,反正我觉得有点骚。就不详细交代技术背景了,简而言之,就是链表中第一个节点前没有节点了,只有一个指向它的指针,所以不能像其他节点一样对第一个节点进行删除操作,代码中必须判断这个特例,详细的参考链表头指针和虚拟头节点小结。可以用一个普通节点当作头节点来指向第一个节点,从而让第一个节点也有前一个节点,统一操作,如下图:
但是头节点里会有一个没用的数据域,浪费空间,所以才想到用二级指针。
原理
首先是单向链表的节点结构体:
struct LinkNode {
LinkNode* next;
int data;
};
重点就是让next
指针作为结构体第一个成员。然后考虑一个指向节点的指针:
LinkNode *head;
// 由于next 指针在结构体头部,所以:
head == &(head->next) // => true
//即,指向节点的指针和指向该节点next 指针的指针相同,是指向同一个地址,或者说
LinkNode **ptr_to_next = &(head->next)
head == ptr_to_next // => true
指向节点的指针可以视为指向next 指针的指针,也就是指向节点的二极指针。那么反过来,指向节点的二级指针就可以当作一级指针来用:
// 创建一个节点,当作链表中的第一个节点
LinkNode node;
// 指向该节点的头指针是一级指针
LinkNode *head = &node;
// 指向头指针的指针是二级指针
LinkNode **ptr_to_head = &head;
// 如果把二级指针强制转换成一级指针
LinkNode *psudo_node_ptr = (LinkNode*)(ptr_to_head);
// 就可以对其使用成员操作符,得到指向第一个节点的指针
psudo_node_ptr->next == &node
// 实际等效于对二级指针解引用,结果就是头指针,或者指向第一节点的指针
psudo_node_ptr->next == *psudo_node_ptr->next == *ptr_to_head == head
可能还是画个图更好理解。首先是之前的虚拟头节点:
用了一个没用的普通节点,其中的next
指针指向第一个节点。如果把next
指针放在结构体头部,再有一个指向头节点的指针:
此时可以用ptr_to_head
访问第一节点,方法是先访问头节点所在的内存,再用头节点里的next
指针访问第一节点。既然头节点的用处只是那个存储了第一节点地址的next
指针,那么如果只要next
指针,不要头节点的其他部分:
此时头节点就变成了指向第一节点的指针,是个一级指针,相应的,ptr_to_head
就变成指向节点的二级指针了。如果把ptr_to_head
强制转换成节点的一级指针:
那么编译器就会当它确实指向一个节点,即原来的头节点。头节点其他部分没了没关系,只要next
指针还在,就可以像头节点还在时一样,通过next
访问第一节点。
我觉得这样表达应该足够清楚了。顺便简单写一下链表类的实现:
class LinkedList {
private:
LinkNode* head; // 把头节点退化成头指针
public:
LinkNode* ptr_to_head() {
return (LinkNode*)(&head); // 把二级指针强制转换成一级指针,转换结果只能使用->next 访问next 成员
// 访问其他成员会BUG,因为不存在那些东西
}
// 传入要删除的目标节点和前一个节点
// 如果要删除第一节点,前一个节点就是ptr_to_head()
void remove_node(LinkNode *target, LinkNode *prev) {
prev->next = target->next;
// 不用为第一节点特殊处理,若prev 是ptr_to_head(),
// prev->next 就等效于 *ptr_to_head,解引用得到head,对head 赋值。
}
};
对比用一个普通节点当头节点的情况:
class LinkedList {
private:
LinkNode head; // 用完整的普通节点作为头节点
public:
LinkNode* ptr_to_head() {
return &head; // 这次是真的指向节点的一级指针,可以访问其他成员,但是没意义
}
// 传入要删除的目标节点和前一个节点
// 如果要删除第一节点,前一个节点就是ptr_to_head()
void remove_node(LinkNode *target, LinkNode *prev) {
prev->next = target->next;
// 使用上和二级指针没区别
}
};
可见,两种实现在使用上没区别,只是要牢记,二级指针的方案中,ptr_to_head
并不指向一个完整的节点,不能访问节点中next
以外的成员。如果节点是动态在堆上创建的,不能用ptr_to_head
删除内存。