归并排序
(1)思想:
将待排序的元素序列分成两个长度相等的子序列,对每一个子序列排序,然后将她们合并成一个序列,合并两个子序列的过程的过程称为二路归并。
(2)步骤:
将数组按二分法分成两个区间,继续将区间进行递归划分,直到区间只有一个数为止;将相邻两个区间进行排序,使之合并为有序的区间;返回上一层递归,继续合并,直到全部被合并。
(3)图解:
(4)复杂度:
由于递归了logN层,每层排序了N常量个树,归并排序不依赖于待排序元素序列的初始输入状态,每次划分时两个序列的长度基本一致,所以归并排序的最好,最差和平均时间复杂度为O(N*log2^N).
它是一种稳定的排序算法。 由于申请了N个大小的空间进行临时存放,所以空间复杂度为O(1).
(5)代码实现:
//将子序列归并为一个完整的序列
递归版本:
void Merge(int* a,int first,int mid,int last,int *arr)
{
int begin1 = first,end1 = mid;
int begin2 = mid+1,end2 = last;
int arr_index = 0;
while(begin1 <= mid && begin2<= last)
{
if(a[begin1]<a[begin2])
{
arr[arr_index++] = a[begin1++];
}
else
{
arr[arr_index++] = a[begin2++];
}
}
while(begin1 <= mid)
{
arr[arr_index++] = a[begin1++];
}
while(begin2<= last)
{
arr[arr_index++] = a[begin2++];
}
for(int i=0;i<arr_index;i++)
{
a[first+i] = arr[i];
}
}
//递归不断划分区间
void Merge_sort(int* a,int first,int last,int* arr)
{
if(first>=last)
return;
int mid = (first+last)/2;
Merge_sort(a,first,mid,arr);//左有序边
Merge_sort(a,mid+1,last,arr);//右边有序
Merge(a,first,mid,last,arr);//将左右序列进行归并排序
}
bool MergeSort(int a[],int n)
{
int *arr = new int[n];
if(arr == NULL)
return false;
Merge_sort(a,0,n-1,arr);
for(int i = 0;i < n;i++)
cout<<a[i]<<" ";
delete[] arr;
return true;
}
int main()
{
int a[] = {2,4,9,3,6,8,7,1,5};
MergeSort(a,sizeof(a)/sizeof(a[0]));
return 0;
}
递归方式将数组划分为不能再划分为止,再依次进行排序后归并,栈帧开销非常大,可以采用小区间优化的方式,在划分的过程利用插入排序直接完成此段空间的排序过程。较少递归次数当数据量很大的时候非递归就比较实用。
优化:
void Merge(int* a,int first,int mid,int last,int *arr)
{
int begin1 = first,end1 = mid;
int begin2 = mid+1,end2 = last;
int arr_index = 0;
while(begin1 <= mid && begin2<= last)
{
if(a[begin1]<a[begin2])
{
arr[arr_index++] = a[begin1++];
}
else
{
arr[arr_index++] = a[begin2++];
}
}
while(begin1 <= mid)
{
arr[arr_index++] = a[begin1++];
}
while(begin2 <= last)
{
arr[arr_index++] = a[begin2++];
}
for(int i=0;i<arr_index;i++)
{
a[first+i] = arr[i];
}
}
void MergeSort(int *a,int len)
{
int size = 1;
int left,right,mid;
left=right=mid = 0;
int *tmp=new int[len];
while(size<=len-1)
{
left = 0;
while(left+size<=len-1)
{
mid = left+size-1;
right = mid+size;
if(right>len-1)
right = len-1;
Merge(a,left,mid,right,tmp);
left = right + 1;
}
size*=2;
}
}
前面学习的一些排序算法均属于比较排序,接下来了解两种非比较排序的排序算法:
计数排序
(1)概念:
计数排序又叫鸽巢原理,是对哈希直接定址法的变形应用
(2)思想:
统计相同元素出现次数;根据统计结果将序列回收到原来的序列中。
哈希表是顺序的,所以我们统计完后直接遍历哈希表,将数据再重新写会原数据空间就可以完成排序。
(3)注意:
计数排序只适用于对数据范围比较集中的数据集合进行排序。如果数据过于分散就会浪费空间。比如有100个数,其中前99个数字都小于100,最后一个数是10000,如果开辟10000个数据大小的哈希表来进行排序就太浪费了。
还有一个问题就是数据集中段不是从0开始,比如有1000个数,1001~2000。此时哈希表如果开0~2000就太浪费空间,直接从1001开始开辟,就需要预先遍历一遍数组,求出数据范围:范围
= 最大值-最小值+1.这里2000-1001+1=1000,用1代表1001,1000代表2000,这样就节省了一半多的空间。
(4)复杂度:
时间复杂度:我们需要遍历两边数组,第一遍遍历原数据统计数据出现的次数,复杂度为O(N);
第二遍遍历哈希表,向原空间写数据,遍历了范围次,时间复杂度为O(range)。
空间复杂度:开辟了范围大小的辅助哈希表,所以空间复杂度为O(range)。
(5)图解:
(6)代码实现:
//计数排序
void CountSort(int *a,int n)
{
assert(a);
int max = a[0];
int min = a[0];
//找出最大数和最小数,
for(int i = 0;i < n;i ++)
{
if(a[i]>max)
max = a[i];
if(a[i]<min)
min = a[i];
}
//确定哈希表的大小
int range = max-min+1;
int *arr = new int[range];
memset(arr,0,sizeof(int)*range);//开辟哈希表后初始化为0
for(int i = 0;i < n;++i)
{
arr[a[i]-min]++;//统计各数据出现的次数
}
int j = 0;
for(int i = 0;i < range;++i)
{
while((arr[i]--)>0)
{
a[j]=i+min;
j++;
}
}
}
void Print(int *a,size_t n)
{
assert(a);
for(size_t i = 0;i<n;i++)
{
cout<<a[i]<<" ";
}
cout<<endl;
}
int main()
{
int a[] = {3,4,3,2,1,2,6,5,4,7};
CountSort(a,sizeof(a)/sizeof(a[0]));
Print(a,sizeof(a)/sizeof(a[0]));
}
基数排序
(1)思想:
以十进制为例,基数指的是数的位,从低位到高位,先比较个位相同的放到一个位置,再比较十位,最后比较百位。这样子最后读取出来就是排序的结果:
(2)分类:
LSD:排序方式由数值的最右边(低位)开始。适用于位数多的数列。
MSD:由数值的最左边(高位)开始。适用于位数少的数列。
(3)图解:
根据个位数值,遍历数据时将它们分配至0-9号对应的桶内(个位数值与桶号一一对应):
分配结束按照桶号有小到大顺序重新收集数据,得到此序列:81 22 73 93 43 14 55 65 28 39
接着根据十位数值再进行一次分配(原理同按个位分配一样),分配结果如下:
得到最终有序序列:
14 22 28 39 43 55 65 73 81 93
(4)局限性:
只适用于正整数或者一定限制的字符串,而且内存花销比较大。