排序算法

来源:互联网 发布:js水泥基防水 编辑:程序博客网 时间:2024/06/05 17:52

一:分类

排序大的分类可以分为两种:内排序和外排序。
在排序过程中,全部记录存放在内存,则称为内排序,如果排序过程中需要使用外存,则称为外排序。本章内的排序都是属于内排序。

内排序可以分为以下几类:
  (1)  插入排序:直接插入排序、二分法插入排序、希尔排序。
  (2)  选择排序:简单选择排序、堆排序。
       (3)  交换排序:冒泡排序、快速排序。
       (4)  归并排序
       (5)  基数排序

重点是考虑每个循环的边界和退出循环的条件

二:插入排序

  • 思想:
                    每次把要插入的数据,插入到前面已经排好序的序列中合适的位置,直到所有的数据都插入排序完为止
                    查找的是已经排好序的序列
                    既然是插入,则肯定要分别向后移动插入位置以后的所有元素以腾出要插入的位置。
  •  核心     在已经插入的有序序列中找到要插入的位置     

                    插入的一定是有序序列。(插入第n个数据,则前面n-1的数据一定是排好序的,每插入一个数据的同时就是给插入的序列排序,插入N完成后N之前的数据一定是有序序列)

                    已经插入完的数都是有序的,未插入的数都是无序的

                    注意每次插入时比较的不是全部的数据,而是已经插入到有序序列中的数据,所以每次循环比较的基数不一样。

  •   方法

                   思想都一样,就是在有序序列中查找插入元素的位置的方法不一样

                   分为:

                   直接插入排序 : 从后向前比较每个元素,每次比较的同时移动元素   O(n2)

                   二分插入排序:  先二分查找到插入的索引位置,再统一移动元素   O(n2)

                   希尔排序 :   分为多组,每组内进行直接插入排序,    O(nlogn)

1.  直接插入排序  (从后向前查找有序序列)

  •    基本思想

               从已插入的有序序列中的最后一个元素(最大的元素)开始从后往前查找每一个元素,和当前要插入的元素比较大小,找到合适的插入位置    
  •     实例
                 

     初始状态是待插入的数据数组
     第一次循环,插入68,有序序列中元素是57,比较68和57,后面的59,52不比较
     第二次循环,插入59,有序序列中元素是57,68,比较68和59,68后移一位到59的位置,然后比较57和59,59就保存在原68的位置
     第三次循环,插入52,有序序列中元素是57,59,68,比较68和52,68后移一位,比较59和52,59后移一位比较57和52,57后移一位,则52保存到原57的位置
  • JAVA算法
public class Sort {public static void main(String[] args) {int[] a = { 49, 38, 65, 97, 76, 13, 27, 49, 78, 34, 12, 64, 1 };// 外循环的执行次数取决于待插入元素数组的大小,要插入多少数据就需要循环比较多少次。for (int i = 1; i < a.length; i++) { //从第二个元素开始比较,一共比较i-1次。// 保存待插入的元素int temp = a[i];int j;// 内循环的执行次数取决于已经插入到有序数列中的元素,i是要插入的元素在原数组中的坐标,则从i-1位置的元素开始倒着查找一直到0,for (j = i - 1; j >= 0; j--) {// a[j]是有序序列中当前的元素,大于temp的话往后移动一位,i-1的元素如果大于插入的i,则移动到i的位置,if (a[j] > temp) {a[j + 1] = a[j]; } else {break; // 找到合适的插入位置了,结束循环}}a[j + 1] = temp; }}}


算法分析:

外循环:

        执行次数: N个元素的数组,需要插入从1到n-1的元素,次数固定

        结束条件: 需要把N-1的元素全部插入完成,自然结束循环

内循环:

        执行次数: 每次需要执行从n-1的位置到0,次数不固定

        结束条件:1是全部元素比较完毕自然结束循环  

                 a[0]>temp,然后j=-1.结束循环,则temp就是保存到a[0]的位置

                          2是提前找到了插入的位置

                 a[j]<temp则提前退出了循环,以前的j+1位置的元素肯定是大于temp的,已经向后移了一位,则j+1就是保存temp的位置


  • 时间复杂度分析

与待排序记录的初始状态有关

最优情况,如果待排序的元素为正序,则每插入的元素就只需要和i-1的元素比较一次就可以了。  内循环都是1次,外循环是n-1次,则时间复杂度为O(n)

最坏情况,如果待排序的元素为倒序,则每插入的元素就需要从i-1一直比较到0,内循环每次都要比较N次。则时间复杂度为O(n2)      

平均时间复杂度为O(n2)

2 二分法插入排序  (按二分法在有序序列中找到插入位置)

  • 基本思想

              在有序队列中查找插入位置的时候是按照二分法查找的,可以减少比较的次数

  • 实例     

       

第一次查找:中点是36,42>mid,更改low值

第二次查找,low=mid+1, 中点是low 53, 42<mid,更改high值

第三次查找,high=mid-1, 此时high<low,循环结束,插入位置是low或者high+1,需要把42插入到53的位置,所以53往后的元素都要往后移动一位

  • java算法

public static void main(String[] args) {       int[] a = { 49, 38, 65, 97, 76, 13, 27, 49, 78, 34, 12, 64, 1 };       // 外循环也是执行i-1次,需要插入i-1个元素。       for (int i=1; i< a.length; i++) {               // 待插入的元素               int temp = a[i];               // 初始化查找的区间               int low = 0;               int high = i-1;               int mid = 0;               // 循环结束条件,low>high时               while (low <= high) {               mid = (low + high)/2;               // 比较插入元素和中点元素的大小,更改查询区间               if (temp<a[mid]) {            high = mid -1;                             } else {            low = mid + 1;               }               }               // 循环结束的时候,只是找到了要插入的元素的位置是low               // 则low到i-1的所有元素都要往后移动一位               for (int j = i-1; j>=low;j--) {               a[j+1] = a[j];               }               // 插入元素到low               if (low !=i) {               a[low] = temp;               }    }}
  • 时间复杂度

与待排序记录的初始状态无关,仅依赖于记录的个数。

不一定比直接插入排序效率高,如果直接插入排序是正序的话每个数据就比较一次,而二分次数反而多

最坏的情况为n2/2,最好的情况为n,平均移动次数为n2.

外循环:

         和插入排序一样。N-1次

内循环:

        结束条件: low > high        

3: 希尔排序  

  •    基本思想
先取一个小于n的整数d1作为第一个增量,把n个全部记录分成d1个组。
所有距离为d1的倍数的记录放在同一个组中。先在各组内进行直接插入排序;
然后,取第二个增量d2<d1重复上述的分组和排序,直至所取的增量dt=1(dt<dt-l<…<d2<d1),即所有记录放在同一组中进行直接插入排序为止。
该方法实质上是一种分组插入方法。
  •    实例     


第一次分组,n/2,d1为5,共分给5组,1和6在一组,2和7在一组这样分别排序
第二次分组,d1/2 , d2=2.5取奇数为3, 共分为3组
第二次分组,d2/2, d3=1.5取奇数为1,都在一个组里排序
  •     java算法     
public static void main(String[] args) {int[] a = { 49, 38, 65, 97, 76, 13, 27, 49, 78, 34, 12, 64, 1 };int d = a.length;// d为1时退出循环while(d!=1){   // 每次分为d组   d = d / 2;   // 外循环就是分为的d组,0-d-1的元素分别就是这d组中的每一组的第一个元素,因为每隔d长度的元素就放到同一个组里   for(int x=0;x<d;x++){   // 每次循环找到属于当前分组中的所有元素,从x+d开始,每隔d长度的数据   for(int i=x+d;i<a.length;i=i+d){   // 每个分组中要插入的数据   int temp = a[i];   int j;   // 每个分组中要插入的数据和这个分组中之前已经排好序的数据进行直接插入排序,找到这个分组中最后一个元素,就是i-d,倒着查找   for(j=i-d;j>=0&&a[j]>temp;j=j-d){   // 往后移动d位,就是分组中移动一位   a[j+d] = a[j];   }   a[j+d] = temp;   }   }}}

最内层的循环和直接插入排序的算法思想是一样的
取出每个分组中的每个要排序的元素进行直接插入排序

外循环:
      取d的值,进行几次分组,退出条件是d=1的时候
第二层循环:
      每次的d分组后
      循环遍历d个分组,需要都遍历完自然退出
第三层循环:
      在每个分组的循环中找到要插入的该分组的元素
第四层循环:
      对要插入的元素在该分组中进行直接插入排序,循环比较该分组中有序的数据

  • 时间复杂度

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

  (1)当文件初态基本有序时直接插入排序所需的比较和移动次数均较少。
  (2)当n值较小时,n和n2的差别也较小,即直接插入排序的最好时间复杂度O(n)和最坏时间复杂度0(n2)差别不大。
  (3)在希尔排序开始时增量较大,分组较多,每组的记录数目少,故各组内直接插入较快,后来增量di逐渐缩小,分组数逐渐减少,而各组的记录数目逐渐增多,但由于已经按di-1作为距离排过序,使文件较接近于有序状态,所以新的一趟排序过程也较快。
  因此,希尔排序在效率上较直接插人排序有较大的改进。
  希尔排序的平均时间复杂度为O(nlogn)

三:  选择排序

  • 思想
                  每次从待排序的记录序列中选择关键字最小的记录放置到已排序表的最前位置,直到全部排完。
                  查找的是待排序的序列
  • 关键
                  在剩余的待排序的序列中找到最小关键字的记录。
                  每次循环只交换2个数,当前位置的数和剩余待排序序列中最小的数,其他数的位置不变。
                  选择排序可以理解为按照位置选择数据

    1. 直接选择排序  O(n2)

  •  思想      
             在要排序的一组数中,选择出最小的一个数和第一个位置交换,然后再剩下的数中选择最小的数和第二个位置交换,
             直到循环到最后一个位置为止。

             
             第一个位置选择所有数里面最小的一个数交换
             第二个位置再剩余的待排序的队列中再选择最小的数相交换
             直到最后一个位置的数选择完毕。

  •  实例
             
         第一次循环,所有数里最小的数为52,和第一个位置的数57交换,其他元素位置不变
         第二次循环,剩余数中最小的数为57,和第二个位置的数68交换
     
  • java算法    
public class Hello {public static void main(String[] args) {int[] a = { 49, 38, 65, 97, 76, 13, 27, 49, 78, 34, 12, 64, 1 };// 从0开始查找,因为是按照位置选元素,第0个位置的元素可能会变化for (int i = 0; i< a.length; i++) {// 给内循环的变量赋初始值,一个是记录最小的值,一个是记录最小值的索引int min = a[i];int n=i;// 第i个位置选择元素每次从i+1的位置开始查找,找到最小的值for(int j=i+1;j<a.length;j++){if(a[j]<min){min = a[j];n = j;}}//把当前位置的元素和最小值进行交换a[n] = a[i];a[i] = min;}}


四:交换排序

1.   冒泡排序

  •   思想
        对待排序的序列中的数据,比较相邻的2个数,让大数往下沉,小数往上冒,如果不符合排序要求,就元素互换
        注意: 每次冒泡比较的2个相邻的数都是待排序列中的数,已经冒泡完的数据不包括在内。不是全部的数据
        和选择排序不同的是:
        选择排序也是比较相邻的2个元素的大小,但只是查找记录元素应该在的位置,然后只进行一次元素互换,就是当前元素和应该所在位置元素的互换
        但是冒泡排序每次比较的同时就进行了元素的互换,有多次
  •    实例
             
       向上冒泡排序算法例子
       从后向前比较相邻的2个元素
       第一次冒泡,比较52和59,52也59交互,然后52和68比较,交换,然后52和57比较交换,最终最小的数52上升到第一个元素的位置。
       第二次冒泡,52除外,在剩下的元素里重复上面的操作
  •    Java算法
     1.  向下冒泡,每次待排序列中的最大数下沉  
public static void main(String[] args) {int[] a = { 49, 38, 65, 97, 76, 13, 27, 49, 78, 34, 12, 64, 1 };// 最大数向下沉,第一次排序后最大数在最后一位,所以正向比较for (int i = 0; i < a.length; i++) {// 第i次排序后,则后i个数已经是有序的了,不需要再进行比较,比较的是未排序的数据for (int j = 0; j < a.length - i - 1; j++) {// 核心算法,比较相邻的2个数的大小交换if (a[j] > a[j + 1]) {int temp = a[j];a[j] = a[j + 1];a[j + 1] = temp;}}}}<span style="font-family: Arial, Helvetica, sans-serif; background-color: rgb(255, 255, 255);">           </span>
     2.  向上冒泡,每次待排序列中的最小数上冒   
public static void main(String[] args) {int[] a = { 49, 38, 65, 97, 1, 7, 45, 23, 4 };// 最小数向上冒,第一次排序后最小数在第一位,所以要逆向比较for (int i = a.length - 1; i >= 0; i--) {// 第i次循环后,前面的数是不需要再比较了,注意这个边界for (int j = a.length - 1; j >= a.length - i; j--) {if (a[j] < a[j - 1]) {int temp = a[j];a[j] = a[j - 1];a[j - 1] = temp;}}}}

算法核心:
          外循环: 总共要排序多少数据就有多少次循环,元素的总个数
          内循环: 还剩多少待排序的数据就有多少次循环,注意循环的起始和结束条件
          核心:     相邻2个元素比较互换
  • 时间复杂度
     O(n2)

2. 快速排序       

  • 思想
            选择一个基准元素,通常选择第一个元素或者最后一个元素,通过一趟扫描,将待排序列分为2部分,一部分比基准元素小,一部分比基准元素大,此时的基准元素在最终的有序序列中的正确位置,然后用同样的方式递归地排序划分只来的2部分
           
            每次的扫描中,也是2个元素的比较然后交换元素,所以也是交换排序,只是比较的这2个元素的选择方法不一样。
            冒泡排序是比较2个相邻的元素,每次比较的2个元素都不一样。但是快速排序比较的2个元素中始终有一个元素就基准元素。

  • 实例


这是一趟扫描的过程描述。
取第一个元素57为基准元素,其他的元素都分别和基准元素比较,最后57左边的序列都是小于57的元素,右边的序列都是大于57的元素

始终有一个指针指向的是57基准元素,其他所有的元素和57分别进行比较,关键就是怎么选择出来和基准元素进行比较的元素
第一行   57为基准元素, 和最后一个元素19比较,19小于57,交换位置
第二行   19已经比较过了,从19的下一位68开始比较,68大于57,交换位置
第三行,68已经比较过了,从68的前面一位24和57比较。后面类似。
  • Java算法
      关键是写出一次扫描的算法描述
      然后递归的调用函数,更改函数的参数。

private static void quickSort(int[]a, int low, int high) {// 递归结束的条件,否则会堆栈溢出if (low < high) {// 一次排序的过程,返回这个区间上基准元素也就是第一个元素最终在序列中的位置    int middle = getMiddle(a,low,high);                    quickSort(a, 0, middle-1);                    quickSort(a, middle+1, high);        }} private static int getMiddle(int[] a, int low, int high) {    // 取排序区间的第一个位置的元素为基准元素        int temp = a[low];        while(low<high){            //基准元素是第一个元素,先从最后一个元素high进行比较,如果high的元素大于基准元素,则不发生位置交换,high减1再比较倒数第2个元素            while(low<high && a[high]>=temp){                high--;            }            // 如果high的元素小于基准元素,则位置互换,高位的元素放到当前基准元素的位置            a[low] = a[high];             // 位置发生变化后,就应该从low位开始往上查找了,如果小于基准元素,则只移动指针,否则元素发生交换            while(low<high && a[low]<=temp){                low++;            }            a[high] = a[low];        }        // 循环结束后,low的位置就是基准元素的位置        a[low] = temp;        return low;}
     
  • 时间复杂度

       快速排序是不稳定的排序。

  快速排序的时间复杂度为O(nlogn)。

  当n较大时使用快排比较好,当序列基本有序时用快排反而不好。

五: 归并排序

  •  思想
       先分组再合并   (重点是怎么分组怎么合并)

       把待排序列先分为若干的子序列,给每个子序列分别排序,然后再把两个或者以上的有序的子序列合并为整体有序的序列。

  •   实例
       
  •    java算法 
        private static void quick(int[] a) {if (a.length > 0) {mergeSort(a,0,a.length-1);}}private static void mergeSort(int[] a, int low, int high) {// 递归结束的条件,否则会堆栈溢出if (low < high) {    int middle = (low+high)/2;    mergeSort(a, low, middle);    mergeSort(a, middle+1, high);    // 类似于合并2个有序链表的算法,合并的时候,左右2个区间的数据已经各自有序了    merge(a,low,middle,high);}}private static void merge(int[] a, int low, int middle, int high) {// 申请临时空间保存2个区间合并后的元素int[] tmpArr = new int[a.length];// 右边区间的启始位置int rightstart = middle+1; int tmp = low;// 临时空间当前元素的启始位置int third = low;// 比较2个区间中每个元素的大小放入临时空间,直到有一个区间元素结束while(low<=middle && rightstart<=high){     if(a[low]<=a[rightstart]){     // 如果左边区间的元素小于右边,则把左边的元素保存到临时空间,保存空间和左边区间的指针加1         tmpArr[third] = a[low];         third++;         low++;     }else{     tmpArr[third++] = a[rightstart++];    }    }// 循环结束后,肯定是有一个区间的元素都比较完了。如果左边还有元素则把剩下的元素直接保存到临时空间,因为这些元素都已经是有序的了while(low<=middle){       tmpArr[third++] = a[low++];}// 右边空间还有剩余元素的话while(rightstart<=high){      tmpArr[third++] = a[rightstart++];    }// 把合并后的临时空间的元素复制回原数组while(tmp<=high){     a[tmp] = tmpArr[tmp++];}}

     mergeSort这个函数相当于递归把全序列的元素不断的找到中点,一分为二,进行分组
    直到不满足条件,退出递归以后,然后调用merge反向进行合并。每合并的2个区间的数据已经各自有序了

  •   时间复杂度   

       归并排序是稳定的排序方法。

  归并排序的时间复杂度为O(nlogn)。

  速度仅次于快速排序,为稳定排序算法,一般用于对总体无序,但是各子项相对有序的数列。

      
0 0
原创粉丝点击