排序是指重新排列列表中的数据,使表中的元素满足按照关键字有序的过程
排序分为外部排序和内部排序两种
内部排序
是指在排序其间元素都放在内存当中的排序
插入排序
基本思想:每次将一个待排记录按其大小插入到前面已经排好序的序列当中。
直接插入排序
代码如下:
void InsertSort(int a[],int n){
//a[0]当作为哨兵,将每次待排记录放入到a[0]
int i,j;
for(i=2;i<n=n;i++)//因此i=1时是一个记录已经有序,所以排序从i=2开始
{
if(a[i]<a[i-1]){
a[0]=a[i];
for(j=i-1;a[0]<a[j];j--){
a[j+1]=a[j];//将所有大于该记录的往后移动一个位置
}
a[j+1]=a[0]//填补上记录
}
}
}
手写步骤如下:
性能分析:
空间效率:仅使用了常数个空间单元,所以空间复杂度是O(1)。
时间效率:需要逐个记录进行插入,需要进行n-1趟,每趟比较元素次数和移动元素次数取决于有序列的初始态。
在最好的情况下,在n-1趟中每次插入的元素都比前有序列的值大,只需要比较一次而不需要移动元素,故时间复杂度是O(n)。
在最坏的情况下,表中元素的顺序刚好与排序结果中的顺序相反,在排序当中比较次数达到最大,移动次数也达到最大,故时间复杂度为O(n2)。
因此直接插入排序的时间复杂度为O(n2)
直接插入排序每次排序不能确定每个元素的最终位置
直接插入排序是一种稳定算法,适用于顺序存储和链式存储的单链表。
折半插入排序
因为直接插入排序边比较边移动元素,在此情况下,我们可以改进这个算法,使用之前学到的折半查找先找到记录的待插入位置,然后统一的进行移动元素,折半查找由于需要随机查找所以是在顺序表的基础上进行操作的。
代码如下:
void InsertSort(int a[],int n){
int i,j,low,high,mid;low、high、mid分别指向有序列的起始位置、结束位置、中间位置
for(i=2;i<=n;i++){
a[0]=a[i];
int low=1;high=i-1;
while(low<=high){
mid=(low+high)/2;
if(a[mid]>a[0]) high=mid-1;查找左半边
else low=mid+1;//查找右半边
}
for(j=i-1;j>=high+1;j--){//将所有大于待排记录的元素一次性移动到后面
a[j+1]=a[j];
}
a[high+1]=a[0];//插入待排记录
}
}
折半插入减少了元素比较次数,约为O(nlog2n),但是由于需要移动元素不变,故时间复杂度仍然是O(n2),该算法也是一种稳定的算法。
希尔排序
由于直接插入适合于基本有序的排序表和数据量不大的排序表。所以改进了直接插入排序,提出了希尔排序这个算法。
希尔排序的基本思想:先将待排序表分割成若干如L[i,i+d,i+2d,...i+kd]的特殊子表,即把相隔某个"增量"的记录组成一个子表,对各个 子表进行直接插入排序,当整个表中的元素"基本有序"的时候,再对整个表进行一次直接插入排序。
增量的大小根据需求进行调整。
代码如下:
void ShellSort(int a[],int n){
int d,i,j;
for(d=n/2;d>=1;d=d/2)//设置每次增量减少一半
{
for(i=d+1;i<=n;i++){
if(a[i]<a[i-d]){//同表元素中前一个比它小才继续查找并移动
a[0]=a[i];
for(j=i-d;j>=0&&a[0]<a[j];j-=d){//找到比它大的元素进行后移
a[j+d]=a[j];
}
a[j+d]=a[0]//插入元素到空缺位置
}
}
}
}
手写步骤如下:
性能分析:
空间效率:只使用了常数个辅助单元,故空间复杂度为O(1)
时间效率:时间复杂度为O(n2)
稳定性:通过上面那个例子可以看出希尔排序不是一个稳定性的算法
适用性:由于希尔排序需要间隔的比较元素,所以需要随机访问的效果,只适用于顺序存储的情况。
交换排序
交换是指根据序列中两个元素关键字的比较结果来兑换这两个记录在序列中的位置。
冒泡排序
基本思想:从后往前两两比较相邻元素的值,如果为逆序则交换位置,直到序列比较完,即为第一趟冒泡,每趟冒泡排序都可以选出序列中最小的一个元素放到序列的第一个位置,而这些已经确定位置的元素在此后的排序当中可以不参与排序。当其中一趟没有交换元素说明此时序列已经有序
代码如下:
void BubbleSort(int a[],int n){
for(int i=0;i<n-1;i++){//i<n-1如果只剩下最后一个元素不用进行排序
bool flag=false;//表示本趟冒泡是否发生交换的标志
for(int j=n-1;j>i;j--){//j>i表示只需要比较到没有确定位置的元素,前面已经确定的序列不用进行比较
if(a[j]<a[j-1]){//如果相邻元素逆序,则交换位置
int tempt=a[j-1];a[j-1]=a[j];a[j]=tempt;
flag=true;//修改标志
}
if(flag==false)//此时本趟没有发生交换则说明序列已经有序,则直接返回
return;
}
}
}
手写步骤如下:
性能分析:
空间效率:在排序过程中只使用了常数个辅助单元,则空间复杂度为O(1)
时间效率:最好情况下此时序列已经有序只需要比较n-1次元素,不需要移动元素,则时间复杂度为O(n),在最坏情况下序列为逆序,则需要n-1趟排序,第i趟排序需要比较n-i个元素,则时间复杂度为O(n2),故平均时间复杂度为O(n2)
稳定性:冒泡排序是一个稳定性的算法。
快速排序
基本思想:在待排序列当中任取出一元素作为基准,通过一趟排序可以将比基准元素小的放在基准元素左边的部分,比基准元素大的放在基准元素右边的部分,然后分别对左边和右边两个部分进行快速排序。
由于左右两边都需要用快速排序,所以可以采用递归算法。
代码如下:
void QuickSort(int a[],int low,int high)//这里的low和high就是对应手写的下面的i和j
{
while(low<high){
int pivotpos=Partition(a,low,high);//这里返回的pivotpos就是基准元素在一趟排序中最终确定的位置
//分别对基准元素左边和右边部分进行快速排序
QuickSort(a,low,pivotpos-1);
QuickSort(a,pivotpos+1,high);
}
}
int Partition(int a[],int low,int high)
{
int pivot=a[low]//将表中第一个元素作为基准
while(low<high){
while(low<high&&a[high]>=pivot){//从右边开始找到第一个比基准元素小的元素
high--;
}
//将找到的比基准元素大的元素放到low指向的位置
a[low]=a[high];
while(low<high&&a[low]<=pivot){//从右边开始找到第一个比基准元素大的元素
low++
}
//将找到的比基准元素大的元素放到high指向的位置
}
//将基准元素放到low指向的位置
a[low]=pivot;//将基准元素放到low和high指向的位置
return low;//返回基准元素所在的下标,便于分别对其左右两边进行快速排序
}
手写步骤如下:
性能分析:
空间效率:由于快速排序是基于递归算法,所以是通过栈来保存信息,那么空间利用容量就和递归调用的深度有关,将n个元素组织成一个二叉树,二叉树的深度就是递归调用的层数,在前面学过n个元素组成的二叉树,层数最少是向下取整+1,层数最多是n,所以快速排序空间复杂度最好情况下是O(),最坏情况下是:O(n),平均情况是O()
时间效率:快速排序和其他排序方法不同,当序列基本有序或者的时候,为最坏的情况,时间复杂度为O(n2)
稳定性:快速排序不是一个稳定的算法
快速排序是所有内部排序当中平均性能最优的排序算法,故叫做快速排序
选择排序
选择排序的基本思想:每一趟(如第i趟)在后面的n-i+1(i=1,2...,n-1)个待排序列当中选取出关键值最小的元素作为有序序列的第i个元素,直到n-1趟做完。
选择排序有简单选择排序和堆排序,堆排序是比较重要的。
简单选择排序
基本思想:假设排序表为L[1...n],第i趟排序即从L[i...n]中选择最小的关键字与L[i]元素交换,每一趟就可以确定一个元素的最终位置,经过n-1趟之后可以使排序表有序。
代码如下:
void SelectSort(int a[],int n){
int i,j,min;
for( i=0;i<n-1;i++)//一共进行n-1趟排序
{
min=i//记录最小值位置,起始值为序列第一个元素
for(j=i+1;j<n;j++)
{
//如果元素小于最小值即修改最小值位置记录
if(a[j]<a[min]) min=j;
}
//如果最小值位置记录发生改变,交换两个位置的元素
if(min!=i) swap(a[min],a[i]);
}
}
性能分析:
空间效率:仅使用常数个辅助单元,所以空间效率为O(1)。
时间效率:因此元素比较的次数与初始序列状态无关,第一趟比较n-1次,第二趟比较n-2次,第三趟比较n-3次.....一共比较n-1次,所以会比较n(n+1)/2次,时间复杂度为O(n2)。
稳定性:在第i趟找到最小元素之后,和第i个元素交换位置,两个相同元素的相对位置可能会发生变化,故该算法不是一种稳定性算法。
堆排序
这是个大大的重点和考点!!!!!!!
介绍堆排序之前,我们先认识一下两个哥们,大根堆、小根堆。
n个关键字序列L[1...n]称为堆
当L(i)>L(2i)且L(i>2i+1),和树类似,双亲结点的值大于孩子结点,就称之为大根堆。
当L(i)<L(2i)且L(i<2i+1),双亲结点的值小于孩子结点,就称之为小根堆。
好了,准备工作已经做好,我们开始来介绍堆排序。
堆排序基本思想:在大根堆(小根堆)的基础上,由于根节点是最大值( 最小值)我们可以将根节点输出,然后将最后一个元素送入堆顶也就是根节点,此时已经部不满足大根堆(小根堆)的性质,我们需要向下调整去与孩子结点比较,找到比根节点大(小)的元素进行交换,满足大根堆(小根堆)性质后,再继续输出堆顶元素,知道只剩下最后一个元素即可,此时排序完成。
看完堆排序基本思想,我们肯定可以看出,在堆排序当中最重要的无非就两个任务①构建大(小)根堆②当大(小)根堆的性质破坏, 如何向下进行调整。
构建大根堆步骤:从最后一个子树开始,判断子树的根结点和孩子结点大小,如果孩子结点的最大值大于子树根节点那么就交换,交换之后,如果使得孩子结点作为根节点的子树不满足大根堆性质,那么继续调整子树,然后依次向前对子树进行调整,最后满足大根堆性质。
调整大根堆步骤:从堆顶开始,依次比较子树根节点和孩子结点大小,如果孩子结点最大值大于根节点则交换,依次先后进行调整子树。
可能看完这些文字,大叫脑子有点懵,那咱直接上图理解!!!
这样就成功的构建了一个大根堆,现在我们开始大根堆排序,将堆顶元素87输出,将最后一个元素09交换到堆顶,此时已经不满足大根堆的性质,那么和上述方法类似向下再次调整,使其成为大根堆,循环执行这两个步骤,就可以成功排序。