前言
今天基本上一天都花在了链表上,用两种方法重写了昨天的设计链表,并写了如下的两道题,还得完善,时间原因只能留到明天了。
内容
一、环形链表
给你一个链表的头节点 head
,判断链表中是否有环。
如果链表中有某个节点,可以通过连续跟踪 next
指针再次到达,则链表中存在环。 为了表示给定链表中的环,评测系统内部使用整数 pos
来表示链表尾连接到链表中的位置(索引从 0 开始)。注意:pos
不作为参数进行传递 。仅仅是为了标识链表的实际情况。
如果链表中存在环 ,则返回 true
。 否则,返回 false
。
哈希表
首先,pos不作为参数传递,(不然就简单了)想到哈希表,存储访问过的结点,再遇到时,说明有环。否则就将该节点加入哈希表中。重复这一过程,直到我们遍历完整个链表即可。
//4ms 5.75MB 哈希表空间换时间,所以内存消耗还是挺高的
func hasCycle(head *ListNode) bool {
hashTable:=map[*ListNode]struct{}{}
for head!=nil{
if _,ok:=hashTable[head];ok{//; := ==
return true
}
hashTable[head]=struct{}{}//这行代码将空结构体 struct{} 存储到 hashTable[head] 中。由于结构体类型的零值是空结构体,这实际上并没有提供任何额外的信息,只是标记了 head 已经访问过。这是使用map的一个常见技巧,用于跟踪哪些元素已经被处理过。
head=head.Next
}
return false
}
快慢指针 (Floyd 判圈算法) (又称龟兔赛跑算法 )
假想「乌龟」和「兔子」在链表上移动,「兔子」跑得快,「乌龟」跑得慢。当「乌龟」和「兔子」从链表上的同一个节点开始移动时,如果该链表中没有环,那么「兔子」将一直处于「乌龟」的前方;如果该链表中有环,那么「兔子」会先于「乌龟」进入环,并且一直在环内移动。等到「乌龟」进入环时,由于「兔子」的速度快,它一定会在某个时刻与乌龟相遇,即套了「乌龟」若干圈。
我们可以根据上述思路来解决本题。定义两个指针,慢指针每次只移动一步,而快指针每次移动两步。//8ms 4.27MB
func hasCycle(head *ListNode) bool{
if head==nil||head.Next==nil{
return false
}
slow,fast:=head,head.Next
for fast!=nil&&fast.Next!=nil{
slow=slow.Next
fast=fast.Next.Next
if slow==fast{
return true
}
}
return false
}
func hasCycle(head *ListNode) bool {
if head == nil || head.Next == nil {
return false
}
slow, fast := head, head.Next
for fast != slow {
if fast == nil || fast.Next == nil {
return false
}
slow = slow.Next
fast = fast.Next.Next
}
return true
}
二、回文链表
给你一个单链表的头节点 head
,请你判断该链表是否为回文链表。如果是,返回 true
;否则,返回 false
。
将值复制到数组中后用双指针法
想到了用前后两个指针 但是写的时候发现,怎么让right指针指向链表最后一个结点呢 就算指向了,最后一个结点也没有前指针啊 怎么向中间遍历呢
//116ms 10.59MB
//总的时间复杂度:O(2n)=O(n)。空间复杂度:O(n),其中 n 指的是链表的元素个数,我们使用了一个数组列表存放链表的元素值
func isPalindrome(head *ListNode) bool {
arr:=[]int{}
for ;head!=nil;head=head.Next{
arr=append(arr,head.Val)
}
n:=len(arr)
for i,v:=range arr[:n/2]{
if v!=arr[n-i-1]{
return false
}
}
return true
}
快慢指针
避免使用 O(n)额外空间的方法就是改变输入。
将后半部分反转,与前半部分比较,最后将后半部分链表还原。
该方法虽然可以将空间复杂度降到 O(1),但是在并发环境下,该方法也有缺点。在并发环境下,函数运行时需要锁定其他线程或进程对链表的访问,因为在函数执行过程中链表会被修改。(还不懂)
//116ms 9.04MB
时间复杂度:O(n),其中 n 指的是链表的大小。
空间复杂度:O(1)。我们只会修改原本链表中节点的指向,而在堆栈上的堆栈帧不超过 O(1)
func reverseList(head *ListNode) *ListNode{
var pre,cur *ListNode=nil,head
for cur!=nil{
temp:=cur.Next
cur.Next=pre
pre=cur
cur=temp
}
return pre
}
func endOfFirstHalf(head *ListNode) *ListNode{
fast:=head
slow:=head
for fast.Next!=nil&&fast.Next.Next!=nil{
fast=fast.Next.Next
slow=slow.Next
}
return slow
}
func isPalindrome(head *ListNode)bool{
if head==nil{
return false
}
firstHalfEnd:=endOfFirstHalf(head)
secondHalfStart:=reverseList(firstHalfEnd.Next)
p1:=head
p2:=secondHalfStart
for p2!=nil{
if p1.Val!=p2.Val{
return false
}
p1=p1.Next
p2=p2.Next
}
firstHalfEnd.Next=reverseList(secondHalfStart)
return true
}
反转链表那块得随手就能写出来
在Go语言中,当你使用for fast.Next != nil && fast != nil
作为循环条件时,可能会出现"invalid memory address or nil pointer dereference"(无效的内存地址或空指针解引用)的错误。这是因为在这个条件判断中,你尝试在fast
可能为nil
的情况下访问它的Next
字段。
当一个指针为nil
时,尝试解引用它是未定义的行为,并且可能会导致运行时错误。所以,在尝试访问指针的字段之前,你应该首先确保指针不为nil
。
相反,如果你使用for fast != nil && fast.Next != nil
作为循环条件,这是正确的做法。在这个条件下,首先检查fast
是否为nil
,然后再检查fast.Next
是否为nil
。这样可以确保在尝试访问fast.Next
之前,fast
已经被正确地初始化并且不为nil
最后
学到了挺多。学习的成就感莫过于巩固基础+学到新知。