惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

大猫的无限游戏
大猫的无限游戏
Webroot Blog
Webroot Blog
cs.AI updates on arXiv.org
cs.AI updates on arXiv.org
T
Threat Research - Cisco Blogs
V2EX - 技术
V2EX - 技术
L
LINUX DO - 热门话题
Google DeepMind News
Google DeepMind News
Recorded Future
Recorded Future
S
Schneier on Security
I
InfoQ
Cyber Security Advisories - MS-ISAC
Cyber Security Advisories - MS-ISAC
cs.CV updates on arXiv.org
cs.CV updates on arXiv.org
The GitHub Blog
The GitHub Blog
S
Security @ Cisco Blogs
O
OpenAI News
W
WeLiveSecurity
Vercel News
Vercel News
阮一峰的网络日志
阮一峰的网络日志
Simon Willison's Weblog
Simon Willison's Weblog
人人都是产品经理
人人都是产品经理
Cloudbric
Cloudbric
The Last Watchdog
The Last Watchdog
The Hacker News
The Hacker News
Google Online Security Blog
Google Online Security Blog
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
GbyAI
GbyAI
NISL@THU
NISL@THU
T
Tailwind CSS Blog
V
Visual Studio Blog
PCI Perspectives
PCI Perspectives
K
KPMG report finds enterprise disconnect between AI and its ROI | CIO
Jina AI
Jina AI
D
DataBreaches.Net
B
Blog RSS Feed
N
News and Events Feed by Topic
N
News and Events Feed by Topic
H
Heimdal Security Blog
cs.CL updates on arXiv.org
cs.CL updates on arXiv.org
腾讯CDC
Latest news
Latest news
V
Vulnerabilities – Threatpost
Hacker News: Ask HN
Hacker News: Ask HN
WordPress大学
WordPress大学
V
V2EX
aimingoo的专栏
aimingoo的专栏
博客园 - 司徒正美
Apple Machine Learning Research
Apple Machine Learning Research
D
Darknet – Hacking Tools, Hacker News & Cyber Security
The Register - Security
The Register - Security
Help Net Security
Help Net Security

smallyu的博客

为什么买的加密货币一定要去中心化 RISC-V 虚拟机无法代替 EVM 虚拟机 Polkadot 的 Existential Deposit 机制错在哪里 利息,套利,交易策略,金融市场 LayerZero 从来不挑战比特币的地位 默认参数引起的以太坊节点运行错误 硬件钱包与资产安全 预测市场比币圈更赌场 为什么去中心化的跨链桥不可能实现 我对于 AI 时代的答案 不要投资任何隐私币 区块链技术世界的三大真理 以太坊 AA 钱包的致命问题是什么 基于 AI 语义执行的 MCP 区块链的设计 一个集成 Geth 和 CometBFT 的兼容层 我的加密货币定投策略(二) DeFi 基础: 理解 AMM 定价机制 Go 语言 GMP 调度器的原理是什么 Web3 项目分析计划 对 Psyche Network 项目的分析 continuation 教程: 理解 CPS 如何开发一个比特币符文(Runes)协议 比特币脚本开发教程 我从王垠的计算机科学视频班学到了什么 区块链技术面试题(2025年版) Rust 语言容易让新手困惑的一个“过度优化” Solana 智能合约开发教程 (1) 尝试开发一个最小 EVM 虚拟机 基于 ZK 的链上身份系统设计 一个 Web3 打赏系统的设计 鼓吹 Cursor 的人技术能力都差 关于 Code Review 的礼节 假如启动一个新的以太坊 PoS 网络 发行加密货币的最好方式 所有 BFT 共识的区块链都是中心化的 对 2025 年区块链行业的预测 Restaking 项目的经济难题 如何看懂任意区块链项目的技术架构 为什么不要做以太坊的二次开发 为什么不要做智能合约开发和 DeFi 开发 我的加密货币定投策略(一) PoS 类型的区块链如何处理分叉 Ethereum Casper 为什么需要 EIP-7251 区块链中的 PBFT 不需要第二次投票 开发者的思维方式 发币的核心要点 炒币投资的小 tips 为什么说 PoW 比 PoS 更加去中心化 牛市对普通人来说意味着什么 如何健康地远程工作 为什么比特币不用概率加密函数 程序员的 “服从权威” 心理 区块链技术面试题 如何区分公有链和联盟链 对 Layer 2 项目创业想法的回复 对区块链共识机制的理解 Pebbling Game 鹅卵石游戏 PDP 文件证明的局限性 不要小瞧 ChatGPT 为什么炒币不是一个好主意 一种在区块链上生成随机数的机制 为什么以太坊的私钥计算不可逆 关于以太坊的私钥碰撞 “猜均值的2/3” dApp 游戏设计 Proof of Storage/Space/Replication 的区别 Proofs of Retrievability 文件证明的含义 对 S-PDP 文件证明的示例和解释 我的加密货币交易机器人 对区块链行业的见闻 随机确认块的共识机制 VRF + BFT 共识引起交易失败的问题 为什么要重视编程思想 对 Web 3.0 的理解 GitBook 好用吗? 一种区块链节点存储扩容的方式 一种基于“自我中心主义”的共识机制 基于 Multi-Linked List 的区块链设想 理解哈希函数与序列化 联盟链比公有链差在哪儿 在 Dijkstra 算法中保存路径 为什么数字货币使用区块链是政治问题 区块链:下一代数字身份认证体系的基石 网页技术能实现 3D 建模吗? 给区块链一个定义 从 Erlang 开始了解 Actor 模型 一种侧边导航栏的交互方式 Rust 的 ownership 是什么? Haskell 中的 Monad 是什么? 浅析 Libra 背后的区块链技术 对区块链的理性认识 Rust 基础语法概述 基于 Java 的爬虫框架 WebCollector Kotlin:简化版的 Scala JavaScript 有关联数组吗? 主流编程语言的异常处理机制 Go 语言基本语法 Scala 语法基础 用 Scala 改写 Java 浅度实践 Python 获取海贼王更新信息 HTML5 音乐可视化
链表常见算法题及解析
2021-10-27 · via smallyu的博客

链表常见算法题及解析

2021-10-27

目录:

  • 翻转链表
  • 判断链表是否有环
  • 链表如果有环,找到环的起点
  • 判断两个链表是否相交
  • 链表如果相交,找到交点
  • 合并两个有序链表

翻转链表

问题

对于一个这样的链表:

希望经过函数处理后,变成这样:

链表结构定义

节点的定义为:

type Node struct {
    Value int
    Next  *Node
}

构造链表方法为:

func createLinkedList(n int) *Node {
    head := &Node{Value: 0}
    node := head
    for i := 0; i < n; i++ {
        if i < n {
            node.Next = &Node{Value: i + 1}
        }
        node = node.Next
    }
    return head
}

函数会返回一个链表的指针。使用指针而不是结构体类型是因为,Go 语言的某些关于变量的设计,无法使用 Node{} == nil 的形式判断变量是否为空,因为理论上 Node{} 不是 nil。这就造成了如果使用Node{}作为链表头部的变量类型,在遍历的时候找不到一个合理的结束时机,只能使用类似 Node{}.Next == nil 这样的形式,还会遗漏掉最后一个节点。

迭代翻转链表

这里不能使用直接改变节点值的方式,比如遍历一次后把链表节点的值按照顺序储存到数组中,然后再遍历一次,一次修改链表节点的值。这个违背了数据结构的意义。可以使用递归完成翻转链表的操作。

比如第一个节点,使用 temp 变量储存翻转前的下一个节点的位置,然后把 head.Next 指向翻转后应该有的节点位置,第一个节点的下一个节点是空节点,第二个节点的下一个节点是节点 1。完成 head.Next 的指向后,head 要指向 temp 也就是原来的下一个节点用以完成遍历。这时还需要要用一个 curr 变量来储存head 跳转前的位置,方便下一次 head.Next 指向上一个节点的位置。这应该是一个简单的过程。

func reverseLinkedList(head *Node) *Node {
    curr := new(Node)
    for head != nil {
        temp := head.Next
        head.Next = curr
        curr = head
        head = temp
    }
    return curr
}
  • 时间复杂度:O(n)
  • 空间复杂度:O(1)

执行

执行程序后结果和预期一致:

func main() {
    head := createLinkedList(4)
    head = reverseLinkedList(head)
    for head != nil {
        fmt.Println(head.Value)
        head = head.Next
    }
}

判断链表是否有环

问题

链表有环是指链表中“最后”的一个节点,它的下一个节点指向了链表中位于它之前的节点。

当程序遍历这个链表,会发现进入了死循环,永远找不到出口了。怎么判断一个链表,是否存在这样的环呢?

分析

常用的解决思路是双指针。设想一个在赛道上的场景,两个人 A 和 B 处于同样的起点,但是他们跑步的速度并不相同,A 的速度 v1=1,B 的速度 v2=2,也就是 B 比 A 快。在这样的情况下,只要他们不停,B 一定会超过 A 一圈然后再次追上 A,这是一种生活中的常识。

在一个圈里,一快一慢的两个点一定会再次相遇,而且他们经过的路程是可以计算的,路程 s1 和 s2 应该满足这样的关系:

s2 - s1 = nR

R 是圆圈的周长,n 是正整数,他们位于出发点时 n=0,第一次相遇的时候 B 比 A 多跑了一圈,多出了 1 倍周长的路程,n=1。

和链表的情景相比较,赛道的场景还少了开始的一段距离,在进入赛道之前,A 和 B 会先从赛道外的小路进入赛道,然后再开始绕圈跑步。他们的起点在赛道外,为了便于计算,他们的速度从始至终不发生变化,那么当他们进入赛道之后,就已经不是同样的起点了。

在这种情况下,他们经过的路程 s1 和 s2 还有规律可循吗?设圆形赛道外的直道距离为 d,相比上面的关系式,他们在圆圈内的路径依然满足 n 倍的周长 R,只不过现在的表达式不同了:

(s2 - d) - (s1 - d) = nR
    s2 - d - s1 + d = nR
            s2 - s1 = nR

结果表达式在相互抵消路径 d 之后,和之前的一样。

A 的路程 s1=v1t,B的路程 s2=v2t,时间 t 是一样的,速度 v1 和 v2 是已知的 1 和 2,有:

    s2 - s1 = nR
  v2t - v1t = nR
     2t - t = nR
          t = nR

取 n = 1,t = R

解决

回到链表的问题,其实我们只要用快慢指针就可以判断链表是否有环了,并不需要知道他们具体相遇的点在哪儿,不过计算路径关系的公式可以辅助我们验证结果的正确性。

回到这个链表,用两个指针 A 和 B 从节点 1 分别以速度 1 和 2 出发:

他们的位置关系将会是:

时间 t 0 1 2 3 4
A 的位置 节点 1 节点 2 节点 3 节点 4 节点 5
B 的位置 节点 1 节点 3 节点 5 节点 3 节点 5

在第 4 个时间点的时候,A 和 B 相遇了,环的周长正好等于 4,满足 t = R 的关系。

链表如果有环,找到环的起点

问题

这个问题是上一个问题的延伸,在判断链表已经有环的基础上,找到环的起点。比如这样的一个链表,环的起点是节点 3。

分析

(1)

在判断链表是否有环的问题中,我们得到了一个至关重要的结论:

t = R

两个快慢指针将会在等于环长度的时间点相遇。对于上图的链表,快慢指针的位置关系是这样:

时间 t 0 1 2 3 4 5 6
A 的位置 节点 1 节点 2 节点 3 节点 4 节点 5 节点 6 节点 7
B 的位置 节点 1 节点 3 节点 5 节点 7 节点 3 节点 5 节点 7

我们可以观察到,环的长度是 6,快慢指针也会在第 6 秒相遇,他们交点位置是节点 7:

(2)

根据上面提到的之前的结论,按照慢指针 v1 = 1 的速度,它经过的路程和时间是一样的,也就是说,从出发点到两指针相遇的路径长度,根据 t = R,此刻的时间是 t,正好是环的长度 R:

(3)

做一个假设,慢指针保持着这个长度为 R 的走过的路径,向前移动一步,会变成这样:

再走一步,变成了这样:

(4)

到这里似乎还不知道我们要干什么。现在对路径设一个变量,从 出发点环的起点 之间的距离设为 l1,整个链表的长度设为 l,环的长度仍然为 R。

这 3 个变量将满足这样的关系:

l - l1 = R

这是太显而易见的事情。

(5)

记得我们一开始的结论吗?从 出发点快慢指针的交点 之间的距离,等于环的长度 R:

变量 l 和 l1 保持不变,图就成了这样:

此时的 l 仍然等于 l1 + R,不同的是,l1R 重合了。

(6)

l - l1 = R

重合之后,等式关系还成立吗?当然成立,因为整个链表没有变,变量的大小没有变。但好像又觉得哪里奇怪。

现在新设一个变量,设从 快慢指针的交点环的起点 的距离为 l2

此时:

l - l2 = R

(7)

经过这样一些比较,发现 l1 == l2,也就是从 出发点环的起点 的距离,等于 快慢指针的交点环的起点 的距离。

解决

出发点 -> 环的起点 == 快慢指针的交点 -> 环的起点

这是一个很重要的结论,因为我们此时的快慢指针就在 快慢指针的交点 上,在节点 7 的位置。

如果这个时候在新增一个指针 p3,在快慢指针相交的时刻,从整个链表的 出发点 1 出发(速度为 1),那么 p3 和慢指针一定会相交,因为 p3环的起点 的距离等于慢指针到 环的起点 的距离。p3 遇到慢指针的位置,就是环的起点。

判断两个链表是否相交

问题

存在两个链表,分别在某一个节点指向了同一个节点作为下个节点:

这里有两个链表:

1 -> 2 -> 3 -> 4
     5 -> 3 -> 4

怎么判断两个链表是否相交?

分析

一种简单的做法是,分别遍历每条链表到最后一个节点,判断最后一个节点是否相同。如果两个链表在中间节点相交,则最后一个节点一定相同。

链表如果相交,找到交点

问题

对于这样两个链表:

如何找到第一个交点 3 ?

分析

一种简单的解决思路是,把这个链表的尾节点和任意一个链表的头节点连起来:

可以是链表 1 的尾节点到链表 2 的头节点,或者链表 2 的尾节点到链表 2 的头节点,总之连起来以后,问题就转变成了,找到链表环的起点。

合并两个有序链表

问题

给出两个有序链表,将两个链表合并为一个有序链表。

分析

思路暴力简单,同时迭代两个链表,按照顺序依次合并就可以了。控制好边界条件。

代码

node 结构定义:

type Node struct {
    Value int
    Next  *Node
}

构建两条链表:

func main() {
    root1 := &Node{
        Value: 1,
    }
    root1.Next = &Node{
        Value: 1,
    }
    root1.Next.Next = &Node{
        Value: 3,
    }
    root1.Next.Next.Next = &Node{
        Value: 5,
    }

    root2 := &Node{
        Value: 1,
    }
    root2.Next = &Node{
        Value: 2,
    }
    root2.Next.Next = &Node{
        Value: 4,
    }

    root := merge(root1, root2)
    for root != nil {
        fmt.Println(root.Value)
        root = root.Next
    }
}

合并链表:

func merge(root1 *Node, root2 *Node) *Node {
    var root *Node
    var temp *Node
    if root1.Value <= root2.Value {
        root = root1
        temp = root2
    } else {
        root = root2
        temp = root1
    }
    p1 := root
    p2 := p1.Next
    for {
        if p2 == nil || temp == nil {
            break
        }
        if p2.Value <= temp.Value {
            p1.Next = p2
            p1 = p1.Next
            p2 = p2.Next
        } else {
            p1.Next = temp
            p1 = p1.Next
            temp = temp.Next
        }
    }
    return root
}