二级指针骚操作实现链表虚拟头节点

文章讨论了如何通过使用二级指针作为头节点来优化链表的内存占用,避免使用额外的数据域。这种方法使得第一个节点也能像其他节点一样进行统一的操作,同时在删除第一个节点时无需特殊处理。虽然需要记住ptr_to_head不指向完整节点,但实现上与传统方法无明显区别,只是在内存释放时需要注意。
摘要由CSDN通过智能技术生成

重点是不用像其他文章里那样,用一个普通节点成员变量当头节点,节省一点空间占用,反正我觉得有点骚。就不详细交代技术背景了,简而言之,就是链表中第一个节点前没有节点了,只有一个指向它的指针,所以不能像其他节点一样对第一个节点进行删除操作,代码中必须判断这个特例,详细的参考链表头指针和虚拟头节点小结。可以用一个普通节点当作头节点来指向第一个节点,从而让第一个节点也有前一个节点,统一操作,如下图:

在这里插入图片描述

但是头节点里会有一个没用的数据域,浪费空间,所以才想到用二级指针。

原理

首先是单向链表的节点结构体:

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 删除内存。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值