排序作为计算机程序设计中的一种重要运算,在实际中应用很广,据-统计,计算机处理的25%的机时是用于排序的。
1、排序的分类
根据排序中所涉及的存储器,可将排序分为内部排序和外部排序两大类
1、内部排序:排序过程中,所有记录都放在内存中处理的是内部排序。
2、外部排序:当待排序的记录太多,排序是不仅要使用内存,而且还要使用外部存储器的排序方法是外部排序。
2、排序算法的性能分析
- 稳定性
1、稳定:如果待排序的记录中,存在多个关键字相同的记录,经排序后这些记录的相对次序仍然保持不变。
2、不稳定:如果待排序的值中,存在多个关键字相同的值,经排序后这些值的相对次序发生改变,称为不稳定的排序。 - 时间复杂度
1、 时间复杂度可以认为是对排序数据的总的操作次数。反映当n变化时,操作次数呈现什么规律。
2、常见的时间复杂度有:常数阶O(1),对数阶O(log2n),线性阶O(n), 线性对数阶O(nlog2n),平方阶O(n2)。
3、时间复杂度O(1):算法中语句执行次数为一个常数,则时间复杂度为O(1)。 - 空间复杂度
1、算法在计算机内执行时所需存储空间的度量,它也是问题规模n的函数。
2、空间复杂度O(1):当一个算法的空间复杂度为一个常量,即不随被处理数据量n的大小而改变时,可表示为O(1)。
3、空间复杂度O(log2N):当一个算法的空间复杂度与以2为底的n的对数成正比时,可表示为O(log2n) ax=N,则x=logaN。
4、空间复杂度O(n):当一个算法的空间复杂度与n成线性比例关系时,可表示为0(n)。 - 复杂度分析: 1.png
3、排序算法
3.1、冒泡排序(Bubble Sort)
基本思路:从R1开始,两两比较相邻值的关键字,即比较Ri和Ri+1(i=1,2,...,n-1)的关键字大小,若逆序(如Ki > Ki+1),则交换Ri 和Ri+1的位置,如此经过一趟排序,关键字最大的记录被安置在最后一个位置(Rn)上。然后再对前n-1个记录进行同样的操作,具有次大关键字的值被安置在Rn-1上。如此反复。
void BobbleSort(int array[],int N){
bool bExchangeFlag;
for (int i = 1; i < N; i++) { //最多做length-1趟排序
bExchangeFlag = false; //
for (int j = 0; j < N - i; j++) {
if (array[j] > array[j+1]) {
swap(&array[i], &array[j+1]);
bExchangeFlag = true;
}
}
if (!bExchangeFlag) {//本趟排序未发生交换,则提前终止算法
break;
}
}
}
3.1.1、性能分析
稳定性:不稳定
分类:内排序
时间复杂度
(1)最好情况:O(n)--正好是正序的,不需要交换数据
(2)最坏情况:O--逆序
(3)平均复杂度:O(n)
(4)空间复杂度:O(1)
3.2、插入排序(Insertion Sort)
基本思路:构建有序序列,对于未排序数据,在已排序中从后向前扫描,找到相应位置插入,直到将未排序队列过滤完毕。
排序过程如下:
2.png
void Insertion_Sort(int array[],int N){
for (int p = 1; p < N; p++) {
int temp = array[p];
int I;
for (i = p; p > 0 && array[i-1] > temp; i--) {
array[i] = array[i-1];
}
array[i] = temp;
}
}
3.2.1、性能分析
稳定性:稳定
分类:内排序
时间复杂度
(1)最好情况:O(n)--当待排序数组是有序时
(2)最坏情况:O)--待排序数组是逆序时
(3)平均复杂度:O(n)
(4)空间复杂度:O(1)
3.3、希尔排序(Shell Insertion Sort)
基本思路:定义增量序列 > >...> = 1,对每个进行“间隔”插入排序。
注意:“间隔”有序的序列,在执行“间隔”排序后,仍然是“间隔”有序的。
排序过程如下:
#include<stdio.h>
#include <math.h>
#define MAXNUM 10
//根据当前增量进行插入排序
void shellInsert(int array[],int n,int dk)
{
int i,j,temp;
for(i=dk;i<n;i++)//分别向每组的有序区域插入
{
temp=array[I];
for(j=i-dk;(j>=i%dk)&&array[j]>temp;j-=dk)//比较与记录后移同时进行
array[j+dk]=array[j];
if(j!=i-dk)
array[j+dk]=temp;//插入
}
}
//计算Hibbard增量
int dkHibbard(int t,int k)
{
return (int)(pow(2,t-k+1)-1);
}
void Shell_Sort(int array[],int n,int t)
{
for(int i=1;i<=t;i++)
shellInsert(array,n,dkHibbard(t,i));
}
void ShellSort(void){
int array[] = {1,10,3,5,4,2,15,9,54,18};
Shell_Sort(array,MAXNUM,(int)(log(MAXNUM+1)/log(2)));//排序趟数应为log2(n+1)的整数部分
for(int i = 0;i< MAXNUM;i++)
printf("%d ",array[I]);
}
再看一个序列:1 9 2 10 3 11 4 12 5 13 6 14 7 15 8 16
4.png
可以看到增量元素不互质,则小增量可能不起作用,所以选择增量序列模式很重要。
目前主流增量序列:
Hibbard 增量序列:= - 1 相邻元素互质
最坏情况:T = O
猜想: = O
Sedgewick 增量序列:{1,5,19,41,109,...}
9-9+1或-3+1
猜想: = O, = O
3.3.1、性能分析
稳定性:不稳定
分类:内排序
时间复杂度
(1)最好情况:O()
(2)最坏情况:O()
(3)平均复杂度:O()
(4)空间复杂度:O()
3.4、快速排序(Shell Insertion Sort)
基本思路:设要排序的数组是A[0]……A[N-1],首先任意选取一个数据(通常选用数组的最后一个数)作为关键数据,然后将所有比它小的数都放到它左边,所有比它大的数都放到它右边,将该数组分成2个数组,再递归的处理左边数组和右边数组。
排序过程如下:
5.png
int Median(int array[],int left,int right){
int center = (left + right)/2;
if (array[left] > array[center]) {
swap(&array[left], &array[center]);
}
if (array[left] > array[right]) {
swap(&array[left], &array[right]);
}
if (array[center] > array[right]) {
swap(&array[center], &array[right]);
}
swap(&array[center], &array[right - 1]);
return array[right - 1];
}
void QickSort(int array[],int left,int right){
if (1 < right-left) {
int pivot = Median(array, left, right);
int i = left;
int j = right - 1;
for (;;) {
while (array[++i] < pivot) {}
while (array[--j] > pivot) {}
if (i < j) {
swap(&array[i], &array[j]);
}else{
break;
};
}
swap(&array[i], &array[right - 1]);
QickSort(array, left, i-1);
QickSort(array, i+1, right);
}
}
void quick_sort(int array[],int N){
QickSort(array, 0, N - 1);
}
void quicksort(void){
int tree[] = {1,10,3,5,4,2,15,9,54,18};
int n = 10;
quick_sort(tree, 10);
for (int i = 0; i < n; i++) {
printf("%d ",tree[I]);
}
}
3.4.1、性能分析
稳定性:不稳定
分类:内排序
时间复杂度
(1)最好情况:O(n)--当待排序数组是有序时
(2)最坏情况:O--待排序数组是逆序时
(3)平均复杂度:O(n)
(4)空间复杂度:O(n)
3.5、直接选择排序(Selection Sort)
基本思路:从待排序的数据元素中选择最小(或最大)的一个元素作为首元素,把它与第一元素交换存储位置,然后再在余下的元素中选出此小的,把它与第二元素交换,以此类推,直到排序完成。
排序过程如下:
5.png
void Select_Sort(int array[],int N){
int nMinIndex;//用于记录最小元素的下标值
for(int i = 0;i < N - 1;i++){
nMinIndex = I;
for (int j = i + 1; j < N; j++) {
if (array[j] < array[nMinIndex]) {
nMinIndex = j;
}
}
if (nMinIndex != i) {
swap(&array[nMinIndex], &array[I]);
}
}
}
3.5.1、性能分析
稳定性:不稳定
分类:内排序
时间复杂度
(1)最好情况:O--当待排序数组是有序时
(2)最坏情况:O--待排序数组是逆序时
(3)平均复杂度:O(n)
(4)空间复杂度:O(1)
3.6、归并排序(Merge Sort)
基本思想:指的是将两个顺序序列合并成一个顺序序列的排序方法。
归并排序的实现方法:
3.6.1递归方法
//L = 左边起始位置,R= 右边起始位置,RightEnd 右边终点位置
void Merge(int Array[],int TempArray[],int L,int R,int RightEnd){
int LeftEnd = R - 1;
int Temp = L; //存放结果数组的起始位置
int NumCount = RightEnd - L + 1;
while (L<=LeftEnd && R <= RightEnd) {
if (Array[L] <= Array[R]) {
TempArray[Temp++] = Array[L++];
}else{
TempArray[Temp++] = Array[R++];
}
}
while(L <= LeftEnd) { //直接复制左边剩下的元素
TempArray[Temp++] = Array[L++];
}
while(R <= RightEnd) {//直接复制右边剩下的元素
TempArray[Temp++] = Array[R++];
}
for(int i = 0;i < NumCount;i++,RightEnd--){
Array[RightEnd] = TempArray[RightEnd];
}
}
void MSort(int Array[],int TempArray[],int L,int RightEnd){
int Center;
if (L < RightEnd) {
Center = (L + RightEnd)/2;
MSort(Array, TempArray, L, Center);
MSort(Array, TempArray, Center+1, RightEnd);
Merge(Array, TempArray, L, Center+1, RightEnd);
}
}
void Merge_Sort(int Array[],int N){
int *tempArray = (int *)malloc(N*sizeof(int));
if (tempArray != NULL) {
MSort(Array, tempArray, 0, N-1);
}
}
3.6.2非递归方法
7.png
void MergePass(int Array[],int TempArray[],int L,int R,int RightEnd){
int LeftEnd = R - 1;
int Temp = L; //存放结果数组的起始位置
int NumCount = RightEnd - L + 1;
while (L<=LeftEnd && R <= RightEnd) {
if (Array[L] <= Array[R]) {
TempArray[Temp++] = Array[L++];
}else{
TempArray[Temp++] = Array[R++];
}
}
while(L <= LeftEnd) { //直接复制左边剩下的元素
TempArray[Temp++] = Array[L++];
}
while(R <= RightEnd) {//直接复制右边剩下的元素
TempArray[Temp++] = Array[R++];
}
}
void Merge_pass(int array[],int TempArray[],int N,int length){ //有序序列长度
for (int i = 0; i < N - 2*length; i +=2*length) {
Merge(array, TempArray, i, i+length, i + 2*length - 1);
if (i + length < N) {
MergePass(array, TempArray, i, i + length, N-1);
}else{
for (int j = i; j < N; j++) {
TempArray[j] = array[j];
}
}
}
}
void Merge_no_recursion_Sort(int Array[],int N){
int length = 1;//子序列长度
int *tempArray = (int *)malloc(N*sizeof(int));
if (tempArray != NULL) {
while (length < N) {
Merge_pass(Array, tempArray, N, length);
length *= 2;
Merge_pass(tempArray, Array, N, length);
length *= 2;
}
free(tempArray);
}
}
3.6.3、性能分析
性能分析:
稳定性:稳定
分类:内排序
时间复杂度
(1)最好情况:O( n)
(2)最坏情况:O( n)
(3)平均复杂度:O( n)
(4)空间复杂度:O(n)
3.7、计数排序(Count Sort)
计数排序是一种非比较排序算法,它的优势在于在对一定范围内的整数排序时,它的复杂度为Ο(n+k)(其中k是整数的范围),快于任何比较排序算法。当然这是一种牺牲空间换取时间的做法,而且当O(k)>O(n*log(n))的时候其效率反而不如基于比较的排序。
基本思想:计数排序的基本思想就是对每一个输入元素x,确定小于x的元素个数,我们就可以直接把x放到最终输出数组中的相应位置上。
排序过程:
1、查询待数组中的最大值和最小值,根据最大元素和最小元素的差值(max-min+1),申请额外空间。
2、遍历待排序数组,将每一个元素出现次数(value)记录到元素值(key)对应的额外空间内。
3、从额外空间的第二个元素开始,将当前元素个数加上前一个元素的个数,即可确定序列中值小于x的元素的个数,也就确定了元素x的位置。
4、将待排序数组的元素倒序移动到新数组中。
示例:
8.png
#include<stdlib.h>
#include<string.h>
#define MAXNUM 10
void Count_Sort(int Array[],int N){
int Max = Array[0];
int Min = Array[0];
for (int i = 1; i < N-1; i++) {
if (Array[i] >= Max) {
Max = Array[i];
}else if(Array[i] <= Min){
Min = Array[i];
}
}
int size = Max - Min +1;
int* Array_B = (int*)malloc(size*sizeof(int));
memset(Array_B, 0, sizeof(int)*size);
for (int i = 0; i < N; i++) {
Array_B[Array[i] - Min]++;
}
for (int i = 1; i < size; i++) {
Array_B[i] = Array_B[i] + Array_B[i - 1];
}
int* Array_C= (int *)malloc((N)*sizeof(int));
memset(Array_C, 0, sizeof(int)*size);
for (int i = N-1; i >= 0; i--) {
Array_B[Array[i] - Min]--;
Array_C[Array_B[Array[i] - Min]] = Array[i];
}
for (int i = 0; i < N; i++) {
Array[i] = Array_C[i];
}
free(Array_B);
free(Array_C);
Array_B = NULL;
Array_C = NULL;
}
void CountSort(void){
int array[] = {1,10,3,5,4,2,15,9,54,18};
Count_Sort(array,MAXNUM);//排序趟数应为log2(n+1)的整数部分
for(int i = 0;i< MAXNUM;i++)
printf("%d ",array[i]);
}
3.8、基数排序(Radix Sort)
基本思想:将整数按位数切割成不同的数字,然后按每个位数分别比较。
排序过程:
1、所有待排序整数统一成位数相同的数值,位数不够的整数需要将缺位补零。
2、从低位开始比较,依此进行排序。
3、从低位到高位比较完成后,待排序数组就是一个有序数组。
//获取数字位数
#include <math.h>
#define MAXNUM 10
int getLoopTimes(int num){
int count = 1;
int temp = num/10;
while (temp != 0) {
count++;
temp = temp/10;
}
return count;
}
//查询数组中的最大值
int findMaxNum(int *p,int n){
int max = 0;
for (int i = 0;i < n;i++) {
if (*(p + i) > max) {
max = *(p+i);
}
}
return max;
}
void Radix_Sort(int *p,int n,int loop){
int buckets[10][MAXNUM] = {};//建立一组桶此处的MAXNUM是预设的根据实际数情况修改,
int tempNum = (int)pow(10,loop - 1);//求整数各占位的数字
for (int i= 0; i < n; i++) {
int row_index = (*(p + i)/tempNum)%10;
for (int j = 0;j < MAXNUM;j++) {
if(buckets[row_index][j] == 0) {
buckets[row_index][j] = *(p + i);
break;
}
}
}
int k = 0;
for (int i = 0; i < 10; i++) {
for (int j = 0; j < MAXNUM; j++) {
if (buckets[i][j] != 0) {
*(p + k) = buckets[i][j];
buckets[i][j] = 0;
k++;
}
}
}
}
void bucketSort(int *p,int n){
int maxNum = findMaxNum(p, n);//获取数组中的最大数
int loopTimes = getLoopTimes(maxNum);//获取最大数的位数
for (int i = 1;i <= loopTimes;i++) {
Radix_Sort(p, n, i);
}
}
void RadixSort(){
int array[] = {512,240,666,520,43,76};
int *array_p = array;
int size = sizeof(array)/sizeof(int);
bucketSort(array_p, size);
for(int i = 0; i < size; i++){
printf("%d\n", array[i]);
}
}
3.8、桶排序(Bucket Sort)
基本思想:根据元素值特性将待排序集合拆分成多个区域,将这些区域称为桶,这些值域(桶)是处于有序的状态。对桶中元素进行排序后,所以元素就处于有序状态了。
思想交换:
1、快速排序是将待排序集合拆分成两个值域(桶)(一个有序队列,一个无序队列,也可以说是两个桶),分别对两个值域(桶)进行排序,有一点不同的是快排是原地排序,是对集合本身排序,而桶排序是多个值域(桶),对每个值域(桶)排序,桶排序需要额外的空间,在额外空间对桶进行排序,避免构建桶的过程的元素交换和比较,同时可以自主选择恰当的排序算法。
2、对计数排序的改进,计数排序也需要额外操作空间,额外空间跨度从最小值到最大值,如果待排序序列不是依次递增的,就会造成操作空间的浪费。桶排序则弱化操作空间的浪费,将从最小值到最大值每个值都申请空间,改进成从最小值到最大值按照一定数值特性分成固定区域申请空间,尽量减少由于元素值不连续而造成的空间浪费。
桶排序过程中两个关键环节【1】:
1、元素值域的划分,也就是元素到桶的映射规则。映射规则需要根据待排序集合的元素分布特性进行选择,若规则设计的过于模糊、宽泛,则可能导致待排序集合中所有元素全部映射到一个桶上,则桶排序向比较性质排序算法演变。若映射规则设计的过于具体、严苛,则可能导致待排序集合中每一个元素值映射到一个桶上,则桶排序向计数排序方式演化。
2、排序算法的选择,从待排序集合中元素映射到各个桶上的过程,并不存在元素的比较和交换操作,在对各个桶中元素进行排序时,可以自主选择合适的排序算法,桶排序算法的复杂度和稳定性,都根据选择的排序算法不同而不同。
示例:
待排序集合为:[-7, 51, 3, 121, -3, 32, 21, 43, 4, 25, 56, 77, 16, 22, 87, 56, -10, 68, 99, 70];
映射规则为:
待续。。
网友评论