本系列是算法通关手册LeeCode的学习笔记
算法通关手册(LeetCode) | 算法通关手册(LeetCode) (itcharge.cn)
本系列为自用笔记,如有版权问题,请私聊我删除。
目录
数组排序
01. 冒泡排序(Bubble Sort)
算法思想:通过相邻元素之间的比较与交换,使值较小的元素逐步从后面移到前面,值较大的元素从前面移到后面。
步骤:
对前 n 个元素执行冒泡,从而使最大值的元素放在正确的位置上;
对 n - 1 个元素执行冒泡,使第二大的元素放在正确的位置上;
重复上述过程,直到数组有序。
代码实现:
class Solution:
def bubbleSort(self, nums: [int]) -> [int]:
for i in range(len(nums) - 1):
flag = False # 交换标志位
for j in range(len(nums) - 1 - i):
if nums[j] > nums[j + 1] :
nums[j], nums[j + 1] = nums[j + 1], nums[j]
flag = True # 发生交换
if not flag: # 未发生交换,此时已经有序
break
return nums
def sortArray(self, nums: [int]) -> [int]:
return self.bubbleSort(nums)
嵌套循环中,每层执行次数都与数组规模 n 相关,时间复杂度为 O(n ^ 2)
空间复杂度为 O(1) ,在原有的数组上进行操作,并未开辟新的内存。
02. 选择排序(Selection Sort)
算法思想:将数组分为左右两个区间,左侧为有序区间,右侧为无序区间,每次选择无序区间中最小的数排在有序区间的末尾,直到无序区间为空。
步骤:
记录有序区间末尾的位置,从 0 开始;
遍历无序区间,找到最小元素所在的位置,交换;
有序区间加一,无序区间减一,直到无序区间为空。
代码实现:
def selectionSort(self, nums: [int]) -> [int]:
cnt = 0
while cnt < len(nums) - 1:
minPos = cnt
minM = nums[cnt]
for i in range(cnt, len(nums)):
if minM >= nums[i]:
minM = nums[i]
minPos = i
nums[cnt], nums[minPos] = nums[minPos], nums[cnt]
cnt += 1
return nums
时间复杂度为 O(n^2)
空间复杂度为 O(1)
03. 插入排序(Insertion Sort)
算法思想:将数组分为左侧有序区间和右侧无序区间。每次从无序区间取出一个元素,然后将其插入到有序区间的适当位置。一个元素视为有序。
步骤:
取得无序区间第一个元素 a ;
将其与有序区间的最后一个元素 b 比较,
若 a > b 表示 a 是有序区间最大的元素;
若 a < b ,将 b 向后移动一位,取得下一个 b 直到有序区间为空或 a > b.
将 a 放入正确位置,有序区间加一, 无序区间减一。
代码实现:
def insertSort(self, nums: [int]) -> [int]:
for i in range(1, len(nums)):
temp = nums[i]
j = i
while j > 0 and nums[j - 1] > temp:
nums[j] = nums[j - 1]
j -= 1
nums[j] = temp
return nums
时间复杂度为 O(n^2)
空间复杂度为 O(1)
04. 希尔排序(Shell Sort)
算法思想:整个数组按照一定的间隔取值划分为若干个子数组,并对子数组排序,直到间隔为一,对整个数组进行排序。
步骤:
确定一个元素间隔数 gap ;
将所有间隔为 gap 的元素视为一个子数组;
对子数组排序;
减少间隔数,划分新的子数组,进行排序;
直到间隔数 gap 为 1, 对整个数组排序。
以插入排序为例:
代码实现:
def shellSort(self, nums: [int]) -> [int]:
size = len(nums)
gap = size // 2
while gap > 0 :
# 以下类比插入排序
for i in range(gap, size):
temp = nums[i]
j = i
while j >= gap and nums[j - gap] > temp:
nums[j] = nums[j - gap]
j -= gap
nums[j] = temp
gap = gap // 2
return nums
曾有过疑问,希尔排序与插入排序的区别在哪
可以看到,上述以 gap = n / 2 的间隔划分子数组,可以更快地使数组有序,而在最后一遍插入排序时,有序度高的数组执行交换的次数随之减少。这是依赖于间隔 gap 的函数,直接给出结论: 时间复杂度在 O(n * log ^ 2(n)) 与 O(n ^ 2) 之间。
空间复杂度 O(1)
特别要说明的是,与先前的排序算法不同,对于相同元素,可能在分组时分到不同组别,分别进行插入排序,最后合并时,可能改变相同元素的相对位置
如 a = 1, b = 2, c = 1, 开始时 a 和 c 都为 1, 而 a 在 c 左侧
排序后可能为 c, a, b 或 a, c, b
虽然两种情况在数值上都是 1, 1, 2 ,实现了排序,但是c, a, b 中,a 和 c 的相对位置改变了,而上面三种不会。
称这种为【不稳定排序算法】,而不改变相同元素相对位置的叫做【稳定排序算法】
不特别说明的都是 稳定排序算法。
05. 归并排序(Merge Sort)
算法思想:经典的分治策略,先递归地将当前数组分成两半,然后将有序数组两两合并,最终得到有序数组。
步骤:
二分当前数组,直到长度为 1;
从长度为 1 的数组逐层合并,直到得到完整数组。
代码实现:
def merge(self, left_nums: [int], right_nums: [int]) -> [int]:
nums = []
left_i, right_i = 0, 0
while left_i < len(left_nums) and right_i < len(right_nums):
# 将两个有序子数组中较小元素依次插入到结果数组中
if left_nums[left_i] < right_nums[right_i]:
nums.append(left_nums[left_i])
left_i += 1
else:
nums.append(right_nums[right_i])
right_i += 1
# 如果左子数组有剩余元素,则将其插入到结果数组中
while left_i < len(left_nums):
nums.append(left_nums[left_i])
left_i += 1
# 如果右子数组有剩余元素,则将其插入到结果数组中
while right_i < len(right_nums):
nums.append(right_nums[right_i])
right_i += 1
# 返回合并后的结果数组
return nums
def mergeSort(self, nums: [int]) -> [int]:
if len(nums) <= 1:
return nums
mid = len(nums) // 2
left_nums = self.mergeSort(nums[ :mid]) # 接受了mergeSort的返回值
right_nums = self.mergeSort(nums[mid: ]) # 而mergeSort的返回值是merge的返回值
return self.merge(left_nums, right_nums) # merge的返回值是新开辟的数组nums
def sortArray(self, nums: [int]) -> [int]:
return self.mergeSort(nums)
时间复杂度为 O(n * logn)
空间复杂度:每次调用 merge 都开辟了新数组,将其赋值给 left_nums 或right_nums 后,又释放,因此所需的最大额外空间为最后一次的 n,故空间复杂度为 O(n)
更详细的过程可看我写的另一篇:
06. 快速排序(Quick Sort)
算法思想:选择一个元素 a ,使得其左侧的元素都小于 a,其右侧的元素都大于 a,以同样的方式,将 a 左右的子数组进行排序,直到整个数组有序。
步骤:
选择一个基准数 pivot;
先从右向左找到第一个小于 pivot 的数,记录其位置 j ;
再从左向右找到第一个大于 pivot 的数, 记录其位置 i;
交换位置 i,j 的元素,继续先从右向左找;
当 i = j 时,找到基准数应在的位置;
因为先找的是大于基准数 pivot 的元素,所以 i 位置一定是小于基准数的元素;
所以交换后不影响左侧都是小于基准数这一效果。
然后递归地处理 pivot 左右两侧的子数组
代码实现:
def patition(self, nums: [int], low: int, high: int) -> int:
pivot = nums[low]
i = low
j = high
while i < j:
while nums[j] >= pivot and i < j:
j -= 1
while nums[i] <= pivot and i < j:
i += 1
nums[i], nums[j] = nums[j], nums[i]
nums[i], nums[low] = nums[low], nums[i]
return i
def quickSort(self, nums: [int], low: int, high: int) -> [int]:
if low < high:
mid = self.patition(nums, low, high)
self.quickSort(nums, low, mid - 1)
self.quickSort(nums, mid + 1, high)
return nums
时间复杂度:最坏情况下,原数组有序时,对于基准元素 pivot 的位置,j 需要从后向前遍历一遍,每次如此,且仅将子数组长度减 1 ,此时时间复杂度为 O(n ^ 2).
平均情况下时间复杂度为 O(n * logn)
空间复杂度为 O(n)
07. 堆排序(Heap Sort)
进行堆排序之前,先大概了解堆的结构与操作。
7.1 堆结构
堆(Heap),一种满足以下两个条件之一的完全二叉树:
大顶(根)堆(Max Heap):任意节点值 >= 其子节点值;
小顶(根)堆(Min Heap):任意节点值 <= 其子节点值;
7.2 堆的存储结构
堆的逻辑结构就是一棵完全二叉树。
当使用数组来表示堆时,堆中元素的节点编号与数组的索引关系为:
7.3 堆的操作
访问堆顶元素
从堆结构中获取位于堆顶的元素。
在堆中,堆顶元素位于根节点,当我们使用数组来表示堆时,堆顶元素就是数组的首个元素
class Maxheap:
......
def peek(self) -> int:
# 大顶堆为空
if not self.max_heap:
return None
return self.max_heap[0]
不依赖于数组中元素个数,时间复杂度为 O(1)
向堆中插入元素
将一个新元素添加到堆中,调整堆的结构,以保持堆的特性不变。
步骤:
将新元素加到堆的末尾,保持完全二叉树结构;
从插入的新节点开始,与父节点比较,若大于父节点则交换,保持大顶堆特性;
直到不再大于父节点或到达根节点。
该过程称为上移 : shift up
class Maxheap:
max_heap = []
def push(self, val: int):
self.max_heap.append(val)
size = len(self.max_heap)
self.__shift_up(size - 1)
def __shift_up(self, i: int):
# (i - 1) // 2 为加入节点的父节点
while (i - 1) // 2 >= 0 and self.max_heap[i] > self.max_heap[(i - 1) // 2] :
self.max_heap[i], self.max_heap[(i - 1) // 2] = self.max_heap[(i - 1) // 2], self.max_heap[i]
i = (i - 1) // 2
在最坏情况下,时间复杂度为 O(logn) ,其中 n 是堆中元素的数量,这是因为堆的高度是 logn
删除堆顶元素
从堆中移除位于堆顶的元素,并重新调整堆的结构,以保持堆的特性不变。
步骤:
将堆的元素与末尾元素交换;
将交换后的末尾元素移除;
从新的堆顶开始,将其与较大的子节点比较,若小于较大的子节点,则交换,以保持大顶堆
直到该元素不再小于其子节点或到达堆的底部。
该过程称为下移: shift down
class Maxheap:
max_heap = []
def pop(self) -> int:
# 堆为空
if not self.max_heap:
raise IndexError("堆为空")
size = len(self.max_heap)
self.max_heap[0], self.max_heap[size - 1] = self.max_heap[size - 1], self.max_heap[0]
# 删除堆顶元素
val = self.max_heap.pop()
size -= 1
self.__shift_down(0, size)
# 拿到移除的元素
return val
def __shift_down(self, i: int, n: int):
# 有子节点
while 2 * i + 1 < n:
left, right = 2 * i + 1, 2 * i + 2
# 找到较大子节点的编号
if right >= n:
# 只有左子节点
larger = left
else:
if self.max_heap[left] > self.max_heap[right]:
larger = left
else:
larger = right
# 当前父节点小于子节点,交换,此时交换后的节点继续与子节点比较
if self.max_heap[i] < self.max_heap[larger]:
self.max_heap[i], self.max_heap[larger] = self.max_heap[larger], self.max_heap[i]
i = larger
# 当前父节点大于子节点,结束
else:
break
时间复杂度为O(logn),其中 n 是堆中元素的数量,因为堆的高度是 logn
7.4 堆排序
算法思想:使用堆中的排序方法,建立大顶堆。重复地从堆中取出堆顶元素,并让剩余的堆结构继续维持大顶堆的性质。
步骤:
定义一个数组实现的堆结构,将原始数组的元素存入堆结构;
从最后一个元素的父节点((size - 2)// 2 )开始,自下而上的,依次通过下移调整,构造大顶堆;
交换堆顶元素与最后一个元素,完成后,使堆的长度减一;
从堆顶元素开始,进行下移调整,维持堆的特性,直到堆的大小为1。
代码实现:
class Maxheap:
......
def __buildMaxHeap(self, nums: [int]): # 建堆
size = len(nums)
for i in range(size):
self.max_heap.append(nums[i])
# 自下向上对每个非叶子节点向下调整,建立大顶堆
for i in range((size - 2) // 2, -1, -1):
self.__shift_down(i, size)
def maxHeapSort(self, nums: [int]) -> [int]:
self.__buildMaxHeap(nums)
size = len(nums)
# 弹出堆顶元素,并将堆的规模减一
for i in range(size - 1, -1, -1):
self.max_heap[0], self.max_heap[i] = self.max_heap[i], self.max_heap[0]
# 对新的堆顶元素向下调整
self.__shift_down(0, i)
return self.max_heap
class Solution:
def maxHeapSort(self, nums: [int]) -> [int]:
return Maxheap().maxHeapSort(nums)
def sortArray(self, nums: [int]) -> [int]:
return self.maxHeapSort(nums)
时间复杂度为 O(n * logn)
空间复杂度为 O(1)
下移调整时,可能改变相同元素的相对位置,因此是 不稳定排序
08. 计数排序(Counting Sort)
算法思想:通过统计数组中每个元素出现的次数,找到每个元素在数组中对应的位置。
步骤:
计算排序范围,找到最大元素 numsmax 与最小元素 numsmin;
定义计数数组 counts ,用于统计每个元素出现的次数;
counts 的索引值为 num - numsmin ,表示元素 num 在nums中的相对位置
生成累积计数数组,从 counts 中的第一个元素开始,每一项累加前一项和,如(设0最小)
counts[0] = 1 表示 0 出现了 1 次;
counts[1] = 2 表示 1 出现了 2 次;
counts[2] = 2 表示 2 出现了 2 次;
累积计数数组中
counts[1] += counts[0] 此时 counts[1] = 3,表示位置 1 到 3 是元素 0 和 1 的位置
同理 counts[2] += counts[1] 此时 counts[2] = 5 ,表示位置 1 到 5 是元素 0、1、2;
逆序填充目标数组,逆序遍历数组 nums,将每个元素 num 填入正确位置;
将其填充到结果数组 res 中,位置为 counts[num - numsmin];
放入后,另累计数组中对应索引位置 减一,从而得到下一个元素的放置位置;
例如
counts[2] = 5
放置了一个元素 2 到位置 5,若放置第 2 个元素 2,要将其放置到位置 4
因此在放置一个元素 2 后 counts[2] -= 1,用于记录下一个元素 2 的放置位置
代码实现:
def countingSort(self, nums: [int]) -> [int]:
numsMax, numsMin = max(nums), min(nums)
size = numsMax - numsMin + 1
counts = [0 for _ in range(size)]
# 计数
for i in range(len(nums)):
counts[nums[i] - numsMin] += 1
# 累积数组
for i in range(1, size):
counts[i] += counts[i - 1]
# 将元素倒序归位
res = [0 for _ in range(len(nums))]
for i in range(len(nums) - 1, -1, -1):
num = nums[i]
res[counts[num - numsMin] - 1] = num
counts[num - numsMin] -= 1
return res
时间复杂度为 O(n + k),其中 k 为待排序数组的值域;
空间复杂度为 O(k)。
09. 桶排序(Bucket Sort)
算法思想:将待排序数组中的元素分散到若干个【桶】中,然后对每个桶中的元素再进行单独排序。
步骤:
确定桶的数量,根据待排序数组的值域范围,划分为 k 个桶;
分配元素,将每个元素根据大小分到对应的桶中;
对每个桶进行排序;
合并桶内元素。
代码实现:
def insertionSort(self, nums: [int]) -> [int]: # 定义桶内的排序算法
for i in range(1, len(nums)):
temp = nums[i]
j = i
while j > 0 and nums[j - 1] > temp:
nums[j] = nums[j - 1]
j -= 1
nums[j] = temp
return nums
def bucketSort(self, nums: [int], bucketSize = 5) -> [int]:
numsMax, numsMin = max(nums), min(nums)
buckectCount = (numsMax - numsMin) // bucketSize + 1
buckets = [[] for _ in range(buckectCount)]
# 遍历数组,将元素根据大小放入桶中
for num in nums:
buckets[(num - numsMin) // bucketSize].append(num)
# 对每个桶内的元素单独排序,并添加到res数组中
res = []
for buckest in buckets:
self.insertionSort(buckest)
res.extend(buckest)
return res
时间复杂度 O(n),当输入元素个数为 n,桶的个数是 m 时,每个桶里的数据就是 k=n / m 个。每个桶内排序的时间复杂度为 O(k×logk)。m 个桶就是
m×O(k×logk)=m×O(n/m×logn/m)=O(n×logn/m)
当桶的个数 m 接近于数据个数 n 时,logn/m 就是一个较小的常数,所以排序桶排序时间复杂度接近于 O(n)
空间复杂度 O(n + m);
排序稳定性取决与桶内部的排序算法。
10. 基数排序(Radix Sort)
算法思想:将整数按位数切割成不同的数字,然后从低位开始,依次到高位,逐位进行排序
步骤:
确定排序最大值的位数;
从最低位到最高位逐一对每一位排序;
定义一个长度为 10 的桶数组 buckects ,每个桶分别代表 0~9 这十个数字;
按照每个元素当前位上的数字,将元素放到对应桶中;
清空原始数组,然后按照桶的顺序,依次取出对应元素,加入原始数组。
代码实现:
def radixSort(self, nums: [int]) -> [int]:
# 桶的大小位所有元素的最大位数
size = len(str(max(nums)))
# 从低位开始,遍历每一位
for i in range(size):
buckets = [[] for _ in range(10)]
# 按照当前位上的数字,放入桶中
for num in nums:
buckets[num // (10 ** i) % 10].append(num)
nums.clear()
for bucket in buckets:
for num in bucket:
nums.append(num)
return nums
时间复杂度 O(n * k);
空间复杂度 O(n + k),其中 k 是数组位数。
排序例题
1. LCR 164. 破解闯关密码 - 力扣(LeetCode)
第二题中,可以参考快速排序的思路,将不等于 0 的元素放在 0 左边,等于 0 放在右边。
PS:图是从题解里拿来的(侵删)
3. 215. 数组中的第K个最大元素 - 力扣(LeetCode)
第三题中,堆排序可以找到第 k 大的元素, 但是时间复杂度还不符合要求,可以考虑使用桶排序,根据桶的大小,对某个桶内的子数组排序,以降低时间复杂度。
5. 88. 合并两个有序数组 - 力扣(LeetCode)
第五题可以类比归并排序中的 merge 过程,我实现的是从小到达的插入
class Solution:
def merge(self, nums1: List[int], m: int, nums2: List[int], n: int) -> None:
"""
Do not return anything, modify nums1 in-place instead.
"""
# 归并排序的 merge 过程
if m == 0:
nums1.clear()
for num in nums2:
nums1.append(num)
return
if n == 0:
return
cnt = len(nums1) - m
for _ in range(cnt):
nums1.pop()
i, j = 0, 0
while i < m and j < n:
if nums2[j] <= nums1[i]:
nums1.insert(i, nums2[j])
j += 1
i += 1
m += 1
else:
i += 1
while j < n:
nums1.append(nums2[j])
j += 1
操纵双指针,实现正确位置的插入过程。
更好的思路是从后向前遍历,题目给出了最后元素的位置,最大元素放在最后位置即可。
第六题是一道比较有趣的排序题,可以使用 zip() 方法绑定排序后的名次与分数,也可以像我一样使用下标手动绑定解决
class Solution:
def findRelativeRanks(self, score: List[int]) -> List[str]:
aid = score.copy()
aid.sort(reverse = True)
ans = []
for i in range(len(score)):
rank = aid.index(score[i]) + 1
if rank == 1:
ans.append("Gold Medal")
elif rank == 2:
ans.append("Silver Medal")
elif rank == 3:
ans.append("Bronze Medal")
else:
ans.append(str(rank))
return ans
7. LCR 170. 交易逆序对的总数 - 力扣(LeetCode)
第七题是归并排序的经典应用场景,在 merge 的过程中,对逆序对计数。
class Solution:
def reversePairs(self, record: List[int]) -> int:
def mergeSort(low, high):
if low >= high:
return 0
mid = (low + high) // 2
ans = mergeSort(low, mid) + mergeSort(mid + 1, high)
# 双指针记录左右两侧的合并位置
i, j = low, mid + 1
tmp[low: high + 1] = record[low: high + 1]
for k in range(low, high + 1):
# 如果左侧为空,则将右侧数据加入
if i == mid + 1:
record[k] = tmp[j]
j += 1
# 如果右侧为空,将左侧数据加入
# 如果右侧大于等于左侧,则没有逆序对,将左侧数据加入
elif j == high + 1 or tmp[j] >= tmp[i]:
record[k] = tmp[i]
i += 1
# 如果右侧小于左侧,则当前右侧元素与左侧剩余的 m 个元素构成了 m 个逆序对
# 左侧剩余元素个数为 mid - i + 1
else:
record[k] = tmp[j]
j += 1
ans += mid - i + 1
return ans
tmp = [0] * len(record)
return mergeSort(0, len(record) - 1)
总结
本盘介绍了十种排序算法,学习的过程中可能有疑问,Python中的 sort() 函数是快速排序算法(Quick Sort)实现的,使用时直接调用即可,为什么还要学习这么排序算法。
其实在做题的过程中可以明白,排序的过程是更重要的,例题中过次用到了归并(merge)的思想,这是分治法的一种生动体现,使用了堆排序找到第 k 大元素,也使用了快速排序中,双指针的思路。
因此思路比实现的效果更重要。
算法通关手册(LeetCode) | 算法通关手册(LeetCode)
原文内容在这里,如有侵权,请联系我删除。