文章目录
前言
本文介绍交换排序,包括冒泡排序和快速排序。
一、交换排序
1.1交换排序基本思想
所谓交换,就是根据序列中两个元素值的大小比较结果来对换这两个元素在序列中的位置
1.1.1交换排序特点
将值较大的元素向序列的尾部移动,值较小的元素向序列的前部移动。
1.1.2为了选出两个元素比较大小,我们该怎么选?选的规律是什么?
①首先最容易想到的便是:数组中相邻位置上的值可以比较大小,所以第一种思路就是选位置相邻两数比较大小(然后判断是否交换),规律就是数组中每个相邻位置都比较一次。用这样的思路走一遍无序的数组,会发现我们数组中最大的数被“沉底”了!
②然后又有人想到了:我们选定一个数组中的一个数,找到比它大的值便放到它的右边,找到比它小的便放到左边。这种方式走过一遍发现我们的选定的该值的位置的左边都是比它小的,它位置的右边都是比它大的。
用这两种不同的思路实现的代码,便是我们两大鼎鼎有名的排序:思路①冒泡排序,思路②快速排序。
2.1利用思路实现代码
2.1.1冒泡排序(BubbleSort)
冒泡排序无论思路还是代码都十分简单,我们先尝试写遍历一次数组,使一个元素沉底的代码:
{
for(int j=1;j<n;j++)
{
if(a[j]<a[j-1])
{
Swap(&a[j],&a[j-1]);
}
}
}
然后我们再加一层循环,用来控制我们每次去遍历比较的范围:
void BubbleSort1(Datatype* a, int n)
{
for(int i=0;i<n;i++)
{
for(int j=1;j<n-i;j++)
{
if(a[j]<a[j-1])
{
Swap(&a[j],&a[j-1]);
}
}
}
}
这样我们就完成了一次最简单的冒牌排序,我们一样可以对它进行一点优化:当它没有进行交换的时候,数组便是有序的,此时我们可以直接结束排序。
void BubbleSort1(Datatype* a, int n)
{
for(int i=0;i<n;i++)
{
bool exchange=false;
for(int j=1;j<n-i;j++)
{
if(a[j]<a[j-1])
{
Swap(&a[j],&a[j-1]);
exchange=true;
}
}
if(exchange==false)
break;
}
}
2.1.2快速排序
实现快速排序的一次预排序:我们需要选定一个“中间”值(key),用两个指针分别从数组的左右两端开始遍历(左/右指针)数组,当右指针找到比我们选定数(key)大的值,并且左指针找到比我们选定值(key)小的数,我们对此时左右指针位置上的值进行交换,重复上面的过程,直到左右指针相遇时结束,最后将我们选定的值放到指针相遇的位置,实现在数组中选定值(key)左边全部是小于key的数,key右边的数全是比它大的数。
我们先实现一次预排序:
//n是数组的元素个数,a是数组名
{
int left=0,right=n-1; //left为左指针,从数组左边开始遍历;right作为右指针从数组右边开始遍历。
int keyi=left; //我们先以数组中第一个位置的值作为选定的key值,keyi用来记录它的位置
while(left<right)
{ //当左边指针位置做keyi,右边先找大,为什么后面说
while(left<right&&a[right]>a[keyi]) //右边找小
right--; //当right位置的值大于key继续找,当有一个值比它小,即找到,停止循环
while(left<right&&a[left]<a[keyi]) //左边找大
left++; //当left位置的值小于key继续找,当有一个值比它大,即找到,停止循环
Swap(&a[left],&a[right]); //两个都找到,就交换
}
}
然后我们对已经分好的key左右两边的数组区间再次进行函数调用,这便形成了递归,最后加上我们递归的结束条件,函数递归到最后便可将数组排好序,这便是一次成功的快速排序。如下图
上图可知每层的递归都需要对n个数据遍历,且共递归logN层,所以快速排序的时间复杂度便是:O(N*logN),十分优秀!
实现代码:
void QuickSort1(Datatype* a, int left, int right)
{
if(left>=right)
return;
int begin=left,end=right;
int keyi=left;
while(left<right)
{
while(left<right&&a[right]>a[keyi])
--right;
while(left<right&&a[left]<a[keyi])
++left;
Swap(a+left,a+right);
}
Swap(a+left,a+keyi);
keyi=left;
QuickSort1(a, begin, keyi-1);
QuickSort1(a, keyi+1, end);
}
至此便完成了一次最简单的快速排序。
稍微分析一下快速排序的效率:
用以上代码实现的快速排序,当我们key的值刚好趋近于整个数组中元素的中间值时:
如下图,对n个元素的数组使用快速排序时,key的值趋于数组中间,我们左半边递归的函数会比左边提前结束,函数递归并不会到达很深层。
但是当我们的数组有序,也就是每层递归我们取的key(keyi=left)都是数组中最小的数时:
我们用每次分割出的左边区间都只有一个,右边分割的数组区间却有n-1个,结果便是每层的左区间都不会进入递归,右边区间却过度的递归深入(递归函数迟迟无法结束),递归的过深后,递归函数本身被调用的过多,栈帧调用过多,便会栈溢出。如下图
由此可见,当数组有序时(每层递归取的key都是最小的),我们需要递归n层,而取的值趋近数组的中间值时,我们只需要递归大概logN层。
所以为了避免递归过深造成的栈溢出结果,我们需要使我们每层递归取的key值趋近于数组中间(至少不要是最小的)。
第一种优化法:随机取key。
void QuickSort2(Datatype* a, int left, int right)
{
if(left>=right)
return;
int begin=left,end=right;
int midi=left+rand()%(right-left);
Swap(a+midi,a+left);
int keyi=left;
while(left<right)
{
while(left<right&&a[right]>a[keyi]
--right;
while(left<right&&a[left]<a[keyi])
++left;
Swap(a+left,a+right);
}
Swap(a+left,a+keyi);
keyi=left;
QuickSort1(a, begin, keyi-1);
QuickSort1(a, keyi+1, end);
}
第二种优化法:三数取中。
我们每次都取数组最左、最右和中间的值比较,这样便可以避免数组是有序的情况了。
int Getmidnum(a,left,right)
{
int mid=(left+right)/2;
if(a[mid]>a[left])
{
if(a[mid]<a[right])
return mid;
else if(a[left]>a[right])
return left;
else
return right;
}
else
{
if(a[left]<a[right])
return left;
else if(a[mid]>a[right])
return mid;
else
return right;
}
}
void QuickSort3(Datatype* a, int left, int right)
{
if(left>=right)
return ;
int begin=left,end=right;
int midi=Getmidnum(a,left,right);
Swap(a+left,a+midi);
int keyi=left;
while(left<right)
{
while(left<right&&a[right]>a[keyi])
--right;
while(left<right&&a[left]<a[keyi])
++left;
Swap(a+left,a+right);
}
Swap(a+left,a+keyi);
keyi=left;
QuickSort3(a,begin, keyi-1);
QuickSort3(a,keyi+1, end);
}
总结
完成了交换排序分析思路和实现,包括冒泡排序和快速排序,同时还分析了快速排序数组有序时的过度递归情况。
本文章为作者的笔记和心得记录,顺便进行知识分享,有任何错误请评论指点:)。