文章目录
一、链表基础知识
参考链接:链表基础知识
以下内容参考这该链接。
1.1链表定义
链表(Linked List):一种线性表数据结构。它使用一组任意的存储单元(可以是连续的,也可以是不连续的),来存储一组具有相同类型
的数据。
可分为单向链表、双向链表、循环链表等等。
1.2链表的基本结构和操作
链表是由节点通过next链接而构成的,所以先来定义一个简单的链节点类,即 ListNode 类。ListNode 类使用成员变量 val表示数据元素的值,使用指针变量 next 表示后继指针。
# 链节点类
class ListNode:
def __init__(self, val=0, next=None):
self.val = val
self.next = next
# 链表类
class LinkedList:
def __init__(self):
self.head = None
1.2.1 建立一个线性链表
- 从所给线性表的第 1 个数据元素开始依次获取表中的数据元素。
- 每获取一个数据元素,就为该数据元素生成一个新节点,将新节点插入到链表的尾部。
- 插入完毕之后返回第 1 个链节点的地址。
建立一个线性链表的时间复杂度为 ,n 为线性表长度。
# 根据 data 初始化一个新链表
def create(self, data):
self.head = ListNode(0) #创建节点
cur = self.head
for i in range(len(data)):
node = ListNode(data[i]) #创建节点
cur.next = node
cur = cur.next
1.2.2 求线性链表的长度
- 让指针变量 cur 指向链表的第 1 个链节点。
- 然后顺着链节点的 next 指针遍历链表,指针变量 cur 每指向一个链节点,计数器就做一次计数。
- 等 cur 指向为空时结束遍历,此时计数器的数值就是链表的长度,将其返回即可。
求线性链表的长度操作的问题规模是链表的链节点数 n,基本操作是 cur 指针的移动,操作的次数为 n,因此算法的时间复杂度为 。
# 获取链表长度
def length(self):
count = 0
cur = self.head
while cur:
count += 1
cur = cur.next
return count
其实也可以使用一个self.size变量去储存链表的长度。该操作就变成O(1)了。
1.2.3 查找元素
在链表中查找值为 val 的位置:链表不能像数组那样进行随机访问,只能从头节点 head 开始,沿着链表一个一个节点逐一进行查找。如果查找成功,返回被查找节点的地址。否则返回 None。
查找元素操作的问题规模是链表的长度 n,而基本操作是指针 cur 的移动操作,所以查找元素算法的时间复杂度为 。
# 查找元素
def find(self, val):
cur = self.head
while cur:
if val == cur.val:
return cur
cur = cur.next
return None
1.2.4 插入元素
- 链表头部插入元素:在链表第 1 个链节点之前插入值为 val 的链节点。
- 链表尾部插入元素:在链表最后 1 个链节点之后插入值为 val 的链节点。
- 链表中间插入元素:在链表第 i 个链节点之前插入值为 val 的链节点。
①链表头部插入元素
因为在链表头部插入链节点与链表的长度无关,所以该算法的时间复杂度为O(1)。
# 头部插入元素
def insertFront(self, val):
node = ListNode(val)
node.next = self.head
self.head = node
如果构造的链表有哨兵节点,就需要让哨兵节点的next指向新创建的节点。
②链表尾部插入元素
- 先创建一个值为 val 的链节点 node。
- 使用指针 cur 指向链表的头节点 head。
- 通过链节点的 next 指针移动 cur 指针,从而遍历链表,直到 cur.next == None。
- 令 cur.next 指向将新的链节点 node。
def insertRear(self, val):
node = ListNode(val)
cur = self.head
while cur.next:
cur = cur.next
cur.next = node
③中间插入元素
- 使用指针变量 cur 和一个计数器 count。令 cur 指向链表的头节点,count 初始值赋值为 0。
- 沿着链节点的 next 指针遍历链表,指针变量 cur 每指向一个链节点,计数器就做一次计数。
- 当 count == index - 1 时,说明遍历到了第 index - 1 个链节点,此时停止遍历。
- 创建一个值为 val 的链节点 node。
- 将 node.next 指向 cur.next。
- 然后令 cur.next 指向 node。
这里注意需要在所插入位置的前一位停下来,即index-1.
# 中间插入元素
def insertInside(self, index, val):
count = 0
cur = self.head
while cur and count < index - 1:
count += 1
cur = cur.next
if not cur:
return 'Error'
node = ListNode(val)
node.next = cur.next
cur.next = node
1.2.5 链表中间修改、删除元素
知道了如何插入元素后,修改和删除也同理。都是先通过head节点遍历到所需位置,再进行增删查改的操作。
二、707. 设计链表
题目描述:
设计链表的实现。您可以选择使用单链表或双链表。单链表中的节点应该具有两个属性:val 和 next。val 是当前节点的值,next 是指向下一个节点的指针/引用。如果要使用双向链表,则还需要一个属性 prev 以指示链表中的上一个节点。假设链表中的所有节点都是 0-index 的。
0-index表示索引从0开始
在链表类中实现这些功能:
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
2.1 题解
该题属于基础的链表构造题,安装上面提到的链表操作的思路写就行了。在下面的实现中,构造了哨兵节点。
class ListNode:
def __init__(self,data = 0,next = None):
self.val = data
self.next = next
class MyLinkedList:
def __init__(self):
self.size = 0
self.head = ListNode(0) #哨兵节点
def get(self, index: int) -> int:
if index > (self.size-1) or index < 0:
return -1
cur_node = self.head
for i in range(0,index+1):
cur_node = cur_node.next
return cur_node.val
def addAtHead(self, val: int) -> None:
insert_node = ListNode(val) #self.head是哨兵节点,应该存在它之后
pred = self.head
insert_node.next = pred.next
pred.next = insert_node
self.size += 1
def addAtTail(self, val: int) -> None:
self.addAtIndex(self.size,val)
def addAtIndex(self, index: int, val: int) -> None:
if index > self.size:
return
if index < 0:
index = 0
inser_node = ListNode(val)
pred = self.head
for i in range(index):
pred = pred.next
inser_node.next = pred.next
pred.next = inser_node
self.size += 1
def deleteAtIndex(self, index: int) -> None:
if index < 0 or index > (self.size-1):
return
pred = self.head
for i in range(index):
pred = pred.next
pred.next = pred.next.next
self.size -= 1
# Your MyLinkedList object will be instantiated and called as such:
# obj = MyLinkedList()
# param_1 = obj.get(index)
# obj.addAtHead(val)
# obj.addAtTail(val)
# obj.addAtIndex(index,val)
# obj.deleteAtIndex(index)
运行结果:
占用了比较多的内存,可能和多构建了个哨兵节点有关。
三、206.反转链表
给你单链表的头节点 head ,请你反转链表,并返回反转后的链表。
输入:head = [1,2,3,4,5]
输出:[5,4,3,2,1]
3.1题解
从head节点开始,每次先需要储存前一个节点和后一个节点,即pred和next。然后让 cur遍历。
每次让cur的next指向pred,即实现反转。
class Solution:
def reverseList(self, head: ListNode) -> ListNode:
pred = None
cur = head
while (cur != None):
next = cur.next
cur.next = pred
pred = cur
cur = next
return pred
这道题还是比较简单的,也不需要构造链表,只是写个Solution而已。
运行结果:
3.2 使用递归的解法
使用递归的方法进行求解:
假设有1-2-3-4-5的链表,使用递归时,需要先按1-2-3-4-5的顺序入栈,从5再一步步出栈,每次出栈修改节点的next指向。即node.next.next=node
。实现反转,但此时注意到原先的next的联系未消除,需要使用node.next=None
将原先的指向消除。
class Solution:
def reverseList(self, head: ListNode) -> ListNode:
if head == None or head.next == None:
return head
newhead = self.reverseList(head.next)
head.next.next = head
head.next = None
return newhead
四、203.移除链表
题目描述:
给你一个链表的头节点 head 和一个整数 val ,请你删除链表中所有满足 Node.val == val 的节点,并返回 新的头节点 。
输入:head = [1,2,6,3,4,5,6], val = 6
输出:[1,2,3,4,5]
4.1题解
需要使用另外一个指针,来处理链表第一个元素就被删除的情况,因为此时pred的指针还没赋值。
# 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: ListNode, val: int) -> ListNode:
cur = head
star = ListNode(0,head)
pred = star
while(cur != None):
if cur.val == val:
pred.next = cur.next
cur = cur.next
else:
pred = cur
cur = cur.next
return star.next
运行结果:
五、328.奇偶链表
题目描述:
给定一个单链表,把所有的奇数节点和偶数节点分别排在一起。请注意,这里的奇数节点和偶数节点指的是节点编号的奇偶性,而不是节点的值的奇偶性。
请尝试使用原地算法完成。你的算法的空间复杂度应为 O(1),时间复杂度应为 O(nodes),nodes 为节点总数。
示例:
输入: 1->2->3->4->5->NULL
输出: 1->3->5->2->4->NULL
分析:
由于题目要求原地完成,不能重新建立一个链表去实现。需要通过改变链表节点的next指向来实现。
即
o
d
d
.
n
e
x
t
=
o
d
d
.
n
e
x
t
.
n
e
x
t
odd.next = odd.next.next
odd.next=odd.next.next
e
v
e
n
.
n
e
x
t
=
e
v
e
n
.
n
e
x
t
.
n
e
x
t
even.next = even.next.next
even.next=even.next.next
并且由于每次循环都是改变一个奇数节点和一个偶数节点,可以通过偶数节点以及偶数节点的next是否为None来判断循环结束。
- 当偶数节点为None时,对应链表数量为奇数,即 1 — — 2 — — 3 — — 4 — — 5 1——2——3——4——5 1——2——3——4——5
- 当偶数节点的next为None时,对应链表数量为偶数,即 1 — — 2 — — 3 — — 4 1——2——3——4 1——2——3——4
5.1题解
class Solution:
def oddEvenList(self, head: ListNode) -> ListNode:
if head is None:
return head
even_head = head.next #记录偶节点开头
odd = head
even = even_head
while even and even.next:
odd.next = odd.next.next
even.next = even.next.next
odd = odd.next
even = even.next
odd.next = even_head
return head
运行结果:
六、234.回文链表
题目描述:
给你一个单链表的头节点 head ,请你判断该链表是否为回文链表。如果是,返回 true ;否则,返回 false 。
示例:
输入:head = [1,2,2,1]
输出:true
分析:
这道题很直接的思路就是把链表的值存成数组,对数组操作就很简单了。原本是想通过将数组复制一份,再使用reverse()函数进行翻转,通过判断两个数组是否相等实现回文链表内容的判断。
但是,我发现只有deepcopy能实现深拷贝,所以在不引入copy模块的前提下,这个方法是行不通的。
因此,考虑使用前面学过的双指针的对撞指针进行解决,将first和last指针指向第一个元素和最后一个元素,然后判断它们是否相等,并更新两个指针的指向元素。
这种方法虽然思路简单,但其空间和时间复杂度都为 O ( N ) O(N) O(N)。
代码示例:
def isPalindrome(head):
value = []
node = head
while node != None:
value.append(node.val)
node = node.next
first = value[0]
last = value[-1]
for i in range(len(value)//2):
if first != last:
return False
first = value[0+i+1]
last = value[-1-i-1]
return True
运行结果:
七、链表排序
参考链接:链表排序
- 适合链表的排序算法:冒泡排序、选择排序、插入排序、归并排序、快速排序、计数排序、桶排序、基数排序。
- 不适合链表的排序算法:希尔排序。
- 可以用于链表排序但不建议使用的排序算法:堆排序。
重点了解链表插入排序、链表归并排序
7.1链表归并排序
补充归并排序:
归并排序的思想:
采用经典的分治策略,先递归地将当前序列平均分成两半。然后将有序序列两两合并,最终合并成一个有序序列。
具体步骤:
- 初始时,将待排序序列中的 n 个记录看成 n 个有序子序列(每个子序列总是有序的),每个子序列的长度均为 1。
- 把当前序列组中有序子序列两两归并,完成一遍之后序列组里的排序序列个数减半,每个子序列的长度加倍。
- 对长度加倍的有序子序列重复上面的操作,最终得到一个长度为 n 的有序序列。
class Solution:
def merge(self, left_arr, right_arr):
arr = []
while left_arr and right_arr:
if left_arr[0] <= right_arr[0]:
arr.append(left_arr.pop(0))
else:
arr.append(right_arr.pop(0))
while left_arr:
arr.append(left_arr.pop(0))
while right_arr:
arr.append(right_arr.pop(0))
return arr
def mergeSort(self, arr):
size = len(arr)
if size < 2:
return arr
mid = len(arr) // 2
left_arr, right_arr = arr[0: mid], arr[mid:]
return self.merge(self.mergeSort(left_arr), self.mergeSort(right_arr))
def sortArray(self, nums: List[int]) -> List[int]:
return self.mergeSort(nums)
链表归并排序:
算法步骤:
-
分割环节:找到链表中心链节点,从中心节点将链表断开,并递归进行分割。
- 使用快慢指针 fast = head.next、slow = head,让 fast 每次移动 2 步,slow 移动 1 步,移动到链表末尾,从而找到链表中心链节点,即 slow。
- 从中心位置将链表从中心位置分为左右两个链表 left_head 和 right_head,并从中心位置将其断开,即 slow.next = None。
- 对左右两个链表分别进行递归分割,直到每个链表中只包含一个链节点。
-
归并环节:将递归后的链表进行两两归并,完成一遍后每个子链表长度加倍。重复进行归并操作,直到得到完整的链表。
- 使用哑节点 dummy_head 构造一个头节点,并使用 cur 指向 dummy_head 用于遍历。
比较两个链表头节点 left 和 right 的值大小。将较小的头节点加入到合并后的链表中。并向后移动该链表的头节点指针。 - 然后重复上一步操作,直到两个链表中出现链表为空的情况。
- 将剩余链表插入到合并中的链表中。
- 将哑节点 dummy_dead 的下一个链节点 dummy_head.next 作为合并后的头节点返回。
- 使用哑节点 dummy_head 构造一个头节点,并使用 cur 指向 dummy_head 用于遍历。
def merge(self, left, right):
# 归并环节
dummy_head = ListNode(-1)
cur = dummy_head
while left and right:
if left.val <= right.val:
cur.next = left
left = left.next
else:
cur.next = right
right = right.next
cur = cur.next
if left:
cur.next = left
elif right:
cur.next = right
return dummy_head.next
def mergeSort(self, head: ListNode):
# 分割环节
if not head or not head.next:
return head
# 快慢指针找到中心链节点
slow, fast = head, head.next
while fast and fast.next:
slow = slow.next
fast = fast.next.next
# 断开左右链节点
left_head, right_head = head, slow.next
slow.next = None
# 归并操作
return self.merge(self.mergeSort(left_head), self.mergeSort(right_head))
八、148.排序链表
题目描述:
给你链表的头结点 head ,请将其按 升序 排列并返回 排序后的链表 。
示例:
输入:head = [4,2,1,3]
输出:[1,2,3,4]
分析:
按照上面归并排序的思路,对链表进行操作即可。所用的解决方法也是归并排序,代码思路基本参考上面所给归并排序的示例。
代码如下:
class Solution:
def sortList(self, head: Optional[ListNode]) -> Optional[ListNode]:
def merge(left,right):
dummy_head = ListNode(0)
cur = dummy_head
while left and right:
if left.val <= right.val:
cur.next = left
left = left.next
else:
cur.next = right
right = right.next
cur = cur.next
if left:
cur.next = left
if right:
cur.next = right
return dummy_head.next
def mergeSort(head):
#快慢指针分割
if not head or not head.next:
return head
#找到中点
slow, fast = head, head.next
while fast and fast.next:
slow = slow.next
fast = fast.next.next
#断开连接
left_head, right_head = head, slow.next
slow.next = None
#归并
return merge(mergeSort(left_head),mergeSort(right_head))
return mergeSort(head)
运行结果:
九、21.合并两个有序链表
题目描述:
将两个升序链表合并为一个新的 升序 链表并返回。新链表是通过拼接给定的两个链表的所有节点组成的。
示例:
输入:l1 = [1,2,4], l2 = [1,3,4]
输出:[1,1,2,3,4,4]
分析:
这道题比较简单,相当于遍历两个节点比大小即可。相当于归并排序中的merge的比较部分而已。
代码:
class ListNode:
def __init__(self, val=0, next=None):
self.val = val
self.next = next
class Solution:
def mergeTwoLists(self, list1: Optional[ListNode], list2: Optional[ListNode]) -> Optional[ListNode]:
dummpy = ListNode(0)
cur = dummpy
while list1 and list2:
if list1.val <= list2.val:
cur.next = list1
list1 = list1.next
else:
cur.next = list2
list2 = list2.next
cur = cur.next
if list1:
cur.next = list1
if list2:
cur.next = list2
return dummpy.next
运行结果:
九.链表双指针
9.1 双指针
双指针(Two Pointers):指的是在遍历元素的过程中,不是使用单个指针进行访问,而是使用两个指针进行访问,从而达到相应的目的。如果两个指针方向相反,则称为对撞时针
。如果两个指针方向相同,则称为快慢指针
。如果两个指针分别属于不同的数组 / 链表,则称为分离双指针
。
在单链表中,一般只会用到快慢指针
和分离双指针
。其中链表的快慢指针
又分为起点不一致的快慢指针
和步长不一致的快慢指针
。
9.2起点不一致的快慢指针
起点不一致的快慢指针:指的是两个指针从同一侧开始遍历链表,但是两个指针的起点不一样。 快指针 fast 比慢指针 slow 先走 n 步,直到快指针移动到链表尾端时为止。
具体步骤如下:
- 使用两个指针 slow、fast。slow、fast 都指向链表的头节点,即:slow = head,fast = head。
- 先将快指针向右移动 n 步。然后再同时向右移动快、慢指针。
- 等到快指针移动到链表尾部(即 fast == Node)时跳出循环体。
代码如下:
slow = head
fast = head
while n:
fast = fast.next
n -= 1
while fast:
fast = fast.next
slow = slow.next
起点不一致的快慢指针主要用于找到链表中倒数第 k 个节点、删除链表倒数第 N 个节点等。
9.3步长不一致的慢指针
步长不一致的慢指针:指的是两个指针从同一侧开始遍历链表,两个指针的起点一样,但是步长不一致。例如,慢指针 slow 每次走 1 步,快指针 fast 每次走两步。直到快指针移动到链表尾端时为止。
具体步骤:
- 使用两个指针 slow、fast。slow、fast 都指向链表的头节点。
- 在循环体中将快、慢指针同时向右移动,但是快、慢指针的移动步长不一致。比如将慢指针每次移动 1 步,即 slow = slow.next。快指针每次移动 2 步,即 fast = fast.next.next。
- 等到快指针移动到链表尾部(即 fast == Node)时跳出循环体。
代码:
fast = head
slow = head
while fast and fast.next:
slow = slow.next
fast = fast.next.next
步长不一致的快慢指针适合寻找链表的中点、判断和检测链表是否有环、找到两个链表的交点等问题。
十.019.删除链表的倒数第 N 个结点
题目描述:
给你一个链表,删除链表的倒数第 n 个结点,并且返回链表的头结点。
示例:
输入:head = [1,2,3,4,5], n = 2
输出:[1,2,3,5]
分析:
这道题使用链表快慢指针解决即可,快慢指针可以定位到想删除的结点位置。但需要注意的是,在这道题为了方便删除操作,将慢指针定位到待删除结点的前面一个结点。而且为了处理链表中只有一个结点的情况,建立一个哑结点dummpy,否则删除结点的操作slow.next = slow.next.next会出错。
另一种官方给的思路:先遍历一遍得到链表的大小,再遍历一遍找到相应的结点,这样需要遍历两次,而快慢指针只需要遍历一次。
代码:
class ListNode:
def __init__(self, val=0, next=None):
self.val = val
self.next = next
class Solution:
def removeNthFromEnd(self, head: ListNode, n: int) -> ListNode:
dummpy = ListNode(0)
dummpy.next = head
slow = dummpy
fast = dummpy
while n:
fast = fast.next
n = n - 1
while fast.next:
fast = fast.next
slow = slow.next
slow.next = slow.next.next
return dummpy.next
运行结果:
十一、876.链表的中间结点
题目描述:
给定一个头结点为 head 的非空单链表,返回链表的中间结点。
如果有两个中间结点,则返回第二个中间结点。
示例:
输入:[1,2,3,4,5]
输出:此列表中的结点 3 (序列化形式:[3,4,5])
返回的结点值为 3 。 (测评系统对该结点序列化表述是 [3,4,5])。
注意,我们返回了一个 ListNode 类型的对象 ans,这样:
ans.val = 3, ans.next.val = 4, ans.next.next.val = 5, 以及 ans.next.next.next = NULL.
输入:[1,2,3,4,5,6]
输出:此列表中的结点 4 (序列化形式:[4,5,6])
由于该列表有两个中间结点,值分别为 3 和 4,我们返回第二个结点。
分析:
使用步长不一致的快慢指针天然可以定位到链表的中间结点。
代码:
class Solution:
def middleNode(self, head: ListNode) -> ListNode:
slow = head
fast = head
while fast and fast.next:
slow = slow.next
fast = fast.next.next
return slow
运行结果:
参考链接:
算法手册
算法手册电子书
欢迎大家给作者star!
以上内容均参考自该电子书,侵联删。