0
点赞
收藏
分享

微信扫一扫

GPTs - 定制版的ChatGPT

老榆 03-04 12:00 阅读 2

题目链接:力扣

选择排序知识

  1. 设第一个元素为比较元素,依次和后面的元素比较,比较完所有元素并找到最小元素,记录最小元素下标,和第0个下表元素进行交换。
  2. 在未排序区域中,重复上述操作,以此类推找出剩余最小元素将它换到前面,即完成排序。

解析

现在让我们思考一下,冒泡排序和选择排序有什么异同?

相同点:都是两层循环,时间复杂度都为 O(n 2 ); 都只使用有限个变量,空间复杂度 O(1)。
不同点:冒泡排序在比较过程中就不断交换;而选择排序增加了一个变量保存最小值 / 最大值的下标,遍历完成后才交换,减少了交换次数。
事实上,冒泡排序和选择排序还有一个非常重要的不同点,那就是:冒泡排序法是稳定的,选择排序法是不稳定的。

排序算法的稳定性
假定在待排序的记录序列中,存在多个具有相同的关键字的记录,若经过排序,这些记录的相对次序保持不变,即在原序列中,r[i] = r[j],且 r[i] 在 r[j] 之前,而在排序后的序列中,r[i] 仍在 r[j] 之前,则称这种排序算法是稳定的;否则称为不稳定的。

理解了稳定性的定义后,我们就能分析出:冒泡排序中,只有左边的数字大于右边的数字时才会发生交换,相等的数字之间不会发生交换,所以它是稳定的。

选择排序算法如何实现稳定排序呢

实现的方式有很多种,这里给出一种最简单的思路:新开一个数组,将每轮找出的最小值依次添加到新数组中,选择排序算法就变成稳定的了。

二元选择排序

选择排序算法也是可以优化的,既然每轮遍历时找出了最小值,何不把最大值也顺便找出来呢?这就是二元选择排序的思想。

我们使用 minIndex 记录最小值的下标,maxIndex 记录最大值的下标。每次遍历后,将最小值交换到首位,最大值交换到末尾,就完成了排序。

由于每一轮遍历可以排好两个数字,所以最外层的遍历只需遍历一半即可。

二元选择排序中有一句很重要的代码,它位于交换最小值和交换最大值的代码中间:


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排序

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 个子序列,然后递归地排序两个子序列。

以「升序排列」为例,其基本步骤为 [摘自@维基百科]:

1. 挑选基准值:从数列中挑出一个元素,称为“基准”(pivot);
2.  分割(partition):重新排序数列,所有比基准值小的元素摆放在基准前面,所有比基准值大的元素摆在基准后面(与基准值相等的数可以到任何一边)。在这个分割结束之后,对基准值的排序就已经完成;
3. 递归排序子序列:递归地将小于基准值元素的子序列和大于基准值元素的子序列排序。

递归到最底部的判断条件是数列的大小是零或一,此时该数列显然已经有序.

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 和两个指针 lowhigh 作为参数,用来确定数组的操作区间。
  • 它首先选择 low 索引处的元素作为轴点(pivot)。
  • 使用两个指针 leftright 从数组的两端开始,向中间移动,并根据元素与轴点的比较结果进行交换,直到两个指针相遇。
  • 最终,轴点元素被放置在其最终位置上,该位置左边的所有元素都不大于轴点,右边的所有元素都不小于轴点。
  • 函数返回轴点的最终位置

这个代码实现了快速选择算法的一个变种,用来找出数组中第 k 大的元素。这个实现采用了“快速排序”的分区思想,并通过随机选择轴点(pivot)来提高算法的效率和避免最坏情况的发生。以下是代码的逐步解析:

randomPartition 函数

  • 为了避免在特定的数组顺序下陷入最坏情况(如已排序的数组),该函数首先在 lowhigh 范围内随机选择一个轴点索引 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-1n-k+1(因为第 k 大元素是第 n-k+1 小元素)来找到第 k 大的元素。

3 partiton

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)
  1. 快速选择函数 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 在减去所有小于轴点的元素后仍大于 bigequal 的总长度),说明第 k 大的元素在 small 中。此时,需要在 small 中寻找新的第 k - (len(nums) - len(small)) 大的元素,因为我们已经排除了一部分更大的元素。
  • 如果上述两种情况都不满足,说明第 k 大的元素在 equal 中,由于 equal 中的所有元素都等于轴点值 pivot,因此直接返回 pivot

5. 返回结果

  • 最终,通过调用 quick_select(nums, k) 执行快速选择逻辑,并返回找到的第 k 大的元素。

时间复杂度:快速选择算法的平均时间复杂度为 O(n),但在最坏情况下可能会达到 O(n2)。

通过随机选择轴点,快速选择算法能够在大多数情况下避免最坏情况的发生,从而保持较高的效率。

精讲:. - 力扣(LeetCode)

. - 力扣(LeetCode)

空间复杂度 O(logN) : 划分函数的平均递归深度为O(logN) 

方法3: 堆

举报

相关推荐

0 条评论