24.两两交换链表中的节点
虚拟头节点
使用虚拟头节点来同一处理逻辑,使用一个中间量记录上一节点的值。根据一个区间有两个元素要处理分为两个状态,一是第一个节点,二是第二个节点
func swapPairs(head *ListNode) *ListNode {
var dummyHead = new(ListNode)
dummyHead.Next = head // 虚拟头节点
preVal := 0 // 上一节点的值
curNode := dummyHead // 当前节点
count := -1 // 用来记录单双数
for curNode.Next != nil {
switch count % 2 {
case 0: // 当前为交换的两个节点的第一个
preVal = curNode.Val
curNode.Val = curNode.Next.Val
case 1: // 当前为交换的两个节点的第二个
curNode.Val = preVal
}
curNode = curNode.Next
count++
}
if count % 2 == 1 {
curNode.Val = preVal
}
return dummyHead.Next
}
19.删除链表的倒数第N个节点
两次遍历
先遍历一次获取长度,再根据长度来确定要删除的目标值位置,注意使用虚拟头指针处理头部问题
func removeNthFromEnd(head *ListNode, n int) *ListNode {
// 暴力
length := 0 // 链表长度
dummyHead := new(ListNode)
dummyHead.Next = head
current := dummyHead
for current.Next != nil {
current = current.Next
length++
}
current = dummyHead
for length-n > 0 {
// 当length == n 时当前节点的下一节点即要删除的节点
current = current.Next
length--
}
current.Next = current.Next.Next // 删除节点
return dummyHead.Next
}
一次遍历
空间换时间,用数组储存对应的节点,类似栈的入栈和出栈。注意使用虚拟头节点处理头节点情况
func removeNthFromEnd(head *ListNode, n int) *ListNode {
// 栈做法
nodeStack := make([]*ListNode, 0) // 存放链表节点的数组
dummyNode := new(ListNode)
dummyNode.Next = head // 虚拟头节点
currentNode := dummyNode // 当前的节点
for currentNode != nil {
nodeStack = append(nodeStack, currentNode)
currentNode = currentNode.Next
}
preNode := nodeStack[len(nodeStack)-n - 1]
preNode.Next = preNode.Next.Next
return dummyNode.Next
}
面试题 02.07. 链表相交
暴力
直接将一个链表存起来然后暴力遍历,时间复杂度为O(n2),空间复杂度为O(n)
func getIntersectionNode(headA, headB *ListNode) *ListNode {
// 存一个链表到数组中,再遍历
nodeList := make([]*ListNode, 0) // 存放链表的数组
for headA != nil{
nodeList = append(nodeList, headA)
headA = headA.Next
}
for headB != nil{
for _, node := range nodeList{
if node == headB{
return node
}
}
headB = headB.Next
}
return nil
}
双指针
先遍历获取两个链表的长度,计算长度差值再通过双指针使两个链表尾部对齐,移动双指针进行比较
func getIntersectionNode(headA, headB *ListNode) *ListNode {
// 先全遍历到底部获取长度,并判断是否相交
lenA, lenB := 0, 0 // 两个链表的长度
dummyA := &ListNode{0, headA}
dummyB := &ListNode{0, headB}
currentA := dummyA
for currentA.Next != nil{
lenA++
currentA = currentA.Next
}
currentB := dummyB
for currentB.Next != nil{
lenB++
currentB = currentB.Next
}
if currentA != currentB{ // 不相交
return nil
}
currentA, currentB = dummyA, dummyB
if lenA > lenB {
for count:= lenA - lenB; count > 0; count--{
currentA = currentA.Next
}
}else{
for count:= lenB - lenA; count > 0; count--{
currentB = currentB.Next
}
}
for currentA != currentB{
currentA = currentA.Next
currentB = currentB.Next
}
return currentA
}
142.环形链表II
暴力做法
直接使用一个数组来存放已经遍历过的元素,每次到达一个节点时都判断是否在数组中,若是,则返回该节点;若不是,则下一节点,直到遍历完或找到节点为止
时间复杂度O(n2)
空间复杂度O(n)
func detectCycle(head *ListNode) *ListNode {
// 暴力做法 使用一个数组存放已经遍历过的元素
nodeList := make([]*ListNode, 0) // 存放节点的列表
dummyHead := &ListNode{Val: 0, Next: head}
current := dummyHead
// 遍历链表并将当前节点放入列表中
for current.Next != nil {
for _, node := range nodeList {
if node == current.Next{
return node
}
}
nodeList = append(nodeList, current.Next)
current = current.Next
}
return nil
}
哈希
在上一种解法的基础上将遍历列表换成查询哈希表,时间复杂度变为O(n),空间复杂度仍为O(n)
func detectCycle(head *ListNode) *ListNode {
hashTable := make(map[*ListNode]bool) // 哈希表,存放已访问过的节点
current := head
for current != nil {
if _, ok := hashTable[current]; ok{
return current
}
hashTable[current] = true // 标记已访问
current = current.Next
}
return nil
}
双指针
使用快慢两个指针,通过数学推理得到解决方案(详见代码随想录讲解),有一说一,算法题还得看数学啊
func detectCycle(head *ListNode) *ListNode {
// 使用双指针来解决
dummyHead := &ListNode{Val: 0, Next: head}
slow, fast := dummyHead, dummyHead // 一快一慢两个指针
isMeet := false
for fast.Next != nil && fast.Next.Next != nil {
fast = fast.Next.Next // 走两步
slow = slow.Next // 走一步
if fast == slow {
// 相遇,但此时不是环的起始节点
isMeet = true
break
}
}
if !isMeet { // 没相遇
return nil
}
slow = dummyHead
for slow != fast {
slow = slow.Next
fast = fast.Next
}
return slow
}
总结
链表方面主要掌握三个思想:
1、虚拟头节点:将头部节点的边界问题统一化,方便编写代码
2、栈:单向链表无法逆向访问,那就开个数组存起来
3、快慢指针:方便访问上一节点