资源:力扣题库、LeetCode 刷题列表、代码随想录
参考《数组排序》、《排序算法总结(Python版)》
冒泡排序(Bubble Sort
)基本思想:通过相邻元素之间的比较与交换,使值较小的元素逐步从后面移到前面,值较大的元素从前面移到后面。这个过程就像水底的气泡一样向上冒,这也是冒泡排序法名字的由来。
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: 某一趟遍历如果没有数据交换,则说明已经排好序了,因此不用再进行迭代了。用一个标记记录这个状态即可。
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
选择排序(Selection Sort
)基本思想:每一趟排序中,从未排序部分中选出一个值最小的元素,与未排序部分第 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
个元素重复上述排序过程,直到所有元素都变为已排序部分,则排序结束。动画演示
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)
插入排序(Insertion Sort
)基本思想:将整个序列分为两部分:前面 i
个元素为有序序列,后面 n - i
个元素为无序序列。每一次排序,将无序序列的第 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
个元素作为无序序列。动画演示:
插入排序比对操作主要是用来寻找新元素的待插入位置,而插入位置是靠倒序遍历前面有序数组来找到的。
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)。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)
从上面分析可以知道,插入排序在初始序列升序排列时,时间复杂度最低。实际上,序列越是有序(升序),插入排序的比对次数就越少。
希尔排序从此入手,对无序列表进行间隔划分,然后对每个子列表进行插入排序。随着子列表数越来越少,整个无序表也越来越有序,从而减少整体排序的比对次数。
希尔排序(Shell Sort
)基本思想:将整个序列切按照一定的间隔值(gap
)划分为若干个子序列,每个子序列分别排序。然后逐渐缩小gap
值,进行下一次排序,直至gap=1
。
gap
。1
个元素开始一次分成若干个子序列,即分别将所有位置相隔为 gap
的元素视为一个子序列。gap = 1
,排序结束。时间复杂度:介于 O ( n × log 2 n ) O(n \times \log_2 n) O(n×log2n) 与 O ( n 2 ) O(n^2) O(n2) 之间。
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) 之间。排序稳定性:希尔排序方法是一种 不稳定排序算法。
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)
归并排序(Merge Sort
)基本思想:采用经典的分治策略,先递归地将当前序列平均分成两半。然后将有序序列两两合并,最终合并成一个有序序列。
mid
,从中心位置将序列分成左右两个子序列 left_arr
、right_arr
。left_arr
、right_arr
分别进行递归分割。arr
存放归并后的有序数组。left
、right
分别指向两个有序子序列 left_arr、right_arr 的开始位置。arr
中,并将指针移动到下一位置。arr
中。arr
。动画演示:
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)。merge(left_arr, right_arr):
算法能够使前一个序列中那个相同元素先被复制,从而确保这两个元素的相对次序不发生改变。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
快速排序(Quick Sort)基本思想: 根据序列的中值将序列分为两半,前一半都小于中值,后一半都大于中值,然后每部分再进行递归的快速排序。
递归结束条件:序列只有一个元素,不需要再排序
中值选取:每次根据中值将序列分为规模相等的两半是最好的情况,此时中值就是序列的中位数,但是寻找中位数也需要开销,所以可以所以找一个数作为中值,比如序列的第一个数。
分裂过程: 目标是找到“中值”的位置。
快速排序算法的时间复杂度主要跟基准数的选择有关。本文中是将当前序列中第 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),也就是平均时间复杂度。
下面来总结一下:
从待排序列中找到一个基准数 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
桶排序(Bucket Sort)基本思想:将未排序数组分到若干个「桶」中,每个桶的元素再进行单独排序。
k
个相同大小的子区间,每个区间称为一个桶。图解演示
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)
堆排序(Heap sort)基本思想:
借用「堆结构」所设计的排序算法。将数组转化为大顶堆,重复从大顶堆中取出数值最大的节点,并让剩余的堆结构继续维持大顶堆性质。
堆(Heap):符合以下两个条件之一的完全二叉树:
1
个大顶堆(初始堆),使得 n
个元素的最大值处于序列的第 1
个位置。1
个元素(最大值元素)与第 n
个元素的位置。将序列前 n - 1
个元素组成的子序列调整成一个新的大顶堆,使得 n - 1
个元素的最大值处于序列第 1
个位置,从而得到第 2
个最大值元素。1
个元素(最大值元素)与第 n - 1
个元素的位置。将序列前 n - 2
个元素组成的子序列调整成一个新的大顶堆,使得 n - 2
个元素的最大值处于序列第 1
个位置,从而得到第 3
个最大值元素。1
个元素(最大值元素)与当前子序列最后一个元素位置,并将其调整成新的大顶堆。直到子序列剩下一个元素时,排序结束。此时整个序列就变成了一个有序序列。从堆排序算法步骤中可以看出:堆排序算法主要涉及「调整堆」和「建立初始堆」两个步骤。
调整堆方法:把移走了最大值元素以后的剩余元素组成的序列再构造为一个新的堆积。具体步骤如下:
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
互换位置。调整堆结束。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
的节点,调用堆调整算法,使其与子树形成大顶堆。[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]
。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)
计数排序(Counting Sort)基本思想:使用一个额外的数组 counts
,其中 counts[i]
表示原数组 arr
中值等于 i
的元素个数。然后根据数组 counts
来将 arr
中的元素排到正确的位置。
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
。动画演示
counts
的长度取决于待排序数组中数据的范围(大小等于待排序数组最大值减去最小值再加 1
)。所以计数排序算法对于数据范围很大的数组,需要大量的内存。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)
假设要对 10 万个手机号码进行排序,显然桶排序和计数排序都不太适合,那怎样才能做到时间复杂度为 O(n) 呢? 此时可以考虑基数排序。
手机号码有这样的规律,假设要比较两个手机号码 a, b 的大小,如果在前面几位中,a 手机号码已经比 b大了,那后面几位就不用看了。所以借助 稳定排序算法,我们可以这么实现:从手机号码的最后一位开始,分别按照每一位的数字对手机号码进行排序,依次往前进行,经过 11 次排序之后,手机号码就都有序了。
基数排序(Radix Sort)基本思想:将所有待比较数值(正整数)统一为同样的数位长度,数位较短的数前面补零。然后后,从最低位开始,依次进行排序。这样从最低位排序一直到最高位排序完成以后,数列就变成一个有序序列。
基数排序算法可以采用「最低位优先法(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]
,完成排序。基数排序是一种非比较型整数排序算法,其原理是将整数按位数切割成不同的数字,然后按每个位数分别比较。由于整数也可以表达字符串(比如名字或日期)和特定格式的浮点数,所以基数排序也不是只能使用于整数。
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)
冒泡排序,简单选择排序,堆排序,直接插入排序,希尔排序的空间复杂度为O(1),因为需要一个临时变量来交换元素位置,(另外遍历序列时自然少不了用一个变量来做索引)
快速排序空间复杂度为logn(因为递归调用了)
归并排序空间复杂是O(n),需要一个大小为n的临时数组.。基数排序的空间复杂是O(n),桶排序的空间复杂度不确定。
所有排序算法中最快的应该是桶排序(很多人误以为是快速排序,实际上不是.不过实际应用中快速排序用的多)。但桶排序一般用的不多,因为有几个比较大的缺陷.
给定一个数组 nums,编写一个函数将所有 0 移动到数组的末尾,同时保持非零元素的相对顺序。请注意 ,必须在不复制数组的情况下原地对数组进行操作。
思路 1:冒泡排序(超时)
冒泡排序的思想,就是通过相邻元素的比较与交换,使得较大元素从前面移到后面。我们可以借用冒泡排序的思想,将值为 0 的元素移动到数组末尾。因为冒泡排序的时间复杂度为 O ( n 2 ) O(n^2) O(n2) 。所以这种做法会导致超时。
思路 2:双指针
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
方法一:单指针
这道题和上一题很类似,最简单的方法是遍历两次,先将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。当我们从左向右遍历整个数组时:
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 大的部分)。具体步骤如下:
nums[index] == 0
,就交换 nums[index] 和 nums[left],同时将 left 右移。如果遇到 nums[index] == 2
,就交换 nums[index] 和 nums[right],同时将 right 左移。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
给定整数数组 nums 和整数 k,请返回数组中第 k 个最大的元素。示例:
这道题使用改进的冒泡排序、选择排序、插入排序都会超时。希尔排序(1440ms)、归并排序(1016ms)、堆排序(640ms),这些都是可以通过的。也可以考虑使用快速排序。
快速排序思路:
使用快速排序在每次调整时,都会确定一个元素的最终位置,且以该元素为界限,将数组分成了左右两个子数组,左子数组中的元素都比该元素小,右子树组中的元素都比该元素大。
这样,只要某次划分的元素位置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
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
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
升序堆排序的思路如下:
将无序序列构造成第 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]
复杂度分析
num
:
k
个,则将当前元素 num
放入优先队列中。k
个,并且当前元素 num
大于优先队列的队头元素,则弹出队头元素,并将当前元素 num
插入到优先队列中。 这里我们借助了 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)
复杂度分析
给你一个整数数组 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
其它排序方法详见算法通关手册。
在数组中的两个数字,如果前面一个数字大于后面的数字,则这两个数字组成一个逆序对。输入一个数组,求出这个数组中的逆序对的总数。示例:
思路 1:归并排序
归并排序主要分为:「分解过程」和「合并过程」。其中「合并过程」实质上是两个有序数组的合并过程。
每当遇到 左子数组当前元素 > 右子树组当前元素时,意味着「左子数组从当前元素开始,一直到左子数组末尾元素」与「右子树组当前元素」构成了若干个逆序对。
比如上图中的左子数组 [0, 3, 5, 7]
与右子树组 [1, 4, 6, 8]
,遇到左子数组中元素 3 大于右子树组中元素 1。则左子数组从 3 开始,经过 5 一直到 7,与右子数组当前元素 1 都构成了逆序对。即 [3, 1]、[5, 1]、[7, 1]
都构成了逆序对。
因此,我们可以在合并两个有序数组的时候计算逆序对。具体做法如下:
count
来存储逆序对的个数。然后进行归并排序。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
中,并将指针移动到下一位置。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 树状数组: 见算法通关手册
参考《『 4种解法一网打尽 』 有序数组、归并排序、树状数组和线段树的实现及注释》
给你一个整数数组 nums ,按要求返回一个新数组 counts 。数组 counts 有该性质: counts[i] 的值是 nums[i] 右侧小于 nums[i] 的元素的数量。
这题类似上一题求逆序对。但是本题我们要求解的是 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
基本思路: 维护一个有序数组 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种解法一网打尽 』 有序数组、归并排序、树状数组和线段树的实现及注释》。
给定一个无序的数组 nums,返回 数组在排序之后,相邻元素之间最大的差值 。如果数组元素个数小于 2,则返回 0 。
您必须编写一个在「线性时间」内运行并使用「线性额外空间」的算法。
示例 :
根据题意可知所有元素都是非负整数,且数值在 32 位有符号整数范围内。所以我们可以选择基数排序。基数排序的步骤如下:
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)))
复杂度分析
例如: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
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
给你两个数组,arr1 和 arr2,arr2 中的元素各不相同,arr2 中的每个元素都出现在 arr1 中。
对 arr1 中的元素进行排序,使 arr1 中项的相对顺序和 arr2 中的相对顺序相同。未在 arr2 中出现过的元素需要按照升序放在 arr1 的末尾。
提示:1 <= arr1.length, arr2.length <= 1000;0 <= arr1[i], arr2[i] <= 1000
因为元素值范围在 [0, 1000],所以可以使用计数排序的思路来解题。
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。
题目中需要满足两个要求,一个是元素值的要求(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
的方法。有两种思路:「桶排序」和「滑动窗口(固定长度)」。
参考【题解】利用桶的原理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
复杂度分析
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
复杂度分析
给定数组 intervals
表示若干个区间的集合,其中单个区间为 intervals[i] = [starti, endi]
。请合并所有重叠的区间,并返回一个不重叠的区间数组,该数组需恰好覆盖输入中的所有区间。
示例 1:
示例 2:
说明:
此题可以考虑对区间进行排序:
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
本质上是给数组进行排序。假设 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)))
类似的还有剑指 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)
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)