冒泡排序(Bubble Sort)基本思想:
经过多次迭代,通过相邻元素之间的比较与交换,使值较小的元素逐步从后面移到前面,值较大的元素从前面移到后面。
这个过程就像水底的气泡一样从底部向上「冒泡」到水面,这也是冒泡排序法名字的由来。
接下来,我们使用「冒泡」的方式来模拟一下这个过程。
<1>
<2>
<3>
<4>
<5>
<6>
<7>
假设数组的元素个数为 n n n 个,则冒泡排序的算法步骤如下:
我们以 [ 5 , 2 , 3 , 6 , 1 , 4 ] [5, 2, 3, 6, 1, 4] [5,2,3,6,1,4] 为例,演示一下冒泡排序的整个过程。
class Solution:
def bubbleSort(self, nums: [int]) -> [int]:
# 第 i 趟「冒泡」
for i in range(len(nums) - 1):
flag = False # 是否发生交换的标志位
# 从数组中前 n - i + 1 个元素的第 1 个元素开始,相邻两个元素进行比较
for j in range(len(nums) - i - 1):
# 相邻两个元素进行比较,如果前者大于后者,则交换位置
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)
选择排序(Selection Sort)基本思想:
将数组分为两个区间:左侧为已排序区间,右侧为未排序区间。每趟从未排序区间中选择一个值最小的元素,放到已排序区间的末尾,从而将该元素划分到已排序区间。
选择排序是一种简单直观的排序算法,其思想简单,代码也相对容易。
假设数组的元素个数为 n n n 个,则选择排序的算法步骤如下:
我们以 [ 5 , 2 , 3 , 6 , 1 , 4 ] [5, 2, 3, 6, 1, 4] [5,2,3,6,1,4] 为例,演示一下选择排序的整个过程。
<1>
<2>
<4>
<5>
<6>
<7>
class Solution:
def selectionSort(self, nums: [int]) -> [int]:
for i in range(len(nums) - 1):
# 记录未排序区间中最小值的位置
min_i = i
for j in range(i + 1, len(nums)):
if nums[j] < nums[min_i]:
min_i = j
# 如果找到最小值的位置,将 i 位置上元素与最小值位置上的元素进行交换
if i != min_i:
nums[i], nums[min_i] = nums[min_i], nums[i]
return nums
def sortArray(self, nums: [int]) -> [int]:
return self.selectionSort(nums)
插入排序(Insertion Sort)基本思想:
将数组分为两个区间:左侧为有序区间,右侧为无序区间。每趟从无序区间取出一个元素,然后将其插入到有序区间的适当位置。
插入排序在每次插入一个元素时,该元素会在有序区间找到合适的位置,因此每次插入后,有序区间都会保持有序。
假设数组的元素个数为 n n n 个,则插入排序的算法步骤如下:
我们以 [ 5 , 2 , 3 , 6 , 1 , 4 ] [5, 2, 3, 6, 1, 4] [5,2,3,6,1,4] 为例,演示一下插入排序的整个过程。
class Solution:
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 sortArray(self, nums: [int]) -> [int]:
return self.insertionSort(nums)
描述:给定一个非负整数数组 nums
。
要求:将数组中的数字拼接起来排成一个数,打印能拼接出的所有数字中的最小的一个。
说明:
0
,最后结果不需要去掉前导 0
。示例:
输入:[3,30,34,5,9]
输出:"3033459"
本质上是给数组进行排序。假设 x
、y
是数组 nums
中的两个元素。则排序的判断规则如下所示:
x + y > y + x
,则 x
大于 y
,y
应该排在 x
前面,从而使拼接起来的数字尽可能的小。x + y < y + x
,则 x
小于 y
,x
应该排在 y
前面,从而使拼接起来的数字尽可能的小。按照上述规则,对原数组进行排序。这里使用了 functools.cmp_to_key
自定义排序函数。
from functools import cmp_to_key
class Solution:
def minNumber(self, nums: List[int]) -> str:
nums = [*map(str, nums)]
nums.sort(key=cmp_to_key(lambda x, y: - (x + y < y + x)))
return "".join(nums)
描述:给定一个数组 nums
。
要求:将所有 0
移动到末尾,并保持原有的非 0 数字的相对顺序。
说明:
示例:
输入: nums = [0,1,0,3,12]
输出: [1,3,12,0,0]
输入: nums = [0]
输出: [0]
slow
,fast
。slow
指向处理好的非 0
数字数组的尾部,fast
指针指向当前待处理元素。fast
指针,每次移动到非零数,则将左右指针对应的数交换,交换同时将 slow
右移。slow
指针左侧均为处理好的非零数,而从 slow
指针指向的位置开始, fast
指针左边为止都为 0
。遍历结束之后,则所有 0
都移动到了右侧,且保持了非零数的相对位置。
class Solution:
def moveZeroes(self, nums: List[int]) -> None:
s = 0
for f in range(len(nums)):
if nums[f]:
if f - s:
nums[s] = nums[f]
nums[f] = 0
s += 1
描述:给定一个整数数组 nums
。
要求:将该数组升序排列。
说明:
示例:
输入:nums = [5,2,3,1]
输出:[1,2,3,5]
输入:nums = [5,1,1,2,0,0]
输出:[0,0,1,1,2,5]
真 · 快速排序基本思想:
class Solution:
def sortArray(self, nums: List[int]) -> List[int]:
nums.sort()
return nums
归并排序(Merge Sort)基本思想:
采用经典的分治策略,先递归地将当前数组平均分成两半,然后将有序数组两两合并,最终合并成一个有序数组。
假设数组的元素个数为 n n n 个,则归并排序的算法步骤如下:
我们以 [ 0 , 5 , 7 , 3 , 1 , 6 , 8 , 4 ] [0, 5, 7, 3, 1, 6, 8, 4] [0,5,7,3,1,6,8,4] 为例,演示一下归并排序的整个过程。
class Solution:
# 合并过程
def merge(self, left_nums: [int], right_nums: [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]:
# 数组元素个数小于等于 1 时,直接返回原数组
if len(nums) <= 1:
return nums
mid = len(nums) // 2 # 将数组从中间位置分为左右两个数组
left_nums = self.mergeSort(nums[0: mid]) # 递归将左子数组进行分解和排序
right_nums = self.mergeSort(nums[mid:]) # 递归将右子数组进行分解和排序
return self.merge(left_nums, right_nums) # 把当前数组组中有序子数组逐层向上,进行两两合并
def sortArray(self, nums: [int]) -> [int]:
return self.mergeSort(nums)
merge(left_nums, right_nums):
的时间复杂度是 O ( n ) O(n) O(n),因此,归并排序算法总的时间复杂度为 O ( n × log n ) O(n \times \log n) O(n×logn)。merge(left_nums, right_nums):
算法能够使前一个数组中那个相等元素先被复制,从而确保这两个元素的相对顺序不发生改变。因此,归并排序算法是一种 稳定排序算法。希尔排序(Shell Sort)基本思想:
将整个数组切按照一定的间隔取值划分为若干个子数组,每个子数组分别进行插入排序。然后逐渐缩小间隔进行下一轮划分子数组和对子数组进行插入排序。直至最后一轮排序间隔为 1 1 1,对整个数组进行插入排序。
假设数组的元素个数为 n n n 个,则希尔排序的算法步骤如下:
我们以 [ 7 , 2 , 6 , 8 , 0 , 4 , 1 , 5 , 9 , 3 ] [7, 2, 6, 8, 0, 4, 1, 5, 9, 3] [7,2,6,8,0,4,1,5,9,3] 为例,演示一下希尔排序的整个过程。
<1>
<2>
<3>
<4>
<5>
<6>
<7>
class Solution:
def shellSort(self, nums: [int]) -> [int]:
size = len(nums)
gap = size // 2
# 按照 gap 分组
while gap > 0:
# 对每组元素进行插入排序
for i in range(gap, size):
# temp 为每组中无序数组第 1 个元素
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 = gap // 2
return nums
def sortArray(self, nums: [int]) -> [int]:
return self.shellSort(nums)
时间复杂度:介于 O ( n × log 2 n ) O(n \times \log^2 n) O(n×log2n) 与 O ( n 2 ) O(n^2) O(n2) 之间。
while gap > 0
的循环次数为 log n \log n logn 数量级,内层插入排序算法循环次数为 n n n 数量级。当子数组分得越多时,子数组内的元素就越少,内层循环的次数也就越少;反之,当所分的子数组个数减少时,子数组内的元素也随之增多,但整个数组也逐步接近有序,而循环次数却不会随之增加。因此,希尔排序算法的时间复杂度在 O ( n × log 2 n ) O(n \times \log^2 n) O(n×log2n) 与 O ( n 2 ) O(n^2) O(n2) 之间。空间复杂度: O ( 1 ) O(1) O(1)。希尔排序中用到的插入排序算法为原地排序算法,只用到指针变量 i i i、 j j j 以及表示无序区间中第 1 1 1 个元素的变量、间隔数 g a p gap gap 等常数项的变量。
排序稳定性:在一次插入排序是稳定的,不会改变相等元素的相对顺序,但是在不同的插入排序中,相等元素可能在各自的插入排序中移动。因此,希尔排序方法是一种 不稳定排序算法。
描述:给定一个长度为 n
的数组 score
。其中 score[i]
表示第 i
名运动员在比赛中的成绩。所有成绩互不相同。
要求:找出他们的相对名次,并授予前三名对应的奖牌。前三名运动员将会被分别授予「金牌(Gold Medal
)」,「银牌(Silver Medal
)」和「铜牌(Bronze Medal
)」。
说明:
score
中的所有值互不相同。示例:
输入:score = [5,4,3,2,1]
输出:["Gold Medal","Silver Medal","Bronze Medal","4","5"]
解释:名次为 [1st, 2nd, 3rd, 4th, 5th] 。
输入:score = [10,3,8,9,4]
输出:["Gold Medal","5","Bronze Medal","Silver Medal","4"]
解释:名次为 [1st, 5th, 3rd, 2nd, 4th] 。
score
进行排序。Gold Medal
, Silver Medal
, Bronze Medal
。class Solution:
def findRelativeRanks(self, score: List[int]) -> List[str]:
mark=("Gold Medal", "Silver Medal", "Bronze Medal")
for i, j in enumerate(sorted(range(len(score)), key=lambda x: -score[x])):
score[j] = str(i + 1) if i > 2 else mark[i]
return score
描述:给定两个有序数组 n u m s 1 nums1 nums1、 n u m s 2 nums2 nums2。
要求:将 n u m s 2 nums2 nums2 合并到 n u m s 1 nums1 nums1 中,使 n u m s 1 nums1 nums1 成为一个有序数组。
说明:
示例:
输入:nums1 = [1,2,3,0,0,0], m = 3, nums2 = [2,5,6], n = 3
输出:[1,2,2,3,5,6]
解释:需要合并 [1,2,3] 和 [2,5,6] 。
合并结果是 [1,2,2,3,5,6] ,其中斜体加粗标注的为 nums1 中的元素。
输入:nums1 = [1], m = 1, nums2 = [], n = 0
输出:[1]
解释:需要合并 [1] 和 [] 。
合并结果是 [1] 。
index1
、index2
分别指向 nums1
、nums2
数组的尾部,再用一个指针 index
指向数组 nums1
的尾部。nums1[index1]
和 nums[index2]
的值大小,将较大值存入 num1[index]
中,然后继续向前遍历。nums2
中剩余元素赋值到 num1
前面对应位置上。class Solution:
def merge(self, nums1: List[int], m: int, nums2: List[int], n: int) -> None:
m, n = m - 1, 1
while nums2:
if m >= 0 and nums1[m] > nums2[-1]:
nums1[-n] = nums1[m]
m -= 1
else:
nums1[-n] = nums2.pop()
n += 1
描述:给定一个数组 nums
。
要求:计算出数组中的逆序对的总数。
说明:
示例:
输入: [7,5,6,4]
输出: 5
数组 tree[i]
表示数字 i
是否在序列中出现过,如果数字 i
已经存在于序列中,tree[i] = 1
,否则 tree[i] = 0
。
nums[i]
的元素当作下标为nums[i]
,赋值为 1
插入树状数组里,这时,比 nums[i]
大的数个数就是 i + 1 - query(a)
。import bisect
class BinaryIndexTree:
def __init__(self, n):
self.size = n
self.tree = [0 for _ in range(n + 1)]
def lowbit(self, index):
return index & (-index)
def update(self, index, delta):
while index <= self.size:
self.tree[index] += delta
index += self.lowbit(index)
def query(self, index):
res = 0
while index > 0:
res += self.tree[index]
index -= self.lowbit(index)
return res
class Solution:
def reversePairs(self, nums: List[int]) -> int:
size = len(nums)
sort_nums = sorted(nums)
for i in range(size):
nums[i] = bisect.bisect_left(sort_nums, nums[i]) + 1
bit = BinaryIndexTree(size)
ans = 0
for i in range(size):
bit.update(nums[i], 1)
ans += (i + 1 - bit.query(nums[i]))
return ans
快速排序(Quick Sort)基本思想:
采用经典的分治策略,选择数组中某个元素作为基准数,通过一趟排序将数组分为独立的两个子数组,一个子数组中所有元素值都比基准数小,另一个子数组中所有元素值都比基准数大。然后再按照同样的方式递归的对两个子数组分别进行快速排序,以达到整个数组有序。
假设数组的元素个数为 n n n 个,则快速排序的算法步骤如下:
我们以 [ 4 , 7 , 5 , 2 , 6 , 1 , 3 ] [4, 7, 5, 2, 6, 1, 3] [4,7,5,2,6,1,3] 为例,演示一下快速排序的整个步骤。
我们先来看一下单次「哨兵划分」的过程。
<1>
<2>
<3>
<4>
<5>
<6>
<7>
在经过一次「哨兵划分」过程之后,数组就被划分为左子数组、基准数、右子树组三个独立部分。接下来只要对划分好的左右子数组分别进行递归排序即可完成排序。整个步骤如下:
import random
class Solution:
# 随机哨兵划分:从 nums[low: high + 1] 中随机挑选一个基准数,并进行移位排序
def randomPartition(self, nums: [int], low: int, high: int) -> int:
# 随机挑选一个基准数
i = random.randint(low, high)
# 将基准数与最低位互换
nums[i], nums[low] = nums[low], nums[i]
# 以最低位为基准数,然后将数组中比基准数大的元素移动到基准数右侧,比他小的元素移动到基准数左侧。最后将基准数放到正确位置上
return self.partition(nums, low, high)
# 哨兵划分:以第 1 位元素 nums[low] 为基准数,然后将比基准数小的元素移动到基准数左侧,将比基准数大的元素移动到基准数右侧,最后将基准数放到正确位置上
def partition(self, nums: [int], low: int, high: int) -> int:
# 以第 1 位元素为基准数
pivot = nums[low]
i, j = low, high
while i < j:
# 从右向左找到第 1 个小于基准数的元素
while i < j and nums[j] >= pivot:
j -= 1
# 从左向右找到第 1 个大于基准数的元素
while i < j and nums[i] <= pivot:
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:
# 按照基准数的位置,将数组划分为左右两个子数组
pivot_i = self.randomPartition(nums, low, high)
# 对左右两个子数组分别进行递归快速排序
self.quickSort(nums, low, pivot_i - 1)
self.quickSort(nums, pivot_i + 1, high)
return nums
def sortArray(self, nums: [int]) -> [int]:
return self.quickSort(nums, 0, len(nums) - 1)
快速排序算法的时间复杂度主要跟基准数的选择有关。本文中是将当前数组中第 1 1 1 个元素作为基准值。
在这种选择下,如果参加排序的元素初始时已经有序的情况下,快速排序方法花费的时间最长。也就是会得到最坏时间复杂度。
在这种情况下,第 1 1 1 趟排序经过 n − 1 n - 1 n−1 次比较以后,将第 1 1 1 个元素仍然确定在原来的位置上,并得到 1 1 1 个长度为 n − 1 n - 1 n−1 的子数组。第 2 2 2 趟排序进过 n − 2 n - 2 n−2 次比较以后,将第 2 2 2 个元素确定在它原来的位置上,又得到 1 1 1 个长度为 n − 2 n - 2 n−2 的子数组。
最终总的比较次数为 ( n − 1 ) + ( n − 2 ) + … + 1 = n ( n − 1 ) 2 (n − 1) + (n − 2) + … + 1 = \frac{n(n − 1)}{2} (n−1)+(n−2)+…+1=2n(n−1)。因此这种情况下的时间复杂度为 O ( n 2 ) O(n^2) O(n2),也是最坏时间复杂度。
我们可以改进一下基准数的选择。如果每次我们选中的基准数恰好能将当前数组平分为两份,也就是刚好取到当前数组的中位数。
在这种选择下,每一次都将数组从 n n n 个元素变为 n 2 \frac{n}{2} 2n 个元素。此时的时间复杂度公式为 T ( n ) = 2 × T ( n 2 ) + Θ ( n ) T(n) = 2 \times T(\frac{n}{2}) + \Theta(n) T(n)=2×T(2n)+Θ(n)。根据主定理可以得出 T ( n ) = O ( n × log n ) T(n) = O(n \times \log n) T(n)=O(n×logn),也是最佳时间复杂度。
而在平均情况下,我们可以从当前数组中随机选择一个元素作为基准数。这样,每一次选择的基准数可以看做是等概率随机的。其期望时间复杂度为 O ( n × log n ) O(n \times \log n) O(n×logn),也就是平均时间复杂度。
下面来总结一下:
「堆排序(Heap sort)」是一种基于「堆结构」实现的高效排序算法。在介绍「堆排序」之前,我们先来了解一下什么是「堆结构」。
堆(Heap):一种满足以下两个条件之一的完全二叉树:
- 大顶堆(Max Heap):任意节点值 ≥ 其子节点值。
- 小顶堆(Min Heap):任意节点值 ≤ 其子节点值。
堆的逻辑结构就是一颗完全二叉树。而我们在「07.树 - 01.二叉树 - 01.树与二叉树的基础知识」章节中学过,对于完全二叉树(尤其是满二叉树)来说,采用顺序存储结构(数组)的形式来表示完全二叉树,能够充分利用存储空间。
当我们使用顺序存储结构(即数组)来表示堆时,堆中元素的节点编号与数组的索引关系为:
class MaxHeap:
def __init__(self):
self.max_heap = []
访问堆顶元素:指的是从堆结构中获取位于堆顶的元素。
在堆中,堆顶元素位于根节点,当我们使用顺序存储结构(即数组)来表示堆时,堆顶元素就是数组的首个元素。
class MaxHeap:
......
def peek(self) -> int:
# 大顶堆为空
if not self.max_heap:
return None
# 返回堆顶元素
return self.max_heap[0]
访问堆顶元素不依赖于数组中元素个数,因此时间复杂度为 O ( 1 ) O(1) O(1)。
向堆中插入元素:指的将一个新的元素添加到堆中,调整堆结构,以保持堆的特性不变。
向堆中插入元素的步骤如下:
这个过程称为「上移调整(Shift Up)」。因为新插入的元素会逐步向堆的上方移动,直到找到了合适的位置,保持堆的有序性。
<1>
<2>
<3>
<4>
<5>
<6>
<7>
class MaxHeap:
......
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):
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 ( log n ) O(\log n) O(logn),其中 n n n 是堆中元素的数量,这是因为堆的高度是 log n \log n logn。
删除堆顶元素:指的是从堆中移除位于堆顶的元素,并重新调整对结果,以保持堆的特性不变。
删除堆顶元素的步骤如下:
这个过程称为「下移调整(Shift Down)」。因为新的堆顶元素会逐步向堆的下方移动,直到找到了合适的位置,保持堆的有序性。
<1>
<2>
<3>
<4>
<5>
<6>
<7>
class MaxHeap:
......
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()
# 节点数减 1
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 2 * i + 2 >= 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 ( log n ) O(\log n) O(logn),其中 n n n 是堆中元素的数量,因为堆的高度是 log n \log n logn。
堆排序(Heap sort)基本思想:
借用「堆结构」所设计的排序算法。将数组转化为大顶堆,重复从大顶堆中取出数值最大的节点,并让剩余的堆结构继续维持大顶堆性质。
构建初始大顶堆:
交换元素,调整堆:
重复交换和调整堆:
<1>
<2>
<3>
<4>
<5>
<6>
<7>
<1>
<2>
<3>
<4>
<5>
<6>
<7>
<8>
<9>
<10>
<11>
<12>
class MaxHeap:
......
def __buildMaxHeap(self, nums: [int]):
size = len(nums)
# 先将数组 nums 的元素按顺序添加到 max_heap 中
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]:
# 根据数组 nums 建立初始堆
self.__buildMaxHeap(nums)
size = len(self.max_heap)
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)
print(Solution().sortArray([10, 25, 6, 8, 7, 1, 20, 23, 16, 19, 17, 3, 18, 14]))
描述:给定一个数组 nums
,元素值只有 0
、1
、2
,分别代表红色、白色、蓝色。
要求:将数组进行排序,使得红色在前,白色在中间,蓝色在最后。
说明:
nums[i]
为 0
、1
或 2
。示例:
输入:nums = [2,0,2,1,1,0]
输出:[0,0,1,1,2,2]
输入:nums = [2,0,1]
输出:[0,1,2]
快速排序算法中的 partition
过程,利用双指针,将序列中比基准数 pivot
大的元素移动到了基准数右侧,将比基准数 pivot
小的元素移动到了基准数左侧。从而将序列分为了三部分:比基准数小的部分、基准数、比基准数大的部分。
这道题我们也可以借鉴快速排序算法中的 partition
过程,将 1
作为基准数 pivot
,然后将序列分为三部分:0
(即比 1
小的部分)、等于 1
的部分、2
(即比 1
大的部分)。具体步骤如下:
left
、right
,分别指向数组的头尾。left
表示当前处理好红色元素的尾部,right
表示当前处理好蓝色的头部。index
遍历数组,如果遇到 nums[index] == 0
,就交换 nums[index]
和 nums[left]
,同时将 left
右移。如果遇到 nums[index] == 2
,就交换 nums[index]
和 nums[right]
,同时将 right
左移。index
移动到 right
位置之后,停止遍历。遍历结束之后,此时 left
左侧都是红色,right
右侧都是蓝色。注意:移动的时候需要判断 index
和 left
的位置,因为 left
左侧是已经处理好的数组,所以需要判断 index
的位置是否小于 left
,小于的话,需要更新 index
位置。
class Solution:
def sortColors(self, nums: List[int]) -> None:
n = len(nums)
p0, p2 = 0, n - 1
i = 0
while i <= p2:
while i <= p2 and nums[i] == 2:
nums[i], nums[p2] = nums[p2], nums[i]
p2 -= 1
if nums[i] == 0:
nums[i], nums[p0] = nums[p0], nums[i]
p0 += 1
i += 1
描述:给定一个未排序的整数数组 nums
和一个整数 k
。
要求:返回数组中第 k
个最大的元素。
说明:
示例:
输入: [3,2,1,5,6,4], k = 2
输出: 5
输入: [3,2,3,1,2,4,5,5,6], k = 4
输出: 4
使用快速排序在每次调整时,都会确定一个元素的最终位置,且以该元素为界限,将数组分成了左右两个子数组,左子数组中的元素都比该元素小,右子树组中的元素都比该元素大。
这样,只要某次划分的元素恰好是第 k
个下标就找到了答案。并且我们只需关注第 k
个最大元素所在区间的排序情况,与第 k
个最大元素无关的区间排序都可以忽略。这样进一步减少了执行步骤。
import random
class Solution:
# 从 arr[low: high + 1] 中随机挑选一个基准数,并进行移动排序
def randomPartition(self, arr: [int], low: int, high: int):
# 随机挑选一个基准数
i = random.randint(low, high)
# 将基准数与最低位互换
arr[i], arr[low] = arr[low], arr[i]
# 以最低位为基准数,然后将序列中比基准数大的元素移动到基准数右侧,比他小的元素移动到基准数左侧。最后将基准数放到正确位置上
return self.partition(arr, low, high)
# 以最低位为基准数,然后将序列中比基准数大的元素移动到基准数右侧,比他小的元素移动到基准数左侧。最后将基准数放到正确位置上
def partition(self, arr: [int], low: int, high: int):
pivot = arr[low] # 以第 1 为为基准数
i = low + 1 # 从基准数后 1 位开始遍历,保证位置 i 之前的元素都小于基准数
for j in range(i, high + 1):
# 发现一个小于基准数的元素
if arr[j] < pivot:
# 将小于基准数的元素 arr[j] 与当前 arr[i] 进行换位,保证位置 i 之前的元素都小于基准数
arr[i], arr[j] = arr[j], arr[i]
# i 之前的元素都小于基准数,所以 i 向右移动一位
i += 1
# 将基准节点放到正确位置上
arr[i - 1], arr[low] = arr[low], arr[i - 1]
# 返回基准数位置
return i - 1
def quickSort(self, arr, low, high, k):
size = len(arr)
if low < high:
# 按照基准数的位置,将序列划分为左右两个子序列
pi = self.randomPartition(arr, low, high)
if pi == size - k:
return arr[size - k]
if pi > size - k:
# 对左子序列进行递归快速排序
self.quickSort(arr, low, pi - 1, k)
if pi < size - k:
# 对右子序列进行递归快速排序
self.quickSort(arr, pi + 1, high, k)
return arr[size - k]
def findKthLargest(self, nums: List[int], k: int) -> int:
return self.quickSort(nums, 0, len(nums) - 1, k)
描述:给定整数数组 arr
,再给定一个整数 k
。
要求:返回数组 arr
中最小的 k
个数。
说明:
示例:
输入:arr = [3,2,1], k = 2
输出:[1,2] 或者 [2,1]
输入:arr = [0,1,2,1], k = 1
输出:[0]
对原数组从小到大排序后取出前 k
个数即可。
class Solution:
def getLeastNumbers(self, arr: List[int], k: int) -> List[int]:
arr.sort()
return arr[:k]
计数排序(Counting Sort)基本思想:
通过统计数组中每个元素在数组中出现的次数,根据这些统计信息将数组元素有序的放置到正确位置,从而达到排序的目的。
计算排序范围:遍历数组,找出待排序序列中最大值元素 n u m s ‾ m a x nums\underline{}max numsmax 和最小值元素 n u m s ‾ m i n nums\underline{}min numsmin,计算出排序范围为 n u m s ‾ m a x − n u m s ‾ m i n + 1 nums\underline{}max - nums\underline{}min + 1 numsmax−numsmin+1。
定义计数数组:定义一个大小为排序范围的计数数组 c o u n t s counts counts,用于统计每个元素的出现次数。其中:
对数组元素进行计数统计:遍历待排序数组 n u m s nums nums,对每个元素在计数数组中进行计数,即将待排序数组中「每个元素值减去最小值」作为索引,将「对计数数组中的值」加 1 1 1,即令 c o u n t s [ n u m − n u m s ‾ m i n ] counts[num - nums\underline{}min] counts[num−numsmin] 加 1 1 1。
生成累积计数数组:从 c o u n t s counts counts 中的第 1 1 1 个元素开始,每一项累家前一项和。此时 c o u n t s [ n u m − n u m s ‾ m i n ] counts[num - nums\underline{}min] counts[num−numsmin] 表示值为 n u m num num 的元素在排序数组中最后一次出现的位置。
逆序填充目标数组:逆序遍历数组 n u m s nums nums,将每个元素 n u m num num 填入正确位置。
将其填充到结果数组 r e s res res 的索引 c o u n t s [ n u m − n u m s ‾ m i n ] counts[num - nums\underline{}min] counts[num−numsmin] 处。
放入后,令累积计数数组中对应索引减 1 1 1,从而得到下个元素 n u m num num 的放置位置。
我们以 [ 3 , 0 , 4 , 2 , 5 , 1 , 3 , 1 , 4 , 5 ] [3, 0, 4, 2, 5, 1, 3, 1, 4, 5] [3,0,4,2,5,1,3,1,4,5] 为例,演示一下计数排序的整个步骤。
class Solution:
def countingSort(self, nums: [int]) -> [int]:
# 计算待排序数组中最大值元素 nums_max 和最小值元素 nums_min
nums_min, nums_max = min(nums), max(nums)
# 定义计数数组 counts,大小为 最大值元素 - 最小值元素 + 1
size = nums_max - nums_min + 1
counts = [0 for _ in range(size)]
# 统计值为 num 的元素出现的次数
for num in nums:
counts[num - nums_min] += 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]
# 根据累积计数数组,将 num 放在数组对应位置
res[counts[num - nums_min] - 1] = num
# 将 num 的对应放置位置减 1,从而得到下个元素 num 的放置位置
counts[nums[i] - nums_min] -= 1
return res
def sortArray(self, nums: [int]) -> [int]:
return self.countingSort(nums)
桶排序(Bucket Sort)基本思想:
将待排序数组中的元素分散到若干个「桶」中,然后对每个桶中的元素再进行单独排序。
我们以 [ 39 , 49 , 8 , 13 , 22 , 15 , 10 , 30 , 5 , 44 ] [39, 49, 8, 13, 22, 15, 10, 30, 5, 44] [39,49,8,13,22,15,10,30,5,44] 为例,演示一下桶排序的整个步骤。
class Solution:
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], bucket_size=5) -> [int]:
# 计算待排序序列中最大值元素 nums_max、最小值元素 nums_min
nums_min, nums_max = min(nums), max(nums)
# 定义桶的个数为 (最大值元素 - 最小值元素) // 每个桶的大小 + 1
bucket_count = (nums_max - nums_min) // bucket_size + 1
# 定义桶数组 buckets
buckets = [[] for _ in range(bucket_count)]
# 遍历待排序数组元素,将每个元素根据大小分配到对应的桶中
for num in nums:
buckets[(num - nums_min) // bucket_size].append(num)
# 对每个非空桶内的元素单独排序,排序之后,按照区间顺序依次合并到 res 数组中
res = []
for bucket in buckets:
self.insertionSort(bucket)
res.extend(bucket)
# 返回结果数组
return res
def sortArray(self, nums: [int]) -> [int]:
return self.bucketSort(nums)
基数排序(Radix Sort)基本思想:
将整数按位数切割成不同的数字,然后从低位开始,依次到高位,逐位进行排序,从而达到排序的目的。
基数排序算法可以采用「最低位优先法(Least Significant Digit First)」或者「最高位优先法(Most Significant Digit first)」。最常用的是「最低位优先法」。
下面我们以最低位优先法为例,讲解一下算法步骤。
我们以 [ 692 , 924 , 969 , 503 , 871 , 704 , 542 , 436 ] [692, 924, 969, 503, 871, 704, 542, 436] [692,924,969,503,871,704,542,436] 为例,演示一下基数排序的整个步骤。
class Solution:
def radixSort(self, nums: [int]) -> [int]:
# 桶的大小为所有元素的最大位数
size = len(str(max(nums)))
# 从最低位(个位)开始,逐位遍历每一位
for i in range(size):
# 定义长度为 10 的桶数组 buckets,每个桶分别代表 0 ~ 9 中的 1 个数字。
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
def sortArray(self, nums: [int]) -> [int]:
return self.radixSort(nums)
描述:给定两个数组,arr1
和 arr2
,其中 arr2
中的元素各不相同,arr2
中的每个元素都出现在 arr1
中。
要求:对 arr1
中的元素进行排序,使 arr1
中项的相对顺序和 arr2
中的相对顺序相同。未在 arr2
中出现过的元素需要按照升序放在 arr1
的末尾。
说明:
示例:
输入:arr1 = [2,3,1,3,2,4,6,7,9,2,19], arr2 = [2,1,4,3,9,6]
输出:[2,2,2,1,4,3,3,9,6,7,19]
输入:arr1 = [28,6,22,8,44,17], arr2 = [22,28,8,6]
输出:[22,28,8,6,17,44]
因为元素值范围在 [0, 1000]
,所以可以使用计数排序的思路来解题。
count
统计 arr1
各个元素个数。arr2
数组,将对应元素num2
按照个数 count[num2]
添加到答案数组 ans
中,同时在 count
数组中减去对应个数。count
中剩余元素,将 count
中大于 0
的元素下标依次添加到答案数组 ans
中。ans
。class Solution:
def relativeSortArray(self, arr1: List[int], arr2: List[int]) -> List[int]:
# 计算待排序序列中最大值元素 arr_max 和最小值元素 arr_min
arr1_min, arr1_max = min(arr1), max(arr1)
# 定义计数数组 counts,大小为 最大值元素 - 最小值元素 + 1
size = arr1_max - arr1_min + 1
counts = [0 for _ in range(size)]
# 统计值为 num 的元素出现的次数
for num in arr1:
counts[num - arr1_min] += 1
res = []
for num in arr2:
while counts[num - arr1_min] > 0:
res.append(num)
counts[num - arr1_min] -= 1
for i in range(size):
while counts[i] > 0:
num = i + arr1_min
res.append(num)
counts[i] -= 1
return res
描述:给定一个整数数组 nums
,以及两个整数 k
、t
。
要求:判断数组中是否存在两个不同下标的 i
和 j
,其对应元素满足 abs(nums[i] - nums[j]) <= t
,同时满足 abs(i - j) <= k
。如果满足条件则返回 True
,不满足条件返回 False
。
说明:
示例:
输入:nums = [1,2,3,1], k = 3, t = 0
输出:True
输入:nums = [1,0,1,1], k = 1, t = 2
输出:True
k
的滑动窗口,每次遍历到 nums[right]
时,滑动窗口内最多包含 nums[right]
之前最多 k
个元素。只需要检查前 k
个元素是否在 [nums[right] - t, nums[right] + t]
区间内即可。k
个元素是否在 [nums[right] - t, nums[right] + t]
区间,可以借助保证有序的数据结构(比如 SortedList
)+ 二分查找来解决,从而减少时间复杂度。具体步骤如下:
window
维护一个长度为 k
的窗口,满足数组内元素有序,且支持增加和删除操作。left
、right
都指向序列的第一个元素。即:left = 0
,right = 0
。window.add(nums[right])
。k
个时,即 right - left > k
,移除窗口最左侧元素,并向右移动 left
。k
个时:
nums[right]
在 window
中的位置 idx
。window[idx]
与相邻位置上元素差值绝对值,若果满足 abs(window[idx] - window[idx - 1]) <= t
或者 abs(window[idx + 1] - window[idx]) <= t
时返回 True
。right
。3
~ 6
步,直到 right
到达数组末尾,如果还没找到满足条件的情况,则返回 False
。from sortedcontainers import SortedList
class Solution:
def containsNearbyAlmostDuplicate(self, nums: List[int], k: int, t: int) -> bool:
size = len(nums)
window = SortedList()
left, right = 0, 0
while right < size:
window.add(nums[right])
if right - left > k:
window.remove(nums[left])
left += 1
idx = bisect.bisect_left(window, nums[right])
if idx > 0 and abs(window[idx] - window[idx - 1]) <= t:
return True
if idx < len(window) - 1 and abs(window[idx + 1] - window[idx]) <= t:
return True
right += 1
return False
描述:给定一个无序数组 nums
。
要求:找出数组在排序之后,相邻元素之间最大的差值。如果数组元素个数小于 2
,则返回 0
。
说明:
32
位有符号整数范围内。示例:
输入: nums = [3,6,9,1]
输出: 3
解释: 排序后的数组是 [1,3,6,9], 其中相邻元素 (3,6) 和 (6,9) 之间都存在最大差值 3。
输入: nums = [10]
输出: 0
解释: 数组元素个数小于 2,因此返回 0。
这道题的难点在于要求时间复杂度和空间复杂度为 O ( n ) O(n) O(n)。
这道题分为两步:
第 2 步直接遍历数组求解即可,时间复杂度为 O ( n ) O(n) O(n)。所以关键点在于找到一个时间复杂度和空间复杂度为 O ( n ) O(n) O(n) 的排序算法。根据题意可知所有元素都是非负整数,且数值在 32 位有符号整数范围内。所以我们可以选择基数排序。基数排序的步骤如下:
最后,还要注意数组元素个数小于 2 的情况需要特别判断一下。
class Solution:
def radixSort(self, arr):
size = len(str(max(arr)))
for i in range(size):
buckets = [[] for _ in range(10)]
for num in arr:
buckets[num // (10 ** i) % 10].append(num)
arr.clear()
for bucket in buckets:
for num in bucket:
arr.append(num)
return arr
def maximumGap(self, nums: List[int]) -> int:
if len(nums) < 2:
return 0
arr = self.radixSort(nums)
return max(arr[i] - arr[i - 1] for i in range(1, len(arr)))
题号 | 标题 | 题解 | 标签 | 难度 |
---|---|---|---|---|
剑指 Offer 45 | 把数组排成最小的数 | 网页链接、Github 链接 | 贪心、字符串、排序 | 中等 |
0283 | 移动零 | 网页链接、Github 链接 | 数组、双指针 | 简单 |
题号 | 标题 | 题解 | 标签 | 难度 |
---|---|---|---|---|
0215 | 数组中的第K个最大元素 | 网页链接、Github 链接 | 数组、分治、快速选择、排序、堆(优先队列) | 中等 |
题号 | 标题 | 题解 | 标签 | 难度 |
---|---|---|---|---|
0075 | 颜色分类 | 网页链接、Github 链接 | 数组、双指针、排序 | 中等 |
题号 | 标题 | 题解 | 标签 | 难度 |
---|---|---|---|---|
0912 | 排序数组 | 网页链接、Github 链接 | 数组、分治、桶排序、计数排序、基数排序、排序、堆(优先队列)、归并排序 | 中等 |
0506 | 相对名次 | 网页链接、Github 链接 | 数组、排序、堆(优先队列) | 简单 |
题号 | 标题 | 题解 | 标签 | 难度 |
---|---|---|---|---|
0912 | 排序数组 | 网页链接、Github 链接 | 数组、分治、桶排序、计数排序、基数排序、排序、堆(优先队列)、归并排序 | 中等 |
0088 | 合并两个有序数组 | 网页链接、Github 链接 | 数组、双指针、排序 | 简单 |
剑指 Offer 51 | 数组中的逆序对 | 网页链接、Github 链接 | 树状数组、线段树、数组、二分查找、分治、有序集合、归并排序 | 困难 |
0315 | 计算右侧小于当前元素的个数 | 树状数组、线段树、数组、二分查找、分治、有序集合、归并排序 | 困难 |
题号 | 标题 | 题解 | 标签 | 难度 |
---|---|---|---|---|
0912 | 排序数组 | 网页链接、Github 链接 | 数组、分治、桶排序、计数排序、基数排序、排序、堆(优先队列)、归并排序 | 中等 |
0169 | 多数元素 | 网页链接、Github 链接 | 数组、哈希表、分治、计数、排序 | 简单 |
题号 | 标题 | 题解 | 标签 | 难度 |
---|---|---|---|---|
0912 | 排序数组 | 网页链接、Github 链接 | 数组、分治、桶排序、计数排序、基数排序、排序、堆(优先队列)、归并排序 | 中等 |
0215 | 数组中的第K个最大元素 | 网页链接、Github 链接 | 数组、分治、快速选择、排序、堆(优先队列) | 中等 |
剑指 Offer 40 | 最小的k个数 | 网页链接、Github 链接 | 数组、分治、快速选择、排序、堆(优先队列) | 简单 |
题号 | 标题 | 题解 | 标签 | 难度 |
---|---|---|---|---|
0912 | 排序数组 | 网页链接、Github 链接 | 数组、分治、桶排序、计数排序、基数排序、排序、堆(优先队列)、归并排序 | 中等 |
1122 | 数组的相对排序 | 网页链接、Github 链接 | 数组、哈希表、计数排序、排序 | 简单 |
题号 | 标题 | 题解 | 标签 | 难度 |
---|---|---|---|---|
0912 | 排序数组 | 网页链接、Github 链接 | 数组、分治、桶排序、计数排序、基数排序、排序、堆(优先队列)、归并排序 | 中等 |
0220 | 存在重复元素 III | 网页链接、Github 链接 | 数组、桶排序、有序集合、排序、滑动窗口 | 困难 |
0164 | 最大间距 | 网页链接、Github 链接 | 数组、桶排序、基数排序、排序 | 困难 |
题号 | 标题 | 题解 | 标签 | 难度 |
---|---|---|---|---|
0164 | 最大间距 | 网页链接、Github 链接 | 数组、桶排序、基数排序、排序 | 困难 |
0561 | 数组拆分 | 贪心、数组、计数排序、排序 | 简单 |
题号 | 标题 | 题解 | 标签 | 难度 |
---|---|---|---|---|
0217 | 存在重复元素 | 网页链接、Github 链接 | 数组、哈希表、排序 | 简单 |
0136 | 只出现一次的数字 | 网页链接、Github 链接 | 位运算、数组 | 简单 |
0056 | 合并区间 | 网页链接、Github 链接 | 数组、排序 | 中等 |
0179 | 最大数 | 网页链接、Github 链接 | 贪心、数组、字符串、排序 | 中等 |
0384 | 打乱数组 | 数组、数学、随机化 | 中等 | |
剑指 Offer 45 | 把数组排成最小的数 | 网页链接、Github 链接 | 贪心、字符串、排序 | 中等 |