排序算法

分类:

一、选择排序 (不稳定)

1. 基本思想:

  每一趟从待排序的数据元素中选出最小(或最大)的一个元素,顺序放在已排好序的数列的最后,直到全部待排序的数据元素排完。

2. 排序过程:

【示例】:

   初始关键字 [49 38 65 97 76 13 27 49]

第一趟排序后 13 [38 65 97 76 49 27 49]

第二趟排序后 13 27 [65 97 76 49 38 49]

第三趟排序后 13 27 38 [97 76 49 65 49]

第四趟排序后 13 27 38 49 [49 97 65 76]

第五趟排序后 13 27 38 49 49 [97 97 76]

第六趟排序后 13 27 38 49 49 76 [76 97]

第七趟排序后 13 27 38 49 49 76 76 [ 97]

最后排序结果 13 27 38 49 49 76 76 97

3.

void selectionSort(int* arr,long len)

{

       for ( int i = 0; i < len - 1; ++i )

        {

            int index = i;

            for ( int j = i + 1; j < len; ++j)

            {

                if ( arr[index] > arr[j];

                index = j;

            }

           if (index ! = i)

               swap( arr[index], arr[i]);

        }  

}

选择排序法的第一层循环从起始元素开始选到倒数第二个元素,主要是在每次进入的第二层循环之 前,将外层循环的下标赋值给临时变量,接下来的第二层循环中,如果发现有比这个最小位置处的元素更小的元素,则将那个更小的元素的下标赋给临时变量,最 后,在二层循环退出后,如果临时变量改变,则说明,有比当前外层循环位置更小的元素,需要将这两个元素交换.

二.直接插入排序(稳定)

插入排序(Insertion Sort)的基本思想是:每次将一个待排序的记录,按其关键字大小插入到前面已经排好序的子文件中的适当位置,直到全部记录插入完成为止。

直接插入排序

  直接插入排序(Straight Insertion Sort):将一个记录插入到排好序的有序表中,从而得到一个新的、记录数增1的有序表。

直接插入排序算法

staticvoid insertion_sort(int[] unsorted)       
(
	for (int i =1; i < unsorted.Length; i++)           
	{       
		if (unsorted[i -1] > unsorted[i])               
		{                   
			int temp = unsorted[i];                   
			int j = i;                   
			while (j >0 && unsorted[j -1] > temp)                   
			{                
				unsorted[j]= unsorted[j -1];                       
				j--;                   
			}                   
			unsorted[j]= temp;               
		}           
	}       
}
 
 

  哨兵(监视哨)有两个作用:一是作为临变量存放R[i](当前要进行比较的关键字)的副本;二是在查找循环中用来监视下标变量j是否越界。

 

  当文件的初始状态不同时,直接插入排序所耗费的时间是有很大差异的。最好情况是文件初态 为正序,此时算法的时间复杂度为O(n),最坏情况是文件初态为反序,相应的时间复杂度为O(n2),算法的平均时间复杂度是O(n2)。算法的辅助空间 复杂度是O(1),是一个就地排序。

直接插入排序是稳定的排序方法。

三. 冒泡排序(稳定)

[算法思想]:将被排序的记录数组R[1..n]垂直排列,每个记录R[i]看作是重量为 R[i].key的气泡。根据轻气泡不能在重气泡之下的原则,从下往上扫描数组R:凡扫描到违反本原则的轻气泡,就使其向上"飘浮"。如此反复进行,直到 最后任何两个气泡都是轻者在上,重者在下为止。

  1. /******************************************************** 
  2. *函数名称:BubbleSort 
  3. *参数说明:pDataArray 无序数组; 
  4. *          iDataNum为无序数据个数 
  5. *说明:    冒泡排序 
  6. *********************************************************/  
  7. void BubbleSort(int* pDataArray, int iDataNum)  
  8. {  
  9.     BOOL flag = FALSE;    //记录是否存在交换  
  10.     for (int i = 0; i < iDataNum - 1; i++)    //走iDataNum-1趟  
  11.     {  
  12.         flag = FALSE;  
  13.         for (int j = 0; j < iDataNum - i - 1; j++)      
  14.             if (pDataArray[j] > pDataArray[j + 1])  
  15.             {  
  16.                 flag = TRUE;  
  17.                 DataSwap(&pDataArray[j], &pDataArray[j + 1]);  
  18.             }  
  19.           
  20.         if (!flag)    //上一趟比较中不存在交换,则退出排序  
  21.             break;  
  22.     }  
  23. }  

平均时间复杂度:O(n2);空间复杂度:O(1)  (用于交换)

 

四. 希尔排序

基本思想:

      先取一个小于n的整数d1作为第一个增量,把文件的全部记录分成d1个组。所有距离为dl的倍数的记录放在同一个组中。先在各组内进行直接插人排序;然 后,取第二个增量d2<d1重复上述的分组和排序,直至所取的增量dt=1(dt<dt-l<…<d2<d1),即所有记 录放在同一组中进行直接插入排序为止。

     该方法实质上是一种分组插入方法。

给定实例的shell排序的排序过程

     假设待排序文件有10个记录,其关键字分别是:

        49,38,65,97,76,13,27,49,55,04。

     增量序列的取值依次为:

        5,3,1

Shell排序的算法实现

1. 不设监视哨的算法描述

void ShellPass(SeqList R,int d)

   {//希尔排序中的一趟排序,d为当前增量

     for(i=d+1;i<=n;i++) //将R[d+1..n]分别插入各组当前的有序区

       if(R[i].key<R[i-d].key){

         R[0]=R[i];j=i-d; //R[0]只是暂存单元,不是哨兵

         do {//查找R[i]的插入位置

            R[j+d];=R[j]; //后移记录

            j=j-d; //查找前一记录

         }while(j>0&&R[0].key<R[j].key);

         R[j+d]=R[0]; //插入R[i]到正确的位置上

       } //endif

   } //ShellPass

void ShellSort(SeqList R)

   {

    int increment=n; //增量初值,不妨设n>0

    do {

          increment=increment/3+1; //求下一增量

          ShellPass(R,increment); //一趟增量为increment的Shell插入排序

       }while(increment>1)

    } //ShellSort

注意:

     当增量d=1时,ShellPass和InsertSort基本一致,只是由于没有哨兵而在内循环中增加了一个循环判定条件"j>0",以防下标越界。

2.设监视哨的shell排序算法

算法分析

1.增量序列的选择

     Shell排序的执行时间依赖于增量序列。

     好的增量序列的共同特征:

  ① 最后一个增量必须为1;

  ② 应该尽量避免序列中的值(尤其是相邻的值)互为倍数的情况。

     有人通过大量的实验,给出了目前较好的结果:当n较大时,比较和移动的次数约在nl.25到1.6n1.25之间。

2.Shell排序的时间性能优于直接插入排序

     希尔排序的时间性能优于直接插入排序的原因:

  ①当文件初态基本有序时直接插入排序所需的比较和移动次数均较少。

  ②当n值较小时,n和n2的差别也较小,即直接插入排序的最好时间复杂度O(n)和最坏时间复杂度0(n2)差别不大。

  ③在希尔排序开始时增量较大,分组较多,每组的记录数目少,故各组内直接插入较快,后来增量di逐渐缩小,分组数逐渐减少,而各组的记录数目逐渐增多,但由于已经按di-1作为距离排过序,使文件较接近于有序状态,所以新的一趟排序过程也较快。

     因此,希尔排序在效率上较直接插人排序有较大的改进。

3.稳定性

     希尔排序是不稳定的。参见上述实例,该例中两个相同关键字49在排序前后的相对次序发生了变化。

五. 堆排序

1、 堆排序定义

     n个关键字序列Kl,K2,…,Kn称为堆,当且仅当该序列满足如下性质(简称为堆性质):

     (1) ki≤K2i且ki≤K2i+1 或(2)Ki≥K2i且ki≥K2i+1(1≤i≤ )

     若将此序列所存储的向量R[1..n]看做是一棵完全二叉树的存储结构,则堆实质上是满足如下性质的完全二叉树:树中任一非叶结点的关键字均不大于(或不小于)其左右孩子(若存在)结点的关键字。

【例】关键字序列(10,15,56,25,30,70)和(70,56,30,25,15,10)分别满足堆性质(1)和(2),故它们均是堆,其对应的完全二叉树分别如小根堆示例和大根堆示例所示。

2、大根堆和小根堆

     根结点(亦称为堆顶)的关键字是堆里所有结点关键字中最小者的堆称为小根堆。

     根结点(亦称为堆顶)的关键字是堆里所有结点关键字中最大者,称为大根堆。

注意:

     ①堆中任一子树亦是堆。

      ②以上讨论的堆实际上是二叉堆(Binary Heap),类似地可定义k叉堆。

3、堆排序特点

     堆排序(HeapSort)是一树形选择排序。

     堆排序的特点是:在排序过程中,将R[l..n]看成是一棵完全二叉树的顺序存储结构,利用完全二叉树中双亲结点和孩子结点之间的内在关系【参见二叉树的顺序存储结构】,在当前无序区中选择关键字最大(或最小)的记录。

4、堆排序与直接插入排序的区别

      直接选择排序中,为了从R[1..n]中选出关键字最小的记录,必须进行n-1次比较,然后在R[2..n]中选出关键字最小的记录,又需要做n-2次 比较。事实上,后面的n-2次比较中,有许多比较可能在前面的n-1次比较中已经做过,但由于前一趟排序时未保留这些比较结果,所以后一趟排序时又重复执 行了这些比较操作。

     堆排序可通过树形结构保存部分比较结果,可减少比较次数。

 

5、堆排序

    堆排序利用了大根堆(或小根堆)堆顶记录的关键字最大(或最小)这一特征,使得在当前无序区中选取最大(或最小)关键字的记录变得简单。

(1)用大根堆排序的基本思想

① 先将初始文件R[1..n]建成一个大根堆,此堆为初始的无序区

② 再将关键字最大的记录R[1](即堆顶)和无序区的最后一个记录R[n]交换,由此得到新的无序区R[1..n-1]和有序区R[n],且满足R[1..n-1].keys≤R[n].key

③  由于交换后新的根R[1]可能违反堆性质,故应将当前无序区R[1..n-1]调整为堆。然后再次将R[1..n-1]中关键字最大的记录R[1]和该区 间的最后一个记录R[n-1]交换,由此得到新的无序区R[1..n-2]和有序区R[n-1..n],且仍满足关系R[1..n- 2].keys≤R[n-1..n].keys,同样要将R[1..n-2]调整为堆。

    ……

直到无序区只有一个元素为止。

(2)大根堆排序算法的基本操作:

① 初始化操作:将R[1..n]构造为初始堆;

② 每一趟排序的基本操作:将当前无序区的堆顶记录R[1]和该区间的最后一个记录交换,然后将新的无序区调整为堆(亦称重建堆)。

注意:

①只需做n-1趟排序,选出较大的n-1个关键字即可以使得文件递增有序。

②用小根堆排序与利用大根堆类似,只不过其排序结果是递减有序的。堆排序和直接选择排序相反:在任何时刻,堆排序中无序区总是在有序区之前,且有序区是在原向量的尾部由后往前逐步扩大至整个向量为止。

(3)堆排序的算法:

void HeapSort(SeqIAst R)

   { //对R[1..n]进行堆排序,不妨用R[0]做暂存单元

    int i;

    BuildHeap(R); //将R[1-n]建成初始堆

    for(i=n;i>1;i--){ //对当前无序区R[1..i]进行堆排序,共做n-1趟。

      R[0]=R[1];R[1]=R[i];R[i]=R[0]; //将堆顶和堆中最后一个记录交换

     Heapify(R,1,i-1); //将R[1..i-1]重新调整为堆,仅有R[1]可能违反堆性质

     } //endfor

   } //HeapSort

(4) BuildHeap和Heapify函数的实现

 因为构造初始堆必须使用到调整堆的操作,先讨论Heapify的实现。

① Heapify函数思想方法

 每趟排序开始前R[l..i]是以R[1]为根的堆,在R[1]与R[i]交换后,新的无 序区R[1..i-1]中只有R[1]的值发生了变化,故除R[1]可能违反堆性质外,其余任何结点为根的子树均是堆。因此,当被调整区间是 R[low..high]时,只须调整以R[low]为根的树即可。

"筛选法"调整堆

   R[low]的左、右子树(若存在)均已是堆,这两棵子树的根R[2low]和R[2low+1]分别是各自子树中关键字最大的结点。若 R[low].key不小于这两个孩子结点的关键字,则R[low]未违反堆性质,以R[low]为根的树已是堆,无须调整;否则必须将R[low]和它 的两个孩子结点中关键字较大者进行交换,即R[low]与R[large] (R[large].key=max(R[2low].key,R[2low+1].key))交换。交换后又可能使结点R[large]违反堆性质,同 样由于该结点的两棵子树(若存在)仍然是堆,故可重复上述的调整过程,对以R[large]为根的树进行调整。此过程直至当前被调整的结点已满足堆性质, 或者该结点已是叶子为止。上述过程就象过筛子一样,把较小的关键字逐层筛下去,而将较大的关键字逐层选上来。因此,有人将此方法称为"筛选法"。

②BuildHeap的实现

  要将初始文件R[l..n]调整为一个大根堆,就必须将它所对应的完全二叉树中以每一结点为根的子树都调整为堆。

  显然只有一个结点的树是堆,而在完全二叉树中,所有序号 的结点都是叶子,因此以这些结点为根的子树均已是堆。这样,我们只需依次将以序号为 -1,…,1的结点作为根的子树都调整为堆即可。

      具体算法【参见教材】。

5、大根堆排序实例

     对于关键字序列(42,13,24,91,23,16,05,88),在建堆过程中完全二叉树及其存储结构的变化情况参见。

6、 算法分析

     堆排序的时间,主要由建立初始堆和反复重建堆这两部分的时间开销构成,它们均是通过调用Heapify实现的。

     堆排序的最坏时间复杂度为O(nlgn)。堆排序的平均性能较接近于最坏性能。

     由于建初始堆所需的比较次数较多,所以堆排序不适宜于记录数较少的文件。

     堆排序是就地排序,辅助空间为O(1),

     它是不稳定的排序方法。

六. 快速排序

快速排序的基本思路是:首先我们选择一个中间值middle(程序中我们可使用数组中间值),把比中间值小的放在其左边,比中间值大的放在其右边。由于这个排序算法较复杂,我们先给出其进行一次排序的程序框架(从各类数据结构教材中可得):

void quick_sort(ints[], int l, int r)

{

    if (l < r)

    {

             //Swap(s[l], s[(l + r) / 2]); //将中间的这个数和第一个数交换参见注1

        int i = l,j = r,x = s[l];

        while (i < j)

        {

           while(i <j && s[j] >=x) //从右向左找第一个小于x的数

                           j--; 

           if(i < j)

                           s[i++] =s[j];

                    

           while(i <j && s[i] <x) //从左向右找第一个大于等于x的数

                           i++; 

           if(i < j)

                           s[j--] =s[i];

        }

        s[i] =x;

        quick_sort(s, l, i - 1);// 递归调用

        quick_sort(s, i + 1, r);

    }

对于n个成员,快速排序法的比较次数大约为n*logn 次,交换次数大约为(n*logn)/6次。如果n为100,冒泡法需要进行4950 次比较,而快速排序法仅需要200  次,快速排序法的效率的确很高。快速排序法的性能与中间值的选定关系密切,如果每一次选择的中间值都是最大值(或最小值),该算法的速度就会大大下降。快 速排序算法最坏情况下的时间复杂度为O(n2),而平均时间复杂度为O(n*logn)。

七. 合并排序

说明

之前所介绍的排序法都是在同一个阵列中的排序,考虑今日有两笔或两笔以上的资料,它可能是不同阵列中的资料,或是不同档案中的资料,如何为它们进行排序?

解法

可以使用合并排序法,合并排序法基本是将两笔已排序的资料合并并进行排序,如果所读入的资料尚未排序,可以先利用其它的排序方式来处理这两笔资料,然后再将排序好的这两笔资料合并。

有人问道,如果两笔资料本身就无排序顺序,何不将所有的资料读入,再一次进行排序?排序的精 神是尽量利用资料已排序的部份,来加快排序的效率,小笔资料的排序较为快速,如果小笔资料排序完成之后,再合并处理时,因为两笔资料都有排序了,所有在合 并排序时会比单纯读入所有的资料再一次排序来的有效率。

那么可不可以直接使用合并排序法本身来处理整个排序的动作?而不动用到其它的排序方式?答案是肯定的,只要将所有的数字不断的分为两个等分,直到最后剩一个数字为止,然后再反过来不断的合并,就如下图所示:

 

 

不过基本上分割又会花去额外的时间,不如使用其它较好的排序法来排序小笔资料,再使用合并排序来的有效率。

下面这个程式范例,我们使用快速排序法来处理小笔资料排序,然后再使用合并排序法处理合并的动作。

例子

 

  • C

 

#include <stdio.h>

#include <stdlib.h>

#include <time.h>

#define MAX1 10

#define MAX2 10

#define SWAP(x,y) {int t; t = x; x = y; y = t;}

int partition(int[], int, int);

void quicksort(int[], int, int);

void mergesort(int[], int, int[], int, int[]);

int main(void) {

int number1[MAX1] = {0};

int number2[MAX1] = {0};

int number3[MAX1+MAX2] = {0};

int i, num;

srand(time(NULL));

printf("排序前:");

printf("\nnumber1[]:");

for(i = 0; i < MAX1; i++) {

number1[i] = rand() % 100;

printf("%d ", number1[i]);

}

printf("\nnumber2[]:");

for(i = 0; i < MAX2; i++) {

number2[i] = rand() % 100;

printf("%d ", number2[i]);

}

// 先排序两笔资料

quicksort(number1, 0, MAX1-1);

quicksort(number2, 0, MAX2-1);

printf("\n排序后:");

printf("\nnumber1[]:");

for(i = 0; i < MAX1; i++)

printf("%d ", number1[i]);

printf("\nnumber2[]:");

for(i = 0; i < MAX2; i++)

printf("%d ", number2[i]);

// 合并排序

mergesort(number1, MAX1, number2, MAX2, number3);

printf("\n合并后:");

for(i = 0; i < MAX1+MAX2; i++)

printf("%d ", number3[i]);

printf("\n");

return 0;

}

int partition(int number[], int left, int right) {

int i, j, s;

s = number[right];

i = left - 1;

for(j = left; j < right; j++) {

if(number[j] <= s) {

i++;

SWAP(number[i], number[j]);

}

}

SWAP(number[i+1], number[right]);

return i+1;

}

void quicksort(int number[], int left, int right) {

int q;

if(left < right) {

q = partition(number, left, right);

quicksort(number, left, q-1);

quicksort(number, q+1, right);

}

}

void mergesort(int number1[], int M, int number2[],

int N, int number3[]) {

int i = 0, j = 0, k = 0;

while(i < M && j < N) {

if(number1[i] <= number2[j])

number3[k++] = number1[i++];

else

number3[k++] = number2[j++];

}

while(i < M)

number3[k++] = number1[i++];

while(j < N)

number3[k++] = number2[j++];

}

 

  • Java

 

public class MergeSort {

public static int[] sort(int[] number1,

int[] number2) {

int[] number3 =

new int[number1.length + number2.length];

int i = 0, j = 0, k = 0;

while(i < number1.length && j < number2.length) {

if(number1[i] <= number2[j])

number3[k++] = number1[i++];

else

number3[k++] = number2[j++];

}

while(i < number1.length)

number3[k++] = number1[i++];

while(j < number2.length)

number3[k++] = number2[j++];

return number3;

}

}

八。基数排序

基数排序是根据组成关键字的各位值,用"分配"和"收集"的方法进行排序。例如,把扑克牌的排序看成由花色和面值两个数据项组成的主关键字排序。

  花色:梅花<方块<红心<黑桃

  面值:2<3<4<...<10<J<Q<K<A

  若要将一副扑克牌排成下列次序:

  梅花2,...,梅花A,方块2,...,方块A,红心2,...,红心A,黑桃2,...,黑桃A。

  有两种排序方法:

  一、先按花色分成四堆,把各堆收集起来;然后对每堆按面值由小到大排列,再按花色从小到大按堆收叠起来。----称为"最高位优先"(MSD)法。

  二、先按面值由小到大排列成13堆,然后从小到大收集起来;再按花色不同分成四堆,最后顺序收集起来。----称为"最低位优先"(LSD)法。

  [例] 设记录键值序列为{88,71,60,31,87,35,56,18},用基数排序(LSD)。如图所示:其中f[i]、e[i]为按位分配面值为i的队列的队头和队尾指针。

   #define D 3

   typedef struct

   { int key;

     float data;

     int link;

   } JD

 

key data link

int jspx(JD r[],int n)

{ /*链式存储表示的基数排序*/

   int i,j,k,t,p,rd,rg,f[10],e[10];

   /*p为r[]的下标,rd,rg为比例因子,f[j],e[j]是代码为j的队的首尾指针*/

   for(i=1;i<n;i++) r[i].link=i+1;

   r[n].link=0;

   p=1;rd=1;rg=10;

   for(i=1;i<=D;i++)

   { for(j=0;j<10;j++) { f[j]=0;e[j]=0; } /*各队列初始化*/

     do /*按位分配--分到各队列中*/

     { k=(r[p].key%rg)/rd; /*取键值的某一位*/

       if(f[k]==0) f[k]=p;

       else r[e[k]].link=p; /*有重复值--修改链接*/

       e[k]=p;

       p=r[p].link; /*取下一个结点的地址*/

     }while(p>0);

     j=0; /*按位收集--调整分配后的链接*/

     while(f[j]==0) j=j+1;

     p=f[j];t=e[j];

     for(k=j+1;k<10;k++)

       if(f[k]>0){ r[t].link=f[k];t=e[k]; }/*调整链接*/

     r[t].link=0; /*链尾为0*/

     rg=rg*10;rd=rd*10; /*提高一位*/

   }

   return(p); /*返回有序链表的首地址*/

九 枚举排序

将每个记录项与其他诸项比较计算出小于该项的记录个数,以确定该项的位置。


十 总结


    1. 选取排序方法需要考虑的因素:
    (1) 待排序的元素数目n;
    (2) 元素本身信息量的大小;
    (3) 关键字的结构及其分布情况;
    (4) 语言工具的条件,辅助空间的大小等。

2. 排序的选择:
   (1) 若n较小(n <= 50),则可以采用直接插入排序或直接选择排序。由于直接插入排序所需的 记录移动操作较直接选择排序多,因而当记录本身信息量较大时,用直接选择排序较好。
   (2) 若文件的初始状态已按关键字基本有序,则选用直接插入或冒泡排序为宜。
   (3) 若n较大,则应采用时间复杂度为O(nlog2n)的排序方法:快速排序、堆排序或归并排序。
快速排序是目前基于比较的内部排序法中被认为是最好的方法。
   (4) 在基于比较排序方法中,每次比较两个关键字的大小之后,仅仅出现两种可能的转移,因此可以用一棵二叉树来描述比较判定过程,由此可以证明:当文件的n个关键字随机分布时,任何借助于"比较"的排序算法,至少需要O(nlog2n)的时间。

   这句话很重要 它告诉我们自己写的算法 是有改进到最优 当然没有必要一直追求最优

(5) 当记录本身信息量较大时,为避免耗费大量时间移动记录,可以用链表作为存储结构。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章