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

链表

  • 第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 main

import (
	"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.Next
		next.Next = pre
		pre = next
		next = 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 := node
	for i := 1; i < max; i++ {
		cur.Next = &LNode{}
		cur.Next.Data = i
		cur = 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.Next
		next.Next = pre
		pre = next
		next = 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 main

import (
	"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 main

import (
	"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 = node
	node.Next = nil
	return 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 = node
	node.Next = nil
	return 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 main

import (
	"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.Next
	node.Next.Next = nil //设置链表第一个结点为尾结点

	//把遍历到的结点插入到头结点的后面
	for cur != nil {
		next = cur.Next
		cur.Next = node.Next
		node.Next = cur
		cur = 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) 对不带头结点的单链表进行逆序

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

  • 晕鸭子笔记
    我是可以的吗?那我写写试试 ,首先要创建不带头结点的单链表
package main

import (
	"fmt"
	//. "github.com/isdamir/gotype" //引入定义的数据结构
)

//LNode 链表定义
type LNode struct {
	Data interface{}
	Next *LNode
}

//CreateNode 创建不带头结点的单链表
func CreateNode(node *LNode, max int) {
	cur := node
	cur.Data = 1
	for i := 2; i < max; i++ {
		cur.Next = &LNode{}
		cur.Next.Data = i
		//fmt.Println("cur:", cur)
		cur = cur.Next
	}
}

//PrintNode 打印不带头结点的单链表
func PrintNode(node *LNode) {
	for cur := node; cur != nil; cur = cur.Next {
		//fmt.Print(cur.Data, " ")
		fmt.Println("cur_node:", cur)
	}
	fmt.Println()
}

func main() {
	head := &LNode{}
	CreateNode(head, 8)
	PrintNode(head)
}

运行结果:创建的不带头结点的单链表

cur_node: &{1 0xc000096078}
cur_node: &{2 0xc000096090}
cur_node: &{3 0xc0000960a8}
cur_node: &{4 0xc0000960c0}
cur_node: &{5 0xc0000960d8}
cur_node: &{6 0xc0000960f0}
cur_node: &{7 <nil>}

这里可以直观的看下有无头结点的区别,下面是之前有头结点的单链表:

cur_node: &{<nil> 0xc000004090}
cur_node: &{1 0xc0000040c0}
cur_node: &{2 0xc0000040f0}
cur_node: &{3 0xc000004120}
cur_node: &{4 0xc000004150}
cur_node: &{5 0xc000004180}
cur_node: &{6 0xc0000041b0}
cur_node: &{7 <nil>}

得到了不带头结点的单链表后,现在要实现将它逆序,用它提示的方法二递归法
emm试了下,没有头结点之后递归完head指针还是指的1,但是1结点的指针域已经为空了……
要打印输出的话得是从结点7开始才得行……怎么改一下我

package main

import (
	"fmt"
	//. "github.com/isdamir/gotype" //引入定义的数据结构
)

//LNode 链表定义
type LNode struct {
	Data interface{}
	Next *LNode
}

//CreateNode 创建不带头结点的单链表
func CreateNode(node *LNode, max int) {
	cur := node
	cur.Data = 1
	for i := 2; i < max; i++ {
		cur.Next = &LNode{}
		cur.Next.Data = i
		cur = cur.Next
	}
}

//PrintNode 打印不带头结点的单链表
func PrintNode(info string, node *LNode) {
	fmt.Print(info)
	for cur := node; cur != nil; cur = cur.Next {
		fmt.Print(cur.Data, " ")
		//fmt.Println("cur_node:", cur)
	}
	fmt.Println()
}

func RecursiveReverseChild(node *LNode) *LNode {
	if node == nil || node.Next == nil {
		return node
	}
	newHead := RecursiveReverseChild(node.Next)
	node.Next.Next = node
	node.Next = nil
	return newHead
}

func main() {
	head := &LNode{}
	CreateNode(head, 8)
	fmt.Println("对不带头结点的单链表进行逆序-递归法")
	PrintNode("逆序前:", head)

	newHead := RecursiveReverseChild(head)
	PrintNode("逆序后:", newHead)
}
对不带头结点的单链表进行逆序-递归法
逆序前:1 2 3 4 5 6 7 
逆序后:7 6 5 4 3 2 1

呜呜呜,好耶,终于好了,在创建不带头结点的单链表那里卡了很久,老笨蛋了555;
最后直接先把第一个结点数据域赋值,循环里的代码就不用动了,最开始把 cur.Next.Data = i 改为了 cur.Data = i,然后引发了一系列晕鸭子反应。。。

(2) 从尾到头输出链表

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

import (
	"fmt"
	. "github.com/isdamir/gotype" //引入定义的数据结构
)

//ReversePrint 从尾到头输出链表-递归
func ReversePrint(node *LNode) {
	if node == nil {
		return
	}
	ReversePrint(node.Next)
	fmt.Print(node.Data, " ")
}

func main() {
	head := &LNode{}
	fmt.Println("从尾到头输出链表")
	CreateNode(head, 8)
	PrintNode("顺序输出:", head)
	fmt.Print("逆序输出:")
	ReversePrint(head.Next)
}
从尾到头输出链表
顺序输出:1 2 3 4 5 6 7 
逆序输出:7 6 5 4 3 2 1

我麻了三天就刚看完第一章第一小节我。。。以后不知道哪个大冤种公司会收我。。。

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

  • 题目描述:
    给定一个没有排序的链表,去掉其重复项,并保留原顺序。
    例如,链表 1->3->1->5->5->7,去掉重复项后为 1->3->5->7。

方法一:顺序删除

  • 思路
    通过双重循环直接在链表上进行删除操作。
    外层循环用一个指针从第一个结点开始遍历整个链表,
    内层循环用另一个指针遍历其余结点,将与外层循环遍历到的指针所指结点的数据域相同的结点删除。
  • 代码实现
package main

import (
	"fmt"
	. "github.com/isdamir/gotype" //引入定义的数据结构
)

//RemoveDup 顺序删除
func RemoveDup(head *LNode) {
	if head == nil || head.Next == nil {
		return
	}
	outerCur := head.Next //用于外层循环,指向链表第一个结点
	var innerCur *LNode   //用于内层循环遍历outerCur后面的结点
	var innerPre *LNode   //innerCur的前驱结点

	for ; outerCur != nil; outerCur = outerCur.Next {
		for innerCur, innerPre = outerCur.Next, outerCur; innerCur != nil; innerPre, innerCur = innerCur, innerCur.Next {
			if innerCur.Data == outerCur.Data {
				innerCur = innerCur.Next
				innerPre.Next = innerCur
			}
		}
	}
}

func main() {
	head := &LNode{}
	fmt.Println("删除重复结点-顺序删除")
	CreateNodeT(head)

	PrintNode("删除重复结点前:", head)
	RemoveDup(head)
	PrintNode("删除重复结点后:", head)
}

func CreateNodeT(node *LNode) {
	CreateNode(node, 7)
	node.Next.Next.Data = 3
	node.Next.Next.Next.Data = 1
	node.Next.Next.Next.Next.Data = 5
	node.Next.Next.Next.Next.Next.Next.Data = 7
}
删除重复结点-顺序删除
删除重复结点前:1 3 1 5 5 7 
删除重复结点后:1 3 5 7
  • 算法性能
    时间复杂度:O(n^2) ,因为采用双重循环对链表进行遍历,n为链表长度。
    空间复杂度:O(1),使用了常量个额外的指针变量。

  • 晕鸭子笔记
    这个代码跟给的不太一样,因为引入的包里没有CreateNodeT,不造CreateNodeT是个啥啊,就自己乱整了。。
    内层循环按书上写else的话就不会拖那么长,但是因为晕鸭子是直肠子所以就这样写了
    反正样例过了嘛嘻嘻 (还能指望晕鸭子写什么高级代码呢,不晕就万幸了)

方法二:递归法

  • 思路
    对于结点cur,首先递归地删除以cur.Next为首的子链表中重复的结点,
    接着从以cur.Next为首的子链表中找出与cur有着相同数据域的结点并删除。
  • 代码实现
package main

import (
	"fmt"
	. "github.com/isdamir/gotype" //引入定义的数据结构
)

//removeDupRecursionChild 递归法
func removeDupRecursionChild(head *LNode) *LNode {
	if head == nil || head.Next == nil {
		return head
	}
	var pointer *LNode
	cur := head
	//对以head.Next为首的子链表删除重复的结点
	head.Next = removeDupRecursionChild(head.Next)
	pointer = head.Next
	//找出以head.Next为首的子链表中与head结点相同的结点并删除
	for pointer != nil {
		if head.Data == pointer.Data {
			cur.Next = pointer.Next
			pointer = cur.Next
		} else {
			pointer = pointer.Next
			cur = cur.Next
		}
	}
	return head
}

func RemoveDupRecursion(head *LNode) {
	if head == nil {
		return
	}
	head.Next = removeDupRecursionChild(head.Next)
}

func main() {
	head := &LNode{}
	fmt.Println("删除重复结点-递归法")
	CreateNodeT(head)

	PrintNode("删除重复结点前:", head)
	removeDupRecursionChild(head)
	PrintNode("删除重复结点后:", head)
}

func CreateNodeT(node *LNode) {
	CreateNode(node, 10)
	node.Next.Next.Data = 3
	node.Next.Next.Next.Data = 1
	node.Next.Next.Next.Next.Data = 5
	node.Next.Next.Next.Next.Next.Next.Data = 7
}

删除重复结点-递归法
删除重复结点前:1 3 1 5 5 7 7 8 9 
删除重复结点后:1 3 5 7 8 9
  • 算法性能
    时间复杂度:O(n^2) ,该方法与方法一类似,本质上需要对链表进行双重遍历,n为链表长度。
    由于递归法会增加许多额外的函数调用,因此,理论上该方法效率比方法一低。

  • 晕鸭子笔记
    额我怎么好像……怎么每次感觉递归调用那句都有点懵 head.Next = removeDupRecursionChild(head.Next)
    呃也还是打出来看看吧

//removeDupRecursionChild 递归法
func removeDupRecursionChild(head *LNode) *LNode {
	fmt.Println("head:", head)
	if head == nil || head.Next == nil {
		return head
	}
	var pointer *LNode
	cur := head
	head.Next = removeDupRecursionChild(head.Next) //对以head.Next为首的子链表删除重复的结点
	pointer = head.Next
	fmt.Println("head:", head, "pointer:", pointer)

	//找出以head.Next为首的子链表中与head结点相同的结点并删除
	for pointer != nil {
		if head.Data == pointer.Data {
			cur.Next = pointer.Next
			pointer = cur.Next
		} else {
			pointer = pointer.Next
			cur = cur.Next
		}
	}
	return head
}
删除重复结点-递归法
删除重复结点前:1 3 1 5 5 7 
head: &{<nil> 0xc000004090}
head: &{1 0xc0000040a8}
head: &{3 0xc0000040c0}
head: &{1 0xc0000040d8}
head: &{5 0xc0000040f0}
head: &{5 0xc000004108}
head: &{7 <nil>}
head: &{5 0xc000004108} pointer: &{7 <nil>}
head: &{5 0xc0000040f0} pointer: &{5 0xc000004108}
head: &{1 0xc0000040d8} pointer: &{5 0xc000004108}
head: &{3 0xc0000040c0} pointer: &{1 0xc0000040d8}
head: &{1 0xc0000040a8} pointer: &{3 0xc0000040c0}
head: &{<nil> 0xc000004090} pointer: &{1 0xc0000040a8}
删除重复结点后:1 3 5 7

对不起我又开始最笨的手推了
还是先从头指针head进了七次递归的函数,没猫饼,然后倒着返回
最先返回的是最后一次调用,即结点7,由于head.Next为nil所以直接return了当前head给上一次调用的head.Next

倒数第二次的head是5因此head.Next = removeDupRecursionChild(head.Next)这个相当于没有变还是5->7,
此时head指向5,pointer指向7,判断这两个数据域是否相同,由于不相同所以cur和pointer一起后移,由于7已经是最后一个结点,pointer再后移会为空,因此结束for循环,return了指向倒数第二个5的head;

倒数第三个数还是5,此时head指向5(倒数第三个数),pointer指向5(倒数第二个),判断这两个数据域是否相同,由于相同,此时cur指向与head相同,修改cur所指结点的指针域让它指向pointer的下一个结点(即结点7),pointer再后移一个,因此,此时倒数第二个结点5已经被从链表中删除了;
【学习笔记】Go程序员面试算法宝典-第1章 链表_第1张图片

至此,cur指向5,pointer指向7,由于7已经是最后一个结点,pointer再后移会为空,因此结束for循环,return了指向5的head;

倒数第四个数为1,此时head指向1,pointer指向5,判断这两个数据域是否相同,由于不相同所以cur和pointer一起后移,此时cur指向5,pointer指向7
由于pointer不为空,因此循环继续判断head与pointer所指数据域是否相同,由于不相同所以cur和pointer一起后移,由于7已经是最后一个结点,pointer再后移会为空,因此结束for循环,return了指向1的head;

倒数第五个数为3,此时head指向3,pointer指向1,判断这两个数据域是否相同,由于不相同所以cur和pointer一起后移,此时cur指向1,pointer指向5
由于pointer不为空,因此循环继续判断head与pointer所指数据域是否相同,由于不相同所以cur和pointer一起后移,此时cur指向5,pointer指向7
由于7已经是最后一个结点,pointer再后移会为空,因此结束for循环,return了指向3的head;

倒数第六个数为1,此时head指向1,pointer指向3,判断这两个数据域是否相同,由于不相同所以cur和pointer一起后移,此时cur指向3,pointer指向1
由于pointer不为空,因此循环继续判断head与pointer所指数据域是否相同,由于相同,修改cur所指结点(3)的指针域让它指向pointer的下一个结点(即结点5),pointer再后移一个,因此,此时原本在倒数第四个的结点1已经被从链表中删除了;
至此,cur指向3,pointer指向5
由于pointer不为空,因此循环继续判断head与pointer所指数据域是否相同,由于1和5不相同所以cur和pointer一起后移,此时cur指向5,pointer指向7
由于7已经是最后一个结点,pointer再后移会为空,因此结束for循环,return了指向1的head;

此时重复的已经删完了,只剩一个头结点;头结点也要走一遍。

emmmm怎么感觉跟方法一相比好像差不多,就是倒过来了呢?

方法三:空间换时间

  • 思路
    1. 建立一个HashSet, HashSet中的内容为已经遍历过的结点内容,并将其初始化为空;
    2. 从头开始遍历链表中的所有结点,存在以下两种可能性:
      如果结点内容已经在HashSet中,则删除此结点,继续向后遍历
      如果结点内容不在HashSet中,则保留此结点,将此结点内容添加到HashSet中,继续向后遍历。
  • 代码实现
    书中没有给出代码,自己写写,看看微信读书笔记里别人写的。
package main

import (
	"fmt"
	. "github.com/isdamir/gotype" //引入定义的数据结构
)

func removeDuplicateNodes(head *LNode) *LNode {
	if head == nil {
		return head
	}
	hashSet := make(map[interface{}]bool)
	cur := head
	for cur.Next != nil {
		if hashSet[cur.Next.Data] {
			cur.Next = cur.Next.Next
		} else {
			hashSet[cur.Next.Data] = true
			cur = cur.Next
		}
		//cur = cur.Next 一开始放这里不对
	}
	return head
}

func main() {
	head := &LNode{}
	fmt.Println("删除重复结点-空间换时间")
	CreateNodeT(head)

	PrintNode("删除重复结点前:", head)
	removeDuplicateNodes(head)
	PrintNode("删除重复结点后:", head)
}

func CreateNodeT(node *LNode) {
	CreateNode(node, 7)
	node.Next.Next.Data = 3
	node.Next.Next.Next.Data = 1
	node.Next.Next.Next.Next.Data = 5
	node.Next.Next.Next.Next.Next.Next.Data = 7
}
删除重复结点-空间换时间
删除重复结点前:1 3 1 5 5 7 
删除重复结点后:1 3 5 7
  • 晕鸭子笔记
    照着思路写的时候觉得两种情况反正都是要继续向后遍历的,所以cur = cur.Next干脆放在外面,
    结果不对,很困惑,晕鸭子又晕了,看了半天才反应过来。。。
    如果是删除的话,已经少了一个结点,所以删除的情况cur就不用后移了,不然就多跳了一个。。。

引申练习:从有序链表中移除重复项

有序链表:链表中的各个结点按照结点数据域中的数据递增/递减有序连接。

上述介绍的从无序链表中移除重复项的方法,也适用于链表有序的情况,但是以上方法没有充分利用到链表有序这个条件,因此,算法的性能肯定不是最优的。
对于有序链表,由于链表具有有序性,因此,不需要对链表进行两次遍历。(如112334,543321,重复项只会挨在一起)

  • 思路:
    用cur指向链表第一个结点,此时需要分为以下两种情况讨论:
    如果 cur.Data == cur.Next.Data,那么删除cur.Next结点;
    如果 cur.Data != cur.Next.Data,那么cur=cur.Next,继续遍历其余结点。

  • 代码实现

package main

import (
	"fmt"
	. "github.com/isdamir/gotype" //引入定义的数据结构
)

func RemoveDup(head *LNode) {
	if head == nil {
		return
	}
	cur := head.Next //cur := head
	for cur.Next != nil {
		if cur.Data == cur.Next.Data {
			cur.Next = cur.Next.Next
		} else {
			cur = cur.Next
		}
	}
}

func main() {
	head := &LNode{}
	fmt.Println("删除有序链表的重复结点-顺序删除")
	CreateNodeT(head)

	PrintNode("删除重复结点前:", head)
	RemoveDup(head)
	PrintNode("删除重复结点后:", head)
}

func CreateNodeT(node *LNode) {
	CreateNode(node, 7)
	node.Next.Next.Data = 1
	node.Next.Next.Next.Data = 2
	node.Next.Next.Next.Next.Data = 3
	node.Next.Next.Next.Next.Next.Data = 3
	node.Next.Next.Next.Next.Next.Next.Data = 4
}
删除有序链表的重复结点-顺序删除
删除重复结点前:1 1 2 3 3 4 
删除重复结点后:1 2 3 4

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

  • 题目描述
    给定两个单链表,链表的每个结点代表一位数,计算两个数的和。
    例如,输入链表(3->1->5)和链表(5->9->2),输出 8->0->8,即513+295=808,注意个位数在链表头。

方法一:整数相加法

  • 思路
    分别遍历两个链表,求出两个链表所代表的整数的值,然后把这两个整数进行相加,最后把它们的和用链表的形式表示出来。
    优点:计算简单
    缺点:当链表所代表的的数很大的时候(超过long int的表示范围),就无法使用该方法了。

方法二:链表相加法

  • 思路
    对链表中的结点直接进行相加操作,把相加的和存储到新的链表中对应的结点中,同时还要记录结点相加后的进位。
    该方法需要注意的问题:

    1. 每组结点相加后需要记录其是否有进位;
    2. 如果两个链表的长度不同(长度分别为L1和L2,且L1
    3. 对链表所有结点都完成计算后,还需要考虑此时是否还有进位,如果有进位,则需要增加新的结点,此结点的数据域为1。
  • 代码实现

package main

import (
	. "github.com/isdamir/gotype" //引入定义的数据结构
)

func Add(h1 *LNode, h2 *LNode) *LNode {
	if h1 == nil || h1.Next == nil {
		return h2
	}
	if h2 == nil || h2.Next == nil {
		return h1
	}
	c := 0                 //记录进位
	sum := 0               //记录两个结点相加的值
	p1 := h1.Next          //遍历h1
	p2 := h2.Next          //遍历h2
	resultHead := &LNode{} //相加后链表头结点
	p := resultHead        //指向链表resultHead最后一个结点

	for p1 != nil && p2 != nil {
		p.Next = &LNode{} //指向新创建的存储相加和的结点
		sum = p1.Data.(int) + p2.Data.(int) + c
		p.Next.Data = sum % 10
		c = sum / 10 //进位
		p = p.Next
		p1 = p1.Next
		p2 = p2.Next
	}
	//链表h2比h1长,接下来只需要考虑h2剩余结点的值
	if p1 == nil {
		for p2 != nil {
			p.Next = &LNode{} //指向新创建的存储相加和的结点
			sum = p2.Data.(int) + c
			p.Next.Data = sum % 10 //两结点相加和
			c = sum / 10           //进位
			p = p.Next
			p2 = p2.Next
		}
	}
	//链表h1比h2长,接下来只需要考虑h1剩余结点的值
	if p2 == nil {
		for p1 != nil {
			p.Next = &LNode{} //指向新创建的存储相加和的结点
			sum = p1.Data.(int) + c
			p.Next.Data = sum % 10 //两结点相加和
			c = sum / 10           //进位
			p = p.Next
			p1 = p1.Next
		}
	}
	if c == 1 {
		p.Next = &LNode{}
		p.Next.Data = 1
	}
	return resultHead
}

//CreateNodeT 创建链表
func CreateNodeT(l1 *LNode, l2 *LNode) {
	cur := l1
	for i := 1; i < 7; i++ {
		cur.Next = &LNode{}
		cur.Next.Data = i + 2
		cur = cur.Next
	}
	cur = l2
	for i := 9; i > 4; i-- {
		cur.Next = &LNode{}
		cur.Next.Data = i
		cur = cur.Next
	}
}

func main() {
	head1 := &LNode{}
	head2 := &LNode{}
	CreateNodeT(head1, head2)

	PrintNode("Head1:", head1)
	PrintNode("Head2:", head2)
	PrintNode("相加后:", Add(head1, head2))
}
Head1:3 4 5 6 7 8 
Head2:9 8 7 6 5
相加后:2 3 3 3 3 9
  • 算法性能
    时间复杂度:O(n) ,因为需要对两个链表都进行遍历,n为较长的链表的长度。
    空间复杂度:O(n),因为计算结果保存在一个新的链表中。

  • 晕鸭子笔记
    微信读书的想法中有网友给出了另一思路,
    书中的这个是根据哪个链表更长后续又分开写了两个,这个代码是将先结束的链表的加数赋值为0。
    代码实现:

package main

import (
	. "github.com/isdamir/gotype" //引入定义的数据结构
)

func Add(h1 *LNode, h2 *LNode) *LNode {
	resultHead := &LNode{} //保存相加结果的链表的头结点
	p := resultHead        //遍历保存结果的链表

	var d1, d2, sum, c int
	p1 := h1.Next //遍历h1
	p2 := h2.Next //遍历h2

	for p1 != nil || p2 != nil {
		if p1 == nil {
			d1 = 0
		} else {
			d1 = p1.Data.(int)
		}

		if p2 == nil {
			d2 = 0
		} else {
			d2 = p2.Data.(int)
		}

		sum = d1 + d2 + c
		c = sum / 10 //进位
		p.Next = &LNode{}
		p.Next.Data = sum % 10
		p = p.Next

		if p1 != nil {
			p1 = p1.Next
		}
		if p2 != nil {
			p2 = p2.Next
		}
	}
	if c == 1 {
		p.Next = &LNode{}
		p.Next.Data = 1
	}
	return resultHead
}

//CreateNodeT 创建链表
func CreateNodeT(l1 *LNode, l2 *LNode) {
	cur := l1
	for i := 1; i < 7; i++ {
		cur.Next = &LNode{}
		cur.Next.Data = i + 2
		cur = cur.Next
	}
	cur = l2
	for i := 9; i > 4; i-- {
		cur.Next = &LNode{}
		cur.Next.Data = i
		cur = cur.Next
	}
}

func main() {
	head1 := &LNode{}
	head2 := &LNode{}
	CreateNodeT(head1, head2)

	PrintNode("Head1:", head1)
	PrintNode("Head2:", head2)
	PrintNode("相加后:", Add(head1, head2))
}
Head1:3 4 5 6 7 8 
Head2:9 8 7 6 5
相加后:2 3 3 3 3 9

1.4 对链表进行重新排序

  • 题目描述
    给定链表 L0->L1->L2…Ln-1->Ln,将链表重新排序为 L0->Ln->L1->Ln-1->L2->Ln-2
    要求:①在原来链表的基础上进行排序,即不能申请新的结点; ②只能修改结点的next域,不能修改数据域。

  • 思路

    1. 首先找到链表的中间结点;
    2. 对链表的后半部分子链表进行逆序;
    3. 把链表的前半部分子链表与后半部分的子链表进行合并,合并的思路为 分别从两个链表各取一个结点进行合并。
  • 代码实现

package main

import (
	. "github.com/isdamir/gotype" //引入定义的数据结构
)

//findMiddleNode 找出链表的中心结点,把链表从中间断成两个子链表
func findMiddleNode(head *LNode) *LNode {
	if head == nil || head.Next == nil {
		return head
	}
	fast := head //遍历链表的每次向前走两步
	slow := head //遍历链表的每次向前走一步
	slowPre := head
	for fast != nil && fast.Next != nil {
		slowPre = slow
		slow = slow.Next
		fast = fast.Next.Next
	}
	slowPre.Next = nil
	return slow
}

//reverse 逆序不带头结点的单链表
func reverse(head *LNode) *LNode {
	if head == nil || head.Next == nil {
		return head
	}
	var pre *LNode
	var next *LNode
	for head != nil {
		next = head.Next
		head.Next = pre
		pre = head
		head = next
	}
	return pre
}

func Recorder(head *LNode) {
	if head == nil || head.Next == nil {
		return
	}
	cur1 := head.Next //前半部分的链表第一个结点
	mid := findMiddleNode(head.Next)
	cur2 := reverse(mid) //后半部分链表逆序后的第一个结点
	var tmp *LNode
	//合并链表
	for cur1.Next != nil {
		tmp = cur1.Next
		cur1.Next = cur2
		cur1 = tmp
		tmp = cur2.Next
		cur2.Next = cur1
		cur2 = tmp
	}
	cur1.Next = cur2
}

func main() {
	head := &LNode{}
	CreateNode(head, 8)

	PrintNode("排序前:", head)
	Recorder(head)
	PrintNode("排序后:", head)
}
排序前:1 2 3 4 5 6 7 
排序后:1 7 2 6 3 5 4

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

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

1.7 把链表相邻元素翻转

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

1.9 合并两个有序链表

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

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

1.12 如何展开链接链表

你可能感兴趣的:(go,go)