链表:如何轻松写出正确的链表代码?
究竟怎样才能比较轻松的写出正确的链表代码?
写链表代码技巧
技巧一:理解指针或引用的含义
要想写对链表代码,首先理解指针,在C中是有指针这个概念,但在java,python中没有,可以理解为“引用”,他们的意思都是执行对象的内存地址。
对于指针的理解:
将某个变量复制给指针,实际上就是将这个变量的地址赋给指针,或者反过来说,指针中存储了这个变量的内存地址,指向了这个变量,通过指针就能找到这个变量。
在编写链表代码的时候,我们经常会有这样的代码p->next=q,这个代表p结点next指针存储了q结点的内存地址。还有一个更复杂的,也就是我们写链表代码经常会遇到的:p->next=p->next->next.这个代码表示p结点的next指针存储了p结点的下下一个结点的内存地址。
技巧二:警惕指针的丢失和内存泄漏
样例
单链表插入操作
分析:我们希望在结点a和结点b之间插入一个结点x,假设当前指针p指向结点a,那么将代码实现成
p->next=x; //将p的next指针指向x的结点;
x->next=p->next;//将x的next指针指向b的结点
p->next指针在完成第一步操作之后,就已经不在指向结点b,而是指向结点x。第二行代码将x赋值给x->next,自己指向自己。因此整个链表也就断成了两半,从结点b往后的所有结点都无法访问到了,所以当我们插入结点时,一定要注意操作顺序,要想讲x->next指向结点b,再去讲a的next指针指向结点x,这样才不会丢失指针,导致内存泄漏,所以将以上代码颠倒一下就可以了
同理,删除链表结点时,也一定要记得手动释放内存空间,否则也会出现内存泄漏的问题。
技巧三:利用哨兵简化实现难度
首先,我们在单链表的操作中进行插入和删除,将p结点后面插入一个新的结点实现代码
x->next=p->next;
p->next=x;
当我们要给一个空链表插入一个结点时,就需要先判断一下,第一个结点和其他结点插入的逻辑就不一样了
if (head==null){
head=new_node;
}
我们在处理单链表的删除操作事,如果要删除结点p的后继结点,我们只需要
p->next=p->next->next;
但是我们在删除最后一个结点,前面删除的代码就不能使用,更插入头结点一样需要进行判断
if(head->next==null){
head=null;
}
思考:我们在对单链表进行插入或者删除操作的时候,都需要对头结点和尾结点进行特殊处理,这样代码看起来会很繁琐,而且也会容易出现考虑不全而出错,那么如何解决?
所以“哨兵”就登场了,哨兵表面意思就是守护边界,解决国家边界问题,同理,链表中提出的哨兵也是解决边界问题,不直接参与业务逻辑
带头链表
在任何情况下,我们不管链表是否是空链表,将head指针都会指向第一个结点(哨兵结点)我们将这种链表叫做带头链表,相反没有有哨兵结点的叫做不带头链表
哨兵结点不存储数据,因为哨兵结点一直存在,所以插入第一个结点和插入其他结点,删除最后一个结点和删除其他结点,都可以统一为相同的代码实现逻辑了
思考:
这里说的是删除最后一个结点,在没有哨兵的时候,哪怕只有一个结点也得给他删除掉,这个时候用
p->next=p->next->next没用,就一个结点的话p->next和p->next->next都是null,相当于null = null。但是加上哨兵就不同了。哨兵是永恒存在于链表中的,插入时new_node->next=p->next(除了哨兵结点的第一个结点),p->next=new_node->next(哨兵结点后继指针指向了真实的第一个结点),删除链表中的最后一个元素,所以当哨兵后还跟着一个元素时,也就是最后一个元素,哨兵这里依旧可以执行p->next=p->next->next进而把最后一个干掉
重点留意边界条件处理
检查链表代码是否正确的边界条件有: 1.如果链表为空时,代码是否能正常工作? 2.如果链表只包含一个结点时,代码是否能正常工作? 3.如果链表只包含两个结点时,代码是都能正常工作?
常见的链表操作
1.单链表反转 2.链表中环的检测 3.两个有序的链表合并 4.删除链表倒数第n个结点 5.求链表的中间结点