数据结构与算法之美第五天

140 阅读5分钟

链表:如何轻松写出正确的链表代码?

究竟怎样才能比较轻松的写出正确的链表代码?

写链表代码技巧

技巧一:理解指针或引用的含义

要想写对链表代码,首先理解指针,在C中是有指针这个概念,但在java,python中没有,可以理解为“引用”,他们的意思都是执行对象的内存地址。

对于指针的理解:

将某个变量复制给指针,实际上就是将这个变量的地址赋给指针,或者反过来说,指针中存储了这个变量的内存地址,指向了这个变量,通过指针就能找到这个变量。

在编写链表代码的时候,我们经常会有这样的代码p->next=q,这个代表p结点next指针存储了q结点的内存地址。还有一个更复杂的,也就是我们写链表代码经常会遇到的:p->next=p->next->next.这个代码表示p结点的next指针存储了p结点的下下一个结点的内存地址。

技巧二:警惕指针的丢失和内存泄漏

样例

单链表插入操作 image.png

分析:我们希望在结点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指针都会指向第一个结点(哨兵结点)我们将这种链表叫做带头链表,相反没有有哨兵结点的叫做不带头链表

image.png

哨兵结点不存储数据,因为哨兵结点一直存在,所以插入第一个结点和插入其他结点,删除最后一个结点和删除其他结点,都可以统一为相同的代码实现逻辑了
思考: 这里说的是删除最后一个结点,在没有哨兵的时候,哪怕只有一个结点也得给他删除掉,这个时候用
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.求链表的中间结点