本宝将要总结排序这一章经常考的基础题目,经常回顾总结是个好习惯,如果有错误的地方多多指教。
插入排序
1.直接插入排序
基本思想:直接插入排序是一种最简单的排序方法,依次将每一个元素插入到一个有序区中。假设元素存放在a[0…n-1]中,a[0…i-1]是已经排好的元素,a[i…n-1]是未排序的元素,插入排序将a[i…n-1]插入到a[0…i-1]中,使之成为有序的。插入a[i]的过程就是完成排序中的一趟,随着有序区的扩大,全部元素完成排序。
基本算法的伪代码:
viod Insertsort(Elemtype a[],int n)
{
int i, j;
Elemtype temp ;
for(i=2;i<n-1;i++)
{
temp=a[i]; //a[i]是要插入的元素;
j=i-1; //j指向有序区最后一个元素;
while(j>=0&&temp.key<a[j].key)
{
a[j+1]=a[j];
j--;
}
a[j+1]=temp; //在j+1处插入temp;
}
}
时间复杂度:当初始元素正序时,执行效率最高,此时的时间复杂度为O(n),当初始元素反序时,执行效率最差,时间复杂度为O(n^2)。
空间复杂度:仅仅使用了常数个辅助空间,因此是O(1)。
稳定性:由于每次插入时候都是从后向前比较再移动,所以不会出现相同元素相对位置发生变化的情况,因此是一个稳定的排序算法。
每趟是否归位一个元素:否
适用性:适用于顺序存储和链式存储的线性表,当为链式存储时,可以从前往后查找指定元素的位置。
2.折半插入排序
基本思想:折半插入将比较和插入区分开来,在前面的直接插入的基础上,加入了折半查找的操作,每次从前面的有序子表寻找插入位置时候,用折半查找的思想,然后再进行插入(插入部分同直接插入排序一样,统一后移元素)。
基本算法的伪代码:
viod Insertsort2(Elemtype a[],int n)
{
int i,j,low,high,mid;
Elemtype temp ;
for(i=2;i<n-1;i++)
{
temp=a[i];
low=0;high=i-1;
while(low<=high)
{
mid=(low+high)/2;
if(temp.key>a[mid].key)
low =mid+1;
else
high =mid-1;
}
for(j-i-1;j>=high+1;j--)
{
a[j+1]=a{j];
}
a[high+1]=temp;
}
}
时间复杂度:折半插入仅仅减少了比较元素的次数,而元素的移动次数不变,因此时间复杂度仍为O(n^2)。
空间复杂度:仅仅使用了常数个辅助空间,因此是O(1)。
稳定性:由于每次插入时候都是从后向前比较再移动,所以不会出现相同元素相对位置发生变化的情况,因此是一个稳定的排序算法。
每趟是否归位一个元素:否
适用性:适用于顺序存储的线性表。
3.希尔排序
基本思想:直接插入排序适用于元素不多,基本有序的顺序表,在直插的基础上,提出了Shell排序,又称缩小增量排序。将待排序元素按照设定好的间隔分成若干个子表,先对每个子表进行直插,重复上述过程直到取得间隔等于1 ,此时元素表已经基本有序,相当于对整体再进行一次直接插入排序。希尔提出的方法是d1=n/2,d(i+1)=(d(i))/2,最后一个增量等于1。
基本算法的伪代码:
viod Insertsort(Elemtype a[],int n)
{
int i, j,d;
Elemtype temp ;
for (d=n/2;d>=1;d++)
{
for(i=d+1;i<n-1;i++)
{
temp=a[i];
j=i-d;
while(j>=0&&temp.key<a[j].key)
{
a[j+d]=a[j];
j=j-d;
}
a[j+d]=temp;
}
}
}
时间复杂度:当初始元素正序时,执行效率最高,当初始元素反序时,执行效率最差,最坏时间复杂度为O(n^2)。
空间复杂度:仅仅使用了常数个辅助空间,因此是O(1)。
稳定性:当元素被划分到不同的子表时,会改变它们的相对次序,是一个不稳定的排序算法。
每趟是否归位一个元素:否
适用性:适用于顺序存储的情况。
选择排序
1.简单选择排序
基本思想:每一趟在待排序序列中选择一个最小的元素与待排序的第一个元素交换。
基本算法的伪代码:
void Slectsort(Elemtype a[],int n)
{
int i,j,min;
for(i=0;i<n;i++)
{
min=i;
for(j=i+1;j<n;j++)
{
if(a[min].key>a[j])
min=j;
}
if(min!=i)
{
swap(a[i],a[min];
}
}
}
时间复杂度:当初始元素正序时,反序时,都需要进行相应的比较,正序并不会减少比较次数,时间复杂度在任何情况下都为O(n^2)。
空间复杂度:仅仅使用了常数个辅助空间,因此是O(1)。
稳定性:不是一个稳定的排序算法。
每趟是否归位一个元素:是
适用性:适用于顺序存储和链式存储的线性表,当为链式存储时,可以从前往后查找指定元素的位置。
2.堆排序
基本思想:主要是进行建堆和调整堆的操作。重点是如何手动调整或者建一个初始堆。大根堆是根节点中的元素最大,小根堆是根节点中的元素最小,堆经常被用来实现优先级队列,优先级队列在操作系统中有广泛的应用。堆排序的关键是构造初始堆,对初始序列建堆是一个反复筛选的过程。N个结点的完全二叉树,最后一个结点是第n/2向下取整个孩子,从它开始依次往前进行筛选。看该节点的值是否大于其左右孩子结点,若不是,就将左右结点的最大值与之交换,交换后可能会破坏下一级的堆,于是继续采用上述方法构造下一级的堆,直到以该结点为根的子树构成堆为止,反复操作直到根结点。
基本算法的伪代码:
void Build(Elemtype a[],int n)
{
for(int i=len/2;i>0;i++)
{
Adjust(a,i,len)
}
}
void Adjust(Elemtype a[],int k,int len)
{
a[0]=a[k];
for(i=2*k;i<=len;i*=2)
{
if(i<len&&a[i]<a[i+1]}
i++;
if(a[0]>a[i])
break;
else
{
a[k]=a[i];
k=i;
}
}
a[k]=a[0];
}
void Heapsort(Elemtype a[],int len)
{
Build(a,len);
for(i=len;i>1;i--)
{
swap(a[i],a[1]);
Adjust(a,1,i-1); //把剩余的i-1个元素整理成堆
}
}
时间复杂度:建堆时间是O(n),之后有n-1次调整操作,每次调整的时间复杂度为O(h),故在最好最坏和平均情况下,堆排序的时间复杂度为O(nlog2n)。
空间复杂度:仅仅使用了常数个辅助空间,因此是O(1)。
稳定性:在进行筛选时,可能把后面的关键字排到前面,是一个不稳定的排序算法。
每趟是否归位一个元素:是
适用性:在排序过程中,将其看成一棵完全二叉树的顺序存储结构。
交换排序
1.冒泡排序
基本思想:假设待排序表长n,从后往前或者是从前往后两两比较相邻的元素,若为逆序,则交换他们,直到序列比较完,我们称之为一趟冒泡。设置一个交换标识,如果本次冒泡标识没有改变,说明序列有序了。
基本算法的伪代码:
void Bubblesort(Elemtype array[],int n)
{
int i,j;
tag=false;
for(i=0;i<n;i++)
{
for(j=n-1;j>i;j--)
{
if(array[j]<array[j-1])
{
swap(array[j-1],array[j]);
tag=true;
}
else
tag=false;
}
if (tag==false)
return ;
}
}
时间复杂度:当初始元素正序时,执行效率最高,比较次数是N-1,此时的时间复杂度为O(n),当初始元素反序时,执行效率最差,平均和最坏时间复杂度都为O(n^2)。
空间复杂度:仅仅使用了常数个辅助空间,因此是O(1)。
稳定性:是一个稳定的排序算法。
每趟是否归位一个元素:是
适用性:适用于顺序存储和链式存储的线性表。
2.快速排序
基本思想:快速排序是对冒泡排序的一种改进,其基本思想是基于分治法的,在待排序元素中找第一个元素作为枢轴,左边的元素都小于它,右边的元素都大于它,表中的元素都会被枢轴值一分为二。然后递归的对其左右两边的表进行排序,直到每一部分只有一个元素或者空为止,此时所有的元素都放在了最终位置上。快速排序关键在于划分操作。
基本算法的伪代码:
viod QuickSort(Elemtype array[],int low,int high)
{
if(low<high)
{
int pivot=Partion(array,low,high);
QuickSort(array,low,pivot-1);
QuickSort(array,pivot+1,high);
}
}
两种分割算法:
1.取数组首个元素为枢轴值,取两个指针low和high,初始值设置为第二个元素和最后一个元素的下标,移动指针,从high的位置开始找一只找到比枢轴值小的一个元素,从low的位置开始找,找到一个比枢轴值大的元素,交换low和high的值,重复操作直到low大于high。
int Partion(Elemtype array[],int first,int last)
{
int low=first+1,int high=n-1;
int pivot=array[first];
while(low<=high)
{
while(low<=high&&array[high]>pivot)
high--;
while(low<=high&&array[low]<pivot)
low++;
swap(array[high],array[low]);
high--;low++;
}
swap(array[first],array[high]);
return high;
}
2.取数组首个元素为枢轴值,取两个指针low和high,初始值设置为第一个元素和最后一个元素的下标,移动指针,从high的位置开始找一只找到比枢轴值小的一个元素,将其放在low位置,从low的位置开始找,找到一个比枢轴值大的元素,将其放在high位置,重复操作直到low大于等于high。把元素放到low位置,返回枢轴存放的最后位置。
int Partion2(Elemtype array[],int first,int last)
{
int low=first,int high=n-1;
int pivot=array[first];
while(low<=high)
{
while(low<=high&&array[high]>pivot) --high;
array[low]=array[high];
while(low<=high&&array[low]<pivot) ++low;
array[high]=array[low];
}
array[low]=pivot;
return low;
}
时间复杂度:最好和平均时间复杂度为O(nlog2n),当初始元素基本有序或者基本逆序时,执行效率最差,时间复杂度为O(n^2)。当递归过程中划分的子序列的规模较小时可以直接采用直插,或者尽量选取一个能够中分数据的枢轴,或者随机选择(rand() % n)这样可以提高算法的效率。快速排序是所有排序算法里平均性能最优的算法。
空间复杂度:由于快速排序是递归的,需要借助一个辅助栈来保存每一层递归调用必要信息,其容量和递归调用的最大深度一致,空间复杂度最坏情况下是O(n),平均情况下是O(log2n)。
稳定性:是一个不稳定的排序算法。
每趟是否归位一个元素:是
适用性:适用于顺序存储。
归并排序
基本思想:
把长度为n的线性表划分成n/2想上取整个长度为2或1的有序表,再两两归并,得到长度为4或者5的n/4向上取整个有序表,如此重复,直到合并成长度为n的一个有序表。由于归并操作是在相邻的两个有序表中进行的,因此也叫二路归并。
首先介绍将两个有序的子表合并成一个有序表的算法:
Elemtype *B=(Elemtype*)malloc((n+1)*sizeof(Elemtype));
void Merge(Elemtype array[],int mid,int low,int high)
{
for(int k=low;k<=high ;k++)
B[k]=array[k];
for(i=low,j=mid+1,k=i;i<=mid&&j<=high;k++)
{
if(B[i]<=B[j])
array[k]=B[i++];
else
array[k]=B[j++];
}
while(i<=mid) array[k++]=B[i];
while(j<=high) array[k++]=B[j];
}
void Mergesort(Elemtype array[],int low,int high)
{
if(low<high)
{
int mid=(low+high)/2;
Mergesort(array.low,mid);
Mergesort(array,mid+1,high);
Merge(array,low,mid,high);
}
}
空间效率:在merge中,辅助单元刚好要占用N所以空间复杂度为O(N).
时间效率:每一趟时间复杂度为O(N),要进行log2n趟归并,所以时间复杂度是O(nlog2n).
稳定性:稳定
是否全局有序:否
基数排序
基数排序借助分配和收集两种操作对关键字进行排序,分为最高位优先(MSD)和最低位优先(LSD).
空间效率:借助r个队列,O®.
时间效率:基数排序需要进行d趟分配和收集,一趟分配需要O(n),一趟收集需要O(r),所以以基数排序的时间复杂度为O(d(n+r)),它与初始序列的状态无关。
稳定性:稳定
是否全局有序:否