leetcode912. Sort an Array

Given an array of integers nums, sort the array in ascending order and return it.

You must solve the problem without using any built-in functions in O(nlog(n)) time complexity and with the smallest space complexity possible.

Example 1:

Input: nums = [5,2,3,1]
Output: [1,2,3,5]
Explanation: After sorting the array, the positions of some numbers are not changed (for example, 2 and 3), while the positions of other numbers are changed (for example, 1 and 5).

Example 2:

Input: nums = [5,1,1,2,0,0]
Output: [0,0,1,1,2,5]
Explanation: Note that the values of nums are not necessairly unique.

Constraints:

1 <= nums.length <= 5 * 104
-5 * 104 <= nums[i] <= 5 * 104

方法一. 快速排序(Quick Sort)

快速排序(英语:Quicksort),又称分区交换排序(partition-exchange sort),简称快排,一种排序算法,最早由东尼·霍尔(Tony Hoare )提出。在平均状况下,排序 n 个项目要 O(nlogn) 次比较。在最坏状况下则需要 O(n2) 次比较,但这种状况并不常见。事实上,快速排序 Θ(nlogn) 通常明显比其他演算法更快,因为它的内部循环(inner loop)可以在大部分的架构上很有效率地达成。

快速排序使用分治法(Divide and conquer)策略来把一个序列(list)分为较小和较大的 2 个子序列,然后递归地排序两个子序列。

其基本步骤为:

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

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

在具体实现上也有多种方式,下图则展示了一种基于“填坑”思路的实现:

以(子)数组最左端元素为pivot;

每次先从右侧(以 right 指代)开始找到小于pivot的元素,并将其填充到左侧空出的“坑”处,再从左侧(以 left 指代)开始找到大于pivot的元素,将其填充到右侧空出的“坑”处;

当左右指针相遇时,将pivot放置于 left/right 处,此时得到了一个有效的分割:小于pivot的元素均在其左侧,大于pivot的元素均在其右侧(等于pivot的元素可放置于任何一边);

对pivot两侧的子数组递归排序,直至子数组无法再分割。

在这里插入图片描述选取基准值pivot也有多种方式,且选取pivot的方法对排序的时间性能有着决定性的影响。例如,对于一个逆序数组,如果每次选取数组中的第一个元素为pivot,那么将其正序排列的过程将会变得非常慢,时间复杂度为 O(n2)。因此,在具体实现中考虑随机化选择基准值pivot也是非常有必要的。

class Solution:
    def sortArray(self, nums: List[int]) -> List[int]:

        def partition(arr, low, high):
            pivot_idx = random.randint(low, high)                   # 随机选择pivot
            arr[low], arr[pivot_idx] = arr[pivot_idx], arr[low]     # pivot放置到最左边
            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 quickSort(arr, low, high):
            if low >= high:             # 递归结束
                return  
            mid = partition(arr, low, high)     # 以mid为分割点
            quickSort(arr, low, mid-1)          # 递归对mid两侧元素进行排序
            quickSort(arr, mid+1, high)
        

        quickSort(nums, 0, len(nums)-1)         # 调用快排函数对nums进行排序
        return nums

方法二. 归并排序(Merge Sort)

归并排序是建立在归并操作上的一种有效,稳定的排序算法,该算法是采用分治法(Divide and Conquer)的一个非常典型的应用。将已有序的子序列合并,得到完全有序的序列;即先使每个子序列有序,再使子序列段间有序。若将两个有序表合并成一个有序表,称为二路归并。

归并排序采用经典的分治(divide-and-conquer)策略来对序列进行排序:

「分」的阶段首先将序列一步步分解成小的子序列进行分段排序;
「治」的阶段则将分段有序的子序列合并在一起,使得整个序列变得有序。

下图给出了归并排序的基本步骤示意图:
在这里插入图片描述
下图以最后一步中合并分段有序的子序列为例做下说明:
在这里插入图片描述设立两个指针 left 和 right,分别指向左右两个待合并的已有序的子数组 nums[low,mid] 和 nums[mid+1,high]。 如图中所示,在合并左右两个子数组时,若 nums[right]<nums[left],则将当前更小的 nums[right] 放入排序结果中。依此类推,即可得到最终排好序的数组。

class Solution:
    def sortArray(self, nums: List[int]) -> List[int]:

        def mergeSort(arr, low, high):
            if low >= high:                 # 递归结束标志
                return
            
            mid = low + (high-low)//2       # 中间位置
            mergeSort(arr, low, mid)        # 递归对前后两部分进行排序
            mergeSort(arr, mid+1, high)

            left, right = low, mid+1        # 将arr一分为二:left指向前半部分(已有序),right指向后半部分(已有序)
            tmp = []                        # 记录排序结果
            while left <= mid and right <= high:    # 比较排序,优先添加前后两部分中的较小者
                if arr[left] <= arr[right]:         # left指示的元素较小
                    tmp.append(arr[left])
                    left += 1
                else:                               # right指示的元素较小
                    tmp.append(arr[right])
                    right += 1
            
            while left <= mid:              # 若左半部分还有剩余,将其直接添加到结果中
                tmp.append(arr[left])
                left += 1
            # tmp += arr[left:mid+1]        # 等价于以上三行

            while right <= high:            # 若右半部分还有剩余,将其直接添加到结果中
                tmp.append(arr[right])
                right += 1
            # tmp += arr[right:high+1]      # 等价于以上三行

            arr[low: high+1] = tmp          # [low, high] 区间完成排序
        

        mergeSort(nums, 0, len(nums)-1)     # 调用mergeSort函数完成排序
        return nums


方法三. 堆排序(Heap Sort)

堆排序(英语:Heapsort)是指利用堆(heap)这种数据结构所设计的一种排序算法。堆是一个近似完全二叉树的结构,并同时满足堆积的性质:即子节点的键值或索引总是小于(或者大于)它的父节点。

堆可看作是一个「完全二叉树」的结构:

记一个二叉树的深度为 h,若除第 h 层外,该二叉树的其它各层 [1, h−1] 的节点数均达到最大值(满的),且第 h 层所有的节点均连续集中在最左边,那么这棵树即为一棵 完全二叉树。

根据父子节点之间的关系,堆又可大致分为两类(如下图所示):

在这里插入图片描述
大根堆/大顶堆:每个节点的值均大于等于其左右孩子节点的值;
小根堆/小顶堆:每个节点的值均小于等于其左右孩子节点的值。

若对堆中的节点按照层序遍历的方式进行编号,则可将其结构映射到数组中。若从 0 开始编号(如上图所示),则节点 i 的「左子节点」为 2i+1,「右子节点」为 2i+2,此时大根堆和小根堆满足以下关系:
{大根堆:nums[i]>=nums[2i+1]&nums>=nums[2i+2]小根堆:nums[i]<=nums[2i+1]&nums[i]<=nums[2i+2]​

同样地,若从 1 开始对堆中的节点进行编号,则节点 i 的「左子节点」为 2i,「右子节点」为 2i+1,此时大根堆和小根堆满足以下关系:
{大根堆:nums[i]>=nums[2i]&nums>=nums[2i+1]小根堆:nums[i]<=nums[2i]&nums[i]<=nums[2i+1]​

堆与排序:

对于一个待排序的包含 n 个元素的数组 nums,堆排序 通常包含以下几个基本步骤:

建堆:将待排序的数组初始化为大根堆(小根堆)。此时,堆顶的元素(即根节点)即为整个数组中的最大值(最小值)。
交换和调整:将堆顶元素与末尾元素进行交换,此时末尾即为最大值(最小值)。除去末尾元素后,将其他 n−1 个元素重新构造成一个大根堆(小根堆),如此便可得到原数组 n 个元素中的次大值(次小值)。
重复步骤二,直至堆中仅剩一个元素,如此便可得到一个有序序列了。

通过以上步骤我们也可以发现:

对于「升序排列」数组,需用到大根堆;
对于「降序排列」数组,则需用到小根堆。

案例:

假设有一个待排序的数组 nums=[5,2,1,9,6,8],我们可将其构造成一个二叉树,如下图所示:
在这里插入图片描述I. 构造大根堆

如何将一个完全二叉树构造成一个大顶堆?

一个很好的实现方式是从最后一个「非叶子节点」为根节点的子树出发,从右往左、从下往上进行调整操作。

需要注意的是:

    若完全二叉树从 0 开始进行编号,则第一个非叶子节点为 n/2−1;若完全二叉树从 1 开始进行编号,则第一个非叶子节点为 n/2。
    调整针对的是以该非叶子节点为根节点的子树,即对该根节点以下的所有部分均进行调整操作。
    由于我们是从右往左、从下往上遍历非叶子节点的,因此当遍历到某个非叶子节点时,它的子树(不包括该节点本身)已经是堆有序的。

对于以某个非叶子节点的子树而言,其基本的调整操作包括:

如果该节点大于等于其左右两个子节点,则无需再对该节点进行调整,因为它的子树已经是堆有序的;
如果该节点小于其左右两个子节点中的较大者,则将该节点与子节点中的较大者进行交换,并从刚刚较大者的位置出发继续进行调整操作,直至堆有序。

对于 nums=[5,2,1,9,6,8],其包含 n=length(nums)=6 个元素,第一个非叶子节点为 n/2−1=2,对应的基本建堆(大根堆)步骤如下:

第一个非叶子节点 2:nums[2]<nums[5],即节点 2 小于其左子节点 5(其右子节点不存在),需要调整交换两者。如下图所示:

在这里插入图片描述第二个非叶子节点 1:nums[1]<nums[3] 且 nums[1]<nums[4],即节点 1 均小于其左右子节点,但其左子节点 3 更大,因此需要调整交换节点 1 与较大的子节点 3。如下图所示:

在这里插入图片描述第三个非叶子节点 0:nums[0]<nums[1] 且 nums[0]<nums[2],即节点 0 均小于其左右子节点,但其左子节点 1 更大,因此需要调整交换节点 0 与较大的子节点 1。如下图所示:
在这里插入图片描述然而,调整完节点 0 与 节点 1 后我们发现原子树的堆序已被打破,此时 nums[1]<nums[4],即节点 1 小于其右子节点 4,因此还需要继续对以节点 1 为根结点的子树继续进行调整,如下图:

在这里插入图片描述至此,全部的调整完毕,我们也就建立起了一个大根堆 nums=[9,6,8,2,5,1]:

max_heap.png
II. 排序

建立起一个大根堆后,便可以对数组中的元素进行排序了。总结来看,将堆顶元素与末尾元素进行交换,此时末尾即为最大值。除去末尾元素后,将其他 n−1 个元素重新构造成一个大根堆,继续将堆顶元素与末尾元素进行交换,如此便可得到原数组 n 个元素中的次大值。如此反复进行交换、重建、交换、重建,便可得到一个「升序排列」的数组。

对于大根堆 nums=[9,6,8,2,5,1],其堆排序基本步骤如下:

最大元素:此时堆顶元素为最大值,将其交换到末尾,如下所示:

在这里插入图片描述

交换完成后,除去末尾最大元素,此时需要对堆进行重建,使得剩余元素继续满足大根堆的要求。如下所示:

在这里插入图片描述

次大元素:此时堆顶元素为待排序元素中的最大值(即原数组中的次大值),将堆顶元素交换到末尾,如下所示:

在这里插入图片描述

交换完成后,除去末尾最大元素,此时需要对堆进行重建,使得剩余元素继续满足大根堆的要求(省略)。

第三大元素:此时堆顶元素为待排序元素中的最大值(即原数组中的第三大值),将堆顶元素交换到末尾,如下所示:

在这里插入图片描述

交换完成后,除去末尾最大元素,此时需要对堆进行重建,使得剩余元素继续满足大根堆的要求(省略)。

第四大元素:此时堆顶元素为待排序元素中的最大值(即原数组中的第四大值),将堆顶元素交换到末尾,如下所示:

在这里插入图片描述

交换完成后,除去末尾最大元素,此时需要对堆进行重建,使得剩余元素继续满足大根堆的要求(省略)。

次小元素(第五大元素):此时堆顶元素为待排序元素中的最大值(即原数组中的次小元素或第五大元素),将堆顶元素交换到末尾,如下所示:

在这里插入图片描述

交换完成后,除去末尾最大元素,此时堆中仅剩一个元素,即为原数组中的最小值。

至此,基于大根堆的升序排列完成,如下所示:

在这里插入图片描述

class Solution:
    def sortArray(self, nums: List[int]) -> List[int]:

        def maxHepify(arr, i, end):     # 大顶堆
            j = 2*i + 1                 # j为i的左子节点【建堆时下标0表示堆顶】
            while j <= end:             # 自上而下进行调整
                if j+1 <= end and arr[j+1] > arr[j]:    # i的左右子节点分别为j和j+1
                    j += 1                              # 取两者之间的较大者
                
                if arr[i] < arr[j]:             # 若i指示的元素小于其子节点中的较大者
                    arr[i], arr[j] = arr[j], arr[i]     # 交换i和j的元素,并继续往下判断
                    i = j                       # 往下走:i调整为其子节点j
                    j = 2*i + 1                 # j调整为i的左子节点
                else:                           # 否则,结束调整
                    break
        

        n = len(nums)
        
        # 建堆【大顶堆】
        for i in range(n//2-1, -1, -1):         # 从第一个非叶子节点n//2-1开始依次往上进行建堆的调整
            maxHepify(nums, i, n-1)

        # 排序:依次将堆顶元素(当前最大值)放置到尾部,并调整堆
        for j in range(n-1, -1, -1):
            nums[0], nums[j] = nums[j], nums[0]     # 堆顶元素(当前最大值)放置到尾部j
            maxHepify(nums, 0, j-1)                 # j-1变成尾部,并从堆顶0开始调整堆
        
        return nums

从下标 1 开始建堆,即在nums最左端补充一位 0:nums = [0] + nums(在树状数组和线段树结构中也有类似的操作,以便计算父子节点之间的关系):

class Solution:
    def sortArray(self, nums: List[int]) -> List[int]:

        def maxHepify(arr, i, end):     # 大顶堆
            j = 2*i                     # j为i的左子节点【建堆时下标1表示堆顶】
            while j <= end:
                if j+1 <= end and arr[j+1] > arr[j]:    # i的左右子节点分别为j和j+1
                    j += 1                              # 取两者之间的较大者
                
                if arr[i] < arr[j]:             # 若i指示的元素小于其子节点中的较大者
                    arr[i], arr[j] = arr[j], arr[i]     # 交换i和j的元素,并继续往下判断
                    i = j                       # 往下走:i调整为其子节点j
                    j = 2*i                     # j调整为i的左子节点
                else:                           # 否则,结束调整
                    break
        
        n = len(nums)
        nums = [0] + nums       # nums头部添加0,满足从下标1开始建堆
        
        # 建堆【大顶堆】
        for i in range(n//2, 0, -1):    # 从第一个非叶子节点n//2开始依次往上进行建堆的调整【注意:此时堆顶为下标1】
            maxHepify(nums, i, n)
        
        # 排序:依次将堆顶元素(当前最大值)放置到尾部,并调整堆
        for j in range(n, 0, -1):
            nums[1], nums[j] = nums[j], nums[1]     # nums[1]为堆顶元素(最大值),将其放置到尾部j
            maxHepify(nums, 1, j-1)                 # j-1变成尾部,并从堆顶1开始调整堆
        
        return nums[1:]
        

 
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值