十大经典排序

参考文章:http://blog.csdn.net/amazing7/article/details/51603682

一.冒泡排序

冒泡排序通过重复地走访过要排序的数列,一次比较两个元素,如果他们的顺序错误就把他们交换过来,直到没有再需要交换的元素为止(对n个项目需要O(n^2)的比较次数)。这个算法的名字由来是因为越小的元素会经由交换慢慢“浮”到数列的顶端。

实现步骤

  1. 比较相邻的元素。如果第一个比第二个大,就交换他们两个。 

  2. 对每一对相邻元素做同样的工作,从开始第一对到结尾的最后一对。这步做完后,最后的元素会是最大的数

  3. 针对所有的元素重复以上的步骤,除了最后一个。

  4. 持续每次对越来越少的元素重复上面的步骤,直到没有任何一对数字需要比较。 

这里写图片描述

代码:
 void bubble_sort(vector<int>&nums)
    {
        int len=nums.size();
        for(int i=0;i<len;i++)
        {
            for(int j=0;j<len-i-1;j++)
            {
                if(nums[j]>nums[j+1])swap(nums[j],nums[j+1]);
                
            }
        }
    }



实现性能

  • 最差时间复杂度

O(n^2)

  • 最优时间复杂度

O(n) 

  • 平均时间复杂度

O(n^2)

  • 最差空间复杂度

总共O(n),需要辅助空间O(1)

稳定性分析:

冒泡排序就是把小的元素往前调或者把大的元素往后调。比较是相邻的两个元素比较,交换也发生在这两个元素之间。所以,如果两个元素相等,我想你是不会再无聊地把他们俩交换一下的;如果两个相等的元素没有相邻,那么即使通过前面的两两交换把两个相邻起来,这时候也不会交换,所以相同元素的前后顺序并没有改变,所以冒泡排序是一种稳定排序算法。

二.选择排序

常用的选择排序方法有简单选择排序和堆排序,这里只说简单选择排序,堆排序后面再说。

简单选择排序

  设所排序序列的记录个数为n,i 取 1,2,…,n-1 。 
  从所有n-i+1个记录(Ri,Ri+1,…,Rn)中找出排序码最小(或最大)的记录,与第i个记录交换。执行n-1趟 后就完成了记录序列的排序。

以排序数组{3,2,1,4,6,5}为例

这里写图片描述

这里写图片描述

代码:

   void select_sort(vector<int>&nums)
    {
        int len=nums.size();
        for(int i=0;i<len;i++)
       {
           int temp=i;
          for(int j=i;j<len;j++)
         {
           if(nums[temp]>nums[j])temp=j;
         }
           swap(nums[i],nums[temp]);
       }
    }

简单选择排序性能

  在简单选择排序过程中,所需移动记录的次数比较少。最好情况下,即待排序记录初始状态就已经是正序排列了,则不需要移动记录。  
  最坏情况下,即待排序记录初始状态是按第一条记录最大,之后的记录从小到大顺序排列,则需要移动记录的次数最多为3(n-1)。

  简单选择排序过程中需要进行的比较次数与初始状态下待排序的记录序列的排列情况无关。 
  当i=1时,需进行n-1次比较;当i=2时,需进行n-2次比较;依次类推,共需要进行的比较次数是(n-1)+(n-2)+…+2+1=n(n-1)/2,即进行比较操作的时间复杂度为O(n^2),进行移动操作的时间复杂度为O(n)。 

  

选择排序是给每个位置选择当前元素最小的,比如给第一个位置选择最小的,在剩余元素里面给第二个元素选择第二小的,依次类推,直到第n - 1个元素,第n个元素不用选择了,因为只剩下它一个最大的元素了。那么,在一趟选择,如果当前元素比一个元素大,而该小的元素又出现在一个和当前元素相等的元素后面,那么交换后稳定性就被破坏了。比较拗口,举个例子,序列5 8 5 2 9,我们知道第一遍选择第1个元素5会和2交换,那么原序列中2个5的相对前后顺序就被破坏了,所以选择排序不是一个稳定的排序算法。


三.插入排序

概述

  将一个数据插入到已经排好序的有序数据中,从而得到一个新的、个数加一的有序数据,算法适用于少量数据的排序,是稳定的排序方法。

  插入排序又分为 直接插入排序 和 折半插入排序。

直接插入排序

  把待排序的纪录按其关键码值的大小逐个插入到一个已经排好序的有序序列中,直到所有的纪录插入完为止,得到一个新的有序序列。

代码:

void insert_sort(vector<int>&nums)
    {
        int len=nums.size();
        for(int i=1;i<len;i++)
        {
            int temp=nums[i];
            int j=i;
            while(j>=1&&temp<nums[j-1])
            {
                nums[j]=nums[j-1];
                j--;
            }
            nums[j]=temp;
        }
    }

效率分析

  空间复杂度O(1)   
   
  平均时间复杂度O(n^2)

最差情况:反序,需要移动n*(n-1)/2个元素 ,运行时间为O(n^2)。 
   
最好情况:正序,不需要移动元素,运行时间为O(n).

稳定性分许:

插入排序是在一个已经有序的小序列的基础上,一次插入一个元素。当然,刚开始这个有序的小序列只有1个元素,就是第一个元素。比较是从有序序列的末尾开始,也就是想要插入的元素和已经有序的最大者开始比起,如果比它大则直接插入在其后面,否则一直往前找直到找到它该插入的位置。如果碰见一个和插入元素相等的,那么插入元素把想插入的元素放在相等元素的后面。所以,相等元素的前后顺序没有改变,从原无序序列出去的顺序就是排好序后的顺序,所以插入排序是稳定的

折半插入排序 

  直接插入排序中要把插入元素与已有序序列元素依次进行比较,效率非常低。  
   
  折半插入排序,使用使用折半查找的方式寻找插入点的位置, 可以减少比较的次数,但移动的次数不变, 时间复杂度和空间复杂度和直接插入排序一样,在元素较多的情况下能提高查找性能。

代码:

 void insert_sort(vector<int>&nums)
    {
        int len=nums.size();
        for(int i=1;i<len;i++)
        {
            int temp=nums[i];
            int pre=0;
            int end=i-1;
            int mid;
            while(pre<=end)
            {
                 mid=(pre+end)/2;
                if(nums[mid]>temp)end=mid-1;
                else if(nums[mid]<=temp)pre=mid+1;
                
            }
            
            for(int j=i;j>=pre+1;j--)
            nums[j]=nums[j-1];
            nums[pre]=temp;
           
        }
    }

四 .希尔排序

概述

  希尔排序法(缩小增量法) 属于插入类排序,是将整个无序列分割成若干小的子序列分别进行插入排序的方法。

  把记录按下标的一定增量分组,对每组使用直接插入排序算法排序;随着增量逐渐减少,每组包含的关键词越来越多,当增量减至1时,整个文件恰被分成一组,算法便终止。

希尔排序是基于插入排序的以下两点性质而提出改进方法的:

  • 插入排序在对几乎已经排好序的数据操作时,效率高,即可以达到线性排序的效率。

  • 但插入排序一般来说是低效的,因为插入排序每次只能将数据移动一位。

实现过程

  先取一个正整数d1小于n,把所有序号相隔d1的数组元素放一组,组内进行直接插入排序;然后取d2小于d1,重复上述分组和排序操作;直至di=1,即所有记录放进一个组中排序为止。

  例如,假设有这样一组数[ 13 14 94 33 82 25 59 94 65 23 45 27 73 25 39 10 ],如果我们以步长为5开始进行排序,我们可以通过将这列表放在有5列的表中来更好地描述算法,这样他们就应该看起来是这样:

13 14 94 33 82 
25 59 94 65 23 
45 27 73 25 39 
10

然后我们对每列进行排序:

10 14 73 25 23 
13 27 94 33 39 
25 59 94 65 82 
45

  将上述四行数字,依序接在一起时我们得到:[ 10 14 73 25 23 13 27 94 33 39 25 59 94 65 82 45 ].这时10已经移至正确位置了,然后再以3为步长进行排序:

10 14 73 
25 23 13 
27 94 33 
39 25 59 
94 65 82 
45

排序之后变为:

10 14 13 
25 23 33 
27 25 59 
39 65 73 
45 94 82 
94

最后以1步长进行排序(此时就是简单的插入

 void shell_sort(vector<int>&nums)
    {
        int len=nums.size();
        int gap=1;
        while(gap<len/3)
        {
            gap=gap*3+1;
        }
        for(;gap>0;gap=gap/3)
        {
            for(int i=gap;i<len;i++)
            {
            int j;
            int temp=nums[i];
            for( j=i-gap;j>=0&&temp<nums[j];j-=gap)
            {nums[j+gap]=nums[j];}
            nums[j+gap]=temp;
            }
        }
    }

排序了)。

实现效率

  希尔排序是一个不稳定的排序,其时间复杂度受步长(增量)的影响。

  空间复杂度: O(1)

  时间复杂度: 平均 O(n^1.3) 
         最好 O(n) 
         最坏 O(n^2)

稳定性:希尔排序是按照不同步长对元素进行插入排序,当刚开始元素很无序的时候,步长最大,所以插入排序的元素个数很少,速度很快;当元素基本有序了,步长很小, 插入排序对于有序的序列效率很高。所以,希尔排序的时间复杂度会比O(n^2)好一些。由于多次插入排序,我们知道一次插入排序是稳定的,不会改变相同元素的相对顺序,但在不同的插入排序过程中,相同的元素可能在各自的插入排序中移动,最后其稳定性就会被打乱,所以shell排序是不稳定的。

五 .归并排序

归并排序,是创建在归并操作上的一种有效的排序算法该算法是采用分治法(Divide and Conquer)的一个非常典型的应用,且各层分治递归可以同时进行。

  即先使每个子序列有序,再将两个已经排序的序列合并成一个序列的操作。若将两个有序表合并成一个有序表,称为二路归并。

例如:

设有数列{6,202,100,301,38,8,1} 
初始状态:6,202,100,301,38,8,1 
第一次归并后:{6,202},{100,301},{8,38},{1},比较次数:3;跨过前面的元素数目:1

第二次归并后:{6,100,202,301},{1,8,38},比较次数:4; 跨过前面的元素数目:3
第三次归并后:{1,6,8,38,100,202,301},比较次数:4; 跨过前面的元素数目:10
总的比较次数为:3+4+4=11,; 跨过前面的元素数目:14
逆序数为14;

这里写图片描述

 归并排序示意图

1.迭代实现:

实现原理:

①申请空间,使其大小为两个已经排序序列之和,该空间用来存放合并后的序列

②设定两个指针,最初位置分别为两个已经排序序列的起始位置

③比较两个指针所指向的元素,选择相对小的元素放入到合并空间,并移动指针到下一位置 

④重复步骤③直到某一指针到达序列尾 

⑤将另一序列剩下的所有元素直接复制到合并序列尾

代码:
void merge_sort(vector<int>&nums)
    {
        int len=nums.size();
        int result[len];
        int i;
        for(int blocks=1;blocks<len;blocks*=2)
        {
            i=0;
        for(int start=0;start<len;start+=2*blocks)
        {
            int start1=start;
            int end1=(start+blocks)>len?len:start+blocks;
            int start2=end1;
            int end2=(start2+blocks)>len?len:start2+blocks;
            while(start1<end1&&start2<end2)
            {
                if(nums[start1]<nums[start2])
                {
                    result[i++]=nums[start1];
                    start1++;
                }
                 else if(nums[start1]>=nums[start2])
                {
                    result[i++]=nums[start2];
                    start2++;
                }
            }
            while(start1<end1)
            {
                result[i++]=nums[start1++];
            }
            while(start2<end2)
            {
                result[i++]=nums[start2++];
            }
            
                
        }
            for(int i=0;i<len;i++)
            nums[i]=result[i];
        }
           
        
    }
2.递归实现

假设序列共有n个元素

①将序列每相邻两个数字进行归并操作,形成floor(n/2)个序列,排序后每个序列包含两个元素。

②将上述序列再次归并,形成floor(n/4)个序列,每个序列包含四个元素

③重复步骤②,直到所有元素排序完毕

代码:

void merge_sort(vector<int>&nums,int*result,int start,int end)
    {
        if(start>=end)return;
        int len=end-start;
        int start1=start;
        int end1=start+len/2;
        int start2=end1+1;;
        int end2=end;
        merge_sort(nums,result,start1,end1);
        merge_sort(nums,result,start2,end2);
        int i=start;
        while(start1<end1+1&&start2<end2+1)
            {
                if(nums[start1]<=nums[start2])
                {
                    result[i++]=nums[start1];
                    start1++;
                }
                 else if(nums[start1]>nums[start2])
                {
                    result[i++]=nums[start2];
                    start2++;
                }
            }
            while(start1<end1+1)
            {
                result[i++]=nums[start1++];
            }
            while(start2<end2+1)
            {
                result[i++]=nums[start2++];
            }
            for(int i=start;i<=end;i++)
            nums[i]=result[i];
            
        
    }

效率

  归并排序速度仅次于快速排序,为稳定排序算法(即相等的元素的顺序不会改变),一般用于对总体无序,但是各子项相对有序的数列. 

  时间复杂度为O(nlogn)  T(n)=2T(n/2)+O(n); 
   
  空间复杂度为 O(n) 

归并排序比较占用内存,但却是一种效率高且稳定的算法。

稳定性分析:

归并排序是把序列递归地分成短序列,递归出口是短序列只有1个元素(认为直接有序)或者2个序列(1次比较和交换),然后把各个有序的段序列合并成一个有序的长序列,不断合并直到原序列全部排好序。可以发现,在1个或2个元素时,1个元素不会交换,2个元素如果大小相等也没有人故意交换,这不会破坏稳定性。那么,在短的有序序列合并的过程中,稳定是是否受到破坏?没有,合并过程中我们可以保证如果两个当前元素相等时,我们把处在前面的序列的元素保存在结果序列的前面,这样就保证了稳定性。所以,归并排序也是稳定的排序算法。

六  .快速排序     

快速排序(Quicksort)是对冒泡排序的一种改进,又称划分交换排序(partition-exchange sort。

  快速排序使用分治法(Divide and conquer)策略来把一个序列(list)分为两个子序列(sub-lists)。

步骤为:

①.从数列中挑出一个元素,称为”基准”(pivot)

②.重新排序数列,所有元素比基准值小的摆放在基准前面,所有元素比基准值大的摆在基准的后面(相同的数可以到任一边)。在这个分区结束之后,该基准就处于数列的中间位置。这个称为分区(partition)操作。

③.递归地(recursive)把小于基准值元素的子数列和大于基准值元素的子数列排序

这里写图片描述 

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

分析:

这里写图片描述

取8为中值,红色箭头表示low,绿色箭头表示high 

①从high开始向前扫描到第一个比8小的值与8交换。

②从low向后扫描第一比8大的值与8交换。

③重复①②过程只到,high=low完成一次快速排序,然后递归子序列。

代码:
 void quick_sort(vector<int>&nums,int start,int end)
    {
       if(start<end)
       {
           int mid=getmid(nums,start,end);
           quick_sort(nums,mid+1,end);
           quick_sort(nums,start,mid-1);
       }
    }
    int getmid(vector<int>&nums,int start,int end)
    {
        int mid=nums[start];
        while(start<end)
        {
            while(start<end&&nums[end]>=mid)
            end--;
            if(start<end)
            nums[start]=nums[end];
            while(start<end&&nums[start]<=mid)
            start++;
            if(start<end)
            nums[end]=nums[start];
            
        }
        nums[start]=mid;
        return start;
    }
排序效率

  在平均状况下,排序n个项目要Ο(n log n)次比较。在最坏状况下则需要Ο(n2)次比较,但这种状况并不常见(有序的情况需要o(n ^2))。事实上,快速排序通常明显比其他Ο(n log n)算法更快,因为它的内部循环(inner loop)可以在大部分的架构上很有效率地被实现出来。

最差时间复杂度 Ο(n^2) 

最优时间复杂度 Ο(n log n) 

平均时间复杂度Ο(n log n) 

最差空间复杂度 根据实现的方式不同而不同

稳定性:
快速排序有两个方向,左边的i下标一直往右走,当a[i] <= a[center_index],其中center_index是中枢元素的数组下标,一般取为数组第0个元素。而右边的j下标一直往左走,当a[j] > a[center_index]。如果i和j都走不动了,i <= j,交换a[i]和a[j],重复上面的过程,直到i > j。 交换a[j]和a[center_index],完成一趟快速排序。在中枢元素和a[j]交换的时候,很有可能把前面的元素的稳定性打乱,比如序列为5 3 3 4 3 8 9 10 11,现在中枢元素5和3(第5个元素,下标从1开始计)交换就会把元素3的稳定性打乱,所以快速排序是一个不稳定的排序算法,不稳定发生在中枢元素和a[j] 交换的时刻。

七   .堆排序

堆排序(Heapsort)是指利用堆这种数据结构所设计的一种排序算法,它是选择排序的一种。可以利用数组的特点快速定位指定索引的元素。堆分为大根堆和小根堆,是完全二叉树。大根堆的要求是每个节点的值都不大于其父节点的值。

  由于堆中每次都只能删除第0个数据,通过 取出第0个数据再执行堆的删除操作、重建堆(实际的操作是将最后一个数据的值赋给根结点,然后再从根结点开始进行一次从上向下的调整。),然后再取,如此重复实现排序。

堆的操作:

这里写图片描述

在堆的数据结构中,堆中的最大值总是位于根节点。堆中定义以下几种操作:

  • 最大堆调整(Max_Heapify):将堆的末端子节点作调整,使得子节点永远小于父节点

  • 创建最大堆(Build_Max_Heap):将堆所有数据重新排序

  • 堆排序(HeapSort):移除位在第一个数据的根节点,并做最大堆调整的递归运算

堆的存储:

这里写图片描述

通常堆是通过一维数组来实现的。在数组起始位置为0的情形中:

  • 父节点i的左子节点在位置(2*i+1); 

  • 父节点i的右子节点在位置(2*i+2);

  • 子节点i的父节点在位置floor((i-1)/2); 


代码:
void maxheapify(vector<int>&nums,int i,int len)
   {
       int temp=nums[i];
       int j=2*i+1;
       while(j<len)
       {
           if(j+1<len&&nums[j+1]>nums[j])
           j++;
           if(nums[j]>temp)
           {nums[i]=nums[j];
           i=j;
           j=2*i+1;
           
           }
           else break;
       }
       nums[i]=temp;
   }
   void build_maxheap(vector<int>&nums)
   {
       int len=nums.size();
       for(int i=len/2-1;i>=0;i--)
       {
           maxheapify(nums,i,len);
       }
   }
   void maxheap_sort(vector<int>&nums)
   {
       build_maxheap(nums);
       int len=nums.size();
       for(int i=len-1;i>0;i--)
       {
           swap(nums[0],nums[i]);
           
           maxheapify(nums,0,i);
          
       }
   }
复杂度分析:建堆时间复杂度为O(n),排序时间复杂度为O(nlgn),空间复杂度为O(1)
稳定性分析:
我们知道堆的结构是节点i的孩子为2 * i和2 * i + 1节点,大顶堆要求父节点大于等于其2个子节点,小顶堆要求父节点小于等于其2个子节点。在一个长为n 的序列,堆排序的过程是从第n / 2开始和其子节点共3个值选择最大(大顶堆)或者最小(小顶堆),这3个元素之间的选择当然不会破坏稳定性。但当为n / 2 - 1, n / 2 - 2, ... 1这些个父节点选择元素时,就会破坏稳定性。有可能第n / 2个父节点交换把后面一个元素交换过去了,而第n / 2 - 1个父节点把后面一个相同的元素没 有交换,那么这2个相同的元素之间的稳定性就被破坏了。所以,堆排序不是稳定的排序算法。


八.桶排序

1.概念

桶排序(Bucket sort)或所谓的箱排序,是一个排序算法。

  假设有一组长度为N的待排关键字序列K[1….n]。首先将这个序列划分成M个的子区间(桶) 。然后基于某种映射函数 ,将待排序列的关键字k映射到第i个桶中(即桶数组B的下标 i) ,那么该关键字k就作为B[i]中的元素。接着对每个桶B[i]中的所有元素进行比较排序(可以使用快排)。然后依次枚举输出B[0]….B[M]中的全部内容即是一个有序序列。

桶排序的步骤:

①设置一个定量的数组当作空桶子。

②寻访序列,并且把项目一个一个放到对应的桶子去。

③对每个不是空的桶子进行排序。

④从不是空的桶子里把项目再放回原来的序列中。

2.性能

数据结构  数组  
最差时间复杂度   O(n^2) 
平均时间复杂度  O(n+k)  
最差空间复杂度 O(n*k)

  平均情况下桶排序以线性时间运行,桶排序是稳定的,排序非常快,但是同时也非常耗空间,基本上是最耗空间的一种排序算法。

  对N个关键字进行桶排序的时间复杂度分为两个部分:

①循环计算每个关键字的桶映射函数,这个时间复杂度是O(N)。

②利用先进的比较排序算法对每个桶内的所有数据进行排序,其时间复杂度为 ∑ O(Ni*logNi) 。其中Ni 为第i个桶的数据量。

  很显然,第②部分是桶排序性能好坏的决定因素。尽量减少桶内数据的数量是提高效率的唯一办法(因为基于比较排序的最好平均时间复杂度只能达到O(N*logN)了)。因此,我们需要尽量做到下面两点:  

① 映射函数f(k)能够将N个数据平均的分配到M个桶中,这样每个桶就有[N/M]个数据量。  
   
②尽量的增大桶的数量。极限情况下每个桶只能得到一个数据,这样就完全避开了桶内数据的“比较”排序操作。 当然,做到这一点很不容易,数据量巨大的情况下,f(k)函数会使得桶集合的数量巨大,空间浪费严重。这就是一个时间代价和空间代价的权衡问题了。

3.java实现

这里写图片描述

  对0~1之间的一组浮点数进行升序排序:

BucketSort.Java

public class BucketSort  {
    /** 
     * 对arr进行桶排序,排序结果仍放在arr中 
     */  
    public static void bucketSort(double arr[]){  
  //-------------------------------------------------分桶-----------------------------------------------      
        int n = arr.length;  
        //存放桶的链表
        ArrayList bucketList[] = new ArrayList [n];  
        //每个桶是一个list,存放此桶的元素   
        for(int i =0;i<n;i++){  
            //下取等
            int temp = (int) Math.floor(n*arr[i]);  
            //若不存在该桶,就新建一个桶并加入到桶链表中
            if(null==bucketList[temp])  
                bucketList[temp] = new ArrayList();  
            //把当前元素加入到对应桶中
            bucketList[temp].add(arr[i]);            
        }  
//-------------------------------------------------桶内排序-----------------------------------------------    
        //对每个桶中的数进行插入排序   
        for(int i = 0;i<n;i++){  
            if(null!=bucketList[i])  
                insert(bucketList[i]);  
        }  
//-------------------------------------------------合并桶内数据-----------------------------------------------    
        //把各个桶的排序结果合并   
        int count = 0; 
        for(int i = 0;i<n;i++){  
            if(null!=bucketList[i]){  
                Iterator iter = bucketList[i].iterator();  
                while(iter.hasNext()){  
                    Double d = (Double)iter.next();  
                    arr[count] = d;  
                    count++;  
                }  
            }  
        }  
    }  

    /** 
     * 用插入排序对每个桶进行排序 
     * 从小到大排序
     */  
    public static void insert(ArrayList list){  

        if(list.size()>1){  
            for(int i =1;i<list.size();i++){  
                if((Double)list.get(i)<(Double)list.get(i-1)){  
                    double temp = (Double) list.get(i);  
                    int j = i-1;  
                    for(;j>=0&&((Double)list.get(j)>(Double)list.get(j+1));j--)  
                        list.set(j+1, list.get(j));  //后移
                    list.set(j+1, temp);  
                }  
            }  
        } 
    }
}
  
  
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59

测试代码:

public static void main(String[] args) {
        double arr [] ={0.21,0.23,0.76,0.12,0.89};
        BucketSort.bucketSort(arr);
        for(double a:arr){
            System.out.println(a);
        }
    }
  
  
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

输出结果:

这里写图片描述 
  

九.基数排序

原理

  基数排序(Radix sort)是一种非比较型整数排序算法,其原理是将整数按位数切割成不同的数字,然后按每个位数分别比较。由于整数也可以表达字符串(比如名字或日期)和特定格式的浮点数,所以基数排序也不是只能使用于整数。

  将所有待比较数值(正整数)统一为同样的数位长度,数位较短的数前面补零。然后,从最低位开始,依次进行一次排序。这样从最低位排序一直到最高位排序完成以后,数列就变成一个有序序列。

效率

  基数排序的时间复杂度是O(k·n),其中n是排序元素个数,k是数字位数。注意这不是说这个时间复杂度一定优于O(n·log(n)),k的大小取决于数字位的选择和待排序数据所属数据类型的全集的大小;k决定了进行多少轮处理,而n是每轮处理的操作数目。

  基数排序基本操作的代价较小,k一般不大于logn,所以基数排序一般要快过基于比较的排序,比如快速排序。

  最差空间复杂度是O(k·n)

Java实现

  这里写图片描述 

  现在有数组:278,109,63,930,589,184,505,269,8,83 。根据各位数将数组划分为10个链表(当然其中的某些链表可能不含有元素) 
   
第一次分配:

0:930 
1: 
2: 
3:63,83 
4:184 
5:505 
6: 
7: 
8:278,8 
9:109,589,269

第一次收集后的数组:

930,63,83,184,505,278,8,109,589,269

第二次分配:

0:505,8,109 
1: 
2: 
3:930 
4: 
5: 
6:63,269 
7:278 
8:83,184,589 
9:

第二次收集后的数组:

505,8,109,930,63,269,278,83,184,589

第三次分配:

0:8,63,83 
1:109,184 
2:278,269 
3: 
4: 
5:505,589 
6: 
7: 
8: 
9:930

最后得到序列:

8,63,83,109,184,269,278,505,589,930

基数排序其实是利用多关键字先达到局部有序,再调整达到全局有序。

java代码实现:

public class Test {
    public static void main(String[] args) {

        int[] array = {278,109,63,930,589,184,505,269,8,83};  
        radixSort(array);  
        for(double a : array){
            System.out.println(a);
        }
    }
public static void radixSort(int[] array){

        //------------------------------------------确定排序的趟数----------------------------------
        int max=array[0];
        for(int i=1;i<array.length;i++){
            if(array[i]>max){
                max=array[i];
            }
        }
        int time=0;
        while(max>0){
            max/=10;
            time++;
        }
        //----------------------------------------初始化10个链表用户分配时暂存-------------------------------
        List<List<Integer>> list=new ArrayList<List<Integer>>();
        for(int i=0;i<10;i++){
            List<Integer> item=new ArrayList<Integer>();
            list.add(item);
        }

        //-----------------------------------------进行time次分配和收集-------------------------------------
      for(int i=0;i<time;i++){
            //分配元素;
            for(int j=0;j<array.length;j++){
                int index = array[j]%(int)Math.pow(10, i+1)/(int)Math.pow(10, i);
                list.get(index).add(array[j]);
            }
            //收集元素;
            int count=0;
            for(int k=0;k<10;k++){
                if(list.get(k).size()>0){
                    for(int a : list.get(k)){
                        array[count]=a;
                        count++;
                    }
                    //清除数据,以便下次收集
                    list.get(k).clear();
                }
            }
        }
    }
}
  
  
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52

运行结果:

这里写图片描述

稳定性分析:
基数排序是按照低位先排序,然后收集;再按照高位排序,然后再收集;依次类推,直到最高位。有时候有些属性是有优先级顺序的,先按低优先级排序,再按高优先级排序,最后的次序就是高优先级高的在前,高优先级相同的低优先级高的在前。基数排序基于分别排序,分别收集,所以其是稳定的排序算法。



十.计数排序

计数排序是一种算法复杂度 O(n) 的排序方法,适合于小范围集合的排序。比如100万学生参加高考,我们想对这100万学生的数学成绩(假设分数为0到100)做个排序。我们如何设计一个最高效的排序算法。本文不光给出计数排序算法的传统写法,还将一步步深入讨论算法的优化,直到时间复杂度和空间复杂度最优。

先看看计数排序的定义

Counting sort (sometimes referred to as ultra sort or math sort[1]) is a sorting algorithm which (like bucket sort) takes advantage of knowing the range of the numbers in the array to be sorted (array A). It uses this range to create an array C of this length. Each index i in array C is then used to count how many elements in A have the value i; then counts stored in C can then be used to put the elements in A into their right position in the resulting sorted array. The algorithm was created by Harold H. Seward in 1954.

计数排序是一个类似于桶排序的排序算法,其优势是对已知数量范围的数组进行排序。它创建一个长度为这个数据范围的数组C,C中每个元素记录要排序数组中对应记录的出现个数。这个算法于1954年由 Harold H. Seward 提出。

下面以示例来说明这个算法

假设要排序的数组为 A = {1,0,3,1,0,1,1}

这里最大值为3,最小值为0,那么我们创建一个数组C,长度为4.

然后一趟扫描数组A,得到A中各个元素的总数,并保持到数组C的对应单元中。

比如0 的出现次数为2次,则 C[0] = 2;1 的出现次数为4次,则C[1] = 4

 

image

由于C 是以A的元素为下标的,所以这样一做,A中的元素在C中自然就成为有序的了,这里我们可以知道 顺序为 0,1,3 (2 的计数为0)

然后我们把这个在C中的记录按每个元素的计数展开到输出数组B中,排序就完成了。

也就是 B[0] 到 B[1] 为0  B[2] 到 B[5] 为1 这样依此类推。

这种排序算法,依靠一个辅助数组来实现,不基于比较,算法复杂度为 O(n) ,但由于要一个辅助数组C,所以空间复杂度要大一些,由于计算机的内存有限,这种算法不适合范围很大的数的排序。

注:基于比较的排序算法的最佳平均时间复杂度为 O(nlogn)

 

Counting sort
Depends on a key assumption: numbers to be sorted are integers in{0, 1, . . . , k}.
Input: A[1 . . n], where A[ j ] ∈ {0, 1, . . . , k} for j = 1, 2, . . . , n. Array A and
values n and k are given as parameters.
Output: B[1 . . n], sorted. B is assumed to be already allocated and is given as a
parameter.
Auxiliary storage: C[0 . . k]
8-4 Lecture Notes for Chapter 8: Sorting in Linear Time
COUNTING-SORT(A, B, n, k)
for i ← 0 to k
do C[i ] ← 0
for j ← 1 to n
do C[A[ j ]] ← C[A[ j ]] + 1
for i ← 1 to k
do C[i ] ← C[i ] + C[i − 1]
for j ← n downto 1
do B[C[A[ j ]]] ← A[ j ]
C[A[ j ]] ← C[A[ j ]] − 1
Do an example for A = 21, 51, 31, 01, 22, 32, 02, 33
Counting sort is stable (keys with same value appear in same order in output as
they did in input) because of how the last loop works.

上面这段引自麻省理工大学计算机算法教材的技术排序部分,我不做翻译了。这个就是这个算法的典型解法,我把它作为方案1.

这个算法的实际扫描次数为 n+k (不包括写的次数)

方案1
 public static void Sort(int[] A, out int[] B, int k)
        {
            Debug.Assert(k > 0);
            Debug.Assert(A != null);
 
            int[] C = new int[k + 1];
            B = new int[A.Length];
 
            for (int j = 0; j < A.Length; j++)
            {
                C[A[j]]++;
            }
 
            for (int i = 1; i <= k; i++)
            {
                C[i] += C[i-1];
            }
 
            for (int j = A.Length - 1; j >= 0; j--)
            {
                B[C[A[j]]-1] = A[j];
                C[A[j]]--;
            }
 
        }


上面代码是方案1 的解法,也是计数排序算法的经典解法,麻省的教材上也是这样解。不过这个解法并不是最优的,因为空间复杂度还应该可以优化,我们完全可以不要那个输出的数组B,直接对A进行排序。在继续看方案2之前,我建议大家先自己思考一下,看看是否有办法省略掉数组B

 

方案2

我们对上述代码进行优化

   public static void Sort(int[] A, int k)
        {
            Debug.Assert(k > 0);
            Debug.Assert(A != null);

            int[] C = new int[k + 1];

            for (int j = 0; j < A.Length; j++)
            {
                C[A[j]]++;
            }

            int z = 0;

            for (int i = 0; i <= k; i++)
            {
                while (C[i]-- > 0)
                {
                    A[z++] = i;
                }
            }
        }

由于C数组下标 i 就是A 的值,所以我们不需要保留A中原来的数了,这个代码减少了一个数组B,而且要比原来的代码简化了很多。

十一.补充:


    

1. 快排的partition函数

    int partion(vector<int>&nums,int start,int end)
    {
        
        int temp=nums[start];
        int i=start;
        
        while(start<end)
        {
           
           
             while(start<end&&nums[end]>=temp)
            end--;
             while(start<end&&nums[start]<=temp)
            start++;
            
            if(start<end)
            swap(nums[start],nums[end]);
        }
        swap(nums[start],nums[i]);
        return start;
        
    }

2.

冒泡排序的改进

思路:

①、加一个标志位,当某一趟冒泡排序没有元素交换时,则冒泡结束,元素已经有序,可以有效的减少冒泡次数。

   void bubble_sort(vector<int>&nums)
    {
        bool flag=false;;
        for(int i=0;i<nums.size();i++)
        {
            flag=false;
           for(int j=0;j<nums.size()-i-1;j++)
         {
             if(nums[j]>nums[j+1])
             {
                 swap(nums[j],nums[j+1]);
                 flag=true;
             }
         } 
          if(!flag)break;
        }
         
    }
②、记录每一次元素交换的位置,当元素交换的位置在第0个元素时,则排序结束。
void bubble_sort(vector<int>&nums)
    {
        int index=0;
        for(int i=0;i<nums.size();i++)
        {
           
           for(int j=0;j<nums.size()-i-1;j++)
         {
                            
             if(nums[j]>nums[j+1])
             {
                 swap(nums[j],nums[j+1]);
                 index=j;
             }
         } 
         if(index==0)break;
          
        }
         
    }




3.快排优化

① 快速排序在处理小规模数据时的表现不好,这个时候可以改用插入排序。

②对于一个每个元素都完全相同的一个序列来讲,快速排序也会退化到 O(n^2)。要将这种情况避免到,可以这样做:

  在分区的时候,将序列分为 3 堆,一堆小于中轴元素,一堆等于中轴元素,一堆大于中轴元素,下次递归调用快速排序的时候,只需对小于和大于中轴元素的两堆数据进行排序,中间等于中轴元素的一堆已经放好。


  • 9
    点赞
  • 40
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值