目录
一,选择排序-直接插入排序(Direct insertion sort)
三,选择排序-简单选择排序(Simple selection sort)
十,外部排序:
概述:
排序有内部排序和外部排序,内部排序是数据记录在内存中进行排序,而外部排序是因排序的数据很大,一次不能容纳全部的排序记录,在排序过程中需要访问外存
这里说的八大排序就是就是内部排序
一,选择排序-直接插入排序(Direct insertion sort)
基本思想:
在一个有序数组里面插入一个数据X,通过遍历比较找到位置,再把后面数据向后移动,把X插入到数组中,数组元素加一(插入单个数据)
(对数组进行排序),设立一个临时变量存储作为临时存储和判断数组边界之用,将序列第一个元素当成是有序的,然后从第2个记录逐个进行插入,直至整个序列有序为止。
如果碰见一个和插入元素相等的,那么插入元素把想插入的元素放在相等元素的后面。所以,相等元素的前后顺序没有改变,从原无序序列出去的顺序就是排好序后的顺序,所以插入排序是稳定的。
void InsertSort(int* a, int n) //插入排序
{
for(int i=0;i<n-1;i++)
{
int tmp=a[i+1];//插入的值
int end=i;//比较的值
while(end>=0)
{
if(tmp<a[end])//tmp小于当前值,把end所指向的位置空出来,然后把tmp插进去
{
a[end+1]=a[end];
end--;
}
else
{
break;//不小于 跳出
}
a[end+1]=tmp;//拆入
}
}
}
1. 元素集合越接近有序,直接插入排序算法的时间效率越高
2.时间复杂度:O(n^2)(情况最差时,即逆序转有序,最好为O(n));
3.空间复杂度:O(1);
4.稳定
二,插入排序-希尔排序(Shell sort)
希尔排序是1959 年由D.L.Shell 提出来的,相对直接排序有较大的改进。希尔排序又叫缩小增量排序
基本思想:
先将整个数组分成若干个子数组,通过对子数组进行排序,达到数组"基本有序",再对整个数组进行插入排序,即可达到有序.
实现方法:
1,先分组,间隔为Gap的数据为一组,然后对这组数据进行排序,再分组,再排,直到数组被分完.
2,然后再把Gap减小,继续分组,排序.
3,此时数组基本有序,然后将最后Gap设为1,即进行直接插入排序,得到有序数组
算法实现:
先简单处理增量序列:增量序列gap=(gap/3+1),gap为要排序的数组数据个数
即:先将要排序的一组记录按某个增量gap(gap/3,gap为要排序数的个数)分成若干组子序列,每组中记录的下标相差gap/3.对每组中全部元素进行直接插入排序,然后再用一个较小的增量(gap/3)对它进行分组,在每组中再进行直接插入排序。继续不断缩小增量直至为1,最后使用直接插入排序完成排序。
希尔排序可以认为是插入排序的优化
具体代码:
void ShellSort(int* a, int n)//希尔排序
{
int gap=n;
while(gap>1)//判断是否还需排序
{
gap=(gap/3+1);//保证最后一次直接插入排序
for(int i=0;i<n-gap;++i)//进入循环,对每一组进行排序
{
int end=i;
int tmp=a[end+gap];
while(end>=0)
{
if(tmp<a[end])
{
a[end+gap]=a[end];
end-=gap;
}
else
{
break;
}
a[end+gap]=tmp;
}
}
}
}
特性:
1. 希尔排序是对直接插入排序的优化。
2. 当gap > 1时都是预排序,目的是让数组更接近于有序。当gap == 1时,数组已经接近有序的了,这样就会很快。这样整体而言,可以达到优化的效果。我们实现后可以进行性能测试的对比。
3. 希尔排序的时间复杂度不好计算,需要进行推导,推导出来平均时间复杂度: O(N^1.3—N^2)
4. 稳定性:不稳定
三,选择排序-简单选择排序(Simple selection sort)
基本思想:
每一次从待排序的数据元素中选出最小(或最大)的一个元素,存放在序列的起始位置,直到全部待排序的
数据元素排完.
实现方法:
1,在数组[0,n]中选取最小的数,
2,交换数据,最小数放在左边,
3,在[1,n-1]再次选取,交换,缩减,直到集合剩一个元素
.
算法实现:
void SelectSort(int*a,int n)
{
int left=0;
while(left<n)
{
int min=left;
for(int i=left;i<n;i++)//找最小值
{
if(a[min]>a[i])
{
min=i;
}
}
Swap(&a[left],&a[min]);//交换数据 然后找次小,交换
left++;
}
}
特性:
1,容易理解,但是效率太低,实际当中不太使用
2,时间复杂度O(n^2),空间复杂度O(1);
3,不稳定
四,选择排序-堆排序(Heap sort)
基本思想:
堆排序(HeaoSort)是基于数据结构堆设计的一种排序算法,通过堆来选择数据,向上(向下)调整,得到小数(大数),然后再与堆底数据进行交换,即可排序,需要注意的是排升序建大堆,排降序建小堆
实现方法:
初始时把要排序的n个数的序列看作是一棵顺序存储的二叉树(一维数组存储二叉树),调整它们的存储序,使之成为一个堆,将堆顶元素输出,得到n 个元素中最小(或最大)的元素,这时堆的根节点的数最小(或者最大)。然后对前面(n-1)个元素重新调整使之成为堆,输出堆顶元素,得到n 个元素中次小(或次大)的元素。依此类推,直到只有两个节点的堆,并对它们作交换,最后得到有n个节点的有序序列。称这个过程为堆排序
1,建大堆,把根交换到最底,然后在减一个元素继续调整
2,向下调整,继续交换,直到最后一个元素
1,交换函数
void Swap(int* p1, int* p2)
{
int tmp = *p1;
*p1 = *p2;
*p2 = tmp;
}
2,向下调整算法
void AdjustDwon(int* a, int n, int root)
{
int parent = root;
int child = parent * 2 + 1;//找到孩子
while (child < n)
{
if (child + 1 < n && a[child + 1] > a[child])//考虑右孩子越界和判断那个孩子大
{
++child;
}
if (a[child] > a[parent])//判断孩子和父亲谁大,孩子大,向上交换
{
Swap(&a[child], &a[parent]);
parent = child;
child = parent * 2 + 1;
}
else
{
break;
}
}
}
3,建堆和排序
void HeapSort(int* a, int n)
{
// 升序 建大堆
for (int i = (n - 1 - 1) / 2; i >= 0; --i)
{
AdjustDwon(a, n, i);
}
int end = n - 1;//向下调整,交换
while (end > 0)
{
Swap(&a[0], &a[end]);
AdjustDwon(a, end, 0);
--end;
}
}
特性:
1,堆排序用来选数,效率就高了很多
2,时间复杂度O(n*logn),空间复杂度O(1);
3,不稳定
五,交换排序-冒泡排序(Bubble Sort)
基本思想:
在要排序的一组数中,对当前还未排好序的范围内的全部数,自上而下对相邻的两个数依次进行比较和调整,让较大的数往下沉,较小的往上冒。即:每当两相邻的数比较后发现它们的排序与排序要求相反时,就将它们互换。。
实现方法:
代码如下:
//冒泡排序
void Bubblesort(int *a,int n)
{
for(int i=0;i<n;i++)//控制交换次数
{
for(int j=0;j<n-i-1;j++)//向后冒泡 ,控制边界
{
if(a[j]>a[j+1])//如果前一个值大于后一个值,交换.
{
swap(&a[j],&a[j+1]);
}
}
}
}
特性总结:
1,容易理解
2,时间复杂度O(n^2),空间复杂度O(1)
3,稳定
六,交换排序-快速排序(Quick-Sort)
基本思想:
任取待排序元素序列中的某元素作为基准值,按照该排序码将待排序集合分割成两子序列,左子序列中所有元素均小于基准值,右子序列中所有元素均大于基准值,然后最左右子序列重复该过程,直到所有元素都排列在相应位置上为止,类似于树形结构,每次排序把Key值放到应在的位置.
将区间按照基准值划分为左右两半部分的常见方式有:
1. hoare版本
2. 挖坑法
3. 前后指针版本
一,Hoare版本(左右指针)
左边值设为key,然后右边先走,找小的,比key小,然后左边走找比key大,然后交换左边右边,
代码实现如下(左右指针):
1,左右指针调用
int PartSort1(int* a, int left, int right)
{
int keyi = left;
while (left < right)
{
// 找小
while (left < right && a[right] >= a[keyi])
--right;
// 找大
while (left < right && a[left] <= a[keyi])
++left;
swap(&a[left], &a[right]);//交换左右值
}
swap(&a[keyi], &a[left]);//最后交换key与left
return left;//返回当前节点,[0,left-1],[left+1,right]递归排序
}
2,递归排序
void QuickSort(int*a,int left,int right)//快排
{
if(left>=right)
{
return ;
}
int key=PartSort1(a, left, right);
// [begin, keyi-1] keyi [keyi+1, end]
//类似于二叉树
QuickSort(a,left,key-1);//递归对左数组排序
QuickSort(a,key+1,right);//递归对右数组排序
}
二,挖坑法
其设定key数组第一个值为坑,右边找下,左边找大,找到一个,交换,形成新的坑,最后把key放到坑里
1,挖坑法
int PartSort2(int* a, int left, int right)
{
int key=a[left];
while(left<right)
{
//找小的
while(left<right&&a[right]>=key)
{
--right;
}
a[left]=a[right];//找到小值,放到坑里,形成新的坑
while(left<right&&a[left]<=key)
{
++left;
}
a[right]=a[left];//找到大值,放到坑里,形成新的坑
}
a[left]=key;//把key放到数组中属于它的位置,左边所有值小于它,右边所有值大于它,
return left; //返回left,分组处理数据
}
2,递归排序
void QuickSort(int*a,int left,int right)//快排
{
if(left>=right)
{
return ;
}
int key=PartSort2(a, left, right);
// [begin, keyi-1] keyi [keyi+1, end]
//类似于二叉树
QuickSort(a,left,key-1);//递归对左数组排序
QuickSort(a,key+1,right);//递归对右数组排序
}
三,前后指针法
如图所示
选取key值,cur小于key值,prev++,交换cur与prev值,
1,前后指针法
int PartSort3(int* a, int left, int right)
{
int keyi=left;
int cur=left+1;
int prev=left;
while(cur<=right)
{
if(a[cur]<a[keyi]&&++prev!=cur)//判断cur与key的值,并且防止自己与自己交换
{
swap(&a[cur],&a[prev]);
}
++cur;
}
swap(&a[keyi],&a[prev]);
return prev;
}
二,递归调用
void QuickSort(int*a,int left,int right)//快排
{
if(left>=right)
{
return ;
}
int key=PartSort3(a, left, right);
// [begin, keyi-1] keyi [keyi+1, end]
//类似于二叉树
QuickSort(a,left,key-1);//递归对左数组排序
QuickSort(a,key+1,right);//递归对右数组排序
}
快排优化
若初始序列按关键码有序或基本有序时,快排序反而蜕化为冒泡排序。为改进之,通常以“三者取中法”来选取基准记录,即将排序区间的两个端点与中点三个记录关键码居中的调整为支点记录,本质在于防止最坏的情况发生(1,已经有序2,数据全部相等)
为了避免这种情况,选取头尾和中间元素,比较大小,找大小处于中间的元素为key值,实现对快排的优化,时间复杂度仍为O(nlog^n),每次调用排序的时候把key置一下.
实现如下;
//快排三数优化
int GetMid(int* a, int left, int right)
{
int mid = (left + right) >> 1;
// left mid right
if (a[left] < a[mid])
{
if (a[mid] < a[right])
{
return mid;
}
else if (a[left] > a[right])
{
return left;
}
else
{
return right;
}
}
else // a[left] > a[mid]
{
if (a[mid] > a[right])
{
return mid;
}
else if (a[left] < a[right])
{
return left;
}
else
{
return right;
}
}
}
四,非递归实现快排
通过使用栈对数据进行排序,递归本身就是一个压栈的过程
void QuickSortNonR(int* a, int begin, int end)
{
Stack st;
StackInit(&st);
StackPush(&st, begin);
StackPush(&st, end);
while (!StackEmpty(&st))
{
int left, right;
right = StackTop(&st);//弹出数据
StackPop(&st);
left = StackTop(&st);
StackPop(&st);
int keyi = PartSort1(a, left, right);
if (left < keyi-1)//比较大小,压大的入栈,
{
StackPush(&st, left);
StackPush(&st, keyi-1);
}
if (keyi+1 < right)
{
StackPush(&st, keyi+1);
StackPush(&st, right);
}
}
StackDestroy(&st);
}
特性总结:
1.快速排序整体的综合性能和使用场景都是比较好的
2,时间复杂度:O(N*logN)
3,空间复杂度:O(N*logN)
4,不稳定
七,归并排序-(Merge sort)
基本思想:
归并排序(MERGE-SORT)是建立在归并操作上的一种有效的排序算法,该算法是采用分治法(Divide andConquer)的一个非常典型的应用。将已有序的子序列合并,得到完全有序的序列;即先使每个子序列有序,再使子序列段间有序。若将两个有序表合并成一个有序表,称为二路归并。通过递归实现对小数组有序,再返回回来,
代码实现:
//排序入口
void MergeSort(int* a, int n)
{
int *temparr=(int*)malloc(n*sizeof(int));
if(temparr!=NULL)
{
msort(a,temparr,0,n-1);//进行排序
free(temparr);
}
else
{
printf("malloc fail\n");
}
}
递归分组进行排序
void msort(int*a,int*arr,int left,int right)
{
//如果只有一个元素,默认有序,只需被归并即可
if(left<right)
{
//找中间点
int mid=(left+right)>>1;
//对左半区进行归并
msort(a,arr,left,mid);
//对右半区归并
msort(a,arr,mid+1,right);
//对已经排序的数组进行合并
merge(a,arr,left,mid,right);
}
}
排序
//归并排序
void merge(int*a,int*arr,int left,int mid,int right)
{
//标记左半区第一个未排序的元素
int l_pos=left;
//标记右半区第一个未排序的元素
int r_pos=mid+1;
//临时数组下标的元素
int pos=left;
//合并
while(l_pos<=mid&&r_pos<=right)
{
if(a[l_pos]<a[r_pos])
arr[pos++]=a[l_pos++];
else
arr[pos++]=a[r_pos++];
}
//合并左半区剩余的元素
while(l_pos<=mid)
{
arr[pos++]=a[l_pos++];
}
//合并右半区剩余的元素
while(r_pos<=right)
{
arr[pos++]=a[r_pos++];
}
//把临时数组合并后的元素复制到a中
while(left<=right)
{
a[left]=arr[left];
left++;
}
}
非递归实现归并排序
其实现过程就是分治,只不过不用递归返回,而是直接排序,排完以后再粘贴到数组中
代码实现如下:
void MergeSortNoR(int* a, int n)
{
int *temparr=(int*)malloc(n*sizeof(int));
if(temparr==NULL)
{
printf("malloc fail\n");
}
int gap=1;
while(gap<n)
{
for(int i=0;i<n;i+=2*gap)//分组
{
int begin1=i,end1=i+gap-1,begin2=i+gap,end2=i+2*gap-1;
// 如果第二个小区间不存在就不需要归并了,结束本次循环
if(begin2>=n)
break;
// 如果第二个小区间存在,但是第二个小区间不够gap个,结束位置越界了,需要修正一下
if(end2>=n)
end2=n-1;
_Merge(a,temparr,begin1,end1,begin2,end2);
}
gap*=2;
}
free(temparr);
}
//非递归实现
void _Merge(int*a,int*tmpArr,int begin1,int end1,int begin2,int end2)
{
//两个半区元素归并
int i=begin1;//每次归并的区间左边界
int left=begin1;
int right=end2;
while(begin1<=end1&&begin2<=end2)
{
if(a[begin1]<a[begin2])
tmpArr[i++]=a[begin1++];
else
tmpArr[i++]=a[begin2++];
}
// 合并左半区元素
while(begin1<=end1)
{
tmpArr[i++]=a[begin1++];
}
//合并右半区元素
while(begin2<=end2)
{
tmpArr[i++]=a[begin2++];
}
//把临时数组的元素复制到a中
while(left<=right)
{
a[left]=tmpArr[left];
left++;
}
}
特性总结:
1. 归并的缺点在于需要O(N)的空间复杂度,归并排序的思考更多的是解决在磁盘中的外排序问题。
2. 时间复杂度:O(N*logN)
3. 空间复杂度:O(N)
4. 稳定性:稳定
八, 基数排序(Count Sort)
基本思想:
1. 统计相同元素出现次数
2. 根据统计的结果将序列回收到原来的序列中
代码如下,加入了优化,避免了空间的浪费
void CountSort(int* a, int n)
{
int max = a[0], min = a[0];
for (int i = 0; i < n; ++i)
{
if (a[i] > max)
max = a[i];
if (a[i] < min)
min = a[i];
}
int range = max - min + 1;
int* count = malloc(sizeof(int)*range);
memset(count, 0, sizeof(int)*range);
for (int i = 0; i < n; ++i)//计数
{
count[a[i] - min]++;
}
int i = 0;
for (int j = 0; j < range; ++j)//排序
{
while (count[j]--)
{
a[i++] = j + min;
}
}
free(count);
}
计数排序的特性总结:
1. 计数排序在数据范围集中时,效率很高,但是适用范围及场景有限。
2. 时间复杂度:O(MAX(N,范围))
3. 空间复杂度:O(范围)
4. 稳定性:稳定
排序算法复杂度及稳定性分析
外部排序:
外排序是数据量较大,内存放不下,数据放到外部磁盘空间,一般使用归并排序进行外排序
假设内存为512m,给10亿个数据,然后内存每次读取512m的数据,排序完成后返回给磁盘,然后重复这个过程,直到拍完,然后外部的小文件,再经过归并,即可得到一个有序的数据.