赞
踩
我们知道分治法就是将原问题划分为若干个规模较小、性质相同的子问题,
然后递归着解决他们,最后进行合并。
而快速排序,也就是快排,是分治法的一个典型的排序算法。
基本原理就是对于每个数都保证比自身小的数在左边,比自身大的数载右边,那么就整体有序。
默认情况是选择需要排序的区间内的第一个元素作为标准,将该区间分成两个部分,左边小,右边大。
像上面的例子:
第一趟:区间为(4~6),故选择 4 作为标准, 将 2 移动到 4 的左边,其余元素均大于 4 。
第二趟:区间为(2~2),只能选择 2 作为标准,只有一个元素不需要操作。
另一个区间为(5~7),选择 5 作为标准,5 均小于 7 和 6,故不移动。
第三趟:区间为(7~6),故选择 7 作为标准,将 6 移动到 7 的左边。
没有剩余的区间需要排序,因此整体排序结束,这就是快排的一个大概过程。
我们可以发现,在每一个区间内我们都做的是同样一件事情,那就是使用第一个元素为标准进行分区,
只不过区间的范围不一样罢了,因此非常适合使用分治法来递归进行。
可是分治法不是划分、解决和合并三个步骤吗?这里似乎没有合并的过程。
是的,在快排算法中重要时间放在了划分问题这个部分,而在合并方面比较少,甚至说是没有。
因为只要划分到最后已经整体有序了,不需要合并的操作。
而对于每个区间,只要对分区后的序列左边进行QuickSort,右边也进行QuickSort就可以。
下面给出我的代码,仅供参考。
void quickSort(int *arr, int begin, int end){
if(end > begin){ // 当end>begin时,说明区间内一个元素都不存在
int pos = partition(arr, begin, end);
quickSort(arr, begin, pos-1);
quickSort(arr, pos+1, end);
}
}
事实上,快排中的分区方法 Partition 是一个重要组成部分,下面列出 3 种常见的方法:
① 单向扫描分区:主元x + 左指针begin + 右指针end
从左往右,若 begin 所指元素 < x,则 begin 右移;
若 begin 所指元素 > x,则 begin 和 end 所指的元素进行交换 swap,
然后 end 左移,begin 不动。
边界:当 begin > end 时,end 左边的元素均 <= x,把 x 与 end 所指元素交换即可。
参考代码:
int partition(int *arr, int begin, int end){ int mid = arr[begin]; // 通过mid将区间分为两个部分 int scan = begin+1; // scan为左指针 int bigger = end; // bigger为右指针 while(scan <= bigger){ if(arr[scan] <= mid){ // 小的元素置于左边 scan++; } else{ // 大的元素置于右边 swap(arr[scan], arr[bigger]); bigger--; } } swap(arr[begin], arr[bigger]); return bigger; }
② 双向扫描法:主元x + 左指针begin + 右指针end
先从左往右,begin 移到第一个 > x 的元素;
再从右往左,end 移到第一个 <= x 的元素;
交换 begin 和 end 所指的元素,重复以上过程。
(这种方法用的比较多)
参考代码:
int partition(int *arr, int begin, int end){
int mid = arr[begin]; // mid将分区划分为两个部分
int left = begin+1; // left为左指针
int right = end; // right为右指针
while(left <= right){
while(left <= right && arr[left] <= mid) left++; // 找到第一个 > x 的元素
while(left <= right && arr[right] > mid) right--; // 找到第一个 <= x 的元素
if(left < right){ // 符合条件则交换
swap(arr[left], arr[right]);
}
}
swap(arr[begin], arr[right]);
return right;
}
③ 三分法:主元x + 左指针begin + 等于指针equal + 右指针end
从左往右,若 begin 所指元素 < x,则 begin 与 equal 的元素交换,均右移;
若 begin 所指元素 = x,则 begin 右移;
若 begin 所指元素 > x,则 begin 与 end 的元素交换,end 右移,begin 不动。
边界:当 begin > end 时,equal 右移,x 与 equal 的元素交换。
参考代码:
void partition(int *arr, int begin, int end, int &pos1, int &pos2){ int mid = arr[begin]; // mid left right含义如上 int left = begin+1; int equal = left; int right = end; while(left <= right){ if(arr[left] == mid){ // 相同则继续扫描 left++; } else if(arr[left] < mid){ // 较小的元素与equal交换 swap(arr[equal], arr[left]); equal++; left++; } else{ // 较大的元素左右交换 swap(arr[left], arr[right]); right--; } } swap(arr[begin], arr[--equal]); pos1 = equal; // pos1保存区间[e, end]的左边界 pos2 = right; // pos2保存区间[e, end]的右边界 }
设快排所需的运行时间为T(n),那么 T(n) = T(i) + T(n-i) + n + 1。
其中 i 为每趟排序标准值的最终位置,这个位置将区间划分为两个长度分别为 i 和 n-i 的子区间继续递归。
而要确定这个最终位置而进行的 Partition 需要遍历一遍区间直至指针交错,故为 n + 1。
我们可以看到,快速排序的时间复杂度是会因为 i 取值的不同而发生变化。
最优情况:当 i 恰好位于区间的中间,即 i = n/2,那么这个区间就被平均地一分为二。
故 T(n) = 2T(n/2) + n + 1,解开这个递归表达式,时间复杂度就为 O(nlgn)。
最坏情况:当 i 恰好位于区间的边界,即 i = 0 || i = n-1,这个时候区间的长度只减少 1。
故 T(n) = T(n-1) + n +1,时间复杂度为 O(n2)。
平均情况:我们知道了 T(n) = T(i) + T(n-i) + n + 1,假设 i 出现在0~n 任意位置的概率相同,
那么 T(n) = ((T(0) + T(n-1) + n + 1) + (T(1) + T(n-2) + n + 1) + … + (T(n-1) + T(0) + n) + 1))/n。
整理得:T(n) = n + 1 + 2/n * (T(0) + T(1) + … + T(n-1))。
解开此递归式,得 T(n) = O(nlgn)。
此处快速排序的时间复杂度分析部分引用自《图解算法》 俞征武 著
在工业实践中的优化:由于 i 的值会直接影响到运行时间的快慢,因此需要对选择的标准值进行甄别。
① 三点中值法:取区间中的 begin、end 所指的元素以及序列中间元素 mid 三者之中的最小值。
这样可以一定程度较小随机性,提高了快排的性能。
② 绝对中值法:以 5 个元素为一组,使用插入排序找到中值,再将所有组的中值进行插入排序
找到多组中值间的中值,即绝对中值,这将会耗费 O(n) 的时间。
这样做会使得快排非常稳定,但是速度较慢。
③ 若待排序的序列长度较短时,转向去使用插入排序。
插入排序 O(n^2) | 快速排序 O(lgn) |
---|---|
↓ | ↓ |
n(n-1)/2 | n(lgn+1) |
当 n <= 8 时,使用插入排序的效率高于快速排序。
问题的要求很简单,就是要在 n 个数中寻找第 k 小元素。
如果使用一般暴力的方法,先排序再找到对应位置的元素,时间复杂度为最快也要 O(nlgn)。
可以更快吗?跟快排有什么关系?
我们来回想一下快排的执行过程:
每一趟进行快排的时候,我们总是选择一个数字作为标准值,就像上图中的 4 。
在经过一个时间复杂度为 O(n) 的 Partition 分区之后标准值 4 落在了位置 1。
而我们发现在最终排序好的结果中 4 的位置就是 1,发现规律了吗?
每一趟分区处理之后标准值所处的位置就是最终排序后的位置。
我们可以利用这一点在遇到落在第 k 位的标准值直接退出,因为这就是要求的元素。
比如我们需要求的是 k = 1 的元素,那么我们只要进行一趟分区就可以直接得到结果了。
但如果是 k = 2 呢?
这里是有讲究的,因为我们已经得知了元素 4 的最终位置为 1,而 k = 2 > 1,因此这个元素
一定位于 4 的右侧,我们完全可以抛弃 4 左侧的所有元素,只对右侧的元素进行下一次分区。
可以类比于二分查找,也是一次丢掉一半,我们这里也是一次丢掉一部分,因此效率提高了。
参考代码:
// 利用了快排的特性,时间复杂度大概为O(n) #include<iostream> using namespace std; int arr[100]; void swap(int &a, int &b){ if(a == b) return; a = a^b; b = a^b; a = a^b; } int partition(int *arr, int begin, int end){ // 单向扫描 int mid = arr[begin]; cout << "mid: " << mid << endl; int left = begin+1; int right = end; while(left <= right){ if(arr[left] > mid){ swap(arr[left], arr[right]); right--; } else{ left++; } } swap(arr[begin], arr[right]); return right; } void quickSort(int *arr, int begin, int end, int th){ if(begin < end){ int mid = partition(arr, begin, end); if(mid == th-1){ // 若已经把th位置的元素排好则返回 return; } else if(mid > th-1){ // 若大于th位置则向左边排序 quickSort(arr, begin, mid-1, th); } else{ // 若小于th位置则向右边排序 quickSort(arr, mid+1, end, th); } } } int main(){ int n; while(cin >> n){ for(int i = 0; i < n; i++){ cin >> arr[i]; } int th; cin >> th; quickSort(arr, 0, n-1, th); cout << arr[th-1] << endl; } return 0; }
【END】感谢观看
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。