文章目录
1 题目
题目:Sort Integer II
lintcode题号——464,难度——easy
描述:给一组整数,请将其在原地按照升序排序。可使用归并排序,快速排序,堆排序或者任何其他O(n*log n)的排序算法。
样例1:
输入:[3,2,1,4,5],
输出:[1,2,3,4,5]。
样例2:
输入:[2,3,1],
输出:[1,2,3]。
2 思路
快速排序与归并排序1一样也基于分治法,将复杂的大问题分解成小问题,自顶向下。排序过程需要在序列中取一个值作为key值,可以是序列头元素、序列中点元素、序列尾元素、随机位置,而快速排序本身又有同向双指针和对向双指针的不同实现方式,所以可以在网上看到各种不同版本的快速排序,但基本思路都是一样的。
快排的核心思想就是使用选定的key值元素将所有小于该值的元素与不小于该值的元素隔开,形成“小值
/key
/大值
”的分隔形式,然后将key左右的区间作为新的排序对象向下递归,这样不断将序列分解变小,直到问题变得足够小——只剩一个元素(单个元素自然是已经排好序的序列),这样整个大序列就自然是有序的了。
不同的快排方式本质上是使用不同手段来实现key值对整个序列的划分,同向双指针和对向双指针是两种最为常见的快排实现。
2.1 两种实现方式
2.1.1 同向双指针
在同向双指针的排序方式中,以key值为序列尾元素为例。具体步骤如下:
尾元素作key、同向双指针的实现步骤:
- 取序列的最后一个位置的元素值作为key值;
- 从头开始用序列中的元素与key值进行比较,将所有小于key的元素交换到序列前部;
- 将key值放在序列最后一个小于key的元素的下一个位置(即小值和大值的中间),这样就形成了“小值/key/大值”的形式;
- 将左边的小值区域重复上面的1、2、3步骤;(该步骤内含子递归)
- 将右边的大值区域重复上面的1、2、3步骤。(该步骤内含子递归)
- 所有递归完成之后,序列即为有序。
2.1.2 对向双指针
在对向双指针的排序方式中,以key值为序列中点元素为例。具体步骤如下:
中点元素作key,对向双指针的实现步骤:
- 去序列的中点位置的元素值作为key值;
- 从序列左边开始寻找大于等于key的元素,从序列右边开始寻找小于key的元素,交换两者;
- 重复步骤2,直到左右序号相遇交错,此时同样形成了“小值/key/大值”的形式;
- 将左边的小值区域重复上面的1、2、3步骤;(该步骤内含子递归)
- 将右边的大值区域重复上面的1、2、3步骤。(该步骤内含子递归)
- 所有递归完成之后,序列即为有序。
两种快排的实现差异在于如何取key以及使用key分隔区间的具体操作上。
2.2 图解
2.2.1 同向双指针
在同向双指针的排序方式中,以key值为序列尾元素为例。图解如下:
待排序数组
取尾元素6,为key值
以从左至右的方向,寻找小于key:6的数(数‘3’符合条件)
将找到的符合条件的第一个数交换到序列第一个位置(与自己交换,即不动)
继续向右,寻找小于key:6的第二个数,并将其交换到序列第二个位置(找到‘5’,也是与自己交换)
继续向右,寻找小于key:6的第三个数,并将其交换到序列第三个位置(找到‘4’,交换到第三个位置)
继续向右,寻找小于key:6的第四个数,并将其交换到序列第四个位置(找到‘2’,交换到第四个位置)
继续向右,寻找小于key:6的第五个数,并将其交换到序列第五个位置(找到‘1’,交换到第五个位置)
遇到序列尾部,此时序列为
将最后一个小于key的数所在位置的下一个位置与key的位置交换
此时完成了第一轮的排序,形成了“小值
/key
/大值
”的形式
左右区间再分别进行递归快排
整个序列排序完成
2.2.2 对向双指针
由于CSDN单篇帖子的字数限制,在下一篇快速排序22中补充对向双指针算法的图解。
2.2 时间复杂度
快速排序的时间复杂度并不稳定,在最好的情况下,每次取key都恰好取到这个序列的中位数,使被分隔的区间平衡,即用该key进行一轮排序之后,“小值”区间和“大值”区间具有的元素个数相同。在这种情况下快速排序可以看成是归并排序,复习一下归并排序1的时间复杂度计算:
对包含n个元素的数组,计算其时间复杂度,先分析拆分过程:
第一层将n个数划分为2个分组,每个分组包含n/2个元素;
第二层将n个数划分为4个分组,每个分组包含n/4个元素;
第三层将n个数划分为8个分组,每个分组包含n/8个元素;
……
第log n层将n个数划分为n个分组,每个分组包含1个元素。再看合并过程:
第log n层将n分组合并成n/2个分组,每个元素都会被遍历一次,每个元素耗时O(1),该层耗时O(n);
……
第3层将8分组合并成4个分组,每个元素都会被遍历一次,每个元素耗时O(1),该层耗时O(n);
第2层将4分组合并成2个分组,每个元素都会被遍历一次,每个元素耗时O(1),该层耗时O(n);
第1层将2分组合并成1个分组,每个元素都会被遍历一次,每个元素耗时O(1),该层耗时O(n);所以每层耗时为O(n),层数为log n,算法最好情况下的时间复杂度为O(n*log n)。
在最差的情况下,快速排序每次取key都恰好是最值(最大值或者最小值),假设每次取的key都是最小值,每一轮排序都只是将key值排好序,即将key值放到序列的前端,一共有n个数,每轮只是排好一个数,且每轮都要使用key值与剩下的未排序的数依次进行比较,这样时间复杂度的计算如下:
第一轮将1个数放在第一个位置,第一轮的比较次数为n-1;
第二轮将1个数放在第二个位置,第二轮的比较次数为n-2;
第三轮将1个数放在第三个位置,第三轮的比较次数为n-3;
……
第n轮将1个数放在第n个位置,第n轮的比较次数为n-n;耗时为:O[(n-1) + (n-2) + (n-3) + …… + (n-n)] = O[
n*n
+ n(1 + n)/2] = O(n*n
),算法最坏情况下的时间复杂度为 O ( n 2 ) O(n^2) O(n2)。
2.3 空间复杂度
快速排序算法是原地排序,只需要用到常量级的额外空间用来存放临时数据,算法的空间复杂度为O(1)。
3 源码
3.1 同向双指针(取尾元素为key)
C++版本:
/**
* @param A: an integer array
* @return: nothing
*/
void sortIntegers2(vector<int> &A) {
// write your code here
if (A.size() <= 1)
{
return;
}
quickSort(A, 0, A.size() - 1);
}
// 递归的定义
void quickSort(vector<int> &A, int start, int end)
{
if (start >= end) // 递归的出口
{
return;
}
int mid = partition(A, start, end);
quickSort(A, start, mid - 1); // 递归的拆解
quickSort(A, mid + 1, end);
}
int partition(vector<int> &A, int start, int end)
{
int index = start;
for (int i = start; i < end; i++)
{
if (A.at(i) < A.at(end))
{
swap(A.at(i), A.at(index++));
}
}
swap(A.at(index), A.at(end));
return index;
}
3.2 对向双指针(取中点元素为key)
注意事项:
- 由于中点元素在排序中可能会被交换移动,所以需要在每次排序前先保存key值;
- 每轮的left和right进行条件判断时,总是需要对left等于right的情况进行处理,让最后的left能够大于right以此完成当前轮的处理,否则在递归向下时候有可能出现[start, right]区间(或者[left,end]区间)与上轮区间范围相同而导致的死循环问题;
- 当前值与key进行比较的时候,对于和key值相等的元素,应该被看作需要交换,否则在key取到的值是序列中的最值的时候,会在第一轮排序中直接将left变为指向序列尾元素的下一个位置,或者让right变为-1指向序列头元素的前一位置,导致排序失败;
- 在每轮排序结束时,left的值总是大于right,所以往下递归的时候要注意需要取的子区间为[start, right]和[left, end]。
C++版本:
/**
* @param A: an integer array
* @return: nothing
*/
void sortIntegers2(vector<int> &A) {
// write your code here
quickSort(A, 0, A.size() - 1);
}
// 递归的定义
void quickSort(vector<int> &A, int start, int end)
{
if (start >= end) // 递归的出口
{
return;
}
int left = start;
int right = end;
int key = A.at(start + (end - start) / 2); // key值需提前保存在临时变量中
while (left <= right)
{
while (left <= right && A.at(left) < key) // 从左向右找到大于等于key的数
{
left++;
}
while (left <= right && A.at(right) > key) // 从右向左找到小于等于key的数
{
right--;
}
if (left <= right)
{
swap(A.at(left++), A.at(right--));
}
}
quickSort(A, start, right); // 递归的拆解
quickSort(A, left, end);
}