排序介绍
本节小菜鸡就给大家介绍一下排序:
排序简单分为内排序和外排序
- 内排序是在排序整个过程中,待排序的所有记录全部被放置在内存中.
- 外排序是由于排序记录个数太多,不能同时放置在内存,整个排序过程需要在内外存之间多次交换数据才能进行.
(其实小菜鸡题目准备写排序的但是怕你们出现认知错误就改了)
以下介绍都是简单C语言的内排序.(其实小菜鸡一般排序用sort嘿嘿!!!)
准备工作
做一个解释:以下所有代码用的都是下面代码的结构体(这是一个很重要的东西).
准备工作代码:
#define MAXSIZE 10000 /* 用于要排序数组个数最大值,可根据需要修改 */
typedef struct
{
int r[MAXSIZE+1]; /* 用于存储要排序数组,r[0]用作哨兵或临时变量 */
int length; /* 用于记录顺序表的长度 */
}SqList;
最简单的排序:冒泡排序
思路
冒泡排序之所以简单,思路简单,实现也简单,它的基本思路是:两两比较,大了就换,直到全部满足顺序(从小到大).
这个图是不是很帮助你理解啊!!(哈哈哈哈)
步骤如下
- 比较相邻的元素。如果第一个比第二个大,就交换它们两个;
- 对每一对相邻元素作同样的工作,从开始第一对到结尾的最后一对,这样在最后的元素应该会是最大的数;
- 针对所有的元素重复以上的步骤,除了最后一个;
- 重复步骤1~3,直到排序完成。
/* 对顺序表L作冒泡排序 */
void BubbleSort(SqList *L)
{
int i,j;
for(i=1;i<L->length;i++)
{
for(j=L->length-1;j>=i;j--) /* 注意j是从后往前循环 */
{
if(L->r[j]>L->r[j+1]) /* 若前者大于后者*/
{
swap(L,j,j+1);/* 交换L->r[j]与L->r[j+1]的值 */
}
}
}
}
swap()函数就是一个交换函数,所以这么简单的函数就自己写吧!!!
我们要学会成长,小菜鸡最近都在努力学习,代码也要成长(优化),我们看看这个最简单的代码有哪些可以优化的!
代码优化
当我给你一串序列{2,1,3,4,5,6,7,8,9},聪明的你一看就知道换一下前面两个不就好了吗!但是代码依旧会全部进行比较,就会进行许多多余的操作,这可是要避免的.所以当序列有序时候,就不让代码进行了!
我要在代码上加一个变量flag来实现这个算法的优化.
/* 对顺序表L作改进冒泡算法 */
void BubbleSort2(SqList *L)
{
int i,j;
int flag=TRUE; /* flag用来作为标记 */
for(i=1;i<L->length && flag;i++) /* 若flag为true说明有过数据交换,否则停止循环 */
{
flag=FALSE; /* 初始为False */
for(j=L->length-1;j>=i;j--)
{
if(L->r[j]>L->r[j+1])
{
swap(L,j,j+1); /* 交换L->r[j]与L->r[j+1]的值 */
flag=TRUE; /* 如果有数据交换,则flag为true */
}
}
}
}
注释已经很清楚了.相信你们能看懂.
复杂度分析
当序列处于逆序时,那么根据最后一个改进的代码,需要比较n*(n-1)/2次,那么你也要移动这么多次,所以他的时间复杂度为O(n^2).
简单选择排序
思路
简单选择排序法(Simple Selection Sort)是n-i次关键字间的比较,从n-i+1个记录中选出关键字最小的记录,并和第i(1≤i≤n)个记录交换之。
是不是看这个有点不理解?简单点说就是我给你一个序列,你先在这序列找最小的跟第一个交换,在剩下的序列找最小的与第二个进行交换,这样一直下去即可得到有序的序列.
代码
/* 对顺序表L作简单选择排序 */
void SelectSort(SqList *L)
{
int i,j,min;
for(i=1;i<L->length;i++)
{
min = i; /* 将当前下标定义为最小值下标 */
for (j = i+1;j<=L->length;j++)/* 循环之后的数据 */
{
if (L->r[min]>L->r[j]) /* 如果有小于当前最小值的关键字 */
min = j; /* 将此关键字的下标赋值给min */
}
if(i!=min) /* 若min不等于i,说明找到最小值,交换 */
swap(L,i,min); /* 交换L->r[i]与L->r[min]的值 */
}
}
是不是这个挺好理解的!!
复杂度分析
对于一个序列,你需要比较n*(n-1)/2次,好的情况的话交换0次,但是最差的情况则是n-1次,时间等于比较和交换次数的和,因此时间复杂度为O(n^2).虽然这个和冒泡差不多,但是性能上是比较好的相比于冒泡.
直接插入排序
思路
**直接插入排序(Straight Insertion Sort)**的基本操作是将一个记录插入到已经排好序的有序表中,从而得到一个新的、记录数增1的有序表。
步骤如下:
- 从第一个元素我们也可以叫做哨兵,开始,该元素可以认为已经被排序;
- 取出下一个元素,在已经排序的元素序列中从后向前扫描;
- 如果该元素(已排序)大于新元素,将该元素移到下一位置;
- 重复步骤3,直到找到已排序的元素小于或者等于新元素的位置;
- 将新元素插入到该位置后;
- 重复步骤2~5。
图来咯!!!
看懂了就来看代码吧!!!
/* 对顺序表L作直接插入排序 */
void InsertSort(SqList *L)
{
int i,j;
for(i=2;i<=L->length;i++)
{
if (L->r[i]<L->r[i-1]) /* 需将L->r[i]插入有序子表 */
{
L->r[0]=L->r[i]; /* 设置哨兵 */
for(j=i-1;L->r[j]>L->r[0];j--)
L->r[j+1]=L->r[j]; /* 记录后移 */
L->r[j+1]=L->r[0]; /* 插入到正确位置 */
}
}
}
根据代码:
出来的红色方块,我就把它放在了0的位置,然后经过比较,放在正确的位置上.
复杂度分析
我们考虑一下,最坏的情况,就是序列是逆序,那么我们需要比较(n+2)(n-1)/2次数
移动的次数达到了最大值(n+4)*(n-1)/2次。
如果好的情况的话,不就是排好了顺序的,就只需要比较n-1次
如果排序记录是随机的,那么根据概率相同的原则,平均比较和移动次约为(n^ 2)/4 次。因此,我们得出直接插入排序法的时间复杂度为O(n^ 2)。从这里也看出,同样的O(n^2)时间复杂度,直接插入排序法比冒泡和简单选择排序的性能要好一些。
希尔排序
希尔排序是D.L.Shell于1959年提出来的一种排序算法,在这之前排序算法的时间复杂度基本都是O(n2)的,希尔排序算法是突破这个时间复杂度的第一批算法之一。
思路
采取跳跃分割的策略:将相距某个“增量”的记录组成一个子序列,这样才能保证在子序列内分别进行直接插入排序后得到的结果是基本有序而不是局部有序。
你可能很难理解上面一段话,先来看看图吧!!说不定看了,你也不需要我来讲解了.
上图相同颜色的方块,之间隔得距离就是增量,每趟排序根据对应的增量,将待排序列分割成若干长度为m 的子序列,分别对各子表进行直接插入排序仅增量因子为1 时,整个序列作为一个表来处理,表长度即为整个序列的长度,最后得到一个有序的序列.
代码
/* 对顺序表L作希尔排序 */
void ShellSort(SqList *L)
{
int i,j,k=0;
int increment=L->length;
do
{
increment=increment/3+1;/* 增量序列 */
for(i=increment+1;i<=L->length;i++)
{
if (L->r[i]<L->r[i-increment])/* 需将L->r[i]插入有序增量子表 */
{
L->r[0]=L->r[i]; /* 暂存在L->r[0] */
for(j=i-increment;j>0 && L->r[0]<L->r[j];j-=increment)
L->r[j+increment]=L->r[j]; /* 记录后移,查找插入位置 */
L->r[j+increment]=L->r[0]; /* 插入 */
}
}
printf(" 第%d趟排序结果: ",++k);
print(*L);
}
while(increment>1);
}
复杂度分析
其实方法很有趣,但是增量怎么找,怎么去找的好,其实很难.目前还是一个数学难题,迄今为止还没有人找到一种最好的增量序列。不过大量的研究表明,当增量序列为
时,可以获得不错的效率,其时间复杂度为O(n^ (3/2)),要好于直接排序的O(n^ 2)。需要注意的是,增量序列的最后一个增量值必须等于1才行。另外由于记录是跳跃式的移动,希尔排序并不是一种稳定的排序算法。
堆排序
思路
堆排序(Heap Sort)就是利用堆(假设利用大顶堆①)进行排序的方法。它的基本思想是,将待排序的序列构造成一个大顶堆。此时,整个序列的最大值就是堆顶的根结点。将它移走(其实就是将其与堆数组的末尾元素交换,此时末尾元素就是最大值),然后将剩余的n-1个序列重新构造成一个堆,这样就会得到n个元素中的次小值。如此反复执行,便能得到一个有序序列了。
解释:
堆是具有下列性质的完全二叉树:每个结点的值都大于或等于其左右孩子结点的值,称为大顶堆;或者每个结点的值都小于或等于其左右孩子结点的值,称为小顶堆
如图(看左边的图即可)
思路是这样的:将一串序列n个数,排成大顶堆,由于大顶堆特性,根节点是最大的,将根节点和n个数的末尾数据进行交换,再把除末尾以外的数n-1个在进行组成大顶堆,再取根节点和n-1个数中的末尾数据进行交换,这样多次重复,最大的总排在了末尾,堆的数组里从最后一个到第一个是从大到小的.
可能有点绕,其实思路和简单交换排序差不多,简单交换排序他取得最小的和第一个交换第二个交换这样以此类推,堆排序呢是取得最大的和末尾进行交换,难点在于我需要不断地排成大顶堆.
我们来看看图吧!!!图在去理解话会更好!!
我自认为核心思路和简单排序是差不多的!
步骤如下:
- 先将无序的序列(R1,R2….Rn)构建成大顶堆.
- 将大顶堆的根节点R1和末尾数据Rn进行交换
- 剩下的序列再组成大顶堆(R1,R2….Rn-1)
- 将大顶堆的根节点R1和末尾数据Rn-1进行交换
- 重复上诉操作1-3即可
思路上应该不存在问题了,我们来看看代码吧!!!
/* 堆排序********************************** */
/* 已知L->r[s..m]中记录的关键字除L->r[s]之外均满足堆的定义, */
/* 本函数调整L->r[s]的关键字,使L->r[s..m]成为一个大顶堆 */
void HeapAdjust(SqList *L,int s,int m)
{
int temp,j;
temp=L->r[s];
for(j=2*s;j<=m;j*=2) /* 沿关键字较大的孩子结点向下筛选 */
{
if(j<m && L->r[j]<L->r[j+1])
++j; /* j为关键字中较大的记录的下标 */
if(temp>=L->r[j])
break; /* rc应插入在位置s上 */
L->r[s]=L->r[j];
s=j;
}
L->r[s]=temp; /* 插入 */
}
/* 对顺序表L进行堆排序 */
void HeapSort(SqList *L)
{
int i;
for(i=L->length/2;i>0;i--) /* 把L中的r构建成一个大顶堆*/
HeapAdjust(L,i,L->length);
for(i=L->length;i>1;i--)
{
swap(L,1,i); /* 将堆顶记录和当前未经排序子序列的最后一个记录交换 */
HeapAdjust(L,1,i-1); /* 将L->r[1..i-1]重新调整为大顶堆 */
}
}
/* **************************************** */
复杂度分析
它的运行时间主要是消耗在初始构建堆和在重建堆时的反复筛选上。
在构建堆的过程中,因为我们是完全二叉树从最下层最右边的非终端结点开始构建,将它与其孩子进行比较和若有必要的互换,对于每个非终端结点来说,其实最多进行两次比较和互换操作,因此整个构建堆的时间复杂度为O(n)。
在正式排序时,第i次取堆顶记录重建堆需要用O(logi)的时间(完全二叉树的某个结点到根结点的距离为⌊log2i⌋+1),并且需要取n-1次堆顶记录,因此,重建堆的时间复杂度为O(nlogn)。
所以总体来说,堆排序的时间复杂度为O(nlogn)。
归并排序
思路
归并排序(Merge Sort) 是采用 分治法(Divide and Conquer) 一种应用.
百度上的分治法解释是:
分治法可以通俗的解释为:把一片领土分解,分解为若干块小部分,然后一块块地占领征服,被分解的可以是不同的政治派别或是其他什么,然后让他们彼此异化。
对于归并排序,是将一串无序序列分成n个小序列(一个数据),两两归并(排序)会得到n/2长度为1或者2的小序列,在两两归并,这样多次重复,最后得到长度为n一个有序序列.
分治法的解释也可以很好的阐述归并排序的思路.
归并排序分成二路归并排序和多路归并排序,我们讲的是二路归并,至于多路归并排序思路上和二路是差不多的.
图来咯!!
图的展示已经很清晰了!!!
接着看代码吧!!
代码
/* 将有序的SR[i..m]和SR[m+1..n]归并为有序的TR[i..n] */
void Merge(int SR[],int TR[],int i,int m,int n)
{
int j,k,l;
for(j=m+1,k=i;i<=m && j<=n;k++) /* 将SR中记录由小到大地并入TR */
{
if (SR[i]<SR[j])
TR[k]=SR[i++];
else
TR[k]=SR[j++];
}
if(i<=m)
{
for(l=0;l<=m-i;l++)
TR[k+l]=SR[i+l]; /* 将剩余的SR[i..m]复制到TR */
}
if(j<=n)
{
for(l=0;l<=n-j;l++)
TR[k+l]=SR[j+l]; /* 将剩余的SR[j..n]复制到TR */
}
}
/* 递归法 */
/* 将SR[s..t]归并排序为TR1[s..t] */
void MSort(int SR[],int TR1[],int s, int t)
{
int m;
int TR2[MAXSIZE+1];
if(s==t)
TR1[s]=SR[s];
else
{
m=(s+t)/2; /* 将SR[s..t]平分为SR[s..m]和SR[m+1..t] */
MSort(SR,TR2,s,m); /* 递归地将SR[s..m]归并为有序的TR2[s..m] */
MSort(SR,TR2,m+1,t); /* 递归地将SR[m+1..t]归并为有序的TR2[m+1..t] */
Merge(TR2,TR1,s,m,t); /* 将TR2[s..m]和TR2[m+1..t]归并到TR1[s..t] */
}
}
/* 对顺序表L作归并排序 */
void MergeSort(SqList *L)
{
MSort(L->r,L->r,1,L->length);
}
上诉的代码用的是递归的方式,其实递归的核心是迭代,我们也可以用非递归的方式去实现它.
/* 非递归法 */
/* 将SR[]中相邻长度为s的子序列两两归并到TR[] */
void MergePass(int SR[],int TR[],int s,int n)
{
int i=1;
int j;
while(i <= n-2*s+1)
{/* 两两归并 */
Merge(SR,TR,i,i+s-1,i+2*s-1);
i=i+2*s;
}
if(i<n-s+1) /* 归并最后两个序列 */
Merge(SR,TR,i,i+s-1,n);
else /* 若最后只剩下单个子序列 */
for(j =i;j <= n;j++)
TR[j] = SR[j];
}
/* 对顺序表L作归并非递归排序 */
void MergeSort2(SqList *L)
{
int* TR=(int*)malloc(L->length * sizeof(int));/* 申请额外空间 */
int k=1;
while(k<L->length)
{
MergePass(L->r,TR,k,L->length);
k=2*k;/* 子序列长度加倍 */
MergePass(TR,L->r,k,L->length);
k=2*k;/* 子序列长度加倍 */
}
}
/* **************************************** */
复杂度分析
归并排序其实很稳定,无论是最好,还是最坏还是平时,时间复杂度都是O(nlog(n))
非递归归并排序在时间上性能比递归的要好,因为递归是个很要时间的方式.
最后一个排序了–快速排序
思路
快速排序(Quick Sort)的基本思想是:通过一趟排序将待排记录分割成独立的两部分,其中一部分记录的关键字均比另一部分记录的关键字小,则可分别对这两部分记录继续进行排序,以达到整个序列有序的目的。
步骤如下:
- 先找一个枢轴(pivot),你随意找,一般是第一个
- 找到后将比pivot小的放在前面,比pivot大的放在后面
- 就会有两个被pivot分开的子串.
- 通过递归将两个子串有序排起来就可以得到有序序列
应该很好理解吧!!聪明的小伙伴们!!
代码
/* 快速排序******************************** */
/* 交换顺序表L中子表的记录,使枢轴记录到位,并返回其所在位置 */
/* 此时在它之前(后)的记录均不大(小)于它。 */
int Partition(SqList *L,int low,int high)
{
int pivotkey;
pivotkey=L->r[low]; /* 用子表的第一个记录作枢轴记录 */
while(low<high) /* 从表的两端交替地向中间扫描 */
{
while(low<high&&L->r[high]>=pivotkey)
high--;
swap(L,low,high);/* 将比枢轴记录小的记录交换到低端 */
while(low<high&&L->r[low]<=pivotkey)
low++;
swap(L,low,high);/* 将比枢轴记录大的记录交换到高端 */
}
return low; /* 返回枢轴所在位置 */
}
/* 对顺序表L中的子序列L->r[low..high]作快速排序 */
void QSort(SqList *L,int low,int high)
{
int pivot;
if(low<high)
{
pivot=Partition(L,low,high); /* 将L->r[low..high]一分为二,算出枢轴值pivot */
QSort(L,low,pivot-1); /* 对低子表递归排序 */
QSort(L,pivot+1,high); /* 对高子表递归排序 */
}
}
/* 对顺序表L作快速排序 */
void QuickSort(SqList *L)
{
QSort(L,1,L->length);
}
/* **************************************** */
复杂度分析
快速排序比较不稳定,好的情况时间复杂度为O(nlog(n)),最坏的情况就是O(n^2),但是这个用的比较广泛比起前面的.因为你学的C++stl库中的sort就有它的影子在里面.
哇!!!终于讲完了…
总结
上诉讲的都是一些比较类排序,其实还有许多是非比较类的排序,
排序的话挺重要的.接下来我会把排序的cpp发出来需要的就去下载好吧!!!结合代码学习是必要的!!!
推荐
这个模块呢是推荐一些事情的,我的这个博客知识点的来源主要是
伍迷大佬的书就是大话数据结构这本书,你们可以去看看,挺好的,有钱就买,没钱就看看他的博客,挺全的,很适合新手学习数据结构.
一像素大佬动图来自于此,他的语言主要是JavaScript如果有前端的小伙伴可以看一下,挺好的一篇博客.
如果大家没有看懂我的讲述请评论,我来改,如果实在不能理解就去看看这两个博客,或者去B站找视频看推荐的话有懒猫老师的数据结构,小甲鱼(我觉得有些还可以有些我觉得有点迷—个人观念)或者清华北大的数据结构
其实我就是一个知识的搬运工.
如果有侵权的请私信我.
毕竟我是个小菜鸡,语言上有些是我自己的理解,有些是大话数据结构上的,中和来看吧,再结合图可能会好点!!挺长的!!如果我的语言让你有了错误的理解,请及时告诉我来更改.!!!
闲话
最近挺忙的,打游戏时间都少了.我对我的未来感到迷茫,不知道自己应该从事什么行业或者某一类的程序员,现在的选择就是不考研,尽量让自己的能力达到工作的水平!!虽然我的专业是物联网,但是我真的不知道怎么走.未来可期吧!!!加油吧!!
你们好,我是大一小菜鸡又菜瘾还大