链表

  • 第1章 链表
    • 1.1 链表的逆序
      • 方法一:就地逆序
      • 方法二:递归法
      • 方法三:插入法
      • 引申练习:
        • (1)对不带头结点的单链表进行逆序
        • (2)从尾到头输出链表
    • 1.2 从无序链表中移除重复项
    • 1.3 计算两个单链表所代表的数之和
    • 1.4 对链表进行重新排序
    • 1.5 找出单链表中的倒数第k个元素
    • 1.6 检测一个较大的单链表是否有环
    • 1.7 把链表相邻元素翻转
    • 1.8 把链表以k个结点为一组进行翻转
    • 1.9 合并两个有序链表
    • 1.10 在只给定单链表中某个结点指针的情况下删除该结点
    • 1.11 判断两个单链表(无环)是否交叉
    • 1.12 如何展开链接链表

第1章 链表

  • 数据结构
    存储单元可以是不连续的;
    除了存储数据元素(数据域),还必须存储其直接后继元素的信息(指针域),这两部分的组合称为结点
    N个结点链在一起被称为链表。
  • 单链表:结点只包含其后继结点信息,1数据域+1指针域
    双链表:结点包含其前驱结点以及后继结点信息,1数据域+2指针域
  • 有头结点的单链表:在单链表的开始结点之前附设一个相同类型的结点,即头结点
    • 头结点的数据域:可以不存储任何信息
    • 头结点的指针域:存储指向开始结点的指针,即第一个元素结点的存储位置
  • 单链表中每个结点的地址都存储在其前驱结点的指针域中,
    对单链表中任何一个结点的访问只能从链表的头指针开始遍历。

1.1 链表的逆序

给定一个带头结点的单链表,请将其逆序。
例如,原来为head->1->2->3->4->5->6->7,逆序后为head->7->6->5->4->3->2->1。

方法一:就地逆序

  • 思路:
    用pre、cur、next三个指针通过不断后移来遍历链表,遍历时逐个完成每个结点的逆序。
    遍历链表时,先保存当前结点的后继结点信息,再修改当前结点指针域的指向,改为指向前驱结点;
    需要用一个指针变量(即pre)保存前驱结点的地址(用于上一条的修改),
    需要用一个指针变量(即next)保存后继结点的地址(为了还能找到后继结点)。

  • 实现代码+运行结果:

package mainimport ("fmt". "github.com/isdamir/gotype" //引入定义的数据结构
)func Reverse(node *LNode) {if node == nil || node.Next == nil {return}var pre *LNode    //前驱结点var cur *LNode    //当前结点next := node.Next //后继结点for next != nil {cur = next.Nextnext.Next = prepre = nextnext = cur}node.Next = pre
}func main() {head := &LNode{}fmt.Println("就地逆序")CreateNode(head, 8)PrintNode("逆序前:", head)Reverse(head)PrintNode("逆序后:", head)
}
就地逆序
逆序前:1 2 3 4 5 6 7
逆序后:7 6 5 4 3 2 1
  • 算法性能
    时间复杂度:O(n),因为需要对链表进行一次遍历,n为链表长度。
    空间复杂度:O(1),因为需要常数个额外的变量来保存当前结点的前驱结点与后继结点。

  • 补充说明:
    这些定义在引入的包中

//链表定义
type LNode struct {Data interface{}Next *LNode
}//创建链表
func CreateNode(node *LNode, max int) {cur := nodefor i := 1; i < max; i++ {cur.Next = &LNode{}cur.Next.Data = icur = cur.Next}
}
  • 晕鸭子笔记:
    遍历链表的地方一开始晕了,太久没学指针都整不会了,后来终于不晕了。
    原先:head->1->2->3->4->5->6->7
    结果:head->7->6->5->4->3->2->1
 next := node.Next //后继结点for next != nil {cur = next.Nextnext.Next = prepre = nextnext = cur}node.Next = pre

关于这一小段代码,下述分别逐行说明:我是真的菜晕鸭子怎么感觉又晕了

  1. 只能从头结点开始遍历链表,因此其中的node都是指传进来的头指针head,遍历前先用next指针记下后继结点,所以假设刚开始运行的话,此时next指针指向的是数据域为1的结点
  2. for循环开始遍历链表,结束条件是当next为空,即不存在后继结点时;
  3. 让cur指针指向next的下一个结点,即cur此时指向数据域为2的结点
  4. 实现逆序,完成指针域的修改,改为上一个结点的地址;此时刚开始pre还是空的,而next此时指向1,next.Next = pre执行后1结点的下一个结点变成了空结点(1的下一个原本是2,完成了指针域中地址的修改);
  5. 让pre指针记下前驱结点,next此时还是在指向1,因此pre指针此时指向数据域为1的结点
  6. 向后移动next指针,cur此时还是在指向2,因此next指针此时指向数据域为2的结点
  7. 第一遍循环结束时的状态是,pre指向1,next和cur都指向2
    第二遍,重复3~6,cur先后移指向3,结点2逆序,改为指向1,pre后移指向2,next后移指向3,第二遍循环结束时的状态是,pre指向2,next和cur都指向3
    ……
    到最后一遍循环进行之前的状态应该是pre指向6,next和cur都指向7;然后最后一遍循环,cur后移为空,结点7完成逆序改为指向6,pre后移指向7,next后移为空,退出循环,此时循环结束,最后状态是pre指向7,next和cur都为空
  8. 修改head头指针的指针域,头结点改为指向结点7,完成逆序。

话说怎么感觉怪怪的,这个代码里next和cur是不是写反了,怎么感觉理解上有点别扭,换一下试试

 cur := node.Next //当前结点从1开始for cur != nil {next = cur.Next //这多合理cur.Next = pre  //这我不就不晕了pre = cur       //太合理了吧cur = next}node.Next = pre

吼吼,虽然本质没变,但是这样看真的顺眼一些嗨!嗨嗨嗨!
让晕鸭子不晕的代码:(好耶好耶)

package mainimport ("fmt". "github.com/isdamir/gotype" //引入定义的数据结构
)func Reverse(node *LNode) {if node == nil || node.Next == nil {return}var pre *LNode   //前驱结点var next *LNode  //后继结点cur := node.Next //当前结点从1开始for cur != nil {next = cur.Next //这多合理cur.Next = pre  //这我不就不晕了pre = cur       //太合理了吧cur = next}node.Next = pre
}func main() {head := &LNode{}fmt.Println("就地逆序")CreateNode(head, 8)PrintNode("逆序前:", head)Reverse(head)PrintNode("逆序后:", head)
}

方法二:递归法

  • 思路:
    先逆序除第一个结点以外的子链表,即,将 1->2->3->4->5->6->7 变为 1->7->6->5->4->3->2
    再把结点1添加到逆序链表的后面,即,1->7->6->5->4->3->2 变为 7->6->5->4->3->2->1
    同理,逆序链表 2->3->4->5->6->7 时,先逆序子链表 3->4->5->6->7,
    即,将 2->3->4->5->6->7 变为 2->7->6->5->4->3;
    再实现整体的逆序,即,2->7->6->5->4->3 转换为 7->6->5->4->3->2;
    同理……
  • 代码实现+运行结果:
package mainimport ("fmt". "github.com/isdamir/gotype" //引入定义的数据结构
)func RecursiveReverseChild(node *LNode) *LNode {if node == nil || node.Next == nil {return node}newHead := RecursiveReverseChild(node.Next)node.Next.Next = nodenode.Next = nilreturn newHead
}func RecursiveReverse(node *LNode) {firstNode := node.Next//递归调用newHead := RecursiveReverseChild(firstNode)node.Next = newHead
}func main() {head := &LNode{}fmt.Println("递归法")CreateNode(head, 8)PrintNode("逆序前:", head)RecursiveReverse(head)PrintNode("逆序后:", head)
}
递归法
逆序前:1 2 3 4 5 6 7
逆序后:7 6 5 4 3 2 1
  • 算法性能
    时间复杂度:O(n),因为需要对链表进行一次遍历,n为链表长度。
    优点:思路比较直观,容易理解,不需要保存前驱结点的地址;
    缺点:算法实现难度较大,且由于递归需要不断调用自己,需要额外的压栈与弹栈操作,因此相比方法一性能有所下降。

  • 晕鸭子笔记:
    哎 方法二递归。。。晕死我算了。。zhu脑过载555,天知道我啥时候能学会自己写递归
    按照思路我理一下,
    要逆序 1->2->3->4->5->6->7 ,则要先逆 2->3->4->5->6->7 ,
    要逆序 2->3->4->5->6->7 ,则要先逆 3->4->5->6->7 ,
    要逆序 3->4->5->6->7,则要先逆 4->5->6->7 ,
    要逆序 4->5->6->7 ,则要先逆 5->6->7 ,
    要逆序 5->6->7 ,则要先逆 6->7 ,
    ……

麻了不懂,打出来看下

func RecursiveReverseChild(node *LNode) *LNode {fmt.Println("node:", node)if node == nil || node.Next == nil {return node}newHead := RecursiveReverseChild(node.Next)fmt.Println("newHead:", newHead, "node:", node)node.Next.Next = nodenode.Next = nilreturn newHead
}func RecursiveReverse(node *LNode) {firstNode := node.Next//递归调用newHead := RecursiveReverseChild(firstNode)node.Next = newHead
}
递归法
逆序前:1 2 3 4 5 6 7
node: &{1 0xc000004090}
node: &{2 0xc0000040a8}
node: &{3 0xc0000040c0}
node: &{4 0xc0000040d8}
node: &{5 0xc0000040f0}
node: &{6 0xc000004108}
node: &{7 <nil>}
newHead: &{7 <nil>} node: &{6 0xc000004108}
newHead: &{7 0xc0000040f0} node: &{5 0xc0000040f0}
newHead: &{7 0xc0000040f0} node: &{4 0xc0000040d8}
newHead: &{7 0xc0000040f0} node: &{3 0xc0000040c0}
newHead: &{7 0xc0000040f0} node: &{2 0xc0000040a8}
newHead: &{7 0xc0000040f0} node: &{1 0xc000004090}
逆序后:7 6 5 4 3 2 1

emmm,从头结点的下一个结点,也就是1开始,1~7都进了一次RecursiveReverseChild,所以返回值也有七次,最先返回的应该是最后调用的,也就是7;
这个时候因为7的指针域为空,函数直接return了没有打印newHead;

下一个返回的是6,即node为6,此时newHead为刚刚return过来的7,
那node.Next.Next是修改了7的指针域,改为指向6,(啊哈!)
node.Next是修改了6的指针域,改为指向空;
那么此时的状态是7->6了。(逆了哎)

下一个返回的是5,即node为5,此时newHead为刚刚6那边return过来的7,
那node.Next.Next是修改了6的指针域,改为指向5,
node.Next是修改了5的指针域,改为指向空;
那么此时的状态是7->6->5了。(!)

下一个返回的是4,即node为4,此时newHead为刚刚5那边return过来的7,
那node.Next.Next是修改了5的指针域,改为指向4,
node.Next是修改了4的指针域,改为指向空;
那么此时的状态是7->6->5->4了。

我不知道我在干嘛我好像在瞎说八道但是我好像不晕了?
……
那最后返回的是1,即node为1,此时newHead为刚刚2那边return过来的7,
那node.Next.Next是修改了2的指针域,改为指向1,
node.Next是修改了1的指针域,改为指向空;
那么此时的状态是7->6->5->4->3->2->1了。

在RecursiveReverse中node.Next = newHead再最后修改头指针的指针域,即 head->7->6->5->4->3->2->1 了!

麻了,我看懂了,但是这让我自己写出来的话我感觉还是不会啊55555

方法三:插入法

  • 思路:
    从链表的第二个结点开始,将遍历到的结点插入到头结点的后面,直到遍历结束。
    原链表为 head->1->2->3->4->5->6->7 时,
    在遍历到2时,将其插入到头结点后,链表变为 head->2->1->3->4->5->6->7 ;之后同理。

  • 代码实现:

package mainimport ("fmt". "github.com/isdamir/gotype" //引入定义的数据结构
)func InsertReverse(node *LNode) {if node == nil || node.Next == nil {return}var cur *LNode  //当前结点var next *LNode //后继结点cur = node.Next.Nextnode.Next.Next = nil //设置链表第一个结点为尾结点//把遍历到的结点插入到头结点的后面for cur != nil {next = cur.Nextcur.Next = node.Nextnode.Next = curcur = next}
}func main() {head := &LNode{}fmt.Println("插入法")CreateNode(head, 8)PrintNode("逆序前:", head)InsertReverse(head)PrintNode("逆序后:", head)
}
插入法
逆序前:1 2 3 4 5 6 7
逆序后:7 6 5 4 3 2 1
  • 算法性能
    时间复杂度:O(n),因为只需要对链表进行一次遍历,n为链表长度。
    与方法一相比:方法三不需要保存前驱结点的地址(少用一个变量);
    与方法二相比:方法三不需要递归地调用,效率更高。

引申练习:

(1)对不带头结点的单链表进行逆序

提示:方法二已经实现了递归的方法

  • 晕鸭子笔记
    我是可以的吗?那我写写试试

(2)从尾到头输出链表

  • 方法一:就地逆序+顺序输出
    首先对链表进行逆序,然后再顺序输出逆序后的链表。
    缺点:改变了链表原来的结构、
  • 方法二:逆序+顺序输出
    每当遍历到一个结点时,申请一块新的存储空间来存储这个结点的数据域,同时把新结点插入到新链表的头结点后。
    缺点:需要申请额外的存储空间。
  • 方法三:递归输出

1.2 从无序链表中移除重复项

1.3 计算两个单链表所代表的数之和

1.4 对链表进行重新排序

1.5 找出单链表中的倒数第k个元素

1.6 检测一个较大的单链表是否有环

1.7 把链表相邻元素翻转

1.8 把链表以k个结点为一组进行翻转

1.9 合并两个有序链表

1.10 在只给定单链表中某个结点指针的情况下删除该结点

1.11 判断两个单链表(无环)是否交叉

1.12 如何展开链接链表

【学习笔记】Go程序员面试算法宝典-第1章链表相关推荐

  1. python程序员面试算法宝典pdf-Python程序员面试笔试宝典

    本书是一本讲解Python程序员面试笔试的百科全书,在写法上,除了讲解如何解答Python程序员面试笔试问题以外,还引入了相关知识点辅以说明,让读者能够更加容易理解.本书将Python程序员面试笔试过 ...

  2. go程序员面试算法宝典 pdf_Go程序员面试算法宝典__目录

    前言 面试笔试经验技巧篇 经验技巧1 如何巧妙地回答面试官的问题2 经验技巧2 如何回答技术性的问题3 经验技巧3 如何回答非技术性问题5 经验技巧4 如何回答快速估算类问题5 经验技巧5 如何回答算 ...

  3. Java自学书籍推荐,java程序员面试算法宝典

    前言 说起来开始进行面试是年前倒数第二周,上午9点,我还在去公司的公交上,突然收到蚂蚁的面试电话,其实算不上真正的面试.面试官只是和我聊了下他们在做的事情(主要是做双十一这里大促的稳定性保障,偏中间件 ...

  4. 读书笔记-Java程序员面试笔试宝典--持续更新中

    文章目录 第四章 Java基础知识 4.1 基础概念 4.2 面向对象技术 4.3 关键字 4.4 基本类型与运算 4.5 字符串与数组 4.6 异常处理 4.7 输入输出流 4.8 Java平台与内 ...

  5. python程序员面试算法宝典 pdf_Python面试宝典之基础篇3

    Python面试宝典之基础篇-03 题目011:Python中为什么没有函数重载? 点评:C++.Java.C#等诸多编程语言都支持函数重载,所谓函数重载指的是在同一个作用域中有多个同名函数,它们拥有 ...

  6. 程序员面试笔试宝典学习笔记(一)

    以下是一些著名互联网企业的部分面试笔试真题以及考察知识点 本文的内容是对一些网址上的知识点介绍做了相应的整理 1.extern的作用 自己理解:应该需要区分extern在C语言中和C++语言中的作用, ...

  7. Java程序员面试笔试宝典-数据结构与算法(四)

    本文内容基于<Java程序员面试笔试宝典>,何昊.薛鹏.叶向阳著. 1. 链表 1.1 如何实现单链表的增删操作? 1.2 如何从链表中删除重复元素? 1.3 如何找出单链表中的倒数第k个 ...

  8. 我的新书——《PHP程序员面试笔试宝典》

    你好,是我琉忆. 一个文艺的PHP开发工程师. 很荣幸能够在这里带来我的第一本新书--<PHP程序员面试笔试宝典>. 一.创作过程 <PHP程序员面试笔试宝典>是我的第一本书, ...

  9. 程序员面试算法_程序员的前20个搜索和排序算法面试问题

    程序员面试算法 大家好,如果您正在准备编程工作面试或正在寻找新工作,那么您知道这不是一个容易的过程. 在您职业的任何阶段,您都必须幸运地接到电话并进行第一轮面试,但是在初学者方面,当您寻找第一份工作时 ...

最新文章

  1. 想学Python?快看看这个教程!收藏!
  2. BZOJ 1001: [BeiJing2006]狼抓兔子【最大流/SPFA+最小割,多解】
  3. android 解决setbackgrounddrawable过时
  4. ssh(Spring+Spring mvc+hibernate)——EmpServiceImpl.java
  5. php foreach ,PHP学习之foreach循环时加符号的说明
  6. Remoting: Server encountered an internal error
  7. python结构_Python 项目的结构
  8. java oracle 换行,oracle中Clob字段中的回车换行在jsp中展示的问题
  9. java用 拼接字符串的原理_Java String 拼接字符串原理详解
  10. linq group by 多个字段取值以及取出重复的数据
  11. 【图像跟踪】基于matlab GUI均值漂移图像跟踪【含Matlab源码 743期】
  12. 资源监视器中看不到磁盘队列等等问题的解决方案
  13. srvany.exe读取配置文件问题
  14. SpringBoot整合WebSocket实现聊天室系统
  15. JSP开发--MVC模式(三)
  16. 概率论与数理统计——多方法解决-双样本方差的F检验-Excel/SPSS
  17. 2021华为软挑部分答疑——哪些你有错却总是找不到的地方,我来带你找啦(含标准输入代码)
  18. 东方博宜OJ 1265 - 【入门】爱因斯坦的数学题
  19. java mongo hint_聊一聊mongodb中的 explain 和 hint
  20. html课堂笔记2.24

热门文章

  1. Python零基础爬取网页数据并导出Excel
  2. 机原自检——第8章 连杆机构及其设计
  3. 必备元器件知识1——电阻
  4. 「历时6个月招聘数据收集」致应届生的一份招聘市场报告
  5. html语言入门百度,【初学Html】百度的界面Html
  6. 在个股回测中,如何才能避开新股的一字涨停?
  7. 云计算应用场景有哪些?
  8. 2018最新 Vue实战POS系统
  9. Unity3D调用摄像头时的最高分辨率
  10. web2.0网站成功三要素