排序大的分类可以分为两种:内排序和外排序。在排序过程中,全部记录存放在内存,则称为内排序,如果排序过程中需要使用外存,则称为外排序。下面讲的排序都是属于内排序。内排序有可以分为以下几类:
一、八大内排序算法介绍
插入排序:(1)直接插入排序
(2)希尔排序
选择排序:(3)简单选择排序
(4)冒泡排序
交换排序:(5)堆排序
(6)快速排序
归并排序:(7)归并排序
桶排序: (8)桶排序
二、各排序算法详解及其代码实现
1.直接插入排序
将数字序列分为两部分:已排序部分和待排序部分,每次从无序序列中取出第一个元素,把它插入到有序序列的合适位置使有序序列仍然有序。先将序列的第一个记录看做是一个有序的子序列,第二个及其后的记录视为一个无序序列,然后从第二个记录开始,每次取出一个记录,与有序序列中的记录(依次与有序序列从后向前的数字)相比较,将其放在合适位置,得到一个新的且记录数加1的有序序列,依照此方法,直至整个序列都有序为止。
下面,给定一个序列{12,5,28,23,19,46,80,44},直接插入排序的过程如图所示:
具体代码实现如下:
void InsertSort(int arr[],int len)
{
int tmp=0;//存放临时量
int i=1;//无序序列首元素下标
int j=i-1;//有序序列尾元素下标
for(i;i<len;i++)
{
tmp=arr[i];
for(j=i-1;j>=0 && arr[j]>tmp;j--)
{
arr[j+1]=arr[j];
}
arr[j+1]=tmp;
}
}
2.希尔排序
希尔排序也叫做增量递减排序。该算法的思想是将整个无序序列分隔成若干个小的子序列分别进行插入排序,是直接插入排序算法的一种更高效的改进版本。希尔排序是把记录按下标的一定增量进行分组,对每组使用直接插入排序算法排序;随着增量逐渐减少,每组包含的记录越来越多,当增量减至1时,整个序列正好被分成一组,进行直接插入排序后,整个序列为有序序列,算法终止。注意:希尔排序增量数组中的最后一个必须为1。
下面给出对序列{46,5,28,23,19,12,80,44,36,57}进行希尔排序的过程(假设增量数组为{5,3,1})
具体代码实现如下:
void Shell(int arr[],int len,int gap)
{
int tmp;
int i;
int j;
for(i=gap;i<len;i++)
{
tmp=arr[i];
for(j=i-gap;j>=0;j-=gap)
{
if(arr[j]<=tmp)
{
break;
}
else
{
arr[j+gap]=arr[j];
}
}
arr[j+gap]=tmp;
}
}
void ShellSort(int arr[],int len)
{
int dk[]={5,3,1};
int dlen=sizeof(dk)/sizeof(dk[0]);
for(int i=0;i<dlen;++i)
{
Shell(arr,len,dk[i]);
}
}
3.简单选择排序
算法思想:每一趟从待排序的元素中选出最小(最大)的元素,顺序放在待排序的数列最前,知道全部待排序的数据元素全部排好序。从待排序列中找出最小的元素与第一个元素换位置,待排序列关键字个数-1,从待排序列其余关键字(不包括第一个关键字)中找到最小元素,插入到已排序序列尾部,依此方法,直到待排序列为个数为1停止。假设待排序列共有n个关键字,则需进行n-1趟排序。
对{12,5,28,23,19,46,80,44}进行简单选择排序的过程如下图所示:
具体代码实现如下:
//简单选择排序
void SimpleSelectSort(int arr[],int len)
{
int i=0;
int j=i+1;
int min=i;
for(i;i<len-1;++i)
{
min=i;
/*假设第一个为最小元素,从后面n-1个元素中逐个与第一个比较大小,每次比较完后,
下标记为min,最后找出最小的,看其是否为第一个元素,若不是,与第一个元素交换。
每趟过后,待排序数字个数-1(用第一层i的循环控制)*/
for(j=i+1;j<len;++j)
{
if(arr[j]<arr[min])
{
min=j;
}
}
if(min!=i)
{
int tmp=arr[min];
arr[min]=arr[i];
arr[i]=tmp;
}
}
}
4.冒泡排序
冒泡排序算法思想:
(1)将整个待排序的记录序列划分成有序区和无序区,初始状态有序区为空,无序区包括所有待排序的记录。
(2)对无序区从前向后依次将相邻记录的关键字进行比较,若逆序将其交换,从而使得关键字值小的记录向上"飘浮"(左移),关键字值大的记录,向下“堕落”(右移)。
每经过一趟冒泡排序,都使无序区中关键字值最大的记录进入有序区,对于由n个记录组成的记录序列,最多经过n-1趟冒泡排序,就可以将这n个记录重新按关键字顺序排列。
对{12,5,28,23,19,46,80,44}进行一趟冒泡排序的过程如下图所示:
具体代码实现如下:
//冒泡排序
void BubbleSort(int arr[],int len)
{
for(int i=0;i<len-1;++i)
{
for(int j=0;j<len-i-1;++j)
{
if(arr[j]>arr[j+1])
{
int tmp=arr[j+1];
arr[j+1]=arr[j];
arr[j]=tmp;
}
}
}
}
5.堆排序
以数字序列{12,5,28,23,19,46,80,44}为例
算法思想:根据父与左右孩子下标之间的关系:父--》子 n--》左:2n+1 右:2n+2;子--》父 n->(n-1)/2
第一次将数据调整为大根堆,因为大根堆中序列中最大的数据总是在根,每次都让根数据与堆上最后一个未排序的叶子节点交换可将待排序部分的最大数据加入到已排序部分。
(1)根据所给的数据序列,构建出一棵完全二叉树;
(2)对该完全二叉树进行调整,将其调整为大堆或者小堆,调整时从最后一个子树开始调整
这里要明确大堆和小堆的概念:
大堆:指的是根节点大于他左右孩子
小堆:指的是根节点小于他左右孩子
(3)将最后一个元素和最顶元素进行交换,得到最大或最小的元素值;该最大或最小的元素值位于堆的最后一个叶子节点处 (4)调整除最后一个元素外的堆,使其成为初始定义的大堆或者小堆;
(5)重复(3)(4),直到整个序列有序为止;
以12,5,28,23,19,46,80,44为例,进行堆排序的过程如下(这里以大顶堆为例):
代码实现:
void HeapAdjust(int arr[],int start,int end)//O(logn)
{
int tmp=arr[start];
int parent=start;
for(int i=2*start+1;i<=end;i=2*i+1)
//why?i每次为2*i+1,我们将它初始化为左孩子,因为右孩子有可能没有,即使左孩子也有可能没有
{
if(i+1<=end && arr[i]<arr[i+1])
{
i++;//保存左右孩子较大值的下标
}
if(arr[i]>tmp)
{
arr[parent]=arr[i];
parent=i;
}
else
{
break;
}
}
arr[parent]=tmp;
}
void HeapSort(int arr[],int len)//O(nlogn);O(1);不稳定
{
//建立大根堆
int i;
for(i=(len-1-1)/2;i>=0;i--)//从最后一个子树的根节点开始 O(nlogn)
{
HeapAdjust(arr,i,len-1);
//end为啥是len-1呢,只有最后一个子树的end下标最大为len-1,我们将其他子树的end尽量放大到len-1
}
int tmp;
for(i=0;i<len-1;i++)//O(nlogn)
{
tmp=arr[0];
arr[0]=arr[len-1-i];
arr[len-1-i]=tmp;
HeapAdjust(arr,0,len-1-i-1);
}
}
6.快速排序
算法思想:通过一趟排序将待排部分分割为独立的两部分,其中一部分记录的关键字均另一部分记录的关键字小,则可分别对这两部分记录继续进行排序,以达到整个序列有序。
首先任意选取一个记录存放在tmp中,通常选取第一个记录,然后设置两个指针left和right,一个从前向后遍历另一个从后向前遍历,遍历的过程中,将所有小于tmp中数据的记录放在其左部分,另外大于tmp的部分放在其右部分,直到两个指针相遇,将tmp放到相遇点,作为分界线;依次类推,直到有序;
以12,5,28,23,19,46,80,44为例,进行快速排序:
int Partition(int arr[],int left,int right)
{
int tmp=arr[left];
while(left<right)
{
while(left<right && arr[right]>=tmp)
{
right--;
}
if(left==right)
{
break;
}
else
{
arr[left]=arr[right];
}
while(left<right && arr[left]<=tmp)
{
left++;
}
if(left==right)
{
break;
}
else
{
arr[right]=arr[left];
}
}
arr[left]=tmp;
return left;
}
void Quick(int arr[],int left,int right)
{
int par=Partition(arr,left,right);
if(left+1<par)//左边至少两个数据
Quick(arr,left,par-1);
if(par+1<right)//右边至少两个数据
Quick(arr,par+1,right);
}
void QuickSort(int arr[],int len)
{
Quick(arr,0,len-1);
}
7.归并排序
算法思想: 初始化序列含若有n个记录,则可称为n个有序的子序列,每个子序列的长度为1,然后两两归并,得到2/n个长度为2或1的有序子序列;再两两归并,...如此重复,直到得到一个长度为n的有序序列为止;
归并排序是基于分治法递归的,
第一步:递归划分初始序列,直到每个元素为一个序列;
第二步:划分完成后,再进行回溯,对每个序列进行排序合并。
以12,5,28,23,19,46,80,44为例,进行归并排序:
//归并排序
/*二路归并 两个归并段low1 high1,low2 high2,low1与low2相比,
谁小谁先下来,下来的那个++,再low1和low2相比依此方法*/
void Merge(int arr[],int len,int gap)//gap归并段的长度 O(n)
{
int *brr=(int *)malloc(len*sizeof(int));
assert(brr!=NULL);
int i=0;//brr下标
int low1=0;//第一个归并段的起始下标,下标可取
int high1=low1+gap-1;//第一个归并段的结束下标,下标可取
int low2=high1+1;//第二个归并段的起始下标,下标可取
int high2=low2+gap-1<len-1 ? low2+gap-1 : len-1;//第二个归并段的结束下标,下标可取
while(low2<len)//判断有两个归并段
{
//两个归并段都有数据
while(low1<=high1 && low2<=high2)
{
if(arr[low1]<=arr[low2])
{
brr[i++]=arr[low1++];
}
else
{
brr[i++]=arr[low2++];
}
}
//一个归并段没有数据,另一个还有
while(low1<=high1)
{
brr[i++]=arr[low1++];
}
while(low2<=high2)
{
brr[i++]=arr[low2++];
}
low1=high2+1;
high1=low1+gap-1;
low2=high1+1;
high2=low2+gap-1<len-1 ? low2+gap-1 : len-1;
}
//不足两个归并段
while(low1<len)//high1有可能越界
{
brr[i++]=arr[low1++];
}
for(i=0;i<len;i++)//将有效数据从brr中放回到arr中
{
arr[i]=brr[i];
}
free(brr);
}
void MergeSort(int *arr,int len)//O(nlogn);O(n);稳定
{
for(int i=1;i<len;i*=2)//O(logn)
{
Merge(arr,len,i);
}
}
8.桶排序
算法思想:桶排序是和前面所学的排序方法完全不同的一种排序,该排序方法是一种借助多关键字排序的思想对单逻辑关键字进行排序的方法;
桶排序的基本步骤是:
(1)遍历整个序列,找到最大的关键字,并获取该关键字的位数;
(2)建立”桶“这一结构,利用二维数组实现;需要表示该“桶”的编号,以及每个”桶“内的编号;
(3)遍历数组,从第一个关键字开始,获取其个位数字,根据所获得的数字将该关键字放到与所获数字对应的“桶”编号内;
(4)一次结束后,将“桶”内的关键字依次出”桶“;
(5)对出桶的元素重复(3)(4),直到获得到最高位的元素并出桶完成;
注意:
桶排序在大多数情况下是要快于快速排序的;桶排序是利用空间替换时间,所需要的空间大,但时间复杂度低;
以123,2567,89042,25,660为例,进行桶排序的过程如下:
#define MAXSIZE 10
int FindMaxFinger(int arr[],int len)
{
int maxnum=arr[0];
for(int i=1;i<len;++i)
{
if(arr[i]>maxnum)
{
maxnum=arr[i];
}
}
int count=0;
while(maxnum!=0)
{
maxnum/=10;
count++;
}
return count;
}
int FindFinNumber(int num,int fin)//找某个数的第fin位
{
return num/(int)pow(10.0,fin)%10;//(pow(x,y)计算x的y次幂)
}
void Radix(int arr[],int len,int fin)//入桶顺序:排的是哪一位(fin)
{
int backet[10][MAXSIZE]={};
int finnum=0;
int num[10]={};
for(int i=0;i<len;++i)
{
finnum=FindFinNumber(arr[i],fin);
backet[finnum][num[finnum]]=arr[i];
num[finnum]++;
}
int aindex=0;
int bindex=0;
for(int i=0;i<10;++i)
{
bindex=0;
while(bindex!=num[i])
{
arr[aindex++]=backet[i][bindex++];
}
}
}
void RadixSort(int arr[],int len)
{
int max=FindMaxFinger(arr,len);//最大数的位数
for(int i=0;i<max;++i)
{
Radix(arr,len,i);
}
}
三、各排序算法总结
本文中所介绍的八大排序均属于内排序,何为内排序:就是将所有的数据放在内存中进行排序,因此当数据量过多时,采用内排序会消耗大量内存。但由于数据量过大时无法将所有的数据都加载到内存中,此时需要借助外存(磁盘)来完成排序,这就称为外排序。
发现一篇好的关于排序的文章:
https://blog.csdn.net/sunxianghuang/article/details/51872360