Loading

排序算法

基本概念

void X_Sort(ElementType A[], int N)
  • N是正整数
  • 只讨论基于比较的排序(<、=、>有定义)
  • 只讨论内部排序
  • 稳定性:任意两个相等的数据,排序前后的相对位置不发生变化
  • 没有一种排序是任何情况下都表现最好的

简单排序

冒泡排序

每一趟冒泡可以确定至少一个元素的位置,确定一个最大值,也就是说,排序过程中序列尾部是排好序的。冒泡排序是稳定的算法。

void Bubble_Sort(ElementType A[], int N){
	int P,i;

	for(P=N-1;P>=0;P--){
		int flag=0;
		for(i=0;i<P;i++){		//一趟冒泡
			if(A[i]>A[i+1]){
				Swap(A[i],A[i+1]);
				flag=1;		//标识发生了交换
			}
		}
		if(flag==0)		//全程无交换
			break;
	}
}

最好情况:顺序 T=O(N),最坏情况:逆序 T=O(N^2)

插入排序

从1号元素开始,依次取出元素与其前面的元素比较,将其插入到合适的位置,排序过程中序列前部是排好序的。插入排序是稳定的算法。

void Insertion_Sort(ElementType A[], int N){
	int P,i;
	ElementType Tmp;

	for(P=1;P<N;P++){
		Tmp=A[P];		//取出未排序序列中的第一个元素
		for(i=P;i>0&&A[i-1]>Tmp;i--)
			A[i]=A[i-1];		//依次与已排序序列中的元素比较并右移
		A[i]=Tmp;		//放入适合的位置
	}
}

最好情况:顺序 T=O(N),最坏情况:逆序 T=O(N^2)


简单排序的平均时间复杂度为O(N^2)。

希尔排序

希尔排序是由Donald Shell提出的,所以叫希尔排序。
要进行希尔排序,首先要定义增量序列Dm > Dm-1 > ··· > D1 = 1,然后就在待排序列上对每个增量序列Dk进行“Dk-间隔”排序(k=m,m-1,···,1)。其中要注意的是,“Dk-间隔”有序的序列,在执行“Dk-1-间隔”排序后,仍然是“Dk-间隔”有序的。
那么,希尔排序的关键就是确定增量序列,之后的每个“Dk-间隔”排序就可以采用简单排序来做。

  • 原始希尔排序 Dm=[N/2], Dk=[Dk+1/2] 向下取整
void Shell_Sort(ElementType A[], int N){
	for(D=N/2;D>0;D/=2){		//希尔增量序列
		for(P=D;P<N;P++){		//插入排序
			Tmp=A[P];
			for(i=P;i>=D&&A[i-D]>Tmp;i-=D)
				A[i]=A[i-D];
			A[i]=Tmp;
		}
	}
}

最坏情况:T=O(N^2)

但是这样选取增量序列有个问题,增量元素不互质,那么小增量可能根本不起作用。

这样也就产生了跟多增量序列

  • Hibbard增量序列

    • Dk=2^k-1 —— 相邻元素互质
    • 最坏情况:T=O(N^3/2)
    • 猜想:Tavg=O(N^5/4)
  • Sedgewick增量序列

    • {1,5,19,41,109,···} —— 9×4i-9×2i+1或4i-3×2i+1
    • 猜想:Tavg=O(N^7/6), Tworst=O(N^4/3)
void Shell_Sort(ElementType A[], int N){
	//希尔排序 - 用Sedgewick增量序列
	int Si,D,P,i;
	ElementTypde Tmp;
	//只列出一小部分增量
	int Sedgewick[]={929,505,209,109,41,19,5,1,0};

	for(Si=0;Sedgewick[Si]>=N;Si++)
		;		//初始的增量Sedgewick[Si]不能超过待排序列的长度,把超过的部分滤除

	for(D=Sedgewick[Si];D>0;D=Sedgewick[++Si]){
		for(P=D;P<N;P++){		//插入排序
			Tmp=A[P];
			for(i=P;i>=D&&A[i-D]>Tmp;i-=D)
				A[i]=A[i-D];
			A[i]=Tmp;
		}
	}
}

堆排序

堆排序是利用最小堆的特性来对待排序列进行排序。

  • 算法1
    用待排序列中的元素建最小堆,再从堆中返回最小元素到新的数组中,最后把新的排好序的数组复制到原序列中。
void Heap_Sort(ElementType A[], int N){
	BuildHeap(A);		//O(N)
	for(i=0;i<N;i++)
		TmpA[i]=DeleteMin(A);		//O(logN)
	for(i=0;i<N;i++)		//O(N)
		A[i]=TmpA[i];
}

T=O(NlogN),需要额外O(N)的空间,并且复制元素需要时间。

  • 算法2
    用待排序列中的元素建最大堆,再对堆直接操作,把根结点最大值和堆的最后一个元素交换,则确定了最大值的位置,然后对其余元素进行调整(下滤),依次进行上述操作。相比算法1不需要额外的空间。
void Swap(ElementType *a, ElementType *b){
	ElementType t=*a;
	*a=*b;
	*b=t;
}

void PercDown(ElementType A[], int p, int N){
	//改编自堆的下滤操作PrecDown(MaxHeap H, int p),参考堆的那篇博客
	//将N个元素的数组中以A[p]为根的子堆调整为最大堆
	int Parent,Child;
	ElementType X;

	X=A[p];		//取出根结点的值
	for(Parent=p;Parent*2+1<N;Parent=Child){
		Child=Parent*2+1;
		if((Child!=N-1)&&(A[Child]<A[Child+1]))
			Child++;		//Child指向左右子结点的最大者
		if(X>=A[Child])		
			break;		//找到了合适位置
		else
			A[Parent]=A[Child];		//下滤
	}
	A[Parent]=X;
}

void Heap_Sort(ElementType A[], int N){
	int i;

	for(i=N/2-1;i>=0;i--)
		PercDown(A,i,N);		//建最大堆,从最后一个父结点开始往前,依次下滤
	for(i=N-1;i>0;i--){
		Swap(&A[0],&A[i]);		//交换最大值和末尾的值,相当于删除最大堆顶
		PercDown(A,0,i);		//对剩余子堆进行调整(下滤)
	}
}

堆排序处理N个不同元素的随机排列的平均比较次数是2NlogN-O(NloglogN)。虽然堆排序给出最佳平均复杂度,但实际效果不如用Sedgewick增量序列的希尔排序。

归并排序

归并排序先将待排序列分成多个序列,每个序列各自排序,最后多个有序序列合并成一个有序序列。归并算法的核心便是有序子列的归并

void Merge(ElementType A[], ElementType TmpA[], int L, int R, int RightEnd){
	//L=左边起始位置,R=右边起始位置,RightEnd=右边终点位置
	//将有序的A[L]-A[R-1]和A[R]-A[RightEnd]归并成一个有序序列
	int LeftEnd,NumElements,Tmp;
	int i;

	LeftEnd=R-1;		//左边重点位置
	Tmp=L;		//有序序列的起始位置
	NumElements=RightEnd-L+1;

	while(L<=LeftEnd&&R<=RightEnd){
		if(A[L]<=A[R])
			TmpA[Tmp++]=A[L++];		//将左边元素复制到TmpA
		else
			TmpA[Tmp++]=A[R++];		//将右边元素复制到TmpA
	}

	while(L<=LeftEnd)
		TmpA[Tmp++]=A[L++];		//直接复制左边剩下的
	while(R<=RightEnd)
		TmpA[Tmp++]=A[R++];		//直接复制右边剩下的

	for(i=0;i<NumElements;i++,RightEnd--)
		A[RightEnd]=TmpA[Rightend];		//将有序的TmpA复制回A
}

归并排序有两种实现方式,递归算法和非递归算法。

递归算法

归并排序的递归算法采用的是分而治之的思想。

void MSort(ElementType A[], ElementType TmpA[], int L, int RightEnd){
	//核心递归排序函数
	int Center;

	if(L<RightEnd){
		Center=(L+RightEnd)/2;
		MSort(A, TmpA, L, Center);		//递归解决左边
		MSort(A, TmpA, Center+1, RightEnd);		//递归解决右边
		Merge(A, TmpA, L, Center+1, RightEnd);		//合并两段有序序列
	}
}

T=O(NlogN),是稳定的算法。

统一函数接口

void Merge_Sort(ElementType A[], int N){
	ElementType *TmpA;
	TmpA=(ElementType *)malloc(N*sizeof(ElementType));

	if(TmpA!=NULL){
		MSort(A, TmpA, 0, N-1);
		free(TmpA);
	}
	else
		printf("空间不足");
}
非递归算法

归并排序的非递归算法由循环实现。

void Merge_pass(ElementType A[], ElementType TmpA[], int N, int length){		//length是当前有序子序列的长度
	//两两归并相邻有序子序列
	int i,j;

	for(i=0;i<=N-2*length;i+=2*length)
		Merge(A, TmpA, i, i+length, i+2*length-1);
	if(i+length<N)		//归并最后2个子列
		Merge(A, TmpA, i, i+length, N-1);
	else 		//最后只剩1个子列
		for(j=i;j<N;j++)
			TmpA[j]=A[j];
}

void Merge_Sort(ElementType A[], int N){
	int length;
	ElementType *TmpA;

	length=1;		//初始化子序列长度
	TmpA=(ElementType *)malloc(N*sizeof(ElementType));
	if(TmpA!=NULL){
		while(length<N){
			Merge_pass(A, TmpA, N, length);
			length*=2;
			Merge_pass(TmpA, A, N, length);
			length*=2;
		}
		free(TmpA);
	}
	else
		printf("空间不足");
}

快速排序

快速排序采用分而治之的思想,先选取一个主元pivot,比主元小的元素放在前面,而比主元大的元素放在后面。问题就是如何选取主元,这里有个方法是取头、中、尾的中位数,比如8、12、3的中位数是8。另外,如果数据规模较小,快速排序的效率可能还不如插入排序快,所以,当递归的数据规模充分小,则停止递归,直接调用简单排序。

ElementType Median3(ElementType A[], int Left, int Right){
	int Center=(Left+Right)/2;
	if(A[Left]>A[Center])
		Swap(&A[Left],&A[Center]);
	if(A[Left]>A[Right])
		Swap(&A[Left],&A[Right]);
	if(A[Center]>A[Right])
		Swap(&A[Center],&A[Right]);
	//此时,A[Left]<=A[Center]<=A[Right]
	Swap(&A[Center],&A[Right-1]);		//将基准Pivot藏到右边
	//只需要考虑A[Left+1],···,A[Right-2]
	return A[Right-1];
}

void Qsort(ElementType A[], int Left, int Right){
	int Pivot,Cutoff,Low,High;

	if(Cutoff<=Right-Left){		//如果序列元素充分多,则进入快速排序
		Pivot=Median3(A,Left,Right);		//选取主元pivot
		Low=Left;
		High=Right-1;
		while(1){		//将序列中比基准小的移到基准左边,大的移到基准右边
			while(A[++Low]<Pivot);
			while(A[--High]>Pivot);
			if(Low<High)
				Swap(&A[Low],&A[High]);
			else
				break;
		}
		Swap(&A[Low],&A[Right-1]);		//将基准放到正确的位置
		Qsort(A,Left,Low-1);		//递归解决左边
		Qsort(A,Low+1,Right);		//递归解决右边
	}
	else
		Insertion_Sort(A+Left,Right-Left+1);		//元素太少,用插入排序
}

void Quick_Sort(ElementType A[], int N){		//统一接口
	Qsort(A,0,N-1);
}

如果pivot选取的好,那么快速排序的时间复杂度为T=O(NlogN)。

基数排序

/* 假设元素最多有MaxDigit个关键字,基数全是同样的Radix */
#define MaxDigit 4
#define Radix 10
 
typedef struct Node *PtrToNode;
struct Node{		//桶元素结点
    int key;
    PtrToNode next;
};

struct HeadNode{		//桶头结点
    PtrToNode head, tail;
};
typedef struct HeadNode Bucket[Radix];
  
int GetDigit(int X, int D){
	/* 默认次位D=1, 主位D<=MaxDigit */
    int d, i;
     
    for(i=1; i<=D; i++){
        d = X % Radix;
        X /= Radix;
    }
    return d;
}

void LSDRadixSort(ElementType A[], int N){    
	//基数排序-次位优先
    int D, Di, i;
    Bucket B;
    PtrToNode tmp, p, List = NULL; 
      
    for(i=0; i<Radix; i++) /* 初始化每个桶为空链表 */
        B[i].head = B[i].tail = NULL;
    for(i=0; i<N; i++){ /* 将原始序列逆序存入初始链表List */
        tmp = (PtrToNode)malloc(sizeof(struct Node));
        tmp->key = A[i];
        tmp->next = List;
        List = tmp;
    }
    /* 下面开始排序 */ 
    for(D=1; D<=MaxDigit; D++){ /* 对数据的每一位循环处理 */
        /* 下面是分配的过程 */
        p = List;
        while(p){
            Di = GetDigit(p->key, D); /* 获得当前元素的当前位数字 */
            /* 从List中摘除 */
            tmp = p; p = p->next;
            /* 插入B[Di]号桶尾 */
            tmp->next = NULL;
            if(B[Di].head == NULL)
                B[Di].head = B[Di].tail = tmp;
            else{
                B[Di].tail->next = tmp;
                B[Di].tail = tmp;
            }
        }
        /* 下面是收集的过程 */
        List = NULL; 
        for(Di=Radix-1; Di>=0; Di--){ /* 将每个桶的元素顺序收集入List */
            if(B[Di].head){ /* 如果桶不为空 */
                /* 整桶插入List表头 */
                B[Di].tail->next = List;
                List = B[Di].head;
                B[Di].head = B[Di].tail = NULL; /* 清空桶 */
            }
        }
    }
    /* 将List倒入A[]并释放空间 */
    for(i=0; i<N; i++){
        tmp = List;
        List = List->next;
        A[i] = tmp->key;
        free(tmp);
    } 
}


//基数排序-主位优先

void MSD(ElementType A[], int L, int R, int D){ 
	/* 核心递归函数: 对A[L]...A[R]的第D位数进行排序 */
    int Di, i, j;
    Bucket B;
    PtrToNode tmp, p, List = NULL; 
    if (D==0) return; /* 递归终止条件 */
      
    for(i=0; i<Radix; i++) /* 初始化每个桶为空链表 */
        B[i].head = B[i].tail = NULL;
    for(i=L; i<=R; i++){ /* 将原始序列逆序存入初始链表List */
        tmp = (PtrToNode)malloc(sizeof(struct Node));
        tmp->key = A[i];
        tmp->next = List;
        List = tmp;
    }
    /* 下面是分配的过程 */
    p = List;
    while (p){
        Di = GetDigit(p->key, D); /* 获得当前元素的当前位数字 */
        /* 从List中摘除 */
        tmp = p; p = p->next;
        /* 插入B[Di]号桶 */
        if(B[Di].head == NULL) B[Di].tail = tmp;
        tmp->next = B[Di].head;
        B[Di].head = tmp;
    }
    /* 下面是收集的过程 */
    i = j = L; /* i, j记录当前要处理的A[]的左右端下标 */
    for(Di=0; Di<Radix; Di++){ /* 对于每个桶 */
        if(B[Di].head){ /* 将非空的桶整桶倒入A[], 递归排序 */
            p = B[Di].head;
            while(p){
                tmp = p;
                p = p->next;
                A[j++] = tmp->key;
                free(tmp);
            }
            /* 递归对该桶数据排序, 位数减1 */
            MSD(A, i, j-1, D-1);
            i = j; /* 为下一个桶对应的A[]左端 */
        } 
    } 
}
 
void MSDRadixSort(ElementType A[], int N){		//统一接口
    MSD(A, 0, N-1, MaxDigit); 
}

排序算法的比较

排序算法 平均时间复杂度 最坏情况下时间复杂度 额外空间复杂度 稳定性
简单选择排序 O(N^2) O(N^2) O(1) 不稳定
冒泡排序 O(N^2) O(N^2) O(1) 稳定
直接插入排序 O(N^2) O(N^2) O(1) 稳定
希尔排序 O(N^d) O(N^2) O(1) 不稳定
堆排序 O(NlogN) O(NlogN) O(1) 不稳定
快速排序 O(NlogN) O(N^2) O(logN) 不稳定
归并排序 O(NlogN) O(NlogN) O(N) 稳定
基数排序 O(P(N+B)) O(P(N+B)) O(N+B) 稳定
posted @ 2020-07-05 18:42  Kinopio  阅读(182)  评论(0编辑  收藏  举报