这是算法记录的第三天,包含4道链表操作相关的题目,涵盖迭代和递归
leetcode:24.203.206.707
链表操作
主要涉及链表的迭代和递归
203.移除链表元素
给你一个链表的头节点 head
和一个整数 val
,请你删除链表中所有满足 Node.val == val
的节点,并返回 新的头节点 。
示例 1:
输入:head = [1,2,6,3,4,5,6], val = 6
输出:[1,2,3,4,5]
示例 2:
输入:head = [], val = 1
输出:[]
示例 3:
输入:head = [7,7,7,7], val = 7
输出:[]
迭代
迭代就很简单直接,就是对整个链表的每个节点依次与目标值比较,若相同了删除就完事
细节:应该用pre.next
进行比较,这样才好删除— pre.next = pre.next.next
,同时可以设置虚拟头节点dummyHead
,使原头节点的操作和剩下的一样优化代码
// 迭代
func removeElements2(head *ListNode, val int) *ListNode {
//虚拟节点,便于head和后面的统一操作
dummyHead := &ListNode{Val: -1, Next: head}
//使用临时节点进行迭代
for tmp := dummyHead; tmp.Next != nil; {
if tmp.Next.Val == val {
//删除next节点
tmp.Next = tmp.Next.Next
} else {
//继续迭代
tmp = tmp.Next
}
}
//最后返回的是头指针
return dummyHead.Next
}
递归
链表的定义有递归的性质,所以常可以用递归来解决链表相关问题
而递归容易绕进去,所以一定要明确递归函数的定义(返回的是什么),而这里返回的就是已经判断完并删除后的新链表的下一个节点head.Next
所以这里只需考虑当前层的行为,判断当前节点的值是否是目标值val
,若是就删除掉。而不同于迭代,这里因为是递归直接返回head.Next
,当前节点就没了
// 递归
func removeElements1(head *ListNode, val int) *ListNode {
//表示到达链表尾部了
if head == nil {
return head
}
//递归,直到链表尾再递归返回,返回值是下一个节点本身或它的下一个节点(即当前节点的next.next)
head.Next = removeElements1(head.Next, val)
if head.Val == val {
//返回当前节点的next,作为前一个节点的next,即意味着删除此节点
return head.Next
}
//若不删除就返回当前节点
return head
}
707.设计链表
设计链表的实现。您可以选择使用单链表或双链表。单链表中的节点应该具有两个属性:val
和 next
。val
是当前节点的值,next
是指向下一个节点的指针/引用。如果要使用双向链表,则还需要一个属性 prev
以指示链表中的上一个节点。假设链表中的所有节点都是 0-index 的。
在链表类中实现这些功能:
- get(index):获取链表中第
index
个节点的值。如果索引无效,则返回-1
。 - addAtHead(val):在链表的第一个元素之前添加一个值为
val
的节点。插入后,新节点将成为链表的第一个节点。 - addAtTail(val):将值为
val
的节点追加到链表的最后一个元素。 - addAtIndex(index,val):在链表中的第
index
个节点之前添加值为val
的节点。如果index
等于链表的长度,则该节点将附加到链表的末尾。如果index
大于链表长度,则不会插入节点。如果index
小于0,则在头部插入节点。 - deleteAtIndex(index):如果索引
index
有效,则删除链表中的第index
个节点。
示例:
MyLinkedList linkedList = new MyLinkedList();
linkedList.addAtHead(1);
linkedList.addAtTail(3);
linkedList.addAtIndex(1,2); //链表变为1-> 2-> 3
linkedList.get(1); //返回2
linkedList.deleteAtIndex(1); //现在链表是1-> 3
linkedList.get(1); //返回3
思路
这道题相当于是熟悉链表的操作,注意的就是一些边界条件,排除非法的索引这些
具体操作的话也熟悉使用了虚拟头节点技巧
代码
type MyLinkedList struct {
dummyHead *ListNode
size int
}
func Constructor() MyLinkedList {
return MyLinkedList{dummyHead: &ListNode{}, size: 0}
}
func (m *MyLinkedList) Get(index int) int {
//排除非法index
if index < 0 || index >= m.size {
return -1
}
tmp := m.dummyHead
//索引为0的不是dummyHead,所以要到取等
for i := 0; i <= index; i++ {
tmp = tmp.Next
}
return tmp.Val
}
func (m *MyLinkedList) AddAtHead(val int) {
m.AddAtIndex(0, val)
}
func (m *MyLinkedList) AddAtTail(val int) {
m.AddAtIndex(m.size, val)
}
func (m *MyLinkedList) AddAtIndex(index int, val int) {
if index > m.size {
return
}
//索引小于0都视为在头节点之前加
if index < 0 {
index = 0
}
tmp := m.dummyHead
//当索引为0时表示在头节点之前加入,即dummyHead之后,所以这里不取等
for i := 0; i < index; i++ {
tmp = tmp.Next
}
//新节点内容
newNode := &ListNode{Val: val, Next: tmp.Next}
tmp.Next = newNode
m.size++
}
func (m *MyLinkedList) DeleteAtIndex(index int) {
if index < 0 || index >= m.size {
return
}
tmp := m.dummyHead
for i := 0; i < index; i++ {
tmp = tmp.Next
}
tmp.Next = tmp.Next.Next
m.size--
}
206.反转链表
给你单链表的头节点 head
,请你反转链表,并返回反转后的链表。
示例 1:
输入:head = [1,2,3,4,5]
输出:[5,4,3,2,1]
示例 2:
输入:head = [1,2]
输出:[2,1]
示例 3:
输入:head = []
输出:[]
迭代
这个就是先添加一个空指针作为新链表的尾,然后顺着链表的方向,不断地修改Next
指针的方向,修改之前得记录好下一个节点,否则修改后就没了
// 迭代
func reverseList1(head *ListNode) *ListNode {
var pre *ListNode = nil
cur := head
for cur != nil {
//记录下一个
next := cur.Next
//反转
cur.Next = pre
//迭代
pre = cur
cur = next
}
//返回最后一个节点的指针,即反转后的head
return pre
}
递归
和移除指定值的节点一样,先确定好递归函数的返回值是什么。
针对这道题,反转之后,本来是后继节点变成了新链表的前序节点,而最后的尾就变成了头,所以返回的一直都是一个东西——新链表的头节点,这个值对实际反转其实没有帮助,真正有用的是将head.Next
的下一个节点指向了head
实现了反转操作,将head.Next
指向了nil
,这个nil
在中间节点中会被覆盖(指向了新的后续节点,即原来的前序节点),而最后到了原本的head,这个nil
就保留下来正好是尾的指向
// 递归
func reverseList2(head *ListNode) *ListNode {
//排除空链表或链尾的next
if head == nil || head.Next == nil {
return head
}
//返回递归之后的head节点
last := reverseList2(head.Next)
//反转,将head.next的下一个指向head
head.Next.Next = head
//当前指向nil,若是头节点就是nil了,若是中间节点会在递归后指向反转后的节点
head.Next = nil
//返回递归后的头节点
return last
}
24.两两交换链表中的节点
给你一个链表,两两交换其中相邻的节点,并返回交换后链表的头节点。你必须在不修改节点内部的值的情况下完成本题(即,只能进行节点交换)。
示例 1:
输入:head = [1,2,3,4]
输出:[2,1,4,3]
示例 2:
输入:head = []
输出:[]
示例 3:
输入:head = [1]
输出:[1]
迭代
这个题迭代是很清晰的,首先要加上虚拟头节点,然后的话就是每两个交换一下位置就行,为了更清晰的表示,这里使用了三个next节点,前两个表示要交换的两个节点,第三个就是需要跟后面连接的,交换完就移动两步
注意边界条件,由于指针会指到pre.next.next.next
,所以需要确保pre.next.next
不为空,同理也是需要pre.next
不为空
// 迭代
func swapPairs1(head *ListNode) *ListNode {
dummyNode := &ListNode{Next: head}
pre := dummyNode
for pre.Next != nil && pre.Next.Next != nil {
//分别记录好后面三个的位置
next1 := pre.Next
next2 := pre.Next.Next
next3 := pre.Next.Next.Next
//前序节点指向交换的第二个
pre.Next = next2
//第二个指向第一个
next2.Next = next1
//第一个指向第三个
next1.Next = next3
//移动两个节点的位置
pre = pre.Next.Next
}
//返回头节点
return dummyNode.Next
}
递归
递归函数是还是一样,需要确定好递归函数是在干嘛,这里是返回的交换好了的,只需要拼接就好,所以用head.Next
去接收,因为head
就是交换后的第二个节点了;而newHead
是两个节点的前一个,交换操作就在于把head
放到newHead
之后。整个递归函数就在每一层递归中都交换了两个节点,然后返回前一个节点
// 递归
func swapPairs2(head *ListNode) *ListNode {
if head == nil || head.Next == nil {
return head
}
//交换的第二个节点,交换后是新的头节点
newHead := head.Next
//递归获取后面节点的头节点,传入是第三个节点
head.Next = swapPairs2(newHead.Next)
//交换,新的头节点的下一个是之前的头节点
newHead.Next = head
//返回新的头节点
return newHead
}