博客
关于我
强烈建议你试试无所不能的chatGPT,快点击我
Golang面试考题记录 ━━ 删除链表的倒数第N个节点, 学习闭包递归、双指针、哨兵节点
阅读量:4115 次
发布时间:2019-05-25

本文共 7472 字,大约阅读时间需要 24 分钟。

问:

题目:《》

给定一个链表,删除链表的倒数第 n 个节点,并且返回链表的头结点。

示例:

给定一个链表: 1->2->3->4->5, 和 n = 2.

当删除了倒数第二个节点后,链表变为 1->2->3->5.

说明:

给定的 n 保证是有效的。

进阶:

你能尝试使用一趟扫描实现吗?

答:

方法一 :利用一个切片记录所有的指针

执行用时 :0 ms, 击败了100.00%的用户
内存消耗 :2.3 MB, 击败了7.14%的用户

func removeNthFromEnd1(head *ListNode, n int) *ListNode {
var data []*ListNode node := head // 将所有节点的计入切片 for {
data = append(data, node) if node.Next == nil {
break } node = node.Next } // 排除没有数据或者只有一个值,删掉的也是一个 l := len(data) if l == 0 || l < n || (l == 1 && n == 1) {
return nil } // 切片定位到n的位置 node = data[l-n] // 已经到最后一个了,只能往前找,让前一个节点的Next为nil if node.Next == nil {
node = data[l-n-1] node.Next = nil } else {
node.Val = node.Next.Val node.Next = node.Next.Next } return head}

方法二 :循环第一遍找到节点数量,循环第二遍找到总数-n的节点

执行用时 :0 ms, 击败了100.00%的用户
内存消耗 :2.2 MB, 击败了35.7%的用户

func removeNthFromEnd2(head *ListNode, n int) *ListNode {
node := head l := 0 // 合计节点数量 for {
l++ if node.Next == nil {
break } node = node.Next } // 排除没有数据或者只有一个值,删掉的也是一个 if l == 0 || l < n || (l == 1 && n == 1) {
return nil } // 找到n的节点位置,删除 node = head temp := head for i := 0; i < l; i++ {
if l-n == i {
// 已经到最后一个了,只能往前找,让前一个节点的Next为nil if node.Next == nil {
node = temp node.Next = nil break } else {
node.Val = node.Next.Val node.Next = node.Next.Next break } } temp = node node = node.Next } return head}

方法三 :思路和方法二一致,但少了变量

执行用时 :0 ms, 击败了100.00%的用户
内存消耗 :2.2 MB, 击败了64.29%的用户

func removeNthFromEnd3(head *ListNode, n int) *ListNode {
node := head l := 0 // 合计节点数量 for {
l++ if node.Next == nil {
break } node = node.Next } // 排除没有数据或者只有一个值,删掉的也是一个 if l == 0 || l < n || (l == 1 && n == 1) {
return nil } // 找到n的节点位置,删除 node = head for i := 0; i < l; i++ {
if n == 1 && i+2 == l {
// 已经到最后一个了,只能往前找,让前一个节点的Next为nil node.Next = nil break } if l-n == i {
node.Val = node.Next.Val node.Next = node.Next.Next break } node = node.Next } return head}

方法四 :利用递归

因为平台在公共变量部分有bug,所以只能使用闭包递归的方法。也正好顺便学习一下。
执行用时 :0 ms, 击败了100.00%的用户
内存消耗 :2.2 MB, 击败了100.00%的用户

func removeNthFromEnd4(head *ListNode, n int) *ListNode {
// 定义函数 type funcType func(*ListNode, int) *ListNode var ra funcType isBack := 1 ra = func(head *ListNode, n int) *ListNode {
// 到底退回 if head.Next == nil {
if n-isBack <= 0 {
return nil } return head } // 只删除最后一个 if n == 1 && head.Next.Next == nil {
head.Next = nil return head } // 递归开始 ra(head.Next, n) // 定位到n的位置再删除 isBack++ if n-isBack == 0 {
head.Val = head.Next.Val head.Next = head.Next.Next } return head } // 执行闭包并返回 return ra(head, n)}

方法五 :利用递归,改进了方法四

执行用时 :0 ms, 击败了100.00%的用户
内存消耗 :2.2 MB, 击败了100.00%的用户

func removeNthFromEnd5(head *ListNode, n int) *ListNode {
// 就一个直接返回空 if n == 1 && head.Next == nil {
return nil } // 定义一个函数类型 type funcType func(*ListNode) // 声明 var ra funcType // 创建一个递归函数 ra = func(head *ListNode) {
// 如果就切割最后一个 // 不能写成if head.Next.Next==nil && n==1{,否则在某些条件里会内存溢出 if n == 1 && head.Next.Next == nil {
head.Next = nil return } // 到达最后一层则返回 if head.Next == nil {
return } // 递归 ra(head.Next) // 减少一层 n-- // 到达返回指定位置则去掉该位置的节点 if n == 1 {
head.Val = head.Next.Val head.Next = head.Next.Next } return } // 执行 ra(head) // 返回 return head}

方法六 :双指针

执行用时 :0 ms, 击败了100.00%的用户
内存消耗 :2.2 MB, 击败了100.00%的用户

func removeNthFromEnd6(head *ListNode, n int) *ListNode {
// 就一个直接返回空 if n == 1 && head.Next == nil {
return nil } // 初始化 nodeI, nodeJ, x := head, head, head // 如果n==1,那么s为1 s := 0 if n == 1 {
s = 1 } // 指针定位到n的位置 for ; nodeI.Next != nil; nodeI = nodeI.Next {
n-- // 第一个指针到达指定位置,开始定位第二个指针 if n < 1 {
// x每次都记录当前的节点指针,则在循环结束的时候,x永远是nodeJ的前一个节点 x = nodeJ nodeJ = nodeJ.Next } } // 判断是否删除最后一个 if s == 1 {
x.Next = nil } else {
nodeJ.Val = nodeJ.Next.Val nodeJ.Next = nodeJ.Next.Next } return head}

方法七 :双指针进化版

执行用时 :0 ms, 击败了100.00%的用户
内存消耗 :2.2 MB, 击败了100.00%的用户

func removeNthFromEnd7(head *ListNode, n int) *ListNode {
// 就一个直接返回空 if n == 1 && head.Next == nil {
return nil } nodeJ := head // 指针定位到n的位置 for nodeI := head; nodeI != nil; nodeI = nodeI.Next {
// 第一个指针到达指定位置,开始定位第二个指针 if n < 1 {
// 判断是否是只删除最后一个 if nodeJ.Next.Next == nil {
nodeJ.Next = nil } else {
nodeJ = nodeJ.Next } } n-- } // 不是最后一个才进行删除,如果是只删除最后一个则已经在for循环中完成 if nodeJ.Next != nil {
*nodeJ = *(nodeJ.Next) } return head}

方法八 :双指针加入哨兵节点

执行用时 :0 ms, 击败了100.00%的用户
内存消耗 :2.2 MB, 击败了100.00%的用户

func removeNthFromEnd8(head *ListNode, n int) *ListNode {
// 就一个直接返回空 if n == 1 && head.Next == nil {
return nil } // 创建一个哨兵节点 var nodeJ = &ListNode{
Val: 0, Next: head, } // 指针定位到n的位置 for nodeI := head; nodeI != nil; nodeI = nodeI.Next {
// 第一个指针到达指定位置,开始定位第二个指针 if n < 1 {
nodeJ = nodeJ.Next } n-- } // for循环已经定位完成,但并没有到达n的位置,是倒数n+1的位置 if nodeJ.Next.Next != nil {
nodeJ.Next.Val = nodeJ.Next.Next.Val nodeJ.Next.Next = nodeJ.Next.Next.Next } else {
nodeJ.Next = nil } return head}

方法九 :双指针加入哨兵节点变化版

相较于方法八少了循环时的条件判断,循环总次数一致,但分为两次循环
执行用时 :0 ms, 击败了100.00%的用户
内存消耗 :2.2 MB, 击败了100.00%的用户

func removeNthFromEnd9(head *ListNode, n int) *ListNode {
if n == 1 && head.Next == nil {
return nil } nodeI := head var nodeJ = &ListNode{
Val: 0, Next: head, } // nodeI先走n步 for i := 0; i < n; i++ {
nodeI = nodeI.Next } // nodeJ开始走,直到nodeI将剩余的链表走完,nodeJ就定位在n-1的位置 for ; nodeI != nil; nodeI = nodeI.Next {
nodeJ = nodeJ.Next } // 如果[n-1,n+1]这三个位置都有节点,则删除n位后,n+1位自动靠前 // 如果n+1的位置没有节点,则删除n后,n-1的nodeJ.Next需要改为nil,以链表,否则越界 if nodeJ.Next.Next == nil {
nodeJ.Next = nil } else {
*(nodeJ.Next) = *(nodeJ.Next.Next) } return head}

方法十 :双指针加入哨兵节点进阶

执行用时 :0 ms, 击败了100.00%的用户
内存消耗 :2.2 MB, 击败了100.00%的用户

func removeNthFromEnd10(head *ListNode, n int) *ListNode {
// 创建一个ListNode,并返回指针 nodeTemp := new(ListNode) // 连上head,此刻的nodeTemp为头部哨兵节点 nodeTemp.Next = head // 初始化快慢指针,与哨兵一致 nodeI, nodeJ := nodeTemp, nodeTemp // 循环n+1次,快指针定位到n+1的节点位置 for i := 0; i <= n; i++ {
nodeI = nodeI.Next } // 从n+1的位置起循环至链表结束,循环次数为(l+1)-(n+1) // 原链表节点数为l,因为增加了一个哨兵节点,所以总数为l+1 // 这样最终的循环次数为l-n,慢指针定位在l-n的位置,其实就是倒数n+1的位置 for ; nodeI != nil; nodeI = nodeI.Next {
nodeJ = nodeJ.Next } //将倒数n+1的位置的Next(即倒数n位置)替换成倒数n-1的位置,相当于将n位置删除 nodeJ.Next = nodeJ.Next.Next // 返回哨兵节点的下一个, // 一般情况下相当于返回head即可 // 但如果题目为[1]1,则表示第一个节点删除, // 如果此时返回head,则为[1] // 如果返回nodeTemp.Next,则为[] // 原理是当第一次循环(i),循环2次,i分别为0、1,此时nodeI定位在了最后的节点,也就是值为1 // 第二次循环由于不满足条件nodeI!=nil,因此不进行循环 // 然后nodeJ.Next=nodeJ.Next.Next,此时的nodeJ仍旧是在哨兵节点,因此,他的nodeJ.Next替换的值就是nil // 最后要返回nodeTemp.Next,则为nil return nodeTemp.Next}

解:

方法一耗费内存最多,其它的方法执行效率和内存占用都差不多。

利用双指针的方式最为高效,其中双指针外加哨兵节点的话,更能减少for循环中的判断语句,理论上应该更快,在方法十中已经没有条件语句了,速度更快。

这道题几个解题思路涉及如下知识点:

  1. 双指针
    一个快指针,一个慢指针。
    快慢之间的间隔根据题目要求来,比如快指针按照一次一层来走,慢指针一次二层;
    本题的快指针一次一层走了n步后,慢指针再出发,但也是一次一层。

参考:

《》


  1. 哨兵节点
    在这里插入图片描述
    在解本题时候有几种特别要注意情况:
  • 如果n和链表节点数量一致,也就是要删除第一个节点;
  • 如果n是1,即删除最后一个;
  • 如果n是1,节点也只有一个,即全部删除

在做本题时,经常遇到越界,不得不一个个排除,直到最后留下了上面的三种情况,开始是用if条件语句来过滤或者临时变量来过渡,直到最后发现了可以使用哨兵节点~~

参考:

《》


  1. 闭包递归
    在做字符串题目时对递归纠结了很长时间,因此现在遇到有分支、层级递进并且需要退回的操作时,第一反应就是递归,这道题很明显符合这种特征:
  • 分支层级:链表的自带属性~~分支和层级;
  • 层层递进:不像数组等,可以直接定位到索引位置,链表只能层层前进,才能到达需要的位置;
  • 需要退回:到达最后一个节点后再退回到n的节点位置;
  • 退回操作:每层退回时都可能存在操作,本题在退回时需要判断节点是否到达n位并进行删除操作。

We’ve written >100k lines of Go code and it has come up maybe once. That kind of frequency doesn’t suggest that a language change is warranted.

意思就是说,在实际编码中遇到需要这种特性的几率很小很小,所以没有必要直接在语言层面去支持,如果偶然遇到就使用替代方案吧

参考:

《》
《》

转载地址:http://otkpi.baihongyu.com/

你可能感兴趣的文章
5个你可能不知道的很棒CSS功能
查看>>
6种你应该知道的强大JavaScript对象方法
查看>>
在职场,这10句话千万不能在公司说,任何一句,就足以毁掉你的职场生涯!...
查看>>
11个你不应该错过的JavaScript库
查看>>
7个你可能不知道但有用的HTML属性
查看>>
JavaScript 执行环境与作用域、函数的创建和调用
查看>>
董明珠:没有人才,一切归零;没有道德,人才归零
查看>>
设计模式之策略模式
查看>>
31个很棒的JavaScript库
查看>>
给老板打工的心态,让你与成功无缘,建议你多看几遍
查看>>
【前端面试题】—30道常见React基础面试题(附答案)
查看>>
15个有用的React动画库,马上让你的项目变得高大上
查看>>
13个小秘诀帮你改善网页设计的视觉与用户体验效果
查看>>
当你想抱怨或辞职的时候,请看看这个故事
查看>>
9个你可能从未听说过的HTML功能
查看>>
20个改善网站设计的简单技巧
查看>>
有时候企业辞退你,真的与能力无关,这9条规则,请多看几遍
查看>>
20个与颜色相关的基本术语,你需要了解一下
查看>>
当Sass和新CSS功能发生冲突时的解决方案
查看>>
8个有用的表单构建工具,你一定要使用并收藏好
查看>>