题目链接:力扣
选择排序知识
- 设第一个元素为比较元素,依次和后面的元素比较,比较完所有元素并找到最小元素,记录最小元素下标,和第0个下表元素进行交换。
- 在未排序区域中,重复上述操作,以此类推找出剩余最小元素将它换到前面,即完成排序。
解析
现在让我们思考一下,冒泡排序和选择排序有什么异同?
相同点:都是两层循环,时间复杂度都为 O(n 2 ); 都只使用有限个变量,空间复杂度 O(1)。
不同点:冒泡排序在比较过程中就不断交换;而选择排序增加了一个变量保存最小值 / 最大值的下标,遍历完成后才交换,减少了交换次数。
事实上,冒泡排序和选择排序还有一个非常重要的不同点,那就是:冒泡排序法是稳定的,选择排序法是不稳定的。
排序算法的稳定性
假定在待排序的记录序列中,存在多个具有相同的关键字的记录,若经过排序,这些记录的相对次序保持不变,即在原序列中,r[i] = r[j],且 r[i] 在 r[j] 之前,而在排序后的序列中,r[i] 仍在 r[j] 之前,则称这种排序算法是稳定的;否则称为不稳定的。
理解了稳定性的定义后,我们就能分析出:冒泡排序中,只有左边的数字大于右边的数字时才会发生交换,相等的数字之间不会发生交换,所以它是稳定的。
选择排序算法如何实现稳定排序呢?
实现的方式有很多种,这里给出一种最简单的思路:新开一个数组,将每轮找出的最小值依次添加到新数组中,选择排序算法就变成稳定的了。
二元选择排序
选择排序算法也是可以优化的,既然每轮遍历时找出了最小值,何不把最大值也顺便找出来呢?这就是二元选择排序的思想。
我们使用 minIndex 记录最小值的下标,maxIndex 记录最大值的下标。每次遍历后,将最小值交换到首位,最大值交换到末尾,就完成了排序。
由于每一轮遍历可以排好两个数字,所以最外层的遍历只需遍历一半即可。
二元选择排序中有一句很重要的代码,它位于交换最小值和交换最大值的代码中间:
python
def selectionSort(arr):
for i in range(len(arr) - 1):
minIndex = i # 记录最小元素的索引
# 找出最小元素
for j in range(i + 1, len(arr)):
if arr[j] < arr[minIndex]:
minIndex = j
# i不是最小元素时,将i和最小元素进行交换
if i != minIndex:
arr[i], arr[minIndex] = arr[minIndex], arr[i]
return arr
if __name__=="__main__":
nums = [1, 42, 65, 876, 34, 656, 4, 6757, 89, 24, 65, 42]
print("start:", nums)
方法1: bf排序
python
class Solution:
def findKthLargest(self, nums: List[int], k: int) -> int:
random.shuffle(nums)
#将数组从大到小排序
nums.sort(reverse=True)
return nums[k-1]
执行用时:208 ms
时间复杂:O(nlogn)
方法2: 快速选择 quick select
快排的改进,快排是一种分治思想的实现,没做一层快排可以将数组分成两份并确定一个数的位置。分析题目可以知道,要找到第 k 个最大的元素,找到这个元素被划分在哪边就可以了。
快速排序(英语:Quicksort),又称分区交换排序(partition-exchange sort),简称快排,一种排序算法,最早由东尼·霍尔(Tony Hoare )提出。在平均状况下,排序 n 个项目要 O(nlogn) 次比较。在最坏状况下则需要 O(n 2 ) 次比较,但这种状况并不常见。事实上,快速排序Θ(nlogn) 通常明显比其他演算法更快,因为它的内部循环(inner loop)可以在大部分的架构上很有效率地达成。
快速排序使用分治法(Divide and conquer)策略来把一个序列(list)分为较小和较大的 2 个子序列,然后递归地排序两个子序列。
以「升序排列」为例,其基本步骤为 [摘自@维基百科]:
-
挑选基准值:从数列中挑出一个元素,称为"基准"(pivot);
-
分割(partition):重新排序数列,所有比基准值小的元素摆放在基准前面,所有比基准值大的元素摆在基准后面(与基准值相等的数可以到任何一边)。在这个分割结束之后,对基准值的排序就已经完成;
-
递归排序子序列:递归地将小于基准值元素的子序列和大于基准值元素的子序列排序。
递归到最底部的判断条件是数列的大小是零或一,此时该数列显然已经有序.
python
class Solution:
def findKthLargest(self, nums: List[int], k: int) -> int:
def partition(arr: List[int], low: int, high: int) -> int:
pivot = arr[low] # 选取最左边为pivot
left, right = low, high # 双指针
while left < right:
while left<right and arr[right] >= pivot: # 找到右边第一个<pivot的元素
right -= 1
arr[left] = arr[right] # 并将其移动到left处
while left<right and arr[left] <= pivot: # 找到左边第一个>pivot的元素
left += 1
arr[right] = arr[left] # 并将其移动到right处
arr[left] = pivot # pivot放置到中间left=right处
return left
def randomPartition(arr: List[int], low: int, high: int) -> int:
pivot_idx = random.randint(low, high) # 随机选择pivot
arr[low], arr[pivot_idx] = arr[pivot_idx], arr[low] # pivot放置到最左边
return partition(arr, low, high) # 调用partition函数
def topKSplit(arr: List[int], low: int, high: int, k: int) -> int:
# mid = partition(arr, low, high) # 以mid为分割点【非随机选择pivot】
mid = randomPartition(arr, low, high) # 以mid为分割点【随机选择pivot】
if mid == k-1: # 第k小元素的下标为k-1
return arr[mid] #【找到即返回】
elif mid < k-1:
return topKSplit(arr, mid+1, high, k) # 递归对mid右侧元素进行排序
else:
return topKSplit(arr, low, mid-1, k) # 递归对mid左侧元素进行排序
n = len(nums)
return topKSplit(nums, 0, n-1, n-k+1) # 第k大元素即为第n-k+1小元素
这个代码实现了快速选择算法的一个变种,用来找出数组中第 k
大的元素。这个实现采用了"快速排序"的分区思想,并通过随机选择轴点(pivot)来提高算法的效率和避免最坏情况的发生。以下是代码的逐步解析:
partition
函数
- 这个函数接受一个数组
arr
和两个指针low
和high
作为参数,用来确定数组的操作区间。 - 它首先选择
low
索引处的元素作为轴点(pivot)。 - 使用两个指针
left
和right
从数组的两端开始,向中间移动,并根据元素与轴点的比较结果进行交换,直到两个指针相遇。 - 最终,轴点元素被放置在其最终位置上,该位置左边的所有元素都不大于轴点,右边的所有元素都不小于轴点。
- 函数返回轴点的最终位置
这个代码实现了快速选择算法的一个变种,用来找出数组中第 k
大的元素。这个实现采用了"快速排序"的分区思想,并通过随机选择轴点(pivot)来提高算法的效率和避免最坏情况的发生。以下是代码的逐步解析:
randomPartition
函数
- 为了避免在特定的数组顺序下陷入最坏情况(如已排序的数组),该函数首先在
low
和high
范围内随机选择一个轴点索引pivot_idx
。 - 然后,它将选定的轴点与区间的第一个元素交换,确保随机选择的轴点被移到了区间的开头。
- 最后,调用
partition
函数执行实际的分区操作。
topKSplit
函数
- 这个函数是快速选择算法的核心,它递归地在数组的一个子区间内查找第
k
小(或第k
大)的元素。 - 它首先调用
randomPartition
对当前考虑的数组区间进行分区,然后根据分区后轴点的位置与k
的关系决定下一步的操作。 - 如果轴点恰好是第
k-1
个元素(因为数组索引从0开始),那么就找到了第k
小的元素,直接返回。 - 如果轴点的位置小于
k-1
,说明第k
小的元素位于轴点右侧的区间内,因此对右侧区间递归调用topKSplit
。 - 如果轴点的位置大于
k-1
,说明第k
小的元素位于轴点左侧的区间内,因此对左侧区间递归调用topKSplit
。
主函数 findKthLargest
- 最后,
findKthLargest
函数通过调用topKSplit
并传入整个数组、起始索引0
、结束索引n-1
和n-k+1
(因为第k
大元素是第n-k+1
小元素)来找到第k
大的元素。
3 partiton
python
class Solution:
def findKthLargest(self, nums: List[int], k: int) -> int:
def quick_select(nums, k):
pivot = random.choice(nums)
big, equal, small = [], [], []
# 将大于、小于、等于 pivot 的元素划分至 big, small, equal 中
for num in nums:
if num > pivot:
big.append(num)
elif num < pivot:
small.append(num)
else:
equal.append(num)
if k <= len(big):
# 第 k 大元素在 big 中,递归划分
return quick_select(big, k)
if len(nums) - len(small) < k:
# 第 k 大元素在 small 中,递归划分
return quick_select(small, k - len(nums) + len(small))
# 第 k 大元素在 equal 中,直接返回 pivot
return pivot
return quick_select(nums, k)
- 快速选择函数
quick_select
:
这是一个内部定义的辅助函数,用于实现快速选择算法。它接受当前考虑的数组 nums
和目标 k
作为参数。
2. 选择轴点:
pivot = random.choice(nums)
从 nums
中随机选择一个元素作为轴点(Pivot)。这种随机化策略有助于提高算法的平均性能,避免在特定情况下的性能退化。
3. 分区:
算法遍历数组 nums
,根据元素与轴点的大小关系,将其分配到三个列表中:big
(存储所有大于轴点的元素)、equal
(存储所有等于轴点的元素)、small
(存储所有小于轴点的元素)。
4. 递归选择:
- 如果
k
小于等于big
列表的长度,说明第k
大的元素在big
中,因此递归地在big
中寻找第k
大的元素。 - n-1-k+1
- 如果
k
大于nums
减去small
列表长度的结果(即k
在减去所有小于轴点的元素后仍大于big
和equal
的总长度),说明第k
大的元素在small
中。此时,需要在small
中寻找新的第k - (len(nums) - len(small))
大的元素,因为我们已经排除了一部分更大的元素。 - 如果上述两种情况都不满足,说明第
k
大的元素在equal
中,由于equal
中的所有元素都等于轴点值pivot
,因此直接返回pivot
。
- 返回结果:
- 最终,通过调用
quick_select(nums, k)
执行快速选择逻辑,并返回找到的第k
大的元素。
时间复杂度:快速选择算法的平均时间复杂度为 O(n),但在最坏情况下可能会达到 O(n2)。
通过随机选择轴点,快速选择算法能够在大多数情况下避免最坏情况的发生,从而保持较高的效率。
空间复杂度 O(logN) : 划分函数的平均递归深度为O(logN)