数据结构和算法是密不可分的,特定的算法需要特定的数据结构才能发挥出应有的效率。
接下来学习一种常见的算法:排序。
首先给出几个前提:
void x_Sort(ElementType a[],int N) //x为排序的名称
- 默认排序是从小到大的;
- 只讨论基于比较的排序;(><=均有定义的)
- 只讨论内部排序;(内存里面的)
- 任意两个相等的数据,排序前后的相对顺序不变。(稳定性)
5.1 简单排序
5.1.1 冒泡排序
冒泡排序(Bubble Sort)其实很简单,就是一个交换排序,伪代码如下:
void BubbleSort(ElementTupe a[],int N)
{
for(P=N-1;P>=0;P--)
{
for(i=0;i<P;i++)//完成一整个for就确定好了最后一个位置
if(a[i]>a[i+1])
{
swap(a[i],a[i+1]);
flag = 1;
}
if(flag==0)//如果一次都没有交换过,说明是已经排好序的,直接退出就好
break;
}
}
它的优点是:实现简单、时间复杂度达到线性(对于基本排好序的数据时)、空间复杂度仅为O(1)、具有稳定性。
缺点为:时间复杂度高(对于规模较大的数据)、效率低下。
5.1.2 插入排序
插入排序(Insertion Sort)是一种简单直观的排序算法,它将待排序的元素分为已排序和未排序两部分,通过逐个将未排序的元素插入到已排序的合适位置,最终完成排序。主要思想是分而治之。伪代码如下:
void Insertion_Sort(ElementType a[],int N)
{
for(p=1;p<N;p++)
{
tmp = a[p];
for(i=p;i>0&&a[i-1]>tmp;i--)
a[i] = a[i-1];
a[i] = tmp;
}
}
它的优点和缺点与冒泡排序如出一辙。
首先解释一个词---逆序对:若 i < j,而 a[i]>a[j] ,那么 i 和 j 就是一对逆序对。从上面可以知道,冒泡排序,交换一次,就消除了一对逆序对。
- 任意N个不同的元素组成的序列平均具有N(N-1)/4个逆序对;
- 任何仅以交换相邻两元素来排序的算法,其平均复杂度为.
这也就说明了,想要提高算法效率,就得交换一次,消除多个逆序对。
5.2 希尔排序
希尔排序(Shell Sort)是一种改良版的插入排序算法,也被称为缩小增量排序。
它的基本原理是:将待排序的一组元素按一定间隔分为若干个序列,分别进行插入排序。开始时设置的“间隔”较大,在每轮排序中将间隔逐步减小,直到“间隔”为1,也就是最后一步是进行简单插入排序。
希尔排序将“间隔”定义为一组增量序列,用来分割待排序列。
void Shell_Sort(ElementType a[],int N)
{
for(D=N/2;D>0;D/=2)//增量序列
{
for(p=D;p<N;p++)//插入排序
{
tmp = a[p];
for(i=p;i>=D&&a[i-D]>tmp;i-=D)
a[i] = a[i-1];
a[i] = tmp;
}
}
}
它的优点是实现简单,相对于简单的排序,它的排序效率有所提高。
它的缺点主要还是时间复杂度的问题,此外,较于简单插入排序,它是不稳定的。
5.3 堆排序
堆排序(Heap Sort)是一种基于二叉堆数据结构的排序算法,它利用堆的特性进行排序操作。它的核心思想:利用最大堆(或最小堆)输出堆顶元素,即最大值(或最小值),将剩余元素重新生成最大堆(或最小堆),继续输出堆顶元素,重复此过程,直到所有元素全部输出,所得输出元素序列即为有序序列。
void Heap_Sort(ElementType a[],int N)
{
for(i=N/2-1;i>=0;i--)//建立最大堆
PercDown(A,i,N);
for(i=N-1;i>0;i--)
{
//删除最大堆顶
swap(&a[0],&a[i]);
PerDown(A,0,i);
}
}
它的实现较为复杂,并且具有不稳定性,但它又是高效的。
5.4 归并排序
归并排序(Merge Sort)是一种常见的排序算法,它采用分治的思想来进行排序操作(分而治之)。归并排序的主要步骤如下:
- 将待排序的序列不断二分为两个子序列,直到每个子序列只有一个元素。
- 将相邻的子序列进行合并,形成一个有序的更大的子序列。
- 重复执行步骤2,直到所有子序列都合并成一个完整的有序序列。
在合并的过程中,需要利用额外的空间来存储合并结果,此处需要使用一个临时数组。归并排序的关键在于合并操作,合并两个有序的子序列可以通过比较两个序列的元素,并按照从小到大的顺序依次放入临时数组中。 将数组分割成多个有序的子序列,逐层合并这些子序列直到得到完全有序的数组。
这是用递归写的归并排序:
void Merge(ElementType a[],ElementType TmpA[],int l,int r,int rightEnd)
//L为左边起始位置,R为右边起始位置,rightEnd为右边终点位置
{
int leftEnd,num,Tmp;
leftEnd = r - 1;//左边的终点
Tmp = l;//有序序列的起始位置
num = righEnd - l + 1;
while(l<=leftEnd&&r<=rightEnd)
{
if(a[l]<=a[r])
TmpA[Tmp++] = a[l++];
else
TmpA[Tmp++] = a[r++];
}
while(l<=leftEnd)
TmpA[Tmp++] = a[l++];
while(r<=rightEnd)
TmpA[Tmp++] = a[r++];
for(int i=0;i<num;i++,rightEnd--)
a[rightEnd] = TmpA[rightEnd];//将有序的TmpA[]复制回a[]
}
void MSort(ElementType a[],ElementType TmpA[],int l,int rightEnd)
//排序
{
int center;
if(l<rightEnd)
{
center = (l + rightEnd)/2;
MSort(A,TmpA,L,center);//递归解决左边
MSort(A,TmpA,center+1,rightEnd);//递归解决右边
Merge(A,TmpA,L,center+1,rightEnd);//合并两端
}
}
void MergeSort(ElementType a[],int N)
//归并排序
{
ElementType *TmpA;
TmpA = (ElementType *)malloc(sizeof(ElementType)*N);
if(TmpA!=NULL)
{
MSort(a,TmpA,0,N-1);
free(TmpA);
}
else
printf("空间不足\n");
}
接下来是非递归的:
void Merge(ElementType a[],ElementType TmpA[],int l,int r,int rightEnd)
//L为左边起始位置,R为右边起始位置,rightEnd为右边终点位置
{
int leftEnd,num,Tmp;
leftEnd = r - 1;//左边的终点
Tmp = l;//有序序列的起始位置
num = righEnd - l + 1;
while(l<=leftEnd&&r<=rightEnd)
{
if(a[l]<=a[r])
TmpA[Tmp++] = a[l++];
else
TmpA[Tmp++] = a[r++];
}
while(l<=leftEnd)
TmpA[Tmp++] = a[l++];
while(r<=rightEnd)
TmpA[Tmp++] = a[r++];
for(int i=0;i<num;i++,rightEnd--)
a[rightEnd] = TmpA[rightEnd];//将有序的TmpA[]复制回a[]
}
void Merge_pass(ElementType a[],ElementType TmpA[],int N,int length)
//length为当前有序子列的长度
//两两归并相邻有序子列
{
for(int i=0;i<=N-2*length;i+=2*length)
Merge(a,TmpA,i,i+length,i+2*length-1);
if(i+length<N)
Merge(a,TmpA,i,i+length,N-1);//归并最后两个子列
else
for(int j=i;j<N;j++)//归并最后一个子列
TmpA[j] = a[j];
}
void Merge_Sort(ElementType a[],int N)
{
int length=1;//初始化子序列长度
ElementType *TmpA;
TmpA = (ElementType*)malloc(sizeof(ElementType)*N);
if(TmpA!=NULL)
{
while(length<N)
{
Merge_pass(a,TmpA,N,length);
length*=2;
Merge_pass(TmpA,a,N,length);
length*=2;
}
free(TmpA);
}
else
printf("空间不足\n");
}
归并排序是一种稳定且较快的排序算法,时间复杂度为O(nlogn),但是它需要额外的临时数组,因此空间复杂度达到O(n)。
5.5 快速排序
快速排序(QuickSort)是一种常用的排序算法,使用分治的思想将一个数组分成两个子数组,并对这两个子数组分别进行排序,然后将结果合并起来。它的原理是:将未排序元素根据一个作为基准的“主元”(Pivot)分为两个子序列,其中一个子序列均大于主元,而另一个子序列均小于主元,然后递归的对这两个子序列用类似的方法进行排序。
关于主元的选择,自然是每次恰好中分,算法时间复杂度是最优的:O(NlogN).
ElementType Median3(ElementType a[],int left,int right)
{
int center = (right + left)/2;
if(a[left]>a[center])
Swap(&a[left],&a[center]);
if(a[left]>a[right])
Swap(&a[left],&a[right]);
if(a[center]>a[right])
Swap(&a[center],&a[right]);
Swap(&a[center],&a[right-1]);//将主元放到最右边
return a[right-1];//返回主元
}
void QSort(ElementType a[],int left,iny right)
{
int pivot,cutoff,low,high;
if(cutoff<=right-left)//若序列元素足够多,则进入快速排序
{
piovt = Median3(a,left,right);//选主元
low = left;
high = right - 1;
while(1)
//序列中,比主元小的放左边,比主元大的放右边
{
while(a[++low]<piovt);
while(a[--high>piovt);
if(low<high)
Swap(&a[low],&a[high]);
else
break;
}
Swap(&a[low],&a[right-1]);//将主元放回正确位置
QSort(a,left,low-1);//递归解决左边
QSort(a,low+1,right);//递归解决右边
}
else//序列过少,直接用简单排序
InsertionSort(a+left,right-left+1);
}
void QuickSort(ElementType a[],int N)//统一接口
{
QSort(a,0,N-1);
}
它是一种原地排序算法,不需要额外的空间存储数组,对于大规模数据排序具有较好的性能。
5.6 表排序
表排序是指对数据库中的表进行排序操作。表排序可以基于一个或多个列进行排序,以便按照特定的顺序检索和显示数据。(仅移动指针、不移动具体数据)
5.6.1 间接排序
间接排序(indirect sorting)是一种排序方法,它不直接对原始数据进行排序,而是通过创建和操作索引数组来实现排序。在间接排序中,每个索引指向原始数据的元素,在排序过程中只对索引进行排序,然后根据索引的顺序访问原始数据。
定义一个指针数组作为“表”(table)。
在这里,table可以理解为应该存放的元素的下标,例如:a[0] 应该存放a[2]的key,因此table为2。
间接排序的优点是可以避免在排序过程中频繁地交换原始数据的位置,减少了时间和空间的消耗。它适用于需要保留原始数据顺序的场景,比如需要根据一列数据对另一列数据进行排序,或者需要对复杂对象的某个属性进行排序。
5.6.2 物理排序
N个数字的排列由若干个独立的环组成。
如图二所示:table[i]==i 时,环结束。
5.7 基数排序
之前说的都是基于比较来排序的,下面说一种非比较性的排序算法---基数排序。
基数排序(Radix Sort)可以看成是桶排序的推广,因此先从桶排序入手。
5.7.1 桶排序
桶排序(Bucket Sort)是一种排序算法,它将数据根据其数值范围划分为有限个桶(或称为容器),然后按照一定的顺序将数据放入对应的桶中,最后再将各个桶中的数据合并得到排序结果。
桶排序的基本思想是,将数据分散到不同的桶中,使每个桶中的数据范围尽量接近,从而降低排序的复杂度。
具体实现步骤如下:
- 确定桶的个数,并创建足够数量的空桶。
- 根据数据的范围和桶的个数,确定每个桶代表的数值范围。
- 遍历待排序的数据集,将每个数据根据其数值大小放入相应的桶中。
- 对每个非空的桶中的数据进行排序,可以选择使用其他排序算法如插入排序或快速排序。
- 最后将所有桶中的数据按照顺序依次输出,即得到排序结果。
例如:有N个学生,现在需要根据他们的成绩来排名次。已知满分为100,那么创建101个桶(M)。
伪代码如下:
void Bucket_Sort(ElementType a[],int N)
{
count[]初始化;
while(读入一个学生的成绩grade)
将该学生信息插入count[grade]链表;
for(i=0;i<M;i++)//M为101个不同的成绩值
if(count[i])
输出整个count[i]链表;
}
}
桶排序的时间复杂度取决于桶的个数和每个桶中数据的排序算法。若每个桶内部使用快速排序等较快的排序算法,且桶的数量足够多,可以达到线性时间复杂度(O(n))。
桶排序通过划分数据范围、分配桶和对每个桶中的数据进行排序,实现了一种高效的线性排序算法,适用于数据集均匀分布、范围已知的情况。
5.7.2 基数排序
据上面而言,若M>>N,则就需要基数排序了。
一般对于有K个关键字的情况,基数排序通常有两种方法:主位优先法(Most Significant Digit Sort,简称MSD)和次位优先法(Least Significant Digit Sort,简称LSD)。
具体实现步骤如下:(以次位优先法为例)
- 确定待排序序列中最大数的位数,记为d。
- 对于每个位数,从最低位开始,利用稳定的排序算法(如计数排序、桶排序)对待排序序列进行按位排序。
- 重复第2步,直到完成对最高位的排序,即排序完成。
例如,给出十个数字:7,21,525,68,34,458,121,211,74,985,要求排序。
最后按顺序输出即可。
5.8 小总结
附图