文章目录
- 排序
- 一、排序算法
- 1.1 冒泡排序
- 1.1.1 算法步骤
- 1.1.2 算法分析
- 1.1.3 代码实现:
- 1.1.4 冒泡排序优化
- 1.2 选择排序
- 1.2.1 算法步骤
- 1.2.2 算法分析
- 1.2.3 代码实现
- 1.3 插入排序
- 1.3.1 算法步骤
- 1.3.2 算法分析
- 1.3.3 代码实现
- 1.4 希尔排序
- 1.4.1 算法步骤
- 1.4.2 算法分析
- 1.4.3 代码实现:
- 1.5 归并排序
- 1.5.1 算法步骤
- 1.5.2 算法分析
- 1.5.3 代码实现:
- 1.6 快速排序
- 1.6.1 算法步骤
- 1.6.2 算法分析
- 1.6.3 代码实现:
- 1.7 桶排序
- 1.7.1 算法步骤
- 1.7.2 算法分析
- 1.7.3 代码实现
- 1.8 堆排序
- 1.8.1 算法思想
- 1.8.2 算法步骤
- 1.8.3 调整堆
- 1.8.4 建立初始堆
- 1.8.5 堆排序方法完整演示
- 1.8.6 堆排序算法分析
- 1.8.7 代码实现
- 1.9 计数排序
- 1.9.1 算法步骤
- 1.9.2 算法分析
- 1.9.3. 代码实现
- 1.10 基数排序
- 1.10.1 算法步骤
- 1.10.2 算法分析
- 1.10.3 代码实现
- 1.11 排序算法总结
- 二、 练习题
- 2.1 移动零(题283)
- 2.2 颜色分类(题75)
- 2.3 数组中的第K个最大元素(题215)
- 2.3.1 快速排序
- 2.3.2 堆排序
- 2.3.3 优先队列
- 2.4 排序数组(题912)
- 2.5 数组中的逆序对(剑指 Offer 51)
- 2.6 计算右侧小于当前元素的个数(题315)
- 2.6.1 归并排序
- 2.6.2 有序数组 (Sorted List)+ 二分搜索
- 2.7 最大间距 (题164)
- 2.7.1 基数排序
- 2.7.2 桶排序
- 2.7.3 你的代码真是无敌了
- 2.8 数组的相对顺序(题1122)
- 2.9 存在重复元素 III(题220)
- 2.9.1 桶排序
- 2.9.2滑动窗口(固定长度)
- 2.10 合并区间(题56)
- 2.11 最大数(题179)
- 2.11.1 自定义排序(内置函数)
- 2.11.2 自定义排序(快速排序)
资源:力扣题库、LeetCode 刷题列表、代码随想录
排序
参考《数组排序》、《排序算法总结(Python版)》
一、排序算法
1.1 冒泡排序
1.1.1 算法步骤
冒泡排序(Bubble Sort
)基本思想:通过相邻元素之间的比较与交换,使值较小的元素逐步从后面移到前面,值较大的元素从前面移到后面。这个过程就像水底的气泡一样向上冒,这也是冒泡排序法名字的由来。
动画演示:
1.1.2 算法分析
- 时间复杂度: O ( n ) O(n) O(n)到 O ( n 2 ) O(n^2) O(n2)
- 冒泡排序适用情况:冒泡排序方法在排序过程中需要移动较多次数的元素,并且排序时间效率比较低。因此,冒泡排序方法比较适合于参加排序序列的数据量较小的情况,尤其是当序列的初始状态为基本有序的情况。
- 排序稳定性:由于元素交换是在相邻元素之间进行的,不会改变值相同元素的相对位置,因此,冒泡排序法是一种 稳定排序算法。
1.1.3 代码实现:
class Solution:
def bubbleSort(self, nums):
# 第 i 趟排序
for i in range(len(nums)):
# 大数排后,所以每次是从序列中前 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]
return nums
def sortnumsay(self, nums: List[int]) -> List[int]:
return self.bubbleSort(nums)
1.1.4 冒泡排序优化
优化1: 某一趟遍历如果没有数据交换,则说明已经排好序了,因此不用再进行迭代了。用一个标记记录这个状态即可。
def bubble_sort2(nums):
n = len(nums)
for i in range(n):
flag = True # 标记
for j in range(1, n - i):
if nums[j] < nums[j-1]:
nums[j], nums[j-1] = nums[j-1], nums[j]
flag = False
# 某一趟遍历如果没有数据交换,则说明已经排好序了,因此不用再进行迭代了
if flag:
break
return nums
优化2: 记录某次遍历时最后发生数据交换的位置,这个位置之后的数据显然已经有序,不用再排序了。因此通过记录最后发生数据交换的位置就可以确定下次循环的范围了。
def bubble_sort3(nums):
n = len(nums)
k = n #k为循环的范围,初始值n
for i in range(n):
flag = True
for j in range(1, k): #只遍历到最后交换的位置即可
if nums[j-1] > nums[j]:
nums[j-1], nums[j] = nums[j], nums[j-1]
k = j #记录最后交换的位置
flag = False
if flag:
break
return nums
1.2 选择排序
选择排序(Selection Sort
)基本思想:每一趟排序中,从未排序部分中选出一个值最小的元素,与未排序部分第 1 个元素交换位置,从而将该元素划分到已排序部分。
1.2.1 算法步骤
- 第
1
趟排序:- 无已排序部分,把第
1
~n
个元素(总共n
个元素)作为未排序部分。 - 遍历
n
个元素,使用变量min_i
记录n
个元素中值最小的元素位置。 - 将
min_i
与未排序部分第1
个元素(也就是序列的第1
个元素)交换位置。如果未排序部分第1
个元素就是值最小的元素位置,则不用交换。 - 此时第
1
个元素为已排序部分,剩余第2
~n
个元素(总共n - 1
个元素)为未排序部分。
- 无已排序部分,把第
- 第
2
趟排序:- 遍历剩余
n - 1
个元素,使用变量min_i
记录n - 1
个元素中值最小的元素位置。 - 将
min_i
与未排序部分第1
个元素(也就是序列的第2
个元素)交换位置。如果未排序部分第1
个元素就是值最小的元素位置,则不用交换。 - 此时第
1
~2
个元素为已排序部分,剩余第3
~n
个元素(总共n - 2
个元素)为未排序部分。
- 遍历剩余
- 依次类推,对剩余
n - 2
个元素重复上述排序过程,直到所有元素都变为已排序部分,则排序结束。
动画演示
1.2.2 算法分析
- 时间复杂度:
O
(
n
2
)
O(n^2)
O(n2)。
排序法所进行的元素之间的比较次数与序列的原始状态无关,时间复杂度总是 O ( n 2 ) O(n^2) O(n2)。这是因为无论序列中元素的初始排列状态如何,第 i 趟排序要找出值最小元素都需要进行 n − i 次元素之间的比较。因此,整个排序过程需要进行的元素之间的比较次数都相同,为 ∑ i = 2 n ( i − 1 ) = n ( n − 1 ) 2 \sum_{i=2}^{n}(i-1)=\frac{n(n-1)}{2} ∑i=2n(i−1)=2n(n−1) 次。 - 适用情况:选择排序方法在排序过程中需要移动较多次数的元素,并且排序时间效率比较低。因此,选择排序方法比较适合于参加排序序列的数据量较小的情况。选择排序的主要优点是仅需要原地操作无需占用其他空间就可以完成排序,因此在空间复杂度要求较高时,可以考虑选择排序。
- 排序稳定性:由于值最小元素与未排序部分第 1 个元素的交换动作是在不相邻的元素之间进行的,因此很有可能会改变值相同元素的前后位置,因此,选择排序法是一种 不稳定排序算法。
- 对比冒泡排序:
- 选择排序对冒泡排序进行了改进,保留了其基本的多趟比对思路,每趟都使当前最大项就位。
- 但选择排序对交换进行了削减,相比起冒泡排序进行多次交换,每趟仅进行1次交换,即记录最大项的所在位置,最后再跟本趟最后一项交换。
- 选择排序的时间复杂度比冒泡排序潲优。比对次数不变,还是 O ( n 2 ) O(n^2) O(n2),交换次数则减少为 O ( n ) O(n) O(n)。
1.2.3 代码实现
class Solution:
def selectionSort(self, nums):
for i in range(len(nums) - 1):
# 记录未排序部分中最小值的位置
min_i = i
# 序列分成两部分,前i个是已排序部分,i+1到le是未排序部分
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 sortnumsay(self, nums: List[int]) -> List[int]:
return self.selectionSort(nums)
1.3 插入排序
插入排序(Insertion Sort
)基本思想:将整个序列分为两部分:前面 i
个元素为有序序列,后面 n - i
个元素为无序序列。每一次排序,将无序序列的第 1
个元素,在有序序列中找到相应的位置并插入。
1.3.1 算法步骤
-
第
1
趟排序:- 第
1
个元素为有序序列,后面第2
~n
个元素(总共n - 1
个元素)为无序序列。 - 从右至左遍历有序序列中的元素,如果遇到「有序序列的元素 > 无序序列的第
1
个元素」的情况时,则将向有序序列的元素后移动一位。 - 如果遇到「有序序列的元素 <= 无序序列的第
1
个元素」的情况或者「到达数组开始位置」时,则说明找到了插入位置。将「无序序列的第1
个元素」插入该位置。
- 第
-
第
2
趟排序:- 第
1
~2
个元素为有序序列,后面第3
~n
个元素(总共n - 2
个元素)为无序序列。 - 从右至左遍历有序序列中的元素,如果遇到「有序序列的元素 > 无序序列的第
1
个元素」的情况时,则将向有序序列的元素后移动一位。 - 如果遇到「有序序列的元素 <= 无序序列的第
1
个元素」的情况或者「到达数组开始位置」时,则说明找到了插入位置。将「无序序列的第1
个元素」插入该位置。
- 第
-
依次类推,对剩余
n - 3
个元素重复上述排序过程,直到所有元素都变为有序序列,则排序结束。
简单来说,插入排序的算法步骤为:
- 先将第
1
个元素作为一个有序序列,将第2
~n
个元素作为无序序列。 - 从左到右遍历一遍无序序列,对于无序序列中的每一个元素:
- 遍历有序序列,找到适当的插入位置。
- 将有序序列中插入位置右侧的元素依次右移一位。
- 将该元素插入到适当位置
动画演示:
1.3.2 算法分析
插入排序比对操作主要是用来寻找新元素的待插入位置,而插入位置是靠倒序遍历前面有序数组来找到的。
- 最佳时间复杂度:
O
(
n
)
O(n)
O(n)。
最好的情况下,初始序列已经是升序排列,这样每个新元素只需要进行一次元素之间的比较, 总共只需要比对n − 1次,也完全并不需要移动有序数组的元素,此时时间复杂度是 O ( n ) O(n) O(n) - 最差时间复杂度:
O
(
n
2
)
O(n^2)
O(n2)。
最差的情况下,初始序列已经是降序排列,对应的每个i
值都要进行i - 1
次元素之间的比较,总的元素之间的比较次数达到最大值,为 ∑ i = 2 n ( i − 1 ) = n ( n − 1 ) 2 ∑^n_{i=2}(i − 1) = \frac{n(n−1)}{2} ∑i=2n(i−1)=2n(n−1)。 - 平均时间复杂度: O ( n 2 ) O(n^2) O(n2)。如果序列的初始情况是随机的,即参加排序的序列中元素可能出现的各种排列的概率相同,则可取上述最小值和最大值的平均值作为插入排序时所进行的元素之间的比较次数,约为 n 2 4 \frac{n^2}{4} 4n2。由此得知,插入排序算法的时间复杂度 O ( n 2 ) O(n^2) O(n2)。
- 排序稳定性:插入排序方法是一种 稳定排序算法。
1.3.3 代码实现
class Solution:
def insertionSort(self, nums):
# 遍历无序序列
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 sortnumsay(self, nums: List[int]) -> List[int]:
return self.insertionSort(nums)
也可以都改成for语句:
class Solution:
def insertionSort(self, nums):
# 遍历无序序列
for i in range(1, len(nums)):
temp = nums[i] # 保存当前元素值
# 从右至左遍历有序序列
for j in range(i,-1,-1):
if nums[j - 1] > temp:
# 将有序序列中插入位置右侧的元素依次右移一位
nums[j] = nums[j - 1]
else:
break
# 将该元素插入到适当位置
nums[j] = temp
return nums
def sortnumsay(self, nums: List[int]) -> List[int]:
return self.insertionSort(nums)
1.4 希尔排序
从上面分析可以知道,插入排序在初始序列升序排列时,时间复杂度最低。实际上,序列越是有序(升序),插入排序的比对次数就越少。
希尔排序从此入手,对无序列表进行间隔划分,然后对每个子列表进行插入排序。随着子列表数越来越少,整个无序表也越来越有序,从而减少整体排序的比对次数。
希尔排序(Shell Sort
)基本思想:将整个序列切按照一定的间隔值(gap
)划分为若干个子序列,每个子序列分别排序。然后逐渐缩小gap
值,进行下一次排序,直至gap=1
。
1.4.1 算法步骤
- 确定一个元素间隔数
gap
。 - 将参加排序的序列按此间隔数从第
1
个元素开始一次分成若干个子序列,即分别将所有位置相隔为gap
的元素视为一个子序列。 - 在各个子序列中采用某种排序算法(例如插入排序算法)进行排序。
- 减少间隔数,并重新将整个序列按新的间隔数分成若干个子序列,再分别对各个子序列进行排序。依次类推,直到间隔数
gap = 1
,排序结束。
图解演示:
1.4.2 算法分析
-
时间复杂度:介于 O ( n × log 2 n ) O(n \times \log_2 n) O(n×log2n) 与 O ( n 2 ) O(n^2) O(n2) 之间。
- 希尔排序方法的速度是一系列间隔数 g a p i gap_i gapi 的函数,而比较次数与 g a p i gap_i gapi 之间的依赖关系比较复杂,不太容易给出完整的数学分析。
- 由于采用 g a p i = ⌊ g a p i − 1 / 2 ⌋ gap_i = \lfloor gap_{i-1}/2 \rfloor gapi=⌊gapi−1/2⌋ 的方法缩小间隔数,对于具有 n n n 个元素的序列,若 g a p 1 = ⌊ n / 2 ⌋ gap_1 = \lfloor n/2 \rfloor gap1=⌊n/2⌋,则经过 p = ⌊ log 2 n ⌋ p = \lfloor \log_2 n \rfloor p=⌊log2n⌋ 趟排序后就有 g a p p = 1 gap_p = 1 gapp=1,因此,希尔排序方法的排序总躺数为 ⌊ log 2 n ⌋ \lfloor \log_2 n \rfloor ⌊log2n⌋。
- 从算法中也可以看到,最外层的
while
循环为 log 2 n \log_2 n log2n 数量级,中间层do-while
循环为n
数量级。当子序列分得越多时,子序列内的元素就越少,最内层的for
循环的次数也就越少;反之,当所分的子序列个数减少时,子序列内的元素也随之增多,但整个序列也逐步接近有序,而循环次数却不会随之增加。因此,希尔排序算法的时间复杂度在 O ( n × log 2 n ) O(n \times \log_2 n) O(n×log2n) 与 O ( n 2 ) O(n^2) O(n2) 之间。
-
排序稳定性:希尔排序方法是一种 不稳定排序算法。
1.4.3 代码实现:
class Solution:
def shellSort(self, nums):
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 sortnumsay(self, nums: List[int]) -> List[int]:
return self.shellSort(nums)
1.5 归并排序
归并排序(Merge Sort
)基本思想:采用经典的分治策略,先递归地将当前序列平均分成两半。然后将有序序列两两合并,最终合并成一个有序序列。
1.5.1 算法步骤
- 分割过程:先递归地将当前序列平均分成两半,直到子序列长度为 1。
- 找到序列中心位置
mid
,从中心位置将序列分成左右两个子序列left_arr
、right_arr
。 - 对左右两个子序列
left_arr
、right_arr
分别进行递归分割。 - 最终将数组分割为 n 个长度均为 1 的有序子序列。
- 找到序列中心位置
- 归并过程:从长度为 1 的有序子序列开始,依次进行两两归并,直到合并成一个长度为 n 的有序序列。
- 使用数组变量
arr
存放归并后的有序数组。 - 使用两个指针
left
、right
分别指向两个有序子序列 left_arr、right_arr 的开始位置。 - 比较两个指针指向的元素,将两个有序子序列中较小元素依次存入到结果数组
arr
中,并将指针移动到下一位置。 - 重复步骤 3,直到某一指针到达子序列末尾。
- 将另一个子序列中的剩余元素存入到结果数组
arr
中。 - 返回归并后的有序数组
arr
。
- 使用数组变量
动画演示:
- 将序列分为为 [6],[2],[1],[3],[7],[5],[4],[8]。
- 第 1 趟排序:将子序列中的有序子序列两两归并,归并后的子序列为:[2, 6],[1, 3],[5, 7],[4, 8]。
- 第 2 趟排序:将子序列中的有序子序列两两归并,归并后的子序列为:[1, 2, 3, 6],[4, 5, 7, 8]。
- 第 3 趟排序:将子序列中的有序子序列两两归并,归并后的子序列为:[1, 2, 3, 4, 5, 6, 7, 8]。得到长度为 n 的有序序列,排序结束。
1.5.2 算法分析
- 时间复杂度:
O
(
n
×
log
2
n
)
O(n \times \log_2n)
O(n×log2n)。归并排序算法的时间复杂度等于归并趟数与每一趟归并的时间复杂度乘积。子算法
merge(left_arr, right_arr):
的时间复杂度是 O ( n ) O(n) O(n),因此,归并排序算法总的时间复杂度为 O ( n × log 2 n ) O(n \times \log_2 n) O(n×log2n)。 - 空间复杂度: O ( n ) O(n) O(n)。归并排序方法需要用到与参加排序的序列同样大小的辅助空间。因此算法的空间复杂度为 O ( n ) O(n) O(n)。
- 排序稳定性:归并排序算法是一种 稳定排序算法。
- 因为在两个有序子序列的归并过程中,如果两个有序序列中出现相同元素,
merge(left_arr, right_arr):
算法能够使前一个序列中那个相同元素先被复制,从而确保这两个元素的相对次序不发生改变。
- 因为在两个有序子序列的归并过程中,如果两个有序序列中出现相同元素,
1.5.3 代码实现:
class Solution(object):
def sortArray(self, nums):
"""
:type nums: List[int]
:rtype: List[int]
"""
return self.merge(nums)
def merge(self,arr): # 分割过程
if len(arr)<=1:
return arr # 数组元素个数小于等于 1 时,直接返回原数组
mid=len(arr)//2
# 注意,这里是递归的写法
left_arr=self.merge(arr[:mid])
right_arr=self.merge(arr[mid:])
return self.mergesort(left_arr,right_arr)
def mergesort(self,left_arr,right_arr): # 归并过程
left,right=0,0
arr=[]
# 将两个有序子序列中较小元素依次插入到结果数组中
# 注意,这里都是<,不能取=,因为left_arr[len(left_arr)]是超出索引的
while left<len(left_arr) and right<len(right_arr):
if left_arr[left]<right_arr[right]:
arr.append(left_arr[left])
left+=1
else:
arr.append(right_arr[right])
right+=1
# 如果子序列有剩余元素,则将其插入到结果数组中
arr=arr+left_arr[left:]+right_arr[right:]
return arr
1.6 快速排序
快速排序(Quick Sort)基本思想: 根据序列的中值将序列分为两半,前一半都小于中值,后一半都大于中值,然后每部分再进行递归的快速排序。
1.6.1 算法步骤
递归结束条件:序列只有一个元素,不需要再排序
中值选取:每次根据中值将序列分为规模相等的两半是最好的情况,此时中值就是序列的中位数,但是寻找中位数也需要开销,所以可以所以找一个数作为中值,比如序列的第一个数。
分裂过程: 目标是找到“中值”的位置。
- 设置左右标(left/right)
- 左标向右移动,右标向左移动
- 左标一直向右移动,碰到比中值大的就停止;右标一直向左移动,碰到比中值小的就停止。然后把左右标所指的数据项交换,
- 继续移动,直到左标移到右标的右侧,停止移动。这时右标所指位置就是“中值”应处的位置,将中值和这个位置交换,并记录此时中值的位置。这样就分裂完成,左半部分比中值小,右半部分比中值大。
图解演示:
动画演示:
1.6.2 算法分析
快速排序算法的时间复杂度主要跟基准数的选择有关。本文中是将当前序列中第 1
个元素作为基准值。在这种选择下,如果参加排序的元素初始时已经有序的情况下,快速排序方法花费的时间最长。也就是会得到最坏时间复杂度。
在这种情况下,第 1
趟排序经过 n - 1
次比较以后,将第 1
个元素仍然确定在原来的位置上,并得到 1
个长度为 n - 1
的子序列。第 2
趟排序进过 n - 2
次比较以后,将第 2
个元素确定在它原来的位置上,又得到 1
个长度为 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),也是最坏时间复杂度,这种情况一般不会发生。
而在平均情况下,我们可以从当前序列中随机选择一个元素作为基准数。这样,每一次选择的基准数可以看做是等概率随机的。其期望时间复杂度为 O ( n × log 2 n ) O(n \times \log_2n) O(n×log2n),也就是平均时间复杂度。
下面来总结一下:
- 最佳时间复杂度: O ( n × log 2 n ) O(n \times \log_2n) O(n×log2n)。每一次选择的基准数都是当前序列的中位数,此时算法时间复杂度满足的递推式为 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 2 n ) T(n) = O(n \times \log_2n) T(n)=O(n×log2n)。
- 最坏时间复杂度: O ( n 2 ) O(n^2) O(n2)。每一次选择的基准数都是序列的最终位置上的值,此时算法时间复杂度满足的递推式为 T ( n ) = T ( n − 1 ) + Θ ( n ) T(n) = T(n - 1) + \Theta(n) T(n)=T(n−1)+Θ(n),累加可得 T ( n ) = O ( n 2 ) T(n) = O(n^2) T(n)=O(n2)。
- 平均时间复杂度: O ( n × log 2 n ) O(n \times \log_2n) O(n×log2n)。在平均情况下,每一次选择的基准数可以看做是等概率随机的。其期望时间复杂度为 O ( n × log 2 n ) O(n \times \log_2n) O(n×log2n)。
- 空间复杂度: O ( n ) O(n) O(n)。无论快速排序算法递归与否,排序过程中都需要用到堆栈或其他结构的辅助空间来存放当前待排序序列的首、尾位置。最坏的情况下,空间复杂度为 O ( n ) O(n) O(n)。如果对算法进行一些改写,在一趟排序之后比较被划分所得到的两个子序列的长度,并且首先对长度较短的子序列进行快速排序,这时候需要的空间复杂度可以达到 O ( l o g 2 n ) O(log_2 n) O(log2n)。
- 排序稳定性:快速排序是一种 不稳定排序算法。
1.6.3 代码实现:
从待排序列中找到一个基准数 pivot
(这里取序列第一个元素),将比 pivot
小的元素都移到序列左侧,比 pivot
大的都移到序列右侧。这样就将待排序列分成了[start,pivot-1]
、pivot
和[pivot+1,end]
三部分。再分别对前后两部分递归调用快速排序。
def quickSort(nums):
return qSort(nums,0,len(nums)-1)
def qSort(nums,start,end):
if start<end:
"""
当分裂后的子序列有两个以上元素时,就进行排序。
使用左右指针分别指向子序列的开头和结尾
"""
pivot=nums[start]
left,right=start+1,end
done=True
"""
左指针一直右移,直到指向的元素大于中值;右指针一直左移,直到指向的元素小于中值。交换左右指针的元素值
继续下一次移动交换,直到左指针越过右指针。此时右指针位置就是中值应该在的位置,进行交换,排序完毕。
"""
while done:
while left<=right and nums[left]<=pivot:
left=left+1
while left<=right and nums[right]>=pivot:
right=right-1
if left>right:
done=False
else:
nums[left],nums[right] = nums[right],nums[left]
nums[start],nums[right]=nums[right],nums[start]
"""
排完序后,中值前的部分[start,pivot-1]都是小于中值,中值后的部分[pivot+1,end]都是大于中值
对这两部分再次进行递归的快速排序
"""
qSort(nums,start,right-1)
qSort(nums,right+1,end)
return nums
也可以写成另一种方式:
class Solution(object):
def findKthLargest(self, nums, k):
"""
:type nums: List[int]
:type k: int
:rtype: int
"""
return self.qsort(nums, 0, len(nums) - 1)
def qsort(self,ary, start, end):
if start < end:
left = start
right = end
pivot = ary[start]
else:
return ary
while left < right:
while left < right and ary[right] >= pivot:
right -= 1
if left < right: # 说明打破while循环的原因是ary[right] <= key
ary[left] = ary[right]
left += 1
while left < right and ary[left] < pivot:
left += 1
if left < right: # 说明打破while循环的原因是ary[left] >= key
ary[right] = ary[left]
right -= 1
ary[left] = pivot # 此时,left=right,用key来填坑
self.qsort(ary, start, left - 1)
self.qsort(ary, left + 1, end)
return ary
1.7 桶排序
桶排序(Bucket Sort)基本思想:将未排序数组分到若干个「桶」中,每个桶的元素再进行单独排序。
1.7.1 算法步骤
- 根据原始数组的值域范围,将数组划分为
k
个相同大小的子区间,每个区间称为一个桶。 - 遍历原始数组元素,将每个元素装入对应区间的桶中。
- 对每个桶内的元素单独排序(使用插入排序、归并排序、快排排序等算法)。
- 最后按照区间顺序将桶内的元素合并起来,完成排序。
图解演示
- 划分子区间
- 将数组元素装入桶中,并对桶内元素单独排序
- 将桶内元素合并起来,完成排序
1.7.2 算法分析
- 时间复杂度: O ( n ) O(n) O(n)。当输入元素个数为 n n n,桶的个数是 m m m 时,每个桶里的数据就是 k = n / m k = n / m k=n/m 个。每个桶内排序的时间复杂度为 O ( k × log 2 k ) O(k \times \log_2 k) O(k×log2k)。 m m m 个桶就是 m ∗ O ( k ∗ l o g 2 k ) = m × O ( ( n / m ) × log 2 ( n / m ) ) = O ( n ∗ l o g 2 ( n / m ) ) m * O(k * log_2k) = m \times O((n / m) \times \log_2(n/m)) = O(n*log_2(n/m)) m∗O(k∗log2k)=m×O((n/m)×log2(n/m))=O(n∗log2(n/m))。当桶的个数 m m m 接近于数据个数 n n n 时, l o g 2 ( n / m ) log_2(n/m) log2(n/m) 就是一个较小的常数,所以排序桶排序时间复杂度接近于 O ( n ) O(n) O(n)。
- 空间复杂度: O ( n + m ) O(n + m) O(n+m)。由于桶排序使用了辅助空间,所以桶排序的空间复杂度是 O ( n + m ) O(n + m) O(n+m)。
- 排序稳定性:如果桶内使用插入排序算法等稳定排序算法,则桶排序也是一种 稳定排序算法。
1.7.3 代码实现
class Solution:
def insertionSort(self, arr):
# 遍历无序序列
for i in range(1, len(arr)):
temp = arr[i]
j = i
# 从右至左遍历有序序列
while j > 0 and arr[j - 1] > temp:
# 将有序序列中插入位置右侧的元素依次右移一位
arr[j] = arr[j - 1]
j -= 1
# 将该元素插入到适当位置
arr[j] = temp
return arr
def bucketSort(self, arr, bucket_size=5):
# 计算待排序序列中最大值元素 arr_max 和最小值元素 arr_min
arr_min, arr_max = min(arr), max(arr)
# 定义桶的个数为 (最大值元素 - 最小值元素) // 每个桶的大小 + 1
bucket_count = (arr_max - arr_min) // bucket_size + 1
# 定义桶数组 buckets
buckets = [[] for _ in range(bucket_count)]
# 遍历原始数组元素,将每个元素装入对应区间的桶中
for num in arr:
buckets[(num - arr_min) // bucket_size].append(num)
# 对每个桶内的元素单独排序,并合并到 res 数组中
res = []
for bucket in buckets:
self.insertionSort(bucket)
res.extend(bucket)
return res
def sortArray(self, nums: List[int]) -> List[int]:
return self.bucketSort(nums)
1.8 堆排序
1.8.1 算法思想
堆排序(Heap sort)基本思想:
借用「堆结构」所设计的排序算法。将数组转化为大顶堆,重复从大顶堆中取出数值最大的节点,并让剩余的堆结构继续维持大顶堆性质。
堆(Heap):符合以下两个条件之一的完全二叉树:
- 大顶堆:根节点值 ≥ 子节点值。
- 小顶堆:根节点值 ≤ 子节点值。
1.8.2 算法步骤
- 建立初始堆:将无序序列构造成第
1
个大顶堆(初始堆),使得n
个元素的最大值处于序列的第1
个位置。 - 调整堆:交换序列的第
1
个元素(最大值元素)与第n
个元素的位置。将序列前n - 1
个元素组成的子序列调整成一个新的大顶堆,使得n - 1
个元素的最大值处于序列第1
个位置,从而得到第2
个最大值元素。 - 调整堆:交换子序列的第
1
个元素(最大值元素)与第n - 1
个元素的位置。将序列前n - 2
个元素组成的子序列调整成一个新的大顶堆,使得n - 2
个元素的最大值处于序列第1
个位置,从而得到第3
个最大值元素。 - 依次类推,不断交换子序列的第
1
个元素(最大值元素)与当前子序列最后一个元素位置,并将其调整成新的大顶堆。直到子序列剩下一个元素时,排序结束。此时整个序列就变成了一个有序序列。
从堆排序算法步骤中可以看出:堆排序算法主要涉及「调整堆」和「建立初始堆」两个步骤。
1.8.3 调整堆
调整堆方法:把移走了最大值元素以后的剩余元素组成的序列再构造为一个新的堆积。具体步骤如下:
- 从根节点开始,自上而下地调整节点的位置,使其成为堆积。
- 判断序号为
i
的节点与其左子树节点(序号为2 * i
)、右子树节点(序号为2 * i + 1
)中值关系。 - 如果序号为
i
节点大于等于左右子节点值,则排序结束。 - 如果序号为
i
节点小于左右子节点值,则将序号为i
节点与左右子节点中值最大的节点交换位置。
- 判断序号为
- 因为交换了位置,使得当前节点的左右子树原有的堆积特性被破坏。于是,从当前节点的左右子树节点开始,自上而下继续进行类似的调整。
- 依次类推,直到整棵完全二叉树成为一个大顶堆。
调整堆方法演示
- 交换序列的第
1
个元素90
与最后1
个元素19
的位置,此时当前节点为根节点19
。 - 判断根节点
19
与其左右子节点值,因为17 < 19 < 36
,所以将根节点19
与左子节点36
互换位置,此时当前节点为根节点19
。 - 判断当前节点
36
与其左右子节点值,因为19 < 25 < 26
,所以将当前节点19
与右节点26
互换位置。调整堆结束。
1.8.4 建立初始堆
- 如果原始序列对应的完全二叉树(不一定是堆)的深度为
d
,则从d - 1
层最右侧分支节点(序号为 ⌊ n 2 ⌋ \lfloor \frac{n}{2} \rfloor ⌊2n⌋)开始,初始时令 i = ⌊ n 2 ⌋ i = \lfloor \frac{n}{2} \rfloor i=⌊2n⌋,调用调整堆算法。 - 每调用一次调整堆算法,执行一次
i = i - 1
,直到i == 1
时,再调用一次,就把原始序列调整为了一个初始堆。
方法演示
- 原始序列为
[2, 7, 26, 25, 19, 17, 1, 90, 3, 36]
,对应完全二叉树的深度为3
。 - 从第
2
层最右侧的分支节点,也就序号为5
的节点开始,调用堆调整算法,使其与子树形成大顶堆。 - 节点序号减
1
,对序号为4
的节点,调用堆调整算法,使其与子树形成大顶堆。 - 节点序号减
1
,对序号为3
的节点,调用堆调整算法,使其与子树形成大顶堆。 - 节点序号减
1
,对序号为2
的节点,调用堆调整算法,使其与子树形成大顶堆。 - 节点序号减
1
,对序号为1
的节点,调用堆调整算法,使其与子树形成大顶堆。 - 此时整个原始序列对应的完全二叉树就成了一个大顶堆,建立初始堆完毕。
1.8.5 堆排序方法完整演示
- 原始序列为
[2, 7, 26, 25, 19, 17, 1, 90, 3, 36]
,先根据原始序列建立一个初始堆。 - 交换序列中第
1
个元素(90
)与第10
个元素(2
)的位置。将序列前9
个元素组成的子序列调整成一个大顶堆,此时堆顶变为36
。 - 交换序列中第
1
个元素(36
)与第9
个元素(3
)的位置。将序列前8
个元素组成的子序列调整成一个大顶堆,此时堆顶变为26
。 - 交换序列中第
1
个元素(26
)与第8
个元素(2
)的位置。将序列前7
个元素组成的子序列调整成一个大顶堆,此时堆顶变为25
。 - 以此类推,不断交换子序列的第
1
个元素(最大值元素)与当前子序列最后一个元素位置,并将其调整成新的大顶堆。直到子序列只剩下最后一个元素1
时,排序结束。此时整个序列变成了一个有序序列,即[1, 2, 3, 7, 17, 19, 25, 26, 36, 90]
。
1.8.6 堆排序算法分析
- 时间复杂度:
O
(
n
×
log
2
n
)
O(n \times \log_2 n)
O(n×log2n)。
- 堆积排序的时间主要花费在两个方面:「建立初始堆」和「调整堆」。
- 设原始序列所对应的完全二叉树深度为
d
d
d,算法由两个独立的循环组成:
- 在第 1 1 1 个循环构造初始堆积时,从 i = d − 1 i = d - 1 i=d−1 层开始,到 i = 1 i = 1 i=1 层为止,对每个分支节点都要调用一次调整堆算法,而一次调整堆算法,对于第 i i i 层一个节点到第 d d d 层上建立的子堆积,所有节点可能移动的最大距离为该子堆积根节点移动到最后一层(第 d d d 层) 的距离,即 d − i d - i d−i。而第 i i i 层上节点最多有 2 i − 1 2^{i-1} 2i−1 个,所以每一次调用调整堆算法的最大移动距离为 2 i − 1 ∗ ( d − i ) 2^{i-1} * (d-i) 2i−1∗(d−i)。因此,堆积排序算法的第 1 1 1 个循环所需时间应该是各层上的节点数与该层上节点可移动的最大距离之积的总和,即: ∑ i = d − 1 1 2 i − 1 ( d − i ) = ∑ j = 1 d − 1 2 d − j − 1 × j = ∑ j = 1 d − 1 2 d − 1 × j 2 j ≤ n ∑ j = 1 d − 1 j 2 j < 2 n \sum_{i = d - 1}^1 2^{i-1} (d-i) = \sum_{j = 1}^{d-1} 2^{d-j-1} \times j = \sum_{j = 1}^{d-1} 2^{d-1} \times {j \over 2^j} \le n \sum_{j = 1}^{d-1} {j \over 2^j} < 2n ∑i=d−112i−1(d−i)=∑j=1d−12d−j−1×j=∑j=1d−12d−1×2jj≤n∑j=1d−12jj<2n。这一部分的时间花费为 O ( n ) O(n) O(n)。
- 在第 2 2 2 个循环中,每次调用调整堆算法一次,节点移动的最大距离为这棵完全二叉树的深度 d = ⌊ log 2 ( n ) ⌋ + 1 d = \lfloor \log_2(n) \rfloor + 1 d=⌊log2(n)⌋+1,一共调用了 n − 1 n - 1 n−1 次调整堆算法,所以,第 2 2 2 个循环的时间花费为 ( n − 1 ) ( ⌊ log 2 ( n ) ⌋ + 1 ) = O ( n × log 2 n ) (n-1)(\lfloor \log_2 (n)\rfloor + 1) = O(n \times \log_2 n) (n−1)(⌊log2(n)⌋+1)=O(n×log2n)。
- 因此,堆积排序的时间复杂度为 O ( n × log 2 n ) O(n \times \log_2 n) O(n×log2n)。
- 空间复杂度: O ( 1 ) O(1) O(1)。由于在堆积排序中只需要一个记录大小的辅助空间,因此,堆积排序的空间复杂度为: O ( 1 ) O(1) O(1)。
- 排序稳定性:堆排序是一种 不稳定排序算法。
1.8.7 代码实现
class Solution:
# 调整为大顶堆
def heapify(self, arr: [int], index: int, end: int):
# 根节点为 index,左节点为 2 * index + 1, 右节点为 2 * index + 2
left = index * 2 + 1
right = left + 1
while left <= end:
# 当前节点为非叶子结点
max_index = index
if arr[left] > arr[max_index]:
max_index = left
if right <= end and arr[right] > arr[max_index]:
max_index = right
if index == max_index:
# 如果不用交换,则说明已经交换结束
break
arr[index], arr[max_index] = arr[max_index], arr[index]
# 继续调整子树
index = max_index
left = index * 2 + 1
right = left + 1
# 初始化大顶堆
def buildMaxHeap(self, arr: [int]):
size = len(arr)
# (size - 2) // 2 是最后一个非叶节点,叶节点不用调整
for i in range((size - 2) // 2, -1, -1):
self.heapify(arr, i, size - 1)
return arr
# 升序堆排序,思路如下:
# 1. 先建立大顶堆
# 2. 让堆顶最大元素与最后一个交换,然后调整第一个元素到倒数第二个元素,这一步获取最大值
# 3. 再交换堆顶元素与倒数第二个元素,然后调整第一个元素到倒数第三个元素,这一步获取第二大值
# 4. 以此类推,直到最后一个元素交换之后完毕。
def maxHeapSort(self, arr: [int]):
self.buildMaxHeap(arr)
size = len(arr)
for i in range(size):
arr[0], arr[size - i - 1] = arr[size - i - 1], arr[0]
self.heapify(arr, 0, size - i - 2)
return arr
def sortArray(self, nums: List[int]) -> List[int]:
return self.maxHeapSort(nums)
1.9 计数排序
计数排序(Counting Sort)基本思想:使用一个额外的数组 counts
,其中 counts[i]
表示原数组 arr
中值等于 i
的元素个数。然后根据数组 counts
来将 arr
中的元素排到正确的位置。
1.9.1 算法步骤
- 找出待排序序列中最大值元素
arr_max
和最小值元素arr_min
。 - 定义大小为
arr_max - arr_min + 1
的数组counts
,初始时,counts
中元素值全为0
。 - 遍历数组
arr
,统计值为num
的元素出现的次数。将其次数存入counts
数组的第num - arr_min
项(counts[num - arr_min]
表示元素值num
出现的次数)。 - 对所有的计数累加,从
counts
中的第一个元素开始,每一项和前一项相加。此时counts[i]
表示值为i
的元素排名。 - 反向填充目标数组:
- 逆序遍历数组
arr
。对于每个元素值arr[i]
,其对应排名为counts[arr[i] - arr_min]
。 - 根据排名,将
arr[i]
放在数组对应位置(因为数组下标是从0
开始的,所以对应位置为排名减1
)。即res[counts[arr[i] - arr_min] - 1] = arr[i]
。 - 放入之后, 将
arr[i]
的对应排名减1
,即counts[arr[i] - arr_min] -= 1
。
- 逆序遍历数组
动画演示
1.9.2 算法分析
- 时间复杂度: O ( n + k ) O(n + k) O(n+k)。其中 k k k 代表待排序序列的值域。
- 空间复杂度:
O
(
k
)
O(k)
O(k)。其中
k
k
k 代表待排序序列的值域。由于用于计数的数组
counts
的长度取决于待排序数组中数据的范围(大小等于待排序数组最大值减去最小值再加1
)。所以计数排序算法对于数据范围很大的数组,需要大量的内存。 - 计数排序适用情况:计数排序一般用于整数排序,不适用于按字母顺序、人名顺序排序。
- 排序稳定性:计数排序是一种 稳定排序算法。
1.9.3. 代码实现
class Solution:
def countingSort(self, arr):
# 计算待排序序列中最大值元素 arr_max 和最小值元素 arr_min
arr_min, arr_max = min(arr), max(arr)
# 定义计数数组 counts,大小为 最大值元素 - 最小值元素 + 1
size = arr_max - arr_min + 1
counts = [0 for _ in range(size)]
# 统计值为 num 的元素出现的次数
for num in arr:
counts[num - arr_min] += 1
# 计算元素排名
for j in range(1, size):
counts[j] += counts[j - 1]
# 反向填充目标数组
res = [0 for _ in range(len(arr))]
for i in range(len(arr) - 1, -1, -1):
# 根据排名,将 arr[i] 放在数组对应位置
res[counts[arr[i] - arr_min] - 1] = arr[i]
# 将 arr[i] 的对应排名减 1
counts[arr[i] - arr_min] -= 1
return res
def sortArray(self, nums: List[int]) -> List[int]:
return self.countingSort(nums)
1.10 基数排序
假设要对 10 万个手机号码进行排序,显然桶排序和计数排序都不太适合,那怎样才能做到时间复杂度为 O(n) 呢? 此时可以考虑基数排序。
手机号码有这样的规律,假设要比较两个手机号码 a, b 的大小,如果在前面几位中,a 手机号码已经比 b大了,那后面几位就不用看了。所以借助 稳定排序算法,我们可以这么实现:从手机号码的最后一位开始,分别按照每一位的数字对手机号码进行排序,依次往前进行,经过 11 次排序之后,手机号码就都有序了。
基数排序(Radix Sort)基本思想:将所有待比较数值(正整数)统一为同样的数位长度,数位较短的数前面补零。然后后,从最低位开始,依次进行排序。这样从最低位排序一直到最高位排序完成以后,数列就变成一个有序序列。
1.10.1 算法步骤
基数排序算法可以采用「最低位优先法(Least Significant Digit First)」或者「最高位优先法(Most Significant Digit first)」。最常用的是「最低位优先法」。
下面我们以最低位优先法为例,讲解一下算法步骤。
- 遍历数组元素,获取数组最大值元素,并取得位数。
- 定义一个长度为
10
的桶buckets
,分别代表0 ~ 9
这10
位数字。 - 以个位元素为索引,根据数组元素个位上的值,将数组元素存入对应数字的桶中。
- 清空原始数组,并从桶中依次取出对应元素,重新加入到原始数组中。
- 之后分别以十位,百位,…,最大值元素的最高位为索引,根据元素对应位上的数字,存入对应数字的桶中。并合并数组,完成排序。
动画演示
- 初始序列为
[32, 1, 10, 96, 57, 7, 62, 47, 82, 25, 79, 5]
,序列所有元素的最大位数为2
。 - 以个位为索引,根据元素个位上的数字,将其分别存入到
0
~9
这10
个桶中。 - 清空原始数组,并从桶中依次取出对应元素,重新加入到原始数组中。此时序列变为
[10, 1, 32, 62, 82, 25, 5, 96, 57, 7, 47, 79]
。 - 以十位为索引,根据元素十位上的数字,将其分别存入到
0
~9
这10
个桶中。 - 清空原始数组,并从桶中依次取出对应元素,重新加入到原始数组中。此时序列变为
[1, 5, 7, 10, 25, 32, 47, 57, 62, 79, 82, 96]
,完成排序。
1.10.2 算法分析
基数排序是一种非比较型整数排序算法,其原理是将整数按位数切割成不同的数字,然后按每个位数分别比较。由于整数也可以表达字符串(比如名字或日期)和特定格式的浮点数,所以基数排序也不是只能使用于整数。
- 时间复杂度:
O
(
n
×
k
)
O(n \times k)
O(n×k)。
- 根据每一位的排序,可以用桶排序或者计数排序来实现,它们的时间复杂度可以做到 O(n)。如果排序的数据有 K位,则总的时间复杂度为 O(K * n),当 K 不大时,基数排序的时间复杂度就近似为 O(n)
- k k k 的大小取决于数字位的选择(十进制位、二进制位)和待排序元素所属数据类型全集的大小。
- 空间复杂度: O ( n + k ) O(n + k) O(n+k)。
- 排序稳定性:基数排序是一种 稳定排序算法。
- 有时候,要排序的数据并不都是等长的,比如我们要对英文单词进行排序。这时候,我们可以把所有单词都补足到相同长度,位数不够的在后面补 ’0‘,所有字母的 ASCII 码都大于 ‘0’,因此不会影响原有的大小顺序。
1.10.3 代码实现
class Solution:
def radixSort(self, arr):
# 桶的大小为所有元素的最大位数
size = len(str(max(arr)))
# 从低位到高位依次遍历每一位,以各个数位值为索引,对数组进行按数位排序
for i in range(size):
# 使用一个长度为 10 的桶来存放各个位上的元素
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 sortArray(self, nums: List[int]) -> List[int]:
return self.radixSort(nums)
1.11 排序算法总结
下面为七种经典排序算法指标对比情况:
-
冒泡排序,简单选择排序,堆排序,直接插入排序,希尔排序的空间复杂度为O(1),因为需要一个临时变量来交换元素位置,(另外遍历序列时自然少不了用一个变量来做索引)
-
快速排序空间复杂度为logn(因为递归调用了)
-
归并排序空间复杂是O(n),需要一个大小为n的临时数组.。基数排序的空间复杂是O(n),桶排序的空间复杂度不确定。
-
所有排序算法中最快的应该是桶排序(很多人误以为是快速排序,实际上不是.不过实际应用中快速排序用的多)。但桶排序一般用的不多,因为有几个比较大的缺陷.
- 待排序的元素不能是负数,小数.
- 空间复杂度不确定,要看待排序元素中最大值是多少.。所需要的辅助数组大小即为最大元素的值.
二、 练习题
2.1 移动零(题283)
给定一个数组 nums,编写一个函数将所有 0 移动到数组的末尾,同时保持非零元素的相对顺序。请注意 ,必须在不复制数组的情况下原地对数组进行操作。
- 示例 1:
- 输入: nums = [0,1,0,3,12]
- 输出: [1,3,12,0,0]
- 示例 2:
- 输入: nums = [0]
- 输出: [0]
思路 1:冒泡排序(超时)
冒泡排序的思想,就是通过相邻元素的比较与交换,使得较大元素从前面移到后面。我们可以借用冒泡排序的思想,将值为 0 的元素移动到数组末尾。因为冒泡排序的时间复杂度为
O
(
n
2
)
O(n^2)
O(n2) 。所以这种做法会导致超时。
思路 2:双指针
- 使用两个指针 slow,fast。slow 指向处理好的非 0 数字数组的尾部,fast 指针指向当前待处理元素。
- 不断向右移动 fast 指针,每次移动到非零数,则将左右指针对应的数交换,交换同时将 slow 右移。
- 此时,slow 指针左侧均为处理好的非零数,而从 slow 指针指向的位置开始, fast 指针左边为止都为 0。
- 遍历结束之后,则所有 0 都移动到了右侧,且保持了非零数的相对位置。
class Solution:
def moveZeroes(self, nums: List[int]) -> None:
"""
Do not return anything, modify nums in-place instead.
"""
slow=0
for i in range(len(nums)):
if nums[i]!=0:
nums[i],nums[slow]=nums[slow],nums[i]
left+=1
return nums
2.2 颜色分类(题75)
方法一:单指针
这道题和上一题很类似,最简单的方法是遍历两次,先将0排到最前面,再接着将1排到前面:
class Solution:
def sortColors(self, nums: List[int]) -> None:
"""
Do not return anything, modify nums in-place instead.
"""
# 两次遍历,先排0再排1
left=0
for i in range(len(nums)):
if nums[i]==0:
nums[left],nums[i]=nums[i],nums[left]
left+=1
right=left # 前面left个位置已经排好了0
for j in range(right,len(nums)):
if nums[j]==1:
nums[right],nums[j]=nums[j],nums[right]
right+=1
return nums
方法二:双指针(官方题解)
我们可以额外使用一个指针,即使用两个指针分别用来交换 0 和1。具体地,我们用指针
p
0
p_0
p0来交换 0,
p
1
p_1
p1来交换 1,初始值都为 0。当我们从左向右遍历整个数组时:
- 如果找到了 1,那么将其与 n u m s [ p 1 ] nums[p_1] nums[p1] 进行交换,并将 p 1 p_1 p1向后移动一个位置,这与方法一是相同的;
- 如果找到了 0,那么将其与
n
u
m
s
[
p
0
]
nums[p_0]
nums[p0] 进行交换,并将
p
0
p_0
p0向后移动一个位置。这样做是正确的吗?
我们可以注意到,因为连续的 0 之后是连续的 1,因此如果我们将 0 与 n u m s [ p 0 ] nums[p_0] nums[p0] 进行交换,那么我们可能会把一个 1 交换出去。当 p 0 < p 1 p_0 < p_1 p0<p1时,我们已经将一些 1 连续地放在头部,此时一定会把一个 1 交换出去,导致答案错误。因此,如果 p 0 < p 1 p_0 < p_1 p0<p1,那么我们需要再将 n u m s [ i ] nums[i] nums[i] 与 n u m s [ p 1 ] nums[p_1] nums[p1]进行交换,其中 i 是当前遍历到的位置。
在进行了第一次交换后, n u m s [ i ] nums[i] nums[i]的值为 1,我们需要将这个 1 放到「头部」的末端。在最后,无论是否有 p 0 < p 1 p_0 < p_1 p0<p1,我们需要将 p 0 p_0 p0 和 p 1 p_1 p1均向后移动一个位置,而不是仅将 p 0 p_0 p0向后移动一个位置。
class Solution:
def sortColors(self, nums: List[int]) -> None:
"""
Do not return anything, modify nums in-place instead.
"""
# 两个指针分别用于交换0和1
p0=p1=0
for i in range(len(nums)):
if nums[i]==1:
nums[i],nums[p1]=nums[p1],nums[i]
p1+=1
elif nums[i]==0:
nums[i],nums[p0]=nums[p0],nums[i]
if p0<p1:
nums[i],nums[p1]=nums[p1],nums[i]
p1+=1
p0+=1
return nums
方法三:快速排序
我们也可以借鉴快速排序算法中的 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:
left = 0
right = len(nums) - 1
index = 0
while index <= right:
if index < left:
index += 1
elif nums[index] == 0:
nums[index], nums[left] = nums[left], nums[index]
left += 1
elif nums[index] == 2:
nums[index], nums[right] = nums[right], nums[index]
right -= 1
else:
index += 1
2.3 数组中的第K个最大元素(题215)
给定整数数组 nums 和整数 k,请返回数组中第 k 个最大的元素。示例:
- 输入: [3,2,1,5,6,4], k = 2
- 输出: 5
这道题使用改进的冒泡排序、选择排序、插入排序都会超时。希尔排序(1440ms)、归并排序(1016ms)、堆排序(640ms),这些都是可以通过的。也可以考虑使用快速排序。
2.3.1 快速排序
快速排序思路:
使用快速排序在每次调整时,都会确定一个元素的最终位置,且以该元素为界限,将数组分成了左右两个子数组,左子数组中的元素都比该元素小,右子树组中的元素都比该元素大。
这样,只要某次划分的元素位置q恰好是第 k 个下标就找到了答案。至于nums[left,q-1]
和nums[q+1,right]
是否有序,我们并不关心。具体来说,在分解的过程当中,我们会对子数组进行划分,如果划分得到的 q
正好就是我们需要的下标,就直接返回 nums[q]
;否则,如果 q
比目标下标小,就递归右子区间,否则递归左子区间。这样就可以把原来递归两个区间变成只递归一个区间,提高了时间效率。
class Solution(object):
import random
def findKthLargest(self, nums, k):
"""
:type nums: List[int]
:type k: int
:rtype: int
"""
return self.qSort(nums,0,len(nums)-1,k)
def qSort(self,nums,start,end,k):
# if start<end:这个是完全排序的结束条件,如果是topk排序,结束条件是pivot_idx=len(nums)-k
pivot_idx=self.partition(nums,start,end)
"""
如果pivot_idx==len(nums)-k,说明中值正好就是第k大的值,直接返回nums[pivot_idx];
否则,如果pivot_idx>len(nums)-k,就递归左子区间,否则递归右子区间。
"""
if pivot_idx==len(nums)-k:
return nums[pivot_idx]
elif pivot_idx>len(nums)-k:
return self.qSort(nums,start,pivot_idx-1,k)
else:
return self.qSort(nums,pivot_idx+1,end,k)
def partition(self,nums,start,end):
"""
左指针一直右移,直到指向的元素大于中值;右指针一直左移,直到指向的元素小于中值。交换左右指针的元素值
继续下一次移动交换,直到左指针越过右指针。此时右指针位置就是中值应该在的位置,进行交换,排序完毕。
"""
# 改进中值,选取序列中随机一个位置的元素,将其和序列开头元素交换位置,作为中值
idx=random.randint(start,end)
nums[start],nums[idx]=nums[idx],nums[start]
pivot=nums[start]
left=start+1
right=end
done=False
while not done:
while left<=right and nums[left]<=pivot:
left=left+1
while left<=right and nums[right]>=pivot:
right=right-1
if left>right:
done=True
else:
nums[left],nums[right] = nums[right],nums[left]
nums[start],nums[right]=nums[right],nums[start]
return right
使用完整的快速排序再取第k大的元素,是2672ms。只排到第k大的元素是640ms。加上随机选取中值之后,是80ms到90ms。
堆排序、优先队列等方法可参考算法通关手册或官方题解。
与这道题类似的还有最小的k个数(剑指 Offer 40),使用随机快速排序:
class Solution(object):
def getLeastNumbers(self, arr, k):
"""
:type arr: List[int]
:type k: int
:rtype: List[int]
"""
if k>0:
return self.qSort(arr,0,len(arr)-1,k)
elif k==0:
return []
def qSort(self,nums,start,end,k):
# if start<end:这个是完全排序的结束条件,如果是topk排序,结束条件是pivot_idx=k-1
pivot_idx=self.partition(nums,start,end)
"""
注意,第k大的数其下标是k-1
如果pivot_idx==k-1,说明中值正好就是第k大的值,直接返回nums[pivot_idx];
否则,如果pivot_idx>k-1,就递归左子区间,否则递归右子区间。
"""
# 第k小的数位于序列的第k-1个位置(快排默认是升序)
if pivot_idx==k-1:
return nums[:pivot_idx+1]
elif pivot_idx>k-1:
return self.qSort(nums,start,pivot_idx-1,k)
else:
return self.qSort(nums,pivot_idx+1,end,k)
def partition(self,nums,start,end):
"""
左指针一直右移,直到指向的元素大于中值;右指针一直左移,直到指向的元素小于中值。交换左右指针的元素值
继续下一次移动交换,直到左指针越过右指针。此时右指针位置就是中值应该在的位置,进行交换,排序完毕。
"""
# 改进中值,选取序列中随机一个位置的元素,将其和序列开头元素交换位置,作为中值
idx=random.randint(start,end)
nums[start],nums[idx]=nums[idx],nums[start]
pivot=nums[start]
left=start+1
right=end
done=False
while not done:
while left<=right and nums[left]<=pivot:
left=left+1
while left<=right and nums[right]>=pivot:
right=right-1
if left>right:
done=True
else:
nums[left],nums[right] = nums[right],nums[left]
nums[start],nums[right]=nums[right],nums[start]
return right
- 时间复杂度: O ( n ) O(n) O(n)。证明过程可参考「算法导论 9.2:期望为线性的选择算法」。
- 空间复杂度: O ( log 2 n ) O(\log_2 n) O(log2n)。递归使用栈空间的空间代价期望为 O ( log 2 n ) O(\log_2n) O(log2n)。
2.3.2 堆排序
升序堆排序的思路如下:
-
将无序序列构造成第
1
个大顶堆(初始堆),使得n
个元素的最大值处于序列的第1
个位置。 -
调整堆:交换序列的第
1
个元素(最大值元素)与第n
个元素的位置。将序列前n - 1
个元素组成的子序列调整成一个新的大顶堆,使得n - 1
个元素的最大值处于序列第1
个位置,从而得到第2
个最大值元素。 -
调整堆:交换子序列的第
1
个元素(最大值元素)与第n - 1
个元素的位置。将序列前n - 2
个元素组成的子序列调整成一个新的大顶堆,使得n - 2
个元素的最大值处于序列第1
个位置,从而得到第3
个最大值元素。 -
依次类推,不断交换子序列的第
1
个元素(最大值元素)与当前子序列最后一个元素位置,并将其调整成新的大顶堆。直到获取第k
个最大值元素为止。
代码:
class Solution:
def findKthLargest(self, nums: List[int], k: int) -> int:
# 调整为大顶堆
def heapify(nums, index, end):
left = index * 2 + 1
right = left + 1
while left <= end:
# 当前节点为非叶子节点
max_index = index
if nums[left] > nums[max_index]:
max_index = left
if right <= end and nums[right] > nums[max_index]:
max_index = right
if index == max_index:
# 如果不用交换,则说明已经交换结束
break
nums[index], nums[max_index] = nums[max_index], nums[index]
# 继续调整子树
index = max_index
left = index * 2 + 1
right = left + 1
# 初始化大顶堆
def buildMaxHeap(nums):
size = len(nums)
# (size-2) // 2 是最后一个非叶节点,叶节点不用调整
for i in range((size - 2) // 2, -1, -1):
heapify(nums, i, size - 1)
return nums
buildMaxHeap(nums)
size = len(nums)
for i in range(k-1):
nums[0], nums[size-i-1] = nums[size-i-1], nums[0]
heapify(nums, 0, size-i-2)
return nums[0]
复杂度分析
- 时间复杂度: O ( n × log 2 n ) O(n \times \log_2n) O(n×log2n)。
- 空间复杂度: O ( 1 ) O(1) O(1)。
2.3.3 优先队列
- 遍历数组元素,对于挡圈元素
num
:- 如果优先队列中的元素个数小于
k
个,则将当前元素num
放入优先队列中。 - 如果优先队列中的元素个数大于等于
k
个,并且当前元素num
大于优先队列的队头元素,则弹出队头元素,并将当前元素num
插入到优先队列中。
- 如果优先队列中的元素个数小于
- 遍历完,此时优先队列的队头元素就是第K个最大元素,将其弹出并返回即可。
这里我们借助了 Python 中的 heapq
模块实现优先队列算法,这一步也可以通过手写堆的方式实现优先队列。
代码
import heapq
class Solution:
def findKthLargest(self, nums: List[int], k: int) -> int:
res = []
for num in nums:
if len(res) < k:
heapq.heappush(res, num)
elif num > res[0]:
heapq.heappop(res)
heapq.heappush(res, num)
return heapq.heappop(res)
复杂度分析
- 时间复杂度: O ( n × log 2 k ) O(n \times \log_2k) O(n×log2k)。
- 空间复杂度: O ( k ) O(k) O(k)。
2.4 排序数组(题912)
给你一个整数数组 nums,请你将该数组升序排列。
本题冒泡排序(改进)是过不了的,估计选择排序、插入排序也不行。可行的有希尔排序(1172ms)、归并排序(1036ms)、快速排序(816ms)。其中,验证的nums列表中有一个是nums全部为2的极端情况,直接快速排序是超时的。所以可以设置归并+快排,例如:
class Solution(object):
def sortArray(self, nums):
"""
:type nums: List[int]
:rtype: List[int]
"""
if len(set(nums))<5: # 随便选的一个值,排除一些极端情况下快排会超时
return self.merge(nums) # 归并排序
else:
return self.qSort(nums,0,len(nums)-1) # 快速排序
def merge(self,arr):
if len(arr)<=1:
return arr
mid=len(arr)//2
# 注意,这里是递归的写法
left_arr=self.merge(arr[:mid])
right_arr=self.merge(arr[mid:])
return self.mergesort(left_arr,right_arr)
def mergesort(self,left_arr,right_arr):
left,right=0,0
arr=[]
# 注意,这里都是<,不能取=,因为left_arr[len(left_arr)]是超出索引的
while left<len(left_arr) and right<len(right_arr):
if left_arr[left]<right_arr[right]:
arr.append(left_arr[left])
left+=1
else:
arr.append(right_arr[right])
right+=1
while left<len(left_arr):
arr.append(left_arr[left])
left+=1
while right<len(right_arr):
arr.append(right_arr[right])
right+=1
#print(left_arr,right_arr,arr)
return arr
def qSort(self,nums,start,end):
if start<end:
pivot_idx=self.partition(nums,start,end)
self.qSort(nums,start,pivot_idx-1)
self.qSort(nums,pivot_idx+1,end)
return nums
def partition(self,nums,start,end):
"""
左指针一直右移,直到指向的元素大于中值;右指针一直左移,直到指向的元素小于中值。交换左右指针的元素值
继续下一次移动交换,直到左指针越过右指针。此时右指针位置就是中值应该在的位置,进行交换,排序完毕。
"""
# 改进中值,选取序列中随机一个位置的元素,将其和序列开头元素交换位置,作为中值
idx=random.randint(start,end)
nums[start],nums[idx]=nums[idx],nums[start]
pivot=nums[start]
left=start+1
right=end
done=False
while not done:
while left<=right and nums[left]<=pivot:
left=left+1
while left<=right and nums[right]>=pivot:
right=right-1
if left>right:
done=True
else:
nums[left],nums[right] = nums[right],nums[left]
nums[start],nums[right]=nums[right],nums[start]
return right
其它排序方法详见算法通关手册。
2.5 数组中的逆序对(剑指 Offer 51)
在数组中的两个数字,如果前面一个数字大于后面的数字,则这两个数字组成一个逆序对。输入一个数组,求出这个数组中的逆序对的总数。示例:
- 输入: [7,5,6,4]
- 输出: 5
思路 1:归并排序
归并排序主要分为:「分解过程」和「合并过程」。其中「合并过程」实质上是两个有序数组的合并过程。
每当遇到 左子数组当前元素 > 右子树组当前元素时,意味着「左子数组从当前元素开始,一直到左子数组末尾元素」与「右子树组当前元素」构成了若干个逆序对。
比如上图中的左子数组 [0, 3, 5, 7]
与右子树组 [1, 4, 6, 8]
,遇到左子数组中元素 3 大于右子树组中元素 1。则左子数组从 3 开始,经过 5 一直到 7,与右子数组当前元素 1 都构成了逆序对。即 [3, 1]、[5, 1]、[7, 1]
都构成了逆序对。
因此,我们可以在合并两个有序数组的时候计算逆序对。具体做法如下:
- 使用全局变量
count
来存储逆序对的个数。然后进行归并排序。 - 归并过程中:
- 使用数组变量 arr 存放归并后的有序数组
- 使用两个指针 left、right 分别指向两个有序子序列 left_arr、right_arr 的开始位置。
- 如果
left_arr[left] <= right_arr[right]
,则将left_arr[left]
存入到结果数组arr
中,并将指针移动到下一位置。 - 如果
left_arr[left] > right_arr[right]
,则 记录当前左子序列中元素与当前右子序列元素所形成的逆序对的个数,并累加到count
中,即self.count += len(left_arr) - left
,然后将right_arr[right]
存入到结果数组arr
中,并将指针移动到下一位置。 - 重复以上,直到某一指针到达子序列末尾,将另一个子序列中的剩余元素存入到结果数组 arr 中。
class Solution(object):
count=0
def reversePairs(self, nums):
"""
:type nums: List[int]
:rtype: int
"""
nums=self.mergeSort(nums)
return self.count
def mergeSort(self, arr): # 分割过程
if len(arr) <= 1: # 数组元素个数小于等于 1 时,直接返回原数组
return arr
mid = len(arr) // 2 # 将数组从中间位置分为左右两个数组。
# 注意这里是递归的写法
left_arr = self.mergeSort(arr[0: mid]) # 递归将左子序列进行分割和排序
right_arr = self.mergeSort(arr[mid:]) # 递归将右子序列进行分割和排序
return self.merge(left_arr, right_arr) # 把当前序列组中有序子序列逐层向上,进行两两合并。
def merge(self, left_arr, right_arr): # 归并过程
arr = []
left, right = 0, 0
# 这里不能取等于,否则超出索引,下同
while left < len(left_arr) and right < len(right_arr):
# 将两个有序子序列中较小元素依次插入到结果数组中
if left_arr[left] <= right_arr[right]:
arr.append(left_arr[left])
left += 1
else:
self.count+=len(left_arr)-left
arr.append(right_arr[right])
right += 1
arr=arr+left_arr[left:]+right_arr[right:]
return arr # 返回排好序的结果数组
思路 2 树状数组: 见算法通关手册
2.6 计算右侧小于当前元素的个数(题315)
参考《『 4种解法一网打尽 』 有序数组、归并排序、树状数组和线段树的实现及注释》
给你一个整数数组 nums ,按要求返回一个新数组 counts 。数组 counts 有该性质: counts[i] 的值是 nums[i] 右侧小于 nums[i] 的元素的数量。
- 示例 1:
- 输入:nums = [5,2,6,1]
- 输出:[2,1,1,0]
- 解释:
- 5 的右侧有 2 个更小的元素 (2 和 1)
- 2 的右侧仅有 1 个更小的元素 (1)
- 6 的右侧有 1 个更小的元素 (1)
- 1 的右侧有 0 个更小的元素
- 示例 2:
- 输入:nums = [-1]
- 输出:[0]
2.6.1 归并排序
这题类似上一题求逆序对。但是本题我们要求解的是 nums[i]
右侧小于 nums[i]
的元素的数量,即以 nums[i]
为左端点的「逆序对」的数目。注意到,在常规的归并排序过程中,数组中的元素其位置会发生变化,所以在本题中我们则需要记录下每个元素的初始位置,以便将每个元素贡献的逆序对数目归功到对应的位置上。
由于数组中的元素会重复,不能使用哈希表,所以考虑为每个数值添加其对应的下标,即对于第 i
个元素 nums[i]
,可将其扩充为(nums[i], i)
。这样在nums排序过程中,即便 nums 中元素的位置发生了变化,也可将每个元素贡献的逆序对数目准确定位到对应的位置上,原理如下图所示:
class Solution:
def countSmaller(self, nums: List[int]) -> List[int]:
'''根据nums[*][0]进行排序,对应的index随之移动'''
def mergeSort(nums, low, high):
if low >= high: # 递归终止
return 0
'''递归排序'''
mid = low + (high-low)//2
mergeSort(nums, low, mid) # 左半部分逆序对数目
mergeSort(nums, mid+1, high) # 右半部分逆序对数目
'''nums[low, mid] 和 nums[mid+1, high] 已排序好'''
tmp = [] # 记录nums[low, high]排序结果
left, right = low, mid+1
while left<=mid and right<=high:
if nums[left][0] <= nums[right][0]: # 根据nums[*][0]进行排序
tmp.append(nums[left])
res[nums[left][1]] += right-(mid+1) # 记录逆序对数目【对应坐标nums[*][1]处】
left += 1
else:
tmp.append(nums[right])
right += 1
'''左或右数组需遍历完(最多只有一个未遍历完)'''
while left<=mid:
tmp.append(nums[left])
res[nums[left][1]] += right -(mid+1) # 记录逆序对数目【对应坐标nums[*][1]处】
left += 1
while right<=high:
tmp.append(nums[right])
right += 1
nums[low:high+1] = tmp
'''主程序'''
n = len(nums)
res = [0] * n # 存储结果
nums = [(num, idx) for idx, num in enumerate(nums)]
# 每个数值附上其对应的索引:
# 此时,nums[i][0]表示原来的数值,而nums[i][1]则表示原数值对应的索引(方便定位)
mergeSort(nums, 0, n-1) # 归并排序
return res
2.6.2 有序数组 (Sorted List)+ 二分搜索
基本思路: 维护一个有序数组 sl,从右往左依次往里添加 nums 中的元素,每次添加 nums[i] 前基于「二分搜索」判断出当前 sl 中比 nums[i] 小的元素个数(即 nums[i] 右侧比 nums[i] 还要小的元素个数),并计入答案即可。
class Solution:
def countSmaller(self, nums: List[int]) -> List[int]:
n = len(nums)
res = [0] * n
sl = [] # 有序数组
def bisect_left(arr, x, low, high):
left, right = low, high
while left<right:
mid = (left+right) // 2
if arr[mid] < x:
left = mid+1
else:
right = mid
# arr.insert(left, x)
return left
for i in range(n-1, -1, -1): # 反向遍历
# pos = bisect.bisect_left(sl, nums[i]) # 找到右边比当前值小的元素个数
pos = bisect_left(sl, nums[i], 0, len(sl)) # 找到右边比当前值小的元素个数
res[i] = pos # 记入答案
sl.insert(pos, nums[i]) # 将当前值加入有序数组中
return res
可简写为:
from sortedcontainers import SortedList
class Solution:
def countSmaller(self, nums: List[int]) -> List[int]:
n = len(nums)
res = [0] * n
sl = SortedList()
for i in range(n-1, -1, -1): # 反向遍历
cnt = sl.bisect_left(nums[i]) # 找到右边比当前值小的元素个数
res[i] = cnt # 记入答案
sl.add(nums[i]) # 将当前值加入有序数组中
return res
树状数组、线段树方法,请参考《『 4种解法一网打尽 』 有序数组、归并排序、树状数组和线段树的实现及注释》。
2.7 最大间距 (题164)
给定一个无序的数组 nums,返回 数组在排序之后,相邻元素之间最大的差值 。如果数组元素个数小于 2,则返回 0 。
您必须编写一个在「线性时间」内运行并使用「线性额外空间」的算法。
示例 :
- 输入: nums = [3,6,9,1]
- 输出: 3
- 解释: 排序后的数组是 [1,3,6,9], 其中相邻元素 (3,6) 和 (6,9) 之间都存在最大差值 3。
- 提示: 1 < = n u m s . l e n g t h < = 1 0 5 1 <= nums.length <= 10^5 1<=nums.length<=105, 0 < = n u m s [ i ] < = 1 0 9 0 <= nums[i] <= 10^9 0<=nums[i]<=109
2.7.1 基数排序
根据题意可知所有元素都是非负整数,且数值在 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)))
复杂度分析
- 时间复杂度:O(N)
- 空间复杂度:O(N)。
2.7.2 桶排序
例如:nums = [1,3,4,5,6,10,11,12,17]
则:每个桶的长度 = (17 - 1) / (9-1) = 2。桶的个数 = (17-1)/ 2 + 1 = 9
所以我们的桶为(左闭右开):
答案 = max(差值) = 5。
注意:在桶长度这里我们进行了和1取max的操作,这是为了一些边界条件的情况,比如数组是
[1,1,1,1]
。当然我们也可以不取max,把向下取整改为向上取整。
在所有排序算法里,我们一般认为快速排序是速度相对较快的,然而桶排序在大多数情况下比快速排序还要快,但是它付出的代价就是牺牲O(n)
空间的复杂度,且比归并排序的空间占用要多一点点,多出来的一点点就是可能出现的空桶。
class Solution:
def maximumGap(self, nums: List[int]) -> int:
if len(nums) < 2: return 0
# 一些初始化
max_ = max(nums)
min_ = min(nums)
max_gap = 0
each_bucket_len = max(1,(max_-min_) // (len(nums)-1))
buckets =[[] for _ in range((max_-min_) // each_bucket_len + 1)]
# 把数字放入桶中
for i in range(len(nums)):
loc = (nums[i] - min_) // each_bucket_len
buckets[loc].append(nums[i])
# 遍历桶更新答案
prev_max = float('inf')
for i in range(len(buckets)):
if buckets[i] and prev_max != float('inf'):
max_gap = max(max_gap, min(buckets[i])-prev_max)
if buckets[i]:
prev_max = max(buckets[i])
return max_gap
2.7.3 你的代码真是无敌了
class Solution(object):
def maximumGap(self, nums):
"""
:type nums: List[int]
:rtype: int
"""
if len(nums)==1:
return 0
else:
nums=sorted(nums)
res=0
for i in range(len(nums)-1):
if nums[i+1]-nums[i]>res:
res= nums[i+1]-nums[i]
return res
之前最快的是224ms。
2.8 数组的相对顺序(题1122)
给你两个数组,arr1 和 arr2,arr2 中的元素各不相同,arr2 中的每个元素都出现在 arr1 中。
对 arr1 中的元素进行排序,使 arr1 中项的相对顺序和 arr2 中的相对顺序相同。未在 arr2 中出现过的元素需要按照升序放在 arr1 的末尾。
- 示例 1:
- 输入: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]
- 示例 2:
- 输入:arr1 = [28,6,22,8,44,17], arr2 = [22,28,8,6]
- 输出:[22,28,8,6,17,44]
提示:1 <= arr1.length, arr2.length <= 1000;0 <= arr1[i], arr2[i] <= 1000
因为元素值范围在 [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
2.9 存在重复元素 III(题220)
给你一个整数数组 nums
和两个整数 k
和 t
。请你判断是否存在 两个不同下标 i 和 j,使得 abs(nums[i] - nums[j]) <= t
,同时又满足 abs(i - j) <= k
。如果存在则返回 true,不存在返回 false。
- 示例 1:
- 输入:nums = [1,2,3,1], k = 3, t = 0
- 输出:true
- 示例2:
- 输入:nums = [1,5,9,1,5,9], k = 2, t = 3
- 输出:false
题目中需要满足两个要求,一个是元素值的要求(abs(nums[i] - nums[j]) <= t) ,一个是下标范围的要求(abs(i - j) <= k)。所以对于任意一个位置 i 来说,合适的 j 应该在区间 [i - k, i + k]
内,同时 nums[j] 值应该在区间 [nums[i] - t, nums[i] + t]
内。
检测相邻 2 * k 个元素是否满足 abs(nums[i] - nums[j]) <= t
的方法。有两种思路:「桶排序」和「滑动窗口(固定长度)」。
2.9.1 桶排序
参考【题解】利用桶的原理O(n),Python3 - 存在重复元素 III - 力扣
- 利用桶排序的思想,将桶的大小设置为
t + 1
。只需要使用一重循环遍历位置i
,然后根据nums[i] // (t + 1)
,从而决定将nums[i]
放入哪个桶中。 - 这样在同一个桶内各个元素之间的差值绝对值都小于等于
t
。而相邻桶之间的元素,只需要校验一下两个桶之间的差值是否不超过t
。这样就可以以 O ( 1 ) O(1) O(1) 的时间复杂度检测相邻2 * k
个元素是否满足abs(nums[i] - nums[j]) <= t
。 - 而
abs(i - j) <= k
条件则可以通过在一重循环遍历时,将超出范围的nums[i - k]
从对应桶中删除,从而保证桶中元素一定满足abs(i - j) <= k
。
具体步骤如下:
- 将每个桶的大小设置为
t + 1
。我们将元素按照大小依次放入不同的桶中。 - 遍历数组
nums
中的元素,对于元素nums[i]
:- 如果
nums[i]
放入桶之前桶里已经有元素了,那么这两个元素必然满足abs(nums[i] - nums[j]) <= t
, - 如果之前桶里没有元素,那么就将
nums[i]
放入对应桶中。 - 再判断左右桶的左右两侧桶中是否有元素满足
abs(nums[i] - nums[j]) <= t
。 - 然后将
nums[i - k]
之前的桶清空,因为这些桶中的元素与nums[i]
已经不满足abs(i - j) <= k
了。
- 如果
- 最后上述满足条件的情况就返回
True
,最终遍历完仍不满足条件就返回False
。
代码
class Solution:
def containsNearbyAlmostDuplicate(self, nums: List[int], k: int, t: int) -> bool:
bucket_dict = dict()
for i in range(len(nums)):
# 将 nums[i] 划分到大小为 t + 1 的不同桶中
num = nums[i] // (t + 1)
# 桶中已经有元素了
if num in bucket_dict:
return True
# 把 nums[i] 放入桶中
bucket_dict[num] = nums[i]
# 判断左侧桶是否满足条件
if (num - 1) in bucket_dict and abs(bucket_dict[num - 1] - nums[i]) <= t:
return True
# 判断右侧桶是否满足条件
if (num + 1) in bucket_dict and abs(bucket_dict[num + 1] - nums[i]) <= t:
return True
# 将 i - k 之前的旧桶清除,因为之前的桶已经不满足条件了
if i >= k:
bucket_dict.pop(nums[i - k] // (t + 1))
return False
复杂度分析
- 时间复杂度: O ( n ) O(n) O(n)。 n n n 是给定数组长度。
- 空间复杂度: O ( m i n ( n , k ) ) O(min(n, k)) O(min(n,k))。桶中最多包含 m i n ( n , k + 1 ) min(n, k + 1) min(n,k+1) 个元素。
2.9.2滑动窗口(固定长度)
- 使用一个长度为
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
复杂度分析
- 时间复杂度: O ( n × log 2 ( m i n ( n , k ) ) ) O(n \times \log_2(min(n, k))) O(n×log2(min(n,k)))。
- 空间复杂度: O ( m i n ( n , k ) ) O(min(n, k)) O(min(n,k))。
2.10 合并区间(题56)
给定数组 intervals
表示若干个区间的集合,其中单个区间为 intervals[i] = [starti, endi]
。请合并所有重叠的区间,并返回一个不重叠的区间数组,该数组需恰好覆盖输入中的所有区间。
-
示例 1:
- 输入:intervals = [[1,3],[2,6],[8,10],[15,18]]
- 输出:[[1,6],[8,10],[15,18]]
- 解释:区间 [1,3] 和 [2,6] 重叠, 将它们合并为 [1,6].
-
示例 2:
- 输入:intervals = [[1,4],[4,5]]
- 输出:[[1,5]]
- 解释:区间 [1,4] 和 [4,5] 可被视为重叠区间。
-
说明:
- 1 ≤ i n t e r v a l s . l e n g t h ≤ 1 0 4 1 \le intervals.length \le 10^4 1≤intervals.length≤104。
- i n t e r v a l s [ i ] . l e n g t h = = 2 intervals[i].length == 2 intervals[i].length==2。
- 0 ≤ s t a r t i ≤ e n d i ≤ 1 0 4 0 \le starti \le endi \le 10^4 0≤starti≤endi≤104。
此题可以考虑对区间进行排序:
- 设定一个数组
ans
用于表示最终不重叠的区间数组 - 对原始区间先按照区间左端点大小从小到大进行排序。
- 遍历所有区间,先将第一个区间加入
ans
数组中。然后依次考虑后边的区间:- 如果第
i
个区间左端点在前一个区间右端点右侧,则这两个区间不会重合,直接将该区间加入ans
数组中。 - 否则的话,这两个区间重合,判断一下两个区间的右区间值,更新前一个区间的右区间值为较大值,然后继续考虑下一个区间,以此类推。
- 如果第
- 最后返回数组
ans
。
代码
class Solution:
def merge(self, intervals: List[List[int]]) -> List[List[int]]:
intervals.sort(key=lambda x: x[0])
ans = []
for interval in intervals:
# not ans表示初始ans为空。ans[-1][1]是最后一个加入的区间的右端点
if not ans or ans[-1][1] < interval[0]:
ans.append(interval)
else:
ans[-1][1] = max(ans[-1][1], interval[1])
return ans
- 时间复杂度: O ( n × log 2 n ) O(n \times \log_2 n) O(n×log2n)。其中 n n n 为区间数量。
- 空间复杂度: O ( n ) O(n) O(n)。
2.11 最大数(题179)
2.11.1 自定义排序(内置函数)
本质上是给数组进行排序。假设 x、y 是数组 nums 中的两个元素,规定 排序判断规则 为:如果拼接字符串 x + y < y + x,则 y > x 。y 应该排在 x 前面。反之,则 y < x。
按照上述规则,对原数组套用任何方法进行排序即可。这里我们使用了 functools.cmp_to_key 自定义排序函数。
import functools
class Solution:
def largestNumber(self, nums: List[int]) -> str:
def cmp(a, b):
if a + b == b + a:
return 0
elif a + b > b + a:
return 1
else:
return -1
nums_s = list(map(str, nums))
nums_s.sort(key=functools.cmp_to_key(cmp), reverse=True)
return str(int(''.join(nums_s)))
- 时间复杂度: O ( n 2 ) O(n^2) O(n2)。其中 n n n 为区间数量。
- 空间复杂度: O ( n ) O(n) O(n)。
类似的还有剑指 Offer 45. 把数组排成最小的数:
import functools
class Solution:
def minNumber(self, nums: List[int]) -> str:
def cmp(a, b):
if a + b == b + a:
return 0
elif a + b > b + a:
return 1
else:
return -1
nums_s = list(map(str, nums))
nums_s.sort(key=functools.cmp_to_key(cmp))
return ''.join(nums_s)
2.11.2 自定义排序(快速排序)
class Solution:
def minNumber(self, nums: List[int]) -> str:
def quick_sort(l , r):
if l >= r: return
i, j = l, r
while i < j:
while strs[j] + strs[l] >= strs[l] + strs[j] and i < j: j -= 1
while strs[i] + strs[l] <= strs[l] + strs[i] and i < j: i += 1
strs[i], strs[j] = strs[j], strs[i]
strs[i], strs[l] = strs[l], strs[i]
quick_sort(l, i - 1)
quick_sort(i + 1, r)
strs = [str(num) for num in nums]
quick_sort(0, len(strs) - 1)
return ''.join(strs)