算法:排序

排序:所谓排序,就是使一串记录,按照其中的某个或某些关键字的大小,递增或递减的排列起来的操作。排序算法,就是如何使得记录按照要求排列的方法。排序算法在很多领域得到相当地重视,尤其是在大量数据的处理方面。一个优秀的算法可以节省大量的资源。在各个领域中考虑到数据的各种限制和规范,要得到一个符合实际的优秀算法,得经过大量的推理和分析。

概述:

排序有内部排序和外部排序,内部排序是数据记录在内存中进行排序,而外部排序是因排序的数据很大,一次不能容纳全部的排序记录,在排序过程中需要访问外存。

我们这里说说八大排序就是内部排序。

当n较大,则应采用时间复杂度为O(nlog2n)的排序方法:快速排序、堆排序或归并排序序。

快速排序:是目前基于比较的内部排序中被认为是最好的方法,当待排序的关键字是随机分布时,快速排序的平均时间最短。

下表为八大排序算法的性能比较表格:

排序方法 平均时间复杂度 稳定性
直接插入排序 (n^2) 稳定
希尔排序 O(nlogn) ~ O(n^2) 不稳定
简单选择排序 (n^2) 不稳定
堆排序 O(nlogn) 不稳定
冒泡排序 (n^2) 稳定
快速排序 O(nlogn) 不稳定
归并排序 O(nlogn) 稳定
基数排序 O(nlog(r)m) 稳定
注:其中r为所采取的基数,而m为堆数

一. 内部排序


1. 插入排序—直接插入排序(Straight Insertion Sort)

插入排序是一种简单直观的排序算法。

基本思想:

将一个记录插入到已排序好的有序表中,从而得到一个新,记录数增1的有序表。即:先将序列的第1个记录看成是一个有序的子序列,然后从第2个记录逐个进行插入,直至整个序列有序为止。

要点:设立哨兵,作为临时存储和判断数组边界之用。

直接插入排序示例:

伪代码如下:

  1. 从第一个元素开始,该元素可以认为已经被排序
  2. 取出下一个元素,在已经排序的元素序列中从后向前扫描
  3. 如果该元素(已排序)大于新元素,将该元素移到下一位置
  4. 重复步骤3,直到找到已排序的元素小于或者等于新元素的位置
  5. 将新元素插入到该位置后
  6. 重复步骤2~5

C语言实现:

#include <stdio.h>
// 分类 ------------- 内部比较排序
// 数据结构 ---------- 数组
// 最差时间复杂度 ---- 最坏情况为输入序列是降序排列的,此时时间复杂度O(n^2)
// 最优时间复杂度 ---- 最好情况为输入序列是升序排列的,此时时间复杂度O(n)
// 平均时间复杂度 ---- O(n^2)
// 所需辅助空间 ------ O(1)
// 稳定性 ------------ 稳定
void InsertionSort(int A[], int n)
{
    for (int i=1;i<n;i++)         // 类似抓扑克牌排序
    {
        int get=A[i];                 // 右手抓到一张扑克牌
        int j =i-1;                  // 拿在左手上的牌总是排序好的
        while (j>= 0 && A[j]>get)    // 将抓到的牌与手牌从右向左进行比较
        {
            A[j+1]=A[j];            // 如果该手牌比抓到的牌大,就将其右移
            j--;
        }
        A[j+1]=get; // 直到该手牌比抓到的牌小(或二者相等),将抓到的牌插入到该手牌右边(相等元素的相对次序未变,所以插入排序是稳定的)
    }
}

int main(void)
{
    int A[]={6,5,3,1,8,7,2,4 };// 从小到大插入排序
    int n=sizeof(A)/sizeof(int);
    InsertionSort(A,n);
    printf("插入排序结果:");
    for (int i=0;i<n;i++)
        printf("%d ",A[i]);
    printf("\n");
    return 0;
}

c++实现:

#include<iostream>
using namespace std;
void print(int a[], int n, int i)
{ 
	cout <<i<< ":";	
	for (int j=0;j<8;j++) 
		cout<<a[j]<< " "; 
	cout<<endl;
}  
void InsertSort(int a[], int n) 
{
	for (int i=1;i< n;i++)
	{
		if (a[i]<a[i-1]) 
		{   //若第i个元素大于i-1元素,直接插入。小于的话,移动有序表后插入			
			int j=i-1;				
			int x=a[i];		 //复制为哨兵,即存储待排序元素			
			a[i]=a[i-1];     //先后移一个元素			
			while(x < a[j])  //查找在有序表的插入位置	
			{	 	
				a[j+1]=a[j];			
				j--; //元素后移		
			}			
			a[j+1] = x;		 //插入到正确位置		
		}	print(a,n,i);	//打印每趟排序的结果	
	}
} 
int main(void)
{	
	int a[8] = {6,5,3,1,8,7,2,4};	
	InsertSort(a,8);	
	print(a,8,8);
	return 0;
}

上述代码对序列{6,5,3,1,8,7,2,4}进行插入排序的实现过程如下:

插入排序不适合对于数据量比较大的排序应用。但是,如果需要排序的数据量很小,比如量级小于千,那么插入排序还是一个不错的选择。 插入排序在工业级库中也有着广泛的应用,在STL的sort算法和stdlib的qsort算法中,都将插入排序作为快速排序的补充,用于少量元素的排序(通常为8个或以下)。


2. 插入排序—希尔排序(Shell`s Sort)

(未学 待补)


3.选择排序—简单选择排序(Simple Selection Sort)

选择排序也是一种简单直观的排序算法。

基本思想:

初始时在序列中找到最小(大)元素,放到序列的起始位置作为已排序序列;然后,再从剩余未排序元素中继续寻找最小(大)元素,放到已排序序列的末尾。以此类推,直到所有元素均排序完毕。

简单选择排序的示例:

伪代码如下:

1. 第一趟,从n 个记录中找出关键码最小的记录与第一个记录交换;

2. 第二趟,从第二个记录开始的n-1 个记录中再选出关键码最小的记录与第二个记录交换;

3.以此类推.....重复操作

4. 第i 趟,则从第i 个记录开始的n-i+1 个记录中选出关键码最小的记录与第i 个记录交换,

5. 直到整个序列按关键码有序。

C语言实现:

#include <stdio.h>
// 分类 -------------- 内部比较排序
// 数据结构 ---------- 数组
// 最差时间复杂度 ---- O(n^2)
// 最优时间复杂度 ---- O(n^2)
// 平均时间复杂度 ---- O(n^2)
// 所需辅助空间 ------ O(1)
// 稳定性 ------------ 不稳定
void Swap(int A[],int i,int j)
{
    int temp=A[i];
    A[i]=A[j];
    A[j]=temp;
}

void SelectionSort(int A[],int n)
{
    for (int i=0;i<n-1;i++)         // i为已排序序列的末尾
    {
        int min=i;
        for (int j=i+1;j<n;j++)     // 未排序序列
        {
            if (A[j]<A[min])              // 找出未排序序列中的最小值
            {
                min=j;
            }
        }
        if (min!=i)
        {
            Swap(A,min,i);    // 放到已排序序列的末尾,该操作很有可能把稳定性打乱,所以选择排序是不稳定的排序算法
        }
    }
}
int main(void)
{
    int A[] ={8,5,2,6,9,3,1,4,0,7}; // 从小到大选择排序
    int n=sizeof(A)/sizeof(int);
    SelectionSort(A,n);
    printf("选择排序结果:");
    for (int i=0;i<n;i++)
        printf("%d ",A[i]);
    printf("\n");
    return 0;
}

C++实现:

#include<iostream>
using namespace std;
void print(int a[],int n,int i) 
{ 
	cout<<"第"<<i+1<<"趟:";	
	for (int j=0;j<10;j++)
		cout<<a[j]<<"  "; 
	cout<<endl; 
}
int SelectMinKey(int a[],int n,int i) 
{ 
	int k=i;	
	for (int j =i+1;j<n;++j) 
		if(a[k]>a[j]) 
			k=j; 
	return k;
}
void selectSort(int a[],int n)   /* 选择排序  */
{
	int key,tmp;	
	for(int i=0;i<n;++i) 
	{
		key=SelectMinKey(a,n,i);           //选择最小的元素		
		if(key!=i)          //最小元素与第i位置元素互换		
		{			
			tmp=a[i];  
			a[i]=a[key];
			a[key]=tmp;
		}		
		print(a,n,i);	
	}
}
int main(void)
{
	int a[10]={8,5,2,6,9,3,1,4,0,7};
	cout<<"初始值:";	
	for (int j=0;j<10;j++) 
		cout<<a[j]<<"  ";	
	cout<<endl<<endl;	
	selectSort(a,10);
	print(a,10,10);
	return 0;
}

上述代码对序列{ 8, 5, 2, 6, 9, 3, 1, 4, 0, 7 }进行选择排序的实现过程如右图:  

选择排序是不稳定的排序算法,不稳定发生在最小元素与A[i]交换的时刻。

注意选择排序与冒泡排序的区别:冒泡排序通过依次交换相邻两个顺序不合法的元素位置,从而将当前最小(大)元素放到合适的位置;而选择排序每遍历一次都记住了当前最小(大)元素的位置,最后仅需一次交换操作即可将其放到合适的位置。


4.选择排序—堆排序(Heap Sort)

堆排序是指利用堆这种数据结构所设计的一种选择排序算法。堆是一种近似完全二叉树的结构,堆排序是一种树形选择排序,是对直接选择排序的有效改进。

基本思想:(小根堆为例)

实现堆排序需解决两个问题:

1. 如何将n 个待排序的数建成堆;

2. 输出堆顶元素后,怎样调整剩余n-1 个元素,使其成为一个新堆。

建堆方法:对初始序列建堆的过程,就是一个反复进行筛选的过程。

1.  n 个结点的完全二叉树,则最后一个结点是第个结点的子树。

2. 筛选从第个结点为根的子树开始,该子树成为堆。

3. 之后向前依次对各结点为根的子树进行筛选,使之成为堆,直到根结点。

调整小根堆的方法:(大根堆反之)

1. 设有m 个元素的堆,输出堆顶元素后,剩下m-1 个元素。将堆底元素送入堆顶((最后一个元素与堆顶进行交换),堆被破坏,其原因仅是根结点不满足堆的性质。

2. 将根结点与左、右子树中较小元素的进行交换。

3. 若与左子树交换:如果左子树堆被破坏,即左子树的根结点不满足堆的性质,则重复第2步。

4. 若与右子树交换,如果右子树堆被破坏,即右子树的根结点不满足堆的性质。则重复第2步。

5. 继续对不满足堆性质的子树进行上述交换操作,直到叶子结点,堆被建成。

堆排序的示例:

伪代码如下:

1. 构建小根堆。

2. 提取堆根并调整移走堆根后的元素为新堆。

3. 重复第2步,直到堆空

4. 每次提取的堆根一次排列即为递增序列。

C语言实现:

#include <stdio.h>
// 分类 -------------- 内部比较排序
// 数据结构 ---------- 数组
// 最差时间复杂度 ---- O(nlogn)
// 最优时间复杂度 ---- O(nlogn)
// 平均时间复杂度 ---- O(nlogn)
// 所需辅助空间 ------ O(1)
// 稳定性 ------------ 不稳定
void Swap(int A[],int i,int j)
{
    int temp=A[i];
    A[i]=A[j];
    A[j]=temp;
}
void Heapify(int A[], int i, int size)  // 从A[i]向下进行堆调整
{
    int left_child=2*i+1;         // 左孩子索引
    int right_child=2*i 2;        // 右孩子索引
    int max=i;                        // 选出当前结点与其左右孩子三者之中的最大值
    if (left_child<size && A[left_child]>A[max])
        max=left_child;
    if (right_child<size && A[right_child]>A[max])
        max=right_child;
    if (max!=i)
    {
        Swap(A,i,max);                // 把当前结点和它的最大(直接)子节点进行交换
        Heapify(A,max,size);          // 递归调用,继续从当前结点向下进行堆调整
    }
}

int BuildHeap(int A[],int n)           // 建堆,时间复杂度O(n)
{
    int heap_size=n;
    for (int i=heap_size/2-1;i>=0;i--) // 从每一个非叶结点开始向下进行堆调整
        Heapify(A,i,heap_size);
    return heap_size;
}

void HeapSort(int A[], int n)
{
    int heap_size=BuildHeap(A,n);    // 建立一个最大堆
    while (heap_size>1)           // 堆(无序区)元素个数大于1,未完成排序
    {
        // 将堆顶元素与堆的最后一个元素互换,并从堆中去掉最后一个元素
        // 此处交换操作很有可能把后面元素的稳定性打乱,所以堆排序是不稳定的排序算法
        Swap(A,0,--heap_size);
        Heapify(A,0,heap_size);     // 从新的堆顶元素开始向下进行堆调整,时间复杂度O(logn)
    }
}
int main(void)
{
    int A[]={5,2,9,4,7,6,1,3,8};// 从小到大堆排序
    int n=sizeof(A)/sizeof(int);
    HeapSort(A,n);
    printf("堆排序结果:");
    for (int i=0;i<n;i++)
        printf("%d ",A[i]);
    printf("\n");
    return 0;
}

C++实现:

#include<iostream>
using namespace std;
void print(int a[], int n)
{
	for (int j=0;j<n;j++)
		cout<<a[j]<<"  ";
	cout<<endl;
}
/*  已知H[s…m]除了H[s] 外均满足堆的定义 调整H[s],使其成为大顶堆 即将对第s个结点为根的子树筛选
	H是待调整的堆数组 s是待调整的数组元素的位置  length是数组的长度  */
void HeapAdjust(int H[],int s,int length)
{
	int tmp=H[s];
	int child=2*s+1;   //左孩子结点的位置 (i+1 为当前调整结点的右孩子结点的位置)
	while(child<length)
	{
		if(child+1<length && H[child]<H[child+1])   // 如果右孩子大于左孩子(找到比当前待调整结点大的孩子结点)
			++child;
		if(H[s]<H[child])      // 如果较大的子结点大于父结点
		{
			H[s]=H[child];     // 那么把较大的子结点往上移动,替换它的父结点
			s=child;		    // 重新设置s ,即待调整的下一个结点的位置
			child=2*s+1;
		}
		else 		          // 如果当前待调整结点大于它的左右孩子,则不需要调整,直接退出
			break;
		H[s]=tmp;			// 当前待调整的结点放到比其大的孩子结点位置上
	}
	print(H,length);
}
// 初始堆进行调整 将H[0..length-1]建成堆  调整完之后第一个元素是序列的最小的元素
void BuildingHeap(int H[],int length)   //最后一个有孩子的节点的位置 i=(length-1)/2
{
	for(int i=(length-1)/2;i>=0;--i)
		HeapAdjust(H,i,length);
}
void HeapSort(int H[],int length)   //  堆排序算法
{
	//初始堆
	BuildingHeap(H, length);
	//从最后一个元素开始对序列进行调整
	for (int i=length-1;i>0;--i)  //交换堆顶元素H[0]和堆中最后一个元素
	{
		int temp=H[i];
		H[i]=H[0];
		H[0]=temp;
		HeapAdjust(H,0,i);  //每次交换堆顶元素和堆中最后一个元素之后,都要对堆进行调整
	}
}
int main(void)
{
	int H[10] ={5,2,9,4,7,6,1,3,8};
	cout<<"初始值:";
	print(H,10);
	HeapSort(H,10);
	cout<<"结果:";
	print(H,10);
	return 0;
}

堆排序算法的演示如下:

堆排序是不稳定的排序算法,不稳定发生在堆顶元素与A[i]交换的时刻。

不适用于待排序记录个数n较少的情况,对于n较大的文件比较有用。


5.交换排序—冒泡排序(Bubble Sort)

冒泡排序是一种极其简单的排序算法,它重复地走访过要排序的元素,依次比较相邻两个元素,如果他们的顺序错误就把他们调换过来,直到没有元素再需要交换,排序完成。这个算法的名字由来是因为越小(或越大)的元素会经由交换慢慢“浮”到数列的顶端。

基本思想:

在要排序的一组数中,对当前还未排好序的范围内的全部数,自上而下对相邻的两个数依次进行比较和调整,让较大的数往下沉,较小的往上冒。即:每当两相邻的数比较后发现它们的排序与排序要求相反时,就将它们互换。

冒泡排序的示例:

伪代码如下:

  1. 比较相邻的元素,如果前一个比后一个大,就把它们两个调换位置。
  2. 对每一对相邻元素作同样的工作,从开始第一对到结尾的最后一对。这步做完后,最后的元素会是最大的数。
  3. 针对所有的元素重复以上的步骤,除了最后一个。
  4. 持续每次对越来越少的元素重复上面的步骤,直到没有任何一对数字需要比较。

C语言实现:

#include <stdio.h>
// 分类 -------------- 内部比较排序
// 数据结构 ---------- 数组
// 最差时间复杂度 ---- O(n^2)
// 最优时间复杂度 ---- 如果能在内部循环第一次运行时,使用一个旗标来表示有无需要交换的可能,可以把最优时间复杂度降低到O(n)
// 平均时间复杂度 ---- O(n^2)
// 所需辅助空间 ------ O(1)
// 稳定性 ------------ 稳定
void Swap(int A[],int i,int j)
{
    int temp=A[i];
    A[i]=A[j];
    A[j]=temp;
}
void BubbleSort(int A[],int n)
{
    for (int j=0;j<n-1;j++)         // 每次最大元素就像气泡一样"浮"到数组的最后
    {
        for (int i=0;i<n-1-j;i++) // 依次比较相邻的两个元素,使较大的那个向后移
        {
            if(A[i]>A[i+1])       // 如果条件改成A[i] >= A[i + 1],则变为不稳定的排序算法
                Swap(A,i,i+1);
        }
    }
}
int main(void)
{
    int A[]={6,5,3,1,8,7,2,4};    // 从小到大冒泡排序
    int n=sizeof(A) / sizeof(int);
    BubbleSort(A,n);
    printf("冒泡排序结果:");
    for (int i=0;i<n;i++)
        printf("%d ",A[i]);
    printf("\n");
    return 0;
}

C++实现:

#include<iostream>
using namespace std;
void print(int a[],int i)
{
	cout<<"第"<<i+1<<"趟:";
	for (int j=0;j<8;j++)
		cout<<a[j]<<"  ";
	cout<<endl;
}
void bubbleSort(int a[],int n) 
{
	for (int i=0;i<n-1;i++)
	{
		for (int j=0;j<n-i-1;j++) 
		{
			if (a[j]>a[j+1])
			{
				int tmp=a[j];
				a[j]=a[j+1]; 
				a[j+1]=tmp;
			}
		}
		print(a, i);
	}
}
int main(void)
{
	int a[8]={6,5,3,1,8,7,2,4};
	cout<<"初始值:";
	for(int j=0;j<8;j++)
		cout<<a[j]<< "  ";
	cout<<endl;
	bubbleSort(a, 8);
	print(a,8);
	return 0;
}

上述代码对序列{ 6, 5, 3, 1, 8, 7, 2, 4 }进行冒泡排序的实现过程如下

    

使用冒泡排序为一列数字进行排序的过程如右图所示:  

尽管冒泡排序是最容易了解和实现的排序算法之一,但它对于少数元素之外的数列排序是很没有效率的。


6.交换排序—快速排序(Quick Sort)

快速排序是由东尼·霍尔所发展的一种排序算法。在平均状况下,排序n个元素要O(nlogn)次比较。在最坏状况下则需要O(n^2)次比较,但这种状况并不常见。事实上,快速排序通常明显比其他O(nlogn)算法更快,因为它的内部循环可以在大部分的架构上很有效率地被实现出来。

基本思想:

1. 选择一个基准元素,通常选择第一个元素或者最后一个元素。

2. 通过一趟排序讲待排序的记录分割成独立的两部分,其中一部分记录的元素值均比基准元素值小。另一部分记录的元素值比基准值大。

3. 此时基准元素在其排好序后的正确位置。

4. 然后分别对这两部分记录用同样的方法继续进行排序,直到整个序列有序。

快速排序示例:

一趟排序的过程:

排序的全过程

伪代码如下:

快速排序使用分治策略(Divide and Conquer)来把一个序列分为两个子序列。步骤为:

1.  从序列中挑出一个元素,作为"基准"(pivot).

2. 把所有比基准值小的元素放在基准前面,所有比基准值大的元素放在基准的后面(相同的数可以到任一边),这个称为分区(partition)操作。

3. 对每个分区递归地进行步骤1~2,递归的结束条件是序列的大小是0或1,这时整体已经被排好序了。

C语言实现:

#include <stdio.h>
// 分类 ------------ 内部比较排序
// 数据结构 --------- 数组
// 最差时间复杂度 ---- 每次选取的基准都是最大(或最小)的元素,导致每次只划分出了一个分区,需要进行n-1次划分才能结束递归,时间复杂度为O(n^2)
// 最优时间复杂度 ---- 每次选取的基准都是中位数,这样每次都均匀的划分出两个分区,只需要logn次划分就能结束递归,时间复杂度为O(nlogn)
// 平均时间复杂度 ---- O(nlogn)
// 所需辅助空间 ------ 主要是递归造成的栈空间的使用(用来保存left和right等局部变量),取决于递归树的深度,一般为O(logn),最差为O(n)       
// 稳定性 ---------- 不稳定
void Swap(int A[], int i, int j)
{
    int temp=A[i];
    A[i]=A[j];
    A[j]=temp;
}

int Partition(int A[],int left,int right)  // 划分函数
{
    int pivot=A[right];               // 这里每次都选择最后一个元素作为基准
    int tail=left-1;                // tail为小于基准的子数组最后一个元素的索引
    for(int i=left;i<right;i++)  // 遍历基准以外的其他元素
    {
        if(A[i]<=pivot)              // 把小于等于基准的元素放到前一个子数组末尾
            Swap(A,++tail,i);
    }
    Swap(A,tail+1,right);  
 // 最后把基准放到前一个子数组的后边,剩下的子数组既是大于基准的子数组
 // 该操作很有可能把后面元素的稳定性打乱,所以快速排序是不稳定的排序算法
    return tail+1;                    // 返回基准的索引
}
void QuickSort(int A[],int left,int right)
{
    if(left>=right)
        return;
    int pivot_index=Partition(A,left,right); // 基准的索引
    QuickSort(A,left,pivot_index-1);
    QuickSort(A,pivot_index+1,right);
}
int main(void)
{
    int A[]={5,2,9,4,7,6,1,3,8}; // 从小到大快速排序
    int n=sizeof(A)/sizeof(int);
    QuickSort(A,0,n-1);
    printf("快速排序结果:");
    for (int i=0;i<n;i++)
        printf("%d ",A[i]);
    printf("\n");
    return 0;
}

c++实现:

#include<iostream>
using namespace std;
void print(int a[], int n) 
{
	for (int j=0;j<n;j++) 
		cout<<a[j]<<"  ";
	cout<<endl;
}
void swap(int *a, int *b)
{
	int tmp=*a;
	*a=*b;
	*b=tmp;
}
int partition(int a[],int low,int high)
{
	int privotKey=a[low];								//基准元素
	while(low<high) 
	{								    //从表的两端交替地向中间扫描
		while(low<high  && a[high]>=privotKey) 
			--high;  //从high 所指位置向前搜索,至多到low+1 位置。将比基准元素小的交换到低端
		swap(&a[low], &a[high]);
		while (low<high  && a[low]<=privotKey) 
			++low;
		swap(&a[low],&a[high]);
	}
	print(a,10);
	return low;
}
void quickSort(int a[],int low,int high) 
{
	if(low<high) 
	{
		int privotLoc=partition(a,low,high);  //将表一分为二
		quickSort(a,low,privotLoc-1);			//递归对低子表递归排序
		quickSort(a,privotLoc+1,high);		//递归对高子表递归排序
	}
}
int main(void)
{
	int a[10]={5,2,9,4,7,6,1,3,8};
	cout<<"初始值:";
	print(a, 10);
	quickSort(a,0,9);
	cout<<"结果:";
	print(a,10);
	return 0;
}

使用快速排序法对一列数字进行排序的过程:  

快速排序是通常被认为在同数量级(O(nlog2n))的排序方法中平均性能最好的。但若初始序列按关键码有序或基本有序时,快排序反而蜕化为冒泡排序。为改进之,通常以“三者取中法”来选取基准记录,即将排序区间的两个端点与中点三个记录关键码居中的调整为支点记录。快速排序是不稳定的排序算法,不稳定发生在基准元素与A[tail+1]交换的时刻。


7.归并排序(Merge Sort)

归并排序是创建在归并操作上的一种有效的排序算法。

归并排序的实现分为递归实现非递归(迭代)实现。递归实现的归并排序是算法设计中分治策略的典型应用,我们将一个大问题分割成小问题分别解决,然后用所有小问题的答案来解决整个大问题。非递归(迭代)实现的归并排序首先进行是两两归并,然后四四归并,然后是八八归并,一直下去直到归并了整个数组。

基本思想:

归并(Merge)排序法是将两个(或两个以上)有序表合并成一个新的有序表,即把待排序序列分为若干个子序列,每个子序列是有序的。然后再把有序子序列合并为整体有序序列。

归并排序示例:

伪代码如下:

  1. 申请空间,使其大小为两个已经排序序列之和,该空间用来存放合并后的序列
  2. 设定两个指针,最初位置分别为两个已经排序序列的起始位置
  3. 比较两个指针所指向的元素,选择相对小的元素放入到合并空间,并移动指针到下一位置
  4. 重复步骤3直到某一指针到达序列尾
  5. 将另一序列剩下的所有元素直接复制到合并序列尾

合并方法:

设r[i…n]由两个有序子表r[i…m]和r[m+1…n]组成,两个子表长度分别为n-i +1、n-m。

1. j=m+1;k=i;i=i; //置两个子表的起始下标及辅助数组的起始下标

2. 若i>m 或j>n,转⑷ //其中一个子表已合并完,比较选取结束

3. 选取r[i]和r[j]较小的存入辅助数组rf

如果r[i]<r[j],rf[k]=r[i]; i++; k++;转到第2步

否则,rf[k]=r[j]; j++; k++;转到第2步

4. 将尚未处理完的子表中元素存入rf

如果i<=m,将r[i…m]存入rf[k…n] //前一子表非空

如果j<=n ,  将r[j…n] 存入rf[k…n] //后一子表非空

5. 合并结束。

C语言实现:

#include <stdio.h>
#include <limits.h>
// 分类 -------------- 内部比较排序
// 数据结构 ---------- 数组
// 最差时间复杂度 ---- O(nlogn)
// 最优时间复杂度 ---- O(nlogn)
// 平均时间复杂度 ---- O(nlogn)
// 所需辅助空间 ------ O(n)
// 稳定性 ------------ 稳定
void Merge(int A[],int left,int mid,int right)
// 合并两个已排好序的数组A[left...mid]和A[mid+1...right]
{
    int len=right-left+1;
    int *temp=newint[len];       // 辅助空间O(n)
    int index=0;
    int i=left;                   // 前一数组的起始元素
    int j=mid+1;                // 后一数组的起始元素
    while(i<=mid && j<=right)
        temp[index++]=A[i]<=A[j]?A[i++]:A[j++];  // 带等号保证归并排序的稳定性
    while (i<=mid)
        temp[index++]=A[i++];
    while (j <=right)
        temp[index++]=A[j++];
    for (int k=0;k<len;k++)
        A[left++]=temp[k];
}

void MergeSortRecursion(int A[],int left,int right)    // 递归实现的归并排序(自顶向下)
{
    if(left==right)    // 当待排序的序列长度为1时,递归开始回溯,进行merge操作
        return;
    int mid=(left+right)/2;
    MergeSortRecursion(A,left,mid);
    MergeSortRecursion(A,mid+1,right);
    Merge(A,left,mid,right);
}

void MergeSortIteration(int A[],int len)    // 非递归(迭代)实现的归并排序(自底向上)
{
    int left,mid,right;// 子数组索引,前一个为A[left...mid],后一个子数组为A[mid+1...right]
    for(int i=1;i<len;i*=2)        // 子数组的大小i初始为1,每轮翻倍
    {
        left=0;
        while(left+i<len)              // 后一个子数组存在(需要归并)
        {
            mid=left+i-1;
            righ =mid+i<len?mid+i:len-1;// 后一个子数组大小可能不够
            Merge(A,left,mid,right);
            left=right+1;               // 前一个子数组索引向后移动
        }
    }
}
int main(void)
{
    int A1[]={6,5,3,1,8,7,2,4};      // 从小到大归并排序
    int A2[]={6,5,3,1,8,7,2,4};
    int n1=sizeof(A1)/sizeof(int);
    int n2=sizeof(A2)/sizeof(int);
    MergeSortRecursion(A1,0,n1-1);          // 递归实现
    MergeSortIteration(A2,n2);                 // 非递归实现
    printf("递归实现的归并排序结果:");
    for(int i=0;i<n1;i++)
        printf("%d ",A1[i]);
    printf("\n");
    printf("非递归实现的归并排序结果:");
    for(int i=0;i<n2;i++)
        printf("%d ",A2[i]);
    printf("\n");
    return 0;
}

上述代码对序列{ 6, 5, 3, 1, 8, 7, 2, 4 }进行归并排序的实例如下 

归并排序除了可以对数组进行排序,还可以高效的求出数组小和(即单调和)以及数组中的逆序对。


8.桶式排序/基数排序(Radix Sort)

(未学 待补)


二. 外部排序

猜你喜欢

转载自blog.csdn.net/qq_41918107/article/details/82931240