链表经典题目: 回文链表leetcode 234
一、原题回放
LeetCode 234:给定一个单向链表,判断它是否为回文链表并返回true或false。
例如下图这样一个链表,它的正序为“122”,反序为“221”,正序和反序不相同,因此我们认为其不是回文链表。
下图所示的链表正序和反序都是“1221”, 则我们认为它是回文链表。
我们需要注意的是,当链表中只有一个节点时,它的正序和反序都是第一个节点本身,当链表中不存在任何节点时,它的正序和反序都是空,所以只有一个节点的链表和没有任何节点的链表一定是回文链表。
二、思路梳理
从题目中我们知道,回文链表就是正序和反序相同的链表,因此我们可以采用依次比较链表中的第1个和第倒数第1个元素、第2个和第倒数第2个元素、第3个和第倒数第3个元素……直到比较完所有元素,只要在比较过程中有一个元素不同就认为它不是回文链表。
不过我们的链表单向链表,最后一个元素要遍历整个链表才能获得,在获取倒数第2个元素时又要将链表遍历一遍,如果我们的算法每次都从链表中直接遍历来获取元素进行比较的话,每次查找尾部元素的时候都要遍历整个链表,算法的时间复杂度将达到O(n^2),效率十分低下。因此我们有一个比较直观的思路,先将链表中的所有元素复制到数组中,然后在数组中取出元素进行比较,由于在数组中可以使用脚标直接获取数据,算法效率将大大提升。
根据上述思路我们得到如下伪代码:
原问题被分解为两个小问题:
- 遍历链表,计算链表长度并找到中点
- 反转链表的后半部分
- 依次比较两个链表元素是否全部相同,如果所有元素都相同则认为它是回文链表,否则它不是回文链表
小伙们伴可以先根据伪代码自行尝试完成一下代码。
三、代码实现
将之前的伪代码进行具体实现可以得到如下代码:
这样的一个算法在执行过程中对整个链表遍历的一遍,对和链表等长的list同样也遍历了一遍,因此它的时间复杂度是O(n);同时我们为了将链表保存到list,使用了和链表元素数量相同的list,因此它的空间复杂度也是O(n)。
在编码过程中,还有几个问题需要大家注意:
-
在依次比较list中头尾元素的时候(第16行),因为是头尾同时向中间比较,头尾各比较一般就已将将整个list全部比较完了,因此这里对长度len进行了除以2的操作。
-
由于list中的元素的脚标是从0开始,因此获取数组尾部元素的时候要注意减1(第17行),否则会出现数组越界错误。
-
在代码的最开始,还要记得判断head为null的特殊情况(第3行)。
四、更优化思路
在上面的解题思路中,我们为了解决获取单向链表获取尾部元素效率低下的问题,将链表中的数据全部复制到了list中,这样没有利用到链表的特性,也使用了更多的内存空间。那我们有没有办法利用链表的特性对当前链表进行一些修改,做到不需要申请额外空间也可以高效率的获取到链表尾部的元素呢?
我们可能会想,如果链表既可以正向依次遍历的同时也可以反向依次遍历就好了。是的,要做到这一点,就需要让链表后一节点的指针指向前一节点,我们通过将链表进行原地反转来做到,但是,我们应该反转全部的链表吗?如果将全部的链表进行反转,虽然获取原链表尾部的节点十分方便,但我们就无法高效的获取头部的节点了。其实我们只需要将链表从中间一分为二,将链表的后半部分进行反转。
根据上述思路我们得到如下伪代码:
原问题被分解为三个小问题:
- 遍历链表,计算链表长度并找到中点
- 反转链表的后半部分
- 依次比较两个链表元素是否全部相同,如果所有元素都相同则认为它是回文链表,否则它不是回文链表。
小伙们伴可以先根据伪代码自行尝试完成一下代码。
五、代码实现
将上面的伪代码进行具体实现可以得到如下代码:
代码的第23行至第31行使用了一个临时节点next对链表进行了原地反转,对于基础没那么强的同学,这里要仔细理解一下。
算法在执行过程中对链表遍历了四次,因此它的时间复杂度是O(n);同时因为我们没有申请额外的空间,因此它的空间复杂度是O(1)。
在编码过程中,还有几个问题需要大家注意:
- 遍历至链表中点时,因为mid以第1个节点head为初始值,因此循环里的循环变量i的初始值要设为1(第18行)。
- 我们将原来的一整个链表分成了前后两个链表,当链表节点数为奇数时,如果链表是回文链表,那么最终间的节点是不需要再和其它节点进行比较的。比如下面这个回文链表中,我们只需要比较头尾的两个节点,最中间的节点“3”就不需要进行比较了。
由于后半部分的链表将比前半部分多一个节点(具体原因为代码第16行,len/2被自动取整),因此在对两个链表进行比对时,需要以前半部分链表的结束为准(第34行),如果这里写pre!=null的话,当链表节点数为奇数时将会产生空指针错误。 - 和前一个思路一样,在代码的最开始,同样需要记得判断head为null的特殊情况(第3行)。
六、Python实现
将上面的C++代码改写为Python代码:
七、面试重要知识点提炼
第二个思路中提到的链表原地反转(代码第23行至第31行)是面试的常见考点,大家一定要理解透彻,并多加练习巩固。
八、进一步思考
在第二个思路中,我们通过对链表遍历两次来获得了链表的中点。其中,第一次遍历时计算了链表的长度和中点的位置,第二次遍历时将指针移动到中点。不过,有没有只遍历一次就可以获得链表的方法呢?
九、相关题目
LeetCode 206. 反转链表[https://leetcode-cn.com/problems/reverse-linked-list/]