说明
【数据结构与算法之美】专栏学习笔记
技巧一:理解指针或引用的含义
指针或引用都是存储所指对象的内存地址。将某个变量赋值给指针,实际上就是将这个变量的地址赋值给指针。
例如:
p —> next = q
:表示 p 结点中的 next 指针存储了 q 结点的内存地址。p —> next = p —> next —> next
:表示 p 结点的 next 指针存储了 p 结点的下下一个结点的内存地址。
技巧二:警惕指针丢失和内存泄漏
例子:在结点 a 和相邻的结点 b 之间插入结点 x,假设当前指针 p 指向结点 a。
错误写法如下:
p —> next = x; x —> next = p —> next;
如果按照上面的写法,先将 p 的 next 指针指向 x 结点,然后将 x 的结点的 next 指针指向 b 结点。但是这里第一步操作完指针指向结点 x,第二步就是 x 自己指向自己,导致链表断裂,结点 b 往后的所有结点都无法访问到。
正确写法如下:顺序倒一下
x —> next = p —> next; p —> next = x;
插入结点时,要注意操作的顺序,这样才不会丢失指针,导致内存泄漏。另外删除链表结点时,也一定要记得手动释放内存空间。
技巧三:利用哨兵简化实现难度
什么是哨兵?
例子1:空链表中插入第一个结点
if (head == null) { head = new_node; }
例子2:删除链表中的最后一个结点
if (head -> next == null) { head = null; }
针对链表的插入、删除操作,需要对插入第一个结点和删除最后一个结点的情况进行特殊处理。哨兵就是用来处理这些边界问题的,它不直接参与业务逻辑。
引入哨兵结点,不管链表是不是空,head 指针都会一直指向这个哨兵结点,并且一直存在,哨兵结点也不存储数据。
带头链表:指有哨兵结点的链表
不带头链表:指没有哨兵结点的链表
带头链表示意图:
删除最后一个节点的时候:
在没有加哨兵的时候,这个时候用 p -> next = p -> next -> next 没用,就一个节点的话 p -> next 和 p -> next -> next 都是 null,相当于null = null。
如果加上哨兵,哨兵是恒存在于链表中的,删除链表中的最后一个元素(是删除哨兵以外的最后一个,哨兵不参与业务逻辑)所以当哨兵后还跟着一个元素时,也就是有最后一个元素时,站在哨兵的位置依旧可以执行 p -> next = p -> next -> next,进而把最后一个删掉。
利用哨兵简化编程难度技巧的常见的有插入排序、归并排序、动态规划等。
技巧四:重点留意边界条件处理
检查链表代码是否正确的边界条件:
- 如果链表为空时、链表只包含一个结点时、链表只包含两个结点时,代码是否可行?
- 代码逻辑在处理头结点和尾结点的时候,代码是否可行?
技巧五:举例画图,辅助思考
对于稍微复杂的链表操作,可以使用举例法和画图法。
示例:往单链表中插入一个数据
技巧六:多写多练,没有捷径
熟练的写出一些常见的链表操作:
- 单链表反转
- 链表中环的检测
- 两个有序的链表合并
- 删除链表倒数第 n 个结点
- 求链表的中间结点