这篇博文拖了好久了,终于今天上午有时间写下来,回顾一下数据结构的大多数排序算法。
如标题所说,这篇博文包括了很多排序算法,我们一个一个来说。
第一个是折半插入排序,看到“折半”这两个字,就能感觉到他和熟悉的折半查找有相似之处。
在折半查找中,我的数组是从1开始的,而不是0,而我的数组的0号位则是用来储存每次读入的新的数据。这个可以自己决定,代码要做相应的小调整。
下面是插入搜索的核心代码。
void BInsertSort(int a[],int n)//n为当前输入的第几个数字
{
if(n == 1)
{
a[1] = a[0];
}
else
{
int low = 1;//第一个元素的位置
int high = n - 1;//最后一个元素的位置
while(low <= high)
{
int mid = (low + high) / 2;
if(a[0] < a[mid])
{
high = mid - 1;
}
else
{
low = mid + 1;
}
}
for(int i = n;i >= high + 1;i--)//将部分元素后移,将high+1的位置空出来
{
a[i + 1] = a[i];
}
a[high + 1] = a[0];
}
}
这里是对折半插入排序的讲解。
如果n是1,就表明当前输入了第一个数字,我们不用做任何处理,直接将其放在数组的第一个有效位置a[1]上,a[0]保存每次新输入的数字。
如果不是第一次输入,我们就要吧新输入的数字和之前的数字作比较,将其插入在合适的地方,这个前提是之前的所有数字都已经是有序的了。(当然,之前只有一个数字我们认为是有序的)。
比如输入第二个数字的时候,根据程序,low=1,high=1,满足while条件,mid=1,如果第二个数比第一个数小,满足第一个if,high=0,不满足while条件,跳出。在后面的赋值循环中,a[2]继承了a[1]的值,a[1]变成了新输入数字的值,数组又变的有序了。
依此推类,每次while循环结束的时候,都是以high = low - 1的形式出来,所以low的位置就是新的元素要插入的位置,我们通过for循环,逐个元素后移,将a[low]的位置空出来,再将a[0]插入,就是一个新的有序数组。
折半插入排序的时间复杂度为O(n2),空间复杂度为O(1)。
快排应该是在数据结构一书中我们最先见到的时间复杂度小于n2的算法了,下面是快排的核心代码。
void QSort(int a[],int s,int e)//s是数组的开始位置,e是数组的结束位置
{
if(s >= e)//分割之后要排序的的数组只有一个元素
{
return ;
}
else//可以默认要放在合适位置的元素为数组的第一个元素,也就是low对应的元素
{
int high = e;
int low = s;
while(high != low)
{
while(high > low&&a[high] >= a[low])
{
--high;
}
if(a[high] < a[low])
{
int temp1 = a[low];//要放在合适位置的元素已经被移动位置,不再是数组的第一个元素
a[low] = a[high];//要变化不指向目标元素的指针low
a[high] = temp1;
}
while(high > low&&a[high] >= a[low])
{
++low;
}
if(a[high] < a[low])
{
int temp2 = a[low];//目标元素第二次改变位置
a[low] = a[high];
a[high] = temp2;
}
}
QSort(a,s,low - 1);
QSort(a,low + 1,e);
}
}
快速排序的核心思想是每次将数组中的一个元素放到合适的位置,这样一直放置直到数组有序,下面是基本过程
需要两个指针,一个指向数组的首元素,一个指向数组的尾元素,需要用到递归,递归的出口是首指针大于等于尾指针。大概率是首指针和尾指针指向了同一个元素。
我们每次选定首元素,将其放置到数组中合适的位置,再从这个位置将数组分成两部分,两部分都不包括这个位置。再分别对前后两个数组进行快速排序,将前后两个数组的首元素放在合适的位置,再将两个数组分成四个数组,以此类推,直到最后分的数组中只有一个元素,也就没有移动的必要了。
下面说一下移动的具体方法。high为尾指针,low为头指针。正常情况下应该是high>low。如果我们想要排序之后的数组元素是从大到小排序的,那么在满足**high>low&&a[high] > a[low]**的时候,只需要移动尾指针,将他前移。
为什么要移动尾指针呢,因为头指针一定要指向我们一开始选定的元素,就是要放到合适位置的元素,所以不能乱动。如果出现了**a[high] < a[low]**的情况,我们就交换a[high]和a[low]中的元素,这时候指向目标元素的指针就是high指针了,下一次正常情况下要移动的就是low指针了。
最后跳出大的while循环一定是high==low的时候,这个时候high和low都指向了目标元素,最后我们在递归的调用函数,将这个数组的前部分和后部分执行相同的操作,同时设置好递归的出口,就能得到一个排好序的数组了。
归并排序的时间复杂度也是小于n2的,但是在归并的过程中要用到一个temp数组,增加了函数的空间复杂度。
归并排序的主要思想是从一开始就将数组分成尽量等长的两部分开头,中间和结尾分别用low,middle,high代表,然后用一个和原数组一样长的数组temp[]来储存排好序的数组,两个指针分别从low和middle+1开始,比较他们所指元素的大小,将较小的放到temp数组中,然后指向较小的的指针后移,重复过程,直到到达指针的结尾。
如果有一个指针到达了结尾就退出第一个while循环,然后寻找那个没有到达结尾的指针,将指针所指的元素依此放入temp数组中,直到到达那个指针的结尾。
下面是归并函数的核心代码。
在这里插入代码片void Merge(int a[],int low,int middle,int high,int temp[])
{
int p_temp = 0,p1 = low,p2 = middle + 1;
while(p1 <= middle&&p2 <= high)//要保证两个指针都没有到达结尾
{
if(a[p1] >= a[p2])//将p1所指的元素放入temp中
{
temp[p_temp++] = a[p1++];
}
else//将p2所指的元素放入temp中
{
temp[p_temp++] = a[p2++];
}
}
while(p1 <= middle)//如果p1没有到达结尾,就将p1所指的元素依此放入temp数组中
{
temp[p_temp++] = a[p1++];
}
while(p2 <= high)//原理同上
{
temp[p_temp++] = a[p2++];
}
for(int i = 0;i < high - low + 1;i++)//将排序好的temp数组复制给a数组
{
a[low + i] = temp[i];
}
}
还有第二段代码
MergeSort是将数组分成两半,Merge是将数组合并,因为递归的原理,每次都会一直把数组分成两半,直到不可再分为止,然后进行递归,所以每一步的Merge中两个数组都是有序的,这就解释了上面函数中为什么最后可以把一个没有到底的数组一股脑全部放入temp中了。
void MergeSort(int a[],int low,int high,int temp[])
{
if(high > low)
{
int middle = low + (high - low) / 2;//将数组分成两半
MergeSort(a,low,middle,temp);
MergeSort(a,middle + 1,high,temp);
Merge(a,low,middle,high,temp);
}
}
在主函数中只需要调用MergeSort函数就可以完成排序了。
基数排序需要我们先认同一个思想,如果我们要将一副扑克牌排列整齐,可以将四个花色1,2,3,4,然后在同一个花色中按照牌数大小进行排序。
像这样先对花色排序,在对牌数排序,我们就能获得一副排序好了的扑克牌。
其实这个思想也可以运用在对数字的排序上。我们可以先按照所有数字的个位数进行排序,然后按照十位,然后是百位。。。。。。,一直到最高的位数,这样一来数字同样也可以排序整齐。
如果有n个数字,每个数字都有r位,那么这个算法的时间复杂度就是O(r * n)。但是这个算法有一个跟大的局限经就是我们需要提前知道数字的范围,再根据数字有几位来决定比较的次数。
基数排序还需要一个“桶”来装对应的数据,比如有这样一串数字
int a[10] = {614,738,921,485,637,101,215,530,790,306};
要对他进行排序,我们需要一个“桶”
int temp1[10][9] = {0};//基数排序要用的桶
比如在对个位数进行计数排序的时候,先将所有数字的个位数取出来,保存在loc1中,在让loc2 = 0,temp1数组中第一个维度是指个位数是几,如果是3,就放在temp[3][]所对应的数组中。l
loc2的作用是,如果temp1[3][0]中已经放有数字了,那么这个应该放在temp1[3][0]中的数字就要放在temp1[3][1]中,这就是loc2的作用。
下面是基数排序的核心代码
void sort(int a[],int temp[][9])//将temp1中的数组按照一个统一的方式
{
int loc = 0;
for(int i = 0;i < 10;i++)
{
for(int j = 8;j >= 0;j--)//在将temp中的数字转移到原数组中的时候可以采用队列或者栈结构
{
if(temp[i][j] != 0)
{
a[loc++] = temp[i][j];
}
}
}
return ;
}
在核心代码中,我们主要做的其实不是排序,而是将刚刚在temp1中的按照某一个位数排序好的数字按照同一个方式取出来再放回a[]数组中。同一个方式是指栈方式或者队列方式,在我的代码中使用的是栈的方式。
如果temp[i]中有多个数字,他们不必要是有序的,只需要按照固定的方式取出来就可以。
通过对每一个位数进行排序,再将其用同一个方法取出来,我们就就能获得一个排列好的数组了。
再强调一下,基数排序对于数据的范围有比较大的要求。这一点在排序的时候一定要注意。
堆排序是树形选择排序的一种,虽然是树,但是不用我们真正将树实现出来,而是要用到树的思想。这里需要用到大根堆和小根堆的概念。
简单来说,如果一棵树只有三个元素,就是一个根节点和两个叶子节点,那么大根堆就是根节点大于两个叶子节点,相反,小根堆就是根节点小于两个叶子节点。
如果我们将很多数字整合成一个小根堆,每次都取根节点上的元素,取完以后,将根节点剔除,将剩下的元素在整合成小根堆,重复过程,直到最后一个元素,我们就能获得一个排序好的数组。
这里有一个完全二叉树的概念需要了解:
对于一颗完全二叉树,他的总节点数为size(从1开始),他的最后一个非叶子节点的编号一定是size/2
如果一个根的编号是a,他的两个叶子节点(如果存在两个),一定是2a和2a+1,堆排序通过比较根节点
和叶子节点的值的大小,如果根节点较大,就把根节点和叶子节点中最小的那个替换,替换之后原来根节
点上的值必须要到达叶子节点的位置,如果没有到达,就要将替换之后的根节点的位置再作为根进行比较
直到到达叶子节点,就是编号大于size/2,把完全二叉树的每一个非叶子节点进行排序,就能获得小根堆
所以在一开始我们随机给定一个数组,元素排列没有规律,我们要先将所给的数组变成一个小根堆,因为是完全二叉树,所以用上面的概念建立即可。
下面是每次生成小根堆的代码
void HeapAdjust(int a[],int root,int size)//size是树的结点的个数的一半,因为只要比较一般的情况
//就可以建立小根堆
{
int temp1 = -100;
int temp2 = -100;//这里是将temp变成数组里不会出现的数字,为了避免混淆,也可以替换成其他数字
if(root * 2 <= size)//这个if和下面的if是为了保证不越界。
{
temp1 = a[root * 2];
}
if(root * 2 + 1 <= size)
{
temp2 = a[root * 2 + 1];
}
if(temp1 != -100&&temp2 != -100)//左右孩子都存在
{
if(temp1 <= temp2)//在当前根的左右子树中寻找一个较小的用来交换
{
if(a[root] >= temp1)//需要作交换
{
a[root * 2] = a[root];
a[root] = temp1;
if(root * 2 <= size / 2)//交换之后的节点没有到达叶子节点,需要再进行比较,直到
//叶子节点
{
HeapAdjust(a,root * 2,size);
}
}
}
else
{
if(a[root] >= temp2)
{
a[root * 2 + 1] = a[root];
a[root] = temp2;
if(root * 2 + 1 <= size / 2)//交换之后的节点没有到达叶子节点,需要再进行比较,直到
//叶子节点
{
HeapAdjust(a,root * 2 + 1,size);
}
}
}
}
else if(temp1 != -100&&temp2 == -100)//这里需要注意,因为是完全二叉树,一定是现有左子树再有
//右子树,不可能出现只有右子树,没有左子树的情况。
{
if(a[root] >= temp1)//这里只需要和temp1进行对比即可。
{
a[root * 2] = a[root];
a[root] = temp1;
if(root * 2 <= size / 2)//交换之后的节点没有到达叶子节点,需要再进行比较,直到
//叶子节点
{
HeapAdjust(a,root * 2,size);
}
}
}
return ;
}
前面提到了,在建立一次小根堆之后,要将整个二叉树的根元素剔除掉,这里我们选择将根元素与数组的最后一个元素交换,(也就是最后的叶子节点),交换之后,将size的值递减,在之后的变化中就不会涉及到这个最小的数。
main函数的主要过程如下
int main()
{
int a[] = {0,49,38,65,97,76,13,27,49};
int size = 8;//总共有8个元素
for(int i = size / 2;i >= 1;i--)//建立初始堆
{
HeapAdjust(a,i,size);
}
for(int i = size;i > 1;i--)
{
int temp = a[1];//交换堆顶元素和最后一个元素
a[1] = a[i];
a[i] = temp;
size--;
for(int j = size / 2;j >= 1;j--)//再将剩下的元素生成小根堆
{
HeapAdjust(a,j,size);
}
for(int i = 1;i <= 8;i++)
{
printf("%d ",a[i]);
}
printf("\n");
}
for(int i = 1;i <= 8;i++)
{
printf("%d ",a[i]);
}
return 0;
}
堆排序只适用于顺序结构,不适用于链式结构,他又较好的时间复杂度,在时间复杂度最坏的情况下是O(n*logn)的,相对于快排的最坏时间复杂度O(n2),它更适合处理比较多的数据。
以上就是这次博文的所有排序算法了。