LeetCode《算法通关手册》 1.2 数组排序
Python强推:算法通关手册(LeetCode) | 算法通关手册(LeetCode) (itcharge.cn)
目录
文章目录
- 1.2 数组排序
- 1.2.1 选择排序
- 1.2.2 冒泡排序
- [283. 移动零 - 力扣(LeetCode)](https://leetcode.cn/problems/move-zeroes/description/)
- 方法一:冒泡排序
- 方法二:快慢指针——交换
- 方法三:快慢指针——前移
- [LCR 164. 破解闯关密码 - 力扣(LeetCode)](https://leetcode.cn/problems/ba-shu-zu-pai-cheng-zui-xiao-de-shu-lcof/description/)
- 方法一:冒泡排序(拼接数字)
- [179. 最大数 - 力扣(LeetCode)](https://leetcode.cn/problems/largest-number/description/)
- 方法一:冒泡排序(拼接数字)
- 1.2.3 插入排序
- 1.2.4 希尔排序
- [506. 相对名次 - 力扣(LeetCode)](https://leetcode.cn/problems/relative-ranks/description/)
- 方法一:希尔排序+字典
- 1.2.5 归并排序
- [88. 合并两个有序数组 - 力扣(LeetCode)](https://leetcode.cn/problems/merge-sorted-array/solutions/666608/he-bing-liang-ge-you-xu-shu-zu-by-leetco-rrb0/)
- 方法一:逆向快慢指针
- [LCR 170. 交易逆序对的总数 - 力扣(LeetCode)](https://leetcode.cn/problems/shu-zu-zhong-de-ni-xu-dui-lcof/description/)
- 方法一:归并排序合并过程中统计
- [315. 计算右侧小于当前元素的个数 - 力扣(LeetCode)](https://leetcode.cn/problems/count-of-smaller-numbers-after-self/description/)
- 方法一:归并排序合并过程中统计
- [169. 多数元素 - 力扣(LeetCode)](https://leetcode.cn/problems/majority-element/description/)
- 方法一:分治
- 方法二:摩尔投票法
- 1.2.6 快速排序
- [75. 颜色分类 - 力扣(LeetCode)](https://leetcode.cn/problems/sort-colors/description/)
- 方法一:三路哨兵划分
- 1.2.7 堆排序
- [215. 数组中的第K个最大元素 - 力扣(LeetCode)](https://leetcode.cn/problems/kth-largest-element-in-an-array/description/)
- 方法一:堆排序
- 方法二:快速排序
- [LCR 159. 库存管理 III - 力扣(LeetCode)](https://leetcode.cn/problems/zui-xiao-de-kge-shu-lcof/description/)
- 方法一:堆排序
- 方法二:快速排序
- 1.2.8 计数排序
- [1122. 数组的相对排序 - 力扣(LeetCode)](https://leetcode.cn/problems/relative-sort-array/description/)
- 方法一:计数排序
- 1.2.9 桶排序
- [220. 存在重复元素 III - 力扣(LeetCode)](https://leetcode.cn/problems/contains-duplicate-iii/description/)
- 方法一:桶排序
- 1.2.10 基数排序
- [164. 最大间距 - 力扣(LeetCode)](https://leetcode.cn/problems/maximum-gap/description/)
- 方法一:基数排序
- 方法二:桶排序
- 1.2.11 其他排序问题
- [217. 存在重复元素 - 力扣(LeetCode)](https://leetcode.cn/problems/contains-duplicate/description/)
- 方法一:集合
- [136. 只出现一次的数字 - 力扣(LeetCode)](https://leetcode.cn/problems/single-number/description/)
- 方法一:位运算
- [56. 合并区间 - 力扣(LeetCode)](https://leetcode.cn/problems/merge-intervals/description/)
- 方法一:排序
1.2 数组排序
1.2.1 选择排序
选择排序(Selection Sort):将数组分为两个区间:左侧为已排序区间,右侧为未排序区间。每趟从未排序区间中选择一个值最小的元素,放到已排序区间的末尾,从而将该元素划分到已排序区间。
# 912. 排序数组
# 题目:给你一个整数数组 nums,请你将该数组升序排列。
class Solution:
def selectionSort(self, nums: List[int]) -> List[int]:
n = len(nums)
for i in range(n - 1):
# 在未排序区间[i,n-1]上找最小值位置,初始为i
min_i = i
for j in range(i + 1, n):
if nums[j] < nums[min_i]:
min_i = j
# 将最小值与i上的元素进行交换
nums[i], nums[min_i] = nums[min_i], nums[i]
return nums
- 时间复杂度: O ( n 2 ) O(n^2) O(n2),需要 n ( n − 1 ) 2 \frac{n(n-1)}{2} 2n(n−1)次元素比较。
- 空间复杂度: O ( 1 ) O(1) O(1),原地排序算法,只用到指针变量 i i i、 j j j、最小值位置 m i n _ i min\_i min_i。
- 适用情况:
- 选择排序需要移动较多次数的元素,并且排序时间效率比较低。
- 选择排序比较适合于参加排序序列的数据量较小的情况。原地操作,在空间复杂度要求较高时适用。
- **排序稳定性:**由于值最小元素与未排序区间第1个元素的交换动作是在不相邻的元素之间进行的,因此很有可能会改变相等元素的相对顺序,因此,选择排序法是一种 不稳定排序算法。
1.2.2 冒泡排序
冒泡排序(Bubble Sort):经过多次迭代,通过相邻元素之间的比较与交换,使值较小的元素逐步从后面移到前面,值较大的元素从前面移到后面。
# 912. 排序数组
# 题目:给你一个整数数组 nums,请你将该数组升序排列。
class Solution:
def bubbleSort(self, nums: [int]) -> [int]:
n = len(nums)
# 第i趟冒泡:对前n-i个元素执行冒泡,使第i+1大的元素被放在正确位置上
for i in range(n - 1):
flag = False # 初始化标志位
# 将未排序区间[0,n-i-1]中相邻两个元素进行比较
for j in range(n - 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
- 时间复杂度: O ( n 2 ) O(n^2) O(n2),需要 n ( n − 1 ) 2 \frac{n(n-1)}{2} 2n(n−1)次元素比较。
- 空间复杂度: O ( 1 ) O(1) O(1),原地排序算法,只用到指针变量 i i i、 j j j、标志位 f l a g flag flag(提前判断是否已经排序完成)。
- 适用情况:
- 冒泡排序需要移动较多次数的元素,并且排序时间效率比较低。
- 冒泡排序比较适合于参加排序序列的数据量较小的情况,尤其是当序列的初始状态为基本有序的情况。原地操作,在空间复杂度要求较高时适用。
- **排序稳定性:**由于元素交换是在相邻元素之间进行的,不会改变相等元素的相对顺序,因此,冒泡排序法是一种 稳定排序算法。
283. 移动零 - 力扣(LeetCode)
题目:
给定一个数组
nums
,编写一个函数将所有0
移动到数组的末尾,同时保持非零元素的相对顺序。请注意 ,必须在不复制数组的情况下原地对数组进行操作。
示例:
输入: nums = [0,1,0,3,12] 输出: [1,3,12,0,0]
输入: nums = [0] 输出: [0]
说明:
1 <= nums.length <= 104
-231 <= nums[i] <= 231 - 1
**标签:**数组、双指针
**难度:**简单
方法一:冒泡排序
**思路:**冒泡排序,如果当前元素为0,与下一个元素交换
时间复杂度: O ( n 2 ) O(n^2) O(n2)
空间复杂度: O ( 1 ) O(1) O(1)
class Solution:
def moveZeroes(self, nums: List[int]) -> None:
n = len(nums)
for i in range(n - 1):
flag = False
for j in range(n - i - 1):
if nums[j] == 0 and nums[j + 1] != 0:
nums[j], nums[j + 1] = nums[j + 1], nums[j]
flag = True
if not flag:
break
方法二:快慢指针——交换
**思路:**快慢指针。
slow指针指向处理好的非0数字数组的尾部,fast指针指向当前待处理元素。
不断向右移动fast指针,每次移动到非零数,则将左右指针对应的数交换,并将slow右移。
一直维持slow指针左侧均为处理好的非零数,从slow指针到fast指针均为0。
时间复杂度: O ( n ) O(n) O(n)
空间复杂度: O ( 1 ) O(1) O(1)
class Solution:
def moveZeroes(self, nums: List[int]) -> None:
n = len(nums)
slow = 0
fast = 0
while fast < n:
if nums[fast] != 0:
nums[slow], nums[fast] = nums[fast], nums[slow]
slow += 1
fast += 1
方法三:快慢指针——前移
**思路:**快慢指针。将所有非0数字移到前面,再在尾部补上相应数量的0
时间复杂度: O ( n ) O(n) O(n)
空间复杂度: O ( 1 ) O(1) O(1)
class Solution:
def moveZeroes(self, nums: List[int]) -> None:
n = len(nums)
slow = 0
fast = 0
while fast < n:
if nums[fast] != 0:
nums[slow] = nums[fast]
slow += 1
fast += 1
for i in range(slow, n):
nums[i] = 0
LCR 164. 破解闯关密码 - 力扣(LeetCode)
题目:
闯关游戏需要破解一组密码,闯关组给出的有关密码的线索是:
- 一个拥有密码所有元素的非负整数数组
password
- 密码是
password
中所有元素拼接后得到的最小的一个数请编写一个程序返回这个密码。
示例:
输入: password = [15, 8, 7] 输出: "1578"
输入: password = [0, 3, 30, 34, 5, 9] 输出: "03033459"
说明:
0 < password.length <= 100
**标签:**贪心、字符串、排序
**难度:**中等
方法一:冒泡排序(拼接数字)
**思路:**如果拼接字符串 x + y > y + x x+y>y+x x+y>y+x,则 y y y应该排在 x x x前面;如果拼接字符串 x + y < y + x x+y<y+x x+y<y+x,则 x x x应该排在 y y y前面。从而使拼接起来的数字尽可能的小。
时间复杂度: O ( n ∗ log n ) O(n*\log n) O(n∗logn)
空间复杂度: O ( 1 ) O(1) O(1)
class Solution:
def crackPassword(self, password: List[int]) -> str:
n = len(password)
password = [str(num) for num in password]
for i in range(n - 1):
flag = False
for j in range(n - i - 1):
if password[j] + password[j + 1] > password[j + 1] + password[j]:
password[j], password[j + 1] = password[j + 1], password[j]
flag = True
if not flag:
break
return ''.join(password)
179. 最大数 - 力扣(LeetCode)
题目:
给定一组非负整数
nums
,重新排列每个数的顺序(每个数不可拆分)使之组成一个最大的整数。**注意:**输出结果可能非常大,所以你需要返回一个字符串而不是整数。
示例:
输入:nums = [10,2] 输出:"210"
输入:nums = [3,30,34,5,9] 输出:"9534330"
说明:
1 <= nums.length <= 100
0 <= nums[i] <= 10^9
**标签:**贪心、数组、字符串、排序
**难度:**中等
方法一:冒泡排序(拼接数字)
**思路:**如果拼接字符串 x + y < y + x x+y<y+x x+y<y+x,则 y y y应该排在 x x x前面;如果拼接字符串 x + y > y + x x+y>y+x x+y>y+x,则 x x x应该排在 y y y前面。从而使拼接起来的数字尽可能的大。
时间复杂度: O ( n ∗ log n ) O(n*\log n) O(n∗logn)
空间复杂度: O ( 1 ) O(1) O(1)
class Solution:
def largestNumber(self, nums: List[int]) -> str:
n = len(nums)
nums = [str(num) for num in nums]
for i in range(n - 1):
flag = False
for j in range(n - i - 1):
if nums[j] + nums[j + 1] < nums[j + 1] + nums[j]:
nums[j], nums[j + 1] = nums[j + 1], nums[j]
flag = True
if not flag:
break
return str(int(''.join(nums)))
1.2.3 插入排序
插入排序(Insertion Sort):将数组分为两个区间:左侧为有序区间,右侧为无序区间。每趟从无序区间取出一个元素,然后将其插入到有序区间的适当位置。
# 912. 排序数组
# 题目:给你一个整数数组 nums,请你将该数组升序排列。
class Solution:
def insertionSort(self, nums: [int]) -> [int]:
n = len(nums)
for i in range(1, n):
# 无序区间[i,n-1]的第一个元素base
base = nums[i]
j = i
# 从右至左遍历有序区间
while j > 0 and nums[j - 1] > base:
# 将大于base的元素依次右移一位
nums[j] = nums[j - 1]
j -= 1
# 将base插入到适当位置
nums[j] = base
return nums
- 时间复杂度: O ( n 2 ) O(n^2) O(n2),需要 n ( n − 1 ) 2 \frac{n(n-1)}{2} 2n(n−1)次元素比较。
- 空间复杂度: O ( 1 ) O(1) O(1),原地排序算法,只用到指针变量 i i i、 j j j、无序区间的第1个元素 b a s e base base。
- 适用情况:
- 插入排序需要移动较多次数的元素,并且排序时间效率比较低。
- 插入排序比较适合于参加排序序列的数据量较小的情况,尤其是当序列的初始状态为基本有序的情况。原地操作,在空间复杂度要求较高时适用。
- 插入排序(基于元素赋值)比冒泡排序(基于元素交换)计算开销更小
- 插入排序(时间与序列初始状态有关)比选择排序(时间与序列初始状态无关)计算效率更高
- **排序稳定性:**在插入操作过程中,每次都将元素插入到相等元素的右侧,并不会改变相等元素的相对顺序。因此,插入排序方法是一种 稳定排序算法。
1.2.4 希尔排序
希尔排序(Shell Sort):将整个数组切按照一定的间隔取值划分为若干个子数组,每个子数组分别进行插入排序。然后逐渐缩小间隔进行下一轮划分子数组和对子数组进行插入排序。直至最后一轮排序间隔为1,对整个数组进行插入排序。
、
# 912. 排序数组
# 题目:给你一个整数数组 nums,请你将该数组升序排列。
class Solution:
def shellSort(self, nums: [int]) -> [int]:
n = len(nums)
gap = n // 2
# 按照gap分组
while gap > 0:
# 对每组进行插入排序
for i in range(gap, n):
base = nums[i]
j = i
while j >= gap and nums[j - gap] > base:
nums[j] = nums[j - gap]
j -= gap
nums[j] = base
# 缩小 gap 间隔
gap = gap // 2
return nums
- 时间复杂度: O ( n ∗ l o g n ) O(n*log n) O(n∗logn),外层循环 log n \log n logn量级,内层插入排序 n n n量级。
- 空间复杂度: O ( 1 ) O(1) O(1),原地排序算法,只用到指针变量 i i i、 j j j、无序区间的第1个元素 b a s e base base、间隔 g a p gap gap。
- **排序稳定性:**在一次插入排序是稳定的,不会改变相等元素的相对顺序,但是在不同的插入排序中,相等元素可能在各自的插入排序中移动。因此,希尔排序方法是一种 不稳定排序算法。
506. 相对名次 - 力扣(LeetCode)
题目:
给你一个长度为
n
的整数数组score
,其中score[i]
是第i
位运动员在比赛中的得分。所有得分都 互不相同 。运动员将根据得分 决定名次 ,其中名次第
1
的运动员得分最高,名次第2
的运动员得分第2
高,依此类推。运动员的名次决定了他们的获奖情况:
- 名次第
1
的运动员获金牌"Gold Medal"
。- 名次第
2
的运动员获银牌"Silver Medal"
。- 名次第
3
的运动员获铜牌"Bronze Medal"
。- 从名次第
4
到第n
的运动员,只能获得他们的名次编号(即,名次第x
的运动员获得编号"x"
)。使用长度为
n
的数组answer
返回获奖,其中answer[i]
是第i
位运动员的获奖情况。示例:
输入: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] 。
说明:
n == score.length
1 <= n <= 10^4
0 <= score[i] <= 10^6
score
中的所有值 互不相同**标签:**数组、排序、堆(优先队列)
**难度:**简单
方法一:希尔排序+字典
**思路:**用字典记录运动员原来的坐标,排序后将名次更新到相应的位置上。
时间复杂度: O ( n ∗ log n ) O(n*\log n) O(n∗logn)
空间复杂度: O ( n ) O(n) O(n)
class Solution:
def shellSort(self, nums: [int]) -> [int]:
n = len(nums)
gap = n // 2
# 按照gap分组
while gap > 0:
# 对每组进行插入排序
for i in range(gap, n):
base = nums[i]
j = i
while j >= gap and nums[j - gap] < base:
nums[j] = nums[j - gap]
j -= gap
nums[j] = base
# 缩小 gap 间隔
gap = gap // 2
return nums
def findRelativeRanks(self, score: List[int]) -> List[str]:
ans = [0 for _ in range(len(score))]
score_dict = dict()
for i, j in enumerate(score):
score_dict[j] = i
score_sort = self.shellSort(score)
for i, j in enumerate(score_sort):
if i == 0:
ans[score_dict[j]] = "Gold Medal"
elif i == 1:
ans[score_dict[j]] = "Silver Medal"
elif i == 2:
ans[score_dict[j]] = "Bronze Medal"
else:
ans[score_dict[j]] = str(i+1)
return ans
1.2.5 归并排序
归并排序(Merge Sort):采用经典的分治策略,先递归地将当前数组平均分成两半,然后将有序数组两两合并,最终合并成一个有序数组。
# 912. 排序数组
# 题目:给你一个整数数组 nums,请你将该数组升序排列。
class Solution:
# 排序+合并
def merge(self, sorted_left_nums: [int], sorted_right_nums: [int]) -> [int]:
sorted_nums = []
left_i, right_i = 0, 0
# 将两个有序子数组中较小元素依次插入到结果数组中
while left_i < len(sorted_left_nums) and right_i < len(sorted_right_nums):
if sorted_left_nums[left_i] < sorted_right_nums[right_i]:
sorted_nums.append(sorted_left_nums[left_i])
left_i += 1
else:
sorted_nums.append(sorted_right_nums[right_i])
right_i += 1
# 如果左子数组有剩余元素,则将其插入到结果数组中
while left_i < len(sorted_left_nums):
sorted_nums.append(sorted_left_nums[left_i])
left_i += 1
# 如果右子数组有剩余元素,则将其插入到结果数组中
while right_i < len(sorted_right_nums):
sorted_nums.append(sorted_right_nums[right_i])
right_i += 1
return sorted_nums
# 归并排序
def mergeSort(self, nums: [int]) -> [int]:
# 子数组长度为1时终止递归
if len(nums) <= 1:
return nums
mid = len(nums) // 2 # 数组中间位置
# 分解为左子数组和右子数组
left_nums = nums[:mid]
right_nums = nums[mid:]
# 递归排序左子数组和右子数组
sorted_left_nums = self.mergeSort(left_nums)
sorted_right_nums = self.mergeSort(right_nums)
# 排序+合并
return self.merge(sorted_left_nums, sorted_right_nums)
递归过程:
- 时间复杂度:
O
(
n
∗
l
o
g
n
)
O(n*log n)
O(n∗logn),外层循环
log
n
\log n
logn量级,子算法
merge
为 n n n量级。 - 空间复杂度: O ( n ) O(n) O(n),归并排序方法需要用到与参加排序的数组同样大小的辅助空间。
- **排序稳定性:**因为在两个有序子数组的归并过程中,如果两个有序数组中出现相等元素,
merge
算法能够使前一个数组中那个相等元素先被复制,从而确保这两个元素的相对顺序不发生改变。因此,归并排序算法是一种 稳定排序算法。
88. 合并两个有序数组 - 力扣(LeetCode)
题目:
给你两个按 非递减顺序 排列的整数数组
nums1
和nums2
,另有两个整数m
和n
,分别表示nums1
和nums2
中的元素数目。请你 合并
nums2
到nums1
中,使合并后的数组同样按 非递减顺序 排列。**注意:**最终,合并后数组不应由函数返回,而是存储在数组
nums1
中。为了应对这种情况,nums1
的初始长度为m + n
,其中前m
个元素表示应合并的元素,后n
个元素为0
,应忽略。nums2
的长度为n
。示例:
输入:nums1 = [1,2,3,0,0,0], m = 3, nums2 = [2,5,6], n = 3 输出:[1,2,2,3,5,6]
输入:nums1 = [1], m = 1, nums2 = [], n = 0 输出:[1]
输入:nums1 = [0], m = 0, nums2 = [1], n = 1 输出:[1]
说明:
nums1.length == m + n
nums2.length == n
0 <= m, n <= 200
1 <= m + n <= 200
-10^9 <= nums1[i], nums2[j] <= 10^9
**标签:**数组、双指针、排序
**难度:**简单
方法一:逆向快慢指针
**思路:**类似归并排序中递归的步骤,逆向遍历,取大者放到后面。最后,如果nums1剩余元素(j=0),则无需改动;如果nums2剩余元素(i=0),则将nums2剩余元素赋值到nums1开头相应位置。
时间复杂度: O ( m + n ) O(m+n) O(m+n)
空间复杂度: O ( 1 ) O(1) O(1)
class Solution:
def merge(self, nums1: List[int], m: int, nums2: List[int], n: int) -> None:
i = m - 1
j = n - 1
k = m + n - 1
while i >= 0 and j >= 0:
if nums1[i] < nums2[j]:
nums1[k] = nums2[j]
j -= 1
else:
nums1[k] = nums1[i]
i -= 1
k -= 1
nums1[:j+1] = nums2[:j+1]
LCR 170. 交易逆序对的总数 - 力扣(LeetCode)
题目:
在股票交易中,如果前一天的股价高于后一天的股价,则可以认为存在一个「交易逆序对」。请设计一个程序,输入一段时间内的股票交易记录
record
,返回其中存在的「交易逆序对」总数。
- 逆序对:在数组中的两个数字,如果前面一个数字大于后面的数字,则这两个数字组成一个逆序对。
示例:
输入:record = [7, 5, 6, 4] 输出:4 解释:逆序对有 (7, 5), (7, 6), (7, 4), (5, 4), (6, 4)。
说明:
- 0 <= record.length <= 50000
**标签:**树状数组、线段树、数组、二分查找、分治、有序集合、归并排序
**难度:**困难
方法一:归并排序合并过程中统计
**思路:**在归并排序合并过程中统计逆序对数量(相比归并排序代码要改成严格大于),合并中一旦左子数组有元素left_i大于右子数组中的某个元素right_i,那么该元素右边的所有元素都大于右子数组中的这个元素right_i
时间复杂度: O ( n ∗ log n ) O(n*\log n) O(n∗logn)
空间复杂度: O ( n ) O(n) O(n)
class Solution:
# 排序+合并
def merge(self, sorted_left_nums: [int], sorted_right_nums: [int]) -> [int]:
sorted_nums = []
left_i, right_i = 0, 0
# 将两个有序子数组中较小元素依次插入到结果数组中
while left_i < len(sorted_left_nums) and right_i < len(sorted_right_nums):
if sorted_left_nums[left_i] <= sorted_right_nums[right_i]:
sorted_nums.append(sorted_left_nums[left_i])
left_i += 1
else:
sorted_nums.append(sorted_right_nums[right_i])
# 一旦左子数组有元素left_i大于右子数组中的某个元素right_i,那么该元素右边的所有元素都大于右子数组中的这个元素right_i
self.ans += (len(sorted_left_nums) - left_i)
right_i += 1
# 如果左子数组有剩余元素,则将其插入到结果数组中
while left_i < len(sorted_left_nums):
sorted_nums.append(sorted_left_nums[left_i])
left_i += 1
# 如果右子数组有剩余元素,则将其插入到结果数组中
while right_i < len(sorted_right_nums):
sorted_nums.append(sorted_right_nums[right_i])
right_i += 1
return sorted_nums
# 归并排序
def mergeSort(self, nums: [int]) -> [int]:
# 子数组长度为1时终止递归
if len(nums) <= 1:
return nums
mid = len(nums) // 2 # 数组中间位置
# 分解为左子数组和右子数组
left_nums = nums[:mid]
right_nums = nums[mid:]
# 递归排序左子数组和右子数组
sorted_left_nums = self.mergeSort(left_nums)
sorted_right_nums = self.mergeSort(right_nums)
# 排序+合并
return self.merge(sorted_left_nums, sorted_right_nums)
# 统计逆序对
def reversePairs(self, record: List[int]) -> int:
self.ans = 0
self.mergeSort(record)
return self.ans
315. 计算右侧小于当前元素的个数 - 力扣(LeetCode)
题目:
给你一个整数数组
nums
,按要求返回一个新数组counts
。数组counts
有该性质:counts[i]
的值是nums[i]
右侧小于nums[i]
的元素的数量。示例:
输入:nums = [5,2,6,1] 输出:[2,1,1,0] 解释: 5 的右侧有 2 个更小的元素 (2 和 1) 2 的右侧仅有 1 个更小的元素 (1) 6 的右侧有 1 个更小的元素 (1) 1 的右侧有 0 个更小的元素
输入:nums = [-1] 输出:[0]
输入:nums = [-1,-1] 输出:[0,0]
说明:
1 <= nums.length <= 10^5
-10^4 <= nums[i] <= 10^4
**标签:**树状数组、线段树、数组、二分查找、分治、有序集合、归并排序
**难度:**困难
方法一:归并排序合并过程中统计
思路:在归并排序合并过程中统计每个位置的逆序对数量(相比归并排序代码要改成严格大于)。将元素值、对应下标和右侧小于 nums[i] 的元素的数量存在数组中。合并中一旦左子数组有元素left_i小于等于右子数组中的某个元素right_i,说明在右子数组中有right_i个元素比该元素left_i小;同时如果合并完后左子数组有剩余,则说明右子数组中所有元素(同样也是right_i个元素)比这些剩余的元素小。(右侧小于 nums[i] 的元素的数量也在随着数组不断递归)
时间复杂度: O ( n ∗ log n ) O(n*\log n) O(n∗logn)
空间复杂度: O ( n ) O(n) O(n)
class Solution:
# 排序+合并
def merge(self, sorted_left_nums: [int], sorted_right_nums: [int]) -> [int]:
sorted_nums = []
left_i, right_i = 0, 0
# 将两个有序子数组中较小元素依次插入到结果数组中
while left_i < len(sorted_left_nums) and right_i < len(sorted_right_nums):
if sorted_left_nums[left_i] <= sorted_right_nums[right_i]:
# sorted_left_nums[left_i] 右侧有 right_i 个比 sorted_left_nums[left_i] 小的
sorted_left_nums[left_i][2] += right_i
sorted_nums.append(sorted_left_nums[left_i])
left_i += 1
else:
sorted_nums.append(sorted_right_nums[right_i])
right_i += 1
# 如果左子数组有剩余元素,则将其插入到结果数组中
while left_i < len(sorted_left_nums):
# sorted_left_nums[left_i] 右侧有 right_i 个比 sorted_left_nums[left_i] 小的
sorted_left_nums[left_i][2] += right_i
sorted_nums.append(sorted_left_nums[left_i])
left_i += 1
# 如果右子数组有剩余元素,则将其插入到结果数组中
while right_i < len(sorted_right_nums):
sorted_nums.append(sorted_right_nums[right_i])
right_i += 1
return sorted_nums
# 归并排序
def mergeSort(self, nums: [int]) -> [int]:
# 子数组长度为1时终止递归
if len(nums) <= 1:
return nums
mid = len(nums) // 2 # 数组中间位置
# 分解为左子数组和右子数组
left_nums = nums[:mid]
right_nums = nums[mid:]
# 递归排序左子数组和右子数组
sorted_left_nums = self.mergeSort(left_nums)
sorted_right_nums = self.mergeSort(right_nums)
# 排序+合并
return self.merge(sorted_left_nums, sorted_right_nums)
# 统计每个位置的逆序对数量
def countSmaller(self, nums: List[int]) -> List[int]:
n = len(nums)
# 将元素值、对应下标、右侧小于 nums[i] 的元素的数量存入数组中
nums = [[num, i, 0] for i, num in enumerate(nums)]
nums = self.mergeSort(nums)
ans = [0 for _ in range(n)]
for num in nums:
ans[num[1]] = num[2]
return ans
169. 多数元素 - 力扣(LeetCode)
题目:
给定一个大小为
n
的数组nums
,返回其中的多数元素。多数元素是指在数组中出现次数 大于⌊ n/2 ⌋
的元素。你可以假设数组是非空的,并且给定的数组总是存在多数元素。
示例:
输入:nums = [3,2,3] 输出:3
输入:nums = [2,2,1,1,1,2,2] 输出:2
说明:
n == nums.length
1 <= n <= 5 * 10^4
-10^9 <= nums[i] <= 10^9
**标签:**数组、哈希表、分治、计数、排序
**难度:**简单
方法一:分治
**思路:**如果 num 是数组 nums 的多数元素,那么我们将 nums 分为两部分,则 num 至少是其中一部分的多数元素。
-
将数组 nums 递归地将当前序列平均分成左右两个数组,直到所有子数组长度为 1。
-
长度为 1 的子数组多数元素肯定是数组中唯一的数,将其返回即可。
-
将两个子数组依次向上两两合并。
- 如果两个子数组的多数元素相同,则说明合并后的数组多数元素为:两个子数组的众数。
- 如果两个子数组的多数元素不同,则需要比较两个多数元素在整个区间哪个才是多数元素。
-
最后返回整个数组的多数元素。
时间复杂度: O ( n ∗ log n ) O(n*\log n) O(n∗logn)
空间复杂度: O ( log n ) O(\log n) O(logn)
class Solution:
def majorityElement(self, nums: List[int]) -> int:
def get_mode(low, high):
if low == high:
return nums[low]
mid = low + (high - low) // 2
left_mod = get_mode(low, mid)
right_mod = get_mode(mid + 1, high)
if left_mod == right_mod:
return left_mod
left_mod_cnt, right_mod_cnt = 0, 0
for i in range(low, high + 1):
if nums[i] == left_mod:
left_mod_cnt += 1
if nums[i] == right_mod:
right_mod_cnt += 1
if left_mod_cnt > right_mod_cnt:
return left_mod
return right_mod
return get_mode(0, len(nums) - 1)
方法二:摩尔投票法
**思路:**多数元素出现次数大于一半,票数正负抵消后,剩余的元素就是多数元素。
- 若记多数元素的票数为+1,非多数元素的票数为-1,则一定有所有数字的票数和>0
- 若数组的前a个数字的票数和=0,则数组剩余(n−a)个数字的票数和一定仍>0,即后(n−a)个数字的多数元素不变。
初始化票数votes为0,将多数元素x设为第一个元素,遍历数组:
- 当votes等于0,则将多数元素x更新为当前数字num。
- 当num=x时,票数votes+1;当num!=x时,票数votes-1
时间复杂度: O ( n ) O(n) O(n)
空间复杂度: O ( 1 ) O(1) O(1)
class Solution:
def majorityElement(self, nums: List[int]) -> int:
votes = 0
for num in nums:
if votes == 0:
x = num
if num == x:
votes += 1
else:
votes -= 1
return x
1.2.6 快速排序
快速排序(Quick Sort):采用经典的分治策略,选择数组中某个元素作为基准数,通过一趟排序将数组分为独立的两个子数组,一个子数组中所有元素值都比基准数小,另一个子数组中所有元素值都比基准数大。然后再按照同样的方式递归的对两个子数组分别进行快速排序,以达到整个数组有序。
-
哨兵划分:选取一个基准数,将数组中比基准数大的元素移动到基准数右侧,比他小的元素移动到基准数左侧。
-
从数组中找到一个基准数 p i v o t pivot pivot(以数组第1个元素作为基准数时 p i v o t = n u m s pivot=nums pivot=nums)
-
使用指针 iii 指向数组开始位置,指针 jjj 指向数组末尾位置。
-
从数组中找到一个基准数 p i v o t pivot pivot(默认为第一个元素 n u m s [ l o w ] nums[low] nums[low]),交换到数组开始的位置。
-
指针 i i i指向数组开始的位置,指针 j j j指向数组末尾的位置。
- 从右向左移动指针 j j j,找到第一个小于基准数的元素位置。
- 从左向右移动指针 i i i,找到第一个大于基准数的元素位置。
- 交换指针 i i i和指针 j j j所指位置的两个元素。
-
重复上述的三个步骤,直到指针 i i i和指针 j j j相遇时停止,最后将基准数与指针 i i i所指位置的元素交换,将基准数放到两个子数组交界的位置。
-
-
递归分解:完成哨兵划分之后,对划分好的左右子数组分别进行递归排序。
- 按照基准数的位置将数组拆分为左右两个子数组。
- 对每个子数组分别重复「哨兵划分」和「递归分解」,直到各个子数组只有1个元素,排序结束。
# 912. 排序数组
# 题目:给你一个整数数组 nums,请你将该数组升序排列。
import random
class Solution:
# 哨兵划分
def partition(self, nums: [int], low: int, high: int) -> int:
pivot = nums[low] # 以第1个元素为基准数
i, j = low, high # i,j指向数组开始和末尾的位置
while i < j:
# 从右向左找到第一个小于基准数的元素位置
while i < j and nums[j] >= pivot:
j -= 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 randomPartition(self, nums: [int], low: int, high: int) -> int:
# 从nums[low: high + 1]中随机挑选作为基准数
i = random.randint(low, high)
# 交换基准数与第1个元素
nums[i], nums[low] = nums[low], nums[i]
# 哨兵划分
return self.partition(nums, low, high)
# 快速排序
def quickSort(self, nums: [int], low: int, high: int) -> [int]:
if low < high:
# 哨兵划分
# pivot_i = self.partition(nums, 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
- 时间复杂度: O ( n ∗ log n ) O(n*\log n) O(n∗logn)。
- 空间复杂度: O ( n ) O(n) O(n),无论快速排序算法递归与否,排序过程中都需要用到堆栈或其他结构的辅助空间来存放当前待排序数组的首、尾位置。最坏的情况下,空间复杂度为 O ( n ) O(n) O(n)。
- **排序稳定性:**在进行哨兵划分时,基准数可能会被交换至相等元素的右侧。因此,快速排序是一种 不稳定排序算法。
75. 颜色分类 - 力扣(LeetCode)
题目:
给定一个包含红色、白色和蓝色、共
n
个元素的数组nums
,**原地**对它们进行排序,使得相同颜色的元素相邻,并按照红色、白色、蓝色顺序排列。我们使用整数
0
、1
和2
分别表示红色、白色和蓝色。必须在不使用库内置的 sort 函数的情况下解决这个问题。
示例:
输入:nums = [2,0,2,1,1,0] 输出:[0,0,1,1,2,2]
输入:nums = [2,0,1] 输出:[0,1,2]
说明:
n == nums.length
1 <= n <= 300
nums[i]
为0
、1
或2
**标签:**数组、双指针、排序
**难度:**中等
方法一:三路哨兵划分
**思路:**参考快速排序中的哨兵划分,一次循环将数组分为红色(小于1)、白色(等于1)、蓝色(大于1)三部分。但哨兵划分中间部分只有一个数,即1还是会分布在左右部分里,所以要增加一个指针将1放到正确位置
时间复杂度: O ( n ) O(n) O(n)
空间复杂度: O ( 1 ) O(1) O(1)
class Solution:
def sortColors(self, nums: List[int]) -> None:
# [0, red)为红色
# [red, white)为白色
# [blue, len(nums) -1]为蓝色(white==blue)
red, white, blue = 0, 0, len(nums) - 1
while white <= blue:
if nums[white] == 0:
nums[red], nums[white] = nums[white], nums[red]
white += 1 # 这里交换得到的一定是白,因为red只有在红时才会动
red += 1
elif nums[white] == 1:
white += 1
elif nums[white] == 2:
nums[white], nums[blue] = nums[blue], nums[white]
blue -= 1 # 这里交换得到的未知,所以只调整blue
1.2.7 堆排序
堆排序(Heap sort):借用「堆结构」所设计的排序算法。将数组转化为大顶堆,重复从大顶堆中取出数值最大的节点,并让剩余的堆结构继续维持大顶堆性质。
堆(Heap):一种满足以下两个条件之一的完全二叉树:
- 大顶堆(Max Heap):任意节点值 ≥ 其子节点值。
- 小顶堆(Min Heap):任意节点值 ≤ 其子节点值。
- 如果某二叉树节点(非叶子节点,即最后一行)的下标为 i i i,那么其左子节点下标为 2 i + 1 2i+1 2i+1,右子节点下标为 2 i + 2 2i+2 2i+2。
- 如果某二叉树节点(非根结点,即第一个)的下标为 i i i,那么其父节点下标为 ( i − 1 ) / 2 (i-1)/2 (i−1)/2(向下取整)。
下移调整:从堆中移除位于堆顶的元素后,重新调整堆结果,以保持堆的特性不变。
- 从新的堆顶元素开始,将其与其较大的子节点进行比较。
- 如果当前节点的值小于其较大的子节点,则将它们交换。这一步是为了将新的堆顶元素「下沉」到适当的位置,以保持最大堆的特性。
- 如果当前节点的值大于等于其较大的子节点,说明已满足最大堆的特性,此时结束。
- 重复上述比较和交换步骤,直到新的堆顶元素不再小于其子节点,或者达到了堆的底部。
初始化大顶堆:从最后一个非叶子节点开始,对每个节点开始进行下移调整。
# 912. 排序数组
# 题目:给你一个整数数组 nums,请你将该数组升序排列。
class Solution:
# 下移调整
def heapify(self, arr, index, end):
left = index * 2 + 1 # index的左子节点
right = left + 1 # index的右子节点
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):
size = len(arr)
# 从最后一个非叶子节点开始,对每个节点开始进行下移调整(多次)
# 如果从根节点开始的话,如果父节点无需调整,那么子节点会跳过
for i in range((size - 2) // 2, -1, -1):
self.heapify(arr, i, size - 1)
return arr
# 堆排序(升序)
def maxHeapSort(self, arr):
# 初始化大顶堆
self.buildMaxHeap(arr)
size = len(arr)
# 第i次交换+调整
for i in range(size):
# 将堆顶元素与第size-i-1个元素交换,将第i+1大的元素放到正确位置
arr[0], arr[size - i - 1] = arr[size - i - 1], arr[0]
# 对未排序区间[0,size-i-2],对根节点开始进行下移调整(一次)
self.heapify(arr, 0, size - i - 2)
return arr
- 时间复杂度: O ( n ∗ log n ) O(n*\log n) O(n∗logn),「初始化大顶堆」时间花费为 O ( n ) O(n) O(n)和「下移调整」时间花费为 O ( n ∗ log n ) O(n*\log n) O(n∗logn),两者独立。
- 空间复杂度: O ( 1 ) O(1) O(1),由于在堆排序中只需要一个记录大小的辅助空间。
- **排序稳定性:**在进行「下移调整」时,相等元素的相对位置可能会发生变化。因此,堆排序是一种 不稳定排序算法。
215. 数组中的第K个最大元素 - 力扣(LeetCode)
题目:
给定整数数组
nums
和整数k
,请返回数组中第**k**
个最大的元素。请注意,你需要找的是数组排序后的第
k
个最大的元素,而不是第k
个不同的元素。你必须设计并实现时间复杂度为
O(n)
的算法解决此问题。示例:
输入: [3,2,1,5,6,4], k = 2 输出: 5
输入: [3,2,3,1,2,4,5,5,6], k = 4 输出: 4
说明:
1 <= k <= nums.length <= 10^5
-10^4 <= nums[i] <= 10^4
**标签:**数组、分治、快速选择、排序、堆(优先队列)
**难度:**中等
方法一:堆排序
**思路:**堆排序,直到将第k大的元素为止
时间复杂度: O ( n ∗ log k ) O(n*\log k) O(n∗logk)
空间复杂度: O ( 1 ) O(1) O(1)
class Solution:
# 下移调整
def heapify(self, arr, index, end):
left = index * 2 + 1 # index的左子节点
right = left + 1 # index的右子节点
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, nums):
size = len(nums)
# 从最后一个非叶子节点开始,对每个节点开始进行下移调整(多次)
# 如果从根节点开始的话,如果父节点无需调整,那么子节点会跳过
for i in range((size - 2) // 2, -1, -1):
self.heapify(nums, i, size - 1)
return nums
def findKthLargest(self, nums: List[int], k: int) -> int:
# 初始化大顶堆
self.buildMaxHeap(nums)
size = len(nums)
# 第i次交换+调整
for i in range(k):
# 将堆顶元素与第size-i-1个元素交换,将第i+1大的元素放到正确位置
nums[0], nums[size - i - 1] = nums[size - i - 1], nums[0]
# 对未排序区间[0,size-i-2],对根节点开始进行下移调整(一次)
self.heapify(nums, 0, size - i - 2)
return nums[size - i - 1]
方法二:快速排序
**思路:**快速排序,如果某次基准数的索引为n-k,则其为第k大的元素
时间复杂度: O ( n ∗ log n ) O(n*\log n) O(n∗logn)
空间复杂度: O ( n ) O(n) O(n)
import random
class Solution:
# 哨兵划分
def partition(self, nums: [int], low: int, high: int) -> int:
pivot = nums[low] # 以第1个元素为基准数
i, j = low, high # i,j指向数组开始和末尾的位置
while i < j:
# 从右向左找到第一个小于基准数的元素位置
while i < j and nums[j] >= pivot:
j -= 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 randomPartition(self, nums: [int], low: int, high: int) -> int:
# 从nums[low: high + 1]中随机挑选作为基准数
i = random.randint(low, high)
# 交换基准数与第1个元素
nums[i], nums[low] = nums[low], nums[i]
# 哨兵划分
return self.partition(nums, low, high)
# 快速排序
def quickSort(self, nums: [int], low: int, high: int, k: int) -> [int]:
size = len(nums)
if low < high:
# 哨兵划分
# pivot_i = self.partition(nums, low, high)
# 随机哨兵划分
pivot_i = self.randomPartition(nums, low, high)
# 对左右两个子数组分别进行递归快速排序
if pivot_i == size - k:
return nums[size - k]
if pivot_i > size -k:
self.quickSort(nums, low, pivot_i - 1, k)
if pivot_i < size - k:
self.quickSort(nums, pivot_i + 1, high, k)
return nums[size - k]
def findKthLargest(self, nums: List[int], k: int) -> int:
return self.quickSort(nums, 0, len(nums) - 1, k)
LCR 159. 库存管理 III - 力扣(LeetCode)
题目:
仓库管理员以数组
stock
形式记录商品库存表,其中stock[i]
表示对应商品库存余量。请返回库存余量最少的cnt
个商品余量,返回 顺序不限。示例:
输入:stock = [2,5,7,4], cnt = 1 输出:[2]
输入:stock = [0,2,3,6], cnt = 2 输出:[0,2] 或 [2,0]
说明:
0 <= cnt <= stock.length <= 10000
0 <= stock[i] <= 10000
**标签:**数组、分治、快速选择、排序、堆(优先队列)
**难度:**简单
方法一:堆排序
**思路:**堆排序,直到将第cnt小的元素为止
时间复杂度: O ( n ∗ log k ) O(n*\log k) O(n∗logk)
空间复杂度: O ( 1 ) O(1) O(1)
class Solution:
def heapify(self, 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 max_index == index:
break
nums[max_index], nums[index] = nums[index], nums[max_index]
index = max_index
left = index * 2 + 1
right = left + 1
def buildMaxHeap(self, nums):
size = len(nums)
for i in range((size - 2) // 2, -1, -1):
self.heapify(nums, i, size - 1)
def inventoryManagement(self, stock: List[int], cnt: int) -> List[int]:
self.buildMaxHeap(stock)
size = len(stock)
for i in range(size - cnt):
stock[0], stock[size - i - 1] = stock[size - i - 1], stock[0]
self.heapify(stock, 0, size - i - 2)
return stock[:cnt]
方法二:快速排序
**思路:**快速排序,如果某次基准数的索引为cnt,则其左边为最小的cnt个数
时间复杂度: O ( n ∗ log n ) O(n*\log n) O(n∗logn)
空间复杂度: O ( n ) O(n) O(n)
import random
class Solution:
def partition(self, nums: [int], low: int, high: int) -> int:
pivot = nums[low]
i, j = low, high
while i < j:
while i < j and nums[j] >= pivot:
j -= 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 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)
def quickSort(self, nums: [int], low: int, high: int, cnt: int) -> [int]:
if low < high:
pivot_i = self.randomPartition(nums, low, high)
if pivot_i == cnt:
return nums[:cnt]
if pivot_i > cnt:
self.quickSort(nums, low, pivot_i - 1, cnt)
if pivot_i < cnt:
self.quickSort(nums, pivot_i + 1, high, cnt)
return nums[:cnt]
def inventoryManagement(self, stock: List[int], cnt: int) -> List[int]:
return self.quickSort(stock, 0, len(stock) - 1, cnt)
1.2.8 计数排序
计数排序(Counting Sort):通过统计数组中每个元素在数组中出现的次数,根据这些统计信息将数组元素有序的放置到正确位置,从而达到排序的目的。
# 912. 排序数组
# 题目:给你一个整数数组 nums,请你将该数组升序排列。
class Solution:
def countingSort(self, nums: [int]) -> [int]:
# 找到最大值元素nums_max和最小值元素nums_min
nums_min, nums_max = min(nums), max(nums)
# 定义计数数组counts,大小为nums_max-nums_min+1
size = nums_max - nums_min + 1
counts = [0 for _ in range(size)]
# 计数统计
for num in nums:
counts[num - nums_min] += 1
# 生成累积计数数组counts
for i in range(1, size):
counts[i] += counts[i - 1]
# 逆序填充目标数组res
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
# 将count的累计计数减1,从而得到下个值为num的元素的位置
counts[num - nums_min] -= 1
return res
- 时间复杂度: O ( n + k ) O(n+k) O(n+k),其中 k k k代表待排序数组的值域。
- 空间复杂度: O ( k ) O(k) O(k),其中 k k k代表待排序数组的值域。
- **适用情况:**计数排序一般用于整数排序,不适用于按字母顺序、人名顺序排序。
- **排序稳定性:**由于向结果数组中填充元素时使用的是逆序遍历,可以避免改变相等元素之间的相对顺序。因此,计数排序是一种 稳定排序算法。
1122. 数组的相对排序 - 力扣(LeetCode)
题目:
给你两个数组,
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]
说明:
1 <= arr1.length, arr2.length <= 1000
0 <= arr1[i], arr2[i] <= 1000
arr2
中的元素arr2[i]
各不相同arr2
中的每个元素arr2[i]
都出现在arr1
中**标签:**数组、哈希表、计数排序、排序
**难度:**简单
方法一:计数排序
**思路:**计数排序,先排arr2中的元素
时间复杂度: O ( n + m ) O(n+m) O(n+m)
空间复杂度: O ( n ) O(n) O(n)
class Solution:
def relativeSortArray(self, arr1: List[int], arr2: List[int]) -> List[int]:
arr1_min, arr1_max = min(arr1), max(arr1)
size = arr1_max - arr1_min + 1
count = [0 for _ in range(size)]
for num1 in arr1:
count[num1 - arr1_min] += 1
res = []
for num2 in arr2:
num2_count = count[num2 - arr1_min]
res += [num2 for _ in range(num2_count)]
count[num2 - arr1_min] = 0
for i in range(size):
if count[i] > 0:
res += [i + arr1_min for _ in range(count[i])]
return res
1.2.9 桶排序
桶排序(Bucket Sort):将待排序数组中的元素分散到若干个「桶」中,然后对每个桶中的元素再进行单独排序(使用插入排序、归并排序、快排排序等算法)。
# 912. 排序数组
# 题目:给你一个整数数组 nums,请你将该数组升序排列。
class Solution:
def insertionSort(self, nums: [int]) -> [int]:
n = len(nums)
for i in range(1, n):
base = nums[i]
j = i
while j > 0 and nums[j - 1] > base:
nums[j] = nums[j - 1]
j -= 1
nums[j] = base
return nums
def bucketSort(self, nums: [int], bucket_size=5) -> [int]:
# 找到最大值元素nums_max和最小值元素nums_min
nums_min, nums_max = min(nums), max(nums)
# 定义桶数组buckets,桶的个数为 (nums_max-nums_max)//桶大小+1
bucket_count = (nums_max - nums_min) // bucket_size + 1
buckets = [[] for _ in range(bucket_count)]
# 遍历待排序数组元素,将每个元素根据大小分配到对应的桶中
for num in nums:
buckets[(num - nums_min) // bucket_size].append(num)
res = []
for bucket in buckets:
# 对每个非空桶内的元素单独排序
self.insertionSort(bucket)
# 按照区间顺序依次合并到res数组中
res.extend(bucket)
return res
- 时间复杂度: O ( n ) O(n) O(n),每个桶里的数据有 n m \frac{n}{m} mn个,桶内时间复杂度为 O ( n m × log n m ) O(\frac{n}{m}\times\log\frac{n}{m}) O(mn×logmn), m m m个桶的时间复杂度为 O ( n × log n m ) O(n\times\log\frac{n}{m}) O(n×logmn),当 m m m接近于 n n n时, log n m \log\frac{n}{m} logmn是一个较小的常数。
- 空间复杂度: O ( n + m ) O(n+m) O(n+m),其中 m m m为桶的个数。
- **排序稳定性:**桶排序的稳定性取决于桶内使用的排序算法。如果桶内使用稳定的排序算法(比如插入排序算法),并且在合并桶的过程中保持相等元素的相对顺序不变,则桶排序是一种 稳定排序算法。反之,则桶排序是一种 不稳定排序算法。
220. 存在重复元素 III - 力扣(LeetCode)
题目:
给你一个整数数组
nums
和两个整数indexDiff
和valueDiff
。找出满足下述条件的下标对
(i, j)
:
i != j
,abs(i - j) <= indexDiff
abs(nums[i] - nums[j]) <= valueDiff
如果存在,返回
true
*;*否则,返回false
。示例:
输入:nums = [1,2,3,1], indexDiff = 3, valueDiff = 0 输出:true 解释:可以找出 (i, j) = (0, 3) 。 满足下述 3 个条件: i != j --> 0 != 3 abs(i - j) <= indexDiff --> abs(0 - 3) <= 3 abs(nums[i] - nums[j]) <= valueDiff --> abs(1 - 1) <= 0
输入:nums = [1,5,9,1,5,9], indexDiff = 2, valueDiff = 3 输出:false 解释:尝试所有可能的下标对 (i, j) ,均无法满足这 3 个条件,因此返回 false 。
说明:
2 <= nums.length <= 10^5
-10^9 <= nums[i] <= 10^9
1 <= indexDiff <= nums.length
0 <= valueDiff <= 10^9
**标签:**数组、桶排序、有序集合、排序、滑动窗口
**难度:**困难
方法一:桶排序
**思路:**利用桶排序的思路,将桶大小设为valueDiff + 1,这样桶内的元素一定满足valueDiff条件。因此只要在把元素放进相应的桶时桶内已经有元素,则一定满足valueDiff条件,此外还要检查左右两侧桶里的元素(最多只会有一个元素)是否满足valueDiff条件。
遍历结束后每次只保留自身和前indexDiff-1个元素,以满足indexDiff条件
时间复杂度: O ( n ) O(n) O(n)
空间复杂度: O ( n ) O(n) O(n)
class Solution:
def containsNearbyAlmostDuplicate(self, nums: List[int], indexDiff: int, valueDiff: int) -> bool:
nums_min, nums_max = min(nums), max(nums)
# 将桶大小设为valueDiff + 1,这样桶内的元素一定满足valueDiff条件
bucket_size = valueDiff + 1
bucket_count = (nums_max - nums_min) // bucket_size + 1
buckets = [[] for _ in range(bucket_count)]
n = len(nums)
for i in range(n):
bucket_i = (nums[i] - nums_min) // bucket_size
if buckets[bucket_i] != []:
return True
buckets[bucket_i].append(nums[i])
# 如果左侧桶存在,且不为空,判断左侧桶是否满足条件
if bucket_i != 0:
if buckets[bucket_i - 1] != [] and abs(buckets[bucket_i - 1][0] - buckets[bucket_i][0]) <= valueDiff:
return True
# 如果右侧桶存在,且不为空,判断右侧桶是否满足条件
if bucket_i != bucket_count - 1:
if buckets[bucket_i + 1] != [] and abs(buckets[bucket_i + 1][0] - buckets[bucket_i][0]) <= valueDiff:
return True
# 只保留自身和前indexDiff-1个元素,以满足indexDiff条件
if i >= indexDiff:
bucket_i_indexDiff = (nums[i-indexDiff] - nums_min) // bucket_size
buckets[bucket_i_indexDiff] = []
return False
1.2.10 基数排序
基数排序(Radix Sort):将整数按位数切割成不同的数字,然后从低位开始,依次到高位,逐位进行排序,从而达到排序的目的。
# 912. 排序数组
# 题目:给你一个整数数组 nums,请你将该数组升序排列。
class Solution:
def radixSort(self, nums: [int]) -> [int]:
# 获取数组最大元素,获得最大位数
size = len(str(max(nums)))
# 从最低位(个位)开始,逐位遍历每一位
for i in range(size):
# 定义长度为10的桶数组buckets,每个桶分别代表0~9
buckets = [[] for _ in range(10)]
# 按照每个元素当前位上的数字,将元素放入对应数字的桶中
for num in nums:
buckets[num // (10 ** i) % 10].append(num)
# 清空原始数组
nums.clear()
# 按照桶的顺序依次取出对应元素,重新加入到原始数组中
for bucket in buckets:
for num in bucket:
nums.append(num)
return nums
- 时间复杂度: O ( n ∗ k ) O(n*k) O(n∗k)。其中 k k k是数字位数,取决于数字位的选择(十进制位、二进制位)和待排序元素所属数据类型全集的大小。
- 空间复杂度: O ( n + k ) O(n+k) O(n+k)。
- **排序稳定性:**基数排序采用的桶排序是稳定的。基数排序是一种 稳定排序算法。
164. 最大间距 - 力扣(LeetCode)
题目:
给定一个无序的数组
nums
,返回 数组在排序之后,相邻元素之间最大的差值 。如果数组元素个数小于 2,则返回0
。您必须编写一个在「线性时间」内运行并使用「线性额外空间」的算法。
示例:
输入: nums = [3,6,9,1] 输出: 3 解释: 排序后的数组是 [1,3,6,9], 其中相邻元素 (3,6) 和 (6,9) 之间都存在最大差值 3。
输入: nums = [10] 输出: 0 解释: 数组元素个数小于 2,因此返回 0。
说明:
1 <= nums.length <= 10^5
0 <= nums[i] <= 10^9
**标签:**数组、桶排序、基数排序、排序
**难度:**中等
方法一:基数排序
**思路:**基数排序保证时间复杂度和空间复杂度都为线性
时间复杂度: O ( n ) O(n) O(n)
空间复杂度: O ( n ) O(n) O(n)
class Solution:
def radixSort(self, nums: [int]) -> [int]:
# 获取数组最大元素,获得最大位数
size = len(str(max(nums)))
# 从最低位(个位)开始,逐位遍历每一位
for i in range(size):
# 定义长度为10的桶数组buckets,每个桶分别代表0~9
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 maximumGap(self, nums: List[int]) -> int:
nums = self.radixSort(nums)
max_diff = 0
for i in range(1, len(nums)):
max_diff = max(max_diff, nums[i] - nums[i-1])
return max_diff
方法二:桶排序
思路:确定合适的桶大小=(最大值-最小值)//(元素数量-1),确保桶内元素的间距不会是最大间距,逐步更新桶内最小值-前一个桶内最大值,考虑到有空桶,前一个桶内最大值需要初始化为nums_min,且需要保留直到被覆盖
时间复杂度: O ( n ) O(n) O(n)
空间复杂度: O ( n ) O(n) O(n)
class Solution:
def maximumGap(self, nums: List[int]) -> int:
n = len(nums)
if n < 2:
return 0
nums_min, nums_max = min(nums), max(nums)
# 桶大小为(最大值-最小值)//(元素数量-1)
bucket_size = max(1, (nums_max - nums_min) // (n - 1))
# 桶数量
bucket_count = (nums_max - nums_min) // bucket_size + 1
buckets = [[] for _ in range(bucket_count)]
# 遍历待排序数组元素,将每个元素根据大小分配到对应的桶中
for num in nums:
buckets[(num - nums_min) // bucket_size].append(num)
max_diff = 0
prev_max = nums_min
for i in range(bucket_count):
if buckets[i] == []:
continue
else:
now_min = min(buckets[i])
max_diff = max(max_diff, now_min - prev_max)
prev_max = max(buckets[i])
return max_diff
1.2.11 其他排序问题
217. 存在重复元素 - 力扣(LeetCode)
题目:
给你一个整数数组
nums
。如果任一值在数组中出现 至少两次 ,返回true
;如果数组中每个元素互不相同,返回false
。示例:
输入:nums = [1,2,3,1] 输出:true
输入:nums = [1,2,3,4] 输出:false
输入:nums = [1,1,1,3,3,4,3,2,4,2] 输出:true
说明:
1 <= nums.length <= 10^5
-10^9 <= nums[i] <= 10^9
**标签:**数组、哈希表、排序
**难度:**简单
方法一:集合
**思路:**利用集合性质
时间复杂度: O ( n ) O(n) O(n)
空间复杂度: O ( n ) O(n) O(n)
class Solution:
def containsDuplicate(self, nums: List[int]) -> bool:
if len(set(nums)) == len(nums):
return False
else:
return True
136. 只出现一次的数字 - 力扣(LeetCode)
题目:
给你一个 非空 整数数组
nums
,除了某个元素只出现一次以外,其余每个元素均出现两次。找出那个只出现了一次的元素。你必须设计并实现线性时间复杂度的算法来解决此问题,且该算法只使用常量额外空间。
示例:
输入:nums = [2,2,1] 输出:1
输入:nums = [4,1,2,1,2] 输出:4
输入:nums = [1] 输出:1
说明:
1 <= nums.length <= 3 * 10^4
-3 * 10^4 <= nums[i] <= 3 * 10^4
- 除了某个元素只出现一次以外,其余每个元素均出现两次。
**标签:**位运算、数组
**难度:**简单
方法一:位运算
**思路:**位运算中的异或运算,异或运算 ⊕ ⊕ ⊕的三个性质:
- 任何数和0做异或运算,结果仍然是原来的数,即 a ⊕ 0 = a a⊕0=a a⊕0=a
- 数和其自身做异或运算,结果是0,即 a ⊕ a = 0 a⊕a=0 a⊕a=0
- 异或运算满足交换率和结合律: a ⊕ b ⊕ a = b ⊕ a ⊕ a = b ⊕ ( a ⊕ a ) = b ⊕ 0 = b a⊕b⊕a=b⊕a⊕a=b⊕(a⊕a)=b⊕0=b a⊕b⊕a=b⊕a⊕a=b⊕(a⊕a)=b⊕0=b
时间复杂度: O ( n ) O(n) O(n)
空间复杂度: O ( 1 ) O(1) O(1)
class Solution:
def singleNumber(self, nums: List[int]) -> int:
ans = 0
for i in range(len(nums)):
ans ^= nums[i]
return ans
56. 合并区间 - 力扣(LeetCode)
题目:
以数组
intervals
表示若干个区间的集合,其中单个区间为intervals[i] = [starti, endi]
。请你合并所有重叠的区间,并返回 一个不重叠的区间数组,该数组需恰好覆盖输入中的所有区间 。示例:
输入:intervals = [[1,3],[2,6],[8,10],[15,18]] 输出:[[1,6],[8,10],[15,18]] 解释:区间 [1,3] 和 [2,6] 重叠, 将它们合并为 [1,6].
输入:intervals = [[1,4],[4,5]] 输出:[[1,5]] 解释:区间 [1,4] 和 [4,5] 可被视为重叠区间。
说明:
1 <= intervals.length <= 10^4
intervals[i].length == 2
0 <= starti <= endi <= 10^4
**标签:**数组、排序
**难度:**中等
方法一:排序
**思路:**按左端点进行排序
如果第i个区间的右端点小于第i+1个区间的左端点,那两个区间不会重合;
如果第i个区间的右端点大于等于第i+1个区间的左端点,两个区间合并后左端点为第i个区间的左端点,右端点为第i个区间的右端点和第i+1个区间的右端点中较大的值
时间复杂度: O ( n ) O(n) O(n)
空间复杂度: O ( 1 ) O(1) O(1)
class Solution:
def merge(self, intervals: List[List[int]]) -> List[List[int]]:
# n = len(intervals)
# for i in range(1, n):
# base = intervals[i][0]
# base_list = intervals[i]
# j = i
# while j > 0 and intervals[j - 1][0] > base:
# intervals[j] = intervals[j - 1]
# j -= 1
# intervals[j] = base_list
intervals.sort(key=lambda x: x[0])
i = 0
while i < len(intervals) - 1:
if intervals[i][-1] >= intervals[i + 1][0]:
intervals[i] = [intervals[i][0], max(intervals[i][-1], intervals[i + 1][-1])]
intervals.pop(i + 1)
else:
i += 1
return intervals