我们在这里主要来讨论的是单个关键字的排序。
排序的稳定性:
假设Ki=Kj,i不等于j,且在排序前的序列中Ki是领先与Kj的。如果排序后Ki仍是领先于Kj的,则称所用的排序方法是稳定的;反之是不稳定的。
内排序与外排序:
根据在排序过程中,待排序的记录是否全部被放置在内存中,排序分为内排序和外排序。
内排序是在整个排序过程中,待排序的所有纪录全部被放置内存中。外排序是由于需要排序的纪录太多,不能同时放置在内存中,整个排序过程需要在内外存之间多次交换数据进行。
1.时间性能
排序是处理数据中经常需要执行的一种操作,因此排序算法的时间开销是衡量其好坏的最重要标志。在内排序中,主要进行两项操作:比较和移动,比较指关键字之间的比较,移动至记录从一个位置移动到另一位置。总之,高效率的内排序算法应该是有尽可能少的关键字比较次数和尽可能少的移动次数。
2.辅助空间
评价待排序算法的另一个主要标准是执行算法需要的辅助存储空间。辅助存储空间时除了存放待排序记录所占用存储空间之外,执行算法所需要的其他存储空间。
这一章要学习七种排序的算法,按照算法的复杂度分为两大类,冒泡排序,选择排序,插入排序属于简单算法,而希尔排序,堆排序,归并排序,快速排序属于改进算法。
本文使用c语言实现相关功能。
在排序前先在结构体中定义线性表的顺序结构,方便实现排序。
typedef int ElemType;
#define MAXNUM 20
typedef struct
{
ElemType data[MAXNUM];
int len;
}SqList;
还要定义一个交换函数和展示函数,需要时直接调用
void Swap(SqList* s1,int m,int n)
{
ElemType t = s1->data[m];
s1->data[m] = s1->data[n];
s1->data[n] = t;
}
void Show(const SqList* s1,int begin=0)
{
for (int i = begin; i < s1->len + begin;++i)
{
printf("%d,",s1->data[i]);
}
puts("\b;");
}
一、冒泡排序
1,一种非标准的冒泡排序,n个待排序元素,需要进行n-1趟比较,每趟的比较次数一次从n-1次到1次。
定义两个变量i和j,一个为控制外层趟数循环,一个控制比较次数。i的初值为0,每次循环+1,到n-2结束。j从i+1开始,到n-1结束,每次循环+1。在内层循环中如果第j个小于第i个,则交换第i和第j个。这样就能保证每趟下来每趟的最小的处于每趟的第一个,这样就完成了非标准的冒泡排序(升序),实际可以将这种冒泡排序称为交换排序。c语言实现的代码如下:
void BuddleSort1(SqList* s1) //非标准的冒泡排序(并非通过比较相邻记录)交换排序而已
{
for (int i = 0; i < s1->len - 1; i++)
{
for (int j = i + 1; j < s1->len; j++)
{
if (s1->data[j] < s1->data[i])
{
Swap(s1, i, j);
}
}
}
}
下面介绍标准的冒泡排序,与上面交换排序类似,趟数相同,j从下标为0开始,直到n-1-i,每趟下来jj+1。每趟中两两相邻的比较若下标为j的大于下标为j+1的则交换。即当前元素大于下一个元素交换,这样每一趟都将最大的放在本趟的最后面。下面看代码
void BuddleSort2(SqList* s1) //正宗冒泡排序
{
for (int i = 0; i < s1->len - 1;i++)
{
for (int j = 0; j < s1->len - 1 - i; j++)//从后往前(当然从后往前也可以)
{
if (s1->data[j]>s1->data[j + 1])
{
Swap(s1,j,j+1);
}
}
}
}
当然排序算法需要考虑效率,如果在冒泡排序过程中,顺序表已经早早的成为有序序列,那么剩下的操作自然就成为了无用操作,增加了运行时间。那么针对此种情况我们可以进行下面操作,对冒泡排序过程进行检测,若已经有序则可以退出。具体一点可以增加一个标志flag,用来检测是否已经有序。详细看代码
void BuddleSort3(SqList* s1) //改进的冒泡排序(设置标记对排序过程进行监控,若以经有序了,可以跳出排序过程,提高效率)
{
int flag = 1;
for (int i = 0; flag == 1 && i < s1->len - 1;i++)
{
flag = 0;
for (int j = 0; j < s1->len - 1 - i; j++)
{
if (s1->data[j]>s1->data[j + 1])
{
Swap(s1, j, j + 1);
flag = 1;
}
}
}
}
冒泡排序的时间复杂度为O(N2),空间复杂度为O(1)。
2、选择排序
与冒泡排序类似,对于n个待排序元素,需要进行n-1趟排序,设置变量min(用来记录下标),默认min为每趟的第一个的下标,如果在比较过程中第j个小于第min个,则将j赋给min。在每趟结束后进行判断,若min变了(不是这一趟第一个),则交换下标为min的值与下标第一个的值。
void SelectSort(SqList* s1)
{
int min, i, j;
for (i = 0; i < s1->len - 1; i++)
{
min = i;
for (j = i + 1; j < s1->len; j++)
{
if (s1->data[j] < s1->data[min])
min = j;
}
if (min != i)
Swap(s1,i,min);
}
}
选择排序的时间复杂度为O(N2),空间复杂度为O(1)。
3、插入排序
我们认为顺序表的第一个元素天然有序,在这里需要定义两个下标i和j,i控制外层循环,j控制内层循环,再定义一个变量t,用于记住下标为i的元素值。有n个待排序元素就要进行n-1次插入,下标i的初值为1,每趟+1。j的初值为i-1,每趟j-1。每趟用t记录下标为i的元素。如果下标为j的元素大于t,则把j的元素值赋值给j+1。否则的话,跳出循环。出内层循环后,把t的值赋给j+1。代码如下:
void InsertSort(SqList* s1)
{
int i, j;
ElemType t;
for (i = 1; i < s1->len;i++)
{
t = s1->data[i];
for (j = i - 1; j>=0;j--)
{
if (s1->data[j] > t)
s1->data[j + 1] = s1->data[j];
else
break;
}
s1->data[j + 1] = t;
}
}
插入排序的时间复杂度为O(N2),空间复杂度为O(1)。
4、希尔排序
直接插入排序,它在某些时候的效率是很高的,如果待排序序列大致有序,那么只需要少量的插入操作,就可以完成排序操作
而希尔排序就是在插入排序的基础上进行了优化,将大量记录进行分组,分割成若干个子序列,此时每个子序列的元素个数就少了,然后在这些子序列中进行插入排序,当整个序列都基本有序时,再对全体元素进行一次插入排序。在这里引入了increment(增量)这一变量,是用来控制组距的,增量的计算公式为increment=increment/3+1。
当增量为1时停止。
void HillSort(SqList* s1)
{
int i,j,increment;
ElemType t;
increment=s1->len;
do
{
increment=increment/3+1;
for(i=increment;ilen;i+=increment)
{
t=s1->data[i];
for(j=i-increment;j>=0;j-=increment)
{
if(s1->data[j]>t)
s1->data[j+increment]=s1->data[j];
else
break;
}
s1->data[j+increment]=t;
}
while(increment>1);
}
希尔排序的发明使得我们终于突破了慢速排时序时代(时间复杂度为O(N2)),它的时间复杂度来到了O(N(3/2))。
5、堆排序
首先介绍下堆的性质,堆是具有特殊性质的完全二叉树。若每个结点的值都大于等于其左右孩子结点的值,称为大顶堆。若每个结点的值都小于等于其左右孩子的结点的值,则称为小顶堆。
这里复习下关于完全二叉树的一个性质,对一棵有n个结点的完全二叉树的结点按层编号,对任一结点i有:
1、如果i=1,则结点i是二叉树的根,如果i>1,则其双亲节点是节点i/2。
2、节点i的的左孩子是2i,节点的右孩子是2i+1。
堆排序就是利用堆进行排序的方法,它的基本思想是,将待排序序列构造称为一个大顶堆,此时整个序列的最大值就是堆顶的那个结点,然后把堆顶结点与末尾结点交换,此时最大元素就跑到了堆的最后一个,再将下标为1到n-1的结点,重新调整为大顶堆。此时再将最大元素与末尾元素交换。以此往复下去,便构成了升序序列,相当于每次把大顶堆中的最大元素与末尾元素交换,然后断开末尾元素,堆的结点数减1。这样就完成了堆排序的整个过程。了解了整个过程,接下来就首先要解决的是如何实现,将待排序序列构造成大顶堆,也就是调整函数。
关于堆的调整函数,根据需要调整的根结点的下标root,把以此根节点root为根的孩子结点全部构造成大顶堆结构。首先需要一个变量t来保存根结点的元素值,定义i是结点的左孩子2*root,此时i指向root的左孩子,因为root可能会有右孩子,所以需要进行判断若i+1如果i<=n的话,说明root存在右孩子。此时再进行判断,若右孩子大于左孩子,那么i++,i此时指向左孩子与右孩子中较大的。接下来进行判断如果i元素的值大于t的值,将i元素的值赋给root元素的值,并且把i赋给root,i自乘2。。若i元素的值不大于t的值,则循环结束。重复循环以上过程,循环的条件为i<=n,i不能越界嘛。循环结束后,将下标为root的元素值改为t。这样就完成了调整函数。代码如下:
注:由于本人将待排序序列,从下标0开始存放在顺序表中。又因为堆的根结点的序号为1,故在函数中作出了相应调整(堆结点序号减1,就对应了相应的元素)。解决了下标不一致的问题。
void HeapAdjust(SqList* s1,int root,int len)
{
//把根用临时变量存起来;
int t = s1->data[root - 1];
//i为root的左;
int i = 2 * root;
while (i<=len)
{
//下面的操作为了将i指向两个孩子中大的那个
//若root存在右孩子并且右孩子的值大于左孩子的值,则将i指向右孩子
if (i + 1 <=len&&s1->data[i+1-1]>s1->data[i-1])
{
i++;
}
//此时i已经指向较大的孩子
//如果i的值大于root的值,则将i的值赋给root
if (s1->data[i-1] >t)
{
s1->data[root-1] = s1->data[i-1];
root = i;
i *= 2;
}
else//否则退出循环
break;
}
s1->data[root - 1] = t;
}
堆排序主体函数为:
void HeapSort(SqList* s1) //堆排序下标从1开始方便,所以定义树的根的序号为root=1,根的值是s1->data[root-1];(因为数组下标从0开始);
{
for (int i = s1->len / 2; i > 0;i--)
{
HeapAdjust(s1,i,s1->len);
}
Swap(s1,0,s1->len-1);
for (int j = s1->len - 2; j > 0;j--)
{
HeapAdjust(s1,1,j+1);
Swap(s1,0,j);
}
}
从主体函数中我们可以明显看出,先将待排序序列调整为大顶堆,i从最后一个有孩子结点的结点开始依次进行大顶堆调整(因为完全二叉树有n个结点,则最后一个有孩子结点的结点为n除以2)。然后交换堆顶元素,调整,交换,调整......直到排成只剩一个根结点。则排序完成。
堆排序的时间复杂度为O(nlogn),在性能上明显好于O(n2)。
6、归并排序
归并排序本质上是利用了递归方法,将待排序序列先拆开了,在进行排序,合并,最终排成有序序列。下面的图能够直观的说明归并排序的原理。
下面附上排序主体函数,与相关的递归操作:
void Merge(SqList* s1,int first,int mid,int last)
{
ElemType t[MAXNUM];
int i, j,k;
k = 0;
i = first;
j = mid + 1;
while (i<=mid&&j<=last)
{
if (s1->data[i] data[j])
{
t[k++] = s1->data[i++];
}
else
{
t[k++] = s1->data[j++];
}
}
while (i <= mid)
{
t[k++] = s1->data[i++];
}
while (j <= last)
{
t[k++] = s1->data[j++];
}
for (int i = 0; i < k; i++)
{
s1->data[first++] = t[i];
}
}
void MSort(SqList* s1,int first,int mid,int last)
{
if (first < last)
{
MSort(s1,first,(first+mid)/2,mid);
MSort(s1,mid+1,(mid+1+last)/2,last);
Merge(s1,first,mid,last);
}
}
void MergeSort(SqList* s1)
{
int first, mid, last;
first = 0, last = s1->len - 1;
mid = (first + last) / 2;
MSort(s1, first, mid, last);
}
主体函数MergeSort给出了first、mid、last等三个参数。接下来调用Msort函数进行递归操作,先递推将序列拆分为n个单个元素。然后回归,进行排序、并在一起的操作。
其中的merge函数是进行归并的操作。
在merge函数中定义一个新的数组t,用来记录有序·序列,并定义i与j、k,i从first开始(记录上半部分下标),j由mid+1开始(j记录下半部分下标),k由0开始(用于记录t数组下标)。
现在就是将两个无序序列比较,将排好序的序列存入数组t中。
如果下标为i的元素小于下标为j的元素,那么将元素i赋给t数组中第k个,并且k与i都向后移一位(即加1),否则将元素j赋给t数组中第k个,并且k与j都向后移一位。在i<=mid并且j<=last的条件下循环。若循环结束后i<=mid,则说明上半部分中还有元素未放置,故将上半部分中剩余的元素一次放入t数组中,同理若是j<=last,将下半部分中剩余的元素一次放入t数组中。
最后将t中排好序的序列,从前往后依次赋值给原序列中下标从first开始到last的元素。
这样就能够完成归并排序的操作。
7、快速排序
我们俗称"快排“,这里需要一个关键字key将待排序序列分为两部分,比关键字小的都在关键字的左边,比它大的都放在关键字的右边。一般我们常会选择序列第一个作为我们基准的关键字。
这里也同样用到了递归,我们先大体写出排序主体函数。定义begin与end,begin为0,end为n-1。然后调用QSort函数进行排序操作。
接下来介绍QSort函数,所有操作在begin 下面减具体代码实现: 快速排序的时间复杂度为O(nlogn)。 以上对于常用算法的解释说明就先到这了,如果有疑问的话,请在下面留言,本人会尽快回复的,谢谢。 void qsort(SqList* s1,int begin,int end)
{
if (begin >= end)
return;
int i, j;
ElemType key;
i = begin;
j = end;
key = s1->data[begin]; //选取第一个作为key值
while (i