标签(题目类型):链表
题目描述
给你一个链表,两两交换其中相邻的节点,并返回交换后链表的头节点。你必须在不修改节点内部的值的情况下完成本题(即,只能进行节点交换)。
示例 1:
输入:head = [1,2,3,4]
输出:[2,1,4,3]
示例 2:
输入:head = []
输出:[]
示例 3:
输入:head = [1]
输出:[1]
提示:
链表中节点的数目在范围 [0, 100] 内
0 <= Node.val <= 100
思路及实现
方式一:迭代法(使用哑节点)
思路
迭代法通过遍历链表,在遍历过程中将相邻的节点进行交换。为了处理链表头部节点可能也参与交换的情况,通常会在链表头部添加一个哑节点(dummy node),这样就不需要单独处理头节点的情况。
Java版本
/**
* Definition for singly-linked list.
* public class ListNode {
* int val;
* ListNode next;
* ListNode(int x) { val = x; }
* }
*/
public class Solution {
public ListNode swapPairs(ListNode head) {
// 创建一个虚拟头节点,方便处理头节点交换的情况
ListNode dummy = new ListNode(0);
dummy.next = head;
// prev 用于指向当前要交换节点的前一个节点
ListNode prev = dummy;
while (head != null && head.next != null) {
// next 用于指向当前要交换的下一个节点
ListNode next = head.next;
// 交换当前节点和下一个节点的位置
prev.next = next;
head.next = next.next;
next.next = head;
// 更新指针位置
prev = head;
head = head.next;
}
// 返回交换后的链表
return dummy.next;
}
}
说明:
- 首先创建了一个虚拟头节点,其next指向原链表的头节点。这样做的好处是,当需要交换头节点时,我们可以像交换其他节点一样处理,无需特殊处理头节点的情况。
- prev指针用于指向当前要交换节点的前一个节点,初始化为虚拟头节点。
- head指针指向当前要交换的节点,初始化为原链表的头节点。
- next指针指向当前要交换的下一个节点。
- 在每次迭代中,我们先交换head和next的位置,然后更新prev、head和next的指向,直到遍历完整个链表。
C语言版本
#include <stdlib.h>
typedef struct ListNode {
int val;
struct ListNode *next;
} ListNode;
ListNode* swapPairs(ListNode* head) {
// 创建一个虚拟头节点
ListNode* dummy = (ListNode*)malloc(sizeof(ListNode));
dummy->next = head;
// prev 用于指向当前要交换节点的前一个节点
ListNode* prev = dummy;
while (head != NULL && head->next != NULL) {
// next 用于指向当前要交换的下一个节点
ListNode* next = head->next;
// 交换当前节点和下一个节点的位置
prev->next = next;
head->next = next->next;
next->next = head;
// 更新指针位置
prev = head;
head = head->next;
}
// 返回交换后的链表
return dummy->next;
}
说明:
C语言版本的实现与Java版本类似,只是由于C语言需要手动管理内存,所以在创建虚拟头节点时需要使用
malloc
分配内存。
Python3版本
class ListNode:
def __init__(self, x):
self.val = x
self.next = None
class Solution:
def swapPairs(self, head: ListNode) -> ListNode:
# 创建一个虚拟头节点
dummy = ListNode(0)
dummy.next = head
# prev 用于指向当前要交换节点的前一个节点
prev = dummy
while head and head.next:
# next 用于指向当前要交换的下一个节点
next_node = head.next
# 交换当前节点和下一个节点的位置
prev.next = next_node
head.next = next_node.next
next_node.next = head
# 更新指针位置
prev = head
head =head.next
# 返回交换后的链表
return dummy.next
说明:
Python版本的实现与Java版本非常相似,只是语法上有些差异。Python不需要显式声明变量类型,且使用
None
表示空指针。
Golang版本
package main
import (
"fmt"
)
type ListNode struct {
Val int
Next *ListNode
}
func swapPairs(head *ListNode) *ListNode {
// 创建一个虚拟头节点
dummy := &ListNode{0, head}
// prev 用于指向当前要交换节点的前一个节点
prev := dummy
for head != nil && head.Next != nil {
// next 用于指向当前要交换的下一个节点
nextNode := head.Next
// 交换当前节点和下一个节点的位置
prev.Next = nextNode
head.Next = nextNode.Next
nextNode.Next = head
// 更新指针位置
prev = head
head = head.Next
}
// 返回交换后的链表
return dummy.Next
}
func main() {
// 示例链表 1->2->3->4
head := &ListNode{1, &ListNode{2, &ListNode{3, &ListNode{4, nil}}}}
// 交换链表中的节点
newHead := swapPairs(head)
// 打印交换后的链表
for newHead != nil {
fmt.Printf("%d ", newHead.Val)
newHead = newHead.Next
}
}
说明:
Golang版本的实现与其他语言版本类似,只是语法和包结构有所不同。在Golang中,我们通常将函数和方法定义在包中,并在
main
函数中调用这些函数来执行程序。
复杂度分析
- 时间复杂度:O(n),其中n是链表的长度。我们需要遍历链表中的每个节点一次。
- 空间复杂度:O(1)。我们只使用了几个指针变量来跟踪链表中的节点,没有使用额外的数据结构来存储节点。
方式二:迭代法(不使用哑节点)
思路
不使用哑节点,而是直接在原链表上进行操作。这种方法稍微复杂一些,因为需要处理头节点可能发生变化的情况。
代码实现
Java版本
public class Solution {
public ListNode swapPairs(ListNode head) {
// 如果链表为空或只有一个节点,则无需交换
if (head == null || head.next == null) {
return head;
}
// 记录头节点的前一个节点(此处为空,因为头节点没有前一个节点)
ListNode prev = null;
// 当前节点
ListNode curr = head;
while (curr != null && curr.next != null) {
// 记录当前节点对的第二个节点
ListNode nextNode = curr.next;
// 如果不是头节点,则处理前一个节点与第二个节点的连接
if (prev != null) {
prev.next = nextNode;
}
// 交换节点对
curr.next = nextNode.next;
nextNode.next = curr;
// 更新prev和curr到下一个需要处理的节点对
prev = curr;
curr = curr.next;
}
// 返回新的头节点(原头节点的下一个节点)
return head.next;
}
}
C语言版本
#include <stdio.h>
#include <stdlib.h>
typedef struct ListNode {
int val;
struct ListNode *next;
} ListNode;
ListNode* swapPairs(ListNode* head) {
ListNode *prev = NULL;
ListNode *curr = head;
while (curr != NULL && curr->next != NULL) {
ListNode *next = curr->next;
// 进行节点交换
curr->next = next->next;
if (prev != NULL) {
prev->next = next;
}
next->next = curr;
// 更新prev和curr
prev = curr;
curr = curr->next;
}
// 如果prev不为空,说明head已经指向了第二个节点
return prev != NULL ? head->next : head;
}
void printList(ListNode* head) {
ListNode *current = head;
while (current != NULL) {
printf("%d ", current->val);
current = current->next;
}
printf("\n");
}
int main() {
// 创建示例链表:1 -> 2 -> 3 -> 4
ListNode *one = (ListNode*)malloc(sizeof(ListNode));
ListNode *two = (ListNode*)malloc(sizeof(ListNode));
ListNode *three = (ListNode*)malloc(sizeof(ListNode));
ListNode *four = (ListNode*)malloc(sizeof(ListNode));
one->val = 1;
two->val = 2;
three->val = 3;
four->val =
Python版本
class ListNode:
def __init__(self, val=0, next=None):
self.val = val
self.next = next
class Solution:
def swapPairs(self, head: ListNode) -> ListNode:
# 如果链表为空或只有一个节点,则无需交换
if not head or not head.next:
return head
# 当前节点
curr = head
# 当链表至少有两个节点时,进行交换
while curr and curr.next:
# 记录当前节点对的第二个节点
next_node = curr.next
# 处理前一个节点与第二个节点的连接(如果是头节点,则不处理)
if curr != head:
prev.next = next_node
# 交换节点对
curr.next = next_node.next
next_node.next = curr
# 更新prev和curr到下一个需要处理的节点对
prev = curr
curr = curr.next
# 返回新的头节点(原头节点的下一个节点)
return head.next if head != curr else head
Go版本
package main
import "fmt"
type ListNode struct {
Val int
Next *ListNode
}
func swapPairs(head *ListNode) *ListNode {
dummy := &ListNode{Next: head} // 创建一个哑节点简化操作
prev := dummy
curr := head
for curr != nil && curr.Next != nil {
next := curr.Next
curr.Next = next.Next
next.Next = curr
prev.Next = next
prev = curr
curr = curr.Next
}
return dummy.Next // 返回哑节点后的链表
}
func printList(head *ListNode) {
for head != nil {
fmt.Printf("%d ", head.Val)
head = head.Next
}
fmt.Println()
}
func main() {
// 创建示例链表:1 -> 2 -> 3 -> 4
one := &ListNode{Val: 1}
two := &ListNode{Val: 2}
three := &ListNode{Val: 3}
four := &ListNode{Val: 4}
one.Next = two
two.Next = three
three.Next = four
// 调用swapPairs函数交换节点对
newHead := swapPairs(one)
// 打印结果链表
printList(newHead)
}
复杂度分析
- 时间复杂度:O(n),其中n是链表的长度。每个节点最多被访问两次。
- 空间复杂度:O(1)。除了几个用于交换的指针变量外,没有使用额外的数据结构。
这种方法与使用哑节点的方法相比,稍微复杂一些,因为需要特别处理头节点的情况。但是,如果不希望使用额外的哑节点,这种方法是一个不错的选择。
方式三:递归法
思路
递归法的主要思路是,先递归地交换链表的后半部分,然后处理当前节点和下一个节点的交换,最后返回交换后的链表。
代码实现
Java版本
public class Solution {
public ListNode swapPairs(ListNode head) {
// 递归终止条件:当链表为空或只有一个节点时,无需交换,直接返回
if (head == null || head.next == null) {
return head;
}
// 递归交换后半部分链表
ListNode newHead = swapPairs(head.next.next);
// 处理当前节点和下一个节点的交换
ListNode next = head.next;
next.next = head;
head.next = newHead;
// 返回交换后的链表
return next;
}
}
说明:
- 递归终止条件是当链表为空或只有一个节点时,此时无需交换,直接返回原链表。
- 递归调用
swapPairs(head.next.next)
来交换后半部分链表,并将返回的新头节点保存在newHead
中。- 处理当前节点和下一个节点的交换,并更新指针指向。
C语言版本
struct ListNode* swapPairs(struct ListNode* head) {
// 递归终止条件
if (head == NULL || head->next == NULL) {
return head;
}
// 递归交换后半部分链表
struct ListNode* newHead = swapPairs(head->next->next);
// 处理当前节点和下一个节点的交换
struct ListNode* next = head->next;
next->next = head;
head->next = newHead;
// 返回交换后的链表
return next;
}
说明:
C语言版本的递归实现与Java版本相似,只是语法上有所差异。
Python3版本
class Solution:
def swapPairs(self, head: ListNode) -> ListNode:
# 递归终止条件
if not head or not head.next:
return head
# 递归交换后半部分链表
new_head = self.swapPairs(head.next.next)
# 处理当前节点和下一个节点的交换
next_node = head.next
next_node.next = head
head.next = new_head
# 返回交换后的链表
return next_node
说明:
Python版本的递归实现与Java版本非常相似,只是语法上有所差异。
复杂度分析
- 时间复杂度:O(n),其中n是链表的长度。递归函数会遍历链表中的每个节点一次。
- 空间复杂度:O(n),其中n是链表的长度。递归函数需要用到栈空间,最坏情况下,递归深度为n,因此空间复杂度为O(n)。
需要注意的是,递归方法的空间复杂度并不是最优的,因为它使用了额外的栈空间。在实际应用中,如果链表较长,可能会导致栈溢出。因此,在实际编程中,通常更倾向于使用迭代方法。
总结
方式 | 优点 | 缺点 | 时间复杂度 | 空间复杂度 | 其他 |
---|---|---|---|---|---|
迭代法(使用哑节点) | 代码简洁,易于理解 | 需要额外的哑节点 | O(n) | O(1) | 适用于链表头部处理 |
迭代法(不使用哑节点) | 不需要额外空间(除了变量) | 处理链表头部时稍复杂 | O(n) | O(1) | 需特别注意链表头部的变化 |
递归法 | 代码优雅,易于理解 | 可能存在栈溢出风险,空间效率不如迭代法 | O(n) | O(n)(递归栈空间) | 适用于链表长度较短的情况 |
在以上三种方式中,迭代法(不使用哑节点)和递归法都是直接对原链表进行操作,不需要额外的节点空间(除了必要的变量)。然而,递归法由于使用系统栈来保存递归调用的状态,空间复杂度为O(n),在链表较长时可能会导致栈溢出。迭代法则没有这个问题,空间复杂度为O(1)。使用哑节点的方式可以简化链表头部的处理,使得代码更简洁,但会占用额外的空间(尽管这个空间是固定的,不随链表长度变化)。
相似题目
以下是与链表操作相关的一些相似题目及其题目名:
相似题目 | 难度 | 链接 |
---|---|---|
移除链表元素 | 中等 | 力扣-203 |
合并两个有序链表 | 中等 | 力扣-21 |
链表中倒数第k个节点 | 中等 | 力扣-19 |
链表的中间节点 | 简单 | 力扣-876 |
环形链表 | 中等 | 力扣-141 |
链表的回文结构 | 简单 | 力扣-234 |
分割链表 | 中等 | 力扣-143 |
复制带随机指针的链表 | 中等 | 力扣-138 |
两个链表的第一个公共节点 | 中等 | 力扣-142 |
有序链表转换二叉搜索树 | 中等 | 力扣-109 |
这些题目涵盖了链表操作中的多个方面,包括节点的插入、删除、反转、查找、合并等,有助于深入理解链表数据结构及其相关算法。通过解决这些题目,可以进一步提升链表操作的熟练度和解决问题的能力。