八大排序算法总结(一)
归并排序
归并算法采用分治策略
1、归并排序的基本思想
将待排序序列R[0…n-1]看成是n个长度为1的有序序列,将相邻的有序表成对归并,得到n/2个长度为2的有序表;将这些有序序列再次归并,得到n/4个长度为4的有序序列;如此反复进行下去,最后得到一个长度为n的有序序列
2、归并排序的算法描述
第一步:申请空间,使其大小为两个已经排序序列之和,该空间用来存放合并后的序列
第二步:设定两个指针,最初位置分别为两个已经排序序列的起始位置
第三步:比较两个指针所指向的元素,选择相对小的元素放入到合并空间,并移动指针到下一位置
重复步骤3直到某一指针超出序列尾,将另一序列剩下的所有元素直接复制到合并序列尾
归并排序其实要做两件事:
(1)“分解”——将序列每次折半划分(递归实现)
(2)“合并”——将划分后的序列段两两合并后排序
如何合并?
在每次合并过程中,都是对两个有序的序列段进行合并,然后排序。
这两个有序序列段分别为 R[low, mid] 和 R[mid+1, high]。
先将他们合并到一个局部的暂存数组R2中,带合并完成后再将R2复制回R中。
我们称 R[low, mid] 第一段,R[mid+1, high] 为第二段。
每次从两个段中取出一个记录进行关键字的比较,将较小者放入R2中,最后将各段中余下的部分直接复制到R2中。
经过这样的过程,R2已经是一个有序的序列,再将其复制回R中,一次合并排序就完成了。
时间复杂度:O(nlogn)
(最好情况和最坏情况都是O(nlogn))
空间复杂度:O(n)
是否稳定: 稳定
#include<stdio.h>
#include<stdlib.h>
/*Merge函数将R[low,mid]和R[mid+1,high]这两排好序的小数组合并处排序大数组R[low,high]*/
void Merge(int *R,int low,int mid, int high)
{
int i = low, j = mid+1, p = 0;
//k指向合并后数组的头
int *R1; //局部变量,暂存数组
R1=(int *)malloc((high-low+1)*sizeof(int));
//分配出一块两个小数组合并后的数组的空间
if(!R1)
{
printf("malloc error!\n");
return; //空间申请失败
}
while(i<=mid && j<=high)
{
if(R[i]<=R[j])
{
R1[p] = R[i];
i++;
}
else
{
R1[p] = R[j];
j++;
}
p++;
}
//然后肯定至少有一个数组是有剩余没排进去的,并且值很大
while(i<=mid) //若第1个子文件非空,则复制剩余记录到R1中
{
R1[p++]=R[i++];
}
while(j<=high) //若第2个子文件非空,则复制剩余记录到R1中
{
R1[p++]=R[j++];
}
for(p=0,i=low;i<=high;p++,i++)
{
R[i]=R1[p]; //归并完成后将结果复制回R[low..high]
}
}
//分治法进行二路归并排序
void MergeSort(int *R, int low, int high)
{
int mid;
if(low<high) //区间长度大于1
{
mid = (low+high)/2;
MergeSort(R,low,mid);
MergeSort(R,mid+1,high);
Merge(R,low,mid,high);
}
}
void main()
{
int a[9]={49,38,65,97,76,13,27,1,19}; //这里对9个元素进行排序
int low=0,high=8; //初始化low和high的值
printf("排序前的序列: ");
for(int i=low;i<=high;i++)
{
printf("%d ",a[i]); //输出测试
}
printf("\n");
MergeSort(a,low,high);
printf("排序后的序列: ");
for( int i=low;i<=high;i++)
{
printf("%d ",a[i]); //输出测试
}
printf("\n");
}
交换排序
- 冒泡排序
算法原理
冒泡排序的原理(以递增序为例)是每次从头开始依次比较相邻的两个元素,如果后面一个元素比前一个要大,说明顺序不对,则将它们交换,本次循环完毕之后再次从头开始扫描,直到某次扫描中没有元素交换,说明每个元素都不比它后面的元素大,至此排序完成。
由于冒泡排序简洁的特点,它通常被用来对于计算机程序设计入门的学生介绍算法的概念。
时间复杂度
若文件的初始状态是排好序的的,一趟扫描即可完成排序。所需的关键字比较次数C和记录移动次数 M 均达到最小值(Cmin = n-1、Mmin = 0)
所以,冒泡排序最好的时间复杂度为O(N)。
若初始文件是反序的,需要进行N趟排序。每趟排序要进行 C = N-1次关键字的比较(1≤i≤N-1)和总共(Mmax = (N*(N-1))/2)次的移动(移动次数由乱序对的个数决定,即多少对元素顺序不对,如 1 3 4 2 5 中共有(3,2)、(4,2)两个乱序对),在这种情况下,比较和移动次数均达到最大值
(Cmax =N*(N-1) + Mmax=(N*(N-1))/2 = O(N^2)。
所以,冒泡排序的最坏时间复杂度为O(N^2)
综上,冒泡排序总的平均时间复杂度为O(N^2)。
算法稳定性
冒泡排序就是把小的元素往前调或者把大的元素往后调。比较是相邻的两个元素比较,交换也发生在这两个元素之间。如果两个相等的元素相邻,那么根据我们的算法。它们之间没有发生交换;如果两个相等的元素没有相邻,那么即使通过前面的两两交换把两个相邻起来,这时候也不会交换,所以相同元素的前后顺序并没有改变,所以冒泡排序是一种稳定排序算法。
单向冒泡排序
void bubble_sort(int *a,int n)
{
int i,j;
int temp = 0;
for(i = 0;i<n-1;i++)
{
for(j = 0;j<n-i-1;j++)
{
if(a[j]>a[j+1]) //每一次运行都把最大的一个数运到尚未排的最后
{
temp = a[j+1];
a[j+1] = a[j];
a[j] = temp;
}
}
}
}
双向冒泡排序
//双向冒泡排序
void double_bubbleswap(int source[],int n)
{
int start = 0,end = n-1;
int i;
while(start<=end)
{
for(i=start;i<end;i++)
{
if(source[i]>source[i+1])
{
int t;
t = source[i];
source[i] = source[i+1];
source[i+1] = t;
}
}
end--;
for(int j=end;j>start;j--)
{
if(source[j]<source[j-1])
{
int k=source[j];
source[j] = source[j-1];
source[j-1] = k;
}
}
start++;
}
}
- 快速排序
基本思想是:从一个数组中随机选出一个数N,通过一趟排序将数组分割成三个部分,1、小于N的区域 2、等于N的区域 3、大于N的区域,然后再按照此方法对小于区的和大于区分别递归进行,从而达到整个数据变成有序数组。
快排的时间复杂度O(N*logN),空间复杂度O(logN) 【因为每次都是随机事件,坏的情况和差的情况,是等概率的,根据数学期望值可以算出时间复杂度和空间复杂度】,不稳定性排序
图解部分源自此网站https://blog.youkuaiyun.com/u010452388/article/details/81218540?utm_medium=distribute.pc_relevant.none-task-blog-BlogCommendFromMachineLearnPai2-2.channel_param&depth_1-utm_source=distribute.pc_relevant.none-task-blog-BlogCommendFromMachineLearnPai2-2.channel_param
//快速排序
void quicksort(int a[],int left,int right)
{
int i = left;
int j = right;
int temp = a[i];
if(left>right)
return;
while(i!=j)
{
/*先从右往左,当遇到第一个小于temp的值得时候
跳出循环。a[i]赋值为a[j],这里的a[i]=temp=a[left];*/
while(i<j&&a[j]>temp)
j--;
if(i<j)
a[i] = a[j];
/*在从左往右,当遇到第一个大于temp的时候,跳出循环
a[j]赋值为a[i]*/
while(i<j&&a[i]<=temp)
i++;
if(i<j)
a[j]=a[i];
}
a[i] = temp;
quicksort(a,left,i-1);
quicksort(a,i+1,right);
}
插入排序
- 直接插入排序
直接插入排序基本思想是每一步将一个待排序的记录,插入到前面已经排好序的有序序列中去,直到插完所有元素为止。
直接插入排序算法的运作如下:
假设有一组无序序列 R0, R1, … , RN-1。
(1) 我们先将这个序列中下标为 0 的元素视为元素个数为 1 的有序序列。
(2) 然后,依次把 R1, R2, … , RN-1 插入到这个有序序列中。所以,我们需要一个外部循环,从下标 1 扫描到 N-1 。
(3) 接下来描述插入过程。假设这是要将 Ri 插入到前面有序的序列中。由前面所述,我们可知,插入Ri时,前 i-1 个数肯定已经是有序了。
所以我们需要将Ri 和R0 ~ Ri-1 进行比较,确定要插入的合适位置。这就需要一个内部循环,我们一般是从后往前比较,即从下标 i-1 开始向 0 进行扫描。
当数据正序时,执行效率最好,每次插入都不用移动前面的元素,时间复杂度为O(N)。
当数据反序时,执行效率最差,每次插入都要前面的元素后移,时间复杂度为O(N*2)。
//直接插入排序
void DirectInsertionsort(int A[],int n)
{
int i,j;
int temp;
for(i=1;i<n;i++) //直接插入排序一般认为A[0]是那个最初排好的第一个序列
{
j=i;
temp = A[i];
while(j>0&&temp<A[j-1])//当未达到数组的第一个元素或者待插入元素小于当前元素
{
A[j] = A[j-1]; //就将该元素后移
j--; //下标减一,继续比较
}
A[j]=temp;
}
}
折半插入排序
1、将待排序序列的第一个元素看做一个有序序列,把第二个元素到最后一个元素当成是未排序序列。
2、从头到尾依次扫描未排序序列,将扫描到的每个元素插入有序序列的适当位置,在查找元素的适当位置时,采用了折半查找方法。(如果待插入的元素与有序序列中的某个元素相等,则将待插入元素插入到相等元素的后面。)
//折半插入排序
void BinaryInsertionsort(Forsort A[],int n)
{
int i,k,r;
Forsort temp;
for(i=1;i<n;i++)
{
temp = A[i];
k = 0;
r = i - 1;
while(k<=r)
{
int m;
m = (k+r)/2;
if(temp.key<A[m].key)
r = m-1;
else
{
k = m+1;
}
}
//找到了插入位置为k
for(r = i;r>k;r--)
{
A[r] = A[r-1];
}
A[k] = temp;
}
}
希尔排序
(可以参考一下这篇博客: 点击蓝色文字)
1.首先根据初始序列的长度,选定一个递减增量序列
t1,t2,t3…tk,其中ti>tj,tk = 1。
根据选定的增量序列元素个数k,对初始序列进行k趟排序。
根据增量序列的值ti,每趟排序会把初始序列划分成若干个元素的子序列,然后对这些子序列使用插入排序,因为这是递减增量序列,所以第一趟的排序,增量值最大,那么划分的子序列数量也就最多,每个子序列的元素也就越少,可以看做是一个“几乎”已经排好序的序列,当增量值越来越小的时候,子序列数量也就越少,每个子序列的元素也就越多,但是,基于前几次的排序,这些子序列虽然元素多,但是已经是一个“几乎”排好序的序列了,当最后一次排序的时候,即增量序列的值为1时,就是对整个无序序列进行排序,这时整个序列已经是一个“几乎”排好序的序列了。
以图为例进行说明,假设给定的初始无序序列如下:
4 5 8 2 3 9 7 1
首先,我们选择一个增量序列,这个增量序列如何选择呢?首先我们得保证第一次的所有子序列元素数量应该至少保证为2个或以上,这样是用插入排序才有意义,如果元素数量为1,也就是增量序列的第一个值为初始序列的长度或者更大,那么这次遍历将“无功而返”,所以至少应该保证子序列元素数量为2或以上,当子序列数量退化到初始序列长度时,希尔排序也退化成了插入排序。事实上,希尔排序的效率依赖于增量序列的选择,好的增量序列可以大大的提高希尔排序的效率,但是增量序列的选择是和初始序列有关系的。
一个好的递减增量序列选取的标准是:第一、递减增量序列最后一个值应该为1;第二、递减增量序列中的值,尤其是相邻的值最好不要互为倍数的关系,如果是互为倍数的关系,那么根据这两个序列值的分组将会有重复的情况,可能会做“无用功”。
该示例中,我们选取的递减增量序列位[3,2,1]。
递减增量序列已经有了,下面开始进行3趟(增量序列长度为3)排序,先取增量序列第一个值3,然后将初始序列分组,如下:
对三组子序列[4,2,7],[5,3,1],[8,9]分别使用插入排序,结果如下:
然后,对该序列再次进行增量序列的分组,这次增量序列的值为2,分组情况如下:
对两组子序列分别使用插入排序,结果如下:
最后,对整个序列做插入排序(增量序列值为1)即可。
从整个过程可以看出,每次的排序,都会将较小的值转移到序列的前边,整个序列的有序性不断的变强,可以使插入排序达到更高的效率。
希尔排序的效率依赖于递减增量序列的选择,时间复杂度最坏的情况是O(nlog2n)。
typedef int Elementtype;
struct forsort
{
Elementtype key;
};
typedef struct forsort Forsort;
void InitForsort(Forsort *FS,int a)
{
FS->key = a;
}
//shell排序的思想是先预选一个数s然后把记录分为S个组,
//所有距离为S的数据分在一个组里面,然后取s2=s>>1;再重复上述过程直到最后si = 1
void Shellsort(Forsort A[],int n,int s)
{
int i,j,k;
Forsort temp;
//分组排序,初始的增量为S,每循环一次增量减半,直到增量为0时结束
for(k = s;k>0;k>>1) // k>>1的意思就是k/=2; 只是这个k>>1的写法会加快运行速度
{
for(i=k;i<n;i++)
{
temp = A[i];
j = i - k;
while(j>=0&&temp.key<A[j].key)
{
A[j+k]=A[j];
j-=k;
}
A[j+k] = temp;
}
}
}
基数排序
基数排序(radix sort)属于“分配式排序”(distribution sort),又称“桶子法”(bucket sort)或binsort,顾名思义,它是透过键值的部份资讯,将要排序的元素分配至某些“桶”中,藉以达到排序的作用,基数排序法是属于稳定性的排序
假设有欲排数据序列如下所示:73 22 93 43 55 14 28 65 39 81首先根据个位数的数值,在遍历数据时将它们各自分配到编号0至9的桶(个位数值与桶号一一对应)中。分配结果(逻辑想象)如下图所示:
分配结束后。接下来将所有桶中所盛数据按照桶号由小到大(桶中由顶至底)依次重新收集串起来,得到如下仍然无序的数据序列:
81 22 73 93 43 14 55 65 28 39
接着,再进行一次分配,这次根据十位数值来分配(原理同上),分配结果(逻辑想象)如下图所示:
//基数排序
#define Max_ 10 //数组个数
#define RADIX_10 10 //整形排序
#define KEYNUM_31 31 //关键字个数,这里为整形位数
//找到num从低到高的第pos位数据
int getnuminpos(int num,int pos)
{
int temp = 1;
for(int i;i<pos-1;i++)
temp *=10;
return (num/temp)%10;
}
//基数排序 pDataArray 无序数组;iDataNum为无序数据个数
void radixsort(int *pDataArray,int iDataNum)
{
int *radixArrays[RADIX_10]; //分别为0~9的序列空间,是个二维数组
for(int i = 0;i<10;i++)
{
radixArrays[i] = (int *)malloc(sizeof(int) * (iDataNum + 1));
radixArrays[i][0] = 0; //index为0处记录这组数据的个数
}
for (int pos = 1; pos <= KEYNUM_31; pos++) //从个位开始到31位
{
for (int i = 0; i < iDataNum; i++) //分配过程
{
int num = GetNumInPos(pDataArray[i], pos);
int index = ++radixArrays[num][0];
radixArrays[num][index] = pDataArray[i];
}
for (int i = 0, j =0; i < RADIX_10; i++) //收集
{
for (int k = 1; k <= radixArrays[i][0]; k++)
pDataArray[j++] = radixArrays[i][k];
radixArrays[i][0] = 0; //复位
}
}
}