我们知道 CPU 运行速度是非常快的,如果 CPU 每次运算都要到内存里去取数据无疑是很耗时的,所以在 CPU 与内存之间往往集成了挺多层级的缓存,这些缓存越接近CPU,速度越快,所以如果能提前把内存中的数据加载到如下图中的 L1, L2, L3 缓存中,那么下一次 CPU 取数的话直接从这些缓存里取即可,能让CPU执行速度加快,那什么情况下内存中的数据会被提前加载到 L1,L2,L3 缓存中呢,答案是当某个元素被用到的时候,那么这个元素地址附近的的元素会被提前加载到缓存中
以上文整型数组 1,2,3,4为例,当程序用到了数组中的第一个元素(即 1)时,由于 CPU 认为既然 1 被用到了,那么紧邻它的元素 2,3,4 被用到的概率会很大,所以会提前把 2,3,4 加到 L1,L2,L3 缓存中去,这样 CPU 再次执行的时候如果用到 2,3,4,直接从 L1,L2,L3 缓存里取就行了,能提升不少性能
画外音:
如果把 CPU 的一个时种看成一秒,则从 L1 读取数据需要 3 秒,从 L2 读取需要 11 秒,L3读取需要 25秒,而从内存读取呢,需要 1 分 40 秒,所以程序局部性原理能对 CPU 执行性能有很大的提升
步骤 1:定义两个节点:pre, cur ,其中 cur 是 pre 的后继结点,如果是首次定义, 需要把 pre 指向 cur 的指针去掉,否则由于之后链表翻转,cur 会指向 pre, 就进行了一个环(如下),这一点需要注意
步骤2:知道了 cur 和 pre,翻转就容易了,把 cur 指向 pre 即可,之后把 cur 设置为 pre ,cur 的后继结点设置为 cur 一直往前重复此步骤即可,完整动图如下
注意:同递归翻转一样,迭代翻转完了之后 head 的后继结点从 4 变成了 1,记得重新设置一下。
知道了解题思路,实现代码就容易多了,直接上代码
/** * 迭代翻转 */ publicvoiditerationInvertLinkedList(){ // 步骤 1 Node pre = head; Node cur = pre.getNext(); pre.setNext(null); // pre 是头结点,避免翻转链表后形成环
// 步骤 2 while (cur != null) { /** * 务必注意!!!:在 cur 指向 pre 之前一定要先保留 cur 的后继结点,不然如果 cur 先指向 pre,之后就再也找不到后继结点了 */ Node next = cur.getNext(); cur.setNext(pre); pre = cur; cur = next; } // 此时 pre 指向的是原链表的尾结点,翻转后即为链表 head 的后继结点 head.next = pre; }
用迭代的思路来做由于循环了 n 次,显然时间复杂度为 O(n),另外由于没有额外的空间使用,也未像递归那样调用递归函数不断压栈,所以空间复杂度是 O(1),对比递归,显然应该使用迭代的方式来处理!
变形题 1
:给定一个链表的头结点 head,以及两个整数 from 和 to ,在链表上把第 from 个节点和第 to 个节点这一部分进行翻转。例如:给定如下链表,from = 2, to = 4
head-->5-->
4-->3-->2
-->1
将其翻转后,链表变成
head-->5--->
2-->3-->4
-->1
有了之前翻转整个链表的解题思路,现在要翻转部分链表就相对简单多了,主要步骤如下:
根据 from 和 to 找到 from-1, from, to, to+1 四个结点(注意
临界条件
,如果 from 从头结点开始,则 from-1 结点为空, 翻转后需要把 to 设置为头结点的后继结点, from 和 to 结点也可能超过尾结点,这两种情况不符合条件不翻转)。
对 from 到 to 的结点进行翻转
将 from-1 节点指向 to 结点,将 from 结点指向 to + 1 结点
知道了以上的思路,代码就简单了,按上面的步骤1,2,3 实现,注释也写得很详细,看以下代码(对 from 到 to 结点的翻转我们使用迭代翻转,当然使用递归也是可以的,限于篇幅关系不展开,大家可以尝试一下)。
/** * 迭代翻转 from 到 to 的结点 */ publicvoiditerationInvertLinkedList(int fromIndex, int toIndex)throws Exception {
Node fromPre = null; // from-1结点 Node from = null; // from 结点 Node to = null; // to 结点 Node toNext = null; // to+1 结点