思想
1)快排是冒泡的升级,都是交换排序类,它增大了元素比较和交换的距离,将值较大的元素直接从前面换到后面,值比较小的元素直接从后面换到前面,而不是相邻相邻地换,从而减少了比较和交换的次数
2)每一轮排序,先选出一个枢轴,让比枢轴小的元素放在枢轴的左边,比枢轴大的元素放在枢轴的右边,再把序列分成按枢轴前后分成两个子序列,对子序列分别进行下一轮排序;不断重复上述操作,直到再也分不出子序列,排序完成
3)枢轴选取的一般规则:选序列的第一个或最后一个元素当枢轴,将选取的枢轴不断交换,将比它小的换到它的左边,比它大的换到它的右边,它自己也在交换中不断更换自己的位置,直到比枢轴小的元素全部都在枢轴的左边,比枢轴大的元素全部都在枢轴的右边
代码实现
1 int partiton(vector<int>& nums, int left, int right) 2 { 3 int pivotKey = nums[left];//这里选序列的第一个元素当枢轴 4 while (left < right)//left == right,一个点了就不用选了,枢轴就是它自己 5 { 6 while (left < right && nums[right] >= pivotKey) 7 --right; 8 swap(nums[left], nums[right]); 9 while (left < right && nums[left] <= pivotKey) 10 ++left; 11 swap(nums[left], nums[right]); 12 } 13 return left; 14 } 15 16 void sortQuick(vector<int>& nums, int begin, int last) 17 { 18 if (begin < last)//begin==last,一个点了就不用排序了 19 { 20 int pivot = partiton(nums, begin, last); 21 sortQuick(nums, begin, pivot - 1); 22 sortQuick(nums, pivot + 1, last); 23 } 24 }
举例:
第一轮:
时间复杂度
最好情况:每次的枢轴划分都很均匀,仅需递归logn次,再考虑比较操作为O(n),最后能得出复杂度为O(nlogn)
最坏情况:待排序序列为正序或者逆序,每次枢轴划分会分出一个空的子序列,用递归树画出来就是一棵斜树,此时需要递归n-1次,此时退化成冒泡排序,再考虑比较操作为O(n),最后能得出复杂度为O(n2)
平均情况:O(nlogn)
空间复杂度
因为递归深度的缘故,最好情况O(logn),最坏情况O(n),平均情况O(logn)
稳定性
快速排序的交换有两个方向,分别从两边向中间靠拢,这样的过程无法保证相同元素排序前后的相对顺序不变,所以是不稳定的。如:序列为5 3 4 3 8 9 10 11,中枢元素5,序列中有相同元素3,5最开始会和第二个3交换,这样第二个3就跑到第一个3地前面去了,稳定性破坏
优化
1.优化枢轴选择规则
选取序列的第一个或最后一个元素当枢轴时,如果这个元素恰好是极端元素(最大值或者最小值),会导致做枢轴划分时,分出一个空的子序列,用递归树画出来就是一棵斜树,大大增加了递归次数,所以我们最好选择一个序列中间数作为枢轴
1.1三数取中法
取序列左端、中间、右端三个数,将它们进行比较的出中间值,放在左端,再把这个左端数作为枢轴
1 int partitonThree(vector<int>& nums, int left, int right) 2 { 3 int mid = left + (right - left) / 2; 4 if (nums[left] < nums[mid])//left放left和mid之中更大的那个 5 swap(nums[left], nums[mid]); 6 if (nums[left] > nums[right])//left放left和right之中更小的那个 7 { 8 swap(nums[left], nums[right]); 9 if (nums[left] < nums[mid])//left放left和right之中更大的那个 10 swap(nums[left], nums[mid]); 11 } 12 13 int pivotKey = nums[left];//最后nums[left]为三数取中的中间值 14 while (left < right) 15 { 16 while (left < right && nums[right] >= pivotKey) 17 --right; 18 swap(nums[left], nums[right]); 19 while (left < right && nums[left] <= pivotKey) 20 ++left; 21 swap(nums[left], nums[right]); 22 } 23 return left; 24 } 25 26 void sortQuick(vector<int>& nums, int begin, int last) 27 { 28 if (begin < last) 29 { 30 int pivot = partitonThree(nums, begin, last); 31 sortQuick(nums, begin, pivot - 1); 32 sortQuick(nums, pivot + 1, last); 33 } 34 }
1.2九数取中法
三数取中对小序列来说可以有很大概率选到中间数,但是对于非常大的序列来说,这个概率又变得很小,所以可以采取九数取中法。先从序列中分三次取样,每次取样取三个数,三个样品都得出中间数,再从三个中间数中再得出中数,作为枢轴
2.优化不必要的交换
在枢轴划分时,某个元素可能会经过若干次的交换到达目的地,这些交换其实可以不需要,可以用“赋值替换”的操作来替代“交换”,使算法性能提升
1 int partitonThree(vector<int>& nums, int left, int right) 2 { 3 int mid = left + (right - left) / 2; 4 if (nums[left] < nums[mid])//left放left和mid之中更大的那个 5 swap(nums[left], nums[mid]); 6 if (nums[left] > nums[right])//left放left和right之中更小的那个 7 { 8 swap(nums[left], nums[right]); 9 if (nums[left] < nums[mid])//left放left和right之中更大的那个 10 swap(nums[left], nums[mid]); 11 } 12 13 int pivotKey = nums[left];//最后nums[left]为三数取中的中间那个数 14 while (left < right)//采用赋值替换而不是交换 15 { 16 while (left < right && nums[right] >= pivotKey) 17 --right; 18 nums[left] = nums[right]; 19 while (left < right && nums[left] <= pivotKey) 20 ++left; 21 nums[right] = nums[left]; 22 } 23 nums[left] = pivotKey;//把覆盖值换回枢轴值 24 return left; 25 }
3.对短序列排序优化
如果序列很小,快速排序反而不如简单排序中的直接插入排序性能好,因为快排用到了递归,因此改进快排。加入一个检测,检测序列的长度,如果长度大于这个值则采用快排,否则采用直接插入排序。有资料认为这个MAX_LENGTH值为7比较合适,也有认为50比较合适,可自行调整。
#define MAX_LENGTH 7 void sortInsert(vector<int>& nums) { int length = nums.size(); for (int i = 1; i <= length-1; ++i) { int j = i; while (j > 0 && nums[j] < nums[j - 1])//已排序序列的最后一个元素是最大的,从已排序序列的最后一个开始往前进行两两比较,最后形成的效果就好像找到了一个合适的位置插入一样 { swap(nums[j], nums[j - 1]); --j; } } } void sortQuick(vector<int>& nums, int begin, int last) { if ((last - begin + 1) > MAX_LENGTH) { if (begin < last) { int pivot = partitonThree(nums, begin, last); sortQuick(nums, begin, pivot - 1); sortQuick(nums, pivot + 1, last); } } else sortInsert(nums, last - begin + 1); }
4.优化递归
4.1尾递归优化
快排在尾部有两次递归,可保留一次递归,对尾递归优化
1 void sortQuick(vector<int>& nums, int begin, int last) 2 { 3 if ((last - begin + 1) > MAX_LENGTH) 4 { 5 while (begin < last)//if改成while 6 { 7 int pivot = partitonThree(nums, begin, last); 8 sortQuick(nums, begin, pivot - 1); 9 begin = pivot + 1;//最后一次递归取消 10 } 11 } 12 else 13 sortInsert(nums, last - begin + 1); 14 }
这样部分递归变成了迭代,可以减少栈的深度
4.2非递归版
使用栈stack
1 void sortQuick_nonrecursive(vector<int>& nums, int begin, int last) 2 { 3 stack<int> sta; 4 sta.push(begin); 5 sta.push(last); 6 while (!sta.empty()) 7 { 8 int right = sta.top(); 9 sta.pop(); 10 int left = sta.top(); 11 sta.pop(); 12 13 int pivot = partitonThree(nums, left, right); 14 if (left < (pivot - 1))//左子序列 15 { 16 sta.push(left); 17 sta.push(pivot - 1); 18 } 19 if ((pivot + 1) < right)//右子序列 20 { 21 sta.push(pivot + 1); 22 sta.push(right); 23 } 24 } 25 }