一道排序题引发的思考
题目:912. 排序数组
给你一个整数数组 nums,请你将该数组升序排列。
示例 1:
输入:nums = [5,2,3,1]
输出:[1,2,3,5]
示例 2:
输入:nums = [5,1,1,2,0,0]
输出:[0,0,1,1,2,5]
提示:
1 <= nums.length <= 50000
-50000 <= nums[i] <= 50000
思考一:朴素快速排序
之所以叫做朴素快速排序是因为在选取枢轴量的时候就是选取最左边的那个元素来当做枢轴量,但是这样有一个很大的问题就是如果所给的排序序列都已经有序的话,这种情况下快速排序的时间复杂度会退化到O(n^2),最后提交的时候也会有一组数据显示超时。
Code:
class Solution { public: void quickSort(vector<int>& nums, int l, int r) { if (l >= r) return; int pivot = partion(nums, l, r); quickSort(nums, l, pivot - 1); quickSort(nums, pivot + 1, r); } int partion(vector<int>& nums, int l, int r) { int temp = nums[l]; while (l < r) { while (l < r && nums[r] >= temp) r--; nums[l] = nums[r]; while (l < r && nums[l] <= temp) l++; nums[r] = nums[l]; } nums[l] = temp; return l; } vector<int> sortArray(vector<int>& nums) { int len = nums.size(); quickSort(nums, 0, len - 1); return nums; } };
结果:
思路二:归并排序
既然朴素快速排序在特定的条件下会退化到O(n^2),如果选择归并排序的话就不会存在时间复杂度退化到O(n^2)的情况,平均时间复杂度为O(nlogn),而且归并排序也是一种稳定的排序算法。但是,归并排序存在的缺点是需要一个额外的存储空间因此空间复杂的变为O(n)。而且平均情况下归并排序的常量因子k(O(knlogn))要比快速排序的常量因子大。
Code:
class Solution { public: vector<int> dummy; void mergeSort(vector<int>& nums, int l, int r) { if (l >= r) return; int m = (l + r) / 2; mergeSort(nums, l, m); mergeSort(nums, m+1, r); merge(nums, l, m, m+1, r); } void merge(vector<int>& nums, int l1, int r1, int l2, int r2) { int start = l1; int end = r2; int index = l1; while(l1 <= r1 && l2 <= r2) { if (nums[l1] <= nums[l2]) { dummy[index++] = nums[l1++]; } else { dummy[index++] = nums[l2++]; } } while (l1 <= r1) { dummy[index++] = nums[l1++]; } while (l2 <= r2) { dummy[index++] = nums[l2++]; } for (int i = start; i <= end; ++i) { nums[i] = dummy[i]; } } vector<int> sortArray(vector<int>& nums) { int len = nums.size(); dummy.resize(len); mergeSort(nums, 0, len - 1); return nums; } };
结果:
思路三:三者取其中快速排序
在《数据结构》(严蔚敏、吴伟民)这本书中介绍了这种方法,具体的做法就是在选取枢轴量的时候不是选取第一个元素作为枢轴量,而是在nums[l], nums[r], nums[mid]中选取一个中间值作为枢轴量,这样的话,如果原来的序列已经有序的话可以减少交换的次数(快速排序是一种基于交换的排序算法,不稳定),从而减少在最坏情况下的时间复杂度,但是仍然不能够做到在对已经有序的序列进行排序时做到O(n)。
Code:
class Solution { public: int findMid(vector<int>& nums, int l, int r) { int m = (l + r) / 2; int x1 = nums[l]; int x2 = nums[m]; int x3 = nums[r]; int t; if (x1 > x2) { t = x1; x1 = x2; x2 = t; } if (x1 > x3) { t = x1; x1 = x3; x3 = t; } if (x2 > x3) { t = x2; x2 = x3; x3 = t; } if (nums[l] == x2) return l; else if (nums[m] == x2) return m; else return r; } void quickSort(vector<int>& nums, int l, int r) { if (l >= r) return; int pivot = partion(nums, l, r); quickSort(nums, l, pivot - 1); quickSort(nums, pivot + 1, r); } int partion(vector<int>& nums, int l, int r) { int index = findMid(nums, l, r); int temp = nums[index]; nums[index] = nums[l]; while (l < r) { while (l < r && nums[r] >= temp) r--; nums[l] = nums[r]; while (l < r && nums[l] <= temp) l++; nums[r] = nums[l]; } nums[l] = temp; return l; } vector<int> sortArray(vector<int>& nums) { int len = nums.size(); quickSort(nums, 0, len - 1); return nums; } };
结果:
从结果中我们可以看出,改进后的快速排序可以通过所有的测试用例,并且比归并排序所用的空间要少。
是否还可以对快速排序继续进行优化?
思路四:交换逆序对+快速排序+冒泡排序
在指针r--和l++的过程中,如果相邻两元素是逆序对的话,那么将逆序对进行交换,这样做可以减少最后冒泡排序交换的次数。当要排序的数字数量很少的时候,快速排序并不能体现出它的优势,这时选用冒泡排序或许是一个更好的选择,所以采用两者相结合的方法,从而提高排序的效率。
Code:
class Solution { public: int findMid(vector<int>& nums, int l, int r) { int m = (l + r) / 2; int x1 = nums[l]; int x2 = nums[m]; int x3 = nums[r]; int t; if (x1 > x2) { t = x1; x1 = x2; x2 = t; } if (x1 > x3) { t = x1; x1 = x3; x3 = t; } if (x2 > x3) { t = x2; x2 = x3; x3 = t; } if (nums[l] == x2) return l; else if (nums[m] == x2) return m; else return r; } void quickSort(vector<int>& nums, int l, int r) { if (l >= r) return; int pivot = partion(nums, l, r); if (r - l < 10) { for (int i = l; i <= r; ++i) { for (int j = r; j > l; --j) { if (nums[j] < nums[j-1]) { pivot = nums[j]; nums[j] = nums[j-1]; nums[j-1] = pivot; } } } } else { quickSort(nums, l, pivot - 1); quickSort(nums, pivot + 1, r); } } int partion(vector<int>& nums, int l, int r) { int index = findMid(nums, l, r); int temp = nums[index]; nums[index] = nums[l]; while (l < r) { while (l < r && nums[r] >= temp) { r--; if (nums[r] > nums[r+1] && l < r) { // 相邻两个元素处于逆序时进行交换 index = nums[r]; nums[r] = nums[r+1]; nums[r+1] = index; } } nums[l] = nums[r]; while (l < r && nums[l] <= temp) { l++; if (nums[l] < nums[l-1] && l < r) { index = nums[l]; nums[l] = nums[l-1]; nums[l-1] = index; } } nums[r] = nums[l]; } nums[l] = temp; return l; } vector<int> sortArray(vector<int>& nums) { int len = nums.size(); quickSort(nums, 0, len - 1); return nums; } };
结果:
从运行的结果可以看出这样做比之前只使用单一的快速排序(240ms)要节省大量的时间。
扩展:
网上也有一些介绍三路划分快速排序的方法,主要是针对待排数据中含有大量重复元素时,运用这种排序方法可能会更好,其基本的思路就是把等于枢轴量的元素放在中间区域,小于枢轴量的元素放在左边,大于枢轴量的元素放在右边。这样一次快速排序结束后中间部分的元素就确定了最终的位置,之后再递归的对左边和右边的元素进行求解就好了。这种算法主要适用于待排数据中含有大量重复元素的情况,比如在对一个学校的学生成绩进行排名时,使用这种排序方法会更加的高效。
另外,针对快速排序不稳定的性质,有的论文也给出了一种稳定的快速排序方法,具体的做法就是使用一块辅助空间来完成稳定的性质,如果只是想要使用稳定的性能的话直接使用归并排序就好了。