总结
(注意:n指数据规模;k指“桶”的个数;In-place指占用常数内存,不占用额外内存;Out-place指占用额外内存)
- 冒泡,插入,归并排序都是保证稳定性的,其他都不是
- 现代操作系统很少使用堆排序,因为它无法利用局部性原理进行缓存,也就是数组元素很少和相邻的元素进行比较和交换。
- 快速排序是最快的通用排序算法,它的内循环的指令很少,而且它还能利用缓存,因为它总是顺序地访问数据。它的运行时间近似为 ~cNlogN,这里的 c 比其它线性对数级别的排序算法都要小。
工程排序特点:
工程排序中,有2个点:
- 当类型是自己定义的,用归并排序,而不用快排,因为要保证稳定性,当类型是内置类型的时候,用快排,因为快排比归并快(虽然复杂度一样,但是系数小,而且额外空间复杂度小 归并是O(n),快排是O(logn) 不是O(1),因为要记住那个断点!!)
- 当要比较的数量小的时候(通常是小于60),用插入排序,因为这时候插入排序更快,虽然复杂度高,但因为系数小,所以数量小的时候,速度更快
leetcode 验证各种排序算法
[https://blog.csdn.net/speargod/article/details/121931432]
1. 快速排序
简介
快速排序是C.R.A.Hoare于1962年提出的一种划分交换排序。它采用了一种分治的策略,通常称其为分治法(Divide-and-ConquerMethod)。
思想
该方法的基本思想是:
- 先从数列中取出一个数作为基准数。
- 分区过程,将比这个数大的数全放到它的右边,小于或等于它的数全放到它的左边。
- 再对左右区间重复第二步,直到各区间只有一个数。
就是先搞定一个数 把它放到正确的位置,再继续搞。
时间复杂度:平均O(NlgN),最坏O(N^2)
- 最坏的情况是:
第一次从最小的元素切分,第二次从第二小的元素切分。
N+N-1+N-2+…+2+1 = N^2- 最好的情况是:
每次都正好将数组对半分,这样递归调用次数才是最少的。复杂度为 O(NlogN)。所以要把数组随机洗牌 确保有序的可能性贼小.
a) 三向切分快排,代码(主要)
void quicksort(vector<int> &vec, int left,int right){
if(left>=right)
return;
int less=left,more=right+1;
int cur=left+1;
int pivot=vec[left];
while(cur<more){
if(ves[cur]<pivot) swap(vec[cur++],vec[++less]);
else if(vec[cur]>pivot) swap(vec[--more],vec[cur]);
else ++cur;
}
swap(vec[less],vec[left]);
quicksort(vec,left,less-1);
quicksort(vec,more,right);
}
三向切分的partition。主要对有大量重复元素的数组好使,这里我们要把数组分成3个区,,小于,等于,大于主元的。
tips: 对于3向切分,我们时刻记住3点即可。
- 一直维护3个int,less是小于主元的最后一个,more是大于主元的第一个,cur是当前正在检查的。
- 假如不传主元,那么我们把a[left]当作主元,那么为了方便起见,把数组从 a[left+1]开始算,但最后别忘了,要swap主元。假如传了主元,那么我们数组从a[left]开始算,这次最后就不用swap了。
- 把 - - more交换过来的时候,cur不动,因为不知道换过来的是什么货色。
//快排 面试版
void quicksort(vector<int>& vec){
quicksort(vec,0,vec.size()-1);
return;
}
void quicksort(vector<int>& vec,int l,int r){
if(l>=r)
return;
int pivot = a[left];//选第一个元素作为主元
int small = left;
//small指向比pivot小的元素段的尾部(那为啥不是left-1呢?
// 因为我们主元选的是a[left],所以数组实际是从a[left+1]开始,不过同样这样处理,我们最后就要交换a[small]和主元a[left])
//遍历数组,如果遇到比主元小的元素依次放在前半部分
for(int i = left+1; i <= right; i++){
if(a[i] < pivot)
swap(a[i], a[++small]);
}
swap(a[left], a[small]);//注意需要交换!
quicksort(vec,l,small-1);
quicksort(vec,small+1,r);
}
不传主元的partition实现
先实现不传主元,直接把a[left]当主元的版本。
vector<int> partition2(int a[],int left,int right){
int pivot = a[left]; //主元
int less=left, more=right+1,cur=left+1; //less是小于主元的最后一个,more是大于主元的第一个,cur是当前正在检查的,同样我们把数组从 a[left+1]开算.
//
【left,less】 全是小于主元的
【less+1,cur-1】是等于主元的
【cur,more-1】都是我们没有遍历过的。
【more,right】全是大于主元的
while(cur<more){
//假如当前小于主元就把它给扔小于区,cur向前移动,往对应的操作就是++less,less往外扩。
if(a[cur]<pivot){
swap(a[++less],a[cur++]); //一个是前++ 一个是后++
}
//假如当前大于主元就把它给扔到大于区,但是cur不移动,因为swap过来的是more-1的元素,这个元素我们没检查过!!!
else if(a[cur]>pivot){
swap(a[cur],a[--more]);
}
//等于主元,自然就是往前移动
else{
++cur;
}
}
swap(a[less],a[left]); //注意,因为我们之前不把a[left]当做数组的一部分,所以最后要交换主元。
vector<int> res;
res.push_back(less);
res.push_back(more);
return res;
}
传主元版,通常用于只partition,不完全sort的题目
传主元版,通常用于只partition,不完全sort的题目
注意和上面不传主元的区别
void partition4(int a[],int left,int right,int pivot){
int less=left-1, cur=left, more=right+1;
//int small=left, more=right+1,cur=left+1; 观察和不传主元有什么区别,应该很明白了
while(cur<more){
if(a[cur]<pivot){
swap(a[++less],a[cur++]);
}
else if(a[cur]>pivot){
swap(a[--more],a[cur]);
}
else{
++cur;
}
}
//注意最后不用swap
return vector 这里就不写了
}
b)基本快排,随便看看吧
有单向切分,和双向切分之分
//基本快排 把第一个元素当主元
写快排要注意两件事:1.指针不要越界 2.两端出发的 最后哪个跟主元交换
void quicksort1(int a[],int left,int right){
//只有1个元素
if(left>=right)
return;
int p1=partition(a,left,right);
quicksort1(a,left,p1-1);
quicksort1(a,p1+1,right);
}
双向扫描的partition
int partition1(int a[],int left,int right){
int pivot =a[left]; //选最左边的为主元
int p1 = left,p2 =right+1;
//因为后面用的是++ 所以选这两个初值p1和p2代表的都是已经符合条件的
所以下面的代码中p1>=p2的时候 break
//主元的左边都是小于等于主元的, 右边都是大于等于主元的
while(true){
//左边的指针要找到一个大于等于主元的(等于为了防止极端情况,元素全部相同的时候),所以小于主元不停,同时可能数组越界,
//比如a[left]是最大的,所以需要检查
while(a[++p1]<pivot && p1!= right)
;
//右边的指针要找到一个小于等于主元的,所以大于主元不停,而且不会发生数组越界,因为a[left]是我们的主元,相当于哨兵
while(a[--p2]>pivot)
;
if(p1>=p2)
break;
swap(a[p1],a[p2]);
}
//必须把主元跟p2交换,因为选的left做主元,而要达到的目的是主元的左边都是小于等于主元的, 右边都是大于等于主元的,而p1停在一个大于等于主元的位置,p2停在一个小于等于主元的位置,要是最后p1跟主元换,那最左边就会是一个大于等于主元的数,也就是可能大于,那就gg。
swap(a[left],a[p2]);
return p2;
}
单向扫描的partition
int partition2(int a[], int left, int right)
{
int pivot = a[left];//选第一个元素作为主元
int small = left;
//small指向比pivot小的元素段的尾部(那为啥不是left-1呢?因为我们主元选的是a[left],所以数组实际是从a[left+1]开始,不过同样这样处理,我们最后就要交换a[small]和主元a[left])
//遍历数组,如果遇到比主元小的元素依次放在前半部分
for(int i = left+1; i <= right; i++){
if(a[i] < pivot)
swap(a[i], a[++small]);
}
swap(a[left], a[small]);//注意需要交换!
return small;
}
c) 三数中值快排,且结合插入排序
//三数中值 当主元,且小数组进行插入排序的优化快排
int median3(int a[],int left,int right){
int center = left+(right-left)/2;
if(a[left]>a[center])
swap(a[left],a[center]);
if(a[left]>a[right])
swap(a[left],a[center]);
if(a[right]<a[center])
swap(a[right],a[center]);
swap(a[center],a[right-1]);
return a[right-1]; //实现三数中值分割,并且a[right-1]当做主元,并且a[left]<a[right-1]<a[right] ,把两个当做了哨兵,避免了越界检查
}
const int cutoff=3;
void quicksort2(int a[],int left,int right){
if(left +cutoff<=right){ //大数组用快排
int pivot = median3(a,left,right);
int p1 = left,p2=right-1;
while(true){
while(a[++p1]<pivot){} //因为median3的实现,对数组动了手脚,所以两个都不需要越界检查
while(a[--p2]>pivot){}
if(p1<p2)
swap(a[p1],a[p2]);
else
break;
}
swap(a[p1],a[right-1]); //这里换p1,不换p2,就是因为主元的位置在右边!,所以换哪个跟主元的位置有关
quicksort2(a,left,p1-1);
quicksort2(a,p1+1,right);
}
else
insertsort(a,left,right); //小数组用插入排序
}
d) 快排非递归版
其实就是用栈来保存下一次要排的两个部分的两个左右指针。
void QuickSort(int *a, int left,int right)
{
if (a == NULL || left < 0 || right <= 0 || left>right)
return;
stack<int>temp;
int i, j;
//(注意保存顺序)先将初始状态的左右指针压栈
temp.push(right);//先存右指针
temp.push(left);//再存左指针
while (!temp.empty())
{
i = temp.top();//先弹出左指针
temp.pop();
j = temp.top();//再弹出右指针
temp.pop();
if (i < j)
{
int k = Pritation(a, i, j);
if (k > i)
{
temp.push(k - 1);//保存中间变量
temp.push(i); //保存中间变量
}
if (j > k)
{
temp.push(j);
temp.push(k + 1);
}
}
}
}
//快排 面试版
void quicksort(vector<int>& vec){
quicksort(vec,0,vec.size()-1);
return;
}
void quicksort(vector<int>& vec,int l,int r){
if(l>=r)
return;
int pivot=vec[l];
int less=l;
for(int i=l+1;i<=r;++i){
if(vec[i]<pivot)
swap(vec[i],vec[++less]);
}
swap(vec[l],vec[less]);
quicksort(vec,l,less-1);
quicksort(vec,less+1,r);
}