Ltd的算法大抄·链表I

leetcode:24.203.206.707

Posted by Ltd on Tuesday, March 7, 2023

这是算法记录的第三天,包含4道链表操作相关的题目,涵盖迭代和递归

leetcode:24.203.206.707

链表操作

主要涉及链表的迭代和递归

203.移除链表元素

给你一个链表的头节点 head 和一个整数 val ,请你删除链表中所有满足 Node.val == val 的节点,并返回 新的头节点

示例 1:

img

输入: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.设计链表

设计链表的实现。您可以选择使用单链表或双链表。单链表中的节点应该具有两个属性:valnextval 是当前节点的值,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:

img

输入:head = [1,2,3,4,5]
输出:[5,4,3,2,1]

示例 2:

img

输入: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:

img

输入: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
}