本系列是算法通关手册LeeCode的学习笔记
算法通关手册(LeetCode) | 算法通关手册(LeetCode) (itcharge.cn)
本系列为自用笔记,如有版权问题,请私聊我删除。
目录
一,冒泡排序
因为链表只能顺序访问,因此需要一个尾节点 tail 用于记录排好的位置。其余思想与数组排序大同小异。
class Solution:
def bubbleSort(self, head: L.ListNode):
node_i = head
tail = None
# 外层循环为 链表节点个数
while node_i:
node_j = head
while node_j and node_j.next != tail:
if node_j.val > node_j.next.val:
node_j.val, node_j.next.val = node_j.next.val, node_j.val
node_j = node_j.next
# 此时链表最后一个元素为最大元素,将尾指针改变位置
tail = node_j
node_i = node_i.next
return head
def sortList(self, head):
return self.bubbleSort(head)
时间复杂度 O(n ** 2)
空间复杂度 O(1)
二,选择排序
设置 min_node 用于记录无序区中,最小元素存储的位置
遍历无序区间,若当前节点元素小于 min_node 中的元素,则交换
def selectSort(self, head: L.ListNode):
node_i = head
while node_i and node_i.next:
min_node = node_i
node_j = node_i.next
while node_j:
if node_j.val < min_node.val:
min_node = node_j
node_j = node_j.next
if node_i != min_node:
node_i.val, min_node.val = min_node.val, node_i.val
node_i = node_i.next
return head
def sortList(self, head):
return self.selectSort(head)
时间复杂度 O(n ** 2)
空间复杂度 O(1)
三,插入排序
构造一个哨兵节点 dummy ,使得可以从 head 开始遍历;
维护一个 selected_list 指向已排序部分的最后一个节点;
cur 为待插入元素,用 pre 记录插入的位置,即插入位置的前一个节点。
def insertSort(self, head: L.ListNode):
if not head or not head.next:
return head
dummy = L.LinkedList(-1)
dummy.next = head
sorted_list = head
cur = head.next
while cur:
if sorted_list.val <= cur.val:
# 有序中最大的元素仍小于待插入元素,将有序区扩展一位
sorted_list = sorted_list.next
else:
pre = dummy
while pre.next.val <= cur.val:
pre = pre.next
sorted_list.next = cur.next
cur.next = pre.next
pre.next = cur
cur = sorted_list.next
return dummy.next
时间复杂度 O(n ** 2)
空间复杂度 O(1)
四,归并排序
分割:
用快慢指针找到链表的中心节点,从中心节点将链表断开,并递归分割:
让 fast 每次移动 2 步, slow 每次移动 1 步,使 slow 停在链表中间位置;
对左右链表进行递归分割,直到每个链表中只包含一个链节点。
归并:
将递归后的链表进行两两归并,直到得到完整链表:
使用哨兵节点构造一个头节点;
比较像个链表头节点的 left 和right 值的大小,较小的节点加入合并后的链表中;
出现空链表后,将另一个链表接到合并链表中。
def merge(self, left, right):
# 归并环节
dummy = L.LinkedList(-1)
cur = dummy
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.next
def mergeSort(self, head: L.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 = head
right_head = slow.next
slow.next = None
return self.merge(self.mergeSort(left_head), self.mergeSort(right_head))
时间复杂度 O(n * logn)
空间复杂度 O(1)
五,快速排序
选择一个基准值 pivot;
通过快慢指针,找到基准值在链表中的正确位置;
将基准值归位并把链表分为两段;
递归地使基准值归位;
def patition(self, left: L.ListNode, right: L.ListNode):
# 左闭右开,区间没有元素或只有一个元素
if left == right or left.next == right:
return left
# 选择当前第一个节点作为基准节点
pivot = left.val
# 使用node_i 、node_j 双指针,保证 node_i 之前的元素都严格小于基准节点的值,
# node_i 、 node_j 之间的元素都大于等于基准结点的值
# 这样 node_i 记录了基准节点 pivot 最后的位置
# 将 pivot 与 node_i 节点的值交换后,实现了 patition 功能
node_i, node_j = left, left.next
while node_j != right:
if node_j.val < pivot:
# 因为 node_i 节点之前的值都小于 pivot 因此先将 node_i 后移一位
# 此时 node_i 的值大于等于 pivot
node_i = node_i.next
# 判断语句表明此时 node_j 的值小于 pivot,
# 交换 i,j 的值,保证node_i 的值小于pivot
node_i.val, node_j.val = node_j.val, node_i.val
node_j = node_j.next
# 循环结束后,node_i 的值指向最后一个严格小于 pivot 的位置,将 pivot 归位
node_i.val, left.val = left.val, node_i.val
return node_i
def quickSort(self, left: L.ListNode, right: L.ListNode):
if left == right or left.next == right:
return left
mid = self.patition(left, right)
self.quickSort(left, mid)
self.quickSort(mid.next, right)
时间复杂度 O(n * logn)
空间复杂度 O(1)
六,基数排序
先遍历一遍链表,找到链表中的最大值 list_max 和最小值 list_min;
使用 counts 数组 存储节点出现的次数;
遍历列表,将 val 出现的次数存入 count 数组中,数组的下标为 cur.val - list_min;
建立哨兵节点 dummy ,遍历 counts 并建立值为 count[ i ]+ list_min 的节点,连接到尾部;
直到 counts 所有元素为 0
def countingSort(self, head: L.ListNode):
if not head:
return head
list_max = float('-inf')
list_min = float('inf')
cur = head
while cur:
if cur.val < list_min:
list_min = cur.val
if cur.val > list_max:
list_max = cur.val
cur = cur.next
size = list_max - list_min + 1
counts = [0 for _ in range(size)]
cur = head
while cur:
counts[cur.val - list_min] += 1
cur = cur.next
dummy = L.ListNode(-1)
cur = dummy
for i in range(size):
while counts[i]:
cur.next = L.ListNode(i + list_min)
counts[i] -= 1
cur = cur.next
return dummy.next
时间复杂度 O(n + k);
空间复杂度 O(k) ,k 为待排序链表中所有元素的值域。
七,桶排序
遍历一遍链表,找到最大值 list_max 和最小值 list_min;
计算出桶的大小;
再遍历一遍元素,将每个元素装入对应的桶中;
对每个桶内的元素进行排序;
按照顺序将桶内元素拼成新链表,并返回。
# 将链表节点值插入到对应的桶中
def insertion(self, buckets, index, val):
# 如果子链为空,则建立一个子链的头节点
if not buckets[index]:
buckets[index] = L.ListNode(val)
return
# 如果子链非空,则将建立新节点并接入子链
# 保持buckets[index]指向子链的头部位置,便于排序的传入
node = L.ListNode(val)
node.next = buckets[index]
buckets[index] = node
def bucketSort(self, head: L.ListNode, bucket_size = 5):
if not head:
return head
list_max = float('-inf')
list_min = float('inf')
cur = head
while cur:
if cur.val < list_min:
list_min = cur.val
if cur.val > list_max:
list_max = cur.val
cur = cur.next
# 计算桶的个数
bucket_count = (list_max - list_min) // bucket_size + 1
buckets = [None for _ in range(bucket_count)]
# 将链表节点值依次添加到对应桶中
cur = head
while cur:
index = (cur.val - list_min) // bucket_size
self.insertion(buckets, index, cur.val)
cur = cur.next
dummy = L.ListNode(-1)
cur = dummy
# 将元素依次出桶,并拼接成有序列表
for bucket_head in buckets:
bucket_cur = self.mergeSort(bucket_head)
# 沿着每个桶内的元素,到达每个新链表的末尾节点,以连接下一个桶
while bucket_cur:
cur.next = bucket_cur
cur = cur.next
bucket_cur = bucket_cur.next
return dummy.next
时间复杂度 O(n)
空间复杂度 O(n + m) ,m 为桶个数
八,基数排序
遍历链表,得到最长位数 size;
建立十个桶,存储 0 - 9 十位数字;
以每个节点对应位数上的数字,放入桶中;
建立一个哨兵节点 dummy,将桶中元素依次取出,接在其后。
def radixSort(self, head: L.ListNode):
# 计算最长的位数
size = 0
cur = head
while cur:
val_len = len(str(cur.val))
if val_len > size:
size = val_len
cur = cur.next
# 从个位到高位遍历,按照从低到高位排序 size 次
for i in range(size):
buckets = [[] for i in range(10)]
cur = head
while cur:
# 将每个节点的值放入对应的桶
buckets[cur.val // (10 ** i) % 10].append(cur.val)
cur = cur.next
dummy = L.ListNode(-1)
cur = dummy
for bucket in buckets:
for num in bucket:
cur.next = L.ListNode(num)
cur = cur.next
return dummy.next
时间复杂度 O(n * k);
空间复杂度 O(n + k) ,其中 n 是待排序元素的个数,k 是数字位数。
总结
由于希尔排序涉及到随机访问,因此不适用于链表的排序。
而堆排序使用到的完全二叉树结构更适合顺序存储的方式。
以上算法重点掌握 插入排序 和 归并排序即可,其余更多的去体会使用节点遍历链表的方法与过程,并能理解对某个链节点操作时,指针的处理顺序。
本篇文章只给了链表排序的实现代码和简洁思路,更详细的图解请移步:
数据结构与算法----复习Part 4(数组排序)-CSDN博客
算法通关手册(LeetCode) | 算法通关手册(LeetCode)
原文内容在这里,如有侵权,请联系我删除。