leetcode 链接:912. 排序数组

题目

给你一个整数数组 nums,请你将该数组升序排列。

示例:

  1. 输入:nums = [5,2,3,1]
  2. 输出:[1,2,3,5]
输入:nums = [5,1,1,2,0,0]
输出:[0,0,1,1,2,5]

解答 & 代码

解法一:快速排序

参考:快速排序 - 菜鸟教程

设数组长度为 n

  • 时间复杂度 O(n log n)
  • 空间复杂度 O(h),h 为快排递归调用的层数,递归调用需要 O(h) 的栈空间

    • 最快情况需要 O(n) 的空间
    • 最优情况每次都平衡,空间复杂度 O(log n)

      class Solution {
      private:
      // 快排的分区函数
      // 随机挑选数组中的一个元素作为基准 pivot,重新排列数组使得 pivot 左边的元素都比它小,右边的元素都比它大
      int partition(vector<int>& nums, int left, int right)
      {
         // 随机选择一个元素作为基准 pivot,并将该元素和 left 位置的元素交换
         int randomIdx = rand() % (right - left + 1) + left;
         swap(nums[left], nums[randomIdx]);
      
         int pivot = nums[left];            // nums[left] 作为基准元素,left 位置就是第一个空缺位置
         while(left < right)
         {
             while(left < right && nums[right] >= pivot)
                 --right;
             nums[left] = nums[right];    // 用 nums[right] 填补左边的空缺,right 就变成新的空缺
             while(left < right && nums[left] <= pivot)
                 ++left;
             nums[right] = nums[left];    // 用 nums[left] 填补右边的空缺,left 就变成新的空缺
         }
         nums[left] = pivot;                // 将基准元素填入最后的空缺,此时基准元素左边的元素都比它小,右边的元素都比它大
         return left;                    // 返回基准元素的位置
      }
      // 快速排序主函数
      void quickSort(vector<int>& nums, int left, int right)
      {
         if(left < right)
         {
             int pivot = partition(nums, left, right);    // 基准元素的位置
             quickSort(nums, left, pivot - 1);            // 递归将基准元素左边排序
             quickSort(nums, pivot + 1, right);            // 递归将基准元素右边排序
         }
      }
      public:
      vector<int> sortArray(vector<int>& nums) {
         quickSort(nums, 0, nums.size() - 1);
         return nums;
      }
      };
      

      执行结果: ``` 执行结果:通过

执行用时:100 ms, 在所有 C++ 提交中击败了 18.37% 的用户 内存消耗:27.7 MB, 在所有 C++ 提交中击败了 55.21% 的用户

<a name="ywjhZ"></a>
## 解法二:堆排序
> 参考:[图解排序算法(三)之堆排序](https://www.cnblogs.com/chengxiao/p/6129630.html)
> [堆排序 - 菜鸟教程](https://www.runoob.com/w3cnote/heap-sort.html)

两种堆排序方法:

- **大顶堆**:每个节点的值都大于等于其左右孩子节点的值,用于**升序排序** 
- **小顶堆**:每个节点的值都小于等于其左右孩子节点的值,用于**降序排序** 

![](https://cdn.nlark.com/yuque/0/2021/png/1324638/1624604312258-015cd235-2f7c-4af0-8e17-9acec78bb02c.png#clientId=ubc0fbd48-881e-4&crop=0&crop=0&crop=1&crop=1&from=paste&id=u77c3ce73&margin=%5Bobject%20Object%5D&originHeight=776&originWidth=1792&originalType=url&ratio=1&rotation=0&showTitle=false&status=done&style=none&taskId=u056fe08a-8ae8-4223-bb5d-adc00899e11&title=)<br />**堆结构的存储**使用的是**数组 **`**nums**`,将上图所示的大顶堆(二叉树)各节点的值按**层序遍历**存储在数组中:

- 数组中下标为 i 的堆节点的**左孩子节点下标为 **`**2i + 1**`**,右孩子节点下标为 **`**2i + 2** `
- **最后一个非叶子节点的下标为 **`**nums.siz()/2 - 1**` ,就是上图大顶堆的“20”这个节点

![](https://cdn.nlark.com/yuque/0/2021/png/1324638/1624604430075-d1023de2-81d5-45c6-b0dc-72d23ed8c6c3.png#clientId=ubc0fbd48-881e-4&crop=0&crop=0&crop=1&crop=1&from=paste&id=u05b1d042&margin=%5Bobject%20Object%5D&originHeight=246&originWidth=1358&originalType=url&ratio=1&rotation=0&showTitle=false&status=done&style=none&taskId=ufc2cc959-e1bc-44cf-9c40-8abbde54bcc&title=)

本题使用大顶堆进行升序排序,**算法流程**:

1. 初始化,将无序序列构建为大顶堆
   1. 就是从最后一个非叶子节点开始,从左往右,从下往上调整
2. 循环:直至当前堆只剩一个节点:
   1. 将当前堆的最大值(也就是堆顶元素)和当前的堆尾元素交换,那么这个堆尾元素就已经是排好序的了,当前堆不再包含该元素(`len - 1`),重新调整新的堆

- **时间复杂度 O(n log n)**:[初始建堆调整的时间复杂度为 O(n)](https://blog.csdn.net/zhpf225/article/details/105962492),之后 n - 1 次调整,每次调整的时间复杂度为 O(log n)
- **空间复杂度 O(1)** 
```cpp
class Solution {
private:
    // 交换数组 idx1 和 idx2 位置对应的元素
    void swap(vector<int>& nums, int idx1, int idx2)
    {
        int temp = nums[idx1];
        nums[idx1] = nums[idx2];
        nums[idx2] = temp;
    }

    // 调整大顶堆
    // start:要调整的二叉树的头节点在数组 nums 中的下标;
    // end:要调整的二叉树的最后一个节点在数组 nums 中的下标
    void adjustHeap(vector<int>& nums, int start, int end)
    {
        int dadIdx = start;                // 父节点下标
        int sonIdx = start * 2 + 1;        // 子节点下标(初始化为左孩子节点)
        // 如果子节点在调整范围内,循环继续调整
        while(sonIdx <= end)
        {
            // 比较左、右孩子节点值得大小,取大的作为子节点
            if(sonIdx + 1 <= end && nums[sonIdx] < nums[sonIdx + 1])
                ++sonIdx;
            // 如果子节点的值 > 父节点的值,交换父子节点的值,继续比较、调整子节点和孙子节点
            if(nums[sonIdx] > nums[dadIdx])
            {
                swap(nums, sonIdx, dadIdx);    // 交换父子节点的值
                dadIdx = sonIdx;
                sonIdx = dadIdx * 2 + 1;
            }
            // 如果子节点的值 <= 父节点的值,代表调整完毕,直接返回跳出函数
            else
                return;
        }
    }

    // 堆排序(大顶堆,升序)
    void maxHeapSort(vector<int>& nums)
    {
        int len = nums.size();
        // 1. 初始化,将无序序列构建为大顶堆
        // 从最后一个非叶子节点(len / 2 - 1)开始,从左往右,从下往上调整二叉树
        for(int i = len / 2 - 1; i >= 0; --i)
            adjustHeap(nums, i, len - 1);

        // 将堆顶元素与当前堆末尾交换
        for(int i = len - 1; i > 0; --i)
        {
            swap(nums, 0, i);                // 将堆顶元素(当前最大值)与当前堆末尾交换
            adjustHeap(nums, 0, i - 1);        // 调整剩下的堆(除去之前已排好序的堆尾元素)
        }
    }
public:
    vector<int> sortArray(vector<int>& nums) {
        maxHeapSort(nums);
        return nums;
    }
};
执行结果:通过

执行用时:132 ms, 在所有 C++ 提交中击败了 9.55% 的用户
内存消耗:27.8 MB, 在所有 C++ 提交中击败了 38.10% 的用户

解法三:归并排序

  • 递归对数组左半边排序
  • 递归对数组右半边排序
  • 将排好序的左、右半边数组合并

  • 时间复杂度 O(n log n)

  • 空间复杂度 O(n):合并两个排序数组的时候需要使用额外的数组 tmp 暂存合并后的结果,空间复杂度 O(n);递归调用需要的栈空间为 O(log n)

    class Solution {
    private:
      // 归并排序
      void mergeSort(vector<int>& nums, int left, int right)
      {
          // 递归结束条件
          if(left >= right)
              return;
    
          int mid = (left + right) / 2;
          mergeSort(nums, left, mid);            // 递归对数组左半边进行排序
          mergeSort(nums, mid + 1, right);    // 递归对数组右半边进行排序
    
          // 双指针,将排序后的左、右半边数组合并
          int idx1 = left;
          int idx2 = mid + 1;
          vector<int> tmp(right - left + 1, 0);    // 合并后的结果暂时存储到 tmp 数组
          int idx = 0;
          while(idx1 <= mid && idx2 <= right)
          {
              if(nums[idx1] < nums[idx2])
              {
                  tmp[idx] = nums[idx1];
                  ++idx1;
              }
              else
              {
                  tmp[idx] = nums[idx2];
                  ++idx2;
              }
              ++idx;
          }
          while(idx1 <= mid)
          {
              tmp[idx] = nums[idx1];
              ++idx1;
              ++idx;
          }
          while(idx2 <= right)
          {
              tmp[idx] = nums[idx2];
              ++idx2;
              ++idx;
          }
          // 将 tmp 数组的值再拷贝回原数组
          for(int i = 0; i < right - left + 1; ++i)
              nums[left + i] = tmp[i];
      }
    public:
      vector<int> sortArray(vector<int>& nums) {
          mergeSort(nums, 0, nums.size() - 1);
          return nums;
      }
    };
    

    执行结果: ``` 执行结果:通过

执行用时:204 ms, 在所有 C++ 提交中击败了 5.93% 的用户 内存消耗:70.4 MB, 在所有 C++ 提交中击败了 4.99% 的用户 ```