1.1链表基础理论
1.1.1 链表的分类
- 单链表(Singly Linked List):每个节点包含一个数据元素和一个指向下一个节点的指针。
- 双向链表(Doubly Linked List):每个节点包含一个数据元素和两个指针,一个指向前一个节点,一个指向下一个节点。
- 循环链表(Circular Linked List):链表的最后一个节点的指针指向链表的第一个节点,形成一个闭环。
- 双向循环链表:结合了双向链表和循环链表的特点。
1.1.2链表的特点
- 动态大小:链表的大小可以在运行时动态变化,不需要预先分配固定大小的存储空间。
- 内存非连续:链表的节点在内存中不必连续存储,每个节点的内存地址可以是任意的。
- 访问效率:访问链表中的元素需要从头节点开始遍历,因此访问特定元素的时间复杂度为O(n)。
- 插入和删除效率高:在链表中插入或删除元素只需要改变指针,不需要移动其他元素,因此这些操作的时间复杂度为O(1),前提是已知插入或删除位置的前一个节点。
2.1 移除链表中的元素
2.1.1移除(除头节点以外的)任意元素
LinkedList
类有一个remove
方法,它接受一个值作为参数,并从链表中移除所有值为该参数的节点。用这个方法遍历链表,使用两个指针current
和previous
来跟踪当前节点和前一个节点。如果当前节点的值等于要移除的值,它会更新previous.next
指针来跳过当前节点。如果当前节点的值不等于要移除的值,它会将previous
更新为current
,然后继续遍历链表。
2.1.2移除头节点元素
如果previous
是None
,表示要移除的是头节点,所以直接更新self.head
。
# Definition for singly-linked list.
class ListNode:
def __init__(self, val=0, next=None):
self.val = val
self.next = next
class Solution:
def removeElements(self, head, val):
"""
:type head: ListNode
:type val: int
:rtype: ListNode
"""
# 创建一个虚拟头部节点,它的 next 指向头节点
dummy = ListNode(0)
dummy.next = head
current = dummy
while current.next:
if current.next.val == val:
current.next = current.next.next # 移除当前节点
else:
current = current.next # 移动到下一个节点
return dummy.next # 返回虚拟头部节点的下一个节点,即新链表的头节点
# 示例
def print_list(node):
while node:
print(node.val, end=" -> ")
node = node.next
print("None")
# 创建链表 1 -> 2 -> 6 -> 3 -> 4 -> 5 -> 6
head = ListNode(1)
head.next = ListNode(2)
head.next.next = ListNode(6)
head.next.next.next = ListNode(3)
head.next.next.next.next = ListNode(4)
head.next.next.next.next.next = ListNode(5)
head.next.next.next.next.next.next = ListNode(6)
# 移除值为 6 的元素
sol = Solution()
new_head = sol.removeElements(head, 6)
# 输出新的链表
print_list(new_head) # 输出: 1 -> 2 -> 3 -> 4 -> 5 -> None
3.1设计链表
3.1.1获取第n个节点的值
获取链表中第n个节点的值通常需要从头节点开始遍历链表,直到到达第n个节点。
一般步骤:
- 初始化一个指针指向链表的头节点。(虚拟指针)
- 遍历链表,每次移动指针到下一个节点。
- 计数遍历过程中经过的节点。
- 当计数器达到n时,当前指针指向的节点就是第n个节点。
- 返回该节点的值。
如果n超出了链表的范围,通常返回-1或者None表示没有找到。
class ListNode:
def __init__(self, val=0, next=None):
self.val = val
self.next = next
class MyLinkedList:
def __init__(self):
self.dummy_head = ListNode()
self.size = 0
def get(self, index):
if index < 0 or index >= self.size:
return -1
current = self.dummy_head.next
for i in range(index):
current = current.next
return current.val
3.1.2头部插入节点
在链表的头部插入节点是一种常见的操作,它通常涉及更新头节点的指针,使其指向新插入的节点。新节点的指针则指向原来的头节点。整个过程不需要遍历整个链表,因此插入操作的时间复杂度为O(1)。
单链表头部插入节点的步骤:
- 创建一个新的节点,将给定的值赋给这个节点。
- 将新节点的
next
指针指向当前的头节点。 - 更新链表的头节点指针,使其指向新节点。
def addAtHead(self, val):
self.dummy_head.next = ListNode(val, self.dummy_head.next)
self.size += 1
3.1.3尾部插入节点
在链表的尾部插入节点是另一种常见的操作,它涉及遍历整个链表直到最后一个节点,然后更新最后一个节点的指针,使其指向新插入的节点。如果链表为空,新节点同时成为头节点。这个过程的时间复杂度为O(n)【因为最坏情况下需要遍历整个链表】
单链表尾部插入节点的步骤:
- 创建一个新的节点,将给定的值赋给这个节点。
- 如果链表为空(头节点为
None
),则新节点成为头节点。 - 如果链表不为空,遍历链表直到最后一个节点。
- 将最后一个节点的
next
指针指向新节点。
def addAtTail(self, val):
current = self.dummy_head
while current.next:
current = current.next
current.next = ListNode(val)
self.size += 1
3.1.4在第n个节点前插入
在链表的第n个节点前插入一个新节点是一个常见的操作,它涉及找到第n个节点,然后在这个节点前插入新节点。这个操作的时间复杂度为O(n)【因为最坏情况下需要遍历整个链表】
在链表的第n个节点前插入新节点的步骤:
- 创建一个新的节点,将给定的值赋给这个节点。
- 如果n为0,表示在头部插入新节点,将新节点的
next
指针指向当前的头节点,然后更新头节点指针。 - 如果n大于0且小于链表长度,找到第n-1个节点,将新节点插入到这个节点和第n个节点之间。
- 如果n大于链表长度,表示在尾部插入新节点。
def addAtIndex(self,index,val):
if index < 0 or index > self.size:
return
current = self.dummy_head
for i in range(index):
current = current.next
current.next = ListNode(val,current.next)
self.size += 1
3.1.5删除第n个节点
删除链表中的第n个节点是一个常见的操作,它涉及找到第n-1个节点,然后调整其next
指针,使其跳过第n个节点,直接指向第n+1个节点。
删除链表中第n个节点的步骤:
- 如果n为0,表示删除头节点,更新头节点指针为头节点的下一个节点。
- 如果n大于0,找到第n-1个节点。
- 如果找到的第n-1个节点存在,将它的
next
指针指向第n个节点的下一个节点,从而删除第n个节点。 - 如果n大于链表长度,不执行任何操作。
def deleteAtIndex(self, index):
if index < 0 or index >= self.size:
return
current = self.dummy_head
for i in range(index):
current = current.next
current.next = current.next.next
self.size -= 1
4.1递归概述
4.1.1递归基本理论
递归:a)在定义一个过程或函数时出现调用本过程或本函数的成分,称之为递归。
b)若调用自身,称之为直接递归。若过程或函数p调用过程或函数q,而q又调用p,称之为间接递归。 如果一个递归过程或递归函数中递归调用语句是最后一条执行语句,则称这种递归调用为尾递归。
递归允许一个函数调用自身来解决问题。递归通常用于解决那些可以分解为多个小而相似问题的情况。递归方法通常包括两个主要部分:
-
基本情况(Base Case):这是递归停止的条件。它定义了问题最简单的形式,可以直接解决而不需要进一步的递归调用。
-
递归步骤(Recursive Step):这是函数调用自身来解决问题的一部分。在每次递归调用中,问题都会向基本情况靠近一步。
4.1.2反转链表
迭代法
迭代法的基本思路是遍历链表,逐个节点地将其指向前一个节点。
- 初始化三个指针:
prev
(指向前一个节点,初始为None
)、curr
(当前节点,初始为链表的头节点)、next
(用于临时存储下一个节点)。 - 遍历链表,直到
curr
为None
(即到达链表末尾)。 - 在每次迭代中,先保存
curr.next
到next
,然后改变curr.next
指向prev
,接着将prev
和curr
向前移动一位。 - 最后,
prev
将指向新的头节点。class ListNode: def __init__(self, val=0, next=None): self.val = val self.next = next def reverseList(head): prev = None curr = head while curr: next = curr.next curr.next = prev prev = curr curr = next return prev
递归法
递归法的基本思路是将链表的后一部分先反转,然后逐个节点地将当前节点接到反转后的链表上。
- 定义一个递归函数,接收当前节点
head
作为参数。 - 基本情况:如果
head
为None
或head.next
为None
,则直接返回head
(即空链表或只有一个节点的链表不需要反转)。 - 递归步骤:递归调用反转
head.next
之后的链表,然后逐个节点将它们接到当前节点head
的后面。 - 最后,将
head.next
设置为None
,表示这是新的尾节点。class ListNode: def __init__(self, val=0, next=None): self.val = val self.next = next def reverseList(head): if not head or not head.next: return head new_head = reverseList(head.next) head.next.next = head head.next = None return new_head
4.1.3 两两交换链表中的节点
迭代法
- 首先检查链表是否为空或者只有一个节点,如果是,则不需要交换,直接返回原链表。
- 初始化一个哑节点(dummy node),它的
next
指向头节点,这样可以简化边界条件的处理。 - 使用两个指针
prev
和curr
,prev
指向哑节点,curr
指向当前需要交换的节点对的第一个节点。 - 遍历链表,每次交换
curr
和curr.next
节点,然后将prev
指向新的节点对。 - 重复上述步骤,直到
curr
为空或curr.next
为空。class ListNode: def __init__(self, val=0, next=None): self.val = val self.next = next def swapPairs(head): if not head or not head.next: return head dummy = ListNode(0) dummy.next = head prev = dummy while prev.next and prev.next.next: first = prev.next second = prev.next.next first.next = second.next second.next = first prev.next = second prev = first return dummy.next
递归法
- 定义一个递归函数,接收当前节点
head
作为参数。 - 基本情况:如果
head
为空或head.next
为空,则直接返回head
。 - 递归步骤:首先递归地交换
head.next.next
之后的链表,然后交换head
和head.next
。 - 最后,将
head.next.next
指向head
,以确保链表的连续性。class ListNode: def __init__(self, val=0, next=None): self.val = val self.next = next def swapPairs(head): if not head or not head.next: return head first = head second = head.next first.next = swapPairs(second.next) second.next = first return second
5.1练习(LeetCode)
707
“设计链表”(Design Linked List)
题目要求设计一个链表结构,支持以下操作:
get(index)
:获取链表中第index
个节点的值。如果索引无效,则返回-1。addAtHead(val)
:在链表的头部添加一个值为val
的节点。addAtTail(val)
:在链表的尾部添加一个值为val
的节点。addAtIndex(index, val)
:如果索引index
已存在,则将值为val
的节点插入到该索引处的前面;如果索引index
大于链表长度,则将val
添加到链表的尾部;如果index
小于0,则返回。deleteAtIndex(index)
:如果索引index
有效,则删除链表中索引为index
的节点。
2
“两数相加”(Add Two Numbers)
题目要求给定两个非负整数的反向链表表示形式,数字以链表中的节点序列的逆序存储,返回它们的和的链表表示形式。
206
“Reverse Linked List”(反转链表)
要求你定义一个函数,实现链表的反转,并返回反转后的链表的头节点。(递归法和迭代法均可使用,以下是递归法演示)
24
“Swap Nodes in Pairs”(两两交换链表中的节点)
要求你编写一个函数,实现两两交换链表中的节点,并返回交换后的链表的头节点。
(迭代法)
用递归法来解决本题(两两交换链表中的节点)【加分版】
递归的基本思想是将链表分成两部分,第一部分包含前两个节点,第二部分是剩余的链表。然后,递归地交换第二部分,最后将第一部分的两个节点与递归交换后的链表连接起来。