复习了几种常见排序算法,整理起来以加强印象。
按稳定性的不同分为:
稳定排序:插入排序,冒泡排序,归并排序,基数排序。
不稳定排序:选择排序,快速排序,希尔排序,堆排序。
1.冒泡排序
思想:每一轮排序都把最大或最小的元素往后调,如果有n个元素,则一共会进行n-1轮排序,每一轮排序会比较n-1-i次。
时间复杂度:O(N*N)
相等元素的前后顺序在排序过程中不会改变,是稳定排序。
代码:
void bubblesort(int a[],int n)
{
int i,j;
for(i=0;i<n-1;i++)
{
for(j=0;j<n-1-i;j++)
{
int temp;
if(a[j]>a[j+1])
{
temp=a[j];
a[j]=a[j+1];
a[j+1]=temp;
}
}
}
}
2.选择排序
思想:第一轮排序在所有元素中选出最大或最小的元素放在第一位,第二轮排序在剩下的元素中选出最大或最小的元素放在第二位,以此类推。
时间复杂度:O(N*N)
相等元素的前后顺序在排序过程中会改变,是不稳定排序。
代码:
void select_sort(int a[],int n)
{
int i,j;
int minindex;//这里是按升序排列
int temp;
for(i=0;i<n-1;i++)
{
minindex=i;
for(j=i+1;j<n;j++)
{
if(a[minindex]>a[j])
{
minindex=j;
}
}
temp=a[i];
a[i]=a[minindex];
a[minindex]=temp;
}
}
3.插入排序
思想:假设第一位是排好序的,从第二位元素开始,第i轮排序把第i个元素插入到前i-1位已排好序的元素中(即从第i-1位元素开始往前比较, 直到找到自己的位置)。
时间复杂度:O(N*N)
相等元素的前后顺序在排序过程中不会改变,是稳定排序。
代码:
void insert_sort(int a[],int n)
{
int key;
int i,j;
for(i=1;i<n;i++)
{
key = a[i]; //默认前i-1个元素是排好序的,现在要将a[i]插入前i-1个元素中,使前i个元素是排好序的
if(key<a[i-1])//如果第i个元素比第i-1个元素大,就肯定比i-1个元素之前的元素都大
{
for(j=i-1;j>=0&&a[j]>key;--j)
{
a[j+1]=a[j];//把大于key的元素都后移一位
}
a[j+1] = key;
}
}
}
思想:将第一个元素设置成枢纽元t,挖出来(挖坑法),此时第一个元素为空,设置i,j两个标记,i初始指向第一个元素,j初始指向最后一个元素。
当i<j时,反复以下循环过程:
1.将j向右移动,直到遇到一个小于t的数,将此数挖出并填到上一个坑(即挖第一个枢纽元t所产生的坑,i往右移一步),此时j指向的元素为空。
2.将i往左移,直到遇到一个大于t的数,将此数挖出并填到上一个坑(即上次挖坑后j指向的空位置,j往左移一步),此时i指向的元素为空。
当i等于j时,跳出循环,并把t填到i指向的坑中,这时,元素t的左边全都小于等于t,右边全都大于等于t。继续分别将左边和右边元素进行快排(分治思想)。
时间复杂度:最理想是O(N*logN),最差是O(N*N)
相等元素的前后顺序在排序过程中会改变(容易发生在枢纽元最后归位的时候),是不稳定排序。
代码:
void quicksort(int a[],int r,int l)
{
int i=r;
int j=l;
int temp = a[r];
if(i<j)
{
while(i<j)
{
while(a[j]>=temp&&i<j)j--;
if(i<j)a[i++]=a[j];
while(temp>=a[i]&&i<j)i++;
if(i<j)a[j--]=a[i];
}
a[i]=temp;
quicksort(a,r,i-1);
quicksort(a,i+1,l);
}
}
5.归并排序
思想:采用分治思想,首先递归将数组进行二分操作,分到不可再分的程度(即每一个分组都包含一个元素),然而将相邻的分组进行有序合并(递归回来),最后得到的数组即是排好序的。
时间复杂度:最理想、平均和最差都是是O(N*logN)
相等元素的前后顺序在排序过程中不会改变,是稳定排序。
代码:
合并分组:
void merge(int a[],int first,int mid,int last,int temp[]){
int i=first,j=mid+1;
int k=0;
while(i<=mid&&j<=last){
if(a[i]<=a[j]){
temp[k++] = a[i++];
}
else{
temp[k++] = a[j++];
}
}
while(i<=mid){
temp[k++]=a[i++];
}
while(j<=last){
temp[k++]=a[j++];
}
for(int v=0;v<k;v++){
a[first+v] = temp[v];
}
}
进行归并排序:
void mergesort(int a[],int first,int last,int temp[]){
if(first < last)
{
int mid = (first + last) / 2;
mergesort(a, first, mid, temp); //左边有序
mergesort(a, mid + 1, last, temp); //右边有序
merge(a, first, mid, last, temp); //再将二个有序数列合并
}
}
6.希尔排序
思想:属于插入排序的一种,又称缩小增量排序。对于一个长度为n的待排序数列,取一个小于n的整数gap(增量)将整个数列分为若干子数列,数列中所有距离为gap的数分在同一个子数列中。然后对每一个子数列进行插入排序,一趟排序过后,该子数列变成了有序的。逐步减少gap的值,直到gap=1时,即所有数字被放到一个子数列中进行插入排序,最后得到的数列是有序的。
时间复杂度:希尔排序的时间复杂度与选取的增量有关,若增量为1,希尔排序则退化成直接插入排序,时间复杂度为O(N*N),若采用Hibbard序列2^k-1,时间复杂度为O(N^1.5)。
相等元素的前后顺序在排序过程中会改变,是不稳定排序。
代码:
void shellsort(int a[], int n) //n为元素个数
{
for(int gap=n/2;gap>0;gap/=2){
for (int j = gap; j < n; j++) {//从数组的第gap个元素开始
if(a[j] < a[j - gap]) {//同一组内进行插入排序
int temp = a[j];
int k = j - gap;
while (k >= 0 && a[k] > temp) {
a[k + gap] = a[k];
k -= gap;
}
a[k + gap] = temp;
}
}
}
}
7.堆排序
思想:构造数组对应的最小堆或最大堆(从非叶子结点开始进行下滤操作),删除堆的第0个数据,用最后一个结点填充,再进行下滤操作。最后输出数组即是排好序的。小根堆为降序,大根堆为升序。
时间复杂度:由于每次重新恢复堆的时间复杂度为O(logN),共N - 1次重新恢复堆操作,再加上前面建立堆时N / 2次向下调整,每次调整时间复杂度也为O(logN)。二次操作时间相加还是O(N * logN)。故堆排序的时间复杂度平均和最坏都为O(N*logN)。
相等元素的前后顺序在排序过程中会改变,是不稳定排序。
代码:
下滤操作:
void minHeapFixdown(int a[],int i,int n){
int temp = a[i];
int j = 2*i+1;
while(j<n){
if(j+1<n&&a[j+1]<a[j])j++;
if(a[j]>=temp)break;
a[i]=a[j];
i=j;
j=2*i+1;
}
a[i] = temp;
}
构造小根堆:
void makeMinHeap(int a[],int n){
for(int i=n/2-1;i>=0;i--){
minHeapFixdown(a,i,n);
}
}
实例:
int main(){
int a[10]={456,23,12,3,1,0,5,7,-1,324};
int i;
int n=10;
makeMinHeap(a,10);
for(i=n-1;i>=1;i--){
swapnum(a[0],a[i]);
minHeapFixdown(a,0,i);
}
for(int i=0;i<n;i++){
cout<<a[i]<<" "<<endl;
}
return 0;
}
其中swapnum函数:
void swapnum(int &a,int &b){
a = a^b;
b = a^b;
a = a^b;
}