前言
我们都知道链表相关的算法题是笔试,面试的高频题,往往链表题的实现代码通常只有几行,但很多人被链表的指针指来指去,极容易绕晕。比如链表反转
的一种实现:
ListNode *Reverse(ListNode *pHead) {
ListNode *pre = NULL;
ListNode *cur = pHead;
while (cur) {
ListNode *tmp = cur->next;
cur->next = pre;
pre = cur;
cur = tmp;
}
return pre;
}
看到类似cur->next=pre;pre=cur;cur=tmp;
这三行到底在整个链表内部是怎么转移的搞不清楚,看貌似看懂了,但自己写又写不出来。
根本原因
原因是什么?核心还是思考方法不对!我们理解链表题的逻辑时,通常,在我们脑海中只存在一条链表,每个指针只是在链表中的某一个位置,每个指针来回指向错综复杂,最终自己把自己搞晕,比如,理解上面链表反转的代码,脑中的思考过程是这样的:
// 初始链表:[1]->[2]->[3]->[4]
ListNode *Reverse(ListNode *pHead) {
ListNode *pre = NULL;
ListNode *cur = pHead;
//一开始pre和cur的指向位置:[pre null] [cur 1] ->[2]->[3]->[4]
while (cur) {
ListNode *tmp = cur->next;
cur->next = pre;
//执行后: [cur 1]->[pre null] [tmp 2]->[3]->[4]
pre = cur;
//执行后:[pre 1]->[null] [tmp 2]->[3]->[4]
cur = tmp;
//执行后: [cur 2]->[3]->[4]
}
return pre;
}
执行一次循环后,cur指向了下一个节点,但是pre指向哪了?pre怎么和cur关联的?整个反转后的链表是保持在哪个指针上?等等一系列疑问回荡在脑海中,挥之不去,看着好像很有道理,但是从头自己写又无从下手!
方法论
可以看到,整个思考过程逻辑很乱,我提出一种全新的方法叫:以指针为核心,而非链表本身。什么意思,就是关注每个辅助指针的变动关系,每个辅助指针为各自独立事件去关注指针本身的变化。看着有点拗口,举两个实例。
举个例子(链表反转):
链表反转这道题,一共定义了pre和cur两个辅助指针,那么以pre和cur指针的变动关系来去看逻辑,这是就没有链表本身没有关系了,只要时刻关注pre和cur两个指针的执行变化。
// 初始链表:[1]->[2]->[3]->[4]
ListNode *Reverse(ListNode *pHead) {
ListNode *pre = NULL;
ListNode *cur = pHead;
/* 循环前2个指针的状态:
[pre null]
[cur 1] ->[2]->[3]->[4]
*/
while (cur) {
ListNode *tmp = cur->next;
/* tmp== cur->next; 又多了一个tmp辅助指针,pre和cur没变化
[pre null ]
[tmp 2]->[3]->[4]
[cur 1] ->[2]->[3]->[4]
*/
cur->next = pre;
/* 把pre赋值给cur->next,变化的只有cur,pre和tmp没变化
[pre null ]
[tmp 2]->[3]->[4]
[cur 1] ->[pre null]
*/
pre = cur;
/*把cur赋值给tmp,变化的只有pre,cur和tmp没变化
[pre 1 ]->[null]
[tmp 2]->[3]->[4]
[cur 1] ->[null]
*/
cur = tmp;
/*
[pre 1 ]->[null]
[tmp 2]->[3]->[4]
[cur 2]->[3]->[4]
*/
}
return pre;
}
以此循环,继续往下推演,就能得到最终反转的链表在pre辅助指针上。两个指针独立看变化过程:
- pre指针:从一开始的null,变为等于[1],所以pre就是保持反转链表后的链表指针,每次循环都把pre等于最后一个元素,一直维持反转链表;
- cur指针:一开始是原始链表本身,循环一次后讲链表头去掉(已经把链表头转移到pre头指针),每次循环少一个元素,知道cur为空结束;
再举一个例子(链表两两交换)
题目:
给定一个链表,两两交换其中相邻的节点,并返回交换后的链表。
示例:
给定 1->2->3->4, 你应该返回 2->1->4->3.
说明:
你的算法只能使用常数的额外空间。
你不能只是单纯的改变节点内部的值,而是需要实际的进行节点交换。
思考过程: 定义了tmp、pre、cur三个辅助指针,看这3个指针的变化过程。
理解过程:
ListNode *SwapPairs(ListNode *head) {
if (head ==NULL) return head;
ListNode tmp(0);
tmp.next = head;
ListNode *pre = &tmp;
ListNode * cur = head;
/*
* 0、初始
* [tmp pre] -> [cur 1]->[2] ->[3]->[4]
* [pre] -> [1]->[2] ->[3]->[4]
* [cur 1]-> [2] ->[3]->[4]
*/
while (cur && cur->next){
pre->next = cur->next;
/*
* 1、 只有两个指针 pre->next = cur->next;
* [tmp pre 0] -> [2] ->[3]->[4]
* [pre 0] -> [2] ->[3]->[4]
* [cur 1] -> [2] ->[3]->[4] 没变
*/
pre = pre->next;
/*
* 2、 pre = cur->next;
* [tmp 0] -> [pre 2] ->[3]->[4]
* [pre 2]->[3]->[4]
* [cur 1]->[2]->[3]->[4] 没变
*/
cur->next = pre->next;
/*
* 3、 cur->next = pre->next;
* [tmp 0] -> [pre 2] ->[3]->[4] 没变
* [pre 2] ->[3]->[4] 没变
* [cur 1] ->[3]->[4]
*/
pre->next = cur;
/*
* 4、pre->next = cur;
* [tmp 0] -> [pre 2] ->[1]->[3]->[4]
* [pre 2] ->[1]->[3]->[4]
* [cur 1] ->[3]->[4]
*/
pre = cur;
/*
* 5、 pre = cur;
* [tmp 0] -> [2] ->[pre 1]->[3]->[4]
* [pre 1] ->[3]->[4]
* [cur 1] ->[3]->[4]
*/
cur = cur->next;
/*
* 6、 cur = cur->next;
* [tmp 0] -> [2] ->[pre 1]->[cur 3]->[4]
* [pre 1] ->[3]->[4]
* [cur 3] ->[4]
*/
}
return tmp.next;
}
可以看到,
- tmp指针:tmp初始化后从头到尾都没有赋值过,所以tmp保持交换后的链表;
- pre指针:开始指向两两交换前一个元素,循环过程中指向第二个元素,然后让tmp指向自己;
- cur指针:一开始指向交换第一个元素,循环过程中pre指向cur,这时完成两元素交换,最后两句完成下标后移;
思路的优点
以指针为核心++,而非链表 的理解思路最大的好处是,你可以独立观察每个辅助指针充当的角色,每个角色各司其职,最终一起完成整个算法逻辑的实现。