【链表系列01】反转链表

系列文章目录

【链表系列01】反转链表
【链表系列02】两两交换链表中的节点



1. 题目概述

1.1. 题目概述

题目来源:力扣(Leetcode) - 第206题,单向链表问题。
难度等级:简单

1.2. 题目解读

给定一个单向链表,反转该链表,并返回。
反转链表
本题主要考察 数据结构-链表

只要有点编程基础的同学,一定非常了解数组。所谓数组,就是在内存上开辟出一块连续的内存空间来存储数据;而链表是比数组稍微复杂一些的数据结构,它并不需要一块连续的内存空间,它通过“指针”将一组零散的内存块串联起来使用

我们知道数组也是有指针的,因为数组的内存空间是连续的,所以根据数组首个元素的地址和数据类型,就可以直接计算出各个元素的指针。例如,我们定义一个整型数组:

int [] a = {8, 6, 3, 2, 1};

那么 a 表示该数组地址(即数组首元素的地址),第 i 个元素的地址即为:a + i*sizeof(int)。因此,访问数组元素的操作,时间复杂度为 O ( 1 ) O(1) O(1)

而链表则不同,链表中每个元素(节点)的地址无法直接计算出来,需要从头节点(head)开始,根据 next 指针,顺藤摸瓜,依次获得每个元素。因此,访问链表元素的操作,时间复杂度为 O ( n ) O(n) O(n)

2. 解体思路

【本题的考点】
数据结构:单链表
算法:迭代或者递归

链表是由多个节点串起来的,对于单链表,每个节点除了存储数据之外,还需要记录链上的下一个节点的地址(一般称为后继指针 next),如下图所示:
单链表
一般将单向链表的开始节点叫作 头节点(head),将最后一个节点叫作 尾节点(tail)。其中,head 记录了单向链表的基地址,通过它可以遍历得到整条链表;对于 tail ,因为它已经是最后一个节点了,所以它的 next 指针指向一个空地址 NULL。

2.1. 迭代

迭代方法很简单,人脑也最容易想到:通过迭代遍历链表,调换next指针——将原来指向后继节点的指针断开,重新指向前一节点,如下图所示。

反转链表图形示例
将上图所示过程表示为伪代码,如下:

  1. 从头节点head开始
    cur = head;
    prev = null;
    
    当前节点设为 head,前继节点设为 null。
  2. 迭代
    while (cur不是null) {
    	temp = cur.next;  // 将当前节点的下一节点保存到中间变量
    	cur.next = prev;  // 断开当前节点的原有链路,将 next 指向前继节点,此时当前节点已被更新
    	prev = cur;       // 前继节点后移
    	cur = temp;       // 当前节点后移
    }
    
    // 迭代结束后,prev 为原链表的尾节点 tail,即新链表的 head
    return prev
    

除了迭代,我们还可以拓宽一下思路,使用递归的方法实现。

2.2. 递归

所谓递归,简单来说,就是函数自己调用自己。

如果你已经学习了一段时间的算法,你可能会觉得算法中有两个比较难理解的知识点:递归和动态规划。

递归之所以难以理解,主要是因为递归的逻辑更像是一种计算机思维,而非人脑思维。不过,不要担心,跟着我的文章,我会带你攻克它,等你习惯了计算机思维,递归就变得很简单了。

而对于动态规划,我们后续会通过动态规划系列专门讲解,敬请期待。

递归作为一种应用非常广泛的算法,我们需要提前打好基础。本专栏的后续内容中,很多的数据结构和算法都将使用递归来实现,例如 (前中后序)二叉树遍历、DFS 深度优先搜索等等。所以,前期我们先通过一些简单的练习来搞懂递归,循序渐进,这样后期学习比较复杂的数据结构和算法的时候就会轻松很多了。

你可能要问了,什么时候我们可以使用递归呢?❓

2.2.1. 递归需要满足的三个条件

为了方便讲解,举一个现实生活中的例子。假如你周末带着女朋友去看电影,去的晚,前面几排坐满了人,你们从第一排开始往后找,找了有座位的一排就坐下了,女朋友问你,咱们是在第几排啊?你们找的时候,没看第几排,而且电影院里面太黑了,看不清,也没法数,怎么办才能知道呢?🤔

你想了想,使用递归可以解决这个问题!你问前面一排的人是第几排,你只要在他的数字上加一,就知道自己在哪一排了;但是,前面的人也看不清啊,所以他也问他前面一排的人在第一排;就这样一排一排往前问,直到问到第一排的人,他说我在第一排,然后再这样一排一排再把数字传回来,直到你前面的人告诉你他在哪一排,你就知道答案了。

以上例子就是一个非常标准的递归求解问题的分解过程,去的过程叫“递”,回来的过程叫“归”。实际上,所有的递归问题都可以用递推公式来表示,上面的例子,递推公式是这样的:

f ( n ) = f ( n − 1 ) + 1 f(n) = f(n - 1) + 1 f(n)=f(n1)+1
f ( 1 ) = 1 f(1)=1 f(1)=1

有了这个递推公式,就不难写出递归代码了:

int f(int n) {
	if (n == 1) {  // 递归终止条件
		return 1;
	}
	return f(n - 1) + 1;  // 分解为子问题
}	

你可能已经注意到了,用递归来解决问题是需要条件的,可以总结为三个条件。

  1. 一个问题的解可以分解为几个子问题的解

    什么是子问题?子问题就是数据规模更小的问题。例如,上文讲的电影院的例子,你要知道“自己在哪一排”的问题,可以分解为“前一排的人在哪一排”这样一个子问题。

  2. 这个问题与分解之后的子问题,除了数据规模不同,求解思路完全一样

    还是上文电影院的例子,你求解“自己在哪一排”的思路,和前面一排人求解“自己在哪一排”的思路,是一模一样的。

  3. 存在递归终止条件

    如上文,我们可以把问题分解为子问题,然后把子问题再分解为子子问题,一层一层分解下去。但是如果没有终止条件的话,就变成无限循环、永远分解下去了,这样永远也得不到答案,因此必须要有终止条件。还是电影院的例子,等分解到第一排,第一排的人不需要再继续询问任何人就知道自己在哪一排,这就是递归的终止条件。

3. 代码实现

3.1. python

单链表节点类:

class LinkNode:
    """链表节点"""

    def __init__(self, val=None, next=None):
        self.val = val
        self.next = next

3.1.1. 迭代方法

def reverse(head: LinkNode) -> LinkNode:
    cur, prev = head, None

    # 迭代,直至最后一个节点
    while cur:
        temp = cur.next
        cur.next = prev
        prev = cur
        cur = temp
    return prev

以上代码,采用了常规做法——设置一个中间变量——实现了“数据交换”,比较繁琐,实际上 python 有一个特性,可以简洁、高效地实现数据交换(第6行代码):

def reverse(head: LinkNode) -> LinkNode:
    cur, prev = head, None

    # 迭代,直至最后一个节点
    while cur:
        cur.next, prev, cur = prev, cur, cur.next
    return prev

以上 cur.next, prev, cur = prev, cur, cur.next代码,对于刚接触 python 的同学来说,可能难以理解,实际上,是把等号右边的值全部都保存了后再一次性赋值给等号左边的变量,即所谓的一次性赋值。

LeetCode运行结果
在这里插入图片描述

3.1.2. 递归方法

def reverse(head: LinkNode) -> LinkNode:
	return reverse_recursion(head)


def reverse_recursion(cur: LinkNode, prev: LinkNode = None) -> LinkNode:
    """
    递归方法
    :param cur: 当前节点
    :param prev: 前继节点
    :return: 结果(头节点head)
    """

    # 递归终止条件
    if cur is None:
        return prev

    # 每个子问题都做相同处理:
    #   调换next指针——将原来指向后继节点的指针断开,重新指向前继节点
    #   当前节点移动到下一个节点
    cur.next, prev, cur = prev, cur, cur.next

    # 递归调用
    return reverse_recursion(cur, prev)

LeetCode运行结果

在这里插入图片描述

3.2. java

单链表节点类:

public class LinkNode {

    public int val;
    public LinkNode next;

    LinkNode() {}

    public LinkNode(int val) { this.val = val; }

    public LinkNode(int val, LinkNode next) { this.val = val; this.next = next;}
}

3.2.1. 迭代方法

public LinkNode reverseList(LinkNode head) {
        LinkNode cur = head;
        LinkNode prev = null;
        while (cur != null) {
            LinkNode temp = cur.next;
            cur.next = prev;
            prev = cur;
            cur = temp;
        }
        return prev;
    }

LeetCode运行结果

在这里插入图片描述

3.2.2. 递归方法

public LinkNode reverse(LinkNode head) {
	return reverse_recursion(head, null);
}

public LinkNode reverse_recursion(LinkNode cur, LinkNode prev) {
	if (cur == null) {
        return prev;
    }

    LinkNode temp = cur.next;
    cur.next = prev;
    prev = cur;
    cur = temp;

    return reverse_recursion(cur, prev);
}

LeetCode运行结果

在这里插入图片描述

3.3. scala

单链表节点类:

class LinkNode(_value: Int = 0, _next: LinkNode = null) {
  var next: LinkNode = _next
  var value: Int = _value
}

3.3.1. 迭代方法

参照 java 写法即可:

def reverse(head: LinkNode): LinkNode = {
    var cur: LinkNode = head;
    var prev: LinkNode = null;
    while (cur != null) {
      val temp = cur.next
      cur.next = prev
      prev = cur
      cur = temp
    }
    prev
  }

LeetCode运行结果

在这里插入图片描述

3.3.2. 递归方法

def reverse(head: LinkNode): LinkNode = {
    reverse_recursion(head, null)
}

def reverse_recursion(cur: LinkNode, prev: LinkNode): LinkNode = {
    if (cur == null) return prev

	// scala 函数的形参为 val类型,不可改变,因此先将形参的值传给可变的量
    var curVar = cur
    var prevVar = prev
    val temp = curVar.next
    curVar.next = prevVar
    prevVar = curVar
    curVar = temp
    
    // 递归调用
    reverse_recursion(curVar, prevVar)
  }

LeetCode运行结果

在这里插入图片描述

4. 迭代和递归性能比较

综合 LeetCode 的运行结果,可以知道递归的性能并不比迭代好,我们来分析下原因。

虽然本题中迭代和递归的时间复杂度都是 O ( n ) O(n) O(n) n n n 为链表的节点个数),但是严格来说,迭代的时间复杂度都是 O ( n ) O(n) O(n),而递归的时间复杂度都是 O ( 2 n ) O(2n) O(2n),因为递归分为两步:递和归,相当于迭代了两次,因此递归的性能是比迭代差的。

下一篇文章将讲解“两两交换链表中的节点”,同样是单链表问题,但难度有进阶,敬请期待!

在这里插入图片描述

不需要打赏啦 😊 喜欢我的文章就关注我😻、点赞👍、收藏⭐吧!谢谢 🤞
如果有问题❓,直接留言就好啦,我会第一时间回复!👨‍🎓

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值