排序问题是数据结构与算法中一门非常重要的学问,很多的问题的解决都是以排序问题为优先的。我总结了几种常见的排序算法,给出它们在c++下的实现代码,并比较了几种算法的运算效率。
1.直接插入排序法
直接插入排序的思想是:给出一个已从小到大排好序的数组,插入第i个数据。这时候,需要从后向前比较该数据与当前位置数据的大小,找到插入位置将该数据插入,原来位置上的元素向后顺移。
这种算法具体的运行时间与待排序元素的排列顺序紧密相关。如果设元素个数为n,最理想情况下需要比较n-1次,移动0次;最不利情况下则需要比较和移动n^2/2次。直接插入排序算法的时间复杂度是O(n^2)。
直接插入排序算法是一种稳定的排序算法。所谓“稳定”,指的是对于原始数组中具有相同比较码的数据,在排序后相对位置不发生改变。它的c++实现代码如下:
void insertion_sorting(vector<int> &data)
{
for(int i=1;i<data.size();i++)
{
for(int j=i;j>=1;j--)
{
if(data[j]<data[j-1])
{
int temp=data[j];
data[j]=data[j-1];
data[j-1]=temp;
}
else break;
}
}
}
2.快速排序法
快速排序法是目前应用的最广泛的排序算法之一,在STL的<algorithm>库下的qsort()函数就是基于快速排序实现的。快速排序的思想是:给定待排序数组,不妨令最左边的数据是基准数据,从而可以将整个数组划分成两部分,一部分所有元素均小于基准数据,另一部分所有元素均大于基准数据。再将这两部分继续划分,直到只有一个元素为止。每一趟排序后,基准元素都会位于它在最后的成序数组中应当处于的位置。快速排序法的c++实现代码如下:
int partition(vector<int> &data,int left,int right)
{
int pivot=data[left],pivotpos=left;
for(int i=left+1;i<=right;i++)
{
if(data[i]<pivot)
{
pivotpos++;
int temp=data[i];
data[i]=data[pivotpos];
data[pivotpos]=temp;
}
}
data[left]=data[pivotpos];
data[pivotpos]=pivot;
return pivotpos;
}
void quick_sorting(vector<int> &data,int start,int end)
{
if(start<end)
{
int pivotpos;
pivotpos=partition(data,start,end);
quick_sorting(data,start,pivotpos-1);
quick_sorting(data,pivotpos+1,end);
}
}
快速排序算法的平均时间复杂度是O(nlogn),要低于直接插入排序法。快速排序法的时间复杂度同样受数组的初始排列影响较大。如果初始数组已排好序,则需要比较n趟才能完成,此时快速排序法的时间复杂度退化到O(n^2)。同时,在n的数量较小时,快速排序法相较其他的排序算法并没有优势。快速排序法是一种不稳定的排序算法。
3.冒泡排序法
冒泡排序法的思想是:每一轮将当前数组中具有最大比较码的数据移动到数组尾部,就如同气泡上浮一般。它的c++实现代码如下:
void bubble_sorting(vector<int> &data)
{
for(int i=0;i<data.size();i++)
{
for(int j=0;j<data.size()-i-1;j++)
if(data[j]>data[j+1])
{
int temp=data[j];
data[j]=data[j+1];
data[j+1]=temp;
}
}
}
冒泡排序法的平均时间复杂度是O(n^2)。最理想情况下,数组已自然成序,则无需移动数组元素,只需要进行n^2/2次比较即可;最坏情况下,数组成倒序,则需要比较和交换n^2/2次。冒泡排序法是一种稳定的排序算法。
可以对快速排序算法进一步优化:设置一个标志位flag=0,如果在一趟排序过程中,没有发生交换,则说明数组已成序,此时将flag值置为1,表示排序已完成。
4.希尔排序法
希尔排序法本质上也是一种插入排序法。它的主要思想是:每轮将间隔一定gap的数组元素构成若干的子序列,将每一个子序列按照直接插入排序法进行排序,每完成一轮的排序就将gap的值按一定比例缩小,直到为1,这样,就完成了所有元素的排序。在数组元素基本成序的情况下,再进行插入排序,可以使得算法的复杂度大大降低。
通常按如下表达式取gap的值:gap(i)=floor(gap(i-1)/3)+1。floor()函数表示向下取整。希尔排序的实现代码如下:
//希尔排序法,又称为缩小增量排序法。每次对间隔gap的元素进行插入排序,并取gap=floor(gap/3)+1再排序,直到gap降为1.
//随着gap的逐渐缩小,各元素距离它们的最终位置很接近,因而减少了排序移动的次数。
void shell_sorting(vector<int> &data)
{
int gap=data.size();
do
{
gap=gap/3+1;
for(int i=0;i<gap;i++) //共有gap轮排序
{
int j=0;
while(i+gap*j<data.size()) //直接插入排序
{
int index=i+gap*j;
while(index-gap>=0&&data[index]<data[index-gap])
{
int temp=data[index];
data[index]=data[index-gap];
data[index-gap]=temp;
index-=gap;
}
j++;
}
}
}while(gap>1);
}
希尔排序算法是一种很实用的排序算法,通常速度要高过直接插入排序法,尤其适用于元素数量较大的数组。希尔排序算法是一种不稳定的排序算法。
5.堆排序算法
堆排序算法是借助数据结构中“最大堆”的概念完成。最大堆是这样一种完全二叉树:任意一个节点的左右子节点的比较码均小于当前节点的比较码,且它的左右子节点也满足此特性。这样,最大堆头结点的比较码是所有节点中最大的。根据最大堆的这种特性可以完成堆排序:将所有数组元素排列成最大堆,取头结点与尾节点,交换它们的比较码。此时,尾节点处存储了最大的数组元素。再将尾节点从数组中删除,将剩余的元素重新组合成最大堆。重复上述过程,可以得到成序的数组。
在c++ stl中的<algorithm>库中有现成的建堆与交换位置的函数。利用它们完成堆排序算法:
void heap_sorting(vector<int> &data)
{
make_heap(data.begin(),data.end());
vector<int> temp(data.size());
int i=data.size()-1;
while(data.size())
{
pop_heap(data.begin(),data.end());
temp[i]=data.back();
data.pop_back();
i--;
}
data=temp;
}
堆排序算法的复杂度主要体现在初始建堆和删除最大元素后的重新调整上。初始建堆的复杂度是O(n)(根据《算法导论》一书),单次堆调整的复杂度是O(log(n)),所有元素进行堆调整的复杂度是O(nlog(n)),因此堆排序的总体复杂度是O(nlog(n))。堆排序是一个不稳定的排序算法。
6.归并排序算法
归并排序算法的原理是将原序列划分成两个长度相等的子序列,为每一个子序列进行排序,再将两个子序列合并成新的序列。归并排序的运行时间不依赖于初始序列的排布,可以避免快速排序算法的最差情况。归并排序算法的c++实现如下:
//归并排序算法,即将数组分成若干的子数组,随后将各子数组逐级合并成一个成序数组
void my_merge(vector<int> &data,int left,int right,int mid) //对left~mid和mid+1~right的数组进行合并
{
vector<int> temp(right-left+1); //辅助数组,记录原始的排列
for(int i=0;i<right-left+1;i++) temp[i]=data[left+i];
int s1=left,s2=mid+1,index=left;
while(s1<=mid&&s2<=right)
{
if(temp[s1-left]<temp[s2-left]) { data[index]=temp[s1-left]; index++; s1++; }
else { data[index]=temp[s2-left];index++; s2++; }
}
while(s1<=mid) { data[index]=temp[s1-left]; index++; s1++; }
while(s2<=right){ data[index]=temp[s2-left]; index++; s2++; }
}
void merge_sorting(vector<int> &data,int left,int right)
{
if(right-left<=0) return;
int mid=(left+right)/2;
merge_sorting(data,left,mid);
merge_sorting(data,mid+1,right);
my_merge(data,left,right,mid);
}
归并排序算法先将所有数据划分成长度相等的两个子序列,再将两个子序列进行合并。总体的算法复杂度是O(nlog(n))。归并算法是一种稳定的排序算法。
7.桶排序算法
桶排序算法的思想,是将所有数据按照大小关系放入到一个个成序的“桶”中。分别对桶内元素进行排序,最后将成序的桶相连接,完成排序。桶排序算法的c++实现代码如下:
//桶排序算法是将所有数据分散到若干桶内,再对桶中的数据按序排列,最后将所有结果合并,得到最终结果
void bucket_sorting(vector<int> &data)
{
vector<vector<int>> my_bucket(20); //桶的集合,分别存放数据在(-1.0,-0.9),(-0.9,-0.8),......(0.0,0.1),......(0.9,1.0)范围内的数据
for(int i=0;i<data.size();i++)
{
double t=data[i]*1.0/INT_MAX;
int bucket_num=(t+1.0)*10;
my_bucket[bucket_num].push_back(data[i]);
}
for(int i=0;i<my_bucket.size();i++) //逐桶进行排序
{
insertion_sorting(my_bucket[i]);
}
int current_buc=0,index=0;
for(int i=0;i<data.size();i++) //将所有桶中的数据放入原数组中
{
if(index>=my_bucket[current_buc].size())
{
current_buc++;
index=0;
i--;
}
else
{
data[i]=my_bucket[current_buc][index];
index++;
}
}
}
随机生成20组大小为1000的数组,对它们分别按照各种排序方法进行排序,计算平均时间,结果如下:
可以看出,快速排序法、希尔排序法和归并排序法的耗时最少,其次是桶排序和堆排序,效率最低的是插入排序和冒泡排序法。